import { useMemo } from 'react'; import type { CreateScheduledTaskInput, DashboardTask, DashboardTaskAction, DashboardTaskContextMode, DashboardTaskScheduleType, UpdateScheduledTaskInput, } from './api'; import { EmptyState } from './EmptyState'; import { localeTags, type Locale, type Messages } from './i18n'; import { redactSecretsForPreview } from './redaction'; import { statusLabel, taskActionsFor } from './dashboardHelpers'; type TaskGroupKey = 'watchers' | 'scheduled' | 'paused' | 'completed'; type TaskResultTone = 'ok' | 'fail' | 'none'; export type TaskActionKey = | 'create' | `${string}:edit` | `${string}:${DashboardTaskAction}`; export interface RoomOption { jid: string; name: string; folder: string; } export interface TaskPanelProps { tasks: DashboardTask[]; rooms: RoomOption[]; locale: Locale; onTaskAction: (task: DashboardTask, action: DashboardTaskAction) => void; onTaskCreate: (input: CreateScheduledTaskInput) => void; onTaskUpdate: (task: DashboardTask, input: UpdateScheduledTaskInput) => void; taskActionKey: TaskActionKey | null; t: Messages; } interface TaskGroup { key: TaskGroupKey; tasks: DashboardTask[]; } interface TaskCreateFormProps { rooms: RoomOption[]; onTaskCreate: (input: CreateScheduledTaskInput) => void; taskActionKey: TaskActionKey | null; t: Messages; } interface TaskGroupSectionProps { group: TaskGroup; locale: Locale; onTaskAction: (task: DashboardTask, action: DashboardTaskAction) => void; onTaskUpdate: (task: DashboardTask, input: UpdateScheduledTaskInput) => void; taskActionKey: TaskActionKey | null; t: Messages; } interface TaskCardProps { groupKey: TaskGroupKey; locale: Locale; onTaskAction: (task: DashboardTask, action: DashboardTaskAction) => void; onTaskUpdate: (task: DashboardTask, input: UpdateScheduledTaskInput) => void; task: DashboardTask; taskActionKey: TaskActionKey | null; t: Messages; } interface TaskActionButtonsProps { onTaskAction: (task: DashboardTask, action: DashboardTaskAction) => void; task: DashboardTask; taskActionKey: TaskActionKey | null; t: Messages; } interface TaskDateProps { locale: Locale; t: Messages; task: DashboardTask; } interface TaskEditFormProps { onTaskUpdate: (task: DashboardTask, input: UpdateScheduledTaskInput) => void; task: DashboardTask; taskActionKey: TaskActionKey | null; t: Messages; } function formatTaskDate( value: string | null | undefined, locale: Locale, ): string { if (!value) return '-'; const date = new Date(value); if (Number.isNaN(date.getTime())) return value; const time = new Intl.DateTimeFormat(localeTags[locale], { hour: '2-digit', minute: '2-digit', hour12: false, }).format(date); if (locale === 'ko') return `${date.getMonth() + 1}월 ${date.getDate()}일 ${time}`; if (locale === 'ja' || locale === 'zh') return `${date.getMonth() + 1}月${date.getDate()}日 ${time}`; return new Intl.DateTimeFormat(localeTags[locale], { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', hour12: false, }).format(date); } function formatRelativeDate( value: string | null | undefined, locale: Locale, t: Messages, ): string { if (!value) return t.tasks.noTime; const date = new Date(value); if (Number.isNaN(date.getTime())) return value; const diffMs = date.getTime() - Date.now(); const absMs = Math.abs(diffMs); if (absMs < 45_000) return t.tasks.now; const units: Array<[Intl.RelativeTimeFormatUnit, number]> = [ ['day', 86_400_000], ['hour', 3_600_000], ['minute', 60_000], ]; const [unit, unitMs] = units.find(([, threshold]) => absMs >= threshold) ?? units.at(-1)!; return new Intl.RelativeTimeFormat(localeTags[locale], { numeric: 'auto', style: 'short', }).format(Math.round(diffMs / unitMs), unit); } function safePreview( value: string | null | undefined, fallback: string, ): string { const cleaned = redactSecretsForPreview(value ?? '') .replace(/<\/?internal[^>]*>/gi, '') .replace(/\s+/g, ' ') .trim(); if (!cleaned) return fallback; return cleaned.length > 120 ? `${cleaned.slice(0, 120)}...` : cleaned; } function taskGroupKey(task: DashboardTask): TaskGroupKey { if (task.status === 'completed') return 'completed'; if (task.status === 'paused') return 'paused'; if (task.isWatcher) return 'watchers'; return 'scheduled'; } function taskResultTone(task: DashboardTask): TaskResultTone { if (!task.lastResult) return 'none'; const normalized = task.lastResult.toLowerCase(); if ( normalized.includes('fail') || normalized.includes('error') || normalized.includes('timeout') || normalized.includes('cancel') || normalized.includes('reject') ) { return 'fail'; } return 'ok'; } function taskDisplayName(task: DashboardTask, t: Messages): string { if (task.isWatcher) return t.tasks.ciWatch; if (task.scheduleType) return task.scheduleType; return task.id; } function isTaskScheduleType( value: FormDataEntryValue | null, ): value is DashboardTaskScheduleType { return value === 'cron' || value === 'interval' || value === 'once'; } function isTaskContextMode( value: FormDataEntryValue | null, ): value is DashboardTaskContextMode { return value === 'group' || value === 'isolated'; } function readRequiredText(form: FormData, name: string): string | null { const value = form.get(name); if (typeof value !== 'string') return null; const trimmed = value.trim(); return trimmed ? trimmed : null; } function readTaskForm( form: FormData, includeRoom: true, ): CreateScheduledTaskInput | null; function readTaskForm( form: FormData, includeRoom: false, ): UpdateScheduledTaskInput | null; function readTaskForm( form: FormData, includeRoom: boolean, ): CreateScheduledTaskInput | UpdateScheduledTaskInput | null { const prompt = readRequiredText(form, 'prompt'); const scheduleValue = readRequiredText(form, 'scheduleValue'); const scheduleTypeValue = form.get('scheduleType'); if (!scheduleValue || !isTaskScheduleType(scheduleTypeValue)) { return null; } const scheduleType = scheduleTypeValue; if (!includeRoom) { return prompt ? { prompt, scheduleType, scheduleValue } : { scheduleType, scheduleValue }; } if (!prompt) { return null; } const roomJid = readRequiredText(form, 'roomJid'); const contextMode = form.get('contextMode'); if (!roomJid || !isTaskContextMode(contextMode)) return null; return { contextMode, prompt, roomJid, scheduleType, scheduleValue, }; } function buildTaskGroups(tasks: DashboardTask[]): TaskGroup[] { const groups: Record = { watchers: [], scheduled: [], paused: [], completed: [], }; for (const task of tasks) { groups[taskGroupKey(task)].push(task); } for (const groupTasks of Object.values(groups)) { groupTasks.sort((a, b) => (a.nextRun ?? a.lastRun ?? a.createdAt).localeCompare( b.nextRun ?? b.lastRun ?? b.createdAt, ), ); } return [ { key: 'watchers', tasks: groups.watchers }, { key: 'scheduled', tasks: groups.scheduled }, { key: 'paused', tasks: groups.paused }, { key: 'completed', tasks: groups.completed }, ]; } function TaskCreateForm({ rooms, onTaskCreate, taskActionKey, t, }: TaskCreateFormProps) { return (
{ event.preventDefault(); const form = new FormData(event.currentTarget); const input = readTaskForm(form, true); if (!input) return; onTaskCreate(input); event.currentTarget.reset(); }} >
{t.tasks.createTitle} {t.tasks.createSubtitle}