mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-09 01:13:55 +00:00
Improve Clone (#1897)
* refactor(rundown): optimise copy-paste performance * chore: configure opt-in compiler * refactor(rundown): stabilise frequently accessed data * feat(clone): allow cloning any element * remove paste above and cue increment from test --------- Co-authored-by: arc-alex <ac@omnivox.dk>
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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,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' },
|
||||
{
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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 }) => {
|
||||
|
||||
@@ -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
+16
-25
@@ -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
|
||||
@@ -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