@ossy/resources 3.7.0 → 3.8.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 +5 -5
- package/src/CreateDocument.jsx +75 -7
- package/src/ResourceGrid.jsx +1 -1
- package/src/ResourceList.jsx +1 -1
- package/src/ResourcePanel.jsx +1 -1
- package/src/VideoResource.jsx +13 -14
- package/src/document-create.helpers.js +78 -0
- package/src/document-create.helpers.spec.js +71 -0
- package/src/en.translations.json +3 -0
- package/src/index.js +1 -0
- package/src/resources.action-media.js +17 -2
- package/src/resources.attach-media-urls.js +22 -5
- package/src/sv.translations.json +3 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ossy/resources",
|
|
3
3
|
"description": "Resource domain — aggregate and events for the Ossy resource model",
|
|
4
|
-
"version": "3.
|
|
4
|
+
"version": "3.8.0",
|
|
5
5
|
"private": false,
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "./src/index.js",
|
|
@@ -23,10 +23,10 @@
|
|
|
23
23
|
"src": "./src"
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"@ossy/event-store": "^3.
|
|
26
|
+
"@ossy/event-store": "^3.8.0",
|
|
27
27
|
"@ossy/fold": "^3.4.0",
|
|
28
|
-
"@ossy/platform": "^3.
|
|
29
|
-
"@ossy/schema": "^3.
|
|
28
|
+
"@ossy/platform": "^3.8.0",
|
|
29
|
+
"@ossy/schema": "^3.8.0"
|
|
30
30
|
},
|
|
31
31
|
"peerDependencies": {
|
|
32
32
|
"@ossy/design-system": ">=1.0.0",
|
|
@@ -49,5 +49,5 @@
|
|
|
49
49
|
"/src",
|
|
50
50
|
"README.md"
|
|
51
51
|
],
|
|
52
|
-
"gitHead": "
|
|
52
|
+
"gitHead": "14548dfc2019e7258e8c75db527acbf66fe829bd"
|
|
53
53
|
}
|
package/src/CreateDocument.jsx
CHANGED
|
@@ -1,36 +1,104 @@
|
|
|
1
|
-
import React, { useState } from 'react'
|
|
1
|
+
import React, { useMemo, useState } from 'react'
|
|
2
2
|
import { createDocument } from './resource.helpers.js'
|
|
3
3
|
import { useSdk } from '@ossy/sdk-react'
|
|
4
|
-
import { Alert, useLocale } from '@ossy/design-system'
|
|
4
|
+
import { Alert, Button, Text, View, useLocale } from '@ossy/design-system'
|
|
5
5
|
import { useRouter } from '@ossy/router-react'
|
|
6
6
|
import { GenericResourceForm } from './GenericResourceForm.jsx'
|
|
7
|
+
import { useSchemas } from './useSchemas.js'
|
|
8
|
+
import { safeReturnTo, schemasForGenericCreate } from './document-create.helpers.js'
|
|
7
9
|
|
|
8
10
|
export const CreateDocument = () => {
|
|
9
11
|
const { t } = useLocale()
|
|
10
12
|
const router = useRouter()
|
|
11
13
|
const schemaId = router.searchParams.schemaId
|
|
12
14
|
const location = router.searchParams.location
|
|
15
|
+
const category = router.searchParams.category
|
|
13
16
|
const sdk = useSdk()
|
|
17
|
+
const schemas = useSchemas()
|
|
14
18
|
const [error, setError] = useState()
|
|
15
19
|
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
20
|
+
const returnTo = safeReturnTo(router.searchParams.returnTo)
|
|
21
|
+
|
|
22
|
+
const creatable = useMemo(
|
|
23
|
+
() => schemasForGenericCreate(schemas, { category: category || undefined }),
|
|
24
|
+
[schemas, category],
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
const grouped = useMemo(() => {
|
|
28
|
+
const groups = new Map()
|
|
29
|
+
for (const schema of creatable) {
|
|
30
|
+
const name = schema.categoryName || t('resources.form.otherCategory')
|
|
31
|
+
const list = groups.get(name) || []
|
|
32
|
+
list.push(schema)
|
|
33
|
+
groups.set(name, list)
|
|
34
|
+
}
|
|
35
|
+
return [...groups.entries()].sort(([a], [b]) => a.localeCompare(b))
|
|
36
|
+
}, [creatable, t])
|
|
37
|
+
|
|
38
|
+
const onDone = () => {
|
|
39
|
+
if (returnTo) {
|
|
40
|
+
router.navigate(returnTo)
|
|
41
|
+
return
|
|
42
|
+
}
|
|
43
|
+
const search = location ? new URLSearchParams({ location }).toString() : ''
|
|
44
|
+
router.navigate(`@storage/home${search ? `?${search}` : ''}`)
|
|
19
45
|
}
|
|
20
46
|
|
|
21
47
|
const onSubmit = ({ name, content, type }) => {
|
|
22
48
|
createDocument(sdk, { type, location, name, content })
|
|
23
|
-
.then(() =>
|
|
49
|
+
.then(() => onDone())
|
|
24
50
|
.catch((err) => setError(err?.message || t('resources.form.createFailed')))
|
|
25
51
|
}
|
|
26
52
|
|
|
53
|
+
const selectSchema = (id) => {
|
|
54
|
+
const params = new URLSearchParams()
|
|
55
|
+
if (location) params.set('location', location)
|
|
56
|
+
if (category) params.set('category', category)
|
|
57
|
+
if (returnTo) params.set('returnTo', returnTo)
|
|
58
|
+
params.set('schemaId', id)
|
|
59
|
+
router.navigate(`@create-document?${params.toString()}`)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (!schemaId) {
|
|
63
|
+
return (
|
|
64
|
+
<View gap="m" style={{ minWidth: 'min(28rem, calc(100vw - 2rem))' }}>
|
|
65
|
+
<View gap="xs">
|
|
66
|
+
<Text variant="heading-tertiary" as="h3" text="resources.createDocument.title" />
|
|
67
|
+
<Text color="secondary" text="resources.createDocument.pickType" />
|
|
68
|
+
</View>
|
|
69
|
+
{grouped.length === 0 ? (
|
|
70
|
+
<Text color="secondary" text="resources.createDocument.noTypes" />
|
|
71
|
+
) : grouped.map(([categoryName, items]) => (
|
|
72
|
+
<View key={categoryName} gap="s">
|
|
73
|
+
<Text variant="small" weight="medium">{categoryName}</Text>
|
|
74
|
+
<View layout="row" gap="s" style={{ flexWrap: 'wrap' }}>
|
|
75
|
+
{items.map((schema) => (
|
|
76
|
+
<Button
|
|
77
|
+
key={schema.id}
|
|
78
|
+
variant="neutral"
|
|
79
|
+
prefix={schema.icon || 'add'}
|
|
80
|
+
onClick={() => selectSchema(schema.id)}
|
|
81
|
+
>
|
|
82
|
+
{schema.name || schema.id}
|
|
83
|
+
</Button>
|
|
84
|
+
))}
|
|
85
|
+
</View>
|
|
86
|
+
</View>
|
|
87
|
+
))}
|
|
88
|
+
<View layout="row" justifyContent="flex-end">
|
|
89
|
+
<Button variant="link" onClick={onDone} label="design-system.cancel" />
|
|
90
|
+
</View>
|
|
91
|
+
</View>
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
|
|
27
95
|
return (
|
|
28
96
|
<>
|
|
29
97
|
<GenericResourceForm
|
|
30
98
|
schemaId={schemaId}
|
|
31
99
|
mode="create"
|
|
32
100
|
onSubmit={onSubmit}
|
|
33
|
-
onCancel={
|
|
101
|
+
onCancel={onDone}
|
|
34
102
|
/>
|
|
35
103
|
{error && <Alert>{error}</Alert>}
|
|
36
104
|
</>
|
package/src/ResourceGrid.jsx
CHANGED
package/src/ResourceList.jsx
CHANGED
|
@@ -160,7 +160,7 @@ const ResourceListItem = memo(function ResourceListItem({
|
|
|
160
160
|
|
|
161
161
|
const leading = (
|
|
162
162
|
<View layout="row" alignItems="center" gap="s" style={{ flexShrink: 0 }}>
|
|
163
|
-
{
|
|
163
|
+
{showSelectionControls && (
|
|
164
164
|
<ResourceSelectionCheckbox
|
|
165
165
|
selected={selected}
|
|
166
166
|
onToggle={handleToggleSelect}
|
package/src/ResourcePanel.jsx
CHANGED
|
@@ -176,7 +176,7 @@ export const ResourcePanel = memo(function ResourcePanel ({
|
|
|
176
176
|
<Switch.Case match={[ViewMode.View]}>
|
|
177
177
|
|
|
178
178
|
<View.Item fill surface="primary" style={{ padding: '4px 8px' }}>
|
|
179
|
-
<Text as="h3" variant="
|
|
179
|
+
<Text as="h3" variant="small" style={{ fontWeight: 'bold' }}>{resourceName}</Text>
|
|
180
180
|
</View.Item>
|
|
181
181
|
|
|
182
182
|
{downloadHref && (
|
package/src/VideoResource.jsx
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import React
|
|
1
|
+
import React from 'react'
|
|
2
2
|
import { metadata as GetResource } from './get.action.js'
|
|
3
3
|
import { useSdk } from '@ossy/sdk-react'
|
|
4
|
-
import { View } from '@ossy/design-system'
|
|
5
4
|
|
|
6
5
|
export const VideoResource = ({
|
|
7
6
|
resourceId
|
|
@@ -10,17 +9,17 @@ export const VideoResource = ({
|
|
|
10
9
|
const { data: resource } = sdk.read(GetResource, { resourceId }, { enabled: !!resourceId })
|
|
11
10
|
|
|
12
11
|
return (
|
|
13
|
-
<
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
</
|
|
12
|
+
<div style={{ display: 'flex', justifyContent: 'center', padding: 'var(--space-m) var(--space-s) var(--space-xl)' }}>
|
|
13
|
+
<video
|
|
14
|
+
controls
|
|
15
|
+
src={resource?.content?.src}
|
|
16
|
+
style={{
|
|
17
|
+
width: '100%',
|
|
18
|
+
maxWidth: '960px',
|
|
19
|
+
height: 'auto',
|
|
20
|
+
margin: 'var(--space-m) auto var(--space-l)',
|
|
21
|
+
}}
|
|
22
|
+
/>
|
|
23
|
+
</div>
|
|
25
24
|
)
|
|
26
25
|
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { parseSchemaId } from '@ossy/schema'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Document types that can be created from the generic create-document GUI.
|
|
5
|
+
* Platform internals and capability schemas are excluded.
|
|
6
|
+
*
|
|
7
|
+
* @param {object} schema
|
|
8
|
+
* @returns {boolean}
|
|
9
|
+
*/
|
|
10
|
+
export function isGenericCreateSchema (schema) {
|
|
11
|
+
if (!schema?.id || !Array.isArray(schema.fields) || schema.fields.length === 0) return false
|
|
12
|
+
const category = schema.categoryName ?? ''
|
|
13
|
+
if (/platform/i.test(category)) return false
|
|
14
|
+
return true
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function kebabConcept (value) {
|
|
18
|
+
return String(value)
|
|
19
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1-$2')
|
|
20
|
+
.replace(/_/g, '-')
|
|
21
|
+
.toLowerCase()
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Treat canonical ADR 0006 ids and pre-migration `@provider/feature/concept` ids
|
|
26
|
+
* as the same create-document type (e.g. employee vs schema/employee).
|
|
27
|
+
*
|
|
28
|
+
* @param {string} [schemaId]
|
|
29
|
+
* @returns {string | undefined}
|
|
30
|
+
*/
|
|
31
|
+
export function schemaCreateIdentity (schemaId) {
|
|
32
|
+
if (typeof schemaId !== 'string' || !schemaId) return schemaId
|
|
33
|
+
const parsed = parseSchemaId(schemaId)
|
|
34
|
+
if (parsed) return `${parsed.provider}/${parsed.feature}/${kebabConcept(parsed.concept)}`
|
|
35
|
+
const legacy = /^@([^/]+)\/([^/]+)\/([^/]+)$/.exec(schemaId)
|
|
36
|
+
if (legacy) return `${legacy[1]}/${legacy[2]}/${kebabConcept(legacy[3])}`
|
|
37
|
+
return schemaId
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function schemaNameKey (schema) {
|
|
41
|
+
const name = schema?.name
|
|
42
|
+
if (!name) return null
|
|
43
|
+
return `name:${schema.categoryName ?? ''}:${name}`
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* @param {object[]} schemas
|
|
48
|
+
* @param {{ category?: string }} [options]
|
|
49
|
+
* @returns {object[]}
|
|
50
|
+
*/
|
|
51
|
+
export function schemasForGenericCreate (schemas, { category } = {}) {
|
|
52
|
+
const list = Array.isArray(schemas) ? schemas : []
|
|
53
|
+
const seen = new Set()
|
|
54
|
+
const result = []
|
|
55
|
+
for (const schema of list) {
|
|
56
|
+
if (!isGenericCreateSchema(schema)) continue
|
|
57
|
+
if (category && schema.categoryName !== category) continue
|
|
58
|
+
const identity = schemaCreateIdentity(schema.id)
|
|
59
|
+
const nameKey = schemaNameKey(schema)
|
|
60
|
+
if (seen.has(identity) || (nameKey && seen.has(nameKey))) continue
|
|
61
|
+
seen.add(identity)
|
|
62
|
+
if (nameKey) seen.add(nameKey)
|
|
63
|
+
result.push(schema)
|
|
64
|
+
}
|
|
65
|
+
return result
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Same-origin relative path only.
|
|
70
|
+
* @param {unknown} value
|
|
71
|
+
* @returns {string | null}
|
|
72
|
+
*/
|
|
73
|
+
export function safeReturnTo (value) {
|
|
74
|
+
if (typeof value !== 'string') return null
|
|
75
|
+
const trimmed = value.trim()
|
|
76
|
+
if (!trimmed.startsWith('/') || trimmed.startsWith('//')) return null
|
|
77
|
+
return trimmed
|
|
78
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { describe, expect, it } from '@jest/globals'
|
|
2
|
+
import { isGenericCreateSchema, safeReturnTo, schemaCreateIdentity, schemasForGenericCreate } from './document-create.helpers.js'
|
|
3
|
+
|
|
4
|
+
describe('document-create helpers', () => {
|
|
5
|
+
it('excludes platform schemas and field-less types', () => {
|
|
6
|
+
const schemas = [
|
|
7
|
+
{ id: '@ossy/consultancy/schema/employee', categoryName: 'Consultancy', fields: [{ name: 'name', type: 'text' }] },
|
|
8
|
+
{ id: '@ossy/platform/schema/file', categoryName: 'Platform', fields: [{ name: 'name', type: 'text' }] },
|
|
9
|
+
{ id: '@ossy/empty/schema/x', categoryName: 'Other', fields: [] },
|
|
10
|
+
]
|
|
11
|
+
expect(schemasForGenericCreate(schemas).map((s) => s.id)).toEqual([
|
|
12
|
+
'@ossy/consultancy/schema/employee',
|
|
13
|
+
])
|
|
14
|
+
expect(isGenericCreateSchema(schemas[1])).toBe(false)
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it('filters by category when asked', () => {
|
|
18
|
+
const schemas = [
|
|
19
|
+
{ id: 'a', categoryName: 'Consultancy', fields: [{ name: 'n', type: 'text' }] },
|
|
20
|
+
{ id: 'b', categoryName: 'Web', fields: [{ name: 'n', type: 'text' }] },
|
|
21
|
+
]
|
|
22
|
+
expect(schemasForGenericCreate(schemas, { category: 'Consultancy' }).map((s) => s.id)).toEqual(['a'])
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('aliases canonical and pre-migration consultancy ids', () => {
|
|
26
|
+
expect(schemaCreateIdentity('@ossy/consultancy/schema/employee')).toBe('ossy/consultancy/employee')
|
|
27
|
+
expect(schemaCreateIdentity('@ossy/consultancy/employee')).toBe('ossy/consultancy/employee')
|
|
28
|
+
expect(schemaCreateIdentity('@ossy/consultancy/companyBasicInfo')).toBe('ossy/consultancy/company-basic-info')
|
|
29
|
+
expect(schemaCreateIdentity('@ossy/consultancy/schema/company-basic-info')).toBe('ossy/consultancy/company-basic-info')
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('hides workspace copies of system schemas on the create picker', () => {
|
|
33
|
+
const schemas = [
|
|
34
|
+
{ id: '@ossy/consultancy/schema/company-basic-info', name: 'Company Basic Info', categoryName: 'Consultancy', icon: 'organisation', fields: [{ name: 'name', type: 'text' }] },
|
|
35
|
+
{ id: '@ossy/consultancy/schema/contract', name: 'Contract', categoryName: 'Consultancy', icon: 'file', fields: [{ name: 'name', type: 'text' }] },
|
|
36
|
+
{ id: '@ossy/consultancy/schema/employee', name: 'Employee', categoryName: 'Consultancy', icon: 'user-add', fields: [{ name: 'name', type: 'text' }] },
|
|
37
|
+
{ id: '@ossy/consultancy/schema/opportunity', name: 'Opportunity', categoryName: 'Consultancy', fields: [{ name: 'name', type: 'text' }] },
|
|
38
|
+
{ id: '@ossy/consultancy/schema/proposal', name: 'Proposal', categoryName: 'Consultancy', icon: 'file', fields: [{ name: 'name', type: 'text' }] },
|
|
39
|
+
{ id: '@ossy/consultancy/schema/vacation', name: 'Vacation', categoryName: 'Consultancy', icon: 'calendar', fields: [{ name: 'name', type: 'text' }] },
|
|
40
|
+
{ id: '@ossy/consultancy/vacation', name: 'Vacation', categoryName: 'Consultancy', fields: [{ name: 'name', type: 'text' }] },
|
|
41
|
+
{ id: '@ossy/consultancy/employee', name: 'Employee', categoryName: 'Consultancy', fields: [{ name: 'name', type: 'text' }] },
|
|
42
|
+
{ id: '@ossy/consultancy/companyBasicInfo', name: 'Company Basic Info', categoryName: 'Consultancy', fields: [{ name: 'name', type: 'text' }] },
|
|
43
|
+
{ id: '@ossy/consultancy/contract', name: 'Contract', categoryName: 'Consultancy', fields: [{ name: 'name', type: 'text' }] },
|
|
44
|
+
]
|
|
45
|
+
expect(schemasForGenericCreate(schemas).map((s) => s.id)).toEqual([
|
|
46
|
+
'@ossy/consultancy/schema/company-basic-info',
|
|
47
|
+
'@ossy/consultancy/schema/contract',
|
|
48
|
+
'@ossy/consultancy/schema/employee',
|
|
49
|
+
'@ossy/consultancy/schema/opportunity',
|
|
50
|
+
'@ossy/consultancy/schema/proposal',
|
|
51
|
+
'@ossy/consultancy/schema/vacation',
|
|
52
|
+
])
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('keeps workspace-only custom types', () => {
|
|
56
|
+
const schemas = [
|
|
57
|
+
{ id: '@ossy/consultancy/schema/employee', name: 'Employee', categoryName: 'Consultancy', fields: [{ name: 'name', type: 'text' }] },
|
|
58
|
+
{ id: 'workspace/custom-invoice', name: 'Invoice', categoryName: 'Consultancy', fields: [{ name: 'name', type: 'text' }] },
|
|
59
|
+
]
|
|
60
|
+
expect(schemasForGenericCreate(schemas).map((s) => s.id)).toEqual([
|
|
61
|
+
'@ossy/consultancy/schema/employee',
|
|
62
|
+
'workspace/custom-invoice',
|
|
63
|
+
])
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it('only allows same-origin relative returnTo paths', () => {
|
|
67
|
+
expect(safeReturnTo('/forecasts')).toBe('/forecasts')
|
|
68
|
+
expect(safeReturnTo('//evil.example')).toBe(null)
|
|
69
|
+
expect(safeReturnTo('https://evil.example')).toBe(null)
|
|
70
|
+
})
|
|
71
|
+
})
|
package/src/en.translations.json
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
"resources.form.createFailed": "Create failed",
|
|
9
9
|
"resources.form.createResource": "Create resource",
|
|
10
10
|
"resources.form.typeRemoved": "This document type has been removed and can't be edited.",
|
|
11
|
+
"resources.form.otherCategory": "Other",
|
|
11
12
|
"@ossy/resources/actions/create.label": "Create resource",
|
|
12
13
|
"@ossy/resources/actions/create.description": "Create a new resource in the workspace",
|
|
13
14
|
"@ossy/resources/actions/list.label": "List resources",
|
|
@@ -41,6 +42,8 @@
|
|
|
41
42
|
"resources.upload.location": "Location: {location}",
|
|
42
43
|
"resources.createDirectory.title": "Create directory",
|
|
43
44
|
"resources.createDocument.title": "Create document",
|
|
45
|
+
"resources.createDocument.pickType": "Choose a document type to create in this location.",
|
|
46
|
+
"resources.createDocument.noTypes": "No document types are available to create here.",
|
|
44
47
|
"resources.createDirectory.description": "Create a new directory to organize your resources. The new directory will be created in the location: {location}",
|
|
45
48
|
"resources.createDirectory.namePlaceholder": "Directory name",
|
|
46
49
|
"resources.createDirectory.submit": "Create directory",
|
package/src/index.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { attachResourceMediaUrls } from './resources.attach-media-urls.js'
|
|
2
|
+
import { createLogger } from '@ossy/observability'
|
|
3
|
+
|
|
4
|
+
const log = createLogger('resources')
|
|
2
5
|
|
|
3
6
|
export function mediaContextFromRun({ payload, req }) {
|
|
4
7
|
return {
|
|
@@ -7,12 +10,24 @@ export function mediaContextFromRun({ payload, req }) {
|
|
|
7
10
|
}
|
|
8
11
|
}
|
|
9
12
|
|
|
13
|
+
async function attachOrKeep (resource, context) {
|
|
14
|
+
try {
|
|
15
|
+
return await attachResourceMediaUrls(resource, context)
|
|
16
|
+
} catch (error) {
|
|
17
|
+
log.warn('[withResourceMedia] skipping media urls', {
|
|
18
|
+
resourceId: resource?.id,
|
|
19
|
+
error: error?.message,
|
|
20
|
+
})
|
|
21
|
+
return resource
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
10
25
|
export async function withResourceMedia(result, context) {
|
|
11
26
|
if (Array.isArray(result)) {
|
|
12
|
-
return Promise.all(result.map(r =>
|
|
27
|
+
return Promise.all(result.map(r => attachOrKeep(r, context)))
|
|
13
28
|
}
|
|
14
29
|
if (result && typeof result === 'object' && result.id) {
|
|
15
|
-
return
|
|
30
|
+
return attachOrKeep(result, context)
|
|
16
31
|
}
|
|
17
32
|
return result
|
|
18
33
|
}
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { StorageClient } from '@ossy/platform'
|
|
2
2
|
import { Aggregate } from '@ossy/event-store'
|
|
3
|
+
import { coerceStorageKey } from '@ossy/platform/storage-keys'
|
|
3
4
|
import { Workspace } from '@ossy/workspaces/server'
|
|
5
|
+
import { createLogger } from '@ossy/observability'
|
|
6
|
+
|
|
7
|
+
const log = createLogger('resources')
|
|
4
8
|
|
|
5
9
|
/**
|
|
6
10
|
* Adds `content.src` (and `content.sizes[*]` URLs) for file resources, respecting
|
|
@@ -39,11 +43,19 @@ export async function attachResourceMediaUrls(resource, { userId, workspaceId }
|
|
|
39
43
|
return { ...resource, content: nextContent }
|
|
40
44
|
}
|
|
41
45
|
|
|
42
|
-
|
|
46
|
+
const originalKey = coerceStorageKey(Key, resource.id)
|
|
47
|
+
if (Key && !originalKey) {
|
|
48
|
+
log.warn('[attachResourceMediaUrls] skipping invalid storage key', {
|
|
49
|
+
resourceId: resource.id,
|
|
50
|
+
key: Key,
|
|
51
|
+
})
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (originalKey) {
|
|
43
55
|
nextContent.src =
|
|
44
56
|
access === 'public'
|
|
45
|
-
? StorageClient.createDownloadUrl(
|
|
46
|
-
: await StorageClient.createPresignedDownloadUrl(
|
|
57
|
+
? StorageClient.createDownloadUrl(originalKey)
|
|
58
|
+
: await StorageClient.createPresignedDownloadUrl(originalKey)
|
|
47
59
|
}
|
|
48
60
|
|
|
49
61
|
if (hasSizes) {
|
|
@@ -54,10 +66,15 @@ export async function attachResourceMediaUrls(resource, { userId, workspaceId }
|
|
|
54
66
|
nextSizes[name] = value
|
|
55
67
|
continue
|
|
56
68
|
}
|
|
69
|
+
const sizeFallback = resource.id && /^[A-Za-z0-9_-]+$/.test(name)
|
|
70
|
+
? `${resource.id}:${name}`
|
|
71
|
+
: resource.id
|
|
72
|
+
const logical = coerceStorageKey(value, sizeFallback)
|
|
73
|
+
if (!logical) continue
|
|
57
74
|
nextSizes[name] =
|
|
58
75
|
access === 'public'
|
|
59
|
-
? StorageClient.createDownloadUrl(
|
|
60
|
-
: await StorageClient.createPresignedDownloadUrl(
|
|
76
|
+
? StorageClient.createDownloadUrl(logical)
|
|
77
|
+
: await StorageClient.createPresignedDownloadUrl(logical)
|
|
61
78
|
}
|
|
62
79
|
nextContent.sizes = nextSizes
|
|
63
80
|
}
|
package/src/sv.translations.json
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
"resources.form.createFailed": "Skapandet misslyckades",
|
|
9
9
|
"resources.form.createResource": "Skapa resurs",
|
|
10
10
|
"resources.form.typeRemoved": "Den här dokumenttypen har tagits bort och kan inte redigeras.",
|
|
11
|
+
"resources.form.otherCategory": "Övrigt",
|
|
11
12
|
"@ossy/resources/actions/create.label": "Skapa resurs",
|
|
12
13
|
"@ossy/resources/actions/create.description": "Skapa en ny resurs i workspace",
|
|
13
14
|
"@ossy/resources/actions/list.label": "Lista resurser",
|
|
@@ -41,6 +42,8 @@
|
|
|
41
42
|
"resources.upload.location": "Plats: {location}",
|
|
42
43
|
"resources.createDirectory.title": "Skapa mapp",
|
|
43
44
|
"resources.createDocument.title": "Skapa dokument",
|
|
45
|
+
"resources.createDocument.pickType": "Välj en dokumenttyp att skapa på den här platsen.",
|
|
46
|
+
"resources.createDocument.noTypes": "Inga dokumenttyper kan skapas här.",
|
|
44
47
|
"resources.createDirectory.description": "Skapa en ny mapp för att organisera dina resurser. Den nya mappen skapas på platsen: {location}",
|
|
45
48
|
"resources.createDirectory.namePlaceholder": "Mappnamn",
|
|
46
49
|
"resources.createDirectory.submit": "Skapa mapp",
|