Feat: Updating Data Erasure and Data Export Views

This commit is contained in:
2026-09-16 00:46:13 +03:00
parent c084eb47cb
commit 027f7e8982
3 changed files with 159 additions and 66 deletions
@@ -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
@@ -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(),
]);
}
@@ -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(),
];
}
}