sanity-plugin-media 4.1.0 → 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 +273 -106
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +273 -106
- package/dist/index.mjs.map +1 -1
- package/package.json +11 -4
- 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/ReduxProvider/index.tsx +2 -1
- package/src/components/UploadDropzone/UploadDropzone.test.tsx +39 -0
- package/src/constants.ts +6 -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 +23 -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/isSupportedAssetType.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,49 @@
|
|
|
1
|
+
// @vitest-environment node
|
|
2
|
+
|
|
3
|
+
import {describe, expect, it} from 'vitest'
|
|
4
|
+
import getDocumentAssetIds from './getDocumentAssetIds'
|
|
5
|
+
|
|
6
|
+
describe('getDocumentAssetIds', () => {
|
|
7
|
+
it('returns empty array for document without asset refs', () => {
|
|
8
|
+
expect(getDocumentAssetIds({_id: 'doc1', _type: 'post'} as any)).toEqual([])
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
it('collects asset _ref from nested portable text–like structures', () => {
|
|
12
|
+
const doc = {
|
|
13
|
+
_id: 'doc1',
|
|
14
|
+
_type: 'post',
|
|
15
|
+
body: [
|
|
16
|
+
{
|
|
17
|
+
_type: 'block',
|
|
18
|
+
asset: {_type: 'reference', _ref: 'image-asset-1'}
|
|
19
|
+
}
|
|
20
|
+
]
|
|
21
|
+
} as any
|
|
22
|
+
|
|
23
|
+
expect(getDocumentAssetIds(doc)).toEqual(['image-asset-1'])
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('dedupes and sorts refs', () => {
|
|
27
|
+
const doc = {
|
|
28
|
+
_id: 'doc1',
|
|
29
|
+
_type: 'post',
|
|
30
|
+
modules: [
|
|
31
|
+
{image: {asset: {_type: 'reference', _ref: 'b'}}},
|
|
32
|
+
{image: {asset: {_type: 'reference', _ref: 'a'}}},
|
|
33
|
+
{image: {asset: {_type: 'reference', _ref: 'b'}}}
|
|
34
|
+
]
|
|
35
|
+
} as any
|
|
36
|
+
|
|
37
|
+
expect(getDocumentAssetIds(doc)).toEqual(['a', 'b'])
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('ignores reference nodes that are not asset references', () => {
|
|
41
|
+
const doc = {
|
|
42
|
+
_id: 'doc1',
|
|
43
|
+
_type: 'post',
|
|
44
|
+
author: {_type: 'reference', _ref: 'person-1'}
|
|
45
|
+
} as any
|
|
46
|
+
|
|
47
|
+
expect(getDocumentAssetIds(doc)).toEqual([])
|
|
48
|
+
})
|
|
49
|
+
})
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import {describe, expect, it} from 'vitest'
|
|
2
|
+
import {getSchemeColor} from './getSchemeColor'
|
|
3
|
+
|
|
4
|
+
describe('getSchemeColor', () => {
|
|
5
|
+
it('returns a hex or theme string for light and dark schemes', () => {
|
|
6
|
+
expect(getSchemeColor('light', 'bg')).toMatch(/^#/)
|
|
7
|
+
expect(getSchemeColor('dark', 'bg')).toMatch(/^#/)
|
|
8
|
+
expect(getSchemeColor('light', 'spotBlue')).toBeTruthy()
|
|
9
|
+
expect(getSchemeColor('dark', 'inputEnabledBorder')).toBeTruthy()
|
|
10
|
+
})
|
|
11
|
+
})
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import {describe, expect, it} from 'vitest'
|
|
2
|
+
import getTagSelectOptions from './getTagSelectOptions'
|
|
3
|
+
import type {Tag, TagItem} from '../types'
|
|
4
|
+
|
|
5
|
+
function tagItem(partial: Partial<TagItem> & Pick<TagItem, 'tag'>): TagItem {
|
|
6
|
+
return {
|
|
7
|
+
_type: 'tag',
|
|
8
|
+
picked: false,
|
|
9
|
+
updating: false,
|
|
10
|
+
...partial
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const makeTag = (id: string, name: string): Tag => ({
|
|
15
|
+
_id: id,
|
|
16
|
+
_type: 'media.tag',
|
|
17
|
+
_createdAt: '',
|
|
18
|
+
_updatedAt: '',
|
|
19
|
+
_rev: 'r1',
|
|
20
|
+
name: {_type: 'slug', current: name}
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
describe('getTagSelectOptions', () => {
|
|
24
|
+
it('maps tag items to label/value options', () => {
|
|
25
|
+
const tags = [tagItem({tag: makeTag('t1', 'alpha')}), tagItem({tag: makeTag('t2', 'beta')})]
|
|
26
|
+
expect(getTagSelectOptions(tags)).toEqual([
|
|
27
|
+
{label: 'alpha', value: 't1'},
|
|
28
|
+
{label: 'beta', value: 't2'}
|
|
29
|
+
])
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('returns an empty array for an empty list', () => {
|
|
33
|
+
expect(getTagSelectOptions([])).toEqual([])
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('skips items without a tag', () => {
|
|
37
|
+
const tags = [
|
|
38
|
+
tagItem({tag: makeTag('t1', 'ok')}),
|
|
39
|
+
{_type: 'tag', tag: undefined, picked: false, updating: false} as unknown as TagItem
|
|
40
|
+
]
|
|
41
|
+
expect(getTagSelectOptions(tags)).toEqual([{label: 'ok', value: 't1'}])
|
|
42
|
+
})
|
|
43
|
+
})
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import {describe, expect, it} from 'vitest'
|
|
2
|
+
import type {SanityDocument} from '@sanity/client'
|
|
3
|
+
import {getUniqueDocuments} from './getUniqueDocuments'
|
|
4
|
+
|
|
5
|
+
describe('getUniqueDocuments', () => {
|
|
6
|
+
it('drops published documents when a drafts.* sibling exists', () => {
|
|
7
|
+
const docs: SanityDocument[] = [
|
|
8
|
+
{_id: 'drafts.post1', _type: 'post'} as SanityDocument,
|
|
9
|
+
{_id: 'post1', _type: 'post'} as SanityDocument
|
|
10
|
+
]
|
|
11
|
+
expect(getUniqueDocuments(docs)).toEqual([{_id: 'drafts.post1', _type: 'post'}])
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
it('keeps published-only and draft-only ids', () => {
|
|
15
|
+
const docs: SanityDocument[] = [
|
|
16
|
+
{_id: 'onlyPub', _type: 'x'} as SanityDocument,
|
|
17
|
+
{_id: 'drafts.onlyDraft', _type: 'x'} as SanityDocument
|
|
18
|
+
]
|
|
19
|
+
expect(getUniqueDocuments(docs)).toEqual(docs)
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('returns an empty array for an empty list', () => {
|
|
23
|
+
expect(getUniqueDocuments([])).toEqual([])
|
|
24
|
+
})
|
|
25
|
+
})
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import {afterEach, describe, expect, it} from 'vitest'
|
|
2
|
+
import imageDprUrl from './imageDprUrl'
|
|
3
|
+
import type {ImageAsset} from '../types'
|
|
4
|
+
|
|
5
|
+
const asset = {
|
|
6
|
+
_id: 'a1',
|
|
7
|
+
_type: 'sanity.imageAsset',
|
|
8
|
+
_createdAt: '',
|
|
9
|
+
_updatedAt: '',
|
|
10
|
+
_rev: 'r1',
|
|
11
|
+
originalFilename: 'x.png',
|
|
12
|
+
size: 1,
|
|
13
|
+
mimeType: 'image/png',
|
|
14
|
+
url: 'https://cdn.test/image.png',
|
|
15
|
+
metadata: {dimensions: {width: 100, height: 100}, isOpaque: true}
|
|
16
|
+
} as ImageAsset
|
|
17
|
+
|
|
18
|
+
describe('imageDprUrl', () => {
|
|
19
|
+
const dpr = window.devicePixelRatio
|
|
20
|
+
|
|
21
|
+
afterEach(() => {
|
|
22
|
+
Object.defineProperty(window, 'devicePixelRatio', {value: dpr, configurable: true})
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('scales width by devicePixelRatio and sets fit=max', () => {
|
|
26
|
+
Object.defineProperty(window, 'devicePixelRatio', {value: 2, configurable: true})
|
|
27
|
+
const url = imageDprUrl(asset, {width: 400})
|
|
28
|
+
expect(url).toBe('https://cdn.test/image.png?fit=max&w=800')
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('includes height when provided, scaled by dpr', () => {
|
|
32
|
+
Object.defineProperty(window, 'devicePixelRatio', {value: 2, configurable: true})
|
|
33
|
+
const url = imageDprUrl(asset, {width: 300, height: 200})
|
|
34
|
+
expect(url).toBe('https://cdn.test/image.png?fit=max&w=600&h=400')
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('uses multiplier 1 when devicePixelRatio is missing', () => {
|
|
38
|
+
Object.defineProperty(window, 'devicePixelRatio', {
|
|
39
|
+
value: undefined as unknown as number,
|
|
40
|
+
configurable: true
|
|
41
|
+
})
|
|
42
|
+
const url = imageDprUrl(asset, {width: 100})
|
|
43
|
+
expect(url).toBe('https://cdn.test/image.png?fit=max&w=100')
|
|
44
|
+
})
|
|
45
|
+
})
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import {describe, expect, it} from 'vitest'
|
|
2
|
+
import {isSupportedAssetType} from './isSupportedAssetType'
|
|
3
|
+
|
|
4
|
+
describe('isSupportedAssetType', () => {
|
|
5
|
+
it('returns true for file and image', () => {
|
|
6
|
+
expect(isSupportedAssetType('file')).toBe(true)
|
|
7
|
+
expect(isSupportedAssetType('image')).toBe(true)
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
it('returns false for unsupported or missing types', () => {
|
|
11
|
+
expect(isSupportedAssetType('video')).toBe(false)
|
|
12
|
+
expect(isSupportedAssetType('')).toBe(false)
|
|
13
|
+
expect(isSupportedAssetType(undefined)).toBe(false)
|
|
14
|
+
})
|
|
15
|
+
})
|
|
@@ -0,0 +1,15 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// @vitest-environment node
|
|
2
|
+
|
|
3
|
+
import {describe, expect, it} from 'vitest'
|
|
4
|
+
import sanitizeFormData from './sanitizeFormData'
|
|
5
|
+
|
|
6
|
+
describe('sanitizeFormData', () => {
|
|
7
|
+
it('maps empty string, undefined, and empty array to null', () => {
|
|
8
|
+
expect(
|
|
9
|
+
sanitizeFormData({
|
|
10
|
+
a: '',
|
|
11
|
+
b: undefined,
|
|
12
|
+
c: []
|
|
13
|
+
})
|
|
14
|
+
).toEqual({
|
|
15
|
+
a: null,
|
|
16
|
+
b: null,
|
|
17
|
+
c: null
|
|
18
|
+
})
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it('trims non-empty strings', () => {
|
|
22
|
+
expect(sanitizeFormData({title: ' hello '})).toEqual({title: 'hello'})
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('recurses into plain objects', () => {
|
|
26
|
+
expect(
|
|
27
|
+
sanitizeFormData({
|
|
28
|
+
opt: {
|
|
29
|
+
media: {
|
|
30
|
+
tags: []
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
})
|
|
34
|
+
).toEqual({
|
|
35
|
+
opt: {
|
|
36
|
+
media: {
|
|
37
|
+
tags: null
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
})
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('preserves null and non-empty arrays', () => {
|
|
44
|
+
expect(
|
|
45
|
+
sanitizeFormData({
|
|
46
|
+
kept: null,
|
|
47
|
+
tags: [{_ref: 't1'}]
|
|
48
|
+
})
|
|
49
|
+
).toEqual({
|
|
50
|
+
kept: null,
|
|
51
|
+
tags: [{_ref: 't1'}]
|
|
52
|
+
})
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('preserves numbers and booleans', () => {
|
|
56
|
+
expect(sanitizeFormData({n: 0, ok: false})).toEqual({n: 0, ok: false})
|
|
57
|
+
})
|
|
58
|
+
})
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import {describe, expect, it} from 'vitest'
|
|
2
|
+
import {isFileAsset, isImageAsset} from './typeGuards'
|
|
3
|
+
import type {Asset} from '../types'
|
|
4
|
+
|
|
5
|
+
describe('typeGuards', () => {
|
|
6
|
+
it('isFileAsset narrows sanity.fileAsset', () => {
|
|
7
|
+
const file = {_type: 'sanity.fileAsset'} as Asset
|
|
8
|
+
expect(isFileAsset(file)).toBe(true)
|
|
9
|
+
expect(isImageAsset(file)).toBe(false)
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
it('isImageAsset narrows sanity.imageAsset', () => {
|
|
13
|
+
const image = {_type: 'sanity.imageAsset'} as Asset
|
|
14
|
+
expect(isImageAsset(image)).toBe(true)
|
|
15
|
+
expect(isFileAsset(image)).toBe(false)
|
|
16
|
+
})
|
|
17
|
+
})
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import {afterEach, describe, expect, it} from 'vitest'
|
|
2
|
+
import {firstValueFrom} from 'rxjs'
|
|
3
|
+
import {hashFile$} from './uploadSanityAsset'
|
|
4
|
+
|
|
5
|
+
describe('hashFile$', () => {
|
|
6
|
+
const cryptoRef = globalThis.crypto
|
|
7
|
+
|
|
8
|
+
afterEach(() => {
|
|
9
|
+
Object.defineProperty(globalThis, 'crypto', {
|
|
10
|
+
value: cryptoRef,
|
|
11
|
+
configurable: true,
|
|
12
|
+
writable: true
|
|
13
|
+
})
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
it('errors when Web Crypto is unavailable', async () => {
|
|
17
|
+
Object.defineProperty(globalThis, 'crypto', {
|
|
18
|
+
value: undefined,
|
|
19
|
+
configurable: true,
|
|
20
|
+
writable: true
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
await expect(firstValueFrom(hashFile$(new File(['x'], 'blob.bin')))).rejects.toMatchObject({
|
|
24
|
+
message: expect.stringMatching(/secure contexts/i),
|
|
25
|
+
statusCode: 500
|
|
26
|
+
})
|
|
27
|
+
})
|
|
28
|
+
})
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import {describe, expect, it} from 'vitest'
|
|
2
|
+
import {Observable, firstValueFrom} from 'rxjs'
|
|
3
|
+
import {createThrottler, withMaxConcurrency} from './withMaxConcurrency'
|
|
4
|
+
|
|
5
|
+
describe('createThrottler', () => {
|
|
6
|
+
it('never runs more observables concurrently than the limit', async () => {
|
|
7
|
+
let active = 0
|
|
8
|
+
let maxActive = 0
|
|
9
|
+
const request = createThrottler(2)
|
|
10
|
+
|
|
11
|
+
const mk = () =>
|
|
12
|
+
new Observable<number>(sub => {
|
|
13
|
+
active++
|
|
14
|
+
maxActive = Math.max(maxActive, active)
|
|
15
|
+
queueMicrotask(() => {
|
|
16
|
+
active--
|
|
17
|
+
sub.next(1)
|
|
18
|
+
sub.complete()
|
|
19
|
+
})
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
await Promise.all([
|
|
23
|
+
firstValueFrom(request(mk())),
|
|
24
|
+
firstValueFrom(request(mk())),
|
|
25
|
+
firstValueFrom(request(mk()))
|
|
26
|
+
])
|
|
27
|
+
|
|
28
|
+
expect(maxActive).toBe(2)
|
|
29
|
+
})
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
describe('withMaxConcurrency', () => {
|
|
33
|
+
it('wraps a function so each call returns a single-value observable', async () => {
|
|
34
|
+
const fn = (n: number) =>
|
|
35
|
+
new Observable<number>(sub => {
|
|
36
|
+
sub.next(n)
|
|
37
|
+
sub.complete()
|
|
38
|
+
})
|
|
39
|
+
const wrapped = withMaxConcurrency(fn, 4)
|
|
40
|
+
await expect(firstValueFrom(wrapped(7))).resolves.toBe(7)
|
|
41
|
+
})
|
|
42
|
+
})
|