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>
107 lines
3.7 KiB
TypeScript
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>
|
|
);
|
|
}
|