@specverse/engines 4.1.28 → 4.2.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.
- package/assets/examples/manifests/frontend-only.yaml +3 -6
- package/assets/examples/manifests/fullstack-app.yaml +5 -7
- package/assets/examples/manifests/fullstack-monorepo.yaml +3 -6
- package/dist/inference/comprehensive-engine.d.ts.map +1 -1
- package/dist/inference/comprehensive-engine.js +3 -19
- package/dist/inference/comprehensive-engine.js.map +1 -1
- package/dist/inference/core/rule-engine.d.ts +31 -0
- package/dist/inference/core/rule-engine.d.ts.map +1 -1
- package/dist/inference/core/rule-engine.js +117 -33
- package/dist/inference/core/rule-engine.js.map +1 -1
- package/dist/inference/core/rule-file-types.d.ts +0 -2
- package/dist/inference/core/rule-file-types.d.ts.map +1 -1
- package/dist/inference/core/rule-file-types.js +3 -6
- package/dist/inference/core/rule-file-types.js.map +1 -1
- package/dist/inference/core/rule-loader.d.ts +5 -15
- package/dist/inference/core/rule-loader.d.ts.map +1 -1
- package/dist/inference/core/rule-loader.js +43 -132
- package/dist/inference/core/rule-loader.js.map +1 -1
- package/dist/inference/core/types.d.ts +0 -6
- package/dist/inference/core/types.d.ts.map +1 -1
- package/dist/inference/core/types.js +0 -4
- package/dist/inference/core/types.js.map +1 -1
- package/dist/inference/logical/generators/component-type-resolver.d.ts +0 -26
- package/dist/inference/logical/generators/component-type-resolver.d.ts.map +1 -1
- package/dist/inference/logical/generators/component-type-resolver.js +0 -19
- package/dist/inference/logical/generators/component-type-resolver.js.map +1 -1
- package/dist/inference/logical/generators/specialist-view-expander.d.ts +1 -17
- package/dist/inference/logical/generators/specialist-view-expander.d.ts.map +1 -1
- package/dist/inference/logical/generators/specialist-view-expander.js +0 -15
- package/dist/inference/logical/generators/specialist-view-expander.js.map +1 -1
- package/dist/inference/logical/generators/view-generator.d.ts +4 -14
- package/dist/inference/logical/generators/view-generator.d.ts.map +1 -1
- package/dist/inference/logical/generators/view-generator.js +6 -26
- package/dist/inference/logical/generators/view-generator.js.map +1 -1
- package/dist/inference/logical/index.d.ts +2 -2
- package/dist/inference/logical/index.d.ts.map +1 -1
- package/dist/inference/logical/logical-engine.d.ts.map +1 -1
- package/dist/inference/logical/logical-engine.js +17 -80
- package/dist/inference/logical/logical-engine.js.map +1 -1
- package/dist/inference/quint-transpiler.d.ts +5 -3
- package/dist/inference/quint-transpiler.d.ts.map +1 -1
- package/dist/inference/quint-transpiler.js +11 -6
- package/dist/inference/quint-transpiler.js.map +1 -1
- package/dist/libs/instance-factories/applications/templates/generic/main-generator.js +3 -3
- package/dist/libs/instance-factories/applications/templates/react/api-client-generator.js +16 -6
- package/dist/libs/instance-factories/applications/templates/react-starter/app-tsx-generator.js +110 -0
- package/dist/libs/instance-factories/applications/templates/react-starter/dashboard-body-composer.js +121 -0
- package/dist/libs/instance-factories/applications/templates/react-starter/detail-body-composer.js +78 -0
- package/dist/libs/instance-factories/applications/templates/react-starter/form-body-composer.js +190 -0
- package/dist/libs/instance-factories/applications/templates/react-starter/helpers-emitter.js +45 -0
- package/dist/libs/instance-factories/applications/templates/react-starter/html-to-jsx.js +192 -0
- package/dist/libs/instance-factories/applications/templates/react-starter/list-body-composer.js +46 -0
- package/dist/libs/instance-factories/applications/templates/react-starter/orchestrator.js +30 -0
- package/dist/libs/instance-factories/applications/templates/react-starter/package-json-generator.js +38 -0
- package/dist/libs/instance-factories/applications/templates/react-starter/regen-safety.js +89 -0
- package/dist/libs/instance-factories/applications/templates/react-starter/view-emitter.js +56 -0
- package/dist/libs/instance-factories/applications/templates/react-starter/views-generator.js +66 -0
- package/dist/libs/instance-factories/cli/templates/commander/command-generator.js +14 -11
- package/dist/libs/instance-factories/controllers/templates/fastify/server-generator.js +11 -3
- package/dist/libs/instance-factories/services/templates/prisma/controller-generator.js +27 -17
- package/dist/libs/instance-factories/shared/path-resolver.js +1 -1
- package/dist/realize/index.d.ts.map +1 -1
- package/dist/realize/index.js +15 -22
- package/dist/realize/index.js.map +1 -1
- package/dist/registry/utils/manifest-adapter.d.ts +8 -1
- package/dist/registry/utils/manifest-adapter.d.ts.map +1 -1
- package/dist/registry/utils/manifest-adapter.js +8 -1
- package/dist/registry/utils/manifest-adapter.js.map +1 -1
- package/libs/instance-factories/applications/react-app-starter.yaml +150 -0
- package/libs/instance-factories/applications/templates/generic/main-generator.ts +3 -3
- package/libs/instance-factories/applications/templates/react/api-client-generator.ts +16 -6
- package/libs/instance-factories/applications/templates/react-starter/README.md +211 -0
- package/libs/instance-factories/applications/templates/react-starter/__tests__/dashboard-body-composer.test.ts +153 -0
- package/libs/instance-factories/applications/templates/react-starter/__tests__/detail-body-composer.test.ts +145 -0
- package/libs/instance-factories/applications/templates/react-starter/__tests__/form-body-composer.test.ts +175 -0
- package/libs/instance-factories/applications/templates/react-starter/__tests__/helpers-emitter.test.ts +55 -0
- package/libs/instance-factories/applications/templates/react-starter/__tests__/html-to-jsx.test.ts +140 -0
- package/libs/instance-factories/applications/templates/react-starter/__tests__/list-body-composer.test.ts +146 -0
- package/libs/instance-factories/applications/templates/react-starter/__tests__/orchestrator.test.ts +163 -0
- package/libs/instance-factories/applications/templates/react-starter/__tests__/parity-p2-factory-imports.test.ts +116 -0
- package/libs/instance-factories/applications/templates/react-starter/__tests__/parity-p3-rendered-output.test.ts +183 -0
- package/libs/instance-factories/applications/templates/react-starter/__tests__/regen-safety.test.ts +144 -0
- package/libs/instance-factories/applications/templates/react-starter/__tests__/starter-generators.test.ts +114 -0
- package/libs/instance-factories/applications/templates/react-starter/__tests__/view-emitter.test.ts +107 -0
- package/libs/instance-factories/applications/templates/react-starter/__tests__/views-generator.test.ts +139 -0
- package/libs/instance-factories/applications/templates/react-starter/app-tsx-generator.ts +141 -0
- package/libs/instance-factories/applications/templates/react-starter/dashboard-body-composer.ts +174 -0
- package/libs/instance-factories/applications/templates/react-starter/detail-body-composer.ts +135 -0
- package/libs/instance-factories/applications/templates/react-starter/form-body-composer.ts +306 -0
- package/libs/instance-factories/applications/templates/react-starter/helpers-emitter.ts +60 -0
- package/libs/instance-factories/applications/templates/react-starter/html-to-jsx.ts +334 -0
- package/libs/instance-factories/applications/templates/react-starter/list-body-composer.ts +120 -0
- package/libs/instance-factories/applications/templates/react-starter/orchestrator.ts +80 -0
- package/libs/instance-factories/applications/templates/react-starter/package-json-generator.ts +57 -0
- package/libs/instance-factories/applications/templates/react-starter/regen-safety.ts +157 -0
- package/libs/instance-factories/applications/templates/react-starter/skeletons/dashboard.tsx.template +47 -0
- package/libs/instance-factories/applications/templates/react-starter/skeletons/detail.tsx.template +94 -0
- package/libs/instance-factories/applications/templates/react-starter/skeletons/form.tsx.template +114 -0
- package/libs/instance-factories/applications/templates/react-starter/skeletons/list.tsx.template +72 -0
- package/libs/instance-factories/applications/templates/react-starter/view-emitter.ts +151 -0
- package/libs/instance-factories/applications/templates/react-starter/views-generator.ts +137 -0
- package/libs/instance-factories/cli/templates/commander/command-generator.ts +14 -11
- package/libs/instance-factories/controllers/templates/fastify/server-generator.ts +11 -3
- package/libs/instance-factories/services/templates/prisma/controller-generator.ts +27 -17
- package/libs/instance-factories/shared/path-resolver.ts +8 -2
- package/package.json +3 -3
- package/dist/libs/instance-factories/applications/templates/react/_view-components-source.js +0 -530
- package/dist/libs/instance-factories/applications/templates/react/app-tsx-generator.js +0 -73
- package/dist/libs/instance-factories/applications/templates/react/field-helpers-generator.js +0 -99
- package/dist/libs/instance-factories/applications/templates/react/package-json-generator.js +0 -49
- package/dist/libs/instance-factories/applications/templates/react/pattern-adapter-generator.js +0 -156
- package/dist/libs/instance-factories/applications/templates/react/react-pattern-adapter.js +0 -935
- package/dist/libs/instance-factories/applications/templates/react/relationship-field-generator.js +0 -143
- package/dist/libs/instance-factories/applications/templates/react/tailwind-adapter-generator.js +0 -646
- package/dist/libs/instance-factories/applications/templates/react/tailwind-adapter-wrapper-generator.js +0 -65
- package/dist/libs/instance-factories/applications/templates/react/view-dashboard-generator.js +0 -143
- package/dist/libs/instance-factories/applications/templates/react/view-detail-generator.js +0 -143
- package/dist/libs/instance-factories/applications/templates/react/view-form-generator.js +0 -355
- package/dist/libs/instance-factories/applications/templates/react/view-list-generator.js +0 -91
- package/dist/libs/instance-factories/applications/templates/react/view-router-generator.js +0 -79
- package/dist/libs/instance-factories/tools/templates/vscode/vscode-extension-generator.js.bak +0 -244
- package/dist/libs/instance-factories/views/index.js +0 -48
- package/dist/libs/instance-factories/views/templates/react/adapters/antd-adapter.js +0 -742
- package/dist/libs/instance-factories/views/templates/react/adapters/mui-adapter.js +0 -824
- package/dist/libs/instance-factories/views/templates/react/adapters/shadcn-adapter.js +0 -719
- package/dist/libs/instance-factories/views/templates/react/app-generator.js +0 -45
- package/dist/libs/instance-factories/views/templates/react/components-generator.js +0 -820
- package/dist/libs/instance-factories/views/templates/react/forms-generator.js +0 -275
- package/dist/libs/instance-factories/views/templates/react/frontend-package-json-generator.js +0 -46
- package/dist/libs/instance-factories/views/templates/react/hooks-generator.js +0 -81
- package/dist/libs/instance-factories/views/templates/react/index-css-generator.js +0 -9
- package/dist/libs/instance-factories/views/templates/react/index-html-generator.js +0 -23
- package/dist/libs/instance-factories/views/templates/react/main-tsx-generator.js +0 -21
- package/dist/libs/instance-factories/views/templates/react/react-component-generator.js +0 -299
- package/dist/libs/instance-factories/views/templates/react/router-generator.js +0 -136
- package/dist/libs/instance-factories/views/templates/react/router-generic-generator.js +0 -107
- package/dist/libs/instance-factories/views/templates/react/shared-utils-generator.js +0 -187
- package/dist/libs/instance-factories/views/templates/react/spec-json-generator.js +0 -7
- package/dist/libs/instance-factories/views/templates/react/types-generator.js +0 -56
- package/dist/libs/instance-factories/views/templates/react/views-metadata-generator.js +0 -27
- package/dist/libs/instance-factories/views/templates/react/vite-config-generator.js +0 -29
- package/dist/libs/instance-factories/views/templates/runtime/runtime-view-renderer.js +0 -261
- package/dist/libs/instance-factories/views/templates/shared/adapter-types.js +0 -34
- package/dist/libs/instance-factories/views/templates/shared/atomic-components-registry.js +0 -800
- package/dist/libs/instance-factories/views/templates/shared/base-generator.js +0 -305
- package/dist/libs/instance-factories/views/templates/shared/component-metadata.js +0 -517
- package/dist/libs/instance-factories/views/templates/shared/composite-pattern-types.js +0 -0
- package/dist/libs/instance-factories/views/templates/shared/composite-patterns.js +0 -445
- package/dist/libs/instance-factories/views/templates/shared/index.js +0 -80
- package/dist/libs/instance-factories/views/templates/shared/pattern-validator.js +0 -210
- package/dist/libs/instance-factories/views/templates/shared/property-mapper.js +0 -492
- package/dist/libs/instance-factories/views/templates/shared/syntax-mapper.js +0 -321
- package/dist/realize/index.js.bak +0 -758
- package/libs/instance-factories/applications/react-app.yaml +0 -186
- package/libs/instance-factories/applications/templates/react/_view-components-source.ts +0 -555
- package/libs/instance-factories/applications/templates/react/app-tsx-generator.ts +0 -94
- package/libs/instance-factories/applications/templates/react/field-helpers-generator.ts +0 -106
- package/libs/instance-factories/applications/templates/react/package-json-generator.ts +0 -57
- package/libs/instance-factories/applications/templates/react/pattern-adapter-generator.ts +0 -179
- package/libs/instance-factories/applications/templates/react/react-pattern-adapter.tsx +0 -1347
- package/libs/instance-factories/applications/templates/react/relationship-field-generator.ts +0 -150
- package/libs/instance-factories/applications/templates/react/tailwind-adapter-generator.ts +0 -704
- package/libs/instance-factories/applications/templates/react/tailwind-adapter-wrapper-generator.ts +0 -84
- package/libs/instance-factories/applications/templates/react/view-dashboard-generator.ts +0 -150
- package/libs/instance-factories/applications/templates/react/view-detail-generator.ts +0 -150
- package/libs/instance-factories/applications/templates/react/view-form-generator.ts +0 -362
- package/libs/instance-factories/applications/templates/react/view-list-generator.ts +0 -98
- package/libs/instance-factories/applications/templates/react/view-router-generator.ts +0 -89
- package/libs/instance-factories/views/README.md +0 -62
- package/libs/instance-factories/views/index.d.ts +0 -13
- package/libs/instance-factories/views/index.d.ts.map +0 -1
- package/libs/instance-factories/views/index.js +0 -18
- package/libs/instance-factories/views/index.js.map +0 -1
- package/libs/instance-factories/views/index.ts +0 -45
- package/libs/instance-factories/views/react-components.yaml +0 -129
- package/libs/instance-factories/views/templates/ARCHITECTURE.md +0 -198
- package/libs/instance-factories/views/templates/react/adapters/antd-adapter.ts +0 -869
- package/libs/instance-factories/views/templates/react/adapters/mui-adapter.ts +0 -953
- package/libs/instance-factories/views/templates/react/adapters/shadcn-adapter.ts +0 -806
- package/libs/instance-factories/views/templates/react/app-generator.ts +0 -55
- package/libs/instance-factories/views/templates/react/components-generator.ts +0 -938
- package/libs/instance-factories/views/templates/react/forms-generator.ts +0 -325
- package/libs/instance-factories/views/templates/react/frontend-package-json-generator.ts +0 -57
- package/libs/instance-factories/views/templates/react/hooks-generator.ts +0 -106
- package/libs/instance-factories/views/templates/react/index-css-generator.ts +0 -14
- package/libs/instance-factories/views/templates/react/index-html-generator.ts +0 -34
- package/libs/instance-factories/views/templates/react/main-tsx-generator.ts +0 -29
- package/libs/instance-factories/views/templates/react/react-component-generator.d.ts +0 -152
- package/libs/instance-factories/views/templates/react/react-component-generator.d.ts.map +0 -1
- package/libs/instance-factories/views/templates/react/react-component-generator.js +0 -398
- package/libs/instance-factories/views/templates/react/react-component-generator.js.map +0 -1
- package/libs/instance-factories/views/templates/react/react-component-generator.ts +0 -533
- package/libs/instance-factories/views/templates/react/router-generator.ts +0 -197
- package/libs/instance-factories/views/templates/react/router-generic-generator.ts +0 -132
- package/libs/instance-factories/views/templates/react/shared-utils-generator.ts +0 -196
- package/libs/instance-factories/views/templates/react/spec-json-generator.ts +0 -17
- package/libs/instance-factories/views/templates/react/types-generator.ts +0 -76
- package/libs/instance-factories/views/templates/react/views-metadata-generator.ts +0 -42
- package/libs/instance-factories/views/templates/react/vite-config-generator.ts +0 -38
- package/libs/instance-factories/views/templates/runtime/runtime-view-renderer.d.ts.map +0 -1
- package/libs/instance-factories/views/templates/runtime/runtime-view-renderer.js.map +0 -1
- package/libs/instance-factories/views/templates/runtime/runtime-view-renderer.ts +0 -474
- package/libs/instance-factories/views/templates/shared/__tests__/composite-patterns.test.ts +0 -242
- package/libs/instance-factories/views/templates/shared/adapter-types.d.ts +0 -77
- package/libs/instance-factories/views/templates/shared/adapter-types.d.ts.map +0 -1
- package/libs/instance-factories/views/templates/shared/adapter-types.js +0 -47
- package/libs/instance-factories/views/templates/shared/adapter-types.js.map +0 -1
- package/libs/instance-factories/views/templates/shared/adapter-types.ts +0 -142
- package/libs/instance-factories/views/templates/shared/atomic-components-registry.d.ts +0 -63
- package/libs/instance-factories/views/templates/shared/atomic-components-registry.d.ts.map +0 -1
- package/libs/instance-factories/views/templates/shared/atomic-components-registry.js +0 -822
- package/libs/instance-factories/views/templates/shared/atomic-components-registry.js.map +0 -1
- package/libs/instance-factories/views/templates/shared/atomic-components-registry.ts +0 -908
- package/libs/instance-factories/views/templates/shared/base-generator.d.ts +0 -247
- package/libs/instance-factories/views/templates/shared/base-generator.d.ts.map +0 -1
- package/libs/instance-factories/views/templates/shared/base-generator.js +0 -363
- package/libs/instance-factories/views/templates/shared/base-generator.js.map +0 -1
- package/libs/instance-factories/views/templates/shared/base-generator.ts +0 -608
- package/libs/instance-factories/views/templates/shared/component-metadata.d.ts +0 -254
- package/libs/instance-factories/views/templates/shared/component-metadata.d.ts.map +0 -1
- package/libs/instance-factories/views/templates/shared/component-metadata.js +0 -602
- package/libs/instance-factories/views/templates/shared/component-metadata.js.map +0 -1
- package/libs/instance-factories/views/templates/shared/component-metadata.ts +0 -803
- package/libs/instance-factories/views/templates/shared/composite-pattern-types.ts +0 -250
- package/libs/instance-factories/views/templates/shared/composite-patterns.ts +0 -535
- package/libs/instance-factories/views/templates/shared/index.ts +0 -68
- package/libs/instance-factories/views/templates/shared/pattern-validator.ts +0 -279
- package/libs/instance-factories/views/templates/shared/property-mapper.d.ts +0 -149
- package/libs/instance-factories/views/templates/shared/property-mapper.d.ts.map +0 -1
- package/libs/instance-factories/views/templates/shared/property-mapper.js +0 -580
- package/libs/instance-factories/views/templates/shared/property-mapper.js.map +0 -1
- package/libs/instance-factories/views/templates/shared/property-mapper.ts +0 -700
- package/libs/instance-factories/views/templates/shared/syntax-mapper.d.ts +0 -143
- package/libs/instance-factories/views/templates/shared/syntax-mapper.d.ts.map +0 -1
- package/libs/instance-factories/views/templates/shared/syntax-mapper.js +0 -420
- package/libs/instance-factories/views/templates/shared/syntax-mapper.js.map +0 -1
- package/libs/instance-factories/views/templates/shared/syntax-mapper.ts +0 -539
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detail-view body composer for ReactAppStarter
|
|
3
|
+
*
|
|
4
|
+
* Renders the interior of a detail view as a JSX-safe definition list:
|
|
5
|
+
* business fields first (prominent), metadata fields second (muted).
|
|
6
|
+
* The outer card + actions live in `skeletons/detail.tsx.template`.
|
|
7
|
+
*
|
|
8
|
+
* Unlike list view, the interior here is mostly label→value pairs, not
|
|
9
|
+
* a table. We use `<dl>` / `<dt>` / `<dd>` directly rather than routing
|
|
10
|
+
* through a specific atomic component — the field-list shape isn't one
|
|
11
|
+
* of the adapter's atomic components, and forcing it through a `card`
|
|
12
|
+
* adapter would add structure without clarifying anything in the
|
|
13
|
+
* generated code.
|
|
14
|
+
*
|
|
15
|
+
* BelongsTo relationships: today we emit the raw FK id. A future pass
|
|
16
|
+
* will resolve FK → entity display name (matches runtime's
|
|
17
|
+
* getEntityDisplayName). Marked with a TODO comment in the output so
|
|
18
|
+
* users inspecting the generated file see the hook.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { METADATA_FIELDS } from '@specverse/runtime/views/core';
|
|
22
|
+
import type { EmitContext, ModelSpec } from './view-emitter.js';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Metadata attribute names rendered in a muted style. Sourced from
|
|
26
|
+
* the canonical pattern library so both this composer and the rest
|
|
27
|
+
* of the system stay in sync automatically.
|
|
28
|
+
*/
|
|
29
|
+
const METADATA_FIELD_NAMES = new Set(METADATA_FIELDS);
|
|
30
|
+
|
|
31
|
+
export function composeDetailBody(context: EmitContext): string {
|
|
32
|
+
const { business, metadata } = partitionFields(context.model);
|
|
33
|
+
const belongsTo = extractBelongsTo(context.model);
|
|
34
|
+
|
|
35
|
+
const lines: string[] = [];
|
|
36
|
+
lines.push('<div className="rounded-lg border border-gray-200 bg-white p-6 dark:border-gray-700 dark:bg-gray-900">');
|
|
37
|
+
|
|
38
|
+
if (business.length > 0) {
|
|
39
|
+
lines.push(' <dl className="grid grid-cols-1 gap-4 sm:grid-cols-2">');
|
|
40
|
+
for (const field of business) {
|
|
41
|
+
lines.push(...renderField(field, { muted: false }));
|
|
42
|
+
}
|
|
43
|
+
lines.push(' </dl>');
|
|
44
|
+
} else {
|
|
45
|
+
lines.push(' <p className="text-sm text-gray-400">No business fields defined for this model.</p>');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (belongsTo.length > 0) {
|
|
49
|
+
lines.push('');
|
|
50
|
+
lines.push(' {/* TODO: resolve FK ids → related entity display names.');
|
|
51
|
+
lines.push(' See @specverse/runtime/views/core/entity-display for the');
|
|
52
|
+
lines.push(' canonical resolver, or load the related record in a hook. */}');
|
|
53
|
+
lines.push(' <dl className="mt-6 grid grid-cols-1 gap-4 sm:grid-cols-2 border-t border-gray-200 dark:border-gray-700 pt-4">');
|
|
54
|
+
for (const rel of belongsTo) {
|
|
55
|
+
lines.push(...renderField({ name: `${rel}Id`, label: humanize(rel) }, { muted: false }));
|
|
56
|
+
}
|
|
57
|
+
lines.push(' </dl>');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (metadata.length > 0) {
|
|
61
|
+
lines.push('');
|
|
62
|
+
lines.push(' <dl className="mt-6 grid grid-cols-1 gap-2 sm:grid-cols-2 border-t border-gray-200 dark:border-gray-700 pt-4 text-xs text-gray-500 dark:text-gray-400">');
|
|
63
|
+
for (const field of metadata) {
|
|
64
|
+
lines.push(...renderField(field, { muted: true }));
|
|
65
|
+
}
|
|
66
|
+
lines.push(' </dl>');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
lines.push('</div>');
|
|
70
|
+
return lines.join('\n');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
interface Field {
|
|
74
|
+
name: string;
|
|
75
|
+
/** Display label; defaults to humanize(name). */
|
|
76
|
+
label?: string;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function partitionFields(model: ModelSpec): { business: Field[]; metadata: Field[] } {
|
|
80
|
+
const attrs = Object.keys(model.attributes ?? {});
|
|
81
|
+
const business: Field[] = [];
|
|
82
|
+
const metadata: Field[] = [];
|
|
83
|
+
for (const name of attrs) {
|
|
84
|
+
const field = { name, label: humanize(name) };
|
|
85
|
+
if (METADATA_FIELD_NAMES.has(name)) {
|
|
86
|
+
metadata.push(field);
|
|
87
|
+
} else {
|
|
88
|
+
business.push(field);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return { business, metadata };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Extract belongsTo relationship names from the model. Returns e.g.
|
|
96
|
+
* `['author']` for a Post that `belongsTo Author`. The skeleton's
|
|
97
|
+
* item already has `authorId` as a scalar field — we surface it
|
|
98
|
+
* explicitly in its own section so the generated UI separates "this
|
|
99
|
+
* entity's data" from "related entities."
|
|
100
|
+
*/
|
|
101
|
+
function extractBelongsTo(model: ModelSpec): string[] {
|
|
102
|
+
const rels = model.relationships ?? {};
|
|
103
|
+
const out: string[] = [];
|
|
104
|
+
for (const [name, def] of Object.entries(rels)) {
|
|
105
|
+
const d = def as { type?: string };
|
|
106
|
+
if (d?.type === 'belongsTo') out.push(name);
|
|
107
|
+
}
|
|
108
|
+
return out;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function renderField(field: Field, opts: { muted: boolean }): string[] {
|
|
112
|
+
const label = field.label ?? humanize(field.name);
|
|
113
|
+
const labelCls = opts.muted
|
|
114
|
+
? 'font-medium uppercase tracking-wide text-gray-400 dark:text-gray-500'
|
|
115
|
+
: 'text-sm font-medium text-gray-500 dark:text-gray-400';
|
|
116
|
+
const valueCls = opts.muted
|
|
117
|
+
? 'text-gray-500 dark:text-gray-400'
|
|
118
|
+
: 'mt-1 text-sm text-gray-900 dark:text-gray-100 break-words';
|
|
119
|
+
|
|
120
|
+
return [
|
|
121
|
+
' <div>',
|
|
122
|
+
` <dt className="${labelCls}">${label}</dt>`,
|
|
123
|
+
` <dd className="${valueCls}">` +
|
|
124
|
+
`{String((item as any).${field.name} ?? '')}` +
|
|
125
|
+
'</dd>',
|
|
126
|
+
' </div>',
|
|
127
|
+
];
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function humanize(name: string): string {
|
|
131
|
+
return name
|
|
132
|
+
.replace(/([A-Z])/g, ' $1')
|
|
133
|
+
.replace(/^./, c => c.toUpperCase())
|
|
134
|
+
.trim();
|
|
135
|
+
}
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Form-view body composer for ReactAppStarter
|
|
3
|
+
*
|
|
4
|
+
* Emits one <label>+<input> pair per editable attribute of the model.
|
|
5
|
+
* Input type is inferred from the attribute's type + constraints
|
|
6
|
+
* (mirrors the canonical mapping in
|
|
7
|
+
* `entities/src/core/views/inference/component-mappings.json`, minus
|
|
8
|
+
* the atomic-component indirection).
|
|
9
|
+
*
|
|
10
|
+
* Auto-generated fields (id, timestamps, `auto=...` markers) are
|
|
11
|
+
* omitted — the backend assigns them.
|
|
12
|
+
*
|
|
13
|
+
* Relationship fields (belongsTo) are emitted as plain-text inputs
|
|
14
|
+
* for the FK column with a TODO comment. Wiring them to a proper
|
|
15
|
+
* dropdown requires a hook that loads the related list at form render
|
|
16
|
+
* time; deferred to a later pass. The inline TODO is the hook for
|
|
17
|
+
* users who want to upgrade.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { METADATA_FIELDS } from '@specverse/runtime/views/core';
|
|
21
|
+
import type { EmitContext, ModelSpec } from './view-emitter.js';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Attribute names the backend always generates — never render as
|
|
25
|
+
* inputs. Sourced from the pattern library's metadata set; any
|
|
26
|
+
* attribute with `auto=...` or category=metadata is additionally
|
|
27
|
+
* skipped via per-field checks in `selectFormFields` below.
|
|
28
|
+
*/
|
|
29
|
+
const AUTO_GENERATED_FIELD_NAMES = new Set(METADATA_FIELDS);
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Compose the form body as JSX-safe source. Dropped at `{{BODY}}`.
|
|
33
|
+
*/
|
|
34
|
+
export function composeFormBody(context: EmitContext): string {
|
|
35
|
+
const belongsTo = extractBelongsTo(context.model);
|
|
36
|
+
// FK columns shadowed by a belongsTo relationship are emitted in the
|
|
37
|
+
// belongsTo section with a nicer label ("Author" instead of
|
|
38
|
+
// "Author Id") — skip them in the main loop so we don't render twice.
|
|
39
|
+
const shadowedFKs = new Set(belongsTo.map(rel => `${rel}Id`));
|
|
40
|
+
const fields = selectFormFields(context.model, shadowedFKs);
|
|
41
|
+
|
|
42
|
+
const lines: string[] = [];
|
|
43
|
+
lines.push('<div className="space-y-4">');
|
|
44
|
+
|
|
45
|
+
if (fields.length === 0 && belongsTo.length === 0) {
|
|
46
|
+
lines.push(' <p className="text-sm text-gray-400">No editable fields for this model.</p>');
|
|
47
|
+
lines.push('</div>');
|
|
48
|
+
return lines.join('\n');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
for (const field of fields) {
|
|
52
|
+
lines.push(...renderInput(field));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (belongsTo.length > 0) {
|
|
56
|
+
lines.push('');
|
|
57
|
+
lines.push(' {/* TODO: swap these text inputs for <select>s that load the related');
|
|
58
|
+
lines.push(' entities. See useEntitiesQuery(TargetModel) in the runtime hooks. */}');
|
|
59
|
+
for (const rel of belongsTo) {
|
|
60
|
+
lines.push(...renderInput({
|
|
61
|
+
name: `${rel}Id`,
|
|
62
|
+
label: humanize(rel),
|
|
63
|
+
type: 'String',
|
|
64
|
+
required: true,
|
|
65
|
+
}));
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
lines.push('</div>');
|
|
70
|
+
return lines.join('\n');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
74
|
+
// Field selection
|
|
75
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
76
|
+
|
|
77
|
+
interface FieldInfo {
|
|
78
|
+
name: string;
|
|
79
|
+
label: string;
|
|
80
|
+
type: string;
|
|
81
|
+
required: boolean;
|
|
82
|
+
values?: string[]; // enum options if present
|
|
83
|
+
isLongString?: boolean;
|
|
84
|
+
auto?: boolean;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function selectFormFields(
|
|
88
|
+
model: ModelSpec,
|
|
89
|
+
skip: Set<string> = new Set()
|
|
90
|
+
): FieldInfo[] {
|
|
91
|
+
const out: FieldInfo[] = [];
|
|
92
|
+
const attrs = model.attributes ?? {};
|
|
93
|
+
for (const [name, rawDef] of Object.entries(attrs)) {
|
|
94
|
+
if (AUTO_GENERATED_FIELD_NAMES.has(name)) continue;
|
|
95
|
+
if (skip.has(name)) continue;
|
|
96
|
+
|
|
97
|
+
const def = rawDef as AttributeShape;
|
|
98
|
+
if (def?.auto) continue;
|
|
99
|
+
|
|
100
|
+
// Heuristic: parse a type like "String required unique" to detect
|
|
101
|
+
// convention-string shapes. We prefer the structured fields if
|
|
102
|
+
// they're present.
|
|
103
|
+
const type = (def?.type ?? parseTypeFromConvention(def)) ?? 'String';
|
|
104
|
+
const required = def?.required === true || hasConventionFlag(def, 'required');
|
|
105
|
+
const values = def?.values as string[] | undefined;
|
|
106
|
+
const maxLength = def?.maxLength ?? def?.max as number | undefined;
|
|
107
|
+
const isLongString =
|
|
108
|
+
typeof maxLength === 'number' && maxLength > 100;
|
|
109
|
+
|
|
110
|
+
out.push({
|
|
111
|
+
name,
|
|
112
|
+
label: humanize(name),
|
|
113
|
+
type: normalizeType(type),
|
|
114
|
+
required,
|
|
115
|
+
values,
|
|
116
|
+
isLongString,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
return out;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Structured attribute object from the parsed spec. */
|
|
123
|
+
interface AttributeShape {
|
|
124
|
+
type?: string;
|
|
125
|
+
required?: boolean;
|
|
126
|
+
unique?: boolean;
|
|
127
|
+
auto?: string;
|
|
128
|
+
values?: string[];
|
|
129
|
+
maxLength?: number;
|
|
130
|
+
max?: number;
|
|
131
|
+
[k: string]: unknown;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function parseTypeFromConvention(def: unknown): string | undefined {
|
|
135
|
+
// Convention form: "String required unique"
|
|
136
|
+
if (typeof def === 'string') {
|
|
137
|
+
const first = def.trim().split(/\s+/)[0];
|
|
138
|
+
return first;
|
|
139
|
+
}
|
|
140
|
+
return undefined;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function hasConventionFlag(def: unknown, flag: string): boolean {
|
|
144
|
+
if (typeof def === 'string') {
|
|
145
|
+
return def.split(/\s+/).includes(flag);
|
|
146
|
+
}
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function normalizeType(type: string): string {
|
|
151
|
+
// Strip any trailing modifiers from a compact convention-string form.
|
|
152
|
+
return type.replace(/[^A-Za-z].*$/, '');
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function extractBelongsTo(model: ModelSpec): string[] {
|
|
156
|
+
const rels = model.relationships ?? {};
|
|
157
|
+
const out: string[] = [];
|
|
158
|
+
for (const [name, def] of Object.entries(rels)) {
|
|
159
|
+
if ((def as { type?: string })?.type === 'belongsTo') out.push(name);
|
|
160
|
+
}
|
|
161
|
+
return out;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
165
|
+
// Input rendering
|
|
166
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
167
|
+
|
|
168
|
+
const INPUT_CLS =
|
|
169
|
+
'w-full rounded border border-gray-300 px-3 py-2 text-sm ' +
|
|
170
|
+
'focus:outline-none focus:ring-2 focus:ring-blue-500 ' +
|
|
171
|
+
'dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100';
|
|
172
|
+
|
|
173
|
+
const LABEL_CLS = 'block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1';
|
|
174
|
+
|
|
175
|
+
function renderInput(field: FieldInfo): string[] {
|
|
176
|
+
const requiredMark = field.required ? ' *' : '';
|
|
177
|
+
const reqAttr = field.required ? ' required' : '';
|
|
178
|
+
|
|
179
|
+
// Enum → <select>
|
|
180
|
+
if (field.values && field.values.length > 0) {
|
|
181
|
+
const options = field.values
|
|
182
|
+
.map(v => ` <option value="${escapeAttr(v)}">${escapeText(v)}</option>`)
|
|
183
|
+
.join('\n');
|
|
184
|
+
return [
|
|
185
|
+
' <div>',
|
|
186
|
+
` <label className="${LABEL_CLS}" htmlFor="${field.name}">${field.label}${requiredMark}</label>`,
|
|
187
|
+
` <select`,
|
|
188
|
+
` id="${field.name}"`,
|
|
189
|
+
` className="${INPUT_CLS}"`,
|
|
190
|
+
` value={String((formData as any).${field.name} ?? '')}`,
|
|
191
|
+
` onChange={e => handleChange('${field.name}', e.target.value)}${reqAttr}`,
|
|
192
|
+
` >`,
|
|
193
|
+
` <option value="">— choose —</option>`,
|
|
194
|
+
options,
|
|
195
|
+
` </select>`,
|
|
196
|
+
' </div>',
|
|
197
|
+
];
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Long-string / Text → <textarea>
|
|
201
|
+
if (field.type === 'Text' || field.isLongString) {
|
|
202
|
+
return [
|
|
203
|
+
' <div>',
|
|
204
|
+
` <label className="${LABEL_CLS}" htmlFor="${field.name}">${field.label}${requiredMark}</label>`,
|
|
205
|
+
` <textarea`,
|
|
206
|
+
` id="${field.name}"`,
|
|
207
|
+
` className="${INPUT_CLS}"`,
|
|
208
|
+
` rows={4}`,
|
|
209
|
+
` value={String((formData as any).${field.name} ?? '')}`,
|
|
210
|
+
` onChange={e => handleChange('${field.name}', e.target.value)}${reqAttr}`,
|
|
211
|
+
` />`,
|
|
212
|
+
' </div>',
|
|
213
|
+
];
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Boolean → checkbox
|
|
217
|
+
if (field.type === 'Boolean') {
|
|
218
|
+
return [
|
|
219
|
+
' <div>',
|
|
220
|
+
` <label className="inline-flex items-center gap-2">`,
|
|
221
|
+
` <input`,
|
|
222
|
+
` id="${field.name}"`,
|
|
223
|
+
` type="checkbox"`,
|
|
224
|
+
` checked={Boolean((formData as any).${field.name})}`,
|
|
225
|
+
` onChange={e => handleChange('${field.name}', e.target.checked)}`,
|
|
226
|
+
` />`,
|
|
227
|
+
` <span className="text-sm text-gray-700 dark:text-gray-300">${field.label}${requiredMark}</span>`,
|
|
228
|
+
` </label>`,
|
|
229
|
+
' </div>',
|
|
230
|
+
];
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const inputType = mapInputType(field.type);
|
|
234
|
+
return [
|
|
235
|
+
' <div>',
|
|
236
|
+
` <label className="${LABEL_CLS}" htmlFor="${field.name}">${field.label}${requiredMark}</label>`,
|
|
237
|
+
` <input`,
|
|
238
|
+
` id="${field.name}"`,
|
|
239
|
+
` type="${inputType}"`,
|
|
240
|
+
` className="${INPUT_CLS}"`,
|
|
241
|
+
` value={String((formData as any).${field.name} ?? '')}`,
|
|
242
|
+
` onChange={e => handleChange('${field.name}', ${coerceOnChange(field.type)})}${reqAttr}`,
|
|
243
|
+
` />`,
|
|
244
|
+
' </div>',
|
|
245
|
+
];
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** Scalar SpecVerse type → HTML input type attribute. */
|
|
249
|
+
function mapInputType(type: string): string {
|
|
250
|
+
switch (type) {
|
|
251
|
+
case 'Integer':
|
|
252
|
+
case 'Float':
|
|
253
|
+
case 'Number':
|
|
254
|
+
case 'Money':
|
|
255
|
+
return 'number';
|
|
256
|
+
case 'DateTime':
|
|
257
|
+
return 'datetime-local';
|
|
258
|
+
case 'Date':
|
|
259
|
+
return 'date';
|
|
260
|
+
case 'Email':
|
|
261
|
+
return 'email';
|
|
262
|
+
case 'URL':
|
|
263
|
+
return 'url';
|
|
264
|
+
case 'String':
|
|
265
|
+
case 'UUID':
|
|
266
|
+
default:
|
|
267
|
+
return 'text';
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* JSX snippet for the onChange handler's second argument — coerces
|
|
273
|
+
* the string value to the right shape for the field. The user can
|
|
274
|
+
* edit this; defaults are a reasonable starter.
|
|
275
|
+
*/
|
|
276
|
+
function coerceOnChange(type: string): string {
|
|
277
|
+
switch (type) {
|
|
278
|
+
case 'Integer':
|
|
279
|
+
case 'Number':
|
|
280
|
+
return "e.target.value === '' ? undefined : Number(e.target.value)";
|
|
281
|
+
case 'Float':
|
|
282
|
+
case 'Money':
|
|
283
|
+
return "e.target.value === '' ? undefined : parseFloat(e.target.value)";
|
|
284
|
+
default:
|
|
285
|
+
return 'e.target.value';
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
290
|
+
// Small helpers
|
|
291
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
292
|
+
|
|
293
|
+
function humanize(name: string): string {
|
|
294
|
+
return name
|
|
295
|
+
.replace(/([A-Z])/g, ' $1')
|
|
296
|
+
.replace(/^./, c => c.toUpperCase())
|
|
297
|
+
.trim();
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function escapeAttr(s: string): string {
|
|
301
|
+
return s.replace(/"/g, '"');
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function escapeText(s: string): string {
|
|
305
|
+
return s.replace(/</g, '<').replace(/>/g, '>');
|
|
306
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Helpers emitter for ReactAppStarter
|
|
3
|
+
*
|
|
4
|
+
* Returns the source of small utility files that generated projects
|
|
5
|
+
* use. Inlined into `src/lib/` at realize time rather than imported
|
|
6
|
+
* from `@specverse/runtime` — Factory B's defining trait is that the
|
|
7
|
+
* generated code has no @specverse/* runtime dependency.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Source of `src/lib/entity-display.ts`.
|
|
12
|
+
*
|
|
13
|
+
* Mirrors `getEntityDisplayName` from
|
|
14
|
+
* `@specverse/runtime/views/core/entity-display`. Used by:
|
|
15
|
+
* - The FK-resolution TODO in detail-view output (users who upgrade
|
|
16
|
+
* the plain-FK display to a resolved display name import this).
|
|
17
|
+
* - Any user-written code that wants to label a record.
|
|
18
|
+
*/
|
|
19
|
+
export function emitEntityDisplay(): string {
|
|
20
|
+
return `/**
|
|
21
|
+
* Pick the best human-readable label for an entity.
|
|
22
|
+
*
|
|
23
|
+
* Walks a prioritized list of candidate fields. Falls back to a
|
|
24
|
+
* truncated id if none match. Inlined into this project by
|
|
25
|
+
* @specverse/realize (ReactAppStarter); edit freely — nothing in
|
|
26
|
+
* the factory's regeneration will clobber your edits.
|
|
27
|
+
*/
|
|
28
|
+
export function getEntityDisplayName(entity: Record<string, unknown> | null | undefined): string {
|
|
29
|
+
if (!entity) return '';
|
|
30
|
+
|
|
31
|
+
const candidates = ['name', 'title', 'displayName', 'label', 'username', 'email'];
|
|
32
|
+
for (const key of candidates) {
|
|
33
|
+
const value = entity[key];
|
|
34
|
+
if (typeof value === 'string' && value.trim().length > 0) {
|
|
35
|
+
return value;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const id = entity.id;
|
|
40
|
+
if (typeof id === 'string') {
|
|
41
|
+
return id.length > 8 ? id.slice(0, 8) + '…' : id;
|
|
42
|
+
}
|
|
43
|
+
return id != null ? String(id) : '';
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Map an entity id to the display name of the record with that id in
|
|
48
|
+
* a provided list. Handy for resolving belongsTo FK columns in
|
|
49
|
+
* list / detail views.
|
|
50
|
+
*/
|
|
51
|
+
export function resolveEntityDisplayName(
|
|
52
|
+
id: unknown,
|
|
53
|
+
records: ReadonlyArray<Record<string, unknown>>
|
|
54
|
+
): string {
|
|
55
|
+
if (id == null) return '';
|
|
56
|
+
const match = records.find(r => r.id === id);
|
|
57
|
+
return match ? getEntityDisplayName(match) : String(id);
|
|
58
|
+
}
|
|
59
|
+
`;
|
|
60
|
+
}
|