@ossy/resources 1.12.2 → 3.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/package.json +16 -4
  2. package/src/AudioResource.jsx +2 -2
  3. package/src/CreateDirectory.jsx +2 -10
  4. package/src/CreateDirectory.page.jsx +1 -1
  5. package/src/CreateDocument.jsx +2 -2
  6. package/src/CreateDocument.page.jsx +1 -1
  7. package/src/Definition.js +3 -2
  8. package/src/DocumentEdit.jsx +10 -38
  9. package/src/DocumentView.jsx +11 -22
  10. package/src/GenericResourceCard.jsx +25 -7
  11. package/src/GenericResourceDetail.jsx +6 -60
  12. package/src/GenericResourceForm.jsx +171 -37
  13. package/src/ImageResource.jsx +1 -1
  14. package/src/PDFResource.jsx +1 -1
  15. package/src/PlatformFileField.jsx +239 -0
  16. package/src/ResourceContentPage.jsx +5 -5
  17. package/src/ResourceDescription.jsx +1 -1
  18. package/src/ResourceDetails.jsx +3 -4
  19. package/src/ResourceDialogMove.jsx +3 -3
  20. package/src/ResourceFactory.jsx +11 -64
  21. package/src/ResourceGenericView.jsx +1 -1
  22. package/src/ResourceList.jsx +89 -106
  23. package/src/ResourcePanel.jsx +40 -57
  24. package/src/ResourceTags.jsx +1 -1
  25. package/src/ResourcesPage.jsx +26 -35
  26. package/src/SchemaDetailView.jsx +97 -0
  27. package/src/SchemaFormDefault.jsx +86 -0
  28. package/src/SchemaPresenter.jsx +207 -0
  29. package/src/Upload.jsx +6 -25
  30. package/src/Upload.page.jsx +1 -1
  31. package/src/UploadResources.jsx +2 -2
  32. package/src/VideoResource.jsx +1 -1
  33. package/src/access-filter.spec.js +1 -1
  34. package/src/create-page-view.action.js +1 -1
  35. package/src/create-page-view.task.js +10 -9
  36. package/src/create.action.js +1 -1
  37. package/src/create.task.js +27 -29
  38. package/src/delete.action.js +1 -1
  39. package/src/delete.task.js +9 -13
  40. package/src/en.translations.json +25 -24
  41. package/src/get-page-view-stats.action.js +1 -1
  42. package/src/get-page-view-stats.task.js +3 -4
  43. package/src/get-resource-alias.api.js +8 -0
  44. package/src/get-resource-info-alias.api.js +8 -0
  45. package/src/get-resource-info.api.js +36 -0
  46. package/src/get-resource-variant.api.js +8 -0
  47. package/src/get-resource.api.js +51 -0
  48. package/src/get.action.js +1 -1
  49. package/src/get.task.js +6 -6
  50. package/src/index.js +10 -0
  51. package/src/list.action.js +1 -1
  52. package/src/list.task.js +1 -1
  53. package/src/platform-file-field.component.jsx +5 -0
  54. package/src/platform-image-field.component.jsx +5 -0
  55. package/src/resource-create.page.jsx +5 -5
  56. package/src/resource-detail.page.jsx +1 -1
  57. package/src/resource-read.helpers.js +151 -0
  58. package/src/resource-stream.helpers.js +20 -0
  59. package/src/resource-stream.js +108 -0
  60. package/src/resource.helpers.js +9 -9
  61. package/src/resources.attach-media-urls.js +9 -3
  62. package/src/resources.events.js +42 -81
  63. package/src/{resources.spec.js → resources.integration.spec.js} +19 -19
  64. package/src/resources.queries.js +5 -2
  65. package/src/schema-detail.component.jsx +24 -0
  66. package/src/schema-form-default.component.jsx +14 -0
  67. package/src/schemaFormSlots.js +6 -0
  68. package/src/schemaViewSlots.js +6 -0
  69. package/src/search.action.js +1 -1
  70. package/src/search.task.js +1 -1
  71. package/src/server.js +2 -1
  72. package/src/sv.translations.json +25 -24
  73. package/src/update-access.action.js +1 -1
  74. package/src/update-access.task.js +9 -8
  75. package/src/update-content.action.js +1 -1
  76. package/src/update-content.task.js +25 -27
  77. package/src/update-location.action.js +1 -1
  78. package/src/update-location.task.js +9 -8
  79. package/src/update-name.action.js +1 -1
  80. package/src/update-name.task.js +9 -8
  81. package/src/upload-named-version.action.js +1 -1
  82. package/src/upload-named-version.task.js +16 -22
  83. package/src/useActivePath.jsx +3 -8
  84. package/src/useDocumentValidator.js +16 -0
  85. package/src/useSchemaEngine.js +9 -0
  86. package/src/useSchemas.js +29 -0
  87. package/src/useSelectedResourceId.jsx +36 -0
  88. package/src/ResourceContentPage.stories.jsx +0 -19
  89. package/src/resource.aggregate.js +0 -86
@@ -9,7 +9,7 @@ export const ImageResource = ({
9
9
  resourceId
10
10
  }) => {
11
11
  const sdk = useSdk()
12
- const { data: resource } = sdk.read(GetResource, { id: resourceId }, { enabled: !!resourceId })
12
+ const { data: resource } = sdk.read(GetResource, { resourceId }, { enabled: !!resourceId })
13
13
 
14
14
  return (
15
15
  <div style={{ display: 'flex', justifyContent: 'center', padding: 'var(--space-m) var(--space-s) var(--space-xl)' }}>
@@ -7,7 +7,7 @@ export const PDFResource = ({
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>
@@ -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,10 +9,10 @@ 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
- <View {...props} slot="content" style={{ flexDirection: 'column', wrap: 'no-wrap', height: '100%' }}>
15
+ <View {...props} data-region="content" style={{ flexDirection: 'column', wrap: 'no-wrap', height: '100%' }}>
16
16
 
17
17
  <style>
18
18
  {`
@@ -66,7 +66,7 @@ export const ResourceContentPage = (props) => {
66
66
  </PageSection>
67
67
 
68
68
  <View layout="off-center" style={{ flexGrow: '1' }}>
69
- <View slot="content" style={{ padding: 'var(--space-m) var(--space-m) var(--space-xxl)'}}>
69
+ <View data-region="content" style={{ padding: 'var(--space-m) var(--space-m) var(--space-xxl)'}}>
70
70
  <Switch on={status}>
71
71
 
72
72
  <Switch.Case match={[AsyncStatus.Error]}>
@@ -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,
@@ -26,13 +26,13 @@ export const ResourceDialogMove = ({
26
26
  layout="off-center-s" style={{ height: '100%' }}
27
27
  >
28
28
 
29
- <View slot="content">
29
+ <View data-region="content">
30
30
  <View surface="primary" roundness="s">
31
31
  <View surface="primary" inset="l" roundness="s" gap="m">
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>