diff --git a/apps/client/package.json b/apps/client/package.json index 7b26d096e..8e01f1ca0 100644 --- a/apps/client/package.json +++ b/apps/client/package.json @@ -18,7 +18,7 @@ "@table-nav/react": "^0.0.7", "@tanstack/react-query": "^5.101.0", "@tanstack/react-query-devtools": "^5.101.0", - "@tanstack/react-table": "^8.21.3", + "@tanstack/react-table": "^9.1.2", "@uiw/codemirror-theme-vscode": "^4.25.10", "autosize": "^6.0.1", "axios": "^1.18.0", diff --git a/apps/client/src/declarations/declaration.d.ts b/apps/client/src/declarations/declaration.d.ts index 08bfc0861..40da1ef1a 100644 --- a/apps/client/src/declarations/declaration.d.ts +++ b/apps/client/src/declarations/declaration.d.ts @@ -1,5 +1,3 @@ -import { AppMode } from '../ontimeConfig'; - declare module '*.scss' { const content: Record; export default content; @@ -32,39 +30,6 @@ declare global { } } -/** - * Declare custom data we pass to the table - * - `handleUpdate` callback to update the entry when the user edits a cell - * - `handleUpdateTimer` callback to update the timer for a specific event - * - `options-showDelayedTimes` whether to show or hide delayed times - * - `options-hideTableSeconds` whether to hide seconds in the table - * - `options-hideIndexColumn` whether to hide the index column - * - `options-cuesheetMode` run or edit mode - * - * And metadata specific for each column - * - `canWrite` whether the user can write to this column - * - `colour` background colour associated with a custom field - */ -declare module '@tanstack/react-table' { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - interface TableMeta { - handleUpdate: (rowIndex: number, accessor: string, payload: string, isCustom: boolean) => void; - handleUpdateTimer: (eventId: string, field: TimeField, payload: string) => void; - options: { - showDelayedTimes: boolean; - hideTableSeconds: boolean; - hideIndexColumn: boolean; - cuesheetMode: AppMode; - }; - } - - // eslint-disable-next-line @typescript-eslint/no-unused-vars - interface ColumnMeta { - canWrite: boolean; - colour?: string; - } -} - /** * Allow passing CSS Properties */ diff --git a/apps/client/src/features/rundown/rundown-table/makeRundownColumns.tsx b/apps/client/src/features/rundown/rundown-table/makeRundownColumns.tsx index 85e611e23..88fe0d6e0 100644 --- a/apps/client/src/features/rundown/rundown-table/makeRundownColumns.tsx +++ b/apps/client/src/features/rundown/rundown-table/makeRundownColumns.tsx @@ -1,15 +1,14 @@ -import type { ColumnDef } from '@tanstack/react-table'; import type { CustomFields } from 'ontime-types'; -import type { ExtendedEntry } from '../../../common/utils/rundownMetadata'; import { AppMode } from '../../../ontimeConfig'; import { makeCuesheetColumns } from '../../../views/cuesheet/cuesheet-table/cuesheet-table-elements/cuesheetColsFactory'; +import type { CuesheetColumnDef } from '../../../views/cuesheet/cuesheet-table/cuesheetTable.features'; /** * Creates column definitions for the rundown table * Reuses cuesheetColsFactory with preset=undefined for full access */ -export function makeRundownColumns(customFields: CustomFields): ColumnDef[] { +export function makeRundownColumns(customFields: CustomFields): CuesheetColumnDef[] { // When preset=undefined, factory defaults to fullRead=true, fullWrite=true // canWrite is determined by editorMode (AppMode.Edit vs AppMode.Run) return makeCuesheetColumns(customFields, AppMode.Edit, undefined); diff --git a/apps/client/src/views/cuesheet/cuesheet-dnd/CuesheetDnd.tsx b/apps/client/src/views/cuesheet/cuesheet-dnd/CuesheetDnd.tsx index 6e3734694..5090e2c2b 100644 --- a/apps/client/src/views/cuesheet/cuesheet-dnd/CuesheetDnd.tsx +++ b/apps/client/src/views/cuesheet/cuesheet-dnd/CuesheetDnd.tsx @@ -7,14 +7,13 @@ import { useSensor, useSensors, } from '@dnd-kit/core'; -import { ColumnDef } from '@tanstack/react-table'; import { PropsWithChildren } from 'react'; -import type { ExtendedEntry } from '../../../common/utils/rundownMetadata'; +import type { CuesheetColumnDef } from '../cuesheet-table/cuesheetTable.features'; import { useColumnOrder } from '../cuesheet-table/useColumnManager'; interface CuesheetDndProps { - columns: ColumnDef[]; + columns: CuesheetColumnDef[]; tableRoot?: 'editor' | 'cuesheet'; } diff --git a/apps/client/src/views/cuesheet/cuesheet-table/CuesheetTable.tsx b/apps/client/src/views/cuesheet/cuesheet-table/CuesheetTable.tsx index 075f0b898..5b3222790 100644 --- a/apps/client/src/views/cuesheet/cuesheet-table/CuesheetTable.tsx +++ b/apps/client/src/views/cuesheet/cuesheet-table/CuesheetTable.tsx @@ -1,5 +1,5 @@ import { useTableNav } from '@table-nav/react'; -import { ColumnDef, Table, getCoreRowModel, useReactTable } from '@tanstack/react-table'; +import { useTable } from '@tanstack/react-table'; import { OntimeEntry, SupportedEntry, TimeField, isOntimeDelay, isOntimeGroup, isOntimeMilestone } from 'ontime-types'; import { ComponentProps, ReactNode, memo, useCallback, useEffect, useMemo, useRef } from 'react'; import { @@ -29,12 +29,17 @@ import GroupRow from './cuesheet-table-elements/GroupRow'; import MilestoneRow from './cuesheet-table-elements/MilestoneRow'; import TableMenu from './cuesheet-table-menu/TableMenu'; import CuesheetTableHeaderToolbar from './cuesheet-table-settings/CuesheetTableHeaderToolbar'; +import { + CuesheetColumnDef, + CuesheetTable as CuesheetTableInstance, + cuesheetTableFeatures, +} from './cuesheetTable.features'; import { useColumnOrder, useColumnSizes, useColumnVisibility } from './useColumnManager'; import style from './CuesheetTable.module.scss'; type CuesheetTableBaseProps = { - columns: ColumnDef[]; + columns: CuesheetColumnDef[]; cuesheetMode: AppMode; source: RundownSource; insertElement?: ReactNode; @@ -120,7 +125,8 @@ export default function CuesheetTable({ const { columnSizing, setColumnSizing } = useColumnSizes(tableRoot); const { columnVisibility, setColumnVisibility } = useColumnVisibility(tableRoot); - const table = useReactTable({ + const table = useTable({ + features: cuesheetTableFeatures, data: flatRundown, columns, columnResizeMode: 'onChange', @@ -131,7 +137,6 @@ export default function CuesheetTable({ }, onColumnVisibilityChange: setColumnVisibility, onColumnSizingChange: setColumnSizing, - getCoreRowModel: getCoreRowModel(), meta, }); @@ -195,7 +200,7 @@ export default function CuesheetTable({ 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]); + }, [table.state.columnResizing, table.state.columnSizing]); const allLeafColumns = table.getAllLeafColumns(); const { rows } = table.getRowModel(); @@ -214,9 +219,7 @@ export default function CuesheetTable({ const computeItemKey = useCallback((_: number, item: ExtendedEntry) => item.id, []); const fixedHeaderContent = useCallback(() => { return table.getHeaderGroups().map((headerGroup) => { - const HeaderComponent = table.getState().columnSizingInfo.isResizingColumn - ? CuesheetHeader - : SortableCuesheetHeader; + const HeaderComponent = table.state.columnResizing.isResizingColumn ? CuesheetHeader : SortableCuesheetHeader; // if the table is being resized, we render non-sortable headers to avoid performance issues return ( @@ -279,8 +282,8 @@ interface CuesheetVirtuosoContext { columnSizeVars: { [key: string]: number }; cursor: string | null; listeners: ReturnType['listeners']; - rows: ReturnType['getRowModel']>['rows']; - table: Table; + rows: ReturnType['rows']; + table: CuesheetTableInstance; handleAddNew?: (type: SupportedEntry) => void; } diff --git a/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-elements/CuesheetHeader.tsx b/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-elements/CuesheetHeader.tsx index 63b031bc7..48174d713 100644 --- a/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-elements/CuesheetHeader.tsx +++ b/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-elements/CuesheetHeader.tsx @@ -1,16 +1,16 @@ import { SortableContext, horizontalListSortingStrategy } from '@dnd-kit/sortable'; -import { HeaderGroup, flexRender } from '@tanstack/react-table'; +import { FlexRender } from '@tanstack/react-table'; import { CSSProperties } from 'react'; -import type { ExtendedEntry } from '../../../../common/utils/rundownMetadata'; import { getAccessibleColour } from '../../../../common/utils/styleUtils'; import { AppMode } from '../../../../ontimeConfig'; +import type { CuesheetHeaderGroup } from '../cuesheetTable.features'; import { Draggable, SortableCell, TableCell } from './SortableCell'; import style from '../CuesheetTable.module.scss'; interface CuesheetHeaderProps { - headerGroup: HeaderGroup; + headerGroup: CuesheetHeaderGroup; cuesheetMode: AppMode; hideIndexColumn: boolean; } @@ -46,7 +46,7 @@ export function SortableCuesheetHeader({ headerGroup, cuesheetMode, hideIndexCol injectedStyles={{ width: `calc(var(--header-${header?.id}-size) * 1px)`, ...customStyles }} draggable={} > - {header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())} + {header.isPlaceholder ? null : } ); })} @@ -85,7 +85,7 @@ export function CuesheetHeader({ headerGroup, cuesheetMode, hideIndexColumn }: C injectedStyles={{ width: `calc(var(--header-${header?.id}-size) * 1px)`, ...customStyles }} draggable={} > - {header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())} + {header.isPlaceholder ? null : } ); })} diff --git a/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-elements/EventRow.tsx b/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-elements/EventRow.tsx index c142ba03a..d77bc7821 100644 --- a/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-elements/EventRow.tsx +++ b/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-elements/EventRow.tsx @@ -1,14 +1,14 @@ -import { Table, flexRender } from '@tanstack/react-table'; -import { EntryId, OntimeEntry, RGBColour, SupportedEntry } from 'ontime-types'; +import { FlexRender } from '@tanstack/react-table'; +import { EntryId, RGBColour, SupportedEntry } from 'ontime-types'; import { colourToHex, cssOrHexToColour } from 'ontime-utils'; import { CSSProperties, memo, useMemo } from 'react'; import { IoEllipsisHorizontal } from 'react-icons/io5'; import IconButton from '../../../../common/components/buttons/IconButton'; -import type { ExtendedEntry } from '../../../../common/utils/rundownMetadata'; import { cx, getAccessibleColour } from '../../../../common/utils/styleUtils'; import { AppMode } from '../../../../ontimeConfig'; import { useCuesheetTableMenu } from '../cuesheet-table-menu/useCuesheetTableMenu'; +import type { CuesheetTable } from '../cuesheetTable.features'; import style from './EventRow.module.scss'; @@ -25,7 +25,7 @@ interface EventRowProps { skip: boolean; parent: EntryId | null; rowIndex: number; - table: Table>; + table: CuesheetTable; injectedStyles?: CSSProperties; hasCursor?: boolean; } @@ -133,7 +133,7 @@ function EventRow({ data-testid={`cuesheet-cell-${cell.column.id}`} data-column-id={cell.column.id} > - {flexRender(cell.column.columnDef.cell, cell.getContext())} + ); })} diff --git a/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-elements/GroupRow.tsx b/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-elements/GroupRow.tsx index 6a7d73fff..93e13f977 100644 --- a/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-elements/GroupRow.tsx +++ b/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-elements/GroupRow.tsx @@ -1,12 +1,12 @@ -import { Table, flexRender } from '@tanstack/react-table'; +import { FlexRender } from '@tanstack/react-table'; import { EntryId, SupportedEntry } from 'ontime-types'; import { CSSProperties, memo } from 'react'; import { IoEllipsisHorizontal } from 'react-icons/io5'; import IconButton from '../../../../common/components/buttons/IconButton'; -import type { ExtendedEntry } from '../../../../common/utils/rundownMetadata'; import { AppMode } from '../../../../ontimeConfig'; import { useCuesheetTableMenu } from '../cuesheet-table-menu/useCuesheetTableMenu'; +import type { CuesheetTable } from '../cuesheetTable.features'; import style from './GroupRow.module.scss'; @@ -15,7 +15,7 @@ interface GroupRowProps { colour: string; rowId: string; rowIndex: number; - table: Table; + table: CuesheetTable; injectedStyles?: CSSProperties; hasCursor?: boolean; } @@ -76,7 +76,7 @@ function GroupRow({ }} role='cell' > - {flexRender(cell.column.columnDef.cell, cell.getContext())} + ); })} diff --git a/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-elements/MilestoneRow.tsx b/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-elements/MilestoneRow.tsx index 611ddf4e8..0567641ff 100644 --- a/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-elements/MilestoneRow.tsx +++ b/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-elements/MilestoneRow.tsx @@ -1,14 +1,14 @@ -import { Table, flexRender } from '@tanstack/react-table'; +import { FlexRender } from '@tanstack/react-table'; import { EntryId, SupportedEntry } from 'ontime-types'; import { colourToHex, cssOrHexToColour } from 'ontime-utils'; import { CSSProperties, memo, useMemo } from 'react'; import { IoEllipsisHorizontal } from 'react-icons/io5'; import IconButton from '../../../../common/components/buttons/IconButton'; -import type { ExtendedEntry } from '../../../../common/utils/rundownMetadata'; import { cx, enDash, getAccessibleColour } from '../../../../common/utils/styleUtils'; import { AppMode } from '../../../../ontimeConfig'; import { useCuesheetTableMenu } from '../cuesheet-table-menu/useCuesheetTableMenu'; +import type { CuesheetTable } from '../cuesheetTable.features'; import style from './MilestoneRow.module.scss'; @@ -20,7 +20,7 @@ interface MilestoneRowProps { colour: string; rowId: string; rowIndex: number; - table: Table; + table: CuesheetTable; injectedStyles?: CSSProperties; hasCursor?: boolean; } @@ -102,7 +102,7 @@ function MilestoneRow({ }} tabIndex={-1} > - {canRender && flexRender(cell.column.columnDef.cell, cell.getContext())} + {canRender && } ); })} diff --git a/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-elements/SortableCell.tsx b/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-elements/SortableCell.tsx index b43c2d9ce..41bf6ebb5 100644 --- a/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-elements/SortableCell.tsx +++ b/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-elements/SortableCell.tsx @@ -1,9 +1,8 @@ import { useSortable } from '@dnd-kit/sortable'; import { CSS } from '@dnd-kit/utilities'; -import { Header } from '@tanstack/react-table'; import { CSSProperties, ReactNode } from 'react'; -import type { ExtendedEntry } from '../../../../common/utils/rundownMetadata'; +import type { CuesheetHeaderCell } from '../cuesheetTable.features'; import style from '../CuesheetTable.module.scss'; @@ -48,7 +47,7 @@ export function TableCell({ colSpan, injectedStyles, children, draggable }: Sort } interface DraggableProps { - header: Header; + header: CuesheetHeaderCell; } export function Draggable({ header }: DraggableProps) { diff --git a/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-elements/cuesheetColsFactory.tsx b/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-elements/cuesheetColsFactory.tsx index e143ee2f7..9c7a8a463 100644 --- a/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-elements/cuesheetColsFactory.tsx +++ b/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-elements/cuesheetColsFactory.tsx @@ -1,4 +1,3 @@ -import { CellContext, ColumnDef } from '@tanstack/react-table'; import { CustomFields, TimeStrategy, URLPreset, isOntimeDelay, isOntimeEvent } from 'ontime-types'; import { millisToString } from 'ontime-utils'; import { useCallback } from 'react'; @@ -8,6 +7,7 @@ import type { ExtendedEntry } from '../../../../common/utils/rundownMetadata'; import { formatDuration, formatTime } from '../../../../common/utils/time'; import { AppMode } from '../../../../ontimeConfig'; import { getCuesheetColumnAccessPolicy } from '../../cuesheet.policies'; +import type { CuesheetCellContext, CuesheetColumnDef } from '../cuesheetTable.features'; import DurationInput from './DurationInput'; import EditableImage from './EditableImage'; import FlagCell from './FlagCell'; @@ -17,11 +17,11 @@ import MutedText from './MutedText'; import SingleLineCell from './SingleLineCell'; import TimeInput from './TimeInput'; -function getColumnLabel(column: CellContext['column']): string { +function getColumnLabel(column: CuesheetCellContext['column']): string { return typeof column.columnDef.header === 'string' ? column.columnDef.header : column.id; } -function MakeStart({ getValue, row, table, column }: CellContext) { +function MakeStart({ getValue, row, table, column }: CuesheetCellContext) { if (!table.options.meta) { return null; } @@ -60,7 +60,7 @@ function MakeStart({ getValue, row, table, column }: CellContext) { +function MakeEnd({ getValue, row, table, column }: CuesheetCellContext) { if (!table.options.meta) { return null; } @@ -100,7 +100,7 @@ function MakeEnd({ getValue, row, table, column }: CellContext) { +function MakeDuration({ getValue, row, table, column }: CuesheetCellContext) { if (!table.options.meta) { return null; } @@ -131,7 +131,7 @@ function MakeDuration({ getValue, row, table, column }: CellContext) { +function MakeMultiLineField({ row, column, table }: CuesheetCellContext) { const update = useCallback( (newValue: string) => { table.options.meta?.handleUpdate(row.index, column.id, newValue, false); @@ -160,7 +160,7 @@ function MakeMultiLineField({ row, column, table }: CellContext) { +function LazyImage({ row, column, table }: CuesheetCellContext) { const update = useCallback( (newValue: string) => { table.options.meta?.handleUpdate(row.index, column.id, newValue, true); @@ -178,7 +178,7 @@ function LazyImage({ row, column, table }: CellContext) return ; } -function MakeSingleLineField({ row, column, table }: CellContext) { +function MakeSingleLineField({ row, column, table }: CuesheetCellContext) { const update = useCallback( (newValue: string) => { table.options.meta?.handleUpdate(row.index, column.id, newValue, false); @@ -207,7 +207,7 @@ function MakeSingleLineField({ row, column, table }: CellContext) { +function MakeFlagField({ row }: CuesheetCellContext) { const event = row.original; if (!isOntimeEvent(event) || !event.flag) { return null; @@ -215,7 +215,7 @@ function MakeFlagField({ row }: CellContext) { return ; } -function MakeCustomField({ row, column, table }: CellContext) { +function MakeCustomField({ row, column, table }: CuesheetCellContext) { const update = useCallback( (newValue: string) => { table.options.meta?.handleUpdate(row.index, column.id, newValue, true); @@ -255,8 +255,8 @@ export function makeCuesheetColumns( customFields: CustomFields, cuesheetMode: AppMode, preset: URLPreset | undefined, -): ColumnDef[] { - const columnsDef: ColumnDef[] = []; +): CuesheetColumnDef[] { + const columnsDef: CuesheetColumnDef[] = []; const { canRead, canWrite } = getCuesheetColumnAccessPolicy(preset, cuesheetMode); if (canRead('flag')) { diff --git a/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-settings/CuesheetTableHeaderToolbar.tsx b/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-settings/CuesheetTableHeaderToolbar.tsx index 44b76a323..bb62f3aad 100644 --- a/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-settings/CuesheetTableHeaderToolbar.tsx +++ b/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-settings/CuesheetTableHeaderToolbar.tsx @@ -2,7 +2,6 @@ import { Popover } from '@base-ui/react/popover'; import { Toggle } from '@base-ui/react/toggle'; import { ToggleGroup } from '@base-ui/react/toggle-group'; import { Toolbar } from '@base-ui/react/toolbar'; -import type { Column } from '@tanstack/react-table'; import { ReactNode } from 'react'; import { IoBookOutline, IoChevronDown, IoOptions } from 'react-icons/io5'; @@ -10,9 +9,9 @@ import Button from '../../../../common/components/buttons/Button'; import Checkbox from '../../../../common/components/checkbox/Checkbox'; import * as Editor from '../../../../common/components/editor-utils/EditorUtils'; import PopoverContents from '../../../../common/components/popover/Popover'; -import type { ExtendedEntry } from '../../../../common/utils/rundownMetadata'; import { AppMode } from '../../../../ontimeConfig'; import { useCuesheetPermissions } from '../../useTablePermissions'; +import type { CuesheetColumn } from '../cuesheetTable.features'; import CuesheetShareModal from './CuesheetShareModal'; import style from './CuesheetTableSettings.module.scss'; @@ -37,7 +36,7 @@ type TableModeControls = { }; interface CuesheetTableHeaderToolbarProps { - columns: Column[]; + columns: CuesheetColumn[]; optionsStore: TableHeaderOptionsStore; handleResetResizing: () => void; handleResetReordering: () => void; @@ -111,7 +110,7 @@ interface ViewSettingsProps { } interface ColumnSettingsProps { - columns: Column[]; + columns: CuesheetColumn[]; handleResetResizing: () => void; handleResetReordering: () => void; handleClearToggles: () => void; @@ -203,7 +202,7 @@ function ColumnSettings({ return ( - + column.toggleVisibility(checked)} /> {columnHeader as ReactNode} ); diff --git a/apps/client/src/views/cuesheet/cuesheet-table/cuesheetTable.features.ts b/apps/client/src/views/cuesheet/cuesheet-table/cuesheetTable.features.ts new file mode 100644 index 000000000..7e1f49a3d --- /dev/null +++ b/apps/client/src/views/cuesheet/cuesheet-table/cuesheetTable.features.ts @@ -0,0 +1,74 @@ +import { + columnOrderingFeature, + columnResizingFeature, + columnSizingFeature, + columnVisibilityFeature, + metaHelper, + tableFeatures, +} from '@tanstack/react-table'; +import type { CellContext, Column, ColumnDef, Header, HeaderGroup, Table } from '@tanstack/react-table'; +import type { TimeField } from 'ontime-types'; + +import type { ExtendedEntry } from '../../../common/utils/rundownMetadata'; +import type { AppMode } from '../../../ontimeConfig'; + +/** + * Custom data we pass to the table + * - `handleUpdate` callback to update the entry when the user edits a cell + * - `handleUpdateTimer` callback to update the timer for a specific event + * - `options-showDelayedTimes` whether to show or hide delayed times + * - `options-hideTableSeconds` whether to hide seconds in the table + * - `options-hideIndexColumn` whether to hide the index column + * - `options-cuesheetMode` run or edit mode + */ +export interface CuesheetTableMeta { + handleUpdate: (rowIndex: number, accessor: string, payload: string, isCustom: boolean) => void; + handleUpdateTimer: (eventId: string, field: TimeField, payload: string) => void; + options: { + showDelayedTimes: boolean; + hideTableSeconds: boolean; + hideIndexColumn: boolean; + cuesheetMode: AppMode; + }; +} + +/** + * Metadata specific for each column + * - `canWrite` whether the user can write to this column + * - `colour` background colour associated with a custom field + */ +export interface CuesheetColumnMeta { + canWrite: boolean; + colour?: string; +} + +/** + * Features registered in the cuesheet and rundown tables. + * In v9 an API only exists once its feature is registered, so this list is the + * source of truth for what the table can do: + * - `columnOrderingFeature`: user reorders columns by dragging the headers + * - `columnVisibilityFeature`: user toggles columns in the table settings + * - `columnSizingFeature`: column widths, exposed to CSS as custom properties + * - `columnResizingFeature`: the drag handle in the header (requires sizing) + * + * The `tableMeta` / `columnMeta` slots replace the v8 global module augmentation: + * they scope our meta types to this table instead of every table in the app. + */ +export const cuesheetTableFeatures = tableFeatures({ + columnOrderingFeature, + columnVisibilityFeature, + columnSizingFeature, + columnResizingFeature, + tableMeta: metaHelper(), + columnMeta: metaHelper(), +}); + +export type CuesheetFeatures = typeof cuesheetTableFeatures; + +/** Convenience aliases so consumers do not need to repeat the feature generic */ +export type CuesheetColumnDef = ColumnDef; +export type CuesheetTable = Table; +export type CuesheetCellContext = CellContext; +export type CuesheetHeaderGroup = HeaderGroup; +export type CuesheetHeaderCell = Header; +export type CuesheetColumn = Column; diff --git a/apps/client/src/views/cuesheet/cuesheet-table/useColumnManager.tsx b/apps/client/src/views/cuesheet/cuesheet-table/useColumnManager.tsx index 752eb824d..abc3d51ce 100644 --- a/apps/client/src/views/cuesheet/cuesheet-table/useColumnManager.tsx +++ b/apps/client/src/views/cuesheet/cuesheet-table/useColumnManager.tsx @@ -1,10 +1,10 @@ import { useLocalStorage } from '@mantine/hooks'; -import { ColumnDef, ColumnSizingState, Updater } from '@tanstack/react-table'; +import { ColumnSizingState, Updater } from '@tanstack/react-table'; import { useCallback, useEffect, useMemo, useState } from 'react'; import { debounce } from '../../../common/utils/debounce'; import { makeStageKey } from '../../../common/utils/localStorage'; -import type { ExtendedEntry } from '../../../common/utils/rundownMetadata'; +import type { CuesheetColumnDef } from './cuesheetTable.features'; type TableRoot = 'editor' | 'cuesheet'; @@ -38,7 +38,7 @@ export function useColumnSizes(tableRoot: TableRoot = 'cuesheet') { }; } -export function useColumnOrder(columns: ColumnDef[], tableRoot: TableRoot = 'cuesheet') { +export function useColumnOrder(columns: CuesheetColumnDef[], tableRoot: TableRoot = 'cuesheet') { const tableOrderKey = useMemo(() => makeStageKey(`${tableRoot}-table-order`), [tableRoot]); const [columnOrder, saveColumnOrder] = useLocalStorage({ diff --git a/e2e/tests/features/202-cuesheet.spec.ts b/e2e/tests/features/202-cuesheet.spec.ts index 1ad9227b6..eabf68b4b 100644 --- a/e2e/tests/features/202-cuesheet.spec.ts +++ b/e2e/tests/features/202-cuesheet.spec.ts @@ -6,6 +6,25 @@ test('cuesheet displays events', async ({ page }) => { await expect(page.getByTestId('cuesheet-event').first()).toBeVisible(); }); +test('cuesheet persists column visibility', async ({ page }) => { + await page.goto('/cuesheet'); + + const noteHeader = page.getByRole('columnheader', { name: 'Note' }); + const noteCell = page.getByTestId('cuesheet-event').first().getByTestId('cuesheet-cell-note'); + await expect(noteHeader).toBeVisible(); + await expect(noteCell).toBeVisible(); + + await page.getByRole('button', { name: 'Columns' }).click(); + await page.getByRole('checkbox', { name: 'Note' }).click(); + + await expect(noteHeader).toBeHidden(); + await expect(noteCell).toBeHidden(); + + await page.reload(); + await expect(noteHeader).toBeHidden(); + await expect(noteCell).toBeHidden(); +}); + test('cuesheet datagrid does not submit timer cells on tab-out or escape', async ({ page }) => { await page.goto('/cuesheet'); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1a92efe1a..99b8b835b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -101,8 +101,8 @@ importers: specifier: ^5.101.0 version: 5.101.0(@tanstack/react-query@5.101.0(react@19.2.7))(react@19.2.7) '@tanstack/react-table': - specifier: ^8.21.3 - version: 8.21.3(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + specifier: ^9.1.2 + version: 9.1.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@uiw/codemirror-theme-vscode': specifier: ^4.25.10 version: 4.25.10(@codemirror/language@6.12.3)(@codemirror/state@6.6.0)(@codemirror/view@6.40.0) @@ -2061,16 +2061,24 @@ packages: peerDependencies: react: ^18 || ^19 - '@tanstack/react-table@8.21.3': - resolution: {integrity: sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww==} - engines: {node: '>=12'} + '@tanstack/react-store@0.11.1': + resolution: {integrity: sha512-HaIGKI3YLmjBYIvy5DFDY23oNaYZIsTZfngey07Uh5iLVJgM3bIGCnZeOFOqzjFld9JHWcaHJnasD/bKoGKwJQ==} peerDependencies: - react: '>=16.8' - react-dom: '>=16.8' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - '@tanstack/table-core@8.21.3': - resolution: {integrity: sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==} - engines: {node: '>=12'} + '@tanstack/react-table@9.1.2': + resolution: {integrity: sha512-YQPZFJ1nIi/bjjwsPZVouABgahDcl7Gdm33CdTStUJBn0DjEVJ2uhSTVmIoWt9MVKdQziXGAsXipSzy949Hygg==} + engines: {node: '>=20'} + peerDependencies: + react: '>=18' + + '@tanstack/store@0.11.1': + resolution: {integrity: sha512-mzTOBhypOuDJAy/D8n2MfUZ1HFkXnmSETviRyhqEC8LUE7/IZQExOTxMANj3KjTofYTkFNpBY67qaVrT41YccA==} + + '@tanstack/table-core@9.1.2': + resolution: {integrity: sha512-ONpWQeass1sfg80CWF1NSwQ8r3GiqxA2lT/EdqIcrDEPZ0Z+0mM94eQoFYLPN0Kztzj8TQVb2+PrSZSItqA61g==} + engines: {node: '>=20'} '@turbo/darwin-64@2.10.10': resolution: {integrity: sha512-gFDD+wRP5hWxBRghGyEbjpbLOY7aIU/wvsnKdMM7odQcp/wHMrnI83p0FyxxMRZnFH9ZD+S59MvcpOC5b+nrCA==} @@ -6550,13 +6558,26 @@ snapshots: '@tanstack/query-core': 5.101.0 react: 19.2.7 - '@tanstack/react-table@8.21.3(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@tanstack/react-store@0.11.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: - '@tanstack/table-core': 8.21.3 + '@tanstack/store': 0.11.1 react: 19.2.7 react-dom: 19.2.7(react@19.2.7) + use-sync-external-store: 1.6.0(react@19.2.7) - '@tanstack/table-core@8.21.3': {} + '@tanstack/react-table@9.1.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@tanstack/react-store': 0.11.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@tanstack/table-core': 9.1.2 + react: 19.2.7 + transitivePeerDependencies: + - react-dom + + '@tanstack/store@0.11.1': {} + + '@tanstack/table-core@9.1.2': + dependencies: + '@tanstack/store': 0.11.1 '@turbo/darwin-64@2.10.10': optional: true