Compare commits

...

5 Commits

Author SHA1 Message Date
arc-alex 6c460988f1 fix test 2025-05-28 17:06:08 +02:00
arc-alex 00dfdb6d9f option to hide custom fields 2025-05-28 17:02:26 +02:00
arc-alex 0ff5f1b79b deffiretiate main and custom edits 2025-05-28 17:02:26 +02:00
arc-alex 549c291f56 bump version 2025-05-28 08:15:03 +02:00
arc-alex d89309a120 lock cusheet edits 2025-05-27 15:33:39 +02:00
22 changed files with 195 additions and 45 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@getontime/cli", "name": "@getontime/cli",
"version": "3.15.2", "version": "3.16.1-alpha.2",
"author": "Carlos Valente", "author": "Carlos Valente",
"description": "Time keeping for live events", "description": "Time keeping for live events",
"repository": "https://github.com/cpvalente/ontime", "repository": "https://github.com/cpvalente/ontime",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "ontime-ui", "name": "ontime-ui",
"version": "3.15.2", "version": "3.16.1-alpha.2",
"private": true, "private": true,
"type": "module", "type": "module",
"dependencies": { "dependencies": {
+10 -1
View File
@@ -20,8 +20,17 @@ export async function generateUrl(
baseUrl: string, baseUrl: string,
path: string, path: string,
lock: boolean, lock: boolean,
lockMainFields: boolean,
lockCustomFields: boolean,
authenticate: boolean, authenticate: boolean,
): Promise<string> { ): Promise<string> {
const res = await axios.post(`${sessionPath}/url`, { baseUrl, path, lock, authenticate }); const res = await axios.post(`${sessionPath}/url`, {
baseUrl,
path,
lock,
lockMainFields,
lockCustomFields,
authenticate,
});
return res.data.url; return res.data.url;
} }
@@ -25,4 +25,4 @@ export const AutoTextArea = (props: TextareaProps & { inputref: RefObject<unknow
{...props} {...props}
/> />
); );
}; };
@@ -64,13 +64,13 @@ describe('getRouteFromPreset()', () => {
describe('handle url sharing edge cases', () => { describe('handle url sharing edge cases', () => {
it('finds the correct preset when the url contains extra arguments', () => { it('finds the correct preset when the url contains extra arguments', () => {
const location = resolvePath('/demopage?locked=true&token=123'); const location = resolvePath('/demopage?locked=true&token=123');
expect(getRouteFromPreset(location, presets)?.startsWith('timer?user=guest&alias=demopage')).toBeTruthy() expect(getRouteFromPreset(location, presets)?.startsWith('timer?user=guest&alias=demopage')).toBeTruthy();
}) });
it('appends the feature params to the alias', () => { it('appends the feature params to the alias', () => {
const location = resolvePath('/demopage?locked=true&token=123'); const location = resolvePath('/demopage?locked=true&token=123');
expect(getRouteFromPreset(location, presets)).toBe('timer?user=guest&alias=demopage&locked=true&token=123') expect(getRouteFromPreset(location, presets)).toBe('timer?user=guest&alias=demopage&locked=true&token=123');
}) });
}); });
}); });
@@ -79,29 +79,30 @@ describe('generatePathFromPreset()', () => {
['timer?user=guest', 'demopage', 'timer?user=guest&alias=demopage'], ['timer?user=guest', 'demopage', 'timer?user=guest&alias=demopage'],
['timer?user=admin', 'demopage', 'timer?user=admin&alias=demopage'], ['timer?user=admin', 'demopage', 'timer?user=admin&alias=demopage'],
])('generates a path from a preset: %s', (path, alias, expected) => { ])('generates a path from a preset: %s', (path, alias, expected) => {
expect(generatePathFromPreset(path, alias, null, null)).toEqual(expected); expect(generatePathFromPreset(path, alias, null, null, null, null)).toEqual(expected);
}); });
test('appends the feature params to the alias', () => { test('appends the feature params to the alias', () => {
expect(generatePathFromPreset('timer?user=guest', 'demopage', 'true', '123')).toBe('timer?user=guest&alias=demopage&locked=true&token=123'); expect(generatePathFromPreset('timer?user=guest', 'demopage', 'true', '123', null, null)).toBe(
'timer?user=guest&alias=demopage&locked=true&token=123',
);
}); });
}); });
describe('arePathsEquivalent()', () => { describe('arePathsEquivalent()', () => {
it("checks whether the paths match", () => { it('checks whether the paths match', () => {
expect(arePathsEquivalent('demopage', 'timer')).toBeFalsy(); expect(arePathsEquivalent('demopage', 'timer')).toBeFalsy();
expect(arePathsEquivalent('timer', 'timer')).toBeTruthy(); expect(arePathsEquivalent('timer', 'timer')).toBeTruthy();
expect(arePathsEquivalent('timer?user=guest', 'timer?user=guest')).toBeTruthy(); expect(arePathsEquivalent('timer?user=guest', 'timer?user=guest')).toBeTruthy();
}) });
it("checks whether the params match", () => { it('checks whether the params match', () => {
expect(arePathsEquivalent('timer?test=a', 'timer?test=b')).toBeFalsy(); expect(arePathsEquivalent('timer?test=a', 'timer?test=b')).toBeFalsy();
expect(arePathsEquivalent('timer?test=a', 'timer?test=a')).toBeTruthy(); expect(arePathsEquivalent('timer?test=a', 'timer?test=a')).toBeTruthy();
}) });
it("considers edge cases for the url sharing feature", () => { it('considers edge cases for the url sharing feature', () => {
expect(arePathsEquivalent('timer?test=a&locked=true=token=123', 'timer?test=b')).toBeFalsy(); expect(arePathsEquivalent('timer?test=a&locked=true=token=123', 'timer?test=b')).toBeFalsy();
expect(arePathsEquivalent('timer?test=a&locked=true=token=123', 'timer?test=a')).toBeTruthy(); expect(arePathsEquivalent('timer?test=a&locked=true=token=123', 'timer?test=a')).toBeTruthy();
}) });
}); });
+24 -6
View File
@@ -45,11 +45,14 @@ export function getRouteFromPreset(location: Path, urlPresets: URLPreset[]): str
const locked = searchParams.get('locked'); const locked = searchParams.get('locked');
const token = searchParams.get('token'); const token = searchParams.get('token');
const lmain = searchParams.get('lmain');
const lcustom = searchParams.get('lcustom');
// we need to check if the whole url is an alias // we need to check if the whole url is an alias
const foundPreset = urlPresets.find((preset) => preset.alias === removeTrailingSlash(currentURL) && preset.enabled); const foundPreset = urlPresets.find((preset) => preset.alias === removeTrailingSlash(currentURL) && preset.enabled);
if (foundPreset) { if (foundPreset) {
// if so, we can redirect to the preset path // if so, we can redirect to the preset path
return generatePathFromPreset(foundPreset.pathAndParams, foundPreset.alias, locked, token); return generatePathFromPreset(foundPreset.pathAndParams, foundPreset.alias, locked, token, lmain, lcustom);
} }
// if the current url is not an alias, we check if the alias is in the search parameters // if the current url is not an alias, we check if the alias is in the search parameters
@@ -63,7 +66,7 @@ export function getRouteFromPreset(location: Path, urlPresets: URLPreset[]): str
for (const preset of urlPresets) { for (const preset of urlPresets) {
// if the page has a known enabled alias, we check if we need to redirect // if the page has a known enabled alias, we check if we need to redirect
if (preset.alias === presetOnPage && preset.enabled) { if (preset.alias === presetOnPage && preset.enabled) {
const newPath = generatePathFromPreset(preset.pathAndParams, preset.alias, locked, token); const newPath = generatePathFromPreset(preset.pathAndParams, preset.alias, locked, token, lmain, lcustom);
if (!arePathsEquivalent(currentPath, newPath)) { if (!arePathsEquivalent(currentPath, newPath)) {
// if current path is out of date // if current path is out of date
// return new path so we can redirect // return new path so we can redirect
@@ -77,7 +80,14 @@ export function getRouteFromPreset(location: Path, urlPresets: URLPreset[]): str
/** /**
* Handles generating a path and search parameters from a preset * Handles generating a path and search parameters from a preset
*/ */
export function generatePathFromPreset(pathAndParams: string, alias: string, locked: string | null, token: string | null ): string { export function generatePathFromPreset(
pathAndParams: string,
alias: string,
locked: string | null,
token: string | null,
lmain: string | null,
lcustom: string | null,
): string {
const path = resolvePath(pathAndParams); const path = resolvePath(pathAndParams);
const searchParams = new URLSearchParams(path.search); const searchParams = new URLSearchParams(path.search);
@@ -93,6 +103,14 @@ export function generatePathFromPreset(pathAndParams: string, alias: string, loc
searchParams.set('token', token); searchParams.set('token', token);
} }
if (lmain) {
searchParams.set('lmain', lmain);
}
if (lcustom) {
searchParams.set('lcustom', lcustom);
}
// return path concatenated without the leading slash // return path concatenated without the leading slash
return `${path.pathname}?${searchParams}`.substring(1); return `${path.pathname}?${searchParams}`.substring(1);
} }
@@ -106,16 +124,16 @@ export function generatePathFromPreset(pathAndParams: string, alias: string, loc
export function arePathsEquivalent(currentPath: string, newPath: string): boolean { export function arePathsEquivalent(currentPath: string, newPath: string): boolean {
const currentUrl = new URL(currentPath, document.location.origin); const currentUrl = new URL(currentPath, document.location.origin);
const newUrl = new URL(newPath, document.location.origin); const newUrl = new URL(newPath, document.location.origin);
// check path // check path
if (currentUrl.pathname !== newUrl.pathname) { if (currentUrl.pathname !== newUrl.pathname) {
return false return false;
} }
// check search params // check search params
// if the params match, we dont need further checks // if the params match, we dont need further checks
if (currentUrl.searchParams.toString() === newUrl.searchParams.toString()) { if (currentUrl.searchParams.toString() === newUrl.searchParams.toString()) {
return true return true;
} }
// if there is no match, we check the edge cases for the url sharing feature // if there is no match, we check the edge cases for the url sharing feature
+2
View File
@@ -31,6 +31,8 @@ declare module '@tanstack/react-table' {
options: { options: {
showDelayedTimes: boolean; showDelayedTimes: boolean;
hideTableSeconds: boolean; hideTableSeconds: boolean;
allowMainEdits: boolean;
allowCustomEdits: boolean;
}; };
} }
} }
@@ -20,6 +20,8 @@ interface GenerateLinkFormOptions {
baseUrl: string; baseUrl: string;
path: string; path: string;
lock: boolean; lock: boolean;
lockMainFields: boolean;
lockCustomFields: boolean;
authenticate: boolean; authenticate: boolean;
} }
@@ -35,13 +37,15 @@ export default function GenerateLinkForm() {
handleSubmit, handleSubmit,
register, register,
setError, setError,
formState: { errors }, formState: { errors, dirtyFields },
} = useForm<GenerateLinkFormOptions>({ } = useForm<GenerateLinkFormOptions>({
mode: 'onChange', mode: 'onChange',
defaultValues: { defaultValues: {
baseUrl: currentHostName, baseUrl: currentHostName,
path: '', path: '',
lock: false, lock: false,
lockMainFields: false,
lockCustomFields: false,
authenticate: false, authenticate: false,
}, },
resetOptions: { resetOptions: {
@@ -53,7 +57,14 @@ export default function GenerateLinkForm() {
try { try {
setFormState('loading'); setFormState('loading');
const baseUrl = linkToOtherHost(options.baseUrl); const baseUrl = linkToOtherHost(options.baseUrl);
const url = await generateUrl(baseUrl, options.path, options.lock, options.authenticate); const url = await generateUrl(
baseUrl,
options.path,
options.lock,
options.lockMainFields,
options.lockCustomFields,
options.authenticate,
);
await copyToClipboard(url); await copyToClipboard(url);
setUrl(url); setUrl(url);
setFormState('success'); setFormState('success');
@@ -119,6 +130,18 @@ export default function GenerateLinkForm() {
/> />
<Switch variant='ontime' size='lg' {...register('lock')} /> <Switch variant='ontime' size='lg' {...register('lock')} />
</Panel.ListItem> </Panel.ListItem>
{dirtyFields.lock && (
<>
<Panel.ListItem>
<Panel.Field title='Lock main field edits' description='Prevent edits to main fields' />
<Switch variant='ontime' size='lg' {...register('lockMainFields')} />
</Panel.ListItem>
<Panel.ListItem>
<Panel.Field title='Lock custom field edits' description='Prevent edits to custom fields' />
<Switch variant='ontime' size='lg' {...register('lockCustomFields')} />
</Panel.ListItem>
</>
)}
<Panel.ListItem> <Panel.ListItem>
<Panel.Field title='Authenticate' description='Whether the URL should be pre-authenticated' /> <Panel.Field title='Authenticate' description='Whether the URL should be pre-authenticated' />
<Switch variant='ontime' size='lg' {...register('authenticate')} /> <Switch variant='ontime' size='lg' {...register('authenticate')} />
@@ -16,7 +16,7 @@ import CuesheetDnd from './cuesheet-dnd/CuesheetDnd';
import CuesheetProgress from './cuesheet-progress/CuesheetProgress'; import CuesheetProgress from './cuesheet-progress/CuesheetProgress';
import { makeCuesheetColumns } from './cuesheet-table/cuesheet-table-elements/cuesheetCols'; import { makeCuesheetColumns } from './cuesheet-table/cuesheet-table-elements/cuesheetCols';
import CuesheetTable from './cuesheet-table/CuesheetTable'; import CuesheetTable from './cuesheet-table/CuesheetTable';
import { cuesheetOptions } from './cuesheet.options'; import { cuesheetOptions, useCuesheetOptions } from './cuesheet.options';
import styles from './CuesheetPage.module.scss'; import styles from './CuesheetPage.module.scss';
@@ -28,8 +28,9 @@ export default function CuesheetPage() {
const { isOpen: isMenuOpen, onOpen, onClose } = useDisclosure(); const { isOpen: isMenuOpen, onOpen, onClose } = useDisclosure();
const { isOpen: isEventEditorOpen, onOpen: onEventEditorOpen, onClose: onEventEditorClose } = useDisclosure(); const { isOpen: isEventEditorOpen, onOpen: onEventEditorOpen, onClose: onEventEditorClose } = useDisclosure();
const [eventId, setEventId] = useState<string | null>(null); const [eventId, setEventId] = useState<string | null>(null);
const { hideCustom } = useCuesheetOptions();
const columns = useMemo(() => makeCuesheetColumns(customFields), [customFields]); const columns = useMemo(() => makeCuesheetColumns(hideCustom ? {} : customFields), [customFields, hideCustom]);
useWindowTitle('Cuesheet'); useWindowTitle('Cuesheet');
@@ -1,4 +1,5 @@
import { useCallback, useRef } from 'react'; import { useCallback, useRef } from 'react';
import { useSearchParams } from 'react-router-dom';
import { useTableNav } from '@table-nav/react'; import { useTableNav } from '@table-nav/react';
import { ColumnDef, getCoreRowModel, useReactTable } from '@tanstack/react-table'; import { ColumnDef, getCoreRowModel, useReactTable } from '@tanstack/react-table';
import { isOntimeEvent, MaybeString, OntimeEvent, OntimeRundown, OntimeRundownEntry, TimeField } from 'ontime-types'; import { isOntimeEvent, MaybeString, OntimeEvent, OntimeRundown, OntimeRundownEntry, TimeField } from 'ontime-types';
@@ -25,6 +26,10 @@ export default function CuesheetTable(props: CuesheetTableProps) {
const { data, columns, showModal } = props; const { data, columns, showModal } = props;
const { updateEvent, updateTimer } = useEventAction(); const { updateEvent, updateTimer } = useEventAction();
const [searchParams] = useSearchParams();
const allowMainEdits = !searchParams.get('lmain');
const allowCustomEdits = !searchParams.get('lcustom');
const { followSelected, showDelayedTimes, hideTableSeconds } = useCuesheetOptions(); const { followSelected, showDelayedTimes, hideTableSeconds } = useCuesheetOptions();
const { columnVisibility, columnOrder, columnSizing, resetColumnOrder, setColumnVisibility, setColumnSizing } = const { columnVisibility, columnOrder, columnSizing, resetColumnOrder, setColumnVisibility, setColumnSizing } =
useColumnManager(columns); useColumnManager(columns);
@@ -77,6 +82,8 @@ export default function CuesheetTable(props: CuesheetTableProps) {
options: { options: {
showDelayedTimes, showDelayedTimes,
hideTableSeconds, hideTableSeconds,
allowMainEdits,
allowCustomEdits,
}, },
}, },
}); });
@@ -6,12 +6,13 @@ import useReactiveTextInput from '../../../../common/components/input/text-input
interface MultiLineCellProps { interface MultiLineCellProps {
initialValue: string; initialValue: string;
handleUpdate: (newValue: string) => void; handleUpdate: (newValue: string) => void;
allowEdits?: boolean;
} }
export default memo(MultiLineCell); export default memo(MultiLineCell);
function MultiLineCell(props: MultiLineCellProps) { function MultiLineCell(props: MultiLineCellProps) {
const { initialValue, handleUpdate } = props; const { initialValue, handleUpdate, allowEdits } = props;
const ref = useRef<HTMLInputElement | null>(null); const ref = useRef<HTMLInputElement | null>(null);
const submitCallback = useCallback((newValue: string) => handleUpdate(newValue), [handleUpdate]); const submitCallback = useCallback((newValue: string) => handleUpdate(newValue), [handleUpdate]);
@@ -38,6 +39,7 @@ function MultiLineCell(props: MultiLineCellProps) {
onBlur={onBlur} onBlur={onBlur}
onKeyDown={onKeyDown} onKeyDown={onKeyDown}
spellCheck={false} spellCheck={false}
isDisabled={!allowEdits}
/> />
); );
} }
@@ -1,17 +1,18 @@
import { forwardRef, memo, useCallback, useImperativeHandle, useRef } from 'react'; import { forwardRef, memo, useCallback, useImperativeHandle, useRef } from 'react';
import { Input } from '@chakra-ui/react'; import { Input, Text } from '@chakra-ui/react';
import useReactiveTextInput from '../../../../common/components/input/text-input/useReactiveTextInput'; import useReactiveTextInput from '../../../../common/components/input/text-input/useReactiveTextInput';
interface SingleLineCellProps { interface SingleLineCellProps {
initialValue: string; initialValue: string;
allowSubmitSameValue?: boolean; allowSubmitSameValue?: boolean;
allowEdits?: boolean;
handleUpdate: (newValue: string) => void; handleUpdate: (newValue: string) => void;
handleCancelUpdate?: () => void; handleCancelUpdate?: () => void;
} }
const SingleLineCell = forwardRef((props: SingleLineCellProps, inputRef) => { const SingleLineCell = forwardRef((props: SingleLineCellProps, inputRef) => {
const { initialValue, allowSubmitSameValue, handleUpdate, handleCancelUpdate } = props; const { initialValue, allowSubmitSameValue, handleUpdate, handleCancelUpdate, allowEdits } = props;
const ref = useRef<HTMLInputElement | null>(null); const ref = useRef<HTMLInputElement | null>(null);
const submitCallback = useCallback((newValue: string) => handleUpdate(newValue), [handleUpdate]); const submitCallback = useCallback((newValue: string) => handleUpdate(newValue), [handleUpdate]);
@@ -38,6 +39,14 @@ const SingleLineCell = forwardRef((props: SingleLineCellProps, inputRef) => {
}; };
}, [ref]); }, [ref]);
if (allowEdits === false) {
return (
<Text ref={ref} size='sm' variant='ontime-transparent' padding={0} fontSize='md'>
{initialValue}
</Text>
);
}
return ( return (
<Input <Input
ref={ref} ref={ref}
@@ -8,6 +8,7 @@ interface TimeInputDurationProps {
initialValue: number; initialValue: number;
lockedValue: boolean; lockedValue: boolean;
delayed?: boolean; delayed?: boolean;
allowEdits?: boolean;
onSubmit: (value: string) => void; onSubmit: (value: string) => void;
} }
@@ -18,7 +19,7 @@ interface ParentFocusableInput extends HTMLInputElement {
export default memo(TimeInputDuration); export default memo(TimeInputDuration);
function TimeInputDuration(props: PropsWithChildren<TimeInputDurationProps>) { function TimeInputDuration(props: PropsWithChildren<TimeInputDurationProps>) {
const { initialValue, lockedValue, delayed, onSubmit, children } = props; const { initialValue, lockedValue, delayed, onSubmit, children, allowEdits } = props;
const [isEditing, setIsEditing] = useState(false); const [isEditing, setIsEditing] = useState(false);
const [value, setValue] = useState(initialValue); const [value, setValue] = useState(initialValue);
@@ -86,7 +87,7 @@ function TimeInputDuration(props: PropsWithChildren<TimeInputDurationProps>) {
const timeString = millisToString(value); const timeString = millisToString(value);
return isEditing ? ( return isEditing && allowEdits ? (
<SingleLineCell <SingleLineCell
ref={inputRef} ref={inputRef}
initialValue={timeString} initialValue={timeString}
@@ -32,7 +32,13 @@ function MakeStart({ getValue, row, table }: CellContext<OntimeRundownEntry, unk
} }
return ( return (
<TimeInput initialValue={startTime} onSubmit={update} lockedValue={isStartLocked} delayed={delayValue !== 0}> <TimeInput
initialValue={startTime}
onSubmit={update}
lockedValue={isStartLocked}
delayed={delayValue !== 0}
allowEdits={table.options.meta?.options.allowMainEdits}
>
{formattedTime} {formattedTime}
<DelayIndicator delayValue={delayValue} tooltipPrefix={millisToString(startTime)} /> <DelayIndicator delayValue={delayValue} tooltipPrefix={millisToString(startTime)} />
</TimeInput> </TimeInput>
@@ -60,7 +66,13 @@ function MakeEnd({ getValue, row, table }: CellContext<OntimeRundownEntry, unkno
} }
return ( return (
<TimeInput initialValue={endTime} onSubmit={update} lockedValue={isEndLocked} delayed={delayValue !== 0}> <TimeInput
initialValue={endTime}
onSubmit={update}
lockedValue={isEndLocked}
delayed={delayValue !== 0}
allowEdits={table.options.meta?.options.allowMainEdits}
>
{formattedTime} {formattedTime}
<DelayIndicator delayValue={delayValue} tooltipPrefix={millisToString(endTime)} /> <DelayIndicator delayValue={delayValue} tooltipPrefix={millisToString(endTime)} />
</TimeInput> </TimeInput>
@@ -81,7 +93,12 @@ function MakeDuration({ getValue, row, table }: CellContext<OntimeRundownEntry,
const formattedDuration = formatDuration(duration, false); const formattedDuration = formatDuration(duration, false);
return ( return (
<TimeInput initialValue={duration} onSubmit={update} lockedValue={isDurationLocked}> <TimeInput
initialValue={duration}
onSubmit={update}
lockedValue={isDurationLocked}
allowEdits={table.options.meta?.options.allowMainEdits}
>
{formattedDuration} {formattedDuration}
</TimeInput> </TimeInput>
); );
@@ -103,7 +120,13 @@ function MakeMultiLineField({ row, column, table }: CellContext<OntimeRundownEnt
const initialValue = event[column.id as keyof OntimeRundownEntry] ?? ''; const initialValue = event[column.id as keyof OntimeRundownEntry] ?? '';
return <MultiLineCell initialValue={initialValue} handleUpdate={update} />; return (
<MultiLineCell
initialValue={initialValue}
handleUpdate={update}
allowEdits={table.options.meta?.options.allowMainEdits}
/>
);
} }
function LazyImage({ row, column, table }: CellContext<OntimeRundownEntry, unknown>) { function LazyImage({ row, column, table }: CellContext<OntimeRundownEntry, unknown>) {
@@ -140,7 +163,13 @@ function MakeSingleLineField({ row, column, table }: CellContext<OntimeRundownEn
const initialValue = event[column.id as keyof OntimeRundownEntry] ?? ''; const initialValue = event[column.id as keyof OntimeRundownEntry] ?? '';
return <SingleLineCell initialValue={initialValue} handleUpdate={update} />; return (
<SingleLineCell
initialValue={initialValue}
handleUpdate={update}
allowEdits={table.options.meta?.options.allowMainEdits}
/>
);
} }
function MakeCustomField({ row, column, table }: CellContext<OntimeRundownEntry, unknown>) { function MakeCustomField({ row, column, table }: CellContext<OntimeRundownEntry, unknown>) {
@@ -158,7 +187,13 @@ function MakeCustomField({ row, column, table }: CellContext<OntimeRundownEntry,
} }
const initialValue = event.custom[column.id] ?? ''; const initialValue = event.custom[column.id] ?? '';
return <MultiLineCell initialValue={initialValue} handleUpdate={update} />; return (
<MultiLineCell
initialValue={initialValue}
handleUpdate={update}
allowEdits={table.options.meta?.options.allowCustomEdits}
/>
);
} }
export function makeCuesheetColumns(customFields: CustomFields): ColumnDef<OntimeRundownEntry>[] { export function makeCuesheetColumns(customFields: CustomFields): ColumnDef<OntimeRundownEntry>[] {
@@ -49,6 +49,13 @@ export const cuesheetOptions: ViewOption[] = [
type: 'boolean', type: 'boolean',
defaultValue: false, defaultValue: false,
}, },
{
id: 'hideCustomColumns',
title: 'Hide custom data columns',
description: 'Whether the hide the custom data in the table',
type: 'boolean',
defaultValue: false,
},
], ],
}, },
{ {
@@ -81,6 +88,7 @@ type CuesheetOptions = {
hideIndexColumn: boolean; hideIndexColumn: boolean;
showDelayedTimes: boolean; showDelayedTimes: boolean;
hideDelays: boolean; hideDelays: boolean;
hideCustom: boolean;
}; };
/** /**
@@ -97,6 +105,7 @@ function getOptionsFromParams(searchParams: URLSearchParams): CuesheetOptions {
hideIndexColumn: isStringBoolean(searchParams.get('hideIndexColumn')), hideIndexColumn: isStringBoolean(searchParams.get('hideIndexColumn')),
showDelayedTimes: isStringBoolean(searchParams.get('showDelayedTimes')), showDelayedTimes: isStringBoolean(searchParams.get('showDelayedTimes')),
hideDelays: isStringBoolean(searchParams.get('hideDelays')), hideDelays: isStringBoolean(searchParams.get('hideDelays')),
hideCustom: isStringBoolean(searchParams.get('hideCustomColumns')),
}; };
} }
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "ontime-electron", "name": "ontime-electron",
"version": "3.15.2", "version": "3.16.1-alpha.2",
"author": "Carlos Valente", "author": "Carlos Valente",
"description": "Time keeping for live events", "description": "Time keeping for live events",
"repository": "https://github.com/cpvalente/ontime", "repository": "https://github.com/cpvalente/ontime",
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "ontime-server", "name": "ontime-server",
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
"version": "3.15.2", "version": "3.16.1-alpha.2",
"exports": "./src/index.js", "exports": "./src/index.js",
"dependencies": { "dependencies": {
"@googleapis/sheets": "^5.0.5", "@googleapis/sheets": "^5.0.5",
@@ -3,17 +3,26 @@ import { generateAuthenticatedUrl } from '../session.service.js';
describe('generateAuthenticatedUrl()', () => { describe('generateAuthenticatedUrl()', () => {
describe('for local IP addresses', () => { describe('for local IP addresses', () => {
it('generates a link without locking or authentication', () => { it('generates a link without locking or authentication', () => {
const localhostNotLocked = generateAuthenticatedUrl('http://localhost:3000', 'timer', false, false); const localhostNotLocked = generateAuthenticatedUrl('http://localhost:3000', 'timer', false, false, false, false);
expect(localhostNotLocked.toString()).toBe('http://localhost:3000/timer'); expect(localhostNotLocked.toString()).toBe('http://localhost:3000/timer');
}); });
it('generates a link with IP locking enabled', () => { it('generates a link with IP locking enabled', () => {
const ipLocked = generateAuthenticatedUrl('http://192.168.10.173:4001', 'timer', true, false); const ipLocked = generateAuthenticatedUrl('http://192.168.10.173:4001', 'timer', true, false, false, false);
expect(ipLocked.toString()).toBe('http://192.168.10.173:4001/timer?locked=true'); expect(ipLocked.toString()).toBe('http://192.168.10.173:4001/timer?locked=true');
}); });
it('generates a link with authentication token and IP locking', () => { it('generates a link with authentication token and IP locking', () => {
const withAuth = generateAuthenticatedUrl('http://192.168.10.173:4001', 'timer', true, true, undefined, '1234'); const withAuth = generateAuthenticatedUrl(
'http://192.168.10.173:4001',
'timer',
true,
false,
false,
true,
undefined,
'1234',
);
expect(withAuth.toString()).toBe('http://192.168.10.173:4001/timer?token=1234&locked=true'); expect(withAuth.toString()).toBe('http://192.168.10.173:4001/timer?token=1234&locked=true');
}); });
}); });
@@ -25,13 +34,23 @@ describe('generateAuthenticatedUrl()', () => {
'timer', 'timer',
false, false,
false, false,
false,
false,
'prefix', 'prefix',
); );
expect(cloudNotLocked.toString()).toBe('https://cloud.getontime.no/prefix/timer'); expect(cloudNotLocked.toString()).toBe('https://cloud.getontime.no/prefix/timer');
}); });
it('generates a link with IP locking enabled', () => { it('generates a link with IP locking enabled', () => {
const ipLocked = generateAuthenticatedUrl('https://cloud.getontime.no/prefix', 'timer', true, false, 'prefix'); const ipLocked = generateAuthenticatedUrl(
'https://cloud.getontime.no/prefix',
'timer',
true,
false,
false,
false,
'prefix',
);
expect(ipLocked.toString()).toBe('https://cloud.getontime.no/prefix/timer?locked=true'); expect(ipLocked.toString()).toBe('https://cloud.getontime.no/prefix/timer?locked=true');
}); });
@@ -40,6 +59,8 @@ describe('generateAuthenticatedUrl()', () => {
'https://cloud.getontime.no/prefix', 'https://cloud.getontime.no/prefix',
'timer', 'timer',
true, true,
false,
false,
true, true,
'prefix', 'prefix',
'1234', '1234',
@@ -31,6 +31,8 @@ export async function generateUrl(req: Request, res: Response<GetUrl | ErrorResp
req.body.baseUrl, req.body.baseUrl,
req.body.path, req.body.path,
req.body.lock, req.body.lock,
req.body.lockMainFields,
req.body.lockCustomFields,
req.body.authenticate, req.body.authenticate,
); );
res.status(200).send({ url: url.toString() }); res.status(200).send({ url: url.toString() });
@@ -59,6 +59,8 @@ export function generateAuthenticatedUrl(
baseUrl: string, baseUrl: string,
path: string, path: string,
lock: boolean, lock: boolean,
lockMainFields: boolean,
lockCustomFields: boolean,
authenticate: boolean, authenticate: boolean,
prefix = routerPrefix, prefix = routerPrefix,
hash = hashedPassword, hash = hashedPassword,
@@ -72,5 +74,11 @@ export function generateAuthenticatedUrl(
if (lock) { if (lock) {
url.searchParams.append('locked', 'true'); url.searchParams.append('locked', 'true');
} }
if (lockMainFields) {
url.searchParams.append('lmain', 'true');
}
if (lockCustomFields) {
url.searchParams.append('lcustom', 'true');
}
return url; return url;
} }
@@ -5,6 +5,8 @@ export const validateGenerateUrl = [
body('baseUrl').exists().isString().notEmpty().trim(), body('baseUrl').exists().isString().notEmpty().trim(),
body('path').exists().isString().trim(), body('path').exists().isString().trim(),
body('lock').exists().isBoolean(), body('lock').exists().isBoolean(),
body('lockMainFields').exists().isBoolean(),
body('lockCustomFields').exists().isBoolean(),
body('authenticate').exists().isBoolean(), body('authenticate').exists().isBoolean(),
(req: Request, res: Response, next: NextFunction) => { (req: Request, res: Response, next: NextFunction) => {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "ontime", "name": "ontime",
"version": "3.15.2", "version": "3.16.1-alpha.2",
"description": "Time keeping for live events", "description": "Time keeping for live events",
"keywords": [ "keywords": [
"ontime", "ontime",