@byline/ui 2.2.9 → 2.3.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-renderer.js +2 -1
- package/dist/fields/file/file-field.d.ts +4 -2
- package/dist/fields/file/file-field.js +182 -81
- package/dist/fields/file/file-field.module.js +10 -5
- package/dist/fields/file/file-field_module.css +99 -32
- package/dist/fields/file/file-upload-field.d.ts +21 -0
- package/dist/fields/file/file-upload-field.js +128 -0
- package/dist/fields/file/file-upload-field.module.js +15 -0
- package/dist/fields/file/file-upload-field_module.css +74 -0
- package/dist/fields/image/image-field.js +33 -17
- package/dist/fields/image/image-field.module.js +1 -0
- package/dist/fields/image/image-field_module.css +23 -10
- package/dist/fields/relation/relation-field.js +37 -24
- package/dist/fields/relation/relation-field.module.js +1 -1
- package/dist/fields/relation/relation-field_module.css +16 -16
- package/dist/forms/form-context.d.ts +11 -0
- package/dist/forms/form-context.js +47 -3
- package/dist/forms/form-renderer.js +5 -3
- package/dist/forms/form-renderer_module.css +1 -2
- package/dist/icons/index.d.ts +1 -0
- package/dist/icons/index.js +1 -0
- package/dist/icons/video-icon.d.ts +6 -0
- package/dist/icons/video-icon.js +36 -0
- package/dist/react.d.ts +1 -0
- package/dist/react.js +1 -0
- package/package.json +5 -4
- package/src/fields/field-renderer.tsx +1 -0
- package/src/fields/file/file-field.module.css +114 -49
- package/src/fields/file/file-field.tsx +220 -56
- package/src/fields/file/file-upload-field.module.css +101 -0
- package/src/fields/file/file-upload-field.tsx +183 -0
- package/src/fields/image/image-field.module.css +27 -13
- package/src/fields/image/image-field.tsx +35 -12
- package/src/fields/relation/relation-field.module.css +21 -21
- package/src/fields/relation/relation-field.tsx +24 -20
- package/src/forms/form-context.tsx +73 -0
- package/src/forms/form-renderer.module.css +1 -2
- package/src/forms/form-renderer.tsx +9 -2
- package/src/icons/index.ts +1 -0
- package/src/icons/video-icon.tsx +32 -0
- package/src/react.ts +1 -0
|
@@ -195,8 +195,7 @@
|
|
|
195
195
|
padding-top: .25rem;
|
|
196
196
|
padding-right: 12px;
|
|
197
197
|
padding-left: var(--spacing-16);
|
|
198
|
-
border-left: var(--border-width-thin) var(--border-style-solid)
|
|
199
|
-
var(--gray-100);
|
|
198
|
+
border-left: var(--border-width-thin) var(--border-style-solid) var(--gray-100);
|
|
200
199
|
margin-bottom: 0;
|
|
201
200
|
position: sticky;
|
|
202
201
|
top: 95px;
|
package/dist/icons/index.d.ts
CHANGED
|
@@ -49,6 +49,7 @@ export * from './stopwatch-icon.js';
|
|
|
49
49
|
export * from './success-icon.js';
|
|
50
50
|
export * from './user-icon.js';
|
|
51
51
|
export * from './users-icon.js';
|
|
52
|
+
export * from './video-icon.js';
|
|
52
53
|
export * from './wallet-icon.js';
|
|
53
54
|
export * from './warning-icon.js';
|
|
54
55
|
export * from './x-icon.js';
|
package/dist/icons/index.js
CHANGED
|
@@ -49,6 +49,7 @@ export * from "./stopwatch-icon.js";
|
|
|
49
49
|
export * from "./success-icon.js";
|
|
50
50
|
export * from "./user-icon.js";
|
|
51
51
|
export * from "./users-icon.js";
|
|
52
|
+
export * from "./video-icon.js";
|
|
52
53
|
export * from "./wallet-icon.js";
|
|
53
54
|
export * from "./warning-icon.js";
|
|
54
55
|
export * from "./x-icon.js";
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import classnames from "classnames";
|
|
3
|
+
import { IconElement } from "./icon-element.js";
|
|
4
|
+
import icons_module from "./icons.module.js";
|
|
5
|
+
const VideoIcon = ({ className, svgClassName, ...rest })=>{
|
|
6
|
+
const applied = classnames(icons_module["fill-none"], icons_module["stroke-current"], svgClassName);
|
|
7
|
+
return /*#__PURE__*/ jsx(IconElement, {
|
|
8
|
+
className: classnames('video-icon', className),
|
|
9
|
+
...rest,
|
|
10
|
+
children: /*#__PURE__*/ jsxs("svg", {
|
|
11
|
+
className: applied,
|
|
12
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
13
|
+
focusable: "false",
|
|
14
|
+
"aria-hidden": "true",
|
|
15
|
+
viewBox: "0 0 24 24",
|
|
16
|
+
strokeWidth: "1.5",
|
|
17
|
+
strokeLinecap: "round",
|
|
18
|
+
strokeLinejoin: "round",
|
|
19
|
+
children: [
|
|
20
|
+
/*#__PURE__*/ jsx("path", {
|
|
21
|
+
stroke: "none",
|
|
22
|
+
d: "M0 0h24v24H0z",
|
|
23
|
+
fill: "none"
|
|
24
|
+
}),
|
|
25
|
+
/*#__PURE__*/ jsx("path", {
|
|
26
|
+
d: "M15 10l4.553 -2.276a1 1 0 0 1 1.447 .894v6.764a1 1 0 0 1 -1.447 .894l-4.553 -2.276v-4z"
|
|
27
|
+
}),
|
|
28
|
+
/*#__PURE__*/ jsx("path", {
|
|
29
|
+
d: "M3 6m0 2a2 2 0 0 1 2 -2h8a2 2 0 0 1 2 2v8a2 2 0 0 1 -2 2h-8a2 2 0 0 1 -2 -2z"
|
|
30
|
+
})
|
|
31
|
+
]
|
|
32
|
+
})
|
|
33
|
+
});
|
|
34
|
+
};
|
|
35
|
+
VideoIcon.displayName = 'VideoIcon';
|
|
36
|
+
export { VideoIcon };
|
package/dist/react.d.ts
CHANGED
|
@@ -45,6 +45,7 @@ export * from './fields/draggable-context-menu.js';
|
|
|
45
45
|
export * from './fields/field-helpers.js';
|
|
46
46
|
export * from './fields/field-renderer.js';
|
|
47
47
|
export * from './fields/file/file-field.js';
|
|
48
|
+
export * from './fields/file/file-upload-field.js';
|
|
48
49
|
export * from './fields/group/group-field.js';
|
|
49
50
|
export * from './fields/image/image-field.js';
|
|
50
51
|
export * from './fields/image/image-upload-field.js';
|
package/dist/react.js
CHANGED
|
@@ -26,6 +26,7 @@ export * from "./fields/draggable-context-menu.js";
|
|
|
26
26
|
export * from "./fields/field-helpers.js";
|
|
27
27
|
export * from "./fields/field-renderer.js";
|
|
28
28
|
export * from "./fields/file/file-field.js";
|
|
29
|
+
export * from "./fields/file/file-upload-field.js";
|
|
29
30
|
export * from "./fields/group/group-field.js";
|
|
30
31
|
export * from "./fields/image/image-field.js";
|
|
31
32
|
export * from "./fields/image/image-upload-field.js";
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"private": false,
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MPL-2.0",
|
|
6
|
-
"version": "2.
|
|
6
|
+
"version": "2.3.0",
|
|
7
7
|
"engines": {
|
|
8
8
|
"node": ">=20.9.0"
|
|
9
9
|
},
|
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
],
|
|
32
32
|
"exports": {
|
|
33
33
|
"./react": {
|
|
34
|
+
"development": "./src/react.ts",
|
|
34
35
|
"types": "./dist/react.d.ts",
|
|
35
36
|
"import": "./dist/react.js",
|
|
36
37
|
"main": "./dist/react.js",
|
|
@@ -65,9 +66,9 @@
|
|
|
65
66
|
"react-diff-viewer-continued": "^4.2.2",
|
|
66
67
|
"zod": "^4.4.3",
|
|
67
68
|
"zod-form-data": "^3.0.1",
|
|
68
|
-
"@byline/admin": "2.
|
|
69
|
-
"@byline/
|
|
70
|
-
"@byline/
|
|
69
|
+
"@byline/admin": "2.3.0",
|
|
70
|
+
"@byline/client": "2.3.0",
|
|
71
|
+
"@byline/core": "2.3.0"
|
|
71
72
|
},
|
|
72
73
|
"peerDependencies": {
|
|
73
74
|
"react": "^19.0.0",
|
|
@@ -1,75 +1,135 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* FileField —
|
|
2
|
+
* FileField — metadata tile + remove/upload affordances for arbitrary file
|
|
3
|
+
* uploads (non-image). Mirrors `image-field.module.css` structurally; the
|
|
4
|
+
* image-only preview / SVG-shape / pending-badge rules are dropped.
|
|
3
5
|
*
|
|
4
6
|
* Override handles:
|
|
5
|
-
* .byline-field-file
|
|
6
|
-
* .byline-field-file-
|
|
7
|
-
* .byline-field-file-
|
|
8
|
-
* .byline-field-file-
|
|
9
|
-
* .byline-field-file-
|
|
10
|
-
* .byline-field-file-
|
|
11
|
-
* .byline-field-file-
|
|
12
|
-
* .byline-field-file-
|
|
13
|
-
* .byline-field-file-
|
|
7
|
+
* .byline-field-file — wrapper div
|
|
8
|
+
* .byline-field-file-header — label row
|
|
9
|
+
* .byline-field-file-actions — top-right icon-button group (download + remove)
|
|
10
|
+
* .byline-field-file-empty — empty-state hint text
|
|
11
|
+
* .byline-field-file-tile — bordered metadata tile
|
|
12
|
+
* .byline-field-file-uploading — centered overlay shown while uploading
|
|
13
|
+
* .byline-field-file-icon-wrap — left-aligned document-icon wrapper
|
|
14
|
+
* .byline-field-file-icon — the document glyph itself
|
|
15
|
+
* .byline-field-file-pending — yellow "pending upload" pill
|
|
16
|
+
* .byline-field-file-meta — metadata list
|
|
17
|
+
* .byline-field-file-meta-key — metadata field-name span
|
|
18
|
+
* .byline-field-file-meta-pending — yellow text inside pending status
|
|
14
19
|
*/
|
|
15
20
|
|
|
16
|
-
.dirty,
|
|
17
|
-
:global(.byline-field-file-dirty) {
|
|
18
|
-
padding: var(--spacing-12);
|
|
19
|
-
border: var(--border-width-thin) var(--border-style-solid) var(--blue-300);
|
|
20
|
-
border-radius: var(--border-radius-md);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
21
|
.header,
|
|
24
22
|
:global(.byline-field-file-header) {
|
|
25
23
|
display: flex;
|
|
26
24
|
align-items: baseline;
|
|
27
|
-
|
|
25
|
+
gap: var(--spacing-8);
|
|
28
26
|
margin-bottom: 0.25rem;
|
|
29
27
|
}
|
|
30
28
|
|
|
31
|
-
.
|
|
32
|
-
:global(.byline-field-file-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
29
|
+
.actions,
|
|
30
|
+
:global(.byline-field-file-actions) {
|
|
31
|
+
position: absolute;
|
|
32
|
+
top: var(--spacing-6);
|
|
33
|
+
right: var(--spacing-6);
|
|
34
|
+
z-index: 1;
|
|
35
|
+
display: flex;
|
|
36
|
+
align-items: center;
|
|
37
|
+
gap: var(--spacing-4);
|
|
36
38
|
}
|
|
37
39
|
|
|
38
|
-
.
|
|
39
|
-
:
|
|
40
|
-
margin-top: 0.125rem;
|
|
41
|
-
color: var(--gray-400);
|
|
42
|
-
font-size: var(--font-size-xs);
|
|
40
|
+
:global(.byline-field-file-actions .byline-button) {
|
|
41
|
+
color: var(--gray-900);
|
|
43
42
|
}
|
|
44
43
|
|
|
45
|
-
.
|
|
46
|
-
:global(.byline-field-file-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
44
|
+
:global(.dark .byline-field-file-actions .byline-button),
|
|
45
|
+
:global([data-theme="dark"] .byline-field-file-actions .byline-button) {
|
|
46
|
+
color: var(--gray-200);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.empty,
|
|
50
|
+
:global(.byline-field-file-empty) {
|
|
51
|
+
color: var(--gray-500);
|
|
51
52
|
font-size: var(--font-size-xs);
|
|
53
|
+
font-style: italic;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.tile,
|
|
57
|
+
:global(.byline-field-file-tile) {
|
|
58
|
+
position: relative;
|
|
59
|
+
display: flex;
|
|
60
|
+
gap: var(--spacing-16);
|
|
61
|
+
margin-top: 0.25rem;
|
|
62
|
+
padding: var(--spacing-8);
|
|
63
|
+
border: var(--border-width-thin) var(--border-style-solid) var(--primary-500);
|
|
64
|
+
border-radius: var(--border-radius-md);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.uploading,
|
|
68
|
+
:global(.byline-field-file-uploading) {
|
|
69
|
+
position: absolute;
|
|
70
|
+
inset: 0;
|
|
71
|
+
z-index: 2;
|
|
72
|
+
display: flex;
|
|
73
|
+
align-items: center;
|
|
74
|
+
justify-content: center;
|
|
75
|
+
background-color: oklch(from var(--gray-950) l c h / 0.5);
|
|
76
|
+
border-radius: var(--border-radius-md);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.icon-wrap,
|
|
80
|
+
:global(.byline-field-file-icon-wrap) {
|
|
81
|
+
position: relative;
|
|
82
|
+
flex-shrink: 0;
|
|
83
|
+
align-self: flex-start;
|
|
84
|
+
display: flex;
|
|
85
|
+
align-items: center;
|
|
86
|
+
justify-content: center;
|
|
87
|
+
width: 4rem;
|
|
88
|
+
height: 4rem;
|
|
89
|
+
border: var(--border-width-thin) var(--border-style-solid) var(--gray-600);
|
|
90
|
+
border-radius: var(--border-radius-sm);
|
|
91
|
+
color: var(--gray-300);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/* Interactive affordance when the wrap is rendered as a link
|
|
95
|
+
(file is stored — clicking opens the asset in a new tab). */
|
|
96
|
+
a.icon-wrap,
|
|
97
|
+
:global(a.byline-field-file-icon-wrap) {
|
|
52
98
|
cursor: pointer;
|
|
99
|
+
text-decoration: none;
|
|
100
|
+
transition:
|
|
101
|
+
border-color 120ms ease,
|
|
102
|
+
color 120ms ease;
|
|
53
103
|
}
|
|
54
104
|
|
|
55
|
-
.
|
|
56
|
-
:global(.byline-field-file-
|
|
57
|
-
color: var(--
|
|
58
|
-
|
|
59
|
-
text-underline-offset: 2px;
|
|
105
|
+
a.icon-wrap:hover,
|
|
106
|
+
:global(a.byline-field-file-icon-wrap):hover {
|
|
107
|
+
border-color: var(--primary-500);
|
|
108
|
+
color: var(--primary-400);
|
|
60
109
|
}
|
|
61
110
|
|
|
62
|
-
.
|
|
63
|
-
:global(.byline-field-file-
|
|
64
|
-
|
|
65
|
-
|
|
111
|
+
a.icon-wrap:focus-visible,
|
|
112
|
+
:global(a.byline-field-file-icon-wrap):focus-visible {
|
|
113
|
+
outline: 2px solid var(--primary-500);
|
|
114
|
+
outline-offset: 2px;
|
|
66
115
|
}
|
|
67
116
|
|
|
68
|
-
.
|
|
69
|
-
:global(.byline-field-file-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
117
|
+
.icon,
|
|
118
|
+
:global(.byline-field-file-icon) {
|
|
119
|
+
opacity: 0.85;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
.pending,
|
|
123
|
+
:global(.byline-field-file-pending) {
|
|
124
|
+
position: absolute;
|
|
125
|
+
top: 0.25rem;
|
|
126
|
+
left: 0.25rem;
|
|
127
|
+
padding: 0.125rem 0.375rem;
|
|
128
|
+
background-color: oklch(from var(--yellow-600) l c h / 0.9);
|
|
129
|
+
color: var(--yellow-100);
|
|
130
|
+
font-size: 0.6rem;
|
|
131
|
+
font-weight: var(--font-weight-medium);
|
|
132
|
+
border-radius: var(--border-radius-sm);
|
|
73
133
|
}
|
|
74
134
|
|
|
75
135
|
.meta,
|
|
@@ -77,7 +137,7 @@
|
|
|
77
137
|
display: flex;
|
|
78
138
|
flex-direction: column;
|
|
79
139
|
gap: 0.125rem;
|
|
80
|
-
|
|
140
|
+
padding-right: var(--spacing-32);
|
|
81
141
|
color: var(--gray-200);
|
|
82
142
|
font-size: var(--font-size-xs);
|
|
83
143
|
}
|
|
@@ -86,3 +146,8 @@
|
|
|
86
146
|
:global(.byline-field-file-meta-key) {
|
|
87
147
|
font-weight: var(--font-weight-semibold);
|
|
88
148
|
}
|
|
149
|
+
|
|
150
|
+
.meta-pending,
|
|
151
|
+
:global(.byline-field-file-meta-pending) {
|
|
152
|
+
color: var(--yellow-400);
|
|
153
|
+
}
|
|
@@ -6,17 +6,53 @@
|
|
|
6
6
|
* Copyright (c) Infonomic Company Limited
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import
|
|
9
|
+
import {
|
|
10
|
+
type FileField as FieldType,
|
|
11
|
+
isPendingStoredFileValue,
|
|
12
|
+
type StoredFileValue,
|
|
13
|
+
} from '@byline/core'
|
|
10
14
|
import cx from 'classnames'
|
|
11
15
|
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
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'
|
|
14
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
|
+
}
|
|
15
51
|
|
|
16
52
|
interface FileFieldProps {
|
|
17
53
|
field: FieldType
|
|
18
|
-
|
|
19
|
-
|
|
54
|
+
/** Collection path required to call the /upload endpoint. */
|
|
55
|
+
collectionPath?: string
|
|
20
56
|
value?: StoredFileValue | null
|
|
21
57
|
defaultValue?: StoredFileValue | null
|
|
22
58
|
onChange?: (value: StoredFileValue | null) => void
|
|
@@ -25,6 +61,7 @@ interface FileFieldProps {
|
|
|
25
61
|
|
|
26
62
|
export const FileField = ({
|
|
27
63
|
field,
|
|
64
|
+
collectionPath,
|
|
28
65
|
value,
|
|
29
66
|
defaultValue,
|
|
30
67
|
onChange: _onChange,
|
|
@@ -34,73 +71,200 @@ export const FileField = ({
|
|
|
34
71
|
const fieldError = useFieldError(fieldPath)
|
|
35
72
|
const isDirty = useIsDirty(fieldPath)
|
|
36
73
|
const fieldValue = useFieldValue<StoredFileValue | null | undefined>(fieldPath)
|
|
37
|
-
const
|
|
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)
|
|
38
85
|
|
|
39
|
-
const
|
|
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 => {
|
|
40
91
|
if (!v || typeof v !== 'object') return false
|
|
41
92
|
const maybe = v as Partial<StoredFileValue>
|
|
42
93
|
return maybe.storageProvider === 'placeholder' && maybe.storagePath === 'pending'
|
|
43
94
|
}
|
|
44
95
|
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
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
|
|
48
114
|
|
|
49
115
|
return (
|
|
50
|
-
<div
|
|
51
|
-
className={cx(
|
|
52
|
-
'byline-field-file',
|
|
53
|
-
field.name,
|
|
54
|
-
isDirty && ['byline-field-file-dirty', styles.dirty]
|
|
55
|
-
)}
|
|
56
|
-
>
|
|
116
|
+
<div className={`byline-field-file ${field.name}`}>
|
|
57
117
|
<div className={cx('byline-field-file-header', styles.header)}>
|
|
58
|
-
<
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
<div className={cx('byline-field-file-help', styles.help)}>{field.helpText}</div>
|
|
65
|
-
)}
|
|
66
|
-
</div>
|
|
67
|
-
{/* Placeholder action area for future upload UI */}
|
|
68
|
-
<button type="button" className={cx('byline-field-file-action', styles.action)} disabled>
|
|
69
|
-
Upload (coming soon)
|
|
70
|
-
</button>
|
|
118
|
+
<Label
|
|
119
|
+
id={htmlId}
|
|
120
|
+
htmlFor={htmlId}
|
|
121
|
+
label={field.label ?? field.name}
|
|
122
|
+
required={!field.optional}
|
|
123
|
+
/>
|
|
71
124
|
</div>
|
|
72
125
|
|
|
73
|
-
{
|
|
74
|
-
|
|
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
|
+
)
|
|
75
139
|
) : (
|
|
76
|
-
<div className={cx('byline-field-file-
|
|
77
|
-
|
|
78
|
-
<
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
{
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
+
)}
|
|
100
262
|
</div>
|
|
101
263
|
</div>
|
|
102
264
|
)}
|
|
103
265
|
|
|
266
|
+
{field.helpText && <HelpText text={field.helpText} />}
|
|
267
|
+
|
|
104
268
|
{fieldError && <ErrorText id={`${field.name}-error`} text={fieldError} />}
|
|
105
269
|
</div>
|
|
106
270
|
)
|