@byline/admin 3.12.1 → 3.13.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/fields/field-services-types.d.ts +50 -0
- package/dist/forms/form-context.js +30 -60
- package/dist/forms/form-modals.d.ts +30 -0
- package/dist/forms/form-modals.js +189 -0
- package/dist/forms/form-renderer.d.ts +8 -1
- package/dist/forms/form-renderer.js +31 -365
- package/dist/forms/form-status-display.d.ts +21 -0
- package/dist/forms/form-status-display.js +94 -0
- package/dist/forms/status-transitions.d.ts +23 -0
- package/dist/forms/status-transitions.js +37 -0
- package/dist/forms/status-transitions.test.node.d.ts +8 -0
- package/dist/forms/tree-placement-widget.d.ts +23 -0
- package/dist/forms/tree-placement-widget.js +176 -0
- package/dist/forms/tree-placement-widget.module.js +15 -0
- package/dist/forms/tree-placement-widget_module.css +69 -0
- package/dist/forms/use-form-layout.d.ts +35 -0
- package/dist/forms/use-form-layout.js +77 -0
- package/dist/forms/use-form-layout.test.node.d.ts +8 -0
- package/dist/forms/use-tracked-slot.d.ts +45 -0
- package/dist/forms/use-tracked-slot.js +46 -0
- package/dist/react.d.ts +1 -1
- package/package.json +5 -5
- package/src/fields/field-services-types.ts +50 -0
- package/src/forms/form-context.tsx +36 -92
- package/src/forms/form-modals.tsx +186 -0
- package/src/forms/form-renderer.tsx +42 -387
- package/src/forms/form-status-display.tsx +108 -0
- package/src/forms/status-transitions.test.node.ts +87 -0
- package/src/forms/status-transitions.ts +86 -0
- package/src/forms/tree-placement-widget.module.css +87 -0
- package/src/forms/tree-placement-widget.tsx +202 -0
- package/src/forms/use-form-layout.test.node.ts +82 -0
- package/src/forms/use-form-layout.ts +134 -0
- package/src/forms/use-tracked-slot.ts +101 -0
- package/src/react.ts +6 -0
|
@@ -20,22 +20,26 @@ import type {
|
|
|
20
20
|
} from '@byline/core'
|
|
21
21
|
import type { DocumentPatch } from '@byline/core/patches'
|
|
22
22
|
import { useTranslation } from '@byline/i18n/react'
|
|
23
|
-
import { Alert, Button,
|
|
23
|
+
import { Alert, Button, ComboButton } from '@byline/ui/react'
|
|
24
24
|
import cx from 'classnames'
|
|
25
25
|
|
|
26
26
|
import { FieldRenderer } from '../fields/field-renderer'
|
|
27
27
|
import { useBylineFieldServices } from '../fields/field-services-context'
|
|
28
|
-
import { LocalDateTime } from '../fields/local-date-time'
|
|
29
28
|
import { AdminGroup } from '../presentation/group'
|
|
30
29
|
import { AdminRow } from '../presentation/row'
|
|
31
30
|
import { AdminTabs } from '../presentation/tabs'
|
|
32
31
|
import { AvailableLocalesWidget } from './available-locales-widget'
|
|
33
32
|
import { DocumentActions, type DocumentActionsLocaleOption } from './document-actions'
|
|
34
33
|
import { FormProvider, useFieldValue, useFormContext } from './form-context'
|
|
34
|
+
import { NavigationGuardModal, SystemFieldsConfirmModal, UnsavedChangesModal } from './form-modals'
|
|
35
35
|
import styles from './form-renderer.module.css'
|
|
36
|
+
import { FormStatusDisplay } from './form-status-display'
|
|
36
37
|
import { useNavigationGuardAdapter } from './navigation-guard'
|
|
37
38
|
import { PathWidget } from './path-widget'
|
|
39
|
+
import { computeStatusTransitions } from './status-transitions'
|
|
40
|
+
import { TreePlacementWidget } from './tree-placement-widget'
|
|
38
41
|
import { executeUploadsWithProgress } from './upload-executor'
|
|
42
|
+
import { useFormLayout } from './use-form-layout'
|
|
39
43
|
import type { UseNavigationGuard } from './navigation-guard'
|
|
40
44
|
|
|
41
45
|
/** Metadata about a previously published version that is still live. */
|
|
@@ -126,6 +130,13 @@ export interface FormRendererProps {
|
|
|
126
130
|
* against the document's `_availableVersionLocales` ledger fact.
|
|
127
131
|
*/
|
|
128
132
|
advertiseLocales?: boolean
|
|
133
|
+
/**
|
|
134
|
+
* Opts the document-tree placement widget into the sidebar (above the
|
|
135
|
+
* available-locales widget). Sourced from `CollectionDefinition.tree` by the
|
|
136
|
+
* caller. Renders only in edit mode (placement needs a persisted document)
|
|
137
|
+
* and only when the host wires the tree services. See docs/DOCUMENT-TREE.md.
|
|
138
|
+
*/
|
|
139
|
+
tree?: boolean
|
|
129
140
|
headingLabel?: string
|
|
130
141
|
headerSlot?: ReactNode
|
|
131
142
|
/** Collection path forwarded to upload-capable fields (e.g. `'media'`). */
|
|
@@ -156,166 +167,6 @@ export interface FormRendererProps {
|
|
|
156
167
|
useNavigationGuard?: UseNavigationGuard
|
|
157
168
|
}
|
|
158
169
|
|
|
159
|
-
const FormStatusDisplay = ({
|
|
160
|
-
initialData,
|
|
161
|
-
workflowStatuses,
|
|
162
|
-
publishedVersion,
|
|
163
|
-
onUnpublish,
|
|
164
|
-
}: {
|
|
165
|
-
initialData?: Record<string, any>
|
|
166
|
-
workflowStatuses?: WorkflowStatus[]
|
|
167
|
-
publishedVersion?: PublishedVersionInfo | null
|
|
168
|
-
onUnpublish?: () => Promise<void>
|
|
169
|
-
}) => {
|
|
170
|
-
const { t } = useTranslation('byline-admin')
|
|
171
|
-
const statusCode = initialData?.status
|
|
172
|
-
const statusLabel = workflowStatuses?.find((s) => s.name === statusCode)?.label ?? statusCode
|
|
173
|
-
// Single-status workflows (e.g. lookups) have no editorial lifecycle —
|
|
174
|
-
// suppress the "Status: …" cell since there is nothing meaningful to convey.
|
|
175
|
-
const showStatusCell = (workflowStatuses?.length ?? 0) > 1
|
|
176
|
-
|
|
177
|
-
return (
|
|
178
|
-
<div className={cx('byline-form-status', styles.status)}>
|
|
179
|
-
<div className={cx('byline-form-status-meta', styles['status-meta'])}>
|
|
180
|
-
{showStatusCell && (
|
|
181
|
-
<div className={cx('byline-form-status-cell', styles['status-cell'])}>
|
|
182
|
-
<span className={cx('byline-form-status-muted', styles['status-muted'])}>
|
|
183
|
-
{t('forms.status.label')}
|
|
184
|
-
</span>
|
|
185
|
-
<span className={cx('byline-form-status-trunc', styles['status-trunc'])}>
|
|
186
|
-
{statusLabel}
|
|
187
|
-
</span>
|
|
188
|
-
</div>
|
|
189
|
-
)}
|
|
190
|
-
|
|
191
|
-
{initialData?.updatedAt != null && (
|
|
192
|
-
<div className={cx('byline-form-status-cell', styles['status-cell'])}>
|
|
193
|
-
<span className={cx('byline-form-status-muted', styles['status-muted'])}>
|
|
194
|
-
{t('forms.status.lastModified')}
|
|
195
|
-
</span>
|
|
196
|
-
<span className={cx('byline-form-status-trunc', styles['status-trunc'])}>
|
|
197
|
-
<LocalDateTime value={initialData.updatedAt} />
|
|
198
|
-
</span>
|
|
199
|
-
</div>
|
|
200
|
-
)}
|
|
201
|
-
|
|
202
|
-
{initialData?.createdAt != null && (
|
|
203
|
-
<div className={cx('byline-form-status-cell', styles['status-cell'])}>
|
|
204
|
-
<span className={cx('byline-form-status-muted', styles['status-muted'])}>
|
|
205
|
-
{t('forms.status.created')}
|
|
206
|
-
</span>
|
|
207
|
-
<span className={cx('byline-form-status-trunc', styles['status-trunc'])}>
|
|
208
|
-
<LocalDateTime value={initialData.createdAt} />
|
|
209
|
-
</span>
|
|
210
|
-
</div>
|
|
211
|
-
)}
|
|
212
|
-
</div>
|
|
213
|
-
|
|
214
|
-
{publishedVersion != null && (
|
|
215
|
-
<div className={cx('byline-form-status-published', styles['status-published'])}>
|
|
216
|
-
<span className={cx('byline-form-status-muted', styles['status-muted'])}>
|
|
217
|
-
{t('forms.status.publishedLive')}{' '}
|
|
218
|
-
{publishedVersion.updatedAt ? (
|
|
219
|
-
<span>
|
|
220
|
-
{t('forms.status.publishedOn', { date: new Date(publishedVersion.updatedAt) })}
|
|
221
|
-
</span>
|
|
222
|
-
) : (
|
|
223
|
-
''
|
|
224
|
-
)}
|
|
225
|
-
</span>
|
|
226
|
-
{onUnpublish && (
|
|
227
|
-
<>
|
|
228
|
-
{' '}
|
|
229
|
-
<button
|
|
230
|
-
type="button"
|
|
231
|
-
onClick={onUnpublish}
|
|
232
|
-
className={cx('byline-form-status-unpublish', styles['status-unpublish'])}
|
|
233
|
-
>
|
|
234
|
-
{t('common.actions.unpublish')}
|
|
235
|
-
</button>
|
|
236
|
-
</>
|
|
237
|
-
)}
|
|
238
|
-
</div>
|
|
239
|
-
)}
|
|
240
|
-
</div>
|
|
241
|
-
)
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
/**
|
|
245
|
-
* Compute the primary and secondary status transitions for the ComboButton.
|
|
246
|
-
* - Primary: the main action (forward step), or the current status itself
|
|
247
|
-
* when the document has reached the final workflow step (terminal state).
|
|
248
|
-
* - Secondary: other available transitions to show as dropdown options.
|
|
249
|
-
* - isTerminal: true when the document is at the final workflow status —
|
|
250
|
-
* the primary button renders as a non-actionable indicator and all
|
|
251
|
-
* back-steps move into the dropdown.
|
|
252
|
-
*/
|
|
253
|
-
function computeStatusTransitions(
|
|
254
|
-
currentStatus: string | undefined,
|
|
255
|
-
workflowStatuses: WorkflowStatus[] | undefined,
|
|
256
|
-
nextStatus: WorkflowStatus | undefined
|
|
257
|
-
): {
|
|
258
|
-
primaryStatus: WorkflowStatus | undefined
|
|
259
|
-
secondaryStatuses: WorkflowStatus[]
|
|
260
|
-
isTerminal: boolean
|
|
261
|
-
} {
|
|
262
|
-
if (!workflowStatuses || workflowStatuses.length === 0 || !currentStatus) {
|
|
263
|
-
return { primaryStatus: nextStatus, secondaryStatuses: [], isTerminal: false }
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
// Single-status workflows (e.g. SINGLE_STATUS_WORKFLOW for lookups) have
|
|
267
|
-
// no transitions — short-circuit so the form shows only Close / Save.
|
|
268
|
-
if (workflowStatuses.length <= 1) {
|
|
269
|
-
return { primaryStatus: undefined, secondaryStatuses: [], isTerminal: false }
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
const currentIndex = workflowStatuses.findIndex((s) => s.name === currentStatus)
|
|
273
|
-
if (currentIndex === -1) {
|
|
274
|
-
return { primaryStatus: nextStatus, secondaryStatuses: [], isTerminal: false }
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
const isAtEnd = currentIndex === workflowStatuses.length - 1
|
|
278
|
-
const isAtStart = currentIndex === 0
|
|
279
|
-
|
|
280
|
-
// Collect all available target statuses
|
|
281
|
-
const availableTargets: WorkflowStatus[] = []
|
|
282
|
-
|
|
283
|
-
// Reset to first (if not at first)
|
|
284
|
-
if (!isAtStart && workflowStatuses[0]) {
|
|
285
|
-
availableTargets.push(workflowStatuses[0])
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
// Back one step (if not at start and the previous is not already the first)
|
|
289
|
-
const prev = workflowStatuses[currentIndex - 1]
|
|
290
|
-
if (currentIndex > 1 && prev) {
|
|
291
|
-
availableTargets.push(prev)
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
// Forward one step (if not at end) - this is the nextStatus
|
|
295
|
-
const next = workflowStatuses[currentIndex + 1]
|
|
296
|
-
if (!isAtEnd && next) {
|
|
297
|
-
availableTargets.push(next)
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
if (isAtEnd) {
|
|
301
|
-
// Terminal state: the primary button is a non-actionable indicator of the
|
|
302
|
-
// current status; both back-steps (revert to previous / reset to first)
|
|
303
|
-
// are surfaced in the dropdown.
|
|
304
|
-
return {
|
|
305
|
-
primaryStatus: workflowStatuses[currentIndex],
|
|
306
|
-
secondaryStatuses: availableTargets,
|
|
307
|
-
isTerminal: true,
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
// Not at end: primary is the forward step (nextStatus)
|
|
312
|
-
return {
|
|
313
|
-
primaryStatus: nextStatus,
|
|
314
|
-
secondaryStatuses: availableTargets.filter((s) => s.name !== nextStatus?.name),
|
|
315
|
-
isTerminal: false,
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
|
|
319
170
|
const FormContent = ({
|
|
320
171
|
mode,
|
|
321
172
|
fields,
|
|
@@ -336,6 +187,7 @@ const FormContent = ({
|
|
|
336
187
|
useAsTitle,
|
|
337
188
|
useAsPath,
|
|
338
189
|
advertiseLocales,
|
|
190
|
+
tree,
|
|
339
191
|
headingLabel,
|
|
340
192
|
headerSlot,
|
|
341
193
|
collectionPath,
|
|
@@ -394,80 +246,12 @@ const FormContent = ({
|
|
|
394
246
|
if (initialLocale) setContentLocale(initialLocale)
|
|
395
247
|
}, [initialLocale])
|
|
396
248
|
|
|
397
|
-
//
|
|
398
|
-
//
|
|
399
|
-
//
|
|
400
|
-
//
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
// ---------------------------------------------------------------------
|
|
404
|
-
|
|
405
|
-
const fieldByName = useMemo(() => {
|
|
406
|
-
const map = new Map<string, Field>()
|
|
407
|
-
for (const field of fields) {
|
|
408
|
-
if ('name' in field) map.set(field.name, field)
|
|
409
|
-
}
|
|
410
|
-
return map
|
|
411
|
-
}, [fields])
|
|
412
|
-
|
|
413
|
-
const tabSetByName = useMemo(() => {
|
|
414
|
-
const map = new Map<string, TabSetDefinition>()
|
|
415
|
-
for (const set of adminConfig?.tabSets ?? []) map.set(set.name, set)
|
|
416
|
-
return map
|
|
417
|
-
}, [adminConfig])
|
|
418
|
-
|
|
419
|
-
const rowByName = useMemo(() => {
|
|
420
|
-
const map = new Map<string, RowDefinition>()
|
|
421
|
-
for (const row of adminConfig?.rows ?? []) map.set(row.name, row)
|
|
422
|
-
return map
|
|
423
|
-
}, [adminConfig])
|
|
424
|
-
|
|
425
|
-
const groupByName = useMemo(() => {
|
|
426
|
-
const map = new Map<string, GroupDefinition>()
|
|
427
|
-
for (const group of adminConfig?.groups ?? []) map.set(group.name, group)
|
|
428
|
-
return map
|
|
429
|
-
}, [adminConfig])
|
|
430
|
-
|
|
431
|
-
// When `layout` is omitted, synthesise main = all schema fields in order.
|
|
432
|
-
const layout = useMemo(() => {
|
|
433
|
-
if (adminConfig?.layout) return adminConfig.layout
|
|
434
|
-
return { main: fields.filter((f) => 'name' in f).map((f) => (f as { name: string }).name) }
|
|
435
|
-
}, [adminConfig, fields])
|
|
436
|
-
|
|
437
|
-
// Reverse index: schema field name → which tab set + tab it lives in.
|
|
438
|
-
// Powers per-tab-set error badge counts. Fields not under any tab set
|
|
439
|
-
// (e.g. raw-field placement directly in `layout.main`) are absent from
|
|
440
|
-
// this map.
|
|
441
|
-
const fieldToTabPath = useMemo(() => {
|
|
442
|
-
const map = new Map<string, { tabSetName: string; tabName: string }>()
|
|
443
|
-
const visit = (
|
|
444
|
-
names: readonly string[],
|
|
445
|
-
tabSetName: string,
|
|
446
|
-
tabName: string,
|
|
447
|
-
seen: Set<string>
|
|
448
|
-
) => {
|
|
449
|
-
for (const name of names) {
|
|
450
|
-
if (fieldByName.has(name)) {
|
|
451
|
-
map.set(name, { tabSetName, tabName })
|
|
452
|
-
} else if (seen.has(name)) {
|
|
453
|
-
} else if (rowByName.has(name)) {
|
|
454
|
-
const row = rowByName.get(name)!
|
|
455
|
-
const next = new Set(seen).add(name)
|
|
456
|
-
visit(row.fields, tabSetName, tabName, next)
|
|
457
|
-
} else if (groupByName.has(name)) {
|
|
458
|
-
const group = groupByName.get(name)!
|
|
459
|
-
const next = new Set(seen).add(name)
|
|
460
|
-
visit(group.fields, tabSetName, tabName, next)
|
|
461
|
-
}
|
|
462
|
-
}
|
|
463
|
-
}
|
|
464
|
-
for (const set of adminConfig?.tabSets ?? []) {
|
|
465
|
-
for (const tab of set.tabs) {
|
|
466
|
-
visit(tab.fields, set.name, tab.name, new Set())
|
|
467
|
-
}
|
|
468
|
-
}
|
|
469
|
-
return map
|
|
470
|
-
}, [adminConfig, fieldByName, rowByName, groupByName])
|
|
249
|
+
// Layout primitives + lookup tables — pure derivations of `adminConfig` +
|
|
250
|
+
// `fields`. The validator at startup guarantees every reachable name
|
|
251
|
+
// resolves and every schema field is placed at most once, so the render-time
|
|
252
|
+
// lookups below are unguarded. See ./use-form-layout.
|
|
253
|
+
const { fieldByName, tabSetByName, rowByName, groupByName, layout, fieldToTabPath } =
|
|
254
|
+
useFormLayout(adminConfig, fields)
|
|
471
255
|
|
|
472
256
|
// ---------------------------------------------------------------------
|
|
473
257
|
// Active-tab state — one tab name per declared tab set.
|
|
@@ -890,6 +674,13 @@ const FormContent = ({
|
|
|
890
674
|
mode={mode}
|
|
891
675
|
/>
|
|
892
676
|
)}
|
|
677
|
+
{tree && mode === 'edit' && typeof initialData?.id === 'string' && (
|
|
678
|
+
<TreePlacementWidget
|
|
679
|
+
collectionPath={collectionPath ?? ''}
|
|
680
|
+
documentId={initialData.id as string}
|
|
681
|
+
useAsTitle={useAsTitle}
|
|
682
|
+
/>
|
|
683
|
+
)}
|
|
893
684
|
{advertiseLocales && (
|
|
894
685
|
<AvailableLocalesWidget
|
|
895
686
|
contentLocales={contentLocales ?? []}
|
|
@@ -901,159 +692,21 @@ const FormContent = ({
|
|
|
901
692
|
{(layout.sidebar ?? []).map((name) => renderItem(name))}
|
|
902
693
|
</div>
|
|
903
694
|
</div>
|
|
904
|
-
{showUnsavedModal && (
|
|
905
|
-
<Modal
|
|
906
|
-
isOpen={true}
|
|
907
|
-
closeOnOverlayClick={true}
|
|
908
|
-
onDismiss={() => setShowUnsavedModal(false)}
|
|
909
|
-
>
|
|
910
|
-
<Modal.Container style={{ maxWidth: '460px' }}>
|
|
911
|
-
<Modal.Header
|
|
912
|
-
className={cx('byline-form-guard-modal-head', styles['guard-modal-head'])}
|
|
913
|
-
>
|
|
914
|
-
<h3 className={cx('byline-form-guard-modal-title', styles['guard-modal-title'])}>
|
|
915
|
-
{t('forms.unsavedChanges.title')}
|
|
916
|
-
</h3>
|
|
917
|
-
</Modal.Header>
|
|
918
|
-
<Modal.Content>
|
|
919
|
-
<p className={cx('byline-form-guard-modal-text', styles['guard-modal-text'])}>
|
|
920
|
-
{t('forms.unsavedChanges.message')}
|
|
921
|
-
</p>
|
|
922
|
-
</Modal.Content>
|
|
923
|
-
<Modal.Actions>
|
|
924
|
-
<Button
|
|
925
|
-
size="sm"
|
|
926
|
-
style={{ minWidth: '60px' }}
|
|
927
|
-
intent="primary"
|
|
928
|
-
type="button"
|
|
929
|
-
onClick={() => setShowUnsavedModal(false)}
|
|
930
|
-
>
|
|
931
|
-
{t('forms.unsavedChanges.okButton')}
|
|
932
|
-
</Button>
|
|
933
|
-
</Modal.Actions>
|
|
934
|
-
</Modal.Container>
|
|
935
|
-
</Modal>
|
|
936
|
-
)}
|
|
695
|
+
{showUnsavedModal && <UnsavedChangesModal onClose={() => setShowUnsavedModal(false)} />}
|
|
937
696
|
{pendingSystemFieldsSubmit != null && (
|
|
938
|
-
<
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
? t('forms.systemFieldsConfirm.bothTitle')
|
|
950
|
-
: t('forms.systemFieldsConfirm.title')}
|
|
951
|
-
</h3>
|
|
952
|
-
<IconButton
|
|
953
|
-
aria-label={t('common.actions.close')}
|
|
954
|
-
size="xs"
|
|
955
|
-
onClick={() => setPendingSystemFieldsSubmit(null)}
|
|
956
|
-
>
|
|
957
|
-
<CloseIcon width="16px" height="16px" svgClassName="white-icon" />
|
|
958
|
-
</IconButton>
|
|
959
|
-
</Modal.Header>
|
|
960
|
-
<Modal.Content className="prose">
|
|
961
|
-
{/* Lead with reassurance: content edits follow the normal
|
|
962
|
-
revision + publish workflow. The immediate, document-level
|
|
963
|
-
system-field write is explained below the divider. */}
|
|
964
|
-
{pendingSystemFieldsSubmit.contentDirty && (
|
|
965
|
-
<p className={cx('byline-form-system-fields-content-note', 'm-0 mt-2')}>
|
|
966
|
-
{t('forms.systemFieldsConfirm.contentNote')}
|
|
967
|
-
</p>
|
|
968
|
-
)}
|
|
969
|
-
<p
|
|
970
|
-
className="m-0 mt-2"
|
|
971
|
-
style={
|
|
972
|
-
pendingSystemFieldsSubmit.contentDirty
|
|
973
|
-
? {
|
|
974
|
-
marginTop: 'var(--spacing-8)',
|
|
975
|
-
paddingTop: 'var(--spacing-12)',
|
|
976
|
-
borderTop: '1px solid var(--border-color)',
|
|
977
|
-
}
|
|
978
|
-
: undefined
|
|
979
|
-
}
|
|
980
|
-
>
|
|
981
|
-
{t('forms.systemFieldsConfirm.intro')}
|
|
982
|
-
</p>
|
|
983
|
-
<ul
|
|
984
|
-
className={cx('byline-form-system-fields-list', styles['guard-modal-text'], 'm-0')}
|
|
985
|
-
>
|
|
986
|
-
{pendingSystemFieldsSubmit.pathDirty && (
|
|
987
|
-
<li>{t('forms.systemFieldsConfirm.bulletPath')}</li>
|
|
988
|
-
)}
|
|
989
|
-
{pendingSystemFieldsSubmit.availableLocalesDirty && (
|
|
990
|
-
<li>{t('forms.systemFieldsConfirm.bulletLocales')}</li>
|
|
991
|
-
)}
|
|
992
|
-
</ul>
|
|
993
|
-
<p
|
|
994
|
-
className={cx('byline-form-system-fields-effect', styles['guard-modal-text'])}
|
|
995
|
-
style={{
|
|
996
|
-
marginTop: 'var(--spacing-4)',
|
|
997
|
-
marginBottom: 0,
|
|
998
|
-
color: 'var(--text-subtle)',
|
|
999
|
-
}}
|
|
1000
|
-
>
|
|
1001
|
-
{t('forms.systemFieldsConfirm.effectLine')}
|
|
1002
|
-
</p>
|
|
1003
|
-
</Modal.Content>
|
|
1004
|
-
<Modal.Actions>
|
|
1005
|
-
<Button
|
|
1006
|
-
size="sm"
|
|
1007
|
-
style={{ minWidth: '80px' }}
|
|
1008
|
-
intent="noeffect"
|
|
1009
|
-
type="button"
|
|
1010
|
-
onClick={() => setPendingSystemFieldsSubmit(null)}
|
|
1011
|
-
>
|
|
1012
|
-
{t('common.actions.cancel')}
|
|
1013
|
-
</Button>
|
|
1014
|
-
<Button
|
|
1015
|
-
size="sm"
|
|
1016
|
-
style={{ minWidth: '80px' }}
|
|
1017
|
-
intent="primary"
|
|
1018
|
-
type="button"
|
|
1019
|
-
onClick={() => {
|
|
1020
|
-
const payload = pendingSystemFieldsSubmit
|
|
1021
|
-
setPendingSystemFieldsSubmit(null)
|
|
1022
|
-
submitPayload(payload)
|
|
1023
|
-
}}
|
|
1024
|
-
>
|
|
1025
|
-
{t('forms.systemFieldsConfirm.confirmButton')}
|
|
1026
|
-
</Button>
|
|
1027
|
-
</Modal.Actions>
|
|
1028
|
-
</Modal.Container>
|
|
1029
|
-
</Modal>
|
|
1030
|
-
)}
|
|
1031
|
-
{guard.isBlocked && (
|
|
1032
|
-
<Modal isOpen={true} closeOnOverlayClick={false} onDismiss={guard.stay}>
|
|
1033
|
-
<Modal.Container style={{ maxWidth: '460px' }}>
|
|
1034
|
-
<Modal.Header
|
|
1035
|
-
className={cx('byline-form-guard-modal-head', styles['guard-modal-head'])}
|
|
1036
|
-
>
|
|
1037
|
-
<h3 className={cx('byline-form-guard-modal-title', styles['guard-modal-title'])}>
|
|
1038
|
-
{t('forms.navigationGuard.title')}
|
|
1039
|
-
</h3>
|
|
1040
|
-
</Modal.Header>
|
|
1041
|
-
<Modal.Content>
|
|
1042
|
-
<p className={cx('byline-form-guard-modal-text', styles['guard-modal-text'])}>
|
|
1043
|
-
{t('forms.navigationGuard.message')}
|
|
1044
|
-
</p>
|
|
1045
|
-
</Modal.Content>
|
|
1046
|
-
<Modal.Actions>
|
|
1047
|
-
<Button size="sm" intent="noeffect" type="button" onClick={guard.stay}>
|
|
1048
|
-
{t('forms.navigationGuard.stayButton')}
|
|
1049
|
-
</Button>
|
|
1050
|
-
<Button size="sm" intent="danger" type="button" onClick={guard.proceed}>
|
|
1051
|
-
{t('forms.navigationGuard.leaveButton')}
|
|
1052
|
-
</Button>
|
|
1053
|
-
</Modal.Actions>
|
|
1054
|
-
</Modal.Container>
|
|
1055
|
-
</Modal>
|
|
697
|
+
<SystemFieldsConfirmModal
|
|
698
|
+
contentDirty={pendingSystemFieldsSubmit.contentDirty}
|
|
699
|
+
pathDirty={pendingSystemFieldsSubmit.pathDirty}
|
|
700
|
+
availableLocalesDirty={pendingSystemFieldsSubmit.availableLocalesDirty}
|
|
701
|
+
onCancel={() => setPendingSystemFieldsSubmit(null)}
|
|
702
|
+
onConfirm={() => {
|
|
703
|
+
const payload = pendingSystemFieldsSubmit
|
|
704
|
+
setPendingSystemFieldsSubmit(null)
|
|
705
|
+
submitPayload(payload)
|
|
706
|
+
}}
|
|
707
|
+
/>
|
|
1056
708
|
)}
|
|
709
|
+
{guard.isBlocked && <NavigationGuardModal onStay={guard.stay} onProceed={guard.proceed} />}
|
|
1057
710
|
</form>
|
|
1058
711
|
)
|
|
1059
712
|
}
|
|
@@ -1078,6 +731,7 @@ export const FormRenderer = ({
|
|
|
1078
731
|
useAsTitle,
|
|
1079
732
|
useAsPath,
|
|
1080
733
|
advertiseLocales,
|
|
734
|
+
tree,
|
|
1081
735
|
headingLabel,
|
|
1082
736
|
headerSlot,
|
|
1083
737
|
collectionPath,
|
|
@@ -1116,6 +770,7 @@ export const FormRenderer = ({
|
|
|
1116
770
|
useAsTitle={useAsTitle}
|
|
1117
771
|
useAsPath={useAsPath}
|
|
1118
772
|
advertiseLocales={advertiseLocales}
|
|
773
|
+
tree={tree}
|
|
1119
774
|
headingLabel={headingLabel}
|
|
1120
775
|
headerSlot={headerSlot}
|
|
1121
776
|
collectionPath={collectionPath}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* This Source Code is subject to the terms of the Mozilla Public
|
|
5
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
6
|
+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
7
|
+
*
|
|
8
|
+
* Copyright (c) Infonomic Company Limited
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { WorkflowStatus } from '@byline/core'
|
|
12
|
+
import { useTranslation } from '@byline/i18n/react'
|
|
13
|
+
import cx from 'classnames'
|
|
14
|
+
|
|
15
|
+
import { LocalDateTime } from '../fields/local-date-time'
|
|
16
|
+
import styles from './form-renderer.module.css'
|
|
17
|
+
import type { PublishedVersionInfo } from './form-renderer'
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The status / metadata strip at the top of a document form — current
|
|
21
|
+
* workflow status (suppressed for single-status workflows), last-modified and
|
|
22
|
+
* created timestamps, and a "published live" indicator with an inline
|
|
23
|
+
* Unpublish action when a previously-published version is still live.
|
|
24
|
+
*/
|
|
25
|
+
export const FormStatusDisplay = ({
|
|
26
|
+
initialData,
|
|
27
|
+
workflowStatuses,
|
|
28
|
+
publishedVersion,
|
|
29
|
+
onUnpublish,
|
|
30
|
+
}: {
|
|
31
|
+
initialData?: Record<string, any>
|
|
32
|
+
workflowStatuses?: WorkflowStatus[]
|
|
33
|
+
publishedVersion?: PublishedVersionInfo | null
|
|
34
|
+
onUnpublish?: () => Promise<void>
|
|
35
|
+
}) => {
|
|
36
|
+
const { t } = useTranslation('byline-admin')
|
|
37
|
+
const statusCode = initialData?.status
|
|
38
|
+
const statusLabel = workflowStatuses?.find((s) => s.name === statusCode)?.label ?? statusCode
|
|
39
|
+
// Single-status workflows (e.g. lookups) have no editorial lifecycle —
|
|
40
|
+
// suppress the "Status: …" cell since there is nothing meaningful to convey.
|
|
41
|
+
const showStatusCell = (workflowStatuses?.length ?? 0) > 1
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<div className={cx('byline-form-status', styles.status)}>
|
|
45
|
+
<div className={cx('byline-form-status-meta', styles['status-meta'])}>
|
|
46
|
+
{showStatusCell && (
|
|
47
|
+
<div className={cx('byline-form-status-cell', styles['status-cell'])}>
|
|
48
|
+
<span className={cx('byline-form-status-muted', styles['status-muted'])}>
|
|
49
|
+
{t('forms.status.label')}
|
|
50
|
+
</span>
|
|
51
|
+
<span className={cx('byline-form-status-trunc', styles['status-trunc'])}>
|
|
52
|
+
{statusLabel}
|
|
53
|
+
</span>
|
|
54
|
+
</div>
|
|
55
|
+
)}
|
|
56
|
+
|
|
57
|
+
{initialData?.updatedAt != null && (
|
|
58
|
+
<div className={cx('byline-form-status-cell', styles['status-cell'])}>
|
|
59
|
+
<span className={cx('byline-form-status-muted', styles['status-muted'])}>
|
|
60
|
+
{t('forms.status.lastModified')}
|
|
61
|
+
</span>
|
|
62
|
+
<span className={cx('byline-form-status-trunc', styles['status-trunc'])}>
|
|
63
|
+
<LocalDateTime value={initialData.updatedAt} />
|
|
64
|
+
</span>
|
|
65
|
+
</div>
|
|
66
|
+
)}
|
|
67
|
+
|
|
68
|
+
{initialData?.createdAt != null && (
|
|
69
|
+
<div className={cx('byline-form-status-cell', styles['status-cell'])}>
|
|
70
|
+
<span className={cx('byline-form-status-muted', styles['status-muted'])}>
|
|
71
|
+
{t('forms.status.created')}
|
|
72
|
+
</span>
|
|
73
|
+
<span className={cx('byline-form-status-trunc', styles['status-trunc'])}>
|
|
74
|
+
<LocalDateTime value={initialData.createdAt} />
|
|
75
|
+
</span>
|
|
76
|
+
</div>
|
|
77
|
+
)}
|
|
78
|
+
</div>
|
|
79
|
+
|
|
80
|
+
{publishedVersion != null && (
|
|
81
|
+
<div className={cx('byline-form-status-published', styles['status-published'])}>
|
|
82
|
+
<span className={cx('byline-form-status-muted', styles['status-muted'])}>
|
|
83
|
+
{t('forms.status.publishedLive')}{' '}
|
|
84
|
+
{publishedVersion.updatedAt ? (
|
|
85
|
+
<span>
|
|
86
|
+
{t('forms.status.publishedOn', { date: new Date(publishedVersion.updatedAt) })}
|
|
87
|
+
</span>
|
|
88
|
+
) : (
|
|
89
|
+
''
|
|
90
|
+
)}
|
|
91
|
+
</span>
|
|
92
|
+
{onUnpublish && (
|
|
93
|
+
<>
|
|
94
|
+
{' '}
|
|
95
|
+
<button
|
|
96
|
+
type="button"
|
|
97
|
+
onClick={onUnpublish}
|
|
98
|
+
className={cx('byline-form-status-unpublish', styles['status-unpublish'])}
|
|
99
|
+
>
|
|
100
|
+
{t('common.actions.unpublish')}
|
|
101
|
+
</button>
|
|
102
|
+
</>
|
|
103
|
+
)}
|
|
104
|
+
</div>
|
|
105
|
+
)}
|
|
106
|
+
</div>
|
|
107
|
+
)
|
|
108
|
+
}
|