@cosmicdrift/kumiko-renderer 0.147.0 → 0.147.2

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.0",
3
+ "version": "0.147.2",
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.0",
19
- "@cosmicdrift/kumiko-headless": "0.147.0",
18
+ "@cosmicdrift/kumiko-framework": "0.147.2",
19
+ "@cosmicdrift/kumiko-headless": "0.147.2",
20
20
  "react": "^19.2.6"
21
21
  },
22
22
  "devDependencies": {
@@ -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 the qualification rule in
212
- // packages/framework/src/engine/qualified-name.ts so the server-side
213
- // handler resolves without extra wiring.
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
  });