@linxin666/dsh-client-ui-task-board 0.1.14 → 0.1.16
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.js +368 -160
- package/lib/client.js.map +1 -1
- package/lib/types/client/PluginSettingsCard.d.ts +26 -11
- package/lib/types/client/board/TaskCard.d.ts +4 -5
- package/lib/types/client/settings-form.d.ts +76 -5
- package/lib/types/core/controller.d.ts +6 -1
- package/lib/types/core/scheduler.d.ts +13 -2
- package/lib/types/core/use-cases/task-create.d.ts +22 -0
- package/lib/types/core/use-cases/task-delete.d.ts +21 -0
- package/lib/types/core/use-cases/task-schedule.d.ts +34 -0
- package/lib/types/core/use-cases/task-update.d.ts +17 -0
- package/package.json +1 -1
- package/src/client/PluginSettingsCard.tsx +90 -18
- package/src/client/board/TaskBoard.tsx +14 -6
- package/src/client/board/TaskCard.tsx +10 -1
- package/src/client/settings-card.module.css +9 -0
- package/src/client/settings-form.ts +153 -19
- package/src/core/controller.ts +25 -26
- package/src/core/scheduler.ts +39 -7
- package/src/core/use-cases/task-create.ts +33 -0
- package/src/core/use-cases/task-delete.ts +33 -0
- package/src/core/use-cases/task-schedule.ts +74 -0
- package/src/core/use-cases/task-update.ts +26 -0
|
@@ -1,22 +1,27 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* plugin and what its settings govern, the controls inside, and the save
|
|
4
|
-
* writes them. Renders nothing while the namespace is unavailable — a
|
|
2
|
+
* Family-shared chrome for plugin settings cards: a disclosure header naming
|
|
3
|
+
* the plugin and what its settings govern, the controls inside, and the save
|
|
4
|
+
* that writes them. Renders nothing while the namespace is unavailable — a
|
|
5
5
|
* deployment that does not compose the owning plugin should show no trace of
|
|
6
|
-
* it.
|
|
7
|
-
*
|
|
6
|
+
* it. Inlined into each consumer's client bundle; mirrors the official
|
|
7
|
+
* ui-plugin-config PluginCard in a self-contained slice.
|
|
8
8
|
*/
|
|
9
9
|
import { type ReactNode } from 'react';
|
|
10
10
|
import type { CardShell } from './settings-form.ts';
|
|
11
|
-
|
|
11
|
+
/** Copy keys the card chrome itself reads; every consumer locale carries this shared vocabulary. */
|
|
12
|
+
export declare const CARD_COPY_KEYS: readonly ["settings.collapse", "settings.expand", "settings.notExposed", "settings.unsaved", "settings.readOnly", "settings.saveFailed", "settings.discard", "settings.save", "settings.saving"];
|
|
13
|
+
/** Copy key the card chrome itself reads. */
|
|
14
|
+
export type CardCopyKey = (typeof CARD_COPY_KEYS)[number];
|
|
15
|
+
/** Key domain for the plugin's own copy (inferred per consumer). */
|
|
16
|
+
export type SettingsCardKey<TKey extends string = string> = TKey | CardCopyKey;
|
|
12
17
|
/** Card chrome shared by every plugin settings card. */
|
|
13
|
-
export interface PluginSettingsCardProps {
|
|
18
|
+
export interface PluginSettingsCardProps<TKey extends string = string> {
|
|
14
19
|
/** Locale reader for this card's copy. */
|
|
15
|
-
t: (key: SettingsCardKey) => string;
|
|
20
|
+
t: (key: SettingsCardKey<TKey>, params?: Record<string, string | number>) => string;
|
|
16
21
|
/** Locale key of the plugin's name. */
|
|
17
|
-
titleKey:
|
|
22
|
+
titleKey: TKey;
|
|
18
23
|
/** Locale key of the line describing what this plugin's settings govern. */
|
|
19
|
-
descriptionKey:
|
|
24
|
+
descriptionKey: TKey;
|
|
20
25
|
/** The card's form state: availability, writability, and what a save would do. */
|
|
21
26
|
state: CardShell;
|
|
22
27
|
/** Write every staged edit. */
|
|
@@ -31,7 +36,7 @@ export interface PluginSettingsCardProps {
|
|
|
31
36
|
* @param props - the plugin's copy keys, its form state, and its controls.
|
|
32
37
|
* @returns the card, or nothing while the namespace is still loading.
|
|
33
38
|
*/
|
|
34
|
-
export declare function PluginSettingsCard(props: PluginSettingsCardProps): import("react").JSX.Element | null;
|
|
39
|
+
export declare function PluginSettingsCard<TKey extends string = string>(props: PluginSettingsCardProps<TKey>): import("react").JSX.Element | null;
|
|
35
40
|
/** Props every field control needs regardless of its value type. */
|
|
36
41
|
export interface FieldProps {
|
|
37
42
|
/** Stable id associating the label with its control. */
|
|
@@ -75,3 +80,13 @@ export declare function BooleanField(props: FieldProps & {
|
|
|
75
80
|
/** Copy for the off option. */
|
|
76
81
|
offLabel: string;
|
|
77
82
|
}): import("react").JSX.Element;
|
|
83
|
+
/** A staged enumerated field rendered as a select. */
|
|
84
|
+
export declare function ChoiceField(props: FieldProps & {
|
|
85
|
+
/** Copy for the inherit option (draft text is the empty string). */
|
|
86
|
+
inheritLabel: string;
|
|
87
|
+
/** Choices rendered in order; `value` is the draft/stored text. */
|
|
88
|
+
choices: ReadonlyArray<{
|
|
89
|
+
value: string;
|
|
90
|
+
label: string;
|
|
91
|
+
}>;
|
|
92
|
+
}): import("react").JSX.Element;
|
|
@@ -1,12 +1,11 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Task card: the board's column item. Clicking opens the task detail — it
|
|
3
|
-
* never executes anything directly (detail holds the Run button).
|
|
4
|
-
*/
|
|
5
1
|
import type { TaskRecord } from '../../core/tasks.ts';
|
|
6
2
|
/** Compact relative/absolute time label. */
|
|
7
3
|
export declare function formatTime(ms: number): string;
|
|
8
4
|
/** One card in a column. */
|
|
9
|
-
|
|
5
|
+
declare function TaskCardInner({ task, onClick }: {
|
|
10
6
|
task: TaskRecord;
|
|
11
7
|
onClick: () => void;
|
|
12
8
|
}): import("react").JSX.Element;
|
|
9
|
+
/** Memoized card: re-renders only when the card's own task record changes. */
|
|
10
|
+
export declare const TaskCard: import("react").MemoExoticComponent<typeof TaskCardInner>;
|
|
11
|
+
export {};
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
* Staged form model behind the plugin settings card. A card stages what the
|
|
3
3
|
* user types and writes it only when they save — the settings write is a
|
|
4
4
|
* durable, revision-fenced document mutation, so staging keeps what is on
|
|
5
|
-
* screen exactly what a save would store.
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* screen exactly what a save would store. Family-shared slice inlined into
|
|
6
|
+
* each plugin's client bundle; mirrors the official ui-plugin-config
|
|
7
|
+
* card-store pattern.
|
|
8
8
|
*/
|
|
9
9
|
import type { SettingsScope, SnapshotStore } from '@deepseek-ai/dsh-client-runtime/client';
|
|
10
10
|
/** The write one field's staged text performs when the card is saved. */
|
|
@@ -18,6 +18,14 @@ export type FieldWrite = {
|
|
|
18
18
|
export interface FieldSpec {
|
|
19
19
|
/** Field name inside the namespace section. */
|
|
20
20
|
field: string;
|
|
21
|
+
/**
|
|
22
|
+
* Whether the Host treats this field as a secret and redacts its value from
|
|
23
|
+
* the read-back (role('secret') in the section schema). Redacted secrets are
|
|
24
|
+
* never compared against the draft on save; the field lands when the scope
|
|
25
|
+
* reports the write succeeded (its secret-set marker under the bridge), so
|
|
26
|
+
* a successful secret save is not misreported as failed.
|
|
27
|
+
*/
|
|
28
|
+
secret?: boolean;
|
|
21
29
|
/** Render a stored value as draft text; the empty string when the section carries none. */
|
|
22
30
|
format: (value: unknown) => string;
|
|
23
31
|
/**
|
|
@@ -57,6 +65,12 @@ export interface CardShell {
|
|
|
57
65
|
saving: boolean;
|
|
58
66
|
/** Whether the last save did not land as staged; cleared by the next edit or save. */
|
|
59
67
|
failed: boolean;
|
|
68
|
+
/**
|
|
69
|
+
* The rejection code/message the Host returned for the last failed save,
|
|
70
|
+
* surfaced next to the generic failure text. Undefined while no save has
|
|
71
|
+
* failed (or the failure carried no server reason).
|
|
72
|
+
*/
|
|
73
|
+
failedReason?: string;
|
|
60
74
|
}
|
|
61
75
|
/** The write actions the card's slot entry injects. */
|
|
62
76
|
export interface CardActions {
|
|
@@ -69,12 +83,59 @@ export interface CardActions {
|
|
|
69
83
|
/** Drop every staged edit. */
|
|
70
84
|
discard: () => void;
|
|
71
85
|
}
|
|
72
|
-
/**
|
|
73
|
-
export
|
|
86
|
+
/** One durable write a batched settings scope performs. */
|
|
87
|
+
export interface BatchedWrite {
|
|
88
|
+
/** Field this entry writes. */
|
|
89
|
+
field: string;
|
|
90
|
+
/** set stores a value; unset drops the leaf. */
|
|
91
|
+
op: 'set' | 'unset';
|
|
92
|
+
/** Value for op set (absent for unset). */
|
|
93
|
+
value?: unknown;
|
|
94
|
+
}
|
|
95
|
+
/** Per-field outcome of one batched scope write. */
|
|
96
|
+
export interface BatchedFieldResult {
|
|
97
|
+
/** Field this entry writes. */
|
|
98
|
+
field: string;
|
|
99
|
+
/** Whether the Host accepted this field's write (per the read-back view). */
|
|
100
|
+
landed: boolean;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Result of a batched scope write. The bridge scope posts every planned write
|
|
104
|
+
* in one /mutate so the Host validate hook judges baseURL+model together; a
|
|
105
|
+
* batched refusal fails the whole save rather than per-field.
|
|
106
|
+
*/
|
|
107
|
+
export interface BatchResult {
|
|
108
|
+
/** Whether the whole mutate was accepted. */
|
|
109
|
+
ok: boolean;
|
|
110
|
+
/** Per-field success, in the request order (always present when ok). */
|
|
111
|
+
fields: BatchedFieldResult[];
|
|
112
|
+
/** Host rejection code (mutate refused). */
|
|
113
|
+
code?: string;
|
|
114
|
+
/** Host rejection message (mutate refused). */
|
|
115
|
+
message?: string;
|
|
116
|
+
}
|
|
117
|
+
/** Constraints a numeric field's accepted drafts must satisfy, mirroring the host schema. */
|
|
118
|
+
export interface NumberConstraints {
|
|
119
|
+
/** The accepted value must be a whole number. */
|
|
120
|
+
integer?: boolean;
|
|
121
|
+
/** The accepted value must be at least this. */
|
|
122
|
+
min?: number;
|
|
123
|
+
}
|
|
124
|
+
/** A whole- or decimal-number field. An empty draft clears the field; any other draft that is not a finite number within the constraints blocks the save. */
|
|
125
|
+
export declare function numberField(field: string, constraints?: NumberConstraints): FieldSpec;
|
|
74
126
|
/** A free-text field. An empty draft clears the field. */
|
|
75
127
|
export declare function textField(field: string): FieldSpec;
|
|
128
|
+
/**
|
|
129
|
+
* A free-text field the Host treats as a secret and redacts from the read-back
|
|
130
|
+
* (role('secret') in the section schema). The card still edits it like text,
|
|
131
|
+
* but a save never compares the redacted value back and relies on the scope
|
|
132
|
+
* reporting the write landed.
|
|
133
|
+
*/
|
|
134
|
+
export declare function secretField(field: string): FieldSpec;
|
|
76
135
|
/** A boolean field, edited through true/false draft text. */
|
|
77
136
|
export declare function booleanField(field: string): FieldSpec;
|
|
137
|
+
/** An enumerated string field; only the listed choices are accepted. An empty draft clears the field. */
|
|
138
|
+
export declare function choiceField(field: string, choices: readonly string[]): FieldSpec;
|
|
78
139
|
/**
|
|
79
140
|
* Stages one card's edits over one settings namespace and writes them on save.
|
|
80
141
|
*
|
|
@@ -90,6 +151,7 @@ export declare class CardForm<T> {
|
|
|
90
151
|
private readonly listeners;
|
|
91
152
|
private saving;
|
|
92
153
|
private failed;
|
|
154
|
+
private failedReason;
|
|
93
155
|
/** @param scope - the bound settings scope for this card's namespace. */
|
|
94
156
|
constructor(scope: SettingsScope<T>, specs: FieldSpec[]);
|
|
95
157
|
/** Publish a projection of this form, rebuilt whenever the scope or a draft changes. */
|
|
@@ -102,9 +164,18 @@ export declare class CardForm<T> {
|
|
|
102
164
|
actions(): CardActions;
|
|
103
165
|
/**
|
|
104
166
|
* Write every staged edit, then re-seed from what the Host accepted.
|
|
167
|
+
*
|
|
168
|
+
* When the scope carries the optional batch surface (the dsh-web-ui
|
|
169
|
+
* bridge scope), every planned write rides one mutation so cross-field
|
|
170
|
+
* validate hooks (baseURL+model) judge the batch as a unit instead of
|
|
171
|
+
* deadlocking on per-field writes. Otherwise the per-field loop runs.
|
|
172
|
+
* A field lands only when the Host reports it held the staged value; a
|
|
173
|
+
* landed field's draft is dropped, a failed one stays staged for the user.
|
|
105
174
|
* @returns settlement after every write and the read-back.
|
|
106
175
|
*/
|
|
107
176
|
save(): Promise<void>;
|
|
177
|
+
/** The scope's batch surface when it supports one; undefined conservatively otherwise. */
|
|
178
|
+
private batchedScope;
|
|
108
179
|
/**
|
|
109
180
|
* Every staged edit a save would write. An entry whose draft is not a value
|
|
110
181
|
* its field accepts carries no write: the form is still dirty, and the save
|
|
@@ -7,6 +7,11 @@
|
|
|
7
7
|
* navigates to a session (the sessions-list `current` selection changes).
|
|
8
8
|
* Framework-free (structural runtime faces) so the whole orchestration is
|
|
9
9
|
* unit-testable with fakes.
|
|
10
|
+
*
|
|
11
|
+
* The per use-case domain transitions (create/update/delete/schedule) live in
|
|
12
|
+
* dedicated modules under core/use-cases and are applied here; the controller
|
|
13
|
+
* owns only the orchestration seam (state, persistence, notify, execution,
|
|
14
|
+
* navigation, reconciliation).
|
|
10
15
|
*/
|
|
11
16
|
import { ExecutionService } from './execution.ts';
|
|
12
17
|
import type { TaskStore } from './store.ts';
|
|
@@ -76,7 +81,7 @@ export declare class BoardController {
|
|
|
76
81
|
* Update a task's schedule rule. A blank or invalid cron expression is
|
|
77
82
|
* rejected (returns false, state untouched). When the rule ends up enabled
|
|
78
83
|
* the next run instant is computed immediately; a disabled rule carries no
|
|
79
|
-
* next-run instant.
|
|
84
|
+
* next-run instant. Delegates the domain transition to the schedule use case.
|
|
80
85
|
* @param id - the task to schedule.
|
|
81
86
|
* @param patch - fields to change (absent fields keep their current value).
|
|
82
87
|
* @returns true when applied, false when rejected (invalid cron / unknown task).
|
|
@@ -31,11 +31,23 @@ export interface SchedulerDeps {
|
|
|
31
31
|
export declare class SchedulerService {
|
|
32
32
|
private readonly deps;
|
|
33
33
|
private timer;
|
|
34
|
+
private environmentListener;
|
|
34
35
|
private disposed;
|
|
36
|
+
private started;
|
|
35
37
|
/** @param deps - tasks/clock/trigger/apply faces (see {@link SchedulerDeps}). */
|
|
36
38
|
constructor(deps: SchedulerDeps);
|
|
37
|
-
/**
|
|
39
|
+
/**
|
|
40
|
+
* Start ticking: one immediate check (catch-up after reload) + the interval.
|
|
41
|
+
* Single-instance: a second start while already armed is a no-op, so a
|
|
42
|
+
* re-entrant mount can never stack a duplicate timer.
|
|
43
|
+
*/
|
|
38
44
|
start(): void;
|
|
45
|
+
/**
|
|
46
|
+
* Stop ticking and drop listeners (idempotent). Preferred shutdown verb for
|
|
47
|
+
* callers that treat the scheduler as a controlled ticker; `dispose` is an
|
|
48
|
+
* alias.
|
|
49
|
+
*/
|
|
50
|
+
stop(): void;
|
|
39
51
|
/** Stop ticking and drop listeners (idempotent). */
|
|
40
52
|
dispose(): void;
|
|
41
53
|
/**
|
|
@@ -45,5 +57,4 @@ export declare class SchedulerService {
|
|
|
45
57
|
* tick instead of being silently dropped.
|
|
46
58
|
*/
|
|
47
59
|
tick(): Promise<void>;
|
|
48
|
-
private readonly onVisibility;
|
|
49
60
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Create-task use case: mint a new task from user input, rejecting a blank
|
|
3
|
+
* title. Pure ledger transition (no persistence or notify — the controller
|
|
4
|
+
* orchestrates those), so it is unit-testable without any runtime face.
|
|
5
|
+
*/
|
|
6
|
+
import { type NewTaskInput, type TaskRecord } from '../tasks.ts';
|
|
7
|
+
/** Result of a create transition: the new task (when accepted) + the next ledger. */
|
|
8
|
+
export interface CreateTaskResult {
|
|
9
|
+
/** The minted task, or undefined when the input was rejected (blank title). */
|
|
10
|
+
task: TaskRecord | undefined;
|
|
11
|
+
/** The next ledger; identical reference when rejected. */
|
|
12
|
+
tasks: readonly TaskRecord[];
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Apply a create against the current ledger. Returns the new task and the
|
|
16
|
+
* appended ledger, or the unchanged ledger when the title is blank.
|
|
17
|
+
* @param tasks - current ledger.
|
|
18
|
+
* @param input - raw user input (title/description/prompt).
|
|
19
|
+
* @param now - clock instant (ms epoch).
|
|
20
|
+
* @param id - minted task id.
|
|
21
|
+
*/
|
|
22
|
+
export declare function applyCreateTask(tasks: readonly TaskRecord[], input: NewTaskInput, now: number, id: string): CreateTaskResult;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Delete-task use case: drop a task from the ledger and clear the selection
|
|
3
|
+
* when it pointed at the removed task. Pure ledger transition (no persistence
|
|
4
|
+
* or notify — the controller orchestrates those).
|
|
5
|
+
*/
|
|
6
|
+
import type { TaskRecord } from '../tasks.ts';
|
|
7
|
+
/** Result of a delete transition. */
|
|
8
|
+
export interface DeleteTaskResult {
|
|
9
|
+
/** The next ledger (the removed task absent). */
|
|
10
|
+
tasks: readonly TaskRecord[];
|
|
11
|
+
/** Whether the previous selection referenced the removed task. */
|
|
12
|
+
selectionCleared: boolean;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Apply a delete across the ledger. The selection (a task id) is cleared when
|
|
16
|
+
* it matches the removed task, so the UI never lingers on a vanished detail.
|
|
17
|
+
* @param tasks - current ledger.
|
|
18
|
+
* @param selectedTaskId - the currently selected task id (may be undefined).
|
|
19
|
+
* @param id - the task to remove.
|
|
20
|
+
*/
|
|
21
|
+
export declare function applyDeleteTask(tasks: readonly TaskRecord[], selectedTaskId: string | undefined, id: string): DeleteTaskResult;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { type TaskRecord } from '../tasks.ts';
|
|
2
|
+
/** Fields the schedule use case may change on a rule. */
|
|
3
|
+
export interface SetSchedulePatch {
|
|
4
|
+
enabled?: boolean;
|
|
5
|
+
cron?: string;
|
|
6
|
+
}
|
|
7
|
+
/** Result of arming/disarming a rule. */
|
|
8
|
+
export interface SetScheduleResult {
|
|
9
|
+
/** The next ledger; unchanged reference when rejected. */
|
|
10
|
+
tasks: readonly TaskRecord[];
|
|
11
|
+
/** Whether the rule was applied (false on unknown task / invalid cron). */
|
|
12
|
+
applied: boolean;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Set a task's schedule rule. A blank or invalid cron is rejected (state
|
|
16
|
+
* untouched); an enabled rule computes the next run instant immediately, a
|
|
17
|
+
* disabled or invalid one carries no next-run instant.
|
|
18
|
+
* @param tasks - current ledger.
|
|
19
|
+
* @param id - the task to schedule.
|
|
20
|
+
* @param patch - rule fields to change (absent fields keep their current value).
|
|
21
|
+
* @param now - clock instant (ms epoch).
|
|
22
|
+
*/
|
|
23
|
+
export declare function applySetSchedule(tasks: readonly TaskRecord[], id: string, patch: SetSchedulePatch, now: number): SetScheduleResult;
|
|
24
|
+
/**
|
|
25
|
+
* Roll a task's schedule rule forward (scheduler callback): persist the next
|
|
26
|
+
* due instant and the trigger instant. No-op for tasks without a rule (deleted
|
|
27
|
+
* mid-tick, for example).
|
|
28
|
+
* @param tasks - current ledger.
|
|
29
|
+
* @param id - the task to roll forward.
|
|
30
|
+
* @param nextRunAt - next due instant (may be undefined to clear).
|
|
31
|
+
* @param lastTriggeredAt - the trigger instant of this run.
|
|
32
|
+
* @param now - clock instant (ms epoch).
|
|
33
|
+
*/
|
|
34
|
+
export declare function applyScheduleNextRun(tasks: readonly TaskRecord[], id: string, nextRunAt: number | undefined, lastTriggeredAt: number | undefined, now: number): readonly TaskRecord[];
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Update-task use case: apply an editable-field patch (title/description/
|
|
3
|
+
* prompt) with a fresh updatedAt. Pure ledger transition (no persistence or
|
|
4
|
+
* notify — the controller orchestrates those).
|
|
5
|
+
*/
|
|
6
|
+
import type { TaskRecord } from '../tasks.ts';
|
|
7
|
+
/** Editable fields on a task (the update patch surface). */
|
|
8
|
+
export type TaskUpdatePatch = Partial<Pick<TaskRecord, 'title' | 'description' | 'prompt'>>;
|
|
9
|
+
/**
|
|
10
|
+
* Apply an update across the ledger. Tasks that do not match the id are left
|
|
11
|
+
* untouched; the matched task receives the patch plus a fresh updatedAt.
|
|
12
|
+
* @param tasks - current ledger.
|
|
13
|
+
* @param id - the task to update.
|
|
14
|
+
* @param patch - editable-field changes.
|
|
15
|
+
* @param now - clock instant (ms epoch).
|
|
16
|
+
*/
|
|
17
|
+
export declare function applyUpdateTask(tasks: readonly TaskRecord[], id: string, patch: TaskUpdatePatch, now: number): readonly TaskRecord[];
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@linxin666/dsh-client-ui-task-board",
|
|
3
3
|
"description": "Task board for the dsh web GUI: a sidebar entry plus a multi-column kanban view with local persistence, task details, and real execution through dsh sessions (session.prompt). Hot-pluggable — mounted via ~/.dsh/cordis.patch.yml + a profile node_modules symlink, no dsh source changes.",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.16",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"types": "lib/types/index.d.ts",
|
|
@@ -1,25 +1,44 @@
|
|
|
1
|
+
// Generated by scripts/sync-shared.mjs from shared/client/settings/PluginSettingsCard.tsx. Do not edit this copy; edit the shared source and run "node scripts/sync-shared.mjs".
|
|
1
2
|
/**
|
|
2
|
-
*
|
|
3
|
-
* plugin and what its settings govern, the controls inside, and the save
|
|
4
|
-
* writes them. Renders nothing while the namespace is unavailable — a
|
|
3
|
+
* Family-shared chrome for plugin settings cards: a disclosure header naming
|
|
4
|
+
* the plugin and what its settings govern, the controls inside, and the save
|
|
5
|
+
* that writes them. Renders nothing while the namespace is unavailable — a
|
|
5
6
|
* deployment that does not compose the owning plugin should show no trace of
|
|
6
|
-
* it.
|
|
7
|
-
*
|
|
7
|
+
* it. Inlined into each consumer's client bundle; mirrors the official
|
|
8
|
+
* ui-plugin-config PluginCard in a self-contained slice.
|
|
8
9
|
*/
|
|
9
10
|
|
|
10
11
|
import { useState, type ReactNode } from 'react'
|
|
11
12
|
import type { CardShell } from './settings-form.ts'
|
|
12
|
-
import type { SettingsCardKey } from './locales.ts'
|
|
13
13
|
import css from './settings-card.module.css'
|
|
14
14
|
|
|
15
|
+
/** Copy keys the card chrome itself reads; every consumer locale carries this shared vocabulary. */
|
|
16
|
+
export const CARD_COPY_KEYS = [
|
|
17
|
+
'settings.collapse',
|
|
18
|
+
'settings.expand',
|
|
19
|
+
'settings.notExposed',
|
|
20
|
+
'settings.unsaved',
|
|
21
|
+
'settings.readOnly',
|
|
22
|
+
'settings.saveFailed',
|
|
23
|
+
'settings.discard',
|
|
24
|
+
'settings.save',
|
|
25
|
+
'settings.saving',
|
|
26
|
+
] as const
|
|
27
|
+
|
|
28
|
+
/** Copy key the card chrome itself reads. */
|
|
29
|
+
export type CardCopyKey = (typeof CARD_COPY_KEYS)[number]
|
|
30
|
+
|
|
31
|
+
/** Key domain for the plugin's own copy (inferred per consumer). */
|
|
32
|
+
export type SettingsCardKey<TKey extends string = string> = TKey | CardCopyKey
|
|
33
|
+
|
|
15
34
|
/** Card chrome shared by every plugin settings card. */
|
|
16
|
-
export interface PluginSettingsCardProps {
|
|
35
|
+
export interface PluginSettingsCardProps<TKey extends string = string> {
|
|
17
36
|
/** Locale reader for this card's copy. */
|
|
18
|
-
t: (key: SettingsCardKey) => string
|
|
37
|
+
t: (key: SettingsCardKey<TKey>, params?: Record<string, string | number>) => string
|
|
19
38
|
/** Locale key of the plugin's name. */
|
|
20
|
-
titleKey:
|
|
39
|
+
titleKey: TKey
|
|
21
40
|
/** Locale key of the line describing what this plugin's settings govern. */
|
|
22
|
-
descriptionKey:
|
|
41
|
+
descriptionKey: TKey
|
|
23
42
|
/** The card's form state: availability, writability, and what a save would do. */
|
|
24
43
|
state: CardShell
|
|
25
44
|
/** Write every staged edit. */
|
|
@@ -35,11 +54,12 @@ export interface PluginSettingsCardProps {
|
|
|
35
54
|
* @param props - the plugin's copy keys, its form state, and its controls.
|
|
36
55
|
* @returns the card, or nothing while the namespace is still loading.
|
|
37
56
|
*/
|
|
38
|
-
export function PluginSettingsCard(props: PluginSettingsCardProps) {
|
|
57
|
+
export function PluginSettingsCard<TKey extends string = string>(props: PluginSettingsCardProps<TKey>) {
|
|
39
58
|
const [open, setOpen] = useState(false)
|
|
40
59
|
const { state } = props
|
|
41
60
|
if (!state.available) return null
|
|
42
61
|
const title = props.t(props.titleKey)
|
|
62
|
+
const description = props.t(props.descriptionKey)
|
|
43
63
|
const blocked = !state.dirty || state.invalid || state.saving
|
|
44
64
|
const cardClass = open ? `${css.cardOpen} ${css.card}` : css.card
|
|
45
65
|
// The namespace exists but the Host does not serve it to this client (the
|
|
@@ -57,8 +77,8 @@ export function PluginSettingsCard(props: PluginSettingsCardProps) {
|
|
|
57
77
|
onClick={() => { setOpen(!open) }}
|
|
58
78
|
>
|
|
59
79
|
<span className={css.headText}>
|
|
60
|
-
<span className={css.name}>{title}</span>
|
|
61
|
-
<span className={css.description}>{
|
|
80
|
+
<span className={css.name} title={title}>{title}</span>
|
|
81
|
+
<span className={css.description} title={description}>{description}</span>
|
|
62
82
|
</span>
|
|
63
83
|
<svg
|
|
64
84
|
width="14"
|
|
@@ -94,10 +114,10 @@ export function PluginSettingsCard(props: PluginSettingsCardProps) {
|
|
|
94
114
|
onClick={() => { setOpen(!open) }}
|
|
95
115
|
>
|
|
96
116
|
<span className={css.headText}>
|
|
97
|
-
<span className={css.name}>{title}</span>
|
|
98
|
-
<span className={css.description}>{
|
|
117
|
+
<span className={css.name} title={title}>{title}</span>
|
|
118
|
+
<span className={css.description} title={description}>{description}</span>
|
|
99
119
|
</span>
|
|
100
|
-
{state.dirty ? <span className={css.pending}>{props.t('settings.unsaved')}</span> : null}
|
|
120
|
+
{state.dirty ? <span className={css.pending} title={props.t('settings.unsaved')}>{props.t('settings.unsaved')}</span> : null}
|
|
101
121
|
<svg
|
|
102
122
|
width="14"
|
|
103
123
|
height="14"
|
|
@@ -118,7 +138,13 @@ export function PluginSettingsCard(props: PluginSettingsCardProps) {
|
|
|
118
138
|
{!state.writable ? <p className={css.readOnly} role="status">{props.t('settings.readOnly')}</p> : null}
|
|
119
139
|
{props.children}
|
|
120
140
|
<div className={css.footer}>
|
|
121
|
-
{state.failed
|
|
141
|
+
{state.failed
|
|
142
|
+
? (
|
|
143
|
+
<p className={css.failed} role="status">
|
|
144
|
+
{props.t('settings.saveFailed')}{state.failedReason ? ' - ' + state.failedReason : ''}
|
|
145
|
+
</p>
|
|
146
|
+
)
|
|
147
|
+
: null}
|
|
122
148
|
<button
|
|
123
149
|
type="button"
|
|
124
150
|
className={css.discard}
|
|
@@ -259,4 +285,50 @@ export function BooleanField(props: FieldProps & {
|
|
|
259
285
|
<p className={css.hint}>{props.hint}</p>
|
|
260
286
|
</div>
|
|
261
287
|
)
|
|
262
|
-
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/** A staged enumerated field rendered as a select. */
|
|
291
|
+
export function ChoiceField(props: FieldProps & {
|
|
292
|
+
/** Copy for the inherit option (draft text is the empty string). */
|
|
293
|
+
inheritLabel: string
|
|
294
|
+
/** Choices rendered in order; `value` is the draft/stored text. */
|
|
295
|
+
choices: ReadonlyArray<{ value: string; label: string }>
|
|
296
|
+
}) {
|
|
297
|
+
return (
|
|
298
|
+
<div className={css.field}>
|
|
299
|
+
<div className={css.head}>
|
|
300
|
+
<label className={css.label} htmlFor={props.id}>{props.label}</label>
|
|
301
|
+
{props.overridden
|
|
302
|
+
? (
|
|
303
|
+
<span className={css.badges}>
|
|
304
|
+
<span className={css.badge}>{props.overriddenLabel}</span>
|
|
305
|
+
<button
|
|
306
|
+
type="button"
|
|
307
|
+
className={css.reset}
|
|
308
|
+
disabled={props.disabled}
|
|
309
|
+
onClick={props.onReset}
|
|
310
|
+
>
|
|
311
|
+
{props.resetLabel}
|
|
312
|
+
</button>
|
|
313
|
+
</span>
|
|
314
|
+
)
|
|
315
|
+
: null}
|
|
316
|
+
</div>
|
|
317
|
+
<select
|
|
318
|
+
id={props.id}
|
|
319
|
+
className={css.select}
|
|
320
|
+
value={props.text}
|
|
321
|
+
disabled={props.disabled}
|
|
322
|
+
onChange={(event) => { props.onEdit(event.target.value) }}
|
|
323
|
+
>
|
|
324
|
+
<option value="">{props.inheritLabel}</option>
|
|
325
|
+
{props.choices.map(choice => (
|
|
326
|
+
<option key={choice.value} value={choice.value}>{choice.label}</option>
|
|
327
|
+
))}
|
|
328
|
+
</select>
|
|
329
|
+
<p className={props.invalid ? css.invalid : css.hint}>
|
|
330
|
+
{props.invalid ? props.invalidLabel : props.hint}
|
|
331
|
+
</p>
|
|
332
|
+
</div>
|
|
333
|
+
)
|
|
334
|
+
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* active. Cards open the task detail (never execute directly); the header
|
|
4
4
|
* offers filter, new-task, and a back-to-chat escape.
|
|
5
5
|
*/
|
|
6
|
-
import { useEffect, useState } from 'react'
|
|
6
|
+
import { memo, useCallback, useEffect, useState } from 'react'
|
|
7
7
|
import { selectedTaskOf, type BoardController } from '../../core/controller.ts'
|
|
8
8
|
import { COLUMNS, type TaskRecord, type TaskStatus } from '../../core/tasks.ts'
|
|
9
9
|
import { t, type TaskBoardKey } from '../locales.ts'
|
|
@@ -28,6 +28,17 @@ function matchesFilter(task: TaskRecord, filter: string): boolean {
|
|
|
28
28
|
return task.title.toLowerCase().includes(needle) || task.description.toLowerCase().includes(needle)
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
/**
|
|
32
|
+
* Memoized per-card adapter: with a stable `onOpen` from the board and an
|
|
33
|
+
* immutable task record (only the changed card gets a new object ref), a card
|
|
34
|
+
* re-renders only when its own task changes — not when a sibling card status,
|
|
35
|
+
* the filter, or the selection moves.
|
|
36
|
+
*/
|
|
37
|
+
const MemoTaskCard = memo(function MemoTaskCard({ task, onOpen }: { task: TaskRecord; onOpen: (id: string) => void }) {
|
|
38
|
+
const onClick = useCallback(() => { onOpen(task.id) }, [task.id, onOpen])
|
|
39
|
+
return <TaskCard task={task} onClick={onClick} />
|
|
40
|
+
})
|
|
41
|
+
|
|
31
42
|
/** Board component; subscribes to the controller snapshot. */
|
|
32
43
|
export function TaskBoard({ controller }: { controller: BoardController }) {
|
|
33
44
|
const [snapshot, setSnapshot] = useState(controller.getSnapshot())
|
|
@@ -39,6 +50,7 @@ export function TaskBoard({ controller }: { controller: BoardController }) {
|
|
|
39
50
|
const [showNew, setShowNew] = useState(false)
|
|
40
51
|
const selected = selectedTaskOf(snapshot)
|
|
41
52
|
const visible = snapshot.tasks.filter(task => matchesFilter(task, filter))
|
|
53
|
+
const openTask = useCallback((id: string): void => { controller.openTask(id) }, [controller])
|
|
42
54
|
|
|
43
55
|
return (
|
|
44
56
|
<div className={css.board} data-dsh-taskboard-board="">
|
|
@@ -80,11 +92,7 @@ export function TaskBoard({ controller }: { controller: BoardController }) {
|
|
|
80
92
|
</header>
|
|
81
93
|
<div className={css.cards}>
|
|
82
94
|
{tasks.map(task => (
|
|
83
|
-
<
|
|
84
|
-
key={task.id}
|
|
85
|
-
task={task}
|
|
86
|
-
onClick={() => { controller.openTask(task.id) }}
|
|
87
|
-
/>
|
|
95
|
+
<MemoTaskCard key={task.id} task={task} onOpen={openTask} />
|
|
88
96
|
))}
|
|
89
97
|
{tasks.length === 0 && <div className={css.columnEmpty}>{t('board.empty')}</div>}
|
|
90
98
|
</div>
|
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Task card: the board's column item. Clicking opens the task detail — it
|
|
3
3
|
* never executes anything directly (detail holds the Run button).
|
|
4
|
+
*
|
|
5
|
+
* Memoized: the card re-renders only when its own task record changes, so a
|
|
6
|
+
* status/filter update on one card (or scrolling) never re-renders every
|
|
7
|
+
* card on the board. The per-card onClick is built with a stable task reference
|
|
8
|
+
* by the board, so the memo boundary is effective.
|
|
4
9
|
*/
|
|
10
|
+
import { memo } from 'react'
|
|
5
11
|
import type { TaskRecord } from '../../core/tasks.ts'
|
|
6
12
|
import { executionLabel } from '../../core/tasks.ts'
|
|
7
13
|
import { t } from '../locales.ts'
|
|
@@ -19,7 +25,7 @@ export function formatTime(ms: number): string {
|
|
|
19
25
|
}
|
|
20
26
|
|
|
21
27
|
/** One card in a column. */
|
|
22
|
-
|
|
28
|
+
function TaskCardInner({ task, onClick }: { task: TaskRecord; onClick: () => void }) {
|
|
23
29
|
const latest = task.executions[task.executions.length - 1]
|
|
24
30
|
const runs = task.executions.length
|
|
25
31
|
return (
|
|
@@ -60,3 +66,6 @@ export function TaskCard({ task, onClick }: { task: TaskRecord; onClick: () => v
|
|
|
60
66
|
</button>
|
|
61
67
|
)
|
|
62
68
|
}
|
|
69
|
+
|
|
70
|
+
/** Memoized card: re-renders only when the card's own task record changes. */
|
|
71
|
+
export const TaskCard = memo(TaskCardInner)
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
/* Generated by scripts/sync-shared.mjs from shared/client/settings/settings-card.module.css. Do not edit this copy; edit the shared source and run "node scripts/sync-shared.mjs". */
|
|
1
2
|
/* Plugin settings card chrome + staged form fields.
|
|
2
3
|
* Aligned with the official ui-settings-plugins PluginCard / fields CSS:
|
|
3
4
|
* same semantic tokens, radius, typography and states so family cards read
|
|
@@ -120,6 +121,9 @@
|
|
|
120
121
|
margin: 0;
|
|
121
122
|
font-size: 12px;
|
|
122
123
|
line-height: 1.5;
|
|
124
|
+
text-overflow: ellipsis;
|
|
125
|
+
overflow: hidden;
|
|
126
|
+
white-space: nowrap;
|
|
123
127
|
}
|
|
124
128
|
|
|
125
129
|
.discard,
|
|
@@ -229,6 +233,11 @@
|
|
|
229
233
|
outline-offset: 2px;
|
|
230
234
|
}
|
|
231
235
|
|
|
236
|
+
.reset:focus-visible {
|
|
237
|
+
outline: 2px solid var(--dsw-alias-brand-primary);
|
|
238
|
+
outline-offset: 2px;
|
|
239
|
+
}
|
|
240
|
+
|
|
232
241
|
.input,
|
|
233
242
|
.select {
|
|
234
243
|
border: 1px solid var(--dsw-alias-border-l2);
|