Skip to content

Commit e35ccb0

Browse files
Copilothotlong
andcommitted
Add actions field to example objects via mergeActionsIntoObjects helper
- Add mergeActionsIntoObjects() to root objectstack.config.ts and console shared config - Create todo task actions (complete, start, clone, defer, set_reminder, assign) - Create kitchen-sink showcase actions (change_status, assign_owner, archive) - Add objectName to project.actions.ts for project_task prefix matching - Wire actions into todo and kitchen-sink defineStack configs fixes #840 Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
1 parent ef6800d commit e35ccb0

File tree

7 files changed

+233
-11
lines changed

7 files changed

+233
-11
lines changed

apps/console/objectstack.shared.ts

Lines changed: 60 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,51 @@ function mergeViewsIntoObjects(objects: any[], configs: any[]): any[] {
6767
});
6868
}
6969

70+
// ---------------------------------------------------------------------------
71+
// Merge stack-level actions into object definitions.
72+
// Actions declared at the stack level (actions[]) need to be merged into
73+
// individual object definitions so the runtime protocol (and Console/Studio)
74+
// can render action buttons directly from objectDef.actions.
75+
// Matching uses explicit objectName on the action, or longest object-name
76+
// prefix of the action name (e.g. "account_send_email" → "account").
77+
// ---------------------------------------------------------------------------
78+
function mergeActionsIntoObjects(objects: any[], configs: any[]): any[] {
79+
const allActions: any[] = [];
80+
for (const config of configs) {
81+
if (Array.isArray(config.actions)) {
82+
allActions.push(...config.actions);
83+
}
84+
}
85+
if (allActions.length === 0) return objects;
86+
87+
// Sort object names longest-first so "order_item" matches before "order"
88+
const objectNames = objects.map((o: any) => o.name as string)
89+
.sort((a, b) => b.length - a.length);
90+
91+
const actionsByObject: Record<string, any[]> = {};
92+
for (const action of allActions) {
93+
let target: string | undefined = action.objectName;
94+
if (!target) {
95+
for (const name of objectNames) {
96+
if (action.name.startsWith(name + '_')) {
97+
target = name;
98+
break;
99+
}
100+
}
101+
}
102+
if (target) {
103+
if (!actionsByObject[target]) actionsByObject[target] = [];
104+
actionsByObject[target].push(action);
105+
}
106+
}
107+
108+
return objects.map((obj: any) => {
109+
const actions = actionsByObject[obj.name];
110+
if (!actions) return obj;
111+
return { ...obj, actions: [...(obj.actions || []), ...actions] };
112+
});
113+
}
114+
70115
const allConfigs = [crmConfig, todoConfig, kitchenSinkConfig];
71116

72117
export const sharedConfig = {
@@ -81,12 +126,15 @@ export const sharedConfig = {
81126
// ============================================================================
82127
// Merged Stack Configuration (CRM + Todo + Kitchen Sink + Mock Metadata)
83128
// ============================================================================
84-
objects: mergeViewsIntoObjects(
85-
[
86-
...(crmConfig.objects || []),
87-
...(todoConfig.objects || []),
88-
...(kitchenSinkConfig.objects || []),
89-
],
129+
objects: mergeActionsIntoObjects(
130+
mergeViewsIntoObjects(
131+
[
132+
...(crmConfig.objects || []),
133+
...(todoConfig.objects || []),
134+
...(kitchenSinkConfig.objects || []),
135+
],
136+
allConfigs,
137+
),
90138
allConfigs,
91139
),
92140
apps: [
@@ -143,10 +191,13 @@ export const sharedConfig = {
143191
};
144192

145193
// defineStack() validates the config but strips non-standard properties like
146-
// listViews from objects. Re-merge listViews after validation so the runtime
147-
// protocol serves objects with their view definitions (calendar, kanban, etc.).
194+
// listViews and actions from objects. Re-merge after validation so the runtime
195+
// protocol serves objects with their view and action definitions.
148196
const validated = defineStack(sharedConfig as Parameters<typeof defineStack>[0]);
149197
export default {
150198
...validated,
151-
objects: mergeViewsIntoObjects(validated.objects || [], allConfigs),
199+
objects: mergeActionsIntoObjects(
200+
mergeViewsIntoObjects(validated.objects || [], allConfigs),
201+
allConfigs,
202+
),
152203
};

examples/crm/src/actions/project.actions.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ export const ProjectActions = [
44
label: 'Change Status',
55
icon: 'circle-check',
66
type: 'api' as const,
7+
objectName: 'project_task',
78
locations: ['record_header' as const, 'list_item' as const],
89
params: [
910
{
@@ -24,6 +25,7 @@ export const ProjectActions = [
2425
label: 'Assign User',
2526
icon: 'user-plus',
2627
type: 'api' as const,
28+
objectName: 'project_task',
2729
locations: ['record_header' as const, 'list_item' as const],
2830
params: [
2931
{ name: 'assignee_id', label: 'Assignee', type: 'lookup' as const, required: true },

examples/kitchen-sink/objectstack.config.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { App } from '@objectstack/spec/ui';
33
import { KitchenSinkObject } from './src/objects/kitchen_sink.object';
44
import { AccountObject } from './src/objects/account.object';
55
import { ShowcaseObject } from './src/objects/showcase.object';
6+
import { ShowcaseActions } from './src/objects/showcase.actions';
67

78
// Helper to create dates relative to today
89
const daysFromNow = (days: number) => {
@@ -54,6 +55,9 @@ export default defineStack({
5455
},
5556
},
5657
],
58+
actions: [
59+
...ShowcaseActions,
60+
],
5761
apps: [
5862
App.create({
5963
name: 'analytics_app',
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
export const ShowcaseActions = [
2+
{
3+
name: 'showcase_change_status',
4+
label: 'Change Status',
5+
icon: 'arrow-right-circle',
6+
type: 'api' as const,
7+
locations: ['record_header' as const, 'list_item' as const],
8+
params: [
9+
{
10+
name: 'new_status', label: 'New Status', type: 'select' as const, required: true,
11+
options: [
12+
{ label: 'Draft', value: 'draft' },
13+
{ label: 'Active', value: 'active' },
14+
{ label: 'Review', value: 'review' },
15+
{ label: 'Completed', value: 'completed' },
16+
{ label: 'Archived', value: 'archived' },
17+
],
18+
},
19+
],
20+
refreshAfter: true,
21+
successMessage: 'Status updated',
22+
},
23+
{
24+
name: 'showcase_assign_owner',
25+
label: 'Assign Owner',
26+
icon: 'user-plus',
27+
type: 'api' as const,
28+
locations: ['record_header' as const],
29+
params: [
30+
{ name: 'owner_email', label: 'Owner Email', type: 'email' as const, required: true },
31+
{ name: 'notify', label: 'Send Notification', type: 'boolean' as const },
32+
],
33+
refreshAfter: true,
34+
successMessage: 'Owner assigned',
35+
},
36+
{
37+
name: 'showcase_archive',
38+
label: 'Archive',
39+
icon: 'archive',
40+
type: 'api' as const,
41+
locations: ['record_more' as const],
42+
variant: 'danger' as const,
43+
confirmText: 'Are you sure you want to archive this item?',
44+
refreshAfter: true,
45+
successMessage: 'Item archived',
46+
},
47+
];

examples/todo/objectstack.config.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { defineStack } from '@objectstack/spec';
22
import { App } from '@objectstack/spec/ui';
33
import { TodoTask } from './src/domains/todo/task.object';
4+
import { TodoTaskActions } from './src/domains/todo/task.actions';
45

56
// Helper to create dates relative to today
67
const daysFromNow = (days: number) => {
@@ -55,6 +56,9 @@ export default defineStack({
5556
},
5657
},
5758
],
59+
actions: [
60+
...TodoTaskActions,
61+
],
5862
apps: [
5963
App.create({
6064
name: 'todo_app',
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
export const TodoTaskActions = [
2+
{
3+
name: 'todo_task_complete',
4+
label: 'Mark Complete',
5+
icon: 'check-circle-2',
6+
type: 'api' as const,
7+
locations: ['record_header' as const, 'list_item' as const],
8+
confirmText: 'Mark this task as complete?',
9+
refreshAfter: true,
10+
successMessage: 'Task marked as complete',
11+
},
12+
{
13+
name: 'todo_task_start',
14+
label: 'Start Task',
15+
icon: 'play',
16+
type: 'api' as const,
17+
locations: ['record_header' as const, 'list_item' as const],
18+
refreshAfter: true,
19+
successMessage: 'Task moved to In Progress',
20+
},
21+
{
22+
name: 'todo_task_clone',
23+
label: 'Clone Task',
24+
icon: 'copy',
25+
type: 'api' as const,
26+
locations: ['record_more' as const],
27+
refreshAfter: true,
28+
successMessage: 'Task cloned successfully',
29+
},
30+
{
31+
name: 'todo_task_defer',
32+
label: 'Defer Task',
33+
icon: 'clock',
34+
type: 'api' as const,
35+
locations: ['record_header' as const],
36+
params: [
37+
{ name: 'defer_until', label: 'Defer Until', type: 'date' as const, required: true },
38+
],
39+
refreshAfter: true,
40+
successMessage: 'Task deferred',
41+
},
42+
{
43+
name: 'todo_task_set_reminder',
44+
label: 'Set Reminder',
45+
icon: 'bell',
46+
type: 'api' as const,
47+
locations: ['record_more' as const],
48+
params: [
49+
{ name: 'reminder_date', label: 'Reminder Date', type: 'datetime' as const, required: true },
50+
{ name: 'reminder_note', label: 'Note', type: 'text' as const },
51+
],
52+
successMessage: 'Reminder set',
53+
},
54+
{
55+
name: 'todo_task_assign',
56+
label: 'Assign',
57+
icon: 'user-plus',
58+
type: 'api' as const,
59+
locations: ['record_header' as const, 'list_item' as const],
60+
params: [
61+
{ name: 'assignee', label: 'Assignee', type: 'text' as const, required: true },
62+
],
63+
refreshAfter: true,
64+
successMessage: 'Task assigned',
65+
},
66+
];

objectstack.config.ts

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,51 @@ function mergeViewsIntoObjects(objects: any[], configs: any[]): any[] {
6464
});
6565
}
6666

67+
// ---------------------------------------------------------------------------
68+
// Merge stack-level actions into object definitions.
69+
// Actions declared at the stack level (actions[]) need to be merged into
70+
// individual object definitions so the runtime protocol (and Console/Studio)
71+
// can render action buttons directly from objectDef.actions.
72+
// Matching uses explicit objectName on the action, or longest object-name
73+
// prefix of the action name (e.g. "account_send_email" → "account").
74+
// ---------------------------------------------------------------------------
75+
function mergeActionsIntoObjects(objects: any[], configs: any[]): any[] {
76+
const allActions: any[] = [];
77+
for (const config of configs) {
78+
if (Array.isArray(config.actions)) {
79+
allActions.push(...config.actions);
80+
}
81+
}
82+
if (allActions.length === 0) return objects;
83+
84+
// Sort object names longest-first so "order_item" matches before "order"
85+
const objectNames = objects.map((o: any) => o.name as string)
86+
.sort((a, b) => b.length - a.length);
87+
88+
const actionsByObject: Record<string, any[]> = {};
89+
for (const action of allActions) {
90+
let target: string | undefined = action.objectName;
91+
if (!target) {
92+
for (const name of objectNames) {
93+
if (action.name.startsWith(name + '_')) {
94+
target = name;
95+
break;
96+
}
97+
}
98+
}
99+
if (target) {
100+
if (!actionsByObject[target]) actionsByObject[target] = [];
101+
actionsByObject[target].push(action);
102+
}
103+
}
104+
105+
return objects.map((obj: any) => {
106+
const actions = actionsByObject[obj.name];
107+
if (!actions) return obj;
108+
return { ...obj, actions: [...(obj.actions || []), ...actions] };
109+
});
110+
}
111+
67112
// Merge all example configs into a single app bundle for AppPlugin
68113
const mergedApp = defineStack({
69114
manifest: {
@@ -104,10 +149,13 @@ const mergedApp = defineStack({
104149
],
105150
} as any);
106151

107-
// Re-merge listViews that defineStack stripped from objects
152+
// Re-merge listViews and actions that defineStack stripped from objects
108153
const mergedAppWithViews = {
109154
...mergedApp,
110-
objects: mergeViewsIntoObjects(mergedApp.objects || [], allConfigs),
155+
objects: mergeActionsIntoObjects(
156+
mergeViewsIntoObjects(mergedApp.objects || [], allConfigs),
157+
allConfigs,
158+
),
111159
};
112160

113161
// Export only plugins — no top-level objects/manifest/apps.

0 commit comments

Comments
 (0)