@sanity/cross-dataset-duplicator 0.3.6 → 1.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 (60) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +53 -53
  3. package/lib/index.esm.js +2 -0
  4. package/lib/index.esm.js.map +1 -0
  5. package/lib/index.js +2 -21
  6. package/lib/index.js.map +1 -1
  7. package/lib/src/index.d.ts +20 -0
  8. package/package.json +64 -38
  9. package/sanity.json +2 -10
  10. package/src/actions/DuplicateToAction.tsx +33 -7
  11. package/src/components/CrossDatasetDuplicator.tsx +42 -46
  12. package/src/components/{DuplicatorTool.tsx → Duplicator.tsx} +183 -126
  13. package/src/components/DuplicatorQuery.tsx +36 -13
  14. package/src/components/DuplicatorWrapper.tsx +66 -0
  15. package/src/components/ResetSecret.tsx +9 -9
  16. package/src/components/SelectButtons.tsx +13 -9
  17. package/src/components/StatusBadge.tsx +13 -9
  18. package/src/helpers/clientConfig.ts +1 -1
  19. package/src/helpers/constants.ts +1 -1
  20. package/src/helpers/getDocumentsInArray.ts +28 -21
  21. package/src/helpers/index.ts +6 -10
  22. package/src/index.ts +36 -0
  23. package/src/tool/index.ts +14 -10
  24. package/src/types/index.ts +9 -9
  25. package/v2-incompatible.js +11 -0
  26. package/.babelrc +0 -3
  27. package/.eslintignore +0 -1
  28. package/config.dist.json +0 -4
  29. package/lib/actions/DuplicateToAction.js +0 -44
  30. package/lib/actions/DuplicateToAction.js.map +0 -1
  31. package/lib/actions/index.js +0 -29
  32. package/lib/actions/index.js.map +0 -1
  33. package/lib/components/CrossDatasetDuplicator.js +0 -81
  34. package/lib/components/CrossDatasetDuplicator.js.map +0 -1
  35. package/lib/components/DuplicatorQuery.js +0 -105
  36. package/lib/components/DuplicatorQuery.js.map +0 -1
  37. package/lib/components/DuplicatorTool.js +0 -503
  38. package/lib/components/DuplicatorTool.js.map +0 -1
  39. package/lib/components/Feedback.js +0 -23
  40. package/lib/components/Feedback.js.map +0 -1
  41. package/lib/components/ResetSecret.js +0 -34
  42. package/lib/components/ResetSecret.js.map +0 -1
  43. package/lib/components/SelectButtons.js +0 -84
  44. package/lib/components/SelectButtons.js.map +0 -1
  45. package/lib/components/StatusBadge.js +0 -85
  46. package/lib/components/StatusBadge.js.map +0 -1
  47. package/lib/helpers/clientConfig.js +0 -11
  48. package/lib/helpers/clientConfig.js.map +0 -1
  49. package/lib/helpers/constants.js +0 -9
  50. package/lib/helpers/constants.js.map +0 -1
  51. package/lib/helpers/getDocumentsInArray.js +0 -74
  52. package/lib/helpers/getDocumentsInArray.js.map +0 -1
  53. package/lib/helpers/index.js +0 -27
  54. package/lib/helpers/index.js.map +0 -1
  55. package/lib/tool/index.js +0 -18
  56. package/lib/tool/index.js.map +0 -1
  57. package/lib/types/index.js +0 -2
  58. package/lib/types/index.js.map +0 -1
  59. package/src/actions/index.ts +0 -22
  60. package/src/index.js +0 -7
@@ -1,8 +1,18 @@
1
1
  /* eslint-disable react/jsx-no-bind */
2
2
  import React, {useState, useEffect} from 'react'
3
+ import {
4
+ useClient,
5
+ Preview,
6
+ useSchema,
7
+ useWorkspaces,
8
+ WorkspaceSummary,
9
+ SanityDocument,
10
+ } from 'sanity'
11
+ // @ts-ignore
3
12
  import mapLimit from 'async/mapLimit'
13
+ // @ts-ignore
4
14
  import asyncify from 'async/asyncify'
5
- import {extract, extractWithPath} from '@sanity/mutator'
15
+ import {extractWithPath} from '@sanity/mutator'
6
16
  import {dset} from 'dset'
7
17
  import {
8
18
  Card,
@@ -15,77 +25,82 @@ import {
15
25
  Select,
16
26
  Flex,
17
27
  Checkbox,
28
+ CardTone,
29
+ useTheme,
18
30
  } from '@sanity/ui'
19
31
  import {ArrowRightIcon, SearchIcon, LaunchIcon} from '@sanity/icons'
20
- import sanityClient from 'part:@sanity/base/client'
21
- import Preview from 'part:@sanity/base/preview'
22
- import schema from 'part:@sanity/base/schema'
23
- import config from 'config:sanity'
32
+ import {SanityAssetDocument} from '@sanity/client'
33
+ import {isAssetId, isSanityFileAsset} from '@sanity/asset-utils'
24
34
 
25
- import {typeIsAsset, stickyStyles, createInitialMessage} from '../helpers'
35
+ import {stickyStyles, createInitialMessage} from '../helpers'
26
36
  import {getDocumentsInArray} from '../helpers/getDocumentsInArray'
27
37
  import SelectButtons from './SelectButtons'
28
- import StatusBadge from './StatusBadge'
38
+ import StatusBadge, {MessageTypes} from './StatusBadge'
29
39
  import Feedback from './Feedback'
30
- import {SanityDocument} from '../types'
31
40
  import {clientConfig} from '../helpers/clientConfig'
41
+ import {PluginConfig} from '../types'
32
42
 
33
- type DuplicatorToolProps = {
43
+ export type DuplicatorProps = {
34
44
  docs: SanityDocument[]
35
- draftIds: string[]
45
+ // TODO: Find out if this is even used?
46
+ // draftIds: string[]
36
47
  token: string
48
+ pluginConfig: PluginConfig
49
+ }
50
+
51
+ export type PayloadItem = {
52
+ doc: SanityDocument
53
+ include: boolean
54
+ status?: keyof MessageTypes
55
+ hasDraft?: boolean
56
+ }
57
+
58
+ type WorkspaceOption = WorkspaceSummary & {
59
+ disabled: boolean
60
+ }
61
+
62
+ type Message = {
63
+ text: string
64
+ tone: CardTone
37
65
  }
38
66
 
39
- export default function DuplicatorTool(props: DuplicatorToolProps) {
40
- const {docs, draftIds, token} = props
67
+ export default function Duplicator(props: DuplicatorProps) {
68
+ const {docs, token, pluginConfig} = props
69
+ const isDarkMode = useTheme().sanity.color.dark
41
70
 
42
71
  // Prepare origin (this Studio) client
43
- // In function-scope so it is up to date on every render
44
- const originClient = sanityClient.withConfig(clientConfig)
72
+ const originClient = useClient(clientConfig)
73
+
74
+ const schema = useSchema()
45
75
 
46
76
  // Create list of dataset options
47
77
  // and set initial value of dropdown
48
- const spacesOptions = config?.__experimental_spaces?.length
49
- ? config.__experimental_spaces.map((space) => ({
50
- ...space,
51
- api: {
52
- ...space.api,
53
- projectId: space.api.projectId || process.env.SANITY_STUDIO_API_PROJECT_ID,
54
- },
55
- usingEnvForProjectId: !space.api.projectId && process.env.SANITY_STUDIO_API_PROJECT_ID,
56
- disabled:
57
- space.api.dataset === originClient.config().dataset &&
58
- space.api.projectId === originClient.config().projectId,
59
- }))
60
- : []
61
-
62
- const [destination, setDestination] = useState(
63
- spacesOptions.length ? spacesOptions.find((space) => !space.disabled) : {}
64
- )
65
- const [message, setMessage] = useState({})
66
- const [payload, setPayload] = useState(
67
- docs.length
68
- ? docs.map((item) => ({
69
- doc: item,
70
- include: true,
71
- status: null,
72
- hasDraft: draftIds?.length ? draftIds.includes(`drafts.${item._id}`) : false,
73
- }))
74
- : []
78
+ const workspaces = useWorkspaces()
79
+ const workspacesOptions: WorkspaceOption[] = workspaces.map((workspace) => ({
80
+ ...workspace,
81
+ disabled: workspace.dataset === originClient.config().dataset,
82
+ }))
83
+
84
+ const [destination, setDestination] = useState<WorkspaceOption | null>(
85
+ workspaces.length ? workspacesOptions.find((space) => !space.disabled) ?? null : null
75
86
  )
87
+ const [message, setMessage] = useState<Message | null>(null)
88
+ const [payload, setPayload] = useState<PayloadItem[]>([])
89
+
76
90
  const [hasReferences, setHasReferences] = useState(false)
77
91
  const [isDuplicating, setIsDuplicating] = useState(false)
78
92
  const [isGathering, setIsGathering] = useState(false)
79
- const [progress, setProgress] = useState([0, 0])
93
+ const [progress, setProgress] = useState<number[]>([0, 0])
80
94
 
81
95
  // Check for References and update message
82
96
  useEffect(() => {
83
97
  const expr = `.._ref`
84
98
  const initialRefs = []
85
- const initialPayload = []
99
+ const initialPayload: PayloadItem[] = []
86
100
 
87
101
  docs.forEach((doc) => {
88
- initialRefs.push(...extract(expr, doc))
102
+ const refs = extractWithPath(expr, doc).map((ref) => ref.value)
103
+ initialRefs.push(...refs)
89
104
  initialPayload.push({include: true, doc})
90
105
  })
91
106
 
@@ -108,10 +123,10 @@ export default function DuplicatorTool(props: DuplicatorToolProps) {
108
123
  // (On initial render + select change)
109
124
  useEffect(() => {
110
125
  updatePayloadStatuses()
111
- }, [destination, docs])
126
+ }, [destination])
112
127
 
113
128
  // Check if payload documents exist at destination
114
- async function updatePayloadStatuses(newPayload = []) {
129
+ async function updatePayloadStatuses(newPayload: PayloadItem[] = []) {
115
130
  const payloadActual = newPayload.length ? newPayload : payload
116
131
 
117
132
  if (!payloadActual.length || !destination?.name) {
@@ -119,12 +134,12 @@ export default function DuplicatorTool(props: DuplicatorToolProps) {
119
134
  }
120
135
 
121
136
  const payloadIds = payloadActual.map(({doc}) => doc._id)
122
- const destinationClient = sanityClient.withConfig({
137
+ const destinationClient = originClient.withConfig({
123
138
  ...clientConfig,
124
- dataset: destination.api.dataset,
125
- projectId: destination.api.projectId,
139
+ dataset: destination.dataset,
140
+ projectId: destination.projectId,
126
141
  })
127
- const destinationData = await destinationClient.fetch(
142
+ const destinationData: SanityDocument[] = await destinationClient.fetch(
128
143
  `*[_id in $payloadIds]{ _id, _updatedAt }`,
129
144
  {payloadIds}
130
145
  )
@@ -155,7 +170,7 @@ export default function DuplicatorTool(props: DuplicatorToolProps) {
155
170
  setPayload(updatedPayload)
156
171
  }
157
172
 
158
- function handleCheckbox(_id) {
173
+ function handleCheckbox(_id: string) {
159
174
  const updatedPayload = payload.map((item) => {
160
175
  if (item.doc._id === _id) {
161
176
  item.include = !item.include
@@ -172,13 +187,17 @@ export default function DuplicatorTool(props: DuplicatorToolProps) {
172
187
  setIsGathering(true)
173
188
  const docIds = docs.map((doc) => doc._id)
174
189
 
175
- const payloadDocs = await getDocumentsInArray(docIds, originClient, null)
176
- const draftDocs = await getDocumentsInArray(
177
- docIds.map((id) => `drafts.${id}`),
178
- originClient,
179
- null,
180
- `{_id}`
181
- )
190
+ const payloadDocs = await getDocumentsInArray({
191
+ fetchIds: docIds,
192
+ client: originClient,
193
+ pluginConfig,
194
+ })
195
+ const draftDocs = await getDocumentsInArray({
196
+ fetchIds: docIds.map((id) => `drafts.${id}`),
197
+ client: originClient,
198
+ projection: `{_id}`,
199
+ pluginConfig,
200
+ })
182
201
  const draftDocsIds = new Set(draftDocs.map(({_id}) => _id))
183
202
 
184
203
  // Shape it up
@@ -187,7 +206,7 @@ export default function DuplicatorTool(props: DuplicatorToolProps) {
187
206
  // Include this in the transaction?
188
207
  include: true,
189
208
  // Does it exist at the destination?
190
- status: '',
209
+ status: undefined,
191
210
  // Does it have any drafts?
192
211
  hasDraft: draftDocsIds.has(`drafts.${doc._id}`),
193
212
  }))
@@ -199,38 +218,45 @@ export default function DuplicatorTool(props: DuplicatorToolProps) {
199
218
 
200
219
  // Duplicate payload to destination dataset
201
220
  async function handleDuplicate() {
221
+ if (!destination) {
222
+ return
223
+ }
224
+
202
225
  setIsDuplicating(true)
203
226
 
204
- const assetsCount = payload.filter(({doc, include}) => include && typeIsAsset(doc._type)).length
227
+ const assetsCount = payload.filter(({doc, include}) => include && isAssetId(doc._id)).length
205
228
  let currentProgress = 0
206
229
  setProgress([currentProgress, assetsCount])
207
230
 
208
- setMessage({text: 'Duplicating...'})
231
+ setMessage({text: 'Duplicating...', tone: `default`})
209
232
 
210
- const destinationClient = sanityClient.withConfig({
233
+ const destinationClient = originClient.withConfig({
211
234
  ...clientConfig,
212
- dataset: destination.api.dataset,
213
- projectId: destination.api.projectId,
235
+ dataset: destination.dataset,
236
+ projectId: destination.projectId,
214
237
  })
215
238
 
216
- const transactionDocs = []
217
- const svgMaps = []
239
+ const transactionDocs: SanityDocument[] = []
240
+ const svgMaps: {old: string; new: string}[] = []
218
241
 
219
242
  // Upload assets and then add to transaction
220
- async function fetchDoc(doc) {
221
- if (typeIsAsset(doc._type)) {
243
+ async function fetchDoc(doc: SanityAssetDocument) {
244
+ if (isAssetId(doc._id)) {
222
245
  // Download and upload asset
223
246
  // Get the *original* image with this dlRaw param to create the same determenistic _id
224
- const uploadType = doc._type.split('.').pop().replace('Asset', '')
225
- const downloadUrl = uploadType === 'image' ? `${doc.url}?dlRaw=true` : doc.url
226
- const downloadConfig =
227
- uploadType === 'image' ? {headers: {Authorization: `Bearer ${token}`}} : {}
247
+ const typeIsFile = isSanityFileAsset(doc)
248
+ const downloadUrl = typeIsFile ? doc.url : `${doc.url}?dlRaw=true`
249
+ const downloadConfig = typeIsFile ? {} : {headers: {Authorization: `Bearer ${token}`}}
228
250
 
229
251
  await fetch(downloadUrl, downloadConfig).then(async (res) => {
230
252
  const assetData = await res.blob()
231
253
 
232
254
  const options = {filename: doc.originalFilename}
233
- const assetDoc = await destinationClient.assets.upload(uploadType, assetData, options)
255
+ const assetDoc = await destinationClient.assets.upload(
256
+ typeIsFile ? `file` : `image`,
257
+ assetData,
258
+ options
259
+ )
234
260
 
235
261
  // SVG _id's need remapping before transaction
236
262
  if (doc?.extension === 'svg') {
@@ -245,6 +271,7 @@ export default function DuplicatorTool(props: DuplicatorToolProps) {
245
271
  text: `Duplicating ${currentProgress}/${assetsCount} ${
246
272
  assetsCount === 1 ? `Assets` : `Assets`
247
273
  }`,
274
+ tone: 'default',
248
275
  })
249
276
 
250
277
  return setProgress([currentProgress, assetsCount])
@@ -257,7 +284,7 @@ export default function DuplicatorTool(props: DuplicatorToolProps) {
257
284
  const result = new Promise((resolve, reject) => {
258
285
  const payloadIncludedDocs = payload.filter((item) => item.include).map((item) => item.doc)
259
286
 
260
- mapLimit(payloadIncludedDocs, 3, asyncify(fetchDoc), (err) => {
287
+ mapLimit(payloadIncludedDocs, 3, asyncify(fetchDoc), (err: Error) => {
261
288
  if (err) {
262
289
  setIsDuplicating(false)
263
290
  setMessage({tone: 'critical', text: `Duplication Failed`})
@@ -265,6 +292,7 @@ export default function DuplicatorTool(props: DuplicatorToolProps) {
265
292
  reject(new Error('Duplication Failed'))
266
293
  }
267
294
 
295
+ // @ts-ignore
268
296
  resolve()
269
297
  })
270
298
  })
@@ -313,33 +341,33 @@ export default function DuplicatorTool(props: DuplicatorToolProps) {
313
341
  })
314
342
 
315
343
  setIsDuplicating(false)
316
- setProgress(0)
344
+ setProgress([0, 0])
317
345
  }
318
346
 
319
- function handleChange(e) {
320
- setDestination(spacesOptions.find((space) => space.name === e.currentTarget.value))
321
- }
347
+ function handleChange(e: React.ChangeEvent<HTMLSelectElement>) {
348
+ if (!workspacesOptions.length) {
349
+ return
350
+ }
322
351
 
323
- if (!spacesOptions.length) {
324
- return (
325
- <Feedback tone="critical">
326
- <code>__experimental_spaces</code> not found in <code>sanity.json</code>
327
- </Feedback>
328
- )
352
+ const targeted = workspacesOptions.find((space) => space.name === e.currentTarget.value)
353
+
354
+ if (targeted) {
355
+ setDestination(targeted)
356
+ }
329
357
  }
330
358
 
331
359
  const payloadCount = payload.length
332
360
  const firstSvgIndex = payload.findIndex(({doc}) => doc.extension === 'svg')
333
361
  const selectedDocumentsCount = payload.filter(
334
- (item) => item.include && !typeIsAsset(item.doc._type)
362
+ (item) => item.include && !isAssetId(item.doc._id)
335
363
  ).length
336
364
  const selectedAssetsCount = payload.filter(
337
- (item) => item.include && typeIsAsset(item.doc._type)
365
+ (item) => item.include && isAssetId(item.doc._id)
338
366
  ).length
339
367
  const selectedTotal = selectedDocumentsCount + selectedAssetsCount
340
368
  const destinationTitle = destination?.title ?? destination?.name
341
369
  const hasMultipleProjectIds =
342
- new Set(spacesOptions.map((space) => space?.api?.projectId).filter(Boolean)).size > 1
370
+ new Set(workspacesOptions.map((space) => space?.projectId).filter(Boolean)).size > 1
343
371
 
344
372
  const headingText = [selectedTotal, `/`, payloadCount, `Documents and Assets selected`].join(` `)
345
373
 
@@ -347,39 +375,59 @@ export default function DuplicatorTool(props: DuplicatorToolProps) {
347
375
  const text = [`Duplicate`]
348
376
 
349
377
  if (selectedDocumentsCount > 1) {
350
- text.push(selectedDocumentsCount, selectedDocumentsCount === 1 ? `Document` : `Documents`)
378
+ text.push(
379
+ String(selectedDocumentsCount),
380
+ selectedDocumentsCount === 1 ? `Document` : `Documents`
381
+ )
351
382
  }
352
383
 
353
384
  if (selectedAssetsCount > 1) {
354
- text.push(`and`, selectedAssetsCount, selectedAssetsCount === 1 ? `Asset` : `Assets`)
385
+ text.push(`and`, String(selectedAssetsCount), selectedAssetsCount === 1 ? `Asset` : `Assets`)
355
386
  }
356
387
 
357
- if (originClient.config().projectId !== destination.api.projectId) {
388
+ if (originClient.config().projectId !== destination?.projectId) {
358
389
  text.push(`between Projects`)
359
390
  }
360
391
 
361
- text.push(`to`, destinationTitle)
392
+ text.push(`to`, String(destinationTitle))
362
393
 
363
394
  return text.join(` `)
364
- }, [selectedDocumentsCount, selectedAssetsCount, destinationTitle])
395
+ }, [
396
+ selectedDocumentsCount,
397
+ selectedAssetsCount,
398
+ originClient,
399
+ destination?.projectId,
400
+ destinationTitle,
401
+ ])
402
+
403
+ if (workspacesOptions.length < 2) {
404
+ return (
405
+ <Feedback tone="critical">
406
+ <code>sanity.config.ts</code> must contain at least two Workspaces to use this plugin.
407
+ </Feedback>
408
+ )
409
+ }
365
410
 
366
411
  return (
367
412
  <Container width={1}>
368
- <Card>
413
+ <Card border>
369
414
  <Stack>
370
415
  <>
371
- <Card borderBottom padding={4} style={stickyStyles}>
416
+ <Card borderBottom padding={4} style={stickyStyles(isDarkMode)}>
372
417
  <Stack space={4}>
373
- <Flex space={3}>
418
+ <Flex gap={3}>
374
419
  <Stack style={{flex: 1}} space={3}>
375
420
  <Label>Duplicate from</Label>
376
- <Select readOnly value={spacesOptions.find((space) => space.disabled)?.name}>
377
- {spacesOptions
421
+ <Select
422
+ readOnly
423
+ value={workspacesOptions.find((space) => space.disabled)?.name}
424
+ >
425
+ {workspacesOptions
378
426
  .filter((space) => space.disabled)
379
427
  .map((space) => (
380
428
  <option key={space.name} value={space.name} disabled={space.disabled}>
381
429
  {space.title ?? space.name}
382
- {hasMultipleProjectIds || space.usingEnvForProjectId ? ` (${space.api.projectId})` : ``}
430
+ {hasMultipleProjectIds ? ` (${space.projectId})` : ``}
383
431
  </option>
384
432
  ))}
385
433
  </Select>
@@ -392,10 +440,10 @@ export default function DuplicatorTool(props: DuplicatorToolProps) {
392
440
  <Stack style={{flex: 1}} space={3}>
393
441
  <Label>To Destination</Label>
394
442
  <Select onChange={handleChange}>
395
- {spacesOptions.map((space) => (
443
+ {workspacesOptions.map((space) => (
396
444
  <option key={space.name} value={space.name} disabled={space.disabled}>
397
445
  {space.title ?? space.name}
398
- {hasMultipleProjectIds || space.usingEnvForProjectId ? ` (${space.api.projectId})` : ``}
446
+ {hasMultipleProjectIds ? ` (${space.projectId})` : ``}
399
447
  {space.disabled ? ` (Current)` : ``}
400
448
  </option>
401
449
  ))}
@@ -426,40 +474,49 @@ export default function DuplicatorTool(props: DuplicatorToolProps) {
426
474
  )}
427
475
  </Stack>
428
476
  </Card>
429
- {message?.text && (
477
+ {message && (
430
478
  <Box paddingX={4} paddingTop={4}>
431
- <Card padding={3} radius={2} shadow={1} tone={message?.tone ?? 'transparent'}>
479
+ <Card padding={3} radius={2} shadow={1} tone={message.tone}>
432
480
  <Text size={1}>{message.text}</Text>
433
481
  </Card>
434
482
  </Box>
435
483
  )}
436
484
  {payload.length > 0 && (
437
485
  <Stack padding={4} space={3}>
438
- {payload.map(({doc, include, status, hasDraft}, index) => (
439
- <React.Fragment key={doc._id}>
440
- <Flex align="center">
441
- <Checkbox checked={include} onChange={() => handleCheckbox(doc._id)} />
442
- <Box flex={1} paddingX={3}>
443
- <Preview value={doc} type={schema.get(doc._type)} />
444
- </Box>
445
- <Flex items="center" gap={2}>
446
- {hasDraft ? <StatusBadge status="UNPUBLISHED" isAsset={false} /> : null}
447
- <StatusBadge status={status} isAsset={typeIsAsset(doc._type)} />
486
+ {payload.map(({doc, include, status, hasDraft}, index) => {
487
+ const schemaType = schema.get(doc._type)
488
+
489
+ return (
490
+ <React.Fragment key={doc._id}>
491
+ <Flex align="center">
492
+ <Checkbox checked={include} onChange={() => handleCheckbox(doc._id)} />
493
+ <Box flex={1} paddingX={3}>
494
+ {schemaType ? (
495
+ <Preview value={doc} schemaType={schemaType} />
496
+ ) : (
497
+ <Card tone="caution">Invalid schema type</Card>
498
+ )}
499
+ </Box>
500
+ <Flex align="center" gap={2}>
501
+ {hasDraft ? <StatusBadge status="UNPUBLISHED" isAsset={false} /> : null}
502
+ <StatusBadge status={status} isAsset={isAssetId(doc._id)} />
503
+ </Flex>
448
504
  </Flex>
449
- </Flex>
450
- {doc?.extension === 'svg' && index === firstSvgIndex && (
451
- <Card padding={3} radius={2} shadow={1} tone="caution">
452
- <Text size={1}>
453
- Due to how SVGs are sanitized after first uploaded, duplicated SVG assets
454
- may have new <code>_id</code>'s at the destination. The newly generated{' '}
455
- <code>_id</code> will be the same in each duplication, but it will never
456
- be the same <code>_id</code> as the first time this Asset was uploaded.
457
- References to the asset will be updated to use the new <code>_id</code>.
458
- </Text>
459
- </Card>
460
- )}
461
- </React.Fragment>
462
- ))}
505
+ {doc?.extension === 'svg' && index === firstSvgIndex && (
506
+ <Card padding={3} radius={2} shadow={1} tone="caution">
507
+ <Text size={1}>
508
+ Due to how SVGs are sanitized after first uploaded, duplicated SVG
509
+ assets may have new <code>_id</code>'s at the destination. The newly
510
+ generated <code>_id</code> will be the same in each duplication, but it
511
+ will never be the same <code>_id</code> as the first time this Asset was
512
+ uploaded. References to the asset will be updated to use the new{' '}
513
+ <code>_id</code>.
514
+ </Text>
515
+ </Card>
516
+ )}
517
+ </React.Fragment>
518
+ )
519
+ })}
463
520
  </Stack>
464
521
  )}
465
522
  <Stack space={2} padding={4} paddingTop={0}>
@@ -1,40 +1,55 @@
1
1
  import React, {useEffect, useState} from 'react'
2
- import sanityClient from 'part:@sanity/base/client'
3
- import schema from 'part:@sanity/base/schema'
4
2
  import {Button, Stack, Box, Label, Text, Card, Flex, Grid, Container, TextInput} from '@sanity/ui'
3
+ import {useSchema, useClient, SanityDocument} from 'sanity'
5
4
 
6
- import DuplicatorTool from './DuplicatorTool'
5
+ import Duplicator from './Duplicator'
7
6
  import {clientConfig} from '../helpers/clientConfig'
8
-
9
- const originClient = sanityClient.withConfig(clientConfig)
7
+ import {PluginConfig} from '../types'
10
8
 
11
9
  type DuplicatorQueryProps = {
12
10
  token: string
11
+ pluginConfig: PluginConfig
13
12
  }
14
13
 
15
- const schemaTypes = schema.getTypeNames()
14
+ type InitialData = {
15
+ docs: SanityDocument[]
16
+ // draftIds: string[]
17
+ }
16
18
 
17
19
  export default function DuplicatorQuery(props: DuplicatorQueryProps) {
18
- const {token} = props
20
+ const {token, pluginConfig} = props
21
+
22
+ const originClient = useClient(clientConfig)
23
+
24
+ const schema = useSchema()
25
+ const schemaTypes = schema.getTypeNames()
19
26
 
20
27
  const [value, setValue] = useState(``)
21
- const [initialData, setInitialData] = useState({docs: [], draftIds: []});
28
+ const [initialData, setInitialData] = useState<InitialData>({
29
+ docs: [],
30
+ // draftIds: []
31
+ })
22
32
 
23
33
  function handleSubmit(e?: any) {
24
34
  if (e) e.preventDefault()
25
35
 
26
36
  originClient
27
37
  .fetch(value)
28
- .then((res) => {
38
+ .then((res: SanityDocument[]) => {
29
39
  // Ensure queried docs are registered to the schema
30
40
  const registeredAndPublishedDocs = res.length
31
41
  ? res
32
42
  .filter((doc) => schemaTypes.includes(doc._type))
33
43
  .filter((doc) => !doc._id.startsWith(`drafts.`))
34
44
  : []
35
- const initialDraftIds = res.length ? res.filter(doc => doc._id.startsWith(`drafts.`)).map(doc => doc._id) : []
45
+ // const initialDraftIds = res.length
46
+ // ? res.filter((doc) => doc._id.startsWith(`drafts.`)).map((doc) => doc._id)
47
+ // : []
36
48
 
37
- setInitialData({docs: registeredAndPublishedDocs, draftIds: initialDraftIds})
49
+ setInitialData({
50
+ docs: registeredAndPublishedDocs,
51
+ // draftIds: initialDraftIds
52
+ })
38
53
  })
39
54
  .catch((err) => console.error(err))
40
55
  }
@@ -44,13 +59,14 @@ export default function DuplicatorQuery(props: DuplicatorQueryProps) {
44
59
  if (!initialData.docs?.length && value) {
45
60
  handleSubmit()
46
61
  }
62
+ // eslint-disable-next-line react-hooks/exhaustive-deps
47
63
  }, [])
48
64
 
49
65
  return (
50
66
  <Container width={[1, 1, 1, 3]} padding={[0, 0, 0, 5]}>
51
67
  <Grid columns={[1, 1, 1, 2]} gap={[1, 1, 1, 4]}>
52
68
  <Box padding={[2, 2, 2, 0]}>
53
- <Card padding={4} scheme="dark" radius={3}>
69
+ <Card padding={4} radius={3} border>
54
70
  <Stack space={4}>
55
71
  <Box>
56
72
  <Label>Initial Documents Query</Label>
@@ -97,7 +113,14 @@ export default function DuplicatorQuery(props: DuplicatorQueryProps) {
97
113
  </Card>
98
114
  </Container>
99
115
  ))}
100
- {initialData.docs?.length > 0 && <DuplicatorTool docs={initialData.docs} draftIds={initialData.draftIds} token={token} />}
116
+ {initialData.docs?.length > 0 && (
117
+ <Duplicator
118
+ docs={initialData.docs}
119
+ // draftIds={initialData.draftIds}
120
+ token={token}
121
+ pluginConfig={pluginConfig}
122
+ />
123
+ )}
101
124
  </Grid>
102
125
  </Container>
103
126
  )
@@ -0,0 +1,66 @@
1
+ import React, {useState, useEffect} from 'react'
2
+ import {Grid, Card, Container, Button} from '@sanity/ui'
3
+ import {SanityDocument, useClient} from 'sanity'
4
+
5
+ import type {DuplicatorProps} from './Duplicator'
6
+ import Duplicator from './Duplicator'
7
+
8
+ export default function DuplicatorWrapper(props: DuplicatorProps) {
9
+ const {docs, token, pluginConfig} = props
10
+ const [inbound, setInbound] = useState<SanityDocument[]>([])
11
+ const {follow = []} = pluginConfig
12
+
13
+ // Make the first mode the default if there's only one
14
+ const [mode, setMode] = useState<'inbound' | 'outbound'>(
15
+ follow.length === 1 ? follow[0] : `outbound`
16
+ )
17
+ const client = useClient()
18
+
19
+ // "Inbound" will start with all documents that reference the first one
20
+ // And then you can gather "Outbound" references thereafter
21
+ useEffect(() => {
22
+ ;(async () => {
23
+ if (follow.includes(`inbound`)) {
24
+ const inboundReferences = await client.fetch(`*[references($id)]`, {id: docs[0]._id})
25
+ setInbound([...props.docs, ...inboundReferences])
26
+ }
27
+ })()
28
+ // eslint-disable-next-line react-hooks/exhaustive-deps
29
+ }, [])
30
+
31
+ return (
32
+ <Container>
33
+ {follow.length > 1 && (follow.includes(`inbound`) || follow.includes(`outbound`)) ? (
34
+ <Card paddingX={4} paddingBottom={4} marginBottom={4} borderBottom>
35
+ <Grid columns={2} gap={4}>
36
+ {follow.includes(`outbound`) ? (
37
+ <Button
38
+ mode="ghost"
39
+ tone="primary"
40
+ selected={mode === 'outbound'}
41
+ onClick={() => setMode('outbound')}
42
+ text="Outbound"
43
+ />
44
+ ) : null}
45
+ {follow.includes(`inbound`) ? (
46
+ <Button
47
+ mode="ghost"
48
+ tone="primary"
49
+ selected={mode === 'inbound'}
50
+ onClick={() => setMode('inbound')}
51
+ disabled={inbound.length === 0}
52
+ text={inbound.length > 0 ? `Inbound (${inbound.length})` : 'No inbound references'}
53
+ />
54
+ ) : null}
55
+ </Grid>
56
+ </Card>
57
+ ) : null}
58
+ <Duplicator
59
+ docs={mode === 'outbound' ? docs : inbound}
60
+ token={token}
61
+ // draftIds={[]}
62
+ pluginConfig={pluginConfig}
63
+ />
64
+ </Container>
65
+ )
66
+ }