Files
ontime/apps/client/src/features/rundown/entry-editor/RundownEntryEditor.tsx
T
Claude b981bae63f feat(rundown): edit multiple events at once in the entry editor
Selecting several events in the editor now turns the entry editor into a
multi editor: fields shared by every selected event show their value,
fields which differ show as mixed, and editing a field applies it to the
whole selection through the existing batch endpoint.

Rather than branching between a single and a multi editor, the editor now
always renders a merged view of N events (N >= 1) where a field is
undefined when the events disagree. For a single event nothing is ever
undefined, so single event editing is unchanged by construction and the
composites carry no multi edit branching.

- add mergeEvents() to build the merged view over a selection
- EventEditor takes an events array and resolves the write path itself,
  replacing the eventId threaded into the composites with a submit callback
- extract EventEditorSchedule, which is only rendered for a single event
  since schedule values cascade through the rundown
- support indeterminate values in Switch, Select, SwatchSelect and TimeInput
- cue, event id, schedule and automations are not available in multi edit

Two related fixes which multi edit depends on:
- useReactiveTextInput submitted an empty string when the initial value was
  undefined, so leaving a mixed field without editing it would have wiped
  the value on every selected event
- the batch optimistic update spread the request body onto the entry
  instead of the patch, and replaced custom fields rather than merging them

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PDETGBrhwgAqgTHjmrmJGJ
2026-07-25 12:29:30 +00:00

83 lines
2.3 KiB
TypeScript

import { OntimeEntry, OntimeEvent, isOntimeEvent, isOntimeGroup, isOntimeMilestone } from 'ontime-types';
import { useMemo } from 'react';
import useRundown from '../../../common/hooks-query/useRundown';
import { useEventSelection } from '../useEventSelection';
import EventEditorFooter from './composite/EventEditorFooter';
import EventEditor from './EventEditor';
import EventEditorEmpty from './EventEditorEmpty';
import GroupEditor from './GroupEditor';
import MilestoneEditor from './MilestoneEditor';
import style from './EntryEditor.module.scss';
export default function RundownEntryEditor() {
const selectedEvents = useEventSelection((state) => state.selectedEvents);
const { data } = useRundown();
/**
* Events in the current selection
* Only events can be multi selected, groups and milestones are always selected on their own
*/
const events = useMemo<OntimeEvent[]>(() => {
if (data.order.length === 0) {
return [];
}
const selection: OntimeEvent[] = [];
selectedEvents.forEach((entryId) => {
const entry = data.entries[entryId];
if (isOntimeEvent(entry)) {
selection.push(entry);
}
});
return selection;
}, [data.order.length, data.entries, selectedEvents]);
const entry = useMemo<OntimeEntry | null>(() => {
if (data.order.length === 0) {
return null;
}
const selectedEventId = Array.from(selectedEvents).at(0);
if (!selectedEventId) {
return null;
}
const event = data.entries[selectedEventId];
return event ?? null;
}, [data.order.length, data.entries, selectedEvents]);
if (events.length > 0) {
const singleEvent = events.length === 1 ? events[0] : null;
return (
<div className={style.rundownEditor} data-testid='editor-container'>
<EventEditor events={events} />
{singleEvent && <EventEditorFooter id={singleEvent.id} cue={singleEvent.cue} />}
</div>
);
}
if (!entry) {
return <EventEditorEmpty />;
}
if (isOntimeMilestone(entry)) {
return (
<div className={style.rundownEditor} data-testid='editor-container'>
<MilestoneEditor milestone={entry} />
</div>
);
}
if (isOntimeGroup(entry)) {
return (
<div className={style.rundownEditor} data-testid='editor-container'>
<GroupEditor group={entry} />
</div>
);
}
return null;
}