mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-12 19:03:47 +00:00
V3 (#657)
* refactor: cleanup routes * style: smaller base font * chore: upgrade dependencies * chore: lock node version to electron * refactor: pass HTTP to integration controller (#652) * refactor: deprecate onair control * refactor: remove playback router * Several project files user folder (#617) * chore: automated screenshots (#667) * feat: app settings (#658) * refactor: remove deprecated event data (#674) * Studio clock (#663) --------- Co-authored-by: Carlos Valente <carlosvalente@pm.me> * Feat: reorder events with alt+ctrl + arrow up/down (#645) * Warning and danger per event (#677) --------- Co-authored-by: Fabian Posenau <fabian@fphome.de> * refactor: stabilise actionHandler (#683) Co-authored-by: Fabian Posenau <fabian@fphome.de> * improvement: hide seconds (#675) * wip: overview (#688) * fix: focus cursor (#695) * refactor: update lower third (#665) * Refactor/time formatting (#696) --------- Co-authored-by: Carlos Valente <34649812+cpvalente@users.noreply.github.com> Co-authored-by: Carlos Valente <carlosvalente@pm.me> * feat: multiple selection (#703) --------- Co-authored-by: asharonbaltazar <asharonbaltazar@outlook.com> Co-authored-by: Alex <ac@omnivox.dk> * fix: test - go to `Edit mode` befor tying to click `Event options` button (#708) * refactor: runtime service (#715) * fix: issue with loosing cursor position on message (#719) * remove info panel (#721) * Event editor continue (#722) * update API - part (#709) --------- Co-authored-by: Carlos Valente <34649812+cpvalente@users.noreply.github.com> Co-authored-by: Carlos Valente <carlosvalente@pm.me> * refactor: update timers (#729) * feat: many timers (#706) --------- Co-authored-by: arc-alex <ac@omnivox.dk> * refactor: excel cleanup (#734) * refactor: allow import of blocks and skip import (#735) * Project manager (#697) * refactor: UI for linking events (#763) * upgraded pipeline actions (#777) * Over under (#771) * custom fields (#744) --------- Co-authored-by: Carlos Valente <carlosvalente@pm.me> * Sheets settings (#774) --------- Co-authored-by: arc-alex <ac@omnivox.dk> * style: tweaks to lower thirds (#785) * refactor: delays account for gaps (#784) * refactor: partial state updates (#780) * feat: generate crash report (#787) * Sheet use limited input device auth flow (#782) --------- Co-authored-by: cv <34649812+cpvalente@users.noreply.github.com> Co-authored-by: Carlos Valente <carlosvalente@pm.me> * Custom fields views (#789) * refactor: deprecate presenter and subtitle (#795) * refactor: organise API around resources (#798) --------- Co-authored-by: Bianca Procopio <biancahprocopio@gmail.com> * Time to end (#804) * Skip fixes (#805) * fix: onair derives from playback * Param nav (#822) --------- Co-authored-by: Alex Christoffer Rasmussen <ac@omnivox.dk> * refactor: download files from interface (#831) * Quick options (#814) * End pause (#832) * chore: bump node version in docker (#834) * refactor: follow in run mode (#840) * fix: uncaught error in http integration (#837) * Apply project (#843) Co-authored-by: Matteo Gheza <matteo.gheza07@gmail.com> Co-authored-by: Ary <arylmoraesn@gmail.com> Co-authored-by: Alex Christoffer Rasmussen <ac@omnivox.dk> Co-authored-by: Fabian Posenau <19673098+kellhogs@users.noreply.github.com> Co-authored-by: Fabian Posenau <fabian@fphome.de> Co-authored-by: Alex Rohleder <alexrohleder96@gmail.com> Co-authored-by: asharonbaltazar <asharonbaltazar@outlook.com> Co-authored-by: Bianca Procopio <biancahprocopio@gmail.com> Co-authored-by: Fabian Posenau <fabianpos99+github@gmail.com>
This commit is contained in:
+200
-314
@@ -1,38 +1,36 @@
|
||||
import {
|
||||
defaultImportMap,
|
||||
generateId,
|
||||
isExcelImportMap,
|
||||
type ExcelImportMap,
|
||||
defaultExcelImportMap,
|
||||
type ImportMap,
|
||||
isKnownTimerType,
|
||||
validateEndAction,
|
||||
validateLinkStart,
|
||||
validateTimerType,
|
||||
type ExcelImportOptions,
|
||||
validateTimes,
|
||||
} from 'ontime-utils';
|
||||
import {
|
||||
CustomFields,
|
||||
DatabaseModel,
|
||||
EventCustomFields,
|
||||
OntimeBlock,
|
||||
OntimeEvent,
|
||||
OntimeRundown,
|
||||
SupportedEvent,
|
||||
ProjectData,
|
||||
UserFields,
|
||||
EndAction,
|
||||
TimerType,
|
||||
TimeStrategy,
|
||||
} from 'ontime-types';
|
||||
|
||||
import fs from 'fs';
|
||||
import xlsx from 'node-xlsx';
|
||||
|
||||
import { event as eventDef } from '../models/eventsDefinition.js';
|
||||
import { dbModel } from '../models/dataModel.js';
|
||||
import { deleteFile, makeString } from './parserUtils.js';
|
||||
import { makeString } from './parserUtils.js';
|
||||
import {
|
||||
parseAliases,
|
||||
parseProject,
|
||||
parseOsc,
|
||||
parseCustomFields,
|
||||
parseHttp,
|
||||
parseOsc,
|
||||
parseProject,
|
||||
parseRundown,
|
||||
parseSettings,
|
||||
parseUserFields,
|
||||
parseUrlPresets,
|
||||
parseViewSettings,
|
||||
} from './parserFunctions.js';
|
||||
import { parseExcelDate } from './time.js';
|
||||
@@ -41,46 +39,51 @@ import { coerceBoolean } from './coerceType.js';
|
||||
export const EXCEL_MIME = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
|
||||
export const JSON_MIME = 'application/json';
|
||||
|
||||
type ExcelData = Pick<DatabaseModel, 'rundown' | 'customFields'> & {
|
||||
rundownMetadata: Record<string, { row: number; col: number }>;
|
||||
};
|
||||
|
||||
export function getCustomFieldData(importMap: ImportMap): {
|
||||
customFields: CustomFields;
|
||||
customFieldImportKeys: Record<keyof CustomFields, string>;
|
||||
} {
|
||||
const customFields = {};
|
||||
const customFieldImportKeys = {};
|
||||
for (const ontimeLabel in importMap.custom) {
|
||||
const ontimeKey = ontimeLabel.toLowerCase();
|
||||
const importLabel = importMap.custom[ontimeLabel].toLowerCase();
|
||||
customFields[ontimeKey] = {
|
||||
type: 'string',
|
||||
colour: '',
|
||||
label: ontimeLabel,
|
||||
};
|
||||
customFieldImportKeys[importLabel] = ontimeKey;
|
||||
}
|
||||
return { customFields, customFieldImportKeys };
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Excel array parser
|
||||
* @param {array} excelData - array with excel sheet
|
||||
* @param {ExcelImportOptions} options - an object that contains the import map
|
||||
* @param {ImportOptions} options - an object that contains the import map
|
||||
* @returns {object} - parsed object
|
||||
*/
|
||||
export const parseExcel = (excelData: unknown[][], options?: Partial<ExcelImportMap>) => {
|
||||
const projectMetadata = {};
|
||||
export const parseExcel = (excelData: unknown[][], options?: Partial<ImportMap>): ExcelData => {
|
||||
const rundownMetadata = {};
|
||||
const importMap: ExcelImportMap = { ...defaultExcelImportMap, ...options };
|
||||
const importMap: ImportMap = { ...defaultImportMap, ...options };
|
||||
|
||||
for (const [key, value] of Object.entries(importMap)) {
|
||||
importMap[key] = value.toLocaleLowerCase();
|
||||
if (typeof value === 'string') {
|
||||
importMap[key] = value.toLocaleLowerCase();
|
||||
}
|
||||
}
|
||||
const projectData: Partial<ProjectData> = {
|
||||
title: '',
|
||||
description: '',
|
||||
publicUrl: '',
|
||||
publicInfo: '',
|
||||
backstageUrl: '',
|
||||
backstageInfo: '',
|
||||
};
|
||||
const customUserFields: Partial<UserFields> = {
|
||||
user0: importMap.user0,
|
||||
user1: importMap.user1,
|
||||
user2: importMap.user2,
|
||||
user3: importMap.user3,
|
||||
user4: importMap.user4,
|
||||
user5: importMap.user5,
|
||||
user6: importMap.user6,
|
||||
user7: importMap.user7,
|
||||
user8: importMap.user8,
|
||||
user9: importMap.user9,
|
||||
};
|
||||
|
||||
const { customFields, customFieldImportKeys } = getCustomFieldData(importMap);
|
||||
const rundown: OntimeRundown = [];
|
||||
|
||||
// title stuff: strings
|
||||
let titleIndex: number | null = null;
|
||||
let cueIndex: number | null = null;
|
||||
let presenterIndex: number | null = null;
|
||||
let subtitleIndex: number | null = null;
|
||||
let notesIndex: number | null = null;
|
||||
let colourIndex: number | null = null;
|
||||
|
||||
@@ -92,62 +95,23 @@ export const parseExcel = (excelData: unknown[][], options?: Partial<ExcelImport
|
||||
let timeStartIndex: number | null = null;
|
||||
let timeEndIndex: number | null = null;
|
||||
let durationIndex: number | null = null;
|
||||
let timeWarningIndex: number | null = null;
|
||||
let timeDangerIndex: number | null = null;
|
||||
|
||||
// options: enum properties
|
||||
let endActionIndex: number | null = null;
|
||||
let timerTypeIndex: number | null = null;
|
||||
|
||||
// user fields: strings
|
||||
let user0Index: number | null = null;
|
||||
let user1Index: number | null = null;
|
||||
let user2Index: number | null = null;
|
||||
let user3Index: number | null = null;
|
||||
let user4Index: number | null = null;
|
||||
let user5Index: number | null = null;
|
||||
let user6Index: number | null = null;
|
||||
let user7Index: number | null = null;
|
||||
let user8Index: number | null = null;
|
||||
let user9Index: number | null = null;
|
||||
// record of column index and the name of the field
|
||||
const customFieldIndexes: Record<number, string> = {};
|
||||
|
||||
excelData.forEach((row, rowIndex) => {
|
||||
if (row.length === 0) {
|
||||
return;
|
||||
}
|
||||
// these fields contain the data to its right
|
||||
let projectTitleNext = false;
|
||||
let projectDescriptionNext = false;
|
||||
let publicUrlNext = false;
|
||||
let publicInfoNext = false;
|
||||
let backstageUrlNext = false;
|
||||
let backstageInfoNext = false;
|
||||
|
||||
const event: Partial<OntimeEvent> = {};
|
||||
// TODO: extract generating handlers from importMap
|
||||
const handlers = {
|
||||
[importMap.projectName]: (row: number, col: number) => {
|
||||
projectTitleNext = true;
|
||||
projectMetadata['title'] = { row, col };
|
||||
},
|
||||
[importMap.projectDescription]: (row: number, col: number) => {
|
||||
projectDescriptionNext = true;
|
||||
projectMetadata['description'] = { row, col };
|
||||
},
|
||||
[importMap.publicUrl]: (row: number, col: number) => {
|
||||
publicUrlNext = true;
|
||||
projectMetadata['publicUrl'] = { row, col };
|
||||
},
|
||||
[importMap.publicInfo]: (row: number, col: number) => {
|
||||
publicInfoNext = true;
|
||||
projectMetadata['publicInfo'] = { row, col };
|
||||
},
|
||||
[importMap.backstageUrl]: (row: number, col: number) => {
|
||||
backstageUrlNext = true;
|
||||
projectMetadata['backstageUrl'] = { row, col };
|
||||
},
|
||||
[importMap.backstageInfo]: (row: number, col: number) => {
|
||||
backstageInfoNext = true;
|
||||
projectMetadata['backstageInfo'] = { row, col };
|
||||
},
|
||||
|
||||
[importMap.timeStart]: (row: number, col: number) => {
|
||||
timeStartIndex = col;
|
||||
rundownMetadata['timeStart'] = { row, col };
|
||||
@@ -169,14 +133,6 @@ export const parseExcel = (excelData: unknown[][], options?: Partial<ExcelImport
|
||||
titleIndex = col;
|
||||
rundownMetadata['title'] = { row, col };
|
||||
},
|
||||
[importMap.presenter]: (row: number, col: number) => {
|
||||
presenterIndex = col;
|
||||
rundownMetadata['presenter'] = { row, col };
|
||||
},
|
||||
[importMap.subtitle]: (row: number, col: number) => {
|
||||
subtitleIndex = col;
|
||||
rundownMetadata['subtitle'] = { row, col };
|
||||
},
|
||||
[importMap.isPublic]: (row: number, col: number) => {
|
||||
isPublicIndex = col;
|
||||
rundownMetadata['isPublic'] = { row, col };
|
||||
@@ -193,7 +149,6 @@ export const parseExcel = (excelData: unknown[][], options?: Partial<ExcelImport
|
||||
colourIndex = col;
|
||||
rundownMetadata['colour'] = { row, col };
|
||||
},
|
||||
|
||||
[importMap.endAction]: (row: number, col: number) => {
|
||||
endActionIndex = col;
|
||||
rundownMetadata['endAction'] = { row, col };
|
||||
@@ -202,83 +157,46 @@ export const parseExcel = (excelData: unknown[][], options?: Partial<ExcelImport
|
||||
timerTypeIndex = col;
|
||||
rundownMetadata['timerType'] = { row, col };
|
||||
},
|
||||
|
||||
[importMap.user0]: (row: number, col: number) => {
|
||||
user0Index = col;
|
||||
rundownMetadata['user0'] = { row, col };
|
||||
[importMap.timeWarning]: (row: number, col: number) => {
|
||||
timeWarningIndex = col;
|
||||
rundownMetadata['timeWarningIndex'] = { row, col };
|
||||
},
|
||||
[importMap.user1]: (row: number, col: number) => {
|
||||
user1Index = col;
|
||||
rundownMetadata['user1'] = { row, col };
|
||||
[importMap.timeDanger]: (row: number, col: number) => {
|
||||
timeDangerIndex = col;
|
||||
rundownMetadata['timeDangerIndex'] = { row, col };
|
||||
},
|
||||
[importMap.user2]: (row: number, col: number) => {
|
||||
user2Index = col;
|
||||
rundownMetadata['user2'] = { row, col };
|
||||
},
|
||||
[importMap.user3]: (row: number, col: number) => {
|
||||
user3Index = col;
|
||||
rundownMetadata['user3'] = { row, col };
|
||||
},
|
||||
[importMap.user4]: (row: number, col: number) => {
|
||||
user4Index = col;
|
||||
rundownMetadata['user4'] = { row, col };
|
||||
},
|
||||
[importMap.user5]: (row: number, col: number) => {
|
||||
user5Index = col;
|
||||
rundownMetadata['user5'] = { row, col };
|
||||
},
|
||||
[importMap.user6]: (row: number, col: number) => {
|
||||
user6Index = col;
|
||||
rundownMetadata['user6'] = { row, col };
|
||||
},
|
||||
[importMap.user7]: (row: number, col: number) => {
|
||||
user7Index = col;
|
||||
rundownMetadata['user7'] = { row, col };
|
||||
},
|
||||
[importMap.user8]: (row: number, col: number) => {
|
||||
user8Index = col;
|
||||
rundownMetadata['user8'] = { row, col };
|
||||
},
|
||||
[importMap.user9]: (row: number, col: number) => {
|
||||
user9Index = col;
|
||||
rundownMetadata['user9'] = { row, col };
|
||||
custom: (row: number, col: number, columnText: string) => {
|
||||
customFieldIndexes[col] = columnText;
|
||||
rundownMetadata[`custom-${columnText}`] = { row, col };
|
||||
},
|
||||
} as const;
|
||||
|
||||
row.forEach((column, j) => {
|
||||
const event: any = {};
|
||||
const eventCustomFields: EventCustomFields = {};
|
||||
|
||||
for (let j = 0; j < row.length; j++) {
|
||||
const column = row[j];
|
||||
// 1. we check if we have set a flag for a known field
|
||||
if (projectTitleNext) {
|
||||
projectData.title = makeString(column, '');
|
||||
projectTitleNext = false;
|
||||
} else if (projectDescriptionNext) {
|
||||
projectData.description = makeString(column, '');
|
||||
projectDescriptionNext = false;
|
||||
} else if (publicUrlNext) {
|
||||
projectData.publicUrl = makeString(column, '');
|
||||
publicUrlNext = false;
|
||||
} else if (publicInfoNext) {
|
||||
projectData.publicInfo = makeString(column, '');
|
||||
publicInfoNext = false;
|
||||
} else if (backstageUrlNext) {
|
||||
projectData.backstageUrl = makeString(column, '');
|
||||
backstageUrlNext = false;
|
||||
} else if (backstageInfoNext) {
|
||||
projectData.backstageInfo = makeString(column, '');
|
||||
backstageInfoNext = false;
|
||||
if (j === timerTypeIndex) {
|
||||
if (column === 'block') {
|
||||
event.type = SupportedEvent.Block;
|
||||
} else if (column === '' || isKnownTimerType(column)) {
|
||||
event.type = SupportedEvent.Event;
|
||||
event.timerType = validateTimerType(column);
|
||||
} else {
|
||||
// if it is not a block or a known type, we dont import it
|
||||
return;
|
||||
}
|
||||
} else if (j === titleIndex) {
|
||||
event.title = makeString(column, '');
|
||||
} else if (j === timeStartIndex) {
|
||||
event.timeStart = parseExcelDate(column);
|
||||
} else if (j === timeEndIndex) {
|
||||
event.timeEnd = parseExcelDate(column);
|
||||
} else if (j === durationIndex) {
|
||||
event.duration = parseExcelDate(column);
|
||||
} else if (j === titleIndex) {
|
||||
event.title = makeString(column, '');
|
||||
} else if (j === cueIndex) {
|
||||
event.cue = makeString(column, '');
|
||||
} else if (j === presenterIndex) {
|
||||
event.presenter = makeString(column, '');
|
||||
} else if (j === subtitleIndex) {
|
||||
event.subtitle = makeString(column, '');
|
||||
} else if (j === isPublicIndex) {
|
||||
event.isPublic = column == 'x' ? true : coerceBoolean(column);
|
||||
} else if (j === skipIndex) {
|
||||
@@ -287,59 +205,60 @@ export const parseExcel = (excelData: unknown[][], options?: Partial<ExcelImport
|
||||
event.note = makeString(column, '');
|
||||
} else if (j === endActionIndex) {
|
||||
event.endAction = validateEndAction(column);
|
||||
} else if (j === timerTypeIndex) {
|
||||
event.timerType = validateTimerType(column);
|
||||
} else if (j === timeWarningIndex) {
|
||||
event.timeWarning = parseExcelDate(column);
|
||||
} else if (j === timeDangerIndex) {
|
||||
event.timeDanger = parseExcelDate(column);
|
||||
} else if (j === colourIndex) {
|
||||
event.colour = makeString(column, '');
|
||||
} else if (j === user0Index) {
|
||||
event.user0 = makeString(column, '');
|
||||
} else if (j === user1Index) {
|
||||
event.user1 = makeString(column, '');
|
||||
} else if (j === user2Index) {
|
||||
event.user2 = makeString(column, '');
|
||||
} else if (j === user3Index) {
|
||||
event.user3 = makeString(column, '');
|
||||
} else if (j === user4Index) {
|
||||
event.user4 = makeString(column, '');
|
||||
} else if (j === user5Index) {
|
||||
event.user5 = makeString(column, '');
|
||||
} else if (j === user6Index) {
|
||||
event.user6 = makeString(column, '');
|
||||
} else if (j === user7Index) {
|
||||
event.user7 = makeString(column, '');
|
||||
} else if (j === user8Index) {
|
||||
event.user8 = makeString(column, '');
|
||||
} else if (j === user9Index) {
|
||||
event.user9 = makeString(column, '');
|
||||
} else if (j in customFieldIndexes) {
|
||||
const importKey = customFieldIndexes[j];
|
||||
const ontimeKey = customFieldImportKeys[importKey];
|
||||
eventCustomFields[ontimeKey] = { value: makeString(column, '') };
|
||||
} else {
|
||||
// 2. if there is no flag, lets see if we know the field type
|
||||
if (typeof column === 'string') {
|
||||
const col = column.toLowerCase();
|
||||
|
||||
if (handlers[col]) {
|
||||
handlers[col](rowIndex, j);
|
||||
// we cant deal with empty content
|
||||
if (column.length === 0) {
|
||||
continue;
|
||||
}
|
||||
const columnText = column.toLowerCase();
|
||||
|
||||
// check if it is an ontime column
|
||||
if (handlers[columnText]) {
|
||||
handlers[columnText](rowIndex, j, undefined);
|
||||
}
|
||||
|
||||
// check if it is a custom field
|
||||
if (columnText in customFieldImportKeys) {
|
||||
handlers.custom(rowIndex, j, columnText);
|
||||
}
|
||||
|
||||
// else. we don't know how to handle this column
|
||||
// just ignore it
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (Object.keys(event).length > 0) {
|
||||
// if any data was found, push to array
|
||||
rundown.push({ ...event, type: SupportedEvent.Event } as OntimeEvent);
|
||||
// if any data was found in row, push to array
|
||||
const keysFound = Object.keys(event).length + Object.keys(eventCustomFields).length;
|
||||
if (keysFound > 0) {
|
||||
// if it is a Block type drop all other filed
|
||||
if (event.type === SupportedEvent.Block) {
|
||||
rundown.push({ type: event.type, id: event.id, title: event.title } as OntimeBlock);
|
||||
} else {
|
||||
if (timerTypeIndex === null) {
|
||||
event.timerType = TimerType.CountDown;
|
||||
event.type = SupportedEvent.Event;
|
||||
}
|
||||
rundown.push({ ...event, custom: { ...eventCustomFields } });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
rundown,
|
||||
project: projectData,
|
||||
settings: {
|
||||
app: 'ontime',
|
||||
version: '2.0.0',
|
||||
},
|
||||
userFields: customUserFields,
|
||||
projectMetadata,
|
||||
customFields,
|
||||
rundownMetadata,
|
||||
};
|
||||
};
|
||||
@@ -349,140 +268,107 @@ export const parseExcel = (excelData: unknown[][], options?: Partial<ExcelImport
|
||||
* @param {object} jsonData - project file to be parsed
|
||||
* @returns {object} - parsed object
|
||||
*/
|
||||
export const parseJson = async (jsonData): Promise<DatabaseModel | null> => {
|
||||
export const parseJson = async (jsonData: Partial<DatabaseModel>): Promise<DatabaseModel | null> => {
|
||||
if (!jsonData || typeof jsonData !== 'object') {
|
||||
return null;
|
||||
}
|
||||
|
||||
// object containing the parsed data
|
||||
const returnData: Partial<DatabaseModel> = {};
|
||||
let settings;
|
||||
|
||||
// parse Events
|
||||
returnData.rundown = parseRundown(jsonData);
|
||||
// parse Event
|
||||
returnData.project = parseProject(jsonData) ?? dbModel.project;
|
||||
// Settings handled partially
|
||||
returnData.settings = parseSettings(jsonData) ?? dbModel.settings;
|
||||
// View settings handled partially
|
||||
returnData.viewSettings = parseViewSettings(jsonData) ?? dbModel.viewSettings;
|
||||
// Import Aliases if any
|
||||
returnData.aliases = parseAliases(jsonData);
|
||||
// Import user fields if any
|
||||
returnData.userFields = parseUserFields(jsonData);
|
||||
// Import OSC settings if any
|
||||
returnData.osc = parseOsc(jsonData) ?? dbModel.osc;
|
||||
// Import HTTP settings if any
|
||||
returnData.http = parseHttp(jsonData) ?? dbModel.http;
|
||||
// check settings first to make sure we can parse it
|
||||
try {
|
||||
settings = parseSettings(jsonData);
|
||||
} catch (error) {
|
||||
// if we cant parse, return an empty project
|
||||
console.log('ERROR: unable to parse settings, missing app or version');
|
||||
return dbModel;
|
||||
}
|
||||
|
||||
return returnData as DatabaseModel;
|
||||
const returnData: DatabaseModel = {
|
||||
rundown: parseRundown(jsonData),
|
||||
project: parseProject(jsonData),
|
||||
settings,
|
||||
viewSettings: parseViewSettings(jsonData),
|
||||
urlPresets: parseUrlPresets(jsonData),
|
||||
customFields: parseCustomFields(jsonData),
|
||||
osc: parseOsc(jsonData),
|
||||
http: parseHttp(jsonData),
|
||||
};
|
||||
|
||||
return returnData;
|
||||
};
|
||||
|
||||
/**
|
||||
* Function infers strategy for a patch with only partial timer data
|
||||
* @param end
|
||||
* @param duration
|
||||
* @param fallback
|
||||
* @returns
|
||||
*/
|
||||
function inferStrategy(end: unknown, duration: unknown, fallback: TimeStrategy): TimeStrategy {
|
||||
if (end && !duration) {
|
||||
return TimeStrategy.LockEnd;
|
||||
}
|
||||
|
||||
if (!end && duration) {
|
||||
return TimeStrategy.LockDuration;
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
export function createPatch(originalEvent: OntimeEvent, patchEvent: Partial<OntimeEvent>): OntimeEvent {
|
||||
if (Object.keys(patchEvent).length === 0) {
|
||||
return originalEvent;
|
||||
}
|
||||
|
||||
const { timeStart, timeEnd, duration, timeStrategy } = validateTimes(
|
||||
patchEvent?.timeStart ?? originalEvent.timeStart,
|
||||
patchEvent?.timeEnd ?? originalEvent.timeEnd,
|
||||
patchEvent?.duration ?? originalEvent.duration,
|
||||
patchEvent?.timeStrategy ?? inferStrategy(patchEvent?.timeEnd, patchEvent?.duration, originalEvent.timeStrategy),
|
||||
);
|
||||
const maybeLinkStart = patchEvent.linkStart !== undefined ? patchEvent.linkStart : originalEvent.linkStart;
|
||||
|
||||
return {
|
||||
id: originalEvent.id,
|
||||
type: SupportedEvent.Event,
|
||||
title: makeString(patchEvent.title, originalEvent.title),
|
||||
timeStart,
|
||||
timeEnd,
|
||||
duration,
|
||||
timeStrategy,
|
||||
linkStart: validateLinkStart(maybeLinkStart),
|
||||
endAction: validateEndAction(patchEvent.endAction, originalEvent.endAction),
|
||||
timerType: validateTimerType(patchEvent.timerType, originalEvent.timerType),
|
||||
isPublic: typeof patchEvent.isPublic === 'boolean' ? patchEvent.isPublic : originalEvent.isPublic,
|
||||
skip: typeof patchEvent.skip === 'boolean' ? patchEvent.skip : originalEvent.skip,
|
||||
note: makeString(patchEvent.note, originalEvent.note),
|
||||
colour: makeString(patchEvent.colour, originalEvent.colour),
|
||||
// short circuit empty string
|
||||
cue: makeString(patchEvent.cue ?? null, originalEvent.cue),
|
||||
revision: originalEvent.revision,
|
||||
timeWarning: patchEvent.timeWarning ?? originalEvent.timeWarning,
|
||||
timeDanger: patchEvent.timeDanger ?? originalEvent.timeDanger,
|
||||
custom: { ...originalEvent.custom, ...patchEvent.custom },
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Enforces formatting for events
|
||||
* @param {object} eventArgs - attributes of event
|
||||
* @param cueFallback
|
||||
* @returns {object|null} - formatted object or null in case is invalid
|
||||
*/
|
||||
|
||||
export const validateEvent = (eventArgs: Partial<OntimeEvent>, cueFallback: string) => {
|
||||
// ensure id is defined and unique
|
||||
const id = eventArgs.id || generateId();
|
||||
|
||||
let event = null;
|
||||
|
||||
// return if object is empty
|
||||
if (Object.keys(eventArgs).length > 0) {
|
||||
// make sure all properties exits
|
||||
// dont load any extra properties than the ones known
|
||||
|
||||
const e = eventArgs;
|
||||
const d = eventDef;
|
||||
|
||||
const { timeStart, timeEnd, duration } = validateTimes(e.timeStart, e.timeEnd, e.duration);
|
||||
|
||||
event = {
|
||||
...d,
|
||||
title: makeString(e.title, d.title),
|
||||
subtitle: makeString(e.subtitle, d.subtitle),
|
||||
presenter: makeString(e.presenter, d.presenter),
|
||||
timeStart,
|
||||
timeEnd,
|
||||
duration,
|
||||
endAction: validateEndAction(e.endAction, EndAction.None),
|
||||
timerType: validateTimerType(e.timerType, TimerType.CountDown),
|
||||
isPublic: typeof e.isPublic === 'boolean' ? e.isPublic : d.isPublic,
|
||||
skip: typeof e.skip === 'boolean' ? e.skip : d.skip,
|
||||
note: makeString(e.note, d.note),
|
||||
user0: makeString(e.user0, d.user0),
|
||||
user1: makeString(e.user1, d.user1),
|
||||
user2: makeString(e.user2, d.user2),
|
||||
user3: makeString(e.user3, d.user3),
|
||||
user4: makeString(e.user4, d.user4),
|
||||
user5: makeString(e.user5, d.user5),
|
||||
user6: makeString(e.user6, d.user6),
|
||||
user7: makeString(e.user7, d.user7),
|
||||
user8: makeString(e.user8, d.user8),
|
||||
user9: makeString(e.user9, d.user9),
|
||||
colour: makeString(e.colour, d.colour),
|
||||
cue: makeString(e.cue, cueFallback),
|
||||
id,
|
||||
type: 'event',
|
||||
};
|
||||
export const createEvent = (eventArgs: Partial<OntimeEvent>, cueFallback: string): OntimeEvent | null => {
|
||||
if (Object.keys(eventArgs).length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const baseEvent = {
|
||||
id: eventArgs?.id ?? generateId(),
|
||||
cue: cueFallback,
|
||||
...eventDef,
|
||||
};
|
||||
const event = createPatch(baseEvent, eventArgs);
|
||||
return event;
|
||||
};
|
||||
|
||||
type ResponseOK = {
|
||||
data: Partial<DatabaseModel>;
|
||||
};
|
||||
|
||||
/**
|
||||
* @description Middleware function that checks file type and calls relevant parser
|
||||
* @param {string} file - reference to file
|
||||
* @param options - import options
|
||||
* @return {object} - parse result message
|
||||
*/
|
||||
export const fileHandler = async (file: string, options: ExcelImportOptions): Promise<Partial<ResponseOK>> => {
|
||||
const res: Partial<ResponseOK> = {};
|
||||
|
||||
// check which file type are we dealing with
|
||||
if (file.endsWith('.xlsx')) {
|
||||
// we need to check that the options are applicable
|
||||
if (!isExcelImportMap(options)) {
|
||||
throw new Error('Got incorrect options to excel import', JSON.parse(options));
|
||||
}
|
||||
|
||||
const excelData = xlsx
|
||||
.parse(file, { cellDates: true })
|
||||
.find(({ name }) => name.toLowerCase() === options.worksheet.toLowerCase());
|
||||
|
||||
if (!excelData?.data) {
|
||||
throw new Error(`Could not find data to import, maybe the worksheet name is incorrect: ${options.worksheet}`);
|
||||
}
|
||||
|
||||
const dataFromExcel = parseExcel(excelData.data, options);
|
||||
// we run the parsed data through an extra step to ensure the objects shape
|
||||
res.data = {};
|
||||
res.data.rundown = parseRundown(dataFromExcel);
|
||||
if (res.data.rundown.length < 1) {
|
||||
throw new Error(`Could not find data to import in the worksheet ${options.worksheet}`);
|
||||
}
|
||||
res.data.project = parseProject(dataFromExcel);
|
||||
res.data.userFields = parseUserFields(dataFromExcel);
|
||||
return res;
|
||||
}
|
||||
|
||||
if (file.endsWith('.json')) {
|
||||
// if json check version
|
||||
const rawdata = fs.readFileSync(file).toString();
|
||||
let uploadedJson = null;
|
||||
|
||||
uploadedJson = JSON.parse(rawdata);
|
||||
res.data = await parseJson(uploadedJson);
|
||||
|
||||
// delete file
|
||||
await deleteFile(file);
|
||||
return res;
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user