sanity-plugin-media 4.3.6 → 5.0.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 (162) hide show
  1. package/package.json +6 -15
  2. package/dist/index.cjs +0 -4721
  3. package/dist/index.cjs.map +0 -1
  4. package/dist/index.d.cts +0 -239
  5. package/dist/index.d.cts.map +0 -1
  6. package/sanity.json +0 -8
  7. package/src/__tests__/fixtures/createEpicTestStore.ts +0 -28
  8. package/src/__tests__/fixtures/listenMock.ts +0 -9
  9. package/src/__tests__/fixtures/mockSanityClient.ts +0 -84
  10. package/src/__tests__/fixtures/renderWithProviders.tsx +0 -55
  11. package/src/__tests__/fixtures/rootState.ts +0 -27
  12. package/src/__tests__/fixtures/withinDialog.ts +0 -28
  13. package/src/components/AssetGridVirtualized/index.tsx +0 -94
  14. package/src/components/AssetMetadata/index.tsx +0 -122
  15. package/src/components/AssetTableVirtualized/index.tsx +0 -73
  16. package/src/components/AutoTagInputWrapper/index.tsx +0 -85
  17. package/src/components/Browser/Browser.test.tsx +0 -45
  18. package/src/components/Browser/index.tsx +0 -90
  19. package/src/components/Browser/useBrowserInit.ts +0 -126
  20. package/src/components/ButtonAssetCopy/index.tsx +0 -65
  21. package/src/components/ButtonViewGroup/index.tsx +0 -39
  22. package/src/components/CardAsset/CardAsset.test.tsx +0 -323
  23. package/src/components/CardAsset/index.tsx +0 -290
  24. package/src/components/CardUpload/index.tsx +0 -161
  25. package/src/components/Controls/index.tsx +0 -136
  26. package/src/components/DebugControls/index.tsx +0 -80
  27. package/src/components/Dialog/index.tsx +0 -11
  28. package/src/components/DialogAssetEdit/Details.tsx +0 -181
  29. package/src/components/DialogAssetEdit/DialogAssetEdit.test.tsx +0 -216
  30. package/src/components/DialogAssetEdit/index.tsx +0 -493
  31. package/src/components/DialogConfirm/index.tsx +0 -90
  32. package/src/components/DialogSearchFacets/index.tsx +0 -42
  33. package/src/components/DialogTagCreate/DialogTagCreate.test.tsx +0 -121
  34. package/src/components/DialogTagCreate/index.tsx +0 -111
  35. package/src/components/DialogTagEdit/DialogTagEdit.test.tsx +0 -165
  36. package/src/components/DialogTagEdit/index.tsx +0 -201
  37. package/src/components/DialogTags/index.tsx +0 -45
  38. package/src/components/Dialogs/index.tsx +0 -76
  39. package/src/components/DocumentList/index.tsx +0 -62
  40. package/src/components/FileAssetPreview/index.tsx +0 -37
  41. package/src/components/FileIcon/index.tsx +0 -43
  42. package/src/components/FormBuilderTool/FormBuilderTool.test.tsx +0 -63
  43. package/src/components/FormBuilderTool/index.tsx +0 -69
  44. package/src/components/FormFieldInputLabel/index.tsx +0 -66
  45. package/src/components/FormFieldInputTags/index.tsx +0 -98
  46. package/src/components/FormFieldInputText/index.tsx +0 -41
  47. package/src/components/FormFieldInputTextarea/index.tsx +0 -43
  48. package/src/components/FormSubmitButton/index.tsx +0 -59
  49. package/src/components/Header/index.tsx +0 -80
  50. package/src/components/Image/index.tsx +0 -41
  51. package/src/components/Items/index.tsx +0 -68
  52. package/src/components/Notifications/index.tsx +0 -24
  53. package/src/components/OrderSelect/index.tsx +0 -66
  54. package/src/components/PickedBar/index.tsx +0 -77
  55. package/src/components/Progress/index.tsx +0 -38
  56. package/src/components/ReduxProvider/index.tsx +0 -96
  57. package/src/components/SearchFacet/index.tsx +0 -66
  58. package/src/components/SearchFacetNumber/index.tsx +0 -133
  59. package/src/components/SearchFacetSelect/index.tsx +0 -110
  60. package/src/components/SearchFacetString/index.tsx +0 -88
  61. package/src/components/SearchFacetTags/index.tsx +0 -121
  62. package/src/components/SearchFacets/index.tsx +0 -72
  63. package/src/components/SearchFacetsControl/index.tsx +0 -140
  64. package/src/components/TableHeader/index.tsx +0 -110
  65. package/src/components/TableHeaderItem/index.tsx +0 -61
  66. package/src/components/TableRowAsset/index.tsx +0 -419
  67. package/src/components/TableRowUpload/index.tsx +0 -164
  68. package/src/components/Tag/index.tsx +0 -200
  69. package/src/components/TagIcon/index.tsx +0 -22
  70. package/src/components/TagView/index.tsx +0 -39
  71. package/src/components/TagViewHeader/index.tsx +0 -70
  72. package/src/components/TagsPanel/index.tsx +0 -40
  73. package/src/components/TagsVirtualized/index.tsx +0 -160
  74. package/src/components/TextInputNumber/index.tsx +0 -32
  75. package/src/components/TextInputSearch/index.tsx +0 -60
  76. package/src/components/Tool/index.tsx +0 -13
  77. package/src/components/UploadDropzone/UploadDropzone.test.tsx +0 -40
  78. package/src/components/UploadDropzone/index.tsx +0 -173
  79. package/src/config/orders.ts +0 -28
  80. package/src/config/searchFacets.ts +0 -312
  81. package/src/constants.ts +0 -87
  82. package/src/contexts/AssetSourceDispatchContext.tsx +0 -38
  83. package/src/contexts/DropzoneDispatchContext.tsx +0 -32
  84. package/src/contexts/ToolOptionsContext.tsx +0 -66
  85. package/src/formSchema/index.test.ts +0 -56
  86. package/src/formSchema/index.ts +0 -39
  87. package/src/hooks/useBreakpointIndex.ts +0 -50
  88. package/src/hooks/useKeyPress.ts +0 -39
  89. package/src/hooks/usePortalPopoverProps.ts +0 -13
  90. package/src/hooks/useTypedSelector.ts +0 -7
  91. package/src/hooks/useVersionedClient.ts +0 -6
  92. package/src/index.ts +0 -5
  93. package/src/modules/assets/actions.ts +0 -42
  94. package/src/modules/assets/deleteAndUpdateEpics.test.ts +0 -87
  95. package/src/modules/assets/fetchEpic.test.ts +0 -73
  96. package/src/modules/assets/index.ts +0 -782
  97. package/src/modules/assets/reducer.test.ts +0 -91
  98. package/src/modules/assets/tagsAndListenerEpics.test.ts +0 -206
  99. package/src/modules/debug/index.ts +0 -28
  100. package/src/modules/dialog/actions.ts +0 -10
  101. package/src/modules/dialog/epics.test.ts +0 -168
  102. package/src/modules/dialog/index.ts +0 -238
  103. package/src/modules/dialog/reducer.test.ts +0 -185
  104. package/src/modules/index.ts +0 -117
  105. package/src/modules/notifications/epics.test.ts +0 -374
  106. package/src/modules/notifications/index.ts +0 -199
  107. package/src/modules/notifications/reducer.test.ts +0 -54
  108. package/src/modules/search/index.test.ts +0 -36
  109. package/src/modules/search/index.ts +0 -167
  110. package/src/modules/selected/index.ts +0 -22
  111. package/src/modules/selectors.test.ts +0 -21
  112. package/src/modules/selectors.ts +0 -17
  113. package/src/modules/tags/epics.test.ts +0 -96
  114. package/src/modules/tags/index.test.ts +0 -42
  115. package/src/modules/tags/index.ts +0 -540
  116. package/src/modules/types.ts +0 -3
  117. package/src/modules/uploads/actions.ts +0 -13
  118. package/src/modules/uploads/epics.test.ts +0 -109
  119. package/src/modules/uploads/index.test.ts +0 -59
  120. package/src/modules/uploads/index.ts +0 -272
  121. package/src/operators/checkTagName.test.ts +0 -29
  122. package/src/operators/checkTagName.ts +0 -33
  123. package/src/operators/debugThrottle.ts +0 -25
  124. package/src/plugin.tsx +0 -54
  125. package/src/schemas/tag.ts +0 -28
  126. package/src/styled/GlobalStyles/index.tsx +0 -40
  127. package/src/styled/react-select/creatable.tsx +0 -184
  128. package/src/styled/react-select/single.tsx +0 -184
  129. package/src/types/index.ts +0 -346
  130. package/src/types/sanity-ui.d.ts +0 -5
  131. package/src/utils/applyMediaTags.ts +0 -87
  132. package/src/utils/blocksToText.test.ts +0 -43
  133. package/src/utils/blocksToText.ts +0 -27
  134. package/src/utils/constructFilter.test.ts +0 -120
  135. package/src/utils/constructFilter.ts +0 -98
  136. package/src/utils/generatePreviewBlobUrl.test.ts +0 -68
  137. package/src/utils/generatePreviewBlobUrl.ts +0 -53
  138. package/src/utils/getAssetResolution.test.ts +0 -13
  139. package/src/utils/getAssetResolution.ts +0 -7
  140. package/src/utils/getDocumentAssetIds.test.ts +0 -50
  141. package/src/utils/getDocumentAssetIds.ts +0 -35
  142. package/src/utils/getSchemeColor.test.ts +0 -12
  143. package/src/utils/getSchemeColor.ts +0 -43
  144. package/src/utils/getTagSelectOptions.test.ts +0 -44
  145. package/src/utils/getTagSelectOptions.ts +0 -16
  146. package/src/utils/getUniqueDocuments.test.ts +0 -26
  147. package/src/utils/getUniqueDocuments.ts +0 -15
  148. package/src/utils/imageDprUrl.test.ts +0 -46
  149. package/src/utils/imageDprUrl.ts +0 -27
  150. package/src/utils/isSupportedAssetType.test.ts +0 -16
  151. package/src/utils/isSupportedAssetType.ts +0 -15
  152. package/src/utils/mediaField.ts +0 -73
  153. package/src/utils/sanitizeFormData.test.ts +0 -59
  154. package/src/utils/sanitizeFormData.ts +0 -26
  155. package/src/utils/typeGuards.test.ts +0 -18
  156. package/src/utils/typeGuards.ts +0 -9
  157. package/src/utils/uploadSanityAsset.test.ts +0 -29
  158. package/src/utils/uploadSanityAsset.ts +0 -97
  159. package/src/utils/withMaxConcurrency.test.ts +0 -43
  160. package/src/utils/withMaxConcurrency.ts +0 -55
  161. package/src/utils/zodFormResolver.ts +0 -17
  162. package/v2-incompatible.js +0 -11
@@ -1,540 +0,0 @@
1
- import {createSelector, createSlice, isAnyOf, type PayloadAction} from '@reduxjs/toolkit'
2
- import type {ClientError, Transaction} from '@sanity/client'
3
- import groq from 'groq'
4
- import type {Selector} from 'react-redux'
5
- import {ofType} from 'redux-observable'
6
- import {from, Observable, of} from 'rxjs'
7
- import {bufferTime, catchError, filter, mergeMap, switchMap, withLatestFrom} from 'rxjs/operators'
8
-
9
- import {TAG_DOCUMENT_NAME} from '../../constants'
10
- import checkTagName from '../../operators/checkTagName'
11
- import debugThrottle from '../../operators/debugThrottle'
12
- import type {Asset, HttpError, MyEpic, TagSelectOption, Tag, TagItem} from '../../types'
13
- import getTagSelectOptions from '../../utils/getTagSelectOptions'
14
- import {ASSETS_ACTIONS} from '../assets/actions'
15
- import {DIALOG_ACTIONS} from '../dialog/actions'
16
- import type {RootReducerState} from '../types'
17
-
18
- type TagsReducerState = {
19
- allIds: string[]
20
- byIds: Record<string, TagItem>
21
- creating: boolean
22
- creatingError?: HttpError
23
- fetchCount: number
24
- fetching: boolean
25
- fetchingError?: HttpError
26
- // totalCount: number
27
- panelVisible: boolean
28
- }
29
-
30
- const initialState = {
31
- allIds: [],
32
- byIds: {},
33
- creating: false,
34
- creatingError: undefined,
35
- fetchCount: -1,
36
- fetching: false,
37
- fetchingError: undefined,
38
- panelVisible: true,
39
- } as TagsReducerState
40
-
41
- const tagsSlice = createSlice({
42
- name: 'tags',
43
- initialState,
44
- extraReducers: (builder) => {
45
- builder
46
- .addCase(DIALOG_ACTIONS.showTagCreate, (state) => {
47
- delete state.creatingError
48
- })
49
- .addCase(DIALOG_ACTIONS.showTagEdit, (state, action) => {
50
- const {tagId} = action.payload
51
- delete state.byIds[tagId]!.error
52
- })
53
- .addMatcher(
54
- isAnyOf(
55
- ASSETS_ACTIONS.tagsAddComplete,
56
- ASSETS_ACTIONS.tagsAddError,
57
- ASSETS_ACTIONS.tagsRemoveComplete,
58
- ASSETS_ACTIONS.tagsRemoveError,
59
- ),
60
- (state, action) => {
61
- const {tag} = action.payload
62
- state.byIds[tag._id]!.updating = false
63
- },
64
- )
65
- .addMatcher(
66
- isAnyOf(ASSETS_ACTIONS.tagsAddRequest, ASSETS_ACTIONS.tagsRemoveRequest),
67
- (state, action) => {
68
- const {tag} = action.payload
69
- state.byIds[tag._id]!.updating = true
70
- },
71
- )
72
- },
73
- reducers: {
74
- createComplete(state, action: PayloadAction<{assetId?: string; tag: Tag}>) {
75
- const {tag} = action.payload
76
- state.creating = false
77
- if (!state.allIds.includes(tag._id)) {
78
- state.allIds.push(tag._id)
79
- }
80
- state.byIds[tag._id] = {
81
- _type: 'tag',
82
- picked: false,
83
- tag,
84
- updating: false,
85
- }
86
- },
87
- createError(state, action: PayloadAction<{error: HttpError; name: string}>) {
88
- state.creating = false
89
- state.creatingError = action.payload.error
90
- },
91
- createRequest(state, _action: PayloadAction<{assetId?: string; name: string}>) {
92
- state.creating = true
93
- delete state.creatingError
94
- },
95
- deleteComplete(state, action: PayloadAction<{tagId: string}>) {
96
- const {tagId} = action.payload
97
- const deleteIndex = state.allIds.indexOf(tagId)
98
- if (deleteIndex >= 0) {
99
- state.allIds.splice(deleteIndex, 1)
100
- }
101
- delete state.byIds[tagId]
102
- },
103
- deleteError(state, action: PayloadAction<{error: HttpError; tag: Tag}>) {
104
- const {error, tag} = action.payload
105
-
106
- const tagId = tag?._id
107
- state.byIds[tagId]!.error = error
108
- state.byIds[tagId]!.updating = false
109
- },
110
- deleteRequest(state, action: PayloadAction<{tag: Tag}>) {
111
- const tagId = action.payload?.tag?._id
112
- state.byIds[tagId]!.picked = false
113
- state.byIds[tagId]!.updating = true
114
-
115
- Object.keys(state.byIds).forEach((key) => {
116
- delete state.byIds[key]!.error
117
- })
118
- },
119
- fetchComplete(state, action: PayloadAction<{tags: Tag[]}>) {
120
- const {tags} = action.payload
121
-
122
- tags?.forEach((tag) => {
123
- state.allIds.push(tag._id)
124
- state.byIds[tag._id] = {
125
- _type: 'tag',
126
- picked: false,
127
- tag,
128
- updating: false,
129
- }
130
- })
131
-
132
- state.fetching = false
133
- state.fetchCount = tags.length || 0
134
- delete state.fetchingError
135
- },
136
- fetchError(state, action: PayloadAction<{error: HttpError}>) {
137
- const {error} = action.payload
138
- state.fetching = false
139
- state.fetchingError = error
140
- },
141
- fetchRequest: {
142
- reducer: (state, _action: PayloadAction<{query: string}>) => {
143
- state.fetching = true
144
- delete state.fetchingError
145
- },
146
- prepare: () => {
147
- // Construct query
148
- const query = groq`
149
- {
150
- "items": *[
151
- _type == "${TAG_DOCUMENT_NAME}"
152
- && !(_id in path("drafts.**"))
153
- ] {
154
- _createdAt,
155
- _updatedAt,
156
- _id,
157
- _rev,
158
- _type,
159
- name
160
- } | order(name.current asc),
161
- }
162
- `
163
- return {payload: {query}}
164
- },
165
- },
166
- // Queue batch tag creation
167
- listenerCreateQueue(_state, _action: PayloadAction<{tag: Tag}>) {
168
- //
169
- },
170
- // Apply created tags (via sanity real-time events)
171
- listenerCreateQueueComplete(state, action: PayloadAction<{tags: Tag[]}>) {
172
- const {tags} = action.payload
173
-
174
- tags?.forEach((tag) => {
175
- state.byIds[tag._id] = {
176
- _type: 'tag',
177
- picked: false,
178
- tag,
179
- updating: false,
180
- }
181
- if (!state.allIds.includes(tag._id)) {
182
- state.allIds.push(tag._id)
183
- }
184
- })
185
- },
186
- // Queue batch tag deletion
187
- listenerDeleteQueue(_state, _action: PayloadAction<{tagId: string}>) {
188
- //
189
- },
190
- // Apply deleted tags (via sanity real-time events)
191
- listenerDeleteQueueComplete(state, action: PayloadAction<{tagIds: string[]}>) {
192
- const {tagIds} = action.payload
193
-
194
- tagIds?.forEach((tagId) => {
195
- const deleteIndex = state.allIds.indexOf(tagId)
196
- if (deleteIndex >= 0) {
197
- state.allIds.splice(deleteIndex, 1)
198
- }
199
- delete state.byIds[tagId]
200
- })
201
- },
202
- // Queue batch tag updates
203
- listenerUpdateQueue(_state, _action: PayloadAction<{tag: Tag}>) {
204
- //
205
- },
206
- // Apply updated tags (via sanity real-time events)
207
- listenerUpdateQueueComplete(state, action: PayloadAction<{tags: Tag[]}>) {
208
- const {tags} = action.payload
209
-
210
- tags?.forEach((tag) => {
211
- if (state.byIds[tag._id]) {
212
- state.byIds[tag._id]!.tag = tag
213
- }
214
- })
215
- },
216
- // Set tag panel visibility
217
- panelVisibleSet(state, action: PayloadAction<{panelVisible: boolean}>) {
218
- const {panelVisible} = action.payload
219
- state.panelVisible = panelVisible
220
- },
221
- // Sort all tags by name
222
- sort(state) {
223
- state.allIds.sort((a, b) => {
224
- const tagA = state.byIds[a]!.tag.name.current
225
- const tagB = state.byIds[b]!.tag.name.current
226
-
227
- if (tagA < tagB) {
228
- return -1
229
- } else if (tagA > tagB) {
230
- return 1
231
- }
232
- return 0
233
- })
234
- },
235
- updateComplete(state, action: PayloadAction<{closeDialogId?: string; tag: Tag}>) {
236
- const {tag} = action.payload
237
- state.byIds[tag._id]!.tag = tag
238
- state.byIds[tag._id]!.updating = false
239
- },
240
- updateError(state, action: PayloadAction<{tag: Tag; error: HttpError}>) {
241
- const {error, tag} = action.payload
242
- const tagId = tag?._id
243
- state.byIds[tagId]!.error = error
244
- state.byIds[tagId]!.updating = false
245
- },
246
- updateRequest(
247
- state,
248
- action: PayloadAction<{
249
- closeDialogId?: string
250
- formData: Record<string, any>
251
- tag: Tag
252
- }>,
253
- ) {
254
- const {tag} = action.payload
255
- state.byIds[tag?._id]!.updating = true
256
- },
257
- },
258
- })
259
-
260
- // Epics
261
-
262
- // On tag create request:
263
- // - async check to see if tag already exists
264
- // - throw if tag already exists
265
- // - otherwise, create new tag
266
- export const tagsCreateEpic: MyEpic = (action$, state$, {client}) =>
267
- action$.pipe(
268
- filter(tagsSlice.actions.createRequest.match),
269
- withLatestFrom(state$),
270
- mergeMap(([action, state]) => {
271
- const {assetId, name} = action.payload
272
-
273
- return of(action).pipe(
274
- debugThrottle(state.debug.badConnection),
275
- checkTagName(client, name),
276
- mergeMap(() =>
277
- client.observable.create({
278
- _type: TAG_DOCUMENT_NAME,
279
- name: {
280
- _type: 'slug',
281
- current: name,
282
- },
283
- }),
284
- ),
285
- mergeMap((result) => of(tagsSlice.actions.createComplete({assetId, tag: result as Tag}))),
286
- catchError((error: ClientError) =>
287
- of(
288
- tagsSlice.actions.createError({
289
- error: {
290
- message: error?.message || 'Internal error',
291
- statusCode: error?.statusCode || 500,
292
- },
293
- name,
294
- }),
295
- ),
296
- ),
297
- )
298
- }),
299
- )
300
-
301
- // On tag delete request
302
- // - find referenced assets
303
- // - remove tag from referenced assets in a sanity transaction
304
- export const tagsDeleteEpic: MyEpic = (action$, state$, {client}) =>
305
- action$.pipe(
306
- filter(tagsSlice.actions.deleteRequest.match),
307
- withLatestFrom(state$),
308
- mergeMap(([action, state]) => {
309
- const {tag} = action.payload
310
- return of(action).pipe(
311
- // Optionally throttle
312
- debugThrottle(state.debug.badConnection),
313
- // Fetch assets which reference this tag
314
- mergeMap(() =>
315
- client.observable.fetch<Asset[]>(
316
- groq`*[
317
- _type in ["sanity.fileAsset", "sanity.imageAsset"]
318
- && references(*[_type == "media.tag" && name.current == $tagName]._id)
319
- ] {
320
- _id,
321
- _rev,
322
- opt
323
- }`,
324
- {tagName: tag.name.current},
325
- ),
326
- ),
327
- // Create transaction which remove tag references from all matched assets and delete tag
328
- mergeMap((assets) => {
329
- const patches = assets.map((asset) => ({
330
- id: asset._id,
331
- patch: {
332
- // this will cause the transaction to fail if the document has been modified since it was fetched.
333
- ifRevisionID: asset._rev,
334
- unset: [`opt.media.tags[_ref == "${tag._id}"]`],
335
- },
336
- }))
337
-
338
- const transaction: Transaction = patches.reduce(
339
- (tx, patch) => tx.patch(patch.id, patch.patch),
340
- client.transaction(),
341
- )
342
-
343
- transaction.delete(tag._id)
344
-
345
- return from(transaction.commit())
346
- }),
347
- // Dispatch complete action
348
- mergeMap(() => of(tagsSlice.actions.deleteComplete({tagId: tag._id}))),
349
- catchError((error: ClientError) =>
350
- of(
351
- tagsSlice.actions.deleteError({
352
- error: {
353
- message: error?.message || 'Internal error',
354
- statusCode: error?.statusCode || 500,
355
- },
356
- tag,
357
- }),
358
- ),
359
- ),
360
- )
361
- }),
362
- )
363
-
364
- // Async fetch tags
365
- export const tagsFetchEpic: MyEpic = (action$, state$, {client}) =>
366
- action$.pipe(
367
- filter(tagsSlice.actions.fetchRequest.match),
368
- withLatestFrom(state$),
369
- switchMap(([action, state]) => {
370
- const {query} = action.payload
371
-
372
- return of(action).pipe(
373
- // Optionally throttle
374
- debugThrottle(state.debug.badConnection),
375
- // Fetch tags
376
- mergeMap(() =>
377
- client.observable.fetch<{
378
- items: Tag[]
379
- }>(query),
380
- ),
381
- // Dispatch complete action
382
- mergeMap((result) => {
383
- const {items} = result
384
- return of(tagsSlice.actions.fetchComplete({tags: items}))
385
- }),
386
- catchError((error: ClientError) =>
387
- of(
388
- tagsSlice.actions.fetchError({
389
- error: {
390
- message: error?.message || 'Internal error',
391
- statusCode: error?.statusCode || 500,
392
- },
393
- }),
394
- ),
395
- ),
396
- )
397
- }),
398
- )
399
-
400
- // TODO: merge all buffer epics
401
- // Buffer tag creation via sanity subscriber
402
- export const tagsListenerCreateQueueEpic: MyEpic = (action$) =>
403
- action$.pipe(
404
- filter(tagsSlice.actions.listenerCreateQueue.match),
405
- bufferTime(2000),
406
- filter((actions) => actions.length > 0),
407
- mergeMap((actions) => {
408
- const tags = actions?.map((action) => action.payload.tag)
409
- return of(tagsSlice.actions.listenerCreateQueueComplete({tags}))
410
- }),
411
- )
412
-
413
- // TODO: merge all buffer epics
414
- // Buffer tag deletion via sanity subscriber
415
- export const tagsListenerDeleteQueueEpic: MyEpic = (action$) =>
416
- action$.pipe(
417
- filter(tagsSlice.actions.listenerDeleteQueue.match),
418
- bufferTime(2000),
419
- filter((actions) => actions.length > 0),
420
- mergeMap((actions) => {
421
- const tagIds = actions?.map((action) => action.payload.tagId)
422
- return of(tagsSlice.actions.listenerDeleteQueueComplete({tagIds}))
423
- }),
424
- )
425
-
426
- // TODO: merge all buffer epics
427
- // Buffer tag update via sanity subscriber
428
- export const tagsListenerUpdateQueueEpic: MyEpic = (action$) =>
429
- action$.pipe(
430
- filter(tagsSlice.actions.listenerUpdateQueue.match),
431
- bufferTime(2000),
432
- filter((actions) => actions.length > 0),
433
- mergeMap((actions) => {
434
- const tags = actions?.map((action) => action.payload.tag)
435
- return of(tagsSlice.actions.listenerUpdateQueueComplete({tags}))
436
- }),
437
- )
438
-
439
- // On successful tag creation or updates:
440
- // - Re-sort all tags
441
- export const tagsSortEpic: MyEpic = (action$) =>
442
- action$.pipe(
443
- ofType(
444
- tagsSlice.actions.listenerCreateQueueComplete.type,
445
- tagsSlice.actions.listenerUpdateQueueComplete.type,
446
- ),
447
- bufferTime(1000),
448
- filter((actions) => actions.length > 0),
449
- mergeMap(() => of(tagsSlice.actions.sort())),
450
- )
451
-
452
- // On tag update request
453
- // - check if tag name already exists
454
- // - throw if tag already exists
455
- // - otherwise, patch document
456
- export const tagsUpdateEpic: MyEpic = (action$, state$, {client}) =>
457
- action$.pipe(
458
- filter(tagsSlice.actions.updateRequest.match),
459
- withLatestFrom(state$),
460
- mergeMap(([action, state]) => {
461
- const {closeDialogId, formData, tag} = action.payload
462
-
463
- return of(action).pipe(
464
- // Optionally throttle
465
- debugThrottle(state.debug.badConnection),
466
- // Check if tag name is available, throw early if not
467
- checkTagName(client, formData?.['name']?.current),
468
- // Patch document (Update tag)
469
- mergeMap(
470
- () =>
471
- from(
472
- client
473
- .patch(tag._id)
474
- .set({name: {_type: 'slug', current: formData?.['name'].current}})
475
- .commit(),
476
- ) as Observable<Tag>,
477
- ),
478
- // Dispatch complete action
479
- mergeMap((updatedTag: Tag) => {
480
- return of(
481
- tagsSlice.actions.updateComplete({
482
- closeDialogId,
483
- tag: updatedTag,
484
- }),
485
- )
486
- }),
487
- catchError((error: ClientError) =>
488
- of(
489
- tagsSlice.actions.updateError({
490
- error: {
491
- message: error?.message || 'Internal error',
492
- statusCode: error?.statusCode || 500,
493
- },
494
- tag,
495
- }),
496
- ),
497
- ),
498
- )
499
- }),
500
- )
501
-
502
- // Selectors
503
-
504
- const selectTagsByIds = (state: RootReducerState) => state.tags.byIds
505
-
506
- const selectTagsAllIds = (state: RootReducerState) => state.tags.allIds
507
-
508
- export const selectTags: Selector<RootReducerState, TagItem[]> = createSelector(
509
- [selectTagsByIds, selectTagsAllIds],
510
- (byIds, allIds) => allIds.map((id) => byIds[id]!),
511
- )
512
-
513
- export const selectTagById = createSelector(
514
- [selectTagsByIds, (_state: RootReducerState, tagId: string) => tagId],
515
- (byIds, tagId) => byIds[tagId],
516
- )
517
-
518
- // TODO: use createSelector
519
- // Map tag references to react-select options, skipping over items with no linked tags
520
- export const selectTagSelectOptions =
521
- (asset?: Asset) =>
522
- (state: RootReducerState): TagSelectOption[] | null => {
523
- const tags = asset?.opt?.media?.tags?.reduce((acc: TagItem[], v) => {
524
- const tagItem = state.tags.byIds[v._ref]
525
- if (tagItem?.tag) {
526
- acc.push(tagItem)
527
- }
528
- return acc
529
- }, [])
530
-
531
- if (tags && tags?.length > 0) {
532
- return getTagSelectOptions(tags)
533
- }
534
-
535
- return null
536
- }
537
-
538
- export const tagsActions = {...tagsSlice.actions}
539
-
540
- export default tagsSlice.reducer
@@ -1,3 +0,0 @@
1
- import {rootReducer} from './index'
2
-
3
- export type RootReducerState = ReturnType<typeof rootReducer>
@@ -1,13 +0,0 @@
1
- import {createAction} from '@reduxjs/toolkit'
2
- import type {SanityAssetDocument, SanityImageAssetDocument} from '@sanity/client'
3
-
4
- export const UPLOADS_ACTIONS = {
5
- uploadComplete: createAction(
6
- 'uploads/uploadComplete',
7
- function prepare({asset}: {asset: SanityAssetDocument | SanityImageAssetDocument}) {
8
- return {
9
- payload: {asset},
10
- }
11
- },
12
- ),
13
- }
@@ -1,109 +0,0 @@
1
- // @vitest-environment jsdom
2
-
3
- import type {SanityImageAssetDocument} from '@sanity/client'
4
- import {of} from 'rxjs'
5
- import {describe, expect, it, vi, beforeEach, afterEach} from 'vitest'
6
-
7
- import {createEpicTestStore} from '../../__tests__/fixtures/createEpicTestStore'
8
- import {createMockSanityClient} from '../../__tests__/fixtures/mockSanityClient'
9
- import {initialState as assetsInitialState} from '../assets'
10
- import {uploadsAssetStartEpic, uploadsCheckRequestEpic, uploadsActions} from './index'
11
-
12
- vi.mock('../../utils/generatePreviewBlobUrl', () => ({
13
- generatePreviewBlobUrl$: () => of('blob:http://preview'),
14
- }))
15
-
16
- const uploadedAsset = {
17
- _id: 'asset-new',
18
- _type: 'sanity.imageAsset',
19
- sha1hash: 'deadbeef',
20
- _createdAt: '',
21
- _updatedAt: '',
22
- _rev: 'r',
23
- originalFilename: 'f.png',
24
- mimeType: 'image/png',
25
- size: 10,
26
- url: '',
27
- } as SanityImageAssetDocument
28
-
29
- vi.mock('../../utils/uploadSanityAsset', () => ({
30
- uploadAsset$: () =>
31
- of({
32
- type: 'complete' as const,
33
- asset: uploadedAsset,
34
- }),
35
- hashFile$: () => of('deadbeef'),
36
- withMaxConcurrency: (fn: unknown) => fn,
37
- }))
38
-
39
- describe('uploadsAssetStartEpic', () => {
40
- it('dispatches preview, progress path, and uploadComplete', async () => {
41
- const client = createMockSanityClient()
42
-
43
- const store = createEpicTestStore(uploadsAssetStartEpic, client)
44
-
45
- const file = new File(['x'], 'f.png', {type: 'image/png'})
46
- const uploadItem = {
47
- _type: 'upload' as const,
48
- assetType: 'image' as const,
49
- hash: 'deadbeef',
50
- name: 'f.png',
51
- size: file.size,
52
- status: 'queued' as const,
53
- }
54
-
55
- store.dispatch(uploadsActions.uploadStart({file, uploadItem}))
56
-
57
- await vi.waitFor(() => {
58
- expect(store.getState().uploads.byIds['deadbeef']?.objectUrl).toBe('blob:http://preview')
59
- })
60
-
61
- await vi.waitFor(() => {
62
- expect(store.getState().assets.byIds['asset-new']).toBeDefined()
63
- })
64
- })
65
- })
66
-
67
- describe('uploadsCheckRequestEpic', () => {
68
- beforeEach(() => {
69
- vi.useFakeTimers()
70
- })
71
-
72
- afterEach(() => {
73
- vi.useRealTimers()
74
- })
75
-
76
- it('after delay, fetches sha hashes and dispatches checkComplete + insertUploads', async () => {
77
- const client = createMockSanityClient({
78
- observable: {
79
- fetch: vi.fn(() => of(['hh'])),
80
- },
81
- })
82
-
83
- const store = createEpicTestStore(uploadsCheckRequestEpic, client, {
84
- assets: {...assetsInitialState, assetTypes: ['image']},
85
- })
86
-
87
- const asset = {
88
- _id: 'id-1',
89
- _type: 'sanity.imageAsset',
90
- sha1hash: 'hh',
91
- _createdAt: '',
92
- _updatedAt: '',
93
- _rev: 'r',
94
- originalFilename: 'f.png',
95
- mimeType: 'image/png',
96
- size: 1,
97
- url: '',
98
- } as SanityImageAssetDocument
99
-
100
- store.dispatch(uploadsActions.checkRequest({assets: [asset]}))
101
-
102
- await vi.advanceTimersByTimeAsync(1100)
103
-
104
- await vi.waitFor(() => {
105
- expect(client.observable.fetch).toHaveBeenCalled()
106
- expect(store.getState().uploads.byIds['hh']).toBeUndefined()
107
- })
108
- })
109
- })