dsh-connect-workbuddy 4.2.0 → 4.3.0
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 +38 -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 +472 -172
- package/lib/{host-heartbeat-C4TXu8ew.js → host-heartbeat-jNlsUMZO.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
|
@@ -649,6 +649,40 @@ window.__ModuleLoader__.load({
|
|
|
649
649
|
}
|
|
650
650
|
}
|
|
651
651
|
//#endregion
|
|
652
|
+
//#region src/client/BusyOverlay.tsx
|
|
653
|
+
/**
|
|
654
|
+
* The veil. Deliberately NOT a modal dialog: it carries no focusable control
|
|
655
|
+
* and no dismissal — there is nothing for the user to decide, only a step to
|
|
656
|
+
* wait through. `aria-busy` plus a polite live region state that to screen
|
|
657
|
+
* readers, which is the whole accessibility contract a wait screen needs.
|
|
658
|
+
*/
|
|
659
|
+
function BusyOverlay({ message, hint, visible }) {
|
|
660
|
+
if (!visible) return null;
|
|
661
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
662
|
+
className: "dsm-workbuddy-veil",
|
|
663
|
+
role: "status",
|
|
664
|
+
"aria-busy": "true",
|
|
665
|
+
"aria-live": "polite",
|
|
666
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
667
|
+
className: "dsm-workbuddy-veil-card",
|
|
668
|
+
children: [
|
|
669
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
670
|
+
className: "dsm-workbuddy-veil-spinner",
|
|
671
|
+
"aria-hidden": "true"
|
|
672
|
+
}),
|
|
673
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
674
|
+
className: "dsm-workbuddy-veil-text",
|
|
675
|
+
children: message
|
|
676
|
+
}),
|
|
677
|
+
hint === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
678
|
+
className: "dsm-workbuddy-veil-hint",
|
|
679
|
+
children: hint
|
|
680
|
+
})
|
|
681
|
+
]
|
|
682
|
+
})
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
//#endregion
|
|
652
686
|
//#region src/client/ndjson.ts
|
|
653
687
|
/**
|
|
654
688
|
* Read a newline-delimited JSON body, one object at a time.
|
|
@@ -1151,6 +1185,24 @@ window.__ModuleLoader__.load({
|
|
|
1151
1185
|
memberAccountIds: pool?.memberAccountIds ?? []
|
|
1152
1186
|
};
|
|
1153
1187
|
const active = draftRegion === region && draft !== null ? draft : saved;
|
|
1188
|
+
/**
|
|
1189
|
+
* Whether the pool is RUNNING right now, which is the SAVED value.
|
|
1190
|
+
*
|
|
1191
|
+
* `active` is the user's draft and only becomes reality at Save. Using it for
|
|
1192
|
+
* the parts that DESCRIBE the running pool made the card state a mode that had
|
|
1193
|
+
* not taken effect yet: flipping the switch off (unsaved) revealed the manual
|
|
1194
|
+
* picker while the pool was still ranking every request, so the picker showed
|
|
1195
|
+
* the rotated account as "in use" and a selection made there was overridden by
|
|
1196
|
+
* the very pool the user was looking at — the "shows A, bills B" trap this
|
|
1197
|
+
* section keeps guarding against. The mirror case was milder but the same
|
|
1198
|
+
* class of wrong: turning the pool ON (unsaved) hid the picker while the manual
|
|
1199
|
+
* account was still the one being billed.
|
|
1200
|
+
*
|
|
1201
|
+
* Everything that reports pool state reads this; only the switch itself (its
|
|
1202
|
+
* controlled value) and `dirty` read the draft, because those two ARE about
|
|
1203
|
+
* the pending edit.
|
|
1204
|
+
*/
|
|
1205
|
+
const poolRunning = saved.enabled;
|
|
1154
1206
|
const dirty = draftRegion === region && draft !== null && (draft.enabled !== saved.enabled || draft.targetModelId !== saved.targetModelId || draft.probeInputTokens !== saved.probeInputTokens || !sameIds(draft.memberAccountIds, saved.memberAccountIds));
|
|
1155
1207
|
/**
|
|
1156
1208
|
* Membership that actually resolves to a local sign-in — what a batch runs on.
|
|
@@ -1405,6 +1457,14 @@ window.__ModuleLoader__.load({
|
|
|
1405
1457
|
/** The account whose per-row action is in flight; gates every row's buttons. */
|
|
1406
1458
|
const [rowBusyAccountId, setRowBusyAccountId] = (0, react.useState)(void 0);
|
|
1407
1459
|
/**
|
|
1460
|
+
* Which per-row action is running, for the veil's scenario line.
|
|
1461
|
+
*
|
|
1462
|
+
* A removal and a test both set `rowBusyAccountId`, but they mean different
|
|
1463
|
+
* things to the person watching ("this account is going away" versus "this
|
|
1464
|
+
* account is being measured") — and the veil exists precisely to say which.
|
|
1465
|
+
*/
|
|
1466
|
+
const [rowBusyAction, setRowBusyAction] = (0, react.useState)(void 0);
|
|
1467
|
+
/**
|
|
1408
1468
|
* Test ONE account from its row.
|
|
1409
1469
|
*
|
|
1410
1470
|
* Same Host rule as the batch (testOne reuses the batch's probe), the same
|
|
@@ -1413,6 +1473,7 @@ window.__ModuleLoader__.load({
|
|
|
1413
1473
|
*/
|
|
1414
1474
|
const runRowTest = (0, react.useCallback)(async (accountId) => {
|
|
1415
1475
|
setRowBusyAccountId(accountId);
|
|
1476
|
+
setRowBusyAction("test");
|
|
1416
1477
|
setActionError(void 0);
|
|
1417
1478
|
const url = withWorkBuddyRegionAndAction(WORKBUDDY_POOL_PATH, region, "test") + "&accountId=" + encodeURIComponent(accountId);
|
|
1418
1479
|
try {
|
|
@@ -1447,7 +1508,10 @@ window.__ModuleLoader__.load({
|
|
|
1447
1508
|
appendLog(t("row.poolLogBatchFailed", { message }), "error");
|
|
1448
1509
|
}
|
|
1449
1510
|
} finally {
|
|
1450
|
-
if (mounted.current)
|
|
1511
|
+
if (mounted.current) {
|
|
1512
|
+
setRowBusyAccountId(void 0);
|
|
1513
|
+
setRowBusyAction(void 0);
|
|
1514
|
+
}
|
|
1451
1515
|
}
|
|
1452
1516
|
}, [
|
|
1453
1517
|
appendLog,
|
|
@@ -1456,12 +1520,17 @@ window.__ModuleLoader__.load({
|
|
|
1456
1520
|
t
|
|
1457
1521
|
]);
|
|
1458
1522
|
/**
|
|
1459
|
-
* Remove ONE account
|
|
1460
|
-
*
|
|
1461
|
-
*
|
|
1523
|
+
* Remove ONE account from its row, via the Host.
|
|
1524
|
+
*
|
|
1525
|
+
* The Host dismisses the account (and deletes its vault entry when it has
|
|
1526
|
+
* one), so the row stays gone across rescans. The desktop app is not signed
|
|
1527
|
+
* out. The message below still distinguishes the two cases, because "there
|
|
1528
|
+
* was no plugin-stored copy" is worth saying: it tells the user the account
|
|
1529
|
+
* comes from the app itself rather than from an OAuth sign-in or an import.
|
|
1462
1530
|
*/
|
|
1463
1531
|
const runRowRemove = (0, react.useCallback)(async (accountId, accountName) => {
|
|
1464
1532
|
setRowBusyAccountId(accountId);
|
|
1533
|
+
setRowBusyAction("remove");
|
|
1465
1534
|
setActionError(void 0);
|
|
1466
1535
|
const url = withWorkBuddyRegionAndAction(WORKBUDDY_POOL_PATH, region, "remove") + "&accountId=" + encodeURIComponent(accountId);
|
|
1467
1536
|
try {
|
|
@@ -1478,7 +1547,7 @@ window.__ModuleLoader__.load({
|
|
|
1478
1547
|
}));
|
|
1479
1548
|
if (!mounted.current) return;
|
|
1480
1549
|
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?.();
|
|
1550
|
+
await onRefresh?.();
|
|
1482
1551
|
} catch (error) {
|
|
1483
1552
|
const message = error instanceof Error ? error.message : t("row.requestFailed");
|
|
1484
1553
|
if (mounted.current) {
|
|
@@ -1486,7 +1555,10 @@ window.__ModuleLoader__.load({
|
|
|
1486
1555
|
appendLog(t("row.poolLogBatchFailed", { message }), "error");
|
|
1487
1556
|
}
|
|
1488
1557
|
} finally {
|
|
1489
|
-
if (mounted.current)
|
|
1558
|
+
if (mounted.current) {
|
|
1559
|
+
setRowBusyAccountId(void 0);
|
|
1560
|
+
setRowBusyAction(void 0);
|
|
1561
|
+
}
|
|
1490
1562
|
}
|
|
1491
1563
|
}, [
|
|
1492
1564
|
appendLog,
|
|
@@ -1517,7 +1589,15 @@ window.__ModuleLoader__.load({
|
|
|
1517
1589
|
*/
|
|
1518
1590
|
const currentAccount = pool.accounts.find((account) => account.current);
|
|
1519
1591
|
const currentName = currentAccount === void 0 ? void 0 : currentAccount.accountName === "" ? t("row.accountUnnamed") : currentAccount.accountName;
|
|
1520
|
-
return (0, react.createElement)("section", { className: "dsm-workbuddy-pool" },
|
|
1592
|
+
return (0, react.createElement)("section", { className: "dsm-workbuddy-pool" }, rowBusyAction === "remove" ? (0, react.createElement)(BusyOverlay, {
|
|
1593
|
+
visible: true,
|
|
1594
|
+
message: t("row.poolRowRemoving"),
|
|
1595
|
+
hint: t("row.veilHintList")
|
|
1596
|
+
}) : null, saving ? (0, react.createElement)(BusyOverlay, {
|
|
1597
|
+
visible: true,
|
|
1598
|
+
message: t("row.poolSaving"),
|
|
1599
|
+
hint: t("row.veilHintList")
|
|
1600
|
+
}) : 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
1601
|
className: "dsm-workbuddy-pool-select dsm-workbuddy-pool-account-select",
|
|
1522
1602
|
value: currentAccount?.accountId ?? "",
|
|
1523
1603
|
disabled: selectingAccount === true,
|
|
@@ -1550,7 +1630,7 @@ window.__ModuleLoader__.load({
|
|
|
1550
1630
|
className: "dsm-btn dsm-btn-outline",
|
|
1551
1631
|
disabled: rescanning === true,
|
|
1552
1632
|
onClick: onRescan
|
|
1553
|
-
}, rescanning === true ? t("row.accountsScanning") : t("row.accountsRescan")))), !
|
|
1633
|
+
}, 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
1634
|
className: "dsm-workbuddy-pool-warn",
|
|
1555
1635
|
role: "status"
|
|
1556
1636
|
}, `${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 +1853,22 @@ window.__ModuleLoader__.load({
|
|
|
1773
1853
|
//#endregion
|
|
1774
1854
|
//#region src/client/OAuthSignIn.tsx
|
|
1775
1855
|
/**
|
|
1776
|
-
* The card's OAuth QR sign-in section
|
|
1777
|
-
*
|
|
1778
|
-
*
|
|
1779
|
-
*
|
|
1780
|
-
*
|
|
1781
|
-
*
|
|
1782
|
-
*
|
|
1783
|
-
*
|
|
1784
|
-
*
|
|
1785
|
-
*
|
|
1786
|
-
*
|
|
1787
|
-
*
|
|
1788
|
-
*
|
|
1856
|
+
* The card's OAuth QR sign-in section: one resting bar row plus a dialog
|
|
1857
|
+
* opened by the sign-in button.
|
|
1858
|
+
*
|
|
1859
|
+
* 参考:workbuddy2api-panel 的「添加账号」弹窗交互形态 —— 静态入口一行,
|
|
1860
|
+
* 点开后弹窗内分步走:获取授权链接 → 自动打开浏览器 → 弹窗内显示链接
|
|
1861
|
+
* 与等待状态 → 完成/失败回填结果。本实现沿用该步骤划分;弹窗样式直接
|
|
1862
|
+
* 用本项目导出/导入弹框的类(dsm-workbuddy-transfer-dialog),不引入
|
|
1863
|
+
* 第二套视觉。
|
|
1864
|
+
* 改动:自动打开浏览器。弹窗拦截的规避方式是「用户手势内同步 window.open
|
|
1865
|
+
* 占位,拿到授权 URL 后再导航」——点击「获取授权链接」的那一刻浏览器
|
|
1866
|
+
* 仍认定这是用户激活的手势链,占位窗口不会被拦;若 Host 环境连占位都
|
|
1867
|
+
* 拦(返回 null),降级为弹窗内手动「打开登录页」链接,功能不损失。
|
|
1868
|
+
*
|
|
1869
|
+
* 轮询循环压成一个组件内 effect:按 `OAUTH_POLL_INTERVAL_MS` 节奏问 Host
|
|
1870
|
+
* 「完成了吗」,完成即停。所有状态就地管理,零跨组件往返;Host 重启
|
|
1871
|
+
* 导致 loginId 失效会得到 done+error,与超时同一条文案路径。
|
|
1789
1872
|
*
|
|
1790
1873
|
* @module dsh-connect-workbuddy/client/OAuthSignIn
|
|
1791
1874
|
*/
|
|
@@ -1793,13 +1876,24 @@ window.__ModuleLoader__.load({
|
|
|
1793
1876
|
function elapsedSeconds(sinceMs, nowMs) {
|
|
1794
1877
|
return Math.max(0, Math.floor((nowMs - sinceMs) / 1e3));
|
|
1795
1878
|
}
|
|
1879
|
+
/** Copy the sign-in URL to the clipboard; true on success. */
|
|
1880
|
+
async function copyUrl(url) {
|
|
1881
|
+
try {
|
|
1882
|
+
await navigator.clipboard.writeText(url);
|
|
1883
|
+
return true;
|
|
1884
|
+
} catch {
|
|
1885
|
+
return false;
|
|
1886
|
+
}
|
|
1887
|
+
}
|
|
1796
1888
|
/**
|
|
1797
1889
|
* The section. Rendered on BOTH sign-in states: a signed-out machine gains
|
|
1798
1890
|
* its first account here; a signed-in one can add another without leaving
|
|
1799
1891
|
* the card.
|
|
1800
1892
|
*/
|
|
1801
|
-
function OAuthSignIn({ t, region }) {
|
|
1893
|
+
function OAuthSignIn({ t, region, onAdded }) {
|
|
1802
1894
|
const [phase, setPhase] = (0, react.useState)({ kind: "idle" });
|
|
1895
|
+
const [dialogOpen, setDialogOpen] = (0, react.useState)(false);
|
|
1896
|
+
const [copied, setCopied] = (0, react.useState)(false);
|
|
1803
1897
|
const [pollTick, setPollTick] = (0, react.useState)(0);
|
|
1804
1898
|
const [nowMs, setNowMs] = (0, react.useState)(() => Date.now());
|
|
1805
1899
|
const mounted = (0, react.useRef)(true);
|
|
@@ -1826,7 +1920,11 @@ window.__ModuleLoader__.load({
|
|
|
1826
1920
|
return () => {
|
|
1827
1921
|
clearInterval(timer);
|
|
1828
1922
|
};
|
|
1829
|
-
}, [
|
|
1923
|
+
}, [
|
|
1924
|
+
waiting,
|
|
1925
|
+
phase,
|
|
1926
|
+
t
|
|
1927
|
+
]);
|
|
1830
1928
|
(0, react.useEffect)(() => {
|
|
1831
1929
|
if (phase.kind !== "waiting") return;
|
|
1832
1930
|
const { loginId } = phase;
|
|
@@ -1862,11 +1960,20 @@ window.__ModuleLoader__.load({
|
|
|
1862
1960
|
message: "error",
|
|
1863
1961
|
error: body.error
|
|
1864
1962
|
});
|
|
1865
|
-
else
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1963
|
+
else {
|
|
1964
|
+
const accountName = body.accountName ?? "";
|
|
1965
|
+
setPhase({
|
|
1966
|
+
kind: "syncing",
|
|
1967
|
+
accountName
|
|
1968
|
+
});
|
|
1969
|
+
await onAdded?.();
|
|
1970
|
+
if (cancelled || !mounted.current) return;
|
|
1971
|
+
setPhase({
|
|
1972
|
+
kind: "finished",
|
|
1973
|
+
message: "added",
|
|
1974
|
+
accountName
|
|
1975
|
+
});
|
|
1976
|
+
}
|
|
1870
1977
|
} catch {
|
|
1871
1978
|
setPollTick((tick) => tick + 1);
|
|
1872
1979
|
}
|
|
@@ -1877,12 +1984,47 @@ window.__ModuleLoader__.load({
|
|
|
1877
1984
|
clearTimeout(timer);
|
|
1878
1985
|
};
|
|
1879
1986
|
}, [
|
|
1987
|
+
onAdded,
|
|
1880
1988
|
phase,
|
|
1881
1989
|
pollTick,
|
|
1882
1990
|
region
|
|
1883
1991
|
]);
|
|
1884
|
-
|
|
1992
|
+
/**
|
|
1993
|
+
* Open a placeholder window WITHOUT `noopener`, and deliberately so.
|
|
1994
|
+
*
|
|
1995
|
+
* `noopener` is the correct feature for a link whose destination never needs
|
|
1996
|
+
* a handle — and that is exactly why it is wrong here. Per the spec, a
|
|
1997
|
+
* `window.open` call carrying `noopener` opens the tab but returns `null`
|
|
1998
|
+
* (MDN: "the new window will not have access to the originating window via
|
|
1999
|
+
* `Window.opener` and returns `null`"). The placeholder exists ONLY to be
|
|
2000
|
+
* navigated once the Host answers, so the handle IS the point: passing
|
|
2001
|
+
* `noopener` made `placeholder` null, the navigation was skipped, and every
|
|
2002
|
+
* sign-in left an empty `about:blank` tab behind instead of the sign-in page.
|
|
2003
|
+
*
|
|
2004
|
+
* The security property is kept, just established on the handle instead of
|
|
2005
|
+
* through the feature: `opener = null` is applied to the placeholder before
|
|
2006
|
+
* anything is loaded into it, so the destination still cannot reach back
|
|
2007
|
+
* through `window.opener`. `noreferrer` is dropped for the same reason —
|
|
2008
|
+
* the spec makes it imply `noopener`, so it would reintroduce the null.
|
|
2009
|
+
* Nothing is lost: the placeholder is same-origin and empty until navigated,
|
|
2010
|
+
* and the referrer would only ever have been the settings page.
|
|
2011
|
+
*/
|
|
2012
|
+
function openPlaceholder() {
|
|
2013
|
+
const placeholder = window.open("", "_blank");
|
|
2014
|
+
if (placeholder !== null) placeholder.opener = null;
|
|
2015
|
+
return placeholder;
|
|
2016
|
+
}
|
|
2017
|
+
/**
|
|
2018
|
+
* Fetch the authorization URL AND open the browser in one gesture chain.
|
|
2019
|
+
* The placeholder window is opened synchronously — inside the click's user
|
|
2020
|
+
* activation, before any await — so popup blocking cannot intervene; the
|
|
2021
|
+
* URL is navigated once the Host answers. A blocked placeholder (null)
|
|
2022
|
+
* degrades to the manual link inside the dialog, losing nothing but the
|
|
2023
|
+
* convenience.
|
|
2024
|
+
*/
|
|
2025
|
+
const start = async (placeholder) => {
|
|
1885
2026
|
setPhase({ kind: "opening" });
|
|
2027
|
+
setCopied(false);
|
|
1886
2028
|
try {
|
|
1887
2029
|
const response = await fetch(withWorkBuddyRouteAction(WORKBUDDY_OAUTH_PATH, region, "start"), {
|
|
1888
2030
|
method: "POST",
|
|
@@ -1890,8 +2032,12 @@ window.__ModuleLoader__.load({
|
|
|
1890
2032
|
credentials: "same-origin"
|
|
1891
2033
|
});
|
|
1892
2034
|
const body = await response.json();
|
|
1893
|
-
if (!mounted.current)
|
|
2035
|
+
if (!mounted.current) {
|
|
2036
|
+
placeholder?.close();
|
|
2037
|
+
return;
|
|
2038
|
+
}
|
|
1894
2039
|
if (!response.ok || typeof body.loginId !== "string" || typeof body.verificationUri !== "string") {
|
|
2040
|
+
placeholder?.close();
|
|
1895
2041
|
setPhase({
|
|
1896
2042
|
kind: "finished",
|
|
1897
2043
|
message: "error",
|
|
@@ -1899,6 +2045,7 @@ window.__ModuleLoader__.load({
|
|
|
1899
2045
|
});
|
|
1900
2046
|
return;
|
|
1901
2047
|
}
|
|
2048
|
+
if (placeholder !== null) placeholder.location.href = body.verificationUri;
|
|
1902
2049
|
setPhase({
|
|
1903
2050
|
kind: "waiting",
|
|
1904
2051
|
loginId: body.loginId,
|
|
@@ -1908,6 +2055,7 @@ window.__ModuleLoader__.load({
|
|
|
1908
2055
|
});
|
|
1909
2056
|
setNowMs(Date.now());
|
|
1910
2057
|
} catch (error) {
|
|
2058
|
+
placeholder?.close();
|
|
1911
2059
|
if (mounted.current) setPhase({
|
|
1912
2060
|
kind: "finished",
|
|
1913
2061
|
message: "error",
|
|
@@ -1915,72 +2063,133 @@ window.__ModuleLoader__.load({
|
|
|
1915
2063
|
});
|
|
1916
2064
|
}
|
|
1917
2065
|
};
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
2066
|
+
/** Open the dialog and begin the login in the same click: the placeholder
|
|
2067
|
+
* window must be created synchronously to count as a user gesture. */
|
|
2068
|
+
const beginSignIn = () => {
|
|
2069
|
+
setDialogOpen(true);
|
|
2070
|
+
start(openPlaceholder());
|
|
2071
|
+
};
|
|
2072
|
+
/** Re-open the sign-in page from the waiting dialog (no new session).
|
|
2073
|
+
*
|
|
2074
|
+
* Here `noopener,noreferrer` IS right: this is a one-way navigation to a
|
|
2075
|
+
* third-party page and nothing needs a handle on it — the contrast with
|
|
2076
|
+
* {@link openPlaceholder} is the whole distinction. */
|
|
2077
|
+
const reopen = () => {
|
|
2078
|
+
if (phase.kind !== "waiting") return;
|
|
2079
|
+
window.open(phase.verificationUri, "_blank", "noopener,noreferrer");
|
|
2080
|
+
};
|
|
2081
|
+
/**
|
|
2082
|
+
* The three strings that NAME the sign-in method, per region.
|
|
2083
|
+
*
|
|
2084
|
+
* CN signs in by scanning a QR code with the WorkBuddy app; the international
|
|
2085
|
+
* build signs in on a web page. Every other line in this section (waiting,
|
|
2086
|
+
* cancel, done, copy) is about the flow, not the method, so it stays shared —
|
|
2087
|
+
* splitting it too would let the two regions drift apart for no gain.
|
|
2088
|
+
*/
|
|
2089
|
+
const titleKey = region === "cn" ? "row.oauthTitle" : "row.oauthTitleGlobal";
|
|
2090
|
+
const startKey = region === "cn" ? "row.oauthStart" : "row.oauthStartGlobal";
|
|
2091
|
+
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
2092
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
1937
2093
|
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
|
-
]
|
|
2094
|
+
"aria-label": t(titleKey),
|
|
2095
|
+
children: [
|
|
2096
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(BusyOverlay, {
|
|
2097
|
+
visible: phase.kind === "syncing",
|
|
2098
|
+
message: t("row.oauthSyncing"),
|
|
2099
|
+
hint: t("row.veilHintList")
|
|
1955
2100
|
}),
|
|
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
|
-
]
|
|
2101
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2102
|
+
className: "dsm-workbuddy-bar-label",
|
|
2103
|
+
children: t(titleKey)
|
|
1963
2104
|
}),
|
|
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
|
-
|
|
2105
|
+
phase.kind === "finished" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2106
|
+
className: `dsm-workbuddy-bar-status ${phase.message === "added" ? "dsm-workbuddy-bar-ok" : "dsm-workbuddy-bar-error"}`,
|
|
2107
|
+
children: finishText
|
|
2108
|
+
}) : phase.kind === "waiting" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2109
|
+
className: "dsm-workbuddy-bar-status",
|
|
2110
|
+
children: t("row.oauthOpened")
|
|
2111
|
+
}) : null,
|
|
2112
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2113
|
+
className: "dsm-workbuddy-bar-actions",
|
|
2114
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2115
|
+
type: "button",
|
|
2116
|
+
className: "dsm-btn dsm-btn-outline",
|
|
2117
|
+
disabled: phase.kind === "opening",
|
|
2118
|
+
onClick: beginSignIn,
|
|
2119
|
+
children: phase.kind === "opening" ? t("row.oauthStarting") : t(startKey)
|
|
2120
|
+
})
|
|
2121
|
+
}),
|
|
2122
|
+
dialogOpen ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2123
|
+
className: "dsm-workbuddy-transfer-dialog",
|
|
2124
|
+
role: "dialog",
|
|
2125
|
+
"aria-label": t(titleKey),
|
|
2126
|
+
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: [
|
|
2127
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t("row.oauthOpenHint") }),
|
|
2128
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2129
|
+
className: "dsm-workbuddy-oauth-url",
|
|
2130
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
|
|
2131
|
+
href: phase.verificationUri,
|
|
2132
|
+
target: "_blank",
|
|
2133
|
+
rel: "noopener noreferrer",
|
|
2134
|
+
children: phase.verificationUri
|
|
2135
|
+
})
|
|
2136
|
+
}),
|
|
2137
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
|
|
2138
|
+
className: "dsm-workbuddy-hint",
|
|
2139
|
+
children: [
|
|
2140
|
+
t("row.oauthWaiting", { seconds: elapsedSeconds(phase.startedAtMs, nowMs) }),
|
|
2141
|
+
" ",
|
|
2142
|
+
t("row.oauthExpires", { minutes: Math.max(1, Math.round((phase.expiresAtMs - nowMs) / 6e4)) })
|
|
2143
|
+
]
|
|
2144
|
+
}),
|
|
2145
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2146
|
+
className: "dsm-workbuddy-transfer-dialog-actions",
|
|
2147
|
+
children: [
|
|
2148
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2149
|
+
type: "button",
|
|
2150
|
+
className: "dsm-btn dsm-btn-outline",
|
|
2151
|
+
onClick: () => {
|
|
2152
|
+
copyUrl(phase.verificationUri).then((ok) => {
|
|
2153
|
+
if (mounted.current) setCopied(ok);
|
|
2154
|
+
});
|
|
2155
|
+
},
|
|
2156
|
+
children: copied ? t("row.oauthCopied") : t("row.oauthCopy")
|
|
2157
|
+
}),
|
|
2158
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2159
|
+
type: "button",
|
|
2160
|
+
className: "dsm-btn dsm-btn-outline",
|
|
2161
|
+
onClick: reopen,
|
|
2162
|
+
children: t("row.oauthReopen")
|
|
2163
|
+
}),
|
|
2164
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2165
|
+
type: "button",
|
|
2166
|
+
className: "dsm-btn dsm-btn-outline",
|
|
2167
|
+
onClick: () => {
|
|
2168
|
+
setPhase({
|
|
2169
|
+
kind: "finished",
|
|
2170
|
+
message: "cancelled"
|
|
2171
|
+
});
|
|
2172
|
+
},
|
|
2173
|
+
children: t("row.oauthCancel")
|
|
2174
|
+
})
|
|
2175
|
+
]
|
|
2176
|
+
})
|
|
2177
|
+
] }) : phase.kind === "finished" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2178
|
+
className: `${phase.message === "added" ? "dsm-workbuddy-bar-ok" : phase.message === "error" ? "dsm-workbuddy-bar-error" : ""}`,
|
|
2179
|
+
children: finishText
|
|
2180
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2181
|
+
className: "dsm-workbuddy-transfer-dialog-actions",
|
|
2182
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2183
|
+
type: "button",
|
|
2184
|
+
className: "dsm-btn dsm-btn-outline",
|
|
2185
|
+
onClick: () => {
|
|
2186
|
+
setDialogOpen(false);
|
|
2187
|
+
},
|
|
2188
|
+
children: t("row.oauthDialogClose")
|
|
2189
|
+
})
|
|
2190
|
+
})] }) : null
|
|
2191
|
+
}) : null
|
|
2192
|
+
]
|
|
1984
2193
|
});
|
|
1985
2194
|
}
|
|
1986
2195
|
//#endregion
|
|
@@ -2022,13 +2231,24 @@ window.__ModuleLoader__.load({
|
|
|
2022
2231
|
/** The section. Rendered on both sign-in states — a signed-out machine can
|
|
2023
2232
|
* still import a file exported elsewhere; export without accounts answers
|
|
2024
2233
|
* the error line. */
|
|
2025
|
-
function AccountTransfer({ t, region, accounts }) {
|
|
2234
|
+
function AccountTransfer({ t, region, accounts, onChanged }) {
|
|
2026
2235
|
const [phase, setPhase] = (0, react.useState)({ kind: "idle" });
|
|
2027
2236
|
const [dialog, setDialog] = (0, react.useState)(void 0);
|
|
2028
2237
|
const [selected, setSelected] = (0, react.useState)(/* @__PURE__ */ new Set());
|
|
2029
2238
|
const [preview, setPreview] = (0, react.useState)(void 0);
|
|
2030
2239
|
const [importSelected, setImportSelected] = (0, react.useState)(/* @__PURE__ */ new Set());
|
|
2031
2240
|
const fileInput = (0, react.useRef)(null);
|
|
2241
|
+
/**
|
|
2242
|
+
* The text of the file the user just picked.
|
|
2243
|
+
*
|
|
2244
|
+
* Held here rather than re-read from the input at import time: the picker
|
|
2245
|
+
* is a bare system dialog that closes immediately (see the import button),
|
|
2246
|
+
* so between preview and confirm there is no dialog on screen holding the
|
|
2247
|
+
* File. Re-reading `fileInput.current.files` would also break the moment
|
|
2248
|
+
* anything clears the input — and it must be cleared after every pick, or
|
|
2249
|
+
* choosing the SAME file twice fires no change event.
|
|
2250
|
+
*/
|
|
2251
|
+
const importText = (0, react.useRef)("");
|
|
2032
2252
|
const mounted = (0, react.useRef)(true);
|
|
2033
2253
|
(0, react.useEffect)(() => {
|
|
2034
2254
|
mounted.current = true;
|
|
@@ -2084,6 +2304,7 @@ window.__ModuleLoader__.load({
|
|
|
2084
2304
|
setPhase({ kind: "importing" });
|
|
2085
2305
|
try {
|
|
2086
2306
|
const text = await file.text();
|
|
2307
|
+
importText.current = text;
|
|
2087
2308
|
const response = await fetch(withWorkBuddyRouteAction(WORKBUDDY_TRANSFER_PATH, region, "preview"), {
|
|
2088
2309
|
method: "POST",
|
|
2089
2310
|
headers: {
|
|
@@ -2113,10 +2334,10 @@ window.__ModuleLoader__.load({
|
|
|
2113
2334
|
}
|
|
2114
2335
|
};
|
|
2115
2336
|
const runImport = async () => {
|
|
2116
|
-
if (preview === void 0 || importSelected.size === 0 ||
|
|
2337
|
+
if (preview === void 0 || importSelected.size === 0 || importText.current === "") return;
|
|
2117
2338
|
setPhase({ kind: "importing" });
|
|
2118
2339
|
try {
|
|
2119
|
-
const text =
|
|
2340
|
+
const text = importText.current;
|
|
2120
2341
|
const response = await fetch(withWorkBuddyRouteAction(WORKBUDDY_TRANSFER_PATH, region, "import"), {
|
|
2121
2342
|
method: "POST",
|
|
2122
2343
|
headers: {
|
|
@@ -2139,12 +2360,19 @@ window.__ModuleLoader__.load({
|
|
|
2139
2360
|
return;
|
|
2140
2361
|
}
|
|
2141
2362
|
closeDialog();
|
|
2363
|
+
const message = t("row.transferImported", {
|
|
2364
|
+
imported: body.imported,
|
|
2365
|
+
skipped: body.skipped ?? 0
|
|
2366
|
+
});
|
|
2367
|
+
setPhase({
|
|
2368
|
+
kind: "refreshing",
|
|
2369
|
+
message
|
|
2370
|
+
});
|
|
2371
|
+
await onChanged?.();
|
|
2372
|
+
if (!mounted.current) return;
|
|
2142
2373
|
setPhase({
|
|
2143
2374
|
kind: "done",
|
|
2144
|
-
message
|
|
2145
|
-
imported: body.imported,
|
|
2146
|
-
skipped: body.skipped ?? 0
|
|
2147
|
-
})
|
|
2375
|
+
message
|
|
2148
2376
|
});
|
|
2149
2377
|
} catch (error) {
|
|
2150
2378
|
if (mounted.current) setPhase({
|
|
@@ -2153,11 +2381,18 @@ window.__ModuleLoader__.load({
|
|
|
2153
2381
|
});
|
|
2154
2382
|
}
|
|
2155
2383
|
};
|
|
2156
|
-
const busy = phase.kind === "exporting" || phase.kind === "importing";
|
|
2384
|
+
const busy = phase.kind === "exporting" || phase.kind === "importing" || phase.kind === "refreshing";
|
|
2385
|
+
/** The veil's scenario line: what is happening, in the user's terms. */
|
|
2386
|
+
const veilMessage = phase.kind === "exporting" ? t("row.transferExporting") : phase.kind === "importing" || phase.kind === "refreshing" ? t("row.transferImporting") : "";
|
|
2157
2387
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
2158
2388
|
className: "dsm-workbuddy-bar",
|
|
2159
2389
|
"aria-label": t("row.transferTitle"),
|
|
2160
2390
|
children: [
|
|
2391
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(BusyOverlay, {
|
|
2392
|
+
visible: busy,
|
|
2393
|
+
message: veilMessage,
|
|
2394
|
+
...busy ? { hint: t("row.veilHintList") } : {}
|
|
2395
|
+
}),
|
|
2161
2396
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2162
2397
|
className: "dsm-workbuddy-bar-label",
|
|
2163
2398
|
children: t("row.transferTitle")
|
|
@@ -2185,13 +2420,27 @@ window.__ModuleLoader__.load({
|
|
|
2185
2420
|
className: "dsm-btn dsm-btn-outline",
|
|
2186
2421
|
disabled: busy,
|
|
2187
2422
|
onClick: () => {
|
|
2188
|
-
|
|
2189
|
-
setImportSelected(/* @__PURE__ */ new Set());
|
|
2190
|
-
setDialog("import");
|
|
2423
|
+
fileInput.current?.click();
|
|
2191
2424
|
},
|
|
2192
2425
|
children: t("row.transferImport")
|
|
2193
2426
|
})]
|
|
2194
2427
|
}),
|
|
2428
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
2429
|
+
ref: fileInput,
|
|
2430
|
+
type: "file",
|
|
2431
|
+
accept: "application/json,.json",
|
|
2432
|
+
className: "dsm-workbuddy-transfer-file",
|
|
2433
|
+
onChange: (event) => {
|
|
2434
|
+
const file = event.target.files?.[0];
|
|
2435
|
+
event.target.value = "";
|
|
2436
|
+
if (file !== void 0) {
|
|
2437
|
+
setPreview(void 0);
|
|
2438
|
+
setImportSelected(/* @__PURE__ */ new Set());
|
|
2439
|
+
setDialog("import");
|
|
2440
|
+
readImportFile(file);
|
|
2441
|
+
}
|
|
2442
|
+
}
|
|
2443
|
+
}),
|
|
2195
2444
|
dialog === "export" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2196
2445
|
className: "dsm-workbuddy-transfer-dialog",
|
|
2197
2446
|
role: "dialog",
|
|
@@ -2238,62 +2487,49 @@ window.__ModuleLoader__.load({
|
|
|
2238
2487
|
})
|
|
2239
2488
|
]
|
|
2240
2489
|
}) : null,
|
|
2241
|
-
dialog === "import" ? /* @__PURE__ */ (0, react_jsx_runtime.
|
|
2490
|
+
dialog === "import" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2242
2491
|
className: "dsm-workbuddy-transfer-dialog",
|
|
2243
2492
|
role: "dialog",
|
|
2244
2493
|
"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
|
-
|
|
2494
|
+
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: [
|
|
2495
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t("row.transferPreviewTotal", { total: preview.total }) }),
|
|
2496
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
|
|
2497
|
+
className: "dsm-workbuddy-transfer-list",
|
|
2498
|
+
children: preview.accounts.map((entry) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", { children: [
|
|
2499
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
2500
|
+
type: "checkbox",
|
|
2501
|
+
disabled: !entry.hasToken,
|
|
2502
|
+
checked: importSelected.has(entry.index),
|
|
2503
|
+
onChange: (event) => {
|
|
2504
|
+
const next = new Set(importSelected);
|
|
2505
|
+
if (event.target.checked) next.add(entry.index);
|
|
2506
|
+
else next.delete(entry.index);
|
|
2507
|
+
setImportSelected(next);
|
|
2508
|
+
}
|
|
2509
|
+
}),
|
|
2510
|
+
" ",
|
|
2511
|
+
entry.nickname !== "" ? entry.nickname : entry.email !== "" ? entry.email : entry.uid !== "" ? entry.uid : t("row.transferEntryUnknown"),
|
|
2512
|
+
entry.hasToken ? "" : ` — ${t("row.transferNoToken")}`
|
|
2513
|
+
] }) }, entry.index))
|
|
2256
2514
|
}),
|
|
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
|
-
]
|
|
2515
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2516
|
+
className: "dsm-workbuddy-transfer-dialog-actions",
|
|
2517
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2518
|
+
type: "button",
|
|
2519
|
+
className: "dsm-btn dsm-btn-outline",
|
|
2520
|
+
onClick: closeDialog,
|
|
2521
|
+
children: t("row.transferCancel")
|
|
2522
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2523
|
+
type: "button",
|
|
2524
|
+
className: "dsm-btn dsm-btn-outline",
|
|
2525
|
+
disabled: importSelected.size === 0 || phase.kind === "importing",
|
|
2526
|
+
onClick: () => {
|
|
2527
|
+
runImport();
|
|
2528
|
+
},
|
|
2529
|
+
children: phase.kind === "importing" ? t("row.transferImporting") : t("row.transferImportConfirm", { count: importSelected.size })
|
|
2530
|
+
})]
|
|
2531
|
+
})
|
|
2532
|
+
] })
|
|
2297
2533
|
}) : null
|
|
2298
2534
|
]
|
|
2299
2535
|
});
|
|
@@ -2645,6 +2881,35 @@ window.__ModuleLoader__.load({
|
|
|
2645
2881
|
.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
2882
|
.dsm-workbuddy-transfer-list input[type="checkbox"]{flex:none}
|
|
2647
2883
|
.dsm-workbuddy-transfer input[type="file"]{font-size:12px;color:var(--dsw-alias-label-primary,#e8e9ed);margin-bottom:8px}
|
|
2884
|
+
/* The import picker's input is driven by the 「import」 button, so it must not
|
|
2885
|
+
render as a visible field: hide it from layout AND from assistive tech (the
|
|
2886
|
+
button carries the accessible name). Not display:none alone — that also
|
|
2887
|
+
removes it from the tab order in some engines, which is fine here, but the
|
|
2888
|
+
clip-based form is the one that reliably keeps programmatic .click() working
|
|
2889
|
+
across browsers. */
|
|
2890
|
+
.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}
|
|
2891
|
+
/* The sign-in URL inside the OAuth dialog: long https links must wrap rather
|
|
2892
|
+
than push the dialog wide — break-all, since URLs have no soft wrap points. */
|
|
2893
|
+
.dsm-workbuddy-oauth-url{word-break:break-all;overflow-wrap:anywhere}
|
|
2894
|
+
.dsm-workbuddy-oauth-url a{color:var(--dsw-alias-brand-primary,#5686fe);text-decoration:none}
|
|
2895
|
+
.dsm-workbuddy-oauth-url a:hover{text-decoration:underline}
|
|
2896
|
+
/* The shared busy veil: a fixed overlay for operations whose result only shows
|
|
2897
|
+
up after the list re-read (remove / import / sign-in).
|
|
2898
|
+
It is NOT a modal: no control, no dismissal — only "this is running" and
|
|
2899
|
+
"here is why nothing moved yet". Fixed positioning so it covers the card
|
|
2900
|
+
regardless of which section started the operation, and a semi-transparent
|
|
2901
|
+
backdrop so the table stays visible underneath — the user is watching a
|
|
2902
|
+
specific row change, and hiding it would defeat the point. */
|
|
2903
|
+
.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}
|
|
2904
|
+
.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)}
|
|
2905
|
+
.dsm-workbuddy-veil-text{font-size:13px;line-height:20px;color:var(--dsw-alias-label-primary,#e8e9ed);text-align:center}
|
|
2906
|
+
.dsm-workbuddy-veil-hint{font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary,#999);text-align:center}
|
|
2907
|
+
/* The spinner is the one thing that proves the UI is alive when the row under
|
|
2908
|
+
it has not changed yet — which is precisely the confusion the veil exists to
|
|
2909
|
+
remove. Reduced-motion users get the static ring instead of a rotation. */
|
|
2910
|
+
.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}
|
|
2911
|
+
@keyframes dsm-workbuddy-veil-spin{to{transform:rotate(360deg)}}
|
|
2912
|
+
@media (prefers-reduced-motion: reduce){.dsm-workbuddy-veil-spinner{animation:none}}
|
|
2648
2913
|
`;
|
|
2649
2914
|
/**
|
|
2650
2915
|
* Composer credit readout styles, injected by the readout component itself.
|
|
@@ -3274,8 +3539,18 @@ window.__ModuleLoader__.load({
|
|
|
3274
3539
|
accounts: [],
|
|
3275
3540
|
selectionExplicit: false
|
|
3276
3541
|
};
|
|
3542
|
+
/**
|
|
3543
|
+
* Whether the ACTIVE region knows of any account at all.
|
|
3544
|
+
*
|
|
3545
|
+
* This is the poll's precondition in place of `status === 'signed-in'`. The
|
|
3546
|
+
* signed-in flag means "a credential resolves", which is false for a region
|
|
3547
|
+
* whose accounts exist but none is usable yet — exactly the state an import
|
|
3548
|
+
* or a fresh OAuth sign-in creates. Polling on it meant those regions never
|
|
3549
|
+
* refreshed, so the list stayed empty until the card was reopened.
|
|
3550
|
+
*/
|
|
3551
|
+
const hasAccounts = status.status !== "error" && status.accounts.length > 0;
|
|
3277
3552
|
(0, react.useEffect)(() => {
|
|
3278
|
-
if (!open ||
|
|
3553
|
+
if (!open || !hasAccounts) return;
|
|
3279
3554
|
const controller = new AbortController();
|
|
3280
3555
|
const timer = window.setInterval(() => {
|
|
3281
3556
|
refreshUsage(activeRegion, controller.signal);
|
|
@@ -3305,6 +3580,7 @@ window.__ModuleLoader__.load({
|
|
|
3305
3580
|
*/
|
|
3306
3581
|
const rescanAccounts = async () => {
|
|
3307
3582
|
setBusy(true);
|
|
3583
|
+
setAccountError(void 0);
|
|
3308
3584
|
try {
|
|
3309
3585
|
const response = await fetch(withWorkBuddyRegion(WORKBUDDY_ACCOUNTS_REFRESH_PATH, activeRegion), {
|
|
3310
3586
|
method: "POST",
|
|
@@ -3313,7 +3589,10 @@ window.__ModuleLoader__.load({
|
|
|
3313
3589
|
});
|
|
3314
3590
|
const body = await response.json();
|
|
3315
3591
|
if (!response.ok || !Array.isArray(body.accounts)) throw new Error(`HTTP ${response.status}`);
|
|
3592
|
+
if (body.dismissalsCleared === false) setAccountError(t("row.rescanHidesKept"));
|
|
3316
3593
|
await refreshUsage(activeRegion);
|
|
3594
|
+
} catch (error) {
|
|
3595
|
+
if (mounted.current) setAccountError(error instanceof Error ? error.message : t("row.requestFailed"));
|
|
3317
3596
|
} finally {
|
|
3318
3597
|
if (mounted.current) setBusy(false);
|
|
3319
3598
|
}
|
|
@@ -3672,10 +3951,12 @@ window.__ModuleLoader__.load({
|
|
|
3672
3951
|
*/
|
|
3673
3952
|
const accountTools = /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(OAuthSignIn, {
|
|
3674
3953
|
t,
|
|
3675
|
-
region: activeRegion
|
|
3954
|
+
region: activeRegion,
|
|
3955
|
+
onAdded: () => refreshUsage(activeRegion)
|
|
3676
3956
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AccountTransfer, {
|
|
3677
3957
|
t,
|
|
3678
3958
|
region: activeRegion,
|
|
3959
|
+
onChanged: () => refreshUsage(activeRegion),
|
|
3679
3960
|
accounts: status.status === "error" ? [] : status.accounts.map((account) => ({
|
|
3680
3961
|
id: account.id,
|
|
3681
3962
|
accountName: account.accountName
|
|
@@ -3781,6 +4062,11 @@ window.__ModuleLoader__.load({
|
|
|
3781
4062
|
className: "dsm-workbuddy-usage-hint",
|
|
3782
4063
|
children: t("row.reloginHint")
|
|
3783
4064
|
}) : null,
|
|
4065
|
+
accountError === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
4066
|
+
className: "dsm-workbuddy-account-error",
|
|
4067
|
+
role: "alert",
|
|
4068
|
+
children: accountError
|
|
4069
|
+
}),
|
|
3784
4070
|
selectionLost ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
3785
4071
|
className: "dsm-workbuddy-usage-hint",
|
|
3786
4072
|
children: t("row.selectionLostHint")
|
|
@@ -3831,9 +4117,7 @@ window.__ModuleLoader__.load({
|
|
|
3831
4117
|
siblingBusy: saving || togglingRegion !== void 0,
|
|
3832
4118
|
onBusyChange: setPoolBusy,
|
|
3833
4119
|
onSaved: async () => await refreshUsage(activeRegion) !== void 0,
|
|
3834
|
-
onRefresh: () =>
|
|
3835
|
-
refreshUsage(activeRegion);
|
|
3836
|
-
}
|
|
4120
|
+
onRefresh: () => refreshUsage(activeRegion)
|
|
3837
4121
|
}),
|
|
3838
4122
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("section", {
|
|
3839
4123
|
className: "dsm-workbuddy-models",
|
|
@@ -4681,6 +4965,7 @@ window.__ModuleLoader__.load({
|
|
|
4681
4965
|
"row.sourceDsh": "plugin copy",
|
|
4682
4966
|
"row.searchedMore": "Show the {count} path(s) that were not found",
|
|
4683
4967
|
"row.reloginHint": "If errors occur, sign back in to the WorkBuddy app.",
|
|
4968
|
+
"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
4969
|
"row.credentialRejectedTitle": "WorkBuddy refused this account's token",
|
|
4685
4970
|
"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
4971
|
"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 +5036,6 @@ window.__ModuleLoader__.load({
|
|
|
4751
5036
|
"row.oauthStart": "Sign in with QR code",
|
|
4752
5037
|
"row.oauthStarting": "Opening…",
|
|
4753
5038
|
"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
5039
|
"row.oauthOpened": "Opened. Waiting for you to finish…",
|
|
4756
5040
|
"row.oauthWaiting": "Waiting for the sign-in to finish… ({seconds}s)",
|
|
4757
5041
|
"row.oauthExpires": "The sign-in window expires in about {minutes} min.",
|
|
@@ -4760,7 +5044,14 @@ window.__ModuleLoader__.load({
|
|
|
4760
5044
|
"row.oauthTimedOut": "The sign-in timed out — start again when ready.",
|
|
4761
5045
|
"row.oauthDone": "Added “{accountName}”. Pick it above to use it.",
|
|
4762
5046
|
"row.oauthError": "Sign-in failed: {message}",
|
|
4763
|
-
"row.
|
|
5047
|
+
"row.oauthCopy": "Copy link",
|
|
5048
|
+
"row.oauthCopied": "Link copied",
|
|
5049
|
+
"row.oauthReopen": "Open page again",
|
|
5050
|
+
"row.oauthDialogClose": "Close",
|
|
5051
|
+
"row.oauthTitleGlobal": "Add an account by web sign-in",
|
|
5052
|
+
"row.oauthStartGlobal": "Sign in on the web",
|
|
5053
|
+
"row.veilHintList": "The list refreshes as soon as this finishes.",
|
|
5054
|
+
"row.oauthSyncing": "Adding the account…",
|
|
4764
5055
|
"row.transferTitle": "Export / import accounts",
|
|
4765
5056
|
"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
5057
|
"row.transferExport": "Export accounts",
|
|
@@ -4770,7 +5061,7 @@ window.__ModuleLoader__.load({
|
|
|
4770
5061
|
"row.transferExported": "Exported {count} account(s) to {fileName}.",
|
|
4771
5062
|
"row.transferImport": "Import accounts",
|
|
4772
5063
|
"row.transferImporting": "Importing…",
|
|
4773
|
-
"row.
|
|
5064
|
+
"row.transferPreviewing": "Reading the file…",
|
|
4774
5065
|
"row.transferPreviewTotal": "{total} account(s) in the file — untick what should stay out. Accounts already stored here are overwritten by the import.",
|
|
4775
5066
|
"row.transferImportConfirm": "Import {count} account(s)",
|
|
4776
5067
|
"row.transferImported": "Imported {imported} account(s); {skipped} skipped (no token, or belongs to the other region).",
|
|
@@ -4878,10 +5169,11 @@ window.__ModuleLoader__.load({
|
|
|
4878
5169
|
"row.poolUnsavedMembers": "Selection changed — save to apply",
|
|
4879
5170
|
"row.poolRowTest": "Test",
|
|
4880
5171
|
"row.poolRowRemove": "Remove",
|
|
4881
|
-
"row.poolRowRemoveHint": "
|
|
5172
|
+
"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
5173
|
"row.poolRowRemoveAria": "Remove the stored credential of {accountName}",
|
|
4883
|
-
"row.poolRowRemoved": "{accountName}:
|
|
4884
|
-
"row.
|
|
5174
|
+
"row.poolRowRemoved": "{accountName}: hidden from the list. 「Detect accounts again」 re-adds it; the desktop app stays signed in.",
|
|
5175
|
+
"row.poolRowRemoving": "Removing the account…",
|
|
5176
|
+
"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
5177
|
"row.showCreditsCn": "Show WB CN credits in the composer",
|
|
4886
5178
|
"row.showCreditsGlobal": "Show WB AI credits in the composer",
|
|
4887
5179
|
"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 +5225,7 @@ window.__ModuleLoader__.load({
|
|
|
4933
5225
|
"row.sourceDsh": "插件副本",
|
|
4934
5226
|
"row.searchedMore": "显示另外 {count} 条「不存在」的路径",
|
|
4935
5227
|
"row.reloginHint": "出现错误,重新登录 WorkBuddy APP 即可。",
|
|
5228
|
+
"row.rescanHidesKept": "已删除的账号仍是隐藏状态:插件没能更新自己的名单文件。Windows 上通常是文件被占用——请关闭可能正在占用 DSH 配置目录的杀毒/同步工具后重试。",
|
|
4936
5229
|
"row.credentialRejectedTitle": "WorkBuddy 拒绝了这个账号的 token",
|
|
4937
5230
|
"row.credentialRejectedIntro": "「{accountName}」的 token 已不被上游接受,所以积分与签到都用不了。重新登录只在「确实是 token 的问题」时才有用——当前实际适用的是下面这一条:",
|
|
4938
5231
|
"row.credentialRejectedSwitch": "本机另一个账号「{accountName}」刚刚实测可用,切换到它即可恢复,不必重新登录。",
|
|
@@ -5003,7 +5296,6 @@ window.__ModuleLoader__.load({
|
|
|
5003
5296
|
"row.oauthStart": "扫码登录",
|
|
5004
5297
|
"row.oauthStarting": "正在打开…",
|
|
5005
5298
|
"row.oauthOpenHint": "打开登录页并完成确认——完成后账号会自动出现在这里:",
|
|
5006
|
-
"row.oauthOpen": "打开登录页",
|
|
5007
5299
|
"row.oauthOpened": "已打开,等待你完成登录…",
|
|
5008
5300
|
"row.oauthWaiting": "正在等待登录完成…({seconds} 秒)",
|
|
5009
5301
|
"row.oauthExpires": "登录窗口约 {minutes} 分钟后过期。",
|
|
@@ -5012,7 +5304,14 @@ window.__ModuleLoader__.load({
|
|
|
5012
5304
|
"row.oauthTimedOut": "登录超时——请重新开始。",
|
|
5013
5305
|
"row.oauthDone": "已添加「{accountName}」。在上方选中它即可使用。",
|
|
5014
5306
|
"row.oauthError": "登录失败:{message}",
|
|
5015
|
-
"row.
|
|
5307
|
+
"row.oauthCopy": "复制链接",
|
|
5308
|
+
"row.oauthCopied": "已复制链接",
|
|
5309
|
+
"row.oauthReopen": "重新打开页面",
|
|
5310
|
+
"row.oauthDialogClose": "关闭",
|
|
5311
|
+
"row.oauthTitleGlobal": "网页登录添加账号",
|
|
5312
|
+
"row.oauthStartGlobal": "网页登录",
|
|
5313
|
+
"row.veilHintList": "完成后列表会立即刷新。",
|
|
5314
|
+
"row.oauthSyncing": "正在添加账号…",
|
|
5016
5315
|
"row.transferTitle": "导出 / 导入账号",
|
|
5017
5316
|
"row.transferHint": "用 JSON 文件在电脑之间迁移账号。文件里是账号凭据——请妥善保管。该格式与同类 WorkBuddy 凭据管理工具通用。",
|
|
5018
5317
|
"row.transferExport": "导出账号",
|
|
@@ -5022,7 +5321,7 @@ window.__ModuleLoader__.load({
|
|
|
5022
5321
|
"row.transferExported": "已导出 {count} 个账号到 {fileName}。",
|
|
5023
5322
|
"row.transferImport": "导入账号",
|
|
5024
5323
|
"row.transferImporting": "正在导入…",
|
|
5025
|
-
"row.
|
|
5324
|
+
"row.transferPreviewing": "正在读取文件…",
|
|
5026
5325
|
"row.transferPreviewTotal": "文件中共 {total} 个账号——不需要的请取消勾选。导入会覆盖这里已存储的同名账号。",
|
|
5027
5326
|
"row.transferImportConfirm": "导入 {count} 个账号",
|
|
5028
5327
|
"row.transferImported": "已导入 {imported} 个账号;跳过 {skipped} 个(缺 token,或属于另一个区域)。",
|
|
@@ -5130,10 +5429,11 @@ window.__ModuleLoader__.load({
|
|
|
5130
5429
|
"row.poolUnsavedMembers": "勾选已改动 —— 保存后生效",
|
|
5131
5430
|
"row.poolRowTest": "测试",
|
|
5132
5431
|
"row.poolRowRemove": "删除",
|
|
5133
|
-
"row.poolRowRemoveHint": "
|
|
5432
|
+
"row.poolRowRemoveHint": "把这个账号从列表中移除。桌面 App 不会被退出登录——它保留自己的登录状态。移除只是暂时隐藏,点「重新检测账号」即可找回。",
|
|
5134
5433
|
"row.poolRowRemoveAria": "删除 {accountName} 的已存凭据",
|
|
5135
|
-
"row.poolRowRemoved": "{accountName}
|
|
5136
|
-
"row.
|
|
5434
|
+
"row.poolRowRemoved": "{accountName}:已从列表中隐藏。点「重新检测账号」会重新加入,桌面 App 的登录不受影响。",
|
|
5435
|
+
"row.poolRowRemoving": "正在删除账号…",
|
|
5436
|
+
"row.poolRowRemoveDesktopOnly": "{accountName}:已从列表中隐藏——这个账号来自桌面 App 自己的登录,该登录未被改动。点「重新检测账号」会重新加入。",
|
|
5137
5437
|
"row.showCreditsCn": "在对话框显示 WB CN 积分",
|
|
5138
5438
|
"row.showCreditsGlobal": "在对话框显示 WB AI 积分",
|
|
5139
5439
|
"row.showCreditsCnHint": "在对话输入框工具栏显示国内版(WB CN)剩余积分,仅当本会话使用 WB CN 模型时出现。点击可看各账号余额并手动刷新(每 5 分钟也会自动刷新)。",
|