Actions and validation
An action describes something a user can do. The same action can appear in a button, menu, command palette, shortcut, batch operation, or guide.
An action keeps the operation, its validation, and its progress in one place.
Define and register an action
The Todo demo's create action owns its name, discovery tags, icon, dialog, operation, and progress message:
export class CreateTodoAction extends Action {
static ID = 'CREATE_TODO';
@inject(TodoStore)
accessor todoStore: TodoStore;
constructor() {
super({
id: CreateTodoAction.ID,
name: 'Create todo item',
tags: ['todo', 'create'],
icon: 'plus'
});
}
protected async fireEvent(event: ActionEvent) {
event.getStatus().pushMessage('Creating a todo');
const name = await this.dialogStore.showInputDialog({ title: 'Create todo' });
if (!name) return false;
this.todoStore.addTodo(new TodoModel({ name }));
}
}
Register actions during module registration:
event.ioc.get(ActionStore).registerAction(new CreateTodoAction());
Registration gives the action a child logger and makes it discoverable by Reactor's action surfaces.
Name actions clearly
The name is what users see. Use aliases for other phrases they may search for and tags for shorter search terms:
super({
id: 'DELETE_TODO',
name: 'Delete todo item',
aliases: ['Remove todo item', 'Discard todo item'],
tags: ['todo', 'cleanup'],
behavior: ActionMacroBehavior.DELETE,
icon: 'trash'
});
Macro behavior contributes conventional tags. DELETE adds delete, remove, and destroy; COPY adds copy, clone, and duplicate.
Name the operation, not the control. Prefer “Create todo” to “Create button clicked.” The action may later run without a button.
Show an action in the UI
Actions generate descriptors and controls that standard Reactor widgets understand:
<PanelButtonWidget {...action.representAsButton({ targetEntity: todo })} />
Available representations include:
representAsButton()representAsIcon()representAsControl()representAsComboBoxItem()
The supplied event data is passed to validation and execution. Each representation keeps checking the action's validator, so widgets do not need their own availability rules.
Validation states
Validators return one result:
| State | Meaning |
|---|---|
ALLOWED | Render and execute normally. |
DEFERRED | More action parameters are required before a decision is possible. |
PENDING | A live asynchronous decision has not completed. |
DISABLED | Keep the action visible but unavailable, optionally with a reason. |
BLOCKED | Prevent execution but allow activation of a remediation flow. |
HIDDEN | Do not show the action here. |
class TodoLockedValidator extends ActionValidator<EntityActionEvent<TodoModel>> {
validate(event: Partial<EntityActionEvent<TodoModel>>): ValidationResult {
if (!event.targetEntity) {
return { type: ActionValidationState.DEFERRED };
}
if (event.targetEntity.locked) {
return {
type: ActionValidationState.DISABLED,
message: 'Unlock the todo before editing it.'
};
}
return { type: ActionValidationState.ALLOWED };
}
}
Use PENDING only when a validator has started asynchronous work and will notify its listeners when the answer changes. If the missing information is an unresolved action parameter, return DEFERRED.
BLOCKED lets the application offer a way to fix the problem:
return {
type: ActionValidationState.BLOCKED,
message: 'Connect before refreshing.',
indicator: { icon: 'plug', tooltip: 'Connection required' },
onActivate: () => connectionStore.openConnectionDialog()
};
Reactor understands the generic outcome; it does not own the application's permission or availability policy.
Entity and coupled actions
ParameterizedAction collects named values before execution. EntityAction targets an object described by the entity system. CoupledAction adds a source and target for assignments, moves, links, and drag-and-drop.
export class DuplicateTodoAction extends EntityAction<TodoModel> {
constructor() {
super({
id: 'DUPLICATE_TODO',
name: 'Duplicate todo',
target: TodoEntities.TODO_ITEM,
behavior: ActionMacroBehavior.COPY,
icon: 'copy'
});
}
protected async fireEvent(event: EntityActionEvent<TodoModel>) {
event.targetEntity.duplicate();
}
}
An action opened from an entity menu already has its target. From the command palette, its entity definition resolves one through a registered search behavior.
Candidate entities are placed into partial action events and validated before selection. The picker can therefore hide or disable candidates that would make the final action unavailable.
Execution lifecycle
The order is:
- collect or resolve parameters;
- create the complete action event;
- run validation;
- activate remediation for
BLOCKED, or stop for another unavailable state; - notify
willFirelisteners; - honor cancellation;
- execute
fireEvent(); - complete or fail the status directive;
- notify
didFirelisteners.
Errors are logged, the action status fails, and Reactor presents a standard error dialog before rethrowing.
Progress indication
event.getStatus() shows the action in the Visor. Use it for work that takes long enough to notice; fast actions do not need progress UI.
getExclusiveExecutionLock() lets a workflow temporarily cancel other actions while allowing explicit exceptions. The returned cleanup function must always be released.
Actions also support a temporary execution lock. Guided workflows use it to stop unrelated actions while a step is active. Always call the returned cleanup function when the step ends.
See Progress and status and Guided workflows.
Destructive and batch behavior
ActionMacroBehavior adds common search terms. ActionRollbackMechanic says whether destructive work can be recovered through source control. Helpers such as setupDeleteConfirmation() add standard confirmation behavior.
Entity actions can opt into batch execution with batch and batch_concurrency. See Batch actions.