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