Files
ersatztv/web/src/schedules/ScheduleForm.tsx
T
timothyandClaude Fable 5 2687d33637 feat(spa): rebuild schedules editor to full mutation parity (#207)
Extract ScheduleScreen from App.tsx into screens/SchedulesScreen.tsx +
schedules/ domain folder (itemRules, pickers, inspector, ScheduleForm).
Draft model with explicit Save (single destructive PUT), Discard, dirty
guard (navigationGuard + beforeunload), schedule CRUD, and all Blazor
item fields/gates/resets. Rewrite api/schedules.ts to the flat DTO +
CRUD + languages/filler-by-kind pickers. Live TopBar Add Schedule via a
window CustomEvent. Screen + nav-guard tests; App.test updated for the
extracted screen.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 00:32:25 +02:00

107 lines
3.7 KiB
TypeScript

import { useState } from 'react';
import { Button, Checkbox, Dialog, Input, Select } from '../components';
import {
createSchedule,
type CreateScheduleRequest,
messageFromScheduleError,
type ProgramSchedule,
updateSchedule
} from '../api';
export interface ScheduleFormProps {
mode: 'create' | 'edit';
schedule: ProgramSchedule | null;
onSaved: (schedule: ProgramSchedule) => void;
onClose: () => void;
}
// Create / edit the six schedule properties. treatCollectionsAsShows is disabled (and the server
// coerces it false) unless keepMultiPartEpisodesTogether is on. Unique-name 422 is surfaced inline.
export function ScheduleForm({ mode, schedule, onSaved, onClose }: ScheduleFormProps) {
const [name, setName] = useState(schedule?.name ?? '');
const [keepMultiPart, setKeepMultiPart] = useState(schedule?.keepMultiPartEpisodesTogether ?? false);
const [treatAsShows, setTreatAsShows] = useState(schedule?.treatCollectionsAsShows ?? false);
const [shuffle, setShuffle] = useState(schedule?.shuffleScheduleItems ?? false);
const [randomStart, setRandomStart] = useState(schedule?.randomStartPoint ?? false);
const [fixedBehavior, setFixedBehavior] = useState<'Strict' | 'Flexible'>(schedule?.fixedStartTimeBehavior ?? 'Strict');
const [error, setError] = useState<string | null>(null);
const [saving, setSaving] = useState(false);
const submit = () => {
const trimmed = name.trim();
if (!trimmed) {
setError('Name is required');
return;
}
if (trimmed.length > 50) {
setError('Name must be 50 characters or fewer');
return;
}
const body: CreateScheduleRequest = {
name: trimmed,
keepMultiPartEpisodesTogether: keepMultiPart,
treatCollectionsAsShows: keepMultiPart ? treatAsShows : false,
shuffleScheduleItems: shuffle,
randomStartPoint: randomStart,
fixedStartTimeBehavior: fixedBehavior
};
setSaving(true);
setError(null);
const request = mode === 'create' ? createSchedule(body) : updateSchedule(schedule!.id, body);
request
.then((saved) => {
setSaving(false);
onSaved(saved);
})
.catch((err: unknown) => {
setSaving(false);
setError(messageFromScheduleError(err, 'Unable to save schedule'));
});
};
return (
<Dialog
open
onClose={onClose}
title={mode === 'create' ? 'Add schedule' : 'Edit schedule'}
footer={
<>
<Button variant="ghost" onClick={onClose} disabled={saving}>Cancel</Button>
<Button onClick={submit} disabled={saving}>{saving ? 'Saving…' : 'Save'}</Button>
</>
}
>
<div className="ctv-schedule-form-stack">
<Input label="Name" value={name} error={error} onChange={(e) => setName(e.target.value)} />
<Checkbox
label="Keep multi-part episodes together"
checked={keepMultiPart}
onChange={(v) => {
setKeepMultiPart(v);
if (!v) {
setTreatAsShows(false);
}
}}
/>
<Checkbox
label="Treat collections as shows"
checked={treatAsShows}
disabled={!keepMultiPart}
onChange={setTreatAsShows}
/>
<Checkbox label="Shuffle schedule items" checked={shuffle} onChange={setShuffle} />
<p className="ctv-field-help">Shuffling disables fixed start times and flood mode.</p>
<Checkbox label="Random start point" checked={randomStart} onChange={setRandomStart} />
<Select
label="Fixed start time behavior"
value={fixedBehavior}
onChange={(e) => setFixedBehavior(e.target.value as 'Strict' | 'Flexible')}
options={['Strict', 'Flexible']}
/>
</div>
</Dialog>
);
}