feat(sheet): round-trip styles, dimensions and freeze panes through xlsx#351
Conversation
lib/xlsx was still v1 (values + formulas only) from PR #306, while the sheet model has since gained style props (M2), col/row dimensions and freeze panes (M4) and font props (#328). Export now writes cell styles, column widths, row heights and frozen first row/col; Import maps them back into the allowlisted prop vocabulary, interning into the StylePool. Imported props pass through sheet.ValidateProps (now exported) as the trust-boundary gate, since uploaded files bypass Op.Validate. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Qodo reviews are paused for this user.Troubleshooting steps vary by plan Learn more → On a Teams plan? Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center? |
PR Summary by QodoRound-trip sheet styles, dimensions, and freeze panes in XLSX import/export
AI Description
Diagram
High-Level Assessment
Files changed (5)
|
Code Review by Qodo
1. Negative col width values
|
| func colWidthToPx(w float64) int { return int(math.Round(w*7 + 5)) } | ||
| func pxToColWidth(px int) float64 { return float64(px-5) / 7 } | ||
| func rowHeightToPx(h float64) int { return int(math.Round(h * 96 / 72)) } | ||
| func pxToRowHeight(px int) float64 { return float64(px) * 72 / 96 } |
There was a problem hiding this comment.
1. Negative col width values 🐞 Bug ≡ Correctness
Export converts pixel column widths via pxToColWidth(px) = (px-5)/7, so valid sheet dimension overrides in the 1–4px range become negative and are passed to excelize.SetColWidth, yielding library-dependent behavior and potentially incorrect export sizing.
Agent Prompt
### Issue description
`pxToColWidth(px)` returns a negative value when `px < 5`, but the sheet model allows any positive dimension size (including 1–4). Export then passes the negative value into `SetColWidth`, which is an invalid/nonsensical XLSX column width.
### Issue Context
- Sheet dimension validation allows `Size > 0` (no minimum aligned to the XLSX conversion).
- XLSX conversion uses `(px-5)/7` which crosses below zero for small px values.
### Fix Focus Areas
- lib/xlsx/style.go[18-21]
- lib/xlsx/export.go[88-95]
- lib/sheet/op.go[133-142]
### Suggested fix
- Clamp in `pxToColWidth`: e.g. `if px < 5 { px = 5 }` (or clamp the computed width to `>= 0`).
- Alternatively (or additionally), tighten `OpSetDimension` validation to enforce a minimum column width that is representable in XLSX.
- Add a small unit test for `pxToColWidth` (or an export smoke test) covering px=1..4 to ensure it never produces a negative width.
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| low := strings.ToLower(code) | ||
| switch { | ||
| case strings.ContainsAny(low, "ymd") && !strings.Contains(low, "red"): // date letters; "[Red]" is a color, not a date | ||
| return "date" | ||
| case strings.Contains(code, "%"): | ||
| return fmt.Sprintf("percent:%d", dec) | ||
| case strings.ContainsAny(code, "$€£") || strings.Contains(code, "[$"): | ||
| return fmt.Sprintf("currency:%d", dec) | ||
| case strings.ContainsAny(code, "0#"): | ||
| return fmt.Sprintf("number:%d", dec) | ||
| } |
There was a problem hiding this comment.
2. Numfmt misclassified as date 🐞 Bug ≡ Correctness
codeToNumFmt checks for any 'y'/'m'/'d' character before currency/percent/number detection, so format codes containing those letters in quoted literals (e.g. "USD") are incorrectly mapped to the model's 'date' numFmt and then accepted into props.
Agent Prompt
### Issue description
`codeToNumFmt` classifies any format code containing the letters `y`, `m`, or `d` as a date *before* checking for percent/currency/number. This misclassifies non-date formats that include those letters inside quoted literals (e.g. `"USD" #,##0.00`) or other non-date tokens.
### Issue Context
- Import uses `codeToNumFmt` to set `props["numFmt"]`.
- `sheet.ValidateProps` allowlists `date`, so the misclassification survives the safety gate and changes how the client formats values.
### Fix Focus Areas
- lib/xlsx/style.go[63-91]
- lib/xlsx/style.go[221-229]
- lib/sheet/op.go[153-180]
### Suggested fix
- Reorder classification to check percent/currency/number before date detection.
- Make date detection stricter by ignoring quoted literals (and optionally bracketed sections like `[Red]`, `[$USD]`) before searching for date tokens.
- Add a regression test for a format like `"USD" #,##0.00` (or similar) ensuring it maps to `currency:<decimals>` (or `number:<decimals>`) rather than `date`.
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
What
lib/xlsxwas still v1 from the original spreadsheet PR (#306): only values and formulas survived import/export. The sheet model has since gained style props (M2 #319), col/row dimensions + freeze panes (M4 #326) and font props (#328). This PR carries all of it through the xlsx round-trip:Export — cell styles (bold/italic/underline, font color/family/size, bg fill, alignment, wrap, border, numFmt), column widths, row heights, frozen first row/col.
Import — the reverse mapping into the allowlisted prop vocabulary, interned into the workbook
StylePool; sparse dimension overrides (probed against the file default so only real overrides are stored); freeze panes clamped to the model's 0/1.Notes
numFmtvocabulary (number:2,currency:2,percent:0,date,text) maps to xlsx format codes both ways; foreign files with builtin numFmt ids are classified best-effort.sheet.validatePropsis now exported (ValidateProps) and re-applied to imported props — uploaded files bypassOp.Validatebut their props still end up as inline CSS on every viewer's DOM.GetRowsand the dimension attribute is not maintained, so import sweeps the fixed 200x52 grid for styles (cheap, import-only path).Tests
0.00%numFmt, ARGB-less color) imports correctly, so mapping gaps can't hide behind a symmetric round-trip.🤖 Generated with Claude Code