@cosmicdrift/kumiko-headless 0.328.0 → 0.328.1
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
package/src/view-model/edit.ts
DELETED
|
@@ -1,645 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
EditExtensionSection,
|
|
3
|
-
EditRelatedListSection,
|
|
4
|
-
EditWriteFormSection,
|
|
5
|
-
EntityDefinition,
|
|
6
|
-
EntityEditScreenDefinition,
|
|
7
|
-
FieldCondition,
|
|
8
|
-
ParsedRefTarget,
|
|
9
|
-
} from "@cosmicdrift/kumiko-framework/ui-types";
|
|
10
|
-
import {
|
|
11
|
-
evalFieldCondition,
|
|
12
|
-
isExtensionEditSection,
|
|
13
|
-
isFieldsEditSection,
|
|
14
|
-
isWriteFormEditSection,
|
|
15
|
-
normalizeEditField,
|
|
16
|
-
parseRefTarget,
|
|
17
|
-
sectionFieldSpecs,
|
|
18
|
-
WRITE_FORM_SECTION_ENTITY,
|
|
19
|
-
} from "@cosmicdrift/kumiko-framework/ui-types";
|
|
20
|
-
import {
|
|
21
|
-
buildOptionLabels,
|
|
22
|
-
embeddedCellLabelKey,
|
|
23
|
-
embeddedCellOptionLabelKey,
|
|
24
|
-
fieldLabelKey,
|
|
25
|
-
fieldOptionLabelKey,
|
|
26
|
-
} from "./list";
|
|
27
|
-
import type {
|
|
28
|
-
EditFieldViewModel,
|
|
29
|
-
EditRelatedListSectionViewModel,
|
|
30
|
-
EditSectionViewModel,
|
|
31
|
-
EditViewModel,
|
|
32
|
-
EditWriteFormSectionViewModel,
|
|
33
|
-
EmbeddedListCellViewModel,
|
|
34
|
-
Translate,
|
|
35
|
-
} from "./types";
|
|
36
|
-
|
|
37
|
-
// Sub-field shape read off an EmbeddedFieldDef.schema entry. Mirrors
|
|
38
|
-
// EmbeddedSubFieldDef from packages/types/src/fields.ts — headless only
|
|
39
|
-
// depends on @cosmicdrift/kumiko-framework/ui-types (client-safe subset),
|
|
40
|
-
// which doesn't re-export the embedded types, so this stays a local cast
|
|
41
|
-
// shape like every other field-type narrowing in this file.
|
|
42
|
-
type EmbeddedSubFieldShape = {
|
|
43
|
-
readonly type:
|
|
44
|
-
| "text"
|
|
45
|
-
| "number"
|
|
46
|
-
| "boolean"
|
|
47
|
-
| "date"
|
|
48
|
-
| "money"
|
|
49
|
-
| "decimal"
|
|
50
|
-
| "select"
|
|
51
|
-
| "reference"
|
|
52
|
-
| "timestamp";
|
|
53
|
-
readonly required?: boolean;
|
|
54
|
-
readonly options?: readonly string[];
|
|
55
|
-
readonly entity?: string;
|
|
56
|
-
readonly labelField?: string;
|
|
57
|
-
readonly optionsQuery?: string;
|
|
58
|
-
readonly scale?: number;
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
export type ComputeEditViewModelInput<
|
|
62
|
-
TValues extends Readonly<Record<string, unknown>> = Readonly<Record<string, unknown>>,
|
|
63
|
-
> = {
|
|
64
|
-
readonly screen: EntityEditScreenDefinition;
|
|
65
|
-
readonly entity: EntityDefinition;
|
|
66
|
-
readonly values: TValues;
|
|
67
|
-
readonly translate: Translate;
|
|
68
|
-
readonly featureName: string;
|
|
69
|
-
};
|
|
70
|
-
|
|
71
|
-
// Resolves a writeForm section's own fieldDefs/fields through the same
|
|
72
|
-
// per-field pipeline as a plain fields section, by reusing the pipeline
|
|
73
|
-
// itself rather than duplicating it: wrap fieldDefs/fields as a throwaway
|
|
74
|
-
// entityEdit screen (same "synthesize a pseudo-entity" idiom as the shims in
|
|
75
|
-
// packages/renderer/src/app/*-shim.ts) and recurse into computeEditViewModel
|
|
76
|
-
// once. Split out of computeEditViewModel's section-map to keep that
|
|
77
|
-
// function's own complexity from absorbing this branch's decision points.
|
|
78
|
-
function computeWriteFormSectionViewModel<TValues extends Readonly<Record<string, unknown>>>(
|
|
79
|
-
sectionSpec: EditWriteFormSection,
|
|
80
|
-
screenId: string,
|
|
81
|
-
values: TValues,
|
|
82
|
-
translate: Translate,
|
|
83
|
-
featureName: string,
|
|
84
|
-
): EditWriteFormSectionViewModel {
|
|
85
|
-
const innerEntity: EntityDefinition = { fields: sectionSpec.fieldDefs } as EntityDefinition;
|
|
86
|
-
const innerScreen: EntityEditScreenDefinition = {
|
|
87
|
-
id: `${screenId}:${sectionSpec.id ?? "write-form"}`,
|
|
88
|
-
type: "entityEdit",
|
|
89
|
-
entity: WRITE_FORM_SECTION_ENTITY,
|
|
90
|
-
layout: {
|
|
91
|
-
sections: [{ kind: "fields", fields: sectionSpec.fields, columns: sectionSpec.columns }],
|
|
92
|
-
},
|
|
93
|
-
};
|
|
94
|
-
const inner = computeEditViewModel({
|
|
95
|
-
screen: innerScreen,
|
|
96
|
-
entity: innerEntity,
|
|
97
|
-
values,
|
|
98
|
-
translate,
|
|
99
|
-
featureName,
|
|
100
|
-
});
|
|
101
|
-
const innerSection = inner.sections[0];
|
|
102
|
-
const fields = innerSection?.kind === "fields" ? innerSection.fields : [];
|
|
103
|
-
return {
|
|
104
|
-
kind: "writeForm" as const,
|
|
105
|
-
...(sectionSpec.title !== undefined && { title: translate(sectionSpec.title) }),
|
|
106
|
-
...(sectionSpec.description !== undefined && {
|
|
107
|
-
description: translate(sectionSpec.description),
|
|
108
|
-
}),
|
|
109
|
-
columns: sectionSpec.columns ?? 1,
|
|
110
|
-
fields,
|
|
111
|
-
...(sectionSpec.icon !== undefined && { icon: sectionSpec.icon }),
|
|
112
|
-
handler: sectionSpec.handler,
|
|
113
|
-
...(sectionSpec.submitLabel !== undefined && {
|
|
114
|
-
submitLabel: translate(sectionSpec.submitLabel),
|
|
115
|
-
}),
|
|
116
|
-
...(sectionSpec.actions !== undefined && { actions: sectionSpec.actions }),
|
|
117
|
-
};
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// relatedList runs its own query — nothing here to resolve against
|
|
121
|
-
// entity/values, the spec passes through verbatim (fw#2166).
|
|
122
|
-
function computeRelatedListSectionViewModel(
|
|
123
|
-
sectionSpec: EditRelatedListSection,
|
|
124
|
-
translate: Translate,
|
|
125
|
-
): EditRelatedListSectionViewModel {
|
|
126
|
-
return {
|
|
127
|
-
kind: "relatedList" as const,
|
|
128
|
-
title: translate(sectionSpec.title),
|
|
129
|
-
query: sectionSpec.query,
|
|
130
|
-
...(sectionSpec.parentParam !== undefined && { parentParam: sectionSpec.parentParam }),
|
|
131
|
-
...(sectionSpec.parentFilter !== undefined && { parentFilter: sectionSpec.parentFilter }),
|
|
132
|
-
columns: sectionSpec.columns,
|
|
133
|
-
...(sectionSpec.description !== undefined && {
|
|
134
|
-
description: translate(sectionSpec.description),
|
|
135
|
-
}),
|
|
136
|
-
...(sectionSpec.itemNoun !== undefined && { itemNoun: sectionSpec.itemNoun }),
|
|
137
|
-
...(sectionSpec.pageSize !== undefined && { pageSize: sectionSpec.pageSize }),
|
|
138
|
-
...(sectionSpec.defaultSort !== undefined && { defaultSort: sectionSpec.defaultSort }),
|
|
139
|
-
...(sectionSpec.searchable !== undefined && { searchable: sectionSpec.searchable }),
|
|
140
|
-
...(sectionSpec.facets !== undefined && { facets: sectionSpec.facets }),
|
|
141
|
-
...(sectionSpec.rowClick !== undefined && { rowClick: sectionSpec.rowClick }),
|
|
142
|
-
...(sectionSpec.rowActions !== undefined && { rowActions: sectionSpec.rowActions }),
|
|
143
|
-
...(sectionSpec.toolbarActions !== undefined && { toolbarActions: sectionSpec.toolbarActions }),
|
|
144
|
-
...(sectionSpec.actions !== undefined && { actions: sectionSpec.actions }),
|
|
145
|
-
...(sectionSpec.emptyState !== undefined && {
|
|
146
|
-
emptyState: {
|
|
147
|
-
title: translate(sectionSpec.emptyState.title),
|
|
148
|
-
...(sectionSpec.emptyState.description !== undefined && {
|
|
149
|
-
description: translate(sectionSpec.emptyState.description),
|
|
150
|
-
}),
|
|
151
|
-
// Action stays untranslated, same convention as rowActions/
|
|
152
|
-
// toolbarActions above — the renderer's row-actions builder
|
|
153
|
-
// translates `action.label` at button-build time.
|
|
154
|
-
...(sectionSpec.emptyState.action !== undefined && {
|
|
155
|
-
action: sectionSpec.emptyState.action,
|
|
156
|
-
}),
|
|
157
|
-
},
|
|
158
|
-
}),
|
|
159
|
-
};
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
// --- Per-field-type view-model hints ------------------------------------
|
|
163
|
-
// Split out of the field-map callback below (was one function covering
|
|
164
|
-
// every field type's derivation, growing into a single complexity hotspot)
|
|
165
|
-
// so each field-type family stays its own small, independently reviewable
|
|
166
|
-
// unit. Pure data derivation, no side effects — each returns only the keys
|
|
167
|
-
// its own field type ever sets.
|
|
168
|
-
|
|
169
|
-
type EntityFieldDef = NonNullable<EntityDefinition["fields"][string]>;
|
|
170
|
-
type NormalizedEditField = ReturnType<typeof normalizeEditField>;
|
|
171
|
-
|
|
172
|
-
type SelectFieldHints = Pick<
|
|
173
|
-
EditFieldViewModel,
|
|
174
|
-
"options" | "optionLabels" | "display" | "columns" | "maxRows"
|
|
175
|
-
>;
|
|
176
|
-
|
|
177
|
-
function deriveSelectFieldHints(
|
|
178
|
-
fieldDef: EntityFieldDef,
|
|
179
|
-
translate: Translate,
|
|
180
|
-
featureName: string,
|
|
181
|
-
entityName: string,
|
|
182
|
-
fieldName: string,
|
|
183
|
-
): SelectFieldHints {
|
|
184
|
-
const options =
|
|
185
|
-
fieldDef.type === "select" || fieldDef.type === "multiSelect"
|
|
186
|
-
? ((fieldDef as unknown as { options?: readonly string[] }).options ?? [])
|
|
187
|
-
: undefined;
|
|
188
|
-
const optionLabels =
|
|
189
|
-
options !== undefined
|
|
190
|
-
? buildOptionLabels(
|
|
191
|
-
translate,
|
|
192
|
-
(value) => fieldOptionLabelKey(featureName, entityName, fieldName, value),
|
|
193
|
-
options,
|
|
194
|
-
)
|
|
195
|
-
: undefined;
|
|
196
|
-
const display =
|
|
197
|
-
fieldDef.type === "multiSelect" || fieldDef.type === "select" ? fieldDef.display : undefined;
|
|
198
|
-
const columns = fieldDef.type === "multiSelect" ? fieldDef.columns : undefined;
|
|
199
|
-
const maxRows = fieldDef.type === "multiSelect" ? fieldDef.maxRows : undefined;
|
|
200
|
-
return {
|
|
201
|
-
...(options !== undefined && { options }),
|
|
202
|
-
...(optionLabels !== undefined && { optionLabels }),
|
|
203
|
-
...(display !== undefined && { display }),
|
|
204
|
-
...(columns !== undefined && { columns }),
|
|
205
|
-
...(maxRows !== undefined && { maxRows }),
|
|
206
|
-
};
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
type TextFieldHints = Pick<EditFieldViewModel, "multiline" | "format">;
|
|
210
|
-
|
|
211
|
-
function deriveTextFieldHints(fieldDef: EntityFieldDef): TextFieldHints {
|
|
212
|
-
const multiline =
|
|
213
|
-
fieldDef.type === "text" || fieldDef.type === "longText"
|
|
214
|
-
? (fieldDef as unknown as { multiline?: boolean | { rows?: number } }).multiline
|
|
215
|
-
: undefined;
|
|
216
|
-
const format =
|
|
217
|
-
fieldDef.type === "text"
|
|
218
|
-
? (fieldDef as unknown as { format?: "email" | "url" | "phone" | "password" }).format
|
|
219
|
-
: undefined;
|
|
220
|
-
return {
|
|
221
|
-
...(multiline !== undefined && { multiline }),
|
|
222
|
-
...(format !== undefined && { format }),
|
|
223
|
-
};
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
type DateFieldHints = Pick<EditFieldViewModel, "wallClock" | "min" | "max" | "dateLocale">;
|
|
227
|
-
|
|
228
|
-
function deriveDateFieldHints(fieldDef: EntityFieldDef): DateFieldHints {
|
|
229
|
-
const wallClock =
|
|
230
|
-
fieldDef.type === "timestamp" &&
|
|
231
|
-
(fieldDef as unknown as { locatedBy?: string }).locatedBy !== undefined
|
|
232
|
-
? true
|
|
233
|
-
: undefined;
|
|
234
|
-
const dateBounds =
|
|
235
|
-
fieldDef.type === "date" ||
|
|
236
|
-
fieldDef.type === "timestamp" ||
|
|
237
|
-
fieldDef.type === "locatedTimestamp"
|
|
238
|
-
? (fieldDef as unknown as { min?: string; max?: string; locale?: string })
|
|
239
|
-
: undefined;
|
|
240
|
-
return {
|
|
241
|
-
...(wallClock !== undefined && { wallClock }),
|
|
242
|
-
...(dateBounds?.min !== undefined && { min: dateBounds.min }),
|
|
243
|
-
...(dateBounds?.max !== undefined && { max: dateBounds.max }),
|
|
244
|
-
...(dateBounds?.locale !== undefined && { dateLocale: dateBounds.locale }),
|
|
245
|
-
};
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
type NumericFieldHints = Pick<EditFieldViewModel, "grouping" | "currency" | "unit">;
|
|
249
|
-
|
|
250
|
-
function deriveNumericFieldHints(
|
|
251
|
-
fieldDef: EntityFieldDef,
|
|
252
|
-
resolvedCurrency: string,
|
|
253
|
-
): NumericFieldHints {
|
|
254
|
-
const grouping = fieldDef.type === "number" ? fieldDef.grouping : undefined;
|
|
255
|
-
return {
|
|
256
|
-
...(grouping !== undefined && { grouping }),
|
|
257
|
-
...(fieldDef.type === "money" && { currency: resolvedCurrency }),
|
|
258
|
-
...(fieldDef.type === "number" && fieldDef.unit !== undefined && { unit: fieldDef.unit }),
|
|
259
|
-
};
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
type ReferenceFieldHints = Pick<
|
|
263
|
-
EditFieldViewModel,
|
|
264
|
-
"refEntity" | "refFeature" | "refLabelField" | "refOptionsQuery" | "refMultiple"
|
|
265
|
-
>;
|
|
266
|
-
|
|
267
|
-
// Declared reference metadata (EditFieldSpec.refEntity) takes priority over
|
|
268
|
-
// fieldDef.type — see computeEditViewModel's effectiveType, same reasoning.
|
|
269
|
-
function deriveReferenceFieldHints(
|
|
270
|
-
fieldDef: EntityFieldDef,
|
|
271
|
-
normalized: NormalizedEditField,
|
|
272
|
-
declaredRefTarget: ParsedRefTarget | undefined,
|
|
273
|
-
featureName: string,
|
|
274
|
-
): ReferenceFieldHints {
|
|
275
|
-
const refRaw =
|
|
276
|
-
declaredRefTarget === undefined && fieldDef.type === "reference"
|
|
277
|
-
? (fieldDef as unknown as { entity?: string }).entity
|
|
278
|
-
: undefined;
|
|
279
|
-
const refTarget =
|
|
280
|
-
declaredRefTarget ?? (refRaw !== undefined ? parseRefTarget(refRaw, featureName) : undefined);
|
|
281
|
-
const refLabelField =
|
|
282
|
-
declaredRefTarget !== undefined
|
|
283
|
-
? (normalized.refLabelField ?? "id")
|
|
284
|
-
: fieldDef.type === "reference"
|
|
285
|
-
? ((fieldDef as unknown as { labelField?: string }).labelField ?? "id")
|
|
286
|
-
: undefined;
|
|
287
|
-
return {
|
|
288
|
-
...(refTarget?.entityName !== undefined && { refEntity: refTarget.entityName }),
|
|
289
|
-
...(refTarget?.featureName !== undefined && { refFeature: refTarget.featureName }),
|
|
290
|
-
...(refLabelField !== undefined && { refLabelField }),
|
|
291
|
-
...deriveReferenceMultiHints(fieldDef, declaredRefTarget),
|
|
292
|
-
};
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
type ReferenceMultiHints = Pick<EditFieldViewModel, "refOptionsQuery" | "refMultiple">;
|
|
296
|
-
|
|
297
|
-
// Declared reference metadata has no `multiple` or `optionsQuery` concept
|
|
298
|
-
// (it targets row-meta/derived fields, always single-valued and resolved
|
|
299
|
-
// through the target entity) — only a real ReferenceFieldDef carries them.
|
|
300
|
-
function deriveReferenceMultiHints(
|
|
301
|
-
fieldDef: EntityFieldDef,
|
|
302
|
-
declaredRefTarget: ParsedRefTarget | undefined,
|
|
303
|
-
): ReferenceMultiHints {
|
|
304
|
-
const ownReferenceDef =
|
|
305
|
-
declaredRefTarget === undefined && fieldDef.type === "reference"
|
|
306
|
-
? (fieldDef as unknown as { multiple?: boolean; optionsQuery?: string })
|
|
307
|
-
: undefined;
|
|
308
|
-
const refMultiple =
|
|
309
|
-
ownReferenceDef === undefined ? undefined : (ownReferenceDef.multiple ?? false);
|
|
310
|
-
return {
|
|
311
|
-
...(ownReferenceDef?.optionsQuery !== undefined && {
|
|
312
|
-
refOptionsQuery: ownReferenceDef.optionsQuery,
|
|
313
|
-
}),
|
|
314
|
-
...(refMultiple !== undefined && { refMultiple }),
|
|
315
|
-
};
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
type FileFieldHints = Pick<
|
|
319
|
-
EditFieldViewModel,
|
|
320
|
-
"accept" | "maxSize" | "entityType" | "fieldName" | "imageVariant" | "capture"
|
|
321
|
-
>;
|
|
322
|
-
|
|
323
|
-
// file/image: entityType/fieldName travel with the upload POST so the
|
|
324
|
-
// endpoint validates against the right field definition.
|
|
325
|
-
function deriveFileFieldHints(
|
|
326
|
-
fieldDef: EntityFieldDef,
|
|
327
|
-
entityName: string,
|
|
328
|
-
fieldName: string,
|
|
329
|
-
): FileFieldHints {
|
|
330
|
-
const isFileType =
|
|
331
|
-
fieldDef.type === "file" || fieldDef.type === "image" || fieldDef.type === "images";
|
|
332
|
-
const fileDef = isFileType
|
|
333
|
-
? (fieldDef as unknown as {
|
|
334
|
-
accept?: readonly string[];
|
|
335
|
-
maxSize?: string;
|
|
336
|
-
variants?: Readonly<Record<string, unknown>>;
|
|
337
|
-
capture?: "environment" | "user";
|
|
338
|
-
})
|
|
339
|
-
: undefined;
|
|
340
|
-
const imageVariant =
|
|
341
|
-
fieldDef.type === "image" || fieldDef.type === "images"
|
|
342
|
-
? Object.keys(fileDef?.variants ?? {})[0]
|
|
343
|
-
: undefined;
|
|
344
|
-
const capture = fieldDef.type === "image" ? fileDef?.capture : undefined;
|
|
345
|
-
return {
|
|
346
|
-
...(fileDef?.accept !== undefined && { accept: fileDef.accept }),
|
|
347
|
-
...(fileDef?.maxSize !== undefined && { maxSize: fileDef.maxSize }),
|
|
348
|
-
...(isFileType && { entityType: entityName, fieldName }),
|
|
349
|
-
...(imageVariant !== undefined && { imageVariant }),
|
|
350
|
-
...(capture !== undefined && { capture }),
|
|
351
|
-
};
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
type EmbeddedListHints = Pick<
|
|
355
|
-
EditFieldViewModel,
|
|
356
|
-
| "embeddedListCells"
|
|
357
|
-
| "embeddedListMinItems"
|
|
358
|
-
| "embeddedListMaxItems"
|
|
359
|
-
| "embeddedListDerived"
|
|
360
|
-
| "embeddedListTotals"
|
|
361
|
-
| "embeddedListCurrency"
|
|
362
|
-
>;
|
|
363
|
-
|
|
364
|
-
// Embedded-LIST field (`multiple: true`) — per-cell metadata for a renderer
|
|
365
|
-
// to draw one row per array item (invoice-positions-style table). A plain
|
|
366
|
-
// (non-list) embedded field emits none of this; the renderer tells the two
|
|
367
|
-
// apart by whether embeddedListCells is set, not by `type` (which stays
|
|
368
|
-
// "embedded" either way).
|
|
369
|
-
function deriveEmbeddedListHints(
|
|
370
|
-
fieldDef: EntityFieldDef,
|
|
371
|
-
translate: Translate,
|
|
372
|
-
featureName: string,
|
|
373
|
-
entityName: string,
|
|
374
|
-
fieldName: string,
|
|
375
|
-
resolvedCurrency: string,
|
|
376
|
-
): EmbeddedListHints {
|
|
377
|
-
const isEmbeddedList =
|
|
378
|
-
fieldDef.type === "embedded" &&
|
|
379
|
-
(fieldDef as unknown as { multiple?: boolean }).multiple === true;
|
|
380
|
-
const embeddedListDef = isEmbeddedList
|
|
381
|
-
? (fieldDef as unknown as {
|
|
382
|
-
schema: Readonly<Record<string, EmbeddedSubFieldShape>>;
|
|
383
|
-
minItems?: number;
|
|
384
|
-
maxItems?: number;
|
|
385
|
-
derived?: Readonly<
|
|
386
|
-
Record<
|
|
387
|
-
string,
|
|
388
|
-
{ readonly op: "multiply" | "sum" | "subtract"; readonly from: readonly string[] }
|
|
389
|
-
>
|
|
390
|
-
>;
|
|
391
|
-
totals?: readonly string[];
|
|
392
|
-
})
|
|
393
|
-
: undefined;
|
|
394
|
-
const embeddedListCells: readonly EmbeddedListCellViewModel[] | undefined =
|
|
395
|
-
embeddedListDef !== undefined
|
|
396
|
-
? Object.entries(embeddedListDef.schema).map(([subFieldName, subField]) => {
|
|
397
|
-
const cellLabel = translate(
|
|
398
|
-
embeddedCellLabelKey(featureName, entityName, fieldName, subFieldName),
|
|
399
|
-
);
|
|
400
|
-
const cellOptions = subField.type === "select" ? (subField.options ?? []) : undefined;
|
|
401
|
-
const cellOptionLabels =
|
|
402
|
-
cellOptions !== undefined
|
|
403
|
-
? buildOptionLabels(
|
|
404
|
-
translate,
|
|
405
|
-
(value) =>
|
|
406
|
-
embeddedCellOptionLabelKey(
|
|
407
|
-
featureName,
|
|
408
|
-
entityName,
|
|
409
|
-
fieldName,
|
|
410
|
-
subFieldName,
|
|
411
|
-
value,
|
|
412
|
-
),
|
|
413
|
-
cellOptions,
|
|
414
|
-
)
|
|
415
|
-
: undefined;
|
|
416
|
-
const cellRef = subField.type === "reference" ? subField : undefined;
|
|
417
|
-
const cellRefTarget =
|
|
418
|
-
cellRef?.entity !== undefined ? parseRefTarget(cellRef.entity, featureName) : undefined;
|
|
419
|
-
const cell: EmbeddedListCellViewModel = {
|
|
420
|
-
field: subFieldName,
|
|
421
|
-
label: cellLabel,
|
|
422
|
-
type: subField.type,
|
|
423
|
-
required: subField.required === true,
|
|
424
|
-
...(cellOptions !== undefined && { options: cellOptions }),
|
|
425
|
-
...(cellOptionLabels !== undefined && { optionLabels: cellOptionLabels }),
|
|
426
|
-
...(cellRefTarget !== undefined && { refEntity: cellRefTarget.entityName }),
|
|
427
|
-
...(cellRefTarget !== undefined && { refFeature: cellRefTarget.featureName }),
|
|
428
|
-
...(cellRef?.labelField !== undefined && { refLabelField: cellRef.labelField }),
|
|
429
|
-
...(cellRef?.optionsQuery !== undefined && {
|
|
430
|
-
refOptionsQuery: cellRef.optionsQuery,
|
|
431
|
-
}),
|
|
432
|
-
...(subField.type === "decimal" &&
|
|
433
|
-
subField.scale !== undefined && { scale: subField.scale }),
|
|
434
|
-
};
|
|
435
|
-
return cell;
|
|
436
|
-
})
|
|
437
|
-
: undefined;
|
|
438
|
-
return {
|
|
439
|
-
...(embeddedListCells !== undefined && { embeddedListCells }),
|
|
440
|
-
...(embeddedListDef?.minItems !== undefined && {
|
|
441
|
-
embeddedListMinItems: embeddedListDef.minItems,
|
|
442
|
-
}),
|
|
443
|
-
...(embeddedListDef?.maxItems !== undefined && {
|
|
444
|
-
embeddedListMaxItems: embeddedListDef.maxItems,
|
|
445
|
-
}),
|
|
446
|
-
...(embeddedListDef?.derived !== undefined && {
|
|
447
|
-
embeddedListDerived: embeddedListDef.derived,
|
|
448
|
-
}),
|
|
449
|
-
...(embeddedListDef?.totals !== undefined && {
|
|
450
|
-
embeddedListTotals: embeddedListDef.totals,
|
|
451
|
-
}),
|
|
452
|
-
// Currency lives on the head aggregate (entity.defaultCurrency), not per
|
|
453
|
-
// row — one value for the whole embedded list.
|
|
454
|
-
...(embeddedListDef !== undefined && { embeddedListCurrency: resolvedCurrency }),
|
|
455
|
-
};
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
function buildExtensionSectionViewModel(
|
|
459
|
-
sectionSpec: EditExtensionSection,
|
|
460
|
-
translate: Translate,
|
|
461
|
-
): Extract<EditSectionViewModel, { kind: "extension" }> {
|
|
462
|
-
return {
|
|
463
|
-
kind: "extension" as const,
|
|
464
|
-
...(sectionSpec.id !== undefined && { id: sectionSpec.id }),
|
|
465
|
-
title: translate(sectionSpec.title),
|
|
466
|
-
component: sectionSpec.component,
|
|
467
|
-
contributesToFormSubmit: sectionSpec.contributesToFormSubmit === true,
|
|
468
|
-
...(sectionSpec.entityName !== undefined && { entityName: sectionSpec.entityName }),
|
|
469
|
-
...(sectionSpec.actions !== undefined && { actions: sectionSpec.actions }),
|
|
470
|
-
};
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
// Pure transform from screen-def + entity-def + row-values to the flat
|
|
474
|
-
// section/field tree the renderer draws. FieldConditions are evaluated here
|
|
475
|
-
// so the renderer never re-runs them during React render.
|
|
476
|
-
export function computeEditViewModel<
|
|
477
|
-
TValues extends Readonly<Record<string, unknown>> = Readonly<Record<string, unknown>>,
|
|
478
|
-
>(input: ComputeEditViewModelInput<TValues>): EditViewModel {
|
|
479
|
-
const { screen, entity, values, translate, featureName } = input;
|
|
480
|
-
|
|
481
|
-
const sections: EditSectionViewModel[] = screen.layout.sections.map((sectionSpec) => {
|
|
482
|
-
if (isExtensionEditSection(sectionSpec)) {
|
|
483
|
-
return buildExtensionSectionViewModel(sectionSpec, translate);
|
|
484
|
-
}
|
|
485
|
-
if (isWriteFormEditSection(sectionSpec)) {
|
|
486
|
-
return computeWriteFormSectionViewModel(
|
|
487
|
-
sectionSpec,
|
|
488
|
-
screen.id,
|
|
489
|
-
values,
|
|
490
|
-
translate,
|
|
491
|
-
featureName,
|
|
492
|
-
);
|
|
493
|
-
}
|
|
494
|
-
if (!isFieldsEditSection(sectionSpec)) {
|
|
495
|
-
return computeRelatedListSectionViewModel(sectionSpec, translate);
|
|
496
|
-
}
|
|
497
|
-
// `groups` flattens into the same per-field pipeline as plain `fields`;
|
|
498
|
-
// the group structure below just re-groups the computed views by name.
|
|
499
|
-
const fields: EditFieldViewModel[] = sectionFieldSpecs(sectionSpec).map((fieldSpec) => {
|
|
500
|
-
const normalized = normalizeEditField(fieldSpec);
|
|
501
|
-
const fieldDef = entity.fields[normalized.field];
|
|
502
|
-
if (!fieldDef) {
|
|
503
|
-
throw new Error(
|
|
504
|
-
`computeEditViewModel: screen "${screen.id}" references unknown field "${normalized.field}" on entity "${screen.entity}"`,
|
|
505
|
-
);
|
|
506
|
-
}
|
|
507
|
-
// Declared reference metadata (EditFieldSpec.refEntity) — for
|
|
508
|
-
// projectionDetail fields, which have no EntityDefinition field to
|
|
509
|
-
// carry a real "reference" type. Takes priority over fieldDef.type
|
|
510
|
-
// below so it also fires against the pseudo-entity that hardcodes
|
|
511
|
-
// every field as "text" (projection-detail-shim) — real entityEdit
|
|
512
|
-
// screens never set this, so their fieldDef.type === "reference"
|
|
513
|
-
// branch is unaffected.
|
|
514
|
-
const declaredRefTarget =
|
|
515
|
-
normalized.refEntity !== undefined
|
|
516
|
-
? parseRefTarget(normalized.refEntity, featureName)
|
|
517
|
-
: undefined;
|
|
518
|
-
const effectiveType = declaredRefTarget !== undefined ? "reference" : fieldDef.type;
|
|
519
|
-
const label = translate(
|
|
520
|
-
screen.fieldLabels?.[normalized.field] ??
|
|
521
|
-
fieldLabelKey(featureName, screen.entity, normalized.field),
|
|
522
|
-
);
|
|
523
|
-
const visible = evalCondition(normalized.visible, true, values);
|
|
524
|
-
// `readOnly` (camelCase) is the name on both sides: EditFieldSpec
|
|
525
|
-
// in the engine, and the view-model emitted here. One convention
|
|
526
|
-
// through the stack beats translating at the boundary.
|
|
527
|
-
const readOnly = evalCondition(normalized.readOnly, false, values);
|
|
528
|
-
// `required` on the field-spec overrides the entity-default. A
|
|
529
|
-
// field that's required at the entity-level but marked required:
|
|
530
|
-
// false on the screen (e.g. a soft-onboarding wizard that
|
|
531
|
-
// collects less up-front) respects the screen override.
|
|
532
|
-
const entityRequired = (fieldDef as unknown as { required?: boolean }).required === true;
|
|
533
|
-
const required = evalCondition(normalized.required, entityRequired, values);
|
|
534
|
-
// ponytail: "EUR" mirrors DEFAULT_CURRENCIES[0] from
|
|
535
|
-
// framework/src/engine/field-helpers.ts — headless has no dependency
|
|
536
|
-
// on that module, so the literal is duplicated here instead of
|
|
537
|
-
// importing it just for one fallback string.
|
|
538
|
-
const resolvedCurrency = entity.defaultCurrency ?? "EUR";
|
|
539
|
-
const selectHints = deriveSelectFieldHints(
|
|
540
|
-
fieldDef,
|
|
541
|
-
translate,
|
|
542
|
-
featureName,
|
|
543
|
-
screen.entity,
|
|
544
|
-
normalized.field,
|
|
545
|
-
);
|
|
546
|
-
const textHints = deriveTextFieldHints(fieldDef);
|
|
547
|
-
const dateHints = deriveDateFieldHints(fieldDef);
|
|
548
|
-
const numericHints = deriveNumericFieldHints(fieldDef, resolvedCurrency);
|
|
549
|
-
const referenceHints = deriveReferenceFieldHints(
|
|
550
|
-
fieldDef,
|
|
551
|
-
normalized,
|
|
552
|
-
declaredRefTarget,
|
|
553
|
-
featureName,
|
|
554
|
-
);
|
|
555
|
-
const fileHints = deriveFileFieldHints(fieldDef, screen.entity, normalized.field);
|
|
556
|
-
const embeddedListHints = deriveEmbeddedListHints(
|
|
557
|
-
fieldDef,
|
|
558
|
-
translate,
|
|
559
|
-
featureName,
|
|
560
|
-
screen.entity,
|
|
561
|
-
normalized.field,
|
|
562
|
-
resolvedCurrency,
|
|
563
|
-
);
|
|
564
|
-
const view: EditFieldViewModel = {
|
|
565
|
-
field: normalized.field,
|
|
566
|
-
label,
|
|
567
|
-
type: effectiveType,
|
|
568
|
-
value: values[normalized.field],
|
|
569
|
-
visible,
|
|
570
|
-
readOnly,
|
|
571
|
-
required,
|
|
572
|
-
...(normalized.span !== undefined && { span: normalized.span }),
|
|
573
|
-
...(normalized.renderer !== undefined && { renderer: normalized.renderer }),
|
|
574
|
-
...selectHints,
|
|
575
|
-
...textHints,
|
|
576
|
-
...dateHints,
|
|
577
|
-
...numericHints,
|
|
578
|
-
...referenceHints,
|
|
579
|
-
...fileHints,
|
|
580
|
-
...(normalized.icon !== undefined && { icon: normalized.icon }),
|
|
581
|
-
...embeddedListHints,
|
|
582
|
-
};
|
|
583
|
-
return view;
|
|
584
|
-
});
|
|
585
|
-
// Boot-validator rejects fields.length === 0 with no groups (screens.ts),
|
|
586
|
-
// so an empty section never reaches this code.
|
|
587
|
-
const visible = fields.some((field) => field.visible);
|
|
588
|
-
// Tabs mode renders this section as its own panel, where two columns reads
|
|
589
|
-
// better than the single-column default a stacked form keeps. Applies to
|
|
590
|
-
// entityEdit tabs as well since fw#3134, not just projectionDetail.
|
|
591
|
-
const defaultColumns = screen.layout.mode === "tabs" ? 2 : 1;
|
|
592
|
-
const groups = sectionSpec.groups?.map((group) => ({
|
|
593
|
-
title: translate(group.title),
|
|
594
|
-
columns: group.columns ?? 2,
|
|
595
|
-
fields: group.fields.map((fieldSpec) => {
|
|
596
|
-
const fieldName = normalizeEditField(fieldSpec).field;
|
|
597
|
-
const fieldView = fields.find((f) => f.field === fieldName);
|
|
598
|
-
if (fieldView === undefined) {
|
|
599
|
-
throw new Error(
|
|
600
|
-
`computeEditViewModel: screen "${screen.id}" group "${group.title}" references field ` +
|
|
601
|
-
`"${fieldName}" that failed to resolve.`,
|
|
602
|
-
);
|
|
603
|
-
}
|
|
604
|
-
return fieldView;
|
|
605
|
-
}),
|
|
606
|
-
}));
|
|
607
|
-
return {
|
|
608
|
-
kind: "fields" as const,
|
|
609
|
-
...(sectionSpec.id !== undefined && { id: sectionSpec.id }),
|
|
610
|
-
visible,
|
|
611
|
-
// Titellose Section (flache Form) → kein h3; nur übersetzen wenn gesetzt.
|
|
612
|
-
...(sectionSpec.title !== undefined && { title: translate(sectionSpec.title) }),
|
|
613
|
-
...(sectionSpec.description !== undefined && {
|
|
614
|
-
description: translate(sectionSpec.description),
|
|
615
|
-
}),
|
|
616
|
-
columns: sectionSpec.columns ?? defaultColumns,
|
|
617
|
-
fields,
|
|
618
|
-
...(groups !== undefined && { groups }),
|
|
619
|
-
...(sectionSpec.icon !== undefined && { icon: sectionSpec.icon }),
|
|
620
|
-
...(sectionSpec.actions !== undefined && { actions: sectionSpec.actions }),
|
|
621
|
-
};
|
|
622
|
-
});
|
|
623
|
-
|
|
624
|
-
const id = (values["id"] as string | undefined) ?? null;
|
|
625
|
-
|
|
626
|
-
return {
|
|
627
|
-
screenId: screen.id,
|
|
628
|
-
entityName: screen.entity,
|
|
629
|
-
id,
|
|
630
|
-
sections,
|
|
631
|
-
...(screen.slots && { slots: screen.slots }),
|
|
632
|
-
};
|
|
633
|
-
}
|
|
634
|
-
|
|
635
|
-
// Resolves a FieldCondition against the current row values.
|
|
636
|
-
// `undefined` means "not declared" — caller substitutes the default.
|
|
637
|
-
function evalCondition<TValues>(
|
|
638
|
-
condition: FieldCondition | undefined,
|
|
639
|
-
fallback: boolean,
|
|
640
|
-
values: TValues,
|
|
641
|
-
): boolean {
|
|
642
|
-
if (condition === undefined) return fallback;
|
|
643
|
-
// @cast-boundary view-model: TValues ist strukturell ein Record.
|
|
644
|
-
return evalFieldCondition(condition, values as Record<string, unknown>);
|
|
645
|
-
}
|