diff --git a/apps/client/src/features/rundown/placements/FinderPlacement.tsx b/apps/client/src/features/rundown/placements/FinderPlacement.tsx index e685d08b4..355075863 100644 --- a/apps/client/src/features/rundown/placements/FinderPlacement.tsx +++ b/apps/client/src/features/rundown/placements/FinderPlacement.tsx @@ -8,8 +8,12 @@ export default memo(FinderPlacement); function FinderPlacement() { const [isOpen, handler] = useDisclosure(); - // the finder handles its own dismissal: Escape is handled by the dialog, mod + f by the search input - useHotkeys([['mod + f', handler.toggle, { preventDefault: true }]]); + /** + * The empty tagsToIgnore is significant: by default the hook skips input elements, + * which would make the shortcut dead while editing an entry, and while typing in the + * finder itself. Escape is handled by the dialog. + */ + useHotkeys([['mod + f', handler.toggle, { preventDefault: true }]], []); if (isOpen) { return ; diff --git a/apps/client/src/views/editor/finder/Finder.tsx b/apps/client/src/views/editor/finder/Finder.tsx index 744d90027..606d63969 100644 --- a/apps/client/src/views/editor/finder/Finder.tsx +++ b/apps/client/src/views/editor/finder/Finder.tsx @@ -39,16 +39,6 @@ export default function Finder({ isOpen, onClose }: FinderProps) { }, [activeEntry?.id]); const navigate = (event: KeyboardEvent) => { - /** - * Mantine ignores hotkeys while an input is focused, so the global toggle - * cannot close the finder once the user is typing - */ - if ((event.metaKey || event.ctrlKey) && event.key === 'f') { - event.preventDefault(); - onClose(); - return; - } - // all operations need results if (results.length === 0) { return; diff --git a/apps/client/src/views/editor/finder/useFinder.tsx b/apps/client/src/views/editor/finder/useFinder.tsx index 0ee047bff..b996e4bfe 100644 --- a/apps/client/src/views/editor/finder/useFinder.tsx +++ b/apps/client/src/views/editor/finder/useFinder.tsx @@ -116,34 +116,48 @@ export default function useFinder() { return { results, error: null }; } - /** Returns maxResults of OntimeEvents that match the cue field */ + /** Returns maxResults of entries which carry a cue and match the cue field */ function searchByCue(searchString: string) { // indexes exposed to the UI are 1-based let eventIndex = 1; // limit amount of results we show let remaining = maxResults; - const results: FilterableEvent[] = []; + const results: FilterableEntry[] = []; for (let i = 0; i < data.length; i++) { if (remaining <= 0) { break; } - const event = data[i]; - if (isOntimeEvent(event)) { - if (event.cue.toLowerCase().includes(searchString)) { + const entry = data[i]; + if (isOntimeEvent(entry)) { + if (entry.cue.toLowerCase().includes(searchString)) { remaining--; results.push({ type: SupportedEntry.Event, - id: event.id, + id: entry.id, index: i, eventIndex, - title: event.title, - cue: event.cue, - colour: event.colour, - parent: event.parent, + title: entry.title, + cue: entry.cue, + colour: entry.colour, + parent: entry.parent, } satisfies FilterableEvent); } eventIndex++; + } else if (isOntimeMilestone(entry)) { + // milestones carry a cue and show it in the rundown, so they belong in a cue search + if (entry.cue.toLowerCase().includes(searchString)) { + remaining--; + results.push({ + type: SupportedEntry.Milestone, + id: entry.id, + index: i, + title: entry.title, + cue: entry.cue, + colour: entry.colour, + parent: entry.parent, + } satisfies FilterableMilestone); + } } } return { results, error: null }; diff --git a/e2e/tests/features/209-rundown-shortcuts.spec.ts b/e2e/tests/features/209-rundown-shortcuts.spec.ts index f701b2a3f..b5828e8dd 100644 --- a/e2e/tests/features/209-rundown-shortcuts.spec.ts +++ b/e2e/tests/features/209-rundown-shortcuts.spec.ts @@ -306,7 +306,10 @@ test('Finder navigates to the result picked with the keyboard', async ({ page }) await page.getByTestId('entry-2').getByTestId('entry__title').fill('finder two'); await page.getByTestId('entry-2').getByTestId('entry__title').press('Enter'); + // deliberately fired while the caret is still in a title field: the shortcut must not be + // swallowed by the input the user happens to be editing await page.keyboard.press('ControlOrMeta+f'); + await expect(page.getByPlaceholder('Search...')).toBeVisible(); await page.getByPlaceholder('Search...').fill('finder'); await expect(page.getByTestId('finder-result')).toHaveCount(2); @@ -319,6 +322,40 @@ test('Finder navigates to the result picked with the keyboard', async ({ page }) await expect(page.getByTestId('entry-2').getByTestId('rundown-event')).toHaveAttribute('data-selected', 'true'); }); +test('Finder searches milestones by cue', async ({ page }) => { + await page.goto('/rundown'); + 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(); + await expect(page.getByTestId('rundown-event')).toHaveCount(0); + + // an event, and a milestone carrying its own cue + await page.getByRole('button', { name: 'Create Event' }).click(); + await page.getByTestId('entry-1').click(); + await page.getByTestId('entry__title').press('Escape'); + await page.getByTestId('rundown-event').locator('div').filter({ hasText: '1' }).press('Alt+M'); + await expect(page.getByTestId('rundown-milestone')).toHaveCount(1); + + await page.getByTestId('rundown-milestone').getByPlaceholder('Cue').fill('MILE9'); + await page.getByTestId('rundown-milestone').getByPlaceholder('Cue').press('Enter'); + await page.getByTestId('rundown-milestone').getByPlaceholder('Title').fill('zebrafish'); + await page.getByTestId('rundown-milestone').getByPlaceholder('Title').press('Enter'); + + await page.keyboard.press('ControlOrMeta+f'); + await expect(page.getByPlaceholder('Search...')).toBeVisible(); + + // milestones were previously skipped by the cue search even though they carry a cue + await page.getByPlaceholder('Search...').fill('cue MILE9'); + await expect(page.getByTestId('finder-result')).toHaveCount(1); + + // and remain findable by title + await page.getByPlaceholder('Search...').fill('zebrafish'); + await expect(page.getByTestId('finder-result')).toHaveCount(1); +}); + test('Open settings', async ({ page }) => { await page.goto('/editor'); await expect(page.getByTestId('editor-container')).toBeVisible();