@cosmicdrift/kumiko-renderer 0.87.3 → 0.89.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",
3
- "version": "0.87.3",
3
+ "version": "0.89.0",
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.87.3",
19
- "@cosmicdrift/kumiko-headless": "0.87.3",
18
+ "@cosmicdrift/kumiko-framework": "0.89.0",
19
+ "@cosmicdrift/kumiko-headless": "0.89.0",
20
20
  "react": "^19.2.6"
21
21
  },
22
22
  "devDependencies": {
@@ -1,6 +1,15 @@
1
1
  import { describe, expect, test } from "bun:test";
2
- import type { DispatcherError, SubmitResult } from "@cosmicdrift/kumiko-headless";
3
- import { resolveExtensionEntityId, shouldNotifyCaller } from "../render-edit-logic";
2
+ import type {
3
+ DispatcherError,
4
+ EditFieldViewModel,
5
+ EditSectionViewModel,
6
+ SubmitResult,
7
+ } from "@cosmicdrift/kumiko-headless";
8
+ import {
9
+ hasEditableSection,
10
+ resolveExtensionEntityId,
11
+ shouldNotifyCaller,
12
+ } from "../render-edit-logic";
4
13
 
5
14
  const error: DispatcherError = {
6
15
  code: "internal_error",
@@ -58,3 +67,48 @@ describe("shouldNotifyCaller", () => {
58
67
  expect(shouldNotifyCaller(validationBlocked, true)).toBe(true);
59
68
  });
60
69
  });
70
+
71
+ const field = (readOnly: boolean): EditFieldViewModel => ({
72
+ field: "f",
73
+ label: "F",
74
+ type: "text",
75
+ value: "",
76
+ visible: true,
77
+ readOnly,
78
+ required: false,
79
+ });
80
+ const fieldsSection = (...readOnly: boolean[]): EditSectionViewModel => ({
81
+ kind: "fields",
82
+ columns: 1,
83
+ fields: readOnly.map(field),
84
+ });
85
+ const extensionSection: EditSectionViewModel = {
86
+ kind: "extension",
87
+ title: "Custom",
88
+ component: {},
89
+ };
90
+
91
+ describe("hasEditableSection", () => {
92
+ // A read-only inspector detail (export-job/download-attempt) marks every field
93
+ // readOnly + has no create/delete — there is nothing to submit, so the Save
94
+ // button must not render at all (a disabled one reads as a broken control).
95
+ test("every field readOnly → false (no Save button)", () => {
96
+ expect(hasEditableSection([fieldsSection(true, true, true)])).toBe(false);
97
+ });
98
+
99
+ test("one editable field → true", () => {
100
+ expect(hasEditableSection([fieldsSection(true, false)])).toBe(true);
101
+ });
102
+
103
+ test("editable field in a later section → true", () => {
104
+ expect(hasEditableSection([fieldsSection(true), fieldsSection(false)])).toBe(true);
105
+ });
106
+
107
+ test("extension section counts as editable (carries its own save)", () => {
108
+ expect(hasEditableSection([extensionSection])).toBe(true);
109
+ });
110
+
111
+ test("no sections → false", () => {
112
+ expect(hasEditableSection([])).toBe(false);
113
+ });
114
+ });
@@ -1,4 +1,12 @@
1
- import type { SubmitResult } from "@cosmicdrift/kumiko-headless";
1
+ import type { EditSectionViewModel, SubmitResult } from "@cosmicdrift/kumiko-headless";
2
+
3
+ // A read-only inspector form — every field readOnly, no editable section — has
4
+ // nothing to submit, so the renderer drops the Save button rather than show a
5
+ // dead disabled one. An extension section carries its own dirty/save, so it
6
+ // counts as editable.
7
+ export function hasEditableSection(sections: readonly EditSectionViewModel[]): boolean {
8
+ return sections.some((s) => s.kind !== "fields" || s.fields.some((f) => !f.readOnly));
9
+ }
2
10
 
3
11
  // Single source of truth for the extension-section entity-id. The section mount
4
12
  // and persistExtensions MUST resolve the same id — otherwise a section mounts
@@ -28,7 +28,11 @@ import { extensionSectionName, useExtensionSectionComponent } from "../app/exten
28
28
  import { useForm } from "../hooks/use-form";
29
29
  import { useTranslation } from "../i18n";
30
30
  import { usePrimitives } from "../primitives";
31
- import { resolveExtensionEntityId, shouldNotifyCaller } from "./render-edit-logic";
31
+ import {
32
+ hasEditableSection,
33
+ resolveExtensionEntityId,
34
+ shouldNotifyCaller,
35
+ } from "./render-edit-logic";
32
36
  import { RenderField } from "./render-field";
33
37
 
34
38
  // End-to-end renderer für einen entityEdit screen. Rendert aus-
@@ -243,6 +247,8 @@ export function RenderEdit<TValues extends FormValues, TCtx = unknown>(
243
247
  [screen, entity, snapshot.values, translate, featureName],
244
248
  );
245
249
 
250
+ const hasEditableField = hasEditableSection(vm.sections);
251
+
246
252
  // Persistiert alle composed Extension-Sections mit der aufgelösten entityId.
247
253
  // false = eine Section schlug fehl (ihr i18n-Key landet im Banner). Ohne
248
254
  // Entity-Kontext (create-mode ohne route-id) gibt es nichts zu schreiben.
@@ -349,15 +355,17 @@ export function RenderEdit<TValues extends FormValues, TCtx = unknown>(
349
355
  {translate("kumiko.actions.cancel")}
350
356
  </Button>
351
357
  )}
352
- <Button
353
- type="submit"
354
- disabled={(snapshot.isUnchanged && !extensionDirty) || isSubmitting}
355
- loading={isSubmitting}
356
- variant="primary"
357
- testId="render-edit-submit"
358
- >
359
- {translate(submitLabel ?? "kumiko.actions.save")}
360
- </Button>
358
+ {hasEditableField && (
359
+ <Button
360
+ type="submit"
361
+ disabled={(snapshot.isUnchanged && !extensionDirty) || isSubmitting}
362
+ loading={isSubmitting}
363
+ variant="primary"
364
+ testId="render-edit-submit"
365
+ >
366
+ {translate(submitLabel ?? "kumiko.actions.save")}
367
+ </Button>
368
+ )}
361
369
  </>
362
370
  );
363
371