Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions docs/API-Reference/command/Commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -854,6 +854,12 @@ Checks for updates
## HELP\_AUTO\_UPDATE
Toggles auto update

**Kind**: global variable
<a name="HELP_MIGRATE_DATA"></a>

## HELP\_MIGRATE\_DATA
Migrates browser data from the legacy web origin

**Kind**: global variable
<a name="CMD_WORKINGSET_SORT_BY_ADDED"></a>

Expand Down
10 changes: 10 additions & 0 deletions src/extensionsIntegrated/MigrateAssist/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,15 @@ define(function (require, exports, module) {
*/
const MIGRATION_DONE_KEY = "migrateAssist.v1.done";

/**
* PhStore key recording that the user has already been asked. The automatic path prompts at most
* once, whatever the outcome: declining, closing the tab, or a transfer that broke half way all
* count as having been asked. Re-prompting on every boot after any of those would be a nag, and
* the Help menu entry is always there for a deliberate retry.
* @type {string}
*/
const MIGRATION_PROMPTED_KEY = "migrateAssist.v1.prompted";

/**
* Dev only override, so the whole cross origin flow can be exercised on one dev server.
* http://localhost:8000 and http://127.0.0.1:8000 are different origins with separate IndexedDB
Expand Down Expand Up @@ -203,6 +212,7 @@ define(function (require, exports, module) {
exports.SUNSET_DATE = SUNSET_DATE;
exports.TWA_STORE_URL = TWA_STORE_URL;
exports.MIGRATION_DONE_KEY = MIGRATION_DONE_KEY;
exports.MIGRATION_PROMPTED_KEY = MIGRATION_PROMPTED_KEY;
exports.getLegacyOrigin = getLegacyOrigin;
exports.getMigrateAssistURL = getMigrateAssistURL;
exports.getLegacyDomainName = getLegacyDomainName;
Expand Down
98 changes: 64 additions & 34 deletions src/extensionsIntegrated/MigrateAssist/migrator.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
define(function (require, exports, module) {
const Dialogs = require("widgets/Dialogs"),
DefaultDialogs = require("widgets/DefaultDialogs"),
NotificationUI = require("widgets/NotificationUI"),
Strings = require("strings"),
StringUtils = require("utils/StringUtils"),
Metrics = require("utils/Metrics"),
Expand All @@ -51,6 +52,7 @@ define(function (require, exports, module) {

const RESULT_MIGRATED = "migrated",
RESULT_DECLINED = "declined",
RESULT_INTERRUPTED = "interrupted",
RESULT_NOTHING = "nothing",
RESULT_UNREACHABLE = "unreachable";

Expand Down Expand Up @@ -235,32 +237,39 @@ define(function (require, exports, module) {
).getPromise();
}

/**
* Everything after the single up front question is reported at the bottom of the window rather
* than in another modal. The user opted in and went back to work; interrupting them again to say
* it finished would undo the point of moving progress out of a dialog in the first place.
*/
function _toast(title, message, style, $extra) {
const $content = $("<div>").append($("<div>").text(message));
if ($extra) {
$content.append($extra);
}
return NotificationUI.createToastFromTemplate(title, $content, {
dismissOnClick: false, // there is a close button, and a stray click must not eat the action
toastStyle: style
});
}

function _showCompletion(migratedFiles, failed) {
const message = failed.length
? StringUtils.format(Strings.MIGRATE_DONE_MESSAGE, migratedFiles) + "<br><br>"
+ StringUtils.format(Strings.MIGRATE_DONE_PARTIAL, failed.length)
: StringUtils.format(Strings.MIGRATE_DONE_MESSAGE, migratedFiles);
Dialogs.showModalDialog(
DefaultDialogs.DIALOG_ID_INFO,
Strings.MIGRATE_DONE_TITLE,
message,
[
{
className: Dialogs.DIALOG_BTN_CLASS_NORMAL,
id: Dialogs.DIALOG_BTN_CANCEL,
text: Strings.MIGRATE_RELOAD_LATER
},
{
className: Dialogs.DIALOG_BTN_CLASS_PRIMARY,
id: Dialogs.DIALOG_BTN_OK,
text: Strings.MIGRATE_RELOAD_NOW
}
]
).done(function (buttonId) {
if (buttonId === Dialogs.DIALOG_BTN_OK) {
CommandManager.execute(Commands.APP_RELOAD);
}
const $actions = $("<div>").addClass("migrate-assist-toast-actions");
const $reload = $("<button>").addClass("btn primary btn-mini")
.text(Strings.MIGRATE_RELOAD_NOW);
$reload.on("click", function () {
CommandManager.execute(Commands.APP_RELOAD);
});
$actions.append($reload);
if (failed.length) {
$actions.prepend($("<div>").addClass("migrate-assist-toast-detail")
.text(StringUtils.format(Strings.MIGRATE_DONE_PARTIAL, failed.length)));
}
_toast(Strings.MIGRATE_DONE_TITLE,
StringUtils.format(Strings.MIGRATE_DONE_MESSAGE, migratedFiles),
failed.length ? NotificationUI.NOTIFICATION_STYLES_CSS_CLASS.WARNING
: NotificationUI.NOTIFICATION_STYLES_CSS_CLASS.SUCCESS,
$actions);
}

/**
Expand Down Expand Up @@ -306,6 +315,7 @@ define(function (require, exports, module) {
migrationRunning = true;
const bridge = _createBridge();
let progress = null;
let userAccepted = false;
try {
const scan = await bridge.scan();
if (!scan.hasData || !scan.files.length) {
Expand All @@ -314,12 +324,17 @@ define(function (require, exports, module) {
return RESULT_NOTHING;
}

// Recorded before the dialog is even answered, so closing the tab on it counts as
// having been asked. The automatic path will not raise it again.
PhStore.setItem(Constants.MIGRATION_PROMPTED_KEY, { at: Date.now() });

// Asked once, before anything is copied. After this the user is left alone.
const choice = await _confirmStart(scan.files.length);
if (choice !== Dialogs.DIALOG_BTN_OK) {
Metrics.countEvent(Metrics.EVENT_TYPE.PLATFORM, "migrateAssist", "declined");
return RESULT_DECLINED;
}
userAccepted = true;

Metrics.countEvent(Metrics.EVENT_TYPE.PLATFORM, "migrateAssist",
manual ? "manualStart" : "autoStart");
Expand Down Expand Up @@ -366,11 +381,21 @@ define(function (require, exports, module) {
return RESULT_MIGRATED;
} catch (err) {
console.error("MigrateAssist: migration could not run", err);
Metrics.countEvent(Metrics.EVENT_TYPE.PLATFORM, "migrateAssist",
manual ? "manualUnreachable" : "autoUnreachable");
if (progress) {
progress.fail();
}
if (userAccepted) {
// They opted in and watched a task start, so a silent stop is not acceptable even on
// the automatic path. Whatever landed before the break stays; a retry overwrites it.
Metrics.countEvent(Metrics.EVENT_TYPE.PLATFORM, "migrateAssist", "interrupted");
_toast(Strings.MIGRATE_INTERRUPTED_TITLE,
StringUtils.format(Strings.MIGRATE_INTERRUPTED_MESSAGE,
Constants.getLegacyDomainName()),
NotificationUI.NOTIFICATION_STYLES_CSS_CLASS.ERROR);
return RESULT_INTERRUPTED;
}
Metrics.countEvent(Metrics.EVENT_TYPE.PLATFORM, "migrateAssist",
manual ? "manualUnreachable" : "autoUnreachable");
return RESULT_UNREACHABLE;
} finally {
migrationRunning = false;
Expand All @@ -392,6 +417,10 @@ define(function (require, exports, module) {
if (PhStore.getItem(Constants.MIGRATION_DONE_KEY)) {
return;
}
// Already asked once. Anything further is on the user, from the Help menu.
if (PhStore.getItem(Constants.MIGRATION_PROMPTED_KEY)) {
return;
}
// Once the legacy origin is gone there is nothing to probe, so the feature disables itself
// rather than opening a doomed iframe on every boot forever.
if (Constants.isPastSunset()) {
Expand All @@ -406,17 +435,18 @@ define(function (require, exports, module) {
*/
async function runManually() {
const result = await run(true);
if (result === RESULT_DECLINED) {
return; // the user said no, they do not need to be told what they just chose
if (result === RESULT_DECLINED || result === RESULT_INTERRUPTED) {
// Declining needs no confirmation, and an interrupted run has already said so itself.
return;
}
if (result === RESULT_NOTHING) {
Dialogs.showModalDialog(DefaultDialogs.DIALOG_ID_INFO,
Strings.MIGRATE_NOTHING_TITLE,
StringUtils.format(Strings.MIGRATE_NOTHING_MESSAGE, Constants.getLegacyDomainName()));
_toast(Strings.MIGRATE_NOTHING_TITLE,
StringUtils.format(Strings.MIGRATE_NOTHING_MESSAGE, Constants.getLegacyDomainName()),
NotificationUI.NOTIFICATION_STYLES_CSS_CLASS.INFO);
} else if (result === RESULT_UNREACHABLE) {
Dialogs.showModalDialog(DefaultDialogs.DIALOG_ID_ERROR,
Strings.MIGRATE_UNREACHABLE_TITLE,
StringUtils.format(Strings.MIGRATE_UNREACHABLE_MESSAGE, Constants.getLegacyDomainName()));
_toast(Strings.MIGRATE_UNREACHABLE_TITLE,
StringUtils.format(Strings.MIGRATE_UNREACHABLE_MESSAGE, Constants.getLegacyDomainName()),
NotificationUI.NOTIFICATION_STYLES_CSS_CLASS.ERROR);
}
}

Expand Down
2 changes: 2 additions & 0 deletions src/nls/root/strings.js
Original file line number Diff line number Diff line change
Expand Up @@ -2942,6 +2942,8 @@ define({
"MIGRATE_RELOAD_NOW": "Reload",
"MIGRATE_NOTHING_TITLE": "Nothing to bring over",
"MIGRATE_NOTHING_MESSAGE": "We could not find any projects, settings or extensions on {0} that need copying.",
"MIGRATE_INTERRUPTED_TITLE": "Migration did not finish",
"MIGRATE_INTERRUPTED_MESSAGE": "The connection to {0} was lost part way through. Anything already copied has been kept. You can pick up where this left off any time from the Help menu.",
"MIGRATE_UNREACHABLE_TITLE": "Could not reach the old site",
"MIGRATE_UNREACHABLE_MESSAGE": "{APP_NAME} could not connect to {0} to check for your data. Please check your connection and try again.",
"MIGRATE_START_MESSAGE": "Found {0} files on {1} to bring over. This runs in the background and you can keep working while it happens. It only needs to be done once.",
Expand Down
15 changes: 15 additions & 0 deletions src/styles/Extn-MigrateAssist.less
Original file line number Diff line number Diff line change
Expand Up @@ -64,3 +64,18 @@
background-position: -200% 0;
}
}

// Completion toast: the reload action and the partial-failure note sit under the message.
.migrate-assist-toast-actions {
margin-top: 10px;
display: flex;
align-items: center;
justify-content: flex-end;
gap: 8px;
}

.migrate-assist-toast-detail {
margin-right: auto;
font-size: 0.9em;
opacity: 0.8;
}
Loading