diff --git a/apps/client/src/features/rundown/Rundown.tsx b/apps/client/src/features/rundown/Rundown.tsx index 3a6bc51f7..71baf82da 100644 --- a/apps/client/src/features/rundown/Rundown.tsx +++ b/apps/client/src/features/rundown/Rundown.tsx @@ -57,6 +57,7 @@ interface RundownProps { } export default function Rundown({ data, rundownMetadata }: RundownProps) { + // invoke the compiler for the component 'use memo'; const { order, entries, id } = data; diff --git a/apps/client/src/features/rundown/RundownExport.tsx b/apps/client/src/features/rundown/RundownExport.tsx index ca4a7150f..3af5bfd1f 100644 --- a/apps/client/src/features/rundown/RundownExport.tsx +++ b/apps/client/src/features/rundown/RundownExport.tsx @@ -14,7 +14,7 @@ import { AppMode, sessionKeys } from '../../ontimeConfig'; import RundownEntryEditor from './entry-editor/RundownEntryEditor'; import FinderPlacement from './placements/FinderPlacement'; import { RundownContextMenu } from './rundown-context-menu/RundownContextMenu'; -import RundownWrapper from './RundownWrapper'; +import RundownWrapper, { RundownViewMode } from './RundownWrapper'; import style from './RundownExport.module.scss'; @@ -26,6 +26,10 @@ function RundownExport() { key: sessionKeys.editorMode, defaultValue: AppMode.Edit, }); + const [viewMode, setViewMode] = useSessionStorage({ + key: 'rundown-view-mode', + defaultValue: 'list', + }); const isSmallDevice = useIsSmallDevice(); if (isSmallDevice && isExtracted) { @@ -41,7 +45,7 @@ function RundownExport() {
- +
@@ -50,7 +54,7 @@ function RundownExport() { ); } - const hideSideBar = isExtracted && editorMode === 'run'; + const hideSideBar = (isExtracted && editorMode === 'run') || viewMode === 'table'; return ( @@ -62,7 +66,7 @@ function RundownExport() { {!isExtracted && handleLinks('rundown', event)} />} - + diff --git a/apps/client/src/features/rundown/RundownWrapper.tsx b/apps/client/src/features/rundown/RundownWrapper.tsx index 15fea84af..028f5735b 100644 --- a/apps/client/src/features/rundown/RundownWrapper.tsx +++ b/apps/client/src/features/rundown/RundownWrapper.tsx @@ -1,27 +1,34 @@ import Empty from '../../common/components/state/Empty'; import { useRundownWithMetadata } from '../../common/hooks-query/useRundown'; +import EditorEditModal from '../../views/cuesheet/cuesheet-edit-modal/EditorEditModal'; import RundownHeader from './rundown-header/RundownHeader'; import RundownHeaderMobile from './rundown-header/RundownHeaderMobile'; +import RundownTable from './rundown-table/RundownTable'; import Rundown from './Rundown'; import styles from './Rundown.module.scss'; +export type RundownViewMode = 'list' | 'table'; + interface RundownWrapperProps { isSmallDevice?: boolean; + viewMode: RundownViewMode; + setViewMode: (mode: RundownViewMode) => void; } -export default function RundownWrapper({ isSmallDevice }: RundownWrapperProps) { +export default function RundownWrapper({ isSmallDevice, viewMode, setViewMode }: RundownWrapperProps) { const { data, status, rundownMetadata } = useRundownWithMetadata(); + const isLoading = status !== 'success' || !data || !rundownMetadata; + return (
- {isSmallDevice ? : } - {status === 'success' && data && rundownMetadata ? ( - - ) : ( - - )} + {isSmallDevice ? : } + {isLoading && } + {!isLoading && viewMode === 'list' && } + {!isLoading && viewMode === 'table' && } +
); } diff --git a/apps/client/src/features/rundown/rundown-header/RundownHeader.tsx b/apps/client/src/features/rundown/rundown-header/RundownHeader.tsx index 264e5c30c..e9fce4f0d 100644 --- a/apps/client/src/features/rundown/rundown-header/RundownHeader.tsx +++ b/apps/client/src/features/rundown/rundown-header/RundownHeader.tsx @@ -13,8 +13,15 @@ import RundownMenu from './RundownMenu'; import style from './RundownHeader.module.scss'; +type RundownViewMode = 'list' | 'table'; + +interface RundownHeaderProps { + viewMode: RundownViewMode; + setViewMode: (mode: RundownViewMode) => void; +} + export default memo(RundownHeader); -function RundownHeader() { +function RundownHeader({ viewMode, setViewMode }: RundownHeaderProps) { const [editorMode, setEditorMode] = useSessionStorage({ key: sessionKeys.editorMode, defaultValue: AppMode.Edit }); const { offsetMode } = useOffsetMode(); @@ -33,6 +40,13 @@ function RundownHeader() { setOffsetMode(newValue); }; + const toggleViewMode = (mode: RundownViewMode[]) => { + // we need to stop user from deselecting a mode + const newValue = mode.at(0); + if (!newValue) return; + setViewMode(newValue); + }; + return ( @@ -55,6 +69,17 @@ function RundownHeader() { + + + + } value='list' className={style.radioButton}> + List + + } value='table' className={style.radioButton}> + Table + + + ); diff --git a/apps/client/src/features/rundown/rundown-table/EditorTableSettings.tsx b/apps/client/src/features/rundown/rundown-table/EditorTableSettings.tsx new file mode 100644 index 000000000..d6117c0a2 --- /dev/null +++ b/apps/client/src/features/rundown/rundown-table/EditorTableSettings.tsx @@ -0,0 +1,42 @@ +import { Toolbar } from '@base-ui/react/toolbar'; +import type { Column } from '@tanstack/react-table'; + +import type { ExtendedEntry } from '../../../common/utils/rundownMetadata'; +import { + ColumnSettings, + ViewSettings, +} from '../../../views/cuesheet/cuesheet-table/cuesheet-table-settings/CuesheetTableSettings'; + +import { usePersistedRundownOptions } from '../rundown.options'; + +import style from '../../../views/cuesheet/cuesheet-table/cuesheet-table-settings/CuesheetTableSettings.module.scss'; + +interface EditorTableSettingsProps { + columns: Column[]; + handleResetResizing: () => void; + handleResetReordering: () => void; + handleClearToggles: () => void; +} + +export default function EditorTableSettings({ + columns, + handleResetResizing, + handleResetReordering, + handleClearToggles, +}: EditorTableSettingsProps) { + const options = usePersistedRundownOptions(); + + return ( + + + + {/* No mode toggle - already in RundownHeader */} + {/* No share modal - cuesheet only */} + + ); +} diff --git a/apps/client/src/features/rundown/rundown-table/RundownTable.tsx b/apps/client/src/features/rundown/rundown-table/RundownTable.tsx new file mode 100644 index 000000000..f17519d6f --- /dev/null +++ b/apps/client/src/features/rundown/rundown-table/RundownTable.tsx @@ -0,0 +1,34 @@ +import { memo, useMemo } from 'react'; +import { useSessionStorage } from '@mantine/hooks'; + +import EmptyPage from '../../../common/components/state/EmptyPage'; +import useCustomFields from '../../../common/hooks-query/useCustomFields'; +import { AppMode, sessionKeys } from '../../../ontimeConfig'; +import CuesheetDnd from '../../../views/cuesheet/cuesheet-dnd/CuesheetDnd'; +import CuesheetTable from '../../../views/cuesheet/cuesheet-table/CuesheetTable'; + +import { makeRundownColumns } from './makeRundownColumns'; + +export default memo(RundownTable); +function RundownTable() { + const { data: customFields, status: customFieldStatus } = useCustomFields(); + + const [editorMode] = useSessionStorage({ + key: sessionKeys.editorMode, + defaultValue: AppMode.Edit, + }); + + const columns = useMemo(() => makeRundownColumns(customFields), [customFields]); + + const isLoading = !customFields || customFieldStatus === 'pending'; + + return ( + + {isLoading ? ( + + ) : ( + + )} + + ); +} diff --git a/apps/client/src/features/rundown/rundown-table/makeRundownColumns.tsx b/apps/client/src/features/rundown/rundown-table/makeRundownColumns.tsx new file mode 100644 index 000000000..85e611e23 --- /dev/null +++ b/apps/client/src/features/rundown/rundown-table/makeRundownColumns.tsx @@ -0,0 +1,16 @@ +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'; + +/** + * Creates column definitions for the rundown table + * Reuses cuesheetColsFactory with preset=undefined for full access + */ +export function makeRundownColumns(customFields: CustomFields): ColumnDef[] { + // 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/features/rundown/rundown.options.ts b/apps/client/src/features/rundown/rundown.options.ts new file mode 100644 index 000000000..ec773948f --- /dev/null +++ b/apps/client/src/features/rundown/rundown.options.ts @@ -0,0 +1,60 @@ +import { CustomFields } from 'ontime-types'; +import { create } from 'zustand'; +import { persist } from 'zustand/middleware'; + +type OptionValues = { + hideTableSeconds: boolean; + hideIndexColumn: boolean; + showDelayedTimes: boolean; + hideDelays: boolean; +}; + +const defaultOptions: OptionValues = { + hideTableSeconds: false, + hideIndexColumn: false, + showDelayedTimes: false, + hideDelays: false, +}; + +export type RundownOptionKeys = keyof OptionValues; + +export interface RundownOptions extends OptionValues { + setOption: (key: K, value: OptionValues[K]) => void; + toggleOption: (key: RundownOptionKeys) => void; + resetOptions: () => void; +} + +export const usePersistedRundownOptions = create()( + persist( + (set) => { + return { + ...defaultOptions, + setOption: (key, value) => set((state) => ({ ...state, [key]: value })), + toggleOption: (key) => set((state) => ({ ...state, [key]: !state[key] })), + resetOptions: () => set(defaultOptions), + }; + }, + { + name: 'editor-options', + }, + ), +); + +export const rundownDefaultColumns = [ + { value: 'flag', label: 'Flag' }, + { value: 'cue', label: 'Cue' }, + { value: 'title', label: 'Title' }, + { value: 'timeStart', label: 'Time start' }, + { value: 'timeEnd', label: 'Time end' }, + { value: 'duration', label: 'Duration' }, + { value: 'note', label: 'Note' }, +]; + +export function makeRundownCustomColumns(customFields: CustomFields) { + return Object.entries(customFields).map(([key, field]) => { + return { + value: `custom-${key}`, + label: field.label, + }; + }); +} diff --git a/apps/client/src/views/cuesheet/cuesheet-dnd/CuesheetDnd.tsx b/apps/client/src/views/cuesheet/cuesheet-dnd/CuesheetDnd.tsx index fc8c5c520..bd776877b 100644 --- a/apps/client/src/views/cuesheet/cuesheet-dnd/CuesheetDnd.tsx +++ b/apps/client/src/views/cuesheet/cuesheet-dnd/CuesheetDnd.tsx @@ -15,10 +15,15 @@ import { useColumnOrder } from '../cuesheet-table/useColumnManager'; interface CuesheetDndProps { columns: ColumnDef[]; + tableRoot?: 'editor' | 'cuesheet'; } -export default function CuesheetDnd({ columns, children }: PropsWithChildren) { - const { columnOrder, saveColumnOrder } = useColumnOrder(columns); +export default function CuesheetDnd({ + columns, + tableRoot = 'cuesheet', + children, +}: PropsWithChildren) { + const { columnOrder, saveColumnOrder } = useColumnOrder(columns, tableRoot); const sensors = useSensors( useSensor(PointerSensor, { diff --git a/apps/client/src/views/cuesheet/cuesheet-edit-modal/EditorEditModal.tsx b/apps/client/src/views/cuesheet/cuesheet-edit-modal/EditorEditModal.tsx new file mode 100644 index 000000000..514d85f0a --- /dev/null +++ b/apps/client/src/views/cuesheet/cuesheet-edit-modal/EditorEditModal.tsx @@ -0,0 +1,26 @@ +import { memo } from 'react'; + +import Modal from '../../../common/components/modal/Modal'; +import CuesheetEntryEditor from '../../../features/rundown/entry-editor/CuesheetEventEditor'; + +import { useEditorEditModal } from './useEditorEditModal'; + +export default memo(EditorEditModal); +function EditorEditModal() { + const entryId = useEditorEditModal((state) => state.selectedEntryId); + const closeModal = useEditorEditModal((state) => state.clearSelection); + + if (entryId === null) { + return null; + } + + return ( + } + /> + ); +} diff --git a/apps/client/src/views/cuesheet/cuesheet-edit-modal/useEditorEditModal.tsx b/apps/client/src/views/cuesheet/cuesheet-edit-modal/useEditorEditModal.tsx new file mode 100644 index 000000000..32ea72fcf --- /dev/null +++ b/apps/client/src/views/cuesheet/cuesheet-edit-modal/useEditorEditModal.tsx @@ -0,0 +1,14 @@ +import { EntryId } from 'ontime-types'; +import { create } from 'zustand'; + +interface SelectedEntryState { + selectedEntryId: EntryId | null; + setEditableEntry: (entryId: EntryId) => void; + clearSelection: () => void; +} + +export const useEditorEditModal = create((set) => ({ + selectedEntryId: null, + setEditableEntry: (entryId: EntryId) => set({ selectedEntryId: entryId }), + clearSelection: () => set({ selectedEntryId: null }), +})); diff --git a/apps/client/src/views/cuesheet/cuesheet-table/CuesheetTable.tsx b/apps/client/src/views/cuesheet/cuesheet-table/CuesheetTable.tsx index a04f691f7..da4804895 100644 --- a/apps/client/src/views/cuesheet/cuesheet-table/CuesheetTable.tsx +++ b/apps/client/src/views/cuesheet/cuesheet-table/CuesheetTable.tsx @@ -10,6 +10,8 @@ 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 { usePersistedRundownOptions } from '../../../features/rundown/rundown.options'; +import EditorTableSettings from '../../../features/rundown/rundown-table/EditorTableSettings'; import { AppMode } from '../../../ontimeConfig'; import { usePersistedCuesheetOptions } from '../cuesheet.options'; @@ -19,6 +21,7 @@ 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 EditorTableMenu from './cuesheet-table-menu/EditorTableMenu'; import CuesheetTableSettings from './cuesheet-table-settings/CuesheetTableSettings'; import { useColumnOrder, useColumnSizes, useColumnVisibility } from './useColumnManager'; @@ -27,14 +30,17 @@ import style from './CuesheetTable.module.scss'; interface CuesheetTableProps { columns: ColumnDef[]; cuesheetMode: AppMode; + tableRoot?: 'editor' | 'cuesheet'; } -export default function CuesheetTable({ columns, cuesheetMode }: CuesheetTableProps) { +export default function CuesheetTable({ columns, cuesheetMode, tableRoot = 'cuesheet' }: 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 useOptions = tableRoot === 'editor' ? usePersistedRundownOptions : usePersistedCuesheetOptions; + const showDelayedTimes = useOptions((state) => state.showDelayedTimes); + const hideTableSeconds = useOptions((state) => state.hideTableSeconds); + const hideIndexColumn = useOptions((state) => state.hideIndexColumn); const { selectedEventId } = useSelectedEventId(); @@ -79,9 +85,9 @@ export default function CuesheetTable({ columns, cuesheetMode }: CuesheetTablePr [cuesheetMode, data, hideIndexColumn, hideTableSeconds, showDelayedTimes, updateEntry, updateTimer], ); - const { columnOrder, resetColumnOrder } = useColumnOrder(columns); - const { columnSizing, setColumnSizing } = useColumnSizes(); - const { columnVisibility, setColumnVisibility } = useColumnVisibility(); + const { columnOrder, resetColumnOrder } = useColumnOrder(columns, tableRoot); + const { columnSizing, setColumnSizing } = useColumnSizes(tableRoot); + const { columnVisibility, setColumnVisibility } = useColumnVisibility(tableRoot); const table = useReactTable({ data, @@ -143,9 +149,13 @@ export default function CuesheetTable({ columns, cuesheetMode }: CuesheetTablePr return ; } + // control components need different implementations for handling permissions + const TableRootSettings = tableRoot === 'editor' ? EditorTableSettings : CuesheetTableSettings; + const TableMenu = tableRoot === 'editor' ? EditorTableMenu : CuesheetTableMenu; + return ( <> - - + ); } diff --git a/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-elements/DelayRow.module.scss b/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-elements/DelayRow.module.scss index aebd947b0..0a721e946 100644 --- a/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-elements/DelayRow.module.scss +++ b/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-elements/DelayRow.module.scss @@ -15,3 +15,14 @@ } } } + +.delayRowHidden { + visibility: hidden; + + td { + padding: 0; + height: 1px; + line-height: 1px; + font-size: 0; + } +} diff --git a/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-elements/DelayRow.tsx b/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-elements/DelayRow.tsx index 422bda2fe..26de665f8 100644 --- a/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-elements/DelayRow.tsx +++ b/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-elements/DelayRow.tsx @@ -14,7 +14,16 @@ function DelayRow({ duration, injectedStyles, ...virtuosoProps }: DelayRowProps) const hideDelays = usePersistedCuesheetOptions((state) => state.hideDelays); if (hideDelays || duration === 0) { - return null; + return ( + + + + ); } const delayTime = millisToDelayString(duration, 'expanded'); diff --git a/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-menu/EditorTableMenu.tsx b/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-menu/EditorTableMenu.tsx new file mode 100644 index 000000000..6acf01d47 --- /dev/null +++ b/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-menu/EditorTableMenu.tsx @@ -0,0 +1,85 @@ +import { memo } from 'react'; +import { IoAdd, IoArrowDown, IoArrowUp, IoDuplicateOutline, IoOptions, IoTrash } from 'react-icons/io5'; +import { SupportedEntry } from 'ontime-types'; + +import { PositionedDropdownMenu } from '../../../../common/components/dropdown-menu/DropdownMenu'; +import { useEntryActions } from '../../../../common/hooks/useEntryAction'; +import { useCuesheetEditModal } from '../../cuesheet-edit-modal/useCuesheetEditModal'; + +import { useCuesheetTableMenu } from './useCuesheetTableMenu'; + +export default memo(EditorTableMenu); + +function EditorTableMenu() { + const { isOpen, entryId, entryIndex, parentId, flag, position, closeMenu } = useCuesheetTableMenu(); + const { addEntry, clone, deleteEntry, move, updateEntry } = useEntryActions(); + const showModal = useCuesheetEditModal((state) => state.setEditableEntry); + + if (!isOpen) { + return null; + } + + return ( + showModal(entryId), + icon: IoOptions, + }, + { type: 'divider' }, + { + type: 'item', + label: flag ? 'Remove flag' : 'Add flag', + onClick: () => updateEntry({ id: entryId, flag: !flag }), + icon: IoDuplicateOutline, + disabled: flag === null, + }, + { type: 'divider' }, + { + type: 'item', + label: 'Add event above', + onClick: () => addEntry({ type: SupportedEntry.Event, parent: parentId }, { before: entryId }), + icon: IoAdd, + }, + { + type: 'item', + label: 'Add event below', + onClick: () => addEntry({ type: SupportedEntry.Event, parent: parentId }, { after: entryId }), + icon: IoAdd, + }, + { + type: 'item', + label: 'Clone event', + onClick: () => clone(entryId), + icon: IoDuplicateOutline, + }, + { type: 'divider' }, + { + type: 'item', + label: 'Move up', + onClick: () => move(entryId, 'up'), + icon: IoArrowUp, + disabled: entryIndex < 1, + }, + { + type: 'item', + label: 'Move down', + onClick: () => move(entryId, 'down'), + icon: IoArrowDown, + }, + { type: 'divider' }, + { + type: 'item', + label: 'Delete', + onClick: () => deleteEntry([entryId]), + icon: IoTrash, + }, + ]} + position={position} + /> + ); +} diff --git a/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-settings/CuesheetTableSettings.tsx b/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-settings/CuesheetTableSettings.tsx index c922e40ec..8436438d6 100644 --- a/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-settings/CuesheetTableSettings.tsx +++ b/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-settings/CuesheetTableSettings.tsx @@ -15,7 +15,7 @@ import { PresetContext } from '../../../../common/context/PresetContext'; import type { ExtendedEntry } from '../../../../common/utils/rundownMetadata'; import { cx } from '../../../../common/utils/styleUtils'; import { AppMode, sessionKeys } from '../../../../ontimeConfig'; -import { usePersistedCuesheetOptions } from '../../cuesheet.options'; +import { CuesheetOptions, usePersistedCuesheetOptions } from '../../cuesheet.options'; import { useCuesheetPermissions } from '../../useTablePermissions'; import CuesheetShareModal from './CuesheetShareModal'; @@ -29,6 +29,17 @@ interface CuesheetTableSettingsProps { handleClearToggles: () => void; } +export interface ViewSettingsProps { + optionsStore: CuesheetOptions; +} + +export interface ColumnSettingsProps { + columns: Column[]; + handleResetResizing: () => void; + handleResetReordering: () => void; + handleClearToggles: () => void; +} + export default function CuesheetTableSettings({ columns, handleResetResizing, @@ -37,6 +48,7 @@ export default function CuesheetTableSettings({ }: CuesheetTableSettingsProps) { const canShare = useCuesheetPermissions((state) => state.canShare); const preset = use(PresetContext); + const options = usePersistedCuesheetOptions(); const [cuesheetMode, setCuesheetMode] = useSessionStorage({ key: preset ? `${preset.alias}${sessionKeys.cuesheetMode}` : sessionKeys.cuesheetMode, @@ -52,7 +64,7 @@ export default function CuesheetTableSettings({ return ( - + @@ -137,12 +149,12 @@ function ViewSettings() { ); } -function ColumnSettings({ +export function ColumnSettings({ columns, handleResetResizing, handleResetReordering, handleClearToggles, -}: CuesheetTableSettingsProps) { +}: ColumnSettingsProps) { return ( ) => { - localStorage.setItem(tableSizesKey, JSON.stringify(sizes)); -}, 500); +export function useColumnSizes(tableRoot: TableRoot = 'cuesheet') { + const tableSizesKey = useMemo(() => makeStageKey(`${tableRoot}-table-sizes`), [tableRoot]); -export function useColumnSizes() { const [columnSizing, setColumnSizingState] = useState>(() => { try { const stored = localStorage.getItem(tableSizesKey); @@ -26,8 +22,11 @@ export function useColumnSizes() { // save sizes to localStorage whenever they change (debounced) useEffect(() => { + const saveSizesToStorage = debounce((sizes: Record) => { + localStorage.setItem(tableSizesKey, JSON.stringify(sizes)); + }, 500); saveSizesToStorage(columnSizing); - }, [columnSizing]); + }, [columnSizing, tableSizesKey]); const setColumnSizing = useCallback((sizesOrUpdater: Updater) => { setColumnSizingState(sizesOrUpdater); @@ -39,7 +38,9 @@ export function useColumnSizes() { }; } -export function useColumnOrder(columns: ColumnDef[]) { +export function useColumnOrder(columns: ColumnDef[], tableRoot: TableRoot = 'cuesheet') { + const tableOrderKey = useMemo(() => makeStageKey(`${tableRoot}-table-order`), [tableRoot]); + const [columnOrder, saveColumnOrder] = useLocalStorage({ key: tableOrderKey, defaultValue: columns.map((col) => col.id as string), @@ -64,7 +65,9 @@ export function useColumnOrder(columns: ColumnDef[]) { }; } -export function useColumnVisibility() { +export function useColumnVisibility(tableRoot: TableRoot = 'cuesheet') { + const tableHiddenKey = useMemo(() => makeStageKey(`${tableRoot}-table-hidden`), [tableRoot]); + const [columnVisibility, setColumnVisibility] = useLocalStorage({ key: tableHiddenKey, defaultValue: {},