@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
@@ -1,24 +1,16 @@
1
- import React, { useState, useEffect } from 'react'
1
+ import React, { useState, useEffect, memo } from 'react'
2
2
  import { metadata as GetResource } from './get.action.js'
3
3
  import { removeResource, updateResourceContent, renameResource } from './resource.helpers.js'
4
4
  import { useSdk } from '@ossy/sdk-react'
5
- import {
6
- Title,
7
- Overlay,
8
- Switch,
9
- Guide,
10
- View,
11
- Text,
12
- Button,
13
- InputTitle,
14
- useInputValue,
15
- } from '@ossy/design-system'
5
+ import { Switch, View, Text, Button, InputTitle, Alert, useInputValue } from '@ossy/design-system'
16
6
  import { ResourceFactory } from './ResourceFactory.jsx'
17
- import { useRouter } from '@ossy/router-react'
18
7
  import { ResourceDetails } from './ResourceDetails.jsx'
19
8
  import { ResourceTags } from './ResourceTags.jsx'
20
9
  import { ResourceDescription } from './ResourceDescription.jsx'
21
10
  import { useForm } from './useForm.js'
11
+ import { useSchema } from './useSchemas.js'
12
+ import { useSchemaEngine } from './useSchemaEngine.js'
13
+ import { validateContent } from '@ossy/schema'
22
14
 
23
15
  const ViewMode = {
24
16
  Closed: 'Closed',
@@ -26,11 +18,6 @@ const ViewMode = {
26
18
  Edit: 'Edit'
27
19
  }
28
20
 
29
- const Overlays = {
30
- None: 'None',
31
- Removedirectory : 'RemoveDirectory'
32
- }
33
-
34
21
  const DefaultViewModes = {
35
22
  'panel-resource-content': ViewMode.View,
36
23
  'panel-header': ViewMode.View,
@@ -39,19 +26,19 @@ const DefaultViewModes = {
39
26
  'panel-resource-tags': ViewMode.Closed,
40
27
  }
41
28
 
42
- export const ResourcePanel = ({
29
+ export const ResourcePanel = memo(function ResourcePanel ({
43
30
  resourceId,
44
31
  onClose: _onClose,
45
32
  extraPanels = [],
46
- }) => {
47
- const router = useRouter()
33
+ }) {
48
34
  const sdk = useSdk()
49
- const [overlay, setOverlay] = useState(Overlays.None)
50
- const [error] = useState()
35
+ const [contentError, setContentError] = useState()
51
36
  const [resourceName, setResourceName] = useInputValue('')
52
37
  const [panelViewModes, setPanelViewModes] = useState(DefaultViewModes)
53
38
 
54
- const { data: resource = {} } = sdk.read(GetResource, { id: resourceId }, { enabled: !!resourceId })
39
+ const { data: resource = {} } = sdk.read(GetResource, { resourceId }, { enabled: !!resourceId })
40
+ const template = useSchema(resource?.type)
41
+ const engine = useSchemaEngine()
55
42
 
56
43
  const form = useForm({ defaultData: resource.content })
57
44
 
@@ -69,6 +56,18 @@ export const ResourcePanel = ({
69
56
  }
70
57
 
71
58
  const onUpdateResource = () => {
59
+ if (template) {
60
+ const result = validateContent(engine, template, form.data)
61
+ if (!result.ok) {
62
+ setContentError(result.message)
63
+ return
64
+ }
65
+ setContentError(undefined)
66
+ updateResourceContent(sdk, resourceId, result.data)
67
+ .then(() => setPanelViewModes(x => ({ ...x, 'panel-resource-content': ViewMode.View })))
68
+ .catch(() => alert('Something went wrong!'))
69
+ return
70
+ }
72
71
  updateResourceContent(sdk, resourceId, form.data)
73
72
  .then(() => setPanelViewModes(x => ({ ...x, 'panel-resource-content': ViewMode.View })))
74
73
  .catch(() => alert('Something went wrong!'))
@@ -91,16 +90,26 @@ export const ResourcePanel = ({
91
90
  setResourceName(resource.name)
92
91
  }, [resource])
93
92
 
93
+ useEffect(() => {
94
+ setContentError(undefined)
95
+ }, [form.data])
96
+
94
97
  const panels = [
95
98
  {
96
99
  id: 'panel-resource-content',
97
100
  label: 'View',
98
101
  suffix: panelViewModes['panel-resource-content'] === ViewMode.Edit
99
102
  ? [
100
- { prefix: 'close', variant: 'command', onClick: () => setPanelViewModes(x => ({ ...x, 'panel-resource-content': ViewMode.View })) },
103
+ { prefix: 'close', variant: 'command', onClick: () => {
104
+ setContentError(undefined)
105
+ setPanelViewModes(x => ({ ...x, 'panel-resource-content': ViewMode.View }))
106
+ } },
101
107
  { prefix: 'check', variant: 'command', onClick: onUpdateResource }
102
108
  ]
103
- : [{ prefix: 'pen', variant: 'command', onClick: () => setPanelViewModes(x => ({ ...x, 'panel-resource-content': ViewMode.Edit })) }],
109
+ : [{ prefix: 'pen', variant: 'command', onClick: () => {
110
+ setContentError(undefined)
111
+ setPanelViewModes(x => ({ ...x, 'panel-resource-content': ViewMode.Edit }))
112
+ } }],
104
113
  fill: true,
105
114
  content: ResourceFactory
106
115
  },
@@ -143,9 +152,9 @@ export const ResourcePanel = ({
143
152
  `}</style>
144
153
 
145
154
  {
146
- !!error && (
155
+ !!contentError && (
147
156
  <View.Item>
148
- {error}
157
+ <Alert variant="danger">{contentError}</Alert>
149
158
  </View.Item>
150
159
  )
151
160
  }
@@ -159,7 +168,7 @@ export const ResourcePanel = ({
159
168
  <Switch.Case match={[ViewMode.View]}>
160
169
 
161
170
  <View.Item fill surface="primary" style={{ padding: '4px 8px' }}>
162
- <Title as="h3" variant="tertiary">{resourceName}</Title>
171
+ <Text as="h3" variant="breadcrumb">{resourceName}</Text>
163
172
  </View.Item>
164
173
 
165
174
  <Button prefix="trash-empty" variant="command-danger" onClick={onRemoveResource}/>
@@ -174,7 +183,7 @@ export const ResourcePanel = ({
174
183
  <InputTitle
175
184
  id="document-name"
176
185
  type="text"
177
- style={{ display: 'block', width: '100%', padding: '16px 8px' }}
186
+ style={{ display: 'block', width: '100%' }}
178
187
  value={resourceName}
179
188
  onChange={setResourceName}
180
189
  onBlur={onRenameResource}
@@ -237,32 +246,6 @@ export const ResourcePanel = ({
237
246
 
238
247
  </View.Item>
239
248
 
240
- {
241
- overlay === Overlays.RemoveDirectory && (
242
- <Overlay isVisible={true} onClose={() => setOverlay(Overlays.None)}>
243
- <View layout="off-center-s" style={{ height: '100%' }}>
244
- <View slot="content">
245
- <View surface="primary" roundness="s">
246
- <View surface="primary" inset="l" roundness="s">
247
- <Guide
248
- title="Remove directory"
249
- text="Are you sure you want to remove this directory and everything in it?"
250
- actions={[
251
- { label: 'Cancel', variant: "command", onClick: () => setOverlay(Overlays.None) },
252
- { label: 'Remove', variant: 'command-danger', onClick: () => {
253
- removeResource(sdk, resourceId, resource?.location)
254
- setOverlay(Overlays.None)
255
- }}
256
- ]}
257
- />
258
- </View>
259
- </View>
260
- </View>
261
- </View>
262
- </Overlay>
263
- )
264
- }
265
-
266
249
  </View>
267
250
  )
268
- }
251
+ })
@@ -5,7 +5,7 @@ import { View, Tags, Text } from '@ossy/design-system'
5
5
 
6
6
  export const ResourceTags = ({ 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
  let resourceContentTags = resource?.content?.Tags || []
11
11
 
@@ -2,7 +2,7 @@ import React from 'react'
2
2
  import { SDK } from '@ossy/sdk'
3
3
  import { metadata as ListResources } from './list.action.js'
4
4
  import { AsyncStatus, WorkspaceProvider, useSdk } from '@ossy/sdk-react'
5
- import { Title, Text, Switch, View, ImageCard, Tags, useLocale } from '@ossy/design-system'
5
+ import { Text, Switch, View, ImageCard, Tags, Page, useLocale } from '@ossy/design-system'
6
6
  import { useRouter } from '@ossy/router-react'
7
7
 
8
8
  const sdk = SDK.of({
@@ -27,41 +27,32 @@ export const ResourcesPage= () => {
27
27
  return (
28
28
  <Switch on={status}>
29
29
  <Switch.Case match={[AsyncStatus.Success]}>
30
-
31
- <main style={{
32
- height: '100%',
33
- gridArea: 'main',
34
- padding: '32px 16px 0'
35
- }}>
36
- <View gap="l" style={{ margin: '0 auto' }}>
37
-
38
- <View gap="m">
39
- <Title>{t('resources.home.title')}</Title>
40
- <Text style={{ maxWidth: '900px'}}>{t('resources.home.description')}</Text>
41
- </View>
42
-
43
- <WorkspaceProvider sdk={sdk}>
44
- <section style={freeResourcesGridStyles}>
45
- { resources.map(resource => (
46
- <ImageCard
47
- href={router.getHref({ id: "@resource", params: { resourceId: resource?.id } })}
48
- src={resource?.content?.sizes?.galleryMedium || resource?.content?.src}
49
- placeholderSrc={resource?.content?.sizes?.['loader-square-blurred-after'] || resource?.content?.src}
50
- style={{ pageBreakInside: 'avoid', cursor: 'pointer' }}
51
- key={resources.id}
52
- />
53
- ))}
54
- </section>
55
- </WorkspaceProvider>
56
-
57
- <div style={{ display: 'flex', flexWrap: 'wrap', gap: 'var(--space-xxs)', marginTop: '24px' }}>
58
- <Tags tags={Object.entries(tagsCount).map(([tag, count]) => `${count} ${tag}`)} />
59
- </div>
30
+ <Page
31
+ maxWidth="xl"
32
+ title="resources.home.title"
33
+ description="resources.home.description"
34
+ descriptionMaxWidth="m"
35
+ >
36
+ <View gap="m">
37
+ <WorkspaceProvider sdk={sdk}>
38
+ <section style={freeResourcesGridStyles}>
39
+ { resources.map(resource => (
40
+ <ImageCard
41
+ href={router.getHref({ id: "@resource", params: { resourceId: resource?.id } })}
42
+ src={resource?.content?.sizes?.galleryMedium || resource?.content?.src}
43
+ placeholderSrc={resource?.content?.sizes?.['loader-square-blurred-after'] || resource?.content?.src}
44
+ style={{ pageBreakInside: 'avoid', cursor: 'pointer' }}
45
+ key={resource.id}
46
+ />
47
+ ))}
48
+ </section>
49
+ </WorkspaceProvider>
50
+
51
+ <div style={{ display: 'flex', flexWrap: 'wrap', gap: 'var(--space-xxs)', marginTop: '24px' }}>
52
+ <Tags tags={Object.entries(tagsCount).map(([tag, count]) => `${count} ${tag}`)} />
53
+ </div>
60
54
  </View>
61
-
62
-
63
- </main>
64
-
55
+ </Page>
65
56
  </Switch.Case>
66
57
  </Switch>
67
58
  )
@@ -0,0 +1,215 @@
1
+ import React, { useMemo } from 'react'
2
+ import { metadata as GetResource } from './get.action.js'
3
+ import { AsyncStatus, useSdk } from '@ossy/sdk-react'
4
+ import { useSchemas } from './useSchemas.js'
5
+ import {
6
+ isMimeType,
7
+ isSchemaDocumentType,
8
+ resolveFormComponentId,
9
+ resolveMimeViewChain,
10
+ resolveViewComponentChain,
11
+ } from '@ossy/schema/resolve'
12
+ import { PlatformSchema } from '@ossy/platform'
13
+ import { Guide, Text, DelayedRender, Slot, View, Fields } from '@ossy/design-system'
14
+ import { useSchema } from './useSchemas.js'
15
+ import { ImageResource } from './ImageResource.jsx'
16
+ import { AudioResource } from './AudioResource.jsx'
17
+ import { VideoResource } from './VideoResource.jsx'
18
+ import { PDFResource } from './PDFResource.jsx'
19
+ import { ResourceGenericView } from './ResourceGenericView.jsx'
20
+ import { GenericResourceForm } from './GenericResourceForm.jsx'
21
+
22
+ /** MIME type for rendering — file envelope stores MIME in content. */
23
+ export function resolveMediaType (resource) {
24
+ if (resource?.content?.ContentType) return resource.content.ContentType
25
+ return resource?.type
26
+ }
27
+
28
+ function SchemaViewFallback ({ resource, projection, resourceId }) {
29
+ const template = useSchema(resource?.type)
30
+
31
+ if (template?.fields?.length) {
32
+ return (
33
+ <View stack gap="m" inset="m">
34
+ <Text variant="heading-default" as="h1">{resource?.name}</Text>
35
+ <Fields data={resource.content || {}} fields={template.fields} />
36
+ </View>
37
+ )
38
+ }
39
+
40
+ if (resource?.content) {
41
+ const fields = Object.entries(resource.content)
42
+ return (
43
+ <View stack gap="m" inset="m">
44
+ <Text variant="heading-default" as="h1">{resource?.name}</Text>
45
+ {fields.map(([name, value]) => (
46
+ <View gap="xs" key={name}>
47
+ <Text style={{ fontWeight: 'bold' }}>{name}</Text>
48
+ <Text>{typeof value === 'object' ? JSON.stringify(value) : String(value ?? '')}</Text>
49
+ </View>
50
+ ))}
51
+ {projection && <Text size="s" color="secondary">projection: {projection}</Text>}
52
+ </View>
53
+ )
54
+ }
55
+
56
+ return <ResourceGenericView resourceId={resourceId} />
57
+ }
58
+
59
+ function SchemaViewPresenter ({ resource, projection, resourceId }) {
60
+ const chain = useMemo(
61
+ () => resolveViewComponentChain(resource.type, projection),
62
+ [resource.type, projection],
63
+ )
64
+ const [primary, secondary] = chain
65
+
66
+ return (
67
+ <Slot
68
+ name={primary}
69
+ resourceId={resourceId}
70
+ resource={resource}
71
+ schemaId={resource.type}
72
+ projection={projection}
73
+ fallback={
74
+ <Slot
75
+ name={secondary}
76
+ resourceId={resourceId}
77
+ resource={resource}
78
+ schemaId={resource.type}
79
+ projection={projection}
80
+ fallback={<SchemaViewFallback resource={resource} projection={projection} resourceId={resourceId} />}
81
+ />
82
+ }
83
+ />
84
+ )
85
+ }
86
+
87
+ function MimeViewPresenter ({ resource, projection, resourceId, onClose }) {
88
+ const mime = resolveMediaType(resource)
89
+ if (['image/jpeg', 'image/png', 'image/webp'].includes(mime)) {
90
+ return <ImageResource resourceId={resourceId} onClose={onClose} />
91
+ }
92
+ if (mime?.startsWith('video/')) {
93
+ return <VideoResource resourceId={resourceId} onClose={onClose} />
94
+ }
95
+ if (mime?.startsWith('audio/')) {
96
+ return <AudioResource resourceId={resourceId} onClose={onClose} />
97
+ }
98
+ if (mime === 'application/pdf') {
99
+ return <PDFResource resourceId={resourceId} onClose={onClose} />
100
+ }
101
+
102
+ const chain = resolveMimeViewChain(mime, projection)
103
+ const [primary, secondary] = chain
104
+ return (
105
+ <Slot
106
+ name={primary}
107
+ resourceId={resourceId}
108
+ resource={resource}
109
+ projection={projection}
110
+ fallback={
111
+ <Slot
112
+ name={secondary}
113
+ resourceId={resourceId}
114
+ resource={resource}
115
+ projection={projection}
116
+ fallback={<ResourceGenericView resourceId={resourceId} onClose={onClose} />}
117
+ />
118
+ }
119
+ />
120
+ )
121
+ }
122
+
123
+ /**
124
+ * ADR 0006 host: `resourceId` → fetch → `resource.type` → view/form component chain.
125
+ */
126
+ export function SchemaPresenter ({
127
+ resourceId,
128
+ resource: resourceProp,
129
+ flow = 'View',
130
+ projection,
131
+ form,
132
+ onClose,
133
+ onSubmit,
134
+ onCancel,
135
+ mode,
136
+ initialName,
137
+ initialContent,
138
+ submitLabel,
139
+ }) {
140
+ const sdk = useSdk()
141
+ const schemas = useSchemas()
142
+ const { data: loaded, status } = sdk.read(GetResource, { resourceId }, { enabled: !!resourceId && !resourceProp })
143
+ const resource = resourceProp ?? loaded
144
+
145
+ if (!resourceProp && AsyncStatus.Error === status) {
146
+ return (
147
+ <Guide
148
+ title="Not found"
149
+ text="We can't find the resource you are looking for"
150
+ style={{ padding: '40px 16px' }}
151
+ />
152
+ )
153
+ }
154
+
155
+ if (!resource && [AsyncStatus.Loading, AsyncStatus.NotInitialized].includes(status)) {
156
+ return (
157
+ <DelayedRender>
158
+ <Text>loading...</Text>
159
+ </DelayedRender>
160
+ )
161
+ }
162
+
163
+ if (!resource) return null
164
+
165
+ const isDocumentSchema =
166
+ isSchemaDocumentType(resource.type) || schemas.some(s => s.id === resource.type)
167
+
168
+ if (flow === 'Edit' || flow === 'edit') {
169
+ if (isDocumentSchema) {
170
+ return (
171
+ <GenericResourceForm
172
+ schemaId={resource.type}
173
+ mode="edit"
174
+ resourceId={resourceId}
175
+ resource={resource}
176
+ form={form}
177
+ onSubmit={onSubmit}
178
+ onCancel={onCancel}
179
+ submitLabel={submitLabel}
180
+ />
181
+ )
182
+ }
183
+ return <ResourceGenericView resourceId={resourceId} onClose={onClose} />
184
+ }
185
+
186
+ if (resource.type === PlatformSchema.directory || resource.type === 'directory') {
187
+ return <Text>directory</Text>
188
+ }
189
+
190
+ if (isDocumentSchema) {
191
+ return <SchemaViewPresenter resource={resource} projection={projection} resourceId={resourceId} />
192
+ }
193
+
194
+ const mime = resolveMediaType(resource)
195
+ if (isMimeType(mime) || resource.type === PlatformSchema.file) {
196
+ return <MimeViewPresenter resource={resource} projection={projection} resourceId={resourceId} onClose={onClose} />
197
+ }
198
+
199
+ return <ResourceGenericView resourceId={resourceId} onClose={onClose} />
200
+ }
201
+
202
+ /**
203
+ * Create flow — resolves form component for a schema id.
204
+ */
205
+ export function SchemaFormPresenter (props) {
206
+ const { schemaId, ...rest } = props
207
+ return (
208
+ <GenericResourceForm
209
+ schemaId={schemaId}
210
+ {...rest}
211
+ />
212
+ )
213
+ }
214
+
215
+ export { resolveFormComponentId }
package/src/Upload.jsx CHANGED
@@ -1,5 +1,5 @@
1
- import React, { useState, useEffect } from 'react'
2
- import { Button, View, Text, Upload as _Upload, Icon } from '@ossy/design-system'
1
+ import React, { useState } from 'react'
2
+ import { Button, View, Text, UploadInput, Icon, LocalFilePreview } from '@ossy/design-system'
3
3
  import { useRouter } from '@ossy/router-react'
4
4
 
5
5
  const FlowStage = {
@@ -53,8 +53,8 @@ export const Upload = ({
53
53
  <View gap="s" style={{ flexGrow: '1', overflow: 'auto', padding: 'var(--space-m) 0' }}>
54
54
  {
55
55
  files.map(({ status, file }) => (
56
- <View layout="row" gap="m" alignItems="center">
57
- <UploadImgPreview file={file} />
56
+ <View key={file.name} layout="row" gap="m" alignItems="center">
57
+ <LocalFilePreview file={file} />
58
58
  <Text style={{ margin: '0', lineHeight: '1', textWrap: 'nowrap', textOverflow: 'ellipsis', flexGrow: '1' }}>{file?.name || 'Untitled'}</Text>
59
59
  <View layout="row" gap="xs">
60
60
  <UploadStatus status={status} size="8px" />
@@ -68,7 +68,8 @@ export const Upload = ({
68
68
  }
69
69
 
70
70
  {files.length === 0 && (
71
- <_Upload
71
+ <UploadInput
72
+ id="upload-resources"
72
73
  type="file"
73
74
  multiple
74
75
  onChange={onUserInput}
@@ -133,26 +134,6 @@ export const Upload = ({
133
134
 
134
135
 
135
136
 
136
- function UploadImgPreview({ file, size = "32px" }) {
137
- const [src, setSrc] = useState()
138
-
139
- useEffect(() => {
140
- const reader = new FileReader()
141
- reader.onload = (e) => setSrc(e.target.result)
142
- reader.readAsDataURL(file)
143
- }, [file])
144
-
145
- return (
146
- <View
147
- as="img"
148
- src={src}
149
- width={size}
150
- height={size}
151
- style={{ backgroundSize: 'cover', borderRadius: 'var(--space-s)' }}
152
- />
153
- )
154
- }
155
-
156
137
  function UploadStatus({ status }) {
157
138
  if (status === 'preview') return <></>
158
139
  if (status === 'uploading') return <Icon name="spinner" size="s" animation="rotate" />
@@ -1,7 +1,7 @@
1
1
  import React from 'react'
2
2
  import { uploadFile } from './resource.helpers.js'
3
3
  import { useSdk } from '@ossy/sdk-react'
4
- import { Title, View, Text, useLocale } from '@ossy/design-system'
4
+ import { View, Text, useLocale } from '@ossy/design-system'
5
5
  import { useRouter } from '@ossy/router-react'
6
6
  import { Upload } from './Upload.jsx'
7
7
 
@@ -14,7 +14,7 @@ export const UploadResources = () => {
14
14
  return (
15
15
  <View gap="s" style={{ height: '100%' }}>
16
16
  <View gap="s">
17
- <Title variant="tertiary" style={{ marginBottom: '0' }}>{t('resources.upload.title')}</Title>
17
+ <Text variant="heading-tertiary" style={{ marginBottom: '0' }} as="h3" text="resources.upload.title" />
18
18
  <View layout="row" gap="s">
19
19
  <Text variant="small">{t('resources.upload.location', { location })}</Text>
20
20
  </View>
@@ -7,7 +7,7 @@ export const VideoResource = ({
7
7
  resourceId
8
8
  }) => {
9
9
  const sdk = useSdk()
10
- const { data: resource } = sdk.read(GetResource, { id: resourceId }, { enabled: !!resourceId })
10
+ const { data: resource } = sdk.read(GetResource, { resourceId }, { enabled: !!resourceId })
11
11
 
12
12
  return (
13
13
  <View stack bordered>
@@ -50,7 +50,7 @@ describe('accessFilter', () => {
50
50
  const user = { id: 'u-1', workspaces: ['ws-1', 'ws-2'], policies: [] }
51
51
  const filter = accessFilter(user)
52
52
  expect(filter.$or).toContainEqual({
53
- 'state.access': { $in: ['public', 'workspace'] },
53
+ 'state.access': { $in: ['public', 'workspace', 'restricted'] },
54
54
  'state.belongsTo': { $in: ['ws-1', 'ws-2'] },
55
55
  })
56
56
  })
@@ -1 +1 @@
1
- export const metadata = { id: 'resources/create-page-view', access: 'workspace' }
1
+ export const metadata = { id: '@ossy/resources/actions/create-page-view', access: 'workspace' }
@@ -1,21 +1,20 @@
1
+ import { nanoid } from 'nanoid'
1
2
  import { Aggregate } from '@ossy/event-store'
2
- import { Resource } from './resource.aggregate.js'
3
- import { ResourcesEvents } from './resources.events.js'
4
3
  import { Workspace } from '@ossy/workspaces/server'
5
- import { normalizeAndValidateDocumentContent } from '@ossy/platform'
4
+ import { ResourcesEvents } from './resources.events.js'
5
+ import { commitResource } from './resource-stream.helpers.js'
6
6
 
7
- export const metadata = { id: 'resources/create-page-view' }
7
+ export const metadata = { id: '@ossy/resources/tasks/create-page-view' }
8
8
 
9
- const PAGE_VIEW_TEMPLATE_ID = '@ossy/web/page-view'
9
+ const PAGE_VIEW_SCHEMA_ID = '@ossy/web/schema/page-view'
10
10
  const PAGE_VIEW_LOCATION = '/@ossy/analytics/page-views/'
11
11
 
12
- export async function run({ payload, req }) {
12
+ export async function run ({ payload, req }) {
13
13
  const workspaceId = payload?.workspaceId ?? req?.workspaceId
14
14
  if (!workspaceId) throw Object.assign(new Error('workspaceId is required'), { status: 400 })
15
15
 
16
16
  const workspace = await Aggregate.Of(Workspace, workspaceId).then(Aggregate.View())
17
17
 
18
- // Page-view template is a built-in; instantiate directly without schema validation
19
18
  const contentInput = {
20
19
  ...(payload ?? {}),
21
20
  userAgent: payload?.userAgent ?? req?.headers?.['user-agent'],
@@ -23,8 +22,10 @@ export async function run({ payload, req }) {
23
22
  path: payload?.path ?? '/',
24
23
  }
25
24
 
25
+ const resourceId = nanoid()
26
26
  const event = ResourcesEvents.Created({
27
- type: PAGE_VIEW_TEMPLATE_ID,
27
+ resourceId,
28
+ schemaId: PAGE_VIEW_SCHEMA_ID,
28
29
  createdBy: payload?.userId ?? req?.userId ?? 'public',
29
30
  belongsTo: workspace.id,
30
31
  location: PAGE_VIEW_LOCATION,
@@ -33,6 +34,6 @@ export async function run({ payload, req }) {
33
34
  access: 'restricted',
34
35
  })
35
36
 
36
- const resource = await Aggregate.Of(Resource, event).then(Aggregate.View())
37
+ const resource = await commitResource(event)
37
38
  return { id: resource.id }
38
39
  }
@@ -1 +1 @@
1
- export const metadata = { id: 'resources/create', access: 'workspace' }
1
+ export const metadata = { id: '@ossy/resources/actions/create', access: 'workspace' }