@byline/ui 1.9.0 → 1.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/forms/document-actions.d.ts +43 -1
- package/dist/forms/document-actions.js +330 -20
- package/dist/forms/document-actions.module.js +17 -1
- package/dist/forms/document-actions_module.css +53 -1
- package/dist/forms/form-renderer.d.ts +26 -1
- package/dist/forms/form-renderer.js +11 -3
- package/package.json +4 -4
- package/src/forms/document-actions.module.css +67 -1
- package/src/forms/document-actions.tsx +336 -2
- package/src/forms/form-renderer.tsx +43 -1
|
@@ -1,6 +1,48 @@
|
|
|
1
1
|
import type { PublishedVersionInfo } from './form-renderer';
|
|
2
|
-
|
|
2
|
+
/**
|
|
3
|
+
* Shape of a content-locale option as consumed by the Copy-to-Locale
|
|
4
|
+
* modal. Matches the host adapter's `ContentLocaleOption`; declared
|
|
5
|
+
* locally so this package does not take a dependency on host code.
|
|
6
|
+
*/
|
|
7
|
+
export interface DocumentActionsLocaleOption {
|
|
8
|
+
code: string;
|
|
9
|
+
label: string;
|
|
10
|
+
}
|
|
11
|
+
export declare function DocumentActions({ publishedVersion, onUnpublish, onDelete, onDuplicate, sourceTitle, onCopyToLocale, sourceLocale, contentLocales, }: {
|
|
3
12
|
publishedVersion?: PublishedVersionInfo | null;
|
|
4
13
|
onUnpublish?: () => Promise<void>;
|
|
5
14
|
onDelete?: () => Promise<void>;
|
|
15
|
+
/**
|
|
16
|
+
* Called when the editor confirms the duplicate modal. The parent runs
|
|
17
|
+
* the server fn, surfaces a toast, and navigates to the new document.
|
|
18
|
+
*/
|
|
19
|
+
onDuplicate?: () => Promise<void>;
|
|
20
|
+
/**
|
|
21
|
+
* The current (saved) value of the source document's `useAsTitle`
|
|
22
|
+
* field, used to render the suffix preview inside the duplicate modal.
|
|
23
|
+
* Sourced from the form's `initialData`, not live form state, so the
|
|
24
|
+
* preview reflects what will actually be duplicated.
|
|
25
|
+
*/
|
|
26
|
+
sourceTitle?: string | null;
|
|
27
|
+
/**
|
|
28
|
+
* Called when the editor confirms the Copy-to-Locale modal. The
|
|
29
|
+
* parent runs the server fn, surfaces a toast, and navigates to the
|
|
30
|
+
* target locale view. Menu item is hidden when omitted, or when fewer
|
|
31
|
+
* than two content locales are configured.
|
|
32
|
+
*/
|
|
33
|
+
onCopyToLocale?: (args: {
|
|
34
|
+
targetLocale: string;
|
|
35
|
+
overwrite: boolean;
|
|
36
|
+
}) => Promise<void>;
|
|
37
|
+
/**
|
|
38
|
+
* The locale the form is currently displaying. Used as the read-only
|
|
39
|
+
* "From" label in the Copy-to-Locale modal and excluded from the
|
|
40
|
+
* target Select.
|
|
41
|
+
*/
|
|
42
|
+
sourceLocale?: string;
|
|
43
|
+
/**
|
|
44
|
+
* All configured content locales (code + display label). The
|
|
45
|
+
* Copy-to-Locale Select lists every locale except `sourceLocale`.
|
|
46
|
+
*/
|
|
47
|
+
contentLocales?: ReadonlyArray<DocumentActionsLocaleOption>;
|
|
6
48
|
}): import("react").JSX.Element;
|
|
@@ -2,14 +2,54 @@
|
|
|
2
2
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { useState } from "react";
|
|
4
4
|
import classnames from "classnames";
|
|
5
|
-
import { Button, CloseIcon, DeleteIcon, Dropdown, EllipsisIcon, IconButton, Modal } from "../uikit.js";
|
|
5
|
+
import { Button, Checkbox, CloseIcon, DeleteIcon, Dropdown, EllipsisIcon, IconButton, Modal, Select } from "../uikit.js";
|
|
6
6
|
import document_actions_module from "./document-actions.module.js";
|
|
7
|
-
|
|
7
|
+
const DUPLICATE_TITLE_SUFFIX = ' (copy)';
|
|
8
|
+
function DocumentActions({ publishedVersion, onUnpublish, onDelete, onDuplicate, sourceTitle, onCopyToLocale, sourceLocale, contentLocales }) {
|
|
8
9
|
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
|
10
|
+
const [showDuplicateConfirm, setShowDuplicateConfirm] = useState(false);
|
|
11
|
+
const [duplicateBusy, setDuplicateBusy] = useState(false);
|
|
12
|
+
const availableTargetLocales = (contentLocales ?? []).filter((loc)=>loc.code !== sourceLocale);
|
|
13
|
+
const copyToLocaleAvailable = null != onCopyToLocale && availableTargetLocales.length > 0;
|
|
14
|
+
const [showCopyToLocaleConfirm, setShowCopyToLocaleConfirm] = useState(false);
|
|
15
|
+
const [copyToLocaleBusy, setCopyToLocaleBusy] = useState(false);
|
|
16
|
+
const [copyTargetLocale, setCopyTargetLocale] = useState(availableTargetLocales[0]?.code ?? '');
|
|
17
|
+
const [copyOverwrite, setCopyOverwrite] = useState(false);
|
|
9
18
|
const handleOnDelete = ()=>{
|
|
10
19
|
setShowDeleteConfirm(false);
|
|
11
20
|
if (onDelete) onDelete();
|
|
12
21
|
};
|
|
22
|
+
const handleOnDuplicate = async ()=>{
|
|
23
|
+
if (!onDuplicate) return;
|
|
24
|
+
setDuplicateBusy(true);
|
|
25
|
+
try {
|
|
26
|
+
await onDuplicate();
|
|
27
|
+
setShowDuplicateConfirm(false);
|
|
28
|
+
} finally{
|
|
29
|
+
setDuplicateBusy(false);
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
const handleOpenCopyToLocale = ()=>{
|
|
33
|
+
setCopyTargetLocale(availableTargetLocales[0]?.code ?? '');
|
|
34
|
+
setCopyOverwrite(false);
|
|
35
|
+
setShowCopyToLocaleConfirm(true);
|
|
36
|
+
};
|
|
37
|
+
const handleOnCopyToLocale = async ()=>{
|
|
38
|
+
if (!onCopyToLocale || !copyTargetLocale) return;
|
|
39
|
+
setCopyToLocaleBusy(true);
|
|
40
|
+
try {
|
|
41
|
+
await onCopyToLocale({
|
|
42
|
+
targetLocale: copyTargetLocale,
|
|
43
|
+
overwrite: copyOverwrite
|
|
44
|
+
});
|
|
45
|
+
setShowCopyToLocaleConfirm(false);
|
|
46
|
+
} finally{
|
|
47
|
+
setCopyToLocaleBusy(false);
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
const duplicatePreviewBefore = sourceTitle ?? '';
|
|
51
|
+
const duplicatePreviewAfter = (sourceTitle ?? '') + DUPLICATE_TITLE_SUFFIX;
|
|
52
|
+
const sourceLocaleLabel = contentLocales?.find((loc)=>loc.code === sourceLocale)?.label ?? sourceLocale ?? '';
|
|
13
53
|
return /*#__PURE__*/ jsxs(Fragment, {
|
|
14
54
|
children: [
|
|
15
55
|
/*#__PURE__*/ jsxs(Dropdown.Root, {
|
|
@@ -33,26 +73,35 @@ function DocumentActions({ publishedVersion, onUnpublish, onDelete }) {
|
|
|
33
73
|
"data-side": "top",
|
|
34
74
|
sideOffset: 10,
|
|
35
75
|
children: [
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
}),
|
|
46
|
-
/*#__PURE__*/ jsx("span", {
|
|
47
|
-
className: classnames('byline-form-actions-item-text', document_actions_module["item-text"]),
|
|
48
|
-
children: "Unpublish"
|
|
49
|
-
})
|
|
50
|
-
]
|
|
76
|
+
copyToLocaleAvailable && /*#__PURE__*/ jsx(Dropdown.Item, {
|
|
77
|
+
onClick: handleOpenCopyToLocale,
|
|
78
|
+
children: /*#__PURE__*/ jsx("div", {
|
|
79
|
+
className: classnames('byline-form-actions-item', document_actions_module.item),
|
|
80
|
+
children: /*#__PURE__*/ jsx("span", {
|
|
81
|
+
className: classnames('byline-form-actions-item-text', document_actions_module["item-text"]),
|
|
82
|
+
children: /*#__PURE__*/ jsx("button", {
|
|
83
|
+
type: "button",
|
|
84
|
+
children: "Copy to Locale"
|
|
51
85
|
})
|
|
52
|
-
})
|
|
53
|
-
|
|
54
|
-
]
|
|
86
|
+
})
|
|
87
|
+
})
|
|
55
88
|
}),
|
|
89
|
+
onDuplicate && /*#__PURE__*/ jsx(Dropdown.Item, {
|
|
90
|
+
onClick: ()=>{
|
|
91
|
+
setShowDuplicateConfirm(true);
|
|
92
|
+
},
|
|
93
|
+
children: /*#__PURE__*/ jsx("div", {
|
|
94
|
+
className: classnames('byline-form-actions-item', document_actions_module.item),
|
|
95
|
+
children: /*#__PURE__*/ jsx("span", {
|
|
96
|
+
className: classnames('byline-form-actions-item-text', document_actions_module["item-text"]),
|
|
97
|
+
children: /*#__PURE__*/ jsx("button", {
|
|
98
|
+
type: "button",
|
|
99
|
+
children: "Duplicate"
|
|
100
|
+
})
|
|
101
|
+
})
|
|
102
|
+
})
|
|
103
|
+
}),
|
|
104
|
+
/*#__PURE__*/ jsx(Dropdown.Separator, {}),
|
|
56
105
|
/*#__PURE__*/ jsx(Dropdown.Item, {
|
|
57
106
|
onClick: ()=>{
|
|
58
107
|
setShowDeleteConfirm(true);
|
|
@@ -131,6 +180,9 @@ function DocumentActions({ publishedVersion, onUnpublish, onDelete }) {
|
|
|
131
180
|
}),
|
|
132
181
|
/*#__PURE__*/ jsx(Button, {
|
|
133
182
|
size: "sm",
|
|
183
|
+
style: {
|
|
184
|
+
minWidth: '80px'
|
|
185
|
+
},
|
|
134
186
|
intent: "noeffect",
|
|
135
187
|
onClick: ()=>{
|
|
136
188
|
setShowDeleteConfirm(false);
|
|
@@ -139,6 +191,9 @@ function DocumentActions({ publishedVersion, onUnpublish, onDelete }) {
|
|
|
139
191
|
}),
|
|
140
192
|
/*#__PURE__*/ jsx(Button, {
|
|
141
193
|
size: "sm",
|
|
194
|
+
style: {
|
|
195
|
+
minWidth: '80px'
|
|
196
|
+
},
|
|
142
197
|
intent: "danger",
|
|
143
198
|
onClick: handleOnDelete,
|
|
144
199
|
children: "Delete"
|
|
@@ -147,6 +202,261 @@ function DocumentActions({ publishedVersion, onUnpublish, onDelete }) {
|
|
|
147
202
|
})
|
|
148
203
|
]
|
|
149
204
|
})
|
|
205
|
+
}),
|
|
206
|
+
/*#__PURE__*/ jsx(Modal, {
|
|
207
|
+
isOpen: showDuplicateConfirm,
|
|
208
|
+
closeOnOverlayClick: !duplicateBusy,
|
|
209
|
+
onDismiss: ()=>{
|
|
210
|
+
if (!duplicateBusy) setShowDuplicateConfirm(false);
|
|
211
|
+
},
|
|
212
|
+
children: /*#__PURE__*/ jsxs(Modal.Container, {
|
|
213
|
+
style: {
|
|
214
|
+
maxWidth: '560px'
|
|
215
|
+
},
|
|
216
|
+
children: [
|
|
217
|
+
/*#__PURE__*/ jsxs(Modal.Header, {
|
|
218
|
+
className: classnames('byline-form-actions-modal-head', document_actions_module["modal-head"]),
|
|
219
|
+
children: [
|
|
220
|
+
/*#__PURE__*/ jsx("h3", {
|
|
221
|
+
className: classnames('byline-form-actions-modal-title', document_actions_module["modal-title"]),
|
|
222
|
+
children: "Duplicate Document"
|
|
223
|
+
}),
|
|
224
|
+
/*#__PURE__*/ jsx(IconButton, {
|
|
225
|
+
"arial-label": "Close",
|
|
226
|
+
size: "xs",
|
|
227
|
+
onClick: ()=>{
|
|
228
|
+
if (!duplicateBusy) setShowDuplicateConfirm(false);
|
|
229
|
+
},
|
|
230
|
+
children: /*#__PURE__*/ jsx(CloseIcon, {
|
|
231
|
+
width: "16px",
|
|
232
|
+
height: "16px",
|
|
233
|
+
svgClassName: "white-icon"
|
|
234
|
+
})
|
|
235
|
+
})
|
|
236
|
+
]
|
|
237
|
+
}),
|
|
238
|
+
/*#__PURE__*/ jsxs(Modal.Content, {
|
|
239
|
+
className: "prose",
|
|
240
|
+
children: [
|
|
241
|
+
/*#__PURE__*/ jsx("p", {
|
|
242
|
+
className: "m-0",
|
|
243
|
+
children: "A new document will be created with all translations (if any) cloned from this one. After the duplicate is created you should:"
|
|
244
|
+
}),
|
|
245
|
+
/*#__PURE__*/ jsxs("ul", {
|
|
246
|
+
className: classnames('byline-form-actions-list', document_actions_module.list),
|
|
247
|
+
children: [
|
|
248
|
+
/*#__PURE__*/ jsxs("li", {
|
|
249
|
+
children: [
|
|
250
|
+
"Update the title of the docuiment (including any translated versions). The title is currently suffixed with ",
|
|
251
|
+
/*#__PURE__*/ jsx("code", {
|
|
252
|
+
children: DUPLICATE_TITLE_SUFFIX.trim()
|
|
253
|
+
}),
|
|
254
|
+
"."
|
|
255
|
+
]
|
|
256
|
+
}),
|
|
257
|
+
/*#__PURE__*/ jsx("li", {
|
|
258
|
+
children: "Review the system path in the path widget — the auto-generated path will reflect the suffixed title and is unlikely to be what you want long-term."
|
|
259
|
+
})
|
|
260
|
+
]
|
|
261
|
+
}),
|
|
262
|
+
null != sourceTitle && sourceTitle.length > 0 && /*#__PURE__*/ jsxs("div", {
|
|
263
|
+
className: classnames('byline-form-actions-preview', document_actions_module.preview),
|
|
264
|
+
children: [
|
|
265
|
+
/*#__PURE__*/ jsx("div", {
|
|
266
|
+
className: classnames('byline-form-actions-preview-label', document_actions_module["preview-label"]),
|
|
267
|
+
children: "Preview (current locale):"
|
|
268
|
+
}),
|
|
269
|
+
/*#__PURE__*/ jsxs("div", {
|
|
270
|
+
className: classnames('byline-form-actions-preview-row', document_actions_module["preview-row"]),
|
|
271
|
+
children: [
|
|
272
|
+
/*#__PURE__*/ jsx("span", {
|
|
273
|
+
className: classnames('byline-form-actions-preview-before', document_actions_module["preview-before"]),
|
|
274
|
+
children: duplicatePreviewBefore
|
|
275
|
+
}),
|
|
276
|
+
/*#__PURE__*/ jsx("span", {
|
|
277
|
+
className: classnames('byline-form-actions-preview-arrow', document_actions_module["preview-arrow"]),
|
|
278
|
+
children: "→"
|
|
279
|
+
}),
|
|
280
|
+
/*#__PURE__*/ jsx("span", {
|
|
281
|
+
className: classnames('byline-form-actions-preview-after', document_actions_module["preview-after"]),
|
|
282
|
+
children: duplicatePreviewAfter
|
|
283
|
+
})
|
|
284
|
+
]
|
|
285
|
+
})
|
|
286
|
+
]
|
|
287
|
+
})
|
|
288
|
+
]
|
|
289
|
+
}),
|
|
290
|
+
/*#__PURE__*/ jsxs(Modal.Actions, {
|
|
291
|
+
children: [
|
|
292
|
+
/*#__PURE__*/ jsx("button", {
|
|
293
|
+
"data-autofocus": true,
|
|
294
|
+
type: "button",
|
|
295
|
+
tabIndex: 0,
|
|
296
|
+
className: classnames('byline-form-actions-sr-only', document_actions_module["sr-only"]),
|
|
297
|
+
children: "no action"
|
|
298
|
+
}),
|
|
299
|
+
/*#__PURE__*/ jsx(Button, {
|
|
300
|
+
size: "sm",
|
|
301
|
+
style: {
|
|
302
|
+
minWidth: '80px'
|
|
303
|
+
},
|
|
304
|
+
intent: "noeffect",
|
|
305
|
+
onClick: ()=>{
|
|
306
|
+
if (!duplicateBusy) setShowDuplicateConfirm(false);
|
|
307
|
+
},
|
|
308
|
+
disabled: duplicateBusy,
|
|
309
|
+
children: "Cancel"
|
|
310
|
+
}),
|
|
311
|
+
/*#__PURE__*/ jsx(Button, {
|
|
312
|
+
size: "sm",
|
|
313
|
+
style: {
|
|
314
|
+
minWidth: '80px'
|
|
315
|
+
},
|
|
316
|
+
intent: "primary",
|
|
317
|
+
onClick: handleOnDuplicate,
|
|
318
|
+
disabled: duplicateBusy,
|
|
319
|
+
children: duplicateBusy ? 'Duplicating...' : 'Duplicate'
|
|
320
|
+
})
|
|
321
|
+
]
|
|
322
|
+
})
|
|
323
|
+
]
|
|
324
|
+
})
|
|
325
|
+
}),
|
|
326
|
+
/*#__PURE__*/ jsx(Modal, {
|
|
327
|
+
isOpen: showCopyToLocaleConfirm,
|
|
328
|
+
closeOnOverlayClick: !copyToLocaleBusy,
|
|
329
|
+
onDismiss: ()=>{
|
|
330
|
+
if (!copyToLocaleBusy) setShowCopyToLocaleConfirm(false);
|
|
331
|
+
},
|
|
332
|
+
children: /*#__PURE__*/ jsxs(Modal.Container, {
|
|
333
|
+
style: {
|
|
334
|
+
maxWidth: '560px'
|
|
335
|
+
},
|
|
336
|
+
children: [
|
|
337
|
+
/*#__PURE__*/ jsxs(Modal.Header, {
|
|
338
|
+
className: classnames('byline-form-actions-modal-head', document_actions_module["modal-head"]),
|
|
339
|
+
children: [
|
|
340
|
+
/*#__PURE__*/ jsx("h3", {
|
|
341
|
+
className: classnames('byline-form-actions-modal-title', document_actions_module["modal-title"]),
|
|
342
|
+
children: "Copy to Locale"
|
|
343
|
+
}),
|
|
344
|
+
/*#__PURE__*/ jsx(IconButton, {
|
|
345
|
+
"arial-label": "Close",
|
|
346
|
+
size: "xs",
|
|
347
|
+
onClick: ()=>{
|
|
348
|
+
if (!copyToLocaleBusy) setShowCopyToLocaleConfirm(false);
|
|
349
|
+
},
|
|
350
|
+
children: /*#__PURE__*/ jsx(CloseIcon, {
|
|
351
|
+
width: "16px",
|
|
352
|
+
height: "16px",
|
|
353
|
+
svgClassName: "white-icon"
|
|
354
|
+
})
|
|
355
|
+
})
|
|
356
|
+
]
|
|
357
|
+
}),
|
|
358
|
+
/*#__PURE__*/ jsxs(Modal.Content, {
|
|
359
|
+
children: [
|
|
360
|
+
/*#__PURE__*/ jsx("p", {
|
|
361
|
+
children: "Copy this document's content from one locale to another. Non-localized fields are shared across locales and will not change."
|
|
362
|
+
}),
|
|
363
|
+
/*#__PURE__*/ jsxs("div", {
|
|
364
|
+
className: classnames('byline-form-actions-copy-row', document_actions_module["copy-row"]),
|
|
365
|
+
style: {
|
|
366
|
+
marginTop: 'var(--spacing-12)'
|
|
367
|
+
},
|
|
368
|
+
children: [
|
|
369
|
+
/*#__PURE__*/ jsx("span", {
|
|
370
|
+
className: classnames('byline-form-actions-copy-label', document_actions_module["copy-label"]),
|
|
371
|
+
style: {
|
|
372
|
+
fontWeight: 500
|
|
373
|
+
},
|
|
374
|
+
children: "From:"
|
|
375
|
+
}),
|
|
376
|
+
' ',
|
|
377
|
+
/*#__PURE__*/ jsx("span", {
|
|
378
|
+
className: classnames('byline-form-actions-copy-source', document_actions_module["copy-source"]),
|
|
379
|
+
children: sourceLocaleLabel
|
|
380
|
+
})
|
|
381
|
+
]
|
|
382
|
+
}),
|
|
383
|
+
/*#__PURE__*/ jsxs("div", {
|
|
384
|
+
className: classnames('byline-form-actions-copy-row', document_actions_module["copy-row"]),
|
|
385
|
+
style: {
|
|
386
|
+
marginTop: 'var(--spacing-12)'
|
|
387
|
+
},
|
|
388
|
+
children: [
|
|
389
|
+
/*#__PURE__*/ jsx("span", {
|
|
390
|
+
className: classnames('byline-form-actions-copy-label', document_actions_module["copy-label"]),
|
|
391
|
+
style: {
|
|
392
|
+
fontWeight: 500,
|
|
393
|
+
marginRight: 'var(--spacing-8)'
|
|
394
|
+
},
|
|
395
|
+
children: "To:"
|
|
396
|
+
}),
|
|
397
|
+
/*#__PURE__*/ jsx(Select, {
|
|
398
|
+
size: "sm",
|
|
399
|
+
ariaLabel: "Target locale",
|
|
400
|
+
value: copyTargetLocale,
|
|
401
|
+
items: availableTargetLocales.map((loc)=>({
|
|
402
|
+
value: loc.code,
|
|
403
|
+
label: loc.label
|
|
404
|
+
})),
|
|
405
|
+
onValueChange: (value)=>{
|
|
406
|
+
if (null != value) setCopyTargetLocale(value);
|
|
407
|
+
},
|
|
408
|
+
disabled: copyToLocaleBusy
|
|
409
|
+
})
|
|
410
|
+
]
|
|
411
|
+
}),
|
|
412
|
+
/*#__PURE__*/ jsx("div", {
|
|
413
|
+
className: classnames('byline-form-actions-copy-row', document_actions_module["copy-row"]),
|
|
414
|
+
style: {
|
|
415
|
+
marginTop: 'var(--spacing-16)'
|
|
416
|
+
},
|
|
417
|
+
children: /*#__PURE__*/ jsx(Checkbox, {
|
|
418
|
+
id: "copy-to-locale-overwrite",
|
|
419
|
+
name: "overwrite",
|
|
420
|
+
label: "Overwrite existing field data in target locale",
|
|
421
|
+
checked: copyOverwrite,
|
|
422
|
+
disabled: copyToLocaleBusy,
|
|
423
|
+
helpText: "Unchecked: only fill in target fields that are currently empty. Checked: replace every translated field with the source's value.",
|
|
424
|
+
onCheckedChange: (value)=>{
|
|
425
|
+
setCopyOverwrite(true === value);
|
|
426
|
+
}
|
|
427
|
+
})
|
|
428
|
+
})
|
|
429
|
+
]
|
|
430
|
+
}),
|
|
431
|
+
/*#__PURE__*/ jsxs(Modal.Actions, {
|
|
432
|
+
children: [
|
|
433
|
+
/*#__PURE__*/ jsx("button", {
|
|
434
|
+
"data-autofocus": true,
|
|
435
|
+
type: "button",
|
|
436
|
+
tabIndex: 0,
|
|
437
|
+
className: classnames('byline-form-actions-sr-only', document_actions_module["sr-only"]),
|
|
438
|
+
children: "no action"
|
|
439
|
+
}),
|
|
440
|
+
/*#__PURE__*/ jsx(Button, {
|
|
441
|
+
size: "sm",
|
|
442
|
+
intent: "noeffect",
|
|
443
|
+
onClick: ()=>{
|
|
444
|
+
if (!copyToLocaleBusy) setShowCopyToLocaleConfirm(false);
|
|
445
|
+
},
|
|
446
|
+
disabled: copyToLocaleBusy,
|
|
447
|
+
children: "Cancel"
|
|
448
|
+
}),
|
|
449
|
+
/*#__PURE__*/ jsx(Button, {
|
|
450
|
+
size: "sm",
|
|
451
|
+
intent: "primary",
|
|
452
|
+
onClick: handleOnCopyToLocale,
|
|
453
|
+
disabled: copyToLocaleBusy || !copyTargetLocale,
|
|
454
|
+
children: copyToLocaleBusy ? 'Copying...' : 'Copy'
|
|
455
|
+
})
|
|
456
|
+
]
|
|
457
|
+
})
|
|
458
|
+
]
|
|
459
|
+
})
|
|
150
460
|
})
|
|
151
461
|
]
|
|
152
462
|
});
|
|
@@ -13,6 +13,22 @@ const document_actions_module = {
|
|
|
13
13
|
"modal-title": "modal-title-e2sqE2",
|
|
14
14
|
modalTitle: "modal-title-e2sqE2",
|
|
15
15
|
"sr-only": "sr-only-VaKvc6",
|
|
16
|
-
srOnly: "sr-only-VaKvc6"
|
|
16
|
+
srOnly: "sr-only-VaKvc6",
|
|
17
|
+
list: "list-TuqTao",
|
|
18
|
+
preview: "preview-oePPZR",
|
|
19
|
+
"preview-label": "preview-label-bvWDqG",
|
|
20
|
+
previewLabel: "preview-label-bvWDqG",
|
|
21
|
+
"preview-row": "preview-row-NyFKjX",
|
|
22
|
+
previewRow: "preview-row-NyFKjX",
|
|
23
|
+
"preview-before": "preview-before-tmxeNQ",
|
|
24
|
+
previewBefore: "preview-before-tmxeNQ",
|
|
25
|
+
"preview-arrow": "preview-arrow-plq330",
|
|
26
|
+
previewArrow: "preview-arrow-plq330",
|
|
27
|
+
"preview-after": "preview-after-pYlSUr",
|
|
28
|
+
previewAfter: "preview-after-pYlSUr",
|
|
29
|
+
"copy-row": "copy-row-EGqeNw",
|
|
30
|
+
copyRow: "copy-row-EGqeNw",
|
|
31
|
+
"copy-source": "copy-source-IV71b6",
|
|
32
|
+
copySource: "copy-source-IV71b6"
|
|
17
33
|
};
|
|
18
34
|
export default document_actions_module;
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
}
|
|
5
5
|
|
|
6
6
|
:is(.menu-IxqAyg, .byline-form-actions-menu) {
|
|
7
|
-
min-width:
|
|
7
|
+
min-width: 140px;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
:is(.item-k3szmy, .byline-form-actions-item) {
|
|
@@ -60,6 +60,58 @@
|
|
|
60
60
|
overflow: hidden;
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
:is(.list-TuqTao, .byline-form-actions-list) {
|
|
64
|
+
margin: var(--spacing-8) 0;
|
|
65
|
+
font-size: var(--font-size-sm);
|
|
66
|
+
padding-left: 1.25rem;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
:is(.preview-oePPZR, .byline-form-actions-preview) {
|
|
70
|
+
margin-top: var(--spacing-16);
|
|
71
|
+
padding: var(--spacing-8) var(--spacing-12);
|
|
72
|
+
background: var(--surface-2, #00000008);
|
|
73
|
+
border: 1px solid var(--border-subtle, #00000014);
|
|
74
|
+
border-radius: var(--radius-md, 4px);
|
|
75
|
+
font-size: var(--font-size-sm);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
:is(.preview-label-bvWDqG, .byline-form-actions-preview-label) {
|
|
79
|
+
margin-bottom: var(--spacing-4);
|
|
80
|
+
opacity: .75;
|
|
81
|
+
font-weight: 500;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
:is(.preview-row-NyFKjX, .byline-form-actions-preview-row) {
|
|
85
|
+
align-items: center;
|
|
86
|
+
gap: var(--spacing-8);
|
|
87
|
+
flex-wrap: wrap;
|
|
88
|
+
display: flex;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
:is(.preview-before-tmxeNQ, .byline-form-actions-preview-before) {
|
|
92
|
+
font-family: var(--font-mono, monospace);
|
|
93
|
+
opacity: .7;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
:is(.preview-arrow-plq330, .byline-form-actions-preview-arrow) {
|
|
97
|
+
opacity: .5;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
:is(.preview-after-pYlSUr, .byline-form-actions-preview-after) {
|
|
101
|
+
font-family: var(--font-mono, monospace);
|
|
102
|
+
font-weight: 500;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
:is(.copy-row-EGqeNw, .byline-form-actions-copy-row) {
|
|
106
|
+
flex-wrap: wrap;
|
|
107
|
+
align-items: center;
|
|
108
|
+
display: flex;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
:is(.copy-source-IV71b6, .byline-form-actions-copy-source) {
|
|
112
|
+
font-family: var(--font-mono, monospace);
|
|
113
|
+
}
|
|
114
|
+
|
|
63
115
|
:is(:is([data-theme="dark"], .dark) .delete-xmSRLN, :is([data-theme="dark"], .dark) .byline-form-actions-delete) {
|
|
64
116
|
color: var(--red-400);
|
|
65
117
|
}
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { type ReactNode } from 'react';
|
|
9
9
|
import type { CollectionAdminConfig, Field, WorkflowStatus } from '@byline/core';
|
|
10
|
+
import { type DocumentActionsLocaleOption } from './document-actions';
|
|
10
11
|
import type { UseNavigationGuard } from './navigation-guard';
|
|
11
12
|
/** Metadata about a previously published version that is still live. */
|
|
12
13
|
export interface PublishedVersionInfo {
|
|
@@ -25,6 +26,30 @@ export interface FormRendererProps {
|
|
|
25
26
|
onStatusChange?: (nextStatus: string) => Promise<void>;
|
|
26
27
|
onUnpublish?: () => Promise<void>;
|
|
27
28
|
onDelete?: () => Promise<void>;
|
|
29
|
+
/**
|
|
30
|
+
* Called when the editor confirms the duplicate modal in
|
|
31
|
+
* `DocumentActions`. Edit views provide a handler that invokes the
|
|
32
|
+
* `duplicateCollectionDocument` server fn and navigates to the new doc.
|
|
33
|
+
* When omitted, the Duplicate menu item is hidden.
|
|
34
|
+
*/
|
|
35
|
+
onDuplicate?: () => Promise<void>;
|
|
36
|
+
/**
|
|
37
|
+
* Called when the editor confirms the Copy-to-Locale modal in
|
|
38
|
+
* `DocumentActions`. Edit views provide a handler that invokes the
|
|
39
|
+
* `copyDocumentToLocale` server fn and navigates to the target-locale
|
|
40
|
+
* view. When omitted (or when fewer than two `contentLocales` are
|
|
41
|
+
* configured), the Copy-to-Locale menu item is hidden.
|
|
42
|
+
*/
|
|
43
|
+
onCopyToLocale?: (args: {
|
|
44
|
+
targetLocale: string;
|
|
45
|
+
overwrite: boolean;
|
|
46
|
+
}) => Promise<void>;
|
|
47
|
+
/**
|
|
48
|
+
* All configured content locales (code + display label) — required for
|
|
49
|
+
* the Copy-to-Locale modal's target Select. Threaded as an opaque list
|
|
50
|
+
* through to `DocumentActions`.
|
|
51
|
+
*/
|
|
52
|
+
contentLocales?: ReadonlyArray<DocumentActionsLocaleOption>;
|
|
28
53
|
nextStatus?: WorkflowStatus;
|
|
29
54
|
workflowStatuses?: WorkflowStatus[];
|
|
30
55
|
publishedVersion?: PublishedVersionInfo | null;
|
|
@@ -70,4 +95,4 @@ export interface FormRendererProps {
|
|
|
70
95
|
*/
|
|
71
96
|
useNavigationGuard?: UseNavigationGuard;
|
|
72
97
|
}
|
|
73
|
-
export declare const FormRenderer: ({ mode, fields, onSubmit, onCancel, onStatusChange, onUnpublish, onDelete, nextStatus, workflowStatuses, publishedVersion, initialData, adminConfig, useAsTitle, useAsPath, headingLabel, headerSlot, collectionPath, initialLocale, onLocaleChange, defaultLocale, useNavigationGuard, restoreWarnings, }: FormRendererProps) => import("react").JSX.Element;
|
|
98
|
+
export declare const FormRenderer: ({ mode, fields, onSubmit, onCancel, onStatusChange, onUnpublish, onDelete, onDuplicate, onCopyToLocale, contentLocales, nextStatus, workflowStatuses, publishedVersion, initialData, adminConfig, useAsTitle, useAsPath, headingLabel, headerSlot, collectionPath, initialLocale, onLocaleChange, defaultLocale, useNavigationGuard, restoreWarnings, }: FormRendererProps) => import("react").JSX.Element;
|
|
@@ -139,7 +139,7 @@ function computeStatusTransitions(currentStatus, workflowStatuses, nextStatus) {
|
|
|
139
139
|
secondaryStatuses
|
|
140
140
|
};
|
|
141
141
|
}
|
|
142
|
-
const FormContent = ({ mode, fields, onSubmit, onCancel, onStatusChange, onUnpublish, onDelete, nextStatus, workflowStatuses, publishedVersion, initialData, adminConfig, useAsTitle, useAsPath, headingLabel, headerSlot, collectionPath, initialLocale, onLocaleChange, defaultLocale = 'en', useNavigationGuard: useNavigationGuardProp, restoreWarnings, _activeTabBySet, _onTabChange })=>{
|
|
142
|
+
const FormContent = ({ mode, fields, onSubmit, onCancel, onStatusChange, onUnpublish, onDelete, onDuplicate, onCopyToLocale, contentLocales, nextStatus, workflowStatuses, publishedVersion, initialData, adminConfig, useAsTitle, useAsPath, headingLabel, headerSlot, collectionPath, initialLocale, onLocaleChange, defaultLocale = 'en', useNavigationGuard: useNavigationGuardProp, restoreWarnings, _activeTabBySet, _onTabChange })=>{
|
|
143
143
|
const { getFieldValues, runFieldHooks, validateForm, errors: initialErrors, hasChanges: hasChangesFn, resetHasChanges, getPatches, getSystemPath, subscribeErrors, subscribeMeta, setFieldValue, setFieldError, getPendingUploads, clearPendingUploads } = useFormContext();
|
|
144
144
|
const [errors, setErrors] = useState(initialErrors);
|
|
145
145
|
const [hasChanges, setHasChanges] = useState(hasChangesFn());
|
|
@@ -445,7 +445,12 @@ const FormContent = ({ mode, fields, onSubmit, onCancel, onStatusChange, onUnpub
|
|
|
445
445
|
/*#__PURE__*/ jsx(DocumentActions, {
|
|
446
446
|
publishedVersion: publishedVersion,
|
|
447
447
|
onUnpublish: onUnpublish,
|
|
448
|
-
onDelete: onDelete
|
|
448
|
+
onDelete: onDelete,
|
|
449
|
+
onDuplicate: onDuplicate,
|
|
450
|
+
sourceTitle: null != useAsTitle && null != initialData ? initialData[useAsTitle] : null,
|
|
451
|
+
onCopyToLocale: onCopyToLocale,
|
|
452
|
+
sourceLocale: contentLocale,
|
|
453
|
+
contentLocales: contentLocales
|
|
449
454
|
})
|
|
450
455
|
]
|
|
451
456
|
})
|
|
@@ -542,7 +547,7 @@ const FormContent = ({ mode, fields, onSubmit, onCancel, onStatusChange, onUnpub
|
|
|
542
547
|
]
|
|
543
548
|
});
|
|
544
549
|
};
|
|
545
|
-
const FormRenderer = ({ mode, fields, onSubmit, onCancel, onStatusChange, onUnpublish, onDelete, nextStatus, workflowStatuses, publishedVersion, initialData, adminConfig, useAsTitle, useAsPath, headingLabel, headerSlot, collectionPath, initialLocale, onLocaleChange, defaultLocale, useNavigationGuard, restoreWarnings })=>{
|
|
550
|
+
const FormRenderer = ({ mode, fields, onSubmit, onCancel, onStatusChange, onUnpublish, onDelete, onDuplicate, onCopyToLocale, contentLocales, nextStatus, workflowStatuses, publishedVersion, initialData, adminConfig, useAsTitle, useAsPath, headingLabel, headerSlot, collectionPath, initialLocale, onLocaleChange, defaultLocale, useNavigationGuard, restoreWarnings })=>{
|
|
546
551
|
const savedTabsRef = useRef({});
|
|
547
552
|
return /*#__PURE__*/ jsx(FormProvider, {
|
|
548
553
|
initialData: initialData,
|
|
@@ -554,6 +559,9 @@ const FormRenderer = ({ mode, fields, onSubmit, onCancel, onStatusChange, onUnpu
|
|
|
554
559
|
onStatusChange: onStatusChange,
|
|
555
560
|
onUnpublish: onUnpublish,
|
|
556
561
|
onDelete: onDelete,
|
|
562
|
+
onDuplicate: onDuplicate,
|
|
563
|
+
onCopyToLocale: onCopyToLocale,
|
|
564
|
+
contentLocales: contentLocales,
|
|
557
565
|
nextStatus: nextStatus,
|
|
558
566
|
workflowStatuses: workflowStatuses,
|
|
559
567
|
publishedVersion: publishedVersion,
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"private": false,
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MPL-2.0",
|
|
6
|
-
"version": "1.
|
|
6
|
+
"version": "1.10.0",
|
|
7
7
|
"engines": {
|
|
8
8
|
"node": ">=20.9.0"
|
|
9
9
|
},
|
|
@@ -65,9 +65,9 @@
|
|
|
65
65
|
"react-diff-viewer-continued": "^4.2.2",
|
|
66
66
|
"zod": "^4.4.2",
|
|
67
67
|
"zod-form-data": "^3.0.1",
|
|
68
|
-
"@byline/
|
|
69
|
-
"@byline/
|
|
70
|
-
"@byline/
|
|
68
|
+
"@byline/client": "1.10.0",
|
|
69
|
+
"@byline/core": "1.10.0",
|
|
70
|
+
"@byline/admin": "1.10.0"
|
|
71
71
|
},
|
|
72
72
|
"peerDependencies": {
|
|
73
73
|
"react": "^19.0.0",
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
|
|
22
22
|
.menu,
|
|
23
23
|
:global(.byline-form-actions-menu) {
|
|
24
|
-
min-width:
|
|
24
|
+
min-width: 140px;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
.item,
|
|
@@ -84,6 +84,72 @@
|
|
|
84
84
|
border: 0;
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
+
.list,
|
|
88
|
+
:global(.byline-form-actions-list) {
|
|
89
|
+
margin: var(--spacing-8) 0;
|
|
90
|
+
padding-left: 1.25rem;
|
|
91
|
+
font-size: var(--font-size-sm);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.preview,
|
|
95
|
+
:global(.byline-form-actions-preview) {
|
|
96
|
+
margin-top: var(--spacing-16);
|
|
97
|
+
padding: var(--spacing-8) var(--spacing-12);
|
|
98
|
+
background: var(--surface-2, rgba(0, 0, 0, 0.03));
|
|
99
|
+
border: 1px solid var(--border-subtle, rgba(0, 0, 0, 0.08));
|
|
100
|
+
border-radius: var(--radius-md, 4px);
|
|
101
|
+
font-size: var(--font-size-sm);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.preview-label,
|
|
105
|
+
:global(.byline-form-actions-preview-label) {
|
|
106
|
+
font-weight: 500;
|
|
107
|
+
margin-bottom: var(--spacing-4);
|
|
108
|
+
opacity: 0.75;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.preview-row,
|
|
112
|
+
:global(.byline-form-actions-preview-row) {
|
|
113
|
+
display: flex;
|
|
114
|
+
align-items: center;
|
|
115
|
+
gap: var(--spacing-8);
|
|
116
|
+
flex-wrap: wrap;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.preview-before,
|
|
120
|
+
:global(.byline-form-actions-preview-before) {
|
|
121
|
+
font-family: var(--font-mono, monospace);
|
|
122
|
+
opacity: 0.7;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.preview-arrow,
|
|
126
|
+
:global(.byline-form-actions-preview-arrow) {
|
|
127
|
+
opacity: 0.5;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.preview-after,
|
|
131
|
+
:global(.byline-form-actions-preview-after) {
|
|
132
|
+
font-family: var(--font-mono, monospace);
|
|
133
|
+
font-weight: 500;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
.copy-row,
|
|
137
|
+
:global(.byline-form-actions-copy-row) {
|
|
138
|
+
display: flex;
|
|
139
|
+
align-items: center;
|
|
140
|
+
flex-wrap: wrap;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.copy-label,
|
|
144
|
+
:global(.byline-form-actions-copy-label) {
|
|
145
|
+
/* Layout-only handle; spacing is set inline alongside the field. */
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
.copy-source,
|
|
149
|
+
:global(.byline-form-actions-copy-source) {
|
|
150
|
+
font-family: var(--font-mono, monospace);
|
|
151
|
+
}
|
|
152
|
+
|
|
87
153
|
/* ─── Dark theme variants ───────────────────────────────────── */
|
|
88
154
|
|
|
89
155
|
:is([data-theme="dark"], :global(.dark)) {
|
|
@@ -14,26 +14,89 @@ import cx from 'classnames'
|
|
|
14
14
|
|
|
15
15
|
import {
|
|
16
16
|
Button,
|
|
17
|
+
Checkbox,
|
|
17
18
|
CloseIcon,
|
|
18
19
|
DeleteIcon,
|
|
19
20
|
Dropdown as DropdownComponent,
|
|
20
21
|
EllipsisIcon,
|
|
21
22
|
IconButton,
|
|
22
23
|
Modal,
|
|
24
|
+
Select,
|
|
23
25
|
} from '../uikit.js'
|
|
24
26
|
import styles from './document-actions.module.css'
|
|
25
27
|
import type { PublishedVersionInfo } from './form-renderer'
|
|
26
28
|
|
|
29
|
+
const DUPLICATE_TITLE_SUFFIX = ' (copy)'
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Shape of a content-locale option as consumed by the Copy-to-Locale
|
|
33
|
+
* modal. Matches the host adapter's `ContentLocaleOption`; declared
|
|
34
|
+
* locally so this package does not take a dependency on host code.
|
|
35
|
+
*/
|
|
36
|
+
export interface DocumentActionsLocaleOption {
|
|
37
|
+
code: string
|
|
38
|
+
label: string
|
|
39
|
+
}
|
|
40
|
+
|
|
27
41
|
export function DocumentActions({
|
|
28
42
|
publishedVersion,
|
|
29
43
|
onUnpublish,
|
|
30
44
|
onDelete,
|
|
45
|
+
onDuplicate,
|
|
46
|
+
sourceTitle,
|
|
47
|
+
onCopyToLocale,
|
|
48
|
+
sourceLocale,
|
|
49
|
+
contentLocales,
|
|
31
50
|
}: {
|
|
32
51
|
publishedVersion?: PublishedVersionInfo | null
|
|
33
52
|
onUnpublish?: () => Promise<void>
|
|
34
53
|
onDelete?: () => Promise<void>
|
|
54
|
+
/**
|
|
55
|
+
* Called when the editor confirms the duplicate modal. The parent runs
|
|
56
|
+
* the server fn, surfaces a toast, and navigates to the new document.
|
|
57
|
+
*/
|
|
58
|
+
onDuplicate?: () => Promise<void>
|
|
59
|
+
/**
|
|
60
|
+
* The current (saved) value of the source document's `useAsTitle`
|
|
61
|
+
* field, used to render the suffix preview inside the duplicate modal.
|
|
62
|
+
* Sourced from the form's `initialData`, not live form state, so the
|
|
63
|
+
* preview reflects what will actually be duplicated.
|
|
64
|
+
*/
|
|
65
|
+
sourceTitle?: string | null
|
|
66
|
+
/**
|
|
67
|
+
* Called when the editor confirms the Copy-to-Locale modal. The
|
|
68
|
+
* parent runs the server fn, surfaces a toast, and navigates to the
|
|
69
|
+
* target locale view. Menu item is hidden when omitted, or when fewer
|
|
70
|
+
* than two content locales are configured.
|
|
71
|
+
*/
|
|
72
|
+
onCopyToLocale?: (args: { targetLocale: string; overwrite: boolean }) => Promise<void>
|
|
73
|
+
/**
|
|
74
|
+
* The locale the form is currently displaying. Used as the read-only
|
|
75
|
+
* "From" label in the Copy-to-Locale modal and excluded from the
|
|
76
|
+
* target Select.
|
|
77
|
+
*/
|
|
78
|
+
sourceLocale?: string
|
|
79
|
+
/**
|
|
80
|
+
* All configured content locales (code + display label). The
|
|
81
|
+
* Copy-to-Locale Select lists every locale except `sourceLocale`.
|
|
82
|
+
*/
|
|
83
|
+
contentLocales?: ReadonlyArray<DocumentActionsLocaleOption>
|
|
35
84
|
}) {
|
|
36
85
|
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false)
|
|
86
|
+
const [showDuplicateConfirm, setShowDuplicateConfirm] = useState(false)
|
|
87
|
+
const [duplicateBusy, setDuplicateBusy] = useState(false)
|
|
88
|
+
|
|
89
|
+
// Copy-to-Locale modal state. The menu item is hidden entirely unless
|
|
90
|
+
// the host has supplied a handler AND there is at least one *other*
|
|
91
|
+
// locale to copy into.
|
|
92
|
+
const availableTargetLocales = (contentLocales ?? []).filter((loc) => loc.code !== sourceLocale)
|
|
93
|
+
const copyToLocaleAvailable = onCopyToLocale != null && availableTargetLocales.length > 0
|
|
94
|
+
const [showCopyToLocaleConfirm, setShowCopyToLocaleConfirm] = useState(false)
|
|
95
|
+
const [copyToLocaleBusy, setCopyToLocaleBusy] = useState(false)
|
|
96
|
+
const [copyTargetLocale, setCopyTargetLocale] = useState<string>(
|
|
97
|
+
availableTargetLocales[0]?.code ?? ''
|
|
98
|
+
)
|
|
99
|
+
const [copyOverwrite, setCopyOverwrite] = useState(false)
|
|
37
100
|
|
|
38
101
|
const handleOnDelete = () => {
|
|
39
102
|
setShowDeleteConfirm(false)
|
|
@@ -42,6 +105,45 @@ export function DocumentActions({
|
|
|
42
105
|
}
|
|
43
106
|
}
|
|
44
107
|
|
|
108
|
+
const handleOnDuplicate = async () => {
|
|
109
|
+
if (!onDuplicate) return
|
|
110
|
+
setDuplicateBusy(true)
|
|
111
|
+
try {
|
|
112
|
+
await onDuplicate()
|
|
113
|
+
setShowDuplicateConfirm(false)
|
|
114
|
+
} finally {
|
|
115
|
+
setDuplicateBusy(false)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const handleOpenCopyToLocale = () => {
|
|
120
|
+
// Reset on open: pick the first available target and clear the
|
|
121
|
+
// overwrite checkbox so a previous-session "overwrite=true" choice
|
|
122
|
+
// is not silently sticky.
|
|
123
|
+
setCopyTargetLocale(availableTargetLocales[0]?.code ?? '')
|
|
124
|
+
setCopyOverwrite(false)
|
|
125
|
+
setShowCopyToLocaleConfirm(true)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const handleOnCopyToLocale = async () => {
|
|
129
|
+
if (!onCopyToLocale || !copyTargetLocale) return
|
|
130
|
+
setCopyToLocaleBusy(true)
|
|
131
|
+
try {
|
|
132
|
+
await onCopyToLocale({ targetLocale: copyTargetLocale, overwrite: copyOverwrite })
|
|
133
|
+
setShowCopyToLocaleConfirm(false)
|
|
134
|
+
} finally {
|
|
135
|
+
setCopyToLocaleBusy(false)
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Preview text shown inside the modal. Falls back to the literal suffix
|
|
140
|
+
// when no source title is supplied (collections without `useAsTitle`).
|
|
141
|
+
const duplicatePreviewBefore = sourceTitle ?? ''
|
|
142
|
+
const duplicatePreviewAfter = (sourceTitle ?? '') + DUPLICATE_TITLE_SUFFIX
|
|
143
|
+
|
|
144
|
+
const sourceLocaleLabel =
|
|
145
|
+
contentLocales?.find((loc) => loc.code === sourceLocale)?.label ?? sourceLocale ?? ''
|
|
146
|
+
|
|
45
147
|
return (
|
|
46
148
|
<>
|
|
47
149
|
<DropdownComponent.Root>
|
|
@@ -62,7 +164,7 @@ export function DocumentActions({
|
|
|
62
164
|
data-side="top"
|
|
63
165
|
sideOffset={10}
|
|
64
166
|
>
|
|
65
|
-
{publishedVersion && (
|
|
167
|
+
{/*{publishedVersion && (
|
|
66
168
|
<>
|
|
67
169
|
<DropdownComponent.Item onClick={onUnpublish}>
|
|
68
170
|
<div className={cx('byline-form-actions-item', styles.item)}>
|
|
@@ -74,7 +176,30 @@ export function DocumentActions({
|
|
|
74
176
|
</DropdownComponent.Item>
|
|
75
177
|
<DropdownComponent.Separator />
|
|
76
178
|
</>
|
|
179
|
+
)}*/}
|
|
180
|
+
{copyToLocaleAvailable && (
|
|
181
|
+
<DropdownComponent.Item onClick={handleOpenCopyToLocale}>
|
|
182
|
+
<div className={cx('byline-form-actions-item', styles.item)}>
|
|
183
|
+
<span className={cx('byline-form-actions-item-text', styles['item-text'])}>
|
|
184
|
+
<button type="button">Copy to Locale</button>
|
|
185
|
+
</span>
|
|
186
|
+
</div>
|
|
187
|
+
</DropdownComponent.Item>
|
|
188
|
+
)}
|
|
189
|
+
{onDuplicate && (
|
|
190
|
+
<DropdownComponent.Item
|
|
191
|
+
onClick={() => {
|
|
192
|
+
setShowDuplicateConfirm(true)
|
|
193
|
+
}}
|
|
194
|
+
>
|
|
195
|
+
<div className={cx('byline-form-actions-item', styles.item)}>
|
|
196
|
+
<span className={cx('byline-form-actions-item-text', styles['item-text'])}>
|
|
197
|
+
<button type="button">Duplicate</button>
|
|
198
|
+
</span>
|
|
199
|
+
</div>
|
|
200
|
+
</DropdownComponent.Item>
|
|
77
201
|
)}
|
|
202
|
+
<DropdownComponent.Separator />
|
|
78
203
|
<DropdownComponent.Item
|
|
79
204
|
onClick={() => {
|
|
80
205
|
setShowDeleteConfirm(true)
|
|
@@ -133,6 +258,7 @@ export function DocumentActions({
|
|
|
133
258
|
</button>
|
|
134
259
|
<Button
|
|
135
260
|
size="sm"
|
|
261
|
+
style={{ minWidth: '80px' }}
|
|
136
262
|
intent="noeffect"
|
|
137
263
|
onClick={() => {
|
|
138
264
|
setShowDeleteConfirm(false)
|
|
@@ -140,12 +266,220 @@ export function DocumentActions({
|
|
|
140
266
|
>
|
|
141
267
|
Cancel
|
|
142
268
|
</Button>
|
|
143
|
-
<Button size="sm" intent="danger" onClick={handleOnDelete}>
|
|
269
|
+
<Button size="sm" style={{ minWidth: '80px' }} intent="danger" onClick={handleOnDelete}>
|
|
144
270
|
Delete
|
|
145
271
|
</Button>
|
|
146
272
|
</Modal.Actions>
|
|
147
273
|
</Modal.Container>
|
|
148
274
|
</Modal>
|
|
275
|
+
|
|
276
|
+
<Modal
|
|
277
|
+
isOpen={showDuplicateConfirm}
|
|
278
|
+
closeOnOverlayClick={!duplicateBusy}
|
|
279
|
+
onDismiss={() => {
|
|
280
|
+
if (!duplicateBusy) setShowDuplicateConfirm(false)
|
|
281
|
+
}}
|
|
282
|
+
>
|
|
283
|
+
<Modal.Container style={{ maxWidth: '560px' }}>
|
|
284
|
+
<Modal.Header className={cx('byline-form-actions-modal-head', styles['modal-head'])}>
|
|
285
|
+
<h3 className={cx('byline-form-actions-modal-title', styles['modal-title'])}>
|
|
286
|
+
Duplicate Document
|
|
287
|
+
</h3>
|
|
288
|
+
<IconButton
|
|
289
|
+
arial-label="Close"
|
|
290
|
+
size="xs"
|
|
291
|
+
onClick={() => {
|
|
292
|
+
if (!duplicateBusy) setShowDuplicateConfirm(false)
|
|
293
|
+
}}
|
|
294
|
+
>
|
|
295
|
+
<CloseIcon width="16px" height="16px" svgClassName="white-icon" />
|
|
296
|
+
</IconButton>
|
|
297
|
+
</Modal.Header>
|
|
298
|
+
<Modal.Content className="prose">
|
|
299
|
+
<p className="m-0">
|
|
300
|
+
A new document will be created with all translations (if any) cloned from this one.
|
|
301
|
+
After the duplicate is created you should:
|
|
302
|
+
</p>
|
|
303
|
+
<ul className={cx('byline-form-actions-list', styles.list)}>
|
|
304
|
+
<li>
|
|
305
|
+
Update the title of the docuiment (including any translated versions). The title is
|
|
306
|
+
currently suffixed with <code>{DUPLICATE_TITLE_SUFFIX.trim()}</code>.
|
|
307
|
+
</li>
|
|
308
|
+
<li>
|
|
309
|
+
Review the system path in the path widget — the auto-generated path will reflect the
|
|
310
|
+
suffixed title and is unlikely to be what you want long-term.
|
|
311
|
+
</li>
|
|
312
|
+
</ul>
|
|
313
|
+
{sourceTitle != null && sourceTitle.length > 0 && (
|
|
314
|
+
<div className={cx('byline-form-actions-preview', styles.preview)}>
|
|
315
|
+
<div className={cx('byline-form-actions-preview-label', styles['preview-label'])}>
|
|
316
|
+
Preview (current locale):
|
|
317
|
+
</div>
|
|
318
|
+
<div className={cx('byline-form-actions-preview-row', styles['preview-row'])}>
|
|
319
|
+
<span
|
|
320
|
+
className={cx('byline-form-actions-preview-before', styles['preview-before'])}
|
|
321
|
+
>
|
|
322
|
+
{duplicatePreviewBefore}
|
|
323
|
+
</span>
|
|
324
|
+
<span
|
|
325
|
+
className={cx('byline-form-actions-preview-arrow', styles['preview-arrow'])}
|
|
326
|
+
>
|
|
327
|
+
→
|
|
328
|
+
</span>
|
|
329
|
+
<span
|
|
330
|
+
className={cx('byline-form-actions-preview-after', styles['preview-after'])}
|
|
331
|
+
>
|
|
332
|
+
{duplicatePreviewAfter}
|
|
333
|
+
</span>
|
|
334
|
+
</div>
|
|
335
|
+
</div>
|
|
336
|
+
)}
|
|
337
|
+
</Modal.Content>
|
|
338
|
+
<Modal.Actions>
|
|
339
|
+
<button
|
|
340
|
+
data-autofocus
|
|
341
|
+
type="button"
|
|
342
|
+
tabIndex={0}
|
|
343
|
+
className={cx('byline-form-actions-sr-only', styles['sr-only'])}
|
|
344
|
+
>
|
|
345
|
+
no action
|
|
346
|
+
</button>
|
|
347
|
+
<Button
|
|
348
|
+
size="sm"
|
|
349
|
+
style={{ minWidth: '80px' }}
|
|
350
|
+
intent="noeffect"
|
|
351
|
+
onClick={() => {
|
|
352
|
+
if (!duplicateBusy) setShowDuplicateConfirm(false)
|
|
353
|
+
}}
|
|
354
|
+
disabled={duplicateBusy}
|
|
355
|
+
>
|
|
356
|
+
Cancel
|
|
357
|
+
</Button>
|
|
358
|
+
<Button
|
|
359
|
+
size="sm"
|
|
360
|
+
style={{ minWidth: '80px' }}
|
|
361
|
+
intent="primary"
|
|
362
|
+
onClick={handleOnDuplicate}
|
|
363
|
+
disabled={duplicateBusy}
|
|
364
|
+
>
|
|
365
|
+
{duplicateBusy ? 'Duplicating...' : 'Duplicate'}
|
|
366
|
+
</Button>
|
|
367
|
+
</Modal.Actions>
|
|
368
|
+
</Modal.Container>
|
|
369
|
+
</Modal>
|
|
370
|
+
|
|
371
|
+
<Modal
|
|
372
|
+
isOpen={showCopyToLocaleConfirm}
|
|
373
|
+
closeOnOverlayClick={!copyToLocaleBusy}
|
|
374
|
+
onDismiss={() => {
|
|
375
|
+
if (!copyToLocaleBusy) setShowCopyToLocaleConfirm(false)
|
|
376
|
+
}}
|
|
377
|
+
>
|
|
378
|
+
<Modal.Container style={{ maxWidth: '560px' }}>
|
|
379
|
+
<Modal.Header className={cx('byline-form-actions-modal-head', styles['modal-head'])}>
|
|
380
|
+
<h3 className={cx('byline-form-actions-modal-title', styles['modal-title'])}>
|
|
381
|
+
Copy to Locale
|
|
382
|
+
</h3>
|
|
383
|
+
<IconButton
|
|
384
|
+
arial-label="Close"
|
|
385
|
+
size="xs"
|
|
386
|
+
onClick={() => {
|
|
387
|
+
if (!copyToLocaleBusy) setShowCopyToLocaleConfirm(false)
|
|
388
|
+
}}
|
|
389
|
+
>
|
|
390
|
+
<CloseIcon width="16px" height="16px" svgClassName="white-icon" />
|
|
391
|
+
</IconButton>
|
|
392
|
+
</Modal.Header>
|
|
393
|
+
<Modal.Content>
|
|
394
|
+
<p>
|
|
395
|
+
Copy this document's content from one locale to another. Non-localized fields are
|
|
396
|
+
shared across locales and will not change.
|
|
397
|
+
</p>
|
|
398
|
+
<div
|
|
399
|
+
className={cx('byline-form-actions-copy-row', styles['copy-row'])}
|
|
400
|
+
style={{ marginTop: 'var(--spacing-12)' }}
|
|
401
|
+
>
|
|
402
|
+
<span
|
|
403
|
+
className={cx('byline-form-actions-copy-label', styles['copy-label'])}
|
|
404
|
+
style={{ fontWeight: 500 }}
|
|
405
|
+
>
|
|
406
|
+
From:
|
|
407
|
+
</span>{' '}
|
|
408
|
+
<span className={cx('byline-form-actions-copy-source', styles['copy-source'])}>
|
|
409
|
+
{sourceLocaleLabel}
|
|
410
|
+
</span>
|
|
411
|
+
</div>
|
|
412
|
+
<div
|
|
413
|
+
className={cx('byline-form-actions-copy-row', styles['copy-row'])}
|
|
414
|
+
style={{ marginTop: 'var(--spacing-12)' }}
|
|
415
|
+
>
|
|
416
|
+
<span
|
|
417
|
+
className={cx('byline-form-actions-copy-label', styles['copy-label'])}
|
|
418
|
+
style={{ fontWeight: 500, marginRight: 'var(--spacing-8)' }}
|
|
419
|
+
>
|
|
420
|
+
To:
|
|
421
|
+
</span>
|
|
422
|
+
<Select<string>
|
|
423
|
+
size="sm"
|
|
424
|
+
ariaLabel="Target locale"
|
|
425
|
+
value={copyTargetLocale}
|
|
426
|
+
items={availableTargetLocales.map((loc) => ({
|
|
427
|
+
value: loc.code,
|
|
428
|
+
label: loc.label,
|
|
429
|
+
}))}
|
|
430
|
+
onValueChange={(value) => {
|
|
431
|
+
if (value != null) setCopyTargetLocale(value)
|
|
432
|
+
}}
|
|
433
|
+
disabled={copyToLocaleBusy}
|
|
434
|
+
/>
|
|
435
|
+
</div>
|
|
436
|
+
<div
|
|
437
|
+
className={cx('byline-form-actions-copy-row', styles['copy-row'])}
|
|
438
|
+
style={{ marginTop: 'var(--spacing-16)' }}
|
|
439
|
+
>
|
|
440
|
+
<Checkbox
|
|
441
|
+
id="copy-to-locale-overwrite"
|
|
442
|
+
name="overwrite"
|
|
443
|
+
label="Overwrite existing field data in target locale"
|
|
444
|
+
checked={copyOverwrite}
|
|
445
|
+
disabled={copyToLocaleBusy}
|
|
446
|
+
helpText="Unchecked: only fill in target fields that are currently empty. Checked: replace every translated field with the source's value."
|
|
447
|
+
onCheckedChange={(value) => {
|
|
448
|
+
setCopyOverwrite(value === true)
|
|
449
|
+
}}
|
|
450
|
+
/>
|
|
451
|
+
</div>
|
|
452
|
+
</Modal.Content>
|
|
453
|
+
<Modal.Actions>
|
|
454
|
+
<button
|
|
455
|
+
data-autofocus
|
|
456
|
+
type="button"
|
|
457
|
+
tabIndex={0}
|
|
458
|
+
className={cx('byline-form-actions-sr-only', styles['sr-only'])}
|
|
459
|
+
>
|
|
460
|
+
no action
|
|
461
|
+
</button>
|
|
462
|
+
<Button
|
|
463
|
+
size="sm"
|
|
464
|
+
intent="noeffect"
|
|
465
|
+
onClick={() => {
|
|
466
|
+
if (!copyToLocaleBusy) setShowCopyToLocaleConfirm(false)
|
|
467
|
+
}}
|
|
468
|
+
disabled={copyToLocaleBusy}
|
|
469
|
+
>
|
|
470
|
+
Cancel
|
|
471
|
+
</Button>
|
|
472
|
+
<Button
|
|
473
|
+
size="sm"
|
|
474
|
+
intent="primary"
|
|
475
|
+
onClick={handleOnCopyToLocale}
|
|
476
|
+
disabled={copyToLocaleBusy || !copyTargetLocale}
|
|
477
|
+
>
|
|
478
|
+
{copyToLocaleBusy ? 'Copying...' : 'Copy'}
|
|
479
|
+
</Button>
|
|
480
|
+
</Modal.Actions>
|
|
481
|
+
</Modal.Container>
|
|
482
|
+
</Modal>
|
|
149
483
|
</>
|
|
150
484
|
)
|
|
151
485
|
}
|
|
@@ -27,7 +27,7 @@ import { FieldRenderer } from '../fields/field-renderer'
|
|
|
27
27
|
import { LocalDateTime } from '../fields/local-date-time'
|
|
28
28
|
import { useBylineFieldServices } from '../services/field-services-context'
|
|
29
29
|
import { Alert, Button, ComboButton, Modal } from '../uikit.js'
|
|
30
|
-
import { DocumentActions } from './document-actions'
|
|
30
|
+
import { DocumentActions, type DocumentActionsLocaleOption } from './document-actions'
|
|
31
31
|
import { FormProvider, useFieldValue, useFormContext } from './form-context'
|
|
32
32
|
import styles from './form-renderer.module.css'
|
|
33
33
|
import { useNavigationGuardAdapter } from './navigation-guard'
|
|
@@ -53,6 +53,27 @@ export interface FormRendererProps {
|
|
|
53
53
|
onStatusChange?: (nextStatus: string) => Promise<void>
|
|
54
54
|
onUnpublish?: () => Promise<void>
|
|
55
55
|
onDelete?: () => Promise<void>
|
|
56
|
+
/**
|
|
57
|
+
* Called when the editor confirms the duplicate modal in
|
|
58
|
+
* `DocumentActions`. Edit views provide a handler that invokes the
|
|
59
|
+
* `duplicateCollectionDocument` server fn and navigates to the new doc.
|
|
60
|
+
* When omitted, the Duplicate menu item is hidden.
|
|
61
|
+
*/
|
|
62
|
+
onDuplicate?: () => Promise<void>
|
|
63
|
+
/**
|
|
64
|
+
* Called when the editor confirms the Copy-to-Locale modal in
|
|
65
|
+
* `DocumentActions`. Edit views provide a handler that invokes the
|
|
66
|
+
* `copyDocumentToLocale` server fn and navigates to the target-locale
|
|
67
|
+
* view. When omitted (or when fewer than two `contentLocales` are
|
|
68
|
+
* configured), the Copy-to-Locale menu item is hidden.
|
|
69
|
+
*/
|
|
70
|
+
onCopyToLocale?: (args: { targetLocale: string; overwrite: boolean }) => Promise<void>
|
|
71
|
+
/**
|
|
72
|
+
* All configured content locales (code + display label) — required for
|
|
73
|
+
* the Copy-to-Locale modal's target Select. Threaded as an opaque list
|
|
74
|
+
* through to `DocumentActions`.
|
|
75
|
+
*/
|
|
76
|
+
contentLocales?: ReadonlyArray<DocumentActionsLocaleOption>
|
|
56
77
|
nextStatus?: WorkflowStatus
|
|
57
78
|
workflowStatuses?: WorkflowStatus[]
|
|
58
79
|
publishedVersion?: PublishedVersionInfo | null
|
|
@@ -254,6 +275,9 @@ const FormContent = ({
|
|
|
254
275
|
onStatusChange,
|
|
255
276
|
onUnpublish,
|
|
256
277
|
onDelete,
|
|
278
|
+
onDuplicate,
|
|
279
|
+
onCopyToLocale,
|
|
280
|
+
contentLocales,
|
|
257
281
|
nextStatus,
|
|
258
282
|
workflowStatuses,
|
|
259
283
|
publishedVersion,
|
|
@@ -684,6 +708,18 @@ const FormContent = ({
|
|
|
684
708
|
publishedVersion={publishedVersion}
|
|
685
709
|
onUnpublish={onUnpublish}
|
|
686
710
|
onDelete={onDelete}
|
|
711
|
+
onDuplicate={onDuplicate}
|
|
712
|
+
sourceTitle={
|
|
713
|
+
useAsTitle != null && initialData != null
|
|
714
|
+
? ((initialData as Record<string, unknown>)[useAsTitle] as
|
|
715
|
+
| string
|
|
716
|
+
| null
|
|
717
|
+
| undefined)
|
|
718
|
+
: null
|
|
719
|
+
}
|
|
720
|
+
onCopyToLocale={onCopyToLocale}
|
|
721
|
+
sourceLocale={contentLocale}
|
|
722
|
+
contentLocales={contentLocales}
|
|
687
723
|
/>
|
|
688
724
|
</div>
|
|
689
725
|
</div>
|
|
@@ -767,6 +803,9 @@ export const FormRenderer = ({
|
|
|
767
803
|
onStatusChange,
|
|
768
804
|
onUnpublish,
|
|
769
805
|
onDelete,
|
|
806
|
+
onDuplicate,
|
|
807
|
+
onCopyToLocale,
|
|
808
|
+
contentLocales,
|
|
770
809
|
nextStatus,
|
|
771
810
|
workflowStatuses,
|
|
772
811
|
publishedVersion,
|
|
@@ -800,6 +839,9 @@ export const FormRenderer = ({
|
|
|
800
839
|
onStatusChange={onStatusChange}
|
|
801
840
|
onUnpublish={onUnpublish}
|
|
802
841
|
onDelete={onDelete}
|
|
842
|
+
onDuplicate={onDuplicate}
|
|
843
|
+
onCopyToLocale={onCopyToLocale}
|
|
844
|
+
contentLocales={contentLocales}
|
|
803
845
|
nextStatus={nextStatus}
|
|
804
846
|
workflowStatuses={workflowStatuses}
|
|
805
847
|
publishedVersion={publishedVersion}
|