@cosmicdrift/kumiko-renderer 0.249.0 → 0.250.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.249.0",
3
+ "version": "0.250.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.249.0",
19
- "@cosmicdrift/kumiko-headless": "0.249.0",
18
+ "@cosmicdrift/kumiko-framework": "0.250.0",
19
+ "@cosmicdrift/kumiko-headless": "0.250.0",
20
20
  "react": "^19.2.6",
21
21
  "temporal-polyfill": "^0.3.2",
22
22
  "zod": "^4.4.3"
@@ -27,7 +27,7 @@
27
27
  "@types/react-dom": "^19.2.3",
28
28
  "jsdom": "^29.1.1",
29
29
  "react-dom": "^19.2.6",
30
- "@cosmicdrift/kumiko-locale-de": "0.249.0"
30
+ "@cosmicdrift/kumiko-locale-de": "0.250.0"
31
31
  },
32
32
  "repository": {
33
33
  "type": "git",
@@ -763,6 +763,7 @@ function EntityEditUpdateForm({
763
763
  id: action.id,
764
764
  label: effectiveTranslate(action.label),
765
765
  ...(action.style !== undefined && { style: action.style }),
766
+ confirmRequired: false,
766
767
  ...(actionIcon !== undefined && { icon: actionIcon }),
767
768
  onPress: () => {
768
769
  if (id === "") return;
@@ -783,6 +784,7 @@ function EntityEditUpdateForm({
783
784
  id: action.id,
784
785
  label: effectiveTranslate(action.label),
785
786
  ...(action.style !== undefined && { style: action.style }),
787
+ confirmRequired: false,
786
788
  ...(actionIcon !== undefined && { icon: actionIcon }),
787
789
  onPress: () => {
788
790
  nav.navigate({
@@ -802,6 +804,7 @@ function EntityEditUpdateForm({
802
804
  id: action.id,
803
805
  label: effectiveTranslate(action.label),
804
806
  ...(action.style !== undefined && { style: action.style }),
807
+ confirmRequired: false,
805
808
  ...(actionIcon !== undefined && { icon: actionIcon }),
806
809
  onPress: () => {
807
810
  const initialValues =
@@ -1612,6 +1615,7 @@ function EntityListBody({
1612
1615
  id: action.id,
1613
1616
  label: effectiveTranslate(action.label),
1614
1617
  ...(action.style !== undefined && { style: action.style }),
1618
+ confirmRequired: false,
1615
1619
  ...(actionIcon !== undefined && { icon: actionIcon }),
1616
1620
  onTrigger: (row: ListRowViewModel) => runNavigate(navigateAction, row),
1617
1621
  ...(actionVisible !== undefined && {
@@ -1627,6 +1631,7 @@ function EntityListBody({
1627
1631
  id: action.id,
1628
1632
  label: effectiveTranslate(action.label),
1629
1633
  ...(action.style !== undefined && { style: action.style }),
1634
+ confirmRequired: false,
1630
1635
  ...(actionIcon !== undefined && { icon: actionIcon }),
1631
1636
  onTrigger: (row: ListRowViewModel) => {
1632
1637
  const initialValues =
@@ -1709,6 +1714,7 @@ function EntityListBody({
1709
1714
  id: action.id,
1710
1715
  label: effectiveTranslate(action.label),
1711
1716
  ...(action.style !== undefined && { style: action.style }),
1717
+ confirmRequired: false,
1712
1718
  ...(actionIcon !== undefined && { icon: actionIcon }),
1713
1719
  onTrigger: () => nav.navigate({ screenId: action.screen }),
1714
1720
  };
@@ -1718,6 +1724,7 @@ function EntityListBody({
1718
1724
  id: action.id,
1719
1725
  label: effectiveTranslate(action.label),
1720
1726
  ...(action.style !== undefined && { style: action.style }),
1727
+ confirmRequired: false,
1721
1728
  ...(actionIcon !== undefined && { icon: actionIcon }),
1722
1729
  onTrigger: () => openDrawer(action),
1723
1730
  };
@@ -2017,6 +2024,7 @@ function ProjectionListBody({
2017
2024
  id: action.id,
2018
2025
  label: effectiveTranslate(action.label),
2019
2026
  ...(action.style !== undefined && { style: action.style }),
2027
+ confirmRequired: false,
2020
2028
  ...(actionIcon !== undefined && { icon: actionIcon }),
2021
2029
  onTrigger: () => nav.navigate({ screenId: target }),
2022
2030
  });
@@ -2027,6 +2035,7 @@ function ProjectionListBody({
2027
2035
  id: action.id,
2028
2036
  label: effectiveTranslate(action.label),
2029
2037
  ...(action.style !== undefined && { style: action.style }),
2038
+ confirmRequired: false,
2030
2039
  ...(actionIcon !== undefined && { icon: actionIcon }),
2031
2040
  onTrigger: () => openDrawer(action),
2032
2041
  });
@@ -2315,6 +2324,7 @@ function ProjectionDetailBody({
2315
2324
  id: action.id,
2316
2325
  label: effectiveTranslate(action.label),
2317
2326
  ...(action.style !== undefined && { style: action.style }),
2327
+ confirmRequired: false,
2318
2328
  ...(actionIcon !== undefined && { icon: actionIcon }),
2319
2329
  onPress: () => {
2320
2330
  if (id === "") return;
@@ -2348,6 +2358,7 @@ function ProjectionDetailBody({
2348
2358
  id: action.id,
2349
2359
  label: effectiveTranslate(action.label),
2350
2360
  ...(action.style !== undefined && { style: action.style }),
2361
+ confirmRequired: false,
2351
2362
  ...(actionIcon !== undefined && { icon: actionIcon }),
2352
2363
  onPress: () => {
2353
2364
  nav.navigate({
@@ -2367,6 +2378,7 @@ function ProjectionDetailBody({
2367
2378
  id: action.id,
2368
2379
  label: effectiveTranslate(action.label),
2369
2380
  ...(action.style !== undefined && { style: action.style }),
2381
+ confirmRequired: false,
2370
2382
  ...(actionIcon !== undefined && { icon: actionIcon }),
2371
2383
  onPress: () => {
2372
2384
  const initialValues =
@@ -2728,6 +2740,7 @@ function ActionFormBody({
2728
2740
  onSubmit={handleSubmitted}
2729
2741
  {...(handleCancel !== undefined && { onCancel: handleCancel })}
2730
2742
  {...(screen.submitLabel !== undefined && { submitLabel: screen.submitLabel })}
2743
+ {...(screen.submitStyle !== undefined && { submitVariant: screen.submitStyle })}
2731
2744
  {...(translate !== undefined && { translate })}
2732
2745
  />
2733
2746
  );
@@ -156,6 +156,7 @@ function buildNavigateRowAction(
156
156
  id: action.id,
157
157
  label: translate(action.label),
158
158
  ...(action.style !== undefined && { style: action.style }),
159
+ confirmRequired: false,
159
160
  ...(actionIcon !== undefined && { icon: actionIcon }),
160
161
  onTrigger: (row: ListRowViewModel) => runProjectionRowNavigate(nav, action, row),
161
162
  ...(visible !== undefined && {
@@ -195,6 +196,7 @@ function buildDrawerRowAction(
195
196
  id: action.id,
196
197
  label: translate(action.label),
197
198
  ...(action.style !== undefined && { style: action.style }),
199
+ confirmRequired: false,
198
200
  ...(actionIcon !== undefined && { icon: actionIcon }),
199
201
  onTrigger: (row: ListRowViewModel) => {
200
202
  openDrawer(action, params !== undefined ? evalRowExtractor(params, row.values) : undefined);
@@ -137,6 +137,26 @@ describe("RenderEditActionButton", () => {
137
137
  await waitFor(() => expect(pressed).toBe(1));
138
138
  });
139
139
 
140
+ // fw#2752: schema-driven navigate/drawer actions set confirmRequired: false
141
+ // to opt a danger-styled action out of the forced dialog — the colour still
142
+ // marks it destructive, but the target form is itself the confirmation.
143
+ test("danger style with confirmRequired=false fires onPress directly, no dialog", async () => {
144
+ let pressed = 0;
145
+ renderAction({
146
+ id: "open-terminate-form",
147
+ label: "Terminate",
148
+ style: "danger",
149
+ confirmRequired: false,
150
+ onPress: async () => {
151
+ pressed += 1;
152
+ },
153
+ });
154
+
155
+ fireEvent.click(rtlScreen.getByTestId("render-edit-action-open-terminate-form"));
156
+ expect(rtlScreen.queryByTestId("render-edit-action-open-terminate-form-dialog")).toBeNull();
157
+ await waitFor(() => expect(pressed).toBe(1));
158
+ });
159
+
140
160
  test("onPress failure reports via onError", async () => {
141
161
  const errors: Array<string | null> = [];
142
162
  renderAction(
@@ -39,8 +39,11 @@ export function RenderEditActionButton({
39
39
 
40
40
  const variant = action.style ?? "secondary";
41
41
  // Same rule as RowActionWriteHandler: "danger" forces a confirm even
42
- // without an explicit confirm key.
43
- const needsConfirm = action.confirm !== undefined || action.style === "danger";
42
+ // without an explicit confirm key — unless `confirmRequired` overrides it
43
+ // (schema-driven navigate/drawer actions, where the target form is itself
44
+ // the confirmation).
45
+ const needsConfirm =
46
+ action.confirm !== undefined || (action.confirmRequired ?? action.style === "danger");
44
47
  const showIconOnly = iconOnly && action.icon !== undefined;
45
48
 
46
49
  return (
@@ -76,6 +76,8 @@ export type RenderEditProps<TValues extends FormValues, TCtx = unknown> = {
76
76
  * "Save" can be replaced by domain-specific strings ("Approve" /
77
77
  * "Dispatch" / etc.). */
78
78
  readonly submitLabel?: string;
79
+ /** Visual style of the submit button (actionForm `submitStyle`). Default "primary". */
80
+ readonly submitVariant?: "primary" | "danger";
79
81
  /** Per-field extra content inline after the label (e.g.
80
82
  * ConfigSourceBadge). Called with the field name, returns a ReactNode or
81
83
  * undefined. */
@@ -168,6 +170,10 @@ export type RenderEditAction = {
168
170
  readonly style?: "primary" | "secondary" | "danger";
169
171
  readonly confirm?: string;
170
172
  readonly confirmLabel?: string;
173
+ /** Overrides the default "danger implies a confirm dialog" rule. Schema-driven
174
+ * navigate/drawer actions set it to false: the colour marks the action as
175
+ * destructive, but the target form is the confirmation. */
176
+ readonly confirmRequired?: boolean;
171
177
  /** Resolved icon (author `RowAction.icon` or the id-derived default) —
172
178
  * drives both the icon-left-of-text render and the icon-only collapse
173
179
  * rule (see `shouldRenderActionsIconOnly`). */
@@ -212,6 +212,7 @@ export function RenderEdit<TValues extends FormValues, TCtx = unknown>(
212
212
  actions,
213
213
  onRelatedListDrawerAction,
214
214
  submitLabel,
215
+ submitVariant,
215
216
  labelAppendix,
216
217
  fieldAppendix,
217
218
  entityId: entityIdProp,
@@ -1032,7 +1033,7 @@ export function RenderEdit<TValues extends FormValues, TCtx = unknown>(
1032
1033
  type="submit"
1033
1034
  disabled={(snapshot.isUnchanged && !extensionDirty) || isSubmitting || disabled}
1034
1035
  loading={isSubmitting}
1035
- variant="primary"
1036
+ variant={submitVariant ?? "primary"}
1036
1037
  icon="check"
1037
1038
  testId="render-edit-submit"
1038
1039
  >
@@ -130,6 +130,10 @@ export type ToolbarActionButton = {
130
130
  readonly style?: "primary" | "secondary" | "danger";
131
131
  readonly confirm?: string;
132
132
  readonly confirmLabel?: string;
133
+ /** Overrides the default "danger implies a confirm dialog" rule. Schema-driven
134
+ * navigate/drawer actions set it to false: the colour marks the action as
135
+ * destructive, but the target form is the confirmation. */
136
+ readonly confirmRequired?: boolean;
133
137
  readonly onTrigger: () => Promise<void> | void;
134
138
  /** Id-derived default icon (ACTION_ICON_BY_ID in kumiko-screen.tsx) —
135
139
  * ToolbarAction has no author-declared icon field, unlike RowAction. */
@@ -492,7 +496,8 @@ function ToolbarActionView({
492
496
  };
493
497
 
494
498
  const variant: "primary" | "secondary" | "danger" = action.style ?? "secondary";
495
- const needsConfirm = action.confirm !== undefined || action.style === "danger";
499
+ const needsConfirm =
500
+ action.confirm !== undefined || (action.confirmRequired ?? action.style === "danger");
496
501
  const showIconOnly = iconOnly && action.icon !== undefined;
497
502
 
498
503
  return (
@@ -488,8 +488,9 @@ export type DataTableRowAction = {
488
488
  readonly id: string;
489
489
  /** Translated Label. */
490
490
  readonly label: string;
491
- /** Visual-Style — danger triggert in der Default-Primitive eine rote
492
- * Variante UND erzwingt einen Confirm-Dialog wenn keiner gesetzt ist. */
491
+ /** Visual style "danger" renders the red variant in the default
492
+ * primitive. Whether it also forces a confirm dialog is governed by
493
+ * `confirmRequired` (falls back to `style === "danger"` when unset). */
493
494
  readonly style?: "primary" | "secondary" | "danger";
494
495
  /** Translated Confirm-Prompt (Description im Dialog) — wenn gesetzt,
495
496
  * öffnet ein Modal vor der Ausführung. Bei style=danger ohne expliziten
@@ -498,6 +499,10 @@ export type DataTableRowAction = {
498
499
  /** Translated Confirm-Button-Label im Dialog. Default = `label`
499
500
  * (Action-Label wird wiederverwendet). */
500
501
  readonly confirmLabel?: string;
502
+ /** Overrides the default "danger implies a confirm dialog" rule. Schema-driven
503
+ * navigate/drawer actions set it to false: the colour marks the action as
504
+ * destructive, but the target form is the confirmation. */
505
+ readonly confirmRequired?: boolean;
501
506
  /** Wird mit der ListRowViewModel der geklickten Row aufgerufen. Async
502
507
  * erlaubt — der Renderer kann während der Promise-Resolution einen
503
508
  * Loading-State auf dem Button zeigen. */