-
-
Notifications
You must be signed in to change notification settings - Fork 0
refactor(ui): extract testable batch-update logic from sidebar.js #416
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| /** | ||
| * Types for batch-update-logic.js (DOM-free batch-update helpers). | ||
| */ | ||
|
|
||
| export interface BatchSelectedCell { | ||
| rowId: string | number; | ||
| rowIdx: number; | ||
| colIdx: number; | ||
| value: unknown; | ||
| } | ||
|
|
||
| export interface BatchColumnDef { | ||
| name: string; | ||
| type?: string; | ||
| } | ||
|
|
||
| /** Minimal shape of a batch-field <input> (real element or test stand-in). */ | ||
| export interface BatchInputLike { | ||
| value: string; | ||
| dataset?: { isnull?: string; ispatch?: string }; | ||
| } | ||
|
|
||
| export interface BatchColumnInfo { | ||
| name: string; | ||
| type?: string; | ||
| values: Set<unknown>; | ||
| } | ||
|
|
||
| export interface PreparedBatchUpdate { | ||
| rowId: string | number; | ||
| column: string; | ||
| value: unknown; | ||
| originalValue: unknown; | ||
| operation: 'set' | 'json_patch'; | ||
| rowIdx: number; | ||
| colIdx: number; | ||
| } | ||
|
|
||
| export function groupSelectedCellsByColumn( | ||
| selectedCells: BatchSelectedCell[], | ||
| tableColumns: BatchColumnDef[] | ||
| ): Map<number, BatchColumnInfo>; | ||
|
|
||
| export function summarizeColumnValue(values: Iterable<unknown>): string; | ||
|
|
||
| export function prepareBatchUpdates( | ||
| selectedCells: BatchSelectedCell[], | ||
| inputsByCol: Map<number, BatchInputLike>, | ||
| tableColumns: BatchColumnDef[] | ||
| ): PreparedBatchUpdate[]; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,104 @@ | ||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||
| * Batch Update Logic (DOM-free, unit-testable) | ||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||
| * Pure helpers extracted from sidebar.js's batch-update flow so the | ||||||||||||||||||||||||||||||||||||
| * value-processing rules (column grouping, value summarisation, type | ||||||||||||||||||||||||||||||||||||
| * coercion, NULL/json_patch handling, skip-empty) can be unit-tested | ||||||||||||||||||||||||||||||||||||
| * without a DOM. sidebar.js wires these into the actual DOM. | ||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||
| * Types live in batch-update-logic.d.ts. | ||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||
| * Group the selected cells by column index. | ||||||||||||||||||||||||||||||||||||
| * @returns Map of colIdx -> { name, type, values } where `values` is the set | ||||||||||||||||||||||||||||||||||||
| * of distinct cell values currently selected in that column. | ||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||
| export function groupSelectedCellsByColumn(selectedCells, tableColumns) { | ||||||||||||||||||||||||||||||||||||
| const columns = new Map(); | ||||||||||||||||||||||||||||||||||||
| for (const cell of selectedCells) { | ||||||||||||||||||||||||||||||||||||
| const colDef = tableColumns && tableColumns[cell.colIdx]; | ||||||||||||||||||||||||||||||||||||
| if (!colDef) continue; // skip stale/out-of-bounds selections (e.g. after a column drop) | ||||||||||||||||||||||||||||||||||||
| if (!columns.has(cell.colIdx)) { | ||||||||||||||||||||||||||||||||||||
| columns.set(cell.colIdx, { | ||||||||||||||||||||||||||||||||||||
| name: colDef.name, | ||||||||||||||||||||||||||||||||||||
| type: colDef.type, | ||||||||||||||||||||||||||||||||||||
| values: new Set() | ||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
| columns.get(cell.colIdx).values.add(cell.value); | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
| return columns; | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||
| * Placeholder text describing a column's current selected value(s): | ||||||||||||||||||||||||||||||||||||
| * '(mixed values)' when the selection spans differing values, otherwise the | ||||||||||||||||||||||||||||||||||||
| * single shared value rendered as NULL / [BLOB] / its string form. | ||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||
| export function summarizeColumnValue(values) { | ||||||||||||||||||||||||||||||||||||
| const uniqueValues = Array.from(values || []); | ||||||||||||||||||||||||||||||||||||
| if (uniqueValues.length === 0) return ''; | ||||||||||||||||||||||||||||||||||||
| if (uniqueValues.length > 1) return '(mixed values)'; | ||||||||||||||||||||||||||||||||||||
| const val = uniqueValues[0]; | ||||||||||||||||||||||||||||||||||||
| if (val === null) return 'NULL'; | ||||||||||||||||||||||||||||||||||||
| if (val instanceof Uint8Array) return '[BLOB]'; | ||||||||||||||||||||||||||||||||||||
| return String(val); | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
Comment on lines
+39
to
+47
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If
Suggested change
|
||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||
| * Build the list of cell updates to send to the backend. | ||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||
| * `inputsByCol` maps a column index to an input-like object | ||||||||||||||||||||||||||||||||||||
| * ({ value, dataset:{ isnull, ispatch } }) — in the browser these are the | ||||||||||||||||||||||||||||||||||||
| * real <input> elements; in tests they are plain stand-ins. Mirrors the | ||||||||||||||||||||||||||||||||||||
| * batch form's rules: skip cells left blank (unless explicitly NULL), tag | ||||||||||||||||||||||||||||||||||||
| * json_patch operations, and coerce numeric column types. | ||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||
| export function prepareBatchUpdates(selectedCells, inputsByCol, tableColumns) { | ||||||||||||||||||||||||||||||||||||
| const updates = []; | ||||||||||||||||||||||||||||||||||||
| for (const cell of selectedCells) { | ||||||||||||||||||||||||||||||||||||
| const input = inputsByCol.get(cell.colIdx); | ||||||||||||||||||||||||||||||||||||
| if (!input) continue; | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| const dataset = input.dataset || {}; | ||||||||||||||||||||||||||||||||||||
| const isNull = dataset.isnull === 'true'; | ||||||||||||||||||||||||||||||||||||
| const isPatch = dataset.ispatch === 'true'; | ||||||||||||||||||||||||||||||||||||
| const value = input.value; | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| // Skip cells left blank unless they were explicitly set to NULL. | ||||||||||||||||||||||||||||||||||||
| if (value === '' && !isNull) continue; | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| const colDef = tableColumns && tableColumns[cell.colIdx]; | ||||||||||||||||||||||||||||||||||||
| if (!colDef) continue; // skip stale/out-of-bounds selections | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| let finalValue = value; | ||||||||||||||||||||||||||||||||||||
| let operation = 'set'; | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| if (isNull) { | ||||||||||||||||||||||||||||||||||||
| finalValue = null; | ||||||||||||||||||||||||||||||||||||
| } else if (isPatch) { | ||||||||||||||||||||||||||||||||||||
| operation = 'json_patch'; | ||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||
| // Coerce numeric column types when the input parses as a number. | ||||||||||||||||||||||||||||||||||||
| // Normalize case: SQLite stores the declared type verbatim, so a column | ||||||||||||||||||||||||||||||||||||
| // may report e.g. 'integer' rather than 'INTEGER'. | ||||||||||||||||||||||||||||||||||||
| const colType = (colDef.type || '').toUpperCase(); | ||||||||||||||||||||||||||||||||||||
| if ((colType === 'INTEGER' || colType === 'REAL' || colType === 'NUMERIC') | ||||||||||||||||||||||||||||||||||||
| && !isNaN(Number(value)) && value.trim() !== '') { | ||||||||||||||||||||||||||||||||||||
| finalValue = Number(value); | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| updates.push({ | ||||||||||||||||||||||||||||||||||||
| rowId: cell.rowId, | ||||||||||||||||||||||||||||||||||||
| column: colDef.name, | ||||||||||||||||||||||||||||||||||||
| value: finalValue, | ||||||||||||||||||||||||||||||||||||
| originalValue: cell.value, | ||||||||||||||||||||||||||||||||||||
| operation, | ||||||||||||||||||||||||||||||||||||
| rowIdx: cell.rowIdx, | ||||||||||||||||||||||||||||||||||||
| colIdx: cell.colIdx | ||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
| return updates; | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If
tableColumnsis undefined orcell.colIdxis out of bounds,colDefwill be undefined, leading to aTypeErrorwhen accessingcolDef.name. Adding a defensive check prevents potential runtime crashes.