From e402cd75f5a95426a7e3300a907bb21367882030 Mon Sep 17 00:00:00 2001 From: Tom Rochette Date: Tue, 16 Jun 2026 04:03:42 -0400 Subject: [PATCH] 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 --- bun.lock | 7 +- packages/ui/package.json | 1 + .../session/ScheduledTaskEditorDialog.tsx | 164 ++++++++++++++++-- packages/ui/src/lib/cron.test.ts | 29 ++++ packages/ui/src/lib/cron.ts | 51 ++++++ packages/ui/src/lib/i18n/messages/en.ts | 12 ++ packages/ui/src/lib/i18n/messages/es.ts | 12 ++ packages/ui/src/lib/i18n/messages/fr.ts | 11 ++ packages/ui/src/lib/i18n/messages/ko.ts | 12 ++ packages/ui/src/lib/i18n/messages/pl.ts | 12 ++ packages/ui/src/lib/i18n/messages/pt-BR.ts | 12 ++ packages/ui/src/lib/i18n/messages/uk.ts | 12 ++ packages/ui/src/lib/i18n/messages/zh-CN.ts | 12 ++ packages/ui/src/lib/i18n/messages/zh-TW.ts | 12 ++ 14 files changed, 341 insertions(+), 18 deletions(-) create mode 100644 packages/ui/src/lib/cron.test.ts create mode 100644 packages/ui/src/lib/cron.ts diff --git a/bun.lock b/bun.lock index 1a0cae52..fe7640a7 100644 --- a/bun.lock +++ b/bun.lock @@ -156,6 +156,7 @@ "clsx": "^2.1.1", "cmdk": "^1.1.1", "codemirror-lang-elixir": "^4.0.0", + "cron-parser": "^5.5.0", "dompurify": "^3.2.7", "express": "^5.1.0", "fuse.js": "^7.1.0", @@ -1676,7 +1677,7 @@ "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=="], @@ -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=="], + "@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-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=="], diff --git a/packages/ui/package.json b/packages/ui/package.json index 0cedffe7..be96ceee 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -51,6 +51,7 @@ "clsx": "^2.1.1", "cmdk": "^1.1.1", "codemirror-lang-elixir": "^4.0.0", + "cron-parser": "^5.5.0", "dompurify": "^3.2.7", "express": "^5.1.0", "fuse.js": "^7.1.0", diff --git a/packages/ui/src/components/session/ScheduledTaskEditorDialog.tsx b/packages/ui/src/components/session/ScheduledTaskEditorDialog.tsx index a481922f..b7310402 100644 --- a/packages/ui/src/components/session/ScheduledTaskEditorDialog.tsx +++ b/packages/ui/src/components/session/ScheduledTaskEditorDialog.tsx @@ -20,6 +20,7 @@ import { useConfigStore } from '@/stores/useConfigStore'; import { useUIStore } from '@/stores/useUIStore'; import type { ScheduledTask } from '@/lib/scheduledTasksApi'; 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; @@ -451,12 +452,13 @@ type ScheduledTaskDraft = { name: string; enabled: boolean; schedule: { - kind: 'daily' | 'weekly' | 'once'; + kind: 'daily' | 'weekly' | 'once' | 'cron'; times: string[]; onceDate: string; onceTime: string; weekdays: number[]; timezone: string; + cronExpression: string; }; execution: { prompt: string; @@ -505,6 +507,7 @@ const toDraft = ( onceTime: '09:00', weekdays: [1], timezone: timezoneFallback, + cronExpression: '', }, execution: { prompt: '', @@ -521,9 +524,11 @@ const toDraft = ( name: task.name, enabled: task.enabled, schedule: { - kind: task.schedule.kind === 'once' - ? 'once' - : (task.schedule.kind === 'weekly' ? 'weekly' : 'daily'), + kind: task.schedule.kind === 'cron' + ? 'cron' + : (task.schedule.kind === 'once' + ? 'once' + : (task.schedule.kind === 'weekly' ? 'weekly' : 'daily')), times: normalizeDraftTimes(task), onceDate: typeof task.schedule.date === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(task.schedule.date) ? task.schedule.date @@ -533,6 +538,9 @@ const toDraft = ( : '09:00', weekdays: Array.isArray(task.schedule.weekdays) ? task.schedule.weekdays : [1], timezone: task.schedule.timezone || timezoneFallback, + cronExpression: task.schedule.kind === 'cron' && typeof task.schedule.cron === 'string' + ? task.schedule.cron + : '', }, execution: { prompt: task.execution.prompt, @@ -563,6 +571,14 @@ const validateDraft = (draft: ScheduledTaskDraft, t: ReturnType[ if (!/^([01]\d|2[0-3]):([0-5]\d)$/.test(draft.schedule.onceTime)) { 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 { const validTimes = draft.schedule.times.filter((value) => /^([01]\d|2[0-3]):([0-5]\d)$/.test(value)); if (validTimes.length === 0) { @@ -586,6 +602,108 @@ const dedupeSortTimes = (times: string[]) => { return Array.from(new Set(filtered)).sort((a, b) => a.localeCompare(b)); }; +const CronScheduleSection: React.FC<{ + draft: ScheduledTaskDraft; + setDraft: React.Dispatch>; + locale: string; + t: ReturnType['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 ( +
+
+ {t('sessions.scheduledTasks.editor.cronExpression.label')} + 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() ? ( + + {t('sessions.scheduledTasks.editor.validation.cronInvalid')} + + ) : null} +
+ + {nextRuns.length > 0 ? ( +
+ {t('sessions.scheduledTasks.editor.cronExpression.nextRuns')} + + {nextRuns.map(formatNextRun).join(', ')} + +
+ ) : null} + +
+ {t('sessions.scheduledTasks.editor.cronExpression.examples')} +
+ {CRON_EXAMPLES.map((example) => ( + + ))} +
+
+ +
+ {t('sessions.scheduledTasks.editor.timezone.label')} + +
+
+ ); +}; + export function ScheduledTaskEditorDialog(props: { open: boolean; task: ScheduledTask | null; @@ -1010,7 +1128,6 @@ export function ScheduledTaskEditorDialog(props: { return; } - const normalizedTimes = dedupeSortTimes(draft.schedule.times); const payload: Partial = { ...(draft.id ? { id: draft.id } : {}), name: draft.name.trim(), @@ -1018,15 +1135,17 @@ export function ScheduledTaskEditorDialog(props: { schedule: { kind: draft.schedule.kind, timezone: draft.schedule.timezone.trim(), - ...(draft.schedule.kind === 'once' - ? { - date: draft.schedule.onceDate, - time: draft.schedule.onceTime, + ...(draft.schedule.kind === 'cron' + ? { cron: draft.schedule.cronExpression.trim() } + : draft.schedule.kind === 'once' + ? { + date: draft.schedule.onceDate, + time: draft.schedule.onceTime, } - : { - times: normalizedTimes, - ...(draft.schedule.kind === 'weekly' ? { weekdays: draft.schedule.weekdays } : {}), - }), + : { + times: dedupeSortTimes(draft.schedule.times), + ...(draft.schedule.kind === 'weekly' ? { weekdays: draft.schedule.weekdays } : {}), + }), }, execution: { prompt: draft.execution.prompt, @@ -1081,10 +1200,16 @@ export function ScheduledTaskEditorDialog(props: { {t('sessions.scheduledTasks.editor.scheduleType.label')} - {draft.schedule.kind === 'once' ? ( + {draft.schedule.kind === 'cron' ? ( + + ) : draft.schedule.kind === 'once' ? (
{t('sessions.scheduledTasks.editor.date.label')} diff --git a/packages/ui/src/lib/cron.test.ts b/packages/ui/src/lib/cron.test.ts new file mode 100644 index 00000000..5b936604 --- /dev/null +++ b/packages/ui/src/lib/cron.test.ts @@ -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([]); + }); +}); diff --git a/packages/ui/src/lib/cron.ts b/packages/ui/src/lib/cron.ts new file mode 100644 index 00000000..7bed4bff --- /dev/null +++ b/packages/ui/src/lib/cron.ts @@ -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' }, +]; diff --git a/packages/ui/src/lib/i18n/messages/en.ts b/packages/ui/src/lib/i18n/messages/en.ts index 0559d1c7..dba49cb5 100644 --- a/packages/ui/src/lib/i18n/messages/en.ts +++ b/packages/ui/src/lib/i18n/messages/en.ts @@ -213,6 +213,7 @@ export const dict = { 'sessions.scheduledTasks.editor.scheduleType.daily': 'Daily', 'sessions.scheduledTasks.editor.scheduleType.weekly': 'Weekly', 'sessions.scheduledTasks.editor.scheduleType.once': 'One-time', + 'sessions.scheduledTasks.editor.scheduleType.cron': 'Cron', 'sessions.scheduledTasks.editor.date.label': 'Date', 'sessions.scheduledTasks.editor.date.placeholder': 'Select date', '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.atLeastOneWeekday': 'Select at least one weekday', '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.actions.closeEsc': 'Close (Esc)', 'multirun.launcher.actions.cancel': 'Cancel', diff --git a/packages/ui/src/lib/i18n/messages/es.ts b/packages/ui/src/lib/i18n/messages/es.ts index c0fd6d05..a92e4127 100644 --- a/packages/ui/src/lib/i18n/messages/es.ts +++ b/packages/ui/src/lib/i18n/messages/es.ts @@ -214,6 +214,7 @@ export const dict: Record = { "sessions.scheduledTasks.editor.scheduleType.daily": "Diario", "sessions.scheduledTasks.editor.scheduleType.weekly": "Semanal", "sessions.scheduledTasks.editor.scheduleType.once": "Una vez", + "sessions.scheduledTasks.editor.scheduleType.cron": "Cron", "sessions.scheduledTasks.editor.date.label": "Fecha", "sessions.scheduledTasks.editor.date.placeholder": "Seleccionar fecha", "sessions.scheduledTasks.editor.date.previousMonth": "Mes anterior", @@ -252,6 +253,17 @@ export const dict: Record = { "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.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.actions.closeEsc": "Cerrar (Esc)", "multirun.launcher.actions.cancel": "Cancelar", diff --git a/packages/ui/src/lib/i18n/messages/fr.ts b/packages/ui/src/lib/i18n/messages/fr.ts index 69411df4..9d66af07 100644 --- a/packages/ui/src/lib/i18n/messages/fr.ts +++ b/packages/ui/src/lib/i18n/messages/fr.ts @@ -141,6 +141,17 @@ export const dict = { '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.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.actions.closeEsc': 'Fermer (Échap)', 'multirun.launcher.actions.cancel': 'Annuler', diff --git a/packages/ui/src/lib/i18n/messages/ko.ts b/packages/ui/src/lib/i18n/messages/ko.ts index 9167e702..06532993 100644 --- a/packages/ui/src/lib/i18n/messages/ko.ts +++ b/packages/ui/src/lib/i18n/messages/ko.ts @@ -214,6 +214,7 @@ export const dict: Record = { 'sessions.scheduledTasks.editor.scheduleType.daily': '매일', 'sessions.scheduledTasks.editor.scheduleType.weekly': '매주', 'sessions.scheduledTasks.editor.scheduleType.once': '한 번', + 'sessions.scheduledTasks.editor.scheduleType.cron': 'Cron', 'sessions.scheduledTasks.editor.date.label': '날짜', 'sessions.scheduledTasks.editor.date.placeholder': '날짜 선택', 'sessions.scheduledTasks.editor.date.previousMonth': '이전 달', @@ -252,6 +253,17 @@ export const dict: Record = { 'sessions.scheduledTasks.editor.validation.atLeastOneTime': '유효한 시간을 하나 이상 추가하세요', 'sessions.scheduledTasks.editor.validation.atLeastOneWeekday': '요일을 하나 이상 선택하세요', '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.actions.closeEsc': '닫기(Esc)', 'multirun.launcher.actions.cancel': '취소', diff --git a/packages/ui/src/lib/i18n/messages/pl.ts b/packages/ui/src/lib/i18n/messages/pl.ts index c4b5f2a9..664f62ed 100644 --- a/packages/ui/src/lib/i18n/messages/pl.ts +++ b/packages/ui/src/lib/i18n/messages/pl.ts @@ -295,6 +295,7 @@ export const dict: Record = { 'sessions.scheduledTasks.editor.scheduleType.daily': 'Codziennie', 'sessions.scheduledTasks.editor.scheduleType.weekly': 'Co tydzień', 'sessions.scheduledTasks.editor.scheduleType.once': 'Jednorazowo', + 'sessions.scheduledTasks.editor.scheduleType.cron': 'Cron', 'sessions.scheduledTasks.editor.date.label': 'Data', 'sessions.scheduledTasks.editor.date.placeholder': 'Wybierz datę', 'sessions.scheduledTasks.editor.date.previousMonth': 'Poprzedni miesiąc', @@ -333,6 +334,17 @@ export const dict: Record = { '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.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.actions.closeEsc': 'Zamknij (Esc)', 'multirun.launcher.actions.cancel': 'Anuluj', diff --git a/packages/ui/src/lib/i18n/messages/pt-BR.ts b/packages/ui/src/lib/i18n/messages/pt-BR.ts index b69e680c..8f345e85 100644 --- a/packages/ui/src/lib/i18n/messages/pt-BR.ts +++ b/packages/ui/src/lib/i18n/messages/pt-BR.ts @@ -214,6 +214,7 @@ export const dict: Record = { "sessions.scheduledTasks.editor.scheduleType.daily": "Diário", "sessions.scheduledTasks.editor.scheduleType.weekly": "Semanal", "sessions.scheduledTasks.editor.scheduleType.once": "Uma vez", + "sessions.scheduledTasks.editor.scheduleType.cron": "Cron", "sessions.scheduledTasks.editor.date.label": "Data", "sessions.scheduledTasks.editor.date.placeholder": "Selecionar data", "sessions.scheduledTasks.editor.date.previousMonth": "Mês anterior", @@ -252,6 +253,17 @@ export const dict: Record = { "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.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.actions.closeEsc": "Fechar (Esc)", "multirun.launcher.actions.cancel": "Cancelar", diff --git a/packages/ui/src/lib/i18n/messages/uk.ts b/packages/ui/src/lib/i18n/messages/uk.ts index 4ef8d45e..42c09339 100644 --- a/packages/ui/src/lib/i18n/messages/uk.ts +++ b/packages/ui/src/lib/i18n/messages/uk.ts @@ -214,6 +214,7 @@ export const dict: Record = { "sessions.scheduledTasks.editor.scheduleType.daily": "Щодня", "sessions.scheduledTasks.editor.scheduleType.weekly": "Щотижня", "sessions.scheduledTasks.editor.scheduleType.once": "Одноразовий", + "sessions.scheduledTasks.editor.scheduleType.cron": "Cron", "sessions.scheduledTasks.editor.date.label": "Дата", "sessions.scheduledTasks.editor.date.placeholder": "Вибрати дату", "sessions.scheduledTasks.editor.date.previousMonth": "Попередній місяць", @@ -252,6 +253,17 @@ export const dict: Record = { "sessions.scheduledTasks.editor.validation.atLeastOneTime": "Додайте принаймні один дійсний час", "sessions.scheduledTasks.editor.validation.atLeastOneWeekday": "Виберіть хоча б один день тижня", "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.actions.closeEsc": "Закрити (Esc)", "multirun.launcher.actions.cancel": "Скасувати", diff --git a/packages/ui/src/lib/i18n/messages/zh-CN.ts b/packages/ui/src/lib/i18n/messages/zh-CN.ts index 02729520..1ffd934d 100644 --- a/packages/ui/src/lib/i18n/messages/zh-CN.ts +++ b/packages/ui/src/lib/i18n/messages/zh-CN.ts @@ -214,6 +214,7 @@ export const dict: Record = { 'sessions.scheduledTasks.editor.scheduleType.daily': '每天', 'sessions.scheduledTasks.editor.scheduleType.weekly': '每周', 'sessions.scheduledTasks.editor.scheduleType.once': '一次性', + 'sessions.scheduledTasks.editor.scheduleType.cron': 'Cron', 'sessions.scheduledTasks.editor.date.label': '日期', 'sessions.scheduledTasks.editor.date.placeholder': '选择日期', 'sessions.scheduledTasks.editor.date.previousMonth': '上个月', @@ -252,6 +253,17 @@ export const dict: Record = { 'sessions.scheduledTasks.editor.validation.atLeastOneTime': '请至少添加一个有效时间', 'sessions.scheduledTasks.editor.validation.atLeastOneWeekday': '请至少选择一个星期', '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.actions.closeEsc': '关闭 (Esc)', 'multirun.launcher.actions.cancel': '取消', diff --git a/packages/ui/src/lib/i18n/messages/zh-TW.ts b/packages/ui/src/lib/i18n/messages/zh-TW.ts index d9dd0a14..08d30254 100644 --- a/packages/ui/src/lib/i18n/messages/zh-TW.ts +++ b/packages/ui/src/lib/i18n/messages/zh-TW.ts @@ -227,6 +227,7 @@ export const dict: Record = { 'sessions.scheduledTasks.editor.scheduleType.daily': '每天', 'sessions.scheduledTasks.editor.scheduleType.weekly': '每週', 'sessions.scheduledTasks.editor.scheduleType.once': '一次性', + 'sessions.scheduledTasks.editor.scheduleType.cron': 'Cron', 'sessions.scheduledTasks.editor.date.label': '日期', 'sessions.scheduledTasks.editor.date.placeholder': '選擇日期', 'sessions.scheduledTasks.editor.date.previousMonth': '上個月', @@ -265,6 +266,17 @@ export const dict: Record = { 'sessions.scheduledTasks.editor.validation.atLeastOneTime': '請至少新增一個有效時間', 'sessions.scheduledTasks.editor.validation.atLeastOneWeekday': '請至少選擇一個星期', '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.actions.closeEsc': '關閉 (Esc)', 'multirun.launcher.actions.cancel': '取消',