@linxin666/dsh-client-ui-task-board 0.1.13 → 0.1.15
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 +338 -152
- package/lib/client.js.map +1 -1
- package/lib/types/client/PluginSettingsCard.d.ts +26 -11
- package/lib/types/client/apply-guard.d.ts +25 -0
- package/lib/types/client/board/TaskCard.d.ts +4 -5
- package/lib/types/client/settings-form.d.ts +14 -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 -2
- package/src/client/PluginSettingsCard.tsx +83 -17
- package/src/client/apply-guard.ts +35 -0
- package/src/client/board/TaskBoard.tsx +14 -6
- package/src/client/board/TaskCard.tsx +10 -1
- package/src/client/index.ts +11 -0
- package/src/client/settings-card.module.css +6 -0
- package/src/client/settings-form.ts +41 -9
- package/src/client/sidebar-entry.ts +7 -0
- 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
package/lib/client.js
CHANGED
|
@@ -8,107 +8,6 @@ window.__ModuleLoader__.load({
|
|
|
8
8
|
let react = require("react");
|
|
9
9
|
let react_jsx_runtime = require("react/jsx-runtime");
|
|
10
10
|
let _deepseek_ai_dsh_client_runtime_client = require("@deepseek-ai/dsh-client-runtime/client");
|
|
11
|
-
//#region src/core/schedule.ts
|
|
12
|
-
/** Inclusive ranges per field, in cron order. */
|
|
13
|
-
const FIELD_RANGES = [
|
|
14
|
-
[0, 59],
|
|
15
|
-
[0, 23],
|
|
16
|
-
[1, 31],
|
|
17
|
-
[1, 12],
|
|
18
|
-
[0, 7]
|
|
19
|
-
];
|
|
20
|
-
/**
|
|
21
|
-
* Parse a 5-field cron expression.
|
|
22
|
-
* @returns the match sets, or null when the expression is invalid.
|
|
23
|
-
*/
|
|
24
|
-
function parseCron(expr) {
|
|
25
|
-
const fields = expr.trim().split(/\s+/);
|
|
26
|
-
if (fields.length !== 5) return null;
|
|
27
|
-
const sets = [];
|
|
28
|
-
for (let index = 0; index < 5; index++) {
|
|
29
|
-
const [min, max] = FIELD_RANGES[index];
|
|
30
|
-
const set = /* @__PURE__ */ new Set();
|
|
31
|
-
if (!parseField(fields[index], min, max, set)) return null;
|
|
32
|
-
sets.push(set);
|
|
33
|
-
}
|
|
34
|
-
const weekdays = /* @__PURE__ */ new Set();
|
|
35
|
-
for (const day of sets[4]) weekdays.add(day === 7 ? 0 : day);
|
|
36
|
-
return {
|
|
37
|
-
minutes: sets[0],
|
|
38
|
-
hours: sets[1],
|
|
39
|
-
days: sets[2],
|
|
40
|
-
months: sets[3],
|
|
41
|
-
weekdays,
|
|
42
|
-
dayWildcard: fields[2] === "*",
|
|
43
|
-
weekdayWildcard: fields[4] === "*"
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
|
-
/** Whether the expression parses. */
|
|
47
|
-
function isValidCron(expr) {
|
|
48
|
-
return parseCron(expr) !== null;
|
|
49
|
-
}
|
|
50
|
-
/**
|
|
51
|
-
* Compute the next matching instant after `fromMs` (ms epoch), in local time,
|
|
52
|
-
* at minute granularity, strictly greater than `fromMs`. Returns the ms epoch
|
|
53
|
-
* of the matching minute's start, or undefined when nothing matches within
|
|
54
|
-
* 366 days (e.g. `0 0 30 2 *`).
|
|
55
|
-
*/
|
|
56
|
-
function nextRunAtMs(expr, fromMs) {
|
|
57
|
-
const schedule = parseCron(expr);
|
|
58
|
-
if (schedule === null) return void 0;
|
|
59
|
-
const from = new Date(fromMs);
|
|
60
|
-
const scan = new Date(from.getFullYear(), from.getMonth(), from.getDate(), from.getHours(), from.getMinutes() + 1, 0, 0);
|
|
61
|
-
const limitMs = fromMs + 366 * 24 * 60 * 60 * 1e3;
|
|
62
|
-
while (scan.getTime() <= limitMs) {
|
|
63
|
-
if (matches(schedule, scan)) return scan.getTime();
|
|
64
|
-
scan.setMinutes(scan.getMinutes() + 1);
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
/** Parse one comma-list field into the match set. */
|
|
68
|
-
function parseField(field, min, max, out) {
|
|
69
|
-
if (field === "*") {
|
|
70
|
-
for (let value = min; value <= max; value++) out.add(value);
|
|
71
|
-
return true;
|
|
72
|
-
}
|
|
73
|
-
for (const part of field.split(",")) {
|
|
74
|
-
if (part === "") return false;
|
|
75
|
-
const [range, stepRaw] = part.split("/");
|
|
76
|
-
let low;
|
|
77
|
-
let high;
|
|
78
|
-
if (range === "*") {
|
|
79
|
-
low = min;
|
|
80
|
-
high = max;
|
|
81
|
-
} else if (range.includes("-")) {
|
|
82
|
-
const [a, b] = range.split("-");
|
|
83
|
-
if (a === "" || b === "" || !isDigits(a) || !isDigits(b)) return false;
|
|
84
|
-
low = Number(a);
|
|
85
|
-
high = Number(b);
|
|
86
|
-
} else if (isDigits(range)) {
|
|
87
|
-
low = Number(range);
|
|
88
|
-
high = Number(range);
|
|
89
|
-
} else return false;
|
|
90
|
-
if (low < min || high > max || low > high) return false;
|
|
91
|
-
const step = stepRaw === void 0 ? 1 : isDigits(stepRaw) ? Number(stepRaw) : NaN;
|
|
92
|
-
if (!Number.isInteger(step) || step < 1) return false;
|
|
93
|
-
for (let value = low; value <= high; value += step) out.add(value);
|
|
94
|
-
}
|
|
95
|
-
return true;
|
|
96
|
-
}
|
|
97
|
-
/** Day/weekday OR semantics: a restricted day field alone gates, and vice versa. */
|
|
98
|
-
function matches(schedule, date) {
|
|
99
|
-
if (!schedule.minutes.has(date.getMinutes())) return false;
|
|
100
|
-
if (!schedule.hours.has(date.getHours())) return false;
|
|
101
|
-
if (!schedule.months.has(date.getMonth() + 1)) return false;
|
|
102
|
-
const dayMatches = schedule.days.has(date.getDate());
|
|
103
|
-
const weekdayMatches = schedule.weekdays.has(date.getDay());
|
|
104
|
-
if (schedule.dayWildcard) return weekdayMatches;
|
|
105
|
-
if (schedule.weekdayWildcard) return dayMatches;
|
|
106
|
-
return dayMatches || weekdayMatches;
|
|
107
|
-
}
|
|
108
|
-
function isDigits(value) {
|
|
109
|
-
return /^\d+$/.test(value);
|
|
110
|
-
}
|
|
111
|
-
//#endregion
|
|
112
11
|
//#region src/core/tasks.ts
|
|
113
12
|
/** The five kanban columns, in display order. */
|
|
114
13
|
const COLUMNS = [
|
|
@@ -249,6 +148,221 @@ window.__ModuleLoader__.load({
|
|
|
249
148
|
return "running";
|
|
250
149
|
}
|
|
251
150
|
//#endregion
|
|
151
|
+
//#region src/core/use-cases/task-create.ts
|
|
152
|
+
/**
|
|
153
|
+
* Create-task use case: mint a new task from user input, rejecting a blank
|
|
154
|
+
* title. Pure ledger transition (no persistence or notify — the controller
|
|
155
|
+
* orchestrates those), so it is unit-testable without any runtime face.
|
|
156
|
+
*/
|
|
157
|
+
/**
|
|
158
|
+
* Apply a create against the current ledger. Returns the new task and the
|
|
159
|
+
* appended ledger, or the unchanged ledger when the title is blank.
|
|
160
|
+
* @param tasks - current ledger.
|
|
161
|
+
* @param input - raw user input (title/description/prompt).
|
|
162
|
+
* @param now - clock instant (ms epoch).
|
|
163
|
+
* @param id - minted task id.
|
|
164
|
+
*/
|
|
165
|
+
function applyCreateTask(tasks, input, now, id) {
|
|
166
|
+
if (input.title.trim() === "") return {
|
|
167
|
+
task: void 0,
|
|
168
|
+
tasks
|
|
169
|
+
};
|
|
170
|
+
const task = createTask(input, now, id);
|
|
171
|
+
return {
|
|
172
|
+
task,
|
|
173
|
+
tasks: [...tasks, task]
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
//#endregion
|
|
177
|
+
//#region src/core/use-cases/task-delete.ts
|
|
178
|
+
/**
|
|
179
|
+
* Apply a delete across the ledger. The selection (a task id) is cleared when
|
|
180
|
+
* it matches the removed task, so the UI never lingers on a vanished detail.
|
|
181
|
+
* @param tasks - current ledger.
|
|
182
|
+
* @param selectedTaskId - the currently selected task id (may be undefined).
|
|
183
|
+
* @param id - the task to remove.
|
|
184
|
+
*/
|
|
185
|
+
function applyDeleteTask(tasks, selectedTaskId, id) {
|
|
186
|
+
return {
|
|
187
|
+
tasks: tasks.filter((task) => task.id !== id),
|
|
188
|
+
selectionCleared: selectedTaskId === id
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
//#endregion
|
|
192
|
+
//#region src/core/schedule.ts
|
|
193
|
+
/** Inclusive ranges per field, in cron order. */
|
|
194
|
+
const FIELD_RANGES = [
|
|
195
|
+
[0, 59],
|
|
196
|
+
[0, 23],
|
|
197
|
+
[1, 31],
|
|
198
|
+
[1, 12],
|
|
199
|
+
[0, 7]
|
|
200
|
+
];
|
|
201
|
+
/**
|
|
202
|
+
* Parse a 5-field cron expression.
|
|
203
|
+
* @returns the match sets, or null when the expression is invalid.
|
|
204
|
+
*/
|
|
205
|
+
function parseCron(expr) {
|
|
206
|
+
const fields = expr.trim().split(/\s+/);
|
|
207
|
+
if (fields.length !== 5) return null;
|
|
208
|
+
const sets = [];
|
|
209
|
+
for (let index = 0; index < 5; index++) {
|
|
210
|
+
const [min, max] = FIELD_RANGES[index];
|
|
211
|
+
const set = /* @__PURE__ */ new Set();
|
|
212
|
+
if (!parseField(fields[index], min, max, set)) return null;
|
|
213
|
+
sets.push(set);
|
|
214
|
+
}
|
|
215
|
+
const weekdays = /* @__PURE__ */ new Set();
|
|
216
|
+
for (const day of sets[4]) weekdays.add(day === 7 ? 0 : day);
|
|
217
|
+
return {
|
|
218
|
+
minutes: sets[0],
|
|
219
|
+
hours: sets[1],
|
|
220
|
+
days: sets[2],
|
|
221
|
+
months: sets[3],
|
|
222
|
+
weekdays,
|
|
223
|
+
dayWildcard: fields[2] === "*",
|
|
224
|
+
weekdayWildcard: fields[4] === "*"
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
/** Whether the expression parses. */
|
|
228
|
+
function isValidCron(expr) {
|
|
229
|
+
return parseCron(expr) !== null;
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Compute the next matching instant after `fromMs` (ms epoch), in local time,
|
|
233
|
+
* at minute granularity, strictly greater than `fromMs`. Returns the ms epoch
|
|
234
|
+
* of the matching minute's start, or undefined when nothing matches within
|
|
235
|
+
* 366 days (e.g. `0 0 30 2 *`).
|
|
236
|
+
*/
|
|
237
|
+
function nextRunAtMs(expr, fromMs) {
|
|
238
|
+
const schedule = parseCron(expr);
|
|
239
|
+
if (schedule === null) return void 0;
|
|
240
|
+
const from = new Date(fromMs);
|
|
241
|
+
const scan = new Date(from.getFullYear(), from.getMonth(), from.getDate(), from.getHours(), from.getMinutes() + 1, 0, 0);
|
|
242
|
+
const limitMs = fromMs + 366 * 24 * 60 * 60 * 1e3;
|
|
243
|
+
while (scan.getTime() <= limitMs) {
|
|
244
|
+
if (matches(schedule, scan)) return scan.getTime();
|
|
245
|
+
scan.setMinutes(scan.getMinutes() + 1);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
/** Parse one comma-list field into the match set. */
|
|
249
|
+
function parseField(field, min, max, out) {
|
|
250
|
+
if (field === "*") {
|
|
251
|
+
for (let value = min; value <= max; value++) out.add(value);
|
|
252
|
+
return true;
|
|
253
|
+
}
|
|
254
|
+
for (const part of field.split(",")) {
|
|
255
|
+
if (part === "") return false;
|
|
256
|
+
const [range, stepRaw] = part.split("/");
|
|
257
|
+
let low;
|
|
258
|
+
let high;
|
|
259
|
+
if (range === "*") {
|
|
260
|
+
low = min;
|
|
261
|
+
high = max;
|
|
262
|
+
} else if (range.includes("-")) {
|
|
263
|
+
const [a, b] = range.split("-");
|
|
264
|
+
if (a === "" || b === "" || !isDigits(a) || !isDigits(b)) return false;
|
|
265
|
+
low = Number(a);
|
|
266
|
+
high = Number(b);
|
|
267
|
+
} else if (isDigits(range)) {
|
|
268
|
+
low = Number(range);
|
|
269
|
+
high = Number(range);
|
|
270
|
+
} else return false;
|
|
271
|
+
if (low < min || high > max || low > high) return false;
|
|
272
|
+
const step = stepRaw === void 0 ? 1 : isDigits(stepRaw) ? Number(stepRaw) : NaN;
|
|
273
|
+
if (!Number.isInteger(step) || step < 1) return false;
|
|
274
|
+
for (let value = low; value <= high; value += step) out.add(value);
|
|
275
|
+
}
|
|
276
|
+
return true;
|
|
277
|
+
}
|
|
278
|
+
/** Day/weekday OR semantics: a restricted day field alone gates, and vice versa. */
|
|
279
|
+
function matches(schedule, date) {
|
|
280
|
+
if (!schedule.minutes.has(date.getMinutes())) return false;
|
|
281
|
+
if (!schedule.hours.has(date.getHours())) return false;
|
|
282
|
+
if (!schedule.months.has(date.getMonth() + 1)) return false;
|
|
283
|
+
const dayMatches = schedule.days.has(date.getDate());
|
|
284
|
+
const weekdayMatches = schedule.weekdays.has(date.getDay());
|
|
285
|
+
if (schedule.dayWildcard) return weekdayMatches;
|
|
286
|
+
if (schedule.weekdayWildcard) return dayMatches;
|
|
287
|
+
return dayMatches || weekdayMatches;
|
|
288
|
+
}
|
|
289
|
+
function isDigits(value) {
|
|
290
|
+
return /^\d+$/.test(value);
|
|
291
|
+
}
|
|
292
|
+
//#endregion
|
|
293
|
+
//#region src/core/use-cases/task-schedule.ts
|
|
294
|
+
/**
|
|
295
|
+
* Schedule use case: arm/disarm a task's cron rule and roll a rule forward.
|
|
296
|
+
* Pure ledger transitions (no persistence or notify — the controller
|
|
297
|
+
* orchestrates those). Validation and next-run computation live here, sharing
|
|
298
|
+
* the core cron parser (schedule.ts) and the withSchedule transition.
|
|
299
|
+
*/
|
|
300
|
+
/**
|
|
301
|
+
* Set a task's schedule rule. A blank or invalid cron is rejected (state
|
|
302
|
+
* untouched); an enabled rule computes the next run instant immediately, a
|
|
303
|
+
* disabled or invalid one carries no next-run instant.
|
|
304
|
+
* @param tasks - current ledger.
|
|
305
|
+
* @param id - the task to schedule.
|
|
306
|
+
* @param patch - rule fields to change (absent fields keep their current value).
|
|
307
|
+
* @param now - clock instant (ms epoch).
|
|
308
|
+
*/
|
|
309
|
+
function applySetSchedule(tasks, id, patch, now) {
|
|
310
|
+
const task = tasks.find((candidate) => candidate.id === id);
|
|
311
|
+
if (task === void 0) return {
|
|
312
|
+
tasks,
|
|
313
|
+
applied: false
|
|
314
|
+
};
|
|
315
|
+
const current = task.schedule;
|
|
316
|
+
const cron = (patch.cron ?? current?.cron ?? "").trim();
|
|
317
|
+
if (cron === "" || !isValidCron(cron)) return {
|
|
318
|
+
tasks,
|
|
319
|
+
applied: false
|
|
320
|
+
};
|
|
321
|
+
const enabled = patch.enabled ?? current?.enabled ?? false;
|
|
322
|
+
const nextRunAt = enabled ? nextRunAtMs(cron, now) : void 0;
|
|
323
|
+
return {
|
|
324
|
+
tasks: tasks.map((candidate) => candidate.id === id ? withSchedule(candidate, {
|
|
325
|
+
enabled,
|
|
326
|
+
cron,
|
|
327
|
+
nextRunAt
|
|
328
|
+
}, now) : candidate),
|
|
329
|
+
applied: true
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* Roll a task's schedule rule forward (scheduler callback): persist the next
|
|
334
|
+
* due instant and the trigger instant. No-op for tasks without a rule (deleted
|
|
335
|
+
* mid-tick, for example).
|
|
336
|
+
* @param tasks - current ledger.
|
|
337
|
+
* @param id - the task to roll forward.
|
|
338
|
+
* @param nextRunAt - next due instant (may be undefined to clear).
|
|
339
|
+
* @param lastTriggeredAt - the trigger instant of this run.
|
|
340
|
+
* @param now - clock instant (ms epoch).
|
|
341
|
+
*/
|
|
342
|
+
function applyScheduleNextRun(tasks, id, nextRunAt, lastTriggeredAt, now) {
|
|
343
|
+
return tasks.map((task) => task.id === id && task.schedule !== void 0 ? withSchedule(task, {
|
|
344
|
+
nextRunAt,
|
|
345
|
+
lastTriggeredAt
|
|
346
|
+
}, now) : task);
|
|
347
|
+
}
|
|
348
|
+
//#endregion
|
|
349
|
+
//#region src/core/use-cases/task-update.ts
|
|
350
|
+
/**
|
|
351
|
+
* Apply an update across the ledger. Tasks that do not match the id are left
|
|
352
|
+
* untouched; the matched task receives the patch plus a fresh updatedAt.
|
|
353
|
+
* @param tasks - current ledger.
|
|
354
|
+
* @param id - the task to update.
|
|
355
|
+
* @param patch - editable-field changes.
|
|
356
|
+
* @param now - clock instant (ms epoch).
|
|
357
|
+
*/
|
|
358
|
+
function applyUpdateTask(tasks, id, patch, now) {
|
|
359
|
+
return tasks.map((task) => task.id === id ? {
|
|
360
|
+
...task,
|
|
361
|
+
...patch,
|
|
362
|
+
updatedAt: now
|
|
363
|
+
} : task);
|
|
364
|
+
}
|
|
365
|
+
//#endregion
|
|
252
366
|
//#region src/core/controller.ts
|
|
253
367
|
/** The selected task (resolved from the ledger), or undefined. */
|
|
254
368
|
function selectedTaskOf(snapshot) {
|
|
@@ -347,18 +461,14 @@ window.__ModuleLoader__.load({
|
|
|
347
461
|
this.notify();
|
|
348
462
|
}
|
|
349
463
|
createTask(input) {
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
this.tasks = [...
|
|
464
|
+
const { task, tasks } = applyCreateTask(this.tasks, input, this.now(), this.uuid());
|
|
465
|
+
if (task === void 0) return void 0;
|
|
466
|
+
this.tasks = [...tasks];
|
|
353
467
|
this.persistAndNotify();
|
|
354
468
|
return task;
|
|
355
469
|
}
|
|
356
470
|
updateTask(id, patch) {
|
|
357
|
-
this.tasks = this.tasks
|
|
358
|
-
...task,
|
|
359
|
-
...patch,
|
|
360
|
-
updatedAt: this.now()
|
|
361
|
-
} : task);
|
|
471
|
+
this.tasks = [...applyUpdateTask(this.tasks, id, patch, this.now())];
|
|
362
472
|
this.persistAndNotify();
|
|
363
473
|
}
|
|
364
474
|
moveTask(id, status) {
|
|
@@ -366,32 +476,24 @@ window.__ModuleLoader__.load({
|
|
|
366
476
|
this.persistAndNotify();
|
|
367
477
|
}
|
|
368
478
|
deleteTask(id) {
|
|
369
|
-
|
|
370
|
-
|
|
479
|
+
const { tasks, selectionCleared } = applyDeleteTask(this.tasks, this.selectedTaskId, id);
|
|
480
|
+
this.tasks = [...tasks];
|
|
481
|
+
if (selectionCleared) this.selectedTaskId = void 0;
|
|
371
482
|
this.persistAndNotify();
|
|
372
483
|
}
|
|
373
484
|
/**
|
|
374
485
|
* Update a task's schedule rule. A blank or invalid cron expression is
|
|
375
486
|
* rejected (returns false, state untouched). When the rule ends up enabled
|
|
376
487
|
* the next run instant is computed immediately; a disabled rule carries no
|
|
377
|
-
* next-run instant.
|
|
488
|
+
* next-run instant. Delegates the domain transition to the schedule use case.
|
|
378
489
|
* @param id - the task to schedule.
|
|
379
490
|
* @param patch - fields to change (absent fields keep their current value).
|
|
380
491
|
* @returns true when applied, false when rejected (invalid cron / unknown task).
|
|
381
492
|
*/
|
|
382
493
|
setSchedule(id, patch) {
|
|
383
|
-
const
|
|
384
|
-
if (
|
|
385
|
-
|
|
386
|
-
const cron = (patch.cron ?? current?.cron ?? "").trim();
|
|
387
|
-
if (cron === "" || !isValidCron(cron)) return false;
|
|
388
|
-
const enabled = patch.enabled ?? current?.enabled ?? false;
|
|
389
|
-
const nextRunAt = enabled ? nextRunAtMs(cron, this.now()) : void 0;
|
|
390
|
-
this.tasks = this.tasks.map((candidate) => candidate.id === id ? withSchedule(candidate, {
|
|
391
|
-
enabled,
|
|
392
|
-
cron,
|
|
393
|
-
nextRunAt
|
|
394
|
-
}, this.now()) : candidate);
|
|
494
|
+
const { tasks, applied } = applySetSchedule(this.tasks, id, patch, this.now());
|
|
495
|
+
if (!applied) return false;
|
|
496
|
+
this.tasks = [...tasks];
|
|
395
497
|
this.persistAndNotify();
|
|
396
498
|
return true;
|
|
397
499
|
}
|
|
@@ -401,10 +503,8 @@ window.__ModuleLoader__.load({
|
|
|
401
503
|
* schedule rule (it was deleted mid-tick, for example).
|
|
402
504
|
*/
|
|
403
505
|
applyScheduleNextRun(id, nextRunAt, lastTriggeredAt) {
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
lastTriggeredAt
|
|
407
|
-
}, this.now()) : task);
|
|
506
|
+
const next = applyScheduleNextRun(this.tasks, id, nextRunAt, lastTriggeredAt, this.now());
|
|
507
|
+
this.tasks = [...next];
|
|
408
508
|
this.persistAndNotify();
|
|
409
509
|
}
|
|
410
510
|
/**
|
|
@@ -736,6 +836,12 @@ window.__ModuleLoader__.load({
|
|
|
736
836
|
* a task still running at its due instant is skipped by the controller's
|
|
737
837
|
* runTask guard and simply waits for the next cron match.
|
|
738
838
|
*
|
|
839
|
+
* The ticker is controlled: `start` arms a single interval guarded by an
|
|
840
|
+
* idempotence check (a second start while running is a no-op, so wiring can
|
|
841
|
+
* never leak a duplicate timer), and `stop`/`dispose` clear the timer and
|
|
842
|
+
* drop the environment listeners. The board unmount path calls stop/dispose,
|
|
843
|
+
* so no interval or listener outlives the board.
|
|
844
|
+
*
|
|
739
845
|
* Framework-free: all runtime access flows through the injected deps
|
|
740
846
|
* (structural faces), so tests drive ticks directly without timers.
|
|
741
847
|
*/
|
|
@@ -746,26 +852,54 @@ window.__ModuleLoader__.load({
|
|
|
746
852
|
var SchedulerService = class {
|
|
747
853
|
deps;
|
|
748
854
|
timer;
|
|
855
|
+
environmentListener;
|
|
749
856
|
disposed = false;
|
|
857
|
+
started = false;
|
|
750
858
|
/** @param deps - tasks/clock/trigger/apply faces (see {@link SchedulerDeps}). */
|
|
751
859
|
constructor(deps) {
|
|
752
860
|
this.deps = deps;
|
|
753
861
|
}
|
|
754
|
-
/**
|
|
862
|
+
/**
|
|
863
|
+
* Start ticking: one immediate check (catch-up after reload) + the interval.
|
|
864
|
+
* Single-instance: a second start while already armed is a no-op, so a
|
|
865
|
+
* re-entrant mount can never stack a duplicate timer.
|
|
866
|
+
*/
|
|
755
867
|
start() {
|
|
756
868
|
if (this.disposed) return;
|
|
869
|
+
if (this.started) return;
|
|
870
|
+
this.started = true;
|
|
757
871
|
this.tick();
|
|
758
872
|
this.timer = setInterval(() => {
|
|
759
873
|
this.tick();
|
|
760
874
|
}, this.deps.tickMs ?? 6e4);
|
|
761
|
-
this.deps.environment
|
|
875
|
+
if (this.deps.environment !== void 0) {
|
|
876
|
+
this.environmentListener = () => {
|
|
877
|
+
this.tick();
|
|
878
|
+
};
|
|
879
|
+
this.deps.environment.addEventListener("visibilitychange", this.environmentListener);
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
/**
|
|
883
|
+
* Stop ticking and drop listeners (idempotent). Preferred shutdown verb for
|
|
884
|
+
* callers that treat the scheduler as a controlled ticker; `dispose` is an
|
|
885
|
+
* alias.
|
|
886
|
+
*/
|
|
887
|
+
stop() {
|
|
888
|
+
this.dispose();
|
|
762
889
|
}
|
|
763
890
|
/** Stop ticking and drop listeners (idempotent). */
|
|
764
891
|
dispose() {
|
|
892
|
+
if (this.disposed && this.timer === void 0 && this.environmentListener === void 0) return;
|
|
765
893
|
this.disposed = true;
|
|
766
|
-
|
|
767
|
-
this.timer
|
|
768
|
-
|
|
894
|
+
this.started = false;
|
|
895
|
+
if (this.timer !== void 0) {
|
|
896
|
+
clearInterval(this.timer);
|
|
897
|
+
this.timer = void 0;
|
|
898
|
+
}
|
|
899
|
+
if (this.environmentListener !== void 0 && this.deps.environment !== void 0) {
|
|
900
|
+
this.deps.environment.removeEventListener("visibilitychange", this.environmentListener);
|
|
901
|
+
this.environmentListener = void 0;
|
|
902
|
+
}
|
|
769
903
|
}
|
|
770
904
|
/**
|
|
771
905
|
* Check every enabled schedule and trigger the due ones. Idempotent per
|
|
@@ -791,9 +925,6 @@ window.__ModuleLoader__.load({
|
|
|
791
925
|
if (await this.deps.runTask(task.id)) this.deps.applySchedule(task.id, next, now);
|
|
792
926
|
}
|
|
793
927
|
}
|
|
794
|
-
onVisibility = () => {
|
|
795
|
-
this.tick();
|
|
796
|
-
};
|
|
797
928
|
};
|
|
798
929
|
//#endregion
|
|
799
930
|
//#region src/core/store.ts
|
|
@@ -953,6 +1084,23 @@ window.__ModuleLoader__.load({
|
|
|
953
1084
|
}
|
|
954
1085
|
};
|
|
955
1086
|
//#endregion
|
|
1087
|
+
//#region src/client/apply-guard.ts
|
|
1088
|
+
/** Claims the plugin apply slot. Returns true when this call won the slot. */
|
|
1089
|
+
function claimTaskboardApply() {
|
|
1090
|
+
if (globalThis.__dshTaskboardApplied === true) return false;
|
|
1091
|
+
globalThis.__dshTaskboardApplied = true;
|
|
1092
|
+
return true;
|
|
1093
|
+
}
|
|
1094
|
+
/**
|
|
1095
|
+
* Releases the claim. Called from the client fiber cleanup so that a
|
|
1096
|
+
* hot-reloaded bundle (the loader unloads the old plugin fiber and invokes
|
|
1097
|
+
* the rebuilt one in the same page) can claim again instead of being
|
|
1098
|
+
* silently dropped.
|
|
1099
|
+
*/
|
|
1100
|
+
function releaseTaskboardApply() {
|
|
1101
|
+
globalThis.__dshTaskboardApplied = void 0;
|
|
1102
|
+
}
|
|
1103
|
+
//#endregion
|
|
956
1104
|
//#region src/client/locales.ts
|
|
957
1105
|
/**
|
|
958
1106
|
* Task-board copy: zh-first dictionaries with an English fallback, selected
|
|
@@ -1335,6 +1483,15 @@ window.__ModuleLoader__.load({
|
|
|
1335
1483
|
}
|
|
1336
1484
|
//#endregion
|
|
1337
1485
|
//#region src/client/board/TaskCard.tsx
|
|
1486
|
+
/**
|
|
1487
|
+
* Task card: the board's column item. Clicking opens the task detail — it
|
|
1488
|
+
* never executes anything directly (detail holds the Run button).
|
|
1489
|
+
*
|
|
1490
|
+
* Memoized: the card re-renders only when its own task record changes, so a
|
|
1491
|
+
* status/filter update on one card (or scrolling) never re-renders every
|
|
1492
|
+
* card on the board. The per-card onClick is built with a stable task reference
|
|
1493
|
+
* by the board, so the memo boundary is effective.
|
|
1494
|
+
*/
|
|
1338
1495
|
/** Compact relative/absolute time label. */
|
|
1339
1496
|
function formatTime(ms) {
|
|
1340
1497
|
const date = new Date(ms);
|
|
@@ -1345,7 +1502,7 @@ window.__ModuleLoader__.load({
|
|
|
1345
1502
|
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`;
|
|
1346
1503
|
}
|
|
1347
1504
|
/** One card in a column. */
|
|
1348
|
-
function
|
|
1505
|
+
function TaskCardInner({ task, onClick }) {
|
|
1349
1506
|
const latest = task.executions[task.executions.length - 1];
|
|
1350
1507
|
const runs = task.executions.length;
|
|
1351
1508
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
@@ -1406,6 +1563,8 @@ window.__ModuleLoader__.load({
|
|
|
1406
1563
|
]
|
|
1407
1564
|
});
|
|
1408
1565
|
}
|
|
1566
|
+
/** Memoized card: re-renders only when the card's own task record changes. */
|
|
1567
|
+
const TaskCard = (0, react.memo)(TaskCardInner);
|
|
1409
1568
|
//#endregion
|
|
1410
1569
|
//#region src/client/board/ConfirmDialog.tsx
|
|
1411
1570
|
/**
|
|
@@ -1809,6 +1968,20 @@ window.__ModuleLoader__.load({
|
|
|
1809
1968
|
const needle = filter.trim().toLowerCase();
|
|
1810
1969
|
return task.title.toLowerCase().includes(needle) || task.description.toLowerCase().includes(needle);
|
|
1811
1970
|
}
|
|
1971
|
+
/**
|
|
1972
|
+
* Memoized per-card adapter: with a stable `onOpen` from the board and an
|
|
1973
|
+
* immutable task record (only the changed card gets a new object ref), a card
|
|
1974
|
+
* re-renders only when its own task changes — not when a sibling card status,
|
|
1975
|
+
* the filter, or the selection moves.
|
|
1976
|
+
*/
|
|
1977
|
+
const MemoTaskCard = (0, react.memo)(function MemoTaskCard({ task, onOpen }) {
|
|
1978
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TaskCard, {
|
|
1979
|
+
task,
|
|
1980
|
+
onClick: (0, react.useCallback)(() => {
|
|
1981
|
+
onOpen(task.id);
|
|
1982
|
+
}, [task.id, onOpen])
|
|
1983
|
+
});
|
|
1984
|
+
});
|
|
1812
1985
|
/** Board component; subscribes to the controller snapshot. */
|
|
1813
1986
|
function TaskBoard({ controller }) {
|
|
1814
1987
|
const [snapshot, setSnapshot] = (0, react.useState)(controller.getSnapshot());
|
|
@@ -1817,6 +1990,9 @@ window.__ModuleLoader__.load({
|
|
|
1817
1990
|
const [showNew, setShowNew] = (0, react.useState)(false);
|
|
1818
1991
|
const selected = selectedTaskOf(snapshot);
|
|
1819
1992
|
const visible = snapshot.tasks.filter((task) => matchesFilter(task, filter));
|
|
1993
|
+
const openTask = (0, react.useCallback)((id) => {
|
|
1994
|
+
controller.openTask(id);
|
|
1995
|
+
}, [controller]);
|
|
1820
1996
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1821
1997
|
className: board_module_css_default.board,
|
|
1822
1998
|
"data-dsh-taskboard-board": "",
|
|
@@ -1882,11 +2058,9 @@ window.__ModuleLoader__.load({
|
|
|
1882
2058
|
]
|
|
1883
2059
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1884
2060
|
className: board_module_css_default.cards,
|
|
1885
|
-
children: [tasks.map((task) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(
|
|
2061
|
+
children: [tasks.map((task) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MemoTaskCard, {
|
|
1886
2062
|
task,
|
|
1887
|
-
|
|
1888
|
-
controller.openTask(task.id);
|
|
1889
|
-
}
|
|
2063
|
+
onOpen: openTask
|
|
1890
2064
|
}, task.id)), tasks.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1891
2065
|
className: board_module_css_default.columnEmpty,
|
|
1892
2066
|
children: t("board.empty")
|
|
@@ -2045,6 +2219,7 @@ window.__ModuleLoader__.load({
|
|
|
2045
2219
|
* @returns disposer removing the entry and its observers.
|
|
2046
2220
|
*/
|
|
2047
2221
|
function mountSidebarEntry(controller) {
|
|
2222
|
+
if (typeof document !== "undefined" && document.querySelector("[data-dsh-taskboard-entry]") !== null) return () => {};
|
|
2048
2223
|
const entry = createEntry(controller);
|
|
2049
2224
|
let root;
|
|
2050
2225
|
let placed = false;
|
|
@@ -2099,7 +2274,7 @@ window.__ModuleLoader__.load({
|
|
|
2099
2274
|
}
|
|
2100
2275
|
//#endregion
|
|
2101
2276
|
//#region \0dsh-css:packages/dsh-task-board/src/client/settings-card.module.css.mjs
|
|
2102
|
-
const css = ".Jh0q7G_card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;list-style:none;transition:border-color .16s,background .16s}.Jh0q7G_card:hover{border-color:var(--dsw-alias-label-dimmed)}.Jh0q7G_cardOpen{background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-label-dimmed)}.Jh0q7G_header{appearance:none;width:100%;font:inherit;color:inherit;text-align:left;cursor:pointer;background:0 0;border:0;border-radius:12px;align-items:center;gap:12px;padding:14px 16px;display:flex}.Jh0q7G_header:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:-2px}.Jh0q7G_headText{flex-direction:column;flex:1;gap:4px;min-width:0;display:flex}.Jh0q7G_name{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:600;line-height:1.4}.Jh0q7G_description{color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:1.5}.Jh0q7G_pending{white-space:nowrap;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary);border-radius:999px;flex:none;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}.Jh0q7G_chevron{color:var(--dsw-alias-label-tertiary);flex:none;transition:transform .16s}.Jh0q7G_chevronOpen{transform:rotate(180deg)}.Jh0q7G_body{border-top:1px solid var(--dsw-alias-border-l2);margin:0 16px;padding-bottom:8px}.Jh0q7G_readOnly{color:var(--dsw-alias-label-tertiary);margin:12px 0 0;font-size:12px;line-height:1.5}.Jh0q7G_notExposed{color:var(--dsw-alias-state-warn-primary);margin:12px 0 0;font-size:12px;line-height:1.5}.Jh0q7G_footer{border-top:1px solid var(--dsw-alias-border-l2);justify-content:flex-end;align-items:center;gap:8px;padding:12px 0 4px;display:flex}.Jh0q7G_failed{min-width:0;color:var(--dsw-alias-label-error);flex:1;margin:0;font-size:12px;line-height:1.5}.Jh0q7G_discard,.Jh0q7G_save{appearance:none;font:inherit;cursor:pointer;border:1px solid #0000;border-radius:8px;padding:5px 14px;font-size:13px;line-height:1.5}.Jh0q7G_discard{border-color:var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);background:0 0}.Jh0q7G_discard:hover:not(:disabled){color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-dimmed)}.Jh0q7G_save{background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3)}.Jh0q7G_discard:disabled,.Jh0q7G_save:disabled{opacity:.4;cursor:default}.Jh0q7G_discard:focus-visible,.Jh0q7G_save:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}.Jh0q7G_field{flex-direction:column;gap:6px;padding:12px 0;display:flex}.Jh0q7G_field+.Jh0q7G_field{border-top:1px solid var(--dsw-alias-border-l2)}.Jh0q7G_head{align-items:center;gap:8px;display:flex}.Jh0q7G_label{min-width:0;color:var(--dsw-alias-label-primary);flex:1;font-size:13px;font-weight:500;line-height:1.5}.Jh0q7G_badges{align-items:center;gap:8px;display:inline-flex}.Jh0q7G_badge{white-space:nowrap;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary);border-radius:999px;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}.Jh0q7G_reset{font:inherit;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;padding:0;font-size:12px;line-height:1.5}.Jh0q7G_reset:hover:not(:disabled){color:var(--dsw-alias-label-primary)}.Jh0q7G_reset:disabled{cursor:default}.Jh0q7G_reset:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px}.Jh0q7G_input,.Jh0q7G_select{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);height:34px;font:inherit;color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 12px;font-size:13px;line-height:1.5}.Jh0q7G_input:focus-visible,.Jh0q7G_select:focus-visible{border-color:var(--dsw-alias-brand-primary);outline:none}.Jh0q7G_input:disabled,.Jh0q7G_select:disabled{color:var(--dsw-alias-label-tertiary);cursor:default}.Jh0q7G_inputInvalid{border:1px solid var(--dsw-alias-label-error);background:var(--dsw-alias-bg-layer-3);height:34px;font:inherit;color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 12px;font-size:13px;line-height:1.5}.Jh0q7G_inputInvalid:focus-visible{outline:2px solid var(--dsw-alias-label-error);outline-offset:1px;border-color:var(--dsw-alias-label-error)}.Jh0q7G_invalid{color:var(--dsw-alias-label-error);margin:0;font-size:12px;line-height:1.5}.Jh0q7G_hint{color:var(--dsw-alias-label-tertiary);margin:0;font-size:12px;line-height:1.5}@media (prefers-reduced-motion:reduce){.Jh0q7G_card,.Jh0q7G_header,.Jh0q7G_chevron,.Jh0q7G_chevronOpen,.Jh0q7G_discard,.Jh0q7G_save{transition:none}}";
|
|
2277
|
+
const css = ".Jh0q7G_card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;list-style:none;transition:border-color .16s,background .16s}.Jh0q7G_card:hover{border-color:var(--dsw-alias-label-dimmed)}.Jh0q7G_cardOpen{background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-label-dimmed)}.Jh0q7G_header{appearance:none;width:100%;font:inherit;color:inherit;text-align:left;cursor:pointer;background:0 0;border:0;border-radius:12px;align-items:center;gap:12px;padding:14px 16px;display:flex}.Jh0q7G_header:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:-2px}.Jh0q7G_headText{flex-direction:column;flex:1;gap:4px;min-width:0;display:flex}.Jh0q7G_name{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:600;line-height:1.4}.Jh0q7G_description{color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:1.5}.Jh0q7G_pending{white-space:nowrap;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary);border-radius:999px;flex:none;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}.Jh0q7G_chevron{color:var(--dsw-alias-label-tertiary);flex:none;transition:transform .16s}.Jh0q7G_chevronOpen{transform:rotate(180deg)}.Jh0q7G_body{border-top:1px solid var(--dsw-alias-border-l2);margin:0 16px;padding-bottom:8px}.Jh0q7G_readOnly{color:var(--dsw-alias-label-tertiary);margin:12px 0 0;font-size:12px;line-height:1.5}.Jh0q7G_notExposed{color:var(--dsw-alias-state-warn-primary);margin:12px 0 0;font-size:12px;line-height:1.5}.Jh0q7G_footer{border-top:1px solid var(--dsw-alias-border-l2);justify-content:flex-end;align-items:center;gap:8px;padding:12px 0 4px;display:flex}.Jh0q7G_failed{min-width:0;color:var(--dsw-alias-label-error);flex:1;margin:0;font-size:12px;line-height:1.5}.Jh0q7G_discard,.Jh0q7G_save{appearance:none;font:inherit;cursor:pointer;border:1px solid #0000;border-radius:8px;padding:5px 14px;font-size:13px;line-height:1.5}.Jh0q7G_discard{border-color:var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);background:0 0}.Jh0q7G_discard:hover:not(:disabled){color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-dimmed)}.Jh0q7G_save{background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3)}.Jh0q7G_discard:disabled,.Jh0q7G_save:disabled{opacity:.4;cursor:default}.Jh0q7G_discard:focus-visible,.Jh0q7G_save:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}.Jh0q7G_field{flex-direction:column;gap:6px;padding:12px 0;display:flex}.Jh0q7G_field+.Jh0q7G_field{border-top:1px solid var(--dsw-alias-border-l2)}.Jh0q7G_head{align-items:center;gap:8px;display:flex}.Jh0q7G_label{min-width:0;color:var(--dsw-alias-label-primary);flex:1;font-size:13px;font-weight:500;line-height:1.5}.Jh0q7G_badges{align-items:center;gap:8px;display:inline-flex}.Jh0q7G_badge{white-space:nowrap;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary);border-radius:999px;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}.Jh0q7G_reset{font:inherit;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;padding:0;font-size:12px;line-height:1.5}.Jh0q7G_reset:hover:not(:disabled){color:var(--dsw-alias-label-primary)}.Jh0q7G_reset:disabled{cursor:default}.Jh0q7G_reset:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px;outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px}.Jh0q7G_input,.Jh0q7G_select{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);height:34px;font:inherit;color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 12px;font-size:13px;line-height:1.5}.Jh0q7G_input:focus-visible,.Jh0q7G_select:focus-visible{border-color:var(--dsw-alias-brand-primary);outline:none}.Jh0q7G_input:disabled,.Jh0q7G_select:disabled{color:var(--dsw-alias-label-tertiary);cursor:default}.Jh0q7G_inputInvalid{border:1px solid var(--dsw-alias-label-error);background:var(--dsw-alias-bg-layer-3);height:34px;font:inherit;color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 12px;font-size:13px;line-height:1.5}.Jh0q7G_inputInvalid:focus-visible{outline:2px solid var(--dsw-alias-label-error);outline-offset:1px;border-color:var(--dsw-alias-label-error)}.Jh0q7G_invalid{color:var(--dsw-alias-label-error);margin:0;font-size:12px;line-height:1.5}.Jh0q7G_hint{color:var(--dsw-alias-label-tertiary);margin:0;font-size:12px;line-height:1.5}@media (prefers-reduced-motion:reduce){.Jh0q7G_card,.Jh0q7G_header,.Jh0q7G_chevron,.Jh0q7G_chevronOpen,.Jh0q7G_discard,.Jh0q7G_save{transition:none}}";
|
|
2103
2278
|
const tagId = "@linxin666/dsh-client-ui-task-board/settings-card.module.css";
|
|
2104
2279
|
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
|
|
2105
2280
|
const tag = document.createElement("style");
|
|
@@ -2140,12 +2315,12 @@ window.__ModuleLoader__.load({
|
|
|
2140
2315
|
//#endregion
|
|
2141
2316
|
//#region src/client/PluginSettingsCard.tsx
|
|
2142
2317
|
/**
|
|
2143
|
-
*
|
|
2144
|
-
* plugin and what its settings govern, the controls inside, and the save
|
|
2145
|
-
* writes them. Renders nothing while the namespace is unavailable — a
|
|
2318
|
+
* Family-shared chrome for plugin settings cards: a disclosure header naming
|
|
2319
|
+
* the plugin and what its settings govern, the controls inside, and the save
|
|
2320
|
+
* that writes them. Renders nothing while the namespace is unavailable — a
|
|
2146
2321
|
* deployment that does not compose the owning plugin should show no trace of
|
|
2147
|
-
* it.
|
|
2148
|
-
*
|
|
2322
|
+
* it. Inlined into each consumer's client bundle; mirrors the official
|
|
2323
|
+
* ui-plugin-config PluginCard in a self-contained slice.
|
|
2149
2324
|
*/
|
|
2150
2325
|
/**
|
|
2151
2326
|
* Render one plugin settings card.
|
|
@@ -2157,6 +2332,7 @@ window.__ModuleLoader__.load({
|
|
|
2157
2332
|
const { state } = props;
|
|
2158
2333
|
if (!state.available) return null;
|
|
2159
2334
|
const title = props.t(props.titleKey);
|
|
2335
|
+
const description = props.t(props.descriptionKey);
|
|
2160
2336
|
const blocked = !state.dirty || state.invalid || state.saving;
|
|
2161
2337
|
const cardClass = open ? `${settings_card_module_css_default.cardOpen} ${settings_card_module_css_default.card}` : settings_card_module_css_default.card;
|
|
2162
2338
|
if (!state.exposed) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
|
|
@@ -2173,10 +2349,12 @@ window.__ModuleLoader__.load({
|
|
|
2173
2349
|
className: settings_card_module_css_default.headText,
|
|
2174
2350
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2175
2351
|
className: settings_card_module_css_default.name,
|
|
2352
|
+
title,
|
|
2176
2353
|
children: title
|
|
2177
2354
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2178
2355
|
className: settings_card_module_css_default.description,
|
|
2179
|
-
|
|
2356
|
+
title: description,
|
|
2357
|
+
children: description
|
|
2180
2358
|
})]
|
|
2181
2359
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
|
|
2182
2360
|
width: "14",
|
|
@@ -2214,14 +2392,17 @@ window.__ModuleLoader__.load({
|
|
|
2214
2392
|
className: settings_card_module_css_default.headText,
|
|
2215
2393
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2216
2394
|
className: settings_card_module_css_default.name,
|
|
2395
|
+
title,
|
|
2217
2396
|
children: title
|
|
2218
2397
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2219
2398
|
className: settings_card_module_css_default.description,
|
|
2220
|
-
|
|
2399
|
+
title: description,
|
|
2400
|
+
children: description
|
|
2221
2401
|
})]
|
|
2222
2402
|
}),
|
|
2223
2403
|
state.dirty ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2224
2404
|
className: settings_card_module_css_default.pending,
|
|
2405
|
+
title: props.t("settings.unsaved"),
|
|
2225
2406
|
children: props.t("settings.unsaved")
|
|
2226
2407
|
}) : null,
|
|
2227
2408
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
|
|
@@ -2337,11 +2518,13 @@ window.__ModuleLoader__.load({
|
|
|
2337
2518
|
field,
|
|
2338
2519
|
format: (value) => typeof value === "boolean" ? String(value) : "",
|
|
2339
2520
|
parse: (text) => {
|
|
2340
|
-
|
|
2521
|
+
const trimmed = text.trim();
|
|
2522
|
+
if (trimmed === "") return { kind: "clear" };
|
|
2523
|
+
if (trimmed === "true") return {
|
|
2341
2524
|
kind: "set",
|
|
2342
2525
|
value: true
|
|
2343
2526
|
};
|
|
2344
|
-
if (
|
|
2527
|
+
if (trimmed === "false") return {
|
|
2345
2528
|
kind: "set",
|
|
2346
2529
|
value: false
|
|
2347
2530
|
};
|
|
@@ -2443,12 +2626,13 @@ window.__ModuleLoader__.load({
|
|
|
2443
2626
|
const plan = this.plan();
|
|
2444
2627
|
const writes = plan.flatMap((item) => item.run === void 0 ? [] : [item.run]);
|
|
2445
2628
|
if (plan.length === 0 || this.saving || writes.length !== plan.length) return;
|
|
2629
|
+
const fields = new Set(plan.map((item) => item.field));
|
|
2446
2630
|
this.saving = true;
|
|
2447
2631
|
this.failed = false;
|
|
2448
2632
|
this.publish();
|
|
2449
2633
|
let landed = true;
|
|
2450
2634
|
for (const write of writes) landed = await write() && landed;
|
|
2451
|
-
if (landed) this.staged.
|
|
2635
|
+
if (landed) for (const field of fields) this.staged.delete(field);
|
|
2452
2636
|
this.saving = false;
|
|
2453
2637
|
this.failed = !landed;
|
|
2454
2638
|
this.publish();
|
|
@@ -2631,6 +2815,8 @@ window.__ModuleLoader__.load({
|
|
|
2631
2815
|
* @param ctx - client root context (services: sessions, workspaces).
|
|
2632
2816
|
*/
|
|
2633
2817
|
function apply(ctx) {
|
|
2818
|
+
if (!claimTaskboardApply()) return;
|
|
2819
|
+
ctx.effect(() => releaseTaskboardApply, "task-board: apply claim");
|
|
2634
2820
|
ctx.effect(() => ctx.locale.register(NS, {
|
|
2635
2821
|
zh,
|
|
2636
2822
|
en
|