dsh-config-manager 0.1.82 → 0.1.83
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/lib/client.d.ts +1 -1
- package/lib/client.js +749 -643
- package/lib/index.d.ts +1 -1
- package/lib/index.js +1 -1
- package/package.json +1 -1
- package/src/client/ConfigManagerSection.tsx +11 -0
- package/src/client/common/MoreMenu.tsx +17 -2
- package/src/client/config-manager.module.css +9 -2
- package/src/client/environment/EnvironmentPanel.tsx +9 -6
- package/src/client/export/ExportView.tsx +6 -5
- package/src/client/history/HistoryPanel.tsx +32 -5
- package/src/client/history/history-locales.ts +3 -0
- package/src/client/home/BackupScheduleCard.tsx +6 -5
- package/src/client/home/HomePanel.tsx +3 -3
- package/src/client/import/ForeignImportView.tsx +3 -3
- package/src/client/import/import-wizard-steps.tsx +6 -5
- package/src/client/library/ArtifactRow.tsx +39 -7
- package/src/client/library/LibraryActions.tsx +78 -49
- package/src/client/library/LibraryPanel.tsx +11 -2
- package/src/client/market/MarketImportReview.tsx +4 -3
- package/src/client/market/MarketPanel.tsx +7 -7
- package/src/client/market/MyConfigsList.tsx +3 -3
- package/src/client/market/MyConfigsView.tsx +1 -1
- package/src/client/market/MyConfigsWizard.tsx +27 -10
- package/src/client/sync/ChannelConfigDialog.tsx +4 -3
- package/src/client/sync/SyncConfirmView.tsx +3 -3
- package/src/client/sync/SyncPanel.tsx +6 -7
- package/src/client/sync/SyncProbeRow.tsx +3 -3
- package/src/client/sync/SyncRepositoryPicker.tsx +5 -5
- package/src/index.ts +1 -1
|
@@ -30,6 +30,7 @@ import { ConfirmDialog } from '../common/ConfirmDialog.tsx'
|
|
|
30
30
|
import { Modal } from '../common/Modal.tsx'
|
|
31
31
|
import { toast } from '../common/toast-store.ts'
|
|
32
32
|
import { ConsultCard } from '../consult/ConsultCard.tsx'
|
|
33
|
+
import type { ArtifactPendingAction } from './ArtifactRow.tsx'
|
|
33
34
|
import { RestorePlanView } from './RestorePlanView.tsx'
|
|
34
35
|
import { BackupInspectView } from './BackupInspectView.tsx'
|
|
35
36
|
import { redact } from '../../security/redaction.ts'
|
|
@@ -76,6 +77,14 @@ export interface LibraryActionsProps {
|
|
|
76
77
|
* 经壳层转成 Task 入参;其余入口不带(= 打开市场列表)。
|
|
77
78
|
*/
|
|
78
79
|
onOpenTask: (kind: 'import' | 'market', marketId?: string) => void
|
|
80
|
+
/**
|
|
81
|
+
* 把「哪一行的哪个动作正在跑」上报给壳层(DESIGN §8.11「行内动作 pending」)。
|
|
82
|
+
*
|
|
83
|
+
* 为什么必须上报:本组件是产物库面板的**兄弟节点**(见文件头),行的读点在面板里。
|
|
84
|
+
* 此前这里是 `const [busy, setBusy] = useState(false)` + `void busy` —— 写了一个
|
|
85
|
+
* **读不到**的状态:行内按钮既不转圈也不禁用,同一个动作可以连点。
|
|
86
|
+
*/
|
|
87
|
+
onPendingChange: (pending: ArtifactPendingAction | null) => void
|
|
79
88
|
}
|
|
80
89
|
|
|
81
90
|
/** 恢复计划弹窗的本地态(dry-run 结果 + 咨询报告) */
|
|
@@ -100,7 +109,7 @@ interface PullState {
|
|
|
100
109
|
error: string | null
|
|
101
110
|
}
|
|
102
111
|
|
|
103
|
-
export function LibraryActions({ api, syncApi, t, target, onDone, onChanged, onOpenTask }: LibraryActionsProps) {
|
|
112
|
+
export function LibraryActions({ api, syncApi, t, target, onDone, onChanged, onOpenTask, onPendingChange }: LibraryActionsProps) {
|
|
104
113
|
const mounted = useRef(true)
|
|
105
114
|
useEffect(() => () => { mounted.current = false }, [])
|
|
106
115
|
|
|
@@ -122,10 +131,14 @@ export function LibraryActions({ api, syncApi, t, target, onDone, onChanged, onO
|
|
|
122
131
|
const [confirmRestore, setConfirmRestore] = useState(false)
|
|
123
132
|
const [confirmDelete, setConfirmDelete] = useState<ArtifactRow | null>(null)
|
|
124
133
|
const [inspect, setInspect] = useState<{ name: string; loading: boolean; error: string | null; result: BackupInspectResult | null } | null>(null)
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
134
|
+
/**
|
|
135
|
+
* 行内 pending 的统一出口:本组件不再自存忙态(自存就是上一个 bug 的成因),
|
|
136
|
+
* 一律写进壳层,由壳层传回面板(`ArtifactRowView.pending`)。
|
|
137
|
+
* 传 null = 这一行的动作收尾(成功或失败都要清,否则行会永远转圈)。
|
|
138
|
+
*/
|
|
139
|
+
const reportPending = useCallback((key: string, capability: ArtifactCapability | null): void => {
|
|
140
|
+
onPendingChange(capability === null ? null : { key, capability })
|
|
141
|
+
}, [onPendingChange])
|
|
129
142
|
|
|
130
143
|
/** 每次触发动作递增:快速连点时作废在途的 dry-run 响应(防晚到结果覆盖新选择)。 */
|
|
131
144
|
const generation = useRef(0)
|
|
@@ -137,6 +150,8 @@ export function LibraryActions({ api, syncApi, t, target, onDone, onChanged, onO
|
|
|
137
150
|
switch (capability) {
|
|
138
151
|
case 'restore': {
|
|
139
152
|
setPlan({ snapshotId: row.ref.snapshotId ?? '', loading: true, plan: null, changeSummary: undefined, planFingerprint: undefined, error: null })
|
|
153
|
+
// dry-run 是网络往返:弹窗里已有骨架,行上也标出来(两份反馈不冲突,各自回答一个问题)
|
|
154
|
+
reportPending(row.key, 'restore')
|
|
140
155
|
// v3(用户要求):恢复计划弹窗**不再内嵌「迁移前咨询」卡** —— 那个弹窗的职责是
|
|
141
156
|
// 「这次恢复会改什么 + 确认执行」;咨询报告有自己的入口(产物库行 ⋯ 菜单 →「迁移前咨询」),
|
|
142
157
|
// 想看报告的人可以单独打开。顺带省掉一次重量级请求(consult 要读快照内容做评分)。
|
|
@@ -144,10 +159,12 @@ export function LibraryActions({ api, syncApi, t, target, onDone, onChanged, onO
|
|
|
144
159
|
(res) => {
|
|
145
160
|
if (!mounted.current || gen !== generation.current) return
|
|
146
161
|
setPlan({ snapshotId: row.ref.snapshotId ?? '', loading: false, plan: res.plan ?? null, changeSummary: res.changeSummary, planFingerprint: res.planFingerprint, error: null })
|
|
162
|
+
reportPending(row.key, null)
|
|
147
163
|
},
|
|
148
164
|
(err) => {
|
|
149
165
|
if (!mounted.current || gen !== generation.current) return
|
|
150
166
|
setPlan({ snapshotId: row.ref.snapshotId ?? '', loading: false, plan: null, changeSummary: undefined, planFingerprint: undefined, error: err instanceof Error ? err.message : String(err) })
|
|
167
|
+
reportPending(row.key, null)
|
|
151
168
|
},
|
|
152
169
|
)
|
|
153
170
|
return
|
|
@@ -249,13 +266,15 @@ export function LibraryActions({ api, syncApi, t, target, onDone, onChanged, onO
|
|
|
249
266
|
)
|
|
250
267
|
return
|
|
251
268
|
}
|
|
252
|
-
|
|
269
|
+
// 本机备份的下载**没有弹窗**(Blob + <a download>,立即),所以反馈只能落在行上:
|
|
270
|
+
// 点击那颗按钮立刻转圈,直到浏览器收下这份文件(此前这里 setBusy 后无人读 ⇒ 零反馈)。
|
|
271
|
+
reportPending(row.key, 'download')
|
|
253
272
|
// 用户要求:直接走浏览器下载(落到「下载」目录),不弹系统保存对话框 ——
|
|
254
273
|
// 备份文件是**可再生成的产物**,多一步选路径没有收益。
|
|
255
274
|
// api.download 缺省即此语义(Blob + <a download>);不要传 saveDialog: true。
|
|
256
275
|
void api.download(row.ref.path ?? '')
|
|
257
276
|
.catch((err) => { toast.error(redactErrorText(err)) })
|
|
258
|
-
.finally(() => { if (mounted.current) {
|
|
277
|
+
.finally(() => { if (mounted.current) { reportPending(row.key, null); onDone() } })
|
|
259
278
|
return
|
|
260
279
|
}
|
|
261
280
|
case 'delete': {
|
|
@@ -263,29 +282,35 @@ export function LibraryActions({ api, syncApi, t, target, onDone, onChanged, onO
|
|
|
263
282
|
return
|
|
264
283
|
}
|
|
265
284
|
case 'consult': {
|
|
266
|
-
|
|
267
|
-
//
|
|
268
|
-
//
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
//
|
|
272
|
-
// 「恢复计划预览」弹窗内部(由 plan !== null 控制)⇒ 点 ⋯ 完全没反应。
|
|
285
|
+
// 四种可迁移源共用一个入口(§6.8 ②):咨询永远进 ⋯,不按来源造分叉。
|
|
286
|
+
//
|
|
287
|
+
// **先开弹窗、再去取参数**(顺序不能反):consultInput 对远端快照要走 /sync/status
|
|
288
|
+
// 拿通道地址(网络),旧顺序是「先 await 再开弹窗」⇒ 那一段页面完全没反应;
|
|
289
|
+
// 而且它会**抛错**(无通道地址时 'sync channel address is unavailable'),
|
|
290
|
+
// 先 await 的写法会让异常直接逃出 dispatch,弹窗永远不出现。
|
|
273
291
|
setConsult({ row, loading: true, error: null, report: null })
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
292
|
+
reportPending(row.key, 'consult')
|
|
293
|
+
void (async () => {
|
|
294
|
+
try {
|
|
295
|
+
const input = await consultInput(row, syncApi)
|
|
296
|
+
if (!mounted.current) return
|
|
297
|
+
const rep = await api.consult(input)
|
|
298
|
+
if (mounted.current) setConsult({ row, loading: false, error: null, report: rep })
|
|
299
|
+
} catch (err) {
|
|
277
300
|
const message = redactErrorText(err)
|
|
278
301
|
if (mounted.current) setConsult({ row, loading: false, error: message, report: null })
|
|
279
302
|
// Toast 与弹窗内错误都给:弹窗可能被立刻关掉,Toast 保证反馈不丢
|
|
280
303
|
toast.error(message)
|
|
281
|
-
}
|
|
282
|
-
|
|
304
|
+
} finally {
|
|
305
|
+
if (mounted.current) { reportPending(row.key, null); onDone() }
|
|
306
|
+
}
|
|
307
|
+
})()
|
|
283
308
|
return
|
|
284
309
|
}
|
|
285
310
|
default:
|
|
286
311
|
onDone()
|
|
287
312
|
}
|
|
288
|
-
}, [api, t, onDone, onChanged, onOpenTask])
|
|
313
|
+
}, [api, t, onDone, onChanged, onOpenTask, reportPending])
|
|
289
314
|
|
|
290
315
|
/**
|
|
291
316
|
* 供 LibraryPanel 调用:把点击转进来。用 ref 暴露,避免面板每次渲染重建回调。
|
|
@@ -325,38 +350,42 @@ export function LibraryActions({ api, syncApi, t, target, onDone, onChanged, onO
|
|
|
325
350
|
).finally(() => { runStoreStopWatch() })
|
|
326
351
|
}
|
|
327
352
|
|
|
328
|
-
|
|
353
|
+
/**
|
|
354
|
+
* 执行删除。**返回 Promise 是有意的**(DESIGN UI-25 ①档):ConfirmDialog 只在 onConfirm
|
|
355
|
+
* 返回 Promise 时自管 busy —— 这样弹窗在删除期间转圈、确认键与取消/遮罩/Esc 全禁用,
|
|
356
|
+
* 用户不会在还在删的时候点第二下(此前的 void 版本:弹窗一动不动,可重复提交)。
|
|
357
|
+
* 远端行还多一步 remotePayload(要通道地址),更要挡住重复点击。
|
|
358
|
+
* 失败时**关窗 + toast**:错误文案在弹窗里既没人读,也会被下一次打开冲掉。
|
|
359
|
+
*/
|
|
360
|
+
const doDelete = async (): Promise<void> => {
|
|
329
361
|
const row = confirmDelete
|
|
330
362
|
if (row === null) return
|
|
331
363
|
const done = (): void => { if (mounted.current) { setConfirmDelete(null); onChanged(); onDone() } }
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
(
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
toast.error(redactErrorText(err))
|
|
358
|
-
}
|
|
359
|
-
})()
|
|
364
|
+
try {
|
|
365
|
+
if (row.kind === 'snapshot') {
|
|
366
|
+
await api.deleteSnapshot(row.ref.snapshotId ?? '')
|
|
367
|
+
toast.ok(t('snapshots.deleted'))
|
|
368
|
+
done()
|
|
369
|
+
return
|
|
370
|
+
}
|
|
371
|
+
if (row.kind === 'backup-file') {
|
|
372
|
+
await api.deleteBackupFile(fileNameOf(row))
|
|
373
|
+
toast.ok(t('backupFiles.deleted', { name: fileNameOf(row) }))
|
|
374
|
+
done()
|
|
375
|
+
return
|
|
376
|
+
}
|
|
377
|
+
if (row.kind === 'remote-snapshot') {
|
|
378
|
+
// 删的是**远端那一份**(只动远端,见宿主 SyncEngine.deleteSnapshot):需要通道 + 地址,
|
|
379
|
+
// 与拉取/咨询同源取(prepareSync 只认 repoUrl/url)。
|
|
380
|
+
const payload = await remotePayload(syncApi)
|
|
381
|
+
await syncApi.deleteSnapshot({ ...payload, snapshotId: row.ref.remoteId ?? '' })
|
|
382
|
+
toast.ok(t('library.remoteDeleted'))
|
|
383
|
+
done()
|
|
384
|
+
}
|
|
385
|
+
} catch (err) {
|
|
386
|
+
if (mounted.current) setConfirmDelete(null)
|
|
387
|
+
// 快照删除有两条可映射的宿主码(被事故恢复引用 / 清单读不出),其余走脱敏原文
|
|
388
|
+
toast.error(row.kind === 'snapshot' ? snapshotDeleteErrorText(err, t) : redactErrorText(err))
|
|
360
389
|
}
|
|
361
390
|
}
|
|
362
391
|
|
|
@@ -27,7 +27,7 @@ import { Badge, Button, Card, Empty } from '../common/ui.tsx'
|
|
|
27
27
|
import { SkeletonRows } from '../common/Skeleton.tsx'
|
|
28
28
|
import { redact } from '../../security/redaction.ts'
|
|
29
29
|
import { InfoHint } from '../common/InfoHint.tsx'
|
|
30
|
-
import { ArtifactRowView } from './ArtifactRow.tsx'
|
|
30
|
+
import { ArtifactRowView, type ArtifactPendingAction } from './ArtifactRow.tsx'
|
|
31
31
|
import css from '../config-manager.module.css'
|
|
32
32
|
|
|
33
33
|
export interface LibraryPanelProps {
|
|
@@ -51,6 +51,13 @@ export interface LibraryPanelProps {
|
|
|
51
51
|
onOpenPublish: () => void
|
|
52
52
|
/** 外部刷新信号(动作完成 / 恢复 / 删除后递增)→ 重拉四份清单 */
|
|
53
53
|
refreshTick: number
|
|
54
|
+
/**
|
|
55
|
+
* 行内动作的进行中态(DESIGN §8.11「行内动作 pending」)。
|
|
56
|
+
* 由壳层持有:动作实现在 **LibraryActions**(本面板的兄弟节点,见其文件头),
|
|
57
|
+
* 它跑起来时把「哪一行的哪个动作」上报给壳层,壳层再以本 prop 传回 →
|
|
58
|
+
* 行内按钮就地转圈/禁用。放壳层还顺带解决了「切页把面板重挂后忙态蒸发」的问题。
|
|
59
|
+
*/
|
|
60
|
+
pending: ArtifactPendingAction | null
|
|
54
61
|
}
|
|
55
62
|
|
|
56
63
|
type SourceStatus = 'loading' | 'ready' | 'error'
|
|
@@ -63,7 +70,7 @@ interface SourceState<T> {
|
|
|
63
70
|
|
|
64
71
|
function initialSource<T>(): SourceState<T> { return { status: 'loading', items: [], error: null } }
|
|
65
72
|
|
|
66
|
-
export function LibraryPanel({ api, syncApi, marketApi, t, uiT, onAction, onOpenTask, onOpenExport, onOpenPublish, refreshTick }: LibraryPanelProps) {
|
|
73
|
+
export function LibraryPanel({ api, syncApi, marketApi, t, uiT, onAction, onOpenTask, onOpenExport, onOpenPublish, refreshTick, pending }: LibraryPanelProps) {
|
|
67
74
|
const state = useSyncExternalStore(runStore.subscribe, runStore.getSnapshot)
|
|
68
75
|
const lib = state.library
|
|
69
76
|
|
|
@@ -263,6 +270,8 @@ export function LibraryPanel({ api, syncApi, marketApi, t, uiT, onAction, onOpen
|
|
|
263
270
|
runStore.patch({ library: { expandedKey: lib.expandedKey === row.key ? null : row.key } })
|
|
264
271
|
}}
|
|
265
272
|
onAction={onAction}
|
|
273
|
+
// 只把**本行**的进行中动作交给行组件:其余行保持可点(一次只挡同一行)
|
|
274
|
+
pending={pending !== null && pending.key === row.key ? pending.capability : null}
|
|
266
275
|
deviceLabel={row.kind === 'remote-snapshot' ? deviceLabelOf.get(row.ref.remoteId ?? '') : undefined}
|
|
267
276
|
/>
|
|
268
277
|
))}
|
|
@@ -37,7 +37,7 @@ import {
|
|
|
37
37
|
type MarketPickerFilter, type MarketReviewStep,
|
|
38
38
|
} from '../../ui/market-import.ts'
|
|
39
39
|
import { redact } from '../../security/redaction.ts'
|
|
40
|
-
import { Badge, Banner, Button, Segmented,
|
|
40
|
+
import { Badge, Banner, Button, Segmented, Stepper } from '../common/ui.tsx'
|
|
41
41
|
import { ContentPicker } from '../common/ContentPicker.tsx'
|
|
42
42
|
import { ConflictList } from '../import/ConflictList.tsx'
|
|
43
43
|
import { ConfirmDialog } from '../common/ConfirmDialog.tsx'
|
|
@@ -337,10 +337,11 @@ export function MarketImportReview({
|
|
|
337
337
|
<Button onClick={() => { go(prevMarketStep(active, hasConflicts)) }}>{t('review.back')}</Button>
|
|
338
338
|
<Button
|
|
339
339
|
variant="primary"
|
|
340
|
-
|
|
340
|
+
loading={importing}
|
|
341
|
+
disabled={!summary.canImport}
|
|
341
342
|
onClick={() => { void runImport() }}
|
|
342
343
|
>
|
|
343
|
-
{
|
|
344
|
+
{t('detail.import')}
|
|
344
345
|
</Button>
|
|
345
346
|
</div>
|
|
346
347
|
</>
|
|
@@ -25,7 +25,7 @@ import type { TranslateNS } from '../client-types.ts'
|
|
|
25
25
|
import type { ConfigManagerApi } from '../api.ts'
|
|
26
26
|
import type { ImportResult, ItemResolution } from '../../core/types.ts'
|
|
27
27
|
import type { SectionId } from '../../schema/types.ts'
|
|
28
|
-
import { Badge, Banner, Button, Card, Empty, SectionTitle
|
|
28
|
+
import { Badge, Banner, Button, Card, Empty, SectionTitle } from '../common/ui.tsx'
|
|
29
29
|
import { InfoHint } from '../common/InfoHint.tsx'
|
|
30
30
|
import { Skeleton, SkeletonList } from '../common/Skeleton.tsx'
|
|
31
31
|
import { Select } from '../common/Select.tsx'
|
|
@@ -477,11 +477,11 @@ export function MarketPanel({ api, myConfigsApi, importApi, syncApi, t, cmT, ope
|
|
|
477
477
|
<Card>
|
|
478
478
|
<span className={css.groupLabel}>{t('config.title')}</span>
|
|
479
479
|
<div className={css.actionRow}>
|
|
480
|
-
<Button variant="primary"
|
|
481
|
-
{
|
|
480
|
+
<Button variant="primary" loading={state.refreshing} disabled={state.importing || state.browsing} onClick={() => { void runRefresh() }}>
|
|
481
|
+
{t('config.refresh')}
|
|
482
482
|
</Button>
|
|
483
|
-
<Button
|
|
484
|
-
{
|
|
483
|
+
<Button loading={state.browsing} disabled={state.refreshing || state.importing} onClick={() => { void runBrowse() }}>
|
|
484
|
+
{t('list.browse')}
|
|
485
485
|
</Button>
|
|
486
486
|
</div>
|
|
487
487
|
</Card>
|
|
@@ -712,8 +712,8 @@ export function MarketPanel({ api, myConfigsApi, importApi, syncApi, t, cmT, ope
|
|
|
712
712
|
</div>
|
|
713
713
|
</div>
|
|
714
714
|
{/* 下载按钮:任一下载进行中全部禁用(防止并发下载详情串扰 + 防重复点击) */}
|
|
715
|
-
<Button disabled={state.downloadingId !== null} onClick={() => { openDownload(it) }}>
|
|
716
|
-
{
|
|
715
|
+
<Button loading={state.downloadingId === it.id} disabled={state.downloadingId !== null} onClick={() => { openDownload(it) }}>
|
|
716
|
+
{t('list.download')}
|
|
717
717
|
</Button>
|
|
718
718
|
</div>
|
|
719
719
|
)
|
|
@@ -7,7 +7,7 @@ import type { TranslateNS } from '../client-types.ts'
|
|
|
7
7
|
import type { MyItemEntry } from './my-configs-api.ts'
|
|
8
8
|
import type { ItemStatusBadge, MyInstallState, MyItemView, MyItemsSummary } from './my-configs-view.ts'
|
|
9
9
|
import { redact } from '../../security/redaction.ts'
|
|
10
|
-
import { Badge, Banner, Button, Card, Empty
|
|
10
|
+
import { Badge, Banner, Button, Card, Empty } from '../common/ui.tsx'
|
|
11
11
|
import { SkeletonList } from '../common/Skeleton.tsx'
|
|
12
12
|
import css from '../config-manager.module.css'
|
|
13
13
|
|
|
@@ -65,8 +65,8 @@ export function MyConfigsList({
|
|
|
65
65
|
})}
|
|
66
66
|
</Badge>
|
|
67
67
|
)}
|
|
68
|
-
<Button
|
|
69
|
-
{
|
|
68
|
+
<Button loading={listLoading} onClick={() => { onRefresh() }}>
|
|
69
|
+
{t('myconfigs.list.refresh')}
|
|
70
70
|
</Button>
|
|
71
71
|
</div>
|
|
72
72
|
{myItemsError !== null && <Banner kind="error">{redact(myItemsError)}</Banner>}
|
|
@@ -697,7 +697,7 @@ export function MyConfigsView({
|
|
|
697
697
|
onFormField={onFormField}
|
|
698
698
|
onRunUpload={() => { void runUpload() }}
|
|
699
699
|
onCancelUpdate={cancelUpdate}
|
|
700
|
-
onRelist={
|
|
700
|
+
onRelist={runRelist}
|
|
701
701
|
onReset={resetWizard}
|
|
702
702
|
/>
|
|
703
703
|
)
|
|
@@ -4,12 +4,13 @@
|
|
|
4
4
|
* 本文件只渲染 + 上抛回调:向导全量状态是受控 prop,所有状态迁移与请求(校验/上传/更新/重试收录)
|
|
5
5
|
* 都在 MyConfigsView.tsx;PR 链接来源优先级复用 ../../ui/my-configs-view.ts 的 myPrLinkSource。
|
|
6
6
|
*/
|
|
7
|
+
import { useState } from 'react'
|
|
7
8
|
import type { ChangeEvent } from 'react'
|
|
8
9
|
import type { TranslateNS } from '../client-types.ts'
|
|
9
10
|
import type { ListingStatusResponse } from '../../market/my-repo.ts'
|
|
10
11
|
import { redact } from '../../security/redaction.ts'
|
|
11
12
|
import { myPrLinkSource } from '../../ui/my-configs-view.ts'
|
|
12
|
-
import { Badge, Banner, Button, Field
|
|
13
|
+
import { Badge, Banner, Button, Field } from '../common/ui.tsx'
|
|
13
14
|
import { Modal } from '../common/Modal.tsx'
|
|
14
15
|
import { InfoHint } from '../common/InfoHint.tsx'
|
|
15
16
|
import { myConfigFormValid } from './my-configs-view.ts'
|
|
@@ -37,8 +38,12 @@ export interface MyConfigsWizardProps {
|
|
|
37
38
|
onRunUpload: () => void
|
|
38
39
|
/** 取消更新:向导回到「一键上传」初始态(弹窗保持打开) */
|
|
39
40
|
onCancelUpdate: () => void
|
|
40
|
-
/**
|
|
41
|
-
|
|
41
|
+
/**
|
|
42
|
+
* 结果卡「重新收录」。**返回 Promise**:重试是一次网络往返 + 重新轮询,
|
|
43
|
+
* 本组件要等它结束才放开按钮(此前返回 void,调用方只能 `void onRelist(...)`,
|
|
44
|
+
* 点击后按钮毫无变化,用户会以为没点上而连点)。
|
|
45
|
+
*/
|
|
46
|
+
onRelist: (itemId: string) => Promise<void>
|
|
42
47
|
/** 重选 zip:update 轻量重置(保留预填表单)/ upload 完全重置 */
|
|
43
48
|
onReset: () => void
|
|
44
49
|
}
|
|
@@ -47,6 +52,18 @@ export function MyConfigsWizard({
|
|
|
47
52
|
wizard, listingStatus, t, fileInput, autoBadges,
|
|
48
53
|
onClose, onPickFile, onFormField, onRunUpload, onCancelUpdate, onRelist, onReset,
|
|
49
54
|
}: MyConfigsWizardProps) {
|
|
55
|
+
/**
|
|
56
|
+
* 「重新收录」的在途态。是**一次点击的瞬时 UI 态**,不是向导的领域状态
|
|
57
|
+
* (向导状态仍全部归容器),所以留在本组件里;等 Promise 落定即放开。
|
|
58
|
+
*/
|
|
59
|
+
const [relisting, setRelisting] = useState(false)
|
|
60
|
+
const runRelist = (itemId: string): void => {
|
|
61
|
+
if (relisting) return
|
|
62
|
+
setRelisting(true)
|
|
63
|
+
// 失败文案由容器统一走常驻 Toast(runRelist 内部已 catch),这里只管收尾
|
|
64
|
+
void onRelist(itemId).finally(() => { setRelisting(false) })
|
|
65
|
+
}
|
|
66
|
+
|
|
50
67
|
/** PR 链接(优先实时任务状态;收录完成后由轮询补上,或直接取同步结果) */
|
|
51
68
|
const prLink = ((): { url: string; label: string } | null => {
|
|
52
69
|
// 来源优先级(实时任务状态 → 向导结果)在 src/ui/my-configs-view.ts 的 myPrLinkSource
|
|
@@ -109,10 +126,10 @@ export function MyConfigsWizard({
|
|
|
109
126
|
<div className={css.actionRow}>
|
|
110
127
|
<Button
|
|
111
128
|
variant="primary"
|
|
112
|
-
|
|
129
|
+
loading={wizard.validating}
|
|
113
130
|
onClick={onReset}
|
|
114
131
|
>
|
|
115
|
-
{
|
|
132
|
+
{t('myconfigs.upload.reselect')}
|
|
116
133
|
</Button>
|
|
117
134
|
</div>
|
|
118
135
|
{wizard.validationError !== null && <Banner kind="error">{redact(wizard.validationError)}</Banner>}
|
|
@@ -197,15 +214,14 @@ export function MyConfigsWizard({
|
|
|
197
214
|
<div className={css.actionRow}>
|
|
198
215
|
<Button
|
|
199
216
|
variant="primary"
|
|
217
|
+
loading={wizard.running}
|
|
200
218
|
disabled={
|
|
201
|
-
wizard.validated !== true || wizard.
|
|
219
|
+
wizard.validated !== true || wizard.zipPath === null
|
|
202
220
|
|| !myConfigFormValid(wizard.formErrors)
|
|
203
221
|
}
|
|
204
222
|
onClick={() => { onRunUpload() }}
|
|
205
223
|
>
|
|
206
|
-
{wizard.
|
|
207
|
-
? <Spinner label={wizard.mode === 'update' ? t('myconfigs.update.running') : t('myconfigs.upload.running')} />
|
|
208
|
-
: (wizard.mode === 'update' ? t('myconfigs.update.run') : t('myconfigs.upload.run'))}
|
|
224
|
+
{wizard.mode === 'update' ? t('myconfigs.update.run') : t('myconfigs.upload.run')}
|
|
209
225
|
</Button>
|
|
210
226
|
{wizard.mode === 'update' && (
|
|
211
227
|
<Button disabled={wizard.running || wizard.validating} onClick={onCancelUpdate}>{t('common.cancel')}</Button>
|
|
@@ -233,7 +249,8 @@ export function MyConfigsWizard({
|
|
|
233
249
|
{listingStatus !== null && listingStatus.listing === 'failed' ? (
|
|
234
250
|
<>
|
|
235
251
|
<Badge kind="error">{t('myconfigs.result.listingFailed')}</Badge>
|
|
236
|
-
|
|
252
|
+
{/* 转圈图标由 Button 原语按 loading 自动渲染(DESIGN §14.11) */}
|
|
253
|
+
<Button variant="danger" loading={relisting} onClick={() => { runRelist(wizard.result!.itemId) }}>
|
|
237
254
|
{t('myconfigs.result.relist')}
|
|
238
255
|
</Button>
|
|
239
256
|
</>
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import type { ChangeEvent } from 'react'
|
|
8
8
|
import type { TranslateNS } from '../client-types.ts'
|
|
9
9
|
import type { UiT } from '../../ui/i18n.ts'
|
|
10
|
-
import { Badge, Banner, Button, Checkbox
|
|
10
|
+
import { Badge, Banner, Button, Checkbox } from '../common/ui.tsx'
|
|
11
11
|
import { InfoHint } from '../common/InfoHint.tsx'
|
|
12
12
|
import { Modal } from '../common/Modal.tsx'
|
|
13
13
|
import { Select } from '../common/Select.tsx'
|
|
@@ -444,10 +444,11 @@ export function ChannelConfigDialog({ open, onClose, api, t, uiT, form, cloudIss
|
|
|
444
444
|
<div className={css.actionRow}>
|
|
445
445
|
<Button
|
|
446
446
|
variant="primary"
|
|
447
|
-
|
|
447
|
+
loading={savingConfig}
|
|
448
|
+
disabled={busy || !remoteReady}
|
|
448
449
|
onClick={() => { onSave() }}
|
|
449
450
|
>
|
|
450
|
-
{
|
|
451
|
+
{t('config.save')}
|
|
451
452
|
</Button>
|
|
452
453
|
<InfoHint text={t('config.saveHint')} label={t('common.infoHint')} />
|
|
453
454
|
</div>
|
|
@@ -13,7 +13,7 @@ import type { ReactNode } from 'react'
|
|
|
13
13
|
import { useEffect } from 'react'
|
|
14
14
|
|
|
15
15
|
import { redact } from '../../security/redaction.ts'
|
|
16
|
-
import { Badge, Banner, Button
|
|
16
|
+
import { Badge, Banner, Button } from '../common/ui.tsx'
|
|
17
17
|
import { Skeleton } from '../common/Skeleton.tsx'
|
|
18
18
|
import { toast } from '../common/toast-store.ts'
|
|
19
19
|
import { ConsultCard } from '../consult/ConsultCard.tsx'
|
|
@@ -494,10 +494,10 @@ function ApplyResultCard({ result, busy, t, onRollback }: ApplyResultCardProps):
|
|
|
494
494
|
{result.restoreId !== '' && (
|
|
495
495
|
<Button
|
|
496
496
|
variant="danger"
|
|
497
|
-
|
|
497
|
+
loading={busy}
|
|
498
498
|
onClick={() => { void onRollback(result.restoreId) }}
|
|
499
499
|
>
|
|
500
|
-
{
|
|
500
|
+
{t('syncflow.rollback')}
|
|
501
501
|
</Button>
|
|
502
502
|
)}
|
|
503
503
|
</>
|
|
@@ -44,7 +44,7 @@ import type { ReactNode } from 'react'
|
|
|
44
44
|
import type { TranslateNS } from '../client-types.ts'
|
|
45
45
|
import type { SyncPullReport, SyncPushPreview, SyncPushReport } from '../../sync/sync-engine.ts'
|
|
46
46
|
import type { SectionId } from '../../schema/types.ts'
|
|
47
|
-
import { Badge, Banner, Button, Card, SectionTitle
|
|
47
|
+
import { Badge, Banner, Button, Card, SectionTitle } from '../common/ui.tsx'
|
|
48
48
|
import { ErrorBanner } from '../common/ErrorBanner.tsx'
|
|
49
49
|
import { InfoHint } from '../common/InfoHint.tsx'
|
|
50
50
|
import { Select } from '../common/Select.tsx'
|
|
@@ -1312,13 +1312,12 @@ export function SyncPanel({ api, t, cmT }: SyncPanelProps) {
|
|
|
1312
1312
|
</label>
|
|
1313
1313
|
<span className={css.pickerAction}>
|
|
1314
1314
|
<Button
|
|
1315
|
-
|
|
1315
|
+
loading={cs.loadingSnapshots === true}
|
|
1316
|
+
disabled={busy}
|
|
1316
1317
|
onClick={() => { void loadSnapshots(undefined, channel, true) }}
|
|
1317
1318
|
title={t('syncflow.refreshSnapshots')}
|
|
1318
1319
|
>
|
|
1319
|
-
{cs.loadingSnapshots === true
|
|
1320
|
-
? <Spinner label={t('syncflow.refreshingSnapshots')} />
|
|
1321
|
-
: <><RefreshIcon size={14} /> {t('syncflow.refreshSnapshots')}</>}
|
|
1320
|
+
{cs.loadingSnapshots === true ? null : <RefreshIcon size={14} />} {t('syncflow.refreshSnapshots')}
|
|
1322
1321
|
</Button>
|
|
1323
1322
|
</span>
|
|
1324
1323
|
</div>
|
|
@@ -1489,10 +1488,10 @@ export function SyncPanel({ api, t, cmT }: SyncPanelProps) {
|
|
|
1489
1488
|
</Button>
|
|
1490
1489
|
<Button
|
|
1491
1490
|
variant="primary"
|
|
1492
|
-
|
|
1491
|
+
loading={state.busy === 'push'}
|
|
1493
1492
|
onClick={() => { void runPush() }}
|
|
1494
1493
|
>
|
|
1495
|
-
{
|
|
1494
|
+
{t('syncflow.pushConfirm')}
|
|
1496
1495
|
</Button>
|
|
1497
1496
|
</Modal.Footer>
|
|
1498
1497
|
</Modal>
|
|
@@ -23,7 +23,7 @@ import { useCallback, useEffect, useRef, useState } from 'react'
|
|
|
23
23
|
import type { ReactNode } from 'react'
|
|
24
24
|
import type { TranslateNS } from '../client-types.ts'
|
|
25
25
|
import type { SyncApi, SyncProbeResult, SyncPushPayload } from './sync-api.ts'
|
|
26
|
-
import { Banner, Button
|
|
26
|
+
import { Banner, Button } from '../common/ui.tsx'
|
|
27
27
|
import { InfoHint } from '../common/InfoHint.tsx'
|
|
28
28
|
import { projectProbeResult } from './probe-view.ts'
|
|
29
29
|
import { toast } from '../common/toast-store.ts'
|
|
@@ -104,8 +104,8 @@ export function SyncProbeRow({ api, t, body, remoteReady, busy, presentation = '
|
|
|
104
104
|
const view = result === null ? null : projectProbeResult(result, t('probe.unsupported'))
|
|
105
105
|
|
|
106
106
|
const button = (
|
|
107
|
-
<Button onClick={() => { void run() }} disabled={busy ||
|
|
108
|
-
{
|
|
107
|
+
<Button onClick={() => { void run() }} loading={loading} disabled={busy || !remoteReady}>
|
|
108
|
+
{t('probe.run')}
|
|
109
109
|
</Button>
|
|
110
110
|
)
|
|
111
111
|
const hintNode = <InfoHint text={t('probe.hint')} label={t('common.infoHint')} />
|
|
@@ -20,7 +20,7 @@ import type { ChangeEvent } from 'react'
|
|
|
20
20
|
|
|
21
21
|
import type { TranslateNS } from '../client-types.ts'
|
|
22
22
|
import { redact } from '../../security/redaction.ts'
|
|
23
|
-
import { Button
|
|
23
|
+
import { Button } from '../common/ui.tsx'
|
|
24
24
|
import { InfoHint } from '../common/InfoHint.tsx'
|
|
25
25
|
import { Select } from '../common/Select.tsx'
|
|
26
26
|
import { toast } from '../common/toast-store.ts'
|
|
@@ -159,8 +159,8 @@ export function SyncRepositoryPicker({ open, api, t, repoUrl, busy, onPick }: Sy
|
|
|
159
159
|
onOpen={() => { if (!loaded && !loading) void load() }}
|
|
160
160
|
onChange={pick}
|
|
161
161
|
/>
|
|
162
|
-
<Button size="sm" disabled={busy
|
|
163
|
-
{
|
|
162
|
+
<Button size="sm" loading={loading} disabled={busy} onClick={() => { void load() }}>
|
|
163
|
+
{t('repoPicker.refresh')}
|
|
164
164
|
</Button>
|
|
165
165
|
</div>
|
|
166
166
|
{/* 空态 / 失败说明留在原地(不进 ⓘ):用户正卡在这里,说明必须看得见 */}
|
|
@@ -200,8 +200,8 @@ export function SyncRepositoryPicker({ open, api, t, repoUrl, busy, onPick }: Sy
|
|
|
200
200
|
{nameHint !== null && <span className={css.formError}>{nameHint}</span>}
|
|
201
201
|
{createError !== null && <span className={css.formError}>{redact(createError)}</span>}
|
|
202
202
|
<div className={css.actionRowTop}>
|
|
203
|
-
<Button variant="primary" size="sm" disabled={busy
|
|
204
|
-
{
|
|
203
|
+
<Button variant="primary" size="sm" loading={creating} disabled={busy} onClick={() => { void submitCreate() }}>
|
|
204
|
+
{t('repoPicker.createSubmit')}
|
|
205
205
|
</Button>
|
|
206
206
|
<Button size="sm" disabled={creating} onClick={() => { setCreateOpen(false) }}>
|
|
207
207
|
{t('repoPicker.createCancel')}
|
package/src/index.ts
CHANGED
|
@@ -180,7 +180,7 @@ export const name = 'config-manager'
|
|
|
180
180
|
export const inject = ['settings', 'credentials']
|
|
181
181
|
|
|
182
182
|
/** Plugin version, kept in sync with package.json ("version"). */
|
|
183
|
-
export const PLUGIN_VERSION = '0.1.
|
|
183
|
+
export const PLUGIN_VERSION = '0.1.83'
|
|
184
184
|
|
|
185
185
|
/** Plugin own package name — excluded from its own exported plugins list. */
|
|
186
186
|
const PLUGIN_NAME = 'dsh-config-manager'
|