Files
breadcrumb-the-shire/templates/partials/app-list-export-dropdown.phtml
fs 12356e2266 feat(core): generic CSV export primitive + helpdesk domains consumer
Add a reusable CSV-export building block to the core so any Grid.js list
page can gain a filter/sort-aware download with two clicks of glue.

Core primitive:
- core/Service/Export/CsvExportService: flavor-aware writer (plain CSV
  or Excel-compatible UTF-8+BOM+semicolon), with OWASP-aligned formula-
  injection escape (=, @, +, -, TAB, CR) applied to both rows *and*
  headers. Value objects for columns (ExportColumn) carry an extractor
  closure and an allowSignedNumeric flag for phone-number-shaped cells.
- core/Support/helpers/export.php: thin HTTP layer (exportSendCsv,
  exportCapLimit, exportResolveFlavor, exportRequireGetRequest) reusing
  the requestInput() contract for GR-CORE-003 consistency. Filename is
  sanitized and length-clamped; callers must still enforce auth.
- templates/partials/app-list-export-dropdown.phtml: zero-config
  <details class="dropdown"> with CSV + Excel triggers.
- web/js/core/app-list-export.js: initListExport({ gridConfig, exportUrl })
  mirrors the current grid filters + sort onto the export URL and
  navigates, preserving session cookies for download.

First consumer — Helpdesk domains:
- DomainListService extracts the shared filter/enrich/sort logic from
  domains-data so the grid endpoint and the new domains/export endpoint
  cannot drift.
- domains/export endpoint delegates to DomainListService, declares
  ExportColumns (with translated level labels), and exits via
  exportSendCsv.
- domains-data now ~20 lines, delegating to DomainListService.

Tests:
- CsvExportServiceTest (10 cases): both flavors, BOM/no-BOM, formula
  escapes incl. TAB/CR, header escape, signed-numeric allowlist,
  quoting, multiline, empty columns, generator-compatible iterable.
- ExportHelpersTest (5 cases): exportCapLimit bounds.
- DomainListServiceTest (8 cases): filter, search, "all" sentinel,
  sort, paging, enrichment, BC failure, tenant-scoped security filter.

Gates: PHPUnit green, PHPStan clean on touched files, module:sync ok.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 21:57:19 +02:00

36 lines
1.3 KiB
PHTML

<?php
/**
* Reusable export dropdown for list pages.
*
* Renders a zero-config <details class="dropdown"> with two triggers:
* [data-list-export="csv"] — plain CSV
* [data-list-export="excel"] — UTF-8 BOM + semicolon (opens in Excel)
*
* The URL is NOT set here. The JS module `/js/core/app-list-export.js`
* picks up the trigger clicks, reads the grid filter/sort state, and
* navigates to the configured export endpoint.
*
* Consumer contract:
* - Pass the export endpoint to the JS module via page config
* (initListExport({ gridConfig, exportUrl })).
* - Place this partial inside $listTitleActionsHtml.
*/
?>
<details class="dropdown app-list-export-dropdown" data-list-export-menu>
<summary role="button" class="secondary outline">
<i class="bi bi-download" aria-hidden="true"></i> <?php e(t('Export')); ?>
</summary>
<ul dir="rtl">
<li dir="ltr">
<button class="transparent" type="button" data-list-export="csv">
<i class="bi bi-filetype-csv" aria-hidden="true"></i> <?php e(t('Export as CSV')); ?>
</button>
</li>
<li dir="ltr">
<button class="transparent" type="button" data-list-export="excel">
<i class="bi bi-file-earmark-spreadsheet" aria-hidden="true"></i> <?php e(t('Export for Excel')); ?>
</button>
</li>
</ul>
</details>