mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-16 12:53:32 +00:00
feat: resolve times on excel import, refs #508
This commit is contained in:
@@ -1,14 +1,23 @@
|
|||||||
import {
|
import {
|
||||||
generateId,
|
generateId,
|
||||||
calculateDuration,
|
|
||||||
isExcelImportMap,
|
isExcelImportMap,
|
||||||
type ExcelImportMap,
|
type ExcelImportMap,
|
||||||
defaultExcelImportMap,
|
defaultExcelImportMap,
|
||||||
validateEndAction,
|
validateEndAction,
|
||||||
validateTimerType,
|
validateTimerType,
|
||||||
type ExcelImportOptions,
|
type ExcelImportOptions,
|
||||||
|
validateTimes,
|
||||||
} from 'ontime-utils';
|
} from 'ontime-utils';
|
||||||
import { DatabaseModel, OntimeEvent, OntimeRundown, SupportedEvent, ProjectData, UserFields } from 'ontime-types';
|
import {
|
||||||
|
DatabaseModel,
|
||||||
|
OntimeEvent,
|
||||||
|
OntimeRundown,
|
||||||
|
SupportedEvent,
|
||||||
|
ProjectData,
|
||||||
|
UserFields,
|
||||||
|
EndAction,
|
||||||
|
TimerType,
|
||||||
|
} from 'ontime-types';
|
||||||
|
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import xlsx from 'node-xlsx';
|
import xlsx from 'node-xlsx';
|
||||||
@@ -73,9 +82,9 @@ export const parseExcel = (excelData: unknown[][], options?: Partial<ExcelImport
|
|||||||
let skipIndex: number | null = null;
|
let skipIndex: number | null = null;
|
||||||
|
|
||||||
// times: numbers
|
// times: numbers
|
||||||
// TODO: handle duration
|
|
||||||
let timeStartIndex: number | null = null;
|
let timeStartIndex: number | null = null;
|
||||||
let timeEndIndex: number | null = null;
|
let timeEndIndex: number | null = null;
|
||||||
|
let durationIndex: number | null = null;
|
||||||
|
|
||||||
// options: enum properties
|
// options: enum properties
|
||||||
let endActionIndex: number | null = null;
|
let endActionIndex: number | null = null;
|
||||||
@@ -130,6 +139,8 @@ export const parseExcel = (excelData: unknown[][], options?: Partial<ExcelImport
|
|||||||
event.timeStart = parseExcelDate(column);
|
event.timeStart = parseExcelDate(column);
|
||||||
} else if (j === timeEndIndex) {
|
} else if (j === timeEndIndex) {
|
||||||
event.timeEnd = parseExcelDate(column);
|
event.timeEnd = parseExcelDate(column);
|
||||||
|
} else if (j === durationIndex) {
|
||||||
|
event.duration = parseExcelDate(column);
|
||||||
} else if (j === titleIndex) {
|
} else if (j === titleIndex) {
|
||||||
event.title = makeString(column, '');
|
event.title = makeString(column, '');
|
||||||
} else if (j === cueIndex) {
|
} else if (j === cueIndex) {
|
||||||
@@ -202,6 +213,9 @@ export const parseExcel = (excelData: unknown[][], options?: Partial<ExcelImport
|
|||||||
case importMap.timeEnd:
|
case importMap.timeEnd:
|
||||||
timeEndIndex = j;
|
timeEndIndex = j;
|
||||||
break;
|
break;
|
||||||
|
case importMap.duration:
|
||||||
|
durationIndex = j;
|
||||||
|
break;
|
||||||
case importMap.cue:
|
case importMap.cue:
|
||||||
cueIndex = j;
|
cueIndex = j;
|
||||||
break;
|
break;
|
||||||
@@ -273,10 +287,10 @@ export const parseExcel = (excelData: unknown[][], options?: Partial<ExcelImport
|
|||||||
|
|
||||||
if (Object.keys(event).length > 0) {
|
if (Object.keys(event).length > 0) {
|
||||||
// if any data was found, push to array
|
// if any data was found, push to array
|
||||||
// take care of it in the next step
|
|
||||||
rundown.push({ ...event, type: SupportedEvent.Event } as OntimeEvent);
|
rundown.push({ ...event, type: SupportedEvent.Event } as OntimeEvent);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
rundown,
|
rundown,
|
||||||
project: projectData,
|
project: projectData,
|
||||||
@@ -344,16 +358,18 @@ export const validateEvent = (eventArgs: Partial<OntimeEvent>, cueFallback: stri
|
|||||||
const start = e.timeStart != null && typeof e.timeStart === 'number' ? e.timeStart : d.timeStart;
|
const start = e.timeStart != null && typeof e.timeStart === 'number' ? e.timeStart : d.timeStart;
|
||||||
const end = e.timeEnd != null && typeof e.timeEnd === 'number' ? e.timeEnd : d.timeEnd;
|
const end = e.timeEnd != null && typeof e.timeEnd === 'number' ? e.timeEnd : d.timeEnd;
|
||||||
|
|
||||||
|
const { timeStart, timeEnd, duration } = validateTimes(start, end, e.duration);
|
||||||
|
|
||||||
event = {
|
event = {
|
||||||
...d,
|
...d,
|
||||||
title: makeString(e.title, d.title),
|
title: makeString(e.title, d.title),
|
||||||
subtitle: makeString(e.subtitle, d.subtitle),
|
subtitle: makeString(e.subtitle, d.subtitle),
|
||||||
presenter: makeString(e.presenter, d.presenter),
|
presenter: makeString(e.presenter, d.presenter),
|
||||||
timeStart: start,
|
timeStart,
|
||||||
timeEnd: end,
|
timeEnd,
|
||||||
endAction: makeString(e.endAction, d.endAction),
|
duration,
|
||||||
timerType: makeString(e.timerType, d.timerType),
|
endAction: validateEndAction(e.endAction, EndAction.None),
|
||||||
duration: calculateDuration(start, end),
|
timerType: validateTimerType(e.timerType, TimerType.CountDown),
|
||||||
isPublic: typeof e.isPublic === 'boolean' ? e.isPublic : d.isPublic,
|
isPublic: typeof e.isPublic === 'boolean' ? e.isPublic : d.isPublic,
|
||||||
skip: typeof e.skip === 'boolean' ? e.skip : d.skip,
|
skip: typeof e.skip === 'boolean' ? e.skip : d.skip,
|
||||||
note: makeString(e.note, d.note),
|
note: makeString(e.note, d.note),
|
||||||
@@ -368,8 +384,8 @@ export const validateEvent = (eventArgs: Partial<OntimeEvent>, cueFallback: stri
|
|||||||
user8: makeString(e.user8, d.user8),
|
user8: makeString(e.user8, d.user8),
|
||||||
user9: makeString(e.user9, d.user9),
|
user9: makeString(e.user9, d.user9),
|
||||||
colour: makeString(e.colour, d.colour),
|
colour: makeString(e.colour, d.colour),
|
||||||
id,
|
|
||||||
cue: makeString(e.cue, cueFallback),
|
cue: makeString(e.cue, cueFallback),
|
||||||
|
id,
|
||||||
type: 'event',
|
type: 'event',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { generateId } from 'ontime-utils';
|
import { generateId } from 'ontime-utils';
|
||||||
import {
|
import {
|
||||||
Alias,
|
Alias,
|
||||||
EndAction,
|
|
||||||
OntimeRundown,
|
OntimeRundown,
|
||||||
OSCSettings,
|
OSCSettings,
|
||||||
OscSubscription,
|
OscSubscription,
|
||||||
@@ -9,7 +8,6 @@ import {
|
|||||||
ProjectData,
|
ProjectData,
|
||||||
Settings,
|
Settings,
|
||||||
TimerLifeCycle,
|
TimerLifeCycle,
|
||||||
TimerType,
|
|
||||||
UserFields,
|
UserFields,
|
||||||
ViewSettings,
|
ViewSettings,
|
||||||
} from 'ontime-types';
|
} from 'ontime-types';
|
||||||
@@ -45,18 +43,6 @@ export const parseRundown = (data): OntimeRundown => {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// validate the right endAction is used
|
|
||||||
if (e.endAction && !Object.values(EndAction).includes(e.endAction)) {
|
|
||||||
e.endAction = EndAction.None;
|
|
||||||
console.log('WARNING: invalid End Action provided, using default');
|
|
||||||
}
|
|
||||||
|
|
||||||
// validate the right timerType is used
|
|
||||||
if (e.timerType && !Object.values(TimerType).includes(e.timerType)) {
|
|
||||||
e.timerType = TimerType.CountDown;
|
|
||||||
console.log('WARNING: invalid Timer Type provided, using default');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (e.type === 'event') {
|
if (e.type === 'event') {
|
||||||
eventIndex += 1;
|
eventIndex += 1;
|
||||||
const event = validateEvent(e, eventIndex.toString());
|
const event = validateEvent(e, eventIndex.toString());
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
// runtime utils
|
// runtime utils
|
||||||
export { getFirst, getFirstEvent, getLastEvent, getNext, getPrevious } from './src/rundown-utils/rundownUtils.js';
|
export { getFirst, getFirstEvent, getLastEvent, getNext, getPrevious } from './src/rundown-utils/rundownUtils.js';
|
||||||
export { validatePlayback } from './src/validate-action/validatePlayback.js';
|
export { validatePlayback } from './src/validate-action/validatePlayback.js';
|
||||||
|
export { validateTimes } from './src/validate-events/validateEvent.js';
|
||||||
|
export { calculateDuration } from './src/validate-events/validateEvent.js';
|
||||||
|
|
||||||
// rundown utils
|
// rundown utils
|
||||||
export { sanitiseCue } from './src/cue-utils/cueUtils.js';
|
export { sanitiseCue } from './src/cue-utils/cueUtils.js';
|
||||||
export { getCueCandidate } from './src/cue-utils/cueUtils.js';
|
export { getCueCandidate } from './src/cue-utils/cueUtils.js';
|
||||||
export { generateId } from './src/generate-id/generateId.js';
|
export { generateId } from './src/generate-id/generateId.js';
|
||||||
export { calculateDuration } from './src/rundown-utils/rundownUtils.js';
|
|
||||||
export { swapOntimeEvents } from './src/rundown-utils/rundownUtils.js';
|
export { swapOntimeEvents } from './src/rundown-utils/rundownUtils.js';
|
||||||
|
|
||||||
// format utils
|
// format utils
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import { isOntimeEvent, OntimeEvent, OntimeRundown, OntimeRundownEntry } from 'ontime-types';
|
import { isOntimeEvent, OntimeEvent, OntimeRundown, OntimeRundownEntry } from 'ontime-types';
|
||||||
|
|
||||||
import { dayInMs } from '../timeConstants.js';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets first event in rundown, if it exists
|
* Gets first event in rundown, if it exists
|
||||||
* @param {OntimeRundownEntry[]} rundown
|
* @param {OntimeRundownEntry[]} rundown
|
||||||
@@ -113,20 +111,6 @@ export function getPreviousEvent(rundown: OntimeRundownEntry[], currentId: strin
|
|||||||
return null;
|
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
|
* @description swaps two OntimeEvents in the rundown
|
||||||
* @param {OntimeRundown} rundown
|
* @param {OntimeRundown} rundown
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { EndAction, TimerType } from 'ontime-types';
|
import { EndAction, TimerType } from 'ontime-types';
|
||||||
import { expect } from 'vitest';
|
import { expect } from 'vitest';
|
||||||
|
|
||||||
import { validateEndAction, validateTimerType } from './validateEvent';
|
import { dayInMs } from '../timeConstants.js';
|
||||||
|
import { validateEndAction, validateTimerType, validateTimes } from './validateEvent.js';
|
||||||
|
|
||||||
describe('validateEndAction()', () => {
|
describe('validateEndAction()', () => {
|
||||||
it('recognises a string representation of an action', () => {
|
it('recognises a string representation of an action', () => {
|
||||||
@@ -28,3 +29,61 @@ describe('validateTimerType()', () => {
|
|||||||
expect(invalidType).toBe(TimerType.CountDown);
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { EndAction, TimerType } from 'ontime-types';
|
import { EndAction, TimerType } from 'ontime-types';
|
||||||
|
|
||||||
|
import { dayInMs } from '../timeConstants.js';
|
||||||
|
|
||||||
export function validateEndAction(maybeAction: unknown, fallback = EndAction.None) {
|
export function validateEndAction(maybeAction: unknown, fallback = EndAction.None) {
|
||||||
if (typeof maybeAction !== 'string') {
|
if (typeof maybeAction !== 'string') {
|
||||||
return fallback;
|
return fallback;
|
||||||
@@ -23,3 +25,50 @@ export function validateTimerType(maybeTimerType: unknown, fallback = TimerType.
|
|||||||
}
|
}
|
||||||
return fallback;
|
return 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;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function validateTimes(_start?: number | null, _end?: number | null, _duration?: number | null) {
|
||||||
|
const timeStart = _start ?? 0;
|
||||||
|
const timeEnd = _end ?? 0;
|
||||||
|
const duration = _duration ?? 0;
|
||||||
|
|
||||||
|
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: _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