Skip to content

Commit be962ae

Browse files
examples(app-crm): showcase fieldGroups MVP on Account/Contact/Opportunity/Lead
Declare logical `fieldGroups` on each of the four primary CRM objects and opt every field in via `group: '<key>'`. No business-logic changes — only form-layout metadata — so validations, workflows, indexes, and state machines are unaffected. - Account: basic / financials / contact_info / ownership / branding / system - Contact: identity / account_info / contact_info / mailing_address / additional / preferences - Opportunity: basic / financials / sales_process / classification / competition / notes / forecast - Lead: identity / company_info / contact_info / qualification / assignment / address / additional / preferences / conversion Verified every field maps to a declared group key (0 orphaned, 0 ungrouped) and all four objects parse successfully with `ObjectSchema.safeParse`. Pre-existing typecheck errors in `src/views/lead.view.ts` and `objectstack.config.ts` (unrelated `'1'|'2'|'3'|'4'` string enum and sharing-rule types) are untouched by this change. CHANGELOG entry added under `[Unreleased] → Changed`. Agent-Logs-Url: https://github.com/objectstack-ai/framework/sessions/c2759af4-65bb-4cff-8b2b-ed605c936273 Co-authored-by: xuyushun441-sys <255036401+xuyushun441-sys@users.noreply.github.com>
1 parent 55743eb commit be962ae

5 files changed

Lines changed: 244 additions & 119 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Changed
11+
- **`examples/app-crm` — showcase `fieldGroups` MVP** — The CRM reference example (`Account`, `Contact`, `Opportunity`, `Lead`) now demonstrates the new `fieldGroups` protocol end to end. Each object declares logical groups (e.g., *Basic Information*, *Financials*, *Contact Information*, *Ownership & Status*, *System*) and every field opts in via `group: '<key>'`. No business logic changed — only field-layout metadata — so existing validations, workflows, indexes, and state machines are unaffected. Useful as a reference when designing multi-group forms and detail pages.
12+
1013
### Added
1114
- **Field Groups (`fieldGroups`) — simplified MVP protocol** — Introduced a data-layer protocol for grouping fields on an object in forms, detail pages, and editors. Designed to be AI-generation- and extension-friendly by intentionally minimizing surface area:
1215
- New `ObjectFieldGroupSchema` in `packages/spec/src/data/object.zod.ts` with `key` (snake_case machine key), `label`, optional `icon`, `description`, `defaultExpanded` (default `true`), and `visibleOn` (expression for conditional visibility). No `order` property — **array declaration order is the display order**.

examples/app-crm/src/objects/account.object.ts

Lines changed: 56 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -10,35 +10,50 @@ export const Account = ObjectSchema.create({
1010
description: 'Companies and organizations doing business with us',
1111
titleFormat: '{account_number} - {name}',
1212
compactLayout: ['account_number', 'name', 'type', 'owner'],
13-
13+
14+
// Field groups organize the form layout. Array order == display order.
15+
// Each field below opts in via `group: '<key>'`.
16+
fieldGroups: [
17+
{ key: 'basic', label: 'Basic Information', icon: 'building' },
18+
{ key: 'financials', label: 'Financials', icon: 'dollar-sign' },
19+
{ key: 'contact_info', label: 'Contact Information', icon: 'phone' },
20+
{ key: 'ownership', label: 'Ownership & Status', icon: 'users' },
21+
{ key: 'branding', label: 'Branding', icon: 'palette', defaultExpanded: false },
22+
{ key: 'system', label: 'System', icon: 'settings', defaultExpanded: false },
23+
],
24+
1425
fields: {
1526
// AutoNumber field - Unique account identifier
1627
account_number: Field.autonumber({
1728
label: 'Account Number',
1829
format: 'ACC-{0000}',
30+
group: 'basic',
1931
}),
20-
32+
2133
// Basic Information
22-
name: Field.text({
23-
label: 'Account Name',
24-
required: true,
34+
name: Field.text({
35+
label: 'Account Name',
36+
required: true,
2537
searchable: true,
2638
maxLength: 255,
39+
group: 'basic',
2740
}),
28-
41+
2942
// Select fields with custom options
3043
type: Field.select({
3144
label: 'Account Type',
45+
group: 'basic',
3246
options: [
3347
{ label: 'Prospect', value: 'prospect', color: '#FFA500', default: true },
3448
{ label: 'Customer', value: 'customer', color: '#00AA00' },
3549
{ label: 'Partner', value: 'partner', color: '#0000FF' },
3650
{ label: 'Former Customer', value: 'former', color: '#999999' },
3751
]
3852
}),
39-
53+
4054
industry: Field.select({
4155
label: 'Industry',
56+
group: 'basic',
4257
options: [
4358
{ label: 'Technology', value: 'technology' },
4459
{ label: 'Finance', value: 'finance' },
@@ -48,75 +63,86 @@ export const Account = ObjectSchema.create({
4863
{ label: 'Education', value: 'education' },
4964
]
5065
}),
51-
66+
67+
description: Field.markdown({
68+
label: 'Description',
69+
group: 'basic',
70+
}),
71+
5272
// Number fields
53-
annual_revenue: Field.currency({
73+
annual_revenue: Field.currency({
5474
label: 'Annual Revenue',
5575
scale: 2,
5676
min: 0,
77+
group: 'financials',
5778
}),
58-
79+
5980
number_of_employees: Field.number({
6081
label: 'Employees',
6182
min: 0,
83+
group: 'financials',
6284
}),
63-
85+
6486
// Contact Information
65-
phone: Field.text({
87+
phone: Field.text({
6688
label: 'Phone',
6789
format: 'phone',
90+
group: 'contact_info',
6891
}),
69-
92+
7093
website: Field.url({
7194
label: 'Website',
95+
group: 'contact_info',
7296
}),
73-
97+
7498
// Structured Address field (new field type)
7599
billing_address: Field.address({
76100
label: 'Billing Address',
77101
addressFormat: 'international',
102+
group: 'contact_info',
78103
}),
79-
104+
80105
// Office Location (new field type)
81106
office_location: Field.location({
82107
label: 'Office Location',
83108
displayMap: true,
84109
allowGeocoding: true,
110+
group: 'contact_info',
85111
}),
86-
112+
87113
// Relationship fields
88114
owner: Field.lookup('user', {
89115
label: 'Account Owner',
90116
required: true,
117+
group: 'ownership',
91118
}),
92-
119+
93120
parent_account: Field.lookup('account', {
94121
label: 'Parent Account',
95122
description: 'Parent company in hierarchy',
123+
group: 'ownership',
96124
}),
97-
98-
// Rich text field
99-
description: Field.markdown({
100-
label: 'Description',
101-
}),
102-
125+
103126
// Boolean field
104127
is_active: Field.boolean({
105128
label: 'Active',
106129
defaultValue: true,
130+
group: 'ownership',
107131
}),
108-
109-
// Date field
110-
last_activity_date: Field.date({
111-
label: 'Last Activity Date',
112-
readonly: true,
113-
}),
114-
132+
115133
// Brand color (new field type)
116134
brand_color: Field.color({
117135
label: 'Brand Color',
118136
colorFormat: 'hex',
119137
presetColors: ['#FF0000', '#00FF00', '#0000FF', '#FFFF00', '#FF00FF', '#00FFFF'],
138+
group: 'branding',
139+
}),
140+
141+
// Date field
142+
last_activity_date: Field.date({
143+
label: 'Last Activity Date',
144+
readonly: true,
145+
group: 'system',
120146
}),
121147
},
122148

examples/app-crm/src/objects/contact.object.ts

Lines changed: 70 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,20 @@ export const Contact = ObjectSchema.create({
99
icon: 'user',
1010
description: 'People associated with accounts',
1111

12+
fieldGroups: [
13+
{ key: 'identity', label: 'Identity', icon: 'user' },
14+
{ key: 'account_info', label: 'Account & Role', icon: 'briefcase' },
15+
{ key: 'contact_info', label: 'Contact Information', icon: 'phone' },
16+
{ key: 'mailing_address', label: 'Mailing Address', icon: 'map-pin', defaultExpanded: false },
17+
{ key: 'additional', label: 'Additional Info', icon: 'info', defaultExpanded: false },
18+
{ key: 'preferences', label: 'Communication Preferences', icon: 'bell-off', defaultExpanded: false },
19+
],
20+
1221
fields: {
1322
// Name fields
1423
salutation: Field.select({
1524
label: 'Salutation',
25+
group: 'identity',
1626
options: [
1727
{ label: 'Mr.', value: 'mr' },
1828
{ label: 'Ms.', value: 'ms' },
@@ -21,55 +31,50 @@ export const Contact = ObjectSchema.create({
2131
{ label: 'Prof.', value: 'prof' },
2232
]
2333
}),
24-
first_name: Field.text({
34+
first_name: Field.text({
2535
label: 'First Name',
2636
required: true,
2737
searchable: true,
38+
group: 'identity',
2839
}),
29-
last_name: Field.text({
40+
last_name: Field.text({
3041
label: 'Last Name',
3142
required: true,
3243
searchable: true,
44+
group: 'identity',
3345
}),
34-
46+
3547
// Formula field - Full name
3648
full_name: Field.formula({
3749
label: 'Full Name',
3850
expression: 'CONCAT(salutation, " ", first_name, " ", last_name)',
51+
group: 'identity',
3952
}),
40-
53+
54+
// Avatar field
55+
avatar: Field.avatar({
56+
label: 'Profile Picture',
57+
group: 'identity',
58+
}),
59+
4160
// Relationship: Link to Account (Master-Detail)
4261
account: Field.masterDetail('account', {
4362
label: 'Account',
4463
required: true,
4564
writeRequiresMasterRead: true,
4665
deleteBehavior: 'cascade', // Delete contacts when account is deleted
66+
group: 'account_info',
4767
}),
48-
49-
// Contact Information
50-
email: Field.email({
51-
label: 'Email',
52-
required: true,
53-
unique: true,
54-
}),
55-
56-
phone: Field.text({
57-
label: 'Phone',
58-
format: 'phone',
59-
}),
60-
61-
mobile: Field.text({
62-
label: 'Mobile',
63-
format: 'phone',
64-
}),
65-
68+
6669
// Professional Information
6770
title: Field.text({
6871
label: 'Job Title',
72+
group: 'account_info',
6973
}),
70-
74+
7175
department: Field.select({
7276
label: 'Department',
77+
group: 'account_info',
7378
options: [
7479
{ label: 'Executive', value: 'executive' },
7580
{ label: 'Sales', value: 'sales' },
@@ -81,32 +86,56 @@ export const Contact = ObjectSchema.create({
8186
{ label: 'Operations', value: 'operations' },
8287
]
8388
}),
84-
89+
8590
// Relationship fields
8691
reports_to: Field.lookup('contact', {
8792
label: 'Reports To',
8893
description: 'Direct manager/supervisor',
94+
group: 'account_info',
8995
}),
90-
96+
9197
owner: Field.lookup('user', {
9298
label: 'Contact Owner',
9399
required: true,
100+
group: 'account_info',
94101
}),
95-
102+
103+
// Contact Information
104+
email: Field.email({
105+
label: 'Email',
106+
required: true,
107+
unique: true,
108+
group: 'contact_info',
109+
}),
110+
111+
phone: Field.text({
112+
label: 'Phone',
113+
format: 'phone',
114+
group: 'contact_info',
115+
}),
116+
117+
mobile: Field.text({
118+
label: 'Mobile',
119+
format: 'phone',
120+
group: 'contact_info',
121+
}),
122+
96123
// Mailing Address
97-
mailing_street: Field.textarea({ label: 'Mailing Street' }),
98-
mailing_city: Field.text({ label: 'Mailing City' }),
99-
mailing_state: Field.text({ label: 'Mailing State/Province' }),
100-
mailing_postal_code: Field.text({ label: 'Mailing Postal Code' }),
101-
mailing_country: Field.text({ label: 'Mailing Country' }),
102-
124+
mailing_street: Field.textarea({ label: 'Mailing Street', group: 'mailing_address' }),
125+
mailing_city: Field.text({ label: 'Mailing City', group: 'mailing_address' }),
126+
mailing_state: Field.text({ label: 'Mailing State/Province', group: 'mailing_address' }),
127+
mailing_postal_code: Field.text({ label: 'Mailing Postal Code', group: 'mailing_address' }),
128+
mailing_country: Field.text({ label: 'Mailing Country', group: 'mailing_address' }),
129+
103130
// Additional Information
104131
birthdate: Field.date({
105132
label: 'Birthdate',
133+
group: 'additional',
106134
}),
107-
135+
108136
lead_source: Field.select({
109137
label: 'Lead Source',
138+
group: 'additional',
110139
options: [
111140
{ label: 'Web', value: 'web' },
112141
{ label: 'Referral', value: 'referral' },
@@ -115,31 +144,30 @@ export const Contact = ObjectSchema.create({
115144
{ label: 'Advertisement', value: 'advertisement' },
116145
]
117146
}),
118-
147+
119148
description: Field.markdown({
120149
label: 'Description',
150+
group: 'additional',
121151
}),
122-
152+
123153
// Flags
124154
is_primary: Field.boolean({
125155
label: 'Primary Contact',
126156
defaultValue: false,
127157
description: 'Is this the main contact for the account?',
158+
group: 'preferences',
128159
}),
129-
160+
130161
do_not_call: Field.boolean({
131162
label: 'Do Not Call',
132163
defaultValue: false,
164+
group: 'preferences',
133165
}),
134-
166+
135167
email_opt_out: Field.boolean({
136168
label: 'Email Opt Out',
137169
defaultValue: false,
138-
}),
139-
140-
// Avatar field
141-
avatar: Field.avatar({
142-
label: 'Profile Picture',
170+
group: 'preferences',
143171
}),
144172
},
145173

0 commit comments

Comments
 (0)