mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-18 05:34:09 +00:00
fix: open group when selecting from finder
This commit is contained in:
committed by
Carlos Valente
parent
82e8b9d66a
commit
daddb7f7b7
@@ -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 };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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 };
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user