@cosmicdrift/kumiko-headless 0.328.0 → 0.329.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/dist/apex/css.d.ts +3 -0
- package/{src/apex/css.ts → dist/apex/css.js} +11 -24
- package/dist/apex/index.d.ts +229 -0
- package/dist/apex/index.js +360 -0
- package/dist/apex/lightbox.d.ts +4 -0
- package/{src/apex/lightbox.ts → dist/apex/lightbox.js} +0 -4
- package/dist/contracts/asset.d.ts +15 -0
- package/dist/contracts/asset.js +17 -0
- package/dist/contracts/index.d.ts +3 -0
- package/dist/contracts/index.js +1 -0
- package/dist/contracts/locale.d.ts +7 -0
- package/dist/contracts/locale.js +11 -0
- package/dist/contracts/primitives.d.ts +93 -0
- package/dist/contracts/primitives.js +21 -0
- package/dist/dispatcher/index.d.ts +2 -0
- package/dist/dispatcher/index.js +1 -0
- package/dist/dispatcher/types.d.ts +86 -0
- package/dist/dispatcher/types.js +34 -0
- package/dist/form/form-controller.d.ts +2 -0
- package/dist/form/form-controller.js +404 -0
- package/dist/form/index.d.ts +3 -0
- package/dist/form/index.js +2 -0
- package/dist/form/types.d.ts +71 -0
- package/dist/form/types.js +1 -0
- package/dist/form/zod-bridge.d.ts +5 -0
- package/{src/form/zod-bridge.ts → dist/form/zod-bridge.js} +52 -58
- package/dist/format/escape.d.ts +5 -0
- package/{src/format/escape.ts → dist/format/escape.js} +43 -47
- package/dist/format/html-template.d.ts +9 -0
- package/dist/format/html-template.js +39 -0
- package/dist/format/index.d.ts +10 -0
- package/dist/format/index.js +192 -0
- package/dist/format/money.d.ts +1 -0
- package/{src/format/money.ts → dist/format/money.js} +6 -5
- package/dist/index.d.ts +14 -0
- package/dist/index.js +9 -0
- package/dist/locale-routing/index.d.ts +39 -0
- package/dist/locale-routing/index.js +143 -0
- package/dist/nav/index.d.ts +2 -0
- package/dist/nav/index.js +1 -0
- package/dist/nav/resolve.d.ts +2 -0
- package/dist/nav/resolve.js +58 -0
- package/dist/nav/types.d.ts +26 -0
- package/dist/nav/types.js +1 -0
- package/dist/store/create-store.d.ts +2 -0
- package/{src/store/create-store.ts → dist/store/create-store.js} +21 -23
- package/dist/store/equality.d.ts +1 -0
- package/{src/store/equality.ts → dist/store/equality.js} +19 -17
- package/dist/store/index.d.ts +3 -0
- package/dist/store/index.js +2 -0
- package/dist/store/types.d.ts +7 -0
- package/dist/store/types.js +10 -0
- package/dist/view-model/edit.d.ts +10 -0
- package/dist/view-model/edit.js +392 -0
- package/dist/view-model/embedded-list.d.ts +27 -0
- package/dist/view-model/embedded-list.js +63 -0
- package/dist/view-model/index.d.ts +7 -0
- package/dist/view-model/index.js +3 -0
- package/dist/view-model/list.d.ts +16 -0
- package/dist/view-model/list.js +201 -0
- package/dist/view-model/types.d.ts +300 -0
- package/dist/view-model/types.js +1 -0
- package/package.json +31 -12
- package/src/apex/__tests__/lightbox.test.ts +0 -20
- package/src/apex/__tests__/page-head.test.ts +0 -63
- package/src/apex/__tests__/render.test.ts +0 -401
- package/src/apex/index.ts +0 -667
- package/src/contracts/__tests__/contracts.test.ts +0 -123
- package/src/contracts/__tests__/field-issue-compat.test.ts +0 -19
- package/src/contracts/asset.ts +0 -48
- package/src/contracts/index.ts +0 -23
- package/src/contracts/locale.ts +0 -33
- package/src/contracts/primitives.ts +0 -158
- package/src/dispatcher/__tests__/contract.test.ts +0 -163
- package/src/dispatcher/index.ts +0 -16
- package/src/dispatcher/types.ts +0 -217
- package/src/form/__tests__/conditional-fields.test.ts +0 -177
- package/src/form/__tests__/form-controller.test.ts +0 -231
- package/src/form/__tests__/submit.test.ts +0 -575
- package/src/form/__tests__/validation.test.ts +0 -274
- package/src/form/form-controller.ts +0 -458
- package/src/form/index.ts +0 -15
- package/src/form/types.ts +0 -297
- package/src/format/__tests__/escape.test.ts +0 -79
- package/src/format/__tests__/format.test.ts +0 -285
- package/src/format/__tests__/html-template.test.ts +0 -47
- package/src/format/html-template.ts +0 -49
- package/src/format/index.ts +0 -223
- package/src/index.ts +0 -118
- package/src/locale-routing/__tests__/locale-routing.test.ts +0 -276
- package/src/locale-routing/index.ts +0 -218
- package/src/nav/__tests__/resolve.test.ts +0 -279
- package/src/nav/index.ts +0 -8
- package/src/nav/resolve.ts +0 -60
- package/src/nav/types.ts +0 -70
- package/src/store/__tests__/create-store.test.ts +0 -139
- package/src/store/__tests__/equality.test.ts +0 -66
- package/src/store/index.ts +0 -3
- package/src/store/types.ts +0 -27
- package/src/view-model/__tests__/edit.test.ts +0 -1072
- package/src/view-model/__tests__/embedded-list.test.ts +0 -96
- package/src/view-model/__tests__/field-labels.test.ts +0 -62
- package/src/view-model/__tests__/list.test.ts +0 -439
- package/src/view-model/edit.ts +0 -645
- package/src/view-model/embedded-list.ts +0 -89
- package/src/view-model/index.ts +0 -43
- package/src/view-model/list.ts +0 -280
- package/src/view-model/types.ts +0 -400
|
@@ -1,1072 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import type {
|
|
3
|
-
EntityDefinition,
|
|
4
|
-
EntityEditScreenDefinition,
|
|
5
|
-
RowAction,
|
|
6
|
-
} from "@cosmicdrift/kumiko-framework/ui-types";
|
|
7
|
-
import { computeEditViewModel } from "../edit";
|
|
8
|
-
import type {
|
|
9
|
-
EditFieldsSectionViewModel,
|
|
10
|
-
EditRelatedListSectionViewModel,
|
|
11
|
-
EditSectionViewModel,
|
|
12
|
-
EditWriteFormSectionViewModel,
|
|
13
|
-
} from "../types";
|
|
14
|
-
|
|
15
|
-
function asFields(s: EditSectionViewModel | undefined): EditFieldsSectionViewModel {
|
|
16
|
-
if (s === undefined || s.kind !== "fields") {
|
|
17
|
-
throw new Error(`expected fields-section, got ${s?.kind ?? "undefined"}`);
|
|
18
|
-
}
|
|
19
|
-
return s;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
const orderEntity = {
|
|
23
|
-
fields: {
|
|
24
|
-
customerName: { type: "text", required: true },
|
|
25
|
-
notes: { type: "text" },
|
|
26
|
-
vatExempt: { type: "boolean" },
|
|
27
|
-
vatReason: { type: "text" },
|
|
28
|
-
kind: { type: "text" },
|
|
29
|
-
},
|
|
30
|
-
} as unknown as EntityDefinition;
|
|
31
|
-
|
|
32
|
-
function editScreen(
|
|
33
|
-
layout: EntityEditScreenDefinition["layout"],
|
|
34
|
-
overrides?: Partial<EntityEditScreenDefinition>,
|
|
35
|
-
): EntityEditScreenDefinition {
|
|
36
|
-
return {
|
|
37
|
-
id: "orders:screen:order-edit",
|
|
38
|
-
type: "entityEdit",
|
|
39
|
-
entity: "order",
|
|
40
|
-
layout,
|
|
41
|
-
...overrides,
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
const translate = (key: string) => key;
|
|
46
|
-
|
|
47
|
-
describe("computeEditViewModel", () => {
|
|
48
|
-
test("flat screen: single section, string-form fields → resolved field view models", () => {
|
|
49
|
-
const vm = computeEditViewModel({
|
|
50
|
-
screen: editScreen({
|
|
51
|
-
sections: [{ title: "Hauptdaten", fields: ["customerName", "notes"] }],
|
|
52
|
-
}),
|
|
53
|
-
entity: orderEntity,
|
|
54
|
-
values: { customerName: "Acme", notes: "VIP" },
|
|
55
|
-
translate,
|
|
56
|
-
featureName: "orders",
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
expect(vm.sections).toHaveLength(1);
|
|
60
|
-
const section = vm.sections[0];
|
|
61
|
-
expect(section?.title).toBe("Hauptdaten");
|
|
62
|
-
expect(asFields(section).columns).toBe(1); // default
|
|
63
|
-
expect(asFields(section).fields).toEqual([
|
|
64
|
-
{
|
|
65
|
-
field: "customerName",
|
|
66
|
-
label: "orders:entity:order:field:customerName",
|
|
67
|
-
type: "text",
|
|
68
|
-
value: "Acme",
|
|
69
|
-
visible: true,
|
|
70
|
-
readOnly: false,
|
|
71
|
-
required: true, // from entity
|
|
72
|
-
},
|
|
73
|
-
{
|
|
74
|
-
field: "notes",
|
|
75
|
-
label: "orders:entity:order:field:notes",
|
|
76
|
-
type: "text",
|
|
77
|
-
value: "VIP",
|
|
78
|
-
visible: true,
|
|
79
|
-
readOnly: false,
|
|
80
|
-
required: false,
|
|
81
|
-
},
|
|
82
|
-
]);
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
test("fieldLabels override the convention key per field; absent fields fall back", () => {
|
|
86
|
-
const vm = computeEditViewModel({
|
|
87
|
-
screen: editScreen(
|
|
88
|
-
{ sections: [{ title: "Main", fields: ["customerName", "notes"] }] },
|
|
89
|
-
{ fieldLabels: { customerName: "config:mask:customer-name" } },
|
|
90
|
-
),
|
|
91
|
-
entity: orderEntity,
|
|
92
|
-
values: {},
|
|
93
|
-
translate,
|
|
94
|
-
featureName: "orders",
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
const fields = asFields(vm.sections[0]).fields;
|
|
98
|
-
// overridden field uses the injected i18n key (the Settings-Hub puts mask.title here)
|
|
99
|
-
expect(fields[0]?.label).toBe("config:mask:customer-name");
|
|
100
|
-
// un-overridden field stays on the `<feature>:entity:<entity>:field:<name>` convention
|
|
101
|
-
expect(fields[1]?.label).toBe("orders:entity:order:field:notes");
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
test("section.columns override is respected; defaults to 1 when absent", () => {
|
|
105
|
-
const vm = computeEditViewModel({
|
|
106
|
-
screen: editScreen({
|
|
107
|
-
sections: [
|
|
108
|
-
{ title: "Main", columns: 2, fields: ["customerName"] },
|
|
109
|
-
{ title: "Notes", fields: ["notes"] },
|
|
110
|
-
],
|
|
111
|
-
}),
|
|
112
|
-
entity: orderEntity,
|
|
113
|
-
values: {},
|
|
114
|
-
translate,
|
|
115
|
-
featureName: "orders",
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
expect(asFields(vm.sections[0]).columns).toBe(2);
|
|
119
|
-
expect(asFields(vm.sections[1]).columns).toBe(1);
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
// The two-column default came in with projectionDetail tabs and now also
|
|
123
|
-
// covers entityEdit tabs (fw#3134) — a tab is its own panel, where a stacked
|
|
124
|
-
// form's single column wastes the width.
|
|
125
|
-
test("a tabs layout defaults sections to 2 columns, not 1", () => {
|
|
126
|
-
const vm = computeEditViewModel({
|
|
127
|
-
screen: editScreen({
|
|
128
|
-
mode: "tabs",
|
|
129
|
-
sections: [
|
|
130
|
-
{ id: "main", title: "Main", fields: ["customerName"] },
|
|
131
|
-
{ id: "notes", title: "Notes", columns: 1, fields: ["notes"] },
|
|
132
|
-
],
|
|
133
|
-
}),
|
|
134
|
-
entity: orderEntity,
|
|
135
|
-
values: {},
|
|
136
|
-
translate,
|
|
137
|
-
featureName: "orders",
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
expect(asFields(vm.sections[0]).columns).toBe(2);
|
|
141
|
-
expect(asFields(vm.sections[1]).columns).toBe(1);
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
test("tabs sections carry their declared id into the view model", () => {
|
|
145
|
-
const vm = computeEditViewModel({
|
|
146
|
-
screen: editScreen({
|
|
147
|
-
mode: "tabs",
|
|
148
|
-
sections: [
|
|
149
|
-
{ id: "main", title: "Main", fields: ["customerName"] },
|
|
150
|
-
{ id: "notes", title: "Notes", fields: ["notes"] },
|
|
151
|
-
],
|
|
152
|
-
}),
|
|
153
|
-
entity: orderEntity,
|
|
154
|
-
values: {},
|
|
155
|
-
translate,
|
|
156
|
-
featureName: "orders",
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
expect(asFields(vm.sections[0]).id).toBe("main");
|
|
160
|
-
expect(asFields(vm.sections[1]).id).toBe("notes");
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
test("visible predicate evaluated against current values (live-reactive)", () => {
|
|
164
|
-
const screen = editScreen({
|
|
165
|
-
sections: [
|
|
166
|
-
{
|
|
167
|
-
title: "VAT",
|
|
168
|
-
fields: [
|
|
169
|
-
"vatExempt",
|
|
170
|
-
{
|
|
171
|
-
field: "vatReason",
|
|
172
|
-
visible: { field: "vatExempt", eq: true },
|
|
173
|
-
required: { field: "vatExempt", eq: true },
|
|
174
|
-
},
|
|
175
|
-
],
|
|
176
|
-
},
|
|
177
|
-
],
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
const hidden = computeEditViewModel({
|
|
181
|
-
screen,
|
|
182
|
-
entity: orderEntity,
|
|
183
|
-
values: { vatExempt: false },
|
|
184
|
-
translate,
|
|
185
|
-
featureName: "orders",
|
|
186
|
-
});
|
|
187
|
-
const reasonHidden = asFields(hidden.sections[0]).fields[1];
|
|
188
|
-
expect(reasonHidden?.visible).toBe(false);
|
|
189
|
-
expect(reasonHidden?.required).toBe(false);
|
|
190
|
-
|
|
191
|
-
const shown = computeEditViewModel({
|
|
192
|
-
screen,
|
|
193
|
-
entity: orderEntity,
|
|
194
|
-
values: { vatExempt: true },
|
|
195
|
-
translate,
|
|
196
|
-
featureName: "orders",
|
|
197
|
-
});
|
|
198
|
-
const reasonShown = asFields(shown.sections[0]).fields[1];
|
|
199
|
-
expect(reasonShown?.visible).toBe(true);
|
|
200
|
-
expect(reasonShown?.required).toBe(true);
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
test("section with all fields hidden by visible condition → section.visible is false but section stays in the array (key stability)", () => {
|
|
204
|
-
const screen = editScreen({
|
|
205
|
-
sections: [
|
|
206
|
-
{
|
|
207
|
-
title: "Person",
|
|
208
|
-
fields: [{ field: "customerName", visible: { field: "kind", eq: "person" } }],
|
|
209
|
-
},
|
|
210
|
-
{
|
|
211
|
-
title: "Firma",
|
|
212
|
-
fields: [{ field: "notes", visible: { field: "kind", eq: "company" } }],
|
|
213
|
-
},
|
|
214
|
-
],
|
|
215
|
-
});
|
|
216
|
-
|
|
217
|
-
const vm = computeEditViewModel({
|
|
218
|
-
screen,
|
|
219
|
-
entity: orderEntity,
|
|
220
|
-
values: { kind: "person", customerName: "Acme" },
|
|
221
|
-
translate,
|
|
222
|
-
featureName: "orders",
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
expect(vm.sections).toHaveLength(2);
|
|
226
|
-
expect(asFields(vm.sections[0]).visible).toBe(true);
|
|
227
|
-
expect(asFields(vm.sections[1]).visible).toBe(false);
|
|
228
|
-
expect(vm.sections[1]?.title).toBe("Firma");
|
|
229
|
-
});
|
|
230
|
-
|
|
231
|
-
test("readonly condition { field, ne } evaluates against form values", () => {
|
|
232
|
-
const screen = editScreen({
|
|
233
|
-
sections: [
|
|
234
|
-
{
|
|
235
|
-
title: "x",
|
|
236
|
-
fields: [{ field: "customerName", readOnly: { field: "isEditable", ne: true } }],
|
|
237
|
-
},
|
|
238
|
-
],
|
|
239
|
-
});
|
|
240
|
-
|
|
241
|
-
const locked = computeEditViewModel({
|
|
242
|
-
screen,
|
|
243
|
-
entity: orderEntity,
|
|
244
|
-
values: { customerName: "A", isEditable: false },
|
|
245
|
-
translate,
|
|
246
|
-
featureName: "orders",
|
|
247
|
-
});
|
|
248
|
-
expect(asFields(locked.sections[0]).fields[0]?.readOnly).toBe(true);
|
|
249
|
-
|
|
250
|
-
const editable = computeEditViewModel({
|
|
251
|
-
screen,
|
|
252
|
-
entity: orderEntity,
|
|
253
|
-
values: { customerName: "A", isEditable: true },
|
|
254
|
-
translate,
|
|
255
|
-
featureName: "orders",
|
|
256
|
-
});
|
|
257
|
-
expect(asFields(editable.sections[0]).fields[0]?.readOnly).toBe(false);
|
|
258
|
-
});
|
|
259
|
-
|
|
260
|
-
test("screen-level required override wins over entity-level required", () => {
|
|
261
|
-
// customerName is required:true on the entity. Screen marks it
|
|
262
|
-
// required:false — a short-form wizard might collect less up-front.
|
|
263
|
-
const vm = computeEditViewModel({
|
|
264
|
-
screen: editScreen({
|
|
265
|
-
sections: [
|
|
266
|
-
{
|
|
267
|
-
title: "x",
|
|
268
|
-
fields: [{ field: "customerName", required: false }],
|
|
269
|
-
},
|
|
270
|
-
],
|
|
271
|
-
}),
|
|
272
|
-
entity: orderEntity,
|
|
273
|
-
values: {},
|
|
274
|
-
translate,
|
|
275
|
-
featureName: "orders",
|
|
276
|
-
});
|
|
277
|
-
|
|
278
|
-
expect(asFields(vm.sections[0]).fields[0]?.required).toBe(false);
|
|
279
|
-
});
|
|
280
|
-
|
|
281
|
-
test("id is extracted from values or null on create (no existing row)", () => {
|
|
282
|
-
const create = computeEditViewModel({
|
|
283
|
-
screen: editScreen({ sections: [{ title: "x", fields: ["customerName"] }] }),
|
|
284
|
-
entity: orderEntity,
|
|
285
|
-
values: { customerName: "new" },
|
|
286
|
-
translate,
|
|
287
|
-
featureName: "orders",
|
|
288
|
-
});
|
|
289
|
-
expect(create.id).toBeNull();
|
|
290
|
-
|
|
291
|
-
const edit = computeEditViewModel({
|
|
292
|
-
screen: editScreen({ sections: [{ title: "x", fields: ["customerName"] }] }),
|
|
293
|
-
entity: orderEntity,
|
|
294
|
-
values: { id: "o-1", customerName: "existing" },
|
|
295
|
-
translate,
|
|
296
|
-
featureName: "orders",
|
|
297
|
-
});
|
|
298
|
-
expect(edit.id).toBe("o-1");
|
|
299
|
-
});
|
|
300
|
-
|
|
301
|
-
test("unknown field in a section throws — no silent-ignore of a typo", () => {
|
|
302
|
-
expect(() =>
|
|
303
|
-
computeEditViewModel({
|
|
304
|
-
screen: editScreen({ sections: [{ title: "x", fields: ["ghost"] }] }),
|
|
305
|
-
entity: orderEntity,
|
|
306
|
-
values: {},
|
|
307
|
-
translate,
|
|
308
|
-
featureName: "orders",
|
|
309
|
-
}),
|
|
310
|
-
).toThrow(/unknown field "ghost"/);
|
|
311
|
-
});
|
|
312
|
-
|
|
313
|
-
test("slots pass through for the renderer to mount", () => {
|
|
314
|
-
const slots = { header: { react: "H" } };
|
|
315
|
-
const vm = computeEditViewModel({
|
|
316
|
-
screen: editScreen({ sections: [{ title: "x", fields: ["customerName"] }] }, { slots }),
|
|
317
|
-
entity: orderEntity,
|
|
318
|
-
values: {},
|
|
319
|
-
translate,
|
|
320
|
-
featureName: "orders",
|
|
321
|
-
});
|
|
322
|
-
expect(vm.slots).toBe(slots);
|
|
323
|
-
});
|
|
324
|
-
|
|
325
|
-
test("span on field-spec propagates to the view model", () => {
|
|
326
|
-
const vm = computeEditViewModel({
|
|
327
|
-
screen: editScreen({
|
|
328
|
-
sections: [{ title: "x", columns: 3, fields: [{ field: "customerName", span: 2 }] }],
|
|
329
|
-
}),
|
|
330
|
-
entity: orderEntity,
|
|
331
|
-
values: {},
|
|
332
|
-
translate,
|
|
333
|
-
featureName: "orders",
|
|
334
|
-
});
|
|
335
|
-
expect(asFields(vm.sections[0]).fields[0]?.span).toBe(2);
|
|
336
|
-
});
|
|
337
|
-
|
|
338
|
-
test("section.description is translated and passed through; absent stays undefined", () => {
|
|
339
|
-
const vm = computeEditViewModel({
|
|
340
|
-
screen: editScreen({
|
|
341
|
-
sections: [
|
|
342
|
-
{
|
|
343
|
-
title: "Basics",
|
|
344
|
-
description: "orders:screen:order-edit.basics.help",
|
|
345
|
-
fields: ["notes"],
|
|
346
|
-
},
|
|
347
|
-
{ title: "Extra", fields: ["kind"] },
|
|
348
|
-
],
|
|
349
|
-
}),
|
|
350
|
-
entity: orderEntity,
|
|
351
|
-
values: {},
|
|
352
|
-
translate,
|
|
353
|
-
featureName: "orders",
|
|
354
|
-
});
|
|
355
|
-
expect(asFields(vm.sections[0]).description).toBe("orders:screen:order-edit.basics.help");
|
|
356
|
-
expect(asFields(vm.sections[1]).description).toBeUndefined();
|
|
357
|
-
});
|
|
358
|
-
|
|
359
|
-
test("field.icon propagates to the view model; absent stays undefined", () => {
|
|
360
|
-
const vm = computeEditViewModel({
|
|
361
|
-
screen: editScreen({
|
|
362
|
-
sections: [{ title: "x", fields: [{ field: "customerName", icon: "user" }, "notes"] }],
|
|
363
|
-
}),
|
|
364
|
-
entity: orderEntity,
|
|
365
|
-
values: {},
|
|
366
|
-
translate,
|
|
367
|
-
featureName: "orders",
|
|
368
|
-
});
|
|
369
|
-
const fields = asFields(vm.sections[0]).fields;
|
|
370
|
-
expect(fields[0]?.icon).toBe("user");
|
|
371
|
-
expect(fields[1]?.icon).toBeUndefined();
|
|
372
|
-
});
|
|
373
|
-
});
|
|
374
|
-
|
|
375
|
-
describe("computeEditViewModel — date/timestamp min/max/locale (#369)", () => {
|
|
376
|
-
test("date-Field reicht min/max/locale ins ViewModel", () => {
|
|
377
|
-
const entity = {
|
|
378
|
-
fields: {
|
|
379
|
-
born: { type: "date", min: "1900-01-01", max: "2026-06-15", locale: "en-US" },
|
|
380
|
-
},
|
|
381
|
-
} as unknown as EntityDefinition;
|
|
382
|
-
const vm = computeEditViewModel({
|
|
383
|
-
screen: editScreen({ sections: [{ title: "X", fields: ["born"] }] }),
|
|
384
|
-
entity,
|
|
385
|
-
values: {},
|
|
386
|
-
translate,
|
|
387
|
-
featureName: "orders",
|
|
388
|
-
});
|
|
389
|
-
const field = asFields(vm.sections[0]).fields[0];
|
|
390
|
-
expect(field?.min).toBe("1900-01-01");
|
|
391
|
-
expect(field?.max).toBe("2026-06-15");
|
|
392
|
-
expect(field?.dateLocale).toBe("en-US");
|
|
393
|
-
});
|
|
394
|
-
|
|
395
|
-
test("date-Field ohne bounds: min/max/dateLocale bleiben weg", () => {
|
|
396
|
-
const entity = {
|
|
397
|
-
fields: { born: { type: "date" } },
|
|
398
|
-
} as unknown as EntityDefinition;
|
|
399
|
-
const vm = computeEditViewModel({
|
|
400
|
-
screen: editScreen({ sections: [{ title: "X", fields: ["born"] }] }),
|
|
401
|
-
entity,
|
|
402
|
-
values: {},
|
|
403
|
-
translate,
|
|
404
|
-
featureName: "orders",
|
|
405
|
-
});
|
|
406
|
-
const field = asFields(vm.sections[0]).fields[0];
|
|
407
|
-
expect(field?.min).toBeUndefined();
|
|
408
|
-
expect(field?.max).toBeUndefined();
|
|
409
|
-
expect(field?.dateLocale).toBeUndefined();
|
|
410
|
-
});
|
|
411
|
-
});
|
|
412
|
-
|
|
413
|
-
describe("computeEditViewModel — embedded-list cells (#1835)", () => {
|
|
414
|
-
const lineFieldSchema = {
|
|
415
|
-
description: { type: "text", required: true },
|
|
416
|
-
quantity: { type: "number", required: true },
|
|
417
|
-
unit: { type: "select", options: ["hour", "day"] },
|
|
418
|
-
product: { type: "reference", entity: "product", labelField: "name" },
|
|
419
|
-
};
|
|
420
|
-
|
|
421
|
-
function embeddedListEntity(overrides?: Record<string, unknown>): EntityDefinition {
|
|
422
|
-
return {
|
|
423
|
-
fields: {
|
|
424
|
-
lines: {
|
|
425
|
-
type: "embedded",
|
|
426
|
-
multiple: true,
|
|
427
|
-
schema: lineFieldSchema,
|
|
428
|
-
...overrides,
|
|
429
|
-
},
|
|
430
|
-
},
|
|
431
|
-
} as unknown as EntityDefinition;
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
test("multiple: true embedded field → embeddedListCells has one entry per schema key with correct type/required/options/optionLabels/refEntity", () => {
|
|
435
|
-
const vm = computeEditViewModel({
|
|
436
|
-
screen: editScreen({ sections: [{ title: "x", fields: ["lines"] }] }),
|
|
437
|
-
entity: embeddedListEntity(),
|
|
438
|
-
values: { lines: [] },
|
|
439
|
-
translate,
|
|
440
|
-
featureName: "orders",
|
|
441
|
-
});
|
|
442
|
-
|
|
443
|
-
const field = asFields(vm.sections[0]).fields[0];
|
|
444
|
-
expect(field?.type).toBe("embedded");
|
|
445
|
-
const cells = field?.embeddedListCells;
|
|
446
|
-
expect(cells).toHaveLength(4);
|
|
447
|
-
|
|
448
|
-
const byField = Object.fromEntries((cells ?? []).map((cell) => [cell.field, cell]));
|
|
449
|
-
expect(byField["description"]).toMatchObject({
|
|
450
|
-
type: "text",
|
|
451
|
-
required: true,
|
|
452
|
-
label: "orders:entity:order:field:lines:cell:description",
|
|
453
|
-
});
|
|
454
|
-
expect(byField["quantity"]).toMatchObject({ type: "number", required: true });
|
|
455
|
-
expect(byField["unit"]).toMatchObject({
|
|
456
|
-
type: "select",
|
|
457
|
-
required: false,
|
|
458
|
-
options: ["hour", "day"],
|
|
459
|
-
optionLabels: { hour: "hour", day: "day" },
|
|
460
|
-
});
|
|
461
|
-
expect(byField["product"]).toMatchObject({
|
|
462
|
-
type: "reference",
|
|
463
|
-
required: false,
|
|
464
|
-
refEntity: "product",
|
|
465
|
-
refFeature: "orders",
|
|
466
|
-
refLabelField: "name",
|
|
467
|
-
});
|
|
468
|
-
expect(byField["product"]?.refOptionsQuery).toBeUndefined();
|
|
469
|
-
});
|
|
470
|
-
|
|
471
|
-
test("a reference sub-field's optionsQuery reaches the cell view-model (fw#2780)", () => {
|
|
472
|
-
const vm = computeEditViewModel({
|
|
473
|
-
screen: editScreen({ sections: [{ title: "x", fields: ["lines"] }] }),
|
|
474
|
-
entity: embeddedListEntity({
|
|
475
|
-
schema: {
|
|
476
|
-
...lineFieldSchema,
|
|
477
|
-
product: {
|
|
478
|
-
type: "reference",
|
|
479
|
-
entity: "product",
|
|
480
|
-
labelField: "name",
|
|
481
|
-
optionsQuery: "orders:query:product:options",
|
|
482
|
-
},
|
|
483
|
-
},
|
|
484
|
-
}),
|
|
485
|
-
values: { lines: [] },
|
|
486
|
-
translate,
|
|
487
|
-
featureName: "orders",
|
|
488
|
-
});
|
|
489
|
-
|
|
490
|
-
const cells = asFields(vm.sections[0]).fields[0]?.embeddedListCells;
|
|
491
|
-
const product = (cells ?? []).find((cell) => cell.field === "product");
|
|
492
|
-
expect(product?.refOptionsQuery).toBe("orders:query:product:options");
|
|
493
|
-
});
|
|
494
|
-
|
|
495
|
-
test("minItems/maxItems/derived/totals pass through onto the field view-model when set", () => {
|
|
496
|
-
const vm = computeEditViewModel({
|
|
497
|
-
screen: editScreen({ sections: [{ title: "x", fields: ["lines"] }] }),
|
|
498
|
-
entity: embeddedListEntity({
|
|
499
|
-
minItems: 1,
|
|
500
|
-
maxItems: 20,
|
|
501
|
-
derived: { total: { op: "multiply", from: ["quantity", "unitPrice"] } },
|
|
502
|
-
totals: ["quantity", "total"],
|
|
503
|
-
}),
|
|
504
|
-
values: { lines: [] },
|
|
505
|
-
translate,
|
|
506
|
-
featureName: "orders",
|
|
507
|
-
});
|
|
508
|
-
|
|
509
|
-
const field = asFields(vm.sections[0]).fields[0];
|
|
510
|
-
expect(field?.embeddedListMinItems).toBe(1);
|
|
511
|
-
expect(field?.embeddedListMaxItems).toBe(20);
|
|
512
|
-
expect(field?.embeddedListDerived).toEqual({
|
|
513
|
-
total: { op: "multiply", from: ["quantity", "unitPrice"] },
|
|
514
|
-
});
|
|
515
|
-
expect(field?.embeddedListTotals).toEqual(["quantity", "total"]);
|
|
516
|
-
});
|
|
517
|
-
|
|
518
|
-
test("minItems/maxItems/derived/totals stay undefined when not declared", () => {
|
|
519
|
-
const vm = computeEditViewModel({
|
|
520
|
-
screen: editScreen({ sections: [{ title: "x", fields: ["lines"] }] }),
|
|
521
|
-
entity: embeddedListEntity(),
|
|
522
|
-
values: { lines: [] },
|
|
523
|
-
translate,
|
|
524
|
-
featureName: "orders",
|
|
525
|
-
});
|
|
526
|
-
|
|
527
|
-
const field = asFields(vm.sections[0]).fields[0];
|
|
528
|
-
expect(field?.embeddedListMinItems).toBeUndefined();
|
|
529
|
-
expect(field?.embeddedListMaxItems).toBeUndefined();
|
|
530
|
-
expect(field?.embeddedListDerived).toBeUndefined();
|
|
531
|
-
expect(field?.embeddedListTotals).toBeUndefined();
|
|
532
|
-
});
|
|
533
|
-
|
|
534
|
-
test("a plain (non-list) embedded field never gets embeddedListCells — that's how the renderer tells it apart from an embedded list", () => {
|
|
535
|
-
const entity = {
|
|
536
|
-
fields: {
|
|
537
|
-
meta: { type: "embedded", schema: { note: { type: "text" } } },
|
|
538
|
-
},
|
|
539
|
-
} as unknown as EntityDefinition;
|
|
540
|
-
|
|
541
|
-
const vm = computeEditViewModel({
|
|
542
|
-
screen: editScreen({ sections: [{ title: "x", fields: ["meta"] }] }),
|
|
543
|
-
entity,
|
|
544
|
-
values: { meta: {} },
|
|
545
|
-
translate,
|
|
546
|
-
featureName: "orders",
|
|
547
|
-
});
|
|
548
|
-
|
|
549
|
-
const field = asFields(vm.sections[0]).fields[0];
|
|
550
|
-
expect(field?.type).toBe("embedded");
|
|
551
|
-
expect(field?.embeddedListCells).toBeUndefined();
|
|
552
|
-
expect(field?.embeddedListMinItems).toBeUndefined();
|
|
553
|
-
});
|
|
554
|
-
|
|
555
|
-
test("select-cell options are translated when registered, fall back to the raw value otherwise", () => {
|
|
556
|
-
const translateUnit = (key: string) =>
|
|
557
|
-
key === "orders:entity:order:field:lines:cell:unit:option:hour" ? "Stunde" : key;
|
|
558
|
-
|
|
559
|
-
const vm = computeEditViewModel({
|
|
560
|
-
screen: editScreen({ sections: [{ title: "x", fields: ["lines"] }] }),
|
|
561
|
-
entity: embeddedListEntity(),
|
|
562
|
-
values: { lines: [] },
|
|
563
|
-
translate: translateUnit,
|
|
564
|
-
featureName: "orders",
|
|
565
|
-
});
|
|
566
|
-
|
|
567
|
-
const field = asFields(vm.sections[0]).fields[0];
|
|
568
|
-
const unitCell = field?.embeddedListCells?.find((cell) => cell.field === "unit");
|
|
569
|
-
expect(unitCell?.optionLabels).toEqual({ hour: "Stunde", day: "day" });
|
|
570
|
-
});
|
|
571
|
-
|
|
572
|
-
test("embeddedListCurrency mirrors entity.defaultCurrency (#1839)", () => {
|
|
573
|
-
const entity = {
|
|
574
|
-
defaultCurrency: "USD",
|
|
575
|
-
fields: {
|
|
576
|
-
lines: { type: "embedded", multiple: true, schema: lineFieldSchema },
|
|
577
|
-
},
|
|
578
|
-
} as unknown as EntityDefinition;
|
|
579
|
-
|
|
580
|
-
const vm = computeEditViewModel({
|
|
581
|
-
screen: editScreen({ sections: [{ title: "x", fields: ["lines"] }] }),
|
|
582
|
-
entity,
|
|
583
|
-
values: { lines: [] },
|
|
584
|
-
translate,
|
|
585
|
-
featureName: "orders",
|
|
586
|
-
});
|
|
587
|
-
|
|
588
|
-
const field = asFields(vm.sections[0]).fields[0];
|
|
589
|
-
expect(field?.embeddedListCurrency).toBe("USD");
|
|
590
|
-
});
|
|
591
|
-
|
|
592
|
-
test("embeddedListCurrency falls back to EUR when the entity has no defaultCurrency (#1839)", () => {
|
|
593
|
-
const vm = computeEditViewModel({
|
|
594
|
-
screen: editScreen({ sections: [{ title: "x", fields: ["lines"] }] }),
|
|
595
|
-
entity: embeddedListEntity(),
|
|
596
|
-
values: { lines: [] },
|
|
597
|
-
translate,
|
|
598
|
-
featureName: "orders",
|
|
599
|
-
});
|
|
600
|
-
|
|
601
|
-
const field = asFields(vm.sections[0]).fields[0];
|
|
602
|
-
expect(field?.embeddedListCurrency).toBe("EUR");
|
|
603
|
-
});
|
|
604
|
-
|
|
605
|
-
test("embeddedListCurrency is undefined for a plain (non-list) embedded field", () => {
|
|
606
|
-
const entity = {
|
|
607
|
-
defaultCurrency: "USD",
|
|
608
|
-
fields: {
|
|
609
|
-
meta: { type: "embedded", schema: { note: { type: "text" } } },
|
|
610
|
-
},
|
|
611
|
-
} as unknown as EntityDefinition;
|
|
612
|
-
|
|
613
|
-
const vm = computeEditViewModel({
|
|
614
|
-
screen: editScreen({ sections: [{ title: "x", fields: ["meta"] }] }),
|
|
615
|
-
entity,
|
|
616
|
-
values: { meta: {} },
|
|
617
|
-
translate,
|
|
618
|
-
featureName: "orders",
|
|
619
|
-
});
|
|
620
|
-
|
|
621
|
-
const field = asFields(vm.sections[0]).fields[0];
|
|
622
|
-
expect(field?.embeddedListCurrency).toBeUndefined();
|
|
623
|
-
});
|
|
624
|
-
|
|
625
|
-
test("imageVariant is the FIRST variant declared on the field def", () => {
|
|
626
|
-
const entity = {
|
|
627
|
-
fields: {
|
|
628
|
-
avatar: {
|
|
629
|
-
type: "image",
|
|
630
|
-
variants: {
|
|
631
|
-
profile: { fit: "cover", size: { width: 512, height: 512 }, format: "webp" },
|
|
632
|
-
big: { fit: "cover", size: { width: 1024, height: 1024 }, format: "webp" },
|
|
633
|
-
},
|
|
634
|
-
},
|
|
635
|
-
},
|
|
636
|
-
} as unknown as EntityDefinition;
|
|
637
|
-
|
|
638
|
-
const vm = computeEditViewModel({
|
|
639
|
-
screen: editScreen({ sections: [{ title: "x", fields: ["avatar"] }] }),
|
|
640
|
-
entity,
|
|
641
|
-
values: {},
|
|
642
|
-
translate,
|
|
643
|
-
featureName: "orders",
|
|
644
|
-
});
|
|
645
|
-
|
|
646
|
-
const field = asFields(vm.sections[0]).fields[0];
|
|
647
|
-
expect(field?.imageVariant).toBe("profile");
|
|
648
|
-
});
|
|
649
|
-
|
|
650
|
-
test("imageVariant is undefined for an image field without variants", () => {
|
|
651
|
-
const entity = {
|
|
652
|
-
fields: {
|
|
653
|
-
avatar: { type: "image" },
|
|
654
|
-
},
|
|
655
|
-
} as unknown as EntityDefinition;
|
|
656
|
-
|
|
657
|
-
const vm = computeEditViewModel({
|
|
658
|
-
screen: editScreen({ sections: [{ title: "x", fields: ["avatar"] }] }),
|
|
659
|
-
entity,
|
|
660
|
-
values: {},
|
|
661
|
-
translate,
|
|
662
|
-
featureName: "orders",
|
|
663
|
-
});
|
|
664
|
-
|
|
665
|
-
const field = asFields(vm.sections[0]).fields[0];
|
|
666
|
-
expect(field?.imageVariant).toBeUndefined();
|
|
667
|
-
});
|
|
668
|
-
|
|
669
|
-
test("capture is forwarded for an image field that declares it", () => {
|
|
670
|
-
const entity = {
|
|
671
|
-
fields: {
|
|
672
|
-
avatar: { type: "image", capture: "environment" },
|
|
673
|
-
},
|
|
674
|
-
} as unknown as EntityDefinition;
|
|
675
|
-
|
|
676
|
-
const vm = computeEditViewModel({
|
|
677
|
-
screen: editScreen({ sections: [{ title: "x", fields: ["avatar"] }] }),
|
|
678
|
-
entity,
|
|
679
|
-
values: {},
|
|
680
|
-
translate,
|
|
681
|
-
featureName: "orders",
|
|
682
|
-
});
|
|
683
|
-
|
|
684
|
-
const field = asFields(vm.sections[0]).fields[0];
|
|
685
|
-
expect(field?.capture).toBe("environment");
|
|
686
|
-
});
|
|
687
|
-
|
|
688
|
-
test("capture is undefined for an image field without it", () => {
|
|
689
|
-
const entity = {
|
|
690
|
-
fields: {
|
|
691
|
-
avatar: { type: "image" },
|
|
692
|
-
},
|
|
693
|
-
} as unknown as EntityDefinition;
|
|
694
|
-
|
|
695
|
-
const vm = computeEditViewModel({
|
|
696
|
-
screen: editScreen({ sections: [{ title: "x", fields: ["avatar"] }] }),
|
|
697
|
-
entity,
|
|
698
|
-
values: {},
|
|
699
|
-
translate,
|
|
700
|
-
featureName: "orders",
|
|
701
|
-
});
|
|
702
|
-
|
|
703
|
-
const field = asFields(vm.sections[0]).fields[0];
|
|
704
|
-
expect(field?.capture).toBeUndefined();
|
|
705
|
-
});
|
|
706
|
-
});
|
|
707
|
-
|
|
708
|
-
describe("computeEditViewModel — multiSelect display/columns/maxRows passthrough", () => {
|
|
709
|
-
function multiSelectEntity(overrides?: Record<string, unknown>): EntityDefinition {
|
|
710
|
-
return {
|
|
711
|
-
fields: {
|
|
712
|
-
languages: {
|
|
713
|
-
type: "multiSelect",
|
|
714
|
-
options: ["en", "de", "es"],
|
|
715
|
-
...overrides,
|
|
716
|
-
},
|
|
717
|
-
},
|
|
718
|
-
} as unknown as EntityDefinition;
|
|
719
|
-
}
|
|
720
|
-
|
|
721
|
-
test("display/columns/maxRows carry through when set on the field def", () => {
|
|
722
|
-
const vm = computeEditViewModel({
|
|
723
|
-
screen: editScreen({ sections: [{ title: "x", fields: ["languages"] }] }),
|
|
724
|
-
entity: multiSelectEntity({ display: "checkboxes", columns: 2, maxRows: 3 }),
|
|
725
|
-
values: {},
|
|
726
|
-
translate,
|
|
727
|
-
featureName: "orders",
|
|
728
|
-
});
|
|
729
|
-
|
|
730
|
-
const field = asFields(vm.sections[0]).fields[0];
|
|
731
|
-
expect(field?.display).toBe("checkboxes");
|
|
732
|
-
expect(field?.columns).toBe(2);
|
|
733
|
-
expect(field?.maxRows).toBe(3);
|
|
734
|
-
});
|
|
735
|
-
|
|
736
|
-
test("display/columns/maxRows are absent when not set on the field def (back-compat)", () => {
|
|
737
|
-
const vm = computeEditViewModel({
|
|
738
|
-
screen: editScreen({ sections: [{ title: "x", fields: ["languages"] }] }),
|
|
739
|
-
entity: multiSelectEntity(),
|
|
740
|
-
values: {},
|
|
741
|
-
translate,
|
|
742
|
-
featureName: "orders",
|
|
743
|
-
});
|
|
744
|
-
|
|
745
|
-
const field = asFields(vm.sections[0]).fields[0];
|
|
746
|
-
expect(field?.display).toBeUndefined();
|
|
747
|
-
expect(field?.columns).toBeUndefined();
|
|
748
|
-
expect(field?.maxRows).toBeUndefined();
|
|
749
|
-
});
|
|
750
|
-
});
|
|
751
|
-
|
|
752
|
-
describe("computeEditViewModel — declared reference metadata (fw#2662)", () => {
|
|
753
|
-
// projectionDetail fields have no real EntityDefinition — the shim that
|
|
754
|
-
// adapts them synthesizes a pseudo-entity where every field is hardcoded
|
|
755
|
-
// to "text" (see projection-detail-shim.ts). EditFieldSpec.refEntity must
|
|
756
|
-
// be evaluated BEFORE the entity.fields lookup, or it never fires against
|
|
757
|
-
// that shape.
|
|
758
|
-
const projectionPseudoEntity = {
|
|
759
|
-
fields: {
|
|
760
|
-
userId: { type: "text" },
|
|
761
|
-
ip: { type: "text" },
|
|
762
|
-
},
|
|
763
|
-
} as unknown as EntityDefinition;
|
|
764
|
-
|
|
765
|
-
test("EditFieldSpec.refEntity marks the field as a reference, even against a pseudo-entity whose fields are all 'text' (fw#2662)", () => {
|
|
766
|
-
const vm = computeEditViewModel({
|
|
767
|
-
screen: editScreen({
|
|
768
|
-
sections: [
|
|
769
|
-
{ fields: [{ field: "userId", refEntity: "user:user", refLabelField: "displayName" }] },
|
|
770
|
-
],
|
|
771
|
-
}),
|
|
772
|
-
entity: projectionPseudoEntity,
|
|
773
|
-
values: { userId: "u-1" },
|
|
774
|
-
translate,
|
|
775
|
-
featureName: "sessions",
|
|
776
|
-
});
|
|
777
|
-
|
|
778
|
-
const field = asFields(vm.sections[0]).fields[0];
|
|
779
|
-
expect(field).toMatchObject({
|
|
780
|
-
field: "userId",
|
|
781
|
-
type: "reference",
|
|
782
|
-
refEntity: "user",
|
|
783
|
-
refFeature: "user",
|
|
784
|
-
refLabelField: "displayName",
|
|
785
|
-
});
|
|
786
|
-
expect(field?.refMultiple).toBeUndefined();
|
|
787
|
-
});
|
|
788
|
-
|
|
789
|
-
test("a reference field's optionsQuery reaches the field view-model, labelField untouched (fw#2780)", () => {
|
|
790
|
-
const entity = {
|
|
791
|
-
fields: {
|
|
792
|
-
leaseId: {
|
|
793
|
-
type: "reference",
|
|
794
|
-
entity: "lease",
|
|
795
|
-
labelField: "startDate",
|
|
796
|
-
optionsQuery: "deposits:query:lease:options",
|
|
797
|
-
},
|
|
798
|
-
},
|
|
799
|
-
} as unknown as EntityDefinition;
|
|
800
|
-
const vm = computeEditViewModel({
|
|
801
|
-
screen: editScreen({ sections: [{ fields: ["leaseId"] }] }),
|
|
802
|
-
entity,
|
|
803
|
-
values: {},
|
|
804
|
-
translate,
|
|
805
|
-
featureName: "deposits",
|
|
806
|
-
});
|
|
807
|
-
|
|
808
|
-
const field = asFields(vm.sections[0]).fields[0];
|
|
809
|
-
expect(field).toMatchObject({
|
|
810
|
-
type: "reference",
|
|
811
|
-
refEntity: "lease",
|
|
812
|
-
refLabelField: "startDate",
|
|
813
|
-
refOptionsQuery: "deposits:query:lease:options",
|
|
814
|
-
});
|
|
815
|
-
});
|
|
816
|
-
|
|
817
|
-
test("non-regression: a projectionDetail field without refEntity metadata stays 'text', unchanged (fw#2662)", () => {
|
|
818
|
-
const vm = computeEditViewModel({
|
|
819
|
-
screen: editScreen({ sections: [{ fields: ["ip"] }] }),
|
|
820
|
-
entity: projectionPseudoEntity,
|
|
821
|
-
values: { ip: "10.0.0.1" },
|
|
822
|
-
translate,
|
|
823
|
-
featureName: "sessions",
|
|
824
|
-
});
|
|
825
|
-
|
|
826
|
-
const field = asFields(vm.sections[0]).fields[0];
|
|
827
|
-
expect(field?.type).toBe("text");
|
|
828
|
-
expect(field?.refEntity).toBeUndefined();
|
|
829
|
-
expect(field?.refFeature).toBeUndefined();
|
|
830
|
-
expect(field?.refLabelField).toBeUndefined();
|
|
831
|
-
});
|
|
832
|
-
});
|
|
833
|
-
|
|
834
|
-
function asRelatedList(s: EditSectionViewModel | undefined): EditRelatedListSectionViewModel {
|
|
835
|
-
if (s === undefined || s.kind !== "relatedList") {
|
|
836
|
-
throw new Error(`expected relatedList-section, got ${s?.kind ?? "undefined"}`);
|
|
837
|
-
}
|
|
838
|
-
return s;
|
|
839
|
-
}
|
|
840
|
-
|
|
841
|
-
function asWriteForm(s: EditSectionViewModel | undefined): EditWriteFormSectionViewModel {
|
|
842
|
-
if (s === undefined || s.kind !== "writeForm") {
|
|
843
|
-
throw new Error(`expected writeForm-section, got ${s?.kind ?? "undefined"}`);
|
|
844
|
-
}
|
|
845
|
-
return s;
|
|
846
|
-
}
|
|
847
|
-
|
|
848
|
-
describe("computeEditViewModel — relatedList rowActions passthrough (fw editable-detail-screens)", () => {
|
|
849
|
-
test("rowActions pass through onto the relatedList section view-model verbatim", () => {
|
|
850
|
-
const rowActions: readonly RowAction[] = [
|
|
851
|
-
{ id: "sync", label: "actions.sync", handler: "app:write:sync", payload: { pick: ["id"] } },
|
|
852
|
-
];
|
|
853
|
-
const vm = computeEditViewModel({
|
|
854
|
-
screen: editScreen({
|
|
855
|
-
sections: [
|
|
856
|
-
{
|
|
857
|
-
kind: "relatedList",
|
|
858
|
-
title: "History",
|
|
859
|
-
query: "app:query:history",
|
|
860
|
-
columns: ["name"],
|
|
861
|
-
rowActions,
|
|
862
|
-
},
|
|
863
|
-
],
|
|
864
|
-
}),
|
|
865
|
-
entity: orderEntity,
|
|
866
|
-
values: {},
|
|
867
|
-
translate,
|
|
868
|
-
featureName: "orders",
|
|
869
|
-
});
|
|
870
|
-
|
|
871
|
-
expect(asRelatedList(vm.sections[0]).rowActions).toEqual(rowActions);
|
|
872
|
-
});
|
|
873
|
-
|
|
874
|
-
test("rowActions is absent from the view-model when the section spec has none", () => {
|
|
875
|
-
const vm = computeEditViewModel({
|
|
876
|
-
screen: editScreen({
|
|
877
|
-
sections: [
|
|
878
|
-
{ kind: "relatedList", title: "History", query: "app:query:history", columns: ["name"] },
|
|
879
|
-
],
|
|
880
|
-
}),
|
|
881
|
-
entity: orderEntity,
|
|
882
|
-
values: {},
|
|
883
|
-
translate,
|
|
884
|
-
featureName: "orders",
|
|
885
|
-
});
|
|
886
|
-
|
|
887
|
-
expect(asRelatedList(vm.sections[0]).rowActions).toBeUndefined();
|
|
888
|
-
});
|
|
889
|
-
});
|
|
890
|
-
|
|
891
|
-
describe("computeEditViewModel — relatedList searchable/facets passthrough (fw#2740)", () => {
|
|
892
|
-
test("searchable and facets pass through onto the relatedList section view-model untranslated", () => {
|
|
893
|
-
const facets = [
|
|
894
|
-
{
|
|
895
|
-
field: "status",
|
|
896
|
-
type: "select" as const,
|
|
897
|
-
label: "app:facet.status.label",
|
|
898
|
-
options: [{ value: "active", label: "app:facet.status.active" }],
|
|
899
|
-
},
|
|
900
|
-
];
|
|
901
|
-
const vm = computeEditViewModel({
|
|
902
|
-
screen: editScreen({
|
|
903
|
-
sections: [
|
|
904
|
-
{
|
|
905
|
-
kind: "relatedList",
|
|
906
|
-
title: "History",
|
|
907
|
-
query: "app:query:history",
|
|
908
|
-
columns: ["name", "status"],
|
|
909
|
-
searchable: true,
|
|
910
|
-
facets,
|
|
911
|
-
},
|
|
912
|
-
],
|
|
913
|
-
}),
|
|
914
|
-
entity: orderEntity,
|
|
915
|
-
values: {},
|
|
916
|
-
translate,
|
|
917
|
-
featureName: "orders",
|
|
918
|
-
});
|
|
919
|
-
|
|
920
|
-
const section = asRelatedList(vm.sections[0]);
|
|
921
|
-
expect(section.searchable).toBe(true);
|
|
922
|
-
expect(section.facets).toEqual(facets);
|
|
923
|
-
});
|
|
924
|
-
|
|
925
|
-
test("searchable and facets are absent from the view-model when the section spec has none", () => {
|
|
926
|
-
const vm = computeEditViewModel({
|
|
927
|
-
screen: editScreen({
|
|
928
|
-
sections: [
|
|
929
|
-
{ kind: "relatedList", title: "History", query: "app:query:history", columns: ["name"] },
|
|
930
|
-
],
|
|
931
|
-
}),
|
|
932
|
-
entity: orderEntity,
|
|
933
|
-
values: {},
|
|
934
|
-
translate,
|
|
935
|
-
featureName: "orders",
|
|
936
|
-
});
|
|
937
|
-
|
|
938
|
-
const section = asRelatedList(vm.sections[0]);
|
|
939
|
-
expect(section.searchable).toBeUndefined();
|
|
940
|
-
expect(section.facets).toBeUndefined();
|
|
941
|
-
});
|
|
942
|
-
});
|
|
943
|
-
|
|
944
|
-
describe("computeEditViewModel — relatedList parentFilter passthrough", () => {
|
|
945
|
-
test("parentFilter passes through onto the relatedList section view-model verbatim", () => {
|
|
946
|
-
const vm = computeEditViewModel({
|
|
947
|
-
screen: editScreen({
|
|
948
|
-
sections: [
|
|
949
|
-
{
|
|
950
|
-
kind: "relatedList",
|
|
951
|
-
title: "History",
|
|
952
|
-
query: "app:query:history",
|
|
953
|
-
columns: ["name"],
|
|
954
|
-
parentFilter: { field: "orderId" },
|
|
955
|
-
},
|
|
956
|
-
],
|
|
957
|
-
}),
|
|
958
|
-
entity: orderEntity,
|
|
959
|
-
values: {},
|
|
960
|
-
translate,
|
|
961
|
-
featureName: "orders",
|
|
962
|
-
});
|
|
963
|
-
|
|
964
|
-
expect(asRelatedList(vm.sections[0]).parentFilter).toEqual({ field: "orderId" });
|
|
965
|
-
});
|
|
966
|
-
|
|
967
|
-
test("parentFilter is absent from the view-model when the section spec has none", () => {
|
|
968
|
-
const vm = computeEditViewModel({
|
|
969
|
-
screen: editScreen({
|
|
970
|
-
sections: [
|
|
971
|
-
{ kind: "relatedList", title: "History", query: "app:query:history", columns: ["name"] },
|
|
972
|
-
],
|
|
973
|
-
}),
|
|
974
|
-
entity: orderEntity,
|
|
975
|
-
values: {},
|
|
976
|
-
translate,
|
|
977
|
-
featureName: "orders",
|
|
978
|
-
});
|
|
979
|
-
|
|
980
|
-
expect(asRelatedList(vm.sections[0]).parentFilter).toBeUndefined();
|
|
981
|
-
});
|
|
982
|
-
});
|
|
983
|
-
|
|
984
|
-
describe("computeEditViewModel — writeForm sections (fw editable-detail-screens)", () => {
|
|
985
|
-
test("resolves the section's own fieldDefs/fields through the same per-field pipeline as a fields section", () => {
|
|
986
|
-
const vm = computeEditViewModel({
|
|
987
|
-
screen: editScreen({
|
|
988
|
-
sections: [
|
|
989
|
-
{
|
|
990
|
-
kind: "writeForm",
|
|
991
|
-
title: "Add note",
|
|
992
|
-
fieldDefs: { note: { type: "text", required: true } },
|
|
993
|
-
fields: ["note"],
|
|
994
|
-
handler: "orders:write:add-note",
|
|
995
|
-
},
|
|
996
|
-
],
|
|
997
|
-
}),
|
|
998
|
-
entity: orderEntity,
|
|
999
|
-
values: {},
|
|
1000
|
-
translate,
|
|
1001
|
-
featureName: "orders",
|
|
1002
|
-
});
|
|
1003
|
-
|
|
1004
|
-
const section = asWriteForm(vm.sections[0]);
|
|
1005
|
-
expect(section.handler).toBe("orders:write:add-note");
|
|
1006
|
-
expect(section.fields).toEqual([
|
|
1007
|
-
{
|
|
1008
|
-
field: "note",
|
|
1009
|
-
label: "orders:entity:__write-form-section__:field:note",
|
|
1010
|
-
type: "text",
|
|
1011
|
-
value: undefined,
|
|
1012
|
-
visible: true,
|
|
1013
|
-
readOnly: false,
|
|
1014
|
-
required: true,
|
|
1015
|
-
},
|
|
1016
|
-
]);
|
|
1017
|
-
});
|
|
1018
|
-
|
|
1019
|
-
test("readOnly/required on a writeForm field come from the section's own fieldDefs, independent of the host entity", () => {
|
|
1020
|
-
const vm = computeEditViewModel({
|
|
1021
|
-
screen: editScreen({
|
|
1022
|
-
sections: [
|
|
1023
|
-
{
|
|
1024
|
-
kind: "writeForm",
|
|
1025
|
-
title: "Add note",
|
|
1026
|
-
// "notes" is optional on orderEntity — the writeForm section's own
|
|
1027
|
-
// fieldDefs declares its own "notes" field as required, and that
|
|
1028
|
-
// (not the host entity's) must win.
|
|
1029
|
-
fieldDefs: { notes: { type: "text", required: true } },
|
|
1030
|
-
fields: ["notes"],
|
|
1031
|
-
handler: "orders:write:add-note",
|
|
1032
|
-
},
|
|
1033
|
-
],
|
|
1034
|
-
}),
|
|
1035
|
-
entity: orderEntity,
|
|
1036
|
-
values: {},
|
|
1037
|
-
translate,
|
|
1038
|
-
featureName: "orders",
|
|
1039
|
-
});
|
|
1040
|
-
|
|
1041
|
-
expect(asWriteForm(vm.sections[0]).fields[0]?.required).toBe(true);
|
|
1042
|
-
});
|
|
1043
|
-
|
|
1044
|
-
test("columns defaults to 1; icon/description/submitLabel are translated and passed through when set", () => {
|
|
1045
|
-
const vm = computeEditViewModel({
|
|
1046
|
-
screen: editScreen({
|
|
1047
|
-
sections: [
|
|
1048
|
-
{
|
|
1049
|
-
kind: "writeForm",
|
|
1050
|
-
title: "Add note",
|
|
1051
|
-
description: "orders:write-form.description",
|
|
1052
|
-
icon: "plus",
|
|
1053
|
-
fieldDefs: { note: { type: "text" } },
|
|
1054
|
-
fields: ["note"],
|
|
1055
|
-
handler: "orders:write:add-note",
|
|
1056
|
-
submitLabel: "orders:write-form.submit",
|
|
1057
|
-
},
|
|
1058
|
-
],
|
|
1059
|
-
}),
|
|
1060
|
-
entity: orderEntity,
|
|
1061
|
-
values: {},
|
|
1062
|
-
translate,
|
|
1063
|
-
featureName: "orders",
|
|
1064
|
-
});
|
|
1065
|
-
|
|
1066
|
-
const section = asWriteForm(vm.sections[0]);
|
|
1067
|
-
expect(section.columns).toBe(1);
|
|
1068
|
-
expect(section.icon).toBe("plus");
|
|
1069
|
-
expect(section.description).toBe("orders:write-form.description");
|
|
1070
|
-
expect(section.submitLabel).toBe("orders:write-form.submit");
|
|
1071
|
-
});
|
|
1072
|
-
});
|