mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-11 10:23:54 +00:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9ba5ebd4bd | |||
| 3f1f06f7c5 | |||
| a78586d2fa | |||
| 8e93b0af25 | |||
| 9a5d50d933 | |||
| 1189a94bff | |||
| f128f7acd2 | |||
| 80ec2d1186 | |||
| 18e6f0d7c4 | |||
| 48f2511764 |
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@getontime/cli",
|
"name": "@getontime/cli",
|
||||||
"version": "4.2.0",
|
"version": "4.2.1",
|
||||||
"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": "4.2.0",
|
"version": "4.2.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -17,7 +17,6 @@
|
|||||||
"@tanstack/react-table": "^8.21.3",
|
"@tanstack/react-table": "^8.21.3",
|
||||||
"autosize": "^6.0.1",
|
"autosize": "^6.0.1",
|
||||||
"axios": "^1.12.2",
|
"axios": "^1.12.2",
|
||||||
"babel-plugin-react-compiler": "19.1.0-rc.3",
|
|
||||||
"csv-stringify": "^6.6.0",
|
"csv-stringify": "^6.6.0",
|
||||||
"prismjs": "^1.30.0",
|
"prismjs": "^1.30.0",
|
||||||
"react": "^19.1.1",
|
"react": "^19.1.1",
|
||||||
@@ -66,6 +65,7 @@
|
|||||||
"@typescript-eslint/eslint-plugin": "catalog:",
|
"@typescript-eslint/eslint-plugin": "catalog:",
|
||||||
"@typescript-eslint/parser": "catalog:",
|
"@typescript-eslint/parser": "catalog:",
|
||||||
"@vitejs/plugin-react": "4.5.1",
|
"@vitejs/plugin-react": "4.5.1",
|
||||||
|
"babel-plugin-react-compiler": "1.0.0",
|
||||||
"eslint": "catalog:",
|
"eslint": "catalog:",
|
||||||
"eslint-config-prettier": "catalog:",
|
"eslint-config-prettier": "catalog:",
|
||||||
"eslint-plugin-jest": "^28.6.0",
|
"eslint-plugin-jest": "^28.6.0",
|
||||||
|
|||||||
@@ -75,7 +75,10 @@ export async function postAddEntry(
|
|||||||
/**
|
/**
|
||||||
* HTTP request to edit an entry
|
* HTTP request to edit an entry
|
||||||
*/
|
*/
|
||||||
export async function putEditEntry(rundownId: RundownId, data: Partial<OntimeEntry>): Promise<AxiosResponse<OntimeEntry>> {
|
export async function putEditEntry(
|
||||||
|
rundownId: RundownId,
|
||||||
|
data: Partial<OntimeEntry>,
|
||||||
|
): Promise<AxiosResponse<OntimeEntry>> {
|
||||||
return axios.put(`${rundownPath}/${rundownId}/entry`, data);
|
return axios.put(`${rundownPath}/${rundownId}/entry`, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,7 +110,11 @@ export async function patchReorderEntry(rundownId: RundownId, data: ReorderEntry
|
|||||||
/**
|
/**
|
||||||
* HTTP request to swap two events
|
* HTTP request to swap two events
|
||||||
*/
|
*/
|
||||||
export async function requestEventSwap(rundownId: RundownId, from: EntryId, to: EntryId): Promise<AxiosResponse<Rundown>> {
|
export async function requestEventSwap(
|
||||||
|
rundownId: RundownId,
|
||||||
|
from: EntryId,
|
||||||
|
to: EntryId,
|
||||||
|
): Promise<AxiosResponse<Rundown>> {
|
||||||
return axios.patch(`${rundownPath}/${rundownId}/swap`, { from, to });
|
return axios.patch(`${rundownPath}/${rundownId}/swap`, { from, to });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,8 +128,12 @@ export async function requestApplyDelay(rundownId: RundownId, delayId: EntryId):
|
|||||||
/**
|
/**
|
||||||
* HTTP request for cloning an entry
|
* HTTP request for cloning an entry
|
||||||
*/
|
*/
|
||||||
export async function postCloneEntry(rundownId: RundownId, entryId: EntryId): Promise<AxiosResponse<Rundown>> {
|
export async function postCloneEntry(
|
||||||
return axios.post(`${rundownPath}/${rundownId}/clone/${entryId}`);
|
rundownId: RundownId,
|
||||||
|
entryId: EntryId,
|
||||||
|
options?: { before?: EntryId; after?: EntryId },
|
||||||
|
): Promise<AxiosResponse<Rundown>> {
|
||||||
|
return axios.post(`${rundownPath}/${rundownId}/clone/${entryId}`, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { Dialog } from '@base-ui-components/react/dialog';
|
|||||||
import { useDisclosure, useFullscreen } from '@mantine/hooks';
|
import { useDisclosure, useFullscreen } from '@mantine/hooks';
|
||||||
|
|
||||||
import { isLocalhost, supportsFullscreen } from '../../../externals';
|
import { isLocalhost, supportsFullscreen } from '../../../externals';
|
||||||
import { useKeepAwakeOptions } from '../../../features/keep-awake/KeepAwake';
|
import { canUseWakeLock, useKeepAwakeOptions } from '../../../features/keep-awake/useWakeLock';
|
||||||
import { navigatorConstants } from '../../../viewerConfig';
|
import { navigatorConstants } from '../../../viewerConfig';
|
||||||
import { useIsSmallScreen } from '../../hooks/useIsSmallScreen';
|
import { useIsSmallScreen } from '../../hooks/useIsSmallScreen';
|
||||||
import { useClientStore } from '../../stores/clientStore';
|
import { useClientStore } from '../../stores/clientStore';
|
||||||
@@ -69,7 +69,7 @@ function NavigationMenu({ isOpen, onClose }: NavigationMenuProps) {
|
|||||||
<IoSwapVertical />
|
<IoSwapVertical />
|
||||||
{mirror && <span className={style.note}>Active</span>}
|
{mirror && <span className={style.note}>Active</span>}
|
||||||
</NavigationMenuItem>
|
</NavigationMenuItem>
|
||||||
{window.isSecureContext && (
|
{canUseWakeLock && (
|
||||||
<NavigationMenuItem active={keepAwake} onClick={toggleKeepAwake}>
|
<NavigationMenuItem active={keepAwake} onClick={toggleKeepAwake}>
|
||||||
Keep Awake
|
Keep Awake
|
||||||
<LuCoffee />
|
<LuCoffee />
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { useCallback } from 'react';
|
|||||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import {
|
import {
|
||||||
EntryId,
|
EntryId,
|
||||||
|
InsertOptions,
|
||||||
isOntimeEvent,
|
isOntimeEvent,
|
||||||
isOntimeGroup,
|
isOntimeGroup,
|
||||||
MaybeString,
|
MaybeString,
|
||||||
@@ -173,7 +174,8 @@ export const useEntryActions = () => {
|
|||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
const { mutateAsync: cloneEntryMutation } = useMutation({
|
const { mutateAsync: cloneEntryMutation } = useMutation({
|
||||||
mutationFn: ([rundownId, entryId]: Parameters<typeof postCloneEntry>) => postCloneEntry(rundownId, entryId),
|
mutationFn: ([rundownId, entryId, options]: Parameters<typeof postCloneEntry>) =>
|
||||||
|
postCloneEntry(rundownId, entryId, options),
|
||||||
onMutate: () => queryClient.cancelQueries({ queryKey: RUNDOWN }),
|
onMutate: () => queryClient.cancelQueries({ queryKey: RUNDOWN }),
|
||||||
onSettled: () => queryClient.invalidateQueries({ queryKey: RUNDOWN }),
|
onSettled: () => queryClient.invalidateQueries({ queryKey: RUNDOWN }),
|
||||||
});
|
});
|
||||||
@@ -182,14 +184,14 @@ export const useEntryActions = () => {
|
|||||||
* Clone an entry
|
* Clone an entry
|
||||||
*/
|
*/
|
||||||
const clone = useCallback(
|
const clone = useCallback(
|
||||||
async (entryId: EntryId) => {
|
async (entryId: EntryId, options?: InsertOptions) => {
|
||||||
try {
|
try {
|
||||||
const rundownId = getCurrentRundownData()?.id;
|
const rundownId = getCurrentRundownData()?.id;
|
||||||
if (!rundownId) {
|
if (!rundownId) {
|
||||||
throw new Error('Rundown not initialised');
|
throw new Error('Rundown not initialised');
|
||||||
}
|
}
|
||||||
|
|
||||||
await cloneEntryMutation([rundownId, entryId]);
|
await cloneEntryMutation([rundownId, entryId, options]);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logAxiosError('Error cloning entry', error);
|
logAxiosError('Error cloning entry', error);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,68 +0,0 @@
|
|||||||
import { EndAction, EntryCustomFields, OntimeEvent, SupportedEntry, TimerType, TimeStrategy } from 'ontime-types';
|
|
||||||
|
|
||||||
import { cloneEvent } from '../clone';
|
|
||||||
|
|
||||||
describe('cloneEvent()', () => {
|
|
||||||
it('creates a stem from a given event', () => {
|
|
||||||
const original: OntimeEvent = {
|
|
||||||
id: 'unique',
|
|
||||||
type: SupportedEntry.Event,
|
|
||||||
flag: false,
|
|
||||||
title: 'title',
|
|
||||||
cue: 'cue',
|
|
||||||
note: 'note',
|
|
||||||
timeStart: 0,
|
|
||||||
duration: 10,
|
|
||||||
timeEnd: 10,
|
|
||||||
timerType: TimerType.CountDown,
|
|
||||||
timeStrategy: TimeStrategy.LockEnd,
|
|
||||||
parent: 'test',
|
|
||||||
linkStart: false,
|
|
||||||
countToEnd: false,
|
|
||||||
endAction: EndAction.None,
|
|
||||||
skip: false,
|
|
||||||
colour: 'F00',
|
|
||||||
revision: 10,
|
|
||||||
timeWarning: 120000,
|
|
||||||
timeDanger: 60000,
|
|
||||||
delay: 0,
|
|
||||||
dayOffset: 0,
|
|
||||||
gap: 0,
|
|
||||||
triggers: [],
|
|
||||||
custom: {
|
|
||||||
lighting: '3',
|
|
||||||
} as EntryCustomFields,
|
|
||||||
};
|
|
||||||
|
|
||||||
const cloned = cloneEvent(original);
|
|
||||||
expect(cloned).not.toBe(original);
|
|
||||||
expect(cloned.custom).not.toBe(original.custom);
|
|
||||||
expect(cloned.triggers).not.toBe(original.triggers);
|
|
||||||
|
|
||||||
expect(cloned).toMatchObject({
|
|
||||||
type: SupportedEntry.Event,
|
|
||||||
flag: original.flag,
|
|
||||||
title: original.title,
|
|
||||||
note: original.note,
|
|
||||||
timeStart: original.timeStart,
|
|
||||||
duration: original.duration,
|
|
||||||
timeEnd: original.timeEnd,
|
|
||||||
timerType: original.timerType,
|
|
||||||
timeStrategy: original.timeStrategy,
|
|
||||||
parent: 'test',
|
|
||||||
countToEnd: original.countToEnd,
|
|
||||||
linkStart: original.linkStart,
|
|
||||||
endAction: original.endAction,
|
|
||||||
skip: original.skip,
|
|
||||||
colour: original.colour,
|
|
||||||
revision: 0,
|
|
||||||
delay: original.delay,
|
|
||||||
dayOffset: original.dayOffset,
|
|
||||||
gap: 0,
|
|
||||||
timeWarning: original.timeWarning,
|
|
||||||
timeDanger: original.timeDanger,
|
|
||||||
triggers: original.triggers,
|
|
||||||
custom: original.custom,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
import { OntimeEvent, SupportedEntry } from 'ontime-types';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description Creates a safe duplicate of an event
|
|
||||||
* @param {OntimeEvent} event
|
|
||||||
* @param {string} [after]
|
|
||||||
* @return {OntimeEvent} clean event
|
|
||||||
*/
|
|
||||||
type ClonedEvent = Omit<OntimeEvent, 'id' | 'cue'>;
|
|
||||||
export const cloneEvent = (event: OntimeEvent): ClonedEvent => {
|
|
||||||
return {
|
|
||||||
type: SupportedEntry.Event,
|
|
||||||
flag: event.flag,
|
|
||||||
title: event.title,
|
|
||||||
note: event.note,
|
|
||||||
timeStart: event.timeStart,
|
|
||||||
duration: event.duration,
|
|
||||||
timeEnd: event.timeEnd,
|
|
||||||
timerType: event.timerType,
|
|
||||||
timeStrategy: event.timeStrategy,
|
|
||||||
countToEnd: event.countToEnd,
|
|
||||||
linkStart: event.linkStart,
|
|
||||||
endAction: event.endAction,
|
|
||||||
skip: event.skip,
|
|
||||||
colour: event.colour,
|
|
||||||
parent: event.parent,
|
|
||||||
revision: 0,
|
|
||||||
delay: event.delay, // the events will be collocated, so having the same metadata is a good start
|
|
||||||
dayOffset: event.dayOffset,
|
|
||||||
gap: 0,
|
|
||||||
timeWarning: event.timeWarning,
|
|
||||||
timeDanger: event.timeDanger,
|
|
||||||
triggers: structuredClone(event.triggers),
|
|
||||||
custom: structuredClone(event.custom),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@@ -1,84 +1,10 @@
|
|||||||
import { use, useCallback, useEffect, useMemo, useState } from 'react';
|
import { useWakelock } from './useWakeLock';
|
||||||
import { useSearchParams } from 'react-router';
|
|
||||||
|
|
||||||
import { PresetContext } from '../../common/context/PresetContext';
|
|
||||||
|
|
||||||
/** @url https://developer.mozilla.org/en-US/docs/Web/API/WakeLock */
|
|
||||||
export default function KeepAwake() {
|
|
||||||
const { keepAwake } = useKeepAwakeOptions();
|
|
||||||
const [wakeLockSentinel, setWakeLockSentinel] = useState<WakeLockSentinel | null>(null);
|
|
||||||
|
|
||||||
const removeLock = () => {
|
|
||||||
if (wakeLockSentinel) wakeLockSentinel.release().finally(() => setWakeLockSentinel(null));
|
|
||||||
};
|
|
||||||
|
|
||||||
const acquireLock = () => {
|
|
||||||
if (!wakeLockSentinel || wakeLockSentinel.released) {
|
|
||||||
setWakeLockSentinel(null);
|
|
||||||
navigator.wakeLock
|
|
||||||
.request('screen')
|
|
||||||
.then((sentinel) => {
|
|
||||||
setWakeLockSentinel(sentinel);
|
|
||||||
})
|
|
||||||
.catch(console.error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const controller = new AbortController();
|
|
||||||
if (keepAwake) {
|
|
||||||
acquireLock();
|
|
||||||
document.addEventListener(
|
|
||||||
'visibilitychange',
|
|
||||||
() => {
|
|
||||||
if (wakeLockSentinel !== null && document.visibilityState === 'visible') {
|
|
||||||
acquireLock();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ signal: controller.signal },
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
removeLock();
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
controller.abort();
|
|
||||||
removeLock();
|
|
||||||
};
|
|
||||||
}, [keepAwake]);
|
|
||||||
|
|
||||||
return <></>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const keepAwakeKey = 'keep-awake';
|
|
||||||
|
|
||||||
function getOptionsFromParams(searchParams: URLSearchParams, defaultValues?: URLSearchParams) {
|
|
||||||
// Helper to get value from either source, prioritizing defaultValues
|
|
||||||
return defaultValues?.has(keepAwakeKey) || searchParams.has(keepAwakeKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hook exposes the keep awake options
|
* Composes the wakelock hook to contain re-renders
|
||||||
*/
|
*/
|
||||||
export function useKeepAwakeOptions() {
|
export default function KeepAwake() {
|
||||||
const [searchParams, setSearchParams] = useSearchParams();
|
useWakelock();
|
||||||
const maybePreset = use(PresetContext);
|
|
||||||
|
|
||||||
const keepAwake = useMemo(() => {
|
return null;
|
||||||
const defaultValues = maybePreset ? new URLSearchParams(maybePreset.search) : undefined;
|
|
||||||
return getOptionsFromParams(searchParams, defaultValues);
|
|
||||||
}, [maybePreset, searchParams]);
|
|
||||||
|
|
||||||
const toggleKeepAwake = useCallback(() => {
|
|
||||||
setSearchParams((searchParams) => {
|
|
||||||
if (keepAwake) {
|
|
||||||
searchParams.delete(keepAwakeKey);
|
|
||||||
} else {
|
|
||||||
searchParams.set(keepAwakeKey, '1');
|
|
||||||
}
|
|
||||||
return searchParams;
|
|
||||||
});
|
|
||||||
}, [keepAwake]);
|
|
||||||
|
|
||||||
return { keepAwake, toggleKeepAwake };
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
import { use, useCallback, useEffect, useMemo, useRef } from 'react';
|
||||||
|
import { useSearchParams } from 'react-router';
|
||||||
|
|
||||||
|
import { PresetContext } from '../../common/context/PresetContext';
|
||||||
|
|
||||||
|
// wakelock is only available in secure contexts
|
||||||
|
// however, that is covered by the navigator check
|
||||||
|
export const canUseWakeLock = typeof window !== 'undefined' && 'wakeLock' in navigator;
|
||||||
|
|
||||||
|
/** @url https://developer.mozilla.org/en-US/docs/Web/API/WakeLock */
|
||||||
|
export function useWakelock() {
|
||||||
|
const { keepAwake } = useKeepAwakeOptions();
|
||||||
|
const wakeLockSentinelRef = useRef<WakeLockSentinel | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const removeLock = () => {
|
||||||
|
if (wakeLockSentinelRef.current) {
|
||||||
|
wakeLockSentinelRef.current.release().finally(() => {
|
||||||
|
wakeLockSentinelRef.current = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const acquireLock = () => {
|
||||||
|
const sentinel = wakeLockSentinelRef.current;
|
||||||
|
if (!sentinel || sentinel.released) {
|
||||||
|
wakeLockSentinelRef.current = null;
|
||||||
|
navigator.wakeLock
|
||||||
|
.request('screen')
|
||||||
|
.then((sentinel) => {
|
||||||
|
wakeLockSentinelRef.current = sentinel;
|
||||||
|
})
|
||||||
|
.catch(console.error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const controller = new AbortController();
|
||||||
|
if (keepAwake) {
|
||||||
|
acquireLock();
|
||||||
|
document.addEventListener(
|
||||||
|
'visibilitychange',
|
||||||
|
() => {
|
||||||
|
if (wakeLockSentinelRef.current !== null && document.visibilityState === 'visible') {
|
||||||
|
acquireLock();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ signal: controller.signal },
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
removeLock();
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
controller.abort();
|
||||||
|
removeLock();
|
||||||
|
};
|
||||||
|
}, [keepAwake]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const keepAwakeKey = 'keep-awake';
|
||||||
|
|
||||||
|
function getOptionsFromParams(searchParams: URLSearchParams, defaultValues?: URLSearchParams) {
|
||||||
|
// Helper to get value from either source, prioritizing defaultValues
|
||||||
|
return defaultValues?.has(keepAwakeKey) || searchParams.has(keepAwakeKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook exposes the keep awake options
|
||||||
|
*/
|
||||||
|
export function useKeepAwakeOptions() {
|
||||||
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
|
const maybePreset = use(PresetContext);
|
||||||
|
|
||||||
|
const keepAwake = useMemo(() => {
|
||||||
|
const defaultValues = maybePreset ? new URLSearchParams(maybePreset.search) : undefined;
|
||||||
|
return getOptionsFromParams(searchParams, defaultValues);
|
||||||
|
}, [maybePreset, searchParams]);
|
||||||
|
|
||||||
|
const toggleKeepAwake = useCallback(() => {
|
||||||
|
setSearchParams((searchParams) => {
|
||||||
|
if (keepAwake) {
|
||||||
|
searchParams.delete(keepAwakeKey);
|
||||||
|
} else {
|
||||||
|
searchParams.set(keepAwakeKey, '1');
|
||||||
|
}
|
||||||
|
return searchParams;
|
||||||
|
});
|
||||||
|
}, [keepAwake, setSearchParams]);
|
||||||
|
|
||||||
|
return { keepAwake, toggleKeepAwake };
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Fragment, lazy, useCallback, useEffect, useRef, useState } from 'react';
|
import { Fragment, lazy, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { TbFlagFilled } from 'react-icons/tb';
|
import { TbFlagFilled } from 'react-icons/tb';
|
||||||
import {
|
import {
|
||||||
closestCenter,
|
closestCenter,
|
||||||
@@ -36,7 +36,6 @@ import { useEntryActions } from '../../common/hooks/useEntryAction';
|
|||||||
import useFollowComponent from '../../common/hooks/useFollowComponent';
|
import useFollowComponent from '../../common/hooks/useFollowComponent';
|
||||||
import { useRundownEditor } from '../../common/hooks/useSocket';
|
import { useRundownEditor } from '../../common/hooks/useSocket';
|
||||||
import { useEntryCopy } from '../../common/stores/entryCopyStore';
|
import { useEntryCopy } from '../../common/stores/entryCopyStore';
|
||||||
import { cloneEvent } from '../../common/utils/clone';
|
|
||||||
import { lastMetadataKey, RundownMetadataObject } from '../../common/utils/rundownMetadata';
|
import { lastMetadataKey, RundownMetadataObject } from '../../common/utils/rundownMetadata';
|
||||||
import { AppMode, sessionKeys } from '../../ontimeConfig';
|
import { AppMode, sessionKeys } from '../../ontimeConfig';
|
||||||
|
|
||||||
@@ -58,6 +57,8 @@ interface RundownProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function Rundown({ data, rundownMetadata }: RundownProps) {
|
export default function Rundown({ data, rundownMetadata }: RundownProps) {
|
||||||
|
'use memo';
|
||||||
|
|
||||||
const { order, entries, id } = data;
|
const { order, entries, id } = data;
|
||||||
// we create a copy of the rundown with a data structured aligned with what dnd-kit needs
|
// we create a copy of the rundown with a data structured aligned with what dnd-kit needs
|
||||||
const featureData = useRundownEditor();
|
const featureData = useRundownEditor();
|
||||||
@@ -68,17 +69,20 @@ export default function Rundown({ data, rundownMetadata }: RundownProps) {
|
|||||||
key: `rundown.${id}-editor-collapsed-groups`,
|
key: `rundown.${id}-editor-collapsed-groups`,
|
||||||
defaultValue: [],
|
defaultValue: [],
|
||||||
});
|
});
|
||||||
|
const collapsedGroupSet = useMemo(() => new Set(collapsedGroups), [collapsedGroups]);
|
||||||
|
|
||||||
const { addEntry, deleteEntry, move, reorderEntry } = useEntryActions();
|
const { addEntry, clone, deleteEntry, move, reorderEntry } = useEntryActions();
|
||||||
|
const setEntryCopyId = useEntryCopy((state) => state.setEntryCopyId);
|
||||||
const { entryCopyId, setEntryCopyId } = useEntryCopy();
|
|
||||||
|
|
||||||
// cursor
|
// cursor
|
||||||
const [editorMode] = useSessionStorage<AppMode>({
|
const [editorMode] = useSessionStorage<AppMode>({
|
||||||
key: sessionKeys.editorMode,
|
key: sessionKeys.editorMode,
|
||||||
defaultValue: AppMode.Edit,
|
defaultValue: AppMode.Edit,
|
||||||
});
|
});
|
||||||
const { clearSelectedEvents, setSelectedEvents, cursor } = useEventSelection();
|
|
||||||
|
const clearSelectedEvents = useEventSelection((state) => state.clearSelectedEvents);
|
||||||
|
const setSelectedEvents = useEventSelection((state) => state.setSelectedEvents);
|
||||||
|
const cursor = useEventSelection((state) => state.cursor);
|
||||||
|
|
||||||
const cursorRef = useRef<HTMLDivElement | null>(null);
|
const cursorRef = useRef<HTMLDivElement | null>(null);
|
||||||
const scrollRef = useRef<HTMLDivElement | null>(null);
|
const scrollRef = useRef<HTMLDivElement | null>(null);
|
||||||
@@ -105,20 +109,30 @@ export default function Rundown({ data, rundownMetadata }: RundownProps) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const insertCopyAtId = useCallback(
|
const insertCopyAtId = useCallback(
|
||||||
(atId: string | null, copyId: string | null, above = false) => {
|
(atId: string | null, above = false) => {
|
||||||
const adjustedCursor = above ? getPreviousNormal(entries, order, atId ?? '').entry?.id ?? null : atId;
|
// lazily get the value from the store
|
||||||
if (copyId === null) {
|
const { entryCopyId } = useEntryCopy.getState();
|
||||||
|
if (entryCopyId === null || !entries[entryCopyId]) {
|
||||||
// we cant clone without selection
|
// we cant clone without selection
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const cloneEntry = entries[copyId];
|
|
||||||
if (cloneEntry?.type === SupportedEntry.Event) {
|
let normalisedAtId = atId;
|
||||||
//if we don't have a cursor add the new event on top
|
|
||||||
const newEvent = cloneEvent(cloneEntry);
|
const elementToCopy = entries[entryCopyId];
|
||||||
addEntry(newEvent, { after: adjustedCursor ?? undefined });
|
const refElement = atId ? entries[atId] : undefined;
|
||||||
|
|
||||||
|
if (refElement && 'parent' in refElement && refElement.parent && elementToCopy.type === SupportedEntry.Group) {
|
||||||
|
normalisedAtId = refElement.parent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
clone(entryCopyId, {
|
||||||
|
after: above ? undefined : normalisedAtId ?? undefined,
|
||||||
|
// if we don't have a cursor add the new event on top
|
||||||
|
before: above ? normalisedAtId ?? undefined : undefined,
|
||||||
|
});
|
||||||
},
|
},
|
||||||
[addEntry, order, entries],
|
[entries, clone],
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -200,9 +214,9 @@ export default function Rundown({ data, rundownMetadata }: RundownProps) {
|
|||||||
*/
|
*/
|
||||||
const getIsCollapsed = useCallback(
|
const getIsCollapsed = useCallback(
|
||||||
(groupId: EntryId): boolean => {
|
(groupId: EntryId): boolean => {
|
||||||
return Boolean(collapsedGroups.find((id) => id === groupId));
|
return collapsedGroupSet.has(groupId);
|
||||||
},
|
},
|
||||||
[collapsedGroups],
|
[collapsedGroupSet],
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -300,12 +314,8 @@ export default function Rundown({ data, rundownMetadata }: RundownProps) {
|
|||||||
],
|
],
|
||||||
|
|
||||||
['mod + C', () => setEntryCopyId(cursor)],
|
['mod + C', () => setEntryCopyId(cursor)],
|
||||||
['mod + V', () => insertCopyAtId(cursor, entryCopyId)],
|
['mod + V', () => insertCopyAtId(cursor)],
|
||||||
[
|
['mod + shift + V', () => insertCopyAtId(cursor, true), { preventDefault: true, usePhysicalKeys: true }],
|
||||||
'mod + shift + V',
|
|
||||||
() => insertCopyAtId(cursor, entryCopyId, true),
|
|
||||||
{ preventDefault: true, usePhysicalKeys: true },
|
|
||||||
],
|
|
||||||
|
|
||||||
['alt + backspace', () => deleteAtCursor(cursor), { preventDefault: true, usePhysicalKeys: true }],
|
['alt + backspace', () => deleteAtCursor(cursor), { preventDefault: true, usePhysicalKeys: true }],
|
||||||
]);
|
]);
|
||||||
@@ -395,7 +405,7 @@ export default function Rundown({ data, rundownMetadata }: RundownProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// keep copy of the current state in case we need to revert
|
// keep copy of the current state in case we need to revert
|
||||||
const currentEntries = structuredClone(sortableData);
|
const currentEntries = [...sortableData];
|
||||||
// we keep a copy of the state as a hack to handle inconsistencies between dnd-kit and async store updates
|
// we keep a copy of the state as a hack to handle inconsistencies between dnd-kit and async store updates
|
||||||
setSortableData((currentEntries) => {
|
setSortableData((currentEntries) => {
|
||||||
return reorderArray(currentEntries, fromIndex, toIndex);
|
return reorderArray(currentEntries, fromIndex, toIndex);
|
||||||
@@ -438,9 +448,12 @@ export default function Rundown({ data, rundownMetadata }: RundownProps) {
|
|||||||
return <RundownEmpty handleAddNew={(type: SupportedEntry) => addEntry({ type })} />;
|
return <RundownEmpty handleAddNew={(type: SupportedEntry) => addEntry({ type })} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1. gather presentation options
|
// gather presentation options
|
||||||
const isEditMode = editorMode === AppMode.Edit;
|
const isEditMode = editorMode === AppMode.Edit;
|
||||||
|
|
||||||
|
// gather rundown wide data
|
||||||
|
const lastEntryId = order.at(-1);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={style.rundownContainer} ref={scrollRef} data-testid='rundown'>
|
<div className={style.rundownContainer} ref={scrollRef} data-testid='rundown'>
|
||||||
<DndContext
|
<DndContext
|
||||||
@@ -509,7 +522,7 @@ export default function Rundown({ data, rundownMetadata }: RundownProps) {
|
|||||||
const groupColour = entryMetadata.groupColour === '' ? '#9d9d9d' : entryMetadata.groupColour;
|
const groupColour = entryMetadata.groupColour === '' ? '#9d9d9d' : entryMetadata.groupColour;
|
||||||
|
|
||||||
const isFirst = index === 0;
|
const isFirst = index === 0;
|
||||||
const isLast = entryId === order.at(-1);
|
const isLast = entryId === lastEntryId;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* We need to provide the parent ID for the QuickAdd components
|
* We need to provide the parent ID for the QuickAdd components
|
||||||
|
|||||||
@@ -1,16 +1,4 @@
|
|||||||
import {
|
import { isOntimeDelay, isOntimeEvent, isOntimeMilestone, OntimeEntry, Playback, SupportedEntry } from 'ontime-types';
|
||||||
isOntimeDelay,
|
|
||||||
isOntimeEvent,
|
|
||||||
isOntimeMilestone,
|
|
||||||
OntimeEntry,
|
|
||||||
OntimeEvent,
|
|
||||||
Playback,
|
|
||||||
SupportedEntry,
|
|
||||||
} from 'ontime-types';
|
|
||||||
|
|
||||||
import { useEntryActions } from '../../common/hooks/useEntryAction';
|
|
||||||
import useMemoisedFn from '../../common/hooks/useMemoisedFn';
|
|
||||||
import { cloneEvent } from '../../common/utils/clone';
|
|
||||||
|
|
||||||
import RundownDelay from './rundown-delay/RundownDelay';
|
import RundownDelay from './rundown-delay/RundownDelay';
|
||||||
import RundownEvent from './rundown-event/RundownEvent';
|
import RundownEvent from './rundown-event/RundownEvent';
|
||||||
@@ -44,13 +32,6 @@ export default function RundownEntry({
|
|||||||
totalGap,
|
totalGap,
|
||||||
isLinkedToLoaded,
|
isLinkedToLoaded,
|
||||||
}: RundownEntryProps) {
|
}: RundownEntryProps) {
|
||||||
const { addEntry } = useEntryActions();
|
|
||||||
|
|
||||||
const createCloneEvent = useMemoisedFn(() => {
|
|
||||||
const newEvent = cloneEvent(data as OntimeEvent);
|
|
||||||
addEntry(newEvent, { after: data.id });
|
|
||||||
});
|
|
||||||
|
|
||||||
if (isOntimeEvent(data)) {
|
if (isOntimeEvent(data)) {
|
||||||
return (
|
return (
|
||||||
<RundownEvent
|
<RundownEvent
|
||||||
@@ -83,7 +64,6 @@ export default function RundownEntry({
|
|||||||
dayOffset={data.dayOffset}
|
dayOffset={data.dayOffset}
|
||||||
totalGap={totalGap}
|
totalGap={totalGap}
|
||||||
isLinkedToLoaded={isLinkedToLoaded}
|
isLinkedToLoaded={isLinkedToLoaded}
|
||||||
createCloneEvent={createCloneEvent}
|
|
||||||
hasTriggers={data.triggers.length > 0}
|
hasTriggers={data.triggers.length > 0}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -56,7 +56,6 @@ interface RundownEventProps {
|
|||||||
dayOffset: number;
|
dayOffset: number;
|
||||||
totalGap: number;
|
totalGap: number;
|
||||||
isLinkedToLoaded: boolean;
|
isLinkedToLoaded: boolean;
|
||||||
createCloneEvent: () => void;
|
|
||||||
hasTriggers: boolean;
|
hasTriggers: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,10 +90,9 @@ export default function RundownEvent({
|
|||||||
totalGap,
|
totalGap,
|
||||||
isLinkedToLoaded,
|
isLinkedToLoaded,
|
||||||
hasTriggers,
|
hasTriggers,
|
||||||
createCloneEvent,
|
|
||||||
}: RundownEventProps) {
|
}: RundownEventProps) {
|
||||||
const { selectedEventId, setSelectedEventId, clearSelectedEventId } = useEventIdSwapping();
|
const { selectedEventId, setSelectedEventId, clearSelectedEventId } = useEventIdSwapping();
|
||||||
const { updateEntry, batchUpdateEvents, deleteEntry, groupEntries, swapEvents } = useEntryActions();
|
const { updateEntry, batchUpdateEvents, clone, deleteEntry, groupEntries, swapEvents } = useEntryActions();
|
||||||
|
|
||||||
const { selectedEvents, unselect, setSelectedEvents, clearSelectedEvents } = useEventSelection();
|
const { selectedEvents, unselect, setSelectedEvents, clearSelectedEvents } = useEventSelection();
|
||||||
const handleRef = useRef<null | HTMLSpanElement>(null);
|
const handleRef = useRef<null | HTMLSpanElement>(null);
|
||||||
@@ -172,7 +170,7 @@ export default function RundownEvent({
|
|||||||
type: 'item',
|
type: 'item',
|
||||||
label: 'Clone',
|
label: 'Clone',
|
||||||
icon: IoDuplicateOutline,
|
icon: IoDuplicateOutline,
|
||||||
onClick: createCloneEvent,
|
onClick: () => clone(eventId, { after: eventId }),
|
||||||
},
|
},
|
||||||
{ type: 'divider' },
|
{ type: 'divider' },
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import { formatTime } from '../../../common/utils/time';
|
|||||||
export function getTimerByType(
|
export function getTimerByType(
|
||||||
freezeEnd: boolean,
|
freezeEnd: boolean,
|
||||||
timerTypeNow: TimerType,
|
timerTypeNow: TimerType,
|
||||||
countToEndNow: boolean,
|
|
||||||
clock: number,
|
clock: number,
|
||||||
timerObject: Pick<TimerState, 'current' | 'elapsed'>,
|
timerObject: Pick<TimerState, 'current' | 'elapsed'>,
|
||||||
timerTypeOverride?: TimerType,
|
timerTypeOverride?: TimerType,
|
||||||
@@ -22,13 +21,6 @@ export function getTimerByType(
|
|||||||
|
|
||||||
const viewTimerType = timerTypeOverride ?? timerTypeNow;
|
const viewTimerType = timerTypeOverride ?? timerTypeNow;
|
||||||
|
|
||||||
if (countToEndNow) {
|
|
||||||
if (timerObject.current === null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return freezeEnd ? Math.max(timerObject.current, 0) : timerObject.current;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (viewTimerType) {
|
switch (viewTimerType) {
|
||||||
case TimerType.CountDown:
|
case TimerType.CountDown:
|
||||||
if (timerObject.current === null) {
|
if (timerObject.current === null) {
|
||||||
@@ -36,7 +28,7 @@ export function getTimerByType(
|
|||||||
}
|
}
|
||||||
return freezeEnd ? Math.max(timerObject.current, 0) : timerObject.current;
|
return freezeEnd ? Math.max(timerObject.current, 0) : timerObject.current;
|
||||||
case TimerType.CountUp:
|
case TimerType.CountUp:
|
||||||
return Math.abs(timerObject.elapsed ?? 0);
|
return timerObject.elapsed;
|
||||||
case TimerType.Clock:
|
case TimerType.Clock:
|
||||||
return clock;
|
return clock;
|
||||||
case TimerType.None:
|
case TimerType.None:
|
||||||
|
|||||||
@@ -37,7 +37,7 @@
|
|||||||
|
|
||||||
.timer {
|
.timer {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
font-family: var(--timer-font, $viewer-font-family);
|
font-family: $viewer-font-family;
|
||||||
color: var(--timer-colour, $ui-white);
|
color: var(--timer-colour, $ui-white);
|
||||||
line-height: 0.9em;
|
line-height: 0.9em;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
@@ -72,17 +72,17 @@
|
|||||||
|
|
||||||
.secondary {
|
.secondary {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
max-height: 100%;
|
||||||
text-overflow: ellipsis;
|
height: auto;
|
||||||
|
|
||||||
margin-top: 0.125em;
|
margin-top: 0.125em;
|
||||||
padding-block: 0.125em;
|
padding-block: 0.125em;
|
||||||
|
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
color: var(--external-color-override, $external-color);
|
color: $external-color;
|
||||||
letter-spacing: 0.5px;
|
letter-spacing: 0.5px;
|
||||||
line-height: 1em;
|
line-height: 1;
|
||||||
transition-property: opacity, height;
|
transition-property: opacity, height;
|
||||||
transition-duration: $viewer-transition-time;
|
transition-duration: $viewer-transition-time;
|
||||||
border-top: 1px solid color-mix(in srgb, $external-color 10%, transparent);
|
border-top: 1px solid color-mix(in srgb, $external-color 10%, transparent);
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ export function PipTimer({ viewSettings }: PipTimerProps) {
|
|||||||
|
|
||||||
// gather timer data
|
// gather timer data
|
||||||
const totalTime = getTotalTime(time.duration, time.addedTime);
|
const totalTime = getTotalTime(time.duration, time.addedTime);
|
||||||
const stageTimer = getTimerByType(false, timerTypeNow, countToEndNow, clock, time, timerTypeNow);
|
const stageTimer = getTimerByType(false, timerTypeNow, clock, time, timerTypeNow);
|
||||||
const display = getFormattedTimer(stageTimer, timerTypeNow, 'min', {
|
const display = getFormattedTimer(stageTimer, timerTypeNow, 'min', {
|
||||||
removeSeconds: false,
|
removeSeconds: false,
|
||||||
removeLeadingZero: false,
|
removeLeadingZero: false,
|
||||||
@@ -59,11 +59,11 @@ export function PipTimer({ viewSettings }: PipTimerProps) {
|
|||||||
return null;
|
return null;
|
||||||
})();
|
})();
|
||||||
|
|
||||||
const secondaryContent = getSecondaryDisplay(message, currentAux, 'min', false, false, false);
|
const secondaryContent = getSecondaryDisplay(message, currentAux, 'min', false, true, false);
|
||||||
|
|
||||||
// gather presentation styles
|
// gather presentation styles
|
||||||
const resolvedTimerColour = getTimerColour(viewSettings, undefined, showWarning, showDanger);
|
const resolvedTimerColour = getTimerColour(viewSettings, undefined, showWarning, showDanger);
|
||||||
const { timerFontSize, externalFontSize } = getEstimatedFontSize(display, secondaryContent);
|
const timerFontSize = getEstimatedFontSize(display, secondaryContent);
|
||||||
const userStyles = {
|
const userStyles = {
|
||||||
...(resolvedTimerColour && { '--timer-colour': resolvedTimerColour }),
|
...(resolvedTimerColour && { '--timer-colour': resolvedTimerColour }),
|
||||||
};
|
};
|
||||||
@@ -84,11 +84,10 @@ export function PipTimer({ viewSettings }: PipTimerProps) {
|
|||||||
>
|
>
|
||||||
{display}
|
{display}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div className={cx(['secondary', !secondaryContent && 'secondary--hidden'])}>
|
||||||
className={cx(['secondary', !secondaryContent && 'secondary--hidden'])}
|
<FitText mode='multi' min={12} max={256}>
|
||||||
style={{ fontSize: `${externalFontSize}vw` }}
|
{secondaryContent}
|
||||||
>
|
</FitText>
|
||||||
{secondaryContent}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -135,8 +135,8 @@
|
|||||||
|
|
||||||
.secondary {
|
.secondary {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
max-height: 100%;
|
||||||
text-overflow: ellipsis;
|
height: auto;
|
||||||
|
|
||||||
margin-top: 0.125em;
|
margin-top: 0.125em;
|
||||||
padding-block: 0.125em;
|
padding-block: 0.125em;
|
||||||
@@ -145,7 +145,7 @@
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
color: var(--external-color-override, $external-color);
|
color: var(--external-color-override, $external-color);
|
||||||
letter-spacing: 0.5px;
|
letter-spacing: 0.5px;
|
||||||
line-height: 1em;
|
line-height: 1;
|
||||||
transition-property: opacity, height;
|
transition-property: opacity, height;
|
||||||
transition-duration: $viewer-transition-time;
|
transition-duration: $viewer-transition-time;
|
||||||
border-top: 1px solid color-mix(in srgb, var(--external-color-override, $external-color) 10%, transparent);
|
border-top: 1px solid color-mix(in srgb, var(--external-color-override, $external-color) 10%, transparent);
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ function Timer({ customFields, projectData, isMirrored, settings, viewSettings }
|
|||||||
// gather timer data
|
// gather timer data
|
||||||
const totalTime = getTotalTime(time.duration, time.addedTime);
|
const totalTime = getTotalTime(time.duration, time.addedTime);
|
||||||
const formattedClock = formatTime(clock);
|
const formattedClock = formatTime(clock);
|
||||||
const stageTimer = getTimerByType(freezeOvertime, timerTypeNow, countToEndNow, clock, time, timerType);
|
const stageTimer = getTimerByType(freezeOvertime, timerTypeNow, clock, time, timerType);
|
||||||
const display = getFormattedTimer(stageTimer, viewTimerType, localisedMinutes, {
|
const display = getFormattedTimer(stageTimer, viewTimerType, localisedMinutes, {
|
||||||
removeSeconds: hideTimerSeconds,
|
removeSeconds: hideTimerSeconds,
|
||||||
removeLeadingZero: removeLeadingZeros,
|
removeLeadingZero: removeLeadingZeros,
|
||||||
@@ -132,7 +132,7 @@ function Timer({ customFields, projectData, isMirrored, settings, viewSettings }
|
|||||||
|
|
||||||
// gather presentation styles
|
// gather presentation styles
|
||||||
const resolvedTimerColour = getTimerColour(viewSettings, timerColour, showWarning, showDanger);
|
const resolvedTimerColour = getTimerColour(viewSettings, timerColour, showWarning, showDanger);
|
||||||
const { timerFontSize, externalFontSize } = getEstimatedFontSize(display, secondaryContent);
|
const timerFontSize = getEstimatedFontSize(display, secondaryContent);
|
||||||
const userStyles = {
|
const userStyles = {
|
||||||
...(keyColour && { '--timer-bg': keyColour }),
|
...(keyColour && { '--timer-bg': keyColour }),
|
||||||
...(resolvedTimerColour && { '--timer-colour': resolvedTimerColour }),
|
...(resolvedTimerColour && { '--timer-colour': resolvedTimerColour }),
|
||||||
@@ -185,11 +185,10 @@ function Timer({ customFields, projectData, isMirrored, settings, viewSettings }
|
|||||||
{display}
|
{display}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div
|
<div className={cx(['secondary', !secondaryContent && 'secondary--hidden'])}>
|
||||||
className={cx(['secondary', !secondaryContent && 'secondary--hidden'])}
|
<FitText mode='multi' min={64} max={256}>
|
||||||
style={{ fontSize: `${externalFontSize}vw` }}
|
{secondaryContent}
|
||||||
>
|
</FitText>
|
||||||
{secondaryContent}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ const fontSizeMap: { [key: number]: number } = {
|
|||||||
* Finds a font size that fits the timer in the screen
|
* Finds a font size that fits the timer in the screen
|
||||||
* Unfortunately hand tweaked
|
* Unfortunately hand tweaked
|
||||||
*/
|
*/
|
||||||
export function getEstimatedFontSize(stageTimer: string, secondaryContent?: string) {
|
export function getEstimatedFontSize(stageTimer: string, secondaryContent?: string): number {
|
||||||
const stageTimerCharacters = stageTimer.length;
|
const stageTimerCharacters = stageTimer.length;
|
||||||
let timerFontSize = (100 / (stageTimerCharacters - 1)) * 1.25;
|
let timerFontSize = (100 / (stageTimerCharacters - 1)) * 1.25;
|
||||||
|
|
||||||
@@ -58,20 +58,13 @@ export function getEstimatedFontSize(stageTimer: string, secondaryContent?: stri
|
|||||||
timerFontSize = fontSizeMap[stageTimerCharacters];
|
timerFontSize = fontSizeMap[stageTimerCharacters];
|
||||||
}
|
}
|
||||||
|
|
||||||
let externalFontSize = timerFontSize * 0.325;
|
// we need to shrink the timer if the external is going to be there
|
||||||
|
// this number has been tweaked to fit in a landscape mobile screen
|
||||||
if (secondaryContent) {
|
if (secondaryContent) {
|
||||||
// we need to shrink the timer if the external is going to be there
|
|
||||||
// this number has been tweaked to fit in a landscape mobile screen
|
|
||||||
timerFontSize *= 0.6;
|
timerFontSize *= 0.6;
|
||||||
if (secondaryContent.length > 25) {
|
|
||||||
externalFontSize = (100 / (secondaryContent.length - 1)) * 1.8;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return timerFontSize;
|
||||||
timerFontSize,
|
|
||||||
externalFontSize,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -9,6 +9,10 @@ import { ONTIME_VERSION } from './src/ONTIME_VERSION';
|
|||||||
|
|
||||||
const sentryAuthToken = process.env.SENTRY_AUTH_TOKEN;
|
const sentryAuthToken = process.env.SENTRY_AUTH_TOKEN;
|
||||||
|
|
||||||
|
const ReactCompilerConfig = {
|
||||||
|
compilationMode: 'annotation',
|
||||||
|
};
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
base: './', // Ontime cloud: we use relative paths to allow them to reference a dynamic base set at runtime
|
base: './', // Ontime cloud: we use relative paths to allow them to reference a dynamic base set at runtime
|
||||||
define: {
|
define: {
|
||||||
@@ -16,7 +20,11 @@ export default defineConfig({
|
|||||||
'import.meta.env.IS_DOCKER': process.env.NODE_ENV === 'docker',
|
'import.meta.env.IS_DOCKER': process.env.NODE_ENV === 'docker',
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
react(),
|
react({
|
||||||
|
babel: {
|
||||||
|
plugins: [['babel-plugin-react-compiler', ReactCompilerConfig]],
|
||||||
|
},
|
||||||
|
}),
|
||||||
svgrPlugin(),
|
svgrPlugin(),
|
||||||
sentryAuthToken &&
|
sentryAuthToken &&
|
||||||
sentryVitePlugin({
|
sentryVitePlugin({
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ontime-electron",
|
"name": "ontime-electron",
|
||||||
"version": "4.2.0",
|
"version": "4.2.1",
|
||||||
"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": "@getontime/resolver",
|
"name": "@getontime/resolver",
|
||||||
"version": "4.0.0",
|
"version": "4.2.1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"repository": "https://github.com/cpvalente/ontime",
|
"repository": "https://github.com/cpvalente/ontime",
|
||||||
"types": "./dist/main.d.ts",
|
"types": "./dist/main.d.ts",
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
"tsup": "^8.5.0",
|
"tsup": "^8.5.0",
|
||||||
"rimraf": "catalog:",
|
"rimraf": "catalog:",
|
||||||
"typescript": "catalog:",
|
"typescript": "catalog:",
|
||||||
"ontime-types": "workspace:^4.0.0"
|
"ontime-types": "workspace:^4.2.1"
|
||||||
},
|
},
|
||||||
"files": ["dist"]
|
"files": ["dist"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"name": "ontime-server",
|
"name": "ontime-server",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"version": "4.2.0",
|
"version": "4.2.1",
|
||||||
"exports": "./src/index.js",
|
"exports": "./src/index.js",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@googleapis/sheets": "^5.0.5",
|
"@googleapis/sheets": "^5.0.5",
|
||||||
@@ -60,6 +60,7 @@
|
|||||||
"build": "node esbuild.js",
|
"build": "node esbuild.js",
|
||||||
|
|
||||||
"test": "cross-env IS_TEST=true vitest",
|
"test": "cross-env IS_TEST=true vitest",
|
||||||
|
"test:inspect": "cross-env IS_TEST=true vitest --inspect --no-file-parallelism",
|
||||||
"test:pipeline": "cross-env IS_TEST=true vitest run"
|
"test:pipeline": "cross-env IS_TEST=true vitest run"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1564,7 +1564,7 @@ describe('rundownMutation.swap()', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('rundownMutation.clone()', () => {
|
describe('rundownMutation.clone()', () => {
|
||||||
it('clones an event and adds it to the rundown', () => {
|
it('clones at the top level of the rundown', () => {
|
||||||
const testRundown = makeRundown({
|
const testRundown = makeRundown({
|
||||||
order: ['1'],
|
order: ['1'],
|
||||||
entries: {
|
entries: {
|
||||||
@@ -1583,7 +1583,7 @@ describe('rundownMutation.clone()', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('clones an event inside a group and adds it to the rundown', () => {
|
it('clones an event inside a group', () => {
|
||||||
const testRundown = makeRundown({
|
const testRundown = makeRundown({
|
||||||
order: ['1'],
|
order: ['1'],
|
||||||
entries: {
|
entries: {
|
||||||
@@ -1621,6 +1621,55 @@ describe('rundownMutation.clone()', () => {
|
|||||||
});
|
});
|
||||||
expect((testRundown.entries[newEntry.id] as OntimeGroup).entries[0]).not.toBe('1a');
|
expect((testRundown.entries[newEntry.id] as OntimeGroup).entries[0]).not.toBe('1a');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('clones an entry from a group inside another group', () => {
|
||||||
|
const testRundown = makeRundown({
|
||||||
|
order: ['group1', 'group2'],
|
||||||
|
entries: {
|
||||||
|
group1: makeOntimeGroup({ id: 'group1', entries: ['event1'] }),
|
||||||
|
group2: makeOntimeGroup({ id: 'group2', entries: ['event2'] }),
|
||||||
|
event1: makeOntimeEvent({ id: 'event1', cue: 'nested-event', parent: 'group1' }),
|
||||||
|
event2: makeOntimeEvent({ id: 'event2', cue: 'nested-event', parent: 'group2' }),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const newEntry = rundownMutation.clone(testRundown, testRundown.entries['event2'], {
|
||||||
|
after: 'event1',
|
||||||
|
}) as OntimeEvent;
|
||||||
|
|
||||||
|
// new event is added to group
|
||||||
|
expect(testRundown.entries['group1']).toMatchObject({
|
||||||
|
entries: ['event1', newEntry.id],
|
||||||
|
});
|
||||||
|
|
||||||
|
// new references the parent group
|
||||||
|
expect(newEntry.parent).toBe('group1');
|
||||||
|
|
||||||
|
// the flat rundown remains unchanged
|
||||||
|
expect(testRundown.order).toStrictEqual(['group1', 'group2']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('clones an event and inserts it before another event', () => {
|
||||||
|
const testRundown = makeRundown({
|
||||||
|
order: ['1', '2'],
|
||||||
|
entries: {
|
||||||
|
'1': makeOntimeEvent({ id: '1', cue: 'event1', parent: null }),
|
||||||
|
'2': makeOntimeEvent({ id: '2', cue: 'event2', parent: null }),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const newEntry = rundownMutation.clone(testRundown, testRundown.entries['1'], { before: '2' });
|
||||||
|
|
||||||
|
// Verify the rundown order is updated correctly
|
||||||
|
expect(testRundown.order).toStrictEqual(['1', newEntry.id, '2']);
|
||||||
|
|
||||||
|
// Verify the cloned entry is added to the rundown
|
||||||
|
expect(testRundown.entries[newEntry.id]).toMatchObject({
|
||||||
|
type: SupportedEntry.Event,
|
||||||
|
cue: 'event1',
|
||||||
|
parent: null,
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('rundownMutation.group()', () => {
|
describe('rundownMutation.group()', () => {
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
duplicateRundown,
|
duplicateRundown,
|
||||||
getInsertAfterId,
|
getInsertAfterId,
|
||||||
hasChanges,
|
hasChanges,
|
||||||
|
makeDeepClone,
|
||||||
} from '../rundown.utils.js';
|
} from '../rundown.utils.js';
|
||||||
import { makeOntimeGroup, makeOntimeEvent, makeRundown } from '../__mocks__/rundown.mocks.js';
|
import { makeOntimeGroup, makeOntimeEvent, makeRundown } from '../__mocks__/rundown.mocks.js';
|
||||||
|
|
||||||
@@ -265,7 +266,7 @@ describe('getInsertAfterId()', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('duplicateRundown', () => {
|
describe('duplicateRundown', () => {
|
||||||
it("duplicates a given rundown", () => {
|
it('duplicates a given rundown', () => {
|
||||||
const demoRundown = demoDb.rundowns['default'];
|
const demoRundown = demoDb.rundowns['default'];
|
||||||
const title = 'Duplicated Rundown';
|
const title = 'Duplicated Rundown';
|
||||||
const duplicatedRundown = duplicateRundown(demoRundown, title);
|
const duplicatedRundown = duplicateRundown(demoRundown, title);
|
||||||
@@ -275,10 +276,51 @@ describe('duplicateRundown', () => {
|
|||||||
entries: expect.any(Object),
|
entries: expect.any(Object),
|
||||||
order: expect.any(Array),
|
order: expect.any(Array),
|
||||||
flatOrder: expect.any(Array),
|
flatOrder: expect.any(Array),
|
||||||
})
|
});
|
||||||
expect(demoRundown.id).not.toEqual(duplicatedRundown.id);
|
expect(demoRundown.id).not.toEqual(duplicatedRundown.id);
|
||||||
expect(duplicatedRundown.order.length).toEqual(demoRundown.order.length);
|
expect(duplicatedRundown.order.length).toEqual(demoRundown.order.length);
|
||||||
expect(duplicatedRundown.flatOrder.length).toEqual(demoRundown.flatOrder.length);
|
expect(duplicatedRundown.flatOrder.length).toEqual(demoRundown.flatOrder.length);
|
||||||
expect(Object.keys(duplicatedRundown.entries).length).toEqual(Object.keys(demoRundown.entries).length);
|
expect(Object.keys(duplicatedRundown.entries).length).toEqual(Object.keys(demoRundown.entries).length);
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
|
|
||||||
|
describe('makeDeepClone()', () => {
|
||||||
|
it('deep clones a group along with its nested entries', () => {
|
||||||
|
const group1 = makeOntimeGroup({ id: 'group1', title: 'Group 1', entries: ['event1', 'event2'] });
|
||||||
|
const rundown = makeRundown({
|
||||||
|
entries: {
|
||||||
|
group1,
|
||||||
|
event1: makeOntimeEvent({ id: 'event1', title: 'Event 1', parent: 'group1' }),
|
||||||
|
event2: makeOntimeEvent({ id: 'event2', title: 'Event 2', parent: 'group1' }),
|
||||||
|
},
|
||||||
|
order: ['group1'],
|
||||||
|
flatOrder: ['group1', 'event1', 'event2'],
|
||||||
|
});
|
||||||
|
|
||||||
|
const { newGroup, nestedEntries } = makeDeepClone(group1, rundown);
|
||||||
|
|
||||||
|
expect(newGroup).toMatchObject({
|
||||||
|
id: expect.any(String),
|
||||||
|
title: 'Group 1 (copy)',
|
||||||
|
entries: [expect.any(String), expect.any(String)],
|
||||||
|
revision: 0,
|
||||||
|
});
|
||||||
|
expect(newGroup.id).not.toEqual('group1');
|
||||||
|
expect(newGroup.entries.length).toEqual(group1.entries.length);
|
||||||
|
|
||||||
|
expect(nestedEntries).toMatchObject([
|
||||||
|
{
|
||||||
|
id: expect.any(String),
|
||||||
|
title: 'Event 1',
|
||||||
|
parent: newGroup.id,
|
||||||
|
revision: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: expect.any(String),
|
||||||
|
title: 'Event 2',
|
||||||
|
parent: newGroup.id,
|
||||||
|
revision: 0,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -24,23 +24,25 @@ import {
|
|||||||
OntimeEvent,
|
OntimeEvent,
|
||||||
PatchWithId,
|
PatchWithId,
|
||||||
Rundown,
|
Rundown,
|
||||||
|
InsertOptions,
|
||||||
} from 'ontime-types';
|
} from 'ontime-types';
|
||||||
import { customFieldLabelToKey, insertAtIndex } from 'ontime-utils';
|
import { customFieldLabelToKey, insertAtIndex } from 'ontime-utils';
|
||||||
|
|
||||||
import { getDataProvider } from '../../classes/data-provider/DataProvider.js';
|
import { getDataProvider } from '../../classes/data-provider/DataProvider.js';
|
||||||
|
import { consoleError } from '../../utils/console.js';
|
||||||
|
|
||||||
import type { RundownMetadata } from './rundown.types.js';
|
import type { RundownMetadata } from './rundown.types.js';
|
||||||
import {
|
import {
|
||||||
applyPatchToEntry,
|
applyPatchToEntry,
|
||||||
cloneGroup,
|
cloneSimpleRundownEntry,
|
||||||
cloneEntry,
|
|
||||||
createGroup,
|
createGroup,
|
||||||
deleteById,
|
deleteById,
|
||||||
doesInvalidateMetadata,
|
doesInvalidateMetadata,
|
||||||
|
getInsertAfterId,
|
||||||
getUniqueId,
|
getUniqueId,
|
||||||
|
makeDeepClone,
|
||||||
} from './rundown.utils.js';
|
} from './rundown.utils.js';
|
||||||
import { makeRundownMetadata, ProcessedRundownMetadata } from './rundown.parser.js';
|
import { makeRundownMetadata, ProcessedRundownMetadata } from './rundown.parser.js';
|
||||||
import { consoleError } from '../../utils/console.js';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The currently loaded rundown in cache
|
* The currently loaded rundown in cache
|
||||||
@@ -171,6 +173,11 @@ export function createTransaction(options: TransactionOptions): Transaction {
|
|||||||
function add(rundown: Rundown, entry: OntimeEntry, afterId: EntryId | null, parent: OntimeGroup | null): OntimeEntry {
|
function add(rundown: Rundown, entry: OntimeEntry, afterId: EntryId | null, parent: OntimeGroup | null): OntimeEntry {
|
||||||
if (parent) {
|
if (parent) {
|
||||||
// 1. inserting an entry inside a group
|
// 1. inserting an entry inside a group
|
||||||
|
|
||||||
|
if ('parent' in entry) {
|
||||||
|
entry.parent = parent.id;
|
||||||
|
}
|
||||||
|
|
||||||
if (afterId) {
|
if (afterId) {
|
||||||
const atEventsIndex = parent.entries.indexOf(afterId) + 1;
|
const atEventsIndex = parent.entries.indexOf(afterId) + 1;
|
||||||
const atFlatIndex = rundown.flatOrder.indexOf(afterId) + 1;
|
const atFlatIndex = rundown.flatOrder.indexOf(afterId) + 1;
|
||||||
@@ -439,40 +446,74 @@ function swap(rundown: Rundown, eventFrom: OntimeEvent, eventTo: OntimeEvent) {
|
|||||||
* Inserts a clone of the given entry into the rundown
|
* Inserts a clone of the given entry into the rundown
|
||||||
* Handles cloning children if the entry is a group
|
* Handles cloning children if the entry is a group
|
||||||
*/
|
*/
|
||||||
function clone(rundown: Rundown, entry: OntimeEntry): OntimeEntry {
|
function clone(rundown: Rundown, entry: OntimeEntry, options?: InsertOptions): OntimeEntry {
|
||||||
if (isOntimeGroup(entry)) {
|
if (isOntimeGroup(entry)) {
|
||||||
const newGroup = cloneGroup(entry, getUniqueId(rundown));
|
const { newGroup, nestedEntries } = makeDeepClone(entry, rundown);
|
||||||
const nestedIds: EntryId[] = [];
|
|
||||||
|
|
||||||
for (let i = 0; i < entry.entries.length; i++) {
|
// insert all entries into the rundown
|
||||||
const nestedEntryId = entry.entries[i];
|
rundown.entries[newGroup.id] = newGroup;
|
||||||
const nestedEntry = rundown.entries[nestedEntryId];
|
for (let i = 0; i < nestedEntries.length; i++) {
|
||||||
if (!nestedEntry) {
|
const nestedEntry = nestedEntries[i];
|
||||||
continue;
|
rundown.entries[nestedEntry.id] = nestedEntry;
|
||||||
}
|
|
||||||
|
|
||||||
// clone the event and assign it to the new group
|
|
||||||
const newNestedEntry = cloneEntry(nestedEntry, getUniqueId(rundown));
|
|
||||||
(newNestedEntry as OntimeEvent | OntimeDelay).parent = newGroup.id;
|
|
||||||
|
|
||||||
nestedIds.push(newNestedEntry.id);
|
|
||||||
// we immediately insert the nested entries into the rundown
|
|
||||||
rundown.entries[newNestedEntry.id] = newNestedEntry;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// indexes + 1 since we are inserting after the cloned group
|
// by default we insert after the cloned element
|
||||||
const atIndex = rundown.order.indexOf(entry.id) + 1;
|
let atIndex = rundown.order.indexOf(entry.id) + 1;
|
||||||
|
|
||||||
newGroup.entries = nestedIds;
|
const referenceId = options?.after ?? options?.before;
|
||||||
newGroup.title = `${entry.title || 'Untitled'} (copy)`;
|
if (referenceId) {
|
||||||
|
// trying to insert relatively to another entry
|
||||||
|
const referenceEntry = rundown.entries[referenceId];
|
||||||
|
if (referenceEntry) {
|
||||||
|
if (options?.after) {
|
||||||
|
atIndex = rundown.order.indexOf(referenceId) + 1;
|
||||||
|
} else if (options?.before) {
|
||||||
|
atIndex = rundown.order.indexOf(referenceId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
rundown.entries[newGroup.id] = newGroup;
|
// we only need to insert the group, the nested entries will be resolved by the rundown engine
|
||||||
rundown.order = insertAtIndex(atIndex, newGroup.id, rundown.order);
|
rundown.order = insertAtIndex(atIndex, newGroup.id, rundown.order);
|
||||||
|
|
||||||
return newGroup;
|
return newGroup;
|
||||||
} else {
|
} else {
|
||||||
const parent: OntimeGroup | null = entry.parent ? (rundown.entries[entry.parent] as OntimeGroup) : null;
|
const clonedEntry = cloneSimpleRundownEntry(entry, getUniqueId(rundown));
|
||||||
return add(rundown, cloneEntry(entry, getUniqueId(rundown)), entry.id, parent);
|
|
||||||
|
let parent: OntimeGroup | null = null;
|
||||||
|
|
||||||
|
// trying to insert relatively to another entry, check that entries parent
|
||||||
|
const referenceId = options?.after ?? options?.before;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* if we have a positioning reference, and that reference has a parent
|
||||||
|
* we need to maintain the same parent for the cloned entry
|
||||||
|
*/
|
||||||
|
if (referenceId) {
|
||||||
|
const referenceEntry = rundown.entries[referenceId];
|
||||||
|
|
||||||
|
if (referenceEntry && !isOntimeGroup(referenceEntry)) {
|
||||||
|
if (referenceEntry.parent) {
|
||||||
|
const maybeParent = rundown.entries[referenceEntry.parent];
|
||||||
|
if (maybeParent && isOntimeGroup(maybeParent)) {
|
||||||
|
parent = maybeParent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (entry.parent) {
|
||||||
|
const maybeParent = rundown.entries[entry.parent];
|
||||||
|
if (maybeParent && isOntimeGroup(maybeParent)) {
|
||||||
|
parent = maybeParent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// if we have resolved a parent, we add it to the cloned entry
|
||||||
|
let after = getInsertAfterId(rundown, parent, options?.after, options?.before);
|
||||||
|
if (!after) {
|
||||||
|
after = entry.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
return add(rundown, clonedEntry, after, parent);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import {
|
|||||||
entryReorderValidator,
|
entryReorderValidator,
|
||||||
entrySwapValidator,
|
entrySwapValidator,
|
||||||
validateRundownMutation,
|
validateRundownMutation,
|
||||||
|
clonePostValidator,
|
||||||
} from './rundown.validation.js';
|
} from './rundown.validation.js';
|
||||||
import { paramsWithId } from '../validation-utils/validationFunction.js';
|
import { paramsWithId } from '../validation-utils/validationFunction.js';
|
||||||
import { getDataProvider } from '../../classes/data-provider/DataProvider.js';
|
import { getDataProvider } from '../../classes/data-provider/DataProvider.js';
|
||||||
@@ -296,10 +297,14 @@ router.patch(
|
|||||||
router.post(
|
router.post(
|
||||||
'/:rundownId/clone/:id',
|
'/:rundownId/clone/:id',
|
||||||
paramsWithId,
|
paramsWithId,
|
||||||
|
clonePostValidator,
|
||||||
validateRundownMutation,
|
validateRundownMutation,
|
||||||
async (req: Request, res: Response<Rundown | ErrorResponse>) => {
|
async (req: Request, res: Response<Rundown | ErrorResponse>) => {
|
||||||
try {
|
try {
|
||||||
const rundown = await cloneEntry(req.params.id);
|
const rundown = await cloneEntry(req.params.id, {
|
||||||
|
before: req.body?.before,
|
||||||
|
after: req.body?.after,
|
||||||
|
});
|
||||||
res.status(200).send(rundown);
|
res.status(200).send(rundown);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = getErrorMessage(error);
|
const message = getErrorMessage(error);
|
||||||
|
|||||||
@@ -14,11 +14,15 @@ import {
|
|||||||
Rundown,
|
Rundown,
|
||||||
LogOrigin,
|
LogOrigin,
|
||||||
ProjectRundowns,
|
ProjectRundowns,
|
||||||
|
InsertOptions,
|
||||||
} from 'ontime-types';
|
} from 'ontime-types';
|
||||||
import { customFieldLabelToKey } from 'ontime-utils';
|
import { customFieldLabelToKey } from 'ontime-utils';
|
||||||
|
|
||||||
import { updateRundownData } from '../../stores/runtimeState.js';
|
import { updateRundownData } from '../../stores/runtimeState.js';
|
||||||
import { runtimeService } from '../../services/runtime-service/runtime.service.js';
|
import { runtimeService } from '../../services/runtime-service/runtime.service.js';
|
||||||
|
import { sendRefetch } from '../../adapters/WebsocketAdapter.js';
|
||||||
|
import { setLastLoadedRundown } from '../../services/app-state-service/AppStateService.js';
|
||||||
|
import { logger } from '../../classes/Logger.js';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
createTransaction,
|
createTransaction,
|
||||||
@@ -29,9 +33,6 @@ import {
|
|||||||
} from './rundown.dao.js';
|
} from './rundown.dao.js';
|
||||||
import type { RundownMetadata } from './rundown.types.js';
|
import type { RundownMetadata } from './rundown.types.js';
|
||||||
import { generateEvent, getInsertAfterId, hasChanges } from './rundown.utils.js';
|
import { generateEvent, getInsertAfterId, hasChanges } from './rundown.utils.js';
|
||||||
import { sendRefetch } from '../../adapters/WebsocketAdapter.js';
|
|
||||||
import { setLastLoadedRundown } from '../../services/app-state-service/AppStateService.js';
|
|
||||||
import { logger } from '../../classes/Logger.js';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* creates a new entry with given data
|
* creates a new entry with given data
|
||||||
@@ -347,17 +348,18 @@ export async function swapEvents(fromId: EntryId, toId: EntryId): Promise<Rundow
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Clones an entry, ensuring that all dependencies are preserved
|
* Clones an entry, ensuring that all dependencies are preserved
|
||||||
|
* Handles cloning children if the entry is a group
|
||||||
* @throws if the entry to clone does not exist
|
* @throws if the entry to clone does not exist
|
||||||
*/
|
*/
|
||||||
export async function cloneEntry(entryId: EntryId): Promise<Rundown> {
|
export async function cloneEntry(entryId: EntryId, options: InsertOptions): Promise<Rundown> {
|
||||||
const { rundown, commit } = createTransaction({ mutableRundown: true, mutableCustomFields: false });
|
const { rundown, commit } = createTransaction({ mutableRundown: true, mutableCustomFields: false });
|
||||||
const originalEntry = rundown.entries[entryId];
|
const originalEntry = rundown.entries[entryId];
|
||||||
|
|
||||||
if (!originalEntry) {
|
if (!originalEntry) {
|
||||||
throw new Error('Did not find event to clone');
|
throw new Error('Could not find entry to clone');
|
||||||
}
|
}
|
||||||
|
|
||||||
const newEntry = rundownMutation.clone(rundown, originalEntry);
|
const newEntry = rundownMutation.clone(rundown, originalEntry, options);
|
||||||
const { rundown: rundownResult, rundownMetadata, revision } = commit();
|
const { rundown: rundownResult, rundownMetadata, revision } = commit();
|
||||||
|
|
||||||
// schedule the side effects
|
// schedule the side effects
|
||||||
@@ -373,7 +375,6 @@ export async function cloneEntry(entryId: EntryId): Promise<Rundown> {
|
|||||||
} else if (isOntimeDelay(newEntry)) {
|
} else if (isOntimeDelay(newEntry)) {
|
||||||
notifyChanges(rundownMetadata, revision, { external: true });
|
notifyChanges(rundownMetadata, revision, { external: true });
|
||||||
}
|
}
|
||||||
notifyChanges(rundownMetadata, revision, { timer: true, external: true });
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return rundownResult;
|
return rundownResult;
|
||||||
|
|||||||
@@ -396,20 +396,54 @@ export function cloneGroup(entry: OntimeGroup, newId: EntryId): OntimeGroup {
|
|||||||
|
|
||||||
// in groups, we need to remove the events references
|
// in groups, we need to remove the events references
|
||||||
newEntry.entries = [];
|
newEntry.entries = [];
|
||||||
|
newEntry.title = `${entry.title || 'Untitled'} (copy)`;
|
||||||
newEntry.revision = 0;
|
newEntry.revision = 0;
|
||||||
return newEntry;
|
return newEntry;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Receives an entry and chooses the correct cloning strategy
|
* Clones a group and all its nested entries
|
||||||
*/
|
*/
|
||||||
export function cloneEntry(entry: OntimeEntry, newId: EntryId): OntimeEntry {
|
export function makeDeepClone(
|
||||||
|
group: OntimeGroup,
|
||||||
|
rundown: Rundown,
|
||||||
|
): { newGroup: OntimeGroup; nestedEntries: OntimeEntry[] } {
|
||||||
|
const newGroupId = getUniqueId(rundown);
|
||||||
|
const newGroup = cloneGroup(group, newGroupId);
|
||||||
|
const nestedEntries: OntimeEntry[] = [];
|
||||||
|
const nestedEntryIds: EntryId[] = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < group.entries.length; i++) {
|
||||||
|
const nestedEntryId = group.entries[i];
|
||||||
|
const nestedEntry = rundown.entries[nestedEntryId];
|
||||||
|
if (!nestedEntry) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// clone the event and assign it to the new group
|
||||||
|
const nestedEntryNewId = getUniqueId(rundown);
|
||||||
|
const newNestedEntry = cloneSimpleRundownEntry(nestedEntry, nestedEntryNewId);
|
||||||
|
(newNestedEntry as OntimeEvent | OntimeDelay | OntimeMilestone).parent = newGroup.id;
|
||||||
|
|
||||||
|
nestedEntryIds.push(nestedEntryNewId);
|
||||||
|
nestedEntries.push(newNestedEntry);
|
||||||
|
}
|
||||||
|
|
||||||
|
// update the new group with the nested entries
|
||||||
|
newGroup.entries = nestedEntryIds;
|
||||||
|
|
||||||
|
return { newGroup, nestedEntries };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Receives an entry and chooses the correct cloning strategy
|
||||||
|
* @throws if the source entry is unknown or a group
|
||||||
|
*/
|
||||||
|
export function cloneSimpleRundownEntry(entry: OntimeEntry, newId: EntryId): OntimeEntry {
|
||||||
if (isOntimeEvent(entry)) {
|
if (isOntimeEvent(entry)) {
|
||||||
return cloneEvent(entry, newId);
|
return cloneEvent(entry, newId);
|
||||||
} else if (isOntimeDelay(entry)) {
|
} else if (isOntimeDelay(entry)) {
|
||||||
return cloneDelay(entry, newId);
|
return cloneDelay(entry, newId);
|
||||||
} else if (isOntimeGroup(entry)) {
|
|
||||||
return cloneGroup(entry, newId);
|
|
||||||
} else if (isOntimeMilestone(entry)) {
|
} else if (isOntimeMilestone(entry)) {
|
||||||
return cloneMilestone(entry, newId);
|
return cloneMilestone(entry, newId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,6 +42,13 @@ export const entryPostValidator = [
|
|||||||
requestValidationFunction,
|
requestValidationFunction,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const clonePostValidator = [
|
||||||
|
body('after').optional().isString(),
|
||||||
|
body('before').optional().isString(),
|
||||||
|
|
||||||
|
requestValidationFunction,
|
||||||
|
];
|
||||||
|
|
||||||
export const entryPutValidator = [body('id').isString().trim().notEmpty(), requestValidationFunction];
|
export const entryPutValidator = [body('id').isString().trim().notEmpty(), requestValidationFunction];
|
||||||
|
|
||||||
export const entryBatchPutValidator = [
|
export const entryBatchPutValidator = [
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
import { EntryCustomFields, OntimeEvent } from 'ontime-types';
|
||||||
|
import { isValidChangeProperty } from '../integration.utils.js';
|
||||||
|
|
||||||
|
describe('isValidChangeProperty()', () => {
|
||||||
|
test('correct value and property', () => {
|
||||||
|
const testEvent = {
|
||||||
|
id: 'test',
|
||||||
|
duration: 111,
|
||||||
|
} as OntimeEvent;
|
||||||
|
|
||||||
|
expect(isValidChangeProperty(testEvent, 'duration', 123)).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('correct property and undefined value', () => {
|
||||||
|
const testEvent = {
|
||||||
|
id: 'test',
|
||||||
|
duration: 111,
|
||||||
|
} as OntimeEvent;
|
||||||
|
|
||||||
|
expect(isValidChangeProperty(testEvent, 'duration', undefined)).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('missing property and undefined value', () => {
|
||||||
|
const testEvent = {
|
||||||
|
id: 'test',
|
||||||
|
duration: 111,
|
||||||
|
} as OntimeEvent;
|
||||||
|
|
||||||
|
expect(isValidChangeProperty(testEvent, 'missing', 123)).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('non existing custom value', () => {
|
||||||
|
const testEvent = {
|
||||||
|
id: 'test',
|
||||||
|
duration: 111,
|
||||||
|
custom: {
|
||||||
|
field: 'test',
|
||||||
|
} as EntryCustomFields,
|
||||||
|
} as OntimeEvent;
|
||||||
|
|
||||||
|
expect(isValidChangeProperty(testEvent, 'custom:test', 123)).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('existing custom value', () => {
|
||||||
|
const testEvent = {
|
||||||
|
id: 'test',
|
||||||
|
duration: 111,
|
||||||
|
custom: {
|
||||||
|
test: 'test',
|
||||||
|
} as EntryCustomFields,
|
||||||
|
} as OntimeEvent;
|
||||||
|
|
||||||
|
expect(isValidChangeProperty(testEvent, 'custom:test', 123)).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('empty custom definition', () => {
|
||||||
|
const testEvent = {
|
||||||
|
id: 'test',
|
||||||
|
duration: 111,
|
||||||
|
custom: {
|
||||||
|
test: 'test',
|
||||||
|
} as EntryCustomFields,
|
||||||
|
} as OntimeEvent;
|
||||||
|
|
||||||
|
expect(isValidChangeProperty(testEvent, 'custom:', 123)).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('build-in in custom', () => {
|
||||||
|
const testEvent = {
|
||||||
|
id: 'test',
|
||||||
|
duration: 111,
|
||||||
|
custom: {
|
||||||
|
test: 'test',
|
||||||
|
} as EntryCustomFields,
|
||||||
|
} as OntimeEvent;
|
||||||
|
|
||||||
|
expect(isValidChangeProperty(testEvent, 'custom:toString', 123)).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('build-in in top object', () => {
|
||||||
|
const testEvent = {
|
||||||
|
id: 'test',
|
||||||
|
duration: 111,
|
||||||
|
custom: {
|
||||||
|
test: 'test',
|
||||||
|
} as EntryCustomFields,
|
||||||
|
} as OntimeEvent;
|
||||||
|
|
||||||
|
expect(isValidChangeProperty(testEvent, 'toString', 123)).toBeFalsy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -18,7 +18,7 @@ import { validateMessage, validateTimerMessage } from '../services/message-servi
|
|||||||
import { runtimeService } from '../services/runtime-service/runtime.service.js';
|
import { runtimeService } from '../services/runtime-service/runtime.service.js';
|
||||||
import { eventStore } from '../stores/EventStore.js';
|
import { eventStore } from '../stores/EventStore.js';
|
||||||
import * as assert from '../utils/assert.js';
|
import * as assert from '../utils/assert.js';
|
||||||
import { parseProperty } from './integration.utils.js';
|
import { parseProperty, isValidChangeProperty } from './integration.utils.js';
|
||||||
import { socket } from '../adapters/WebsocketAdapter.js';
|
import { socket } from '../adapters/WebsocketAdapter.js';
|
||||||
import { throttle } from '../utils/throttle.js';
|
import { throttle } from '../utils/throttle.js';
|
||||||
import { coerceEnum } from '../utils/coerceType.js';
|
import { coerceEnum } from '../utils/coerceType.js';
|
||||||
@@ -77,10 +77,9 @@ const actionHandlers: Record<ApiActionTag, ActionHandler> = {
|
|||||||
let shouldThrottle = false;
|
let shouldThrottle = false;
|
||||||
|
|
||||||
Object.entries(data).forEach(([property, value]) => {
|
Object.entries(data).forEach(([property, value]) => {
|
||||||
if (typeof property !== 'string' || value === undefined || !(property in targetEntry)) {
|
if (!isValidChangeProperty(targetEntry, property, value)) {
|
||||||
throw new Error('Invalid property or value');
|
throw new Error('Invalid property or value');
|
||||||
}
|
}
|
||||||
// parseProperty is async because of the data lock
|
|
||||||
const newObjectProperty = parseProperty(property, value);
|
const newObjectProperty = parseProperty(property, value);
|
||||||
const key = Object.keys(newObjectProperty)[0];
|
const key = Object.keys(newObjectProperty)[0];
|
||||||
shouldThrottle = shouldThrottle || willCauseRegeneration(key);
|
shouldThrottle = shouldThrottle || willCauseRegeneration(key);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { EndAction, TimeStrategy, TimerType, isKeyOfType } from 'ontime-types';
|
import { EndAction, OntimeEntry, TimeStrategy, TimerType, isKeyOfType } from 'ontime-types';
|
||||||
import { maxDuration } from 'ontime-utils';
|
import { maxDuration } from 'ontime-utils';
|
||||||
|
|
||||||
import { coerceBoolean, coerceColour, coerceEnum, coerceNumber, coerceString } from '../utils/coerceType.js';
|
import { coerceBoolean, coerceColour, coerceEnum, coerceNumber, coerceString } from '../utils/coerceType.js';
|
||||||
@@ -60,3 +60,14 @@ export function parseProperty(property: string, value: unknown) {
|
|||||||
const parserFn = propertyConversion[property];
|
const parserFn = propertyConversion[property];
|
||||||
return { [property]: parserFn(value) };
|
return { [property]: parserFn(value) };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isValidChangeProperty(target: OntimeEntry, property: string, value: unknown): boolean {
|
||||||
|
if (typeof property !== 'string') return false;
|
||||||
|
if (value === undefined) return false;
|
||||||
|
if (property.startsWith('custom:') && 'custom' in target) {
|
||||||
|
const customProperty = property.slice('custom:'.length);
|
||||||
|
if (!customProperty) return false;
|
||||||
|
return Object.hasOwn(target.custom, customProperty);
|
||||||
|
}
|
||||||
|
return Object.hasOwn(target, property);
|
||||||
|
}
|
||||||
|
|||||||
@@ -316,10 +316,17 @@ export async function patchCurrentProject(data: Partial<DatabaseModel>) {
|
|||||||
// we can pass some stuff straight to the data provider
|
// we can pass some stuff straight to the data provider
|
||||||
await getDataProvider().mergeIntoData(rest);
|
await getDataProvider().mergeIntoData(rest);
|
||||||
|
|
||||||
// ... but rundown and custom fields need to be checked
|
// the rundown depends on custom fields
|
||||||
if (rundowns != null) {
|
// so custom fields needs to be checked first
|
||||||
const customFields = parseCustomFields(data);
|
if (customFields) {
|
||||||
const result = parseRundowns(data, customFields);
|
const parsedCustomFields = parseCustomFields(data);
|
||||||
|
await getDataProvider().mergeIntoData({ customFields: parsedCustomFields });
|
||||||
|
}
|
||||||
|
|
||||||
|
// then we can checked the rundown
|
||||||
|
if (rundowns) {
|
||||||
|
const parsedCustomFields = await getDataProvider().getCustomFields();
|
||||||
|
const result = parseRundowns(data, parsedCustomFields);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The user may have multiple rundowns
|
* The user may have multiple rundowns
|
||||||
@@ -330,7 +337,7 @@ export async function patchCurrentProject(data: Partial<DatabaseModel>) {
|
|||||||
const rundownToLoad =
|
const rundownToLoad =
|
||||||
last?.rundownId && last.rundownId in result ? result[last.rundownId] : getFirstRundown(result);
|
last?.rundownId && last.rundownId in result ? result[last.rundownId] : getFirstRundown(result);
|
||||||
|
|
||||||
await initRundown(rundownToLoad, customFields, true);
|
await initRundown(rundownToLoad, parsedCustomFields, true); //mergeIntoData is handled by the init function
|
||||||
}
|
}
|
||||||
|
|
||||||
const updatedData = await getDataProvider().getData();
|
const updatedData = await getDataProvider().getData();
|
||||||
|
|||||||
@@ -26,17 +26,9 @@ test('Copy-paste', async ({ page }) => {
|
|||||||
// assert
|
// assert
|
||||||
await expect(page.getByTestId('entry-2')).toBeVisible();
|
await expect(page.getByTestId('entry-2')).toBeVisible();
|
||||||
await expect(page.getByTestId('entry-2').getByTestId('entry__title')).toHaveValue('test');
|
await expect(page.getByTestId('entry-2').getByTestId('entry__title')).toHaveValue('test');
|
||||||
await expect(page.getByTestId('entry-2').getByTestId('rundown-event')).toContainText('5');
|
await expect(page.getByTestId('entry-2').getByTestId('rundown-event')).toContainText('4');
|
||||||
|
|
||||||
// copy paste above
|
//TODO: reintroduce the past above test
|
||||||
await page.getByTestId('rundown-event').locator('div').filter({ hasText: '5' }).click();
|
|
||||||
await page.getByTestId('rundown-event').locator('div').filter({ hasText: '5' }).press('Control+c');
|
|
||||||
await page.getByTestId('rundown-event').locator('div').filter({ hasText: '5' }).press('Control+Shift+v');
|
|
||||||
|
|
||||||
// assert
|
|
||||||
await expect(page.getByTestId('entry-2')).toBeVisible();
|
|
||||||
await expect(page.getByTestId('entry-2').getByTestId('entry__title')).toHaveValue('test');
|
|
||||||
await expect(page.getByTestId('entry-2').getByTestId('rundown-event')).toContainText('4.1');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Move', async ({ page }) => {
|
test('Move', async ({ page }) => {
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ontime",
|
"name": "ontime",
|
||||||
"version": "4.2.0",
|
"version": "4.2.1",
|
||||||
"description": "Time keeping for live events",
|
"description": "Time keeping for live events",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"ontime",
|
"ontime",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"version": "4.0.0-beta.4",
|
"version": "4.2.1",
|
||||||
"name": "ontime-types",
|
"name": "ontime-types",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./src/index.ts",
|
"main": "./src/index.ts",
|
||||||
|
|||||||
@@ -3,15 +3,14 @@ import type { MaybeNumber } from '../../utils/utils.type.js';
|
|||||||
|
|
||||||
export type PatchWithId<T extends OntimeEntry = OntimeEntry> = Partial<T> & { id: EntryId };
|
export type PatchWithId<T extends OntimeEntry = OntimeEntry> = Partial<T> & { id: EntryId };
|
||||||
|
|
||||||
export type EventPostPayload = Partial<OntimeEntry> & {
|
export type InsertOptions = {
|
||||||
after?: EntryId;
|
after?: EntryId;
|
||||||
before?: EntryId;
|
before?: EntryId;
|
||||||
};
|
}
|
||||||
|
|
||||||
export type TransientEventPayload = Partial<OntimeEntry> & {
|
export type EventPostPayload = Partial<OntimeEntry> & InsertOptions;
|
||||||
after?: EntryId;
|
|
||||||
before?: EntryId;
|
export type TransientEventPayload = Partial<OntimeEntry> & InsertOptions;
|
||||||
};
|
|
||||||
|
|
||||||
export type ProjectRundown = {
|
export type ProjectRundown = {
|
||||||
id: string;
|
id: string;
|
||||||
|
|||||||
@@ -79,6 +79,7 @@ export type {
|
|||||||
} from './api/ontime-controller/BackendResponse.type.js';
|
} from './api/ontime-controller/BackendResponse.type.js';
|
||||||
export type {
|
export type {
|
||||||
EventPostPayload,
|
EventPostPayload,
|
||||||
|
InsertOptions,
|
||||||
PatchWithId,
|
PatchWithId,
|
||||||
ProjectRundown,
|
ProjectRundown,
|
||||||
ProjectRundownsList,
|
ProjectRundownsList,
|
||||||
|
|||||||
Generated
+17
-26
@@ -137,9 +137,6 @@ importers:
|
|||||||
axios:
|
axios:
|
||||||
specifier: ^1.12.2
|
specifier: ^1.12.2
|
||||||
version: 1.12.2
|
version: 1.12.2
|
||||||
babel-plugin-react-compiler:
|
|
||||||
specifier: 19.1.0-rc.3
|
|
||||||
version: 19.1.0-rc.3
|
|
||||||
csv-stringify:
|
csv-stringify:
|
||||||
specifier: ^6.6.0
|
specifier: ^6.6.0
|
||||||
version: 6.6.0
|
version: 6.6.0
|
||||||
@@ -207,6 +204,9 @@ importers:
|
|||||||
'@vitejs/plugin-react':
|
'@vitejs/plugin-react':
|
||||||
specifier: 4.5.1
|
specifier: 4.5.1
|
||||||
version: 4.5.1(vite@6.3.1(@types/node@22.15.26)(jiti@2.4.2)(sass@1.92.0)(tsx@4.20.5))
|
version: 4.5.1(vite@6.3.1(@types/node@22.15.26)(jiti@2.4.2)(sass@1.92.0)(tsx@4.20.5))
|
||||||
|
babel-plugin-react-compiler:
|
||||||
|
specifier: 1.0.0
|
||||||
|
version: 1.0.0
|
||||||
eslint:
|
eslint:
|
||||||
specifier: 'catalog:'
|
specifier: 'catalog:'
|
||||||
version: 8.56.0
|
version: 8.56.0
|
||||||
@@ -295,7 +295,7 @@ importers:
|
|||||||
specifier: 'catalog:'
|
specifier: 'catalog:'
|
||||||
version: 8.56.0
|
version: 8.56.0
|
||||||
ontime-types:
|
ontime-types:
|
||||||
specifier: workspace:^4.0.0
|
specifier: workspace:^4.2.1
|
||||||
version: link:../../packages/types
|
version: link:../../packages/types
|
||||||
rimraf:
|
rimraf:
|
||||||
specifier: 'catalog:'
|
specifier: 'catalog:'
|
||||||
@@ -738,10 +738,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-Y1GkI4ktrtvmawoSq+4FCVHNryea6uR+qUQy0AGxLSsjCX0nVmkYQMBLHDkXZuo5hGx7eYdnIaslsdBFm7zbUw==}
|
resolution: {integrity: sha512-Y1GkI4ktrtvmawoSq+4FCVHNryea6uR+qUQy0AGxLSsjCX0nVmkYQMBLHDkXZuo5hGx7eYdnIaslsdBFm7zbUw==}
|
||||||
engines: {node: '>=6.9.0'}
|
engines: {node: '>=6.9.0'}
|
||||||
|
|
||||||
'@babel/types@7.27.7':
|
|
||||||
resolution: {integrity: sha512-8OLQgDScAOHXnAz2cV+RfzzNMipuLVBz2biuAJFMV9bfkNf393je3VM8CLkjQodW5+iWsSJdSgSWT6rsZoXHPw==}
|
|
||||||
engines: {node: '>=6.9.0'}
|
|
||||||
|
|
||||||
'@babel/types@7.28.2':
|
'@babel/types@7.28.2':
|
||||||
resolution: {integrity: sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==}
|
resolution: {integrity: sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==}
|
||||||
engines: {node: '>=6.9.0'}
|
engines: {node: '>=6.9.0'}
|
||||||
@@ -2183,8 +2179,8 @@ packages:
|
|||||||
axios@1.12.2:
|
axios@1.12.2:
|
||||||
resolution: {integrity: sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==}
|
resolution: {integrity: sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==}
|
||||||
|
|
||||||
babel-plugin-react-compiler@19.1.0-rc.3:
|
babel-plugin-react-compiler@1.0.0:
|
||||||
resolution: {integrity: sha512-mjRn69WuTz4adL0bXGx8Rsyk1086zFJeKmes6aK0xPuK3aaXmDJdLHqwKKMrpm6KAI1MCoUK72d2VeqQbu8YIA==}
|
resolution: {integrity: sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw==}
|
||||||
|
|
||||||
balanced-match@1.0.2:
|
balanced-match@1.0.2:
|
||||||
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
|
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
|
||||||
@@ -5380,7 +5376,7 @@ snapshots:
|
|||||||
'@babel/parser': 7.23.6
|
'@babel/parser': 7.23.6
|
||||||
'@babel/template': 7.22.15
|
'@babel/template': 7.22.15
|
||||||
'@babel/traverse': 7.23.6
|
'@babel/traverse': 7.23.6
|
||||||
'@babel/types': 7.27.7
|
'@babel/types': 7.28.2
|
||||||
convert-source-map: 2.0.0
|
convert-source-map: 2.0.0
|
||||||
debug: 4.4.1
|
debug: 4.4.1
|
||||||
gensync: 1.0.0-beta.2
|
gensync: 1.0.0-beta.2
|
||||||
@@ -5439,7 +5435,7 @@ snapshots:
|
|||||||
'@babel/generator@7.27.5':
|
'@babel/generator@7.27.5':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/parser': 7.27.5
|
'@babel/parser': 7.27.5
|
||||||
'@babel/types': 7.27.7
|
'@babel/types': 7.28.2
|
||||||
'@jridgewell/gen-mapping': 0.3.8
|
'@jridgewell/gen-mapping': 0.3.8
|
||||||
'@jridgewell/trace-mapping': 0.3.25
|
'@jridgewell/trace-mapping': 0.3.25
|
||||||
jsesc: 3.1.0
|
jsesc: 3.1.0
|
||||||
@@ -5594,7 +5590,7 @@ snapshots:
|
|||||||
'@babel/helpers@7.27.4':
|
'@babel/helpers@7.27.4':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/template': 7.27.2
|
'@babel/template': 7.27.2
|
||||||
'@babel/types': 7.27.7
|
'@babel/types': 7.28.2
|
||||||
|
|
||||||
'@babel/helpers@7.28.3':
|
'@babel/helpers@7.28.3':
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -5610,7 +5606,7 @@ snapshots:
|
|||||||
|
|
||||||
'@babel/parser@7.23.6':
|
'@babel/parser@7.23.6':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/types': 7.27.7
|
'@babel/types': 7.28.2
|
||||||
|
|
||||||
'@babel/parser@7.27.5':
|
'@babel/parser@7.27.5':
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -5654,7 +5650,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@babel/code-frame': 7.27.1
|
'@babel/code-frame': 7.27.1
|
||||||
'@babel/parser': 7.27.5
|
'@babel/parser': 7.27.5
|
||||||
'@babel/types': 7.27.7
|
'@babel/types': 7.28.2
|
||||||
|
|
||||||
'@babel/traverse@7.23.6':
|
'@babel/traverse@7.23.6':
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -5677,7 +5673,7 @@ snapshots:
|
|||||||
'@babel/generator': 7.27.5
|
'@babel/generator': 7.27.5
|
||||||
'@babel/parser': 7.27.5
|
'@babel/parser': 7.27.5
|
||||||
'@babel/template': 7.27.2
|
'@babel/template': 7.27.2
|
||||||
'@babel/types': 7.27.7
|
'@babel/types': 7.28.2
|
||||||
debug: 4.4.1
|
debug: 4.4.1
|
||||||
globals: 11.12.0
|
globals: 11.12.0
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
@@ -5718,11 +5714,6 @@ snapshots:
|
|||||||
'@babel/helper-string-parser': 7.27.1
|
'@babel/helper-string-parser': 7.27.1
|
||||||
'@babel/helper-validator-identifier': 7.27.1
|
'@babel/helper-validator-identifier': 7.27.1
|
||||||
|
|
||||||
'@babel/types@7.27.7':
|
|
||||||
dependencies:
|
|
||||||
'@babel/helper-string-parser': 7.27.1
|
|
||||||
'@babel/helper-validator-identifier': 7.27.1
|
|
||||||
|
|
||||||
'@babel/types@7.28.2':
|
'@babel/types@7.28.2':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/helper-string-parser': 7.27.1
|
'@babel/helper-string-parser': 7.27.1
|
||||||
@@ -6541,7 +6532,7 @@ snapshots:
|
|||||||
|
|
||||||
'@svgr/hast-util-to-babel-ast@8.0.0':
|
'@svgr/hast-util-to-babel-ast@8.0.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/types': 7.27.7
|
'@babel/types': 7.28.2
|
||||||
entities: 4.5.0
|
entities: 4.5.0
|
||||||
|
|
||||||
'@svgr/plugin-jsx@8.1.0(@svgr/core@8.1.0(typescript@5.5.3))':
|
'@svgr/plugin-jsx@8.1.0(@svgr/core@8.1.0(typescript@5.5.3))':
|
||||||
@@ -6607,16 +6598,16 @@ snapshots:
|
|||||||
|
|
||||||
'@types/babel__generator@7.6.8':
|
'@types/babel__generator@7.6.8':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/types': 7.27.7
|
'@babel/types': 7.28.2
|
||||||
|
|
||||||
'@types/babel__template@7.4.4':
|
'@types/babel__template@7.4.4':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/parser': 7.27.5
|
'@babel/parser': 7.27.5
|
||||||
'@babel/types': 7.27.7
|
'@babel/types': 7.28.2
|
||||||
|
|
||||||
'@types/babel__traverse@7.20.4':
|
'@types/babel__traverse@7.20.4':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/types': 7.27.7
|
'@babel/types': 7.28.2
|
||||||
|
|
||||||
'@types/body-parser@1.19.2':
|
'@types/body-parser@1.19.2':
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -7223,7 +7214,7 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- debug
|
- debug
|
||||||
|
|
||||||
babel-plugin-react-compiler@19.1.0-rc.3:
|
babel-plugin-react-compiler@1.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/types': 7.28.2
|
'@babel/types': 7.28.2
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user