dsh-connect-workbuddy 4.2.0 → 4.3.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/CHANGELOG.md +49 -0
- package/README.en.md +1 -1
- package/README.md +1 -1
- package/docs/WINDOWS.md +38 -0
- package/lib/bin.js +1 -1
- package/lib/client.js +615 -188
- package/lib/{host-heartbeat-C4TXu8ew.js → host-heartbeat-BQkmibKu.js} +256 -14
- package/lib/index.d.ts +95 -10
- package/lib/index.js +8 -3
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -309,6 +309,23 @@ window.__ModuleLoader__.load({
|
|
|
309
309
|
* fallback and must not import Node-only host code.
|
|
310
310
|
*/
|
|
311
311
|
const WORKBUDDY_SETTINGS_NS = "workbuddy";
|
|
312
|
+
/**
|
|
313
|
+
* Every namespace spelling the browser half follows when looking for the
|
|
314
|
+
* settings form the Host actually serves, in preference order.
|
|
315
|
+
*
|
|
316
|
+
* `describe()` keys a form by its Loader entry id, and the profile patch picks
|
|
317
|
+
* that id — so the plugin must ASK rather than assume. The observed spellings
|
|
318
|
+
* are the bare package name, the `include:`-wrapped id the live Desktop host
|
|
319
|
+
* uses, and the declared name as the last resort. `configForms.whileServed`
|
|
320
|
+
* follows all of them and re-runs when the mirror arrives, which is what keeps
|
|
321
|
+
* the binding correct on a host whose mirror populates after `apply` (the
|
|
322
|
+
* 4.3.0 readout defect, issue #46).
|
|
323
|
+
*/
|
|
324
|
+
const WORKBUDDY_SETTINGS_NAMESPACE_CANDIDATES = [
|
|
325
|
+
"dsh-connect-workbuddy",
|
|
326
|
+
"include:dsh-connect-workbuddy",
|
|
327
|
+
WORKBUDDY_SETTINGS_NS
|
|
328
|
+
];
|
|
312
329
|
//#endregion
|
|
313
330
|
//#region src/client/account-selection.ts
|
|
314
331
|
/**
|
|
@@ -649,6 +666,40 @@ window.__ModuleLoader__.load({
|
|
|
649
666
|
}
|
|
650
667
|
}
|
|
651
668
|
//#endregion
|
|
669
|
+
//#region src/client/BusyOverlay.tsx
|
|
670
|
+
/**
|
|
671
|
+
* The veil. Deliberately NOT a modal dialog: it carries no focusable control
|
|
672
|
+
* and no dismissal — there is nothing for the user to decide, only a step to
|
|
673
|
+
* wait through. `aria-busy` plus a polite live region state that to screen
|
|
674
|
+
* readers, which is the whole accessibility contract a wait screen needs.
|
|
675
|
+
*/
|
|
676
|
+
function BusyOverlay({ message, hint, visible }) {
|
|
677
|
+
if (!visible) return null;
|
|
678
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
679
|
+
className: "dsm-workbuddy-veil",
|
|
680
|
+
role: "status",
|
|
681
|
+
"aria-busy": "true",
|
|
682
|
+
"aria-live": "polite",
|
|
683
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
684
|
+
className: "dsm-workbuddy-veil-card",
|
|
685
|
+
children: [
|
|
686
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
687
|
+
className: "dsm-workbuddy-veil-spinner",
|
|
688
|
+
"aria-hidden": "true"
|
|
689
|
+
}),
|
|
690
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
691
|
+
className: "dsm-workbuddy-veil-text",
|
|
692
|
+
children: message
|
|
693
|
+
}),
|
|
694
|
+
hint === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
695
|
+
className: "dsm-workbuddy-veil-hint",
|
|
696
|
+
children: hint
|
|
697
|
+
})
|
|
698
|
+
]
|
|
699
|
+
})
|
|
700
|
+
});
|
|
701
|
+
}
|
|
702
|
+
//#endregion
|
|
652
703
|
//#region src/client/ndjson.ts
|
|
653
704
|
/**
|
|
654
705
|
* Read a newline-delimited JSON body, one object at a time.
|
|
@@ -1151,6 +1202,24 @@ window.__ModuleLoader__.load({
|
|
|
1151
1202
|
memberAccountIds: pool?.memberAccountIds ?? []
|
|
1152
1203
|
};
|
|
1153
1204
|
const active = draftRegion === region && draft !== null ? draft : saved;
|
|
1205
|
+
/**
|
|
1206
|
+
* Whether the pool is RUNNING right now, which is the SAVED value.
|
|
1207
|
+
*
|
|
1208
|
+
* `active` is the user's draft and only becomes reality at Save. Using it for
|
|
1209
|
+
* the parts that DESCRIBE the running pool made the card state a mode that had
|
|
1210
|
+
* not taken effect yet: flipping the switch off (unsaved) revealed the manual
|
|
1211
|
+
* picker while the pool was still ranking every request, so the picker showed
|
|
1212
|
+
* the rotated account as "in use" and a selection made there was overridden by
|
|
1213
|
+
* the very pool the user was looking at — the "shows A, bills B" trap this
|
|
1214
|
+
* section keeps guarding against. The mirror case was milder but the same
|
|
1215
|
+
* class of wrong: turning the pool ON (unsaved) hid the picker while the manual
|
|
1216
|
+
* account was still the one being billed.
|
|
1217
|
+
*
|
|
1218
|
+
* Everything that reports pool state reads this; only the switch itself (its
|
|
1219
|
+
* controlled value) and `dirty` read the draft, because those two ARE about
|
|
1220
|
+
* the pending edit.
|
|
1221
|
+
*/
|
|
1222
|
+
const poolRunning = saved.enabled;
|
|
1154
1223
|
const dirty = draftRegion === region && draft !== null && (draft.enabled !== saved.enabled || draft.targetModelId !== saved.targetModelId || draft.probeInputTokens !== saved.probeInputTokens || !sameIds(draft.memberAccountIds, saved.memberAccountIds));
|
|
1155
1224
|
/**
|
|
1156
1225
|
* Membership that actually resolves to a local sign-in — what a batch runs on.
|
|
@@ -1405,6 +1474,14 @@ window.__ModuleLoader__.load({
|
|
|
1405
1474
|
/** The account whose per-row action is in flight; gates every row's buttons. */
|
|
1406
1475
|
const [rowBusyAccountId, setRowBusyAccountId] = (0, react.useState)(void 0);
|
|
1407
1476
|
/**
|
|
1477
|
+
* Which per-row action is running, for the veil's scenario line.
|
|
1478
|
+
*
|
|
1479
|
+
* A removal and a test both set `rowBusyAccountId`, but they mean different
|
|
1480
|
+
* things to the person watching ("this account is going away" versus "this
|
|
1481
|
+
* account is being measured") — and the veil exists precisely to say which.
|
|
1482
|
+
*/
|
|
1483
|
+
const [rowBusyAction, setRowBusyAction] = (0, react.useState)(void 0);
|
|
1484
|
+
/**
|
|
1408
1485
|
* Test ONE account from its row.
|
|
1409
1486
|
*
|
|
1410
1487
|
* Same Host rule as the batch (testOne reuses the batch's probe), the same
|
|
@@ -1413,6 +1490,7 @@ window.__ModuleLoader__.load({
|
|
|
1413
1490
|
*/
|
|
1414
1491
|
const runRowTest = (0, react.useCallback)(async (accountId) => {
|
|
1415
1492
|
setRowBusyAccountId(accountId);
|
|
1493
|
+
setRowBusyAction("test");
|
|
1416
1494
|
setActionError(void 0);
|
|
1417
1495
|
const url = withWorkBuddyRegionAndAction(WORKBUDDY_POOL_PATH, region, "test") + "&accountId=" + encodeURIComponent(accountId);
|
|
1418
1496
|
try {
|
|
@@ -1447,7 +1525,10 @@ window.__ModuleLoader__.load({
|
|
|
1447
1525
|
appendLog(t("row.poolLogBatchFailed", { message }), "error");
|
|
1448
1526
|
}
|
|
1449
1527
|
} finally {
|
|
1450
|
-
if (mounted.current)
|
|
1528
|
+
if (mounted.current) {
|
|
1529
|
+
setRowBusyAccountId(void 0);
|
|
1530
|
+
setRowBusyAction(void 0);
|
|
1531
|
+
}
|
|
1451
1532
|
}
|
|
1452
1533
|
}, [
|
|
1453
1534
|
appendLog,
|
|
@@ -1456,12 +1537,17 @@ window.__ModuleLoader__.load({
|
|
|
1456
1537
|
t
|
|
1457
1538
|
]);
|
|
1458
1539
|
/**
|
|
1459
|
-
* Remove ONE account
|
|
1460
|
-
*
|
|
1461
|
-
*
|
|
1540
|
+
* Remove ONE account from its row, via the Host.
|
|
1541
|
+
*
|
|
1542
|
+
* The Host dismisses the account (and deletes its vault entry when it has
|
|
1543
|
+
* one), so the row stays gone across rescans. The desktop app is not signed
|
|
1544
|
+
* out. The message below still distinguishes the two cases, because "there
|
|
1545
|
+
* was no plugin-stored copy" is worth saying: it tells the user the account
|
|
1546
|
+
* comes from the app itself rather than from an OAuth sign-in or an import.
|
|
1462
1547
|
*/
|
|
1463
1548
|
const runRowRemove = (0, react.useCallback)(async (accountId, accountName) => {
|
|
1464
1549
|
setRowBusyAccountId(accountId);
|
|
1550
|
+
setRowBusyAction("remove");
|
|
1465
1551
|
setActionError(void 0);
|
|
1466
1552
|
const url = withWorkBuddyRegionAndAction(WORKBUDDY_POOL_PATH, region, "remove") + "&accountId=" + encodeURIComponent(accountId);
|
|
1467
1553
|
try {
|
|
@@ -1478,7 +1564,7 @@ window.__ModuleLoader__.load({
|
|
|
1478
1564
|
}));
|
|
1479
1565
|
if (!mounted.current) return;
|
|
1480
1566
|
appendLog(body?.removed === true ? t("row.poolRowRemoved", { accountName: accountName === "" ? t("row.accountUnnamed") : accountName }) : t("row.poolRowRemoveDesktopOnly", { accountName: accountName === "" ? t("row.accountUnnamed") : accountName }), body?.removed === true ? "ok" : "info");
|
|
1481
|
-
onRefresh?.();
|
|
1567
|
+
await onRefresh?.();
|
|
1482
1568
|
} catch (error) {
|
|
1483
1569
|
const message = error instanceof Error ? error.message : t("row.requestFailed");
|
|
1484
1570
|
if (mounted.current) {
|
|
@@ -1486,7 +1572,10 @@ window.__ModuleLoader__.load({
|
|
|
1486
1572
|
appendLog(t("row.poolLogBatchFailed", { message }), "error");
|
|
1487
1573
|
}
|
|
1488
1574
|
} finally {
|
|
1489
|
-
if (mounted.current)
|
|
1575
|
+
if (mounted.current) {
|
|
1576
|
+
setRowBusyAccountId(void 0);
|
|
1577
|
+
setRowBusyAction(void 0);
|
|
1578
|
+
}
|
|
1490
1579
|
}
|
|
1491
1580
|
}, [
|
|
1492
1581
|
appendLog,
|
|
@@ -1517,7 +1606,15 @@ window.__ModuleLoader__.load({
|
|
|
1517
1606
|
*/
|
|
1518
1607
|
const currentAccount = pool.accounts.find((account) => account.current);
|
|
1519
1608
|
const currentName = currentAccount === void 0 ? void 0 : currentAccount.accountName === "" ? t("row.accountUnnamed") : currentAccount.accountName;
|
|
1520
|
-
return (0, react.createElement)("section", { className: "dsm-workbuddy-pool" },
|
|
1609
|
+
return (0, react.createElement)("section", { className: "dsm-workbuddy-pool" }, rowBusyAction === "remove" ? (0, react.createElement)(BusyOverlay, {
|
|
1610
|
+
visible: true,
|
|
1611
|
+
message: t("row.poolRowRemoving"),
|
|
1612
|
+
hint: t("row.veilHintList")
|
|
1613
|
+
}) : null, saving ? (0, react.createElement)(BusyOverlay, {
|
|
1614
|
+
visible: true,
|
|
1615
|
+
message: t("row.poolSaving"),
|
|
1616
|
+
hint: t("row.veilHintList")
|
|
1617
|
+
}) : null, (0, react.createElement)("div", { className: "dsm-workbuddy-pool-head" }, (0, react.createElement)("h3", { className: "dsm-workbuddy-pool-title" }, t("row.poolTitle")), currentName === void 0 ? null : (0, react.createElement)("span", { className: "dsm-workbuddy-pool-current-badge" }, t("row.poolCurrentHeader", { account: currentName }))), !poolRunning && onSelectAccount !== void 0 && pool.accounts.length > 0 ? (0, react.createElement)("div", { className: "dsm-workbuddy-pool-set" }, (0, react.createElement)("span", { className: "dsm-workbuddy-pool-set-copy" }, (0, react.createElement)("b", null, t("row.poolManualAccountLabel")), (0, react.createElement)("span", null, t("row.poolManualAccountHint"))), (0, react.createElement)("span", { className: "dsm-workbuddy-pool-set-ctl" }, (0, react.createElement)("select", {
|
|
1521
1618
|
className: "dsm-workbuddy-pool-select dsm-workbuddy-pool-account-select",
|
|
1522
1619
|
value: currentAccount?.accountId ?? "",
|
|
1523
1620
|
disabled: selectingAccount === true,
|
|
@@ -1550,7 +1647,7 @@ window.__ModuleLoader__.load({
|
|
|
1550
1647
|
className: "dsm-btn dsm-btn-outline",
|
|
1551
1648
|
disabled: rescanning === true,
|
|
1552
1649
|
onClick: onRescan
|
|
1553
|
-
}, rescanning === true ? t("row.accountsScanning") : t("row.accountsRescan")))), !
|
|
1650
|
+
}, rescanning === true ? t("row.accountsScanning") : t("row.accountsRescan")))), !poolRunning ? (0, react.createElement)("p", { className: "dsm-workbuddy-pool-note" }, t("row.poolEnabledHint")) : null, poolRunning && usable.length === 0 ? (0, react.createElement)("p", {
|
|
1554
1651
|
className: "dsm-workbuddy-pool-warn",
|
|
1555
1652
|
role: "status"
|
|
1556
1653
|
}, `${t("row.poolNoCandidate")} ${t("row.poolNoCandidateHint")}`) : null, (0, react.createElement)("div", { className: "dsm-workbuddy-pool-members" }, (0, react.createElement)("div", { className: "dsm-workbuddy-pool-members-head" }, (0, react.createElement)("div", null, (0, react.createElement)("h4", { className: "dsm-workbuddy-pool-members-title" }, t("row.poolMembersTitle")), (0, react.createElement)("p", { className: "dsm-workbuddy-pool-note" }, t("row.poolMembersHint"))), (0, react.createElement)("div", { className: "dsm-workbuddy-pool-members-actions" }, (0, react.createElement)("span", { className: "dsm-workbuddy-pool-hint" }, t("row.poolSelectedCount", {
|
|
@@ -1773,19 +1870,22 @@ window.__ModuleLoader__.load({
|
|
|
1773
1870
|
//#endregion
|
|
1774
1871
|
//#region src/client/OAuthSignIn.tsx
|
|
1775
1872
|
/**
|
|
1776
|
-
* The card's OAuth QR sign-in section
|
|
1777
|
-
*
|
|
1778
|
-
*
|
|
1779
|
-
*
|
|
1780
|
-
*
|
|
1781
|
-
*
|
|
1782
|
-
*
|
|
1783
|
-
*
|
|
1784
|
-
*
|
|
1785
|
-
*
|
|
1786
|
-
*
|
|
1787
|
-
*
|
|
1788
|
-
*
|
|
1873
|
+
* The card's OAuth QR sign-in section: one resting bar row plus a dialog
|
|
1874
|
+
* opened by the sign-in button.
|
|
1875
|
+
*
|
|
1876
|
+
* 参考:workbuddy2api-panel 的「添加账号」弹窗交互形态 —— 静态入口一行,
|
|
1877
|
+
* 点开后弹窗内分步走:获取授权链接 → 自动打开浏览器 → 弹窗内显示链接
|
|
1878
|
+
* 与等待状态 → 完成/失败回填结果。本实现沿用该步骤划分;弹窗样式直接
|
|
1879
|
+
* 用本项目导出/导入弹框的类(dsm-workbuddy-transfer-dialog),不引入
|
|
1880
|
+
* 第二套视觉。
|
|
1881
|
+
* 改动:自动打开浏览器。弹窗拦截的规避方式是「用户手势内同步 window.open
|
|
1882
|
+
* 占位,拿到授权 URL 后再导航」——点击「获取授权链接」的那一刻浏览器
|
|
1883
|
+
* 仍认定这是用户激活的手势链,占位窗口不会被拦;若 Host 环境连占位都
|
|
1884
|
+
* 拦(返回 null),降级为弹窗内手动「打开登录页」链接,功能不损失。
|
|
1885
|
+
*
|
|
1886
|
+
* 轮询循环压成一个组件内 effect:按 `OAUTH_POLL_INTERVAL_MS` 节奏问 Host
|
|
1887
|
+
* 「完成了吗」,完成即停。所有状态就地管理,零跨组件往返;Host 重启
|
|
1888
|
+
* 导致 loginId 失效会得到 done+error,与超时同一条文案路径。
|
|
1789
1889
|
*
|
|
1790
1890
|
* @module dsh-connect-workbuddy/client/OAuthSignIn
|
|
1791
1891
|
*/
|
|
@@ -1793,13 +1893,24 @@ window.__ModuleLoader__.load({
|
|
|
1793
1893
|
function elapsedSeconds(sinceMs, nowMs) {
|
|
1794
1894
|
return Math.max(0, Math.floor((nowMs - sinceMs) / 1e3));
|
|
1795
1895
|
}
|
|
1896
|
+
/** Copy the sign-in URL to the clipboard; true on success. */
|
|
1897
|
+
async function copyUrl(url) {
|
|
1898
|
+
try {
|
|
1899
|
+
await navigator.clipboard.writeText(url);
|
|
1900
|
+
return true;
|
|
1901
|
+
} catch {
|
|
1902
|
+
return false;
|
|
1903
|
+
}
|
|
1904
|
+
}
|
|
1796
1905
|
/**
|
|
1797
1906
|
* The section. Rendered on BOTH sign-in states: a signed-out machine gains
|
|
1798
1907
|
* its first account here; a signed-in one can add another without leaving
|
|
1799
1908
|
* the card.
|
|
1800
1909
|
*/
|
|
1801
|
-
function OAuthSignIn({ t, region }) {
|
|
1910
|
+
function OAuthSignIn({ t, region, onAdded }) {
|
|
1802
1911
|
const [phase, setPhase] = (0, react.useState)({ kind: "idle" });
|
|
1912
|
+
const [dialogOpen, setDialogOpen] = (0, react.useState)(false);
|
|
1913
|
+
const [copied, setCopied] = (0, react.useState)(false);
|
|
1803
1914
|
const [pollTick, setPollTick] = (0, react.useState)(0);
|
|
1804
1915
|
const [nowMs, setNowMs] = (0, react.useState)(() => Date.now());
|
|
1805
1916
|
const mounted = (0, react.useRef)(true);
|
|
@@ -1826,7 +1937,11 @@ window.__ModuleLoader__.load({
|
|
|
1826
1937
|
return () => {
|
|
1827
1938
|
clearInterval(timer);
|
|
1828
1939
|
};
|
|
1829
|
-
}, [
|
|
1940
|
+
}, [
|
|
1941
|
+
waiting,
|
|
1942
|
+
phase,
|
|
1943
|
+
t
|
|
1944
|
+
]);
|
|
1830
1945
|
(0, react.useEffect)(() => {
|
|
1831
1946
|
if (phase.kind !== "waiting") return;
|
|
1832
1947
|
const { loginId } = phase;
|
|
@@ -1862,11 +1977,20 @@ window.__ModuleLoader__.load({
|
|
|
1862
1977
|
message: "error",
|
|
1863
1978
|
error: body.error
|
|
1864
1979
|
});
|
|
1865
|
-
else
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1980
|
+
else {
|
|
1981
|
+
const accountName = body.accountName ?? "";
|
|
1982
|
+
setPhase({
|
|
1983
|
+
kind: "syncing",
|
|
1984
|
+
accountName
|
|
1985
|
+
});
|
|
1986
|
+
await onAdded?.();
|
|
1987
|
+
if (cancelled || !mounted.current) return;
|
|
1988
|
+
setPhase({
|
|
1989
|
+
kind: "finished",
|
|
1990
|
+
message: "added",
|
|
1991
|
+
accountName
|
|
1992
|
+
});
|
|
1993
|
+
}
|
|
1870
1994
|
} catch {
|
|
1871
1995
|
setPollTick((tick) => tick + 1);
|
|
1872
1996
|
}
|
|
@@ -1877,12 +2001,47 @@ window.__ModuleLoader__.load({
|
|
|
1877
2001
|
clearTimeout(timer);
|
|
1878
2002
|
};
|
|
1879
2003
|
}, [
|
|
2004
|
+
onAdded,
|
|
1880
2005
|
phase,
|
|
1881
2006
|
pollTick,
|
|
1882
2007
|
region
|
|
1883
2008
|
]);
|
|
1884
|
-
|
|
2009
|
+
/**
|
|
2010
|
+
* Open a placeholder window WITHOUT `noopener`, and deliberately so.
|
|
2011
|
+
*
|
|
2012
|
+
* `noopener` is the correct feature for a link whose destination never needs
|
|
2013
|
+
* a handle — and that is exactly why it is wrong here. Per the spec, a
|
|
2014
|
+
* `window.open` call carrying `noopener` opens the tab but returns `null`
|
|
2015
|
+
* (MDN: "the new window will not have access to the originating window via
|
|
2016
|
+
* `Window.opener` and returns `null`"). The placeholder exists ONLY to be
|
|
2017
|
+
* navigated once the Host answers, so the handle IS the point: passing
|
|
2018
|
+
* `noopener` made `placeholder` null, the navigation was skipped, and every
|
|
2019
|
+
* sign-in left an empty `about:blank` tab behind instead of the sign-in page.
|
|
2020
|
+
*
|
|
2021
|
+
* The security property is kept, just established on the handle instead of
|
|
2022
|
+
* through the feature: `opener = null` is applied to the placeholder before
|
|
2023
|
+
* anything is loaded into it, so the destination still cannot reach back
|
|
2024
|
+
* through `window.opener`. `noreferrer` is dropped for the same reason —
|
|
2025
|
+
* the spec makes it imply `noopener`, so it would reintroduce the null.
|
|
2026
|
+
* Nothing is lost: the placeholder is same-origin and empty until navigated,
|
|
2027
|
+
* and the referrer would only ever have been the settings page.
|
|
2028
|
+
*/
|
|
2029
|
+
function openPlaceholder() {
|
|
2030
|
+
const placeholder = window.open("", "_blank");
|
|
2031
|
+
if (placeholder !== null) placeholder.opener = null;
|
|
2032
|
+
return placeholder;
|
|
2033
|
+
}
|
|
2034
|
+
/**
|
|
2035
|
+
* Fetch the authorization URL AND open the browser in one gesture chain.
|
|
2036
|
+
* The placeholder window is opened synchronously — inside the click's user
|
|
2037
|
+
* activation, before any await — so popup blocking cannot intervene; the
|
|
2038
|
+
* URL is navigated once the Host answers. A blocked placeholder (null)
|
|
2039
|
+
* degrades to the manual link inside the dialog, losing nothing but the
|
|
2040
|
+
* convenience.
|
|
2041
|
+
*/
|
|
2042
|
+
const start = async (placeholder) => {
|
|
1885
2043
|
setPhase({ kind: "opening" });
|
|
2044
|
+
setCopied(false);
|
|
1886
2045
|
try {
|
|
1887
2046
|
const response = await fetch(withWorkBuddyRouteAction(WORKBUDDY_OAUTH_PATH, region, "start"), {
|
|
1888
2047
|
method: "POST",
|
|
@@ -1890,8 +2049,12 @@ window.__ModuleLoader__.load({
|
|
|
1890
2049
|
credentials: "same-origin"
|
|
1891
2050
|
});
|
|
1892
2051
|
const body = await response.json();
|
|
1893
|
-
if (!mounted.current)
|
|
2052
|
+
if (!mounted.current) {
|
|
2053
|
+
placeholder?.close();
|
|
2054
|
+
return;
|
|
2055
|
+
}
|
|
1894
2056
|
if (!response.ok || typeof body.loginId !== "string" || typeof body.verificationUri !== "string") {
|
|
2057
|
+
placeholder?.close();
|
|
1895
2058
|
setPhase({
|
|
1896
2059
|
kind: "finished",
|
|
1897
2060
|
message: "error",
|
|
@@ -1899,6 +2062,7 @@ window.__ModuleLoader__.load({
|
|
|
1899
2062
|
});
|
|
1900
2063
|
return;
|
|
1901
2064
|
}
|
|
2065
|
+
if (placeholder !== null) placeholder.location.href = body.verificationUri;
|
|
1902
2066
|
setPhase({
|
|
1903
2067
|
kind: "waiting",
|
|
1904
2068
|
loginId: body.loginId,
|
|
@@ -1908,6 +2072,7 @@ window.__ModuleLoader__.load({
|
|
|
1908
2072
|
});
|
|
1909
2073
|
setNowMs(Date.now());
|
|
1910
2074
|
} catch (error) {
|
|
2075
|
+
placeholder?.close();
|
|
1911
2076
|
if (mounted.current) setPhase({
|
|
1912
2077
|
kind: "finished",
|
|
1913
2078
|
message: "error",
|
|
@@ -1915,72 +2080,133 @@ window.__ModuleLoader__.load({
|
|
|
1915
2080
|
});
|
|
1916
2081
|
}
|
|
1917
2082
|
};
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
2083
|
+
/** Open the dialog and begin the login in the same click: the placeholder
|
|
2084
|
+
* window must be created synchronously to count as a user gesture. */
|
|
2085
|
+
const beginSignIn = () => {
|
|
2086
|
+
setDialogOpen(true);
|
|
2087
|
+
start(openPlaceholder());
|
|
2088
|
+
};
|
|
2089
|
+
/** Re-open the sign-in page from the waiting dialog (no new session).
|
|
2090
|
+
*
|
|
2091
|
+
* Here `noopener,noreferrer` IS right: this is a one-way navigation to a
|
|
2092
|
+
* third-party page and nothing needs a handle on it — the contrast with
|
|
2093
|
+
* {@link openPlaceholder} is the whole distinction. */
|
|
2094
|
+
const reopen = () => {
|
|
2095
|
+
if (phase.kind !== "waiting") return;
|
|
2096
|
+
window.open(phase.verificationUri, "_blank", "noopener,noreferrer");
|
|
2097
|
+
};
|
|
2098
|
+
/**
|
|
2099
|
+
* The three strings that NAME the sign-in method, per region.
|
|
2100
|
+
*
|
|
2101
|
+
* CN signs in by scanning a QR code with the WorkBuddy app; the international
|
|
2102
|
+
* build signs in on a web page. Every other line in this section (waiting,
|
|
2103
|
+
* cancel, done, copy) is about the flow, not the method, so it stays shared —
|
|
2104
|
+
* splitting it too would let the two regions drift apart for no gain.
|
|
2105
|
+
*/
|
|
2106
|
+
const titleKey = region === "cn" ? "row.oauthTitle" : "row.oauthTitleGlobal";
|
|
2107
|
+
const startKey = region === "cn" ? "row.oauthStart" : "row.oauthStartGlobal";
|
|
2108
|
+
const finishText = phase.kind === "finished" ? phase.message === "added" ? t("row.oauthDone", { accountName: phase.accountName === "" || phase.accountName === void 0 ? t("row.accountUnnamed") : phase.accountName }) : phase.message === "cancelled" ? t("row.oauthCancelled") : t("row.oauthError", { message: phase.error ?? "" }) : "";
|
|
1936
2109
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
1937
2110
|
className: "dsm-workbuddy-bar",
|
|
1938
|
-
"aria-label": t(
|
|
1939
|
-
children: [
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
className: "dsm-workbuddy-bar-status",
|
|
1945
|
-
children: [
|
|
1946
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
|
|
1947
|
-
href: phase.verificationUri,
|
|
1948
|
-
target: "_blank",
|
|
1949
|
-
rel: "noopener noreferrer",
|
|
1950
|
-
children: t("row.oauthOpen")
|
|
1951
|
-
}),
|
|
1952
|
-
" — ",
|
|
1953
|
-
t("row.oauthOpened")
|
|
1954
|
-
]
|
|
2111
|
+
"aria-label": t(titleKey),
|
|
2112
|
+
children: [
|
|
2113
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(BusyOverlay, {
|
|
2114
|
+
visible: phase.kind === "syncing",
|
|
2115
|
+
message: t("row.oauthSyncing"),
|
|
2116
|
+
hint: t("row.veilHintList")
|
|
1955
2117
|
}),
|
|
1956
|
-
/* @__PURE__ */ (0, react_jsx_runtime.
|
|
1957
|
-
className: "dsm-workbuddy-bar-
|
|
1958
|
-
children:
|
|
1959
|
-
t("row.oauthWaiting", { seconds: elapsedSeconds(phase.startedAtMs, nowMs) }),
|
|
1960
|
-
" ",
|
|
1961
|
-
t("row.oauthExpires", { minutes: Math.max(1, Math.round((phase.expiresAtMs - nowMs) / 6e4)) })
|
|
1962
|
-
]
|
|
2118
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2119
|
+
className: "dsm-workbuddy-bar-label",
|
|
2120
|
+
children: t(titleKey)
|
|
1963
2121
|
}),
|
|
1964
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
children:
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
2122
|
+
phase.kind === "finished" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2123
|
+
className: `dsm-workbuddy-bar-status ${phase.message === "added" ? "dsm-workbuddy-bar-ok" : "dsm-workbuddy-bar-error"}`,
|
|
2124
|
+
children: finishText
|
|
2125
|
+
}) : phase.kind === "waiting" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2126
|
+
className: "dsm-workbuddy-bar-status",
|
|
2127
|
+
children: t("row.oauthOpened")
|
|
2128
|
+
}) : null,
|
|
2129
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2130
|
+
className: "dsm-workbuddy-bar-actions",
|
|
2131
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2132
|
+
type: "button",
|
|
2133
|
+
className: "dsm-btn dsm-btn-outline",
|
|
2134
|
+
disabled: phase.kind === "opening",
|
|
2135
|
+
onClick: beginSignIn,
|
|
2136
|
+
children: phase.kind === "opening" ? t("row.oauthStarting") : t(startKey)
|
|
2137
|
+
})
|
|
2138
|
+
}),
|
|
2139
|
+
dialogOpen ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2140
|
+
className: "dsm-workbuddy-transfer-dialog",
|
|
2141
|
+
role: "dialog",
|
|
2142
|
+
"aria-label": t(titleKey),
|
|
2143
|
+
children: phase.kind === "opening" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t("row.oauthStarting") }) : phase.kind === "waiting" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
2144
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t("row.oauthOpenHint") }),
|
|
2145
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2146
|
+
className: "dsm-workbuddy-oauth-url",
|
|
2147
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
|
|
2148
|
+
href: phase.verificationUri,
|
|
2149
|
+
target: "_blank",
|
|
2150
|
+
rel: "noopener noreferrer",
|
|
2151
|
+
children: phase.verificationUri
|
|
2152
|
+
})
|
|
2153
|
+
}),
|
|
2154
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
|
|
2155
|
+
className: "dsm-workbuddy-hint",
|
|
2156
|
+
children: [
|
|
2157
|
+
t("row.oauthWaiting", { seconds: elapsedSeconds(phase.startedAtMs, nowMs) }),
|
|
2158
|
+
" ",
|
|
2159
|
+
t("row.oauthExpires", { minutes: Math.max(1, Math.round((phase.expiresAtMs - nowMs) / 6e4)) })
|
|
2160
|
+
]
|
|
2161
|
+
}),
|
|
2162
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2163
|
+
className: "dsm-workbuddy-transfer-dialog-actions",
|
|
2164
|
+
children: [
|
|
2165
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2166
|
+
type: "button",
|
|
2167
|
+
className: "dsm-btn dsm-btn-outline",
|
|
2168
|
+
onClick: () => {
|
|
2169
|
+
copyUrl(phase.verificationUri).then((ok) => {
|
|
2170
|
+
if (mounted.current) setCopied(ok);
|
|
2171
|
+
});
|
|
2172
|
+
},
|
|
2173
|
+
children: copied ? t("row.oauthCopied") : t("row.oauthCopy")
|
|
2174
|
+
}),
|
|
2175
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2176
|
+
type: "button",
|
|
2177
|
+
className: "dsm-btn dsm-btn-outline",
|
|
2178
|
+
onClick: reopen,
|
|
2179
|
+
children: t("row.oauthReopen")
|
|
2180
|
+
}),
|
|
2181
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2182
|
+
type: "button",
|
|
2183
|
+
className: "dsm-btn dsm-btn-outline",
|
|
2184
|
+
onClick: () => {
|
|
2185
|
+
setPhase({
|
|
2186
|
+
kind: "finished",
|
|
2187
|
+
message: "cancelled"
|
|
2188
|
+
});
|
|
2189
|
+
},
|
|
2190
|
+
children: t("row.oauthCancel")
|
|
2191
|
+
})
|
|
2192
|
+
]
|
|
2193
|
+
})
|
|
2194
|
+
] }) : phase.kind === "finished" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2195
|
+
className: `${phase.message === "added" ? "dsm-workbuddy-bar-ok" : phase.message === "error" ? "dsm-workbuddy-bar-error" : ""}`,
|
|
2196
|
+
children: finishText
|
|
2197
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2198
|
+
className: "dsm-workbuddy-transfer-dialog-actions",
|
|
2199
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2200
|
+
type: "button",
|
|
2201
|
+
className: "dsm-btn dsm-btn-outline",
|
|
2202
|
+
onClick: () => {
|
|
2203
|
+
setDialogOpen(false);
|
|
2204
|
+
},
|
|
2205
|
+
children: t("row.oauthDialogClose")
|
|
2206
|
+
})
|
|
2207
|
+
})] }) : null
|
|
2208
|
+
}) : null
|
|
2209
|
+
]
|
|
1984
2210
|
});
|
|
1985
2211
|
}
|
|
1986
2212
|
//#endregion
|
|
@@ -2022,13 +2248,24 @@ window.__ModuleLoader__.load({
|
|
|
2022
2248
|
/** The section. Rendered on both sign-in states — a signed-out machine can
|
|
2023
2249
|
* still import a file exported elsewhere; export without accounts answers
|
|
2024
2250
|
* the error line. */
|
|
2025
|
-
function AccountTransfer({ t, region, accounts }) {
|
|
2251
|
+
function AccountTransfer({ t, region, accounts, onChanged }) {
|
|
2026
2252
|
const [phase, setPhase] = (0, react.useState)({ kind: "idle" });
|
|
2027
2253
|
const [dialog, setDialog] = (0, react.useState)(void 0);
|
|
2028
2254
|
const [selected, setSelected] = (0, react.useState)(/* @__PURE__ */ new Set());
|
|
2029
2255
|
const [preview, setPreview] = (0, react.useState)(void 0);
|
|
2030
2256
|
const [importSelected, setImportSelected] = (0, react.useState)(/* @__PURE__ */ new Set());
|
|
2031
2257
|
const fileInput = (0, react.useRef)(null);
|
|
2258
|
+
/**
|
|
2259
|
+
* The text of the file the user just picked.
|
|
2260
|
+
*
|
|
2261
|
+
* Held here rather than re-read from the input at import time: the picker
|
|
2262
|
+
* is a bare system dialog that closes immediately (see the import button),
|
|
2263
|
+
* so between preview and confirm there is no dialog on screen holding the
|
|
2264
|
+
* File. Re-reading `fileInput.current.files` would also break the moment
|
|
2265
|
+
* anything clears the input — and it must be cleared after every pick, or
|
|
2266
|
+
* choosing the SAME file twice fires no change event.
|
|
2267
|
+
*/
|
|
2268
|
+
const importText = (0, react.useRef)("");
|
|
2032
2269
|
const mounted = (0, react.useRef)(true);
|
|
2033
2270
|
(0, react.useEffect)(() => {
|
|
2034
2271
|
mounted.current = true;
|
|
@@ -2084,6 +2321,7 @@ window.__ModuleLoader__.load({
|
|
|
2084
2321
|
setPhase({ kind: "importing" });
|
|
2085
2322
|
try {
|
|
2086
2323
|
const text = await file.text();
|
|
2324
|
+
importText.current = text;
|
|
2087
2325
|
const response = await fetch(withWorkBuddyRouteAction(WORKBUDDY_TRANSFER_PATH, region, "preview"), {
|
|
2088
2326
|
method: "POST",
|
|
2089
2327
|
headers: {
|
|
@@ -2113,10 +2351,10 @@ window.__ModuleLoader__.load({
|
|
|
2113
2351
|
}
|
|
2114
2352
|
};
|
|
2115
2353
|
const runImport = async () => {
|
|
2116
|
-
if (preview === void 0 || importSelected.size === 0 ||
|
|
2354
|
+
if (preview === void 0 || importSelected.size === 0 || importText.current === "") return;
|
|
2117
2355
|
setPhase({ kind: "importing" });
|
|
2118
2356
|
try {
|
|
2119
|
-
const text =
|
|
2357
|
+
const text = importText.current;
|
|
2120
2358
|
const response = await fetch(withWorkBuddyRouteAction(WORKBUDDY_TRANSFER_PATH, region, "import"), {
|
|
2121
2359
|
method: "POST",
|
|
2122
2360
|
headers: {
|
|
@@ -2139,12 +2377,19 @@ window.__ModuleLoader__.load({
|
|
|
2139
2377
|
return;
|
|
2140
2378
|
}
|
|
2141
2379
|
closeDialog();
|
|
2380
|
+
const message = t("row.transferImported", {
|
|
2381
|
+
imported: body.imported,
|
|
2382
|
+
skipped: body.skipped ?? 0
|
|
2383
|
+
});
|
|
2384
|
+
setPhase({
|
|
2385
|
+
kind: "refreshing",
|
|
2386
|
+
message
|
|
2387
|
+
});
|
|
2388
|
+
await onChanged?.();
|
|
2389
|
+
if (!mounted.current) return;
|
|
2142
2390
|
setPhase({
|
|
2143
2391
|
kind: "done",
|
|
2144
|
-
message
|
|
2145
|
-
imported: body.imported,
|
|
2146
|
-
skipped: body.skipped ?? 0
|
|
2147
|
-
})
|
|
2392
|
+
message
|
|
2148
2393
|
});
|
|
2149
2394
|
} catch (error) {
|
|
2150
2395
|
if (mounted.current) setPhase({
|
|
@@ -2153,11 +2398,18 @@ window.__ModuleLoader__.load({
|
|
|
2153
2398
|
});
|
|
2154
2399
|
}
|
|
2155
2400
|
};
|
|
2156
|
-
const busy = phase.kind === "exporting" || phase.kind === "importing";
|
|
2401
|
+
const busy = phase.kind === "exporting" || phase.kind === "importing" || phase.kind === "refreshing";
|
|
2402
|
+
/** The veil's scenario line: what is happening, in the user's terms. */
|
|
2403
|
+
const veilMessage = phase.kind === "exporting" ? t("row.transferExporting") : phase.kind === "importing" || phase.kind === "refreshing" ? t("row.transferImporting") : "";
|
|
2157
2404
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
2158
2405
|
className: "dsm-workbuddy-bar",
|
|
2159
2406
|
"aria-label": t("row.transferTitle"),
|
|
2160
2407
|
children: [
|
|
2408
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(BusyOverlay, {
|
|
2409
|
+
visible: busy,
|
|
2410
|
+
message: veilMessage,
|
|
2411
|
+
...busy ? { hint: t("row.veilHintList") } : {}
|
|
2412
|
+
}),
|
|
2161
2413
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2162
2414
|
className: "dsm-workbuddy-bar-label",
|
|
2163
2415
|
children: t("row.transferTitle")
|
|
@@ -2185,13 +2437,27 @@ window.__ModuleLoader__.load({
|
|
|
2185
2437
|
className: "dsm-btn dsm-btn-outline",
|
|
2186
2438
|
disabled: busy,
|
|
2187
2439
|
onClick: () => {
|
|
2188
|
-
|
|
2189
|
-
setImportSelected(/* @__PURE__ */ new Set());
|
|
2190
|
-
setDialog("import");
|
|
2440
|
+
fileInput.current?.click();
|
|
2191
2441
|
},
|
|
2192
2442
|
children: t("row.transferImport")
|
|
2193
2443
|
})]
|
|
2194
2444
|
}),
|
|
2445
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
2446
|
+
ref: fileInput,
|
|
2447
|
+
type: "file",
|
|
2448
|
+
accept: "application/json,.json",
|
|
2449
|
+
className: "dsm-workbuddy-transfer-file",
|
|
2450
|
+
onChange: (event) => {
|
|
2451
|
+
const file = event.target.files?.[0];
|
|
2452
|
+
event.target.value = "";
|
|
2453
|
+
if (file !== void 0) {
|
|
2454
|
+
setPreview(void 0);
|
|
2455
|
+
setImportSelected(/* @__PURE__ */ new Set());
|
|
2456
|
+
setDialog("import");
|
|
2457
|
+
readImportFile(file);
|
|
2458
|
+
}
|
|
2459
|
+
}
|
|
2460
|
+
}),
|
|
2195
2461
|
dialog === "export" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2196
2462
|
className: "dsm-workbuddy-transfer-dialog",
|
|
2197
2463
|
role: "dialog",
|
|
@@ -2238,62 +2504,49 @@ window.__ModuleLoader__.load({
|
|
|
2238
2504
|
})
|
|
2239
2505
|
]
|
|
2240
2506
|
}) : null,
|
|
2241
|
-
dialog === "import" ? /* @__PURE__ */ (0, react_jsx_runtime.
|
|
2507
|
+
dialog === "import" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2242
2508
|
className: "dsm-workbuddy-transfer-dialog",
|
|
2243
2509
|
role: "dialog",
|
|
2244
2510
|
"aria-label": t("row.transferImport"),
|
|
2245
|
-
children: [
|
|
2246
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t("row.
|
|
2247
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2511
|
+
children: preview === void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t("row.transferPreviewing") }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
2512
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t("row.transferPreviewTotal", { total: preview.total }) }),
|
|
2513
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
|
|
2514
|
+
className: "dsm-workbuddy-transfer-list",
|
|
2515
|
+
children: preview.accounts.map((entry) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", { children: [
|
|
2516
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
2517
|
+
type: "checkbox",
|
|
2518
|
+
disabled: !entry.hasToken,
|
|
2519
|
+
checked: importSelected.has(entry.index),
|
|
2520
|
+
onChange: (event) => {
|
|
2521
|
+
const next = new Set(importSelected);
|
|
2522
|
+
if (event.target.checked) next.add(entry.index);
|
|
2523
|
+
else next.delete(entry.index);
|
|
2524
|
+
setImportSelected(next);
|
|
2525
|
+
}
|
|
2526
|
+
}),
|
|
2527
|
+
" ",
|
|
2528
|
+
entry.nickname !== "" ? entry.nickname : entry.email !== "" ? entry.email : entry.uid !== "" ? entry.uid : t("row.transferEntryUnknown"),
|
|
2529
|
+
entry.hasToken ? "" : ` — ${t("row.transferNoToken")}`
|
|
2530
|
+
] }) }, entry.index))
|
|
2256
2531
|
}),
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
entry.hasToken ? "" : ` — ${t("row.transferNoToken")}`
|
|
2276
|
-
] }) }, entry.index))
|
|
2277
|
-
}),
|
|
2278
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2279
|
-
className: "dsm-workbuddy-transfer-dialog-actions",
|
|
2280
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2281
|
-
type: "button",
|
|
2282
|
-
className: "dsm-btn dsm-btn-outline",
|
|
2283
|
-
onClick: closeDialog,
|
|
2284
|
-
children: t("row.transferCancel")
|
|
2285
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2286
|
-
type: "button",
|
|
2287
|
-
className: "dsm-btn dsm-btn-outline",
|
|
2288
|
-
disabled: importSelected.size === 0 || phase.kind === "importing",
|
|
2289
|
-
onClick: () => {
|
|
2290
|
-
runImport();
|
|
2291
|
-
},
|
|
2292
|
-
children: phase.kind === "importing" ? t("row.transferImporting") : t("row.transferImportConfirm", { count: importSelected.size })
|
|
2293
|
-
})]
|
|
2294
|
-
})
|
|
2295
|
-
] })
|
|
2296
|
-
]
|
|
2532
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2533
|
+
className: "dsm-workbuddy-transfer-dialog-actions",
|
|
2534
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2535
|
+
type: "button",
|
|
2536
|
+
className: "dsm-btn dsm-btn-outline",
|
|
2537
|
+
onClick: closeDialog,
|
|
2538
|
+
children: t("row.transferCancel")
|
|
2539
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2540
|
+
type: "button",
|
|
2541
|
+
className: "dsm-btn dsm-btn-outline",
|
|
2542
|
+
disabled: importSelected.size === 0 || phase.kind === "importing",
|
|
2543
|
+
onClick: () => {
|
|
2544
|
+
runImport();
|
|
2545
|
+
},
|
|
2546
|
+
children: phase.kind === "importing" ? t("row.transferImporting") : t("row.transferImportConfirm", { count: importSelected.size })
|
|
2547
|
+
})]
|
|
2548
|
+
})
|
|
2549
|
+
] })
|
|
2297
2550
|
}) : null
|
|
2298
2551
|
]
|
|
2299
2552
|
});
|
|
@@ -2645,6 +2898,35 @@ window.__ModuleLoader__.load({
|
|
|
2645
2898
|
.dsm-workbuddy-transfer-list label{display:flex;align-items:center;gap:6px;font-size:12px;color:var(--dsw-alias-label-primary,#e8e9ed);cursor:pointer;padding:2px 0}
|
|
2646
2899
|
.dsm-workbuddy-transfer-list input[type="checkbox"]{flex:none}
|
|
2647
2900
|
.dsm-workbuddy-transfer input[type="file"]{font-size:12px;color:var(--dsw-alias-label-primary,#e8e9ed);margin-bottom:8px}
|
|
2901
|
+
/* The import picker's input is driven by the 「import」 button, so it must not
|
|
2902
|
+
render as a visible field: hide it from layout AND from assistive tech (the
|
|
2903
|
+
button carries the accessible name). Not display:none alone — that also
|
|
2904
|
+
removes it from the tab order in some engines, which is fine here, but the
|
|
2905
|
+
clip-based form is the one that reliably keeps programmatic .click() working
|
|
2906
|
+
across browsers. */
|
|
2907
|
+
.dsm-workbuddy-transfer-file{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}
|
|
2908
|
+
/* The sign-in URL inside the OAuth dialog: long https links must wrap rather
|
|
2909
|
+
than push the dialog wide — break-all, since URLs have no soft wrap points. */
|
|
2910
|
+
.dsm-workbuddy-oauth-url{word-break:break-all;overflow-wrap:anywhere}
|
|
2911
|
+
.dsm-workbuddy-oauth-url a{color:var(--dsw-alias-brand-primary,#5686fe);text-decoration:none}
|
|
2912
|
+
.dsm-workbuddy-oauth-url a:hover{text-decoration:underline}
|
|
2913
|
+
/* The shared busy veil: a fixed overlay for operations whose result only shows
|
|
2914
|
+
up after the list re-read (remove / import / sign-in).
|
|
2915
|
+
It is NOT a modal: no control, no dismissal — only "this is running" and
|
|
2916
|
+
"here is why nothing moved yet". Fixed positioning so it covers the card
|
|
2917
|
+
regardless of which section started the operation, and a semi-transparent
|
|
2918
|
+
backdrop so the table stays visible underneath — the user is watching a
|
|
2919
|
+
specific row change, and hiding it would defeat the point. */
|
|
2920
|
+
.dsm-workbuddy-veil{position:fixed;inset:0;z-index:60;display:flex;align-items:center;justify-content:center;background:rgba(0,0,0,.38);cursor:progress}
|
|
2921
|
+
.dsm-workbuddy-veil-card{display:flex;flex-direction:column;align-items:center;gap:8px;max-width:min(340px,80vw);padding:16px 20px;border:1px solid var(--dsw-alias-border-l2,#3a3d45);border-radius:12px;background:var(--dsw-alias-bg-primary,#1b1c20);box-shadow:0 8px 28px rgba(0,0,0,.35)}
|
|
2922
|
+
.dsm-workbuddy-veil-text{font-size:13px;line-height:20px;color:var(--dsw-alias-label-primary,#e8e9ed);text-align:center}
|
|
2923
|
+
.dsm-workbuddy-veil-hint{font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary,#999);text-align:center}
|
|
2924
|
+
/* The spinner is the one thing that proves the UI is alive when the row under
|
|
2925
|
+
it has not changed yet — which is precisely the confusion the veil exists to
|
|
2926
|
+
remove. Reduced-motion users get the static ring instead of a rotation. */
|
|
2927
|
+
.dsm-workbuddy-veil-spinner{width:18px;height:18px;border:2px solid var(--dsw-alias-border-l2,#3a3d45);border-top-color:var(--dsw-alias-brand-primary,#5686fe);border-radius:50%;animation:dsm-workbuddy-veil-spin .7s linear infinite}
|
|
2928
|
+
@keyframes dsm-workbuddy-veil-spin{to{transform:rotate(360deg)}}
|
|
2929
|
+
@media (prefers-reduced-motion: reduce){.dsm-workbuddy-veil-spinner{animation:none}}
|
|
2648
2930
|
`;
|
|
2649
2931
|
/**
|
|
2650
2932
|
* Composer credit readout styles, injected by the readout component itself.
|
|
@@ -3274,8 +3556,18 @@ window.__ModuleLoader__.load({
|
|
|
3274
3556
|
accounts: [],
|
|
3275
3557
|
selectionExplicit: false
|
|
3276
3558
|
};
|
|
3559
|
+
/**
|
|
3560
|
+
* Whether the ACTIVE region knows of any account at all.
|
|
3561
|
+
*
|
|
3562
|
+
* This is the poll's precondition in place of `status === 'signed-in'`. The
|
|
3563
|
+
* signed-in flag means "a credential resolves", which is false for a region
|
|
3564
|
+
* whose accounts exist but none is usable yet — exactly the state an import
|
|
3565
|
+
* or a fresh OAuth sign-in creates. Polling on it meant those regions never
|
|
3566
|
+
* refreshed, so the list stayed empty until the card was reopened.
|
|
3567
|
+
*/
|
|
3568
|
+
const hasAccounts = status.status !== "error" && status.accounts.length > 0;
|
|
3277
3569
|
(0, react.useEffect)(() => {
|
|
3278
|
-
if (!open ||
|
|
3570
|
+
if (!open || !hasAccounts) return;
|
|
3279
3571
|
const controller = new AbortController();
|
|
3280
3572
|
const timer = window.setInterval(() => {
|
|
3281
3573
|
refreshUsage(activeRegion, controller.signal);
|
|
@@ -3305,6 +3597,7 @@ window.__ModuleLoader__.load({
|
|
|
3305
3597
|
*/
|
|
3306
3598
|
const rescanAccounts = async () => {
|
|
3307
3599
|
setBusy(true);
|
|
3600
|
+
setAccountError(void 0);
|
|
3308
3601
|
try {
|
|
3309
3602
|
const response = await fetch(withWorkBuddyRegion(WORKBUDDY_ACCOUNTS_REFRESH_PATH, activeRegion), {
|
|
3310
3603
|
method: "POST",
|
|
@@ -3313,7 +3606,10 @@ window.__ModuleLoader__.load({
|
|
|
3313
3606
|
});
|
|
3314
3607
|
const body = await response.json();
|
|
3315
3608
|
if (!response.ok || !Array.isArray(body.accounts)) throw new Error(`HTTP ${response.status}`);
|
|
3609
|
+
if (body.dismissalsCleared === false) setAccountError(t("row.rescanHidesKept"));
|
|
3316
3610
|
await refreshUsage(activeRegion);
|
|
3611
|
+
} catch (error) {
|
|
3612
|
+
if (mounted.current) setAccountError(error instanceof Error ? error.message : t("row.requestFailed"));
|
|
3317
3613
|
} finally {
|
|
3318
3614
|
if (mounted.current) setBusy(false);
|
|
3319
3615
|
}
|
|
@@ -3672,10 +3968,12 @@ window.__ModuleLoader__.load({
|
|
|
3672
3968
|
*/
|
|
3673
3969
|
const accountTools = /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(OAuthSignIn, {
|
|
3674
3970
|
t,
|
|
3675
|
-
region: activeRegion
|
|
3971
|
+
region: activeRegion,
|
|
3972
|
+
onAdded: () => refreshUsage(activeRegion)
|
|
3676
3973
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AccountTransfer, {
|
|
3677
3974
|
t,
|
|
3678
3975
|
region: activeRegion,
|
|
3976
|
+
onChanged: () => refreshUsage(activeRegion),
|
|
3679
3977
|
accounts: status.status === "error" ? [] : status.accounts.map((account) => ({
|
|
3680
3978
|
id: account.id,
|
|
3681
3979
|
accountName: account.accountName
|
|
@@ -3781,6 +4079,11 @@ window.__ModuleLoader__.load({
|
|
|
3781
4079
|
className: "dsm-workbuddy-usage-hint",
|
|
3782
4080
|
children: t("row.reloginHint")
|
|
3783
4081
|
}) : null,
|
|
4082
|
+
accountError === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
4083
|
+
className: "dsm-workbuddy-account-error",
|
|
4084
|
+
role: "alert",
|
|
4085
|
+
children: accountError
|
|
4086
|
+
}),
|
|
3784
4087
|
selectionLost ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
3785
4088
|
className: "dsm-workbuddy-usage-hint",
|
|
3786
4089
|
children: t("row.selectionLostHint")
|
|
@@ -3831,9 +4134,7 @@ window.__ModuleLoader__.load({
|
|
|
3831
4134
|
siblingBusy: saving || togglingRegion !== void 0,
|
|
3832
4135
|
onBusyChange: setPoolBusy,
|
|
3833
4136
|
onSaved: async () => await refreshUsage(activeRegion) !== void 0,
|
|
3834
|
-
onRefresh: () =>
|
|
3835
|
-
refreshUsage(activeRegion);
|
|
3836
|
-
}
|
|
4137
|
+
onRefresh: () => refreshUsage(activeRegion)
|
|
3837
4138
|
}),
|
|
3838
4139
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("section", {
|
|
3839
4140
|
className: "dsm-workbuddy-models",
|
|
@@ -4279,6 +4580,17 @@ window.__ModuleLoader__.load({
|
|
|
4279
4580
|
if (usage.status !== "signed-in") return void 0;
|
|
4280
4581
|
return usage.credits?.total;
|
|
4281
4582
|
}
|
|
4583
|
+
/**
|
|
4584
|
+
* The Host's per-region readout switch off a usage document, when it carries one.
|
|
4585
|
+
*
|
|
4586
|
+
* The `error` variant has no data at all, so it can only ever answer `undefined`
|
|
4587
|
+
* — "the Host did not say", which leaves the settings scope in charge.
|
|
4588
|
+
*/
|
|
4589
|
+
function hostCreditsShownOf(usage) {
|
|
4590
|
+
if (usage === void 0 || usage.status === "error") return void 0;
|
|
4591
|
+
const value = usage.showCreditsInMainUi;
|
|
4592
|
+
return typeof value === "boolean" ? value : void 0;
|
|
4593
|
+
}
|
|
4282
4594
|
/** Group the digits so a four-figure balance stays readable in a narrow row. */
|
|
4283
4595
|
function formatCredits(value) {
|
|
4284
4596
|
return value.toLocaleString("en-US", { maximumFractionDigits: 0 });
|
|
@@ -4365,8 +4677,9 @@ window.__ModuleLoader__.load({
|
|
|
4365
4677
|
if (Array.isArray(data.accounts)) setAccountCredits(data.accounts);
|
|
4366
4678
|
} catch {}
|
|
4367
4679
|
}, [activeRegion]);
|
|
4680
|
+
const hostHidden = hostCreditsShownOf(usage) === false;
|
|
4368
4681
|
(0, react.useEffect)(() => {
|
|
4369
|
-
if (owned === void 0) return void 0;
|
|
4682
|
+
if (owned === void 0 || hostHidden) return void 0;
|
|
4370
4683
|
let cancelled = false;
|
|
4371
4684
|
let timer;
|
|
4372
4685
|
const controller = new AbortController();
|
|
@@ -4381,7 +4694,11 @@ window.__ModuleLoader__.load({
|
|
|
4381
4694
|
if (timer !== void 0) window.clearInterval(timer);
|
|
4382
4695
|
controller.abort();
|
|
4383
4696
|
};
|
|
4384
|
-
}, [
|
|
4697
|
+
}, [
|
|
4698
|
+
owned,
|
|
4699
|
+
hostHidden,
|
|
4700
|
+
fetchUsage
|
|
4701
|
+
]);
|
|
4385
4702
|
(0, react.useEffect)(() => {
|
|
4386
4703
|
if (!open || activeRegion === void 0) return void 0;
|
|
4387
4704
|
const controller = new AbortController();
|
|
@@ -4430,6 +4747,22 @@ window.__ModuleLoader__.load({
|
|
|
4430
4747
|
}
|
|
4431
4748
|
};
|
|
4432
4749
|
if (owned === void 0) return null;
|
|
4750
|
+
/**
|
|
4751
|
+
* The Host's own answer for this region, when the document carries one.
|
|
4752
|
+
*
|
|
4753
|
+
* The Host is the AUTHORITY on whether this readout is wanted, exactly as the
|
|
4754
|
+
* card's `creditsShownOn` treats it — and here it is also the fix for a defect
|
|
4755
|
+
* shipped in 4.3.0 (issue #46): the gate's decision rests on the settings
|
|
4756
|
+
* scope, which on a host whose describe mirror lands late was bound to a
|
|
4757
|
+
* namespace nothing serves. Every read of that scope is then `undefined`, and
|
|
4758
|
+
* the opt-out rule (`!== false`) reads "shown" forever — so unticking the
|
|
4759
|
+
* checkbox wrote correctly and changed nothing on screen.
|
|
4760
|
+
*
|
|
4761
|
+
* Checked AFTER the fetch rather than before it because the answer only
|
|
4762
|
+
* arrives with the document. The cost is one usage read for a readout the user
|
|
4763
|
+
* may have hidden, against a persistent wrong number on screen.
|
|
4764
|
+
*/
|
|
4765
|
+
if (hostCreditsShownOf(usage) === false) return null;
|
|
4433
4766
|
const signedIn = usage?.status === "signed-in" ? usage : void 0;
|
|
4434
4767
|
const total = signedIn === void 0 ? void 0 : creditsOf(signedIn);
|
|
4435
4768
|
const signedOut = usage !== void 0 && usage.status === "signed-out";
|
|
@@ -4616,11 +4949,22 @@ window.__ModuleLoader__.load({
|
|
|
4616
4949
|
* The region's own switches are applied HERE rather than inside the component,
|
|
4617
4950
|
* so a region that is switched off (or whose provider is off) starts no fetch
|
|
4618
4951
|
* loop at all.
|
|
4952
|
+
*
|
|
4953
|
+
* `hostValue` is the Host's own `showCreditsInMainUi` answer for this region,
|
|
4954
|
+
* when a usage document carrying it is already in hand. It OUTRANKS the settings
|
|
4955
|
+
* scope, exactly as the card's `creditsShownOn` does and for the same measured
|
|
4956
|
+
* reason: the browser's settings mirror is documented as stale for this
|
|
4957
|
+
* namespace on the affected deployments, and — the shipped 4.3.0 defect (issue
|
|
4958
|
+
* #46) — the scope can be bound to a namespace nothing serves, where every read
|
|
4959
|
+
* is `undefined` and the opt-out rule then reads "shown" forever. The Host is
|
|
4960
|
+
* the authority; the scope is the fallback for a Host line that predates the
|
|
4961
|
+
* field.
|
|
4619
4962
|
*/
|
|
4620
|
-
function composerCreditsRegionOf(projection, scope) {
|
|
4963
|
+
function composerCreditsRegionOf(projection, scope, hostValue) {
|
|
4621
4964
|
const provider = selectedProviderOf(projection);
|
|
4622
4965
|
const region = provider === void 0 ? void 0 : WORKBUDDY_COMPOSER_PROVIDERS[provider];
|
|
4623
4966
|
if (region === void 0) return void 0;
|
|
4967
|
+
if (typeof hostValue === "boolean") return hostValue ? region : void 0;
|
|
4624
4968
|
const value = scope === void 0 ? void 0 : unwrapVolatileDeep(scope.getSnapshot().value);
|
|
4625
4969
|
if (!regionCreditsShownOf(value, region)) return void 0;
|
|
4626
4970
|
if (!regionEnabledOf(value, region)) return void 0;
|
|
@@ -4681,6 +5025,7 @@ window.__ModuleLoader__.load({
|
|
|
4681
5025
|
"row.sourceDsh": "plugin copy",
|
|
4682
5026
|
"row.searchedMore": "Show the {count} path(s) that were not found",
|
|
4683
5027
|
"row.reloginHint": "If errors occur, sign back in to the WorkBuddy app.",
|
|
5028
|
+
"row.rescanHidesKept": "The accounts you removed are still hidden: the plugin could not update its own list file. On Windows this is usually a file lock — close antivirus or sync tools that may be holding files in the DSH profile, then try again.",
|
|
4684
5029
|
"row.credentialRejectedTitle": "WorkBuddy refused this account's token",
|
|
4685
5030
|
"row.credentialRejectedIntro": "The token for \"{accountName}\" is no longer accepted by the upstream, so credits and check-in are unavailable. Signing in again only helps if the problem is the token itself — here is what actually applies:",
|
|
4686
5031
|
"row.credentialRejectedSwitch": "Another sign-in on this machine answered the upstream: \"{accountName}\". Switching to it fixes this — no need to sign in again.",
|
|
@@ -4751,7 +5096,6 @@ window.__ModuleLoader__.load({
|
|
|
4751
5096
|
"row.oauthStart": "Sign in with QR code",
|
|
4752
5097
|
"row.oauthStarting": "Opening…",
|
|
4753
5098
|
"row.oauthOpenHint": "Open the sign-in page and confirm — the account is added here automatically when you finish:",
|
|
4754
|
-
"row.oauthOpen": "Open sign-in page",
|
|
4755
5099
|
"row.oauthOpened": "Opened. Waiting for you to finish…",
|
|
4756
5100
|
"row.oauthWaiting": "Waiting for the sign-in to finish… ({seconds}s)",
|
|
4757
5101
|
"row.oauthExpires": "The sign-in window expires in about {minutes} min.",
|
|
@@ -4760,7 +5104,14 @@ window.__ModuleLoader__.load({
|
|
|
4760
5104
|
"row.oauthTimedOut": "The sign-in timed out — start again when ready.",
|
|
4761
5105
|
"row.oauthDone": "Added “{accountName}”. Pick it above to use it.",
|
|
4762
5106
|
"row.oauthError": "Sign-in failed: {message}",
|
|
4763
|
-
"row.
|
|
5107
|
+
"row.oauthCopy": "Copy link",
|
|
5108
|
+
"row.oauthCopied": "Link copied",
|
|
5109
|
+
"row.oauthReopen": "Open page again",
|
|
5110
|
+
"row.oauthDialogClose": "Close",
|
|
5111
|
+
"row.oauthTitleGlobal": "Add an account by web sign-in",
|
|
5112
|
+
"row.oauthStartGlobal": "Sign in on the web",
|
|
5113
|
+
"row.veilHintList": "The list refreshes as soon as this finishes.",
|
|
5114
|
+
"row.oauthSyncing": "Adding the account…",
|
|
4764
5115
|
"row.transferTitle": "Export / import accounts",
|
|
4765
5116
|
"row.transferHint": "Move accounts between computers as a JSON file. The file carries the credentials — keep it private. The format is shared with other WorkBuddy credential managers.",
|
|
4766
5117
|
"row.transferExport": "Export accounts",
|
|
@@ -4770,7 +5121,7 @@ window.__ModuleLoader__.load({
|
|
|
4770
5121
|
"row.transferExported": "Exported {count} account(s) to {fileName}.",
|
|
4771
5122
|
"row.transferImport": "Import accounts",
|
|
4772
5123
|
"row.transferImporting": "Importing…",
|
|
4773
|
-
"row.
|
|
5124
|
+
"row.transferPreviewing": "Reading the file…",
|
|
4774
5125
|
"row.transferPreviewTotal": "{total} account(s) in the file — untick what should stay out. Accounts already stored here are overwritten by the import.",
|
|
4775
5126
|
"row.transferImportConfirm": "Import {count} account(s)",
|
|
4776
5127
|
"row.transferImported": "Imported {imported} account(s); {skipped} skipped (no token, or belongs to the other region).",
|
|
@@ -4878,10 +5229,11 @@ window.__ModuleLoader__.load({
|
|
|
4878
5229
|
"row.poolUnsavedMembers": "Selection changed — save to apply",
|
|
4879
5230
|
"row.poolRowTest": "Test",
|
|
4880
5231
|
"row.poolRowRemove": "Remove",
|
|
4881
|
-
"row.poolRowRemoveHint": "
|
|
5232
|
+
"row.poolRowRemoveHint": "Remove this account from the list. The WorkBuddy desktop app is not signed out — it keeps its own sign-in. Removing only hides the account here; 「Detect accounts again」 brings it back.",
|
|
4882
5233
|
"row.poolRowRemoveAria": "Remove the stored credential of {accountName}",
|
|
4883
|
-
"row.poolRowRemoved": "{accountName}:
|
|
4884
|
-
"row.
|
|
5234
|
+
"row.poolRowRemoved": "{accountName}: hidden from the list. 「Detect accounts again」 re-adds it; the desktop app stays signed in.",
|
|
5235
|
+
"row.poolRowRemoving": "Removing the account…",
|
|
5236
|
+
"row.poolRowRemoveDesktopOnly": "{accountName}: hidden from the list — this account comes from the desktop app's own sign-in, which is left untouched. 「Detect accounts again」 re-adds it.",
|
|
4885
5237
|
"row.showCreditsCn": "Show WB CN credits in the composer",
|
|
4886
5238
|
"row.showCreditsGlobal": "Show WB AI credits in the composer",
|
|
4887
5239
|
"row.showCreditsCnHint": "Show the domestic (WB CN) credit balance in the composer toolbar, but only while this session uses a WB CN model. Click it for per-account balances and a manual refresh (refreshes by itself every 5 minutes).",
|
|
@@ -4933,6 +5285,7 @@ window.__ModuleLoader__.load({
|
|
|
4933
5285
|
"row.sourceDsh": "插件副本",
|
|
4934
5286
|
"row.searchedMore": "显示另外 {count} 条「不存在」的路径",
|
|
4935
5287
|
"row.reloginHint": "出现错误,重新登录 WorkBuddy APP 即可。",
|
|
5288
|
+
"row.rescanHidesKept": "已删除的账号仍是隐藏状态:插件没能更新自己的名单文件。Windows 上通常是文件被占用——请关闭可能正在占用 DSH 配置目录的杀毒/同步工具后重试。",
|
|
4936
5289
|
"row.credentialRejectedTitle": "WorkBuddy 拒绝了这个账号的 token",
|
|
4937
5290
|
"row.credentialRejectedIntro": "「{accountName}」的 token 已不被上游接受,所以积分与签到都用不了。重新登录只在「确实是 token 的问题」时才有用——当前实际适用的是下面这一条:",
|
|
4938
5291
|
"row.credentialRejectedSwitch": "本机另一个账号「{accountName}」刚刚实测可用,切换到它即可恢复,不必重新登录。",
|
|
@@ -5003,7 +5356,6 @@ window.__ModuleLoader__.load({
|
|
|
5003
5356
|
"row.oauthStart": "扫码登录",
|
|
5004
5357
|
"row.oauthStarting": "正在打开…",
|
|
5005
5358
|
"row.oauthOpenHint": "打开登录页并完成确认——完成后账号会自动出现在这里:",
|
|
5006
|
-
"row.oauthOpen": "打开登录页",
|
|
5007
5359
|
"row.oauthOpened": "已打开,等待你完成登录…",
|
|
5008
5360
|
"row.oauthWaiting": "正在等待登录完成…({seconds} 秒)",
|
|
5009
5361
|
"row.oauthExpires": "登录窗口约 {minutes} 分钟后过期。",
|
|
@@ -5012,7 +5364,14 @@ window.__ModuleLoader__.load({
|
|
|
5012
5364
|
"row.oauthTimedOut": "登录超时——请重新开始。",
|
|
5013
5365
|
"row.oauthDone": "已添加「{accountName}」。在上方选中它即可使用。",
|
|
5014
5366
|
"row.oauthError": "登录失败:{message}",
|
|
5015
|
-
"row.
|
|
5367
|
+
"row.oauthCopy": "复制链接",
|
|
5368
|
+
"row.oauthCopied": "已复制链接",
|
|
5369
|
+
"row.oauthReopen": "重新打开页面",
|
|
5370
|
+
"row.oauthDialogClose": "关闭",
|
|
5371
|
+
"row.oauthTitleGlobal": "网页登录添加账号",
|
|
5372
|
+
"row.oauthStartGlobal": "网页登录",
|
|
5373
|
+
"row.veilHintList": "完成后列表会立即刷新。",
|
|
5374
|
+
"row.oauthSyncing": "正在添加账号…",
|
|
5016
5375
|
"row.transferTitle": "导出 / 导入账号",
|
|
5017
5376
|
"row.transferHint": "用 JSON 文件在电脑之间迁移账号。文件里是账号凭据——请妥善保管。该格式与同类 WorkBuddy 凭据管理工具通用。",
|
|
5018
5377
|
"row.transferExport": "导出账号",
|
|
@@ -5022,7 +5381,7 @@ window.__ModuleLoader__.load({
|
|
|
5022
5381
|
"row.transferExported": "已导出 {count} 个账号到 {fileName}。",
|
|
5023
5382
|
"row.transferImport": "导入账号",
|
|
5024
5383
|
"row.transferImporting": "正在导入…",
|
|
5025
|
-
"row.
|
|
5384
|
+
"row.transferPreviewing": "正在读取文件…",
|
|
5026
5385
|
"row.transferPreviewTotal": "文件中共 {total} 个账号——不需要的请取消勾选。导入会覆盖这里已存储的同名账号。",
|
|
5027
5386
|
"row.transferImportConfirm": "导入 {count} 个账号",
|
|
5028
5387
|
"row.transferImported": "已导入 {imported} 个账号;跳过 {skipped} 个(缺 token,或属于另一个区域)。",
|
|
@@ -5130,10 +5489,11 @@ window.__ModuleLoader__.load({
|
|
|
5130
5489
|
"row.poolUnsavedMembers": "勾选已改动 —— 保存后生效",
|
|
5131
5490
|
"row.poolRowTest": "测试",
|
|
5132
5491
|
"row.poolRowRemove": "删除",
|
|
5133
|
-
"row.poolRowRemoveHint": "
|
|
5492
|
+
"row.poolRowRemoveHint": "把这个账号从列表中移除。桌面 App 不会被退出登录——它保留自己的登录状态。移除只是暂时隐藏,点「重新检测账号」即可找回。",
|
|
5134
5493
|
"row.poolRowRemoveAria": "删除 {accountName} 的已存凭据",
|
|
5135
|
-
"row.poolRowRemoved": "{accountName}
|
|
5136
|
-
"row.
|
|
5494
|
+
"row.poolRowRemoved": "{accountName}:已从列表中隐藏。点「重新检测账号」会重新加入,桌面 App 的登录不受影响。",
|
|
5495
|
+
"row.poolRowRemoving": "正在删除账号…",
|
|
5496
|
+
"row.poolRowRemoveDesktopOnly": "{accountName}:已从列表中隐藏——这个账号来自桌面 App 自己的登录,该登录未被改动。点「重新检测账号」会重新加入。",
|
|
5137
5497
|
"row.showCreditsCn": "在对话框显示 WB CN 积分",
|
|
5138
5498
|
"row.showCreditsGlobal": "在对话框显示 WB AI 积分",
|
|
5139
5499
|
"row.showCreditsCnHint": "在对话输入框工具栏显示国内版(WB CN)剩余积分,仅当本会话使用 WB CN 模型时出现。点击可看各账号余额并手动刷新(每 5 分钟也会自动刷新)。",
|
|
@@ -5210,17 +5570,10 @@ window.__ModuleLoader__.load({
|
|
|
5210
5570
|
const softGet = (name) => ctx.get(name);
|
|
5211
5571
|
let settingsScope;
|
|
5212
5572
|
const forms = softGet("configForms");
|
|
5213
|
-
|
|
5214
|
-
let ns = WORKBUDDY_SETTINGS_NS;
|
|
5215
|
-
try {
|
|
5216
|
-
const served = (forms.describe().getSnapshot().view?.namespaces ?? []).find((entry) => entry.ns === "workbuddy" || /workbuddy/i.test(entry.ns));
|
|
5217
|
-
if (served !== void 0) ns = served.ns;
|
|
5218
|
-
} catch {}
|
|
5219
|
-
settingsScope = forms.get(ns);
|
|
5220
|
-
}
|
|
5573
|
+
/** Register one card slot; returns its disposer, or undefined if it failed. */
|
|
5221
5574
|
const registerCard = (slotName, key) => {
|
|
5222
5575
|
try {
|
|
5223
|
-
ctx.slots.inject(slotName, () => ctx.slots.register({
|
|
5576
|
+
return ctx.slots.inject(slotName, () => ctx.slots.register({
|
|
5224
5577
|
name: slotName,
|
|
5225
5578
|
key,
|
|
5226
5579
|
priority: 30,
|
|
@@ -5231,10 +5584,9 @@ window.__ModuleLoader__.load({
|
|
|
5231
5584
|
}, WorkBuddyCard));
|
|
5232
5585
|
} catch (error) {
|
|
5233
5586
|
console.error(`[dsh-connect-workbuddy] card slot "${slotName}" failed to register (host provider unaffected):`, error);
|
|
5587
|
+
return;
|
|
5234
5588
|
}
|
|
5235
5589
|
};
|
|
5236
|
-
registerCard("plugins.bundle.config", "dsh-connect-workbuddy");
|
|
5237
|
-
registerCard("plugins.row.config", "dsh-connect-workbuddy#dsh-connect-workbuddy");
|
|
5238
5590
|
/**
|
|
5239
5591
|
* Credit readout in the composer tool row (`conversation.input.left`, a
|
|
5240
5592
|
* session-scope list slot).
|
|
@@ -5255,7 +5607,7 @@ window.__ModuleLoader__.load({
|
|
|
5255
5607
|
*/
|
|
5256
5608
|
const registerComposerCredits = () => {
|
|
5257
5609
|
try {
|
|
5258
|
-
ctx.slots.inject("conversation.input.left", () => ctx.slots.register({
|
|
5610
|
+
return ctx.slots.inject("conversation.input.left", () => ctx.slots.register({
|
|
5259
5611
|
name: "conversation.input.left",
|
|
5260
5612
|
id: "dsh-connect-workbuddy-composer-credits",
|
|
5261
5613
|
order: 4,
|
|
@@ -5267,9 +5619,84 @@ window.__ModuleLoader__.load({
|
|
|
5267
5619
|
}, ComposerCreditsGate));
|
|
5268
5620
|
} catch (error) {
|
|
5269
5621
|
console.error("[dsh-connect-workbuddy] composer credits slot failed to register (host provider unaffected):", error);
|
|
5622
|
+
return;
|
|
5270
5623
|
}
|
|
5271
5624
|
};
|
|
5272
|
-
|
|
5625
|
+
/**
|
|
5626
|
+
* Every slot this plugin contributes, and how to (re)create them.
|
|
5627
|
+
*
|
|
5628
|
+
* Kept as one list because the binding of `settingsScope` and the slot
|
|
5629
|
+
* registrations must move TOGETHER: see {@link bindSettingsScope} for why a
|
|
5630
|
+
* late-arriving namespace has to produce fresh registrations rather than a
|
|
5631
|
+
* mutated closure.
|
|
5632
|
+
*/
|
|
5633
|
+
/**
|
|
5634
|
+
* Disposers of the slots registered for the CURRENT namespace binding.
|
|
5635
|
+
*
|
|
5636
|
+
* Declared before {@link bindSettingsScope} because that function closes
|
|
5637
|
+
* over it and reassigns it; the binding is only armed at the end of `apply`,
|
|
5638
|
+
* so this is initialised by then either way — hoisting it keeps that
|
|
5639
|
+
* ordering a non-requirement rather than a trap.
|
|
5640
|
+
*/
|
|
5641
|
+
let liveDisposers = [];
|
|
5642
|
+
const registerAllSlots = () => [
|
|
5643
|
+
registerCard("plugins.bundle.config", "dsh-connect-workbuddy"),
|
|
5644
|
+
registerCard("plugins.row.config", "dsh-connect-workbuddy#dsh-connect-workbuddy"),
|
|
5645
|
+
registerComposerCredits()
|
|
5646
|
+
].filter((disposer) => disposer !== void 0);
|
|
5647
|
+
/**
|
|
5648
|
+
* Bind `settingsScope` to whichever namespace the Host really serves, and
|
|
5649
|
+
* RE-REGISTER every slot when that answer changes.
|
|
5650
|
+
*
|
|
5651
|
+
* Why this is not a one-shot read at startup — the shipped 4.3.0 defect
|
|
5652
|
+
* (issue #46): the mirror is filled by an async `remote.settings.describe()`,
|
|
5653
|
+
* so at `apply` time `view` is usually still absent. The old code read that
|
|
5654
|
+
* absence as "the declared name must be right", bound
|
|
5655
|
+
* `forms.get('workbuddy')`, and never looked again. `ConfigFormController.
|
|
5656
|
+
* derive()` matches the namespace EXACTLY, so the guessed form stayed
|
|
5657
|
+
* `unavailable` forever; the composer gate — reading only that scope —
|
|
5658
|
+
* judged the opt-out switch `undefined !== false` and kept rendering the
|
|
5659
|
+
* readout. Unticking the box wrote `false` correctly and changed nothing on
|
|
5660
|
+
* screen.
|
|
5661
|
+
*
|
|
5662
|
+
* `whileServed` is the Host's own idiom for this (`mirror.subscribe` plus an
|
|
5663
|
+
* immediate sync). It is strictly better than re-reading the snapshot by
|
|
5664
|
+
* hand because it ALSO withdraws the binding if the Host stops serving our
|
|
5665
|
+
* namespace, and it hands `register` the set of namespaces actually served
|
|
5666
|
+
* — so the choice is made from live data instead of a captured guess.
|
|
5667
|
+
*
|
|
5668
|
+
* `register` runs on the unserved→served EDGE, not on every mirror update
|
|
5669
|
+
* (see the service's `sync`), so the disposal below is not a churn loop: it
|
|
5670
|
+
* clears whatever a previous edge left registered. That replacement is NOT
|
|
5671
|
+
* optional, because the renderer caches each entry's inject result by object
|
|
5672
|
+
* identity (`rootInjectCache` / `cachedRootInject` in
|
|
5673
|
+
* `dsh-client-ui-renderer`: `if (!props) { props = runInject(...) }`), so an
|
|
5674
|
+
* entry registered before the mirror landed is never re-evaluated. Late
|
|
5675
|
+
* binding can therefore only work if the OLD registrations are disposed and
|
|
5676
|
+
* NEW ones created — which is what this callback does.
|
|
5677
|
+
*/
|
|
5678
|
+
const bindSettingsScope = () => {
|
|
5679
|
+
if (forms === void 0 || typeof forms.whileServed !== "function") {
|
|
5680
|
+
settingsScope = forms?.get(WORKBUDDY_SETTINGS_NS);
|
|
5681
|
+
liveDisposers = registerAllSlots();
|
|
5682
|
+
return;
|
|
5683
|
+
}
|
|
5684
|
+
ctx.effect(() => forms.whileServed(WORKBUDDY_SETTINGS_NAMESPACE_CANDIDATES, (served) => {
|
|
5685
|
+
const ns = WORKBUDDY_SETTINGS_NAMESPACE_CANDIDATES.find((candidate) => served.has(candidate)) ?? [...served].find((entry) => /workbuddy/i.test(entry));
|
|
5686
|
+
settingsScope = ns === void 0 ? void 0 : forms.get(ns);
|
|
5687
|
+
for (const dispose of liveDisposers) try {
|
|
5688
|
+
dispose();
|
|
5689
|
+
} catch {}
|
|
5690
|
+
liveDisposers = registerAllSlots();
|
|
5691
|
+
return () => {
|
|
5692
|
+
for (const dispose of liveDisposers) try {
|
|
5693
|
+
dispose();
|
|
5694
|
+
} catch {}
|
|
5695
|
+
liveDisposers = [];
|
|
5696
|
+
};
|
|
5697
|
+
}), "dsh-connect-workbuddy: settings namespace binding");
|
|
5698
|
+
};
|
|
5699
|
+
bindSettingsScope();
|
|
5273
5700
|
} catch (error) {
|
|
5274
5701
|
console.error("[dsh-connect-workbuddy] client card failed to load (host provider unaffected):", error);
|
|
5275
5702
|
}
|