Skip to content

Commit 5fd1830

Browse files
authored
Merge pull request #941 from objectstack-ai/copilot/add-actions-field-to-objects
2 parents d31049e + 7353ae6 commit 5fd1830

File tree

12 files changed

+297
-14
lines changed

12 files changed

+297
-14
lines changed

ROADMAP.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1164,6 +1164,7 @@ The `FlowDesigner` is a canvas-based flow editor that bridges the gap between th
11641164
- [x] **P1: Chart Widget Server-Side Aggregation** — Fixed chart widgets (bar/line/area/pie/donut/scatter) downloading all raw data and aggregating client-side. Added optional `aggregate()` method to `DataSource` interface (`AggregateParams`, `AggregateResult` types) enabling server-side grouping/aggregation via analytics API (e.g. `GET /api/v1/analytics/{resource}?category=…&metric=…&agg=…`). `ObjectChart` now prefers `dataSource.aggregate()` when available, falling back to `dataSource.find()` + client-side aggregation for backward compatibility. Implemented `aggregate()` in `ValueDataSource` (in-memory), `ApiDataSource` (HTTP), and `ObjectStackAdapter` (analytics API with client-side fallback). Only detail widgets (grid/table/list) continue to fetch full data. 9 new tests.
11651165
- [x] **P1: Spec-Aligned CRM I18n** — Fixed CRM internationalization not taking effect on the console. Root cause: CRM metadata used plain string labels instead of spec-aligned `I18nLabel` objects. Fix: (1) Updated CRM app/dashboard/navigation metadata to use `I18nLabel` objects (`{ key, defaultValue }`) per spec. (2) Updated `NavigationItem` and `NavigationArea` types to support I18nLabel. (3) Added `resolveLabel()` helper in NavigationRenderer. (4) Updated `resolveI18nLabel()` to accept `t()` function for translation. (5) Added `loadLanguage` callback in I18nProvider for API-based translation loading. (6) Added `/api/v1/i18n/:lang` endpoint to mock server. Console contains zero CRM-specific code.
11661166
- [x] **P0: Opportunity List View & ObjectDef Column Enrichment** — Fixed ObjectGrid not using objectDef field metadata for type-aware rendering when columns are `string[]` or `ListColumn[]` without full options. (1) Schema resolution always fetches full schema from DataSource for field type metadata. (2) String[] column path enriched with objectDef types, options (with colors), currency, precision for proper CurrencyCellRenderer, SelectCellRenderer (colored badges), PercentCellRenderer, DateCellRenderer. (3) ListColumn[] fieldMeta deep-merged with objectDef field properties (select options with colors, currency code, precision). (4) Opportunity view columns upgraded from bare `string[]` to `ListColumn[]` with explicit types, alignment, and summary aggregation. 9 new tests.
1167+
- [x] **P1: Actions Merge into Object Definitions** — Fixed action buttons never showing in Console/Studio because example object definitions lacked `actions` field. Added `mergeActionsIntoObjects()` helper (mirrors existing `mergeViewsIntoObjects` pattern) to root config and console shared config. Uses longest-prefix name matching with explicit `objectName` fallback. Created todo task actions (6: complete, start, clone, defer, set_reminder, assign) and kitchen-sink showcase actions (3: change_status, assign_owner, archive). All CRM/Todo/Kitchen Sink objects now serve `actions` in metadata. Fixes #840.
11671168

11681169
### Ecosystem & Marketplace
11691170
- Plugin marketplace website with search, ratings, and install count

apps/console/objectstack.shared.ts

Lines changed: 61 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,52 @@ 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+
.filter(Boolean)
90+
.sort((a, b) => b.length - a.length);
91+
92+
const actionsByObject: Record<string, any[]> = {};
93+
for (const action of allActions) {
94+
let target: string | undefined = action.objectName;
95+
if (!target) {
96+
for (const name of objectNames) {
97+
if (action.name.startsWith(name + '_')) {
98+
target = name;
99+
break;
100+
}
101+
}
102+
}
103+
if (target) {
104+
if (!actionsByObject[target]) actionsByObject[target] = [];
105+
actionsByObject[target].push(action);
106+
}
107+
}
108+
109+
return objects.map((obj: any) => {
110+
const actions = actionsByObject[obj.name];
111+
if (!actions) return obj;
112+
return { ...obj, actions: [...(obj.actions || []), ...actions] };
113+
});
114+
}
115+
70116
const allConfigs = [crmConfig, todoConfig, kitchenSinkConfig];
71117

72118
export const sharedConfig = {
@@ -81,12 +127,15 @@ export const sharedConfig = {
81127
// ============================================================================
82128
// Merged Stack Configuration (CRM + Todo + Kitchen Sink + Mock Metadata)
83129
// ============================================================================
84-
objects: mergeViewsIntoObjects(
85-
[
86-
...(crmConfig.objects || []),
87-
...(todoConfig.objects || []),
88-
...(kitchenSinkConfig.objects || []),
89-
],
130+
objects: mergeActionsIntoObjects(
131+
mergeViewsIntoObjects(
132+
[
133+
...(crmConfig.objects || []),
134+
...(todoConfig.objects || []),
135+
...(kitchenSinkConfig.objects || []),
136+
],
137+
allConfigs,
138+
),
90139
allConfigs,
91140
),
92141
apps: [
@@ -143,10 +192,13 @@ export const sharedConfig = {
143192
};
144193

145194
// 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.).
195+
// listViews and actions from objects. Re-merge after validation so the runtime
196+
// protocol serves objects with their view and action definitions.
148197
const validated = defineStack(sharedConfig as Parameters<typeof defineStack>[0]);
149198
export default {
150199
...validated,
151-
objects: mergeViewsIntoObjects(validated.objects || [], allConfigs),
200+
objects: mergeActionsIntoObjects(
201+
mergeViewsIntoObjects(validated.objects || [], allConfigs),
202+
allConfigs,
203+
),
152204
};

examples/crm/src/actions/account.actions.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ export const AccountActions = [
1717
label: 'Assign Owner',
1818
icon: 'user-plus',
1919
type: 'api' as const,
20-
locations: ['record_header' as const, 'list_item' as const],
20+
locations: ['record_header' as const, 'list_item' as const, 'list_toolbar' as const],
21+
bulkEnabled: true,
2122
params: [
2223
{ name: 'owner_id', label: 'New Owner', type: 'lookup' as const, required: true },
2324
],

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ export const ContactActions = [
44
label: 'Send Email',
55
icon: 'mail',
66
type: 'api' as const,
7-
locations: ['record_header' as const, 'list_item' as const],
7+
locations: ['record_header' as const, 'list_item' as const, 'list_toolbar' as const],
8+
bulkEnabled: true,
89
params: [
910
{ name: 'subject', label: 'Subject', type: 'text' as const, required: true },
1011
{ name: 'body', label: 'Message', type: 'textarea' as const },
@@ -17,6 +18,7 @@ export const ContactActions = [
1718
icon: 'user-check',
1819
type: 'api' as const,
1920
locations: ['record_header' as const],
21+
visible: "type !== 'Customer'",
2022
confirmText: 'Convert this contact to a customer?',
2123
refreshAfter: true,
2224
successMessage: 'Contact converted to customer',

examples/crm/src/actions/opportunity.actions.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ export const OpportunityActions = [
44
label: 'Change Stage',
55
icon: 'arrow-right-circle',
66
type: 'api' as const,
7-
locations: ['record_header' as const, 'list_item' as const],
7+
locations: ['record_header' as const, 'list_item' as const, 'list_toolbar' as const],
8+
bulkEnabled: true,
9+
visible: "stage !== 'closed_won' && stage !== 'closed_lost'",
810
params: [
911
{
1012
name: 'new_stage', label: 'New Stage', type: 'select' as const, required: true,
@@ -28,6 +30,7 @@ export const OpportunityActions = [
2830
type: 'api' as const,
2931
locations: ['record_header' as const],
3032
variant: 'primary' as const,
33+
visible: "stage !== 'closed_won' && stage !== 'closed_lost'",
3134
confirmText: 'Mark this opportunity as Closed Won?',
3235
refreshAfter: true,
3336
successMessage: 'Opportunity marked as won!',
@@ -39,6 +42,7 @@ export const OpportunityActions = [
3942
type: 'api' as const,
4043
locations: ['record_more' as const],
4144
variant: 'danger' as const,
45+
visible: "stage !== 'closed_won' && stage !== 'closed_lost'",
4246
params: [
4347
{ name: 'loss_reason', label: 'Reason for Loss', type: 'text' as const, required: true },
4448
],

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: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
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, 'list_toolbar' as const],
8+
bulkEnabled: true,
9+
visible: "status !== 'archived'",
10+
params: [
11+
{
12+
name: 'new_status', label: 'New Status', type: 'select' as const, required: true,
13+
options: [
14+
{ label: 'Draft', value: 'draft' },
15+
{ label: 'Active', value: 'active' },
16+
{ label: 'Review', value: 'review' },
17+
{ label: 'Completed', value: 'completed' },
18+
{ label: 'Archived', value: 'archived' },
19+
],
20+
},
21+
],
22+
refreshAfter: true,
23+
successMessage: 'Status updated',
24+
},
25+
{
26+
name: 'showcase_assign_owner',
27+
label: 'Assign Owner',
28+
icon: 'user-plus',
29+
type: 'api' as const,
30+
locations: ['record_header' as const, 'list_toolbar' as const],
31+
bulkEnabled: true,
32+
params: [
33+
{ name: 'owner_email', label: 'Owner Email', type: 'email' as const, required: true },
34+
{ name: 'notify', label: 'Send Notification', type: 'boolean' as const },
35+
],
36+
refreshAfter: true,
37+
successMessage: 'Owner assigned',
38+
},
39+
{
40+
name: 'showcase_archive',
41+
label: 'Archive',
42+
icon: 'archive',
43+
type: 'api' as const,
44+
locations: ['record_more' as const],
45+
variant: 'danger' as const,
46+
visible: "status !== 'archived'",
47+
confirmText: 'Are you sure you want to archive this item?',
48+
refreshAfter: true,
49+
successMessage: 'Item archived',
50+
},
51+
];

examples/msw-todo/objectstack.config.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,45 @@ export const TaskObject = ObjectSchema.create({
2626
},
2727
});
2828

29+
/**
30+
* Task Actions — complete, toggle, and delete operations
31+
*/
32+
export const TaskActions = [
33+
{
34+
name: 'task_complete',
35+
label: 'Mark Complete',
36+
icon: 'check-circle-2',
37+
type: 'api' as const,
38+
locations: ['record_header' as const, 'list_item' as const, 'list_toolbar' as const],
39+
visible: 'is_completed === false',
40+
bulkEnabled: true,
41+
refreshAfter: true,
42+
successMessage: 'Task completed',
43+
},
44+
{
45+
name: 'task_reopen',
46+
label: 'Reopen Task',
47+
icon: 'rotate-ccw',
48+
type: 'api' as const,
49+
locations: ['record_header' as const, 'list_item' as const],
50+
visible: 'is_completed === true',
51+
refreshAfter: true,
52+
successMessage: 'Task reopened',
53+
},
54+
{
55+
name: 'task_delete',
56+
label: 'Delete Task',
57+
icon: 'trash-2',
58+
type: 'api' as const,
59+
locations: ['record_more' as const, 'list_toolbar' as const],
60+
variant: 'danger' as const,
61+
bulkEnabled: true,
62+
confirmText: 'Are you sure you want to delete this task?',
63+
refreshAfter: true,
64+
successMessage: 'Task deleted',
65+
},
66+
];
67+
2968
/**
3069
* App Configuration — Standard ObjectStackDefinition format
3170
*/
@@ -47,6 +86,9 @@ export default defineStack({
4786
},
4887
},
4988
],
89+
actions: [
90+
...TaskActions,
91+
],
5092
apps: [
5193
App.create({
5294
name: 'task_app',

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',

0 commit comments

Comments
 (0)