## Summary - keep `/admin/reviews/workspace` workspace-scoped in shell and sidebar context - treat `tenant` query hints on the customer review workspace as page-level filters only - update the customer review workspace tests and Spec 311 navigation contract to match the workspace-hub IA ## Testing - `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Reviews/CustomerReviewWorkspacePageTest.php` - `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Filament/WorkspaceContextTopbarAndTenantSelectionTest.php tests/Feature/Filament/PanelNavigationSegregationTest.php` - `cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent` - `git diff --check` Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de> Reviewed-on: #366
154 lines
6.3 KiB
Markdown
154 lines
6.3 KiB
Markdown
# TenantPilot Filament Guidelines
|
|
|
|
Status: 2026-05-15
|
|
Applies to: Filament v5, Livewire v4.1, Laravel 12.
|
|
|
|
## Version Contract
|
|
|
|
- Livewire v4.0+ compliance: satisfied by Livewire 4.1.4.
|
|
- Panel provider location: `apps/platform/bootstrap/providers.php` registers `AdminPanelProvider` and `SystemPanelProvider`.
|
|
- Admin panel path: `/admin`.
|
|
- System panel path: `/system`.
|
|
- Filament asset deployment: any registered Filament assets require `cd apps/platform && php artisan filament:assets` in deployment or release build.
|
|
|
|
## Global Search Contract
|
|
|
|
- A resource may use global search only when it has a View or Edit page and a `$recordTitleAttribute`.
|
|
- Relationship-backed global search details must eager-load relationships in `getGlobalSearchEloquentQuery()`.
|
|
- If a resource is tenant-sensitive or lacks safe View/Edit URLs, set `protected static bool $isGloballySearchable = false`.
|
|
- Current examples: `PolicyResource`, `ProviderConnectionResource`, and `ManagedEnvironmentResource` disable global search, which is correct for sensitive tenant-scoped surfaces.
|
|
|
|
## Destructive and High-Impact Actions
|
|
|
|
Every destructive or high-impact action must have:
|
|
|
|
- `->action(...)`, not URL-only execution.
|
|
- `->requiresConfirmation()`.
|
|
- Policy or gate authorization inside the action handler.
|
|
- `UiEnforcement` or `WorkspaceUiEnforcement` on the visible/disabled UI state.
|
|
- Audit log entry.
|
|
- Success/error notification.
|
|
- Pest test for visible/disabled/denied/executed behavior.
|
|
|
|
Destructive examples: delete, force delete, restore, archive, retry restore, run restore, disable provider connection, purge, revoke, credential rotation, backup/restore mutations.
|
|
|
|
## Filament Do's
|
|
|
|
- Use native Filament resources, pages, tables, forms, schemas, actions, relation managers, widgets, clusters, and notifications before custom Blade/JS.
|
|
- Use render hooks and CSS hook classes instead of publishing internal Filament views.
|
|
- Keep tables scan-first: default sort, explicit empty state, sensible pagination profile, hidden technical detail columns.
|
|
- Use `ActionSurfaceDeclaration` when the resource participates in the project action-surface contract.
|
|
- Keep RelationManagers lazy-loaded unless an operator workflow requires eager loading.
|
|
- Use policies for model authorization and `UiEnforcement` for UI affordance consistency.
|
|
- Use `rateLimit()` or Laravel rate limiting for actions that can trigger expensive remote or queued work repeatedly.
|
|
|
|
## Filament Don'ts
|
|
|
|
- Do not put business workflows directly in long action closures when they mutate data or dispatch remote work.
|
|
- Do not assume confirmation modals on `->url(...)` actions.
|
|
- Do not expose user-controlled URLs to `url()` without scheme validation.
|
|
- Do not use `preserveFilenames()` for uploads on local/public disks.
|
|
- Do not enable global search on resources that cannot safely link to View/Edit pages.
|
|
- Do not hide unauthorized UI as the only security control.
|
|
- Do not add custom pages when a Resource, RelationManager, or action modal covers the workflow.
|
|
|
|
## Project-Specific Patterns
|
|
|
|
### Safe Table Action
|
|
|
|
```php
|
|
use App\Actions\BackupSchedules\StartBackupScheduleRun;
|
|
use App\Models\BackupSchedule;
|
|
use App\Support\Auth\Capabilities;
|
|
use App\Support\Rbac\UiEnforcement;
|
|
use Filament\Actions\Action;
|
|
use Filament\Notifications\Notification;
|
|
|
|
UiEnforcement::forTableAction(
|
|
Action::make('runNow')
|
|
->label('Run now')
|
|
->icon('heroicon-o-play')
|
|
->requiresConfirmation()
|
|
->modalHeading('Run backup schedule now?')
|
|
->action(function (BackupSchedule $record, StartBackupScheduleRun $starter): void {
|
|
$runId = $starter->handle(auth()->user(), $record);
|
|
|
|
Notification::make()
|
|
->title('Backup run queued')
|
|
->body("Operation run #{$runId} was created.")
|
|
->success()
|
|
->send();
|
|
}),
|
|
fn (BackupSchedule $record): mixed => $record->managedEnvironment,
|
|
)
|
|
->requireCapability(Capabilities::TENANT_BACKUP_SCHEDULES_RUN)
|
|
->apply();
|
|
```
|
|
|
|
### Extracted Schema
|
|
|
|
```php
|
|
namespace App\Filament\Resources\BackupScheduleResource\Schemas;
|
|
|
|
use Filament\Forms\Components\Select;
|
|
use Filament\Forms\Components\TextInput;
|
|
use Filament\Forms\Components\Toggle;
|
|
use Filament\Schemas\Schema;
|
|
|
|
final class BackupScheduleForm
|
|
{
|
|
public static function configure(Schema $schema): Schema
|
|
{
|
|
return $schema->schema([
|
|
TextInput::make('name')->required()->maxLength(255),
|
|
Select::make('frequency')->required()->options([
|
|
'daily' => 'Daily',
|
|
'weekly' => 'Weekly',
|
|
]),
|
|
Toggle::make('is_enabled')->label('Enabled'),
|
|
]);
|
|
}
|
|
}
|
|
```
|
|
|
|
### Extracted Table
|
|
|
|
```php
|
|
namespace App\Filament\Resources\BackupScheduleResource\Tables;
|
|
|
|
use App\Support\Filament\TablePaginationProfiles;
|
|
use Filament\Tables\Columns\TextColumn;
|
|
use Filament\Tables\Table;
|
|
|
|
final class BackupScheduleTable
|
|
{
|
|
public static function configure(Table $table): Table
|
|
{
|
|
return $table
|
|
->defaultSort('next_run_at')
|
|
->paginationPageOptions(TablePaginationProfiles::resource())
|
|
->columns([
|
|
TextColumn::make('name')->searchable()->sortable(),
|
|
TextColumn::make('status')->badge(),
|
|
TextColumn::make('next_run_at')->since()->sortable(),
|
|
])
|
|
->emptyStateHeading('No backup schedules')
|
|
->emptyStateDescription('Create a schedule after selecting a managed environment.');
|
|
}
|
|
}
|
|
```
|
|
|
|
## Migration Plan for Bad Patterns
|
|
|
|
1. Identify resource files above 1,000 LOC or actions above 60 LOC.
|
|
2. Extract repeated action orchestration into `app/Actions/<Domain>/...`.
|
|
3. Extract table columns/filters/actions into resource-local builder classes only when they reduce review risk.
|
|
4. Add policy tests before deleting resource-level authorization logic.
|
|
5. Keep one feature branch per refactor slice to avoid broad conflicts.
|
|
|
|
## Testing Plan
|
|
|
|
- Resource pages and relation managers are Livewire components and must be tested through Pest/Livewire.
|
|
- Mutating actions must use Filament action testing helpers such as `callAction`, `mountAction`, `callTableAction`, `assertActionDisabled`, and `assertTableActionVisible`.
|
|
- Browser tests are reserved for critical multi-step workflows, JS errors, accessibility regressions, and visual smoke checks.
|