mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-02 22:18:08 +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",
|
||||
"version": "4.2.0",
|
||||
"version": "4.2.1",
|
||||
"author": "Carlos Valente",
|
||||
"description": "Time keeping for live events",
|
||||
"repository": "https://github.com/cpvalente/ontime",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ontime-ui",
|
||||
"version": "4.2.0",
|
||||
"version": "4.2.1",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
@@ -17,7 +17,6 @@
|
||||
"@tanstack/react-table": "^8.21.3",
|
||||
"autosize": "^6.0.1",
|
||||
"axios": "^1.12.2",
|
||||
"babel-plugin-react-compiler": "19.1.0-rc.3",
|
||||
"csv-stringify": "^6.6.0",
|
||||
"prismjs": "^1.30.0",
|
||||
"react": "^19.1.1",
|
||||
@@ -66,6 +65,7 @@
|
||||
"@typescript-eslint/eslint-plugin": "catalog:",
|
||||
"@typescript-eslint/parser": "catalog:",
|
||||
"@vitejs/plugin-react": "4.5.1",
|
||||
"babel-plugin-react-compiler": "1.0.0",
|
||||
"eslint": "catalog:",
|
||||
"eslint-config-prettier": "catalog:",
|
||||
"eslint-plugin-jest": "^28.6.0",
|
||||
|
||||
@@ -75,7 +75,10 @@ export async function postAddEntry(
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
|
||||
@@ -107,7 +110,11 @@ export async function patchReorderEntry(rundownId: RundownId, data: ReorderEntry
|
||||
/**
|
||||
* 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 });
|
||||
}
|
||||
|
||||
@@ -121,8 +128,12 @@ export async function requestApplyDelay(rundownId: RundownId, delayId: EntryId):
|
||||
/**
|
||||
* HTTP request for cloning an entry
|
||||
*/
|
||||
export async function postCloneEntry(rundownId: RundownId, entryId: EntryId): Promise<AxiosResponse<Rundown>> {
|
||||
return axios.post(`${rundownPath}/${rundownId}/clone/${entryId}`);
|
||||
export async function postCloneEntry(
|
||||
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 { isLocalhost, supportsFullscreen } from '../../../externals';
|
||||
import { useKeepAwakeOptions } from '../../../features/keep-awake/KeepAwake';
|
||||
import { canUseWakeLock, useKeepAwakeOptions } from '../../../features/keep-awake/useWakeLock';
|
||||
import { navigatorConstants } from '../../../viewerConfig';
|
||||
import { useIsSmallScreen } from '../../hooks/useIsSmallScreen';
|
||||
import { useClientStore } from '../../stores/clientStore';
|
||||
@@ -69,7 +69,7 @@ function NavigationMenu({ isOpen, onClose }: NavigationMenuProps) {
|
||||
<IoSwapVertical />
|
||||
{mirror && <span className={style.note}>Active</span>}
|
||||
</NavigationMenuItem>
|
||||
{window.isSecureContext && (
|
||||
{canUseWakeLock && (
|
||||
<NavigationMenuItem active={keepAwake} onClick={toggleKeepAwake}>
|
||||
Keep Awake
|
||||
<LuCoffee />
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useCallback } from 'react';
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import {
|
||||
EntryId,
|
||||
InsertOptions,
|
||||
isOntimeEvent,
|
||||
isOntimeGroup,
|
||||
MaybeString,
|
||||
@@ -173,7 +174,8 @@ export const useEntryActions = () => {
|
||||
* @private
|
||||
*/
|
||||
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 }),
|
||||
onSettled: () => queryClient.invalidateQueries({ queryKey: RUNDOWN }),
|
||||
});
|
||||
@@ -182,14 +184,14 @@ export const useEntryActions = () => {
|
||||
* Clone an entry
|
||||
*/
|
||||
const clone = useCallback(
|
||||
async (entryId: EntryId) => {
|
||||
async (entryId: EntryId, options?: InsertOptions) => {
|
||||
try {
|
||||
const rundownId = getCurrentRundownData()?.id;
|
||||
if (!rundownId) {
|
||||
throw new Error('Rundown not initialised');
|
||||
}
|
||||
|
||||
await cloneEntryMutation([rundownId, entryId]);
|
||||
await cloneEntryMutation([rundownId, entryId, options]);
|
||||
} catch (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 { 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);
|
||||
}
|
||||
import { useWakelock } from './useWakeLock';
|
||||
|
||||
/**
|
||||
* Hook exposes the keep awake options
|
||||
* Composes the wakelock hook to contain re-renders
|
||||
*/
|
||||
export function useKeepAwakeOptions() {
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const maybePreset = use(PresetContext);
|
||||
export default function KeepAwake() {
|
||||
useWakelock();
|
||||
|
||||
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]);
|
||||
|
||||
return { keepAwake, toggleKeepAwake };
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
closestCenter,
|
||||
@@ -36,7 +36,6 @@ import { useEntryActions } from '../../common/hooks/useEntryAction';
|
||||
import useFollowComponent from '../../common/hooks/useFollowComponent';
|
||||
import { useRundownEditor } from '../../common/hooks/useSocket';
|
||||
import { useEntryCopy } from '../../common/stores/entryCopyStore';
|
||||
import { cloneEvent } from '../../common/utils/clone';
|
||||
import { lastMetadataKey, RundownMetadataObject } from '../../common/utils/rundownMetadata';
|
||||
import { AppMode, sessionKeys } from '../../ontimeConfig';
|
||||
|
||||
@@ -58,6 +57,8 @@ interface RundownProps {
|
||||
}
|
||||
|
||||
export default function Rundown({ data, rundownMetadata }: RundownProps) {
|
||||
'use memo';
|
||||
|
||||
const { order, entries, id } = data;
|
||||
// we create a copy of the rundown with a data structured aligned with what dnd-kit needs
|
||||
const featureData = useRundownEditor();
|
||||
@@ -68,17 +69,20 @@ export default function Rundown({ data, rundownMetadata }: RundownProps) {
|
||||
key: `rundown.${id}-editor-collapsed-groups`,
|
||||
defaultValue: [],
|
||||
});
|
||||
const collapsedGroupSet = useMemo(() => new Set(collapsedGroups), [collapsedGroups]);
|
||||
|
||||
const { addEntry, deleteEntry, move, reorderEntry } = useEntryActions();
|
||||
|
||||
const { entryCopyId, setEntryCopyId } = useEntryCopy();
|
||||
const { addEntry, clone, deleteEntry, move, reorderEntry } = useEntryActions();
|
||||
const setEntryCopyId = useEntryCopy((state) => state.setEntryCopyId);
|
||||
|
||||
// cursor
|
||||
const [editorMode] = useSessionStorage<AppMode>({
|
||||
key: sessionKeys.editorMode,
|
||||
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 scrollRef = useRef<HTMLDivElement | null>(null);
|
||||
@@ -105,20 +109,30 @@ export default function Rundown({ data, rundownMetadata }: RundownProps) {
|
||||
);
|
||||
|
||||
const insertCopyAtId = useCallback(
|
||||
(atId: string | null, copyId: string | null, above = false) => {
|
||||
const adjustedCursor = above ? getPreviousNormal(entries, order, atId ?? '').entry?.id ?? null : atId;
|
||||
if (copyId === null) {
|
||||
(atId: string | null, above = false) => {
|
||||
// lazily get the value from the store
|
||||
const { entryCopyId } = useEntryCopy.getState();
|
||||
if (entryCopyId === null || !entries[entryCopyId]) {
|
||||
// we cant clone without selection
|
||||
return;
|
||||
}
|
||||
const cloneEntry = entries[copyId];
|
||||
if (cloneEntry?.type === SupportedEntry.Event) {
|
||||
//if we don't have a cursor add the new event on top
|
||||
const newEvent = cloneEvent(cloneEntry);
|
||||
addEntry(newEvent, { after: adjustedCursor ?? undefined });
|
||||
|
||||
let normalisedAtId = atId;
|
||||
|
||||
const elementToCopy = entries[entryCopyId];
|
||||
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(
|
||||
(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 + V', () => insertCopyAtId(cursor, entryCopyId)],
|
||||
[
|
||||
'mod + shift + V',
|
||||
() => insertCopyAtId(cursor, entryCopyId, true),
|
||||
{ preventDefault: true, usePhysicalKeys: true },
|
||||
],
|
||||
['mod + V', () => insertCopyAtId(cursor)],
|
||||
['mod + shift + V', () => insertCopyAtId(cursor, true), { 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
|
||||
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
|
||||
setSortableData((currentEntries) => {
|
||||
return reorderArray(currentEntries, fromIndex, toIndex);
|
||||
@@ -438,9 +448,12 @@ export default function Rundown({ data, rundownMetadata }: RundownProps) {
|
||||
return <RundownEmpty handleAddNew={(type: SupportedEntry) => addEntry({ type })} />;
|
||||
}
|
||||
|
||||
// 1. gather presentation options
|
||||
// gather presentation options
|
||||
const isEditMode = editorMode === AppMode.Edit;
|
||||
|
||||
// gather rundown wide data
|
||||
const lastEntryId = order.at(-1);
|
||||
|
||||
return (
|
||||
<div className={style.rundownContainer} ref={scrollRef} data-testid='rundown'>
|
||||
<DndContext
|
||||
@@ -509,7 +522,7 @@ export default function Rundown({ data, rundownMetadata }: RundownProps) {
|
||||
const groupColour = entryMetadata.groupColour === '' ? '#9d9d9d' : entryMetadata.groupColour;
|
||||
|
||||
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
|
||||
|
||||
@@ -1,16 +1,4 @@
|
||||
import {
|
||||
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 { isOntimeDelay, isOntimeEvent, isOntimeMilestone, OntimeEntry, Playback, SupportedEntry } from 'ontime-types';
|
||||
|
||||
import RundownDelay from './rundown-delay/RundownDelay';
|
||||
import RundownEvent from './rundown-event/RundownEvent';
|
||||
@@ -44,13 +32,6 @@ export default function RundownEntry({
|
||||
totalGap,
|
||||
isLinkedToLoaded,
|
||||
}: RundownEntryProps) {
|
||||
const { addEntry } = useEntryActions();
|
||||
|
||||
const createCloneEvent = useMemoisedFn(() => {
|
||||
const newEvent = cloneEvent(data as OntimeEvent);
|
||||
addEntry(newEvent, { after: data.id });
|
||||
});
|
||||
|
||||
if (isOntimeEvent(data)) {
|
||||
return (
|
||||
<RundownEvent
|
||||
@@ -83,7 +64,6 @@ export default function RundownEntry({
|
||||
dayOffset={data.dayOffset}
|
||||
totalGap={totalGap}
|
||||
isLinkedToLoaded={isLinkedToLoaded}
|
||||
createCloneEvent={createCloneEvent}
|
||||
hasTriggers={data.triggers.length > 0}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -56,7 +56,6 @@ interface RundownEventProps {
|
||||
dayOffset: number;
|
||||
totalGap: number;
|
||||
isLinkedToLoaded: boolean;
|
||||
createCloneEvent: () => void;
|
||||
hasTriggers: boolean;
|
||||
}
|
||||
|
||||
@@ -91,10 +90,9 @@ export default function RundownEvent({
|
||||
totalGap,
|
||||
isLinkedToLoaded,
|
||||
hasTriggers,
|
||||
createCloneEvent,
|
||||
}: RundownEventProps) {
|
||||
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 handleRef = useRef<null | HTMLSpanElement>(null);
|
||||
@@ -172,7 +170,7 @@ export default function RundownEvent({
|
||||
type: 'item',
|
||||
label: 'Clone',
|
||||
icon: IoDuplicateOutline,
|
||||
onClick: createCloneEvent,
|
||||
onClick: () => clone(eventId, { after: eventId }),
|
||||
},
|
||||
{ type: 'divider' },
|
||||
{
|
||||
|
||||
@@ -11,7 +11,6 @@ import { formatTime } from '../../../common/utils/time';
|
||||
export function getTimerByType(
|
||||
freezeEnd: boolean,
|
||||
timerTypeNow: TimerType,
|
||||
countToEndNow: boolean,
|
||||
clock: number,
|
||||
timerObject: Pick<TimerState, 'current' | 'elapsed'>,
|
||||
timerTypeOverride?: TimerType,
|
||||
@@ -22,13 +21,6 @@ export function getTimerByType(
|
||||
|
||||
const viewTimerType = timerTypeOverride ?? timerTypeNow;
|
||||
|
||||
if (countToEndNow) {
|
||||
if (timerObject.current === null) {
|
||||
return null;
|
||||
}
|
||||
return freezeEnd ? Math.max(timerObject.current, 0) : timerObject.current;
|
||||
}
|
||||
|
||||
switch (viewTimerType) {
|
||||
case TimerType.CountDown:
|
||||
if (timerObject.current === null) {
|
||||
@@ -36,7 +28,7 @@ export function getTimerByType(
|
||||
}
|
||||
return freezeEnd ? Math.max(timerObject.current, 0) : timerObject.current;
|
||||
case TimerType.CountUp:
|
||||
return Math.abs(timerObject.elapsed ?? 0);
|
||||
return timerObject.elapsed;
|
||||
case TimerType.Clock:
|
||||
return clock;
|
||||
case TimerType.None:
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
|
||||
.timer {
|
||||
opacity: 1;
|
||||
font-family: var(--timer-font, $viewer-font-family);
|
||||
font-family: $viewer-font-family;
|
||||
color: var(--timer-colour, $ui-white);
|
||||
line-height: 0.9em;
|
||||
text-align: center;
|
||||
@@ -72,17 +72,17 @@
|
||||
|
||||
.secondary {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-height: 100%;
|
||||
height: auto;
|
||||
|
||||
margin-top: 0.125em;
|
||||
padding-block: 0.125em;
|
||||
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
color: var(--external-color-override, $external-color);
|
||||
color: $external-color;
|
||||
letter-spacing: 0.5px;
|
||||
line-height: 1em;
|
||||
line-height: 1;
|
||||
transition-property: opacity, height;
|
||||
transition-duration: $viewer-transition-time;
|
||||
border-top: 1px solid color-mix(in srgb, $external-color 10%, transparent);
|
||||
|
||||
@@ -40,7 +40,7 @@ export function PipTimer({ viewSettings }: PipTimerProps) {
|
||||
|
||||
// gather timer data
|
||||
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', {
|
||||
removeSeconds: false,
|
||||
removeLeadingZero: false,
|
||||
@@ -59,11 +59,11 @@ export function PipTimer({ viewSettings }: PipTimerProps) {
|
||||
return null;
|
||||
})();
|
||||
|
||||
const secondaryContent = getSecondaryDisplay(message, currentAux, 'min', false, false, false);
|
||||
const secondaryContent = getSecondaryDisplay(message, currentAux, 'min', false, true, false);
|
||||
|
||||
// gather presentation styles
|
||||
const resolvedTimerColour = getTimerColour(viewSettings, undefined, showWarning, showDanger);
|
||||
const { timerFontSize, externalFontSize } = getEstimatedFontSize(display, secondaryContent);
|
||||
const timerFontSize = getEstimatedFontSize(display, secondaryContent);
|
||||
const userStyles = {
|
||||
...(resolvedTimerColour && { '--timer-colour': resolvedTimerColour }),
|
||||
};
|
||||
@@ -84,11 +84,10 @@ export function PipTimer({ viewSettings }: PipTimerProps) {
|
||||
>
|
||||
{display}
|
||||
</div>
|
||||
<div
|
||||
className={cx(['secondary', !secondaryContent && 'secondary--hidden'])}
|
||||
style={{ fontSize: `${externalFontSize}vw` }}
|
||||
>
|
||||
{secondaryContent}
|
||||
<div className={cx(['secondary', !secondaryContent && 'secondary--hidden'])}>
|
||||
<FitText mode='multi' min={12} max={256}>
|
||||
{secondaryContent}
|
||||
</FitText>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -135,8 +135,8 @@
|
||||
|
||||
.secondary {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-height: 100%;
|
||||
height: auto;
|
||||
|
||||
margin-top: 0.125em;
|
||||
padding-block: 0.125em;
|
||||
@@ -145,7 +145,7 @@
|
||||
text-align: center;
|
||||
color: var(--external-color-override, $external-color);
|
||||
letter-spacing: 0.5px;
|
||||
line-height: 1em;
|
||||
line-height: 1;
|
||||
transition-property: opacity, height;
|
||||
transition-duration: $viewer-transition-time;
|
||||
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
|
||||
const totalTime = getTotalTime(time.duration, time.addedTime);
|
||||
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, {
|
||||
removeSeconds: hideTimerSeconds,
|
||||
removeLeadingZero: removeLeadingZeros,
|
||||
@@ -132,7 +132,7 @@ function Timer({ customFields, projectData, isMirrored, settings, viewSettings }
|
||||
|
||||
// gather presentation styles
|
||||
const resolvedTimerColour = getTimerColour(viewSettings, timerColour, showWarning, showDanger);
|
||||
const { timerFontSize, externalFontSize } = getEstimatedFontSize(display, secondaryContent);
|
||||
const timerFontSize = getEstimatedFontSize(display, secondaryContent);
|
||||
const userStyles = {
|
||||
...(keyColour && { '--timer-bg': keyColour }),
|
||||
...(resolvedTimerColour && { '--timer-colour': resolvedTimerColour }),
|
||||
@@ -185,11 +185,10 @@ function Timer({ customFields, projectData, isMirrored, settings, viewSettings }
|
||||
{display}
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={cx(['secondary', !secondaryContent && 'secondary--hidden'])}
|
||||
style={{ fontSize: `${externalFontSize}vw` }}
|
||||
>
|
||||
{secondaryContent}
|
||||
<div className={cx(['secondary', !secondaryContent && 'secondary--hidden'])}>
|
||||
<FitText mode='multi' min={64} max={256}>
|
||||
{secondaryContent}
|
||||
</FitText>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@ const fontSizeMap: { [key: number]: number } = {
|
||||
* Finds a font size that fits the timer in the screen
|
||||
* Unfortunately hand tweaked
|
||||
*/
|
||||
export function getEstimatedFontSize(stageTimer: string, secondaryContent?: string) {
|
||||
export function getEstimatedFontSize(stageTimer: string, secondaryContent?: string): number {
|
||||
const stageTimerCharacters = stageTimer.length;
|
||||
let timerFontSize = (100 / (stageTimerCharacters - 1)) * 1.25;
|
||||
|
||||
@@ -58,20 +58,13 @@ export function getEstimatedFontSize(stageTimer: string, secondaryContent?: stri
|
||||
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) {
|
||||
// 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;
|
||||
if (secondaryContent.length > 25) {
|
||||
externalFontSize = (100 / (secondaryContent.length - 1)) * 1.8;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
timerFontSize,
|
||||
externalFontSize,
|
||||
};
|
||||
return timerFontSize;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -9,6 +9,10 @@ import { ONTIME_VERSION } from './src/ONTIME_VERSION';
|
||||
|
||||
const sentryAuthToken = process.env.SENTRY_AUTH_TOKEN;
|
||||
|
||||
const ReactCompilerConfig = {
|
||||
compilationMode: 'annotation',
|
||||
};
|
||||
|
||||
export default defineConfig({
|
||||
base: './', // Ontime cloud: we use relative paths to allow them to reference a dynamic base set at runtime
|
||||
define: {
|
||||
@@ -16,7 +20,11 @@ export default defineConfig({
|
||||
'import.meta.env.IS_DOCKER': process.env.NODE_ENV === 'docker',
|
||||
},
|
||||
plugins: [
|
||||
react(),
|
||||
react({
|
||||
babel: {
|
||||
plugins: [['babel-plugin-react-compiler', ReactCompilerConfig]],
|
||||
},
|
||||
}),
|
||||
svgrPlugin(),
|
||||
sentryAuthToken &&
|
||||
sentryVitePlugin({
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ontime-electron",
|
||||
"version": "4.2.0",
|
||||
"version": "4.2.1",
|
||||
"author": "Carlos Valente",
|
||||
"description": "Time keeping for live events",
|
||||
"repository": "https://github.com/cpvalente/ontime",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@getontime/resolver",
|
||||
"version": "4.0.0",
|
||||
"version": "4.2.1",
|
||||
"type": "module",
|
||||
"repository": "https://github.com/cpvalente/ontime",
|
||||
"types": "./dist/main.d.ts",
|
||||
@@ -23,7 +23,7 @@
|
||||
"tsup": "^8.5.0",
|
||||
"rimraf": "catalog:",
|
||||
"typescript": "catalog:",
|
||||
"ontime-types": "workspace:^4.0.0"
|
||||
"ontime-types": "workspace:^4.2.1"
|
||||
},
|
||||
"files": ["dist"]
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "ontime-server",
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"version": "4.2.0",
|
||||
"version": "4.2.1",
|
||||
"exports": "./src/index.js",
|
||||
"dependencies": {
|
||||
"@googleapis/sheets": "^5.0.5",
|
||||
@@ -60,6 +60,7 @@
|
||||
"build": "node esbuild.js",
|
||||
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1564,7 +1564,7 @@ describe('rundownMutation.swap()', () => {
|
||||
});
|
||||
|
||||
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({
|
||||
order: ['1'],
|
||||
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({
|
||||
order: ['1'],
|
||||
entries: {
|
||||
@@ -1621,6 +1621,55 @@ describe('rundownMutation.clone()', () => {
|
||||
});
|
||||
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()', () => {
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
duplicateRundown,
|
||||
getInsertAfterId,
|
||||
hasChanges,
|
||||
makeDeepClone,
|
||||
} from '../rundown.utils.js';
|
||||
import { makeOntimeGroup, makeOntimeEvent, makeRundown } from '../__mocks__/rundown.mocks.js';
|
||||
|
||||
@@ -265,7 +266,7 @@ describe('getInsertAfterId()', () => {
|
||||
});
|
||||
|
||||
describe('duplicateRundown', () => {
|
||||
it("duplicates a given rundown", () => {
|
||||
it('duplicates a given rundown', () => {
|
||||
const demoRundown = demoDb.rundowns['default'];
|
||||
const title = 'Duplicated Rundown';
|
||||
const duplicatedRundown = duplicateRundown(demoRundown, title);
|
||||
@@ -275,10 +276,51 @@ describe('duplicateRundown', () => {
|
||||
entries: expect.any(Object),
|
||||
order: expect.any(Array),
|
||||
flatOrder: expect.any(Array),
|
||||
})
|
||||
});
|
||||
expect(demoRundown.id).not.toEqual(duplicatedRundown.id);
|
||||
expect(duplicatedRundown.order.length).toEqual(demoRundown.order.length);
|
||||
expect(duplicatedRundown.flatOrder.length).toEqual(demoRundown.flatOrder.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,
|
||||
PatchWithId,
|
||||
Rundown,
|
||||
InsertOptions,
|
||||
} from 'ontime-types';
|
||||
import { customFieldLabelToKey, insertAtIndex } from 'ontime-utils';
|
||||
|
||||
import { getDataProvider } from '../../classes/data-provider/DataProvider.js';
|
||||
import { consoleError } from '../../utils/console.js';
|
||||
|
||||
import type { RundownMetadata } from './rundown.types.js';
|
||||
import {
|
||||
applyPatchToEntry,
|
||||
cloneGroup,
|
||||
cloneEntry,
|
||||
cloneSimpleRundownEntry,
|
||||
createGroup,
|
||||
deleteById,
|
||||
doesInvalidateMetadata,
|
||||
getInsertAfterId,
|
||||
getUniqueId,
|
||||
makeDeepClone,
|
||||
} from './rundown.utils.js';
|
||||
import { makeRundownMetadata, ProcessedRundownMetadata } from './rundown.parser.js';
|
||||
import { consoleError } from '../../utils/console.js';
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
if (parent) {
|
||||
// 1. inserting an entry inside a group
|
||||
|
||||
if ('parent' in entry) {
|
||||
entry.parent = parent.id;
|
||||
}
|
||||
|
||||
if (afterId) {
|
||||
const atEventsIndex = parent.entries.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
|
||||
* 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)) {
|
||||
const newGroup = cloneGroup(entry, getUniqueId(rundown));
|
||||
const nestedIds: EntryId[] = [];
|
||||
const { newGroup, nestedEntries } = makeDeepClone(entry, rundown);
|
||||
|
||||
for (let i = 0; i < entry.entries.length; i++) {
|
||||
const nestedEntryId = entry.entries[i];
|
||||
const nestedEntry = rundown.entries[nestedEntryId];
|
||||
if (!nestedEntry) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 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;
|
||||
// insert all entries into the rundown
|
||||
rundown.entries[newGroup.id] = newGroup;
|
||||
for (let i = 0; i < nestedEntries.length; i++) {
|
||||
const nestedEntry = nestedEntries[i];
|
||||
rundown.entries[nestedEntry.id] = nestedEntry;
|
||||
}
|
||||
|
||||
// indexes + 1 since we are inserting after the cloned group
|
||||
const atIndex = rundown.order.indexOf(entry.id) + 1;
|
||||
// by default we insert after the cloned element
|
||||
let atIndex = rundown.order.indexOf(entry.id) + 1;
|
||||
|
||||
newGroup.entries = nestedIds;
|
||||
newGroup.title = `${entry.title || 'Untitled'} (copy)`;
|
||||
const referenceId = options?.after ?? options?.before;
|
||||
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);
|
||||
|
||||
return newGroup;
|
||||
} else {
|
||||
const parent: OntimeGroup | null = entry.parent ? (rundown.entries[entry.parent] as OntimeGroup) : null;
|
||||
return add(rundown, cloneEntry(entry, getUniqueId(rundown)), entry.id, parent);
|
||||
const clonedEntry = cloneSimpleRundownEntry(entry, getUniqueId(rundown));
|
||||
|
||||
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,
|
||||
entrySwapValidator,
|
||||
validateRundownMutation,
|
||||
clonePostValidator,
|
||||
} from './rundown.validation.js';
|
||||
import { paramsWithId } from '../validation-utils/validationFunction.js';
|
||||
import { getDataProvider } from '../../classes/data-provider/DataProvider.js';
|
||||
@@ -296,10 +297,14 @@ router.patch(
|
||||
router.post(
|
||||
'/:rundownId/clone/:id',
|
||||
paramsWithId,
|
||||
clonePostValidator,
|
||||
validateRundownMutation,
|
||||
async (req: Request, res: Response<Rundown | ErrorResponse>) => {
|
||||
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);
|
||||
} catch (error) {
|
||||
const message = getErrorMessage(error);
|
||||
|
||||
@@ -14,11 +14,15 @@ import {
|
||||
Rundown,
|
||||
LogOrigin,
|
||||
ProjectRundowns,
|
||||
InsertOptions,
|
||||
} from 'ontime-types';
|
||||
import { customFieldLabelToKey } from 'ontime-utils';
|
||||
|
||||
import { updateRundownData } from '../../stores/runtimeState.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 {
|
||||
createTransaction,
|
||||
@@ -29,9 +33,6 @@ import {
|
||||
} from './rundown.dao.js';
|
||||
import type { RundownMetadata } from './rundown.types.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
|
||||
@@ -347,17 +348,18 @@ export async function swapEvents(fromId: EntryId, toId: EntryId): Promise<Rundow
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
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 originalEntry = rundown.entries[entryId];
|
||||
|
||||
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();
|
||||
|
||||
// schedule the side effects
|
||||
@@ -373,7 +375,6 @@ export async function cloneEntry(entryId: EntryId): Promise<Rundown> {
|
||||
} else if (isOntimeDelay(newEntry)) {
|
||||
notifyChanges(rundownMetadata, revision, { external: true });
|
||||
}
|
||||
notifyChanges(rundownMetadata, revision, { timer: true, external: true });
|
||||
});
|
||||
|
||||
return rundownResult;
|
||||
|
||||
@@ -396,20 +396,54 @@ export function cloneGroup(entry: OntimeGroup, newId: EntryId): OntimeGroup {
|
||||
|
||||
// in groups, we need to remove the events references
|
||||
newEntry.entries = [];
|
||||
newEntry.title = `${entry.title || 'Untitled'} (copy)`;
|
||||
newEntry.revision = 0;
|
||||
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)) {
|
||||
return cloneEvent(entry, newId);
|
||||
} else if (isOntimeDelay(entry)) {
|
||||
return cloneDelay(entry, newId);
|
||||
} else if (isOntimeGroup(entry)) {
|
||||
return cloneGroup(entry, newId);
|
||||
} else if (isOntimeMilestone(entry)) {
|
||||
return cloneMilestone(entry, newId);
|
||||
}
|
||||
|
||||
@@ -42,6 +42,13 @@ export const entryPostValidator = [
|
||||
requestValidationFunction,
|
||||
];
|
||||
|
||||
export const clonePostValidator = [
|
||||
body('after').optional().isString(),
|
||||
body('before').optional().isString(),
|
||||
|
||||
requestValidationFunction,
|
||||
];
|
||||
|
||||
export const entryPutValidator = [body('id').isString().trim().notEmpty(), requestValidationFunction];
|
||||
|
||||
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 { eventStore } from '../stores/EventStore.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 { throttle } from '../utils/throttle.js';
|
||||
import { coerceEnum } from '../utils/coerceType.js';
|
||||
@@ -77,10 +77,9 @@ const actionHandlers: Record<ApiActionTag, ActionHandler> = {
|
||||
let shouldThrottle = false;
|
||||
|
||||
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');
|
||||
}
|
||||
// parseProperty is async because of the data lock
|
||||
const newObjectProperty = parseProperty(property, value);
|
||||
const key = Object.keys(newObjectProperty)[0];
|
||||
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 { coerceBoolean, coerceColour, coerceEnum, coerceNumber, coerceString } from '../utils/coerceType.js';
|
||||
@@ -60,3 +60,14 @@ export function parseProperty(property: string, value: unknown) {
|
||||
const parserFn = propertyConversion[property];
|
||||
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
|
||||
await getDataProvider().mergeIntoData(rest);
|
||||
|
||||
// ... but rundown and custom fields need to be checked
|
||||
if (rundowns != null) {
|
||||
const customFields = parseCustomFields(data);
|
||||
const result = parseRundowns(data, customFields);
|
||||
// the rundown depends on custom fields
|
||||
// so custom fields needs to be checked first
|
||||
if (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
|
||||
@@ -330,7 +337,7 @@ export async function patchCurrentProject(data: Partial<DatabaseModel>) {
|
||||
const rundownToLoad =
|
||||
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();
|
||||
|
||||
@@ -26,17 +26,9 @@ test('Copy-paste', async ({ page }) => {
|
||||
// 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('5');
|
||||
await expect(page.getByTestId('entry-2').getByTestId('rundown-event')).toContainText('4');
|
||||
|
||||
// copy paste above
|
||||
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');
|
||||
//TODO: reintroduce the past above test
|
||||
});
|
||||
|
||||
test('Move', async ({ page }) => {
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ontime",
|
||||
"version": "4.2.0",
|
||||
"version": "4.2.1",
|
||||
"description": "Time keeping for live events",
|
||||
"keywords": [
|
||||
"ontime",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": "4.0.0-beta.4",
|
||||
"version": "4.2.1",
|
||||
"name": "ontime-types",
|
||||
"type": "module",
|
||||
"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 EventPostPayload = Partial<OntimeEntry> & {
|
||||
export type InsertOptions = {
|
||||
after?: EntryId;
|
||||
before?: EntryId;
|
||||
};
|
||||
}
|
||||
|
||||
export type TransientEventPayload = Partial<OntimeEntry> & {
|
||||
after?: EntryId;
|
||||
before?: EntryId;
|
||||
};
|
||||
export type EventPostPayload = Partial<OntimeEntry> & InsertOptions;
|
||||
|
||||
export type TransientEventPayload = Partial<OntimeEntry> & InsertOptions;
|
||||
|
||||
export type ProjectRundown = {
|
||||
id: string;
|
||||
|
||||
@@ -79,6 +79,7 @@ export type {
|
||||
} from './api/ontime-controller/BackendResponse.type.js';
|
||||
export type {
|
||||
EventPostPayload,
|
||||
InsertOptions,
|
||||
PatchWithId,
|
||||
ProjectRundown,
|
||||
ProjectRundownsList,
|
||||
|
||||
Generated
+17
-26
@@ -137,9 +137,6 @@ importers:
|
||||
axios:
|
||||
specifier: ^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:
|
||||
specifier: ^6.6.0
|
||||
version: 6.6.0
|
||||
@@ -207,6 +204,9 @@ importers:
|
||||
'@vitejs/plugin-react':
|
||||
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))
|
||||
babel-plugin-react-compiler:
|
||||
specifier: 1.0.0
|
||||
version: 1.0.0
|
||||
eslint:
|
||||
specifier: 'catalog:'
|
||||
version: 8.56.0
|
||||
@@ -295,7 +295,7 @@ importers:
|
||||
specifier: 'catalog:'
|
||||
version: 8.56.0
|
||||
ontime-types:
|
||||
specifier: workspace:^4.0.0
|
||||
specifier: workspace:^4.2.1
|
||||
version: link:../../packages/types
|
||||
rimraf:
|
||||
specifier: 'catalog:'
|
||||
@@ -738,10 +738,6 @@ packages:
|
||||
resolution: {integrity: sha512-Y1GkI4ktrtvmawoSq+4FCVHNryea6uR+qUQy0AGxLSsjCX0nVmkYQMBLHDkXZuo5hGx7eYdnIaslsdBFm7zbUw==}
|
||||
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':
|
||||
resolution: {integrity: sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
@@ -2183,8 +2179,8 @@ packages:
|
||||
axios@1.12.2:
|
||||
resolution: {integrity: sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==}
|
||||
|
||||
babel-plugin-react-compiler@19.1.0-rc.3:
|
||||
resolution: {integrity: sha512-mjRn69WuTz4adL0bXGx8Rsyk1086zFJeKmes6aK0xPuK3aaXmDJdLHqwKKMrpm6KAI1MCoUK72d2VeqQbu8YIA==}
|
||||
babel-plugin-react-compiler@1.0.0:
|
||||
resolution: {integrity: sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw==}
|
||||
|
||||
balanced-match@1.0.2:
|
||||
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
|
||||
@@ -5380,7 +5376,7 @@ snapshots:
|
||||
'@babel/parser': 7.23.6
|
||||
'@babel/template': 7.22.15
|
||||
'@babel/traverse': 7.23.6
|
||||
'@babel/types': 7.27.7
|
||||
'@babel/types': 7.28.2
|
||||
convert-source-map: 2.0.0
|
||||
debug: 4.4.1
|
||||
gensync: 1.0.0-beta.2
|
||||
@@ -5439,7 +5435,7 @@ snapshots:
|
||||
'@babel/generator@7.27.5':
|
||||
dependencies:
|
||||
'@babel/parser': 7.27.5
|
||||
'@babel/types': 7.27.7
|
||||
'@babel/types': 7.28.2
|
||||
'@jridgewell/gen-mapping': 0.3.8
|
||||
'@jridgewell/trace-mapping': 0.3.25
|
||||
jsesc: 3.1.0
|
||||
@@ -5594,7 +5590,7 @@ snapshots:
|
||||
'@babel/helpers@7.27.4':
|
||||
dependencies:
|
||||
'@babel/template': 7.27.2
|
||||
'@babel/types': 7.27.7
|
||||
'@babel/types': 7.28.2
|
||||
|
||||
'@babel/helpers@7.28.3':
|
||||
dependencies:
|
||||
@@ -5610,7 +5606,7 @@ snapshots:
|
||||
|
||||
'@babel/parser@7.23.6':
|
||||
dependencies:
|
||||
'@babel/types': 7.27.7
|
||||
'@babel/types': 7.28.2
|
||||
|
||||
'@babel/parser@7.27.5':
|
||||
dependencies:
|
||||
@@ -5654,7 +5650,7 @@ snapshots:
|
||||
dependencies:
|
||||
'@babel/code-frame': 7.27.1
|
||||
'@babel/parser': 7.27.5
|
||||
'@babel/types': 7.27.7
|
||||
'@babel/types': 7.28.2
|
||||
|
||||
'@babel/traverse@7.23.6':
|
||||
dependencies:
|
||||
@@ -5677,7 +5673,7 @@ snapshots:
|
||||
'@babel/generator': 7.27.5
|
||||
'@babel/parser': 7.27.5
|
||||
'@babel/template': 7.27.2
|
||||
'@babel/types': 7.27.7
|
||||
'@babel/types': 7.28.2
|
||||
debug: 4.4.1
|
||||
globals: 11.12.0
|
||||
transitivePeerDependencies:
|
||||
@@ -5718,11 +5714,6 @@ snapshots:
|
||||
'@babel/helper-string-parser': 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':
|
||||
dependencies:
|
||||
'@babel/helper-string-parser': 7.27.1
|
||||
@@ -6541,7 +6532,7 @@ snapshots:
|
||||
|
||||
'@svgr/hast-util-to-babel-ast@8.0.0':
|
||||
dependencies:
|
||||
'@babel/types': 7.27.7
|
||||
'@babel/types': 7.28.2
|
||||
entities: 4.5.0
|
||||
|
||||
'@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':
|
||||
dependencies:
|
||||
'@babel/types': 7.27.7
|
||||
'@babel/types': 7.28.2
|
||||
|
||||
'@types/babel__template@7.4.4':
|
||||
dependencies:
|
||||
'@babel/parser': 7.27.5
|
||||
'@babel/types': 7.27.7
|
||||
'@babel/types': 7.28.2
|
||||
|
||||
'@types/babel__traverse@7.20.4':
|
||||
dependencies:
|
||||
'@babel/types': 7.27.7
|
||||
'@babel/types': 7.28.2
|
||||
|
||||
'@types/body-parser@1.19.2':
|
||||
dependencies:
|
||||
@@ -7223,7 +7214,7 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- debug
|
||||
|
||||
babel-plugin-react-compiler@19.1.0-rc.3:
|
||||
babel-plugin-react-compiler@1.0.0:
|
||||
dependencies:
|
||||
'@babel/types': 7.28.2
|
||||
|
||||
|
||||
Reference in New Issue
Block a user