@cosmicdrift/kumiko-renderer-web 0.34.2 → 0.35.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-renderer-web",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.35.0",
|
|
4
4
|
"description": "Web-platform bindings for @cosmicdrift/kumiko-renderer. HTML default-primitives, browser history-based navigation, EventSource-backed live events, and a one-call createKumikoApp that mounts the whole stack via react-dom.",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -57,20 +57,20 @@ describe("RenderList — column-renderer registry", () => {
|
|
|
57
57
|
warnSpy.mockRestore();
|
|
58
58
|
});
|
|
59
59
|
|
|
60
|
-
test("
|
|
61
|
-
const
|
|
60
|
+
test("FormatSpec-renderer: currency formatiert Wert + Symbol", () => {
|
|
61
|
+
const screenWithFmt: EntityListScreenDefinition = {
|
|
62
62
|
...baseScreen,
|
|
63
|
-
columns: ["title", { field: "color", renderer:
|
|
63
|
+
columns: ["title", { field: "color", renderer: { format: "currency", symbol: "€" } }],
|
|
64
64
|
};
|
|
65
65
|
render(
|
|
66
66
|
<RenderList
|
|
67
|
-
screen={
|
|
67
|
+
screen={screenWithFmt}
|
|
68
68
|
entity={taskEntity}
|
|
69
|
-
rows={[{ id: "r1", title: "Alpha", color: "
|
|
69
|
+
rows={[{ id: "r1", title: "Alpha", color: "42" }]}
|
|
70
70
|
featureName="tasks"
|
|
71
71
|
/>,
|
|
72
72
|
);
|
|
73
|
-
expect(screen.getByTestId("cell-r1-color").textContent).toBe("
|
|
73
|
+
expect(screen.getByTestId("cell-r1-color").textContent).toBe("42 €");
|
|
74
74
|
});
|
|
75
75
|
|
|
76
76
|
test("__component-renderer mit Provider → Component wird gemountet, value+row+column kommen an", () => {
|
|
@@ -4,7 +4,12 @@
|
|
|
4
4
|
// money-input seine Pure-Logik exportiert). Kein DOM.
|
|
5
5
|
|
|
6
6
|
import { describe, expect, test } from "bun:test";
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
applyFormatSpec,
|
|
9
|
+
computeVisiblePages,
|
|
10
|
+
defaultCellRender,
|
|
11
|
+
isComponentRendererRef,
|
|
12
|
+
} from "../index";
|
|
8
13
|
|
|
9
14
|
describe("computeVisiblePages", () => {
|
|
10
15
|
test("<= 7 Seiten: alle Seiten, keine Ellipsis", () => {
|
|
@@ -55,6 +60,52 @@ describe("isComponentRendererRef", () => {
|
|
|
55
60
|
});
|
|
56
61
|
});
|
|
57
62
|
|
|
63
|
+
describe("applyFormatSpec", () => {
|
|
64
|
+
test("null/undefined/leer → leerer String (alle Formate)", () => {
|
|
65
|
+
expect(applyFormatSpec({ format: "boolean" }, null)).toBe("");
|
|
66
|
+
expect(applyFormatSpec({ format: "currency", symbol: "€" }, undefined)).toBe("");
|
|
67
|
+
expect(applyFormatSpec({ format: "priority" }, "")).toBe("");
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("boolean: true → ✓, false → leer (defaults)", () => {
|
|
71
|
+
expect(applyFormatSpec({ format: "boolean" }, true)).toBe("✓");
|
|
72
|
+
expect(applyFormatSpec({ format: "boolean" }, false)).toBe("");
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("boolean: benutzerdefinierte Labels", () => {
|
|
76
|
+
expect(
|
|
77
|
+
applyFormatSpec({ format: "boolean", trueLabel: "Public", falseLabel: "Hidden" }, true),
|
|
78
|
+
).toBe("Public");
|
|
79
|
+
expect(
|
|
80
|
+
applyFormatSpec({ format: "boolean", trueLabel: "Public", falseLabel: "Hidden" }, false),
|
|
81
|
+
).toBe("Hidden");
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("currency: value + Leerzeichen + symbol", () => {
|
|
85
|
+
expect(applyFormatSpec({ format: "currency", symbol: "€" }, 42)).toBe("42 €");
|
|
86
|
+
expect(applyFormatSpec({ format: "currency", symbol: "$" }, "99.90")).toBe("99.90 $");
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("currency: kein symbol → reiner Wert", () => {
|
|
90
|
+
expect(applyFormatSpec({ format: "currency" }, 42)).toBe("42");
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test("priority: 0 → Standard-Dash, Zahl → prefix+value", () => {
|
|
94
|
+
expect(applyFormatSpec({ format: "priority", prefix: "P" }, 0)).toBe("—");
|
|
95
|
+
expect(applyFormatSpec({ format: "priority", prefix: "P" }, 3)).toBe("P3");
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("priority: benutzerdefinierter emptyLabel + kein prefix", () => {
|
|
99
|
+
expect(applyFormatSpec({ format: "priority", emptyLabel: "none" }, 0)).toBe("none");
|
|
100
|
+
expect(applyFormatSpec({ format: "priority" }, 5)).toBe("5");
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("unbekanntes format → String(value) fallback", () => {
|
|
104
|
+
expect(applyFormatSpec({ format: "custom-app-format" }, 42)).toBe("42");
|
|
105
|
+
expect(applyFormatSpec({ format: "custom-app-format" }, "hello")).toBe("hello");
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
58
109
|
describe("defaultCellRender", () => {
|
|
59
110
|
test("null/undefined/leerer String → leerer String", () => {
|
|
60
111
|
expect(defaultCellRender(null, "text")).toBe("");
|
package/src/primitives/index.tsx
CHANGED
|
@@ -1056,8 +1056,40 @@ export function isComponentRendererRef(renderer: unknown): { readonly name: stri
|
|
|
1056
1056
|
return { name: component };
|
|
1057
1057
|
}
|
|
1058
1058
|
|
|
1059
|
+
// Applies a FormatSpec ({ format: "timestamp" } etc.) to a value.
|
|
1060
|
+
// Handles all built-in FieldFormatRegistry keys; unknown app-specific keys
|
|
1061
|
+
// fall back to String(value). Exported for unit tests.
|
|
1062
|
+
export function applyFormatSpec(
|
|
1063
|
+
spec: { format: string } & Record<string, unknown>,
|
|
1064
|
+
value: unknown,
|
|
1065
|
+
): string {
|
|
1066
|
+
if (value === null || value === undefined || value === "") return "";
|
|
1067
|
+
switch (spec.format) {
|
|
1068
|
+
case "timestamp":
|
|
1069
|
+
case "date":
|
|
1070
|
+
return formatDateCell(value, spec.format);
|
|
1071
|
+
case "boolean": {
|
|
1072
|
+
if (value === true) return (spec["trueLabel"] as string | undefined) ?? "✓";
|
|
1073
|
+
if (value === false) return (spec["falseLabel"] as string | undefined) ?? "";
|
|
1074
|
+
return "";
|
|
1075
|
+
}
|
|
1076
|
+
case "currency": {
|
|
1077
|
+
const sym = (spec["symbol"] as string | undefined) ?? "";
|
|
1078
|
+
return sym.length > 0 ? `${value} ${sym}` : String(value);
|
|
1079
|
+
}
|
|
1080
|
+
case "priority": {
|
|
1081
|
+
const emptyLabel = (spec["emptyLabel"] as string | undefined) ?? "—";
|
|
1082
|
+
const prefix = (spec["prefix"] as string | undefined) ?? "";
|
|
1083
|
+
if (value === 0 || value === undefined || value === null) return emptyLabel;
|
|
1084
|
+
return `${prefix}${value}`;
|
|
1085
|
+
}
|
|
1086
|
+
default:
|
|
1087
|
+
return typeof value === "string" ? value : String(value);
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1059
1091
|
// Type-spezifische Default-Cell-Renderer. Author kann pro Spalte einen
|
|
1060
|
-
// expliziten renderer setzen (
|
|
1092
|
+
// expliziten renderer setzen (FormatSpec oder PlatformComponent); ohne
|
|
1061
1093
|
// expliziten renderer fällt DataTableCell hier durch.
|
|
1062
1094
|
//
|
|
1063
1095
|
// - boolean → ✓ / leer
|
|
@@ -1135,14 +1167,14 @@ type DataTableCellProps = {
|
|
|
1135
1167
|
};
|
|
1136
1168
|
|
|
1137
1169
|
// Cell-Renderer als Component (statt reiner Funktion) damit der
|
|
1138
|
-
// useColumnRenderer-Hook aus dem Provider lesen kann. Die
|
|
1139
|
-
//
|
|
1140
|
-
//
|
|
1141
|
-
//
|
|
1142
|
-
//
|
|
1170
|
+
// useColumnRenderer-Hook aus dem Provider lesen kann. Die vier Pfade:
|
|
1171
|
+
// 1. FormatSpec (`{ format: "timestamp" }` etc.) → applyFormatSpec.
|
|
1172
|
+
// 2. RuntimeRenderer (Funktion) → direkter Aufruf. Nur für render-list-
|
|
1173
|
+
// interne Reference-Lookup-Closures — niemals aus dem serialisierten Schema.
|
|
1174
|
+
// 3. PlatformComponent (`{ react: { __component: "X" } }`) → schaut
|
|
1143
1175
|
// "X" über useColumnRenderer auf und rendert `<X value row column/>`.
|
|
1144
1176
|
// Nicht registriert → einmalige Warnung + Default-Fallback.
|
|
1145
|
-
//
|
|
1177
|
+
// 4. Sonst → defaultCellRender (Type-basierter String-Renderer).
|
|
1146
1178
|
function DataTableCell({
|
|
1147
1179
|
value,
|
|
1148
1180
|
row,
|
|
@@ -1153,9 +1185,10 @@ function DataTableCell({
|
|
|
1153
1185
|
}: DataTableCellProps): ReactNode {
|
|
1154
1186
|
const componentRef = isComponentRendererRef(renderer);
|
|
1155
1187
|
const ResolvedComponent = useColumnRenderer(componentRef?.name);
|
|
1188
|
+
if (typeof renderer === "object" && renderer !== null && "format" in renderer) {
|
|
1189
|
+
return applyFormatSpec(renderer as { format: string } & Record<string, unknown>, value);
|
|
1190
|
+
}
|
|
1156
1191
|
if (typeof renderer === "function") {
|
|
1157
|
-
// 2. Argument: ganze Row als read-only — function-Renderer können
|
|
1158
|
-
// context-aware sein (Tier 2.7e-Eagerload nutzt das für _refs).
|
|
1159
1192
|
const fn = renderer as (v: unknown, r?: Readonly<Record<string, unknown>>) => string;
|
|
1160
1193
|
return fn(value, row);
|
|
1161
1194
|
}
|
|
@@ -1163,7 +1196,7 @@ function DataTableCell({
|
|
|
1163
1196
|
if (ResolvedComponent !== undefined) {
|
|
1164
1197
|
return <ResolvedComponent value={value} row={row} column={{ field }} />;
|
|
1165
1198
|
}
|
|
1166
|
-
// Renderer im Schema referenziert, aber client-side
|
|
1199
|
+
// Renderer im Schema referenziert, aber client-side kein Map-Eintrag —
|
|
1167
1200
|
// typischer Fall: clientFeatures.columnRenderers vergessen oder
|
|
1168
1201
|
// Tippfehler im __component-Key. Warnen statt crashen, damit ein
|
|
1169
1202
|
// Schema-Boot trotzdem funktioniert (Default-Type-Renderer übernimmt).
|