* refactor: align header columns

* refactor: improve scrollbar visibility

* refactor: center align table elements

* refactor: make param elements stateful

* fix: issue with collapsed elements not loosing value

* fix: prevent search params containing multiple alias references

* fix: the issue where a file disappears if it is both migrated and recovered in the same load operation (#1744)

* refactor: disable group action for elements in groups

* refactor: move context menu items into the event element (#1747)

* feat: sheet import new features for v4 (#1730)

* import milestone

* fixup! import milestone

* test: milestone import

* add entries to group

stop on new group or on group-end type

* fixup! add entries to group

* cleanup

* add event target duration

* link start if undefined

* add skip import type

* extract some to the excel paresing functions

* tweaks to presentation

* move file

---------

Co-authored-by: Carlos Valente <carlosvalente@pm.me>

* fix: notify runtimeStore of events bieng groupd

* fix: improve authentication and stage detection in demo

* chore: ship logo with project

* fix: client is referenced by name

* fix: prevent reflow in event editor

* fix: stale render on selected event due to ref mismatch

* Create/Load/Delete multiple rundowns (#1696)

* refactor: restore last loaded rundown

* refactor: initialise rundown in ProjectService

* feat: allow switching rundowns

ensure on coordination between the db object and the working object

server provide list of rundowns

implement switch in the UI

implement delete

implement new rundown button

* fix: render order for floating button

* refactor: appropriate names to service

* refactor: rundown endpoints

* refactor: save last loaded rundown ID

* refactor: rundown management UI

* refactor: emit refetch all on project load

---------

Co-authored-by: Carlos Valente <carlosvalente@pm.me>

* feat: recover single event subscription

* fixup! feat: sheet import new features for v4 (#1730)

* fix: prevent dropping a group inside another

* fixup! refactor: move context menu items into the event element (#1747)

* fix: prevent stale references to custom fields

* fix: propagate updates to all rundowns

* refactor: client rundown metadata (#1728)

* generate metadata in the hook

* move test

* ensure there is always a last element

* use for-loop

* update metadata in useEfect

* fully extract metadata generation

* use direct assignment

* cleanup

---------

Co-authored-by: Carlos Valente <carlosvalente@pm.me>

* refactor: small imporvement and tests for coerce functions (#1752)

* refactor: small imporvement and tests for coerce functions

add test `coerceString`

add test `coerceBoolean`

add test `coerceColour`

* remove old todo

* fix: consistent quick add behaviour

* refactor: create flat rundown with metadata

* fix: show add buttons on top

* feat: allow editing milestones

* refactor: style tweaks to rundown elements

refactor: milestones are full width

refactor: cuesheet header alignment

fix: editor styling in cuesheet

* refactor: virtualise table

* refactor: improve overscan (#1758)

* bump version to 4.0.0-alpha.5

---------

Co-authored-by: Alex Christoffer Rasmussen <ac@omnivox.dk>
This commit is contained in:
Carlos Valente
2025-09-03 15:50:20 +02:00
committed by GitHub
parent 3c41b40c5f
commit ae15f3cdc5
102 changed files with 2950 additions and 2104 deletions
+260 -102
View File
@@ -1,5 +1,5 @@
import { ErrorResponse, MessageResponse, OntimeEntry, ProjectRundownsList, Rundown } from 'ontime-types';
import { getErrorMessage } from 'ontime-utils';
import { ErrorResponse, OntimeEntry, ProjectRundownsList, Rundown } from 'ontime-types';
import { generateId, getErrorMessage } from 'ontime-utils';
import type { Request, Response } from 'express';
import express from 'express';
@@ -14,40 +14,36 @@ import {
deleteEntries,
editEntry,
groupEntries,
initRundown,
reorderEntry,
swapEvents,
ungroupEntries,
} from './rundown.service.js';
import {
rundownArrayOfIds,
rundownBatchPutValidator,
entryBatchPutValidator,
entryPostValidator,
rundownPostValidator,
rundownPutValidator,
rundownReorderValidator,
rundownSwapValidator,
entryPutValidator,
entryReorderValidator,
entrySwapValidator,
validateRundownMutation,
} from './rundown.validation.js';
import { paramsWithId } from '../validation-utils/validationFunction.js';
import { getDataProvider } from '../../classes/data-provider/DataProvider.js';
import { defaultRundown } from '../../models/dataModel.js';
import { normalisedToRundownArray } from './rundown.utils.js';
export const router = express.Router();
// #region operations on project rundowns =========================
/**
* Returns all rundowns in the project
*/
router.get('/', async (_req: Request, res: Response<ProjectRundownsList>) => {
const rundown = getCurrentRundown();
// TODO: we currently make a project with only the current rundown
res.json({
loaded: rundown.id,
rundowns: [
{
id: rundown.id,
title: rundown.title,
numEntries: rundown.order.length,
revision: rundown.revision,
},
],
});
const projectRundowns = getDataProvider().getProjectRundowns();
res.json({ loaded: getCurrentRundown().id, rundowns: normalisedToRundownArray(projectRundowns) });
});
/**
@@ -58,113 +54,275 @@ router.get('/current', async (_req: Request, res: Response<Rundown>) => {
res.json(rundown);
});
router.post('/', rundownPostValidator, async (req: Request, res: Response<OntimeEntry | ErrorResponse>) => {
/**
* Loads a given rundown
*/
router.post('/:id/load', paramsWithId, async (req: Request, res: Response<ProjectRundownsList | ErrorResponse>) => {
try {
const newEvent = await addEntry(req.body);
res.status(201).send(newEvent);
// maybe the rundown is already loaded
if (req.params.id === getCurrentRundown().id) {
const projectRundowns = getDataProvider().getProjectRundowns();
res.status(200).json({ loaded: getCurrentRundown().id, rundowns: normalisedToRundownArray(projectRundowns) });
return;
}
const dataProvider = getDataProvider();
const rundown = dataProvider.getRundown(req.params.id);
const customField = dataProvider.getCustomFields();
await initRundown(rundown, customField);
const projectRundowns = getDataProvider().getProjectRundowns();
res.status(200).json({ loaded: getCurrentRundown().id, rundowns: normalisedToRundownArray(projectRundowns) });
} catch (error) {
const message = getErrorMessage(error);
res.status(400).send({ message });
}
});
router.put('/', rundownPutValidator, async (req: Request, res: Response<OntimeEntry | ErrorResponse>) => {
/**
* Creates a new rundown
*/
router.post('/', rundownPostValidator, async (req: Request, res: Response<ProjectRundownsList | ErrorResponse>) => {
try {
const event = await editEntry(req.body);
res.status(200).send(event);
const id = generateId();
await getDataProvider().setRundown(id, { ...defaultRundown, id, title: req.body.title });
const projectRundowns = getDataProvider().getProjectRundowns();
res.status(201).json({ loaded: getCurrentRundown().id, rundowns: normalisedToRundownArray(projectRundowns) });
} catch (error) {
const message = getErrorMessage(error);
res.status(400).send({ message });
}
});
router.put('/batch', rundownBatchPutValidator, async (req: Request, res: Response<Rundown | ErrorResponse>) => {
/**
* Deletes a rundown if not loaded
*/
router.delete('/:id', paramsWithId, async (req: Request, res: Response<ProjectRundownsList | ErrorResponse>) => {
try {
const rundown = await batchEditEntries(req.body.ids, req.body.data);
res.status(200).send(rundown);
if (req.params.id === getCurrentRundown().id) {
res.status(400).send({ message: 'Cannot delete loaded rundown' });
return;
}
const dataProvider = getDataProvider();
const projectRundowns = dataProvider.getProjectRundowns();
if (Object.keys(projectRundowns).length <= 1) {
// might never hit this as it is likely covered by the case of trying to delete the loaded rundown
res.status(400).send({ message: 'Cannot delete the last rundown' });
return;
}
await dataProvider.deleteRundown(req.params.id);
const newProjectRundowns = getDataProvider().getProjectRundowns();
res.status(200).json({ loaded: getCurrentRundown().id, rundowns: normalisedToRundownArray(newProjectRundowns) });
} catch (error) {
const message = getErrorMessage(error);
res.status(400).send({ message });
}
});
router.patch('/reorder', rundownReorderValidator, async (req: Request, res: Response<Rundown | ErrorResponse>) => {
try {
const { entryId, destinationId, order } = req.body;
const newRundown = await reorderEntry(entryId, destinationId, order);
res.status(200).send(newRundown);
} catch (error) {
const message = getErrorMessage(error);
res.status(400).send({ message });
}
});
// #endregion operations on project rundowns ======================
router.patch('/swap', rundownSwapValidator, async (req: Request, res: Response<Rundown | ErrorResponse>) => {
try {
const rundown = await swapEvents(req.body.from, req.body.to);
res.status(200).send(rundown);
} catch (error) {
const message = getErrorMessage(error);
res.status(400).send({ message });
}
});
// #region operations on rundown entries ==========================
router.patch('/applydelay/:id', paramsWithId, async (req: Request, res: Response<Rundown | ErrorResponse>) => {
try {
const newRundown = await applyDelay(req.params.id);
res.status(200).send(newRundown);
} catch (error) {
const message = getErrorMessage(error);
res.status(400).send({ message });
}
});
/**
* Creates a new entry in a given rundown
*/
router.post(
'/:rundownId/entry',
entryPostValidator,
validateRundownMutation,
async (req: Request, res: Response<OntimeEntry | ErrorResponse>) => {
try {
const newEvent = await addEntry(req.body);
res.status(201).send(newEvent);
} catch (error) {
const message = getErrorMessage(error);
res.status(400).send({ message });
}
},
);
router.post('/clone/:id', paramsWithId, async (req: Request, res: Response<Rundown | ErrorResponse>) => {
try {
const newRundown = await cloneEntry(req.params.id);
res.status(200).send(newRundown);
} catch (error) {
const message = getErrorMessage(error);
res.status(400).send({ message });
}
});
/**
* Edits an entry in a given rundown
*/
router.put(
'/:rundownId/entry',
entryPutValidator,
validateRundownMutation,
async (req: Request, res: Response<OntimeEntry | ErrorResponse>) => {
try {
const event = await editEntry(req.body);
res.status(200).send(event);
} catch (error) {
const message = getErrorMessage(error);
res.status(400).send({ message });
}
},
);
router.post('/group', rundownArrayOfIds, async (req: Request, res: Response<Rundown | ErrorResponse>) => {
try {
const newRundown = await groupEntries(req.body.ids);
res.status(200).send(newRundown);
} catch (error) {
const message = getErrorMessage(error);
res.status(400).send({ message });
}
});
/**
* Edits an entry in a given rundown
*/
router.put(
'/:rundownId/batch',
entryBatchPutValidator,
validateRundownMutation,
async (req: Request, res: Response<Rundown | ErrorResponse>) => {
try {
const rundown = await batchEditEntries(req.body.ids, req.body.data);
res.status(200).send(rundown);
} catch (error) {
const message = getErrorMessage(error);
res.status(400).send({ message });
}
},
);
router.post('/ungroup/:id', paramsWithId, async (req: Request, res: Response<Rundown | ErrorResponse>) => {
try {
const newRundown = await ungroupEntries(req.params.id);
res.status(200).send(newRundown);
} catch (error) {
const message = getErrorMessage(error);
res.status(400).send({ message });
}
});
/**
* Reorders two entries in a rundown
*/
router.patch(
'/:rundownId/reorder',
entryReorderValidator,
validateRundownMutation,
async (req: Request, res: Response<Rundown | ErrorResponse>) => {
try {
const { entryId, destinationId, order } = req.body;
const rundown = await reorderEntry(entryId, destinationId, order);
res.status(200).send(rundown);
} catch (error) {
const message = getErrorMessage(error);
res.status(400).send({ message });
}
},
);
router.delete('/', rundownArrayOfIds, async (req: Request, res: Response<MessageResponse | ErrorResponse>) => {
try {
await deleteEntries(req.body.ids);
res.status(204).send({ message: 'Events deleted' });
} catch (error) {
const message = getErrorMessage(error);
res.status(400).send({ message });
}
});
/**
* Applies a delay into the schedule
*/
router.patch(
'/:rundownId/applydelay/:id',
paramsWithId,
validateRundownMutation,
async (req: Request, res: Response<Rundown | ErrorResponse>) => {
try {
const rundown = await applyDelay(req.params.id);
res.status(200).send(rundown);
} catch (error) {
const message = getErrorMessage(error);
res.status(400).send({ message });
}
},
);
router.delete('/all', async (_req: Request, res: Response<Rundown | ErrorResponse>) => {
try {
const rundown = await deleteAllEntries();
res.status(204).send(rundown);
} catch (error) {
const message = getErrorMessage(error);
res.status(400).send({ message });
}
});
/**
* Swaps data between two Ontime events
*/
router.patch(
'/:rundownId/swap',
entrySwapValidator,
validateRundownMutation,
async (req: Request, res: Response<Rundown | ErrorResponse>) => {
try {
const rundown = await swapEvents(req.body.from, req.body.to);
res.status(200).send(rundown);
} catch (error) {
const message = getErrorMessage(error);
res.status(400).send({ message });
}
},
);
/**
* Clones the contents of an entry into a new one
*/
router.post(
'/:rundownId/clone/:id',
paramsWithId,
validateRundownMutation,
async (req: Request, res: Response<Rundown | ErrorResponse>) => {
try {
const rundown = await cloneEntry(req.params.id);
res.status(200).send(rundown);
} catch (error) {
const message = getErrorMessage(error);
res.status(400).send({ message });
}
},
);
/**
* Creates a group out of a list of entries
*/
router.post(
'/:rundownId/group',
rundownArrayOfIds,
validateRundownMutation,
async (req: Request, res: Response<Rundown | ErrorResponse>) => {
try {
const rundown = await groupEntries(req.body.ids);
res.status(200).send(rundown);
} catch (error) {
const message = getErrorMessage(error);
res.status(400).send({ message });
}
},
);
/**
* Dissolves a group by moving its children to the main rundown
*/
router.post(
'/:rundownId/ungroup/:id',
paramsWithId,
validateRundownMutation,
async (req: Request, res: Response<Rundown | ErrorResponse>) => {
try {
const rundown = await ungroupEntries(req.params.id);
res.status(200).send(rundown);
} catch (error) {
const message = getErrorMessage(error);
res.status(400).send({ message });
}
},
);
/**
* Deletes a list of entries by their ID
*/
router.delete(
'/:rundownId/entries',
rundownArrayOfIds,
validateRundownMutation,
async (req: Request, res: Response<Rundown | ErrorResponse>) => {
try {
const rundown = await deleteEntries(req.body.ids);
res.status(200).send(rundown);
} catch (error) {
const message = getErrorMessage(error);
res.status(400).send({ message });
}
},
);
/**
* Deletes all entries in a given rundown
*/
router.delete(
'/:rundownId/all',
validateRundownMutation,
async (_req: Request, res: Response<Rundown | ErrorResponse>) => {
try {
const rundown = await deleteAllEntries();
res.status(200).send(rundown);
} catch (error) {
const message = getErrorMessage(error);
res.status(400).send({ message });
}
},
);
// #endregion operations on rundown entries =======================