mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-18 21:54:09 +00:00
ae15f3cdc5
* refactor: align header columns * refactor: improve scrollbar visibility * refactor: center align table elements * refactor: make param elements stateful * fix: issue with collapsed elements not loosing value * fix: prevent search params containing multiple alias references * fix: the issue where a file disappears if it is both migrated and recovered in the same load operation (#1744) * refactor: disable group action for elements in groups * refactor: move context menu items into the event element (#1747) * feat: sheet import new features for v4 (#1730) * import milestone * fixup! import milestone * test: milestone import * add entries to group stop on new group or on group-end type * fixup! add entries to group * cleanup * add event target duration * link start if undefined * add skip import type * extract some to the excel paresing functions * tweaks to presentation * move file --------- Co-authored-by: Carlos Valente <carlosvalente@pm.me> * fix: notify runtimeStore of events bieng groupd * fix: improve authentication and stage detection in demo * chore: ship logo with project * fix: client is referenced by name * fix: prevent reflow in event editor * fix: stale render on selected event due to ref mismatch * Create/Load/Delete multiple rundowns (#1696) * refactor: restore last loaded rundown * refactor: initialise rundown in ProjectService * feat: allow switching rundowns ensure on coordination between the db object and the working object server provide list of rundowns implement switch in the UI implement delete implement new rundown button * fix: render order for floating button * refactor: appropriate names to service * refactor: rundown endpoints * refactor: save last loaded rundown ID * refactor: rundown management UI * refactor: emit refetch all on project load --------- Co-authored-by: Carlos Valente <carlosvalente@pm.me> * feat: recover single event subscription * fixup! feat: sheet import new features for v4 (#1730) * fix: prevent dropping a group inside another * fixup! refactor: move context menu items into the event element (#1747) * fix: prevent stale references to custom fields * fix: propagate updates to all rundowns * refactor: client rundown metadata (#1728) * generate metadata in the hook * move test * ensure there is always a last element * use for-loop * update metadata in useEfect * fully extract metadata generation * use direct assignment * cleanup --------- Co-authored-by: Carlos Valente <carlosvalente@pm.me> * refactor: small imporvement and tests for coerce functions (#1752) * refactor: small imporvement and tests for coerce functions add test `coerceString` add test `coerceBoolean` add test `coerceColour` * remove old todo * fix: consistent quick add behaviour * refactor: create flat rundown with metadata * fix: show add buttons on top * feat: allow editing milestones * refactor: style tweaks to rundown elements refactor: milestones are full width refactor: cuesheet header alignment fix: editor styling in cuesheet * refactor: virtualise table * refactor: improve overscan (#1758) * bump version to 4.0.0-alpha.5 --------- Co-authored-by: Alex Christoffer Rasmussen <ac@omnivox.dk>
247 lines
8.6 KiB
TypeScript
247 lines
8.6 KiB
TypeScript
import { useCallback, useEffect, useMemo, useRef } from 'react';
|
|
import { TableVirtuoso, TableVirtuosoHandle } from 'react-virtuoso';
|
|
import { useTableNav } from '@table-nav/react';
|
|
import { ColumnDef, getCoreRowModel, useReactTable } from '@tanstack/react-table';
|
|
import { isOntimeDelay, isOntimeGroup, isOntimeMilestone, OntimeEntry, TimeField } from 'ontime-types';
|
|
|
|
import EmptyPage from '../../../common/components/state/EmptyPage';
|
|
import EmptyTableBody from '../../../common/components/state/EmptyTableBody';
|
|
import { useEntryActions } from '../../../common/hooks/useEntryAction';
|
|
import { useSelectedEventId } from '../../../common/hooks/useSocket';
|
|
import { useFlatRundownWithMetadata } from '../../../common/hooks-query/useRundown';
|
|
import type { ExtendedEntry } from '../../../common/utils/rundownMetadata';
|
|
import { AppMode } from '../../../ontimeConfig';
|
|
import { usePersistedCuesheetOptions } from '../cuesheet.options';
|
|
|
|
import CuesheetHeader from './cuesheet-table-elements/CuesheetHeader';
|
|
import DelayRow from './cuesheet-table-elements/DelayRow';
|
|
import EventRow from './cuesheet-table-elements/EventRow';
|
|
import GroupRow from './cuesheet-table-elements/GroupRow';
|
|
import MilestoneRow from './cuesheet-table-elements/MilestoneRow';
|
|
import CuesheetTableMenu from './cuesheet-table-menu/CuesheetTableMenu';
|
|
import CuesheetTableSettings from './cuesheet-table-settings/CuesheetTableSettings';
|
|
import useColumnManager from './useColumnManager';
|
|
|
|
import style from './CuesheetTable.module.scss';
|
|
|
|
interface CuesheetTableProps {
|
|
columns: ColumnDef<ExtendedEntry>[];
|
|
cuesheetMode: AppMode;
|
|
}
|
|
|
|
export default function CuesheetTable({ columns, cuesheetMode }: CuesheetTableProps) {
|
|
const { data, status } = useFlatRundownWithMetadata();
|
|
const { updateEntry, updateTimer } = useEntryActions();
|
|
const showDelayedTimes = usePersistedCuesheetOptions((state) => state.showDelayedTimes);
|
|
const hideTableSeconds = usePersistedCuesheetOptions((state) => state.hideTableSeconds);
|
|
const hideIndexColumn = usePersistedCuesheetOptions((state) => state.hideIndexColumn);
|
|
|
|
const { selectedEventId } = useSelectedEventId();
|
|
|
|
const virtuosoRef = useRef<TableVirtuosoHandle | null>(null);
|
|
const { listeners } = useTableNav();
|
|
|
|
const meta = useMemo(
|
|
() => ({
|
|
handleUpdate: (rowIndex: number, accessor: string, payload: string, isCustom = false) => {
|
|
// check if value is the same
|
|
const event = data[rowIndex];
|
|
|
|
if (!event) {
|
|
return;
|
|
}
|
|
|
|
// skip if there is no value change
|
|
const key = accessor as keyof OntimeEntry;
|
|
const previousValue = event[key];
|
|
if (previousValue === payload) {
|
|
return;
|
|
}
|
|
|
|
if (isCustom) {
|
|
updateEntry({ id: event.id, custom: { [accessor]: payload } });
|
|
return;
|
|
}
|
|
|
|
updateEntry({ id: event.id, [accessor]: payload });
|
|
},
|
|
handleUpdateTimer: (eventId: string, field: TimeField, payload: string) => {
|
|
// the timer element already contains logic to avoid submitting a unchanged value
|
|
updateTimer(eventId, field, payload, true);
|
|
},
|
|
options: {
|
|
showDelayedTimes,
|
|
hideTableSeconds,
|
|
cuesheetMode,
|
|
hideIndexColumn,
|
|
},
|
|
}),
|
|
[cuesheetMode, data, hideIndexColumn, hideTableSeconds, showDelayedTimes, updateEntry, updateTimer],
|
|
);
|
|
|
|
const { columnVisibility, columnOrder, columnSizing, resetColumnOrder, setColumnVisibility, setColumnSizing } =
|
|
useColumnManager(columns);
|
|
|
|
const table = useReactTable({
|
|
data,
|
|
columns,
|
|
columnResizeMode: 'onChange',
|
|
state: {
|
|
columnOrder,
|
|
columnVisibility,
|
|
columnSizing,
|
|
},
|
|
onColumnVisibilityChange: setColumnVisibility,
|
|
onColumnSizingChange: setColumnSizing,
|
|
getCoreRowModel: getCoreRowModel(),
|
|
meta,
|
|
});
|
|
|
|
const setAllVisible = useCallback(() => {
|
|
table.toggleAllColumnsVisible(true);
|
|
}, [table]);
|
|
|
|
const resetColumnResizing = useCallback(() => {
|
|
setColumnSizing({});
|
|
}, [setColumnSizing]);
|
|
|
|
// in run mode, we follow the selected row
|
|
useEffect(() => {
|
|
if (cuesheetMode === AppMode.Edit || virtuosoRef.current === null || !selectedEventId) {
|
|
return;
|
|
}
|
|
|
|
const eventIndex = data.findIndex((event) => event.id === selectedEventId);
|
|
virtuosoRef.current.scrollToIndex({ index: eventIndex, behavior: 'smooth' });
|
|
}, [cuesheetMode, data, selectedEventId]);
|
|
|
|
/**
|
|
* To improve performance on resizing, we memoise the column sizes
|
|
* and pass them as CSS variables to the table container.
|
|
*/
|
|
const columnSizeVars = useMemo(() => {
|
|
const headers = table.getFlatHeaders();
|
|
const colSizes: { [key: string]: number } = {};
|
|
for (let i = 0; i < headers.length; i++) {
|
|
const header = headers[i];
|
|
if (!header) continue;
|
|
colSizes[`--header-${header.id}-size`] = header.getSize();
|
|
colSizes[`--col-${header.column.id}-size`] = header.column.getSize();
|
|
}
|
|
return colSizes;
|
|
// eslint-disable-next-line react-compiler/react-compiler -- unfortunately this is what we need
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps -- this works well and follows documentation
|
|
}, [table.getState().columnSizingInfo, table.getState().columnSizing]);
|
|
|
|
const allLeafColumns = table.getAllLeafColumns();
|
|
const { rows } = table.getRowModel();
|
|
|
|
const isLoading = !data || status === 'pending';
|
|
|
|
if (isLoading) {
|
|
return <EmptyPage text='Loading...' />;
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<CuesheetTableSettings
|
|
columns={allLeafColumns}
|
|
handleResetResizing={resetColumnResizing}
|
|
handleResetReordering={resetColumnOrder}
|
|
handleClearToggles={setAllVisible}
|
|
/>
|
|
<TableVirtuoso
|
|
ref={virtuosoRef}
|
|
data={data}
|
|
increaseViewportBy={{ top: 100, bottom: 200 }}
|
|
components={{
|
|
EmptyPlaceholder: () => <EmptyTableBody text='No data in rundown' />,
|
|
Table: ({ style: injectedStyles, ...virtuosoProps }) => {
|
|
return (
|
|
<table
|
|
className={style.cuesheet}
|
|
id='cuesheet'
|
|
style={{ ...injectedStyles, ...columnSizeVars }}
|
|
{...listeners}
|
|
{...virtuosoProps}
|
|
/>
|
|
);
|
|
},
|
|
TableRow: ({ item: _item, ...virtuosoProps }) => {
|
|
// eslint-disable-next-line react/destructuring-assignment
|
|
const rowIndex = virtuosoProps['data-index'];
|
|
const row = rows[rowIndex];
|
|
const key = row.original.id;
|
|
const entry = row.original;
|
|
|
|
if (isOntimeGroup(entry)) {
|
|
return (
|
|
<GroupRow
|
|
key={key}
|
|
groupId={entry.id}
|
|
colour={entry.colour}
|
|
rowId={row.id}
|
|
rowIndex={row.index}
|
|
table={table}
|
|
{...virtuosoProps}
|
|
/>
|
|
);
|
|
}
|
|
|
|
if (isOntimeDelay(entry)) {
|
|
return <DelayRow key={key} duration={entry.duration} {...virtuosoProps} />;
|
|
}
|
|
|
|
if (isOntimeMilestone(entry)) {
|
|
return (
|
|
<MilestoneRow
|
|
key={key}
|
|
entryId={entry.id}
|
|
isPast={entry.isPast}
|
|
parentBgColour={entry.groupColour}
|
|
parentId={entry.parent}
|
|
colour={entry.colour}
|
|
rowId={row.id}
|
|
rowIndex={rowIndex}
|
|
table={table}
|
|
{...virtuosoProps}
|
|
/>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<EventRow
|
|
key={row.id}
|
|
id={entry.id}
|
|
eventIndex={entry.eventIndex}
|
|
colour={entry.colour}
|
|
isFirstAfterGroup={entry.isFirstAfterGroup}
|
|
isLoaded={entry.isLoaded}
|
|
isPast={entry.isPast}
|
|
groupColour={entry.groupColour}
|
|
flag={entry.flag}
|
|
skip={entry.skip}
|
|
parent={entry.parent}
|
|
rowId={row.id}
|
|
rowIndex={rowIndex}
|
|
table={table}
|
|
{...virtuosoProps}
|
|
/>
|
|
);
|
|
},
|
|
TableHead: (virtuosoProps) => <thead className={style.tableHeader} {...virtuosoProps} />,
|
|
}}
|
|
fixedHeaderContent={() => {
|
|
return table
|
|
.getHeaderGroups()
|
|
.map((headerGroup) => (
|
|
<CuesheetHeader key={headerGroup.id} cuesheetMode={cuesheetMode} headerGroup={headerGroup} />
|
|
));
|
|
}}
|
|
/>
|
|
|
|
<CuesheetTableMenu />
|
|
</>
|
|
);
|
|
}
|