sanity-plugin-media 2.1.1 → 2.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sanity-plugin-media",
3
- "version": "2.1.1",
3
+ "version": "2.2.0",
4
4
  "description": "This version of `sanity-plugin-media` is for Sanity Studio V3.",
5
5
  "keywords": [
6
6
  "sanity",
@@ -6,6 +6,7 @@ import groq from 'groq'
6
6
  import React, {ReactNode, useCallback, useEffect, useRef, useState} from 'react'
7
7
  import {SubmitHandler, useForm} from 'react-hook-form'
8
8
  import {useDispatch} from 'react-redux'
9
+ import {WithReferringDocuments, useDocumentStore} from 'sanity'
9
10
  import {assetFormSchema} from '../../formSchema'
10
11
  import useTypedSelector from '../../hooks/useTypedSelector'
11
12
  import useVersionedClient from '../../hooks/useVersionedClient'
@@ -13,6 +14,7 @@ import {assetsActions, selectAssetById} from '../../modules/assets'
13
14
  import {dialogActions} from '../../modules/dialog'
14
15
  import {selectTags, selectTagSelectOptions, tagsActions} from '../../modules/tags'
15
16
  import getTagSelectOptions from '../../utils/getTagSelectOptions'
17
+ import {getUniqueDocuments} from '../../utils/getUniqueDocuments'
16
18
  import imageDprUrl from '../../utils/imageDprUrl'
17
19
  import sanitizeFormData from '../../utils/sanitizeFormData'
18
20
  import {isFileAsset, isImageAsset} from '../../utils/typeGuards'
@@ -39,6 +41,8 @@ const DialogAssetEdit = (props: Props) => {
39
41
 
40
42
  const client = useVersionedClient()
41
43
 
44
+ const documentStore = useDocumentStore()
45
+
42
46
  const dispatch = useDispatch()
43
47
  const assetItem = useTypedSelector(state => selectAssetById(state, String(assetId))) // TODO: check casting
44
48
  const tags = useTypedSelector(selectTags)
@@ -238,110 +242,128 @@ const DialogAssetEdit = (props: Props) => {
238
242
  */}
239
243
  <Flex direction={['column-reverse', 'column-reverse', 'row-reverse']}>
240
244
  <Box flex={1} marginTop={[5, 5, 0]} padding={4}>
241
- {/* Tabs */}
242
- <TabList space={2}>
243
- <Tab
244
- aria-controls="details-panel"
245
- disabled={formUpdating}
246
- id="details-tab"
247
- label="Details"
248
- onClick={() => setTabSection('details')}
249
- selected={tabSection === 'details'}
250
- size={2}
251
- />
252
- <Tab
253
- aria-controls="references-panel"
254
- disabled={formUpdating}
255
- id="references-tab"
256
- label="References"
257
- onClick={() => setTabSection('references')}
258
- selected={tabSection === 'references'}
259
- size={2}
260
- />
261
- </TabList>
262
-
263
- {/* Form fields */}
264
- <Box as="form" marginTop={4} onSubmit={handleSubmit(onSubmit)}>
265
- {/* Deleted notification */}
266
- {!assetItem && (
267
- <Card marginBottom={3} padding={3} radius={2} shadow={1} tone="critical">
268
- <Text size={1}>This file cannot be found – it may have been deleted.</Text>
269
- </Card>
270
- )}
271
-
272
- {/* Hidden button to enable enter key submissions */}
273
- <button style={{display: 'none'}} tabIndex={-1} type="submit" />
274
-
275
- {/* Panel: details */}
276
- <TabPanel
277
- aria-labelledby="details"
278
- hidden={tabSection !== 'details'}
279
- id="details-panel"
280
- >
281
- <Stack space={3}>
282
- {/* Tags */}
283
- <FormFieldInputTags
284
- control={control}
285
- disabled={formUpdating}
286
- error={errors?.opt?.media?.tags?.message}
287
- label="Tags"
288
- name="opt.media.tags"
289
- onCreateTag={handleCreateTag}
290
- options={allTagOptions}
291
- placeholder="Select or create..."
292
- value={assetTagOptions}
293
- />
294
- {/* Filename */}
295
- <FormFieldInputText
296
- {...register('originalFilename')}
297
- disabled={formUpdating}
298
- error={errors?.originalFilename?.message}
299
- label="Filename"
300
- name="originalFilename"
301
- value={currentAsset?.originalFilename}
302
- />
303
- {/* Title */}
304
- <FormFieldInputText
305
- {...register('title')}
306
- disabled={formUpdating}
307
- error={errors?.title?.message}
308
- label="Title"
309
- name="title"
310
- value={currentAsset?.title}
311
- />
312
- {/* Alt text */}
313
- <FormFieldInputText
314
- {...register('altText')}
315
- disabled={formUpdating}
316
- error={errors?.altText?.message}
317
- label="Alt Text"
318
- name="altText"
319
- value={currentAsset?.altText}
320
- />
321
- {/* Description */}
322
- <FormFieldInputTextarea
323
- {...register('description')}
324
- disabled={formUpdating}
325
- error={errors?.description?.message}
326
- label="Description"
327
- name="description"
328
- rows={3}
329
- value={currentAsset?.description}
330
- />
331
- </Stack>
332
- </TabPanel>
333
-
334
- {/* Panel: References */}
335
- <TabPanel
336
- aria-labelledby="references"
337
- hidden={tabSection !== 'references'}
338
- id="references-panel"
339
- >
340
- <Box marginTop={5}>
341
- {assetItem?.asset && <DocumentList assetId={assetItem?.asset._id} />}
342
- </Box>
343
- </TabPanel>
344
- </Box>
245
+ <WithReferringDocuments documentStore={documentStore} id={assetItem.asset._id}>
246
+ {({isLoading, referringDocuments}) => {
247
+ const uniqueReferringDocuments = getUniqueDocuments(referringDocuments)
248
+ return (
249
+ <>
250
+ {/* Tabs */}
251
+ <TabList space={2}>
252
+ <Tab
253
+ aria-controls="details-panel"
254
+ disabled={formUpdating}
255
+ id="details-tab"
256
+ label="Details"
257
+ onClick={() => setTabSection('details')}
258
+ selected={tabSection === 'details'}
259
+ size={2}
260
+ />
261
+ <Tab
262
+ aria-controls="references-panel"
263
+ disabled={formUpdating}
264
+ id="references-tab"
265
+ label={`References${
266
+ !isLoading && Array.isArray(uniqueReferringDocuments)
267
+ ? ` (${uniqueReferringDocuments.length})`
268
+ : ''
269
+ }`}
270
+ onClick={() => setTabSection('references')}
271
+ selected={tabSection === 'references'}
272
+ size={2}
273
+ />
274
+ </TabList>
275
+
276
+ {/* Form fields */}
277
+ <Box as="form" marginTop={4} onSubmit={handleSubmit(onSubmit)}>
278
+ {/* Deleted notification */}
279
+ {!assetItem && (
280
+ <Card marginBottom={3} padding={3} radius={2} shadow={1} tone="critical">
281
+ <Text size={1}>This file cannot be found – it may have been deleted.</Text>
282
+ </Card>
283
+ )}
284
+
285
+ {/* Hidden button to enable enter key submissions */}
286
+ <button style={{display: 'none'}} tabIndex={-1} type="submit" />
287
+
288
+ {/* Panel: details */}
289
+ <TabPanel
290
+ aria-labelledby="details"
291
+ hidden={tabSection !== 'details'}
292
+ id="details-panel"
293
+ >
294
+ <Stack space={3}>
295
+ {/* Tags */}
296
+ <FormFieldInputTags
297
+ control={control}
298
+ disabled={formUpdating}
299
+ error={errors?.opt?.media?.tags?.message}
300
+ label="Tags"
301
+ name="opt.media.tags"
302
+ onCreateTag={handleCreateTag}
303
+ options={allTagOptions}
304
+ placeholder="Select or create..."
305
+ value={assetTagOptions}
306
+ />
307
+ {/* Filename */}
308
+ <FormFieldInputText
309
+ {...register('originalFilename')}
310
+ disabled={formUpdating}
311
+ error={errors?.originalFilename?.message}
312
+ label="Filename"
313
+ name="originalFilename"
314
+ value={currentAsset?.originalFilename}
315
+ />
316
+ {/* Title */}
317
+ <FormFieldInputText
318
+ {...register('title')}
319
+ disabled={formUpdating}
320
+ error={errors?.title?.message}
321
+ label="Title"
322
+ name="title"
323
+ value={currentAsset?.title}
324
+ />
325
+ {/* Alt text */}
326
+ <FormFieldInputText
327
+ {...register('altText')}
328
+ disabled={formUpdating}
329
+ error={errors?.altText?.message}
330
+ label="Alt Text"
331
+ name="altText"
332
+ value={currentAsset?.altText}
333
+ />
334
+ {/* Description */}
335
+ <FormFieldInputTextarea
336
+ {...register('description')}
337
+ disabled={formUpdating}
338
+ error={errors?.description?.message}
339
+ label="Description"
340
+ name="description"
341
+ rows={3}
342
+ value={currentAsset?.description}
343
+ />
344
+ </Stack>
345
+ </TabPanel>
346
+
347
+ {/* Panel: References */}
348
+ <TabPanel
349
+ aria-labelledby="references"
350
+ hidden={tabSection !== 'references'}
351
+ id="references-panel"
352
+ >
353
+ <Box marginTop={5}>
354
+ {assetItem?.asset && (
355
+ <DocumentList
356
+ documents={uniqueReferringDocuments}
357
+ isLoading={isLoading}
358
+ />
359
+ )}
360
+ </Box>
361
+ </TabPanel>
362
+ </Box>
363
+ </>
364
+ )
365
+ }}
366
+ </WithReferringDocuments>
345
367
  </Box>
346
368
 
347
369
  <Box flex={1} padding={4}>
@@ -1,54 +1,37 @@
1
1
  import type {SanityDocument} from '@sanity/client'
2
2
  import {Box, Button, Card, Stack, Text} from '@sanity/ui'
3
3
  import React from 'react'
4
- import {Preview, SchemaType, useDocumentStore, useSchema, WithReferringDocuments} from 'sanity'
4
+ import {Preview, SchemaType, useSchema} from 'sanity'
5
5
  import {useIntentLink} from 'sanity/router'
6
6
 
7
7
  type Props = {
8
- assetId: string
8
+ documents: SanityDocument[]
9
+ isLoading: boolean
9
10
  }
10
11
 
11
- const DocumentList = (props: Props) => {
12
- const {assetId} = props
13
-
14
- const documentStore = useDocumentStore()
15
-
16
- return (
17
- <WithReferringDocuments documentStore={documentStore} id={assetId}>
18
- {({isLoading, referringDocuments}) => (
19
- <ReferringDocuments isLoading={isLoading} referringDocuments={referringDocuments} />
20
- )}
21
- </WithReferringDocuments>
22
- )
23
- }
24
-
25
- const ReferringDocuments = (props: {isLoading: boolean; referringDocuments: SanityDocument[]}) => {
26
- const {isLoading, referringDocuments} = props
27
-
12
+ const DocumentList = ({documents, isLoading}: Props) => {
28
13
  const schema = useSchema()
29
14
 
30
- const draftIds = referringDocuments.reduce(
31
- (acc: string[], doc: SanityDocument) =>
32
- doc._id.startsWith('drafts.') ? acc.concat(doc._id.slice(7)) : acc,
33
- []
34
- )
35
-
36
- const filteredDocuments: SanityDocument[] = referringDocuments.filter(
37
- (doc: SanityDocument) => !draftIds.includes(doc._id)
38
- )
39
-
40
15
  if (isLoading) {
41
- return <Text size={1}>Loading...</Text>
16
+ return (
17
+ <Text muted size={1}>
18
+ Loading...
19
+ </Text>
20
+ )
42
21
  }
43
22
 
44
- if (filteredDocuments.length === 0) {
45
- return <Text size={1}>No documents are referencing this asset</Text>
23
+ if (documents.length === 0) {
24
+ return (
25
+ <Text muted size={1}>
26
+ No documents are referencing this asset
27
+ </Text>
28
+ )
46
29
  }
47
30
 
48
31
  return (
49
32
  <Card flex={1} marginBottom={2} padding={2} radius={2} shadow={1}>
50
33
  <Stack space={2}>
51
- {filteredDocuments?.map(doc => (
34
+ {documents?.map(doc => (
52
35
  <ReferringDocument doc={doc} key={doc._id} schemaType={schema.get(doc._type)} />
53
36
  ))}
54
37
  </Stack>
@@ -93,6 +93,7 @@ const TableHeader = () => {
93
93
  <TableHeaderItem field="mimeType" title="MIME type" />
94
94
  <TableHeaderItem field="size" title="Size" />
95
95
  <TableHeaderItem field="_updatedAt" title="Last updated" />
96
+ <TableHeaderItem title="References" />
96
97
  <TableHeaderItem />
97
98
  </Grid>
98
99
  )
@@ -13,8 +13,9 @@ import {
13
13
  } from '@sanity/ui'
14
14
  import formatRelative from 'date-fns/formatRelative'
15
15
  import filesize from 'filesize'
16
- import React, {memo, MouseEvent, RefObject} from 'react'
16
+ import React, {memo, MouseEvent, RefObject, useCallback, useEffect, useRef, useState} from 'react'
17
17
  import {useDispatch} from 'react-redux'
18
+ import {WithReferringDocuments} from 'sanity'
18
19
  import styled, {css} from 'styled-components'
19
20
  import {GRID_TEMPLATE_COLUMNS} from '../../constants'
20
21
  import {useAssetSourceActions} from '../../contexts/AssetSourceDispatchContext'
@@ -27,6 +28,10 @@ import imageDprUrl from '../../utils/imageDprUrl'
27
28
  import {isFileAsset, isImageAsset} from '../../utils/typeGuards'
28
29
  import FileIcon from '../FileIcon'
29
30
  import Image from '../Image'
31
+ import {getUniqueDocuments} from '../../utils/getUniqueDocuments'
32
+
33
+ // Duration (ms) to wait before reference counts (and associated listeners) are rendered
34
+ const REFERENCE_COUNT_VISIBILITY_DELAY = 750
30
35
 
31
36
  type Props = {
32
37
  id: string
@@ -68,13 +73,15 @@ const StyledWarningIcon = styled(WarningFilledIcon)(({theme}) => {
68
73
  }
69
74
  })
70
75
 
76
+ // eslint-disable-next-line complexity
71
77
  const TableRowAsset = (props: Props) => {
72
78
  const {id, selected} = props
73
79
 
74
- // Refs
75
80
  const shiftPressed: RefObject<boolean> = useKeyPress('shift')
76
81
 
77
- // Redux
82
+ const [referenceCountVisible, setReferenceCountVisible] = useState(false)
83
+ const refCountVisibleTimeout = useRef<ReturnType<typeof window.setTimeout>>()
84
+
78
85
  const dispatch = useDispatch()
79
86
  const lastPicked = useTypedSelector(state => state.assets.lastPicked)
80
87
  const item = useTypedSelector(state => selectAssetById(state, id))
@@ -89,48 +96,61 @@ const TableRowAsset = (props: Props) => {
89
96
 
90
97
  const {onSelect} = useAssetSourceActions()
91
98
 
92
- // Short circuit if no asset is available
93
- if (!asset) {
94
- return null
95
- }
96
-
97
- // Callbacks
98
- const handleContextActionClick = (e: MouseEvent<HTMLDivElement>) => {
99
- e.stopPropagation()
99
+ const handleContextActionClick = useCallback(
100
+ (e: MouseEvent<HTMLDivElement>) => {
101
+ e.stopPropagation()
100
102
 
101
- if (onSelect) {
102
- dispatch(dialogActions.showAssetEdit({assetId: asset._id}))
103
- } else if (shiftPressed.current && !picked) {
104
- dispatch(assetsActions.pickRange({startId: lastPicked || asset._id, endId: asset._id}))
105
- } else {
106
- dispatch(assetsActions.pick({assetId: asset._id, picked: !picked}))
107
- }
108
- }
103
+ if (onSelect) {
104
+ dispatch(dialogActions.showAssetEdit({assetId: asset._id}))
105
+ } else if (shiftPressed.current && !picked) {
106
+ dispatch(assetsActions.pickRange({startId: lastPicked || asset._id, endId: asset._id}))
107
+ } else {
108
+ dispatch(assetsActions.pick({assetId: asset._id, picked: !picked}))
109
+ }
110
+ },
111
+ [asset._id, dispatch, lastPicked, onSelect, picked, shiftPressed]
112
+ )
109
113
 
110
- const handleClick = (e: MouseEvent<HTMLDivElement>) => {
111
- e.stopPropagation()
114
+ const handleClick = useCallback(
115
+ (e: MouseEvent<HTMLDivElement>) => {
116
+ e.stopPropagation()
112
117
 
113
- if (onSelect) {
114
- onSelect([
115
- {
116
- kind: 'assetDocumentId',
117
- value: asset._id
118
+ if (onSelect) {
119
+ onSelect([{kind: 'assetDocumentId', value: asset._id}])
120
+ } else if (shiftPressed.current) {
121
+ if (picked) {
122
+ dispatch(assetsActions.pick({assetId: asset._id, picked: !picked}))
123
+ } else {
124
+ dispatch(assetsActions.pickRange({startId: lastPicked || asset._id, endId: asset._id}))
118
125
  }
119
- ])
120
- } else if (shiftPressed.current) {
121
- if (picked) {
122
- dispatch(assetsActions.pick({assetId: asset._id, picked: !picked}))
123
126
  } else {
124
- dispatch(assetsActions.pickRange({startId: lastPicked || asset._id, endId: asset._id}))
127
+ dispatch(dialogActions.showAssetEdit({assetId: asset._id}))
125
128
  }
126
- } else {
127
- dispatch(dialogActions.showAssetEdit({assetId: asset._id}))
128
- }
129
- }
129
+ },
130
+ [asset._id, dispatch, lastPicked, onSelect, picked, shiftPressed]
131
+ )
130
132
 
131
133
  const opacityCell = updating ? 0.5 : 1
132
134
  const opacityPreview = selected || updating ? 0.1 : 1
133
135
 
136
+ // Display reference count after an initial delay to prevent over-eager fetching
137
+ useEffect(() => {
138
+ refCountVisibleTimeout.current = setTimeout(
139
+ () => setReferenceCountVisible(true),
140
+ REFERENCE_COUNT_VISIBILITY_DELAY
141
+ )
142
+ return () => {
143
+ if (refCountVisibleTimeout.current) {
144
+ clearTimeout(refCountVisibleTimeout.current)
145
+ }
146
+ }
147
+ }, [])
148
+
149
+ // Short circuit if no asset is available
150
+ if (!asset) {
151
+ return null
152
+ }
153
+
134
154
  return (
135
155
  <ContainerGrid
136
156
  onClick={selected ? undefined : handleClick}
@@ -310,12 +330,39 @@ const TableRowAsset = (props: Props) => {
310
330
  </Text>
311
331
  </Box>
312
332
 
333
+ {/* References */}
334
+ <Box
335
+ style={{
336
+ display: mediaIndex < 3 ? 'none' : 'block',
337
+ gridColumn: 8,
338
+ gridRow: 'auto',
339
+ opacity: opacityCell
340
+ }}
341
+ >
342
+ <Text muted size={1} style={{lineHeight: '2em'}} textOverflow="ellipsis">
343
+ {referenceCountVisible ? (
344
+ <WithReferringDocuments id={id}>
345
+ {({isLoading, referringDocuments}) => {
346
+ const uniqueDocuments = getUniqueDocuments(referringDocuments)
347
+ return isLoading ? (
348
+ <>-</>
349
+ ) : (
350
+ <>{Array.isArray(uniqueDocuments) ? uniqueDocuments.length : 0}</>
351
+ )
352
+ }}
353
+ </WithReferringDocuments>
354
+ ) : (
355
+ <>-</>
356
+ )}
357
+ </Text>
358
+ </Box>
359
+
313
360
  {/* Error */}
314
361
  <Flex
315
362
  align="center"
316
363
  justify="center"
317
364
  style={{
318
- gridColumn: mediaIndex < 3 ? 4 : 8,
365
+ gridColumn: mediaIndex < 3 ? 4 : 9,
319
366
  gridRowStart: '1',
320
367
  gridRowEnd: mediaIndex < 3 ? 'span 5' : 'auto',
321
368
  opacity: opacityCell
package/src/constants.ts CHANGED
@@ -67,7 +67,7 @@ export const FACETS: (SearchFacetDivider | SearchFacetGroup | SearchFacetInputPr
67
67
 
68
68
  export const GRID_TEMPLATE_COLUMNS = {
69
69
  SMALL: '3rem 100px auto 1.5rem',
70
- LARGE: '3rem 100px auto 5.5rem 5.5rem 3.5rem 8.5rem 2rem'
70
+ LARGE: '3rem 100px auto 5.5rem 5.5rem 3.5rem 8.5rem 4.75rem 2rem'
71
71
  }
72
72
  export const PANEL_HEIGHT = 32 // px
73
73
  export const TAG_DOCUMENT_NAME = 'media.tag'
@@ -0,0 +1,15 @@
1
+ import {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
+ }