@cosmicdrift/kumiko-framework 0.31.1 → 0.32.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-framework",
3
- "version": "0.31.1",
3
+ "version": "0.32.1",
4
4
  "description": "Framework core — engine, pipeline, API, DB, and every other bit that makes Kumiko go.",
5
5
  "license": "BUSL-1.1",
6
6
  "author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
@@ -1287,6 +1287,7 @@ describe("boot-validator", () => {
1287
1287
  readonly fields: readonly string[];
1288
1288
  }>;
1289
1289
  readonly redirect?: string;
1290
+ readonly cancelTarget?: string | false;
1290
1291
  readonly extraScreens?: readonly string[];
1291
1292
  };
1292
1293
 
@@ -1318,6 +1319,7 @@ describe("boot-validator", () => {
1318
1319
  fields: fields as never,
1319
1320
  layout: { sections: sections as never },
1320
1321
  ...(override.redirect !== undefined && { redirect: override.redirect }),
1322
+ ...(override.cancelTarget !== undefined && { cancelTarget: override.cancelTarget }),
1321
1323
  });
1322
1324
  for (const extra of override.extraScreens ?? []) {
1323
1325
  r.screen({
@@ -1387,6 +1389,22 @@ describe("boot-validator", () => {
1387
1389
  );
1388
1390
  });
1389
1391
 
1392
+ test("cancelTarget → existing screen-id → kein Throw", () => {
1393
+ expect(() =>
1394
+ validateBoot([makeFeature({ cancelTarget: "after-form", extraScreens: ["after-form"] })]),
1395
+ ).not.toThrow();
1396
+ });
1397
+
1398
+ test("cancelTarget → unknown screen-id → Throw", () => {
1399
+ expect(() => validateBoot([makeFeature({ cancelTarget: "ghost-screen" })])).toThrow(
1400
+ /cancelTarget "ghost-screen" does not resolve to a registered screen/,
1401
+ );
1402
+ });
1403
+
1404
+ test("cancelTarget=false (Button abgeschaltet) → kein Throw", () => {
1405
+ expect(() => validateBoot([makeFeature({ cancelTarget: false })])).not.toThrow();
1406
+ });
1407
+
1390
1408
  test("extension section ohne component → Throw (Parität zu entityEdit)", () => {
1391
1409
  // synthesizeActionFormScreen reicht die layout 1:1 an RenderEdit weiter —
1392
1410
  // eine Extension-Section ohne react/native-Marker rendert sonst stumm leer.
@@ -210,6 +210,19 @@ export function validateScreens(
210
210
  );
211
211
  }
212
212
  }
213
+ if (typeof screen.cancelTarget === "string") {
214
+ // Gleiche Regel wie redirect — `false` (kein Cancel-Button)
215
+ // braucht keine Validierung.
216
+ const candidateQn = qualifyEntityName(feature.name, "screen", screen.cancelTarget);
217
+ if (!allScreenQns.has(candidateQn)) {
218
+ throw new Error(
219
+ `[Feature ${feature.name}] Screen "${screenId}" (actionForm) cancelTarget "${screen.cancelTarget}" ` +
220
+ `does not resolve to a registered screen in this feature. Known screens: ${
221
+ [...Object.keys(feature.screens)].sort().join(", ") || "(none)"
222
+ }.`,
223
+ );
224
+ }
225
+ }
213
226
  continue;
214
227
  }
215
228
 
@@ -177,6 +177,11 @@ export type RowActionNavigate = {
177
177
  /** Screen-id (kurz, unqualified) zu dem navigiert wird. Boot-
178
178
  * Validator prüft Existenz im selben Feature. */
179
179
  readonly screen: string;
180
+ /** Optional: Entity-Id für entityEdit-Targets — landet als Pfad-
181
+ * Segment (`/<workspace>/<screen>/<entityId>`). entityEdit liest die
182
+ * Id AUSSCHLIESSLICH aus dem Pfad; ein `?id=`-Search-Param öffnet
183
+ * den Create-Mode. ⚠️ Function-Form nur im Monolith-Bundle-Pattern. */
184
+ readonly entityId?: (row: Readonly<Record<string, unknown>>) => string;
180
185
  /** Optional: URL-Search-Params aus row-Context. Wird in actionForm-
181
186
  * Targets als initial values gelesen ("Edit Customer X" → URL hat
182
187
  * `?customerId=row-uuid`, actionForm initial values pre-fillen).
@@ -359,6 +364,13 @@ export type ActionFormScreenDefinition = {
359
364
  * Wenn nicht gesetzt, bleibt der User auf dem Form-Screen. Boot-
360
365
  * Validator prüft dass die ID einen registrierten Screen meint. */
361
366
  readonly redirect?: string;
367
+ /** Ziel des Abbrechen-Buttons. Default: `redirect` (historisches
368
+ * Verhalten — Cancel und Submit-Redirect landen dann am selben Ort).
369
+ * `false` = kein Abbrechen-Button; richtig für Single-Action-Screens
370
+ * ohne verwerfbaren Zustand (z.B. "Test-Mail senden"), wo Abbrechen
371
+ * nur ein zweiter Weg zum selben Ziel wäre. Boot-Validator prüft
372
+ * String-Targets wie `redirect`. */
373
+ readonly cancelTarget?: string | false;
362
374
  readonly slots?: ScreenSlots;
363
375
  readonly access?: AccessRule;
364
376
  };