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:
Родитель
e7c25ffdd1
Коммит
b9b87a8da9
|
@ -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();
|
||||
}
|
||||
);
|
||||
|
|
Загрузка…
Ссылка в новой задаче