import { useMemo, useState } from "react"; import type { ScheduledProjectTask, ScheduledTaskSchedule, ScheduledTaskWeekday, } from "@/domain"; import type { ScheduledTaskDefinitionInput } from "@/ports"; import { Button, Field, Input } from "@/shared"; import { useI18n } from "@/i18n"; type Preset = "hourly" | "daily" | "weekly" | "custom"; const DAYS: Array<[ScheduledTaskWeekday, string, string]> = [ ["monday", "Lun", "Mon"], ["tuesday", "Mar", "Tue"], ["wednesday", "Mer", "Wed"], ["thursday", "Jeu", "Thu"], ["friday", "Ven", "Fri"], ["saturday", "Sam", "Sat"], ["sunday", "Dim", "Sun"], ]; const control = "h-9 rounded-md border border-border bg-raised px-3 text-sm text-content outline-none focus:border-primary"; function initialPreset(schedule?: ScheduledTaskSchedule): Preset { if (!schedule) return "daily"; if (schedule.kind === "interval" && schedule.everyMinutes === 60) return "hourly"; if (schedule.kind === "interval") return "custom"; return schedule.kind; } function shellWord(value: string): string { return /^[A-Za-z0-9_./:@%+=,-]+$/.test(value) ? value : `'${value.split("'").join(`'\\''`)}'`; } export function exactCommand(input: ScheduledTaskDefinitionInput): string { return [input.command.command, ...input.command.args].map(shellWord).join(" "); } type Tr = (french: string, english: string) => string; const fr: Tr = (french) => french; export function scheduleLabel(schedule: ScheduledTaskSchedule, tr: Tr = fr): string { if (schedule.kind === "interval") { return schedule.everyMinutes === 60 ? tr("Toutes les heures", "Hourly") : tr(`Toutes les ${schedule.everyMinutes} minutes`, `Every ${schedule.everyMinutes} minutes`); } if (schedule.kind === "daily") return tr(`Chaque jour à ${schedule.time}`, `Every day at ${schedule.time}`); const frLabels = schedule.days.map((day) => DAYS.find(([key]) => key === day)?.[1] ?? day); const enLabels = schedule.days.map((day) => DAYS.find(([key]) => key === day)?.[2] ?? day); return tr(`Chaque semaine (${frLabels.join(", ")}) à ${schedule.time}`, `Every week (${enLabels.join(", ")}) at ${schedule.time}`); } export function ScheduledTaskForm({ task, saving, onCancel, onSubmit, }: { task?: ScheduledProjectTask; saving: boolean; onCancel: () => void; onSubmit: (input: ScheduledTaskDefinitionInput) => void; }) { const { tr } = useI18n(); const [name, setName] = useState(task?.name ?? ""); const [description, setDescription] = useState(task?.description ?? ""); const [command, setCommand] = useState(task?.command.command ?? ""); const [args, setArgs] = useState(task?.command.args.join("\n") ?? ""); const [cwd, setCwd] = useState(task?.command.cwd ?? "."); const [preset, setPreset] = useState(initialPreset(task?.schedule)); const [minutes, setMinutes] = useState( task?.schedule.kind === "interval" ? String(task.schedule.everyMinutes) : "15", ); const [time, setTime] = useState( task?.schedule.kind === "daily" || task?.schedule.kind === "weekly" ? task.schedule.time : "09:00", ); const [days, setDays] = useState( task?.schedule.kind === "weekly" ? task.schedule.days : ["monday"], ); const [timezone, setTimezone] = useState( task?.timezone ?? Intl.DateTimeFormat().resolvedOptions().timeZone ?? "UTC", ); const [timeoutMinutes, setTimeoutMinutes] = useState( task?.timeoutMs ? String(Math.round(task.timeoutMs / 60_000)) : "", ); const [enabled, setEnabled] = useState(task?.approval === "pending" ? true : task?.enabled ?? true); const [review, setReview] = useState(false); const input = useMemo(() => { const schedule: ScheduledTaskSchedule = preset === "hourly" ? { kind: "interval", everyMinutes: 60 } : preset === "custom" ? { kind: "interval", everyMinutes: Number(minutes) } : preset === "weekly" ? { kind: "weekly", days, time } : { kind: "daily", time }; return { name: name.trim(), ...(description.trim() ? { description: description.trim() } : {}), command: { command: command.trim(), args: args.split("\n").map((arg) => arg.trim()).filter(Boolean), ...(cwd.trim() && cwd.trim() !== "." ? { cwd: cwd.trim() } : {}), }, schedule, timezone: timezone.trim(), enabled, ...(timeoutMinutes ? { timeoutMs: Number(timeoutMinutes) * 60_000 } : {}), }; }, [args, command, cwd, days, description, enabled, minutes, name, preset, time, timeoutMinutes, timezone]); const valid = input.name.length > 0 && input.command.command.length > 0 && input.timezone.length > 0 && (input.schedule.kind !== "interval" || input.schedule.everyMinutes >= 5) && (input.schedule.kind !== "weekly" || input.schedule.days.length > 0) && (!timeoutMinutes || Number(timeoutMinutes) > 0); if (review) { return (
{tr("Cette tâche pourra modifier ou supprimer des fichiers automatiquement.", "This task will be able to edit or delete files automatically.")}
{tr("Commande exacte", "Exact command")}
{exactCommand(input)}
{tr("Dossier", "Directory")}
{input.command.cwd ?? "."} ({tr("relatif au projet", "relative to project")})
{tr("Fréquence", "Frequency")}
{scheduleLabel(input.schedule, tr)}
{tr("Prochaine exécution", "Next run")}
{tr("Calculée par IdeA lors de l’activation", "Calculated by IdeA when enabled")}
{tr("Fuseau horaire", "Time zone")}
{input.timezone}
{tr("Permissions", "Permissions")}
{tr("Lecture, modification et suppression dans le projet", "Read, edit, and delete within the project")}
{tr("Bac à sable", "Sandbox")}
{tr("Projet — lecture/écriture ; aucun accès en écriture hors projet", "Project — read/write; no write access outside the project")}

{tr("IdeA doit fonctionner et le projet doit être ouvert pour déclencher cette tâche.", "IdeA must be running and the project must be open to trigger this task.")}

); } return (
{ event.preventDefault(); enabled ? setReview(true) : onSubmit(input); }}>
{({ id, describedBy }) => setName(e.target.value)} />} {({ id, describedBy }) => setCwd(e.target.value)} />}
{({ id, describedBy }) =>