@cosmicdrift/kumiko-renderer 0.147.0 → 0.147.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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-renderer",
|
|
3
|
-
"version": "0.147.
|
|
3
|
+
"version": "0.147.1",
|
|
4
4
|
"description": "Platform-agnostic React renderer for Kumiko screens. Contains the shared logic — primitives-contract, hooks, KumikoScreen, navigation & SSE abstractions — that any platform-specific renderer (web, native) composes. No DOM, no EventSource, no react-dom.",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
}
|
|
16
16
|
},
|
|
17
17
|
"dependencies": {
|
|
18
|
-
"@cosmicdrift/kumiko-framework": "0.147.
|
|
19
|
-
"@cosmicdrift/kumiko-headless": "0.147.
|
|
18
|
+
"@cosmicdrift/kumiko-framework": "0.147.1",
|
|
19
|
+
"@cosmicdrift/kumiko-headless": "0.147.1",
|
|
20
20
|
"react": "^19.2.6"
|
|
21
21
|
},
|
|
22
22
|
"devDependencies": {
|
package/src/__tests__/qn.test.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import { lastSegment } from "../app/qn";
|
|
2
|
+
import { lastSegment, toKebab } from "../app/qn";
|
|
3
3
|
|
|
4
4
|
describe("lastSegment", () => {
|
|
5
5
|
test("strips feature-prefix from screen-QN", () => {
|
|
@@ -38,3 +38,18 @@ describe("lastSegment", () => {
|
|
|
38
38
|
expect(lastSegment("publicstatus:screen:")).toBe("");
|
|
39
39
|
});
|
|
40
40
|
});
|
|
41
|
+
|
|
42
|
+
describe("toKebab", () => {
|
|
43
|
+
test("camelCase entity ids match server qualifyEntityName", () => {
|
|
44
|
+
expect(toKebab("driverModel")).toBe("driver-model");
|
|
45
|
+
expect(toKebab("statementUpload")).toBe("statement-upload");
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("already kebab unchanged", () => {
|
|
49
|
+
expect(toKebab("driver-model")).toBe("driver-model");
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("preserves colon segments", () => {
|
|
53
|
+
expect(toKebab("driverModel:list")).toBe("driver-model:list");
|
|
54
|
+
});
|
|
55
|
+
});
|
|
@@ -39,7 +39,7 @@ import { useDashboardBody } from "./dashboard-body";
|
|
|
39
39
|
import type { FeatureSchema } from "./feature-schema";
|
|
40
40
|
import { useNav } from "./nav";
|
|
41
41
|
import { synthesizeProjectionEntity, synthesizeProjectionScreen } from "./projection-list-shim";
|
|
42
|
-
import { lastSegment } from "./qn";
|
|
42
|
+
import { lastSegment, toKebab } from "./qn";
|
|
43
43
|
import { dispatcherErrorText, WriteFailedError } from "./write-failed-error";
|
|
44
44
|
|
|
45
45
|
function evalRowExtractor(
|
|
@@ -208,15 +208,15 @@ function CustomScreenBody({ screenId }: { readonly screenId: string }): ReactNod
|
|
|
208
208
|
// ---- entity-edit ----
|
|
209
209
|
|
|
210
210
|
// Derives `<feature>:write:<entity>:<verb>` from the screen's entity
|
|
211
|
-
// and the schema's feature name. Matches
|
|
212
|
-
// packages/framework/src/engine/qualified-name.ts so
|
|
213
|
-
//
|
|
211
|
+
// and the schema's feature name. Matches qualifyEntityName / toKebab in
|
|
212
|
+
// packages/framework/src/engine/qualified-name.ts so camelCase entity
|
|
213
|
+
// ids (e.g. driverModel) resolve to server QNs (driver-model:create).
|
|
214
214
|
function entityWriteCommand(
|
|
215
215
|
featureName: string,
|
|
216
216
|
entity: string,
|
|
217
217
|
verb: "create" | "update" | "delete",
|
|
218
218
|
): string {
|
|
219
|
-
return `${featureName}:write:${entity}:${verb}`;
|
|
219
|
+
return `${toKebab(featureName)}:write:${toKebab(entity)}:${verb}`;
|
|
220
220
|
}
|
|
221
221
|
|
|
222
222
|
// Default "success → zurück zur Liste"-Navigation. Findet den ersten
|
|
@@ -395,7 +395,7 @@ function EntityEditUpdateBody({
|
|
|
395
395
|
readonly translate?: Translate;
|
|
396
396
|
}): ReactNode {
|
|
397
397
|
const { Banner, Text } = usePrimitives();
|
|
398
|
-
const detailQn = `${schema.featureName}:query:${screen.entity}:detail`;
|
|
398
|
+
const detailQn = `${toKebab(schema.featureName)}:query:${toKebab(screen.entity)}:detail`;
|
|
399
399
|
const detailQuery = useQuery<Readonly<Record<string, unknown>>>(detailQn, { id: entityId });
|
|
400
400
|
|
|
401
401
|
if (detailQuery.loading && detailQuery.data === null) {
|
|
@@ -539,7 +539,7 @@ function EntityEditUpdateForm({
|
|
|
539
539
|
// ---- entity-list ----
|
|
540
540
|
|
|
541
541
|
function entityQueryCommand(featureName: string, entity: string, verb: "list"): string {
|
|
542
|
-
return `${featureName}:query:${entity}:${verb}`;
|
|
542
|
+
return `${toKebab(featureName)}:query:${toKebab(entity)}:${verb}`;
|
|
543
543
|
}
|
|
544
544
|
|
|
545
545
|
// Server-side entity-query-handlers return the paged envelope
|
package/src/app/qn.ts
CHANGED
|
@@ -21,3 +21,16 @@ export function lastSegment(qn: string): string {
|
|
|
21
21
|
const idx = qn.lastIndexOf(":");
|
|
22
22
|
return idx < 0 ? qn : qn.slice(idx + 1);
|
|
23
23
|
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Client-safe copy of framework `toKebab` (engine/qualified-name.ts).
|
|
27
|
+
* Must stay in sync — do NOT import from `@cosmicdrift/kumiko-framework/engine`
|
|
28
|
+
* here: that barrel pulls server deps into the browser bundle.
|
|
29
|
+
*/
|
|
30
|
+
export function toKebab(input: string): string {
|
|
31
|
+
return input
|
|
32
|
+
.replace(/\./g, "-")
|
|
33
|
+
.replace(/([A-Z]+)([A-Z][a-z])/g, "$1-$2")
|
|
34
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1-$2")
|
|
35
|
+
.toLowerCase();
|
|
36
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { EditFieldViewModel, FieldIssue } from "@cosmicdrift/kumiko-headless";
|
|
2
2
|
import { type ReactNode, useCallback, useMemo, useState } from "react";
|
|
3
|
+
import { toKebab } from "../app/qn";
|
|
3
4
|
import { REFERENCE_COMBOBOX_LIMIT } from "../hooks/reference-limits";
|
|
4
5
|
import { useQuery } from "../hooks/use-query";
|
|
5
6
|
import { useLocale } from "../i18n";
|
|
@@ -118,7 +119,7 @@ function ReferenceInput({
|
|
|
118
119
|
// Tier 2.7e Cross-Feature: refFeature kann ≠ featureName sein
|
|
119
120
|
// (z.B. items.assignee → users:query:user:list). Default ist
|
|
120
121
|
// same-feature, kommt aus dem ViewModel (parseRefTarget).
|
|
121
|
-
const queryQn = `${refFeature}:query:${refEntity}:list`;
|
|
122
|
+
const queryQn = `${toKebab(refFeature)}:query:${toKebab(refEntity)}:list`;
|
|
122
123
|
// Tier 2.7e Remote-Search: User tippt im Combobox → Server filtert
|
|
123
124
|
// via existing list-payload `search`-Param (Tier 2.6c). Combobox
|
|
124
125
|
// debounced den keystroke selbst (300ms) und ruft onSearchChange.
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
// Entity, Live-Updates kommen via SSE (use-query-live).
|
|
19
19
|
|
|
20
20
|
import { useMemo } from "react";
|
|
21
|
+
import { toKebab } from "../app/qn";
|
|
21
22
|
import { REFERENCE_LIST_LOOKUP_LIMIT } from "./reference-limits";
|
|
22
23
|
import { useQuery } from "./use-query";
|
|
23
24
|
|
|
@@ -35,7 +36,7 @@ export function useReferenceLookup(
|
|
|
35
36
|
refEntity: string,
|
|
36
37
|
labelField: string,
|
|
37
38
|
): { readonly map: ReferenceLookupMap; readonly loading: boolean } {
|
|
38
|
-
const queryQn = `${featureName}:query:${refEntity}:list`;
|
|
39
|
+
const queryQn = `${toKebab(featureName)}:query:${toKebab(refEntity)}:list`;
|
|
39
40
|
const result = useQuery<{ rows: ReadonlyArray<Record<string, unknown>> }>(queryQn, {
|
|
40
41
|
limit: REFERENCE_LIST_LOOKUP_LIMIT,
|
|
41
42
|
});
|