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.
- package/package.json +6 -15
- package/dist/index.cjs +0 -4721
- package/dist/index.cjs.map +0 -1
- package/dist/index.d.cts +0 -239
- package/dist/index.d.cts.map +0 -1
- package/sanity.json +0 -8
- package/src/__tests__/fixtures/createEpicTestStore.ts +0 -28
- package/src/__tests__/fixtures/listenMock.ts +0 -9
- package/src/__tests__/fixtures/mockSanityClient.ts +0 -84
- package/src/__tests__/fixtures/renderWithProviders.tsx +0 -55
- package/src/__tests__/fixtures/rootState.ts +0 -27
- package/src/__tests__/fixtures/withinDialog.ts +0 -28
- package/src/components/AssetGridVirtualized/index.tsx +0 -94
- package/src/components/AssetMetadata/index.tsx +0 -122
- package/src/components/AssetTableVirtualized/index.tsx +0 -73
- package/src/components/AutoTagInputWrapper/index.tsx +0 -85
- package/src/components/Browser/Browser.test.tsx +0 -45
- package/src/components/Browser/index.tsx +0 -90
- package/src/components/Browser/useBrowserInit.ts +0 -126
- package/src/components/ButtonAssetCopy/index.tsx +0 -65
- package/src/components/ButtonViewGroup/index.tsx +0 -39
- package/src/components/CardAsset/CardAsset.test.tsx +0 -323
- package/src/components/CardAsset/index.tsx +0 -290
- package/src/components/CardUpload/index.tsx +0 -161
- package/src/components/Controls/index.tsx +0 -136
- package/src/components/DebugControls/index.tsx +0 -80
- package/src/components/Dialog/index.tsx +0 -11
- package/src/components/DialogAssetEdit/Details.tsx +0 -181
- package/src/components/DialogAssetEdit/DialogAssetEdit.test.tsx +0 -216
- package/src/components/DialogAssetEdit/index.tsx +0 -493
- package/src/components/DialogConfirm/index.tsx +0 -90
- package/src/components/DialogSearchFacets/index.tsx +0 -42
- package/src/components/DialogTagCreate/DialogTagCreate.test.tsx +0 -121
- package/src/components/DialogTagCreate/index.tsx +0 -111
- package/src/components/DialogTagEdit/DialogTagEdit.test.tsx +0 -165
- package/src/components/DialogTagEdit/index.tsx +0 -201
- package/src/components/DialogTags/index.tsx +0 -45
- package/src/components/Dialogs/index.tsx +0 -76
- package/src/components/DocumentList/index.tsx +0 -62
- package/src/components/FileAssetPreview/index.tsx +0 -37
- package/src/components/FileIcon/index.tsx +0 -43
- package/src/components/FormBuilderTool/FormBuilderTool.test.tsx +0 -63
- package/src/components/FormBuilderTool/index.tsx +0 -69
- package/src/components/FormFieldInputLabel/index.tsx +0 -66
- package/src/components/FormFieldInputTags/index.tsx +0 -98
- package/src/components/FormFieldInputText/index.tsx +0 -41
- package/src/components/FormFieldInputTextarea/index.tsx +0 -43
- package/src/components/FormSubmitButton/index.tsx +0 -59
- package/src/components/Header/index.tsx +0 -80
- package/src/components/Image/index.tsx +0 -41
- package/src/components/Items/index.tsx +0 -68
- package/src/components/Notifications/index.tsx +0 -24
- package/src/components/OrderSelect/index.tsx +0 -66
- package/src/components/PickedBar/index.tsx +0 -77
- package/src/components/Progress/index.tsx +0 -38
- package/src/components/ReduxProvider/index.tsx +0 -96
- package/src/components/SearchFacet/index.tsx +0 -66
- package/src/components/SearchFacetNumber/index.tsx +0 -133
- package/src/components/SearchFacetSelect/index.tsx +0 -110
- package/src/components/SearchFacetString/index.tsx +0 -88
- package/src/components/SearchFacetTags/index.tsx +0 -121
- package/src/components/SearchFacets/index.tsx +0 -72
- package/src/components/SearchFacetsControl/index.tsx +0 -140
- package/src/components/TableHeader/index.tsx +0 -110
- package/src/components/TableHeaderItem/index.tsx +0 -61
- package/src/components/TableRowAsset/index.tsx +0 -419
- package/src/components/TableRowUpload/index.tsx +0 -164
- package/src/components/Tag/index.tsx +0 -200
- package/src/components/TagIcon/index.tsx +0 -22
- package/src/components/TagView/index.tsx +0 -39
- package/src/components/TagViewHeader/index.tsx +0 -70
- package/src/components/TagsPanel/index.tsx +0 -40
- package/src/components/TagsVirtualized/index.tsx +0 -160
- package/src/components/TextInputNumber/index.tsx +0 -32
- package/src/components/TextInputSearch/index.tsx +0 -60
- package/src/components/Tool/index.tsx +0 -13
- package/src/components/UploadDropzone/UploadDropzone.test.tsx +0 -40
- package/src/components/UploadDropzone/index.tsx +0 -173
- package/src/config/orders.ts +0 -28
- package/src/config/searchFacets.ts +0 -312
- package/src/constants.ts +0 -87
- package/src/contexts/AssetSourceDispatchContext.tsx +0 -38
- package/src/contexts/DropzoneDispatchContext.tsx +0 -32
- package/src/contexts/ToolOptionsContext.tsx +0 -66
- package/src/formSchema/index.test.ts +0 -56
- package/src/formSchema/index.ts +0 -39
- package/src/hooks/useBreakpointIndex.ts +0 -50
- package/src/hooks/useKeyPress.ts +0 -39
- package/src/hooks/usePortalPopoverProps.ts +0 -13
- package/src/hooks/useTypedSelector.ts +0 -7
- package/src/hooks/useVersionedClient.ts +0 -6
- package/src/index.ts +0 -5
- package/src/modules/assets/actions.ts +0 -42
- package/src/modules/assets/deleteAndUpdateEpics.test.ts +0 -87
- package/src/modules/assets/fetchEpic.test.ts +0 -73
- package/src/modules/assets/index.ts +0 -782
- package/src/modules/assets/reducer.test.ts +0 -91
- package/src/modules/assets/tagsAndListenerEpics.test.ts +0 -206
- package/src/modules/debug/index.ts +0 -28
- package/src/modules/dialog/actions.ts +0 -10
- package/src/modules/dialog/epics.test.ts +0 -168
- package/src/modules/dialog/index.ts +0 -238
- package/src/modules/dialog/reducer.test.ts +0 -185
- package/src/modules/index.ts +0 -117
- package/src/modules/notifications/epics.test.ts +0 -374
- package/src/modules/notifications/index.ts +0 -199
- package/src/modules/notifications/reducer.test.ts +0 -54
- package/src/modules/search/index.test.ts +0 -36
- package/src/modules/search/index.ts +0 -167
- package/src/modules/selected/index.ts +0 -22
- package/src/modules/selectors.test.ts +0 -21
- package/src/modules/selectors.ts +0 -17
- package/src/modules/tags/epics.test.ts +0 -96
- package/src/modules/tags/index.test.ts +0 -42
- package/src/modules/tags/index.ts +0 -540
- package/src/modules/types.ts +0 -3
- package/src/modules/uploads/actions.ts +0 -13
- package/src/modules/uploads/epics.test.ts +0 -109
- package/src/modules/uploads/index.test.ts +0 -59
- package/src/modules/uploads/index.ts +0 -272
- package/src/operators/checkTagName.test.ts +0 -29
- package/src/operators/checkTagName.ts +0 -33
- package/src/operators/debugThrottle.ts +0 -25
- package/src/plugin.tsx +0 -54
- package/src/schemas/tag.ts +0 -28
- package/src/styled/GlobalStyles/index.tsx +0 -40
- package/src/styled/react-select/creatable.tsx +0 -184
- package/src/styled/react-select/single.tsx +0 -184
- package/src/types/index.ts +0 -346
- package/src/types/sanity-ui.d.ts +0 -5
- package/src/utils/applyMediaTags.ts +0 -87
- package/src/utils/blocksToText.test.ts +0 -43
- package/src/utils/blocksToText.ts +0 -27
- package/src/utils/constructFilter.test.ts +0 -120
- package/src/utils/constructFilter.ts +0 -98
- package/src/utils/generatePreviewBlobUrl.test.ts +0 -68
- package/src/utils/generatePreviewBlobUrl.ts +0 -53
- package/src/utils/getAssetResolution.test.ts +0 -13
- package/src/utils/getAssetResolution.ts +0 -7
- package/src/utils/getDocumentAssetIds.test.ts +0 -50
- package/src/utils/getDocumentAssetIds.ts +0 -35
- package/src/utils/getSchemeColor.test.ts +0 -12
- package/src/utils/getSchemeColor.ts +0 -43
- package/src/utils/getTagSelectOptions.test.ts +0 -44
- package/src/utils/getTagSelectOptions.ts +0 -16
- package/src/utils/getUniqueDocuments.test.ts +0 -26
- package/src/utils/getUniqueDocuments.ts +0 -15
- package/src/utils/imageDprUrl.test.ts +0 -46
- package/src/utils/imageDprUrl.ts +0 -27
- package/src/utils/isSupportedAssetType.test.ts +0 -16
- package/src/utils/isSupportedAssetType.ts +0 -15
- package/src/utils/mediaField.ts +0 -73
- package/src/utils/sanitizeFormData.test.ts +0 -59
- package/src/utils/sanitizeFormData.ts +0 -26
- package/src/utils/typeGuards.test.ts +0 -18
- package/src/utils/typeGuards.ts +0 -9
- package/src/utils/uploadSanityAsset.test.ts +0 -29
- package/src/utils/uploadSanityAsset.ts +0 -97
- package/src/utils/withMaxConcurrency.test.ts +0 -43
- package/src/utils/withMaxConcurrency.ts +0 -55
- package/src/utils/zodFormResolver.ts +0 -17
- package/v2-incompatible.js +0 -11
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
import {DownloadIcon} from '@sanity/icons'
|
|
2
|
-
import {Box, Button, Flex, Inline, Stack, Text} from '@sanity/ui'
|
|
3
|
-
import {format} from 'date-fns'
|
|
4
|
-
import filesize from 'filesize'
|
|
5
|
-
import {type ReactNode} from 'react'
|
|
6
|
-
|
|
7
|
-
import type {Asset, AssetItem} from '../../types'
|
|
8
|
-
import getAssetResolution from '../../utils/getAssetResolution'
|
|
9
|
-
import {isImageAsset} from '../../utils/typeGuards'
|
|
10
|
-
import ButtonAssetCopy from '../ButtonAssetCopy'
|
|
11
|
-
|
|
12
|
-
type Props = {
|
|
13
|
-
asset: Asset
|
|
14
|
-
item?: AssetItem
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
const Row = ({label, value}: {label: string; value: ReactNode}) => {
|
|
18
|
-
return (
|
|
19
|
-
<Flex justify="space-between">
|
|
20
|
-
<Text
|
|
21
|
-
size={1}
|
|
22
|
-
style={{
|
|
23
|
-
opacity: 0.8,
|
|
24
|
-
width: '40%',
|
|
25
|
-
}}
|
|
26
|
-
textOverflow="ellipsis"
|
|
27
|
-
>
|
|
28
|
-
{label}
|
|
29
|
-
</Text>
|
|
30
|
-
<Text
|
|
31
|
-
size={1}
|
|
32
|
-
style={{
|
|
33
|
-
opacity: 0.4,
|
|
34
|
-
textAlign: 'right',
|
|
35
|
-
width: '60%',
|
|
36
|
-
}}
|
|
37
|
-
textOverflow="ellipsis"
|
|
38
|
-
>
|
|
39
|
-
{value}
|
|
40
|
-
</Text>
|
|
41
|
-
</Flex>
|
|
42
|
-
)
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
const AssetMetadata = (props: Props) => {
|
|
46
|
-
const {asset, item} = props
|
|
47
|
-
|
|
48
|
-
const exif = asset?.metadata?.exif
|
|
49
|
-
|
|
50
|
-
// Callbacks
|
|
51
|
-
const handleDownload = () => {
|
|
52
|
-
window.location.href = `${asset.url}?dl=${asset.originalFilename}`
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
return (
|
|
56
|
-
<Box marginTop={3}>
|
|
57
|
-
{/* Base */}
|
|
58
|
-
<Box>
|
|
59
|
-
<Stack space={3}>
|
|
60
|
-
<Row label="Size" value={filesize(asset?.size, {base: 10, round: 0})} />
|
|
61
|
-
<Row label="MIME type" value={asset?.mimeType} />
|
|
62
|
-
<Row label="Extension" value={asset?.extension?.toUpperCase()} />
|
|
63
|
-
{isImageAsset(asset) && <Row label="Dimensions" value={getAssetResolution(asset)} />}
|
|
64
|
-
</Stack>
|
|
65
|
-
</Box>
|
|
66
|
-
{/* EXIF */}
|
|
67
|
-
{exif &&
|
|
68
|
-
(exif.DateTimeOriginal ||
|
|
69
|
-
exif.FNumber ||
|
|
70
|
-
exif.FocalLength ||
|
|
71
|
-
exif.ExposureTime ||
|
|
72
|
-
exif.ISO) && (
|
|
73
|
-
<>
|
|
74
|
-
{/* Divider */}
|
|
75
|
-
<Box
|
|
76
|
-
marginY={4}
|
|
77
|
-
style={{
|
|
78
|
-
background: 'var(--card-border-color)',
|
|
79
|
-
height: '1px',
|
|
80
|
-
width: '100%',
|
|
81
|
-
}}
|
|
82
|
-
/>
|
|
83
|
-
<Box>
|
|
84
|
-
<Stack space={3}>
|
|
85
|
-
{exif.ISO && <Row label="ISO" value={exif.ISO} />}
|
|
86
|
-
{exif.FNumber && <Row label="Aperture" value={`ƒ/${exif.FNumber}`} />}
|
|
87
|
-
{exif.FocalLength && <Row label="Focal length" value={`${exif.FocalLength}mm`} />}
|
|
88
|
-
{exif.ExposureTime && (
|
|
89
|
-
<Row label="Exposure time" value={`1/${1 / exif.ExposureTime}`} />
|
|
90
|
-
)}
|
|
91
|
-
{exif.DateTimeOriginal && (
|
|
92
|
-
<Row
|
|
93
|
-
label="Original date"
|
|
94
|
-
value={format(new Date(exif.DateTimeOriginal), 'PPp')}
|
|
95
|
-
/>
|
|
96
|
-
)}
|
|
97
|
-
</Stack>
|
|
98
|
-
</Box>
|
|
99
|
-
</>
|
|
100
|
-
)}
|
|
101
|
-
|
|
102
|
-
{/* Asset actions */}
|
|
103
|
-
<Box marginTop={5}>
|
|
104
|
-
<Inline space={2}>
|
|
105
|
-
{/* Download */}
|
|
106
|
-
<Button
|
|
107
|
-
disabled={!item || item?.updating}
|
|
108
|
-
fontSize={1}
|
|
109
|
-
icon={DownloadIcon}
|
|
110
|
-
mode="ghost"
|
|
111
|
-
onClick={handleDownload}
|
|
112
|
-
text="Download"
|
|
113
|
-
/>
|
|
114
|
-
{/* Copy to clipboard */}
|
|
115
|
-
<ButtonAssetCopy disabled={!item || item?.updating} url={asset.url} />
|
|
116
|
-
</Inline>
|
|
117
|
-
</Box>
|
|
118
|
-
</Box>
|
|
119
|
-
)
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
export default AssetMetadata
|
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
import {Box} from '@sanity/ui'
|
|
2
|
-
import {memo} from 'react'
|
|
3
|
-
import {GroupedVirtuoso} from 'react-virtuoso'
|
|
4
|
-
|
|
5
|
-
import useTypedSelector from '../../hooks/useTypedSelector'
|
|
6
|
-
import type {CardAssetData, CardUploadData} from '../../types'
|
|
7
|
-
import TableHeader from '../TableHeader'
|
|
8
|
-
import TableRowAsset from '../TableRowAsset'
|
|
9
|
-
import TableRowUpload from '../TableRowUpload'
|
|
10
|
-
|
|
11
|
-
type Props = {
|
|
12
|
-
items: (CardAssetData | CardUploadData)[]
|
|
13
|
-
onLoadMore?: () => void
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
const VirtualRow = memo(
|
|
17
|
-
({item, selected}: {item: CardAssetData | CardUploadData; selected: boolean}) => {
|
|
18
|
-
if (item?.type === 'asset') {
|
|
19
|
-
return (
|
|
20
|
-
<Box style={{height: '100px'}}>
|
|
21
|
-
<TableRowAsset id={item.id} selected={selected} />
|
|
22
|
-
</Box>
|
|
23
|
-
)
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
if (item?.type === 'upload') {
|
|
27
|
-
return (
|
|
28
|
-
<Box style={{height: '100px'}}>
|
|
29
|
-
<TableRowUpload id={item.id} />
|
|
30
|
-
</Box>
|
|
31
|
-
)
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
return null
|
|
35
|
-
},
|
|
36
|
-
)
|
|
37
|
-
|
|
38
|
-
const AssetTableVirtualized = (props: Props) => {
|
|
39
|
-
const {items, onLoadMore} = props
|
|
40
|
-
|
|
41
|
-
// Redux
|
|
42
|
-
const selectedAssets = useTypedSelector((state) => state.selected.assets)
|
|
43
|
-
|
|
44
|
-
const selectedIds = (selectedAssets && selectedAssets.map((asset) => asset._id)) || []
|
|
45
|
-
const totalCount = items?.length
|
|
46
|
-
|
|
47
|
-
if (totalCount === 0) {
|
|
48
|
-
return null
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
return (
|
|
52
|
-
<GroupedVirtuoso
|
|
53
|
-
className="media__custom-scrollbar"
|
|
54
|
-
computeItemKey={(index) => {
|
|
55
|
-
const item = items[index]
|
|
56
|
-
return item?.id || index
|
|
57
|
-
}}
|
|
58
|
-
endReached={onLoadMore}
|
|
59
|
-
groupCounts={Array(1).fill(totalCount)}
|
|
60
|
-
groupContent={() => {
|
|
61
|
-
return <TableHeader />
|
|
62
|
-
}}
|
|
63
|
-
itemContent={(index) => {
|
|
64
|
-
const item = items[index]
|
|
65
|
-
const selected = selectedIds.includes(item?.id || '')
|
|
66
|
-
return <VirtualRow item={item!} selected={selected} />
|
|
67
|
-
}}
|
|
68
|
-
style={{overflowX: 'hidden'}}
|
|
69
|
-
/>
|
|
70
|
-
)
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
export default AssetTableVirtualized
|
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
import {useToast} from '@sanity/ui'
|
|
2
|
-
import {useEffect, useRef} from 'react'
|
|
3
|
-
import {type InputProps} from 'sanity'
|
|
4
|
-
|
|
5
|
-
import {useToolOptions} from '../../contexts/ToolOptionsContext'
|
|
6
|
-
import useVersionedClient from '../../hooks/useVersionedClient'
|
|
7
|
-
import {applyMediaTags} from '../../utils/applyMediaTags'
|
|
8
|
-
|
|
9
|
-
type AssetValue = {
|
|
10
|
-
_type: 'image' | 'file'
|
|
11
|
-
asset?: {
|
|
12
|
-
_ref: string
|
|
13
|
-
_type: 'reference'
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export type AutoTagInputProps = InputProps & {
|
|
18
|
-
mediaTags?: string[]
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Input component that automatically applies media tags when an asset is selected or uploaded.
|
|
23
|
-
*
|
|
24
|
-
* Apply explicitly to image/file fields that should be auto-tagged:
|
|
25
|
-
* ```ts
|
|
26
|
-
* import {AutoTagInput} from 'sanity-plugin-media'
|
|
27
|
-
*
|
|
28
|
-
* defineField({
|
|
29
|
-
* type: 'image',
|
|
30
|
-
* options: { mediaTags: ['product'] }, // also pre-filters the media browser
|
|
31
|
-
* components: { input: AutoTagInput },
|
|
32
|
-
* })
|
|
33
|
-
* ```
|
|
34
|
-
*
|
|
35
|
-
* Pass `mediaTags` as a prop to override or use without `options`:
|
|
36
|
-
* ```ts
|
|
37
|
-
* components: { input: (props) => <AutoTagInput {...props} mediaTags={['product']} /> }
|
|
38
|
-
* ```
|
|
39
|
-
*/
|
|
40
|
-
export function AutoTagInput(props: AutoTagInputProps) {
|
|
41
|
-
const {renderDefault, schemaType, value, mediaTags: mediaTagsProp} = props
|
|
42
|
-
const toast = useToast()
|
|
43
|
-
|
|
44
|
-
// Prop takes precedence; fall back to schemaType.options.mediaTags (set for browser pre-filtering)
|
|
45
|
-
const mediaTags =
|
|
46
|
-
mediaTagsProp ?? (schemaType?.options as {mediaTags?: string[]} | undefined)?.mediaTags
|
|
47
|
-
|
|
48
|
-
const client = useVersionedClient()
|
|
49
|
-
const {createTagsOnUpload} = useToolOptions()
|
|
50
|
-
|
|
51
|
-
const prevAssetRef = useRef<string | undefined>(undefined)
|
|
52
|
-
const isInitialMount = useRef(true)
|
|
53
|
-
|
|
54
|
-
const currentAssetRef = (value as AssetValue | undefined)?.asset?._ref
|
|
55
|
-
|
|
56
|
-
useEffect(() => {
|
|
57
|
-
if (isInitialMount.current) {
|
|
58
|
-
isInitialMount.current = false
|
|
59
|
-
prevAssetRef.current = currentAssetRef
|
|
60
|
-
return
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
const previousRef = prevAssetRef.current
|
|
64
|
-
prevAssetRef.current = currentAssetRef
|
|
65
|
-
|
|
66
|
-
if (!mediaTags?.length || !currentAssetRef || currentAssetRef === previousRef) return
|
|
67
|
-
|
|
68
|
-
applyMediaTags({
|
|
69
|
-
client,
|
|
70
|
-
assetId: currentAssetRef,
|
|
71
|
-
mediaTags,
|
|
72
|
-
createTagsOnUpload,
|
|
73
|
-
}).catch((err) => {
|
|
74
|
-
console.error('[sanity-plugin-media] Failed to apply auto-tags:', err)
|
|
75
|
-
const label = mediaTags.length === 1 ? 'tag' : 'tags'
|
|
76
|
-
toast.push({
|
|
77
|
-
closable: true,
|
|
78
|
-
status: 'error',
|
|
79
|
-
title: `Failed to apply the media ${label} ${mediaTags.join(', ')}`,
|
|
80
|
-
})
|
|
81
|
-
})
|
|
82
|
-
}, [currentAssetRef, mediaTags, client, createTagsOnUpload, toast])
|
|
83
|
-
|
|
84
|
-
return renderDefault(props as InputProps)
|
|
85
|
-
}
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
import {studioTheme, ThemeProvider, ToastProvider} from '@sanity/ui'
|
|
2
|
-
import {render, screen, waitFor} from '@testing-library/react'
|
|
3
|
-
import {of} from 'rxjs'
|
|
4
|
-
import {ColorSchemeProvider} from 'sanity'
|
|
5
|
-
import {describe, expect, it, vi, beforeEach} from 'vitest'
|
|
6
|
-
|
|
7
|
-
import {createListenMock} from '../../__tests__/fixtures/listenMock'
|
|
8
|
-
import {createMockSanityClient} from '../../__tests__/fixtures/mockSanityClient'
|
|
9
|
-
import {ToolOptionsProvider} from '../../contexts/ToolOptionsContext'
|
|
10
|
-
import useVersionedClient from '../../hooks/useVersionedClient'
|
|
11
|
-
import Browser from './index'
|
|
12
|
-
|
|
13
|
-
vi.mock('../../hooks/useVersionedClient', () => ({
|
|
14
|
-
default: vi.fn(),
|
|
15
|
-
}))
|
|
16
|
-
|
|
17
|
-
describe('Browser', () => {
|
|
18
|
-
beforeEach(() => {
|
|
19
|
-
const fetch = vi.fn().mockReturnValue(of({items: []}))
|
|
20
|
-
vi.mocked(useVersionedClient).mockReturnValue(
|
|
21
|
-
createMockSanityClient({
|
|
22
|
-
listen: createListenMock(),
|
|
23
|
-
observable: {fetch},
|
|
24
|
-
}),
|
|
25
|
-
)
|
|
26
|
-
})
|
|
27
|
-
|
|
28
|
-
it('renders Browse Assets header in tool mode', async () => {
|
|
29
|
-
render(
|
|
30
|
-
<ColorSchemeProvider scheme="light">
|
|
31
|
-
<ThemeProvider theme={studioTheme}>
|
|
32
|
-
<ToastProvider>
|
|
33
|
-
<ToolOptionsProvider options={{creditLine: {enabled: false}}}>
|
|
34
|
-
<Browser />
|
|
35
|
-
</ToolOptionsProvider>
|
|
36
|
-
</ToastProvider>
|
|
37
|
-
</ThemeProvider>
|
|
38
|
-
</ColorSchemeProvider>,
|
|
39
|
-
)
|
|
40
|
-
|
|
41
|
-
await waitFor(() => {
|
|
42
|
-
expect(screen.getByText('Browse Assets')).toBeInTheDocument()
|
|
43
|
-
})
|
|
44
|
-
})
|
|
45
|
-
})
|
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
import {Card, Flex, PortalProvider} from '@sanity/ui'
|
|
2
|
-
import {useState} from 'react'
|
|
3
|
-
import {type AssetSourceComponentProps, type SanityDocument} from 'sanity'
|
|
4
|
-
|
|
5
|
-
import {AssetBrowserDispatchProvider} from '../../contexts/AssetSourceDispatchContext'
|
|
6
|
-
import useVersionedClient from '../../hooks/useVersionedClient'
|
|
7
|
-
import GlobalStyle from '../../styled/GlobalStyles'
|
|
8
|
-
import Controls from '../Controls'
|
|
9
|
-
import DebugControls from '../DebugControls'
|
|
10
|
-
import Dialogs from '../Dialogs'
|
|
11
|
-
import Header from '../Header'
|
|
12
|
-
import Items from '../Items'
|
|
13
|
-
import Notifications from '../Notifications'
|
|
14
|
-
import PickedBar from '../PickedBar'
|
|
15
|
-
import ReduxProvider from '../ReduxProvider'
|
|
16
|
-
import TagsPanel from '../TagsPanel'
|
|
17
|
-
import UploadDropzone from '../UploadDropzone'
|
|
18
|
-
import {useBrowserInit} from './useBrowserInit'
|
|
19
|
-
|
|
20
|
-
type Props = {
|
|
21
|
-
assetType?: AssetSourceComponentProps['assetType']
|
|
22
|
-
document?: SanityDocument
|
|
23
|
-
onClose?: AssetSourceComponentProps['onClose']
|
|
24
|
-
onSelect?: AssetSourceComponentProps['onSelect']
|
|
25
|
-
selectedAssets?: AssetSourceComponentProps['selectedAssets']
|
|
26
|
-
schemaType?: AssetSourceComponentProps['schemaType']
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const BrowserContent = ({
|
|
30
|
-
onClose,
|
|
31
|
-
schemaType,
|
|
32
|
-
}: {
|
|
33
|
-
onClose?: AssetSourceComponentProps['onClose']
|
|
34
|
-
schemaType?: AssetSourceComponentProps['schemaType']
|
|
35
|
-
}) => {
|
|
36
|
-
const client = useVersionedClient()
|
|
37
|
-
const [portalElement, setPortalElement] = useState<HTMLDivElement | null>(null)
|
|
38
|
-
|
|
39
|
-
useBrowserInit(client, schemaType)
|
|
40
|
-
|
|
41
|
-
return (
|
|
42
|
-
<PortalProvider element={portalElement}>
|
|
43
|
-
<UploadDropzone>
|
|
44
|
-
<Dialogs />
|
|
45
|
-
<Notifications />
|
|
46
|
-
|
|
47
|
-
<Card display="flex" height="fill" ref={setPortalElement}>
|
|
48
|
-
<Flex direction="column" flex={1}>
|
|
49
|
-
{/* Header */}
|
|
50
|
-
<Header onClose={onClose} />
|
|
51
|
-
|
|
52
|
-
{/* Browser Controls */}
|
|
53
|
-
<Controls />
|
|
54
|
-
|
|
55
|
-
<Flex flex={1}>
|
|
56
|
-
<Flex align="flex-end" direction="column" flex={1} style={{position: 'relative'}}>
|
|
57
|
-
<PickedBar />
|
|
58
|
-
<Items />
|
|
59
|
-
</Flex>
|
|
60
|
-
<TagsPanel />
|
|
61
|
-
</Flex>
|
|
62
|
-
|
|
63
|
-
{/* Debug */}
|
|
64
|
-
<DebugControls />
|
|
65
|
-
</Flex>
|
|
66
|
-
</Card>
|
|
67
|
-
</UploadDropzone>
|
|
68
|
-
</PortalProvider>
|
|
69
|
-
)
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const Browser = (props: Props) => {
|
|
73
|
-
const client = useVersionedClient()
|
|
74
|
-
|
|
75
|
-
return (
|
|
76
|
-
<ReduxProvider
|
|
77
|
-
assetType={props?.assetType}
|
|
78
|
-
client={client}
|
|
79
|
-
document={props?.document}
|
|
80
|
-
selectedAssets={props?.selectedAssets}
|
|
81
|
-
>
|
|
82
|
-
<AssetBrowserDispatchProvider onSelect={props?.onSelect}>
|
|
83
|
-
<GlobalStyle />
|
|
84
|
-
<BrowserContent onClose={props?.onClose} schemaType={props?.schemaType} />
|
|
85
|
-
</AssetBrowserDispatchProvider>
|
|
86
|
-
</ReduxProvider>
|
|
87
|
-
)
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
export default Browser
|
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
import type {MutationEvent, SanityClient} from '@sanity/client'
|
|
2
|
-
import groq from 'groq'
|
|
3
|
-
import {useEffect} from 'react'
|
|
4
|
-
import {useDispatch, useSelector} from 'react-redux'
|
|
5
|
-
import type {Dispatch} from 'redux'
|
|
6
|
-
import type {AssetSourceComponentProps} from 'sanity'
|
|
7
|
-
|
|
8
|
-
import {inputs} from '../../config/searchFacets'
|
|
9
|
-
import {TAG_DOCUMENT_NAME} from '../../constants'
|
|
10
|
-
import {assetsActions} from '../../modules/assets'
|
|
11
|
-
import {searchActions} from '../../modules/search'
|
|
12
|
-
import {tagsActions} from '../../modules/tags'
|
|
13
|
-
import type {RootReducerState} from '../../modules/types'
|
|
14
|
-
import type {Asset, Tag} from '../../types'
|
|
15
|
-
|
|
16
|
-
function getMediaTagNames(schemaType?: AssetSourceComponentProps['schemaType']): string[] {
|
|
17
|
-
const mediaTags = (schemaType?.options as {mediaTags?: string[]} | undefined)?.mediaTags
|
|
18
|
-
if (!mediaTags?.length) return []
|
|
19
|
-
const unique = new Set(
|
|
20
|
-
mediaTags.map((t) => t?.trim()).filter((t): t is string => Boolean(t?.length)),
|
|
21
|
-
)
|
|
22
|
-
return Array.from(unique)
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function createAssetHandler(dispatch: Dispatch) {
|
|
26
|
-
return (update: MutationEvent) => {
|
|
27
|
-
const {documentId, result, transition} = update
|
|
28
|
-
|
|
29
|
-
switch (transition) {
|
|
30
|
-
case 'appear':
|
|
31
|
-
dispatch(assetsActions.listenerCreateQueue({asset: result as Asset}))
|
|
32
|
-
break
|
|
33
|
-
case 'disappear':
|
|
34
|
-
dispatch(assetsActions.listenerDeleteQueue({assetId: documentId}))
|
|
35
|
-
break
|
|
36
|
-
case 'update':
|
|
37
|
-
dispatch(assetsActions.listenerUpdateQueue({asset: result as Asset}))
|
|
38
|
-
break
|
|
39
|
-
default:
|
|
40
|
-
break
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function createTagHandler(dispatch: Dispatch) {
|
|
46
|
-
return (update: MutationEvent) => {
|
|
47
|
-
const {documentId, result, transition} = update
|
|
48
|
-
|
|
49
|
-
switch (transition) {
|
|
50
|
-
case 'appear':
|
|
51
|
-
dispatch(tagsActions.listenerCreateQueue({tag: result as Tag}))
|
|
52
|
-
break
|
|
53
|
-
case 'disappear':
|
|
54
|
-
dispatch(tagsActions.listenerDeleteQueue({tagId: documentId}))
|
|
55
|
-
break
|
|
56
|
-
case 'update':
|
|
57
|
-
dispatch(tagsActions.listenerUpdateQueue({tag: result as Tag}))
|
|
58
|
-
break
|
|
59
|
-
default:
|
|
60
|
-
break
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export function useBrowserInit(
|
|
66
|
-
client: SanityClient,
|
|
67
|
-
schemaType?: AssetSourceComponentProps['schemaType'],
|
|
68
|
-
): void {
|
|
69
|
-
const dispatch = useDispatch()
|
|
70
|
-
const tagsByIds = useSelector((state: RootReducerState) => state.tags.byIds)
|
|
71
|
-
const tagsFetchCount = useSelector((state: RootReducerState) => state.tags.fetchCount)
|
|
72
|
-
|
|
73
|
-
const tagNames = getMediaTagNames(schemaType)
|
|
74
|
-
const hasMediaTags = tagNames.length > 0
|
|
75
|
-
|
|
76
|
-
useEffect(() => {
|
|
77
|
-
if (!hasMediaTags) {
|
|
78
|
-
dispatch(searchActions.facetsClear())
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
dispatch(tagsActions.fetchRequest())
|
|
82
|
-
|
|
83
|
-
const assetSubscription = client
|
|
84
|
-
.listen(
|
|
85
|
-
groq`*[_type in ["sanity.fileAsset", "sanity.imageAsset"] && !(_id in path("drafts.**"))]`,
|
|
86
|
-
)
|
|
87
|
-
.subscribe(createAssetHandler(dispatch))
|
|
88
|
-
|
|
89
|
-
const tagSubscription = client
|
|
90
|
-
.listen(groq`*[_type == "${TAG_DOCUMENT_NAME}" && !(_id in path("drafts.**"))]`)
|
|
91
|
-
.subscribe(createTagHandler(dispatch))
|
|
92
|
-
|
|
93
|
-
return () => {
|
|
94
|
-
assetSubscription.unsubscribe()
|
|
95
|
-
tagSubscription.unsubscribe()
|
|
96
|
-
}
|
|
97
|
-
}, [client, dispatch, hasMediaTags])
|
|
98
|
-
|
|
99
|
-
// When mediaTags are configured, wait for the tag fetch to complete then apply facets.
|
|
100
|
-
// Dispatching clear + add synchronously keeps all actions within assetsSearchEpic's
|
|
101
|
-
// 400ms debounce window, so the browser performs exactly one asset fetch on open.
|
|
102
|
-
useEffect(() => {
|
|
103
|
-
if (!hasMediaTags || tagsFetchCount < 0) return
|
|
104
|
-
|
|
105
|
-
const tagFacetInput = inputs.tag
|
|
106
|
-
if (tagFacetInput.type !== 'searchable') return
|
|
107
|
-
|
|
108
|
-
const resolvedTags = tagNames
|
|
109
|
-
.map((name) => Object.values(tagsByIds).find((item) => item.tag.name.current === name))
|
|
110
|
-
.filter((item): item is NonNullable<typeof item> => Boolean(item))
|
|
111
|
-
|
|
112
|
-
dispatch(searchActions.facetsClear())
|
|
113
|
-
|
|
114
|
-
for (const tagItem of resolvedTags) {
|
|
115
|
-
dispatch(
|
|
116
|
-
searchActions.facetsAdd({
|
|
117
|
-
facet: {
|
|
118
|
-
...tagFacetInput,
|
|
119
|
-
operatorType: 'references',
|
|
120
|
-
value: {label: tagItem.tag.name.current, value: tagItem.tag._id},
|
|
121
|
-
},
|
|
122
|
-
}),
|
|
123
|
-
)
|
|
124
|
-
}
|
|
125
|
-
}, [tagsFetchCount, hasMediaTags]) // eslint-disable-line react-hooks/exhaustive-deps
|
|
126
|
-
}
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
import {ClipboardIcon} from '@sanity/icons'
|
|
2
|
-
import {Button, Popover, Text} from '@sanity/ui'
|
|
3
|
-
import copy from 'copy-to-clipboard'
|
|
4
|
-
import {useEffect, useRef, useState} from 'react'
|
|
5
|
-
|
|
6
|
-
import {usePortalPopoverProps} from '../../hooks/usePortalPopoverProps'
|
|
7
|
-
|
|
8
|
-
type Props = {
|
|
9
|
-
disabled?: boolean
|
|
10
|
-
url: string
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
const ButtonAssetCopy = ({disabled, url}: Props) => {
|
|
14
|
-
const popoverProps = usePortalPopoverProps()
|
|
15
|
-
const refPopoverTimeout = useRef<ReturnType<typeof window.setTimeout>>(null)
|
|
16
|
-
const [popoverVisible, setPopoverVisible] = useState(false)
|
|
17
|
-
|
|
18
|
-
const handleClick = () => {
|
|
19
|
-
if (refPopoverTimeout.current) {
|
|
20
|
-
clearTimeout(refPopoverTimeout.current)
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
setPopoverVisible(true)
|
|
24
|
-
copy(url)
|
|
25
|
-
|
|
26
|
-
refPopoverTimeout.current = setTimeout(() => {
|
|
27
|
-
setPopoverVisible(false)
|
|
28
|
-
}, 1250)
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
// Effects
|
|
32
|
-
useEffect(() => {
|
|
33
|
-
return () => {
|
|
34
|
-
if (refPopoverTimeout.current) {
|
|
35
|
-
clearTimeout(refPopoverTimeout.current)
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
}, [])
|
|
39
|
-
|
|
40
|
-
return (
|
|
41
|
-
<Popover
|
|
42
|
-
content={
|
|
43
|
-
<Text muted size={1}>
|
|
44
|
-
Copied!
|
|
45
|
-
</Text>
|
|
46
|
-
}
|
|
47
|
-
open={popoverVisible}
|
|
48
|
-
padding={2}
|
|
49
|
-
placement="top"
|
|
50
|
-
radius={1}
|
|
51
|
-
{...popoverProps}
|
|
52
|
-
>
|
|
53
|
-
<Button
|
|
54
|
-
disabled={disabled}
|
|
55
|
-
fontSize={1}
|
|
56
|
-
icon={ClipboardIcon}
|
|
57
|
-
mode="ghost"
|
|
58
|
-
onClick={handleClick}
|
|
59
|
-
text="Copy URL"
|
|
60
|
-
/>
|
|
61
|
-
</Popover>
|
|
62
|
-
)
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export default ButtonAssetCopy
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import {ThLargeIcon, ThListIcon} from '@sanity/icons'
|
|
2
|
-
import {Button, Inline} from '@sanity/ui'
|
|
3
|
-
import {useDispatch} from 'react-redux'
|
|
4
|
-
|
|
5
|
-
import useTypedSelector from '../../hooks/useTypedSelector'
|
|
6
|
-
import {assetsActions} from '../../modules/assets'
|
|
7
|
-
|
|
8
|
-
const ButtonViewGroup = () => {
|
|
9
|
-
// Redux
|
|
10
|
-
const dispatch = useDispatch()
|
|
11
|
-
const view = useTypedSelector((state) => state.assets.view)
|
|
12
|
-
|
|
13
|
-
return (
|
|
14
|
-
<Inline space={0} style={{whiteSpace: 'nowrap'}}>
|
|
15
|
-
<Button
|
|
16
|
-
fontSize={1}
|
|
17
|
-
icon={ThLargeIcon}
|
|
18
|
-
mode={view === 'grid' ? 'default' : 'ghost'}
|
|
19
|
-
onClick={() => dispatch(assetsActions.viewSet({view: 'grid'}))}
|
|
20
|
-
style={{
|
|
21
|
-
borderBottomRightRadius: 0,
|
|
22
|
-
borderTopRightRadius: 0,
|
|
23
|
-
}}
|
|
24
|
-
/>
|
|
25
|
-
<Button
|
|
26
|
-
fontSize={1}
|
|
27
|
-
icon={ThListIcon}
|
|
28
|
-
mode={view === 'table' ? 'default' : 'ghost'}
|
|
29
|
-
onClick={() => dispatch(assetsActions.viewSet({view: 'table'}))}
|
|
30
|
-
style={{
|
|
31
|
-
borderBottomLeftRadius: 0,
|
|
32
|
-
borderTopLeftRadius: 0,
|
|
33
|
-
}}
|
|
34
|
-
/>
|
|
35
|
-
</Inline>
|
|
36
|
-
)
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export default ButtonViewGroup
|