diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/hooks.ts b/specifyweb/frontend/js_src/lib/components/WorkBench/hooks.ts index a5ed8be0fab..4ed3896e7f8 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/hooks.ts +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/hooks.ts @@ -1,4 +1,5 @@ import type Handsontable from 'handsontable'; +import type { CellChange } from 'handsontable/common'; import type { Events } from 'handsontable/pluginHooks'; import type { Action } from 'handsontable/plugins/undoRedo'; import React from 'react'; @@ -33,31 +34,46 @@ export function useHotHooks({ readonly isReadOnly: boolean; readonly isResultsOpen: boolean; }): Partial { - let sortConfigIsSet: boolean = false; + let sortConfigIsSet = false; const loading = React.useContext(LoadingContext); + const validateWorkbenchState = (_context: string): boolean => + Boolean( + workbench?.hot && + workbench?.dataset && + Array.isArray(workbench.dataset.columns) && + workbench?.cells + ); + + const isMappedCol = React.useCallback( + (physicalCol: number): boolean => { + const mappingCol = physicalColToMappingCol(physicalCol); + return mappingCol !== undefined && mappingCol !== -1; + }, + [physicalColToMappingCol] + ); + return { - /* - * After cell is rendered, we need to reApply metaData classes - * NOTE: - * .issues are handled automatically by the comments plugin. - * This is why, afterRenderer only has to handle the isModified and isNew - * cases - * - */ - afterRenderer: (td, visualRow, visualCol, property, _value) => { - if (workbench.hot === undefined) { + afterInit () { + /* No-op (kept for parity) */ + }, + + afterRenderer: (td, visualRow, _visualCol, property) => { + if (!validateWorkbenchState('afterRenderer')) { td.classList.add('text-gray-500'); return; } - const physicalRow = workbench.hot.toPhysicalRow(visualRow); - const physicalCol = - typeof property === 'number' - ? property - : workbench.hot.toPhysicalColumn(visualCol); - if (physicalCol >= workbench.dataset.columns.length) return; + + const colCount = workbench.dataset.columns.length; + const physicalRow = workbench.hot!.toPhysicalRow(visualRow); + const vcol = workbench.hot!.propToCol(property as any); + const physicalCol = workbench.hot!.toPhysicalColumn(vcol); + + if (colCount === 0 || physicalCol >= colCount) return; + if (physicalRow < 0 || physicalCol < 0) return; + const metaArray = workbench.cells.cellMeta?.[physicalRow]?.[physicalCol]; - const cellMetaToUpdate: RA = [ + const toUpdate: RA = [ 'isModified', 'isNew', 'isSearchResult', @@ -65,55 +81,37 @@ export function useHotHooks({ 'isMatchedAndChanged', 'isDeleted', ]; - cellMetaToUpdate.forEach((metaType) => { - if (workbench.cells.getCellMetaFromArray(metaArray, metaType)) { - workbench.cells.runMetaUpdateEffects( - td, - metaType, - true, - visualRow, - visualCol - ); + + toUpdate.forEach((k) => { + if (workbench.cells.getCellMetaFromArray(metaArray, k)) { + workbench.cells.runMetaUpdateEffects(td, k, true, visualRow, vcol); } }); - if (workbench.mappings?.mappedHeaders?.[physicalCol] === undefined) + + if (workbench.mappings?.mappedHeaders?.[physicalCol] === undefined) { td.classList.add('text-gray-500'); - if (workbench.mappings?.coordinateColumns?.[physicalCol] !== undefined) + } + if (workbench.mappings?.coordinateColumns?.[physicalCol] !== undefined) { td.classList.add('wb-coordinate-cell'); + } }, - // Make HOT use defaultValues for validation if cell is empty beforeValidate: (value, _visualRow, property) => { - if (Boolean(value) || workbench.hot === undefined) return value; - - const visualCol = workbench.hot.propToCol(property); - const physicalCol = workbench.hot.toPhysicalColumn(visualCol); + if (Boolean(value) || !validateWorkbenchState('beforeValidate')) return value; - return workbench.mappings?.defaultValues[physicalCol] ?? value; + const visualCol = workbench.hot!.propToCol(property as any); + const physicalCol = workbench.hot!.toPhysicalColumn(visualCol); + return workbench.mappings?.defaultValues?.[physicalCol] ?? value; }, - afterValidate: ( - isValid, - value: string | null = '', - visualRow, - property - ) => { - if (workbench.hot === undefined) return; - const visualCol = workbench.hot.propToCol(property); - - const physicalRow = workbench.hot.toPhysicalRow(visualRow); - const physicalCol = workbench.hot.toPhysicalColumn(visualCol); - const issues = workbench.cells.getCellMeta( - physicalRow, - physicalCol, - 'issues' - ); - /* - * Don't duplicate failedParsingPickList message if both front-end and - * back-end identified the same issue. - * - * This is the only type of validation that is done on the front-end - */ + afterValidate: (isValid, value: string | null = '', visualRow, property) => { + if (!validateWorkbenchState('afterValidate')) return; + + const visualCol = workbench.hot!.propToCol(property as any); + const physicalRow = workbench.hot!.toPhysicalRow(visualRow); + const physicalCol = workbench.hot!.toPhysicalColumn(visualCol); + + const issues = workbench.cells.getCellMeta(physicalRow, physicalCol, 'issues'); const newIssues = f.unique([ ...(isValid ? [] @@ -127,53 +125,57 @@ export function useHotHooks({ ...issues.filter( (issue) => !issue.endsWith( - whitespaceSensitive( - backEndText.failedParsingPickList({ value: '' }) - ) + whitespaceSensitive(backEndText.failedParsingPickList({ value: '' })) ) ), ]); - if (JSON.stringify(issues) !== JSON.stringify(newIssues)) - workbench.cells.updateCellMeta( - physicalRow, - physicalCol, - 'issues', - newIssues - ); + + if (JSON.stringify(issues) !== JSON.stringify(newIssues)) { + workbench.cells.updateCellMeta(physicalRow, physicalCol, 'issues', newIssues); + } }, afterUndo: (data) => afterUndoRedo(workbench, 'undo', data), - afterRedo: (data) => afterUndoRedo(workbench, 'redo', data), + beforeCopy: (data, coords) => { + if (!validateWorkbenchState('beforeCopy')) return; + + coords.forEach((coord) => { + for (let row = coord.startRow; row <= coord.endRow; row++) { + const rowIndex = row - coord.startRow; + for (let col = coord.startCol; col <= coord.endCol; col++) { + const colIndex = col - coord.startCol; + const cellMeta = workbench.hot!.getCellMeta(row, col); + if (cellMeta?.renderer && cellMeta?.formattedValue) { + data[rowIndex][colIndex] = cellMeta.formattedValue; + } + } + } + }); + }, + beforePaste: () => !isReadOnly, - /* - * If copying values from a 1x3 area and pasting into the last cell, HOT - * would create 2 invisible columns) - * - * This intercepts Paste to prevent creation of these columns - * - * This logic wasn't be put into beforePaste because it receives - * arguments that are inconvenient to work with - * - */ beforeChange: (unfilteredChanges, source) => { if (source !== 'CopyPaste.paste') return true; + if (!validateWorkbenchState('beforeChange')) return false; - const filteredChanges = unfilteredChanges.filter( - ([, property]) => - (property as number) < workbench.dataset.columns.length - ); - if ( - filteredChanges.length === unfilteredChanges.length || - workbench.hot === undefined - ) - return true; - workbench.hot.setDataAtCell( + const colCount = workbench.dataset.columns.length; + + const filteredChanges = unfilteredChanges + .filter((change): change is CellChange => change !== null) + .filter(([, property]) => { + const vcol = workbench.hot!.propToCol(property as any); + return vcol < colCount; + }); + + if (filteredChanges.length === unfilteredChanges.length) return true; + + workbench.hot!.setDataAtCell( filteredChanges.map(([visualRow, property, _oldValue, newValue]) => [ visualRow, - workbench.hot!.propToCol(property as number), + workbench.hot!.propToCol(property as any), newValue, ]), 'CopyPaste.paste' @@ -182,99 +184,74 @@ export function useHotHooks({ }, afterChange: (unfilteredChanges, source) => { + const validSources = [ + 'edit', + 'CopyPaste.paste', + 'CopyPaste.cut', + 'Autofill.fill', + 'UndoRedo.undo', + 'UndoRedo.redo', + ]; if ( - ![ - 'edit', - 'CopyPaste.paste', - 'CopyPaste.cut', - 'Autofill.fill', - 'UndoRedo.undo', - 'UndoRedo.redo', - ].includes(source) || - workbench.hot === undefined || + !validSources.includes(source) || + !validateWorkbenchState('afterChange') || unfilteredChanges === null ) return; + + const colCount = workbench.dataset.columns.length; + const changes = unfilteredChanges - .map(([visualRow, property, oldValue, newValue]) => ({ - visualRow, - visualCol: workbench.hot!.propToCol(property), - physicalRow: workbench.hot!.toPhysicalRow(visualRow), - physicalCol: - typeof property === 'number' - ? property - : workbench.hot!.toPhysicalColumn( - workbench.hot!.propToCol(property as number | string) - ), - oldValue, - newValue, - })) + .map(([visualRow, property, oldValue, newValue]) => { + const visualCol = workbench.hot!.propToCol(property as any); + return { + visualRow, + visualCol, + physicalRow: workbench.hot!.toPhysicalRow(visualRow), + physicalCol: workbench.hot!.toPhysicalColumn(visualCol), + oldValue, + newValue, + }; + }) .filter( ({ oldValue, newValue, visualCol }) => - /* - * Ignore cases where value didn't change - * (happens when double click a cell and then click on another cell) - * - */ oldValue !== newValue && - // Or where value changed from null to empty (oldValue !== null || newValue !== '') && - // Or the column does not exist (that can happen on paste) - visualCol < workbench.dataset.columns.length + visualCol < colCount ); if (changes.length === 0) return; const changedRows = new Set( changes - // Ignore changes to unmapped columns - .filter( - ({ physicalCol }) => physicalColToMappingCol(physicalCol) !== -1 - ) + .filter(({ physicalCol }) => isMappedCol(physicalCol)) .sort(sortFunction(({ visualRow }) => visualRow)) .map(({ physicalRow }) => physicalRow) ); - /* - * Don't clear disambiguation when afterChange is triggered by - * hot.undo() from inside of afterUndoRedo() - * FEATURE: consider not clearing disambiguation at all - */ - if (!workbench.undoRedoIsHandled) + if (!workbench.undoRedoIsHandled) { changedRows.forEach((physicalRow) => workbench.disambiguation.clearDisambiguation(physicalRow) ); + } changes.forEach( - ({ - visualRow, - visualCol, - physicalRow, - physicalCol, - oldValue = '', - newValue, - }) => { + ({ visualRow, visualCol, physicalRow, physicalCol, oldValue = '', newValue }) => { if ( - workbench.cells.getCellMeta( - physicalRow, - physicalCol, - 'originalValue' - ) === undefined - ) - workbench.cells.setCellMeta( - physicalRow, - physicalCol, - 'originalValue', - oldValue - ); + workbench.cells.getCellMeta(physicalRow, physicalCol, 'originalValue') === undefined + ) { + workbench.cells.setCellMeta(physicalRow, physicalCol, 'originalValue', oldValue); + } + workbench.cells.recalculateIsModifiedState(physicalRow, physicalCol, { visualRow, visualCol, }); + if ( workbench.utils.searchPreferences.search.liveUpdate && workbench.utils.searchQuery !== undefined - ) + ) { workbench.cells.updateCellMeta( physicalRow, physicalCol, @@ -282,70 +259,50 @@ export function useHotHooks({ workbench.utils.searchFunction(newValue), { visualRow, visualCol } ); + } } ); spreadsheetChanged(); workbench.cells.updateCellInfoStats(); - if (workbench.dataset.uploadplan) - changedRows.forEach((physicalRow) => - workbench.validation.startValidateRow(physicalRow) - ); + if (workbench.dataset.uploadplan) { + changedRows.forEach((physicalRow) => workbench.validation.startValidateRow(physicalRow)); + } }, - /* - * This may be called before full initialization of the workbench because - * of the minSpareRows setting in HOT. Thus, be sure to check if - * wbView.hotIsReady is true - * - * Also, I don't think this is ever called with amount > 1. - * Even if multiple new rows where created at once (e.x on paste), HOT calls - * this hook one row at a time - * - * Also, this function needs to be called before afterValidate, thus I used - * beforeCreateRow, instead of afterCreateRow - * - */ beforeCreateRow: (visualRowStart, amount, source) => { - const addedRows = Array.from( - { length: amount }, - (_, index) => - /* - * If HOT is not yet fully initialized, we can assume that physical row - * order and visual row order is the same - */ - workbench.hot?.toPhysicalRow(visualRowStart + index) ?? - visualRowStart + index - // REFACTOR: use sortFunction here - ).sort(); + const addedRows = Array.from({ length: amount }, (_, index) => + workbench.hot?.toPhysicalRow(visualRowStart + index) ?? visualRowStart + index + ).sort((a, b) => a - b); + + if (workbench.cells.indexedCellMeta !== undefined) { + workbench.cells.indexedCellMeta = undefined; + } - workbench.cells.indexedCellMeta = undefined; addedRows .filter((physicalRow) => physicalRow < workbench.cells.cellMeta.length) - .forEach((physicalRow) => - workbench.cells?.cellMeta.splice(physicalRow, 0, []) - ); - if (workbench.hot !== undefined && source !== 'auto') - spreadsheetChanged(); + .forEach((physicalRow) => workbench.cells?.cellMeta.splice(physicalRow, 0, [])); + if (workbench.hot !== undefined && source !== 'auto') { + spreadsheetChanged(); + } return true; }, - beforeRemoveRow: (visualRowStart, amount, _, source) => { - if (workbench.hot === undefined) return; - // Get indexes of removed rows in reverse order + beforeRemoveRow: (visualRowStart, amount, _variables, source) => { + if (!validateWorkbenchState('beforeRemoveRow')) return false; + const removedRows = Array.from({ length: amount }, (_, index) => workbench.hot!.toPhysicalRow(visualRowStart + index) ) .filter((physicalRow) => physicalRow < workbench.cells.cellMeta.length) - // REFACTOR: use sortFunction here - .sort() + .sort((a, b) => a - b) .reverse(); removedRows.forEach((physicalRow) => { workbench.cells.cellMeta.splice(physicalRow, 1); - workbench.validation.liveValidationStack.splice(physicalRow, 1); + workbench.validation?.liveValidationStack?.splice(physicalRow, 1); }); workbench.cells.indexedCellMeta = undefined; @@ -354,24 +311,20 @@ export function useHotHooks({ spreadsheetChanged(); workbench.cells.updateCellInfoStats(); } - return true; }, - /* - * If a tree column is about to be sorted, overwrite the sort config by - * finding all lower level ranks of that tree (within the same -to-many) - * and sorting them in the same direction - */ beforeColumnSort: (currentSortConfig, newSortConfig) => { - workbench.cells.indexedCellMeta = undefined; - + if (workbench.cells.indexedCellMeta !== undefined) { + workbench.cells.indexedCellMeta = undefined; + } if ( workbench.mappings === undefined || sortConfigIsSet || - workbench.hot === undefined - ) + !validateWorkbenchState('beforeColumnSort') + ) { return true; + } const findTreeColumns = ( sortConfig: RA, @@ -387,28 +340,15 @@ export function useHotHooks({ ...rest, rankGroup: workbench .mappings!.treeRanks?.map((rankGroup, groupIndex) => ({ - rankId: rankGroup.find( - (mapping) => mapping.physicalCol === physicalCol - )?.rankId, + rankId: rankGroup.find((m) => m.physicalCol === physicalCol)?.rankId, groupIndex, })) .find(({ rankId }) => rankId !== undefined), })) - // Filter out columns that aren't tree ranks .filter(({ rankGroup }) => rankGroup !== undefined) - /* - * Filter out columns that didn't change - * In the end, there should only be 0 or 1 columns - * - */ .find(({ sortOrder, visualCol }) => { - const deltaColumnState = deltaSearchConfig.find( - ({ column }) => column === visualCol - ); - return ( - deltaColumnState === undefined || - deltaColumnState.sortOrder !== sortOrder - ); + const deltaColumnState = deltaSearchConfig.find(({ column }) => column === visualCol); + return deltaColumnState === undefined || deltaColumnState.sortOrder !== sortOrder; }); let changedTreeColumn = findTreeColumns(newSortConfig, currentSortConfig); @@ -418,21 +358,12 @@ export function useHotHooks({ changedTreeColumn = findTreeColumns(currentSortConfig, newSortConfig); newSortOrderIsUnset = true; } - if (changedTreeColumn === undefined) return true; - /* - * Filter out columns with higher rank than the changed column - * (lower rankId corresponds to a higher tree rank) - * - */ - const columnsToSort = workbench.mappings.treeRanks[ - changedTreeColumn.rankGroup!.groupIndex - ] + const columnsToSort = workbench.mappings.treeRanks[changedTreeColumn.rankGroup!.groupIndex] .filter(({ rankId }) => rankId >= changedTreeColumn!.rankGroup!.rankId!) .map(({ physicalCol }) => workbench.hot!.toVisualColumn(physicalCol)); - // Filter out columns that are about to be sorted const partialSortConfig = newSortConfig.filter( ({ column }) => !columnsToSort.includes(column) ); @@ -447,35 +378,34 @@ export function useHotHooks({ }))), ]; + const columnSorting = getHotPlugin(workbench.hot!, 'multiColumnSorting'); + if (!columnSorting) return true; + sortConfigIsSet = true; - getHotPlugin(workbench.hot, 'multiColumnSorting').sort(fullSortConfig); + columnSorting.sort(fullSortConfig); sortConfigIsSet = false; - return false; }, - // Cache sort config to preserve column sort order across sessions - afterColumnSort: async (_previousSortConfig, sortConfig) => { - if (workbench.hot === undefined) return; - const physicalSortConfig = sortConfig.map((rest) => ({ - ...rest, - physicalCol: workbench.hot!.toPhysicalColumn(rest.column), + afterColumnSort: async (_previous, sortConfig) => { + if (!validateWorkbenchState('afterColumnSort')) return; + + const physicalSortConfig = sortConfig.map((config) => ({ + ...config, + physicalCol: workbench.hot!.toPhysicalColumn(config.column), })); - setCache( + await setCache( 'workBenchSortConfig', `${schema.domainLevelIds.collection}_${workbench.dataset.id}`, physicalSortConfig ); }, - beforeColumnMove: (_columnIndexes, _finalIndex, dropIndex) => - !isResultsOpen && - (dropIndex !== undefined || workbench.hot !== undefined), + beforeColumnMove: (_columnIndexes, _finalIndex, dropIndex) => !isResultsOpen && dropIndex !== undefined && workbench.hot !== undefined, - // Save new visualOrder on the back end afterColumnMove: (_columnIndexes, _finalIndex, dropIndex) => { - // An ugly fix for jQuery's dialogs conflicting with HOT - if (dropIndex === undefined || workbench.hot == undefined) return; + if (dropIndex === undefined || !validateWorkbenchState('afterColumnMove')) return; + workbench.cells.indexedCellMeta = undefined; const columnOrder = workbench.dataset.columns.map((_, visualCol) => @@ -484,9 +414,7 @@ export function useHotHooks({ if ( workbench.dataset.visualorder === null || - columnOrder.some( - (i, index) => i !== workbench.dataset.visualorder![index] - ) + columnOrder.some((i, index) => i !== workbench.dataset.visualorder![index]) ) { overwriteReadOnly(workbench.dataset, 'visualorder', columnOrder); loading( @@ -499,41 +427,43 @@ export function useHotHooks({ } }, - // Do not scroll the viewport to the last column after inserting a row afterPaste: (data, coords) => { + if (!validateWorkbenchState('afterPaste')) return; + const lastCoords = coords.at(-1); if ( typeof lastCoords === 'object' && - data.some((row) => row.length === workbench.dataset.columns.length) && - workbench.hot !== undefined - ) - workbench.hot.scrollViewportTo(lastCoords.endRow, lastCoords.startCol); + data.some((row) => row.length === (workbench.dataset?.columns?.length ?? 0)) + ) { + workbench.hot!.scrollViewportTo(lastCoords.endRow, lastCoords.startCol); + } }, - /* - * Disallow user from selecting several times the same cell - */ afterSelection: () => { - if (workbench.hot === undefined) return; - const selection = workbench.hot?.getSelected() ?? []; - const newSelection = f + if (!validateWorkbenchState('afterSelection')) return; + + const selection = workbench.hot!.getSelected() ?? []; + const uniqueSelection = f .unique(selection.map((row) => JSON.stringify(row))) .map((row) => JSON.parse(row)); - if (newSelection.length !== selection.length) { - workbench.hot?.deselectCell(); - workbench.hot?.selectCells(newSelection); + + if (uniqueSelection.length !== selection.length) { + workbench.hot!.deselectCell(); + workbench.hot!.selectCells(uniqueSelection); } }, }; } -/** - * Any change to a row clears disambiguation results - * Clearing disambiguation creates a separate point in the undo/redo stack - * This runs undo twice when undoing a change that caused disambiguation - * clear and similarly redoes the change twice - * - */ +function safeParseJSON(s: unknown): any { + if (typeof s !== 'string') return {}; + try { + return JSON.parse(s) ?? {}; + } catch { + return {}; + } +} + function afterUndoRedo( workbench: Workbench, type: 'redo' | 'undo', @@ -544,34 +474,32 @@ function afterUndoRedo( data.actionType !== 'change' || data.changes.length !== 1 || workbench.hot === undefined - ) + ) { return; + } const [visualRow, visualCol, newData, oldData] = data.changes[0]; const physicalRow = workbench.hot.toPhysicalRow(visualRow); const physicalCol = workbench.hot.toPhysicalColumn(visualCol as number); - if (physicalCol !== workbench.dataset.columns.length) return; - const newValue = JSON.parse(newData || '{}').disambiguation; - const oldValue = JSON.parse(oldData || '{}').disambiguation; + const colCount = workbench.dataset?.columns?.length ?? -1; + if (colCount < 0 || physicalCol !== colCount) return; + + const newValue = (safeParseJSON(newData) ).disambiguation ?? {}; + const oldValue = (safeParseJSON(oldData) ).disambiguation ?? {}; - /* - * Disambiguation results are cleared when any cell in a row changes. - * That change creates a separate point in the undo stack. - * Thus, if HOT tries to undo disambiguation clearing, we need to - * also need to undo the change that caused disambiguation clearing - */ if ( type === 'undo' && Object.keys(newValue ?? {}).length > 0 && Object.keys(oldValue ?? {}).length === 0 - ) - // HOT doesn't seem to like calling undo from inside of afterUndo - globalThis.setTimeout(() => { + ) { + setTimeout(() => { workbench.undoRedoIsHandled = true; workbench.hot?.undo(); workbench.undoRedoIsHandled = false; workbench.disambiguation.afterChangeDisambiguation(physicalRow); }, 0); - else workbench.disambiguation.afterChangeDisambiguation(physicalRow); + } else { + workbench.disambiguation.afterChangeDisambiguation(physicalRow); + } } diff --git a/specifyweb/frontend/js_src/package-lock.json b/specifyweb/frontend/js_src/package-lock.json index 9f1d1ebb40d..63c717c919e 100644 --- a/specifyweb/frontend/js_src/package-lock.json +++ b/specifyweb/frontend/js_src/package-lock.json @@ -27,7 +27,7 @@ "d3": "^7.6.1", "dayjs": "^1.10.7", "globals": "^13.19.0", - "handsontable": "^12.1.0", + "handsontable": "^12.4.0", "jquery": "~1.12.0", "leaflet": "^1.7.1", "leaflet-gesture-handling": "^1.2.2", @@ -9570,27 +9570,19 @@ } }, "node_modules/handsontable": { - "version": "12.1.0", - "resolved": "https://registry.npmjs.org/handsontable/-/handsontable-12.1.0.tgz", - "integrity": "sha512-MdplUt3MNc1Ir6dTIlwHUOjCm0GTvB3RpKvvBgb9Jzp/TYVZ3YbfpS6II6Q5sYpUh40ILgOkDtS9gdZTM/OFsQ==", + "version": "12.4.0", + "resolved": "https://registry.npmjs.org/handsontable/-/handsontable-12.4.0.tgz", + "integrity": "sha512-bivyvW41RKT3SNIo+f2SOgvV376NbtTkfWYYRCSTOW/bx6EnjH4S2Pl4vfVpFPRQLOFPGTEWniuXLg8b1VkpBQ==", "dependencies": { "@types/pikaday": "1.7.4", "core-js": "^3.0.0", "dompurify": "^2.1.1", - "moment": "2.29.3", + "moment": "2.29.4", "numbro": "2.1.2", "pikaday": "1.8.2" }, "optionalDependencies": { - "hyperformula": "^2.0.0" - } - }, - "node_modules/handsontable/node_modules/moment": { - "version": "2.29.3", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.3.tgz", - "integrity": "sha512-c6YRvhEo//6T2Jz/vVtYzqBzwvPT95JBQ+smCytzf7c50oMZRsR/a4w88aD34I+/QVSfnoAnSBFPJHItlOMJVw==", - "engines": { - "node": "*" + "hyperformula": "^2.4.0" } }, "node_modules/has": { @@ -24016,24 +24008,17 @@ } }, "handsontable": { - "version": "12.1.0", - "resolved": "https://registry.npmjs.org/handsontable/-/handsontable-12.1.0.tgz", - "integrity": "sha512-MdplUt3MNc1Ir6dTIlwHUOjCm0GTvB3RpKvvBgb9Jzp/TYVZ3YbfpS6II6Q5sYpUh40ILgOkDtS9gdZTM/OFsQ==", + "version": "12.4.0", + "resolved": "https://registry.npmjs.org/handsontable/-/handsontable-12.4.0.tgz", + "integrity": "sha512-bivyvW41RKT3SNIo+f2SOgvV376NbtTkfWYYRCSTOW/bx6EnjH4S2Pl4vfVpFPRQLOFPGTEWniuXLg8b1VkpBQ==", "requires": { "@types/pikaday": "1.7.4", "core-js": "^3.0.0", "dompurify": "^2.1.1", - "hyperformula": "^2.0.0", - "moment": "2.29.3", + "hyperformula": "^2.4.0", + "moment": "2.29.4", "numbro": "2.1.2", "pikaday": "1.8.2" - }, - "dependencies": { - "moment": { - "version": "2.29.3", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.3.tgz", - "integrity": "sha512-c6YRvhEo//6T2Jz/vVtYzqBzwvPT95JBQ+smCytzf7c50oMZRsR/a4w88aD34I+/QVSfnoAnSBFPJHItlOMJVw==" - } } }, "has": { @@ -25671,7 +25656,7 @@ "decimal.js": "^10.3.1", "domexception": "^4.0.0", "escodegen": "^2.0.0", - "form-data": "4.0.4", + "form-data": ">=4.0.4", "html-encoding-sniffer": "^3.0.0", "http-proxy-agent": "^5.0.0", "https-proxy-agent": "^5.0.0", diff --git a/specifyweb/frontend/js_src/package.json b/specifyweb/frontend/js_src/package.json index e915cee300c..e9b34f74c78 100644 --- a/specifyweb/frontend/js_src/package.json +++ b/specifyweb/frontend/js_src/package.json @@ -47,7 +47,7 @@ "d3": "^7.6.1", "dayjs": "^1.10.7", "globals": "^13.19.0", - "handsontable": "^12.1.0", + "handsontable": "^12.4.0", "jquery": "~1.12.0", "leaflet": "^1.7.1", "leaflet-gesture-handling": "^1.2.2",