diff --git a/.changeset/action-undoable.md b/.changeset/action-undoable.md new file mode 100644 index 0000000000..4561fbf88c --- /dev/null +++ b/.changeset/action-undoable.md @@ -0,0 +1,12 @@ +--- +"@objectstack/spec": minor +--- + +feat(action): `undoable` flag on the UI Action schema + +Single-record update actions can declare `undoable: true`. The runtime captures +the record's prior field values and offers an "Undo" affordance on the success +toast (backed by the client UndoManager). Pairs with the objectui runtime that +honours it. Also documents that conditional `visible` / `disabled` CEL +predicates are evaluated by the action renderers (used here to hide an action +when it no longer applies, e.g. Convert Lead on an already-converted lead). diff --git a/examples/app-crm/src/actions/convert-lead.action.ts b/examples/app-crm/src/actions/convert-lead.action.ts index e31d1376be..91045a4a81 100644 --- a/examples/app-crm/src/actions/convert-lead.action.ts +++ b/examples/app-crm/src/actions/convert-lead.action.ts @@ -16,4 +16,9 @@ export const ConvertLeadAction: UI.Action = { target: 'crm_convert_lead_wizard', locations: ['list_item', 'record_header', 'record_more'], recordIdParam: 'recordId', + // Conditional visibility (CEL): hide the action once the lead is already + // converted — so the button disappears rather than the user clicking it and + // hitting the flow's "already converted" guard screen. The flow keeps that + // guard as a server-side backstop. + visible: 'record.status != "converted"', }; diff --git a/examples/app-crm/src/actions/index.ts b/examples/app-crm/src/actions/index.ts index 0957b456b8..9490e28e5f 100644 --- a/examples/app-crm/src/actions/index.ts +++ b/examples/app-crm/src/actions/index.ts @@ -1,3 +1,4 @@ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. export { ConvertLeadAction } from './convert-lead.action.js'; +export { ParkLeadAction } from './park-lead.action.js'; diff --git a/examples/app-crm/src/actions/park-lead.action.ts b/examples/app-crm/src/actions/park-lead.action.ts new file mode 100644 index 0000000000..3960c831fd --- /dev/null +++ b/examples/app-crm/src/actions/park-lead.action.ts @@ -0,0 +1,31 @@ +// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. + +import type { UI } from '@objectstack/spec'; + +/** + * Row-level action on crm_lead — reassign the lead's owner. + * + * Demonstrates the `undoable` action affordance: it's a single-record field + * update (`assigned_to`), so the runtime captures the prior owner and the + * success toast offers an "Undo" that restores it (backed by the client + * UndoManager — Ctrl+Z works too). Prompts for the new owner via one param + * (pre-filled with "Triage Queue"). + */ +export const ParkLeadAction: UI.Action = { + name: 'crm_park_lead', + label: 'Reassign Lead', + description: 'Reassign this lead to a new owner (undoable).', + icon: 'UserPlus', + objectName: 'crm_lead', + // `type: 'api'` with a non-URL target routes to the console runtime's generic + // `dataSource.update` path (the row id comes from the list_item row record). + type: 'api', + target: 'crm_lead', + // List-row only: the apiHandler needs the row record to capture prior values. + locations: ['list_item'], + params: [ + { field: 'assigned_to', label: 'Reassign to', defaultValue: 'Triage Queue', required: true }, + ], + undoable: true, + successMessage: 'Lead reassigned.', +}; diff --git a/packages/spec/src/ui/action.zod.ts b/packages/spec/src/ui/action.zod.ts index db0cf8345e..d26cd40301 100644 --- a/packages/spec/src/ui/action.zod.ts +++ b/packages/spec/src/ui/action.zod.ts @@ -316,6 +316,10 @@ export const ActionSchema = lazySchema(() => z.object({ // set a friendly failure toast instead of surfacing the raw error string. errorMessage: I18nLabelSchema.optional().describe('Error message to show when the action fails (overrides the raw error).'), refreshAfter: z.boolean().default(false).describe('Refresh view after execution'), + // Single-record update actions only. When true, the runtime captures the + // record's prior field values and offers an "Undo" affordance on the success + // toast (backed by the client UndoManager) to restore them. + undoable: z.boolean().optional().describe('Offer an Undo affordance after this single-record update action succeeds.'), /** * Result Dialog — describe how to render the API response on success.