dsh-config-manager 0.1.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/README.md +216 -0
- package/README.zh-CN.md +218 -0
- package/cordis.patch.yml +45 -0
- package/dsh.bundle.patch +21 -0
- package/dsh.client +39 -0
- package/lib/adapters/agent-presets.d.ts +13 -0
- package/lib/adapters/agent-presets.js +14 -0
- package/lib/adapters/agent-presets.js.map +1 -0
- package/lib/adapters/credentials.d.ts +36 -0
- package/lib/adapters/credentials.js +121 -0
- package/lib/adapters/credentials.js.map +1 -0
- package/lib/adapters/file-collection.d.ts +14 -0
- package/lib/adapters/file-collection.js +102 -0
- package/lib/adapters/file-collection.js.map +1 -0
- package/lib/adapters/index.d.ts +38 -0
- package/lib/adapters/index.js +46 -0
- package/lib/adapters/index.js.map +1 -0
- package/lib/adapters/mcp.d.ts +27 -0
- package/lib/adapters/mcp.js +175 -0
- package/lib/adapters/mcp.js.map +1 -0
- package/lib/adapters/plugin-files.d.ts +15 -0
- package/lib/adapters/plugin-files.js +91 -0
- package/lib/adapters/plugin-files.js.map +1 -0
- package/lib/adapters/plugins.d.ts +13 -0
- package/lib/adapters/plugins.js +177 -0
- package/lib/adapters/plugins.js.map +1 -0
- package/lib/adapters/prompts.d.ts +29 -0
- package/lib/adapters/prompts.js +228 -0
- package/lib/adapters/prompts.js.map +1 -0
- package/lib/adapters/providers.d.ts +33 -0
- package/lib/adapters/providers.js +143 -0
- package/lib/adapters/providers.js.map +1 -0
- package/lib/adapters/sessions.d.ts +14 -0
- package/lib/adapters/sessions.js +15 -0
- package/lib/adapters/sessions.js.map +1 -0
- package/lib/adapters/settings.d.ts +23 -0
- package/lib/adapters/settings.js +142 -0
- package/lib/adapters/settings.js.map +1 -0
- package/lib/adapters/skills.d.ts +13 -0
- package/lib/adapters/skills.js +14 -0
- package/lib/adapters/skills.js.map +1 -0
- package/lib/adapters/test-helpers.d.ts +99 -0
- package/lib/adapters/test-helpers.js +192 -0
- package/lib/adapters/test-helpers.js.map +1 -0
- package/lib/adapters/ui.d.ts +26 -0
- package/lib/adapters/ui.js +88 -0
- package/lib/adapters/ui.js.map +1 -0
- package/lib/adapters/workspaces.d.ts +12 -0
- package/lib/adapters/workspaces.js +92 -0
- package/lib/adapters/workspaces.js.map +1 -0
- package/lib/client.d.ts +377 -0
- package/lib/client.js +2598 -0
- package/lib/client.js.map +1 -0
- package/lib/core/analyzer.d.ts +35 -0
- package/lib/core/analyzer.js +563 -0
- package/lib/core/analyzer.js.map +1 -0
- package/lib/core/backup.d.ts +29 -0
- package/lib/core/backup.js +184 -0
- package/lib/core/backup.js.map +1 -0
- package/lib/core/exporter.d.ts +37 -0
- package/lib/core/exporter.js +222 -0
- package/lib/core/exporter.js.map +1 -0
- package/lib/core/importer.d.ts +42 -0
- package/lib/core/importer.js +25 -0
- package/lib/core/importer.js.map +1 -0
- package/lib/core/index.d.ts +12 -0
- package/lib/core/index.js +11 -0
- package/lib/core/index.js.map +1 -0
- package/lib/core/rollback.d.ts +9 -0
- package/lib/core/rollback.js +136 -0
- package/lib/core/rollback.js.map +1 -0
- package/lib/core/types.d.ts +331 -0
- package/lib/core/types.js +18 -0
- package/lib/core/types.js.map +1 -0
- package/lib/core/validator.d.ts +18 -0
- package/lib/core/validator.js +77 -0
- package/lib/core/validator.js.map +1 -0
- package/lib/index.d.ts +51 -0
- package/lib/index.js +875 -0
- package/lib/index.js.map +1 -0
- package/lib/migrations/index.d.ts +29 -0
- package/lib/migrations/index.js +56 -0
- package/lib/migrations/index.js.map +1 -0
- package/lib/migrations/v1-to-v2.d.ts +9 -0
- package/lib/migrations/v1-to-v2.js +10 -0
- package/lib/migrations/v1-to-v2.js.map +1 -0
- package/lib/profiles/index.d.ts +4 -0
- package/lib/profiles/index.js +5 -0
- package/lib/profiles/index.js.map +1 -0
- package/lib/profiles/profile-manager.d.ts +102 -0
- package/lib/profiles/profile-manager.js +522 -0
- package/lib/profiles/profile-manager.js.map +1 -0
- package/lib/schema/config.d.ts +28 -0
- package/lib/schema/config.js +136 -0
- package/lib/schema/config.js.map +1 -0
- package/lib/schema/manifest.d.ts +28 -0
- package/lib/schema/manifest.js +109 -0
- package/lib/schema/manifest.js.map +1 -0
- package/lib/schema/types.d.ts +169 -0
- package/lib/schema/types.js +8 -0
- package/lib/schema/types.js.map +1 -0
- package/lib/schema/versions.d.ts +27 -0
- package/lib/schema/versions.js +50 -0
- package/lib/schema/versions.js.map +1 -0
- package/lib/security/encryption.d.ts +41 -0
- package/lib/security/encryption.js +152 -0
- package/lib/security/encryption.js.map +1 -0
- package/lib/security/index.d.ts +16 -0
- package/lib/security/index.js +17 -0
- package/lib/security/index.js.map +1 -0
- package/lib/security/integrity.d.ts +30 -0
- package/lib/security/integrity.js +78 -0
- package/lib/security/integrity.js.map +1 -0
- package/lib/security/redaction.d.ts +26 -0
- package/lib/security/redaction.js +95 -0
- package/lib/security/redaction.js.map +1 -0
- package/lib/security/secret-scanner.d.ts +48 -0
- package/lib/security/secret-scanner.js +253 -0
- package/lib/security/secret-scanner.js.map +1 -0
- package/lib/security/zip-security.d.ts +27 -0
- package/lib/security/zip-security.js +154 -0
- package/lib/security/zip-security.js.map +1 -0
- package/lib/ui/conflict-view.d.ts +28 -0
- package/lib/ui/conflict-view.js +49 -0
- package/lib/ui/conflict-view.js.map +1 -0
- package/lib/ui/errors.d.ts +17 -0
- package/lib/ui/errors.js +76 -0
- package/lib/ui/errors.js.map +1 -0
- package/lib/ui/export-flow.d.ts +62 -0
- package/lib/ui/export-flow.js +79 -0
- package/lib/ui/export-flow.js.map +1 -0
- package/lib/ui/import-wizard.d.ts +67 -0
- package/lib/ui/import-wizard.js +169 -0
- package/lib/ui/import-wizard.js.map +1 -0
- package/lib/ui/path-mapping.d.ts +38 -0
- package/lib/ui/path-mapping.js +75 -0
- package/lib/ui/path-mapping.js.map +1 -0
- package/lib/ui/progress.d.ts +27 -0
- package/lib/ui/progress.js +76 -0
- package/lib/ui/progress.js.map +1 -0
- package/lib/ui/report.d.ts +21 -0
- package/lib/ui/report.js +146 -0
- package/lib/ui/report.js.map +1 -0
- package/lib/ui/test-helpers.d.ts +62 -0
- package/lib/ui/test-helpers.js +147 -0
- package/lib/ui/test-helpers.js.map +1 -0
- package/lib/ui/types.d.ts +129 -0
- package/lib/ui/types.js +21 -0
- package/lib/ui/types.js.map +1 -0
- package/lib/utils/hashing.d.ts +17 -0
- package/lib/utils/hashing.js +41 -0
- package/lib/utils/hashing.js.map +1 -0
- package/lib/utils/json.d.ts +24 -0
- package/lib/utils/json.js +76 -0
- package/lib/utils/json.js.map +1 -0
- package/lib/utils/logger.d.ts +39 -0
- package/lib/utils/logger.js +82 -0
- package/lib/utils/logger.js.map +1 -0
- package/lib/utils/paths.d.ts +31 -0
- package/lib/utils/paths.js +132 -0
- package/lib/utils/paths.js.map +1 -0
- package/lib/utils/zip.d.ts +54 -0
- package/lib/utils/zip.js +285 -0
- package/lib/utils/zip.js.map +1 -0
- package/package.json +109 -0
- package/src/adapters/agent-presets.ts +14 -0
- package/src/adapters/credentials.test.ts +90 -0
- package/src/adapters/credentials.ts +141 -0
- package/src/adapters/file-collection.ts +110 -0
- package/src/adapters/files.test.ts +113 -0
- package/src/adapters/index.ts +69 -0
- package/src/adapters/mcp.test.ts +96 -0
- package/src/adapters/mcp.ts +181 -0
- package/src/adapters/plugin-files.ts +96 -0
- package/src/adapters/plugins.test.ts +72 -0
- package/src/adapters/plugins.ts +176 -0
- package/src/adapters/prompts.test.ts +110 -0
- package/src/adapters/prompts.ts +226 -0
- package/src/adapters/providers.test.ts +125 -0
- package/src/adapters/providers.ts +161 -0
- package/src/adapters/roundtrip.test.ts +188 -0
- package/src/adapters/sessions.ts +15 -0
- package/src/adapters/settings.test.ts +93 -0
- package/src/adapters/settings.ts +165 -0
- package/src/adapters/skills.ts +14 -0
- package/src/adapters/test-helpers.ts +207 -0
- package/src/adapters/ui.test.ts +56 -0
- package/src/adapters/ui.ts +106 -0
- package/src/adapters/workspaces.test.ts +79 -0
- package/src/adapters/workspaces.ts +95 -0
- package/src/client/CLIENT_DEPENDENCIES.md +101 -0
- package/src/client/ConfigManagerSection.tsx +60 -0
- package/src/client/api.ts +270 -0
- package/src/client/client-types.ts +27 -0
- package/src/client/common/ErrorBanner.tsx +68 -0
- package/src/client/common/ProgressBar.tsx +49 -0
- package/src/client/common/ReportView.tsx +117 -0
- package/src/client/common/ui.tsx +165 -0
- package/src/client/config-manager.module.css +759 -0
- package/src/client/css-modules.d.ts +8 -0
- package/src/client/export/ExportView.tsx +222 -0
- package/src/client/import/ConflictList.tsx +72 -0
- package/src/client/import/ImportWizardView.tsx +406 -0
- package/src/client/import/PathMappingForm.tsx +77 -0
- package/src/client/index.ts +65 -0
- package/src/client/locales.ts +182 -0
- package/src/core/analyzer.ts +631 -0
- package/src/core/backup.ts +212 -0
- package/src/core/exporter.ts +246 -0
- package/src/core/importer.ts +66 -0
- package/src/core/index.ts +39 -0
- package/src/core/rollback.ts +140 -0
- package/src/core/smoke.test.ts +717 -0
- package/src/core/types.ts +343 -0
- package/src/core/validator.ts +84 -0
- package/src/index.ts +942 -0
- package/src/migrations/index.ts +71 -0
- package/src/migrations/v1-to-v2.ts +17 -0
- package/src/profiles/index.ts +8 -0
- package/src/profiles/profile-manager.test.ts +280 -0
- package/src/profiles/profile-manager.ts +606 -0
- package/src/schema/config.ts +137 -0
- package/src/schema/manifest.ts +124 -0
- package/src/schema/types.ts +179 -0
- package/src/schema/versions.ts +55 -0
- package/src/security/encryption.ts +180 -0
- package/src/security/index.ts +16 -0
- package/src/security/integrity.ts +102 -0
- package/src/security/redaction.ts +101 -0
- package/src/security/secret-scanner.ts +275 -0
- package/src/security/security.test.ts +796 -0
- package/src/security/zip-security.ts +180 -0
- package/src/ui/conflict-view.test.ts +62 -0
- package/src/ui/conflict-view.ts +65 -0
- package/src/ui/errors.test.ts +44 -0
- package/src/ui/errors.ts +95 -0
- package/src/ui/export-flow.test.ts +75 -0
- package/src/ui/export-flow.ts +128 -0
- package/src/ui/import-wizard.test.ts +128 -0
- package/src/ui/import-wizard.ts +208 -0
- package/src/ui/path-mapping.test.ts +67 -0
- package/src/ui/path-mapping.ts +103 -0
- package/src/ui/progress.test.ts +42 -0
- package/src/ui/progress.ts +90 -0
- package/src/ui/report.test.ts +93 -0
- package/src/ui/report.ts +140 -0
- package/src/ui/test-helpers.ts +177 -0
- package/src/ui/types.ts +193 -0
- package/src/utils/hashing.ts +53 -0
- package/src/utils/json.ts +77 -0
- package/src/utils/logger.ts +114 -0
- package/src/utils/paths.ts +122 -0
- package/src/utils/zip.ts +332 -0
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 导出视图(规范 §1 / §21,绑 src/ui/export-flow.ts 的 ExportFlow 控制器)。
|
|
3
|
+
*
|
|
4
|
+
* - Quick:一键导出推荐分区(ExportFlow.quickSelection());
|
|
5
|
+
* - Custom:按 §1 分组目录(EXPORT_GROUPS × DEFAULT_CATEGORIES)逐项勾选,
|
|
6
|
+
* ExportFlow.validateSelection() 给出设备相关分区警告;
|
|
7
|
+
* - Include secrets:可选加密备份(需设置加密密码,密码仅本次内存使用,
|
|
8
|
+
* 经 api.exportPassword 随请求体传给 Host 半,绝不落盘/入 manifest);
|
|
9
|
+
* - 进度:ExportFlow.run 发出 ProgressEvent → ProgressBar;
|
|
10
|
+
* - 结果:ReportView(export) + 下载按钮(File System Access API 流式落盘)。
|
|
11
|
+
*/
|
|
12
|
+
import { useCallback, useState } from 'react'
|
|
13
|
+
import type { ChangeEvent } from 'react'
|
|
14
|
+
import { ExportFlow, DEFAULT_CATEGORIES } from '../../ui/export-flow.ts'
|
|
15
|
+
import { EXPORT_GROUPS } from '../../ui/types.ts'
|
|
16
|
+
import type { ExportRunResult } from '../../ui/export-flow.ts'
|
|
17
|
+
import type { ProgressEvent } from '../../ui/types.ts'
|
|
18
|
+
import type { SectionId } from '../../schema/types.ts'
|
|
19
|
+
import type { TranslateNS } from '../client-types.ts'
|
|
20
|
+
import type { ConfigManagerApi } from '../api.ts'
|
|
21
|
+
import { Badge, Banner, Button, Card, Checkbox, SectionTitle, Spinner } from '../common/ui.tsx'
|
|
22
|
+
import { ErrorBanner } from '../common/ErrorBanner.tsx'
|
|
23
|
+
import { ProgressBar } from '../common/ProgressBar.tsx'
|
|
24
|
+
import { ReportView } from '../common/ReportView.tsx'
|
|
25
|
+
import css from '../config-manager.module.css'
|
|
26
|
+
|
|
27
|
+
export interface ExportViewProps {
|
|
28
|
+
api: ConfigManagerApi
|
|
29
|
+
t: TranslateNS<'config-manager'>
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
type ExportMode = 'quick' | 'custom'
|
|
33
|
+
|
|
34
|
+
/** Custom 模式的初始勾选 = 推荐分区(可再调整) */
|
|
35
|
+
function defaultCustomSelection(): SectionId[] {
|
|
36
|
+
return DEFAULT_CATEGORIES.filter((c) => c.defaultIncluded).map((c) => c.id)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* 导出主视图:Quick/Custom 切换 → 勾选/密码 → 执行 → 进度 → 报告 → 下载。
|
|
41
|
+
*/
|
|
42
|
+
export function ExportView({ api, t }: ExportViewProps) {
|
|
43
|
+
const [mode, setMode] = useState<ExportMode>('quick')
|
|
44
|
+
const [selection, setSelection] = useState<SectionId[]>(defaultCustomSelection)
|
|
45
|
+
const [includeSecrets, setIncludeSecrets] = useState(false)
|
|
46
|
+
const [password, setPassword] = useState('')
|
|
47
|
+
const [passwordConfirm, setPasswordConfirm] = useState('')
|
|
48
|
+
const [running, setRunning] = useState(false)
|
|
49
|
+
const [progress, setProgress] = useState<ProgressEvent | null>(null)
|
|
50
|
+
const [result, setResult] = useState<ExportRunResult | null>(null)
|
|
51
|
+
const [error, setError] = useState<unknown>(null)
|
|
52
|
+
const [downloaded, setDownloaded] = useState(false)
|
|
53
|
+
|
|
54
|
+
// ExportFlow 控制器实例(port=api,onProgress 桥到 React state)
|
|
55
|
+
const [flow] = useState(() => new ExportFlow({
|
|
56
|
+
port: api,
|
|
57
|
+
onProgress: (event) => { setProgress(event) },
|
|
58
|
+
}))
|
|
59
|
+
|
|
60
|
+
const toggleSection = useCallback((id: SectionId, checked: boolean) => {
|
|
61
|
+
setSelection((prev) => {
|
|
62
|
+
const has = prev.includes(id)
|
|
63
|
+
if (checked && !has) return [...prev, id]
|
|
64
|
+
if (!checked && has) return prev.filter((s) => s !== id)
|
|
65
|
+
return prev
|
|
66
|
+
})
|
|
67
|
+
}, [])
|
|
68
|
+
|
|
69
|
+
const passwordInvalid =
|
|
70
|
+
includeSecrets && (password === '' || password !== passwordConfirm)
|
|
71
|
+
|
|
72
|
+
/** 执行导出(Quick 或 Custom) */
|
|
73
|
+
const runExport = async (): Promise<void> => {
|
|
74
|
+
if (passwordInvalid) return
|
|
75
|
+
setRunning(true)
|
|
76
|
+
setError(null)
|
|
77
|
+
setResult(null)
|
|
78
|
+
setDownloaded(false)
|
|
79
|
+
setProgress(null)
|
|
80
|
+
try {
|
|
81
|
+
// 加密密码随本次导出请求体传给 Host 半(仅内存)
|
|
82
|
+
api.exportPassword = includeSecrets ? password : null
|
|
83
|
+
const run = await flow.run(mode, selection, { includeSecrets })
|
|
84
|
+
setResult(run)
|
|
85
|
+
} catch (err) {
|
|
86
|
+
setError(err)
|
|
87
|
+
} finally {
|
|
88
|
+
setRunning(false)
|
|
89
|
+
setProgress(null)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** 下载导出的 ZIP 到本机 */
|
|
94
|
+
const download = async (): Promise<void> => {
|
|
95
|
+
if (result === null) return
|
|
96
|
+
try {
|
|
97
|
+
setError(null)
|
|
98
|
+
const outcome = await api.download(result.zipPath)
|
|
99
|
+
setDownloaded(true)
|
|
100
|
+
void outcome // blob/streamed 由 api 处理;此处仅确认成功
|
|
101
|
+
} catch (err) {
|
|
102
|
+
setError(err)
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Custom 模式下的设备相关分区警告
|
|
107
|
+
const customWarnings = mode === 'custom' ? flow.validateSelection(selection).warnings : []
|
|
108
|
+
|
|
109
|
+
return (
|
|
110
|
+
<div className={css.viewBody}>
|
|
111
|
+
<SectionTitle title={t('view.export')} subtitle={t('section.description')} />
|
|
112
|
+
|
|
113
|
+
{/* 模式切换 */}
|
|
114
|
+
<div className={css.modeTabs} role="tablist">
|
|
115
|
+
<button type="button" role="tab" aria-selected={mode === 'quick'} data-active={mode === 'quick' ? '' : undefined} className={css.modeTab} onClick={() => { setMode('quick') }}>
|
|
116
|
+
{t('export.mode.quick')}
|
|
117
|
+
</button>
|
|
118
|
+
<button type="button" role="tab" aria-selected={mode === 'custom'} data-active={mode === 'custom' ? '' : undefined} className={css.modeTab} onClick={() => { setMode('custom') }}>
|
|
119
|
+
{t('export.mode.custom')}
|
|
120
|
+
</button>
|
|
121
|
+
</div>
|
|
122
|
+
<div className={css.modeHint}>
|
|
123
|
+
{mode === 'quick' ? t('export.mode.quickHint') : t('export.mode.customHint')}
|
|
124
|
+
</div>
|
|
125
|
+
|
|
126
|
+
{/* Custom:分组勾选目录 */}
|
|
127
|
+
{mode === 'custom' && (
|
|
128
|
+
<div className={css.groupList}>
|
|
129
|
+
{EXPORT_GROUPS.map((group) => {
|
|
130
|
+
const categories = flow.categories.filter((c) => c.group === group.id)
|
|
131
|
+
if (categories.length === 0) return null
|
|
132
|
+
return (
|
|
133
|
+
<Card key={group.id} className={css.groupCard}>
|
|
134
|
+
<div className={css.groupHeader}>
|
|
135
|
+
<span className={css.groupLabel}>{group.label}</span>
|
|
136
|
+
{group.note !== undefined && <span className={css.groupNote}>{group.note}</span>}
|
|
137
|
+
</div>
|
|
138
|
+
<div className={css.groupItems}>
|
|
139
|
+
{categories.map((cat) => (
|
|
140
|
+
<Checkbox
|
|
141
|
+
key={cat.id}
|
|
142
|
+
checked={selection.includes(cat.id)}
|
|
143
|
+
onChange={(checked) => { toggleSection(cat.id, checked) }}
|
|
144
|
+
label={
|
|
145
|
+
<span className={css.categoryItem}>
|
|
146
|
+
<span className={css.categoryName}>{cat.label}</span>
|
|
147
|
+
<span className={css.categoryDesc}>{cat.description}</span>
|
|
148
|
+
{cat.portability !== 'portable' && (
|
|
149
|
+
<Badge kind={cat.portability === 'deviceSpecific' ? 'warn' : 'info'}>{cat.portability}</Badge>
|
|
150
|
+
)}
|
|
151
|
+
{cat.sensitive === true && <Badge kind="warn">secret</Badge>}
|
|
152
|
+
</span>
|
|
153
|
+
}
|
|
154
|
+
/>
|
|
155
|
+
))}
|
|
156
|
+
</div>
|
|
157
|
+
</Card>
|
|
158
|
+
)
|
|
159
|
+
})}
|
|
160
|
+
</div>
|
|
161
|
+
)}
|
|
162
|
+
|
|
163
|
+
{/* 设备相关分区警告 */}
|
|
164
|
+
{customWarnings.length > 0 && (
|
|
165
|
+
<Banner kind="warn">
|
|
166
|
+
{t('export.selectionWarnings')}
|
|
167
|
+
<ul className={css.warnList}>
|
|
168
|
+
{customWarnings.map((w, i) => <li key={i}>{w}</li>)}
|
|
169
|
+
</ul>
|
|
170
|
+
</Banner>
|
|
171
|
+
)}
|
|
172
|
+
|
|
173
|
+
{/* 加密备份选项 */}
|
|
174
|
+
<Card className={css.optionsCard}>
|
|
175
|
+
<Checkbox
|
|
176
|
+
checked={includeSecrets}
|
|
177
|
+
onChange={setIncludeSecrets}
|
|
178
|
+
label={<span className={css.categoryName}>{t('export.includeSecrets')}</span>}
|
|
179
|
+
/>
|
|
180
|
+
<div className={css.hint}>{t('export.includeSecretsHint')}</div>
|
|
181
|
+
{includeSecrets && (
|
|
182
|
+
<div className={css.secretFields}>
|
|
183
|
+
<label className={css.field}>
|
|
184
|
+
<span className={css.fieldLabel}>{t('export.password')}</span>
|
|
185
|
+
<input type="password" className={css.input} value={password} onChange={(e: ChangeEvent<HTMLInputElement>) => { setPassword(e.target.value) }} />
|
|
186
|
+
</label>
|
|
187
|
+
<label className={css.field}>
|
|
188
|
+
<span className={css.fieldLabel}>{t('export.passwordConfirm')}</span>
|
|
189
|
+
<input type="password" className={css.input} value={passwordConfirm} onChange={(e: ChangeEvent<HTMLInputElement>) => { setPasswordConfirm(e.target.value) }} />
|
|
190
|
+
</label>
|
|
191
|
+
{password !== '' && password !== passwordConfirm && (
|
|
192
|
+
<span className={css.formError}>{t('export.passwordMismatch')}</span>
|
|
193
|
+
)}
|
|
194
|
+
{password === '' && includeSecrets && (
|
|
195
|
+
<span className={css.formError}>{t('export.passwordRequired')}</span>
|
|
196
|
+
)}
|
|
197
|
+
</div>
|
|
198
|
+
)}
|
|
199
|
+
</Card>
|
|
200
|
+
|
|
201
|
+
{/* 执行 */}
|
|
202
|
+
<div className={css.actionRow}>
|
|
203
|
+
<Button variant="primary" disabled={running || passwordInvalid} onClick={() => { void runExport() }}>
|
|
204
|
+
{running ? <Spinner label={t('export.running')} /> : t('export.run')}
|
|
205
|
+
</Button>
|
|
206
|
+
</div>
|
|
207
|
+
|
|
208
|
+
{running && <ProgressBar event={progress} active />}
|
|
209
|
+
|
|
210
|
+
{error !== null && (
|
|
211
|
+
<ErrorBanner error={error} onRetry={() => { void runExport() }} retrying={running} />
|
|
212
|
+
)}
|
|
213
|
+
|
|
214
|
+
{result !== null && !running && (
|
|
215
|
+
<>
|
|
216
|
+
<ReportView kind="export" exportReport={result.report} onDownload={() => { void download() }} />
|
|
217
|
+
{downloaded && <Banner kind="ok">Saved: {result.report.file.name}</Banner>}
|
|
218
|
+
</>
|
|
219
|
+
)}
|
|
220
|
+
</div>
|
|
221
|
+
)
|
|
222
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 冲突决策列表(规范 §11,绑 src/ui/conflict-view.ts 的 ConflictCollector)。
|
|
3
|
+
*
|
|
4
|
+
* 渲染 plan 中 kind === 'Conflict' 的项,用户逐项选择
|
|
5
|
+
* Keep Current / Use Imported / Review;决策实时写入 collector,
|
|
6
|
+
* 完成时父组件调用 collector.toResolutions() → wizard.setResolutions()。
|
|
7
|
+
*/
|
|
8
|
+
import { useState } from 'react'
|
|
9
|
+
import { ConflictCollector } from '../../ui/conflict-view.ts'
|
|
10
|
+
import type { ItemResolution } from '../../core/types.ts'
|
|
11
|
+
import type { TranslateNS } from '../client-types.ts'
|
|
12
|
+
import { Banner } from '../common/ui.tsx'
|
|
13
|
+
import css from '../config-manager.module.css'
|
|
14
|
+
|
|
15
|
+
export interface ConflictListProps {
|
|
16
|
+
collector: ConflictCollector
|
|
17
|
+
t: TranslateNS<'config-manager'>
|
|
18
|
+
/** 任意决策变化后通知父组件刷新(tick) */
|
|
19
|
+
onChanged: () => void
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const RESOLUTION_OPTIONS: { value: ItemResolution; key: string }[] = [
|
|
23
|
+
{ value: 'keepCurrent', key: 'import.conflicts.keepCurrent' },
|
|
24
|
+
{ value: 'useImported', key: 'import.conflicts.useImported' },
|
|
25
|
+
{ value: 'review', key: 'import.conflicts.review' },
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
/** 冲突项决策列表 */
|
|
29
|
+
export function ConflictList({ collector, t, onChanged }: ConflictListProps) {
|
|
30
|
+
const [tick, setTick] = useState(0)
|
|
31
|
+
const items = collector.viewItems()
|
|
32
|
+
const unresolved = collector.unresolved().length
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<div className={css.conflictList}>
|
|
36
|
+
{unresolved > 0 && <Banner kind="warn">{t('import.conflicts.unresolved', { count: String(unresolved) })}</Banner>}
|
|
37
|
+
{items.map((view) => {
|
|
38
|
+
const item = view.item
|
|
39
|
+
return (
|
|
40
|
+
<div key={item.id} className={css.conflictItem}>
|
|
41
|
+
<div className={css.conflictHead}>
|
|
42
|
+
<span className={css.conflictId}>{item.adapter}: {item.description}</span>
|
|
43
|
+
{item.severity === 'error' && <span className={css.severityError}>error</span>}
|
|
44
|
+
</div>
|
|
45
|
+
{item.detail !== undefined && item.detail !== '' && (
|
|
46
|
+
<pre className={css.conflictDetail}>{item.detail}</pre>
|
|
47
|
+
)}
|
|
48
|
+
<div className={css.conflictOptions}>
|
|
49
|
+
{RESOLUTION_OPTIONS.map((opt) => (
|
|
50
|
+
<label key={opt.value} className={css.radioLabel}>
|
|
51
|
+
<input
|
|
52
|
+
type="radio"
|
|
53
|
+
name={`conflict-${item.id}`}
|
|
54
|
+
checked={view.resolution === opt.value}
|
|
55
|
+
onChange={() => {
|
|
56
|
+
collector.resolve(item.id, opt.value)
|
|
57
|
+
setTick((v) => v + 1)
|
|
58
|
+
onChanged()
|
|
59
|
+
}}
|
|
60
|
+
/>
|
|
61
|
+
{t(opt.key as 'import.conflicts.keepCurrent' | 'import.conflicts.useImported' | 'import.conflicts.review')}
|
|
62
|
+
</label>
|
|
63
|
+
))}
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
)
|
|
67
|
+
})}
|
|
68
|
+
{items.length === 0 && <div className={css.empty}>No conflicts</div>}
|
|
69
|
+
{void tick}
|
|
70
|
+
</div>
|
|
71
|
+
)
|
|
72
|
+
}
|