Skip to content

Batch Operations ​

The Vyuh Entity System supports multi-selection on entity lists and bulk operations on the resulting set. This page covers CollectionAction<T>, the selection model, the batch action bar, and Authorize-aware gating.

Multi-Selection in Collection Layouts ​

The stock EntityTableConfig<T> supports multi-select. Toggle it on when constructing the layout:

dart
final courseTableLayout = EntityTableConfig<Course>(
  identifier: 'table',
  title: 'Table',
  icon: FluentIcons.table_24_regular,
  columns: [
    EntityTableColumn<Course>(fieldDef: CourseFields.name),
    EntityTableColumn<Course>(fieldDef: CourseFields.level),
    EntityTableColumn<Course>(fieldDef: CourseFields.status),
  ],
  enableMultiSelect: true,
);

Selection state lives on the EntityListController<T> that backs the list. You read it through EntityProvider:

dart
final controller = EntityProvider.listControllerOf<Course>(context);
final selected = controller.selectedEntities;   // Set<Course>
final isMultiSelect = controller.multiSelectMode;

When the user picks one or more rows, the workspace replaces the single detail panel with the batch action bar.

CollectionAction ​

CollectionAction<T> represents an action that operates on the collection (or a selected subset). It is the same type used for header actions and for batch actions:

dart
class CollectionAction<T extends EntityBase> {
  final IconData icon;
  final String title;
  final Future<void> Function(BuildContext context) handler;
  final Future<bool> Function(BuildContext context)? isAvailable;
  final Authorize? authorize;          // Declarative gate
  final ButtonStyle? style;
  final Color? iconColor;
  final bool isDestructive;
  final bool isPrimary;
  final List<CollectionAction<T>> splitMenuItems;
  final Widget Function(BuildContext context)? widgetBuilder;
}

Key Properties ​

PropertyPurpose
icon / titleButton visual + accessibility label
handlerAsync callback executed on click
authorizeDeclarative gate via the Authorize DSL
isAvailableCustom async availability check (mutually exclusive with authorize)
isDestructiveRenders with destructive styling
isPrimaryRenders as a filled button instead of an icon button
splitMenuItemsTurns the button into a split-button with a dropdown
widgetBuilderEscape hatch — renders a verbatim widget instead of the standard button

WARNING

authorize and isAvailable are mutually exclusive. Pick one or the other.

Defining Collection Actions ​

dart
final exportAction = CollectionAction<Course>(
  icon: FluentIcons.arrow_download_24_regular,
  title: 'Export Courses',
  authorize: Authorize.permission('lms.courses.export'),
  handler: (context) async {
    final controller = EntityProvider.listControllerOf<Course>(context);
    final entities = controller.selectedEntities.isNotEmpty
        ? controller.selectedEntities
        : controller.entities;
    await CourseExporter.exportToCsv(entities);
  },
);

final bulkPublishAction = CollectionAction<Course>(
  icon: FluentIcons.checkmark_circle_24_regular,
  title: 'Publish All Drafts',
  isPrimary: true,
  authorize: Authorize.permission('lms.courses.publish'),
  handler: (context) async {
    final ok = await ConfirmationDialog.confirm(
      context: context,
      title: 'Publish all draft courses?',
      message: 'They will become visible to participants.',
      confirmLabel: 'Publish All',
    );
    if (ok != true) return;

    await courseApi.publishAllDrafts();
    vyuh.entity?.services.queryCache?.invalidateEntity('courses');
  },
);

final bulkDeleteAction = CollectionAction<Course>(
  icon: FluentIcons.delete_24_regular,
  title: 'Delete Selected',
  isDestructive: true,
  authorize: Authorize.permission('lms.courses.delete'),
  handler: (context) async {
    final controller = EntityProvider.listControllerOf<Course>(context);
    final selected = controller.selectedEntities.toList();

    final ok = await ConfirmationDialog.destructive(
      context: context,
      title: 'Delete ${selected.length} courses?',
      message: 'This cannot be undone.',
    );
    if (ok != true) return;

    for (final c in selected) {
      await courseApi.delete(c.id);
    }
    controller.clearSelection();
  },
);

Registering Collection Actions ​

Collection actions live on EntityActions<T>:

dart
final courseConfig = EntityConfiguration<Course>(
  // ...
  actions: EntityActions<Course>(
    inline: StandardEntityActions.inline<Course>(),
    header: StandardEntityActions.header<Course>(),
    collection: [
      exportAction,
      bulkPublishAction,
      bulkDeleteAction,
    ],
  ),
);

Where Each Slot Renders ​

SlotSurfacePurpose
inlinePer-row dropdown / detail headerPer-entity actions (Edit, Deactivate) — EntityAction<T>
headerList page headerCollection-wide primary buttons (Create, Import)
collectionBatch action bar (appears with selection)Operations on the selected set
menuDropdown groupsGrouped advanced actions — ActionGroup<T>

StandardEntityActions.inline<T>() ships the default Edit + Deactivate buttons. StandardEntityActions.header<T>() ships the Create button, and auto-derives its Authorize gate from the entity's routing.permissions.create so it stays hidden for actors who lack the declared create permission.

The Batch Action Bar ​

When the controller has at least one selected entity, the workspace replaces the detail pane with a batch action bar showing:

  1. The selection count ("3 selected")
  2. Authorized collection actions
  3. A clear-selection control

Authorization-Aware Actions ​

CollectionAction.authorize resolves through the entity system's AuthorizationProvider. Actions whose expression evaluates to false are hidden from the bar entirely.

dart
final approveAction = CollectionAction<Certification>(
  icon: FluentIcons.checkmark_seal_24_regular,
  title: 'Approve Selected',
  authorize: Authorize.all([
    Authorize.permission('lms.certifications.approve'),
    Authorize.role('quality_reviewer'),
  ]),
  handler: (context) async {
    // Only reaches here if both checks pass.
  },
);

Use isAvailable for non-authorization logic — a feature-flag check, an async lookup of "is the document closed?", etc.:

dart
final certifyAction = CollectionAction<Course>(
  icon: FluentIcons.ribbon_24_regular,
  title: 'Issue Certificates',
  isAvailable: (context) async {
    return await featureFlags.isEnabled('lms.auto_certificates');
  },
  handler: (context) async {
    await certificationApi.issueForCompletedCourses();
  },
);

LMS Example: Bulk Publish ​

dart
final bulkPublishAction = CollectionAction<Course>(
  icon: FluentIcons.checkmark_circle_24_regular,
  title: 'Publish Selected',
  isPrimary: true,
  authorize: Authorize.permission('lms.courses.publish'),
  handler: (context) async {
    final controller = EntityProvider.listControllerOf<Course>(context);
    final selected = controller.selectedEntities
        .where((c) => c.status == 'draft')
        .toList();

    if (selected.isEmpty) {
      vyuh.entity?.services.errorDisplay.showValidationError(
        context,
        'No draft courses in the selection.',
        title: 'Nothing to publish',
      );
      return;
    }

    final ok = await ConfirmationDialog.confirm(
      context: context,
      title: 'Publish ${selected.length} drafts?',
      message: 'They will become visible to all participants.',
      confirmLabel: 'Publish',
    );
    if (ok != true) return;

    for (final c in selected) {
      await courseApi.updateStatus(c.id, 'published');
    }

    vyuh.entity?.services.queryCache?.invalidateEntity('courses');
    controller.clearSelection();
  },
);

final courseConfig = EntityConfiguration<Course>(
  metadata: EntityMetadata(
    identifier: 'courses',
    name: 'Course',
    pluralName: 'Courses',
    icon: FluentIcons.book_24_regular,
  ),
  api: const CourseApi(),
  routing: EntityRouting<Course>(
    path: NavigationPathBuilder.collection(prefix: '/lms/courses'),
    builder: StandardRouteBuilder<Course>(),
  ),
  layouts: EntityLayouts<Course>(
    list: [
      EntityTableConfig<Course>(
        identifier: 'table',
        title: 'Table',
        icon: FluentIcons.table_24_regular,
        columns: [
          EntityTableColumn<Course>(fieldDef: CourseFields.name),
          EntityTableColumn<Course>(fieldDef: CourseFields.level),
          EntityTableColumn<Course>(fieldDef: CourseFields.status),
        ],
        enableMultiSelect: true,
      ),
    ],
  ),
  actions: EntityActions<Course>(
    inline: StandardEntityActions.inline<Course>(),
    header: StandardEntityActions.header<Course>(),
    collection: [bulkPublishAction],
  ),
);

Next Steps ​