summaryrefslogtreecommitdiffstats
path: root/toolkit/components/satchel/megalist
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/satchel/megalist')
-rw-r--r--toolkit/components/satchel/megalist/MegalistViewModel.sys.mjs28
-rw-r--r--toolkit/components/satchel/megalist/aggregator/Aggregator.sys.mjs14
-rw-r--r--toolkit/components/satchel/megalist/aggregator/datasources/AddressesDataSource.sys.mjs168
-rw-r--r--toolkit/components/satchel/megalist/aggregator/datasources/BankCardDataSource.sys.mjs302
-rw-r--r--toolkit/components/satchel/megalist/aggregator/datasources/DataSourceBase.sys.mjs41
-rw-r--r--toolkit/components/satchel/megalist/aggregator/datasources/LoginDataSource.sys.mjs522
-rw-r--r--toolkit/components/satchel/megalist/content/Dialog.mjs116
-rw-r--r--toolkit/components/satchel/megalist/content/MegalistView.mjs98
-rw-r--r--toolkit/components/satchel/megalist/content/megalist.css93
-rw-r--r--toolkit/components/satchel/megalist/content/megalist.ftl28
-rw-r--r--toolkit/components/satchel/megalist/content/megalist.html175
-rw-r--r--toolkit/components/satchel/megalist/content/search-input.mjs36
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);