mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-20 06:29:07 +00:00
refactor(rundown): new entries are appended to rundown
This commit is contained in:
committed by
Carlos Valente
parent
8363f06a5c
commit
a8c611911d
@@ -153,6 +153,41 @@ describe('mcp.service', () => {
|
||||
expect(result.created.map((entry) => entry.id)).toEqual(['entry-1', 'entry-2', 'entry-3', 'entry-4']);
|
||||
});
|
||||
|
||||
it('omits insert anchors when creating an entry without a position', async () => {
|
||||
await createEntryForMcp({
|
||||
type: SupportedEntry.Milestone,
|
||||
title: 'End marker',
|
||||
});
|
||||
|
||||
expect(addEntryMock).toHaveBeenCalledWith(
|
||||
'loaded-rundown',
|
||||
expect.objectContaining({ type: SupportedEntry.Milestone, title: 'End marker' }),
|
||||
);
|
||||
expect(addEntryMock.mock.calls[0][1]).not.toHaveProperty('after');
|
||||
expect(addEntryMock.mock.calls[0][1]).not.toHaveProperty('before');
|
||||
});
|
||||
|
||||
it('uses before true for the first batch entry and chains the rest', async () => {
|
||||
await batchCreateEntriesForMcp({
|
||||
before: true,
|
||||
entries: [
|
||||
{ type: SupportedEntry.Event, title: 'First' },
|
||||
{ type: SupportedEntry.Event, title: 'Second' },
|
||||
],
|
||||
});
|
||||
|
||||
expect(addEntryMock).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
'loaded-rundown',
|
||||
expect.objectContaining({ type: SupportedEntry.Event, title: 'First', before: true }),
|
||||
);
|
||||
expect(addEntryMock).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
'loaded-rundown',
|
||||
expect.objectContaining({ type: SupportedEntry.Event, title: 'Second', after: 'entry-1' }),
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects nested groups before creating entries', async () => {
|
||||
await expect(
|
||||
batchCreateEntriesForMcp({
|
||||
|
||||
@@ -71,10 +71,10 @@ Read the ontime://schema resource if you need a data model reference.
|
||||
|
||||
Steps:
|
||||
1. Call ontime_list_rundowns and identify the target rundown. If the user wants a background rundown, pass its \`rundownId\` in all entry read/write calls instead of loading it.
|
||||
2. Call ontime_get_rundown with the chosen \`rundownId\` to see current state and identify an \`after\` anchor if appending.
|
||||
2. Call ontime_get_rundown with the chosen \`rundownId\` to see current state and avoid duplicating existing entries.
|
||||
3. Call ontime_get_timer_state. If playback is not \`stop\` and the target is the loaded rundown, explain that MCP edits affect the live rundown and ask the user to confirm before changing it. If the target is a background rundown, it can be edited without interrupting playback.
|
||||
4. Build an array of entries in order and call ontime_batch_create_entries ONCE with all of them. This is much faster than calling ontime_create_entry per item.
|
||||
5. If the rundown already has events, pass \`after: <last event id>\` on the batch call so new events chain from the end.
|
||||
5. Omit \`after\` and \`before\` on the batch call to append the new entries to the end of the rundown. Use \`after: true\` for an explicit append request, or \`before: true\` for an explicit prepend request.
|
||||
|
||||
Entry type guidance:
|
||||
- Use \`event\` for anything with a scheduled time and duration (talks, panels, breaks, meals).
|
||||
|
||||
@@ -63,6 +63,11 @@ export type BatchCreateEntryArgs = CreateEntryArgs & { children?: BatchCreateEnt
|
||||
export type UpdateEntryArgs = EntryFieldArgs & TargetRundownArgs & { id: EntryId };
|
||||
export type GroupEntriesArgs = GroupFieldArgs & TargetRundownArgs & { ids: EntryId[] };
|
||||
export type UngroupEntryArgs = TargetRundownArgs & { id: EntryId };
|
||||
type BatchCreateEntriesArgs = TargetRundownArgs & {
|
||||
entries: BatchCreateEntryArgs[];
|
||||
after?: EntryId | true;
|
||||
before?: EntryId | true;
|
||||
};
|
||||
|
||||
export function resolveTargetRundownId(args: TargetRundownArgs): string {
|
||||
return args.rundownId ?? getCurrentRundownId();
|
||||
@@ -133,20 +138,24 @@ export function assertKnownCustomFields(...customValues: Array<EntryFieldArgs['c
|
||||
/** Translates tool arguments into the payload consumed by rundown.service addEntry */
|
||||
export function toEntryPayload(args: CreateEntryArgs): EventPostPayload {
|
||||
const { type = SupportedEntry.Event, after, before } = args;
|
||||
const insertOptions = {
|
||||
...(after !== undefined ? { after } : {}),
|
||||
...(before !== undefined ? { before } : {}),
|
||||
};
|
||||
|
||||
switch (type) {
|
||||
case SupportedEntry.Delay:
|
||||
return { type: SupportedEntry.Delay, duration: args.duration, after, before };
|
||||
return { type: SupportedEntry.Delay, duration: args.duration, ...insertOptions };
|
||||
case SupportedEntry.Milestone: {
|
||||
const { cue, title, note, colour, custom } = args;
|
||||
return { type: SupportedEntry.Milestone, cue, title, note, colour, custom, after, before };
|
||||
return { type: SupportedEntry.Milestone, cue, title, note, colour, custom, ...insertOptions };
|
||||
}
|
||||
case SupportedEntry.Group:
|
||||
// group creation currently only accepts a title, see generateEvent in rundown.utils.ts
|
||||
return { type: SupportedEntry.Group, title: args.title, after, before };
|
||||
return { type: SupportedEntry.Group, title: args.title, ...insertOptions };
|
||||
case SupportedEntry.Event: {
|
||||
const { type: _type, rundownId: _rundownId, ...eventFields } = args;
|
||||
return { type: SupportedEntry.Event, ...eventFields };
|
||||
const { type: _type, rundownId: _rundownId, after: _after, before: _before, ...eventFields } = args;
|
||||
return { type: SupportedEntry.Event, ...eventFields, ...insertOptions };
|
||||
}
|
||||
default:
|
||||
throw new Error(`Invalid entry type: ${String(type)}`);
|
||||
@@ -259,20 +268,25 @@ export async function ungroupEntryForMcp(args: UngroupEntryArgs) {
|
||||
return { target: getTargetMeta(rundownId), ungrouped: args.id, order: updatedRundown.order };
|
||||
}
|
||||
|
||||
export async function batchCreateEntriesForMcp(
|
||||
args: TargetRundownArgs & { entries: BatchCreateEntryArgs[]; after?: EntryId },
|
||||
) {
|
||||
const { entries = [], after } = args;
|
||||
export async function batchCreateEntriesForMcp(args: BatchCreateEntriesArgs) {
|
||||
const { entries = [], after, before } = args;
|
||||
validateBatchCreateEntries(entries);
|
||||
const allEntries = flattenBatchCreateEntries(entries);
|
||||
assertKnownCustomFields(...allEntries.map((entry) => entry.custom));
|
||||
const rundownId = resolveTargetRundownId(args);
|
||||
let previousId = after;
|
||||
let previousId: EntryId | undefined;
|
||||
const created: OntimeEntry[] = [];
|
||||
|
||||
for (const entryArgs of entries) {
|
||||
const firstInsertOptions =
|
||||
created.length === 0
|
||||
? {
|
||||
...(after !== undefined ? { after } : {}),
|
||||
...(before !== undefined ? { before } : {}),
|
||||
}
|
||||
: undefined;
|
||||
// eslint-disable-next-line no-await-in-loop -- top-level entries chain after the previously created one
|
||||
const entry = await createBatchEntry(rundownId, entryArgs, previousId);
|
||||
const entry = await createBatchEntry(rundownId, entryArgs, previousId, undefined, firstInsertOptions);
|
||||
created.push(...entry.created);
|
||||
previousId = entry.entry.id;
|
||||
}
|
||||
@@ -303,6 +317,7 @@ async function createBatchEntry(
|
||||
entryArgs: BatchCreateEntryArgs,
|
||||
previousId?: EntryId,
|
||||
parentId?: EntryId,
|
||||
firstInsertOptions?: InsertOptions,
|
||||
): Promise<{ entry: OntimeEntry; created: OntimeEntry[] }> {
|
||||
if (parentId && entryArgs.type === SupportedEntry.Group) {
|
||||
throw new Error('Cannot create a group inside another group.');
|
||||
@@ -311,8 +326,9 @@ async function createBatchEntry(
|
||||
const { children: _children, ...createArgs } = entryArgs;
|
||||
const payload = toEntryPayload(createArgs);
|
||||
const insertOptions = {
|
||||
...(previousId ? { after: previousId } : {}),
|
||||
...(parentId ? { parent: parentId } : {}),
|
||||
...(previousId !== undefined ? { after: previousId } : {}),
|
||||
...(parentId !== undefined ? { parent: parentId } : {}),
|
||||
...(previousId === undefined ? firstInsertOptions : {}),
|
||||
};
|
||||
|
||||
const createdEntry = await addEntry(rundownId, { ...payload, ...insertOptions } as EventPostPayload);
|
||||
|
||||
@@ -96,7 +96,7 @@ export const TOOL_DEFINITIONS = [
|
||||
{
|
||||
name: 'ontime_create_entry',
|
||||
description:
|
||||
'Create a new entry. Omit rundownId for the currently loaded live rundown, or provide a rundownId from ontime_list_rundowns to edit a background rundown without loading it. If playback is running and rundownId is omitted or matches the loaded rundown, confirm the user intends to change the live rundown before calling. Omit after/before to append at the end. For type "event" provide title plus enough timing data for Ontime to infer a strategy: timeStart+duration calculates timeEnd, timeStart+timeEnd calculates duration and locks end, timeEnd+duration calculates timeStart, and all three prioritise duration. For "milestone" provide cue/title/note/colour and optional custom values using existing project custom field keys. For "delay" provide duration. For "group" provide title plus optional note/colour/custom/targetDuration.',
|
||||
'Create a new entry. Omit after/before to append at the end, use after: true to explicitly append, use before: true to prepend, or use before/after with an entry ID to position the entry. Omit rundownId for the currently loaded live rundown, or provide a rundownId from ontime_list_rundowns to edit a background rundown without loading it. If playback is running and rundownId is omitted or matches the loaded rundown, confirm the user intends to change the live rundown before calling. For type "event" provide title plus enough timing data for Ontime to infer a strategy: timeStart+duration calculates timeEnd, timeStart+timeEnd calculates duration and locks end, timeEnd+duration calculates timeStart, and all three prioritise duration. For "milestone" provide cue/title/note/colour and optional custom values using existing project custom field keys. For "delay" provide duration. For "group" provide title plus optional note/colour/custom/targetDuration.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
@@ -114,8 +114,14 @@ export const TOOL_DEFINITIONS = [
|
||||
description: 'Duration in ms (events: should equal timeEnd - timeStart; delays: the schedule shift)',
|
||||
},
|
||||
targetDuration: { type: 'number', description: 'Groups only: planned length of the group in ms' },
|
||||
after: { type: 'string', description: 'Insert after this entry ID' },
|
||||
before: { type: 'string', description: 'Insert before this entry ID' },
|
||||
after: {
|
||||
type: ['string', 'boolean'],
|
||||
description: 'Insert after this entry ID, or true to append',
|
||||
},
|
||||
before: {
|
||||
type: ['string', 'boolean'],
|
||||
description: 'Insert before this entry ID, or true to prepend',
|
||||
},
|
||||
...EVENT_WRITABLE_FIELDS,
|
||||
},
|
||||
},
|
||||
@@ -218,13 +224,20 @@ export const TOOL_DEFINITIONS = [
|
||||
{
|
||||
name: 'ontime_batch_create_entries',
|
||||
description:
|
||||
'Create multiple entries, including groups with nested children. Omit rundownId for the currently loaded live rundown, or provide a rundownId from ontime_list_rundowns to edit a background rundown without loading it. If playback is running and rundownId is omitted or matches the loaded rundown, confirm the user intends to change the live rundown before calling. Use this for "build from agenda" flows to avoid many round trips. Entries are inserted in array order; if `after` is provided it positions the first top-level entry, subsequent top-level entries chain from the previous. A group entry may include `children`; those entries are created inside the group in array order. Groups cannot be nested. For events, provide title plus enough timing data for Ontime to infer a strategy: timeStart+duration calculates timeEnd, timeStart+timeEnd calculates duration and locks end, timeEnd+duration calculates timeStart, and all three prioritise duration.',
|
||||
'Create multiple entries, including groups with nested children. Omit rundownId for the currently loaded live rundown, or provide a rundownId from ontime_list_rundowns to edit a background rundown without loading it. If playback is running and rundownId is omitted or matches the loaded rundown, confirm the user intends to change the live rundown before calling. Use this for "build from agenda" flows to avoid many round trips. Entries are inserted in array order; omit after/before to append the first entry at the end, use after: true to explicitly append, use before: true to prepend, or use after/before with an entry ID to position the first top-level entry. Subsequent top-level entries chain from the previous. A group entry may include `children`; those entries are created inside the group in array order. Groups cannot be nested. For events, provide title plus enough timing data for Ontime to infer a strategy: timeStart+duration calculates timeEnd, timeStart+timeEnd calculates duration and locks end, timeEnd+duration calculates timeStart, and all three prioritise duration.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
required: ['entries'],
|
||||
properties: {
|
||||
...RUNDOWN_TARGET_FIELD,
|
||||
after: { type: 'string', description: 'Insert the first entry after this entry ID' },
|
||||
after: {
|
||||
type: ['string', 'boolean'],
|
||||
description: 'Insert the first entry after this entry ID, or true to append',
|
||||
},
|
||||
before: {
|
||||
type: ['string', 'boolean'],
|
||||
description: 'Insert the first entry before this entry ID, or true to prepend',
|
||||
},
|
||||
entries: {
|
||||
type: 'array',
|
||||
description: 'Array of entries to create, in desired order',
|
||||
@@ -588,7 +601,13 @@ const TOOL_HANDLERS: Record<ToolName, (args: Record<string, unknown>) => Promise
|
||||
|
||||
ontime_batch_create_entries: async (args) => {
|
||||
return ok(
|
||||
await batchCreateEntriesForMcp(args as TargetRundownArgs & { entries: BatchCreateEntryArgs[]; after?: EntryId }),
|
||||
await batchCreateEntriesForMcp(
|
||||
args as TargetRundownArgs & {
|
||||
entries: BatchCreateEntryArgs[];
|
||||
after?: EntryId | true;
|
||||
before?: EntryId | true;
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
|
||||
|
||||
Reference in New Issue
Block a user