@ossy/resources 1.12.2 → 1.12.3

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.
Files changed (79) hide show
  1. package/package.json +11 -3
  2. package/src/AudioResource.jsx +1 -1
  3. package/src/CreateDirectory.jsx +2 -10
  4. package/src/CreateDocument.jsx +2 -2
  5. package/src/Definition.js +2 -1
  6. package/src/DocumentEdit.jsx +10 -38
  7. package/src/DocumentView.jsx +3 -4
  8. package/src/GenericResourceCard.jsx +25 -7
  9. package/src/GenericResourceDetail.jsx +6 -60
  10. package/src/GenericResourceForm.jsx +66 -31
  11. package/src/ImageResource.jsx +1 -1
  12. package/src/PDFResource.jsx +1 -1
  13. package/src/PlatformFileField.jsx +239 -0
  14. package/src/ResourceContentPage.jsx +3 -3
  15. package/src/ResourceDescription.jsx +1 -1
  16. package/src/ResourceDetails.jsx +3 -4
  17. package/src/ResourceDialogMove.jsx +2 -2
  18. package/src/ResourceFactory.jsx +11 -64
  19. package/src/ResourceGenericView.jsx +1 -1
  20. package/src/ResourceList.jsx +89 -106
  21. package/src/ResourcePanel.jsx +40 -57
  22. package/src/ResourceTags.jsx +1 -1
  23. package/src/ResourcesPage.jsx +26 -35
  24. package/src/SchemaPresenter.jsx +215 -0
  25. package/src/Upload.jsx +6 -25
  26. package/src/UploadResources.jsx +2 -2
  27. package/src/VideoResource.jsx +1 -1
  28. package/src/access-filter.spec.js +1 -1
  29. package/src/create-page-view.action.js +1 -1
  30. package/src/create-page-view.task.js +10 -9
  31. package/src/create.action.js +1 -1
  32. package/src/create.task.js +28 -29
  33. package/src/delete.action.js +1 -1
  34. package/src/delete.task.js +10 -13
  35. package/src/en.translations.json +24 -24
  36. package/src/get-page-view-stats.action.js +1 -1
  37. package/src/get-page-view-stats.task.js +3 -4
  38. package/src/get-resource-alias.api.js +8 -0
  39. package/src/get-resource-info-alias.api.js +8 -0
  40. package/src/get-resource-info.api.js +36 -0
  41. package/src/get-resource-variant.api.js +8 -0
  42. package/src/get-resource.api.js +51 -0
  43. package/src/get.action.js +1 -1
  44. package/src/get.task.js +6 -6
  45. package/src/index.js +6 -0
  46. package/src/list.action.js +1 -1
  47. package/src/list.task.js +1 -1
  48. package/src/platform-file-field.component.jsx +5 -0
  49. package/src/platform-image-field.component.jsx +5 -0
  50. package/src/resource-create.page.jsx +4 -4
  51. package/src/resource-read.helpers.js +151 -0
  52. package/src/resource-stream.helpers.js +20 -0
  53. package/src/resource-stream.js +108 -0
  54. package/src/resource.helpers.js +9 -9
  55. package/src/resources.attach-media-urls.js +9 -3
  56. package/src/resources.events.js +42 -81
  57. package/src/{resources.spec.js → resources.integration.spec.js} +19 -19
  58. package/src/resources.queries.js +5 -2
  59. package/src/search.action.js +1 -1
  60. package/src/search.task.js +1 -1
  61. package/src/server.js +2 -1
  62. package/src/sv.translations.json +24 -24
  63. package/src/update-access.action.js +1 -1
  64. package/src/update-access.task.js +9 -8
  65. package/src/update-content.action.js +1 -1
  66. package/src/update-content.task.js +25 -27
  67. package/src/update-location.action.js +1 -1
  68. package/src/update-location.task.js +9 -8
  69. package/src/update-name.action.js +1 -1
  70. package/src/update-name.task.js +9 -8
  71. package/src/upload-named-version.action.js +1 -1
  72. package/src/upload-named-version.task.js +16 -22
  73. package/src/useActivePath.jsx +3 -8
  74. package/src/useDocumentValidator.js +16 -0
  75. package/src/useSchemaEngine.js +9 -0
  76. package/src/useSchemas.js +29 -0
  77. package/src/useSelectedResourceId.jsx +36 -0
  78. package/src/ResourceContentPage.stories.jsx +0 -19
  79. package/src/resource.aggregate.js +0 -86
@@ -0,0 +1,239 @@
1
+ import React, { useCallback, useEffect, useState } from 'react'
2
+ import { useSdk } from '@ossy/sdk-react'
3
+ import {
4
+ View,
5
+ Text,
6
+ Button,
7
+ UploadInput,
8
+ LocalFilePreview,
9
+ acceptsFile,
10
+ } from '@ossy/design-system'
11
+ import { uploadFile } from './resource.helpers.js'
12
+ import { metadata as GetResource } from './get.action.js'
13
+
14
+ function commitResourceValue(name, value, onChange) {
15
+ onChange({
16
+ target: {
17
+ id: name,
18
+ name,
19
+ dataset: { ossyFileResourceReplace: 'true' },
20
+ value: JSON.stringify(value),
21
+ },
22
+ })
23
+ }
24
+
25
+ function resourceIdFromValue(value) {
26
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
27
+ return typeof value.resourceId === 'string' ? value.resourceId : null
28
+ }
29
+ return null
30
+ }
31
+
32
+ function resourceIdsFromValue(value) {
33
+ if (!Array.isArray(value)) return []
34
+ return value
35
+ .map(item => (item && typeof item.resourceId === 'string' ? item.resourceId : null))
36
+ .filter(Boolean)
37
+ }
38
+
39
+ /**
40
+ * Platform file field — eager upload on pick via SDK create + PUT.
41
+ * Register as `@ossy/design-system/input/file` / `@ossy/design-system/input/image` through app component slots.
42
+ */
43
+ export function PlatformFileField({
44
+ name,
45
+ label,
46
+ value,
47
+ onChange,
48
+ accept,
49
+ min = 0,
50
+ max = 1,
51
+ disabled,
52
+ required,
53
+ uploadLocation = '/media/uploads/',
54
+ style = {},
55
+ ...rest
56
+ }) {
57
+ const sdk = useSdk()
58
+ const multi = max > 1
59
+ const [localFile, setLocalFile] = useState(null)
60
+ const [previewUrl, setPreviewUrl] = useState(null)
61
+ const [uploading, setUploading] = useState(false)
62
+ const [error, setError] = useState(null)
63
+
64
+ const resourceId = multi ? null : resourceIdFromValue(value)
65
+ const resourceIds = multi ? resourceIdsFromValue(value) : []
66
+
67
+ useEffect(() => {
68
+ if (!resourceId || localFile) {
69
+ if (!localFile) setPreviewUrl(null)
70
+ return
71
+ }
72
+
73
+ let cancelled = false
74
+ sdk.invoke(GetResource, { resourceId })
75
+ .then(resource => {
76
+ if (cancelled) return
77
+ const src = resource?.content?.src || `/r/${resourceId}`
78
+ setPreviewUrl(src)
79
+ })
80
+ .catch(() => {
81
+ if (!cancelled) setPreviewUrl(`/r/${resourceId}`)
82
+ })
83
+
84
+ return () => {
85
+ cancelled = true
86
+ }
87
+ }, [resourceId, localFile, sdk])
88
+
89
+ const uploadSingle = useCallback(
90
+ async file => {
91
+ setUploading(true)
92
+ setError(null)
93
+ setLocalFile(file)
94
+ try {
95
+ const resource = await uploadFile(sdk, uploadLocation, file)
96
+ commitResourceValue(name, { resourceId: resource.id }, onChange)
97
+ setLocalFile(null)
98
+ } catch (err) {
99
+ setError(err?.message || 'Upload failed')
100
+ setLocalFile(null)
101
+ } finally {
102
+ setUploading(false)
103
+ }
104
+ },
105
+ [name, onChange, sdk, uploadLocation],
106
+ )
107
+
108
+ const onSinglePick = useCallback(
109
+ e => {
110
+ const file = e.target.files?.[0]
111
+ e.target.value = ''
112
+ if (!file || uploading) return
113
+ if (!acceptsFile(file, accept)) {
114
+ setError(`File type not accepted: ${file.name}`)
115
+ return
116
+ }
117
+ uploadSingle(file)
118
+ },
119
+ [accept, uploadSingle, uploading],
120
+ )
121
+
122
+ const onMultiPick = useCallback(
123
+ async e => {
124
+ const picked = Array.from(e.target.files || [])
125
+ e.target.value = ''
126
+ if (!picked.length || uploading) return
127
+ const valid = picked.filter(f => acceptsFile(f, accept))
128
+ if (valid.length !== picked.length) {
129
+ setError('Some files were rejected by accept filter')
130
+ return
131
+ }
132
+ const remaining = max - resourceIds.length
133
+ const batch = valid.slice(0, remaining)
134
+ setUploading(true)
135
+ setError(null)
136
+ try {
137
+ const uploaded = []
138
+ for (const file of batch) {
139
+ const resource = await uploadFile(sdk, uploadLocation, file)
140
+ uploaded.push({ resourceId: resource.id })
141
+ }
142
+ commitResourceValue(name, [...resourceIds.map(id => ({ resourceId: id })), ...uploaded], onChange)
143
+ } catch (err) {
144
+ setError(err?.message || 'Upload failed')
145
+ } finally {
146
+ setUploading(false)
147
+ }
148
+ },
149
+ [accept, max, name, onChange, resourceIds, sdk, uploadLocation, uploading],
150
+ )
151
+
152
+ const removeMulti = useCallback(
153
+ id => {
154
+ commitResourceValue(
155
+ name,
156
+ resourceIds.filter(rid => rid !== id).map(rid => ({ resourceId: rid })),
157
+ onChange,
158
+ )
159
+ },
160
+ [name, onChange, resourceIds],
161
+ )
162
+
163
+ if (multi) {
164
+ const canAdd = resourceIds.length < max
165
+ return (
166
+ <View gap="s" style={style} {...rest}>
167
+ {resourceIds.map(id => (
168
+ <View key={id} layout="row" gap="s" alignItems="center">
169
+ <View
170
+ as="img"
171
+ src={`/r/${id}`}
172
+ alt=""
173
+ width="32px"
174
+ height="32px"
175
+ style={{ objectFit: 'cover', borderRadius: 'var(--space-s)' }}
176
+ />
177
+ <Text style={{ flex: 1, fontSize: 14 }}>{id}</Text>
178
+ <Button
179
+ type="button"
180
+ variant="command-danger"
181
+ prefix="close"
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>
202
+ )
203
+ }
204
+
205
+ const previewSrc = localFile ? null : previewUrl
206
+
207
+ return (
208
+ <View gap="s" style={style} {...rest}>
209
+ {(localFile || previewSrc || resourceId) && (
210
+ <View layout="row" gap="s" alignItems="center">
211
+ {localFile && <LocalFilePreview file={localFile} size="48px" />}
212
+ {!localFile && previewSrc && (
213
+ <View
214
+ as="img"
215
+ src={previewSrc}
216
+ alt={label || name}
217
+ width="48px"
218
+ height="48px"
219
+ style={{ objectFit: 'cover', borderRadius: 'var(--space-s)' }}
220
+ />
221
+ )}
222
+ {localFile && (
223
+ <Text style={{ flex: 1, fontSize: 14 }}>{localFile.name}</Text>
224
+ )}
225
+ {uploading && <Text style={{ fontSize: 12 }}>Uploading…</Text>}
226
+ </View>
227
+ )}
228
+ <UploadInput
229
+ id={name}
230
+ name={name}
231
+ accept={accept}
232
+ disabled={disabled || uploading}
233
+ required={required && !resourceId}
234
+ onChange={onSinglePick}
235
+ />
236
+ {error && <Text style={{ fontSize: 12, color: 'var(--color-danger, crimson)' }}>{error}</Text>}
237
+ </View>
238
+ )
239
+ }
@@ -1,7 +1,7 @@
1
1
  import React from 'react'
2
2
  import { metadata as GetResource } from './get.action.js'
3
3
  import { AsyncStatus, useSdk } from '@ossy/sdk-react'
4
- import { Switch, View, DelayedRender, Button, Icon, PageSection, Title, Text, Tags, ImageCard } from '@ossy/design-system'
4
+ import { Switch, View, DelayedRender, Button, Icon, PageSection, Text, Tags, ImageCard } from '@ossy/design-system'
5
5
  import { useRouter } from '@ossy/router-react'
6
6
 
7
7
 
@@ -9,7 +9,7 @@ export const ResourceContentPage = (props) => {
9
9
  const router = useRouter()
10
10
  const resourceId = router.params.resourceId
11
11
  const sdk = useSdk()
12
- const { status, data: resource } = sdk.read(GetResource, { id: resourceId }, { enabled: !!resourceId })
12
+ const { status, data: resource } = sdk.read(GetResource, { resourceId }, { enabled: !!resourceId })
13
13
 
14
14
  return (
15
15
  <View {...props} slot="content" style={{ flexDirection: 'column', wrap: 'no-wrap', height: '100%' }}>
@@ -87,7 +87,7 @@ export const ResourceContentPage = (props) => {
87
87
  <View gap="l" className="resource-content-page__text-view">
88
88
 
89
89
  <View gap="m">
90
- <Title>{resource?.name}</Title>
90
+ <Text variant="heading-default" as="h1">{resource?.name}</Text>
91
91
  <Text>{resource?.content?.description}</Text>
92
92
  </View>
93
93
 
@@ -5,7 +5,7 @@ import { View, Text } from '@ossy/design-system'
5
5
 
6
6
  export const ResourceDescription = ({ resourceId }) => {
7
7
  const sdk = useSdk()
8
- const { data: resource } = sdk.read(GetResource, { id: resourceId }, { enabled: !!resourceId })
8
+ const { data: resource } = sdk.read(GetResource, { resourceId }, { enabled: !!resourceId })
9
9
 
10
10
  const description = resource?.content?.description;
11
11
 
@@ -1,16 +1,15 @@
1
1
  import React, { useState } from 'react'
2
2
  import { metadata as GetResource } from './get.action.js'
3
3
  import { updateResourceAccess } from './resource.helpers.js'
4
- import { GetWorkspace } from '@ossy/workspaces'
5
4
  import { useSdk } from '@ossy/sdk-react'
5
+ import { useSchema } from './useSchemas.js'
6
6
  import { Text, View } from '@ossy/design-system'
7
7
  import { formatBytes } from './utils/format-bytes.js'
8
8
 
9
9
  export const ResourceDetails = ({ resourceId }) => {
10
10
  const sdk = useSdk()
11
- const { data: resource } = sdk.read(GetResource, { id: resourceId }, { enabled: !!resourceId })
12
- const { data: workspace } = sdk.read(GetWorkspace)
13
- const template = workspace?.resourceTemplates?.find(t => t.id === resource?.type)
11
+ const { data: resource } = sdk.read(GetResource, { resourceId }, { enabled: !!resourceId })
12
+ const template = useSchema(resource?.type)
14
13
  const [accessBusy, setAccessBusy] = useState(false)
15
14
  const [accessError, setAccessError] = useState(null)
16
15
 
@@ -1,7 +1,7 @@
1
1
  import React from 'react'
2
2
  import { moveResource } from './resource.helpers.js'
3
3
  import { useSdk } from '@ossy/sdk-react'
4
- import { Button, View, Title, Input, Text, useInputValue } from '@ossy/design-system'
4
+ import { Button, View, Input, Text, useInputValue } from '@ossy/design-system'
5
5
 
6
6
  export const ResourceDialogMove = ({
7
7
  resourceId,
@@ -32,7 +32,7 @@ export const ResourceDialogMove = ({
32
32
 
33
33
 
34
34
  <View>
35
- <Title variant="tertiary">Move Resource</Title>
35
+ <Text variant="heading-tertiary" as="h3">Move Resource</Text>
36
36
  </View>
37
37
 
38
38
  <View>
@@ -1,71 +1,18 @@
1
1
  import React from 'react'
2
2
  import { metadata as GetResource } from './get.action.js'
3
3
  import { AsyncStatus, useSdk } from '@ossy/sdk-react'
4
- import { GetWorkspace } from '@ossy/workspaces'
5
- import { Guide, Text, DelayedRender } from '@ossy/design-system'
6
- import { DocumentEdit } from './DocumentEdit.jsx'
7
- import { ImageResource } from './ImageResource.jsx'
8
- import { AudioResource } from './AudioResource.jsx'
9
- import { VideoResource } from './VideoResource.jsx'
10
- import { PDFResource } from './PDFResource.jsx'
11
- import { ResourceGenericView } from './ResourceGenericView.jsx'
12
- import { GenericResourceDetail } from './GenericResourceDetail.jsx'
4
+ import { SchemaPresenter } from './SchemaPresenter.jsx'
13
5
 
14
6
  export const ResourceFactory = ({
15
7
  resourceId,
16
8
  mode,
17
- onClose: _onClose,
18
- form
19
- }) => {
20
- const sdk = useSdk()
21
- const { data: workspace } = sdk.read(GetWorkspace)
22
- const { data: resource, status } = sdk.read(GetResource, { id: resourceId }, { enabled: !!resourceId })
23
-
24
- console.log('ResourceFactory', { resourceId, mode, resource, status })
25
-
26
- if (AsyncStatus.Error === status) {
27
- return (
28
- <Guide
29
- title="Not found"
30
- text="We can't find the resource you are looking for"
31
- style={{ padding: '40px 16px' }}
32
- />
33
- )
34
- }
35
-
36
- if ([AsyncStatus.Loading, AsyncStatus.NotInitialized].includes(status)) {
37
- return (
38
- <DelayedRender>
39
- <Text>loading...</Text>
40
- </DelayedRender>
41
- )
42
- }
43
-
44
- if (workspace?.resourceTemplates?.find(({ id }) => id === resource.type)) {
45
- if (mode === 'View') {
46
- return <GenericResourceDetail resourceId={resourceId} />
47
- }
48
- if (mode === 'Edit') {
49
- return <DocumentEdit resourceId={resourceId} form={form} />
50
- }
51
- }
52
-
53
- if (['image/jpeg', 'image/png'].includes(resource.type)) {
54
- return <ImageResource resourceId={resourceId} onClose={_onClose} />
55
- }
56
-
57
- if (resource.type.startsWith('video/')) {
58
- return <VideoResource resourceId={resourceId} onClose={_onClose} />
59
- }
60
-
61
- if (resource.type.startsWith('audio/')) {
62
- return <AudioResource resourceId={resourceId} onClose={_onClose} />
63
- }
64
-
65
- if (resource.type === 'application/pdf') {
66
- return <PDFResource resourceId={resourceId} onClose={_onClose} />
67
- }
68
-
69
- return <ResourceGenericView resourceId={resourceId} onClose={_onClose} />
70
-
71
- }
9
+ onClose,
10
+ form,
11
+ }) => (
12
+ <SchemaPresenter
13
+ resourceId={resourceId}
14
+ flow={mode}
15
+ form={form}
16
+ onClose={onClose}
17
+ />
18
+ )
@@ -10,7 +10,7 @@ export const ResourceGenericView = ({
10
10
  resourceId
11
11
  }) => {
12
12
  const sdk = useSdk()
13
- const { data: resource } = sdk.read(GetResource, { id: resourceId }, { enabled: !!resourceId })
13
+ const { data: resource } = sdk.read(GetResource, { resourceId }, { enabled: !!resourceId })
14
14
 
15
15
  return (
16
16
  <View stack bordered>
@@ -1,7 +1,6 @@
1
1
  'use client'
2
- import React, { useRef, useEffect } from 'react'
3
- import { useSdk } from '@ossy/sdk-react'
4
- import { GetWorkspace } from '@ossy/workspaces'
2
+ import React, { useRef, useEffect, useMemo, useCallback, memo } from 'react'
3
+ import { useSchemas } from './useSchemas.js'
5
4
  import {
6
5
  Dropdown,
7
6
  DropZone,
@@ -11,29 +10,37 @@ import {
11
10
  Text,
12
11
  Button,
13
12
  ContextMenu,
13
+ List,
14
14
  } from '@ossy/design-system'
15
15
  import { formatBytes } from './utils/format-bytes.js'
16
16
 
17
+ function useSchemaMap() {
18
+ const templates = useSchemas()
19
+ return useMemo(
20
+ () => templates.reduce((acc, template) => ({ ...acc, [template.id]: template }), {}),
21
+ [templates],
22
+ )
23
+ }
24
+
17
25
  export const ResourceList = ({
18
26
  resources = [],
19
27
  onClick = () => {},
20
28
  inlineFolder = null,
21
29
  }) => {
30
+ const templateMap = useSchemaMap()
22
31
 
23
32
  return (
24
- <>
33
+ <List>
25
34
  {inlineFolder && <InlineFolderRow {...inlineFolder} />}
26
35
  {resources.map(resource => (
27
36
  <ResourceListItem
28
- {...resource}
29
37
  key={resource.id}
30
- onClick={() => {
31
- onClick(resource)
32
- resource?.onClick?.(resource)
33
- }}
38
+ resource={resource}
39
+ templateMap={templateMap}
40
+ onItemClick={onClick}
34
41
  />
35
42
  ))}
36
- </>
43
+ </List>
37
44
  )
38
45
  }
39
46
 
@@ -64,51 +71,47 @@ function InlineFolderRow({ name, onChange, onSave, onCancel }) {
64
71
  }
65
72
 
66
73
  return (
67
- <View
68
- layout="row"
69
- gap="m"
70
- style={{
71
- height: '56px',
72
- flexShrink: 0,
73
- borderBottom: '1px solid var(--separator-primary)',
74
- }}
74
+ <List.Item
75
+ leading={<Icon size="s" name="folder" style={{ fill: 'hsl(0, 0%, 60%)' }} />}
75
76
  >
76
- <View gap="m" layout="row" alignItems="center" style={{ flexGrow: 1, padding: '12px 0 12px 20px' }}>
77
- <Icon size="s" name="folder" style={{ fill: 'hsl(0, 0%, 60%)' }} />
78
- <input
79
- ref={inputRef}
80
- value={name}
81
- onChange={event => onChange(event.target.value)}
82
- onKeyDown={handleKeyDown}
83
- onBlur={handleBlur}
84
- style={{
85
- flexGrow: 1,
86
- border: 'none',
87
- background: 'transparent',
88
- outline: 'none',
89
- fontSize: 'var(--text-default-font-size, 16px)',
90
- fontFamily: 'var(--text-default-font-family, sans-serif)',
91
- fontWeight: 'var(--text-default-font-weight, 400)',
92
- color: 'var(--text-default-color, CanvasText)',
93
- padding: 0,
94
- minWidth: 0,
95
- }}
96
- />
97
- </View>
98
- </View>
77
+ <input
78
+ ref={inputRef}
79
+ value={name}
80
+ onChange={event => onChange(event.target.value)}
81
+ onKeyDown={handleKeyDown}
82
+ onBlur={handleBlur}
83
+ style={{
84
+ flexGrow: 1,
85
+ border: 'none',
86
+ background: 'transparent',
87
+ outline: 'none',
88
+ fontSize: 'var(--text-default-font-size, 16px)',
89
+ fontFamily: 'var(--text-default-font-family, sans-serif)',
90
+ fontWeight: 'var(--text-default-font-weight, 400)',
91
+ color: 'var(--text-default-color, CanvasText)',
92
+ padding: 0,
93
+ minWidth: 0,
94
+ width: '100%',
95
+ }}
96
+ />
97
+ </List.Item>
99
98
  )
100
99
  }
101
100
 
102
- function ResourceListItem({
103
- onClick,
104
- href,
105
- onDrop,
106
- dragData,
107
- ...resource
101
+ const ResourceListItem = memo(function ResourceListItem({
102
+ resource,
103
+ templateMap,
104
+ onItemClick,
108
105
  }) {
109
-
106
+ const { onClick, href, onDrop, dragData, actions, name, type, content } = resource
107
+
108
+ const handleClick = useCallback(() => {
109
+ onItemClick(resource)
110
+ onClick?.(resource)
111
+ }, [onItemClick, onClick, resource])
112
+
110
113
  let Container = ({ children }) => <>{children}</>
111
-
114
+
112
115
  if (onDrop) {
113
116
  Container = DropZone
114
117
  } else if (dragData) {
@@ -117,77 +120,58 @@ function ResourceListItem({
117
120
 
118
121
  return (
119
122
  <Container onDrop={onDrop} dragData={dragData}>
120
- <View
121
- layout="row"
123
+ <List.Item
124
+ href={href}
125
+ onClick={handleClick}
122
126
  selectable
123
- gap="m"
124
- style={{
125
- height: '56px',
126
- flexShrink: 0,
127
- borderBottom: '1px solid var(--separator-primary)',
128
- }}
127
+ leading={<ResourceIcon type={type} content={content} templateMap={templateMap} />}
128
+ meta={<Size content={content} />}
129
+ trailing={<RowActions actions={actions} />}
130
+ data-resource-id={resource.id}
129
131
  >
130
- <View as={href ? 'a' : undefined} gap="m" layout='row' alignItems="center" style={{ flexGrow: 1, padding: '12px 0 12px 20px', }} href={href} onClick={onClick}>
131
- <ResourceIcon {...resource} />
132
- <Text>{typeof resource.name === 'string' && resource.name}</Text>
133
- <View style={{ flexGrow: 1 }}/>
134
- <Size {...resource} />
135
- </View>
136
-
137
- <View style={{ padding: '12px 8px 12px 0'}}>
138
- <RowActions {...resource} />
139
- </View>
140
- </View>
132
+ {typeof name === 'string' ? name : ''}
133
+ </List.Item>
141
134
  </Container>
142
135
  )
143
-
144
- }
145
-
146
- function Size(resource) {
147
-
148
- if (resource.content?.ContentLength) {
149
- return (
150
- <Text variant="small">
151
- {formatBytes(resource.content.ContentLength)}
152
- </Text>
153
- )
154
- }
155
-
156
- return <></>
157
- }
158
-
159
- function ResourceIcon({ ...resource }) {
160
- const sdk = useSdk()
161
- const { data: workspace } = sdk.read(GetWorkspace)
162
-
163
- const resourceTemplates = (workspace.resourceTemplates || [])
164
- .reduce((acc, curr) => ({ ...acc, [curr.id]: curr }), {})
165
-
166
- if (resource?.type === 'directory') {
136
+ })
137
+
138
+ const ResourceIcon = memo(function ResourceIcon({ type, content, templateMap }) {
139
+ if (type === 'directory') {
167
140
  return (
168
141
  <Icon size="s" name="folder" style={{ fill: 'hsl(0, 0%, 60%)'}} />
169
142
  )
170
143
  }
171
-
172
- else if (resource.type.startsWith('image')) {
144
+
145
+ if (type?.startsWith('image')) {
173
146
  return (
174
147
  <Image
175
- src={resource?.content?.sizes?.thumbnailSmall || resource?.content?.src}
176
- placeholderSrc={resource?.content?.sizes?.['loader-square-blurred-after'] || resource?.content?.src}
148
+ src={content?.sizes?.thumbnailSmall || content?.src}
149
+ placeholderSrc={content?.sizes?.['loader-square-blurred-after'] || content?.src}
177
150
  style={{ width: '24px', height: '24px', borderRadius: '25%' }}
178
151
  />
179
152
  )
180
153
  }
181
-
154
+
182
155
  return (
183
- <Icon size="s" name={resourceTemplates[resource.type]?.icon || 'file'} style={{ fill: 'hsl(0, 0%, 80%)'}} />
156
+ <Icon size="s" name={templateMap[type]?.icon || 'file'} style={{ fill: 'hsl(0, 0%, 80%)'}} />
184
157
  )
185
- }
186
-
187
- function RowActions({actions}) {
188
-
189
- if (!actions) return <></>
190
-
158
+ })
159
+
160
+ function Size({ content }) {
161
+ if (content?.ContentLength) {
162
+ return (
163
+ <Text variant="small">
164
+ {formatBytes(content.ContentLength)}
165
+ </Text>
166
+ )
167
+ }
168
+
169
+ return null
170
+ }
171
+
172
+ function RowActions({ actions }) {
173
+ if (!actions) return null
174
+
191
175
  return (
192
176
  <Dropdown trigger={<Button prefix="more-vertical-alt" variant="command" /> } >
193
177
  <View inset="xs" surface="primary" roundness="s">
@@ -197,5 +181,4 @@ function ResourceListItem({
197
181
  </View>
198
182
  </Dropdown>
199
183
  )
200
-
201
- }
184
+ }