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>
22 lines
748 B
PHP
22 lines
748 B
PHP
<?php
|
|
|
|
use MintyPHP\Http\SessionStoreInterface;
|
|
use MintyPHP\Module\Helpdesk\HelpdeskAuthorizationPolicy;
|
|
use MintyPHP\Module\Helpdesk\Service\DomainListService;
|
|
use MintyPHP\Support\Guard;
|
|
|
|
Guard::requireLogin();
|
|
Guard::requireAbilityOrForbidden(HelpdeskAuthorizationPolicy::ABILITY_ACCESS);
|
|
gridRequireGetRequest();
|
|
|
|
$session = app(SessionStoreInterface::class)->all();
|
|
$tenantId = (int) ($session['current_tenant']['id'] ?? 0);
|
|
|
|
$filters = gridParseFiltersFromSchemaFile(__DIR__ . '/domains/filter-schema.php');
|
|
$filters['limit'] = (int) ($filters['limit'] ?? 10);
|
|
$filters['offset'] = (int) ($filters['offset'] ?? 0);
|
|
|
|
$result = app(DomainListService::class)->query($tenantId, $filters);
|
|
|
|
gridJsonDataResult($result['rows'], $result['total']);
|