Skip to content

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.

SubclassRendersUsed for
CollectionLayout<T>List<T>Tables, grids, kanban — anything multi-entity.
EntityLayout<T>one TDetail tabs, summary cards, version detail.
AggregateLayout<T>nothing (handles its own data)Dashboards, analytics, charts.
EntityItemLayout<T>one T snapshotVersion detail card, palette previews, tiles.
EntityComparisonLayout<T>two T snapshotsAudit 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>:

SlotTypeRequiredUse
listList<CollectionLayout<T>>yesMulti-entity surfaces (table, grid, kanban).
detailsList<EntityLayout<T>>noDetail-page tabs.
summaryEntityLayout<T>?noCompact card; rendered as the singleton main view, in references, and in pickers.
dashboardAggregateLayout<T>?noAggregate landing page at /<prefix>/dashboard.
analyticsList<AggregateLayout<T>>noCharts and trend visualisations.
itemsList<EntityItemLayout<T>>noSingle-entity primitives looked up by identifier (getItem('default')).
comparisonsList<EntityComparisonLayout<T>>noMulti-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

  1. The route builder resolves the URL into (entityId, tab).
  2. The workspace fetches the entity through its EntityListController / detail view.
  3. EntityTabsWidget reads config.layouts.details, filters by Authorize, caches the result through EntityPermissionCache, and selects the layout whose identifier matches tab.
  4. 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:

dart
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 defaultSortField and bool 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 with Versionable.
  • EntityAuditLayout<T> — chronological audit of every change with field diffs, formatted via FieldFormatter.

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:

dart
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.

WorkspaceReads from layouts
EntityWorkspace<T>list, details, summary, dashboard, analytics.
SingletonEntityWorkspace<T>summary (default main view), details (tabs), dashboard if present. No list.
GroupedEntityWorkspaceEach 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:

ModeEffect
noneFires onSelectionChanged; no navigation.
navigateDefault. Tap pushes the detail route.
responsiveNavigate 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:

ScopePath shapeUse 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.
  • PermissionsAuthorize filtering details.
  • Building UI — using EntityListView, EntityView, and the workspace shapes in practice.