Files
ontime/e2e/tests/features/215-multi-edit.spec.ts
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

63 lines
3.0 KiB
TypeScript

import { expect, test } from '@playwright/test';
test('Editing multiple events', async ({ page }) => {
await page.goto('/editor');
await page.getByRole('button', { name: 'Edit' }).click();
// clear rundown
await page.getByRole('button', { name: 'Rundown menu' }).click();
await page.getByRole('menuitem', { name: 'Clear all' }).click();
await page.getByRole('button', { name: 'Delete all' }).click();
// create two events with distinct titles
await page.getByRole('button', { name: 'Create Event' }).click();
await page.getByRole('button', { name: 'Event', exact: true }).nth(1).click();
await expect(page.getByTestId('rundown-event')).toHaveCount(2);
await page.getByTestId('entry-1').getByTestId('entry__title').fill('first');
await page.getByTestId('entry-1').getByTestId('entry__title').press('Enter');
await page.getByTestId('entry-2').getByTestId('entry__title').fill('second');
await page.getByTestId('entry-2').getByTestId('entry__title').press('Enter');
const editor = page.getByTestId('editor-container');
// a single selection shows the schedule and the event id
await page.getByTestId('entry-1').getByTestId('rundown-event').click();
await expect(editor.getByText('Event schedule')).toBeVisible();
await expect(editor.getByLabel('Title', { exact: true })).toHaveValue('first');
// selecting both events shows a merged view
await page
.getByTestId('entry-2')
.getByTestId('rundown-event')
.click({ modifiers: ['Shift'] });
await expect(editor.locator('#eventId')).toHaveValue('2 events selected');
await expect(editor.getByText('Event schedule')).toBeHidden();
await expect(editor.getByText('Automations are not available when editing multiple events')).toBeVisible();
// fields which do not agree are shown as mixed
const title = editor.getByLabel('Title', { exact: true });
await expect(title).toHaveValue('');
await expect(title).toHaveAttribute('placeholder', 'Mixed');
// leaving a mixed field without editing it does not overwrite the events
await title.click();
await page.keyboard.press('Tab');
await expect(page.getByTestId('entry-1').getByTestId('entry__title')).toHaveValue('first');
await expect(page.getByTestId('entry-2').getByTestId('entry__title')).toHaveValue('second');
// editing a field applies it to the whole selection
await title.fill('shared title');
await title.press('Enter');
await expect(page.getByTestId('entry-1').getByTestId('entry__title')).toHaveValue('shared title');
await expect(page.getByTestId('entry-2').getByTestId('entry__title')).toHaveValue('shared title');
// the value is no longer mixed
await expect(editor.getByLabel('Title', { exact: true })).toHaveValue('shared title');
// going back to a single selection restores the full editor
await page.getByTestId('entry-1').getByTestId('rundown-event').click();
await expect(editor.getByText('Event schedule')).toBeVisible();
await expect(editor.locator('#eventId')).not.toHaveValue('2 events selected');
});