Platform & tenant admin CRUD

List pages, toolbar, and gap-4 layout pattern for /platform and /t/{slug}.

Source: docs/ux/patterns/platform-admin-crud.md

Patrón — Admin List CRUD (tenant + platform)

Objetivo

Cualquier list CRUD admin (Permissions, Roles, Users, Audit, tenant Roles, …) MUST verse y comportarse como Locations/S-045: misma anatomía de lista, mismos organismos @ivprior/ui/data-table-*, mismo bulk/export/RBAC, mismos márgenes (AdminPageToolbar fuera + content gap-4 p-4). No inventar layouts por feature.

Ámbito

IncluyeExcluye
Listas CRUD /platform/...Login / forgot password
Listas CRUD finance routes (/settings/..., listados) (access, etc.)Dashboard KPI-only sin tabla CRUD
Modales create/edit/deleteImpersonation flows one-off
Filtros, export, bulk, paginación

Form surface (create/edit): gate admin-create-edit-surface.mdA URL full-page · B modal form · C quick-create nested. Si multi-paso o matriz compleja (ej. S-046 Roles), MUST wizard full-page (admin-wizard.md / S-012) — no AdminWizardModalShell en features nuevas. La lista sigue este patrón al 100 %.

Gate Mirror (agentes)

Antes de implementar o refactorizar una lista:

  1. Declarar Mirror: <path> (ej. apps/web/features/platform-admin/roles/components/PlatformRolesListPage.tsx).
  2. Leer Mirror + *Table asociado.
  3. Replicar chrome; no copiar un hermano tenant que ya divergió.
ConcernMirror (path en IVPrior, DOC-024)
Lista CRUD genéricaPrimer *ListPage local (Users o Platform Permissions)
Table + mobile cardsPrimer *Table local (AdminMobileList)
Átomos / toolbarpackages/ui · packages/admin-layout
Wizard multi-pasoPrimer *Wizard local + admin-wizard.md

Esqueleto ListPage (obligatorio)

AdminPageToolbar
  title + description
  actions = [Add primary CTA only]
  ← sin sticky · sin breadcrumbs en lista · sin Export · sin bulk

div.flex.flex-1.flex-col.gap-4.p-4   ← OBLIGATORIO (alineación con toolbar px-4)
  pageState: loading | error | empty | loaded | forbidden
  (preferir un solo return; no envolver todo en space-y-4 sin p-4)

  Card (*Table)
    CardHeader → DataTableToolbar
      search (debounce 300 ms)
      Filter popover (Apply + Reset)
      statusSlot «Refreshing…»
      trailing → DataTableExportMenu
    {bulkToolbar} → DataTableBulkBar (si selectedCount > 0)
    CardContent → AdminMobileList (cards `<lg`) → Table (`lg+`)
      checkbox col si canManage
      columns …
      row actions ⋮ (texto; Edit / Delete)
    CardFooter → DataTablePagination (10/20/30/50/All)

Dialogs: FormDialog · DeleteDialog · BulkDeleteDialog

Anti-patrón (visto en tenant Roles): div.space-y-4 envolviendo toolbar + card sin p-4 → título inset y card full-bleed.

Checklist (no omitir)

#ElementoNotas
1AdminPageToolbarSin sticky blur; lista sin breadcrumbs
2Add CTAPlusIcon + gap-1.5 shadow-sm; solo manage
3BulkDataTableBulkBar entre CardHeader y CardContent — checkboxes + «N selected / Clear / acciones dominio»
3bMobile list cardsMUST AdminMobileList + AdminMobileListCard en todo *Table admin con filas de datos — Mirror LocationsTable; cards <lg, tabla lg+; colas courier incluidas
4SearchPlaceholder "Search" / "Buscar"; texto libre sobre code, name, description, resource, action, etc.
5FilterPopover: status + DateRangeFilterFields (presets + From/To) si hay createdAt/updatedAt — ADR-062
6ExportSolo en trailing del toolbar de tabla
7SortDataTableSortHeader en columnas de datos; sortBy/sortOrder server-side
8PaginationDataTablePagination + preferencia page size persistida
9Empty / error / forbiddenVer data-states.md; forbidden = sin permiso read
10RBACpermissionCodes de session; read lista · manage mutaciones
11Help ADR-080MDX EN/ES + registry S-xxx en el mismo slice
12IAM FEAT-041Codes en catálogo; no hardcodear roles legacy

Formulario modal (create / edit)

ReglaDetalle
ChromeCancel + Save en header (derecha del título); sin ×; sin footer
HelpUn solo ? junto al título que resuma el form
Campos derivadosNo mostrar inputs editables (ej. Code); generar en submit; ver en tabla
Selects de catálogoResource: SearchableCombobox. Create Actions: SearchableMultiCombobox (un permiso por action). Edit Action: single.
DirtyFormCloseDialog
StatusToggle + hint (paridad S-045)

API list filters

Si la UI expone date range, el list API MUST aceptar query params alineados (ej. createdFrom / createdTo ISO YYYY-MM-DD) y filtrar en repositorio — no filtrar solo client-side con paginación server-side.

Si la UI expone sort en headers (DataTableSortHeader), el list API MUST aceptar sortBy + sortOrder (asc|desc) con whitelist de campos; default estable (Permissions: code asc).

i18n

Namespace platformAdmin.<feature>.* o tenantAdmin.<feature>.* (EN + ES). Keys mínimas de lista: search, filter*, export*, pageSize*, bulk*, empty*, error*, column*.

Referencias código

RecursoDónde (este repo)
Átomos / tabla / exportpackages/ui
Toolbar / wizard shellpackages/admin-layout
List / Table goldPrimer *ListPage / *Table en DOC-024

Nuevo CRUD admin (platform o tenant) — orden

  1. Spec UX S-xxx + wireframe approved + sitemap.
  2. Leer este doc + admin-ui-coherence.md + fila en DOC-024-canonical-implementations.md.
  3. Declarar Mirror (path IVPrior) y leerlo; copiar estructura del gold local.
  4. Wire API client + filters + export + bulk + RBAC.
  5. Elegir surface A/B/C (admin-create-edit-surface.md): modal form header actions + un help, o wizard/form URL S-012.
  6. Help MDX + help-content-sync.
  7. RTL: manage / read-only / forbidden / 409 / bulk checkboxes.
  8. Checklist .specify/checklists/implementation-coherence.md (ítem Mirror).

Documentos relacionados