Skip to content

Add WithAutoFilter, WithFrozenRows/Columns, and FromTemplate#12

Merged
khatabwedaa merged 2 commits into
mainfrom
feat/v0.1.0-export-xlsx-csv
Mar 25, 2026
Merged

Add WithAutoFilter, WithFrozenRows/Columns, and FromTemplate#12
khatabwedaa merged 2 commits into
mainfrom
feat/v0.1.0-export-xlsx-csv

Conversation

@khatabwedaa
Copy link
Copy Markdown
Contributor

Summary

Implements two v0.1.x issues:

WithAutoFilter (#4)

  • Set Excel auto-filter dropdowns with explicit range ('A1:D1') or 'auto' mode that detects the range from headings
  • Works correctly with WithCustomStartCell

WithFrozenRows / WithFrozenColumns (#4)

  • WithFrozenRows — freeze N rows from the top (sticky headers when scrolling)
  • WithFrozenColumns — freeze N columns from the left
  • Can be combined for both-axis freezing

FromTemplate (#5)

  • Load an existing .xlsx template and replace {{placeholder}} patterns with values from bindings()
  • Single-placeholder cells get raw typed values (numbers, dates) instead of strings
  • Embedded placeholders in longer strings are concatenated correctly
  • WithTemplateData — insert repeating row data at a specific start cell
  • Preserves all original formatting, formulas, images, and merged cells
  • Combines with WithProperties, WithCsvSettings, and WithEvents

Coverage

Metric Value
Statements 99.7%
Branches 94.5%
Functions 100%
Lines 100%
Tests 76 passing

Test plan

  • WithAutoFilter — explicit range, auto mode, with custom start cell, without headings
  • WithFrozenRows — freeze heading row
  • WithFrozenColumns — freeze columns
  • Both frozen rows + columns combined
  • FromTemplate — placeholder replacement, repeating rows, preserved formatting, missing file error
  • FromTemplate + WithProperties, + CSV output, + BOM, embedded placeholders, partial bindings, async data
  • All 76 tests pass, build clean

🤖 Generated with Claude Code

Closes #4 — WithAutoFilter + WithFrozenRows:
- WithAutoFilter: set Excel auto-filter dropdowns with explicit range
  or 'auto' mode that detects range from headings
- WithFrozenRows: freeze N rows from top (sticky headers)
- WithFrozenColumns: freeze N columns from left
- numberToColumnLetter helper for auto-filter range calculation

Closes #5 — FromTemplate:
- Load an existing .xlsx template and replace {{placeholder}} bindings
- WithTemplateData: insert repeating row data at a specific start cell
- Preserves all original formatting, formulas, and merged cells
- Supports combining with WithProperties and WithCsvSettings
- Handles embedded placeholders in longer strings
- Single-placeholder cells get raw typed values (numbers, dates, etc.)

Tests: 76 passing (18 new), 100% lines/functions coverage.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings March 25, 2026 03:39
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds new “concerns” to enhance generated spreadsheets (auto-filters, freeze panes) and introduces a template-based export mode that fills existing .xlsx files with placeholder bindings and optional repeating row data.

Changes:

  • Add WithAutoFilter (explicit range or auto-detected from headings) and WithFrozenRows / WithFrozenColumns (freeze panes) support.
  • Add FromTemplate + WithTemplateData to load .xlsx templates, replace {{placeholders}}, and write repeating rows.
  • Add helper numberToColumnLetter() and expand test coverage for the new concerns/features.

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
test/fixtures/create-template.ts Adds a programmatic .xlsx template fixture used by template-related tests.
test/excel.service.spec.ts Adds tests for numberToColumnLetter, auto-filtering, freeze panes, and template exports.
src/index.ts Re-exports new concerns from the public package entrypoint.
src/helpers/index.ts Re-exports numberToColumnLetter from helpers.
src/helpers/file-type-detector.ts Implements numberToColumnLetter() helper.
src/excel.writer.ts Adds template-based export path (FromTemplate, WithTemplateData) and CSV/XLSX serialization for templates.
src/excel.sheet.ts Implements WithAutoFilter and frozen panes logic during sheet population.
src/concerns/with-frozen-rows.interface.ts Introduces WithFrozenRows / WithFrozenColumns interfaces.
src/concerns/with-auto-filter.interface.ts Introduces WithAutoFilter interface.
src/concerns/from-template.interface.ts Introduces FromTemplate / WithTemplateData interfaces.
src/concerns/index.ts Exports the newly added concerns.
Comments suppressed due to low confidence (1)

src/excel.sheet.ts:181

  • In auto mode, the filter row is computed using headingStartRow, which is currently set to the first heading row. Since WithHeadings supports multiple heading rows, this will place the auto-filter dropdowns on the wrong row when headings are provided as string[][]. Consider tracking the last heading row written (e.g., headingEndRow = currentRow - 1 after the headings loop) and using that row for the auto-filter range.
  let headingColCount = 0;
  let headingStartRow = startRow;

  // --- headings -----------------------------------------------------
  if (isWithHeadings(exportable)) {
    const headings = exportable.headings();
    const headingRows = Array.isArray(headings[0]) ? headings : [headings];
    headingStartRow = currentRow;

    for (const headingRow of headingRows as string[][]) {
      if (headingRow.length > headingColCount) {
        headingColCount = headingRow.length;
      }
      const row = worksheet.getRow(currentRow);
      headingRow.forEach((val, idx) => {
        row.getCell(startCol + idx).value = val;
      });
      row.commit();
      currentRow++;
    }
  }

  // --- data rows ----------------------------------------------------

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/excel.writer.ts
Comment on lines +191 to +265
async function writeTemplateExport(
exportable: FromTemplate & object,
type: ExcelType,
options: ExcelModuleOptions,
): Promise<Buffer> {
const templatePath = exportable.templatePath();
if (!fs.existsSync(templatePath)) {
throw new Error(`Template file not found: "${templatePath}"`);
}

const workbook = new Workbook();
await workbook.xlsx.readFile(templatePath);

const bindings = exportable.bindings();

// --- replace placeholders in all sheets ---------------------------
for (const worksheet of workbook.worksheets) {
worksheet.eachRow((row) => {
row.eachCell((cell) => {
if (typeof cell.value === "string") {
let value: string = cell.value;
for (const [placeholder, replacement] of Object.entries(bindings)) {
if (value.includes(placeholder)) {
value = value.split(placeholder).join(String(replacement));
}
}
// If the entire cell was a single placeholder, use the raw type
if (
Object.keys(bindings).length > 0 &&
Object.keys(bindings).some((k) => cell.value === k)
) {
cell.value = bindings[cell.value as string];
} else if (value !== cell.value) {
cell.value = value;
}
}
});
});
}

// --- insert repeating row data ------------------------------------
if (isWithTemplateData(exportable)) {
const startRef = parseCellRef(exportable.dataStartCell());
const rows = await exportable.templateData();
const worksheet = workbook.worksheets[0];

for (let i = 0; i < rows.length; i++) {
const wsRow = worksheet.getRow(startRef.row + i);
rows[i].forEach((val, colIdx) => {
wsRow.getCell(startRef.col + colIdx).value = val;
});
wsRow.commit();
}
}

// --- apply document properties if present -------------------------
if (isWithProperties(exportable)) {
const props = (exportable as any).properties();
if (props.creator) workbook.creator = props.creator;
if (props.lastModifiedBy) workbook.lastModifiedBy = props.lastModifiedBy;
if (props.title) workbook.title = props.title;
if (props.subject) workbook.subject = props.subject;
if (props.description) workbook.description = props.description;
if (props.keywords) workbook.keywords = props.keywords;
if (props.category) workbook.category = props.category;
if (props.company) workbook.company = props.company;
if (props.manager) workbook.manager = props.manager;
}

// --- fire events --------------------------------------------------
fireEvent(exportable, ExcelExportEvent.BEFORE_WRITING, {
exportable,
workbook,
});

Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Template-based exports currently only fire the BEFORE_WRITING event. The normal export path also fires BEFORE_EXPORT and BEFORE_SHEET/AFTER_SHEET, so WithEvents handlers will observe a different lifecycle depending on export type. To keep the event contract consistent (and match the PR description that templating composes with WithEvents), mirror the same event sequence in writeTemplateExport (e.g., BEFORE_EXPORT after loading the workbook, and BEFORE_SHEET/AFTER_SHEET around per-worksheet placeholder/data mutations).

Copilot uses AI. Check for mistakes.
Comment on lines +56 to +65
export function numberToColumnLetter(num: number): string {
let result = "";
let n = num;
while (n > 0) {
const remainder = (n - 1) % 26;
result = String.fromCharCode(65 + remainder) + result;
n = Math.floor((n - 1) / 26);
}
return result;
}
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

numberToColumnLetter() silently returns an empty string for num <= 0 (and produces potentially surprising results for non-integers). Since this helper is now part of the public helpers API, consider validating input (e.g., require a positive integer) and throwing a clear error when the input is out of range to avoid generating invalid cell/range references downstream.

Copilot uses AI. Check for mistakes.
Corrects auto-filter positioning to apply to the last heading row instead of the first when multiple heading rows exist, ensuring filters appear on the correct data columns.

Adds event lifecycle support for template-based exports, firing beforeExport, beforeSheet, afterSheet, and beforeWriting events to enable customization of template workbooks.

Enhances column letter conversion helper with validation to prevent invalid input and throw descriptive errors for non-positive integers.

Updates documentation with comprehensive examples for auto-filter, frozen rows/columns, and template features including data population and placeholder bindings.
@khatabwedaa khatabwedaa merged commit 2945aff into main Mar 25, 2026
2 checks passed
@khatabwedaa khatabwedaa deleted the feat/v0.1.0-export-xlsx-csv branch March 25, 2026 04:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

FromTemplate — fill existing Excel templates with data WithAutoFilter + WithFrozenRows — freeze panes and filter dropdowns

2 participants