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.
Files changed (59) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +56 -4
  3. package/dist/index.d.mts +131 -57
  4. package/dist/index.d.ts +131 -57
  5. package/dist/index.js +259 -98
  6. package/dist/index.js.map +1 -1
  7. package/dist/index.mjs +259 -98
  8. package/dist/index.mjs.map +1 -1
  9. package/package.json +9 -2
  10. package/src/__tests__/fixtures/createEpicTestStore.ts +27 -0
  11. package/src/__tests__/fixtures/listenMock.ts +9 -0
  12. package/src/__tests__/fixtures/mockSanityClient.ts +84 -0
  13. package/src/__tests__/fixtures/renderWithProviders.tsx +54 -0
  14. package/src/__tests__/fixtures/rootState.ts +27 -0
  15. package/src/__tests__/fixtures/withinDialog.ts +28 -0
  16. package/src/components/Browser/Browser.test.tsx +44 -0
  17. package/src/components/CardAsset/CardAsset.test.tsx +322 -0
  18. package/src/components/DialogAssetEdit/Details.tsx +123 -44
  19. package/src/components/DialogAssetEdit/DialogAssetEdit.test.tsx +215 -0
  20. package/src/components/DialogAssetEdit/index.tsx +138 -30
  21. package/src/components/DialogTagCreate/DialogTagCreate.test.tsx +120 -0
  22. package/src/components/DialogTagEdit/DialogTagEdit.test.tsx +164 -0
  23. package/src/components/FormBuilderTool/FormBuilderTool.test.tsx +62 -0
  24. package/src/components/UploadDropzone/UploadDropzone.test.tsx +39 -0
  25. package/src/contexts/ToolOptionsContext.tsx +6 -3
  26. package/src/formSchema/index.test.ts +55 -0
  27. package/src/formSchema/index.ts +28 -12
  28. package/src/hooks/useVersionedClient.ts +1 -1
  29. package/src/modules/assets/deleteAndUpdateEpics.test.ts +86 -0
  30. package/src/modules/assets/fetchEpic.test.ts +72 -0
  31. package/src/modules/assets/reducer.test.ts +90 -0
  32. package/src/modules/assets/tagsAndListenerEpics.test.ts +205 -0
  33. package/src/modules/dialog/epics.test.ts +167 -0
  34. package/src/modules/dialog/reducer.test.ts +184 -0
  35. package/src/modules/notifications/epics.test.ts +373 -0
  36. package/src/modules/notifications/index.ts +24 -4
  37. package/src/modules/notifications/reducer.test.ts +53 -0
  38. package/src/modules/search/index.test.ts +35 -0
  39. package/src/modules/selectors.test.ts +20 -0
  40. package/src/modules/tags/epics.test.ts +95 -0
  41. package/src/modules/tags/index.test.ts +41 -0
  42. package/src/modules/uploads/epics.test.ts +108 -0
  43. package/src/modules/uploads/index.test.ts +58 -0
  44. package/src/operators/checkTagName.test.ts +28 -0
  45. package/src/types/index.ts +20 -7
  46. package/src/utils/blocksToText.test.ts +42 -0
  47. package/src/utils/constructFilter.test.ts +119 -0
  48. package/src/utils/generatePreviewBlobUrl.test.ts +69 -0
  49. package/src/utils/getAssetResolution.test.ts +12 -0
  50. package/src/utils/getDocumentAssetIds.test.ts +49 -0
  51. package/src/utils/getSchemeColor.test.ts +11 -0
  52. package/src/utils/getTagSelectOptions.test.ts +43 -0
  53. package/src/utils/getUniqueDocuments.test.ts +25 -0
  54. package/src/utils/imageDprUrl.test.ts +45 -0
  55. package/src/utils/isSupportedAssetType.test.ts +15 -0
  56. package/src/utils/sanitizeFormData.test.ts +58 -0
  57. package/src/utils/typeGuards.test.ts +17 -0
  58. package/src/utils/uploadSanityAsset.test.ts +28 -0
  59. package/src/utils/withMaxConcurrency.test.ts +42 -0
@@ -0,0 +1,90 @@
1
+ // @vitest-environment node
2
+
3
+ import {describe, expect, it} from 'vitest'
4
+ import type {AssetType, ImageAsset} from '../../types'
5
+ import assetsReducer, {assetsActions, initialState, type AssetsReducerState} from './index'
6
+
7
+ const minimalImage = {
8
+ _id: 'img-1',
9
+ _type: 'sanity.imageAsset',
10
+ _createdAt: '2020-01-01',
11
+ _updatedAt: '2020-01-01',
12
+ _rev: 'r1',
13
+ originalFilename: 'a.png',
14
+ size: 1,
15
+ mimeType: 'image/png',
16
+ url: 'https://example.com/a.png'
17
+ } as ImageAsset
18
+
19
+ describe('assets slice', () => {
20
+ function stateWithOneAsset(): AssetsReducerState {
21
+ return {
22
+ ...initialState,
23
+ assetTypes: ['image'] as AssetType[],
24
+ allIds: ['img-1'],
25
+ byIds: {
26
+ 'img-1': {
27
+ _type: 'asset',
28
+ asset: minimalImage,
29
+ picked: false,
30
+ updating: false
31
+ }
32
+ }
33
+ }
34
+ }
35
+
36
+ it('pick toggles picked flag', () => {
37
+ let state = stateWithOneAsset()
38
+ state = assetsReducer(state, assetsActions.pick({assetId: 'img-1', picked: true}))
39
+ expect(state.byIds['img-1'].picked).toBe(true)
40
+ expect(state.lastPicked).toBe('img-1')
41
+ })
42
+
43
+ it('pickClear clears selection', () => {
44
+ let state = stateWithOneAsset()
45
+ state = assetsReducer(state, assetsActions.pick({assetId: 'img-1', picked: true}))
46
+ state = assetsReducer(state, assetsActions.pickClear())
47
+ expect(state.byIds['img-1'].picked).toBe(false)
48
+ expect(state.lastPicked).toBeUndefined()
49
+ })
50
+
51
+ it('fetchComplete merges assets', () => {
52
+ let state = assetsReducer({...initialState, assetTypes: ['image'] as AssetType[]}, {
53
+ type: '@@INIT'
54
+ } as never)
55
+ state = assetsReducer(state, assetsActions.fetchComplete({assets: [minimalImage]}))
56
+ expect(state.allIds).toContain('img-1')
57
+ expect(state.fetching).toBe(false)
58
+ expect(state.fetchCount).toBe(1)
59
+ })
60
+
61
+ it('orderSet resets page index', () => {
62
+ let state: AssetsReducerState = {
63
+ ...initialState,
64
+ assetTypes: ['image'] as AssetType[],
65
+ pageIndex: 3
66
+ }
67
+ state = assetsReducer(
68
+ state,
69
+ assetsActions.orderSet({
70
+ order: {field: 'size', direction: 'asc'}
71
+ })
72
+ )
73
+ expect(state.pageIndex).toBe(0)
74
+ expect(state.order.field).toBe('size')
75
+ })
76
+
77
+ it('listenerDeleteQueueComplete removes assets', () => {
78
+ let state = stateWithOneAsset()
79
+ state = assetsReducer(state, assetsActions.listenerDeleteQueueComplete({assetIds: ['img-1']}))
80
+ expect(state.allIds).toHaveLength(0)
81
+ expect(state.byIds['img-1']).toBeUndefined()
82
+ })
83
+
84
+ it('listenerUpdateQueueComplete updates asset document', () => {
85
+ let state = stateWithOneAsset()
86
+ const updated = {...minimalImage, title: 'New'}
87
+ state = assetsReducer(state, assetsActions.listenerUpdateQueueComplete({assets: [updated]}))
88
+ expect(state.byIds['img-1'].asset.title).toBe('New')
89
+ })
90
+ })
@@ -0,0 +1,205 @@
1
+ // @vitest-environment node
2
+
3
+ import {describe, expect, it, vi, beforeEach, afterEach} from 'vitest'
4
+ import {
5
+ assetsActions,
6
+ assetsListenerCreateQueueEpic,
7
+ assetsListenerDeleteQueueEpic,
8
+ assetsListenerUpdateQueueEpic,
9
+ assetsTagsAddEpic,
10
+ assetsTagsRemoveEpic
11
+ } from './index'
12
+ import {ASSETS_ACTIONS} from './actions'
13
+ import {createEpicTestStore} from '../../__tests__/fixtures/createEpicTestStore'
14
+ import {
15
+ createMockSanityClient,
16
+ mockTransactionCommit
17
+ } from '../../__tests__/fixtures/mockSanityClient'
18
+ import {initialState as assetsInitialState} from './index'
19
+ import type {ImageAsset, Tag} from '../../types'
20
+
21
+ const sampleAsset = {
22
+ _id: 'a1',
23
+ _type: 'sanity.imageAsset',
24
+ _createdAt: '',
25
+ _updatedAt: '',
26
+ _rev: 'r1',
27
+ originalFilename: 'x.png',
28
+ size: 1,
29
+ mimeType: 'image/png',
30
+ url: ''
31
+ } as ImageAsset
32
+
33
+ const sampleTag: Tag = {
34
+ _id: 't1',
35
+ _type: 'media.tag',
36
+ _createdAt: '',
37
+ _updatedAt: '',
38
+ _rev: 'tr',
39
+ name: {_type: 'slug', current: 'tag-a'}
40
+ }
41
+
42
+ describe('assetsTagsAddEpic', () => {
43
+ it('runs transaction.commit when adding tag to picked assets', async () => {
44
+ const tx = mockTransactionCommit(undefined)
45
+ const client = createMockSanityClient({
46
+ transaction: vi.fn(() => tx)
47
+ })
48
+
49
+ const assetItem = {
50
+ _type: 'asset' as const,
51
+ asset: sampleAsset,
52
+ picked: true,
53
+ updating: false
54
+ }
55
+
56
+ const store = createEpicTestStore(assetsTagsAddEpic, client, {
57
+ assets: {
58
+ ...assetsInitialState,
59
+ assetTypes: ['image'],
60
+ allIds: ['a1'],
61
+ byIds: {a1: assetItem}
62
+ },
63
+ tags: {
64
+ allIds: ['t1'],
65
+ byIds: {
66
+ t1: {_type: 'tag', tag: sampleTag, picked: false, updating: false}
67
+ },
68
+ creating: false,
69
+ fetchCount: -1,
70
+ fetching: false,
71
+ panelVisible: true
72
+ }
73
+ })
74
+
75
+ store.dispatch(
76
+ ASSETS_ACTIONS.tagsAddRequest({
77
+ assets: [assetItem],
78
+ tag: sampleTag
79
+ })
80
+ )
81
+
82
+ await vi.waitFor(() => {
83
+ expect(tx.commit).toHaveBeenCalled()
84
+ })
85
+ })
86
+ })
87
+
88
+ describe('assetsTagsRemoveEpic', () => {
89
+ it('runs transaction.commit for tag removal', async () => {
90
+ const tx = mockTransactionCommit(undefined)
91
+ const client = createMockSanityClient({
92
+ transaction: vi.fn(() => tx)
93
+ })
94
+
95
+ const assetItem = {
96
+ _type: 'asset' as const,
97
+ asset: sampleAsset,
98
+ picked: true,
99
+ updating: false
100
+ }
101
+
102
+ const store = createEpicTestStore(assetsTagsRemoveEpic, client, {
103
+ assets: {
104
+ ...assetsInitialState,
105
+ assetTypes: ['image'],
106
+ allIds: ['a1'],
107
+ byIds: {a1: assetItem}
108
+ },
109
+ tags: {
110
+ allIds: ['t1'],
111
+ byIds: {
112
+ t1: {_type: 'tag', tag: sampleTag, picked: false, updating: false}
113
+ },
114
+ creating: false,
115
+ fetchCount: -1,
116
+ fetching: false,
117
+ panelVisible: true
118
+ }
119
+ })
120
+
121
+ store.dispatch(
122
+ ASSETS_ACTIONS.tagsRemoveRequest({
123
+ assets: [assetItem],
124
+ tag: sampleTag
125
+ })
126
+ )
127
+
128
+ await vi.waitFor(() => {
129
+ expect(tx.commit).toHaveBeenCalled()
130
+ })
131
+ })
132
+ })
133
+
134
+ describe('assets listener queue epics', () => {
135
+ beforeEach(() => {
136
+ vi.useFakeTimers()
137
+ })
138
+
139
+ afterEach(() => {
140
+ vi.useRealTimers()
141
+ })
142
+
143
+ it('assetsListenerCreateQueueEpic batches creates after buffer window', async () => {
144
+ const store = createEpicTestStore(assetsListenerCreateQueueEpic, createMockSanityClient(), {
145
+ assets: {
146
+ ...assetsInitialState,
147
+ assetTypes: ['image'],
148
+ allIds: ['a1'],
149
+ byIds: {
150
+ a1: {_type: 'asset', asset: sampleAsset, picked: false, updating: false}
151
+ }
152
+ }
153
+ })
154
+
155
+ const updated = {...sampleAsset, title: 'L'}
156
+ store.dispatch(assetsActions.listenerCreateQueue({asset: updated}))
157
+
158
+ await vi.advanceTimersByTimeAsync(2000)
159
+
160
+ await vi.waitFor(() => {
161
+ expect(store.getState().assets.byIds.a1.asset.title).toBe('L')
162
+ })
163
+ })
164
+
165
+ it('assetsListenerDeleteQueueEpic batches deletes', async () => {
166
+ const store = createEpicTestStore(assetsListenerDeleteQueueEpic, createMockSanityClient(), {
167
+ assets: {
168
+ ...assetsInitialState,
169
+ assetTypes: ['image'],
170
+ allIds: ['a1'],
171
+ byIds: {
172
+ a1: {_type: 'asset', asset: sampleAsset, picked: false, updating: false}
173
+ }
174
+ }
175
+ })
176
+
177
+ store.dispatch(assetsActions.listenerDeleteQueue({assetId: 'a1'}))
178
+ await vi.advanceTimersByTimeAsync(2000)
179
+
180
+ await vi.waitFor(() => {
181
+ expect(store.getState().assets.byIds.a1).toBeUndefined()
182
+ })
183
+ })
184
+
185
+ it('assetsListenerUpdateQueueEpic batches updates', async () => {
186
+ const store = createEpicTestStore(assetsListenerUpdateQueueEpic, createMockSanityClient(), {
187
+ assets: {
188
+ ...assetsInitialState,
189
+ assetTypes: ['image'],
190
+ allIds: ['a1'],
191
+ byIds: {
192
+ a1: {_type: 'asset', asset: sampleAsset, picked: false, updating: false}
193
+ }
194
+ }
195
+ })
196
+
197
+ const updated = {...sampleAsset, title: 'Buffered'}
198
+ store.dispatch(assetsActions.listenerUpdateQueue({asset: updated}))
199
+ await vi.advanceTimersByTimeAsync(2000)
200
+
201
+ await vi.waitFor(() => {
202
+ expect(store.getState().assets.byIds.a1.asset.title).toBe('Buffered')
203
+ })
204
+ })
205
+ })
@@ -0,0 +1,167 @@
1
+ // @vitest-environment node
2
+
3
+ import {describe, expect, it, vi} from 'vitest'
4
+ import {dialogClearOnAssetUpdateEpic, dialogTagCreateEpic, dialogTagDeleteEpic} from './index'
5
+ import {assetsActions, initialState as assetsInitialState} from '../assets'
6
+ import {tagsActions} from '../tags'
7
+ import {createEpicTestStore} from '../../__tests__/fixtures/createEpicTestStore'
8
+ import {createMockSanityClient} from '../../__tests__/fixtures/mockSanityClient'
9
+ import type {ImageAsset, Tag} from '../../types'
10
+
11
+ const sampleAsset = {
12
+ _id: 'a1',
13
+ _type: 'sanity.imageAsset',
14
+ _createdAt: '',
15
+ _updatedAt: '',
16
+ _rev: 'r1',
17
+ originalFilename: 'x.png',
18
+ size: 1,
19
+ mimeType: 'image/png',
20
+ url: 'https://example.com/x.png'
21
+ } as ImageAsset
22
+
23
+ const sampleTag: Tag = {
24
+ _id: 't1',
25
+ _type: 'media.tag',
26
+ _createdAt: '',
27
+ _updatedAt: '',
28
+ _rev: 'tr',
29
+ name: {_type: 'slug', current: 'alpha'}
30
+ }
31
+
32
+ const tagWithId = (id: string): Tag => ({
33
+ ...sampleTag,
34
+ _id: id
35
+ })
36
+
37
+ describe('dialogClearOnAssetUpdateEpic', () => {
38
+ it('removes the dialog when assets.updateComplete carries closeDialogId', async () => {
39
+ const store = createEpicTestStore(dialogClearOnAssetUpdateEpic, createMockSanityClient({}), {
40
+ assets: {
41
+ ...assetsInitialState,
42
+ assetTypes: ['image'],
43
+ allIds: ['a1'],
44
+ byIds: {
45
+ a1: {_type: 'asset', asset: sampleAsset, picked: false, updating: true}
46
+ }
47
+ },
48
+ dialog: {
49
+ items: [{id: 'a1', type: 'assetEdit', assetId: 'a1'}]
50
+ }
51
+ })
52
+
53
+ store.dispatch(assetsActions.updateComplete({asset: sampleAsset, closeDialogId: 'a1'}))
54
+
55
+ await vi.waitFor(() => {
56
+ expect(store.getState().dialog.items).toHaveLength(0)
57
+ })
58
+ })
59
+
60
+ it('removes the dialog when tags.updateComplete carries closeDialogId', async () => {
61
+ const store = createEpicTestStore(dialogClearOnAssetUpdateEpic, createMockSanityClient({}), {
62
+ tags: {
63
+ allIds: ['t1'],
64
+ byIds: {
65
+ t1: {_type: 'tag', tag: sampleTag, picked: false, updating: true}
66
+ },
67
+ creating: false,
68
+ fetchCount: -1,
69
+ fetching: false,
70
+ panelVisible: true
71
+ },
72
+ dialog: {
73
+ items: [{id: 't1', type: 'tagEdit', tagId: 't1'}]
74
+ }
75
+ })
76
+
77
+ store.dispatch(tagsActions.updateComplete({tag: sampleTag, closeDialogId: 't1'}))
78
+
79
+ await vi.waitFor(() => {
80
+ expect(store.getState().dialog.items).toHaveLength(0)
81
+ })
82
+ })
83
+
84
+ it('does not emit remove when closeDialogId is absent', async () => {
85
+ const store = createEpicTestStore(dialogClearOnAssetUpdateEpic, createMockSanityClient({}), {
86
+ assets: {
87
+ ...assetsInitialState,
88
+ assetTypes: ['image'],
89
+ allIds: ['a1'],
90
+ byIds: {
91
+ a1: {_type: 'asset', asset: sampleAsset, picked: false, updating: true}
92
+ }
93
+ },
94
+ dialog: {
95
+ items: [{id: 'a1', type: 'assetEdit', assetId: 'a1'}]
96
+ }
97
+ })
98
+
99
+ store.dispatch(assetsActions.updateComplete({asset: sampleAsset}))
100
+
101
+ expect(store.getState().dialog.items).toHaveLength(1)
102
+ })
103
+ })
104
+
105
+ describe('dialogTagCreateEpic', () => {
106
+ it('dispatches inlineTagCreate when createComplete includes assetId', async () => {
107
+ const store = createEpicTestStore(dialogTagCreateEpic, createMockSanityClient({}), {
108
+ dialog: {
109
+ items: [{id: 'a1', type: 'assetEdit', assetId: 'a1'}]
110
+ }
111
+ })
112
+
113
+ store.dispatch(tagsActions.createComplete({assetId: 'a1', tag: sampleTag}))
114
+
115
+ await vi.waitFor(() => {
116
+ const item = store.getState().dialog.items[0]
117
+ expect(item?.type).toBe('assetEdit')
118
+ expect('lastCreatedTag' in item && item.lastCreatedTag).toEqual({
119
+ label: 'alpha',
120
+ value: 't1'
121
+ })
122
+ })
123
+ })
124
+
125
+ it('removes tag create dialog when createComplete has no assetId', async () => {
126
+ const store = createEpicTestStore(dialogTagCreateEpic, createMockSanityClient({}), {
127
+ dialog: {
128
+ items: [{id: 'tagCreate', type: 'tagCreate'}]
129
+ }
130
+ })
131
+
132
+ store.dispatch(tagsActions.createComplete({tag: sampleTag}))
133
+
134
+ await vi.waitFor(() => {
135
+ expect(store.getState().dialog.items).toHaveLength(0)
136
+ })
137
+ })
138
+ })
139
+
140
+ describe('dialogTagDeleteEpic', () => {
141
+ it('dispatches inlineTagRemove with listener tag ids', async () => {
142
+ const store = createEpicTestStore(dialogTagDeleteEpic, createMockSanityClient({}), {
143
+ tags: {
144
+ allIds: ['t9'],
145
+ byIds: {
146
+ t9: {_type: 'tag', tag: tagWithId('t9'), picked: false, updating: false}
147
+ },
148
+ creating: false,
149
+ fetchCount: -1,
150
+ fetching: false,
151
+ panelVisible: true
152
+ },
153
+ dialog: {
154
+ items: [{id: 'a1', type: 'assetEdit', assetId: 'a1'}]
155
+ }
156
+ })
157
+
158
+ store.dispatch(tagsActions.listenerDeleteQueueComplete({tagIds: ['t9']}))
159
+
160
+ await vi.waitFor(() => {
161
+ expect(store.getState().dialog.items[0]).toMatchObject({
162
+ type: 'assetEdit',
163
+ lastRemovedTagIds: ['t9']
164
+ })
165
+ })
166
+ })
167
+ })
@@ -0,0 +1,184 @@
1
+ // @vitest-environment node
2
+
3
+ import {describe, expect, it} from 'vitest'
4
+ import {DIALOG_ACTIONS} from './actions'
5
+ import dialogReducer, {dialogActions} from './index'
6
+ import {assetsActions} from '../assets'
7
+ import {ASSETS_ACTIONS} from '../assets/actions'
8
+ import {tagsActions} from '../tags'
9
+ import type {AssetItem, ImageAsset, Tag} from '../../types'
10
+ import {createTestRootState} from '../../__tests__/fixtures/rootState'
11
+
12
+ const sampleAsset = {
13
+ _id: 'a1',
14
+ _type: 'sanity.imageAsset',
15
+ _createdAt: '',
16
+ _updatedAt: '',
17
+ _rev: 'r1',
18
+ originalFilename: 'x.png',
19
+ size: 1,
20
+ mimeType: 'image/png',
21
+ url: 'https://example.com/x.png'
22
+ } as ImageAsset
23
+
24
+ const sampleTag: Tag = {
25
+ _id: 't1',
26
+ _type: 'media.tag',
27
+ _createdAt: '',
28
+ _updatedAt: '',
29
+ _rev: 'tr',
30
+ name: {_type: 'slug', current: 'alpha'}
31
+ }
32
+
33
+ const assetItem = (asset: ImageAsset = sampleAsset): AssetItem => ({
34
+ _type: 'asset',
35
+ asset,
36
+ picked: false,
37
+ updating: false
38
+ })
39
+
40
+ function dialogState() {
41
+ return createTestRootState().dialog
42
+ }
43
+
44
+ describe('dialog slice reducers', () => {
45
+ it('clear removes all items', () => {
46
+ const state = dialogReducer(
47
+ {...dialogState(), items: [{id: 'x', type: 'tags'}]},
48
+ dialogActions.clear()
49
+ )
50
+ expect(state.items).toEqual([])
51
+ })
52
+
53
+ it('remove filters out the dialog with the given id', () => {
54
+ const state = dialogReducer(
55
+ {
56
+ ...dialogState(),
57
+ items: [
58
+ {id: 'a', type: 'tags'},
59
+ {id: 'b', type: 'searchFacets'}
60
+ ]
61
+ },
62
+ dialogActions.remove({id: 'a'})
63
+ )
64
+ expect(state.items).toEqual([{id: 'b', type: 'searchFacets'}])
65
+ })
66
+
67
+ it('showAssetEdit appends an asset edit dialog', () => {
68
+ const state = dialogReducer(dialogState(), dialogActions.showAssetEdit({assetId: 'a1'}))
69
+ expect(state.items).toEqual([{assetId: 'a1', id: 'a1', type: 'assetEdit'}])
70
+ })
71
+
72
+ it('showSearchFacets and showTags append the expected dialogs', () => {
73
+ let state = dialogReducer(dialogState(), dialogActions.showSearchFacets())
74
+ state = dialogReducer(state, dialogActions.showTags())
75
+ expect(state.items).toEqual([
76
+ {id: 'searchFacets', type: 'searchFacets'},
77
+ {id: 'tags', type: 'tags'}
78
+ ])
79
+ })
80
+
81
+ it('inlineTagCreate sets lastCreatedTag on matching assetEdit items', () => {
82
+ const state = dialogReducer(
83
+ {
84
+ ...dialogState(),
85
+ items: [
86
+ {id: 'a1', type: 'assetEdit', assetId: 'a1'},
87
+ {id: 'a2', type: 'assetEdit', assetId: 'a2'}
88
+ ]
89
+ },
90
+ dialogActions.inlineTagCreate({assetId: 'a1', tag: sampleTag})
91
+ )
92
+ const a1 = state.items.find(i => i.type === 'assetEdit' && i.assetId === 'a1')
93
+ const a2 = state.items.find(i => i.type === 'assetEdit' && i.assetId === 'a2')
94
+ expect(a1 && 'lastCreatedTag' in a1 && a1.lastCreatedTag).toEqual({
95
+ label: 'alpha',
96
+ value: 't1'
97
+ })
98
+ expect(a2).toBeDefined()
99
+ expect(Object.prototype.hasOwnProperty.call(a2, 'lastCreatedTag')).toBe(false)
100
+ })
101
+
102
+ it('inlineTagRemove sets lastRemovedTagIds on all assetEdit items', () => {
103
+ const state = dialogReducer(
104
+ {
105
+ ...dialogState(),
106
+ items: [
107
+ {id: 'a1', type: 'assetEdit', assetId: 'a1'},
108
+ {id: 'tags', type: 'tags'}
109
+ ]
110
+ },
111
+ dialogActions.inlineTagRemove({tagIds: ['x', 'y']})
112
+ )
113
+ const edit = state.items.find(i => i.type === 'assetEdit')
114
+ expect(edit && 'lastRemovedTagIds' in edit && edit.lastRemovedTagIds).toEqual(['x', 'y'])
115
+ const tagsPanel = state.items.find(i => i.type === 'tags')
116
+ expect(tagsPanel && 'lastRemovedTagIds' in tagsPanel).toBeFalsy()
117
+ })
118
+
119
+ it('showConfirmDeleteAssets pushes a confirm dialog wired to assets deleteRequest', () => {
120
+ const item = assetItem()
121
+ const state = dialogReducer(
122
+ dialogState(),
123
+ dialogActions.showConfirmDeleteAssets({assets: [item], closeDialogId: 'a1'})
124
+ )
125
+ const confirm = state.items[0]
126
+ expect(confirm?.type).toBe('confirm')
127
+ expect(confirm && 'title' in confirm && confirm.title).toBe('Permanently delete 1 asset?')
128
+ const cb = confirm && 'confirmCallbackAction' in confirm ? confirm.confirmCallbackAction : null
129
+ expect(cb).toEqual(assetsActions.deleteRequest({assets: [sampleAsset]}))
130
+ })
131
+
132
+ it('showConfirmDeleteTag pushes a confirm dialog wired to tags deleteRequest', () => {
133
+ const state = dialogReducer(
134
+ dialogState(),
135
+ dialogActions.showConfirmDeleteTag({closeDialogId: 't1', tag: sampleTag})
136
+ )
137
+ const confirm = state.items[0]
138
+ expect(confirm?.type).toBe('confirm')
139
+ expect(confirm && 'title' in confirm && confirm.title).toMatch(/permanently delete/i)
140
+ const cb = confirm && 'confirmCallbackAction' in confirm ? confirm.confirmCallbackAction : null
141
+ expect(cb).toEqual(tagsActions.deleteRequest({tag: sampleTag}))
142
+ })
143
+
144
+ it('showConfirmAssetsTagAdd uses plural copy for multiple assets', () => {
145
+ const a2 = {...sampleAsset, _id: 'a2', originalFilename: 'y.png'} as ImageAsset
146
+ const state = dialogReducer(
147
+ dialogState(),
148
+ dialogActions.showConfirmAssetsTagAdd({
149
+ assetsPicked: [assetItem(), assetItem(a2)],
150
+ tag: sampleTag
151
+ })
152
+ )
153
+ const confirm = state.items[0]
154
+ expect(confirm && 'title' in confirm && confirm.title).toContain('2 assets')
155
+ const cb = confirm && 'confirmCallbackAction' in confirm ? confirm.confirmCallbackAction : null
156
+ expect(cb).toEqual(
157
+ ASSETS_ACTIONS.tagsAddRequest({assets: [assetItem(), assetItem(a2)], tag: sampleTag})
158
+ )
159
+ })
160
+
161
+ it('showConfirmAssetsTagRemove pushes removal confirm with tagsRemoveRequest', () => {
162
+ const state = dialogReducer(
163
+ dialogState(),
164
+ dialogActions.showConfirmAssetsTagRemove({
165
+ assetsPicked: [assetItem()],
166
+ tag: sampleTag
167
+ })
168
+ )
169
+ const confirm = state.items[0]
170
+ expect(confirm && 'tone' in confirm && confirm.tone).toBe('critical')
171
+ const cb = confirm && 'confirmCallbackAction' in confirm ? confirm.confirmCallbackAction : null
172
+ expect(cb).toEqual(ASSETS_ACTIONS.tagsRemoveRequest({assets: [assetItem()], tag: sampleTag}))
173
+ })
174
+
175
+ it('DIALOG_ACTIONS.showTagCreate appends tag create dialog', () => {
176
+ const state = dialogReducer(dialogState(), DIALOG_ACTIONS.showTagCreate())
177
+ expect(state.items).toEqual([{id: 'tagCreate', type: 'tagCreate'}])
178
+ })
179
+
180
+ it('DIALOG_ACTIONS.showTagEdit appends tag edit dialog with tag id', () => {
181
+ const state = dialogReducer(dialogState(), DIALOG_ACTIONS.showTagEdit({tagId: 't9'}))
182
+ expect(state.items).toEqual([{id: 't9', tagId: 't9', type: 'tagEdit'}])
183
+ })
184
+ })