Feat: Updating Data Erasure and Data Export Views
This commit is contained in:
@@ -5,11 +5,11 @@ namespace Modules\Core\Privacy\Filament\Resources;
|
|||||||
use Filament\Schemas\Schema;
|
use Filament\Schemas\Schema;
|
||||||
use Filament\Actions\ViewAction;
|
use Filament\Actions\ViewAction;
|
||||||
use Filament\Actions\Action;
|
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\ListDataErasureRequests;
|
||||||
use Modules\Core\Privacy\Filament\Resources\DataErasureRequestResource\Pages\ViewDataErasureRequest;
|
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\Resources\Resource;
|
||||||
use Filament\Tables\Columns\TextColumn;
|
use Filament\Tables\Columns\TextColumn;
|
||||||
use Filament\Tables\Filters\SelectFilter;
|
use Filament\Tables\Filters\SelectFilter;
|
||||||
@@ -40,45 +40,86 @@ class DataErasureRequestResource extends Resource
|
|||||||
|
|
||||||
protected static ?string $pluralModelLabel = 'Erasure Requests';
|
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([
|
return $schema->components([
|
||||||
Placeholder::make('subject')
|
Section::make('Request')
|
||||||
->label('Subject')
|
->icon('heroicon-o-shield-exclamation')
|
||||||
->content(fn (DataErasureRequest $record) => sprintf(
|
->columns(4)
|
||||||
'%s (%s)',
|
->components([
|
||||||
DataErasureRequest::displayNameFor($record->subject),
|
TextEntry::make('subject')
|
||||||
$record->isForCustomer() ? 'Customer account' : 'Individual user'
|
->label('Subject')
|
||||||
)),
|
->state(fn (DataErasureRequest $record) => DataErasureRequest::displayNameFor($record->subject))
|
||||||
Placeholder::make('requested_by')
|
->weight('bold')
|
||||||
->label('Requested by')
|
->size('lg'),
|
||||||
->content(fn (DataErasureRequest $record) => DataErasureRequest::displayNameFor($record->requestedBy)),
|
TextEntry::make('subject_type')
|
||||||
TextInput::make('email')
|
->label('Scope')
|
||||||
->label('Email (snapshot at request time)')
|
->formatStateUsing(fn (DataErasureRequest $record) => $record->isForCustomer() ? 'Customer account' : 'Individual user')
|
||||||
->disabled(),
|
->badge()
|
||||||
Placeholder::make('status')
|
->icon(fn (DataErasureRequest $record) => $record->isForCustomer() ? 'heroicon-o-building-office' : 'heroicon-o-user')
|
||||||
->content(fn (DataErasureRequest $record) => $record->status->value),
|
->color(fn (DataErasureRequest $record) => $record->isForCustomer() ? 'info' : 'warning'),
|
||||||
Placeholder::make('scheduled_for')
|
TextEntry::make('email')
|
||||||
->label('Scheduled for')
|
->label('Email (snapshot at request time)')
|
||||||
->content(fn (DataErasureRequest $record) => $record->scheduled_for->toDayDateTimeString()),
|
->icon('heroicon-o-envelope')
|
||||||
Placeholder::make('cancelled_at')
|
->copyable(),
|
||||||
->label('Cancelled at')
|
TextEntry::make('requested_by')
|
||||||
->content(fn (DataErasureRequest $record) => $record->cancelled_at?->toDayDateTimeString() ?? '—'),
|
->label('Requested by')
|
||||||
Placeholder::make('completed_at')
|
->state(fn (DataErasureRequest $record) => DataErasureRequest::displayNameFor($record->requestedBy))
|
||||||
->label('Completed at')
|
->icon('heroicon-o-user-circle'),
|
||||||
->content(fn (DataErasureRequest $record) => $record->completed_at?->toDayDateTimeString() ?? '—'),
|
TextEntry::make('status')
|
||||||
Placeholder::make('caused_by')
|
->badge()
|
||||||
->label('Caused by (cascade)')
|
->formatStateUsing(fn (ErasureRequestStatus $state) => ucfirst($state->value))
|
||||||
->content(fn (DataErasureRequest $record) => $record->causedBy
|
->color(fn (ErasureRequestStatus $state) => match ($state) {
|
||||||
? "Request #{$record->causedBy->id} (".DataErasureRequest::displayNameFor($record->causedBy->subject).')'
|
ErasureRequestStatus::Pending => 'warning',
|
||||||
: 'Not a cascade — directly requested')
|
ErasureRequestStatus::Cancelled => 'gray',
|
||||||
->visible(fn (DataErasureRequest $record) => $record->caused_by_request_id !== null),
|
ErasureRequestStatus::Completed => 'success',
|
||||||
KeyValue::make('report')
|
}),
|
||||||
->label('Per-provider outcome')
|
TextEntry::make('created_at')
|
||||||
->disabled()
|
->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)
|
->visible(fn (DataErasureRequest $record) => $record->report !== null)
|
||||||
->helperText('Each provider\'s outcome once the erasure completed — see docs/privacy.md.'),
|
->components([
|
||||||
])->columns(2);
|
KeyValueEntry::make('report')
|
||||||
|
->label(''),
|
||||||
|
]),
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function table(Table $table): Table
|
public static function table(Table $table): Table
|
||||||
|
|||||||
@@ -5,10 +5,10 @@ namespace Modules\Core\Privacy\Filament\Resources;
|
|||||||
use Filament\Schemas\Schema;
|
use Filament\Schemas\Schema;
|
||||||
use Filament\Actions\ViewAction;
|
use Filament\Actions\ViewAction;
|
||||||
use Filament\Actions\Action;
|
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\ListDataExportRequests;
|
||||||
use Modules\Core\Privacy\Filament\Resources\DataExportRequestResource\Pages\ViewDataExportRequest;
|
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\Resources\Resource;
|
||||||
use Filament\Tables\Columns\TextColumn;
|
use Filament\Tables\Columns\TextColumn;
|
||||||
use Filament\Tables\Filters\SelectFilter;
|
use Filament\Tables\Filters\SelectFilter;
|
||||||
@@ -35,28 +35,77 @@ class DataExportRequestResource extends Resource
|
|||||||
|
|
||||||
protected static ?string $pluralModelLabel = 'Export Requests';
|
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([
|
return $schema->components([
|
||||||
Placeholder::make('subject')
|
Section::make('Request')
|
||||||
->label('Subject')
|
->icon('heroicon-o-arrow-down-tray')
|
||||||
->content(fn (DataExportRequest $record) => sprintf(
|
->columns(4)
|
||||||
'%s (%s)',
|
->components([
|
||||||
DataErasureRequest::displayNameFor($record->subject),
|
TextEntry::make('subject')
|
||||||
$record->isForCustomer() ? 'Customer account' : 'Individual user'
|
->label('Subject')
|
||||||
)),
|
->state(fn (DataExportRequest $record) => DataErasureRequest::displayNameFor($record->subject))
|
||||||
TextInput::make('email')
|
->weight('bold')
|
||||||
->label('Email (snapshot at request time)')
|
->size('lg'),
|
||||||
->disabled(),
|
TextEntry::make('subject_type')
|
||||||
Placeholder::make('status')
|
->label('Scope')
|
||||||
->content(fn (DataExportRequest $record) => $record->status->value),
|
->formatStateUsing(fn (DataExportRequest $record) => $record->isForCustomer() ? 'Customer account' : 'Individual user')
|
||||||
Placeholder::make('completed_at')
|
->badge()
|
||||||
->label('Completed at')
|
->icon(fn (DataExportRequest $record) => $record->isForCustomer() ? 'heroicon-o-building-office' : 'heroicon-o-user')
|
||||||
->content(fn (DataExportRequest $record) => $record->completed_at?->toDayDateTimeString() ?? '—'),
|
->color(fn (DataExportRequest $record) => $record->isForCustomer() ? 'info' : 'warning'),
|
||||||
Placeholder::make('file_path')
|
TextEntry::make('email')
|
||||||
->label('Export file')
|
->label('Email (snapshot at request time)')
|
||||||
->content(fn (DataExportRequest $record) => $record->file_path ?? 'Not generated yet'),
|
->icon('heroicon-o-envelope')
|
||||||
])->columns(2);
|
->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
|
public static function table(Table $table): Table
|
||||||
@@ -104,11 +153,7 @@ class DataExportRequestResource extends Resource
|
|||||||
])
|
])
|
||||||
->recordActions([
|
->recordActions([
|
||||||
ViewAction::make(),
|
ViewAction::make(),
|
||||||
Action::make('download')
|
self::downloadAction(),
|
||||||
->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)),
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+7
@@ -8,4 +8,11 @@ use Modules\Core\Privacy\Filament\Resources\DataExportRequestResource;
|
|||||||
class ViewDataExportRequest extends ViewRecord
|
class ViewDataExportRequest extends ViewRecord
|
||||||
{
|
{
|
||||||
protected static string $resource = DataExportRequestResource::class;
|
protected static string $resource = DataExportRequestResource::class;
|
||||||
|
|
||||||
|
protected function getHeaderActions(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
DataExportRequestResource::downloadAction(),
|
||||||
|
];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user