@byline/ui 2.5.2 → 2.6.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/components/shimmer/shimmer.d.ts +13 -1
- package/dist/components/shimmer/shimmer.js +29 -20
- package/dist/components/shimmer/shimmer_module.css +4 -4
- package/dist/dnd/draggable-sortable/demo/draggable-list-demo.js +1 -1
- package/dist/react.d.ts +18 -54
- package/dist/react.js +0 -35
- package/dist/styles/styles.css +3 -0
- package/dist/uikit.d.ts +1 -0
- package/dist/uikit.js +1 -0
- package/package.json +2 -8
- package/src/components/shimmer/shimmer.module.css +8 -4
- package/src/components/shimmer/shimmer.tsx +34 -9
- package/src/dnd/draggable-sortable/demo/draggable-list-demo.tsx +1 -1
- package/src/react.ts +20 -68
- package/src/styles/functional/surfaces.css +13 -1
- package/src/uikit.ts +1 -0
- package/dist/admin/group.d.ts +0 -27
- package/dist/admin/group.js +0 -14
- package/dist/admin/group.module.js +0 -6
- package/dist/admin/group_module.css +0 -19
- package/dist/admin/row.d.ts +0 -25
- package/dist/admin/row.js +0 -8
- package/dist/admin/row.module.js +0 -5
- package/dist/admin/row_module.css +0 -18
- package/dist/admin/tabs.d.ts +0 -25
- package/dist/admin/tabs.js +0 -35
- package/dist/admin/tabs.module.js +0 -10
- package/dist/admin/tabs_module.css +0 -68
- package/dist/fields/array/array-field.d.ts +0 -14
- package/dist/fields/array/array-field.js +0 -176
- package/dist/fields/array/array-field.module.js +0 -11
- package/dist/fields/array/array-field_module.css +0 -32
- package/dist/fields/blocks/blocks-field.d.ts +0 -13
- package/dist/fields/blocks/blocks-field.js +0 -244
- package/dist/fields/blocks/blocks-field.module.js +0 -26
- package/dist/fields/blocks/blocks-field_module.css +0 -107
- package/dist/fields/checkbox/checkbox-field.d.ts +0 -16
- package/dist/fields/checkbox/checkbox-field.js +0 -28
- package/dist/fields/checkbox/checkbox-field.module.js +0 -6
- package/dist/fields/checkbox/checkbox-field_module.css +0 -4
- package/dist/fields/column-formatter.d.ts +0 -20
- package/dist/fields/column-formatter.js +0 -15
- package/dist/fields/date-time-formatter.d.ts +0 -16
- package/dist/fields/date-time-formatter.js +0 -8
- package/dist/fields/datetime/datetime-field.d.ts +0 -16
- package/dist/fields/datetime/datetime-field.js +0 -37
- package/dist/fields/datetime/datetime-field.module.js +0 -5
- package/dist/fields/datetime/datetime-field_module.css +0 -4
- package/dist/fields/draggable-context-menu.d.ts +0 -6
- package/dist/fields/draggable-context-menu.js +0 -83
- package/dist/fields/draggable-context-menu.module.js +0 -15
- package/dist/fields/draggable-context-menu_module.css +0 -91
- package/dist/fields/field-helpers.d.ts +0 -26
- package/dist/fields/field-helpers.js +0 -50
- package/dist/fields/field-renderer.d.ts +0 -37
- package/dist/fields/field-renderer.js +0 -206
- package/dist/fields/field-renderer.module.js +0 -8
- package/dist/fields/field-renderer_module.css +0 -11
- package/dist/fields/file/file-field.d.ts +0 -19
- package/dist/fields/file/file-field.js +0 -226
- package/dist/fields/file/file-field.module.js +0 -18
- package/dist/fields/file/file-field_module.css +0 -131
- package/dist/fields/file/file-upload-field.d.ts +0 -21
- package/dist/fields/file/file-upload-field.js +0 -128
- package/dist/fields/file/file-upload-field.module.js +0 -15
- package/dist/fields/file/file-upload-field_module.css +0 -74
- package/dist/fields/group/group-field.d.ts +0 -15
- package/dist/fields/group/group-field.js +0 -59
- package/dist/fields/group/group-field.module.js +0 -9
- package/dist/fields/group/group-field_module.css +0 -27
- package/dist/fields/image/image-field.d.ts +0 -19
- package/dist/fields/image/image-field.js +0 -242
- package/dist/fields/image/image-field.module.js +0 -22
- package/dist/fields/image/image-field_module.css +0 -121
- package/dist/fields/image/image-upload-field.d.ts +0 -21
- package/dist/fields/image/image-upload-field.js +0 -187
- package/dist/fields/image/image-upload-field.module.js +0 -19
- package/dist/fields/image/image-upload-field_module.css +0 -92
- package/dist/fields/local-date-time.d.ts +0 -27
- package/dist/fields/local-date-time.js +0 -49
- package/dist/fields/locale-badge.d.ts +0 -18
- package/dist/fields/locale-badge.js +0 -10
- package/dist/fields/locale-badge.module.js +0 -5
- package/dist/fields/locale-badge_module.css +0 -27
- package/dist/fields/numerical/numerical-field.d.ts +0 -18
- package/dist/fields/numerical/numerical-field.js +0 -74
- package/dist/fields/relation/relation-display.d.ts +0 -40
- package/dist/fields/relation/relation-display.js +0 -58
- package/dist/fields/relation/relation-display.module.js +0 -9
- package/dist/fields/relation/relation-display_module.css +0 -21
- package/dist/fields/relation/relation-field.d.ts +0 -18
- package/dist/fields/relation/relation-field.js +0 -146
- package/dist/fields/relation/relation-field.module.js +0 -13
- package/dist/fields/relation/relation-field_module.css +0 -62
- package/dist/fields/relation/relation-picker.d.ts +0 -49
- package/dist/fields/relation/relation-picker.js +0 -233
- package/dist/fields/relation/relation-picker.module.js +0 -26
- package/dist/fields/relation/relation-picker_module.css +0 -124
- package/dist/fields/relation/relation-summary.d.ts +0 -31
- package/dist/fields/relation/relation-summary.js +0 -50
- package/dist/fields/relation/relation-summary.module.js +0 -11
- package/dist/fields/relation/relation-summary_module.css +0 -37
- package/dist/fields/select/select-field.d.ts +0 -16
- package/dist/fields/select/select-field.js +0 -50
- package/dist/fields/select/select-field.module.js +0 -5
- package/dist/fields/select/select-field_module.css +0 -4
- package/dist/fields/sortable-item.d.ts +0 -15
- package/dist/fields/sortable-item.js +0 -80
- package/dist/fields/sortable-item.module.js +0 -22
- package/dist/fields/sortable-item_module.css +0 -124
- package/dist/fields/text/text-field.d.ts +0 -20
- package/dist/fields/text/text-field.js +0 -104
- package/dist/fields/text/text-field.module.js +0 -6
- package/dist/fields/text/text-field_module.css +0 -5
- package/dist/fields/text-area/text-area-field.d.ts +0 -20
- package/dist/fields/text-area/text-area-field.js +0 -105
- package/dist/fields/text-area/text-area-field.module.js +0 -6
- package/dist/fields/text-area/text-area-field_module.css +0 -5
- package/dist/fields/use-field-change-handler.d.ts +0 -23
- package/dist/fields/use-field-change-handler.js +0 -52
- package/dist/forms/document-actions.d.ts +0 -48
- package/dist/forms/document-actions.js +0 -469
- package/dist/forms/document-actions.module.js +0 -34
- package/dist/forms/document-actions_module.css +0 -118
- package/dist/forms/form-context.d.ts +0 -89
- package/dist/forms/form-context.js +0 -466
- package/dist/forms/form-renderer.d.ts +0 -98
- package/dist/forms/form-renderer.js +0 -591
- package/dist/forms/form-renderer.module.js +0 -46
- package/dist/forms/form-renderer_module.css +0 -245
- package/dist/forms/navigation-guard.d.ts +0 -54
- package/dist/forms/navigation-guard.js +0 -22
- package/dist/forms/path-widget.d.ts +0 -36
- package/dist/forms/path-widget.js +0 -107
- package/dist/forms/path-widget.module.js +0 -8
- package/dist/forms/path-widget_module.css +0 -29
- package/dist/forms/upload-executor.d.ts +0 -57
- package/dist/forms/upload-executor.js +0 -92
- package/dist/services/field-services-context.d.ts +0 -16
- package/dist/services/field-services-context.js +0 -13
- package/dist/services/field-services-types.d.ts +0 -63
- package/dist/services/field-services-types.js +0 -1
- package/dist/widgets/diff-viewer/diff-modal.d.ts +0 -22
- package/dist/widgets/diff-viewer/diff-modal.js +0 -146
- package/dist/widgets/diff-viewer/diff-modal.module.js +0 -14
- package/dist/widgets/diff-viewer/diff-modal_module.css +0 -56
- package/dist/widgets/status-badge/status-badge.d.ts +0 -25
- package/dist/widgets/status-badge/status-badge.js +0 -35
- package/dist/widgets/status-badge/status-badge.module.js +0 -7
- package/dist/widgets/status-badge/status-badge_module.css +0 -20
- package/src/admin/group.module.css +0 -41
- package/src/admin/group.tsx +0 -40
- package/src/admin/row.module.css +0 -32
- package/src/admin/row.tsx +0 -33
- package/src/admin/tabs.module.css +0 -107
- package/src/admin/tabs.tsx +0 -82
- package/src/fields/array/array-field.module.css +0 -48
- package/src/fields/array/array-field.tsx +0 -266
- package/src/fields/blocks/blocks-field.module.css +0 -148
- package/src/fields/blocks/blocks-field.tsx +0 -312
- package/src/fields/checkbox/checkbox-field.module.css +0 -4
- package/src/fields/checkbox/checkbox-field.tsx +0 -54
- package/src/fields/column-formatter.tsx +0 -31
- package/src/fields/date-time-formatter.tsx +0 -22
- package/src/fields/datetime/datetime-field.module.css +0 -13
- package/src/fields/datetime/datetime-field.tsx +0 -54
- package/src/fields/draggable-context-menu.module.css +0 -127
- package/src/fields/draggable-context-menu.tsx +0 -85
- package/src/fields/field-helpers.ts +0 -69
- package/src/fields/field-renderer.module.css +0 -22
- package/src/fields/field-renderer.tsx +0 -288
- package/src/fields/file/file-field.module.css +0 -153
- package/src/fields/file/file-field.tsx +0 -271
- package/src/fields/file/file-upload-field.module.css +0 -101
- package/src/fields/file/file-upload-field.tsx +0 -183
- package/src/fields/group/group-field.module.css +0 -43
- package/src/fields/group/group-field.tsx +0 -84
- package/src/fields/image/image-field.module.css +0 -155
- package/src/fields/image/image-field.tsx +0 -291
- package/src/fields/image/image-upload-field.module.css +0 -123
- package/src/fields/image/image-upload-field.tsx +0 -270
- package/src/fields/local-date-time.tsx +0 -88
- package/src/fields/locale-badge.module.css +0 -37
- package/src/fields/locale-badge.tsx +0 -32
- package/src/fields/numerical/numerical-field.tsx +0 -114
- package/src/fields/relation/relation-display.module.css +0 -36
- package/src/fields/relation/relation-display.tsx +0 -130
- package/src/fields/relation/relation-field.module.css +0 -83
- package/src/fields/relation/relation-field.tsx +0 -206
- package/src/fields/relation/relation-picker.module.css +0 -168
- package/src/fields/relation/relation-picker.tsx +0 -325
- package/src/fields/relation/relation-summary.module.css +0 -55
- package/src/fields/relation/relation-summary.tsx +0 -123
- package/src/fields/select/select-field.module.css +0 -13
- package/src/fields/select/select-field.tsx +0 -61
- package/src/fields/sortable-item.module.css +0 -167
- package/src/fields/sortable-item.tsx +0 -101
- package/src/fields/text/text-field.module.css +0 -13
- package/src/fields/text/text-field.tsx +0 -146
- package/src/fields/text-area/text-area-field.module.css +0 -13
- package/src/fields/text-area/text-area-field.tsx +0 -147
- package/src/fields/use-field-change-handler.ts +0 -112
- package/src/forms/document-actions.module.css +0 -160
- package/src/forms/document-actions.tsx +0 -487
- package/src/forms/form-context.tsx +0 -704
- package/src/forms/form-renderer.module.css +0 -321
- package/src/forms/form-renderer.tsx +0 -888
- package/src/forms/navigation-guard.tsx +0 -98
- package/src/forms/path-widget.module.css +0 -41
- package/src/forms/path-widget.test.tsx +0 -217
- package/src/forms/path-widget.tsx +0 -181
- package/src/forms/upload-executor.ts +0 -190
- package/src/services/field-services-context.tsx +0 -35
- package/src/services/field-services-types.ts +0 -68
- package/src/widgets/diff-viewer/diff-modal.module.css +0 -79
- package/src/widgets/diff-viewer/diff-modal.tsx +0 -184
- package/src/widgets/status-badge/status-badge.module.css +0 -31
- package/src/widgets/status-badge/status-badge.tsx +0 -69
|
@@ -1,888 +0,0 @@
|
|
|
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 ReactNode, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|
12
|
-
|
|
13
|
-
import type {
|
|
14
|
-
CollectionAdminConfig,
|
|
15
|
-
Field,
|
|
16
|
-
GroupDefinition,
|
|
17
|
-
RowDefinition,
|
|
18
|
-
TabSetDefinition,
|
|
19
|
-
WorkflowStatus,
|
|
20
|
-
} from '@byline/core'
|
|
21
|
-
import cx from 'classnames'
|
|
22
|
-
|
|
23
|
-
import { AdminGroup } from '../admin/group'
|
|
24
|
-
import { AdminRow } from '../admin/row'
|
|
25
|
-
import { AdminTabs } from '../admin/tabs'
|
|
26
|
-
import { FieldRenderer } from '../fields/field-renderer'
|
|
27
|
-
import { LocalDateTime } from '../fields/local-date-time'
|
|
28
|
-
import { useBylineFieldServices } from '../services/field-services-context'
|
|
29
|
-
import { Alert, Button, ComboButton, Modal } from '../uikit.js'
|
|
30
|
-
import { DocumentActions, type DocumentActionsLocaleOption } from './document-actions'
|
|
31
|
-
import { FormProvider, useFieldValue, useFormContext } from './form-context'
|
|
32
|
-
import styles from './form-renderer.module.css'
|
|
33
|
-
import { useNavigationGuardAdapter } from './navigation-guard'
|
|
34
|
-
import { PathWidget } from './path-widget'
|
|
35
|
-
import { executeUploadsWithProgress } from './upload-executor'
|
|
36
|
-
import type { UseNavigationGuard } from './navigation-guard'
|
|
37
|
-
|
|
38
|
-
/** Metadata about a previously published version that is still live. */
|
|
39
|
-
export interface PublishedVersionInfo {
|
|
40
|
-
id: string
|
|
41
|
-
versionId: string
|
|
42
|
-
status: string
|
|
43
|
-
createdAt: string | Date
|
|
44
|
-
updatedAt: string | Date
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/** Props shared by both the public FormRenderer and its internal FormContent component. */
|
|
48
|
-
export interface FormRendererProps {
|
|
49
|
-
mode: 'create' | 'edit'
|
|
50
|
-
fields: Field[]
|
|
51
|
-
onSubmit: (data: any) => void
|
|
52
|
-
onCancel: () => void
|
|
53
|
-
onStatusChange?: (nextStatus: string) => Promise<void>
|
|
54
|
-
onUnpublish?: () => Promise<void>
|
|
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>
|
|
77
|
-
nextStatus?: WorkflowStatus
|
|
78
|
-
workflowStatuses?: WorkflowStatus[]
|
|
79
|
-
publishedVersion?: PublishedVersionInfo | null
|
|
80
|
-
initialData?: Record<string, any>
|
|
81
|
-
adminConfig?: CollectionAdminConfig
|
|
82
|
-
/**
|
|
83
|
-
* Name of the schema field to render as the live form heading.
|
|
84
|
-
* Sourced from `CollectionDefinition.useAsTitle` by the caller.
|
|
85
|
-
*/
|
|
86
|
-
useAsTitle?: string
|
|
87
|
-
/**
|
|
88
|
-
* Name of the schema field that initialises the system path.
|
|
89
|
-
* Sourced from `CollectionDefinition.useAsPath` by the caller. When
|
|
90
|
-
* present the path widget renders in the sidebar.
|
|
91
|
-
*/
|
|
92
|
-
useAsPath?: string
|
|
93
|
-
headingLabel?: string
|
|
94
|
-
headerSlot?: ReactNode
|
|
95
|
-
/** Collection path forwarded to upload-capable fields (e.g. `'media'`). */
|
|
96
|
-
collectionPath?: string
|
|
97
|
-
/** The active content locale — initialised from the route query string. */
|
|
98
|
-
initialLocale?: string
|
|
99
|
-
/** Called when the user picks a different content locale. */
|
|
100
|
-
onLocaleChange?: (locale: string) => void
|
|
101
|
-
/**
|
|
102
|
-
* Schema-mismatch warnings produced by a "best-effort" reconstruction
|
|
103
|
-
* of the document (`findById({ lenient: true })`). When present, the
|
|
104
|
-
* form renders an inline Alert telling the editor that fields from a
|
|
105
|
-
* previous schema have been dropped — saving the form will overwrite
|
|
106
|
-
* them with the new shape.
|
|
107
|
-
*/
|
|
108
|
-
restoreWarnings?: string[]
|
|
109
|
-
/**
|
|
110
|
-
* Default content locale used when no `initialLocale` is supplied and as the
|
|
111
|
-
* fallback inside `PathWidget`. Hosts typically pass their app-wide
|
|
112
|
-
* `i18n.content.defaultLocale`. Defaults to `'en'`.
|
|
113
|
-
*/
|
|
114
|
-
defaultLocale?: string
|
|
115
|
-
/**
|
|
116
|
-
* Framework-specific navigation guard hook.
|
|
117
|
-
* When provided, this overrides the adapter from `NavigationGuardProvider` context.
|
|
118
|
-
* If neither is set, a no-op `beforeunload`-only guard is used.
|
|
119
|
-
*/
|
|
120
|
-
useNavigationGuard?: UseNavigationGuard
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
const FormStatusDisplay = ({
|
|
124
|
-
initialData,
|
|
125
|
-
workflowStatuses,
|
|
126
|
-
publishedVersion,
|
|
127
|
-
onUnpublish,
|
|
128
|
-
}: {
|
|
129
|
-
initialData?: Record<string, any>
|
|
130
|
-
workflowStatuses?: WorkflowStatus[]
|
|
131
|
-
publishedVersion?: PublishedVersionInfo | null
|
|
132
|
-
onUnpublish?: () => Promise<void>
|
|
133
|
-
}) => {
|
|
134
|
-
const statusCode = initialData?.status
|
|
135
|
-
const statusLabel = workflowStatuses?.find((s) => s.name === statusCode)?.label ?? statusCode
|
|
136
|
-
// Single-status workflows (e.g. lookups) have no editorial lifecycle —
|
|
137
|
-
// suppress the "Status: …" cell since there is nothing meaningful to convey.
|
|
138
|
-
const showStatusCell = (workflowStatuses?.length ?? 0) > 1
|
|
139
|
-
|
|
140
|
-
return (
|
|
141
|
-
<div className={cx('byline-form-status', styles.status)}>
|
|
142
|
-
<div className={cx('byline-form-status-meta', styles['status-meta'])}>
|
|
143
|
-
{showStatusCell && (
|
|
144
|
-
<div className={cx('byline-form-status-cell', styles['status-cell'])}>
|
|
145
|
-
<span className={cx('byline-form-status-muted', styles['status-muted'])}>Status:</span>
|
|
146
|
-
<span className={cx('byline-form-status-trunc', styles['status-trunc'])}>
|
|
147
|
-
{statusLabel}
|
|
148
|
-
</span>
|
|
149
|
-
</div>
|
|
150
|
-
)}
|
|
151
|
-
|
|
152
|
-
{initialData?.updatedAt != null && (
|
|
153
|
-
<div className={cx('byline-form-status-cell', styles['status-cell'])}>
|
|
154
|
-
<span className={cx('byline-form-status-muted', styles['status-muted'])}>
|
|
155
|
-
Last modified:
|
|
156
|
-
</span>
|
|
157
|
-
<span className={cx('byline-form-status-trunc', styles['status-trunc'])}>
|
|
158
|
-
<LocalDateTime value={initialData.updatedAt} />
|
|
159
|
-
</span>
|
|
160
|
-
</div>
|
|
161
|
-
)}
|
|
162
|
-
|
|
163
|
-
{initialData?.createdAt != null && (
|
|
164
|
-
<div className={cx('byline-form-status-cell', styles['status-cell'])}>
|
|
165
|
-
<span className={cx('byline-form-status-muted', styles['status-muted'])}>Created:</span>
|
|
166
|
-
<span className={cx('byline-form-status-trunc', styles['status-trunc'])}>
|
|
167
|
-
<LocalDateTime value={initialData.createdAt} />
|
|
168
|
-
</span>
|
|
169
|
-
</div>
|
|
170
|
-
)}
|
|
171
|
-
</div>
|
|
172
|
-
|
|
173
|
-
{publishedVersion != null && (
|
|
174
|
-
<div className={cx('byline-form-status-published', styles['status-published'])}>
|
|
175
|
-
<span className={cx('byline-form-status-muted', styles['status-muted'])}>
|
|
176
|
-
A published version is currently live.{' '}
|
|
177
|
-
{publishedVersion.updatedAt ? (
|
|
178
|
-
<span>
|
|
179
|
-
Published on <LocalDateTime value={publishedVersion.updatedAt} />
|
|
180
|
-
</span>
|
|
181
|
-
) : (
|
|
182
|
-
''
|
|
183
|
-
)}
|
|
184
|
-
</span>
|
|
185
|
-
{onUnpublish && (
|
|
186
|
-
<>
|
|
187
|
-
{' '}
|
|
188
|
-
<button
|
|
189
|
-
type="button"
|
|
190
|
-
onClick={onUnpublish}
|
|
191
|
-
className={cx('byline-form-status-unpublish', styles['status-unpublish'])}
|
|
192
|
-
>
|
|
193
|
-
Unpublish
|
|
194
|
-
</button>
|
|
195
|
-
</>
|
|
196
|
-
)}
|
|
197
|
-
</div>
|
|
198
|
-
)}
|
|
199
|
-
</div>
|
|
200
|
-
)
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
/**
|
|
204
|
-
* Compute the primary and secondary status transitions for the ComboButton.
|
|
205
|
-
* - Primary: the main action (forward step), or the current status itself
|
|
206
|
-
* when the document has reached the final workflow step (terminal state).
|
|
207
|
-
* - Secondary: other available transitions to show as dropdown options.
|
|
208
|
-
* - isTerminal: true when the document is at the final workflow status —
|
|
209
|
-
* the primary button renders as a non-actionable indicator and all
|
|
210
|
-
* back-steps move into the dropdown.
|
|
211
|
-
*/
|
|
212
|
-
function computeStatusTransitions(
|
|
213
|
-
currentStatus: string | undefined,
|
|
214
|
-
workflowStatuses: WorkflowStatus[] | undefined,
|
|
215
|
-
nextStatus: WorkflowStatus | undefined
|
|
216
|
-
): {
|
|
217
|
-
primaryStatus: WorkflowStatus | undefined
|
|
218
|
-
secondaryStatuses: WorkflowStatus[]
|
|
219
|
-
isTerminal: boolean
|
|
220
|
-
} {
|
|
221
|
-
if (!workflowStatuses || workflowStatuses.length === 0 || !currentStatus) {
|
|
222
|
-
return { primaryStatus: nextStatus, secondaryStatuses: [], isTerminal: false }
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
// Single-status workflows (e.g. SINGLE_STATUS_WORKFLOW for lookups) have
|
|
226
|
-
// no transitions — short-circuit so the form shows only Close / Save.
|
|
227
|
-
if (workflowStatuses.length <= 1) {
|
|
228
|
-
return { primaryStatus: undefined, secondaryStatuses: [], isTerminal: false }
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
const currentIndex = workflowStatuses.findIndex((s) => s.name === currentStatus)
|
|
232
|
-
if (currentIndex === -1) {
|
|
233
|
-
return { primaryStatus: nextStatus, secondaryStatuses: [], isTerminal: false }
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
const isAtEnd = currentIndex === workflowStatuses.length - 1
|
|
237
|
-
const isAtStart = currentIndex === 0
|
|
238
|
-
|
|
239
|
-
// Collect all available target statuses
|
|
240
|
-
const availableTargets: WorkflowStatus[] = []
|
|
241
|
-
|
|
242
|
-
// Reset to first (if not at first)
|
|
243
|
-
if (!isAtStart && workflowStatuses[0]) {
|
|
244
|
-
availableTargets.push(workflowStatuses[0])
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
// Back one step (if not at start and the previous is not already the first)
|
|
248
|
-
if (currentIndex > 1 && workflowStatuses[currentIndex - 1]) {
|
|
249
|
-
availableTargets.push(workflowStatuses[currentIndex - 1])
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
// Forward one step (if not at end) - this is the nextStatus
|
|
253
|
-
if (!isAtEnd && workflowStatuses[currentIndex + 1]) {
|
|
254
|
-
availableTargets.push(workflowStatuses[currentIndex + 1])
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
if (isAtEnd) {
|
|
258
|
-
// Terminal state: the primary button is a non-actionable indicator of the
|
|
259
|
-
// current status; both back-steps (revert to previous / reset to first)
|
|
260
|
-
// are surfaced in the dropdown.
|
|
261
|
-
return {
|
|
262
|
-
primaryStatus: workflowStatuses[currentIndex],
|
|
263
|
-
secondaryStatuses: availableTargets,
|
|
264
|
-
isTerminal: true,
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
// Not at end: primary is the forward step (nextStatus)
|
|
269
|
-
return {
|
|
270
|
-
primaryStatus: nextStatus,
|
|
271
|
-
secondaryStatuses: availableTargets.filter((s) => s.name !== nextStatus?.name),
|
|
272
|
-
isTerminal: false,
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
const FormContent = ({
|
|
277
|
-
mode,
|
|
278
|
-
fields,
|
|
279
|
-
onSubmit,
|
|
280
|
-
onCancel,
|
|
281
|
-
onStatusChange,
|
|
282
|
-
onUnpublish,
|
|
283
|
-
onDelete,
|
|
284
|
-
onDuplicate,
|
|
285
|
-
onCopyToLocale,
|
|
286
|
-
contentLocales,
|
|
287
|
-
nextStatus,
|
|
288
|
-
workflowStatuses,
|
|
289
|
-
publishedVersion,
|
|
290
|
-
initialData,
|
|
291
|
-
adminConfig,
|
|
292
|
-
useAsTitle,
|
|
293
|
-
useAsPath,
|
|
294
|
-
headingLabel,
|
|
295
|
-
headerSlot,
|
|
296
|
-
collectionPath,
|
|
297
|
-
initialLocale,
|
|
298
|
-
onLocaleChange,
|
|
299
|
-
defaultLocale = 'en',
|
|
300
|
-
useNavigationGuard: useNavigationGuardProp,
|
|
301
|
-
restoreWarnings,
|
|
302
|
-
_activeTabBySet,
|
|
303
|
-
_onTabChange,
|
|
304
|
-
}: FormRendererProps & {
|
|
305
|
-
/** Lifted active-tab-per-set map from FormRenderer — preserves tab choices across locale-change remounts. */
|
|
306
|
-
_activeTabBySet?: Record<string, string>
|
|
307
|
-
_onTabChange?: (tabSetName: string, tabName: string) => void
|
|
308
|
-
}) => {
|
|
309
|
-
const {
|
|
310
|
-
getFieldValues,
|
|
311
|
-
runFieldHooks,
|
|
312
|
-
validateForm,
|
|
313
|
-
errors: initialErrors,
|
|
314
|
-
hasChanges: hasChangesFn,
|
|
315
|
-
resetHasChanges,
|
|
316
|
-
getPatches,
|
|
317
|
-
getSystemPath,
|
|
318
|
-
subscribeErrors,
|
|
319
|
-
subscribeMeta,
|
|
320
|
-
setFieldValue,
|
|
321
|
-
setFieldError,
|
|
322
|
-
getPendingUploads,
|
|
323
|
-
clearPendingUploads,
|
|
324
|
-
setFieldUploading,
|
|
325
|
-
} = useFormContext()
|
|
326
|
-
|
|
327
|
-
const [errors, setErrors] = useState(initialErrors)
|
|
328
|
-
const [hasChanges, setHasChanges] = useState(hasChangesFn())
|
|
329
|
-
const [statusBusy, setStatusBusy] = useState(false)
|
|
330
|
-
const [isUploading, setIsUploading] = useState(false)
|
|
331
|
-
const [contentLocale, setContentLocale] = useState(initialLocale ?? defaultLocale)
|
|
332
|
-
const { uploadField } = useBylineFieldServices()
|
|
333
|
-
|
|
334
|
-
// Sync contentLocale when the route re-fetches with a different locale.
|
|
335
|
-
useEffect(() => {
|
|
336
|
-
if (initialLocale) setContentLocale(initialLocale)
|
|
337
|
-
}, [initialLocale])
|
|
338
|
-
|
|
339
|
-
// ---------------------------------------------------------------------
|
|
340
|
-
// Layout primitives + lookup tables.
|
|
341
|
-
//
|
|
342
|
-
// Built once per render from `adminConfig`. The validator at startup
|
|
343
|
-
// guarantees every reachable name resolves and every schema field is
|
|
344
|
-
// placed at most once, so render-time lookups are unguarded.
|
|
345
|
-
// ---------------------------------------------------------------------
|
|
346
|
-
|
|
347
|
-
const fieldByName = useMemo(() => {
|
|
348
|
-
const map = new Map<string, Field>()
|
|
349
|
-
for (const field of fields) {
|
|
350
|
-
if ('name' in field) map.set(field.name, field)
|
|
351
|
-
}
|
|
352
|
-
return map
|
|
353
|
-
}, [fields])
|
|
354
|
-
|
|
355
|
-
const tabSetByName = useMemo(() => {
|
|
356
|
-
const map = new Map<string, TabSetDefinition>()
|
|
357
|
-
for (const set of adminConfig?.tabSets ?? []) map.set(set.name, set)
|
|
358
|
-
return map
|
|
359
|
-
}, [adminConfig])
|
|
360
|
-
|
|
361
|
-
const rowByName = useMemo(() => {
|
|
362
|
-
const map = new Map<string, RowDefinition>()
|
|
363
|
-
for (const row of adminConfig?.rows ?? []) map.set(row.name, row)
|
|
364
|
-
return map
|
|
365
|
-
}, [adminConfig])
|
|
366
|
-
|
|
367
|
-
const groupByName = useMemo(() => {
|
|
368
|
-
const map = new Map<string, GroupDefinition>()
|
|
369
|
-
for (const group of adminConfig?.groups ?? []) map.set(group.name, group)
|
|
370
|
-
return map
|
|
371
|
-
}, [adminConfig])
|
|
372
|
-
|
|
373
|
-
// When `layout` is omitted, synthesise main = all schema fields in order.
|
|
374
|
-
const layout = useMemo(() => {
|
|
375
|
-
if (adminConfig?.layout) return adminConfig.layout
|
|
376
|
-
return { main: fields.filter((f) => 'name' in f).map((f) => (f as { name: string }).name) }
|
|
377
|
-
}, [adminConfig, fields])
|
|
378
|
-
|
|
379
|
-
// Reverse index: schema field name → which tab set + tab it lives in.
|
|
380
|
-
// Powers per-tab-set error badge counts. Fields not under any tab set
|
|
381
|
-
// (e.g. raw-field placement directly in `layout.main`) are absent from
|
|
382
|
-
// this map.
|
|
383
|
-
const fieldToTabPath = useMemo(() => {
|
|
384
|
-
const map = new Map<string, { tabSetName: string; tabName: string }>()
|
|
385
|
-
const visit = (
|
|
386
|
-
names: readonly string[],
|
|
387
|
-
tabSetName: string,
|
|
388
|
-
tabName: string,
|
|
389
|
-
seen: Set<string>
|
|
390
|
-
) => {
|
|
391
|
-
for (const name of names) {
|
|
392
|
-
if (fieldByName.has(name)) {
|
|
393
|
-
map.set(name, { tabSetName, tabName })
|
|
394
|
-
} else if (seen.has(name)) {
|
|
395
|
-
} else if (rowByName.has(name)) {
|
|
396
|
-
const row = rowByName.get(name)!
|
|
397
|
-
const next = new Set(seen).add(name)
|
|
398
|
-
visit(row.fields, tabSetName, tabName, next)
|
|
399
|
-
} else if (groupByName.has(name)) {
|
|
400
|
-
const group = groupByName.get(name)!
|
|
401
|
-
const next = new Set(seen).add(name)
|
|
402
|
-
visit(group.fields, tabSetName, tabName, next)
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
for (const set of adminConfig?.tabSets ?? []) {
|
|
407
|
-
for (const tab of set.tabs) {
|
|
408
|
-
visit(tab.fields, set.name, tab.name, new Set())
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
return map
|
|
412
|
-
}, [adminConfig, fieldByName, rowByName, groupByName])
|
|
413
|
-
|
|
414
|
-
// ---------------------------------------------------------------------
|
|
415
|
-
// Active-tab state — one tab name per declared tab set.
|
|
416
|
-
// Lifted into FormRenderer via `_activeTabBySet` / `_onTabChange` so the
|
|
417
|
-
// user's tab choices survive the locale-change remount triggered by
|
|
418
|
-
// FormProvider's `key` prop.
|
|
419
|
-
// ---------------------------------------------------------------------
|
|
420
|
-
|
|
421
|
-
const tabSets = adminConfig?.tabSets ?? []
|
|
422
|
-
|
|
423
|
-
const initialActiveTabBySet = useMemo<Record<string, string>>(() => {
|
|
424
|
-
const result: Record<string, string> = {}
|
|
425
|
-
for (const set of tabSets) {
|
|
426
|
-
const saved = _activeTabBySet?.[set.name]
|
|
427
|
-
if (saved && set.tabs.some((t) => t.name === saved)) {
|
|
428
|
-
result[set.name] = saved
|
|
429
|
-
} else {
|
|
430
|
-
result[set.name] = set.tabs[0]?.name ?? ''
|
|
431
|
-
}
|
|
432
|
-
}
|
|
433
|
-
return result
|
|
434
|
-
// initial-only; subsequent updates flow through setActiveTabBySet.
|
|
435
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
436
|
-
}, [tabSets, _activeTabBySet])
|
|
437
|
-
|
|
438
|
-
const [activeTabBySet, setActiveTabBySet] =
|
|
439
|
-
useState<Record<string, string>>(initialActiveTabBySet)
|
|
440
|
-
|
|
441
|
-
const handleTabChange = useCallback(
|
|
442
|
-
(tabSetName: string, tabName: string) => {
|
|
443
|
-
setActiveTabBySet((prev) => ({ ...prev, [tabSetName]: tabName }))
|
|
444
|
-
_onTabChange?.(tabSetName, tabName)
|
|
445
|
-
},
|
|
446
|
-
[_onTabChange]
|
|
447
|
-
)
|
|
448
|
-
|
|
449
|
-
// Track live form data so TabDefinition.condition functions can react to
|
|
450
|
-
// field changes. Re-evaluated per keystroke via the meta-subscribe loop.
|
|
451
|
-
const [formData, setFormData] = useState<Record<string, any>>(() => getFieldValues())
|
|
452
|
-
|
|
453
|
-
// Live document heading — tracks the useAsTitle field as the user types
|
|
454
|
-
const liveTitle = useFieldValue<string>(useAsTitle ?? '')
|
|
455
|
-
const heading =
|
|
456
|
-
liveTitle ||
|
|
457
|
-
(headingLabel
|
|
458
|
-
? `${mode === 'create' ? 'Create' : 'Edit'} ${headingLabel}`
|
|
459
|
-
: mode === 'create'
|
|
460
|
-
? 'Create'
|
|
461
|
-
: 'Edit')
|
|
462
|
-
|
|
463
|
-
// Navigation guard — block router navigation and browser unload when dirty.
|
|
464
|
-
// The guard hook is injected by the consuming framework (prop > context > no-op fallback).
|
|
465
|
-
const guardFromContext = useNavigationGuardAdapter()
|
|
466
|
-
const useGuard = useNavigationGuardProp ?? guardFromContext
|
|
467
|
-
const guard = useGuard(hasChanges)
|
|
468
|
-
|
|
469
|
-
// Compute available status transitions
|
|
470
|
-
const currentStatus = initialData?.status
|
|
471
|
-
const { primaryStatus, secondaryStatuses, isTerminal } = computeStatusTransitions(
|
|
472
|
-
currentStatus,
|
|
473
|
-
workflowStatuses,
|
|
474
|
-
nextStatus
|
|
475
|
-
)
|
|
476
|
-
|
|
477
|
-
useEffect(() => {
|
|
478
|
-
return subscribeErrors((newErrors) => setErrors(newErrors))
|
|
479
|
-
}, [subscribeErrors])
|
|
480
|
-
|
|
481
|
-
useEffect(() => {
|
|
482
|
-
return subscribeMeta(() => setHasChanges(hasChangesFn()))
|
|
483
|
-
}, [subscribeMeta, hasChangesFn])
|
|
484
|
-
|
|
485
|
-
// Keep formData in sync for evaluating TabDefinition.condition functions
|
|
486
|
-
useEffect(() => {
|
|
487
|
-
return subscribeMeta(() => setFormData(getFieldValues()))
|
|
488
|
-
}, [subscribeMeta, getFieldValues])
|
|
489
|
-
|
|
490
|
-
const handleCancel = () => {
|
|
491
|
-
if (onCancel && typeof onCancel === 'function') {
|
|
492
|
-
onCancel()
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
const handleSubmit = (e: React.SubmitEvent<HTMLFormElement>) => {
|
|
497
|
-
e.preventDefault()
|
|
498
|
-
|
|
499
|
-
// Run field-level beforeValidate hooks (submit-time), then validate
|
|
500
|
-
void (async () => {
|
|
501
|
-
const hookErrors = await runFieldHooks(fields)
|
|
502
|
-
const formErrors = validateForm(fields)
|
|
503
|
-
const allErrors = [...hookErrors, ...formErrors]
|
|
504
|
-
|
|
505
|
-
if (allErrors.length > 0) {
|
|
506
|
-
console.error('Form validation failed:', allErrors)
|
|
507
|
-
return
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
// Execute any pending uploads before submitting
|
|
511
|
-
const pendingUploads = getPendingUploads()
|
|
512
|
-
if (pendingUploads.size > 0) {
|
|
513
|
-
setIsUploading(true)
|
|
514
|
-
try {
|
|
515
|
-
const uploadResult = await executeUploadsWithProgress(
|
|
516
|
-
pendingUploads,
|
|
517
|
-
uploadField,
|
|
518
|
-
({ fieldPath, status }) => {
|
|
519
|
-
setFieldUploading(fieldPath, status === 'uploading')
|
|
520
|
-
}
|
|
521
|
-
)
|
|
522
|
-
|
|
523
|
-
// Check for upload errors
|
|
524
|
-
if (!uploadResult.allSucceeded) {
|
|
525
|
-
// Set field-level errors for failed uploads
|
|
526
|
-
for (const [fieldPath, errorMessage] of uploadResult.errors.entries()) {
|
|
527
|
-
setFieldError(fieldPath, `Upload failed: ${errorMessage}`)
|
|
528
|
-
}
|
|
529
|
-
console.error('One or more uploads failed:', uploadResult.errors)
|
|
530
|
-
setIsUploading(false)
|
|
531
|
-
return
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
// Replace pending StoredFileValues with real ones in form data
|
|
535
|
-
for (const [fieldPath, storedFile] of uploadResult.successful.entries()) {
|
|
536
|
-
setFieldValue(fieldPath, storedFile)
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
// Clear pending uploads (blob URLs already revoked by clearPendingUploads)
|
|
540
|
-
clearPendingUploads()
|
|
541
|
-
} catch (err) {
|
|
542
|
-
console.error('Upload execution error:', err)
|
|
543
|
-
setIsUploading(false)
|
|
544
|
-
return
|
|
545
|
-
}
|
|
546
|
-
setIsUploading(false)
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
const data = getFieldValues()
|
|
550
|
-
const patches = getPatches()
|
|
551
|
-
const systemPath = getSystemPath()
|
|
552
|
-
|
|
553
|
-
if (onSubmit && typeof onSubmit === 'function') {
|
|
554
|
-
onSubmit({ data, patches, systemPath })
|
|
555
|
-
resetHasChanges()
|
|
556
|
-
}
|
|
557
|
-
})()
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
// Per-tab-set error counts: { [tabSetName]: { [tabName]: count } }.
|
|
561
|
-
// Each <Tabs> bar consumes its own slice.
|
|
562
|
-
const tabErrorCountsBySet = useMemo<Record<string, Record<string, number>>>(() => {
|
|
563
|
-
const result: Record<string, Record<string, number>> = {}
|
|
564
|
-
for (const err of errors) {
|
|
565
|
-
const path = fieldToTabPath.get(err.field)
|
|
566
|
-
if (!path) continue
|
|
567
|
-
result[path.tabSetName] ??= {}
|
|
568
|
-
result[path.tabSetName]![path.tabName] = (result[path.tabSetName]?.[path.tabName] ?? 0) + 1
|
|
569
|
-
}
|
|
570
|
-
return result
|
|
571
|
-
}, [errors, fieldToTabPath])
|
|
572
|
-
|
|
573
|
-
// -------------------------------------------------------------------
|
|
574
|
-
// Layout walk — recursively dispatches each name in a region to the
|
|
575
|
-
// appropriate primitive renderer or to <FieldRenderer>.
|
|
576
|
-
// -------------------------------------------------------------------
|
|
577
|
-
|
|
578
|
-
const renderField = (fieldName: string): ReactNode => {
|
|
579
|
-
const field = fieldByName.get(fieldName)
|
|
580
|
-
if (!field) return null
|
|
581
|
-
return (
|
|
582
|
-
<FieldRenderer
|
|
583
|
-
key={field.name}
|
|
584
|
-
field={field}
|
|
585
|
-
defaultValue={initialData?.fields?.[field.name]}
|
|
586
|
-
collectionPath={collectionPath}
|
|
587
|
-
contentLocale={contentLocale}
|
|
588
|
-
components={adminConfig?.fields?.[field.name]?.components}
|
|
589
|
-
editor={adminConfig?.fields?.[field.name]?.editor}
|
|
590
|
-
/>
|
|
591
|
-
)
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
const renderItem = (name: string): ReactNode => {
|
|
595
|
-
const tabSet = tabSetByName.get(name)
|
|
596
|
-
if (tabSet) return renderTabSet(tabSet)
|
|
597
|
-
|
|
598
|
-
const group = groupByName.get(name)
|
|
599
|
-
if (group) return renderGroup(group)
|
|
600
|
-
|
|
601
|
-
const row = rowByName.get(name)
|
|
602
|
-
if (row) return renderRow(row)
|
|
603
|
-
|
|
604
|
-
return renderField(name)
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
const renderRow = (row: RowDefinition): ReactNode => (
|
|
608
|
-
<AdminRow key={`row:${row.name}`}>{row.fields.map((name) => renderField(name))}</AdminRow>
|
|
609
|
-
)
|
|
610
|
-
|
|
611
|
-
const renderGroup = (group: GroupDefinition): ReactNode => (
|
|
612
|
-
<AdminGroup key={`group:${group.name}`} label={group.label}>
|
|
613
|
-
{group.fields.map((name) => renderItem(name))}
|
|
614
|
-
</AdminGroup>
|
|
615
|
-
)
|
|
616
|
-
|
|
617
|
-
const renderTabSet = (set: TabSetDefinition): ReactNode => {
|
|
618
|
-
const visibleTabs = set.tabs.filter((tab) => !tab.condition || tab.condition(formData))
|
|
619
|
-
const requested = activeTabBySet[set.name] ?? ''
|
|
620
|
-
const resolvedActive =
|
|
621
|
-
visibleTabs.length > 0 && !visibleTabs.some((t) => t.name === requested)
|
|
622
|
-
? (visibleTabs[0]?.name ?? requested)
|
|
623
|
-
: requested
|
|
624
|
-
const activeTab = visibleTabs.find((t) => t.name === resolvedActive)
|
|
625
|
-
|
|
626
|
-
return (
|
|
627
|
-
<div key={`tabset:${set.name}`} className={cx('byline-form-tabset', styles.tabset)}>
|
|
628
|
-
{visibleTabs.length > 0 && (
|
|
629
|
-
<AdminTabs
|
|
630
|
-
tabs={visibleTabs}
|
|
631
|
-
activeTab={resolvedActive}
|
|
632
|
-
onChange={(tabName) => handleTabChange(set.name, tabName)}
|
|
633
|
-
errorCounts={tabErrorCountsBySet[set.name]}
|
|
634
|
-
className={cx('byline-form-tabset-tabs', styles['tabset-tabs'])}
|
|
635
|
-
/>
|
|
636
|
-
)}
|
|
637
|
-
{activeTab && (
|
|
638
|
-
<div className={cx('byline-form-tabset-fields', styles['tabset-fields'])}>
|
|
639
|
-
{activeTab.fields.map((name) => renderItem(name))}
|
|
640
|
-
</div>
|
|
641
|
-
)}
|
|
642
|
-
</div>
|
|
643
|
-
)
|
|
644
|
-
}
|
|
645
|
-
|
|
646
|
-
return (
|
|
647
|
-
<form noValidate onSubmit={handleSubmit} className={cx('byline-form', styles.form)}>
|
|
648
|
-
<div className={cx('byline-form-heading-row', styles['heading-row'])}>
|
|
649
|
-
<h1 className={cx('byline-form-heading', styles.heading)}>{heading}</h1>
|
|
650
|
-
{headerSlot}
|
|
651
|
-
</div>
|
|
652
|
-
<div className={cx('byline-form-status-bar', styles['status-bar'])}>
|
|
653
|
-
<FormStatusDisplay
|
|
654
|
-
initialData={initialData}
|
|
655
|
-
workflowStatuses={workflowStatuses}
|
|
656
|
-
publishedVersion={publishedVersion}
|
|
657
|
-
onUnpublish={onUnpublish}
|
|
658
|
-
/>
|
|
659
|
-
<div className={cx('byline-form-actions', styles.actions)}>
|
|
660
|
-
<Button
|
|
661
|
-
className={cx('byline-form-actions-button', styles['actions-button'])}
|
|
662
|
-
size="sm"
|
|
663
|
-
intent="noeffect"
|
|
664
|
-
type="button"
|
|
665
|
-
onClick={handleCancel}
|
|
666
|
-
>
|
|
667
|
-
{hasChanges === false ? 'Close' : 'Cancel'}
|
|
668
|
-
</Button>
|
|
669
|
-
<Button
|
|
670
|
-
className={cx('byline-form-actions-button', styles['actions-button'])}
|
|
671
|
-
size="sm"
|
|
672
|
-
type="submit"
|
|
673
|
-
disabled={hasChanges === false || isUploading}
|
|
674
|
-
>
|
|
675
|
-
{isUploading ? 'Uploading…' : 'Save'}
|
|
676
|
-
</Button>
|
|
677
|
-
{primaryStatus && onStatusChange && (
|
|
678
|
-
<div className={cx('byline-form-actions-status-wrap', styles['actions-status-wrap'])}>
|
|
679
|
-
<ComboButton
|
|
680
|
-
buttonClassName={cx(
|
|
681
|
-
'byline-form-actions-combo-button',
|
|
682
|
-
styles['actions-combo-button']
|
|
683
|
-
)}
|
|
684
|
-
triggerClassName={cx(
|
|
685
|
-
'byline-form-actions-combo-trigger',
|
|
686
|
-
styles['actions-combo-trigger']
|
|
687
|
-
)}
|
|
688
|
-
options={secondaryStatuses.map((s) => ({
|
|
689
|
-
label: isTerminal
|
|
690
|
-
? `Revert to ${s.label ?? s.name}`
|
|
691
|
-
: (s.verb ?? s.label ?? s.name),
|
|
692
|
-
value: s.name,
|
|
693
|
-
}))}
|
|
694
|
-
sideOffset={5}
|
|
695
|
-
size="sm"
|
|
696
|
-
type="button"
|
|
697
|
-
intent={isTerminal ? 'info' : 'success'}
|
|
698
|
-
disabled={statusBusy}
|
|
699
|
-
onOptionSelect={async (value: string) => {
|
|
700
|
-
setStatusBusy(true)
|
|
701
|
-
try {
|
|
702
|
-
await onStatusChange(value)
|
|
703
|
-
} finally {
|
|
704
|
-
setStatusBusy(false)
|
|
705
|
-
}
|
|
706
|
-
}}
|
|
707
|
-
onButtonClick={
|
|
708
|
-
isTerminal
|
|
709
|
-
? undefined
|
|
710
|
-
: async () => {
|
|
711
|
-
setStatusBusy(true)
|
|
712
|
-
try {
|
|
713
|
-
await onStatusChange(primaryStatus.name)
|
|
714
|
-
} finally {
|
|
715
|
-
setStatusBusy(false)
|
|
716
|
-
}
|
|
717
|
-
}
|
|
718
|
-
}
|
|
719
|
-
>
|
|
720
|
-
{statusBusy
|
|
721
|
-
? '...'
|
|
722
|
-
: isTerminal
|
|
723
|
-
? (primaryStatus.label ?? primaryStatus.name)
|
|
724
|
-
: (primaryStatus.verb ?? primaryStatus.label ?? primaryStatus.name)}
|
|
725
|
-
</ComboButton>
|
|
726
|
-
</div>
|
|
727
|
-
)}
|
|
728
|
-
<DocumentActions
|
|
729
|
-
publishedVersion={publishedVersion}
|
|
730
|
-
onUnpublish={onUnpublish}
|
|
731
|
-
onDelete={onDelete}
|
|
732
|
-
onDuplicate={onDuplicate}
|
|
733
|
-
sourceTitle={
|
|
734
|
-
useAsTitle != null && initialData != null
|
|
735
|
-
? ((initialData as Record<string, unknown>)[useAsTitle] as
|
|
736
|
-
| string
|
|
737
|
-
| null
|
|
738
|
-
| undefined)
|
|
739
|
-
: null
|
|
740
|
-
}
|
|
741
|
-
onCopyToLocale={onCopyToLocale}
|
|
742
|
-
sourceLocale={contentLocale}
|
|
743
|
-
contentLocales={contentLocales}
|
|
744
|
-
/>
|
|
745
|
-
</div>
|
|
746
|
-
</div>
|
|
747
|
-
{restoreWarnings && restoreWarnings.length > 0 && (
|
|
748
|
-
<Alert
|
|
749
|
-
className="m-0 mt-4"
|
|
750
|
-
intent="warning"
|
|
751
|
-
icon={true}
|
|
752
|
-
close={false}
|
|
753
|
-
title="This document was loaded with a best-effort reconstruction"
|
|
754
|
-
>
|
|
755
|
-
<p>
|
|
756
|
-
The collection schema has changed since this document was last saved, and{' '}
|
|
757
|
-
{restoreWarnings.length === 1
|
|
758
|
-
? '1 field could not be restored against the current shape.'
|
|
759
|
-
: `${restoreWarnings.length} fields could not be restored against the current shape.`}{' '}
|
|
760
|
-
The form below shows only the fields that match the new schema. Saving will overwrite
|
|
761
|
-
the document with the new shape — any data that did not match will be lost. To preserve
|
|
762
|
-
it, copy what you need before saving, or delete this document and recreate it. Errors:
|
|
763
|
-
</p>
|
|
764
|
-
<ul>
|
|
765
|
-
{restoreWarnings.map((w) => (
|
|
766
|
-
<li key={w}>{w}</li>
|
|
767
|
-
))}
|
|
768
|
-
</ul>
|
|
769
|
-
</Alert>
|
|
770
|
-
)}
|
|
771
|
-
<div className={cx('byline-form-layout', styles.layout)}>
|
|
772
|
-
<div className={cx('byline-form-content', styles.content)}>
|
|
773
|
-
{layout.main.map((name) => renderItem(name))}
|
|
774
|
-
</div>
|
|
775
|
-
<div className={cx('byline-form-sidebar', styles.sidebar)}>
|
|
776
|
-
{(useAsPath ||
|
|
777
|
-
(typeof initialData?.path === 'string' && initialData.path.length > 0)) && (
|
|
778
|
-
<PathWidget
|
|
779
|
-
useAsPath={useAsPath}
|
|
780
|
-
collectionPath={collectionPath ?? ''}
|
|
781
|
-
defaultLocale={defaultLocale}
|
|
782
|
-
activeLocale={contentLocale}
|
|
783
|
-
mode={mode}
|
|
784
|
-
/>
|
|
785
|
-
)}
|
|
786
|
-
{(layout.sidebar ?? []).map((name) => renderItem(name))}
|
|
787
|
-
</div>
|
|
788
|
-
</div>
|
|
789
|
-
{guard.isBlocked && (
|
|
790
|
-
<Modal isOpen={true} closeOnOverlayClick={false} onDismiss={guard.stay}>
|
|
791
|
-
<Modal.Container style={{ maxWidth: '460px' }}>
|
|
792
|
-
<Modal.Header
|
|
793
|
-
className={cx('byline-form-guard-modal-head', styles['guard-modal-head'])}
|
|
794
|
-
>
|
|
795
|
-
<h3 className={cx('byline-form-guard-modal-title', styles['guard-modal-title'])}>
|
|
796
|
-
Leave without saving?
|
|
797
|
-
</h3>
|
|
798
|
-
</Modal.Header>
|
|
799
|
-
<Modal.Content>
|
|
800
|
-
<p className={cx('byline-form-guard-modal-text', styles['guard-modal-text'])}>
|
|
801
|
-
Your changes have not been saved. If you leave now, you will lose your changes.
|
|
802
|
-
</p>
|
|
803
|
-
</Modal.Content>
|
|
804
|
-
<Modal.Actions>
|
|
805
|
-
<Button size="sm" intent="noeffect" type="button" onClick={guard.stay}>
|
|
806
|
-
Stay on this page
|
|
807
|
-
</Button>
|
|
808
|
-
<Button size="sm" intent="danger" type="button" onClick={guard.proceed}>
|
|
809
|
-
Leave anyway
|
|
810
|
-
</Button>
|
|
811
|
-
</Modal.Actions>
|
|
812
|
-
</Modal.Container>
|
|
813
|
-
</Modal>
|
|
814
|
-
)}
|
|
815
|
-
</form>
|
|
816
|
-
)
|
|
817
|
-
}
|
|
818
|
-
|
|
819
|
-
export const FormRenderer = ({
|
|
820
|
-
mode,
|
|
821
|
-
fields,
|
|
822
|
-
onSubmit,
|
|
823
|
-
onCancel,
|
|
824
|
-
onStatusChange,
|
|
825
|
-
onUnpublish,
|
|
826
|
-
onDelete,
|
|
827
|
-
onDuplicate,
|
|
828
|
-
onCopyToLocale,
|
|
829
|
-
contentLocales,
|
|
830
|
-
nextStatus,
|
|
831
|
-
workflowStatuses,
|
|
832
|
-
publishedVersion,
|
|
833
|
-
initialData,
|
|
834
|
-
adminConfig,
|
|
835
|
-
useAsTitle,
|
|
836
|
-
useAsPath,
|
|
837
|
-
headingLabel,
|
|
838
|
-
headerSlot,
|
|
839
|
-
collectionPath,
|
|
840
|
-
initialLocale,
|
|
841
|
-
onLocaleChange,
|
|
842
|
-
defaultLocale,
|
|
843
|
-
useNavigationGuard,
|
|
844
|
-
restoreWarnings,
|
|
845
|
-
}: FormRendererProps) => {
|
|
846
|
-
// Persists per-tab-set active tab across locale-change remounts of FormContent.
|
|
847
|
-
// useRef so mutations never trigger a re-render of FormRenderer itself.
|
|
848
|
-
const savedTabsRef = useRef<Record<string, string>>({})
|
|
849
|
-
|
|
850
|
-
return (
|
|
851
|
-
<FormProvider
|
|
852
|
-
key={`${initialLocale ?? 'default'}-${initialData?.versionId ?? ''}`}
|
|
853
|
-
initialData={initialData}
|
|
854
|
-
>
|
|
855
|
-
<FormContent
|
|
856
|
-
mode={mode}
|
|
857
|
-
fields={fields}
|
|
858
|
-
onSubmit={onSubmit}
|
|
859
|
-
onCancel={onCancel}
|
|
860
|
-
onStatusChange={onStatusChange}
|
|
861
|
-
onUnpublish={onUnpublish}
|
|
862
|
-
onDelete={onDelete}
|
|
863
|
-
onDuplicate={onDuplicate}
|
|
864
|
-
onCopyToLocale={onCopyToLocale}
|
|
865
|
-
contentLocales={contentLocales}
|
|
866
|
-
nextStatus={nextStatus}
|
|
867
|
-
workflowStatuses={workflowStatuses}
|
|
868
|
-
publishedVersion={publishedVersion}
|
|
869
|
-
initialData={initialData}
|
|
870
|
-
adminConfig={adminConfig}
|
|
871
|
-
useAsTitle={useAsTitle}
|
|
872
|
-
useAsPath={useAsPath}
|
|
873
|
-
headingLabel={headingLabel}
|
|
874
|
-
headerSlot={headerSlot}
|
|
875
|
-
collectionPath={collectionPath}
|
|
876
|
-
initialLocale={initialLocale}
|
|
877
|
-
onLocaleChange={onLocaleChange}
|
|
878
|
-
defaultLocale={defaultLocale}
|
|
879
|
-
useNavigationGuard={useNavigationGuard}
|
|
880
|
-
restoreWarnings={restoreWarnings}
|
|
881
|
-
_activeTabBySet={savedTabsRef.current}
|
|
882
|
-
_onTabChange={(tabSetName, tabName) => {
|
|
883
|
-
savedTabsRef.current = { ...savedTabsRef.current, [tabSetName]: tabName }
|
|
884
|
-
}}
|
|
885
|
-
/>
|
|
886
|
-
</FormProvider>
|
|
887
|
-
)
|
|
888
|
-
}
|