@byline/admin 3.0.1 → 3.0.2
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/dist/fields/array/array-field.d.ts +7 -1
- package/dist/fields/array/array-field.js +5 -3
- package/dist/fields/blocks/blocks-field.d.ts +7 -1
- package/dist/fields/blocks/blocks-field.js +3 -2
- package/dist/fields/field-renderer.js +6 -3
- package/dist/fields/group/group-field.d.ts +7 -1
- package/dist/fields/group/group-field.js +3 -2
- package/dist/forms/document-actions.d.ts +34 -1
- package/dist/forms/document-actions.js +156 -4
- package/dist/forms/form-renderer.d.ts +11 -1
- package/dist/forms/form-renderer.js +49 -3
- package/package.json +5 -5
- package/src/fields/array/array-field.tsx +9 -0
- package/src/fields/blocks/blocks-field.tsx +8 -0
- package/src/fields/field-renderer.tsx +3 -0
- package/src/fields/group/group-field.tsx +8 -1
- package/src/forms/document-actions.tsx +188 -5
- package/src/forms/form-renderer.tsx +62 -0
|
@@ -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.
|
|
5
|
+
"version": "3.0.2",
|
|
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.
|
|
151
|
-
"@byline/core": "3.0.
|
|
152
|
-
"@byline/i18n": "3.0.
|
|
153
|
-
"@byline/ui": "3.0.
|
|
150
|
+
"@byline/auth": "3.0.2",
|
|
151
|
+
"@byline/core": "3.0.2",
|
|
152
|
+
"@byline/i18n": "3.0.2",
|
|
153
|
+
"@byline/ui": "3.0.2"
|
|
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}
|