Bug 1631902 - Add event and task preview to ICS file import dialog. r=darktrojan
authorPaul Morris <paul@thunderbird.net>
Sat, 30 May 2020 11:00:01 +0300
changeset 39281 37b8335f94dc8ffef42d8278819270a5ea916bb9
parent 39280 7b9c7c1b0f4c798f899f92ff0bc4bd2a546c9488
child 39282 b7dc8d39df5624c9812ea5a22cff3b15b61d69c4
push id402
push userclokep@gmail.com
push dateMon, 29 Jun 2020 20:48:04 +0000
reviewersdarktrojan
bugs1631902
Bug 1631902 - Add event and task preview to ICS file import dialog. r=darktrojan Also allow events and tasks to be imported individually in addition to being able to import the whole file all at once.
calendar/base/content/dialogs/calendar-ics-file-dialog.css
calendar/base/content/dialogs/calendar-ics-file-dialog.js
calendar/base/content/dialogs/calendar-ics-file-dialog.xhtml
calendar/base/content/import-export.js
calendar/base/content/widgets/calendar-item-summary.js
calendar/base/themes/common/dialogs/calendar-event-dialog.css
calendar/lightning/content/lightning-item-iframe.xhtml
calendar/locales/en-US/calendar/calendar-ics-file-dialog.ftl
--- a/calendar/base/content/dialogs/calendar-ics-file-dialog.css
+++ b/calendar/base/content/dialogs/calendar-ics-file-dialog.css
@@ -1,7 +1,48 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
-:root {
-  min-width: 29em;
+/* We want the calendar items container to span the full width of the dialog
+   window, so there's no space between its scrollbar and the right side of the
+   window. Set the dialog element's padding to 0 and then set the equivalent
+   padding on its children. The dialog buttons area is in the shadow dom, so we
+   add padding to it via JS in the window load handler function. */
+#calendar-ics-file-dialog {
+  padding-inline: 0;
+}
+
+#calendar-ics-file-dialog-header,
+#calendar-ics-file-dialog-items-container {
+  /* This padding needs to change elsewhere if it changes here.
+     See the note above the styles for #calendar-ics-file-dialog */
+  padding-inline: 10px;
+}
+
+#calendar-ics-file-dialog-calendar-menu-label {
+  margin-top: 1em;
 }
+
+#calendar-ics-file-dialog-items-container {
+  border-block: 1px solid #ccc;
+  margin: 0.7em 0;
+  padding-block: 0 1.5em;
+  min-height: 200px;
+  overflow-x: hidden;
+  overflow-y: auto;
+}
+
+.calendar-ics-file-dialog-item-frame {
+  background-color: #fff;
+  border: 1px solid #ccc;
+  margin-block: 1.5em 0;
+  margin-inline: 6px 5px;
+  padding: 0.7em;
+}
+
+.calendar-ics-file-dialog-item-import-button {
+  margin-block: 0.7em 0;
+}
+
+.calendar-caption {
+  display: none;
+}
--- a/calendar/base/content/dialogs/calendar-ics-file-dialog.js
+++ b/calendar/base/content/dialogs/calendar-ics-file-dialog.js
@@ -1,74 +1,217 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
-/* globals loadEventsFromFile */
+/* globals addMenuItem, getItemsFromFile, putItemsIntoCal, removeChildren,
+           sortCalendarArray */
+
+var { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm");
+
+const gModel = {
+  /** @type {calICalendar[]} */
+  calendars: [],
+
+  /** @type {calIItemBase[]} */
+  itemsToImport: [],
+
+  /** @type {nsIFile | null} */
+  file: null,
+};
 
-/* exported onLoad */
+/**
+ * Window load event handler.
+ */
+async function onWindowLoad() {
+  // Workaround to add padding to the dialog buttons area which is in shadow dom.
+  // If the padding value changes here it should also change in the CSS.
+  let dialog = document.getElementsByTagName("dialog")[0];
+  dialog.shadowRoot.querySelector(".dialog-button-box").style = "padding-inline: 10px;";
+
+  gModel.file = window.arguments[0];
+  document.getElementById("calendar-ics-file-dialog-file-path").value = gModel.file.path;
+
+  gModel.itemsToImport = getItemsFromFile(gModel.file);
+  if (!gModel.itemsToImport.length) {
+    // No items to import, close the window. An error dialog has already been
+    // shown by `getItemsFromFile`.
+    window.close();
+    return;
+  }
 
-async function onLoad() {
-  let file = window.arguments[0];
-  let fileName = file.leafName;
+  gModel.calendars = getCalendarsThatCanImport(cal.getCalendarManager().getCalendars());
+  if (!gModel.calendars.length) {
+    // No calendars to import into. Show error dialog and close the window.
+    cal.showError(await document.l10n.formatValue("calendar-ics-file-dialog-no-calendars"), window);
+    window.close();
+    return;
+  }
+
+  setUpCalendarMenu(gModel.calendars);
+  setUpItemSummaries(gModel.itemsToImport, gModel.file.path);
+
+  document.addEventListener("dialogaccept", importRemainingItems);
+  window.sizeToContent();
+}
+window.addEventListener("load", onWindowLoad);
 
-  // Add the main dialog message, with file name. Use l10n.formatValue so we
-  // can await it and then resize the window to fit its content.
-  let message = await document.l10n.formatValue("calendar-ics-file-dialog-message", { fileName });
+/**
+ * Takes an array of calendars and returns a sorted array of the calendars
+ * that can import items.
+ *
+ * @param {calICalendar[]} calendars - An array of calendars.
+ * @return {calICalendar[]} Sorted array of calendars that can import items.
+ */
+function getCalendarsThatCanImport(calendars) {
+  let calendarsThatCanImport = calendars.filter(
+    calendar =>
+      calendar &&
+      cal.acl.isCalendarWritable(calendar) &&
+      cal.acl.userCanAddItemsToCalendar(calendar)
+  );
+  return sortCalendarArray(calendarsThatCanImport);
+}
 
-  document.getElementById("calendar-ics-file-dialog-message").value = message;
-  window.sizeToContent();
+/**
+ * Add calendars to the calendar drop down menu.
+ *
+ * @param {calICalendar[]} calendars - An array of calendars.
+ */
+function setUpCalendarMenu(calendars) {
+  let menulist = document.getElementById("calendar-ics-file-dialog-calendar-menu");
+  for (let calendar of calendars) {
+    addMenuItem(menulist, calendar.name, calendar.name);
+  }
+  menulist.selectedIndex = 0;
 }
 
 /**
- * "Import" button click handler.
+ * Display summaries of each calendar item from the file being imported.
+ *
+ * @param {calIItemBase[]} items - An array of calendar events and tasks.
+ * @param {string} filePath - The path to the file being imported.
  */
-async function handleImportClick(event) {
+function setUpItemSummaries(items, filePath) {
+  let itemsContainer = document.getElementById("calendar-ics-file-dialog-items-container");
+
+  items.forEach(async (item, index) => {
+    let itemFrame = document.createXULElement("vbox");
+    itemFrame.classList.add("calendar-ics-file-dialog-item-frame");
+
+    let importButton = document.createXULElement("button");
+    importButton.classList.add("calendar-ics-file-dialog-item-import-button");
+
+    let buttonTextIdentifier = cal.item.isEvent(item)
+      ? "calendar-ics-file-dialog-import-event-button-label"
+      : "calendar-ics-file-dialog-import-task-button-label";
+
+    let buttonText = await document.l10n.formatValue(buttonTextIdentifier);
+    importButton.setAttribute("label", buttonText);
+
+    importButton.addEventListener("command", importSingleItem.bind(null, item, index, filePath));
+
+    let buttonBox = document.createXULElement("hbox");
+    buttonBox.setAttribute("pack", "end");
+    buttonBox.setAttribute("align", "end");
+
+    let summary = document.createXULElement("calendar-item-summary");
+    summary.setAttribute("id", "import-item-summary-" + index);
+
+    itemFrame.appendChild(summary);
+    buttonBox.appendChild(importButton);
+    itemFrame.appendChild(buttonBox);
+
+    itemsContainer.appendChild(itemFrame);
+    summary.item = item;
+
+    summary.updateItemDetails();
+  });
+}
+
+/**
+ * Get the currently selected calendar.
+ *
+ * @return {calICalendar} The currently selected calendar.
+ */
+function getCurrentlySelectedCalendar() {
+  let menulist = document.getElementById("calendar-ics-file-dialog-calendar-menu");
+  let calendar = gModel.calendars[menulist.selectedIndex];
+  return calendar;
+}
+
+/**
+ * Handler for buttons that import a single item. The arguments are bound for
+ * each button instance, except for the event argument.
+ *
+ * @param {calIItemBase} item - Calendar item.
+ * @param {number} itemIndex - Index of the calendar item in the item array.
+ * @param {string} filePath - Path to the file being imported.
+ * @param {Event} event - The button event.
+ */
+async function importSingleItem(item, itemIndex, filePath, event) {
+  event.target.closest(".calendar-ics-file-dialog-item-frame").remove();
+  delete gModel.itemsToImport[itemIndex];
+
+  let calendar = getCurrentlySelectedCalendar();
+
+  putItemsIntoCal(calendar, [item], filePath);
+
+  if (!gModel.itemsToImport.some(item => item)) {
+    // No more items to import, remove the "Import All" option.
+    document.removeEventListener("dialogaccept", importRemainingItems);
+
+    let dialog = document.getElementsByTagName("dialog")[0];
+    dialog.getButton("cancel").hidden = true;
+    dialog.getButton("accept").label = await document.l10n.formatValue(
+      "calendar-ics-file-accept-button-ok-label"
+    );
+  }
+}
+
+/**
+ * "Import All" button command handler.
+ *
+ * @param {Event} event - Button command event.
+ */
+async function importRemainingItems(event) {
   event.preventDefault();
 
   let dialog = document.getElementsByTagName("dialog")[0];
   let acceptButton = dialog.getButton("accept");
   let cancelButton = dialog.getButton("cancel");
 
   acceptButton.disabled = true;
-  cancelButton.disabled = true;
+  cancelButton.hidden = true;
 
-  let file = window.arguments[0];
+  document.getElementById("calendar-ics-file-dialog-file-path").hidden = true;
+  document.getElementById("calendar-ics-file-dialog-items-container").hidden = true;
+  document.getElementById("calendar-ics-file-dialog-calendar-menu-label").hidden = true;
+  document.getElementById("calendar-ics-file-dialog-calendar-menu").hidden = true;
+
+  document.removeEventListener("dialogaccept", importRemainingItems);
+
+  let calendar = getCurrentlySelectedCalendar();
+  let remainingItems = gModel.itemsToImport.filter(item => item);
 
   let [importResult] = await Promise.allSettled([
-    loadEventsFromFile(file),
+    putItemsIntoCal(calendar, remainingItems, gModel.file.path),
     new Promise(resolve => setTimeout(resolve, 500)),
   ]);
 
-  acceptButton.disabled = false;
-  cancelButton.disabled = false;
-
-  if (importResult.status === "fulfilled" && importResult.value === false) {
-    // Do nothing, user probably canceled out of the calendar picker dialog.
-    return;
-  } else if (importResult.status === "rejected") {
-    // An error occurred, change the text and resize the window to fit its new content.
-    let errorMessage = await document.l10n.formatValue("calendar-ics-file-import-error");
+  let messageIdentifier = importResult.value
+    ? "calendar-ics-file-import-success"
+    : "calendar-ics-file-import-error";
 
-    document.getElementById("calendar-ics-file-dialog-message").value = errorMessage;
-    document.getElementById("calendar-ics-file-dialog-error-message").value = importResult.reason;
+  let messageElement = document.getElementById("calendar-ics-file-dialog-message");
+  messageElement.value = await document.l10n.formatValue(messageIdentifier);
 
-    window.sizeToContent();
-  } else {
-    // Import succeeded.
-    let successMessage = await document.l10n.formatValue("calendar-ics-file-import-success");
-    document.getElementById("calendar-ics-file-dialog-message").value = successMessage;
-  }
-
-  cancelButton.hidden = true;
   acceptButton.label = await document.l10n.formatValue("calendar-ics-file-accept-button-ok-label");
-
-  document.removeEventListener("dialogaccept", handleImportClick);
+  acceptButton.disabled = false;
 }
 
-document.addEventListener("dialogaccept", handleImportClick);
-
 /**
- * These functions are called via `loadEventsFromFile` in import-export.js so
- * they need to be defined in global scope but they aren't needed in this case.
+ * These functions are called via `putItemsIntoCal` in import-export.js so
+ * they need to be defined in global scope but they don't need to do anything
+ * in this case.
  */
 function startBatchTransaction() {}
 function endBatchTransaction() {}
--- a/calendar/base/content/dialogs/calendar-ics-file-dialog.xhtml
+++ b/calendar/base/content/dialogs/calendar-ics-file-dialog.xhtml
@@ -1,30 +1,49 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <!-- This Source Code Form is subject to the terms of the Mozilla Public
    - License, v. 2.0. If a copy of the MPL was not distributed with this
    - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
 
 <?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+<?xml-stylesheet type="text/css" href="chrome://calendar/skin/shared/calendar-alarms.css"?>
+<?xml-stylesheet type="text/css" href="chrome://calendar/skin/shared/calendar-attendees.css"?>
+<?xml-stylesheet type="text/css" href="chrome://messenger/skin/input-fields.css"?>
+<?xml-stylesheet type="text/css" href="chrome://calendar/skin/calendar-event-dialog.css"?>
+<?xml-stylesheet type="text/css" href="chrome://calendar/skin/shared/dialogs/calendar-event-dialog.css"?>
+<?xml-stylesheet type="text/css" href="chrome://calendar/content/calendar-summary-dialog.css"?>
 <?xml-stylesheet href="chrome://calendar/content/calendar-ics-file-dialog.css" type="text/css"?>
 
 <window windowtype="Calendar:ICSFile"
         xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
         xmlns:html="http://www.w3.org/1999/xhtml"
-        onload="onLoad()"
-        data-l10n-id="calendar-ics-file-window">
+        data-l10n-id="calendar-ics-file-window-2"
+        height="600">
 
-  <dialog buttons="accept,cancel"
-          data-l10n-id="calendar-ics-file-dialog"
+  <dialog id="calendar-ics-file-dialog"
+          buttons="accept,cancel"
+          data-l10n-id="calendar-ics-file-dialog-2"
           data-l10n-attrs="buttonlabelaccept">
 
     <html:link rel="localization" href="calendar/calendar-ics-file-dialog.ftl"/>
 
     <script src="chrome://calendar/content/calendar-ics-file-dialog.js"></script>
     <script src="chrome://calendar/content/import-export.js"></script>
+    <script src="chrome://calendar/content/widgets/calendar-item-summary.js"></script>
+    <script src="chrome://calendar/content/calendar-dialog-utils.js"></script>
+    <script src="chrome://calendar/content/calendar-ui-utils.js"></script>
 
-    <vbox>
-      <description id="calendar-ics-file-dialog-message"></description>
-      <description id="calendar-ics-file-dialog-error-message"></description>
+    <vbox id="calendar-ics-file-dialog-header">
+      <description id="calendar-ics-file-dialog-message"
+                   data-l10n-id="calendar-ics-file-dialog-message-2"></description>
+      <description id="calendar-ics-file-dialog-file-path"></description>
+
+      <label id="calendar-ics-file-dialog-calendar-menu-label"
+             data-l10n-id="calendar-ics-file-dialog-calendar-menu-label"
+             control="calendar-ics-file-dialog-calendar-menu"></label>
+
+      <menulist id="calendar-ics-file-dialog-calendar-menu"/>
     </vbox>
 
+    <vbox id="calendar-ics-file-dialog-items-container" flex="1"></vbox>
+
   </dialog>
 </window>
--- a/calendar/base/content/import-export.js
+++ b/calendar/base/content/import-export.js
@@ -180,19 +180,20 @@ function getCalendarToImportInto() {
 
   return targetCalendar;
 }
 
 /**
  * Put items into a certain calendar, catching errors and showing them to the
  * user.
  *
- * @param destCal       The destination calendar.
- * @param aItems        An array of items to put into the calendar.
- * @param aFilePath     The original file path, for error messages.
+ * @param {calICalendar} destCal    The destination calendar.
+ * @param {calIItemBase[]} aItems   An array of items to put into the calendar.
+ * @param {string} aFilePath        The original file path, for error messages.
+ * @return {Promise<boolean>}       True for successful import, false for errors.
  */
 async function putItemsIntoCal(destCal, aItems, aFilePath) {
   // Set batch for the undo/redo transaction manager
   startBatchTransaction();
 
   // And set batch mode on the calendar, to tell the views to not
   // redraw until all items are imported
   destCal.startBatch();
@@ -204,53 +205,58 @@ async function putItemsIntoCal(destCal, 
   let failedCount = 0;
   let duplicateCount = 0;
   // Used to store the last error. Only the last error, because we don't
   // want to bomb the user with thousands of error messages in case
   // something went really wrong.
   // (example of something very wrong: importing the same file twice.
   //  quite easy to trigger, so we really should do this)
   let lastError;
+  let didImportSucceed = true;
 
   let pcal = cal.async.promisifyCalendar(destCal);
   for (let item of aItems) {
     // XXX prompt when finding a duplicate.
     try {
       await pcal.addItem(item);
       count++;
       // See if it is time to end the calendar's batch.
       if (count == aItems.length) {
         destCal.endBatch();
         if (failedCount) {
           cal.showError(
             cal.l10n.getCalString("importItemsFailed", [failedCount, lastError.toString()]),
             window
           );
+          didImportSucceed = false;
         } else if (duplicateCount) {
           cal.showError(
             cal.l10n.getCalString("duplicateError", [duplicateCount, aFilePath]),
             window
           );
+          didImportSucceed = false;
         }
       }
     } catch (e) {
       count++;
       if (e == Ci.calIErrors.DUPLICATE_ID) {
         duplicateCount++;
       } else {
         failedCount++;
         lastError = e;
       }
 
       Cu.reportError("Import error: " + e);
+      didImportSucceed = false;
     }
   }
 
   // End transmgr batch
   endBatchTransaction();
+  return didImportSucceed;
 }
 
 /**
  * Save data to a file. Create the file or overwrite an existing file.
  *
  * @param calendarEventArray (required) Array of calendar events that should
  *                                      be saved to file.
  * @param aDefaultFileName   (optional) Initial filename shown in SaveAs dialog.
--- a/calendar/base/content/widgets/calendar-item-summary.js
+++ b/calendar/base/content/widgets/calendar-item-summary.js
@@ -52,17 +52,17 @@
                     <html:th>
                       &read.only.title.label;
                     </html:th>
                     <html:td>
                       <html:input class="item-title selectable-label plain input-inline"
                                   readonly="readonly"/>
                     </html:td>
                   </html:tr>
-                  <html:tr>
+                  <html:tr class="calendar-row" hidden="hidden">
                     <html:th>
                       &read.only.calendar.label;
                     </html:th>
                     <html:td>
                       <html:input class="item-calendar selectable-label plain input-inline"
                                   readonly="readonly"/>
                     </html:td>
                   </html:tr>
@@ -352,26 +352,30 @@
       });
       urlLink.addEventListener("command", event => {
         launchBrowser(urlLink.getAttribute("href"), event);
       });
     }
 
     set item(item) {
       this.mItem = item;
-      this.mCalendar = cal.wrapInstance(item.calendar, Ci.calISchedulingSupport);
       this.mIsToDoItem = cal.item.isToDo(item);
 
-      this.mReadOnly = !(
-        cal.acl.isCalendarWritable(this.mCalendar) &&
-        (cal.acl.userCanModifyItem(item) ||
-          (this.mCalendar &&
-            this.mCalendar.isInvitation(item) &&
-            cal.acl.userCanRespondToInvitation(item)))
-      );
+      // When used in places like the import dialog, there is no calendar (yet).
+      if (item.calendar) {
+        this.mCalendar = cal.wrapInstance(item.calendar, Ci.calISchedulingSupport);
+
+        this.mReadOnly = !(
+          cal.acl.isCalendarWritable(this.mCalendar) &&
+          (cal.acl.userCanModifyItem(item) ||
+            (this.mCalendar &&
+              this.mCalendar.isInvitation(item) &&
+              cal.acl.userCanRespondToInvitation(item)))
+        );
+      }
 
       return item;
     }
 
     get item() {
       return this.mItem;
     }
 
@@ -391,17 +395,21 @@
       if (!this.item) {
         // Setup not complete, do nothing for now.
         return;
       }
       let item = this.item;
       let isToDoItem = this.mIsToDoItem;
 
       this.querySelector(".item-title").value = item.title;
-      this.querySelector(".item-calendar").value = this.calendar.name;
+
+      if (this.calendar) {
+        this.querySelector(".calendar-row").removeAttribute("hidden");
+        this.querySelector(".item-calendar").value = this.calendar.name;
+      }
 
       // Show start date.
       let itemStartDate = item[cal.dtz.startDateProp(item)];
 
       let itemStartRowLabel = this.querySelector(".item-start-row-label");
       let itemDateRowStartDate = this.querySelector(".item-date-row-start-date");
 
       itemStartRowLabel.style.visibility = itemStartDate ? "visible" : "collapse";
@@ -514,18 +522,18 @@
     }
     /**
      * Updates the dialog w.r.t recurrence, i.e shows a text describing the item's
      * recurrence.
      *
      * @param {string} details - Recurrence details as a string.
      */
     updateRecurrenceDetails(details) {
-      let repeatRow = document.querySelector(".repeat-row");
-      let repeatDetails = document.querySelector(".repeat-details");
+      let repeatRow = this.querySelector(".repeat-row");
+      let repeatDetails = repeatRow.querySelector(".repeat-details");
 
       if (!details) {
         repeatRow.setAttribute("hidden", "true");
         repeatDetails.setAttribute("collapsed", "true");
 
         while (repeatDetails.children.length) {
           repeatDetails.lastChild.remove();
         }
--- a/calendar/base/themes/common/dialogs/calendar-event-dialog.css
+++ b/calendar/base/themes/common/dialogs/calendar-event-dialog.css
@@ -809,54 +809,62 @@ calendar-event-freebusy-day > box {
   margin-top: -8px;
   margin-inline-start: -8px;
   margin-inline-end: -10px;
   margin-bottom: 10px;
 }
 
 #calendar-summary-dialog,
 #calendar-event-summary-dialog,
-#calendar-task-summary-dialog {
+#calendar-task-summary-dialog,
+#calendar-ics-file-dialog {
   min-width: 35em;
 }
 
 #calendar-summary-dialog .item-attachment-cell,
 #calendar-event-summary-dialog .item-attachment-cell,
-#calendar-task-summary-dialog .item-attachment-cell {
+#calendar-task-summary-dialog .item-attachment-cell,
+#calendar-ics-file-dialog .item-attachment-cell {
   margin-left: 6px;
 }
 
 #calendar-summary-dialog .item-attachment-cell-label,
 #calendar-event-summary-dialog .item-attachment-cell-label,
-#calendar-task-summary-dialog .item-attachment-cell-label {
+#calendar-task-summary-dialog .item-attachment-cell-label,
+#calendar-ics-file-dialog .item-attachment-cell-label {
   margin-left: 3px;
 }
 
 #calendar-summary-dialog .item-description-wrapper,
 #calendar-event-summary-dialog .item-description-wrapper,
-#calendar-task-summary-dialog .item-description-wrapper {
+#calendar-task-summary-dialog .item-description-wrapper,
+#calendar-ics-file-dialog .item-description-wrapper {
   display: flex;
 }
 
 #calendar-summary-dialog .item-description,
 #calendar-event-summary-dialog .item-description,
-#calendar-task-summary-dialog .item-description {
+#calendar-task-summary-dialog .item-description,
+#calendar-ics-file-dialog .item-description {
   width: 100%;
   box-sizing: border-box;
   min-height: 54px;
   margin: 2px 4px 0;
 }
 
 #calendar-summary-dialog .selectable-label,
 #calendar-event-summary-dialog .selectable-label,
-#calendar-task-summary-dialog .selectable-label {
+#calendar-task-summary-dialog .selectable-label,
+#calendar-ics-file-dialog .selectable-label {
   background-color: inherit;
   color: inherit;
 }
 
 #calendar-summary-dialog #item-start-row .headline,
 #calendar-event-summary-dialog #item-start-row .headline,
 #calendar-task-summary-dialog #item-start-row .headline,
+#calendar-ics-file-dialog #item-start-row .headline,
 #calendar-summary-dialog #item-end-row .headline,
 #calendar-event-summary-dialog #item-end-row .headline,
-#calendar-task-summary-dialog #item-end-row .headline {
+#calendar-task-summary-dialog #item-end-row .headline,
+#calendar-ics-file-dialog #item-end-row .headline {
   font-weight: normal;
 }
--- a/calendar/lightning/content/lightning-item-iframe.xhtml
+++ b/calendar/lightning/content/lightning-item-iframe.xhtml
@@ -127,16 +127,17 @@
     </vbox>
   </hbox>
 
   <hbox id="event-dialog-notifications">
     <!-- notificationbox will be added here lazily. -->
   </hbox>
 
   <html:table id="event-grid">
+    <!-- Calendar -->
     <html:tr>
       <html:th>
         <label id="item-calendar-label"
                value="&event.calendar.label;"
                accesskey="&event.calendar.accesskey;"
                control="item-calendar"
                disable-on-readonly="true"/>
         <label id="item-calendar-aux-label"
@@ -179,17 +180,17 @@
       </html:th>
       <html:td class="event-input-td">
         <html:input id="item-location"
                     disable-on-readonly="true"
                     aria-labelledby="item-location-label"/>
       </html:td>
     </html:tr>
 
-    <!-- Category & Calendar -->
+    <!-- Category -->
     <html:tr id="event-grid-category-color-row">
       <html:th class="above-separator">
         <hbox id="event-grid-category-labels-box">
           <label id="item-categories-label"
                  value="&event.categories.label;"
                  accesskey="&event.categories.accesskey;"
                  control="item-categories"
                  disable-on-readonly="true"/>
--- a/calendar/locales/en-US/calendar/calendar-ics-file-dialog.ftl
+++ b/calendar/locales/en-US/calendar/calendar-ics-file-dialog.ftl
@@ -1,16 +1,21 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
-calendar-ics-file-window =
-  .title = Import Calendar Events
+calendar-ics-file-window-2 =
+  .title = Import Calendar Events and Tasks
 
-calendar-ics-file-dialog =
-  .buttonlabelaccept = Import
+calendar-ics-file-dialog-import-event-button-label = Import Event
+calendar-ics-file-dialog-import-task-button-label = Import Task
+
+calendar-ics-file-dialog-2 =
+  .buttonlabelaccept = Import All
 
 calendar-ics-file-accept-button-ok-label = OK
 
-# $fileName (string) - The name of the file.
-calendar-ics-file-dialog-message = Do you want to import the file "{ $fileName }"?
+calendar-ics-file-dialog-message-2 = Import from file:
+calendar-ics-file-dialog-calendar-menu-label = Import into calendar:
 calendar-ics-file-import-success = Successfully imported!
 calendar-ics-file-import-error = An error occurred and the import failed.
+
+calendar-ics-file-dialog-no-calendars = There are no calendars that can import events or tasks.