TenantAtlas/docs/filament-guidelines.md
ahmido bf43dad3d1 fix: enforce workspace surface scope for customer review workspace (#366)
## 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
2026-05-15 20:52:37 +00:00

6.3 KiB

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

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

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

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.