@sanity/cross-dataset-duplicator 1.5.0 → 2.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.
@@ -1,67 +0,0 @@
1
- import {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, onDuplicated} = props
10
- const [inbound, setInbound] = useState<SanityDocument[]>([])
11
- const {follow = [], apiVersion} = 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({apiVersion})
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
- onDuplicated={onDuplicated}
64
- />
65
- </Container>
66
- )
67
- }
@@ -1,18 +0,0 @@
1
- import React from 'react'
2
- import {Card, Text} from '@sanity/ui'
3
- import type {BadgeTone} from '@sanity/ui'
4
-
5
- type FeedbackProps = {
6
- children?: React.ReactNode
7
- tone?: BadgeTone
8
- }
9
-
10
- export default function Feedback(props: FeedbackProps) {
11
- const {children, tone = `caution`} = props
12
-
13
- return (
14
- <Card padding={3} radius={2} shadow={1} tone={tone}>
15
- <Text size={1}>{children}</Text>
16
- </Card>
17
- )
18
- }
@@ -1,30 +0,0 @@
1
- import {useCallback} from 'react'
2
- import {useClient} from 'sanity'
3
- import {Button, Flex} from '@sanity/ui'
4
-
5
- import {SECRET_NAMESPACE} from '../helpers/constants'
6
-
7
- type ResetSecretProps = {
8
- apiVersion: string
9
- }
10
-
11
- export default function ResetSecret({apiVersion}: ResetSecretProps) {
12
- const client = useClient({apiVersion})
13
-
14
- const handleClick = useCallback(() => {
15
- client.delete({query: `*[_id == "secrets.${SECRET_NAMESPACE}"]`})
16
- }, [client])
17
-
18
- return (
19
- <Flex align="center" justify="flex-end" paddingX={[2, 2, 2, 5]} paddingY={5}>
20
- <Button
21
- text="Reset Secret"
22
- onClick={handleClick}
23
- mode="ghost"
24
- tone="critical"
25
- fontSize={1}
26
- padding={2}
27
- />
28
- </Flex>
29
- )
30
- }
@@ -1,84 +0,0 @@
1
- import {useState, useEffect} from 'react'
2
- import {Button, Card, Flex} from '@sanity/ui'
3
-
4
- import {PayloadItem} from './Duplicator'
5
- import {isAssetId} from '@sanity/asset-utils'
6
-
7
- const buttons = [`All`, `None`, null, `New`, `Existing`, `Older`, null, `Documents`, `Assets`]
8
-
9
- type Action = 'ALL' | 'NONE' | 'NEW' | 'EXISTING' | 'OLDER' | 'ASSETS' | 'DOCUMENTS'
10
-
11
- type SelectButtonsProps = {
12
- payload: PayloadItem[]
13
- setPayload: Function
14
- }
15
-
16
- export default function SelectButtons(props: SelectButtonsProps) {
17
- const {payload, setPayload} = props
18
- const [disabledActions, setDisabledActions] = useState<Action[]>([])
19
-
20
- // Set intiial disabled button
21
- useEffect(() => {
22
- if (!disabledActions?.length && payload.every((item) => item.include)) {
23
- setDisabledActions([`ALL`])
24
- }
25
- }, [disabledActions?.length, payload])
26
-
27
- function handleSelectButton(action?: Action) {
28
- if (!action || !payload.length) return
29
-
30
- const newPayload = [...payload]
31
-
32
- switch (action) {
33
- case 'ALL':
34
- newPayload.map((item) => (item.include = true))
35
- break
36
- case 'NONE':
37
- newPayload.map((item) => (item.include = false))
38
- break
39
- case 'NEW':
40
- newPayload.map((item) => (item.include = Boolean(item.status === 'CREATE')))
41
- break
42
- case 'EXISTING':
43
- newPayload.map((item) => (item.include = Boolean(item.status === 'EXISTS')))
44
- break
45
- case 'OLDER':
46
- newPayload.map((item) => (item.include = Boolean(item.status === 'OVERWRITE')))
47
- break
48
- case 'ASSETS':
49
- newPayload.map((item) => (item.include = isAssetId(item.doc._id)))
50
- break
51
- case 'DOCUMENTS':
52
- newPayload.map((item) => (item.include = !isAssetId(item.doc._id)))
53
- break
54
- default:
55
- break
56
- }
57
-
58
- setDisabledActions([action])
59
- setPayload(newPayload)
60
- }
61
-
62
- return (
63
- <Card padding={1} radius={3} shadow={1}>
64
- <Flex gap={2} wrap="wrap">
65
- {buttons.map((action, actionIndex) =>
66
- action ? (
67
- <Button
68
- key={action}
69
- fontSize={1}
70
- mode="bleed"
71
- padding={2}
72
- text={action}
73
- disabled={disabledActions.includes(action.toUpperCase() as Action)}
74
- onClick={() => handleSelectButton(action.toUpperCase() as Action)}
75
- />
76
- ) : (
77
- // eslint-disable-next-line react/no-array-index-key
78
- <Card key={`divider-${actionIndex}`} borderLeft />
79
- )
80
- )}
81
- </Flex>
82
- </Card>
83
- )
84
- }
@@ -1,111 +0,0 @@
1
- import {InfoOutlineIcon} from '@sanity/icons'
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
- UNPUBLISHED: BadgeTone
11
- }
12
-
13
- const documentTones: StatusTones = {
14
- EXISTS: `primary`,
15
- OVERWRITE: `critical`,
16
- UPDATE: `caution`,
17
- CREATE: `positive`,
18
- UNPUBLISHED: `caution`,
19
- }
20
-
21
- const assetTones: StatusTones = {
22
- EXISTS: `critical`,
23
- OVERWRITE: `critical`,
24
- UPDATE: `critical`,
25
- CREATE: `positive`,
26
- UNPUBLISHED: `default`,
27
- }
28
-
29
- export type MessageTypes = {
30
- EXISTS: string
31
- OVERWRITE: string
32
- UPDATE: string
33
- CREATE: string
34
- UNPUBLISHED: string
35
- }
36
-
37
- const documentMessages: MessageTypes = {
38
- // Only happens once document is copied the first time, and _updatedAt is the same
39
- EXISTS: `This document already exists at the Destination with the same ID with the same Updated time.`,
40
- // Is true immediately after transaction as _updatedAt is updated by API after mutation
41
- // Is also true if the document at the destination has been manually modified
42
- // Presently, the plugin doesn't actually compare the two documents
43
- OVERWRITE: `A newer version of this document exists at the Destination, and it will be overwritten with this version.`,
44
- // Document at destination is older
45
- UPDATE: `An older version of this document exists at the Destination, and it will be overwritten with this version.`,
46
- // Document at destination doesn't exist
47
- CREATE: `This document will be created at the destination.`,
48
- UNPUBLISHED: `A Draft version of this Document exists in this Dataset, but only the Published version will be duplicated to the destination.`,
49
- }
50
-
51
- const assetMessages: MessageTypes = {
52
- EXISTS: `This Asset already exists at the Destination`,
53
- OVERWRITE: `This Asset already exists at the Destination`,
54
- UPDATE: `This Asset already exists at the Destination`,
55
- CREATE: `This Asset does not yet exist at the Destination`,
56
- UNPUBLISHED: ``,
57
- }
58
-
59
- const assetStatus: MessageTypes = {
60
- EXISTS: `RE-UPLOAD`,
61
- OVERWRITE: `RE-UPLOAD`,
62
- UPDATE: `RE-UPLOAD`,
63
- CREATE: `UPLOAD`,
64
- UNPUBLISHED: ``,
65
- }
66
-
67
- type StatusBadgeProps = {
68
- isAsset: boolean
69
- status?: keyof MessageTypes
70
- }
71
-
72
- export default function StatusBadge(props: StatusBadgeProps) {
73
- const {status, isAsset} = props
74
-
75
- if (!status) {
76
- return null
77
- }
78
-
79
- const badgeTone = isAsset ? assetTones[status] : documentTones[status]
80
-
81
- if (!badgeTone) {
82
- return (
83
- <Badge muted padding={2} fontSize={1} mode="outline">
84
- Checking...
85
- </Badge>
86
- )
87
- }
88
-
89
- const badgeText = isAsset ? assetMessages[status] : documentMessages[status]
90
- const badgeStatus = isAsset ? assetStatus[status] : status
91
-
92
- return (
93
- <Tooltip
94
- content={
95
- <Box padding={3} style={{maxWidth: 200}}>
96
- <Text size={1}>{badgeText}</Text>
97
- </Box>
98
- }
99
- fallbackPlacements={['right', 'left']}
100
- placement="top"
101
- portal
102
- >
103
- <Badge muted padding={3} fontSize={1} tone={badgeTone} mode="outline">
104
- {badgeStatus}
105
- <Box marginLeft={2} display={'inline-block'} as="span">
106
- <InfoOutlineIcon />
107
- </Box>
108
- </Badge>
109
- </Tooltip>
110
- )
111
- }
@@ -1,30 +0,0 @@
1
- import {useContext} from 'react'
2
- import {createContext} from 'react'
3
- import {LayoutProps} from 'sanity'
4
-
5
- import {DEFAULT_CONFIG} from '../helpers/constants'
6
- import {PluginConfig} from '../types'
7
-
8
- const CrossDatasetDuplicatorContext = createContext(DEFAULT_CONFIG)
9
-
10
- type ConfigProviderProps = LayoutProps & {pluginConfig: Required<PluginConfig>}
11
-
12
- /**
13
- * Plugin config context hook from the Cross Dataset Duplicator plugin
14
- * @public
15
- */
16
- export function useCrossDatasetDuplicatorConfig() {
17
- const pluginConfig = useContext(CrossDatasetDuplicatorContext)
18
-
19
- return pluginConfig
20
- }
21
-
22
- export function ConfigProvider(props: ConfigProviderProps) {
23
- const {pluginConfig, ...rest} = props
24
-
25
- return (
26
- <CrossDatasetDuplicatorContext.Provider value={pluginConfig}>
27
- {props.renderDefault(rest)}
28
- </CrossDatasetDuplicatorContext.Provider>
29
- )
30
- }
@@ -1,12 +0,0 @@
1
- import {PluginConfig} from '../types'
2
-
3
- export const SECRET_NAMESPACE = `CrossDatasetDuplicator`
4
-
5
- export const DEFAULT_CONFIG: Required<PluginConfig> = {
6
- apiVersion: '2025-02-19',
7
- tool: true,
8
- types: [],
9
- filter: '',
10
- follow: ['outbound'],
11
- queries: [],
12
- }
@@ -1,86 +0,0 @@
1
- import {extractWithPath} from '@sanity/mutator'
2
- import {SanityClient, SanityDocument} from 'sanity'
3
- import {PluginConfig} from '../types'
4
-
5
- type OptionsBag = {
6
- fetchIds: string[]
7
- client: SanityClient
8
- pluginConfig: PluginConfig
9
- currentIds?: Set<string> | null
10
- projection?: string
11
- }
12
-
13
- // Recursively fetch Documents from an array of _id's and their references
14
- // Heavy use of Set is to avoid recursively querying for id's already in the payload
15
- export async function getDocumentsInArray(options: OptionsBag): Promise<SanityDocument[]> {
16
- const {fetchIds, client, pluginConfig, currentIds, projection} = options
17
- const collection: SanityDocument[] = []
18
-
19
- // Find initial docs
20
- const filter = ['_id in $fetchIds', pluginConfig.filter].filter(Boolean).join(' && ')
21
- const query = `*[${filter}]${projection ?? ``}`
22
- const data: SanityDocument[] = await client.fetch(query, {
23
- fetchIds: fetchIds ?? [],
24
- })
25
-
26
- if (!data?.length) {
27
- return []
28
- }
29
-
30
- const localCurrentIds = currentIds ?? new Set<string>()
31
-
32
- // Find new ids in the returned data
33
- // Unless we started with an empty set, get the _ids from the data
34
- const newDataIds = new Set<string>(
35
- data
36
- .map((dataDoc) => dataDoc._id)
37
- .filter((id) => (currentIds?.size ? !localCurrentIds.has(id) : Boolean(id)))
38
- )
39
-
40
- if (newDataIds.size) {
41
- collection.push(...data)
42
- // @ts-ignore
43
- localCurrentIds.add(...newDataIds)
44
-
45
- // Check new data for more references
46
- await Promise.all(
47
- data.map(async (doc) => {
48
- const expr = `.._ref`
49
- const references: string[] = extractWithPath(expr, doc).map((ref) => ref.value as string)
50
-
51
- if (references.length) {
52
- // Find references not already in the Collection
53
- const newReferenceIds = new Set<string>(
54
- references.filter((ref) => !localCurrentIds.has(ref))
55
- )
56
-
57
- if (newReferenceIds.size) {
58
- // Recursive query for new documents
59
- const referenceDocs = await getDocumentsInArray({
60
- fetchIds: Array.from(newReferenceIds),
61
- currentIds: localCurrentIds,
62
- client,
63
- pluginConfig,
64
- })
65
-
66
- if (referenceDocs?.length) {
67
- collection.push(...referenceDocs)
68
- }
69
- }
70
- }
71
- })
72
- )
73
- }
74
-
75
- // Create a unique array of objects from collection
76
- // Set() wasn't working for unique id's ¯\_(ツ)_/¯
77
- const uniqueCollection = collection.filter(Boolean).reduce((acc: SanityDocument[], cur) => {
78
- if (acc.some((doc) => doc._id === cur._id)) {
79
- return acc
80
- }
81
-
82
- return [...acc, cur]
83
- }, [])
84
-
85
- return uniqueCollection
86
- }
@@ -1,19 +0,0 @@
1
- import {CSSProperties} from 'react'
2
-
3
- export function createInitialMessage(docCount = 0, refsCount = 0): string {
4
- const message = [
5
- docCount === 1 ? `This Document contains` : `These ${docCount} Documents contain`,
6
- refsCount === 1 ? `1 Reference.` : `${refsCount} References.`,
7
- refsCount === 1 ? `That Document` : `Those Documents`,
8
- `may have References too. If referenced Documents do not exist at the target Destination, this transaction will fail.`,
9
- ]
10
-
11
- return message.join(` `)
12
- }
13
-
14
- export const stickyStyles = (isDarkMode = true): CSSProperties => ({
15
- position: 'sticky',
16
- top: 0,
17
- zIndex: 100,
18
- backgroundColor: isDarkMode ? `rgba(10,10,10,0.95)` : `rgba(255,255,255,0.95)`,
19
- })
package/src/index.ts DELETED
@@ -1,5 +0,0 @@
1
- export * from './types'
2
- export * from './plugin'
3
- export * from './actions/DuplicateToAction'
4
- export {useCrossDatasetDuplicatorConfig} from './context/ConfigProvider'
5
- export {CrossDatasetDuplicatorAction} from './components/CrossDatasetDuplicatorAction'
package/src/plugin.tsx DELETED
@@ -1,31 +0,0 @@
1
- import {definePlugin} from 'sanity'
2
-
3
- import {DuplicateToAction} from './actions/DuplicateToAction'
4
- import {ConfigProvider} from './context/ConfigProvider'
5
- import {DEFAULT_CONFIG} from './helpers/constants'
6
- import {crossDatasetDuplicatorTool} from './tool'
7
- import {PluginConfig} from './types'
8
-
9
- /**
10
- * Plugin: Cross Dataset Duplicator
11
- * @public
12
- */
13
- export const crossDatasetDuplicator = definePlugin<Partial<PluginConfig> | void>((config = {}) => {
14
- const pluginConfig = {...DEFAULT_CONFIG, ...config}
15
- const {types} = pluginConfig
16
-
17
- return {
18
- name: '@sanity/cross-dataset-duplicator',
19
- tools: (prev) => (pluginConfig.tool ? [...prev, crossDatasetDuplicatorTool()] : prev),
20
- studio: {
21
- components: {
22
- layout: (props) => ConfigProvider({...props, pluginConfig}),
23
- },
24
- },
25
- document: {
26
- actions: (prev, {schemaType}) => {
27
- return types && types.includes(schemaType) ? [...prev, DuplicateToAction] : prev
28
- },
29
- },
30
- }
31
- })
package/src/tool/index.ts DELETED
@@ -1,14 +0,0 @@
1
- import type {Tool} from 'sanity'
2
- import {LaunchIcon} from '@sanity/icons'
3
-
4
- import {CrossDatasetDuplicatorTool, MultiToolConfig} from '../components/CrossDatasetDuplicatorTool'
5
-
6
- export const crossDatasetDuplicatorTool = (): Tool<MultiToolConfig> => ({
7
- title: 'Duplicator',
8
- name: 'duplicator',
9
- icon: LaunchIcon,
10
- component: CrossDatasetDuplicatorTool,
11
- options: {
12
- docs: [],
13
- },
14
- })
@@ -1,27 +0,0 @@
1
- import {SanityDocument} from 'sanity'
2
-
3
- type PreDefinedQuery = {
4
- label: string
5
- query: string
6
- }
7
- /**
8
- * Plugin configuration
9
- * @public
10
- */
11
- export interface PluginConfig {
12
- apiVersion?: string
13
- tool?: boolean
14
- types?: string[]
15
- filter?: string
16
- follow?: ('inbound' | 'outbound')[]
17
- queries?: PreDefinedQuery[]
18
- }
19
-
20
- /**
21
- * Cross Dataset Duplicator document action props
22
- * @public
23
- */
24
- export type CrossDatasetDuplicatorActionProps = {
25
- docs: SanityDocument[]
26
- onDuplicated?: () => Promise<void>
27
- }
@@ -1,11 +0,0 @@
1
- const {showIncompatiblePluginDialog} = require('@sanity/incompatible-plugin')
2
- const {name, version, sanityExchangeUrl} = require('./package.json')
3
-
4
- export default showIncompatiblePluginDialog({
5
- name: name,
6
- versions: {
7
- v3: version,
8
- v2: undefined,
9
- },
10
- sanityExchangeUrl,
11
- })