From a3538f647005783a08fcebbc1082a68683592ada Mon Sep 17 00:00:00 2001 From: Codex Agent Date: Tue, 6 Jan 2026 12:59:38 +0100 Subject: [PATCH] Fix data exports UI and scope format --- .../Tables/DataExportTable.php | 12 ++++++- resources/js/admin/mobile/DataExportsPage.tsx | 30 ++++++++++++++++-- .../js/admin/mobile/lib/dataExports.test.ts | 22 +++++++++++++ resources/js/admin/mobile/lib/dataExports.ts | 5 +++ tests/Unit/DataExportTableTest.php | 31 +++++++++++++++++++ 5 files changed, 96 insertions(+), 4 deletions(-) create mode 100644 resources/js/admin/mobile/lib/dataExports.test.ts create mode 100644 resources/js/admin/mobile/lib/dataExports.ts create mode 100644 tests/Unit/DataExportTableTest.php diff --git a/app/Filament/Resources/DataExportResource/Tables/DataExportTable.php b/app/Filament/Resources/DataExportResource/Tables/DataExportTable.php index 84576c1..3b06410 100644 --- a/app/Filament/Resources/DataExportResource/Tables/DataExportTable.php +++ b/app/Filament/Resources/DataExportResource/Tables/DataExportTable.php @@ -2,6 +2,7 @@ namespace App\Filament\Resources\DataExportResource\Tables; +use App\Enums\DataExportScope; use App\Jobs\GenerateDataExport; use App\Models\DataExport; use App\Services\Audit\SuperAdminAuditLogger; @@ -14,6 +15,15 @@ use Illuminate\Support\Number; class DataExportTable { + public static function formatScope(DataExportScope|string|null $state): string + { + if ($state instanceof DataExportScope) { + $state = $state->value; + } + + return $state ? __('admin.data_exports.scope.'.$state) : '—'; + } + public static function configure(Table $table): Table { return $table @@ -31,7 +41,7 @@ class DataExportTable TextColumn::make('scope') ->label(__('admin.data_exports.fields.scope')) ->badge() - ->formatStateUsing(fn (?string $state) => $state ? __('admin.data_exports.scope.'.$state) : '—'), + ->formatStateUsing(fn (DataExportScope|string|null $state): string => self::formatScope($state)), TextColumn::make('status') ->label(__('admin.data_exports.fields.status')) ->badge() diff --git a/resources/js/admin/mobile/DataExportsPage.tsx b/resources/js/admin/mobile/DataExportsPage.tsx index 1b23e4d..3187fc4 100644 --- a/resources/js/admin/mobile/DataExportsPage.tsx +++ b/resources/js/admin/mobile/DataExportsPage.tsx @@ -23,6 +23,7 @@ import { formatRelativeTime } from './lib/relativeTime'; import { useAdminTheme } from './theme'; import i18n from '../i18n'; import { triggerDownloadFromBlob } from './invite-layout/export-utils'; +import { hasInProgressExports } from './lib/dataExports'; const statusTone: Record = { pending: 'warning', @@ -89,6 +90,22 @@ export default function MobileDataExportsPage() { void load(); }, [load]); + const hasInProgress = hasInProgressExports(exports); + + React.useEffect(() => { + if (!hasInProgress) { + return; + } + + const interval = window.setInterval(() => { + void load(); + }, 5000); + + return () => { + window.clearInterval(interval); + }; + }, [hasInProgress, load]); + const handleRequest = async () => { if (requesting) { return; @@ -152,7 +169,7 @@ export default function MobileDataExportsPage() { value={scope} onChange={(event) => setScope(event.target.value as 'tenant' | 'event')} compact - style={{ minWidth: 160 }} + style={{ minWidth: 140, maxWidth: 180 }} > @@ -167,7 +184,7 @@ export default function MobileDataExportsPage() { value={eventId ? String(eventId) : ''} onChange={(event) => setEventId(event.target.value ? Number(event.target.value) : null)} compact - style={{ minWidth: 200 }} + style={{ minWidth: 180, maxWidth: 220 }} > {events.map((event) => ( @@ -187,7 +204,14 @@ export default function MobileDataExportsPage() { {t('dataExports.fields.includeMediaHint', 'Bigger ZIP; choose when needed.')} - + + + { + it('detects in-progress exports', () => { + const result = hasInProgressExports([ + { id: 1, status: 'pending' } as any, + { id: 2, status: 'ready' } as any, + ]); + + expect(result).toBe(true); + }); + + it('returns false when exports are terminal', () => { + const result = hasInProgressExports([ + { id: 1, status: 'ready' } as any, + { id: 2, status: 'failed' } as any, + ]); + + expect(result).toBe(false); + }); +}); diff --git a/resources/js/admin/mobile/lib/dataExports.ts b/resources/js/admin/mobile/lib/dataExports.ts new file mode 100644 index 0000000..e03f458 --- /dev/null +++ b/resources/js/admin/mobile/lib/dataExports.ts @@ -0,0 +1,5 @@ +import type { DataExportSummary } from '../../api'; + +export function hasInProgressExports(exports: DataExportSummary[]): boolean { + return exports.some((entry) => entry.status === 'pending' || entry.status === 'processing'); +} diff --git a/tests/Unit/DataExportTableTest.php b/tests/Unit/DataExportTableTest.php new file mode 100644 index 0000000..e492553 --- /dev/null +++ b/tests/Unit/DataExportTableTest.php @@ -0,0 +1,31 @@ +assertSame(__('admin.data_exports.scope.tenant'), $label); + } + + public function test_formats_scope_from_string(): void + { + $label = DataExportTable::formatScope('event'); + + $this->assertSame(__('admin.data_exports.scope.event'), $label); + } + + public function test_formats_scope_from_null(): void + { + $label = DataExportTable::formatScope(null); + + $this->assertSame('—', $label); + } +}