Skip to Content

Actions

Reusable action DSL used by ModelForm and ModelTable.

Use this document for:

  • Action
  • BulkAction
  • action type selection
  • shared action props
  • placement rules in form and table containers

Related docs:

Import

import { Action } from "@/components/actions/Action"; import { BulkAction } from "@/components/actions/BulkAction";

Choose The Right Component

ComponentUse whenTypical scope
ActionSingle-record action, row action, form action, or linkModelForm, ModelTable
BulkActionSelection-based action over multiple table rowsModelTable only

Action

Use a single Action component with discriminated type.

Action supports both static values and context-driven values via:

type ActionValue<T> = | T | ((context: { id: string | null; modelName?: string; scope: "form" | "model-table"; mode: "create" | "edit" | "read"; isDirty: boolean; values?: Record<string, unknown>; row?: Record<string, unknown>; }) => T);

Shared Props

PropTypeRequiredDefaultNotes
type"default" | "dialog" | "link" | "custom" | "form"No"default"Action behavior. Omit to use direct API invoke.
labelNameReactNodeYes-Action label.
placement"toolbar" | "more" | "header" | "inline"Nocontainer-dependentPlacement support depends on parent container.
confirmMessageActionValue<string>No-Optional confirmation prompt before action execution.
successMessageActionValue<string>No-Success toast message for default and dialog actions.
errorMessageActionValue<string>No-Error toast message for default and dialog actions.
iconComponentType<{ className?: string }>No-Action icon.
disabledboolean | FilterCondition | dependsOn(...)NofalseDisabled state.
hiddenboolean | FilterCondition | dependsOn(...)NofalseHide the action when the condition resolves to true.

Behavior-Specific Props

ComponentRequired Behavior PropsDefaultNotes
type omitted or type="default"operation-Calls POST /{modelName}/{operation} with current record id in query params.
type="dialog"operation, component-component={MyDialogComponent}. Open/close, operation, success/error messaging are injected from Action.
type="link"hrefopens in new tabhref supports string or ({ id, modelName }) => string.
type="custom"onClick-Use for pure UI/local behaviors. Signature: onClick({ id, modelName, scope, mode, isDirty, values, row }) => void.
type="form"component, relatedField-Opens a dialog containing an independent ModelForm. component renders the child form view; relatedField names the child-model field that references the parent record. The parent id is automatically injected into ModelForm.defaultValues as { [relatedField]: parentId } and included in the create/update API payload.

Action condition notes:

  • disabled and hidden share the same runtime condition model as Field: boolean, FilterCondition, dependsOn([...], evaluator)
  • FilterCondition is evaluated against current scope values and automatically tracks {{ fieldName }} references
  • bare function conditions are not supported; wrap function logic with dependsOn([...], evaluator)
  • if there is no field dependency, prefer plain boolean

Action Type Examples

// 1) default (type omitted): direct API invoke <Action labelName="Lock Account" operation="lockAccount" placement="more" confirmMessage="Lock this user account?" successMessage="User account locked." errorMessage="Failed to lock user account." /> // 2) dialog: open custom dialog component, operation injected into dialog runtime <Action type="dialog" labelName="Unlock Account" operation="unlockAccount" placement="more" component={UserAccountUnlockActionDialog} successMessage="User account unlocked." errorMessage="Failed to unlock user account." /> // 3) link: open URL <Action type="link" labelName="Open Audit" placement="more" href={({ id, modelName }) => `/${modelName}/audit?id=${id}`} /> // 4) custom: local UI logic <Action type="custom" labelName="Run Health Check" placement="more" onClick={({ modelName }) => console.log(`${modelName} health check started.`)} /> // 5) form: open independent ModelForm in dialog <Action type="form" labelName="Add Config Group" placement="toolbar" component={ConfigGroupForm} relatedField="tenantConfigId" />

type="form" Component Definition

The component is a standard React component that renders a ModelForm with its own modelName. When opened via Action type="form", ModelForm automatically adapts to dialog mode:

  • ignores route params.id (uses only the id prop)
  • on create/update success: closes the dialog instead of navigating
  • on cancel: closes the dialog instead of navigating
  • relatedField value is injected into defaultValues and included in the API payload, even if the field is not displayed
import { FormSection } from "@/components/common/form-section"; import { Field } from "@/components/fields"; import { FormBody } from "@/components/views/form/components/FormBody"; import { FormToolbar } from "@/components/views/form/components/FormToolbar"; import { ModelForm } from "@/components/views/form/ModelForm"; function ConfigGroupForm() { return ( <ModelForm modelName="TenantConfigGroup"> <FormToolbar /> <FormBody enableAuditLog={false}> <FormSection labelName="General" hideHeader> <Field fieldName="groupName" /> <Field fieldName="description" /> </FormSection> </FormBody> </ModelForm> ); }

BulkAction

BulkAction is the selection-scoped variant for ModelTable.

Execution context:

{ ids: string[]; rows: Record<string, unknown>[]; modelName?: string; }

Supported behavior:

  • types: default | dialog
  • placements: toolbar | more
  • common visual props follow the same pattern as Action: labelName, confirmMessage, successMessage, errorMessage, icon, disabled

Behavior-specific props:

ComponentRequired Behavior PropsNotes
type omitted or type="default"operationExecutes the bulk operation with selected ids.
type="dialog"operation, componentOpens a dialog whose submit is bound to the bulk operation runtime.

Actions In ModelForm

Container support:

ContainerSupported Action TypesSupported Placements
FormToolbardefault, dialog, link, custom, formtoolbar, more
FormSectionlink, customheader, inline

Rules:

  • FormToolbar is the action area for page-level business actions
  • FormSection is a local UI action area and does not execute model API actions directly
  • for API actions (default / dialog), place actions in FormToolbar
  • edit mode with unsaved changes: clicking business actions asks whether to discard changes before continuing
  • create mode: built-in Duplicate / Delete remain visible but disabled

Complete example:

import { Action } from "@/components/actions/Action"; import { FormSection } from "@/components/common/form-section"; import { Field } from "@/components/fields"; import { ActionDialog } from "@/components/views/dialogs"; import { FormBody } from "@/components/views/form/components/FormBody"; import { FormToolbar } from "@/components/views/form/components/FormToolbar"; import { ModelForm } from "@/components/views/form/ModelForm"; import { ExternalLink, Lock, RefreshCw, ShieldCheck } from "lucide-react"; function UnlockDialog() { return ( <ActionDialog title="Unlock Account"> <Field fieldName="reason" labelName="Reason" widgetType="Text" /> </ActionDialog> ); } <ModelForm modelName="UserAccount"> <FormToolbar> <Action labelName="Lock" operation="lockAccount" placement="toolbar" icon={Lock} confirmMessage="Lock this account?" /> <Action type="dialog" labelName="Unlock" operation="unlockAccount" placement="more" icon={ShieldCheck} component={UnlockDialog} /> </FormToolbar> <FormBody> <FormSection labelName="Credentials"> <Action type="link" labelName="Open Docs" placement="header" icon={ExternalLink} href="https://docs.example.com/credentials" /> <Action type="custom" labelName="Regenerate Preview" placement="inline" icon={RefreshCw} onClick={() => console.log("regenerate")} /> <Field fieldName="username" /> <Field fieldName="status" /> </FormSection> </FormBody> </ModelForm>;

Actions In ModelTable

Rules:

  • <Action placement="toolbar" /> renders in the table toolbar custom action area
  • <Action placement="inline" /> renders in the last-column inline action area
  • <Action placement="more" /> renders in the last-column More Actions dropdown
  • active inline-edit rows resolve action context from the current draft row values
  • clicking a row action while the active row is dirty asks whether to discard the draft before continuing
  • BulkAction is selection-scoped and only shown when rows are selected
  • BulkAction placement="toolbar" appears between Columns and More
  • BulkAction placement="more" appears in the toolbar More dropdown bulk section

Complete example:

import { Action } from "@/components/actions/Action"; import { BulkAction } from "@/components/actions/BulkAction"; import { Field } from "@/components/fields"; import { ActionDialog } from "@/components/views/dialogs"; import { ModelTable } from "@/components/views/table/ModelTable"; import { ExternalLink, Lock, Pencil, ShieldCheck } from "lucide-react"; function UnlockDialog() { return ( <ActionDialog title="Unlock Account"> <Field fieldName="reason" labelName="Reason" widgetType="Text" /> </ActionDialog> ); } <ModelTable modelName="UserAccount"> <Field fieldName="username" /> <Field fieldName="email" /> <Field fieldName="status" /> <Action type="custom" labelName="Refresh" placement="toolbar" onClick={() => console.log("refresh")} /> <Action type="custom" labelName="Quick Edit" placement="inline" icon={Pencil} onClick={({ id }) => console.log("quick edit:", id)} /> <Action type="default" labelName="Lock Account" placement="more" icon={Lock} operation="lockAccount" confirmMessage="Lock this account?" /> <Action type="dialog" labelName="Unlock Account" placement="more" icon={ShieldCheck} operation="unlockAccount" component={UnlockDialog} /> <Action type="link" labelName="Open Audit" placement="more" icon={ExternalLink} href={({ id }) => `/user/user-account/${id}/audit`} /> <BulkAction labelName="Lock Selected" operation="lockByIds" placement="toolbar" /> <BulkAction type="dialog" labelName="Unlock Selected" operation="unlockByIds" placement="more" component={UnlockDialog} /> </ModelTable>;
Last updated on