From 42984eed87287969d1cc95515dd539450b5bf28a Mon Sep 17 00:00:00 2001 From: Alex Christoffer Rasmussen Date: Tue, 22 Oct 2024 19:13:07 +0200 Subject: [PATCH] Improve xlsx import (#1271) * use xlsx * add existing custom fields to import map * keep colours of existing custom fields * use unknown * fix * revert * allow space and upper case * fix test * define empty sheet --- .../import-map/ImportMapForm.tsx | 10 +- .../sources-panel/preview/PreviewRundown.tsx | 9 +- apps/server/package.json | 4 +- .../src/api-data/excel/excel.service.ts | 19 +- .../services/sheet-service/SheetService.ts | 5 +- .../server/src/utils/__tests__/parser.test.ts | 94 +++++++-- apps/server/src/utils/parser.ts | 19 +- pnpm-lock.yaml | 186 +++++++++--------- 8 files changed, 211 insertions(+), 135 deletions(-) diff --git a/apps/client/src/features/app-settings/panel/sources-panel/import-map/ImportMapForm.tsx b/apps/client/src/features/app-settings/panel/sources-panel/import-map/ImportMapForm.tsx index f04ad5403..87467ee59 100644 --- a/apps/client/src/features/app-settings/panel/sources-panel/import-map/ImportMapForm.tsx +++ b/apps/client/src/features/app-settings/panel/sources-panel/import-map/ImportMapForm.tsx @@ -3,9 +3,8 @@ import { useFieldArray, useForm } from 'react-hook-form'; import { Button, IconButton, Input, Select, Tooltip } from '@chakra-ui/react'; import { IoAdd } from '@react-icons/all-files/io5/IoAdd'; import { IoTrash } from '@react-icons/all-files/io5/IoTrash'; -import { ImportMap } from 'ontime-utils'; +import { ImportMap, isAlphanumericWithSpace } from 'ontime-utils'; -import { isAlphanumeric } from '../../../../../common/utils/regex'; import * as Panel from '../../PanelUtils'; import useGoogleSheet from '../useGoogleSheet'; import { useSheetStore } from '../useSheetStore'; @@ -190,9 +189,10 @@ export default function ImportMapForm(props: ImportMapFormProps) { defaultValue={ontimeName} placeholder='Name of the field as shown in Ontime' {...register(`custom.${index}.ontimeName`, { - pattern: { - value: isAlphanumeric, - message: 'Custom field name must be alphanumeric', + validate: (value) => { + if (!isAlphanumericWithSpace(value)) + return 'Only alphanumeric characters and space are allowed'; + return true; }, })} /> diff --git a/apps/client/src/features/app-settings/panel/sources-panel/preview/PreviewRundown.tsx b/apps/client/src/features/app-settings/panel/sources-panel/preview/PreviewRundown.tsx index 0a53989cf..ddb40bc6a 100644 --- a/apps/client/src/features/app-settings/panel/sources-panel/preview/PreviewRundown.tsx +++ b/apps/client/src/features/app-settings/panel/sources-panel/preview/PreviewRundown.tsx @@ -24,7 +24,8 @@ export default function PreviewRundown(props: PreviewRundownProps) { // we only count Ontime Events which are 1 based in client let eventIndex = 0; - const fieldHeaders = Object.keys(customFields); + const fieldKeys = Object.keys(customFields); + const fieldLabels = fieldKeys.map((key) => customFields[key].label); return ( @@ -44,8 +45,8 @@ export default function PreviewRundown(props: PreviewRundownProps) { Colour Timer Type End Action - {fieldHeaders.map((field) => ( - {field} + {fieldLabels.map((label) => ( + {label} ))} @@ -102,7 +103,7 @@ export default function PreviewRundown(props: PreviewRundownProps) { {event.endAction} {isOntimeEvent(event) && - fieldHeaders.map((field) => { + fieldKeys.map((field) => { let value = ''; if (field in event.custom) { value = event.custom[field]; diff --git a/apps/server/package.json b/apps/server/package.json index c195bde98..3a03d15f8 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -17,11 +17,11 @@ "lowdb": "^7.0.1", "multer": "^1.4.5-lts.1", "node-osc": "^9.0.2", - "node-xlsx": "^0.23.0", "ontime-utils": "workspace:*", "sanitize-filename": "^1.6.3", "steno": "^4.0.2", - "ws": "^8.13.0" + "ws": "^8.13.0", + "xlsx": "^0.18.5" }, "devDependencies": { "@types/cors": "^2.8.17", diff --git a/apps/server/src/api-data/excel/excel.service.ts b/apps/server/src/api-data/excel/excel.service.ts index 5123c820c..04000b05b 100644 --- a/apps/server/src/api-data/excel/excel.service.ts +++ b/apps/server/src/api-data/excel/excel.service.ts @@ -8,13 +8,15 @@ import { ImportMap } from 'ontime-utils'; import { extname } from 'path'; import { existsSync } from 'fs'; -import xlsx from 'node-xlsx'; +import xlsx from 'xlsx'; +import type { WorkBook } from 'xlsx'; import { parseExcel } from '../../utils/parser.js'; import { parseRundown } from '../../utils/parserFunctions.js'; import { deleteFile } from '../../utils/parserUtils.js'; +import { getCustomFields } from '../../services/rundown-service/rundownCache.js'; -let excelData: { name: string; data: unknown[][] }[] = []; +let excelData: WorkBook = xlsx.utils.book_new(); export async function saveExcelFile(filePath: string) { if (!existsSync(filePath)) { @@ -23,24 +25,25 @@ export async function saveExcelFile(filePath: string) { if (extname(filePath) != '.xlsx') { throw new Error('Wrong file format'); } - excelData = xlsx.parse(filePath, { cellDates: true }); + excelData = xlsx.readFile(filePath, { cellDates: true, cellFormula: false }); await deleteFile(filePath); } -export function listWorksheets() { - return excelData.map((value) => value.name); +export function listWorksheets(): string[] { + return excelData.SheetNames; } export function generateRundownPreview(options: ImportMap): { rundown: OntimeRundown; customFields: CustomFields } { - const data = excelData.find(({ name }) => name.toLowerCase() === options.worksheet.toLowerCase())?.data; + const data = excelData.Sheets[options.worksheet]; if (!data) { throw new Error(`Could not find data to import, maybe the worksheet name is incorrect: ${options.worksheet}`); } - const dataFromExcel = parseExcel(data, options); + const arrayOfData: unknown[][] = xlsx.utils.sheet_to_json(data, { header: 1, blankrows: false, raw: false }); + const dataFromExcel = parseExcel(arrayOfData, getCustomFields(), options); // we run the parsed data through an extra step to ensure the objects shape const { rundown, customFields } = parseRundown(dataFromExcel); if (rundown.length === 0) { @@ -48,7 +51,7 @@ export function generateRundownPreview(options: ImportMap): { rundown: OntimeRun } // clear the data - excelData = []; + excelData = undefined; return { rundown, customFields }; } diff --git a/apps/server/src/services/sheet-service/SheetService.ts b/apps/server/src/services/sheet-service/SheetService.ts index bc237d6ba..f675e1a1a 100644 --- a/apps/server/src/services/sheet-service/SheetService.ts +++ b/apps/server/src/services/sheet-service/SheetService.ts @@ -15,6 +15,7 @@ import { parseExcel } from '../../utils/parser.js'; import { logger } from '../../classes/Logger.js'; import { parseRundown } from '../../utils/parserFunctions.js'; import { getRundown } from '../rundown-service/rundownUtils.js'; +import { getCustomFields } from '../rundown-service/rundownCache.js'; import { cellRequestFromEvent, type ClientSecret, getA1Notation, validateClientSecret } from './sheetUtils.js'; @@ -286,7 +287,7 @@ export async function upload(sheetId: string, options: ImportMap) { throw new Error(`Sheet read failed: ${readResponse.statusText}`); } - const { rundownMetadata } = parseExcel(readResponse.data.values, options); + const { rundownMetadata } = parseExcel(readResponse.data.values, getCustomFields(), options); const rundown = getRundown(); const titleRow = Object.values(rundownMetadata)[0]['row']; const updateRundown = Array(); @@ -363,7 +364,7 @@ export async function download( throw new Error(`Sheet read failed: ${googleResponse.statusText}`); } - const dataFromSheet = parseExcel(googleResponse.data.values, options); + const dataFromSheet = parseExcel(googleResponse.data.values, getCustomFields(), options); const { customFields, rundown } = parseRundown(dataFromSheet); if (rundown.length < 1) { throw new Error('Sheet: Could not find data to import in the worksheet'); diff --git a/apps/server/src/utils/__tests__/parser.test.ts b/apps/server/src/utils/__tests__/parser.test.ts index b7cbc9863..dc9d765e0 100644 --- a/apps/server/src/utils/__tests__/parser.test.ts +++ b/apps/server/src/utils/__tests__/parser.test.ts @@ -2,6 +2,7 @@ import { assertType, vi } from 'vitest'; import { + CustomFields, DatabaseModel, EndAction, OntimeEvent, @@ -788,7 +789,7 @@ describe('getCustomFieldData()', () => { }, } as ImportMap; - const result = getCustomFieldData(importMap); + const result = getCustomFieldData(importMap, {}); expect(result.customFields).toStrictEqual({ lighting: { type: 'string', @@ -807,6 +808,61 @@ describe('getCustomFieldData()', () => { }, }); + // it is an inverted record of + expect(result.customFieldImportKeys).toStrictEqual({ + lx: 'lighting', + sound: 'sound', + av: 'video', + }); + }); + it('keeps colour information from existing fields', () => { + const importMap = { + worksheet: 'event schedule', + timeStart: 'time start', + linkStart: 'link start', + timeEnd: 'time end', + duration: 'duration', + cue: 'cue', + title: 'title', + isPublic: 'public', + skip: 'skip', + note: 'notes', + colour: 'colour', + endAction: 'end action', + timerType: 'timer type', + timeWarning: 'warning time', + timeDanger: 'danger time', + custom: { + lighting: 'lx', + sound: 'sound', + video: 'av', + }, + } as ImportMap; + + const customFields: CustomFields = { + lighting: { label: 'lx', type: 'string', colour: 'red' }, + sound: { label: 'sound', type: 'string', colour: 'green' }, + }; + + const result = getCustomFieldData(importMap, customFields); + expect(result.customFields).toStrictEqual({ + lighting: { + type: 'string', + colour: 'red', + label: 'lighting', + }, + sound: { + type: 'string', + colour: 'green', + label: 'sound', + }, + video: { + type: 'string', + colour: '', + label: 'video', + }, + }); + // it is an inverted record of expect(result.customFieldImportKeys).toStrictEqual({ lx: 'lighting', @@ -919,7 +975,7 @@ describe('parseExcel()', () => { note: 'Ballyhoo', custom: { user0: 'a0', - user1: 'a1', + User1: 'a1', user2: 'a2', user3: 'a3', user4: 'a4', @@ -952,21 +1008,27 @@ describe('parseExcel()', () => { }, ]; - const parsedData = parseExcel(testdata, importMap); + const existingCustomFields: CustomFields = { + user0: { type: 'string', colour: 'red', label: 'user0' }, + User1: { type: 'string', colour: 'green', label: 'user1' }, + user2: { type: 'string', colour: 'blue', label: 'user2' }, + }; + + const parsedData = parseExcel(testdata, existingCustomFields, importMap); expect(parsedData.customFields).toStrictEqual({ user0: { type: 'string', - colour: '', + colour: 'red', label: 'user0', }, - user1: { + User1: { type: 'string', - colour: '', + colour: 'green', label: 'User1', }, user2: { type: 'string', - colour: '', + colour: 'blue', label: 'user2', }, user3: { @@ -1123,7 +1185,7 @@ describe('parseExcel()', () => { }, ]; - const parsedData = parseExcel(testdata, importMap); + const parsedData = parseExcel(testdata, {}, importMap); expect(parsedData.customFields).toStrictEqual({ niu1: { type: 'string', @@ -1229,7 +1291,7 @@ describe('parseExcel()', () => { timeDanger: 'danger time', custom: {}, }; - const result = parseExcel(testdata, importMap); + const result = parseExcel(testdata, {}, importMap); expect(result.rundown.length).toBe(1); expect((result.rundown.at(0) as OntimeEvent).title).toBe('A song from the hearth'); }); @@ -1322,7 +1384,7 @@ describe('parseExcel()', () => { timeDanger: 'danger time', custom: {}, }; - const result = parseExcel(testdata, importMap); + const result = parseExcel(testdata, {}, importMap); expect(result.rundown.length).toBe(2); expect((result.rundown.at(0) as OntimeEvent).type).toBe(SupportedEvent.Block); }); @@ -1392,7 +1454,7 @@ describe('parseExcel()', () => { timeDanger: 'danger time', custom: {}, }; - const result = parseExcel(testdata, importMap); + const result = parseExcel(testdata, {}, importMap); expect(result.rundown.length).toBe(2); expect((result.rundown.at(0) as OntimeEvent).type).toBe(SupportedEvent.Event); expect((result.rundown.at(0) as OntimeEvent).timerType).toBe(TimerType.CountDown); @@ -1510,7 +1572,7 @@ describe('parseExcel()', () => { timeDanger: 'danger time', custom: {}, }; - const result = parseExcel(testdata, importMap); + const result = parseExcel(testdata, {}, importMap); expect(result.rundown.length).toBe(3); expect((result.rundown.at(0) as OntimeEvent).type).toBe(SupportedEvent.Event); expect((result.rundown.at(0) as OntimeEvent).timerType).toBe(TimerType.CountDown); @@ -1551,7 +1613,7 @@ describe('parseExcel()', () => { timeDanger: 'danger time', custom: {}, }; - const result = parseExcel(testData, importMap); + const result = parseExcel(testData, {}, importMap); const { rundown } = parseRundown(result); const events = rundown.filter((e) => e.type === SupportedEvent.Event) as OntimeEvent[]; expect((events.at(0) as OntimeEvent).timeStart).toEqual(16200000); @@ -1588,7 +1650,7 @@ describe('parseExcel()', () => { custom: {}, }; - const result = parseExcel(testData, importMap); + const result = parseExcel(testData, {}, importMap); const { rundown } = parseRundown(result); const events = rundown.filter((e) => e.type === SupportedEvent.Event) as OntimeEvent[]; expect((events.at(0) as OntimeEvent).timeStart).toEqual(16200000); //<--leading white space in MAP @@ -1640,7 +1702,7 @@ describe('parseExcel()', () => { custom: {}, }; - const result = parseExcel(testData, importMap); + const result = parseExcel(testData, {}, importMap); const parseResult = parseRundown(result); cache.init(parseResult.rundown, parseResult.customFields); @@ -1774,7 +1836,7 @@ describe('parseExcel()', () => { ['MEET4', '#779BE7', '', '', 30, true, 'Meeting 4', '', 'count-up', 'none', 11, '00:05:00', 'TRUE', 'FALSE'], ]; - const parsedData = parseExcel(testData); + const parsedData = parseExcel(testData, {}); const { rundown } = parsedData; // elements in bug report diff --git a/apps/server/src/utils/parser.ts b/apps/server/src/utils/parser.ts index bf4dd6f44..db410bfb6 100644 --- a/apps/server/src/utils/parser.ts +++ b/apps/server/src/utils/parser.ts @@ -1,4 +1,5 @@ import { + customFieldLabelToKey, defaultImportMap, generateId, type ImportMap, @@ -54,18 +55,22 @@ function parseBooleanString(value: unknown): boolean { return value.toLowerCase() !== 'false'; } -export function getCustomFieldData(importMap: ImportMap): { +export function getCustomFieldData( + importMap: ImportMap, + existingCustomFields: CustomFields, +): { customFields: CustomFields; customFieldImportKeys: Record; } { const customFields = {}; const customFieldImportKeys = {}; for (const ontimeLabel in importMap.custom) { - const ontimeKey = ontimeLabel.toLowerCase(); + const ontimeKey = customFieldLabelToKey(ontimeLabel); const importLabel = importMap.custom[ontimeLabel].toLowerCase(); + const colour = ontimeKey in existingCustomFields ? existingCustomFields[ontimeKey].colour : ''; customFields[ontimeKey] = { type: 'string', - colour: '', + colour, label: ontimeLabel, }; customFieldImportKeys[importLabel] = ontimeKey; @@ -79,7 +84,11 @@ export function getCustomFieldData(importMap: ImportMap): { * @param {ImportOptions} options - an object that contains the import map * @returns {object} - parsed object */ -export const parseExcel = (excelData: unknown[][], options?: Partial): ExcelData => { +export const parseExcel = ( + excelData: unknown[][], + existingCustomFields: CustomFields, + options?: Partial, +): ExcelData => { const rundownMetadata = {}; const importMap: ImportMap = { ...defaultImportMap, ...options }; @@ -89,7 +98,7 @@ export const parseExcel = (excelData: unknown[][], options?: Partial) } } - const { customFields, customFieldImportKeys } = getCustomFieldData(importMap); + const { customFields, customFieldImportKeys } = getCustomFieldData(importMap, existingCustomFields); const rundown: OntimeRundown = []; // title stuff: strings diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6acde450e..cd15b7057 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4,6 +4,30 @@ settings: autoInstallPeers: true excludeLinksFromLockfile: false +catalogs: + default: + '@types/node': + specifier: 20.14.10 + version: 20.14.10 + '@typescript-eslint/eslint-plugin': + specifier: 7.16.1 + version: 7.16.1 + '@typescript-eslint/parser': + specifier: 7.16.1 + version: 7.16.1 + eslint: + specifier: 8.56.0 + version: 8.56.0 + eslint-plugin-prettier: + specifier: 5.1.3 + version: 5.1.3 + prettier: + specifier: 3.3.1 + version: 3.3.1 + typescript: + specifier: 5.5.3 + version: 5.5.3 + importers: .: @@ -291,9 +315,6 @@ importers: node-osc: specifier: ^9.0.2 version: 9.0.2 - node-xlsx: - specifier: ^0.23.0 - version: 0.23.0 ontime-utils: specifier: workspace:* version: link:../../packages/utils @@ -306,6 +327,9 @@ importers: ws: specifier: ^8.13.0 version: 8.13.0 + xlsx: + specifier: ^0.18.5 + version: 0.18.5 devDependencies: '@types/cors': specifier: ^2.8.17 @@ -2358,6 +2382,7 @@ packages: abab@2.0.6: resolution: {integrity: sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==} + deprecated: Use your platform's native atob() and btoa() methods instead accepts@1.3.8: resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} @@ -2371,10 +2396,6 @@ packages: peerDependencies: acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 - acorn-walk@8.3.1: - resolution: {integrity: sha512-TgUZgYvqZprrl7YldZNoa9OciCAyZR+Ejm9eXzKCmjsF5IKp/wgQ7Z/ZpjpGTIUPwrHQIcYeI8qDh4PsEwxMbw==} - engines: {node: '>=0.4.0'} - acorn-walk@8.3.2: resolution: {integrity: sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==} engines: {node: '>=0.4.0'} @@ -2384,15 +2405,9 @@ packages: engines: {node: '>=0.4.0'} hasBin: true - acorn@8.8.1: - resolution: {integrity: sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA==} - engines: {node: '>=0.4.0'} - hasBin: true - - acorn@8.8.2: - resolution: {integrity: sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==} - engines: {node: '>=0.4.0'} - hasBin: true + adler-32@1.3.1: + resolution: {integrity: sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==} + engines: {node: '>=0.8'} agent-base@6.0.2: resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} @@ -2651,6 +2666,10 @@ packages: caniuse-lite@1.0.30001570: resolution: {integrity: sha512-+3e0ASu4sw1SWaoCtvPeyXp+5PsjigkSt8OXZbF9StH5pQWbxEjLAZE3n8Aup5udop1uRiKA7a4utUk/uoSpUw==} + cfb@1.2.2: + resolution: {integrity: sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==} + engines: {node: '>=0.8'} + chai@4.3.10: resolution: {integrity: sha512-0UXG04VuVbruMUYbJ6JctvH0YnC/4q3/AkT18q4NaITo91CUm0liMS9VqzT9vZhVQ/1eqPanMWjBM+Juhfb/9g==} engines: {node: '>=4'} @@ -2700,6 +2719,10 @@ packages: clone-response@1.0.3: resolution: {integrity: sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==} + codepage@1.15.0: + resolution: {integrity: sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==} + engines: {node: '>=0.8'} + color-convert@1.9.3: resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} @@ -2974,6 +2997,7 @@ packages: domexception@4.0.0: resolution: {integrity: sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==} engines: {node: '>=12'} + deprecated: Use your platform's native DOMException instead dot-case@3.0.4: resolution: {integrity: sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==} @@ -3033,10 +3057,6 @@ packages: end-of-stream@1.4.4: resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==} - entities@4.4.0: - resolution: {integrity: sha512-oYp7156SP8LkeGD0GF85ad1X9Ai79WtRsZ2gxJqtBuzH+98YUV6jkHEKlZkMbcrjJjIVJNIDP/3WL9wQkoPbWA==} - engines: {node: '>=0.12'} - entities@4.5.0: resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} engines: {node: '>=0.12'} @@ -3369,6 +3389,10 @@ packages: resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} engines: {node: '>= 0.6'} + frac@1.1.2: + resolution: {integrity: sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==} + engines: {node: '>=0.8'} + framer-motion@10.11.2: resolution: {integrity: sha512-IrwuC9regNOU99JoM/Z62CAMA3awGV6AcF7e3bcgXk/ZoNlGSt5aVq0J7UAwtLmCkwVlRvBkiMnvv2mZ1GW2pg==} peerDependencies: @@ -3487,10 +3511,12 @@ packages: glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + deprecated: Glob versions prior to v9 are no longer supported glob@8.1.0: resolution: {integrity: sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==} engines: {node: '>=12'} + deprecated: Glob versions prior to v9 are no longer supported glob@9.3.2: resolution: {integrity: sha512-BTv/JhKXFEHsErMte/AnfiSv8yYOLLiyH2lTg8vn02O21zWFgHPTfxtgn1QRe7NRgggUhC8hacR2Re94svHqeA==} @@ -4178,11 +4204,6 @@ packages: node-releases@2.0.14: resolution: {integrity: sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==} - node-xlsx@0.23.0: - resolution: {integrity: sha512-r3KaSZSsSrK92rbPXnX/vDdxURmPPik0rjJ3A+Pybzpjyrk4G6WyGfj8JIz5dMMEpCmWVpmO4qoVPBxnpLv/8Q==} - engines: {node: '>=10.0.0'} - hasBin: true - normalize-path@3.0.0: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} engines: {node: '>=0.10.0'} @@ -4779,10 +4800,6 @@ packages: snake-case@3.0.4: resolution: {integrity: sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==} - source-map-js@1.0.2: - resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==} - engines: {node: '>=0.10.0'} - source-map-js@1.2.0: resolution: {integrity: sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==} engines: {node: '>=0.10.0'} @@ -4801,6 +4818,10 @@ packages: sprintf-js@1.1.3: resolution: {integrity: sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==} + ssf@0.11.2: + resolution: {integrity: sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==} + engines: {node: '>=0.8'} + stack-utils@2.0.6: resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} engines: {node: '>=10'} @@ -5315,10 +5336,18 @@ packages: engines: {node: '>=8'} hasBin: true + wmf@1.0.2: + resolution: {integrity: sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==} + engines: {node: '>=0.8'} + word-wrap@1.2.3: resolution: {integrity: sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==} engines: {node: '>=0.10.0'} + word@0.3.0: + resolution: {integrity: sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==} + engines: {node: '>=0.8'} + wrap-ansi@7.0.0: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} engines: {node: '>=10'} @@ -5326,18 +5355,6 @@ packages: wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} - ws@8.12.0: - resolution: {integrity: sha512-kU62emKIdKVeEIOIKVegvqpXMSTAMLJozpHZaJNDYqBjzlSYXQGviYwN1osDLJ9av68qHd4a2oSjd7yD4pacig==} - engines: {node: '>=10.0.0'} - peerDependencies: - bufferutil: ^4.0.1 - utf-8-validate: '>=5.0.2' - peerDependenciesMeta: - bufferutil: - optional: true - utf-8-validate: - optional: true - ws@8.13.0: resolution: {integrity: sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==} engines: {node: '>=10.0.0'} @@ -5350,9 +5367,8 @@ packages: utf-8-validate: optional: true - xlsx@https://cdn.sheetjs.com/xlsx-0.19.3/xlsx-0.19.3.tgz: - resolution: {tarball: https://cdn.sheetjs.com/xlsx-0.19.3/xlsx-0.19.3.tgz} - version: 0.19.3 + xlsx@0.18.5: + resolution: {integrity: sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==} engines: {node: '>=0.8'} hasBin: true @@ -5427,33 +5443,6 @@ packages: react: optional: true -catalogs: - default: - '@types/node': - specifier: 20.14.10 - version: 20.14.10 - '@typescript-eslint/eslint-plugin': - specifier: 7.16.1 - version: 7.16.1 - '@typescript-eslint/parser': - specifier: 7.16.1 - version: 7.16.1 - eslint: - specifier: 8.56.0 - version: 8.56.0 - eslint-config-prettier: - specifier: 9.1.0 - version: 9.1.0 - eslint-plugin-prettier: - specifier: 5.1.3 - version: 5.1.3 - prettier: - specifier: 3.3.1 - version: 3.3.1 - typescript: - specifier: 5.5.3 - version: 5.5.3 - snapshots: 7zip-bin@5.2.0: {} @@ -7633,22 +7622,18 @@ snapshots: acorn-globals@7.0.1: dependencies: - acorn: 8.8.2 - acorn-walk: 8.3.1 + acorn: 8.11.2 + acorn-walk: 8.3.2 acorn-jsx@5.3.2(acorn@8.11.2): dependencies: acorn: 8.11.2 - acorn-walk@8.3.1: {} - acorn-walk@8.3.2: {} acorn@8.11.2: {} - acorn@8.8.1: {} - - acorn@8.8.2: {} + adler-32@1.3.1: {} agent-base@6.0.2: dependencies: @@ -8014,6 +7999,11 @@ snapshots: caniuse-lite@1.0.30001570: {} + cfb@1.2.2: + dependencies: + adler-32: 1.3.1 + crc-32: 1.2.2 + chai@4.3.10: dependencies: assertion-error: 1.1.0 @@ -8080,6 +8070,8 @@ snapshots: dependencies: mimic-response: 1.0.1 + codepage@1.15.0: {} + color-convert@1.9.3: dependencies: color-name: 1.1.3 @@ -8449,8 +8441,6 @@ snapshots: dependencies: once: 1.4.0 - entities@4.4.0: {} - entities@4.5.0: {} env-paths@2.2.1: {} @@ -8953,6 +8943,8 @@ snapshots: forwarded@0.2.0: {} + frac@1.1.2: {} + framer-motion@10.11.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: tslib: 2.5.0 @@ -9509,7 +9501,7 @@ snapshots: jsdom@21.1.0: dependencies: abab: 2.0.6 - acorn: 8.8.1 + acorn: 8.11.2 acorn-globals: 7.0.1 cssom: 0.5.0 cssstyle: 2.3.0 @@ -9532,7 +9524,7 @@ snapshots: whatwg-encoding: 2.0.0 whatwg-mimetype: 3.0.0 whatwg-url: 11.0.0 - ws: 8.12.0 + ws: 8.13.0 xml-name-validator: 4.0.0 transitivePeerDependencies: - bufferutil @@ -9809,10 +9801,6 @@ snapshots: node-releases@2.0.14: {} - node-xlsx@0.23.0: - dependencies: - xlsx: https://cdn.sheetjs.com/xlsx-0.19.3/xlsx-0.19.3.tgz - normalize-path@3.0.0: {} normalize-url@6.1.0: {} @@ -9931,7 +9919,7 @@ snapshots: parse5@7.1.2: dependencies: - entities: 4.4.0 + entities: 4.5.0 parseurl@1.3.3: {} @@ -10328,7 +10316,7 @@ snapshots: dependencies: chokidar: 3.5.3 immutable: 4.2.2 - source-map-js: 1.0.2 + source-map-js: 1.2.0 sax@1.3.0: {} @@ -10444,8 +10432,6 @@ snapshots: dot-case: 3.0.4 tslib: 2.6.2 - source-map-js@1.0.2: {} - source-map-js@1.2.0: {} source-map-support@0.5.21: @@ -10460,6 +10446,10 @@ snapshots: sprintf-js@1.1.3: optional: true + ssf@0.11.2: + dependencies: + frac: 1.1.2 + stack-utils@2.0.6: dependencies: escape-string-regexp: 2.0.0 @@ -10969,8 +10959,12 @@ snapshots: siginfo: 2.0.0 stackback: 0.0.2 + wmf@1.0.2: {} + word-wrap@1.2.3: {} + word@0.3.0: {} + wrap-ansi@7.0.0: dependencies: ansi-styles: 4.3.0 @@ -10979,11 +10973,17 @@ snapshots: wrappy@1.0.2: {} - ws@8.12.0: {} - ws@8.13.0: {} - xlsx@https://cdn.sheetjs.com/xlsx-0.19.3/xlsx-0.19.3.tgz: {} + xlsx@0.18.5: + dependencies: + adler-32: 1.3.1 + cfb: 1.2.2 + codepage: 1.15.0 + crc-32: 1.2.2 + ssf: 0.11.2 + wmf: 1.0.2 + word: 0.3.0 xml-name-validator@4.0.0: {}