From 44508ce8b40a5c786dc5e8ccd3c78a9abc6a6858 Mon Sep 17 00:00:00 2001 From: Carlos Valente Date: Thu, 17 Jul 2025 06:44:21 +0200 Subject: [PATCH] refactor: block writing to base fields in run mode --- .../__tests__/viewParams.utils.test.ts | 1 - apps/client/src/declarations/declaration.d.ts | 16 +- .../src/views/cuesheet/CuesheetPage.tsx | 17 +- .../views/cuesheet/CuesheetTableWrapper.tsx | 30 +++ .../CuesheetHeader.tsx | 12 +- .../cuesheet-table-elements/SortableCell.tsx | 3 +- .../cuesheetColsFactory.tsx | 199 +++++++++++------- .../src/api-data/rundown/rundown.service.ts | 2 - 8 files changed, 177 insertions(+), 103 deletions(-) create mode 100644 apps/client/src/views/cuesheet/CuesheetTableWrapper.tsx diff --git a/apps/client/src/common/components/view-params-editor/__tests__/viewParams.utils.test.ts b/apps/client/src/common/components/view-params-editor/__tests__/viewParams.utils.test.ts index 3162c1fcc..2a0134fde 100644 --- a/apps/client/src/common/components/view-params-editor/__tests__/viewParams.utils.test.ts +++ b/apps/client/src/common/components/view-params-editor/__tests__/viewParams.utils.test.ts @@ -232,7 +232,6 @@ describe('getURLSearchParamsFromObj()', () => { bool2: 'on', }; const result = getURLSearchParamsFromObj(params, mockOptionsWithBooleans); - console.log('Result:', result.toString()); expect(result.get('bool1')).toBe('false'); expect(result.get('bool2')).toBe('true'); }); diff --git a/apps/client/src/declarations/declaration.d.ts b/apps/client/src/declarations/declaration.d.ts index 70cd162d1..60b363132 100644 --- a/apps/client/src/declarations/declaration.d.ts +++ b/apps/client/src/declarations/declaration.d.ts @@ -21,7 +21,15 @@ declare global { } /** - * We pass a custom property to the table meta to allow field update + * 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 + * + * 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 @@ -33,6 +41,12 @@ declare module '@tanstack/react-table' { hideTableSeconds: boolean; }; } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + interface ColumnMeta { + canWrite: boolean; + colour?: string; + } } /** diff --git a/apps/client/src/views/cuesheet/CuesheetPage.tsx b/apps/client/src/views/cuesheet/CuesheetPage.tsx index c4a78228b..1c2778a08 100644 --- a/apps/client/src/views/cuesheet/CuesheetPage.tsx +++ b/apps/client/src/views/cuesheet/CuesheetPage.tsx @@ -1,35 +1,24 @@ -import { useMemo } from 'react'; import { IoApps } from 'react-icons/io5'; import { useDisclosure } from '@mantine/hooks'; import IconButton from '../../common/components/buttons/IconButton'; import NavigationMenu from '../../common/components/navigation-menu/NavigationMenu'; import useViewEditor from '../../common/components/navigation-menu/useViewEditor'; -import EmptyPage from '../../common/components/state/EmptyPage'; import { useWindowTitle } from '../../common/hooks/useWindowTitle'; -import useCustomFields from '../../common/hooks-query/useCustomFields'; -import { useFlatRundown } from '../../common/hooks-query/useRundown'; import CuesheetOverview from '../../features/overview/CuesheetOverview'; -import CuesheetDnd from './cuesheet-dnd/CuesheetDnd'; import CuesheetEditModal from './cuesheet-edit-modal/CuesheetEditModal'; import CuesheetProgress from './cuesheet-progress/CuesheetProgress'; -import { makeCuesheetColumns } from './cuesheet-table/cuesheet-table-elements/cuesheetColsFactory'; -import CuesheetTable from './cuesheet-table/CuesheetTable'; +import CuesheetTableWrapper from './CuesheetTableWrapper'; import styles from './CuesheetPage.module.scss'; export default function CuesheetPage() { - const { data: flatRundown, status: rundownStatus } = useFlatRundown(); - const { data: customFields, status: customFieldStatus } = useCustomFields(); const { isViewLocked } = useViewEditor({ isLockable: true }); const [isMenuOpen, menuHandler] = useDisclosure(); - const columns = useMemo(() => makeCuesheetColumns(customFields), [customFields]); useWindowTitle('Cuesheet'); - const isLoading = !customFields || !flatRundown || rundownStatus === 'pending' || customFieldStatus === 'pending'; - return ( <> @@ -43,9 +32,7 @@ export default function CuesheetPage() { )} - - {isLoading ? : } - + ); diff --git a/apps/client/src/views/cuesheet/CuesheetTableWrapper.tsx b/apps/client/src/views/cuesheet/CuesheetTableWrapper.tsx new file mode 100644 index 000000000..797c834b4 --- /dev/null +++ b/apps/client/src/views/cuesheet/CuesheetTableWrapper.tsx @@ -0,0 +1,30 @@ +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 { useFlatRundown } from '../../common/hooks-query/useRundown'; +import { AppMode, sessionKeys } from '../../ontimeConfig'; + +import CuesheetDnd from './cuesheet-dnd/CuesheetDnd'; +import { makeCuesheetColumns } from './cuesheet-table/cuesheet-table-elements/cuesheetColsFactory'; +import CuesheetTable from './cuesheet-table/CuesheetTable'; + +export default memo(CuesheetTableWrapper); +function CuesheetTableWrapper() { + const { data: flatRundown, status: rundownStatus } = useFlatRundown(); + const { data: customFields, status: customFieldStatus } = useCustomFields(); + + const [cuesheetMode] = useSessionStorage({ + key: sessionKeys.cuesheetMode, + defaultValue: AppMode.Edit, + }); + const columns = useMemo(() => makeCuesheetColumns(customFields, cuesheetMode), [customFields, cuesheetMode]); + const isLoading = !customFields || !flatRundown || rundownStatus === 'pending' || customFieldStatus === 'pending'; + + return ( + + {isLoading ? : } + + ); +} 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 0681dcf6c..3765db5ed 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,3 +1,4 @@ +import { CSSProperties } from 'react'; import { horizontalListSortingStrategy, SortableContext } from '@dnd-kit/sortable'; import { useSessionStorage } from '@mantine/hooks'; import { flexRender, HeaderGroup } from '@tanstack/react-table'; @@ -37,13 +38,16 @@ export default function CuesheetHeader({ headerGroups }: CuesheetHeaderProps) { )} {headerGroup.headers.map((header) => { - // @ts-expect-error -- we inject this into react-table - const customBackground = header.column.columnDef?.meta?.colour; + const customBackground = header.column.columnDef.meta?.colour; + const canWrite = header.column.columnDef.meta?.canWrite; - let customStyles = {}; + const customStyles: CSSProperties = { + opacity: canWrite ? 1 : 0.6, + }; if (customBackground) { const customColour = getAccessibleColour(customBackground); - customStyles = { backgroundColor: customColour.backgroundColor, color: customColour.color }; + customStyles.backgroundColor = customColour.backgroundColor; + customStyles.color = customColour.color; } return ( 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 9f7753b44..7752bc947 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 @@ -15,14 +15,13 @@ interface SortableCellProps { export function SortableCell({ header, injectedStyles, children }: SortableCellProps) { const { column, colSpan } = header; - const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ + const { attributes, listeners, setNodeRef, transform, transition } = useSortable({ id: column.id, }); // build drag styles const dragStyle = { ...injectedStyles, - opacity: isDragging ? 0.5 : 1, transform: CSS.Translate.toString(transform), transition, }; 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 e4caf9953..c5deb2dff 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 @@ -5,6 +5,7 @@ import { millisToString } from 'ontime-utils'; import DelayIndicator from '../../../../common/components/delay-indicator/DelayIndicator'; import { formatDuration, formatTime } from '../../../../common/utils/time'; +import { AppMode } from '../../../../ontimeConfig'; import DurationInput from './DurationInput'; import EditableImage from './EditableImage'; @@ -14,7 +15,7 @@ import MutedText from './MutedText'; import SingleLineCell from './SingleLineCell'; import TimeInput from './TimeInput'; -function MakeStart({ getValue, row, table }: CellContext) { +function MakeStart({ getValue, row, table, column }: CellContext) { if (!table.options.meta) { return null; } @@ -33,11 +34,18 @@ function MakeStart({ getValue, row, table }: CellContext) const startTime = getValue() as number; const isStartLocked = !event.linkStart; - const displayTime = showDelayedTimes ? startTime + event.delay : startTime; - const formattedTime = formatTime(displayTime, formatOpts); + const canWrite = column.columnDef.meta?.canWrite; + if (!canWrite) { + return ( + + {formattedTime} + + + ); + } return ( {formattedTime} @@ -46,7 +54,7 @@ function MakeStart({ getValue, row, table }: CellContext) ); } -function MakeEnd({ getValue, row, table }: CellContext) { +function MakeEnd({ getValue, row, table, column }: CellContext) { if (!table.options.meta) { return null; } @@ -65,11 +73,19 @@ function MakeEnd({ getValue, row, table }: CellContext) { const endTime = getValue() as number; const isEndLocked = event.timeStrategy === TimeStrategy.LockEnd; - const displayTime = showDelayedTimes ? endTime + event.delay : endTime; - const formattedTime = formatTime(displayTime, formatOpts); + const canWrite = column.columnDef.meta?.canWrite; + if (!canWrite) { + return ( + + {formattedTime} + + + ); + } + return ( {formattedTime} @@ -78,7 +94,7 @@ function MakeEnd({ getValue, row, table }: CellContext) { ); } -function MakeDuration({ getValue, row, table }: CellContext) { +function MakeDuration({ getValue, row, table, column }: CellContext) { if (!table.options.meta) { return null; } @@ -97,6 +113,11 @@ function MakeDuration({ getValue, row, table }: CellContext{formattedDuration}; + } + return ( {formattedDuration} @@ -182,78 +203,100 @@ function MakeCustomField({ row, column, table }: CellContext; } -export function makeCuesheetColumns(customFields: CustomFields): ColumnDef[] { - /** - * we cant use the createColumnHelper() because we have custom logic for rendering the cells - * This means that the display columns: index and action are added inline by the row components - */ - const dynamicCustomFields = Object.keys(customFields).map((key) => ({ - accessorKey: key, - id: key, - header: customFields[key].label, - meta: { colour: customFields[key].colour, type: customFields[key].type }, - cell: customFields[key].type === 'text' ? MakeCustomField : LazyImage, +/** + * we cant use the createColumnHelper() because we have custom logic for rendering the cells + * This means that the display columns: index and action are added inline by the row components + */ +export function makeCuesheetColumns(customFields: CustomFields, cuesheetMode: AppMode): ColumnDef[] { + const columnsDef: ColumnDef[] = []; + const customFieldKeys = Object.keys(customFields); + const modeAllowsWrite = cuesheetMode === AppMode.Edit; + + for (let i = 0; i < customFieldKeys.length; i++) { + const key = customFieldKeys[i]; + columnsDef.push({ + accessorKey: key, + id: key, + header: customFields[key].label, + cell: customFields[key].type === 'text' ? MakeCustomField : LazyImage, + size: 250, + minSize: 75, + meta: { + colour: customFields[key].colour, + canWrite: true, + }, + }); + } + + columnsDef.push({ + accessorKey: 'flag', + id: 'flag', + header: 'Flag', + cell: MakeFlagField, + size: 45, + minSize: 45, + meta: { canWrite: modeAllowsWrite }, + }); + + columnsDef.push({ + accessorKey: 'cue', + id: 'cue', + header: 'Cue', + cell: MakeSingleLineField, + size: 75, + minSize: 40, + meta: { canWrite: modeAllowsWrite }, + }); + + columnsDef.push({ + accessorKey: 'timeStart', + id: 'timeStart', + header: 'Start', + cell: MakeStart, + size: 75, + minSize: 75, + meta: { canWrite: modeAllowsWrite }, + }); + + columnsDef.push({ + accessorKey: 'timeEnd', + id: 'timeEnd', + header: 'End', + cell: MakeEnd, + size: 75, + minSize: 75, + meta: { canWrite: modeAllowsWrite }, + }); + + columnsDef.push({ + accessorKey: 'duration', + id: 'duration', + header: 'Duration', + cell: MakeDuration, + size: 75, + minSize: 75, + meta: { canWrite: modeAllowsWrite }, + }); + + columnsDef.push({ + accessorKey: 'title', + id: 'title', + header: 'Title', + cell: MakeSingleLineField, size: 250, minSize: 75, - })); + meta: { canWrite: modeAllowsWrite }, + }); - return [ - { - accessorKey: 'flag', - id: 'flag', - header: 'Flag', - cell: MakeFlagField, - size: 45, - minSize: 45, - }, - { - accessorKey: 'cue', - id: 'cue', - header: 'Cue', - cell: MakeSingleLineField, - size: 75, - minSize: 40, - }, - { - accessorKey: 'timeStart', - id: 'timeStart', - header: 'Start', - cell: MakeStart, - size: 75, - minSize: 75, - }, - { - accessorKey: 'timeEnd', - id: 'timeEnd', - header: 'End', - cell: MakeEnd, - size: 75, - minSize: 75, - }, - { - accessorKey: 'duration', - id: 'duration', - header: 'Duration', - cell: MakeDuration, - size: 75, - minSize: 75, - }, - { - accessorKey: 'title', - id: 'title', - header: 'Title', - cell: MakeSingleLineField, - size: 250, - minSize: 75, - }, - { - accessorKey: 'note', - id: 'note', - header: 'Note', - cell: MakeMultiLineField, - size: 250, - minSize: 75, - }, - ...dynamicCustomFields, - ]; + columnsDef.push({ + accessorKey: 'note', + id: 'note', + header: 'Note', + cell: MakeMultiLineField, + size: 250, + minSize: 75, + meta: { canWrite: modeAllowsWrite }, + }); + + return columnsDef; } diff --git a/apps/server/src/api-data/rundown/rundown.service.ts b/apps/server/src/api-data/rundown/rundown.service.ts index ed50bf181..0d67cde89 100644 --- a/apps/server/src/api-data/rundown/rundown.service.ts +++ b/apps/server/src/api-data/rundown/rundown.service.ts @@ -38,8 +38,6 @@ export async function addEntry(eventData: EventPostPayload): Promise