@ossy/resources 1.0.1 → 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.
@@ -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
+ }
@@ -0,0 +1,6 @@
1
+ import React from 'react'
2
+ import { ResourceContentPage } from './ResourceContentPage.jsx'
3
+
4
+ export const ResourcePage = () => {
5
+ return <ResourceContentPage />
6
+ }