From 027f7e8982ad3299a8fb99b685cca115f3daf7a0 Mon Sep 17 00:00:00 2001 From: Konstantinos Arvanitakis Date: Wed, 16 Sep 2026 00:46:13 +0300 Subject: [PATCH] Feat: Updating Data Erasure and Data Export Views --- .../Resources/DataErasureRequestResource.php | 119 ++++++++++++------ .../Resources/DataExportRequestResource.php | 99 +++++++++++---- .../Pages/ViewDataExportRequest.php | 7 ++ 3 files changed, 159 insertions(+), 66 deletions(-) diff --git a/src/Privacy/Filament/Resources/DataErasureRequestResource.php b/src/Privacy/Filament/Resources/DataErasureRequestResource.php index 84b2fc2..490e9f2 100644 --- a/src/Privacy/Filament/Resources/DataErasureRequestResource.php +++ b/src/Privacy/Filament/Resources/DataErasureRequestResource.php @@ -5,11 +5,11 @@ namespace Modules\Core\Privacy\Filament\Resources; use Filament\Schemas\Schema; use Filament\Actions\ViewAction; use Filament\Actions\Action; +use Filament\Infolists\Components\KeyValueEntry; +use Filament\Infolists\Components\TextEntry; +use Filament\Schemas\Components\Section; use Modules\Core\Privacy\Filament\Resources\DataErasureRequestResource\Pages\ListDataErasureRequests; use Modules\Core\Privacy\Filament\Resources\DataErasureRequestResource\Pages\ViewDataErasureRequest; -use Filament\Forms\Components\KeyValue; -use Filament\Forms\Components\Placeholder; -use Filament\Forms\Components\TextInput; use Filament\Resources\Resource; use Filament\Tables\Columns\TextColumn; use Filament\Tables\Filters\SelectFilter; @@ -40,45 +40,86 @@ class DataErasureRequestResource extends Resource protected static ?string $pluralModelLabel = 'Erasure Requests'; - public static function form(Schema $schema): Schema + /** + * A real infolist, not form()'s disabled inputs/Placeholders — ViewRecord + * falls back to rendering form() in read-only mode when a resource has no + * infolist() at all (Filament\Resources\Pages\ViewRecord::hasInfolist()), + * which is what this resource did before: every field rendered as a + * plain, unstyled label/value pair with no grouping, badges, or icons. + */ + public static function infolist(Schema $schema): Schema { return $schema->components([ - Placeholder::make('subject') - ->label('Subject') - ->content(fn (DataErasureRequest $record) => sprintf( - '%s (%s)', - DataErasureRequest::displayNameFor($record->subject), - $record->isForCustomer() ? 'Customer account' : 'Individual user' - )), - Placeholder::make('requested_by') - ->label('Requested by') - ->content(fn (DataErasureRequest $record) => DataErasureRequest::displayNameFor($record->requestedBy)), - TextInput::make('email') - ->label('Email (snapshot at request time)') - ->disabled(), - Placeholder::make('status') - ->content(fn (DataErasureRequest $record) => $record->status->value), - Placeholder::make('scheduled_for') - ->label('Scheduled for') - ->content(fn (DataErasureRequest $record) => $record->scheduled_for->toDayDateTimeString()), - Placeholder::make('cancelled_at') - ->label('Cancelled at') - ->content(fn (DataErasureRequest $record) => $record->cancelled_at?->toDayDateTimeString() ?? '—'), - Placeholder::make('completed_at') - ->label('Completed at') - ->content(fn (DataErasureRequest $record) => $record->completed_at?->toDayDateTimeString() ?? '—'), - Placeholder::make('caused_by') - ->label('Caused by (cascade)') - ->content(fn (DataErasureRequest $record) => $record->causedBy - ? "Request #{$record->causedBy->id} (".DataErasureRequest::displayNameFor($record->causedBy->subject).')' - : 'Not a cascade — directly requested') - ->visible(fn (DataErasureRequest $record) => $record->caused_by_request_id !== null), - KeyValue::make('report') - ->label('Per-provider outcome') - ->disabled() + Section::make('Request') + ->icon('heroicon-o-shield-exclamation') + ->columns(4) + ->components([ + TextEntry::make('subject') + ->label('Subject') + ->state(fn (DataErasureRequest $record) => DataErasureRequest::displayNameFor($record->subject)) + ->weight('bold') + ->size('lg'), + TextEntry::make('subject_type') + ->label('Scope') + ->formatStateUsing(fn (DataErasureRequest $record) => $record->isForCustomer() ? 'Customer account' : 'Individual user') + ->badge() + ->icon(fn (DataErasureRequest $record) => $record->isForCustomer() ? 'heroicon-o-building-office' : 'heroicon-o-user') + ->color(fn (DataErasureRequest $record) => $record->isForCustomer() ? 'info' : 'warning'), + TextEntry::make('email') + ->label('Email (snapshot at request time)') + ->icon('heroicon-o-envelope') + ->copyable(), + TextEntry::make('requested_by') + ->label('Requested by') + ->state(fn (DataErasureRequest $record) => DataErasureRequest::displayNameFor($record->requestedBy)) + ->icon('heroicon-o-user-circle'), + TextEntry::make('status') + ->badge() + ->formatStateUsing(fn (ErasureRequestStatus $state) => ucfirst($state->value)) + ->color(fn (ErasureRequestStatus $state) => match ($state) { + ErasureRequestStatus::Pending => 'warning', + ErasureRequestStatus::Cancelled => 'gray', + ErasureRequestStatus::Completed => 'success', + }), + TextEntry::make('created_at') + ->label('Requested at') + ->dateTime() + ->icon('heroicon-o-calendar'), + TextEntry::make('scheduled_for') + ->label('Scheduled for') + ->dateTime() + ->icon('heroicon-o-calendar-days'), + TextEntry::make('completed_at') + ->label('Completed at') + ->dateTime() + ->placeholder('—') + ->icon('heroicon-o-check-circle') + ->color(fn (DataErasureRequest $record) => $record->completed_at ? 'success' : 'gray'), + TextEntry::make('cancelled_at') + ->label('Cancelled at') + ->dateTime() + ->placeholder('—') + ->icon('heroicon-o-x-circle') + ->color(fn (DataErasureRequest $record) => $record->cancelled_at ? 'danger' : 'gray') + ->visible(fn (DataErasureRequest $record) => $record->cancelled_at !== null), + TextEntry::make('caused_by') + ->label('Cascade') + ->icon('heroicon-o-arrow-turn-down-right') + ->state(fn (DataErasureRequest $record) => $record->causedBy + ? "From request #{$record->causedBy->id} (".DataErasureRequest::displayNameFor($record->causedBy->subject).')' + : 'Directly requested') + ->color(fn (DataErasureRequest $record) => $record->caused_by_request_id !== null ? 'info' : 'gray'), + ]), + + Section::make('Outcome') + ->description('Each provider\'s outcome once the erasure completed — see docs/privacy.md.') + ->icon('heroicon-o-document-check') ->visible(fn (DataErasureRequest $record) => $record->report !== null) - ->helperText('Each provider\'s outcome once the erasure completed — see docs/privacy.md.'), - ])->columns(2); + ->components([ + KeyValueEntry::make('report') + ->label(''), + ]), + ]); } public static function table(Table $table): Table diff --git a/src/Privacy/Filament/Resources/DataExportRequestResource.php b/src/Privacy/Filament/Resources/DataExportRequestResource.php index 2b87982..e34e93d 100644 --- a/src/Privacy/Filament/Resources/DataExportRequestResource.php +++ b/src/Privacy/Filament/Resources/DataExportRequestResource.php @@ -5,10 +5,10 @@ namespace Modules\Core\Privacy\Filament\Resources; use Filament\Schemas\Schema; use Filament\Actions\ViewAction; use Filament\Actions\Action; +use Filament\Infolists\Components\TextEntry; +use Filament\Schemas\Components\Section; use Modules\Core\Privacy\Filament\Resources\DataExportRequestResource\Pages\ListDataExportRequests; use Modules\Core\Privacy\Filament\Resources\DataExportRequestResource\Pages\ViewDataExportRequest; -use Filament\Forms\Components\Placeholder; -use Filament\Forms\Components\TextInput; use Filament\Resources\Resource; use Filament\Tables\Columns\TextColumn; use Filament\Tables\Filters\SelectFilter; @@ -35,28 +35,77 @@ class DataExportRequestResource extends Resource protected static ?string $pluralModelLabel = 'Export Requests'; - public static function form(Schema $schema): Schema + /** + * A real infolist, not form()'s disabled inputs/Placeholders — see + * DataErasureRequestResource::infolist()'s own docblock for why. + */ + public static function infolist(Schema $schema): Schema { return $schema->components([ - Placeholder::make('subject') - ->label('Subject') - ->content(fn (DataExportRequest $record) => sprintf( - '%s (%s)', - DataErasureRequest::displayNameFor($record->subject), - $record->isForCustomer() ? 'Customer account' : 'Individual user' - )), - TextInput::make('email') - ->label('Email (snapshot at request time)') - ->disabled(), - Placeholder::make('status') - ->content(fn (DataExportRequest $record) => $record->status->value), - Placeholder::make('completed_at') - ->label('Completed at') - ->content(fn (DataExportRequest $record) => $record->completed_at?->toDayDateTimeString() ?? '—'), - Placeholder::make('file_path') - ->label('Export file') - ->content(fn (DataExportRequest $record) => $record->file_path ?? 'Not generated yet'), - ])->columns(2); + Section::make('Request') + ->icon('heroicon-o-arrow-down-tray') + ->columns(4) + ->components([ + TextEntry::make('subject') + ->label('Subject') + ->state(fn (DataExportRequest $record) => DataErasureRequest::displayNameFor($record->subject)) + ->weight('bold') + ->size('lg'), + TextEntry::make('subject_type') + ->label('Scope') + ->formatStateUsing(fn (DataExportRequest $record) => $record->isForCustomer() ? 'Customer account' : 'Individual user') + ->badge() + ->icon(fn (DataExportRequest $record) => $record->isForCustomer() ? 'heroicon-o-building-office' : 'heroicon-o-user') + ->color(fn (DataExportRequest $record) => $record->isForCustomer() ? 'info' : 'warning'), + TextEntry::make('email') + ->label('Email (snapshot at request time)') + ->icon('heroicon-o-envelope') + ->copyable(), + TextEntry::make('status') + ->badge() + ->formatStateUsing(fn (ExportRequestStatus $state) => ucfirst($state->value)) + ->color(fn (ExportRequestStatus $state) => match ($state) { + ExportRequestStatus::Pending => 'warning', + ExportRequestStatus::Failed => 'danger', + ExportRequestStatus::Completed => 'success', + }), + TextEntry::make('created_at') + ->label('Requested at') + ->dateTime() + ->icon('heroicon-o-calendar'), + TextEntry::make('completed_at') + ->label('Completed at') + ->dateTime() + ->placeholder('Not generated yet') + ->icon('heroicon-o-check-circle') + ->color(fn (DataExportRequest $record) => $record->completed_at ? 'success' : 'gray'), + TextEntry::make('file_path') + ->label('File') + // Just the filename, not the full server path — a raw + // filesystem path (/var/www/.../export_2_....zip) isn't + // actionable for staff and previously rendered as if it + // were a clickable link. The actual download is the + // "Download" header action below (self::downloadAction()), + // shared with the table's row action. + ->state(fn (DataExportRequest $record) => $record->file_path ? basename($record->file_path) : 'Not generated yet') + ->icon('heroicon-o-document') + ->color(fn (DataExportRequest $record) => $record->file_path ? 'success' : 'gray'), + ]), + ]); + } + + /** + * Shared by the table's row action and the view page's header action + * (ViewDataExportRequest::getHeaderActions()) so "is this downloadable" + * and the download itself are defined in exactly one place. + */ + public static function downloadAction(): Action + { + return Action::make('download') + ->label('Download') + ->icon('heroicon-o-arrow-down-tray') + ->visible(fn (DataExportRequest $record) => $record->status === ExportRequestStatus::Completed && $record->file_path && file_exists($record->file_path)) + ->action(fn (DataExportRequest $record) => response()->download($record->file_path)); } public static function table(Table $table): Table @@ -104,11 +153,7 @@ class DataExportRequestResource extends Resource ]) ->recordActions([ ViewAction::make(), - Action::make('download') - ->label('Download') - ->icon('heroicon-o-arrow-down-tray') - ->visible(fn (DataExportRequest $record) => $record->status === ExportRequestStatus::Completed && $record->file_path && file_exists($record->file_path)) - ->action(fn (DataExportRequest $record) => response()->download($record->file_path)), + self::downloadAction(), ]); } diff --git a/src/Privacy/Filament/Resources/DataExportRequestResource/Pages/ViewDataExportRequest.php b/src/Privacy/Filament/Resources/DataExportRequestResource/Pages/ViewDataExportRequest.php index 3b470ff..e26858f 100644 --- a/src/Privacy/Filament/Resources/DataExportRequestResource/Pages/ViewDataExportRequest.php +++ b/src/Privacy/Filament/Resources/DataExportRequestResource/Pages/ViewDataExportRequest.php @@ -8,4 +8,11 @@ use Modules\Core\Privacy\Filament\Resources\DataExportRequestResource; class ViewDataExportRequest extends ViewRecord { protected static string $resource = DataExportRequestResource::class; + + protected function getHeaderActions(): array + { + return [ + DataExportRequestResource::downloadAction(), + ]; + } }