dsh-taskboard 0.5.5 → 0.6.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 +22 -2
- package/lib/client.js +2604 -767
- package/lib/host/execution.js +3 -0
- package/lib/host/execution.js.map +1 -1
- package/lib/host/routes.js +31 -1
- package/lib/host/routes.js.map +1 -1
- package/lib/host/session-sync.js +210 -10
- package/lib/host/session-sync.js.map +1 -1
- package/lib/host/store.js +9 -2
- package/lib/host/store.js.map +1 -1
- package/lib/index.js +100 -1
- package/lib/index.js.map +1 -1
- package/lib/shared/api.js.map +1 -1
- package/lib/shared/protocol.js +19 -1
- package/lib/shared/protocol.js.map +1 -1
- package/package.json +75 -75
- package/src/client/api.ts +8 -0
- package/src/client/board/AlertModal.tsx +3 -1
- package/src/client/board/ImportModal.tsx +26 -24
- package/src/client/board/SettingsModal.tsx +66 -26
- package/src/client/board/SlashPromptInput.tsx +272 -0
- package/src/client/board/TaskBoard.tsx +53 -49
- package/src/client/board/TaskCard.tsx +33 -21
- package/src/client/board/TaskDetail.tsx +169 -104
- package/src/client/board/TaskFormModal.tsx +254 -202
- package/src/client/board/TemplateManager.tsx +32 -29
- package/src/client/board/labels.ts +36 -27
- package/src/client/controller.ts +62 -2
- package/src/client/i18n/en.ts +455 -0
- package/src/client/i18n/runtime.ts +155 -0
- package/src/client/i18n/zh.ts +460 -0
- package/src/client/index.ts +182 -42
- package/src/client/sidebar-entry.ts +13 -3
- package/src/client/styles.ts +131 -0
- package/src/host/execution.ts +13 -0
- package/src/host/routes.ts +49 -1
- package/src/host/session-sync.ts +334 -14
- package/src/host/store.ts +15 -1
- package/src/index.ts +115 -1
- package/src/shared/api.ts +47 -0
- package/src/shared/protocol.ts +41 -0
- package/src/shared/version.ts +1 -1
|
@@ -13,7 +13,8 @@ import type { ExecutionRecord, TaskRecord } from '../../shared/protocol.ts'
|
|
|
13
13
|
import { canTransition, checklistProgress } from '../../shared/protocol.ts'
|
|
14
14
|
import { useAlert } from './AlertModal.tsx'
|
|
15
15
|
import { fmtTime, isStaleClaim } from './format.ts'
|
|
16
|
-
import {
|
|
16
|
+
import { MOVE_KEYS, OUTCOME_KEYS, STATUS_KEYS, URGENCY_KEYS } from './labels.ts'
|
|
17
|
+
import { useT } from '../i18n/runtime.ts'
|
|
17
18
|
|
|
18
19
|
/** Statuses a user may move this task to, per the state machine. */
|
|
19
20
|
function moveTargets(task: TaskRecord): TaskRecord['status'][] {
|
|
@@ -41,6 +42,62 @@ function Chip({ icon, children, tone, title }: { icon?: string; children: ReactN
|
|
|
41
42
|
return <span className="dsh-atb-chip2" data-tone={tone} title={title}>{icon !== undefined && <span className="dsh-atb-chip2-icon">{icon}</span>}{children}</span>
|
|
42
43
|
}
|
|
43
44
|
|
|
45
|
+
/** Render markdown text with embedded clickable images and lightbox preview. */
|
|
46
|
+
function MarkdownContent({ text }: { text: string }) {
|
|
47
|
+
const t = useT()
|
|
48
|
+
const [lightboxUrl, setLightboxUrl] = useState<string | null>(null)
|
|
49
|
+
const regex = /!\[(.*?)\]\(((?:data:image\/[^)]+)|(?:https?:\/\/[^)]+)|(?:[^)]+\.(?:png|jpg|jpeg|gif|webp|svg)))\)/gi
|
|
50
|
+
const parts: ReactNode[] = []
|
|
51
|
+
let lastIndex = 0
|
|
52
|
+
let match: RegExpExecArray | null
|
|
53
|
+
let count = 0
|
|
54
|
+
|
|
55
|
+
while ((match = regex.exec(text)) !== null) {
|
|
56
|
+
if (match.index > lastIndex) {
|
|
57
|
+
parts.push(<span key={`txt-${lastIndex}`}>{text.slice(lastIndex, match.index)}</span>)
|
|
58
|
+
}
|
|
59
|
+
const alt = match[1] || t('md.imageAlt', { n: ++count })
|
|
60
|
+
const url = match[2] ?? ''
|
|
61
|
+
parts.push(
|
|
62
|
+
<div key={`img-${match.index}`} className="dsh-atb-detail-img-wrap">
|
|
63
|
+
<img
|
|
64
|
+
src={url}
|
|
65
|
+
alt={alt}
|
|
66
|
+
className="dsh-atb-detail-img"
|
|
67
|
+
onClick={() => setLightboxUrl(url)}
|
|
68
|
+
title={t('md.imageTitle', { alt })}
|
|
69
|
+
/>
|
|
70
|
+
<span className="dsh-atb-detail-img-caption">{alt}</span>
|
|
71
|
+
</div>,
|
|
72
|
+
)
|
|
73
|
+
lastIndex = regex.lastIndex
|
|
74
|
+
}
|
|
75
|
+
if (lastIndex < text.length) {
|
|
76
|
+
parts.push(<span key={`txt-${lastIndex}`}>{text.slice(lastIndex)}</span>)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<>
|
|
81
|
+
<div className="dsh-atb-markdown-body">{parts}</div>
|
|
82
|
+
{lightboxUrl !== null && (
|
|
83
|
+
<div className="dsh-atb-lightbox-backdrop" onClick={() => setLightboxUrl(null)}>
|
|
84
|
+
<div className="dsh-atb-lightbox-content" onClick={e => e.stopPropagation()}>
|
|
85
|
+
<img src={lightboxUrl} alt={t('md.lightboxAlt')} className="dsh-atb-lightbox-img" />
|
|
86
|
+
<button
|
|
87
|
+
type="button"
|
|
88
|
+
className="dsh-atb-lightbox-close"
|
|
89
|
+
title={t('md.closePreview')}
|
|
90
|
+
onClick={() => setLightboxUrl(null)}
|
|
91
|
+
>
|
|
92
|
+
✕
|
|
93
|
+
</button>
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
)}
|
|
97
|
+
</>
|
|
98
|
+
)
|
|
99
|
+
}
|
|
100
|
+
|
|
44
101
|
/** The most recent execution carrying isolation facts, newest first. */
|
|
45
102
|
function latestIsolated(task: TaskRecord): ExecutionRecord | undefined {
|
|
46
103
|
return [...task.executions].reverse().find(e => e.isolation !== undefined || e.worktreePath !== undefined || e.isolationNote !== undefined)
|
|
@@ -71,6 +128,7 @@ function DiffView({ controller, task, execution, commit, path }: {
|
|
|
71
128
|
commit?: string
|
|
72
129
|
path?: string
|
|
73
130
|
}) {
|
|
131
|
+
const t = useT()
|
|
74
132
|
const [state, setState] = useState<{ loading: boolean; diff?: string; truncated?: boolean; failed?: boolean }>({ loading: true })
|
|
75
133
|
useEffect(() => {
|
|
76
134
|
let alive = true
|
|
@@ -85,12 +143,12 @@ function DiffView({ controller, task, execution, commit, path }: {
|
|
|
85
143
|
return (
|
|
86
144
|
<div className="dsh-atb-diffview">
|
|
87
145
|
<div className="dsh-atb-diffview-head">
|
|
88
|
-
<span className="dsh-atb-diffview-title">{commit !== undefined ?
|
|
89
|
-
{state.loading && <span className="dsh-atb-diffview-hint"
|
|
90
|
-
{state.truncated === true && <span className="dsh-atb-diffview-hint"
|
|
146
|
+
<span className="dsh-atb-diffview-title">{commit !== undefined ? t('diff.commit', { hash: shortHash(commit) }) : t('diff.file', { path: path ?? '' })}</span>
|
|
147
|
+
{state.loading && <span className="dsh-atb-diffview-hint">{t('shared.loading')}</span>}
|
|
148
|
+
{state.truncated === true && <span className="dsh-atb-diffview-hint">{t('diff.truncated')}</span>}
|
|
91
149
|
</div>
|
|
92
150
|
{state.failed === true
|
|
93
|
-
? <div className="dsh-atb-diffview-error"
|
|
151
|
+
? <div className="dsh-atb-diffview-error">{t('diff.failed')}</div>
|
|
94
152
|
: <pre className="dsh-atb-diffview-pre">{state.diff ?? ''}</pre>}
|
|
95
153
|
</div>
|
|
96
154
|
)
|
|
@@ -101,6 +159,7 @@ function DiffView({ controller, task, execution, commit, path }: {
|
|
|
101
159
|
* per row; unchecked items highlight while the task sits in in_review.
|
|
102
160
|
*/
|
|
103
161
|
function ChecklistBlock({ task, controller }: { task: TaskRecord; controller: BoardController }) {
|
|
162
|
+
const t = useT()
|
|
104
163
|
const items = task.checklist ?? []
|
|
105
164
|
if (items.length === 0) return null
|
|
106
165
|
const { done, total } = checklistProgress(task)
|
|
@@ -109,9 +168,9 @@ function ChecklistBlock({ task, controller }: { task: TaskRecord; controller: Bo
|
|
|
109
168
|
return (
|
|
110
169
|
<div className="dsh-atb-fieldcard" data-kind="checklist">
|
|
111
170
|
<div className="dsh-atb-fieldcard-label">
|
|
112
|
-
|
|
171
|
+
{t('checklist.title')}
|
|
113
172
|
<span className="dsh-atb-cl-progress" data-tone={reviewing && unchecked > 0 ? 'bad' : undefined}>
|
|
114
|
-
☑ {done}/{total}{reviewing && unchecked > 0 ?
|
|
173
|
+
☑ {done}/{total}{reviewing && unchecked > 0 ? t('checklist.unchecked', { n: unchecked }) : done === total ? t('checklist.allDone') : ''}
|
|
115
174
|
</span>
|
|
116
175
|
</div>
|
|
117
176
|
<div className="dsh-atb-cl-items">
|
|
@@ -130,9 +189,9 @@ function ChecklistBlock({ task, controller }: { task: TaskRecord; controller: Bo
|
|
|
130
189
|
<span className="dsh-atb-cl-text">{item.text}</span>
|
|
131
190
|
<span className="dsh-atb-cl-meta">
|
|
132
191
|
{item.checked
|
|
133
|
-
? `${item.checkedBy === 'user' ? '
|
|
134
|
-
: '
|
|
135
|
-
{item.note !== undefined && item.note.length > 0 && <span className="dsh-atb-cl-note" title={item.note}
|
|
192
|
+
? `${item.checkedBy === 'user' ? t('checklist.byUser') : `🤖 ${shortId(item.checkedBy)}`} · ${fmtTime(item.checkedAt)}`
|
|
193
|
+
: t('checklist.uncheckedItem')}
|
|
194
|
+
{item.note !== undefined && item.note.length > 0 && <span className="dsh-atb-cl-note" title={item.note}>{t('checklist.evidence', { note: item.note })}</span>}
|
|
136
195
|
</span>
|
|
137
196
|
</label>
|
|
138
197
|
))}
|
|
@@ -146,6 +205,7 @@ function ChecklistBlock({ task, controller }: { task: TaskRecord; controller: Bo
|
|
|
146
205
|
* carries one, rendered section by section for the reviewer.
|
|
147
206
|
*/
|
|
148
207
|
function ReportBlock({ task }: { task: TaskRecord }) {
|
|
208
|
+
const t = useT()
|
|
149
209
|
const execution = [...task.executions].reverse().find(e => e.report !== undefined)
|
|
150
210
|
const report = execution?.report
|
|
151
211
|
if (execution === undefined || report === undefined) return null
|
|
@@ -159,14 +219,14 @@ function ReportBlock({ task }: { task: TaskRecord }) {
|
|
|
159
219
|
: null
|
|
160
220
|
return (
|
|
161
221
|
<div className="dsh-atb-fieldcard" data-kind="report">
|
|
162
|
-
<div className="dsh-atb-fieldcard-label"
|
|
222
|
+
<div className="dsh-atb-fieldcard-label">{t('report.title')}<span className="dsh-atb-cl-progress">{t('report.submitted', { time: fmtTime(execution.endedAt ?? execution.startedAt) })}</span></div>
|
|
163
223
|
<div className="dsh-atb-rpt-summary">{report.summary}</div>
|
|
164
|
-
{section('
|
|
165
|
-
{section('
|
|
166
|
-
{section('
|
|
224
|
+
{section(t('report.changedFiles'), report.changedFiles)}
|
|
225
|
+
{section(t('report.checks'), report.checks)}
|
|
226
|
+
{section(t('report.artifacts'), report.artifacts)}
|
|
167
227
|
{report.risk.length > 0 && (
|
|
168
228
|
<div className="dsh-atb-rpt-sec">
|
|
169
|
-
<div className="dsh-atb-rpt-label"
|
|
229
|
+
<div className="dsh-atb-rpt-label">{t('report.risk')}</div>
|
|
170
230
|
<div className="dsh-atb-rpt-risk">{report.risk}</div>
|
|
171
231
|
</div>
|
|
172
232
|
)}
|
|
@@ -180,6 +240,7 @@ function ReportBlock({ task }: { task: TaskRecord }) {
|
|
|
180
240
|
* remove worktree — plan §3.3).
|
|
181
241
|
*/
|
|
182
242
|
function IsolationBlock({ task, controller }: { task: TaskRecord; controller: BoardController }) {
|
|
243
|
+
const t = useT()
|
|
183
244
|
const { alert: showAlert, el: alertEl } = useAlert()
|
|
184
245
|
const [confirmMerge, setConfirmMerge] = useState(false)
|
|
185
246
|
const [confirmRemove, setConfirmRemove] = useState<'wt' | 'wtb' | null>(null)
|
|
@@ -196,8 +257,8 @@ function IsolationBlock({ task, controller }: { task: TaskRecord; controller: Bo
|
|
|
196
257
|
void controller.mergeBranch(task.id).then(result => {
|
|
197
258
|
setBusy(false)
|
|
198
259
|
setConfirmMerge(false)
|
|
199
|
-
if (!result.ok) showAlert(
|
|
200
|
-
else if (result.noop === true) showAlert('
|
|
260
|
+
if (!result.ok) showAlert(t('iso.merge.failed', { error: result.error }))
|
|
261
|
+
else if (result.noop === true) showAlert(t('iso.merge.noop'))
|
|
201
262
|
})
|
|
202
263
|
}
|
|
203
264
|
|
|
@@ -206,8 +267,8 @@ function IsolationBlock({ task, controller }: { task: TaskRecord; controller: Bo
|
|
|
206
267
|
void controller.removeWorktree(task.id, deleteBranch).then(result => {
|
|
207
268
|
setBusy(false)
|
|
208
269
|
setConfirmRemove(null)
|
|
209
|
-
if (!result.ok) showAlert(
|
|
210
|
-
else if (result.branchError !== undefined) showAlert(
|
|
270
|
+
if (!result.ok) showAlert(t('iso.remove.failed', { error: result.error }))
|
|
271
|
+
else if (result.branchError !== undefined) showAlert(t('iso.remove.branchFailed', { error: result.branchError }))
|
|
211
272
|
})
|
|
212
273
|
}
|
|
213
274
|
|
|
@@ -215,8 +276,8 @@ function IsolationBlock({ task, controller }: { task: TaskRecord; controller: Bo
|
|
|
215
276
|
if (execution.isolation !== 'worktree' || execution.worktreePath === undefined) {
|
|
216
277
|
return (
|
|
217
278
|
<div className="dsh-atb-fieldcard" data-kind="isolation">
|
|
218
|
-
<div className="dsh-atb-fieldcard-label"
|
|
219
|
-
<div className="dsh-atb-iso-none"
|
|
279
|
+
<div className="dsh-atb-fieldcard-label">{t('iso.title')}</div>
|
|
280
|
+
<div className="dsh-atb-iso-none">{t('iso.none')}{execution.isolationNote !== undefined ? ` · ${execution.isolationNote}` : ''}</div>
|
|
220
281
|
{alertEl}
|
|
221
282
|
</div>
|
|
222
283
|
)
|
|
@@ -229,12 +290,12 @@ function IsolationBlock({ task, controller }: { task: TaskRecord; controller: Bo
|
|
|
229
290
|
|
|
230
291
|
return (
|
|
231
292
|
<div className="dsh-atb-fieldcard" data-kind="isolation">
|
|
232
|
-
<div className="dsh-atb-fieldcard-label"
|
|
293
|
+
<div className="dsh-atb-fieldcard-label">{t('iso.worktreeTitle')}</div>
|
|
233
294
|
<div className="dsh-atb-iso-facts">
|
|
234
|
-
<span className="dsh-atb-iso-fact" title={execution.worktreePath}
|
|
235
|
-
<span className="dsh-atb-iso-fact"
|
|
295
|
+
<span className="dsh-atb-iso-fact" title={execution.worktreePath}>{t('iso.branch')} <b>{execution.branch ?? task.branch}</b></span>
|
|
296
|
+
<span className="dsh-atb-iso-fact">{t('iso.baseline', { base: shortHash(execution.baseCommit), head: shortHash(execution.headCommit) })}</span>
|
|
236
297
|
{execution.changedFiles !== undefined && execution.changedFiles > 0 && (
|
|
237
|
-
<span className="dsh-atb-iso-fact"
|
|
298
|
+
<span className="dsh-atb-iso-fact">{t('iso.changed', { n: execution.changedFiles })}</span>
|
|
238
299
|
)}
|
|
239
300
|
{execution.diffStat !== undefined && <span className="dsh-atb-iso-fact" title={execution.diffStat}>{execution.diffStat}</span>}
|
|
240
301
|
</div>
|
|
@@ -247,7 +308,7 @@ function IsolationBlock({ task, controller }: { task: TaskRecord; controller: Bo
|
|
|
247
308
|
<button
|
|
248
309
|
type="button"
|
|
249
310
|
className="dsh-atb-iso-commit-btn"
|
|
250
|
-
title=
|
|
311
|
+
title={t('iso.commit.openTitle')}
|
|
251
312
|
onClick={() => setOpenDiff(openDiff?.commit === c.hash ? null : { commit: c.hash })}
|
|
252
313
|
>
|
|
253
314
|
<code>{shortHash(c.hash)}</code>
|
|
@@ -258,15 +319,15 @@ function IsolationBlock({ task, controller }: { task: TaskRecord; controller: Bo
|
|
|
258
319
|
)}
|
|
259
320
|
</div>
|
|
260
321
|
))}
|
|
261
|
-
{commitTotal > 10 && <div className="dsh-atb-iso-more"
|
|
322
|
+
{commitTotal > 10 && <div className="dsh-atb-iso-more">{t('iso.commits.more', { n: commitTotal })}</div>}
|
|
262
323
|
</div>
|
|
263
324
|
)
|
|
264
|
-
: <div className="dsh-atb-iso-nocommit"
|
|
325
|
+
: <div className="dsh-atb-iso-nocommit">{t('iso.nocommit')}</div>}
|
|
265
326
|
|
|
266
327
|
{dirtyTotal > 0 && (
|
|
267
328
|
<div className="dsh-atb-iso-dirty">
|
|
268
329
|
<button type="button" className="dsh-atb-iso-dirty-toggle" onClick={() => setDirtyOpen(!dirtyOpen)}>
|
|
269
|
-
|
|
330
|
+
{t('iso.dirty.toggle', { n: dirtyTotal })}{dirtyOpen ? t('iso.dirty.collapse') : t('iso.dirty.expand')}
|
|
270
331
|
</button>
|
|
271
332
|
{dirtyOpen && (
|
|
272
333
|
<div className="dsh-atb-iso-dirty-files">
|
|
@@ -277,14 +338,14 @@ function IsolationBlock({ task, controller }: { task: TaskRecord; controller: Bo
|
|
|
277
338
|
key={`${line}-${index}`}
|
|
278
339
|
type="button"
|
|
279
340
|
className="dsh-atb-iso-dirty-file"
|
|
280
|
-
title=
|
|
341
|
+
title={t('iso.dirty.openTitle')}
|
|
281
342
|
onClick={() => setOpenDiff(openDiff?.path === filePath ? null : { path: filePath })}
|
|
282
343
|
>
|
|
283
344
|
<code>{line.slice(0, 2)}</code> {filePath}
|
|
284
345
|
</button>
|
|
285
346
|
)
|
|
286
347
|
})}
|
|
287
|
-
{dirtyTotal > 30 && <div className="dsh-atb-iso-more"
|
|
348
|
+
{dirtyTotal > 30 && <div className="dsh-atb-iso-more">{t('iso.dirty.more', { n: dirtyTotal })}</div>}
|
|
288
349
|
</div>
|
|
289
350
|
)}
|
|
290
351
|
{openDiff?.path !== undefined && dirtyOpen && (
|
|
@@ -295,13 +356,13 @@ function IsolationBlock({ task, controller }: { task: TaskRecord; controller: Bo
|
|
|
295
356
|
|
|
296
357
|
<div className="dsh-atb-iso-actions">
|
|
297
358
|
{running
|
|
298
|
-
? <span className="dsh-atb-iso-hint"
|
|
359
|
+
? <span className="dsh-atb-iso-hint">{t('iso.hint.running')}</span>
|
|
299
360
|
: confirmMerge
|
|
300
361
|
? (
|
|
301
362
|
<span className="dsh-atb-confirm">
|
|
302
|
-
<span className="dsh-atb-confirm-label"
|
|
303
|
-
<button type="button" className="dsh-atb-btn" data-primary="true" disabled={busy} onClick={doMerge}
|
|
304
|
-
<button type="button" className="dsh-atb-btn" onClick={() => setConfirmMerge(false)}
|
|
363
|
+
<span className="dsh-atb-confirm-label">{t('iso.merge.confirm')}</span>
|
|
364
|
+
<button type="button" className="dsh-atb-btn" data-primary="true" disabled={busy} onClick={doMerge}>{t('iso.merge.go')}</button>
|
|
365
|
+
<button type="button" className="dsh-atb-btn" onClick={() => setConfirmMerge(false)}>{t('shared.cancel')}</button>
|
|
305
366
|
</span>
|
|
306
367
|
)
|
|
307
368
|
: (
|
|
@@ -309,10 +370,10 @@ function IsolationBlock({ task, controller }: { task: TaskRecord; controller: Bo
|
|
|
309
370
|
type="button"
|
|
310
371
|
className="dsh-atb-btn"
|
|
311
372
|
disabled={busy}
|
|
312
|
-
title=
|
|
373
|
+
title={t('iso.merge.title')}
|
|
313
374
|
onClick={() => setConfirmMerge(true)}
|
|
314
375
|
>
|
|
315
|
-
|
|
376
|
+
{t('iso.merge.button')}
|
|
316
377
|
</button>
|
|
317
378
|
)}
|
|
318
379
|
{!running && (confirmRemove === null
|
|
@@ -323,10 +384,10 @@ function IsolationBlock({ task, controller }: { task: TaskRecord; controller: Bo
|
|
|
323
384
|
className="dsh-atb-btn"
|
|
324
385
|
data-danger="true"
|
|
325
386
|
disabled={busy}
|
|
326
|
-
title=
|
|
387
|
+
title={t('iso.remove.wtTitle')}
|
|
327
388
|
onClick={() => setConfirmRemove('wt')}
|
|
328
389
|
>
|
|
329
|
-
|
|
390
|
+
{t('iso.remove.wt')}
|
|
330
391
|
</button>
|
|
331
392
|
{task.branch !== undefined && (
|
|
332
393
|
<button
|
|
@@ -334,22 +395,22 @@ function IsolationBlock({ task, controller }: { task: TaskRecord; controller: Bo
|
|
|
334
395
|
className="dsh-atb-btn"
|
|
335
396
|
data-danger="true"
|
|
336
397
|
disabled={busy}
|
|
337
|
-
title=
|
|
398
|
+
title={t('iso.remove.wtbTitle')}
|
|
338
399
|
onClick={() => setConfirmRemove('wtb')}
|
|
339
400
|
>
|
|
340
|
-
|
|
401
|
+
{t('iso.remove.wtb')}
|
|
341
402
|
</button>
|
|
342
403
|
)}
|
|
343
404
|
</>
|
|
344
405
|
)
|
|
345
406
|
: (
|
|
346
407
|
<span className="dsh-atb-confirm">
|
|
347
|
-
<span className="dsh-atb-confirm-label">{confirmRemove === 'wtb' ? '
|
|
348
|
-
<button type="button" className="dsh-atb-btn" data-danger="true" disabled={busy} onClick={() => doRemove(confirmRemove === 'wtb')}
|
|
349
|
-
<button type="button" className="dsh-atb-btn" onClick={() => setConfirmRemove(null)}
|
|
408
|
+
<span className="dsh-atb-confirm-label">{confirmRemove === 'wtb' ? t('iso.remove.confirmWtb') : t('iso.remove.confirmWt')}</span>
|
|
409
|
+
<button type="button" className="dsh-atb-btn" data-danger="true" disabled={busy} onClick={() => doRemove(confirmRemove === 'wtb')}>{t('shared.confirmDelete')}</button>
|
|
410
|
+
<button type="button" className="dsh-atb-btn" onClick={() => setConfirmRemove(null)}>{t('shared.cancel')}</button>
|
|
350
411
|
</span>
|
|
351
412
|
))}
|
|
352
|
-
{!running && confirmRemove === null && !confirmMerge && <span className="dsh-atb-iso-hint"
|
|
413
|
+
{!running && confirmRemove === null && !confirmMerge && <span className="dsh-atb-iso-hint">{t('iso.hint.keep')}</span>}
|
|
353
414
|
</div>
|
|
354
415
|
{alertEl}
|
|
355
416
|
</div>
|
|
@@ -363,6 +424,7 @@ function IsolationBlock({ task, controller }: { task: TaskRecord; controller: Bo
|
|
|
363
424
|
* @param now - current epoch ms (stale-claim highlight).
|
|
364
425
|
*/
|
|
365
426
|
export function TaskDetail({ task, controller, now }: { task: TaskRecord; controller: BoardController; now?: number }) {
|
|
427
|
+
const t = useT()
|
|
366
428
|
const [comment, setComment] = useState('')
|
|
367
429
|
const [confirmDone, setConfirmDone] = useState(false)
|
|
368
430
|
const [confirmPurge, setConfirmPurge] = useState(false)
|
|
@@ -391,9 +453,9 @@ export function TaskDetail({ task, controller, now }: { task: TaskRecord; contro
|
|
|
391
453
|
/** Jump to an execution's session; prompt precisely when it cannot open. */
|
|
392
454
|
const jumpToSession = (sessionId: string): void => {
|
|
393
455
|
void controller.openSession(sessionId).then(result => {
|
|
394
|
-
if (result === 'missing') showAlert(
|
|
395
|
-
else if (result === 'archived') showAlert(
|
|
396
|
-
else if (result === 'unavailable') showAlert(
|
|
456
|
+
if (result === 'missing') showAlert(t('card.session.missing', { id: shortId(sessionId) }))
|
|
457
|
+
else if (result === 'archived') showAlert(t('card.session.archived', { id: shortId(sessionId) }))
|
|
458
|
+
else if (result === 'unavailable') showAlert(t('card.session.unavailable', { id: sessionId }))
|
|
397
459
|
})
|
|
398
460
|
}
|
|
399
461
|
|
|
@@ -403,50 +465,53 @@ export function TaskDetail({ task, controller, now }: { task: TaskRecord; contro
|
|
|
403
465
|
<div className="dsh-atb-detail-titlewrap">
|
|
404
466
|
<div className="dsh-atb-detail-titlebar">
|
|
405
467
|
<h3>{task.title}</h3>
|
|
406
|
-
<span className="dsh-atb-statuspill" data-status={task.status}>{
|
|
468
|
+
<span className="dsh-atb-statuspill" data-status={task.status}>{t(STATUS_KEYS[task.status] ?? task.status)}</span>
|
|
407
469
|
</div>
|
|
408
470
|
<div className="dsh-atb-detail-chips">
|
|
409
|
-
<Chip tone={task.urgency}>● {
|
|
471
|
+
<Chip tone={task.urgency}>● {t(URGENCY_KEYS[task.urgency] ?? task.urgency)}</Chip>
|
|
410
472
|
<Chip icon="📁">{ws?.title ?? shortId(task.workspaceId)}</Chip>
|
|
411
473
|
{task.model !== undefined && (
|
|
412
474
|
<Chip
|
|
413
475
|
icon="✦"
|
|
414
|
-
title={
|
|
476
|
+
title={t('card.badge.modelTitle', { model: task.model.provider + '/' + task.model.model }) + (task.model.reasoningEffort !== undefined ? t('card.badge.modelEffort', { effort: task.model.reasoningEffort }) : '')}
|
|
415
477
|
>
|
|
416
478
|
{task.model.model}{task.model.reasoningEffort !== undefined ? ` · ${task.model.reasoningEffort}` : ''}
|
|
417
479
|
</Chip>
|
|
418
480
|
)}
|
|
419
481
|
{task.presetId !== undefined && <Chip icon="🎛" >{task.presetId}</Chip>}
|
|
420
482
|
{task.execution.mode === 'scheduled' && (
|
|
421
|
-
<Chip icon="⏰">{task.execution.cron
|
|
483
|
+
<Chip icon="⏰">{t('detail.chip.nextRun', { cron: task.execution.cron ?? '', time: fmtTime(task.execution.nextRunAt) })}</Chip>
|
|
422
484
|
)}
|
|
423
|
-
{task.blocked && <Chip icon="⛔" tone="urgent"
|
|
485
|
+
{task.blocked && <Chip icon="⛔" tone="urgent">{t('shared.blocked')}</Chip>}
|
|
424
486
|
{task.checklist !== undefined && task.checklist.length > 0 && (
|
|
425
487
|
<Chip icon="☑" tone={task.status === 'in_review' && task.checklist.some(i => !i.checked) ? 'urgent' : undefined}>
|
|
426
|
-
|
|
488
|
+
{t('detail.chip.checklist', { done: checklistProgress(task).done, total: task.checklist.length })}
|
|
427
489
|
</Chip>
|
|
428
490
|
)}
|
|
429
491
|
{task.branch !== undefined && (
|
|
430
492
|
<Chip icon="🌿" tone={undefined}>Worktree · {task.branch.length > 28 ? `${task.branch.slice(0, 28)}…` : task.branch}</Chip>
|
|
431
493
|
)}
|
|
432
|
-
{(task.isolation === undefined || task.isolation === 'worktree') && task.branch === undefined && <Chip icon="🌿">
|
|
494
|
+
{(task.isolation === undefined || task.isolation === 'worktree') && task.branch === undefined && <Chip icon="🌿">{t('detail.chip.isolated')}</Chip>}
|
|
495
|
+
{task.permission === 'read-only' && <Chip icon="🔒" tone="urgent">{t('detail.chip.permReadOnly')}</Chip>}
|
|
496
|
+
{task.permission === 'danger-full-access' && <Chip icon="⚡" tone="urgent">{t('detail.chip.permFull')}</Chip>}
|
|
497
|
+
{(task.permission === 'workspace-write' || task.permission === undefined) && <Chip icon="📁">{t('detail.chip.permWrite')}</Chip>}
|
|
433
498
|
{holder !== undefined && (
|
|
434
499
|
<button
|
|
435
500
|
type="button"
|
|
436
501
|
className="dsh-atb-chip2 dsh-atb-chip-btn"
|
|
437
502
|
data-tone={stale ? 'urgent' : undefined}
|
|
438
|
-
title={
|
|
503
|
+
title={t('detail.chip.holderTitle', { id: holder })}
|
|
439
504
|
onClick={() => jumpToSession(holder)}
|
|
440
505
|
>
|
|
441
506
|
<span className="dsh-atb-chip2-icon">{stale ? '⏱' : '🤖'}</span>
|
|
442
|
-
{stale ? '
|
|
507
|
+
{stale ? t('detail.chip.holderStale') : t('detail.chip.holderBy')}{shortId(holder)}{t('detail.chip.holderSuffix')}
|
|
443
508
|
</button>
|
|
444
509
|
)}
|
|
445
|
-
{task.trashedAt !== undefined && <Chip icon="🗑" tone="urgent"
|
|
510
|
+
{task.trashedAt !== undefined && <Chip icon="🗑" tone="urgent">{t('detail.chip.trashed')}</Chip>}
|
|
446
511
|
<Chip>v{task.version}</Chip>
|
|
447
512
|
</div>
|
|
448
513
|
<div className="dsh-atb-detail-sub">
|
|
449
|
-
|
|
514
|
+
{t('detail.sub.line', { time: fmtTime(task.updatedAt), who: task.updatedBy.kind === 'agent' ? `🤖 ${shortId(task.updatedBy.sessionId)}` : task.updatedBy.kind === 'system' ? t('detail.updatedBy.system') : t('detail.updatedBy.user') })}
|
|
450
515
|
</div>
|
|
451
516
|
</div>
|
|
452
517
|
<div className="dsh-atb-detail-topbtns">
|
|
@@ -454,62 +519,62 @@ export function TaskDetail({ task, controller, now }: { task: TaskRecord; contro
|
|
|
454
519
|
<button
|
|
455
520
|
type="button"
|
|
456
521
|
className="dsh-atb-detail-session"
|
|
457
|
-
title={
|
|
522
|
+
title={t('detail.session.jumpTitle', { id: targetSessionId })}
|
|
458
523
|
onClick={() => jumpToSession(targetSessionId)}
|
|
459
524
|
>
|
|
460
|
-
|
|
525
|
+
{t('detail.session.jump')}
|
|
461
526
|
</button>
|
|
462
527
|
)}
|
|
463
|
-
<button type="button" className="dsh-atb-detail-edit" onClick={() => controller.openEditor(task.id)}
|
|
528
|
+
<button type="button" className="dsh-atb-detail-edit" onClick={() => controller.openEditor(task.id)}>{t('detail.action.edit')}</button>
|
|
464
529
|
<button
|
|
465
530
|
type="button"
|
|
466
531
|
className="dsh-atb-detail-edit"
|
|
467
|
-
title=
|
|
532
|
+
title={t('detail.action.duplicateTitle')}
|
|
468
533
|
disabled={actionBusy}
|
|
469
534
|
onClick={() => runAction(() => controller.duplicate(task))}
|
|
470
535
|
>
|
|
471
|
-
|
|
536
|
+
{t('detail.action.duplicate')}
|
|
472
537
|
</button>
|
|
473
538
|
<button
|
|
474
539
|
type="button"
|
|
475
540
|
className="dsh-atb-detail-edit"
|
|
476
|
-
title=
|
|
541
|
+
title={t('detail.action.saveTplTitle')}
|
|
477
542
|
disabled={actionBusy}
|
|
478
543
|
onClick={() => runAction(async () => {
|
|
479
544
|
const ok = await controller.saveAsTemplate(task)
|
|
480
|
-
if (ok) showAlert('
|
|
545
|
+
if (ok) showAlert(t('detail.action.saveTplDone'))
|
|
481
546
|
})}
|
|
482
547
|
>
|
|
483
|
-
|
|
548
|
+
{t('detail.action.saveTpl')}
|
|
484
549
|
</button>
|
|
485
550
|
{canRun && task.branch !== undefined && (
|
|
486
551
|
<button
|
|
487
552
|
type="button"
|
|
488
553
|
className="dsh-atb-detail-run"
|
|
489
|
-
title=
|
|
554
|
+
title={t('detail.action.reuseTitle')}
|
|
490
555
|
disabled={actionBusy}
|
|
491
556
|
onClick={() => runAction(() => controller.run(task.id, true))}
|
|
492
557
|
>
|
|
493
|
-
|
|
558
|
+
{t('detail.action.reuse')}
|
|
494
559
|
</button>
|
|
495
560
|
)}
|
|
496
561
|
{canRun && (
|
|
497
562
|
<button
|
|
498
563
|
type="button"
|
|
499
564
|
className="dsh-atb-detail-run"
|
|
500
|
-
title={task.model !== undefined ?
|
|
565
|
+
title={task.model !== undefined ? t('detail.action.runTitleModel', { model: task.model.model }) : t('detail.action.runTitleDefault')}
|
|
501
566
|
disabled={actionBusy}
|
|
502
567
|
onClick={() => runAction(() => controller.run(task.id))}
|
|
503
568
|
>
|
|
504
|
-
|
|
569
|
+
{t('detail.action.run')}
|
|
505
570
|
</button>
|
|
506
571
|
)}
|
|
507
572
|
{runningExecution !== undefined && (confirmCancel
|
|
508
573
|
? (
|
|
509
574
|
<span className="dsh-atb-confirm">
|
|
510
|
-
<span className="dsh-atb-confirm-label"
|
|
511
|
-
<button type="button" className="dsh-atb-btn" data-danger="true" onClick={() => { void controller.cancel(task.id); setConfirmCancel(false) }}
|
|
512
|
-
<button type="button" className="dsh-atb-btn" onClick={() => setConfirmCancel(false)}
|
|
575
|
+
<span className="dsh-atb-confirm-label">{t('detail.action.stopConfirm')}</span>
|
|
576
|
+
<button type="button" className="dsh-atb-btn" data-danger="true" onClick={() => { void controller.cancel(task.id); setConfirmCancel(false) }}>{t('detail.action.stop')}</button>
|
|
577
|
+
<button type="button" className="dsh-atb-btn" onClick={() => setConfirmCancel(false)}>{t('shared.cancel')}</button>
|
|
513
578
|
</span>
|
|
514
579
|
)
|
|
515
580
|
: (
|
|
@@ -517,27 +582,27 @@ export function TaskDetail({ task, controller, now }: { task: TaskRecord; contro
|
|
|
517
582
|
type="button"
|
|
518
583
|
className="dsh-atb-detail-run"
|
|
519
584
|
data-danger="true"
|
|
520
|
-
title={
|
|
585
|
+
title={t('detail.action.stopTitle', { id: runningExecution.sessionId ?? '' })}
|
|
521
586
|
onClick={() => setConfirmCancel(true)}
|
|
522
587
|
>
|
|
523
|
-
|
|
588
|
+
{t('detail.action.stopExec')}
|
|
524
589
|
</button>
|
|
525
590
|
))}
|
|
526
|
-
<button type="button" className="dsh-atb-detail-close" aria-label=
|
|
591
|
+
<button type="button" className="dsh-atb-detail-close" aria-label={t('shared.close')} onClick={() => controller.select(undefined)}>✕</button>
|
|
527
592
|
</div>
|
|
528
593
|
</div>
|
|
529
594
|
|
|
530
595
|
{task.description.length > 0 && (
|
|
531
596
|
<div className="dsh-atb-fieldcard">
|
|
532
|
-
<div className="dsh-atb-fieldcard-label"
|
|
533
|
-
<div className="dsh-atb-desc"
|
|
597
|
+
<div className="dsh-atb-fieldcard-label">{t('detail.field.description')}</div>
|
|
598
|
+
<div className="dsh-atb-desc"><MarkdownContent text={task.description} /></div>
|
|
534
599
|
</div>
|
|
535
600
|
)}
|
|
536
601
|
|
|
537
602
|
{task.prompt.length > 0 && (
|
|
538
603
|
<div className="dsh-atb-fieldcard" data-kind="prompt">
|
|
539
|
-
<div className="dsh-atb-fieldcard-label"
|
|
540
|
-
<div className="dsh-atb-promptbox"
|
|
604
|
+
<div className="dsh-atb-fieldcard-label">{t('detail.field.prompt')}</div>
|
|
605
|
+
<div className="dsh-atb-promptbox"><MarkdownContent text={task.prompt} /></div>
|
|
541
606
|
</div>
|
|
542
607
|
)}
|
|
543
608
|
|
|
@@ -554,39 +619,39 @@ export function TaskDetail({ task, controller, now }: { task: TaskRecord; contro
|
|
|
554
619
|
? (
|
|
555
620
|
<span key={to} className="dsh-atb-confirm">
|
|
556
621
|
<span className="dsh-atb-confirm-label" data-tone={unchecked > 0 ? 'bad' : undefined}>
|
|
557
|
-
{unchecked > 0 ?
|
|
622
|
+
{unchecked > 0 ? t('detail.move.confirmDoneUnchecked', { n: unchecked }) : t('detail.move.confirmDone')}
|
|
558
623
|
</span>
|
|
559
|
-
<button type="button" className="dsh-atb-btn" data-primary="true" onClick={() => { void controller.move(task.id, task.version, 'done'); setConfirmDone(false) }}
|
|
560
|
-
<button type="button" className="dsh-atb-btn" onClick={() => setConfirmDone(false)}
|
|
624
|
+
<button type="button" className="dsh-atb-btn" data-primary="true" onClick={() => { void controller.move(task.id, task.version, 'done'); setConfirmDone(false) }}>{t('detail.move.confirm')}</button>
|
|
625
|
+
<button type="button" className="dsh-atb-btn" onClick={() => setConfirmDone(false)}>{t('shared.cancel')}</button>
|
|
561
626
|
</span>
|
|
562
627
|
)
|
|
563
|
-
: <button key={to} type="button" className="dsh-atb-movebtn" data-to={to} onClick={() => setConfirmDone(true)}
|
|
628
|
+
: <button key={to} type="button" className="dsh-atb-movebtn" data-to={to} onClick={() => setConfirmDone(true)}>{t('detail.move.to', { status: t(MOVE_KEYS[to]) })}</button>)
|
|
564
629
|
: (
|
|
565
630
|
<button key={to} type="button" className="dsh-atb-movebtn" data-to={to} onClick={() => void controller.move(task.id, task.version, to)}>
|
|
566
|
-
|
|
631
|
+
{t('detail.move.to', { status: t(MOVE_KEYS[to]) })}
|
|
567
632
|
</button>
|
|
568
633
|
))}
|
|
569
634
|
<button type="button" className="dsh-atb-movebtn" data-to="blocked" onClick={() => void controller.toggleBlocked(task)}>
|
|
570
|
-
{task.blocked ? '
|
|
635
|
+
{task.blocked ? t('detail.blocked.unmark') : t('detail.blocked.mark')}
|
|
571
636
|
</button>
|
|
572
637
|
{holder !== undefined && (
|
|
573
638
|
<button
|
|
574
639
|
type="button"
|
|
575
640
|
className="dsh-atb-movebtn"
|
|
576
641
|
data-to="release"
|
|
577
|
-
title={
|
|
642
|
+
title={t('detail.release.title', { id: holder })}
|
|
578
643
|
onClick={() => void controller.move(task.id, task.version, 'todo')}
|
|
579
644
|
>
|
|
580
|
-
|
|
645
|
+
{t('detail.release.button')}
|
|
581
646
|
</button>
|
|
582
647
|
)}
|
|
583
648
|
</div>
|
|
584
649
|
</div>
|
|
585
650
|
|
|
586
651
|
<div className="dsh-atb-section">
|
|
587
|
-
<h4
|
|
652
|
+
<h4>{t('detail.comments.title')}{task.comments.length > 0 && <span className="dsh-atb-count2">{task.comments.length}</span>}</h4>
|
|
588
653
|
{task.comments.length === 0
|
|
589
|
-
? <div className="dsh-atb-empty2"
|
|
654
|
+
? <div className="dsh-atb-empty2">{t('detail.comments.empty')}</div>
|
|
590
655
|
: (
|
|
591
656
|
<div className="dsh-atb-commentlist">
|
|
592
657
|
{task.comments.map(c => (
|
|
@@ -594,7 +659,7 @@ export function TaskDetail({ task, controller, now }: { task: TaskRecord; contro
|
|
|
594
659
|
<div className="dsh-atb-bubble-avatar">{c.threadId !== undefined ? '🤖' : '👤'}</div>
|
|
595
660
|
<div className="dsh-atb-bubble-main">
|
|
596
661
|
<div className="dsh-atb-bubble-meta">
|
|
597
|
-
<b>{c.threadId !== undefined ? `agent ${shortId(c.threadId)}` : '
|
|
662
|
+
<b>{c.threadId !== undefined ? `agent ${shortId(c.threadId)}` : t('detail.comments.user')}</b>
|
|
598
663
|
<span>{fmtTime(c.createdAt)}</span>
|
|
599
664
|
</div>
|
|
600
665
|
<div className="dsh-atb-bubble-body">{c.body}</div>
|
|
@@ -607,7 +672,7 @@ export function TaskDetail({ task, controller, now }: { task: TaskRecord; contro
|
|
|
607
672
|
<textarea
|
|
608
673
|
className="dsh-atb-composer-input"
|
|
609
674
|
value={comment}
|
|
610
|
-
placeholder=
|
|
675
|
+
placeholder={t('detail.composer.placeholder')}
|
|
611
676
|
onChange={e => setComment(e.target.value)}
|
|
612
677
|
onKeyDown={e => {
|
|
613
678
|
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter' && comment.trim().length > 0) {
|
|
@@ -624,30 +689,30 @@ export function TaskDetail({ task, controller, now }: { task: TaskRecord; contro
|
|
|
624
689
|
void controller.comment(task.id, comment).then(ok => { if (ok) setComment('') })
|
|
625
690
|
}}
|
|
626
691
|
>
|
|
627
|
-
|
|
692
|
+
{t('detail.composer.send')}
|
|
628
693
|
</button>
|
|
629
694
|
</div>
|
|
630
695
|
</div>
|
|
631
696
|
|
|
632
697
|
{task.executions.length > 0 && (
|
|
633
698
|
<div className="dsh-atb-section">
|
|
634
|
-
<h4
|
|
699
|
+
<h4>{t('detail.exec.title')}<span className="dsh-atb-count2">{task.executions.length}</span>
|
|
635
700
|
{task.executionsPruned !== undefined && task.executionsPruned > 0 && (
|
|
636
|
-
<span className="dsh-atb-count2" title={
|
|
701
|
+
<span className="dsh-atb-count2" title={t('detail.exec.prunedTitle', { n: task.executionsPruned })}>{t('detail.exec.pruned', { n: task.executionsPruned })}</span>
|
|
637
702
|
)}
|
|
638
703
|
</h4>
|
|
639
704
|
<div className="dsh-atb-execlist">
|
|
640
705
|
{[...task.executions].reverse().map(e => (
|
|
641
706
|
<div key={e.id} className="dsh-atb-exec-row">
|
|
642
707
|
<span className="dsh-atb-exec-dot" data-outcome={e.outcome} />
|
|
643
|
-
<span className="dsh-atb-exec-trigger">{e.trigger === 'manual' ? '
|
|
644
|
-
<span className="dsh-atb-exec-outcome" data-outcome={e.outcome}>{
|
|
708
|
+
<span className="dsh-atb-exec-trigger">{e.trigger === 'manual' ? t('detail.exec.trigger.manual') : t('detail.exec.trigger.scheduled')}</span>
|
|
709
|
+
<span className="dsh-atb-exec-outcome" data-outcome={e.outcome}>{t(OUTCOME_KEYS[e.outcome] ?? e.outcome)}</span>
|
|
645
710
|
<span className="dsh-atb-exec-time">{fmtTime(e.startedAt)}{e.endedAt !== undefined && ` · ${duration(e.startedAt, e.endedAt)}`}</span>
|
|
646
711
|
{e.sessionId !== undefined && (
|
|
647
712
|
<button
|
|
648
713
|
type="button"
|
|
649
714
|
className="dsh-atb-exec-session"
|
|
650
|
-
title={
|
|
715
|
+
title={t('detail.exec.openTitle', { id: e.sessionId })}
|
|
651
716
|
onClick={() => jumpToSession(e.sessionId!)}
|
|
652
717
|
>
|
|
653
718
|
🤖 {shortId(e.sessionId)} ↗
|
|
@@ -662,16 +727,16 @@ export function TaskDetail({ task, controller, now }: { task: TaskRecord; contro
|
|
|
662
727
|
|
|
663
728
|
<div className="dsh-atb-dangerzone">
|
|
664
729
|
{task.trashedAt === undefined
|
|
665
|
-
? <button type="button" className="dsh-atb-btn" data-danger="true" onClick={() => void controller.remove(task.id, task.version, false)}
|
|
730
|
+
? <button type="button" className="dsh-atb-btn" data-danger="true" onClick={() => void controller.remove(task.id, task.version, false)}>{t('detail.danger.delete')}</button>
|
|
666
731
|
: (confirmPurge
|
|
667
732
|
? (
|
|
668
733
|
<span className="dsh-atb-confirm">
|
|
669
|
-
<span className="dsh-atb-confirm-label"
|
|
670
|
-
<button type="button" className="dsh-atb-btn" data-danger="true" onClick={() => { void controller.remove(task.id, task.version, true); setConfirmPurge(false) }}
|
|
671
|
-
<button type="button" className="dsh-atb-btn" onClick={() => setConfirmPurge(false)}
|
|
734
|
+
<span className="dsh-atb-confirm-label">{t('detail.danger.purgeConfirm')}</span>
|
|
735
|
+
<button type="button" className="dsh-atb-btn" data-danger="true" onClick={() => { void controller.remove(task.id, task.version, true); setConfirmPurge(false) }}>{t('detail.danger.purgeGo')}</button>
|
|
736
|
+
<button type="button" className="dsh-atb-btn" onClick={() => setConfirmPurge(false)}>{t('shared.cancel')}</button>
|
|
672
737
|
</span>
|
|
673
738
|
)
|
|
674
|
-
: <button type="button" className="dsh-atb-btn" data-danger="true" onClick={() => setConfirmPurge(true)}
|
|
739
|
+
: <button type="button" className="dsh-atb-btn" data-danger="true" onClick={() => setConfirmPurge(true)}>{t('detail.danger.purge')}</button>)}
|
|
675
740
|
</div>
|
|
676
741
|
|
|
677
742
|
{alertEl}
|