dsh-config-manager 0.1.39 → 0.1.41
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 +90 -8
- package/lib/client.js +1904 -1086
- package/lib/client.js.map +1 -1
- package/lib/core/cache-cleaner.d.ts +39 -0
- package/lib/core/cache-cleaner.js +164 -0
- package/lib/core/cache-cleaner.js.map +1 -0
- package/lib/index.js +172 -3
- package/lib/index.js.map +1 -1
- package/lib/market/github-repos.d.ts +19 -8
- package/lib/market/github-repos.js +62 -33
- package/lib/market/github-repos.js.map +1 -1
- package/lib/market/my-repo.d.ts +111 -3
- package/lib/market/my-repo.js +309 -26
- package/lib/market/my-repo.js.map +1 -1
- package/lib/market/prepare.js +3 -2
- package/lib/market/prepare.js.map +1 -1
- package/lib/market/types.d.ts +2 -0
- package/lib/security/secret-scanner.d.ts +18 -3
- package/lib/security/secret-scanner.js +144 -20
- package/lib/security/secret-scanner.js.map +1 -1
- package/package.json +1 -1
- package/src/client/common/ConfirmDialog.tsx +135 -0
- package/src/client/config-manager.module.css +101 -0
- package/src/client/market/MarketPanel.tsx +138 -45
- package/src/client/market/MyConfigsView.tsx +515 -132
- package/src/client/market/disclaimer.test.ts +57 -0
- package/src/client/market/disclaimer.ts +43 -0
- package/src/client/market/market-api.test.ts +3 -0
- package/src/client/market/market-locales.ts +37 -13
- package/src/client/market/my-configs-api.ts +20 -2
- package/src/client/market/my-configs-view.test.ts +137 -2
- package/src/client/market/my-configs-view.ts +174 -2
- package/src/client/run-store.test.ts +40 -2
- package/src/client/run-store.ts +70 -10
- package/src/client/sync/SyncConfirmView.tsx +28 -22
- package/src/client/sync/SyncSettingsView.tsx +175 -65
- package/src/client/sync/sync-api.ts +19 -0
- package/src/client/sync/sync-locales.ts +14 -2
- package/src/core/cache-cleaner.test.ts +333 -0
- package/src/core/cache-cleaner.ts +196 -0
- package/src/index.ts +165 -5
- package/src/market/github-repos.test.ts +99 -8
- package/src/market/github-repos.ts +67 -35
- package/src/market/my-repo.test.ts +218 -27
- package/src/market/my-repo.ts +403 -30
- package/src/market/prepare.test.ts +71 -1
- package/src/market/prepare.ts +3 -2
- package/src/market/types.ts +2 -0
- package/src/security/secret-scanner.ts +139 -22
- package/src/security/security.test.ts +111 -2
|
@@ -25,25 +25,29 @@ import { useEffect, useRef, useState } from 'react'
|
|
|
25
25
|
import type { ChangeEvent, ReactNode } from 'react'
|
|
26
26
|
import type { TranslateNS } from '../client-types.ts'
|
|
27
27
|
import type { ConfigManagerApi } from '../api.ts'
|
|
28
|
-
import type { ImportResult } from '../../core/types.ts'
|
|
29
|
-
import type { MarketDownloadResult } from '../../market/types.ts'
|
|
30
28
|
import { MARKET_UPSTREAM_OWNER, MARKET_UPSTREAM_REPO } from '../../market/github-repos.ts'
|
|
31
29
|
import type { MarketApi } from './market-api.ts'
|
|
32
|
-
import type { MyConfigsApi, MyItemEntry
|
|
30
|
+
import type { MyConfigsApi, MyItemEntry } from './my-configs-api.ts'
|
|
31
|
+
import type { ListingStatusResponse } from '../../market/my-repo.ts'
|
|
33
32
|
import type { SyncApi, GithubPollResponse } from '../sync/sync-api.ts'
|
|
34
33
|
import { Badge, Banner, Button, Card, Checkbox, Empty, Field, SectionTitle, Spinner } from '../common/ui.tsx'
|
|
34
|
+
import { ConfirmDialog } from '../common/ConfirmDialog.tsx'
|
|
35
35
|
import { redact } from '../../security/redaction.ts'
|
|
36
36
|
import { computeGithubLoginView, githubPollMessage } from '../sync/sync-view.ts'
|
|
37
37
|
import type { GithubLoginPhase } from '../sync/sync-view.ts'
|
|
38
|
+
import { readDisclaimerDismissed, writeDisclaimerDismissed } from './disclaimer.ts'
|
|
39
|
+
import type { DisclaimerKey } from './disclaimer.ts'
|
|
38
40
|
import {
|
|
39
|
-
autoFieldBadges, deriveLoginState,
|
|
40
|
-
myConfigFormValid, parseCategories,
|
|
41
|
+
autoFieldBadges, deriveLoginState, initialWizard, itemStatusFromHost,
|
|
42
|
+
myConfigFormValid, parseCategories, restoreMyInstall, restoreMyWizard, summarizeMyItems,
|
|
43
|
+
toMyInstallSlice, toMyItemView, toMyWizardSlice, validateMyConfigForm,
|
|
44
|
+
} from './my-configs-view.ts'
|
|
45
|
+
import type {
|
|
46
|
+
LoginView, MyConfigForm, MeStatusData, MyInstallSlice, MyInstallState, MyWizardSlice, MyWizardState,
|
|
41
47
|
} from './my-configs-view.ts'
|
|
42
|
-
import type { LoginView, MyConfigForm, MyConfigFormErrors, MeStatusData } from './my-configs-view.ts'
|
|
43
48
|
import {
|
|
44
49
|
approvalRows, approvedAdapterSummary, buildApprovedPlan, defaultApprovals, marketDetailView,
|
|
45
50
|
} from './market-view.ts'
|
|
46
|
-
import type { MarketApprovals } from './market-view.ts'
|
|
47
51
|
import css from '../config-manager.module.css'
|
|
48
52
|
|
|
49
53
|
export interface MyConfigsViewProps {
|
|
@@ -62,8 +66,18 @@ export interface MyConfigsViewProps {
|
|
|
62
66
|
myItemsError: string | null
|
|
63
67
|
/** 列表状态上抛(MarketPanel 统一 commit/patch 镜像 runStore,切 tab 不丢) */
|
|
64
68
|
onMyItemsChange: (items: MyItemEntry[] | null, error: string | null) => void
|
|
65
|
-
/**
|
|
66
|
-
|
|
69
|
+
/** 上传/更新向导持久化切片(受控:MarketPanel 经 runStore 持有;null = 未开始) */
|
|
70
|
+
myWizard: MyWizardSlice | null
|
|
71
|
+
/** 向导切片上抛(MarketPanel 统一 commit/patch 镜像 runStore,切 tab/刷新不丢) */
|
|
72
|
+
onMyWizardChange: (wizard: MyWizardSlice | null) => void
|
|
73
|
+
/** 装回本地(下载+逐分区批准+导入结果)持久化切片(受控:MarketPanel 经 runStore 持有;null = 未开始/已关闭) */
|
|
74
|
+
myInstall: MyInstallSlice | null
|
|
75
|
+
/** 装回本地切片上抛(MarketPanel 统一 commit/patch 镜像 runStore,切 tab/刷新不丢) */
|
|
76
|
+
onMyInstallChange: (install: MyInstallSlice | null) => void
|
|
77
|
+
/** 删除确认弹窗目标条目 id(受控:MarketPanel 经 runStore 持有;null = 无确认中的删除) */
|
|
78
|
+
myConfirmDeleteId: string | null
|
|
79
|
+
/** 删除确认态上抛(MarketPanel 统一 commit/patch 镜像 runStore,切 tab/刷新不丢) */
|
|
80
|
+
onMyConfirmDeleteChange: (id: string | null) => void
|
|
67
81
|
}
|
|
68
82
|
|
|
69
83
|
/* -------------------------------- GitHub device flow 状态(仅内存,token 只存宿主) */
|
|
@@ -81,50 +95,18 @@ const initialGithubFlow: GithubFlowState = {
|
|
|
81
95
|
phase: 'idle', flowId: '', userCode: '', verificationUri: '', interval: 5, error: null,
|
|
82
96
|
}
|
|
83
97
|
|
|
84
|
-
/**
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
validated: boolean
|
|
92
|
-
validationError: string | null
|
|
93
|
-
form: MyConfigForm
|
|
94
|
-
formErrors: MyConfigFormErrors
|
|
95
|
-
running: boolean
|
|
96
|
-
result: MyUploadResult | null
|
|
97
|
-
error: string | null
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function initialWizard(mode: 'upload' | 'update'): WizardState {
|
|
101
|
-
return {
|
|
102
|
-
mode,
|
|
103
|
-
step: 'select',
|
|
104
|
-
zipPath: null,
|
|
105
|
-
fileName: null,
|
|
106
|
-
validating: false,
|
|
107
|
-
validated: false,
|
|
108
|
-
validationError: null,
|
|
109
|
-
form: { ...EMPTY_MY_CONFIG_FORM },
|
|
110
|
-
formErrors: { name: null },
|
|
111
|
-
running: false,
|
|
112
|
-
result: null,
|
|
113
|
-
error: null,
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/** 装回本地(下载 + 逐分区批准 + 执行导入)状态 */
|
|
118
|
-
interface InstallState {
|
|
119
|
-
itemId: string
|
|
120
|
-
detail: MarketDownloadResult | null
|
|
121
|
-
approvals: MarketApprovals
|
|
122
|
-
importing: boolean
|
|
123
|
-
importResult: ImportResult | null
|
|
124
|
-
error: string | null
|
|
125
|
-
}
|
|
98
|
+
/**
|
|
99
|
+
* 上传/更新向导状态(全量模型在 my-configs-view.ts 的 MyWizardState;本组件持有的是
|
|
100
|
+
* 持久化切片 myWizard(受控 props),经 restoreMyWizard 恢复全量、toMyWizardSlice 上抛镜像。
|
|
101
|
+
* 瞬态(validating/running/formErrors)由 restore 重建,切 tab/刷新恢复后为初始态。
|
|
102
|
+
* 装回本地状态(MyInstallState)同模式:持久化切片 myInstall(受控 props),
|
|
103
|
+
* 经 restoreMyInstall 恢复全量(importing 瞬态归零)、toMyInstallSlice 上抛镜像。
|
|
104
|
+
*/
|
|
126
105
|
|
|
127
|
-
export function MyConfigsView({
|
|
106
|
+
export function MyConfigsView({
|
|
107
|
+
meApi, api, importApi, syncApi, t, myItems, myItemsError, onMyItemsChange, myWizard, onMyWizardChange,
|
|
108
|
+
myInstall, onMyInstallChange, myConfirmDeleteId, onMyConfirmDeleteChange,
|
|
109
|
+
}: MyConfigsViewProps) {
|
|
128
110
|
const uiT = meApi.t // 展示层翻译器(myConfigs.* 键经 UiT;同 MarketPanel 用 api.t)
|
|
129
111
|
|
|
130
112
|
/* ---------------- 登录状态(/me/status;statusFailed=401 → token 失效) ---------------- */
|
|
@@ -138,11 +120,140 @@ export function MyConfigsView({ meApi, api, importApi, syncApi, t, myItems, myIt
|
|
|
138
120
|
/* ---------------- 已上传列表(受控:MarketPanel 持有并镜像 runStore market.myItems) ---------------- */
|
|
139
121
|
const [listLoading, setListLoading] = useState(false)
|
|
140
122
|
|
|
141
|
-
/* ----------------
|
|
142
|
-
const [wizard, setWizard] = useState<
|
|
143
|
-
|
|
123
|
+
/* ---------------- 向导(受控:切片来自 runStore;瞬态本地重建)/ 装回本地 / 删除 ---------------- */
|
|
124
|
+
const [wizard, setWizard] = useState<MyWizardState>(() => restoreMyWizard(myWizard))
|
|
125
|
+
/** 最近一次 wizard 全量(commitWizard 读最新值,避免闭包过期) */
|
|
126
|
+
const wizardRef = useRef<MyWizardState>(wizard)
|
|
127
|
+
/** 收录/下架任务状态(结果卡轮询 /me/listing 的实时结果) */
|
|
128
|
+
const [listingStatus, setListingStatus] = useState<ListingStatusResponse | null>(null)
|
|
129
|
+
/** 收录/下架任务轮询定时器 */
|
|
130
|
+
const listingPollTimer = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
131
|
+
/** 删除确认弹窗目标条目 id(受控:MarketPanel 经 runStore 持有,切 tab/刷新不丢;null = 无确认中的删除) */
|
|
132
|
+
const confirmDeleteId = myConfirmDeleteId
|
|
133
|
+
/** 正在删除的条目 id(行级 spinner + 防重复点击) */
|
|
134
|
+
const [deletingId, setDeletingId] = useState<string | null>(null)
|
|
135
|
+
/** 装回本地状态(受控:切片来自 runStore;瞬态 importing 本地重建) */
|
|
136
|
+
const [install, setInstall] = useState<MyInstallState | null>(() => restoreMyInstall(myInstall))
|
|
137
|
+
/** 最近一次 install 全量(commitInstall 读最新值,避免闭包过期) */
|
|
138
|
+
const installRef = useRef<MyInstallState | null>(install)
|
|
144
139
|
const fileInput = useRef<HTMLInputElement>(null)
|
|
145
140
|
|
|
141
|
+
/* ---------------- 弹窗交互(2026-08-21:上传/装回本地搬进弹窗 + 免责前置) ---------------- */
|
|
142
|
+
/** 上传/更新向导弹窗开关(瞬态 UI,不持久化:切 tab 弹窗关闭,数据仍在 runStore) */
|
|
143
|
+
const [uploadOpen, setUploadOpen] = useState(false)
|
|
144
|
+
/** 装回本地弹窗开关(同上) */
|
|
145
|
+
const [installOpen, setInstallOpen] = useState(false)
|
|
146
|
+
/** 当前展示的免责弹窗操作(null = 无;upload/download/install 三操作分开记「不再提示」) */
|
|
147
|
+
const [disclaimerKey, setDisclaimerKey] = useState<DisclaimerKey | null>(null)
|
|
148
|
+
/** 免责弹窗「不再提示」勾选(每次打开重置) */
|
|
149
|
+
const [dontAsk, setDontAsk] = useState(false)
|
|
150
|
+
/** localStorage(浏览器环境;免责「不再提示」跨会话持久化) */
|
|
151
|
+
const storage: Pick<Storage, 'getItem' | 'setItem'> = window.localStorage
|
|
152
|
+
|
|
153
|
+
/** 打开上传/更新弹窗(更新模式表单已预填):未勾「不再提示」→ 先弹免责,确认后开弹窗 */
|
|
154
|
+
const openUpload = (): void => {
|
|
155
|
+
if (readDisclaimerDismissed('upload', storage)) {
|
|
156
|
+
setUploadOpen(true)
|
|
157
|
+
return
|
|
158
|
+
}
|
|
159
|
+
setDontAsk(false)
|
|
160
|
+
setDisclaimerKey('upload')
|
|
161
|
+
}
|
|
162
|
+
/** 待装回本地的目标条目(免责确认后取用;避免免责流程中闭包过期) */
|
|
163
|
+
const pendingInstallEntry = useRef<MyItemEntry | null>(null)
|
|
164
|
+
/** 打开装回本地弹窗:未勾「不再提示」→ 先弹免责,确认后开弹窗并启动下载 */
|
|
165
|
+
const openInstall = (entry: MyItemEntry): void => {
|
|
166
|
+
pendingInstallEntry.current = entry
|
|
167
|
+
if (readDisclaimerDismissed('install', storage)) {
|
|
168
|
+
setInstallOpen(true)
|
|
169
|
+
void runDownload(entry)
|
|
170
|
+
return
|
|
171
|
+
}
|
|
172
|
+
setDontAsk(false)
|
|
173
|
+
setDisclaimerKey('install')
|
|
174
|
+
}
|
|
175
|
+
/** 关闭上传/更新弹窗:重置向导为初始态(弹窗即会话,关闭即放弃本次操作;
|
|
176
|
+
* 更新模式也切回「一键上传」入口,避免入口按钮残留 update 态) */
|
|
177
|
+
const closeUpload = (): void => {
|
|
178
|
+
setUploadOpen(false)
|
|
179
|
+
commitWizard(initialWizard('upload'))
|
|
180
|
+
setListingStatus(null)
|
|
181
|
+
}
|
|
182
|
+
/** 取消更新:放弃本次更新,向导回到默认「一键上传」初始态(清空预填/暂存/校验态),弹窗保持打开 */
|
|
183
|
+
const cancelUpdate = (): void => {
|
|
184
|
+
commitWizard(initialWizard('upload'))
|
|
185
|
+
setListingStatus(null)
|
|
186
|
+
}
|
|
187
|
+
/** 关闭装回本地弹窗:清 install 会话 */
|
|
188
|
+
const closeInstall = (): void => {
|
|
189
|
+
setInstallOpen(false)
|
|
190
|
+
commitInstall(null)
|
|
191
|
+
}
|
|
192
|
+
/** 免责弹窗确认:勾选则记录「不再提示」→ 关闭免责 → 打开对应操作弹窗 */
|
|
193
|
+
const confirmDisclaimer = (): void => {
|
|
194
|
+
const key = disclaimerKey
|
|
195
|
+
if (key === null) return
|
|
196
|
+
if (dontAsk) writeDisclaimerDismissed(key, storage)
|
|
197
|
+
setDisclaimerKey(null)
|
|
198
|
+
if (key === 'upload') {
|
|
199
|
+
setUploadOpen(true)
|
|
200
|
+
} else if (key === 'install') {
|
|
201
|
+
const entry = pendingInstallEntry.current
|
|
202
|
+
setInstallOpen(true)
|
|
203
|
+
if (entry !== null && entry !== undefined) void runDownload(entry)
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
/** 取消免责弹窗:关闭,不打开操作弹窗;若向导处于 update 残留态则重置为上传初始态 */
|
|
207
|
+
const cancelDisclaimer = (): void => {
|
|
208
|
+
setDisclaimerKey(null)
|
|
209
|
+
if (disclaimerKey === 'upload' && wizardRef.current.mode === 'update') {
|
|
210
|
+
commitWizard(initialWizard('upload'))
|
|
211
|
+
setListingStatus(null)
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
/** 免责弹窗文案(MyConfigsView 只触发 upload / install 两种;default 兜底返回空串防误显) */
|
|
215
|
+
const disclaimerText = (): string => {
|
|
216
|
+
switch (disclaimerKey) {
|
|
217
|
+
case 'upload': return t('disclaimer.upload.text')
|
|
218
|
+
case 'install': return t('disclaimer.install.text')
|
|
219
|
+
default: return ''
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* 向导状态统一提交:更新 ref → setState → **总是**镜像切片上抛(MarketPanel 落 runStore)。
|
|
225
|
+
* 镜像不依赖 effect flush:异步回调在组件已卸载(切走 tab)时也能落库,切回恢复。
|
|
226
|
+
* 镜像上抛包 try/catch:镜像失败(store 异常)绝不影响本地 UI 更新(防「点击无反应」)。
|
|
227
|
+
*/
|
|
228
|
+
const commitWizard = (next: MyWizardState): void => {
|
|
229
|
+
wizardRef.current = next
|
|
230
|
+
setWizard(next)
|
|
231
|
+
try {
|
|
232
|
+
onMyWizardChange(toMyWizardSlice(next))
|
|
233
|
+
} catch {
|
|
234
|
+
// 镜像失败不影响本地状态:下轮 commit 会再尝试
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
const patchWizard = (p: Partial<MyWizardState>): void => commitWizard({ ...wizardRef.current, ...p })
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* 装回本地状态统一提交:更新 ref → setState → **总是**镜像切片上抛(MarketPanel 落 runStore)。
|
|
241
|
+
* 镜像不依赖 effect flush:异步回调在组件已卸载(切走 tab)时也能落库,切回恢复。
|
|
242
|
+
*/
|
|
243
|
+
const commitInstall = (next: MyInstallState | null): void => {
|
|
244
|
+
installRef.current = next
|
|
245
|
+
setInstall(next)
|
|
246
|
+
try {
|
|
247
|
+
onMyInstallChange(next === null ? null : toMyInstallSlice(next))
|
|
248
|
+
} catch {
|
|
249
|
+
// 镜像失败不影响本地状态
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
const patchInstall = (p: Partial<MyInstallState>): void => {
|
|
253
|
+
if (installRef.current === null) return
|
|
254
|
+
commitInstall({ ...installRef.current, ...p })
|
|
255
|
+
}
|
|
256
|
+
|
|
146
257
|
/** 读取登录态(挂载 / 登录成功 / 状态刷新),返回 status 供后续判断 */
|
|
147
258
|
const loadStatus = async (): Promise<MeStatusData | null> => {
|
|
148
259
|
setStatusLoading(true)
|
|
@@ -249,100 +360,211 @@ export function MyConfigsView({ meApi, api, importApi, syncApi, t, myItems, myIt
|
|
|
249
360
|
|
|
250
361
|
/* ------------------------------------------------ 上传 / 更新向导 */
|
|
251
362
|
|
|
252
|
-
/**
|
|
363
|
+
/** 选 zip → upload 受控临时区;upload 模式预填 name 为 zip 文件名(可改);
|
|
364
|
+
* update 模式(更新直达表单页)留在 form 步骤并自动跑校验 */
|
|
253
365
|
const onPickFile = async (file: File | undefined): Promise<void> => {
|
|
254
366
|
if (file === undefined) return
|
|
255
|
-
|
|
367
|
+
patchWizard({ fileName: file.name, error: null, validationError: null })
|
|
256
368
|
try {
|
|
257
369
|
const uploaded = await importApi.upload(file)
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
370
|
+
const base = { zipPath: uploaded.zipPath, fileName: file.name }
|
|
371
|
+
if (wizardRef.current.mode === 'update') {
|
|
372
|
+
// 更新模式:留在表单页,选完新 zip 自动校验(校验通过才可一键更新)
|
|
373
|
+
patchWizard({ ...base, validated: false })
|
|
374
|
+
await runValidateWith(uploaded.zipPath)
|
|
375
|
+
} else {
|
|
376
|
+
patchWizard({
|
|
377
|
+
...base,
|
|
378
|
+
step: 'validate',
|
|
379
|
+
/* 预填 zip 文件名(去 .zip 后缀,可改) */
|
|
380
|
+
form: { ...wizardRef.current.form, name: file.name.replace(/\.zip$/i, '') },
|
|
381
|
+
})
|
|
382
|
+
}
|
|
267
383
|
} catch (err) {
|
|
268
|
-
|
|
384
|
+
patchWizard({ error: err instanceof Error ? err.message : String(err) })
|
|
269
385
|
}
|
|
270
386
|
}
|
|
271
387
|
|
|
272
388
|
/** 步骤 2:analyzeImport dry-run(零写入)—— 无密钥 + 内容合法才放行;通过后进表单 */
|
|
273
389
|
const runValidate = async (): Promise<void> => {
|
|
274
390
|
if (wizard.zipPath === null) return
|
|
275
|
-
|
|
391
|
+
await runValidateWith(wizard.zipPath)
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/** 校验指定 zipPath(update 模式选新 zip 后自动调用;任何异常都落到 validationError 展示,不静默) */
|
|
395
|
+
const runValidateWith = async (zipPath: string): Promise<void> => {
|
|
276
396
|
try {
|
|
277
|
-
|
|
397
|
+
patchWizard({ validating: true, validationError: null })
|
|
398
|
+
} catch (err) {
|
|
399
|
+
// 进入校验态失败(极端情况):仍展示错误而不是无反应
|
|
400
|
+
setWizard((w) => ({ ...w, validationError: err instanceof Error ? err.message : String(err) }))
|
|
401
|
+
return
|
|
402
|
+
}
|
|
403
|
+
try {
|
|
404
|
+
const analysis = await importApi.analyzeImport(zipPath)
|
|
278
405
|
if (analysis.secretCount > 0) {
|
|
279
|
-
|
|
406
|
+
patchWizard({ validating: false, validated: false, validationError: t('myconfigs.upload.validateSecrets') })
|
|
280
407
|
} else if (!analysis.valid) {
|
|
281
|
-
|
|
408
|
+
patchWizard({ validating: false, validated: false, validationError: t('myconfigs.upload.validateInvalid') })
|
|
282
409
|
} else {
|
|
283
|
-
|
|
410
|
+
// 校验通过 → 自动进入表单步骤(upload 模式从 validate 进 form;update 模式本就是 form,值相同无害)
|
|
411
|
+
patchWizard({ validating: false, validated: true, validationError: null, step: 'form' })
|
|
284
412
|
}
|
|
285
413
|
} catch (err) {
|
|
286
|
-
|
|
287
|
-
|
|
414
|
+
patchWizard({
|
|
415
|
+
validating: false, validated: false,
|
|
288
416
|
validationError: err instanceof Error ? err.message : String(err),
|
|
289
|
-
})
|
|
417
|
+
})
|
|
290
418
|
}
|
|
291
419
|
}
|
|
292
420
|
|
|
293
421
|
/** 表单字段更新 + 实时校验(pure 模型) */
|
|
294
422
|
const onFormField = (field: keyof MyConfigForm, value: string): void => {
|
|
295
|
-
const next = { ...
|
|
296
|
-
|
|
423
|
+
const next = { ...wizardRef.current.form, [field]: value }
|
|
424
|
+
patchWizard({ form: next, formErrors: validateMyConfigForm(next, uiT) })
|
|
297
425
|
}
|
|
298
426
|
|
|
299
427
|
/** 「一键上传 / 一键更新」→ meUpload / meUpdate */
|
|
300
428
|
const runUpload = async (): Promise<void> => {
|
|
301
|
-
|
|
302
|
-
const
|
|
303
|
-
|
|
429
|
+
const w = wizardRef.current
|
|
430
|
+
const zipPath = w.zipPath
|
|
431
|
+
if (zipPath === null) return
|
|
432
|
+
const errs = validateMyConfigForm(w.form, uiT)
|
|
433
|
+
patchWizard({ formErrors: errs })
|
|
304
434
|
if (!myConfigFormValid(errs)) return
|
|
305
|
-
const categories = parseCategories(
|
|
435
|
+
const categories = parseCategories(w.form.categories)
|
|
306
436
|
const form = {
|
|
307
|
-
name:
|
|
308
|
-
|
|
437
|
+
name: w.form.name.trim(),
|
|
438
|
+
// update 模式携带显式条目 id(后端按 id 更新,避免 name→slug 猜测失配)
|
|
439
|
+
...(w.mode === 'update' && w.form.id !== '' ? { id: w.form.id } : {}),
|
|
440
|
+
...(w.form.description.trim() !== '' ? { description: w.form.description.trim() } : {}),
|
|
309
441
|
...(categories.length > 0 ? { categories } : {}),
|
|
310
442
|
}
|
|
311
|
-
|
|
443
|
+
patchWizard({ running: true, error: null, result: null })
|
|
312
444
|
try {
|
|
313
|
-
const result =
|
|
314
|
-
? await meApi.meUpdate({ zipPath
|
|
315
|
-
: await meApi.meUpload({ zipPath
|
|
316
|
-
|
|
317
|
-
//
|
|
445
|
+
const result = w.mode === 'update'
|
|
446
|
+
? await meApi.meUpdate({ zipPath, form })
|
|
447
|
+
: await meApi.meUpload({ zipPath, form })
|
|
448
|
+
commitWizard({ ...wizardRef.current, running: false, result })
|
|
449
|
+
// 上传/更新成功后:清空旧收录状态 + 若收录后台进行中则轮询状态 + 刷新列表
|
|
450
|
+
setListingStatus(null)
|
|
451
|
+
if (result.ok && result.listing === 'pending') startListingPoll(result.itemId)
|
|
318
452
|
void loadItems({ silent: true })
|
|
319
453
|
} catch (err) {
|
|
320
|
-
|
|
454
|
+
patchWizard({ running: false, error: err instanceof Error ? err.message : String(err) })
|
|
321
455
|
}
|
|
322
456
|
}
|
|
323
457
|
|
|
324
|
-
/** 行操作:更新 →
|
|
458
|
+
/** 行操作:更新 → 打开「更新配置」表单页(step='form',预填条目信息;表单内选新 zip 自动校验) */
|
|
325
459
|
const startUpdate = (entry: MyItemEntry): void => {
|
|
326
|
-
|
|
460
|
+
commitWizard({
|
|
327
461
|
...initialWizard('update'),
|
|
462
|
+
step: 'form',
|
|
328
463
|
form: {
|
|
464
|
+
id: entry.id,
|
|
329
465
|
name: entry.name,
|
|
330
466
|
description: entry.description ?? '',
|
|
331
467
|
categories: (entry.categories ?? []).join(', '),
|
|
332
468
|
},
|
|
333
469
|
})
|
|
470
|
+
setListingStatus(null)
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/* ------------------------------------------------ 收录/下架任务状态轮询 */
|
|
474
|
+
|
|
475
|
+
/** 轮询 /me/listing 直到任务终态(done/failed/null);间隔 3s、最多 40 次(≈2 分钟),
|
|
476
|
+
* 后台 fork 更久时超时停止,用户可稍后手动刷新列表/点「重新收录」 */
|
|
477
|
+
const startListingPoll = (itemId: string): void => {
|
|
478
|
+
if (listingPollTimer.current !== null) clearTimeout(listingPollTimer.current)
|
|
479
|
+
let count = 0
|
|
480
|
+
const tick = (): void => {
|
|
481
|
+
void (async () => {
|
|
482
|
+
try {
|
|
483
|
+
const s = await meApi.meListing(itemId)
|
|
484
|
+
if (s === null) {
|
|
485
|
+
// 任务表未命中且实况也无(重启丢失/从未提交)→ 停止轮询,状态由列表徽章体现
|
|
486
|
+
setListingStatus({ itemId, listing: 'done', prNumber: null, prUrl: null })
|
|
487
|
+
return
|
|
488
|
+
}
|
|
489
|
+
setListingStatus(s)
|
|
490
|
+
if (s.listing !== 'pending') return // done/failed → 停止轮询
|
|
491
|
+
} catch {
|
|
492
|
+
// 轮询失败不打断:下一轮继续
|
|
493
|
+
}
|
|
494
|
+
count += 1
|
|
495
|
+
if (count >= 40) {
|
|
496
|
+
listingPollTimer.current = null
|
|
497
|
+
return
|
|
498
|
+
}
|
|
499
|
+
listingPollTimer.current = setTimeout(tick, 3000)
|
|
500
|
+
})()
|
|
501
|
+
}
|
|
502
|
+
tick()
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/** 挂载恢复:若持久化的向导结果仍是「收录处理中」(pending),继续轮询(仿 resume 模式,避免刷新后永久 pending 卡死) */
|
|
506
|
+
useEffect(() => {
|
|
507
|
+
const w = wizardRef.current
|
|
508
|
+
if (w.result !== null && w.result.ok && w.result.listing === 'pending') {
|
|
509
|
+
startListingPoll(w.result.itemId)
|
|
510
|
+
}
|
|
511
|
+
return () => {
|
|
512
|
+
if (listingPollTimer.current !== null) clearTimeout(listingPollTimer.current)
|
|
513
|
+
}
|
|
514
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
515
|
+
}, [])
|
|
516
|
+
|
|
517
|
+
/* ------------------------------------------------ 删除条目(行内两步确认) */
|
|
518
|
+
|
|
519
|
+
/** 重新提交收录(收录失败 / 进程重启丢失后的一键重试)→ 重新轮询状态 */
|
|
520
|
+
const runRelist = async (itemId: string): Promise<void> => {
|
|
521
|
+
try {
|
|
522
|
+
const s = await meApi.meRelist(itemId)
|
|
523
|
+
setListingStatus(s)
|
|
524
|
+
startListingPoll(itemId)
|
|
525
|
+
} catch (err) {
|
|
526
|
+
patchWizard({ error: err instanceof Error ? err.message : String(err) })
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/** 删除:调 /me/delete(同步删本地索引+文件;已收录自动后台提下架 PR)→ 刷新列表 */
|
|
531
|
+
const runDelete = async (entry: MyItemEntry): Promise<void> => {
|
|
532
|
+
if (deletingId !== null) return
|
|
533
|
+
setDeletingId(entry.id)
|
|
534
|
+
onMyConfirmDeleteChange(null)
|
|
535
|
+
try {
|
|
536
|
+
const result = await meApi.meDelete(entry.id)
|
|
537
|
+
if (result.ok) {
|
|
538
|
+
if (result.delisted) {
|
|
539
|
+
patchWizard({ error: t('myconfigs.delete.delistStarted') })
|
|
540
|
+
} else if (result.prNumber !== null) {
|
|
541
|
+
patchWizard({ error: t('myconfigs.delete.prClosed') })
|
|
542
|
+
}
|
|
543
|
+
void loadItems({ silent: true })
|
|
544
|
+
} else {
|
|
545
|
+
patchWizard({ error: result.error ?? t('common.unknownError') })
|
|
546
|
+
}
|
|
547
|
+
} catch (err) {
|
|
548
|
+
patchWizard({ error: err instanceof Error ? err.message : String(err) })
|
|
549
|
+
} finally {
|
|
550
|
+
setDeletingId(null)
|
|
551
|
+
}
|
|
334
552
|
}
|
|
335
553
|
|
|
336
554
|
/* ------------------------------------------------ 装回本地(复用市场下载 + 逐分区批准链路) */
|
|
337
555
|
|
|
338
556
|
/** 装回本地(复用市场下载 + 逐分区批准链路;条目内容在用户自己的公开仓库,必须带 repo 来源) */
|
|
339
557
|
const runDownload = async (entry: MyItemEntry): Promise<void> => {
|
|
340
|
-
|
|
558
|
+
commitInstall({ itemId: entry.id, detail: null, approvals: {}, importing: false, importResult: null, error: null })
|
|
341
559
|
try {
|
|
342
560
|
const detail = await api.download(entry.id, entry.repoUrl)
|
|
343
|
-
|
|
561
|
+
if (installRef.current !== null) {
|
|
562
|
+
commitInstall({ ...installRef.current, detail, approvals: defaultApprovals(detail.plan) })
|
|
563
|
+
}
|
|
344
564
|
} catch (err) {
|
|
345
|
-
|
|
565
|
+
if (installRef.current !== null) {
|
|
566
|
+
patchInstall({ error: err instanceof Error ? err.message : String(err) })
|
|
567
|
+
}
|
|
346
568
|
}
|
|
347
569
|
}
|
|
348
570
|
|
|
@@ -350,19 +572,19 @@ export function MyConfigsView({ meApi, api, importApi, syncApi, t, myItems, myIt
|
|
|
350
572
|
if (install === null || install.detail === null) return
|
|
351
573
|
const approvedPlan = buildApprovedPlan(install.detail.plan, install.approvals)
|
|
352
574
|
if (approvedPlan.items.length === 0) {
|
|
353
|
-
|
|
575
|
+
patchInstall({ error: t('detail.noApproval') })
|
|
354
576
|
return
|
|
355
577
|
}
|
|
356
|
-
|
|
578
|
+
patchInstall({ importing: true, error: null })
|
|
357
579
|
try {
|
|
358
580
|
const executed = await importApi.executeImportPlan(
|
|
359
581
|
install.detail.zipPath,
|
|
360
582
|
approvedPlan,
|
|
361
583
|
{ confirm: true, rollbackOnError: true },
|
|
362
584
|
)
|
|
363
|
-
|
|
585
|
+
patchInstall({ importing: false, importResult: executed })
|
|
364
586
|
} catch (err) {
|
|
365
|
-
|
|
587
|
+
patchInstall({ importing: false, error: err instanceof Error ? err.message : String(err) })
|
|
366
588
|
}
|
|
367
589
|
}
|
|
368
590
|
|
|
@@ -455,15 +677,57 @@ export function MyConfigsView({ meApi, api, importApi, syncApi, t, myItems, myIt
|
|
|
455
677
|
|
|
456
678
|
/** 上传 / 更新向导卡片(选 zip → 校验 → 表单 → 结果) */
|
|
457
679
|
const renderWizard = (): ReactNode => {
|
|
458
|
-
|
|
680
|
+
/** PR 链接(优先实时任务状态;收录完成后由轮询补上,或直接取同步结果) */
|
|
681
|
+
const prLink = ((): { url: string; label: string } | null => {
|
|
682
|
+
const live = listingStatus
|
|
683
|
+
const url = (live !== null && live.prUrl !== null && live.prUrl !== '')
|
|
684
|
+
? live.prUrl
|
|
685
|
+
: (wizardRef.current.result?.prUrl ?? null)
|
|
686
|
+
if (url === null || url === '') return null
|
|
687
|
+
const number = live !== null && live.prNumber !== null ? live.prNumber : wizardRef.current.result?.prNumber
|
|
688
|
+
return {
|
|
689
|
+
url,
|
|
690
|
+
label: number !== null && number !== undefined
|
|
691
|
+
? t('myconfigs.result.pr', { number: String(number) })
|
|
692
|
+
: t('myconfigs.result.openPr'),
|
|
693
|
+
}
|
|
694
|
+
})()
|
|
695
|
+
/** 重置:update 模式轻量重置(只清 zip/校验,**保留预填表单**);upload 模式完全重置 */
|
|
696
|
+
const reset = (): void => {
|
|
697
|
+
const w = wizardRef.current
|
|
698
|
+
if (w.mode === 'update') {
|
|
699
|
+
commitWizard({
|
|
700
|
+
...initialWizard('update'),
|
|
701
|
+
step: 'form',
|
|
702
|
+
form: { ...w.form },
|
|
703
|
+
})
|
|
704
|
+
} else {
|
|
705
|
+
commitWizard(initialWizard('upload'))
|
|
706
|
+
}
|
|
707
|
+
setListingStatus(null)
|
|
708
|
+
}
|
|
459
709
|
return (
|
|
460
|
-
<
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
710
|
+
<div
|
|
711
|
+
className={css.dialogMask}
|
|
712
|
+
onMouseDown={(e) => { if (e.target === e.currentTarget && !wizard.running && !wizard.validating) closeUpload() }}
|
|
713
|
+
>
|
|
714
|
+
<div className={`${css.dialogCard} ${css.dialogWide}`} role="dialog" aria-modal="true" aria-label={wizard.mode === 'update' ? t('myconfigs.update.title') : t('myconfigs.upload.title')}>
|
|
715
|
+
<div className={css.dialogHeaderRow}>
|
|
716
|
+
<span className={css.dialogHeader}>
|
|
717
|
+
{wizard.mode === 'update' ? t('myconfigs.update.title') : t('myconfigs.upload.title')}
|
|
718
|
+
{wizard.mode === 'update' && <Badge kind="info">{t('myconfigs.update.hint')}</Badge>}
|
|
719
|
+
</span>
|
|
720
|
+
<button
|
|
721
|
+
type="button"
|
|
722
|
+
className={css.dialogClose}
|
|
723
|
+
aria-label={t('common.close')}
|
|
724
|
+
disabled={wizard.running || wizard.validating}
|
|
725
|
+
onClick={closeUpload}
|
|
726
|
+
>
|
|
727
|
+
×
|
|
728
|
+
</button>
|
|
729
|
+
</div>
|
|
730
|
+
<div className={css.dialogBodyScroll}>
|
|
467
731
|
|
|
468
732
|
{/* 步骤 1:选配置包 */}
|
|
469
733
|
{wizard.step === 'select' && (
|
|
@@ -507,9 +771,37 @@ export function MyConfigsView({ meApi, api, importApi, syncApi, t, myItems, myIt
|
|
|
507
771
|
</div>
|
|
508
772
|
)}
|
|
509
773
|
|
|
510
|
-
{/* 步骤 3:精简表单(仅 name/description/categories;其余系统自动) →
|
|
774
|
+
{/* 步骤 3:精简表单(仅 name/description/categories;其余系统自动) → 上传/更新 */}
|
|
511
775
|
{wizard.step === 'form' && (
|
|
512
776
|
<div>
|
|
777
|
+
{/* update 模式:表单页内嵌「选择新 ZIP」入口(选中自动校验,通过后才可一键更新) */}
|
|
778
|
+
{wizard.mode === 'update' && (
|
|
779
|
+
<div>
|
|
780
|
+
<span className={css.hint}>{t('myconfigs.update.zipHint')}</span>
|
|
781
|
+
<input
|
|
782
|
+
ref={fileInput}
|
|
783
|
+
type="file"
|
|
784
|
+
accept=".zip,application/zip"
|
|
785
|
+
className={css.hiddenFile}
|
|
786
|
+
onChange={(e: ChangeEvent<HTMLInputElement>) => {
|
|
787
|
+
const picked = e.target.files?.[0]
|
|
788
|
+
e.target.value = ''
|
|
789
|
+
void onPickFile(picked)
|
|
790
|
+
}}
|
|
791
|
+
/>
|
|
792
|
+
<div className={css.actionRow}>
|
|
793
|
+
<Button variant="primary" disabled={wizard.validating || wizard.running} onClick={() => { fileInput.current?.click() }}>
|
|
794
|
+
{wizard.fileName !== null && wizard.zipPath !== null
|
|
795
|
+
? t('myconfigs.upload.selected', { name: wizard.fileName })
|
|
796
|
+
: t('myconfigs.update.selectZip')}
|
|
797
|
+
</Button>
|
|
798
|
+
{wizard.zipPath !== null && (
|
|
799
|
+
<Button disabled={wizard.validating || wizard.running} onClick={reset}>{t('myconfigs.upload.reselect')}</Button>
|
|
800
|
+
)}
|
|
801
|
+
</div>
|
|
802
|
+
{wizard.validationError !== null && <Banner kind="error">{redact(wizard.validationError)}</Banner>}
|
|
803
|
+
</div>
|
|
804
|
+
)}
|
|
513
805
|
{wizard.validated && (
|
|
514
806
|
<div className={css.statRow}>
|
|
515
807
|
<Badge kind="ok">{t('myconfigs.upload.validateOk')}</Badge>
|
|
@@ -533,19 +825,29 @@ export function MyConfigsView({ meApi, api, importApi, syncApi, t, myItems, myIt
|
|
|
533
825
|
))}
|
|
534
826
|
</div>
|
|
535
827
|
<div className={css.actionRow}>
|
|
536
|
-
<Button
|
|
828
|
+
<Button
|
|
829
|
+
variant="primary"
|
|
830
|
+
disabled={
|
|
831
|
+
wizard.validated !== true || wizard.running || wizard.zipPath === null
|
|
832
|
+
|| !myConfigFormValid(wizard.formErrors)
|
|
833
|
+
}
|
|
834
|
+
onClick={() => { void runUpload() }}
|
|
835
|
+
>
|
|
537
836
|
{wizard.running
|
|
538
837
|
? <Spinner label={wizard.mode === 'update' ? t('myconfigs.update.running') : t('myconfigs.upload.running')} />
|
|
539
838
|
: (wizard.mode === 'update' ? t('myconfigs.update.run') : t('myconfigs.upload.run'))}
|
|
540
839
|
</Button>
|
|
541
|
-
|
|
840
|
+
{wizard.mode === 'update' && (
|
|
841
|
+
<Button disabled={wizard.running || wizard.validating} onClick={cancelUpdate}>{t('common.cancel')}</Button>
|
|
842
|
+
)}
|
|
843
|
+
{wizard.mode === 'upload' && <Button disabled={wizard.running} onClick={reset}>{t('myconfigs.upload.reselect')}</Button>}
|
|
542
844
|
</div>
|
|
543
845
|
</div>
|
|
544
846
|
)}
|
|
545
847
|
|
|
546
848
|
{wizard.error !== null && <Banner kind="error">{redact(wizard.error)}</Banner>}
|
|
547
849
|
|
|
548
|
-
{/*
|
|
850
|
+
{/* 结果卡:收录状态(异步)/ PR 链接 / 仓库链接 / sha256 / 分区 */}
|
|
549
851
|
{wizard.result !== null && (
|
|
550
852
|
wizard.result.ok ? (
|
|
551
853
|
<div>
|
|
@@ -555,15 +857,29 @@ export function MyConfigsView({ meApi, api, importApi, syncApi, t, myItems, myIt
|
|
|
555
857
|
<Badge kind="info">{t('myconfigs.result.sha256', { hash: wizard.result.sha256 })}</Badge>
|
|
556
858
|
<Badge kind="info">{t('myconfigs.result.sections', { sections: wizard.result.sections.join(', ') })}</Badge>
|
|
557
859
|
</div>
|
|
860
|
+
{/* 收录状态:pending=后台处理中(轮询中);failed=失败可重试;done=已提交(PR 链接) */}
|
|
861
|
+
{wizard.result.listing === 'pending' && (
|
|
862
|
+
<div className={css.statRow}>
|
|
863
|
+
{listingStatus !== null && listingStatus.listing === 'failed' ? (
|
|
864
|
+
<>
|
|
865
|
+
<Badge kind="error">{t('myconfigs.result.listingFailed')}</Badge>
|
|
866
|
+
<Button variant="danger" onClick={() => { void runRelist(wizard.result!.itemId) }}>
|
|
867
|
+
{t('myconfigs.result.relist')}
|
|
868
|
+
</Button>
|
|
869
|
+
</>
|
|
870
|
+
) : (
|
|
871
|
+
<Badge kind="info">{t('myconfigs.result.listingPending')}</Badge>
|
|
872
|
+
)}
|
|
873
|
+
</div>
|
|
874
|
+
)}
|
|
875
|
+
{listingStatus !== null && listingStatus.listing === 'failed' && (
|
|
876
|
+
<Banner kind="error">{redact(listingStatus.error ?? t('common.unknownError'))}</Banner>
|
|
877
|
+
)}
|
|
558
878
|
<div className={css.actionRow}>
|
|
559
879
|
<Badge kind="info">{t('myconfigs.result.repo')}</Badge>
|
|
560
880
|
<Button href={wizard.result.repoUrl}>{t('myconfigs.result.openRepo')}</Button>
|
|
561
|
-
{
|
|
562
|
-
<Button href={
|
|
563
|
-
{wizard.result.prNumber !== null
|
|
564
|
-
? t('myconfigs.result.pr', { number: String(wizard.result.prNumber) })
|
|
565
|
-
: t('myconfigs.result.openPr')}
|
|
566
|
-
</Button>
|
|
881
|
+
{(prLink !== null) && (
|
|
882
|
+
<Button href={prLink.url}>{prLink.label}</Button>
|
|
567
883
|
)}
|
|
568
884
|
</div>
|
|
569
885
|
</div>
|
|
@@ -573,7 +889,9 @@ export function MyConfigsView({ meApi, api, importApi, syncApi, t, myItems, myIt
|
|
|
573
889
|
</Banner>
|
|
574
890
|
)
|
|
575
891
|
)}
|
|
576
|
-
|
|
892
|
+
</div>
|
|
893
|
+
</div>
|
|
894
|
+
</div>
|
|
577
895
|
)
|
|
578
896
|
}
|
|
579
897
|
|
|
@@ -617,10 +935,10 @@ export function MyConfigsView({ meApi, api, importApi, syncApi, t, myItems, myIt
|
|
|
617
935
|
</div>
|
|
618
936
|
</div>
|
|
619
937
|
<div className={css.rowActions}>
|
|
620
|
-
<Button onClick={() => { startUpdate(entry) }}>{t('myconfigs.item.update')}</Button>
|
|
938
|
+
<Button onClick={() => { startUpdate(entry); openUpload() }}>{t('myconfigs.item.update')}</Button>
|
|
621
939
|
<Button
|
|
622
940
|
disabled={install !== null && install.itemId === view.id && install.detail === null}
|
|
623
|
-
onClick={() => {
|
|
941
|
+
onClick={() => { openInstall(entry) }}
|
|
624
942
|
>
|
|
625
943
|
{t('myconfigs.item.install')}
|
|
626
944
|
</Button>
|
|
@@ -628,6 +946,14 @@ export function MyConfigsView({ meApi, api, importApi, syncApi, t, myItems, myIt
|
|
|
628
946
|
{badge.kind === 'warn' && badge.prUrl !== undefined && badge.prUrl !== '' && (
|
|
629
947
|
<Button href={badge.prUrl}>{t('myconfigs.item.openPr')}</Button>
|
|
630
948
|
)}
|
|
949
|
+
{/* 删除:danger 按钮 → 弹窗二次确认(ConfirmDialog;不可恢复,已收录自动提交下架 PR) */}
|
|
950
|
+
<Button
|
|
951
|
+
variant="danger"
|
|
952
|
+
disabled={deletingId !== null}
|
|
953
|
+
onClick={() => { onMyConfirmDeleteChange(view.id) }}
|
|
954
|
+
>
|
|
955
|
+
{t('myconfigs.delete.run')}
|
|
956
|
+
</Button>
|
|
631
957
|
</div>
|
|
632
958
|
</div>
|
|
633
959
|
))}
|
|
@@ -645,11 +971,24 @@ export function MyConfigsView({ meApi, api, importApi, syncApi, t, myItems, myIt
|
|
|
645
971
|
const approvalSummary = detail !== null ? approvedAdapterSummary(detail.plan, install.approvals) : null
|
|
646
972
|
const detailView = detail !== null ? marketDetailView(detail, detail.repo ?? entryRepoUrl(install.itemId), true, uiT) : null
|
|
647
973
|
return (
|
|
648
|
-
<
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
974
|
+
<div
|
|
975
|
+
className={css.dialogMask}
|
|
976
|
+
onMouseDown={(e) => { if (e.target === e.currentTarget && !install.importing) closeInstall() }}
|
|
977
|
+
>
|
|
978
|
+
<div className={`${css.dialogCard} ${css.dialogWide}`} role="dialog" aria-modal="true" aria-label={t('detail.title')}>
|
|
979
|
+
<div className={css.dialogHeaderRow}>
|
|
980
|
+
<span className={css.dialogHeader}>{t('detail.title')}:{install.itemId}</span>
|
|
981
|
+
<button
|
|
982
|
+
type="button"
|
|
983
|
+
className={css.dialogClose}
|
|
984
|
+
aria-label={t('common.close')}
|
|
985
|
+
disabled={install.importing}
|
|
986
|
+
onClick={closeInstall}
|
|
987
|
+
>
|
|
988
|
+
×
|
|
989
|
+
</button>
|
|
990
|
+
</div>
|
|
991
|
+
<div className={css.dialogBodyScroll}>
|
|
653
992
|
{install.error !== null && <Banner kind="error">{redact(install.error)}</Banner>}
|
|
654
993
|
{detail === null && <div className={css.statRow}><Spinner label={t('list.loading')} /></div>}
|
|
655
994
|
{detail !== null && detailView !== null && (
|
|
@@ -671,7 +1010,9 @@ export function MyConfigsView({ meApi, api, importApi, syncApi, t, myItems, myIt
|
|
|
671
1010
|
key={row.adapter}
|
|
672
1011
|
checked={row.approved}
|
|
673
1012
|
onChange={(checked) => {
|
|
674
|
-
|
|
1013
|
+
if (installRef.current !== null) {
|
|
1014
|
+
patchInstall({ approvals: { ...installRef.current.approvals, [row.adapter]: checked } })
|
|
1015
|
+
}
|
|
675
1016
|
}}
|
|
676
1017
|
label={
|
|
677
1018
|
<span>
|
|
@@ -715,7 +1056,9 @@ export function MyConfigsView({ meApi, api, importApi, syncApi, t, myItems, myIt
|
|
|
715
1056
|
{install.importResult.needsRestart && ' · 部分改动需重启 DSH 后生效'}
|
|
716
1057
|
</Banner>
|
|
717
1058
|
)}
|
|
718
|
-
|
|
1059
|
+
</div>
|
|
1060
|
+
</div>
|
|
1061
|
+
</div>
|
|
719
1062
|
)
|
|
720
1063
|
}
|
|
721
1064
|
|
|
@@ -727,19 +1070,59 @@ export function MyConfigsView({ meApi, api, importApi, syncApi, t, myItems, myIt
|
|
|
727
1070
|
|
|
728
1071
|
return (
|
|
729
1072
|
<div className={css.viewBody}>
|
|
730
|
-
<div className={css.actionRow}>
|
|
731
|
-
<Button onClick={onBack}>{t('myconfigs.back')}</Button>
|
|
732
|
-
</div>
|
|
733
1073
|
<SectionTitle title={t('myconfigs.tab.myconfigs')} subtitle={t('myconfigs.login.hint')} />
|
|
734
1074
|
{renderLoginCard()}
|
|
735
1075
|
{/* 已登录才允许上传 / 查看列表 / 装回本地 */}
|
|
736
1076
|
{loginView.kind === 'logged-in' && (
|
|
737
1077
|
<>
|
|
738
|
-
{
|
|
1078
|
+
{/* 一键上传入口:点按钮 → 免责(首次)→ 弹窗向导 */}
|
|
1079
|
+
<Card>
|
|
1080
|
+
<div className={css.actionRow}>
|
|
1081
|
+
<span className={css.groupLabel}>{t('myconfigs.upload.title')}</span>
|
|
1082
|
+
<Button variant="primary" onClick={openUpload}>{t('myconfigs.upload.run')}</Button>
|
|
1083
|
+
</div>
|
|
1084
|
+
<span className={css.hint}>{t('myconfigs.upload.selectHint')}</span>
|
|
1085
|
+
</Card>
|
|
739
1086
|
{renderList()}
|
|
740
|
-
{
|
|
1087
|
+
{/* 弹窗:上传/更新向导(uploadOpen)与装回本地(installOpen),条件渲染 */}
|
|
1088
|
+
{uploadOpen && renderWizard()}
|
|
1089
|
+
{installOpen && renderInstall()}
|
|
741
1090
|
</>
|
|
742
1091
|
)}
|
|
1092
|
+
{/* 免责弹窗(复用 ConfirmDialog + 「不再提示」勾选;三操作分开记) */}
|
|
1093
|
+
<ConfirmDialog
|
|
1094
|
+
open={disclaimerKey !== null}
|
|
1095
|
+
title={t('disclaimer.title')}
|
|
1096
|
+
message={disclaimerText()}
|
|
1097
|
+
confirmLabel={t('disclaimer.confirm')}
|
|
1098
|
+
cancelLabel={t('common.cancel')}
|
|
1099
|
+
onConfirm={confirmDisclaimer}
|
|
1100
|
+
onCancel={cancelDisclaimer}
|
|
1101
|
+
>
|
|
1102
|
+
<label className={css.checkboxRow}>
|
|
1103
|
+
<input
|
|
1104
|
+
type="checkbox"
|
|
1105
|
+
checked={dontAsk}
|
|
1106
|
+
onChange={(e: ChangeEvent<HTMLInputElement>) => { setDontAsk(e.target.checked) }}
|
|
1107
|
+
/>
|
|
1108
|
+
<span>{t('disclaimer.dontAsk')}</span>
|
|
1109
|
+
</label>
|
|
1110
|
+
</ConfirmDialog>
|
|
1111
|
+
{/* 删除确认弹窗(二次确认;不可恢复;确认执行中 busy 防重复提交) */}
|
|
1112
|
+
<ConfirmDialog
|
|
1113
|
+
open={confirmDeleteId !== null}
|
|
1114
|
+
title={t('myconfigs.delete.confirmTitle')}
|
|
1115
|
+
message={t('myconfigs.delete.confirmText')}
|
|
1116
|
+
confirmLabel={t('myconfigs.delete.confirm')}
|
|
1117
|
+
cancelLabel={t('common.cancel')}
|
|
1118
|
+
danger
|
|
1119
|
+
busy={deletingId !== null}
|
|
1120
|
+
onConfirm={async () => {
|
|
1121
|
+
const entry = (myItems ?? []).find((it) => it.id === confirmDeleteId)
|
|
1122
|
+
if (entry !== undefined) await runDelete(entry)
|
|
1123
|
+
}}
|
|
1124
|
+
onCancel={() => { onMyConfirmDeleteChange(null) }}
|
|
1125
|
+
/>
|
|
743
1126
|
</div>
|
|
744
1127
|
)
|
|
745
1128
|
}
|