mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-15 04:13:47 +00:00
feat: excel import (#527)
* refactor: simplify excel import * chore: add external deepmerge utility * chore: create import map utilities * chore: increase size limits on uploads * style: small presentation tweaks * feat: resolve times on excel import, refs #508
This commit is contained in:
+2
@@ -0,0 +1,2 @@
|
||||
// evaluating the library, we re-export to make it easy to detach
|
||||
export { deepmerge } from 'deepmerge-ts';
|
||||
@@ -0,0 +1,44 @@
|
||||
export type ExcelImportOptions = keyof typeof defaultExcelImportMap;
|
||||
export type ExcelImportMap = typeof defaultExcelImportMap;
|
||||
|
||||
export const defaultExcelImportMap = {
|
||||
worksheet: 'event schedule',
|
||||
projectName: 'project name',
|
||||
projectDescription: 'project description',
|
||||
publicUrl: 'public url',
|
||||
publicInfo: 'public info',
|
||||
backstageUrl: 'backstage url',
|
||||
backstageInfo: 'backstage info',
|
||||
timeStart: 'time start',
|
||||
timeEnd: 'time end',
|
||||
duration: 'duration',
|
||||
cue: 'cue',
|
||||
title: 'title',
|
||||
presenter: 'presenter',
|
||||
subtitle: 'subtitle',
|
||||
isPublic: 'public',
|
||||
skip: 'skip',
|
||||
note: 'notes',
|
||||
colour: 'colour',
|
||||
endAction: 'end action',
|
||||
timerType: 'timer type',
|
||||
user0: 'user0',
|
||||
user1: 'user1',
|
||||
user2: 'user2',
|
||||
user3: 'user3',
|
||||
user4: 'user4',
|
||||
user5: 'user5',
|
||||
user6: 'user6',
|
||||
user7: 'user7',
|
||||
user8: 'user8',
|
||||
user9: 'user9',
|
||||
};
|
||||
|
||||
export function isExcelImportMap(obj: unknown): obj is ExcelImportMap {
|
||||
if (typeof obj !== 'object' || obj === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const keys = Object.keys(obj);
|
||||
return keys.every((key) => Object.hasOwn(defaultExcelImportMap, key));
|
||||
}
|
||||
@@ -1,8 +1,6 @@
|
||||
import { OntimeRundown, SupportedEvent } from 'ontime-types';
|
||||
|
||||
import { dayInMs } from '../timeConstants.js';
|
||||
import { getNextEvent, getPreviousEvent } from './rundownUtils';
|
||||
import { calculateDuration } from './rundownUtils.js';
|
||||
|
||||
describe('getNextEvent()', () => {
|
||||
it('returns the next event of type event', () => {
|
||||
@@ -71,25 +69,3 @@ describe('getPreviousEvent()', () => {
|
||||
expect(previous).toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('calculateDuration()', () => {
|
||||
describe('Given start and end values', () => {
|
||||
it('is the difference between end and start', () => {
|
||||
const duration = calculateDuration(10, 20);
|
||||
expect(duration).toBe(10);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Handles edge cases', () => {
|
||||
it('handles events that go over midnight', () => {
|
||||
const duration = calculateDuration(51, 50);
|
||||
expect(duration).toBe(dayInMs - 1);
|
||||
});
|
||||
it('handles no difference', () => {
|
||||
const duration1 = calculateDuration(0, 0);
|
||||
const duration2 = calculateDuration(dayInMs, dayInMs);
|
||||
expect(duration1).toBe(0);
|
||||
expect(duration2).toBe(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { isOntimeEvent, OntimeEvent, OntimeRundown, OntimeRundownEntry } from 'ontime-types';
|
||||
|
||||
import { dayInMs } from '../timeConstants.js';
|
||||
|
||||
/**
|
||||
* Gets first event in rundown, if it exists
|
||||
* @param {OntimeRundownEntry[]} rundown
|
||||
@@ -113,20 +111,6 @@ export function getPreviousEvent(rundown: OntimeRundownEntry[], currentId: strin
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description calculates event duration considering midnight
|
||||
* @param {number} timeStart
|
||||
* @param {number} timeEnd
|
||||
* @returns {number}
|
||||
*/
|
||||
export const calculateDuration = (timeStart: number, timeEnd: number): number => {
|
||||
// Durations must be positive
|
||||
if (timeEnd < timeStart) {
|
||||
return timeEnd + dayInMs - timeStart;
|
||||
}
|
||||
return timeEnd - timeStart;
|
||||
};
|
||||
|
||||
/**
|
||||
* @description swaps two OntimeEvents in the rundown
|
||||
* @param {OntimeRundown} rundown
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
import { EndAction, TimerType } from 'ontime-types';
|
||||
import { expect } from 'vitest';
|
||||
|
||||
import { dayInMs } from '../timeConstants.js';
|
||||
import { calculateDuration, validateEndAction, validateTimerType, validateTimes } from './validateEvent.js';
|
||||
|
||||
describe('validateEndAction()', () => {
|
||||
it('recognises a string representation of an action', () => {
|
||||
const endAction = validateEndAction('load-next');
|
||||
expect(endAction).toBe(EndAction.LoadNext);
|
||||
});
|
||||
it('returns fallback otherwise', () => {
|
||||
const emptyAction = validateEndAction('', EndAction.Stop);
|
||||
const invalidAction = validateEndAction('this-does-not-exist', EndAction.PlayNext);
|
||||
expect(emptyAction).toBe(EndAction.Stop);
|
||||
expect(invalidAction).toBe(EndAction.PlayNext);
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateTimerType()', () => {
|
||||
it('recognises a string representation of an action', () => {
|
||||
const timerType = validateTimerType('time-to-end');
|
||||
expect(timerType).toBe(TimerType.TimeToEnd);
|
||||
});
|
||||
it('returns fallback otherwise', () => {
|
||||
const emptyType = validateTimerType('', TimerType.Clock);
|
||||
const invalidType = validateTimerType('this-does-not-exist', TimerType.CountDown);
|
||||
expect(emptyType).toBe(TimerType.Clock);
|
||||
expect(invalidType).toBe(TimerType.CountDown);
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateTimes()', () => {
|
||||
it('passes through a well defined time list', () => {
|
||||
const { timeStart, timeEnd, duration } = validateTimes(5, 10, 5);
|
||||
expect(timeStart).toBe(5);
|
||||
expect(timeEnd).toBe(10);
|
||||
expect(duration).toBe(5);
|
||||
});
|
||||
|
||||
it('handles cases when no times are given', () => {
|
||||
const { timeStart, timeEnd, duration } = validateTimes(null, undefined, null);
|
||||
expect(timeStart).toBe(0);
|
||||
expect(timeEnd).toBe(0);
|
||||
expect(duration).toBe(0);
|
||||
});
|
||||
|
||||
it('calculates duration', () => {
|
||||
const { timeStart, timeEnd, duration } = validateTimes(5, 10);
|
||||
expect(timeStart).toBe(5);
|
||||
expect(timeEnd).toBe(10);
|
||||
expect(duration).toBe(5);
|
||||
});
|
||||
|
||||
it('calculates end time', () => {
|
||||
const { timeStart, timeEnd, duration } = validateTimes(5, undefined, 10);
|
||||
expect(timeStart).toBe(5);
|
||||
expect(timeEnd).toBe(15);
|
||||
expect(duration).toBe(10);
|
||||
});
|
||||
|
||||
it('handles events that finish the day after', () => {
|
||||
const { timeStart, timeEnd, duration } = validateTimes(100, 10);
|
||||
expect(timeStart).toBe(100);
|
||||
expect(timeEnd).toBe(10);
|
||||
expect(duration).toBe(dayInMs - 90);
|
||||
});
|
||||
|
||||
it('corrects time in case of conflicts', () => {
|
||||
const { timeStart, timeEnd, duration } = validateTimes(5, 15, 15);
|
||||
expect(timeStart).toBe(5);
|
||||
expect(timeEnd).toBe(15);
|
||||
expect(duration).toBe(10);
|
||||
});
|
||||
|
||||
it('calculates start time', () => {
|
||||
const { timeStart, timeEnd, duration } = validateTimes(undefined, 15, 10);
|
||||
expect(timeStart).toBe(5);
|
||||
expect(timeEnd).toBe(15);
|
||||
expect(duration).toBe(10);
|
||||
});
|
||||
|
||||
it('calculates start and end time', () => {
|
||||
const { timeStart, timeEnd, duration } = validateTimes(undefined, undefined, 10);
|
||||
expect(timeStart).toBe(0);
|
||||
expect(timeEnd).toBe(10);
|
||||
expect(duration).toBe(10);
|
||||
});
|
||||
|
||||
it('ensures values are integers', () => {
|
||||
const { timeStart, timeEnd, duration } = validateTimes(0.000001, 10.312335342, 10);
|
||||
expect(timeStart).toBe(0);
|
||||
expect(timeEnd).toBe(10);
|
||||
expect(duration).toBe(10);
|
||||
});
|
||||
});
|
||||
|
||||
describe('calculateDuration()', () => {
|
||||
describe('Given start and end values', () => {
|
||||
it('is the difference between end and start', () => {
|
||||
const duration = calculateDuration(10, 20);
|
||||
expect(duration).toBe(10);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Handles edge cases', () => {
|
||||
it('handles events that go over midnight', () => {
|
||||
const duration = calculateDuration(51, 50);
|
||||
expect(duration).toBe(dayInMs - 1);
|
||||
});
|
||||
it('handles no difference', () => {
|
||||
const duration1 = calculateDuration(0, 0);
|
||||
const duration2 = calculateDuration(dayInMs, dayInMs);
|
||||
expect(duration1).toBe(0);
|
||||
expect(duration2).toBe(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,85 @@
|
||||
import { EndAction, TimerType } from 'ontime-types';
|
||||
|
||||
import { dayInMs } from '../timeConstants.js';
|
||||
|
||||
/**
|
||||
* Checks if given value is a valid type of EndAction, returns the fallback otherwise
|
||||
* @param {EndAction} maybeAction
|
||||
* @param {EndAction} [fallback]
|
||||
*/
|
||||
export function validateEndAction(maybeAction: unknown, fallback = EndAction.None) {
|
||||
return Object.values(EndAction).includes(maybeAction as any) ? (maybeAction as EndAction) : fallback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if given value is a valid type of TimerType, returns the fallback otherwise
|
||||
* @param {TimerType} maybeTimerType
|
||||
* @param {TimerType} [fallback]
|
||||
*/
|
||||
export function validateTimerType(maybeTimerType: unknown, fallback = TimerType.CountDown) {
|
||||
return Object.values(TimerType).includes(maybeTimerType as any) ? (maybeTimerType as TimerType) : fallback;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description calculates event duration considering midnight
|
||||
* @param {number} timeStart
|
||||
* @param {number} timeEnd
|
||||
* @returns {number}
|
||||
*/
|
||||
export const calculateDuration = (timeStart: number, timeEnd: number): number => {
|
||||
// Durations must be positive
|
||||
if (timeEnd < timeStart) {
|
||||
return timeEnd + dayInMs - timeStart;
|
||||
}
|
||||
return timeEnd - timeStart;
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts a given value to an int, returns 0 otherwise
|
||||
* @param value
|
||||
* number
|
||||
*/
|
||||
function convertToInteger(value: unknown): number {
|
||||
const result = Number(value);
|
||||
return isNaN(result) ? 0 : Math.floor(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures the time input variables are valid in relationship to each other
|
||||
* Infers values if necessary
|
||||
* @param _start
|
||||
* @param _end
|
||||
* @param _duration
|
||||
*/
|
||||
export function validateTimes(_start?: unknown, _end?: unknown, _duration?: unknown) {
|
||||
const timeStart = convertToInteger(_start);
|
||||
const timeEnd = convertToInteger(_end);
|
||||
const duration = convertToInteger(_duration);
|
||||
|
||||
if (_start != null && _end != null) {
|
||||
// Case 1. if we have start and end, duration must be derived
|
||||
return { timeStart, duration: calculateDuration(timeStart, timeEnd), timeEnd };
|
||||
}
|
||||
|
||||
if (_start == null && _end == null) {
|
||||
if (_duration == null) {
|
||||
// Case 2. no valid times were given
|
||||
return { timeStart, duration, timeEnd };
|
||||
}
|
||||
// Case 3. we have a duration and infer the rest
|
||||
return { timeStart, duration, timeEnd: duration };
|
||||
}
|
||||
|
||||
if (_start != null) {
|
||||
// Case 5. with only start, we can calculate the rest
|
||||
return { timeStart, duration, timeEnd: timeStart + duration };
|
||||
}
|
||||
|
||||
if (_end != null) {
|
||||
// Case 6. with only end, we can calculate the rest
|
||||
return { timeStart: timeEnd - duration, duration, timeEnd };
|
||||
}
|
||||
|
||||
// we should have covered all cases
|
||||
return { timeStart, duration, timeEnd };
|
||||
}
|
||||
Reference in New Issue
Block a user