@byline/ui 2.5.1 → 2.6.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/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/uikit.d.ts +1 -0
- package/dist/uikit.js +1 -0
- package/package.json +2 -8
- package/src/dnd/draggable-sortable/demo/draggable-list-demo.tsx +1 -1
- package/src/react.ts +20 -68
- 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,271 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* This Source Code is subject to the terms of the Mozilla Public
|
|
3
|
-
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
4
|
-
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
5
|
-
*
|
|
6
|
-
* Copyright (c) Infonomic Company Limited
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import {
|
|
10
|
-
type FileField as FieldType,
|
|
11
|
-
isPendingStoredFileValue,
|
|
12
|
-
type StoredFileValue,
|
|
13
|
-
} from '@byline/core'
|
|
14
|
-
import cx from 'classnames'
|
|
15
|
-
|
|
16
|
-
import { IconButton } from '../../components/button/icon-button.js'
|
|
17
|
-
import {
|
|
18
|
-
useFieldError,
|
|
19
|
-
useFieldValue,
|
|
20
|
-
useFormContext,
|
|
21
|
-
useIsDirty,
|
|
22
|
-
useIsFieldUploading,
|
|
23
|
-
} from '../../forms/form-context'
|
|
24
|
-
import { CloseIcon } from '../../icons/close-icon.js'
|
|
25
|
-
import { DocumentIcon } from '../../icons/document-icon.js'
|
|
26
|
-
import { DownloadIcon } from '../../icons/download-icon.js'
|
|
27
|
-
import { VideoIcon } from '../../icons/video-icon.js'
|
|
28
|
-
import { ErrorText, HelpText, Label, LoaderRing } from '../../uikit.js'
|
|
29
|
-
import { useFieldChangeHandler } from '../use-field-change-handler'
|
|
30
|
-
import styles from './file-field.module.css'
|
|
31
|
-
import { FileUploadField } from './file-upload-field'
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Trigger a download via a temporary anchor. Mirrors the helper in
|
|
35
|
-
* `image-lightbox.tsx`: same-origin URLs respect the `download` attribute and
|
|
36
|
-
* save with the suggested filename; cross-origin URLs without CORS headers
|
|
37
|
-
* fall through to navigation in a new tab, where the user can right-click
|
|
38
|
-
* Save As.
|
|
39
|
-
*/
|
|
40
|
-
function triggerDownload(url: string, filename?: string) {
|
|
41
|
-
if (typeof document === 'undefined') return
|
|
42
|
-
const a = document.createElement('a')
|
|
43
|
-
a.href = url
|
|
44
|
-
if (filename) a.download = filename
|
|
45
|
-
a.target = '_blank'
|
|
46
|
-
a.rel = 'noreferrer'
|
|
47
|
-
document.body.appendChild(a)
|
|
48
|
-
a.click()
|
|
49
|
-
document.body.removeChild(a)
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
interface FileFieldProps {
|
|
53
|
-
field: FieldType
|
|
54
|
-
/** Collection path required to call the /upload endpoint. */
|
|
55
|
-
collectionPath?: string
|
|
56
|
-
value?: StoredFileValue | null
|
|
57
|
-
defaultValue?: StoredFileValue | null
|
|
58
|
-
onChange?: (value: StoredFileValue | null) => void
|
|
59
|
-
path?: string
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export const FileField = ({
|
|
63
|
-
field,
|
|
64
|
-
collectionPath,
|
|
65
|
-
value,
|
|
66
|
-
defaultValue,
|
|
67
|
-
onChange: _onChange,
|
|
68
|
-
path,
|
|
69
|
-
}: FileFieldProps) => {
|
|
70
|
-
const fieldPath = path ?? field.name
|
|
71
|
-
const fieldError = useFieldError(fieldPath)
|
|
72
|
-
const isDirty = useIsDirty(fieldPath)
|
|
73
|
-
const fieldValue = useFieldValue<StoredFileValue | null | undefined>(fieldPath)
|
|
74
|
-
const isUploading = useIsFieldUploading(fieldPath)
|
|
75
|
-
const { removePendingUpload } = useFormContext()
|
|
76
|
-
|
|
77
|
-
const handleChange = useFieldChangeHandler(field, fieldPath)
|
|
78
|
-
|
|
79
|
-
// Mirror the image-field rule: once the field has been touched, the form
|
|
80
|
-
// value is authoritative (even when null, so a click-to-remove sticks);
|
|
81
|
-
// otherwise fall back to props.
|
|
82
|
-
const incomingValue = isDirty
|
|
83
|
-
? (fieldValue ?? null)
|
|
84
|
-
: (value ?? fieldValue ?? defaultValue ?? null)
|
|
85
|
-
|
|
86
|
-
const isPending = isPendingStoredFileValue(incomingValue)
|
|
87
|
-
|
|
88
|
-
// Legacy placeholder shape — kept for backwards compatibility with older
|
|
89
|
-
// seed data, matching the image-field check.
|
|
90
|
-
const isOldPlaceholder = (v: unknown): boolean => {
|
|
91
|
-
if (!v || typeof v !== 'object') return false
|
|
92
|
-
const maybe = v as Partial<StoredFileValue>
|
|
93
|
-
return maybe.storageProvider === 'placeholder' && maybe.storagePath === 'pending'
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
const showUploadWidget = incomingValue == null || isOldPlaceholder(incomingValue)
|
|
97
|
-
|
|
98
|
-
const handleRemove = () => {
|
|
99
|
-
if (isPending) {
|
|
100
|
-
removePendingUpload(fieldPath)
|
|
101
|
-
}
|
|
102
|
-
handleChange(null)
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// MIME-driven glyph dispatch. Until a dedicated VideoField primitive lands,
|
|
106
|
-
// the FileField is the canonical home for video uploads — the schema's
|
|
107
|
-
// `upload.allowedMimeTypes` decides what gets in, and we swap the glyph
|
|
108
|
-
// here based on the resolved MIME so the tile reads as "video" rather
|
|
109
|
-
// than "generic document".
|
|
110
|
-
const isVideo = incomingValue?.mimeType?.startsWith('video/') === true
|
|
111
|
-
const FileGlyph = isVideo ? VideoIcon : DocumentIcon
|
|
112
|
-
|
|
113
|
-
const htmlId = fieldPath
|
|
114
|
-
|
|
115
|
-
return (
|
|
116
|
-
<div className={`byline-field-file ${field.name}`}>
|
|
117
|
-
<div className={cx('byline-field-file-header', styles.header)}>
|
|
118
|
-
<Label
|
|
119
|
-
id={htmlId}
|
|
120
|
-
htmlFor={htmlId}
|
|
121
|
-
label={field.label ?? field.name}
|
|
122
|
-
required={!field.optional}
|
|
123
|
-
/>
|
|
124
|
-
</div>
|
|
125
|
-
|
|
126
|
-
{showUploadWidget ? (
|
|
127
|
-
collectionPath ? (
|
|
128
|
-
<FileUploadField
|
|
129
|
-
field={field}
|
|
130
|
-
collectionPath={collectionPath}
|
|
131
|
-
fieldPath={fieldPath}
|
|
132
|
-
onUploaded={(uploaded) => {
|
|
133
|
-
handleChange(uploaded)
|
|
134
|
-
}}
|
|
135
|
-
/>
|
|
136
|
-
) : (
|
|
137
|
-
<div className={cx('byline-field-file-empty', styles.empty)}>No file selected</div>
|
|
138
|
-
)
|
|
139
|
-
) : (
|
|
140
|
-
<div className={cx('byline-field-file-tile', styles.tile)}>
|
|
141
|
-
{isUploading && (
|
|
142
|
-
<div
|
|
143
|
-
className={cx('byline-field-file-uploading', styles.uploading)}
|
|
144
|
-
aria-live="polite"
|
|
145
|
-
aria-busy="true"
|
|
146
|
-
>
|
|
147
|
-
<LoaderRing />
|
|
148
|
-
</div>
|
|
149
|
-
)}
|
|
150
|
-
{collectionPath && (
|
|
151
|
-
<div className={cx('byline-field-file-actions', styles.actions)}>
|
|
152
|
-
{!isPending && incomingValue?.storageUrl && (
|
|
153
|
-
<IconButton
|
|
154
|
-
type="button"
|
|
155
|
-
intent="noeffect"
|
|
156
|
-
onClick={() =>
|
|
157
|
-
triggerDownload(
|
|
158
|
-
incomingValue.storageUrl as string,
|
|
159
|
-
incomingValue.originalFilename ?? incomingValue.filename
|
|
160
|
-
)
|
|
161
|
-
}
|
|
162
|
-
size="xs"
|
|
163
|
-
disabled={isUploading}
|
|
164
|
-
aria-label="Download file"
|
|
165
|
-
>
|
|
166
|
-
<DownloadIcon width="15px" height="15px" />
|
|
167
|
-
</IconButton>
|
|
168
|
-
)}
|
|
169
|
-
<IconButton
|
|
170
|
-
type="button"
|
|
171
|
-
intent="noeffect"
|
|
172
|
-
onClick={handleRemove}
|
|
173
|
-
size="xs"
|
|
174
|
-
disabled={isUploading}
|
|
175
|
-
aria-label="Remove file"
|
|
176
|
-
>
|
|
177
|
-
<CloseIcon width="15px" height="15px" />
|
|
178
|
-
</IconButton>
|
|
179
|
-
</div>
|
|
180
|
-
)}
|
|
181
|
-
{/* Document icon + (optional) pending badge — mirrors the
|
|
182
|
-
image-field's preview-wrap so the file tile has the same
|
|
183
|
-
visual hierarchy: glyph on the left, metadata on the right.
|
|
184
|
-
When the file is stored (non-pending and resolvable storageUrl),
|
|
185
|
-
the wrap is rendered as an anchor that opens the asset in a new
|
|
186
|
-
tab — browser-native viewer dispatch (PDFs render inline,
|
|
187
|
-
non-renderable types fall through to download). */}
|
|
188
|
-
{!isPending && incomingValue?.storageUrl ? (
|
|
189
|
-
<a
|
|
190
|
-
href={incomingValue.storageUrl}
|
|
191
|
-
target="_blank"
|
|
192
|
-
rel="noreferrer"
|
|
193
|
-
aria-label={`Open ${incomingValue.originalFilename ?? incomingValue.filename} in a new tab`}
|
|
194
|
-
className={cx('byline-field-file-icon-wrap', styles['icon-wrap'])}
|
|
195
|
-
>
|
|
196
|
-
<FileGlyph
|
|
197
|
-
width="48px"
|
|
198
|
-
height="48px"
|
|
199
|
-
className={cx('byline-field-file-icon', styles.icon)}
|
|
200
|
-
/>
|
|
201
|
-
</a>
|
|
202
|
-
) : (
|
|
203
|
-
<div className={cx('byline-field-file-icon-wrap', styles['icon-wrap'])}>
|
|
204
|
-
<FileGlyph
|
|
205
|
-
width="48px"
|
|
206
|
-
height="48px"
|
|
207
|
-
className={cx('byline-field-file-icon', styles.icon)}
|
|
208
|
-
/>
|
|
209
|
-
{isPending && (
|
|
210
|
-
<div className={cx('byline-field-file-pending', styles.pending)}>
|
|
211
|
-
Pending upload
|
|
212
|
-
</div>
|
|
213
|
-
)}
|
|
214
|
-
</div>
|
|
215
|
-
)}
|
|
216
|
-
<div className={cx('byline-field-file-meta', styles.meta)}>
|
|
217
|
-
<div>
|
|
218
|
-
<span className={cx('byline-field-file-meta-key', styles['meta-key'])}>
|
|
219
|
-
Filename:
|
|
220
|
-
</span>{' '}
|
|
221
|
-
{incomingValue?.filename}
|
|
222
|
-
</div>
|
|
223
|
-
<div>
|
|
224
|
-
<span className={cx('byline-field-file-meta-key', styles['meta-key'])}>
|
|
225
|
-
Original:
|
|
226
|
-
</span>{' '}
|
|
227
|
-
{incomingValue?.originalFilename}
|
|
228
|
-
</div>
|
|
229
|
-
<div>
|
|
230
|
-
<span className={cx('byline-field-file-meta-key', styles['meta-key'])}>Type:</span>{' '}
|
|
231
|
-
{incomingValue?.mimeType}
|
|
232
|
-
</div>
|
|
233
|
-
<div>
|
|
234
|
-
<span className={cx('byline-field-file-meta-key', styles['meta-key'])}>Size:</span>{' '}
|
|
235
|
-
{incomingValue?.fileSize}
|
|
236
|
-
</div>
|
|
237
|
-
{isPending ? (
|
|
238
|
-
<div>
|
|
239
|
-
<span className={cx('byline-field-file-meta-key', styles['meta-key'])}>
|
|
240
|
-
Status:
|
|
241
|
-
</span>{' '}
|
|
242
|
-
<span className={cx('byline-field-file-meta-pending', styles['meta-pending'])}>
|
|
243
|
-
Will upload on save
|
|
244
|
-
</span>
|
|
245
|
-
</div>
|
|
246
|
-
) : (
|
|
247
|
-
<>
|
|
248
|
-
<div>
|
|
249
|
-
<span className={cx('byline-field-file-meta-key', styles['meta-key'])}>
|
|
250
|
-
Storage:
|
|
251
|
-
</span>{' '}
|
|
252
|
-
{incomingValue?.storageProvider}
|
|
253
|
-
</div>
|
|
254
|
-
<div>
|
|
255
|
-
<span className={cx('byline-field-file-meta-key', styles['meta-key'])}>
|
|
256
|
-
Path:
|
|
257
|
-
</span>{' '}
|
|
258
|
-
{incomingValue?.storagePath}
|
|
259
|
-
</div>
|
|
260
|
-
</>
|
|
261
|
-
)}
|
|
262
|
-
</div>
|
|
263
|
-
</div>
|
|
264
|
-
)}
|
|
265
|
-
|
|
266
|
-
{field.helpText && <HelpText text={field.helpText} />}
|
|
267
|
-
|
|
268
|
-
{fieldError && <ErrorText id={`${field.name}-error`} text={fieldError} />}
|
|
269
|
-
</div>
|
|
270
|
-
)
|
|
271
|
-
}
|
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* FileUploadField — drag-and-drop file picker that registers a deferred
|
|
3
|
-
* upload in form context.
|
|
4
|
-
*
|
|
5
|
-
* Override handles:
|
|
6
|
-
* .byline-field-file-upload — root wrapper
|
|
7
|
-
* .byline-field-file-upload-input — visually-hidden file input
|
|
8
|
-
* .byline-field-file-upload-zone — clickable / drop target
|
|
9
|
-
* .byline-field-file-upload-zone-active — drag-hovered state
|
|
10
|
-
* .byline-field-file-upload-zone-busy — processing state
|
|
11
|
-
* .byline-field-file-upload-icon — upload icon svg
|
|
12
|
-
* .byline-field-file-upload-label — primary text inside the zone
|
|
13
|
-
* .byline-field-file-upload-action — "browse" link inside the label
|
|
14
|
-
* .byline-field-file-upload-error — error message paragraph
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
.root,
|
|
18
|
-
:global(.byline-field-file-upload) {
|
|
19
|
-
margin-top: 0.25rem;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
.input,
|
|
23
|
-
:global(.byline-field-file-upload-input) {
|
|
24
|
-
position: absolute;
|
|
25
|
-
width: 1px;
|
|
26
|
-
height: 1px;
|
|
27
|
-
padding: 0;
|
|
28
|
-
margin: -1px;
|
|
29
|
-
overflow: hidden;
|
|
30
|
-
clip: rect(0, 0, 0, 0);
|
|
31
|
-
white-space: nowrap;
|
|
32
|
-
border: 0;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
.zone,
|
|
36
|
-
:global(.byline-field-file-upload-zone) {
|
|
37
|
-
display: flex;
|
|
38
|
-
flex-direction: column;
|
|
39
|
-
align-items: center;
|
|
40
|
-
justify-content: center;
|
|
41
|
-
gap: var(--spacing-8);
|
|
42
|
-
padding: 1.5rem 1rem;
|
|
43
|
-
border: 2px dashed var(--gray-600);
|
|
44
|
-
border-radius: var(--border-radius-lg);
|
|
45
|
-
color: var(--gray-400);
|
|
46
|
-
text-align: center;
|
|
47
|
-
cursor: pointer;
|
|
48
|
-
user-select: none;
|
|
49
|
-
transition:
|
|
50
|
-
color 150ms ease,
|
|
51
|
-
background-color 150ms ease,
|
|
52
|
-
border-color 150ms ease;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
.zone:hover,
|
|
56
|
-
:global(.byline-field-file-upload-zone):hover {
|
|
57
|
-
border-color: var(--primary-500);
|
|
58
|
-
background-color: oklch(from var(--primary-900) l c h / 0.1);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
.zone-active,
|
|
62
|
-
:global(.byline-field-file-upload-zone-active) {
|
|
63
|
-
border-color: var(--primary-400);
|
|
64
|
-
background-color: oklch(from var(--primary-900) l c h / 0.2);
|
|
65
|
-
color: var(--primary-300);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
.zone-busy,
|
|
69
|
-
:global(.byline-field-file-upload-zone-busy) {
|
|
70
|
-
border-color: var(--gray-700);
|
|
71
|
-
background-color: oklch(from var(--canvas-800) l c h / 0.5);
|
|
72
|
-
color: var(--gray-600);
|
|
73
|
-
cursor: not-allowed;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
.icon,
|
|
77
|
-
:global(.byline-field-file-upload-icon) {
|
|
78
|
-
width: 1.75rem;
|
|
79
|
-
height: 1.75rem;
|
|
80
|
-
opacity: 0.6;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
.label,
|
|
84
|
-
:global(.byline-field-file-upload-label) {
|
|
85
|
-
font-size: var(--font-size-xs);
|
|
86
|
-
font-weight: var(--font-weight-medium);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
.action,
|
|
90
|
-
:global(.byline-field-file-upload-action) {
|
|
91
|
-
color: var(--primary-400);
|
|
92
|
-
text-decoration: underline;
|
|
93
|
-
text-underline-offset: 2px;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
.error,
|
|
97
|
-
:global(.byline-field-file-upload-error) {
|
|
98
|
-
margin-top: 0.375rem;
|
|
99
|
-
color: var(--red-400);
|
|
100
|
-
font-size: var(--font-size-xs);
|
|
101
|
-
}
|
|
@@ -1,183 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* This Source Code is subject to the terms of the Mozilla Public
|
|
3
|
-
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
4
|
-
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
5
|
-
*
|
|
6
|
-
* Copyright (c) Infonomic Company Limited
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* FileUploadField
|
|
11
|
-
*
|
|
12
|
-
* Generic drag-and-drop / click-to-browse file picker that prepares a file for
|
|
13
|
-
* upload. Mirrors `ImageUploadField` but without image-specific validation or
|
|
14
|
-
* dimension extraction. The actual upload is deferred until form submission —
|
|
15
|
-
* this component stores the file in the form context's pending uploads and
|
|
16
|
-
* emits a placeholder StoredFileValue with a blob URL (used by the form
|
|
17
|
-
* orchestrator for cleanup; not shown to the user).
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
import type { ChangeEvent, DragEvent } from 'react'
|
|
21
|
-
import { useCallback, useRef, useState } from 'react'
|
|
22
|
-
|
|
23
|
-
import {
|
|
24
|
-
createPendingStoredFileValue,
|
|
25
|
-
type FileField as FieldType,
|
|
26
|
-
type PendingStoredFileValue,
|
|
27
|
-
type StoredFileValue,
|
|
28
|
-
} from '@byline/core'
|
|
29
|
-
import cx from 'classnames'
|
|
30
|
-
|
|
31
|
-
import { useFormContext } from '../../forms/form-context'
|
|
32
|
-
import styles from './file-upload-field.module.css'
|
|
33
|
-
|
|
34
|
-
interface FileUploadFieldProps {
|
|
35
|
-
field: FieldType
|
|
36
|
-
/** Collection path used to build the upload URL (e.g. `'media'`). */
|
|
37
|
-
collectionPath: string
|
|
38
|
-
/** Field path in the form (e.g. `'attachment'` or `'content.0.attachment'`). */
|
|
39
|
-
fieldPath: string
|
|
40
|
-
/** Called with the PendingStoredFileValue for immediate UI update. */
|
|
41
|
-
onUploaded: (value: StoredFileValue | PendingStoredFileValue) => void
|
|
42
|
-
/** Optional `accept` MIME-type / extension string for the native file input. */
|
|
43
|
-
accept?: string
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
type SelectionStatus = 'idle' | 'processing' | 'error'
|
|
47
|
-
|
|
48
|
-
export const FileUploadField = ({
|
|
49
|
-
field: _field,
|
|
50
|
-
collectionPath,
|
|
51
|
-
fieldPath,
|
|
52
|
-
onUploaded,
|
|
53
|
-
accept,
|
|
54
|
-
}: FileUploadFieldProps) => {
|
|
55
|
-
const inputRef = useRef<HTMLInputElement>(null)
|
|
56
|
-
const [status, setStatus] = useState<SelectionStatus>('idle')
|
|
57
|
-
const [errorMessage, setErrorMessage] = useState<string | null>(null)
|
|
58
|
-
const [isDragOver, setIsDragOver] = useState(false)
|
|
59
|
-
const { addPendingUpload } = useFormContext()
|
|
60
|
-
|
|
61
|
-
const handleFileSelected = useCallback(
|
|
62
|
-
(file: File) => {
|
|
63
|
-
setStatus('processing')
|
|
64
|
-
setErrorMessage(null)
|
|
65
|
-
|
|
66
|
-
// Blob URL is created so the form orchestrator can revoke it on cleanup
|
|
67
|
-
// alongside image-field uploads; it isn't surfaced in the UI here.
|
|
68
|
-
const previewUrl = URL.createObjectURL(file)
|
|
69
|
-
|
|
70
|
-
const pendingValue = createPendingStoredFileValue(file, previewUrl)
|
|
71
|
-
|
|
72
|
-
addPendingUpload(fieldPath, {
|
|
73
|
-
file,
|
|
74
|
-
previewUrl,
|
|
75
|
-
collectionPath,
|
|
76
|
-
})
|
|
77
|
-
|
|
78
|
-
setStatus('idle')
|
|
79
|
-
onUploaded(pendingValue)
|
|
80
|
-
},
|
|
81
|
-
[collectionPath, fieldPath, addPendingUpload, onUploaded]
|
|
82
|
-
)
|
|
83
|
-
|
|
84
|
-
const handleFileChange = useCallback(
|
|
85
|
-
(e: ChangeEvent<HTMLInputElement>) => {
|
|
86
|
-
const file = e.target.files?.[0]
|
|
87
|
-
if (file) handleFileSelected(file)
|
|
88
|
-
// Reset so re-selecting the same file fires the event again.
|
|
89
|
-
e.target.value = ''
|
|
90
|
-
},
|
|
91
|
-
[handleFileSelected]
|
|
92
|
-
)
|
|
93
|
-
|
|
94
|
-
const handleBrowseClick = useCallback(() => {
|
|
95
|
-
inputRef.current?.click()
|
|
96
|
-
}, [])
|
|
97
|
-
|
|
98
|
-
const handleDragOver = useCallback((e: DragEvent<HTMLDivElement>) => {
|
|
99
|
-
e.preventDefault()
|
|
100
|
-
setIsDragOver(true)
|
|
101
|
-
}, [])
|
|
102
|
-
|
|
103
|
-
const handleDragLeave = useCallback((e: DragEvent<HTMLDivElement>) => {
|
|
104
|
-
e.preventDefault()
|
|
105
|
-
setIsDragOver(false)
|
|
106
|
-
}, [])
|
|
107
|
-
|
|
108
|
-
const handleDrop = useCallback(
|
|
109
|
-
(e: DragEvent<HTMLDivElement>) => {
|
|
110
|
-
e.preventDefault()
|
|
111
|
-
setIsDragOver(false)
|
|
112
|
-
const file = e.dataTransfer.files?.[0]
|
|
113
|
-
if (file) handleFileSelected(file)
|
|
114
|
-
},
|
|
115
|
-
[handleFileSelected]
|
|
116
|
-
)
|
|
117
|
-
|
|
118
|
-
const isProcessing = status === 'processing'
|
|
119
|
-
|
|
120
|
-
return (
|
|
121
|
-
<div className={cx('byline-field-file-upload', styles.root)}>
|
|
122
|
-
<input
|
|
123
|
-
ref={inputRef}
|
|
124
|
-
type="file"
|
|
125
|
-
accept={accept}
|
|
126
|
-
className={cx('byline-field-file-upload-input', styles.input)}
|
|
127
|
-
onChange={handleFileChange}
|
|
128
|
-
disabled={isProcessing}
|
|
129
|
-
aria-hidden="true"
|
|
130
|
-
tabIndex={-1}
|
|
131
|
-
/>
|
|
132
|
-
|
|
133
|
-
<div
|
|
134
|
-
role="button"
|
|
135
|
-
tabIndex={0}
|
|
136
|
-
aria-label="Upload file — drag and drop or click to browse"
|
|
137
|
-
onDragOver={handleDragOver}
|
|
138
|
-
onDragLeave={handleDragLeave}
|
|
139
|
-
onDrop={handleDrop}
|
|
140
|
-
onClick={handleBrowseClick}
|
|
141
|
-
onKeyDown={(e) => {
|
|
142
|
-
if (e.key === 'Enter' || e.key === ' ') {
|
|
143
|
-
e.preventDefault()
|
|
144
|
-
handleBrowseClick()
|
|
145
|
-
}
|
|
146
|
-
}}
|
|
147
|
-
className={cx(
|
|
148
|
-
'byline-field-file-upload-zone',
|
|
149
|
-
styles.zone,
|
|
150
|
-
isDragOver &&
|
|
151
|
-
!isProcessing && ['byline-field-file-upload-zone-active', styles['zone-active']],
|
|
152
|
-
isProcessing && ['byline-field-file-upload-zone-busy', styles['zone-busy']]
|
|
153
|
-
)}
|
|
154
|
-
>
|
|
155
|
-
<svg
|
|
156
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
157
|
-
className={cx('byline-field-file-upload-icon', styles.icon)}
|
|
158
|
-
fill="none"
|
|
159
|
-
viewBox="0 0 24 24"
|
|
160
|
-
stroke="currentColor"
|
|
161
|
-
strokeWidth={1.5}
|
|
162
|
-
aria-hidden="true"
|
|
163
|
-
>
|
|
164
|
-
<path
|
|
165
|
-
strokeLinecap="round"
|
|
166
|
-
strokeLinejoin="round"
|
|
167
|
-
d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5m-13.5-9L12 3m0 0l4.5 4.5M12 3v13.5"
|
|
168
|
-
/>
|
|
169
|
-
</svg>
|
|
170
|
-
<span className={cx('byline-field-file-upload-label', styles.label)}>
|
|
171
|
-
Drop file here or{' '}
|
|
172
|
-
<span className={cx('byline-field-file-upload-action', styles.action)}>browse</span>
|
|
173
|
-
</span>
|
|
174
|
-
</div>
|
|
175
|
-
|
|
176
|
-
{status === 'error' && errorMessage && (
|
|
177
|
-
<p className={cx('byline-field-file-upload-error', styles.error)} role="alert">
|
|
178
|
-
{errorMessage}
|
|
179
|
-
</p>
|
|
180
|
-
)}
|
|
181
|
-
</div>
|
|
182
|
-
)
|
|
183
|
-
}
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* GroupField — fixed-order group of child fields.
|
|
3
|
-
*
|
|
4
|
-
* Override handles:
|
|
5
|
-
* .byline-field-group — outer wrapper
|
|
6
|
-
* .byline-field-group-header — label/help text block
|
|
7
|
-
* .byline-field-group-title — title heading
|
|
8
|
-
* .byline-field-group-required — required asterisk
|
|
9
|
-
* .byline-field-group-help — help text below title
|
|
10
|
-
* .byline-field-group-body — vertical stack of children
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
.header,
|
|
14
|
-
:global(.byline-field-group-header) {
|
|
15
|
-
display: flex;
|
|
16
|
-
flex-direction: column;
|
|
17
|
-
gap: 0.125rem;
|
|
18
|
-
margin-bottom: var(--spacing-8);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
.title,
|
|
22
|
-
:global(.byline-field-group-title) {
|
|
23
|
-
font-size: 1rem;
|
|
24
|
-
font-weight: var(--font-weight-medium);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
.required,
|
|
28
|
-
:global(.byline-field-group-required) {
|
|
29
|
-
color: var(--red-500);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
.help,
|
|
33
|
-
:global(.byline-field-group-help) {
|
|
34
|
-
color: var(--gray-500);
|
|
35
|
-
font-size: var(--font-size-xs);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
.body,
|
|
39
|
-
:global(.byline-field-group-body) {
|
|
40
|
-
display: flex;
|
|
41
|
-
flex-direction: column;
|
|
42
|
-
gap: var(--spacing-8);
|
|
43
|
-
}
|
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* This Source Code is subject to the terms of the Mozilla Public
|
|
3
|
-
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
4
|
-
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
5
|
-
*
|
|
6
|
-
* Copyright (c) Infonomic Company Limited
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { useMemo } from 'react'
|
|
10
|
-
|
|
11
|
-
import type { Field, GroupField as GroupFieldType } from '@byline/core'
|
|
12
|
-
import cx from 'classnames'
|
|
13
|
-
|
|
14
|
-
import { placeholderForField } from '../../fields/field-helpers'
|
|
15
|
-
import { FieldRenderer } from '../../fields/field-renderer'
|
|
16
|
-
import { useFieldError } from '../../forms/form-context'
|
|
17
|
-
import { ErrorText } from '../../uikit.js'
|
|
18
|
-
import styles from './group-field.module.css'
|
|
19
|
-
|
|
20
|
-
// ---------------------------------------------------------------------------
|
|
21
|
-
// GroupField — renders a fixed-order group of child fields wrapped in a
|
|
22
|
-
// single div. No drag-and-drop. No add/remove.
|
|
23
|
-
// The outer div carries the field type ('group') and field name as classes
|
|
24
|
-
// so consumers can target individual groups via CSS.
|
|
25
|
-
//
|
|
26
|
-
// Stable override handles: `.byline-field-group`, `.byline-field-group-header`,
|
|
27
|
-
// `.byline-field-group-title`, `.byline-field-group-help`,
|
|
28
|
-
// `.byline-field-group-body`.
|
|
29
|
-
// ---------------------------------------------------------------------------
|
|
30
|
-
|
|
31
|
-
interface GroupFieldProps {
|
|
32
|
-
field: GroupFieldType
|
|
33
|
-
defaultValue: any
|
|
34
|
-
path: string
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export const GroupField = ({ field, defaultValue, path }: GroupFieldProps) => {
|
|
38
|
-
const fieldError = useFieldError(field.name)
|
|
39
|
-
// Default value for a group field is a plain object: { rating: 5, comment: '...' }
|
|
40
|
-
// Normalize to a plain object if not already one.
|
|
41
|
-
const groupData = useMemo(() => {
|
|
42
|
-
if (defaultValue && typeof defaultValue === 'object' && !Array.isArray(defaultValue)) {
|
|
43
|
-
return defaultValue
|
|
44
|
-
}
|
|
45
|
-
// Fallback: build a placeholder object from child field definitions
|
|
46
|
-
const placeholder: Record<string, any> = {}
|
|
47
|
-
for (const childField of field.fields as Field[]) {
|
|
48
|
-
placeholder[childField.name] = placeholderForField(childField)
|
|
49
|
-
}
|
|
50
|
-
return placeholder
|
|
51
|
-
}, [defaultValue, field.fields])
|
|
52
|
-
|
|
53
|
-
return (
|
|
54
|
-
<div className={`byline-field-group ${field.name}`}>
|
|
55
|
-
{field.label && (
|
|
56
|
-
<div className={cx('byline-field-group-header', styles.header)}>
|
|
57
|
-
<h3 className={cx('byline-field-group-title', styles.title)}>
|
|
58
|
-
{field.label}{' '}
|
|
59
|
-
{!field.optional && (
|
|
60
|
-
<span className={cx('byline-field-group-required', styles.required)}>*</span>
|
|
61
|
-
)}
|
|
62
|
-
</h3>
|
|
63
|
-
{field.helpText && (
|
|
64
|
-
<p className={cx('byline-field-group-help', styles.help)}>{field.helpText}</p>
|
|
65
|
-
)}
|
|
66
|
-
</div>
|
|
67
|
-
)}
|
|
68
|
-
<div className={cx('byline-field-group-body', styles.body)}>
|
|
69
|
-
{(field.fields as Field[]).map((innerField) => {
|
|
70
|
-
return (
|
|
71
|
-
<FieldRenderer
|
|
72
|
-
key={innerField.name}
|
|
73
|
-
field={innerField}
|
|
74
|
-
defaultValue={groupData[innerField.name]}
|
|
75
|
-
basePath={path}
|
|
76
|
-
disableSorting={true}
|
|
77
|
-
/>
|
|
78
|
-
)
|
|
79
|
-
})}
|
|
80
|
-
</div>
|
|
81
|
-
{fieldError && <ErrorText id={`${field.name}-error`} text={fieldError} />}
|
|
82
|
-
</div>
|
|
83
|
-
)
|
|
84
|
-
}
|