fix: open group when selecting from finder

This commit is contained in:
Carlos Valente
2025-08-02 09:27:59 +02:00
committed by Carlos Valente
parent 82e8b9d66a
commit daddb7f7b7
4 changed files with 208 additions and 160 deletions
@@ -56,6 +56,7 @@ export function useFlatRundown() {
}, [data.entries, data.flatOrder, data.revision, prevRevision]); }, [data.entries, data.flatOrder, data.revision, prevRevision]);
// TODO: should we have a project id field? // TODO: should we have a project id field?
// TODO(v4): cleanup as part of load multiple rundowns
// invalidate current version if project changes // invalidate current version if project changes
useEffect(() => { useEffect(() => {
if (projectData?.title !== loadedProject.current) { if (projectData?.title !== loadedProject.current) {
@@ -64,7 +65,7 @@ export function useFlatRundown() {
} }
}, [projectData]); }, [projectData]);
return { data: flatRundown, status }; return { data: flatRundown, rundownId: data.id, status };
} }
/** /**
+13
View File
@@ -114,6 +114,19 @@ table {
word-break: break-word; word-break: break-word;
} }
// reset list styles
ul,
ol {
list-style: none;
margin: 0;
padding: 0;
}
li {
margin: 0;
padding: 0;
}
// remove buttons in number inputs // remove buttons in number inputs
/* WebKit and Blink */ /* WebKit and Blink */
input::-webkit-outer-spin-button, input::-webkit-outer-spin-button,
@@ -4,7 +4,6 @@ import { SupportedEntry } from 'ontime-types';
import Input from '../../../common/components/input/input/Input'; import Input from '../../../common/components/input/input/Input';
import Modal from '../../../common/components/modal/Modal'; import Modal from '../../../common/components/modal/Modal';
import { useEventSelection } from '../../../features/rundown/useEventSelection';
import useFinder from './useFinder'; import useFinder from './useFinder';
@@ -16,10 +15,9 @@ interface FinderProps {
} }
export default function Finder({ isOpen, onClose }: FinderProps) { export default function Finder({ isOpen, onClose }: FinderProps) {
const { find, results, error } = useFinder(); const { find, select, results, error } = useFinder();
const [selected, setSelected] = useState(0); const [selected, setSelected] = useState(0);
const setSelectedEvents = useEventSelection((state) => state.setSelectedEvents);
const debouncedFind = useDebouncedCallback(find, 100); const debouncedFind = useDebouncedCallback(find, 100);
const navigate = (event: KeyboardEvent<HTMLDivElement>) => { const navigate = (event: KeyboardEvent<HTMLDivElement>) => {
@@ -34,13 +32,15 @@ export default function Finder({ isOpen, onClose }: FinderProps) {
setSelected((prev) => (prev - 1 + results.length) % results.length); setSelected((prev) => (prev - 1 + results.length) % results.length);
} }
if (event.key === 'Enter') { if (event.key === 'Enter') {
event.preventDefault();
event.stopPropagation();
submit(); submit();
} }
}; };
const submit = () => { const submit = () => {
const selectedEvent = results[selected]; const selectedEvent = results[selected];
setSelectedEvents({ id: selectedEvent.id, index: selectedEvent.index, selectMode: 'click' }); select(selectedEvent);
onClose(); onClose();
}; };
+189 -155
View File
@@ -1,7 +1,9 @@
import { ChangeEvent, useEffect, useRef, useState } from 'react'; import { ChangeEvent, useCallback, useEffect, useRef, useState } from 'react';
import { isOntimeBlock, isOntimeEvent, MaybeString, SupportedEntry } from 'ontime-types'; import { useSessionStorage } from '@mantine/hooks';
import { EntryId, isOntimeBlock, isOntimeEvent, MaybeString, SupportedEntry } from 'ontime-types';
import { useFlatRundown } from '../../../common/hooks-query/useRundown'; import { useFlatRundown } from '../../../common/hooks-query/useRundown';
import { useEventSelection } from '../../../features/rundown/useEventSelection';
const maxResults = 12; const maxResults = 12;
@@ -20,16 +22,198 @@ type FilterableEvent = {
title: string; title: string;
cue: string; cue: string;
colour: string; colour: string;
parent: MaybeString;
}; };
type FilterableEntry = FilterableBlock | FilterableEvent; type FilterableEntry = FilterableBlock | FilterableEvent;
export default function useFinder() { export default function useFinder() {
const { data } = useFlatRundown(); const { data, rundownId } = useFlatRundown();
const [results, setResults] = useState<FilterableEntry[]>([]); const [results, setResults] = useState<FilterableEntry[]>([]);
const [error, setError] = useState<MaybeString>(null); const [error, setError] = useState<MaybeString>(null);
const lastSearchString = useRef(''); const lastSearchString = useRef('');
const setSelectedEvents = useEventSelection((state) => state.setSelectedEvents);
const [collapsedGroups, setCollapsedGroups] = useSessionStorage<EntryId[]>({
// we ensure that this is unique to the rundown
key: `rundown.${rundownId}-editor-collapsed-groups`,
defaultValue: [],
});
/** Filters the rundown to a given evaluation */
const find = useCallback(
(event: ChangeEvent<HTMLInputElement>) => {
if (!data || data.length === 0) {
setError('No data');
return;
}
setError(null);
if (event.target.value === '') {
setResults([]);
return;
}
const searchValue = event.target.value.toLowerCase();
lastSearchString.current = searchValue;
if (searchValue.startsWith('index ')) {
const searchString = searchValue.replace('index ', '').trim();
const { results, error } = searchByIndex(searchString);
setResults(results);
setError(error);
return;
}
if (searchValue.startsWith('cue ')) {
const searchString = searchValue.replace('cue ', '').trim();
const { results, error } = searchByCue(searchString);
setResults(results);
setError(error);
return;
}
const searchString = searchValue.replace('title ', '').trim();
const { results, error } = searchByTitle(searchString);
setResults(results);
setError(error);
/** Returns a single item with a matching index */
function searchByIndex(searchString: string) {
const searchIndex = Number(searchString);
if (isNaN(searchIndex) || searchIndex < 1) {
return { results: [], error: 'Invalid index' };
}
if (searchIndex > data.length) {
return { results: [], error: null };
}
// indexes exposed to the UI are 1-based
let eventIndex = 1;
const results: FilterableEvent[] = [];
for (let i = 0; i < data.length; i++) {
const event = data[i];
if (isOntimeEvent(event)) {
if (eventIndex === searchIndex) {
results.push({
type: SupportedEntry.Event,
id: event.id,
index: i,
eventIndex,
title: event.title,
cue: event.cue,
colour: event.colour,
parent: event.parent,
} satisfies FilterableEvent);
break;
}
eventIndex++;
}
}
return { results, error: null };
}
/** Returns maxResults of OntimeEvents that 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[] = [];
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)) {
remaining--;
results.push({
type: SupportedEntry.Event,
id: event.id,
index: i,
eventIndex,
title: event.title,
cue: event.cue,
colour: event.colour,
parent: event.parent,
} satisfies FilterableEvent);
}
eventIndex++;
}
}
return { results, error: null };
}
/** Returns maxResults of OntimeEvents that match the title field*/
function searchByTitle(searchString: string) {
// indexes exposed to the UI are 1-based
let eventIndex = 1;
// limit amount of results we show
let remaining = maxResults;
const results: FilterableEntry[] = [];
for (let i = 0; i < data.length; i++) {
if (remaining <= 0) {
break;
}
const event = data[i];
if (isOntimeEvent(event)) {
if (event.title.toLowerCase().includes(searchString)) {
remaining--;
results.push({
type: SupportedEntry.Event,
id: event.id,
index: i,
eventIndex,
title: event.title,
cue: event.cue,
colour: event.colour,
parent: event.parent,
} satisfies FilterableEvent);
}
eventIndex++;
}
if (isOntimeBlock(event)) {
if (event.title.toLowerCase().includes(searchString)) {
remaining--;
results.push({
type: SupportedEntry.Block,
id: event.id,
index: i,
title: event.title,
} satisfies FilterableBlock);
}
}
}
return { results, error: null };
}
},
[data],
);
const select = useCallback(
(selectedEvent: FilterableEntry) => {
// First expand the parent group if this is an event inside a group
if (selectedEvent.type === SupportedEntry.Event && selectedEvent.parent !== null) {
// Try direct state update instead of using callback
const currentGroups = [...new Set(collapsedGroups)];
const newGroups = currentGroups.filter((id) => id !== selectedEvent.parent);
// Force a direct update
setCollapsedGroups(newGroups);
}
// Then select the event
setSelectedEvents({ id: selectedEvent.id, index: selectedEvent.index, selectMode: 'click' });
},
[collapsedGroups, setCollapsedGroups, setSelectedEvents],
);
/** clear results when source data changes */ /** clear results when source data changes */
useEffect(() => { useEffect(() => {
setResults([]); setResults([]);
@@ -38,157 +222,7 @@ export default function useFinder() {
if (lastSearchString.current) { if (lastSearchString.current) {
find({ target: { value: lastSearchString.current } } as ChangeEvent<HTMLInputElement>); find({ target: { value: lastSearchString.current } } as ChangeEvent<HTMLInputElement>);
} }
}, [data]); }, [data, find]);
/** Returns a single item with a matching index */ return { find, select, results, error };
const searchByIndex = (searchString: string) => {
const searchIndex = Number(searchString);
if (isNaN(searchIndex) || searchIndex < 1) {
return { results: [], error: 'Invalid index' };
}
if (searchIndex > data.length) {
return { results: [], error: null };
}
// indexes exposed to the UI are 1-based
let eventIndex = 1;
const results: FilterableEvent[] = [];
for (let i = 0; i < data.length; i++) {
const event = data[i];
if (isOntimeEvent(event)) {
if (eventIndex === searchIndex) {
results.push({
type: SupportedEntry.Event,
id: event.id,
index: i,
eventIndex,
title: event.title,
cue: event.cue,
colour: event.colour,
} satisfies FilterableEvent);
break;
}
eventIndex++;
}
}
return { results, error: null };
};
/** Returns maxResults of OntimeEvents that match the cue field */
const 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[] = [];
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)) {
remaining--;
results.push({
type: SupportedEntry.Event,
id: event.id,
index: i,
eventIndex,
title: event.title,
cue: event.cue,
colour: event.colour,
} satisfies FilterableEvent);
}
eventIndex++;
}
}
return { results, error: null };
};
/** Returns maxResults of OntimeEvents that match the title field*/
const searchByTitle = (searchString: string) => {
// indexes exposed to the UI are 1-based
let eventIndex = 1;
// limit amount of results we show
let remaining = maxResults;
const results: FilterableEntry[] = [];
for (let i = 0; i < data.length; i++) {
if (remaining <= 0) {
break;
}
const event = data[i];
if (isOntimeEvent(event)) {
if (event.title.toLowerCase().includes(searchString)) {
remaining--;
results.push({
type: SupportedEntry.Event,
id: event.id,
index: i,
eventIndex,
title: event.title,
cue: event.cue,
colour: event.colour,
} satisfies FilterableEvent);
}
eventIndex++;
}
if (isOntimeBlock(event)) {
if (event.title.toLowerCase().includes(searchString)) {
remaining--;
results.push({
type: SupportedEntry.Block,
id: event.id,
index: i,
title: event.title,
} satisfies FilterableBlock);
}
}
}
return { results, error: null };
};
/** Filters the rundown to a given evaluation */
const find = (event: ChangeEvent<HTMLInputElement>) => {
if (!data || data.length === 0) {
setError('No data');
return;
}
setError(null);
if (event.target.value === '') {
setResults([]);
return;
}
const searchValue = event.target.value.toLowerCase();
lastSearchString.current = searchValue;
if (searchValue.startsWith('index ')) {
const searchString = searchValue.replace('index ', '').trim();
const { results, error } = searchByIndex(searchString);
setResults(results);
setError(error);
return;
}
if (searchValue.startsWith('cue ')) {
const searchString = searchValue.replace('cue ', '').trim();
const { results, error } = searchByCue(searchString);
setResults(results);
setError(error);
return;
}
const searchString = searchValue.replace('title ', '').trim();
const { results, error } = searchByTitle(searchString);
setResults(results);
setError(error);
};
return { find, results, error };
} }