From 1d8c04d0b430ab240df1a39687ccd6a7614350e2 Mon Sep 17 00:00:00 2001 From: Carlos Valente Date: Tue, 7 Oct 2025 22:05:37 +0200 Subject: [PATCH] feat: export excel file refactor: align id columns between export and import refactor: match column names with template --- apps/client/src/common/api/excel.ts | 16 + .../panel/manage-panel/ManageRundowns.tsx | 49 ++- .../import-map/importMapUtils.ts | 2 +- .../__snapshots__/excel.utils.test.ts.snap | 363 ++++++++++++++++++ .../excel/__tests__/excel.utils.test.ts | 26 +- .../src/api-data/excel/excel.constants.ts | 1 + .../src/api-data/excel/excel.middleware.ts | 2 +- .../server/src/api-data/excel/excel.parser.ts | 2 +- .../server/src/api-data/excel/excel.router.ts | 28 +- .../src/api-data/excel/excel.service.ts | 26 +- apps/server/src/api-data/excel/excel.utils.ts | 178 ++++++++- .../src/api-data/excel/excel.validation.ts | 5 +- .../services/sheet-service/SheetService.ts | 9 +- .../types/src/definitions/DataModel.type.ts | 1 - .../__tests__/spreadsheetImport.test.ts | 4 +- .../spreadsheet-import/spreadsheetImport.ts | 2 +- 16 files changed, 672 insertions(+), 42 deletions(-) create mode 100644 apps/server/src/api-data/excel/__tests__/__snapshots__/excel.utils.test.ts.snap create mode 100644 apps/server/src/api-data/excel/excel.constants.ts diff --git a/apps/client/src/common/api/excel.ts b/apps/client/src/common/api/excel.ts index 5c21c987a..bc51c1bb3 100644 --- a/apps/client/src/common/api/excel.ts +++ b/apps/client/src/common/api/excel.ts @@ -3,6 +3,7 @@ import { CustomFields, Rundown } from 'ontime-types'; import { ImportMap } from 'ontime-utils'; import { apiEntryUrl } from './constants'; +import { downloadBlob } from './utils'; const excelPath = `${apiEntryUrl}/excel`; @@ -39,3 +40,18 @@ export async function importRundownPreview(options: ImportMap): Promise { + await downloadAsExcel(rundownId, title); + }; + return ( <> @@ -93,19 +100,35 @@ export default function ManageRundowns() { {title} {isLoaded && Loaded} - - - - + + + ); })} diff --git a/apps/client/src/features/app-settings/panel/manage-panel/sources-panel/import-map/importMapUtils.ts b/apps/client/src/features/app-settings/panel/manage-panel/sources-panel/import-map/importMapUtils.ts index eb0f92a2c..30820fed9 100644 --- a/apps/client/src/features/app-settings/panel/manage-panel/sources-panel/import-map/importMapUtils.ts +++ b/apps/client/src/features/app-settings/panel/manage-panel/sources-panel/import-map/importMapUtils.ts @@ -59,7 +59,7 @@ export function convertToImportMap(namedImportMap: NamedImportMap): ImportMap { timeWarning: namedImportMap['Time warning'], timeDanger: namedImportMap['Time danger'], custom, - entryId: namedImportMap.ID, + id: namedImportMap.ID, }; } diff --git a/apps/server/src/api-data/excel/__tests__/__snapshots__/excel.utils.test.ts.snap b/apps/server/src/api-data/excel/__tests__/__snapshots__/excel.utils.test.ts.snap new file mode 100644 index 000000000..eab1bbbac --- /dev/null +++ b/apps/server/src/api-data/excel/__tests__/__snapshots__/excel.utils.test.ts.snap @@ -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", + "", + "", + "", + "", + ], +] +`; diff --git a/apps/server/src/api-data/excel/__tests__/excel.utils.test.ts b/apps/server/src/api-data/excel/__tests__/excel.utils.test.ts index b10efc8a1..239bde956 100644 --- a/apps/server/src/api-data/excel/__tests__/excel.utils.test.ts +++ b/apps/server/src/api-data/excel/__tests__/excel.utils.test.ts @@ -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(); + }); +}); diff --git a/apps/server/src/api-data/excel/excel.constants.ts b/apps/server/src/api-data/excel/excel.constants.ts new file mode 100644 index 000000000..aff67c2c0 --- /dev/null +++ b/apps/server/src/api-data/excel/excel.constants.ts @@ -0,0 +1 @@ +export const EXCEL_MIME = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'; diff --git a/apps/server/src/api-data/excel/excel.middleware.ts b/apps/server/src/api-data/excel/excel.middleware.ts index cee4c605b..7feb85f37 100644 --- a/apps/server/src/api-data/excel/excel.middleware.ts +++ b/apps/server/src/api-data/excel/excel.middleware.ts @@ -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)) { diff --git a/apps/server/src/api-data/excel/excel.parser.ts b/apps/server/src/api-data/excel/excel.parser.ts index 8226ea917..5d367a081 100644 --- a/apps/server/src/api-data/excel/excel.parser.ts +++ b/apps/server/src/api-data/excel/excel.parser.ts @@ -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]; diff --git a/apps/server/src/api-data/excel/excel.router.ts b/apps/server/src/api-data/excel/excel.router.ts index cce0a46e4..d27d8dc8a 100644 --- a/apps/server/src/api-data/excel/excel.router.ts +++ b/apps/server/src/api-data/excel/excel.router.ts @@ -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' }); + } +}); diff --git a/apps/server/src/api-data/excel/excel.service.ts b/apps/server/src/api-data/excel/excel.service.ts index 92794c23a..40b770d1f 100644 --- a/apps/server/src/api-data/excel/excel.service.ts +++ b/apps/server/src/api-data/excel/excel.service.ts @@ -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' }); +} diff --git a/apps/server/src/api-data/excel/excel.utils.ts b/apps/server/src/api-data/excel/excel.utils.ts index 428d85c2a..1b9c2efa9 100644 --- a/apps/server/src/api-data/excel/excel.utils.ts +++ b/apps/server/src/api-data/excel/excel.utils.ts @@ -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); +} diff --git a/apps/server/src/api-data/excel/excel.validation.ts b/apps/server/src/api-data/excel/excel.validation.ts index 48924a45b..84fa02f6c 100644 --- a/apps/server/src/api-data/excel/excel.validation.ts +++ b/apps/server/src/api-data/excel/excel.validation.ts @@ -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]; diff --git a/apps/server/src/services/sheet-service/SheetService.ts b/apps/server/src/services/sheet-service/SheetService.ts index 5a52675f4..20018fd98 100644 --- a/apps/server/src/services/sheet-service/SheetService.ts +++ b/apps/server/src/services/sheet-service/SheetService.ts @@ -8,6 +8,7 @@ import { AuthenticationStatus, CustomFields, DatabaseModel, + EntryId, isOntimeEvent, isOntimeMilestone, LogOrigin, @@ -360,7 +361,7 @@ export async function upload(sheetId: string, options: ImportMap) { const rundown = getCurrentRundown(); const sheetOrder: string[] = []; - let prevGroup: string | null = null; + let prevGroup: EntryId | null = null; for (const id of rundown.flatOrder) { const entry = rundown.entries[id]; @@ -418,12 +419,12 @@ export async function upload(sheetId: string, options: ImportMap) { const isGroupEnd = entryId.startsWith('group-end-'); const id = isGroupEnd ? entryId.split('group-end-')[1] : entryId; const entry = isGroupEnd - ? ({ id: entryId, type: SupportedEntry.Group } as OntimeGroup) - : structuredClone(rundown.entries[id]); + ? ({ id: entryId, type: SupportedEntry.Group } as OntimeGroup) + : structuredClone(rundown.entries[id]); updateRundown.push(cellRequestFromEvent(entry, index, worksheetId, sheetMetadata)); }); } catch (e) { - throw new Error(`Sheet write failed to correctly parse rundown: ${e}`) + throw new Error(`Sheet write failed to correctly parse rundown: ${e}`); } const writeResponse = await sheets({ version: 'v4', auth: currentAuthClient }).spreadsheets.batchUpdate({ diff --git a/packages/types/src/definitions/DataModel.type.ts b/packages/types/src/definitions/DataModel.type.ts index 51533d967..481a78eba 100644 --- a/packages/types/src/definitions/DataModel.type.ts +++ b/packages/types/src/definitions/DataModel.type.ts @@ -1,4 +1,3 @@ - import type { AutomationSettings } from './core/Automation.type.js'; import type { CustomFields } from './core/CustomFields.type.js'; import type { ProjectData } from './core/ProjectData.type.js'; diff --git a/packages/utils/src/feature/spreadsheet-import/__tests__/spreadsheetImport.test.ts b/packages/utils/src/feature/spreadsheet-import/__tests__/spreadsheetImport.test.ts index 7b84a1621..6a65e91ec 100644 --- a/packages/utils/src/feature/spreadsheet-import/__tests__/spreadsheetImport.test.ts +++ b/packages/utils/src/feature/spreadsheet-import/__tests__/spreadsheetImport.test.ts @@ -21,7 +21,7 @@ describe('isImportMap()', () => { timeWarning: 'warning time', timeDanger: 'danger time', custom: {}, - entryId: 'id', + id: 'id', }; expect(isImportMap(importMap)).toBe(true); @@ -72,7 +72,7 @@ describe('isImportMap()', () => { userDefined: 'userDefined', anotherOne: 'anotherOne', }, - entryId: 'id', + id: 'id', }; expect(isImportMap(importMap)).toBe(true); diff --git a/packages/utils/src/feature/spreadsheet-import/spreadsheetImport.ts b/packages/utils/src/feature/spreadsheet-import/spreadsheetImport.ts index b227e89c8..e131ecc7e 100644 --- a/packages/utils/src/feature/spreadsheet-import/spreadsheetImport.ts +++ b/packages/utils/src/feature/spreadsheet-import/spreadsheetImport.ts @@ -21,7 +21,7 @@ export const defaultImportMap = { timeWarning: 'warning time', timeDanger: 'danger time', custom: {}, - entryId: 'id', + id: 'id', }; /**