Skip to content

Commit 9427c67

Browse files
os-zhuangclaude
andauthored
docs(ui): field-group derivation is authorized by fieldGroups, not by field.group alone (#5458) (#5763)
Two UI guides taught that form sectioning derives from `field.group` by itself, and neither page mentioned `fieldGroups` at all. Copying either one yields a FLAT form: `deriveFieldGroupLayout` (ADR-0085 §5) buckets only fields whose `group` matches a declared `fieldGroups[].key`, and with no declared group it returns null outright — so an undeclared `group` renders exactly like no `group`, and `os lint` reports `field-group-undeclared`. Same falsehood as #5443, one layer up: that issue fixed the showcase object the two pages link to as their runnable example; this fixes the prose that cites it. - create-vs-edit-form.mdx §2: the derivation's authorization source, the undeclared-equals-unwritten equivalence, and the lint rule name. Also drops "equivalent grouped form" for the two measured differences a derived form really has (platform-injected `owner_id` lands in the trailing untitled section; `columns` is a form-view knob the group declaration cannot carry). - field-grouping-and-order.mdx: same three facts on the semantic-grouping section, and its sample — previously a `fields: {…}` fragment with no declaration side — becomes a self-contained `ObjectSchema.create` with the matching `fieldGroups`, marked `{/* os:check */}` so the gate type-checks it against the built spec instead of leaving the corrected example ungated. Co-authored-by: Claude <noreply@anthropic.com>
1 parent de770bf commit 9427c67

2 files changed

Lines changed: 45 additions & 15 deletions

File tree

content/docs/ui/create-vs-edit-form.mdx

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,25 @@ The create-form subset is not an arbitrary pick; it is *derivable* from signals
2323
| `readonly` / formula / rollup / autonumber / system-stamped | **never** on create (you can't set it) |
2424
| `defaultValue` present | **can be omitted** from create (it self-fills) |
2525
| `hidden` | off everywhere by default |
26-
| `group` | which section the field belongs to (semantic, travels with the model) |
26+
| `group` | which section the field belongs to (semantic, travels with the model — and only once that group is **declared** in `fieldGroups`; see §2) |
2727
| *declaration order* | the order you write fields in **is** the default order everywhere — there is no `field.order` |
2828

2929
So a sensible create form is: *editable, required-or-core fields, in declaration order* — and it **falls out** of the object. This is ADR-0047's guardrail: **omission is correct** — emit nothing extra and you still get a complete, correct form.
3030

31-
### 2. The default (edit) form derives from `field.group`
31+
### 2. The default (edit) form derives from `field.group` — as authorized by `fieldGroups`
3232

33-
The full edit form materialises each `field.group` into a section. You can omit it entirely and let the platform derive an equivalent grouped form. When you do write it, list fields as **bare strings** so each one inherits its type / validation / FLS / default from the object — the form carries layout only, never data semantics.
33+
`field.group` on its own does not create a section. The **authorization source is the object's `fieldGroups` declaration**: `deriveFieldGroupLayout` (ADR-0085 §5) — the single derivation every renderer and the i18n walker consume — sections only those fields whose `group` matches a declared `fieldGroups[].key`.
34+
35+
- An **undeclared** `group` renders **exactly like no `group` at all**: the field drops into a trailing untitled bucket.
36+
- Declare **no** `fieldGroups` at all and grouping does not apply — the derivation yields nothing and you get a flat form.
37+
- Either way, `os lint` (and `os build` / `os validate`) reports every unmatched reference as **`field-group-undeclared`**.
38+
39+
So tag the fields **and** declare the groups — [`contact.object.ts`](#runnable-example) does both. With both halves present, the full edit form materialises each declared group into a section, and you can omit the `form` view entirely and let the platform derive the grouped form. Two ways the derived form still differs from the hand-written one below:
40+
41+
- fields the platform injects and you never grouped (`owner_id`, under `sharingModel: 'private'`) surface in that trailing untitled section;
42+
- `columns` is a **form-view** knob, and the group declaration has no column count to give — a derived section carries only `key` / `label` / `icon` / `description` / `collapse` and its member fields — so a hand-written `columns: 2` is not part of what derivation gives back.
43+
44+
When you do write the form, list fields as **bare strings** so each one inherits its type / validation / FLS / default from the object — the form carries layout only, never data semantics.
3445

3546
### 3. Hand-shape the create form *only when layout or flow diverges*
3647

@@ -50,7 +61,8 @@ export const ContactViews = defineView({
5061
addRecord: { enabled: true, mode: 'form', formView: 'create' },
5162
},
5263

53-
// Full edit form — grouped by field.group; bare strings inherit field defs.
64+
// Full edit form — one section per group DECLARED in the object's
65+
// `fieldGroups`; bare strings inherit field defs.
5466
form: {
5567
type: 'simple', data,
5668
sections: [
@@ -102,7 +114,7 @@ Rule of thumb: **"different field subset" → derive. "different layout or flow"
102114

103115
## Runnable example
104116

105-
- Object: [`examples/app-showcase/src/data/objects/contact.object.ts`](https://github.com/objectstack-ai/objectstack/blob/main/examples/app-showcase/src/data/objects/contact.object.ts) — flat, grouped, intent-tagged field set.
117+
- Object: [`examples/app-showcase/src/data/objects/contact.object.ts`](https://github.com/objectstack-ai/objectstack/blob/main/examples/app-showcase/src/data/objects/contact.object.ts) — flat, intent-tagged field set, with the four `fieldGroups` its `group` tags point at.
106118
- Views: [`examples/app-showcase/src/ui/views/contact.view.ts`](https://github.com/objectstack-ai/objectstack/blob/main/examples/app-showcase/src/ui/views/contact.view.ts) — full edit form + sparse `formViews.create` + `addRecord` binding.
107119

108120
## Anti-patterns

content/docs/ui/field-grouping-and-order.mdx

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -25,17 +25,34 @@ So "flat vs grouped" is not a contradiction — it's one flat set seen through d
2525

2626
There are **two** grouping concepts. Keep them distinct:
2727

28-
**1. Semantic grouping — `field.group` (on the object).** A field's logical home ("billing", "contact_info", "system"). It travels with the model. The Studio field editor folds fields by it, and auto-generated forms use it as the **default** sectioning — so you are not starting from zero.
28+
**1. Semantic grouping — `field.group` + `fieldGroups` (on the object).** A field's logical home ("billing", "contact_info", "system"). It travels with the model. The Studio field editor folds fields by it, and auto-generated forms use it as the **default** sectioning — so you are not starting from zero.
2929

30+
Semantic grouping has **two halves, and you need both**. `fieldGroups` on the object is the **authorization source** for the derivation: `deriveFieldGroupLayout` (ADR-0085 §5) — the one implementation every renderer and the i18n walker consume — sections only those fields whose `group` matches a declared `fieldGroups[].key`. An **undeclared** `group` renders **exactly like no `group` at all** (the field falls into a trailing untitled bucket); with **no** `fieldGroups` declared at all, grouping does not apply and the form comes out flat. `os lint` (and `os build` / `os validate`) reports the mismatch as **`field-group-undeclared`**.
31+
32+
{/* os:check */}
3033
```ts
31-
fields: {
32-
name: Field.text({ label: 'Full name', group: 'contact' }),
33-
email: Field.email({ label: 'Email', group: 'contact' }),
34-
stage: Field.select({ label: 'Stage', group: 'status', options: [/**/] }),
35-
}
34+
import { ObjectSchema, Field } from '@objectstack/spec/data';
35+
36+
export const Contact = ObjectSchema.create({
37+
name: 'showcase_contact',
38+
label: 'Contact',
39+
40+
// The DECLARATION half — array order is display order (there is no `order` key).
41+
fieldGroups: [
42+
{ key: 'contact', label: 'Contact' },
43+
{ key: 'status', label: 'Status' },
44+
],
45+
46+
// The MEMBERSHIP half — each `group` must match a `key` declared above.
47+
fields: {
48+
name: Field.text({ label: 'Full name', group: 'contact' }),
49+
email: Field.email({ label: 'Email', group: 'contact' }),
50+
stage: Field.select({ label: 'Stage', group: 'status', options: [{ label: 'New', value: 'new' }] }),
51+
},
52+
});
3653
```
3754

38-
**2. Layout grouping — form `sections` (on a view).** A specific form's explicit arrangement: which fields, which section, how many columns, collapsible. It can **inherit** `field.group` as the default or **override** it per form.
55+
**2. Layout grouping — form `sections` (on a view).** A specific form's explicit arrangement: which fields, which section, how many columns, collapsible. It can **inherit** the object's declared groups as its default or **override** them per form.
3956

4057
```ts
4158
form: {
@@ -68,17 +85,18 @@ Keeping grouping off the field (beyond an optional semantic hint) is what lets *
6885

6986
## Runnable example
7087

71-
- [`examples/app-showcase/src/data/objects/contact.object.ts`](https://github.com/objectstack-ai/objectstack/blob/main/examples/app-showcase/src/data/objects/contact.object.ts) — fields tagged with `group`.
88+
- [`examples/app-showcase/src/data/objects/contact.object.ts`](https://github.com/objectstack-ai/objectstack/blob/main/examples/app-showcase/src/data/objects/contact.object.ts)four declared `fieldGroups`, and the fields tagged with the matching `group`.
7289
- [`examples/app-showcase/src/ui/views/contact.view.ts`](https://github.com/objectstack-ai/objectstack/blob/main/examples/app-showcase/src/ui/views/contact.view.ts) — sections that materialise those groups.
7390

7491
## Anti-patterns
7592

7693
- **Adding structural nesting to the data model to satisfy a form.** The model is flat; let the form group.
77-
- **Re-typing the grouping in every form.** Declare `field.group` once; forms inherit and only override on real divergence.
94+
- **Re-typing the grouping in every form.** Declare the groups (`fieldGroups`) and tag the fields (`field.group`) once; forms inherit and only override on real divergence.
95+
- **Tagging fields with a `group` you never declared in `fieldGroups`.** The tag reads as intent but authorizes nothing — the fields render ungrouped, and `os lint` says so as `field-group-undeclared`.
7896
- **Assuming a grid "group" will section your form fields.** It buckets rows by value — a different axis entirely.
7997

8098
## See also
8199

82100
- [Create form ≠ edit form](/docs/ui/create-vs-edit-form).
83-
- Reference: [Field schema](/docs/references/data/field) (`group`), [View schema](/docs/references/ui/view) (`sections`, `grouping`).
101+
- Reference: [Object schema](/docs/references/data/object) (`fieldGroups`), [Field schema](/docs/references/data/field) (`group`), [View schema](/docs/references/ui/view) (`sections`, `grouping`).
84102
- Studio: [Object Designer](/docs/references/studio/object-designer) — field editor groups by `field.group`.

0 commit comments

Comments
 (0)