diff options
Diffstat (limited to 'toolkit/components/satchel/megalist')
12 files changed, 1032 insertions, 589 deletions
diff --git a/toolkit/components/satchel/megalist/MegalistViewModel.sys.mjs b/toolkit/components/satchel/megalist/MegalistViewModel.sys.mjs index f11a8a3198..66a062fa06 100644 --- a/toolkit/components/satchel/megalist/MegalistViewModel.sys.mjs +++ b/toolkit/components/satchel/megalist/MegalistViewModel.sys.mjs @@ -67,6 +67,13 @@ export class MegalistViewModel { } } + #commandsArray(snapshot) { + if (Array.isArray(snapshot.commands)) { + return snapshot.commands; + } + return Array.from(snapshot.commands()); + } + /** * * Send snapshot of necessary line data across parent-child boundary. @@ -95,7 +102,7 @@ export class MegalistViewModel { snapshot.end = snapshotData.end; } if ("commands" in snapshotData) { - snapshot.commands = snapshotData.commands; + snapshot.commands = this.#commandsArray(snapshotData); } if ("valueIcon" in snapshotData) { snapshot.valueIcon = snapshotData.valueIcon; @@ -104,7 +111,7 @@ export class MegalistViewModel { snapshot.href = snapshotData.href; } if (snapshotData.stickers) { - for (const sticker of snapshotData.stickers) { + for (const sticker of snapshotData.stickers()) { snapshot.stickers ??= []; snapshot.stickers.push(sticker); } @@ -177,13 +184,22 @@ export class MegalistViewModel { } async receiveCommand({ commandId, snapshotId, value } = {}) { + const dotIndex = commandId?.indexOf("."); + if (dotIndex >= 0) { + const dataSourceName = commandId.substring(0, dotIndex); + const functionName = commandId.substring(dotIndex + 1); + MegalistViewModel.#aggregator.callFunction(dataSourceName, functionName); + return; + } + const index = snapshotId ? snapshotId - this.#firstSnapshotId : this.#selectedIndex; const snapshot = this.#snapshots[index]; if (snapshot) { - commandId = commandId ?? snapshot.commands[0]?.id; - const mustVerify = snapshot.commands.find(c => c.id == commandId)?.verify; + const commands = this.#commandsArray(snapshot); + commandId = commandId ?? commands[0]?.id; + const mustVerify = commands.find(c => c.id == commandId)?.verify; if (!mustVerify || (await this.#verifyUser())) { // TODO:Enter the prompt message and pref for #verifyUser() await snapshot[`execute${commandId}`]?.(value); @@ -230,6 +246,10 @@ export class MegalistViewModel { } } + setLayout(layout) { + this.#messageToView("SetLayout", { layout }); + } + async #verifyUser(promptMessage, prefName) { if (!this.getOSAuthEnabled(prefName)) { promptMessage = false; diff --git a/toolkit/components/satchel/megalist/aggregator/Aggregator.sys.mjs b/toolkit/components/satchel/megalist/aggregator/Aggregator.sys.mjs index e101fadd16..f3e39ade28 100644 --- a/toolkit/components/satchel/megalist/aggregator/Aggregator.sys.mjs +++ b/toolkit/components/satchel/megalist/aggregator/Aggregator.sys.mjs @@ -60,6 +60,16 @@ export class Aggregator { this.#sources.push(source); } + callFunction(dataSource, functionName) { + const source = this.#sources.find( + source => source.constructor.name === dataSource + ); + + if (source && source[functionName]) { + source[functionName](); + } + } + /** * Exposes interface for a datasource to communicate with Aggregator. */ @@ -73,6 +83,10 @@ export class Aggregator { refreshAllLinesOnScreen() { aggregator.forEachViewModel(vm => vm.refreshAllLinesOnScreen()); }, + + setLayout(layout) { + aggregator.forEachViewModel(vm => vm.setLayout(layout)); + }, }; } } diff --git a/toolkit/components/satchel/megalist/aggregator/datasources/AddressesDataSource.sys.mjs b/toolkit/components/satchel/megalist/aggregator/datasources/AddressesDataSource.sys.mjs index f00df0b40b..f38d89f88f 100644 --- a/toolkit/components/satchel/megalist/aggregator/datasources/AddressesDataSource.sys.mjs +++ b/toolkit/components/satchel/megalist/aggregator/datasources/AddressesDataSource.sys.mjs @@ -56,103 +56,87 @@ export class AddressesDataSource extends DataSourceBase { constructor(...args) { super(...args); - this.formatMessages( - "addresses-section-label", - "address-name-label", - "address-phone-label", - "address-email-label", - "command-copy", - "addresses-disabled", - "command-delete", - "command-edit", - "addresses-command-create" - ).then( - ([ - headerLabel, - nameLabel, - phoneLabel, - emailLabel, - copyLabel, - addressesDisabled, - deleteLabel, - editLabel, - createLabel, - ]) => { - const copyCommand = { id: "Copy", label: copyLabel }; - const editCommand = { id: "Edit", label: editLabel }; - const deleteCommand = { id: "Delete", label: deleteLabel }; - this.#addressesDisabledMessage = addressesDisabled; - this.#header = this.createHeaderLine(headerLabel); - this.#header.commands.push({ id: "Create", label: createLabel }); - - let self = this; - - function prototypeLine(label, key, options = {}) { - return self.prototypeDataLine({ - label: { value: label }, - value: { - get() { - return this.editingValue ?? this.record[key]; - }, + this.localizeStrings({ + headerLabel: "addresses-section-label", + nameLabel: "address-name-label", + phoneLabel: "address-phone-label", + emailLabel: "address-email-label", + addressesDisabled: "addresses-disabled", + }).then(strings => { + const copyCommand = { id: "Copy", label: "command-copy" }; + const editCommand = { id: "Edit", label: "command-edit" }; + const deleteCommand = { id: "Delete", label: "command-delete" }; + this.#addressesDisabledMessage = strings.addressesDisabled; + this.#header = this.createHeaderLine(strings.headerLabel); + this.#header.commands.push({ + id: "Create", + label: "addresses-command-create", + }); + + let self = this; + + function prototypeLine(label, key, options = {}) { + return self.prototypeDataLine({ + label: { value: label }, + value: { + get() { + return this.editingValue ?? this.record[key]; }, - commands: { - value: [copyCommand, editCommand, "-", deleteCommand], + }, + commands: { + value: [copyCommand, editCommand, "-", deleteCommand], + }, + executeEdit: { + value() { + this.editingValue = this.record[key] ?? ""; + this.refreshOnScreen(); }, - executeEdit: { - value() { - this.editingValue = this.record[key] ?? ""; - this.refreshOnScreen(); - }, + }, + executeSave: { + async value(value) { + if (await updateAddress(this.record, key, value)) { + this.executeCancel(); + } }, - executeSave: { - async value(value) { - if (await updateAddress(this.record, key, value)) { - this.executeCancel(); - } - }, - }, - ...options, - }); - } - - this.#namePrototype = prototypeLine(nameLabel, "name", { - start: { value: true }, + }, + ...options, }); - this.#organizationPrototype = prototypeLine( - "Organization", - "organization" - ); - this.#streetAddressPrototype = prototypeLine( - "Street Address", - "street-address" - ); - this.#addressLevelThreePrototype = prototypeLine( - "Neighbourhood", - "address-level3" - ); - this.#addressLevelTwoPrototype = prototypeLine( - "City", - "address-level2" - ); - this.#addressLevelOnePrototype = prototypeLine( - "Province", - "address-level1" - ); - this.#postalCodePrototype = prototypeLine("Postal Code", "postal-code"); - this.#countryPrototype = prototypeLine("Country", "country"); - this.#phonePrototype = prototypeLine(phoneLabel, "tel"); - this.#emailPrototype = prototypeLine(emailLabel, "email", { - end: { value: true }, - }); - - Services.obs.addObserver(this, "formautofill-storage-changed"); - Services.prefs.addObserver( - "extensions.formautofill.addresses.enabled", - this - ); - this.#reloadDataSource(); } - ); + + this.#namePrototype = prototypeLine(strings.nameLabel, "name", { + start: { value: true }, + }); + this.#organizationPrototype = prototypeLine( + "Organization", + "organization" + ); + this.#streetAddressPrototype = prototypeLine( + "Street Address", + "street-address" + ); + this.#addressLevelThreePrototype = prototypeLine( + "Neighbourhood", + "address-level3" + ); + this.#addressLevelTwoPrototype = prototypeLine("City", "address-level2"); + this.#addressLevelOnePrototype = prototypeLine( + "Province", + "address-level1" + ); + this.#postalCodePrototype = prototypeLine("Postal Code", "postal-code"); + this.#countryPrototype = prototypeLine("Country", "country"); + this.#phonePrototype = prototypeLine(strings.phoneLabel, "tel"); + this.#emailPrototype = prototypeLine(strings.emailLabel, "email", { + end: { value: true }, + }); + + Services.obs.addObserver(this, "formautofill-storage-changed"); + Services.prefs.addObserver( + "extensions.formautofill.addresses.enabled", + this + ); + this.#reloadDataSource(); + }); } async #reloadDataSource() { diff --git a/toolkit/components/satchel/megalist/aggregator/datasources/BankCardDataSource.sys.mjs b/toolkit/components/satchel/megalist/aggregator/datasources/BankCardDataSource.sys.mjs index 06266a7979..9fc1a4e429 100644 --- a/toolkit/components/satchel/megalist/aggregator/datasources/BankCardDataSource.sys.mjs +++ b/toolkit/components/satchel/megalist/aggregator/datasources/BankCardDataSource.sys.mjs @@ -68,187 +68,157 @@ export class BankCardDataSource extends DataSourceBase { constructor(...args) { super(...args); // Wait for Fluent to provide strings before loading data - this.formatMessages( - "payments-section-label", - "card-number-label", - "card-expiration-label", - "card-holder-label", - "command-copy", - "command-reveal", - "command-conceal", - "payments-disabled", - "command-delete", - "command-edit", - "payments-command-create" - ).then( - ([ - headerLabel, - numberLabel, - expirationLabel, - holderLabel, - copyCommandLabel, - revealCommandLabel, - concealCommandLabel, - cardsDisabled, - deleteCommandLabel, - editCommandLabel, - cardsCreateCommandLabel, - ]) => { - const copyCommand = { id: "Copy", label: copyCommandLabel }; - const editCommand = { - id: "Edit", - label: editCommandLabel, - verify: true, - }; - const deleteCommand = { - id: "Delete", - label: deleteCommandLabel, - verify: true, - }; - this.#cardsDisabledMessage = cardsDisabled; - this.#header = this.createHeaderLine(headerLabel); - this.#header.commands.push({ - id: "Create", - label: cardsCreateCommandLabel, - }); - this.#cardNumberPrototype = this.prototypeDataLine({ - label: { value: numberLabel }, - concealed: { value: true, writable: true }, - start: { value: true }, - value: { - async get() { - if (this.editingValue !== undefined) { - return this.editingValue; - } + this.localizeStrings({ + headerLabel: "payments-section-label", + numberLabel: "card-number-label", + expirationLabel: "card-expiration-label", + holderLabel: "card-holder-label", + cardsDisabled: "payments-disabled", + }).then(strings => { + const copyCommand = { id: "Copy", label: "command-copy" }; + const editCommand = { id: "Edit", label: "command-edit", verify: true }; + const deleteCommand = { + id: "Delete", + label: "command-delete", + verify: true, + }; + this.#cardsDisabledMessage = strings.cardsDisabled; + this.#header = this.createHeaderLine(strings.headerLabel); + this.#header.commands.push({ + id: "Create", + label: "payments-command-create", + }); + this.#cardNumberPrototype = this.prototypeDataLine({ + label: { value: strings.numberLabel }, + concealed: { value: true, writable: true }, + start: { value: true }, + value: { + async get() { + if (this.isEditing()) { + return this.editingValue; + } - if (this.concealed) { - return ( - "••••••••" + - this.record["cc-number"].replaceAll("*", "").substr(-4) - ); - } - - await decryptCard(this.record); - return this.record["cc-number-decrypted"]; - }, - }, - valueIcon: { - get() { - const typeToImage = { - amex: "third-party/cc-logo-amex.png", - cartebancaire: "third-party/cc-logo-cartebancaire.png", - diners: "third-party/cc-logo-diners.svg", - discover: "third-party/cc-logo-discover.png", - jcb: "third-party/cc-logo-jcb.svg", - mastercard: "third-party/cc-logo-mastercard.svg", - mir: "third-party/cc-logo-mir.svg", - unionpay: "third-party/cc-logo-unionpay.svg", - visa: "third-party/cc-logo-visa.svg", - }; + if (this.concealed) { return ( - "chrome://formautofill/content/" + - (typeToImage[this.record["cc-type"]] ?? - "icon-credit-card-generic.svg") + "••••••••" + + this.record["cc-number"].replaceAll("*", "").substr(-4) ); - }, - }, - commands: { - get() { - const commands = [ - { id: "Conceal", label: concealCommandLabel }, - { ...copyCommand, verify: true }, - editCommand, - "-", - deleteCommand, - ]; - if (this.concealed) { - commands[0] = { - id: "Reveal", - label: revealCommandLabel, - verify: true, - }; - } - return commands; - }, + } + + await decryptCard(this.record); + return this.record["cc-number-decrypted"]; }, - executeReveal: { - value() { - this.concealed = false; - this.refreshOnScreen(); - }, + }, + valueIcon: { + get() { + const typeToImage = { + amex: "third-party/cc-logo-amex.png", + cartebancaire: "third-party/cc-logo-cartebancaire.png", + diners: "third-party/cc-logo-diners.svg", + discover: "third-party/cc-logo-discover.png", + jcb: "third-party/cc-logo-jcb.svg", + mastercard: "third-party/cc-logo-mastercard.svg", + mir: "third-party/cc-logo-mir.svg", + unionpay: "third-party/cc-logo-unionpay.svg", + visa: "third-party/cc-logo-visa.svg", + }; + return ( + "chrome://formautofill/content/" + + (typeToImage[this.record["cc-type"]] ?? + "icon-credit-card-generic.svg") + ); }, - executeConceal: { - value() { - this.concealed = true; - this.refreshOnScreen(); - }, + }, + commands: { + *value() { + if (this.concealed) { + yield { id: "Reveal", label: "command-reveal", verify: true }; + } else { + yield { id: "Conceal", label: "command-conceal" }; + } + yield { ...copyCommand, verify: true }; + yield editCommand; + yield "-"; + yield deleteCommand; }, - executeCopy: { - async value() { - await decryptCard(this.record); - this.copyToClipboard(this.record["cc-number-decrypted"]); - }, + }, + executeReveal: { + value() { + this.concealed = false; + this.refreshOnScreen(); }, - executeEdit: { - async value() { - await decryptCard(this.record); - this.editingValue = this.record["cc-number-decrypted"] ?? ""; - this.refreshOnScreen(); - }, + }, + executeConceal: { + value() { + this.concealed = true; + this.refreshOnScreen(); }, - executeSave: { - async value(value) { - if (updateCard(this.record, "cc-number", value)) { - this.executeCancel(); - } - }, + }, + executeCopy: { + async value() { + await decryptCard(this.record); + this.copyToClipboard(this.record["cc-number-decrypted"]); }, - }); - this.#expirationPrototype = this.prototypeDataLine({ - label: { value: expirationLabel }, - value: { - get() { - return `${this.record["cc-exp-month"]}/${this.record["cc-exp-year"]}`; - }, + }, + executeEdit: { + async value() { + await decryptCard(this.record); + this.editingValue = this.record["cc-number-decrypted"] ?? ""; + this.refreshOnScreen(); }, - commands: { - value: [copyCommand, editCommand, "-", deleteCommand], + }, + executeSave: { + async value(value) { + if (updateCard(this.record, "cc-number", value)) { + this.executeCancel(); + } }, - }); - this.#holderNamePrototype = this.prototypeDataLine({ - label: { value: holderLabel }, - end: { value: true }, - value: { - get() { - return this.editingValue ?? this.record["cc-name"]; - }, + }, + }); + this.#expirationPrototype = this.prototypeDataLine({ + label: { value: strings.expirationLabel }, + value: { + get() { + return `${this.record["cc-exp-month"]}/${this.record["cc-exp-year"]}`; }, - commands: { - value: [copyCommand, editCommand, "-", deleteCommand], + }, + commands: { + value: [copyCommand, editCommand, "-", deleteCommand], + }, + }); + this.#holderNamePrototype = this.prototypeDataLine({ + label: { value: strings.holderLabel }, + end: { value: true }, + value: { + get() { + return this.editingValue ?? this.record["cc-name"]; }, - executeEdit: { - value() { - this.editingValue = this.record["cc-name"] ?? ""; - this.refreshOnScreen(); - }, + }, + commands: { + value: [copyCommand, editCommand, "-", deleteCommand], + }, + executeEdit: { + value() { + this.editingValue = this.record["cc-name"] ?? ""; + this.refreshOnScreen(); }, - executeSave: { - async value(value) { - if (updateCard(this.record, "cc-name", value)) { - this.executeCancel(); - } - }, + }, + executeSave: { + async value(value) { + if (updateCard(this.record, "cc-name", value)) { + this.executeCancel(); + } }, - }); + }, + }); - Services.obs.addObserver(this, "formautofill-storage-changed"); - Services.prefs.addObserver( - "extensions.formautofill.creditCards.enabled", - this - ); - this.#reloadDataSource(); - } - ); + Services.obs.addObserver(this, "formautofill-storage-changed"); + Services.prefs.addObserver( + "extensions.formautofill.creditCards.enabled", + this + ); + this.#reloadDataSource(); + }); } /** @@ -280,7 +250,7 @@ export class BankCardDataSource extends DataSourceBase { `${card["cc-exp-month"]}/${card["cc-exp-year"]}` .toUpperCase() .includes(searchText) || - card["cc-name"].toUpperCase().includes(searchText) + card["cc-name"]?.toUpperCase().includes(searchText) ); this.formatMessages({ diff --git a/toolkit/components/satchel/megalist/aggregator/datasources/DataSourceBase.sys.mjs b/toolkit/components/satchel/megalist/aggregator/datasources/DataSourceBase.sys.mjs index 49be733aef..ee7dfed5eb 100644 --- a/toolkit/components/satchel/megalist/aggregator/datasources/DataSourceBase.sys.mjs +++ b/toolkit/components/satchel/megalist/aggregator/datasources/DataSourceBase.sys.mjs @@ -63,7 +63,30 @@ export class DataSourceBase { this.#aggregatorApi.refreshAllLinesOnScreen(); } + setLayout(layout) { + this.#aggregatorApi.setLayout(layout); + } + formatMessages = createFormatMessages("preview/megalist.ftl"); + static ftl = new Localization(["preview/megalist.ftl"]); + + async localizeStrings(strings) { + const keys = Object.keys(strings); + const localisationIds = Object.values(strings).map(id => ({ id })); + const messages = await DataSourceBase.ftl.formatMessages(localisationIds); + + for (let i = 0; i < messages.length; i++) { + let { attributes, value } = messages[i]; + if (attributes) { + value = attributes.reduce( + (result, { name, value }) => ({ ...result, [name]: value }), + {} + ); + } + strings[keys[i]] = value; + } + return strings; + } /** * Prototype for the each line. @@ -94,6 +117,10 @@ export class DataSourceBase { return true; }, + isEditing() { + return this.editingValue !== undefined; + }, + copyToClipboard(text) { lazy.ClipboardHelper.copyString(text, lazy.ClipboardHelper.Sensitive); }, @@ -135,6 +162,9 @@ export class DataSourceBase { refreshOnScreen() { this.source.refreshSingleLineOnScreen(this); }, + setLayout(data) { + this.source.setLayout(data); + }, }; /** @@ -144,7 +174,6 @@ export class DataSourceBase { * @returns {object} section header line */ createHeaderLine(label) { - const toggleCommand = { id: "Toggle", label: "" }; const result = { label, value: "", @@ -164,7 +193,7 @@ export class DataSourceBase { lineIsReady: () => true, - commands: [toggleCommand], + commands: [{ id: "Toggle", label: "command-toggle" }], executeToggle() { this.collapsed = !this.collapsed; @@ -172,10 +201,6 @@ export class DataSourceBase { }, }; - this.formatMessages("command-toggle").then(([toggleLabel]) => { - toggleCommand.label = toggleLabel; - }); - return result; } @@ -244,6 +269,10 @@ export class DataSourceBase { return this.lines[index]; } + cancelDialog() { + this.setLayout(null); + } + *enumerateLinesForMatchingRecords(searchText, stats, match) { stats.total = 0; stats.count = 0; diff --git a/toolkit/components/satchel/megalist/aggregator/datasources/LoginDataSource.sys.mjs b/toolkit/components/satchel/megalist/aggregator/datasources/LoginDataSource.sys.mjs index 324bc4d141..7e74ce2488 100644 --- a/toolkit/components/satchel/megalist/aggregator/datasources/LoginDataSource.sys.mjs +++ b/toolkit/components/satchel/megalist/aggregator/datasources/LoginDataSource.sys.mjs @@ -19,13 +19,6 @@ XPCOMUtils.defineLazyPreferenceGetter( false ); -XPCOMUtils.defineLazyPreferenceGetter( - lazy, - "VULNERABLE_PASSWORDS_ENABLED", - "signon.management.page.vulnerable-passwords.enabled", - false -); - /** * Data source for Logins. * @@ -45,235 +38,239 @@ export class LoginDataSource extends DataSourceBase { constructor(...args) { super(...args); // Wait for Fluent to provide strings before loading data - this.formatMessages( - "passwords-section-label", - "passwords-origin-label", - "passwords-username-label", - "passwords-password-label", - "command-open", - "command-copy", - "command-reveal", - "command-conceal", - "passwords-disabled", - "command-delete", - "command-edit", - "passwords-command-create", - "passwords-command-import", - "passwords-command-export", - "passwords-command-remove-all", - "passwords-command-settings", - "passwords-command-help", - "passwords-import-file-picker-title", - "passwords-import-file-picker-import-button", - "passwords-import-file-picker-csv-filter-title", - "passwords-import-file-picker-tsv-filter-title" - ).then( - ([ - headerLabel, - originLabel, - usernameLabel, - passwordLabel, - openCommandLabel, - copyCommandLabel, - revealCommandLabel, - concealCommandLabel, - passwordsDisabled, - deleteCommandLabel, - editCommandLabel, - passwordsCreateCommandLabel, - passwordsImportCommandLabel, - passwordsExportCommandLabel, - passwordsRemoveAllCommandLabel, - passwordsSettingsCommandLabel, - passwordsHelpCommandLabel, - passwordsImportFilePickerTitle, - passwordsImportFilePickerImportButton, - passwordsImportFilePickerCsvFilterTitle, - passwordsImportFilePickerTsvFilterTitle, - ]) => { - const copyCommand = { id: "Copy", label: copyCommandLabel }; - const editCommand = { id: "Edit", label: editCommandLabel }; - const deleteCommand = { id: "Delete", label: deleteCommandLabel }; - this.breachedSticker = { type: "warning", label: "BREACH" }; - this.vulnerableSticker = { type: "risk", label: "🤮 Vulnerable" }; - this.#loginsDisabledMessage = passwordsDisabled; - this.#header = this.createHeaderLine(headerLabel); - this.#header.commands.push( - { id: "Create", label: passwordsCreateCommandLabel }, - { id: "Import", label: passwordsImportCommandLabel }, - { id: "Export", label: passwordsExportCommandLabel }, - { id: "RemoveAll", label: passwordsRemoveAllCommandLabel }, - { id: "Settings", label: passwordsSettingsCommandLabel }, - { id: "Help", label: passwordsHelpCommandLabel } + this.localizeStrings({ + headerLabel: "passwords-section-label", + originLabel: "passwords-origin-label", + usernameLabel: "passwords-username-label", + passwordLabel: "passwords-password-label", + passwordsDisabled: "passwords-disabled", + passwordsImportFilePickerTitle: "passwords-import-file-picker-title", + passwordsImportFilePickerImportButton: + "passwords-import-file-picker-import-button", + passwordsImportFilePickerCsvFilterTitle: + "passwords-import-file-picker-csv-filter-title", + passwordsImportFilePickerTsvFilterTitle: + "passwords-import-file-picker-tsv-filter-title", + dismissBreachCommandLabel: "passwords-dismiss-breach-alert-command", + }).then(strings => { + const copyCommand = { id: "Copy", label: "command-copy" }; + const editCommand = { id: "Edit", label: "command-edit" }; + const deleteCommand = { id: "Delete", label: "command-delete" }; + const dismissBreachCommand = { + id: "DismissBreach", + label: strings.dismissBreachCommandLabel, + }; + const noOriginSticker = { type: "error", label: "😾 Missing origin" }; + const noPasswordSticker = { type: "error", label: "😾 Missing password" }; + const breachedSticker = { type: "warning", label: "BREACH" }; + const vulnerableSticker = { type: "risk", label: "🤮 Vulnerable" }; + this.#loginsDisabledMessage = strings.passwordsDisabled; + this.#header = this.createHeaderLine(strings.headerLabel); + this.#header.commands.push( + { id: "Create", label: "passwords-command-create" }, + { id: "Import", label: "passwords-command-import" }, + { id: "Export", label: "passwords-command-export" }, + { id: "RemoveAll", label: "passwords-command-remove-all" }, + { id: "Settings", label: "passwords-command-settings" }, + { id: "Help", label: "passwords-command-help" } + ); + this.#header.executeImport = async () => + this.#importFromFile( + strings.passwordsImportFilePickerTitle, + strings.passwordsImportFilePickerImportButton, + strings.passwordsImportFilePickerCsvFilterTitle, + strings.passwordsImportFilePickerTsvFilterTitle ); - this.#header.executeImport = async () => { - await this.#importFromFile( - passwordsImportFilePickerTitle, - passwordsImportFilePickerImportButton, - passwordsImportFilePickerCsvFilterTitle, - passwordsImportFilePickerTsvFilterTitle - ); - }; - this.#header.executeSettings = () => { - this.#openPreferences(); - }; - this.#header.executeHelp = () => { - this.#getHelp(); - }; - - this.#originPrototype = this.prototypeDataLine({ - label: { value: originLabel }, - start: { value: true }, - value: { - get() { - return this.record.displayOrigin; - }, + + this.#header.executeRemoveAll = () => this.#removeAllPasswords(); + this.#header.executeSettings = () => this.#openPreferences(); + this.#header.executeHelp = () => this.#getHelp(); + this.#header.executeExport = () => this.#exportAllPasswords(); + + this.#originPrototype = this.prototypeDataLine({ + label: { value: strings.originLabel }, + start: { value: true }, + value: { + get() { + return this.record.displayOrigin; + }, + }, + valueIcon: { + get() { + return `page-icon:${this.record.origin}`; }, - valueIcon: { - get() { - return `page-icon:${this.record.origin}`; - }, + }, + href: { + get() { + return this.record.origin; }, - href: { - get() { - return this.record.origin; - }, + }, + commands: { + *value() { + yield { id: "Open", label: "command-open" }; + yield copyCommand; + yield "-"; + yield deleteCommand; + + if (this.breached) { + yield dismissBreachCommand; + } }, - commands: { - value: [ - { id: "Open", label: openCommandLabel }, - copyCommand, - "-", - deleteCommand, - ], + }, + executeDismissBreach: { + value() { + lazy.LoginBreaches.recordBreachAlertDismissal(this.record.guid); + delete this.breached; + this.refreshOnScreen(); }, - executeCopy: { - value() { - this.copyToClipboard(this.record.origin); - }, + }, + executeCopy: { + value() { + this.copyToClipboard(this.record.origin); }, - }); - this.#usernamePrototype = this.prototypeDataLine({ - label: { value: usernameLabel }, - value: { - get() { - return this.editingValue ?? this.record.username; - }, + }, + executeDelete: { + value() { + this.setLayout({ id: "remove-login" }); + }, + }, + stickers: { + *value() { + if (this.isEditing() && !this.editingValue.length) { + yield noOriginSticker; + } + + if (this.breached) { + yield breachedSticker; + } }, - commands: { value: [copyCommand, editCommand, "-", deleteCommand] }, - executeEdit: { - value() { - this.editingValue = this.record.username ?? ""; - this.refreshOnScreen(); - }, + }, + }); + this.#usernamePrototype = this.prototypeDataLine({ + label: { value: strings.usernameLabel }, + value: { + get() { + return this.editingValue ?? this.record.username; }, - executeSave: { - value(value) { - try { - const modifiedLogin = this.record.clone(); - modifiedLogin.username = value; - Services.logins.modifyLogin(this.record, modifiedLogin); - } catch (error) { - //todo - console.error("failed to modify login", error); - } - this.executeCancel(); - }, + }, + commands: { value: [copyCommand, editCommand, "-", deleteCommand] }, + executeEdit: { + value() { + this.editingValue = this.record.username ?? ""; + this.refreshOnScreen(); }, - }); - this.#passwordPrototype = this.prototypeDataLine({ - label: { value: passwordLabel }, - concealed: { value: true, writable: true }, - end: { value: true }, - value: { - get() { - return ( - this.editingValue ?? - (this.concealed ? "••••••••" : this.record.password) - ); - }, + }, + executeSave: { + value(value) { + try { + const modifiedLogin = this.record.clone(); + modifiedLogin.username = value; + Services.logins.modifyLogin(this.record, modifiedLogin); + } catch (error) { + //todo + console.error("failed to modify login", error); + } + this.executeCancel(); + }, + }, + }); + this.#passwordPrototype = this.prototypeDataLine({ + label: { value: strings.passwordLabel }, + concealed: { value: true, writable: true }, + end: { value: true }, + value: { + get() { + return ( + this.editingValue ?? + (this.concealed ? "••••••••" : this.record.password) + ); }, - commands: { - get() { - const commands = [ - { id: "Conceal", label: concealCommandLabel }, - { - id: "Copy", - label: copyCommandLabel, - verify: true, - }, - editCommand, - "-", - deleteCommand, - ]; - if (this.concealed) { - commands[0] = { - id: "Reveal", - label: revealCommandLabel, - verify: true, - }; - } - return commands; - }, + }, + stickers: { + *value() { + if (this.isEditing() && !this.editingValue.length) { + yield noPasswordSticker; + } + + if (this.vulnerable) { + yield vulnerableSticker; + } }, - executeReveal: { - value() { - this.concealed = false; - this.refreshOnScreen(); - }, + }, + commands: { + *value() { + if (this.concealed) { + yield { id: "Reveal", label: "command-reveal", verify: true }; + } else { + yield { id: "Conceal", label: "command-conceal" }; + } + yield { ...copyCommand, verify: true }; + yield editCommand; + yield "-"; + yield deleteCommand; }, - executeConceal: { - value() { - this.concealed = true; - this.refreshOnScreen(); - }, + }, + executeReveal: { + value() { + this.concealed = false; + this.refreshOnScreen(); }, - executeCopy: { - value() { - this.copyToClipboard(this.record.password); - }, + }, + executeConceal: { + value() { + this.concealed = true; + this.refreshOnScreen(); }, - executeEdit: { - value() { - this.editingValue = this.record.password ?? ""; - this.refreshOnScreen(); - }, + }, + executeCopy: { + value() { + this.copyToClipboard(this.record.password); }, - executeSave: { - value(value) { - try { - const modifiedLogin = this.record.clone(); - modifiedLogin.password = value; - Services.logins.modifyLogin(this.record, modifiedLogin); - } catch (error) { - //todo - console.error("failed to modify login", error); - } - this.executeCancel(); - }, + }, + executeEdit: { + value() { + this.editingValue = this.record.password ?? ""; + this.refreshOnScreen(); }, - }); + }, + executeSave: { + value(value) { + if (!value) { + return; + } - Services.obs.addObserver(this, "passwordmgr-storage-changed"); - Services.prefs.addObserver("signon.rememberSignons", this); - Services.prefs.addObserver( - "signon.management.page.breach-alerts.enabled", - this - ); - Services.prefs.addObserver( - "signon.management.page.vulnerable-passwords.enabled", - this - ); - this.#reloadDataSource(); - } - ); + try { + const modifiedLogin = this.record.clone(); + modifiedLogin.password = value; + Services.logins.modifyLogin(this.record, modifiedLogin); + } catch (error) { + //todo + console.error("failed to modify login", error); + } + this.executeCancel(); + }, + }, + }); + + Services.obs.addObserver(this, "passwordmgr-storage-changed"); + Services.prefs.addObserver("signon.rememberSignons", this); + Services.prefs.addObserver( + "signon.management.page.breach-alerts.enabled", + this + ); + Services.prefs.addObserver( + "signon.management.page.vulnerable-passwords.enabled", + this + ); + this.#reloadDataSource(); + }); } async #importFromFile(title, buttonLabel, csvTitle, tsvTitle) { const { BrowserWindowTracker } = ChromeUtils.importESModule( "resource:///modules/BrowserWindowTracker.sys.mjs" ); - const browser = BrowserWindowTracker.getTopWindow().gBrowser; + const browsingContext = BrowserWindowTracker.getTopWindow().browsingContext; let { result, path } = await this.openFilePickerDialog( title, buttonLabel, @@ -287,26 +284,38 @@ export class LoginDataSource extends DataSourceBase { extensionPattern: "*.tsv", }, ], - browser.ownerGlobal + browsingContext ); if (result != Ci.nsIFilePicker.returnCancel) { - let summary; try { - summary = await LoginCSVImport.importFromCSV(path); + const summary = await LoginCSVImport.importFromCSV(path); + const counts = { added: 0, modified: 0, no_change: 0, error: 0 }; + + for (const item of summary) { + counts[item.result] += 1; + } + const l10nArgs = Object.values(counts).map(count => ({ count })); + + this.setLayout({ + id: "import-logins", + l10nArgs, + }); } catch (e) { - // TODO: Display error for import - } - if (summary) { - // TODO: Display successful import summary + this.setLayout({ id: "import-error" }); } } } - async openFilePickerDialog(title, okButtonLabel, appendFilters, ownerGlobal) { + async openFilePickerDialog( + title, + okButtonLabel, + appendFilters, + browsingContext + ) { return new Promise(resolve => { let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker); - fp.init(ownerGlobal, title, Ci.nsIFilePicker.modeOpen); + fp.init(browsingContext, title, Ci.nsIFilePicker.modeOpen); for (const appendFilter of appendFilters) { fp.appendFilter(appendFilter.title, appendFilter.extensionPattern); } @@ -318,6 +327,48 @@ export class LoginDataSource extends DataSourceBase { }); } + #removeAllPasswords() { + let count = 0; + let currentRecord; + for (const line of this.lines) { + if (line.record != currentRecord) { + count += 1; + currentRecord = line.record; + } + } + + this.setLayout({ id: "remove-logins", l10nArgs: [{ count }] }); + } + + #exportAllPasswords() { + this.setLayout({ id: "export-logins" }); + } + + confirmRemoveAll() { + Services.logins.removeAllLogins(); + this.cancelDialog(); + } + + confirmExportLogins() { + // TODO: Implement this. + // We need to simplify this function first + // https://searchfox.org/mozilla-central/source/browser/components/aboutlogins/AboutLoginsParent.sys.mjs#377 + // It's too messy right now. + this.cancelDialog(); + } + + confirmRemoveLogin() { + // TODO: Simplify getting record directly. + const login = this.lines?.[0]?.record; + Services.logins.removeLogin(login); + this.cancelDialog(); + } + + confirmRetryImport() { + // TODO: Implement this. + this.cancelDialog(); + } + #openPreferences() { const { BrowserWindowTracker } = ChromeUtils.importESModule( "resource:///modules/BrowserWindowTracker.sys.mjs" @@ -422,31 +473,8 @@ export class LoginDataSource extends DataSourceBase { this.#passwordPrototype ); - let breachIndex = - originLine.stickers?.findIndex(s => s === this.breachedSticker) ?? -1; - let breach = breachesMap.get(login.guid); - if (breach && breachIndex < 0) { - originLine.stickers ??= []; - originLine.stickers.push(this.breachedSticker); - } else if (!breach && breachIndex >= 0) { - originLine.stickers.splice(breachIndex, 1); - } - - const vulnerable = lazy.VULNERABLE_PASSWORDS_ENABLED - ? lazy.LoginBreaches.getPotentiallyVulnerablePasswordsByLoginGUID([ - login, - ]).size - : 0; - - let vulnerableIndex = - passwordLine.stickers?.findIndex(s => s === this.vulnerableSticker) ?? - -1; - if (vulnerable && vulnerableIndex < 0) { - passwordLine.stickers ??= []; - passwordLine.stickers.push(this.vulnerableSticker); - } else if (!vulnerable && vulnerableIndex >= 0) { - passwordLine.stickers.splice(vulnerableIndex, 1); - } + originLine.breached = breachesMap.has(login.guid); + passwordLine.vulnerable = lazy.LoginBreaches.isVulnerablePassword(login); }); this.afterReloadingDataSource(); diff --git a/toolkit/components/satchel/megalist/content/Dialog.mjs b/toolkit/components/satchel/megalist/content/Dialog.mjs new file mode 100644 index 0000000000..f2eca6376c --- /dev/null +++ b/toolkit/components/satchel/megalist/content/Dialog.mjs @@ -0,0 +1,116 @@ +/* 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 https://mozilla.org/MPL/2.0/. */ + +const GENERIC_DIALOG_TEMPLATE = document.querySelector("#dialog-template"); + +const DIALOGS = { + "remove-login": { + template: "#remove-login-dialog-template", + }, + "export-logins": { + template: "#export-logins-dialog-template", + }, + "remove-logins": { + template: "#remove-logins-dialog-template", + callback: dialog => { + const primaryButton = dialog.querySelector("button.primary"); + const checkbox = dialog.querySelector(".confirm-checkbox"); + const toggleButton = () => (primaryButton.disabled = !checkbox.checked); + checkbox.addEventListener("change", toggleButton); + toggleButton(); + }, + }, + "import-logins": { + template: "#import-logins-dialog-template", + }, + "import-error": { + template: "#import-error-dialog-template", + }, +}; + +/** + * Setup dismiss and command handling logic for the dialog overlay. + * + * @param {Element} overlay - The overlay element containing the dialog + * @param {Function} messageHandler - Function to send message back to view model. + */ +const setupControls = (overlay, messageHandler) => { + const dialog = overlay.querySelector(".dialog-container"); + const commandButtons = dialog.querySelectorAll("[data-command]"); + for (const commandButton of commandButtons) { + const commandId = commandButton.dataset.command; + commandButton.addEventListener("click", () => messageHandler(commandId)); + } + + dialog.querySelectorAll("[close-dialog]").forEach(element => { + element.addEventListener("click", cancelDialog, { once: true }); + }); + + document.addEventListener("keyup", function handleKeyUp(ev) { + if (ev.key === "Escape") { + cancelDialog(); + document.removeEventListener("keyup", handleKeyUp); + } + }); + + document.addEventListener("click", function handleClickOutside(ev) { + if (!dialog.contains(ev.target)) { + cancelDialog(); + document.removeEventListener("click", handleClickOutside); + } + }); + dialog.querySelector("[autofocus]")?.focus(); +}; + +/** + * Add data-l10n-args to elements with localizable attribute + * + * @param {Element} dialog - The dialog element. + * @param {Array<object>} l10nArgs - List of localization arguments. + */ +const populateL10nArgs = (dialog, l10nArgs) => { + const localizableElements = dialog.querySelectorAll("[localizable]"); + for (const [index, localizableElement] of localizableElements.entries()) { + localizableElement.dataset.l10nArgs = JSON.stringify(l10nArgs[index]) ?? ""; + } +}; + +/** + * Remove the currently displayed dialog overlay from the DOM. + */ +export const cancelDialog = () => + document.querySelector(".dialog-overlay")?.remove(); + +/** + * Create a new dialog overlay and populate it using the specified template and data. + * + * @param {object} dialogData - Data required to populate the dialog, includes template and localization args. + * @param {Function} messageHandler - Function to send message back to view model. + */ +export const createDialog = (dialogData, messageHandler) => { + const templateData = DIALOGS[dialogData?.id]; + + const genericTemplateClone = document.importNode( + GENERIC_DIALOG_TEMPLATE.content, + true + ); + + const overlay = genericTemplateClone.querySelector(".dialog-overlay"); + const dialog = genericTemplateClone.querySelector(".dialog-container"); + + const overrideTemplate = document.querySelector(templateData.template); + const overrideTemplateClone = document.importNode( + overrideTemplate.content, + true + ); + + genericTemplateClone + .querySelector(".dialog-wrapper") + .appendChild(overrideTemplateClone); + + populateL10nArgs(genericTemplateClone, dialogData.l10nArgs); + setupControls(overlay, messageHandler); + document.body.appendChild(genericTemplateClone); + templateData?.callback?.(dialog, messageHandler); +}; diff --git a/toolkit/components/satchel/megalist/content/MegalistView.mjs b/toolkit/components/satchel/megalist/content/MegalistView.mjs index 44a0198692..feec2409f8 100644 --- a/toolkit/components/satchel/megalist/content/MegalistView.mjs +++ b/toolkit/components/satchel/megalist/content/MegalistView.mjs @@ -4,13 +4,14 @@ import { html } from "chrome://global/content/vendor/lit.all.mjs"; import { MozLitElement } from "chrome://global/content/lit-utils.mjs"; +import { + createDialog, + cancelDialog, +} from "chrome://global/content/megalist/Dialog.mjs"; // eslint-disable-next-line import/no-unassigned-import import "chrome://global/content/megalist/VirtualizedList.mjs"; -// eslint-disable-next-line import/no-unassigned-import -import "chrome://global/content/megalist/search-input.mjs"; - /** * Map with limit on how many entries it can have. * When over limit entries are added, oldest one are removed. @@ -77,6 +78,7 @@ export class MegalistView extends MozLitElement { super(); this.selectedIndex = 0; this.searchText = ""; + this.layout = null; window.addEventListener("MessageFromViewModel", ev => this.#onMessageFromViewModel(ev) @@ -88,6 +90,7 @@ export class MegalistView extends MozLitElement { listLength: { type: Number }, selectedIndex: { type: Number }, searchText: { type: String }, + layout: { type: Object }, }; } @@ -112,6 +115,10 @@ export class MegalistView extends MozLitElement { #templates = {}; + static queries = { + searchInput: ".search", + }; + connectedCallback() { super.connectedCallback(); this.ownerDocument.addEventListener("keydown", e => this.#handleKeydown(e)); @@ -296,6 +303,16 @@ export class MegalistView extends MozLitElement { this.requestUpdate(); } + receiveSetLayout({ layout }) { + if (layout) { + createDialog(layout, commandId => + this.#messageToViewModel("Command", { commandId }) + ); + } else { + cancelDialog(); + } + } + #handleInputChange(e) { const searchText = e.target.value; this.#messageToViewModel("UpdateFilter", { searchText }); @@ -333,6 +350,15 @@ export class MegalistView extends MozLitElement { } #handleClick(e) { + const elementWithCommand = e.composedTarget.closest("[data-command]"); + if (elementWithCommand) { + const commandId = elementWithCommand.dataset.command; + if (commandId) { + this.#messageToViewModel("Command", { commandId }); + return; + } + } + const lineElement = e.composedTarget.closest(".line"); if (!lineElement) { return; @@ -360,6 +386,12 @@ export class MegalistView extends MozLitElement { const popup = this.ownerDocument.createElement("div"); popup.className = "menuPopup"; + + let closeMenu = () => { + popup.remove(); + this.searchInput.focus(); + }; + popup.addEventListener( "keydown", e => { @@ -385,7 +417,7 @@ export class MegalistView extends MozLitElement { switch (e.code) { case "Escape": - popup.remove(); + closeMenu(); break; case "Tab": if (e.shiftKey) { @@ -416,9 +448,7 @@ export class MegalistView extends MozLitElement { e.composedTarget?.closest(".menuPopup") != e.relatedTarget?.closest(".menuPopup") ) { - // TODO: this triggers on macOS before "click" event. Due to this, - // we are not receiving the command. - popup.remove(); + closeMenu(); } }, { capture: true } @@ -433,7 +463,7 @@ export class MegalistView extends MozLitElement { } const menuItem = this.ownerDocument.createElement("button"); - menuItem.textContent = command.label; + menuItem.setAttribute("data-l10n-id", command.label); menuItem.addEventListener("click", e => { this.#messageToViewModel("Command", { snapshotId, @@ -449,26 +479,50 @@ export class MegalistView extends MozLitElement { popup.querySelector("button")?.focus(); } + /** + * Renders data-source specific UI that should be displayed before the + * virtualized list. This is determined by the "SetLayout" message provided + * by the View Model. Defaults to displaying the search input. + */ + renderBeforeList() { + return html` + <input + class="search" + type="search" + data-l10n-id="filter-placeholder" + .value=${this.searchText} + @input=${e => this.#handleInputChange(e)} + /> + `; + } + + renderList() { + if (this.layout) { + return null; + } + + return html` <virtualized-list + .lineCount=${this.listLength} + .lineHeight=${MegalistView.LINE_HEIGHT} + .selectedIndex=${this.selectedIndex} + .createLineElement=${index => this.createLineElement(index)} + @click=${e => this.#handleClick(e)} + > + </virtualized-list>`; + } + + renderAfterList() {} + render() { return html` <link rel="stylesheet" href="chrome://global/content/megalist/megalist.css" /> - <div class="container"> - <search-input - .value=${this.searchText} - .change=${e => this.#handleInputChange(e)} - > - </search-input> - <virtualized-list - .lineCount=${this.listLength} - .lineHeight=${MegalistView.LINE_HEIGHT} - .selectedIndex=${this.selectedIndex} - .createLineElement=${index => this.createLineElement(index)} - @click=${e => this.#handleClick(e)} - > - </virtualized-list> + <div @click=${this.#handleClick} class="container"> + <div class="beforeList">${this.renderBeforeList()}</div> + ${this.renderList()} + <div class="afterList">${this.renderAfterList()}</div> </div> `; } diff --git a/toolkit/components/satchel/megalist/content/megalist.css b/toolkit/components/satchel/megalist/content/megalist.css index b442a7b60d..3f8bb9de2c 100644 --- a/toolkit/components/satchel/megalist/content/megalist.css +++ b/toolkit/components/satchel/megalist/content/megalist.css @@ -8,18 +8,27 @@ display: flex; flex-direction: column; justify-content: center; - max-height: 100vh; + height: 100vh; - > search-input { + > .beforeList { margin: 20px; + + .search { + padding: 8px; + border-radius: 4px; + border: 1px solid var(--in-content-border-color); + box-sizing: border-box; + width: 100%; + } } } virtualized-list { position: relative; overflow: auto; - margin: 20px; - + margin-block: 20px; + padding-inline: 20px; + flex-grow: 1; .lines-container { padding-inline-start: unset; } @@ -29,7 +38,7 @@ virtualized-list { display: flex; align-items: stretch; position: absolute; - width: 100%; + width: calc(100% - 40px); user-select: none; box-sizing: border-box; height: 64px; @@ -93,11 +102,19 @@ virtualized-list { > .content { flex-grow: 1; + &:not(.section) { + display: grid; + grid-template-rows: max-content 1fr; + grid-template-columns: max-content; + grid-column-gap: 8px; + padding-inline-start: 8px; + padding-block-start: 4px; + } + > div { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; - padding-inline-start: 10px; &:last-child { padding-block-end: 10px; @@ -115,6 +132,8 @@ virtualized-list { > .label { color: var(--text-color-deemphasized); padding-block: 2px 4px; + grid-row: 1; + align-content: end; } > .value { @@ -125,7 +144,7 @@ virtualized-list { fill: currentColor; width: auto; height: 16px; - margin-inline: 4px; + margin-inline-end: 4px; vertical-align: text-bottom; } @@ -139,12 +158,14 @@ virtualized-list { } > .stickers { - text-align: end; - margin-block-start: 2px; + grid-row: 1; + align-content: start; > span { - padding: 2px; + padding: 4px; margin-inline-end: 2px; + border-radius: 24px; + font-size: xx-small; } /* Hard-coded colors will be addressed in FXCM-1013 */ @@ -159,6 +180,12 @@ virtualized-list { border: 1px solid maroon; color: whitesmoke; } + + > span.error { + background-color: orange; + border: 1px solid orangered; + color: black; + } } &.section { @@ -199,10 +226,46 @@ virtualized-list { } } -.search { - padding: 8px; - border-radius: 4px; - border: 1px solid var(--in-content-border-color); - box-sizing: border-box; +/* Dialog styles */ +.dialog-overlay { + display: flex; + justify-content: center; + align-items: center; + padding: 16px; + position: fixed; + top: 0; + left: 0; width: 100%; + height: 100%; + z-index: 1; + background-color: rgba(0, 0, 0, 0.5); + box-sizing: border-box; + /* TODO: probably want to remove this later ? */ + backdrop-filter: blur(6px); +} + +.dialog-container { + display: grid; + padding: 16px 32px; + color: var(--in-content-text-color); + background-color: var(--in-content-box-background); + border: 1px solid var(--in-content-border-color); + box-shadow: 0 2px 8px 0 rgba(0, 0, 0, 0.1); +} + +.dialog-title { + margin: 0; +} + +.dismiss-button { + justify-self: end; +} + +.dialog-content { + margin-block-start: 16px; + margin-block-end: 16px; + + .checkbox-text { + margin-block-start: 8px; + } } diff --git a/toolkit/components/satchel/megalist/content/megalist.ftl b/toolkit/components/satchel/megalist/content/megalist.ftl index 69d085a7c5..4089477add 100644 --- a/toolkit/components/satchel/megalist/content/megalist.ftl +++ b/toolkit/components/satchel/megalist/content/megalist.ftl @@ -23,6 +23,7 @@ command-cancel = Cancel passwords-section-label = Passwords passwords-disabled = Passwords are disabled +passwords-dismiss-breach-alert-command = Dismiss breach alert passwords-command-create = Add Password passwords-command-import = Import from a File… passwords-command-export = Export Passwords… @@ -65,6 +66,33 @@ passwords-filtered-count = *[other] { $count } of { $total } passwords } +# Confirm the removal of all saved passwords +# $total (number) - Total number of passwords +passwords-remove-all-title = + { $total -> + [one] Remove { $total } password? + *[other] Remove all { $total } passwords? + } + +# Checkbox label to confirm the removal of saved passwords +# $total (number) - Total number of passwords +passwords-remove-all-confirm = + { $total -> + [1] Yes, remove password + *[other] Yes, remove passwords + } + +# Button label to confirm removal of saved passwords +passwords-remove-all-confirm-button = Confirm + +# Message to confirm the removal of saved passwords +# $total (number) - Total number of passwords +passwords-remove-all-message = + { $total -> + [1] This will remove your saved password and any breach alerts. You cannot undo this action. + *[other] This will remove your saved passwords and any breach alerts. You cannot undo this action. + } + passwords-origin-label = Website address passwords-username-label = Username passwords-password-label = Password diff --git a/toolkit/components/satchel/megalist/content/megalist.html b/toolkit/components/satchel/megalist/content/megalist.html index 6ff3f089fc..9d15587033 100644 --- a/toolkit/components/satchel/megalist/content/megalist.html +++ b/toolkit/components/satchel/megalist/content/megalist.html @@ -15,7 +15,12 @@ src="chrome://global/content/megalist/MegalistView.mjs" ></script> <link rel="stylesheet" href="chrome://global/skin/in-content/common.css" /> + <link + rel="stylesheet" + href="chrome://global/content/megalist/megalist.css" + /> <link rel="localization" href="preview/megalist.ftl" /> + <link rel="localization" href="browser/aboutLogins.ftl" /> </head> <body> @@ -56,11 +61,11 @@ <template id="lineTemplate"> <div class="content"> <div class="label"></div> + <div class="stickers"></div> <div class="value"> <img class="icon" /> <span></span> </div> - <div class="stickers"></div> </div> </template> @@ -74,5 +79,173 @@ <div class="stickers"></div> </div> </template> + + <template id="dialog-template"> + <div class="dialog-overlay"> + <div class="dialog-container"> + <moz-button + data-l10n-id="confirmation-dialog-dismiss-button" + iconSrc="chrome://global/skin/icons/close.svg" + size="small" + type="icon ghost" + class="dismiss-button" + close-dialog + > + </moz-button> + <div class="dialog-wrapper"></div> + </div> + </div> + </template> + + <template id="remove-logins-dialog-template"> + <h2 + class="dialog-title" + data-l10n-id="about-logins-confirm-remove-all-sync-dialog-title2" + localizable + ></h2> + <div class="dialog-content" slot="dialog-content"> + <p data-l10n-id="about-logins-confirm-export-dialog-message2"></p> + <label> + <input type="checkbox" class="confirm-checkbox checkbox" autofocus /> + <span + class="checkbox-text" + data-l10n-id="about-logins-confirm-remove-all-dialog-checkbox-label2" + ></span> + </label> + </div> + <moz-button-group> + <button + class="primary danger-button" + data-l10n-id="about-logins-confirm-remove-all-dialog-confirm-button-label" + data-command="LoginDataSource.confirmRemoveAll" + ></button> + <button + close-dialog + data-l10n-id="confirmation-dialog-cancel-button" + ></button> + </moz-button-group> + </template> + + <template id="remove-login-dialog-template"> + <h2 + class="dialog-title" + data-l10n-id="about-logins-confirm-delete-dialog-title" + ></h2> + <div class="dialog-content" slot="dialog-content"> + <p data-l10n-id="about-logins-confirm-delete-dialog-message"></p> + </div> + <moz-button-group> + <button + class="primary danger-button" + data-l10n-id="about-logins-confirm-remove-dialog-confirm-button" + data-command="LoginDataSource.confirmRemoveLogin" + ></button> + <button + close-dialog + data-l10n-id="confirmation-dialog-cancel-button" + ></button> + </moz-button-group> + </template> + <template id="export-logins-dialog-template"> + <h2 + class="dialog-title" + data-l10n-id="about-logins-confirm-export-dialog-title2" + ></h2> + <div class="dialog-content" slot="dialog-content"> + <p data-l10n-id="about-logins-confirm-export-dialog-message2"></p> + </div> + <moz-button-group> + <button + class="primary danger-button" + data-l10n-id="about-logins-confirm-export-dialog-confirm-button2" + data-command="LoginDataSource.confirmExportLogins" + ></button> + <button + close-dialog + data-l10n-id="confirmation-dialog-cancel-button" + ></button> + </moz-button-group> + </template> + + <template id="import-logins-dialog-template"> + <h2 + class="dialog-title" + data-l10n-id="about-logins-import-dialog-title" + ></h2> + <div class="dialog-content"> + <div data-l10n-id="about-logins-import-dialog-items-added2" localizable> + <span></span> + <span data-l10n-name="count"></span> + </div> + <div + data-l10n-id="about-logins-import-dialog-items-modified2" + localizable + > + <span></span> + <span data-l10n-name="count"></span> + </div> + <div + data-l10n-id="about-logins-import-dialog-items-no-change2" + data-l10n-name="no-change" + localizable + > + <span></span> + <span data-l10n-name="count"></span> + <span data-l10n-name="meta"></span> + </div> + <div data-l10n-id="about-logins-import-dialog-items-error" localizable> + <span></span> + <span data-l10n-name="count"></span> + <span data-l10n-name="meta"></span> + </div> + <a + class="open-detailed-report" + href="about:loginsimportreport" + target="_blank" + data-l10n-id="about-logins-alert-import-message" + ></a> + </div> + <button + class="primary" + data-l10n-id="about-logins-import-dialog-done" + close-dialog + ></button> + </template> + + <template id="import-error-dialog-template"> + <h2 + class="dialog-title" + data-l10n-id="about-logins-import-dialog-error-title" + ></h2> + <div class="dialog-content"> + <p + data-l10n-id="about-logins-import-dialog-error-file-format-title" + ></p> + <p + data-l10n-id="about-logins-import-dialog-error-file-format-description" + ></p> + <p + data-l10n-id="about-logins-import-dialog-error-no-logins-imported" + ></p> + <a + class="error-learn-more-link" + href="https://support.mozilla.org/kb/import-login-data-file" + data-l10n-id="about-logins-import-dialog-error-learn-more" + target="_blank" + rel="noreferrer" + ></a> + </div> + <moz-button-group> + <button + class="primary" + data-l10n-id="about-logins-import-dialog-error-try-import-again" + data-command="LoginDataSource.confirmRetryImport" + ></button> + <button + close-dialog + data-l10n-id="confirmation-dialog-cancel-button" + ></button> + </moz-button-group> + </template> </body> </html> diff --git a/toolkit/components/satchel/megalist/content/search-input.mjs b/toolkit/components/satchel/megalist/content/search-input.mjs deleted file mode 100644 index e30d13ef2a..0000000000 --- a/toolkit/components/satchel/megalist/content/search-input.mjs +++ /dev/null @@ -1,36 +0,0 @@ -/* 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/. */ - -import { html } from "chrome://global/content/vendor/lit.all.mjs"; -import { MozLitElement } from "chrome://global/content/lit-utils.mjs"; - -export default class SearchInput extends MozLitElement { - static get properties() { - return { - items: { type: Array }, - change: { type: Function }, - value: { type: String }, - }; - } - - render() { - return html` <link - rel="stylesheet" - href="chrome://global/content/megalist/megalist.css" - /> - <link - rel="stylesheet" - href="chrome://global/skin/in-content/common.css" - /> - <input - class="search" - type="search" - data-l10n-id="filter-placeholder" - @input=${this.change} - .value=${this.value} - />`; - } -} - -customElements.define("search-input", SearchInput); |