mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-17 21:24:11 +00:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8e7ab54d39 | |||
| 4a16378786 | |||
| e5b30770ce | |||
| 8465b04c89 | |||
| 718a6ad2b7 | |||
| 6ca4bfc363 | |||
| 4859026989 |
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@getontime/cli",
|
"name": "@getontime/cli",
|
||||||
"version": "3.13.1",
|
"version": "3.14.0",
|
||||||
"author": "Carlos Valente",
|
"author": "Carlos Valente",
|
||||||
"description": "Time keeping for live events",
|
"description": "Time keeping for live events",
|
||||||
"repository": "https://github.com/cpvalente/ontime",
|
"repository": "https://github.com/cpvalente/ontime",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ontime-ui",
|
"name": "ontime-ui",
|
||||||
"version": "3.13.1",
|
"version": "3.14.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -15,6 +15,7 @@
|
|||||||
"@mantine/hooks": "^7.13.3",
|
"@mantine/hooks": "^7.13.3",
|
||||||
"@react-icons/all-files": "^4.1.0",
|
"@react-icons/all-files": "^4.1.0",
|
||||||
"@sentry/react": "^8.43.0",
|
"@sentry/react": "^8.43.0",
|
||||||
|
"@table-nav/react": "^0.0.7",
|
||||||
"@tanstack/react-query": "^5.62.7",
|
"@tanstack/react-query": "^5.62.7",
|
||||||
"@tanstack/react-query-devtools": "^5.62.7",
|
"@tanstack/react-query-devtools": "^5.62.7",
|
||||||
"@tanstack/react-table": "^8.20.5",
|
"@tanstack/react-table": "^8.20.5",
|
||||||
|
|||||||
@@ -158,7 +158,14 @@ export default function AppRouter() {
|
|||||||
{/*/!* Protected Routes *!/*/}
|
{/*/!* Protected Routes *!/*/}
|
||||||
<Route path='/editor' element={<Editor />} />
|
<Route path='/editor' element={<Editor />} />
|
||||||
<Route path='/cuesheet' element={<PCuesheet />} />
|
<Route path='/cuesheet' element={<PCuesheet />} />
|
||||||
<Route path='/op' element={<POperator />} />
|
<Route
|
||||||
|
path='/op'
|
||||||
|
element={
|
||||||
|
<ViewLoader>
|
||||||
|
<POperator />
|
||||||
|
</ViewLoader>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
{/*/!* Protected Routes - Elements *!/*/}
|
{/*/!* Protected Routes - Elements *!/*/}
|
||||||
<Route
|
<Route
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ export default function useReactiveTextInput(
|
|||||||
submitOnCtrlEnter?: boolean;
|
submitOnCtrlEnter?: boolean;
|
||||||
onCancelUpdate?: () => void;
|
onCancelUpdate?: () => void;
|
||||||
allowSubmitSameValue?: boolean;
|
allowSubmitSameValue?: boolean;
|
||||||
|
allowKeyboardNavigation?: boolean;
|
||||||
},
|
},
|
||||||
): UseReactiveTextInputReturn {
|
): UseReactiveTextInputReturn {
|
||||||
const [text, setText] = useState<string>(initialText);
|
const [text, setText] = useState<string>(initialText);
|
||||||
@@ -58,7 +59,14 @@ export default function useReactiveTextInput(
|
|||||||
setText(cleanVal);
|
setText(cleanVal);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
setTimeout(() => ref.current?.blur()); // Immediate timeout to ensure text is set before bluring
|
|
||||||
|
setTimeout(() => {
|
||||||
|
if (options?.allowKeyboardNavigation) {
|
||||||
|
ref.current?.parentElement?.focus(); // Focus on parent element to continue keyboard navigation
|
||||||
|
} else {
|
||||||
|
ref.current?.blur();
|
||||||
|
}
|
||||||
|
}); // Immediate timeout to ensure text is set before blurring
|
||||||
},
|
},
|
||||||
[initialText, options, ref, submitCallback],
|
[initialText, options, ref, submitCallback],
|
||||||
);
|
);
|
||||||
@@ -70,12 +78,25 @@ export default function useReactiveTextInput(
|
|||||||
const handleEscape = useCallback(() => {
|
const handleEscape = useCallback(() => {
|
||||||
// No need to update if it hasn't changed
|
// No need to update if it hasn't changed
|
||||||
setText(initialText);
|
setText(initialText);
|
||||||
|
// force the text to be the initial value
|
||||||
|
if (ref.current) {
|
||||||
|
ref.current.value = initialText;
|
||||||
|
}
|
||||||
options?.onCancelUpdate?.();
|
options?.onCancelUpdate?.();
|
||||||
setTimeout(() => ref.current?.blur()); // Immediate timeout to ensure text is set before bluring
|
setTimeout(() => ref.current?.blur()); // Immediate timeout to ensure text is set before blurring
|
||||||
}, [initialText, options, ref]);
|
}, [initialText, options, ref]);
|
||||||
|
|
||||||
const keyHandler = useMemo(() => {
|
const keyHandler = useMemo(() => {
|
||||||
const hotKeys: HotkeyItem[] = [['Escape', handleEscape, { preventDefault: true }]];
|
const hotKeys: HotkeyItem[] = [
|
||||||
|
[
|
||||||
|
'Escape',
|
||||||
|
(event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
handleEscape();
|
||||||
|
},
|
||||||
|
{ preventDefault: true },
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
if (options?.submitOnEnter) {
|
if (options?.submitOnEnter) {
|
||||||
hotKeys.push(['Enter', () => handleSubmit(text)]);
|
hotKeys.push(['Enter', () => handleSubmit(text)]);
|
||||||
@@ -84,7 +105,22 @@ export default function useReactiveTextInput(
|
|||||||
if (options?.submitOnCtrlEnter) {
|
if (options?.submitOnCtrlEnter) {
|
||||||
hotKeys.push(['mod + Enter', () => handleSubmit(text)]);
|
hotKeys.push(['mod + Enter', () => handleSubmit(text)]);
|
||||||
}
|
}
|
||||||
return getHotkeyHandler(hotKeys);
|
|
||||||
|
const hotKeyHandler = getHotkeyHandler(hotKeys);
|
||||||
|
|
||||||
|
return (event: KeyboardEvent<HTMLElement>) => {
|
||||||
|
// allow moving in input field with arrow keys
|
||||||
|
if (
|
||||||
|
event.key === 'ArrowLeft' ||
|
||||||
|
event.key === 'ArrowRight' ||
|
||||||
|
event.key === 'ArrowUp' ||
|
||||||
|
event.key === 'ArrowDown'
|
||||||
|
) {
|
||||||
|
event.stopPropagation();
|
||||||
|
}
|
||||||
|
|
||||||
|
hotKeyHandler(event);
|
||||||
|
};
|
||||||
}, [handleEscape, handleSubmit, options?.submitOnCtrlEnter, options?.submitOnEnter, text]);
|
}, [handleEscape, handleSubmit, options?.submitOnCtrlEnter, options?.submitOnEnter, text]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -126,9 +126,9 @@ function MultiOption(props: EditFormMultiOptionProps) {
|
|||||||
onChange={(value) => setParamState(Array.isArray(value) ? value : [value])}
|
onChange={(value) => setParamState(Array.isArray(value) ? value : [value])}
|
||||||
>
|
>
|
||||||
{Object.values(paramField.values).map((option) => {
|
{Object.values(paramField.values).map((option) => {
|
||||||
const { value, label } = option;
|
const { value, label, colour } = option;
|
||||||
return (
|
return (
|
||||||
<MenuItemOption value={value} key={value}>
|
<MenuItemOption value={value} key={value} style={{ borderRight: `8px solid ${colour}` }}>
|
||||||
{label}
|
{label}
|
||||||
</MenuItemOption>
|
</MenuItemOption>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { CustomFields } from 'ontime-types';
|
import { CustomFields } from 'ontime-types';
|
||||||
|
|
||||||
import type { ParamField } from './types';
|
import type { MultiselectOptions, ParamField } from './types';
|
||||||
|
|
||||||
export const makeOptionsFromCustomFields = (
|
export const makeOptionsFromCustomFields = (
|
||||||
customFields: CustomFields,
|
customFields: CustomFields,
|
||||||
@@ -18,6 +18,17 @@ export const makeOptionsFromCustomFields = (
|
|||||||
return options;
|
return options;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export function makeCustomFieldSelectOptions(customFields: CustomFields, filterImageType = true): MultiselectOptions {
|
||||||
|
const options: MultiselectOptions = {};
|
||||||
|
for (const [key, value] of Object.entries(customFields)) {
|
||||||
|
if (filterImageType && value.type === 'image') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
options[key] = { value: key, label: value.label, colour: value.colour };
|
||||||
|
}
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
|
||||||
export const getTimeOption = (timeFormat: string): ParamField => {
|
export const getTimeOption = (timeFormat: string): ParamField => {
|
||||||
const placeholder = `${timeFormat} (default)`;
|
const placeholder = `${timeFormat} (default)`;
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -12,9 +12,11 @@ type OptionsField = {
|
|||||||
defaultValue?: string;
|
defaultValue?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type MultiselectOption = { value: string; label: string; colour: string };
|
||||||
|
export type MultiselectOptions = Record<string, MultiselectOption>;
|
||||||
type MultiOptionsField = {
|
type MultiOptionsField = {
|
||||||
type: 'multi-option';
|
type: 'multi-option';
|
||||||
values: Record<string, { value: string; label: string; colour: string }>;
|
values: MultiselectOptions;
|
||||||
defaultValue?: string;
|
defaultValue?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
/**
|
||||||
|
* Allows lazy evaluation and caching of a functions result
|
||||||
|
*/
|
||||||
|
export function lazyEvaluate<T>(fn: () => T): () => T {
|
||||||
|
let result: T | undefined;
|
||||||
|
return function () {
|
||||||
|
if (result === undefined) {
|
||||||
|
result = fn();
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -116,7 +116,7 @@ export default function Operator() {
|
|||||||
|
|
||||||
// subscriptions is a MultiSelect and may have multiple values
|
// subscriptions is a MultiSelect and may have multiple values
|
||||||
const subscriptions = searchParams.getAll('subscribe').filter((value) => Object.hasOwn(customFields, value));
|
const subscriptions = searchParams.getAll('subscribe').filter((value) => Object.hasOwn(customFields, value));
|
||||||
const canEdit = shouldEdit && subscriptions;
|
const canEdit = shouldEdit && subscriptions.length;
|
||||||
|
|
||||||
const main = searchParams.get('main') as keyof TitleFields | null;
|
const main = searchParams.get('main') as keyof TitleFields | null;
|
||||||
const secondary = searchParams.get('secondary');
|
const secondary = searchParams.get('secondary');
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { CustomFields } from 'ontime-types';
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
getTimeOption,
|
getTimeOption,
|
||||||
|
makeCustomFieldSelectOptions,
|
||||||
makeOptionsFromCustomFields,
|
makeOptionsFromCustomFields,
|
||||||
OptionTitle,
|
OptionTitle,
|
||||||
} from '../../common/components/view-params-editor/constants';
|
} from '../../common/components/view-params-editor/constants';
|
||||||
@@ -9,13 +10,7 @@ import { ViewOption } from '../../common/components/view-params-editor/types';
|
|||||||
|
|
||||||
export const getOperatorOptions = (customFields: CustomFields, timeFormat: string): ViewOption[] => {
|
export const getOperatorOptions = (customFields: CustomFields, timeFormat: string): ViewOption[] => {
|
||||||
const fieldOptions = makeOptionsFromCustomFields(customFields, { title: 'Title', note: 'Note' });
|
const fieldOptions = makeOptionsFromCustomFields(customFields, { title: 'Title', note: 'Note' });
|
||||||
|
const customFieldSelect = makeCustomFieldSelectOptions(customFields);
|
||||||
const customFieldSelect = Object.entries(customFields).reduce<
|
|
||||||
Record<string, { value: string; label: string; colour: string }>
|
|
||||||
>((acc, [key, field]) => {
|
|
||||||
acc[key] = { value: key, label: field.label, colour: field.colour };
|
|
||||||
return acc;
|
|
||||||
}, {});
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{ title: OptionTitle.ClockOptions, collapsible: true, options: [getTimeOption(timeFormat)] },
|
{ title: OptionTitle.ClockOptions, collapsible: true, options: [getTimeOption(timeFormat)] },
|
||||||
@@ -46,6 +41,13 @@ export const getOperatorOptions = (customFields: CustomFields, timeFormat: strin
|
|||||||
type: 'multi-option',
|
type: 'multi-option',
|
||||||
values: customFieldSelect,
|
values: customFieldSelect,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'shouldEdit',
|
||||||
|
title: 'Edit custom field',
|
||||||
|
description: 'Allows editing an highlighted custom field by long pressing',
|
||||||
|
type: 'boolean',
|
||||||
|
defaultValue: false,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -59,13 +61,6 @@ export const getOperatorOptions = (customFields: CustomFields, timeFormat: strin
|
|||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
defaultValue: false,
|
defaultValue: false,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
id: 'shouldEdit',
|
|
||||||
title: 'Edit custom field',
|
|
||||||
description: 'Allows editing an events selected custom field by long pressing.',
|
|
||||||
type: 'boolean',
|
|
||||||
defaultValue: false,
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { useEffect, useState } from 'react';
|
|||||||
import { isOntimeEvent, OntimeEvent } from 'ontime-types';
|
import { isOntimeEvent, OntimeEvent } from 'ontime-types';
|
||||||
|
|
||||||
import useRundown from '../../../common/hooks-query/useRundown';
|
import useRundown from '../../../common/hooks-query/useRundown';
|
||||||
|
import { cx } from '../../../common/utils/styleUtils';
|
||||||
|
|
||||||
import EventEditor from './EventEditor';
|
import EventEditor from './EventEditor';
|
||||||
|
|
||||||
@@ -37,7 +38,7 @@ export default function CuesheetEventEditor(props: CuesheetEventEditorProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={style.eventEditor} data-testid='editor-container'>
|
<div className={cx([style.eventEditor, style.inModal])} data-testid='editor-container'>
|
||||||
<EventEditor event={event} />
|
<EventEditor event={event} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
.eventEditor {
|
.eventEditor {
|
||||||
max-height: 80vh;
|
max-height: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
|
|
||||||
|
&.inModal {
|
||||||
|
max-height: 80vh;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
|
|||||||
@@ -23,8 +23,8 @@ import styles from './CuesheetPage.module.scss';
|
|||||||
|
|
||||||
export default function CuesheetPage() {
|
export default function CuesheetPage() {
|
||||||
// TODO: can we use the normalised rundown for the table?
|
// TODO: can we use the normalised rundown for the table?
|
||||||
const { data: flatRundown } = useFlatRundown();
|
const { data: flatRundown, status: rundownStatus } = useFlatRundown();
|
||||||
const { data: customFields } = useCustomFields();
|
const { data: customFields, status: customFieldStatus } = useCustomFields();
|
||||||
const { showEditFormDrawer, isViewLocked } = useViewEditor({ isLockable: true });
|
const { showEditFormDrawer, isViewLocked } = useViewEditor({ isLockable: true });
|
||||||
const { isOpen: isMenuOpen, onOpen, onClose } = useDisclosure();
|
const { isOpen: isMenuOpen, onOpen, onClose } = useDisclosure();
|
||||||
const { isOpen: isEventEditorOpen, onOpen: onEventEditorOpen, onClose: onEventEditorClose } = useDisclosure();
|
const { isOpen: isEventEditorOpen, onOpen: onEventEditorOpen, onClose: onEventEditorClose } = useDisclosure();
|
||||||
@@ -50,7 +50,7 @@ export default function CuesheetPage() {
|
|||||||
[onEventEditorClose, onEventEditorOpen],
|
[onEventEditorClose, onEventEditorOpen],
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!customFields || !flatRundown) {
|
if (!customFields || !flatRundown || rundownStatus === 'pending' || customFieldStatus === 'pending') {
|
||||||
return <EmptyPage text='Loading...' />;
|
return <EmptyPage text='Loading...' />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,11 @@ $table-header-font-size: calc(1rem - 2px);
|
|||||||
position: relative;
|
position: relative;
|
||||||
@include ellipsis-overflow;
|
@include ellipsis-overflow;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
th:focus,
|
||||||
|
td:focus {
|
||||||
|
outline: 2px solid $ui-white;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.tableHeader,
|
.tableHeader,
|
||||||
|
|||||||
@@ -1,27 +1,14 @@
|
|||||||
import { useCallback, useRef } from 'react';
|
import { useCallback, useRef } from 'react';
|
||||||
|
import { useTableNav } from '@table-nav/react';
|
||||||
import { ColumnDef, getCoreRowModel, useReactTable } from '@tanstack/react-table';
|
import { ColumnDef, getCoreRowModel, useReactTable } from '@tanstack/react-table';
|
||||||
import Color from 'color';
|
import { isOntimeEvent, MaybeString, OntimeEvent, OntimeRundown, OntimeRundownEntry, TimeField } from 'ontime-types';
|
||||||
import {
|
|
||||||
isOntimeBlock,
|
|
||||||
isOntimeDelay,
|
|
||||||
isOntimeEvent,
|
|
||||||
MaybeString,
|
|
||||||
OntimeEvent,
|
|
||||||
OntimeRundown,
|
|
||||||
OntimeRundownEntry,
|
|
||||||
TimeField,
|
|
||||||
} from 'ontime-types';
|
|
||||||
|
|
||||||
import { useEventAction } from '../../../common/hooks/useEventAction';
|
import { useEventAction } from '../../../common/hooks/useEventAction';
|
||||||
import useFollowComponent from '../../../common/hooks/useFollowComponent';
|
import useFollowComponent from '../../../common/hooks/useFollowComponent';
|
||||||
import { useSelectedEventId } from '../../../common/hooks/useSocket';
|
|
||||||
import { getAccessibleColour } from '../../../common/utils/styleUtils';
|
|
||||||
import { useCuesheetOptions } from '../cuesheet.options';
|
import { useCuesheetOptions } from '../cuesheet.options';
|
||||||
|
|
||||||
import BlockRow from './cuesheet-table-elements/BlockRow';
|
import CuesheetBody from './cuesheet-table-elements/CuesheetBody';
|
||||||
import CuesheetHeader from './cuesheet-table-elements/CuesheetHeader';
|
import CuesheetHeader from './cuesheet-table-elements/CuesheetHeader';
|
||||||
import DelayRow from './cuesheet-table-elements/DelayRow';
|
|
||||||
import EventRow from './cuesheet-table-elements/EventRow';
|
|
||||||
import CuesheetTableMenu from './cuesheet-table-menu/CuesheetTableMenu';
|
import CuesheetTableMenu from './cuesheet-table-menu/CuesheetTableMenu';
|
||||||
import CuesheetTableSettings from './cuesheet-table-settings/CuesheetTableSettings';
|
import CuesheetTableSettings from './cuesheet-table-settings/CuesheetTableSettings';
|
||||||
import useColumnManager from './useColumnManager';
|
import useColumnManager from './useColumnManager';
|
||||||
@@ -38,8 +25,7 @@ export default function CuesheetTable(props: CuesheetTableProps) {
|
|||||||
const { data, columns, showModal } = props;
|
const { data, columns, showModal } = props;
|
||||||
|
|
||||||
const { updateEvent, updateTimer } = useEventAction();
|
const { updateEvent, updateTimer } = useEventAction();
|
||||||
const { selectedEventId } = useSelectedEventId();
|
const { followSelected, showDelayedTimes, hideTableSeconds } = useCuesheetOptions();
|
||||||
const { followSelected, hideDelays, hidePast, showDelayedTimes, hideTableSeconds } = useCuesheetOptions();
|
|
||||||
const { columnVisibility, columnOrder, columnSizing, resetColumnOrder, setColumnVisibility, setColumnSizing } =
|
const { columnVisibility, columnOrder, columnSizing, resetColumnOrder, setColumnVisibility, setColumnSizing } =
|
||||||
useColumnManager(columns);
|
useColumnManager(columns);
|
||||||
|
|
||||||
@@ -47,6 +33,8 @@ export default function CuesheetTable(props: CuesheetTableProps) {
|
|||||||
const tableContainerRef = useRef<HTMLDivElement | null>(null);
|
const tableContainerRef = useRef<HTMLDivElement | null>(null);
|
||||||
useFollowComponent({ followRef: selectedRef, scrollRef: tableContainerRef, doFollow: followSelected });
|
useFollowComponent({ followRef: selectedRef, scrollRef: tableContainerRef, doFollow: followSelected });
|
||||||
|
|
||||||
|
const { listeners } = useTableNav();
|
||||||
|
|
||||||
const table = useReactTable({
|
const table = useReactTable({
|
||||||
data,
|
data,
|
||||||
columns,
|
columns,
|
||||||
@@ -63,6 +51,7 @@ export default function CuesheetTable(props: CuesheetTableProps) {
|
|||||||
handleUpdate: (rowIndex: number, accessor: string, payload: string, isCustom = false) => {
|
handleUpdate: (rowIndex: number, accessor: string, payload: string, isCustom = false) => {
|
||||||
// check if value is the same
|
// check if value is the same
|
||||||
const event = data[rowIndex];
|
const event = data[rowIndex];
|
||||||
|
|
||||||
if (!event || !isOntimeEvent(event)) {
|
if (!event || !isOntimeEvent(event)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -104,10 +93,6 @@ export default function CuesheetTable(props: CuesheetTableProps) {
|
|||||||
const rowModel = table.getRowModel();
|
const rowModel = table.getRowModel();
|
||||||
const allLeafColumns = table.getAllLeafColumns();
|
const allLeafColumns = table.getAllLeafColumns();
|
||||||
|
|
||||||
let eventIndex = 0;
|
|
||||||
// for the first event, it will be past if there is something selected
|
|
||||||
let isPast = Boolean(selectedEventId);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<CuesheetTableSettings
|
<CuesheetTableSettings
|
||||||
@@ -117,74 +102,9 @@ export default function CuesheetTable(props: CuesheetTableProps) {
|
|||||||
handleClearToggles={setAllVisible}
|
handleClearToggles={setAllVisible}
|
||||||
/>
|
/>
|
||||||
<div ref={tableContainerRef} className={style.cuesheetContainer}>
|
<div ref={tableContainerRef} className={style.cuesheetContainer}>
|
||||||
<table className={style.cuesheet} id='cuesheet'>
|
<table className={style.cuesheet} id='cuesheet' {...listeners}>
|
||||||
<CuesheetHeader headerGroups={headerGroups} />
|
<CuesheetHeader headerGroups={headerGroups} />
|
||||||
<tbody>
|
<CuesheetBody rowModel={rowModel} selectedRef={selectedRef} table={table} columnSizing={columnSizing} />
|
||||||
{rowModel.rows.map((row, index) => {
|
|
||||||
const key = row.original.id;
|
|
||||||
const isSelected = selectedEventId === key;
|
|
||||||
const entry = row.original;
|
|
||||||
if (isSelected) {
|
|
||||||
isPast = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isOntimeBlock(entry)) {
|
|
||||||
return <BlockRow key={key} title={entry.title} hidePast={isPast && hidePast} />;
|
|
||||||
}
|
|
||||||
if (isOntimeDelay(entry)) {
|
|
||||||
if (isPast && hidePast) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const delayVal = entry.duration;
|
|
||||||
if (hideDelays || delayVal === 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return <DelayRow key={key} duration={delayVal} />;
|
|
||||||
}
|
|
||||||
if (isOntimeEvent(entry)) {
|
|
||||||
eventIndex++;
|
|
||||||
const isSelected = key === selectedEventId;
|
|
||||||
|
|
||||||
if (isPast && hidePast) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
let rowBgColour: string | undefined;
|
|
||||||
if (isSelected) {
|
|
||||||
rowBgColour = '#D20300'; // $red-700
|
|
||||||
} else if (entry.colour) {
|
|
||||||
try {
|
|
||||||
// the colour is user defined and might be invalid
|
|
||||||
const accessibleBackgroundColor = Color(getAccessibleColour(entry.colour).backgroundColor);
|
|
||||||
rowBgColour = accessibleBackgroundColor.fade(0.75).hexa();
|
|
||||||
} catch (_error) {
|
|
||||||
/* we do not handle errors here */
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<EventRow
|
|
||||||
key={`${row.id}-${entry.revision}`}
|
|
||||||
rowId={row.id}
|
|
||||||
eventId={entry.id}
|
|
||||||
eventIndex={eventIndex}
|
|
||||||
rowIndex={index}
|
|
||||||
isPast={isPast}
|
|
||||||
selectedRef={isSelected ? selectedRef : undefined}
|
|
||||||
skip={entry.skip}
|
|
||||||
colour={entry.colour}
|
|
||||||
rowBgColour={rowBgColour}
|
|
||||||
table={table}
|
|
||||||
columnSizing={columnSizing}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// currently there is no scenario where entryType is not handled above, either way...
|
|
||||||
return null;
|
|
||||||
})}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<CuesheetTableMenu showModal={showModal} />
|
<CuesheetTableMenu showModal={showModal} />
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { memo } from 'react';
|
import { memo, useRef } from 'react';
|
||||||
|
|
||||||
import { useCurrentBlockId } from '../../../../common/hooks/useSocket';
|
import { useCurrentBlockId } from '../../../../common/hooks/useSocket';
|
||||||
|
|
||||||
@@ -7,19 +7,37 @@ import style from '../CuesheetTable.module.scss';
|
|||||||
interface BlockRowProps {
|
interface BlockRowProps {
|
||||||
hidePast: boolean;
|
hidePast: boolean;
|
||||||
title: string;
|
title: string;
|
||||||
|
columnCount: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
function BlockRow(props: BlockRowProps) {
|
function BlockRow(props: BlockRowProps) {
|
||||||
const { hidePast, title } = props;
|
const { hidePast, title, columnCount } = props;
|
||||||
const { currentBlockId } = useCurrentBlockId();
|
const { currentBlockId } = useCurrentBlockId();
|
||||||
|
const firstCellRef = useRef<null | HTMLTableCellElement>(null);
|
||||||
|
|
||||||
if (hidePast && !currentBlockId) {
|
if (hidePast && !currentBlockId) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const paddingRows = new Array(columnCount - 1).fill(null);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<tr className={style.blockRow}>
|
<tr className={style.blockRow}>
|
||||||
<td>{title}</td>
|
<td tabIndex={-1} role='cell' ref={firstCellRef}>
|
||||||
|
{title}
|
||||||
|
</td>
|
||||||
|
{paddingRows.map((_value, index) => {
|
||||||
|
return (
|
||||||
|
<td
|
||||||
|
key={index}
|
||||||
|
tabIndex={-1}
|
||||||
|
role='cell'
|
||||||
|
onFocus={() => {
|
||||||
|
firstCellRef.current?.focus();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</tr>
|
</tr>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+100
@@ -0,0 +1,100 @@
|
|||||||
|
import { MutableRefObject } from 'react';
|
||||||
|
import { RowModel, Table } from '@tanstack/react-table';
|
||||||
|
import Color from 'color';
|
||||||
|
import { isOntimeBlock, isOntimeDelay, isOntimeEvent, OntimeRundownEntry } from 'ontime-types';
|
||||||
|
|
||||||
|
import { useSelectedEventId } from '../../../../common/hooks/useSocket';
|
||||||
|
import { lazyEvaluate } from '../../../../common/utils/lazyEvaluate';
|
||||||
|
import { getAccessibleColour } from '../../../../common/utils/styleUtils';
|
||||||
|
import { useCuesheetOptions } from '../../../cuesheet/cuesheet.options';
|
||||||
|
|
||||||
|
import BlockRow from './BlockRow';
|
||||||
|
import DelayRow from './DelayRow';
|
||||||
|
import EventRow from './EventRow';
|
||||||
|
|
||||||
|
interface CuesheetBodyProps {
|
||||||
|
rowModel: RowModel<OntimeRundownEntry>;
|
||||||
|
selectedRef: MutableRefObject<HTMLTableRowElement | null>;
|
||||||
|
table: Table<OntimeRundownEntry>;
|
||||||
|
columnSizing: Record<string, number>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function CuesheetBody(props: CuesheetBodyProps) {
|
||||||
|
const { rowModel, selectedRef, table, columnSizing } = props;
|
||||||
|
|
||||||
|
const { selectedEventId } = useSelectedEventId();
|
||||||
|
const { hideDelays, hidePast } = useCuesheetOptions();
|
||||||
|
|
||||||
|
const getColumnCount = lazyEvaluate(() => table.getVisibleFlatColumns().length);
|
||||||
|
|
||||||
|
let eventIndex = 0;
|
||||||
|
// for the first event, it will be past if there is something selected
|
||||||
|
let isPast = Boolean(selectedEventId);
|
||||||
|
return (
|
||||||
|
<tbody>
|
||||||
|
{rowModel.rows.map((row, index) => {
|
||||||
|
const key = row.original.id;
|
||||||
|
const isSelected = selectedEventId === key;
|
||||||
|
const entry = row.original;
|
||||||
|
if (isSelected) {
|
||||||
|
isPast = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isOntimeBlock(entry)) {
|
||||||
|
const columnCount = getColumnCount();
|
||||||
|
return <BlockRow columnCount={columnCount} key={key} title={entry.title} hidePast={isPast && hidePast} />;
|
||||||
|
}
|
||||||
|
if (isOntimeDelay(entry)) {
|
||||||
|
if (isPast && hidePast) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const delayVal = entry.duration;
|
||||||
|
if (hideDelays || delayVal === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <DelayRow key={key} duration={delayVal} />;
|
||||||
|
}
|
||||||
|
if (isOntimeEvent(entry)) {
|
||||||
|
eventIndex++;
|
||||||
|
const isSelected = key === selectedEventId;
|
||||||
|
|
||||||
|
if (isPast && hidePast) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let rowBgColour: string | undefined;
|
||||||
|
if (isSelected) {
|
||||||
|
rowBgColour = '#D20300'; // $red-700
|
||||||
|
} else if (entry.colour) {
|
||||||
|
try {
|
||||||
|
// the colour is user defined and might be invalid
|
||||||
|
const accessibleBackgroundColor = Color(getAccessibleColour(entry.colour).backgroundColor);
|
||||||
|
rowBgColour = accessibleBackgroundColor.fade(0.75).hexa();
|
||||||
|
} catch (_error) {
|
||||||
|
/* we do not handle errors here */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<EventRow
|
||||||
|
key={row.id}
|
||||||
|
rowId={row.id}
|
||||||
|
event={entry}
|
||||||
|
eventIndex={eventIndex}
|
||||||
|
rowIndex={index}
|
||||||
|
isPast={isPast}
|
||||||
|
selectedRef={isSelected ? selectedRef : undefined}
|
||||||
|
rowBgColour={rowBgColour}
|
||||||
|
table={table}
|
||||||
|
columnSizing={columnSizing}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// currently there is no scenario where entryType is not handled above, either way...
|
||||||
|
return null;
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
);
|
||||||
|
}
|
||||||
+6
-2
@@ -24,8 +24,12 @@ export default function CuesheetHeader(props: CuesheetHeaderProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<tr key={headerGroup.id}>
|
<tr key={headerGroup.id}>
|
||||||
{showActionMenu && <th className={style.actionColumn} />}
|
{showActionMenu && <th className={style.actionColumn} tabIndex={-1} />}
|
||||||
{!hideIndexColumn && <th className={style.indexColumn}>#</th>}
|
{!hideIndexColumn && (
|
||||||
|
<th className={style.indexColumn} tabIndex={-1}>
|
||||||
|
#
|
||||||
|
</th>
|
||||||
|
)}
|
||||||
<SortableContext key={key} items={headerGroup.headers} strategy={horizontalListSortingStrategy}>
|
<SortableContext key={key} items={headerGroup.headers} strategy={horizontalListSortingStrategy}>
|
||||||
{headerGroup.headers.map((header) => {
|
{headerGroup.headers.map((header) => {
|
||||||
const width = header.getSize();
|
const width = header.getSize();
|
||||||
|
|||||||
@@ -14,7 +14,9 @@ function DelayRow(props: DelayRowProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<tr className={style.delayRow}>
|
<tr className={style.delayRow}>
|
||||||
<td>{delayTime}</td>
|
<td tabIndex={0} role='cell'>
|
||||||
|
{delayTime}
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+29
-12
@@ -2,7 +2,7 @@ import { memo, MutableRefObject, useLayoutEffect, useRef, useState } from 'react
|
|||||||
import { IoEllipsisHorizontal } from '@react-icons/all-files/io5/IoEllipsisHorizontal';
|
import { IoEllipsisHorizontal } from '@react-icons/all-files/io5/IoEllipsisHorizontal';
|
||||||
import { flexRender, Table } from '@tanstack/react-table';
|
import { flexRender, Table } from '@tanstack/react-table';
|
||||||
import Color from 'color';
|
import Color from 'color';
|
||||||
import { OntimeRundownEntry } from 'ontime-types';
|
import { OntimeEvent, OntimeRundownEntry } from 'ontime-types';
|
||||||
|
|
||||||
import IconButton from '../../../../common/components/buttons/IconButton';
|
import IconButton from '../../../../common/components/buttons/IconButton';
|
||||||
import { cx, getAccessibleColour } from '../../../../common/utils/styleUtils';
|
import { cx, getAccessibleColour } from '../../../../common/utils/styleUtils';
|
||||||
@@ -13,7 +13,7 @@ import style from '../CuesheetTable.module.scss';
|
|||||||
|
|
||||||
interface EventRowProps {
|
interface EventRowProps {
|
||||||
rowId: string;
|
rowId: string;
|
||||||
eventId: string;
|
event: OntimeEvent;
|
||||||
eventIndex: number;
|
eventIndex: number;
|
||||||
rowIndex: number;
|
rowIndex: number;
|
||||||
isPast?: boolean;
|
isPast?: boolean;
|
||||||
@@ -26,10 +26,22 @@ interface EventRowProps {
|
|||||||
columnSizing: Record<string, number>;
|
columnSizing: Record<string, number>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default memo(EventRow);
|
export default memo(EventRow, (prevProps, nextProps) => {
|
||||||
|
return (
|
||||||
|
prevProps.rowId === nextProps.rowId &&
|
||||||
|
prevProps.event.revision === nextProps.event.revision &&
|
||||||
|
prevProps.eventIndex === nextProps.eventIndex &&
|
||||||
|
prevProps.rowIndex === nextProps.rowIndex &&
|
||||||
|
prevProps.isPast === nextProps.isPast &&
|
||||||
|
prevProps.selectedRef === nextProps.selectedRef &&
|
||||||
|
prevProps.rowBgColour === nextProps.rowBgColour &&
|
||||||
|
prevProps.table === nextProps.table &&
|
||||||
|
prevProps.columnSizing === nextProps.columnSizing
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
function EventRow(props: EventRowProps) {
|
function EventRow(props: EventRowProps) {
|
||||||
const { rowId, eventId, eventIndex, rowIndex, isPast, selectedRef, skip, colour, rowBgColour, table } = props;
|
const { rowId, event, eventIndex, rowIndex, isPast, selectedRef, rowBgColour, table } = props;
|
||||||
const { hideIndexColumn, showActionMenu } = useCuesheetOptions();
|
const { hideIndexColumn, showActionMenu } = useCuesheetOptions();
|
||||||
const ownRef = useRef<HTMLTableRowElement>(null);
|
const ownRef = useRef<HTMLTableRowElement>(null);
|
||||||
const [isVisible, setIsVisible] = useState(false);
|
const [isVisible, setIsVisible] = useState(false);
|
||||||
@@ -63,23 +75,23 @@ function EventRow(props: EventRowProps) {
|
|||||||
};
|
};
|
||||||
}, [ownRef, selectedRef]);
|
}, [ownRef, selectedRef]);
|
||||||
|
|
||||||
const { color, backgroundColor } = getAccessibleColour(colour);
|
const { color, backgroundColor } = getAccessibleColour(event.colour);
|
||||||
const mutedText = Color(color).fade(0.4).hexa();
|
const mutedText = Color(color).fade(0.4).hexa();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<tr
|
<tr
|
||||||
className={cx([style.eventRow, skip ?? style.skip])}
|
className={cx([style.eventRow, event.skip ?? style.skip])}
|
||||||
style={{ opacity: `${isPast ? '0.2' : '1'}` }}
|
style={{ opacity: `${isPast ? '0.2' : '1'}` }}
|
||||||
ref={selectedRef ?? ownRef}
|
ref={selectedRef ?? ownRef}
|
||||||
>
|
>
|
||||||
{showActionMenu && (
|
{showActionMenu && (
|
||||||
<td className={style.actionColumn}>
|
<td className={style.actionColumn} tabIndex={-1} role='cell'>
|
||||||
<IconButton
|
<IconButton
|
||||||
aria-label='Options'
|
aria-label='Options'
|
||||||
onClick={(event) => {
|
onClick={(e) => {
|
||||||
const rect = event.currentTarget.getBoundingClientRect();
|
const rect = e.currentTarget.getBoundingClientRect();
|
||||||
const yPos = 8 + rect.y + rect.height / 2;
|
const yPos = 8 + rect.y + rect.height / 2;
|
||||||
openMenu({ x: rect.x, y: yPos }, eventId, rowIndex);
|
openMenu({ x: rect.x, y: yPos }, event.id, rowIndex);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<IoEllipsisHorizontal />
|
<IoEllipsisHorizontal />
|
||||||
@@ -87,7 +99,7 @@ function EventRow(props: EventRowProps) {
|
|||||||
</td>
|
</td>
|
||||||
)}
|
)}
|
||||||
{!hideIndexColumn && (
|
{!hideIndexColumn && (
|
||||||
<td className={style.indexColumn} style={{ backgroundColor, color: mutedText }}>
|
<td className={style.indexColumn} style={{ backgroundColor, color: mutedText }} tabIndex={-1} role='cell'>
|
||||||
{eventIndex}
|
{eventIndex}
|
||||||
</td>
|
</td>
|
||||||
)}
|
)}
|
||||||
@@ -97,7 +109,12 @@ function EventRow(props: EventRowProps) {
|
|||||||
?.getVisibleCells()
|
?.getVisibleCells()
|
||||||
.map((cell) => {
|
.map((cell) => {
|
||||||
return (
|
return (
|
||||||
<td key={cell.id} style={{ width: cell.column.getSize(), backgroundColor: rowBgColour }}>
|
<td
|
||||||
|
key={cell.id}
|
||||||
|
style={{ width: cell.column.getSize(), backgroundColor: rowBgColour }}
|
||||||
|
tabIndex={-1}
|
||||||
|
role='cell'
|
||||||
|
>
|
||||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||||
</td>
|
</td>
|
||||||
);
|
);
|
||||||
|
|||||||
+1
@@ -17,6 +17,7 @@ function MultiLineCell(props: MultiLineCellProps) {
|
|||||||
|
|
||||||
const { value, onChange, onBlur, onKeyDown } = useReactiveTextInput(initialValue, submitCallback, ref, {
|
const { value, onChange, onBlur, onKeyDown } = useReactiveTextInput(initialValue, submitCallback, ref, {
|
||||||
submitOnCtrlEnter: true,
|
submitOnCtrlEnter: true,
|
||||||
|
allowKeyboardNavigation: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
+4
@@ -17,6 +17,7 @@ const SingleLineCell = forwardRef((props: SingleLineCellProps, inputRef) => {
|
|||||||
|
|
||||||
const { value, onChange, onBlur, onKeyDown } = useReactiveTextInput(initialValue, submitCallback, ref, {
|
const { value, onChange, onBlur, onKeyDown } = useReactiveTextInput(initialValue, submitCallback, ref, {
|
||||||
allowSubmitSameValue,
|
allowSubmitSameValue,
|
||||||
|
allowKeyboardNavigation: true,
|
||||||
submitOnEnter: true, // single line should submit on enter
|
submitOnEnter: true, // single line should submit on enter
|
||||||
submitOnCtrlEnter: true,
|
submitOnCtrlEnter: true,
|
||||||
onCancelUpdate: handleCancelUpdate,
|
onCancelUpdate: handleCancelUpdate,
|
||||||
@@ -31,6 +32,9 @@ const SingleLineCell = forwardRef((props: SingleLineCellProps, inputRef) => {
|
|||||||
select() {
|
select() {
|
||||||
ref.current?.select();
|
ref.current?.select();
|
||||||
},
|
},
|
||||||
|
focusParentElement() {
|
||||||
|
ref.current?.parentElement?.focus();
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}, [ref]);
|
}, [ref]);
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -28,7 +28,7 @@ export function SortableCell({ header, style, children }: SortableCellProps) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<th ref={setNodeRef} style={dragStyle} colSpan={colSpan}>
|
<th ref={setNodeRef} style={dragStyle} colSpan={colSpan} tabIndex={-1}>
|
||||||
<div {...attributes} {...listeners}>
|
<div {...attributes} {...listeners}>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+18
-6
@@ -1,22 +1,34 @@
|
|||||||
import { HTMLAttributes, memo, PropsWithChildren } from 'react';
|
import { forwardRef, HTMLAttributes, memo, PropsWithChildren, useImperativeHandle, useRef } from 'react';
|
||||||
|
|
||||||
import { cx } from '../../../../common/utils/styleUtils';
|
import { cx } from '../../../../common/utils/styleUtils';
|
||||||
|
|
||||||
import style from './TextLikeInput.module.scss';
|
import style from './TextLikeInput.module.scss';
|
||||||
|
|
||||||
export default memo(TextLikeInput);
|
|
||||||
|
|
||||||
interface TextLikeInputProps extends HTMLAttributes<HTMLSpanElement> {
|
interface TextLikeInputProps extends HTMLAttributes<HTMLSpanElement> {
|
||||||
delayed?: boolean;
|
delayed?: boolean;
|
||||||
muted?: boolean;
|
muted?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function TextLikeInput(props: PropsWithChildren<TextLikeInputProps>) {
|
const TextLikeInput = forwardRef((props: PropsWithChildren<TextLikeInputProps>, textRef) => {
|
||||||
const { delayed, muted, children, className, ...elementProps } = props;
|
const { delayed, muted, children, className, ...elementProps } = props;
|
||||||
|
const ref = useRef<HTMLDivElement | null>(null);
|
||||||
const classes = cx([style.textInput, delayed && style.delayed, muted && style.muted, className]);
|
const classes = cx([style.textInput, delayed && style.delayed, muted && style.muted, className]);
|
||||||
|
|
||||||
|
useImperativeHandle(textRef, () => {
|
||||||
|
return {
|
||||||
|
focusParentElement() {
|
||||||
|
ref.current?.parentElement?.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classes} {...elementProps} tabIndex={0}>
|
<div className={classes} {...elementProps} tabIndex={0} ref={ref}>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
|
||||||
|
TextLikeInput.displayName = 'TextLikeInput';
|
||||||
|
|
||||||
|
export default memo(TextLikeInput);
|
||||||
|
|||||||
+22
-3
@@ -11,6 +11,10 @@ interface TimeInputDurationProps {
|
|||||||
onSubmit: (value: string) => void;
|
onSubmit: (value: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ParentFocusableInput extends HTMLInputElement {
|
||||||
|
focusParentElement: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
export default memo(TimeInputDuration);
|
export default memo(TimeInputDuration);
|
||||||
|
|
||||||
function TimeInputDuration(props: PropsWithChildren<TimeInputDurationProps>) {
|
function TimeInputDuration(props: PropsWithChildren<TimeInputDurationProps>) {
|
||||||
@@ -18,7 +22,8 @@ function TimeInputDuration(props: PropsWithChildren<TimeInputDurationProps>) {
|
|||||||
|
|
||||||
const [isEditing, setIsEditing] = useState(false);
|
const [isEditing, setIsEditing] = useState(false);
|
||||||
const [value, setValue] = useState(initialValue);
|
const [value, setValue] = useState(initialValue);
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
const inputRef = useRef<ParentFocusableInput>(null);
|
||||||
|
const textRef = useRef<ParentFocusableInput>(null);
|
||||||
|
|
||||||
// when we go into edit mode, set focus to the input
|
// when we go into edit mode, set focus to the input
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -36,7 +41,10 @@ function TimeInputDuration(props: PropsWithChildren<TimeInputDurationProps>) {
|
|||||||
}, [initialValue, isEditing]);
|
}, [initialValue, isEditing]);
|
||||||
|
|
||||||
const handleFakeFocus = () => setIsEditing(true);
|
const handleFakeFocus = () => setIsEditing(true);
|
||||||
const handleFakeBlur = () => setIsEditing(false);
|
const handleFakeBlur = () => {
|
||||||
|
setIsEditing(false);
|
||||||
|
setTimeout(() => textRef.current?.focusParentElement()); // Immediate timeout to ensure state change takes place first
|
||||||
|
};
|
||||||
|
|
||||||
const handleUpdate = useCallback(
|
const handleUpdate = useCallback(
|
||||||
(newValue: string) => {
|
(newValue: string) => {
|
||||||
@@ -45,28 +53,33 @@ function TimeInputDuration(props: PropsWithChildren<TimeInputDurationProps>) {
|
|||||||
// if the user sends an empty string, we want to clear the value
|
// if the user sends an empty string, we want to clear the value
|
||||||
if (newValue === '') {
|
if (newValue === '') {
|
||||||
onSubmit(newValue);
|
onSubmit(newValue);
|
||||||
|
inputRef.current?.focusParentElement();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// we dont know the values in the rundown, escalate to handler
|
// we dont know the values in the rundown, escalate to handler
|
||||||
if (newValue.startsWith('p') || newValue.startsWith('+')) {
|
if (newValue.startsWith('p') || newValue.startsWith('+')) {
|
||||||
onSubmit(newValue);
|
onSubmit(newValue);
|
||||||
|
inputRef.current?.focusParentElement();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const valueInMillis = parseUserTime(newValue);
|
const valueInMillis = parseUserTime(newValue);
|
||||||
if (valueInMillis < 0 || isNaN(valueInMillis)) {
|
if (valueInMillis < 0 || isNaN(valueInMillis)) {
|
||||||
setValue(initialValue);
|
setValue(initialValue);
|
||||||
|
setTimeout(() => textRef.current?.focusParentElement()); // Immediate timeout to ensure state change takes place first
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// if the value is the same, we may still want to push the lock change
|
// if the value is the same, we may still want to push the lock change
|
||||||
if (valueInMillis === initialValue && lockedValue) {
|
if (valueInMillis === initialValue && lockedValue) {
|
||||||
|
inputRef.current?.focusParentElement();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
onSubmit(newValue);
|
onSubmit(newValue);
|
||||||
setValue(Number(newValue));
|
setValue(Number(newValue));
|
||||||
|
setTimeout(() => textRef.current?.focusParentElement()); // Immediate timeout to ensure state change takes place first
|
||||||
},
|
},
|
||||||
[initialValue, lockedValue, onSubmit],
|
[initialValue, lockedValue, onSubmit],
|
||||||
);
|
);
|
||||||
@@ -82,7 +95,13 @@ function TimeInputDuration(props: PropsWithChildren<TimeInputDurationProps>) {
|
|||||||
handleCancelUpdate={handleFakeBlur}
|
handleCancelUpdate={handleFakeBlur}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<TextLikeInput onClick={handleFakeFocus} onFocus={handleFakeFocus} muted={!lockedValue} delayed={delayed}>
|
<TextLikeInput
|
||||||
|
onClick={handleFakeFocus}
|
||||||
|
onFocus={handleFakeFocus}
|
||||||
|
muted={!lockedValue}
|
||||||
|
delayed={delayed}
|
||||||
|
ref={textRef}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</TextLikeInput>
|
</TextLikeInput>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ontime-electron",
|
"name": "ontime-electron",
|
||||||
"version": "3.13.1",
|
"version": "3.14.0",
|
||||||
"author": "Carlos Valente",
|
"author": "Carlos Valente",
|
||||||
"description": "Time keeping for live events",
|
"description": "Time keeping for live events",
|
||||||
"repository": "https://github.com/cpvalente/ontime",
|
"repository": "https://github.com/cpvalente/ontime",
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"name": "ontime-server",
|
"name": "ontime-server",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"version": "3.13.1",
|
"version": "3.14.0",
|
||||||
"exports": "./src/index.js",
|
"exports": "./src/index.js",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@googleapis/sheets": "^5.0.5",
|
"@googleapis/sheets": "^5.0.5",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ontime",
|
"name": "ontime",
|
||||||
"version": "3.13.1",
|
"version": "3.14.0",
|
||||||
"description": "Time keeping for live events",
|
"description": "Time keeping for live events",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"ontime",
|
"ontime",
|
||||||
|
|||||||
Generated
+17
@@ -119,6 +119,9 @@ importers:
|
|||||||
'@sentry/react':
|
'@sentry/react':
|
||||||
specifier: ^8.43.0
|
specifier: ^8.43.0
|
||||||
version: 8.45.0(react@18.3.1)
|
version: 8.45.0(react@18.3.1)
|
||||||
|
'@table-nav/react':
|
||||||
|
specifier: ^0.0.7
|
||||||
|
version: 0.0.7(@table-nav/core@0.0.7)
|
||||||
'@tanstack/react-query':
|
'@tanstack/react-query':
|
||||||
specifier: ^5.62.7
|
specifier: ^5.62.7
|
||||||
version: 5.62.7(react@18.3.1)
|
version: 5.62.7(react@18.3.1)
|
||||||
@@ -1949,6 +1952,14 @@ packages:
|
|||||||
resolution: {integrity: sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==}
|
resolution: {integrity: sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==}
|
||||||
engines: {node: '>=14.16'}
|
engines: {node: '>=14.16'}
|
||||||
|
|
||||||
|
'@table-nav/core@0.0.7':
|
||||||
|
resolution: {integrity: sha512-pCh18jHDRe3tw9sJZXfKi4cSD6VjHbn40CYdqhp5X91SIX7rakDEQAsTx6F7Fv9TUv265l+5rUDcYNaJ0N0cqQ==}
|
||||||
|
|
||||||
|
'@table-nav/react@0.0.7':
|
||||||
|
resolution: {integrity: sha512-S+DsD/qDqp50Z4dqt5tZFMWA3sRu0OOT/grMQuq/z/52jPEKJB+b9t+YSH8Ms55vCJOJ0DxuYldJpYrJLMG5ew==}
|
||||||
|
peerDependencies:
|
||||||
|
'@table-nav/core': ^0.0.7
|
||||||
|
|
||||||
'@tanstack/eslint-plugin-query@5.8.4':
|
'@tanstack/eslint-plugin-query@5.8.4':
|
||||||
resolution: {integrity: sha512-KVgcMc+Bn1qbwkxYVWQoiVSNEIN4IAiLj3cUH/SAHT8m8E59Y97o8ON1syp0Rcw094ItG8pEVZFyQuOaH6PDgQ==}
|
resolution: {integrity: sha512-KVgcMc+Bn1qbwkxYVWQoiVSNEIN4IAiLj3cUH/SAHT8m8E59Y97o8ON1syp0Rcw094ItG8pEVZFyQuOaH6PDgQ==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -6698,6 +6709,12 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
defer-to-connect: 2.0.1
|
defer-to-connect: 2.0.1
|
||||||
|
|
||||||
|
'@table-nav/core@0.0.7': {}
|
||||||
|
|
||||||
|
'@table-nav/react@0.0.7(@table-nav/core@0.0.7)':
|
||||||
|
dependencies:
|
||||||
|
'@table-nav/core': 0.0.7
|
||||||
|
|
||||||
'@tanstack/eslint-plugin-query@5.8.4(eslint@8.56.0)(typescript@5.5.3)':
|
'@tanstack/eslint-plugin-query@5.8.4(eslint@8.56.0)(typescript@5.5.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@typescript-eslint/utils': 5.62.0(eslint@8.56.0)(typescript@5.5.3)
|
'@typescript-eslint/utils': 5.62.0(eslint@8.56.0)(typescript@5.5.3)
|
||||||
|
|||||||
Reference in New Issue
Block a user