@qihongmu/dsh-client-ui-scheduled-task 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cordis.patch.yml +5 -0
- package/lib/client.js +1563 -0
- package/lib/types/client/ScheduleEditor.d.ts +40 -0
- package/lib/types/client/ScheduleEditor.js +114 -0
- package/lib/types/client/ScheduledTaskCreate.d.ts +26 -0
- package/lib/types/client/ScheduledTaskCreate.js +194 -0
- package/lib/types/client/ScheduledTasksList.d.ts +20 -0
- package/lib/types/client/ScheduledTasksList.js +45 -0
- package/lib/types/client/ScheduledTasksPanel.d.ts +13 -0
- package/lib/types/client/ScheduledTasksPanel.js +134 -0
- package/lib/types/client/format.d.ts +14 -0
- package/lib/types/client/format.js +46 -0
- package/lib/types/client/index.d.ts +30 -0
- package/lib/types/client/index.js +70 -0
- package/lib/types/client/locales.d.ts +294 -0
- package/lib/types/client/locales.js +290 -0
- package/lib/types/client/slots.d.ts +44 -0
- package/lib/types/client/slots.js +4 -0
- package/lib/types/index.d.ts +9 -0
- package/lib/types/index.js +8 -0
- package/lib/types/invariant.d.ts +16 -0
- package/lib/types/invariant.js +21 -0
- package/package.json +71 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Inline schedule editor (图 of 调度): a preset dropdown (每小时/每天/每周/每月/自定义)
|
|
3
|
+
* followed per preset by compact sub-selectors, a live summary line, and a
|
|
4
|
+
* clear (trash) action. Controlled by the parent create form, which owns the
|
|
5
|
+
* state and turns the chosen preset into a wire rule selector. The wall-clock
|
|
6
|
+
* presets carry an editable IANA time-zone field; the custom presets keep
|
|
7
|
+
* their own at/after/every fields in the parent.
|
|
8
|
+
*/
|
|
9
|
+
import type { ScheduledTaskTranslate } from './format.ts';
|
|
10
|
+
/** The schedule presets the editor offers. */
|
|
11
|
+
export type SchedulePreset = '' | 'hourly' | 'daily' | 'weekly' | 'monthly' | 'custom';
|
|
12
|
+
/**
|
|
13
|
+
* Timezone field: displays the GMT offset (e.g. `GMT+8`) and hides the IANA
|
|
14
|
+
* region name; clicking the chip swaps to a short IANA editor (the wire value
|
|
15
|
+
* must stay an IANA name), and Enter / blur / Escape return to the offset chip.
|
|
16
|
+
*/
|
|
17
|
+
export declare function TimezoneField({ t, value, onChange }: {
|
|
18
|
+
t: ScheduledTaskTranslate;
|
|
19
|
+
value: string;
|
|
20
|
+
onChange: (next: string) => void;
|
|
21
|
+
}): import("react").JSX.Element;
|
|
22
|
+
export interface ScheduleEditorProps {
|
|
23
|
+
readonly t: ScheduledTaskTranslate;
|
|
24
|
+
readonly preset: SchedulePreset;
|
|
25
|
+
readonly minute: string;
|
|
26
|
+
readonly time: string;
|
|
27
|
+
readonly timeZone: string;
|
|
28
|
+
readonly weekdays: number[];
|
|
29
|
+
readonly dayOfMonth: string;
|
|
30
|
+
readonly onPresetChange: (preset: SchedulePreset) => void;
|
|
31
|
+
readonly onMinuteChange: (minute: string) => void;
|
|
32
|
+
readonly onTimeChange: (time: string) => void;
|
|
33
|
+
readonly onWeekdaysChange: (weekdays: number[]) => void;
|
|
34
|
+
readonly onDayOfMonthChange: (day: string) => void;
|
|
35
|
+
readonly onTimeZoneChange: (timeZone: string) => void;
|
|
36
|
+
readonly onClear: () => void;
|
|
37
|
+
}
|
|
38
|
+
/** Render the inline schedule editor field. */
|
|
39
|
+
export declare function ScheduleEditor({ t, preset, minute, time, timeZone, weekdays, dayOfMonth, onPresetChange, onMinuteChange, onTimeChange, onWeekdaysChange, onDayOfMonthChange, onTimeZoneChange, onClear, }: ScheduleEditorProps): import("react").JSX.Element;
|
|
40
|
+
//# sourceMappingURL=ScheduleEditor.d.ts.map
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Inline schedule editor (图 of 调度): a preset dropdown (每小时/每天/每周/每月/自定义)
|
|
4
|
+
* followed per preset by compact sub-selectors, a live summary line, and a
|
|
5
|
+
* clear (trash) action. Controlled by the parent create form, which owns the
|
|
6
|
+
* state and turns the chosen preset into a wire rule selector. The wall-clock
|
|
7
|
+
* presets carry an editable IANA time-zone field; the custom presets keep
|
|
8
|
+
* their own at/after/every fields in the parent.
|
|
9
|
+
*/
|
|
10
|
+
import { useEffect, useRef, useState } from 'react';
|
|
11
|
+
import { IconChevronDownOutline14, IconTrashOutline16 } from '@deepseek-ai/dsh-client-ui-primitives';
|
|
12
|
+
import { weekdaySummary } from "./format.js";
|
|
13
|
+
import css from './ScheduledTasksPanel.module.css';
|
|
14
|
+
/** Locale key per weekday (1=Mon..7=Sun). */
|
|
15
|
+
const WEEKDAY_KEYS = {
|
|
16
|
+
1: 'schedule.wd.1', 2: 'schedule.wd.2', 3: 'schedule.wd.3', 4: 'schedule.wd.4',
|
|
17
|
+
5: 'schedule.wd.5', 6: 'schedule.wd.6', 7: 'schedule.wd.7',
|
|
18
|
+
};
|
|
19
|
+
const MINUTES = Array.from({ length: 60 }, (_, index) => String(index).padStart(2, '0'));
|
|
20
|
+
const DAYS = Array.from({ length: 31 }, (_, index) => String(index + 1));
|
|
21
|
+
/** Render the current schedule as the summary line under/next to the segments. */
|
|
22
|
+
function previewLine(t, preset, minute, time, weekdays, dayOfMonth) {
|
|
23
|
+
switch (preset) {
|
|
24
|
+
case 'hourly':
|
|
25
|
+
return t('schedule.preview.hourly', { minute });
|
|
26
|
+
case 'daily':
|
|
27
|
+
return t('schedule.preview.daily', { time });
|
|
28
|
+
case 'weekly':
|
|
29
|
+
return t('schedule.preview.weekly', {
|
|
30
|
+
weekdays: weekdaySummary(t, weekdays),
|
|
31
|
+
time,
|
|
32
|
+
});
|
|
33
|
+
case 'monthly':
|
|
34
|
+
return t('schedule.preview.monthly', { day: dayOfMonth, time });
|
|
35
|
+
default:
|
|
36
|
+
return '';
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/** Compact GMT offset (e.g. `GMT+8`, `GMT+5:30`, `GMT-4`); falls back to the IANA name. */
|
|
40
|
+
function gmtOffset(timeZone) {
|
|
41
|
+
if (timeZone.trim() === '')
|
|
42
|
+
return '';
|
|
43
|
+
try {
|
|
44
|
+
const part = new Intl.DateTimeFormat('en-US', { timeZone, timeZoneName: 'shortOffset' })
|
|
45
|
+
.formatToParts(new Date())
|
|
46
|
+
.find(piece => piece.type === 'timeZoneName');
|
|
47
|
+
return part?.value ?? timeZone;
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return timeZone;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Timezone field: displays the GMT offset (e.g. `GMT+8`) and hides the IANA
|
|
55
|
+
* region name; clicking the chip swaps to a short IANA editor (the wire value
|
|
56
|
+
* must stay an IANA name), and Enter / blur / Escape return to the offset chip.
|
|
57
|
+
*/
|
|
58
|
+
export function TimezoneField({ t, value, onChange }) {
|
|
59
|
+
const [editing, setEditing] = useState(false);
|
|
60
|
+
const inputRef = useRef(null);
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
if (editing && inputRef.current !== null)
|
|
63
|
+
inputRef.current.focus();
|
|
64
|
+
}, [editing]);
|
|
65
|
+
if (!editing) {
|
|
66
|
+
return (_jsx("button", { type: "button", className: css.timeZoneChip, title: value, "aria-label": t('form.timeZone'), onClick: () => { setEditing(true); }, children: gmtOffset(value) }));
|
|
67
|
+
}
|
|
68
|
+
return (_jsx("input", { ref: inputRef, className: css.timeZoneInput, value: value, spellCheck: false, "aria-label": t('form.timeZone'), onChange: (event) => { onChange(event.target.value); }, onBlur: () => { setEditing(false); }, onKeyDown: (event) => {
|
|
69
|
+
if (event.key === 'Enter' || event.key === 'Escape')
|
|
70
|
+
event.currentTarget.blur();
|
|
71
|
+
} }));
|
|
72
|
+
}
|
|
73
|
+
/** A compact weekday multi-select (toggle chips in a popover). */
|
|
74
|
+
function WeekdayPicker({ t, value, onChange }) {
|
|
75
|
+
const [open, setOpen] = useState(false);
|
|
76
|
+
const rootRef = useRef(null);
|
|
77
|
+
const toggle = (day, present) => {
|
|
78
|
+
onChange(present ? value.filter(item => item !== day) : [...value, day].sort((a, b) => a - b));
|
|
79
|
+
};
|
|
80
|
+
const summary = value.length === 0 ? t('schedule.pickWeekdays') : weekdaySummary(t, value);
|
|
81
|
+
// Dismiss on outside mousedown and on Escape. Listened on `document` (not a
|
|
82
|
+
// fixed backdrop, which used to swallow the toggle's mousedown and
|
|
83
|
+
// close-then-reopen it) and on the CAPTURE phase so Escape closes only this
|
|
84
|
+
// popover — the drawer's own Escape handler must not fire at the same time.
|
|
85
|
+
useEffect(() => {
|
|
86
|
+
if (!open)
|
|
87
|
+
return;
|
|
88
|
+
const onMouseDown = (event) => {
|
|
89
|
+
if (rootRef.current !== null && !rootRef.current.contains(event.target))
|
|
90
|
+
setOpen(false);
|
|
91
|
+
};
|
|
92
|
+
const onKeyDown = (event) => {
|
|
93
|
+
if (event.key !== 'Escape')
|
|
94
|
+
return;
|
|
95
|
+
setOpen(false);
|
|
96
|
+
event.stopImmediatePropagation();
|
|
97
|
+
};
|
|
98
|
+
document.addEventListener('mousedown', onMouseDown);
|
|
99
|
+
document.addEventListener('keydown', onKeyDown, true);
|
|
100
|
+
return () => {
|
|
101
|
+
document.removeEventListener('mousedown', onMouseDown);
|
|
102
|
+
document.removeEventListener('keydown', onKeyDown, true);
|
|
103
|
+
};
|
|
104
|
+
}, [open]);
|
|
105
|
+
return (_jsxs("div", { ref: rootRef, className: css.weekPicker, children: [_jsxs("button", { type: "button", className: css.segmentButton, "aria-expanded": open, "aria-haspopup": "listbox", onClick: () => { setOpen(current => !current); }, children: [_jsx("span", { children: summary }), _jsx(IconChevronDownOutline14, { size: 12 })] }), open && (_jsx("div", { className: css.weekMenu, role: "listbox", "aria-multiselectable": "true", children: [1, 2, 3, 4, 5, 6, 7].map(day => {
|
|
106
|
+
const checked = value.includes(day);
|
|
107
|
+
return (_jsxs("label", { className: css.weekOption, children: [_jsx("input", { type: "checkbox", checked: checked, onChange: () => { toggle(day, checked); } }), _jsx("span", { children: t(WEEKDAY_KEYS[day]) })] }, day));
|
|
108
|
+
}) }))] }));
|
|
109
|
+
}
|
|
110
|
+
/** Render the inline schedule editor field. */
|
|
111
|
+
export function ScheduleEditor({ t, preset, minute, time, timeZone, weekdays, dayOfMonth, onPresetChange, onMinuteChange, onTimeChange, onWeekdaysChange, onDayOfMonthChange, onTimeZoneChange, onClear, }) {
|
|
112
|
+
const preview = previewLine(t, preset, minute, time, weekdays, dayOfMonth);
|
|
113
|
+
return (_jsx("div", { className: css.scheduleEditor, children: _jsxs("div", { className: css.scheduleRow, children: [_jsxs("select", { className: css.segmentSelect, value: preset, onChange: (event) => { onPresetChange(event.target.value); }, "aria-label": t('form.schedule.label'), children: [_jsx("option", { value: "", children: t('form.schedule.add') }), _jsx("option", { value: "hourly", children: t('schedule.hourly') }), _jsx("option", { value: "daily", children: t('schedule.daily') }), _jsx("option", { value: "weekly", children: t('schedule.weekly') }), _jsx("option", { value: "monthly", children: t('schedule.monthly') }), _jsx("option", { value: "custom", children: t('schedule.custom') })] }), preset === 'hourly' && (_jsxs(_Fragment, { children: [_jsx("span", { className: css.segmentText, children: t('schedule.hourlyAtMinute') }), _jsx("select", { className: css.segmentSelect, value: minute, onChange: (event) => { onMinuteChange(event.target.value); }, children: MINUTES.map(value => _jsx("option", { value: value, children: value }, value)) }), _jsx("span", { className: css.segmentText, children: t('schedule.minuteUnit') })] })), (preset === 'daily' || preset === 'weekly' || preset === 'monthly') && (_jsxs(_Fragment, { children: [preset === 'daily' && _jsx("span", { className: css.segmentText, children: t('schedule.at') }), preset === 'weekly' && (_jsx(WeekdayPicker, { t: t, value: weekdays, onChange: onWeekdaysChange })), preset === 'weekly' && _jsx("span", { className: css.segmentText, children: t('schedule.at') }), preset === 'monthly' && (_jsxs(_Fragment, { children: [_jsx("select", { className: css.segmentSelect, value: dayOfMonth, onChange: (event) => { onDayOfMonthChange(event.target.value); }, children: DAYS.map(value => _jsxs("option", { value: value, children: [value, " ", t('schedule.dayUnit')] }, value)) }), _jsx("span", { className: css.segmentText, children: t('schedule.at') })] })), _jsx("input", { className: css.segmentTime, type: "time", value: time, onChange: (event) => { onTimeChange(event.target.value); } }), _jsx(TimezoneField, { t: t, value: timeZone, onChange: onTimeZoneChange })] })), preview !== '' && _jsx("span", { className: css.schedulePreview, children: preview }), _jsx("button", { type: "button", className: css.clearButton, "aria-label": t('schedule.clear'), onClick: onClear, children: _jsx(IconTrashOutline16, { size: 14 }) })] }) }));
|
|
114
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 新建定时任务 create/edit view (the drawer body): the inline schedule editor
|
|
3
|
+
* (每小时/每天/每周/每月/自定义), title, the instruction Prompt, project (DSH
|
|
4
|
+
* workspace), confirm-before-change toggle, and model selection.
|
|
5
|
+
*/
|
|
6
|
+
import type { ScheduledTaskCreateInput, ScheduledTaskView } from '@qihongmu/dsh-plugins-scheduled-task/types';
|
|
7
|
+
import type { ScheduledTaskModelOption, ScheduledTaskProjectOption } from './slots.ts';
|
|
8
|
+
import type { ScheduledTaskTranslate } from './format.ts';
|
|
9
|
+
export interface ScheduledTaskCreateProps {
|
|
10
|
+
readonly t: ScheduledTaskTranslate;
|
|
11
|
+
readonly busy: boolean;
|
|
12
|
+
/** Present in edit mode; absent creates a new task. */
|
|
13
|
+
readonly task?: ScheduledTaskView | undefined;
|
|
14
|
+
readonly listProjects: () => Promise<readonly ScheduledTaskProjectOption[]>;
|
|
15
|
+
readonly listModels: () => Promise<readonly ScheduledTaskModelOption[]>;
|
|
16
|
+
readonly onCancel: () => void;
|
|
17
|
+
readonly onSubmit: (input: ScheduledTaskCreateInput) => Promise<{
|
|
18
|
+
ok: true;
|
|
19
|
+
} | {
|
|
20
|
+
ok: false;
|
|
21
|
+
message: string;
|
|
22
|
+
}>;
|
|
23
|
+
}
|
|
24
|
+
/** Render the create/edit scheduled-task form. */
|
|
25
|
+
export declare function ScheduledTaskCreate({ t, busy, task, listProjects, listModels, onCancel, onSubmit, }: ScheduledTaskCreateProps): import("react").JSX.Element;
|
|
26
|
+
//# sourceMappingURL=ScheduledTaskCreate.d.ts.map
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* 新建定时任务 create/edit view (the drawer body): the inline schedule editor
|
|
4
|
+
* (每小时/每天/每周/每月/自定义), title, the instruction Prompt, project (DSH
|
|
5
|
+
* workspace), confirm-before-change toggle, and model selection.
|
|
6
|
+
*/
|
|
7
|
+
import { useEffect, useState } from 'react';
|
|
8
|
+
import { ScheduleEditor, TimezoneField } from "./ScheduleEditor.js";
|
|
9
|
+
import css from './ScheduledTasksPanel.module.css';
|
|
10
|
+
/** Local zone used as the default for daily/weekly/monthly. */
|
|
11
|
+
function localTimeZone() {
|
|
12
|
+
return Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
13
|
+
}
|
|
14
|
+
/** Weekday set assumed when a stored rule does not carry one (Mon–Fri). */
|
|
15
|
+
const WORKDAYS = [1, 2, 3, 4, 5];
|
|
16
|
+
/** Neutral editor state shared by every preset before its own overrides. */
|
|
17
|
+
const DEFAULT_SCHEDULE = {
|
|
18
|
+
customKind: 'at',
|
|
19
|
+
minute: '00',
|
|
20
|
+
time: '09:00',
|
|
21
|
+
timeZone: localTimeZone(),
|
|
22
|
+
weekdays: [...WORKDAYS],
|
|
23
|
+
dayOfMonth: '1',
|
|
24
|
+
};
|
|
25
|
+
/** Resolve the editor state from an edit task (or create defaults). */
|
|
26
|
+
function initialSchedule(task) {
|
|
27
|
+
const base = { ...DEFAULT_SCHEDULE, timeZone: localTimeZone() };
|
|
28
|
+
if (task === undefined)
|
|
29
|
+
return { ...base, preset: '' };
|
|
30
|
+
const rule = task.rule;
|
|
31
|
+
switch (rule.kind) {
|
|
32
|
+
case 'hourly':
|
|
33
|
+
return { ...base, preset: 'hourly', minute: String(rule.minute).padStart(2, '0') };
|
|
34
|
+
case 'daily':
|
|
35
|
+
return { ...base, preset: 'daily', time: rule.time, timeZone: rule.time_zone };
|
|
36
|
+
case 'weekly':
|
|
37
|
+
return { ...base, preset: 'weekly', time: rule.time, timeZone: rule.time_zone, weekdays: [...rule.weekdays] };
|
|
38
|
+
case 'monthly':
|
|
39
|
+
return { ...base, preset: 'monthly', time: rule.time, timeZone: rule.time_zone, dayOfMonth: String(rule.dayOfMonth) };
|
|
40
|
+
case 'every':
|
|
41
|
+
return { ...base, preset: 'custom', customKind: 'every' };
|
|
42
|
+
case 'after':
|
|
43
|
+
return { ...base, preset: 'custom', customKind: 'after' };
|
|
44
|
+
case 'at':
|
|
45
|
+
return { ...base, preset: 'custom', customKind: 'at' };
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/** Validate an `HH:mm` string. */
|
|
49
|
+
function isTime(time) {
|
|
50
|
+
return /^([01]\d|2[0-3]):([0-5]\d)$/.test(time);
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Split a `<providerId>/<modelId>` option key at the FIRST slash — model ids may
|
|
54
|
+
* themselves contain slashes (e.g. openrouter's `stealth/ox-alpha`), so a naive
|
|
55
|
+
* `split('/')` would truncate the model id.
|
|
56
|
+
*/
|
|
57
|
+
function parseModelKey(key) {
|
|
58
|
+
const at = key.indexOf('/');
|
|
59
|
+
if (at <= 0 || at === key.length - 1)
|
|
60
|
+
return undefined;
|
|
61
|
+
return { provider: key.slice(0, at), model: key.slice(at + 1) };
|
|
62
|
+
}
|
|
63
|
+
const IDLE_OPTIONS = { status: 'loading', items: [], error: '' };
|
|
64
|
+
/** Render the create/edit scheduled-task form. */
|
|
65
|
+
export function ScheduledTaskCreate({ t, busy, task, listProjects, listModels, onCancel, onSubmit, }) {
|
|
66
|
+
const [title, setTitle] = useState(task?.title ?? '');
|
|
67
|
+
const [prompt, setPrompt] = useState(task?.prompt ?? '');
|
|
68
|
+
const [schedule, setSchedule] = useState(() => initialSchedule(task));
|
|
69
|
+
const [date, setDate] = useState('');
|
|
70
|
+
const [atTime, setAtTime] = useState('');
|
|
71
|
+
const [afterSeconds, setAfterSeconds] = useState('60');
|
|
72
|
+
const [everySeconds, setEverySeconds] = useState('300');
|
|
73
|
+
const [projectKey, setProjectKey] = useState(task?.workspaceId ?? '');
|
|
74
|
+
const [modelKey, setModelKey] = useState(task?.model === undefined ? '' : `${task.model.provider}/${task.model.model}`);
|
|
75
|
+
const [confirmBeforeChange, setConfirmBeforeChange] = useState(task?.confirmBeforeChange ?? false);
|
|
76
|
+
const [projects, setProjects] = useState(IDLE_OPTIONS);
|
|
77
|
+
const [models, setModels] = useState(IDLE_OPTIONS);
|
|
78
|
+
const [formError, setFormError] = useState(null);
|
|
79
|
+
const { preset, customKind, minute, time, timeZone, weekdays, dayOfMonth } = schedule;
|
|
80
|
+
// Edit-mode `at` rules prefill the calendar fields from the stored instant.
|
|
81
|
+
useEffect(() => {
|
|
82
|
+
if (task?.rule.kind !== 'at')
|
|
83
|
+
return;
|
|
84
|
+
const at = new Date(task.rule.scheduledAt);
|
|
85
|
+
setDate([at.getFullYear(), String(at.getMonth() + 1).padStart(2, '0'), String(at.getDate()).padStart(2, '0')].join('-'));
|
|
86
|
+
setAtTime(`${String(at.getHours()).padStart(2, '0')}:${String(at.getMinutes()).padStart(2, '0')}`);
|
|
87
|
+
}, [task]);
|
|
88
|
+
// Load the two option catalogs once; failures keep the fields optional.
|
|
89
|
+
useEffect(() => {
|
|
90
|
+
let alive = true;
|
|
91
|
+
listProjects().then(items => { if (alive)
|
|
92
|
+
setProjects({ status: 'ready', items }); }, error => {
|
|
93
|
+
if (alive)
|
|
94
|
+
setProjects({ status: 'failed', items: [], error: error instanceof Error ? error.message : String(error) });
|
|
95
|
+
});
|
|
96
|
+
listModels().then(items => { if (alive)
|
|
97
|
+
setModels({ status: 'ready', items }); }, error => {
|
|
98
|
+
if (alive)
|
|
99
|
+
setModels({ status: 'failed', items: [], error: error instanceof Error ? error.message : String(error) });
|
|
100
|
+
});
|
|
101
|
+
return () => { alive = false; };
|
|
102
|
+
}, [listProjects, listModels]);
|
|
103
|
+
const patchSchedule = (patch) => {
|
|
104
|
+
setSchedule(current => ({ ...current, ...patch }));
|
|
105
|
+
};
|
|
106
|
+
const setPreset = (next) => { patchSchedule({ preset: next }); };
|
|
107
|
+
const selectorValid = (() => {
|
|
108
|
+
switch (preset) {
|
|
109
|
+
case '':
|
|
110
|
+
return false;
|
|
111
|
+
case 'hourly':
|
|
112
|
+
return /^\d{1,2}$/.test(minute) && Number(minute) >= 0 && Number(minute) <= 59;
|
|
113
|
+
case 'daily':
|
|
114
|
+
return isTime(time) && timeZone.trim() !== '';
|
|
115
|
+
case 'weekly':
|
|
116
|
+
return weekdays.length > 0 && isTime(time) && timeZone.trim() !== '';
|
|
117
|
+
case 'monthly':
|
|
118
|
+
return isTime(time) && timeZone.trim() !== '' && Number(dayOfMonth) >= 1 && Number(dayOfMonth) <= 31;
|
|
119
|
+
case 'custom':
|
|
120
|
+
switch (customKind) {
|
|
121
|
+
case 'at':
|
|
122
|
+
return date !== '' && atTime !== '' && timeZone.trim() !== '';
|
|
123
|
+
case 'after':
|
|
124
|
+
return Number.isInteger(Number(afterSeconds)) && Number(afterSeconds) > 0;
|
|
125
|
+
case 'every':
|
|
126
|
+
return Number.isInteger(Number(everySeconds)) && Number(everySeconds) >= 300;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
})();
|
|
130
|
+
/** Build the wire selector object (not including title/prompt/carry). */
|
|
131
|
+
const buildSelector = () => {
|
|
132
|
+
switch (preset) {
|
|
133
|
+
case '':
|
|
134
|
+
return undefined;
|
|
135
|
+
case 'hourly':
|
|
136
|
+
return { hourly: { minute: Number(minute) } };
|
|
137
|
+
case 'daily':
|
|
138
|
+
return { daily: { time, time_zone: timeZone.trim() } };
|
|
139
|
+
case 'weekly':
|
|
140
|
+
return { weekly: { weekdays, time, time_zone: timeZone.trim() } };
|
|
141
|
+
case 'monthly':
|
|
142
|
+
return { monthly: { dayOfMonth: Number(dayOfMonth), time, time_zone: timeZone.trim() } };
|
|
143
|
+
case 'custom':
|
|
144
|
+
if (customKind === 'at')
|
|
145
|
+
return { at: { date, time: atTime, time_zone: timeZone.trim() } };
|
|
146
|
+
if (customKind === 'after')
|
|
147
|
+
return { after_seconds: Number(afterSeconds) };
|
|
148
|
+
return { every_seconds: Number(everySeconds) };
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
/** Assemble the wire input from the form state. */
|
|
152
|
+
const buildInput = () => {
|
|
153
|
+
if (!selectorValid)
|
|
154
|
+
return undefined;
|
|
155
|
+
const trimmedPrompt = prompt.trim();
|
|
156
|
+
const effectiveTitle = title.trim() !== '' ? title.trim() : trimmedPrompt.slice(0, 40);
|
|
157
|
+
const project = projects.items.find(item => item.workspaceId === projectKey);
|
|
158
|
+
const model = modelKey === '' ? undefined : parseModelKey(modelKey);
|
|
159
|
+
const base = {
|
|
160
|
+
title: effectiveTitle,
|
|
161
|
+
prompt: trimmedPrompt,
|
|
162
|
+
confirmBeforeChange,
|
|
163
|
+
...(project === undefined ? {} : { workspaceId: project.workspaceId, cwd: project.path }),
|
|
164
|
+
};
|
|
165
|
+
if (model !== undefined)
|
|
166
|
+
base.model = model;
|
|
167
|
+
const selector = buildSelector();
|
|
168
|
+
return { ...base, ...(selector === undefined ? {} : selector) };
|
|
169
|
+
};
|
|
170
|
+
const submit = async () => {
|
|
171
|
+
if (prompt.trim() === '') {
|
|
172
|
+
setFormError(t('form.prompt.required'));
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
const input = buildInput();
|
|
176
|
+
if (input === undefined)
|
|
177
|
+
return;
|
|
178
|
+
setFormError(null);
|
|
179
|
+
const result = await onSubmit(input);
|
|
180
|
+
if (!result.ok)
|
|
181
|
+
setFormError(result.message);
|
|
182
|
+
};
|
|
183
|
+
return (_jsxs(_Fragment, { children: [_jsxs("header", { className: css.head, children: [_jsx("h2", { className: css.title, children: task === undefined ? t('create.title') : t('create.editTitle') }), _jsxs("div", { className: css.headActions, children: [_jsx("button", { type: "button", className: css.ghostButton, disabled: busy, onClick: onCancel, children: t('form.cancel') }), _jsx("button", { type: "button", className: css.primaryButton, disabled: busy || prompt.trim() === '' || !selectorValid, onClick: () => { void submit(); }, children: busy ? t('form.submitBusy') : task === undefined ? t('create.submit') : t('create.save') })] })] }), _jsx("p", { className: css.subtitle, children: t('create.subtitle') }), _jsxs("div", { className: `${css.body} ${css.formBody}`, children: [_jsxs("label", { className: css.fieldLine, children: [_jsx("span", { children: t('form.title.label') }), _jsx("input", { className: css.textInput, value: title, onChange: (event) => { setTitle(event.target.value); }, placeholder: t('form.title.placeholder') })] }), _jsxs("div", { className: css.fieldLine, children: [_jsx("span", { children: t('form.schedule.label') }), _jsx(ScheduleEditor, { t: t, preset: preset, minute: minute, time: time, timeZone: timeZone, weekdays: weekdays, dayOfMonth: dayOfMonth, onPresetChange: setPreset, onMinuteChange: (value) => { patchSchedule({ minute: value }); }, onTimeChange: (value) => { patchSchedule({ time: value }); }, onWeekdaysChange: (value) => { patchSchedule({ weekdays: value }); }, onDayOfMonthChange: (value) => { patchSchedule({ dayOfMonth: value }); }, onTimeZoneChange: (value) => { patchSchedule({ timeZone: value }); }, onClear: () => { patchSchedule({ preset: '' }); } }), !selectorValid && (_jsx("span", { className: css.fieldHint, children: preset === '' ? t('form.schedule.required') : t('form.schedule.invalid') }))] }), preset === 'custom' && (_jsxs("div", { className: `${css.formBody} ${css.customGroup}`, children: [_jsxs("label", { className: css.fieldLine, children: [_jsx("span", { children: t('form.schedule.label') }), _jsxs("select", { className: css.textInput, value: customKind, onChange: (event) => { patchSchedule({ customKind: event.target.value }); }, children: [_jsx("option", { value: "at", children: t('form.kind.once') }), _jsx("option", { value: "after", children: t('form.kind.delay') }), customKind === 'every' && _jsx("option", { value: "every", disabled: true, children: t('form.kind.interval') })] })] }), customKind === 'at' && (_jsxs(_Fragment, { children: [_jsxs("label", { className: css.fieldLine, children: [_jsx("span", { children: t('form.date') }), _jsx("input", { className: css.textInput, type: "date", value: date, onChange: (event) => { setDate(event.target.value); } })] }), _jsxs("label", { className: css.fieldLine, children: [_jsx("span", { children: t('form.time') }), _jsx("input", { className: css.textInput, type: "time", value: atTime, onChange: (event) => { setAtTime(event.target.value); } })] }), _jsxs("label", { className: css.fieldLine, children: [_jsx("span", { children: t('form.timeZone') }), _jsx(TimezoneField, { t: t, value: timeZone, onChange: (value) => { patchSchedule({ timeZone: value }); } })] })] })), customKind === 'after' && (_jsxs("label", { className: css.fieldLine, children: [_jsx("span", { children: t('form.afterSeconds') }), _jsx("input", { className: css.textInput, type: "number", min: 1, value: afterSeconds, onChange: (event) => { setAfterSeconds(event.target.value); } })] })), customKind === 'every' && (_jsxs("label", { className: css.fieldLine, children: [_jsx("span", { children: t('form.everySeconds') }), _jsx("input", { className: css.textInput, type: "number", min: 300, value: everySeconds, onChange: (event) => { setEverySeconds(event.target.value); } })] }))] })), _jsxs("label", { className: css.fieldLine, children: [_jsx("span", { children: t('form.prompt.label') }), _jsx("textarea", { className: css.textArea, value: prompt, onChange: (event) => { setPrompt(event.target.value); }, placeholder: t('form.prompt.placeholder') })] }), _jsxs("div", { className: css.bottomBar, children: [_jsxs("label", { className: css.fieldLine, children: [_jsx("span", { children: t('form.project.label') }), _jsxs("select", { className: css.textInput, value: projectKey, disabled: projects.status !== 'ready', onChange: (event) => { setProjectKey(event.target.value); }, children: [_jsx("option", { value: "", children: projects.status === 'loading'
|
|
184
|
+
? t('form.project.loading')
|
|
185
|
+
: projects.status === 'failed'
|
|
186
|
+
? t('form.project.failed', { message: projects.error ?? '' })
|
|
187
|
+
: t('form.project.none') }), projects.items.map(item => (_jsx("option", { value: item.workspaceId, children: item.title }, item.workspaceId)))] })] }), _jsxs("div", { className: css.switchRow, children: [_jsxs("div", { className: css.switchCopy, children: [_jsx("span", { children: t('form.confirm.label') }), _jsx("span", { className: css.hint, children: t('form.confirm.hint') })] }), _jsx("button", { type: "button", className: css.switch, "data-on": confirmBeforeChange || undefined, role: "switch", "aria-checked": confirmBeforeChange, "aria-label": t('form.confirm.label'), onClick: () => { setConfirmBeforeChange(value => !value); } })] }), _jsxs("label", { className: css.fieldLine, children: [_jsx("span", { children: t('form.model.label') }), _jsxs("select", { className: css.textInput, value: modelKey, disabled: models.status !== 'ready', onChange: (event) => { setModelKey(event.target.value); }, children: [_jsx("option", { value: "", children: models.status === 'loading'
|
|
188
|
+
? t('form.model.loading')
|
|
189
|
+
: models.status === 'failed'
|
|
190
|
+
? t('form.model.failed', { message: models.error ?? '' })
|
|
191
|
+
: models.items.length === 0
|
|
192
|
+
? t('form.model.empty')
|
|
193
|
+
: t('form.model.default') }), models.items.map(item => (_jsxs("option", { value: `${item.providerId}/${item.modelId}`, children: [item.providerName, "/", item.modelName] }, `${item.providerId}/${item.modelId}`)))] })] })] }), formError !== null && _jsx("p", { className: css.readError, role: "alert", children: formError })] })] }));
|
|
194
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 已安排的任务 list view (the drawer body): search box, status filter tabs, and
|
|
3
|
+
* the task rows with hover-revealed actions over the Remote.
|
|
4
|
+
*/
|
|
5
|
+
import type { ScheduledTaskSettableStatus, ScheduledTaskView } from '@qihongmu/dsh-plugins-scheduled-task/types';
|
|
6
|
+
import type { ScheduledTaskTranslate } from './format.ts';
|
|
7
|
+
export interface ScheduledTasksListProps {
|
|
8
|
+
readonly t: ScheduledTaskTranslate;
|
|
9
|
+
readonly tasks: readonly ScheduledTaskView[];
|
|
10
|
+
readonly busy: boolean;
|
|
11
|
+
readonly onCreate: () => void;
|
|
12
|
+
readonly onClose: () => void;
|
|
13
|
+
readonly onEdit: (task: ScheduledTaskView) => void;
|
|
14
|
+
readonly onSetStatus: (task: ScheduledTaskView, status: ScheduledTaskSettableStatus) => void;
|
|
15
|
+
readonly onDelete: (task: ScheduledTaskView) => void;
|
|
16
|
+
readonly onMarkRead: (task: ScheduledTaskView) => void;
|
|
17
|
+
}
|
|
18
|
+
/** Render the scheduled-tasks list view. */
|
|
19
|
+
export declare function ScheduledTasksList({ t, tasks, busy, onCreate, onClose, onEdit, onSetStatus, onDelete, onMarkRead, }: ScheduledTasksListProps): import("react").JSX.Element;
|
|
20
|
+
//# sourceMappingURL=ScheduledTasksList.d.ts.map
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* 已安排的任务 list view (the drawer body): search box, status filter tabs, and
|
|
4
|
+
* the task rows with hover-revealed actions over the Remote.
|
|
5
|
+
*/
|
|
6
|
+
import { useState } from 'react';
|
|
7
|
+
import { IconCloseOutline16, IconEditOutline16, IconPauseOutline16, IconPlayOutline16, IconPlusOutline16, IconSearchOutline16, IconTrashOutline16, Tooltip, } from '@deepseek-ai/dsh-client-ui-primitives';
|
|
8
|
+
import { localizedDate, ruleSummary } from "./format.js";
|
|
9
|
+
import css from './ScheduledTasksPanel.module.css';
|
|
10
|
+
const FILTER_LABELS = {
|
|
11
|
+
all: 'filter.all',
|
|
12
|
+
active: 'filter.active',
|
|
13
|
+
paused: 'filter.paused',
|
|
14
|
+
};
|
|
15
|
+
const STATUS_LABELS = {
|
|
16
|
+
active: 'row.status.active',
|
|
17
|
+
paused: 'row.status.paused',
|
|
18
|
+
completed: 'row.status.completed',
|
|
19
|
+
};
|
|
20
|
+
/** Row action wrapper: a tooltip over a compact icon button. */
|
|
21
|
+
function RowAction({ label, children, ...props }) {
|
|
22
|
+
return (_jsx(Tooltip, { label: label, side: "bottom", delayMs: 500, children: _jsx("button", { type: "button", className: css.actionButton, "aria-label": label, ...props, children: children }) }));
|
|
23
|
+
}
|
|
24
|
+
/** Render the scheduled-tasks list view. */
|
|
25
|
+
export function ScheduledTasksList({ t, tasks, busy, onCreate, onClose, onEdit, onSetStatus, onDelete, onMarkRead, }) {
|
|
26
|
+
const [search, setSearch] = useState('');
|
|
27
|
+
const [filter, setFilter] = useState('all');
|
|
28
|
+
const query = search.trim().toLowerCase();
|
|
29
|
+
const filtered = tasks.filter(task => {
|
|
30
|
+
if (filter === 'active' && task.status !== 'active')
|
|
31
|
+
return false;
|
|
32
|
+
if (filter === 'paused' && task.status !== 'paused')
|
|
33
|
+
return false;
|
|
34
|
+
if (query !== '' && !task.title.toLowerCase().includes(query))
|
|
35
|
+
return false;
|
|
36
|
+
return true;
|
|
37
|
+
});
|
|
38
|
+
return (_jsxs(_Fragment, { children: [_jsxs("header", { className: css.head, children: [_jsx("h2", { className: css.title, children: t('panel.title') }), _jsxs("div", { className: css.headActions, children: [_jsxs("button", { type: "button", className: css.primaryButton, onClick: onCreate, children: [_jsx(IconPlusOutline16, { size: 14 }), _jsx("span", { children: t('action.create') })] }), _jsx(Tooltip, { label: t('drawer.close'), side: "bottom", delayMs: 500, children: _jsx("button", { type: "button", className: css.closeButton, "aria-label": t('drawer.close'), onClick: onClose, children: _jsx(IconCloseOutline16, { size: 16 }) }) })] })] }), _jsx("p", { className: css.subtitle, children: t('panel.subtitle') }), _jsx("div", { className: css.toolbar, children: _jsxs("label", { className: css.searchBox, children: [_jsx(IconSearchOutline16, { size: 14 }), _jsx("input", { className: css.searchInput, value: search, onChange: (event) => { setSearch(event.target.value); }, placeholder: t('search.placeholder') })] }) }), _jsx("nav", { className: css.tabs, "aria-label": t('panel.title'), children: Object.keys(FILTER_LABELS).map(item => (_jsx("button", { type: "button", className: css.tab, "data-active": filter === item || undefined, onClick: () => { setFilter(item); }, children: t(FILTER_LABELS[item]) }, item))) }), _jsxs("div", { className: css.body, children: [filtered.length === 0 && (_jsx("p", { className: css.note, children: t('panel.empty') })), filtered.length > 0 && (_jsx("ul", { className: css.rows, children: filtered.map(task => (_jsxs("li", { className: css.row, children: [_jsx("span", { className: css.statusDot, "data-status": task.status, role: "img", "aria-label": t(STATUS_LABELS[task.status]) }), _jsxs("div", { className: css.rowMain, "data-unread": task.unread || undefined, role: task.unread ? 'button' : undefined, tabIndex: task.unread ? 0 : undefined, "aria-label": task.unread ? t('action.markRead') : undefined, onClick: () => { if (task.unread && !busy)
|
|
39
|
+
onMarkRead(task); }, onKeyDown: (event) => {
|
|
40
|
+
if (task.unread && !busy && (event.key === 'Enter' || event.key === ' ')) {
|
|
41
|
+
event.preventDefault();
|
|
42
|
+
onMarkRead(task);
|
|
43
|
+
}
|
|
44
|
+
}, children: [_jsxs("div", { className: css.rowHead, children: [task.unread && _jsx("span", { className: css.unreadDot }), _jsx("span", { className: css.rowTitle, children: task.title })] }), _jsxs("div", { className: css.rowMeta, children: [_jsx("span", { className: css.rowSchedule, children: ruleSummary(task.rule, t) }), task.nextRunAt !== undefined && task.status !== 'completed' && (_jsx("span", { className: css.rowNext, children: t('row.next', { time: localizedDate(task.nextRunAt) }) })), task.state === 'overdue' && _jsx("span", { className: css.rowOverdue, children: t('row.state.overdue') }), task.lastError !== undefined && (_jsx("span", { className: css.rowLastError, title: `${task.lastError.at} — ${task.lastError.message}`, children: t('row.lastError') })), _jsx("span", { className: css.rowStatus, "data-state": task.state, children: t(STATUS_LABELS[task.status]) })] })] }), _jsxs("div", { className: css.rowActions, children: [task.status === 'active' && (_jsx(RowAction, { label: t('action.pause'), disabled: busy, onClick: () => { onSetStatus(task, 'paused'); }, children: _jsx(IconPauseOutline16, { size: 14 }) })), task.status === 'paused' && (_jsx(RowAction, { label: t('action.resume'), disabled: busy, onClick: () => { onSetStatus(task, 'active'); }, children: _jsx(IconPlayOutline16, { size: 14 }) })), _jsx(RowAction, { label: t('action.edit'), disabled: busy, onClick: () => { onEdit(task); }, children: _jsx(IconEditOutline16, { size: 14 }) }), _jsx(RowAction, { label: t('action.delete'), disabled: busy, onClick: () => { onDelete(task); }, children: _jsx(IconTrashOutline16, { size: 14 }) })] })] }, task.id))) }))] })] }));
|
|
45
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Global scheduled-task panel shell (external plugin): the sidebar footer
|
|
3
|
+
* trigger opens a right-docked drawer hosting two views — the 已安排的任务
|
|
4
|
+
* history list (search/filter/create) and the 新建定时任务 form — over the
|
|
5
|
+
* Remote's list/create/update/setStatus/delete/markRead surface.
|
|
6
|
+
*/
|
|
7
|
+
import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots';
|
|
8
|
+
import type { ScheduledTasksPanelFace } from './slots.ts';
|
|
9
|
+
/** Full panel props composed by the sidebar footer-action slot. */
|
|
10
|
+
export type ScheduledTasksPanelProps = PropsRuntime<'sidebar.footer.action'> & InjectFace<ScheduledTasksPanelFace> & PropsLocale<'scheduled-tasks'>;
|
|
11
|
+
/** Render the scheduled-tasks trigger and its right-side drawer. */
|
|
12
|
+
export declare function ScheduledTasksPanel({ wide, remote, listProjects, listModels, t }: ScheduledTasksPanelProps): import("react").JSX.Element;
|
|
13
|
+
//# sourceMappingURL=ScheduledTasksPanel.d.ts.map
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Global scheduled-task panel shell (external plugin): the sidebar footer
|
|
4
|
+
* trigger opens a right-docked drawer hosting two views — the 已安排的任务
|
|
5
|
+
* history list (search/filter/create) and the 新建定时任务 form — over the
|
|
6
|
+
* Remote's list/create/update/setStatus/delete/markRead surface.
|
|
7
|
+
*/
|
|
8
|
+
import { useEffect, useState } from 'react';
|
|
9
|
+
import { IconRefreshOutline16 } from '@deepseek-ai/dsh-client-ui-primitives';
|
|
10
|
+
import { ScheduledTaskCreate } from "./ScheduledTaskCreate.js";
|
|
11
|
+
import { ScheduledTasksList } from "./ScheduledTasksList.js";
|
|
12
|
+
import css from './ScheduledTasksPanel.module.css';
|
|
13
|
+
/** Unwrap one Typert envelope; a falsey `ok` keeps the inner error message. */
|
|
14
|
+
function unwrap(answered) {
|
|
15
|
+
if (!answered.ok)
|
|
16
|
+
return { ok: false, message: `${answered.error.code}: ${answered.error.message}` };
|
|
17
|
+
return { ok: true, value: answered.value };
|
|
18
|
+
}
|
|
19
|
+
/** Render the scheduled-tasks trigger and its right-side drawer. */
|
|
20
|
+
export function ScheduledTasksPanel({ wide, remote, listProjects, listModels, t }) {
|
|
21
|
+
const [open, setOpen] = useState(false);
|
|
22
|
+
const [view, setView] = useState('list');
|
|
23
|
+
const [tasks, setTasks] = useState([]);
|
|
24
|
+
const [read, setRead] = useState(false);
|
|
25
|
+
const [error, setError] = useState(null);
|
|
26
|
+
const [busy, setBusy] = useState(false);
|
|
27
|
+
const [editingId, setEditingId] = useState(null);
|
|
28
|
+
const unread = tasks.filter(task => task.unread).length;
|
|
29
|
+
const refresh = async () => {
|
|
30
|
+
// Transport-level failures REJECT (envelope failures resolve); catch both
|
|
31
|
+
// so the drawer never sticks on the loading state with an unhandled rejection.
|
|
32
|
+
try {
|
|
33
|
+
const answered = await remote.list();
|
|
34
|
+
const envelope = unwrap(answered);
|
|
35
|
+
if (!envelope.ok) {
|
|
36
|
+
setRead(true);
|
|
37
|
+
setError(envelope.message);
|
|
38
|
+
setTasks([]);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
setRead(true);
|
|
42
|
+
setError(null);
|
|
43
|
+
setTasks(envelope.value);
|
|
44
|
+
}
|
|
45
|
+
catch (caught) {
|
|
46
|
+
setRead(true);
|
|
47
|
+
setError(t('panel.readFailed', { message: caught instanceof Error ? caught.message : String(caught) }));
|
|
48
|
+
setTasks([]);
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
if (open)
|
|
53
|
+
void refresh();
|
|
54
|
+
}, [open]);
|
|
55
|
+
// Esc closes the drawer from either view.
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
if (!open)
|
|
58
|
+
return;
|
|
59
|
+
const onKey = (event) => {
|
|
60
|
+
if (event.key === 'Escape')
|
|
61
|
+
setOpen(false);
|
|
62
|
+
};
|
|
63
|
+
window.addEventListener('keydown', onKey);
|
|
64
|
+
return () => { window.removeEventListener('keydown', onKey); };
|
|
65
|
+
}, [open]);
|
|
66
|
+
const openList = () => {
|
|
67
|
+
setEditingId(null);
|
|
68
|
+
setView('list');
|
|
69
|
+
};
|
|
70
|
+
const runMutation = async (action) => {
|
|
71
|
+
if (busy)
|
|
72
|
+
return;
|
|
73
|
+
setBusy(true);
|
|
74
|
+
try {
|
|
75
|
+
await action();
|
|
76
|
+
setError(null);
|
|
77
|
+
}
|
|
78
|
+
catch (caught) {
|
|
79
|
+
setError(caught instanceof Error ? caught.message : String(caught));
|
|
80
|
+
}
|
|
81
|
+
finally {
|
|
82
|
+
setBusy(false);
|
|
83
|
+
try {
|
|
84
|
+
await refresh();
|
|
85
|
+
}
|
|
86
|
+
catch { /* refresh reports its own errors */ }
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
const setStatus = (task, status) => {
|
|
90
|
+
void runMutation(async () => {
|
|
91
|
+
const next = unwrap(await remote.setStatus(task.id, status));
|
|
92
|
+
if (!next.ok)
|
|
93
|
+
throw new Error(next.message);
|
|
94
|
+
if (!next.value.ok)
|
|
95
|
+
throw new Error(`${next.value.code}: ${next.value.message}`);
|
|
96
|
+
});
|
|
97
|
+
};
|
|
98
|
+
const removeTask = (task) => {
|
|
99
|
+
void runMutation(async () => {
|
|
100
|
+
const next = unwrap(await remote.delete(task.id));
|
|
101
|
+
if (!next.ok)
|
|
102
|
+
throw new Error(next.message);
|
|
103
|
+
if (!next.value.ok)
|
|
104
|
+
throw new Error(`${next.value.code}: ${next.value.message}`);
|
|
105
|
+
});
|
|
106
|
+
};
|
|
107
|
+
const markRead = (task) => {
|
|
108
|
+
void runMutation(async () => {
|
|
109
|
+
const next = unwrap(await remote.markRead(task.id));
|
|
110
|
+
if (!next.ok)
|
|
111
|
+
throw new Error(next.message);
|
|
112
|
+
});
|
|
113
|
+
};
|
|
114
|
+
/** Create or update from the create view; errors surface inside the form. */
|
|
115
|
+
const submit = async (input) => {
|
|
116
|
+
let answered;
|
|
117
|
+
try {
|
|
118
|
+
answered = editingId === null ? await remote.create(input) : await remote.update(editingId, input);
|
|
119
|
+
}
|
|
120
|
+
catch (caught) {
|
|
121
|
+
return { ok: false, message: caught instanceof Error ? caught.message : String(caught) };
|
|
122
|
+
}
|
|
123
|
+
const envelope = unwrap(answered);
|
|
124
|
+
if (!envelope.ok)
|
|
125
|
+
return { ok: false, message: envelope.message };
|
|
126
|
+
if (!envelope.value.ok)
|
|
127
|
+
return { ok: false, message: `${envelope.value.code}: ${envelope.value.message}` };
|
|
128
|
+
openList();
|
|
129
|
+
void refresh();
|
|
130
|
+
return { ok: true };
|
|
131
|
+
};
|
|
132
|
+
const editing = editingId === null ? undefined : tasks.find(task => task.id === editingId);
|
|
133
|
+
return (_jsxs("div", { className: wide ? css.layer : `${css.layer} ${css.rail}`, children: [open && (_jsxs(_Fragment, { children: [_jsx("div", { className: css.backdrop, onClick: () => { setOpen(false); } }), _jsxs("section", { className: css.drawer, "aria-label": t('panel.title'), children: [view === 'list' ? (_jsx(ScheduledTasksList, { t: t, tasks: tasks, busy: busy, onCreate: () => { setEditingId(null); setView('create'); }, onClose: () => { setOpen(false); }, onEdit: (task) => { setEditingId(task.id); setView('create'); }, onSetStatus: setStatus, onDelete: removeTask, onMarkRead: markRead })) : (_jsx(ScheduledTaskCreate, { t: t, busy: busy, task: editing, listProjects: listProjects, listModels: listModels, onCancel: openList, onSubmit: submit }, editingId ?? 'new')), error !== null && (_jsx("p", { className: `${css.readError} ${css.drawerError}`, role: "alert", children: t('panel.readFailed', { message: error }) })), !read && _jsx("p", { className: `${css.note} ${css.drawerError}`, children: t('panel.loading') })] })] })), _jsx("div", { className: css.footerButtons, children: _jsxs("button", { type: "button", className: css.badge, "data-active": open || undefined, "aria-label": t('trigger.aria'), "aria-expanded": open, onClick: () => { setOpen(current => !current); }, children: [_jsx(IconRefreshOutline16, {}), wide && _jsx("span", { className: css.badgeLabel, children: t('trigger.label') }), unread > 0 && _jsx("span", { className: css.badgeCount, children: unread })] }) })] }));
|
|
134
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** Shared formatting helpers for the scheduled-tasks UI. */
|
|
2
|
+
import type { ScheduledTaskRule } from '@qihongmu/dsh-plugins-scheduled-task/types';
|
|
3
|
+
import type { TranslateNS } from '@deepseek-ai/dsh-client-ui-slots';
|
|
4
|
+
/** The namespace-bound translate seat shared by the panel's views. */
|
|
5
|
+
export type ScheduledTaskTranslate = TranslateNS<'scheduled-tasks'>;
|
|
6
|
+
/** Format a wire ISO timestamp in the browser's default zone. */
|
|
7
|
+
export declare function localizedDate(value: string | undefined): string;
|
|
8
|
+
/** Humanize a whole-second duration through the duration dictionary keys. */
|
|
9
|
+
export declare function humanizeDuration(t: ScheduledTaskTranslate, seconds: number): string;
|
|
10
|
+
/** Join localized weekday names with the locale's list separator. */
|
|
11
|
+
export declare function weekdaySummary(t: ScheduledTaskTranslate, days: readonly number[]): string;
|
|
12
|
+
/** Localize one schedule rule into a one-line summary (list rows, previews). */
|
|
13
|
+
export declare function ruleSummary(rule: ScheduledTaskRule, t: ScheduledTaskTranslate): string;
|
|
14
|
+
//# sourceMappingURL=format.d.ts.map
|