Full document, spreadsheet, slideshow, and diagram tooling
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix(sheets): formula-bar commit runs auto-format parser (#711)

Closes #711

scott d24d8c47 5cc2c37a

+185 -4
+1
CHANGELOG.md
··· 9 9 10 10 ### Fixed 11 11 - Topbar status chips — unified Synced + E2EE indicators across slides / forms / diagrams / calendar (v0.62.7, #695). Docs and sheets already rendered three chips (Saved / Synced / E2EE) but slides, forms, diagrams, and calendar only showed the shared Saved indicator, leaving users unable to tell whether the document had synced to the server or whether it was encrypted. Extracted the Synced-chip wiring into `src/lib/status-chips.ts` (`wireStatusChips({ provider })`) that listens to the provider's `status` + `sync` events and flips `#status-dot` + `#status-text` — same pattern docs and sheets have used inline. Added the matching HTML (two `.status-indicator` spans) to all four editor templates that were missing them, and wired `wireStatusChips` into each editor's `main.ts` next to `wireSaveStatus`. 9 regression tests in `tests/status-chips.test.ts`: 3 for the helper behavior (status toggle, sync transition, no-op when host markup absent) + 6 that scan every editor template for both the Synced status-indicator and the E2EE chip markup so no editor can silently regress. (#695) 12 + - Sheets: formula-bar input now runs the same auto-format parser as the cell-editor input (v0.62.8, #711). Before: typing `$100` into the formula bar stored the raw string `"$100"`, so any downstream formula like `=A1*2` silently evaluated to `0`. The cell-editor path in `src/sheets/cell-editing.ts` called `detectAndParseEntry()` to parse `$100`→`100`/`currency`, `75%`→`0.75`/`percent`, `1,234`→`1234`/`number`, `2026-03-15`→timestamp/`date`; the formula-bar path in `src/sheets/formula-bar-ui.ts:115` did plain `Number(raw)` instead, which `NaN`'d on those patterns and fell through to storing the raw string. `commitFormulaBar()` now mirrors `commitEdit()`'s full parse + format-stamp logic (including the "existing explicit format wins" rule so typing `42` into a currency cell still lands as a plain number under the existing format). 8 regression tests in `tests/sheets-formula-bar-autoformat.test.ts` cover the parse matrix: currency, percent, comma-number, ISO date, plain number, formula passthrough, existing-format override, and non-matching text. Caught live by entering `$100` in A1 via the formula bar and observing `=A1*2` → `0`. (#711) 12 13 13 14 ### Changed 14 15 - Calendar: topbar settings button now renders as a proper 8-tooth gear glyph instead of a sunburst that was visually indistinguishable from the adjacent theme-toggle sun (v0.62.4, #697). The old `#btn-cal-settings` SVG was a circle-with-8-radiating-lines (`circle r=2.5` + eight `M..v.. / M..h..` rays) and the theme-toggle `#btn-theme-toggle` right next to it was the same circle-with-8-rays pattern at `r=3.5` — users clicked the wrong one constantly, with the settings panel hiding some destructive controls. Replaced the settings icon with a true 32-vertex 8-tooth gear polygon (outer tooth tip radius 6.8, valley radius 4.6, hub circle r=2.2) traced in `src/calendar/index.html`. Added an `icon-gear` marker class so future refactors can't silently regress to a sunburst, plus four Playwright regression tests in `e2e/calendar.spec.ts` that pin the contract: (1) the icon carries the `icon-gear` class; (2) the settings path `d` attribute differs from the theme-toggle path; (3) the path has either curves, a closed subpath, or >24 draw commands (sunburst had ~16 and no close); (4) both buttons remain visible and adjacent. Also added `aria-label` + `aria-hidden` attributes that the theme-toggle already had for a11y parity. (#697)
+1 -1
package.json
··· 1 1 { 2 2 "name": "tools", 3 - "version": "0.62.7", 3 + "version": "0.62.8", 4 4 "private": true, 5 5 "type": "module", 6 6 "main": "electron/main.js",
+40 -3
src/sheets/formula-bar-ui.ts
··· 5 5 */ 6 6 7 7 import { cellId, colToLetter } from './formulas.js'; 8 + import { detectAndParseEntry } from './auto-format.js'; 8 9 import { tokenizeForHighlighting, renderHighlightedFormula } from './formula-highlighter.js'; 9 10 import { extractFormulaRanges, assignRangeColors, renderGridHighlights, clearGridHighlights } from './range-highlight.js'; 10 11 import { detectCurrentFunction, renderTooltip, hideTooltip } from './formula-tooltip.js'; ··· 120 121 if (raw.startsWith('=')) { 121 122 deps.setCellData(id, { v: '', f: raw.slice(1) }); 122 123 } else { 123 - const numVal = Number(raw); 124 - const value = raw === '' ? '' : (!isNaN(numVal) && raw !== '' ? numVal : raw); 125 - deps.setCellData(id, { v: value, f: '' }); 124 + // Mirror commitEdit() in cell-editing.ts: auto-detect currency/percent/ 125 + // date/number patterns when the cell has no explicit format, so typing 126 + // "$100" via the formula bar stores the parsed number 100 (not the 127 + // string "$100") and downstream formulas can reference it. Regression 128 + // guard for #711 — previously only cell-editor input path ran the 129 + // parser, so a value entered through the formula bar was silently 130 + // broken for any formula that referenced it. 131 + const existingData = deps.getCellData(id); 132 + const existingFormat = existingData?.s?.format; 133 + let value: string | number; 134 + let autoFormat: string | undefined; 135 + 136 + if (!existingFormat) { 137 + const detected = detectAndParseEntry(raw); 138 + value = detected.value; 139 + autoFormat = detected.format; 140 + } else { 141 + const numVal = Number(raw); 142 + value = raw === '' ? '' : (!isNaN(numVal) && raw !== '' ? numVal : raw); 143 + } 144 + 145 + // Fallback numeric coercion when auto-format didn't fire (e.g. plain "42"). 146 + if (autoFormat === undefined && typeof value === 'string' && value !== '') { 147 + const numVal = Number(value); 148 + if (!isNaN(numVal) && value.trim() !== '') value = numVal; 149 + } 150 + 151 + // Date-formatted cell + user typed a date string → parse to timestamp 152 + if (typeof value === 'string' && value !== '' && existingFormat === 'date') { 153 + const parsed = Date.parse(value); 154 + if (!isNaN(parsed)) value = parsed; 155 + } 156 + 157 + if (autoFormat) { 158 + const existingStyle = existingData?.s || {}; 159 + deps.setCellData(id, { v: value, f: '', s: { ...existingStyle, format: autoFormat } }); 160 + } else { 161 + deps.setCellData(id, { v: value, f: '' }); 162 + } 126 163 } 127 164 128 165 deps.evalCache.clear(); deps.clearSpillMaps(); deps.invalidateRecalcEngine();
+143
tests/sheets-formula-bar-autoformat.test.ts
··· 1 + /** 2 + * @vitest-environment jsdom 3 + * 4 + * Regression test for #711: the formula-bar commit path (commitFormulaBar) 5 + * previously did NOT run the auto-format parser that the cell-editor path 6 + * uses. Typing "$100" via the formula bar stored the string "$100", so 7 + * downstream formulas like =A1*2 silently evaluated to 0. 8 + * 9 + * The fix routes formula-bar commits through the same detectAndParseEntry() 10 + * logic as commitEdit() in cell-editing.ts. These tests pin that invariant. 11 + */ 12 + import { describe, it, expect, beforeEach, vi } from 'vitest'; 13 + import { commitFormulaBar } from '../src/sheets/formula-bar-ui.js'; 14 + 15 + type CellData = { v: string | number; f: string; s?: { format?: string } }; 16 + 17 + function makeDeps(overrides: Record<string, unknown> = {}) { 18 + const input = document.createElement('input'); 19 + input.id = 'formula-input'; 20 + input.className = 'formula-input'; 21 + document.body.appendChild(input); 22 + 23 + const grid = document.createElement('div'); 24 + grid.className = 'sheet-grid'; 25 + document.body.appendChild(grid); 26 + 27 + const cells = new Map<string, CellData>(); 28 + const setCellData = vi.fn((id: string, data: CellData) => { 29 + cells.set(id, data); 30 + }); 31 + 32 + return { 33 + formulaInput: input, 34 + formulaHighlightLayer: null, 35 + grid, 36 + getSelectedCell: () => ({ col: 1, row: 1 }), // A1 37 + getCellData: (id: string) => cells.get(id), 38 + setCellData, 39 + evalCache: { clear: vi.fn() }, 40 + clearSpillMaps: vi.fn(), 41 + invalidateRecalcEngine: vi.fn(), 42 + moveSelection: vi.fn(), 43 + updateFormulaBar: vi.fn(), 44 + // refreshVisibleCells() pokes at these; stub so it no-ops cleanly. 45 + computeDisplayValue: (_id: string, data: any) => data?.v, 46 + getCfRulesArray: () => [], 47 + getValidationForCell: () => null, 48 + renderSparklines: vi.fn(), 49 + _cells: cells, 50 + ...overrides, 51 + }; 52 + } 53 + 54 + describe('#711 — commitFormulaBar runs auto-format parser', () => { 55 + beforeEach(() => { 56 + document.body.innerHTML = ''; 57 + }); 58 + 59 + it('parses "$100" into numeric value 100 with format="currency"', () => { 60 + const deps = makeDeps(); 61 + deps.formulaInput.value = '$100'; 62 + commitFormulaBar(deps as any); 63 + 64 + expect(deps.setCellData).toHaveBeenCalledWith('A1', { 65 + v: 100, 66 + f: '', 67 + s: { format: 'currency' }, 68 + }); 69 + }); 70 + 71 + it('parses "75%" into numeric 0.75 with format="percent"', () => { 72 + const deps = makeDeps(); 73 + deps.formulaInput.value = '75%'; 74 + commitFormulaBar(deps as any); 75 + 76 + expect(deps.setCellData).toHaveBeenCalledWith('A1', { 77 + v: 0.75, 78 + f: '', 79 + s: { format: 'percent' }, 80 + }); 81 + }); 82 + 83 + it('parses "1,234" into numeric 1234 with format="number"', () => { 84 + const deps = makeDeps(); 85 + deps.formulaInput.value = '1,234'; 86 + commitFormulaBar(deps as any); 87 + 88 + expect(deps.setCellData).toHaveBeenCalledWith('A1', { 89 + v: 1234, 90 + f: '', 91 + s: { format: 'number' }, 92 + }); 93 + }); 94 + 95 + it('parses "2026-03-15" into timestamp with format="date"', () => { 96 + const deps = makeDeps(); 97 + deps.formulaInput.value = '2026-03-15'; 98 + commitFormulaBar(deps as any); 99 + 100 + const call = deps.setCellData.mock.calls[0]!; 101 + expect(call[0]).toBe('A1'); 102 + expect((call[1] as CellData).f).toBe(''); 103 + expect((call[1] as CellData).s?.format).toBe('date'); 104 + expect(typeof (call[1] as CellData).v).toBe('number'); 105 + expect((call[1] as CellData).v).toBe(Date.parse('2026-03-15')); 106 + }); 107 + 108 + it('plain number "42" still stores as numeric 42 without format', () => { 109 + const deps = makeDeps(); 110 + deps.formulaInput.value = '42'; 111 + commitFormulaBar(deps as any); 112 + 113 + expect(deps.setCellData).toHaveBeenCalledWith('A1', { v: 42, f: '' }); 114 + }); 115 + 116 + it('leaves formula "=SUM(B1:B3)" alone (no auto-format on formulas)', () => { 117 + const deps = makeDeps(); 118 + deps.formulaInput.value = '=SUM(B1:B3)'; 119 + commitFormulaBar(deps as any); 120 + 121 + expect(deps.setCellData).toHaveBeenCalledWith('A1', { v: '', f: 'SUM(B1:B3)' }); 122 + }); 123 + 124 + it('respects existing cell format (e.g. currency) over auto-detection', () => { 125 + const deps = makeDeps(); 126 + // Pre-populate A1 with a currency-formatted cell 127 + deps._cells.set('A1', { v: 0, f: '', s: { format: 'currency' } }); 128 + deps.formulaInput.value = '42'; 129 + commitFormulaBar(deps as any); 130 + 131 + // Plain "42" typed into a currency cell should stay plain number under 132 + // the existing format, not get re-auto-detected as plain-number format. 133 + expect(deps.setCellData).toHaveBeenCalledWith('A1', { v: 42, f: '' }); 134 + }); 135 + 136 + it('stores "random text" verbatim when nothing matches', () => { 137 + const deps = makeDeps(); 138 + deps.formulaInput.value = 'random text'; 139 + commitFormulaBar(deps as any); 140 + 141 + expect(deps.setCellData).toHaveBeenCalledWith('A1', { v: 'random text', f: '' }); 142 + }); 143 + });