@byline/admin 3.0.1 → 3.1.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.
@@ -6,9 +6,15 @@
6
6
  * Copyright (c) Infonomic Company Limited
7
7
  */
8
8
  import type { ArrayField as ArrayFieldType } from '@byline/core';
9
- export declare const ArrayField: ({ field, defaultValue, path, disableSorting, }: {
9
+ export declare const ArrayField: ({ field, defaultValue, path, disableSorting, contentLocale, }: {
10
10
  field: ArrayFieldType;
11
11
  defaultValue: any;
12
12
  path: string;
13
13
  disableSorting?: boolean;
14
+ /**
15
+ * Active content locale, forwarded to each array item's fields so
16
+ * localized widgets nested inside an array (e.g. a `localized` richText)
17
+ * can render their locale badge.
18
+ */
19
+ contentLocale?: string;
14
20
  }) => import("react").JSX.Element;
@@ -8,7 +8,7 @@ import { FieldRenderer } from "../field-renderer.js";
8
8
  import { SortableItem } from "../sortable-item.js";
9
9
  import { useFormContext } from "../../forms/form-context.js";
10
10
  import array_field_module from "./array-field.module.js";
11
- const ArrayField = ({ field, defaultValue, path, disableSorting = false })=>{
11
+ const ArrayField = ({ field, defaultValue, path, disableSorting = false, contentLocale })=>{
12
12
  const { appendPatch, getFieldValue, getFieldValues, setFieldStore } = useFormContext();
13
13
  const { t } = useTranslation('byline-admin');
14
14
  const [items, setItems] = useState([]);
@@ -113,7 +113,8 @@ const ArrayField = ({ field, defaultValue, path, disableSorting = false })=>{
113
113
  field: innerField,
114
114
  defaultValue: groupData[innerField.name],
115
115
  basePath: `${arrayElementPath}.${childField.name}`,
116
- disableSorting: true
116
+ disableSorting: true,
117
+ contentLocale: contentLocale
117
118
  }, innerField.name))
118
119
  ]
119
120
  }, childField.name);
@@ -122,7 +123,8 @@ const ArrayField = ({ field, defaultValue, path, disableSorting = false })=>{
122
123
  field: childField,
123
124
  defaultValue: initial,
124
125
  basePath: arrayElementPath,
125
- disableSorting: true
126
+ disableSorting: true,
127
+ contentLocale: contentLocale
126
128
  }, childField.name);
127
129
  });
128
130
  const label = field.label ?? field.name;
@@ -6,8 +6,14 @@
6
6
  * Copyright (c) Infonomic Company Limited
7
7
  */
8
8
  import type { BlocksField as BlocksFieldType } from '@byline/core';
9
- export declare const BlocksField: ({ field, defaultValue, path, }: {
9
+ export declare const BlocksField: ({ field, defaultValue, path, contentLocale, }: {
10
10
  field: BlocksFieldType;
11
11
  defaultValue: any;
12
12
  path: string;
13
+ /**
14
+ * Active content locale, forwarded to each block item's fields so
15
+ * localized widgets nested inside a block (e.g. a `localized` richText)
16
+ * can render their locale badge.
17
+ */
18
+ contentLocale?: string;
13
19
  }) => import("react").JSX.Element;
@@ -8,7 +8,7 @@ import { GroupField } from "../group/group-field.js";
8
8
  import { SortableItem } from "../sortable-item.js";
9
9
  import { useFormContext } from "../../forms/form-context.js";
10
10
  import blocks_field_module from "./blocks-field.module.js";
11
- const BlocksField = ({ field, defaultValue, path })=>{
11
+ const BlocksField = ({ field, defaultValue, path, contentLocale })=>{
12
12
  const { appendPatch, getFieldValue, getFieldValues, setFieldStore } = useFormContext();
13
13
  const { t } = useTranslation('byline-admin');
14
14
  const [items, setItems] = useState([]);
@@ -125,7 +125,8 @@ const BlocksField = ({ field, defaultValue, path })=>{
125
125
  label: void 0
126
126
  },
127
127
  defaultValue: fieldData,
128
- path: arrayElementPath
128
+ path: arrayElementPath,
129
+ contentLocale: contentLocale
129
130
  }, subField.blockType);
130
131
  return /*#__PURE__*/ jsx(SortableItem, {
131
132
  id: itemWrapper.id,
@@ -169,14 +169,16 @@ const FieldRenderer = ({ field, defaultValue, basePath, disableSorting, hideLabe
169
169
  label: void 0
170
170
  } : field,
171
171
  defaultValue: defaultValue,
172
- path: path
172
+ path: path,
173
+ contentLocale: contentLocale
173
174
  });
174
175
  case 'blocks':
175
176
  if (!field.blocks) return null;
176
177
  return /*#__PURE__*/ jsx(BlocksField, {
177
178
  field: field,
178
179
  defaultValue: defaultValue,
179
- path: path
180
+ path: path,
181
+ contentLocale: contentLocale
180
182
  });
181
183
  case 'array':
182
184
  if (!field.fields) return null;
@@ -184,7 +186,8 @@ const FieldRenderer = ({ field, defaultValue, basePath, disableSorting, hideLabe
184
186
  field: field,
185
187
  defaultValue: defaultValue,
186
188
  path: path,
187
- disableSorting: disableSorting
189
+ disableSorting: disableSorting,
190
+ contentLocale: contentLocale
188
191
  });
189
192
  default:
190
193
  return null;
@@ -10,6 +10,12 @@ interface GroupFieldProps {
10
10
  field: GroupFieldType;
11
11
  defaultValue: any;
12
12
  path: string;
13
+ /**
14
+ * Active content locale, forwarded to child fields so localized widgets
15
+ * nested inside the group (e.g. a `localized` richText) can render their
16
+ * locale badge.
17
+ */
18
+ contentLocale?: string;
13
19
  }
14
- export declare const GroupField: ({ field, defaultValue, path }: GroupFieldProps) => import("react").JSX.Element;
20
+ export declare const GroupField: ({ field, defaultValue, path, contentLocale }: GroupFieldProps) => import("react").JSX.Element;
15
21
  export {};
@@ -6,7 +6,7 @@ import { placeholderForField } from "../field-helpers.js";
6
6
  import { FieldRenderer } from "../field-renderer.js";
7
7
  import { useFieldError } from "../../forms/form-context.js";
8
8
  import group_field_module from "./group-field.module.js";
9
- const GroupField = ({ field, defaultValue, path })=>{
9
+ const GroupField = ({ field, defaultValue, path, contentLocale })=>{
10
10
  const fieldError = useFieldError(field.name);
11
11
  const groupData = useMemo(()=>{
12
12
  if (defaultValue && 'object' == typeof defaultValue && !Array.isArray(defaultValue)) return defaultValue;
@@ -46,7 +46,8 @@ const GroupField = ({ field, defaultValue, path })=>{
46
46
  field: innerField,
47
47
  defaultValue: groupData[innerField.name],
48
48
  basePath: path,
49
- disableSorting: true
49
+ disableSorting: true,
50
+ contentLocale: contentLocale
50
51
  }, innerField.name))
51
52
  }),
52
53
  fieldError && /*#__PURE__*/ jsx(ErrorText, {
@@ -8,7 +8,7 @@ export interface DocumentActionsLocaleOption {
8
8
  code: string;
9
9
  label: string;
10
10
  }
11
- export declare function DocumentActions({ publishedVersion, onUnpublish, onDelete, onDuplicate, sourceTitle, onCopyToLocale, sourceLocale, contentLocales, }: {
11
+ export declare function DocumentActions({ publishedVersion, onUnpublish, onDelete, onDuplicate, sourceTitle, onCopyToLocale, sourceLocale, contentLocales, hasUnsavedChanges, onUnsavedChanges, onDeleteLocale, defaultLocale, availableLocales, }: {
12
12
  publishedVersion?: PublishedVersionInfo | null;
13
13
  onUnpublish?: () => Promise<void>;
14
14
  onDelete?: () => Promise<void>;
@@ -45,4 +45,37 @@ export declare function DocumentActions({ publishedVersion, onUnpublish, onDelet
45
45
  * Copy-to-Locale Select lists every locale except `sourceLocale`.
46
46
  */
47
47
  contentLocales?: ReadonlyArray<DocumentActionsLocaleOption>;
48
+ /**
49
+ * Whether the form currently has unsaved changes. Duplicate and
50
+ * Copy-to-Locale operate on the *saved* version, so when this is true
51
+ * the action is blocked and `onUnsavedChanges` fires instead of opening
52
+ * the action's modal. Delete is intentionally not gated.
53
+ */
54
+ hasUnsavedChanges?: boolean;
55
+ /**
56
+ * Called when a save-gated action (duplicate / copy-to-locale /
57
+ * delete-locale) is triggered while `hasUnsavedChanges` is true. The parent
58
+ * surfaces a "save first" prompt.
59
+ */
60
+ onUnsavedChanges?: () => void;
61
+ /**
62
+ * Called when the editor confirms the Delete-Locale modal. The parent runs
63
+ * the server fn, surfaces a toast, and navigates to a surviving locale.
64
+ * Menu item is hidden when omitted, or when the document has no non-default
65
+ * locale to delete.
66
+ */
67
+ onDeleteLocale?: (args: {
68
+ targetLocale: string;
69
+ }) => Promise<void>;
70
+ /**
71
+ * The default content locale (the document's anchor). Excluded from the
72
+ * Delete-Locale list — it can never be removed.
73
+ */
74
+ defaultLocale?: string;
75
+ /**
76
+ * The locales the document currently has content in (the derived
77
+ * `_availableVersionLocales` ledger). The Delete-Locale list is this set
78
+ * minus the default locale and the `'all'` sentinel.
79
+ */
80
+ availableLocales?: string[];
48
81
  }): import("react").JSX.Element;
@@ -6,7 +6,7 @@ import { Button, Checkbox, CloseIcon, DeleteIcon, Dropdown, EllipsisIcon, IconBu
6
6
  import classnames from "classnames";
7
7
  import document_actions_module from "./document-actions.module.js";
8
8
  const DUPLICATE_TITLE_SUFFIX = ' (copy)';
9
- function DocumentActions({ publishedVersion, onUnpublish, onDelete, onDuplicate, sourceTitle, onCopyToLocale, sourceLocale, contentLocales }) {
9
+ function DocumentActions({ publishedVersion, onUnpublish, onDelete, onDuplicate, sourceTitle, onCopyToLocale, sourceLocale, contentLocales, hasUnsavedChanges, onUnsavedChanges, onDeleteLocale, defaultLocale, availableLocales }) {
10
10
  const { t } = useTranslation('byline-admin');
11
11
  const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
12
12
  const [showDuplicateConfirm, setShowDuplicateConfirm] = useState(false);
@@ -17,6 +17,14 @@ function DocumentActions({ publishedVersion, onUnpublish, onDelete, onDuplicate,
17
17
  const [copyToLocaleBusy, setCopyToLocaleBusy] = useState(false);
18
18
  const [copyTargetLocale, setCopyTargetLocale] = useState(availableTargetLocales[0]?.code ?? '');
19
19
  const [copyOverwrite, setCopyOverwrite] = useState(false);
20
+ const deletableLocales = (availableLocales ?? []).filter((code)=>code !== defaultLocale && 'all' !== code).map((code)=>({
21
+ code,
22
+ label: contentLocales?.find((loc)=>loc.code === code)?.label ?? code
23
+ }));
24
+ const deleteLocaleAvailable = null != onDeleteLocale && deletableLocales.length > 0;
25
+ const [showDeleteLocaleConfirm, setShowDeleteLocaleConfirm] = useState(false);
26
+ const [deleteLocaleBusy, setDeleteLocaleBusy] = useState(false);
27
+ const [deleteTargetLocale, setDeleteTargetLocale] = useState('');
20
28
  const handleOnDelete = ()=>{
21
29
  setShowDeleteConfirm(false);
22
30
  if (onDelete) onDelete();
@@ -31,7 +39,12 @@ function DocumentActions({ publishedVersion, onUnpublish, onDelete, onDuplicate,
31
39
  setDuplicateBusy(false);
32
40
  }
33
41
  };
42
+ const handleOpenDuplicate = ()=>{
43
+ if (hasUnsavedChanges) return void onUnsavedChanges?.();
44
+ setShowDuplicateConfirm(true);
45
+ };
34
46
  const handleOpenCopyToLocale = ()=>{
47
+ if (hasUnsavedChanges) return void onUnsavedChanges?.();
35
48
  setCopyTargetLocale(availableTargetLocales[0]?.code ?? '');
36
49
  setCopyOverwrite(false);
37
50
  setShowCopyToLocaleConfirm(true);
@@ -49,6 +62,24 @@ function DocumentActions({ publishedVersion, onUnpublish, onDelete, onDuplicate,
49
62
  setCopyToLocaleBusy(false);
50
63
  }
51
64
  };
65
+ const handleOpenDeleteLocale = ()=>{
66
+ if (hasUnsavedChanges) return void onUnsavedChanges?.();
67
+ const preferred = deletableLocales.find((loc)=>loc.code === sourceLocale)?.code;
68
+ setDeleteTargetLocale(preferred ?? deletableLocales[0]?.code ?? '');
69
+ setShowDeleteLocaleConfirm(true);
70
+ };
71
+ const handleOnDeleteLocale = async ()=>{
72
+ if (!onDeleteLocale || !deleteTargetLocale) return;
73
+ setDeleteLocaleBusy(true);
74
+ try {
75
+ await onDeleteLocale({
76
+ targetLocale: deleteTargetLocale
77
+ });
78
+ setShowDeleteLocaleConfirm(false);
79
+ } finally{
80
+ setDeleteLocaleBusy(false);
81
+ }
82
+ };
52
83
  const duplicatePreviewBefore = sourceTitle ?? '';
53
84
  const duplicatePreviewAfter = (sourceTitle ?? '') + DUPLICATE_TITLE_SUFFIX;
54
85
  const sourceLocaleLabel = contentLocales?.find((loc)=>loc.code === sourceLocale)?.label ?? sourceLocale ?? '';
@@ -88,10 +119,21 @@ function DocumentActions({ publishedVersion, onUnpublish, onDelete, onDuplicate,
88
119
  })
89
120
  })
90
121
  }),
122
+ deleteLocaleAvailable && /*#__PURE__*/ jsx(Dropdown.Item, {
123
+ onClick: handleOpenDeleteLocale,
124
+ children: /*#__PURE__*/ jsx("div", {
125
+ className: classnames('byline-form-actions-item', document_actions_module.item),
126
+ children: /*#__PURE__*/ jsx("span", {
127
+ className: classnames('byline-form-actions-item-text', document_actions_module["item-text"]),
128
+ children: /*#__PURE__*/ jsx("button", {
129
+ type: "button",
130
+ children: t('documentActions.deleteLocale.menuItem')
131
+ })
132
+ })
133
+ })
134
+ }),
91
135
  onDuplicate && /*#__PURE__*/ jsx(Dropdown.Item, {
92
- onClick: ()=>{
93
- setShowDuplicateConfirm(true);
94
- },
136
+ onClick: handleOpenDuplicate,
95
137
  children: /*#__PURE__*/ jsx("div", {
96
138
  className: classnames('byline-form-actions-item', document_actions_module.item),
97
139
  children: /*#__PURE__*/ jsx("span", {
@@ -468,6 +510,116 @@ function DocumentActions({ publishedVersion, onUnpublish, onDelete, onDuplicate,
468
510
  })
469
511
  ]
470
512
  })
513
+ }),
514
+ /*#__PURE__*/ jsx(Modal, {
515
+ isOpen: showDeleteLocaleConfirm,
516
+ closeOnOverlayClick: !deleteLocaleBusy,
517
+ onDismiss: ()=>{
518
+ if (!deleteLocaleBusy) setShowDeleteLocaleConfirm(false);
519
+ },
520
+ children: /*#__PURE__*/ jsxs(Modal.Container, {
521
+ style: {
522
+ maxWidth: '560px'
523
+ },
524
+ children: [
525
+ /*#__PURE__*/ jsxs(Modal.Header, {
526
+ className: classnames('byline-form-actions-modal-head', document_actions_module["modal-head"]),
527
+ children: [
528
+ /*#__PURE__*/ jsx("h3", {
529
+ className: classnames('byline-form-actions-modal-title', document_actions_module["modal-title"]),
530
+ children: t('documentActions.deleteLocale.title')
531
+ }),
532
+ /*#__PURE__*/ jsx(IconButton, {
533
+ "arial-label": t('common.actions.close'),
534
+ size: "xs",
535
+ onClick: ()=>{
536
+ if (!deleteLocaleBusy) setShowDeleteLocaleConfirm(false);
537
+ },
538
+ children: /*#__PURE__*/ jsx(CloseIcon, {
539
+ width: "16px",
540
+ height: "16px",
541
+ svgClassName: "white-icon"
542
+ })
543
+ })
544
+ ]
545
+ }),
546
+ /*#__PURE__*/ jsxs(Modal.Content, {
547
+ children: [
548
+ /*#__PURE__*/ jsx("p", {
549
+ children: t('documentActions.deleteLocale.intro')
550
+ }),
551
+ /*#__PURE__*/ jsxs("div", {
552
+ className: classnames('byline-form-actions-copy-row', document_actions_module["copy-row"]),
553
+ style: {
554
+ marginTop: 'var(--spacing-12)'
555
+ },
556
+ children: [
557
+ /*#__PURE__*/ jsx("span", {
558
+ className: classnames('byline-form-actions-copy-label', document_actions_module["copy-label"]),
559
+ style: {
560
+ fontWeight: 500,
561
+ marginRight: 'var(--spacing-8)'
562
+ },
563
+ children: t('documentActions.deleteLocale.localeLabel')
564
+ }),
565
+ /*#__PURE__*/ jsx(Select, {
566
+ size: "sm",
567
+ ariaLabel: t('documentActions.deleteLocale.targetAriaLabel'),
568
+ value: deleteTargetLocale,
569
+ items: deletableLocales.map((loc)=>({
570
+ value: loc.code,
571
+ label: loc.label
572
+ })),
573
+ onValueChange: (value)=>{
574
+ if (null != value) setDeleteTargetLocale(value);
575
+ },
576
+ disabled: deleteLocaleBusy
577
+ })
578
+ ]
579
+ }),
580
+ /*#__PURE__*/ jsx("p", {
581
+ style: {
582
+ marginTop: 'var(--spacing-12)'
583
+ },
584
+ children: t('documentActions.deleteLocale.warning')
585
+ })
586
+ ]
587
+ }),
588
+ /*#__PURE__*/ jsxs(Modal.Actions, {
589
+ children: [
590
+ /*#__PURE__*/ jsx("button", {
591
+ "data-autofocus": true,
592
+ type: "button",
593
+ tabIndex: 0,
594
+ className: classnames('byline-form-actions-sr-only', document_actions_module["sr-only"]),
595
+ children: "no action"
596
+ }),
597
+ /*#__PURE__*/ jsx(Button, {
598
+ size: "sm",
599
+ style: {
600
+ minWidth: '80px'
601
+ },
602
+ intent: "noeffect",
603
+ onClick: ()=>{
604
+ if (!deleteLocaleBusy) setShowDeleteLocaleConfirm(false);
605
+ },
606
+ disabled: deleteLocaleBusy,
607
+ children: t('common.actions.cancel')
608
+ }),
609
+ /*#__PURE__*/ jsx(Button, {
610
+ size: "sm",
611
+ style: {
612
+ minWidth: '80px'
613
+ },
614
+ intent: "danger",
615
+ onClick: handleOnDeleteLocale,
616
+ disabled: deleteLocaleBusy || !deleteTargetLocale,
617
+ children: deleteLocaleBusy ? t('documentActions.deleteLocale.busyButton') : t('documentActions.deleteLocale.confirmButton')
618
+ })
619
+ ]
620
+ })
621
+ ]
622
+ })
471
623
  })
472
624
  ]
473
625
  });
@@ -44,6 +44,16 @@ export interface FormRendererProps {
44
44
  targetLocale: string;
45
45
  overwrite: boolean;
46
46
  }) => Promise<void>;
47
+ /**
48
+ * Called when the editor confirms the Delete-Locale modal in
49
+ * `DocumentActions`. Edit views provide a handler that invokes the
50
+ * `deleteDocumentLocale` server fn and navigates to a surviving locale.
51
+ * When omitted (or when the document has no non-default locale with
52
+ * content), the Delete-Locale menu item is hidden.
53
+ */
54
+ onDeleteLocale?: (args: {
55
+ targetLocale: string;
56
+ }) => Promise<void>;
47
57
  /**
48
58
  * All configured content locales (code + display label) — required for
49
59
  * the Copy-to-Locale modal's target Select. Threaded as an opaque list
@@ -102,4 +112,4 @@ export interface FormRendererProps {
102
112
  */
103
113
  useNavigationGuard?: UseNavigationGuard;
104
114
  }
105
- export declare const FormRenderer: ({ mode, fields, onSubmit, onCancel, onStatusChange, onUnpublish, onDelete, onDuplicate, onCopyToLocale, contentLocales, nextStatus, workflowStatuses, publishedVersion, initialData, adminConfig, useAsTitle, useAsPath, advertiseLocales, headingLabel, headerSlot, collectionPath, initialLocale, onLocaleChange, defaultLocale, useNavigationGuard, restoreWarnings, }: FormRendererProps) => import("react").JSX.Element;
115
+ export declare const FormRenderer: ({ mode, fields, onSubmit, onCancel, onStatusChange, onUnpublish, onDelete, onDuplicate, onCopyToLocale, onDeleteLocale, contentLocales, nextStatus, workflowStatuses, publishedVersion, initialData, adminConfig, useAsTitle, useAsPath, advertiseLocales, headingLabel, headerSlot, collectionPath, initialLocale, onLocaleChange, defaultLocale, useNavigationGuard, restoreWarnings, }: FormRendererProps) => import("react").JSX.Element;
@@ -140,13 +140,14 @@ function computeStatusTransitions(currentStatus, workflowStatuses, nextStatus) {
140
140
  isTerminal: false
141
141
  };
142
142
  }
143
- const FormContent = ({ mode, fields, onSubmit, onCancel, onStatusChange, onUnpublish, onDelete, onDuplicate, onCopyToLocale, contentLocales, nextStatus, workflowStatuses, publishedVersion, initialData, adminConfig, useAsTitle, useAsPath, advertiseLocales, headingLabel, headerSlot, collectionPath, initialLocale, onLocaleChange, defaultLocale = 'en', useNavigationGuard: useNavigationGuardProp, restoreWarnings, _activeTabBySet, _onTabChange })=>{
143
+ const FormContent = ({ mode, fields, onSubmit, onCancel, onStatusChange, onUnpublish, onDelete, onDuplicate, onCopyToLocale, onDeleteLocale, contentLocales, nextStatus, workflowStatuses, publishedVersion, initialData, adminConfig, useAsTitle, useAsPath, advertiseLocales, headingLabel, headerSlot, collectionPath, initialLocale, onLocaleChange, defaultLocale = 'en', useNavigationGuard: useNavigationGuardProp, restoreWarnings, _activeTabBySet, _onTabChange })=>{
144
144
  const { getFieldValues, runFieldHooks, validateForm, errors: initialErrors, hasChanges: hasChangesFn, resetHasChanges, getPatches, getSystemPath, getSystemAvailableLocales, subscribeErrors, subscribeMeta, setFieldValue, setFieldError, getPendingUploads, clearPendingUploads, setFieldUploading } = useFormContext();
145
145
  const { t } = useTranslation('byline-admin');
146
146
  const [errors, setErrors] = useState(initialErrors);
147
147
  const [hasChanges, setHasChanges] = useState(hasChangesFn());
148
148
  const [statusBusy, setStatusBusy] = useState(false);
149
149
  const [isUploading, setIsUploading] = useState(false);
150
+ const [showUnsavedModal, setShowUnsavedModal] = useState(false);
150
151
  const [contentLocale, setContentLocale] = useState(initialLocale ?? defaultLocale);
151
152
  const { uploadField } = useBylineFieldServices();
152
153
  useEffect(()=>{
@@ -438,6 +439,7 @@ const FormContent = ({ mode, fields, onSubmit, onCancel, onStatusChange, onUnpub
438
439
  intent: isTerminal ? 'info' : 'success',
439
440
  disabled: statusBusy,
440
441
  onOptionSelect: async (value)=>{
442
+ if (hasChanges) return void setShowUnsavedModal(true);
441
443
  setStatusBusy(true);
442
444
  try {
443
445
  await onStatusChange(value);
@@ -446,6 +448,7 @@ const FormContent = ({ mode, fields, onSubmit, onCancel, onStatusChange, onUnpub
446
448
  }
447
449
  },
448
450
  onButtonClick: isTerminal ? void 0 : async ()=>{
451
+ if (hasChanges) return void setShowUnsavedModal(true);
449
452
  setStatusBusy(true);
450
453
  try {
451
454
  await onStatusChange(primaryStatus.name);
@@ -464,7 +467,12 @@ const FormContent = ({ mode, fields, onSubmit, onCancel, onStatusChange, onUnpub
464
467
  sourceTitle: null != useAsTitle && null != initialData ? initialData[useAsTitle] : null,
465
468
  onCopyToLocale: onCopyToLocale,
466
469
  sourceLocale: contentLocale,
467
- contentLocales: contentLocales
470
+ contentLocales: contentLocales,
471
+ hasUnsavedChanges: hasChanges,
472
+ onUnsavedChanges: ()=>setShowUnsavedModal(true),
473
+ onDeleteLocale: onDeleteLocale,
474
+ defaultLocale: defaultLocale,
475
+ availableLocales: initialData?._availableVersionLocales
468
476
  })
469
477
  ]
470
478
  })
@@ -515,6 +523,43 @@ const FormContent = ({ mode, fields, onSubmit, onCancel, onStatusChange, onUnpub
515
523
  })
516
524
  ]
517
525
  }),
526
+ showUnsavedModal && /*#__PURE__*/ jsx(Modal, {
527
+ isOpen: true,
528
+ closeOnOverlayClick: true,
529
+ onDismiss: ()=>setShowUnsavedModal(false),
530
+ children: /*#__PURE__*/ jsxs(Modal.Container, {
531
+ style: {
532
+ maxWidth: '460px'
533
+ },
534
+ children: [
535
+ /*#__PURE__*/ jsx(Modal.Header, {
536
+ className: classnames('byline-form-guard-modal-head', form_renderer_module["guard-modal-head"]),
537
+ children: /*#__PURE__*/ jsx("h3", {
538
+ className: classnames('byline-form-guard-modal-title', form_renderer_module["guard-modal-title"]),
539
+ children: t('forms.unsavedChanges.title')
540
+ })
541
+ }),
542
+ /*#__PURE__*/ jsx(Modal.Content, {
543
+ children: /*#__PURE__*/ jsx("p", {
544
+ className: classnames('byline-form-guard-modal-text', form_renderer_module["guard-modal-text"]),
545
+ children: t('forms.unsavedChanges.message')
546
+ })
547
+ }),
548
+ /*#__PURE__*/ jsx(Modal.Actions, {
549
+ children: /*#__PURE__*/ jsx(Button, {
550
+ size: "sm",
551
+ style: {
552
+ minWidth: '60px'
553
+ },
554
+ intent: "primary",
555
+ type: "button",
556
+ onClick: ()=>setShowUnsavedModal(false),
557
+ children: t('forms.unsavedChanges.okButton')
558
+ })
559
+ })
560
+ ]
561
+ })
562
+ }),
518
563
  guard.isBlocked && /*#__PURE__*/ jsx(Modal, {
519
564
  isOpen: true,
520
565
  closeOnOverlayClick: false,
@@ -561,7 +606,7 @@ const FormContent = ({ mode, fields, onSubmit, onCancel, onStatusChange, onUnpub
561
606
  ]
562
607
  });
563
608
  };
564
- const FormRenderer = ({ mode, fields, onSubmit, onCancel, onStatusChange, onUnpublish, onDelete, onDuplicate, onCopyToLocale, contentLocales, nextStatus, workflowStatuses, publishedVersion, initialData, adminConfig, useAsTitle, useAsPath, advertiseLocales, headingLabel, headerSlot, collectionPath, initialLocale, onLocaleChange, defaultLocale, useNavigationGuard, restoreWarnings })=>{
609
+ const FormRenderer = ({ mode, fields, onSubmit, onCancel, onStatusChange, onUnpublish, onDelete, onDuplicate, onCopyToLocale, onDeleteLocale, contentLocales, nextStatus, workflowStatuses, publishedVersion, initialData, adminConfig, useAsTitle, useAsPath, advertiseLocales, headingLabel, headerSlot, collectionPath, initialLocale, onLocaleChange, defaultLocale, useNavigationGuard, restoreWarnings })=>{
565
610
  const savedTabsRef = useRef({});
566
611
  return /*#__PURE__*/ jsx(FormProvider, {
567
612
  initialData: initialData,
@@ -575,6 +620,7 @@ const FormRenderer = ({ mode, fields, onSubmit, onCancel, onStatusChange, onUnpu
575
620
  onDelete: onDelete,
576
621
  onDuplicate: onDuplicate,
577
622
  onCopyToLocale: onCopyToLocale,
623
+ onDeleteLocale: onDeleteLocale,
578
624
  contentLocales: contentLocales,
579
625
  nextStatus: nextStatus,
580
626
  workflowStatuses: workflowStatuses,
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@byline/admin",
3
3
  "private": false,
4
4
  "license": "MPL-2.0",
5
- "version": "3.0.1",
5
+ "version": "3.1.0",
6
6
  "engines": {
7
7
  "node": ">=20.9.0"
8
8
  },
@@ -147,10 +147,10 @@
147
147
  "uuid": "^14.0.0",
148
148
  "zod": "^4.4.3",
149
149
  "zod-form-data": "^3.0.1",
150
- "@byline/auth": "3.0.1",
151
- "@byline/core": "3.0.1",
152
- "@byline/i18n": "3.0.1",
153
- "@byline/ui": "3.0.1"
150
+ "@byline/auth": "3.1.0",
151
+ "@byline/core": "3.1.0",
152
+ "@byline/ui": "3.1.0",
153
+ "@byline/i18n": "3.1.0"
154
154
  },
155
155
  "peerDependencies": {
156
156
  "react": "^19.0.0",
@@ -29,11 +29,18 @@ export const ArrayField = ({
29
29
  defaultValue,
30
30
  path,
31
31
  disableSorting = false,
32
+ contentLocale,
32
33
  }: {
33
34
  field: ArrayFieldType
34
35
  defaultValue: any
35
36
  path: string
36
37
  disableSorting?: boolean
38
+ /**
39
+ * Active content locale, forwarded to each array item's fields so
40
+ * localized widgets nested inside an array (e.g. a `localized` richText)
41
+ * can render their locale badge.
42
+ */
43
+ contentLocale?: string
37
44
  }) => {
38
45
  const { appendPatch, getFieldValue, getFieldValues, setFieldStore } = useFormContext()
39
46
  const { t } = useTranslation('byline-admin')
@@ -190,6 +197,7 @@ export const ArrayField = ({
190
197
  defaultValue={groupData[innerField.name]}
191
198
  basePath={`${arrayElementPath}.${childField.name}`}
192
199
  disableSorting={true}
200
+ contentLocale={contentLocale}
193
201
  />
194
202
  ))}
195
203
  </div>
@@ -203,6 +211,7 @@ export const ArrayField = ({
203
211
  defaultValue={initial}
204
212
  basePath={arrayElementPath}
205
213
  disableSorting={true}
214
+ contentLocale={contentLocale}
206
215
  />
207
216
  )
208
217
  })
@@ -40,10 +40,17 @@ export const BlocksField = ({
40
40
  field,
41
41
  defaultValue,
42
42
  path,
43
+ contentLocale,
43
44
  }: {
44
45
  field: BlocksFieldType
45
46
  defaultValue: any
46
47
  path: string
48
+ /**
49
+ * Active content locale, forwarded to each block item's fields so
50
+ * localized widgets nested inside a block (e.g. a `localized` richText)
51
+ * can render their locale badge.
52
+ */
53
+ contentLocale?: string
47
54
  }) => {
48
55
  const { appendPatch, getFieldValue, getFieldValues, setFieldStore } = useFormContext()
49
56
  const { t } = useTranslation('byline-admin')
@@ -219,6 +226,7 @@ export const BlocksField = ({
219
226
  }
220
227
  defaultValue={fieldData}
221
228
  path={arrayElementPath}
229
+ contentLocale={contentLocale}
222
230
  />
223
231
  )
224
232
 
@@ -243,6 +243,7 @@ export const FieldRenderer = ({
243
243
  }
244
244
  defaultValue={defaultValue}
245
245
  path={path}
246
+ contentLocale={contentLocale}
246
247
  />
247
248
  )
248
249
  case 'blocks':
@@ -252,6 +253,7 @@ export const FieldRenderer = ({
252
253
  field={field as unknown as BlocksFieldType}
253
254
  defaultValue={defaultValue}
254
255
  path={path}
256
+ contentLocale={contentLocale}
255
257
  />
256
258
  )
257
259
  case 'array':
@@ -262,6 +264,7 @@ export const FieldRenderer = ({
262
264
  defaultValue={defaultValue}
263
265
  path={path}
264
266
  disableSorting={disableSorting}
267
+ contentLocale={contentLocale}
265
268
  />
266
269
  )
267
270
  default:
@@ -32,9 +32,15 @@ interface GroupFieldProps {
32
32
  field: GroupFieldType
33
33
  defaultValue: any
34
34
  path: string
35
+ /**
36
+ * Active content locale, forwarded to child fields so localized widgets
37
+ * nested inside the group (e.g. a `localized` richText) can render their
38
+ * locale badge.
39
+ */
40
+ contentLocale?: string
35
41
  }
36
42
 
37
- export const GroupField = ({ field, defaultValue, path }: GroupFieldProps) => {
43
+ export const GroupField = ({ field, defaultValue, path, contentLocale }: GroupFieldProps) => {
38
44
  const fieldError = useFieldError(field.name)
39
45
  // Default value for a group field is a plain object: { rating: 5, comment: '...' }
40
46
  // Normalize to a plain object if not already one.
@@ -74,6 +80,7 @@ export const GroupField = ({ field, defaultValue, path }: GroupFieldProps) => {
74
80
  defaultValue={groupData[innerField.name]}
75
81
  basePath={path}
76
82
  disableSorting={true}
83
+ contentLocale={contentLocale}
77
84
  />
78
85
  )
79
86
  })}
@@ -48,6 +48,11 @@ export function DocumentActions({
48
48
  onCopyToLocale,
49
49
  sourceLocale,
50
50
  contentLocales,
51
+ hasUnsavedChanges,
52
+ onUnsavedChanges,
53
+ onDeleteLocale,
54
+ defaultLocale,
55
+ availableLocales,
51
56
  }: {
52
57
  publishedVersion?: PublishedVersionInfo | null
53
58
  onUnpublish?: () => Promise<void>
@@ -82,6 +87,37 @@ export function DocumentActions({
82
87
  * Copy-to-Locale Select lists every locale except `sourceLocale`.
83
88
  */
84
89
  contentLocales?: ReadonlyArray<DocumentActionsLocaleOption>
90
+ /**
91
+ * Whether the form currently has unsaved changes. Duplicate and
92
+ * Copy-to-Locale operate on the *saved* version, so when this is true
93
+ * the action is blocked and `onUnsavedChanges` fires instead of opening
94
+ * the action's modal. Delete is intentionally not gated.
95
+ */
96
+ hasUnsavedChanges?: boolean
97
+ /**
98
+ * Called when a save-gated action (duplicate / copy-to-locale /
99
+ * delete-locale) is triggered while `hasUnsavedChanges` is true. The parent
100
+ * surfaces a "save first" prompt.
101
+ */
102
+ onUnsavedChanges?: () => void
103
+ /**
104
+ * Called when the editor confirms the Delete-Locale modal. The parent runs
105
+ * the server fn, surfaces a toast, and navigates to a surviving locale.
106
+ * Menu item is hidden when omitted, or when the document has no non-default
107
+ * locale to delete.
108
+ */
109
+ onDeleteLocale?: (args: { targetLocale: string }) => Promise<void>
110
+ /**
111
+ * The default content locale (the document's anchor). Excluded from the
112
+ * Delete-Locale list — it can never be removed.
113
+ */
114
+ defaultLocale?: string
115
+ /**
116
+ * The locales the document currently has content in (the derived
117
+ * `_availableVersionLocales` ledger). The Delete-Locale list is this set
118
+ * minus the default locale and the `'all'` sentinel.
119
+ */
120
+ availableLocales?: string[]
85
121
  }) {
86
122
  const { t } = useTranslation('byline-admin')
87
123
  const [showDeleteConfirm, setShowDeleteConfirm] = useState(false)
@@ -100,6 +136,21 @@ export function DocumentActions({
100
136
  )
101
137
  const [copyOverwrite, setCopyOverwrite] = useState(false)
102
138
 
139
+ // Delete-Locale modal state. The menu item is hidden unless the host
140
+ // supplied a handler AND the document has at least one non-default locale
141
+ // with content. The default locale is the document's anchor and is never
142
+ // listed (it cannot be removed).
143
+ const deletableLocales = (availableLocales ?? [])
144
+ .filter((code) => code !== defaultLocale && code !== 'all')
145
+ .map((code) => ({
146
+ code,
147
+ label: contentLocales?.find((loc) => loc.code === code)?.label ?? code,
148
+ }))
149
+ const deleteLocaleAvailable = onDeleteLocale != null && deletableLocales.length > 0
150
+ const [showDeleteLocaleConfirm, setShowDeleteLocaleConfirm] = useState(false)
151
+ const [deleteLocaleBusy, setDeleteLocaleBusy] = useState(false)
152
+ const [deleteTargetLocale, setDeleteTargetLocale] = useState<string>('')
153
+
103
154
  const handleOnDelete = () => {
104
155
  setShowDeleteConfirm(false)
105
156
  if (onDelete) {
@@ -118,7 +169,22 @@ export function DocumentActions({
118
169
  }
119
170
  }
120
171
 
172
+ const handleOpenDuplicate = () => {
173
+ // Duplicate copies the saved version — block when the form is dirty so
174
+ // unsaved edits are not silently dropped from the copy.
175
+ if (hasUnsavedChanges) {
176
+ onUnsavedChanges?.()
177
+ return
178
+ }
179
+ setShowDuplicateConfirm(true)
180
+ }
181
+
121
182
  const handleOpenCopyToLocale = () => {
183
+ // Copy-to-Locale reads the saved version — block when the form is dirty.
184
+ if (hasUnsavedChanges) {
185
+ onUnsavedChanges?.()
186
+ return
187
+ }
122
188
  // Reset on open: pick the first available target and clear the
123
189
  // overwrite checkbox so a previous-session "overwrite=true" choice
124
190
  // is not silently sticky.
@@ -138,6 +204,31 @@ export function DocumentActions({
138
204
  }
139
205
  }
140
206
 
207
+ const handleOpenDeleteLocale = () => {
208
+ // Delete-Locale removes the saved version's locale content — block when
209
+ // the form is dirty so the editor saves (or discards) first.
210
+ if (hasUnsavedChanges) {
211
+ onUnsavedChanges?.()
212
+ return
213
+ }
214
+ // Default to the currently-viewed locale when it is deletable, otherwise
215
+ // the first available target.
216
+ const preferred = deletableLocales.find((loc) => loc.code === sourceLocale)?.code
217
+ setDeleteTargetLocale(preferred ?? deletableLocales[0]?.code ?? '')
218
+ setShowDeleteLocaleConfirm(true)
219
+ }
220
+
221
+ const handleOnDeleteLocale = async () => {
222
+ if (!onDeleteLocale || !deleteTargetLocale) return
223
+ setDeleteLocaleBusy(true)
224
+ try {
225
+ await onDeleteLocale({ targetLocale: deleteTargetLocale })
226
+ setShowDeleteLocaleConfirm(false)
227
+ } finally {
228
+ setDeleteLocaleBusy(false)
229
+ }
230
+ }
231
+
141
232
  // Preview text shown inside the modal. Falls back to the literal suffix
142
233
  // when no source title is supplied (collections without `useAsTitle`).
143
234
  const duplicatePreviewBefore = sourceTitle ?? ''
@@ -188,12 +279,17 @@ export function DocumentActions({
188
279
  </div>
189
280
  </DropdownComponent.Item>
190
281
  )}
282
+ {deleteLocaleAvailable && (
283
+ <DropdownComponent.Item onClick={handleOpenDeleteLocale}>
284
+ <div className={cx('byline-form-actions-item', styles.item)}>
285
+ <span className={cx('byline-form-actions-item-text', styles['item-text'])}>
286
+ <button type="button">{t('documentActions.deleteLocale.menuItem')}</button>
287
+ </span>
288
+ </div>
289
+ </DropdownComponent.Item>
290
+ )}
191
291
  {onDuplicate && (
192
- <DropdownComponent.Item
193
- onClick={() => {
194
- setShowDuplicateConfirm(true)
195
- }}
196
- >
292
+ <DropdownComponent.Item onClick={handleOpenDuplicate}>
197
293
  <div className={cx('byline-form-actions-item', styles.item)}>
198
294
  <span className={cx('byline-form-actions-item-text', styles['item-text'])}>
199
295
  <button type="button">{t('common.actions.duplicate')}</button>
@@ -477,6 +573,93 @@ export function DocumentActions({
477
573
  </Modal.Actions>
478
574
  </Modal.Container>
479
575
  </Modal>
576
+
577
+ <Modal
578
+ isOpen={showDeleteLocaleConfirm}
579
+ closeOnOverlayClick={!deleteLocaleBusy}
580
+ onDismiss={() => {
581
+ if (!deleteLocaleBusy) setShowDeleteLocaleConfirm(false)
582
+ }}
583
+ >
584
+ <Modal.Container style={{ maxWidth: '560px' }}>
585
+ <Modal.Header className={cx('byline-form-actions-modal-head', styles['modal-head'])}>
586
+ <h3 className={cx('byline-form-actions-modal-title', styles['modal-title'])}>
587
+ {t('documentActions.deleteLocale.title')}
588
+ </h3>
589
+ <IconButton
590
+ arial-label={t('common.actions.close')}
591
+ size="xs"
592
+ onClick={() => {
593
+ if (!deleteLocaleBusy) setShowDeleteLocaleConfirm(false)
594
+ }}
595
+ >
596
+ <CloseIcon width="16px" height="16px" svgClassName="white-icon" />
597
+ </IconButton>
598
+ </Modal.Header>
599
+ <Modal.Content>
600
+ <p>{t('documentActions.deleteLocale.intro')}</p>
601
+ <div
602
+ className={cx('byline-form-actions-copy-row', styles['copy-row'])}
603
+ style={{ marginTop: 'var(--spacing-12)' }}
604
+ >
605
+ <span
606
+ className={cx('byline-form-actions-copy-label', styles['copy-label'])}
607
+ style={{ fontWeight: 500, marginRight: 'var(--spacing-8)' }}
608
+ >
609
+ {t('documentActions.deleteLocale.localeLabel')}
610
+ </span>
611
+ <Select<string>
612
+ size="sm"
613
+ ariaLabel={t('documentActions.deleteLocale.targetAriaLabel')}
614
+ value={deleteTargetLocale}
615
+ items={deletableLocales.map((loc) => ({
616
+ value: loc.code,
617
+ label: loc.label,
618
+ }))}
619
+ onValueChange={(value) => {
620
+ if (value != null) setDeleteTargetLocale(value)
621
+ }}
622
+ disabled={deleteLocaleBusy}
623
+ />
624
+ </div>
625
+ <p style={{ marginTop: 'var(--spacing-12)' }}>
626
+ {t('documentActions.deleteLocale.warning')}
627
+ </p>
628
+ </Modal.Content>
629
+ <Modal.Actions>
630
+ <button
631
+ data-autofocus
632
+ type="button"
633
+ tabIndex={0}
634
+ className={cx('byline-form-actions-sr-only', styles['sr-only'])}
635
+ >
636
+ no action
637
+ </button>
638
+ <Button
639
+ size="sm"
640
+ style={{ minWidth: '80px' }}
641
+ intent="noeffect"
642
+ onClick={() => {
643
+ if (!deleteLocaleBusy) setShowDeleteLocaleConfirm(false)
644
+ }}
645
+ disabled={deleteLocaleBusy}
646
+ >
647
+ {t('common.actions.cancel')}
648
+ </Button>
649
+ <Button
650
+ size="sm"
651
+ style={{ minWidth: '80px' }}
652
+ intent="danger"
653
+ onClick={handleOnDeleteLocale}
654
+ disabled={deleteLocaleBusy || !deleteTargetLocale}
655
+ >
656
+ {deleteLocaleBusy
657
+ ? t('documentActions.deleteLocale.busyButton')
658
+ : t('documentActions.deleteLocale.confirmButton')}
659
+ </Button>
660
+ </Modal.Actions>
661
+ </Modal.Container>
662
+ </Modal>
480
663
  </>
481
664
  )
482
665
  }
@@ -70,6 +70,14 @@ export interface FormRendererProps {
70
70
  * configured), the Copy-to-Locale menu item is hidden.
71
71
  */
72
72
  onCopyToLocale?: (args: { targetLocale: string; overwrite: boolean }) => Promise<void>
73
+ /**
74
+ * Called when the editor confirms the Delete-Locale modal in
75
+ * `DocumentActions`. Edit views provide a handler that invokes the
76
+ * `deleteDocumentLocale` server fn and navigates to a surviving locale.
77
+ * When omitted (or when the document has no non-default locale with
78
+ * content), the Delete-Locale menu item is hidden.
79
+ */
80
+ onDeleteLocale?: (args: { targetLocale: string }) => Promise<void>
73
81
  /**
74
82
  * All configured content locales (code + display label) — required for
75
83
  * the Copy-to-Locale modal's target Select. Threaded as an opaque list
@@ -299,6 +307,7 @@ const FormContent = ({
299
307
  onDelete,
300
308
  onDuplicate,
301
309
  onCopyToLocale,
310
+ onDeleteLocale,
302
311
  contentLocales,
303
312
  nextStatus,
304
313
  workflowStatuses,
@@ -347,6 +356,11 @@ const FormContent = ({
347
356
  const [hasChanges, setHasChanges] = useState(hasChangesFn())
348
357
  const [statusBusy, setStatusBusy] = useState(false)
349
358
  const [isUploading, setIsUploading] = useState(false)
359
+ // Block-only "save first" guard. Set true when the editor triggers a
360
+ // guarded action (status change, duplicate, copy-to-locale) while the form
361
+ // is dirty — those actions operate on the saved version, so unsaved edits
362
+ // would be silently excluded.
363
+ const [showUnsavedModal, setShowUnsavedModal] = useState(false)
350
364
  const [contentLocale, setContentLocale] = useState(initialLocale ?? defaultLocale)
351
365
  const { uploadField } = useBylineFieldServices()
352
366
 
@@ -726,6 +740,10 @@ const FormContent = ({
726
740
  intent={isTerminal ? 'info' : 'success'}
727
741
  disabled={statusBusy}
728
742
  onOptionSelect={async (value: string) => {
743
+ if (hasChanges) {
744
+ setShowUnsavedModal(true)
745
+ return
746
+ }
729
747
  setStatusBusy(true)
730
748
  try {
731
749
  await onStatusChange(value)
@@ -737,6 +755,10 @@ const FormContent = ({
737
755
  isTerminal
738
756
  ? undefined
739
757
  : async () => {
758
+ if (hasChanges) {
759
+ setShowUnsavedModal(true)
760
+ return
761
+ }
740
762
  setStatusBusy(true)
741
763
  try {
742
764
  await onStatusChange(primaryStatus.name)
@@ -770,6 +792,11 @@ const FormContent = ({
770
792
  onCopyToLocale={onCopyToLocale}
771
793
  sourceLocale={contentLocale}
772
794
  contentLocales={contentLocales}
795
+ hasUnsavedChanges={hasChanges}
796
+ onUnsavedChanges={() => setShowUnsavedModal(true)}
797
+ onDeleteLocale={onDeleteLocale}
798
+ defaultLocale={defaultLocale}
799
+ availableLocales={initialData?._availableVersionLocales as string[] | undefined}
773
800
  />
774
801
  </div>
775
802
  </div>
@@ -815,6 +842,39 @@ const FormContent = ({
815
842
  {(layout.sidebar ?? []).map((name) => renderItem(name))}
816
843
  </div>
817
844
  </div>
845
+ {showUnsavedModal && (
846
+ <Modal
847
+ isOpen={true}
848
+ closeOnOverlayClick={true}
849
+ onDismiss={() => setShowUnsavedModal(false)}
850
+ >
851
+ <Modal.Container style={{ maxWidth: '460px' }}>
852
+ <Modal.Header
853
+ className={cx('byline-form-guard-modal-head', styles['guard-modal-head'])}
854
+ >
855
+ <h3 className={cx('byline-form-guard-modal-title', styles['guard-modal-title'])}>
856
+ {t('forms.unsavedChanges.title')}
857
+ </h3>
858
+ </Modal.Header>
859
+ <Modal.Content>
860
+ <p className={cx('byline-form-guard-modal-text', styles['guard-modal-text'])}>
861
+ {t('forms.unsavedChanges.message')}
862
+ </p>
863
+ </Modal.Content>
864
+ <Modal.Actions>
865
+ <Button
866
+ size="sm"
867
+ style={{ minWidth: '60px' }}
868
+ intent="primary"
869
+ type="button"
870
+ onClick={() => setShowUnsavedModal(false)}
871
+ >
872
+ {t('forms.unsavedChanges.okButton')}
873
+ </Button>
874
+ </Modal.Actions>
875
+ </Modal.Container>
876
+ </Modal>
877
+ )}
818
878
  {guard.isBlocked && (
819
879
  <Modal isOpen={true} closeOnOverlayClick={false} onDismiss={guard.stay}>
820
880
  <Modal.Container style={{ maxWidth: '460px' }}>
@@ -855,6 +915,7 @@ export const FormRenderer = ({
855
915
  onDelete,
856
916
  onDuplicate,
857
917
  onCopyToLocale,
918
+ onDeleteLocale,
858
919
  contentLocales,
859
920
  nextStatus,
860
921
  workflowStatuses,
@@ -892,6 +953,7 @@ export const FormRenderer = ({
892
953
  onDelete={onDelete}
893
954
  onDuplicate={onDuplicate}
894
955
  onCopyToLocale={onCopyToLocale}
956
+ onDeleteLocale={onDeleteLocale}
895
957
  contentLocales={contentLocales}
896
958
  nextStatus={nextStatus}
897
959
  workflowStatuses={workflowStatuses}