@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
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
import type { EmbeddedDerivedCellDef } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
2
|
-
import type { FieldIssue } from "../dispatcher";
|
|
3
|
-
|
|
4
|
-
export type EmbeddedDerivedOp = EmbeddedDerivedCellDef["op"];
|
|
5
|
-
|
|
6
|
-
export type { DerivedCellRoundingTarget } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
7
|
-
// Canonical implementation moved to packages/framework/src/engine/embedded-derived.ts
|
|
8
|
-
// (the write-schema preprocess needs it server-side too).
|
|
9
|
-
export {
|
|
10
|
-
computeDerivedCellValue,
|
|
11
|
-
roundDerivedCellValue,
|
|
12
|
-
} from "@cosmicdrift/kumiko-framework/ui-types";
|
|
13
|
-
|
|
14
|
-
/** Sums a numeric/money/decimal column across all rows. Non-numeric or
|
|
15
|
-
* missing values count as 0. Money columns are minor-unit integers —
|
|
16
|
-
* the sum stays in minor units, never rounds through a major-unit float. */
|
|
17
|
-
export function sumEmbeddedListColumn(
|
|
18
|
-
rows: readonly Readonly<Record<string, unknown>>[],
|
|
19
|
-
field: string,
|
|
20
|
-
): number {
|
|
21
|
-
let sum = 0;
|
|
22
|
-
for (const row of rows) {
|
|
23
|
-
const value = row[field];
|
|
24
|
-
if (typeof value === "number" && Number.isFinite(value)) sum += value;
|
|
25
|
-
}
|
|
26
|
-
return sum;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export type EmbeddedListIssueGroups = {
|
|
30
|
-
/** Issues at exactly `listField` — e.g. min/max row-count violations. */
|
|
31
|
-
readonly listIssues: readonly FieldIssue[];
|
|
32
|
-
/** Issues at exactly `${listField}.${rowIndex}` — a row-level check
|
|
33
|
-
* (e.g. "row incomplete") that isn't attributable to one cell. */
|
|
34
|
-
readonly rowIssues: Readonly<Record<number, readonly FieldIssue[]>>;
|
|
35
|
-
/** Issues at exactly `${listField}.${rowIndex}.${cellField}`, keyed
|
|
36
|
-
* `${rowIndex}.${cellField}` (not the full path — the caller already
|
|
37
|
-
* knows listField). */
|
|
38
|
-
readonly cellIssues: Readonly<Record<string, readonly FieldIssue[]>>;
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
/** Buckets a flat issues-by-path map (FormSnapshot.errors shape — see
|
|
42
|
-
* packages/headless/src/form/zod-bridge.ts groupIssuesByPath) into the
|
|
43
|
-
* three levels an embedded-list widget renders. Only keys that are
|
|
44
|
-
* exactly `listField`, `listField.N`, or `listField.N.subfield` are
|
|
45
|
-
* claimed; unrelated keys (other top-level fields, or deeper nesting
|
|
46
|
-
* than this field ever produces) are ignored. */
|
|
47
|
-
export function groupEmbeddedListIssues(
|
|
48
|
-
allIssues: Readonly<Record<string, readonly FieldIssue[]>>,
|
|
49
|
-
listField: string,
|
|
50
|
-
): EmbeddedListIssueGroups {
|
|
51
|
-
const listIssues: FieldIssue[] = [];
|
|
52
|
-
const rowIssues: Record<number, readonly FieldIssue[]> = {};
|
|
53
|
-
const cellIssues: Record<string, readonly FieldIssue[]> = {};
|
|
54
|
-
|
|
55
|
-
const prefix = `${listField}.`;
|
|
56
|
-
for (const [path, issues] of Object.entries(allIssues)) {
|
|
57
|
-
if (path === listField) {
|
|
58
|
-
listIssues.push(...issues);
|
|
59
|
-
continue;
|
|
60
|
-
}
|
|
61
|
-
if (!path.startsWith(prefix)) continue;
|
|
62
|
-
const rest = path.slice(prefix.length);
|
|
63
|
-
const segments = rest.split(".");
|
|
64
|
-
if (segments.length === 1) {
|
|
65
|
-
const rowIndex = parsePureRowIndex(segments[0]);
|
|
66
|
-
if (rowIndex === undefined) continue;
|
|
67
|
-
rowIssues[rowIndex] = issues;
|
|
68
|
-
continue;
|
|
69
|
-
}
|
|
70
|
-
if (segments.length === 2) {
|
|
71
|
-
const rowIndex = parsePureRowIndex(segments[0]);
|
|
72
|
-
if (rowIndex === undefined) continue;
|
|
73
|
-
const cellField = segments[1];
|
|
74
|
-
cellIssues[`${rowIndex}.${cellField}`] = issues;
|
|
75
|
-
}
|
|
76
|
-
// Deeper than `listField.N.subfield` — not a shape this field ever
|
|
77
|
-
// produces; ignore rather than misattribute to a cell.
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
return { listIssues, rowIssues, cellIssues };
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function parsePureRowIndex(segment: string | undefined): number | undefined {
|
|
84
|
-
if (segment === undefined || segment.length === 0) return undefined;
|
|
85
|
-
for (const char of segment) {
|
|
86
|
-
if (char < "0" || char > "9") return undefined;
|
|
87
|
-
}
|
|
88
|
-
return Number(segment);
|
|
89
|
-
}
|
package/src/view-model/index.ts
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
export type { ComputeEditViewModelInput } from "./edit";
|
|
2
|
-
export { computeEditViewModel } from "./edit";
|
|
3
|
-
export type {
|
|
4
|
-
DerivedCellRoundingTarget,
|
|
5
|
-
EmbeddedDerivedOp,
|
|
6
|
-
EmbeddedListIssueGroups,
|
|
7
|
-
} from "./embedded-list";
|
|
8
|
-
export {
|
|
9
|
-
computeDerivedCellValue,
|
|
10
|
-
groupEmbeddedListIssues,
|
|
11
|
-
roundDerivedCellValue,
|
|
12
|
-
sumEmbeddedListColumn,
|
|
13
|
-
} from "./embedded-list";
|
|
14
|
-
export type { ComputeListViewModelInput } from "./list";
|
|
15
|
-
export {
|
|
16
|
-
computeListViewModel,
|
|
17
|
-
embeddedCellLabelKey,
|
|
18
|
-
embeddedCellOptionLabelKey,
|
|
19
|
-
fieldLabelKey,
|
|
20
|
-
fieldOptionLabelKey,
|
|
21
|
-
fieldOptionLabelKeyPrefix,
|
|
22
|
-
} from "./list";
|
|
23
|
-
export type {
|
|
24
|
-
EditExtensionSectionViewModel,
|
|
25
|
-
EditFieldSpec,
|
|
26
|
-
EditFieldsSectionViewModel,
|
|
27
|
-
EditFieldViewModel,
|
|
28
|
-
EditRelatedListSectionViewModel,
|
|
29
|
-
EditSectionSpec,
|
|
30
|
-
EditSectionViewModel,
|
|
31
|
-
EditViewModel,
|
|
32
|
-
EditWriteFormSectionViewModel,
|
|
33
|
-
EmbeddedListCellViewModel,
|
|
34
|
-
FieldConditionCtx,
|
|
35
|
-
FieldRenderer,
|
|
36
|
-
ListColumnSpec,
|
|
37
|
-
ListColumnViewModel,
|
|
38
|
-
ListRowViewModel,
|
|
39
|
-
ListViewModel,
|
|
40
|
-
RuntimeRenderer,
|
|
41
|
-
ScreenSlots,
|
|
42
|
-
Translate,
|
|
43
|
-
} from "./types";
|
package/src/view-model/list.ts
DELETED
|
@@ -1,280 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
EntityDefinition,
|
|
3
|
-
EntityListScreenDefinition,
|
|
4
|
-
FieldDefinition,
|
|
5
|
-
} from "@cosmicdrift/kumiko-framework/ui-types";
|
|
6
|
-
import {
|
|
7
|
-
LIST_ROW_META_COLUMNS,
|
|
8
|
-
LIST_ROW_META_REFERENCES,
|
|
9
|
-
normalizeListColumn,
|
|
10
|
-
parseRefTarget,
|
|
11
|
-
} from "@cosmicdrift/kumiko-framework/ui-types";
|
|
12
|
-
import type { ListColumnViewModel, ListRowViewModel, ListViewModel, Translate } from "./types";
|
|
13
|
-
|
|
14
|
-
export type ComputeListViewModelInput = {
|
|
15
|
-
readonly screen: EntityListScreenDefinition;
|
|
16
|
-
readonly entity: EntityDefinition;
|
|
17
|
-
readonly rows: readonly Readonly<Record<string, unknown>>[];
|
|
18
|
-
// Translate callback — normally LocaleResolver.translate. Labels use the
|
|
19
|
-
// i18n convention "{feature}:entity:{entityName}:field:{fieldName}"; if
|
|
20
|
-
// the key is absent from the active bundle, i18next falls back to the
|
|
21
|
-
// key itself, which is fine for dev.
|
|
22
|
-
readonly translate: Translate;
|
|
23
|
-
// Feature + entity name are required for i18n key composition —
|
|
24
|
-
// ScreenDefinition carries `entity: string` but not the feature scope,
|
|
25
|
-
// and i18n keys are prefixed by feature. Passed in by the caller (the
|
|
26
|
-
// renderer knows its host-feature context).
|
|
27
|
-
readonly featureName: string;
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
// Pure transform: takes the declared screen + entity + incoming rows, spits
|
|
31
|
-
// out the flat shape the renderer draws. No conditions, no access-checks —
|
|
32
|
-
// those are list-level decisions that the caller makes BEFORE calling
|
|
33
|
-
// here (e.g. filtering rows by ownership on the server / before query).
|
|
34
|
-
// Field-level read-access (Level 3 of UI-architecture.md §Permission) is a
|
|
35
|
-
// follow-up; this v1 assumes the caller passed a row-filter that drops any
|
|
36
|
-
// field the user may not see.
|
|
37
|
-
export function computeListViewModel(input: ComputeListViewModelInput): ListViewModel {
|
|
38
|
-
const { screen, entity, rows, translate, featureName } = input;
|
|
39
|
-
|
|
40
|
-
const columns: ListColumnViewModel[] = [];
|
|
41
|
-
for (const spec of screen.columns) {
|
|
42
|
-
const normalized = normalizeListColumn(spec);
|
|
43
|
-
if (normalized.refEntity !== undefined) {
|
|
44
|
-
// Declared reference metadata (ListColumnSpec.refEntity) — for
|
|
45
|
-
// projectionList/relatedList columns, which have no EntityDefinition
|
|
46
|
-
// field to carry a real "reference" type. Checked before the
|
|
47
|
-
// entity.fields lookup below so it also fires against the pseudo-
|
|
48
|
-
// entities that hardcode every field as "text" (projection-list-shim,
|
|
49
|
-
// related-list-section) — those never reach the reference branch
|
|
50
|
-
// further down since fieldDef.type is always "text" there.
|
|
51
|
-
const refTarget = parseRefTarget(normalized.refEntity, featureName);
|
|
52
|
-
columns.push({
|
|
53
|
-
field: normalized.field,
|
|
54
|
-
label: translate(
|
|
55
|
-
normalized.label ?? fieldLabelKey(featureName, screen.entity, normalized.field),
|
|
56
|
-
),
|
|
57
|
-
type: "reference",
|
|
58
|
-
// The declared value is the referenced id, not the resolved label —
|
|
59
|
-
// there is no column to server-sort by, same as row-meta references.
|
|
60
|
-
sortable: false,
|
|
61
|
-
refEntity: refTarget.entityName,
|
|
62
|
-
refFeature: refTarget.featureName,
|
|
63
|
-
refLabelField: normalized.refLabelField ?? "id",
|
|
64
|
-
...(normalized.renderer !== undefined && { renderer: normalized.renderer }),
|
|
65
|
-
});
|
|
66
|
-
continue;
|
|
67
|
-
}
|
|
68
|
-
const fieldDef = entity.fields[normalized.field];
|
|
69
|
-
if (!fieldDef) {
|
|
70
|
-
// Not a stored field — may be a read-time derived field (value appended
|
|
71
|
-
// by the list-query handler). Render it as its display valueType; derived
|
|
72
|
-
// columns carry no reference/select metadata and never server-sort.
|
|
73
|
-
const derivedDef = entity.derivedFields?.[normalized.field];
|
|
74
|
-
if (!derivedDef) {
|
|
75
|
-
// Row-meta column (id/tenantId/version/insertedAt/modifiedAt/...) —
|
|
76
|
-
// a real base-table column that's never a declared entity field, e.g.
|
|
77
|
-
// a SystemAdmin cross-tenant list exposing tenantId. Unlike the
|
|
78
|
-
// derivedFields branch above, these are actual DB columns, so
|
|
79
|
-
// server-side ORDER BY works — sortable stays true, except for the
|
|
80
|
-
// ID columns in LIST_ROW_META_REFERENCES (see their reference branch).
|
|
81
|
-
const rowMetaType = LIST_ROW_META_COLUMNS[normalized.field];
|
|
82
|
-
if (rowMetaType !== undefined) {
|
|
83
|
-
const rowMetaRef = LIST_ROW_META_REFERENCES[normalized.field];
|
|
84
|
-
columns.push({
|
|
85
|
-
field: normalized.field,
|
|
86
|
-
label: translate(
|
|
87
|
-
normalized.label ?? fieldLabelKey(featureName, screen.entity, normalized.field),
|
|
88
|
-
),
|
|
89
|
-
type: rowMetaRef !== undefined ? "reference" : rowMetaType,
|
|
90
|
-
// The DB column holds the GUID, not the resolved label — server-side
|
|
91
|
-
// ORDER BY would sort by GUID while the cell shows the name.
|
|
92
|
-
sortable: rowMetaRef === undefined,
|
|
93
|
-
...(normalized.renderer !== undefined && { renderer: normalized.renderer }),
|
|
94
|
-
...(rowMetaRef !== undefined && {
|
|
95
|
-
refEntity: rowMetaRef.refEntity,
|
|
96
|
-
refFeature: rowMetaRef.refFeature,
|
|
97
|
-
refLabelField: rowMetaRef.refLabelField,
|
|
98
|
-
}),
|
|
99
|
-
});
|
|
100
|
-
continue;
|
|
101
|
-
}
|
|
102
|
-
// A virtual presentational column — drawn entirely by a columnRenderer
|
|
103
|
-
// component from the row (e.g. tag chips); value is undefined and it
|
|
104
|
-
// never server-sorts. `field` is just the column key. It needs BOTH a
|
|
105
|
-
// label AND a renderer — renderer is what actually draws it; a label
|
|
106
|
-
// alone with no renderer would push an empty, unrendered column into
|
|
107
|
-
// the view model. Missing either is an author typo (or a stale
|
|
108
|
-
// field-rename) → fail loud instead of silently drawing nothing.
|
|
109
|
-
if (normalized.label !== undefined && normalized.renderer !== undefined) {
|
|
110
|
-
columns.push({
|
|
111
|
-
field: normalized.field,
|
|
112
|
-
label: translate(normalized.label),
|
|
113
|
-
type: "text",
|
|
114
|
-
sortable: false,
|
|
115
|
-
renderer: normalized.renderer,
|
|
116
|
-
});
|
|
117
|
-
continue;
|
|
118
|
-
}
|
|
119
|
-
throw new Error(
|
|
120
|
-
`computeListViewModel: screen "${screen.id}" references unknown field "${normalized.field}" on entity "${screen.entity}"`,
|
|
121
|
-
);
|
|
122
|
-
}
|
|
123
|
-
columns.push({
|
|
124
|
-
field: normalized.field,
|
|
125
|
-
label: translate(
|
|
126
|
-
normalized.label ?? fieldLabelKey(featureName, screen.entity, normalized.field),
|
|
127
|
-
),
|
|
128
|
-
type: derivedDef.valueType,
|
|
129
|
-
// Display-only: a header sort would round-trip to the server, which has
|
|
130
|
-
// no column to sort by (see DerivedFieldDef). Never offer the affordance.
|
|
131
|
-
sortable: false,
|
|
132
|
-
...(normalized.renderer !== undefined && { renderer: normalized.renderer }),
|
|
133
|
-
});
|
|
134
|
-
continue;
|
|
135
|
-
}
|
|
136
|
-
const label = translate(
|
|
137
|
-
normalized.label ?? fieldLabelKey(featureName, screen.entity, normalized.field),
|
|
138
|
-
);
|
|
139
|
-
// Tier 2.7e-3 + Cross-Feature: Reference-Field — entity-String
|
|
140
|
-
// kann same-feature ("user") oder cross-feature ("users:user")
|
|
141
|
-
// sein. parseRefTarget gibt (featureName, entityName), der
|
|
142
|
-
// Renderer baut die Lookup-QN als
|
|
143
|
-
// `<refFeature>:query:<refEntity>:list`.
|
|
144
|
-
const refRaw =
|
|
145
|
-
fieldDef.type === "reference"
|
|
146
|
-
? (fieldDef as unknown as { entity?: string }).entity
|
|
147
|
-
: undefined;
|
|
148
|
-
const refTarget = refRaw !== undefined ? parseRefTarget(refRaw, featureName) : undefined;
|
|
149
|
-
const refEntity = refTarget?.entityName;
|
|
150
|
-
const refFeature = refTarget?.featureName;
|
|
151
|
-
const refLabelField =
|
|
152
|
-
fieldDef.type === "reference"
|
|
153
|
-
? ((fieldDef as unknown as { labelField?: string }).labelField ?? "id")
|
|
154
|
-
: undefined;
|
|
155
|
-
// For select and multiSelect fields: bake in translated option labels.
|
|
156
|
-
// Convention matches the form path → one translation source for list +
|
|
157
|
-
// form. A missing key returns the key itself per convention; the
|
|
158
|
-
// renderer then falls back to humanizeSlug.
|
|
159
|
-
const optionLabels =
|
|
160
|
-
fieldDef.type === "select" || fieldDef.type === "multiSelect"
|
|
161
|
-
? buildOptionLabels(
|
|
162
|
-
translate,
|
|
163
|
-
(value) => fieldOptionLabelKey(featureName, screen.entity, normalized.field, value),
|
|
164
|
-
(fieldDef as unknown as { options?: readonly string[] }).options ?? [],
|
|
165
|
-
)
|
|
166
|
-
: undefined;
|
|
167
|
-
const optionTones = fieldDef.type === "select" ? fieldDef.optionTones : undefined;
|
|
168
|
-
const grouping = fieldDef.type === "number" ? fieldDef.grouping : undefined;
|
|
169
|
-
const column: ListColumnViewModel = {
|
|
170
|
-
field: normalized.field,
|
|
171
|
-
label,
|
|
172
|
-
type: fieldDef.type,
|
|
173
|
-
sortable: fieldIsSortable(fieldDef),
|
|
174
|
-
...(normalized.renderer !== undefined && { renderer: normalized.renderer }),
|
|
175
|
-
...(optionLabels !== undefined && { optionLabels }),
|
|
176
|
-
...(optionTones !== undefined && { optionTones }),
|
|
177
|
-
...(refEntity !== undefined && { refEntity }),
|
|
178
|
-
...(refFeature !== undefined && { refFeature }),
|
|
179
|
-
...(refLabelField !== undefined && { refLabelField }),
|
|
180
|
-
...(grouping !== undefined && { grouping }),
|
|
181
|
-
...(normalized.hideOnNarrow === true && { hideOnNarrow: true }),
|
|
182
|
-
};
|
|
183
|
-
columns.push(column);
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
const listRows: ListRowViewModel[] = rows.map((row) => ({
|
|
187
|
-
id: String(row["id"] ?? ""),
|
|
188
|
-
values: row,
|
|
189
|
-
}));
|
|
190
|
-
|
|
191
|
-
return {
|
|
192
|
-
screenId: screen.id,
|
|
193
|
-
entityName: screen.entity,
|
|
194
|
-
columns,
|
|
195
|
-
rows: listRows,
|
|
196
|
-
...(screen.slots && { slots: screen.slots }),
|
|
197
|
-
isEmpty: listRows.length === 0,
|
|
198
|
-
};
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
// Field-i18n-key convention matches what features register translations
|
|
202
|
-
// under (see packages/framework/src/i18n/ for the pattern). Duplicated
|
|
203
|
-
// here as a plain function — the ui-core boundary forbids depending on
|
|
204
|
-
// the i18n module directly.
|
|
205
|
-
export function fieldLabelKey(featureName: string, entityName: string, fieldName: string): string {
|
|
206
|
-
return `${featureName}:entity:${entityName}:field:${fieldName}`;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
export function fieldOptionLabelKeyPrefix(
|
|
210
|
-
featureName: string,
|
|
211
|
-
entityName: string,
|
|
212
|
-
fieldName: string,
|
|
213
|
-
): string {
|
|
214
|
-
return `${fieldLabelKey(featureName, entityName, fieldName)}:option:`;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
export function fieldOptionLabelKey(
|
|
218
|
-
featureName: string,
|
|
219
|
-
entityName: string,
|
|
220
|
-
fieldName: string,
|
|
221
|
-
value: string,
|
|
222
|
-
): string {
|
|
223
|
-
return `${fieldOptionLabelKeyPrefix(featureName, entityName, fieldName)}${value}`;
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
// Embedded-list cell label key — one level deeper than fieldLabelKey,
|
|
227
|
-
// keyed by the sub-field name inside an embedded-LIST field's `schema`
|
|
228
|
-
// (invoice-positions-style tables built via createEmbeddedListField()).
|
|
229
|
-
export function embeddedCellLabelKey(
|
|
230
|
-
featureName: string,
|
|
231
|
-
entityName: string,
|
|
232
|
-
fieldName: string,
|
|
233
|
-
subFieldName: string,
|
|
234
|
-
): string {
|
|
235
|
-
return `${featureName}:entity:${entityName}:field:${fieldName}:cell:${subFieldName}`;
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
export function embeddedCellOptionLabelKey(
|
|
239
|
-
featureName: string,
|
|
240
|
-
entityName: string,
|
|
241
|
-
fieldName: string,
|
|
242
|
-
subFieldName: string,
|
|
243
|
-
value: string,
|
|
244
|
-
): string {
|
|
245
|
-
return `${featureName}:entity:${entityName}:field:${fieldName}:cell:${subFieldName}:option:${value}`;
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
// Build a value→label map for a select-field's options. Convention:
|
|
249
|
-
// translate() returns the input key when the lookup misses (i18next
|
|
250
|
-
// default + LocaleResolver convention) — we surface the *raw value* in
|
|
251
|
-
// that case so the renderer's humanizeSlug fallback can take over.
|
|
252
|
-
// Without that fallback, an unlabeled option would render as the full
|
|
253
|
-
// `feature:entity:field:option:value`-key.
|
|
254
|
-
//
|
|
255
|
-
// `keyFor` carries whichever key convention the caller needs (top-level
|
|
256
|
-
// field via fieldOptionLabelKey, embedded-list cell via
|
|
257
|
-
// embeddedCellOptionLabelKey) — shared between list-VM, edit-VM, and
|
|
258
|
-
// embedded-list cells so all three produce identical option-translations.
|
|
259
|
-
export function buildOptionLabels(
|
|
260
|
-
translate: (key: string, params?: Readonly<Record<string, unknown>>) => string,
|
|
261
|
-
keyFor: (value: string) => string,
|
|
262
|
-
options: readonly string[],
|
|
263
|
-
): Readonly<Record<string, string>> {
|
|
264
|
-
const out: Record<string, string> = {};
|
|
265
|
-
for (const value of options) {
|
|
266
|
-
const key = keyFor(value);
|
|
267
|
-
const translated = translate(key);
|
|
268
|
-
out[value] = translated === key ? value : translated;
|
|
269
|
-
}
|
|
270
|
-
return out;
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
// A field can declare `sortable: true` on the FieldDefinition. This is
|
|
274
|
-
// framework-level metadata used both by the server's list-query builder
|
|
275
|
-
// (ORDER BY safety) and by the UI (column header click indicator). All
|
|
276
|
-
// field types can bear the flag; it's off by default.
|
|
277
|
-
function fieldIsSortable(field: FieldDefinition): boolean {
|
|
278
|
-
const flag = (field as unknown as { sortable?: boolean }).sortable;
|
|
279
|
-
return flag === true;
|
|
280
|
-
}
|