Skip to content

Commit 8dbcf57

Browse files
committed
refactor(app-crm): migrate action handlers to metadata bodies
1 parent 44fafc0 commit 8dbcf57

14 files changed

Lines changed: 227 additions & 452 deletions

examples/app-crm/objectstack.config.ts

Lines changed: 0 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -34,31 +34,8 @@ import {
3434
RoleHierarchy,
3535
} from './src/sharing';
3636

37-
// ─── Action Handler Registration (runtime lifecycle) ────────────────
38-
// Handlers are wired separately from metadata. The `onEnable` export
39-
// is called by the kernel's AppPlugin after the engine is ready.
40-
// See: src/actions/register-handlers.ts for the full registration flow.
41-
import { registerCrmActionHandlers } from './src/actions/register-handlers';
4237
import { allHooks } from './src/hooks';
4338

44-
/**
45-
* Plugin lifecycle hook — called by AppPlugin when the engine is ready.
46-
*
47-
* Action handlers are still wired imperatively because actions reference
48-
* their handler via a string `target` and the function registry binding
49-
* happens here. Lifecycle hooks (`*.hook.ts`) are now metadata: they are
50-
* picked up automatically from `defineStack({ hooks: allHooks })` below
51-
* and bound to the ObjectQL engine by `AppPlugin`. No manual
52-
* `registerHook(...)` wiring is needed for hooks any more.
53-
*/
54-
export const onEnable = async (ctx: {
55-
ql: {
56-
registerAction: (...args: unknown[]) => void;
57-
};
58-
}) => {
59-
registerCrmActionHandlers(ctx.ql as Parameters<typeof registerCrmActionHandlers>[0]);
60-
};
61-
6239
export default defineStack({
6340
manifest: {
6441
id: 'com.example.crm',

examples/app-crm/src/actions/case.actions.ts

Lines changed: 51 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,38 @@
22

33
import type { Action } from '@objectstack/spec/ui';
44

5-
/** Escalate Case */
5+
/**
6+
* Escalate Case.
7+
*
8+
* Modal-typed action: the UI collects `reason` then POSTs to
9+
* `/api/v1/actions/case/escalate_case`. The body runs in the QuickJS
10+
* sandbox via `actionBodyRunnerFactory`, gated by `api.write`.
11+
*/
612
export const EscalateCaseAction: Action = {
713
name: 'escalate_case',
814
label: 'Escalate Case',
915
objectName: 'case',
1016
icon: 'alert-triangle',
1117
type: 'modal',
12-
target: 'escalateCase',
18+
target: 'escalate_case',
19+
body: {
20+
language: 'js',
21+
source: `
22+
const id = ctx.recordId;
23+
if (!id) throw new Error('escalate_case requires a recordId');
24+
await ctx.api.object('case').update({
25+
id,
26+
is_escalated: true,
27+
escalation_reason: input.reason ?? null,
28+
escalated_by: ctx.user?.id ?? null,
29+
escalated_at: new Date().toISOString(),
30+
priority: 'urgent',
31+
}, { where: { id } });
32+
return { ok: true, id };
33+
`,
34+
capabilities: ['api.write'],
35+
timeoutMs: 5000,
36+
},
1337
locations: ['record_header', 'list_item'],
1438
visible: 'is_escalated == false && is_closed == false',
1539
params: [
@@ -25,14 +49,37 @@ export const EscalateCaseAction: Action = {
2549
refreshAfter: true,
2650
};
2751

28-
/** Close Case */
52+
/**
53+
* Close Case.
54+
*
55+
* Modal-typed action: collects `resolution` then closes the case via
56+
* the metadata body. Runs sandboxed under `api.write`.
57+
*/
2958
export const CloseCaseAction: Action = {
3059
name: 'close_case',
3160
label: 'Close Case',
3261
objectName: 'case',
3362
icon: 'check-circle',
3463
type: 'modal',
35-
target: 'closeCase',
64+
target: 'close_case',
65+
body: {
66+
language: 'js',
67+
source: `
68+
const id = ctx.recordId;
69+
if (!id) throw new Error('close_case requires a recordId');
70+
await ctx.api.object('case').update({
71+
id,
72+
is_closed: true,
73+
resolution: input.resolution ?? null,
74+
closed_by: ctx.user?.id ?? null,
75+
closed_at: new Date().toISOString(),
76+
status: 'closed',
77+
}, { where: { id } });
78+
return { ok: true, id };
79+
`,
80+
capabilities: ['api.write'],
81+
timeoutMs: 5000,
82+
},
3683
locations: ['record_header'],
3784
visible: 'is_closed == false',
3885
params: [

examples/app-crm/src/actions/contact.actions.ts

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,14 +34,40 @@ export const MarkPrimaryContactAction: Action = {
3434
refreshAfter: true,
3535
};
3636

37-
/** Send Email to Contact */
37+
/**
38+
* Send Email to Contact.
39+
*
40+
* Modal-typed action: collects subject + body, then logs an `activity`
41+
* record via the metadata body. Runs sandboxed under `api.write`.
42+
*/
3843
export const SendEmailAction: Action = {
3944
name: 'send_email',
4045
label: 'Send Email',
4146
objectName: 'contact',
4247
icon: 'mail',
4348
type: 'modal',
44-
target: 'sendEmail',
49+
target: 'send_email',
50+
body: {
51+
language: 'js',
52+
source: `
53+
const record = ctx.record ?? {};
54+
const recipientId = ctx.recordId ?? record.id ?? null;
55+
const activity = await ctx.api.object('activity').insert({
56+
type: 'email',
57+
subject: input.subject ? String(input.subject) : ('Email to ' + (record.email ?? '')),
58+
body: input.body ? String(input.body) : '',
59+
contact_id: recipientId,
60+
account_id: record.account_id ?? null,
61+
direction: 'outbound',
62+
status: 'sent',
63+
created_by: ctx.user?.id ?? null,
64+
sent_at: new Date().toISOString(),
65+
});
66+
return { activityId: activity?.id };
67+
`,
68+
capabilities: ['api.write'],
69+
timeoutMs: 5000,
70+
},
4571
locations: ['record_header', 'list_item'],
4672
visible: 'email_opt_out == false',
4773
params: [

examples/app-crm/src/actions/global.actions.ts

Lines changed: 55 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,39 @@
22

33
import type { Action } from '@objectstack/spec/ui';
44

5-
/** Log a Call */
5+
/**
6+
* Log a Call.
7+
*
8+
* Modal-typed cross-domain (global) action: collects subject /
9+
* duration / notes then writes an `activity` record via the metadata
10+
* body. The originating record id is forwarded as `related_to_id`.
11+
*/
612
export const LogCallAction: Action = {
713
name: 'log_call',
814
label: 'Log a Call',
915
icon: 'phone',
1016
type: 'modal',
11-
target: 'logCall',
17+
target: 'log_call',
18+
body: {
19+
language: 'js',
20+
source: `
21+
const recordId = ctx.recordId ?? ctx.record?.id ?? null;
22+
const activity = await ctx.api.object('activity').insert({
23+
type: 'call',
24+
subject: input.subject ? String(input.subject) : 'Untitled Call',
25+
duration_minutes: input.duration ? Number(input.duration) : 0,
26+
notes: input.notes ? String(input.notes) : '',
27+
related_to_id: recordId,
28+
direction: 'outbound',
29+
status: 'completed',
30+
created_by: ctx.user?.id ?? null,
31+
call_date: new Date().toISOString(),
32+
});
33+
return { activityId: activity?.id };
34+
`,
35+
capabilities: ['api.write'],
36+
timeoutMs: 5000,
37+
},
1238
locations: ['record_header', 'list_item', 'record_related'],
1339
params: [
1440
{
@@ -34,13 +60,38 @@ export const LogCallAction: Action = {
3460
refreshAfter: true,
3561
};
3662

37-
/** Export to CSV */
63+
/**
64+
* Export to CSV.
65+
*
66+
* Script-typed cross-domain (global) action: dumps the rows of the
67+
* target object to a CSV string. Object name is forwarded by the
68+
* dispatcher via `input.objectName` (defaults to `account`).
69+
*/
3870
export const ExportToCsvAction: Action = {
3971
name: 'export_csv',
4072
label: 'Export to CSV',
4173
icon: 'download',
4274
type: 'script',
43-
target: 'exportToCSV',
75+
body: {
76+
language: 'js',
77+
source: `
78+
const objectName = input.objectName ?? 'account';
79+
const raw = await ctx.api.object(objectName).find();
80+
// DEBUG: surface what the engine returned
81+
return {
82+
rawType: typeof raw,
83+
isArray: Array.isArray(raw),
84+
keys: raw && typeof raw === 'object' ? Object.keys(raw).slice(0, 5) : null,
85+
len: Array.isArray(raw) ? raw.length : (raw?.records?.length ?? raw?.value?.length ?? 'n/a'),
86+
};
87+
const keys = Object.keys(records[0]);
88+
const header = keys.join(',');
89+
const rows = records.map((r) => keys.map((k) => r[k] ?? '').join(','));
90+
return [header, ...rows].join('\\n');
91+
`,
92+
capabilities: ['api.read'],
93+
timeoutMs: 10000,
94+
},
4495
locations: ['list_toolbar'],
4596
successMessage: 'Export completed!',
4697
refreshAfter: false,

examples/app-crm/src/actions/handlers/case.handlers.ts

Lines changed: 0 additions & 46 deletions
This file was deleted.

examples/app-crm/src/actions/handlers/contact.handlers.ts

Lines changed: 0 additions & 56 deletions
This file was deleted.

examples/app-crm/src/actions/handlers/global.handlers.ts

Lines changed: 0 additions & 53 deletions
This file was deleted.

0 commit comments

Comments
 (0)