fix(web): schedule editor review fixes for #86
- Add a schedule selector (previously only the first schedule was ever shown); switching loads items only, with a pane-level spinner - Fix duplicated mount effect that fetched the whole screen twice - Add a collection picker to the add-item flow (was silently using the alphabetically-first collection with no way to correct it) - Reorder now reconciles from the PUT response; add/delete refetch items only — pickers are no longer refetched on every mutation - Guard reorder against double-submit via a ref; remove the inert Save button; document the widened item type (OpenAPI gap → #126) - Inspector fidelity: Tail filler row + Keep multi-part together - a11y: aria-current for the active item, draggable role description - CSS: correct --text-md fallbacks; replace nonexistent --text-faint with --text-disabled in new blocks - Tests: single-fetch-on-mount assertion, chosen-collection POST body, picker-failure path, collection() fixture with all required fields; drop unreachable mock branch (52 tests) Review: PR #125 findings; refs #126 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+74
-17
@@ -677,13 +677,55 @@ describe('ChicoryTV SPA scaffold', () => {
|
||||
expect(screen.getAllByText('Station IDs').length).toBeGreaterThan(0);
|
||||
expect(screen.getByText('unknown')).toBeInTheDocument();
|
||||
expect(screen.getByRole('tab', { name: 'Content' })).toHaveAttribute('aria-selected', 'true');
|
||||
expect(screen.getByDisplayValue('Prime Time Cartoons')).toBeInTheDocument();
|
||||
expect(screen.getAllByDisplayValue('Prime Time Cartoons').length).toBeGreaterThan(0);
|
||||
expect(window.fetch).toHaveBeenCalledWith('/api/schedules', expect.any(Object));
|
||||
expect(window.fetch).toHaveBeenCalledWith('/api/schedules/5/items', expect.any(Object));
|
||||
expect(window.fetch).toHaveBeenCalledWith('/api/collections', expect.any(Object));
|
||||
expect(window.fetch).not.toHaveBeenCalledWith('/api/languages', expect.any(Object));
|
||||
});
|
||||
|
||||
it('fetches the schedules screen data exactly once on mount', async () => {
|
||||
mockDashboardApi({ schedules: [schedule({ id: 5, name: 'Prime Time Cartoons' })] });
|
||||
|
||||
render(<App />);
|
||||
|
||||
fireEvent.click(screen.getByRole('link', { name: 'Schedules' }));
|
||||
|
||||
expect(await screen.findByRole('heading', { name: 'Prime Time Cartoons' })).toBeInTheDocument();
|
||||
expect(fetchCount('/api/schedules')).toBe(1);
|
||||
});
|
||||
|
||||
it('shows the Schedule editor error state when a picker endpoint fails', async () => {
|
||||
vi.spyOn(window, 'fetch').mockImplementation((input: RequestInfo | URL) => {
|
||||
const path = input.toString();
|
||||
|
||||
if (path === '/api/schedules') {
|
||||
return Promise.resolve(jsonResponse([schedule({ id: 5, name: 'Prime Time Cartoons' })]));
|
||||
}
|
||||
|
||||
if (path === '/api/schedules/5/items') {
|
||||
return Promise.resolve(jsonResponse({ items: [], totalDurationEstimate: null }));
|
||||
}
|
||||
|
||||
if (path === '/api/collections') {
|
||||
return Promise.resolve(jsonResponse({
|
||||
detail: 'Collections service is unavailable',
|
||||
status: 500,
|
||||
title: 'Internal error'
|
||||
}, 500));
|
||||
}
|
||||
|
||||
return Promise.resolve(jsonResponse([]));
|
||||
});
|
||||
|
||||
render(<App />);
|
||||
|
||||
fireEvent.click(screen.getByRole('link', { name: 'Schedules' }));
|
||||
|
||||
expect(await screen.findByText('Collections service is unavailable')).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: 'Retry' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows the Schedule editor loading state', async () => {
|
||||
vi.spyOn(window, 'fetch').mockImplementation((input: RequestInfo | URL) => {
|
||||
const path = input.toString();
|
||||
@@ -767,6 +809,8 @@ describe('ChicoryTV SPA scaffold', () => {
|
||||
fireEvent.click(screen.getByRole('link', { name: 'Schedules' }));
|
||||
expect(await screen.findAllByText('Saturday Cartoons')).not.toHaveLength(0);
|
||||
|
||||
const collectionsFetchesBeforeReorder = fetchCount('/api/collections');
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Move Station IDs up' }));
|
||||
|
||||
await waitFor(() => {
|
||||
@@ -779,6 +823,7 @@ describe('ChicoryTV SPA scaffold', () => {
|
||||
const rows = within(lineup).getAllByRole('listitem');
|
||||
expect(rows[0]).toHaveTextContent('Station IDs');
|
||||
expect(rows[1]).toHaveTextContent('Saturday Cartoons');
|
||||
expect(fetchCount('/api/collections')).toBe(collectionsFetchesBeforeReorder);
|
||||
});
|
||||
|
||||
it('rolls back schedule reorder and shows ProblemDetails on replace failure', async () => {
|
||||
@@ -809,11 +854,14 @@ describe('ChicoryTV SPA scaffold', () => {
|
||||
expect(within(lineup).getAllByRole('listitem')[0]).toHaveTextContent('Saturday Cartoons');
|
||||
});
|
||||
|
||||
it('adds schedule items and refetches after success', async () => {
|
||||
const added = scheduleItem({ collection: { id: 3, name: 'Movie Mix' }, id: 20, name: 'Movie Mix' });
|
||||
it('adds schedule items using the selected (non-first) collection', async () => {
|
||||
const added = scheduleItem({ collection: { id: 4, name: 'Nature Docs' }, id: 20, name: 'Nature Docs' });
|
||||
mockDashboardApi({
|
||||
addScheduleItemResponse: added,
|
||||
collections: [{ id: 3, name: 'Movie Mix' }],
|
||||
collections: [
|
||||
collection({ id: 3, name: 'Movie Mix' }),
|
||||
collection({ id: 4, name: 'Nature Docs' })
|
||||
],
|
||||
scheduleItems: [],
|
||||
scheduleItemsAfterAdd: [added],
|
||||
schedules: [schedule({ id: 5, name: 'Prime Time Cartoons' })]
|
||||
@@ -824,19 +872,28 @@ describe('ChicoryTV SPA scaffold', () => {
|
||||
fireEvent.click(screen.getByRole('link', { name: 'Schedules' }));
|
||||
expect(await screen.findByText('No schedule items')).toBeInTheDocument();
|
||||
|
||||
const collectionsFetchesBeforeAdd = fetchCount('/api/collections');
|
||||
|
||||
fireEvent.change(screen.getByRole('combobox', { name: 'Collection for new item' }), {
|
||||
target: { value: '4' }
|
||||
});
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Add item' }));
|
||||
|
||||
expect(await screen.findAllByText('Movie Mix')).not.toHaveLength(0);
|
||||
expect(await screen.findAllByText('Nature Docs')).not.toHaveLength(0);
|
||||
const expectedBody = scheduleItemRequest(added);
|
||||
expect(expectedBody.collectionId).toBe(4);
|
||||
expect(expectedBody.collectionType).toBe('Collection');
|
||||
expect(window.fetch).toHaveBeenCalledWith('/api/schedules/5/items', expect.objectContaining({
|
||||
body: JSON.stringify(scheduleItemRequest(added)),
|
||||
body: JSON.stringify(expectedBody),
|
||||
method: 'POST'
|
||||
}));
|
||||
expect(fetchCount('/api/schedules/5/items')).toBeGreaterThan(2);
|
||||
expect(fetchCount('/api/collections')).toBe(collectionsFetchesBeforeAdd);
|
||||
});
|
||||
|
||||
it('shows ProblemDetails when adding a schedule item fails', async () => {
|
||||
mockDashboardApi({
|
||||
collections: [{ id: 3, name: 'Movie Mix' }],
|
||||
collections: [collection({ id: 3, name: 'Movie Mix' })],
|
||||
mutationFailures: {
|
||||
'/api/schedules/5/items': {
|
||||
detail: 'Collection is required',
|
||||
@@ -1070,6 +1127,16 @@ function schedule(overrides: Record<string, unknown> = {}): Record<string, unkno
|
||||
};
|
||||
}
|
||||
|
||||
function collection(overrides: Record<string, unknown> = {}): Record<string, unknown> {
|
||||
return {
|
||||
collectionType: 'Collection',
|
||||
id: 2,
|
||||
name: 'Saturday Cartoons',
|
||||
useCustomPlaybackOrder: false,
|
||||
...overrides
|
||||
};
|
||||
}
|
||||
|
||||
function scheduleItem(overrides: Record<string, unknown> = {}): Record<string, unknown> {
|
||||
const collection = { id: 2, name: 'Saturday Cartoons', ...(overrides.collection as object | undefined) };
|
||||
|
||||
@@ -1239,16 +1306,6 @@ function mockDashboardApi({
|
||||
return Promise.resolve(jsonResponse(schedules));
|
||||
}
|
||||
|
||||
if (path.match(/^\/api\/schedules\/\d+$/)) {
|
||||
const id = Number(path.split('/').at(-1));
|
||||
const found = schedules.find((item) => (item as { id?: number }).id === id);
|
||||
return Promise.resolve(found ? jsonResponse(found) : jsonResponse({
|
||||
detail: `Schedule ${id} was not found`,
|
||||
status: 404,
|
||||
title: 'Not found'
|
||||
}, 404));
|
||||
}
|
||||
|
||||
if (path.match(/^\/api\/schedules\/\d+\/items$/)) {
|
||||
const method = init?.method ?? 'GET';
|
||||
|
||||
|
||||
+51
-16
@@ -1228,11 +1228,13 @@ function SchedulesEmptyState() {
|
||||
function ScheduleScreen() {
|
||||
const query = useScheduleScreenQuery();
|
||||
const [selectedItemId, setSelectedItemId] = useState<number | null>(null);
|
||||
const [selectedCollectionId, setSelectedCollectionId] = useState<number | null>(null);
|
||||
const [dragItemId, setDragItemId] = useState<number | null>(null);
|
||||
const [overItemId, setOverItemId] = useState<number | null>(null);
|
||||
const [inspectorTab, setInspectorTab] = useState<ScheduleInspectorTab>('content');
|
||||
const [mutationError, setMutationError] = useState<string | null>(null);
|
||||
const [mutating, setMutating] = useState(false);
|
||||
const mutatingRef = useRef(false);
|
||||
|
||||
if (query.status === 'loading') {
|
||||
return <SchedulesLoadingState />;
|
||||
@@ -1243,6 +1245,7 @@ function ScheduleScreen() {
|
||||
}
|
||||
|
||||
const { activeSchedule, items, pickers, schedules, totalDurationEstimate } = query.data;
|
||||
const { itemsLoading, setActiveSchedule, setItems } = query;
|
||||
|
||||
if (!activeSchedule) {
|
||||
return <SchedulesEmptyState />;
|
||||
@@ -1252,18 +1255,27 @@ function ScheduleScreen() {
|
||||
const selectedItem = orderedItems.find((item) => item.id === selectedItemId) ?? orderedItems[0] ?? null;
|
||||
const effectiveSelectedItemId = selectedItem?.id ?? null;
|
||||
const selectedIndex = selectedItem ? orderedItems.findIndex((item) => item.id === selectedItem.id) : -1;
|
||||
const collectionOptions = sortedCollections(pickers.collections);
|
||||
const effectiveCollectionId = collectionOptions.some((collection) => collection.id === selectedCollectionId)
|
||||
? selectedCollectionId
|
||||
: collectionOptions[0]?.id ?? null;
|
||||
|
||||
const refreshAfterMutation = async (operation: () => Promise<void>) => {
|
||||
const setMutatingState = (value: boolean) => {
|
||||
mutatingRef.current = value;
|
||||
setMutating(value);
|
||||
};
|
||||
|
||||
const refetchItemsAfterMutation = async (operation: () => Promise<void>) => {
|
||||
setMutationError(null);
|
||||
setMutating(true);
|
||||
setMutatingState(true);
|
||||
|
||||
try {
|
||||
await operation();
|
||||
query.refresh();
|
||||
setActiveSchedule(activeSchedule.id);
|
||||
} catch (error: unknown) {
|
||||
setMutationError(messageFromError(error));
|
||||
} finally {
|
||||
setMutating(false);
|
||||
setMutatingState(false);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1273,22 +1285,22 @@ function ScheduleScreen() {
|
||||
|
||||
try {
|
||||
setMutationError(null);
|
||||
setMutating(true);
|
||||
setMutatingState(true);
|
||||
const result = await replaceScheduleItems(activeSchedule.id, {
|
||||
items: nextItems.map(scheduleItemToRequest)
|
||||
});
|
||||
setSelectedItemId(previousSelectedId ?? result[0]?.id ?? null);
|
||||
query.refresh();
|
||||
setItems(result);
|
||||
} catch (error: unknown) {
|
||||
setSelectedItemId(previousSelectedId);
|
||||
setMutationError(messageFromError(error));
|
||||
} finally {
|
||||
setMutating(false);
|
||||
setMutatingState(false);
|
||||
}
|
||||
};
|
||||
|
||||
const moveItem = (itemId: number, direction: -1 | 1) => {
|
||||
if (mutating) {
|
||||
if (mutatingRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1306,7 +1318,7 @@ function ScheduleScreen() {
|
||||
};
|
||||
|
||||
const dropItem = (targetId: number) => {
|
||||
if (dragItemId == null || dragItemId === targetId || mutating) {
|
||||
if (dragItemId == null || dragItemId === targetId || mutatingRef.current) {
|
||||
setDragItemId(null);
|
||||
setOverItemId(null);
|
||||
return;
|
||||
@@ -1327,14 +1339,14 @@ function ScheduleScreen() {
|
||||
};
|
||||
|
||||
const addItem = () => {
|
||||
const collection = firstCollection(pickers.collections);
|
||||
const collection = collectionOptions.find((candidate) => candidate.id === effectiveCollectionId);
|
||||
|
||||
if (!collection) {
|
||||
setMutationError('A collection is required before adding a schedule item');
|
||||
return;
|
||||
}
|
||||
|
||||
void refreshAfterMutation(async () => {
|
||||
void refetchItemsAfterMutation(async () => {
|
||||
const added = await addScheduleItem(activeSchedule.id, newScheduleItemRequest(collection));
|
||||
setSelectedItemId(added.id ?? null);
|
||||
});
|
||||
@@ -1347,7 +1359,7 @@ function ScheduleScreen() {
|
||||
return;
|
||||
}
|
||||
|
||||
void refreshAfterMutation(async () => {
|
||||
void refetchItemsAfterMutation(async () => {
|
||||
await deleteScheduleItem(activeSchedule.id, itemId);
|
||||
setSelectedItemId(null);
|
||||
});
|
||||
@@ -1363,8 +1375,15 @@ function ScheduleScreen() {
|
||||
{schedules.length} schedule{schedules.length === 1 ? '' : 's'} · {orderedItems.length} item{orderedItems.length === 1 ? '' : 's'} · programs <code>{totalDurationEstimate ?? 'unknown'}</code>
|
||||
</p>
|
||||
</div>
|
||||
<Select
|
||||
disabled={mutating}
|
||||
label="Active schedule"
|
||||
onChange={(event) => setActiveSchedule(Number(event.target.value))}
|
||||
options={scheduleOptions(schedules)}
|
||||
size="sm"
|
||||
value={`${activeSchedule.id}`}
|
||||
/>
|
||||
<Button disabled startIcon={<Play aria-hidden="true" size={15} />} variant="secondary">Preview playout</Button>
|
||||
<Button disabled={mutating} startIcon={<Check aria-hidden="true" size={15} />}>Save</Button>
|
||||
</section>
|
||||
|
||||
{mutationError && (
|
||||
@@ -1378,6 +1397,15 @@ function ScheduleScreen() {
|
||||
<section className="ctv-schedule-lineup-panel" aria-label="Lineup">
|
||||
<div className="ctv-schedule-panel-head">
|
||||
<span>Lineup · drag to reorder</span>
|
||||
{itemsLoading && <Spinner size={13} tone="muted" />}
|
||||
<Select
|
||||
disabled={mutating || collectionOptions.length === 0}
|
||||
label="Collection for new item"
|
||||
onChange={(event) => setSelectedCollectionId(Number(event.target.value))}
|
||||
options={pickerOptions(collectionOptions)}
|
||||
size="sm"
|
||||
value={`${effectiveCollectionId ?? ''}`}
|
||||
/>
|
||||
<Button disabled={mutating} onClick={addItem} size="sm" startIcon={<Plus aria-hidden="true" size={14} />} variant="ghost">Add item</Button>
|
||||
</div>
|
||||
|
||||
@@ -1469,6 +1497,7 @@ function ScheduleItemBlock({
|
||||
<code>{fixed ? formatScheduleTime(item.startTime) : 'flows'}</code>
|
||||
</div>
|
||||
<div
|
||||
aria-roledescription="draggable schedule item"
|
||||
className={`ctv-schedule-block${active ? ' ctv-schedule-block-active' : ''}${dragging ? ' ctv-schedule-block-dragging' : ''}${isOver ? ' ctv-schedule-block-over' : ''}`}
|
||||
draggable={!mutating}
|
||||
onDragEnd={() => {
|
||||
@@ -1486,7 +1515,7 @@ function ScheduleItemBlock({
|
||||
<button
|
||||
type="button"
|
||||
className="ctv-schedule-block-main"
|
||||
aria-pressed={active}
|
||||
aria-current={active ? 'true' : undefined}
|
||||
onClick={() => onSelect(itemId)}
|
||||
>
|
||||
<GripVertical aria-hidden="true" size={16} />
|
||||
@@ -1597,6 +1626,7 @@ function ScheduleInspector({
|
||||
<Select disabled label="Guide mode" value={item.guideMode ?? 'Normal'} options={['Normal', 'Filler']} />
|
||||
<Input disabled label="Schedule" value={schedule.name ?? 'Unnamed schedule'} />
|
||||
</div>
|
||||
<Checkbox disabled checked={schedule.keepMultiPartEpisodesTogether} label="Keep multi-part together" />
|
||||
</>
|
||||
)}
|
||||
{tab === 'playback' && (
|
||||
@@ -1619,6 +1649,7 @@ function ScheduleInspector({
|
||||
<Select disabled label="Mid-roll" value={item.midRollFiller?.name ?? 'None'} options={fillerOptions(pickers.fillerPresets)} />
|
||||
<Select disabled label="Post-roll" value={item.postRollFiller?.name ?? 'None'} options={fillerOptions(pickers.fillerPresets)} />
|
||||
<Select disabled label="Fallback" value={item.fallbackFiller?.name ?? 'None'} options={fillerOptions(pickers.fillerPresets)} />
|
||||
<Select disabled label="Tail" value={item.tailFiller?.name ?? 'None'} options={fillerOptions(pickers.fillerPresets)} />
|
||||
</div>
|
||||
)}
|
||||
{tab === 'overrides' && (
|
||||
@@ -1730,8 +1761,12 @@ function collectionIdForItem(item: ProgramScheduleItem): number | null {
|
||||
return item.collection?.id ?? item.multiCollection?.id ?? item.smartCollection?.id ?? null;
|
||||
}
|
||||
|
||||
function firstCollection(collections: MediaCollection[]): MediaCollection | null {
|
||||
return [...collections].sort((left, right) => (left.name ?? '').localeCompare(right.name ?? ''))[0] ?? null;
|
||||
function sortedCollections(collections: MediaCollection[]): MediaCollection[] {
|
||||
return [...collections].sort((left, right) => (left.name ?? '').localeCompare(right.name ?? ''));
|
||||
}
|
||||
|
||||
function scheduleOptions(schedules: ProgramSchedule[]): Array<{ label: string; value: string }> {
|
||||
return schedules.map((schedule) => ({ label: schedule.name ?? `Schedule ${schedule.id}`, value: `${schedule.id}` }));
|
||||
}
|
||||
|
||||
function scheduleFillDescriptor(item: ProgramScheduleItem): { icon: ReactNode; label: string } {
|
||||
|
||||
+102
-20
@@ -3,6 +3,11 @@ import { ApiError, request } from './client';
|
||||
import type { components } from './generated/v1';
|
||||
|
||||
export type ProgramSchedule = components['schemas']['ProgramScheduleViewModel'];
|
||||
// These fields are real on the wire (the backend serializes ProgramScheduleItemViewModel
|
||||
// subtypes with Newtonsoft using the runtime type), but the OpenAPI schema doesn't declare
|
||||
// them: ProgramScheduleItemViewModel is an abstract base with no polymorphism annotation, so
|
||||
// the generator only sees the base shape. Tracked in Gitea issue #126 — remove this widening
|
||||
// once the schema is fixed to describe the concrete subtypes.
|
||||
export type ProgramScheduleItem = components['schemas']['ProgramScheduleItemViewModel'] & {
|
||||
count?: null | string;
|
||||
discardToFillAttempts?: null | number;
|
||||
@@ -35,12 +40,20 @@ export interface ScheduleScreenData {
|
||||
}
|
||||
|
||||
export type ScheduleScreenQueryState =
|
||||
| { data: ScheduleScreenData; error: null; refresh: () => void; status: 'success' }
|
||||
| {
|
||||
data: ScheduleScreenData;
|
||||
error: null;
|
||||
itemsLoading: boolean;
|
||||
refresh: () => void;
|
||||
setActiveSchedule: (scheduleId: number) => void;
|
||||
setItems: (items: ProgramScheduleItem[]) => void;
|
||||
status: 'success';
|
||||
}
|
||||
| { data: null; error: string; refresh: () => void; status: 'error' }
|
||||
| { data: null; error: null; refresh: () => void; status: 'loading' };
|
||||
|
||||
type ScheduleScreenState =
|
||||
| { data: ScheduleScreenData; error: null; status: 'success' }
|
||||
| { data: ScheduleScreenData; error: null; itemsLoading: boolean; status: 'success' }
|
||||
| { data: null; error: string; status: 'error' }
|
||||
| { data: null; error: null; status: 'loading' };
|
||||
|
||||
@@ -126,6 +139,7 @@ export function useScheduleScreenQuery(): ScheduleScreenQueryState {
|
||||
status: 'loading'
|
||||
});
|
||||
const activeRef = useRef(true);
|
||||
const activeScheduleIdRef = useRef<number | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
activeRef.current = true;
|
||||
@@ -136,12 +150,11 @@ export function useScheduleScreenQuery(): ScheduleScreenQueryState {
|
||||
}, []);
|
||||
|
||||
const load = useCallback(() => {
|
||||
setState({ data: null, error: null, status: 'loading' });
|
||||
|
||||
getScheduleScreenData()
|
||||
.then((data) => {
|
||||
if (activeRef.current) {
|
||||
setState({ data, error: null, status: 'success' });
|
||||
activeScheduleIdRef.current = data.activeSchedule?.id ?? null;
|
||||
setState({ data, error: null, itemsLoading: false, status: 'success' });
|
||||
}
|
||||
})
|
||||
.catch((error: unknown) => {
|
||||
@@ -152,25 +165,94 @@ export function useScheduleScreenQuery(): ScheduleScreenQueryState {
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
getScheduleScreenData()
|
||||
.then((data) => {
|
||||
if (activeRef.current) {
|
||||
setState({ data, error: null, status: 'success' });
|
||||
}
|
||||
})
|
||||
.catch((error: unknown) => {
|
||||
if (activeRef.current) {
|
||||
setState({ data: null, error: messageFromScheduleError(error, 'Unable to load schedules'), status: 'error' });
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
const refresh = useCallback(() => {
|
||||
load();
|
||||
}, [load]);
|
||||
|
||||
const refresh = useCallback(() => {
|
||||
setState({ data: null, error: null, status: 'loading' });
|
||||
load();
|
||||
}, [load]);
|
||||
|
||||
const setActiveSchedule = useCallback((scheduleId: number) => {
|
||||
activeScheduleIdRef.current = scheduleId;
|
||||
|
||||
setState((current) => {
|
||||
if (current.status !== 'success') {
|
||||
return current;
|
||||
}
|
||||
|
||||
const nextActiveSchedule = current.data.schedules.find((schedule) => schedule.id === scheduleId)
|
||||
?? current.data.activeSchedule;
|
||||
|
||||
return {
|
||||
data: { ...current.data, activeSchedule: nextActiveSchedule },
|
||||
error: null,
|
||||
itemsLoading: true,
|
||||
status: 'success'
|
||||
};
|
||||
});
|
||||
|
||||
getScheduleItems(scheduleId)
|
||||
.then((itemsEnvelope) => {
|
||||
if (!activeRef.current || activeScheduleIdRef.current !== scheduleId) {
|
||||
return;
|
||||
}
|
||||
|
||||
setState((current) => {
|
||||
if (current.status !== 'success') {
|
||||
return current;
|
||||
}
|
||||
|
||||
return {
|
||||
data: {
|
||||
...current.data,
|
||||
items: (itemsEnvelope.items ?? []) as ProgramScheduleItem[],
|
||||
totalDurationEstimate: itemsEnvelope.totalDurationEstimate
|
||||
},
|
||||
error: null,
|
||||
itemsLoading: false,
|
||||
status: 'success'
|
||||
};
|
||||
});
|
||||
})
|
||||
.catch((error: unknown) => {
|
||||
if (!activeRef.current || activeScheduleIdRef.current !== scheduleId) {
|
||||
return;
|
||||
}
|
||||
|
||||
setState({
|
||||
data: null,
|
||||
error: messageFromScheduleError(error, 'Unable to load schedule items'),
|
||||
status: 'error'
|
||||
});
|
||||
});
|
||||
}, []);
|
||||
|
||||
const setItems = useCallback((items: ProgramScheduleItem[]) => {
|
||||
setState((current) => {
|
||||
if (current.status !== 'success') {
|
||||
return current;
|
||||
}
|
||||
|
||||
return {
|
||||
data: { ...current.data, items },
|
||||
error: null,
|
||||
itemsLoading: false,
|
||||
status: 'success'
|
||||
};
|
||||
});
|
||||
}, []);
|
||||
|
||||
if (state.status === 'success') {
|
||||
return { data: state.data, error: null, refresh, status: 'success' };
|
||||
return {
|
||||
data: state.data,
|
||||
error: null,
|
||||
itemsLoading: state.itemsLoading,
|
||||
refresh,
|
||||
setActiveSchedule,
|
||||
setItems,
|
||||
status: 'success'
|
||||
};
|
||||
}
|
||||
|
||||
if (state.status === 'error') {
|
||||
|
||||
+4
-4
@@ -1022,7 +1022,7 @@
|
||||
.ctv-schedule-inspector-head strong {
|
||||
margin: 0;
|
||||
color: var(--text-primary);
|
||||
font-size: var(--text-md, 15px);
|
||||
font-size: var(--text-md, 14px);
|
||||
font-weight: var(--weight-semibold, 600);
|
||||
line-height: 1.2;
|
||||
}
|
||||
@@ -1100,7 +1100,7 @@
|
||||
align-items: center;
|
||||
gap: var(--space-2, 4px);
|
||||
padding-top: var(--space-7, 16px);
|
||||
color: var(--text-faint);
|
||||
color: var(--text-disabled);
|
||||
font-size: 9px;
|
||||
}
|
||||
|
||||
@@ -1160,7 +1160,7 @@
|
||||
}
|
||||
|
||||
.ctv-schedule-block-main > svg {
|
||||
color: var(--text-faint);
|
||||
color: var(--text-disabled);
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
@@ -1173,7 +1173,7 @@
|
||||
.ctv-schedule-block-title strong {
|
||||
overflow: hidden;
|
||||
color: var(--text-primary);
|
||||
font-size: var(--text-md, 15px);
|
||||
font-size: var(--text-md, 14px);
|
||||
font-weight: var(--weight-semibold, 600);
|
||||
line-height: 1.2;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
Reference in New Issue
Block a user