@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
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
import React, { useRef, useEffect, useMemo, useCallback, memo, useState } from 'react'
|
|
3
|
+
import { useSchemas } from './useSchemas.js'
|
|
4
|
+
import {
|
|
5
|
+
Dropdown,
|
|
6
|
+
DropZone,
|
|
7
|
+
Icon,
|
|
8
|
+
Image,
|
|
9
|
+
View,
|
|
10
|
+
Text,
|
|
11
|
+
Button,
|
|
12
|
+
ContextMenu,
|
|
13
|
+
} from '@ossy/design-system'
|
|
14
|
+
import { formatBytes } from './utils/format-bytes.js'
|
|
15
|
+
import { ResourceSelectionCheckbox } from './ResourceSelectionCheckbox.jsx'
|
|
16
|
+
|
|
17
|
+
const TILE_WIDTH = 160
|
|
18
|
+
|
|
19
|
+
function useSchemaMap() {
|
|
20
|
+
const templates = useSchemas()
|
|
21
|
+
return useMemo(
|
|
22
|
+
() => templates.reduce((acc, template) => ({ ...acc, [template.id]: template }), {}),
|
|
23
|
+
[templates],
|
|
24
|
+
)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const ResourceGrid = ({
|
|
28
|
+
resources = [],
|
|
29
|
+
onClick = () => {},
|
|
30
|
+
onToggleSelect,
|
|
31
|
+
inlineFolder = null,
|
|
32
|
+
selectedIds,
|
|
33
|
+
showSelectionControls = false,
|
|
34
|
+
}) => {
|
|
35
|
+
const templateMap = useSchemaMap()
|
|
36
|
+
const selectedSet = useMemo(() => {
|
|
37
|
+
if (selectedIds instanceof Set) return selectedIds
|
|
38
|
+
return new Set(selectedIds || [])
|
|
39
|
+
}, [selectedIds])
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<View
|
|
43
|
+
layout="row-wrap"
|
|
44
|
+
gap="m"
|
|
45
|
+
inset="m"
|
|
46
|
+
alignItems="stretch"
|
|
47
|
+
style={{ alignContent: 'flex-start' }}
|
|
48
|
+
>
|
|
49
|
+
{inlineFolder && <InlineFolderTile {...inlineFolder} />}
|
|
50
|
+
{resources.map(resource => (
|
|
51
|
+
<ResourceGridItem
|
|
52
|
+
key={resource.id}
|
|
53
|
+
resource={resource}
|
|
54
|
+
templateMap={templateMap}
|
|
55
|
+
onItemClick={onClick}
|
|
56
|
+
onToggleSelect={onToggleSelect}
|
|
57
|
+
showSelectionControls={showSelectionControls}
|
|
58
|
+
selected={selectedSet.has(resource.id) || Boolean(resource.selected)}
|
|
59
|
+
/>
|
|
60
|
+
))}
|
|
61
|
+
</View>
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function InlineFolderTile({ name, onChange, onSave, onCancel }) {
|
|
66
|
+
const inputRef = useRef(null)
|
|
67
|
+
const handledRef = useRef(false)
|
|
68
|
+
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
if (inputRef.current) {
|
|
71
|
+
inputRef.current.focus()
|
|
72
|
+
inputRef.current.select()
|
|
73
|
+
}
|
|
74
|
+
}, [])
|
|
75
|
+
|
|
76
|
+
const handleKeyDown = (event) => {
|
|
77
|
+
if (event.key === 'Enter') {
|
|
78
|
+
handledRef.current = true
|
|
79
|
+
onSave(event.target.value)
|
|
80
|
+
} else if (event.key === 'Escape') {
|
|
81
|
+
handledRef.current = true
|
|
82
|
+
onCancel()
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const handleBlur = (event) => {
|
|
87
|
+
if (handledRef.current) return
|
|
88
|
+
onSave(event.target.value)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return (
|
|
92
|
+
<View
|
|
93
|
+
surface="primary"
|
|
94
|
+
roundness="m"
|
|
95
|
+
inset="m"
|
|
96
|
+
gap="s"
|
|
97
|
+
style={{
|
|
98
|
+
width: TILE_WIDTH,
|
|
99
|
+
border: '1px dashed var(--separator-primary)',
|
|
100
|
+
boxSizing: 'border-box',
|
|
101
|
+
}}
|
|
102
|
+
>
|
|
103
|
+
<View layout="row" alignItems="center" justifyContent="center" style={{ height: 96 }}>
|
|
104
|
+
<Icon size="m" name="folder" style={{ fill: 'hsl(0, 0%, 60%)' }} />
|
|
105
|
+
</View>
|
|
106
|
+
<input
|
|
107
|
+
ref={inputRef}
|
|
108
|
+
value={name}
|
|
109
|
+
onChange={event => onChange(event.target.value)}
|
|
110
|
+
onKeyDown={handleKeyDown}
|
|
111
|
+
onBlur={handleBlur}
|
|
112
|
+
style={{
|
|
113
|
+
width: '100%',
|
|
114
|
+
border: 'none',
|
|
115
|
+
background: 'transparent',
|
|
116
|
+
outline: 'none',
|
|
117
|
+
fontSize: 'var(--text-small-font-size, 14px)',
|
|
118
|
+
fontFamily: 'var(--text-default-font-family, sans-serif)',
|
|
119
|
+
fontWeight: 'var(--text-default-font-weight, 400)',
|
|
120
|
+
color: 'var(--text-default-color, CanvasText)',
|
|
121
|
+
padding: 0,
|
|
122
|
+
minWidth: 0,
|
|
123
|
+
boxSizing: 'border-box',
|
|
124
|
+
}}
|
|
125
|
+
/>
|
|
126
|
+
</View>
|
|
127
|
+
)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const ResourceGridItem = memo(function ResourceGridItem({
|
|
131
|
+
resource,
|
|
132
|
+
templateMap,
|
|
133
|
+
onItemClick,
|
|
134
|
+
onToggleSelect,
|
|
135
|
+
showSelectionControls = false,
|
|
136
|
+
selected = false,
|
|
137
|
+
}) {
|
|
138
|
+
const { onClick, href, onDrop, onFilesDrop, dragData, actions, name, type, content } = resource
|
|
139
|
+
const [menuOpen, setMenuOpen] = useState(false)
|
|
140
|
+
const [menuPosition, setMenuPosition] = useState(null)
|
|
141
|
+
|
|
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)
|
|
148
|
+
}, [onItemClick, onClick, resource])
|
|
149
|
+
|
|
150
|
+
const handleToggleSelect = useCallback((event) => {
|
|
151
|
+
onToggleSelect?.(resource, event)
|
|
152
|
+
}, [onToggleSelect, resource])
|
|
153
|
+
|
|
154
|
+
const handleContextMenu = useCallback((event) => {
|
|
155
|
+
if (!actions) return
|
|
156
|
+
event.preventDefault()
|
|
157
|
+
event.stopPropagation()
|
|
158
|
+
setMenuPosition({ top: event.clientY, left: event.clientX })
|
|
159
|
+
setMenuOpen(true)
|
|
160
|
+
}, [actions])
|
|
161
|
+
|
|
162
|
+
const handleMenuOpenChange = useCallback((open) => {
|
|
163
|
+
setMenuOpen(open)
|
|
164
|
+
if (!open) setMenuPosition(null)
|
|
165
|
+
}, [])
|
|
166
|
+
|
|
167
|
+
const handleActionsTriggerClick = useCallback(() => {
|
|
168
|
+
setMenuPosition(null)
|
|
169
|
+
}, [])
|
|
170
|
+
|
|
171
|
+
let Container = ({ children }) => <>{children}</>
|
|
172
|
+
|
|
173
|
+
if (onDrop || onFilesDrop) {
|
|
174
|
+
Container = DropZone
|
|
175
|
+
} else if (dragData) {
|
|
176
|
+
Container = DropZone.Dragable
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const tile = (
|
|
180
|
+
<View
|
|
181
|
+
as={href ? 'a' : 'div'}
|
|
182
|
+
href={href}
|
|
183
|
+
onClick={handleClick}
|
|
184
|
+
onContextMenu={handleContextMenu}
|
|
185
|
+
selectable
|
|
186
|
+
data-selected={selected || undefined}
|
|
187
|
+
surface="primary"
|
|
188
|
+
roundness="m"
|
|
189
|
+
inset="m"
|
|
190
|
+
gap="s"
|
|
191
|
+
data-resource-id={resource.id}
|
|
192
|
+
style={{
|
|
193
|
+
width: TILE_WIDTH,
|
|
194
|
+
textDecoration: 'none',
|
|
195
|
+
color: 'inherit',
|
|
196
|
+
cursor: 'pointer',
|
|
197
|
+
border: selected
|
|
198
|
+
? '1px solid var(--foreground-active, var(--separator-primary))'
|
|
199
|
+
: '1px solid var(--separator-primary)',
|
|
200
|
+
boxSizing: 'border-box',
|
|
201
|
+
position: 'relative',
|
|
202
|
+
}}
|
|
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
|
+
)}
|
|
220
|
+
{actions ? (
|
|
221
|
+
<View
|
|
222
|
+
style={{
|
|
223
|
+
position: 'absolute',
|
|
224
|
+
top: 'var(--space-xs, 4px)',
|
|
225
|
+
right: 'var(--space-xs, 4px)',
|
|
226
|
+
zIndex: 1,
|
|
227
|
+
}}
|
|
228
|
+
onClick={event => event.stopPropagation()}
|
|
229
|
+
>
|
|
230
|
+
<RowActions
|
|
231
|
+
actions={actions}
|
|
232
|
+
open={menuOpen}
|
|
233
|
+
onOpenChange={handleMenuOpenChange}
|
|
234
|
+
position={menuPosition}
|
|
235
|
+
onTriggerClick={handleActionsTriggerClick}
|
|
236
|
+
/>
|
|
237
|
+
</View>
|
|
238
|
+
) : null}
|
|
239
|
+
<View layout="row" alignItems="center" justifyContent="center" style={{ height: 96 }}>
|
|
240
|
+
<ResourceThumb type={type} content={content} templateMap={templateMap} />
|
|
241
|
+
</View>
|
|
242
|
+
<Text
|
|
243
|
+
variant="small"
|
|
244
|
+
style={{
|
|
245
|
+
fontWeight: 'bold',
|
|
246
|
+
overflow: 'hidden',
|
|
247
|
+
textOverflow: 'ellipsis',
|
|
248
|
+
whiteSpace: 'nowrap',
|
|
249
|
+
}}
|
|
250
|
+
>
|
|
251
|
+
{typeof name === 'string' ? name : ''}
|
|
252
|
+
</Text>
|
|
253
|
+
<Size content={content} />
|
|
254
|
+
</View>
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
return (
|
|
258
|
+
<Container onDrop={onDrop} onFilesDrop={onFilesDrop} dragData={dragData}>
|
|
259
|
+
{tile}
|
|
260
|
+
</Container>
|
|
261
|
+
)
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
const ResourceThumb = memo(function ResourceThumb({ type, content, templateMap }) {
|
|
265
|
+
if (type === '@ossy/platform/schema/directory' || type === 'directory') {
|
|
266
|
+
return (
|
|
267
|
+
<Icon size="l" name="folder" style={{ fill: 'hsl(0, 0%, 60%)' }} />
|
|
268
|
+
)
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const mediaType = content?.ContentType || type
|
|
272
|
+
if (typeof mediaType === 'string' && mediaType.startsWith('image')) {
|
|
273
|
+
return (
|
|
274
|
+
<Image
|
|
275
|
+
src={content?.sizes?.thumbnailSmall || content?.src}
|
|
276
|
+
placeholderSrc={content?.sizes?.['loader-square-blurred-after'] || content?.src}
|
|
277
|
+
style={{
|
|
278
|
+
width: 96,
|
|
279
|
+
height: 96,
|
|
280
|
+
objectFit: 'cover',
|
|
281
|
+
borderRadius: 'var(--roundness-s, 8px)',
|
|
282
|
+
}}
|
|
283
|
+
/>
|
|
284
|
+
)
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return (
|
|
288
|
+
<Icon size="l" name={templateMap[type]?.icon || 'file'} style={{ fill: 'hsl(0, 0%, 80%)' }} />
|
|
289
|
+
)
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
function Size({ content }) {
|
|
293
|
+
if (content?.ContentLength) {
|
|
294
|
+
return (
|
|
295
|
+
<Text variant="small" style={{ color: 'var(--text-secondary-color, hsl(0, 0%, 50%))' }}>
|
|
296
|
+
{formatBytes(content.ContentLength)}
|
|
297
|
+
</Text>
|
|
298
|
+
)
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
return null
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function RowActions({ actions, open, onOpenChange, position, onTriggerClick }) {
|
|
305
|
+
if (!actions) return null
|
|
306
|
+
|
|
307
|
+
return (
|
|
308
|
+
<Dropdown
|
|
309
|
+
trigger={<Button prefix="more-vertical-alt" variant="command" onClick={onTriggerClick} />}
|
|
310
|
+
open={open}
|
|
311
|
+
onOpenChange={onOpenChange}
|
|
312
|
+
position={position}
|
|
313
|
+
>
|
|
314
|
+
<View inset="xs" surface="primary" roundness="s">
|
|
315
|
+
<ContextMenu roundness="s" surface="primary">
|
|
316
|
+
{actions}
|
|
317
|
+
</ContextMenu>
|
|
318
|
+
</View>
|
|
319
|
+
</Dropdown>
|
|
320
|
+
)
|
|
321
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import React, { useMemo, useState } from 'react'
|
|
2
|
+
import { Button, ContextMenu, Dropdown, View } from '@ossy/design-system'
|
|
3
|
+
import { WorkspaceProvider } from '@ossy/sdk-react'
|
|
4
|
+
import { ResourceGrid } from './ResourceGrid.jsx'
|
|
5
|
+
import { ResourceList } from './ResourceList.jsx'
|
|
6
|
+
|
|
7
|
+
const mockSdk = {
|
|
8
|
+
invoke: async () => [],
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const sampleResources = [
|
|
12
|
+
{
|
|
13
|
+
id: 'folder-1',
|
|
14
|
+
name: 'Projects',
|
|
15
|
+
type: '@ossy/platform/schema/directory',
|
|
16
|
+
content: {},
|
|
17
|
+
actions: [
|
|
18
|
+
<ContextMenu.Item key="open" prefix="mail-forward">Open</ContextMenu.Item>,
|
|
19
|
+
<ContextMenu.Separator key="sep" />,
|
|
20
|
+
<ContextMenu.Item key="remove" variant="command-danger" prefix="trash-empty">Remove</ContextMenu.Item>,
|
|
21
|
+
],
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
id: 'file-1',
|
|
25
|
+
name: 'brief.pdf',
|
|
26
|
+
type: '@ossy/platform/schema/file',
|
|
27
|
+
content: { ContentType: 'application/pdf', ContentLength: 245760 },
|
|
28
|
+
actions: [
|
|
29
|
+
<ContextMenu.Item key="view" prefix="details-more">View</ContextMenu.Item>,
|
|
30
|
+
<ContextMenu.Item key="remove" variant="command-danger" prefix="trash-empty">Remove</ContextMenu.Item>,
|
|
31
|
+
],
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
id: 'file-2',
|
|
35
|
+
name: 'notes.txt',
|
|
36
|
+
type: '@ossy/platform/schema/file',
|
|
37
|
+
content: { ContentType: 'text/plain', ContentLength: 1024 },
|
|
38
|
+
actions: [
|
|
39
|
+
<ContextMenu.Item key="view" prefix="details-more">View</ContextMenu.Item>,
|
|
40
|
+
<ContextMenu.Item key="remove" variant="command-danger" prefix="trash-empty">Remove</ContextMenu.Item>,
|
|
41
|
+
],
|
|
42
|
+
},
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
function ViewToggleDemo() {
|
|
46
|
+
const [viewMode, setViewMode] = useState('list')
|
|
47
|
+
const resources = useMemo(() => sampleResources, [])
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<WorkspaceProvider sdk={mockSdk}>
|
|
51
|
+
<View gap="m">
|
|
52
|
+
<Dropdown
|
|
53
|
+
trigger={(
|
|
54
|
+
<Button
|
|
55
|
+
prefix={viewMode === 'grid' ? 'layout-grid' : 'layout-list'}
|
|
56
|
+
variant="command"
|
|
57
|
+
aria-label="Layout"
|
|
58
|
+
/>
|
|
59
|
+
)}
|
|
60
|
+
>
|
|
61
|
+
<View inset="xs" surface="primary" roundness="s">
|
|
62
|
+
<ContextMenu roundness="s" surface="primary">
|
|
63
|
+
<ContextMenu.Item
|
|
64
|
+
prefix="layout-list"
|
|
65
|
+
suffix={viewMode === 'list' ? 'check' : undefined}
|
|
66
|
+
onClick={() => setViewMode('list')}
|
|
67
|
+
>
|
|
68
|
+
List view
|
|
69
|
+
</ContextMenu.Item>
|
|
70
|
+
<ContextMenu.Item
|
|
71
|
+
prefix="layout-grid"
|
|
72
|
+
suffix={viewMode === 'grid' ? 'check' : undefined}
|
|
73
|
+
onClick={() => setViewMode('grid')}
|
|
74
|
+
>
|
|
75
|
+
Grid view
|
|
76
|
+
</ContextMenu.Item>
|
|
77
|
+
</ContextMenu>
|
|
78
|
+
</View>
|
|
79
|
+
</Dropdown>
|
|
80
|
+
{viewMode === 'grid'
|
|
81
|
+
? <ResourceGrid resources={resources} />
|
|
82
|
+
: <ResourceList resources={resources} />}
|
|
83
|
+
</View>
|
|
84
|
+
</WorkspaceProvider>
|
|
85
|
+
)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export default {
|
|
89
|
+
title: 'Resources/Resource views',
|
|
90
|
+
component: ResourceGrid,
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export const ListAndGridToggle = {
|
|
94
|
+
render: () => <ViewToggleDemo />,
|
|
95
|
+
}
|
package/src/ResourceList.jsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
'use client'
|
|
2
|
-
import React, { useRef, useEffect, useMemo, useCallback, memo } from 'react'
|
|
2
|
+
import React, { useRef, useEffect, useMemo, useCallback, memo, useState } from 'react'
|
|
3
3
|
import { useSchemas } from './useSchemas.js'
|
|
4
4
|
import {
|
|
5
5
|
Dropdown,
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
List,
|
|
14
14
|
} from '@ossy/design-system'
|
|
15
15
|
import { formatBytes } from './utils/format-bytes.js'
|
|
16
|
+
import { ResourceSelectionCheckbox } from './ResourceSelectionCheckbox.jsx'
|
|
16
17
|
|
|
17
18
|
function useSchemaMap() {
|
|
18
19
|
const templates = useSchemas()
|
|
@@ -25,9 +26,16 @@ function useSchemaMap() {
|
|
|
25
26
|
export const ResourceList = ({
|
|
26
27
|
resources = [],
|
|
27
28
|
onClick = () => {},
|
|
29
|
+
onToggleSelect,
|
|
28
30
|
inlineFolder = null,
|
|
31
|
+
selectedIds,
|
|
32
|
+
showSelectionControls = false,
|
|
29
33
|
}) => {
|
|
30
34
|
const templateMap = useSchemaMap()
|
|
35
|
+
const selectedSet = useMemo(() => {
|
|
36
|
+
if (selectedIds instanceof Set) return selectedIds
|
|
37
|
+
return new Set(selectedIds || [])
|
|
38
|
+
}, [selectedIds])
|
|
31
39
|
|
|
32
40
|
return (
|
|
33
41
|
<List>
|
|
@@ -38,6 +46,9 @@ export const ResourceList = ({
|
|
|
38
46
|
resource={resource}
|
|
39
47
|
templateMap={templateMap}
|
|
40
48
|
onItemClick={onClick}
|
|
49
|
+
onToggleSelect={onToggleSelect}
|
|
50
|
+
showSelectionControls={showSelectionControls}
|
|
51
|
+
selected={selectedSet.has(resource.id) || Boolean(resource.selected)}
|
|
41
52
|
/>
|
|
42
53
|
))}
|
|
43
54
|
</List>
|
|
@@ -102,14 +113,43 @@ const ResourceListItem = memo(function ResourceListItem({
|
|
|
102
113
|
resource,
|
|
103
114
|
templateMap,
|
|
104
115
|
onItemClick,
|
|
116
|
+
onToggleSelect,
|
|
117
|
+
showSelectionControls = false,
|
|
118
|
+
selected = false,
|
|
105
119
|
}) {
|
|
106
120
|
const { onClick, href, onDrop, onFilesDrop, dragData, actions, name, type, content } = resource
|
|
121
|
+
const [menuOpen, setMenuOpen] = useState(false)
|
|
122
|
+
const [menuPosition, setMenuPosition] = useState(null)
|
|
107
123
|
|
|
108
|
-
const handleClick = useCallback(() => {
|
|
109
|
-
|
|
110
|
-
|
|
124
|
+
const handleClick = useCallback((event) => {
|
|
125
|
+
if (event?.metaKey || event?.ctrlKey || event?.shiftKey) {
|
|
126
|
+
event.preventDefault?.()
|
|
127
|
+
}
|
|
128
|
+
onItemClick?.(resource, event)
|
|
129
|
+
onClick?.(resource, event)
|
|
111
130
|
}, [onItemClick, onClick, resource])
|
|
112
131
|
|
|
132
|
+
const handleToggleSelect = useCallback((event) => {
|
|
133
|
+
onToggleSelect?.(resource, event)
|
|
134
|
+
}, [onToggleSelect, resource])
|
|
135
|
+
|
|
136
|
+
const handleContextMenu = useCallback((event) => {
|
|
137
|
+
if (!actions) return
|
|
138
|
+
event.preventDefault()
|
|
139
|
+
event.stopPropagation()
|
|
140
|
+
setMenuPosition({ top: event.clientY, left: event.clientX })
|
|
141
|
+
setMenuOpen(true)
|
|
142
|
+
}, [actions])
|
|
143
|
+
|
|
144
|
+
const handleMenuOpenChange = useCallback((open) => {
|
|
145
|
+
setMenuOpen(open)
|
|
146
|
+
if (!open) setMenuPosition(null)
|
|
147
|
+
}, [])
|
|
148
|
+
|
|
149
|
+
const handleActionsTriggerClick = useCallback(() => {
|
|
150
|
+
setMenuPosition(null)
|
|
151
|
+
}, [])
|
|
152
|
+
|
|
113
153
|
let Container = ({ children }) => <>{children}</>
|
|
114
154
|
|
|
115
155
|
if (onDrop || onFilesDrop) {
|
|
@@ -118,15 +158,37 @@ const ResourceListItem = memo(function ResourceListItem({
|
|
|
118
158
|
Container = DropZone.Dragable
|
|
119
159
|
}
|
|
120
160
|
|
|
161
|
+
const leading = (
|
|
162
|
+
<View layout="row" alignItems="center" gap="s" style={{ flexShrink: 0 }}>
|
|
163
|
+
{(showSelectionControls || selected) && (
|
|
164
|
+
<ResourceSelectionCheckbox
|
|
165
|
+
selected={selected}
|
|
166
|
+
onToggle={handleToggleSelect}
|
|
167
|
+
/>
|
|
168
|
+
)}
|
|
169
|
+
<ResourceIcon type={type} content={content} templateMap={templateMap} />
|
|
170
|
+
</View>
|
|
171
|
+
)
|
|
172
|
+
|
|
121
173
|
return (
|
|
122
174
|
<Container onDrop={onDrop} onFilesDrop={onFilesDrop} dragData={dragData}>
|
|
123
175
|
<List.Item
|
|
124
176
|
href={href}
|
|
125
177
|
onClick={handleClick}
|
|
178
|
+
onContextMenu={handleContextMenu}
|
|
126
179
|
selectable
|
|
127
|
-
|
|
180
|
+
selected={selected}
|
|
181
|
+
leading={leading}
|
|
128
182
|
meta={<Size content={content} />}
|
|
129
|
-
trailing={
|
|
183
|
+
trailing={(
|
|
184
|
+
<RowActions
|
|
185
|
+
actions={actions}
|
|
186
|
+
open={menuOpen}
|
|
187
|
+
onOpenChange={handleMenuOpenChange}
|
|
188
|
+
position={menuPosition}
|
|
189
|
+
onTriggerClick={handleActionsTriggerClick}
|
|
190
|
+
/>
|
|
191
|
+
)}
|
|
130
192
|
data-resource-id={resource.id}
|
|
131
193
|
>
|
|
132
194
|
{typeof name === 'string' ? name : ''}
|
|
@@ -149,6 +211,9 @@ const ResourceIcon = memo(function ResourceIcon({ type, content, templateMap })
|
|
|
149
211
|
<Image
|
|
150
212
|
src={content?.sizes?.thumbnailSmall || content?.src}
|
|
151
213
|
placeholderSrc={content?.sizes?.['loader-square-blurred-after'] || content?.src}
|
|
214
|
+
blurhash={content?.blurhash}
|
|
215
|
+
width={content?.width}
|
|
216
|
+
height={content?.height}
|
|
152
217
|
style={{ width: '24px', height: '24px', borderRadius: '25%' }}
|
|
153
218
|
/>
|
|
154
219
|
)
|
|
@@ -171,11 +236,16 @@ function Size({ content }) {
|
|
|
171
236
|
return null
|
|
172
237
|
}
|
|
173
238
|
|
|
174
|
-
function RowActions({ actions }) {
|
|
239
|
+
function RowActions({ actions, open, onOpenChange, position, onTriggerClick }) {
|
|
175
240
|
if (!actions) return null
|
|
176
241
|
|
|
177
242
|
return (
|
|
178
|
-
<Dropdown
|
|
243
|
+
<Dropdown
|
|
244
|
+
trigger={<Button prefix="more-vertical-alt" variant="command" onClick={onTriggerClick} />}
|
|
245
|
+
open={open}
|
|
246
|
+
onOpenChange={onOpenChange}
|
|
247
|
+
position={position}
|
|
248
|
+
>
|
|
179
249
|
<View inset="xs" surface="primary" roundness="s">
|
|
180
250
|
<ContextMenu roundness="s" surface="primary">
|
|
181
251
|
{actions}
|
package/src/ResourcePanel.jsx
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
import React, { useState, useEffect, memo } from 'react'
|
|
2
2
|
import { metadata as GetResource } from './get.action.js'
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
isDownloadableResource,
|
|
5
|
+
removeResource,
|
|
6
|
+
renameResource,
|
|
7
|
+
resourceDownloadHref,
|
|
8
|
+
updateResourceContent,
|
|
9
|
+
} from './resource.helpers.js'
|
|
4
10
|
import { useSdk } from '@ossy/sdk-react'
|
|
5
|
-
import { Switch, View, Text, Button, InputTitle, Alert, useInputValue } from '@ossy/design-system'
|
|
11
|
+
import { Switch, View, Text, Button, InputTitle, Alert, useInputValue, useLocale } from '@ossy/design-system'
|
|
6
12
|
import { ResourceFactory } from './ResourceFactory.jsx'
|
|
7
13
|
import { ResourceDetails } from './ResourceDetails.jsx'
|
|
8
14
|
import { ResourceTags } from './ResourceTags.jsx'
|
|
@@ -32,6 +38,7 @@ export const ResourcePanel = memo(function ResourcePanel ({
|
|
|
32
38
|
extraPanels = [],
|
|
33
39
|
}) {
|
|
34
40
|
const sdk = useSdk()
|
|
41
|
+
const { t } = useLocale()
|
|
35
42
|
const [contentError, setContentError] = useState()
|
|
36
43
|
const [resourceName, setResourceName] = useInputValue('')
|
|
37
44
|
const [panelViewModes, setPanelViewModes] = useState(DefaultViewModes)
|
|
@@ -39,6 +46,7 @@ export const ResourcePanel = memo(function ResourcePanel ({
|
|
|
39
46
|
const { data: resource = {} } = sdk.read(GetResource, { resourceId }, { enabled: !!resourceId })
|
|
40
47
|
const template = useSchema(resource?.type)
|
|
41
48
|
const engine = useSchemaEngine()
|
|
49
|
+
const downloadHref = isDownloadableResource(resource) ? resourceDownloadHref(resource) : null
|
|
42
50
|
|
|
43
51
|
const form = useForm({ defaultData: resource.content })
|
|
44
52
|
|
|
@@ -171,6 +179,15 @@ export const ResourcePanel = memo(function ResourcePanel ({
|
|
|
171
179
|
<Text as="h3" variant="breadcrumb">{resourceName}</Text>
|
|
172
180
|
</View.Item>
|
|
173
181
|
|
|
182
|
+
{downloadHref && (
|
|
183
|
+
<Button
|
|
184
|
+
prefix="software-download"
|
|
185
|
+
variant="command"
|
|
186
|
+
href={downloadHref}
|
|
187
|
+
download={resource.name}
|
|
188
|
+
aria-label={t('resources.panel.download')}
|
|
189
|
+
/>
|
|
190
|
+
)}
|
|
174
191
|
<Button prefix="trash-empty" variant="command-danger" onClick={onRemoveResource}/>
|
|
175
192
|
<Button prefix="pen" variant="command" onClick={() => setPanelViewModes(x => ({ ...x, 'panel-header': ViewMode.Edit }))} />
|
|
176
193
|
{ !!_onClose && (<Button prefix="close" variant="command" onClick={onCloseResource} /> ) }
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { View } from '@ossy/design-system'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Checkbox affordance for multi-select rows/tiles.
|
|
6
|
+
* Click toggles selection; does not navigate or open the resource panel.
|
|
7
|
+
*/
|
|
8
|
+
export function ResourceSelectionCheckbox ({
|
|
9
|
+
selected = false,
|
|
10
|
+
onToggle,
|
|
11
|
+
'aria-label': ariaLabel = 'Select',
|
|
12
|
+
}) {
|
|
13
|
+
return (
|
|
14
|
+
<View
|
|
15
|
+
role="checkbox"
|
|
16
|
+
aria-checked={selected}
|
|
17
|
+
aria-label={ariaLabel}
|
|
18
|
+
tabIndex={0}
|
|
19
|
+
onClick={(event) => {
|
|
20
|
+
event.preventDefault()
|
|
21
|
+
event.stopPropagation()
|
|
22
|
+
onToggle?.(event)
|
|
23
|
+
}}
|
|
24
|
+
onKeyDown={(event) => {
|
|
25
|
+
if (event.key !== 'Enter' && event.key !== ' ') return
|
|
26
|
+
event.preventDefault()
|
|
27
|
+
event.stopPropagation()
|
|
28
|
+
onToggle?.(event)
|
|
29
|
+
}}
|
|
30
|
+
style={{
|
|
31
|
+
boxSizing: 'border-box',
|
|
32
|
+
width: 18,
|
|
33
|
+
height: 18,
|
|
34
|
+
flexShrink: 0,
|
|
35
|
+
borderRadius: 4,
|
|
36
|
+
border: selected
|
|
37
|
+
? '1px solid var(--foreground-active, var(--foreground))'
|
|
38
|
+
: '1px solid var(--separator, hsl(0, 0%, 50%))',
|
|
39
|
+
background: selected
|
|
40
|
+
? 'var(--foreground-active, var(--foreground))'
|
|
41
|
+
: 'transparent',
|
|
42
|
+
display: 'inline-flex',
|
|
43
|
+
alignItems: 'center',
|
|
44
|
+
justifyContent: 'center',
|
|
45
|
+
cursor: 'pointer',
|
|
46
|
+
}}
|
|
47
|
+
>
|
|
48
|
+
{selected ? (
|
|
49
|
+
<svg
|
|
50
|
+
width="12"
|
|
51
|
+
height="12"
|
|
52
|
+
viewBox="0 0 12 12"
|
|
53
|
+
aria-hidden="true"
|
|
54
|
+
style={{ display: 'block' }}
|
|
55
|
+
>
|
|
56
|
+
<path
|
|
57
|
+
d="M2.5 6.2L4.8 8.5L9.5 3.5"
|
|
58
|
+
fill="none"
|
|
59
|
+
stroke="var(--background, Canvas)"
|
|
60
|
+
strokeWidth="1.75"
|
|
61
|
+
strokeLinecap="round"
|
|
62
|
+
strokeLinejoin="round"
|
|
63
|
+
/>
|
|
64
|
+
</svg>
|
|
65
|
+
) : null}
|
|
66
|
+
</View>
|
|
67
|
+
)
|
|
68
|
+
}
|
package/src/ResourcesPage.jsx
CHANGED
|
@@ -41,6 +41,9 @@ export const ResourcesPage= () => {
|
|
|
41
41
|
href={router.getHref({ id: "@resource", params: { resourceId: resource?.id } })}
|
|
42
42
|
src={resource?.content?.sizes?.galleryMedium || resource?.content?.src}
|
|
43
43
|
placeholderSrc={resource?.content?.sizes?.['loader-square-blurred-after'] || resource?.content?.src}
|
|
44
|
+
blurhash={resource?.content?.blurhash}
|
|
45
|
+
width={resource?.content?.width}
|
|
46
|
+
height={resource?.content?.height}
|
|
44
47
|
style={{ pageBreakInside: 'avoid', cursor: 'pointer' }}
|
|
45
48
|
key={resource.id}
|
|
46
49
|
/>
|