@sanity/cross-dataset-duplicator 1.5.1 → 2.0.1
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/dist/index.d.ts +42 -61
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +720 -492
- package/dist/index.js.map +1 -1
- package/package.json +38 -77
- package/dist/index.d.mts +0 -81
- package/dist/index.mjs +0 -654
- package/dist/index.mjs.map +0 -1
- package/sanity.json +0 -8
- package/src/actions/DuplicateToAction.tsx +0 -34
- package/src/components/CrossDatasetDuplicator.tsx +0 -93
- package/src/components/CrossDatasetDuplicatorAction.tsx +0 -13
- package/src/components/CrossDatasetDuplicatorTool.tsx +0 -17
- package/src/components/Duplicator.tsx +0 -568
- package/src/components/DuplicatorQuery.tsx +0 -144
- package/src/components/DuplicatorWrapper.tsx +0 -67
- package/src/components/Feedback.tsx +0 -18
- package/src/components/ResetSecret.tsx +0 -30
- package/src/components/SelectButtons.tsx +0 -84
- package/src/components/StatusBadge.tsx +0 -111
- package/src/context/ConfigProvider.tsx +0 -30
- package/src/helpers/constants.ts +0 -12
- package/src/helpers/getDocumentsInArray.ts +0 -86
- package/src/helpers/index.ts +0 -19
- package/src/index.ts +0 -5
- package/src/plugin.tsx +0 -31
- package/src/tool/index.ts +0 -14
- package/src/types/index.ts +0 -27
- package/v2-incompatible.js +0 -11
|
@@ -1,568 +0,0 @@
|
|
|
1
|
-
/* eslint-disable react/jsx-no-bind */
|
|
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
|
|
12
|
-
import mapLimit from 'async/mapLimit'
|
|
13
|
-
// @ts-ignore
|
|
14
|
-
import asyncify from 'async/asyncify'
|
|
15
|
-
import {extractWithPath} from '@sanity/mutator'
|
|
16
|
-
import {dset} from 'dset'
|
|
17
|
-
import {
|
|
18
|
-
Card,
|
|
19
|
-
Container,
|
|
20
|
-
Text,
|
|
21
|
-
Box,
|
|
22
|
-
Button,
|
|
23
|
-
Label,
|
|
24
|
-
Stack,
|
|
25
|
-
Select,
|
|
26
|
-
Flex,
|
|
27
|
-
Checkbox,
|
|
28
|
-
CardTone,
|
|
29
|
-
useTheme,
|
|
30
|
-
Spinner,
|
|
31
|
-
} from '@sanity/ui'
|
|
32
|
-
import {ArrowRightIcon, SearchIcon, LaunchIcon} from '@sanity/icons'
|
|
33
|
-
import {SanityAssetDocument} from '@sanity/client'
|
|
34
|
-
import {isAssetId, isSanityFileAsset} from '@sanity/asset-utils'
|
|
35
|
-
|
|
36
|
-
import {stickyStyles, createInitialMessage} from '../helpers'
|
|
37
|
-
import {getDocumentsInArray} from '../helpers/getDocumentsInArray'
|
|
38
|
-
import SelectButtons from './SelectButtons'
|
|
39
|
-
import StatusBadge, {MessageTypes} from './StatusBadge'
|
|
40
|
-
import Feedback from './Feedback'
|
|
41
|
-
import {PluginConfig} from '../types'
|
|
42
|
-
|
|
43
|
-
export type DuplicatorProps = {
|
|
44
|
-
docs: SanityDocument[]
|
|
45
|
-
// TODO: Find out if this is even used?
|
|
46
|
-
// draftIds: string[]
|
|
47
|
-
token: string
|
|
48
|
-
pluginConfig: Required<PluginConfig>
|
|
49
|
-
onDuplicated?: () => Promise<void>
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export type PayloadItem = {
|
|
53
|
-
doc: SanityDocument
|
|
54
|
-
include: boolean
|
|
55
|
-
status?: keyof MessageTypes
|
|
56
|
-
hasDraft?: boolean
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
type WorkspaceOption = WorkspaceSummary & {
|
|
60
|
-
disabled: boolean
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
type Message = {
|
|
64
|
-
text: string
|
|
65
|
-
tone: CardTone
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
export default function Duplicator(props: DuplicatorProps) {
|
|
69
|
-
const {docs, token, pluginConfig, onDuplicated} = props
|
|
70
|
-
const isDarkMode = useTheme().sanity.color.dark
|
|
71
|
-
|
|
72
|
-
// Prepare origin (this Studio) client
|
|
73
|
-
const originClient = useClient({apiVersion: pluginConfig.apiVersion})
|
|
74
|
-
|
|
75
|
-
const schema = useSchema()
|
|
76
|
-
|
|
77
|
-
// Create list of dataset options
|
|
78
|
-
// and set initial value of dropdown
|
|
79
|
-
const workspaces = useWorkspaces()
|
|
80
|
-
const workspacesOptions: WorkspaceOption[] = workspaces.map((workspace) => ({
|
|
81
|
-
...workspace,
|
|
82
|
-
disabled:
|
|
83
|
-
workspace.dataset === originClient.config().dataset &&
|
|
84
|
-
workspace.projectId === originClient.config().projectId,
|
|
85
|
-
}))
|
|
86
|
-
|
|
87
|
-
const [destination, setDestination] = useState<WorkspaceOption | null>(
|
|
88
|
-
workspaces.length ? workspacesOptions.find((space) => !space.disabled) ?? null : null
|
|
89
|
-
)
|
|
90
|
-
const [message, setMessage] = useState<Message | null>(null)
|
|
91
|
-
const [payload, setPayload] = useState<PayloadItem[]>([])
|
|
92
|
-
|
|
93
|
-
const [hasReferences, setHasReferences] = useState(false)
|
|
94
|
-
const [isDuplicating, setIsDuplicating] = useState(false)
|
|
95
|
-
const [isGathering, setIsGathering] = useState(false)
|
|
96
|
-
const [progress, setProgress] = useState<number[]>([0, 0])
|
|
97
|
-
|
|
98
|
-
// Check for References and update message
|
|
99
|
-
useEffect(() => {
|
|
100
|
-
const expr = `.._ref`
|
|
101
|
-
const initialRefs = []
|
|
102
|
-
const initialPayload: PayloadItem[] = []
|
|
103
|
-
|
|
104
|
-
docs.forEach((doc) => {
|
|
105
|
-
const refs = extractWithPath(expr, doc).map((ref) => ref.value)
|
|
106
|
-
initialRefs.push(...refs)
|
|
107
|
-
initialPayload.push({include: true, doc})
|
|
108
|
-
})
|
|
109
|
-
|
|
110
|
-
updatePayloadStatuses(initialPayload)
|
|
111
|
-
|
|
112
|
-
const docCount = docs.length
|
|
113
|
-
const refsCount = initialRefs.length
|
|
114
|
-
|
|
115
|
-
if (initialRefs.length) {
|
|
116
|
-
setHasReferences(true)
|
|
117
|
-
|
|
118
|
-
setMessage({
|
|
119
|
-
tone: `caution`,
|
|
120
|
-
text: createInitialMessage(docCount, refsCount),
|
|
121
|
-
})
|
|
122
|
-
}
|
|
123
|
-
}, [docs])
|
|
124
|
-
|
|
125
|
-
// Re-check payload on destination when value changes
|
|
126
|
-
// (On initial render + select change)
|
|
127
|
-
useEffect(() => {
|
|
128
|
-
updatePayloadStatuses()
|
|
129
|
-
}, [destination])
|
|
130
|
-
|
|
131
|
-
// Check if payload documents exist at destination
|
|
132
|
-
async function updatePayloadStatuses(newPayload: PayloadItem[] = []) {
|
|
133
|
-
const payloadActual = newPayload.length ? newPayload : payload
|
|
134
|
-
|
|
135
|
-
if (!payloadActual.length || !destination?.name) {
|
|
136
|
-
return
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
const payloadIds = payloadActual.map(({doc}) => doc._id)
|
|
140
|
-
const destinationClient = originClient.withConfig({
|
|
141
|
-
dataset: destination.dataset,
|
|
142
|
-
projectId: destination.projectId,
|
|
143
|
-
})
|
|
144
|
-
const destinationData: SanityDocument[] = await destinationClient.fetch(
|
|
145
|
-
`*[_id in $payloadIds]{ _id, _updatedAt }`,
|
|
146
|
-
{payloadIds}
|
|
147
|
-
)
|
|
148
|
-
|
|
149
|
-
const updatedPayload = payloadActual.map((item) => {
|
|
150
|
-
const existingDoc = destinationData.find((doc) => doc._id === item.doc._id)
|
|
151
|
-
|
|
152
|
-
if (existingDoc?._updatedAt && item?.doc?._updatedAt) {
|
|
153
|
-
if (existingDoc._updatedAt === item.doc._updatedAt) {
|
|
154
|
-
// Exact same document exists at destination
|
|
155
|
-
// We don't compare by _rev because that is updated in a transaction
|
|
156
|
-
item.status = `EXISTS`
|
|
157
|
-
} else if (existingDoc._updatedAt && item.doc._updatedAt) {
|
|
158
|
-
item.status =
|
|
159
|
-
new Date(existingDoc._updatedAt) > new Date(item.doc._updatedAt)
|
|
160
|
-
? // Document at destination is newer
|
|
161
|
-
`OVERWRITE`
|
|
162
|
-
: // Document at destination is older
|
|
163
|
-
`UPDATE`
|
|
164
|
-
}
|
|
165
|
-
} else {
|
|
166
|
-
item.status = 'CREATE'
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
return item
|
|
170
|
-
})
|
|
171
|
-
|
|
172
|
-
setPayload(updatedPayload)
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
function handleCheckbox(_id: string) {
|
|
176
|
-
const updatedPayload = payload.map((item) => {
|
|
177
|
-
if (item.doc._id === _id) {
|
|
178
|
-
item.include = !item.include
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
return item
|
|
182
|
-
})
|
|
183
|
-
|
|
184
|
-
setPayload(updatedPayload)
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
// Find and recursively follow references beginning with this document
|
|
188
|
-
async function handleReferences() {
|
|
189
|
-
setIsGathering(true)
|
|
190
|
-
const docIds = docs.map((doc) => doc._id)
|
|
191
|
-
|
|
192
|
-
const payloadDocs = await getDocumentsInArray({
|
|
193
|
-
fetchIds: docIds,
|
|
194
|
-
client: originClient,
|
|
195
|
-
pluginConfig,
|
|
196
|
-
})
|
|
197
|
-
const draftDocs = await getDocumentsInArray({
|
|
198
|
-
fetchIds: docIds.map((id) => `drafts.${id}`),
|
|
199
|
-
client: originClient,
|
|
200
|
-
projection: `{_id}`,
|
|
201
|
-
pluginConfig,
|
|
202
|
-
})
|
|
203
|
-
const draftDocsIds = new Set(draftDocs.map(({_id}) => _id))
|
|
204
|
-
|
|
205
|
-
// Shape it up
|
|
206
|
-
const payloadShaped = payloadDocs.map((doc) => ({
|
|
207
|
-
doc,
|
|
208
|
-
// Include this in the transaction?
|
|
209
|
-
include: true,
|
|
210
|
-
// Does it exist at the destination?
|
|
211
|
-
status: undefined,
|
|
212
|
-
// Does it have any drafts?
|
|
213
|
-
hasDraft: draftDocsIds.has(`drafts.${doc._id}`),
|
|
214
|
-
}))
|
|
215
|
-
|
|
216
|
-
setPayload(payloadShaped)
|
|
217
|
-
updatePayloadStatuses(payloadShaped)
|
|
218
|
-
setIsGathering(false)
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
// Duplicate payload to destination dataset
|
|
222
|
-
async function handleDuplicate() {
|
|
223
|
-
if (!destination) {
|
|
224
|
-
return
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
setIsDuplicating(true)
|
|
228
|
-
|
|
229
|
-
const assetsCount = payload.filter(({doc, include}) => include && isAssetId(doc._id)).length
|
|
230
|
-
let currentProgress = 0
|
|
231
|
-
setProgress([currentProgress, assetsCount])
|
|
232
|
-
|
|
233
|
-
setMessage({text: 'Duplicating...', tone: `transparent`})
|
|
234
|
-
|
|
235
|
-
const destinationClient = originClient.withConfig({
|
|
236
|
-
apiVersion: pluginConfig.apiVersion,
|
|
237
|
-
dataset: destination.dataset,
|
|
238
|
-
projectId: destination.projectId,
|
|
239
|
-
})
|
|
240
|
-
|
|
241
|
-
const transactionDocs: SanityDocument[] = []
|
|
242
|
-
const svgMaps: {old: string; new: string}[] = []
|
|
243
|
-
|
|
244
|
-
// Upload assets and then add to transaction
|
|
245
|
-
async function fetchDoc(doc: SanityAssetDocument) {
|
|
246
|
-
if (isAssetId(doc._id)) {
|
|
247
|
-
// Download and upload asset
|
|
248
|
-
// Get the *original* image with this dlRaw param to create the same deterministic _id
|
|
249
|
-
const typeIsFile = isSanityFileAsset(doc)
|
|
250
|
-
const downloadUrl = typeIsFile ? doc.url : `${doc.url}?dlRaw=true`
|
|
251
|
-
const downloadConfig = typeIsFile ? {} : {headers: {Authorization: `Bearer ${token}`}}
|
|
252
|
-
|
|
253
|
-
await fetch(downloadUrl, downloadConfig).then(async (res) => {
|
|
254
|
-
const assetData = await res.blob()
|
|
255
|
-
|
|
256
|
-
const options = {filename: doc.originalFilename}
|
|
257
|
-
const assetDoc = await destinationClient.assets.upload(
|
|
258
|
-
typeIsFile ? `file` : `image`,
|
|
259
|
-
assetData,
|
|
260
|
-
options
|
|
261
|
-
)
|
|
262
|
-
|
|
263
|
-
// SVG _id's need remapping before transaction
|
|
264
|
-
if (doc?.extension === 'svg') {
|
|
265
|
-
svgMaps.push({old: doc._id, new: assetDoc._id})
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
// This adds the newly created asset document to the transaction but ...
|
|
269
|
-
// it doesn't have some of the original asset's metadata like `altText` or `title`
|
|
270
|
-
transactionDocs.push(assetDoc)
|
|
271
|
-
|
|
272
|
-
// So the original `doc` is added to the transaction as well below
|
|
273
|
-
// However, we don't want to retain `url` or `path` keys
|
|
274
|
-
// because these strings contain the origin's dataset name
|
|
275
|
-
doc.url = assetDoc.url
|
|
276
|
-
doc.path = assetDoc.path
|
|
277
|
-
})
|
|
278
|
-
|
|
279
|
-
currentProgress += 1
|
|
280
|
-
setMessage({
|
|
281
|
-
text: `Duplicating ${currentProgress}/${assetsCount} ${
|
|
282
|
-
assetsCount === 1 ? `Assets` : `Assets`
|
|
283
|
-
}`,
|
|
284
|
-
tone: 'default',
|
|
285
|
-
})
|
|
286
|
-
|
|
287
|
-
setProgress([currentProgress, assetsCount])
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
return transactionDocs.push(doc)
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
// Promises are limited to three at once
|
|
294
|
-
const result = new Promise((resolve, reject) => {
|
|
295
|
-
const payloadIncludedDocs = payload.filter((item) => item.include).map((item) => item.doc)
|
|
296
|
-
|
|
297
|
-
mapLimit(payloadIncludedDocs, 3, asyncify(fetchDoc), (err: Error) => {
|
|
298
|
-
if (err) {
|
|
299
|
-
setIsDuplicating(false)
|
|
300
|
-
setMessage({tone: 'critical', text: `Duplication Failed`})
|
|
301
|
-
console.error(err)
|
|
302
|
-
reject(new Error('Duplication Failed'))
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
// @ts-ignore
|
|
306
|
-
resolve()
|
|
307
|
-
})
|
|
308
|
-
})
|
|
309
|
-
|
|
310
|
-
await result
|
|
311
|
-
|
|
312
|
-
// Remap SVG references to new _id's
|
|
313
|
-
const transactionDocsMapped = transactionDocs.map((doc) => {
|
|
314
|
-
const expr = `.._ref`
|
|
315
|
-
const references = extractWithPath(expr, doc)
|
|
316
|
-
|
|
317
|
-
if (!references.length) {
|
|
318
|
-
return doc
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
// For every found _ref, search for an SVG asset _id and update
|
|
322
|
-
references.forEach((ref) => {
|
|
323
|
-
const newRefValue = svgMaps.find((asset) => asset.old === ref.value)?.new
|
|
324
|
-
|
|
325
|
-
if (newRefValue) {
|
|
326
|
-
const refPath = ref.path.join('.')
|
|
327
|
-
|
|
328
|
-
dset(doc, refPath, newRefValue)
|
|
329
|
-
}
|
|
330
|
-
})
|
|
331
|
-
|
|
332
|
-
return doc
|
|
333
|
-
})
|
|
334
|
-
|
|
335
|
-
// Create transaction
|
|
336
|
-
const transaction = destinationClient.transaction()
|
|
337
|
-
|
|
338
|
-
transactionDocsMapped.forEach((doc) => {
|
|
339
|
-
transaction.createOrReplace(doc)
|
|
340
|
-
})
|
|
341
|
-
|
|
342
|
-
await transaction
|
|
343
|
-
.commit()
|
|
344
|
-
.then((res) => {
|
|
345
|
-
setMessage({tone: 'positive', text: 'Duplication complete!'})
|
|
346
|
-
|
|
347
|
-
updatePayloadStatuses()
|
|
348
|
-
})
|
|
349
|
-
.catch((err) => {
|
|
350
|
-
setMessage({tone: 'critical', text: err.details.description})
|
|
351
|
-
})
|
|
352
|
-
|
|
353
|
-
setIsDuplicating(false)
|
|
354
|
-
setProgress([0, 0])
|
|
355
|
-
if (onDuplicated) {
|
|
356
|
-
try {
|
|
357
|
-
await onDuplicated()
|
|
358
|
-
} catch (error) {
|
|
359
|
-
setMessage({tone: 'critical', text: `Error in onDuplicated hook: ${error}`})
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
function handleChange(e: React.ChangeEvent<HTMLSelectElement>) {
|
|
365
|
-
if (!workspacesOptions.length) {
|
|
366
|
-
return
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
const targeted = workspacesOptions.find((space) => space.name === e.currentTarget.value)
|
|
370
|
-
|
|
371
|
-
if (targeted) {
|
|
372
|
-
setDestination(targeted)
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
const payloadCount = payload.length
|
|
377
|
-
const firstSvgIndex = payload.findIndex(({doc}) => doc.extension === 'svg')
|
|
378
|
-
const selectedDocumentsCount = payload.filter(
|
|
379
|
-
(item) => item.include && !isAssetId(item.doc._id)
|
|
380
|
-
).length
|
|
381
|
-
const selectedAssetsCount = payload.filter(
|
|
382
|
-
(item) => item.include && isAssetId(item.doc._id)
|
|
383
|
-
).length
|
|
384
|
-
const selectedTotal = selectedDocumentsCount + selectedAssetsCount
|
|
385
|
-
const destinationTitle = destination?.title ?? destination?.name
|
|
386
|
-
const hasMultipleProjectIds =
|
|
387
|
-
new Set(workspacesOptions.map((space) => space?.projectId).filter(Boolean)).size > 1
|
|
388
|
-
|
|
389
|
-
const headingText = [selectedTotal, `/`, payloadCount, `Documents and Assets selected`].join(` `)
|
|
390
|
-
|
|
391
|
-
const buttonText = React.useMemo(() => {
|
|
392
|
-
const text = [`Duplicate`]
|
|
393
|
-
|
|
394
|
-
if (selectedDocumentsCount > 1) {
|
|
395
|
-
text.push(
|
|
396
|
-
String(selectedDocumentsCount),
|
|
397
|
-
selectedDocumentsCount === 1 ? `Document` : `Documents`
|
|
398
|
-
)
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
if (selectedAssetsCount > 1) {
|
|
402
|
-
text.push(`and`, String(selectedAssetsCount), selectedAssetsCount === 1 ? `Asset` : `Assets`)
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
if (originClient.config().projectId !== destination?.projectId) {
|
|
406
|
-
text.push(`between Projects`)
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
text.push(`to`, String(destinationTitle))
|
|
410
|
-
|
|
411
|
-
return text.join(` `)
|
|
412
|
-
}, [
|
|
413
|
-
selectedDocumentsCount,
|
|
414
|
-
selectedAssetsCount,
|
|
415
|
-
originClient,
|
|
416
|
-
destination?.projectId,
|
|
417
|
-
destinationTitle,
|
|
418
|
-
])
|
|
419
|
-
|
|
420
|
-
if (workspacesOptions.length < 2) {
|
|
421
|
-
return (
|
|
422
|
-
<Feedback tone="critical">
|
|
423
|
-
<code>sanity.config.ts</code> must contain at least two Workspaces to use this plugin.
|
|
424
|
-
</Feedback>
|
|
425
|
-
)
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
return (
|
|
429
|
-
<Container width={1}>
|
|
430
|
-
<Card border>
|
|
431
|
-
<Stack>
|
|
432
|
-
<Card padding={4} style={stickyStyles(isDarkMode)}>
|
|
433
|
-
<Stack space={4}>
|
|
434
|
-
<Flex gap={3}>
|
|
435
|
-
<Stack style={{flex: 1}} space={3}>
|
|
436
|
-
<Label>Duplicate from</Label>
|
|
437
|
-
<Select readOnly value={workspacesOptions.find((space) => space.disabled)?.name}>
|
|
438
|
-
{workspacesOptions
|
|
439
|
-
.filter((space) => space.disabled)
|
|
440
|
-
.map((space) => (
|
|
441
|
-
<option key={space.name} value={space.name} disabled={space.disabled}>
|
|
442
|
-
{space.title ?? space.name}
|
|
443
|
-
{hasMultipleProjectIds ? ` (${space.projectId})` : ``}
|
|
444
|
-
</option>
|
|
445
|
-
))}
|
|
446
|
-
</Select>
|
|
447
|
-
</Stack>
|
|
448
|
-
<Box padding={4} paddingTop={5} paddingBottom={0}>
|
|
449
|
-
<Text size={3}>
|
|
450
|
-
<ArrowRightIcon />
|
|
451
|
-
</Text>
|
|
452
|
-
</Box>
|
|
453
|
-
<Stack style={{flex: 1}} space={3}>
|
|
454
|
-
<Label>To Destination</Label>
|
|
455
|
-
<Select onChange={handleChange}>
|
|
456
|
-
{workspacesOptions.map((space) => (
|
|
457
|
-
<option key={space.name} value={space.name} disabled={space.disabled}>
|
|
458
|
-
{space.title ?? space.name}
|
|
459
|
-
{hasMultipleProjectIds ? ` (${space.projectId})` : ``}
|
|
460
|
-
{space.disabled ? ` (Current)` : ``}
|
|
461
|
-
</option>
|
|
462
|
-
))}
|
|
463
|
-
</Select>
|
|
464
|
-
</Stack>
|
|
465
|
-
</Flex>
|
|
466
|
-
|
|
467
|
-
{isDuplicating && (
|
|
468
|
-
<Card border radius={2}>
|
|
469
|
-
<Card
|
|
470
|
-
style={{
|
|
471
|
-
width: '100%',
|
|
472
|
-
transform: `scaleX(${progress[0] / progress[1]})`,
|
|
473
|
-
transformOrigin: 'left',
|
|
474
|
-
transition: 'transform .2s ease',
|
|
475
|
-
boxSizing: 'border-box',
|
|
476
|
-
}}
|
|
477
|
-
padding={1}
|
|
478
|
-
tone="positive"
|
|
479
|
-
/>
|
|
480
|
-
</Card>
|
|
481
|
-
)}
|
|
482
|
-
{payload.length > 0 && (
|
|
483
|
-
<>
|
|
484
|
-
<Label>{headingText}</Label>
|
|
485
|
-
<SelectButtons payload={payload} setPayload={setPayload} />
|
|
486
|
-
</>
|
|
487
|
-
)}
|
|
488
|
-
</Stack>
|
|
489
|
-
</Card>
|
|
490
|
-
<Card borderTop padding={4}>
|
|
491
|
-
<Stack space={3}>
|
|
492
|
-
{message && (
|
|
493
|
-
<Card padding={3} radius={2} shadow={1} tone={message.tone}>
|
|
494
|
-
<Text size={1}>{message.text}</Text>
|
|
495
|
-
</Card>
|
|
496
|
-
)}
|
|
497
|
-
{payload.length > 0 ? (
|
|
498
|
-
<Stack>
|
|
499
|
-
{payload.map(({doc, include, status, hasDraft}, index) => {
|
|
500
|
-
const schemaType = schema.get(doc._type)
|
|
501
|
-
|
|
502
|
-
return (
|
|
503
|
-
<React.Fragment key={doc._id}>
|
|
504
|
-
<Flex align="center">
|
|
505
|
-
<Checkbox checked={include} onChange={() => handleCheckbox(doc._id)} />
|
|
506
|
-
<Box flex={1} paddingX={3}>
|
|
507
|
-
{schemaType ? (
|
|
508
|
-
<Preview value={doc} schemaType={schemaType} />
|
|
509
|
-
) : (
|
|
510
|
-
<Card tone="caution">Invalid schema type</Card>
|
|
511
|
-
)}
|
|
512
|
-
</Box>
|
|
513
|
-
<Flex align="center" gap={2}>
|
|
514
|
-
{hasDraft ? <StatusBadge status="UNPUBLISHED" isAsset={false} /> : null}
|
|
515
|
-
<StatusBadge status={status} isAsset={isAssetId(doc._id)} />
|
|
516
|
-
</Flex>
|
|
517
|
-
</Flex>
|
|
518
|
-
{doc?.extension === 'svg' && index === firstSvgIndex && (
|
|
519
|
-
<Card padding={3} radius={2} shadow={1} tone="caution">
|
|
520
|
-
<Text size={1}>
|
|
521
|
-
Due to how SVGs are sanitized after first uploaded, duplicated SVG
|
|
522
|
-
assets may have new <code>_id</code>'s at the destination. The newly
|
|
523
|
-
generated <code>_id</code> will be the same in each duplication, but
|
|
524
|
-
it will never be the same <code>_id</code> as the first time this
|
|
525
|
-
Asset was uploaded. References to the asset will be updated to use the
|
|
526
|
-
new <code>_id</code>.
|
|
527
|
-
</Text>
|
|
528
|
-
</Card>
|
|
529
|
-
)}
|
|
530
|
-
</React.Fragment>
|
|
531
|
-
)
|
|
532
|
-
})}
|
|
533
|
-
</Stack>
|
|
534
|
-
) : (
|
|
535
|
-
<Flex padding={4} align="center" justify="center">
|
|
536
|
-
<Spinner />
|
|
537
|
-
</Flex>
|
|
538
|
-
)}
|
|
539
|
-
<Stack space={2}>
|
|
540
|
-
{hasReferences && (
|
|
541
|
-
<Button
|
|
542
|
-
fontSize={2}
|
|
543
|
-
padding={4}
|
|
544
|
-
tone="positive"
|
|
545
|
-
mode="ghost"
|
|
546
|
-
icon={SearchIcon}
|
|
547
|
-
onClick={handleReferences}
|
|
548
|
-
text="Gather References"
|
|
549
|
-
disabled={isDuplicating || !selectedTotal || isGathering}
|
|
550
|
-
/>
|
|
551
|
-
)}
|
|
552
|
-
<Button
|
|
553
|
-
fontSize={2}
|
|
554
|
-
padding={4}
|
|
555
|
-
tone="positive"
|
|
556
|
-
icon={LaunchIcon}
|
|
557
|
-
onClick={handleDuplicate}
|
|
558
|
-
text={buttonText}
|
|
559
|
-
disabled={isDuplicating || !selectedTotal || isGathering}
|
|
560
|
-
/>
|
|
561
|
-
</Stack>
|
|
562
|
-
</Stack>
|
|
563
|
-
</Card>
|
|
564
|
-
</Stack>
|
|
565
|
-
</Card>
|
|
566
|
-
</Container>
|
|
567
|
-
)
|
|
568
|
-
}
|
|
@@ -1,144 +0,0 @@
|
|
|
1
|
-
import {useEffect, useState} from 'react'
|
|
2
|
-
import {Button, Stack, Box, Label, Text, Card, Flex, Grid, Container, TextInput} from '@sanity/ui'
|
|
3
|
-
import {useSchema, useClient, SanityDocument} from 'sanity'
|
|
4
|
-
|
|
5
|
-
import Duplicator from './Duplicator'
|
|
6
|
-
import {PluginConfig} from '../types'
|
|
7
|
-
|
|
8
|
-
type DuplicatorQueryProps = {
|
|
9
|
-
token: string
|
|
10
|
-
pluginConfig: Required<PluginConfig>
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
type InitialData = {
|
|
14
|
-
docs: SanityDocument[]
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export default function DuplicatorQuery(props: DuplicatorQueryProps) {
|
|
18
|
-
const {token, pluginConfig} = props
|
|
19
|
-
|
|
20
|
-
const {queries: preDefinedQueries, apiVersion} = pluginConfig
|
|
21
|
-
const originClient = useClient({apiVersion})
|
|
22
|
-
|
|
23
|
-
const schema = useSchema()
|
|
24
|
-
const schemaTypes = schema.getTypeNames()
|
|
25
|
-
|
|
26
|
-
const [value, setValue] = useState(``)
|
|
27
|
-
const [fetched, setFetched] = useState(false)
|
|
28
|
-
const [initialData, setInitialData] = useState<InitialData>({
|
|
29
|
-
docs: [],
|
|
30
|
-
})
|
|
31
|
-
function handleSubmit(e?: any) {
|
|
32
|
-
if (e) e.preventDefault()
|
|
33
|
-
|
|
34
|
-
originClient
|
|
35
|
-
.fetch(value)
|
|
36
|
-
.then((res: SanityDocument[]) => {
|
|
37
|
-
// Ensure queried docs are registered to the schema
|
|
38
|
-
const registeredAndPublishedDocs = res.length
|
|
39
|
-
? res
|
|
40
|
-
.filter((doc) => schemaTypes.includes(doc._type))
|
|
41
|
-
.filter((doc) => !doc._id.startsWith(`drafts.`))
|
|
42
|
-
: []
|
|
43
|
-
|
|
44
|
-
setInitialData({
|
|
45
|
-
docs: registeredAndPublishedDocs,
|
|
46
|
-
})
|
|
47
|
-
setFetched(true)
|
|
48
|
-
})
|
|
49
|
-
.catch((err) => console.error(err))
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// Auto-load initial textinput value
|
|
53
|
-
useEffect(() => {
|
|
54
|
-
if (!initialData.docs?.length && value) {
|
|
55
|
-
handleSubmit()
|
|
56
|
-
}
|
|
57
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
58
|
-
}, [])
|
|
59
|
-
|
|
60
|
-
return (
|
|
61
|
-
<Card padding={[0, 0, 0, 5]}>
|
|
62
|
-
<Container>
|
|
63
|
-
<Grid columns={[1, 1, 1, 2]} gap={[1, 1, 1, 4]}>
|
|
64
|
-
<Box padding={[2, 2, 2, 0]}>
|
|
65
|
-
<Card padding={4} radius={3} border>
|
|
66
|
-
<Stack space={4}>
|
|
67
|
-
<Box>
|
|
68
|
-
<Label>Initial Documents Query</Label>
|
|
69
|
-
</Box>
|
|
70
|
-
<Box>
|
|
71
|
-
<Text>
|
|
72
|
-
Start with a valid GROQ query to load initial documents. The query will need to
|
|
73
|
-
return an Array of Objects. Drafts will be removed from the results.
|
|
74
|
-
</Text>
|
|
75
|
-
</Box>
|
|
76
|
-
<form onSubmit={handleSubmit}>
|
|
77
|
-
<Flex>
|
|
78
|
-
<Box flex={1} paddingRight={2}>
|
|
79
|
-
<TextInput
|
|
80
|
-
style={{fontFamily: 'monospace'}}
|
|
81
|
-
fontSize={2}
|
|
82
|
-
// eslint-disable-next-line react/jsx-no-bind
|
|
83
|
-
onChange={(event) => setValue(event.currentTarget.value)}
|
|
84
|
-
padding={4}
|
|
85
|
-
placeholder={`*[_type == "article"]`}
|
|
86
|
-
value={value ?? ``}
|
|
87
|
-
/>
|
|
88
|
-
</Box>
|
|
89
|
-
<Button
|
|
90
|
-
padding={2}
|
|
91
|
-
paddingX={4}
|
|
92
|
-
tone="primary"
|
|
93
|
-
onClick={handleSubmit}
|
|
94
|
-
text="Query"
|
|
95
|
-
disabled={!value}
|
|
96
|
-
/>
|
|
97
|
-
</Flex>
|
|
98
|
-
</form>
|
|
99
|
-
</Stack>
|
|
100
|
-
</Card>
|
|
101
|
-
{preDefinedQueries && preDefinedQueries?.length > 0 && (
|
|
102
|
-
<Card marginTop={2} padding={4} radius={3} border>
|
|
103
|
-
<Box>
|
|
104
|
-
<Stack space={4}>
|
|
105
|
-
<Box>
|
|
106
|
-
<Label>Predefined Queries</Label>
|
|
107
|
-
</Box>
|
|
108
|
-
<Stack space={2}>
|
|
109
|
-
{preDefinedQueries.map((query) => (
|
|
110
|
-
<Button
|
|
111
|
-
key={query.label.replace(/\s+/g, '-')}
|
|
112
|
-
padding={2}
|
|
113
|
-
paddingX={4}
|
|
114
|
-
tone="primary"
|
|
115
|
-
onClick={() => setValue(`*[${query.query}]`)}
|
|
116
|
-
text={query.label}
|
|
117
|
-
/>
|
|
118
|
-
))}
|
|
119
|
-
</Stack>
|
|
120
|
-
</Stack>
|
|
121
|
-
</Box>
|
|
122
|
-
</Card>
|
|
123
|
-
)}
|
|
124
|
-
</Box>
|
|
125
|
-
{fetched && initialData.docs.length < 1 && (
|
|
126
|
-
<Container width={1}>
|
|
127
|
-
<Card padding={5}>
|
|
128
|
-
{value ? `No documents match this query` : `Start with a valid GROQ query`}
|
|
129
|
-
</Card>
|
|
130
|
-
</Container>
|
|
131
|
-
)}
|
|
132
|
-
{initialData.docs?.length > 0 && (
|
|
133
|
-
<Duplicator
|
|
134
|
-
docs={initialData.docs}
|
|
135
|
-
// draftIds={initialData.draftIds}
|
|
136
|
-
token={token}
|
|
137
|
-
pluginConfig={pluginConfig}
|
|
138
|
-
/>
|
|
139
|
-
)}
|
|
140
|
-
</Grid>
|
|
141
|
-
</Container>
|
|
142
|
-
</Card>
|
|
143
|
-
)
|
|
144
|
-
}
|