mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-10 01:43:43 +00:00
Compare commits
5 Commits
perf
...
3.16.1-alpha.2
| Author | SHA1 | Date | |
|---|---|---|---|
| 6c460988f1 | |||
| 00dfdb6d9f | |||
| 0ff5f1b79b | |||
| 549c291f56 | |||
| d89309a120 |
@@ -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,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": {
|
||||||
|
|||||||
@@ -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();
|
||||||
})
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
+3
-1
@@ -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}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-2
@@ -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}
|
||||||
|
|||||||
+41
-6
@@ -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,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",
|
||||||
|
|||||||
@@ -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
@@ -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",
|
||||||
|
|||||||
Reference in New Issue
Block a user