# Action surface contract This project enforces a small “action surface contract” for Filament Resources / Pages / RelationManagers to keep table UIs consistent, quiet, and safe. ## Inspect affordance (required) Any list-style surface that exposes records must provide an **inspect affordance** so an admin can open a record. ### Surface-type decision tree The inspect model is driven by the declaration `surfaceType`: - **CRUD / List-first Resource** Use one-click open by default, normally `recordUrl()`. `PrimaryLinkColumn` is allowed only as an explicit exception with a concrete reason. - **Read-only Registry / Report** Use one-click open by default, normally `recordUrl()`. This includes scan-first reporting surfaces such as Monitoring Operations, Review Register, Evidence Overview, and read-only registry resources. - **Queue / Review** Use explicit inspect (`Inspect` row action or equivalent same-page selected detail). Do not make the full row clickable. - **History / Audit** Use explicit inspect (`Inspect` row action or equivalent same-page selected detail). Do not make the full row clickable. - **Config-lite** Edit-as-inspect is allowed, but it still uses one obvious open path and must not add a competing `View` action. ### Accepted implementations - **Clickable rows** (preferred): set `recordUrl()` for the table. - **Inspect action**: a row action used only on queue / review or history / audit surfaces where context must stay on the same page. - **Primary link column**: a column that is clearly the primary affordance to open the record, with an explicit `PrimaryLinkColumn` reason in the declaration. ### Rule: no lone “View” button Avoid rendering a table that only has a single inspect-style row action on a clickable-row surface. This creates visual noise and adds an unnecessary Actions column. Preferred approach: - Make the row clickable via `recordUrl()` and set `actions([])` so no Actions column is rendered. ### PrimaryLinkColumn exception rule Use `PrimaryLinkColumn` only when full-row click is the wrong interaction model for that specific surface. - The declaration must use a clickable surface type (`CrudListFirstResource`, `ReadOnlyRegistryReport`, or `ConfigLite`). - The declaration must include a non-empty `primaryLinkColumnReason`. - Queue / review and history / audit surfaces may not use `PrimaryLinkColumn` as a shortcut around explicit inspect. ### Reporting / evidence register rule Review and evidence registers are governed as **ReadOnlyRegistryReport** surfaces. - `ReviewRegister` and `EvidenceOverview` keep clickable-row inspection as the primary open path. - Do not add a duplicate `View review` or equivalent open action beside the row click. - Safe non-inspect shortcuts may remain when they are clearly secondary. ## More-menu ordering Governed `ActionGroup` and `BulkActionGroup` menus use one stable order: - Navigation or inspect helpers first - Non-destructive workflow or lifecycle actions next - Destructive actions last Examples: - `Policies`: export before sync, sync before ignore/delete - `Backup schedules`: run/retry before archive or force delete - `Tenants`: related onboarding and safe navigation shortcuts before sync or verification, with archive/force delete trailing ## Placeholder groups are forbidden `ActionGroup` and `BulkActionGroup` exist to hold real secondary actions, not to reserve layout space. - Do not render an empty `More` menu after visibility, record-state, or RBAC filtering removes every effective action. - On clickable-row surfaces with only one safe shortcut, that shortcut may still live under `More` when it preserves a cleaner scan-first list. ## RBAC / safety - If the current user cannot inspect a record, `recordUrl()` must return `null` for that record. - UI visibility is not authorization; always enforce permissions at the policy / resource level.