@cosmicdrift/kumiko-renderer 0.379.0 → 0.381.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.
@@ -44,6 +44,10 @@ export type ExtensionSectionProps = {
44
44
  * `snapshot.errors` on the field instead of a collective message.
45
45
  * Undefined outside entityEdit sections. */
46
46
  readonly validate?: () => boolean;
47
+ /** Marks the current host form values as saved after the section wrote
48
+ * them itself, so a following navigation does not ask to discard.
49
+ * Undefined outside entityEdit sections and the entityEdit footer slot. */
50
+ readonly markSaved?: () => void;
47
51
  /** Whether the host form has unsaved changes. Only set in the entityEdit
48
52
  * footer slot (`screen.slots.footer`); undefined in all other mounts. */
49
53
  readonly hasUnsavedChanges?: boolean;
@@ -165,7 +165,7 @@ function isFieldsStepFilled(section, parse) {
165
165
  // mounts it with the host entity name + id. Hook lives in its own
166
166
  // component so we don't call `use*` inside vm.sections.map (rules-of-
167
167
  // hooks would punish reordering sections between renders).
168
- function ExtensionSectionMount({ section, entityName: hostEntityName, entityId, initialValues, values, patch, validate, hideTitle, actions, stepKey, onReportStepComplete, }) {
168
+ function ExtensionSectionMount({ section, entityName: hostEntityName, entityId, initialValues, values, patch, validate, markSaved, hideTitle, actions, stepKey, onReportStepComplete, }) {
169
169
  const { Banner, FillContainer, Section, Text } = usePrimitives();
170
170
  const name = extensionSectionName(section.component);
171
171
  const Component = useExtensionSectionComponent(name);
@@ -184,7 +184,7 @@ function ExtensionSectionMount({ section, entityName: hostEntityName, entityId,
184
184
  // uses for its own hideTitle branch).
185
185
  return hideTitle === true ? (FillContainer !== undefined ? (_jsx(FillContainer, { testId: testId, children: placeholder })) : (placeholder)) : (_jsx(Section, { title: section.title, testId: testId, children: placeholder }, section.title));
186
186
  }
187
- const mounted = (_jsx(Component, { entityName: section.entityName ?? hostEntityName, entityId: entityId, initialValues: initialValues, values: values, patch: patch, validate: validate, ...(onReportStepComplete !== undefined && { reportStepComplete }) }));
187
+ const mounted = (_jsx(Component, { entityName: section.entityName ?? hostEntityName, entityId: entityId, initialValues: initialValues, values: values, patch: patch, validate: validate, markSaved: markSaved, ...(onReportStepComplete !== undefined && { reportStepComplete }) }));
188
188
  // Same reasoning as the placeholder branch above: tabs mode's outer Card
189
189
  // is the only frame this content gets, no nested Section divider.
190
190
  return hideTitle === true ? (FillContainer !== undefined ? (_jsx(FillContainer, { testId: testId, children: mounted })) : (mounted)) : (_jsx(Section, { ...(actions !== undefined && { actions }), title: section.title, testId: testId, children: mounted }));
@@ -195,7 +195,7 @@ function ExtensionSectionMount({ section, entityName: hostEntityName, entityId,
195
195
  // component for both slots — header passes no hasUnsavedChanges/wizardStep,
196
196
  // footer does. Own component for rules-of-hooks (useExtensionSectionComponent
197
197
  // must not run conditionally inside RenderEdit's render body).
198
- function EditSlotMount({ slot, slotName, screenId, entityName, entityId, values, hasUnsavedChanges, wizardStep, }) {
198
+ function EditSlotMount({ slot, slotName, screenId, entityName, entityId, values, markSaved, hasUnsavedChanges, wizardStep, }) {
199
199
  const name = extensionSectionName(slot);
200
200
  const Component = useExtensionSectionComponent(name);
201
201
  useEffect(() => {
@@ -206,7 +206,7 @@ function EditSlotMount({ slot, slotName, screenId, entityName, entityId, values,
206
206
  }, [name, Component, screenId, slotName]);
207
207
  if (Component === undefined)
208
208
  return null;
209
- return (_jsx(Component, { entityName: entityName, entityId: entityId, values: values, ...(hasUnsavedChanges !== undefined && { hasUnsavedChanges }), ...(wizardStep !== undefined && { wizardStep }) }));
209
+ return (_jsx(Component, { entityName: entityName, entityId: entityId, values: values, ...(markSaved !== undefined && { markSaved }), ...(hasUnsavedChanges !== undefined && { hasUnsavedChanges }), ...(wizardStep !== undefined && { wizardStep }) }));
210
210
  }
211
211
  function formSectionDomId(sectionIndex) {
212
212
  return `form-section-${sectionIndex}`;
@@ -442,6 +442,11 @@ export function RenderEdit(props) {
442
442
  const scopeFieldNamesRef = useRef(scopeFieldNames);
443
443
  scopeFieldNamesRef.current = scopeFieldNames;
444
444
  const scopedValidate = useCallback(() => controller.validate(scopeFieldNamesRef.current), [controller]);
445
+ const markSaved = useCallback(() => {
446
+ controller.rebase();
447
+ extensionDirtyRef.current = false;
448
+ setExtensionDirty(false);
449
+ }, [controller]);
445
450
  // `saveDraft` (defined below) is a fresh closure every render, over that
446
451
  // render's `draftKey`/`draftId` — the ref lets the debounce timer below
447
452
  // always call the CURRENT one even though the timer was scheduled by an
@@ -1226,7 +1231,7 @@ export function RenderEdit(props) {
1226
1231
  ? 1
1227
1232
  : 0;
1228
1233
  const isSettingsList = screen.layout.variant === "settings-list" && hideSectionTitles !== true;
1229
- const formActions = (_jsxs(_Fragment, { children: [showWizardBack && (_jsx(Button, { type: "button", variant: "secondary", icon: "chevron-left", onClick: handleWizardBack, testId: "render-edit-wizard-back", [FOOTER_ACTION_ROLE_PROP]: "back", children: translate("kumiko.actions.back") })), footerSlot !== undefined && (_jsx(EditSlotMount, { slot: footerSlot, slotName: "footer", screenId: screen.id, entityName: vm.entityName, entityId: resolveExtensionEntityId(entityIdProp, vm.id), values: snapshot.values, hasUnsavedChanges: hasUnsavedInput, wizardStep: isWizard ? { index: currentStep, isLast: isLastWizardStep } : undefined, ...(screen.slots?.footerPrimary === true && { [STICKY_PRIMARY_ACTION_PROP]: true }) })), isWizard && !isLastWizardStep && showsSubmit && !isCreateMode && (_jsx(Button, { type: "button", variant: "ghost", disabled: isSubmitting || disabled, onClick: () => void handleSubmit({ saveNow: true }), testId: "render-edit-wizard-save-close", [STICKY_PRIMARY_ACTION_PROP]: true, children: translate("kumiko.wizard.save-and-close") })), showWizardNext && (_jsx(Button, { type: "submit", variant: "primary", iconEnd: "chevron-right", testId: "render-edit-wizard-next", [FOOTER_ACTION_ROLE_PROP]: "primary",
1234
+ const formActions = (_jsxs(_Fragment, { children: [showWizardBack && (_jsx(Button, { type: "button", variant: "secondary", icon: "chevron-left", onClick: handleWizardBack, testId: "render-edit-wizard-back", [FOOTER_ACTION_ROLE_PROP]: "back", children: translate("kumiko.actions.back") })), footerSlot !== undefined && (_jsx(EditSlotMount, { slot: footerSlot, slotName: "footer", screenId: screen.id, entityName: vm.entityName, entityId: resolveExtensionEntityId(entityIdProp, vm.id), values: snapshot.values, markSaved: markSaved, hasUnsavedChanges: hasUnsavedInput, wizardStep: isWizard ? { index: currentStep, isLast: isLastWizardStep } : undefined, ...(screen.slots?.footerPrimary === true && { [STICKY_PRIMARY_ACTION_PROP]: true }) })), isWizard && !isLastWizardStep && showsSubmit && !isCreateMode && (_jsx(Button, { type: "button", variant: "ghost", disabled: isSubmitting || disabled, onClick: () => void handleSubmit({ saveNow: true }), testId: "render-edit-wizard-save-close", [STICKY_PRIMARY_ACTION_PROP]: true, children: translate("kumiko.wizard.save-and-close") })), showWizardNext && (_jsx(Button, { type: "submit", variant: "primary", iconEnd: "chevron-right", testId: "render-edit-wizard-next", [FOOTER_ACTION_ROLE_PROP]: "primary",
1230
1235
  [NARROW_LABEL_PROP]: translate("kumiko.actions.next"), children: nextStepTitle !== undefined
1231
1236
  ? translate("kumiko.wizard.next-with-title", { title: nextStepTitle })
1232
1237
  : translate("kumiko.actions.next") })), showSubmit &&
@@ -1403,7 +1408,7 @@ export function RenderEdit(props) {
1403
1408
  const mount = (_jsx(ExtensionSectionMount, { section: section, entityName: vm.entityName, entityId: resolveExtensionEntityId(entityIdProp, vm.id), initialValues: extensionInitialValues, values: snapshot.values,
1404
1409
  // @cast-boundary form-values: ExtensionSectionProps is not generic
1405
1410
  // over TValues; controller is mount-lifetime-stable, see onControlsReady above.
1406
- patch: patchAndScheduleDraftSave, validate: scopedValidate, hideTitle: hideSectionTitles, ...(isWizard &&
1411
+ patch: patchAndScheduleDraftSave, validate: scopedValidate, markSaved: markSaved, hideTitle: hideSectionTitles, ...(isWizard &&
1407
1412
  !isCreateMode && {
1408
1413
  stepKey: wizardStepKey(section, sectionIndex),
1409
1414
  onReportStepComplete: reportExtensionStepComplete,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-renderer",
3
- "version": "0.379.0",
3
+ "version": "0.381.0",
4
4
  "description": "React renderer for Kumiko screens. The root export holds the platform-agnostic logic (primitives contract, hooks, KumikoScreen, navigation and SSE abstractions); ./headless the DOM-free UI logic, ./live the HTTP dispatcher, ./web the browser bindings (HTML primitives, history navigation, EventSource live events, createKumikoApp).",
5
5
  "license": "BUSL-1.1",
6
6
  "author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
@@ -48,8 +48,8 @@
48
48
  }
49
49
  },
50
50
  "dependencies": {
51
- "@cosmicdrift/kumiko-framework": "0.379.0",
52
- "@cosmicdrift/kumiko-types": "0.379.0",
51
+ "@cosmicdrift/kumiko-framework": "0.381.0",
52
+ "@cosmicdrift/kumiko-types": "0.381.0",
53
53
  "@radix-ui/react-dialog": "^1.1.15",
54
54
  "@radix-ui/react-dropdown-menu": "^2.1.16",
55
55
  "@radix-ui/react-label": "^2.1.8",
@@ -92,8 +92,8 @@
92
92
  }
93
93
  },
94
94
  "devDependencies": {
95
- "@cosmicdrift/kumiko-locale-de": "0.379.0",
96
- "@cosmicdrift/kumiko-testing": "0.379.0",
95
+ "@cosmicdrift/kumiko-locale-de": "0.381.0",
96
+ "@cosmicdrift/kumiko-testing": "0.381.0",
97
97
  "@tailwindcss/cli": "^4.3.0",
98
98
  "@testing-library/react": "^16.3.2",
99
99
  "@testing-library/user-event": "^14.6.1",
package/src/changes.json CHANGED
@@ -1,4 +1,11 @@
1
1
  [
2
+ {
3
+ "version": "0.381.0",
4
+ "type": "improvement",
5
+ "title": "ExtensionSectionProps.markSaved marks host form values as saved",
6
+ "detail": "ExtensionSectionProps gains an optional markSaved callback, set in entityEdit sections and the entityEdit footer slot. An extension section that writes the host entity's values through its own command calls it after the write succeeds; the host form then counts as clean and a following navigation passes the leave guard without the discard dialog. Additive API.",
7
+ "migration": "None. Additive API."
8
+ },
2
9
  {
3
10
  "version": "0.379.0",
4
11
  "type": "fix",