mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-13 03:13:47 +00:00
feat: export excel file
refactor: align id columns between export and import refactor: match column names with template
This commit is contained in:
committed by
Carlos Valente
parent
10762f10bf
commit
1d8c04d0b4
@@ -0,0 +1,363 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`rundownToTabular() > returns an array of arrays describing a rundown 1`] = `
|
||||
[
|
||||
[
|
||||
"ID",
|
||||
"Flag",
|
||||
"Cue",
|
||||
"Title",
|
||||
"Colour",
|
||||
"Time Start",
|
||||
"Time End",
|
||||
"Duration",
|
||||
"Link Start",
|
||||
"Count to end",
|
||||
"Note",
|
||||
"Timer Type",
|
||||
"End Action",
|
||||
"Warning Time",
|
||||
"Danger Time",
|
||||
"Skip",
|
||||
"Custom Field",
|
||||
"Images",
|
||||
],
|
||||
[
|
||||
"e2163f",
|
||||
"",
|
||||
"Demo",
|
||||
"Clear all, or Create New Project to start fresh",
|
||||
"#9d9d9d",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"Go to Settings cog wheel top left of Editor, Project > Create",
|
||||
"milestone",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"Create Custom Fields to store specific text information or images.
|
||||
|
||||
Go to Settings > Project data > Custom fields",
|
||||
"https://www.getontime.no/images/icons/ontime-logo.png",
|
||||
],
|
||||
[
|
||||
"7eaf99",
|
||||
"",
|
||||
"",
|
||||
"Morning Sessions",
|
||||
"#339E4E",
|
||||
"10:00:00",
|
||||
"12:00:00",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"group",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
],
|
||||
[
|
||||
"9bf60f",
|
||||
"",
|
||||
"1",
|
||||
"Pre-show Countdown",
|
||||
"#77C785",
|
||||
"10:00:00",
|
||||
"11:00:00",
|
||||
"01:00:00",
|
||||
"",
|
||||
"",
|
||||
"Music plays, holding slide on screens",
|
||||
"count-down",
|
||||
"",
|
||||
"00:10:00",
|
||||
"00:05:00",
|
||||
"",
|
||||
"Put additional info here",
|
||||
"",
|
||||
],
|
||||
[
|
||||
"bf71a2",
|
||||
"",
|
||||
"Standby",
|
||||
"10:45 - Presenters ready side stage",
|
||||
"#A790F5",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"milestone",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
],
|
||||
[
|
||||
"c2697f",
|
||||
"",
|
||||
"1.1",
|
||||
"Welcome",
|
||||
"#FFCC78",
|
||||
"11:00:00",
|
||||
"11:10:00",
|
||||
"00:10:00",
|
||||
"TRUE",
|
||||
"",
|
||||
"Emma Thompson",
|
||||
"count-down",
|
||||
"",
|
||||
"00:02:00",
|
||||
"00:01:00",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
],
|
||||
[
|
||||
"fa593e",
|
||||
"TRUE",
|
||||
"1.2",
|
||||
"Session 1",
|
||||
"#77C785",
|
||||
"11:10:00",
|
||||
"12:00:00",
|
||||
"00:50:00",
|
||||
"TRUE",
|
||||
"",
|
||||
"Liam Carter, Sophia Patel + PowerPoint",
|
||||
"count-down",
|
||||
"",
|
||||
"00:02:00",
|
||||
"00:01:00",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
],
|
||||
[
|
||||
"a8b0b3",
|
||||
"",
|
||||
"House",
|
||||
"11:30 - House staff setup lunch in lobby",
|
||||
"#A790F5",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"milestone",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
],
|
||||
[
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"group-end",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
],
|
||||
[
|
||||
"f60403",
|
||||
"",
|
||||
"",
|
||||
"Lunch",
|
||||
"#3E75E8",
|
||||
"12:00:00",
|
||||
"13:00:00",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"group",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
],
|
||||
[
|
||||
"0aaa7d",
|
||||
"",
|
||||
"2.1",
|
||||
"Lunch / Countdown to next session",
|
||||
"#779BE7",
|
||||
"12:00:00",
|
||||
"13:00:00",
|
||||
"01:00:00",
|
||||
"TRUE",
|
||||
"",
|
||||
"Buffet in lobby
|
||||
Music plays, holding slide on screens",
|
||||
"count-down",
|
||||
"",
|
||||
"00:02:00",
|
||||
"00:01:00",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
],
|
||||
[
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"group-end",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
],
|
||||
[
|
||||
"6b0edb",
|
||||
"",
|
||||
"",
|
||||
"Afternoon Sessions",
|
||||
"#339E4E",
|
||||
"13:00:00",
|
||||
"14:00:00",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"group",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
],
|
||||
[
|
||||
"02afca",
|
||||
"",
|
||||
"Standby",
|
||||
"12:45 - Presenters ready side stage",
|
||||
"#A790F5",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"milestone",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
],
|
||||
[
|
||||
"75ce86",
|
||||
"",
|
||||
"3.1",
|
||||
"Session 2",
|
||||
"#77C785",
|
||||
"13:00:00",
|
||||
"13:50:00",
|
||||
"00:50:00",
|
||||
"TRUE",
|
||||
"",
|
||||
"Ethan Brooks + PowerPoint + Video playback",
|
||||
"count-down",
|
||||
"",
|
||||
"00:02:00",
|
||||
"00:01:00",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
],
|
||||
[
|
||||
"e10ed9",
|
||||
"",
|
||||
"3.2",
|
||||
"Wrap up",
|
||||
"#FFCC78",
|
||||
"13:50:00",
|
||||
"14:00:00",
|
||||
"00:10:00",
|
||||
"",
|
||||
"",
|
||||
"Lucas Bennett",
|
||||
"count-down",
|
||||
"",
|
||||
"00:02:00",
|
||||
"00:01:00",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
],
|
||||
[
|
||||
"07df89",
|
||||
"",
|
||||
"Strike",
|
||||
"14:00 - AV & Room strike",
|
||||
"#A790F5",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"milestone",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
],
|
||||
[
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"group-end",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
],
|
||||
]
|
||||
`;
|
||||
@@ -1,11 +1,13 @@
|
||||
import { CustomFields } from 'ontime-types';
|
||||
import { defaultImportMap, ImportMap } from 'ontime-utils';
|
||||
|
||||
import { getCustomFieldData } from '../excel.utils.js';
|
||||
import { demoDb } from '../../../models/demoProject.js';
|
||||
|
||||
import { getCustomFieldData, rundownToTabular } from '../excel.utils.js';
|
||||
|
||||
describe('getCustomFieldData()', () => {
|
||||
it('generates a list of keys from the given import map', () => {
|
||||
const importMap = {
|
||||
const importMap: ImportMap = {
|
||||
worksheet: 'event schedule',
|
||||
timeStart: 'time start',
|
||||
linkStart: 'link start',
|
||||
@@ -27,8 +29,8 @@ describe('getCustomFieldData()', () => {
|
||||
sound: 'sound',
|
||||
video: 'av',
|
||||
},
|
||||
entryId: 'id',
|
||||
} as ImportMap;
|
||||
id: 'id',
|
||||
};
|
||||
|
||||
const result = getCustomFieldData(importMap, {});
|
||||
expect(result.mergedCustomFields).toStrictEqual({
|
||||
@@ -58,7 +60,7 @@ describe('getCustomFieldData()', () => {
|
||||
});
|
||||
|
||||
it('keeps colour information from existing fields', () => {
|
||||
const importMap = {
|
||||
const importMap: ImportMap = {
|
||||
worksheet: 'event schedule',
|
||||
timeStart: 'time start',
|
||||
linkStart: 'link start',
|
||||
@@ -81,8 +83,8 @@ describe('getCustomFieldData()', () => {
|
||||
video: 'av',
|
||||
'ontime key': 'excel label',
|
||||
},
|
||||
entryId: 'id',
|
||||
} as ImportMap;
|
||||
id: 'id',
|
||||
};
|
||||
|
||||
const existingCustomFields: CustomFields = {
|
||||
lighting: { label: 'lighting', type: 'text', colour: 'red' },
|
||||
@@ -160,3 +162,13 @@ describe('getCustomFieldData()', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('rundownToTabular()', () => {
|
||||
it('returns an array of arrays describing a rundown', () => {
|
||||
const testRundown = demoDb.rundowns['default'];
|
||||
const testCustomFields = demoDb.customFields;
|
||||
|
||||
const result = rundownToTabular(testRundown, testCustomFields);
|
||||
expect(result).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export const EXCEL_MIME = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
|
||||
@@ -3,7 +3,7 @@ import multer, { type FileFilterCallback } from 'multer';
|
||||
|
||||
import { storage } from '../../utils/upload.js';
|
||||
|
||||
export const EXCEL_MIME = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
|
||||
import { EXCEL_MIME } from './excel.constants.js';
|
||||
|
||||
const filterExcel = (_req: Request, file: Express.Multer.File, cb: FileFilterCallback) => {
|
||||
if (file.mimetype.includes(EXCEL_MIME)) {
|
||||
|
||||
@@ -132,7 +132,7 @@ export const parseExcel = (
|
||||
entry.timeDanger = parseExcelDate(column);
|
||||
} else if (j === indexMap.colour) {
|
||||
entry.colour = makeString(column, '');
|
||||
} else if (j === indexMap.entryId) {
|
||||
} else if (j === indexMap.id) {
|
||||
entry.id = encodeURIComponent(makeString(column, undefined));
|
||||
} else if (j in indexMap.custom) {
|
||||
const importKey = indexMap.custom[j];
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
import express from 'express';
|
||||
import type { Request, Response } from 'express';
|
||||
import { uploadExcel } from './excel.middleware.js';
|
||||
import { validateFileExists, validateImportMapOptions } from './excel.validation.js';
|
||||
import { CustomFields, ErrorResponse, Rundown } from 'ontime-types';
|
||||
import { generateRundownPreview, listWorksheets, saveExcelFile } from './excel.service.js';
|
||||
|
||||
import { getProjectCustomFields } from '../rundown/rundown.dao.js';
|
||||
|
||||
import { uploadExcel } from './excel.middleware.js';
|
||||
import { validateFileExists, validateImportMapOptions, validateRundownExport } from './excel.validation.js';
|
||||
import { generateExcelFile, generateRundownPreview, listWorksheets, readExcelFile } from './excel.service.js';
|
||||
import { EXCEL_MIME } from './excel.constants.js';
|
||||
import { getDataProvider } from '../../classes/data-provider/DataProvider.js';
|
||||
|
||||
export const router = express.Router();
|
||||
|
||||
@@ -11,7 +16,7 @@ router.post('/upload', uploadExcel, validateFileExists, async (req: Request, res
|
||||
try {
|
||||
// file has been validated by middleware
|
||||
const filePath = (req.file as Express.Multer.File).path;
|
||||
await saveExcelFile(filePath);
|
||||
await readExcelFile(filePath);
|
||||
res.status(201).send();
|
||||
} catch (error) {
|
||||
res.status(500).send({ message: String(error) });
|
||||
@@ -40,3 +45,18 @@ router.post(
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
router.get('/:rundownId/export', validateRundownExport, (req: Request, res: Response) => {
|
||||
try {
|
||||
const rundown = getDataProvider().getRundown(req.params.rundownId);
|
||||
const customFields = getProjectCustomFields();
|
||||
const buffer = generateExcelFile(rundown, customFields);
|
||||
|
||||
res.setHeader('Content-Disposition', `attachment;`);
|
||||
res.setHeader('Content-Type', EXCEL_MIME);
|
||||
res.setHeader('Content-Length', buffer.length.toString());
|
||||
res.status(200).send(buffer);
|
||||
} catch (error) {
|
||||
res.status(500).send({ message: 'Failed to generate Excel file' });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -18,10 +18,15 @@ import { getProjectCustomFields } from '../rundown/rundown.dao.js';
|
||||
import { parseCustomFields } from '../custom-fields/customFields.parser.js';
|
||||
|
||||
import { parseExcel } from './excel.parser.js';
|
||||
import { rundownToTabular } from './excel.utils.js';
|
||||
|
||||
let excelData: WorkBook = xlsx.utils.book_new();
|
||||
|
||||
export async function saveExcelFile(filePath: string) {
|
||||
/**
|
||||
* Receives and parses an excel file
|
||||
* The file is deleted after being read
|
||||
*/
|
||||
export async function readExcelFile(filePath: string) {
|
||||
if (!existsSync(filePath)) {
|
||||
throw new Error('Upload of excel file failed');
|
||||
}
|
||||
@@ -33,6 +38,9 @@ export async function saveExcelFile(filePath: string) {
|
||||
await deleteFile(filePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* List all worksheets in the current spreadsheet file
|
||||
*/
|
||||
export function listWorksheets(): string[] {
|
||||
return excelData.SheetNames;
|
||||
}
|
||||
@@ -60,3 +68,19 @@ export function generateRundownPreview(options: ImportMap): { rundown: Rundown;
|
||||
|
||||
return { rundown, customFields: parsedCustomFields };
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an xlsx file from a given rundown and custom fields
|
||||
* @throws if the rundown is empty
|
||||
*/
|
||||
export function generateExcelFile(rundown: Rundown, customFields: CustomFields): Buffer {
|
||||
if (rundown.order.length === 0) {
|
||||
throw new Error('Cannot generate an Excel file from an empty rundown');
|
||||
}
|
||||
|
||||
const workbook = xlsx.utils.book_new();
|
||||
const worksheet = xlsx.utils.aoa_to_sheet(rundownToTabular(rundown, customFields));
|
||||
xlsx.utils.book_append_sheet(workbook, worksheet, rundown.title || 'Rundown');
|
||||
|
||||
return xlsx.write(workbook, { type: 'buffer', bookType: 'xlsx' });
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { CustomFieldKey, CustomFields, MaybeNumber } from 'ontime-types';
|
||||
import { checkRegex, customFieldLabelToKey, ImportMap } from 'ontime-utils';
|
||||
import { CustomFieldKey, CustomFields, MaybeNumber, OntimeEntry, Rundown } from 'ontime-types';
|
||||
import { checkRegex, customFieldLabelToKey, ImportMap, millisToString } from 'ontime-utils';
|
||||
|
||||
/**
|
||||
* Receives an import map which contains custom field labels and a custom fields object
|
||||
@@ -87,7 +87,7 @@ export function generateImportHandlers(importMap: ImportMap) {
|
||||
timeDanger: null,
|
||||
endAction: null,
|
||||
timerType: null,
|
||||
entryId: null,
|
||||
id: null,
|
||||
custom: {},
|
||||
};
|
||||
|
||||
@@ -155,8 +155,8 @@ export function generateImportHandlers(importMap: ImportMap) {
|
||||
indexMap.timeDanger = col;
|
||||
sheetMetadata.timeDanger = { row, col };
|
||||
},
|
||||
[importMap.entryId]: (row: number, col: number) => {
|
||||
indexMap.entryId = col;
|
||||
[importMap.id]: (row: number, col: number) => {
|
||||
indexMap.id = col;
|
||||
sheetMetadata['id'] = { row, col }; // important this will be used in a normal context where the id is not called entryId
|
||||
},
|
||||
custom: (row: number, col: number, columnText: string, ontimeKey: string) => {
|
||||
@@ -167,3 +167,171 @@ export function generateImportHandlers(importMap: ImportMap) {
|
||||
|
||||
return { handlers, indexMap, sheetMetadata };
|
||||
}
|
||||
|
||||
const emptyCellValue = '';
|
||||
|
||||
/**
|
||||
* Converts a given rundown to 2D array tabular format
|
||||
* All fields are converted to their string representation as specified by excel
|
||||
*/
|
||||
export function rundownToTabular(rundown: Rundown, customFields: CustomFields): string[][] {
|
||||
const flatRundown: string[][] = [];
|
||||
// we reuse the keys from the import map so import <> export works by default
|
||||
const keys = Object.keys(matchedWithDefaultImportMap) as (keyof typeof matchedWithDefaultImportMap)[];
|
||||
const customFieldKeys = Object.keys(customFields) as CustomFieldKey[];
|
||||
|
||||
// create header row
|
||||
const headerRow: string[] = [];
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const key = keys[i];
|
||||
headerRow.push(matchedWithDefaultImportMap[key]);
|
||||
}
|
||||
for (let i = 0; i < customFieldKeys.length; i++) {
|
||||
const key = customFieldKeys[i];
|
||||
headerRow.push(customFields[key].label);
|
||||
}
|
||||
flatRundown.push(headerRow);
|
||||
|
||||
// create table body
|
||||
let previousEntry: OntimeEntry | undefined;
|
||||
for (let i = 0; i < rundown.flatOrder.length; i++) {
|
||||
const entryId = rundown.flatOrder[i];
|
||||
const entry = rundown.entries[entryId];
|
||||
if (!entry) continue;
|
||||
// we opt for not exporting delays as they are only relevant for runtime
|
||||
if (entry.type === 'delay') continue;
|
||||
|
||||
// we need a group-end if:
|
||||
// - the previous entry was a group and now we have another group (both parents are undefined)
|
||||
// - the previous entry had a parent and the new entry is a group (parent undefined)
|
||||
// - the previous entry had a parent and the new one doesnt (parent null)
|
||||
if (
|
||||
previousEntry &&
|
||||
((previousEntry.type === 'group' && entry.type === 'group') ||
|
||||
// @ts-expect-error -- we safely check if the property is nullish
|
||||
(previousEntry?.parent !== null && entry?.parent == null))
|
||||
) {
|
||||
addGroupEnd();
|
||||
}
|
||||
|
||||
previousEntry = entry;
|
||||
const entryRow: string[] = [];
|
||||
|
||||
// each entry is a row in the table
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const key = keys[i];
|
||||
|
||||
// handle edge cases for group
|
||||
if (entry.type === 'group') {
|
||||
if (key === 'duration') {
|
||||
const groupDuration = entry.targetDuration === null ? '' : parseField('duration', entry.targetDuration);
|
||||
entryRow.push(groupDuration);
|
||||
continue;
|
||||
} else if (key === 'timerType') {
|
||||
entryRow.push('group');
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// handle edge cases for milestones
|
||||
if (entry.type === 'milestone') {
|
||||
if (key === 'timerType') {
|
||||
entryRow.push('milestone');
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// handle edge cases for events
|
||||
if (entry.type === 'event') {
|
||||
if (key === 'endAction' && entry.endAction === 'none') {
|
||||
entryRow.push(emptyCellValue);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (key in entry) {
|
||||
entryRow.push(parseField(key, entry[key as keyof typeof entry]));
|
||||
} else {
|
||||
// we push an empty cell to keep the column positioning
|
||||
entryRow.push(emptyCellValue);
|
||||
}
|
||||
}
|
||||
|
||||
// add custom fields
|
||||
if ('custom' in entry) {
|
||||
for (let i = 0; i < customFieldKeys.length; i++) {
|
||||
const customFieldKey = customFieldKeys[i] as CustomFieldKey;
|
||||
const value = entry.custom?.[customFieldKey] ?? emptyCellValue;
|
||||
entryRow.push(String(value));
|
||||
}
|
||||
}
|
||||
flatRundown.push(entryRow);
|
||||
}
|
||||
|
||||
// close dangling groups
|
||||
// - last element as a group: previous entry.parent is undefined
|
||||
// - last element was in a group: previous entry.parent is string
|
||||
if (previousEntry && (previousEntry.type === 'group' || previousEntry.parent !== null)) {
|
||||
addGroupEnd();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new row with group-end timerType
|
||||
* Ensures that all columns are filled with the correct type
|
||||
*/
|
||||
function addGroupEnd() {
|
||||
const emptyRow = Array(keys.length).fill(emptyCellValue);
|
||||
const timerTypeIndex = keys.indexOf('timerType');
|
||||
emptyRow[timerTypeIndex] = 'group-end';
|
||||
flatRundown.push(emptyRow);
|
||||
}
|
||||
|
||||
return flatRundown;
|
||||
}
|
||||
|
||||
/**
|
||||
* Map between the ontime key and the excel column name
|
||||
* This needs to match match the default import map
|
||||
* TODO: could we organise these objects to have a single source of truth?
|
||||
*/
|
||||
const matchedWithDefaultImportMap = {
|
||||
id: 'ID',
|
||||
flag: 'Flag',
|
||||
cue: 'Cue',
|
||||
title: 'Title',
|
||||
colour: 'Colour',
|
||||
timeStart: 'Time Start',
|
||||
timeEnd: 'Time End',
|
||||
duration: 'Duration',
|
||||
linkStart: 'Link Start',
|
||||
countToEnd: 'Count to end',
|
||||
note: 'Note',
|
||||
timerType: 'Timer Type',
|
||||
endAction: 'End Action',
|
||||
timeWarning: 'Warning Time',
|
||||
timeDanger: 'Danger Time',
|
||||
skip: 'Skip',
|
||||
};
|
||||
|
||||
/**
|
||||
* Parses a fields value into a string suitable for excel
|
||||
*/
|
||||
function parseField(field: string, data: unknown) {
|
||||
if (data == null) return emptyCellValue;
|
||||
|
||||
if (
|
||||
field === 'timeStart' ||
|
||||
field === 'timeEnd' ||
|
||||
field === 'duration' ||
|
||||
field === 'timeWarning' ||
|
||||
field === 'timeDanger'
|
||||
) {
|
||||
return millisToString(data as MaybeNumber, { fallback: emptyCellValue });
|
||||
}
|
||||
|
||||
if (typeof data === 'boolean') {
|
||||
return data ? 'TRUE' : emptyCellValue;
|
||||
}
|
||||
|
||||
return typeof data == 'string' ? data : String(data);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { isImportMap } from 'ontime-utils';
|
||||
|
||||
import { body } from 'express-validator';
|
||||
import { body, param } from 'express-validator';
|
||||
|
||||
import {
|
||||
requestValidationFunction,
|
||||
requestValidationFunctionWithFile,
|
||||
@@ -17,3 +18,5 @@ export const validateImportMapOptions = [
|
||||
|
||||
requestValidationFunction,
|
||||
];
|
||||
|
||||
export const validateRundownExport = [param('rundownId').isString().trim().notEmpty(), requestValidationFunction];
|
||||
|
||||
Reference in New Issue
Block a user