Skip to content

Commit 5b5f7d5

Browse files
authored
feat(plugin-form): render object fieldGroups as full-width, collapsible form sections (#1789)
* feat(plugin-form): render object fieldGroups as form sections ObjectForm only honored field groups in the designer preview. Now the runtime SimpleObjectForm auto-derives sections from an object's declared `fieldGroups` + each field's `group` when no explicit sections are given, so authored groups show up on the actual record form. - Add fieldGroups.ts helper (readObjectFieldGroups / deriveFieldGroupSections) mirroring the designer's grouping semantics (declared order, trailing untitled bucket for ungrouped fields). - Carry field `group` onto generated FormFields. - Reuse the existing section-render path (i18n + FLS) via derived sections. - Unit + integration tests. * feat(plugin-form): full-width, collapsible fieldGroup form sections Render fieldGroup-derived simple-form sections as full-width, optionally collapsible groups, and fix a latent multi-section submit bug. Previously each derived section rendered its own <SchemaRenderer type="form"> — i.e. N independent react-hook-form instances — with the submit button only on the last one. Submitting therefore captured just the last group's fields and silently dropped every other group's edits. All sections now share ONE form (the same single-form pattern DrawerForm uses): a virtual `section-divider` field renders each group's header and the group's fields follow inline. This fixes submit (every field is captured), lets a collapsed group simply hide its fields (`hidden: true`) while react-hook-form retains their values, and lets cross-section field conditions resolve via watch(). Field groups gain `collapsible` / `collapsed` flags: `readObjectFieldGroups` reads them and `deriveFieldGroupSections` passes them through to the section. The byo-backend-console example is fixed and extended to demo this: the Tailwind v4 entry (index.css) is corrected so @object-ui components are styled, a standalone /form/:objectName route renders the simple ObjectForm, and the contact mock declares two collapsible groups plus an ungrouped field. Tests: readObjectFieldGroups reads/validates the flags; deriveFieldGroupSections passes them through and omits them when absent; ObjectForm collapses a group on header click (its fields leave the DOM) and restores them on expand.
1 parent 05799db commit 5b5f7d5

9 files changed

Lines changed: 758 additions & 113 deletions

File tree

examples/byo-backend-console/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
"dependencies": {
1313
"@object-ui/app-shell": "workspace:*",
1414
"@object-ui/components": "workspace:*",
15+
"@object-ui/plugin-form": "workspace:*",
1516
"@object-ui/plugin-view": "workspace:*",
1617
"@object-ui/providers": "workspace:*",
1718
"@object-ui/react": "workspace:*",

examples/byo-backend-console/src/App.tsx

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { BrowserRouter, Routes, Route, Link, useParams } from 'react-router-dom';
22
import { AppShell } from '@object-ui/app-shell';
33
import { ObjectView } from '@object-ui/plugin-view';
4+
import { ObjectForm } from '@object-ui/plugin-form';
45
import { ThemeProvider, DataSourceProvider } from '@object-ui/providers';
56
import { mockDataSource } from './mockDataSource';
67

@@ -19,6 +20,7 @@ function App() {
1920
<AppShell sidebar={<Sidebar />}>
2021
<Routes>
2122
<Route path="/" element={<Home />} />
23+
<Route path="/form/:objectName" element={<ObjectFormPage />} />
2224
<Route path="/:objectName" element={<ObjectPage />} />
2325
</Routes>
2426
</AppShell>
@@ -52,6 +54,12 @@ function Sidebar() {
5254
>
5355
Accounts
5456
</Link>
57+
<Link
58+
to="/form/contact"
59+
className="block rounded px-3 py-2 text-sm hover:bg-accent"
60+
>
61+
Contact Form (grouped)
62+
</Link>
5563
</div>
5664

5765
<div className="mt-auto border-t pt-4 text-xs text-muted-foreground">
@@ -137,4 +145,29 @@ function ObjectPage() {
137145
);
138146
}
139147

148+
/**
149+
* Standalone simple ObjectForm. Unlike the grid's modal create/view dialogs,
150+
* this is the default `formType: 'simple'` form — so when the object declares
151+
* `fieldGroups` (see mockDataSource), the form renders grouped sections.
152+
*/
153+
function ObjectFormPage() {
154+
const { objectName } = useParams<{ objectName: string }>();
155+
156+
return (
157+
<div className="mx-auto max-w-2xl p-8">
158+
<h1 className="mb-6 text-2xl font-bold">
159+
New {objectName} (simple form)
160+
</h1>
161+
<ObjectForm
162+
schema={{
163+
type: 'object-form',
164+
objectName: objectName || '',
165+
mode: 'create',
166+
}}
167+
dataSource={mockDataSource}
168+
/>
169+
</div>
170+
);
171+
}
172+
140173
export default App;
Lines changed: 168 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,176 @@
11
/**
2-
* Minimal CSS for the example
3-
* In a real app, you'd import your full Tailwind setup
2+
* Tailwind v4 setup for the example.
3+
*
4+
* This project depends on Tailwind v4 (`@tailwindcss/postcss`), so it MUST use
5+
* the v4 entry syntax — NOT the legacy v3 `@tailwind base/components/utilities`
6+
* directives (v4 ignores them, which is why the form/grid/app-shell rendered
7+
* completely unstyled).
8+
*
9+
* Three pieces are required, mirroring apps/console/src/index.css:
10+
* 1. `@import 'tailwindcss'` — Tailwind v4 engine + preflight.
11+
* 2. `@import '@object-ui/app-shell/styles.css'` — shared variant/theming contract.
12+
* 3. `@theme { --color-*: hsl(var(--*)) }` + `:root`/`.dark` token blocks —
13+
* registers `bg-card` / `text-muted-foreground` / `border` etc. as real
14+
* utilities AND gives them values. Without the `@theme` mapping those
15+
* class names don't exist in v4.
16+
* 4. `@source` globs — Tailwind only generates classes it can see; the class
17+
* names live inside the `@object-ui/*` package sources, so those dirs must
18+
* be scanned or the components stay unstyled.
419
*/
520

6-
@tailwind base;
7-
@tailwind components;
8-
@tailwind utilities;
9-
10-
:root {
11-
--background: 0 0% 100%;
12-
--foreground: 222.2 84% 4.9%;
13-
--muted: 210 40% 96.1%;
14-
--muted-foreground: 215.4 16.3% 46.9%;
15-
--accent: 210 40% 96.1%;
16-
--accent-foreground: 222.2 47.4% 11.2%;
17-
--primary: 222.2 47.4% 11.2%;
18-
--primary-foreground: 210 40% 98%;
19-
--destructive: 0 84.2% 60.2%;
20-
--destructive-foreground: 210 40% 98%;
21-
--border: 214.3 31.8% 91.4%;
22-
}
21+
@import 'tailwindcss';
22+
@import '@object-ui/app-shell/styles.css';
2323

24-
.dark {
25-
--background: 222.2 84% 4.9%;
26-
--foreground: 210 40% 98%;
27-
--muted: 217.2 32.6% 17.5%;
28-
--muted-foreground: 215 20.2% 65.1%;
29-
--accent: 217.2 32.6% 17.5%;
30-
--accent-foreground: 210 40% 98%;
31-
--primary: 210 40% 98%;
32-
--primary-foreground: 222.2 47.4% 11.2%;
33-
--destructive: 0 62.8% 30.6%;
34-
--destructive-foreground: 210 40% 98%;
35-
--border: 217.2 32.6% 17.5%;
36-
}
24+
/* Scan this app + every ObjectUI package whose components we render. */
25+
@source '../src/**/*.{ts,tsx}';
26+
@source '../../../packages/app-shell/src/**/*.{ts,tsx}';
27+
@source '../../../packages/components/src/**/*.{ts,tsx}';
28+
@source '../../../packages/layout/src/**/*.{ts,tsx}';
29+
@source '../../../packages/fields/src/**/*.{ts,tsx}';
30+
@source '../../../packages/react/src/**/*.{ts,tsx}';
31+
@source '../../../packages/plugin-form/src/**/*.{ts,tsx}';
32+
@source '../../../packages/plugin-view/src/**/*.{ts,tsx}';
33+
@source '../../../packages/plugin-grid/src/**/*.{ts,tsx}';
34+
@source '../../../packages/plugin-list/src/**/*.{ts,tsx}';
35+
@source '../../../packages/plugin-detail/src/**/*.{ts,tsx}';
36+
37+
/* Map the shadcn-style HSL design tokens to Tailwind v4 color utilities. */
38+
@theme {
39+
--radius-lg: var(--radius);
40+
--radius-md: calc(var(--radius) - 2px);
41+
--radius-sm: calc(var(--radius) - 4px);
42+
43+
--color-border: hsl(var(--border));
44+
--color-input: hsl(var(--input));
45+
--color-ring: hsl(var(--ring));
46+
--color-background: hsl(var(--background));
47+
--color-foreground: hsl(var(--foreground));
48+
--color-primary: hsl(var(--primary));
49+
--color-primary-foreground: hsl(var(--primary-foreground));
50+
--color-secondary: hsl(var(--secondary));
51+
--color-secondary-foreground: hsl(var(--secondary-foreground));
52+
--color-destructive: hsl(var(--destructive));
53+
--color-destructive-foreground: hsl(var(--destructive-foreground));
54+
--color-muted: hsl(var(--muted));
55+
--color-muted-foreground: hsl(var(--muted-foreground));
56+
--color-accent: hsl(var(--accent));
57+
--color-accent-foreground: hsl(var(--accent-foreground));
58+
--color-popover: hsl(var(--popover));
59+
--color-popover-foreground: hsl(var(--popover-foreground));
60+
--color-card: hsl(var(--card));
61+
--color-card-foreground: hsl(var(--card-foreground));
62+
--color-sidebar: hsl(var(--sidebar));
63+
--color-sidebar-foreground: hsl(var(--sidebar-foreground));
64+
--color-sidebar-primary: hsl(var(--sidebar-primary));
65+
--color-sidebar-primary-foreground: hsl(var(--sidebar-primary-foreground));
66+
--color-sidebar-accent: hsl(var(--sidebar-accent));
67+
--color-sidebar-accent-foreground: hsl(var(--sidebar-accent-foreground));
68+
--color-sidebar-border: hsl(var(--sidebar-border));
69+
--color-sidebar-ring: hsl(var(--sidebar-ring));
3770

38-
* {
39-
border-color: hsl(var(--border));
71+
--color-success: hsl(var(--success));
72+
--color-success-foreground: hsl(var(--success-foreground));
73+
--color-warning: hsl(var(--warning));
74+
--color-warning-foreground: hsl(var(--warning-foreground));
4075
}
4176

42-
body {
43-
background-color: hsl(var(--background));
44-
color: hsl(var(--foreground));
77+
@layer base {
78+
:root {
79+
--background: 0 0% 100%;
80+
--foreground: 222.2 84% 4.9%;
81+
82+
--card: 0 0% 100%;
83+
--card-foreground: 222.2 84% 4.9%;
84+
85+
--popover: 0 0% 100%;
86+
--popover-foreground: 222.2 84% 4.9%;
87+
88+
--primary: 243 75% 59%;
89+
--primary-foreground: 0 0% 100%;
90+
91+
--secondary: 210 40% 96.1%;
92+
--secondary-foreground: 222.2 47.4% 11.2%;
93+
94+
--muted: 210 40% 96.1%;
95+
--muted-foreground: 215.4 16.3% 46.9%;
96+
97+
--accent: 210 40% 96.1%;
98+
--accent-foreground: 222.2 47.4% 11.2%;
99+
100+
--destructive: 0 84.2% 60.2%;
101+
--destructive-foreground: 210 40% 98%;
102+
103+
--border: 214.3 31.8% 91.4%;
104+
--input: 214.3 31.8% 91.4%;
105+
--ring: 243 75% 59%;
106+
107+
--radius: 0.5rem;
108+
109+
--sidebar: 0 0% 98%;
110+
--sidebar-foreground: 240 5.3% 26.1%;
111+
--sidebar-primary: 243 75% 59%;
112+
--sidebar-primary-foreground: 0 0% 98%;
113+
--sidebar-accent: 240 4.8% 95.9%;
114+
--sidebar-accent-foreground: 240 5.9% 10%;
115+
--sidebar-border: 220 13% 91%;
116+
--sidebar-ring: 243 75% 59%;
117+
118+
--success: 142 71% 45%;
119+
--success-foreground: 0 0% 100%;
120+
--warning: 38 92% 50%;
121+
--warning-foreground: 0 0% 100%;
122+
}
123+
124+
.dark {
125+
--background: 222.2 84% 4.9%;
126+
--foreground: 210 40% 98%;
127+
128+
--card: 222.2 84% 4.9%;
129+
--card-foreground: 210 40% 98%;
130+
131+
--popover: 222.2 84% 4.9%;
132+
--popover-foreground: 210 40% 98%;
133+
134+
--primary: 239 84% 67%;
135+
--primary-foreground: 0 0% 100%;
136+
137+
--secondary: 217.2 32.6% 17.5%;
138+
--secondary-foreground: 210 40% 98%;
139+
140+
--muted: 217.2 32.6% 17.5%;
141+
--muted-foreground: 215 20.2% 65.1%;
142+
143+
--accent: 217.2 32.6% 17.5%;
144+
--accent-foreground: 210 40% 98%;
145+
146+
--destructive: 0 62.8% 30.6%;
147+
--destructive-foreground: 210 40% 98%;
148+
149+
--border: 217.2 32.6% 17.5%;
150+
--input: 217.2 32.6% 17.5%;
151+
--ring: 239 84% 67%;
152+
153+
--sidebar: 240 5.9% 10%;
154+
--sidebar-foreground: 240 4.8% 95.9%;
155+
--sidebar-primary: 239 84% 67%;
156+
--sidebar-primary-foreground: 0 0% 100%;
157+
--sidebar-accent: 240 3.7% 15.9%;
158+
--sidebar-accent-foreground: 240 4.8% 95.9%;
159+
--sidebar-border: 240 3.7% 15.9%;
160+
--sidebar-ring: 239 84% 67%;
161+
162+
--success: 142 65% 50%;
163+
--success-foreground: 0 0% 100%;
164+
--warning: 38 92% 55%;
165+
--warning-foreground: 0 0% 10%;
166+
}
167+
168+
* {
169+
border-color: hsl(var(--border));
170+
}
171+
172+
body {
173+
background-color: hsl(var(--background));
174+
color: hsl(var(--foreground));
175+
}
45176
}

0 commit comments

Comments
 (0)