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 init — EntityEditor.init() (e.g. SignatureDrivenEditor resolves lifecycle requirements and configures actions)
  2. Create parts — EntityEditor.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 data — getData() collects data from each part
  8. Transform — EntityTransformer.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-save — onSaveSuccess() is called on each part
  11. Dispose — dispose() cleans up resources on each part, then on the editor itself

See Also ​