@metaobjectsdev/codegen-ts-react 0.13.1 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"form-file.d.ts","sourceRoot":"","sources":["../../src/templates/form-file.ts"],"names":[],"mappings":"AAmBA,OAAO,EAAa,UAAU,
|
|
1
|
+
{"version":3,"file":"form-file.d.ts","sourceRoot":"","sources":["../../src/templates/form-file.ts"],"names":[],"mappings":"AAmBA,OAAO,EAAa,UAAU,EAAgB,MAAM,0BAA0B,CAAC;AAkB/E,OAAO,EAAE,KAAK,aAAa,EAAgE,MAAM,4BAA4B,CAAC;AA0N9H,wBAAgB,cAAc,CAAC,MAAM,EAAE,UAAU,EAAE,GAAG,EAAE,aAAa,GAAG,MAAM,CAmH7E"}
|
|
@@ -16,8 +16,8 @@
|
|
|
16
16
|
// Form generation is OPT-IN per entity via `@emitForm: true` on the
|
|
17
17
|
// object metadata. Default off. Most projects don't need stock forms.
|
|
18
18
|
import { code, imp } from "ts-poet";
|
|
19
|
-
import { MetaField, MetaObject } from "@metaobjectsdev/metadata";
|
|
20
|
-
import { IDENTITY_SUBTYPE_PRIMARY, IDENTITY_ATTR_FIELDS, FIELD_ATTR_DEFAULT, } from "@metaobjectsdev/metadata";
|
|
19
|
+
import { MetaField, MetaObject, stripPackage } from "@metaobjectsdev/metadata";
|
|
20
|
+
import { IDENTITY_SUBTYPE_PRIMARY, IDENTITY_ATTR_FIELDS, FIELD_ATTR_DEFAULT, FIELD_SUBTYPE_OBJECT, FIELD_ATTR_OBJECT_REF, FIELD_SUBTYPE_INT, FIELD_SUBTYPE_LONG, FIELD_SUBTYPE_DOUBLE, FIELD_SUBTYPE_FLOAT, FIELD_SUBTYPE_DECIMAL, FIELD_SUBTYPE_CURRENCY, FIELD_SUBTYPE_BOOLEAN, FIELD_SUBTYPE_DATE, FIELD_SUBTYPE_TIME, FIELD_SUBTYPE_TIMESTAMP, } from "@metaobjectsdev/metadata";
|
|
21
21
|
import { entityModuleSpecifier, GENERATED_HEADER, tphDiscriminatorPin } from "@metaobjectsdev/codegen-ts";
|
|
22
22
|
function primaryFieldNames(entity) {
|
|
23
23
|
const set = new Set();
|
|
@@ -47,7 +47,7 @@ function isAutoManaged(field) {
|
|
|
47
47
|
* and injected server-side, never rendered. */
|
|
48
48
|
function visibleFields(entity, discField) {
|
|
49
49
|
const pkNames = primaryFieldNames(entity);
|
|
50
|
-
const
|
|
50
|
+
const out = [];
|
|
51
51
|
// fields() returns effective fields, so inherited fields (from extends:/super:) are included in forms.
|
|
52
52
|
for (const child of entity.fields()) {
|
|
53
53
|
if (child.ownAttr("formExclude") === true)
|
|
@@ -58,9 +58,164 @@ function visibleFields(entity, discField) {
|
|
|
58
58
|
continue;
|
|
59
59
|
if (discField !== undefined && child.name === discField)
|
|
60
60
|
continue;
|
|
61
|
-
|
|
61
|
+
out.push(child);
|
|
62
62
|
}
|
|
63
|
-
return
|
|
63
|
+
return out;
|
|
64
|
+
}
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
// Nested value-object sub-forms (issue #95)
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
/** Max recursion depth for nested value objects — a backstop alongside the
|
|
69
|
+
* per-branch visited-set cycle guard. */
|
|
70
|
+
const MAX_VO_DEPTH = 5;
|
|
71
|
+
/** Resolve a `field.object`'s `@objectRef` to the referenced value object from
|
|
72
|
+
* the loaded root. Mirrors the resolution the Zod / inferred-type templates use
|
|
73
|
+
* (read `@objectRef`, strip the package, look the bare name up on the root). */
|
|
74
|
+
function resolveValueObject(field, ctx) {
|
|
75
|
+
if (field.subType !== FIELD_SUBTYPE_OBJECT)
|
|
76
|
+
return undefined;
|
|
77
|
+
const ref = field.attr(FIELD_ATTR_OBJECT_REF);
|
|
78
|
+
if (typeof ref !== "string" || ref.length === 0)
|
|
79
|
+
return undefined;
|
|
80
|
+
const base = stripPackage(ref);
|
|
81
|
+
return ctx.loadedRoot.objects().find((o) => o.name === base);
|
|
82
|
+
}
|
|
83
|
+
/** camelCase / PascalCase → human label ("llmConfig" → "Llm Config"). */
|
|
84
|
+
function humanize(s) {
|
|
85
|
+
return s
|
|
86
|
+
.replace(/([a-z])([A-Z])/g, "$1 $2")
|
|
87
|
+
.replace(/_/g, " ")
|
|
88
|
+
.replace(/\b\w/g, (c) => c.toUpperCase())
|
|
89
|
+
.trim();
|
|
90
|
+
}
|
|
91
|
+
/** A real HTML <input type=...> for a scalar field subtype, or undefined (text). */
|
|
92
|
+
function htmlTypeForSubType(subType) {
|
|
93
|
+
switch (subType) {
|
|
94
|
+
case FIELD_SUBTYPE_INT:
|
|
95
|
+
case FIELD_SUBTYPE_LONG:
|
|
96
|
+
case FIELD_SUBTYPE_DOUBLE:
|
|
97
|
+
case FIELD_SUBTYPE_FLOAT:
|
|
98
|
+
case FIELD_SUBTYPE_DECIMAL:
|
|
99
|
+
case FIELD_SUBTYPE_CURRENCY:
|
|
100
|
+
return "number";
|
|
101
|
+
case FIELD_SUBTYPE_BOOLEAN:
|
|
102
|
+
return "checkbox";
|
|
103
|
+
case FIELD_SUBTYPE_DATE:
|
|
104
|
+
return "date";
|
|
105
|
+
case FIELD_SUBTYPE_TIME:
|
|
106
|
+
return "time";
|
|
107
|
+
case FIELD_SUBTYPE_TIMESTAMP:
|
|
108
|
+
return "datetime-local";
|
|
109
|
+
default:
|
|
110
|
+
return undefined;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
/** The react-hook-form `register(...)` argument for a nested field path. A
|
|
114
|
+
* static path is a plain string literal; a path threaded through an array
|
|
115
|
+
* element carries a runtime `${index}` and must be a template literal. */
|
|
116
|
+
function registerArg(path, dynamic) {
|
|
117
|
+
return dynamic ? `\`${path}\`` : JSON.stringify(path);
|
|
118
|
+
}
|
|
119
|
+
/** A stable, identifier-safe useFieldArray hook variable for a static path. */
|
|
120
|
+
function arrayHookVar(path) {
|
|
121
|
+
return `${path.replace(/[^A-Za-z0-9]+/g, "_")}Array`;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Render one NESTED field inside a value-object sub-form. Dispatches on subtype:
|
|
125
|
+
* - a resolvable `field.object` (single) → a nested <fieldset> recursing into
|
|
126
|
+
* the value object's fields;
|
|
127
|
+
* - a resolvable `field.object` array → a useFieldArray repeatable group (when
|
|
128
|
+
* the path is static) or a degraded element-shape fieldset + TODO (when the
|
|
129
|
+
* path is already inside an array element, i.e. dynamic);
|
|
130
|
+
* - anything else → a single <input> bound via a nested react-hook-form path.
|
|
131
|
+
*/
|
|
132
|
+
function renderNestedField(field, path, dynamic, ctx, visited, depth) {
|
|
133
|
+
const vo = resolveValueObject(field, ctx);
|
|
134
|
+
if (vo === undefined) {
|
|
135
|
+
// Scalar / enum / unresolved object → a single bound input.
|
|
136
|
+
const htmlType = htmlTypeForSubType(field.subType);
|
|
137
|
+
const typeAttr = htmlType !== undefined ? ` type=${JSON.stringify(htmlType)}` : "";
|
|
138
|
+
return {
|
|
139
|
+
jsx: `<div className="metaobjects-field" key=${JSON.stringify(path)}>
|
|
140
|
+
<label className="metaobjects-field-label">${humanize(field.name)}</label>
|
|
141
|
+
<input className="metaobjects-field-input"${typeAttr} {...form.register(${registerArg(path, dynamic)})} />
|
|
142
|
+
</div>`,
|
|
143
|
+
hooks: [],
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
const voKey = vo.fqn();
|
|
147
|
+
if (visited.has(voKey) || depth >= MAX_VO_DEPTH) {
|
|
148
|
+
// Cycle or too deep — emit a labeled shell + TODO instead of a flat input.
|
|
149
|
+
return {
|
|
150
|
+
jsx: `<fieldset className="metaobjects-fieldset" key=${JSON.stringify(path)}>
|
|
151
|
+
<legend className="metaobjects-fieldset-legend">${humanize(field.name)}</legend>
|
|
152
|
+
{/* TODO(#95): nested value object ${vo.name} not expanded (recursion guard). Author this sub-form by hand. */}
|
|
153
|
+
</fieldset>`,
|
|
154
|
+
hooks: [],
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
const nextVisited = new Set(visited);
|
|
158
|
+
nextVisited.add(voKey);
|
|
159
|
+
if (!field.isArray) {
|
|
160
|
+
// Single nested value object → a fieldset recursing into its fields.
|
|
161
|
+
const inner = [];
|
|
162
|
+
const hooks = [];
|
|
163
|
+
for (const sub of vo.fields()) {
|
|
164
|
+
const r = renderNestedField(sub, `${path}.${sub.name}`, dynamic, ctx, nextVisited, depth + 1);
|
|
165
|
+
inner.push(r.jsx);
|
|
166
|
+
hooks.push(...r.hooks);
|
|
167
|
+
}
|
|
168
|
+
return {
|
|
169
|
+
jsx: `<fieldset className="metaobjects-fieldset" key=${JSON.stringify(path)}>
|
|
170
|
+
<legend className="metaobjects-fieldset-legend">${humanize(field.name)}</legend>
|
|
171
|
+
${inner.join("\n")}
|
|
172
|
+
</fieldset>`,
|
|
173
|
+
hooks,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
// Array of value objects.
|
|
177
|
+
if (dynamic) {
|
|
178
|
+
// Already inside an array element — a static useFieldArray name is impossible.
|
|
179
|
+
// Degrade to the element shape + a single clear TODO (never a flat input).
|
|
180
|
+
return {
|
|
181
|
+
jsx: `<fieldset className="metaobjects-fieldset" key=${JSON.stringify(path)}>
|
|
182
|
+
<legend className="metaobjects-fieldset-legend">${humanize(field.name)}</legend>
|
|
183
|
+
{/* TODO(#95): arrays of value objects nested inside another array are not auto-wired
|
|
184
|
+
(the react-hook-form path is dynamic). Render this repeatable group by hand. */}
|
|
185
|
+
</fieldset>`,
|
|
186
|
+
hooks: [],
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
// Static path → a useFieldArray-based repeatable group.
|
|
190
|
+
const hookVar = arrayHookVar(path);
|
|
191
|
+
const elementPath = `${path}.\${index}`;
|
|
192
|
+
const inner = [];
|
|
193
|
+
const hooks = [`const ${hookVar} = useFieldArray({ control: form.control, name: ${JSON.stringify(path)} as never });`];
|
|
194
|
+
for (const sub of vo.fields()) {
|
|
195
|
+
const r = renderNestedField(sub, `${elementPath}.${sub.name}`, true, ctx, nextVisited, depth + 1);
|
|
196
|
+
inner.push(r.jsx);
|
|
197
|
+
hooks.push(...r.hooks);
|
|
198
|
+
}
|
|
199
|
+
const label = humanize(field.name);
|
|
200
|
+
const itemLabel = humanize(vo.name);
|
|
201
|
+
return {
|
|
202
|
+
jsx: `<div className="metaobjects-field-array" key=${JSON.stringify(path)}>
|
|
203
|
+
<span className="metaobjects-field-array-label">${label}</span>
|
|
204
|
+
{${hookVar}.fields.map((item, index) => (
|
|
205
|
+
<fieldset className="metaobjects-fieldset" key={item.id}>
|
|
206
|
+
<legend className="metaobjects-fieldset-legend">${itemLabel} {index + 1}</legend>
|
|
207
|
+
${inner.join("\n")}
|
|
208
|
+
<button type="button" className="metaobjects-field-array-remove" onClick={() => ${hookVar}.remove(index)}>
|
|
209
|
+
Remove
|
|
210
|
+
</button>
|
|
211
|
+
</fieldset>
|
|
212
|
+
))}
|
|
213
|
+
<button type="button" className="metaobjects-field-array-add" onClick={() => ${hookVar}.append({} as never)}>
|
|
214
|
+
Add ${itemLabel}
|
|
215
|
+
</button>
|
|
216
|
+
</div>`,
|
|
217
|
+
hooks,
|
|
218
|
+
};
|
|
64
219
|
}
|
|
65
220
|
export function renderFormFile(entity, ctx) {
|
|
66
221
|
const entityName = entity.name;
|
|
@@ -79,10 +234,9 @@ export function renderFormFile(entity, ctx) {
|
|
|
79
234
|
const ReactElementSym = imp("t:ReactElement@react");
|
|
80
235
|
const SubmitHandlerSym = imp("t:SubmitHandler@react-hook-form");
|
|
81
236
|
const useEntityFormSym = imp("useEntityForm@@metaobjectsdev/react");
|
|
82
|
-
//
|
|
83
|
-
//
|
|
84
|
-
const
|
|
85
|
-
.map((f) => ` <div className="metaobjects-field" key=${JSON.stringify(f)}>
|
|
237
|
+
// The flat scalar block: a label + pre-bound input + error span, driven
|
|
238
|
+
// entirely by the entity constants via `form.input.<field>`. Unchanged.
|
|
239
|
+
const scalarBlock = (f) => ` <div className="metaobjects-field" key=${JSON.stringify(f)}>
|
|
86
240
|
<label className="metaobjects-field-label" htmlFor={${entityName}.${f}.name}>
|
|
87
241
|
{${entityName}.${f}.label}
|
|
88
242
|
</label>
|
|
@@ -92,15 +246,33 @@ export function renderFormFile(entity, ctx) {
|
|
|
92
246
|
{String(form.formState.errors.${f}?.message ?? '')}
|
|
93
247
|
</span>
|
|
94
248
|
)}
|
|
95
|
-
</div
|
|
96
|
-
|
|
249
|
+
</div>`;
|
|
250
|
+
// For each visible field: scalars keep the flat `form.input.<field>` block; a
|
|
251
|
+
// `field.object` with a resolvable `@objectRef` recurses into the referenced
|
|
252
|
+
// value object as a nested <fieldset> sub-form (issue #95). useFieldArray
|
|
253
|
+
// declarations for array-of-value-object fields hoist to the component top.
|
|
254
|
+
const blocks = [];
|
|
255
|
+
const fieldArrayHooks = [];
|
|
256
|
+
for (const f of fields) {
|
|
257
|
+
if (resolveValueObject(f, ctx) === undefined) {
|
|
258
|
+
blocks.push(scalarBlock(f.name));
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
const r = renderNestedField(f, f.name, false, ctx, new Set(), 0);
|
|
262
|
+
blocks.push(r.jsx);
|
|
263
|
+
fieldArrayHooks.push(...r.hooks);
|
|
264
|
+
}
|
|
265
|
+
const fieldBlocks = blocks.join("\n");
|
|
266
|
+
const hookSection = fieldArrayHooks.length > 0 ? `\n ${fieldArrayHooks.join("\n ")}` : "";
|
|
267
|
+
// Only pull in useFieldArray when an array-of-value-object field needs it.
|
|
268
|
+
const useFieldArrayImport = fieldArrayHooks.length > 0 ? `import { useFieldArray } from "react-hook-form";\n` : "";
|
|
97
269
|
const literalImports = code `
|
|
98
270
|
import {
|
|
99
271
|
${entityName},
|
|
100
272
|
${entityName}InsertSchema,
|
|
101
273
|
} from ${JSON.stringify(entityFileSpec)};
|
|
102
274
|
import type { ${entityName} as ${entityName}Row } from ${JSON.stringify(entityFileSpec)};
|
|
103
|
-
`;
|
|
275
|
+
${useFieldArrayImport}`;
|
|
104
276
|
const body = code `
|
|
105
277
|
export interface ${entityName}FormProps {
|
|
106
278
|
onSubmit: ${SubmitHandlerSym}<Partial<${entityName}Row>>;
|
|
@@ -124,7 +296,7 @@ export function ${entityName}Form(props: ${entityName}FormProps): ${ReactElement
|
|
|
124
296
|
${entityName},
|
|
125
297
|
${formSchema},
|
|
126
298
|
props.defaultValues !== undefined ? { defaultValues: props.defaultValues } : {},
|
|
127
|
-
)
|
|
299
|
+
);${hookSection}
|
|
128
300
|
return (
|
|
129
301
|
<form
|
|
130
302
|
className={props.className ?? 'metaobjects-form'}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"form-file.js","sourceRoot":"","sources":["../../src/templates/form-file.ts"],"names":[],"mappings":"AAAA,yEAAyE;AACzE,kEAAkE;AAClE,uEAAuE;AACvE,mDAAmD;AACnD,EAAE;AACF,qBAAqB;AACrB,0EAA0E;AAC1E,gDAAgD;AAChD,oEAAoE;AACpE,EAAE;AACF,4CAA4C;AAC5C,6CAA6C;AAC7C,qDAAqD;AACrD,2DAA2D;AAC3D,EAAE;AACF,oEAAoE;AACpE,sEAAsE;AAEtE,OAAO,EAAE,IAAI,EAAE,GAAG,EAAE,MAAM,SAAS,CAAC;AACpC,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAC;
|
|
1
|
+
{"version":3,"file":"form-file.js","sourceRoot":"","sources":["../../src/templates/form-file.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@metaobjectsdev/codegen-ts-react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"description": "React codegen for metaobjects — emits <Entity>.form.tsx files using react-hook-form and @metaobjectsdev/react helpers.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -45,8 +45,8 @@
|
|
|
45
45
|
"access": "public"
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
|
-
"@metaobjectsdev/metadata": "0.
|
|
49
|
-
"@metaobjectsdev/codegen-ts": "0.
|
|
48
|
+
"@metaobjectsdev/metadata": "0.14.0",
|
|
49
|
+
"@metaobjectsdev/codegen-ts": "0.14.0",
|
|
50
50
|
"ts-poet": "^6.10.0"
|
|
51
51
|
},
|
|
52
52
|
"peerDependencies": {
|
|
@@ -17,11 +17,23 @@
|
|
|
17
17
|
// object metadata. Default off. Most projects don't need stock forms.
|
|
18
18
|
|
|
19
19
|
import { code, imp } from "ts-poet";
|
|
20
|
-
import { MetaField, MetaObject } from "@metaobjectsdev/metadata";
|
|
20
|
+
import { MetaField, MetaObject, stripPackage } from "@metaobjectsdev/metadata";
|
|
21
21
|
import {
|
|
22
22
|
IDENTITY_SUBTYPE_PRIMARY,
|
|
23
23
|
IDENTITY_ATTR_FIELDS,
|
|
24
24
|
FIELD_ATTR_DEFAULT,
|
|
25
|
+
FIELD_SUBTYPE_OBJECT,
|
|
26
|
+
FIELD_ATTR_OBJECT_REF,
|
|
27
|
+
FIELD_SUBTYPE_INT,
|
|
28
|
+
FIELD_SUBTYPE_LONG,
|
|
29
|
+
FIELD_SUBTYPE_DOUBLE,
|
|
30
|
+
FIELD_SUBTYPE_FLOAT,
|
|
31
|
+
FIELD_SUBTYPE_DECIMAL,
|
|
32
|
+
FIELD_SUBTYPE_CURRENCY,
|
|
33
|
+
FIELD_SUBTYPE_BOOLEAN,
|
|
34
|
+
FIELD_SUBTYPE_DATE,
|
|
35
|
+
FIELD_SUBTYPE_TIME,
|
|
36
|
+
FIELD_SUBTYPE_TIMESTAMP,
|
|
25
37
|
} from "@metaobjectsdev/metadata";
|
|
26
38
|
import { type RenderContext, entityModuleSpecifier, GENERATED_HEADER, tphDiscriminatorPin } from "@metaobjectsdev/codegen-ts";
|
|
27
39
|
|
|
@@ -49,18 +61,196 @@ function isAutoManaged(field: MetaField): boolean {
|
|
|
49
61
|
/** Visible form fields = all fields minus PK, DB-auto-defaulted, and (for a TPH
|
|
50
62
|
* subtype) the discriminator — which is implicit (the form is subtype-specific)
|
|
51
63
|
* and injected server-side, never rendered. */
|
|
52
|
-
function visibleFields(entity: MetaObject, discField?: string):
|
|
64
|
+
function visibleFields(entity: MetaObject, discField?: string): MetaField[] {
|
|
53
65
|
const pkNames = primaryFieldNames(entity);
|
|
54
|
-
const
|
|
66
|
+
const out: MetaField[] = [];
|
|
55
67
|
// fields() returns effective fields, so inherited fields (from extends:/super:) are included in forms.
|
|
56
68
|
for (const child of entity.fields()) {
|
|
57
69
|
if (child.ownAttr("formExclude") === true) continue;
|
|
58
70
|
if (pkNames.has(child.name)) continue;
|
|
59
71
|
if (isAutoManaged(child)) continue;
|
|
60
72
|
if (discField !== undefined && child.name === discField) continue;
|
|
61
|
-
|
|
73
|
+
out.push(child);
|
|
62
74
|
}
|
|
63
|
-
return
|
|
75
|
+
return out;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
// Nested value-object sub-forms (issue #95)
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
|
|
82
|
+
/** Max recursion depth for nested value objects — a backstop alongside the
|
|
83
|
+
* per-branch visited-set cycle guard. */
|
|
84
|
+
const MAX_VO_DEPTH = 5;
|
|
85
|
+
|
|
86
|
+
/** Resolve a `field.object`'s `@objectRef` to the referenced value object from
|
|
87
|
+
* the loaded root. Mirrors the resolution the Zod / inferred-type templates use
|
|
88
|
+
* (read `@objectRef`, strip the package, look the bare name up on the root). */
|
|
89
|
+
function resolveValueObject(field: MetaField, ctx: RenderContext): MetaObject | undefined {
|
|
90
|
+
if (field.subType !== FIELD_SUBTYPE_OBJECT) return undefined;
|
|
91
|
+
const ref = field.attr(FIELD_ATTR_OBJECT_REF);
|
|
92
|
+
if (typeof ref !== "string" || ref.length === 0) return undefined;
|
|
93
|
+
const base = stripPackage(ref);
|
|
94
|
+
return ctx.loadedRoot.objects().find((o) => o.name === base) as MetaObject | undefined;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** camelCase / PascalCase → human label ("llmConfig" → "Llm Config"). */
|
|
98
|
+
function humanize(s: string): string {
|
|
99
|
+
return s
|
|
100
|
+
.replace(/([a-z])([A-Z])/g, "$1 $2")
|
|
101
|
+
.replace(/_/g, " ")
|
|
102
|
+
.replace(/\b\w/g, (c) => c.toUpperCase())
|
|
103
|
+
.trim();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** A real HTML <input type=...> for a scalar field subtype, or undefined (text). */
|
|
107
|
+
function htmlTypeForSubType(subType: string): string | undefined {
|
|
108
|
+
switch (subType) {
|
|
109
|
+
case FIELD_SUBTYPE_INT:
|
|
110
|
+
case FIELD_SUBTYPE_LONG:
|
|
111
|
+
case FIELD_SUBTYPE_DOUBLE:
|
|
112
|
+
case FIELD_SUBTYPE_FLOAT:
|
|
113
|
+
case FIELD_SUBTYPE_DECIMAL:
|
|
114
|
+
case FIELD_SUBTYPE_CURRENCY:
|
|
115
|
+
return "number";
|
|
116
|
+
case FIELD_SUBTYPE_BOOLEAN:
|
|
117
|
+
return "checkbox";
|
|
118
|
+
case FIELD_SUBTYPE_DATE:
|
|
119
|
+
return "date";
|
|
120
|
+
case FIELD_SUBTYPE_TIME:
|
|
121
|
+
return "time";
|
|
122
|
+
case FIELD_SUBTYPE_TIMESTAMP:
|
|
123
|
+
return "datetime-local";
|
|
124
|
+
default:
|
|
125
|
+
return undefined;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** The react-hook-form `register(...)` argument for a nested field path. A
|
|
130
|
+
* static path is a plain string literal; a path threaded through an array
|
|
131
|
+
* element carries a runtime `${index}` and must be a template literal. */
|
|
132
|
+
function registerArg(path: string, dynamic: boolean): string {
|
|
133
|
+
return dynamic ? `\`${path}\`` : JSON.stringify(path);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** A stable, identifier-safe useFieldArray hook variable for a static path. */
|
|
137
|
+
function arrayHookVar(path: string): string {
|
|
138
|
+
return `${path.replace(/[^A-Za-z0-9]+/g, "_")}Array`;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
interface FieldRender {
|
|
142
|
+
jsx: string;
|
|
143
|
+
/** useFieldArray declarations to hoist to the top of the component. */
|
|
144
|
+
hooks: string[];
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Render one NESTED field inside a value-object sub-form. Dispatches on subtype:
|
|
149
|
+
* - a resolvable `field.object` (single) → a nested <fieldset> recursing into
|
|
150
|
+
* the value object's fields;
|
|
151
|
+
* - a resolvable `field.object` array → a useFieldArray repeatable group (when
|
|
152
|
+
* the path is static) or a degraded element-shape fieldset + TODO (when the
|
|
153
|
+
* path is already inside an array element, i.e. dynamic);
|
|
154
|
+
* - anything else → a single <input> bound via a nested react-hook-form path.
|
|
155
|
+
*/
|
|
156
|
+
function renderNestedField(
|
|
157
|
+
field: MetaField,
|
|
158
|
+
path: string,
|
|
159
|
+
dynamic: boolean,
|
|
160
|
+
ctx: RenderContext,
|
|
161
|
+
visited: Set<string>,
|
|
162
|
+
depth: number,
|
|
163
|
+
): FieldRender {
|
|
164
|
+
const vo = resolveValueObject(field, ctx);
|
|
165
|
+
if (vo === undefined) {
|
|
166
|
+
// Scalar / enum / unresolved object → a single bound input.
|
|
167
|
+
const htmlType = htmlTypeForSubType(field.subType);
|
|
168
|
+
const typeAttr = htmlType !== undefined ? ` type=${JSON.stringify(htmlType)}` : "";
|
|
169
|
+
return {
|
|
170
|
+
jsx: `<div className="metaobjects-field" key=${JSON.stringify(path)}>
|
|
171
|
+
<label className="metaobjects-field-label">${humanize(field.name)}</label>
|
|
172
|
+
<input className="metaobjects-field-input"${typeAttr} {...form.register(${registerArg(path, dynamic)})} />
|
|
173
|
+
</div>`,
|
|
174
|
+
hooks: [],
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const voKey = vo.fqn();
|
|
179
|
+
if (visited.has(voKey) || depth >= MAX_VO_DEPTH) {
|
|
180
|
+
// Cycle or too deep — emit a labeled shell + TODO instead of a flat input.
|
|
181
|
+
return {
|
|
182
|
+
jsx: `<fieldset className="metaobjects-fieldset" key=${JSON.stringify(path)}>
|
|
183
|
+
<legend className="metaobjects-fieldset-legend">${humanize(field.name)}</legend>
|
|
184
|
+
{/* TODO(#95): nested value object ${vo.name} not expanded (recursion guard). Author this sub-form by hand. */}
|
|
185
|
+
</fieldset>`,
|
|
186
|
+
hooks: [],
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
const nextVisited = new Set(visited);
|
|
190
|
+
nextVisited.add(voKey);
|
|
191
|
+
|
|
192
|
+
if (!field.isArray) {
|
|
193
|
+
// Single nested value object → a fieldset recursing into its fields.
|
|
194
|
+
const inner: string[] = [];
|
|
195
|
+
const hooks: string[] = [];
|
|
196
|
+
for (const sub of vo.fields()) {
|
|
197
|
+
const r = renderNestedField(sub, `${path}.${sub.name}`, dynamic, ctx, nextVisited, depth + 1);
|
|
198
|
+
inner.push(r.jsx);
|
|
199
|
+
hooks.push(...r.hooks);
|
|
200
|
+
}
|
|
201
|
+
return {
|
|
202
|
+
jsx: `<fieldset className="metaobjects-fieldset" key=${JSON.stringify(path)}>
|
|
203
|
+
<legend className="metaobjects-fieldset-legend">${humanize(field.name)}</legend>
|
|
204
|
+
${inner.join("\n")}
|
|
205
|
+
</fieldset>`,
|
|
206
|
+
hooks,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Array of value objects.
|
|
211
|
+
if (dynamic) {
|
|
212
|
+
// Already inside an array element — a static useFieldArray name is impossible.
|
|
213
|
+
// Degrade to the element shape + a single clear TODO (never a flat input).
|
|
214
|
+
return {
|
|
215
|
+
jsx: `<fieldset className="metaobjects-fieldset" key=${JSON.stringify(path)}>
|
|
216
|
+
<legend className="metaobjects-fieldset-legend">${humanize(field.name)}</legend>
|
|
217
|
+
{/* TODO(#95): arrays of value objects nested inside another array are not auto-wired
|
|
218
|
+
(the react-hook-form path is dynamic). Render this repeatable group by hand. */}
|
|
219
|
+
</fieldset>`,
|
|
220
|
+
hooks: [],
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Static path → a useFieldArray-based repeatable group.
|
|
225
|
+
const hookVar = arrayHookVar(path);
|
|
226
|
+
const elementPath = `${path}.\${index}`;
|
|
227
|
+
const inner: string[] = [];
|
|
228
|
+
const hooks: string[] = [`const ${hookVar} = useFieldArray({ control: form.control, name: ${JSON.stringify(path)} as never });`];
|
|
229
|
+
for (const sub of vo.fields()) {
|
|
230
|
+
const r = renderNestedField(sub, `${elementPath}.${sub.name}`, true, ctx, nextVisited, depth + 1);
|
|
231
|
+
inner.push(r.jsx);
|
|
232
|
+
hooks.push(...r.hooks);
|
|
233
|
+
}
|
|
234
|
+
const label = humanize(field.name);
|
|
235
|
+
const itemLabel = humanize(vo.name);
|
|
236
|
+
return {
|
|
237
|
+
jsx: `<div className="metaobjects-field-array" key=${JSON.stringify(path)}>
|
|
238
|
+
<span className="metaobjects-field-array-label">${label}</span>
|
|
239
|
+
{${hookVar}.fields.map((item, index) => (
|
|
240
|
+
<fieldset className="metaobjects-fieldset" key={item.id}>
|
|
241
|
+
<legend className="metaobjects-fieldset-legend">${itemLabel} {index + 1}</legend>
|
|
242
|
+
${inner.join("\n")}
|
|
243
|
+
<button type="button" className="metaobjects-field-array-remove" onClick={() => ${hookVar}.remove(index)}>
|
|
244
|
+
Remove
|
|
245
|
+
</button>
|
|
246
|
+
</fieldset>
|
|
247
|
+
))}
|
|
248
|
+
<button type="button" className="metaobjects-field-array-add" onClick={() => ${hookVar}.append({} as never)}>
|
|
249
|
+
Add ${itemLabel}
|
|
250
|
+
</button>
|
|
251
|
+
</div>`,
|
|
252
|
+
hooks,
|
|
253
|
+
};
|
|
64
254
|
}
|
|
65
255
|
|
|
66
256
|
export function renderFormFile(entity: MetaObject, ctx: RenderContext): string {
|
|
@@ -88,11 +278,9 @@ export function renderFormFile(entity: MetaObject, ctx: RenderContext): string {
|
|
|
88
278
|
const SubmitHandlerSym = imp("t:SubmitHandler@react-hook-form");
|
|
89
279
|
const useEntityFormSym = imp("useEntityForm@@metaobjectsdev/react");
|
|
90
280
|
|
|
91
|
-
//
|
|
92
|
-
//
|
|
93
|
-
const
|
|
94
|
-
.map(
|
|
95
|
-
(f) => ` <div className="metaobjects-field" key=${JSON.stringify(f)}>
|
|
281
|
+
// The flat scalar block: a label + pre-bound input + error span, driven
|
|
282
|
+
// entirely by the entity constants via `form.input.<field>`. Unchanged.
|
|
283
|
+
const scalarBlock = (f: string) => ` <div className="metaobjects-field" key=${JSON.stringify(f)}>
|
|
96
284
|
<label className="metaobjects-field-label" htmlFor={${entityName}.${f}.name}>
|
|
97
285
|
{${entityName}.${f}.label}
|
|
98
286
|
</label>
|
|
@@ -102,9 +290,30 @@ export function renderFormFile(entity: MetaObject, ctx: RenderContext): string {
|
|
|
102
290
|
{String(form.formState.errors.${f}?.message ?? '')}
|
|
103
291
|
</span>
|
|
104
292
|
)}
|
|
105
|
-
</div
|
|
106
|
-
|
|
107
|
-
|
|
293
|
+
</div>`;
|
|
294
|
+
|
|
295
|
+
// For each visible field: scalars keep the flat `form.input.<field>` block; a
|
|
296
|
+
// `field.object` with a resolvable `@objectRef` recurses into the referenced
|
|
297
|
+
// value object as a nested <fieldset> sub-form (issue #95). useFieldArray
|
|
298
|
+
// declarations for array-of-value-object fields hoist to the component top.
|
|
299
|
+
const blocks: string[] = [];
|
|
300
|
+
const fieldArrayHooks: string[] = [];
|
|
301
|
+
for (const f of fields) {
|
|
302
|
+
if (resolveValueObject(f, ctx) === undefined) {
|
|
303
|
+
blocks.push(scalarBlock(f.name));
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
const r = renderNestedField(f, f.name, false, ctx, new Set<string>(), 0);
|
|
307
|
+
blocks.push(r.jsx);
|
|
308
|
+
fieldArrayHooks.push(...r.hooks);
|
|
309
|
+
}
|
|
310
|
+
const fieldBlocks = blocks.join("\n");
|
|
311
|
+
const hookSection =
|
|
312
|
+
fieldArrayHooks.length > 0 ? `\n ${fieldArrayHooks.join("\n ")}` : "";
|
|
313
|
+
|
|
314
|
+
// Only pull in useFieldArray when an array-of-value-object field needs it.
|
|
315
|
+
const useFieldArrayImport =
|
|
316
|
+
fieldArrayHooks.length > 0 ? `import { useFieldArray } from "react-hook-form";\n` : "";
|
|
108
317
|
|
|
109
318
|
const literalImports = code`
|
|
110
319
|
import {
|
|
@@ -112,7 +321,7 @@ import {
|
|
|
112
321
|
${entityName}InsertSchema,
|
|
113
322
|
} from ${JSON.stringify(entityFileSpec)};
|
|
114
323
|
import type { ${entityName} as ${entityName}Row } from ${JSON.stringify(entityFileSpec)};
|
|
115
|
-
`;
|
|
324
|
+
${useFieldArrayImport}`;
|
|
116
325
|
|
|
117
326
|
const body = code`
|
|
118
327
|
export interface ${entityName}FormProps {
|
|
@@ -137,7 +346,7 @@ export function ${entityName}Form(props: ${entityName}FormProps): ${ReactElement
|
|
|
137
346
|
${entityName},
|
|
138
347
|
${formSchema},
|
|
139
348
|
props.defaultValues !== undefined ? { defaultValues: props.defaultValues } : {},
|
|
140
|
-
)
|
|
349
|
+
);${hookSection}
|
|
141
350
|
return (
|
|
142
351
|
<form
|
|
143
352
|
className={props.className ?? 'metaobjects-form'}
|