feat(scheduled-tasks): add cron syntax support to task editor dialog (#1593)
Adds a 'Cron' schedule type option to the scheduled task editor, allowing users to create and edit cron-based schedules through the UI. - Add cron expression input with inline validation (cron-parser) - Show next 4 upcoming run times as a preview - Provide clickable example chips (every 5min, hourly, Monday 9am, etc.) - Preserve cron expressions when editing existing cron tasks - Add cron.ts utility module for validation and next-run computation - Add i18n keys across all 8 locale files Closes #1586 Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
This commit is contained in:
committed by
GitHub
co-authored by
Bohdan Triapitsyn
parent
e904abda04
commit
e402cd75f5
@@ -156,6 +156,7 @@
|
|||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "^1.1.1",
|
"cmdk": "^1.1.1",
|
||||||
"codemirror-lang-elixir": "^4.0.0",
|
"codemirror-lang-elixir": "^4.0.0",
|
||||||
|
"cron-parser": "^5.5.0",
|
||||||
"dompurify": "^3.2.7",
|
"dompurify": "^3.2.7",
|
||||||
"express": "^5.1.0",
|
"express": "^5.1.0",
|
||||||
"fuse.js": "^7.1.0",
|
"fuse.js": "^7.1.0",
|
||||||
@@ -1676,7 +1677,7 @@
|
|||||||
|
|
||||||
"crelt": ["crelt@1.0.6", "", {}, "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g=="],
|
"crelt": ["crelt@1.0.6", "", {}, "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g=="],
|
||||||
|
|
||||||
"cron-parser": ["cron-parser@4.9.0", "", { "dependencies": { "luxon": "^3.2.1" } }, "sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q=="],
|
"cron-parser": ["cron-parser@5.5.0", "", { "dependencies": { "luxon": "^3.7.1" } }, "sha512-oML4lKUXxizYswqmxuOCpgFS8BNUJpIu6k/2HVHyaL8Ynnf3wdf9tkns0yRdJLSIjkJ+b0DXHMZEHGpMwjnPww=="],
|
||||||
|
|
||||||
"cross-dirname": ["cross-dirname@0.1.0", "", {}, "sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q=="],
|
"cross-dirname": ["cross-dirname@0.1.0", "", {}, "sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q=="],
|
||||||
|
|
||||||
@@ -3378,6 +3379,10 @@
|
|||||||
|
|
||||||
"@npmcli/agent/socks-proxy-agent": ["socks-proxy-agent@8.0.5", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "^4.3.4", "socks": "^2.8.3" } }, "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw=="],
|
"@npmcli/agent/socks-proxy-agent": ["socks-proxy-agent@8.0.5", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "^4.3.4", "socks": "^2.8.3" } }, "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw=="],
|
||||||
|
|
||||||
|
"@openchamber/ui/ghostty-web": ["ghostty-web@0.4.0", "", {}, "sha512-0puDBik2qapbD/QQBW9o5ZHfXnZBqZWx/ctBiVtKZ6ZLds4NYb+wZuw1cRLXZk9zYovIQ908z3rvFhexAvc5Hg=="],
|
||||||
|
|
||||||
|
"@openchamber/web/cron-parser": ["cron-parser@4.9.0", "", { "dependencies": { "luxon": "^3.2.1" } }, "sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q=="],
|
||||||
|
|
||||||
"@radix-ui/react-collection/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
|
"@radix-ui/react-collection/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
|
||||||
|
|
||||||
"@radix-ui/react-dialog/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
|
"@radix-ui/react-dialog/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
|
||||||
|
|||||||
@@ -51,6 +51,7 @@
|
|||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "^1.1.1",
|
"cmdk": "^1.1.1",
|
||||||
"codemirror-lang-elixir": "^4.0.0",
|
"codemirror-lang-elixir": "^4.0.0",
|
||||||
|
"cron-parser": "^5.5.0",
|
||||||
"dompurify": "^3.2.7",
|
"dompurify": "^3.2.7",
|
||||||
"express": "^5.1.0",
|
"express": "^5.1.0",
|
||||||
"fuse.js": "^7.1.0",
|
"fuse.js": "^7.1.0",
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import { useConfigStore } from '@/stores/useConfigStore';
|
|||||||
import { useUIStore } from '@/stores/useUIStore';
|
import { useUIStore } from '@/stores/useUIStore';
|
||||||
import type { ScheduledTask } from '@/lib/scheduledTasksApi';
|
import type { ScheduledTask } from '@/lib/scheduledTasksApi';
|
||||||
import { useI18n } from '@/lib/i18n';
|
import { useI18n } from '@/lib/i18n';
|
||||||
|
import { isValidCronExpression, getNextRuns, CRON_EXAMPLES } from '@/lib/cron';
|
||||||
|
|
||||||
const WEEKDAY_INDEXES = [0, 1, 2, 3, 4, 5, 6] as const;
|
const WEEKDAY_INDEXES = [0, 1, 2, 3, 4, 5, 6] as const;
|
||||||
|
|
||||||
@@ -451,12 +452,13 @@ type ScheduledTaskDraft = {
|
|||||||
name: string;
|
name: string;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
schedule: {
|
schedule: {
|
||||||
kind: 'daily' | 'weekly' | 'once';
|
kind: 'daily' | 'weekly' | 'once' | 'cron';
|
||||||
times: string[];
|
times: string[];
|
||||||
onceDate: string;
|
onceDate: string;
|
||||||
onceTime: string;
|
onceTime: string;
|
||||||
weekdays: number[];
|
weekdays: number[];
|
||||||
timezone: string;
|
timezone: string;
|
||||||
|
cronExpression: string;
|
||||||
};
|
};
|
||||||
execution: {
|
execution: {
|
||||||
prompt: string;
|
prompt: string;
|
||||||
@@ -505,6 +507,7 @@ const toDraft = (
|
|||||||
onceTime: '09:00',
|
onceTime: '09:00',
|
||||||
weekdays: [1],
|
weekdays: [1],
|
||||||
timezone: timezoneFallback,
|
timezone: timezoneFallback,
|
||||||
|
cronExpression: '',
|
||||||
},
|
},
|
||||||
execution: {
|
execution: {
|
||||||
prompt: '',
|
prompt: '',
|
||||||
@@ -521,9 +524,11 @@ const toDraft = (
|
|||||||
name: task.name,
|
name: task.name,
|
||||||
enabled: task.enabled,
|
enabled: task.enabled,
|
||||||
schedule: {
|
schedule: {
|
||||||
kind: task.schedule.kind === 'once'
|
kind: task.schedule.kind === 'cron'
|
||||||
? 'once'
|
? 'cron'
|
||||||
: (task.schedule.kind === 'weekly' ? 'weekly' : 'daily'),
|
: (task.schedule.kind === 'once'
|
||||||
|
? 'once'
|
||||||
|
: (task.schedule.kind === 'weekly' ? 'weekly' : 'daily')),
|
||||||
times: normalizeDraftTimes(task),
|
times: normalizeDraftTimes(task),
|
||||||
onceDate: typeof task.schedule.date === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(task.schedule.date)
|
onceDate: typeof task.schedule.date === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(task.schedule.date)
|
||||||
? task.schedule.date
|
? task.schedule.date
|
||||||
@@ -533,6 +538,9 @@ const toDraft = (
|
|||||||
: '09:00',
|
: '09:00',
|
||||||
weekdays: Array.isArray(task.schedule.weekdays) ? task.schedule.weekdays : [1],
|
weekdays: Array.isArray(task.schedule.weekdays) ? task.schedule.weekdays : [1],
|
||||||
timezone: task.schedule.timezone || timezoneFallback,
|
timezone: task.schedule.timezone || timezoneFallback,
|
||||||
|
cronExpression: task.schedule.kind === 'cron' && typeof task.schedule.cron === 'string'
|
||||||
|
? task.schedule.cron
|
||||||
|
: '',
|
||||||
},
|
},
|
||||||
execution: {
|
execution: {
|
||||||
prompt: task.execution.prompt,
|
prompt: task.execution.prompt,
|
||||||
@@ -563,6 +571,14 @@ const validateDraft = (draft: ScheduledTaskDraft, t: ReturnType<typeof useI18n>[
|
|||||||
if (!/^([01]\d|2[0-3]):([0-5]\d)$/.test(draft.schedule.onceTime)) {
|
if (!/^([01]\d|2[0-3]):([0-5]\d)$/.test(draft.schedule.onceTime)) {
|
||||||
return t('sessions.scheduledTasks.editor.validation.timeFormat');
|
return t('sessions.scheduledTasks.editor.validation.timeFormat');
|
||||||
}
|
}
|
||||||
|
} else if (draft.schedule.kind === 'cron') {
|
||||||
|
if (!draft.schedule.cronExpression.trim()) {
|
||||||
|
return t('sessions.scheduledTasks.editor.validation.cronRequired');
|
||||||
|
}
|
||||||
|
const cronResult = isValidCronExpression(draft.schedule.cronExpression);
|
||||||
|
if (!cronResult.valid) {
|
||||||
|
return t('sessions.scheduledTasks.editor.validation.cronInvalid');
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
const validTimes = draft.schedule.times.filter((value) => /^([01]\d|2[0-3]):([0-5]\d)$/.test(value));
|
const validTimes = draft.schedule.times.filter((value) => /^([01]\d|2[0-3]):([0-5]\d)$/.test(value));
|
||||||
if (validTimes.length === 0) {
|
if (validTimes.length === 0) {
|
||||||
@@ -586,6 +602,108 @@ const dedupeSortTimes = (times: string[]) => {
|
|||||||
return Array.from(new Set(filtered)).sort((a, b) => a.localeCompare(b));
|
return Array.from(new Set(filtered)).sort((a, b) => a.localeCompare(b));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const CronScheduleSection: React.FC<{
|
||||||
|
draft: ScheduledTaskDraft;
|
||||||
|
setDraft: React.Dispatch<React.SetStateAction<ScheduledTaskDraft>>;
|
||||||
|
locale: string;
|
||||||
|
t: ReturnType<typeof useI18n>['t'];
|
||||||
|
}> = ({ draft, setDraft, locale, t }) => {
|
||||||
|
const cronExpression = draft.schedule.cronExpression;
|
||||||
|
const cronValidation = React.useMemo(
|
||||||
|
() => (cronExpression.trim() ? isValidCronExpression(cronExpression) : null),
|
||||||
|
[cronExpression],
|
||||||
|
);
|
||||||
|
const nextRuns = React.useMemo(() => {
|
||||||
|
if (!cronValidation?.valid || !cronExpression.trim()) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return getNextRuns(cronExpression, draft.schedule.timezone);
|
||||||
|
}, [cronExpression, cronValidation, draft.schedule.timezone]);
|
||||||
|
|
||||||
|
const formatNextRun = React.useCallback(
|
||||||
|
(date: Date) => new Intl.DateTimeFormat(locale, {
|
||||||
|
timeZone: draft.schedule.timezone,
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric',
|
||||||
|
hour: 'numeric',
|
||||||
|
minute: '2-digit',
|
||||||
|
}).format(date),
|
||||||
|
[locale, draft.schedule.timezone],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<FieldLabel htmlFor="sched-cron" required>{t('sessions.scheduledTasks.editor.cronExpression.label')}</FieldLabel>
|
||||||
|
<Input
|
||||||
|
id="sched-cron"
|
||||||
|
value={cronExpression}
|
||||||
|
onChange={(event) => setDraft((prev) => ({
|
||||||
|
...prev,
|
||||||
|
schedule: { ...prev.schedule, cronExpression: event.target.value },
|
||||||
|
}))}
|
||||||
|
placeholder={t('sessions.scheduledTasks.editor.cronExpression.placeholder')}
|
||||||
|
className="w-full max-w-xs font-mono"
|
||||||
|
/>
|
||||||
|
{cronValidation && !cronValidation.valid && cronExpression.trim() ? (
|
||||||
|
<span className="typography-micro text-destructive">
|
||||||
|
{t('sessions.scheduledTasks.editor.validation.cronInvalid')}
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{nextRuns.length > 0 ? (
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<span className="typography-meta text-muted-foreground">{t('sessions.scheduledTasks.editor.cronExpression.nextRuns')}</span>
|
||||||
|
<span className="typography-micro text-foreground">
|
||||||
|
{nextRuns.map(formatNextRun).join(', ')}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<span className="typography-meta text-muted-foreground">{t('sessions.scheduledTasks.editor.cronExpression.examples')}</span>
|
||||||
|
<div className="flex flex-wrap gap-1.5">
|
||||||
|
{CRON_EXAMPLES.map((example) => (
|
||||||
|
<button
|
||||||
|
key={example.expression}
|
||||||
|
type="button"
|
||||||
|
className="inline-flex items-center gap-1.5 rounded-md border border-border px-2 py-1 typography-micro text-foreground hover:bg-interactive-hover"
|
||||||
|
onClick={() => setDraft((prev) => ({
|
||||||
|
...prev,
|
||||||
|
schedule: { ...prev.schedule, cronExpression: example.expression },
|
||||||
|
}))}
|
||||||
|
>
|
||||||
|
<span className="font-mono">{example.expression}</span>
|
||||||
|
<span className="text-muted-foreground">{t(example.labelKey as Parameters<typeof t>[0])}</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<FieldLabel>{t('sessions.scheduledTasks.editor.timezone.label')}</FieldLabel>
|
||||||
|
<Select
|
||||||
|
value={draft.schedule.timezone}
|
||||||
|
onValueChange={(timezone) => {
|
||||||
|
setDraft((prev) => ({
|
||||||
|
...prev,
|
||||||
|
schedule: { ...prev.schedule, timezone },
|
||||||
|
}));
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-fit max-w-full"><SelectValue /></SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{TIMEZONE_OPTIONS.map((timezone) => (
|
||||||
|
<SelectItem key={timezone} value={timezone}>{timezone}</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export function ScheduledTaskEditorDialog(props: {
|
export function ScheduledTaskEditorDialog(props: {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
task: ScheduledTask | null;
|
task: ScheduledTask | null;
|
||||||
@@ -1010,7 +1128,6 @@ export function ScheduledTaskEditorDialog(props: {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const normalizedTimes = dedupeSortTimes(draft.schedule.times);
|
|
||||||
const payload: Partial<ScheduledTask> = {
|
const payload: Partial<ScheduledTask> = {
|
||||||
...(draft.id ? { id: draft.id } : {}),
|
...(draft.id ? { id: draft.id } : {}),
|
||||||
name: draft.name.trim(),
|
name: draft.name.trim(),
|
||||||
@@ -1018,15 +1135,17 @@ export function ScheduledTaskEditorDialog(props: {
|
|||||||
schedule: {
|
schedule: {
|
||||||
kind: draft.schedule.kind,
|
kind: draft.schedule.kind,
|
||||||
timezone: draft.schedule.timezone.trim(),
|
timezone: draft.schedule.timezone.trim(),
|
||||||
...(draft.schedule.kind === 'once'
|
...(draft.schedule.kind === 'cron'
|
||||||
? {
|
? { cron: draft.schedule.cronExpression.trim() }
|
||||||
date: draft.schedule.onceDate,
|
: draft.schedule.kind === 'once'
|
||||||
time: draft.schedule.onceTime,
|
? {
|
||||||
|
date: draft.schedule.onceDate,
|
||||||
|
time: draft.schedule.onceTime,
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
times: normalizedTimes,
|
times: dedupeSortTimes(draft.schedule.times),
|
||||||
...(draft.schedule.kind === 'weekly' ? { weekdays: draft.schedule.weekdays } : {}),
|
...(draft.schedule.kind === 'weekly' ? { weekdays: draft.schedule.weekdays } : {}),
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
execution: {
|
execution: {
|
||||||
prompt: draft.execution.prompt,
|
prompt: draft.execution.prompt,
|
||||||
@@ -1081,10 +1200,16 @@ export function ScheduledTaskEditorDialog(props: {
|
|||||||
<FieldLabel>{t('sessions.scheduledTasks.editor.scheduleType.label')}</FieldLabel>
|
<FieldLabel>{t('sessions.scheduledTasks.editor.scheduleType.label')}</FieldLabel>
|
||||||
<Select
|
<Select
|
||||||
value={draft.schedule.kind}
|
value={draft.schedule.kind}
|
||||||
onValueChange={(value: 'daily' | 'weekly' | 'once') => {
|
onValueChange={(value: 'daily' | 'weekly' | 'once' | 'cron') => {
|
||||||
setDraft((prev) => ({
|
setDraft((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
schedule: { ...prev.schedule, kind: value },
|
schedule: {
|
||||||
|
...prev.schedule,
|
||||||
|
kind: value,
|
||||||
|
...(value === 'cron' && !prev.schedule.cronExpression
|
||||||
|
? { cronExpression: '0 * * * *' }
|
||||||
|
: {}),
|
||||||
|
},
|
||||||
}));
|
}));
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -1094,20 +1219,25 @@ export function ScheduledTaskEditorDialog(props: {
|
|||||||
? t('sessions.scheduledTasks.editor.scheduleType.daily')
|
? t('sessions.scheduledTasks.editor.scheduleType.daily')
|
||||||
: value === 'weekly'
|
: value === 'weekly'
|
||||||
? t('sessions.scheduledTasks.editor.scheduleType.weekly')
|
? t('sessions.scheduledTasks.editor.scheduleType.weekly')
|
||||||
: t('sessions.scheduledTasks.editor.scheduleType.once')}
|
: value === 'cron'
|
||||||
|
? t('sessions.scheduledTasks.editor.scheduleType.cron')
|
||||||
|
: t('sessions.scheduledTasks.editor.scheduleType.once')}
|
||||||
</SelectValue>
|
</SelectValue>
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="daily">{t('sessions.scheduledTasks.editor.scheduleType.daily')}</SelectItem>
|
<SelectItem value="daily">{t('sessions.scheduledTasks.editor.scheduleType.daily')}</SelectItem>
|
||||||
<SelectItem value="weekly">{t('sessions.scheduledTasks.editor.scheduleType.weekly')}</SelectItem>
|
<SelectItem value="weekly">{t('sessions.scheduledTasks.editor.scheduleType.weekly')}</SelectItem>
|
||||||
<SelectItem value="once">{t('sessions.scheduledTasks.editor.scheduleType.once')}</SelectItem>
|
<SelectItem value="once">{t('sessions.scheduledTasks.editor.scheduleType.once')}</SelectItem>
|
||||||
|
<SelectItem value="cron">{t('sessions.scheduledTasks.editor.scheduleType.cron')}</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{draft.schedule.kind === 'once' ? (
|
{draft.schedule.kind === 'cron' ? (
|
||||||
|
<CronScheduleSection draft={draft} setDraft={setDraft} locale={locale} t={t} />
|
||||||
|
) : draft.schedule.kind === 'once' ? (
|
||||||
<div className="grid grid-cols-1 gap-x-4 gap-y-3 sm:grid-cols-2">
|
<div className="grid grid-cols-1 gap-x-4 gap-y-3 sm:grid-cols-2">
|
||||||
<div className="flex flex-col gap-1" ref={datePickerRef}>
|
<div className="flex flex-col gap-1" ref={datePickerRef}>
|
||||||
<FieldLabel>{t('sessions.scheduledTasks.editor.date.label')}</FieldLabel>
|
<FieldLabel>{t('sessions.scheduledTasks.editor.date.label')}</FieldLabel>
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import { describe, expect, test } from 'bun:test';
|
||||||
|
|
||||||
|
import { getNextRuns, isValidCronExpression } from './cron';
|
||||||
|
|
||||||
|
describe('cron helpers', () => {
|
||||||
|
test('accepts valid cron expressions', () => {
|
||||||
|
expect(isValidCronExpression('*/5 * * * *').valid).toBe(true);
|
||||||
|
expect(isValidCronExpression('0 9 * * 1').valid).toBe(true);
|
||||||
|
expect(isValidCronExpression('0 0 9 * * 1').valid).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('rejects empty and invalid cron expressions', () => {
|
||||||
|
expect(isValidCronExpression('').valid).toBe(false);
|
||||||
|
expect(isValidCronExpression(' ').valid).toBe(false);
|
||||||
|
expect(isValidCronExpression('abc').valid).toBe(false);
|
||||||
|
expect(isValidCronExpression('61 * * * *').valid).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns the requested number of next runs', () => {
|
||||||
|
const runs = getNextRuns('*/5 * * * *', 'UTC', 3);
|
||||||
|
|
||||||
|
expect(runs).toHaveLength(3);
|
||||||
|
expect(runs.every((run) => run instanceof Date && Number.isFinite(run.getTime()))).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns an empty list for invalid expressions', () => {
|
||||||
|
expect(getNextRuns('not cron', 'UTC')).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import { CronExpressionParser } from 'cron-parser';
|
||||||
|
|
||||||
|
export type CronValidationResult = {
|
||||||
|
valid: boolean;
|
||||||
|
error?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function isValidCronExpression(expression: string): CronValidationResult {
|
||||||
|
if (!expression || !expression.trim()) {
|
||||||
|
return { valid: false, error: 'required' };
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
CronExpressionParser.parse(expression);
|
||||||
|
return { valid: true };
|
||||||
|
} catch (err) {
|
||||||
|
const message = err instanceof Error ? err.message : 'Invalid cron expression';
|
||||||
|
return { valid: false, error: message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getNextRuns(
|
||||||
|
expression: string,
|
||||||
|
timezone: string,
|
||||||
|
count: number = 4,
|
||||||
|
): Date[] {
|
||||||
|
try {
|
||||||
|
const options = timezone ? { tz: timezone, currentDate: new Date() } : { currentDate: new Date() };
|
||||||
|
const interval = CronExpressionParser.parse(expression, options);
|
||||||
|
const dates: Date[] = [];
|
||||||
|
for (let i = 0; i < count; i += 1) {
|
||||||
|
const next = interval.next();
|
||||||
|
if (next) {
|
||||||
|
dates.push(next.toDate());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return dates;
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CRON_EXAMPLES: ReadonlyArray<{
|
||||||
|
expression: string;
|
||||||
|
labelKey: string;
|
||||||
|
}> = [
|
||||||
|
{ expression: '*/5 * * * *', labelKey: 'sessions.scheduledTasks.editor.cronExpression.examples.every5min' },
|
||||||
|
{ expression: '0 * * * *', labelKey: 'sessions.scheduledTasks.editor.cronExpression.examples.everyHour' },
|
||||||
|
{ expression: '0 9 * * 1', labelKey: 'sessions.scheduledTasks.editor.cronExpression.examples.monday9am' },
|
||||||
|
{ expression: '0 9,17 * * *', labelKey: 'sessions.scheduledTasks.editor.cronExpression.examples.9am5pm' },
|
||||||
|
{ expression: '0 0 1 * *', labelKey: 'sessions.scheduledTasks.editor.cronExpression.examples.firstOfMonth' },
|
||||||
|
];
|
||||||
@@ -213,6 +213,7 @@ export const dict = {
|
|||||||
'sessions.scheduledTasks.editor.scheduleType.daily': 'Daily',
|
'sessions.scheduledTasks.editor.scheduleType.daily': 'Daily',
|
||||||
'sessions.scheduledTasks.editor.scheduleType.weekly': 'Weekly',
|
'sessions.scheduledTasks.editor.scheduleType.weekly': 'Weekly',
|
||||||
'sessions.scheduledTasks.editor.scheduleType.once': 'One-time',
|
'sessions.scheduledTasks.editor.scheduleType.once': 'One-time',
|
||||||
|
'sessions.scheduledTasks.editor.scheduleType.cron': 'Cron',
|
||||||
'sessions.scheduledTasks.editor.date.label': 'Date',
|
'sessions.scheduledTasks.editor.date.label': 'Date',
|
||||||
'sessions.scheduledTasks.editor.date.placeholder': 'Select date',
|
'sessions.scheduledTasks.editor.date.placeholder': 'Select date',
|
||||||
'sessions.scheduledTasks.editor.date.previousMonth': 'Previous month',
|
'sessions.scheduledTasks.editor.date.previousMonth': 'Previous month',
|
||||||
@@ -251,6 +252,17 @@ export const dict = {
|
|||||||
'sessions.scheduledTasks.editor.validation.atLeastOneTime': 'Add at least one valid time',
|
'sessions.scheduledTasks.editor.validation.atLeastOneTime': 'Add at least one valid time',
|
||||||
'sessions.scheduledTasks.editor.validation.atLeastOneWeekday': 'Select at least one weekday',
|
'sessions.scheduledTasks.editor.validation.atLeastOneWeekday': 'Select at least one weekday',
|
||||||
'sessions.scheduledTasks.editor.validation.timezoneRequired': 'Timezone is required',
|
'sessions.scheduledTasks.editor.validation.timezoneRequired': 'Timezone is required',
|
||||||
|
'sessions.scheduledTasks.editor.validation.cronRequired': 'Cron expression is required',
|
||||||
|
'sessions.scheduledTasks.editor.validation.cronInvalid': 'Invalid cron expression',
|
||||||
|
'sessions.scheduledTasks.editor.cronExpression.label': 'Cron expression',
|
||||||
|
'sessions.scheduledTasks.editor.cronExpression.placeholder': '*/5 * * * *',
|
||||||
|
'sessions.scheduledTasks.editor.cronExpression.nextRuns': 'Next runs',
|
||||||
|
'sessions.scheduledTasks.editor.cronExpression.examples': 'Examples',
|
||||||
|
'sessions.scheduledTasks.editor.cronExpression.examples.every5min': 'Every 5 minutes',
|
||||||
|
'sessions.scheduledTasks.editor.cronExpression.examples.everyHour': 'Every hour',
|
||||||
|
'sessions.scheduledTasks.editor.cronExpression.examples.monday9am': 'Every Monday at 9am',
|
||||||
|
'sessions.scheduledTasks.editor.cronExpression.examples.9am5pm': '9am and 5pm daily',
|
||||||
|
'sessions.scheduledTasks.editor.cronExpression.examples.firstOfMonth': 'First of every month',
|
||||||
'multirun.launcher.title': 'New Multi-Run',
|
'multirun.launcher.title': 'New Multi-Run',
|
||||||
'multirun.launcher.actions.closeEsc': 'Close (Esc)',
|
'multirun.launcher.actions.closeEsc': 'Close (Esc)',
|
||||||
'multirun.launcher.actions.cancel': 'Cancel',
|
'multirun.launcher.actions.cancel': 'Cancel',
|
||||||
|
|||||||
@@ -214,6 +214,7 @@ export const dict: Record<I18nKey, string> = {
|
|||||||
"sessions.scheduledTasks.editor.scheduleType.daily": "Diario",
|
"sessions.scheduledTasks.editor.scheduleType.daily": "Diario",
|
||||||
"sessions.scheduledTasks.editor.scheduleType.weekly": "Semanal",
|
"sessions.scheduledTasks.editor.scheduleType.weekly": "Semanal",
|
||||||
"sessions.scheduledTasks.editor.scheduleType.once": "Una vez",
|
"sessions.scheduledTasks.editor.scheduleType.once": "Una vez",
|
||||||
|
"sessions.scheduledTasks.editor.scheduleType.cron": "Cron",
|
||||||
"sessions.scheduledTasks.editor.date.label": "Fecha",
|
"sessions.scheduledTasks.editor.date.label": "Fecha",
|
||||||
"sessions.scheduledTasks.editor.date.placeholder": "Seleccionar fecha",
|
"sessions.scheduledTasks.editor.date.placeholder": "Seleccionar fecha",
|
||||||
"sessions.scheduledTasks.editor.date.previousMonth": "Mes anterior",
|
"sessions.scheduledTasks.editor.date.previousMonth": "Mes anterior",
|
||||||
@@ -252,6 +253,17 @@ export const dict: Record<I18nKey, string> = {
|
|||||||
"sessions.scheduledTasks.editor.validation.atLeastOneTime": "Añadir al menos una hora válida",
|
"sessions.scheduledTasks.editor.validation.atLeastOneTime": "Añadir al menos una hora válida",
|
||||||
"sessions.scheduledTasks.editor.validation.atLeastOneWeekday": "Seleccionar al menos un día de la semana",
|
"sessions.scheduledTasks.editor.validation.atLeastOneWeekday": "Seleccionar al menos un día de la semana",
|
||||||
"sessions.scheduledTasks.editor.validation.timezoneRequired": "Se requiere zona horaria",
|
"sessions.scheduledTasks.editor.validation.timezoneRequired": "Se requiere zona horaria",
|
||||||
|
"sessions.scheduledTasks.editor.validation.cronRequired": "Se requiere una expresión cron",
|
||||||
|
"sessions.scheduledTasks.editor.validation.cronInvalid": "Expresión cron no válida",
|
||||||
|
"sessions.scheduledTasks.editor.cronExpression.label": "Expresión cron",
|
||||||
|
"sessions.scheduledTasks.editor.cronExpression.placeholder": "*/5 * * * *",
|
||||||
|
"sessions.scheduledTasks.editor.cronExpression.nextRuns": "Próximas ejecuciones",
|
||||||
|
"sessions.scheduledTasks.editor.cronExpression.examples": "Ejemplos",
|
||||||
|
"sessions.scheduledTasks.editor.cronExpression.examples.every5min": "Cada 5 minutos",
|
||||||
|
"sessions.scheduledTasks.editor.cronExpression.examples.everyHour": "Cada hora",
|
||||||
|
"sessions.scheduledTasks.editor.cronExpression.examples.monday9am": "Cada lunes a las 9 a. m.",
|
||||||
|
"sessions.scheduledTasks.editor.cronExpression.examples.9am5pm": "9 a. m. y 5 p. m. todos los días",
|
||||||
|
"sessions.scheduledTasks.editor.cronExpression.examples.firstOfMonth": "Primero de cada mes",
|
||||||
"multirun.launcher.title": "Nuevo Multi-Run",
|
"multirun.launcher.title": "Nuevo Multi-Run",
|
||||||
"multirun.launcher.actions.closeEsc": "Cerrar (Esc)",
|
"multirun.launcher.actions.closeEsc": "Cerrar (Esc)",
|
||||||
"multirun.launcher.actions.cancel": "Cancelar",
|
"multirun.launcher.actions.cancel": "Cancelar",
|
||||||
|
|||||||
@@ -141,6 +141,17 @@ export const dict = {
|
|||||||
'sessions.scheduledTasks.editor.validation.atLeastOneTime': 'Ajoutez au moins une heure valide',
|
'sessions.scheduledTasks.editor.validation.atLeastOneTime': 'Ajoutez au moins une heure valide',
|
||||||
'sessions.scheduledTasks.editor.validation.atLeastOneWeekday': 'Sélectionnez au moins un jour de la semaine',
|
'sessions.scheduledTasks.editor.validation.atLeastOneWeekday': 'Sélectionnez au moins un jour de la semaine',
|
||||||
'sessions.scheduledTasks.editor.validation.timezoneRequired': 'Le fuseau horaire est requis',
|
'sessions.scheduledTasks.editor.validation.timezoneRequired': 'Le fuseau horaire est requis',
|
||||||
|
'sessions.scheduledTasks.editor.validation.cronRequired': 'Une expression cron est requise',
|
||||||
|
'sessions.scheduledTasks.editor.validation.cronInvalid': 'Expression cron invalide',
|
||||||
|
'sessions.scheduledTasks.editor.cronExpression.label': 'Expression cron',
|
||||||
|
'sessions.scheduledTasks.editor.cronExpression.placeholder': '*/5 * * * *',
|
||||||
|
'sessions.scheduledTasks.editor.cronExpression.nextRuns': 'Prochaines exécutions',
|
||||||
|
'sessions.scheduledTasks.editor.cronExpression.examples': 'Exemples',
|
||||||
|
'sessions.scheduledTasks.editor.cronExpression.examples.every5min': 'Toutes les 5 minutes',
|
||||||
|
'sessions.scheduledTasks.editor.cronExpression.examples.everyHour': 'Toutes les heures',
|
||||||
|
'sessions.scheduledTasks.editor.cronExpression.examples.monday9am': 'Chaque lundi à 9 h',
|
||||||
|
'sessions.scheduledTasks.editor.cronExpression.examples.9am5pm': '9 h et 17 h chaque jour',
|
||||||
|
'sessions.scheduledTasks.editor.cronExpression.examples.firstOfMonth': 'Premier jour de chaque mois',
|
||||||
'multirun.launcher.title': 'Nouveau multi-run',
|
'multirun.launcher.title': 'Nouveau multi-run',
|
||||||
'multirun.launcher.actions.closeEsc': 'Fermer (Échap)',
|
'multirun.launcher.actions.closeEsc': 'Fermer (Échap)',
|
||||||
'multirun.launcher.actions.cancel': 'Annuler',
|
'multirun.launcher.actions.cancel': 'Annuler',
|
||||||
|
|||||||
@@ -214,6 +214,7 @@ export const dict: Record<I18nKey, string> = {
|
|||||||
'sessions.scheduledTasks.editor.scheduleType.daily': '매일',
|
'sessions.scheduledTasks.editor.scheduleType.daily': '매일',
|
||||||
'sessions.scheduledTasks.editor.scheduleType.weekly': '매주',
|
'sessions.scheduledTasks.editor.scheduleType.weekly': '매주',
|
||||||
'sessions.scheduledTasks.editor.scheduleType.once': '한 번',
|
'sessions.scheduledTasks.editor.scheduleType.once': '한 번',
|
||||||
|
'sessions.scheduledTasks.editor.scheduleType.cron': 'Cron',
|
||||||
'sessions.scheduledTasks.editor.date.label': '날짜',
|
'sessions.scheduledTasks.editor.date.label': '날짜',
|
||||||
'sessions.scheduledTasks.editor.date.placeholder': '날짜 선택',
|
'sessions.scheduledTasks.editor.date.placeholder': '날짜 선택',
|
||||||
'sessions.scheduledTasks.editor.date.previousMonth': '이전 달',
|
'sessions.scheduledTasks.editor.date.previousMonth': '이전 달',
|
||||||
@@ -252,6 +253,17 @@ export const dict: Record<I18nKey, string> = {
|
|||||||
'sessions.scheduledTasks.editor.validation.atLeastOneTime': '유효한 시간을 하나 이상 추가하세요',
|
'sessions.scheduledTasks.editor.validation.atLeastOneTime': '유효한 시간을 하나 이상 추가하세요',
|
||||||
'sessions.scheduledTasks.editor.validation.atLeastOneWeekday': '요일을 하나 이상 선택하세요',
|
'sessions.scheduledTasks.editor.validation.atLeastOneWeekday': '요일을 하나 이상 선택하세요',
|
||||||
'sessions.scheduledTasks.editor.validation.timezoneRequired': '시간대 필수',
|
'sessions.scheduledTasks.editor.validation.timezoneRequired': '시간대 필수',
|
||||||
|
'sessions.scheduledTasks.editor.validation.cronRequired': 'Cron 표현식이 필요합니다',
|
||||||
|
'sessions.scheduledTasks.editor.validation.cronInvalid': '유효하지 않은 cron 표현식',
|
||||||
|
'sessions.scheduledTasks.editor.cronExpression.label': 'Cron 표현식',
|
||||||
|
'sessions.scheduledTasks.editor.cronExpression.placeholder': '*/5 * * * *',
|
||||||
|
'sessions.scheduledTasks.editor.cronExpression.nextRuns': '다음 실행',
|
||||||
|
'sessions.scheduledTasks.editor.cronExpression.examples': '예시',
|
||||||
|
'sessions.scheduledTasks.editor.cronExpression.examples.every5min': '5분마다',
|
||||||
|
'sessions.scheduledTasks.editor.cronExpression.examples.everyHour': '매시간',
|
||||||
|
'sessions.scheduledTasks.editor.cronExpression.examples.monday9am': '매주 월요일 오전 9시',
|
||||||
|
'sessions.scheduledTasks.editor.cronExpression.examples.9am5pm': '매일 오전 9시와 오후 5시',
|
||||||
|
'sessions.scheduledTasks.editor.cronExpression.examples.firstOfMonth': '매월 1일',
|
||||||
'multirun.launcher.title': '새 다중 실행',
|
'multirun.launcher.title': '새 다중 실행',
|
||||||
'multirun.launcher.actions.closeEsc': '닫기(Esc)',
|
'multirun.launcher.actions.closeEsc': '닫기(Esc)',
|
||||||
'multirun.launcher.actions.cancel': '취소',
|
'multirun.launcher.actions.cancel': '취소',
|
||||||
|
|||||||
@@ -295,6 +295,7 @@ export const dict: Record<I18nKey, string> = {
|
|||||||
'sessions.scheduledTasks.editor.scheduleType.daily': 'Codziennie',
|
'sessions.scheduledTasks.editor.scheduleType.daily': 'Codziennie',
|
||||||
'sessions.scheduledTasks.editor.scheduleType.weekly': 'Co tydzień',
|
'sessions.scheduledTasks.editor.scheduleType.weekly': 'Co tydzień',
|
||||||
'sessions.scheduledTasks.editor.scheduleType.once': 'Jednorazowo',
|
'sessions.scheduledTasks.editor.scheduleType.once': 'Jednorazowo',
|
||||||
|
'sessions.scheduledTasks.editor.scheduleType.cron': 'Cron',
|
||||||
'sessions.scheduledTasks.editor.date.label': 'Data',
|
'sessions.scheduledTasks.editor.date.label': 'Data',
|
||||||
'sessions.scheduledTasks.editor.date.placeholder': 'Wybierz datę',
|
'sessions.scheduledTasks.editor.date.placeholder': 'Wybierz datę',
|
||||||
'sessions.scheduledTasks.editor.date.previousMonth': 'Poprzedni miesiąc',
|
'sessions.scheduledTasks.editor.date.previousMonth': 'Poprzedni miesiąc',
|
||||||
@@ -333,6 +334,17 @@ export const dict: Record<I18nKey, string> = {
|
|||||||
'sessions.scheduledTasks.editor.validation.atLeastOneTime': 'Dodaj przynajmniej jeden prawidłowy czas',
|
'sessions.scheduledTasks.editor.validation.atLeastOneTime': 'Dodaj przynajmniej jeden prawidłowy czas',
|
||||||
'sessions.scheduledTasks.editor.validation.atLeastOneWeekday': 'Wybierz przynajmniej jeden dzień tygodnia',
|
'sessions.scheduledTasks.editor.validation.atLeastOneWeekday': 'Wybierz przynajmniej jeden dzień tygodnia',
|
||||||
'sessions.scheduledTasks.editor.validation.timezoneRequired': 'Strefa czasowa jest wymagana',
|
'sessions.scheduledTasks.editor.validation.timezoneRequired': 'Strefa czasowa jest wymagana',
|
||||||
|
'sessions.scheduledTasks.editor.validation.cronRequired': 'Wyrażenie cron jest wymagane',
|
||||||
|
'sessions.scheduledTasks.editor.validation.cronInvalid': 'Nieprawidłowe wyrażenie cron',
|
||||||
|
'sessions.scheduledTasks.editor.cronExpression.label': 'Wyrażenie cron',
|
||||||
|
'sessions.scheduledTasks.editor.cronExpression.placeholder': '*/5 * * * *',
|
||||||
|
'sessions.scheduledTasks.editor.cronExpression.nextRuns': 'Następne uruchomienia',
|
||||||
|
'sessions.scheduledTasks.editor.cronExpression.examples': 'Przykłady',
|
||||||
|
'sessions.scheduledTasks.editor.cronExpression.examples.every5min': 'Co 5 minut',
|
||||||
|
'sessions.scheduledTasks.editor.cronExpression.examples.everyHour': 'Co godzinę',
|
||||||
|
'sessions.scheduledTasks.editor.cronExpression.examples.monday9am': 'W każdy poniedziałek o 9:00',
|
||||||
|
'sessions.scheduledTasks.editor.cronExpression.examples.9am5pm': 'Codziennie o 9:00 i 17:00',
|
||||||
|
'sessions.scheduledTasks.editor.cronExpression.examples.firstOfMonth': 'Pierwszego dnia każdego miesiąca',
|
||||||
'multirun.launcher.title': 'Nowe wielokrotne uruchomienie',
|
'multirun.launcher.title': 'Nowe wielokrotne uruchomienie',
|
||||||
'multirun.launcher.actions.closeEsc': 'Zamknij (Esc)',
|
'multirun.launcher.actions.closeEsc': 'Zamknij (Esc)',
|
||||||
'multirun.launcher.actions.cancel': 'Anuluj',
|
'multirun.launcher.actions.cancel': 'Anuluj',
|
||||||
|
|||||||
@@ -214,6 +214,7 @@ export const dict: Record<I18nKey, string> = {
|
|||||||
"sessions.scheduledTasks.editor.scheduleType.daily": "Diário",
|
"sessions.scheduledTasks.editor.scheduleType.daily": "Diário",
|
||||||
"sessions.scheduledTasks.editor.scheduleType.weekly": "Semanal",
|
"sessions.scheduledTasks.editor.scheduleType.weekly": "Semanal",
|
||||||
"sessions.scheduledTasks.editor.scheduleType.once": "Uma vez",
|
"sessions.scheduledTasks.editor.scheduleType.once": "Uma vez",
|
||||||
|
"sessions.scheduledTasks.editor.scheduleType.cron": "Cron",
|
||||||
"sessions.scheduledTasks.editor.date.label": "Data",
|
"sessions.scheduledTasks.editor.date.label": "Data",
|
||||||
"sessions.scheduledTasks.editor.date.placeholder": "Selecionar data",
|
"sessions.scheduledTasks.editor.date.placeholder": "Selecionar data",
|
||||||
"sessions.scheduledTasks.editor.date.previousMonth": "Mês anterior",
|
"sessions.scheduledTasks.editor.date.previousMonth": "Mês anterior",
|
||||||
@@ -252,6 +253,17 @@ export const dict: Record<I18nKey, string> = {
|
|||||||
"sessions.scheduledTasks.editor.validation.atLeastOneTime": "Adicionar ao menos uma hora válida",
|
"sessions.scheduledTasks.editor.validation.atLeastOneTime": "Adicionar ao menos uma hora válida",
|
||||||
"sessions.scheduledTasks.editor.validation.atLeastOneWeekday": "Selecione ao menos um dia da semana",
|
"sessions.scheduledTasks.editor.validation.atLeastOneWeekday": "Selecione ao menos um dia da semana",
|
||||||
"sessions.scheduledTasks.editor.validation.timezoneRequired": "É necessário informar o fuso horário",
|
"sessions.scheduledTasks.editor.validation.timezoneRequired": "É necessário informar o fuso horário",
|
||||||
|
"sessions.scheduledTasks.editor.validation.cronRequired": "É necessário informar uma expressão cron",
|
||||||
|
"sessions.scheduledTasks.editor.validation.cronInvalid": "Expressão cron inválida",
|
||||||
|
"sessions.scheduledTasks.editor.cronExpression.label": "Expressão cron",
|
||||||
|
"sessions.scheduledTasks.editor.cronExpression.placeholder": "*/5 * * * *",
|
||||||
|
"sessions.scheduledTasks.editor.cronExpression.nextRuns": "Próximas execuções",
|
||||||
|
"sessions.scheduledTasks.editor.cronExpression.examples": "Exemplos",
|
||||||
|
"sessions.scheduledTasks.editor.cronExpression.examples.every5min": "A cada 5 minutos",
|
||||||
|
"sessions.scheduledTasks.editor.cronExpression.examples.everyHour": "A cada hora",
|
||||||
|
"sessions.scheduledTasks.editor.cronExpression.examples.monday9am": "Toda segunda-feira às 9h",
|
||||||
|
"sessions.scheduledTasks.editor.cronExpression.examples.9am5pm": "9h e 17h diariamente",
|
||||||
|
"sessions.scheduledTasks.editor.cronExpression.examples.firstOfMonth": "Primeiro dia de cada mês",
|
||||||
"multirun.launcher.title": "Novo Multi-Run",
|
"multirun.launcher.title": "Novo Multi-Run",
|
||||||
"multirun.launcher.actions.closeEsc": "Fechar (Esc)",
|
"multirun.launcher.actions.closeEsc": "Fechar (Esc)",
|
||||||
"multirun.launcher.actions.cancel": "Cancelar",
|
"multirun.launcher.actions.cancel": "Cancelar",
|
||||||
|
|||||||
@@ -214,6 +214,7 @@ export const dict: Record<I18nKey, string> = {
|
|||||||
"sessions.scheduledTasks.editor.scheduleType.daily": "Щодня",
|
"sessions.scheduledTasks.editor.scheduleType.daily": "Щодня",
|
||||||
"sessions.scheduledTasks.editor.scheduleType.weekly": "Щотижня",
|
"sessions.scheduledTasks.editor.scheduleType.weekly": "Щотижня",
|
||||||
"sessions.scheduledTasks.editor.scheduleType.once": "Одноразовий",
|
"sessions.scheduledTasks.editor.scheduleType.once": "Одноразовий",
|
||||||
|
"sessions.scheduledTasks.editor.scheduleType.cron": "Cron",
|
||||||
"sessions.scheduledTasks.editor.date.label": "Дата",
|
"sessions.scheduledTasks.editor.date.label": "Дата",
|
||||||
"sessions.scheduledTasks.editor.date.placeholder": "Вибрати дату",
|
"sessions.scheduledTasks.editor.date.placeholder": "Вибрати дату",
|
||||||
"sessions.scheduledTasks.editor.date.previousMonth": "Попередній місяць",
|
"sessions.scheduledTasks.editor.date.previousMonth": "Попередній місяць",
|
||||||
@@ -252,6 +253,17 @@ export const dict: Record<I18nKey, string> = {
|
|||||||
"sessions.scheduledTasks.editor.validation.atLeastOneTime": "Додайте принаймні один дійсний час",
|
"sessions.scheduledTasks.editor.validation.atLeastOneTime": "Додайте принаймні один дійсний час",
|
||||||
"sessions.scheduledTasks.editor.validation.atLeastOneWeekday": "Виберіть хоча б один день тижня",
|
"sessions.scheduledTasks.editor.validation.atLeastOneWeekday": "Виберіть хоча б один день тижня",
|
||||||
"sessions.scheduledTasks.editor.validation.timezoneRequired": "Потрібно вказати часовий пояс",
|
"sessions.scheduledTasks.editor.validation.timezoneRequired": "Потрібно вказати часовий пояс",
|
||||||
|
"sessions.scheduledTasks.editor.validation.cronRequired": "Потрібно вказати cron-вираз",
|
||||||
|
"sessions.scheduledTasks.editor.validation.cronInvalid": "Недійсний cron-вираз",
|
||||||
|
"sessions.scheduledTasks.editor.cronExpression.label": "Cron-вираз",
|
||||||
|
"sessions.scheduledTasks.editor.cronExpression.placeholder": "*/5 * * * *",
|
||||||
|
"sessions.scheduledTasks.editor.cronExpression.nextRuns": "Наступні запуски",
|
||||||
|
"sessions.scheduledTasks.editor.cronExpression.examples": "Приклади",
|
||||||
|
"sessions.scheduledTasks.editor.cronExpression.examples.every5min": "Кожні 5 хвилин",
|
||||||
|
"sessions.scheduledTasks.editor.cronExpression.examples.everyHour": "Щогодини",
|
||||||
|
"sessions.scheduledTasks.editor.cronExpression.examples.monday9am": "Щопонеділка о 9:00",
|
||||||
|
"sessions.scheduledTasks.editor.cronExpression.examples.9am5pm": "Щодня о 9:00 і 17:00",
|
||||||
|
"sessions.scheduledTasks.editor.cronExpression.examples.firstOfMonth": "Першого числа кожного місяця",
|
||||||
"multirun.launcher.title": "Новий Multi-Run",
|
"multirun.launcher.title": "Новий Multi-Run",
|
||||||
"multirun.launcher.actions.closeEsc": "Закрити (Esc)",
|
"multirun.launcher.actions.closeEsc": "Закрити (Esc)",
|
||||||
"multirun.launcher.actions.cancel": "Скасувати",
|
"multirun.launcher.actions.cancel": "Скасувати",
|
||||||
|
|||||||
@@ -214,6 +214,7 @@ export const dict: Record<I18nKey, string> = {
|
|||||||
'sessions.scheduledTasks.editor.scheduleType.daily': '每天',
|
'sessions.scheduledTasks.editor.scheduleType.daily': '每天',
|
||||||
'sessions.scheduledTasks.editor.scheduleType.weekly': '每周',
|
'sessions.scheduledTasks.editor.scheduleType.weekly': '每周',
|
||||||
'sessions.scheduledTasks.editor.scheduleType.once': '一次性',
|
'sessions.scheduledTasks.editor.scheduleType.once': '一次性',
|
||||||
|
'sessions.scheduledTasks.editor.scheduleType.cron': 'Cron',
|
||||||
'sessions.scheduledTasks.editor.date.label': '日期',
|
'sessions.scheduledTasks.editor.date.label': '日期',
|
||||||
'sessions.scheduledTasks.editor.date.placeholder': '选择日期',
|
'sessions.scheduledTasks.editor.date.placeholder': '选择日期',
|
||||||
'sessions.scheduledTasks.editor.date.previousMonth': '上个月',
|
'sessions.scheduledTasks.editor.date.previousMonth': '上个月',
|
||||||
@@ -252,6 +253,17 @@ export const dict: Record<I18nKey, string> = {
|
|||||||
'sessions.scheduledTasks.editor.validation.atLeastOneTime': '请至少添加一个有效时间',
|
'sessions.scheduledTasks.editor.validation.atLeastOneTime': '请至少添加一个有效时间',
|
||||||
'sessions.scheduledTasks.editor.validation.atLeastOneWeekday': '请至少选择一个星期',
|
'sessions.scheduledTasks.editor.validation.atLeastOneWeekday': '请至少选择一个星期',
|
||||||
'sessions.scheduledTasks.editor.validation.timezoneRequired': '时区为必填项',
|
'sessions.scheduledTasks.editor.validation.timezoneRequired': '时区为必填项',
|
||||||
|
'sessions.scheduledTasks.editor.validation.cronRequired': 'Cron 表达式为必填项',
|
||||||
|
'sessions.scheduledTasks.editor.validation.cronInvalid': 'Cron 表达式无效',
|
||||||
|
'sessions.scheduledTasks.editor.cronExpression.label': 'Cron 表达式',
|
||||||
|
'sessions.scheduledTasks.editor.cronExpression.placeholder': '*/5 * * * *',
|
||||||
|
'sessions.scheduledTasks.editor.cronExpression.nextRuns': '下次运行',
|
||||||
|
'sessions.scheduledTasks.editor.cronExpression.examples': '示例',
|
||||||
|
'sessions.scheduledTasks.editor.cronExpression.examples.every5min': '每 5 分钟',
|
||||||
|
'sessions.scheduledTasks.editor.cronExpression.examples.everyHour': '每小时',
|
||||||
|
'sessions.scheduledTasks.editor.cronExpression.examples.monday9am': '每周一上午 9 点',
|
||||||
|
'sessions.scheduledTasks.editor.cronExpression.examples.9am5pm': '每天上午 9 点和下午 5 点',
|
||||||
|
'sessions.scheduledTasks.editor.cronExpression.examples.firstOfMonth': '每月第一天',
|
||||||
'multirun.launcher.title': '新建多运行',
|
'multirun.launcher.title': '新建多运行',
|
||||||
'multirun.launcher.actions.closeEsc': '关闭 (Esc)',
|
'multirun.launcher.actions.closeEsc': '关闭 (Esc)',
|
||||||
'multirun.launcher.actions.cancel': '取消',
|
'multirun.launcher.actions.cancel': '取消',
|
||||||
|
|||||||
@@ -227,6 +227,7 @@ export const dict: Record<I18nKey, string> = {
|
|||||||
'sessions.scheduledTasks.editor.scheduleType.daily': '每天',
|
'sessions.scheduledTasks.editor.scheduleType.daily': '每天',
|
||||||
'sessions.scheduledTasks.editor.scheduleType.weekly': '每週',
|
'sessions.scheduledTasks.editor.scheduleType.weekly': '每週',
|
||||||
'sessions.scheduledTasks.editor.scheduleType.once': '一次性',
|
'sessions.scheduledTasks.editor.scheduleType.once': '一次性',
|
||||||
|
'sessions.scheduledTasks.editor.scheduleType.cron': 'Cron',
|
||||||
'sessions.scheduledTasks.editor.date.label': '日期',
|
'sessions.scheduledTasks.editor.date.label': '日期',
|
||||||
'sessions.scheduledTasks.editor.date.placeholder': '選擇日期',
|
'sessions.scheduledTasks.editor.date.placeholder': '選擇日期',
|
||||||
'sessions.scheduledTasks.editor.date.previousMonth': '上個月',
|
'sessions.scheduledTasks.editor.date.previousMonth': '上個月',
|
||||||
@@ -265,6 +266,17 @@ export const dict: Record<I18nKey, string> = {
|
|||||||
'sessions.scheduledTasks.editor.validation.atLeastOneTime': '請至少新增一個有效時間',
|
'sessions.scheduledTasks.editor.validation.atLeastOneTime': '請至少新增一個有效時間',
|
||||||
'sessions.scheduledTasks.editor.validation.atLeastOneWeekday': '請至少選擇一個星期',
|
'sessions.scheduledTasks.editor.validation.atLeastOneWeekday': '請至少選擇一個星期',
|
||||||
'sessions.scheduledTasks.editor.validation.timezoneRequired': '時區為必填項',
|
'sessions.scheduledTasks.editor.validation.timezoneRequired': '時區為必填項',
|
||||||
|
'sessions.scheduledTasks.editor.validation.cronRequired': 'Cron 表達式為必填項',
|
||||||
|
'sessions.scheduledTasks.editor.validation.cronInvalid': 'Cron 表達式無效',
|
||||||
|
'sessions.scheduledTasks.editor.cronExpression.label': 'Cron 表達式',
|
||||||
|
'sessions.scheduledTasks.editor.cronExpression.placeholder': '*/5 * * * *',
|
||||||
|
'sessions.scheduledTasks.editor.cronExpression.nextRuns': '下次執行',
|
||||||
|
'sessions.scheduledTasks.editor.cronExpression.examples': '範例',
|
||||||
|
'sessions.scheduledTasks.editor.cronExpression.examples.every5min': '每 5 分鐘',
|
||||||
|
'sessions.scheduledTasks.editor.cronExpression.examples.everyHour': '每小時',
|
||||||
|
'sessions.scheduledTasks.editor.cronExpression.examples.monday9am': '每週一上午 9 點',
|
||||||
|
'sessions.scheduledTasks.editor.cronExpression.examples.9am5pm': '每天上午 9 點和下午 5 點',
|
||||||
|
'sessions.scheduledTasks.editor.cronExpression.examples.firstOfMonth': '每月第一天',
|
||||||
'multirun.launcher.title': '新增 Multi-run',
|
'multirun.launcher.title': '新增 Multi-run',
|
||||||
'multirun.launcher.actions.closeEsc': '關閉 (Esc)',
|
'multirun.launcher.actions.closeEsc': '關閉 (Esc)',
|
||||||
'multirun.launcher.actions.cancel': '取消',
|
'multirun.launcher.actions.cancel': '取消',
|
||||||
|
|||||||
Reference in New Issue
Block a user