Skip to content

Editors

The editor system creates and edits entities through forms and custom interfaces. It supports single-part editors (one form) and multi-part editors (tabbed interfaces with mixed form and custom parts).

EntityEditor (abstract)

abstract class EntityEditor<T extends EntityBase> is the contract the editor controller talks to. The runtime calls its getters polymorphically — there are no type checks anywhere in the controller. Subclasses override getters to change behavior.

The framework ships two concrete subclasses:

SubclassPurpose
StandardEntityEditorNon-regulated entities. Plain Save + Cancel.
SignatureDrivenEditorRegulated entities (pharma). Resolves verification & remarks via a LifecycleResolver callback in init(), then injects a verified-save action.

You can also subclass EntityEditor<T> directly when neither suffices, but in practice the two built-ins cover almost every case.

Constructor

dart
const EntityEditor({this.allowDrafts = false});

EntityEditor itself only carries the draft flag. All other behavior is expressed through getter overrides on subclasses.

Display getters (overridable)

GetterTypeDefaultPurpose
displayModeEditorDisplayModeEditorDisplayMode.panelHow the editor surface is rendered
titleString?nullCustom title (overrides "New X" / "Edit X")
showTitlebooltrueShow the title in the AppBar
hideCancelboolfalseHide the Cancel button
stayOpenAfterSaveboolfalseStay on the editor route after a successful save
allowDraftsboolfalseWhether Save as Draft is offered

Lifecycle (overridable)

MethodPurpose
init()Async initialization. Override to resolve lifecycle requirements, configure actions, or perform startup work. Called by the controller before any UI renders.
dispose()Cleanup. Called by the controller during teardown.

Action / part getters

GetterTypePurpose
createParts()List<EntityEditorPart<T>>Abstract. Called every time the editor opens to produce fresh part instances.
transformerEntityTransformer<T>Abstract. Maps form data ↔ entity.
actionsList<EntityAction<T>>The button bar. Default = Cancel (TextButton) + Save (FilledButton, no verification). SignatureDrivenEditor overrides to inject a verified Save (and optional Save as Draft) built from the resolved LifecycleRequirements.

Computed

GetterDescription
isSingleParttrue when createParts() returns exactly one part
isMultiParttrue when createParts() returns more than one part

Factory: EntityEditor.function

Used when you need to defer construction of the editor (e.g. when it depends on services that aren't ready at config-construction time):

dart
factory EntityEditor.function(EntityEditor<T> Function() factory);

The factory is called eagerly on construction so display getters are available before init() runs (e.g., during route building). Async setup still happens in init(). Use this from @Entity(editor: _createAreaEditor) to keep complex wiring out of const-expressible annotations.


StandardEntityEditor

final class StandardEntityEditor<T extends EntityBase> extends EntityEditor<T> — concrete non-regulated editor. Use this for entities that don't need e-signature verification.

Constructor

dart
StandardEntityEditor({
  required EntityTransformer<T> transformer,
  required List<EntityEditorPart<T>> Function() parts,
  EditorDisplayMode displayMode = EditorDisplayMode.panel,
  bool stayOpenAfterSave = false,
  String? title,
  bool showTitle = true,
  bool hideCancel = false,
  super.allowDrafts,
});
ParameterTypeDefaultDescription
transformerEntityTransformer<T>--Maps form data ↔ entity
partsList<EntityEditorPart<T>> Function()--Factory called each time the editor opens to produce fresh parts
displayModeEditorDisplayModepanelHow the editor is rendered
stayOpenAfterSaveboolfalseKeep editor open after a successful save
titleString?nullOverride the auto-generated title
showTitlebooltrueShow title in the AppBar
hideCancelboolfalseHide the Cancel button
allowDraftsboolfalseOffer Save as Draft (inherited)

Single-form example

dart
editor: StandardEntityEditor<Equipment>(
  transformer: DefaultEntityTransformer<Equipment>(
    fromJson: Equipment.fromJson,
  ),
  parts: () => [
    FormEditorPart<Equipment>(
      title: 'Details',
      icon: FluentIcons.edit_24_regular,
      identifier: 'details',
      getForm: _getEquipmentForm,
    ),
  ],
),

Multi-part example

A multi-part editor is just a StandardEntityEditor whose parts factory returns more than one part. Each part renders as a tab.

dart
editor: StandardEntityEditor<Checklist>(
  transformer: DefaultEntityTransformer<Checklist>(
    fromJson: Checklist.fromJson,
  ),
  parts: () => [
    FormEditorPart<Checklist>(
      title: 'Settings',
      icon: FluentIcons.settings_24_regular,
      identifier: 'settings',
      getForm: _getChecklistForm,
    ),
    CustomEditorPart<Checklist>(
      title: 'Designer',
      icon: FluentIcons.flowchart_24_regular,
      identifier: 'designer',
      onInitialize: (context) {
        final store = ChecklistStore();
        context.setCustomData('store', store);
      },
      builder: (ctx, pc) => WorkflowDesigner(
        store: pc.getCustomData<ChecklistStore>('store')!,
      ),
      getDataCallback: (pc) => {
        'template': pc.getCustomData<ChecklistStore>('store')!.toJson(),
      },
    ),
  ],
  displayMode: EditorDisplayMode.fullWidth,
),

SignatureDrivenEditor

final class SignatureDrivenEditor<T extends EntityBase> extends EntityEditor<T> — concrete regulated editor. Resolves its verification requirements during init() by calling a LifecycleResolver callback, then injects a verified-save action whose handler runs the signature dialog and threads remarks through to the API.

Constructor

dart
SignatureDrivenEditor({
  required EntityTransformer<T> transformer,
  required List<EntityEditorPart<T>> Function() parts,
  required LifecycleResolver lifecycleResolver,
  EditorDisplayMode displayMode = EditorDisplayMode.panel,
  bool stayOpenAfterSave = false,
  String? title,
  bool showTitle = true,
  bool hideCancel = false,
  super.allowDrafts,
});

The constructor is identical to StandardEntityEditor plus the required lifecycleResolver callback. There are no verifyCreate / verifyUpdate / signatureRole flags — those are returned by the resolver and resolved per-entity, per-runtime.

Action shape

After init() resolves the requirements, actions returns:

  1. Cancel (EditorActionType.cancel)
  2. Save as Draft (EditorActionType.draft) — only when allowDrafts
  3. Save (EditorActionType.save) — handler routes through the signature verification dialog when the resolved requirements demand it; remarks captured in the dialog flow into api.create / api.update.

When LifecycleRequirements.hasApprovals is true, no inline verification runs — every save goes through the draft / approvals workflow and verification happens at submission time.

Example

dart
editor: SignatureDrivenEditor<Equipment>(
  transformer: DefaultEntityTransformer<Equipment>(
    fromJson: Equipment.fromJson,
  ),
  parts: () => [
    FormEditorPart<Equipment>(
      title: 'Details',
      icon: FluentIcons.edit_24_regular,
      identifier: 'details',
      getForm: _getEquipmentForm,
    ),
  ],
  lifecycleResolver: lmsLifecycleResolver<Equipment>(),
  allowDrafts: true,
),

Why a callback?

The compliance configuration that determines whether a particular operation needs a signature lives in the app layer (e.g., flutter_lms's lifecycle config tables), not in the entity declaration. Resolving it through a callback at runtime keeps the entity layer ignorant of the regulated-vs-unregulated distinction and lets the same entity work in apps with different compliance regimes.

When the resolver throws, the editor defaults to LifecycleRequirements.fallback — verification required, no remarks, no approvals (the safe choice).


LifecycleRequirements / LifecycleResolver

The value returned by LifecycleResolver, describing what the regulated editor must do at save time.

dart
class LifecycleRequirements {
  final bool requiresVerification;
  final bool requiresRemarks;
  final bool hasApprovals;
  final SignatureRole signatureRole;

  const LifecycleRequirements({
    required this.requiresVerification,
    required this.requiresRemarks,
    this.hasApprovals = false,
    this.signatureRole = SignatureRole.doer,
  });

  static const fallback = LifecycleRequirements(
    requiresVerification: true,
    requiresRemarks: false,
  );

  static const none = LifecycleRequirements(
    requiresVerification: false,
    requiresRemarks: false,
  );
}

typedef LifecycleResolver = Future<LifecycleRequirements> Function();
FieldEffect
requiresVerificationIf true, the save action shows the e-signature dialog before persisting.
requiresRemarksIf true, the dialog shows a remarks field, threaded into api.create() / api.update() as the remarks parameter.
hasApprovalsIf true, all saves go through the draft / approvals workflow — no inline verification at save time.
signatureRoleWhich signature role to enforce (doer, witness, reviewer).

The resolver is async to allow it to consult the API or a config service.

EditorActionCancelledException

dart
class EditorActionCancelledException implements Exception {
  const EditorActionCancelledException();
}

Thrown by editor action executors when the user cancels (e.g. dismisses the verification dialog). The controller catches this silently — it is not an error, just a signal that the save should be aborted.


EditorAction & EditorActionType

A semantic subclass of EntityAction for editor save-flow actions. See Actions for the full reference.

dart
enum EditorActionType { save, draft, cancel }

class EditorAction<T extends EntityBase> extends EntityAction<T> {
  final EditorActionType actionType;
  final Future<T?> Function(BuildContext, T, {required SaveCallback<T> save})? executor;
  // …
}

typedef SaveCallback<T> = Future<T> Function(T entity, {String? remarks});

The executor receives a SaveCallback from the controller. The controller owns the implementation (draft-aware, create vs update, API routing); the executor decides when to call save (e.g. inside a verification dialog handler so remarks flow through).


EditorDisplayMode

dart
enum EditorDisplayMode { panel, fullWidth, adaptive }
ValueDescription
panelSide panel (traditional split view)
fullWidthFull-width (entire screen)
adaptiveAdapts based on screen size and complexity

EntityEditorPart

abstract class EntityEditorPart<T extends EntityBase> — base class for editor parts that compose an entity editor. Parts are rendered as tabs in multi-part editors or directly for single-part editors.

Constructor

dart
const EntityEditorPart({
  required this.title,
  required this.icon,
  required this.identifier,
  this.description,
});

Properties

PropertyTypeDescription
titleStringTitle shown in tab or section header
iconIconDataIcon shown in tab or section header
identifierStringUnique identifier (used for routing)
descriptionString?Optional description

Lifecycle methods

MethodSignatureDescription
initializevoid initialize(EntityEditorPartContext<T> context)Initialize with editing context
loadDatavoid loadData(Map<String, dynamic> data)Load entity data into part
buildWidget build(BuildContext context, EntityEditorPartContext<T> partContext)Build the editor UI
validatebool validate()Synchronous validation
validateAsyncFuture<bool> validateAsync()Asynchronous validation
getValidationErrorsList<String> getValidationErrors()Get validation error messages
getDataMap<String, dynamic> getData()Extract data for saving
disposevoid dispose()Clean up resources
onSaveSuccessvoid onSaveSuccess(EntityEditorPartContext<T> partContext, T savedEntity)Callback after successful save
getToolbarActionsList<Widget> getToolbarActions(BuildContext context)Left-side toolbar actions
getCustomActionsList<Widget> getCustomActions(BuildContext context)Right-side toolbar actions

Properties (overridable)

PropertyTypeDefaultDescription
isPristineboolfalseWhether form has no unsaved changes

FormEditorPart

class FormEditorPart<T extends EntityBase> extends EntityEditorPart<T> — built-in editor part that uses a Vyuh Form (vyuh_feature_forms) for entity editing.

Constructor

dart
FormEditorPart({
  required super.title,
  required super.icon,
  required super.identifier,
  super.description,
  required this.getForm,
  this.watchFields,
});

Properties

PropertyTypeDescription
getFormForm Function({String? entityId, bool isDraft, Map<String, dynamic>? initialValues})Factory for the form instance
watchFieldsList<String>?Field names to publish as EditorPartEvents on the shared event bus

The form factory receives:

  • entityId: null for create mode, non-null for edit
  • isDraft: true if editing a draft (allows editing immutable fields)
  • initialValues: Entity data as JSON for pre-population (already passed through EntityTransformer.from if a transformer is configured)

watchFields lets other editor parts react to value changes via EntityEditorPartContext.events — useful when (e.g.) a designer tab needs to refresh when a resource_type changes in the form tab.


CustomEditorPart

class CustomEditorPart<T extends EntityBase> extends EntityEditorPart<T> — callback-based editor part for custom editing interfaces.

Constructor

dart
CustomEditorPart({
  required super.title,
  required super.icon,
  required super.identifier,
  super.description,
  this.onInitialize,
  this.onLoadData,
  required this.builder,
  required this.getDataCallback,
  this.validateCallback,
  this.getValidationErrorsCallback,
  this.onDispose,
  void Function(EntityEditorPartContext<T> partContext, T savedEntity)? onSaveSuccess,
  this.getToolbarActionsCallback,
  this.getCustomActionsCallback,
});

Callback properties

PropertyTypeRequiredDescription
onInitializevoid Function(EntityEditorPartContext<T>)?NoCalled once during init
onLoadDatavoid Function(Map<String, dynamic>, EntityEditorPartContext<T>)?NoLoad entity data
builderWidget Function(BuildContext, EntityEditorPartContext<T>)YesBuild the UI
getDataCallbackMap<String, dynamic> Function(EntityEditorPartContext<T>)YesExtract data for saving
validateCallbackbool Function(EntityEditorPartContext<T>)?NoValidate before save
getValidationErrorsCallbackList<String> Function(EntityEditorPartContext<T>)?NoGet error messages
onDisposevoid Function(EntityEditorPartContext<T>)?NoCleanup callback
onSaveSuccessvoid Function(EntityEditorPartContext<T>, T)?NoPost-save callback
getToolbarActionsCallbackList<Widget> Function(BuildContext, EntityEditorPartContext<T>)?NoToolbar actions (left)
getCustomActionsCallbackList<Widget> Function(BuildContext, EntityEditorPartContext<T>)?NoCustom actions (right)

EntityTransformer

abstract class EntityTransformer<T extends EntityBase> — transforms data between entity objects and form field values.

dart
abstract class EntityTransformer<T extends EntityBase> {
  Map<String, dynamic> from(T entity);
  T to(Map<String, dynamic> formData, T? existingEntity);
}
MethodDescription
fromEntity to form data
toForm data to entity

DefaultEntityTransformer

class DefaultEntityTransformer<T extends EntityBase> extends EntityTransformer<T> — pass-through transformer.

dart
class DefaultEntityTransformer<T extends EntityBase>
    extends EntityTransformer<T> {
  final T Function(Map<String, dynamic>) fromJson;

  DefaultEntityTransformer({required this.fromJson});
}

Behavior:

  • from(entity) — returns entity.toJson().
  • to(formData, existing) — for updates, merges existing.toJson() with formData then calls fromJson(). For creates, calls fromJson({...formData, 'id': ''}).

SnakeCaseEntityTransformer

class SnakeCaseEntityTransformer<T extends EntityBase> extends DefaultEntityTransformer<T> — auto-converts between snake_case entity JSON and camelCase form fields. Useful when entities use @JsonSerializable(fieldRename: FieldRename.snake).

dart
class SnakeCaseEntityTransformer<T extends EntityBase>
    extends DefaultEntityTransformer<T> {
  final Set<String> excludeFromEntity;
  final Map<String, dynamic Function(dynamic)>? fieldTransforms;

  SnakeCaseEntityTransformer({
    required super.fromJson,
    this.excludeFromEntity = const {},
    this.fieldTransforms,
  });
}
PropertyTypeDefaultDescription
excludeFromEntitySet<String>{}Form-only fields removed before fromJson (preserved across roundtrips)
fieldTransformsMap<String, dynamic Function(dynamic)>?nullPer-field value transforms applied during to()

Usage

dart
editor: StandardEntityEditor<User>(
  transformer: SnakeCaseEntityTransformer<User>(
    fromJson: User.fromJson,
    excludeFromEntity: {'password'},
    fieldTransforms: {
      'email': (value) => (value as String?)?.toLowerCase(),
    },
  ),
  parts: () => [
    FormEditorPart<User>(
      title: 'Profile',
      icon: FluentIcons.person_24_regular,
      identifier: 'profile',
      getForm: _getUserForm,
    ),
  ],
)

Editor lifecycle

The editor follows a specific lifecycle when opening and saving:

  1. Editor initEntityEditor.init() (e.g. SignatureDrivenEditor resolves lifecycle requirements and configures actions)
  2. Create partsEntityEditor.createParts() produces fresh part instances
  3. Initialize parts — each part receives EntityEditorPartContext<T> via initialize()
  4. Load data — entity data passed to each part via loadData() (FormEditorPart ignores this; data is baked into the form during init)
  5. Build — parts render their UI via build()
  6. Validate — on save, validate() and validateAsync() are called
  7. Extract datagetData() collects data from each part
  8. TransformEntityTransformer.to() converts merged data to entity
  9. Save — entity is created/updated via the API; EditorAction.executor may wrap the save in a verification dialog
  10. Post-saveonSaveSuccess() is called on each part
  11. Disposedispose() cleans up resources on each part, then on the editor itself

See Also