diff --git a/apps/client/src/views/cuesheet/cuesheet-dnd/CuesheetDnd.tsx b/apps/client/src/views/cuesheet/cuesheet-dnd/CuesheetDnd.tsx index 53298e9bf..fc8c5c520 100644 --- a/apps/client/src/views/cuesheet/cuesheet-dnd/CuesheetDnd.tsx +++ b/apps/client/src/views/cuesheet/cuesheet-dnd/CuesheetDnd.tsx @@ -11,14 +11,14 @@ import { import { ColumnDef } from '@tanstack/react-table'; import type { ExtendedEntry } from '../../../common/utils/rundownMetadata'; -import useColumnManager from '../cuesheet-table/useColumnManager'; +import { useColumnOrder } from '../cuesheet-table/useColumnManager'; interface CuesheetDndProps { columns: ColumnDef[]; } export default function CuesheetDnd({ columns, children }: PropsWithChildren) { - const { columnOrder, saveColumnOrder } = useColumnManager(columns); + const { columnOrder, saveColumnOrder } = useColumnOrder(columns); const sensors = useSensors( useSensor(PointerSensor, { diff --git a/apps/client/src/views/cuesheet/cuesheet-table/CuesheetTable.tsx b/apps/client/src/views/cuesheet/cuesheet-table/CuesheetTable.tsx index 15225b87f..a04f691f7 100644 --- a/apps/client/src/views/cuesheet/cuesheet-table/CuesheetTable.tsx +++ b/apps/client/src/views/cuesheet/cuesheet-table/CuesheetTable.tsx @@ -13,14 +13,14 @@ import type { ExtendedEntry } from '../../../common/utils/rundownMetadata'; import { AppMode } from '../../../ontimeConfig'; import { usePersistedCuesheetOptions } from '../cuesheet.options'; -import CuesheetHeader from './cuesheet-table-elements/CuesheetHeader'; +import { CuesheetHeader, SortableCuesheetHeader } 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 { useColumnOrder, useColumnSizes, useColumnVisibility } from './useColumnManager'; import style from './CuesheetTable.module.scss'; @@ -79,8 +79,9 @@ export default function CuesheetTable({ columns, cuesheetMode }: CuesheetTablePr [cuesheetMode, data, hideIndexColumn, hideTableSeconds, showDelayedTimes, updateEntry, updateTimer], ); - const { columnVisibility, columnOrder, columnSizing, resetColumnOrder, setColumnVisibility, setColumnSizing } = - useColumnManager(columns); + const { columnOrder, resetColumnOrder } = useColumnOrder(columns); + const { columnSizing, setColumnSizing } = useColumnSizes(); + const { columnVisibility, setColumnVisibility } = useColumnVisibility(); const table = useReactTable({ data, @@ -237,11 +238,14 @@ export default function CuesheetTable({ columns, cuesheetMode }: CuesheetTablePr TableHead: (virtuosoProps) => , }} fixedHeaderContent={() => { - return table - .getHeaderGroups() - .map((headerGroup) => ( - - )); + return table.getHeaderGroups().map((headerGroup) => { + const HeaderComponent = table.getState().columnSizingInfo.isResizingColumn + ? CuesheetHeader + : SortableCuesheetHeader; + + // if the table is being resized, we render non-sortable headers to avoid performance issues + return ; + }); }} /> 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 9b62be8f6..634a5180b 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 @@ -7,7 +7,7 @@ import { getAccessibleColour } from '../../../../common/utils/styleUtils'; import { AppMode } from '../../../../ontimeConfig'; import { usePersistedCuesheetOptions } from '../../cuesheet.options'; -import { SortableCell } from './SortableCell'; +import { Draggable, SortableCell, TableCell } from './SortableCell'; import style from '../CuesheetTable.module.scss'; @@ -16,8 +16,9 @@ interface CuesheetHeaderProps { cuesheetMode: AppMode; } -export default function CuesheetHeader({ headerGroup, cuesheetMode }: CuesheetHeaderProps) { +export function SortableCuesheetHeader({ headerGroup, cuesheetMode }: CuesheetHeaderProps) { const hideIndexColumn = usePersistedCuesheetOptions((state) => state.hideIndexColumn); + return ( {cuesheetMode === AppMode.Edit && } @@ -43,8 +44,10 @@ export default function CuesheetHeader({ headerGroup, cuesheetMode }: CuesheetHe return ( } > {header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())} @@ -54,3 +57,43 @@ export default function CuesheetHeader({ headerGroup, cuesheetMode }: CuesheetHe ); } + +export function CuesheetHeader({ headerGroup, cuesheetMode }: CuesheetHeaderProps) { + const hideIndexColumn = usePersistedCuesheetOptions((state) => state.hideIndexColumn); + + return ( + + {cuesheetMode === AppMode.Edit && } + {!hideIndexColumn && ( + + # + + )} + {headerGroup.headers.map((header) => { + const customBackground = header.column.columnDef.meta?.colour; + const canWrite = header.column.columnDef.meta?.canWrite; + + const customStyles: CSSProperties = { + opacity: canWrite ? 1 : 0.6, + }; + if (customBackground) { + const customColour = getAccessibleColour(customBackground); + customStyles.backgroundColor = customColour.backgroundColor; + customStyles.color = customColour.color; + } + + return ( + } + > + {header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())} + + ); + })} + + ); +} 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 3b7360989..b3f959510 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 @@ -8,16 +8,16 @@ import type { ExtendedEntry } from '../../../../common/utils/rundownMetadata'; import style from '../CuesheetTable.module.scss'; interface SortableCellProps { - header: Header; + columnId: string; + colSpan: number; injectedStyles: CSSProperties; children: ReactNode; + draggable: ReactNode; } -export function SortableCell({ header, injectedStyles, children }: SortableCellProps) { - const { column, colSpan } = header; - +export function SortableCell({ columnId, colSpan, injectedStyles, children, draggable }: SortableCellProps) { const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ - id: column.id, + id: columnId, }); // build drag styles @@ -34,12 +34,31 @@ export function SortableCell({ header, injectedStyles, children }: SortableCellP
{children}
-
header.column.resetSize()} - onMouseDown={header.getResizeHandler()} - onTouchStart={header.getResizeHandler()} - className={style.resizer} - /> + {draggable} ); } + +export function TableCell({ colSpan, injectedStyles, children, draggable }: SortableCellProps) { + return ( + +
{children}
+ {draggable} + + ); +} + +interface DraggableProps { + header: Header; +} + +export function Draggable({ header }: DraggableProps) { + return ( +
header.column.resetSize()} + onMouseDown={header.getResizeHandler()} + onTouchStart={header.getResizeHandler()} + className={style.resizer} + /> + ); +} diff --git a/apps/client/src/views/cuesheet/cuesheet-table/useColumnManager.tsx b/apps/client/src/views/cuesheet/cuesheet-table/useColumnManager.tsx index 4fc747417..f77fc2ad9 100644 --- a/apps/client/src/views/cuesheet/cuesheet-table/useColumnManager.tsx +++ b/apps/client/src/views/cuesheet/cuesheet-table/useColumnManager.tsx @@ -1,6 +1,6 @@ import { useCallback, useEffect, useState } from 'react'; import { useLocalStorage } from '@mantine/hooks'; -import { ColumnDef } from '@tanstack/react-table'; +import { ColumnDef, ColumnSizingState, Updater } from '@tanstack/react-table'; import { debounce } from '../../../common/utils/debounce'; import { makeStageKey } from '../../../common/utils/localStorage'; @@ -14,17 +14,8 @@ const saveSizesToStorage = debounce((sizes: Record) => { localStorage.setItem(tableSizesKey, JSON.stringify(sizes)); }, 500); -export default function useColumnManager(columns: ColumnDef[]) { - const [columnVisibility, setColumnVisibility] = useLocalStorage({ - key: tableHiddenKey, - defaultValue: {}, - }); - const [columnOrder, saveColumnOrder] = useLocalStorage({ - key: tableOrderKey, - defaultValue: columns.map((col) => col.id as string), - }); - - const [columnSizing, setColumnSizingState] = useState(() => { +export function useColumnSizes() { + const [columnSizing, setColumnSizingState] = useState>(() => { try { const stored = localStorage.getItem(tableSizesKey); return stored ? JSON.parse(stored) : {}; @@ -38,10 +29,22 @@ export default function useColumnManager(columns: ColumnDef[]) { saveSizesToStorage(columnSizing); }, [columnSizing]); - const setColumnSizing = useCallback((sizes: typeof columnSizing) => { - setColumnSizingState(sizes); + const setColumnSizing = useCallback((sizesOrUpdater: Updater) => { + setColumnSizingState(sizesOrUpdater); }, []); + return { + columnSizing, + setColumnSizing, + }; +} + +export function useColumnOrder(columns: ColumnDef[]) { + const [columnOrder, saveColumnOrder] = useLocalStorage({ + key: tableOrderKey, + defaultValue: columns.map((col) => col.id as string), + }); + // update column order if columns change useEffect(() => { const newColumns = columns.map((col) => col.id as string); @@ -55,12 +58,20 @@ export default function useColumnManager(columns: ColumnDef[]) { }, [columns, saveColumnOrder]); return { - columnVisibility, columnOrder, - columnSizing, - resetColumnOrder, - setColumnVisibility, saveColumnOrder, - setColumnSizing, + resetColumnOrder, + }; +} + +export function useColumnVisibility() { + const [columnVisibility, setColumnVisibility] = useLocalStorage({ + key: tableHiddenKey, + defaultValue: {}, + }); + + return { + columnVisibility, + setColumnVisibility, }; }