@specverse/engines 4.1.22 → 4.1.24
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/dist/inference/ui-contracts/index.d.ts +38 -0
- package/dist/inference/ui-contracts/index.d.ts.map +1 -0
- package/dist/inference/ui-contracts/index.js +212 -0
- package/dist/inference/ui-contracts/index.js.map +1 -0
- package/dist/inference/ui-contracts/rules/_shared.d.ts +32 -0
- package/dist/inference/ui-contracts/rules/_shared.d.ts.map +1 -0
- package/dist/inference/ui-contracts/rules/_shared.js +103 -0
- package/dist/inference/ui-contracts/rules/_shared.js.map +1 -0
- package/dist/inference/ui-contracts/rules/action-buttons-present.d.ts +21 -0
- package/dist/inference/ui-contracts/rules/action-buttons-present.d.ts.map +1 -0
- package/dist/inference/ui-contracts/rules/action-buttons-present.js +62 -0
- package/dist/inference/ui-contracts/rules/action-buttons-present.js.map +1 -0
- package/dist/inference/ui-contracts/rules/create-reflects-in-list.d.ts +22 -0
- package/dist/inference/ui-contracts/rules/create-reflects-in-list.d.ts.map +1 -0
- package/dist/inference/ui-contracts/rules/create-reflects-in-list.js +58 -0
- package/dist/inference/ui-contracts/rules/create-reflects-in-list.js.map +1 -0
- package/dist/inference/ui-contracts/rules/delete-reflects-in-list.d.ts +22 -0
- package/dist/inference/ui-contracts/rules/delete-reflects-in-list.d.ts.map +1 -0
- package/dist/inference/ui-contracts/rules/delete-reflects-in-list.js +64 -0
- package/dist/inference/ui-contracts/rules/delete-reflects-in-list.js.map +1 -0
- package/dist/inference/ui-contracts/rules/detail-view-renders.d.ts +24 -0
- package/dist/inference/ui-contracts/rules/detail-view-renders.d.ts.map +1 -0
- package/dist/inference/ui-contracts/rules/detail-view-renders.js +34 -0
- package/dist/inference/ui-contracts/rules/detail-view-renders.js.map +1 -0
- package/dist/inference/ui-contracts/rules/evolve-reflects-in-list.d.ts +21 -0
- package/dist/inference/ui-contracts/rules/evolve-reflects-in-list.d.ts.map +1 -0
- package/dist/inference/ui-contracts/rules/evolve-reflects-in-list.js +63 -0
- package/dist/inference/ui-contracts/rules/evolve-reflects-in-list.js.map +1 -0
- package/dist/inference/ui-contracts/rules/form-shows-required-indicators.d.ts +15 -0
- package/dist/inference/ui-contracts/rules/form-shows-required-indicators.d.ts.map +1 -0
- package/dist/inference/ui-contracts/rules/form-shows-required-indicators.js +38 -0
- package/dist/inference/ui-contracts/rules/form-shows-required-indicators.js.map +1 -0
- package/dist/inference/ui-contracts/rules/hasmany-shows-children-in-detail.d.ts +17 -0
- package/dist/inference/ui-contracts/rules/hasmany-shows-children-in-detail.d.ts.map +1 -0
- package/dist/inference/ui-contracts/rules/hasmany-shows-children-in-detail.js +39 -0
- package/dist/inference/ui-contracts/rules/hasmany-shows-children-in-detail.js.map +1 -0
- package/dist/inference/ui-contracts/rules/lifecycle-state-visible-in-detail.d.ts +25 -0
- package/dist/inference/ui-contracts/rules/lifecycle-state-visible-in-detail.d.ts.map +1 -0
- package/dist/inference/ui-contracts/rules/lifecycle-state-visible-in-detail.js +66 -0
- package/dist/inference/ui-contracts/rules/lifecycle-state-visible-in-detail.js.map +1 -0
- package/dist/inference/ui-contracts/rules/list-shows-business-columns.d.ts +17 -0
- package/dist/inference/ui-contracts/rules/list-shows-business-columns.d.ts.map +1 -0
- package/dist/inference/ui-contracts/rules/list-shows-business-columns.js +39 -0
- package/dist/inference/ui-contracts/rules/list-shows-business-columns.js.map +1 -0
- package/dist/inference/ui-contracts/rules/list-view-renders.d.ts +19 -0
- package/dist/inference/ui-contracts/rules/list-view-renders.d.ts.map +1 -0
- package/dist/inference/ui-contracts/rules/list-view-renders.js +29 -0
- package/dist/inference/ui-contracts/rules/list-view-renders.js.map +1 -0
- package/dist/inference/ui-contracts/rules/nav-has-model-entries.d.ts +20 -0
- package/dist/inference/ui-contracts/rules/nav-has-model-entries.d.ts.map +1 -0
- package/dist/inference/ui-contracts/rules/nav-has-model-entries.js +29 -0
- package/dist/inference/ui-contracts/rules/nav-has-model-entries.js.map +1 -0
- package/dist/inference/ui-contracts/test-case-types.d.ts +135 -0
- package/dist/inference/ui-contracts/test-case-types.d.ts.map +1 -0
- package/dist/inference/ui-contracts/test-case-types.js +14 -0
- package/dist/inference/ui-contracts/test-case-types.js.map +1 -0
- package/dist/inference/ui-contracts/translator.d.ts +17 -0
- package/dist/inference/ui-contracts/translator.d.ts.map +1 -0
- package/dist/inference/ui-contracts/translator.js +142 -0
- package/dist/inference/ui-contracts/translator.js.map +1 -0
- package/dist/libs/instance-factories/applications/templates/react/api-client-generator.js +5 -2
- package/dist/libs/instance-factories/applications/templates/react/vite-config-generator.js +11 -0
- package/dist/libs/instance-factories/scaffolding/templates/generic/package-json-generator.js +10 -0
- package/dist/libs/instance-factories/views/templates/react/components-generator.js +34 -23
- package/dist/libs/instance-factories/views/templates/react/hooks-generator.js +51 -81
- package/dist/realize/index.d.ts.map +1 -1
- package/dist/realize/index.js +204 -0
- package/dist/realize/index.js.map +1 -1
- package/libs/instance-factories/applications/templates/react/api-client-generator.ts +5 -2
- package/libs/instance-factories/applications/templates/react/vite-config-generator.ts +11 -0
- package/libs/instance-factories/scaffolding/templates/generic/package-json-generator.ts +13 -0
- package/libs/instance-factories/views/templates/react/components-generator.ts +34 -23
- package/libs/instance-factories/views/templates/react/hooks-generator.ts +72 -88
- package/package.json +1 -1
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rule: nav-has-model-entries
|
|
3
|
+
*
|
|
4
|
+
* For every model declared in the spec, assert that the runtime UI
|
|
5
|
+
* exposes a visible reference to the model (nav entry, sidebar link,
|
|
6
|
+
* dashboard card — any of them count). v1 uses a loose "text appears
|
|
7
|
+
* on the page" assertion; a stricter ARIA-role variant can come
|
|
8
|
+
* later once the runtime standardizes its nav component's semantics.
|
|
9
|
+
*
|
|
10
|
+
* One TestCase per model.
|
|
11
|
+
*
|
|
12
|
+
* This is the most important rule: if the runtime can't even show
|
|
13
|
+
* the model name after initial render, everything else downstream is
|
|
14
|
+
* certainly broken. It's also the cheapest — one navigation, one
|
|
15
|
+
* text query per model.
|
|
16
|
+
*/
|
|
17
|
+
export const RULE_ID = 'nav-has-model-entries';
|
|
18
|
+
export const navHasModelEntries = (spec) => {
|
|
19
|
+
return spec.models.map(model => ({
|
|
20
|
+
ruleId: RULE_ID,
|
|
21
|
+
specElement: model.name,
|
|
22
|
+
name: `[${RULE_ID}] ${model.name} is visible in the nav`,
|
|
23
|
+
steps: [
|
|
24
|
+
{ action: 'bootRuntime' },
|
|
25
|
+
{ action: 'expectNavHasModel', modelName: model.name },
|
|
26
|
+
],
|
|
27
|
+
}));
|
|
28
|
+
};
|
|
29
|
+
//# sourceMappingURL=nav-has-model-entries.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"nav-has-model-entries.js","sourceRoot":"","sources":["../../../../src/inference/ui-contracts/rules/nav-has-model-entries.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAIH,MAAM,CAAC,MAAM,OAAO,GAAG,uBAAuB,CAAC;AAE/C,MAAM,CAAC,MAAM,kBAAkB,GAAmB,CAAC,IAAoB,EAAc,EAAE;IACrF,OAAO,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;QAC/B,MAAM,EAAE,OAAO;QACf,WAAW,EAAE,KAAK,CAAC,IAAI;QACvB,IAAI,EAAE,IAAI,OAAO,KAAK,KAAK,CAAC,IAAI,wBAAwB;QACxD,KAAK,EAAE;YACL,EAAE,MAAM,EAAE,aAAa,EAAE;YACzB,EAAE,MAAM,EAAE,mBAAmB,EAAE,SAAS,EAAE,KAAK,CAAC,IAAI,EAAE;SACvD;KACF,CAAC,CAAC,CAAC;AACN,CAAC,CAAC"}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TestCase — declarative description of a UI contract test.
|
|
3
|
+
*
|
|
4
|
+
* Rules (pure functions) produce TestCase[]. The translator renders
|
|
5
|
+
* each TestCase into a Playwright `.spec.ts` file that imports helpers
|
|
6
|
+
* from `@specverse/runtime/test-harness`. Neither rules nor test cases
|
|
7
|
+
* know anything about React Query, cache invalidation, or any
|
|
8
|
+
* implementation detail — they speak only spec language.
|
|
9
|
+
*
|
|
10
|
+
* v1 steps cover navigation + assertion. Later phases add create /
|
|
11
|
+
* delete / evolve / expect-absent for state-sync rules.
|
|
12
|
+
*/
|
|
13
|
+
export type TestStep = {
|
|
14
|
+
action: 'bootRuntime';
|
|
15
|
+
} | {
|
|
16
|
+
action: 'expectNavHasModel';
|
|
17
|
+
modelName: string;
|
|
18
|
+
} | {
|
|
19
|
+
action: 'navigateToModel';
|
|
20
|
+
modelName: string;
|
|
21
|
+
} | {
|
|
22
|
+
action: 'navigateToModelForm';
|
|
23
|
+
modelName: string;
|
|
24
|
+
} | {
|
|
25
|
+
action: 'navigateToModelDetail';
|
|
26
|
+
modelName: string;
|
|
27
|
+
} | {
|
|
28
|
+
action: 'expectViewRenders';
|
|
29
|
+
} | {
|
|
30
|
+
action: 'expectListColumn';
|
|
31
|
+
modelName: string;
|
|
32
|
+
attributeName: string;
|
|
33
|
+
} | {
|
|
34
|
+
action: 'expectFormInput';
|
|
35
|
+
modelName: string;
|
|
36
|
+
attributeName: string;
|
|
37
|
+
required?: boolean;
|
|
38
|
+
} | {
|
|
39
|
+
action: 'expectLifecycleState';
|
|
40
|
+
modelName: string;
|
|
41
|
+
stateName: string;
|
|
42
|
+
} | {
|
|
43
|
+
action: 'expectRelationshipSection';
|
|
44
|
+
modelName: string;
|
|
45
|
+
relationshipName: string;
|
|
46
|
+
} | {
|
|
47
|
+
action: 'expectActionButton';
|
|
48
|
+
modelName: string;
|
|
49
|
+
actionLabel: string;
|
|
50
|
+
} | {
|
|
51
|
+
action: 'createEntity';
|
|
52
|
+
modelName: string;
|
|
53
|
+
data: Record<string, unknown>;
|
|
54
|
+
bindAs?: string;
|
|
55
|
+
uniqueField?: string;
|
|
56
|
+
} | {
|
|
57
|
+
action: 'deleteEntity';
|
|
58
|
+
modelName: string;
|
|
59
|
+
idExpr: string;
|
|
60
|
+
} | {
|
|
61
|
+
action: 'evolveEntity';
|
|
62
|
+
modelName: string;
|
|
63
|
+
idExpr: string;
|
|
64
|
+
targetState: string;
|
|
65
|
+
} | {
|
|
66
|
+
action: 'expectEntityInList';
|
|
67
|
+
modelName: string;
|
|
68
|
+
displayValue?: string;
|
|
69
|
+
displayExpr?: string;
|
|
70
|
+
} | {
|
|
71
|
+
action: 'expectEntityAbsent';
|
|
72
|
+
modelName: string;
|
|
73
|
+
displayValue?: string;
|
|
74
|
+
displayExpr?: string;
|
|
75
|
+
};
|
|
76
|
+
export interface TestCase {
|
|
77
|
+
/** The rule that produced this test case. Used in test names for attribution. */
|
|
78
|
+
ruleId: string;
|
|
79
|
+
/** The spec element the case applies to (e.g. 'Poll', 'PollController.delete'). */
|
|
80
|
+
specElement: string;
|
|
81
|
+
/** Full test name — shown by Playwright reporter. */
|
|
82
|
+
name: string;
|
|
83
|
+
/** Ordered sequence of actions/assertions. */
|
|
84
|
+
steps: TestStep[];
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* A rule is a pure function from a normalized spec to a list of test
|
|
88
|
+
* cases. Rules never touch the filesystem, the network, or any mutable
|
|
89
|
+
* state — they just describe what should be tested.
|
|
90
|
+
*/
|
|
91
|
+
export type UiContractRule = (spec: NormalizedSpec) => TestCase[];
|
|
92
|
+
/**
|
|
93
|
+
* Minimal normalized spec shape the rules operate on. The realize
|
|
94
|
+
* pipeline converts raw spec output into this shape before running
|
|
95
|
+
* rules. Kept deliberately small — rules grow their own helpers as
|
|
96
|
+
* they need richer data.
|
|
97
|
+
*/
|
|
98
|
+
export interface NormalizedSpec {
|
|
99
|
+
models: ModelInfo[];
|
|
100
|
+
controllers?: ControllerInfo[];
|
|
101
|
+
services?: ServiceInfo[];
|
|
102
|
+
views?: ViewInfo[];
|
|
103
|
+
}
|
|
104
|
+
export interface ModelInfo {
|
|
105
|
+
name: string;
|
|
106
|
+
attributes?: AttributeInfo[];
|
|
107
|
+
lifecycles?: Record<string, unknown>;
|
|
108
|
+
relationships?: RelationshipInfo[];
|
|
109
|
+
}
|
|
110
|
+
export interface AttributeInfo {
|
|
111
|
+
name: string;
|
|
112
|
+
type?: string;
|
|
113
|
+
required?: boolean;
|
|
114
|
+
category?: 'business' | 'metadata' | 'relationship';
|
|
115
|
+
}
|
|
116
|
+
export interface RelationshipInfo {
|
|
117
|
+
name: string;
|
|
118
|
+
type: string;
|
|
119
|
+
target?: string;
|
|
120
|
+
}
|
|
121
|
+
export interface ControllerInfo {
|
|
122
|
+
name: string;
|
|
123
|
+
model?: string;
|
|
124
|
+
cured?: Record<string, unknown>;
|
|
125
|
+
}
|
|
126
|
+
export interface ServiceInfo {
|
|
127
|
+
name: string;
|
|
128
|
+
operations?: Record<string, unknown>;
|
|
129
|
+
}
|
|
130
|
+
export interface ViewInfo {
|
|
131
|
+
name: string;
|
|
132
|
+
type: string;
|
|
133
|
+
model?: string;
|
|
134
|
+
}
|
|
135
|
+
//# sourceMappingURL=test-case-types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"test-case-types.d.ts","sourceRoot":"","sources":["../../../src/inference/ui-contracts/test-case-types.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TestCase — declarative description of a UI contract test.
|
|
3
|
+
*
|
|
4
|
+
* Rules (pure functions) produce TestCase[]. The translator renders
|
|
5
|
+
* each TestCase into a Playwright `.spec.ts` file that imports helpers
|
|
6
|
+
* from `@specverse/runtime/test-harness`. Neither rules nor test cases
|
|
7
|
+
* know anything about React Query, cache invalidation, or any
|
|
8
|
+
* implementation detail — they speak only spec language.
|
|
9
|
+
*
|
|
10
|
+
* v1 steps cover navigation + assertion. Later phases add create /
|
|
11
|
+
* delete / evolve / expect-absent for state-sync rules.
|
|
12
|
+
*/
|
|
13
|
+
export {};
|
|
14
|
+
//# sourceMappingURL=test-case-types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"test-case-types.js","sourceRoot":"","sources":["../../../src/inference/ui-contracts/test-case-types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TestCase → Playwright .spec.ts translator.
|
|
3
|
+
*
|
|
4
|
+
* Renders a list of TestCase objects into a single Playwright spec
|
|
5
|
+
* file. The output file imports from `@specverse/runtime/test-harness`
|
|
6
|
+
* and nothing else from the runtime — all logic lives in the harness
|
|
7
|
+
* helpers. The translator is intentionally dumb: each step maps to a
|
|
8
|
+
* helper call, nothing more.
|
|
9
|
+
*/
|
|
10
|
+
import type { TestCase } from './test-case-types.js';
|
|
11
|
+
/**
|
|
12
|
+
* Render a full `.spec.ts` file for one or more test cases sharing
|
|
13
|
+
* the same rule. One file per rule keeps failures easy to attribute
|
|
14
|
+
* and lets `npx playwright test` filter by rule ID via file glob.
|
|
15
|
+
*/
|
|
16
|
+
export declare function renderSpecFile(ruleId: string, testCases: TestCase[]): string;
|
|
17
|
+
//# sourceMappingURL=translator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"translator.d.ts","sourceRoot":"","sources":["../../../src/inference/ui-contracts/translator.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAY,MAAM,sBAAsB,CAAC;AAuG/D;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,QAAQ,EAAE,GAAG,MAAM,CA6B5E"}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TestCase → Playwright .spec.ts translator.
|
|
3
|
+
*
|
|
4
|
+
* Renders a list of TestCase objects into a single Playwright spec
|
|
5
|
+
* file. The output file imports from `@specverse/runtime/test-harness`
|
|
6
|
+
* and nothing else from the runtime — all logic lives in the harness
|
|
7
|
+
* helpers. The translator is intentionally dumb: each step maps to a
|
|
8
|
+
* helper call, nothing more.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Render a single step as a line of test body code.
|
|
12
|
+
*/
|
|
13
|
+
function renderStep(step) {
|
|
14
|
+
switch (step.action) {
|
|
15
|
+
case 'bootRuntime':
|
|
16
|
+
// Assign `page` once per test body. Multiple bootRuntime steps
|
|
17
|
+
// inside one test aren't supported and would shadow the binding.
|
|
18
|
+
return ` const { page } = await bootRuntime(browser);`;
|
|
19
|
+
case 'expectNavHasModel':
|
|
20
|
+
return ` await expectNavHasModel(page, ${JSON.stringify(step.modelName)});`;
|
|
21
|
+
case 'navigateToModel':
|
|
22
|
+
return ` await navigateToModel(page, ${JSON.stringify(step.modelName)});`;
|
|
23
|
+
case 'navigateToModelForm':
|
|
24
|
+
return ` await navigateToModelForm(page, ${JSON.stringify(step.modelName)});`;
|
|
25
|
+
case 'navigateToModelDetail':
|
|
26
|
+
return ` await navigateToModelDetail(page, ${JSON.stringify(step.modelName)});`;
|
|
27
|
+
case 'expectViewRenders':
|
|
28
|
+
return ` await expectViewRenders(page);`;
|
|
29
|
+
case 'expectListColumn':
|
|
30
|
+
return ` await expectListColumn(page, ${JSON.stringify(step.modelName)}, ${JSON.stringify(step.attributeName)});`;
|
|
31
|
+
case 'expectFormInput': {
|
|
32
|
+
const opts = step.required ? `, { required: true }` : ``;
|
|
33
|
+
return ` await expectFormInput(page, ${JSON.stringify(step.modelName)}, ${JSON.stringify(step.attributeName)}${opts});`;
|
|
34
|
+
}
|
|
35
|
+
case 'expectLifecycleState':
|
|
36
|
+
return ` await expectLifecycleState(page, ${JSON.stringify(step.modelName)}, ${JSON.stringify(step.stateName)});`;
|
|
37
|
+
case 'expectRelationshipSection':
|
|
38
|
+
return ` await expectRelationshipSection(page, ${JSON.stringify(step.modelName)}, ${JSON.stringify(step.relationshipName)});`;
|
|
39
|
+
case 'expectActionButton':
|
|
40
|
+
return ` await expectActionButton(page, ${JSON.stringify(step.modelName)}, ${JSON.stringify(step.actionLabel)});`;
|
|
41
|
+
case 'createEntity': {
|
|
42
|
+
const varName = step.bindAs || 'created';
|
|
43
|
+
const dataLit = JSON.stringify(step.data);
|
|
44
|
+
const optsLit = step.uniqueField
|
|
45
|
+
? `, { uniqueField: ${JSON.stringify(step.uniqueField)} }`
|
|
46
|
+
: '';
|
|
47
|
+
return ` const ${varName} = await createEntity(page, ${JSON.stringify(step.modelName)}, ${dataLit}${optsLit});`;
|
|
48
|
+
}
|
|
49
|
+
case 'deleteEntity':
|
|
50
|
+
return ` await deleteEntity(page, ${JSON.stringify(step.modelName)}, ${step.idExpr});`;
|
|
51
|
+
case 'evolveEntity':
|
|
52
|
+
return ` await evolveEntity(page, ${JSON.stringify(step.modelName)}, ${step.idExpr}, ${JSON.stringify(step.targetState)});`;
|
|
53
|
+
case 'expectEntityInList': {
|
|
54
|
+
const display = step.displayExpr ?? JSON.stringify(step.displayValue ?? '');
|
|
55
|
+
return ` await expectEntityInList(page, ${JSON.stringify(step.modelName)}, ${display});`;
|
|
56
|
+
}
|
|
57
|
+
case 'expectEntityAbsent': {
|
|
58
|
+
const display = step.displayExpr ?? JSON.stringify(step.displayValue ?? '');
|
|
59
|
+
return ` await expectEntityAbsent(page, ${JSON.stringify(step.modelName)}, ${display});`;
|
|
60
|
+
}
|
|
61
|
+
default: {
|
|
62
|
+
const _exhaustive = step;
|
|
63
|
+
throw new Error(`translator: unknown step action: ${JSON.stringify(_exhaustive)}`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Collect the set of harness helpers a test case depends on so the
|
|
69
|
+
* import list only contains what's actually used.
|
|
70
|
+
*/
|
|
71
|
+
function helpersFor(step) {
|
|
72
|
+
switch (step.action) {
|
|
73
|
+
case 'bootRuntime':
|
|
74
|
+
return ['bootRuntime'];
|
|
75
|
+
case 'expectNavHasModel':
|
|
76
|
+
return ['expectNavHasModel'];
|
|
77
|
+
case 'navigateToModel':
|
|
78
|
+
return ['navigateToModel'];
|
|
79
|
+
case 'navigateToModelForm':
|
|
80
|
+
return ['navigateToModelForm'];
|
|
81
|
+
case 'navigateToModelDetail':
|
|
82
|
+
return ['navigateToModelDetail'];
|
|
83
|
+
case 'expectViewRenders':
|
|
84
|
+
return ['expectViewRenders'];
|
|
85
|
+
case 'expectListColumn':
|
|
86
|
+
return ['expectListColumn'];
|
|
87
|
+
case 'expectFormInput':
|
|
88
|
+
return ['expectFormInput'];
|
|
89
|
+
case 'expectLifecycleState':
|
|
90
|
+
return ['expectLifecycleState'];
|
|
91
|
+
case 'expectRelationshipSection':
|
|
92
|
+
return ['expectRelationshipSection'];
|
|
93
|
+
case 'expectActionButton':
|
|
94
|
+
return ['expectActionButton'];
|
|
95
|
+
case 'createEntity':
|
|
96
|
+
return ['createEntity'];
|
|
97
|
+
case 'deleteEntity':
|
|
98
|
+
return ['deleteEntity'];
|
|
99
|
+
case 'evolveEntity':
|
|
100
|
+
return ['evolveEntity'];
|
|
101
|
+
case 'expectEntityInList':
|
|
102
|
+
return ['expectEntityInList'];
|
|
103
|
+
case 'expectEntityAbsent':
|
|
104
|
+
return ['expectEntityAbsent'];
|
|
105
|
+
default:
|
|
106
|
+
return [];
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Render a full `.spec.ts` file for one or more test cases sharing
|
|
111
|
+
* the same rule. One file per rule keeps failures easy to attribute
|
|
112
|
+
* and lets `npx playwright test` filter by rule ID via file glob.
|
|
113
|
+
*/
|
|
114
|
+
export function renderSpecFile(ruleId, testCases) {
|
|
115
|
+
const helpers = new Set();
|
|
116
|
+
for (const tc of testCases) {
|
|
117
|
+
for (const step of tc.steps) {
|
|
118
|
+
for (const h of helpersFor(step))
|
|
119
|
+
helpers.add(h);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
const importList = [...helpers].sort().join(', ');
|
|
123
|
+
const header = `/**
|
|
124
|
+
* Auto-generated UI contract test file.
|
|
125
|
+
* Rule: ${ruleId}
|
|
126
|
+
* DO NOT EDIT — regenerated every \`spv realize\` run. Hand edits are
|
|
127
|
+
* lost on the next regenerate. Change the rule or the spec instead.
|
|
128
|
+
*/
|
|
129
|
+
|
|
130
|
+
import { test } from '@playwright/test';
|
|
131
|
+
import { ${importList} } from '@specverse/runtime/test-harness';
|
|
132
|
+
`;
|
|
133
|
+
const body = testCases.map(tc => {
|
|
134
|
+
const stepLines = tc.steps.map(renderStep).join('\n');
|
|
135
|
+
return `
|
|
136
|
+
test(${JSON.stringify(tc.name)}, async ({ browser }) => {
|
|
137
|
+
${stepLines}
|
|
138
|
+
});`;
|
|
139
|
+
}).join('\n');
|
|
140
|
+
return header + body + '\n';
|
|
141
|
+
}
|
|
142
|
+
//# sourceMappingURL=translator.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"translator.js","sourceRoot":"","sources":["../../../src/inference/ui-contracts/translator.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAIH;;GAEG;AACH,SAAS,UAAU,CAAC,IAAc;IAChC,QAAQ,IAAI,CAAC,MAAM,EAAE,CAAC;QACpB,KAAK,aAAa;YAChB,+DAA+D;YAC/D,iEAAiE;YACjE,OAAO,gDAAgD,CAAC;QAC1D,KAAK,mBAAmB;YACtB,OAAO,mCAAmC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC;QAC/E,KAAK,iBAAiB;YACpB,OAAO,iCAAiC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC;QAC7E,KAAK,qBAAqB;YACxB,OAAO,qCAAqC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC;QACjF,KAAK,uBAAuB;YAC1B,OAAO,uCAAuC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC;QACnF,KAAK,mBAAmB;YACtB,OAAO,kCAAkC,CAAC;QAC5C,KAAK,kBAAkB;YACrB,OAAO,kCAAkC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC;QACrH,KAAK,iBAAiB,CAAC,CAAC,CAAC;YACvB,MAAM,IAAI,GAAG,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,sBAAsB,CAAC,CAAC,CAAC,EAAE,CAAC;YACzD,OAAO,iCAAiC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,aAAa,CAAC,GAAG,IAAI,IAAI,CAAC;QAC3H,CAAC;QACD,KAAK,sBAAsB;YACzB,OAAO,sCAAsC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC;QACrH,KAAK,2BAA2B;YAC9B,OAAO,2CAA2C,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,gBAAgB,CAAC,IAAI,CAAC;QACjI,KAAK,oBAAoB;YACvB,OAAO,oCAAoC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC;QACrH,KAAK,cAAc,CAAC,CAAC,CAAC;YACpB,MAAM,OAAO,GAAG,IAAI,CAAC,MAAM,IAAI,SAAS,CAAC;YACzC,MAAM,OAAO,GAAG,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YAC1C,MAAM,OAAO,GAAG,IAAI,CAAC,WAAW;gBAC9B,CAAC,CAAC,oBAAoB,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,WAAW,CAAC,IAAI;gBAC1D,CAAC,CAAC,EAAE,CAAC;YACP,OAAO,WAAW,OAAO,+BAA+B,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,OAAO,GAAG,OAAO,IAAI,CAAC;QACnH,CAAC;QACD,KAAK,cAAc;YACjB,OAAO,8BAA8B,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,IAAI,CAAC,MAAM,IAAI,CAAC;QAC1F,KAAK,cAAc;YACjB,OAAO,8BAA8B,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,IAAI,CAAC,MAAM,KAAK,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC;QAC/H,KAAK,oBAAoB,CAAC,CAAC,CAAC;YAC1B,MAAM,OAAO,GAAG,IAAI,CAAC,WAAW,IAAI,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,YAAY,IAAI,EAAE,CAAC,CAAC;YAC5E,OAAO,oCAAoC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,OAAO,IAAI,CAAC;QAC5F,CAAC;QACD,KAAK,oBAAoB,CAAC,CAAC,CAAC;YAC1B,MAAM,OAAO,GAAG,IAAI,CAAC,WAAW,IAAI,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,YAAY,IAAI,EAAE,CAAC,CAAC;YAC5E,OAAO,oCAAoC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,OAAO,IAAI,CAAC;QAC5F,CAAC;QACD,OAAO,CAAC,CAAC,CAAC;YACR,MAAM,WAAW,GAAU,IAAI,CAAC;YAChC,MAAM,IAAI,KAAK,CAAC,oCAAoC,IAAI,CAAC,SAAS,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC;QACrF,CAAC;IACH,CAAC;AACH,CAAC;AAED;;;GAGG;AACH,SAAS,UAAU,CAAC,IAAc;IAChC,QAAQ,IAAI,CAAC,MAAM,EAAE,CAAC;QACpB,KAAK,aAAa;YAChB,OAAO,CAAC,aAAa,CAAC,CAAC;QACzB,KAAK,mBAAmB;YACtB,OAAO,CAAC,mBAAmB,CAAC,CAAC;QAC/B,KAAK,iBAAiB;YACpB,OAAO,CAAC,iBAAiB,CAAC,CAAC;QAC7B,KAAK,qBAAqB;YACxB,OAAO,CAAC,qBAAqB,CAAC,CAAC;QACjC,KAAK,uBAAuB;YAC1B,OAAO,CAAC,uBAAuB,CAAC,CAAC;QACnC,KAAK,mBAAmB;YACtB,OAAO,CAAC,mBAAmB,CAAC,CAAC;QAC/B,KAAK,kBAAkB;YACrB,OAAO,CAAC,kBAAkB,CAAC,CAAC;QAC9B,KAAK,iBAAiB;YACpB,OAAO,CAAC,iBAAiB,CAAC,CAAC;QAC7B,KAAK,sBAAsB;YACzB,OAAO,CAAC,sBAAsB,CAAC,CAAC;QAClC,KAAK,2BAA2B;YAC9B,OAAO,CAAC,2BAA2B,CAAC,CAAC;QACvC,KAAK,oBAAoB;YACvB,OAAO,CAAC,oBAAoB,CAAC,CAAC;QAChC,KAAK,cAAc;YACjB,OAAO,CAAC,cAAc,CAAC,CAAC;QAC1B,KAAK,cAAc;YACjB,OAAO,CAAC,cAAc,CAAC,CAAC;QAC1B,KAAK,cAAc;YACjB,OAAO,CAAC,cAAc,CAAC,CAAC;QAC1B,KAAK,oBAAoB;YACvB,OAAO,CAAC,oBAAoB,CAAC,CAAC;QAChC,KAAK,oBAAoB;YACvB,OAAO,CAAC,oBAAoB,CAAC,CAAC;QAChC;YACE,OAAO,EAAE,CAAC;IACd,CAAC;AACH,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,cAAc,CAAC,MAAc,EAAE,SAAqB;IAClE,MAAM,OAAO,GAAG,IAAI,GAAG,EAAU,CAAC;IAClC,KAAK,MAAM,EAAE,IAAI,SAAS,EAAE,CAAC;QAC3B,KAAK,MAAM,IAAI,IAAI,EAAE,CAAC,KAAK,EAAE,CAAC;YAC5B,KAAK,MAAM,CAAC,IAAI,UAAU,CAAC,IAAI,CAAC;gBAAE,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;QACnD,CAAC;IACH,CAAC;IACD,MAAM,UAAU,GAAG,CAAC,GAAG,OAAO,CAAC,CAAC,IAAI,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAElD,MAAM,MAAM,GAAG;;WAEN,MAAM;;;;;;WAMN,UAAU;CACpB,CAAC;IAEA,MAAM,IAAI,GAAG,SAAS,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE;QAC9B,MAAM,SAAS,GAAG,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACtD,OAAO;OACJ,IAAI,CAAC,SAAS,CAAC,EAAE,CAAC,IAAI,CAAC;EAC5B,SAAS;IACP,CAAC;IACH,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAEd,OAAO,MAAM,GAAG,IAAI,GAAG,IAAI,CAAC;AAC9B,CAAC"}
|
|
@@ -43,9 +43,12 @@ export const WS_URL = apiUrl
|
|
|
43
43
|
: \`\${window.location.protocol === 'https:' ? 'wss:' : 'ws:'}/\${window.location.host}/ws\`;
|
|
44
44
|
|
|
45
45
|
/**
|
|
46
|
-
* Generic API request helper
|
|
46
|
+
* Generic API request helper \u2014 exported so view components can
|
|
47
|
+
* use it as a drop-in replacement for raw fetch calls. Always
|
|
48
|
+
* routes through this module so the conformance check
|
|
49
|
+
* \`no-raw-fetch-in-views\` stays green.
|
|
47
50
|
*/
|
|
48
|
-
async function apiRequest<T = any>(
|
|
51
|
+
export async function apiRequest<T = any>(
|
|
49
52
|
method: string,
|
|
50
53
|
path: string,
|
|
51
54
|
body: any = null
|
|
@@ -21,6 +21,17 @@ export default defineConfig({
|
|
|
21
21
|
target: '${apiProxy}',
|
|
22
22
|
changeOrigin: true,
|
|
23
23
|
},
|
|
24
|
+
// WebSocket proxy \u2014 the runtime's useEntitySync hook opens
|
|
25
|
+
// ws://<host>/ws to receive entity mutation events from the
|
|
26
|
+
// backend event bus. Without this proxy the socket tries to
|
|
27
|
+
// connect to the vite dev server on /ws and fails, forcing
|
|
28
|
+
// state-sync to fall back to React Query's refetchInterval
|
|
29
|
+
// polling (usable but slow).
|
|
30
|
+
'/ws': {
|
|
31
|
+
target: '${apiProxy.replace(/^http/, "ws")}',
|
|
32
|
+
ws: true,
|
|
33
|
+
changeOrigin: true,
|
|
34
|
+
},
|
|
24
35
|
},
|
|
25
36
|
},
|
|
26
37
|
build: {
|
package/dist/libs/instance-factories/scaffolding/templates/generic/package-json-generator.js
CHANGED
|
@@ -29,6 +29,13 @@ function generatePackageJson(context) {
|
|
|
29
29
|
"test": "npm test --workspaces",
|
|
30
30
|
"test:backend": `npm test --workspace=${backendDir}`,
|
|
31
31
|
"test:frontend": `npm test --workspace=${frontendDir}`,
|
|
32
|
+
// UI contract tests — auto-generated from spec inference rules.
|
|
33
|
+
// Assumes backend + frontend are already running (see docs/proposals/UI-CONTRACT-INFERENCE.md).
|
|
34
|
+
"test:contract": "playwright test --config=playwright.contract.config.ts",
|
|
35
|
+
// Implementation conformance — static grep-level checks that
|
|
36
|
+
// verify the runtime implementation has no escape hatches
|
|
37
|
+
// violating the design contract. Fast (millis), no browser.
|
|
38
|
+
"test:conformance": "node scripts/test-conformance.mjs",
|
|
32
39
|
// Database management (backend)
|
|
33
40
|
"db:setup": `npm run db:setup --workspace=${backendDir}`,
|
|
34
41
|
"db:generate": `npm run db:generate --workspace=${backendDir}`,
|
|
@@ -39,6 +46,9 @@ function generatePackageJson(context) {
|
|
|
39
46
|
"lint": "npm run lint --workspaces",
|
|
40
47
|
"lint:fix": "npm run lint:fix --workspaces"
|
|
41
48
|
};
|
|
49
|
+
pkg.devDependencies = {
|
|
50
|
+
"@playwright/test": "^1.40.0"
|
|
51
|
+
};
|
|
42
52
|
} else {
|
|
43
53
|
pkg.scripts = {
|
|
44
54
|
...aggregated.scripts,
|
|
@@ -135,13 +135,14 @@ function pluralize(s) {
|
|
|
135
135
|
function generateListView(name, model, lower, plural, api, classified, belongsTo, lifecycle, view) {
|
|
136
136
|
const displayCols = classified.business.slice(0, 5);
|
|
137
137
|
const statusField = lifecycle?.statusField || classified.lifecycle[0]?.name;
|
|
138
|
+
const apiPath = api.replace(/^\/api/, "");
|
|
138
139
|
const relFetches = belongsTo.map((r) => {
|
|
139
140
|
const tLower = r.target.charAt(0).toLowerCase() + r.target.slice(1);
|
|
140
141
|
return ` const [${tLower}Map, set${r.target}Map] = useState<Record<string, any>>({});`;
|
|
141
142
|
}).join("\n");
|
|
142
143
|
const relEffects = belongsTo.map((r) => {
|
|
143
144
|
const tLower = r.target.charAt(0).toLowerCase() + r.target.slice(1);
|
|
144
|
-
return `
|
|
145
|
+
return ` apiRequest('GET', '/${tLower}s').then(data => {
|
|
145
146
|
if (Array.isArray(data)) {
|
|
146
147
|
const m: Record<string, any> = {};
|
|
147
148
|
data.forEach(e => { m[e.id] = e; });
|
|
@@ -156,6 +157,7 @@ function generateListView(name, model, lower, plural, api, classified, belongsTo
|
|
|
156
157
|
});
|
|
157
158
|
return `import { useState, useEffect } from 'react';
|
|
158
159
|
import { Link } from 'react-router-dom';
|
|
160
|
+
import { apiRequest } from '../lib/apiClient';
|
|
159
161
|
import { getEntityDisplayName } from '../lib/field-helpers';
|
|
160
162
|
import { formatValue, formatDate, StatusBadge } from '../lib/view-helpers';
|
|
161
163
|
|
|
@@ -165,7 +167,7 @@ function ${name}() {
|
|
|
165
167
|
${relFetches}
|
|
166
168
|
|
|
167
169
|
useEffect(() => {
|
|
168
|
-
|
|
170
|
+
apiRequest('GET', '${apiPath}').then(data => {
|
|
169
171
|
setItems(Array.isArray(data) ? data : []);
|
|
170
172
|
setLoading(false);
|
|
171
173
|
}).catch(() => setLoading(false));
|
|
@@ -228,6 +230,7 @@ export default ${name};
|
|
|
228
230
|
}
|
|
229
231
|
function generateDetailView(name, model, lower, plural, api, classified, belongsTo, hasMany, lifecycle, view) {
|
|
230
232
|
const statusField = lifecycle?.statusField || classified.lifecycle[0]?.name;
|
|
233
|
+
const apiPath = api.replace(/^\/api/, "");
|
|
231
234
|
const relFetches = belongsTo.map((r) => {
|
|
232
235
|
const tLower = r.target.charAt(0).toLowerCase() + r.target.slice(1);
|
|
233
236
|
return ` const [${tLower}Ref, set${r.target}Ref] = useState<any>(null);`;
|
|
@@ -235,7 +238,7 @@ function generateDetailView(name, model, lower, plural, api, classified, belongs
|
|
|
235
238
|
const relEffects = belongsTo.map((r) => {
|
|
236
239
|
const tLower = r.target.charAt(0).toLowerCase() + r.target.slice(1);
|
|
237
240
|
const fk = `${r.name}_id`;
|
|
238
|
-
return ` if (data.${fk})
|
|
241
|
+
return ` if (data.${fk}) apiRequest('GET', \`/${tLower}s/\${data.${fk}}\`).then(d => set${r.target}Ref(d)).catch(() => {});`;
|
|
239
242
|
}).join("\n");
|
|
240
243
|
const hasManyFetches = hasMany.map((r) => {
|
|
241
244
|
const tLower = r.target.charAt(0).toLowerCase() + r.target.slice(1);
|
|
@@ -243,10 +246,11 @@ function generateDetailView(name, model, lower, plural, api, classified, belongs
|
|
|
243
246
|
}).join("\n");
|
|
244
247
|
const hasManyEffects = hasMany.map((r) => {
|
|
245
248
|
const tLower = r.target.charAt(0).toLowerCase() + r.target.slice(1);
|
|
246
|
-
return `
|
|
249
|
+
return ` apiRequest('GET', \`/${tLower}s?${lower}Id=\${data.id}\`).then(d => set${capitalize(r.name)}Items(Array.isArray(d) ? d : [])).catch(() => {});`;
|
|
247
250
|
}).join("\n");
|
|
248
251
|
return `import { useState, useEffect } from 'react';
|
|
249
252
|
import { Link, useSearchParams, useNavigate } from 'react-router-dom';
|
|
253
|
+
import { apiRequest } from '../lib/apiClient';
|
|
250
254
|
import { getEntityDisplayName } from '../lib/field-helpers';
|
|
251
255
|
import { formatValue, formatDate, StatusBadge } from '../lib/view-helpers';
|
|
252
256
|
|
|
@@ -261,7 +265,7 @@ ${hasManyFetches}
|
|
|
261
265
|
|
|
262
266
|
useEffect(() => {
|
|
263
267
|
if (!id) { setLoading(false); return; }
|
|
264
|
-
|
|
268
|
+
apiRequest('GET', \`${apiPath}/\${id}\`).then(data => {
|
|
265
269
|
setItem(data);
|
|
266
270
|
setLoading(false);
|
|
267
271
|
if (data) {
|
|
@@ -276,7 +280,7 @@ ${hasManyEffects}
|
|
|
276
280
|
|
|
277
281
|
const handleDelete = async () => {
|
|
278
282
|
if (!confirm('Delete this ${lower}?')) return;
|
|
279
|
-
await
|
|
283
|
+
await apiRequest('DELETE', \`${apiPath}/\${id}\`);
|
|
280
284
|
navigate('/${lower}list');
|
|
281
285
|
};
|
|
282
286
|
|
|
@@ -358,12 +362,13 @@ export default ${name};
|
|
|
358
362
|
function generateFormView(name, model, lower, plural, api, classified, belongsTo, lifecycle, view) {
|
|
359
363
|
const editableFields = [...classified.business, ...classified.lifecycle];
|
|
360
364
|
const statusField = lifecycle?.statusField;
|
|
365
|
+
const apiPath = api.replace(/^\/api/, "");
|
|
361
366
|
const relStates = belongsTo.map((r) => {
|
|
362
367
|
return ` const [${r.target.charAt(0).toLowerCase() + r.target.slice(1)}Options, set${r.target}Options] = useState<any[]>([]);`;
|
|
363
368
|
}).join("\n");
|
|
364
369
|
const relEffects = belongsTo.map((r) => {
|
|
365
370
|
const tLower = r.target.charAt(0).toLowerCase() + r.target.slice(1);
|
|
366
|
-
return `
|
|
371
|
+
return ` apiRequest('GET', '/${tLower}s').then(d => set${r.target}Options(Array.isArray(d) ? d : [])).catch(() => {});`;
|
|
367
372
|
}).join("\n");
|
|
368
373
|
function inputForField(attr) {
|
|
369
374
|
const { name: n, type, required, values } = attr;
|
|
@@ -390,6 +395,7 @@ ${lifecycle.states.map((s) => ` <option value="${s}">${s.replace(/[_-]/
|
|
|
390
395
|
}
|
|
391
396
|
return `import { useState, useEffect } from 'react';
|
|
392
397
|
import { useSearchParams, useNavigate } from 'react-router-dom';
|
|
398
|
+
import { apiRequest } from '../lib/apiClient';
|
|
393
399
|
import { getEntityDisplayName } from '../lib/field-helpers';
|
|
394
400
|
|
|
395
401
|
function ${name}() {
|
|
@@ -403,7 +409,7 @@ ${relStates}
|
|
|
403
409
|
|
|
404
410
|
useEffect(() => {
|
|
405
411
|
if (editId) {
|
|
406
|
-
|
|
412
|
+
apiRequest('GET', \`${apiPath}/\${editId}\`).then(data => setForm(data || {})).catch(() => {});
|
|
407
413
|
}
|
|
408
414
|
${relEffects}
|
|
409
415
|
}, [editId]);
|
|
@@ -418,17 +424,12 @@ ${relEffects}
|
|
|
418
424
|
setSaving(true);
|
|
419
425
|
setError('');
|
|
420
426
|
try {
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
headers: { 'Content-Type': 'application/json' },
|
|
426
|
-
body: JSON.stringify(form),
|
|
427
|
-
});
|
|
428
|
-
if (!res.ok) {
|
|
429
|
-
const err = await res.json().catch(() => ({}));
|
|
430
|
-
throw new Error(err.message || 'Save failed');
|
|
427
|
+
if (editId) {
|
|
428
|
+
await apiRequest('PUT', \`${apiPath}/\${editId}\`, form);
|
|
429
|
+
} else {
|
|
430
|
+
await apiRequest('POST', '${apiPath}', form);
|
|
431
431
|
}
|
|
432
|
+
// apiRequest throws on non-2xx so reaching here means success.
|
|
432
433
|
navigate('/${lower}list');
|
|
433
434
|
} catch (err: any) {
|
|
434
435
|
setError(err.message);
|
|
@@ -490,7 +491,9 @@ export default ${name};
|
|
|
490
491
|
function generateDashboardView(name, model, lower, plural, api, classified, view, modelDef) {
|
|
491
492
|
const numericAttrs = classified.business.filter((a) => ["integer", "number", "money", "decimal", "float"].includes(a.type.toLowerCase()));
|
|
492
493
|
const metricNames = numericAttrs.map((a) => a.name).slice(0, 4);
|
|
494
|
+
const apiPath = api.replace(/^\/api/, "");
|
|
493
495
|
return `import { useState, useEffect } from 'react';
|
|
496
|
+
import { apiRequest } from '../lib/apiClient';
|
|
494
497
|
import { getEntityDisplayName } from '../lib/field-helpers';
|
|
495
498
|
import { formatDate, StatusBadge } from '../lib/view-helpers';
|
|
496
499
|
|
|
@@ -499,7 +502,7 @@ function ${name}() {
|
|
|
499
502
|
const [loading, setLoading] = useState(true);
|
|
500
503
|
|
|
501
504
|
useEffect(() => {
|
|
502
|
-
|
|
505
|
+
apiRequest('GET', '${apiPath}').then(data => {
|
|
503
506
|
setItems(Array.isArray(data) ? data : []);
|
|
504
507
|
setLoading(false);
|
|
505
508
|
}).catch(() => setLoading(false));
|
|
@@ -552,8 +555,10 @@ export default ${name};
|
|
|
552
555
|
function generateBoardView(name, model, lower, plural, api, lifecycle, view) {
|
|
553
556
|
const states = lifecycle?.states || ["todo", "in_progress", "done"];
|
|
554
557
|
const statusField = lifecycle?.statusField || "status";
|
|
558
|
+
const apiPath = api.replace(/^\/api/, "");
|
|
555
559
|
return `import { useState, useEffect } from 'react';
|
|
556
560
|
import { Link } from 'react-router-dom';
|
|
561
|
+
import { apiRequest } from '../lib/apiClient';
|
|
557
562
|
import { getEntityDisplayName } from '../lib/field-helpers';
|
|
558
563
|
import { statusColor } from '../lib/view-helpers';
|
|
559
564
|
|
|
@@ -562,7 +567,7 @@ function ${name}() {
|
|
|
562
567
|
const [loading, setLoading] = useState(true);
|
|
563
568
|
|
|
564
569
|
useEffect(() => {
|
|
565
|
-
|
|
570
|
+
apiRequest('GET', '${apiPath}').then(data => {
|
|
566
571
|
setItems(Array.isArray(data) ? data : []);
|
|
567
572
|
setLoading(false);
|
|
568
573
|
}).catch(() => setLoading(false));
|
|
@@ -614,8 +619,10 @@ export default ${name};
|
|
|
614
619
|
`;
|
|
615
620
|
}
|
|
616
621
|
function generateTimelineView(name, model, lower, plural, api, view) {
|
|
622
|
+
const apiPath = api.replace(/^\/api/, "");
|
|
617
623
|
return `import { useState, useEffect } from 'react';
|
|
618
624
|
import { Link } from 'react-router-dom';
|
|
625
|
+
import { apiRequest } from '../lib/apiClient';
|
|
619
626
|
import { getEntityDisplayName } from '../lib/field-helpers';
|
|
620
627
|
import { formatDate, StatusBadge } from '../lib/view-helpers';
|
|
621
628
|
|
|
@@ -624,7 +631,7 @@ function ${name}() {
|
|
|
624
631
|
const [loading, setLoading] = useState(true);
|
|
625
632
|
|
|
626
633
|
useEffect(() => {
|
|
627
|
-
|
|
634
|
+
apiRequest('GET', '${apiPath}').then(data => {
|
|
628
635
|
const sorted = (Array.isArray(data) ? data : []).sort((a, b) =>
|
|
629
636
|
new Date(b.createdAt || 0).getTime() - new Date(a.createdAt || 0).getTime()
|
|
630
637
|
);
|
|
@@ -669,7 +676,9 @@ function generateCalendarView(name, model, lower, plural, api, view, modelDef) {
|
|
|
669
676
|
const dateField = attrs.find(
|
|
670
677
|
(a) => ["startdate", "duedate", "scheduledat", "eventdate", "date"].includes(a.name.toLowerCase())
|
|
671
678
|
)?.name || "createdAt";
|
|
679
|
+
const apiPath = api.replace(/^\/api/, "");
|
|
672
680
|
return `import { useState, useEffect } from 'react';
|
|
681
|
+
import { apiRequest } from '../lib/apiClient';
|
|
673
682
|
import { getEntityDisplayName } from '../lib/field-helpers';
|
|
674
683
|
|
|
675
684
|
function ${name}() {
|
|
@@ -678,7 +687,7 @@ function ${name}() {
|
|
|
678
687
|
const [currentMonth, setCurrentMonth] = useState(new Date());
|
|
679
688
|
|
|
680
689
|
useEffect(() => {
|
|
681
|
-
|
|
690
|
+
apiRequest('GET', '${apiPath}').then(data => {
|
|
682
691
|
setItems(Array.isArray(data) ? data : []);
|
|
683
692
|
setLoading(false);
|
|
684
693
|
}).catch(() => setLoading(false));
|
|
@@ -737,14 +746,16 @@ export default ${name};
|
|
|
737
746
|
function generateAnalyticsView(name, model, lower, plural, api, classified, lifecycle, view, modelDef) {
|
|
738
747
|
const numericAttrs = classified.business.filter((a) => ["integer", "number", "money", "decimal", "float"].includes(a.type.toLowerCase()));
|
|
739
748
|
const statusField = lifecycle?.statusField || "status";
|
|
749
|
+
const apiPath = api.replace(/^\/api/, "");
|
|
740
750
|
return `import { useState, useEffect } from 'react';
|
|
751
|
+
import { apiRequest } from '../lib/apiClient';
|
|
741
752
|
|
|
742
753
|
function ${name}() {
|
|
743
754
|
const [items, setItems] = useState<any[]>([]);
|
|
744
755
|
const [loading, setLoading] = useState(true);
|
|
745
756
|
|
|
746
757
|
useEffect(() => {
|
|
747
|
-
|
|
758
|
+
apiRequest('GET', '${apiPath}').then(data => {
|
|
748
759
|
setItems(Array.isArray(data) ? data : []);
|
|
749
760
|
setLoading(false);
|
|
750
761
|
}).catch(() => setLoading(false));
|