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