Layouts
Layouts describe how an entity is rendered. The framework distinguishes five concerns by class hierarchy — collection, single-entity, aggregate, single-entity primitive ("item"), and multi-entity primitive ("comparison"). EntityLayouts<T> groups them all behind one descriptor on the configuration. The workspace picks the right slot for the URL, applies Authorize filters once at mount, and renders.
The hierarchy
LayoutBase<T> carries metadata (identifier, title, icon, optional category, priority, Authorize gate, and *Resolver overloads for localisation). Each subclass adds the build signature appropriate to its job.
| Subclass | Renders | Used for |
|---|---|---|
CollectionLayout<T> | List<T> | Tables, grids, kanban — anything multi-entity. |
EntityLayout<T> | one T | Detail tabs, summary cards, version detail. |
AggregateLayout<T> | nothing (handles its own data) | Dashboards, analytics, charts. |
EntityItemLayout<T> | one T snapshot | Version detail card, palette previews, tiles. |
EntityComparisonLayout<T> | two T snapshots | Audit deltas, side-by-side diffs. |
The framework ships default item / comparison layouts (a field/value table and a field/value delta table) that activate when the configuration does not register its own 'default'.
EntityLayouts slots
EntityLayouts<T> is the descriptor on EntityConfiguration<T>:
| Slot | Type | Required | Use |
|---|---|---|---|
list | List<CollectionLayout<T>> | yes | Multi-entity surfaces (table, grid, kanban). |
details | List<EntityLayout<T>> | no | Detail-page tabs. |
summary | EntityLayout<T>? | no | Compact card; rendered as the singleton main view, in references, and in pickers. |
dashboard | AggregateLayout<T>? | no | Aggregate landing page at /<prefix>/dashboard. |
analytics | List<AggregateLayout<T>> | no | Charts and trend visualisations. |
items | List<EntityItemLayout<T>> | no | Single-entity primitives looked up by identifier (getItem('default')). |
comparisons | List<EntityComparisonLayout<T>> | no | Multi-entity primitives looked up by identifier (getComparison('default')). |
EntityLayouts.copyWith(...) is the canonical extension hook for feature packages that want to add tabs to a base config without re-declaring it.
How a workspace picks a layout
- The route builder resolves the URL into
(entityId, tab). - The workspace fetches the entity through its
EntityListController/ detail view. EntityTabsWidgetreadsconfig.layouts.details, filters byAuthorize, caches the result throughEntityPermissionCache, and selects the layout whose identifier matchestab.- The chosen
EntityLayout<T>.build(context, entity)renders.
The same flow applies for the list layouts — the user toggles between collection layouts via viewModeId ('list', 'grid', 'inbox'), and the header surfaces the toggle only when there is more than one mode.
Permission filtering
Every layout has an optional Authorize expression. EntityLayouts.filterByPermission returns a copy with denied layouts removed:
final visibleLayouts = layouts.filterByPermission(
vyuh.entity!.authorizationProvider.can,
);The workspace shell calls this once at mount and caches the resulting detail-tab identifiers through EntityPermissionCache.setDetailTabs(...), so per-rebuild evaluations stay cheap. Item and comparison layouts have no authorization gate today — they are rendering primitives, not user-facing tabs.
See Permissions for the Authorize DSL and the AuthorizationProvider model.
Collection layouts
CollectionLayout<T> adds these to LayoutBase<T>:
Widget build(BuildContext, List<T>).bool get enableSearch— defaults to true.bool get enableMultiSelect— defaults to false.List<ListAction<List<T>>> get batchActions— empty by default.String? get viewModeId— declares which slot of the view-mode toggle the layout fills ('list','grid','inbox', etc.). When null the layout is not part of the toggle.bool get showFilterPresets— defaults to true; some layouts (e.g. kanban) opt out.String? get defaultSortFieldandbool get defaultSortAscending— initial sort applied when the layout mounts.
The shipped table implementation (EntityTableConfig in vyuh_cdx_ui / app code) takes a list of EntityTableColumn(fieldDef: ...) entries — width and minWidth live on the column, not on the field definition itself.
Entity layouts
EntityLayout<T>.build(context, entity) returns the body that fills a tab or summary surface. The framework provides default implementations for auditing and versioning:
EntityVersionLayout<T>— version timeline + diff actions for any entity withVersionable.EntityAuditLayout<T>— chronological audit of every change with field diffs, formatted viaFieldFormatter.
Domain features add tabs by extending EntityLayout<T> directly (or via helpers in their feature package).
Aggregate layouts
AggregateLayout<T>.build(context) receives no entity — the layout collects its own data through the API or services. Used for the dashboard slot and analytics tabs.
Items and comparisons
These two slots back the content shape used inside surfaces that show snapshots — version detail cards, audit delta cards, sidebar previews, palette tiles. Look them up by identifier:
final defaultItem = config.layouts.getItem('default');
final defaultDelta = config.layouts.getComparison('default');If a config does not register its own primitive, the framework's FieldValueTableItemLayout (single entity) and FieldValueDeltaComparisonLayout (two entities) take over, both rendering fields through the FieldFormatterRegistry.
How layouts compose with workspace shapes
The same EntityLayouts<T> feeds three different workspace shapes; the shape decides which slots are exercised.
| Workspace | Reads from layouts |
|---|---|
EntityWorkspace<T> | list, details, summary, dashboard, analytics. |
SingletonEntityWorkspace<T> | summary (default main view), details (tabs), dashboard if present. No list. |
GroupedEntityWorkspace | Each tab hosts its own child workspace; the host doesn't read layouts itself. |
Singletons skip the entire master-detail scaffold: the URL is the source of truth. /<base> shows the summary, /<base>/edit shows the editor, /<base>/<tab> shows that detail layout. Custom singleton main views (e.g. SecuritySettings' company badge + actions strip) are provided by overriding the singleton main route in a custom StandardRouteBuilder, never by injecting widgets into the workspace.
Responsive behaviour
EntityWorkspace integrates with cdx_panes to handle responsive panel modes (split, stacked, dock) based on the breakpoint. The EntitySelectionMode on EntityRouting controls how a row tap dispatches:
| Mode | Effect |
|---|---|
none | Fires onSelectionChanged; no navigation. |
navigate | Default. Tap pushes the detail route. |
responsive | Navigate on desktop, embed inline on mobile. |
The body layout itself reacts to viewport size via LayoutBuilder / SheetMediaQuery, not via raw MediaQuery, so content sized inside a smooth-sheet does not get clipped.
Custom routes
EntityCustomRoute<T> (declared on StandardRouteBuilder.customRoutes) attaches additional pages to the entity's route tree:
| Scope | Path shape | Use case |
|---|---|---|
| Per-entity (default constructor) | <prefix>/:id/<path> | Previews, related-data views. Builder receives the entity id. |
Top-level (EntityCustomRoute.topLevel) | <prefix>/<path> | Collection-wide pages (kanban, calendar). Builder receives no id. |
Display modes (CustomRouteDisplay.fullWidth / CustomRouteDisplay.dialog) control whether the route renders as a full page, a DialogShell on desktop, or a modal sheet on mobile. Authorize gating is propagated via the route's optional permissions list.
Next steps
- Configuration — how layouts attach to
EntityConfiguration. - Permissions —
Authorizefiltering details. - Building UI — using
EntityListView,EntityView, and the workspace shapes in practice.