@ossy/resources 3.3.0 → 3.5.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/package.json +6 -6
- package/src/ImageResource.jsx +11 -3
- package/src/PlatformFileField.jsx +120 -85
- package/src/PlatformReferenceField.jsx +216 -0
- package/src/ResourceContentPage.jsx +5 -2
- package/src/ResourceGrid.jsx +321 -0
- package/src/ResourceGrid.stories.jsx +95 -0
- package/src/ResourceList.jsx +78 -8
- package/src/ResourcePanel.jsx +19 -2
- package/src/ResourceSelectionCheckbox.jsx +68 -0
- package/src/ResourcesPage.jsx +3 -0
- package/src/SchemaDetailView.jsx +18 -18
- package/src/SchemaPresenter.jsx +6 -4
- package/src/Upload.jsx +28 -13
- package/src/create.task.js +4 -0
- package/src/en.translations.json +4 -1
- package/src/get-resource.api.js +15 -0
- package/src/index.js +4 -0
- package/src/markdown.component.jsx +20 -0
- package/src/markdown.schema.js +13 -0
- package/src/markdown.spec.js +43 -0
- package/src/patch-content.action.js +1 -0
- package/src/patch-content.task.js +28 -0
- package/src/platform-reference-field.component.jsx +5 -0
- package/src/reference-field.helpers.js +57 -0
- package/src/reference-field.helpers.spec.js +100 -0
- package/src/resource-download.js +194 -0
- package/src/resource-download.spec.js +93 -0
- package/src/resource-read.helpers.js +5 -1
- package/src/resource-selection.js +91 -0
- package/src/resource-selection.spec.js +77 -0
- package/src/resource-upload-access.js +63 -0
- package/src/resource-upload-access.spec.js +100 -0
- package/src/resource.helpers.js +30 -5
- package/src/resources.attach-reference-links.js +100 -0
- package/src/resources.attach-reference-links.spec.js +118 -0
- package/src/resources.validate-references.js +104 -0
- package/src/resources.validate-references.spec.js +182 -0
- package/src/schema-field-display.js +12 -0
- package/src/server.js +13 -0
- package/src/sv.translations.json +4 -1
- package/src/update-content.task.js +4 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ossy/resources",
|
|
3
3
|
"description": "Resource domain — aggregate and events for the Ossy resource model",
|
|
4
|
-
"version": "3.
|
|
4
|
+
"version": "3.5.0",
|
|
5
5
|
"private": false,
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "./src/index.js",
|
|
@@ -23,10 +23,10 @@
|
|
|
23
23
|
"src": "./src"
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"@ossy/event-store": "^3.
|
|
27
|
-
"@ossy/fold": "^3.0
|
|
28
|
-
"@ossy/platform": "^3.
|
|
29
|
-
"@ossy/schema": "^3.0
|
|
26
|
+
"@ossy/event-store": "^3.4.0",
|
|
27
|
+
"@ossy/fold": "^3.4.0",
|
|
28
|
+
"@ossy/platform": "^3.5.0",
|
|
29
|
+
"@ossy/schema": "^3.5.0"
|
|
30
30
|
},
|
|
31
31
|
"peerDependencies": {
|
|
32
32
|
"@ossy/design-system": ">=1.0.0",
|
|
@@ -49,5 +49,5 @@
|
|
|
49
49
|
"/src",
|
|
50
50
|
"README.md"
|
|
51
51
|
],
|
|
52
|
-
"gitHead": "
|
|
52
|
+
"gitHead": "23167600f991596f122765a53f2b9df08314e115"
|
|
53
53
|
}
|
package/src/ImageResource.jsx
CHANGED
|
@@ -14,9 +14,17 @@ export const ImageResource = ({
|
|
|
14
14
|
return (
|
|
15
15
|
<div style={{ display: 'flex', justifyContent: 'center', padding: 'var(--space-m) var(--space-s) var(--space-xl)' }}>
|
|
16
16
|
<Image
|
|
17
|
-
src={resource?.content?.sizes?.galleryLarge || resource
|
|
18
|
-
placeholderSrc={resource?.content?.sizes?.['loader-square-blurred-after'] || resource
|
|
19
|
-
|
|
17
|
+
src={resource?.content?.sizes?.galleryLarge || resource?.content?.src}
|
|
18
|
+
placeholderSrc={resource?.content?.sizes?.['loader-square-blurred-after'] || resource?.content?.src}
|
|
19
|
+
blurhash={resource?.content?.blurhash}
|
|
20
|
+
width={resource?.content?.width}
|
|
21
|
+
height={resource?.content?.height}
|
|
22
|
+
style={{
|
|
23
|
+
width: '100%',
|
|
24
|
+
maxWidth: '960px',
|
|
25
|
+
height: 'auto',
|
|
26
|
+
margin: 'var(--space-m) auto var(--space-l)',
|
|
27
|
+
}}
|
|
20
28
|
/>
|
|
21
29
|
</div>
|
|
22
30
|
)
|
|
@@ -6,7 +6,9 @@ import {
|
|
|
6
6
|
Button,
|
|
7
7
|
UploadInput,
|
|
8
8
|
LocalFilePreview,
|
|
9
|
+
DropZone,
|
|
9
10
|
acceptsFile,
|
|
11
|
+
partitionAcceptedFiles,
|
|
10
12
|
} from '@ossy/design-system'
|
|
11
13
|
import { uploadFile } from './resource.helpers.js'
|
|
12
14
|
import { metadata as GetResource } from './get.action.js'
|
|
@@ -37,7 +39,7 @@ function resourceIdsFromValue(value) {
|
|
|
37
39
|
}
|
|
38
40
|
|
|
39
41
|
/**
|
|
40
|
-
* Platform file field — eager upload on pick via SDK create + PUT.
|
|
42
|
+
* Platform file field — eager upload on pick or drop via SDK create + PUT.
|
|
41
43
|
* Register as `@ossy/design-system/input/file` / `@ossy/design-system/input/image` through app component slots.
|
|
42
44
|
*/
|
|
43
45
|
export function PlatformFileField({
|
|
@@ -63,6 +65,7 @@ export function PlatformFileField({
|
|
|
63
65
|
|
|
64
66
|
const resourceId = multi ? null : resourceIdFromValue(value)
|
|
65
67
|
const resourceIds = multi ? resourceIdsFromValue(value) : []
|
|
68
|
+
const busy = disabled || uploading
|
|
66
69
|
|
|
67
70
|
useEffect(() => {
|
|
68
71
|
if (!resourceId || localFile) {
|
|
@@ -105,6 +108,29 @@ export function PlatformFileField({
|
|
|
105
108
|
[name, onChange, sdk, uploadLocation],
|
|
106
109
|
)
|
|
107
110
|
|
|
111
|
+
const uploadMany = useCallback(
|
|
112
|
+
async files => {
|
|
113
|
+
const remaining = max - resourceIds.length
|
|
114
|
+
const batch = files.slice(0, remaining)
|
|
115
|
+
if (!batch.length) return
|
|
116
|
+
setUploading(true)
|
|
117
|
+
setError(null)
|
|
118
|
+
try {
|
|
119
|
+
const uploaded = []
|
|
120
|
+
for (const file of batch) {
|
|
121
|
+
const resource = await uploadFile(sdk, uploadLocation, file)
|
|
122
|
+
uploaded.push({ resourceId: resource.id })
|
|
123
|
+
}
|
|
124
|
+
commitResourceValue(name, [...resourceIds.map(id => ({ resourceId: id })), ...uploaded], onChange)
|
|
125
|
+
} catch (err) {
|
|
126
|
+
setError(err?.message || 'Upload failed')
|
|
127
|
+
} finally {
|
|
128
|
+
setUploading(false)
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
[max, name, onChange, resourceIds, sdk, uploadLocation],
|
|
132
|
+
)
|
|
133
|
+
|
|
108
134
|
const onSinglePick = useCallback(
|
|
109
135
|
e => {
|
|
110
136
|
const file = e.target.files?.[0]
|
|
@@ -120,33 +146,38 @@ export function PlatformFileField({
|
|
|
120
146
|
)
|
|
121
147
|
|
|
122
148
|
const onMultiPick = useCallback(
|
|
123
|
-
|
|
149
|
+
e => {
|
|
124
150
|
const picked = Array.from(e.target.files || [])
|
|
125
151
|
e.target.value = ''
|
|
126
152
|
if (!picked.length || uploading) return
|
|
127
|
-
const
|
|
128
|
-
if (
|
|
153
|
+
const { accepted, rejected } = partitionAcceptedFiles(picked, accept)
|
|
154
|
+
if (rejected.length) {
|
|
129
155
|
setError('Some files were rejected by accept filter')
|
|
130
156
|
return
|
|
131
157
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
setError(err?.message || 'Upload failed')
|
|
145
|
-
} finally {
|
|
146
|
-
setUploading(false)
|
|
158
|
+
uploadMany(accepted)
|
|
159
|
+
},
|
|
160
|
+
[accept, uploadMany, uploading],
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
const onFilesDrop = useCallback(
|
|
164
|
+
files => {
|
|
165
|
+
if (busy) return
|
|
166
|
+
const { accepted, rejected } = partitionAcceptedFiles(files, accept)
|
|
167
|
+
if (!accepted.length) {
|
|
168
|
+
if (rejected.length) setError('File type not accepted')
|
|
169
|
+
return
|
|
147
170
|
}
|
|
171
|
+
if (rejected.length) {
|
|
172
|
+
setError('Some files were rejected by accept filter')
|
|
173
|
+
}
|
|
174
|
+
if (multi) {
|
|
175
|
+
uploadMany(accepted)
|
|
176
|
+
return
|
|
177
|
+
}
|
|
178
|
+
uploadSingle(accepted[0])
|
|
148
179
|
},
|
|
149
|
-
[accept,
|
|
180
|
+
[accept, busy, multi, uploadMany, uploadSingle],
|
|
150
181
|
)
|
|
151
182
|
|
|
152
183
|
const removeMulti = useCallback(
|
|
@@ -163,77 +194,81 @@ export function PlatformFileField({
|
|
|
163
194
|
if (multi) {
|
|
164
195
|
const canAdd = resourceIds.length < max
|
|
165
196
|
return (
|
|
166
|
-
<
|
|
167
|
-
{
|
|
168
|
-
|
|
169
|
-
<View
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
197
|
+
<DropZone onFilesDrop={canAdd ? onFilesDrop : undefined}>
|
|
198
|
+
<View gap="s" style={style} data-ossy-file-dropzone {...rest}>
|
|
199
|
+
{resourceIds.map(id => (
|
|
200
|
+
<View key={id} layout="row" gap="s" alignItems="center">
|
|
201
|
+
<View
|
|
202
|
+
as="img"
|
|
203
|
+
src={`/r/${id}`}
|
|
204
|
+
alt=""
|
|
205
|
+
width="32px"
|
|
206
|
+
height="32px"
|
|
207
|
+
style={{ objectFit: 'cover', borderRadius: 'var(--space-s)' }}
|
|
208
|
+
/>
|
|
209
|
+
<Text style={{ flex: 1, fontSize: 14 }}>{id}</Text>
|
|
210
|
+
<Button
|
|
211
|
+
type="button"
|
|
212
|
+
variant="command-danger"
|
|
213
|
+
prefix="close"
|
|
214
|
+
size="s"
|
|
215
|
+
disabled={busy}
|
|
216
|
+
onClick={() => removeMulti(id)}
|
|
217
|
+
/>
|
|
218
|
+
</View>
|
|
219
|
+
))}
|
|
220
|
+
{canAdd && (
|
|
221
|
+
<UploadInput
|
|
222
|
+
id={name}
|
|
223
|
+
name={name}
|
|
224
|
+
accept={accept}
|
|
225
|
+
disabled={busy}
|
|
226
|
+
required={required && resourceIds.length === 0}
|
|
227
|
+
multiple
|
|
228
|
+
onChange={onMultiPick}
|
|
176
229
|
/>
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
size="s"
|
|
183
|
-
disabled={disabled || uploading}
|
|
184
|
-
onClick={() => removeMulti(id)}
|
|
185
|
-
/>
|
|
186
|
-
</View>
|
|
187
|
-
))}
|
|
188
|
-
{canAdd && (
|
|
189
|
-
<UploadInput
|
|
190
|
-
id={name}
|
|
191
|
-
name={name}
|
|
192
|
-
accept={accept}
|
|
193
|
-
disabled={disabled || uploading}
|
|
194
|
-
required={required && resourceIds.length === 0}
|
|
195
|
-
multiple
|
|
196
|
-
onChange={onMultiPick}
|
|
197
|
-
/>
|
|
198
|
-
)}
|
|
199
|
-
{uploading && <Text style={{ fontSize: 12 }}>Uploading…</Text>}
|
|
200
|
-
{error && <Text style={{ fontSize: 12, color: 'var(--color-danger, crimson)' }}>{error}</Text>}
|
|
201
|
-
</View>
|
|
230
|
+
)}
|
|
231
|
+
{uploading && <Text style={{ fontSize: 12 }}>Uploading…</Text>}
|
|
232
|
+
{error && <Text style={{ fontSize: 12, color: 'var(--color-danger, crimson)' }}>{error}</Text>}
|
|
233
|
+
</View>
|
|
234
|
+
</DropZone>
|
|
202
235
|
)
|
|
203
236
|
}
|
|
204
237
|
|
|
205
238
|
const previewSrc = localFile ? null : previewUrl
|
|
206
239
|
|
|
207
240
|
return (
|
|
208
|
-
<
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
241
|
+
<DropZone onFilesDrop={onFilesDrop}>
|
|
242
|
+
<View gap="s" style={style} data-ossy-file-dropzone {...rest}>
|
|
243
|
+
{(localFile || previewSrc || resourceId) && (
|
|
244
|
+
<View layout="row" gap="s" alignItems="center">
|
|
245
|
+
{localFile && <LocalFilePreview file={localFile} size="48px" />}
|
|
246
|
+
{!localFile && previewSrc && (
|
|
247
|
+
<View
|
|
248
|
+
as="img"
|
|
249
|
+
src={previewSrc}
|
|
250
|
+
alt={label || name}
|
|
251
|
+
width="48px"
|
|
252
|
+
height="48px"
|
|
253
|
+
style={{ objectFit: 'cover', borderRadius: 'var(--space-s)' }}
|
|
254
|
+
/>
|
|
255
|
+
)}
|
|
256
|
+
{localFile && (
|
|
257
|
+
<Text style={{ flex: 1, fontSize: 14 }}>{localFile.name}</Text>
|
|
258
|
+
)}
|
|
259
|
+
{uploading && <Text style={{ fontSize: 12 }}>Uploading…</Text>}
|
|
260
|
+
</View>
|
|
261
|
+
)}
|
|
262
|
+
<UploadInput
|
|
263
|
+
id={name}
|
|
264
|
+
name={name}
|
|
265
|
+
accept={accept}
|
|
266
|
+
disabled={busy}
|
|
267
|
+
required={required && !resourceId}
|
|
268
|
+
onChange={onSinglePick}
|
|
269
|
+
/>
|
|
270
|
+
{error && <Text style={{ fontSize: 12, color: 'var(--color-danger, crimson)' }}>{error}</Text>}
|
|
271
|
+
</View>
|
|
272
|
+
</DropZone>
|
|
238
273
|
)
|
|
239
274
|
}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import React, { useCallback, useMemo, useState } from 'react'
|
|
2
|
+
import { View, Text, Button, Select } from '@ossy/design-system'
|
|
3
|
+
import { useSdk, AsyncStatus } from '@ossy/sdk-react'
|
|
4
|
+
import { metadata as SearchResources } from './search.action.js'
|
|
5
|
+
import {
|
|
6
|
+
commitResourceValue,
|
|
7
|
+
resourceIdFromValue,
|
|
8
|
+
resourceIdsFromValue,
|
|
9
|
+
resourcePickerLabel,
|
|
10
|
+
sortResourcesForPicker,
|
|
11
|
+
toReferenceArray,
|
|
12
|
+
} from './reference-field.helpers.js'
|
|
13
|
+
import { useSchema } from './useSchemas.js'
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Platform document-link field — stores `{ resourceId }` (or `{ resourceId }[]` when
|
|
17
|
+
* `max > 1`) pointing at other documents. When `of` is set, lists matching workspace
|
|
18
|
+
* resources via SearchResources for picking.
|
|
19
|
+
* Register as `@ossy/design-system/input/reference` through app component slots.
|
|
20
|
+
*
|
|
21
|
+
* @param {object} props
|
|
22
|
+
* @param {string} [props.of] Target schema id (e.g. `@ossy/profile/schema/profile`)
|
|
23
|
+
*/
|
|
24
|
+
export function PlatformReferenceField({
|
|
25
|
+
name,
|
|
26
|
+
label,
|
|
27
|
+
value,
|
|
28
|
+
onChange,
|
|
29
|
+
of: targetSchemaId,
|
|
30
|
+
max = 1,
|
|
31
|
+
disabled,
|
|
32
|
+
required,
|
|
33
|
+
style = {},
|
|
34
|
+
...rest
|
|
35
|
+
}) {
|
|
36
|
+
const sdk = useSdk()
|
|
37
|
+
const targetSchema = useSchema(targetSchemaId)
|
|
38
|
+
const targetLabel = targetSchema?.name || targetSchemaId
|
|
39
|
+
const multi = max > 1
|
|
40
|
+
const resourceId = multi ? null : resourceIdFromValue(value)
|
|
41
|
+
const resourceIds = multi ? resourceIdsFromValue(value) : []
|
|
42
|
+
|
|
43
|
+
const searchPayload = useMemo(
|
|
44
|
+
() => (targetSchemaId ? { type: targetSchemaId } : null),
|
|
45
|
+
[targetSchemaId],
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
const { status, data: resources = [] } = searchPayload
|
|
49
|
+
? sdk.read(SearchResources, searchPayload)
|
|
50
|
+
: { status: AsyncStatus.Success, data: [] }
|
|
51
|
+
|
|
52
|
+
const options = useMemo(() => sortResourcesForPicker(resources), [resources])
|
|
53
|
+
|
|
54
|
+
const labelForId = useCallback(
|
|
55
|
+
id => {
|
|
56
|
+
const match = options.find(resource => resource.id === id)
|
|
57
|
+
return match ? resourcePickerLabel(match) : id
|
|
58
|
+
},
|
|
59
|
+
[options],
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
const selectResourceSingle = useCallback(
|
|
63
|
+
event => {
|
|
64
|
+
const next = event.target.value
|
|
65
|
+
if (!next) {
|
|
66
|
+
commitResourceValue(name, null, onChange)
|
|
67
|
+
return
|
|
68
|
+
}
|
|
69
|
+
commitResourceValue(name, { resourceId: next }, onChange)
|
|
70
|
+
},
|
|
71
|
+
[name, onChange],
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
const clearSingle = useCallback(() => {
|
|
75
|
+
commitResourceValue(name, null, onChange)
|
|
76
|
+
}, [name, onChange])
|
|
77
|
+
|
|
78
|
+
const addMulti = useCallback(
|
|
79
|
+
nextId => {
|
|
80
|
+
const id = typeof nextId === 'string' ? nextId.trim() : ''
|
|
81
|
+
if (!id || resourceIds.includes(id) || resourceIds.length >= max) return
|
|
82
|
+
commitResourceValue(name, toReferenceArray([...resourceIds, id]), onChange)
|
|
83
|
+
},
|
|
84
|
+
[max, name, onChange, resourceIds],
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
const selectResourceMulti = useCallback(
|
|
88
|
+
event => {
|
|
89
|
+
const next = event.target.value
|
|
90
|
+
event.target.value = ''
|
|
91
|
+
addMulti(next)
|
|
92
|
+
},
|
|
93
|
+
[addMulti],
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
const removeMulti = useCallback(
|
|
97
|
+
id => {
|
|
98
|
+
commitResourceValue(
|
|
99
|
+
name,
|
|
100
|
+
toReferenceArray(resourceIds.filter(rid => rid !== id)),
|
|
101
|
+
onChange,
|
|
102
|
+
)
|
|
103
|
+
},
|
|
104
|
+
[name, onChange, resourceIds],
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
if (!targetSchemaId) {
|
|
108
|
+
return (
|
|
109
|
+
<Text variant="small" color="neutral-600">
|
|
110
|
+
Reference fields require a target schema (`of`).
|
|
111
|
+
</Text>
|
|
112
|
+
)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const loading =
|
|
116
|
+
status === AsyncStatus.Loading || status === AsyncStatus.NotInitialized
|
|
117
|
+
|
|
118
|
+
if (multi) {
|
|
119
|
+
const canAdd = resourceIds.length < max
|
|
120
|
+
const availableOptions = options.filter(resource => !resourceIds.includes(resource.id))
|
|
121
|
+
|
|
122
|
+
return (
|
|
123
|
+
<View gap="s" style={style} data-ossy-reference-field={name} data-ossy-reference-multi="">
|
|
124
|
+
<Text variant="small" color="neutral-600">
|
|
125
|
+
Links to {targetLabel} (up to {max})
|
|
126
|
+
</Text>
|
|
127
|
+
|
|
128
|
+
{resourceIds.map(id => (
|
|
129
|
+
<View
|
|
130
|
+
key={id}
|
|
131
|
+
layout="row"
|
|
132
|
+
gap="s"
|
|
133
|
+
style={{ alignItems: 'center' }}
|
|
134
|
+
data-ossy-reference-selected={id}
|
|
135
|
+
>
|
|
136
|
+
<Text variant="small">{labelForId(id)}</Text>
|
|
137
|
+
<Button
|
|
138
|
+
type="button"
|
|
139
|
+
variant="command"
|
|
140
|
+
prefix="remove"
|
|
141
|
+
aria-label="Remove"
|
|
142
|
+
disabled={disabled}
|
|
143
|
+
onClick={() => removeMulti(id)}
|
|
144
|
+
data-ossy-reference-remove={id}
|
|
145
|
+
/>
|
|
146
|
+
</View>
|
|
147
|
+
))}
|
|
148
|
+
|
|
149
|
+
{canAdd ? (
|
|
150
|
+
<Select
|
|
151
|
+
{...rest}
|
|
152
|
+
id={name}
|
|
153
|
+
name={name}
|
|
154
|
+
value=""
|
|
155
|
+
disabled={disabled || loading}
|
|
156
|
+
required={required && resourceIds.length === 0}
|
|
157
|
+
aria-label={label || name}
|
|
158
|
+
onChange={selectResourceMulti}
|
|
159
|
+
data-ossy-reference-picker={name}
|
|
160
|
+
style={{ minWidth: '50%' }}
|
|
161
|
+
>
|
|
162
|
+
<option value="">
|
|
163
|
+
{loading ? 'Loading…' : 'Add a document…'}
|
|
164
|
+
</option>
|
|
165
|
+
{availableOptions.map(resource => (
|
|
166
|
+
<option key={resource.id} value={resource.id}>
|
|
167
|
+
{resourcePickerLabel(resource)}
|
|
168
|
+
</option>
|
|
169
|
+
))}
|
|
170
|
+
</Select>
|
|
171
|
+
) : null}
|
|
172
|
+
</View>
|
|
173
|
+
)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const selectedInOptions = options.some(resource => resource.id === resourceId)
|
|
177
|
+
|
|
178
|
+
return (
|
|
179
|
+
<View gap="s" style={style} data-ossy-reference-field={name}>
|
|
180
|
+
<Text variant="small" color="neutral-600">
|
|
181
|
+
Links to {targetLabel}
|
|
182
|
+
</Text>
|
|
183
|
+
|
|
184
|
+
<Select
|
|
185
|
+
{...rest}
|
|
186
|
+
id={name}
|
|
187
|
+
name={name}
|
|
188
|
+
value={selectedInOptions ? resourceId : ''}
|
|
189
|
+
disabled={disabled || loading}
|
|
190
|
+
required={required && !resourceId}
|
|
191
|
+
aria-label={label || name}
|
|
192
|
+
onChange={selectResourceSingle}
|
|
193
|
+
data-ossy-reference-picker={name}
|
|
194
|
+
style={{ minWidth: '50%' }}
|
|
195
|
+
>
|
|
196
|
+
<option value="">
|
|
197
|
+
{loading ? 'Loading…' : 'Select a document…'}
|
|
198
|
+
</option>
|
|
199
|
+
{options.map(resource => (
|
|
200
|
+
<option key={resource.id} value={resource.id}>
|
|
201
|
+
{resourcePickerLabel(resource)}
|
|
202
|
+
</option>
|
|
203
|
+
))}
|
|
204
|
+
{resourceId && !selectedInOptions ? (
|
|
205
|
+
<option value={resourceId}>{resourceId}</option>
|
|
206
|
+
) : null}
|
|
207
|
+
</Select>
|
|
208
|
+
|
|
209
|
+
{resourceId ? (
|
|
210
|
+
<Button type="button" variant="neutral" disabled={disabled} onClick={clearSingle}>
|
|
211
|
+
Clear
|
|
212
|
+
</Button>
|
|
213
|
+
) : null}
|
|
214
|
+
</View>
|
|
215
|
+
)
|
|
216
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
2
|
import { metadata as GetResource } from './get.action.js'
|
|
3
|
+
import { resourceDownloadHref } from './resource-download.js'
|
|
3
4
|
import { AsyncStatus, useSdk } from '@ossy/sdk-react'
|
|
4
5
|
import { Switch, View, DelayedRender, Button, Icon, PageSection, Text, Tags, ImageCard } from '@ossy/design-system'
|
|
5
6
|
import { useRouter } from '@ossy/router-react'
|
|
@@ -94,8 +95,7 @@ export const ResourceContentPage = (props) => {
|
|
|
94
95
|
<View layout="row" gap="m">
|
|
95
96
|
<Button
|
|
96
97
|
variant="cta"
|
|
97
|
-
|
|
98
|
-
href={resource?.content?.src}
|
|
98
|
+
href={resourceDownloadHref(resource)}
|
|
99
99
|
download={resource?.name}
|
|
100
100
|
suffix="software-download"
|
|
101
101
|
>Download
|
|
@@ -115,6 +115,9 @@ export const ResourceContentPage = (props) => {
|
|
|
115
115
|
onClick={() => { }}
|
|
116
116
|
src={resource?.content?.sizes?.galleryLarge || resource?.content?.src}
|
|
117
117
|
placeholderSrc={resource?.content?.sizes?.['loader-square-blurred-after'] || resource?.content?.src}
|
|
118
|
+
blurhash={resource?.content?.blurhash}
|
|
119
|
+
width={resource?.content?.width}
|
|
120
|
+
height={resource?.content?.height}
|
|
118
121
|
style={{ pageBreakInside: 'avoid', order: 1 }}
|
|
119
122
|
/>
|
|
120
123
|
)
|