mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-31 11:59:10 +00:00
1849b4d39f
* chore: migrate eslint to oxlint * chore: migrate prettier to oxfmt * chore: migrate typescript * chore: toThrow should have a expected value * chore: cast test value as Day * chore: small title fix * chore: mocks should be hoisted * chore: incorrect async useage * chore: test should be inside description * chore: test sohuld include an expeced * chore: oxfmt --------- Co-authored-by: alex-Arc <omnivox@LAPTOP-RC5SNBVV.localdomain>
369 lines
11 KiB
TypeScript
369 lines
11 KiB
TypeScript
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';
|
|
|
|
import DelayIndicator from '../../../../common/components/delay-indicator/DelayIndicator';
|
|
import type { ExtendedEntry } from '../../../../common/utils/rundownMetadata';
|
|
import { formatDuration, formatTime } from '../../../../common/utils/time';
|
|
import { AppMode } from '../../../../ontimeConfig';
|
|
import { getCuesheetColumnAccessPolicy } from '../../cuesheet.policies';
|
|
import DurationInput from './DurationInput';
|
|
import EditableImage from './EditableImage';
|
|
import FlagCell from './FlagCell';
|
|
import GhostedText from './GhostedText';
|
|
import MultiLineCell from './MultiLineCell';
|
|
import MutedText from './MutedText';
|
|
import SingleLineCell from './SingleLineCell';
|
|
import TimeInput from './TimeInput';
|
|
|
|
function getColumnLabel(column: CellContext<ExtendedEntry, unknown>['column']): string {
|
|
return typeof column.columnDef.header === 'string' ? column.columnDef.header : column.id;
|
|
}
|
|
|
|
function MakeStart({ getValue, row, table, column }: CellContext<ExtendedEntry, unknown>) {
|
|
if (!table.options.meta) {
|
|
return null;
|
|
}
|
|
|
|
const { showDelayedTimes, hideTableSeconds } = table.options.meta.options;
|
|
const formatOpts = hideTableSeconds ? { format12: 'h:mm a', format24: 'HH:mm' } : undefined;
|
|
|
|
const event = row.original;
|
|
if (!isOntimeEvent(event)) {
|
|
return <MutedText numeric>{formatTime(getValue() as number, formatOpts)}</MutedText>;
|
|
}
|
|
|
|
const { handleUpdateTimer } = table.options.meta;
|
|
|
|
const update = (newValue: string) => handleUpdateTimer(row.original.id, 'timeStart', newValue);
|
|
|
|
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}
|
|
<DelayIndicator delayValue={event.delay} tooltipPrefix={millisToString(startTime)} />
|
|
</TimeInput>
|
|
);
|
|
}
|
|
|
|
function MakeEnd({ getValue, row, table, column }: CellContext<ExtendedEntry, unknown>) {
|
|
if (!table.options.meta) {
|
|
return null;
|
|
}
|
|
|
|
const { showDelayedTimes, hideTableSeconds } = table.options.meta.options;
|
|
const formatOpts = hideTableSeconds ? { format12: 'h:mm a', format24: 'HH:mm' } : undefined;
|
|
|
|
const event = row.original;
|
|
if (!isOntimeEvent(event)) {
|
|
return <MutedText numeric>{formatTime(getValue() as number, formatOpts)}</MutedText>;
|
|
}
|
|
|
|
const { handleUpdateTimer } = table.options.meta;
|
|
|
|
const update = (newValue: string) => handleUpdateTimer(row.original.id, 'timeEnd', newValue);
|
|
|
|
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}
|
|
<DelayIndicator delayValue={event.delay} tooltipPrefix={millisToString(endTime)} />
|
|
</TimeInput>
|
|
);
|
|
}
|
|
|
|
function MakeDuration({ getValue, row, table, column }: CellContext<ExtendedEntry, unknown>) {
|
|
if (!table.options.meta) {
|
|
return null;
|
|
}
|
|
|
|
const { hideTableSeconds } = table.options.meta.options;
|
|
const event = row.original;
|
|
if (!isOntimeEvent(event)) {
|
|
return <MutedText numeric>{formatDuration(getValue() as number, hideTableSeconds)}</MutedText>;
|
|
}
|
|
|
|
const { handleUpdateTimer } = table.options.meta;
|
|
|
|
const update = (newValue: string) => handleUpdateTimer(row.original.id, 'duration', newValue);
|
|
|
|
const duration = getValue() as number;
|
|
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}
|
|
</DurationInput>
|
|
);
|
|
}
|
|
|
|
function MakeMultiLineField({ row, column, table }: CellContext<ExtendedEntry, unknown>) {
|
|
const update = useCallback(
|
|
(newValue: string) => {
|
|
table.options.meta?.handleUpdate(row.index, column.id, newValue, false);
|
|
},
|
|
[column.id, row.index, table.options.meta],
|
|
);
|
|
|
|
// not all entries have all properties (eg groups)
|
|
const initialValue = row.original[column.id as keyof ExtendedEntry];
|
|
if (typeof initialValue !== 'string') {
|
|
return null;
|
|
}
|
|
|
|
const canWrite = column.columnDef.meta?.canWrite;
|
|
if (!canWrite) {
|
|
return <GhostedText multiline>{initialValue}</GhostedText>;
|
|
}
|
|
|
|
return (
|
|
<MultiLineCell
|
|
initialValue={initialValue as string}
|
|
fieldId={column.id}
|
|
fieldLabel={getColumnLabel(column)}
|
|
handleUpdate={update}
|
|
/>
|
|
);
|
|
}
|
|
|
|
function LazyImage({ row, column, table }: CellContext<ExtendedEntry, unknown>) {
|
|
const update = useCallback(
|
|
(newValue: string) => {
|
|
table.options.meta?.handleUpdate(row.index, column.id, newValue, true);
|
|
},
|
|
[column.id, row.index, table.options.meta],
|
|
);
|
|
|
|
const event = row.original;
|
|
if (isOntimeDelay(event)) {
|
|
return null;
|
|
}
|
|
|
|
const canWrite = column.columnDef.meta?.canWrite;
|
|
const initialValue = event.custom[column.id];
|
|
return <EditableImage initialValue={initialValue} updateValue={update} readOnly={!canWrite} />;
|
|
}
|
|
|
|
function MakeSingleLineField({ row, column, table }: CellContext<ExtendedEntry, unknown>) {
|
|
const update = useCallback(
|
|
(newValue: string) => {
|
|
table.options.meta?.handleUpdate(row.index, column.id, newValue, false);
|
|
},
|
|
[column.id, row.index, table.options.meta],
|
|
);
|
|
|
|
// not all entries have all properties (eg groups)
|
|
const initialValue = row.original[column.id as keyof ExtendedEntry];
|
|
if (typeof initialValue !== 'string') {
|
|
return null;
|
|
}
|
|
|
|
const canWrite = column.columnDef.meta?.canWrite;
|
|
if (!canWrite) {
|
|
return <GhostedText>{initialValue}</GhostedText>;
|
|
}
|
|
|
|
return (
|
|
<SingleLineCell
|
|
initialValue={initialValue as string}
|
|
fieldId={column.id}
|
|
fieldLabel={getColumnLabel(column)}
|
|
handleUpdate={update}
|
|
/>
|
|
);
|
|
}
|
|
|
|
function MakeFlagField({ row }: CellContext<ExtendedEntry, unknown>) {
|
|
const event = row.original;
|
|
if (!isOntimeEvent(event) || !event.flag) {
|
|
return null;
|
|
}
|
|
return <FlagCell />;
|
|
}
|
|
|
|
function MakeCustomField({ row, column, table }: CellContext<ExtendedEntry, unknown>) {
|
|
const update = useCallback(
|
|
(newValue: string) => {
|
|
table.options.meta?.handleUpdate(row.index, column.id, newValue, true);
|
|
},
|
|
[column.id, row.index, table.options.meta],
|
|
);
|
|
|
|
const event = row.original;
|
|
if (isOntimeDelay(event)) {
|
|
return null;
|
|
}
|
|
|
|
// entries will not contain the field if there is no value set by the user
|
|
// event if there is no initial value, we still render the cell
|
|
const initialValue = event.custom[column.id] ?? '';
|
|
|
|
const canWrite = column.columnDef.meta?.canWrite;
|
|
if (!canWrite) {
|
|
return <GhostedText multiline>{initialValue}</GhostedText>;
|
|
}
|
|
|
|
return (
|
|
<MultiLineCell
|
|
initialValue={initialValue}
|
|
fieldId={column.id}
|
|
fieldLabel={getColumnLabel(column)}
|
|
handleUpdate={update}
|
|
/>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* 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,
|
|
preset: URLPreset | undefined,
|
|
): ColumnDef<ExtendedEntry>[] {
|
|
const columnsDef: ColumnDef<ExtendedEntry>[] = [];
|
|
const { canRead, canWrite } = getCuesheetColumnAccessPolicy(preset, cuesheetMode);
|
|
|
|
if (canRead('flag')) {
|
|
columnsDef.push({
|
|
accessorKey: 'flag',
|
|
id: 'flag',
|
|
header: 'Flag',
|
|
cell: MakeFlagField,
|
|
size: 45,
|
|
minSize: 45,
|
|
meta: { canWrite: canWrite('flag') },
|
|
});
|
|
}
|
|
|
|
if (canRead('cue')) {
|
|
columnsDef.push({
|
|
accessorKey: 'cue',
|
|
id: 'cue',
|
|
header: 'Cue',
|
|
cell: MakeSingleLineField,
|
|
size: 75,
|
|
minSize: 40,
|
|
meta: { canWrite: canWrite('cue') },
|
|
});
|
|
}
|
|
|
|
if (canRead('timeStart')) {
|
|
columnsDef.push({
|
|
accessorKey: 'timeStart',
|
|
id: 'timeStart',
|
|
header: 'Start',
|
|
cell: MakeStart,
|
|
size: 75,
|
|
minSize: 75,
|
|
meta: { canWrite: canWrite('timeStart') },
|
|
});
|
|
}
|
|
|
|
if (canRead('timeEnd')) {
|
|
columnsDef.push({
|
|
accessorKey: 'timeEnd',
|
|
id: 'timeEnd',
|
|
header: 'End',
|
|
cell: MakeEnd,
|
|
size: 75,
|
|
minSize: 75,
|
|
meta: { canWrite: canWrite('timeEnd') },
|
|
});
|
|
}
|
|
|
|
if (canRead('duration')) {
|
|
columnsDef.push({
|
|
accessorKey: 'duration',
|
|
id: 'duration',
|
|
header: 'Duration',
|
|
cell: MakeDuration,
|
|
size: 75,
|
|
minSize: 75,
|
|
meta: { canWrite: canWrite('duration') },
|
|
});
|
|
}
|
|
|
|
if (canRead('title')) {
|
|
columnsDef.push({
|
|
accessorKey: 'title',
|
|
id: 'title',
|
|
header: 'Title',
|
|
cell: MakeSingleLineField,
|
|
size: 250,
|
|
minSize: 75,
|
|
meta: { canWrite: canWrite('title') },
|
|
});
|
|
}
|
|
|
|
if (canRead('note')) {
|
|
columnsDef.push({
|
|
accessorKey: 'note',
|
|
id: 'note',
|
|
header: 'Note',
|
|
cell: MakeMultiLineField,
|
|
size: 250,
|
|
minSize: 75,
|
|
meta: { canWrite: canWrite('note') },
|
|
});
|
|
}
|
|
|
|
// custom fields at the end
|
|
const customFieldKeys = Object.keys(customFields);
|
|
|
|
for (let i = 0; i < customFieldKeys.length; i++) {
|
|
const key = customFieldKeys[i];
|
|
const permissionKey = `custom-${key}`;
|
|
if (!canRead(permissionKey)) continue;
|
|
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: canWrite(permissionKey),
|
|
},
|
|
});
|
|
}
|
|
|
|
return columnsDef;
|
|
}
|