@cosmicdrift/kumiko-headless 0.327.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/changes.json +7 -0
- 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 -281
- package/src/format/__tests__/html-template.test.ts +0 -47
- package/src/format/html-template.ts +0 -49
- package/src/format/index.ts +0 -219
- 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 -428
- package/src/view-model/edit.ts +0 -641
- package/src/view-model/embedded-list.ts +0 -89
- package/src/view-model/index.ts +0 -43
- package/src/view-model/list.ts +0 -277
- package/src/view-model/types.ts +0 -392
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
import { evalFieldCondition, isExtensionEditSection, isFieldsEditSection, isWriteFormEditSection, normalizeEditField, parseRefTarget, sectionFieldSpecs, WRITE_FORM_SECTION_ENTITY, } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
2
|
+
import { buildOptionLabels, embeddedCellLabelKey, embeddedCellOptionLabelKey, fieldLabelKey, fieldOptionLabelKey, } from "./list.js";
|
|
3
|
+
// Resolves a writeForm section's own fieldDefs/fields through the same
|
|
4
|
+
// per-field pipeline as a plain fields section, by reusing the pipeline
|
|
5
|
+
// itself rather than duplicating it: wrap fieldDefs/fields as a throwaway
|
|
6
|
+
// entityEdit screen (same "synthesize a pseudo-entity" idiom as the shims in
|
|
7
|
+
// packages/renderer/src/app/*-shim.ts) and recurse into computeEditViewModel
|
|
8
|
+
// once. Split out of computeEditViewModel's section-map to keep that
|
|
9
|
+
// function's own complexity from absorbing this branch's decision points.
|
|
10
|
+
function computeWriteFormSectionViewModel(sectionSpec, screenId, values, translate, featureName) {
|
|
11
|
+
const innerEntity = { fields: sectionSpec.fieldDefs };
|
|
12
|
+
const innerScreen = {
|
|
13
|
+
id: `${screenId}:${sectionSpec.id ?? "write-form"}`,
|
|
14
|
+
type: "entityEdit",
|
|
15
|
+
entity: WRITE_FORM_SECTION_ENTITY,
|
|
16
|
+
layout: {
|
|
17
|
+
sections: [{ kind: "fields", fields: sectionSpec.fields, columns: sectionSpec.columns }],
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
const inner = computeEditViewModel({
|
|
21
|
+
screen: innerScreen,
|
|
22
|
+
entity: innerEntity,
|
|
23
|
+
values,
|
|
24
|
+
translate,
|
|
25
|
+
featureName,
|
|
26
|
+
});
|
|
27
|
+
const innerSection = inner.sections[0];
|
|
28
|
+
const fields = innerSection?.kind === "fields" ? innerSection.fields : [];
|
|
29
|
+
return {
|
|
30
|
+
kind: "writeForm",
|
|
31
|
+
...(sectionSpec.title !== undefined && { title: translate(sectionSpec.title) }),
|
|
32
|
+
...(sectionSpec.description !== undefined && {
|
|
33
|
+
description: translate(sectionSpec.description),
|
|
34
|
+
}),
|
|
35
|
+
columns: sectionSpec.columns ?? 1,
|
|
36
|
+
fields,
|
|
37
|
+
...(sectionSpec.icon !== undefined && { icon: sectionSpec.icon }),
|
|
38
|
+
handler: sectionSpec.handler,
|
|
39
|
+
...(sectionSpec.submitLabel !== undefined && {
|
|
40
|
+
submitLabel: translate(sectionSpec.submitLabel),
|
|
41
|
+
}),
|
|
42
|
+
...(sectionSpec.actions !== undefined && { actions: sectionSpec.actions }),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
// relatedList runs its own query — nothing here to resolve against
|
|
46
|
+
// entity/values, the spec passes through verbatim (fw#2166).
|
|
47
|
+
function computeRelatedListSectionViewModel(sectionSpec, translate) {
|
|
48
|
+
return {
|
|
49
|
+
kind: "relatedList",
|
|
50
|
+
title: translate(sectionSpec.title),
|
|
51
|
+
query: sectionSpec.query,
|
|
52
|
+
...(sectionSpec.parentParam !== undefined && { parentParam: sectionSpec.parentParam }),
|
|
53
|
+
...(sectionSpec.parentFilter !== undefined && { parentFilter: sectionSpec.parentFilter }),
|
|
54
|
+
columns: sectionSpec.columns,
|
|
55
|
+
...(sectionSpec.description !== undefined && {
|
|
56
|
+
description: translate(sectionSpec.description),
|
|
57
|
+
}),
|
|
58
|
+
...(sectionSpec.itemNoun !== undefined && { itemNoun: sectionSpec.itemNoun }),
|
|
59
|
+
...(sectionSpec.pageSize !== undefined && { pageSize: sectionSpec.pageSize }),
|
|
60
|
+
...(sectionSpec.defaultSort !== undefined && { defaultSort: sectionSpec.defaultSort }),
|
|
61
|
+
...(sectionSpec.searchable !== undefined && { searchable: sectionSpec.searchable }),
|
|
62
|
+
...(sectionSpec.facets !== undefined && { facets: sectionSpec.facets }),
|
|
63
|
+
...(sectionSpec.rowClick !== undefined && { rowClick: sectionSpec.rowClick }),
|
|
64
|
+
...(sectionSpec.rowActions !== undefined && { rowActions: sectionSpec.rowActions }),
|
|
65
|
+
...(sectionSpec.toolbarActions !== undefined && { toolbarActions: sectionSpec.toolbarActions }),
|
|
66
|
+
...(sectionSpec.actions !== undefined && { actions: sectionSpec.actions }),
|
|
67
|
+
...(sectionSpec.emptyState !== undefined && {
|
|
68
|
+
emptyState: {
|
|
69
|
+
title: translate(sectionSpec.emptyState.title),
|
|
70
|
+
...(sectionSpec.emptyState.description !== undefined && {
|
|
71
|
+
description: translate(sectionSpec.emptyState.description),
|
|
72
|
+
}),
|
|
73
|
+
// Action stays untranslated, same convention as rowActions/
|
|
74
|
+
// toolbarActions above — the renderer's row-actions builder
|
|
75
|
+
// translates `action.label` at button-build time.
|
|
76
|
+
...(sectionSpec.emptyState.action !== undefined && {
|
|
77
|
+
action: sectionSpec.emptyState.action,
|
|
78
|
+
}),
|
|
79
|
+
},
|
|
80
|
+
}),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
function deriveSelectFieldHints(fieldDef, translate, featureName, entityName, fieldName) {
|
|
84
|
+
const options = fieldDef.type === "select" || fieldDef.type === "multiSelect"
|
|
85
|
+
? (fieldDef.options ?? [])
|
|
86
|
+
: undefined;
|
|
87
|
+
const optionLabels = options !== undefined
|
|
88
|
+
? buildOptionLabels(translate, (value) => fieldOptionLabelKey(featureName, entityName, fieldName, value), options)
|
|
89
|
+
: undefined;
|
|
90
|
+
const display = fieldDef.type === "multiSelect" || fieldDef.type === "select" ? fieldDef.display : undefined;
|
|
91
|
+
const columns = fieldDef.type === "multiSelect" ? fieldDef.columns : undefined;
|
|
92
|
+
const maxRows = fieldDef.type === "multiSelect" ? fieldDef.maxRows : undefined;
|
|
93
|
+
return {
|
|
94
|
+
...(options !== undefined && { options }),
|
|
95
|
+
...(optionLabels !== undefined && { optionLabels }),
|
|
96
|
+
...(display !== undefined && { display }),
|
|
97
|
+
...(columns !== undefined && { columns }),
|
|
98
|
+
...(maxRows !== undefined && { maxRows }),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
function deriveTextFieldHints(fieldDef) {
|
|
102
|
+
const multiline = fieldDef.type === "text" || fieldDef.type === "longText"
|
|
103
|
+
? fieldDef.multiline
|
|
104
|
+
: undefined;
|
|
105
|
+
const format = fieldDef.type === "text"
|
|
106
|
+
? fieldDef.format
|
|
107
|
+
: undefined;
|
|
108
|
+
return {
|
|
109
|
+
...(multiline !== undefined && { multiline }),
|
|
110
|
+
...(format !== undefined && { format }),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
function deriveDateFieldHints(fieldDef) {
|
|
114
|
+
const wallClock = fieldDef.type === "timestamp" &&
|
|
115
|
+
fieldDef.locatedBy !== undefined
|
|
116
|
+
? true
|
|
117
|
+
: undefined;
|
|
118
|
+
const dateBounds = fieldDef.type === "date" ||
|
|
119
|
+
fieldDef.type === "timestamp" ||
|
|
120
|
+
fieldDef.type === "locatedTimestamp"
|
|
121
|
+
? fieldDef
|
|
122
|
+
: undefined;
|
|
123
|
+
return {
|
|
124
|
+
...(wallClock !== undefined && { wallClock }),
|
|
125
|
+
...(dateBounds?.min !== undefined && { min: dateBounds.min }),
|
|
126
|
+
...(dateBounds?.max !== undefined && { max: dateBounds.max }),
|
|
127
|
+
...(dateBounds?.locale !== undefined && { dateLocale: dateBounds.locale }),
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
function deriveNumericFieldHints(fieldDef, resolvedCurrency) {
|
|
131
|
+
const grouping = fieldDef.type === "number" ? fieldDef.grouping : undefined;
|
|
132
|
+
return {
|
|
133
|
+
...(grouping !== undefined && { grouping }),
|
|
134
|
+
...(fieldDef.type === "money" && { currency: resolvedCurrency }),
|
|
135
|
+
...(fieldDef.type === "number" && fieldDef.unit !== undefined && { unit: fieldDef.unit }),
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
// Declared reference metadata (EditFieldSpec.refEntity) takes priority over
|
|
139
|
+
// fieldDef.type — see computeEditViewModel's effectiveType, same reasoning.
|
|
140
|
+
function deriveReferenceFieldHints(fieldDef, normalized, declaredRefTarget, featureName) {
|
|
141
|
+
const refRaw = declaredRefTarget === undefined && fieldDef.type === "reference"
|
|
142
|
+
? fieldDef.entity
|
|
143
|
+
: undefined;
|
|
144
|
+
const refTarget = declaredRefTarget ?? (refRaw !== undefined ? parseRefTarget(refRaw, featureName) : undefined);
|
|
145
|
+
const refLabelField = declaredRefTarget !== undefined
|
|
146
|
+
? (normalized.refLabelField ?? "id")
|
|
147
|
+
: fieldDef.type === "reference"
|
|
148
|
+
? (fieldDef.labelField ?? "id")
|
|
149
|
+
: undefined;
|
|
150
|
+
return {
|
|
151
|
+
...(refTarget?.entityName !== undefined && { refEntity: refTarget.entityName }),
|
|
152
|
+
...(refTarget?.featureName !== undefined && { refFeature: refTarget.featureName }),
|
|
153
|
+
...(refLabelField !== undefined && { refLabelField }),
|
|
154
|
+
...deriveReferenceMultiHints(fieldDef, declaredRefTarget),
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
// Declared reference metadata has no `multiple` or `optionsQuery` concept
|
|
158
|
+
// (it targets row-meta/derived fields, always single-valued and resolved
|
|
159
|
+
// through the target entity) — only a real ReferenceFieldDef carries them.
|
|
160
|
+
function deriveReferenceMultiHints(fieldDef, declaredRefTarget) {
|
|
161
|
+
const ownReferenceDef = declaredRefTarget === undefined && fieldDef.type === "reference"
|
|
162
|
+
? fieldDef
|
|
163
|
+
: undefined;
|
|
164
|
+
const refMultiple = ownReferenceDef === undefined ? undefined : (ownReferenceDef.multiple ?? false);
|
|
165
|
+
return {
|
|
166
|
+
...(ownReferenceDef?.optionsQuery !== undefined && {
|
|
167
|
+
refOptionsQuery: ownReferenceDef.optionsQuery,
|
|
168
|
+
}),
|
|
169
|
+
...(refMultiple !== undefined && { refMultiple }),
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
// file/image: entityType/fieldName travel with the upload POST so the
|
|
173
|
+
// endpoint validates against the right field definition.
|
|
174
|
+
function deriveFileFieldHints(fieldDef, entityName, fieldName) {
|
|
175
|
+
const isFileType = fieldDef.type === "file" || fieldDef.type === "image" || fieldDef.type === "images";
|
|
176
|
+
const fileDef = isFileType
|
|
177
|
+
? fieldDef
|
|
178
|
+
: undefined;
|
|
179
|
+
const imageVariant = fieldDef.type === "image" || fieldDef.type === "images"
|
|
180
|
+
? Object.keys(fileDef?.variants ?? {})[0]
|
|
181
|
+
: undefined;
|
|
182
|
+
const capture = fieldDef.type === "image" ? fileDef?.capture : undefined;
|
|
183
|
+
return {
|
|
184
|
+
...(fileDef?.accept !== undefined && { accept: fileDef.accept }),
|
|
185
|
+
...(fileDef?.maxSize !== undefined && { maxSize: fileDef.maxSize }),
|
|
186
|
+
...(isFileType && { entityType: entityName, fieldName }),
|
|
187
|
+
...(imageVariant !== undefined && { imageVariant }),
|
|
188
|
+
...(capture !== undefined && { capture }),
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
// Embedded-LIST field (`multiple: true`) — per-cell metadata for a renderer
|
|
192
|
+
// to draw one row per array item (invoice-positions-style table). A plain
|
|
193
|
+
// (non-list) embedded field emits none of this; the renderer tells the two
|
|
194
|
+
// apart by whether embeddedListCells is set, not by `type` (which stays
|
|
195
|
+
// "embedded" either way).
|
|
196
|
+
function deriveEmbeddedListHints(fieldDef, translate, featureName, entityName, fieldName, resolvedCurrency) {
|
|
197
|
+
const isEmbeddedList = fieldDef.type === "embedded" &&
|
|
198
|
+
fieldDef.multiple === true;
|
|
199
|
+
const embeddedListDef = isEmbeddedList
|
|
200
|
+
? fieldDef
|
|
201
|
+
: undefined;
|
|
202
|
+
const embeddedListCells = embeddedListDef !== undefined
|
|
203
|
+
? Object.entries(embeddedListDef.schema).map(([subFieldName, subField]) => {
|
|
204
|
+
const cellLabel = translate(embeddedCellLabelKey(featureName, entityName, fieldName, subFieldName));
|
|
205
|
+
const cellOptions = subField.type === "select" ? (subField.options ?? []) : undefined;
|
|
206
|
+
const cellOptionLabels = cellOptions !== undefined
|
|
207
|
+
? buildOptionLabels(translate, (value) => embeddedCellOptionLabelKey(featureName, entityName, fieldName, subFieldName, value), cellOptions)
|
|
208
|
+
: undefined;
|
|
209
|
+
const cellRef = subField.type === "reference" ? subField : undefined;
|
|
210
|
+
const cellRefTarget = cellRef?.entity !== undefined ? parseRefTarget(cellRef.entity, featureName) : undefined;
|
|
211
|
+
const cell = {
|
|
212
|
+
field: subFieldName,
|
|
213
|
+
label: cellLabel,
|
|
214
|
+
type: subField.type,
|
|
215
|
+
required: subField.required === true,
|
|
216
|
+
...(cellOptions !== undefined && { options: cellOptions }),
|
|
217
|
+
...(cellOptionLabels !== undefined && { optionLabels: cellOptionLabels }),
|
|
218
|
+
...(cellRefTarget !== undefined && { refEntity: cellRefTarget.entityName }),
|
|
219
|
+
...(cellRefTarget !== undefined && { refFeature: cellRefTarget.featureName }),
|
|
220
|
+
...(cellRef?.labelField !== undefined && { refLabelField: cellRef.labelField }),
|
|
221
|
+
...(cellRef?.optionsQuery !== undefined && {
|
|
222
|
+
refOptionsQuery: cellRef.optionsQuery,
|
|
223
|
+
}),
|
|
224
|
+
...(subField.type === "decimal" &&
|
|
225
|
+
subField.scale !== undefined && { scale: subField.scale }),
|
|
226
|
+
};
|
|
227
|
+
return cell;
|
|
228
|
+
})
|
|
229
|
+
: undefined;
|
|
230
|
+
return {
|
|
231
|
+
...(embeddedListCells !== undefined && { embeddedListCells }),
|
|
232
|
+
...(embeddedListDef?.minItems !== undefined && {
|
|
233
|
+
embeddedListMinItems: embeddedListDef.minItems,
|
|
234
|
+
}),
|
|
235
|
+
...(embeddedListDef?.maxItems !== undefined && {
|
|
236
|
+
embeddedListMaxItems: embeddedListDef.maxItems,
|
|
237
|
+
}),
|
|
238
|
+
...(embeddedListDef?.derived !== undefined && {
|
|
239
|
+
embeddedListDerived: embeddedListDef.derived,
|
|
240
|
+
}),
|
|
241
|
+
...(embeddedListDef?.totals !== undefined && {
|
|
242
|
+
embeddedListTotals: embeddedListDef.totals,
|
|
243
|
+
}),
|
|
244
|
+
// Currency lives on the head aggregate (entity.defaultCurrency), not per
|
|
245
|
+
// row — one value for the whole embedded list.
|
|
246
|
+
...(embeddedListDef !== undefined && { embeddedListCurrency: resolvedCurrency }),
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
function buildExtensionSectionViewModel(sectionSpec, translate) {
|
|
250
|
+
return {
|
|
251
|
+
kind: "extension",
|
|
252
|
+
...(sectionSpec.id !== undefined && { id: sectionSpec.id }),
|
|
253
|
+
title: translate(sectionSpec.title),
|
|
254
|
+
component: sectionSpec.component,
|
|
255
|
+
contributesToFormSubmit: sectionSpec.contributesToFormSubmit === true,
|
|
256
|
+
...(sectionSpec.entityName !== undefined && { entityName: sectionSpec.entityName }),
|
|
257
|
+
...(sectionSpec.actions !== undefined && { actions: sectionSpec.actions }),
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
// Pure transform from screen-def + entity-def + row-values to the flat
|
|
261
|
+
// section/field tree the renderer draws. FieldConditions are evaluated here
|
|
262
|
+
// so the renderer never re-runs them during React render.
|
|
263
|
+
export function computeEditViewModel(input) {
|
|
264
|
+
const { screen, entity, values, translate, featureName } = input;
|
|
265
|
+
const sections = screen.layout.sections.map((sectionSpec) => {
|
|
266
|
+
if (isExtensionEditSection(sectionSpec)) {
|
|
267
|
+
return buildExtensionSectionViewModel(sectionSpec, translate);
|
|
268
|
+
}
|
|
269
|
+
if (isWriteFormEditSection(sectionSpec)) {
|
|
270
|
+
return computeWriteFormSectionViewModel(sectionSpec, screen.id, values, translate, featureName);
|
|
271
|
+
}
|
|
272
|
+
if (!isFieldsEditSection(sectionSpec)) {
|
|
273
|
+
return computeRelatedListSectionViewModel(sectionSpec, translate);
|
|
274
|
+
}
|
|
275
|
+
// `groups` flattens into the same per-field pipeline as plain `fields`;
|
|
276
|
+
// the group structure below just re-groups the computed views by name.
|
|
277
|
+
const fields = sectionFieldSpecs(sectionSpec).map((fieldSpec) => {
|
|
278
|
+
const normalized = normalizeEditField(fieldSpec);
|
|
279
|
+
const fieldDef = entity.fields[normalized.field];
|
|
280
|
+
if (!fieldDef) {
|
|
281
|
+
throw new Error(`computeEditViewModel: screen "${screen.id}" references unknown field "${normalized.field}" on entity "${screen.entity}"`);
|
|
282
|
+
}
|
|
283
|
+
// Declared reference metadata (EditFieldSpec.refEntity) — for
|
|
284
|
+
// projectionDetail fields, which have no EntityDefinition field to
|
|
285
|
+
// carry a real "reference" type. Takes priority over fieldDef.type
|
|
286
|
+
// below so it also fires against the pseudo-entity that hardcodes
|
|
287
|
+
// every field as "text" (projection-detail-shim) — real entityEdit
|
|
288
|
+
// screens never set this, so their fieldDef.type === "reference"
|
|
289
|
+
// branch is unaffected.
|
|
290
|
+
const declaredRefTarget = normalized.refEntity !== undefined
|
|
291
|
+
? parseRefTarget(normalized.refEntity, featureName)
|
|
292
|
+
: undefined;
|
|
293
|
+
const effectiveType = declaredRefTarget !== undefined ? "reference" : fieldDef.type;
|
|
294
|
+
const label = translate(screen.fieldLabels?.[normalized.field] ??
|
|
295
|
+
fieldLabelKey(featureName, screen.entity, normalized.field));
|
|
296
|
+
const visible = evalCondition(normalized.visible, true, values);
|
|
297
|
+
// `readOnly` (camelCase) is the name on both sides: EditFieldSpec
|
|
298
|
+
// in the engine, and the view-model emitted here. One convention
|
|
299
|
+
// through the stack beats translating at the boundary.
|
|
300
|
+
const readOnly = evalCondition(normalized.readOnly, false, values);
|
|
301
|
+
// `required` on the field-spec overrides the entity-default. A
|
|
302
|
+
// field that's required at the entity-level but marked required:
|
|
303
|
+
// false on the screen (e.g. a soft-onboarding wizard that
|
|
304
|
+
// collects less up-front) respects the screen override.
|
|
305
|
+
const entityRequired = fieldDef.required === true;
|
|
306
|
+
const required = evalCondition(normalized.required, entityRequired, values);
|
|
307
|
+
// ponytail: "EUR" mirrors DEFAULT_CURRENCIES[0] from
|
|
308
|
+
// framework/src/engine/field-helpers.ts — headless has no dependency
|
|
309
|
+
// on that module, so the literal is duplicated here instead of
|
|
310
|
+
// importing it just for one fallback string.
|
|
311
|
+
const resolvedCurrency = entity.defaultCurrency ?? "EUR";
|
|
312
|
+
const selectHints = deriveSelectFieldHints(fieldDef, translate, featureName, screen.entity, normalized.field);
|
|
313
|
+
const textHints = deriveTextFieldHints(fieldDef);
|
|
314
|
+
const dateHints = deriveDateFieldHints(fieldDef);
|
|
315
|
+
const numericHints = deriveNumericFieldHints(fieldDef, resolvedCurrency);
|
|
316
|
+
const referenceHints = deriveReferenceFieldHints(fieldDef, normalized, declaredRefTarget, featureName);
|
|
317
|
+
const fileHints = deriveFileFieldHints(fieldDef, screen.entity, normalized.field);
|
|
318
|
+
const embeddedListHints = deriveEmbeddedListHints(fieldDef, translate, featureName, screen.entity, normalized.field, resolvedCurrency);
|
|
319
|
+
const view = {
|
|
320
|
+
field: normalized.field,
|
|
321
|
+
label,
|
|
322
|
+
type: effectiveType,
|
|
323
|
+
value: values[normalized.field],
|
|
324
|
+
visible,
|
|
325
|
+
readOnly,
|
|
326
|
+
required,
|
|
327
|
+
...(normalized.span !== undefined && { span: normalized.span }),
|
|
328
|
+
...(normalized.renderer !== undefined && { renderer: normalized.renderer }),
|
|
329
|
+
...selectHints,
|
|
330
|
+
...textHints,
|
|
331
|
+
...dateHints,
|
|
332
|
+
...numericHints,
|
|
333
|
+
...referenceHints,
|
|
334
|
+
...fileHints,
|
|
335
|
+
...(normalized.icon !== undefined && { icon: normalized.icon }),
|
|
336
|
+
...embeddedListHints,
|
|
337
|
+
};
|
|
338
|
+
return view;
|
|
339
|
+
});
|
|
340
|
+
// Boot-validator rejects fields.length === 0 with no groups (screens.ts),
|
|
341
|
+
// so an empty section never reaches this code.
|
|
342
|
+
const visible = fields.some((field) => field.visible);
|
|
343
|
+
// Tabs mode renders this section as its own panel, where two columns reads
|
|
344
|
+
// better than the single-column default a stacked form keeps. Applies to
|
|
345
|
+
// entityEdit tabs as well since fw#3134, not just projectionDetail.
|
|
346
|
+
const defaultColumns = screen.layout.mode === "tabs" ? 2 : 1;
|
|
347
|
+
const groups = sectionSpec.groups?.map((group) => ({
|
|
348
|
+
title: translate(group.title),
|
|
349
|
+
columns: group.columns ?? 2,
|
|
350
|
+
fields: group.fields.map((fieldSpec) => {
|
|
351
|
+
const fieldName = normalizeEditField(fieldSpec).field;
|
|
352
|
+
const fieldView = fields.find((f) => f.field === fieldName);
|
|
353
|
+
if (fieldView === undefined) {
|
|
354
|
+
throw new Error(`computeEditViewModel: screen "${screen.id}" group "${group.title}" references field ` +
|
|
355
|
+
`"${fieldName}" that failed to resolve.`);
|
|
356
|
+
}
|
|
357
|
+
return fieldView;
|
|
358
|
+
}),
|
|
359
|
+
}));
|
|
360
|
+
return {
|
|
361
|
+
kind: "fields",
|
|
362
|
+
...(sectionSpec.id !== undefined && { id: sectionSpec.id }),
|
|
363
|
+
visible,
|
|
364
|
+
// Titellose Section (flache Form) → kein h3; nur übersetzen wenn gesetzt.
|
|
365
|
+
...(sectionSpec.title !== undefined && { title: translate(sectionSpec.title) }),
|
|
366
|
+
...(sectionSpec.description !== undefined && {
|
|
367
|
+
description: translate(sectionSpec.description),
|
|
368
|
+
}),
|
|
369
|
+
columns: sectionSpec.columns ?? defaultColumns,
|
|
370
|
+
fields,
|
|
371
|
+
...(groups !== undefined && { groups }),
|
|
372
|
+
...(sectionSpec.icon !== undefined && { icon: sectionSpec.icon }),
|
|
373
|
+
...(sectionSpec.actions !== undefined && { actions: sectionSpec.actions }),
|
|
374
|
+
};
|
|
375
|
+
});
|
|
376
|
+
const id = values["id"] ?? null;
|
|
377
|
+
return {
|
|
378
|
+
screenId: screen.id,
|
|
379
|
+
entityName: screen.entity,
|
|
380
|
+
id,
|
|
381
|
+
sections,
|
|
382
|
+
...(screen.slots && { slots: screen.slots }),
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
// Resolves a FieldCondition against the current row values.
|
|
386
|
+
// `undefined` means "not declared" — caller substitutes the default.
|
|
387
|
+
function evalCondition(condition, fallback, values) {
|
|
388
|
+
if (condition === undefined)
|
|
389
|
+
return fallback;
|
|
390
|
+
// @cast-boundary view-model: TValues ist strukturell ein Record.
|
|
391
|
+
return evalFieldCondition(condition, values);
|
|
392
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { EmbeddedDerivedCellDef } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
2
|
+
import type { FieldIssue } from "../dispatcher/index.js";
|
|
3
|
+
export type EmbeddedDerivedOp = EmbeddedDerivedCellDef["op"];
|
|
4
|
+
export type { DerivedCellRoundingTarget } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
5
|
+
export { computeDerivedCellValue, roundDerivedCellValue, } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
6
|
+
/** Sums a numeric/money/decimal column across all rows. Non-numeric or
|
|
7
|
+
* missing values count as 0. Money columns are minor-unit integers —
|
|
8
|
+
* the sum stays in minor units, never rounds through a major-unit float. */
|
|
9
|
+
export declare function sumEmbeddedListColumn(rows: readonly Readonly<Record<string, unknown>>[], field: string): number;
|
|
10
|
+
export type EmbeddedListIssueGroups = {
|
|
11
|
+
/** Issues at exactly `listField` — e.g. min/max row-count violations. */
|
|
12
|
+
readonly listIssues: readonly FieldIssue[];
|
|
13
|
+
/** Issues at exactly `${listField}.${rowIndex}` — a row-level check
|
|
14
|
+
* (e.g. "row incomplete") that isn't attributable to one cell. */
|
|
15
|
+
readonly rowIssues: Readonly<Record<number, readonly FieldIssue[]>>;
|
|
16
|
+
/** Issues at exactly `${listField}.${rowIndex}.${cellField}`, keyed
|
|
17
|
+
* `${rowIndex}.${cellField}` (not the full path — the caller already
|
|
18
|
+
* knows listField). */
|
|
19
|
+
readonly cellIssues: Readonly<Record<string, readonly FieldIssue[]>>;
|
|
20
|
+
};
|
|
21
|
+
/** Buckets a flat issues-by-path map (FormSnapshot.errors shape — see
|
|
22
|
+
* packages/headless/src/form/zod-bridge.ts groupIssuesByPath) into the
|
|
23
|
+
* three levels an embedded-list widget renders. Only keys that are
|
|
24
|
+
* exactly `listField`, `listField.N`, or `listField.N.subfield` are
|
|
25
|
+
* claimed; unrelated keys (other top-level fields, or deeper nesting
|
|
26
|
+
* than this field ever produces) are ignored. */
|
|
27
|
+
export declare function groupEmbeddedListIssues(allIssues: Readonly<Record<string, readonly FieldIssue[]>>, listField: string): EmbeddedListIssueGroups;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// Canonical implementation moved to packages/framework/src/engine/embedded-derived.ts
|
|
2
|
+
// (the write-schema preprocess needs it server-side too).
|
|
3
|
+
export { computeDerivedCellValue, roundDerivedCellValue, } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
4
|
+
/** Sums a numeric/money/decimal column across all rows. Non-numeric or
|
|
5
|
+
* missing values count as 0. Money columns are minor-unit integers —
|
|
6
|
+
* the sum stays in minor units, never rounds through a major-unit float. */
|
|
7
|
+
export function sumEmbeddedListColumn(rows, field) {
|
|
8
|
+
let sum = 0;
|
|
9
|
+
for (const row of rows) {
|
|
10
|
+
const value = row[field];
|
|
11
|
+
if (typeof value === "number" && Number.isFinite(value))
|
|
12
|
+
sum += value;
|
|
13
|
+
}
|
|
14
|
+
return sum;
|
|
15
|
+
}
|
|
16
|
+
/** Buckets a flat issues-by-path map (FormSnapshot.errors shape — see
|
|
17
|
+
* packages/headless/src/form/zod-bridge.ts groupIssuesByPath) into the
|
|
18
|
+
* three levels an embedded-list widget renders. Only keys that are
|
|
19
|
+
* exactly `listField`, `listField.N`, or `listField.N.subfield` are
|
|
20
|
+
* claimed; unrelated keys (other top-level fields, or deeper nesting
|
|
21
|
+
* than this field ever produces) are ignored. */
|
|
22
|
+
export function groupEmbeddedListIssues(allIssues, listField) {
|
|
23
|
+
const listIssues = [];
|
|
24
|
+
const rowIssues = {};
|
|
25
|
+
const cellIssues = {};
|
|
26
|
+
const prefix = `${listField}.`;
|
|
27
|
+
for (const [path, issues] of Object.entries(allIssues)) {
|
|
28
|
+
if (path === listField) {
|
|
29
|
+
listIssues.push(...issues);
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
if (!path.startsWith(prefix))
|
|
33
|
+
continue;
|
|
34
|
+
const rest = path.slice(prefix.length);
|
|
35
|
+
const segments = rest.split(".");
|
|
36
|
+
if (segments.length === 1) {
|
|
37
|
+
const rowIndex = parsePureRowIndex(segments[0]);
|
|
38
|
+
if (rowIndex === undefined)
|
|
39
|
+
continue;
|
|
40
|
+
rowIssues[rowIndex] = issues;
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
if (segments.length === 2) {
|
|
44
|
+
const rowIndex = parsePureRowIndex(segments[0]);
|
|
45
|
+
if (rowIndex === undefined)
|
|
46
|
+
continue;
|
|
47
|
+
const cellField = segments[1];
|
|
48
|
+
cellIssues[`${rowIndex}.${cellField}`] = issues;
|
|
49
|
+
}
|
|
50
|
+
// Deeper than `listField.N.subfield` — not a shape this field ever
|
|
51
|
+
// produces; ignore rather than misattribute to a cell.
|
|
52
|
+
}
|
|
53
|
+
return { listIssues, rowIssues, cellIssues };
|
|
54
|
+
}
|
|
55
|
+
function parsePureRowIndex(segment) {
|
|
56
|
+
if (segment === undefined || segment.length === 0)
|
|
57
|
+
return undefined;
|
|
58
|
+
for (const char of segment) {
|
|
59
|
+
if (char < "0" || char > "9")
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
return Number(segment);
|
|
63
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export type { ComputeEditViewModelInput } from "./edit.js";
|
|
2
|
+
export { computeEditViewModel } from "./edit.js";
|
|
3
|
+
export type { DerivedCellRoundingTarget, EmbeddedDerivedOp, EmbeddedListIssueGroups, } from "./embedded-list.js";
|
|
4
|
+
export { computeDerivedCellValue, groupEmbeddedListIssues, roundDerivedCellValue, sumEmbeddedListColumn, } from "./embedded-list.js";
|
|
5
|
+
export type { ComputeListViewModelInput } from "./list.js";
|
|
6
|
+
export { computeListViewModel, embeddedCellLabelKey, embeddedCellOptionLabelKey, fieldLabelKey, fieldOptionLabelKey, fieldOptionLabelKeyPrefix, } from "./list.js";
|
|
7
|
+
export type { EditExtensionSectionViewModel, EditFieldSpec, EditFieldsSectionViewModel, EditFieldViewModel, EditRelatedListSectionViewModel, EditSectionSpec, EditSectionViewModel, EditViewModel, EditWriteFormSectionViewModel, EmbeddedListCellViewModel, FieldConditionCtx, FieldRenderer, ListColumnSpec, ListColumnViewModel, ListRowViewModel, ListViewModel, RuntimeRenderer, ScreenSlots, Translate, } from "./types.js";
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { computeEditViewModel } from "./edit.js";
|
|
2
|
+
export { computeDerivedCellValue, groupEmbeddedListIssues, roundDerivedCellValue, sumEmbeddedListColumn, } from "./embedded-list.js";
|
|
3
|
+
export { computeListViewModel, embeddedCellLabelKey, embeddedCellOptionLabelKey, fieldLabelKey, fieldOptionLabelKey, fieldOptionLabelKeyPrefix, } from "./list.js";
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { EntityDefinition, EntityListScreenDefinition } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
2
|
+
import type { ListViewModel, Translate } from "./types.js";
|
|
3
|
+
export type ComputeListViewModelInput = {
|
|
4
|
+
readonly screen: EntityListScreenDefinition;
|
|
5
|
+
readonly entity: EntityDefinition;
|
|
6
|
+
readonly rows: readonly Readonly<Record<string, unknown>>[];
|
|
7
|
+
readonly translate: Translate;
|
|
8
|
+
readonly featureName: string;
|
|
9
|
+
};
|
|
10
|
+
export declare function computeListViewModel(input: ComputeListViewModelInput): ListViewModel;
|
|
11
|
+
export declare function fieldLabelKey(featureName: string, entityName: string, fieldName: string): string;
|
|
12
|
+
export declare function fieldOptionLabelKeyPrefix(featureName: string, entityName: string, fieldName: string): string;
|
|
13
|
+
export declare function fieldOptionLabelKey(featureName: string, entityName: string, fieldName: string, value: string): string;
|
|
14
|
+
export declare function embeddedCellLabelKey(featureName: string, entityName: string, fieldName: string, subFieldName: string): string;
|
|
15
|
+
export declare function embeddedCellOptionLabelKey(featureName: string, entityName: string, fieldName: string, subFieldName: string, value: string): string;
|
|
16
|
+
export declare function buildOptionLabels(translate: (key: string, params?: Readonly<Record<string, unknown>>) => string, keyFor: (value: string) => string, options: readonly string[]): Readonly<Record<string, string>>;
|