Skip to content

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 ​

dart
const EntityLayouts({
  required this.list,
  this.details = const [],
  this.items = const [],
  this.comparisons = const [],
  this.summary,
  this.dashboard,
  this.analytics = const [],
});

Properties ​

PropertyTypeDefaultDescription
listList<CollectionLayout<T>>--List-view layouts (tables, grids, custom)
detailsList<EntityLayout<T>>[]Detail tabs (info, audit, versions, etc.)
itemsList<EntityItemLayout<T>>[]Single-entity rendering primitives — used by version cards, sidebars, command-palette previews. Looked up by identifier via getItem.
comparisonsList<EntityComparisonLayout<T>>[]Multi-entity rendering primitives — used by audit delta cards, future trend / matrix views. Looked up via getComparison.
summaryEntityLayout<T>?nullSummary / card layout for entity preview
dashboardAggregateLayout<T>?nullDashboard layout for entity overview
analyticsList<AggregateLayout<T>>[]Analytics layouts (charts, trends, heatmaps)

Methods ​

MethodSignatureDescription
EntityLayouts.functionfactory EntityLayouts.function(EntityLayouts<T> Function() factory)Build via factory (evaluated eagerly)
filterByPermissionEntityLayouts<T> filterByPermission(bool Function(Authorize?)? authorizeResolver)Remove layouts the user is not authorized to see
copyWithEntityLayouts<T> copyWith({list?, details?, items?, comparisons?, summary?, dashboard?, analytics?})Returns a copy
getItemEntityItemLayout<T>? getItem(String identifier)Find an item layout by identifier
getComparisonEntityComparisonLayout<T>? getComparison(String identifier)Find a comparison layout by identifier
getListCollectionLayout<T>? getList(String identifier)Find a collection layout by identifier
getCachedDetailTabIdentifiersList<String>? getCachedDetailTabIdentifiers(String entityIdentifier)Cached identifiers from EntityPermissionCache
getCachedDetailTabsList<EntityLayout<T>>? getCachedDetailTabs(String entityIdentifier)Cached, materialised detail tabs (clears stale entries automatically)
setCachedDetailTabsvoid setCachedDetailTabs(String entityIdentifier, List<EntityLayout<T>> tabs)Cache filtered detail tabs
hasDetailTabCachebool hasDetailTabCache(String entityIdentifier)Whether cache exists

Usage Example ​

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

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

PropertyTypeDefaultDescription
schemaTypeString--Schema type identifier (from SchemaItem)
identifierString--Unique identifier for this layout
titleString--Display title (uses titleResolver if set)
iconIconData--Icon representing this layout
descriptionString?nullDescription (uses descriptionResolver if set)
priorityint0Sort priority (higher = shown first)
categoryString?nullCategory grouping (uses categoryResolver if set)
enabledbooltrueWhether the layout is shown by default
titleResolverString Function()?nullLocalized title resolver
descriptionResolverString Function()?nullLocalized description resolver
categoryResolverString Function()?nullLocalized category resolver
authorizeAuthorize?nullAuthorization expression gating visibility
headerActionsBuilderWidget Function(BuildContext)?nullOptional 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 ​

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

dart
Widget build(BuildContext context, List<T> items);

Overridable getters ​

GetterTypeDefaultDescription
enableSearchbooltrueWhether this layout supports search
enableMultiSelectboolfalseWhether multi-select is supported
batchActionsList<ListAction<List<T>>>[]Batch actions for selected items
viewModeIdString?nullView mode toggle identifier ('list', 'grid', 'inbox', ...). Layouts without an id are excluded from the toggle.
showFilterPresetsbooltrueShow filter preset buttons in header
defaultSortFieldString?nullDefault sort field on first display
defaultSortAscendingbooltrueDefault sort direction

Example ​

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

dart
Widget build(BuildContext context, T entity);

Example ​

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

dart
Widget build(BuildContext context);

Unlike EntityLayout.build(), this does not receive an entity instance — aggregate layouts operate on the entity type.

Example ​

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

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

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

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