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
This commit is contained in:
Alex Christoffer Rasmussen
2024-10-22 19:13:07 +02:00
committed by GitHub
parent 59c1c77d48
commit 42984eed87
8 changed files with 211 additions and 135 deletions
@@ -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;
},
})}
/>
@@ -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 (
<Panel.Table>
@@ -44,8 +45,8 @@ export default function PreviewRundown(props: PreviewRundownProps) {
<th>Colour</th>
<th>Timer Type</th>
<th>End Action</th>
{fieldHeaders.map((field) => (
<th key={field}>{field}</th>
{fieldLabels.map((label) => (
<th key={label}>{label}</th>
))}
</tr>
</thead>
@@ -102,7 +103,7 @@ export default function PreviewRundown(props: PreviewRundownProps) {
<Tag>{event.endAction}</Tag>
</td>
{isOntimeEvent(event) &&
fieldHeaders.map((field) => {
fieldKeys.map((field) => {
let value = '';
if (field in event.custom) {
value = event.custom[field];
+2 -2
View File
@@ -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",
@@ -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 };
}
@@ -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<sheets_v4.Schema$Request>();
@@ -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');
+78 -16
View File
@@ -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 <importKey, ontimeKey>
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 <importKey, ontimeKey>
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
+14 -5
View File
@@ -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<keyof CustomFields, string>;
} {
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<ImportMap>): ExcelData => {
export const parseExcel = (
excelData: unknown[][],
existingCustomFields: CustomFields,
options?: Partial<ImportMap>,
): ExcelData => {
const rundownMetadata = {};
const importMap: ImportMap = { ...defaultImportMap, ...options };
@@ -89,7 +98,7 @@ export const parseExcel = (excelData: unknown[][], options?: Partial<ImportMap>)
}
}
const { customFields, customFieldImportKeys } = getCustomFieldData(importMap);
const { customFields, customFieldImportKeys } = getCustomFieldData(importMap, existingCustomFields);
const rundown: OntimeRundown = [];
// title stuff: strings
+93 -93
View File
@@ -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: {}