@byline/admin 6.6.0 → 6.7.1
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/exports-parity.test.node.js +37 -0
- package/dist/fields/array/array-field.jsx +209 -0
- package/dist/fields/blocks/blocks-field.jsx +248 -0
- package/dist/fields/checkbox/checkbox-field.jsx +24 -0
- package/dist/fields/code/code-editor.jsx +211 -0
- package/dist/fields/code/code-field.jsx +109 -0
- package/dist/fields/column-formatter.jsx +25 -0
- package/dist/fields/date-time-formatter.jsx +18 -0
- package/dist/fields/datetime/datetime-field.jsx +22 -0
- package/dist/fields/draggable-context-menu.jsx +50 -0
- package/dist/fields/field-admin.test.node.js +40 -0
- package/dist/fields/field-renderer.jsx +136 -0
- package/dist/fields/field-services-context.jsx +17 -0
- package/dist/fields/file/file-field.jsx +176 -0
- package/dist/fields/file/file-upload-field.jsx +89 -0
- package/dist/fields/group/group-field.jsx +46 -0
- package/dist/fields/image/image-field.jsx +183 -0
- package/dist/fields/image/image-upload-field.jsx +157 -0
- package/dist/fields/local-date-time.jsx +69 -0
- package/dist/fields/locale-badge.jsx +19 -0
- package/dist/fields/numerical/numerical-field.jsx +93 -0
- package/dist/fields/relation/relation-column-formatter.jsx +74 -0
- package/dist/fields/relation/relation-display.jsx +118 -0
- package/dist/fields/relation/relation-field.jsx +108 -0
- package/dist/fields/relation/relation-many-field.jsx +142 -0
- package/dist/fields/relation/relation-picker.jsx +240 -0
- package/dist/fields/relation/relation-summary.jsx +45 -0
- package/dist/fields/select/select-field.jsx +27 -0
- package/dist/fields/sortable-item.jsx +54 -0
- package/dist/fields/text/text-field.jsx +79 -0
- package/dist/fields/text-area/text-area-field.jsx +79 -0
- package/dist/forms/available-locales-reconcile.test.node.js +43 -0
- package/dist/forms/available-locales-widget.d.ts +7 -1
- package/dist/forms/available-locales-widget.js +8 -3
- package/dist/forms/available-locales-widget.jsx +63 -0
- package/dist/forms/document-actions.jsx +486 -0
- package/dist/forms/form-context.d.ts +3 -2
- package/dist/forms/form-context.jsx +598 -0
- package/dist/forms/form-modals.d.ts +5 -1
- package/dist/forms/form-modals.js +4 -0
- package/dist/forms/form-modals.jsx +128 -0
- package/dist/forms/form-page-chrome.js +2 -1
- package/dist/forms/form-renderer.jsx +532 -0
- package/dist/forms/form-status-display.jsx +69 -0
- package/dist/forms/navigation-guard.jsx +68 -0
- package/dist/forms/nested-path.test.node.js +115 -0
- package/dist/forms/path-widget.jsx +125 -0
- package/dist/forms/pending-uploads.test.node.js +19 -0
- package/dist/forms/repeating-items.test.node.js +29 -0
- package/dist/forms/scheduled-publication-control.jsx +389 -0
- package/dist/forms/scheduled-publication-state.test.node.js +121 -0
- package/dist/forms/scheduled-publication-time.test.node.js +70 -0
- package/dist/forms/status-transitions.test.node.js +76 -0
- package/dist/forms/tree-placement-widget.jsx +160 -0
- package/dist/forms/upload-executor.test.node.js +368 -0
- package/dist/forms/use-form-layout.test.node.js +69 -0
- package/dist/modules/admin-account/components/change-password.jsx +169 -0
- package/dist/modules/admin-account/components/container.jsx +164 -0
- package/dist/modules/admin-account/components/preferences.jsx +148 -0
- package/dist/modules/admin-account/components/theme-switch.jsx +52 -0
- package/dist/modules/admin-account/components/update.jsx +186 -0
- package/dist/modules/admin-permissions/components/inspector.jsx +200 -0
- package/dist/modules/admin-preferences/schemas.test.node.js +48 -0
- package/dist/modules/admin-roles/components/create.jsx +155 -0
- package/dist/modules/admin-roles/components/permissions.jsx +239 -0
- package/dist/modules/admin-roles/components/update.jsx +148 -0
- package/dist/modules/admin-users/components/create.jsx +206 -0
- package/dist/modules/admin-users/components/roles.jsx +126 -0
- package/dist/modules/admin-users/components/set-password.jsx +133 -0
- package/dist/modules/admin-users/components/update.jsx +215 -0
- package/dist/modules/analytics/components/dashboard.jsx +208 -0
- package/dist/modules/analytics/components/dashboard.test.node.js +162 -0
- package/dist/modules/analytics/components/timeseries.jsx +193 -0
- package/dist/modules/auth/components/sign-in-form.jsx +89 -0
- package/dist/modules/auth/safe-redirect.test.node.js +40 -0
- package/dist/modules/auth/sign-in-form-props.test.node.js +7 -0
- package/dist/presentation/group.jsx +26 -0
- package/dist/presentation/row.jsx +23 -0
- package/dist/presentation/tabs.jsx +39 -0
- package/dist/services/admin-services-context.jsx +17 -0
- package/dist/widgets/diff-viewer/diff-modal.jsx +110 -0
- package/dist/widgets/source-locale-badge/source-locale-badge.jsx +28 -0
- package/dist/widgets/status-badge/status-badge.jsx +46 -0
- package/package.json +7 -7
- package/src/forms/available-locales-widget.tsx +15 -1
- package/src/forms/form-context.tsx +3 -2
- package/src/forms/form-modals.tsx +12 -1
- package/src/forms/form-page-chrome.tsx +1 -0
- package/src/forms/locale-visibility-copy.test.tsx +180 -0
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
/**
|
|
3
|
+
* This Source Code is subject to the terms of the Mozilla Public
|
|
4
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
5
|
+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
6
|
+
*
|
|
7
|
+
* Copyright (c) Infonomic Company Limited
|
|
8
|
+
*/
|
|
9
|
+
import { useCallback, useMemo } from 'react';
|
|
10
|
+
import { useTranslation } from '@byline/i18n/react';
|
|
11
|
+
import { Checkbox, Label } from '@byline/ui/react';
|
|
12
|
+
import cx from 'clsx';
|
|
13
|
+
import { reconcileLocaleState } from './available-locales-reconcile';
|
|
14
|
+
import styles from './available-locales-widget.module.css';
|
|
15
|
+
import { useFormContext, useSystemAvailableLocales } from './form-context';
|
|
16
|
+
export { reconcileLocaleState } from './available-locales-reconcile';
|
|
17
|
+
/**
|
|
18
|
+
* System-managed `availableLocales` widget — the editorial "advertise these
|
|
19
|
+
* content locales" control. Renders one checkbox per content locale in the
|
|
20
|
+
* sidebar, directly below the path widget. The checked state reflects the
|
|
21
|
+
* stored advertised set (`useSystemAvailableLocales`); each row's intent
|
|
22
|
+
* reconciles that against the structural ledger fact (`availableVersionLocales`)
|
|
23
|
+
* via {@link reconcileLocaleState}. Opt-in: nothing is advertised until the
|
|
24
|
+
* editor checks a (green) locale.
|
|
25
|
+
*
|
|
26
|
+
* Stable override handles: `.byline-form-available-locales`,
|
|
27
|
+
* `.byline-form-available-locales-list`.
|
|
28
|
+
*/
|
|
29
|
+
export const AvailableLocalesWidget = ({ disabled: mutationsBlocked = false, contentLocales, availableVersionLocales, }) => {
|
|
30
|
+
const { t } = useTranslation('byline-admin');
|
|
31
|
+
const { setSystemAvailableLocales } = useFormContext();
|
|
32
|
+
const advertised = useSystemAvailableLocales();
|
|
33
|
+
const advertisedSet = useMemo(() => new Set(advertised), [advertised]);
|
|
34
|
+
const ledgerSet = useMemo(() => new Set(availableVersionLocales), [availableVersionLocales]);
|
|
35
|
+
const toggle = useCallback((code, checked) => {
|
|
36
|
+
if (mutationsBlocked)
|
|
37
|
+
return;
|
|
38
|
+
const next = new Set(advertised);
|
|
39
|
+
if (checked) {
|
|
40
|
+
next.add(code);
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
next.delete(code);
|
|
44
|
+
}
|
|
45
|
+
setSystemAvailableLocales([...next]);
|
|
46
|
+
}, [mutationsBlocked, advertised, setSystemAvailableLocales]);
|
|
47
|
+
if (contentLocales.length === 0) {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
return (<div className={cx('byline-form-available-locales', styles.container)}>
|
|
51
|
+
<Label id="available-locales" htmlFor="available-locales-group" label={t('availableLocalesWidget.label')}/>
|
|
52
|
+
<div id="available-locales-group" className={cx('byline-form-available-locales-list', styles.list)} role="group" aria-labelledby="label-for-available-locales" aria-describedby="available-locales-description">
|
|
53
|
+
{contentLocales.map(({ code, label }) => {
|
|
54
|
+
const checked = advertisedSet.has(code);
|
|
55
|
+
const { intent, disabled } = reconcileLocaleState(checked, ledgerSet.has(code));
|
|
56
|
+
return (<Checkbox key={code} id={`available-locale-${code}`} name={`__availableLocale_${code}__`} label={label} intent={intent} checked={checked} disabled={mutationsBlocked || disabled} onCheckedChange={(value) => toggle(code, value === true)}/>);
|
|
57
|
+
})}
|
|
58
|
+
</div>
|
|
59
|
+
<span id="available-locales-description" className={cx('byline-form-available-locales-sr-only', styles['sr-only'])}>
|
|
60
|
+
{t('availableLocalesWidget.srDescription')}
|
|
61
|
+
</span>
|
|
62
|
+
</div>);
|
|
63
|
+
};
|
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
/**
|
|
3
|
+
* This Source Code is subject to the terms of the Mozilla Public
|
|
4
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
5
|
+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
6
|
+
*
|
|
7
|
+
* Copyright (c) Infonomic Company Limited
|
|
8
|
+
*/
|
|
9
|
+
import { useState } from 'react';
|
|
10
|
+
import { useTranslation } from '@byline/i18n/react';
|
|
11
|
+
import { Button, Checkbox, CloseIcon, DeleteIcon, Dropdown as DropdownComponent, EllipsisIcon, IconButton, Modal, Select, } from '@byline/ui/react';
|
|
12
|
+
import cx from 'clsx';
|
|
13
|
+
import styles from './document-actions.module.css';
|
|
14
|
+
const DUPLICATE_TITLE_SUFFIX = ' (copy)';
|
|
15
|
+
export function DocumentActions({ disabled = false, publishedVersion, onUnpublish, onDelete, onDuplicate, sourceTitle, onCopyToLocale, sourceLocale, contentLocales, hasUnsavedChanges, onUnsavedChanges, onDeleteLocale, defaultLocale, availableLocales, scheduledPublicationState, onSchedulePublication, onConfirmScheduledPublication, onCancelScheduledPublication, }) {
|
|
16
|
+
const { t } = useTranslation('byline-admin');
|
|
17
|
+
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
|
18
|
+
const [showDuplicateConfirm, setShowDuplicateConfirm] = useState(false);
|
|
19
|
+
const [duplicateBusy, setDuplicateBusy] = useState(false);
|
|
20
|
+
// Copy-to-Locale modal state. The menu item is hidden entirely unless
|
|
21
|
+
// the host has supplied a handler AND there is at least one *other*
|
|
22
|
+
// locale to copy into.
|
|
23
|
+
const availableTargetLocales = (contentLocales ?? []).filter((loc) => loc.code !== sourceLocale);
|
|
24
|
+
const copyToLocaleAvailable = onCopyToLocale != null && availableTargetLocales.length > 0;
|
|
25
|
+
const [showCopyToLocaleConfirm, setShowCopyToLocaleConfirm] = useState(false);
|
|
26
|
+
const [copyToLocaleBusy, setCopyToLocaleBusy] = useState(false);
|
|
27
|
+
const [copyTargetLocale, setCopyTargetLocale] = useState(availableTargetLocales[0]?.code ?? '');
|
|
28
|
+
const [copyOverwrite, setCopyOverwrite] = useState(false);
|
|
29
|
+
// Delete-Locale modal state. The menu item is hidden unless the host
|
|
30
|
+
// supplied a handler AND the document has at least one non-default locale
|
|
31
|
+
// with content. The default locale is the document's anchor and is never
|
|
32
|
+
// listed (it cannot be removed).
|
|
33
|
+
const deletableLocales = (availableLocales ?? [])
|
|
34
|
+
.filter((code) => code !== defaultLocale && code !== 'all')
|
|
35
|
+
.map((code) => ({
|
|
36
|
+
code,
|
|
37
|
+
label: contentLocales?.find((loc) => loc.code === code)?.label ?? code,
|
|
38
|
+
}));
|
|
39
|
+
const deleteLocaleAvailable = onDeleteLocale != null && deletableLocales.length > 0;
|
|
40
|
+
const [showDeleteLocaleConfirm, setShowDeleteLocaleConfirm] = useState(false);
|
|
41
|
+
const [deleteLocaleBusy, setDeleteLocaleBusy] = useState(false);
|
|
42
|
+
const [deleteTargetLocale, setDeleteTargetLocale] = useState('');
|
|
43
|
+
// Scheduled-publication menu items. The derived state already accounts for
|
|
44
|
+
// eligibility and for the abilities the actor holds, so the only thing left
|
|
45
|
+
// here is to map the permitted operations onto entries. The group renders
|
|
46
|
+
// above the locale and duplicate items because scheduling acts on the
|
|
47
|
+
// document's lifecycle rather than on its copies.
|
|
48
|
+
const scheduling = scheduledPublicationState;
|
|
49
|
+
const schedulingActions = [];
|
|
50
|
+
if (scheduling != null) {
|
|
51
|
+
if (scheduling.actions.schedule && onSchedulePublication != null) {
|
|
52
|
+
schedulingActions.push({
|
|
53
|
+
key: 'schedule',
|
|
54
|
+
// Short form: the menu supplies the "this document" context that the
|
|
55
|
+
// modal's title and submit button have to spell out for themselves.
|
|
56
|
+
label: t('scheduledPublication.actions.scheduleMenuItem'),
|
|
57
|
+
onSelect: onSchedulePublication,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
if (scheduling.actions.confirm && onConfirmScheduledPublication != null) {
|
|
61
|
+
schedulingActions.push({
|
|
62
|
+
key: 'confirm',
|
|
63
|
+
label: t('scheduledPublication.actions.confirm'),
|
|
64
|
+
onSelect: () => void onConfirmScheduledPublication(),
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
if (scheduling.actions.reschedule && onSchedulePublication != null) {
|
|
68
|
+
schedulingActions.push({
|
|
69
|
+
key: 'reschedule',
|
|
70
|
+
label: t('scheduledPublication.actions.reschedule'),
|
|
71
|
+
onSelect: onSchedulePublication,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
if (scheduling.actions.cancel && onCancelScheduledPublication != null) {
|
|
75
|
+
schedulingActions.push({
|
|
76
|
+
key: 'cancel',
|
|
77
|
+
label: t('scheduledPublication.actions.cancel'),
|
|
78
|
+
onSelect: () => void onCancelScheduledPublication(),
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
// Whether the ellipsis menu has anything to show. Every entry below is
|
|
83
|
+
// conditional, so with no available action the trigger would open an empty
|
|
84
|
+
// menu.
|
|
85
|
+
const hasAnyAction = schedulingActions.length > 0 ||
|
|
86
|
+
copyToLocaleAvailable ||
|
|
87
|
+
deleteLocaleAvailable ||
|
|
88
|
+
onDuplicate != null ||
|
|
89
|
+
onDelete != null;
|
|
90
|
+
const handleOnDelete = () => {
|
|
91
|
+
if (disabled)
|
|
92
|
+
return;
|
|
93
|
+
setShowDeleteConfirm(false);
|
|
94
|
+
if (onDelete) {
|
|
95
|
+
void onDelete().catch(() => { });
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
const handleOnDuplicate = async () => {
|
|
99
|
+
if (disabled)
|
|
100
|
+
return;
|
|
101
|
+
if (!onDuplicate)
|
|
102
|
+
return;
|
|
103
|
+
setDuplicateBusy(true);
|
|
104
|
+
try {
|
|
105
|
+
await onDuplicate();
|
|
106
|
+
setShowDuplicateConfirm(false);
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
// The host reports the failure and retains the editor observation.
|
|
110
|
+
}
|
|
111
|
+
finally {
|
|
112
|
+
setDuplicateBusy(false);
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
const handleOpenDuplicate = () => {
|
|
116
|
+
if (disabled)
|
|
117
|
+
return;
|
|
118
|
+
// Duplicate copies the saved version — block when the form is dirty so
|
|
119
|
+
// unsaved edits are not silently dropped from the copy.
|
|
120
|
+
if (hasUnsavedChanges) {
|
|
121
|
+
onUnsavedChanges?.();
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
setShowDuplicateConfirm(true);
|
|
125
|
+
};
|
|
126
|
+
const handleOpenCopyToLocale = () => {
|
|
127
|
+
if (disabled)
|
|
128
|
+
return;
|
|
129
|
+
// Copy-to-Locale reads the saved version — block when the form is dirty.
|
|
130
|
+
if (hasUnsavedChanges) {
|
|
131
|
+
onUnsavedChanges?.();
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
// Reset on open: pick the first available target and clear the
|
|
135
|
+
// overwrite checkbox so a previous-session "overwrite=true" choice
|
|
136
|
+
// is not silently sticky.
|
|
137
|
+
setCopyTargetLocale(availableTargetLocales[0]?.code ?? '');
|
|
138
|
+
setCopyOverwrite(false);
|
|
139
|
+
setShowCopyToLocaleConfirm(true);
|
|
140
|
+
};
|
|
141
|
+
const handleOnCopyToLocale = async () => {
|
|
142
|
+
if (disabled)
|
|
143
|
+
return;
|
|
144
|
+
if (!onCopyToLocale || !copyTargetLocale)
|
|
145
|
+
return;
|
|
146
|
+
setCopyToLocaleBusy(true);
|
|
147
|
+
try {
|
|
148
|
+
await onCopyToLocale({ targetLocale: copyTargetLocale, overwrite: copyOverwrite });
|
|
149
|
+
setShowCopyToLocaleConfirm(false);
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
// The host reports the failure and retains the editor observation.
|
|
153
|
+
}
|
|
154
|
+
finally {
|
|
155
|
+
setCopyToLocaleBusy(false);
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
const handleOpenDeleteLocale = () => {
|
|
159
|
+
if (disabled)
|
|
160
|
+
return;
|
|
161
|
+
// Delete-Locale removes the saved version's locale content — block when
|
|
162
|
+
// the form is dirty so the editor saves (or discards) first.
|
|
163
|
+
if (hasUnsavedChanges) {
|
|
164
|
+
onUnsavedChanges?.();
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
// Default to the currently-viewed locale when it is deletable, otherwise
|
|
168
|
+
// the first available target.
|
|
169
|
+
const preferred = deletableLocales.find((loc) => loc.code === sourceLocale)?.code;
|
|
170
|
+
setDeleteTargetLocale(preferred ?? deletableLocales[0]?.code ?? '');
|
|
171
|
+
setShowDeleteLocaleConfirm(true);
|
|
172
|
+
};
|
|
173
|
+
const handleOnDeleteLocale = async () => {
|
|
174
|
+
if (disabled)
|
|
175
|
+
return;
|
|
176
|
+
if (!onDeleteLocale || !deleteTargetLocale)
|
|
177
|
+
return;
|
|
178
|
+
setDeleteLocaleBusy(true);
|
|
179
|
+
try {
|
|
180
|
+
await onDeleteLocale({ targetLocale: deleteTargetLocale });
|
|
181
|
+
setShowDeleteLocaleConfirm(false);
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
// The host reports the failure and retains the editor observation.
|
|
185
|
+
}
|
|
186
|
+
finally {
|
|
187
|
+
setDeleteLocaleBusy(false);
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
// Preview text shown inside the modal. Falls back to the literal suffix
|
|
191
|
+
// when no source title is supplied (collections without `useAsTitle`).
|
|
192
|
+
const duplicatePreviewBefore = sourceTitle ?? '';
|
|
193
|
+
const duplicatePreviewAfter = (sourceTitle ?? '') + DUPLICATE_TITLE_SUFFIX;
|
|
194
|
+
const sourceLocaleLabel = contentLocales?.find((loc) => loc.code === sourceLocale)?.label ?? sourceLocale ?? '';
|
|
195
|
+
return (<>
|
|
196
|
+
{hasAnyAction && (<DropdownComponent.Root>
|
|
197
|
+
<DropdownComponent.Trigger disabled={disabled} render={<IconButton variant="text" intent="noeffect" size="sm"/>}>
|
|
198
|
+
<EllipsisIcon className={cx('byline-form-actions-icon', styles.icon)} width="15px" height="15px"/>
|
|
199
|
+
</DropdownComponent.Trigger>
|
|
200
|
+
|
|
201
|
+
<DropdownComponent.Portal>
|
|
202
|
+
<DropdownComponent.Content className={cx('byline-form-actions-menu', styles.menu)} align="end" data-side="top" sideOffset={10}>
|
|
203
|
+
{/*{publishedVersion && (
|
|
204
|
+
<>
|
|
205
|
+
<DropdownComponent.Item disabled={disabled} onClick={onUnpublish}>
|
|
206
|
+
<div className={cx('byline-form-actions-item', styles.item)}>
|
|
207
|
+
<span className={cx('byline-form-actions-item-icon', styles['item-icon'])} />
|
|
208
|
+
<span className={cx('byline-form-actions-item-text', styles['item-text'])}>
|
|
209
|
+
Unpublish
|
|
210
|
+
</span>
|
|
211
|
+
</div>
|
|
212
|
+
</DropdownComponent.Item>
|
|
213
|
+
<DropdownComponent.Separator />
|
|
214
|
+
</>
|
|
215
|
+
)}*/}
|
|
216
|
+
{schedulingActions.length > 0 && (<>
|
|
217
|
+
{schedulingActions.map((action) => (<DropdownComponent.Item disabled={disabled} key={action.key} onClick={action.onSelect}>
|
|
218
|
+
<div className={cx('byline-form-actions-item', styles.item)}>
|
|
219
|
+
<span className={cx('byline-form-actions-item-text', styles['item-text'])}>
|
|
220
|
+
<button type="button" disabled={disabled}>
|
|
221
|
+
{action.label}
|
|
222
|
+
</button>
|
|
223
|
+
</span>
|
|
224
|
+
</div>
|
|
225
|
+
</DropdownComponent.Item>))}
|
|
226
|
+
<DropdownComponent.Separator />
|
|
227
|
+
</>)}
|
|
228
|
+
{copyToLocaleAvailable && (<DropdownComponent.Item disabled={disabled} onClick={handleOpenCopyToLocale}>
|
|
229
|
+
<div className={cx('byline-form-actions-item', styles.item)}>
|
|
230
|
+
<span className={cx('byline-form-actions-item-text', styles['item-text'])}>
|
|
231
|
+
<button type="button" disabled={disabled}>
|
|
232
|
+
{t('documentActions.copyToLocaleMenuItem')}
|
|
233
|
+
</button>
|
|
234
|
+
</span>
|
|
235
|
+
</div>
|
|
236
|
+
</DropdownComponent.Item>)}
|
|
237
|
+
{deleteLocaleAvailable && (<DropdownComponent.Item disabled={disabled} onClick={handleOpenDeleteLocale}>
|
|
238
|
+
<div className={cx('byline-form-actions-item', styles.item)}>
|
|
239
|
+
<span className={cx('byline-form-actions-item-text', styles['item-text'])}>
|
|
240
|
+
<button type="button" disabled={disabled}>
|
|
241
|
+
{t('documentActions.deleteLocale.menuItem')}
|
|
242
|
+
</button>
|
|
243
|
+
</span>
|
|
244
|
+
</div>
|
|
245
|
+
</DropdownComponent.Item>)}
|
|
246
|
+
{onDuplicate && (<DropdownComponent.Item disabled={disabled} onClick={handleOpenDuplicate}>
|
|
247
|
+
<div className={cx('byline-form-actions-item', styles.item)}>
|
|
248
|
+
<span className={cx('byline-form-actions-item-text', styles['item-text'])}>
|
|
249
|
+
<button type="button" disabled={disabled}>
|
|
250
|
+
{t('common.actions.duplicate')}
|
|
251
|
+
</button>
|
|
252
|
+
</span>
|
|
253
|
+
</div>
|
|
254
|
+
</DropdownComponent.Item>)}
|
|
255
|
+
{onDelete && (<>
|
|
256
|
+
<DropdownComponent.Separator />
|
|
257
|
+
<DropdownComponent.Item disabled={disabled} onClick={() => {
|
|
258
|
+
setShowDeleteConfirm(true);
|
|
259
|
+
}}>
|
|
260
|
+
<div className={cx('byline-form-actions-item', styles.item)}>
|
|
261
|
+
<span className={cx('byline-form-actions-item-icon', styles['item-icon'])}>
|
|
262
|
+
<DeleteIcon width="16px" height="16px"/>
|
|
263
|
+
</span>
|
|
264
|
+
<span className={cx('byline-form-actions-item-text', styles['item-text'])}>
|
|
265
|
+
<button type="button" className={cx('byline-form-actions-delete', styles.delete)}>
|
|
266
|
+
{t('common.actions.delete')}
|
|
267
|
+
</button>
|
|
268
|
+
</span>
|
|
269
|
+
</div>
|
|
270
|
+
</DropdownComponent.Item>
|
|
271
|
+
</>)}
|
|
272
|
+
</DropdownComponent.Content>
|
|
273
|
+
</DropdownComponent.Portal>
|
|
274
|
+
</DropdownComponent.Root>)}
|
|
275
|
+
|
|
276
|
+
<Modal isOpen={showDeleteConfirm} closeOnOverlayClick={true} onDismiss={() => {
|
|
277
|
+
setShowDeleteConfirm(false);
|
|
278
|
+
}}>
|
|
279
|
+
<Modal.Container style={{ maxWidth: '500px' }}>
|
|
280
|
+
<Modal.Header className={cx('byline-form-actions-modal-head', styles['modal-head'])}>
|
|
281
|
+
<h3 className={cx('byline-form-actions-modal-title', styles['modal-title'])}>
|
|
282
|
+
{t('documentActions.delete.title')}
|
|
283
|
+
</h3>
|
|
284
|
+
<IconButton arial-label={t('common.actions.close')} size="xs" onClick={() => {
|
|
285
|
+
setShowDeleteConfirm(false);
|
|
286
|
+
}}>
|
|
287
|
+
<CloseIcon width="16px" height="16px" svgClassName="white-icon"/>
|
|
288
|
+
</IconButton>
|
|
289
|
+
</Modal.Header>
|
|
290
|
+
<Modal.Content>
|
|
291
|
+
<p>{t('documentActions.delete.warning')}</p>
|
|
292
|
+
</Modal.Content>
|
|
293
|
+
<Modal.Actions>
|
|
294
|
+
<button data-autofocus type="button" tabIndex={0} className={cx('byline-form-actions-sr-only', styles['sr-only'])}>
|
|
295
|
+
no action
|
|
296
|
+
</button>
|
|
297
|
+
<Button size="sm" style={{ minWidth: '80px' }} intent="noeffect" onClick={() => {
|
|
298
|
+
setShowDeleteConfirm(false);
|
|
299
|
+
}}>
|
|
300
|
+
{t('common.actions.cancel')}
|
|
301
|
+
</Button>
|
|
302
|
+
<Button size="sm" style={{ minWidth: '80px' }} intent="danger" disabled={disabled} onClick={handleOnDelete}>
|
|
303
|
+
{t('common.actions.delete')}
|
|
304
|
+
</Button>
|
|
305
|
+
</Modal.Actions>
|
|
306
|
+
</Modal.Container>
|
|
307
|
+
</Modal>
|
|
308
|
+
|
|
309
|
+
<Modal isOpen={showDuplicateConfirm} closeOnOverlayClick={!duplicateBusy} onDismiss={() => {
|
|
310
|
+
if (!duplicateBusy)
|
|
311
|
+
setShowDuplicateConfirm(false);
|
|
312
|
+
}}>
|
|
313
|
+
<Modal.Container style={{ maxWidth: '560px' }}>
|
|
314
|
+
<Modal.Header className={cx('byline-form-actions-modal-head', styles['modal-head'])}>
|
|
315
|
+
<h3 className={cx('byline-form-actions-modal-title', styles['modal-title'])}>
|
|
316
|
+
{t('documentActions.duplicate.title')}
|
|
317
|
+
</h3>
|
|
318
|
+
<IconButton arial-label={t('common.actions.close')} size="xs" onClick={() => {
|
|
319
|
+
if (!duplicateBusy)
|
|
320
|
+
setShowDuplicateConfirm(false);
|
|
321
|
+
}}>
|
|
322
|
+
<CloseIcon width="16px" height="16px" svgClassName="white-icon"/>
|
|
323
|
+
</IconButton>
|
|
324
|
+
</Modal.Header>
|
|
325
|
+
<Modal.Content className="prose">
|
|
326
|
+
<p className="m-0">{t('documentActions.duplicate.intro')}</p>
|
|
327
|
+
<ul className={cx('byline-form-actions-list', styles.list)}>
|
|
328
|
+
<li>
|
|
329
|
+
{t('documentActions.duplicate.bulletTitle')}{' '}
|
|
330
|
+
<code>{DUPLICATE_TITLE_SUFFIX.trim()}</code>.
|
|
331
|
+
</li>
|
|
332
|
+
<li>{t('documentActions.duplicate.bulletPath')}</li>
|
|
333
|
+
</ul>
|
|
334
|
+
{sourceTitle != null && sourceTitle.length > 0 && (<div className={cx('byline-form-actions-preview', styles.preview)}>
|
|
335
|
+
<div className={cx('byline-form-actions-preview-label', styles['preview-label'])}>
|
|
336
|
+
{t('documentActions.duplicate.previewLabel')}
|
|
337
|
+
</div>
|
|
338
|
+
<div className={cx('byline-form-actions-preview-row', styles['preview-row'])}>
|
|
339
|
+
<span className={cx('byline-form-actions-preview-before', styles['preview-before'])}>
|
|
340
|
+
{duplicatePreviewBefore}
|
|
341
|
+
</span>
|
|
342
|
+
<span className={cx('byline-form-actions-preview-arrow', styles['preview-arrow'])}>
|
|
343
|
+
→
|
|
344
|
+
</span>
|
|
345
|
+
<span className={cx('byline-form-actions-preview-after', styles['preview-after'])}>
|
|
346
|
+
{duplicatePreviewAfter}
|
|
347
|
+
</span>
|
|
348
|
+
</div>
|
|
349
|
+
</div>)}
|
|
350
|
+
</Modal.Content>
|
|
351
|
+
<Modal.Actions>
|
|
352
|
+
<button data-autofocus type="button" tabIndex={0} className={cx('byline-form-actions-sr-only', styles['sr-only'])}>
|
|
353
|
+
no action
|
|
354
|
+
</button>
|
|
355
|
+
<Button size="sm" style={{ minWidth: '80px' }} intent="noeffect" onClick={() => {
|
|
356
|
+
if (!duplicateBusy)
|
|
357
|
+
setShowDuplicateConfirm(false);
|
|
358
|
+
}} disabled={disabled || duplicateBusy}>
|
|
359
|
+
{t('common.actions.cancel')}
|
|
360
|
+
</Button>
|
|
361
|
+
<Button size="sm" style={{ minWidth: '80px' }} intent="primary" onClick={handleOnDuplicate} disabled={disabled || duplicateBusy}>
|
|
362
|
+
{duplicateBusy
|
|
363
|
+
? t('documentActions.duplicate.busyButton')
|
|
364
|
+
: t('common.actions.duplicate')}
|
|
365
|
+
</Button>
|
|
366
|
+
</Modal.Actions>
|
|
367
|
+
</Modal.Container>
|
|
368
|
+
</Modal>
|
|
369
|
+
|
|
370
|
+
<Modal isOpen={showCopyToLocaleConfirm} closeOnOverlayClick={!copyToLocaleBusy} onDismiss={() => {
|
|
371
|
+
if (!copyToLocaleBusy)
|
|
372
|
+
setShowCopyToLocaleConfirm(false);
|
|
373
|
+
}}>
|
|
374
|
+
<Modal.Container style={{ maxWidth: '560px' }}>
|
|
375
|
+
<Modal.Header className={cx('byline-form-actions-modal-head', styles['modal-head'])}>
|
|
376
|
+
<h3 className={cx('byline-form-actions-modal-title', styles['modal-title'])}>
|
|
377
|
+
{t('documentActions.copyToLocale.title')}
|
|
378
|
+
</h3>
|
|
379
|
+
<IconButton arial-label={t('common.actions.close')} size="xs" onClick={() => {
|
|
380
|
+
if (!copyToLocaleBusy)
|
|
381
|
+
setShowCopyToLocaleConfirm(false);
|
|
382
|
+
}}>
|
|
383
|
+
<CloseIcon width="16px" height="16px" svgClassName="white-icon"/>
|
|
384
|
+
</IconButton>
|
|
385
|
+
</Modal.Header>
|
|
386
|
+
<Modal.Content>
|
|
387
|
+
<p>{t('documentActions.copyToLocale.intro')}</p>
|
|
388
|
+
<div className={cx('byline-form-actions-copy-row', styles['copy-row'])} style={{ marginTop: 'var(--spacing-12)' }}>
|
|
389
|
+
<span className={cx('byline-form-actions-copy-label', styles['copy-label'])} style={{ fontWeight: 500 }}>
|
|
390
|
+
{t('documentActions.copyToLocale.fromLabel')}
|
|
391
|
+
</span>
|
|
392
|
+
<span className={cx('byline-form-actions-copy-source', styles['copy-source'])}>
|
|
393
|
+
{sourceLocaleLabel}
|
|
394
|
+
</span>
|
|
395
|
+
</div>
|
|
396
|
+
<div className={cx('byline-form-actions-copy-row', styles['copy-row'])} style={{ marginTop: 'var(--spacing-12)' }}>
|
|
397
|
+
<span className={cx('byline-form-actions-copy-label', styles['copy-label'])} style={{ fontWeight: 500, marginRight: 'var(--spacing-8)' }}>
|
|
398
|
+
{t('documentActions.copyToLocale.toLabel')}
|
|
399
|
+
</span>
|
|
400
|
+
<Select size="sm" ariaLabel={t('documentActions.copyToLocale.targetAriaLabel')} value={copyTargetLocale} items={availableTargetLocales.map((loc) => ({
|
|
401
|
+
value: loc.code,
|
|
402
|
+
label: loc.label,
|
|
403
|
+
}))} onValueChange={(value) => {
|
|
404
|
+
if (value != null)
|
|
405
|
+
setCopyTargetLocale(value);
|
|
406
|
+
}} disabled={disabled || copyToLocaleBusy}/>
|
|
407
|
+
</div>
|
|
408
|
+
<div className={cx('byline-form-actions-copy-row', styles['copy-row'])} style={{ marginTop: 'var(--spacing-16)' }}>
|
|
409
|
+
<Checkbox id="copy-to-locale-overwrite" name="overwrite" label={t('documentActions.copyToLocale.overwriteLabel')} checked={copyOverwrite} disabled={disabled || copyToLocaleBusy} helpText={t('documentActions.copyToLocale.overwriteHelp')} onCheckedChange={(value) => {
|
|
410
|
+
setCopyOverwrite(value === true);
|
|
411
|
+
}}/>
|
|
412
|
+
</div>
|
|
413
|
+
</Modal.Content>
|
|
414
|
+
<Modal.Actions>
|
|
415
|
+
<button data-autofocus type="button" tabIndex={0} className={cx('byline-form-actions-sr-only', styles['sr-only'])}>
|
|
416
|
+
no action
|
|
417
|
+
</button>
|
|
418
|
+
<Button size="sm" style={{ minWidth: '80px' }} intent="noeffect" onClick={() => {
|
|
419
|
+
if (!copyToLocaleBusy)
|
|
420
|
+
setShowCopyToLocaleConfirm(false);
|
|
421
|
+
}} disabled={disabled || copyToLocaleBusy}>
|
|
422
|
+
{t('common.actions.cancel')}
|
|
423
|
+
</Button>
|
|
424
|
+
<Button size="sm" style={{ minWidth: '80px' }} intent="primary" onClick={handleOnCopyToLocale} disabled={disabled || copyToLocaleBusy || !copyTargetLocale}>
|
|
425
|
+
{copyToLocaleBusy
|
|
426
|
+
? t('documentActions.copyToLocale.busyButton')
|
|
427
|
+
: t('documentActions.copyToLocale.confirmButton')}
|
|
428
|
+
</Button>
|
|
429
|
+
</Modal.Actions>
|
|
430
|
+
</Modal.Container>
|
|
431
|
+
</Modal>
|
|
432
|
+
|
|
433
|
+
<Modal isOpen={showDeleteLocaleConfirm} closeOnOverlayClick={!deleteLocaleBusy} onDismiss={() => {
|
|
434
|
+
if (!deleteLocaleBusy)
|
|
435
|
+
setShowDeleteLocaleConfirm(false);
|
|
436
|
+
}}>
|
|
437
|
+
<Modal.Container style={{ maxWidth: '560px' }}>
|
|
438
|
+
<Modal.Header className={cx('byline-form-actions-modal-head', styles['modal-head'])}>
|
|
439
|
+
<h3 className={cx('byline-form-actions-modal-title', styles['modal-title'])}>
|
|
440
|
+
{t('documentActions.deleteLocale.title')}
|
|
441
|
+
</h3>
|
|
442
|
+
<IconButton arial-label={t('common.actions.close')} size="xs" onClick={() => {
|
|
443
|
+
if (!deleteLocaleBusy)
|
|
444
|
+
setShowDeleteLocaleConfirm(false);
|
|
445
|
+
}}>
|
|
446
|
+
<CloseIcon width="16px" height="16px" svgClassName="white-icon"/>
|
|
447
|
+
</IconButton>
|
|
448
|
+
</Modal.Header>
|
|
449
|
+
<Modal.Content>
|
|
450
|
+
<p>{t('documentActions.deleteLocale.intro')}</p>
|
|
451
|
+
<div className={cx('byline-form-actions-copy-row', styles['copy-row'])} style={{ marginTop: 'var(--spacing-12)' }}>
|
|
452
|
+
<span className={cx('byline-form-actions-copy-label', styles['copy-label'])} style={{ fontWeight: 500, marginRight: 'var(--spacing-8)' }}>
|
|
453
|
+
{t('documentActions.deleteLocale.localeLabel')}
|
|
454
|
+
</span>
|
|
455
|
+
<Select size="sm" ariaLabel={t('documentActions.deleteLocale.targetAriaLabel')} value={deleteTargetLocale} items={deletableLocales.map((loc) => ({
|
|
456
|
+
value: loc.code,
|
|
457
|
+
label: loc.label,
|
|
458
|
+
}))} onValueChange={(value) => {
|
|
459
|
+
if (value != null)
|
|
460
|
+
setDeleteTargetLocale(value);
|
|
461
|
+
}} disabled={disabled || deleteLocaleBusy}/>
|
|
462
|
+
</div>
|
|
463
|
+
<p style={{ marginTop: 'var(--spacing-12)' }}>
|
|
464
|
+
{t('documentActions.deleteLocale.warning')}
|
|
465
|
+
</p>
|
|
466
|
+
</Modal.Content>
|
|
467
|
+
<Modal.Actions>
|
|
468
|
+
<button data-autofocus type="button" tabIndex={0} className={cx('byline-form-actions-sr-only', styles['sr-only'])}>
|
|
469
|
+
no action
|
|
470
|
+
</button>
|
|
471
|
+
<Button size="sm" style={{ minWidth: '80px' }} intent="noeffect" onClick={() => {
|
|
472
|
+
if (!deleteLocaleBusy)
|
|
473
|
+
setShowDeleteLocaleConfirm(false);
|
|
474
|
+
}} disabled={disabled || deleteLocaleBusy}>
|
|
475
|
+
{t('common.actions.cancel')}
|
|
476
|
+
</Button>
|
|
477
|
+
<Button size="sm" style={{ minWidth: '80px' }} intent="danger" onClick={handleOnDeleteLocale} disabled={disabled || deleteLocaleBusy || !deleteTargetLocale}>
|
|
478
|
+
{deleteLocaleBusy
|
|
479
|
+
? t('documentActions.deleteLocale.busyButton')
|
|
480
|
+
: t('documentActions.deleteLocale.confirmButton')}
|
|
481
|
+
</Button>
|
|
482
|
+
</Modal.Actions>
|
|
483
|
+
</Modal.Container>
|
|
484
|
+
</Modal>
|
|
485
|
+
</>);
|
|
486
|
+
}
|
|
@@ -34,8 +34,9 @@ type FieldUploadingListener = (uploading: boolean) => void;
|
|
|
34
34
|
* Why the form is dirty, partitioned by write semantics — drives the single
|
|
35
35
|
* Save button. `content` mints a new version (normal workflow). `direct-write`
|
|
36
36
|
* is an immediate, non-versioned write of the document-grain system fields
|
|
37
|
-
* (path / advertised locales) that does NOT reset workflow status. `both`
|
|
38
|
-
*
|
|
37
|
+
* (path / advertised locales) that does NOT reset workflow status. `both`
|
|
38
|
+
* saves the two together: one request, one guarded transaction, so both commit
|
|
39
|
+
* or neither does. See docs/08-internationalization/03-content-locales.md.
|
|
39
40
|
*/
|
|
40
41
|
export type DirtyReason = 'none' | 'content' | 'direct-write' | 'both';
|
|
41
42
|
export interface DirtyBreakdown {
|