@ossy/resources 3.3.0 → 3.4.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/ResourceContentPage.jsx +3 -0
- package/src/ResourceGrid.jsx +281 -0
- package/src/ResourceGrid.stories.jsx +95 -0
- package/src/ResourceList.jsx +40 -4
- package/src/ResourcesPage.jsx +3 -0
- package/src/index.js +2 -0
- package/src/patch-content.action.js +1 -0
- package/src/patch-content.task.js +28 -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.4.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.4.0",
|
|
29
|
+
"@ossy/schema": "^3.4.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": "d36b69444268d172bc3e8e1dc77e67afc63f8650"
|
|
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
|
)
|
|
@@ -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
|
)
|
|
@@ -0,0 +1,281 @@
|
|
|
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
|
+
|
|
16
|
+
const TILE_WIDTH = 160
|
|
17
|
+
|
|
18
|
+
function useSchemaMap() {
|
|
19
|
+
const templates = useSchemas()
|
|
20
|
+
return useMemo(
|
|
21
|
+
() => templates.reduce((acc, template) => ({ ...acc, [template.id]: template }), {}),
|
|
22
|
+
[templates],
|
|
23
|
+
)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const ResourceGrid = ({
|
|
27
|
+
resources = [],
|
|
28
|
+
onClick = () => {},
|
|
29
|
+
inlineFolder = null,
|
|
30
|
+
}) => {
|
|
31
|
+
const templateMap = useSchemaMap()
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<View
|
|
35
|
+
layout="row-wrap"
|
|
36
|
+
gap="m"
|
|
37
|
+
inset="m"
|
|
38
|
+
alignItems="stretch"
|
|
39
|
+
style={{ alignContent: 'flex-start' }}
|
|
40
|
+
>
|
|
41
|
+
{inlineFolder && <InlineFolderTile {...inlineFolder} />}
|
|
42
|
+
{resources.map(resource => (
|
|
43
|
+
<ResourceGridItem
|
|
44
|
+
key={resource.id}
|
|
45
|
+
resource={resource}
|
|
46
|
+
templateMap={templateMap}
|
|
47
|
+
onItemClick={onClick}
|
|
48
|
+
/>
|
|
49
|
+
))}
|
|
50
|
+
</View>
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function InlineFolderTile({ name, onChange, onSave, onCancel }) {
|
|
55
|
+
const inputRef = useRef(null)
|
|
56
|
+
const handledRef = useRef(false)
|
|
57
|
+
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
if (inputRef.current) {
|
|
60
|
+
inputRef.current.focus()
|
|
61
|
+
inputRef.current.select()
|
|
62
|
+
}
|
|
63
|
+
}, [])
|
|
64
|
+
|
|
65
|
+
const handleKeyDown = (event) => {
|
|
66
|
+
if (event.key === 'Enter') {
|
|
67
|
+
handledRef.current = true
|
|
68
|
+
onSave(event.target.value)
|
|
69
|
+
} else if (event.key === 'Escape') {
|
|
70
|
+
handledRef.current = true
|
|
71
|
+
onCancel()
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const handleBlur = (event) => {
|
|
76
|
+
if (handledRef.current) return
|
|
77
|
+
onSave(event.target.value)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
<View
|
|
82
|
+
surface="primary"
|
|
83
|
+
roundness="m"
|
|
84
|
+
inset="m"
|
|
85
|
+
gap="s"
|
|
86
|
+
style={{
|
|
87
|
+
width: TILE_WIDTH,
|
|
88
|
+
border: '1px dashed var(--separator-primary)',
|
|
89
|
+
boxSizing: 'border-box',
|
|
90
|
+
}}
|
|
91
|
+
>
|
|
92
|
+
<View layout="row" alignItems="center" justifyContent="center" style={{ height: 96 }}>
|
|
93
|
+
<Icon size="m" name="folder" style={{ fill: 'hsl(0, 0%, 60%)' }} />
|
|
94
|
+
</View>
|
|
95
|
+
<input
|
|
96
|
+
ref={inputRef}
|
|
97
|
+
value={name}
|
|
98
|
+
onChange={event => onChange(event.target.value)}
|
|
99
|
+
onKeyDown={handleKeyDown}
|
|
100
|
+
onBlur={handleBlur}
|
|
101
|
+
style={{
|
|
102
|
+
width: '100%',
|
|
103
|
+
border: 'none',
|
|
104
|
+
background: 'transparent',
|
|
105
|
+
outline: 'none',
|
|
106
|
+
fontSize: 'var(--text-small-font-size, 14px)',
|
|
107
|
+
fontFamily: 'var(--text-default-font-family, sans-serif)',
|
|
108
|
+
fontWeight: 'var(--text-default-font-weight, 400)',
|
|
109
|
+
color: 'var(--text-default-color, CanvasText)',
|
|
110
|
+
padding: 0,
|
|
111
|
+
minWidth: 0,
|
|
112
|
+
boxSizing: 'border-box',
|
|
113
|
+
}}
|
|
114
|
+
/>
|
|
115
|
+
</View>
|
|
116
|
+
)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const ResourceGridItem = memo(function ResourceGridItem({
|
|
120
|
+
resource,
|
|
121
|
+
templateMap,
|
|
122
|
+
onItemClick,
|
|
123
|
+
}) {
|
|
124
|
+
const { onClick, href, onDrop, onFilesDrop, dragData, actions, name, type, content } = resource
|
|
125
|
+
const [menuOpen, setMenuOpen] = useState(false)
|
|
126
|
+
const [menuPosition, setMenuPosition] = useState(null)
|
|
127
|
+
|
|
128
|
+
const handleClick = useCallback(() => {
|
|
129
|
+
onItemClick(resource)
|
|
130
|
+
onClick?.(resource)
|
|
131
|
+
}, [onItemClick, onClick, resource])
|
|
132
|
+
|
|
133
|
+
const handleContextMenu = useCallback((event) => {
|
|
134
|
+
if (!actions) return
|
|
135
|
+
event.preventDefault()
|
|
136
|
+
event.stopPropagation()
|
|
137
|
+
setMenuPosition({ top: event.clientY, left: event.clientX })
|
|
138
|
+
setMenuOpen(true)
|
|
139
|
+
}, [actions])
|
|
140
|
+
|
|
141
|
+
const handleMenuOpenChange = useCallback((open) => {
|
|
142
|
+
setMenuOpen(open)
|
|
143
|
+
if (!open) setMenuPosition(null)
|
|
144
|
+
}, [])
|
|
145
|
+
|
|
146
|
+
const handleActionsTriggerClick = useCallback(() => {
|
|
147
|
+
setMenuPosition(null)
|
|
148
|
+
}, [])
|
|
149
|
+
|
|
150
|
+
let Container = ({ children }) => <>{children}</>
|
|
151
|
+
|
|
152
|
+
if (onDrop || onFilesDrop) {
|
|
153
|
+
Container = DropZone
|
|
154
|
+
} else if (dragData) {
|
|
155
|
+
Container = DropZone.Dragable
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const tile = (
|
|
159
|
+
<View
|
|
160
|
+
as={href ? 'a' : 'div'}
|
|
161
|
+
href={href}
|
|
162
|
+
onClick={handleClick}
|
|
163
|
+
onContextMenu={handleContextMenu}
|
|
164
|
+
selectable
|
|
165
|
+
surface="primary"
|
|
166
|
+
roundness="m"
|
|
167
|
+
inset="m"
|
|
168
|
+
gap="s"
|
|
169
|
+
data-resource-id={resource.id}
|
|
170
|
+
style={{
|
|
171
|
+
width: TILE_WIDTH,
|
|
172
|
+
textDecoration: 'none',
|
|
173
|
+
color: 'inherit',
|
|
174
|
+
cursor: 'pointer',
|
|
175
|
+
border: '1px solid var(--separator-primary)',
|
|
176
|
+
boxSizing: 'border-box',
|
|
177
|
+
position: 'relative',
|
|
178
|
+
}}
|
|
179
|
+
>
|
|
180
|
+
{actions ? (
|
|
181
|
+
<View
|
|
182
|
+
style={{
|
|
183
|
+
position: 'absolute',
|
|
184
|
+
top: 'var(--space-xs, 4px)',
|
|
185
|
+
right: 'var(--space-xs, 4px)',
|
|
186
|
+
zIndex: 1,
|
|
187
|
+
}}
|
|
188
|
+
onClick={event => event.stopPropagation()}
|
|
189
|
+
>
|
|
190
|
+
<RowActions
|
|
191
|
+
actions={actions}
|
|
192
|
+
open={menuOpen}
|
|
193
|
+
onOpenChange={handleMenuOpenChange}
|
|
194
|
+
position={menuPosition}
|
|
195
|
+
onTriggerClick={handleActionsTriggerClick}
|
|
196
|
+
/>
|
|
197
|
+
</View>
|
|
198
|
+
) : null}
|
|
199
|
+
<View layout="row" alignItems="center" justifyContent="center" style={{ height: 96 }}>
|
|
200
|
+
<ResourceThumb type={type} content={content} templateMap={templateMap} />
|
|
201
|
+
</View>
|
|
202
|
+
<Text
|
|
203
|
+
variant="small"
|
|
204
|
+
style={{
|
|
205
|
+
fontWeight: 'bold',
|
|
206
|
+
overflow: 'hidden',
|
|
207
|
+
textOverflow: 'ellipsis',
|
|
208
|
+
whiteSpace: 'nowrap',
|
|
209
|
+
}}
|
|
210
|
+
>
|
|
211
|
+
{typeof name === 'string' ? name : ''}
|
|
212
|
+
</Text>
|
|
213
|
+
<Size content={content} />
|
|
214
|
+
</View>
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
return (
|
|
218
|
+
<Container onDrop={onDrop} onFilesDrop={onFilesDrop} dragData={dragData}>
|
|
219
|
+
{tile}
|
|
220
|
+
</Container>
|
|
221
|
+
)
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
const ResourceThumb = memo(function ResourceThumb({ type, content, templateMap }) {
|
|
225
|
+
if (type === '@ossy/platform/schema/directory' || type === 'directory') {
|
|
226
|
+
return (
|
|
227
|
+
<Icon size="l" name="folder" style={{ fill: 'hsl(0, 0%, 60%)' }} />
|
|
228
|
+
)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const mediaType = content?.ContentType || type
|
|
232
|
+
if (typeof mediaType === 'string' && mediaType.startsWith('image')) {
|
|
233
|
+
return (
|
|
234
|
+
<Image
|
|
235
|
+
src={content?.sizes?.thumbnailSmall || content?.src}
|
|
236
|
+
placeholderSrc={content?.sizes?.['loader-square-blurred-after'] || content?.src}
|
|
237
|
+
style={{
|
|
238
|
+
width: 96,
|
|
239
|
+
height: 96,
|
|
240
|
+
objectFit: 'cover',
|
|
241
|
+
borderRadius: 'var(--roundness-s, 8px)',
|
|
242
|
+
}}
|
|
243
|
+
/>
|
|
244
|
+
)
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return (
|
|
248
|
+
<Icon size="l" name={templateMap[type]?.icon || 'file'} style={{ fill: 'hsl(0, 0%, 80%)' }} />
|
|
249
|
+
)
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
function Size({ content }) {
|
|
253
|
+
if (content?.ContentLength) {
|
|
254
|
+
return (
|
|
255
|
+
<Text variant="small" style={{ color: 'var(--text-secondary-color, hsl(0, 0%, 50%))' }}>
|
|
256
|
+
{formatBytes(content.ContentLength)}
|
|
257
|
+
</Text>
|
|
258
|
+
)
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return null
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function RowActions({ actions, open, onOpenChange, position, onTriggerClick }) {
|
|
265
|
+
if (!actions) return null
|
|
266
|
+
|
|
267
|
+
return (
|
|
268
|
+
<Dropdown
|
|
269
|
+
trigger={<Button prefix="more-vertical-alt" variant="command" onClick={onTriggerClick} />}
|
|
270
|
+
open={open}
|
|
271
|
+
onOpenChange={onOpenChange}
|
|
272
|
+
position={position}
|
|
273
|
+
>
|
|
274
|
+
<View inset="xs" surface="primary" roundness="s">
|
|
275
|
+
<ContextMenu roundness="s" surface="primary">
|
|
276
|
+
{actions}
|
|
277
|
+
</ContextMenu>
|
|
278
|
+
</View>
|
|
279
|
+
</Dropdown>
|
|
280
|
+
)
|
|
281
|
+
}
|
|
@@ -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,
|
|
@@ -104,12 +104,31 @@ const ResourceListItem = memo(function ResourceListItem({
|
|
|
104
104
|
onItemClick,
|
|
105
105
|
}) {
|
|
106
106
|
const { onClick, href, onDrop, onFilesDrop, dragData, actions, name, type, content } = resource
|
|
107
|
+
const [menuOpen, setMenuOpen] = useState(false)
|
|
108
|
+
const [menuPosition, setMenuPosition] = useState(null)
|
|
107
109
|
|
|
108
110
|
const handleClick = useCallback(() => {
|
|
109
111
|
onItemClick(resource)
|
|
110
112
|
onClick?.(resource)
|
|
111
113
|
}, [onItemClick, onClick, resource])
|
|
112
114
|
|
|
115
|
+
const handleContextMenu = useCallback((event) => {
|
|
116
|
+
if (!actions) return
|
|
117
|
+
event.preventDefault()
|
|
118
|
+
event.stopPropagation()
|
|
119
|
+
setMenuPosition({ top: event.clientY, left: event.clientX })
|
|
120
|
+
setMenuOpen(true)
|
|
121
|
+
}, [actions])
|
|
122
|
+
|
|
123
|
+
const handleMenuOpenChange = useCallback((open) => {
|
|
124
|
+
setMenuOpen(open)
|
|
125
|
+
if (!open) setMenuPosition(null)
|
|
126
|
+
}, [])
|
|
127
|
+
|
|
128
|
+
const handleActionsTriggerClick = useCallback(() => {
|
|
129
|
+
setMenuPosition(null)
|
|
130
|
+
}, [])
|
|
131
|
+
|
|
113
132
|
let Container = ({ children }) => <>{children}</>
|
|
114
133
|
|
|
115
134
|
if (onDrop || onFilesDrop) {
|
|
@@ -123,10 +142,19 @@ const ResourceListItem = memo(function ResourceListItem({
|
|
|
123
142
|
<List.Item
|
|
124
143
|
href={href}
|
|
125
144
|
onClick={handleClick}
|
|
145
|
+
onContextMenu={handleContextMenu}
|
|
126
146
|
selectable
|
|
127
147
|
leading={<ResourceIcon type={type} content={content} templateMap={templateMap} />}
|
|
128
148
|
meta={<Size content={content} />}
|
|
129
|
-
trailing={
|
|
149
|
+
trailing={(
|
|
150
|
+
<RowActions
|
|
151
|
+
actions={actions}
|
|
152
|
+
open={menuOpen}
|
|
153
|
+
onOpenChange={handleMenuOpenChange}
|
|
154
|
+
position={menuPosition}
|
|
155
|
+
onTriggerClick={handleActionsTriggerClick}
|
|
156
|
+
/>
|
|
157
|
+
)}
|
|
130
158
|
data-resource-id={resource.id}
|
|
131
159
|
>
|
|
132
160
|
{typeof name === 'string' ? name : ''}
|
|
@@ -149,6 +177,9 @@ const ResourceIcon = memo(function ResourceIcon({ type, content, templateMap })
|
|
|
149
177
|
<Image
|
|
150
178
|
src={content?.sizes?.thumbnailSmall || content?.src}
|
|
151
179
|
placeholderSrc={content?.sizes?.['loader-square-blurred-after'] || content?.src}
|
|
180
|
+
blurhash={content?.blurhash}
|
|
181
|
+
width={content?.width}
|
|
182
|
+
height={content?.height}
|
|
152
183
|
style={{ width: '24px', height: '24px', borderRadius: '25%' }}
|
|
153
184
|
/>
|
|
154
185
|
)
|
|
@@ -171,11 +202,16 @@ function Size({ content }) {
|
|
|
171
202
|
return null
|
|
172
203
|
}
|
|
173
204
|
|
|
174
|
-
function RowActions({ actions }) {
|
|
205
|
+
function RowActions({ actions, open, onOpenChange, position, onTriggerClick }) {
|
|
175
206
|
if (!actions) return null
|
|
176
207
|
|
|
177
208
|
return (
|
|
178
|
-
<Dropdown
|
|
209
|
+
<Dropdown
|
|
210
|
+
trigger={<Button prefix="more-vertical-alt" variant="command" onClick={onTriggerClick} />}
|
|
211
|
+
open={open}
|
|
212
|
+
onOpenChange={onOpenChange}
|
|
213
|
+
position={position}
|
|
214
|
+
>
|
|
179
215
|
<View inset="xs" surface="primary" roundness="s">
|
|
180
216
|
<ContextMenu roundness="s" surface="primary">
|
|
181
217
|
{actions}
|
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
|
/>
|
package/src/index.js
CHANGED
|
@@ -4,6 +4,7 @@ export { metadata as ListResources } from './list.action.js'
|
|
|
4
4
|
export { metadata as SearchResources } from './search.action.js'
|
|
5
5
|
export { metadata as DeleteResource } from './delete.action.js'
|
|
6
6
|
export { metadata as UpdateResourceContent } from './update-content.action.js'
|
|
7
|
+
export { metadata as PatchResourceContent } from './patch-content.action.js'
|
|
7
8
|
export { metadata as UpdateResourceLocation } from './update-location.action.js'
|
|
8
9
|
export { metadata as UpdateResourceName } from './update-name.action.js'
|
|
9
10
|
export { metadata as UpdateResourceAccess } from './update-access.action.js'
|
|
@@ -28,6 +29,7 @@ export * from './GenericResourceForm.jsx'
|
|
|
28
29
|
export * from './GenericResourceCard.jsx'
|
|
29
30
|
export * from './ResourceGenericView.jsx'
|
|
30
31
|
export * from './ResourceList.jsx'
|
|
32
|
+
export * from './ResourceGrid.jsx'
|
|
31
33
|
export * from './ResourcePage.jsx'
|
|
32
34
|
export * from './ResourcePanel.jsx'
|
|
33
35
|
export * from './ResourceTags.jsx'
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const metadata = { id: '@ossy/resources/actions/patch-content', access: 'workspace' }
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { ResourcesEvents } from './resources.events.js'
|
|
2
|
+
import { mutateResource } from './resource-stream.helpers.js'
|
|
3
|
+
import { resolveResourceId } from './resource-read.helpers.js'
|
|
4
|
+
import { mediaContextFromRun, withResourceMedia } from './resources.action-media.js'
|
|
5
|
+
|
|
6
|
+
export const metadata = { id: '@ossy/resources/tasks/patch-content' }
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Deep-merge patch into resource `content` (ADR 0008 `Patched`).
|
|
10
|
+
* Prefer this over update-content when adding fields like blurhash/sizes
|
|
11
|
+
* without replacing the full content document.
|
|
12
|
+
*/
|
|
13
|
+
export async function run ({ payload, req }) {
|
|
14
|
+
const createdBy = payload?.userId ?? req?.userId
|
|
15
|
+
const resourceId = resolveResourceId({ payload, req })
|
|
16
|
+
const content = payload?.content
|
|
17
|
+
|
|
18
|
+
if (!resourceId) throw Object.assign(new Error('resourceId is required'), { status: 400 })
|
|
19
|
+
if (!content || typeof content !== 'object' || Array.isArray(content)) {
|
|
20
|
+
throw Object.assign(new Error('content is required'), { status: 400 })
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const resource = await mutateResource(
|
|
24
|
+
resourceId,
|
|
25
|
+
ResourcesEvents.Patched({ createdBy, content }),
|
|
26
|
+
)
|
|
27
|
+
return withResourceMedia(resource, mediaContextFromRun({ payload, req }))
|
|
28
|
+
}
|