@linxin666/dsh-client-ui-task-board 0.4.4 → 0.4.5
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.i18n.yaml +2 -2
- package/README.md +42 -11
- package/README.zh.md +42 -11
- package/lib/client.js +4958 -472
- package/lib/client.js.map +1 -1
- package/lib/index.js +2841 -121
- package/lib/types/client/PluginSettingsCard.d.ts +7 -0
- package/lib/types/client/TaskBoardSettingsCard.d.ts +19 -3
- package/lib/types/client/board/ClampedMarkdown.d.ts +3 -0
- package/lib/types/client/board/ConfirmDialog.d.ts +4 -1
- package/lib/types/client/board/EditTaskModal.d.ts +5 -2
- package/lib/types/client/board/LinkSubtaskModal.d.ts +5 -2
- package/lib/types/client/board/NewTaskModal.d.ts +4 -1
- package/lib/types/client/board/TagManagerModal.d.ts +26 -0
- package/lib/types/client/board/TaskBoard.d.ts +10 -1
- package/lib/types/client/board/TaskCard.d.ts +14 -1
- package/lib/types/client/board/TaskDetail.d.ts +3 -1
- package/lib/types/client/board/TaskForm.d.ts +4 -1
- package/lib/types/client/board/VerificationReport.d.ts +23 -0
- package/lib/types/client/board/card-view.d.ts +113 -0
- package/lib/types/client/board/icons.d.ts +32 -0
- package/lib/types/client/board/overlay.d.ts +69 -0
- package/lib/types/client/board/status-key.d.ts +15 -0
- package/lib/types/client/board/task-markdown.d.ts +19 -0
- package/lib/types/client/board-card-seat.d.ts +34 -0
- package/lib/types/client/index.d.ts +19 -0
- package/lib/types/client/locales.d.ts +103 -1
- package/lib/types/client/native-panel.d.ts +6 -20
- package/lib/types/client/seats.d.ts +18 -0
- package/lib/types/client/service.d.ts +32 -0
- package/lib/types/client/settings-form.d.ts +16 -0
- package/lib/types/core/controller.d.ts +37 -1
- package/lib/types/core/extension.d.ts +283 -0
- package/lib/types/core/handover.d.ts +13 -1
- package/lib/types/core/issue-form.d.ts +16 -0
- package/lib/types/core/tasks.d.ts +84 -3
- package/lib/types/core/use-cases/task-tag.d.ts +48 -0
- package/lib/types/core/use-cases/task-update.d.ts +5 -3
- package/lib/types/core/verification.d.ts +386 -0
- package/lib/types/core/workspace-target.d.ts +41 -0
- package/lib/types/host/agent-tools.d.ts +1 -1
- package/lib/types/host/extension-registry.d.ts +111 -0
- package/lib/types/host/llm-dispatch.d.ts +63 -0
- package/lib/types/host/verification-gate.d.ts +71 -0
- package/lib/types/host/verification-runner.d.ts +108 -0
- package/lib/types/host/workspace-evidence.d.ts +53 -0
- package/lib/types/host-ledger.d.ts +52 -8
- package/lib/types/host-runner.d.ts +23 -1
- package/lib/types/host-service.d.ts +88 -2
- package/lib/types/index.d.ts +80 -26
- package/lib/types/protocol.d.ts +49 -5
- package/lib/types/tool-surface.d.ts +71 -0
- package/package.json +27 -26
- package/src/client/PluginSettingsCard.tsx +9 -0
- package/src/client/TaskBoardSettingsCard.tsx +304 -65
- package/src/client/board/ClampedMarkdown.tsx +69 -0
- package/src/client/board/ConfirmDialog.tsx +7 -3
- package/src/client/board/EditTaskModal.tsx +5 -2
- package/src/client/board/LinkSubtaskModal.tsx +12 -7
- package/src/client/board/NewTaskModal.tsx +17 -1
- package/src/client/board/TagManagerModal.tsx +235 -0
- package/src/client/board/TaskBoard.tsx +259 -103
- package/src/client/board/TaskCard.tsx +214 -81
- package/src/client/board/TaskDetail.tsx +71 -26
- package/src/client/board/TaskForm.tsx +13 -3
- package/src/client/board/VerificationReport.tsx +203 -0
- package/src/client/board/card-view.ts +220 -0
- package/src/client/board/icons.tsx +89 -0
- package/src/client/board/overlay.tsx +189 -0
- package/src/client/board/status-key.ts +27 -0
- package/src/client/board/task-markdown.module.css +137 -0
- package/src/client/board/task-markdown.tsx +106 -0
- package/src/client/board-card-seat.ts +102 -0
- package/src/client/board-settings.module.css +66 -0
- package/src/client/board.module.css +488 -63
- package/src/client/index.ts +43 -5
- package/src/client/locales.ts +208 -4
- package/src/client/native-panel.tsx +18 -2
- package/src/client/seats.tsx +58 -0
- package/src/client/service.ts +94 -0
- package/src/client/settings-card.module.css +31 -5
- package/src/client/settings-form.ts +27 -1
- package/src/core/controller.ts +94 -3
- package/src/core/extension.ts +376 -0
- package/src/core/freeze-snapshot.ts +2 -2
- package/src/core/handover.ts +16 -1
- package/src/core/issue-form.ts +19 -0
- package/src/core/store.ts +16 -0
- package/src/core/tasks.ts +100 -6
- package/src/core/use-cases/task-parent.ts +3 -2
- package/src/core/use-cases/task-tag.ts +110 -0
- package/src/core/use-cases/task-update.ts +10 -4
- package/src/core/verification.ts +826 -0
- package/src/core/workspace-target.ts +59 -0
- package/src/host/agent-tools.ts +20 -18
- package/src/host/extension-registry.ts +509 -0
- package/src/host/llm-dispatch.ts +87 -0
- package/src/host/verification-gate.ts +331 -0
- package/src/host/verification-runner.ts +535 -0
- package/src/host/workspace-evidence.ts +176 -0
- package/src/host-ai.ts +15 -9
- package/src/host-ledger.ts +143 -32
- package/src/host-routes.ts +15 -2
- package/src/host-runner.ts +60 -11
- package/src/host-service.ts +253 -10
- package/src/index.ts +204 -16
- package/src/protocol.ts +102 -8
- package/src/tool-surface.ts +95 -0
|
@@ -7,9 +7,10 @@
|
|
|
7
7
|
import { useMemo, useState } from 'react'
|
|
8
8
|
import type { BoardController } from '../../core/controller.ts'
|
|
9
9
|
import { DEFAULT_SUBTASK_DEPTH, buildLineageIndex, checkParentLink } from '../../core/subtask.ts'
|
|
10
|
-
import type
|
|
10
|
+
import { hasOpenExecution, type TaskRecord } from '../../core/tasks.ts'
|
|
11
11
|
import { t } from '../locales.ts'
|
|
12
12
|
import css from '../board.module.css'
|
|
13
|
+
import { useDialog, type OverlayPhase } from './overlay.tsx'
|
|
13
14
|
import { STATUS_KEY } from './status-key.ts'
|
|
14
15
|
|
|
15
16
|
/** Link-subtask overlay props. */
|
|
@@ -18,10 +19,13 @@ export interface LinkSubtaskModalProps {
|
|
|
18
19
|
/** The task the picked card becomes a subtask of. */
|
|
19
20
|
parent: TaskRecord
|
|
20
21
|
onClose: () => void
|
|
22
|
+
/** Which leg of the enter/exit motion pair the surface is on. */
|
|
23
|
+
phase?: OverlayPhase
|
|
21
24
|
}
|
|
22
25
|
|
|
23
26
|
/** Link-subtask overlay. */
|
|
24
|
-
export function LinkSubtaskModal({ controller, parent, onClose }: LinkSubtaskModalProps) {
|
|
27
|
+
export function LinkSubtaskModal({ controller, parent, onClose, phase = 'open' }: LinkSubtaskModalProps) {
|
|
28
|
+
const dialog = useDialog<HTMLDivElement>(onClose, phase)
|
|
25
29
|
const [pending, setPending] = useState(false)
|
|
26
30
|
const [error, setError] = useState<string | undefined>(undefined)
|
|
27
31
|
const snapshot = controller.getSnapshot()
|
|
@@ -39,9 +43,10 @@ export function LinkSubtaskModal({ controller, parent, onClose }: LinkSubtaskMod
|
|
|
39
43
|
const lineage = buildLineageIndex(tasks)
|
|
40
44
|
return tasks.filter(task =>
|
|
41
45
|
task.archivedAt === undefined
|
|
42
|
-
//
|
|
43
|
-
// Host refuses the move, so it is not offered here.
|
|
44
|
-
|
|
46
|
+
// An executing task may already be a participant of another run group;
|
|
47
|
+
// the Host refuses the move, so it is not offered here. A card parked in
|
|
48
|
+
// the running column by hand carries no execution and stays linkable.
|
|
49
|
+
&& !hasOpenExecution(task)
|
|
45
50
|
&& task.parentId === undefined
|
|
46
51
|
&& task.id !== parent.id
|
|
47
52
|
&& checkParentLink(tasks, task.id, parent.id, limit, lineage).ok,
|
|
@@ -58,8 +63,8 @@ export function LinkSubtaskModal({ controller, parent, onClose }: LinkSubtaskMod
|
|
|
58
63
|
setError(controller.getSnapshot().transportError ?? t('new.required'))
|
|
59
64
|
}
|
|
60
65
|
return (
|
|
61
|
-
<div className={css.modalBackdrop}
|
|
62
|
-
<div className={css.modal} role="dialog" aria-label={t('link.subtask.title')}>
|
|
66
|
+
<div className={css.modalBackdrop} data-state={phase} onMouseDown={dialog.onMouseDown}>
|
|
67
|
+
<div ref={dialog.attach} className={css.modal} role="dialog" aria-modal="true" aria-label={t('link.subtask.title')} tabIndex={-1}>
|
|
63
68
|
<h2 className={css.modalTitle}>{t('link.subtask.title')}</h2>
|
|
64
69
|
<p className={css.fieldHint}>{t('link.subtask.hint')}</p>
|
|
65
70
|
{candidates.length === 0
|
|
@@ -12,6 +12,7 @@ import { t, type TaskBoardKey } from '../locales.ts'
|
|
|
12
12
|
import { SCHEDULE_PRESETS } from '../schedule-presets.ts'
|
|
13
13
|
import { nextRunLabel, zoneChoices } from '../schedule-zone.ts'
|
|
14
14
|
import { CollapsibleSection, ModalShell, TaskContentFields, TaskTagFields, cleanTags } from './TaskForm.tsx'
|
|
15
|
+
import type { OverlayPhase } from './overlay.tsx'
|
|
15
16
|
import { formatHostTimestamp } from './TaskCard.tsx'
|
|
16
17
|
import { readParseModelPreference, writeParseModelPreference } from './parse-model-pref.ts'
|
|
17
18
|
import { inheritPresetLabel, isBuiltinPreset, presetLabel } from './preset-label.ts'
|
|
@@ -35,10 +36,12 @@ export interface NewTaskModalProps {
|
|
|
35
36
|
* carries the parent link; unset targets inherit again at run time.
|
|
36
37
|
*/
|
|
37
38
|
parentTask?: TaskRecord
|
|
39
|
+
/** Which leg of the enter/exit motion pair the surface is on. */
|
|
40
|
+
phase?: OverlayPhase
|
|
38
41
|
}
|
|
39
42
|
|
|
40
43
|
/** New-task form overlay. */
|
|
41
|
-
export function NewTaskModal({ controller, onClose, initialTask, defaultWorkspaceId, onDuplicateSuccess, parentTask }: NewTaskModalProps) {
|
|
44
|
+
export function NewTaskModal({ controller, onClose, initialTask, defaultWorkspaceId, onDuplicateSuccess, parentTask, phase }: NewTaskModalProps) {
|
|
42
45
|
const isDuplicate = initialTask !== undefined
|
|
43
46
|
const [title, setTitle] = useState(initialTask?.title ?? '')
|
|
44
47
|
const [description, setDescription] = useState(initialTask?.description ?? '')
|
|
@@ -57,6 +60,7 @@ export function NewTaskModal({ controller, onClose, initialTask, defaultWorkspac
|
|
|
57
60
|
// Checked by default: a new task starts its runs with dsh's built-in /goal
|
|
58
61
|
// unless the user opts out here. A duplicate keeps the original card's choice.
|
|
59
62
|
const [goalRun, setGoalRun] = useState(initialTask?.goalRun ?? true)
|
|
63
|
+
const [skipVerification, setSkipVerification] = useState(initialTask?.skipVerification ?? false)
|
|
60
64
|
const [scheduleEnabled, setScheduleEnabled] = useState(initialTask?.schedule?.enabled ?? false)
|
|
61
65
|
const [scheduleCron, setScheduleCron] = useState(initialTask?.schedule?.cron ?? '')
|
|
62
66
|
// '' means "follow the Host zone" (store no zone), which is the default a
|
|
@@ -185,6 +189,7 @@ export function NewTaskModal({ controller, onClose, initialTask, defaultWorkspac
|
|
|
185
189
|
model: model === '' ? undefined : model,
|
|
186
190
|
...(reuseSession ? { reuseSession: true } : {}),
|
|
187
191
|
...(goalRun ? {} : { goalRun: false }),
|
|
192
|
+
...(skipVerification ? { skipVerification: true } : {}),
|
|
188
193
|
...(tagList.length > 0 ? { tags: tagList } : {}),
|
|
189
194
|
schedule: scheduleEnabled
|
|
190
195
|
? { enabled: true, cron: scheduleCron.trim(), ...(scheduleZone === '' ? {} : { timeZone: scheduleZone }) }
|
|
@@ -277,6 +282,7 @@ export function NewTaskModal({ controller, onClose, initialTask, defaultWorkspac
|
|
|
277
282
|
submitLabel={t('new.submit')}
|
|
278
283
|
onSubmit={() => { void submit(false) }}
|
|
279
284
|
onClose={onClose}
|
|
285
|
+
phase={phase}
|
|
280
286
|
secondaryAction={{ label: t('new.createAndRun'), onSubmit: () => { void submit(true) } }}
|
|
281
287
|
>
|
|
282
288
|
{canParse && (
|
|
@@ -466,6 +472,16 @@ export function NewTaskModal({ controller, onClose, initialTask, defaultWorkspac
|
|
|
466
472
|
<span>{t('exec.goalRun')}</span>
|
|
467
473
|
</label>
|
|
468
474
|
<p className={css.detailText}>{t('exec.goalRunHint')}</p>
|
|
475
|
+
|
|
476
|
+
<label className={css.scheduleToggle}>
|
|
477
|
+
<input
|
|
478
|
+
type="checkbox"
|
|
479
|
+
checked={skipVerification}
|
|
480
|
+
onChange={event => { setSkipVerification(event.target.checked) }}
|
|
481
|
+
/>
|
|
482
|
+
<span>{t('exec.skipVerification')}</span>
|
|
483
|
+
</label>
|
|
484
|
+
<p className={css.detailText}>{t('exec.skipVerificationHint')}</p>
|
|
469
485
|
</CollapsibleSection>
|
|
470
486
|
|
|
471
487
|
<CollapsibleSection
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Label manager: the board's label row can only shrink through here.
|
|
3
|
+
*
|
|
4
|
+
* The filter row is the union of every label in use across the ledger, so a
|
|
5
|
+
* label created once — by an experiment, a pasted task, an import — stayed on
|
|
6
|
+
* the board forever: there was no way to rename a typo, merge two labels that
|
|
7
|
+
* mean the same thing, or remove one nobody uses. This dialog is that way out.
|
|
8
|
+
* It lists the labels with their usage counts and offers the two ledger-wide
|
|
9
|
+
* transitions, both of which go through the controller so the Host ledger stays
|
|
10
|
+
* the authority (and a shared board stays consistent for everyone watching it).
|
|
11
|
+
*
|
|
12
|
+
* Renaming onto a name already in use merges the two labels; the dialog says so
|
|
13
|
+
* before the edit is submitted. Deleting is confirmed first, and the
|
|
14
|
+
* confirmation names how many tasks the label is removed from.
|
|
15
|
+
*/
|
|
16
|
+
import { useMemo, useState } from 'react'
|
|
17
|
+
import type { BoardController } from '../../core/controller.ts'
|
|
18
|
+
import { TAG_NAME_MAX_LENGTH, collectKnownTags, type TaskRecord } from '../../core/tasks.ts'
|
|
19
|
+
import { t } from '../locales.ts'
|
|
20
|
+
import css from '../board.module.css'
|
|
21
|
+
import { ConfirmDialog } from './ConfirmDialog.tsx'
|
|
22
|
+
import { IconClose } from './icons.tsx'
|
|
23
|
+
import { useDialog, usePresence, type OverlayPhase } from './overlay.tsx'
|
|
24
|
+
|
|
25
|
+
/** One label as the manager lists it: its definition plus how many tasks carry it. */
|
|
26
|
+
export interface TagUsage {
|
|
27
|
+
name: string
|
|
28
|
+
promptPrefix?: string
|
|
29
|
+
/** Tasks carrying the label right now. */
|
|
30
|
+
count: number
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Labels in use, in board order (first occurrence wins, exactly like the
|
|
35
|
+
* filter row), each with its usage count.
|
|
36
|
+
* @param tasks - the whole ledger (board and archive).
|
|
37
|
+
* @returns one row per label.
|
|
38
|
+
*/
|
|
39
|
+
export function tagUsage(tasks: readonly TaskRecord[]): TagUsage[] {
|
|
40
|
+
const counts = new Map<string, number>()
|
|
41
|
+
for (const task of tasks) {
|
|
42
|
+
for (const tag of task.tags ?? []) counts.set(tag.name, (counts.get(tag.name) ?? 0) + 1)
|
|
43
|
+
}
|
|
44
|
+
return collectKnownTags(tasks).map(tag => ({
|
|
45
|
+
name: tag.name,
|
|
46
|
+
...(tag.promptPrefix === undefined ? {} : { promptPrefix: tag.promptPrefix }),
|
|
47
|
+
count: counts.get(tag.name) ?? 0,
|
|
48
|
+
}))
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Label-manager overlay props. */
|
|
52
|
+
export interface TagManagerModalProps {
|
|
53
|
+
controller: BoardController
|
|
54
|
+
onClose: () => void
|
|
55
|
+
/** Which leg of the enter/exit motion pair the surface is on. */
|
|
56
|
+
phase?: OverlayPhase
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Label-manager overlay. */
|
|
60
|
+
export function TagManagerModal({ controller, onClose, phase = 'open' }: TagManagerModalProps) {
|
|
61
|
+
/** The label whose row is being renamed, and the draft name. */
|
|
62
|
+
const [editing, setEditing] = useState<string | undefined>(undefined)
|
|
63
|
+
const [draft, setDraft] = useState('')
|
|
64
|
+
const [busy, setBusy] = useState(false)
|
|
65
|
+
const [error, setError] = useState<string | undefined>(undefined)
|
|
66
|
+
const [confirmDelete, setConfirmDelete] = useState<string | undefined>(undefined)
|
|
67
|
+
const confirmPresence = usePresence(confirmDelete !== undefined)
|
|
68
|
+
// Escape and a backdrop press peel one layer at a time: an open row editor
|
|
69
|
+
// first, then the dialog — so a half-typed rename is never lost to the same
|
|
70
|
+
// key that closes the surface under it.
|
|
71
|
+
const dismiss = (): void => {
|
|
72
|
+
if (editing !== undefined) {
|
|
73
|
+
setEditing(undefined)
|
|
74
|
+
setError(undefined)
|
|
75
|
+
return
|
|
76
|
+
}
|
|
77
|
+
onClose()
|
|
78
|
+
}
|
|
79
|
+
const dialog = useDialog<HTMLDivElement>(dismiss, phase)
|
|
80
|
+
const snapshot = controller.getSnapshot()
|
|
81
|
+
const rows = useMemo(() => tagUsage(snapshot.tasks), [snapshot.tasks])
|
|
82
|
+
const deleting = rows.find(row => row.name === confirmDelete)
|
|
83
|
+
/** The label the typed name would merge into, when it is already in use. */
|
|
84
|
+
const mergesInto = editing === undefined ? undefined : rows.find(row => row.name === draft.trim() && row.name !== editing)
|
|
85
|
+
|
|
86
|
+
const startRename = (row: TagUsage): void => {
|
|
87
|
+
setEditing(row.name)
|
|
88
|
+
setDraft(row.name)
|
|
89
|
+
setError(undefined)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const submitRename = async (from: string): Promise<void> => {
|
|
93
|
+
const to = draft.trim()
|
|
94
|
+
if (to === '' || to === from) {
|
|
95
|
+
setEditing(undefined)
|
|
96
|
+
return
|
|
97
|
+
}
|
|
98
|
+
setBusy(true)
|
|
99
|
+
setError(undefined)
|
|
100
|
+
const accepted = await controller.renameTag(from, to)
|
|
101
|
+
setBusy(false)
|
|
102
|
+
if (accepted) {
|
|
103
|
+
setEditing(undefined)
|
|
104
|
+
return
|
|
105
|
+
}
|
|
106
|
+
setError(controller.getSnapshot().transportError ?? t('tags.renameFailed'))
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const remove = async (name: string): Promise<void> => {
|
|
110
|
+
setBusy(true)
|
|
111
|
+
setError(undefined)
|
|
112
|
+
const accepted = await controller.deleteTag(name)
|
|
113
|
+
setBusy(false)
|
|
114
|
+
if (!accepted) setError(controller.getSnapshot().transportError ?? t('tags.deleteFailed'))
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return (
|
|
118
|
+
<>
|
|
119
|
+
<div className={css.modalBackdrop} data-state={phase} onMouseDown={dialog.onMouseDown}>
|
|
120
|
+
<div
|
|
121
|
+
ref={dialog.attach}
|
|
122
|
+
className={css.modal}
|
|
123
|
+
role="dialog"
|
|
124
|
+
aria-modal="true"
|
|
125
|
+
aria-label={t('tags.title')}
|
|
126
|
+
tabIndex={-1}
|
|
127
|
+
>
|
|
128
|
+
<header className={css.modalHeader}>
|
|
129
|
+
<h2 className={css.modalTitle}>{t('tags.title')}</h2>
|
|
130
|
+
<button
|
|
131
|
+
type="button"
|
|
132
|
+
className={css.iconButton}
|
|
133
|
+
aria-label={t('detail.close')}
|
|
134
|
+
title={t('detail.close')}
|
|
135
|
+
onClick={onClose}
|
|
136
|
+
>
|
|
137
|
+
<IconClose size={16} />
|
|
138
|
+
</button>
|
|
139
|
+
</header>
|
|
140
|
+
|
|
141
|
+
<div className={css.modalBody}>
|
|
142
|
+
<p className={css.fieldHint}>{t('tags.hint')}</p>
|
|
143
|
+
{rows.length === 0
|
|
144
|
+
? <p className={css.detailText}>{t('tags.empty')}</p>
|
|
145
|
+
: (
|
|
146
|
+
<ul className={css.tagManageList}>
|
|
147
|
+
{rows.map(row => (
|
|
148
|
+
<li key={row.name} className={css.tagManageRow}>
|
|
149
|
+
{editing === row.name
|
|
150
|
+
? (
|
|
151
|
+
<>
|
|
152
|
+
<input
|
|
153
|
+
className={css.input + ' ' + css.tagManageInput}
|
|
154
|
+
value={draft}
|
|
155
|
+
maxLength={TAG_NAME_MAX_LENGTH}
|
|
156
|
+
spellCheck={false}
|
|
157
|
+
aria-label={t('tags.renameLabel', { name: row.name })}
|
|
158
|
+
onChange={event => { setDraft(event.target.value); setError(undefined) }}
|
|
159
|
+
onKeyDown={event => {
|
|
160
|
+
if (event.key === 'Enter') void submitRename(row.name)
|
|
161
|
+
}}
|
|
162
|
+
/>
|
|
163
|
+
<button
|
|
164
|
+
type="button"
|
|
165
|
+
className={css.primaryButton}
|
|
166
|
+
disabled={busy || draft.trim() === ''}
|
|
167
|
+
onClick={() => { void submitRename(row.name) }}
|
|
168
|
+
>
|
|
169
|
+
{t('tags.save')}
|
|
170
|
+
</button>
|
|
171
|
+
<button
|
|
172
|
+
type="button"
|
|
173
|
+
className={css.ghostButton}
|
|
174
|
+
disabled={busy}
|
|
175
|
+
onClick={() => { setEditing(undefined); setError(undefined) }}
|
|
176
|
+
>
|
|
177
|
+
{t('new.cancel')}
|
|
178
|
+
</button>
|
|
179
|
+
</>
|
|
180
|
+
)
|
|
181
|
+
: (
|
|
182
|
+
<>
|
|
183
|
+
<span className={css.tagManageName}>{row.name}</span>
|
|
184
|
+
<span className={css.tagManageCount}>{t('tags.usage', { count: String(row.count) })}</span>
|
|
185
|
+
<button
|
|
186
|
+
type="button"
|
|
187
|
+
className={css.ghostButton}
|
|
188
|
+
disabled={busy}
|
|
189
|
+
onClick={() => { startRename(row) }}
|
|
190
|
+
>
|
|
191
|
+
{t('tags.rename')}
|
|
192
|
+
</button>
|
|
193
|
+
<button
|
|
194
|
+
type="button"
|
|
195
|
+
className={css.ghostButton}
|
|
196
|
+
disabled={busy}
|
|
197
|
+
onClick={() => { setConfirmDelete(row.name) }}
|
|
198
|
+
>
|
|
199
|
+
{t('tags.delete')}
|
|
200
|
+
</button>
|
|
201
|
+
</>
|
|
202
|
+
)}
|
|
203
|
+
{editing === row.name && mergesInto !== undefined && (
|
|
204
|
+
<span className={css.tagManageMerge}>{t('tags.mergeHint', { name: mergesInto.name })}</span>
|
|
205
|
+
)}
|
|
206
|
+
</li>
|
|
207
|
+
))}
|
|
208
|
+
</ul>
|
|
209
|
+
)}
|
|
210
|
+
{error !== undefined && <p className={css.formError}>{error}</p>}
|
|
211
|
+
</div>
|
|
212
|
+
|
|
213
|
+
<footer className={css.modalFooter}>
|
|
214
|
+
<button type="button" className={css.ghostButton} onClick={onClose}>{t('tags.done')}</button>
|
|
215
|
+
</footer>
|
|
216
|
+
</div>
|
|
217
|
+
</div>
|
|
218
|
+
|
|
219
|
+
{confirmPresence.mounted && deleting !== undefined && (
|
|
220
|
+
<ConfirmDialog
|
|
221
|
+
title={t('tags.deleteTitle')}
|
|
222
|
+
message={t('tags.deleteConfirm', { name: deleting.name, count: String(deleting.count) })}
|
|
223
|
+
confirmLabel={t('tags.deleteOk')}
|
|
224
|
+
danger
|
|
225
|
+
phase={confirmPresence.phase}
|
|
226
|
+
onCancel={() => { setConfirmDelete(undefined) }}
|
|
227
|
+
onConfirm={() => {
|
|
228
|
+
setConfirmDelete(undefined)
|
|
229
|
+
void remove(deleting.name)
|
|
230
|
+
}}
|
|
231
|
+
/>
|
|
232
|
+
)}
|
|
233
|
+
</>
|
|
234
|
+
)
|
|
235
|
+
}
|