@sanity/cross-dataset-duplicator 0.3.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 (55) hide show
  1. package/.babelrc +3 -0
  2. package/.eslintignore +1 -0
  3. package/LICENSE +21 -0
  4. package/README.md +101 -0
  5. package/config.dist.json +4 -0
  6. package/lib/actions/DuplicateToAction.js +59 -0
  7. package/lib/actions/DuplicateToAction.js.map +1 -0
  8. package/lib/actions/index.js +38 -0
  9. package/lib/actions/index.js.map +1 -0
  10. package/lib/components/CrossDatasetDuplicator.js +107 -0
  11. package/lib/components/CrossDatasetDuplicator.js.map +1 -0
  12. package/lib/components/DuplicatorQuery.js +113 -0
  13. package/lib/components/DuplicatorQuery.js.map +1 -0
  14. package/lib/components/DuplicatorTool.js +557 -0
  15. package/lib/components/DuplicatorTool.js.map +1 -0
  16. package/lib/components/Feedback.js +27 -0
  17. package/lib/components/Feedback.js.map +1 -0
  18. package/lib/components/ResetSecret.js +43 -0
  19. package/lib/components/ResetSecret.js.map +1 -0
  20. package/lib/components/SelectButtons.js +109 -0
  21. package/lib/components/SelectButtons.js.map +1 -0
  22. package/lib/components/StatusBadge.js +87 -0
  23. package/lib/components/StatusBadge.js.map +1 -0
  24. package/lib/helpers/clientConfig.js +11 -0
  25. package/lib/helpers/clientConfig.js.map +1 -0
  26. package/lib/helpers/constants.js +9 -0
  27. package/lib/helpers/constants.js.map +1 -0
  28. package/lib/helpers/getDocumentsInArray.js +80 -0
  29. package/lib/helpers/getDocumentsInArray.js.map +1 -0
  30. package/lib/helpers/index.js +30 -0
  31. package/lib/helpers/index.js.map +1 -0
  32. package/lib/index.js +24 -0
  33. package/lib/index.js.map +1 -0
  34. package/lib/tool/index.js +24 -0
  35. package/lib/tool/index.js.map +1 -0
  36. package/lib/types/index.js +2 -0
  37. package/lib/types/index.js.map +1 -0
  38. package/package.json +71 -0
  39. package/sanity.json +16 -0
  40. package/src/actions/DuplicateToAction.tsx +22 -0
  41. package/src/actions/index.ts +22 -0
  42. package/src/components/CrossDatasetDuplicator.tsx +100 -0
  43. package/src/components/DuplicatorQuery.tsx +93 -0
  44. package/src/components/DuplicatorTool.tsx +460 -0
  45. package/src/components/Feedback.tsx +18 -0
  46. package/src/components/ResetSecret.tsx +27 -0
  47. package/src/components/SelectButtons.tsx +80 -0
  48. package/src/components/StatusBadge.tsx +97 -0
  49. package/src/helpers/clientConfig.ts +1 -0
  50. package/src/helpers/constants.ts +1 -0
  51. package/src/helpers/getDocumentsInArray.ts +74 -0
  52. package/src/helpers/index.ts +23 -0
  53. package/src/index.js +7 -0
  54. package/src/tool/index.ts +13 -0
  55. package/src/types/index.ts +10 -0
@@ -0,0 +1,80 @@
1
+ import React, {useState, useEffect} from 'react'
2
+ import {Button, Card, Flex} from '@sanity/ui'
3
+ import {typeIsAsset} from '../helpers'
4
+ import {PayloadItem} from '../types'
5
+
6
+ const buttons = [`All`, `None`, null, `New`, `Existing`, `Older`, null, `Documents`, `Assets`]
7
+
8
+ type SelectButtonsProps = {
9
+ payload: PayloadItem[]
10
+ setPayload: Function
11
+ }
12
+
13
+ export default function SelectButtons(props: SelectButtonsProps) {
14
+ const {payload, setPayload} = props
15
+ const [disabledActions, setDisabledActions] = useState([])
16
+
17
+ // Set intiial disabled button
18
+ useEffect(() => {
19
+ if (!disabledActions?.length && payload.every((item) => item.include)) {
20
+ setDisabledActions([`ALL`])
21
+ }
22
+ }, [])
23
+
24
+ function handleSelectButton(action = ``) {
25
+ if (!action || !payload.length) return
26
+
27
+ const newPayload = [...payload]
28
+
29
+ switch (action) {
30
+ case 'ALL':
31
+ newPayload.map((item) => (item.include = true))
32
+ break
33
+ case 'NONE':
34
+ newPayload.map((item) => (item.include = false))
35
+ break
36
+ case 'NEW':
37
+ newPayload.map((item) => (item.include = Boolean(item.status === 'CREATE')))
38
+ break
39
+ case 'EXISTING':
40
+ newPayload.map((item) => (item.include = Boolean(item.status === 'EXISTS')))
41
+ break
42
+ case 'OLDER':
43
+ newPayload.map((item) => (item.include = Boolean(item.status === 'OVERWRITE')))
44
+ break
45
+ case 'ASSETS':
46
+ newPayload.map((item) => (item.include = typeIsAsset(item.doc._type)))
47
+ break
48
+ case 'DOCUMENTS':
49
+ newPayload.map((item) => (item.include = !typeIsAsset(item.doc._type)))
50
+ break
51
+ default:
52
+ break
53
+ }
54
+
55
+ setDisabledActions([action])
56
+ setPayload(newPayload)
57
+ }
58
+
59
+ return (
60
+ <Card padding={1} radius={3} shadow={1}>
61
+ <Flex gap={2}>
62
+ {buttons.map((action, actionIndex) =>
63
+ action ? (
64
+ <Button
65
+ key={action}
66
+ fontSize={1}
67
+ mode="bleed"
68
+ padding={2}
69
+ text={action}
70
+ disabled={disabledActions.includes(action.toUpperCase())}
71
+ onClick={() => handleSelectButton(action.toUpperCase())}
72
+ />
73
+ ) : (
74
+ <Card key={`divider-${actionIndex}`} borderLeft />
75
+ )
76
+ )}
77
+ </Flex>
78
+ </Card>
79
+ )
80
+ }
@@ -0,0 +1,97 @@
1
+ import React from 'react'
2
+ import {Box, Text, Badge, Tooltip} from '@sanity/ui'
3
+ import type {BadgeTone} from '@sanity/ui'
4
+
5
+ type StatusTones = {
6
+ EXISTS: BadgeTone
7
+ OVERWRITE: BadgeTone
8
+ UPDATE: BadgeTone
9
+ CREATE: BadgeTone
10
+ }
11
+
12
+ const documentTones: StatusTones = {
13
+ EXISTS: `primary`,
14
+ OVERWRITE: `critical`,
15
+ UPDATE: `caution`,
16
+ CREATE: `positive`,
17
+ }
18
+
19
+ const assetTones: StatusTones = {
20
+ EXISTS: `critical`,
21
+ OVERWRITE: `critical`,
22
+ UPDATE: `critical`,
23
+ CREATE: `positive`,
24
+ }
25
+
26
+ type messageTypes = {
27
+ EXISTS: string
28
+ OVERWRITE: string
29
+ UPDATE: string
30
+ CREATE: string
31
+ }
32
+
33
+ const documentMessages: messageTypes = {
34
+ // Only happens once document is copied the first time, and _updatedAt is the same
35
+ EXISTS: `This document already exists at the Destination with the same ID with the same Updated time.`,
36
+ // Is true immediately after transaction as _updatedAt is updated by API after mutation
37
+ // Is also true if the document at the destination has been manually modified
38
+ // Presently, the plugin doesn't actually compare the two documents
39
+ OVERWRITE: `A newer version of this document exists at the Destination, and it will be overwritten with this version.`,
40
+ // Document at destination is older
41
+ UPDATE: `An older version of this document exists at the Destination, and it will be overwritten with this version.`,
42
+ // Document at destination doesn't exist
43
+ CREATE: `This document will be created at the destination.`,
44
+ }
45
+
46
+ const assetMessages: messageTypes = {
47
+ EXISTS: `This Asset already exists at the Destination`,
48
+ OVERWRITE: `This Asset already exists at the Destination`,
49
+ UPDATE: `This Asset already exists at the Destination`,
50
+ CREATE: `This Asset does not yet exist at the Destination`,
51
+ }
52
+
53
+ const assetStatus: messageTypes = {
54
+ EXISTS: `RE-UPLOAD`,
55
+ OVERWRITE: `RE-UPLOAD`,
56
+ UPDATE: `RE-UPLOAD`,
57
+ CREATE: `UPLOAD`,
58
+ }
59
+
60
+ type StatusBadgeProps = {
61
+ status: 'EXISTS' | 'OVERWRITE' | 'UPDATE' | 'CREATE' | undefined
62
+ isAsset: boolean
63
+ }
64
+
65
+ export default function StatusBadge(props: StatusBadgeProps) {
66
+ const {status, isAsset} = props
67
+
68
+ const badgeTone = isAsset ? assetTones[status] : documentTones[status]
69
+
70
+ if (!badgeTone) {
71
+ return (
72
+ <Badge muted padding={2} fontSize={1} mode="outline">
73
+ Checking...
74
+ </Badge>
75
+ )
76
+ }
77
+
78
+ const badgeText = isAsset ? assetMessages[status] : documentMessages[status]
79
+ const badgeStatus = isAsset ? assetStatus[status] : status
80
+
81
+ return (
82
+ <Tooltip
83
+ content={
84
+ <Box padding={3} style={{maxWidth: 200}}>
85
+ <Text size={1}>{badgeText}</Text>
86
+ </Box>
87
+ }
88
+ fallbackPlacements={['right', 'left']}
89
+ placement="top"
90
+ portal
91
+ >
92
+ <Badge muted padding={2} fontSize={1} tone={badgeTone} mode="outline">
93
+ {badgeStatus}
94
+ </Badge>
95
+ </Tooltip>
96
+ )
97
+ }
@@ -0,0 +1 @@
1
+ export const clientConfig = {apiVersion: `2021-05-19`}
@@ -0,0 +1 @@
1
+ export const SECRET_NAMESPACE = `CrossDatasetDuplicator`
@@ -0,0 +1,74 @@
1
+ import {extract} from '@sanity/mutator'
2
+ import {SanityDocument} from '../types'
3
+
4
+ // Recursively fetch Documents from an array of _id's and their references
5
+ // Heavy use of Set is to avoid recursively querying for id's already in the payload
6
+ export async function getDocumentsInArray(
7
+ fetchIds: string[],
8
+ client: any,
9
+ currentIds?: Set<string>
10
+ ) {
11
+ const collection = []
12
+
13
+ // Find initial docs
14
+ const data: SanityDocument[] = await client.fetch(`*[_id in $fetchIds]`, {
15
+ fetchIds: fetchIds ?? [],
16
+ })
17
+
18
+ if (!data?.length) {
19
+ return []
20
+ }
21
+
22
+ const localCurrentIds = currentIds ?? new Set()
23
+
24
+ // Find new ids in the returned data
25
+ // Unless we started with an empty set, get the _ids from the data
26
+ const newDataIds: Set<string> = new Set(
27
+ data
28
+ .map((dataDoc) => dataDoc._id)
29
+ .filter((id) => (currentIds?.size ? !localCurrentIds.has(id) : Boolean(id)))
30
+ )
31
+
32
+ if (newDataIds.size) {
33
+ collection.push(...data)
34
+ localCurrentIds.add(...newDataIds)
35
+
36
+ // Check new data for more references
37
+ await Promise.all(
38
+ data.map(async (doc) => {
39
+ const expr = `.._ref`
40
+ const references = extract(expr, doc)
41
+
42
+ if (references.length) {
43
+ // Find references not already in the Collection
44
+ const newReferenceIds = new Set(references.filter((refId) => !localCurrentIds.has(refId)))
45
+
46
+ if (newReferenceIds.size) {
47
+ // Recusive query for new documents
48
+ const referenceDocs = await getDocumentsInArray(
49
+ Array.from(newReferenceIds),
50
+ client,
51
+ localCurrentIds
52
+ )
53
+
54
+ if (referenceDocs?.length) {
55
+ collection.push(...referenceDocs)
56
+ }
57
+ }
58
+ }
59
+ })
60
+ )
61
+ }
62
+
63
+ // Create a unique array of objects from collection
64
+ // Set() wasn't working for unique id's ¯\_(ツ)_/¯
65
+ const uniqueCollection = collection.filter(Boolean).reduce((acc, cur) => {
66
+ if (acc.some((doc) => doc._id === cur._id)) {
67
+ return acc
68
+ }
69
+
70
+ return [...acc, cur]
71
+ }, [])
72
+
73
+ return uniqueCollection
74
+ }
@@ -0,0 +1,23 @@
1
+ export function typeIsAsset(type = ``) {
2
+ if (!type) return false
3
+
4
+ return ['sanity.imageAsset', 'sanity.fileAsset'].includes(type)
5
+ }
6
+
7
+ export function createInitialMessage(docCount = 0, refsCount = 0) {
8
+ const message = [
9
+ docCount === 1 ? `This Document contains` : `These ${docCount} Documents contain`,
10
+ refsCount === 1 ? `1 Reference.` : `${refsCount} References.`,
11
+ refsCount === 1 ? `That Document` : `Those Documents`,
12
+ `may have References too. If referenced Documents do not exist at the target Destination, this transaction will fail.`
13
+ ]
14
+
15
+ return message.join(` `)
16
+ }
17
+
18
+ export const stickyStyles = {
19
+ position: 'sticky',
20
+ top: 0,
21
+ zIndex: 100,
22
+ backgroundColor: `rgba(255,255,255,0.95)`,
23
+ }
package/src/index.js ADDED
@@ -0,0 +1,7 @@
1
+ import DuplicateToAction from "./actions/DuplicateToAction";
2
+ import CrossDatasetDuplicator from "./components/CrossDatasetDuplicator";
3
+
4
+ export {
5
+ DuplicateToAction,
6
+ CrossDatasetDuplicator
7
+ }
@@ -0,0 +1,13 @@
1
+ import {LaunchIcon} from '@sanity/icons'
2
+ import config from 'config:@sanity/cross-dataset-duplicator'
3
+
4
+ import CrossDatasetDuplicator from '../components/CrossDatasetDuplicator'
5
+
6
+ export default config?.tool
7
+ ? {
8
+ title: 'Duplicator',
9
+ name: 'duplicator',
10
+ icon: LaunchIcon,
11
+ component: CrossDatasetDuplicator,
12
+ }
13
+ : null
@@ -0,0 +1,10 @@
1
+ export type SanityDocument = {
2
+ _id: string
3
+ _type: string
4
+ }
5
+
6
+ export type PayloadItem = {
7
+ include: boolean
8
+ status: 'EXISTS' | 'OVERWRITE' | 'UPDATE' | 'CREATE'
9
+ doc: SanityDocument
10
+ }