@ossy/resources 1.1.0 → 1.2.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 +2 -2
- package/src/AudioResource.jsx +27 -0
- package/src/CreateDirectory.jsx +98 -0
- package/src/CreateDirectory.page.jsx +24 -0
- package/src/CreateDocument.jsx +73 -0
- package/src/CreateDocument.page.jsx +24 -0
- package/src/Definition.js +19 -0
- package/src/DocumentEdit.jsx +48 -0
- package/src/DocumentView.jsx +32 -0
- package/src/ImageResource.jsx +22 -0
- package/src/PDFResource.jsx +33 -0
- package/src/ResourceContentPage.jsx +145 -0
- package/src/ResourceContentPage.stories.jsx +19 -0
- package/src/ResourceDescription.jsx +28 -0
- package/src/ResourceDetails.jsx +73 -0
- package/src/ResourceDialogMove.jsx +64 -0
- package/src/ResourceFactory.jsx +64 -0
- package/src/ResourceGenericView.jsx +32 -0
- package/src/ResourceList.jsx +199 -0
- package/src/ResourcePage.jsx +6 -0
- package/src/ResourcePanel.jsx +272 -0
- package/src/ResourceTags.jsx +40 -0
- package/src/ResourcesPage.jsx +66 -0
- package/src/Upload.jsx +177 -0
- package/src/Upload.page.jsx +24 -0
- package/src/UploadResources.jsx +31 -0
- package/src/VideoResource.jsx +24 -0
- package/src/index.js +25 -0
- package/src/useActivePath.jsx +23 -0
- package/src/useForm.js +61 -0
- package/src/utils/format-bytes.js +14 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import React, { useState } from 'react'
|
|
2
|
+
import { useResource, useResourceTemplate } from '@ossy/sdk-react'
|
|
3
|
+
import { Text, View } from '@ossy/design-system'
|
|
4
|
+
import { formatBytes } from './utils/format-bytes.js'
|
|
5
|
+
|
|
6
|
+
export const ResourceDetails = ({ resourceId }) => {
|
|
7
|
+
const { resource, access: saveResourceAccess } = useResource(resourceId)
|
|
8
|
+
const template = useResourceTemplate(resource.type)
|
|
9
|
+
const [accessBusy, setAccessBusy] = useState(false)
|
|
10
|
+
const [accessError, setAccessError] = useState(null)
|
|
11
|
+
|
|
12
|
+
const details = {
|
|
13
|
+
Id: resource?.id,
|
|
14
|
+
Template: template?.name,
|
|
15
|
+
Type: resource?.type,
|
|
16
|
+
Size: resource?.content?.ContentLength && formatBytes(resource?.content?.ContentLength),
|
|
17
|
+
'Last updated': resource?.lastUpdated && new Date(resource?.lastUpdated).toLocaleString(),
|
|
18
|
+
Created: resource?.created && new Date(resource?.created).toLocaleString(),
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const showAccessToggle = resource?.id && resource?.type !== 'directory'
|
|
22
|
+
|
|
23
|
+
const onAccessChange = (event) => {
|
|
24
|
+
const next = event.target.checked ? 'public' : 'restricted'
|
|
25
|
+
setAccessError(null)
|
|
26
|
+
setAccessBusy(true)
|
|
27
|
+
saveResourceAccess({ access: next })
|
|
28
|
+
.catch(() => setAccessError('Could not update access'))
|
|
29
|
+
.finally(() => setAccessBusy(false))
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<View gap="s" style={{ maxWidth: '320px' }}>
|
|
34
|
+
{
|
|
35
|
+
Object.entries(details)
|
|
36
|
+
.filter(([, value]) => ![undefined, null].includes(value))
|
|
37
|
+
.map(([key, value]) => (
|
|
38
|
+
<View layout='row'gap="m" key={key}>
|
|
39
|
+
<Text variant="small" style={{ fontWeight: 'bold' }}>{key}:</Text>
|
|
40
|
+
<span style={{ flexGrow: 1 }}></span>
|
|
41
|
+
<Text variant="small">{value}</Text>
|
|
42
|
+
</View>
|
|
43
|
+
))
|
|
44
|
+
}
|
|
45
|
+
{showAccessToggle && (
|
|
46
|
+
<View gap="xs" style={{ paddingTop: 'var(--space-s)' }}>
|
|
47
|
+
<View layout="row" gap="m" alignItems="center">
|
|
48
|
+
<Text variant="small" style={{ fontWeight: 'bold', flexShrink: 0 }}>
|
|
49
|
+
Public media link
|
|
50
|
+
</Text>
|
|
51
|
+
<span style={{ flexGrow: 1 }} />
|
|
52
|
+
<input
|
|
53
|
+
type="checkbox"
|
|
54
|
+
checked={(resource?.access || 'restricted') === 'public'}
|
|
55
|
+
disabled={accessBusy}
|
|
56
|
+
onChange={onAccessChange}
|
|
57
|
+
aria-label="Allow anyone with the link to view media"
|
|
58
|
+
/>
|
|
59
|
+
</View>
|
|
60
|
+
<Text variant="small" style={{ opacity: 0.85, lineHeight: 1.35 }}>
|
|
61
|
+
When on, image and file URLs use the CDN and work without signing in.
|
|
62
|
+
When off, only workspace members get time-limited download links.
|
|
63
|
+
</Text>
|
|
64
|
+
{accessError && (
|
|
65
|
+
<Text variant="small" style={{ color: 'var(--palette-danger, #c00)' }}>
|
|
66
|
+
{accessError}
|
|
67
|
+
</Text>
|
|
68
|
+
)}
|
|
69
|
+
</View>
|
|
70
|
+
)}
|
|
71
|
+
</View>
|
|
72
|
+
)
|
|
73
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { useResources } from '@ossy/sdk-react'
|
|
3
|
+
import { Button, View, Title, Input, Text, useInputValue } from '@ossy/design-system'
|
|
4
|
+
|
|
5
|
+
export const ResourceDialogMove = ({
|
|
6
|
+
resourceId,
|
|
7
|
+
onClose = () => {},
|
|
8
|
+
}) => {
|
|
9
|
+
const { moveResource } = useResources()
|
|
10
|
+
const [location, setLocation] = useInputValue('')
|
|
11
|
+
|
|
12
|
+
const onMove = () => {
|
|
13
|
+
moveResource(resourceId, location)
|
|
14
|
+
.then(() => {
|
|
15
|
+
onClose()
|
|
16
|
+
})
|
|
17
|
+
.catch(error => {
|
|
18
|
+
console.error('Failed to move resource:', error)
|
|
19
|
+
// Handle error (e.g., show a notification)
|
|
20
|
+
})
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<View
|
|
25
|
+
layout="off-center-s" style={{ height: '100%' }}
|
|
26
|
+
>
|
|
27
|
+
|
|
28
|
+
<View slot="content">
|
|
29
|
+
<View surface="primary" roundness="s">
|
|
30
|
+
<View surface="primary" inset="l" roundness="s" gap="m">
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
<View>
|
|
34
|
+
<Title variant="tertiary">Move Resource</Title>
|
|
35
|
+
</View>
|
|
36
|
+
|
|
37
|
+
<View>
|
|
38
|
+
<Text style={{ fontWeight: 'bold' }}>
|
|
39
|
+
New location:
|
|
40
|
+
</Text>
|
|
41
|
+
<Input
|
|
42
|
+
value={location}
|
|
43
|
+
onChange={setLocation}
|
|
44
|
+
placeholder="/path/to/target/resource"
|
|
45
|
+
/>
|
|
46
|
+
</View>
|
|
47
|
+
|
|
48
|
+
<View>
|
|
49
|
+
|
|
50
|
+
<Button onClick={onClose} style={{ marginTop: 'var(--space-m)' }}>
|
|
51
|
+
Close
|
|
52
|
+
</Button>
|
|
53
|
+
|
|
54
|
+
<Button variant="cta" onClick={onMove}>
|
|
55
|
+
Move
|
|
56
|
+
</Button>
|
|
57
|
+
</View>
|
|
58
|
+
|
|
59
|
+
</View>
|
|
60
|
+
</View>
|
|
61
|
+
</View>
|
|
62
|
+
</View>
|
|
63
|
+
)
|
|
64
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { useResource, AsyncStatus, useWorkspace } from '@ossy/sdk-react'
|
|
3
|
+
import { Guide, Text, DelayedRender } from '@ossy/design-system'
|
|
4
|
+
import { DocumentView } from './DocumentView.jsx'
|
|
5
|
+
import { DocumentEdit } from './DocumentEdit.jsx'
|
|
6
|
+
import { ImageResource } from './ImageResource.jsx'
|
|
7
|
+
import { AudioResource } from './AudioResource.jsx'
|
|
8
|
+
import { VideoResource } from './VideoResource.jsx'
|
|
9
|
+
import { PDFResource } from './PDFResource.jsx'
|
|
10
|
+
import { ResourceGenericView } from './ResourceGenericView.jsx'
|
|
11
|
+
|
|
12
|
+
export const ResourceFactory = ({
|
|
13
|
+
resourceId,
|
|
14
|
+
mode,
|
|
15
|
+
onClose: _onClose,
|
|
16
|
+
form
|
|
17
|
+
}) => {
|
|
18
|
+
const { workspace } = useWorkspace()
|
|
19
|
+
const { resource, status } = useResource(resourceId)
|
|
20
|
+
|
|
21
|
+
console.log('ResourceFactory', { resourceId, mode, resource, status })
|
|
22
|
+
|
|
23
|
+
if (AsyncStatus.Error === status) {
|
|
24
|
+
return (
|
|
25
|
+
<Guide
|
|
26
|
+
title="Not found"
|
|
27
|
+
text="We can't find the resource you are looking for"
|
|
28
|
+
style={{ padding: '40px 16px' }}
|
|
29
|
+
/>
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if ([AsyncStatus.Loading, AsyncStatus.NotInitialized].includes(status)) {
|
|
34
|
+
return (
|
|
35
|
+
<DelayedRender>
|
|
36
|
+
<Text>loading...</Text>
|
|
37
|
+
</DelayedRender>
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (workspace?.resourceTemplates?.find(({ id }) => id === resource.type)) {
|
|
42
|
+
if (mode === 'View') return <DocumentView resourceId={resourceId} onClose={_onClose} mode={mode} />
|
|
43
|
+
if (mode === 'Edit') return <DocumentEdit resourceId={resourceId} onClose={_onClose} mode={mode} form={form} />
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (['image/jpeg', 'image/png'].includes(resource.type)) {
|
|
47
|
+
return <ImageResource resourceId={resourceId} onClose={_onClose} />
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (resource.type.startsWith('video/')) {
|
|
51
|
+
return <VideoResource resourceId={resourceId} onClose={_onClose} />
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (resource.type.startsWith('audio/')) {
|
|
55
|
+
return <AudioResource resourceId={resourceId} onClose={_onClose} />
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (resource.type === 'application/pdf') {
|
|
59
|
+
return <PDFResource resourceId={resourceId} onClose={_onClose} />
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return <ResourceGenericView resourceId={resourceId} onClose={_onClose} />
|
|
63
|
+
|
|
64
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { useResource } from '@ossy/sdk-react'
|
|
3
|
+
import {
|
|
4
|
+
Stack,
|
|
5
|
+
View,
|
|
6
|
+
Icon2,
|
|
7
|
+
} from '@ossy/design-system'
|
|
8
|
+
|
|
9
|
+
export const ResourceGenericView = ({
|
|
10
|
+
resourceId
|
|
11
|
+
}) => {
|
|
12
|
+
const { resource } = useResource(resourceId)
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<Stack bordered>
|
|
16
|
+
<Stack.Item fill>
|
|
17
|
+
<Stack bordered>
|
|
18
|
+
|
|
19
|
+
<Stack.Item fill style={{ padding: '16px 8px' }}>
|
|
20
|
+
|
|
21
|
+
<View alignItems="center" justifyContent="center" style={{ height: '100%', maxHeight: '400px' }}>
|
|
22
|
+
<View roundness="m" style={{ border: '1px solid var(--separator)', padding: 'var(--space-xl) var(--space-xl)'}}>
|
|
23
|
+
<Icon2 size="xl" name="file" />
|
|
24
|
+
</View>
|
|
25
|
+
</View>
|
|
26
|
+
</Stack.Item>
|
|
27
|
+
|
|
28
|
+
</Stack>
|
|
29
|
+
</Stack.Item>
|
|
30
|
+
</Stack>
|
|
31
|
+
)
|
|
32
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
import React, { useRef, useEffect } from 'react'
|
|
3
|
+
import { useWorkspace } from '@ossy/sdk-react'
|
|
4
|
+
import {
|
|
5
|
+
Dropdown,
|
|
6
|
+
DropZone,
|
|
7
|
+
Icon2,
|
|
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
|
+
export const ResourceList = ({
|
|
17
|
+
resources = [],
|
|
18
|
+
onClick = () => {},
|
|
19
|
+
inlineFolder = null,
|
|
20
|
+
}) => {
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<>
|
|
24
|
+
{inlineFolder && <InlineFolderRow {...inlineFolder} />}
|
|
25
|
+
{resources.map(resource => (
|
|
26
|
+
<ResourceListItem
|
|
27
|
+
{...resource}
|
|
28
|
+
key={resource.id}
|
|
29
|
+
onClick={() => {
|
|
30
|
+
onClick(resource)
|
|
31
|
+
resource?.onClick?.(resource)
|
|
32
|
+
}}
|
|
33
|
+
/>
|
|
34
|
+
))}
|
|
35
|
+
</>
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function InlineFolderRow({ name, onChange, onSave, onCancel }) {
|
|
40
|
+
const inputRef = useRef(null)
|
|
41
|
+
const handledRef = useRef(false)
|
|
42
|
+
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
if (inputRef.current) {
|
|
45
|
+
inputRef.current.focus()
|
|
46
|
+
inputRef.current.select()
|
|
47
|
+
}
|
|
48
|
+
}, [])
|
|
49
|
+
|
|
50
|
+
const handleKeyDown = (event) => {
|
|
51
|
+
if (event.key === 'Enter') {
|
|
52
|
+
handledRef.current = true
|
|
53
|
+
onSave(event.target.value)
|
|
54
|
+
} else if (event.key === 'Escape') {
|
|
55
|
+
handledRef.current = true
|
|
56
|
+
onCancel()
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const handleBlur = (event) => {
|
|
61
|
+
if (handledRef.current) return
|
|
62
|
+
onSave(event.target.value)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<View
|
|
67
|
+
layout="row"
|
|
68
|
+
gap="m"
|
|
69
|
+
style={{
|
|
70
|
+
height: '56px',
|
|
71
|
+
flexShrink: 0,
|
|
72
|
+
borderBottom: '1px solid var(--separator-primary)',
|
|
73
|
+
}}
|
|
74
|
+
>
|
|
75
|
+
<View gap="m" layout="row" alignItems="center" style={{ flexGrow: 1, padding: '12px 0 12px 20px' }}>
|
|
76
|
+
<Icon2 size="s" name="folder" style={{ fill: 'hsl(0, 0%, 60%)' }} />
|
|
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
|
+
}}
|
|
95
|
+
/>
|
|
96
|
+
</View>
|
|
97
|
+
</View>
|
|
98
|
+
)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function ResourceListItem({
|
|
102
|
+
onClick,
|
|
103
|
+
href,
|
|
104
|
+
onDrop,
|
|
105
|
+
dragData,
|
|
106
|
+
...resource
|
|
107
|
+
}) {
|
|
108
|
+
|
|
109
|
+
let Container = ({ children }) => <>{children}</>
|
|
110
|
+
|
|
111
|
+
if (onDrop) {
|
|
112
|
+
Container = DropZone
|
|
113
|
+
} else if (dragData) {
|
|
114
|
+
Container = DropZone.Dragable
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return (
|
|
118
|
+
<Container onDrop={onDrop} dragData={dragData}>
|
|
119
|
+
<View
|
|
120
|
+
layout="row"
|
|
121
|
+
selectable
|
|
122
|
+
gap="m"
|
|
123
|
+
style={{
|
|
124
|
+
height: '56px',
|
|
125
|
+
flexShrink: 0,
|
|
126
|
+
borderBottom: '1px solid var(--separator-primary)',
|
|
127
|
+
}}
|
|
128
|
+
>
|
|
129
|
+
<View as={href ? 'a' : undefined} gap="m" layout='row' alignItems="center" style={{ flexGrow: 1, padding: '12px 0 12px 20px', }} href={href} onClick={onClick}>
|
|
130
|
+
<ResourceIcon {...resource} />
|
|
131
|
+
<Text>{typeof resource.name === 'string' && resource.name}</Text>
|
|
132
|
+
<View style={{ flexGrow: 1 }}/>
|
|
133
|
+
<Size {...resource} />
|
|
134
|
+
</View>
|
|
135
|
+
|
|
136
|
+
<View style={{ padding: '12px 8px 12px 0'}}>
|
|
137
|
+
<RowActions {...resource} />
|
|
138
|
+
</View>
|
|
139
|
+
</View>
|
|
140
|
+
</Container>
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function Size(resource) {
|
|
146
|
+
|
|
147
|
+
if (resource.content?.ContentLength) {
|
|
148
|
+
return (
|
|
149
|
+
<Text variant="small">
|
|
150
|
+
{formatBytes(resource.content.ContentLength)}
|
|
151
|
+
</Text>
|
|
152
|
+
)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return <></>
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function ResourceIcon({ ...resource }) {
|
|
159
|
+
const { workspace } = useWorkspace()
|
|
160
|
+
|
|
161
|
+
const resourceTemplates = (workspace.resourceTemplates || [])
|
|
162
|
+
.reduce((acc, curr) => ({ ...acc, [curr.id]: curr }), {})
|
|
163
|
+
|
|
164
|
+
if (resource?.type === 'directory') {
|
|
165
|
+
return (
|
|
166
|
+
<Icon2 size="s" name="folder" style={{ fill: 'hsl(0, 0%, 60%)'}} />
|
|
167
|
+
)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
else if (resource.type.startsWith('image')) {
|
|
171
|
+
return (
|
|
172
|
+
<Image
|
|
173
|
+
src={resource?.content?.sizes?.thumbnailSmall || resource?.content?.src}
|
|
174
|
+
placeholderSrc={resource?.content?.sizes?.['loader-square-blurred-after'] || resource?.content?.src}
|
|
175
|
+
style={{ width: '24px', height: '24px', borderRadius: '25%' }}
|
|
176
|
+
/>
|
|
177
|
+
)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return (
|
|
181
|
+
<Icon2 size="s" name={resourceTemplates[resource.type]?.icon || 'file'} style={{ fill: 'hsl(0, 0%, 80%)'}} />
|
|
182
|
+
)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function RowActions({actions}) {
|
|
186
|
+
|
|
187
|
+
if (!actions) return <></>
|
|
188
|
+
|
|
189
|
+
return (
|
|
190
|
+
<Dropdown trigger={<Button prefix="more-vertical-alt" variant="command" /> } >
|
|
191
|
+
<View inset="xs" surface="primary" roundness="s">
|
|
192
|
+
<ContextMenu roundness="s" surface="primary">
|
|
193
|
+
{actions}
|
|
194
|
+
</ContextMenu>
|
|
195
|
+
</View>
|
|
196
|
+
</Dropdown>
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
}
|