dsh-qoder-connect 0.2.1 → 0.2.3-alpha.1
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/LICENSE +21 -21
- package/README.en.md +177 -173
- package/README.md +178 -174
- package/cordis.patch.yml +6 -6
- package/lib/bin.js +1 -1
- package/lib/client.js +366 -190
- package/lib/index.d.ts +15 -2
- package/lib/index.js +1 -1
- package/lib/{variants-CjWW4lVI.js → variants-CU8HDUwi.js} +162 -20
- package/package.json +120 -120
package/lib/client.js
CHANGED
|
@@ -158,11 +158,11 @@ window.__ModuleLoader__.load({
|
|
|
158
158
|
global: void 0
|
|
159
159
|
};
|
|
160
160
|
/**
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
*
|
|
161
|
+
* Store one variant's document and sign-in fact — the raw publish, with no
|
|
162
|
+
* sequence bookkeeping. Direct callers go through {@link noteQuotaStatus};
|
|
163
|
+
* read results go through {@link publishRead}.
|
|
164
164
|
*/
|
|
165
|
-
function
|
|
165
|
+
function storeStatus(variantId, status) {
|
|
166
166
|
if (variantId === "qoder" && statusDocuments.cn !== status) {
|
|
167
167
|
statusDocuments.cn = status;
|
|
168
168
|
statusFetchedAt.cn = Date.now();
|
|
@@ -174,6 +174,16 @@ window.__ModuleLoader__.load({
|
|
|
174
174
|
}
|
|
175
175
|
noteQuotaSignIn(variantId, status.status === "signed-in");
|
|
176
176
|
}
|
|
177
|
+
/**
|
|
178
|
+
* Publish one variant's document as an AUTHORITATIVE fact: the host just
|
|
179
|
+
* answered a write, or a surface synthesized the outcome (clearing a PAT).
|
|
180
|
+
* It supersedes every read that started before it, so a response predating
|
|
181
|
+
* the write cannot roll the screen back.
|
|
182
|
+
*/
|
|
183
|
+
function noteQuotaStatus(variantId, status) {
|
|
184
|
+
storeStatus(variantId, status);
|
|
185
|
+
if (variantId === "qoder" || variantId === "qoder-global") publishedSeq[variantId] = readSeq[variantId];
|
|
186
|
+
}
|
|
177
187
|
/** Read one variant's latest status document. */
|
|
178
188
|
function quotaStatus(variantId) {
|
|
179
189
|
return variantId === "qoder" ? statusDocuments.cn : statusDocuments.global;
|
|
@@ -207,6 +217,186 @@ window.__ModuleLoader__.load({
|
|
|
207
217
|
if (fetchedAt === void 0 || document === void 0) return false;
|
|
208
218
|
return Date.now() - fetchedAt < maxAgeMs;
|
|
209
219
|
}
|
|
220
|
+
/** The last read failure per variant, cleared by the next successful read. */
|
|
221
|
+
const readFailures = {
|
|
222
|
+
qoder: void 0,
|
|
223
|
+
"qoder-global": void 0
|
|
224
|
+
};
|
|
225
|
+
/** Read one variant's last read failure, if the last read failed. */
|
|
226
|
+
function quotaReadFailure(variantId) {
|
|
227
|
+
return variantId === "qoder" ? readFailures.qoder : readFailures["qoder-global"];
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Cadence an expanded card reads at — one request per variant per minute,
|
|
231
|
+
* whatever the number of cards showing it.
|
|
232
|
+
*/
|
|
233
|
+
const CARD_POLL_MS = 6e4;
|
|
234
|
+
/** The status route each variant polls (mirrors the host's locked route table). */
|
|
235
|
+
const STATUS_ROUTES = {
|
|
236
|
+
qoder: QODER_STATUS_PATH,
|
|
237
|
+
"qoder-global": QODER_GLOBAL_STATUS_PATH
|
|
238
|
+
};
|
|
239
|
+
/**
|
|
240
|
+
* Who needs a variant live. A `card` holder (an expanded settings card) reads
|
|
241
|
+
* every minute; a `surface` holder (an enabled sidebar card, an open quota
|
|
242
|
+
* panel) reads at the settings interval. A variant with any holder is read at
|
|
243
|
+
* the TIGHTEST cadence its holders ask for — an expanded card beside an
|
|
244
|
+
* enabled sidebar card costs ONE request a minute between them, not two.
|
|
245
|
+
*/
|
|
246
|
+
const demand = {
|
|
247
|
+
qoder: {
|
|
248
|
+
cards: 0,
|
|
249
|
+
surfaces: 0
|
|
250
|
+
},
|
|
251
|
+
"qoder-global": {
|
|
252
|
+
cards: 0,
|
|
253
|
+
surfaces: 0
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
/** The single ticker every holder shares; undefined while nothing is live. */
|
|
257
|
+
let ticker;
|
|
258
|
+
/**
|
|
259
|
+
* Read-start sequences per variant, and the sequence of the last PUBLISHED
|
|
260
|
+
* read. A read publishes only when no later-started read has published yet,
|
|
261
|
+
* so a response that predates a newer one can never roll the screen back —
|
|
262
|
+
* the guard the per-card `readSeq` used to provide, now shared across every
|
|
263
|
+
* surface of the variant.
|
|
264
|
+
*/
|
|
265
|
+
const readSeq = {
|
|
266
|
+
qoder: 0,
|
|
267
|
+
"qoder-global": 0
|
|
268
|
+
};
|
|
269
|
+
const publishedSeq = {
|
|
270
|
+
qoder: 0,
|
|
271
|
+
"qoder-global": 0
|
|
272
|
+
};
|
|
273
|
+
/** Whether the page is visible; a hidden page reads nothing and catches up on return. */
|
|
274
|
+
function pageHidden() {
|
|
275
|
+
return typeof document !== "undefined" && document.hidden === true;
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Publish one read's outcome under the sequence guard: a read publishes only
|
|
279
|
+
* when no read that started later has published yet, so a response that
|
|
280
|
+
* predates a newer one — whichever surface started it — can never roll the
|
|
281
|
+
* screen back.
|
|
282
|
+
*/
|
|
283
|
+
function publishRead(variantId, seq, outcome) {
|
|
284
|
+
if (seq <= publishedSeq[variantId]) return;
|
|
285
|
+
if ("doc" in outcome) {
|
|
286
|
+
storeStatus(variantId, outcome.doc);
|
|
287
|
+
publishReadFailure(variantId, void 0);
|
|
288
|
+
} else publishReadFailure(variantId, outcome.failure);
|
|
289
|
+
publishedSeq[variantId] = seq;
|
|
290
|
+
}
|
|
291
|
+
/** Publish (or clear) one variant's read failure; identity-stable between changes. */
|
|
292
|
+
function publishReadFailure(variantId, failure) {
|
|
293
|
+
const previous = readFailures[variantId];
|
|
294
|
+
if (previous === failure) return;
|
|
295
|
+
if (previous !== void 0 && failure !== void 0 && previous.kind === failure.kind && (previous.kind !== "transport" || failure.kind !== "transport" || previous.message === failure.message)) return;
|
|
296
|
+
readFailures[variantId] = failure;
|
|
297
|
+
bump();
|
|
298
|
+
}
|
|
299
|
+
/** A 200 whose body is not a status document — reported as `invalid`, not as a transport failure. */
|
|
300
|
+
var InvalidStatusDocumentError = class extends Error {};
|
|
301
|
+
/**
|
|
302
|
+
* Fetch one variant's status document and publish it (or its failure) for
|
|
303
|
+
* every surface. The request is never shared with another read: a tick reads
|
|
304
|
+
* each live variant once, and an explicit read must be able to postdate a
|
|
305
|
+
* write, so it starts its own request.
|
|
306
|
+
*
|
|
307
|
+
* @returns the published document, or undefined when the read failed.
|
|
308
|
+
*/
|
|
309
|
+
async function readVariant(variantId) {
|
|
310
|
+
const seq = ++readSeq[variantId];
|
|
311
|
+
try {
|
|
312
|
+
const response = await fetch(STATUS_ROUTES[variantId], {
|
|
313
|
+
headers: { accept: "application/json" },
|
|
314
|
+
credentials: "same-origin"
|
|
315
|
+
});
|
|
316
|
+
const body = await response.json().catch(() => void 0);
|
|
317
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
318
|
+
if (!isQoderWebStatus(body)) throw new InvalidStatusDocumentError();
|
|
319
|
+
publishRead(variantId, seq, { doc: body });
|
|
320
|
+
return body;
|
|
321
|
+
} catch (error) {
|
|
322
|
+
publishRead(variantId, seq, { failure: error instanceof InvalidStatusDocumentError ? { kind: "invalid" } : {
|
|
323
|
+
kind: "transport",
|
|
324
|
+
message: error instanceof Error ? error.message : "request failed"
|
|
325
|
+
} });
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
/** Whether a variant is due for its next read: held at all, past its cadence, and not parked signed-out. */
|
|
330
|
+
function dueForRead(variantId) {
|
|
331
|
+
const holders = demand[variantId];
|
|
332
|
+
if (holders.cards === 0 && holders.surfaces === 0) return false;
|
|
333
|
+
if (holders.cards > 0 && holders.surfaces === 0 && quotaStatus(variantId)?.status === "signed-out") return false;
|
|
334
|
+
if (holders.cards > 0) return true;
|
|
335
|
+
return !quotaStatusIsFresh(variantId, Math.max(CARD_POLL_MS, pollIntervalMs));
|
|
336
|
+
}
|
|
337
|
+
/** One tick: read every variant that is due, once, whatever the number of holders. */
|
|
338
|
+
function tickQuotaPoll() {
|
|
339
|
+
if (pageHidden()) return;
|
|
340
|
+
for (const variantId of ["qoder", "qoder-global"]) if (dueForRead(variantId)) readVariant(variantId);
|
|
341
|
+
}
|
|
342
|
+
function startTicker() {
|
|
343
|
+
if (ticker !== void 0) return;
|
|
344
|
+
ticker = window.setInterval(tickQuotaPoll, CARD_POLL_MS);
|
|
345
|
+
if (typeof document !== "undefined") document.addEventListener("visibilitychange", tickQuotaPoll);
|
|
346
|
+
}
|
|
347
|
+
function stopTicker() {
|
|
348
|
+
if (ticker === void 0) return;
|
|
349
|
+
window.clearInterval(ticker);
|
|
350
|
+
ticker = void 0;
|
|
351
|
+
if (typeof document !== "undefined") document.removeEventListener("visibilitychange", tickQuotaPoll);
|
|
352
|
+
}
|
|
353
|
+
/** Record one holder of `kind` and start the shared ticker when it is the first. */
|
|
354
|
+
function addHolder(variantId, kind) {
|
|
355
|
+
const holders = demand[variantId];
|
|
356
|
+
holders[kind] += 1;
|
|
357
|
+
if (holders.cards + holders.surfaces === 1) startTicker();
|
|
358
|
+
}
|
|
359
|
+
/** Drop one holder and stop the shared ticker when nothing is live any more. */
|
|
360
|
+
function removeHolder(variantId, kind) {
|
|
361
|
+
const holders = demand[variantId];
|
|
362
|
+
holders[kind] = Math.max(0, holders[kind] - 1);
|
|
363
|
+
if (demand.qoder.cards + demand.qoder.surfaces === 0 && demand["qoder-global"].cards + demand["qoder-global"].surfaces === 0) stopTicker();
|
|
364
|
+
}
|
|
365
|
+
/** Sync the holder count with the ticker, reading at once when the document is due. */
|
|
366
|
+
function acquire(variantId, kind) {
|
|
367
|
+
const readNow = !quotaStatusIsFresh(variantId, kind === "cards" ? CARD_POLL_MS : Math.max(CARD_POLL_MS, pollIntervalMs));
|
|
368
|
+
addHolder(variantId, kind);
|
|
369
|
+
if (readNow) readVariant(variantId);
|
|
370
|
+
return () => {
|
|
371
|
+
removeHolder(variantId, kind);
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* Acquire one variant as an EXPANDED CARD shows it: reads every minute while
|
|
376
|
+
* any card holds it, stops on a signed-out document (nothing changes without
|
|
377
|
+
* a PAT). The release disposer belongs to the card's effect lifetime.
|
|
378
|
+
*/
|
|
379
|
+
function acquireLiveCard(variantId) {
|
|
380
|
+
return acquire(variantId, "cards");
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* Acquire one variant as a CADENCE-BOUND SURFACE shows it (an enabled sidebar
|
|
384
|
+
* card, an open quota panel): reads at the settings interval, whatever the
|
|
385
|
+
* sign-in state. The release disposer belongs to the surface's lifetime.
|
|
386
|
+
*/
|
|
387
|
+
function acquireLiveSurface(variantId) {
|
|
388
|
+
return acquire(variantId, "surfaces");
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* Force one shared read now. The explicit path — a manual refresh, or the
|
|
392
|
+
* re-read that follows a write — always starts its own request, so its answer
|
|
393
|
+
* can postdate the write that preceded it.
|
|
394
|
+
*
|
|
395
|
+
* @returns whether the read produced a document.
|
|
396
|
+
*/
|
|
397
|
+
function refreshQuotaStatus(variantId) {
|
|
398
|
+
return readVariant(variantId).then((doc) => doc !== void 0);
|
|
399
|
+
}
|
|
210
400
|
//#endregion
|
|
211
401
|
//#region src/client/QuotaSettingsCard.tsx
|
|
212
402
|
/**
|
|
@@ -706,7 +896,6 @@ window.__ModuleLoader__.load({
|
|
|
706
896
|
};
|
|
707
897
|
/** Both cards, in display order (China first). */
|
|
708
898
|
const QODER_CARD_VARIANTS = [QODER_CN_CARD, QODER_GLOBAL_CARD];
|
|
709
|
-
const POLL_INTERVAL_MS = 6e4;
|
|
710
899
|
const cardStyle = {
|
|
711
900
|
listStyle: "none",
|
|
712
901
|
borderWidth: "0.5px",
|
|
@@ -1584,18 +1773,15 @@ window.__ModuleLoader__.load({
|
|
|
1584
1773
|
}
|
|
1585
1774
|
/** Render Qoder PAT state, quota, catalog, and context capacities as one expandable card. */
|
|
1586
1775
|
function QoderPluginCard(props) {
|
|
1587
|
-
const { t, scope, signedIn, variant, unified } = props;
|
|
1776
|
+
const { t, scope, signedIn, variant, unified, defaultOpen } = props;
|
|
1588
1777
|
if (t === void 0) throw new Error("Qoder plugin card requires its translation function");
|
|
1589
1778
|
const isUnified = unified === true;
|
|
1590
1779
|
const liveSignIn = (0, react.useSyncExternalStore)(onQuotaSettingsChange, quotaSignInState);
|
|
1591
1780
|
const [activeVariantId, setActiveVariantId] = (0, react.useState)("qoder");
|
|
1592
1781
|
const currentVariant = isUnified ? activeVariantId === "qoder" ? QODER_CN_CARD : QODER_GLOBAL_CARD : variant ?? QODER_CN_CARD;
|
|
1593
|
-
const [open, setOpen] = (0, react.useState)(
|
|
1782
|
+
const [open, setOpen] = (0, react.useState)(defaultOpen === true);
|
|
1594
1783
|
const [hovered, setHovered] = (0, react.useState)(false);
|
|
1595
1784
|
const [headerFocused, setHeaderFocused] = (0, react.useState)(false);
|
|
1596
|
-
const [status, setStatus] = (0, react.useState)();
|
|
1597
|
-
const [signedInState, setSignedInState] = (0, react.useState)();
|
|
1598
|
-
const [readFailure, setReadFailure] = (0, react.useState)();
|
|
1599
1785
|
const [busy, setBusy] = (0, react.useState)(false);
|
|
1600
1786
|
const [patDraft, setPatDraft] = (0, react.useState)("");
|
|
1601
1787
|
const [replacing, setReplacing] = (0, react.useState)(false);
|
|
@@ -1607,8 +1793,9 @@ window.__ModuleLoader__.load({
|
|
|
1607
1793
|
const [checkingIn, setCheckingIn] = (0, react.useState)(false);
|
|
1608
1794
|
const [clearingLogs, setClearingLogs] = (0, react.useState)(false);
|
|
1609
1795
|
const [checkInNotice, setCheckInNotice] = (0, react.useState)();
|
|
1796
|
+
/** A write action's failure, reported on the same line as a read failure. */
|
|
1797
|
+
const [writeFailure, setWriteFailure] = (0, react.useState)();
|
|
1610
1798
|
const mounted = (0, react.useRef)(true);
|
|
1611
|
-
const readSeq = (0, react.useRef)(0);
|
|
1612
1799
|
const manualControllers = (0, react.useRef)(/* @__PURE__ */ new Set());
|
|
1613
1800
|
(0, react.useEffect)(() => {
|
|
1614
1801
|
mounted.current = true;
|
|
@@ -1623,83 +1810,70 @@ window.__ModuleLoader__.load({
|
|
|
1623
1810
|
manualControllers.current.add(controller);
|
|
1624
1811
|
return controller;
|
|
1625
1812
|
}, []);
|
|
1813
|
+
/**
|
|
1814
|
+
* The shown variant's shared read state.
|
|
1815
|
+
*
|
|
1816
|
+
* The document and the last read failure arrive through ONE revision bump,
|
|
1817
|
+
* whichever surface read them: the shared poller reads each live variant
|
|
1818
|
+
* once for every surface that shows it, so this card expanded here and the
|
|
1819
|
+
* same card in the plugin-manager panel cost one request a minute between
|
|
1820
|
+
* them, not two. Before the first document lands a failed read shows as an
|
|
1821
|
+
* error document; beside a document already on screen it shows as a notice
|
|
1822
|
+
* under the account row, exactly as the per-card fetch did.
|
|
1823
|
+
*/
|
|
1824
|
+
(0, react.useSyncExternalStore)(onQuotaSettingsChange, quotaSettingsRevision);
|
|
1825
|
+
const readFailure = quotaReadFailure(currentVariant.id);
|
|
1826
|
+
const sharedDocument = quotaStatus(currentVariant.id);
|
|
1827
|
+
const status = readFailure !== void 0 && sharedDocument === void 0 ? {
|
|
1828
|
+
status: "error",
|
|
1829
|
+
message: readFailure.kind === "invalid" ? t("statusResponseInvalid") : readFailure.message
|
|
1830
|
+
} : sharedDocument;
|
|
1831
|
+
const signedInState = status?.status === "signed-in" ? true : status?.status === "signed-out" ? false : void 0;
|
|
1626
1832
|
const authKey = status === void 0 || status.status === "error" ? void 0 : status.authKey;
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
return false;
|
|
1660
|
-
}
|
|
1661
|
-
}, [currentVariant.statusPath, t]);
|
|
1833
|
+
/**
|
|
1834
|
+
* Force one shared read of the shown variant.
|
|
1835
|
+
*
|
|
1836
|
+
* The store's read always starts its own request — an explicit read must be
|
|
1837
|
+
* able to postdate the write or the click that precedes it — and publishes
|
|
1838
|
+
* for every surface of the variant, so a manual refresh here updates the
|
|
1839
|
+
* sidebar cards and the quota panel too. A successful read also clears a
|
|
1840
|
+
* write action's notice, exactly as the per-card fetch did.
|
|
1841
|
+
*/
|
|
1842
|
+
const refresh = (0, react.useCallback)(async () => {
|
|
1843
|
+
const ok = await refreshQuotaStatus(currentVariant.id);
|
|
1844
|
+
if (ok) setWriteFailure(void 0);
|
|
1845
|
+
return ok;
|
|
1846
|
+
}, [currentVariant.id]);
|
|
1847
|
+
/**
|
|
1848
|
+
* The expanded card is a poll holder of the shown variant.
|
|
1849
|
+
*
|
|
1850
|
+
* The shared ticker reads the variant once a minute while any card holds it
|
|
1851
|
+
* and parks it on a signed-out document (nothing changes without a PAT);
|
|
1852
|
+
* collapsing or unmounting releases the demand. Opening re-reads at once
|
|
1853
|
+
* only when the shared document is past its minute — a second card opening
|
|
1854
|
+
* beside a live one shows the numbers already on screen.
|
|
1855
|
+
*/
|
|
1856
|
+
(0, react.useEffect)(() => {
|
|
1857
|
+
if (!open) return void 0;
|
|
1858
|
+
return acquireLiveCard(currentVariant.id);
|
|
1859
|
+
}, [open, currentVariant.id]);
|
|
1860
|
+
/**
|
|
1861
|
+
* Opening (or switching variant) clears the transient entry state — a
|
|
1862
|
+
* discarded draft must not wait for the user on the next open. The document
|
|
1863
|
+
* itself is shared state now and stays on screen while the card re-opens.
|
|
1864
|
+
*/
|
|
1662
1865
|
(0, react.useEffect)(() => {
|
|
1663
1866
|
if (!open) return;
|
|
1664
|
-
setStatus(void 0);
|
|
1665
|
-
setSignedInState(void 0);
|
|
1666
|
-
setReadFailure(void 0);
|
|
1667
1867
|
setPatDraft("");
|
|
1668
1868
|
setReplacing(false);
|
|
1669
1869
|
setPatError(void 0);
|
|
1670
1870
|
setPatNotice(void 0);
|
|
1671
|
-
|
|
1672
|
-
refresh(controller.signal);
|
|
1673
|
-
return () => {
|
|
1674
|
-
controller.abort();
|
|
1675
|
-
};
|
|
1676
|
-
}, [
|
|
1677
|
-
open,
|
|
1678
|
-
currentVariant.statusPath,
|
|
1679
|
-
refresh
|
|
1680
|
-
]);
|
|
1681
|
-
(0, react.useEffect)(() => {
|
|
1682
|
-
if (!open || signedInState === false) return;
|
|
1683
|
-
const controller = new AbortController();
|
|
1684
|
-
const timer = window.setInterval(() => {
|
|
1685
|
-
refresh(controller.signal);
|
|
1686
|
-
}, POLL_INTERVAL_MS);
|
|
1687
|
-
return () => {
|
|
1688
|
-
window.clearInterval(timer);
|
|
1689
|
-
controller.abort();
|
|
1690
|
-
};
|
|
1691
|
-
}, [
|
|
1692
|
-
open,
|
|
1693
|
-
refresh,
|
|
1694
|
-
signedInState
|
|
1695
|
-
]);
|
|
1871
|
+
}, [open, currentVariant.id]);
|
|
1696
1872
|
const manualRefresh = async () => {
|
|
1697
1873
|
setBusy(true);
|
|
1698
|
-
const controller = trackController();
|
|
1699
1874
|
try {
|
|
1700
|
-
await refresh(
|
|
1875
|
+
await refresh();
|
|
1701
1876
|
} finally {
|
|
1702
|
-
manualControllers.current.delete(controller);
|
|
1703
1877
|
if (mounted.current) setBusy(false);
|
|
1704
1878
|
}
|
|
1705
1879
|
};
|
|
@@ -1729,14 +1903,14 @@ window.__ModuleLoader__.load({
|
|
|
1729
1903
|
});
|
|
1730
1904
|
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
1731
1905
|
} catch (error) {
|
|
1732
|
-
if (mounted.current && controller.signal.aborted !== true)
|
|
1906
|
+
if (mounted.current && controller.signal.aborted !== true) setWriteFailure(error instanceof Error ? error.message : t("requestFailed"));
|
|
1733
1907
|
manualControllers.current.delete(controller);
|
|
1734
1908
|
return;
|
|
1735
1909
|
} finally {
|
|
1736
1910
|
if (mounted.current) setBusy(false);
|
|
1737
1911
|
}
|
|
1738
1912
|
try {
|
|
1739
|
-
await refresh(
|
|
1913
|
+
await refresh();
|
|
1740
1914
|
} finally {
|
|
1741
1915
|
manualControllers.current.delete(controller);
|
|
1742
1916
|
}
|
|
@@ -1777,7 +1951,7 @@ window.__ModuleLoader__.load({
|
|
|
1777
1951
|
if (mounted.current) setCheckingIn(false);
|
|
1778
1952
|
}
|
|
1779
1953
|
try {
|
|
1780
|
-
await refresh(
|
|
1954
|
+
await refresh();
|
|
1781
1955
|
} finally {
|
|
1782
1956
|
manualControllers.current.delete(controller);
|
|
1783
1957
|
}
|
|
@@ -1813,7 +1987,7 @@ window.__ModuleLoader__.load({
|
|
|
1813
1987
|
if (mounted.current) setClearingLogs(false);
|
|
1814
1988
|
}
|
|
1815
1989
|
try {
|
|
1816
|
-
await refresh(
|
|
1990
|
+
await refresh();
|
|
1817
1991
|
} finally {
|
|
1818
1992
|
manualControllers.current.delete(controller);
|
|
1819
1993
|
}
|
|
@@ -1856,9 +2030,9 @@ window.__ModuleLoader__.load({
|
|
|
1856
2030
|
const reason = typeof value === "object" && value !== null && "reason" in value ? String(value["reason"]) : t("requestFailed");
|
|
1857
2031
|
throw new Error(reason);
|
|
1858
2032
|
}
|
|
1859
|
-
await refresh(
|
|
2033
|
+
await refresh();
|
|
1860
2034
|
} catch (error) {
|
|
1861
|
-
if (mounted.current && controller.signal.aborted !== true)
|
|
2035
|
+
if (mounted.current && controller.signal.aborted !== true) setWriteFailure(error instanceof Error ? error.message : t("requestFailed"));
|
|
1862
2036
|
} finally {
|
|
1863
2037
|
manualControllers.current.delete(controller);
|
|
1864
2038
|
if (mounted.current) setBusy(false);
|
|
@@ -1916,7 +2090,7 @@ window.__ModuleLoader__.load({
|
|
|
1916
2090
|
setReplacing(false);
|
|
1917
2091
|
setPatNotice(t("patSaved"));
|
|
1918
2092
|
noteQuotaSignIn(currentVariant.id, true);
|
|
1919
|
-
await refresh(
|
|
2093
|
+
await refresh();
|
|
1920
2094
|
} catch (error) {
|
|
1921
2095
|
if (mounted.current && controller.signal.aborted !== true) setPatError(t("patSaveFailed", { message: error instanceof Error ? error.message : t("requestFailed") }));
|
|
1922
2096
|
} finally {
|
|
@@ -1960,15 +2134,11 @@ window.__ModuleLoader__.load({
|
|
|
1960
2134
|
return;
|
|
1961
2135
|
}
|
|
1962
2136
|
setPatDraft("");
|
|
1963
|
-
|
|
2137
|
+
noteQuotaStatus(currentVariant.id, {
|
|
1964
2138
|
status: "signed-out",
|
|
1965
2139
|
authKey: key
|
|
1966
|
-
};
|
|
1967
|
-
|
|
1968
|
-
setSignedInState(false);
|
|
1969
|
-
noteQuotaStatus(currentVariant.id, signedOutDoc);
|
|
1970
|
-
noteQuotaSignIn(currentVariant.id, false);
|
|
1971
|
-
await refresh(controller.signal);
|
|
2140
|
+
});
|
|
2141
|
+
await refresh();
|
|
1972
2142
|
} catch (error) {
|
|
1973
2143
|
if (mounted.current && controller.signal.aborted !== true) setPatError(t("patSaveFailed", { message: error instanceof Error ? error.message : t("requestFailed") }));
|
|
1974
2144
|
} finally {
|
|
@@ -2208,9 +2378,9 @@ window.__ModuleLoader__.load({
|
|
|
2208
2378
|
})] })
|
|
2209
2379
|
]
|
|
2210
2380
|
}),
|
|
2211
|
-
readFailure === void 0 || signedInState === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2381
|
+
readFailure === void 0 && writeFailure === void 0 || signedInState === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2212
2382
|
style: errorStyle,
|
|
2213
|
-
children: t("statusRefreshFailed", { message: readFailure })
|
|
2383
|
+
children: t("statusRefreshFailed", { message: readFailure !== void 0 ? readFailure.kind === "invalid" ? t("statusResponseInvalid") : readFailure.message : writeFailure })
|
|
2214
2384
|
}),
|
|
2215
2385
|
status?.status === "signed-in" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
2216
2386
|
status.pat === void 0 ? null : patSummaryLine(status.pat),
|
|
@@ -3032,9 +3202,11 @@ window.__ModuleLoader__.load({
|
|
|
3032
3202
|
* row, and opens a dashboard in the layout's keyed `main` slot on click. In
|
|
3033
3203
|
* the 56px rail it collapses to a 36px icon button carrying the ring.
|
|
3034
3204
|
*
|
|
3035
|
-
* Data comes from the
|
|
3036
|
-
*
|
|
3037
|
-
*
|
|
3205
|
+
* Data comes from the shared status store: the card acquires its variant and
|
|
3206
|
+
* the store's ONE ticker reads each live variant once for every surface
|
|
3207
|
+
* (this card, the settings cards, the dashboard), paused while the page is
|
|
3208
|
+
* hidden; strings come from the `panel.qoder-quota` locale namespace; classes
|
|
3209
|
+
* come from `./quota-styles.ts` (`qdp-` prefix).
|
|
3038
3210
|
*/
|
|
3039
3211
|
/** Fallback translator: renders keys bare rather than throwing unbound. */
|
|
3040
3212
|
const fallbackT = (key) => key;
|
|
@@ -3138,51 +3310,14 @@ window.__ModuleLoader__.load({
|
|
|
3138
3310
|
const variantId = statusPath !== void 0 ? variantOfStatusPath(statusPath) : "qoder";
|
|
3139
3311
|
const nameKey = variantId === "qoder-global" ? "quotaCardGlobal" : "quotaCardCN";
|
|
3140
3312
|
const wide = props.wide !== false;
|
|
3141
|
-
const [failed, setFailed] = (0, react.useState)(false);
|
|
3142
3313
|
(0, react.useSyncExternalStore)(onQuotaSettingsChange, quotaSettingsRevision);
|
|
3143
3314
|
const enabled = variantId === "qoder" ? quotaToggles().cn : quotaToggles().global;
|
|
3315
|
+
const failed = quotaReadFailure(variantId) !== void 0;
|
|
3144
3316
|
const status = quotaStatus(variantId);
|
|
3145
3317
|
const signedIn = status?.status === "signed-in";
|
|
3146
3318
|
(0, react.useEffect)(() => {
|
|
3147
3319
|
if (statusPath === void 0 || !enabled) return void 0;
|
|
3148
|
-
|
|
3149
|
-
let timer;
|
|
3150
|
-
const controller = new AbortController();
|
|
3151
|
-
const refresh = async () => {
|
|
3152
|
-
try {
|
|
3153
|
-
const response = await fetch(statusPath, {
|
|
3154
|
-
signal: controller.signal,
|
|
3155
|
-
headers: { accept: "application/json" }
|
|
3156
|
-
});
|
|
3157
|
-
const body = await response.json();
|
|
3158
|
-
if (disposed) return;
|
|
3159
|
-
if (!response.ok || !isQoderWebStatus(body)) {
|
|
3160
|
-
setFailed(true);
|
|
3161
|
-
return;
|
|
3162
|
-
}
|
|
3163
|
-
setFailed(false);
|
|
3164
|
-
noteQuotaStatus(variantId, body);
|
|
3165
|
-
} catch {
|
|
3166
|
-
if (!disposed) setFailed(true);
|
|
3167
|
-
}
|
|
3168
|
-
};
|
|
3169
|
-
const isHidden = () => typeof document !== "undefined" && document.hidden;
|
|
3170
|
-
const loop = () => {
|
|
3171
|
-
if (isHidden()) return;
|
|
3172
|
-
refresh();
|
|
3173
|
-
};
|
|
3174
|
-
timer = window.setInterval(loop, Math.max(6e4, quotaPollMs()));
|
|
3175
|
-
refresh();
|
|
3176
|
-
const onVisible = () => {
|
|
3177
|
-
if (!isHidden()) loop();
|
|
3178
|
-
};
|
|
3179
|
-
if (typeof document !== "undefined") document.addEventListener("visibilitychange", onVisible);
|
|
3180
|
-
return () => {
|
|
3181
|
-
disposed = true;
|
|
3182
|
-
controller.abort();
|
|
3183
|
-
if (timer !== void 0) window.clearInterval(timer);
|
|
3184
|
-
if (typeof document !== "undefined") document.removeEventListener("visibilitychange", onVisible);
|
|
3185
|
-
};
|
|
3320
|
+
return acquireLiveSurface(variantId);
|
|
3186
3321
|
}, [
|
|
3187
3322
|
statusPath,
|
|
3188
3323
|
enabled,
|
|
@@ -3301,10 +3436,11 @@ window.__ModuleLoader__.load({
|
|
|
3301
3436
|
* pattern): CN and international are separate accounts with separate package
|
|
3302
3437
|
* lists, so mixing them into one column would misattribute every number.
|
|
3303
3438
|
*
|
|
3304
|
-
* The panel
|
|
3305
|
-
*
|
|
3306
|
-
*
|
|
3307
|
-
*
|
|
3439
|
+
* The panel holds its shown variant live through the shared store — it reads
|
|
3440
|
+
* at once on open (unless the shared document is still within the settings
|
|
3441
|
+
* interval, which a click then shows as cached numbers) and the shared ticker
|
|
3442
|
+
* keeps it current from then on, so a user opening the panel never waits for
|
|
3443
|
+
* a poll and never meets a stale "sign in" because nothing had read yet.
|
|
3308
3444
|
*/
|
|
3309
3445
|
function QuotaDashboard(props) {
|
|
3310
3446
|
const { t = fallbackT, statusPaths, refresh, close, useQuotaDashboard, onVariantPicked } = props;
|
|
@@ -3316,7 +3452,7 @@ window.__ModuleLoader__.load({
|
|
|
3316
3452
|
const activeVariant = variantOfStatusPath(activePathResolved);
|
|
3317
3453
|
const status = quotaStatus(activeVariant);
|
|
3318
3454
|
const loading = state.loading;
|
|
3319
|
-
const fetchedAt =
|
|
3455
|
+
const fetchedAt = quotaStatusFetchedAt(activeVariant);
|
|
3320
3456
|
const credits = status !== void 0 && "credits" in status ? status.credits : void 0;
|
|
3321
3457
|
const rows = credits === void 0 ? [] : buildPackageRows(credits.accounts ?? []);
|
|
3322
3458
|
const nameKey = activeVariant === "qoder-global" ? "quotaCardGlobal" : "quotaCardCN";
|
|
@@ -4078,6 +4214,20 @@ window.__ModuleLoader__.load({
|
|
|
4078
4214
|
/** The shared block's title, fixed by the contract so all three plugins agree. */
|
|
4079
4215
|
const SHARED_SECTION_LABEL = "插件设置";
|
|
4080
4216
|
/**
|
|
4217
|
+
* The plugin manager's bundle-configuration seat and this bundle's key in it.
|
|
4218
|
+
*
|
|
4219
|
+
* The sidebar's Plugins panel (the `plugins` main panel the Host's plugin
|
|
4220
|
+
* manager registers) renders one bundle's own configuration on the bundle's
|
|
4221
|
+
* detail page — between the description and the component rows — through the
|
|
4222
|
+
* `plugins.bundle.config` keyed slot, keyed by the bundle's npm package name.
|
|
4223
|
+
* The key is what the page's configuration ledger reads to decide whether the
|
|
4224
|
+
* configuration section shows at all, so it must spell this package's name
|
|
4225
|
+
* exactly; the slot itself is declared by the Host (see
|
|
4226
|
+
* `plugin-manager-slots.ts` for the structural restatement).
|
|
4227
|
+
*/
|
|
4228
|
+
const PLUGIN_MANAGER_SLOT = "plugins.bundle.config";
|
|
4229
|
+
const PLUGIN_MANAGER_KEY = "dsh-qoder-connect";
|
|
4230
|
+
/**
|
|
4081
4231
|
* The shared 《插件设置》 section the 0.1.7 card list lives in.
|
|
4082
4232
|
*
|
|
4083
4233
|
* It renders nothing but its child slot: every connect plugin contributes its
|
|
@@ -4102,7 +4252,8 @@ window.__ModuleLoader__.load({
|
|
|
4102
4252
|
}
|
|
4103
4253
|
/**
|
|
4104
4254
|
* Register card copy, the shared quota-settings card, the two variant cards,
|
|
4105
|
-
*
|
|
4255
|
+
* the sidebar quota cards, and the same unified card on this bundle's page in
|
|
4256
|
+
* the sidebar's Plugins panel.
|
|
4106
4257
|
*
|
|
4107
4258
|
* The entire body is wrapped so that a DSH slot-API breaking change (for
|
|
4108
4259
|
* example the rc.6→rc.7 `id`→`key` / `order`→`priority` rename) degrades
|
|
@@ -4157,20 +4308,27 @@ window.__ModuleLoader__.load({
|
|
|
4157
4308
|
applySnapshot();
|
|
4158
4309
|
scope.subscribe(applySnapshot);
|
|
4159
4310
|
};
|
|
4160
|
-
|
|
4161
|
-
|
|
4162
|
-
|
|
4163
|
-
|
|
4164
|
-
|
|
4165
|
-
|
|
4166
|
-
|
|
4167
|
-
|
|
4168
|
-
|
|
4169
|
-
|
|
4170
|
-
|
|
4171
|
-
|
|
4172
|
-
|
|
4173
|
-
|
|
4311
|
+
/**
|
|
4312
|
+
* The unified card's inject face, shared by its two surfaces: the shared
|
|
4313
|
+
* 《插件设置》 block and this bundle's own page in the sidebar's Plugins
|
|
4314
|
+
* panel. One face keeps both cards reading the same live values — the same
|
|
4315
|
+
* scope, the same sign-in fact, the same `unified` layout — so a save on
|
|
4316
|
+
* either surface is a save for both. The Plugins-panel registration opens
|
|
4317
|
+
* the card at once; the settings block lists it collapsed.
|
|
4318
|
+
*/
|
|
4319
|
+
const unifiedCardInject = (defaultOpen = false) => ({
|
|
4320
|
+
t,
|
|
4321
|
+
signedIn: () => quotaSignInState(),
|
|
4322
|
+
scope: quotaScope,
|
|
4323
|
+
unified: true,
|
|
4324
|
+
defaultOpen
|
|
4325
|
+
});
|
|
4326
|
+
const registerUnifiedCard = () => ctx.slots.inject(SHARED_ITEM_SLOT, () => ctx.slots.register({
|
|
4327
|
+
name: SHARED_ITEM_SLOT,
|
|
4328
|
+
id: SHARED_ITEM_ID,
|
|
4329
|
+
order: 30,
|
|
4330
|
+
inject: unifiedCardInject
|
|
4331
|
+
}, QoderPluginCard));
|
|
4174
4332
|
/**
|
|
4175
4333
|
* Attach this plugin's card to the shared 《插件设置》 block, building the
|
|
4176
4334
|
* block first if nobody has.
|
|
@@ -4209,6 +4367,33 @@ window.__ModuleLoader__.load({
|
|
|
4209
4367
|
});
|
|
4210
4368
|
};
|
|
4211
4369
|
/**
|
|
4370
|
+
* Contribute the same unified card to this bundle's page in the sidebar's
|
|
4371
|
+
* Plugins panel, beside its enable switch and component rows.
|
|
4372
|
+
*
|
|
4373
|
+
* `ctx.slots.inject` defers the factory until the Host's plugin manager
|
|
4374
|
+
* declares the seat (its `main` registration commits the child table), so
|
|
4375
|
+
* a deployment that ships no plugin manager — or loads it after this
|
|
4376
|
+
* bundle — never throws: the callback simply never runs, and the shared
|
|
4377
|
+
* 《插件设置》 block stays the single surface, exactly as before. The
|
|
4378
|
+
* keyed key is this package's name, the same key the page's configuration
|
|
4379
|
+
* ledger reads to decide whether the configuration section shows.
|
|
4380
|
+
*
|
|
4381
|
+
* Its own boundary mirrors the dashboard and footer-card ones: a slot-API
|
|
4382
|
+
* breaking change degrades to a console.error instead of taking the
|
|
4383
|
+
* settings block or the model channel with it.
|
|
4384
|
+
*/
|
|
4385
|
+
const joinPluginManagerBlock = () => {
|
|
4386
|
+
try {
|
|
4387
|
+
ctx.slots.inject(PLUGIN_MANAGER_SLOT, () => ctx.slots.register({
|
|
4388
|
+
name: PLUGIN_MANAGER_SLOT,
|
|
4389
|
+
key: PLUGIN_MANAGER_KEY,
|
|
4390
|
+
inject: () => unifiedCardInject(true)
|
|
4391
|
+
}, QoderPluginCard));
|
|
4392
|
+
} catch (error) {
|
|
4393
|
+
console.error("[dsh-qoder-connect] could not join the plugin manager page:", error);
|
|
4394
|
+
}
|
|
4395
|
+
};
|
|
4396
|
+
/**
|
|
4212
4397
|
* 插件自有配置(`<profile>/.dsh-qoder-connect/settings.json`):两条宿主线的
|
|
4213
4398
|
* 读写都走宿主半的 settings face,不再经过 settingsScope / configForms。
|
|
4214
4399
|
*
|
|
@@ -4220,13 +4405,9 @@ window.__ModuleLoader__.load({
|
|
|
4220
4405
|
ownQuotaScope.load();
|
|
4221
4406
|
adoptQuotaScope(ownQuotaScope);
|
|
4222
4407
|
joinSharedSettingsBlock();
|
|
4408
|
+
joinPluginManagerBlock();
|
|
4223
4409
|
const QUOTA_PANEL_ID = "qoder-quota-panel";
|
|
4224
4410
|
const CONVERSATION_PANEL_ID = "conversation";
|
|
4225
|
-
const dashboardDocuments = {
|
|
4226
|
-
cn: void 0,
|
|
4227
|
-
global: void 0
|
|
4228
|
-
};
|
|
4229
|
-
let dashboardFetchedAt;
|
|
4230
4411
|
let dashboardLoading = false;
|
|
4231
4412
|
let dashboardRequestedPath = QODER_STATUS_PATH;
|
|
4232
4413
|
/** Whether the dashboard is the CURRENT center panel (its mount owns this). */
|
|
@@ -4250,15 +4431,11 @@ window.__ModuleLoader__.load({
|
|
|
4250
4431
|
* mutator builds the next snapshot and publishes exactly once.
|
|
4251
4432
|
*/
|
|
4252
4433
|
let dashboardSnap = {
|
|
4253
|
-
documents: [void 0, void 0],
|
|
4254
|
-
fetchedAt: void 0,
|
|
4255
4434
|
loading: false,
|
|
4256
4435
|
activePath: QODER_STATUS_PATH
|
|
4257
4436
|
};
|
|
4258
4437
|
const rebuildSnapshot = () => {
|
|
4259
4438
|
const next = {
|
|
4260
|
-
documents: [dashboardDocuments.cn, dashboardDocuments.global],
|
|
4261
|
-
fetchedAt: dashboardFetchedAt,
|
|
4262
4439
|
loading: dashboardLoading,
|
|
4263
4440
|
activePath: dashboardRequestedPath
|
|
4264
4441
|
};
|
|
@@ -4291,6 +4468,12 @@ window.__ModuleLoader__.load({
|
|
|
4291
4468
|
* variant with NO result yet always fetches. A manual Refresh click
|
|
4292
4469
|
* (force=true) bypasses the freshness check: an explicit user action
|
|
4293
4470
|
* always re-reads.
|
|
4471
|
+
*
|
|
4472
|
+
* The read itself is the SHARED one: it publishes through the store's
|
|
4473
|
+
* sequence guard, so this panel, the sidebar cards and the settings cards
|
|
4474
|
+
* converge on the same document — and a settings card polling the variant
|
|
4475
|
+
* already keeps this panel current, which is why the panel no longer
|
|
4476
|
+
* keeps a timer of its own.
|
|
4294
4477
|
*/
|
|
4295
4478
|
const refreshDashboard = async (options = {}) => {
|
|
4296
4479
|
if (dashboardLoading) return;
|
|
@@ -4299,44 +4482,36 @@ window.__ModuleLoader__.load({
|
|
|
4299
4482
|
dashboardLoading = true;
|
|
4300
4483
|
rebuildSnapshot();
|
|
4301
4484
|
try {
|
|
4302
|
-
|
|
4303
|
-
if (result !== void 0) noteQuotaStatus(variantId, result);
|
|
4304
|
-
dashboardFetchedAt = Date.now();
|
|
4485
|
+
await refreshQuotaStatus(variantId);
|
|
4305
4486
|
} finally {
|
|
4306
4487
|
dashboardLoading = false;
|
|
4307
4488
|
rebuildSnapshot();
|
|
4308
4489
|
}
|
|
4309
4490
|
};
|
|
4310
|
-
|
|
4311
|
-
|
|
4312
|
-
|
|
4313
|
-
|
|
4314
|
-
|
|
4315
|
-
|
|
4316
|
-
|
|
4317
|
-
|
|
4491
|
+
/**
|
|
4492
|
+
* The panel's hold on its shown variant. The hold is what keeps the
|
|
4493
|
+
* variant live: the shared ticker reads it at the settings interval while
|
|
4494
|
+
* the panel stands — the cadence the panel's own timer used to keep — and
|
|
4495
|
+
* an expanded settings card beside it tightens the read to once a minute.
|
|
4496
|
+
* A variant switch moves the hold to the newly shown variant.
|
|
4497
|
+
*/
|
|
4498
|
+
let dashboardHold;
|
|
4499
|
+
const holdDashboardVariant = (path) => {
|
|
4500
|
+
dashboardHold?.();
|
|
4501
|
+
dashboardHold = acquireLiveSurface(variantOfStatusPath(path));
|
|
4318
4502
|
};
|
|
4319
|
-
const
|
|
4320
|
-
|
|
4321
|
-
|
|
4322
|
-
dashboardTimer = void 0;
|
|
4503
|
+
const releaseDashboardVariant = () => {
|
|
4504
|
+
dashboardHold?.();
|
|
4505
|
+
dashboardHold = void 0;
|
|
4323
4506
|
};
|
|
4324
|
-
async function fetchStatusDocument(path) {
|
|
4325
|
-
try {
|
|
4326
|
-
const response = await fetch(path, { headers: { accept: "application/json" } });
|
|
4327
|
-
const body = await response.json();
|
|
4328
|
-
return response.ok && isQoderWebStatus(body) ? body : void 0;
|
|
4329
|
-
} catch {
|
|
4330
|
-
return;
|
|
4331
|
-
}
|
|
4332
|
-
}
|
|
4333
4507
|
function QuotaDashboardWithLifecycle(props) {
|
|
4334
4508
|
(0, react.useEffect)(() => {
|
|
4335
4509
|
quotaPanelOpen = true;
|
|
4336
|
-
|
|
4510
|
+
holdDashboardVariant(dashboardRequestedPath);
|
|
4511
|
+
refreshDashboard();
|
|
4337
4512
|
return () => {
|
|
4338
4513
|
quotaPanelOpen = false;
|
|
4339
|
-
|
|
4514
|
+
releaseDashboardVariant();
|
|
4340
4515
|
};
|
|
4341
4516
|
}, []);
|
|
4342
4517
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(QuotaDashboard, { ...props });
|
|
@@ -4350,6 +4525,7 @@ window.__ModuleLoader__.load({
|
|
|
4350
4525
|
},
|
|
4351
4526
|
onVariantPicked: (path) => {
|
|
4352
4527
|
dashboardRequestedPath = path;
|
|
4528
|
+
holdDashboardVariant(path);
|
|
4353
4529
|
notifyDashboard();
|
|
4354
4530
|
refreshDashboard();
|
|
4355
4531
|
},
|