diff --git a/packages/jsonforms-renderers/src/renderers/SpreadsheetControl.tsx b/packages/jsonforms-renderers/src/renderers/SpreadsheetControl.tsx index c736bb5..83fb133 100644 --- a/packages/jsonforms-renderers/src/renderers/SpreadsheetControl.tsx +++ b/packages/jsonforms-renderers/src/renderers/SpreadsheetControl.tsx @@ -9,11 +9,11 @@ import { formatBytes, formatAccept } from '../utils/format' import { parseWorkbookToMatrix, columnLetter, - evaluateExpressions, + processMatrix, SheetParseError, type CellValue, type FormulaConfigEntry, - type FormulaResult, + type SpreadsheetValue, } from '../utils/spreadsheet' interface XSpreadsheetOptions { @@ -37,13 +37,6 @@ type SpreadsheetControlProps = ControlProps & { schema: JsonSchema & { 'x-spreadsheet'?: XSpreadsheetOptions; 'x-evaluate'?: FormulaConfigEntry[] } } -// Persisted value shape — the field's data is an object, not a string key. -// No `fileName`: there's no storage service to name a retrievable file for. -interface SpreadsheetValue { - sheet?: CellValue[][] - derivations: FormulaResult[] -} - type Status = 'empty' | 'parsing' | 'ready' | 'error' const DEFAULT_ACCEPT = @@ -150,11 +143,8 @@ const SpreadsheetControl = ({ } setLocalMatrix(parsedMatrix) - const newDerivations = await evaluateExpressions(parsedMatrix, xEvaluate) - handleChange( - path, - persistSheet ? { sheet: parsedMatrix, derivations: newDerivations } : { derivations: newDerivations }, - ) + const value = await processMatrix(parsedMatrix, xEvaluate, { persistSheet }) + handleChange(path, value) setStatus('ready') }, [accept, maxSize, persistSheet, sheetName, xEvaluate, path, handleChange], diff --git a/packages/jsonforms-renderers/src/utils/spreadsheet/index.ts b/packages/jsonforms-renderers/src/utils/spreadsheet/index.ts index 8826444..4309101 100644 --- a/packages/jsonforms-renderers/src/utils/spreadsheet/index.ts +++ b/packages/jsonforms-renderers/src/utils/spreadsheet/index.ts @@ -1,3 +1,13 @@ export { parseWorkbookToMatrix, columnLetter, SheetParseError } from './parse' export { evaluateExpression, evaluateExpressions } from './expression' -export type { CellValue, Matrix, ParsedSheet, FormulaConfigEntry, FormulaResult, FormulaErrorCode } from './types' +export { processMatrix } from './process' +export type { ProcessMatrixOptions } from './process' +export type { + CellValue, + Matrix, + ParsedSheet, + FormulaConfigEntry, + FormulaResult, + FormulaErrorCode, + SpreadsheetValue, +} from './types' diff --git a/packages/jsonforms-renderers/src/utils/spreadsheet/process.test.ts b/packages/jsonforms-renderers/src/utils/spreadsheet/process.test.ts new file mode 100644 index 0000000..f77cc43 --- /dev/null +++ b/packages/jsonforms-renderers/src/utils/spreadsheet/process.test.ts @@ -0,0 +1,47 @@ +import { describe, expect, it } from 'vitest' +import { processMatrix } from './process' +import type { CellValue } from './types' + +const matrix: CellValue[][] = [ + ['Item', 'Qty'], + ['Widget', 10], + ['Gadget', 20], +] + +describe('processMatrix', () => { + it('includes the raw sheet by default (persistSheet omitted)', async () => { + const result = await processMatrix(matrix, [{ label: 'Total', expression: '=SUM(B2:B3)' }]) + expect(result).toEqual({ sheet: matrix, derivations: [{ label: 'Total', value: 30 }] }) + }) + + it('includes the raw sheet when persistSheet is explicitly true', async () => { + const result = await processMatrix(matrix, [{ label: 'Total', expression: '=SUM(B2:B3)' }], { + persistSheet: true, + }) + expect(result).toEqual({ sheet: matrix, derivations: [{ label: 'Total', value: 30 }] }) + }) + + it('omits the raw sheet when persistSheet is false', async () => { + const result = await processMatrix(matrix, [{ label: 'Total', expression: '=SUM(B2:B3)' }], { + persistSheet: false, + }) + expect(result).toEqual({ derivations: [{ label: 'Total', value: 30 }] }) + expect(result).not.toHaveProperty('sheet') + }) + + it('yields an empty derivations array for an empty formulas config', async () => { + const result = await processMatrix(matrix, []) + expect(result).toEqual({ sheet: matrix, derivations: [] }) + }) + + it('surfaces an evaluateExpressions error entry unchanged, alongside a good one', async () => { + const result = await processMatrix(matrix, [ + { label: 'Good', expression: '=SUM(B2:B3)' }, + { label: 'Bad', expression: '=FOO(B2)' }, + ]) + expect(result.derivations).toEqual([ + { label: 'Good', value: 30 }, + { label: 'Bad', value: null, error: '#NAME?' }, + ]) + }) +}) diff --git a/packages/jsonforms-renderers/src/utils/spreadsheet/process.ts b/packages/jsonforms-renderers/src/utils/spreadsheet/process.ts new file mode 100644 index 0000000..9f9fc27 --- /dev/null +++ b/packages/jsonforms-renderers/src/utils/spreadsheet/process.ts @@ -0,0 +1,18 @@ +import { evaluateExpressions } from './expression' +import type { CellValue, FormulaConfigEntry, SpreadsheetValue } from './types' + +export interface ProcessMatrixOptions { + persistSheet?: boolean +} + +// Matrix-in, persisted-value-out. Deliberately format-agnostic: doesn't care +// whether the matrix came from an xlsx/csv upload or (in the future) an XML +// one — this is the reusable seam for both. +export async function processMatrix( + matrix: CellValue[][], + formulas: FormulaConfigEntry[], + options: ProcessMatrixOptions = {}, +): Promise { + const derivations = await evaluateExpressions(matrix, formulas) + return options.persistSheet !== false ? { sheet: matrix, derivations } : { derivations } +} diff --git a/packages/jsonforms-renderers/src/utils/spreadsheet/types.ts b/packages/jsonforms-renderers/src/utils/spreadsheet/types.ts index f2757d3..1e17a44 100644 --- a/packages/jsonforms-renderers/src/utils/spreadsheet/types.ts +++ b/packages/jsonforms-renderers/src/utils/spreadsheet/types.ts @@ -28,3 +28,13 @@ export interface FormulaResult { // class; ERROR is our own catch-all for anything that doesn't map cleanly // (a syntax error, or a wrapped cause we don't otherwise recognize). export type FormulaErrorCode = 'REF' | 'VALUE' | 'DIV0' | 'NAME' | 'ERROR' | 'NA' | 'NUM' | 'NULL' + +// Persisted value shape for SpreadsheetControl — the field's data is an +// object, not a string key. No `fileName`: there's no storage service to +// name a retrievable file for. Shared here (rather than kept private to +// SpreadsheetControl.tsx) so sibling renderers, like ComputedControl, can +// read a SpreadsheetControl's persisted `derivations` with the same type. +export interface SpreadsheetValue { + sheet?: CellValue[][] + derivations: FormulaResult[] +}