@ossy/resources 3.9.0 → 3.11.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 +4 -4
- package/src/CreateDirectory.jsx +47 -53
- package/src/CreateDocument.jsx +1 -0
- package/src/GenericResourceForm.jsx +49 -31
- package/src/ResourceDialogMove.jsx +99 -50
- package/src/ResourceGrid.jsx +36 -84
- package/src/ResourceList.jsx +10 -1
- package/src/ResourcePanel.jsx +96 -17
- package/src/ResourceShareDialog.jsx +50 -12
- package/src/SchemaDetailView.jsx +15 -3
- package/src/Upload.jsx +25 -8
- package/src/batch-delete.flow.js +81 -0
- package/src/batch-download.flow.js +69 -0
- package/src/cancel-upload.action.js +4 -0
- package/src/cancel-upload.flow.js +40 -0
- package/src/close-share.action.js +4 -0
- package/src/confirm-upload.action.js +4 -0
- package/src/copy-share-link.action.js +4 -0
- package/src/create-directory.action.js +5 -0
- package/src/create-directory.flow.js +30 -0
- package/src/create-directory.form.js +5 -0
- package/src/create-directory.schema.js +9 -0
- package/src/create-document.flow.js +36 -0
- package/src/create-document.form.js +5 -0
- package/src/create-document.schema.js +11 -0
- package/src/create.action.js +5 -1
- package/src/delete-directory.flow.js +39 -0
- package/src/delete-file.flow.js +32 -0
- package/src/done-selecting.action.js +5 -0
- package/src/download-file.flow.js +31 -0
- package/src/download-resource.action.js +4 -0
- package/src/download-selected.action.js +4 -0
- package/src/en.translations.json +18 -0
- package/src/finish-upload.action.js +4 -0
- package/src/index.js +42 -0
- package/src/list.task.js +17 -6
- package/src/list.task.spec.js +81 -0
- package/src/make-private-resource.flow.js +72 -0
- package/src/make-private.action.js +4 -0
- package/src/make-public.action.js +4 -0
- package/src/move-file.flow.js +78 -0
- package/src/move.form.js +5 -0
- package/src/move.schema.js +9 -0
- package/src/open-delete-directory.action.js +7 -0
- package/src/open-move.action.js +4 -0
- package/src/open-remove-selected.action.js +5 -0
- package/src/open-rename.action.js +4 -0
- package/src/open-share.action.js +4 -0
- package/src/open-sort.action.js +5 -0
- package/src/open-view.action.js +5 -0
- package/src/rename-file.flow.js +43 -0
- package/src/rename.form.js +5 -0
- package/src/rename.schema.js +9 -0
- package/src/resource-list-query.js +182 -0
- package/src/resource-list-query.spec.js +75 -0
- package/src/resource-selection.js +25 -0
- package/src/resource-selection.spec.js +40 -0
- package/src/resource.helpers.js +8 -1
- package/src/resources-list-options.js +169 -0
- package/src/resources-list-options.spec.js +141 -0
- package/src/resources.action-media.js +6 -0
- package/src/resources.queries.js +91 -6
- package/src/schema-field-display.js +31 -0
- package/src/schema-field-display.spec.js +37 -0
- package/src/search-payload.js +10 -0
- package/src/search-payload.spec.js +20 -0
- package/src/search.task.js +2 -1
- package/src/share-resource.flow.js +59 -0
- package/src/sort-ascending.action.js +5 -0
- package/src/sort-by-created.action.js +5 -0
- package/src/sort-by-last-updated.action.js +5 -0
- package/src/sort-by-name.action.js +5 -0
- package/src/sort-by-size.action.js +5 -0
- package/src/sort-by-type.action.js +5 -0
- package/src/sort-descending.action.js +5 -0
- package/src/sort-storage-created.flow.js +87 -0
- package/src/sort-storage-last-updated.flow.js +119 -0
- package/src/sort-storage-name-asc.flow.js +34 -0
- package/src/sort-storage-size-desc.flow.js +34 -0
- package/src/sort-storage-size.flow.js +87 -0
- package/src/sort-storage-type.flow.js +83 -0
- package/src/sort-storage.flow.js +86 -0
- package/src/sv.translations.json +18 -0
- package/src/toggle-selection-mode.action.js +4 -0
- package/src/upload-file.flow.js +44 -0
- package/src/useResourceList.js +94 -0
- package/src/view-grid.action.js +5 -0
- package/src/view-list.action.js +5 -0
- package/src/view-storage.flow.js +49 -0
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { describe, expect, it } from '@jest/globals'
|
|
2
|
+
import {
|
|
3
|
+
DEFAULT_RESOURCE_LIST_LIMIT,
|
|
4
|
+
extractResourceListOptions,
|
|
5
|
+
mergeResourceListParams,
|
|
6
|
+
mongoSortForResourceList,
|
|
7
|
+
paginateResources,
|
|
8
|
+
sortResources,
|
|
9
|
+
} from './resources-list-options.js'
|
|
10
|
+
|
|
11
|
+
describe('extractResourceListOptions', () => {
|
|
12
|
+
it('leaves filter fields and strips list controls', () => {
|
|
13
|
+
const { filter, limit, offset, sortBy, sortOrder, paginated } = extractResourceListOptions({
|
|
14
|
+
location: '/docs/',
|
|
15
|
+
belongsTo: 'ws-1',
|
|
16
|
+
limit: '25',
|
|
17
|
+
offset: '50',
|
|
18
|
+
sortBy: 'created',
|
|
19
|
+
sortOrder: 'desc',
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
expect(filter).toEqual({ location: '/docs/', belongsTo: 'ws-1' })
|
|
23
|
+
expect(limit).toBe(25)
|
|
24
|
+
expect(offset).toBe(50)
|
|
25
|
+
expect(sortBy).toBe('created')
|
|
26
|
+
expect(sortOrder).toBe('desc')
|
|
27
|
+
expect(paginated).toBe(true)
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('parses sort shorthand and clamps limit', () => {
|
|
31
|
+
const options = extractResourceListOptions({
|
|
32
|
+
sort: 'size:desc',
|
|
33
|
+
limit: 9999,
|
|
34
|
+
offset: -10,
|
|
35
|
+
})
|
|
36
|
+
expect(options.sortBy).toBe('size')
|
|
37
|
+
expect(options.sortOrder).toBe('desc')
|
|
38
|
+
expect(options.limit).toBe(200)
|
|
39
|
+
expect(options.offset).toBe(0)
|
|
40
|
+
expect(options.paginated).toBe(true)
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('defaults sortBy to name when paginating without sort', () => {
|
|
44
|
+
const options = extractResourceListOptions({ limit: 10 })
|
|
45
|
+
expect(options.sortBy).toBe('name')
|
|
46
|
+
expect(options.sortOrder).toBe('asc')
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('keeps sortBy null when neither sort nor pagination is requested', () => {
|
|
50
|
+
const options = extractResourceListOptions({ location: '/' })
|
|
51
|
+
expect(options.sortBy).toBeNull()
|
|
52
|
+
expect(options.paginated).toBe(false)
|
|
53
|
+
expect(options.limit).toBeNull()
|
|
54
|
+
})
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
describe('mergeResourceListParams', () => {
|
|
58
|
+
it('prefers top-level payload list controls over nested query', () => {
|
|
59
|
+
const merged = mergeResourceListParams(
|
|
60
|
+
{
|
|
61
|
+
query: { location: '/a/', limit: 10 },
|
|
62
|
+
location: '/b/',
|
|
63
|
+
limit: 20,
|
|
64
|
+
sortBy: 'name',
|
|
65
|
+
},
|
|
66
|
+
{ offset: 5 },
|
|
67
|
+
)
|
|
68
|
+
expect(merged).toEqual({
|
|
69
|
+
location: '/b/',
|
|
70
|
+
limit: 20,
|
|
71
|
+
sortBy: 'name',
|
|
72
|
+
offset: 5,
|
|
73
|
+
})
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('keeps nested query location when top-level location is absent', () => {
|
|
77
|
+
const merged = mergeResourceListParams(
|
|
78
|
+
{ query: { location: '/nested/' } },
|
|
79
|
+
{ location: '/from-req/', limit: 10 },
|
|
80
|
+
)
|
|
81
|
+
expect(merged.location).toBe('/nested/')
|
|
82
|
+
expect(merged.limit).toBe(10)
|
|
83
|
+
})
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
describe('sortResources', () => {
|
|
87
|
+
const resources = [
|
|
88
|
+
{ id: 'f2', name: 'zeta.txt', type: '@ossy/platform/schema/file', created: 2, content: { ContentLength: 10 } },
|
|
89
|
+
{ id: 'd1', name: 'Beta', type: '@ossy/platform/schema/directory', created: 3 },
|
|
90
|
+
{ id: 'f1', name: 'alpha.txt', type: '@ossy/platform/schema/file', created: 1, content: { ContentLength: 30 } },
|
|
91
|
+
{ id: 'd2', name: 'Alpha', type: 'directory', created: 4 },
|
|
92
|
+
]
|
|
93
|
+
|
|
94
|
+
it('keeps directories first and sorts by name ascending', () => {
|
|
95
|
+
expect(sortResources(resources).map(r => r.id)).toEqual(['d2', 'd1', 'f1', 'f2'])
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('sorts files by size descending while keeping directories first', () => {
|
|
99
|
+
expect(sortResources(resources, { sortBy: 'size', sortOrder: 'desc' }).map(r => r.id))
|
|
100
|
+
.toEqual(['d2', 'd1', 'f1', 'f2'])
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it('sorts by created ascending', () => {
|
|
104
|
+
expect(sortResources(resources, { sortBy: 'created', sortOrder: 'asc' }).map(r => r.id))
|
|
105
|
+
.toEqual(['d1', 'd2', 'f1', 'f2'])
|
|
106
|
+
})
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
describe('paginateResources', () => {
|
|
110
|
+
it('slices with total metadata', () => {
|
|
111
|
+
const items = Array.from({ length: 5 }, (_, i) => ({ id: String(i) }))
|
|
112
|
+
expect(paginateResources(items, { limit: 2, offset: 2 })).toEqual({
|
|
113
|
+
items: [{ id: '2' }, { id: '3' }],
|
|
114
|
+
total: 5,
|
|
115
|
+
limit: 2,
|
|
116
|
+
offset: 2,
|
|
117
|
+
})
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
it('falls back to default page size', () => {
|
|
121
|
+
const items = Array.from({ length: DEFAULT_RESOURCE_LIST_LIMIT + 5 }, (_, i) => ({ id: String(i) }))
|
|
122
|
+
const page = paginateResources(items)
|
|
123
|
+
expect(page.limit).toBe(DEFAULT_RESOURCE_LIST_LIMIT)
|
|
124
|
+
expect(page.items).toHaveLength(DEFAULT_RESOURCE_LIST_LIMIT)
|
|
125
|
+
expect(page.total).toBe(DEFAULT_RESOURCE_LIST_LIMIT + 5)
|
|
126
|
+
})
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
describe('mongoSortForResourceList', () => {
|
|
130
|
+
it('builds directory-first mongo sort', () => {
|
|
131
|
+
expect(mongoSortForResourceList({ sortBy: 'name', sortOrder: 'asc' })).toEqual({
|
|
132
|
+
_isDirectory: 1,
|
|
133
|
+
'state.name': 1,
|
|
134
|
+
})
|
|
135
|
+
expect(mongoSortForResourceList({ sortBy: 'size', sortOrder: 'desc' })).toEqual({
|
|
136
|
+
_isDirectory: 1,
|
|
137
|
+
'state.content.ContentLength': -1,
|
|
138
|
+
'state.name': 1,
|
|
139
|
+
})
|
|
140
|
+
})
|
|
141
|
+
})
|
|
@@ -26,6 +26,12 @@ export async function withResourceMedia(result, context) {
|
|
|
26
26
|
if (Array.isArray(result)) {
|
|
27
27
|
return Promise.all(result.map(r => attachOrKeep(r, context)))
|
|
28
28
|
}
|
|
29
|
+
if (result && typeof result === 'object' && Array.isArray(result.items)) {
|
|
30
|
+
return {
|
|
31
|
+
...result,
|
|
32
|
+
items: await Promise.all(result.items.map(r => attachOrKeep(r, context))),
|
|
33
|
+
}
|
|
34
|
+
}
|
|
29
35
|
if (result && typeof result === 'object' && result.id) {
|
|
30
36
|
return attachOrKeep(result, context)
|
|
31
37
|
}
|
package/src/resources.queries.js
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { Aggregate } from '@ossy/event-store'
|
|
2
2
|
import { createLogger } from '@ossy/observability'
|
|
3
|
+
import {
|
|
4
|
+
extractResourceListOptions,
|
|
5
|
+
isDirectoryResourceType,
|
|
6
|
+
mongoSortForResourceList,
|
|
7
|
+
sortResources,
|
|
8
|
+
} from './resources-list-options.js'
|
|
3
9
|
|
|
4
10
|
const log = createLogger('resources')
|
|
5
11
|
|
|
@@ -79,16 +85,95 @@ export class ResourcesQueries {
|
|
|
79
85
|
}
|
|
80
86
|
|
|
81
87
|
static GetResources(_query = {}, user = null) {
|
|
88
|
+
const {
|
|
89
|
+
filter,
|
|
90
|
+
sortBy,
|
|
91
|
+
sortOrder,
|
|
92
|
+
limit,
|
|
93
|
+
offset,
|
|
94
|
+
paginated,
|
|
95
|
+
} = extractResourceListOptions(_query)
|
|
96
|
+
|
|
82
97
|
log.info('[ResourcesQueries][GetResources] Building query')
|
|
83
|
-
const mongoQuery = ResourcesQueries.buildMongoQuery(
|
|
84
|
-
const workspaceScope =
|
|
98
|
+
const mongoQuery = ResourcesQueries.buildMongoQuery(filter, user)
|
|
99
|
+
const workspaceScope = filter.belongsTo ?? '(no workspace scope)'
|
|
85
100
|
log.info(`[ResourcesQueries][GetResources] Fetching resources for ${workspaceScope}`)
|
|
86
101
|
|
|
87
|
-
|
|
88
|
-
.
|
|
89
|
-
|
|
90
|
-
|
|
102
|
+
if (!paginated && !sortBy) {
|
|
103
|
+
return Aggregate.Collection.find(mongoQuery, { projection: { state: true } }).toArray()
|
|
104
|
+
.then(resources => {
|
|
105
|
+
log.info(`[ResourcesQueries][GetResources] Found ${resources.length} resources`)
|
|
106
|
+
return resources.map(resource => resource.state)
|
|
107
|
+
})
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const sort = mongoSortForResourceList({ sortBy: sortBy ?? 'name', sortOrder })
|
|
111
|
+
|
|
112
|
+
// Mongo find projections cannot compute fields; use aggregation when sorting/paging.
|
|
113
|
+
const pipeline = [
|
|
114
|
+
{ $match: mongoQuery },
|
|
115
|
+
{
|
|
116
|
+
$addFields: {
|
|
117
|
+
_isDirectory: {
|
|
118
|
+
$cond: [
|
|
119
|
+
{
|
|
120
|
+
$in: [
|
|
121
|
+
'$state.type',
|
|
122
|
+
['directory', '@ossy/platform/schema/directory'],
|
|
123
|
+
],
|
|
124
|
+
},
|
|
125
|
+
0,
|
|
126
|
+
1,
|
|
127
|
+
],
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
{ $sort: sort },
|
|
132
|
+
]
|
|
133
|
+
|
|
134
|
+
if (paginated) {
|
|
135
|
+
pipeline.push({ $skip: offset })
|
|
136
|
+
pipeline.push({ $limit: limit })
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
pipeline.push({ $project: { state: true } })
|
|
140
|
+
|
|
141
|
+
const itemsPromise = Aggregate.Collection.aggregate(pipeline).toArray()
|
|
142
|
+
.then(resources => resources.map(resource => resource.state))
|
|
143
|
+
|
|
144
|
+
if (!paginated) {
|
|
145
|
+
return itemsPromise.then(items => {
|
|
146
|
+
log.info(`[ResourcesQueries][GetResources] Found ${items.length} resources`)
|
|
147
|
+
return items
|
|
91
148
|
})
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return Promise.all([
|
|
152
|
+
itemsPromise,
|
|
153
|
+
Aggregate.Collection.countDocuments(mongoQuery),
|
|
154
|
+
]).then(([items, total]) => {
|
|
155
|
+
log.info(`[ResourcesQueries][GetResources] Found ${items.length}/${total} resources (limit=${limit}, offset=${offset})`)
|
|
156
|
+
return {
|
|
157
|
+
items,
|
|
158
|
+
total,
|
|
159
|
+
limit,
|
|
160
|
+
offset,
|
|
161
|
+
sortBy: sortBy ?? 'name',
|
|
162
|
+
sortOrder,
|
|
163
|
+
}
|
|
164
|
+
})
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* In-memory folder-first sort for callers that already hold a resource array.
|
|
169
|
+
* Prefer GetResources sort options for large collections.
|
|
170
|
+
*/
|
|
171
|
+
static SortResources(resources, options) {
|
|
172
|
+
return sortResources(resources, options)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
static IsDirectoryType(type) {
|
|
176
|
+
return isDirectoryResourceType(type)
|
|
92
177
|
}
|
|
93
178
|
|
|
94
179
|
static GetNestedResources({ location }) {
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
const TEMPORAL_FIELD_TYPES = new Set(['date', 'month', 'timestamp'])
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Whether a schema field value should render as Markdown in detail views.
|
|
3
5
|
*
|
|
@@ -10,3 +12,32 @@ export function shouldRenderFieldAsMarkdown (field, value) {
|
|
|
10
12
|
&& typeof value === 'string'
|
|
11
13
|
&& value.trim().length > 0
|
|
12
14
|
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @param {{ type?: string } | null | undefined} field
|
|
18
|
+
*/
|
|
19
|
+
export function isTemporalField (field) {
|
|
20
|
+
return TEMPORAL_FIELD_TYPES.has(field?.type)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Localized label for date / month / timestamp schema values (Unix ms).
|
|
25
|
+
*
|
|
26
|
+
* @param {unknown} value
|
|
27
|
+
* @param {{ type?: string } | null | undefined} field
|
|
28
|
+
* @param {string} [locale]
|
|
29
|
+
* @returns {string | null}
|
|
30
|
+
*/
|
|
31
|
+
export function formatTemporalFieldValue (value, field, locale = 'en-GB') {
|
|
32
|
+
if (value == null || value === '') return null
|
|
33
|
+
const ms = typeof value === 'number' ? value : Number(value)
|
|
34
|
+
if (!Number.isFinite(ms)) return null
|
|
35
|
+
|
|
36
|
+
const options = field?.type === 'month'
|
|
37
|
+
? { year: 'numeric', month: 'long' }
|
|
38
|
+
: field?.type === 'timestamp'
|
|
39
|
+
? { dateStyle: 'medium', timeStyle: 'short' }
|
|
40
|
+
: { dateStyle: 'medium' }
|
|
41
|
+
|
|
42
|
+
return new Intl.DateTimeFormat(locale, options).format(new Date(ms))
|
|
43
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { describe, expect, it } from '@jest/globals'
|
|
2
|
+
import {
|
|
3
|
+
formatTemporalFieldValue,
|
|
4
|
+
isTemporalField,
|
|
5
|
+
} from './schema-field-display.js'
|
|
6
|
+
|
|
7
|
+
describe('isTemporalField', () => {
|
|
8
|
+
it('matches date, month, and timestamp', () => {
|
|
9
|
+
expect(isTemporalField({ type: 'date' })).toBe(true)
|
|
10
|
+
expect(isTemporalField({ type: 'month' })).toBe(true)
|
|
11
|
+
expect(isTemporalField({ type: 'timestamp' })).toBe(true)
|
|
12
|
+
expect(isTemporalField({ type: 'number' })).toBe(false)
|
|
13
|
+
expect(isTemporalField({})).toBe(false)
|
|
14
|
+
})
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
describe('formatTemporalFieldValue', () => {
|
|
18
|
+
const ms = Date.UTC(2020, 7, 8)
|
|
19
|
+
|
|
20
|
+
it('formats date fields as a calendar day', () => {
|
|
21
|
+
const label = formatTemporalFieldValue(ms, { type: 'date' }, 'en-GB')
|
|
22
|
+
expect(label).toContain('2020')
|
|
23
|
+
expect(label).not.toMatch(/1596844800000/)
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('formats month fields as month and year', () => {
|
|
27
|
+
const label = formatTemporalFieldValue(ms, { type: 'month' }, 'en-GB')
|
|
28
|
+
expect(label.toLowerCase()).toContain('august')
|
|
29
|
+
expect(label).toContain('2020')
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('returns em-dash fallback as null for empty values', () => {
|
|
33
|
+
expect(formatTemporalFieldValue(null, { type: 'date' })).toBe(null)
|
|
34
|
+
expect(formatTemporalFieldValue('', { type: 'date' })).toBe(null)
|
|
35
|
+
expect(formatTemporalFieldValue('not-a-date', { type: 'date' })).toBe(null)
|
|
36
|
+
})
|
|
37
|
+
})
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { extractResourceListOptions } from './resources-list-options.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Strip pagination/sort keys from search payloads so callers that accidentally
|
|
5
|
+
* forward list options keep receiving an array (not `{ items, total }`).
|
|
6
|
+
*/
|
|
7
|
+
export function sanitizeSearchPayload (payload = {}) {
|
|
8
|
+
const { filter } = extractResourceListOptions(payload ?? {})
|
|
9
|
+
return filter
|
|
10
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { describe, it, expect } from '@jest/globals'
|
|
2
|
+
import { sanitizeSearchPayload } from './search-payload.js'
|
|
3
|
+
|
|
4
|
+
describe('sanitizeSearchPayload', () => {
|
|
5
|
+
it('strips list options so search stays an array-shaped query', () => {
|
|
6
|
+
const result = sanitizeSearchPayload({
|
|
7
|
+
location: '/docs/',
|
|
8
|
+
limit: 10,
|
|
9
|
+
offset: 20,
|
|
10
|
+
sortBy: 'name',
|
|
11
|
+
sortOrder: 'desc',
|
|
12
|
+
q: 'brief',
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
expect(result).toEqual({
|
|
16
|
+
location: '/docs/',
|
|
17
|
+
q: 'brief',
|
|
18
|
+
})
|
|
19
|
+
})
|
|
20
|
+
})
|
package/src/search.task.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { ResourcesQueries } from './resources.queries.js'
|
|
2
2
|
import { mediaContextFromRun, withResourceMedia } from './resources.action-media.js'
|
|
3
|
+
import { sanitizeSearchPayload } from './search-payload.js'
|
|
3
4
|
|
|
4
5
|
export const metadata = { id: '@ossy/resources/tasks/search' }
|
|
5
6
|
|
|
6
7
|
export async function run({ payload, req }) {
|
|
7
|
-
const query =
|
|
8
|
+
const query = sanitizeSearchPayload(payload ?? {})
|
|
8
9
|
const workspaceId = payload?.workspaceId ?? req?.workspaceId
|
|
9
10
|
const user = payload?.user ?? req?.user
|
|
10
11
|
let location = query.location
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import uploadFileFlow from './upload-file.flow.js'
|
|
2
|
+
import { metadata as OpenShare } from './open-share.action.js'
|
|
3
|
+
import { metadata as MakePublic } from './make-public.action.js'
|
|
4
|
+
import { metadata as CopyShareLink } from './copy-share-link.action.js'
|
|
5
|
+
import { metadata as CloseShare } from './close-share.action.js'
|
|
6
|
+
|
|
7
|
+
export const metadata = {
|
|
8
|
+
id: '@ossy/resources/flows/share-resource',
|
|
9
|
+
feature: 'resources',
|
|
10
|
+
requires: ['server', 'database'],
|
|
11
|
+
timeout: 150_000,
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* After uploading a file, open the share dialog from the resource panel,
|
|
16
|
+
* make the file public, see the copy-link control plus public access marker,
|
|
17
|
+
* then close the share dialog.
|
|
18
|
+
*/
|
|
19
|
+
export default {
|
|
20
|
+
title: 'Share uploaded resource',
|
|
21
|
+
description:
|
|
22
|
+
'Signed-in user uploads a file, opens share from the panel, makes it public, sees the share link controls, then closes the dialog',
|
|
23
|
+
steps: [
|
|
24
|
+
...uploadFileFlow.steps,
|
|
25
|
+
{ click: { selector: '[data-resource-name="e2e-upload.txt"]' } },
|
|
26
|
+
{ result: { action: OpenShare, timeout: 15000 } },
|
|
27
|
+
{ action: OpenShare },
|
|
28
|
+
{
|
|
29
|
+
result: {
|
|
30
|
+
selector: '[data-region="resource-share"][data-share-status="private"]',
|
|
31
|
+
timeout: 15000,
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
{ result: { action: MakePublic, timeout: 10000 } },
|
|
35
|
+
{ action: MakePublic },
|
|
36
|
+
{
|
|
37
|
+
result: {
|
|
38
|
+
selector: '[data-region="resource-share"][data-share-status="public"]',
|
|
39
|
+
timeout: 20000,
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
{ result: { action: CopyShareLink, timeout: 10000 } },
|
|
43
|
+
{
|
|
44
|
+
result: {
|
|
45
|
+
selector: '[data-resource-name="e2e-upload.txt"][data-resource-access="public"]',
|
|
46
|
+
timeout: 20000,
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
{ result: { action: CloseShare, timeout: 10000 } },
|
|
50
|
+
{ action: CloseShare },
|
|
51
|
+
{
|
|
52
|
+
result: {
|
|
53
|
+
selector: '[data-region="resource-share"]',
|
|
54
|
+
hidden: true,
|
|
55
|
+
timeout: 10000,
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
],
|
|
59
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import signUpFlow from '@ossy/authentication/sign-up.flow.js'
|
|
2
|
+
import { metadata as ConfirmUpload } from './confirm-upload.action.js'
|
|
3
|
+
import { metadata as FinishUpload } from './finish-upload.action.js'
|
|
4
|
+
import { metadata as OpenSort } from './open-sort.action.js'
|
|
5
|
+
import { metadata as SortByCreated } from './sort-by-created.action.js'
|
|
6
|
+
|
|
7
|
+
export const metadata = {
|
|
8
|
+
id: '@ossy/resources/flows/sort-storage-created',
|
|
9
|
+
feature: 'resources',
|
|
10
|
+
requires: ['server', 'database'],
|
|
11
|
+
timeout: 180_000,
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Upload two files whose name order is opposite their created order, open the
|
|
16
|
+
* storage sort menu, switch to created ascending, and assert list order + markers.
|
|
17
|
+
*/
|
|
18
|
+
export default {
|
|
19
|
+
title: 'Sort storage resources by created',
|
|
20
|
+
description:
|
|
21
|
+
'Signed-in user uploads two files, opens sort, chooses created, and sees created ascending order',
|
|
22
|
+
steps: [
|
|
23
|
+
...signUpFlow.steps,
|
|
24
|
+
{ page: '@upload-media', search: { location: '/' } },
|
|
25
|
+
{ result: { selector: '#upload-resources', timeout: 15000 } },
|
|
26
|
+
{ result: { selector: '[data-flow-stage="upload-idle"]', timeout: 15000 } },
|
|
27
|
+
{
|
|
28
|
+
files: {
|
|
29
|
+
selector: '#upload-resources',
|
|
30
|
+
items: [{
|
|
31
|
+
name: 'e2e-created-z.txt',
|
|
32
|
+
mimeType: 'text/plain',
|
|
33
|
+
// Uploaded first so created-asc places this before the later "a" file.
|
|
34
|
+
content: 'created first z',
|
|
35
|
+
}],
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
{ result: { selector: '[data-flow-stage="upload-preview"]', timeout: 15000 } },
|
|
39
|
+
{ result: { action: ConfirmUpload, timeout: 10000 } },
|
|
40
|
+
{ action: ConfirmUpload },
|
|
41
|
+
{ result: { selector: '[data-flow-stage="upload-done"]', timeout: 30000 } },
|
|
42
|
+
{ result: { action: FinishUpload, timeout: 10000 } },
|
|
43
|
+
{ action: FinishUpload },
|
|
44
|
+
{ result: { page: '@storage/home', timeout: 20000 } },
|
|
45
|
+
{ result: { selector: '[data-resource-name="e2e-created-z.txt"]', timeout: 20000 } },
|
|
46
|
+
{ page: '@upload-media', search: { location: '/' } },
|
|
47
|
+
{ result: { selector: '#upload-resources', timeout: 15000 } },
|
|
48
|
+
{ result: { selector: '[data-flow-stage="upload-idle"]', timeout: 15000 } },
|
|
49
|
+
{
|
|
50
|
+
files: {
|
|
51
|
+
selector: '#upload-resources',
|
|
52
|
+
items: [{
|
|
53
|
+
name: 'e2e-created-a.txt',
|
|
54
|
+
mimeType: 'text/plain',
|
|
55
|
+
content: 'created second a',
|
|
56
|
+
}],
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
{ result: { selector: '[data-flow-stage="upload-preview"]', timeout: 15000 } },
|
|
60
|
+
{ result: { action: ConfirmUpload, timeout: 10000 } },
|
|
61
|
+
{ action: ConfirmUpload },
|
|
62
|
+
{ result: { selector: '[data-flow-stage="upload-done"]', timeout: 30000 } },
|
|
63
|
+
{ result: { action: FinishUpload, timeout: 10000 } },
|
|
64
|
+
{ action: FinishUpload },
|
|
65
|
+
{ result: { page: '@storage/home', timeout: 20000 } },
|
|
66
|
+
{ result: { selector: '[data-resource-name="e2e-created-a.txt"]', timeout: 20000 } },
|
|
67
|
+
{ result: { selector: '[data-resource-name="e2e-created-z.txt"]', timeout: 20000 } },
|
|
68
|
+
{ result: { selector: '[data-sort-by="name"][data-sort-order="asc"]', timeout: 10000 } },
|
|
69
|
+
{
|
|
70
|
+
result: {
|
|
71
|
+
selector: '[data-resource-name="e2e-created-a.txt"] ~ [data-resource-name="e2e-created-z.txt"]',
|
|
72
|
+
timeout: 10000,
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
{ result: { action: OpenSort, timeout: 10000 } },
|
|
76
|
+
{ action: OpenSort },
|
|
77
|
+
{ result: { action: SortByCreated, timeout: 10000 } },
|
|
78
|
+
{ action: SortByCreated },
|
|
79
|
+
{ result: { selector: '[data-sort-by="created"][data-sort-order="asc"]', timeout: 10000 } },
|
|
80
|
+
{
|
|
81
|
+
result: {
|
|
82
|
+
selector: '[data-resource-name="e2e-created-z.txt"] ~ [data-resource-name="e2e-created-a.txt"]',
|
|
83
|
+
timeout: 10000,
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
],
|
|
87
|
+
}
|