Member Junction
    Preparing search index...

    Module @memberjunction/ng-entity-action-ux

    @memberjunction/ng-entity-action-ux

    A pluggable runtime-UX framework for entity actions, plus the flagship driver: a safe, two-step Record Process bulk-update runner (dry-run → review the exact per-record diff → confirm → apply).

    Most entity actions "fire and forget." Some need a real interaction first — a preview, a confirmation, a small form. This package lets an entity action declare a RuntimeUXDriverClass; when present, the host mounts that interactive component instead of invoking the action blind. New drivers register via the MJ ClassFactory and need zero host changes.

    ┌─ entity grid / list / form ─────────────────────────────────────────────┐
    │ user clicks an entity action │
    │ │ EntityActionRequested { action, selectedRecords, ... } │
    │ ▼ │
    │ parent: does action.runtimeUXDriverClass exist? │
    │ ├─ no → invoke the action normally (existing path) │
    │ └─ yes → <mj-entity-action-ux-host [DriverClass] [Context]> │
    │ │ ClassFactory.GetRegistration(Base…, DriverClass) │
    │ ▼ │
    │ RecordProcessRunnerUX (or any registered driver) │
    │ dry-run → diff → confirm → apply → Completed │
    └──────────────────────────────────────────────────────────────────────────┘
    Export Kind Purpose
    BaseEntityActionRuntimeUX @Directive() base The driver contract: @Input() Context, @Output() Completed/Cancelled, abstract Start().
    EntityActionUXHostComponent <mj-entity-action-ux-host> Resolves a driver key via ClassFactory and mounts it with a context.
    EntityActionUXContext / EntityActionUXResult types The data the host hands a driver / the outcome it returns.
    RecordProcessRunnerUXComponent driver (RecordProcessRunnerUX) The flagship: runs a MJ: Record Processes as a dry-run, shows the diff, applies on confirm.
    AIPromptSelectorComponent <mj-ai-prompt-selector> Category-grouped, searchable AI-prompt picker.
    FieldRulesBuilderComponent <mj-field-rules-builder> Visual authoring for a FieldRuleSet (static / field / formula / lookup / prompt + condition), with live metadata validation.
    field-rules-model / scope / prompt-grouping pure fns Angular-free, unit-tested logic the components delegate to.

    The runner turns a FieldRules Record Process into a safe bulk update:

    1. Dry-run — calls the server (RunRecordProcess, DryRun: true), which computes the exact per-record field changes without writing, and persists them to MJ: Process Run Details.
    2. Review — reads those details (RunView) and renders a per-record old → new diff, with a summary ("42 of 120 records will change · 67 field updates").
    3. Confirm — only on the user's click does it call RunRecordProcess again with DryRun: false. MJ's built-in Record Changes versioning captures every before/after automatically.

    Nothing is ever written until the user has seen precisely what will change.

    The MJ entity-data-grid already emits everything you need (LoadEntityActionsRequested, EntityActionRequested) and exposes ShowEntityActionButtons to toggle visibility. The parent (an Explorer view component) owns action resolution + mounting:

    import { EntityActionEngine } from '@memberjunction/actions';
    import { EntityActionUXHostComponent, type EntityActionUXContext } from '@memberjunction/ng-entity-action-ux';
    import type { EntityActionConfig } from '@memberjunction/ng-entity-viewer';

    // 1) Resolve the entity's actions → grid configs (carry the driver key + its RecordProcessID param)
    onLoadEntityActionsRequested(e: { entityInfo: EntityInfo }) {
    const actions = EntityActionEngine.Instance.GetActionsByEntityName(e.entityInfo.Name, 'Active');
    this.dataGrid.EntityActions = actions.map(a => ({
    id: a.ID, name: a.Action, icon: 'fa-solid fa-list-check',
    // the invocation's RuntimeUXDriverClass + the action's RecordProcessID param:
    runtimeUXDriverClass: a.invocations.find(i => i.RuntimeUXDriverClass)?.RuntimeUXDriverClass,
    metadata: { RecordProcessID: a.params.find(p => p.Name === 'RecordProcessID')?.Value },
    }));
    }

    // 2) On request — mount the driver if one is named, else invoke normally
    onEntityActionRequested(e: { entityInfo: EntityInfo; action: EntityActionConfig; selectedRecords: Record<string, unknown>[] }) {
    if (e.action.runtimeUXDriverClass) {
    this.activeDriver = {
    DriverClass: e.action.runtimeUXDriverClass,
    Context: <EntityActionUXContext>{
    EntityInfo: e.entityInfo,
    ScopeKind: 'records',
    SelectedRecordIDs: e.selectedRecords.map(r => r[e.entityInfo.FirstPrimaryKey.Name] as string),
    Config: { RecordProcessID: e.action.metadata?.['RecordProcessID'] },
    Provider: this.Provider, // multi-provider: thread your provider through
    },
    };
    } else {
    /* existing: invoke the action directly */
    }
    }
    <mj-entity-data-grid #dataGrid [Entity]="entityName" [ShowEntityActionButtons]="true"
    (LoadEntityActionsRequested)="onLoadEntityActionsRequested($event)"
    (EntityActionRequested)="onEntityActionRequested($event)">
    </mj-entity-data-grid>

    @if (activeDriver) {
    <mj-entity-action-ux-host
    [DriverClass]="activeDriver.DriverClass" [Context]="activeDriver.Context"
    (Completed)="activeDriver = null; dataGrid.RefreshData()"
    (Cancelled)="activeDriver = null">
    </mj-entity-action-ux-host>
    }

    Tree-shaking: call LoadEntityActionUX() once from your app bootstrap so the driver's @RegisterClass side effect survives bundling (same pattern as other MJ dynamically-resolved classes).

    • RunRecordProcess GraphQL mutation (@memberjunction/server) → typed GraphQLRecordProcessClient (@memberjunction/graphql-dataprovider). The runner uses the client; you never inline gql.
    • Run Record Process Action (@memberjunction/core-actions) — the agent / workflow / low-code surface for the same engine. Seeded by metadata/actions/.record-processing.json.
    • The engine is RecordProcessExecutor (@memberjunction/record-set-processor); FieldRules sit on the pure rules engine in @memberjunction/global.
    <mj-field-rules-builder [EntityName]="'Accounts'" [Provider]="Provider"
    (ValueChange)="ruleSet = $event" (ValidChange)="canSave = $event">
    </mj-field-rules-builder>

    Serialize ruleSet into the Record Process's Configuration column (JSON.stringify(ruleSet)). The builder validates live against the entity's metadata (EntityFieldRules.Validate) and shows per-rule errors.

    Wire the runner to an entity in three records. Seed a MJ: Record Processes (FieldRules), then an EntityAction for the same entity whose invocation names the driver and whose param supplies the process ID:

    // 1) the process — metadata/record-processes/.demo.json  (entity: "MJ: Record Processes")
    { "fields": {
    "Name": "Deactivate selected Accounts",
    "EntityID": "@lookup:MJ: Entities.Name=Accounts",
    "WorkType": "FieldRules",
    "ScopeType": "Filter",
    "Status": "Active",
    "Configuration": "{\"Rules\":[{\"TargetField\":\"Status\",\"Source\":{\"Kind\":\"static\",\"Value\":\"Inactive\"}}]}"
    } }

    // 2) the entity action — links the generic "Run Record Process" action to the Accounts entity,
    // names the driver on the invocation, and pins the RecordProcessID param.
    { "fields": {
    "EntityID": "@lookup:MJ: Entities.Name=Accounts",
    "ActionID": "@lookup:MJ: Actions.Name=Run Record Process",
    "Status": "Active"
    },
    "relatedEntities": {
    "MJ: Entity Action Invocations": [ { "fields": {
    "InvocationTypeID": "@lookup:MJ: Entity Action Invocation Types.Name=Multi-Record",
    "RuntimeUXDriverClass": "RecordProcessRunnerUX", "Status": "Active" } } ],
    "MJ: Entity Action Params": [ { "fields": {
    "Name": "RecordProcessID", "ValueType": "Static",
    "Value": "@lookup:MJ: Record Processes.Name=Deactivate selected Accounts" } } ]
    } }
    • Field-Rules authoring agent — an AI agent that turns a natural-language instruction ("set Status to Inactive for accounts with no activity in 12 months") into a validated FieldRuleSet, creates the Record Process, and offers to run it via the Run Record Process action. Designed; intentionally not seeded here because it needs a live system + prompt-tuning to validate.
    • entityDocument source in the visual builder (engine already supports it; today it round-trips as a static slot).
    • Saved-rule templates and a side-by-side "before/after" record preview in the runner.

    Standalone components · inject() DI · @if/@for/@switch · PascalCase public members · --mj-* design tokens · confirm-left/cancel-right buttons · multi-provider via Context.Provider / [Provider].

    Classes

    AIPromptSelectorComponent
    BaseEntityActionRuntimeUX
    EntityActionUXHostComponent
    FieldRulesBuilderComponent
    RecordProcessRunnerUXComponent

    Interfaces

    EntityActionUXContext
    EntityActionUXResult
    PromptGroup
    PromptOption
    RuleDraft

    Type Aliases

    BuilderSourceKind
    EntityActionUXScopeKind

    Functions

    blankRuleDraft
    BlankRuleDraft
    buildRecordProcessScope
    BuildRecordProcessScope
    displayValue
    DisplayValue
    draftsToRuleSet
    DraftsToRuleSet
    draftToSource
    DraftToSource
    filterPromptGroups
    FilterPromptGroups
    groupPromptsByCategory
    GroupPromptsByCategory
    LoadEntityActionUX
    ruleToDraft
    RuleToDraft