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,44 +0,0 @@
1
- import {describe, expect, it} from 'vitest'
2
-
3
- import type {Tag, TagItem} from '../types'
4
- import getTagSelectOptions from './getTagSelectOptions'
5
-
6
- function tagItem(partial: Partial<TagItem> & Pick<TagItem, 'tag'>): TagItem {
7
- return {
8
- _type: 'tag',
9
- picked: false,
10
- updating: false,
11
- ...partial,
12
- }
13
- }
14
-
15
- const makeTag = (id: string, name: string): Tag => ({
16
- _id: id,
17
- _type: 'media.tag',
18
- _createdAt: '',
19
- _updatedAt: '',
20
- _rev: 'r1',
21
- name: {_type: 'slug', current: name},
22
- })
23
-
24
- describe('getTagSelectOptions', () => {
25
- it('maps tag items to label/value options', () => {
26
- const tags = [tagItem({tag: makeTag('t1', 'alpha')}), tagItem({tag: makeTag('t2', 'beta')})]
27
- expect(getTagSelectOptions(tags)).toEqual([
28
- {label: 'alpha', value: 't1'},
29
- {label: 'beta', value: 't2'},
30
- ])
31
- })
32
-
33
- it('returns an empty array for an empty list', () => {
34
- expect(getTagSelectOptions([])).toEqual([])
35
- })
36
-
37
- it('skips items without a tag', () => {
38
- const tags = [
39
- tagItem({tag: makeTag('t1', 'ok')}),
40
- {_type: 'tag', tag: undefined, picked: false, updating: false} as unknown as TagItem,
41
- ]
42
- expect(getTagSelectOptions(tags)).toEqual([{label: 'ok', value: 't1'}])
43
- })
44
- })
@@ -1,16 +0,0 @@
1
- import type {TagSelectOption, TagItem} from '../types'
2
-
3
- const getTagSelectOptions = (tags: TagItem[]): TagSelectOption[] => {
4
- return tags.reduce((acc: TagSelectOption[], val) => {
5
- const tag = val?.tag
6
- if (tag) {
7
- acc.push({
8
- label: tag?.name?.current,
9
- value: tag?._id,
10
- })
11
- }
12
- return acc
13
- }, [])
14
- }
15
-
16
- export default getTagSelectOptions
@@ -1,26 +0,0 @@
1
- import type {SanityDocument} from '@sanity/client'
2
- import {describe, expect, it} from 'vitest'
3
-
4
- import {getUniqueDocuments} from './getUniqueDocuments'
5
-
6
- describe('getUniqueDocuments', () => {
7
- it('drops published documents when a drafts.* sibling exists', () => {
8
- const docs: SanityDocument[] = [
9
- {_id: 'drafts.post1', _type: 'post'} as SanityDocument,
10
- {_id: 'post1', _type: 'post'} as SanityDocument,
11
- ]
12
- expect(getUniqueDocuments(docs)).toEqual([{_id: 'drafts.post1', _type: 'post'}])
13
- })
14
-
15
- it('keeps published-only and draft-only ids', () => {
16
- const docs: SanityDocument[] = [
17
- {_id: 'onlyPub', _type: 'x'} as SanityDocument,
18
- {_id: 'drafts.onlyDraft', _type: 'x'} as SanityDocument,
19
- ]
20
- expect(getUniqueDocuments(docs)).toEqual(docs)
21
- })
22
-
23
- it('returns an empty array for an empty list', () => {
24
- expect(getUniqueDocuments([])).toEqual([])
25
- })
26
- })
@@ -1,15 +0,0 @@
1
- import type {SanityDocument} from '@sanity/client'
2
-
3
- export function getUniqueDocuments(documents: SanityDocument[]): SanityDocument[] {
4
- const draftIds = documents.reduce(
5
- (acc: string[], doc: SanityDocument) =>
6
- doc._id.startsWith('drafts.') ? acc.concat(doc._id.slice(7)) : acc,
7
- [],
8
- )
9
-
10
- const filteredDocuments: SanityDocument[] = documents.filter(
11
- (doc: SanityDocument) => !draftIds.includes(doc._id),
12
- )
13
-
14
- return filteredDocuments
15
- }
@@ -1,46 +0,0 @@
1
- import {afterEach, describe, expect, it} from 'vitest'
2
-
3
- import type {ImageAsset} from '../types'
4
- import imageDprUrl from './imageDprUrl'
5
-
6
- const asset = {
7
- _id: 'a1',
8
- _type: 'sanity.imageAsset',
9
- _createdAt: '',
10
- _updatedAt: '',
11
- _rev: 'r1',
12
- originalFilename: 'x.png',
13
- size: 1,
14
- mimeType: 'image/png',
15
- url: 'https://cdn.test/image.png',
16
- metadata: {dimensions: {width: 100, height: 100}, isOpaque: true},
17
- } as ImageAsset
18
-
19
- describe('imageDprUrl', () => {
20
- const dpr = window.devicePixelRatio
21
-
22
- afterEach(() => {
23
- Object.defineProperty(window, 'devicePixelRatio', {value: dpr, configurable: true})
24
- })
25
-
26
- it('scales width by devicePixelRatio and sets fit=max', () => {
27
- Object.defineProperty(window, 'devicePixelRatio', {value: 2, configurable: true})
28
- const url = imageDprUrl(asset, {width: 400})
29
- expect(url).toBe('https://cdn.test/image.png?fit=max&w=800')
30
- })
31
-
32
- it('includes height when provided, scaled by dpr', () => {
33
- Object.defineProperty(window, 'devicePixelRatio', {value: 2, configurable: true})
34
- const url = imageDprUrl(asset, {width: 300, height: 200})
35
- expect(url).toBe('https://cdn.test/image.png?fit=max&w=600&h=400')
36
- })
37
-
38
- it('uses multiplier 1 when devicePixelRatio is missing', () => {
39
- Object.defineProperty(window, 'devicePixelRatio', {
40
- value: undefined as unknown as number,
41
- configurable: true,
42
- })
43
- const url = imageDprUrl(asset, {width: 100})
44
- expect(url).toBe('https://cdn.test/image.png?fit=max&w=100')
45
- })
46
- })
@@ -1,27 +0,0 @@
1
- import type {ImageAsset} from '../types'
2
-
3
- const imageDprUrl = (
4
- asset: ImageAsset,
5
- options: {
6
- width: number
7
- height?: number
8
- },
9
- ): string => {
10
- const dpi =
11
- typeof window === 'undefined' || !window.devicePixelRatio
12
- ? 1
13
- : Math.round(window.devicePixelRatio)
14
- const imgH = options?.height ? options?.height * Math.max(1, dpi) : undefined
15
- const imgW = options.width * Math.max(1, dpi)
16
-
17
- const urlParams = new URLSearchParams()
18
- urlParams.append('fit', 'max')
19
- urlParams.append('w', imgW.toString())
20
- if (imgH) {
21
- urlParams.append('h', imgH.toString())
22
- }
23
-
24
- return `${asset.url}?${urlParams.toString()}`
25
- }
26
-
27
- export default imageDprUrl
@@ -1,16 +0,0 @@
1
- import {describe, expect, it} from 'vitest'
2
-
3
- import {isSupportedAssetType} from './isSupportedAssetType'
4
-
5
- describe('isSupportedAssetType', () => {
6
- it('returns true for file and image', () => {
7
- expect(isSupportedAssetType('file')).toBe(true)
8
- expect(isSupportedAssetType('image')).toBe(true)
9
- })
10
-
11
- it('returns false for unsupported or missing types', () => {
12
- expect(isSupportedAssetType('video')).toBe(false)
13
- expect(isSupportedAssetType('')).toBe(false)
14
- expect(isSupportedAssetType(undefined)).toBe(false)
15
- })
16
- })
@@ -1,15 +0,0 @@
1
- import {SUPPORTED_ASSET_TYPES} from '../constants'
2
- import type {AssetType} from '../types'
3
-
4
- /**
5
- * Determines whether or not the provided asset type (eg 'image', 'file', 'arbitrary')
6
- * is a supported asset type for this plugin.
7
- *
8
- * @param assetType - The asset type to check.
9
- * @returns True if the asset type is supported, false otherwise.
10
- * @internal
11
- */
12
- export function isSupportedAssetType(assetType?: string): assetType is AssetType {
13
- const supported: string[] = SUPPORTED_ASSET_TYPES
14
- return assetType ? supported.includes(assetType) : false
15
- }
@@ -1,73 +0,0 @@
1
- import type {
2
- FieldDefinitionBase,
3
- FileDefinition,
4
- ImageDefinition,
5
- WidenInitialValue,
6
- WidenValidation,
7
- } from 'sanity'
8
-
9
- import {AutoTagInput} from '../components/AutoTagInputWrapper'
10
-
11
- type ImageMediaFieldConfig = Omit<ImageDefinition, 'options'> &
12
- FieldDefinitionBase & {
13
- name: string
14
- mediaTags: string[]
15
- options?: ImageDefinition['options']
16
- }
17
-
18
- type FileMediaFieldConfig = Omit<FileDefinition, 'options'> &
19
- FieldDefinitionBase & {
20
- name: string
21
- mediaTags: string[]
22
- options?: FileDefinition['options']
23
- }
24
-
25
- type ImageMediaFieldResult = Omit<ImageDefinition, 'options'> &
26
- FieldDefinitionBase & {
27
- options?: ImageDefinition['options'] & {mediaTags: string[]}
28
- components: {input: typeof AutoTagInput}
29
- } & WidenValidation &
30
- WidenInitialValue
31
-
32
- type FileMediaFieldResult = Omit<FileDefinition, 'options'> &
33
- FieldDefinitionBase & {
34
- options?: FileDefinition['options'] & {mediaTags: string[]}
35
- components: {input: typeof AutoTagInput}
36
- } & WidenValidation &
37
- WidenInitialValue
38
-
39
- /**
40
- * Defines an image or file field with automatic media tag application when an asset is selected.
41
- *
42
- * Pass `mediaTags` at the top level — they are moved into `options.mediaTags` (for media browser
43
- * pre-filtering) and wire up {@link AutoTagInput} as the field component automatically:
44
- * ```ts
45
- * import {mediaField} from 'sanity-plugin-media'
46
- *
47
- * mediaField({
48
- * name: 'coverImage',
49
- * type: 'image',
50
- * mediaTags: ['product-cover'],
51
- * options: { hotspot: true },
52
- * })
53
- * ```
54
- *
55
- * For file fields, set `type: 'file'`:
56
- * ```ts
57
- * mediaField({ name: 'drawing', type: 'file', mediaTags: ['model-drawing'] })
58
- * ```
59
- */
60
- export function mediaField(config: ImageMediaFieldConfig): ImageMediaFieldResult
61
- export function mediaField(config: FileMediaFieldConfig): FileMediaFieldResult
62
- export function mediaField(
63
- config: ImageMediaFieldConfig | FileMediaFieldConfig,
64
- ): ImageMediaFieldResult | FileMediaFieldResult {
65
- const {mediaTags, options, components, ...rest} = config as ImageMediaFieldConfig & {
66
- components?: Record<string, unknown>
67
- }
68
- return {
69
- ...rest,
70
- options: {...options, mediaTags},
71
- components: {...components, input: AutoTagInput},
72
- } as unknown as ImageMediaFieldResult
73
- }
@@ -1,59 +0,0 @@
1
- // @vitest-environment node
2
-
3
- import {describe, expect, it} from 'vitest'
4
-
5
- import sanitizeFormData from './sanitizeFormData'
6
-
7
- describe('sanitizeFormData', () => {
8
- it('maps empty string, undefined, and empty array to null', () => {
9
- expect(
10
- sanitizeFormData({
11
- a: '',
12
- b: undefined,
13
- c: [],
14
- }),
15
- ).toEqual({
16
- a: null,
17
- b: null,
18
- c: null,
19
- })
20
- })
21
-
22
- it('trims non-empty strings', () => {
23
- expect(sanitizeFormData({title: ' hello '})).toEqual({title: 'hello'})
24
- })
25
-
26
- it('recurses into plain objects', () => {
27
- expect(
28
- sanitizeFormData({
29
- opt: {
30
- media: {
31
- tags: [],
32
- },
33
- },
34
- }),
35
- ).toEqual({
36
- opt: {
37
- media: {
38
- tags: null,
39
- },
40
- },
41
- })
42
- })
43
-
44
- it('preserves null and non-empty arrays', () => {
45
- expect(
46
- sanitizeFormData({
47
- kept: null,
48
- tags: [{_ref: 't1'}],
49
- }),
50
- ).toEqual({
51
- kept: null,
52
- tags: [{_ref: 't1'}],
53
- })
54
- })
55
-
56
- it('preserves numbers and booleans', () => {
57
- expect(sanitizeFormData({n: 0, ok: false})).toEqual({n: 0, ok: false})
58
- })
59
- })
@@ -1,26 +0,0 @@
1
- // Recursively sanitize form data:
2
- // - convert empty strings, undefined values and empty arrays to null (to correctly unset / delete fields)
3
- // - trim whitespace on string fleids
4
-
5
- type FormData = Record<string, any>
6
-
7
- const sanitizeFormData = (formData: FormData): FormData => {
8
- return Object.keys(formData).reduce((acc: FormData, key) => {
9
- const val = formData[key]
10
-
11
- // TODO: refactor
12
- if (typeof val === 'object' && val !== null && val.constructor !== Array) {
13
- acc[key] = sanitizeFormData(val)
14
- } else if (val === '' || typeof val === 'undefined' || val?.length === 0) {
15
- acc[key] = null
16
- } else if (typeof val === 'string' && val) {
17
- acc[key] = formData[key].trim()
18
- } else {
19
- acc[key] = formData[key]
20
- }
21
-
22
- return acc
23
- }, {})
24
- }
25
-
26
- export default sanitizeFormData
@@ -1,18 +0,0 @@
1
- import {describe, expect, it} from 'vitest'
2
-
3
- import type {Asset} from '../types'
4
- import {isFileAsset, isImageAsset} from './typeGuards'
5
-
6
- describe('typeGuards', () => {
7
- it('isFileAsset narrows sanity.fileAsset', () => {
8
- const file = {_type: 'sanity.fileAsset'} as Asset
9
- expect(isFileAsset(file)).toBe(true)
10
- expect(isImageAsset(file)).toBe(false)
11
- })
12
-
13
- it('isImageAsset narrows sanity.imageAsset', () => {
14
- const image = {_type: 'sanity.imageAsset'} as Asset
15
- expect(isImageAsset(image)).toBe(true)
16
- expect(isFileAsset(image)).toBe(false)
17
- })
18
- })
@@ -1,9 +0,0 @@
1
- import type {Asset, FileAsset, ImageAsset} from '../types'
2
-
3
- export const isFileAsset = (asset: Asset): asset is FileAsset => {
4
- return (asset as FileAsset)._type === 'sanity.fileAsset'
5
- }
6
-
7
- export const isImageAsset = (asset: Asset): asset is ImageAsset => {
8
- return (asset as ImageAsset)._type === 'sanity.imageAsset'
9
- }
@@ -1,29 +0,0 @@
1
- import {firstValueFrom} from 'rxjs'
2
- import {afterEach, describe, expect, it} from 'vitest'
3
-
4
- import {hashFile$} from './uploadSanityAsset'
5
-
6
- describe('hashFile$', () => {
7
- const cryptoRef = globalThis.crypto
8
-
9
- afterEach(() => {
10
- Object.defineProperty(globalThis, 'crypto', {
11
- value: cryptoRef,
12
- configurable: true,
13
- writable: true,
14
- })
15
- })
16
-
17
- it('errors when Web Crypto is unavailable', async () => {
18
- Object.defineProperty(globalThis, 'crypto', {
19
- value: undefined,
20
- configurable: true,
21
- writable: true,
22
- })
23
-
24
- await expect(firstValueFrom(hashFile$(new File(['x'], 'blob.bin')))).rejects.toMatchObject({
25
- message: expect.stringMatching(/secure contexts/i),
26
- statusCode: 500,
27
- })
28
- })
29
- })
@@ -1,97 +0,0 @@
1
- // Sourced from:
2
- // https://github.com/sanity-io/sanity/blob/ccb777e115a8cdf20d81a9a2bc9d8c228568faff/packages/%40sanity/form-builder/src/sanity/inputs/client-adapters/assets.ts
3
-
4
- import type {SanityAssetDocument, SanityClient, SanityImageAssetDocument} from '@sanity/client'
5
- import {Observable, of, throwError} from 'rxjs'
6
- import {map, mergeMap} from 'rxjs/operators'
7
-
8
- import type {HttpError} from '../types'
9
- import {withMaxConcurrency} from './withMaxConcurrency'
10
-
11
- const fetchExisting$ = (client: SanityClient, type: string, hash: string) => {
12
- return client.observable.fetch('*[_type == $documentType && sha1hash == $hash][0]', {
13
- documentType: type,
14
- hash,
15
- })
16
- }
17
-
18
- const readFile$ = (file: File): Observable<ArrayBuffer> => {
19
- return new Observable((subscriber) => {
20
- const reader = new FileReader()
21
- reader.onload = () => {
22
- subscriber.next(reader.result as ArrayBuffer)
23
- subscriber.complete()
24
- }
25
- reader.onerror = (err) => {
26
- subscriber.error(err)
27
- }
28
- reader.readAsArrayBuffer(file)
29
- return () => {
30
- reader.abort()
31
- }
32
- })
33
- }
34
-
35
- const hexFromBuffer = (buffer: ArrayBuffer): string => {
36
- return Array.prototype.map
37
- .call(new Uint8Array(buffer), (x) => `00${x.toString(16)}`.slice(-2))
38
- .join('')
39
- }
40
-
41
- export const hashFile$ = (file: File): Observable<string> => {
42
- if (!window.crypto || !window.crypto.subtle || !window.FileReader) {
43
- return throwError({
44
- message: 'Unable to generate hash: uploads are only allowed in secure contexts',
45
- statusCode: 500,
46
- })
47
- }
48
- return readFile$(file).pipe(
49
- mergeMap((arrayBuffer) => window.crypto.subtle.digest('SHA-1', arrayBuffer)),
50
- map(hexFromBuffer),
51
- )
52
- }
53
-
54
- const uploadSanityAsset$ = (
55
- client: SanityClient,
56
- assetType: 'file' | 'image',
57
- file: File,
58
- hash: string,
59
- ) => {
60
- return of(null).pipe(
61
- // NOTE: the sanity api will still dedupe unique files, but this saves us from uploading the asset file entirely
62
- mergeMap(() => fetchExisting$(client, `sanity.${assetType}Asset`, hash)),
63
- // Cancel if the asset already exists
64
- mergeMap((existingAsset: SanityAssetDocument | SanityImageAssetDocument | null) => {
65
- if (existingAsset) {
66
- return throwError({
67
- message: 'Asset already exists',
68
- statusCode: 409,
69
- } as HttpError)
70
- }
71
-
72
- return of(null)
73
- }),
74
- mergeMap(() => {
75
- // Begin upload if no existing asset found
76
- return client.observable.assets
77
- .upload(assetType, file, {
78
- extract: ['blurhash', 'exif', 'location', 'lqip', 'palette'],
79
- preserveFilename: true,
80
- })
81
- .pipe(
82
- map((event) =>
83
- event.type === 'response'
84
- ? {
85
- // rewrite to a 'complete' event
86
- asset: event.body.document,
87
- id: event.body.document._id,
88
- type: 'complete',
89
- }
90
- : event,
91
- ),
92
- )
93
- }),
94
- )
95
- }
96
-
97
- export const uploadAsset$ = withMaxConcurrency(uploadSanityAsset$)
@@ -1,43 +0,0 @@
1
- import {Observable, firstValueFrom} from 'rxjs'
2
- import {describe, expect, it} from 'vitest'
3
-
4
- import {createThrottler, withMaxConcurrency} from './withMaxConcurrency'
5
-
6
- describe('createThrottler', () => {
7
- it('never runs more observables concurrently than the limit', async () => {
8
- let active = 0
9
- let maxActive = 0
10
- const request = createThrottler(2)
11
-
12
- const mk = () =>
13
- new Observable<number>((sub) => {
14
- active++
15
- maxActive = Math.max(maxActive, active)
16
- queueMicrotask(() => {
17
- active--
18
- sub.next(1)
19
- sub.complete()
20
- })
21
- })
22
-
23
- await Promise.all([
24
- firstValueFrom(request(mk())),
25
- firstValueFrom(request(mk())),
26
- firstValueFrom(request(mk())),
27
- ])
28
-
29
- expect(maxActive).toBe(2)
30
- })
31
- })
32
-
33
- describe('withMaxConcurrency', () => {
34
- it('wraps a function so each call returns a single-value observable', async () => {
35
- const fn = (n: number) =>
36
- new Observable<number>((sub) => {
37
- sub.next(n)
38
- sub.complete()
39
- })
40
- const wrapped = withMaxConcurrency(fn, 4)
41
- await expect(firstValueFrom(wrapped(7))).resolves.toBe(7)
42
- })
43
- })
@@ -1,55 +0,0 @@
1
- import {Subject, Subscription, Observable, from} from 'rxjs'
2
- // Takes a observable-returning function and returns a new function that limits on the number of
3
- // concurrent observables.
4
- import {first, mergeMap} from 'rxjs/operators'
5
-
6
- const DEFAULT_CONCURRENCY = 4
7
-
8
- function remove<T>(array: Array<T>, item: T): Array<T> {
9
- const index = array.indexOf(item)
10
- if (index > -1) {
11
- array.splice(index, 1)
12
- }
13
- return array
14
- }
15
-
16
- export const createThrottler = (concurrency: number = DEFAULT_CONCURRENCY) => {
17
- const currentSubscriptions: Array<Subscription> = []
18
- const pendingObservables: Array<Observable<any>> = []
19
- const ready$ = new Subject()
20
-
21
- function request(observable: Observable<any>): Observable<any> {
22
- return new Observable((observer) => {
23
- if (currentSubscriptions.length >= concurrency) {
24
- return scheduleAndWait$(observable)
25
- .pipe(mergeMap(request)) //
26
- .subscribe(observer)
27
- }
28
- const subscription = observable.subscribe(observer)
29
- currentSubscriptions.push(subscription)
30
- return () => {
31
- remove(currentSubscriptions, subscription)
32
- remove(pendingObservables, observable)
33
- subscription.unsubscribe()
34
- while (pendingObservables.length > 0 && currentSubscriptions.length < concurrency) {
35
- ready$.next(pendingObservables.shift())
36
- }
37
- }
38
- })
39
- }
40
-
41
- function scheduleAndWait$(observable: Observable<any>): Observable<any> {
42
- pendingObservables.push(observable)
43
- return ready$.asObservable().pipe(first((obs) => obs === observable))
44
- }
45
-
46
- return request
47
- }
48
-
49
- export const withMaxConcurrency = (
50
- func: (...args: any[]) => Observable<any>,
51
- concurrency: number = DEFAULT_CONCURRENCY,
52
- ) => {
53
- const throttler = createThrottler(concurrency)
54
- return (...args: Array<any>) => from(throttler(func(...args)))
55
- }
@@ -1,17 +0,0 @@
1
- import {zodResolver} from '@hookform/resolvers/zod'
2
- import type {FieldValues, Resolver} from 'react-hook-form'
3
-
4
- /**
5
- * `@hookform/resolvers/zod` resolves its own `zod` typings through dependency
6
- * hoisting, which in this monorepo lands on zod v4, while this plugin authors its
7
- * form schemas with zod v3. The resolver accepts v3 schema instances correctly at
8
- * runtime, so this thin wrapper only bridges the type-only v3/v4 mismatch. The
9
- * react-hook-form field types stay fully checked at the call sites.
10
- */
11
- export default function zodFormResolver<TFieldValues extends FieldValues>(
12
- schema: unknown,
13
- ): Resolver<TFieldValues> {
14
- return zodResolver(
15
- schema as Parameters<typeof zodResolver>[0],
16
- ) as unknown as Resolver<TFieldValues>
17
- }
@@ -1,11 +0,0 @@
1
- const {showIncompatiblePluginDialog} = require('@sanity/incompatible-plugin')
2
- const {name, version} = require('./package.json')
3
-
4
- export default showIncompatiblePluginDialog({
5
- name: name,
6
- versions: {
7
- v3: version,
8
- v2: '^1.4.13',
9
- },
10
- sanityExchangeUrl: 'https://www.sanity.io/plugins/sanity-plugin-media',
11
- })