mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-06 07:53:54 +00:00
fix: send refetch on all rundown edits (#2098)
* fix(server): send refetch on all rundown edits * fix(test): cuesheet tabing * chore: remove unneeded async * refactor: small cleanups * chore: spelling Co-authored-by: Carlos Valente <34649812+cpvalente@users.noreply.github.com> * chore: remove unneded async/await --------- Co-authored-by: Carlos Valente <34649812+cpvalente@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
5dbe7afae8
commit
41a09bf5c3
@@ -2,13 +2,11 @@ import { EndAction, OntimeEvent, TimeStrategy, TimerType } from 'ontime-types';
|
||||
import { MILLIS_PER_HOUR, createEvent } from 'ontime-utils';
|
||||
import { assertType } from 'vitest';
|
||||
|
||||
import { demoDb } from '../../../models/demoProject.js';
|
||||
import { makeOntimeEvent, makeOntimeGroup, makeRundown } from '../__mocks__/rundown.mocks.js';
|
||||
import {
|
||||
calculateDayOffset,
|
||||
deleteById,
|
||||
doesInvalidateMetadata,
|
||||
duplicateRundown,
|
||||
getIntegerAndFraction,
|
||||
hasChanges,
|
||||
makeDeepClone,
|
||||
@@ -223,25 +221,6 @@ describe('calculateDayOffset()', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('duplicateRundown', () => {
|
||||
it('duplicates a given rundown', () => {
|
||||
const demoRundown = demoDb.rundowns['default'];
|
||||
const title = 'Duplicated Rundown';
|
||||
const duplicatedRundown = duplicateRundown(demoRundown, title);
|
||||
|
||||
expect(duplicatedRundown).toMatchObject({
|
||||
title: title,
|
||||
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'] });
|
||||
|
||||
@@ -15,16 +15,18 @@ import {
|
||||
createNewRundown,
|
||||
deleteAllEntries,
|
||||
deleteEntries,
|
||||
deleteRundown,
|
||||
duplicateRundown,
|
||||
editEntry,
|
||||
groupEntries,
|
||||
initRundown,
|
||||
loadRundown,
|
||||
renameRundown,
|
||||
renumberEntries,
|
||||
reorderEntry,
|
||||
swapEvents,
|
||||
ungroupEntries,
|
||||
} from './rundown.service.js';
|
||||
import { duplicateRundown, normalisedToRundownArray } from './rundown.utils.js';
|
||||
import { normalisedToRundownArray } from './rundown.utils.js';
|
||||
import {
|
||||
clonePostValidator,
|
||||
entryBatchPutValidator,
|
||||
@@ -34,6 +36,7 @@ import {
|
||||
entryReorderValidator,
|
||||
entrySwapValidator,
|
||||
rundownArrayOfIds,
|
||||
rundownPatchValidator,
|
||||
rundownPostValidator,
|
||||
} from './rundown.validation.js';
|
||||
|
||||
@@ -44,7 +47,7 @@ export const router: Router = express.Router();
|
||||
/**
|
||||
* Returns all rundowns in the project
|
||||
*/
|
||||
router.get('/', async (_req: Request, res: Response<ProjectRundownsList>) => {
|
||||
router.get('/', (_req: Request, res: Response<ProjectRundownsList>) => {
|
||||
const projectRundowns = getDataProvider().getProjectRundowns();
|
||||
res.json({ loaded: getCurrentRundown().id, rundowns: normalisedToRundownArray(projectRundowns) });
|
||||
});
|
||||
@@ -52,7 +55,7 @@ router.get('/', async (_req: Request, res: Response<ProjectRundownsList>) => {
|
||||
/**
|
||||
* Returns the current rundown
|
||||
*/
|
||||
router.get('/current', async (_req: Request, res: Response<Rundown>) => {
|
||||
router.get('/current', (_req: Request, res: Response<Rundown>) => {
|
||||
const rundown = getCurrentRundown();
|
||||
res.json(rundown);
|
||||
});
|
||||
@@ -60,7 +63,7 @@ router.get('/current', async (_req: Request, res: Response<Rundown>) => {
|
||||
/**
|
||||
* Returns a given rundown in its normalised client shape
|
||||
*/
|
||||
router.get('/:id', paramsWithId, async (req: Request, res: Response<Rundown | ErrorResponse>) => {
|
||||
router.get('/:id', paramsWithId, (req: Request, res: Response<Rundown | ErrorResponse>) => {
|
||||
try {
|
||||
const rundown = getProcessedRundown(req.params.id);
|
||||
res.json(rundown);
|
||||
@@ -104,13 +107,7 @@ router.post(
|
||||
paramsWithId,
|
||||
async (req: Request, res: Response<ProjectRundownsList | ErrorResponse>) => {
|
||||
try {
|
||||
const dataProvider = getDataProvider();
|
||||
const rundown = dataProvider.getRundown(req.params.id);
|
||||
|
||||
const duplicatedRundown: Rundown = duplicateRundown(rundown, `Copy of ${rundown.title}`);
|
||||
await dataProvider.setRundown(duplicatedRundown.id, duplicatedRundown);
|
||||
|
||||
const projectRundowns = getDataProvider().getProjectRundowns();
|
||||
const projectRundowns = await duplicateRundown(req.params.id);
|
||||
res.status(201).json({ loaded: getCurrentRundown().id, rundowns: normalisedToRundownArray(projectRundowns) });
|
||||
} catch (error) {
|
||||
const message = getErrorMessage(error);
|
||||
@@ -123,54 +120,26 @@ router.post(
|
||||
* Patches the data of an existing rundown
|
||||
* Currently only the title can be changed
|
||||
*/
|
||||
router.patch('/:id', paramsWithId, async (req: Request, res: Response<ProjectRundownsList | ErrorResponse>) => {
|
||||
try {
|
||||
const dataProvider = getDataProvider();
|
||||
const rundown = dataProvider.getRundown(req.params.id);
|
||||
if (!rundown) throw new Error(`Rundown with ID ${req.params.id} not found`);
|
||||
if (!req.body.title) throw new Error('No title provided');
|
||||
|
||||
await dataProvider.setRundown(rundown.id, { ...rundown, title: req.body.title });
|
||||
|
||||
/**
|
||||
* If loaded we re-init the rundown
|
||||
* This is likely over-kill but the simplest way to ensure state consistency
|
||||
*/
|
||||
if (req.params.id === getCurrentRundown().id) {
|
||||
const rundown = dataProvider.getRundown(req.params.id);
|
||||
const customField = dataProvider.getCustomFields();
|
||||
await initRundown(rundown, customField);
|
||||
router.patch(
|
||||
'/:id',
|
||||
rundownPatchValidator,
|
||||
async (req: Request, res: Response<ProjectRundownsList | ErrorResponse>) => {
|
||||
try {
|
||||
const projectRundowns = await renameRundown(req.params.id, req.body.title);
|
||||
res.status(200).json({ loaded: getCurrentRundown().id, rundowns: normalisedToRundownArray(projectRundowns) });
|
||||
} catch (error) {
|
||||
const message = getErrorMessage(error);
|
||||
res.status(400).send({ message });
|
||||
}
|
||||
|
||||
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 });
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* Deletes a rundown if not loaded
|
||||
*/
|
||||
router.delete('/:id', paramsWithId, async (req: Request, res: Response<ProjectRundownsList | ErrorResponse>) => {
|
||||
try {
|
||||
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();
|
||||
const newProjectRundowns = await deleteRundown(req.params.id);
|
||||
res.status(200).json({ loaded: getCurrentRundown().id, rundowns: normalisedToRundownArray(newProjectRundowns) });
|
||||
} catch (error) {
|
||||
const message = getErrorMessage(error);
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
isOntimeEvent,
|
||||
isOntimeGroup,
|
||||
} from 'ontime-types';
|
||||
import { customFieldLabelToKey, getInsertAfterId, resolveInsertParent } from 'ontime-utils';
|
||||
import { customFieldLabelToKey, generateId, getInsertAfterId, resolveInsertParent } from 'ontime-utils';
|
||||
|
||||
import { sendRefetch } from '../../adapters/WebsocketAdapter.js';
|
||||
import { getDataProvider } from '../../classes/data-provider/DataProvider.js';
|
||||
@@ -643,7 +643,7 @@ export function isCurrentRundown(id: string) {
|
||||
/**
|
||||
* @throws if the provided id does not exist
|
||||
*/
|
||||
export async function loadRundown(id: string) {
|
||||
export function loadRundown(id: string) {
|
||||
const dataProvider = getDataProvider();
|
||||
if (isCurrentRundown(id)) {
|
||||
return dataProvider.getProjectRundowns();
|
||||
@@ -651,7 +651,7 @@ export async function loadRundown(id: string) {
|
||||
|
||||
const rundown = dataProvider.getRundown(id);
|
||||
const customField = dataProvider.getCustomFields();
|
||||
await initRundown(rundown, customField);
|
||||
initRundown(rundown, customField);
|
||||
return dataProvider.getProjectRundowns();
|
||||
}
|
||||
|
||||
@@ -659,11 +659,7 @@ export async function loadRundown(id: string) {
|
||||
* Sets a new rundown in the cache
|
||||
* and marks it as the currently loaded one
|
||||
*/
|
||||
export async function initRundown(
|
||||
rundown: Readonly<Rundown>,
|
||||
customFields: Readonly<CustomFields>,
|
||||
reload: boolean = false,
|
||||
) {
|
||||
export function initRundown(rundown: Readonly<Rundown>, customFields: Readonly<CustomFields>, reload: boolean = false) {
|
||||
runtimeService.stop();
|
||||
const { rundownMetadata, revision } = rundownCache.init(rundown, customFields);
|
||||
logger.info(LogOrigin.Server, `Switch to rundown: ${rundown.id}`);
|
||||
@@ -692,3 +688,73 @@ export async function createNewRundown(title: string) {
|
||||
|
||||
return projectRundowns;
|
||||
}
|
||||
|
||||
/**
|
||||
* duplicate a rundown
|
||||
* @throws
|
||||
*/
|
||||
export async function duplicateRundown(id: string) {
|
||||
const dataProvider = getDataProvider();
|
||||
const rundown = dataProvider.getRundown(id);
|
||||
|
||||
const newRundownId = generateId();
|
||||
const newRundown: Rundown = structuredClone(rundown);
|
||||
newRundown.id = newRundownId;
|
||||
newRundown.title = `Copy of ${rundown.title}`;
|
||||
newRundown.revision = 0;
|
||||
|
||||
const newProjectRundowns = await dataProvider.setRundown(newRundownId, newRundown);
|
||||
|
||||
setImmediate(() => {
|
||||
sendRefetch(RefetchKey.ProjectRundowns);
|
||||
});
|
||||
|
||||
return newProjectRundowns;
|
||||
}
|
||||
|
||||
/**
|
||||
* rename a rundown
|
||||
* @throws
|
||||
*/
|
||||
export async function renameRundown(id: string, title: string) {
|
||||
const dataProvider = getDataProvider();
|
||||
const rundown = dataProvider.getRundown(id);
|
||||
const newProjectRundowns = await dataProvider.setRundown(rundown.id, { ...rundown, title });
|
||||
|
||||
/**
|
||||
* If we are modifying the loaded rundown we re-init it
|
||||
* This is likely over-kill but the simplest way to ensure state consistency
|
||||
*/
|
||||
if (isCurrentRundown(id)) {
|
||||
const rundown = dataProvider.getRundown(id);
|
||||
const customField = dataProvider.getCustomFields();
|
||||
initRundown(rundown, customField);
|
||||
} else {
|
||||
setImmediate(() => {
|
||||
sendRefetch(RefetchKey.ProjectRundowns);
|
||||
});
|
||||
}
|
||||
|
||||
return newProjectRundowns;
|
||||
}
|
||||
|
||||
/**
|
||||
* delete a rundown
|
||||
* @throws
|
||||
*/
|
||||
export async function deleteRundown(id: string) {
|
||||
if (isCurrentRundown(id)) throw new Error('Cannot delete loaded rundown');
|
||||
|
||||
const dataProvider = getDataProvider();
|
||||
const projectRundowns = dataProvider.getProjectRundowns();
|
||||
|
||||
// might never hit this as it is likely covered by the case of trying to delete the loaded rundown
|
||||
if (Object.keys(projectRundowns).length <= 1) throw new Error('Cannot delete the last rundown');
|
||||
const newProjectRundowns = await dataProvider.deleteRundown(id);
|
||||
|
||||
setImmediate(() => {
|
||||
sendRefetch(RefetchKey.ProjectRundowns);
|
||||
});
|
||||
|
||||
return newProjectRundowns;
|
||||
}
|
||||
|
||||
@@ -460,20 +460,6 @@ export function normalisedToRundownArray(rundowns: ProjectRundowns): ProjectRund
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Duplicates an existing rundown ensuring all IDs are unique
|
||||
*/
|
||||
export function duplicateRundown(rundown: Rundown, newTitle: string): Rundown {
|
||||
const newRundownId = generateId();
|
||||
|
||||
const newRundown = structuredClone(rundown);
|
||||
newRundown.id = newRundownId;
|
||||
newRundown.title = newTitle;
|
||||
newRundown.revision = 0;
|
||||
|
||||
return newRundown;
|
||||
}
|
||||
|
||||
export type IncrementNumber = {
|
||||
integer: number;
|
||||
faction: number;
|
||||
|
||||
@@ -5,6 +5,11 @@ import { requestValidationFunction } from '../validation-utils/validationFunctio
|
||||
// #region operations on project rundowns =========================
|
||||
|
||||
export const rundownPostValidator = [body('title').isString().trim().notEmpty(), requestValidationFunction];
|
||||
export const rundownPatchValidator = [
|
||||
param('id').isString().trim().notEmpty(),
|
||||
body('title').isString().trim().notEmpty().withMessage('No title provided'),
|
||||
requestValidationFunction,
|
||||
];
|
||||
|
||||
// #endregion operations on project rundowns ======================
|
||||
// #region operations on rundown entries ==========================
|
||||
|
||||
@@ -101,9 +101,10 @@ function getCustomFields(): Readonly<CustomFields> {
|
||||
return db.data.customFields;
|
||||
}
|
||||
|
||||
async function setRundown(rundownKey: string, newData: Rundown): Promise<void> {
|
||||
async function setRundown(rundownKey: string, newData: Rundown): ReadonlyPromise<ProjectRundowns> {
|
||||
db.data.rundowns[rundownKey] = structuredClone(newData);
|
||||
await persist();
|
||||
return db.data.rundowns;
|
||||
}
|
||||
|
||||
function getSettings(): Readonly<Settings> {
|
||||
|
||||
@@ -30,6 +30,8 @@ test('cuesheet datagrid does not submit timer cells on tab-out or escape', async
|
||||
|
||||
// re-enter edit mode: original value should be unchanged
|
||||
await durationCell.click();
|
||||
// tabbing selects the next input field so we have to click twice to first leave input field and then select
|
||||
await durationCell.click();
|
||||
await expect(durationCell.locator('input')).toHaveValue(originalDuration);
|
||||
await durationCell.locator('input').press('Escape');
|
||||
|
||||
|
||||
Reference in New Issue
Block a user