@sanity/cross-dataset-duplicator 1.4.1 → 1.5.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/README.md +5 -1
- package/dist/index.d.mts +81 -0
- package/dist/index.d.ts +3 -4
- package/dist/index.js +413 -885
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +654 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +26 -31
- package/src/components/CrossDatasetDuplicator.tsx +1 -1
- package/src/components/Duplicator.tsx +3 -5
- package/src/components/DuplicatorQuery.tsx +3 -10
- package/src/components/DuplicatorWrapper.tsx +2 -3
- package/src/components/ResetSecret.tsx +6 -3
- package/src/context/ConfigProvider.tsx +1 -1
- package/src/helpers/constants.ts +2 -1
- package/src/plugin.tsx +1 -1
- package/src/types/index.ts +1 -0
- package/dist/index.cjs.mjs +0 -7
- package/dist/index.esm.js +0 -1104
- package/dist/index.esm.js.map +0 -1
- package/src/helpers/clientConfig.ts +0 -1
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,654 @@
|
|
|
1
|
+
import { useClient, useSchema, useWorkspaces, Preview, definePlugin } from "sanity";
|
|
2
|
+
import { jsx, jsxs, Fragment } from "react/jsx-runtime";
|
|
3
|
+
import React, { useState, useEffect, useCallback, createContext, useContext } from "react";
|
|
4
|
+
import { InfoOutlineIcon, ArrowRightIcon, SearchIcon, LaunchIcon } from "@sanity/icons";
|
|
5
|
+
import { useSecrets, SettingsView } from "@sanity/studio-secrets";
|
|
6
|
+
import { Card, Flex, Button, Badge, Tooltip, Box, Text, useTheme, Container, Stack, Label, Select, Checkbox, Spinner, Grid, TextInput } from "@sanity/ui";
|
|
7
|
+
import mapLimit from "async/mapLimit";
|
|
8
|
+
import asyncify from "async/asyncify";
|
|
9
|
+
import { extractWithPath } from "@sanity/mutator";
|
|
10
|
+
import { dset } from "dset";
|
|
11
|
+
import { isAssetId, isSanityFileAsset } from "@sanity/asset-utils";
|
|
12
|
+
function createInitialMessage(docCount = 0, refsCount = 0) {
|
|
13
|
+
return [
|
|
14
|
+
docCount === 1 ? "This Document contains" : `These ${docCount} Documents contain`,
|
|
15
|
+
refsCount === 1 ? "1 Reference." : `${refsCount} References.`,
|
|
16
|
+
refsCount === 1 ? "That Document" : "Those Documents",
|
|
17
|
+
"may have References too. If referenced Documents do not exist at the target Destination, this transaction will fail."
|
|
18
|
+
].join(" ");
|
|
19
|
+
}
|
|
20
|
+
const stickyStyles = (isDarkMode = !0) => ({
|
|
21
|
+
position: "sticky",
|
|
22
|
+
top: 0,
|
|
23
|
+
zIndex: 100,
|
|
24
|
+
backgroundColor: isDarkMode ? "rgba(10,10,10,0.95)" : "rgba(255,255,255,0.95)"
|
|
25
|
+
});
|
|
26
|
+
async function getDocumentsInArray(options) {
|
|
27
|
+
const { fetchIds, client, pluginConfig, currentIds, projection } = options, collection = [], query = `*[${["_id in $fetchIds", pluginConfig.filter].filter(Boolean).join(" && ")}]${projection ?? ""}`, data = await client.fetch(query, {
|
|
28
|
+
fetchIds: fetchIds ?? []
|
|
29
|
+
});
|
|
30
|
+
if (!data?.length)
|
|
31
|
+
return [];
|
|
32
|
+
const localCurrentIds = currentIds ?? /* @__PURE__ */ new Set(), newDataIds = new Set(
|
|
33
|
+
data.map((dataDoc) => dataDoc._id).filter((id) => currentIds?.size ? !localCurrentIds.has(id) : !!id)
|
|
34
|
+
);
|
|
35
|
+
return newDataIds.size && (collection.push(...data), localCurrentIds.add(...newDataIds), await Promise.all(
|
|
36
|
+
data.map(async (doc) => {
|
|
37
|
+
const references = extractWithPath(".._ref", doc).map((ref) => ref.value);
|
|
38
|
+
if (references.length) {
|
|
39
|
+
const newReferenceIds = new Set(
|
|
40
|
+
references.filter((ref) => !localCurrentIds.has(ref))
|
|
41
|
+
);
|
|
42
|
+
if (newReferenceIds.size) {
|
|
43
|
+
const referenceDocs = await getDocumentsInArray({
|
|
44
|
+
fetchIds: Array.from(newReferenceIds),
|
|
45
|
+
currentIds: localCurrentIds,
|
|
46
|
+
client,
|
|
47
|
+
pluginConfig
|
|
48
|
+
});
|
|
49
|
+
referenceDocs?.length && collection.push(...referenceDocs);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
})
|
|
53
|
+
)), collection.filter(Boolean).reduce((acc, cur) => acc.some((doc) => doc._id === cur._id) ? acc : [...acc, cur], []);
|
|
54
|
+
}
|
|
55
|
+
const buttons = ["All", "None", null, "New", "Existing", "Older", null, "Documents", "Assets"];
|
|
56
|
+
function SelectButtons(props) {
|
|
57
|
+
const { payload, setPayload } = props, [disabledActions, setDisabledActions] = useState([]);
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
!disabledActions?.length && payload.every((item) => item.include) && setDisabledActions(["ALL"]);
|
|
60
|
+
}, [disabledActions?.length, payload]);
|
|
61
|
+
function handleSelectButton(action) {
|
|
62
|
+
if (!action || !payload.length) return;
|
|
63
|
+
const newPayload = [...payload];
|
|
64
|
+
switch (action) {
|
|
65
|
+
case "ALL":
|
|
66
|
+
newPayload.map((item) => item.include = !0);
|
|
67
|
+
break;
|
|
68
|
+
case "NONE":
|
|
69
|
+
newPayload.map((item) => item.include = !1);
|
|
70
|
+
break;
|
|
71
|
+
case "NEW":
|
|
72
|
+
newPayload.map((item) => item.include = item.status === "CREATE");
|
|
73
|
+
break;
|
|
74
|
+
case "EXISTING":
|
|
75
|
+
newPayload.map((item) => item.include = item.status === "EXISTS");
|
|
76
|
+
break;
|
|
77
|
+
case "OLDER":
|
|
78
|
+
newPayload.map((item) => item.include = item.status === "OVERWRITE");
|
|
79
|
+
break;
|
|
80
|
+
case "ASSETS":
|
|
81
|
+
newPayload.map((item) => item.include = isAssetId(item.doc._id));
|
|
82
|
+
break;
|
|
83
|
+
case "DOCUMENTS":
|
|
84
|
+
newPayload.map((item) => item.include = !isAssetId(item.doc._id));
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
setDisabledActions([action]), setPayload(newPayload);
|
|
88
|
+
}
|
|
89
|
+
return /* @__PURE__ */ jsx(Card, { padding: 1, radius: 3, shadow: 1, children: /* @__PURE__ */ jsx(Flex, { gap: 2, wrap: "wrap", children: buttons.map(
|
|
90
|
+
(action, actionIndex) => action ? /* @__PURE__ */ jsx(
|
|
91
|
+
Button,
|
|
92
|
+
{
|
|
93
|
+
fontSize: 1,
|
|
94
|
+
mode: "bleed",
|
|
95
|
+
padding: 2,
|
|
96
|
+
text: action,
|
|
97
|
+
disabled: disabledActions.includes(action.toUpperCase()),
|
|
98
|
+
onClick: () => handleSelectButton(action.toUpperCase())
|
|
99
|
+
},
|
|
100
|
+
action
|
|
101
|
+
) : (
|
|
102
|
+
// eslint-disable-next-line react/no-array-index-key
|
|
103
|
+
/* @__PURE__ */ jsx(Card, { borderLeft: !0 }, `divider-${actionIndex}`)
|
|
104
|
+
)
|
|
105
|
+
) }) });
|
|
106
|
+
}
|
|
107
|
+
const documentTones = {
|
|
108
|
+
EXISTS: "primary",
|
|
109
|
+
OVERWRITE: "critical",
|
|
110
|
+
UPDATE: "caution",
|
|
111
|
+
CREATE: "positive",
|
|
112
|
+
UNPUBLISHED: "caution"
|
|
113
|
+
}, assetTones = {
|
|
114
|
+
EXISTS: "critical",
|
|
115
|
+
OVERWRITE: "critical",
|
|
116
|
+
UPDATE: "critical",
|
|
117
|
+
CREATE: "positive",
|
|
118
|
+
UNPUBLISHED: "default"
|
|
119
|
+
}, documentMessages = {
|
|
120
|
+
// Only happens once document is copied the first time, and _updatedAt is the same
|
|
121
|
+
EXISTS: "This document already exists at the Destination with the same ID with the same Updated time.",
|
|
122
|
+
// Is true immediately after transaction as _updatedAt is updated by API after mutation
|
|
123
|
+
// Is also true if the document at the destination has been manually modified
|
|
124
|
+
// Presently, the plugin doesn't actually compare the two documents
|
|
125
|
+
OVERWRITE: "A newer version of this document exists at the Destination, and it will be overwritten with this version.",
|
|
126
|
+
// Document at destination is older
|
|
127
|
+
UPDATE: "An older version of this document exists at the Destination, and it will be overwritten with this version.",
|
|
128
|
+
// Document at destination doesn't exist
|
|
129
|
+
CREATE: "This document will be created at the destination.",
|
|
130
|
+
UNPUBLISHED: "A Draft version of this Document exists in this Dataset, but only the Published version will be duplicated to the destination."
|
|
131
|
+
}, assetMessages = {
|
|
132
|
+
EXISTS: "This Asset already exists at the Destination",
|
|
133
|
+
OVERWRITE: "This Asset already exists at the Destination",
|
|
134
|
+
UPDATE: "This Asset already exists at the Destination",
|
|
135
|
+
CREATE: "This Asset does not yet exist at the Destination",
|
|
136
|
+
UNPUBLISHED: ""
|
|
137
|
+
}, assetStatus = {
|
|
138
|
+
EXISTS: "RE-UPLOAD",
|
|
139
|
+
OVERWRITE: "RE-UPLOAD",
|
|
140
|
+
UPDATE: "RE-UPLOAD",
|
|
141
|
+
CREATE: "UPLOAD",
|
|
142
|
+
UNPUBLISHED: ""
|
|
143
|
+
};
|
|
144
|
+
function StatusBadge(props) {
|
|
145
|
+
const { status, isAsset } = props;
|
|
146
|
+
if (!status)
|
|
147
|
+
return null;
|
|
148
|
+
const badgeTone = isAsset ? assetTones[status] : documentTones[status];
|
|
149
|
+
if (!badgeTone)
|
|
150
|
+
return /* @__PURE__ */ jsx(Badge, { muted: !0, padding: 2, fontSize: 1, mode: "outline", children: "Checking..." });
|
|
151
|
+
const badgeText = isAsset ? assetMessages[status] : documentMessages[status], badgeStatus = isAsset ? assetStatus[status] : status;
|
|
152
|
+
return /* @__PURE__ */ jsx(
|
|
153
|
+
Tooltip,
|
|
154
|
+
{
|
|
155
|
+
content: /* @__PURE__ */ jsx(Box, { padding: 3, style: { maxWidth: 200 }, children: /* @__PURE__ */ jsx(Text, { size: 1, children: badgeText }) }),
|
|
156
|
+
fallbackPlacements: ["right", "left"],
|
|
157
|
+
placement: "top",
|
|
158
|
+
portal: !0,
|
|
159
|
+
children: /* @__PURE__ */ jsxs(Badge, { muted: !0, padding: 3, fontSize: 1, tone: badgeTone, mode: "outline", children: [
|
|
160
|
+
badgeStatus,
|
|
161
|
+
/* @__PURE__ */ jsx(Box, { marginLeft: 2, display: "inline-block", as: "span", children: /* @__PURE__ */ jsx(InfoOutlineIcon, {}) })
|
|
162
|
+
] })
|
|
163
|
+
}
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
function Feedback(props) {
|
|
167
|
+
const { children, tone = "caution" } = props;
|
|
168
|
+
return /* @__PURE__ */ jsx(Card, { padding: 3, radius: 2, shadow: 1, tone, children: /* @__PURE__ */ jsx(Text, { size: 1, children }) });
|
|
169
|
+
}
|
|
170
|
+
function Duplicator(props) {
|
|
171
|
+
const { docs, token, pluginConfig, onDuplicated } = props, isDarkMode = useTheme().sanity.color.dark, originClient = useClient({ apiVersion: pluginConfig.apiVersion }), schema = useSchema(), workspaces = useWorkspaces(), workspacesOptions = workspaces.map((workspace) => ({
|
|
172
|
+
...workspace,
|
|
173
|
+
disabled: workspace.dataset === originClient.config().dataset && workspace.projectId === originClient.config().projectId
|
|
174
|
+
})), [destination, setDestination] = useState(
|
|
175
|
+
workspaces.length ? workspacesOptions.find((space) => !space.disabled) ?? null : null
|
|
176
|
+
), [message, setMessage] = useState(null), [payload, setPayload] = useState([]), [hasReferences, setHasReferences] = useState(!1), [isDuplicating, setIsDuplicating] = useState(!1), [isGathering, setIsGathering] = useState(!1), [progress, setProgress] = useState([0, 0]);
|
|
177
|
+
useEffect(() => {
|
|
178
|
+
const expr = ".._ref", initialRefs = [], initialPayload = [];
|
|
179
|
+
docs.forEach((doc) => {
|
|
180
|
+
const refs = extractWithPath(expr, doc).map((ref) => ref.value);
|
|
181
|
+
initialRefs.push(...refs), initialPayload.push({ include: !0, doc });
|
|
182
|
+
}), updatePayloadStatuses(initialPayload);
|
|
183
|
+
const docCount = docs.length, refsCount = initialRefs.length;
|
|
184
|
+
initialRefs.length && (setHasReferences(!0), setMessage({
|
|
185
|
+
tone: "caution",
|
|
186
|
+
text: createInitialMessage(docCount, refsCount)
|
|
187
|
+
}));
|
|
188
|
+
}, [docs]), useEffect(() => {
|
|
189
|
+
updatePayloadStatuses();
|
|
190
|
+
}, [destination]);
|
|
191
|
+
async function updatePayloadStatuses(newPayload = []) {
|
|
192
|
+
const payloadActual = newPayload.length ? newPayload : payload;
|
|
193
|
+
if (!payloadActual.length || !destination?.name)
|
|
194
|
+
return;
|
|
195
|
+
const payloadIds = payloadActual.map(({ doc }) => doc._id), destinationData = await originClient.withConfig({
|
|
196
|
+
dataset: destination.dataset,
|
|
197
|
+
projectId: destination.projectId
|
|
198
|
+
}).fetch(
|
|
199
|
+
"*[_id in $payloadIds]{ _id, _updatedAt }",
|
|
200
|
+
{ payloadIds }
|
|
201
|
+
), updatedPayload = payloadActual.map((item) => {
|
|
202
|
+
const existingDoc = destinationData.find((doc) => doc._id === item.doc._id);
|
|
203
|
+
return existingDoc?._updatedAt && item?.doc?._updatedAt ? existingDoc._updatedAt === item.doc._updatedAt ? item.status = "EXISTS" : existingDoc._updatedAt && item.doc._updatedAt && (item.status = new Date(existingDoc._updatedAt) > new Date(item.doc._updatedAt) ? (
|
|
204
|
+
// Document at destination is newer
|
|
205
|
+
"OVERWRITE"
|
|
206
|
+
) : (
|
|
207
|
+
// Document at destination is older
|
|
208
|
+
"UPDATE"
|
|
209
|
+
)) : item.status = "CREATE", item;
|
|
210
|
+
});
|
|
211
|
+
setPayload(updatedPayload);
|
|
212
|
+
}
|
|
213
|
+
function handleCheckbox(_id) {
|
|
214
|
+
const updatedPayload = payload.map((item) => (item.doc._id === _id && (item.include = !item.include), item));
|
|
215
|
+
setPayload(updatedPayload);
|
|
216
|
+
}
|
|
217
|
+
async function handleReferences() {
|
|
218
|
+
setIsGathering(!0);
|
|
219
|
+
const docIds = docs.map((doc) => doc._id), payloadDocs = await getDocumentsInArray({
|
|
220
|
+
fetchIds: docIds,
|
|
221
|
+
client: originClient,
|
|
222
|
+
pluginConfig
|
|
223
|
+
}), draftDocs = await getDocumentsInArray({
|
|
224
|
+
fetchIds: docIds.map((id) => `drafts.${id}`),
|
|
225
|
+
client: originClient,
|
|
226
|
+
projection: "{_id}",
|
|
227
|
+
pluginConfig
|
|
228
|
+
}), draftDocsIds = new Set(draftDocs.map(({ _id }) => _id)), payloadShaped = payloadDocs.map((doc) => ({
|
|
229
|
+
doc,
|
|
230
|
+
// Include this in the transaction?
|
|
231
|
+
include: !0,
|
|
232
|
+
// Does it exist at the destination?
|
|
233
|
+
status: void 0,
|
|
234
|
+
// Does it have any drafts?
|
|
235
|
+
hasDraft: draftDocsIds.has(`drafts.${doc._id}`)
|
|
236
|
+
}));
|
|
237
|
+
setPayload(payloadShaped), updatePayloadStatuses(payloadShaped), setIsGathering(!1);
|
|
238
|
+
}
|
|
239
|
+
async function handleDuplicate() {
|
|
240
|
+
if (!destination)
|
|
241
|
+
return;
|
|
242
|
+
setIsDuplicating(!0);
|
|
243
|
+
const assetsCount = payload.filter(({ doc, include }) => include && isAssetId(doc._id)).length;
|
|
244
|
+
let currentProgress = 0;
|
|
245
|
+
setProgress([currentProgress, assetsCount]), setMessage({ text: "Duplicating...", tone: "transparent" });
|
|
246
|
+
const destinationClient = originClient.withConfig({
|
|
247
|
+
apiVersion: pluginConfig.apiVersion,
|
|
248
|
+
dataset: destination.dataset,
|
|
249
|
+
projectId: destination.projectId
|
|
250
|
+
}), transactionDocs = [], svgMaps = [];
|
|
251
|
+
async function fetchDoc(doc) {
|
|
252
|
+
if (isAssetId(doc._id)) {
|
|
253
|
+
const typeIsFile = isSanityFileAsset(doc), downloadUrl = typeIsFile ? doc.url : `${doc.url}?dlRaw=true`, downloadConfig = typeIsFile ? {} : { headers: { Authorization: `Bearer ${token}` } };
|
|
254
|
+
await fetch(downloadUrl, downloadConfig).then(async (res) => {
|
|
255
|
+
const assetData = await res.blob(), options = { filename: doc.originalFilename }, assetDoc = await destinationClient.assets.upload(
|
|
256
|
+
typeIsFile ? "file" : "image",
|
|
257
|
+
assetData,
|
|
258
|
+
options
|
|
259
|
+
);
|
|
260
|
+
doc?.extension === "svg" && svgMaps.push({ old: doc._id, new: assetDoc._id }), transactionDocs.push(assetDoc), doc.url = assetDoc.url, doc.path = assetDoc.path;
|
|
261
|
+
}), currentProgress += 1, setMessage({
|
|
262
|
+
text: `Duplicating ${currentProgress}/${assetsCount} Assets`,
|
|
263
|
+
tone: "default"
|
|
264
|
+
}), setProgress([currentProgress, assetsCount]);
|
|
265
|
+
}
|
|
266
|
+
return transactionDocs.push(doc);
|
|
267
|
+
}
|
|
268
|
+
await new Promise((resolve, reject) => {
|
|
269
|
+
const payloadIncludedDocs = payload.filter((item) => item.include).map((item) => item.doc);
|
|
270
|
+
mapLimit(payloadIncludedDocs, 3, asyncify(fetchDoc), (err) => {
|
|
271
|
+
err && (setIsDuplicating(!1), setMessage({ tone: "critical", text: "Duplication Failed" }), console.error(err), reject(new Error("Duplication Failed"))), resolve();
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
const transactionDocsMapped = transactionDocs.map((doc) => {
|
|
275
|
+
const references = extractWithPath(".._ref", doc);
|
|
276
|
+
return references.length && references.forEach((ref) => {
|
|
277
|
+
const newRefValue = svgMaps.find((asset) => asset.old === ref.value)?.new;
|
|
278
|
+
if (newRefValue) {
|
|
279
|
+
const refPath = ref.path.join(".");
|
|
280
|
+
dset(doc, refPath, newRefValue);
|
|
281
|
+
}
|
|
282
|
+
}), doc;
|
|
283
|
+
}), transaction = destinationClient.transaction();
|
|
284
|
+
if (transactionDocsMapped.forEach((doc) => {
|
|
285
|
+
transaction.createOrReplace(doc);
|
|
286
|
+
}), await transaction.commit().then((res) => {
|
|
287
|
+
setMessage({ tone: "positive", text: "Duplication complete!" }), updatePayloadStatuses();
|
|
288
|
+
}).catch((err) => {
|
|
289
|
+
setMessage({ tone: "critical", text: err.details.description });
|
|
290
|
+
}), setIsDuplicating(!1), setProgress([0, 0]), onDuplicated)
|
|
291
|
+
try {
|
|
292
|
+
await onDuplicated();
|
|
293
|
+
} catch (error) {
|
|
294
|
+
setMessage({ tone: "critical", text: `Error in onDuplicated hook: ${error}` });
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
function handleChange(e) {
|
|
298
|
+
if (!workspacesOptions.length)
|
|
299
|
+
return;
|
|
300
|
+
const targeted = workspacesOptions.find((space) => space.name === e.currentTarget.value);
|
|
301
|
+
targeted && setDestination(targeted);
|
|
302
|
+
}
|
|
303
|
+
const payloadCount = payload.length, firstSvgIndex = payload.findIndex(({ doc }) => doc.extension === "svg"), selectedDocumentsCount = payload.filter(
|
|
304
|
+
(item) => item.include && !isAssetId(item.doc._id)
|
|
305
|
+
).length, selectedAssetsCount = payload.filter(
|
|
306
|
+
(item) => item.include && isAssetId(item.doc._id)
|
|
307
|
+
).length, selectedTotal = selectedDocumentsCount + selectedAssetsCount, destinationTitle = destination?.title ?? destination?.name, hasMultipleProjectIds = new Set(workspacesOptions.map((space) => space?.projectId).filter(Boolean)).size > 1, headingText = [selectedTotal, "/", payloadCount, "Documents and Assets selected"].join(" "), buttonText = React.useMemo(() => {
|
|
308
|
+
const text = ["Duplicate"];
|
|
309
|
+
return selectedDocumentsCount > 1 && text.push(
|
|
310
|
+
String(selectedDocumentsCount),
|
|
311
|
+
selectedDocumentsCount === 1 ? "Document" : "Documents"
|
|
312
|
+
), selectedAssetsCount > 1 && text.push("and", String(selectedAssetsCount), selectedAssetsCount === 1 ? "Asset" : "Assets"), originClient.config().projectId !== destination?.projectId && text.push("between Projects"), text.push("to", String(destinationTitle)), text.join(" ");
|
|
313
|
+
}, [
|
|
314
|
+
selectedDocumentsCount,
|
|
315
|
+
selectedAssetsCount,
|
|
316
|
+
originClient,
|
|
317
|
+
destination?.projectId,
|
|
318
|
+
destinationTitle
|
|
319
|
+
]);
|
|
320
|
+
return workspacesOptions.length < 2 ? /* @__PURE__ */ jsxs(Feedback, { tone: "critical", children: [
|
|
321
|
+
/* @__PURE__ */ jsx("code", { children: "sanity.config.ts" }),
|
|
322
|
+
" must contain at least two Workspaces to use this plugin."
|
|
323
|
+
] }) : /* @__PURE__ */ jsx(Container, { width: 1, children: /* @__PURE__ */ jsx(Card, { border: !0, children: /* @__PURE__ */ jsxs(Stack, { children: [
|
|
324
|
+
/* @__PURE__ */ jsx(Card, { padding: 4, style: stickyStyles(isDarkMode), children: /* @__PURE__ */ jsxs(Stack, { space: 4, children: [
|
|
325
|
+
/* @__PURE__ */ jsxs(Flex, { gap: 3, children: [
|
|
326
|
+
/* @__PURE__ */ jsxs(Stack, { style: { flex: 1 }, space: 3, children: [
|
|
327
|
+
/* @__PURE__ */ jsx(Label, { children: "Duplicate from" }),
|
|
328
|
+
/* @__PURE__ */ jsx(Select, { readOnly: !0, value: workspacesOptions.find((space) => space.disabled)?.name, children: workspacesOptions.filter((space) => space.disabled).map((space) => /* @__PURE__ */ jsxs("option", { value: space.name, disabled: space.disabled, children: [
|
|
329
|
+
space.title ?? space.name,
|
|
330
|
+
hasMultipleProjectIds ? ` (${space.projectId})` : ""
|
|
331
|
+
] }, space.name)) })
|
|
332
|
+
] }),
|
|
333
|
+
/* @__PURE__ */ jsx(Box, { padding: 4, paddingTop: 5, paddingBottom: 0, children: /* @__PURE__ */ jsx(Text, { size: 3, children: /* @__PURE__ */ jsx(ArrowRightIcon, {}) }) }),
|
|
334
|
+
/* @__PURE__ */ jsxs(Stack, { style: { flex: 1 }, space: 3, children: [
|
|
335
|
+
/* @__PURE__ */ jsx(Label, { children: "To Destination" }),
|
|
336
|
+
/* @__PURE__ */ jsx(Select, { onChange: handleChange, children: workspacesOptions.map((space) => /* @__PURE__ */ jsxs("option", { value: space.name, disabled: space.disabled, children: [
|
|
337
|
+
space.title ?? space.name,
|
|
338
|
+
hasMultipleProjectIds ? ` (${space.projectId})` : "",
|
|
339
|
+
space.disabled ? " (Current)" : ""
|
|
340
|
+
] }, space.name)) })
|
|
341
|
+
] })
|
|
342
|
+
] }),
|
|
343
|
+
isDuplicating && /* @__PURE__ */ jsx(Card, { border: !0, radius: 2, children: /* @__PURE__ */ jsx(
|
|
344
|
+
Card,
|
|
345
|
+
{
|
|
346
|
+
style: {
|
|
347
|
+
width: "100%",
|
|
348
|
+
transform: `scaleX(${progress[0] / progress[1]})`,
|
|
349
|
+
transformOrigin: "left",
|
|
350
|
+
transition: "transform .2s ease",
|
|
351
|
+
boxSizing: "border-box"
|
|
352
|
+
},
|
|
353
|
+
padding: 1,
|
|
354
|
+
tone: "positive"
|
|
355
|
+
}
|
|
356
|
+
) }),
|
|
357
|
+
payload.length > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
358
|
+
/* @__PURE__ */ jsx(Label, { children: headingText }),
|
|
359
|
+
/* @__PURE__ */ jsx(SelectButtons, { payload, setPayload })
|
|
360
|
+
] })
|
|
361
|
+
] }) }),
|
|
362
|
+
/* @__PURE__ */ jsx(Card, { borderTop: !0, padding: 4, children: /* @__PURE__ */ jsxs(Stack, { space: 3, children: [
|
|
363
|
+
message && /* @__PURE__ */ jsx(Card, { padding: 3, radius: 2, shadow: 1, tone: message.tone, children: /* @__PURE__ */ jsx(Text, { size: 1, children: message.text }) }),
|
|
364
|
+
payload.length > 0 ? /* @__PURE__ */ jsx(Stack, { children: payload.map(({ doc, include, status, hasDraft }, index) => {
|
|
365
|
+
const schemaType = schema.get(doc._type);
|
|
366
|
+
return /* @__PURE__ */ jsxs(React.Fragment, { children: [
|
|
367
|
+
/* @__PURE__ */ jsxs(Flex, { align: "center", children: [
|
|
368
|
+
/* @__PURE__ */ jsx(Checkbox, { checked: include, onChange: () => handleCheckbox(doc._id) }),
|
|
369
|
+
/* @__PURE__ */ jsx(Box, { flex: 1, paddingX: 3, children: schemaType ? /* @__PURE__ */ jsx(Preview, { value: doc, schemaType }) : /* @__PURE__ */ jsx(Card, { tone: "caution", children: "Invalid schema type" }) }),
|
|
370
|
+
/* @__PURE__ */ jsxs(Flex, { align: "center", gap: 2, children: [
|
|
371
|
+
hasDraft ? /* @__PURE__ */ jsx(StatusBadge, { status: "UNPUBLISHED", isAsset: !1 }) : null,
|
|
372
|
+
/* @__PURE__ */ jsx(StatusBadge, { status, isAsset: isAssetId(doc._id) })
|
|
373
|
+
] })
|
|
374
|
+
] }),
|
|
375
|
+
doc?.extension === "svg" && index === firstSvgIndex && /* @__PURE__ */ jsx(Card, { padding: 3, radius: 2, shadow: 1, tone: "caution", children: /* @__PURE__ */ jsxs(Text, { size: 1, children: [
|
|
376
|
+
"Due to how SVGs are sanitized after first uploaded, duplicated SVG assets may have new ",
|
|
377
|
+
/* @__PURE__ */ jsx("code", { children: "_id" }),
|
|
378
|
+
"'s at the destination. The newly generated ",
|
|
379
|
+
/* @__PURE__ */ jsx("code", { children: "_id" }),
|
|
380
|
+
" will be the same in each duplication, but it will never be the same ",
|
|
381
|
+
/* @__PURE__ */ jsx("code", { children: "_id" }),
|
|
382
|
+
" as the first time this Asset was uploaded. References to the asset will be updated to use the new ",
|
|
383
|
+
/* @__PURE__ */ jsx("code", { children: "_id" }),
|
|
384
|
+
"."
|
|
385
|
+
] }) })
|
|
386
|
+
] }, doc._id);
|
|
387
|
+
}) }) : /* @__PURE__ */ jsx(Flex, { padding: 4, align: "center", justify: "center", children: /* @__PURE__ */ jsx(Spinner, {}) }),
|
|
388
|
+
/* @__PURE__ */ jsxs(Stack, { space: 2, children: [
|
|
389
|
+
hasReferences && /* @__PURE__ */ jsx(
|
|
390
|
+
Button,
|
|
391
|
+
{
|
|
392
|
+
fontSize: 2,
|
|
393
|
+
padding: 4,
|
|
394
|
+
tone: "positive",
|
|
395
|
+
mode: "ghost",
|
|
396
|
+
icon: SearchIcon,
|
|
397
|
+
onClick: handleReferences,
|
|
398
|
+
text: "Gather References",
|
|
399
|
+
disabled: isDuplicating || !selectedTotal || isGathering
|
|
400
|
+
}
|
|
401
|
+
),
|
|
402
|
+
/* @__PURE__ */ jsx(
|
|
403
|
+
Button,
|
|
404
|
+
{
|
|
405
|
+
fontSize: 2,
|
|
406
|
+
padding: 4,
|
|
407
|
+
tone: "positive",
|
|
408
|
+
icon: LaunchIcon,
|
|
409
|
+
onClick: handleDuplicate,
|
|
410
|
+
text: buttonText,
|
|
411
|
+
disabled: isDuplicating || !selectedTotal || isGathering
|
|
412
|
+
}
|
|
413
|
+
)
|
|
414
|
+
] })
|
|
415
|
+
] }) })
|
|
416
|
+
] }) }) });
|
|
417
|
+
}
|
|
418
|
+
function DuplicatorQuery(props) {
|
|
419
|
+
const { token, pluginConfig } = props, { queries: preDefinedQueries, apiVersion } = pluginConfig, originClient = useClient({ apiVersion }), schemaTypes = useSchema().getTypeNames(), [value, setValue] = useState(""), [fetched, setFetched] = useState(!1), [initialData, setInitialData] = useState({
|
|
420
|
+
docs: []
|
|
421
|
+
});
|
|
422
|
+
function handleSubmit(e) {
|
|
423
|
+
e && e.preventDefault(), originClient.fetch(value).then((res) => {
|
|
424
|
+
const registeredAndPublishedDocs = res.length ? res.filter((doc) => schemaTypes.includes(doc._type)).filter((doc) => !doc._id.startsWith("drafts.")) : [];
|
|
425
|
+
setInitialData({
|
|
426
|
+
docs: registeredAndPublishedDocs
|
|
427
|
+
}), setFetched(!0);
|
|
428
|
+
}).catch((err) => console.error(err));
|
|
429
|
+
}
|
|
430
|
+
return useEffect(() => {
|
|
431
|
+
!initialData.docs?.length && value && handleSubmit();
|
|
432
|
+
}, []), /* @__PURE__ */ jsx(Card, { padding: [0, 0, 0, 5], children: /* @__PURE__ */ jsx(Container, { children: /* @__PURE__ */ jsxs(Grid, { columns: [1, 1, 1, 2], gap: [1, 1, 1, 4], children: [
|
|
433
|
+
/* @__PURE__ */ jsxs(Box, { padding: [2, 2, 2, 0], children: [
|
|
434
|
+
/* @__PURE__ */ jsx(Card, { padding: 4, radius: 3, border: !0, children: /* @__PURE__ */ jsxs(Stack, { space: 4, children: [
|
|
435
|
+
/* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsx(Label, { children: "Initial Documents Query" }) }),
|
|
436
|
+
/* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsx(Text, { children: "Start with a valid GROQ query to load initial documents. The query will need to return an Array of Objects. Drafts will be removed from the results." }) }),
|
|
437
|
+
/* @__PURE__ */ jsx("form", { onSubmit: handleSubmit, children: /* @__PURE__ */ jsxs(Flex, { children: [
|
|
438
|
+
/* @__PURE__ */ jsx(Box, { flex: 1, paddingRight: 2, children: /* @__PURE__ */ jsx(
|
|
439
|
+
TextInput,
|
|
440
|
+
{
|
|
441
|
+
style: { fontFamily: "monospace" },
|
|
442
|
+
fontSize: 2,
|
|
443
|
+
onChange: (event) => setValue(event.currentTarget.value),
|
|
444
|
+
padding: 4,
|
|
445
|
+
placeholder: '*[_type == "article"]',
|
|
446
|
+
value: value ?? ""
|
|
447
|
+
}
|
|
448
|
+
) }),
|
|
449
|
+
/* @__PURE__ */ jsx(
|
|
450
|
+
Button,
|
|
451
|
+
{
|
|
452
|
+
padding: 2,
|
|
453
|
+
paddingX: 4,
|
|
454
|
+
tone: "primary",
|
|
455
|
+
onClick: handleSubmit,
|
|
456
|
+
text: "Query",
|
|
457
|
+
disabled: !value
|
|
458
|
+
}
|
|
459
|
+
)
|
|
460
|
+
] }) })
|
|
461
|
+
] }) }),
|
|
462
|
+
preDefinedQueries && preDefinedQueries?.length > 0 && /* @__PURE__ */ jsx(Card, { marginTop: 2, padding: 4, radius: 3, border: !0, children: /* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsxs(Stack, { space: 4, children: [
|
|
463
|
+
/* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsx(Label, { children: "Predefined Queries" }) }),
|
|
464
|
+
/* @__PURE__ */ jsx(Stack, { space: 2, children: preDefinedQueries.map((query) => /* @__PURE__ */ jsx(
|
|
465
|
+
Button,
|
|
466
|
+
{
|
|
467
|
+
padding: 2,
|
|
468
|
+
paddingX: 4,
|
|
469
|
+
tone: "primary",
|
|
470
|
+
onClick: () => setValue(`*[${query.query}]`),
|
|
471
|
+
text: query.label
|
|
472
|
+
},
|
|
473
|
+
query.label.replace(/\s+/g, "-")
|
|
474
|
+
)) })
|
|
475
|
+
] }) }) })
|
|
476
|
+
] }),
|
|
477
|
+
fetched && initialData.docs.length < 1 && /* @__PURE__ */ jsx(Container, { width: 1, children: /* @__PURE__ */ jsx(Card, { padding: 5, children: value ? "No documents match this query" : "Start with a valid GROQ query" }) }),
|
|
478
|
+
initialData.docs?.length > 0 && /* @__PURE__ */ jsx(
|
|
479
|
+
Duplicator,
|
|
480
|
+
{
|
|
481
|
+
docs: initialData.docs,
|
|
482
|
+
token,
|
|
483
|
+
pluginConfig
|
|
484
|
+
}
|
|
485
|
+
)
|
|
486
|
+
] }) }) });
|
|
487
|
+
}
|
|
488
|
+
function DuplicatorWrapper(props) {
|
|
489
|
+
const { docs, token, pluginConfig, onDuplicated } = props, [inbound, setInbound] = useState([]), { follow = [], apiVersion } = pluginConfig, [mode, setMode] = useState(
|
|
490
|
+
follow.length === 1 ? follow[0] : "outbound"
|
|
491
|
+
), client = useClient({ apiVersion });
|
|
492
|
+
return useEffect(() => {
|
|
493
|
+
(async () => {
|
|
494
|
+
if (follow.includes("inbound")) {
|
|
495
|
+
const inboundReferences = await client.fetch("*[references($id)]", { id: docs[0]._id });
|
|
496
|
+
setInbound([...props.docs, ...inboundReferences]);
|
|
497
|
+
}
|
|
498
|
+
})();
|
|
499
|
+
}, []), /* @__PURE__ */ jsxs(Container, { children: [
|
|
500
|
+
follow.length > 1 && (follow.includes("inbound") || follow.includes("outbound")) ? /* @__PURE__ */ jsx(Card, { paddingX: 4, paddingBottom: 4, marginBottom: 4, borderBottom: !0, children: /* @__PURE__ */ jsxs(Grid, { columns: 2, gap: 4, children: [
|
|
501
|
+
follow.includes("outbound") ? /* @__PURE__ */ jsx(
|
|
502
|
+
Button,
|
|
503
|
+
{
|
|
504
|
+
mode: "ghost",
|
|
505
|
+
tone: "primary",
|
|
506
|
+
selected: mode === "outbound",
|
|
507
|
+
onClick: () => setMode("outbound"),
|
|
508
|
+
text: "Outbound"
|
|
509
|
+
}
|
|
510
|
+
) : null,
|
|
511
|
+
follow.includes("inbound") ? /* @__PURE__ */ jsx(
|
|
512
|
+
Button,
|
|
513
|
+
{
|
|
514
|
+
mode: "ghost",
|
|
515
|
+
tone: "primary",
|
|
516
|
+
selected: mode === "inbound",
|
|
517
|
+
onClick: () => setMode("inbound"),
|
|
518
|
+
disabled: inbound.length === 0,
|
|
519
|
+
text: inbound.length > 0 ? `Inbound (${inbound.length})` : "No inbound references"
|
|
520
|
+
}
|
|
521
|
+
) : null
|
|
522
|
+
] }) }) : null,
|
|
523
|
+
/* @__PURE__ */ jsx(
|
|
524
|
+
Duplicator,
|
|
525
|
+
{
|
|
526
|
+
docs: mode === "outbound" ? docs : inbound,
|
|
527
|
+
token,
|
|
528
|
+
pluginConfig,
|
|
529
|
+
onDuplicated
|
|
530
|
+
}
|
|
531
|
+
)
|
|
532
|
+
] });
|
|
533
|
+
}
|
|
534
|
+
const SECRET_NAMESPACE = "CrossDatasetDuplicator", DEFAULT_CONFIG = {
|
|
535
|
+
apiVersion: "2025-02-19",
|
|
536
|
+
tool: !0,
|
|
537
|
+
types: [],
|
|
538
|
+
filter: "",
|
|
539
|
+
follow: ["outbound"],
|
|
540
|
+
queries: []
|
|
541
|
+
};
|
|
542
|
+
function ResetSecret({ apiVersion }) {
|
|
543
|
+
const client = useClient({ apiVersion }), handleClick = useCallback(() => {
|
|
544
|
+
client.delete({ query: `*[_id == "secrets.${SECRET_NAMESPACE}"]` });
|
|
545
|
+
}, [client]);
|
|
546
|
+
return /* @__PURE__ */ jsx(Flex, { align: "center", justify: "flex-end", paddingX: [2, 2, 2, 5], paddingY: 5, children: /* @__PURE__ */ jsx(
|
|
547
|
+
Button,
|
|
548
|
+
{
|
|
549
|
+
text: "Reset Secret",
|
|
550
|
+
onClick: handleClick,
|
|
551
|
+
mode: "ghost",
|
|
552
|
+
tone: "critical",
|
|
553
|
+
fontSize: 1,
|
|
554
|
+
padding: 2
|
|
555
|
+
}
|
|
556
|
+
) });
|
|
557
|
+
}
|
|
558
|
+
const CrossDatasetDuplicatorContext = createContext(DEFAULT_CONFIG);
|
|
559
|
+
function useCrossDatasetDuplicatorConfig() {
|
|
560
|
+
return useContext(CrossDatasetDuplicatorContext);
|
|
561
|
+
}
|
|
562
|
+
function ConfigProvider(props) {
|
|
563
|
+
const { pluginConfig, ...rest } = props;
|
|
564
|
+
return /* @__PURE__ */ jsx(CrossDatasetDuplicatorContext.Provider, { value: pluginConfig, children: props.renderDefault(rest) });
|
|
565
|
+
}
|
|
566
|
+
const secretConfigKeys = [
|
|
567
|
+
{
|
|
568
|
+
key: "bearerToken",
|
|
569
|
+
title: "An API token with Viewer permissions is required to duplicate the original files of assets, and will be used for all Duplications. Create one at sanity.io/manage",
|
|
570
|
+
description: ""
|
|
571
|
+
}
|
|
572
|
+
];
|
|
573
|
+
function CrossDatasetDuplicator(props) {
|
|
574
|
+
const { mode = "tool", docs = [], onDuplicated } = props ?? {}, pluginConfig = useCrossDatasetDuplicatorConfig(), { loading, secrets } = useSecrets(SECRET_NAMESPACE), [showSecretsPrompt, setShowSecretsPrompt] = useState(!1);
|
|
575
|
+
return useEffect(() => {
|
|
576
|
+
secrets && setShowSecretsPrompt(!secrets?.bearerToken);
|
|
577
|
+
}, [secrets]), loading ? /* @__PURE__ */ jsx(Flex, { justify: "center", align: "center", children: /* @__PURE__ */ jsx(Box, { padding: 5, children: /* @__PURE__ */ jsx(Spinner, {}) }) }) : !loading && showSecretsPrompt || !secrets?.bearerToken ? /* @__PURE__ */ jsx(
|
|
578
|
+
SettingsView,
|
|
579
|
+
{
|
|
580
|
+
title: "Token Required",
|
|
581
|
+
namespace: SECRET_NAMESPACE,
|
|
582
|
+
keys: secretConfigKeys,
|
|
583
|
+
onClose: () => setShowSecretsPrompt(!1)
|
|
584
|
+
}
|
|
585
|
+
) : mode === "tool" && pluginConfig ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
586
|
+
/* @__PURE__ */ jsx(DuplicatorQuery, { token: secrets?.bearerToken, pluginConfig }),
|
|
587
|
+
/* @__PURE__ */ jsx(ResetSecret, { apiVersion: pluginConfig.apiVersion })
|
|
588
|
+
] }) : docs?.length ? pluginConfig ? /* @__PURE__ */ jsx(
|
|
589
|
+
DuplicatorWrapper,
|
|
590
|
+
{
|
|
591
|
+
docs,
|
|
592
|
+
token: secrets?.bearerToken,
|
|
593
|
+
pluginConfig,
|
|
594
|
+
onDuplicated
|
|
595
|
+
}
|
|
596
|
+
) : /* @__PURE__ */ jsx(Feedback, { children: "No plugin config" }) : /* @__PURE__ */ jsx(Feedback, { children: "No docs passed into Duplicator Tool" });
|
|
597
|
+
}
|
|
598
|
+
function CrossDatasetDuplicatorAction(props) {
|
|
599
|
+
const { docs = [], onDuplicated } = props;
|
|
600
|
+
return /* @__PURE__ */ jsx(CrossDatasetDuplicator, { mode: "action", docs, onDuplicated });
|
|
601
|
+
}
|
|
602
|
+
const DuplicateToAction = (props) => {
|
|
603
|
+
const { draft, published, onComplete } = props, [dialogOpen, setDialogOpen] = useState(!1);
|
|
604
|
+
return {
|
|
605
|
+
disabled: draft,
|
|
606
|
+
title: draft ? "Document must be Published to begin" : null,
|
|
607
|
+
label: "Duplicate to...",
|
|
608
|
+
dialog: dialogOpen && published && {
|
|
609
|
+
type: "modal",
|
|
610
|
+
title: "Cross Dataset Duplicator",
|
|
611
|
+
content: /* @__PURE__ */ jsx(CrossDatasetDuplicatorAction, { docs: [published] }),
|
|
612
|
+
onClose: () => {
|
|
613
|
+
onComplete(), setDialogOpen(!1);
|
|
614
|
+
}
|
|
615
|
+
},
|
|
616
|
+
onHandle: () => setDialogOpen(!0),
|
|
617
|
+
icon: LaunchIcon
|
|
618
|
+
};
|
|
619
|
+
};
|
|
620
|
+
DuplicateToAction.action = "duplicateTo";
|
|
621
|
+
function CrossDatasetDuplicatorTool(props) {
|
|
622
|
+
const { docs = [] } = props.tool.options ?? {};
|
|
623
|
+
return /* @__PURE__ */ jsx(CrossDatasetDuplicator, { mode: "tool", docs });
|
|
624
|
+
}
|
|
625
|
+
const crossDatasetDuplicatorTool = () => ({
|
|
626
|
+
title: "Duplicator",
|
|
627
|
+
name: "duplicator",
|
|
628
|
+
icon: LaunchIcon,
|
|
629
|
+
component: CrossDatasetDuplicatorTool,
|
|
630
|
+
options: {
|
|
631
|
+
docs: []
|
|
632
|
+
}
|
|
633
|
+
}), crossDatasetDuplicator = definePlugin((config = {}) => {
|
|
634
|
+
const pluginConfig = { ...DEFAULT_CONFIG, ...config }, { types } = pluginConfig;
|
|
635
|
+
return {
|
|
636
|
+
name: "@sanity/cross-dataset-duplicator",
|
|
637
|
+
tools: (prev) => pluginConfig.tool ? [...prev, crossDatasetDuplicatorTool()] : prev,
|
|
638
|
+
studio: {
|
|
639
|
+
components: {
|
|
640
|
+
layout: (props) => ConfigProvider({ ...props, pluginConfig })
|
|
641
|
+
}
|
|
642
|
+
},
|
|
643
|
+
document: {
|
|
644
|
+
actions: (prev, { schemaType }) => types && types.includes(schemaType) ? [...prev, DuplicateToAction] : prev
|
|
645
|
+
}
|
|
646
|
+
};
|
|
647
|
+
});
|
|
648
|
+
export {
|
|
649
|
+
CrossDatasetDuplicatorAction,
|
|
650
|
+
DuplicateToAction,
|
|
651
|
+
crossDatasetDuplicator,
|
|
652
|
+
useCrossDatasetDuplicatorConfig
|
|
653
|
+
};
|
|
654
|
+
//# sourceMappingURL=index.mjs.map
|