Skip to content

Commit 84249a4

Browse files
os-zhuangclaude
andauthored
feat(crm,action): conditional action visibility + undoable reassign demo (#1992)
- spec: UI Action gains `undoable` (single-record update actions offer an Undo affordance on success). `visible`/`disabled` CEL predicates already existed. - app-crm: Convert Lead action gains `visible: record.status != "converted"` (the button hides on converted leads instead of relying on the flow's guard screen). New "Reassign Lead" list action (`undoable`) demonstrates the Undo affordance — reassigns `assigned_to`, restorable from the toast. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 67c29ee commit 84249a4

5 files changed

Lines changed: 53 additions & 0 deletions

File tree

.changeset/action-undoable.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
"@objectstack/spec": minor
3+
---
4+
5+
feat(action): `undoable` flag on the UI Action schema
6+
7+
Single-record update actions can declare `undoable: true`. The runtime captures
8+
the record's prior field values and offers an "Undo" affordance on the success
9+
toast (backed by the client UndoManager). Pairs with the objectui runtime that
10+
honours it. Also documents that conditional `visible` / `disabled` CEL
11+
predicates are evaluated by the action renderers (used here to hide an action
12+
when it no longer applies, e.g. Convert Lead on an already-converted lead).

examples/app-crm/src/actions/convert-lead.action.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,9 @@ export const ConvertLeadAction: UI.Action = {
1616
target: 'crm_convert_lead_wizard',
1717
locations: ['list_item', 'record_header', 'record_more'],
1818
recordIdParam: 'recordId',
19+
// Conditional visibility (CEL): hide the action once the lead is already
20+
// converted — so the button disappears rather than the user clicking it and
21+
// hitting the flow's "already converted" guard screen. The flow keeps that
22+
// guard as a server-side backstop.
23+
visible: 'record.status != "converted"',
1924
};
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
22

33
export { ConvertLeadAction } from './convert-lead.action.js';
4+
export { ParkLeadAction } from './park-lead.action.js';
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
import type { UI } from '@objectstack/spec';
4+
5+
/**
6+
* Row-level action on crm_lead — reassign the lead's owner.
7+
*
8+
* Demonstrates the `undoable` action affordance: it's a single-record field
9+
* update (`assigned_to`), so the runtime captures the prior owner and the
10+
* success toast offers an "Undo" that restores it (backed by the client
11+
* UndoManager — Ctrl+Z works too). Prompts for the new owner via one param
12+
* (pre-filled with "Triage Queue").
13+
*/
14+
export const ParkLeadAction: UI.Action = {
15+
name: 'crm_park_lead',
16+
label: 'Reassign Lead',
17+
description: 'Reassign this lead to a new owner (undoable).',
18+
icon: 'UserPlus',
19+
objectName: 'crm_lead',
20+
// `type: 'api'` with a non-URL target routes to the console runtime's generic
21+
// `dataSource.update` path (the row id comes from the list_item row record).
22+
type: 'api',
23+
target: 'crm_lead',
24+
// List-row only: the apiHandler needs the row record to capture prior values.
25+
locations: ['list_item'],
26+
params: [
27+
{ field: 'assigned_to', label: 'Reassign to', defaultValue: 'Triage Queue', required: true },
28+
],
29+
undoable: true,
30+
successMessage: 'Lead reassigned.',
31+
};

packages/spec/src/ui/action.zod.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,10 @@ export const ActionSchema = lazySchema(() => z.object({
316316
// set a friendly failure toast instead of surfacing the raw error string.
317317
errorMessage: I18nLabelSchema.optional().describe('Error message to show when the action fails (overrides the raw error).'),
318318
refreshAfter: z.boolean().default(false).describe('Refresh view after execution'),
319+
// Single-record update actions only. When true, the runtime captures the
320+
// record's prior field values and offers an "Undo" affordance on the success
321+
// toast (backed by the client UndoManager) to restore them.
322+
undoable: z.boolean().optional().describe('Offer an Undo affordance after this single-record update action succeeds.'),
319323

320324
/**
321325
* Result Dialog — describe how to render the API response on success.

0 commit comments

Comments
 (0)