@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,96 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import type { FieldIssue } from "../../dispatcher";
|
|
3
|
-
import {
|
|
4
|
-
computeDerivedCellValue,
|
|
5
|
-
groupEmbeddedListIssues,
|
|
6
|
-
roundDerivedCellValue,
|
|
7
|
-
sumEmbeddedListColumn,
|
|
8
|
-
} from "../embedded-list";
|
|
9
|
-
|
|
10
|
-
function issue(path: string): FieldIssue {
|
|
11
|
-
return { path, code: "custom", i18nKey: "errors.validation.custom" };
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
describe("computeDerivedCellValue", () => {
|
|
15
|
-
test("multiply with two values", () => {
|
|
16
|
-
expect(computeDerivedCellValue("multiply", [3, 4])).toBe(12);
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
test("multiply with three values", () => {
|
|
20
|
-
expect(computeDerivedCellValue("multiply", [2, 3, 5])).toBe(30);
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
test("sum adds all values", () => {
|
|
24
|
-
expect(computeDerivedCellValue("sum", [1, 2, 3])).toBe(6);
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
test("subtract subtracts every value after the first", () => {
|
|
28
|
-
expect(computeDerivedCellValue("subtract", [10, 3, 2])).toBe(5);
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
test("multiply returns undefined when a source is missing", () => {
|
|
32
|
-
expect(computeDerivedCellValue("multiply", [3, undefined])).toBeUndefined();
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
test("sum treats a missing source as 0", () => {
|
|
36
|
-
expect(computeDerivedCellValue("sum", [1, undefined, 2])).toBe(3);
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
test("subtract treats a missing source as 0", () => {
|
|
40
|
-
expect(computeDerivedCellValue("subtract", [10, undefined])).toBe(10);
|
|
41
|
-
});
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
describe("roundDerivedCellValue", () => {
|
|
45
|
-
test("rounds a money target to whole minor units, kaufmännisch", () => {
|
|
46
|
-
expect(roundDerivedCellValue(2307.58, { type: "money" })).toBe(2308);
|
|
47
|
-
expect(roundDerivedCellValue(-2307.5, { type: "money" })).toBe(-2308);
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
test("leaves a number target unrounded", () => {
|
|
51
|
-
expect(roundDerivedCellValue(1.5, { type: "number" })).toBe(1.5);
|
|
52
|
-
});
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
describe("sumEmbeddedListColumn", () => {
|
|
56
|
-
test("sums a numeric column across rows", () => {
|
|
57
|
-
const rows = [{ amount: 100 }, { amount: 250 }, { amount: 50 }];
|
|
58
|
-
expect(sumEmbeddedListColumn(rows, "amount")).toBe(400);
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
test("treats a row missing the field as 0", () => {
|
|
62
|
-
const rows = [{ amount: 100 }, { other: 1 }, { amount: 50 }];
|
|
63
|
-
expect(sumEmbeddedListColumn(rows, "amount")).toBe(150);
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
test("returns 0 for an empty rows array", () => {
|
|
67
|
-
expect(sumEmbeddedListColumn([], "amount")).toBe(0);
|
|
68
|
-
});
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
describe("groupEmbeddedListIssues", () => {
|
|
72
|
-
test("buckets list/row/cell issues, ignoring unrelated and too-deep keys", () => {
|
|
73
|
-
const allIssues: Record<string, readonly FieldIssue[]> = {
|
|
74
|
-
lines: [issue("lines")],
|
|
75
|
-
"lines.0": [issue("lines.0")],
|
|
76
|
-
"lines.0.amount": [issue("lines.0.amount")],
|
|
77
|
-
"lines.1.qty": [issue("lines.1.qty")],
|
|
78
|
-
title: [issue("title")],
|
|
79
|
-
"lines.0.amount.nested": [issue("lines.0.amount.nested")],
|
|
80
|
-
};
|
|
81
|
-
|
|
82
|
-
const result = groupEmbeddedListIssues(allIssues, "lines");
|
|
83
|
-
|
|
84
|
-
expect(result.listIssues).toEqual([issue("lines")]);
|
|
85
|
-
expect(result.rowIssues).toEqual({ 0: [issue("lines.0")] });
|
|
86
|
-
expect(result.cellIssues).toEqual({
|
|
87
|
-
"0.amount": [issue("lines.0.amount")],
|
|
88
|
-
"1.qty": [issue("lines.1.qty")],
|
|
89
|
-
});
|
|
90
|
-
// Unrelated top-level key and the too-deep key must not leak anywhere.
|
|
91
|
-
expect(Object.values(result.cellIssues).flat()).not.toContainEqual(issue("title"));
|
|
92
|
-
expect(Object.values(result.cellIssues).flat()).not.toContainEqual(
|
|
93
|
-
issue("lines.0.amount.nested"),
|
|
94
|
-
);
|
|
95
|
-
});
|
|
96
|
-
});
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import {
|
|
3
|
-
buildOptionLabels,
|
|
4
|
-
embeddedCellLabelKey,
|
|
5
|
-
embeddedCellOptionLabelKey,
|
|
6
|
-
fieldLabelKey,
|
|
7
|
-
fieldOptionLabelKey,
|
|
8
|
-
} from "../list";
|
|
9
|
-
|
|
10
|
-
describe("fieldLabelKey", () => {
|
|
11
|
-
test("follows feature:entity:field convention", () => {
|
|
12
|
-
expect(fieldLabelKey("billing", "invoice", "amount")).toBe(
|
|
13
|
-
"billing:entity:invoice:field:amount",
|
|
14
|
-
);
|
|
15
|
-
});
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
describe("fieldOptionLabelKey", () => {
|
|
19
|
-
test("appends option value segment", () => {
|
|
20
|
-
expect(fieldOptionLabelKey("billing", "invoice", "status", "paid")).toBe(
|
|
21
|
-
"billing:entity:invoice:field:status:option:paid",
|
|
22
|
-
);
|
|
23
|
-
});
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
describe("embeddedCellLabelKey", () => {
|
|
27
|
-
test("adds a cell segment for the sub-field name", () => {
|
|
28
|
-
expect(embeddedCellLabelKey("billing", "invoice", "lines", "quantity")).toBe(
|
|
29
|
-
"billing:entity:invoice:field:lines:cell:quantity",
|
|
30
|
-
);
|
|
31
|
-
});
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
describe("embeddedCellOptionLabelKey", () => {
|
|
35
|
-
test("appends option value segment after the cell segment", () => {
|
|
36
|
-
expect(embeddedCellOptionLabelKey("billing", "invoice", "lines", "unit", "hour")).toBe(
|
|
37
|
-
"billing:entity:invoice:field:lines:cell:unit:option:hour",
|
|
38
|
-
);
|
|
39
|
-
});
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
describe("buildOptionLabels", () => {
|
|
43
|
-
test("maps option values to translated labels with fallback to raw value", () => {
|
|
44
|
-
const labels = buildOptionLabels(
|
|
45
|
-
(key) => (key.endsWith(":option:draft") ? "Draft" : key),
|
|
46
|
-
(value) => fieldOptionLabelKey("tasks", "task", "status", value),
|
|
47
|
-
["draft", "done"],
|
|
48
|
-
);
|
|
49
|
-
expect(labels["draft"]).toBe("Draft");
|
|
50
|
-
expect(labels["done"]).toBe("done");
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
test("keyFor lets the caller supply any key convention (e.g. embedded cell)", () => {
|
|
54
|
-
const labels = buildOptionLabels(
|
|
55
|
-
(key) => (key.endsWith(":option:hour") ? "Hour" : key),
|
|
56
|
-
(value) => embeddedCellOptionLabelKey("billing", "invoice", "lines", "unit", value),
|
|
57
|
-
["hour", "day"],
|
|
58
|
-
);
|
|
59
|
-
expect(labels["hour"]).toBe("Hour");
|
|
60
|
-
expect(labels["day"]).toBe("day");
|
|
61
|
-
});
|
|
62
|
-
});
|
|
@@ -1,439 +0,0 @@
|
|
|
1
|
-
import { describe, expect, mock, test } from "bun:test";
|
|
2
|
-
import type {
|
|
3
|
-
EntityDefinition,
|
|
4
|
-
EntityListScreenDefinition,
|
|
5
|
-
} from "@cosmicdrift/kumiko-framework/ui-types";
|
|
6
|
-
import {
|
|
7
|
-
LIST_ROW_META_COLUMNS,
|
|
8
|
-
LIST_ROW_META_REFERENCES,
|
|
9
|
-
} from "@cosmicdrift/kumiko-framework/ui-types";
|
|
10
|
-
import { computeListViewModel } from "../list";
|
|
11
|
-
|
|
12
|
-
// Minimal EntityDefinition-shape. ui-core's view-model only reads
|
|
13
|
-
// entity.fields and per-field metadata; tests stay untyped-casted via
|
|
14
|
-
// `as unknown as EntityDefinition` so we don't pull the entire framework
|
|
15
|
-
// FieldDefinition union into fixtures.
|
|
16
|
-
const taskEntity = {
|
|
17
|
-
fields: {
|
|
18
|
-
title: { type: "text", required: true, sortable: true },
|
|
19
|
-
done: { type: "boolean" },
|
|
20
|
-
priority: { type: "number", sortable: true },
|
|
21
|
-
role: { type: "select", options: ["admin", "member"] },
|
|
22
|
-
labels: { type: "multiSelect", options: ["urgent", "low-priority"] },
|
|
23
|
-
},
|
|
24
|
-
derivedFields: {
|
|
25
|
-
// Read-time computed (value appended by the list-query handler). The
|
|
26
|
-
// view-model only reads valueType; the derive body never runs here, so a
|
|
27
|
-
// no-op stand-in is enough.
|
|
28
|
-
statusLabel: { valueType: "text", derive: () => "" },
|
|
29
|
-
ageDays: { valueType: "number", derive: () => 0 },
|
|
30
|
-
},
|
|
31
|
-
} as unknown as EntityDefinition;
|
|
32
|
-
|
|
33
|
-
function listScreen(columns: EntityListScreenDefinition["columns"]): EntityListScreenDefinition {
|
|
34
|
-
return {
|
|
35
|
-
id: "tasks:screen:task-list",
|
|
36
|
-
type: "entityList",
|
|
37
|
-
entity: "task",
|
|
38
|
-
columns,
|
|
39
|
-
};
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// Fake translate passes the key through so tests can assert on the
|
|
43
|
-
// key-composition convention without wiring i18next.
|
|
44
|
-
const translate = (key: string) => key;
|
|
45
|
-
|
|
46
|
-
describe("computeListViewModel", () => {
|
|
47
|
-
test("string columns expand to field-name + resolved label + type", () => {
|
|
48
|
-
const vm = computeListViewModel({
|
|
49
|
-
screen: listScreen(["title", "done"]),
|
|
50
|
-
entity: taskEntity,
|
|
51
|
-
rows: [],
|
|
52
|
-
translate,
|
|
53
|
-
featureName: "tasks",
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
expect(vm.columns).toEqual([
|
|
57
|
-
{
|
|
58
|
-
field: "title",
|
|
59
|
-
label: "tasks:entity:task:field:title",
|
|
60
|
-
type: "text",
|
|
61
|
-
sortable: true,
|
|
62
|
-
},
|
|
63
|
-
{
|
|
64
|
-
field: "done",
|
|
65
|
-
label: "tasks:entity:task:field:done",
|
|
66
|
-
type: "boolean",
|
|
67
|
-
sortable: false,
|
|
68
|
-
},
|
|
69
|
-
]);
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
test("object-form column carries renderer through to the view model", () => {
|
|
73
|
-
const fmt = { format: "currency" as const, symbol: "€" };
|
|
74
|
-
const vm = computeListViewModel({
|
|
75
|
-
screen: listScreen([{ field: "title", renderer: fmt }]),
|
|
76
|
-
entity: taskEntity,
|
|
77
|
-
rows: [],
|
|
78
|
-
translate,
|
|
79
|
-
featureName: "tasks",
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
expect(vm.columns[0]?.renderer).toEqual(fmt);
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
test("rows map to { id, values } with id pulled from the row", () => {
|
|
86
|
-
const vm = computeListViewModel({
|
|
87
|
-
screen: listScreen(["title"]),
|
|
88
|
-
entity: taskEntity,
|
|
89
|
-
rows: [
|
|
90
|
-
{ id: "t-1", title: "first" },
|
|
91
|
-
{ id: "t-2", title: "second" },
|
|
92
|
-
],
|
|
93
|
-
translate,
|
|
94
|
-
featureName: "tasks",
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
expect(vm.rows).toEqual([
|
|
98
|
-
{ id: "t-1", values: { id: "t-1", title: "first" } },
|
|
99
|
-
{ id: "t-2", values: { id: "t-2", title: "second" } },
|
|
100
|
-
]);
|
|
101
|
-
expect(vm.isEmpty).toBe(false);
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
test("empty rows list flags isEmpty for the renderer's 'no results' state", () => {
|
|
105
|
-
const vm = computeListViewModel({
|
|
106
|
-
screen: listScreen(["title"]),
|
|
107
|
-
entity: taskEntity,
|
|
108
|
-
rows: [],
|
|
109
|
-
translate,
|
|
110
|
-
featureName: "tasks",
|
|
111
|
-
});
|
|
112
|
-
expect(vm.isEmpty).toBe(true);
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
test("unknown field reference throws — stale rename caught at render-time, not silently", () => {
|
|
116
|
-
expect(() =>
|
|
117
|
-
computeListViewModel({
|
|
118
|
-
screen: listScreen(["doesNotExist"]),
|
|
119
|
-
entity: taskEntity,
|
|
120
|
-
rows: [],
|
|
121
|
-
translate,
|
|
122
|
-
featureName: "tasks",
|
|
123
|
-
}),
|
|
124
|
-
).toThrow(/unknown field "doesNotExist"/);
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
test("labeled column with no matching field → virtual presentational column (no throw)", () => {
|
|
128
|
-
const vm = computeListViewModel({
|
|
129
|
-
screen: listScreen([
|
|
130
|
-
"title",
|
|
131
|
-
{ field: "tags", label: "Tags", renderer: { react: { __component: "TagsCell" } } },
|
|
132
|
-
]),
|
|
133
|
-
entity: taskEntity,
|
|
134
|
-
rows: [],
|
|
135
|
-
translate,
|
|
136
|
-
featureName: "tasks",
|
|
137
|
-
});
|
|
138
|
-
// `field` becomes the column key; label is taken verbatim (translate is
|
|
139
|
-
// identity here), type defaults to text, never server-sortable.
|
|
140
|
-
expect(vm.columns[1]).toEqual({
|
|
141
|
-
field: "tags",
|
|
142
|
-
label: "Tags",
|
|
143
|
-
type: "text",
|
|
144
|
-
sortable: false,
|
|
145
|
-
renderer: { react: { __component: "TagsCell" } },
|
|
146
|
-
});
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
test("labeled column with no matching field AND no renderer → throws (label alone isn't enough)", () => {
|
|
150
|
-
// Regression (697/1): renderer is what actually draws a virtual column —
|
|
151
|
-
// a label with no renderer would otherwise push an empty, unrendered
|
|
152
|
-
// column into the view model instead of catching the author typo.
|
|
153
|
-
expect(() =>
|
|
154
|
-
computeListViewModel({
|
|
155
|
-
screen: listScreen(["title", { field: "tags", label: "Tags" }]),
|
|
156
|
-
entity: taskEntity,
|
|
157
|
-
rows: [],
|
|
158
|
-
translate,
|
|
159
|
-
featureName: "tasks",
|
|
160
|
-
}),
|
|
161
|
-
).toThrow(/unknown field "tags"/);
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
test("label overrides the field-convention header on a real field", () => {
|
|
165
|
-
const vm = computeListViewModel({
|
|
166
|
-
screen: listScreen([{ field: "title", label: "custom.header" }]),
|
|
167
|
-
entity: taskEntity,
|
|
168
|
-
rows: [],
|
|
169
|
-
translate,
|
|
170
|
-
featureName: "tasks",
|
|
171
|
-
});
|
|
172
|
-
// label goes through translate (identity here) instead of the
|
|
173
|
-
// tasks:entity:task:field:title convention key.
|
|
174
|
-
expect(vm.columns[0]?.label).toBe("custom.header");
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
test("translate is called with the expected i18n-key per field", () => {
|
|
178
|
-
const spy = mock((key: string) => `T:${key}`);
|
|
179
|
-
computeListViewModel({
|
|
180
|
-
screen: listScreen(["title", "priority"]),
|
|
181
|
-
entity: taskEntity,
|
|
182
|
-
rows: [],
|
|
183
|
-
translate: spy,
|
|
184
|
-
featureName: "tasks",
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
expect(spy).toHaveBeenCalledWith("tasks:entity:task:field:title");
|
|
188
|
-
expect(spy).toHaveBeenCalledWith("tasks:entity:task:field:priority");
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
test("derived-field column carries its valueType, is display-only (never sortable), no stored-field metadata", () => {
|
|
192
|
-
const vm = computeListViewModel({
|
|
193
|
-
screen: listScreen(["statusLabel", "ageDays"]),
|
|
194
|
-
entity: taskEntity,
|
|
195
|
-
rows: [],
|
|
196
|
-
translate,
|
|
197
|
-
featureName: "tasks",
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
expect(vm.columns).toEqual([
|
|
201
|
-
{
|
|
202
|
-
field: "statusLabel",
|
|
203
|
-
label: "tasks:entity:task:field:statusLabel",
|
|
204
|
-
type: "text",
|
|
205
|
-
sortable: false,
|
|
206
|
-
},
|
|
207
|
-
{
|
|
208
|
-
field: "ageDays",
|
|
209
|
-
label: "tasks:entity:task:field:ageDays",
|
|
210
|
-
type: "number",
|
|
211
|
-
sortable: false,
|
|
212
|
-
},
|
|
213
|
-
]);
|
|
214
|
-
});
|
|
215
|
-
|
|
216
|
-
test("derived column value passes through from the row (handler already appended it)", () => {
|
|
217
|
-
const vm = computeListViewModel({
|
|
218
|
-
screen: listScreen(["title", "statusLabel"]),
|
|
219
|
-
entity: taskEntity,
|
|
220
|
-
rows: [{ id: "t-1", title: "first", statusLabel: "overdue" }],
|
|
221
|
-
translate,
|
|
222
|
-
featureName: "tasks",
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
expect(vm.rows[0]?.values).toEqual({ id: "t-1", title: "first", statusLabel: "overdue" });
|
|
226
|
-
});
|
|
227
|
-
|
|
228
|
-
test("derived columns accept an object-form renderer like stored columns", () => {
|
|
229
|
-
const fmt = { format: "currency" as const, symbol: "€" };
|
|
230
|
-
const vm = computeListViewModel({
|
|
231
|
-
screen: listScreen([{ field: "ageDays", renderer: fmt }]),
|
|
232
|
-
entity: taskEntity,
|
|
233
|
-
rows: [],
|
|
234
|
-
translate,
|
|
235
|
-
featureName: "tasks",
|
|
236
|
-
});
|
|
237
|
-
|
|
238
|
-
expect(vm.columns[0]?.renderer).toEqual(fmt);
|
|
239
|
-
});
|
|
240
|
-
|
|
241
|
-
test("select field column includes translated optionLabels", () => {
|
|
242
|
-
const vm = computeListViewModel({
|
|
243
|
-
screen: listScreen(["role"]),
|
|
244
|
-
entity: taskEntity,
|
|
245
|
-
rows: [],
|
|
246
|
-
translate: (key) => (key === "tasks:entity:task:field:role:option:admin" ? "Admin" : key),
|
|
247
|
-
featureName: "tasks",
|
|
248
|
-
});
|
|
249
|
-
|
|
250
|
-
expect(vm.columns[0]?.optionLabels).toEqual({
|
|
251
|
-
admin: "Admin",
|
|
252
|
-
member: "member",
|
|
253
|
-
});
|
|
254
|
-
});
|
|
255
|
-
|
|
256
|
-
test("multiSelect field column includes translated optionLabels (fw#2491)", () => {
|
|
257
|
-
const vm = computeListViewModel({
|
|
258
|
-
screen: listScreen(["labels"]),
|
|
259
|
-
entity: taskEntity,
|
|
260
|
-
rows: [],
|
|
261
|
-
translate: (key) => (key === "tasks:entity:task:field:labels:option:urgent" ? "Urgent" : key),
|
|
262
|
-
featureName: "tasks",
|
|
263
|
-
});
|
|
264
|
-
|
|
265
|
-
expect(vm.columns[0]?.optionLabels).toEqual({
|
|
266
|
-
urgent: "Urgent",
|
|
267
|
-
"low-priority": "low-priority",
|
|
268
|
-
});
|
|
269
|
-
});
|
|
270
|
-
|
|
271
|
-
// Table-driven over LIST_ROW_META_COLUMNS itself (not one-off cases) — a
|
|
272
|
-
// row-meta column (id/tenantId/version/insertedAt/...) is a base-table
|
|
273
|
-
// column, never a declared entity field (a SystemAdmin cross-tenant list
|
|
274
|
-
// picks tenantId as a column this way). Together with boot-validator.test.ts
|
|
275
|
-
// (both boot validators accept the same set) and list-row-meta-drift.test.ts
|
|
276
|
-
// (LIST_ROW_META_COLUMNS key-set drift guard), this pins "whatever boot
|
|
277
|
-
// accepts as a row-meta column, the renderer can actually draw" — a #2601
|
|
278
|
-
// review found the two sides had drifted apart.
|
|
279
|
-
test("every row-meta column resolves to its declared type, sortable, no throw", () => {
|
|
280
|
-
for (const [field, expectedType] of Object.entries(LIST_ROW_META_COLUMNS)) {
|
|
281
|
-
const vm = computeListViewModel({
|
|
282
|
-
screen: listScreen(["title", field]),
|
|
283
|
-
entity: taskEntity,
|
|
284
|
-
rows: [],
|
|
285
|
-
translate,
|
|
286
|
-
featureName: "tasks",
|
|
287
|
-
});
|
|
288
|
-
|
|
289
|
-
const ref = LIST_ROW_META_REFERENCES[field];
|
|
290
|
-
expect(vm.columns[1]).toEqual({
|
|
291
|
-
field,
|
|
292
|
-
label: `tasks:entity:task:field:${field}`,
|
|
293
|
-
...(ref !== undefined
|
|
294
|
-
? {
|
|
295
|
-
type: "reference",
|
|
296
|
-
sortable: false,
|
|
297
|
-
refFeature: ref.refFeature,
|
|
298
|
-
refEntity: ref.refEntity,
|
|
299
|
-
refLabelField: ref.refLabelField,
|
|
300
|
-
}
|
|
301
|
-
: { type: expectedType, sortable: true }),
|
|
302
|
-
});
|
|
303
|
-
}
|
|
304
|
-
});
|
|
305
|
-
|
|
306
|
-
// tenantId carries a GUID in the DB but should render the referenced
|
|
307
|
-
// tenant's display name, like a declared reference field does — not the
|
|
308
|
-
// raw GUID. insertedById stays unchanged: this also pins that the map
|
|
309
|
-
// switches exactly one column, not every row-meta ID column.
|
|
310
|
-
test("tenantId row-meta column resolves as a reference column, insertedById unchanged", () => {
|
|
311
|
-
const vm = computeListViewModel({
|
|
312
|
-
screen: listScreen(["tenantId", "insertedById", "id"]),
|
|
313
|
-
entity: taskEntity,
|
|
314
|
-
rows: [],
|
|
315
|
-
translate,
|
|
316
|
-
featureName: "tasks",
|
|
317
|
-
});
|
|
318
|
-
|
|
319
|
-
expect(vm.columns[0]).toEqual({
|
|
320
|
-
field: "tenantId",
|
|
321
|
-
label: "tasks:entity:task:field:tenantId",
|
|
322
|
-
type: "reference",
|
|
323
|
-
sortable: false,
|
|
324
|
-
refFeature: "tenant",
|
|
325
|
-
refEntity: "tenant",
|
|
326
|
-
refLabelField: "name",
|
|
327
|
-
});
|
|
328
|
-
expect(vm.columns[1]).toEqual({
|
|
329
|
-
field: "insertedById",
|
|
330
|
-
label: "tasks:entity:task:field:insertedById",
|
|
331
|
-
type: "text",
|
|
332
|
-
sortable: true,
|
|
333
|
-
});
|
|
334
|
-
expect(vm.columns[2]).toEqual({
|
|
335
|
-
field: "id",
|
|
336
|
-
label: "tasks:entity:task:field:id",
|
|
337
|
-
type: "text",
|
|
338
|
-
sortable: true,
|
|
339
|
-
});
|
|
340
|
-
});
|
|
341
|
-
|
|
342
|
-
// projectionList/relatedList columns have no real EntityDefinition — the
|
|
343
|
-
// shim that adapts them synthesizes a pseudo-entity where every field is
|
|
344
|
-
// hardcoded to "text" (see projection-list-shim.ts /
|
|
345
|
-
// related-list-section.tsx). ListColumnSpec.refEntity must be evaluated
|
|
346
|
-
// BEFORE the entity.fields lookup, or it never fires against that shape.
|
|
347
|
-
const projectionPseudoEntity = {
|
|
348
|
-
fields: {
|
|
349
|
-
tenantId: { type: "text" },
|
|
350
|
-
type: { type: "text" },
|
|
351
|
-
},
|
|
352
|
-
} as unknown as EntityDefinition;
|
|
353
|
-
|
|
354
|
-
test("ListColumnSpec.refEntity marks the column as a reference, even against a pseudo-entity whose fields are all 'text' (fw#2662)", () => {
|
|
355
|
-
const vm = computeListViewModel({
|
|
356
|
-
screen: listScreen([
|
|
357
|
-
{
|
|
358
|
-
field: "tenantId",
|
|
359
|
-
label: "delivery.log.col.tenantId",
|
|
360
|
-
refEntity: "tenant:tenant",
|
|
361
|
-
refLabelField: "name",
|
|
362
|
-
},
|
|
363
|
-
]),
|
|
364
|
-
entity: projectionPseudoEntity,
|
|
365
|
-
rows: [],
|
|
366
|
-
translate,
|
|
367
|
-
featureName: "delivery",
|
|
368
|
-
});
|
|
369
|
-
|
|
370
|
-
expect(vm.columns[0]).toEqual({
|
|
371
|
-
field: "tenantId",
|
|
372
|
-
label: "delivery.log.col.tenantId",
|
|
373
|
-
type: "reference",
|
|
374
|
-
sortable: false,
|
|
375
|
-
refFeature: "tenant",
|
|
376
|
-
refEntity: "tenant",
|
|
377
|
-
refLabelField: "name",
|
|
378
|
-
});
|
|
379
|
-
});
|
|
380
|
-
|
|
381
|
-
test("non-regression: a projectionList column without refEntity metadata stays 'text', unchanged (fw#2662)", () => {
|
|
382
|
-
const vm = computeListViewModel({
|
|
383
|
-
screen: listScreen([{ field: "type", label: "delivery.log.col.type" }]),
|
|
384
|
-
entity: projectionPseudoEntity,
|
|
385
|
-
rows: [],
|
|
386
|
-
translate,
|
|
387
|
-
featureName: "delivery",
|
|
388
|
-
});
|
|
389
|
-
|
|
390
|
-
expect(vm.columns[0]).toEqual({
|
|
391
|
-
field: "type",
|
|
392
|
-
label: "delivery.log.col.type",
|
|
393
|
-
type: "text",
|
|
394
|
-
sortable: false,
|
|
395
|
-
});
|
|
396
|
-
});
|
|
397
|
-
|
|
398
|
-
test("row-meta column: explicit label + renderer are passed through unchanged", () => {
|
|
399
|
-
const fmt = { format: "currency" as const, symbol: "€" };
|
|
400
|
-
const vm = computeListViewModel({
|
|
401
|
-
screen: listScreen([{ field: "insertedAt", label: "custom.created", renderer: fmt }]),
|
|
402
|
-
entity: taskEntity,
|
|
403
|
-
rows: [],
|
|
404
|
-
translate,
|
|
405
|
-
featureName: "tasks",
|
|
406
|
-
});
|
|
407
|
-
|
|
408
|
-
expect(vm.columns[0]).toEqual({
|
|
409
|
-
field: "insertedAt",
|
|
410
|
-
label: "custom.created",
|
|
411
|
-
type: "timestamp",
|
|
412
|
-
sortable: true,
|
|
413
|
-
renderer: fmt,
|
|
414
|
-
});
|
|
415
|
-
});
|
|
416
|
-
|
|
417
|
-
test("slots pass through unchanged for the renderer to mount", () => {
|
|
418
|
-
const slots = { header: { react: { component: "HeaderRef" } } };
|
|
419
|
-
const vm = computeListViewModel({
|
|
420
|
-
screen: { ...listScreen(["title"]), slots },
|
|
421
|
-
entity: taskEntity,
|
|
422
|
-
rows: [],
|
|
423
|
-
translate,
|
|
424
|
-
featureName: "tasks",
|
|
425
|
-
});
|
|
426
|
-
expect(vm.slots).toBe(slots);
|
|
427
|
-
});
|
|
428
|
-
test("hideOnNarrow is carried onto the column view model only when set", () => {
|
|
429
|
-
const vm = computeListViewModel({
|
|
430
|
-
screen: listScreen(["title", { field: "priority", hideOnNarrow: true }]),
|
|
431
|
-
entity: taskEntity,
|
|
432
|
-
rows: [],
|
|
433
|
-
translate,
|
|
434
|
-
featureName: "tasks",
|
|
435
|
-
});
|
|
436
|
-
expect(vm.columns[0]?.hideOnNarrow).toBeUndefined();
|
|
437
|
-
expect(vm.columns[1]?.hideOnNarrow).toBe(true);
|
|
438
|
-
});
|
|
439
|
-
});
|