sanity-plugin-media 4.1.1 → 4.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/LICENSE +1 -1
- package/README.md +56 -4
- package/dist/index.d.mts +131 -57
- package/dist/index.d.ts +131 -57
- package/dist/index.js +259 -98
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +259 -98
- package/dist/index.mjs.map +1 -1
- package/package.json +9 -2
- package/src/__tests__/fixtures/createEpicTestStore.ts +27 -0
- package/src/__tests__/fixtures/listenMock.ts +9 -0
- package/src/__tests__/fixtures/mockSanityClient.ts +84 -0
- package/src/__tests__/fixtures/renderWithProviders.tsx +54 -0
- package/src/__tests__/fixtures/rootState.ts +27 -0
- package/src/__tests__/fixtures/withinDialog.ts +28 -0
- package/src/components/Browser/Browser.test.tsx +44 -0
- package/src/components/CardAsset/CardAsset.test.tsx +322 -0
- package/src/components/DialogAssetEdit/Details.tsx +123 -44
- package/src/components/DialogAssetEdit/DialogAssetEdit.test.tsx +215 -0
- package/src/components/DialogAssetEdit/index.tsx +138 -30
- package/src/components/DialogTagCreate/DialogTagCreate.test.tsx +120 -0
- package/src/components/DialogTagEdit/DialogTagEdit.test.tsx +164 -0
- package/src/components/FormBuilderTool/FormBuilderTool.test.tsx +62 -0
- package/src/components/UploadDropzone/UploadDropzone.test.tsx +39 -0
- package/src/contexts/ToolOptionsContext.tsx +6 -3
- package/src/formSchema/index.test.ts +55 -0
- package/src/formSchema/index.ts +28 -12
- package/src/hooks/useVersionedClient.ts +1 -1
- package/src/modules/assets/deleteAndUpdateEpics.test.ts +86 -0
- package/src/modules/assets/fetchEpic.test.ts +72 -0
- package/src/modules/assets/reducer.test.ts +90 -0
- package/src/modules/assets/tagsAndListenerEpics.test.ts +205 -0
- package/src/modules/dialog/epics.test.ts +167 -0
- package/src/modules/dialog/reducer.test.ts +184 -0
- package/src/modules/notifications/epics.test.ts +373 -0
- package/src/modules/notifications/index.ts +24 -4
- package/src/modules/notifications/reducer.test.ts +53 -0
- package/src/modules/search/index.test.ts +35 -0
- package/src/modules/selectors.test.ts +20 -0
- package/src/modules/tags/epics.test.ts +95 -0
- package/src/modules/tags/index.test.ts +41 -0
- package/src/modules/uploads/epics.test.ts +108 -0
- package/src/modules/uploads/index.test.ts +58 -0
- package/src/operators/checkTagName.test.ts +28 -0
- package/src/types/index.ts +20 -7
- package/src/utils/blocksToText.test.ts +42 -0
- package/src/utils/constructFilter.test.ts +119 -0
- package/src/utils/generatePreviewBlobUrl.test.ts +69 -0
- package/src/utils/getAssetResolution.test.ts +12 -0
- package/src/utils/getDocumentAssetIds.test.ts +49 -0
- package/src/utils/getSchemeColor.test.ts +11 -0
- package/src/utils/getTagSelectOptions.test.ts +43 -0
- package/src/utils/getUniqueDocuments.test.ts +25 -0
- package/src/utils/imageDprUrl.test.ts +45 -0
- package/src/utils/isSupportedAssetType.test.ts +15 -0
- package/src/utils/sanitizeFormData.test.ts +58 -0
- package/src/utils/typeGuards.test.ts +17 -0
- package/src/utils/uploadSanityAsset.test.ts +28 -0
- package/src/utils/withMaxConcurrency.test.ts +42 -0
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
// @vitest-environment node
|
|
2
|
+
|
|
3
|
+
import {describe, expect, it, vi} from 'vitest'
|
|
4
|
+
import {
|
|
5
|
+
notificationsAssetsDeleteCompleteEpic,
|
|
6
|
+
notificationsAssetsDeleteErrorEpic,
|
|
7
|
+
notificationsAssetsTagsAddCompleteEpic,
|
|
8
|
+
notificationsAssetsTagsRemoveCompleteEpic,
|
|
9
|
+
notificationsAssetsUpdateCompleteEpic,
|
|
10
|
+
notificationsAssetsUploadCompleteEpic,
|
|
11
|
+
notificationsGenericErrorEpic,
|
|
12
|
+
notificationsTagCreateCompleteEpic,
|
|
13
|
+
notificationsTagDeleteCompleteEpic,
|
|
14
|
+
notificationsTagUpdateCompleteEpic
|
|
15
|
+
} from './index'
|
|
16
|
+
import {assetsActions, initialState as assetsInitialState} from '../assets'
|
|
17
|
+
import {ASSETS_ACTIONS} from '../assets/actions'
|
|
18
|
+
import {tagsActions} from '../tags'
|
|
19
|
+
import {uploadsActions} from '../uploads'
|
|
20
|
+
import {createEpicTestStore} from '../../__tests__/fixtures/createEpicTestStore'
|
|
21
|
+
import {createMockSanityClient} from '../../__tests__/fixtures/mockSanityClient'
|
|
22
|
+
import type {AssetItem, AssetType, ImageAsset, Tag} from '../../types'
|
|
23
|
+
|
|
24
|
+
const sampleAsset = {
|
|
25
|
+
_id: 'a1',
|
|
26
|
+
_type: 'sanity.imageAsset',
|
|
27
|
+
_createdAt: '',
|
|
28
|
+
_updatedAt: '',
|
|
29
|
+
_rev: 'r1',
|
|
30
|
+
originalFilename: 'x.png',
|
|
31
|
+
size: 1,
|
|
32
|
+
mimeType: 'image/png',
|
|
33
|
+
url: 'https://example.com/x.png'
|
|
34
|
+
} as ImageAsset
|
|
35
|
+
|
|
36
|
+
const sampleAsset2 = {
|
|
37
|
+
...sampleAsset,
|
|
38
|
+
_id: 'a2'
|
|
39
|
+
} as ImageAsset
|
|
40
|
+
|
|
41
|
+
const sampleTag: Tag = {
|
|
42
|
+
_id: 't1',
|
|
43
|
+
_type: 'media.tag',
|
|
44
|
+
_createdAt: '',
|
|
45
|
+
_updatedAt: '',
|
|
46
|
+
_rev: 'tr',
|
|
47
|
+
name: {_type: 'slug', current: 'alpha'}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const assetItem = (asset: ImageAsset): AssetItem => ({
|
|
51
|
+
_type: 'asset',
|
|
52
|
+
asset,
|
|
53
|
+
picked: false,
|
|
54
|
+
updating: false
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
function assetsWithRows(rows: Record<string, AssetItem>) {
|
|
58
|
+
return {
|
|
59
|
+
...assetsInitialState,
|
|
60
|
+
assetTypes: ['image'] as AssetType[],
|
|
61
|
+
allIds: Object.keys(rows),
|
|
62
|
+
byIds: rows
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
describe('notificationsAssetsDeleteCompleteEpic', () => {
|
|
67
|
+
it('adds info notification with pluralized asset count', async () => {
|
|
68
|
+
const store = createEpicTestStore(
|
|
69
|
+
notificationsAssetsDeleteCompleteEpic,
|
|
70
|
+
createMockSanityClient({}),
|
|
71
|
+
{}
|
|
72
|
+
)
|
|
73
|
+
store.dispatch(assetsActions.deleteComplete({assetIds: ['x', 'y']}))
|
|
74
|
+
await vi.waitFor(() => {
|
|
75
|
+
expect(store.getState().notifications.items).toEqual([
|
|
76
|
+
{asset: undefined, status: 'info', title: '2 assets deleted'}
|
|
77
|
+
])
|
|
78
|
+
})
|
|
79
|
+
})
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
describe('notificationsAssetsDeleteErrorEpic', () => {
|
|
83
|
+
it('adds error notification with count', async () => {
|
|
84
|
+
const store = createEpicTestStore(
|
|
85
|
+
notificationsAssetsDeleteErrorEpic,
|
|
86
|
+
createMockSanityClient({}),
|
|
87
|
+
{
|
|
88
|
+
assets: assetsWithRows({
|
|
89
|
+
a1: {...assetItem(sampleAsset), updating: true}
|
|
90
|
+
})
|
|
91
|
+
}
|
|
92
|
+
)
|
|
93
|
+
store.dispatch(assetsActions.deleteError({assetIds: ['a1'], error: {} as any}))
|
|
94
|
+
await vi.waitFor(() => {
|
|
95
|
+
const [n] = store.getState().notifications.items
|
|
96
|
+
expect(n.status).toBe('error')
|
|
97
|
+
expect(n.title).toBe(
|
|
98
|
+
'Unable to delete 1 asset. Please review any asset errors and try again.'
|
|
99
|
+
)
|
|
100
|
+
})
|
|
101
|
+
})
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
describe('notificationsAssetsUploadCompleteEpic', () => {
|
|
105
|
+
it('adds info notification from upload check results count', async () => {
|
|
106
|
+
const store = createEpicTestStore(
|
|
107
|
+
notificationsAssetsUploadCompleteEpic,
|
|
108
|
+
createMockSanityClient({}),
|
|
109
|
+
{}
|
|
110
|
+
)
|
|
111
|
+
store.dispatch(
|
|
112
|
+
uploadsActions.checkComplete({
|
|
113
|
+
results: {h1: 'id1', h2: null}
|
|
114
|
+
})
|
|
115
|
+
)
|
|
116
|
+
await vi.waitFor(() => {
|
|
117
|
+
expect(store.getState().notifications.items).toEqual([
|
|
118
|
+
{asset: undefined, status: 'info', title: 'Uploaded 2 assets'}
|
|
119
|
+
])
|
|
120
|
+
})
|
|
121
|
+
})
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
const tagsWithTagUpdating = {
|
|
125
|
+
allIds: ['t1'],
|
|
126
|
+
byIds: {
|
|
127
|
+
t1: {_type: 'tag' as const, tag: sampleTag, picked: false, updating: true}
|
|
128
|
+
},
|
|
129
|
+
creating: false,
|
|
130
|
+
fetchCount: -1,
|
|
131
|
+
fetching: false,
|
|
132
|
+
panelVisible: true
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
describe('notificationsAssetsTagsAddCompleteEpic', () => {
|
|
136
|
+
it('adds info notification with asset count', async () => {
|
|
137
|
+
const store = createEpicTestStore(
|
|
138
|
+
notificationsAssetsTagsAddCompleteEpic,
|
|
139
|
+
createMockSanityClient({}),
|
|
140
|
+
{
|
|
141
|
+
assets: assetsWithRows({
|
|
142
|
+
a1: {...assetItem(sampleAsset), updating: true},
|
|
143
|
+
a2: {...assetItem(sampleAsset2), updating: true}
|
|
144
|
+
}),
|
|
145
|
+
tags: tagsWithTagUpdating
|
|
146
|
+
}
|
|
147
|
+
)
|
|
148
|
+
store.dispatch(
|
|
149
|
+
ASSETS_ACTIONS.tagsAddComplete({
|
|
150
|
+
assets: [assetItem(sampleAsset), assetItem(sampleAsset2)],
|
|
151
|
+
tag: sampleTag
|
|
152
|
+
})
|
|
153
|
+
)
|
|
154
|
+
await vi.waitFor(() => {
|
|
155
|
+
expect(store.getState().notifications.items).toEqual([
|
|
156
|
+
{asset: undefined, status: 'info', title: 'Tag added to 2 assets'}
|
|
157
|
+
])
|
|
158
|
+
})
|
|
159
|
+
})
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
describe('notificationsAssetsTagsRemoveCompleteEpic', () => {
|
|
163
|
+
it('adds info notification with asset count', async () => {
|
|
164
|
+
const store = createEpicTestStore(
|
|
165
|
+
notificationsAssetsTagsRemoveCompleteEpic,
|
|
166
|
+
createMockSanityClient({}),
|
|
167
|
+
{
|
|
168
|
+
assets: assetsWithRows({
|
|
169
|
+
a1: {...assetItem(sampleAsset), updating: true}
|
|
170
|
+
}),
|
|
171
|
+
tags: tagsWithTagUpdating
|
|
172
|
+
}
|
|
173
|
+
)
|
|
174
|
+
store.dispatch(
|
|
175
|
+
ASSETS_ACTIONS.tagsRemoveComplete({
|
|
176
|
+
assets: [assetItem(sampleAsset)],
|
|
177
|
+
tag: sampleTag
|
|
178
|
+
})
|
|
179
|
+
)
|
|
180
|
+
await vi.waitFor(() => {
|
|
181
|
+
expect(store.getState().notifications.items).toEqual([
|
|
182
|
+
{asset: undefined, status: 'info', title: 'Tag removed from 1 asset'}
|
|
183
|
+
])
|
|
184
|
+
})
|
|
185
|
+
})
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
describe('notificationsAssetsUpdateCompleteEpic', () => {
|
|
189
|
+
it('batches multiple updateComplete actions into one notification after buffer window', async () => {
|
|
190
|
+
vi.useFakeTimers()
|
|
191
|
+
const store = createEpicTestStore(
|
|
192
|
+
notificationsAssetsUpdateCompleteEpic,
|
|
193
|
+
createMockSanityClient({}),
|
|
194
|
+
{
|
|
195
|
+
assets: assetsWithRows({
|
|
196
|
+
a1: {...assetItem(sampleAsset), updating: true},
|
|
197
|
+
a2: {...assetItem(sampleAsset2), updating: true}
|
|
198
|
+
})
|
|
199
|
+
}
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
store.dispatch(assetsActions.updateComplete({asset: sampleAsset}))
|
|
203
|
+
store.dispatch(assetsActions.updateComplete({asset: sampleAsset2}))
|
|
204
|
+
await vi.advanceTimersByTimeAsync(2000)
|
|
205
|
+
|
|
206
|
+
expect(store.getState().notifications.items).toEqual([
|
|
207
|
+
{asset: undefined, status: 'info', title: '2 assets updated'}
|
|
208
|
+
])
|
|
209
|
+
vi.useRealTimers()
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
it('emits separate notifications when updates fall in different buffer windows', async () => {
|
|
213
|
+
vi.useFakeTimers()
|
|
214
|
+
const store = createEpicTestStore(
|
|
215
|
+
notificationsAssetsUpdateCompleteEpic,
|
|
216
|
+
createMockSanityClient({}),
|
|
217
|
+
{
|
|
218
|
+
assets: assetsWithRows({
|
|
219
|
+
a1: {...assetItem(sampleAsset), updating: true},
|
|
220
|
+
a2: {...assetItem(sampleAsset2), updating: true}
|
|
221
|
+
})
|
|
222
|
+
}
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
store.dispatch(assetsActions.updateComplete({asset: sampleAsset}))
|
|
226
|
+
await vi.advanceTimersByTimeAsync(2000)
|
|
227
|
+
store.dispatch(assetsActions.updateComplete({asset: sampleAsset2}))
|
|
228
|
+
await vi.advanceTimersByTimeAsync(2000)
|
|
229
|
+
|
|
230
|
+
expect(store.getState().notifications.items).toEqual([
|
|
231
|
+
{asset: undefined, status: 'info', title: '1 asset updated'},
|
|
232
|
+
{asset: undefined, status: 'info', title: '1 asset updated'}
|
|
233
|
+
])
|
|
234
|
+
vi.useRealTimers()
|
|
235
|
+
})
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
describe('notificationsGenericErrorEpic', () => {
|
|
239
|
+
it('maps assets.updateError to error notification title', async () => {
|
|
240
|
+
const store = createEpicTestStore(notificationsGenericErrorEpic, createMockSanityClient({}), {
|
|
241
|
+
assets: assetsWithRows({
|
|
242
|
+
a1: {...assetItem(sampleAsset), updating: true}
|
|
243
|
+
})
|
|
244
|
+
})
|
|
245
|
+
store.dispatch(
|
|
246
|
+
assetsActions.updateError({
|
|
247
|
+
asset: sampleAsset,
|
|
248
|
+
error: {message: 'patch failed', statusCode: 500}
|
|
249
|
+
})
|
|
250
|
+
)
|
|
251
|
+
await vi.waitFor(() => {
|
|
252
|
+
expect(store.getState().notifications.items).toEqual([
|
|
253
|
+
{asset: undefined, status: 'error', title: 'An error occurred: patch failed'}
|
|
254
|
+
])
|
|
255
|
+
})
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
it('maps assets.fetchError (bare HttpError payload) to error notification title', async () => {
|
|
259
|
+
const store = createEpicTestStore(notificationsGenericErrorEpic, createMockSanityClient({}), {
|
|
260
|
+
assets: {
|
|
261
|
+
...assetsInitialState,
|
|
262
|
+
assetTypes: ['image'] as AssetType[],
|
|
263
|
+
fetching: true
|
|
264
|
+
}
|
|
265
|
+
})
|
|
266
|
+
store.dispatch(
|
|
267
|
+
assetsActions.fetchError({
|
|
268
|
+
message: 'fetch failed',
|
|
269
|
+
statusCode: 503
|
|
270
|
+
})
|
|
271
|
+
)
|
|
272
|
+
await vi.waitFor(() => {
|
|
273
|
+
expect(store.getState().notifications.items[0].title).toBe('An error occurred: fetch failed')
|
|
274
|
+
})
|
|
275
|
+
})
|
|
276
|
+
|
|
277
|
+
it('maps tags.createError to error notification title', async () => {
|
|
278
|
+
const store = createEpicTestStore(notificationsGenericErrorEpic, createMockSanityClient({}), {
|
|
279
|
+
tags: {
|
|
280
|
+
creating: true,
|
|
281
|
+
creatingError: undefined,
|
|
282
|
+
allIds: [],
|
|
283
|
+
byIds: {},
|
|
284
|
+
fetchCount: -1,
|
|
285
|
+
fetching: false,
|
|
286
|
+
panelVisible: true
|
|
287
|
+
} as any
|
|
288
|
+
})
|
|
289
|
+
store.dispatch(
|
|
290
|
+
tagsActions.createError({
|
|
291
|
+
name: 'n',
|
|
292
|
+
error: {message: 'tag create', statusCode: 400}
|
|
293
|
+
})
|
|
294
|
+
)
|
|
295
|
+
await vi.waitFor(() => {
|
|
296
|
+
expect(store.getState().notifications.items[0].title).toBe('An error occurred: tag create')
|
|
297
|
+
})
|
|
298
|
+
})
|
|
299
|
+
|
|
300
|
+
it('maps uploads.uploadError to error notification title', async () => {
|
|
301
|
+
const store = createEpicTestStore(notificationsGenericErrorEpic, createMockSanityClient({}), {
|
|
302
|
+
uploads: {allIds: ['h'], byIds: {h: {} as any}}
|
|
303
|
+
})
|
|
304
|
+
store.dispatch(
|
|
305
|
+
uploadsActions.uploadError({
|
|
306
|
+
hash: 'h',
|
|
307
|
+
error: {message: 'upload bad', statusCode: 413}
|
|
308
|
+
})
|
|
309
|
+
)
|
|
310
|
+
await vi.waitFor(() => {
|
|
311
|
+
expect(store.getState().notifications.items[0].title).toBe('An error occurred: upload bad')
|
|
312
|
+
})
|
|
313
|
+
})
|
|
314
|
+
})
|
|
315
|
+
|
|
316
|
+
describe('notificationsTagCreateCompleteEpic', () => {
|
|
317
|
+
it('adds tag created notification', async () => {
|
|
318
|
+
const store = createEpicTestStore(
|
|
319
|
+
notificationsTagCreateCompleteEpic,
|
|
320
|
+
createMockSanityClient({}),
|
|
321
|
+
{}
|
|
322
|
+
)
|
|
323
|
+
store.dispatch(tagsActions.createComplete({tag: sampleTag}))
|
|
324
|
+
await vi.waitFor(() => {
|
|
325
|
+
expect(store.getState().notifications.items).toEqual([
|
|
326
|
+
{asset: undefined, status: 'info', title: 'Tag created'}
|
|
327
|
+
])
|
|
328
|
+
})
|
|
329
|
+
})
|
|
330
|
+
})
|
|
331
|
+
|
|
332
|
+
describe('notificationsTagDeleteCompleteEpic', () => {
|
|
333
|
+
it('adds tag deleted notification', async () => {
|
|
334
|
+
const store = createEpicTestStore(
|
|
335
|
+
notificationsTagDeleteCompleteEpic,
|
|
336
|
+
createMockSanityClient({}),
|
|
337
|
+
{}
|
|
338
|
+
)
|
|
339
|
+
store.dispatch(tagsActions.deleteComplete({tagId: 't1'}))
|
|
340
|
+
await vi.waitFor(() => {
|
|
341
|
+
expect(store.getState().notifications.items).toEqual([
|
|
342
|
+
{asset: undefined, status: 'info', title: 'Tag deleted'}
|
|
343
|
+
])
|
|
344
|
+
})
|
|
345
|
+
})
|
|
346
|
+
})
|
|
347
|
+
|
|
348
|
+
describe('notificationsTagUpdateCompleteEpic', () => {
|
|
349
|
+
it('adds tag updated notification', async () => {
|
|
350
|
+
const store = createEpicTestStore(
|
|
351
|
+
notificationsTagUpdateCompleteEpic,
|
|
352
|
+
createMockSanityClient({}),
|
|
353
|
+
{
|
|
354
|
+
tags: {
|
|
355
|
+
allIds: ['t1'],
|
|
356
|
+
byIds: {
|
|
357
|
+
t1: {_type: 'tag', tag: sampleTag, picked: false, updating: true}
|
|
358
|
+
},
|
|
359
|
+
creating: false,
|
|
360
|
+
fetchCount: -1,
|
|
361
|
+
fetching: false,
|
|
362
|
+
panelVisible: true
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
)
|
|
366
|
+
store.dispatch(tagsActions.updateComplete({tag: sampleTag}))
|
|
367
|
+
await vi.waitFor(() => {
|
|
368
|
+
expect(store.getState().notifications.items).toEqual([
|
|
369
|
+
{asset: undefined, status: 'info', title: 'Tag updated'}
|
|
370
|
+
])
|
|
371
|
+
})
|
|
372
|
+
})
|
|
373
|
+
})
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {type PayloadAction, createSlice} from '@reduxjs/toolkit'
|
|
2
|
-
import type {
|
|
2
|
+
import type {AnyAction} from 'redux'
|
|
3
|
+
import type {HttpError, ImageAsset, MyEpic} from '../../types'
|
|
3
4
|
import pluralize from 'pluralize'
|
|
4
5
|
import {ofType} from 'redux-observable'
|
|
5
6
|
import {of} from 'rxjs'
|
|
@@ -19,6 +20,25 @@ type NotificationsReducerState = {
|
|
|
19
20
|
items: Notification[]
|
|
20
21
|
}
|
|
21
22
|
|
|
23
|
+
function messageFromGenericErrorPayload(payload: unknown): string {
|
|
24
|
+
if (!payload || typeof payload !== 'object') {
|
|
25
|
+
return 'Unknown error'
|
|
26
|
+
}
|
|
27
|
+
if (
|
|
28
|
+
'error' in payload &&
|
|
29
|
+
payload.error &&
|
|
30
|
+
typeof payload.error === 'object' &&
|
|
31
|
+
payload.error !== null &&
|
|
32
|
+
'message' in payload.error
|
|
33
|
+
) {
|
|
34
|
+
return String((payload.error as HttpError).message)
|
|
35
|
+
}
|
|
36
|
+
if ('message' in payload && typeof (payload as HttpError).message === 'string') {
|
|
37
|
+
return String((payload as HttpError).message)
|
|
38
|
+
}
|
|
39
|
+
return 'Unknown error'
|
|
40
|
+
}
|
|
41
|
+
|
|
22
42
|
const initialState = {
|
|
23
43
|
items: []
|
|
24
44
|
} as NotificationsReducerState
|
|
@@ -144,12 +164,12 @@ export const notificationsGenericErrorEpic: MyEpic = action$ =>
|
|
|
144
164
|
tagsActions.updateError.type,
|
|
145
165
|
uploadsActions.uploadError.type
|
|
146
166
|
),
|
|
147
|
-
mergeMap((action:
|
|
148
|
-
const
|
|
167
|
+
mergeMap((action: AnyAction) => {
|
|
168
|
+
const title = `An error occurred: ${messageFromGenericErrorPayload(action.payload)}`
|
|
149
169
|
return of(
|
|
150
170
|
notificationsSlice.actions.add({
|
|
151
171
|
status: 'error',
|
|
152
|
-
title
|
|
172
|
+
title
|
|
153
173
|
})
|
|
154
174
|
)
|
|
155
175
|
})
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// @vitest-environment node
|
|
2
|
+
|
|
3
|
+
import {describe, expect, it} from 'vitest'
|
|
4
|
+
import notificationsReducer, {notificationsActions} from './index'
|
|
5
|
+
import type {ImageAsset} from '../../types'
|
|
6
|
+
import {createTestRootState} from '../../__tests__/fixtures/rootState'
|
|
7
|
+
|
|
8
|
+
const sampleAsset = {
|
|
9
|
+
_id: 'a1',
|
|
10
|
+
_type: 'sanity.imageAsset',
|
|
11
|
+
_createdAt: '',
|
|
12
|
+
_updatedAt: '',
|
|
13
|
+
_rev: 'r1',
|
|
14
|
+
originalFilename: 'x.png',
|
|
15
|
+
size: 1,
|
|
16
|
+
mimeType: 'image/png',
|
|
17
|
+
url: 'https://example.com/x.png'
|
|
18
|
+
} as ImageAsset
|
|
19
|
+
|
|
20
|
+
describe('notificationsReducer', () => {
|
|
21
|
+
it('starts with no items', () => {
|
|
22
|
+
const state = notificationsReducer(undefined, {type: '@@init'})
|
|
23
|
+
expect(state.items).toEqual([])
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('add appends a notification with asset, status, and title', () => {
|
|
27
|
+
const prev = createTestRootState().notifications
|
|
28
|
+
const next = notificationsReducer(
|
|
29
|
+
prev,
|
|
30
|
+
notificationsActions.add({
|
|
31
|
+
asset: sampleAsset,
|
|
32
|
+
status: 'success',
|
|
33
|
+
title: 'Done'
|
|
34
|
+
})
|
|
35
|
+
)
|
|
36
|
+
expect(next.items).toHaveLength(1)
|
|
37
|
+
expect(next.items[0]).toEqual({
|
|
38
|
+
asset: sampleAsset,
|
|
39
|
+
status: 'success',
|
|
40
|
+
title: 'Done'
|
|
41
|
+
})
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('add allows partial payloads', () => {
|
|
45
|
+
let state = createTestRootState().notifications
|
|
46
|
+
state = notificationsReducer(state, notificationsActions.add({status: 'error', title: 'X'}))
|
|
47
|
+
state = notificationsReducer(state, notificationsActions.add({title: 'Y'}))
|
|
48
|
+
expect(state.items).toEqual([
|
|
49
|
+
{asset: undefined, status: 'error', title: 'X'},
|
|
50
|
+
{asset: undefined, status: undefined, title: 'Y'}
|
|
51
|
+
])
|
|
52
|
+
})
|
|
53
|
+
})
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// @vitest-environment node
|
|
2
|
+
|
|
3
|
+
import {describe, expect, it} from 'vitest'
|
|
4
|
+
import searchReducer, {searchActions} from './index'
|
|
5
|
+
import {inputs} from '../../config/searchFacets'
|
|
6
|
+
|
|
7
|
+
describe('search slice', () => {
|
|
8
|
+
it('facetsAdd assigns an id and appends facet', () => {
|
|
9
|
+
let state = searchReducer(undefined, {type: '@@INIT'} as never)
|
|
10
|
+
state = searchReducer(state, searchActions.facetsAdd({facet: {...inputs.title}}))
|
|
11
|
+
expect(state.facets).toHaveLength(1)
|
|
12
|
+
expect(state.facets[0].name).toBe('title')
|
|
13
|
+
expect(state.facets[0].id).toBeDefined()
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
it('querySet updates search string', () => {
|
|
17
|
+
let state = searchReducer(undefined, {type: '@@INIT'} as never)
|
|
18
|
+
state = searchReducer(state, searchActions.querySet({searchQuery: 'cats'}))
|
|
19
|
+
expect(state.query).toBe('cats')
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('facetsClear removes all facets', () => {
|
|
23
|
+
let state = searchReducer(undefined, {type: '@@INIT'} as never)
|
|
24
|
+
state = searchReducer(state, searchActions.facetsAdd({facet: {...inputs.title}}))
|
|
25
|
+
state = searchReducer(state, searchActions.facetsClear())
|
|
26
|
+
expect(state.facets).toHaveLength(0)
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('facetsRemoveByName filters facets', () => {
|
|
30
|
+
let state = searchReducer(undefined, {type: '@@INIT'} as never)
|
|
31
|
+
state = searchReducer(state, searchActions.facetsAdd({facet: {...inputs.title}}))
|
|
32
|
+
state = searchReducer(state, searchActions.facetsRemoveByName({facetName: 'title'}))
|
|
33
|
+
expect(state.facets).toHaveLength(0)
|
|
34
|
+
})
|
|
35
|
+
})
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// @vitest-environment node
|
|
2
|
+
|
|
3
|
+
import {describe, expect, it} from 'vitest'
|
|
4
|
+
import type {RootReducerState} from './types'
|
|
5
|
+
import {selectCombinedItems} from './selectors'
|
|
6
|
+
|
|
7
|
+
describe('selectCombinedItems', () => {
|
|
8
|
+
it('places upload items before asset items', () => {
|
|
9
|
+
const state = {
|
|
10
|
+
assets: {allIds: ['a1', 'a2']},
|
|
11
|
+
uploads: {allIds: ['u1']}
|
|
12
|
+
} as RootReducerState
|
|
13
|
+
|
|
14
|
+
expect(selectCombinedItems(state)).toEqual([
|
|
15
|
+
{id: 'u1', type: 'upload'},
|
|
16
|
+
{id: 'a1', type: 'asset'},
|
|
17
|
+
{id: 'a2', type: 'asset'}
|
|
18
|
+
])
|
|
19
|
+
})
|
|
20
|
+
})
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// @vitest-environment node
|
|
2
|
+
|
|
3
|
+
import {describe, expect, it, vi} from 'vitest'
|
|
4
|
+
import {of} from 'rxjs'
|
|
5
|
+
import {tagsCreateEpic, tagsDeleteEpic, tagsActions} from './index'
|
|
6
|
+
import {createEpicTestStore} from '../../__tests__/fixtures/createEpicTestStore'
|
|
7
|
+
import {
|
|
8
|
+
createMockSanityClient,
|
|
9
|
+
mockTransactionCommit
|
|
10
|
+
} from '../../__tests__/fixtures/mockSanityClient'
|
|
11
|
+
import type {Tag} from '../../types'
|
|
12
|
+
|
|
13
|
+
const sampleTag: Tag = {
|
|
14
|
+
_id: 't1',
|
|
15
|
+
_type: 'media.tag',
|
|
16
|
+
_createdAt: '',
|
|
17
|
+
_updatedAt: '',
|
|
18
|
+
_rev: 'tr',
|
|
19
|
+
name: {_type: 'slug', current: 'alpha'}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
describe('tagsCreateEpic', () => {
|
|
23
|
+
it('creates tag when checkTagName passes', async () => {
|
|
24
|
+
const client = createMockSanityClient({
|
|
25
|
+
fetch: vi.fn().mockResolvedValue(0),
|
|
26
|
+
observable: {
|
|
27
|
+
create: vi.fn(() => of(sampleTag))
|
|
28
|
+
}
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
const store = createEpicTestStore(tagsCreateEpic, client)
|
|
32
|
+
store.dispatch(tagsActions.createRequest({name: 'alpha'}))
|
|
33
|
+
|
|
34
|
+
await vi.waitFor(() => {
|
|
35
|
+
expect(store.getState().tags.byIds.t1?.tag).toEqual(sampleTag)
|
|
36
|
+
expect(client.observable.create).toHaveBeenCalled()
|
|
37
|
+
})
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('dispatches createError when tag exists', async () => {
|
|
41
|
+
const client = createMockSanityClient({
|
|
42
|
+
fetch: vi.fn().mockResolvedValue(1),
|
|
43
|
+
observable: {
|
|
44
|
+
create: vi.fn(() => of(sampleTag))
|
|
45
|
+
}
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
const store = createEpicTestStore(tagsCreateEpic, client)
|
|
49
|
+
store.dispatch(tagsActions.createRequest({name: 'dup'}))
|
|
50
|
+
|
|
51
|
+
await vi.waitFor(() => {
|
|
52
|
+
expect(store.getState().tags.creatingError?.statusCode).toBe(409)
|
|
53
|
+
expect(client.observable.create).not.toHaveBeenCalled()
|
|
54
|
+
})
|
|
55
|
+
})
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
describe('tagsDeleteEpic', () => {
|
|
59
|
+
it('fetches referencing assets and commits transaction', async () => {
|
|
60
|
+
const tx = mockTransactionCommit(undefined)
|
|
61
|
+
const client = createMockSanityClient({
|
|
62
|
+
observable: {
|
|
63
|
+
fetch: vi.fn(() =>
|
|
64
|
+
of([
|
|
65
|
+
{_id: 'a1', _rev: 'r1', opt: {}},
|
|
66
|
+
{_id: 'a2', _rev: 'r2', opt: {}}
|
|
67
|
+
])
|
|
68
|
+
)
|
|
69
|
+
},
|
|
70
|
+
transaction: vi.fn(() => tx)
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
const store = createEpicTestStore(tagsDeleteEpic, client, {
|
|
74
|
+
tags: {
|
|
75
|
+
allIds: ['t1'],
|
|
76
|
+
byIds: {
|
|
77
|
+
t1: {_type: 'tag', tag: sampleTag, picked: false, updating: false}
|
|
78
|
+
},
|
|
79
|
+
creating: false,
|
|
80
|
+
fetchCount: -1,
|
|
81
|
+
fetching: false,
|
|
82
|
+
panelVisible: true
|
|
83
|
+
}
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
store.dispatch(tagsActions.deleteRequest({tag: sampleTag}))
|
|
87
|
+
|
|
88
|
+
await vi.waitFor(() => {
|
|
89
|
+
expect(tx.patch).toHaveBeenCalled()
|
|
90
|
+
expect(tx.delete).toHaveBeenCalledWith('t1')
|
|
91
|
+
expect(tx.commit).toHaveBeenCalled()
|
|
92
|
+
expect(store.getState().tags.byIds.t1).toBeUndefined()
|
|
93
|
+
})
|
|
94
|
+
})
|
|
95
|
+
})
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// @vitest-environment node
|
|
2
|
+
|
|
3
|
+
import {describe, expect, it} from 'vitest'
|
|
4
|
+
import type {Tag} from '../../types'
|
|
5
|
+
import tagsReducer, {tagsActions} from './index'
|
|
6
|
+
|
|
7
|
+
const sampleTag: Tag = {
|
|
8
|
+
_id: 'tag-1',
|
|
9
|
+
_type: 'media.tag',
|
|
10
|
+
_createdAt: '2020-01-01',
|
|
11
|
+
_updatedAt: '2020-01-01',
|
|
12
|
+
_rev: 'r1',
|
|
13
|
+
name: {_type: 'slug', current: 'alpha'}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
describe('tags slice', () => {
|
|
17
|
+
it('createComplete adds tag', () => {
|
|
18
|
+
let state = tagsReducer(undefined, {type: '@@INIT'} as never)
|
|
19
|
+
state = tagsReducer(state, tagsActions.createComplete({tag: sampleTag}))
|
|
20
|
+
expect(state.allIds).toContain('tag-1')
|
|
21
|
+
expect(state.byIds['tag-1'].tag).toEqual(sampleTag)
|
|
22
|
+
expect(state.creating).toBe(false)
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('deleteComplete removes tag', () => {
|
|
26
|
+
let state = tagsReducer(undefined, {type: '@@INIT'} as never)
|
|
27
|
+
state = tagsReducer(state, tagsActions.createComplete({tag: sampleTag}))
|
|
28
|
+
state = tagsReducer(state, tagsActions.deleteComplete({tagId: 'tag-1'}))
|
|
29
|
+
expect(state.allIds).not.toContain('tag-1')
|
|
30
|
+
expect(state.byIds['tag-1']).toBeUndefined()
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('fetchComplete hydrates tag list', () => {
|
|
34
|
+
let state = tagsReducer(undefined, {type: '@@INIT'} as never)
|
|
35
|
+
state = tagsReducer(state, tagsActions.fetchRequest())
|
|
36
|
+
expect(state.fetching).toBe(true)
|
|
37
|
+
state = tagsReducer(state, tagsActions.fetchComplete({tags: [sampleTag]}))
|
|
38
|
+
expect(state.fetching).toBe(false)
|
|
39
|
+
expect(state.byIds['tag-1'].tag).toEqual(sampleTag)
|
|
40
|
+
})
|
|
41
|
+
})
|