@ossy/resources 3.4.0 → 3.5.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/package.json +4 -4
- package/src/PlatformFileField.jsx +120 -85
- package/src/PlatformReferenceField.jsx +216 -0
- package/src/ResourceContentPage.jsx +2 -2
- package/src/ResourceGrid.jsx +44 -4
- package/src/ResourceList.jsx +38 -4
- package/src/ResourcePanel.jsx +19 -2
- package/src/ResourceSelectionCheckbox.jsx +68 -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 +2 -0
- package/src/markdown.component.jsx +20 -0
- package/src/markdown.schema.js +13 -0
- package/src/markdown.spec.js +43 -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.1",
|
|
5
5
|
"private": false,
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "./src/index.js",
|
|
@@ -25,8 +25,8 @@
|
|
|
25
25
|
"dependencies": {
|
|
26
26
|
"@ossy/event-store": "^3.4.0",
|
|
27
27
|
"@ossy/fold": "^3.4.0",
|
|
28
|
-
"@ossy/platform": "^3.
|
|
29
|
-
"@ossy/schema": "^3.
|
|
28
|
+
"@ossy/platform": "^3.5.1",
|
|
29
|
+
"@ossy/schema": "^3.5.1"
|
|
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": "cafa2d72ae9c4f6197ebec0b5b857919a062ceff"
|
|
53
53
|
}
|
|
@@ -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
|
package/src/ResourceGrid.jsx
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
ContextMenu,
|
|
13
13
|
} from '@ossy/design-system'
|
|
14
14
|
import { formatBytes } from './utils/format-bytes.js'
|
|
15
|
+
import { ResourceSelectionCheckbox } from './ResourceSelectionCheckbox.jsx'
|
|
15
16
|
|
|
16
17
|
const TILE_WIDTH = 160
|
|
17
18
|
|
|
@@ -26,9 +27,16 @@ function useSchemaMap() {
|
|
|
26
27
|
export const ResourceGrid = ({
|
|
27
28
|
resources = [],
|
|
28
29
|
onClick = () => {},
|
|
30
|
+
onToggleSelect,
|
|
29
31
|
inlineFolder = null,
|
|
32
|
+
selectedIds,
|
|
33
|
+
showSelectionControls = false,
|
|
30
34
|
}) => {
|
|
31
35
|
const templateMap = useSchemaMap()
|
|
36
|
+
const selectedSet = useMemo(() => {
|
|
37
|
+
if (selectedIds instanceof Set) return selectedIds
|
|
38
|
+
return new Set(selectedIds || [])
|
|
39
|
+
}, [selectedIds])
|
|
32
40
|
|
|
33
41
|
return (
|
|
34
42
|
<View
|
|
@@ -45,6 +53,9 @@ export const ResourceGrid = ({
|
|
|
45
53
|
resource={resource}
|
|
46
54
|
templateMap={templateMap}
|
|
47
55
|
onItemClick={onClick}
|
|
56
|
+
onToggleSelect={onToggleSelect}
|
|
57
|
+
showSelectionControls={showSelectionControls}
|
|
58
|
+
selected={selectedSet.has(resource.id) || Boolean(resource.selected)}
|
|
48
59
|
/>
|
|
49
60
|
))}
|
|
50
61
|
</View>
|
|
@@ -120,16 +131,26 @@ const ResourceGridItem = memo(function ResourceGridItem({
|
|
|
120
131
|
resource,
|
|
121
132
|
templateMap,
|
|
122
133
|
onItemClick,
|
|
134
|
+
onToggleSelect,
|
|
135
|
+
showSelectionControls = false,
|
|
136
|
+
selected = false,
|
|
123
137
|
}) {
|
|
124
138
|
const { onClick, href, onDrop, onFilesDrop, dragData, actions, name, type, content } = resource
|
|
125
139
|
const [menuOpen, setMenuOpen] = useState(false)
|
|
126
140
|
const [menuPosition, setMenuPosition] = useState(null)
|
|
127
141
|
|
|
128
|
-
const handleClick = useCallback(() => {
|
|
129
|
-
|
|
130
|
-
|
|
142
|
+
const handleClick = useCallback((event) => {
|
|
143
|
+
if (event?.metaKey || event?.ctrlKey || event?.shiftKey) {
|
|
144
|
+
event.preventDefault?.()
|
|
145
|
+
}
|
|
146
|
+
onItemClick?.(resource, event)
|
|
147
|
+
onClick?.(resource, event)
|
|
131
148
|
}, [onItemClick, onClick, resource])
|
|
132
149
|
|
|
150
|
+
const handleToggleSelect = useCallback((event) => {
|
|
151
|
+
onToggleSelect?.(resource, event)
|
|
152
|
+
}, [onToggleSelect, resource])
|
|
153
|
+
|
|
133
154
|
const handleContextMenu = useCallback((event) => {
|
|
134
155
|
if (!actions) return
|
|
135
156
|
event.preventDefault()
|
|
@@ -162,6 +183,7 @@ const ResourceGridItem = memo(function ResourceGridItem({
|
|
|
162
183
|
onClick={handleClick}
|
|
163
184
|
onContextMenu={handleContextMenu}
|
|
164
185
|
selectable
|
|
186
|
+
data-selected={selected || undefined}
|
|
165
187
|
surface="primary"
|
|
166
188
|
roundness="m"
|
|
167
189
|
inset="m"
|
|
@@ -172,11 +194,29 @@ const ResourceGridItem = memo(function ResourceGridItem({
|
|
|
172
194
|
textDecoration: 'none',
|
|
173
195
|
color: 'inherit',
|
|
174
196
|
cursor: 'pointer',
|
|
175
|
-
border:
|
|
197
|
+
border: selected
|
|
198
|
+
? '1px solid var(--foreground-active, var(--separator-primary))'
|
|
199
|
+
: '1px solid var(--separator-primary)',
|
|
176
200
|
boxSizing: 'border-box',
|
|
177
201
|
position: 'relative',
|
|
178
202
|
}}
|
|
179
203
|
>
|
|
204
|
+
{(showSelectionControls || selected) && (
|
|
205
|
+
<View
|
|
206
|
+
style={{
|
|
207
|
+
position: 'absolute',
|
|
208
|
+
top: 'var(--space-xs, 4px)',
|
|
209
|
+
left: 'var(--space-xs, 4px)',
|
|
210
|
+
zIndex: 1,
|
|
211
|
+
}}
|
|
212
|
+
onClick={event => event.stopPropagation()}
|
|
213
|
+
>
|
|
214
|
+
<ResourceSelectionCheckbox
|
|
215
|
+
selected={selected}
|
|
216
|
+
onToggle={handleToggleSelect}
|
|
217
|
+
/>
|
|
218
|
+
</View>
|
|
219
|
+
)}
|
|
180
220
|
{actions ? (
|
|
181
221
|
<View
|
|
182
222
|
style={{
|