@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.
Files changed (89) hide show
  1. package/package.json +4 -4
  2. package/src/CreateDirectory.jsx +47 -53
  3. package/src/CreateDocument.jsx +1 -0
  4. package/src/GenericResourceForm.jsx +49 -31
  5. package/src/ResourceDialogMove.jsx +99 -50
  6. package/src/ResourceGrid.jsx +36 -84
  7. package/src/ResourceList.jsx +10 -1
  8. package/src/ResourcePanel.jsx +96 -17
  9. package/src/ResourceShareDialog.jsx +50 -12
  10. package/src/SchemaDetailView.jsx +15 -3
  11. package/src/Upload.jsx +25 -8
  12. package/src/batch-delete.flow.js +81 -0
  13. package/src/batch-download.flow.js +69 -0
  14. package/src/cancel-upload.action.js +4 -0
  15. package/src/cancel-upload.flow.js +40 -0
  16. package/src/close-share.action.js +4 -0
  17. package/src/confirm-upload.action.js +4 -0
  18. package/src/copy-share-link.action.js +4 -0
  19. package/src/create-directory.action.js +5 -0
  20. package/src/create-directory.flow.js +30 -0
  21. package/src/create-directory.form.js +5 -0
  22. package/src/create-directory.schema.js +9 -0
  23. package/src/create-document.flow.js +36 -0
  24. package/src/create-document.form.js +5 -0
  25. package/src/create-document.schema.js +11 -0
  26. package/src/create.action.js +5 -1
  27. package/src/delete-directory.flow.js +39 -0
  28. package/src/delete-file.flow.js +32 -0
  29. package/src/done-selecting.action.js +5 -0
  30. package/src/download-file.flow.js +31 -0
  31. package/src/download-resource.action.js +4 -0
  32. package/src/download-selected.action.js +4 -0
  33. package/src/en.translations.json +18 -0
  34. package/src/finish-upload.action.js +4 -0
  35. package/src/index.js +42 -0
  36. package/src/list.task.js +17 -6
  37. package/src/list.task.spec.js +81 -0
  38. package/src/make-private-resource.flow.js +72 -0
  39. package/src/make-private.action.js +4 -0
  40. package/src/make-public.action.js +4 -0
  41. package/src/move-file.flow.js +78 -0
  42. package/src/move.form.js +5 -0
  43. package/src/move.schema.js +9 -0
  44. package/src/open-delete-directory.action.js +7 -0
  45. package/src/open-move.action.js +4 -0
  46. package/src/open-remove-selected.action.js +5 -0
  47. package/src/open-rename.action.js +4 -0
  48. package/src/open-share.action.js +4 -0
  49. package/src/open-sort.action.js +5 -0
  50. package/src/open-view.action.js +5 -0
  51. package/src/rename-file.flow.js +43 -0
  52. package/src/rename.form.js +5 -0
  53. package/src/rename.schema.js +9 -0
  54. package/src/resource-list-query.js +182 -0
  55. package/src/resource-list-query.spec.js +75 -0
  56. package/src/resource-selection.js +25 -0
  57. package/src/resource-selection.spec.js +40 -0
  58. package/src/resource.helpers.js +8 -1
  59. package/src/resources-list-options.js +169 -0
  60. package/src/resources-list-options.spec.js +141 -0
  61. package/src/resources.action-media.js +6 -0
  62. package/src/resources.queries.js +91 -6
  63. package/src/schema-field-display.js +31 -0
  64. package/src/schema-field-display.spec.js +37 -0
  65. package/src/search-payload.js +10 -0
  66. package/src/search-payload.spec.js +20 -0
  67. package/src/search.task.js +2 -1
  68. package/src/share-resource.flow.js +59 -0
  69. package/src/sort-ascending.action.js +5 -0
  70. package/src/sort-by-created.action.js +5 -0
  71. package/src/sort-by-last-updated.action.js +5 -0
  72. package/src/sort-by-name.action.js +5 -0
  73. package/src/sort-by-size.action.js +5 -0
  74. package/src/sort-by-type.action.js +5 -0
  75. package/src/sort-descending.action.js +5 -0
  76. package/src/sort-storage-created.flow.js +87 -0
  77. package/src/sort-storage-last-updated.flow.js +119 -0
  78. package/src/sort-storage-name-asc.flow.js +34 -0
  79. package/src/sort-storage-size-desc.flow.js +34 -0
  80. package/src/sort-storage-size.flow.js +87 -0
  81. package/src/sort-storage-type.flow.js +83 -0
  82. package/src/sort-storage.flow.js +86 -0
  83. package/src/sv.translations.json +18 -0
  84. package/src/toggle-selection-mode.action.js +4 -0
  85. package/src/upload-file.flow.js +44 -0
  86. package/src/useResourceList.js +94 -0
  87. package/src/view-grid.action.js +5 -0
  88. package/src/view-list.action.js +5 -0
  89. 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
  }
@@ -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(_query, user)
84
- const workspaceScope = _query.belongsTo ?? '(no workspace scope)'
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
- return Aggregate.Collection.find(mongoQuery, { state: true }).toArray()
88
- .then(resources => {
89
- log.info(`[ResourcesQueries][GetResources] Found ${resources.length} resources`)
90
- return resources.map(resource => resource.state)
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
+ })
@@ -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 = { ...(payload ?? {}) }
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,5 @@
1
+ /** Client-only — sort storage resources ascending. */
2
+ export const metadata = {
3
+ id: '@ossy/resources/actions/sort-ascending',
4
+ access: 'workspace',
5
+ }
@@ -0,0 +1,5 @@
1
+ /** Client-only — sort storage resources by created time. */
2
+ export const metadata = {
3
+ id: '@ossy/resources/actions/sort-by-created',
4
+ access: 'workspace',
5
+ }
@@ -0,0 +1,5 @@
1
+ /** Client-only — sort storage resources by last updated time. */
2
+ export const metadata = {
3
+ id: '@ossy/resources/actions/sort-by-last-updated',
4
+ access: 'workspace',
5
+ }
@@ -0,0 +1,5 @@
1
+ /** Client-only — sort storage resources by name. */
2
+ export const metadata = {
3
+ id: '@ossy/resources/actions/sort-by-name',
4
+ access: 'workspace',
5
+ }
@@ -0,0 +1,5 @@
1
+ /** Client-only — sort storage resources by size. */
2
+ export const metadata = {
3
+ id: '@ossy/resources/actions/sort-by-size',
4
+ access: 'workspace',
5
+ }
@@ -0,0 +1,5 @@
1
+ /** Client-only — sort storage resources by type. */
2
+ export const metadata = {
3
+ id: '@ossy/resources/actions/sort-by-type',
4
+ access: 'workspace',
5
+ }
@@ -0,0 +1,5 @@
1
+ /** Client-only — sort storage resources descending. */
2
+ export const metadata = {
3
+ id: '@ossy/resources/actions/sort-descending',
4
+ access: 'workspace',
5
+ }
@@ -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
+ }