Layouts
The layout system provides a structured way to render entities in different contexts: list views (tables, grids), detail tabs, summary cards, dashboards, analytics, and one-off "snapshot" displays for versioning / audit.
All layout classes share the LayoutBase foundation and are organized through the EntityLayouts<T> container.
EntityLayouts
class EntityLayouts<T extends EntityBase> — container that groups all layout types for an entity.
Constructor
const EntityLayouts({
required this.list,
this.details = const [],
this.items = const [],
this.comparisons = const [],
this.summary,
this.dashboard,
this.analytics = const [],
});Properties
| Property | Type | Default | Description |
|---|---|---|---|
list | List<CollectionLayout<T>> | -- | List-view layouts (tables, grids, custom) |
details | List<EntityLayout<T>> | [] | Detail tabs (info, audit, versions, etc.) |
items | List<EntityItemLayout<T>> | [] | Single-entity rendering primitives — used by version cards, sidebars, command-palette previews. Looked up by identifier via getItem. |
comparisons | List<EntityComparisonLayout<T>> | [] | Multi-entity rendering primitives — used by audit delta cards, future trend / matrix views. Looked up via getComparison. |
summary | EntityLayout<T>? | null | Summary / card layout for entity preview |
dashboard | AggregateLayout<T>? | null | Dashboard layout for entity overview |
analytics | List<AggregateLayout<T>> | [] | Analytics layouts (charts, trends, heatmaps) |
Methods
| Method | Signature | Description |
|---|---|---|
EntityLayouts.function | factory EntityLayouts.function(EntityLayouts<T> Function() factory) | Build via factory (evaluated eagerly) |
filterByPermission | EntityLayouts<T> filterByPermission(bool Function(Authorize?)? authorizeResolver) | Remove layouts the user is not authorized to see |
copyWith | EntityLayouts<T> copyWith({list?, details?, items?, comparisons?, summary?, dashboard?, analytics?}) | Returns a copy |
getItem | EntityItemLayout<T>? getItem(String identifier) | Find an item layout by identifier |
getComparison | EntityComparisonLayout<T>? getComparison(String identifier) | Find a comparison layout by identifier |
getList | CollectionLayout<T>? getList(String identifier) | Find a collection layout by identifier |
getCachedDetailTabIdentifiers | List<String>? getCachedDetailTabIdentifiers(String entityIdentifier) | Cached identifiers from EntityPermissionCache |
getCachedDetailTabs | List<EntityLayout<T>>? getCachedDetailTabs(String entityIdentifier) | Cached, materialised detail tabs (clears stale entries automatically) |
setCachedDetailTabs | void setCachedDetailTabs(String entityIdentifier, List<EntityLayout<T>> tabs) | Cache filtered detail tabs |
hasDetailTabCache | bool hasDetailTabCache(String entityIdentifier) | Whether cache exists |
Usage Example
final layouts = EntityLayouts<Equipment>(
list: [
EquipmentTableLayout(),
EquipmentGridLayout(),
],
details: [
EquipmentDetailTab(),
EquipmentVersionsTab(),
EquipmentAuditTab(),
],
items: const [], // optional snapshot renderers
comparisons: const [], // optional multi-snapshot renderers
summary: EquipmentSummaryLayout(),
dashboard: EquipmentDashboardLayout(),
analytics: [
EquipmentUsageHeatmap(),
EquipmentDistributionChart(),
],
);LayoutBase
abstract class LayoutBase<T extends EntityBase> implements SchemaItem — foundation for every layout type. Provides metadata, localization, and authorization gating.
Constructor
const LayoutBase({
required this.schemaType,
required this.identifier,
required String title,
required this.icon,
String? description,
this.priority = 0,
String? category,
this.enabled = true,
this.titleResolver,
this.descriptionResolver,
this.categoryResolver,
this.authorize,
});Properties
| Property | Type | Default | Description |
|---|---|---|---|
schemaType | String | -- | Schema type identifier (from SchemaItem) |
identifier | String | -- | Unique identifier for this layout |
title | String | -- | Display title (uses titleResolver if set) |
icon | IconData | -- | Icon representing this layout |
description | String? | null | Description (uses descriptionResolver if set) |
priority | int | 0 | Sort priority (higher = shown first) |
category | String? | null | Category grouping (uses categoryResolver if set) |
enabled | bool | true | Whether the layout is shown by default |
titleResolver | String Function()? | null | Localized title resolver |
descriptionResolver | String Function()? | null | Localized description resolver |
categoryResolver | String Function()? | null | Localized category resolver |
authorize | Authorize? | null | Authorization expression gating visibility |
headerActionsBuilder | Widget Function(BuildContext)? | null | Optional builder for layout-specific header actions |
Authorization replaces permissions: List<String>?
Layouts now gate visibility through an Authorize expression rather than a raw permission list. Use Authorize.permission('lms.area.view') or combinators like Authorize.allOf([...]). See Permissions.
Localization
EquipmentDetailTab()
: super(
schemaType: 'equipment.layout.detail',
identifier: 'details',
title: 'Details',
titleResolver: () => t.strings.equipment.tabs.details,
icon: Icons.info,
);CollectionLayout
abstract class CollectionLayout<T extends EntityBase> extends LayoutBase<T> — displays multiple entity instances. Used for tables, card grids, and custom list displays.
Renamed from ListLayout
The old ListLayout<T> is gone. Use CollectionLayout<T>. The ListAction<T> model is unchanged (lives in collection_layout.dart).
Abstract method
Widget build(BuildContext context, List<T> items);Overridable getters
| Getter | Type | Default | Description |
|---|---|---|---|
enableSearch | bool | true | Whether this layout supports search |
enableMultiSelect | bool | false | Whether multi-select is supported |
batchActions | List<ListAction<List<T>>> | [] | Batch actions for selected items |
viewModeId | String? | null | View mode toggle identifier ('list', 'grid', 'inbox', ...). Layouts without an id are excluded from the toggle. |
showFilterPresets | bool | true | Show filter preset buttons in header |
defaultSortField | String? | null | Default sort field on first display |
defaultSortAscending | bool | true | Default sort direction |
Example
class EquipmentTableLayout extends CollectionLayout<Equipment> {
const EquipmentTableLayout()
: super(
schemaType: 'equipment.layout.table',
identifier: 'table',
title: 'Table',
icon: Icons.table_chart,
);
@override
String? get viewModeId => 'list';
@override
Widget build(BuildContext context, List<Equipment> items) {
return EquipmentTable(items: items);
}
}EntityLayout
abstract class EntityLayout<T extends EntityBase> extends LayoutBase<T> — displays a single entity instance. Used for detail tabs and summary views.
Widget build(BuildContext context, T entity);Example
class EquipmentDetailTab extends EntityLayout<Equipment> {
const EquipmentDetailTab()
: super(
schemaType: 'equipment.layout.detail',
identifier: 'details',
title: 'Details',
icon: Icons.info,
);
@override
Widget build(BuildContext context, Equipment entity) {
return EquipmentDetailView(entity: entity);
}
}AggregateLayout
abstract class AggregateLayout<T extends EntityBase> extends LayoutBase<T> — displays aggregate data without a specific entity instance. Used for dashboards and analytics views.
Widget build(BuildContext context);Unlike EntityLayout.build(), this does not receive an entity instance — aggregate layouts operate on the entity type.
Example
class EquipmentDashboard extends AggregateLayout<Equipment> {
const EquipmentDashboard()
: super(
schemaType: 'equipment.analytics.dashboard',
identifier: 'dashboard',
title: 'Dashboard',
icon: Icons.dashboard,
);
@override
Widget build(BuildContext context) => EquipmentDashboardWidget();
}EntityItemLayout
abstract class EntityItemLayout<T extends EntityBase> — renders a single entity instance as a self-contained widget. Used wherever a typed snapshot of one entity needs to be displayed:
- The version detail card (current state of one version)
- Past-activity sidebars, dashboard tiles, command-palette previews
- Anywhere else that today reaches for hand-rolled
Card+Text(entity.name)
abstract class EntityItemLayout<T extends EntityBase> {
String get identifier;
String? get title;
Widget build(BuildContext context, T entity);
}Lookup uses identifier — 'default' is the framework's field/value table; entities can register 'compact', 'card', 'past-activity', etc. for domain-specific renderings.
The builder always receives the entity as a typed T. Callers with a Map<String, dynamic> snapshot (versioning / audit pipelines) deserialize through EntityApi<T>.fromJson first; schema-incompatible snapshots fail at the boundary, not inside the layout.
EntityComparisonLayout
abstract class EntityComparisonLayout<T extends EntityBase> — renders multiple entity instances side-by-side for comparison. Generalises the audit before/after delta into a "primary + secondaries" shape that scales beyond two snapshots.
abstract class EntityComparisonLayout<T extends EntityBase> {
String get identifier;
String? get title;
Widget build(
BuildContext context, {
required ({String label, T entity}) primary,
required List<({String label, T entity})> secondaries,
});
}The default registered implementation ('default') is the field/value delta table audit uses today — old/new columns with red/blue tinted cells. Future comparison layouts ('trend', 'matrix', ...) can render charts, version-over-version timelines, A/B-style diff views, etc.
ListAction
Defined in collection_layout.dart — defines an action available in collection layouts (e.g., row-level quick actions).
class ListAction<T> {
final String label;
final IconData icon;
final Future<void> Function(T entity, BuildContext context) onPressed;
final bool Function(T entity)? isVisible;
final bool Function(T entity)? isEnabled;
const ListAction({
required this.label,
required this.icon,
required this.onPressed,
this.isVisible,
this.isEnabled,
});
}Authorization filtering
Layouts opt into authorization gating via LayoutBase.authorize. The EntityLayouts.filterByPermission method removes layouts whose Authorize expression does not pass.
final filteredLayouts = layouts.filterByPermission(
(auth) => auth == null
? true
: (vyuh.entity?.authorizationProvider.can(auth) ?? true),
);Detail-tab permission results are cached per entity identifier via EntityPermissionCache so the filter only runs once per type. The cache is cleared on logout via EntityConfiguration.clearAllPermissionCaches(). Stale cache entries (e.g. after hot restart) are cleared automatically by getCachedDetailTabs when the cached identifier set is smaller than the current layout list.
Item & comparison layouts have no authorize gating
They're rendering primitives, not user-facing tabs/menus. filterByPermission passes them through unchanged.
See Also
- EntityConfiguration — Where layouts are registered
- Permissions —
AuthorizeDSL andEntityPermissionCache - Glossary — Term definitions