Bug 1683063 - In import dialog, allow filtering of items to import. r=mkmelin

Differential Revision: https://phabricator.services.mozilla.com/D100547

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Geoff Lankow 2021-03-15 22:18:30 +00:00
Родитель e7c25ffdd1
Коммит b9b87a8da9
4 изменённых файлов: 170 добавлений и 22 удалений

Просмотреть файл

@ -68,6 +68,11 @@ async function onWindowLoad() {
await new Promise(resolve => requestAnimationFrame(resolve));
}
// Not much point filtering or sorting if there's only one event.
if (gModel.itemsToImport.length == 1) {
document.getElementById("calendar-ics-file-dialog-filters").collapsed = true;
}
await setUpItemSummaries(gModel.itemsToImport);
// Remove the loading message from the DOM to avoid it causing problems later.
@ -184,6 +189,74 @@ async function setUpItemSummaries(items) {
});
}
/**
* Filter item summaries by search string.
*
* @param {searchString} [searchString] - Terms to search for.
*/
function filterItemSummaries(searchString = "") {
let itemsContainer = document.getElementById("calendar-ics-file-dialog-items-container");
searchString = searchString.trim();
// Nothing to search for. Display all item summaries.
if (!searchString) {
gModel.itemSummaries.forEach(s => {
s.closest(".calendar-ics-file-dialog-item-frame").hidden = false;
});
itemsContainer.scrollTo(0, 0);
return;
}
searchString = searchString.toLowerCase().normalize();
// Split the search string into tokens. Quoted strings are preserved.
let searchTokens = [];
let startIndex;
while ((startIndex = searchString.indexOf('"')) != -1) {
let endIndex = searchString.indexOf('"', startIndex + 1);
if (endIndex == -1) {
endIndex = searchString.length;
}
searchTokens.push(searchString.substring(startIndex + 1, endIndex));
let query = searchString.substring(0, startIndex);
if (endIndex < searchString.length) {
query += searchString.substr(endIndex + 1);
}
searchString = query.trim();
}
if (searchString.length != 0) {
searchTokens = searchTokens.concat(searchString.split(/\s+/));
}
// Check the title and description of each item for matches.
gModel.itemSummaries.forEach(s => {
let title, description;
let matches = searchTokens.every(term => {
if (title === undefined) {
title = s.item.title.toLowerCase().normalize();
}
if (title?.includes(term)) {
return true;
}
if (description === undefined) {
description = s.item
.getProperty("description")
?.toLowerCase()
.normalize();
}
return description?.includes(term);
});
s.closest(".calendar-ics-file-dialog-item-frame").hidden = !matches;
});
itemsContainer.scrollTo(0, 0);
}
/**
* Get the currently selected calendar.
*
@ -261,17 +334,13 @@ async function importRemainingItems(event) {
let cancelButton = dialog.getButton("cancel");
acceptButton.disabled = true;
cancelButton.hidden = true;
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);
cancelButton.disabled = true;
let calendar = getCurrentlySelectedCalendar();
let remainingItems = gModel.itemsToImport.filter(item => item);
let filteredSummaries = gModel.itemSummaries.filter(
summary => summary && !summary.closest(".calendar-ics-file-dialog-item-frame").hidden
);
let remainingItems = filteredSummaries.map(summary => summary.item);
let progressElement = document.getElementById("calendar-ics-file-dialog-progress");
let duplicatesElement = document.getElementById("calendar-ics-file-dialog-duplicates-message");
@ -318,13 +387,38 @@ async function importRemainingItems(event) {
});
errorsElement.hidden = this.errorsCount == 0;
let btnLabel = await document.l10n.formatValue("calendar-ics-file-accept-button-ok-label");
setTimeout(() => {
acceptButton.label = btnLabel;
acceptButton.disabled = false;
let [acceptButtonLabel, cancelButtonLabel] = await document.l10n.formatValues([
{ id: "calendar-ics-file-accept-button-ok-label" },
{ id: "calendar-ics-file-cancel-button-close-label" },
]);
filteredSummaries.forEach(summary => {
let itemIndex = parseInt(summary.id.substring("import-item-summary-".length), 10);
delete gModel.itemsToImport[itemIndex];
delete gModel.itemSummaries[itemIndex];
summary.closest(".calendar-ics-file-dialog-item-frame").remove();
});
document.getElementById("calendar-ics-file-dialog-search-input").value = "";
filterItemSummaries();
let itemsRemain = !!document.querySelector(".calendar-ics-file-dialog-item-frame");
// An artificial delay so the progress pane doesn't appear then immediately disappear.
setTimeout(() => {
if (itemsRemain) {
acceptButton.disabled = false;
cancelButton.label = cancelButtonLabel;
cancelButton.disabled = false;
} else {
acceptButton.label = acceptButtonLabel;
acceptButton.disabled = false;
cancelButton.hidden = true;
document.removeEventListener("dialogaccept", importRemainingItems);
}
optionsPane.hidden = !itemsRemain;
progressPane.hidden = true;
resultPane.hidden = false;
resultPane.hidden = itemsRemain;
}, 500);
},
};

Просмотреть файл

@ -46,6 +46,14 @@
<menulist id="calendar-ics-file-dialog-calendar-menu"
oncommand="updateCalendarMenu();"/>
<hbox id="calendar-ics-file-dialog-filters">
<search-textbox id="calendar-ics-file-dialog-search-input"
flex="1"
data-l10n-id="calendar-ics-file-dialog-search-input"
data-l10n-attrs="placeholder"
oncommand="filterItemSummaries(this.value);"/>
</hbox>
</vbox>
<vbox id="calendar-ics-file-dialog-items-container" flex="1">

Просмотреть файл

@ -20,6 +20,9 @@ calendar-ics-file-dialog-calendar-menu-label = Import into calendar:
calendar-ics-file-dialog-items-loading-message =
.value = Loading items…
calendar-ics-file-dialog-search-input =
.placeholder = Filter items…
calendar-ics-file-dialog-progress-message = Importing…
calendar-ics-file-import-success = Successfully imported!

Просмотреть файл

@ -133,23 +133,67 @@ add_task(async () => {
"event 4 end date should be correct"
);
function check_displayed_titles(expectedTitles) {
let items = doc.querySelectorAll(
".calendar-ics-file-dialog-item-frame:not([hidden]) > calendar-item-summary"
);
Assert.deepEqual(
[...items].map(summary => summary.item.title),
expectedTitles
);
}
let filterInput = doc.getElementById("calendar-ics-file-dialog-search-input");
async function check_filter(filterText, expectedTitles) {
let commandPromise = BrowserTestUtils.waitForEvent(filterInput, "command");
EventUtils.synthesizeMouseAtCenter(filterInput, {}, dialogWindow);
if (filterText) {
EventUtils.synthesizeKey("a", { accelKey: true }, dialogWindow);
EventUtils.sendString(filterText, dialogWindow);
} else {
EventUtils.synthesizeKey("VK_ESCAPE", {}, dialogWindow);
}
await commandPromise;
check_displayed_titles(expectedTitles);
}
await check_filter("event", ["Event One", "Event Two", "Event Three", "Event Four"]);
await check_filter("four", ["Event Four"]);
await check_filter("ONE", ["Event One"]);
await check_filter(`"event t"`, ["Event Two", "Event Three"]);
await check_filter("", ["Event One", "Event Two", "Event Three", "Event Four"]);
// Import just the first item, and check that the correct number of items remains.
let firstItemImportButton = items[0].querySelector(
".calendar-ics-file-dialog-item-import-button"
);
EventUtils.synthesizeMouseAtCenter(firstItemImportButton, { clickCount: 1 }, dialogWindow);
let remainingItems;
await TestUtils.waitForCondition(() => {
remainingItems = doc.querySelectorAll(".calendar-ics-file-dialog-item-frame");
let remainingItems = doc.querySelectorAll(".calendar-ics-file-dialog-item-frame");
return remainingItems.length == 3;
}, "three items remain after importing the first item");
is(
remainingItems[0].querySelector(".item-title").textContent,
"Event Two",
"'Event Two' should now be the first item in the dialog"
);
check_displayed_titles(["Event Two", "Event Three", "Event Four"]);
// Filter and import the shown items.
await check_filter("four", ["Event Four"]);
dialogElement.getButton("accept").click();
ok(optionsPane.hidden);
ok(!progressPane.hidden);
ok(resultPane.hidden);
await TestUtils.waitForCondition(() => !optionsPane.hidden);
ok(progressPane.hidden);
ok(resultPane.hidden);
is(filterInput.value, "");
check_displayed_titles(["Event Two", "Event Three"]);
// Click the accept button to import the remaining items.
dialogElement.getButton("accept").click();
ok(optionsPane.hidden);
ok(!progressPane.hidden);
@ -162,7 +206,6 @@ add_task(async () => {
let messageElement = doc.querySelector("#calendar-ics-file-dialog-result-message");
is(messageElement.textContent, "Import complete.", "import success message appeared");
// Click the accept button to import the remaining items.
dialogElement.getButton("accept").click();
}
);