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:
| Subclass | Purpose |
|---|---|
StandardEntityEditor | Non-regulated entities. Plain Save + Cancel. |
SignatureDrivenEditor | Regulated 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
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)
| Getter | Type | Default | Purpose |
|---|---|---|---|
displayMode | EditorDisplayMode | EditorDisplayMode.panel | How the editor surface is rendered |
title | String? | null | Custom title (overrides "New X" / "Edit X") |
showTitle | bool | true | Show the title in the AppBar |
hideCancel | bool | false | Hide the Cancel button |
stayOpenAfterSave | bool | false | Stay on the editor route after a successful save |
allowDrafts | bool | false | Whether Save as Draft is offered |
Lifecycle (overridable)
| Method | Purpose |
|---|---|
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
| Getter | Type | Purpose |
|---|---|---|
createParts() | List<EntityEditorPart<T>> | Abstract. Called every time the editor opens to produce fresh part instances. |
transformer | EntityTransformer<T> | Abstract. Maps form data ↔ entity. |
actions | List<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
| Getter | Description |
|---|---|
isSinglePart | true when createParts() returns exactly one part |
isMultiPart | true 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):
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
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,
});| Parameter | Type | Default | Description |
|---|---|---|---|
transformer | EntityTransformer<T> | -- | Maps form data ↔ entity |
parts | List<EntityEditorPart<T>> Function() | -- | Factory called each time the editor opens to produce fresh parts |
displayMode | EditorDisplayMode | panel | How the editor is rendered |
stayOpenAfterSave | bool | false | Keep editor open after a successful save |
title | String? | null | Override the auto-generated title |
showTitle | bool | true | Show title in the AppBar |
hideCancel | bool | false | Hide the Cancel button |
allowDrafts | bool | false | Offer Save as Draft (inherited) |
Single-form example
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.
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
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:
- Cancel (
EditorActionType.cancel) - Save as Draft (
EditorActionType.draft) — only whenallowDrafts - Save (
EditorActionType.save) — handler routes through the signature verification dialog when the resolved requirements demand it; remarks captured in the dialog flow intoapi.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
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.
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();| Field | Effect |
|---|---|
requiresVerification | If true, the save action shows the e-signature dialog before persisting. |
requiresRemarks | If true, the dialog shows a remarks field, threaded into api.create() / api.update() as the remarks parameter. |
hasApprovals | If true, all saves go through the draft / approvals workflow — no inline verification at save time. |
signatureRole | Which signature role to enforce (doer, witness, reviewer). |
The resolver is async to allow it to consult the API or a config service.
EditorActionCancelledException
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.
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
enum EditorDisplayMode { panel, fullWidth, adaptive }| Value | Description |
|---|---|
panel | Side panel (traditional split view) |
fullWidth | Full-width (entire screen) |
adaptive | Adapts 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
const EntityEditorPart({
required this.title,
required this.icon,
required this.identifier,
this.description,
});Properties
| Property | Type | Description |
|---|---|---|
title | String | Title shown in tab or section header |
icon | IconData | Icon shown in tab or section header |
identifier | String | Unique identifier (used for routing) |
description | String? | Optional description |
Lifecycle methods
| Method | Signature | Description |
|---|---|---|
initialize | void initialize(EntityEditorPartContext<T> context) | Initialize with editing context |
loadData | void loadData(Map<String, dynamic> data) | Load entity data into part |
build | Widget build(BuildContext context, EntityEditorPartContext<T> partContext) | Build the editor UI |
validate | bool validate() | Synchronous validation |
validateAsync | Future<bool> validateAsync() | Asynchronous validation |
getValidationErrors | List<String> getValidationErrors() | Get validation error messages |
getData | Map<String, dynamic> getData() | Extract data for saving |
dispose | void dispose() | Clean up resources |
onSaveSuccess | void onSaveSuccess(EntityEditorPartContext<T> partContext, T savedEntity) | Callback after successful save |
getToolbarActions | List<Widget> getToolbarActions(BuildContext context) | Left-side toolbar actions |
getCustomActions | List<Widget> getCustomActions(BuildContext context) | Right-side toolbar actions |
Properties (overridable)
| Property | Type | Default | Description |
|---|---|---|---|
isPristine | bool | false | Whether 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
FormEditorPart({
required super.title,
required super.icon,
required super.identifier,
super.description,
required this.getForm,
this.watchFields,
});Properties
| Property | Type | Description |
|---|---|---|
getForm | Form Function({String? entityId, bool isDraft, Map<String, dynamic>? initialValues}) | Factory for the form instance |
watchFields | List<String>? | Field names to publish as EditorPartEvents on the shared event bus |
The form factory receives:
entityId:nullfor create mode, non-null for editisDraft:trueif editing a draft (allows editing immutable fields)initialValues: Entity data as JSON for pre-population (already passed throughEntityTransformer.fromif 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
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
| Property | Type | Required | Description |
|---|---|---|---|
onInitialize | void Function(EntityEditorPartContext<T>)? | No | Called once during init |
onLoadData | void Function(Map<String, dynamic>, EntityEditorPartContext<T>)? | No | Load entity data |
builder | Widget Function(BuildContext, EntityEditorPartContext<T>) | Yes | Build the UI |
getDataCallback | Map<String, dynamic> Function(EntityEditorPartContext<T>) | Yes | Extract data for saving |
validateCallback | bool Function(EntityEditorPartContext<T>)? | No | Validate before save |
getValidationErrorsCallback | List<String> Function(EntityEditorPartContext<T>)? | No | Get error messages |
onDispose | void Function(EntityEditorPartContext<T>)? | No | Cleanup callback |
onSaveSuccess | void Function(EntityEditorPartContext<T>, T)? | No | Post-save callback |
getToolbarActionsCallback | List<Widget> Function(BuildContext, EntityEditorPartContext<T>)? | No | Toolbar actions (left) |
getCustomActionsCallback | List<Widget> Function(BuildContext, EntityEditorPartContext<T>)? | No | Custom actions (right) |
EntityTransformer
abstract class EntityTransformer<T extends EntityBase> — transforms data between entity objects and form field values.
abstract class EntityTransformer<T extends EntityBase> {
Map<String, dynamic> from(T entity);
T to(Map<String, dynamic> formData, T? existingEntity);
}| Method | Description |
|---|---|
from | Entity to form data |
to | Form data to entity |
DefaultEntityTransformer
class DefaultEntityTransformer<T extends EntityBase> extends EntityTransformer<T> — pass-through transformer.
class DefaultEntityTransformer<T extends EntityBase>
extends EntityTransformer<T> {
final T Function(Map<String, dynamic>) fromJson;
DefaultEntityTransformer({required this.fromJson});
}Behavior:
from(entity)— returnsentity.toJson().to(formData, existing)— for updates, mergesexisting.toJson()withformDatathen callsfromJson(). For creates, callsfromJson({...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).
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,
});
}| Property | Type | Default | Description |
|---|---|---|---|
excludeFromEntity | Set<String> | {} | Form-only fields removed before fromJson (preserved across roundtrips) |
fieldTransforms | Map<String, dynamic Function(dynamic)>? | null | Per-field value transforms applied during to() |
Usage
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:
- Editor init —
EntityEditor.init()(e.g.SignatureDrivenEditorresolves lifecycle requirements and configures actions) - Create parts —
EntityEditor.createParts()produces fresh part instances - Initialize parts — each part receives
EntityEditorPartContext<T>viainitialize() - Load data — entity data passed to each part via
loadData()(FormEditorPartignores this; data is baked into the form during init) - Build — parts render their UI via
build() - Validate — on save,
validate()andvalidateAsync()are called - Extract data —
getData()collects data from each part - Transform —
EntityTransformer.to()converts merged data to entity - Save — entity is created/updated via the API;
EditorAction.executormay wrap the save in a verification dialog - Post-save —
onSaveSuccess()is called on each part - Dispose —
dispose()cleans up resources on each part, then on the editor itself
See Also
- EntityConfiguration — Where editors are registered
- Actions —
EditorAction,SaveCallback, the action evaluation pipeline - EntityBase — Entity class that editors create/modify
- Permissions — Authorization gating via
Authorize