@metricinsights/pp-dev 1.5.0-beta.3 → 1.5.0-beta.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +198 -145
- package/dist/cjs/auth.provider-BT1sgf1x.js +2 -0
- package/dist/cjs/auth.provider-BT1sgf1x.js.map +1 -0
- package/dist/cjs/cli.js +1 -1
- package/dist/cjs/cli.js.map +1 -1
- package/dist/cjs/index-D9zJqX2X.js +2 -0
- package/dist/cjs/index-D9zJqX2X.js.map +1 -0
- package/dist/cjs/index.js +1 -1
- package/dist/cjs/migrate-CuzJuHC1.js +2 -0
- package/dist/cjs/migrate-CuzJuHC1.js.map +1 -0
- package/dist/cjs/package.json +2 -135
- package/dist/cjs/plugin.js +1 -1
- package/dist/cjs/plugin.js.map +1 -1
- package/dist/cjs/request-capture.middleware-Bihrs8O-.js +2 -0
- package/dist/cjs/request-capture.middleware-Bihrs8O-.js.map +1 -0
- package/dist/cjs/version-manifest-5tqQXsyA.js +2 -0
- package/dist/cjs/version-manifest-5tqQXsyA.js.map +1 -0
- package/dist/cjs/version-plugin-JOQRBtAn.js +2 -0
- package/dist/cjs/version-plugin-JOQRBtAn.js.map +1 -0
- package/dist/client/client.js +269 -125
- package/dist/client/client.js.map +1 -1
- package/dist/client/request-inspector.css +3 -0
- package/dist/client/request-inspector.css.map +1 -0
- package/dist/client/request-inspector.html +87 -0
- package/dist/client/request-inspector.js +527 -0
- package/dist/client/request-inspector.js.map +1 -0
- package/dist/client/variables-editor.css +3 -0
- package/dist/client/variables-editor.css.map +1 -0
- package/dist/client/variables-editor.html +44 -0
- package/dist/client/variables-editor.js +3474 -0
- package/dist/client/variables-editor.js.map +1 -0
- package/dist/esm/auth.provider-CMlTFLKr.js +2 -0
- package/dist/esm/auth.provider-CMlTFLKr.js.map +1 -0
- package/dist/esm/cli.js +1 -1
- package/dist/esm/cli.js.map +1 -1
- package/dist/esm/index-B4F5_U6S.js +2 -0
- package/dist/esm/index-B4F5_U6S.js.map +1 -0
- package/dist/esm/index.js +1 -1
- package/dist/esm/migrate-r4PvE5UL.js +2 -0
- package/dist/esm/{migrate-B2gJ62fm.js.map → migrate-r4PvE5UL.js.map} +1 -1
- package/dist/esm/package.json +2 -135
- package/dist/esm/plugin.js +1 -1
- package/dist/esm/plugin.js.map +1 -1
- package/dist/esm/request-capture.middleware-CYtDXIxP.js +2 -0
- package/dist/esm/request-capture.middleware-CYtDXIxP.js.map +1 -0
- package/dist/esm/version-manifest-BG-RUMD6.js +2 -0
- package/dist/esm/version-manifest-BG-RUMD6.js.map +1 -0
- package/dist/esm/version-plugin-BtTT_xRU.js +2 -0
- package/dist/esm/version-plugin-BtTT_xRU.js.map +1 -0
- package/dist/types/helpers.d.ts +29 -0
- package/dist/types/index.d.ts +15 -155
- package/dist/types/plugin.d.ts +8 -0
- package/dist/types/pp-dev-config-D889A-PO.d.ts +189 -0
- package/docs/features.md +2 -1
- package/package.json +26 -35
- package/dist/CHANGELOG.md +0 -442
- package/dist/README.md +0 -724
- package/dist/cjs/index-CCUrjlJE.js +0 -2
- package/dist/cjs/index-CCUrjlJE.js.map +0 -1
- package/dist/cjs/migrate-BYno__sx.js +0 -2
- package/dist/cjs/migrate-BYno__sx.js.map +0 -1
- package/dist/cjs/plugin-D_RXI7ru.js +0 -2
- package/dist/cjs/plugin-D_RXI7ru.js.map +0 -1
- package/dist/cjs/version-plugin-Bf4jLh17.js +0 -2
- package/dist/cjs/version-plugin-Bf4jLh17.js.map +0 -1
- package/dist/esm/index-BazQY12s.js +0 -2
- package/dist/esm/index-BazQY12s.js.map +0 -1
- package/dist/esm/migrate-B2gJ62fm.js +0 -2
- package/dist/esm/plugin-NIWxLreC.js +0 -2
- package/dist/esm/plugin-NIWxLreC.js.map +0 -1
- package/dist/esm/version-plugin-DwuZCHob.js +0 -2
- package/dist/esm/version-plugin-DwuZCHob.js.map +0 -1
package/dist/client/client.js
CHANGED
|
@@ -1,17 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* (
|
|
4
|
-
*
|
|
5
|
-
* custom-event wire shape Vite uses: `{ type: 'custom', event, data }`.
|
|
6
|
-
*
|
|
7
|
-
* Used only as a fallback: when `import.meta.hot` exists (Vite), that is used
|
|
8
|
-
* instead and this code never runs.
|
|
9
|
-
*/
|
|
10
|
-
/**
|
|
11
|
-
* WebSocket path the pp-dev Next.js server listens on for dev-panel messages.
|
|
12
|
-
* Kept in sync with `PP_DEV_HMR_WS_PATH` in `src/constants.ts` (server side).
|
|
2
|
+
* The WebSocket messages between the dev server and the dev panel, for both sides: the server
|
|
3
|
+
* (`src/lib/client.service.ts`, `src/lib/pp-ws-server.ts`) and the panel (`src/client/**`) import
|
|
4
|
+
* them from here, so they can't drift apart. No Node or DOM imports: the panel bundle includes it.
|
|
13
5
|
*/
|
|
6
|
+
/** WebSocket path the `pp-dev next` server listens on for dev-panel messages. */
|
|
14
7
|
const PP_DEV_HMR_WS_PATH = '/@pp-dev-hmr';
|
|
8
|
+
/** `template:sync:response` code for a sync requested while another one runs. */
|
|
9
|
+
const SYNC_IN_PROGRESS_CODE = 'SYNC_IN_PROGRESS';
|
|
10
|
+
|
|
15
11
|
const MAX_OUTBOX_SIZE = 50;
|
|
16
12
|
function resolveWebSocketUrl() {
|
|
17
13
|
const { protocol, host } = window.location;
|
|
@@ -23,6 +19,8 @@ function createPPDevHotContext() {
|
|
|
23
19
|
/** Outgoing messages queued while the socket is not OPEN; flushed on connect. */
|
|
24
20
|
const outbox = [];
|
|
25
21
|
let socket = null;
|
|
22
|
+
/** The connection state last reported to the panel, so each change is reported once. */
|
|
23
|
+
let reported = null;
|
|
26
24
|
let reconnectTimer = null;
|
|
27
25
|
let reconnectDelay = 1_000;
|
|
28
26
|
const MAX_RECONNECT_DELAY = 10_000;
|
|
@@ -60,6 +58,8 @@ function createPPDevHotContext() {
|
|
|
60
58
|
}
|
|
61
59
|
socket.addEventListener('open', () => {
|
|
62
60
|
reconnectDelay = 1_000;
|
|
61
|
+
reported = 'connected';
|
|
62
|
+
dispatch('vite:ws:connect', {});
|
|
63
63
|
flushOutbox();
|
|
64
64
|
});
|
|
65
65
|
socket.addEventListener('message', (ev) => {
|
|
@@ -76,6 +76,14 @@ function createPPDevHotContext() {
|
|
|
76
76
|
});
|
|
77
77
|
socket.addEventListener('close', () => {
|
|
78
78
|
socket = null;
|
|
79
|
+
// Same events as Vite's own HMR client, so the panel can tell a lost connection apart
|
|
80
|
+
// (e.g. a sync whose response will never arrive, or a click that can't be sent). Once per
|
|
81
|
+
// loss: each failed reconnect attempt is not a new one, but a first connection that never
|
|
82
|
+
// opened is.
|
|
83
|
+
if (reported !== 'disconnected') {
|
|
84
|
+
reported = 'disconnected';
|
|
85
|
+
dispatch('vite:ws:disconnect', {});
|
|
86
|
+
}
|
|
79
87
|
scheduleReconnect();
|
|
80
88
|
});
|
|
81
89
|
socket.addEventListener('error', () => {
|
|
@@ -119,6 +127,9 @@ function createPPDevHotContext() {
|
|
|
119
127
|
outbox.push(payload);
|
|
120
128
|
}
|
|
121
129
|
},
|
|
130
|
+
// Not part of Vite's API: lets the panel tell an open connection from one still connecting, whose
|
|
131
|
+
// queued messages would go out whenever it opens (see `src/client/sync-button.ts`).
|
|
132
|
+
isConnected: () => socket?.readyState === WebSocket.OPEN,
|
|
122
133
|
// Unused by the dev-panel client; provided as no-ops to satisfy the shape.
|
|
123
134
|
accept: noop,
|
|
124
135
|
acceptExports: noop,
|
|
@@ -644,6 +655,37 @@ function initPanelSettings($panel, controller, hooks) {
|
|
|
644
655
|
});
|
|
645
656
|
}
|
|
646
657
|
|
|
658
|
+
/**
|
|
659
|
+
* The rules for a saved user's name, which is also its host (`<name>.localhost`), for both sides:
|
|
660
|
+
* the server checks them (`src/lib/user-registry.ts`) and the dev panel's Users popover
|
|
661
|
+
* (`src/client/users-panel.ts`) checks them as you type. No Node or DOM imports: the panel bundle
|
|
662
|
+
* includes it.
|
|
663
|
+
*/
|
|
664
|
+
/** Longest user name; well under the 63 characters a DNS label allows. */
|
|
665
|
+
const MAX_USER_NAME_LENGTH = 32;
|
|
666
|
+
/**
|
|
667
|
+
* A DNS label: lowercase latin letters, digits and `-`, starting with a letter and not ending with
|
|
668
|
+
* `-`. Also used as an input's `pattern`, which browsers compile with the `v` flag: there an
|
|
669
|
+
* unescaped `-` in a class is a syntax error, and the whole pattern was silently ignored.
|
|
670
|
+
*/
|
|
671
|
+
const USER_NAME_PATTERN = String.raw `[a-z](?:[a-z0-9\-]*[a-z0-9])?`;
|
|
672
|
+
const USER_NAME_REGEXP = new RegExp(`^(?:${USER_NAME_PATTERN})$`);
|
|
673
|
+
/** The suffix of a user's host: `viewer` works on `viewer.localhost`. */
|
|
674
|
+
const USER_HOST_SUFFIX = '.localhost';
|
|
675
|
+
/** Why `name` can't be a user name, or `undefined` if it can. */
|
|
676
|
+
function getUserNameError(name) {
|
|
677
|
+
if (!name) {
|
|
678
|
+
return 'The name is required';
|
|
679
|
+
}
|
|
680
|
+
if (name.length > MAX_USER_NAME_LENGTH) {
|
|
681
|
+
return `The name must be at most ${MAX_USER_NAME_LENGTH} characters long`;
|
|
682
|
+
}
|
|
683
|
+
if (!USER_NAME_REGEXP.test(name)) {
|
|
684
|
+
return 'The name may contain only lowercase latin letters, digits and "-", must start with a letter and must not end with "-"';
|
|
685
|
+
}
|
|
686
|
+
return undefined;
|
|
687
|
+
}
|
|
688
|
+
|
|
647
689
|
/**
|
|
648
690
|
* The dev panel's "Users" popover (PP-4597): the user of this tab, the saved users (open as, log
|
|
649
691
|
* in, make default, remove) and adding a user by API token or by username + password. Each user
|
|
@@ -651,7 +693,8 @@ function initPanelSettings($panel, controller, hooks) {
|
|
|
651
693
|
*/
|
|
652
694
|
const USERS_API = '/@api/users';
|
|
653
695
|
const REFRESH_INTERVAL = 3000;
|
|
654
|
-
|
|
696
|
+
// The server's name rules (src/shared/user-names.ts), not a copy of them.
|
|
697
|
+
const USER_HOST_REGEXP = new RegExp(`^(${USER_NAME_PATTERN})${USER_HOST_SUFFIX.replace('.', '\\.')}$`);
|
|
655
698
|
/** Marks the user this tab works as, apart from the radio, which picks localhost's user. */
|
|
656
699
|
const THIS_TAB_BADGE = '<span class="pp-dev-info__users-badge">this tab</span>';
|
|
657
700
|
const STATE_LABELS = {
|
|
@@ -823,7 +866,7 @@ function initUsersPanel($panel, hooks) {
|
|
|
823
866
|
const name = escapeHtml(user.name);
|
|
824
867
|
if (user.kind === 'token' && user.name === editing) {
|
|
825
868
|
return `<form class="pp-dev-info__users-row pp-dev-info__users-edit" data-user="${name}">
|
|
826
|
-
<input class="pp-dev-info__users-input pp-dev-info__users-edit-name" value="${name}" maxlength="
|
|
869
|
+
<input class="pp-dev-info__users-input pp-dev-info__users-edit-name" value="${name}" maxlength="${MAX_USER_NAME_LENGTH}" pattern="${USER_NAME_PATTERN}"
|
|
827
870
|
autocomplete="off" spellcheck="false" aria-label="Name" data-focus-key="${name}:edit-name"
|
|
828
871
|
title="Lowercase latin letters, digits and -, starting with a letter. The user opens on <name>.localhost." />
|
|
829
872
|
<input class="pp-dev-info__users-input pp-dev-info__users-edit-token" type="password" autocomplete="off"
|
|
@@ -868,6 +911,9 @@ function initUsersPanel($panel, hooks) {
|
|
|
868
911
|
$open.addEventListener('click', (ev) => {
|
|
869
912
|
ev.preventDefault();
|
|
870
913
|
const target = $open.dataset.open;
|
|
914
|
+
if (!target) {
|
|
915
|
+
return;
|
|
916
|
+
}
|
|
871
917
|
window.open($open.dataset.raw ? target : samePageOn(target), '_blank');
|
|
872
918
|
});
|
|
873
919
|
});
|
|
@@ -880,7 +926,7 @@ function initUsersPanel($panel, hooks) {
|
|
|
880
926
|
$list.querySelectorAll('[data-edit]').forEach(($edit) => {
|
|
881
927
|
$edit.addEventListener('click', (ev) => {
|
|
882
928
|
ev.preventDefault();
|
|
883
|
-
startEditing($edit.dataset.edit);
|
|
929
|
+
startEditing($edit.dataset.edit ?? null);
|
|
884
930
|
});
|
|
885
931
|
});
|
|
886
932
|
$list.querySelector('[data-cancel-edit]')?.addEventListener('click', (ev) => {
|
|
@@ -892,8 +938,11 @@ function initUsersPanel($panel, hooks) {
|
|
|
892
938
|
ev.preventDefault();
|
|
893
939
|
const name = editing;
|
|
894
940
|
const $form = ev.currentTarget;
|
|
895
|
-
|
|
896
|
-
|
|
941
|
+
if (!name) {
|
|
942
|
+
return;
|
|
943
|
+
}
|
|
944
|
+
const newName = $form.querySelector('.pp-dev-info__users-edit-name')?.value.trim() ?? '';
|
|
945
|
+
const token = $form.querySelector('.pp-dev-info__users-edit-token')?.value.trim() ?? '';
|
|
897
946
|
const response = await request('PATCH', `${USERS_API}/${encodeURIComponent(name)}`, {
|
|
898
947
|
name: newName,
|
|
899
948
|
...(token ? { token } : {}),
|
|
@@ -914,7 +963,8 @@ function initUsersPanel($panel, hooks) {
|
|
|
914
963
|
$list.querySelectorAll('[data-remove]').forEach(($remove) => {
|
|
915
964
|
$remove.addEventListener('click', async (ev) => {
|
|
916
965
|
ev.preventDefault();
|
|
917
|
-
|
|
966
|
+
const name = $remove.dataset.remove;
|
|
967
|
+
if (name && (await request('DELETE', `${USERS_API}/${encodeURIComponent(name)}`))) {
|
|
918
968
|
await refresh();
|
|
919
969
|
}
|
|
920
970
|
});
|
|
@@ -995,20 +1045,34 @@ function initUsersPanel($panel, hooks) {
|
|
|
995
1045
|
const $name = $popover.querySelector('.pp-dev-info__users-name-input');
|
|
996
1046
|
const $token = $popover.querySelector('.pp-dev-info__users-token');
|
|
997
1047
|
const $submit = $popover.querySelector('.pp-dev-info__users-submit');
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1048
|
+
if (!$name || !$token || !$submit) {
|
|
1049
|
+
return;
|
|
1050
|
+
}
|
|
1051
|
+
const name = $name.value.trim();
|
|
1052
|
+
const nameError = getUserNameError(name);
|
|
1053
|
+
if (nameError) {
|
|
1054
|
+
setError(nameError);
|
|
1055
|
+
return;
|
|
1056
|
+
}
|
|
1057
|
+
const body = addKind === 'token' ? { name, token: $token.value } : { name, kind: 'session' };
|
|
1058
|
+
// A session user logs in on a tab of its own. Opened here, while the click still counts as the
|
|
1059
|
+
// user's: opened after the request, Safari and Chrome blocked it as a popup.
|
|
1060
|
+
const loginTab = addKind === 'session' ? window.open('', '_blank') : null;
|
|
1001
1061
|
$submit.disabled = true;
|
|
1002
1062
|
const response = await request('POST', USERS_API, body);
|
|
1003
1063
|
$submit.disabled = false;
|
|
1004
1064
|
if (!response) {
|
|
1065
|
+
loginTab?.close();
|
|
1005
1066
|
return;
|
|
1006
1067
|
}
|
|
1007
1068
|
const { user } = (await response.json());
|
|
1008
1069
|
$name.value = '';
|
|
1009
1070
|
$token.value = '';
|
|
1010
|
-
if (user.kind === 'session' && user.loginUrl) {
|
|
1011
|
-
|
|
1071
|
+
if (user.kind === 'session' && user.loginUrl && loginTab) {
|
|
1072
|
+
loginTab.location.href = user.loginUrl;
|
|
1073
|
+
}
|
|
1074
|
+
else {
|
|
1075
|
+
loginTab?.close();
|
|
1012
1076
|
}
|
|
1013
1077
|
await refresh();
|
|
1014
1078
|
};
|
|
@@ -1033,8 +1097,8 @@ function initUsersPanel($panel, hooks) {
|
|
|
1033
1097
|
<button type="button" class="pp-dev-info__theme-btn pp-dev-info__users-kind" data-kind="token">API token</button>
|
|
1034
1098
|
<button type="button" class="pp-dev-info__theme-btn pp-dev-info__users-kind" data-kind="session">Username + password</button>
|
|
1035
1099
|
</div>
|
|
1036
|
-
<input class="pp-dev-info__users-input pp-dev-info__users-name-input" placeholder="Name, e.g. viewer" maxlength="
|
|
1037
|
-
autocomplete="off" spellcheck="false" pattern="
|
|
1100
|
+
<input class="pp-dev-info__users-input pp-dev-info__users-name-input" placeholder="Name, e.g. viewer" maxlength="${MAX_USER_NAME_LENGTH}"
|
|
1101
|
+
autocomplete="off" spellcheck="false" pattern="${USER_NAME_PATTERN}"
|
|
1038
1102
|
title="Lowercase latin letters, digits and -, starting with a letter. The user opens on <name>.localhost." />
|
|
1039
1103
|
<input class="pp-dev-info__users-input pp-dev-info__users-token" type="password" placeholder="API token" autocomplete="off" />
|
|
1040
1104
|
<button type="submit" class="pp-dev-info__vars-btn pp-dev-info__users-submit">Add user</button>
|
|
@@ -1050,7 +1114,7 @@ function initUsersPanel($panel, hooks) {
|
|
|
1050
1114
|
renderKind();
|
|
1051
1115
|
});
|
|
1052
1116
|
});
|
|
1053
|
-
$popover.querySelector('.pp-dev-info__users-add')
|
|
1117
|
+
$popover.querySelector('.pp-dev-info__users-add')?.addEventListener('submit', (ev) => {
|
|
1054
1118
|
ev.preventDefault();
|
|
1055
1119
|
submit();
|
|
1056
1120
|
});
|
|
@@ -1139,6 +1203,175 @@ function createPopupElement(opts) {
|
|
|
1139
1203
|
$popup.appendChild($popupContent);
|
|
1140
1204
|
return $popup;
|
|
1141
1205
|
}
|
|
1206
|
+
/**
|
|
1207
|
+
* Calls `onElapsed` once the page has been visible for `duration` ms in all (time in a hidden tab
|
|
1208
|
+
* doesn't count). Returns a function that stops waiting, which also removes the page's
|
|
1209
|
+
* `visibilitychange` listener: one used to stay behind for every popup ever shown.
|
|
1210
|
+
*/
|
|
1211
|
+
function startAutoDismiss(duration, onElapsed) {
|
|
1212
|
+
let remaining = duration;
|
|
1213
|
+
let lastUpdate = Date.now();
|
|
1214
|
+
let visible = true;
|
|
1215
|
+
let frame;
|
|
1216
|
+
const tick = () => {
|
|
1217
|
+
frame = undefined;
|
|
1218
|
+
if (!visible) {
|
|
1219
|
+
return;
|
|
1220
|
+
}
|
|
1221
|
+
const now = Date.now();
|
|
1222
|
+
remaining -= now - lastUpdate;
|
|
1223
|
+
lastUpdate = now;
|
|
1224
|
+
if (remaining <= 0) {
|
|
1225
|
+
stop();
|
|
1226
|
+
onElapsed();
|
|
1227
|
+
return;
|
|
1228
|
+
}
|
|
1229
|
+
frame = requestAnimationFrame(tick);
|
|
1230
|
+
};
|
|
1231
|
+
const onVisibilityChange = () => {
|
|
1232
|
+
visible = !document.hidden;
|
|
1233
|
+
if (visible) {
|
|
1234
|
+
lastUpdate = Date.now();
|
|
1235
|
+
frame ??= requestAnimationFrame(tick);
|
|
1236
|
+
}
|
|
1237
|
+
};
|
|
1238
|
+
const stop = () => {
|
|
1239
|
+
document.removeEventListener('visibilitychange', onVisibilityChange);
|
|
1240
|
+
if (frame !== undefined) {
|
|
1241
|
+
cancelAnimationFrame(frame);
|
|
1242
|
+
frame = undefined;
|
|
1243
|
+
}
|
|
1244
|
+
};
|
|
1245
|
+
document.addEventListener('visibilitychange', onVisibilityChange);
|
|
1246
|
+
frame = requestAnimationFrame(tick);
|
|
1247
|
+
return stop;
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
const TITLE_AVAILABLE = 'Sync template';
|
|
1251
|
+
const TITLE_UNAVAILABLE = 'Sync is unavailable on this instance';
|
|
1252
|
+
/**
|
|
1253
|
+
* Wires the dev panel's "Sync template" button to the dev server.
|
|
1254
|
+
*
|
|
1255
|
+
* The button is disabled while a sync runs, so a double click can't start a second build, and is
|
|
1256
|
+
* enabled again by whatever ends the sync: its response, or a lost connection. It stays disabled
|
|
1257
|
+
* only when the server says sync is unavailable on this instance (`config.canSync === false`);
|
|
1258
|
+
* a failed sync (MI unreachable, a build error) can simply be retried.
|
|
1259
|
+
*/
|
|
1260
|
+
function initSyncButton(hot, $button, deps) {
|
|
1261
|
+
const reload = deps.reload ?? (() => window.location.reload());
|
|
1262
|
+
let available = !$button.disabled && !$button.classList.contains('disabled');
|
|
1263
|
+
let syncing = false;
|
|
1264
|
+
// Vite's connection is taken as open until reported lost: it may have opened before these handlers
|
|
1265
|
+
// were registered, and its client reloads the page once a lost one is back. The `pp-dev next`
|
|
1266
|
+
// transport says itself whether its socket is open, also while the first one is still connecting.
|
|
1267
|
+
let connected = true;
|
|
1268
|
+
const isConnected = () => connected && (hot.isConnected?.() ?? true);
|
|
1269
|
+
const applyState = () => {
|
|
1270
|
+
$button.disabled = syncing || !available;
|
|
1271
|
+
$button.classList.toggle('syncing', syncing);
|
|
1272
|
+
$button.classList.toggle('disabled', !available);
|
|
1273
|
+
$button.title = available ? TITLE_AVAILABLE : TITLE_UNAVAILABLE;
|
|
1274
|
+
if (syncing) {
|
|
1275
|
+
$button.setAttribute('aria-busy', 'true');
|
|
1276
|
+
}
|
|
1277
|
+
else {
|
|
1278
|
+
$button.removeAttribute('aria-busy');
|
|
1279
|
+
}
|
|
1280
|
+
};
|
|
1281
|
+
const endSync = () => {
|
|
1282
|
+
syncing = false;
|
|
1283
|
+
applyState();
|
|
1284
|
+
};
|
|
1285
|
+
hot.on('client:config:update', (data) => {
|
|
1286
|
+
if (typeof data?.config?.canSync !== 'boolean') {
|
|
1287
|
+
return;
|
|
1288
|
+
}
|
|
1289
|
+
// The server sends `canSync: true` at the start of every sync: it must not enable the
|
|
1290
|
+
// button while that sync runs, which `applyState` takes care of.
|
|
1291
|
+
available = data.config.canSync;
|
|
1292
|
+
applyState();
|
|
1293
|
+
});
|
|
1294
|
+
hot.on('template:sync:action-required', async (payload) => {
|
|
1295
|
+
// The spinner keeps running while a confirmation modal is shown: the sync is still in
|
|
1296
|
+
// progress and only ends on `template:sync:response`.
|
|
1297
|
+
const approved = await deps.confirmModal({
|
|
1298
|
+
title: payload.title,
|
|
1299
|
+
content: payload.content,
|
|
1300
|
+
confirmText: payload.confirmText,
|
|
1301
|
+
cancelText: payload.cancelText,
|
|
1302
|
+
});
|
|
1303
|
+
hot.send('template:sync:action-response', {
|
|
1304
|
+
requestId: payload.requestId,
|
|
1305
|
+
approved,
|
|
1306
|
+
});
|
|
1307
|
+
});
|
|
1308
|
+
hot.on('template:sync:response', (payload) => {
|
|
1309
|
+
deps.closeAllConfirmModals();
|
|
1310
|
+
if ('error' in payload && payload.config?.canSync === false) {
|
|
1311
|
+
available = false;
|
|
1312
|
+
}
|
|
1313
|
+
endSync();
|
|
1314
|
+
if ('cancelled' in payload && payload.cancelled) {
|
|
1315
|
+
deps.infoPopup({ title: 'Sync cancelled', content: payload.message, type: 'warning' });
|
|
1316
|
+
return;
|
|
1317
|
+
}
|
|
1318
|
+
if ('error' in payload && typeof payload.error !== 'undefined') {
|
|
1319
|
+
if (payload.code === SYNC_IN_PROGRESS_CODE) {
|
|
1320
|
+
deps.infoPopup({ title: 'Sync already running', content: payload.error, type: 'warning' });
|
|
1321
|
+
return;
|
|
1322
|
+
}
|
|
1323
|
+
deps.infoPopup({ title: 'Sync error', content: payload.error, type: 'danger' });
|
|
1324
|
+
if (payload.refresh) {
|
|
1325
|
+
setTimeout(reload);
|
|
1326
|
+
}
|
|
1327
|
+
return;
|
|
1328
|
+
}
|
|
1329
|
+
if ('syncedAt' in payload && typeof payload.syncedAt !== 'undefined') {
|
|
1330
|
+
deps.infoPopup({
|
|
1331
|
+
title: 'Sync success',
|
|
1332
|
+
content: `Synced at ${new Date(payload.syncedAt).toLocaleString()}.\nBackup filename: ${payload.backupFilename}`,
|
|
1333
|
+
type: 'success',
|
|
1334
|
+
});
|
|
1335
|
+
}
|
|
1336
|
+
});
|
|
1337
|
+
// A response sent to a socket that has closed never arrives. Without this, the button would
|
|
1338
|
+
// spin, disabled, until the page is reloaded.
|
|
1339
|
+
hot.on('vite:ws:connect', () => {
|
|
1340
|
+
connected = true;
|
|
1341
|
+
});
|
|
1342
|
+
hot.on('vite:ws:disconnect', () => {
|
|
1343
|
+
connected = false;
|
|
1344
|
+
if (!syncing) {
|
|
1345
|
+
return;
|
|
1346
|
+
}
|
|
1347
|
+
deps.closeAllConfirmModals();
|
|
1348
|
+
endSync();
|
|
1349
|
+
deps.infoPopup({
|
|
1350
|
+
title: 'Sync result unknown',
|
|
1351
|
+
content: 'The connection to the dev server was lost during the sync. Check its terminal for the result before syncing again.',
|
|
1352
|
+
type: 'warning',
|
|
1353
|
+
});
|
|
1354
|
+
});
|
|
1355
|
+
$button.addEventListener('click', (ev) => {
|
|
1356
|
+
ev.preventDefault();
|
|
1357
|
+
if (syncing || !available) {
|
|
1358
|
+
return;
|
|
1359
|
+
}
|
|
1360
|
+
// Sent now, the request would wait for a reconnect with the button spinning, and start a sync
|
|
1361
|
+
// whenever that happens. Say so instead, and let the click be repeated once connected.
|
|
1362
|
+
if (!isConnected()) {
|
|
1363
|
+
deps.infoPopup({
|
|
1364
|
+
title: 'No connection',
|
|
1365
|
+
content: 'The dev server is not connected. Check that it is running, then try again.',
|
|
1366
|
+
type: 'warning',
|
|
1367
|
+
});
|
|
1368
|
+
return;
|
|
1369
|
+
}
|
|
1370
|
+
syncing = true;
|
|
1371
|
+
applyState();
|
|
1372
|
+
hot.send('template:sync', {});
|
|
1373
|
+
});
|
|
1374
|
+
}
|
|
1142
1375
|
|
|
1143
1376
|
/// <reference types="vite/client" />
|
|
1144
1377
|
// Applied unconditionally (even if the panel itself is hidden) — this is what makes the
|
|
@@ -1211,7 +1444,16 @@ function animatePopup($popup, type) {
|
|
|
1211
1444
|
function infoPopup(opts) {
|
|
1212
1445
|
const $popup = createPopupElement(opts);
|
|
1213
1446
|
const $closeButton = $popup.querySelector('.pp-dev-info__popup-title-close');
|
|
1447
|
+
let removed = false;
|
|
1448
|
+
let stopAutoDismiss;
|
|
1449
|
+
// Once only: closing it by hand used to leave its timer running, which removed it again and
|
|
1450
|
+
// counted it out of `activePopups` twice.
|
|
1214
1451
|
const removePopup = async () => {
|
|
1452
|
+
if (removed) {
|
|
1453
|
+
return;
|
|
1454
|
+
}
|
|
1455
|
+
removed = true;
|
|
1456
|
+
stopAutoDismiss?.();
|
|
1215
1457
|
await animatePopup($popup, 'exit');
|
|
1216
1458
|
$popup.remove();
|
|
1217
1459
|
updatePopupPositions();
|
|
@@ -1224,32 +1466,7 @@ function infoPopup(opts) {
|
|
|
1224
1466
|
animatePopup($popup, 'enter');
|
|
1225
1467
|
const duration = opts.duration ?? 10000;
|
|
1226
1468
|
if (duration > 0) {
|
|
1227
|
-
|
|
1228
|
-
let lastUpdate = Date.now();
|
|
1229
|
-
let isVisible = true;
|
|
1230
|
-
const scheduleDismiss = () => {
|
|
1231
|
-
if (!isVisible) {
|
|
1232
|
-
return;
|
|
1233
|
-
}
|
|
1234
|
-
const now = Date.now();
|
|
1235
|
-
const elapsed = now - lastUpdate;
|
|
1236
|
-
remainingTime -= elapsed;
|
|
1237
|
-
lastUpdate = now;
|
|
1238
|
-
if (remainingTime <= 0) {
|
|
1239
|
-
removePopup();
|
|
1240
|
-
return;
|
|
1241
|
-
}
|
|
1242
|
-
requestAnimationFrame(scheduleDismiss);
|
|
1243
|
-
};
|
|
1244
|
-
// Handle visibility change
|
|
1245
|
-
document.addEventListener('visibilitychange', () => {
|
|
1246
|
-
isVisible = !document.hidden;
|
|
1247
|
-
if (isVisible) {
|
|
1248
|
-
lastUpdate = Date.now();
|
|
1249
|
-
requestAnimationFrame(scheduleDismiss);
|
|
1250
|
-
}
|
|
1251
|
-
});
|
|
1252
|
-
requestAnimationFrame(scheduleDismiss);
|
|
1469
|
+
stopAutoDismiss = startAutoDismiss(duration, removePopup);
|
|
1253
1470
|
}
|
|
1254
1471
|
return { close: removePopup };
|
|
1255
1472
|
}
|
|
@@ -1372,82 +1589,9 @@ if (hot) {
|
|
|
1372
1589
|
hot.on('redirect', (data) => {
|
|
1373
1590
|
window.location.href = data.url;
|
|
1374
1591
|
});
|
|
1375
|
-
hot.on('client:config:update', (data) => {
|
|
1376
|
-
if (typeof data.config?.canSync === 'boolean') {
|
|
1377
|
-
if (data.config.canSync) {
|
|
1378
|
-
const $syncButton = document.getElementById('sync-template');
|
|
1379
|
-
if ($syncButton) {
|
|
1380
|
-
$syncButton.disabled = false;
|
|
1381
|
-
$syncButton.classList.remove('disabled');
|
|
1382
|
-
$syncButton.title = 'Sync template';
|
|
1383
|
-
}
|
|
1384
|
-
}
|
|
1385
|
-
else {
|
|
1386
|
-
const $syncButton = document.getElementById('sync-template');
|
|
1387
|
-
if ($syncButton) {
|
|
1388
|
-
$syncButton.disabled = true;
|
|
1389
|
-
$syncButton.classList.add('disabled');
|
|
1390
|
-
$syncButton.title = 'Sync is unavailable on this instance';
|
|
1391
|
-
}
|
|
1392
|
-
}
|
|
1393
|
-
}
|
|
1394
|
-
});
|
|
1395
1592
|
const $syncButton = document.getElementById('sync-template');
|
|
1396
1593
|
if ($syncButton) {
|
|
1397
|
-
hot
|
|
1398
|
-
// Keep the sync spinner running while a confirmation modal is shown — the sync
|
|
1399
|
-
// process is still in progress and only ends on `template:sync:response`.
|
|
1400
|
-
const approved = await confirmModal({
|
|
1401
|
-
title: payload.title,
|
|
1402
|
-
content: payload.content,
|
|
1403
|
-
confirmText: payload.confirmText,
|
|
1404
|
-
cancelText: payload.cancelText,
|
|
1405
|
-
});
|
|
1406
|
-
hot.send('template:sync:action-response', {
|
|
1407
|
-
requestId: payload.requestId,
|
|
1408
|
-
approved,
|
|
1409
|
-
});
|
|
1410
|
-
});
|
|
1411
|
-
hot.on('template:sync:response', (payload) => {
|
|
1412
|
-
closeAllConfirmModals();
|
|
1413
|
-
$syncButton.classList.remove('syncing');
|
|
1414
|
-
if ('cancelled' in payload && payload.cancelled) {
|
|
1415
|
-
infoPopup({
|
|
1416
|
-
title: 'Sync cancelled',
|
|
1417
|
-
content: payload.message,
|
|
1418
|
-
type: 'warning',
|
|
1419
|
-
});
|
|
1420
|
-
}
|
|
1421
|
-
else if ('error' in payload && typeof payload.error !== 'undefined') {
|
|
1422
|
-
infoPopup({
|
|
1423
|
-
title: 'Sync error',
|
|
1424
|
-
content: payload.error,
|
|
1425
|
-
type: 'danger',
|
|
1426
|
-
});
|
|
1427
|
-
if (payload.refresh) {
|
|
1428
|
-
setTimeout(() => {
|
|
1429
|
-
window.location.reload();
|
|
1430
|
-
});
|
|
1431
|
-
}
|
|
1432
|
-
else {
|
|
1433
|
-
$syncButton.disabled = true;
|
|
1434
|
-
$syncButton.classList.add('disabled');
|
|
1435
|
-
$syncButton.title = 'Sync is unavailable on this instance';
|
|
1436
|
-
}
|
|
1437
|
-
}
|
|
1438
|
-
else if ('syncedAt' in payload && typeof payload.syncedAt !== 'undefined') {
|
|
1439
|
-
infoPopup({
|
|
1440
|
-
title: 'Sync success',
|
|
1441
|
-
content: `Synced at ${new Date(payload.syncedAt).toLocaleString()}.\nBackup filename: ${payload.backupFilename}`,
|
|
1442
|
-
type: 'success',
|
|
1443
|
-
});
|
|
1444
|
-
}
|
|
1445
|
-
});
|
|
1446
|
-
$syncButton.addEventListener('click', (ev) => {
|
|
1447
|
-
ev.preventDefault();
|
|
1448
|
-
$syncButton.classList.add('syncing');
|
|
1449
|
-
hot.send('template:sync', {});
|
|
1450
|
-
});
|
|
1594
|
+
initSyncButton(hot, $syncButton, { infoPopup, confirmModal, closeAllConfirmModals });
|
|
1451
1595
|
}
|
|
1452
1596
|
hot.on('page-variables:reload:response', (payload) => {
|
|
1453
1597
|
pendingReloadVariablesPopup?.close();
|