Fix data exports UI and scope format
This commit is contained in:
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Filament\Resources\DataExportResource\Tables;
|
namespace App\Filament\Resources\DataExportResource\Tables;
|
||||||
|
|
||||||
|
use App\Enums\DataExportScope;
|
||||||
use App\Jobs\GenerateDataExport;
|
use App\Jobs\GenerateDataExport;
|
||||||
use App\Models\DataExport;
|
use App\Models\DataExport;
|
||||||
use App\Services\Audit\SuperAdminAuditLogger;
|
use App\Services\Audit\SuperAdminAuditLogger;
|
||||||
@@ -14,6 +15,15 @@ use Illuminate\Support\Number;
|
|||||||
|
|
||||||
class DataExportTable
|
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
|
public static function configure(Table $table): Table
|
||||||
{
|
{
|
||||||
return $table
|
return $table
|
||||||
@@ -31,7 +41,7 @@ class DataExportTable
|
|||||||
TextColumn::make('scope')
|
TextColumn::make('scope')
|
||||||
->label(__('admin.data_exports.fields.scope'))
|
->label(__('admin.data_exports.fields.scope'))
|
||||||
->badge()
|
->badge()
|
||||||
->formatStateUsing(fn (?string $state) => $state ? __('admin.data_exports.scope.'.$state) : '—'),
|
->formatStateUsing(fn (DataExportScope|string|null $state): string => self::formatScope($state)),
|
||||||
TextColumn::make('status')
|
TextColumn::make('status')
|
||||||
->label(__('admin.data_exports.fields.status'))
|
->label(__('admin.data_exports.fields.status'))
|
||||||
->badge()
|
->badge()
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import { formatRelativeTime } from './lib/relativeTime';
|
|||||||
import { useAdminTheme } from './theme';
|
import { useAdminTheme } from './theme';
|
||||||
import i18n from '../i18n';
|
import i18n from '../i18n';
|
||||||
import { triggerDownloadFromBlob } from './invite-layout/export-utils';
|
import { triggerDownloadFromBlob } from './invite-layout/export-utils';
|
||||||
|
import { hasInProgressExports } from './lib/dataExports';
|
||||||
|
|
||||||
const statusTone: Record<DataExportSummary['status'], 'success' | 'warning' | 'muted'> = {
|
const statusTone: Record<DataExportSummary['status'], 'success' | 'warning' | 'muted'> = {
|
||||||
pending: 'warning',
|
pending: 'warning',
|
||||||
@@ -89,6 +90,22 @@ export default function MobileDataExportsPage() {
|
|||||||
void load();
|
void load();
|
||||||
}, [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 () => {
|
const handleRequest = async () => {
|
||||||
if (requesting) {
|
if (requesting) {
|
||||||
return;
|
return;
|
||||||
@@ -152,7 +169,7 @@ export default function MobileDataExportsPage() {
|
|||||||
value={scope}
|
value={scope}
|
||||||
onChange={(event) => setScope(event.target.value as 'tenant' | 'event')}
|
onChange={(event) => setScope(event.target.value as 'tenant' | 'event')}
|
||||||
compact
|
compact
|
||||||
style={{ minWidth: 160 }}
|
style={{ minWidth: 140, maxWidth: 180 }}
|
||||||
>
|
>
|
||||||
<option value="tenant">{t('dataExports.scopes.tenant', 'Tenant')}</option>
|
<option value="tenant">{t('dataExports.scopes.tenant', 'Tenant')}</option>
|
||||||
<option value="event">{t('dataExports.scopes.event', 'Event')}</option>
|
<option value="event">{t('dataExports.scopes.event', 'Event')}</option>
|
||||||
@@ -167,7 +184,7 @@ export default function MobileDataExportsPage() {
|
|||||||
value={eventId ? String(eventId) : ''}
|
value={eventId ? String(eventId) : ''}
|
||||||
onChange={(event) => setEventId(event.target.value ? Number(event.target.value) : null)}
|
onChange={(event) => setEventId(event.target.value ? Number(event.target.value) : null)}
|
||||||
compact
|
compact
|
||||||
style={{ minWidth: 200 }}
|
style={{ minWidth: 180, maxWidth: 220 }}
|
||||||
>
|
>
|
||||||
<option value="">{t('dataExports.fields.eventPlaceholder', 'Choose event')}</option>
|
<option value="">{t('dataExports.fields.eventPlaceholder', 'Choose event')}</option>
|
||||||
{events.map((event) => (
|
{events.map((event) => (
|
||||||
@@ -187,7 +204,14 @@ export default function MobileDataExportsPage() {
|
|||||||
{t('dataExports.fields.includeMediaHint', 'Bigger ZIP; choose when needed.')}
|
{t('dataExports.fields.includeMediaHint', 'Bigger ZIP; choose when needed.')}
|
||||||
</Text>
|
</Text>
|
||||||
</YStack>
|
</YStack>
|
||||||
<Switch checked={includeMedia} onCheckedChange={setIncludeMedia} />
|
<Switch
|
||||||
|
size="$3"
|
||||||
|
checked={includeMedia}
|
||||||
|
onCheckedChange={setIncludeMedia}
|
||||||
|
aria-label={t('dataExports.fields.includeMedia', 'Include raw media')}
|
||||||
|
>
|
||||||
|
<Switch.Thumb />
|
||||||
|
</Switch>
|
||||||
</XStack>
|
</XStack>
|
||||||
</YStack>
|
</YStack>
|
||||||
<CTAButton
|
<CTAButton
|
||||||
|
|||||||
22
resources/js/admin/mobile/lib/dataExports.test.ts
Normal file
22
resources/js/admin/mobile/lib/dataExports.test.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import { hasInProgressExports } from './dataExports';
|
||||||
|
|
||||||
|
describe('dataExports helpers', () => {
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
5
resources/js/admin/mobile/lib/dataExports.ts
Normal file
5
resources/js/admin/mobile/lib/dataExports.ts
Normal file
@@ -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');
|
||||||
|
}
|
||||||
31
tests/Unit/DataExportTableTest.php
Normal file
31
tests/Unit/DataExportTableTest.php
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Unit;
|
||||||
|
|
||||||
|
use App\Enums\DataExportScope;
|
||||||
|
use App\Filament\Resources\DataExportResource\Tables\DataExportTable;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
class DataExportTableTest extends TestCase
|
||||||
|
{
|
||||||
|
public function test_formats_scope_from_enum(): void
|
||||||
|
{
|
||||||
|
$label = DataExportTable::formatScope(DataExportScope::TENANT);
|
||||||
|
|
||||||
|
$this->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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user