Up until now, we've used the connectedCallback to initialize the MigrationWizard. That's been fine, except that it requires us to remove and then re-add the element to the DOM if we want to "reset" it and start over. This patch adds a method "requestState" to the MigrationWizard that kicks off the initialization. Embedders can set the `auto-request-state` attribute on the element if they're happy to just use the connectedCallback. Finally, this adds an intrinsic width to the entire MigrationWizard element to reduce flicker when transitioning between states. Differential Revision: https://phabricator.services.mozilla.com/D171742
483 lines
17 KiB
JavaScript
483 lines
17 KiB
JavaScript
/* 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/. */
|
|
|
|
// eslint-disable-next-line import/no-unassigned-import
|
|
import "chrome://global/content/elements/moz-button-group.mjs";
|
|
import { MigrationWizardConstants } from "chrome://browser/content/migration/migration-wizard-constants.mjs";
|
|
|
|
/**
|
|
* This component contains the UI that steps users through migrating their
|
|
* data from other browsers to this one. This component only contains very
|
|
* basic logic and structure for the UI, and most of the state management
|
|
* occurs in the MigrationWizardChild JSWindowActor.
|
|
*/
|
|
export class MigrationWizard extends HTMLElement {
|
|
static #template = null;
|
|
|
|
#deck = null;
|
|
#browserProfileSelector = null;
|
|
#resourceTypeList = null;
|
|
#shadowRoot = null;
|
|
#importButton = null;
|
|
|
|
static get markup() {
|
|
return `
|
|
<template>
|
|
<link rel="stylesheet" href="chrome://browser/skin/migration/migration-wizard.css">
|
|
<named-deck id="wizard-deck" selected-view="page-loading" aria-live="polite" aria-busy="true">
|
|
<div name="page-loading">
|
|
<h3 data-l10n-id="migration-wizard-selection-header"></h3>
|
|
<div class="loading-block large"></div>
|
|
<div class="loading-block small"></div>
|
|
<div class="loading-block small"></div>
|
|
<moz-button-group class="buttons">
|
|
<!-- If possible, use the same button labels as the SELECTION page with the same strings.
|
|
That'll prevent flicker when the load state exits if we then enter the SELECTION page. -->
|
|
<button class="cancel-close" data-l10n-id="migration-cancel-button-label" disabled></button>
|
|
<button data-l10n-id="migration-import-button-label" disabled></button>
|
|
</moz-button-group>
|
|
</div>
|
|
|
|
<div name="page-selection">
|
|
<h3 data-l10n-id="migration-wizard-selection-header"></h3>
|
|
<select id="browser-profile-selector">
|
|
</select>
|
|
<details class="resource-selection-details" open="true">
|
|
<summary>
|
|
<div data-l10n-id="migration-all-available-data-label"></div>
|
|
<div data-l10n-id="migration-available-data-label"></div>
|
|
<span class="dropdown-icon" role="img"></span>
|
|
</summary>
|
|
<fieldset id="resource-type-list">
|
|
<label id="select-all">
|
|
<input type="checkbox" class="select-all-checkbox"/><span data-l10n-id="migration-select-all-option-label"></span>
|
|
</label>
|
|
<label id="bookmarks" data-resource-type="BOOKMARKS"/>
|
|
<input type="checkbox"/><span data-l10n-id="migration-bookmarks-option-label"></span>
|
|
</label>
|
|
<label id="logins-and-passwords" data-resource-type="PASSWORDS">
|
|
<input type="checkbox"/><span data-l10n-id="migration-logins-and-passwords-option-label"></span>
|
|
</label>
|
|
<label id="history" data-resource-type="HISTORY">
|
|
<input type="checkbox"/><span data-l10n-id="migration-history-option-label"></span>
|
|
</label>
|
|
<label id="form-autofill" data-resource-type="FORMDATA">
|
|
<input type="checkbox"/><span data-l10n-id="migration-form-autofill-option-label"></span>
|
|
</label>
|
|
</fieldset>
|
|
</details>
|
|
|
|
<moz-button-group class="buttons">
|
|
<button class="cancel-close" data-l10n-id="migration-cancel-button-label"></button>
|
|
<button id="import" class="primary" data-l10n-id="migration-import-button-label"></button>
|
|
</moz-button-group>
|
|
</div>
|
|
|
|
<div name="page-progress">
|
|
<h3 id="progress-header" data-l10n-id="migration-wizard-progress-header"></h3>
|
|
<div class="resource-progress">
|
|
<div data-resource-type="BOOKMARKS" class="resource-progress-group">
|
|
<span class="progress-icon-parent"><span class="progress-icon" role="img"></span></span>
|
|
<span data-l10n-id="migration-bookmarks-option-label"></span>
|
|
<span class="success-text"> </span>
|
|
</div>
|
|
|
|
<div data-resource-type="PASSWORDS" class="resource-progress-group">
|
|
<span class="progress-icon-parent"><span class="progress-icon" role="img"></span></span>
|
|
<span data-l10n-id="migration-logins-and-passwords-option-label"></span>
|
|
<span class="success-text"> </span>
|
|
</div>
|
|
|
|
<div data-resource-type="HISTORY" class="resource-progress-group">
|
|
<span class="progress-icon-parent"><span class="progress-icon" role="img"></span></span>
|
|
<span data-l10n-id="migration-history-option-label"></span>
|
|
<span class="success-text"> </span>
|
|
</div>
|
|
|
|
<div data-resource-type="FORMDATA" class="resource-progress-group">
|
|
<span class="progress-icon-parent"><span class="progress-icon" role="img"></span></span>
|
|
<span data-l10n-id="migration-form-autofill-option-label"></span>
|
|
<span class="success-text"> </span>
|
|
</div>
|
|
</div>
|
|
<moz-button-group class="buttons">
|
|
<button class="cancel-close" data-l10n-id="migration-cancel-button-label" disabled></button>
|
|
<button class="primary" id="done-button" data-l10n-id="migration-done-button-label"></button>
|
|
</moz-button-group>
|
|
</div>
|
|
|
|
<div name="page-safari-permission">
|
|
<h3>TODO: Safari permission page</h3>
|
|
</div>
|
|
|
|
<div name="page-no-browsers-found">
|
|
<h3 data-l10n-id="migration-wizard-selection-header"></h3>
|
|
<div class="no-browsers-found">
|
|
<span class="error-icon" role="img"></span>
|
|
<div class="no-browsers-found-message" data-l10n-id="migration-wizard-import-browser-no-browsers"></div>
|
|
</div>
|
|
<moz-button-group class="buttons">
|
|
<button class="cancel-close" data-l10n-id="migration-cancel-button-label"></button>
|
|
</moz-button-group>
|
|
</div>
|
|
</named-deck>
|
|
</template>
|
|
`;
|
|
}
|
|
|
|
static get fragment() {
|
|
if (!MigrationWizard.#template) {
|
|
let parser = new DOMParser();
|
|
let doc = parser.parseFromString(MigrationWizard.markup, "text/html");
|
|
MigrationWizard.#template = document.importNode(
|
|
doc.querySelector("template"),
|
|
true
|
|
);
|
|
}
|
|
let fragment = MigrationWizard.#template.content.cloneNode(true);
|
|
if (window.IS_STORYBOOK) {
|
|
// If we're using Storybook, load the CSS from the static local file
|
|
// system rather than chrome:// to take advantage of auto-reloading.
|
|
fragment.querySelector("link[rel=stylesheet]").href =
|
|
"./migration/migration-wizard.css";
|
|
}
|
|
return fragment;
|
|
}
|
|
|
|
constructor() {
|
|
super();
|
|
const shadow = this.attachShadow({ mode: "closed" });
|
|
|
|
if (window.MozXULElement) {
|
|
window.MozXULElement.insertFTLIfNeeded("branding/brand.ftl");
|
|
window.MozXULElement.insertFTLIfNeeded(
|
|
"locales-preview/migrationWizard.ftl"
|
|
);
|
|
}
|
|
document.l10n.connectRoot(shadow);
|
|
|
|
shadow.appendChild(MigrationWizard.fragment);
|
|
|
|
this.#deck = shadow.querySelector("#wizard-deck");
|
|
this.#browserProfileSelector = shadow.querySelector(
|
|
"#browser-profile-selector"
|
|
);
|
|
|
|
let cancelCloseButtons = shadow.querySelectorAll(".cancel-close");
|
|
for (let button of cancelCloseButtons) {
|
|
button.addEventListener("click", this);
|
|
}
|
|
|
|
let doneCloseButtons = shadow.querySelector("#done-button");
|
|
doneCloseButtons.addEventListener("click", this);
|
|
|
|
this.#importButton = shadow.querySelector("#import");
|
|
this.#importButton.addEventListener("click", this);
|
|
|
|
this.#browserProfileSelector.addEventListener("change", this);
|
|
this.#resourceTypeList = shadow.querySelector("#resource-type-list");
|
|
|
|
let selectAllCheckbox = shadow.querySelector("#select-all").control;
|
|
selectAllCheckbox.addEventListener("change", this);
|
|
|
|
this.#shadowRoot = shadow;
|
|
}
|
|
|
|
connectedCallback() {
|
|
if (this.hasAttribute("auto-request-state")) {
|
|
this.requestState();
|
|
}
|
|
}
|
|
|
|
requestState() {
|
|
this.dispatchEvent(
|
|
new CustomEvent("MigrationWizard:RequestState", { bubbles: true })
|
|
);
|
|
}
|
|
|
|
/**
|
|
* This setter can be used in the event that the MigrationWizard is being
|
|
* inserted via Lit, and the caller wants to set state declaratively using
|
|
* a property expression.
|
|
*
|
|
* @param {object} state
|
|
* The state object to pass to setState.
|
|
* @see MigrationWizard.setState.
|
|
*/
|
|
set state(state) {
|
|
this.setState(state);
|
|
}
|
|
|
|
/**
|
|
* This is the main entrypoint for updating the state and appearance of
|
|
* the wizard.
|
|
*
|
|
* @param {object} state The state to be represented by the component.
|
|
* @param {string} state.page The page of the wizard to display. This should
|
|
* be one of the MigrationWizardConstants.PAGES constants.
|
|
*/
|
|
setState(state) {
|
|
switch (state.page) {
|
|
case MigrationWizardConstants.PAGES.SELECTION: {
|
|
this.#onShowingSelection(state);
|
|
break;
|
|
}
|
|
case MigrationWizardConstants.PAGES.PROGRESS: {
|
|
this.#onShowingProgress(state);
|
|
break;
|
|
}
|
|
}
|
|
|
|
this.#deck.toggleAttribute(
|
|
"aria-busy",
|
|
state.page == MigrationWizardConstants.PAGES.LOADING
|
|
);
|
|
this.#deck.setAttribute("selected-view", `page-${state.page}`);
|
|
|
|
if (window.IS_STORYBOOK) {
|
|
this.#updateForStorybook();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Reacts to changes to the browser / profile selector dropdown. This
|
|
* should update the list of resource types to match what's supported
|
|
* by the selected migrator and profile.
|
|
*/
|
|
#onBrowserProfileSelectionChanged() {
|
|
let resourceTypes = this.#browserProfileSelector.selectedOptions[0]
|
|
.resourceTypes;
|
|
for (let child of this.#resourceTypeList.children) {
|
|
child.hidden = true;
|
|
child.control.checked = false;
|
|
}
|
|
|
|
for (let resourceType of resourceTypes) {
|
|
let resourceLabel = this.#resourceTypeList.querySelector(
|
|
`label[data-resource-type="${resourceType}"]`
|
|
);
|
|
if (resourceLabel) {
|
|
resourceLabel.hidden = false;
|
|
resourceLabel.control.checked = true;
|
|
}
|
|
}
|
|
let selectAll = this.#shadowRoot.querySelector("#select-all").control;
|
|
selectAll.checked = true;
|
|
}
|
|
|
|
/**
|
|
* Called when showing the browser/profile selection page of the wizard.
|
|
*
|
|
* @param {object} state
|
|
* The state object passed into setState. The following properties are
|
|
* used:
|
|
* @param {string[]} state.migrators An array of source browser names that
|
|
* can be migrated from.
|
|
*/
|
|
#onShowingSelection(state) {
|
|
this.#browserProfileSelector.textContent = "";
|
|
|
|
let selectionPage = this.#shadowRoot.querySelector(
|
|
"div[name='page-selection']"
|
|
);
|
|
selectionPage.toggleAttribute("show-import-all", state.showImportAll);
|
|
|
|
for (let migrator of state.migrators) {
|
|
let opt = document.createElement("option");
|
|
opt.value = migrator.key;
|
|
opt.profile = migrator.profile;
|
|
opt.resourceTypes = migrator.resourceTypes;
|
|
|
|
if (migrator.profile) {
|
|
document.l10n.setAttributes(
|
|
opt,
|
|
"migration-wizard-selection-option-with-profile",
|
|
{
|
|
sourceBrowser: migrator.displayName,
|
|
profileName: migrator.profile.name,
|
|
}
|
|
);
|
|
} else {
|
|
document.l10n.setAttributes(
|
|
opt,
|
|
"migration-wizard-selection-option-without-profile",
|
|
{
|
|
sourceBrowser: migrator.displayName,
|
|
}
|
|
);
|
|
}
|
|
|
|
this.#browserProfileSelector.appendChild(opt);
|
|
}
|
|
|
|
if (state.migrators.length) {
|
|
this.#onBrowserProfileSelectionChanged();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @typedef {object} ProgressState
|
|
* The migration progress state for a resource.
|
|
* @property {boolean} inProgress
|
|
* True if progress is still underway.
|
|
* @property {string} [message=undefined]
|
|
* An optional message to display underneath the resource in
|
|
* the progress dialog. This message is only shown when inProgress
|
|
* is `false`.
|
|
*/
|
|
|
|
/**
|
|
* Called when showing the progress / success page of the wizard.
|
|
*
|
|
* @param {object} state
|
|
* The state object passed into setState. The following properties are
|
|
* used:
|
|
* @param {Object<string, ProgressState>} state.progress
|
|
* An object whose keys match one of DISPLAYED_RESOURCE_TYPES.
|
|
*
|
|
* Any resource type not included in state.progress will be hidden.
|
|
*/
|
|
#onShowingProgress(state) {
|
|
// Any resource progress group not included in state.progress is hidden.
|
|
let resourceGroups = this.#shadowRoot.querySelectorAll(
|
|
".resource-progress-group"
|
|
);
|
|
let totalProgressGroups = Object.keys(state.progress).length;
|
|
let remainingProgressGroups = totalProgressGroups;
|
|
|
|
for (let group of resourceGroups) {
|
|
let resourceType = group.dataset.resourceType;
|
|
if (!state.progress.hasOwnProperty(resourceType)) {
|
|
group.hidden = true;
|
|
continue;
|
|
}
|
|
group.hidden = false;
|
|
|
|
let progressIcon = group.querySelector(".progress-icon");
|
|
let successText = group.querySelector(".success-text");
|
|
|
|
if (state.progress[resourceType].inProgress) {
|
|
document.l10n.setAttributes(
|
|
progressIcon,
|
|
"migration-wizard-progress-icon-in-progress"
|
|
);
|
|
progressIcon.classList.remove("completed");
|
|
// With no status text, we re-insert the so that the status
|
|
// text area does not fully collapse.
|
|
successText.appendChild(document.createTextNode("\u00A0"));
|
|
} else {
|
|
document.l10n.setAttributes(
|
|
progressIcon,
|
|
"migration-wizard-progress-icon-completed"
|
|
);
|
|
progressIcon.classList.add("completed");
|
|
successText.textContent = state.progress[resourceType].message;
|
|
remainingProgressGroups--;
|
|
}
|
|
}
|
|
|
|
let migrationDone = remainingProgressGroups == 0;
|
|
let headerL10nID = migrationDone
|
|
? "migration-wizard-progress-done-header"
|
|
: "migration-wizard-progress-header";
|
|
let header = this.#shadowRoot.getElementById("progress-header");
|
|
document.l10n.setAttributes(header, headerL10nID);
|
|
|
|
let progressPage = this.#shadowRoot.querySelector(
|
|
"div[name='page-progress']"
|
|
);
|
|
let doneButton = progressPage.querySelector("#done-button");
|
|
let cancelButton = progressPage.querySelector(".cancel-close");
|
|
doneButton.hidden = !migrationDone;
|
|
cancelButton.hidden = migrationDone;
|
|
}
|
|
|
|
/**
|
|
* Certain parts of the MigrationWizard need to be modified slightly
|
|
* in order to work properly with Storybook. This method should be called
|
|
* to apply those changes after changing state.
|
|
*/
|
|
#updateForStorybook() {
|
|
// The CSS mask used for the progress spinner cannot be loaded via
|
|
// chrome:// URIs in Storybook. We work around this by exposing the
|
|
// progress elements as custom parts that the MigrationWizard story
|
|
// can style on its own.
|
|
this.#shadowRoot.querySelectorAll(".progress-icon").forEach(progressEl => {
|
|
if (progressEl.classList.contains("completed")) {
|
|
progressEl.removeAttribute("part");
|
|
} else {
|
|
progressEl.setAttribute("part", "progress-spinner");
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Takes the current state of the selections page and bundles them
|
|
* up into a MigrationWizard:BeginMigration event that can be handled
|
|
* externally to perform the actual migration.
|
|
*/
|
|
#doImport() {
|
|
let option = this.#browserProfileSelector.options[
|
|
this.#browserProfileSelector.selectedIndex
|
|
];
|
|
let key = option.value;
|
|
let profile = option.profile;
|
|
let resourceTypeFields = this.#resourceTypeList.querySelectorAll(
|
|
"label[data-resource-type]"
|
|
);
|
|
let resourceTypes = [];
|
|
for (let resourceTypeField of resourceTypeFields) {
|
|
if (resourceTypeField.control.checked) {
|
|
resourceTypes.push(resourceTypeField.dataset.resourceType);
|
|
}
|
|
}
|
|
|
|
this.dispatchEvent(
|
|
new CustomEvent("MigrationWizard:BeginMigration", {
|
|
bubbles: true,
|
|
detail: {
|
|
key,
|
|
profile,
|
|
resourceTypes,
|
|
},
|
|
})
|
|
);
|
|
}
|
|
|
|
handleEvent(event) {
|
|
switch (event.type) {
|
|
case "click": {
|
|
if (event.target == this.#importButton) {
|
|
this.#doImport();
|
|
} else if (
|
|
event.target.classList.contains("cancel-close") ||
|
|
event.target.id == "done-button"
|
|
) {
|
|
this.dispatchEvent(
|
|
new CustomEvent("MigrationWizard:Close", { bubbles: true })
|
|
);
|
|
}
|
|
break;
|
|
}
|
|
case "change": {
|
|
if (event.target == this.#browserProfileSelector) {
|
|
this.#onBrowserProfileSelectionChanged();
|
|
} else if (event.target.classList.contains("select-all-checkbox")) {
|
|
let checkboxes = this.#shadowRoot.querySelectorAll(
|
|
'label[data-resource-type] > input[type="checkbox"]'
|
|
);
|
|
for (let checkbox of checkboxes) {
|
|
checkbox.checked = event.target.checked;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (globalThis.customElements) {
|
|
customElements.define("migration-wizard", MigrationWizard);
|
|
}
|