mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-09 09:23:51 +00:00
refactor: block writing to base fields in run mode
This commit is contained in:
committed by
Carlos Valente
parent
90105a199a
commit
44508ce8b4
-1
@@ -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');
|
||||
});
|
||||
|
||||
+15
-1
@@ -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<TData extends RowData, TValue> {
|
||||
canWrite: boolean;
|
||||
colour?: string;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
<NavigationMenu isOpen={isMenuOpen} onClose={menuHandler.close} />
|
||||
@@ -43,9 +32,7 @@ export default function CuesheetPage() {
|
||||
)}
|
||||
</CuesheetOverview>
|
||||
<CuesheetProgress />
|
||||
<CuesheetDnd columns={columns}>
|
||||
{isLoading ? <EmptyPage text='Loading...' /> : <CuesheetTable data={flatRundown} columns={columns} />}
|
||||
</CuesheetDnd>
|
||||
<CuesheetTableWrapper />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -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 (
|
||||
<CuesheetDnd columns={columns}>
|
||||
{isLoading ? <EmptyPage text='Loading...' /> : <CuesheetTable data={flatRundown} columns={columns} />}
|
||||
</CuesheetDnd>
|
||||
);
|
||||
}
|
||||
+8
-4
@@ -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) {
|
||||
)}
|
||||
<SortableContext key={key} items={headerGroup.headers} strategy={horizontalListSortingStrategy}>
|
||||
{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 (
|
||||
|
||||
+1
-2
@@ -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,
|
||||
};
|
||||
|
||||
+121
-78
@@ -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<OntimeEntry, unknown>) {
|
||||
function MakeStart({ getValue, row, table, column }: CellContext<OntimeEntry, unknown>) {
|
||||
if (!table.options.meta) {
|
||||
return null;
|
||||
}
|
||||
@@ -33,11 +34,18 @@ function MakeStart({ getValue, row, table }: CellContext<OntimeEntry, unknown>)
|
||||
|
||||
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 (
|
||||
<MutedText numeric>
|
||||
{formattedTime}
|
||||
<DelayIndicator delayValue={event.delay} tooltipPrefix={millisToString(startTime)} />
|
||||
</MutedText>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<TimeInput initialValue={startTime} onSubmit={update} lockedValue={isStartLocked} delayed={event.delay !== 0}>
|
||||
{formattedTime}
|
||||
@@ -46,7 +54,7 @@ function MakeStart({ getValue, row, table }: CellContext<OntimeEntry, unknown>)
|
||||
);
|
||||
}
|
||||
|
||||
function MakeEnd({ getValue, row, table }: CellContext<OntimeEntry, unknown>) {
|
||||
function MakeEnd({ getValue, row, table, column }: CellContext<OntimeEntry, unknown>) {
|
||||
if (!table.options.meta) {
|
||||
return null;
|
||||
}
|
||||
@@ -65,11 +73,19 @@ function MakeEnd({ getValue, row, table }: CellContext<OntimeEntry, unknown>) {
|
||||
|
||||
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 (
|
||||
<MutedText numeric>
|
||||
{formattedTime}
|
||||
<DelayIndicator delayValue={event.delay} tooltipPrefix={millisToString(endTime)} />
|
||||
</MutedText>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<TimeInput initialValue={endTime} onSubmit={update} lockedValue={isEndLocked} delayed={event.delay !== 0}>
|
||||
{formattedTime}
|
||||
@@ -78,7 +94,7 @@ function MakeEnd({ getValue, row, table }: CellContext<OntimeEntry, unknown>) {
|
||||
);
|
||||
}
|
||||
|
||||
function MakeDuration({ getValue, row, table }: CellContext<OntimeEntry, unknown>) {
|
||||
function MakeDuration({ getValue, row, table, column }: CellContext<OntimeEntry, unknown>) {
|
||||
if (!table.options.meta) {
|
||||
return null;
|
||||
}
|
||||
@@ -97,6 +113,11 @@ function MakeDuration({ getValue, row, table }: CellContext<OntimeEntry, unknown
|
||||
const isDurationLocked = event.timeStrategy === TimeStrategy.LockDuration;
|
||||
const formattedDuration = formatDuration(duration, hideTableSeconds);
|
||||
|
||||
const canWrite = column.columnDef.meta?.canWrite;
|
||||
if (!canWrite) {
|
||||
return <MutedText numeric>{formattedDuration}</MutedText>;
|
||||
}
|
||||
|
||||
return (
|
||||
<DurationInput initialValue={duration} onSubmit={update} lockedValue={isDurationLocked}>
|
||||
{formattedDuration}
|
||||
@@ -182,78 +203,100 @@ function MakeCustomField({ row, column, table }: CellContext<OntimeEntry, unknow
|
||||
return <MultiLineCell initialValue={initialValue} handleUpdate={update} />;
|
||||
}
|
||||
|
||||
export function makeCuesheetColumns(customFields: CustomFields): ColumnDef<OntimeEntry>[] {
|
||||
/**
|
||||
* 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<OntimeEntry>[] {
|
||||
const columnsDef: ColumnDef<OntimeEntry>[] = [];
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -38,8 +38,6 @@ export async function addEntry(eventData: EventPostPayload): Promise<OntimeEntry
|
||||
// the parent can be provided or inferred from position
|
||||
let parent: OntimeBlock | null = null;
|
||||
|
||||
console.log('Adding entry with data:', eventData);
|
||||
|
||||
if ('parent' in eventData && eventData.parent != null) {
|
||||
// if the user provides a parent (inside a group), we make sure it exists and it is a group
|
||||
const maybeParent = rundown.entries[eventData.parent];
|
||||
|
||||
Reference in New Issue
Block a user