Skip to content

Commit 8d65f8f

Browse files
feat: added duplicate action to doc list (#68)
1 parent 186af27 commit 8d65f8f

7 files changed

+134
-11
lines changed

README.md

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# sanity-plugin-documents-pane
22

3-
>This is a **Sanity Studio v3** plugin.
3+
> This is a **Sanity Studio v3** plugin.
44
> For the v2 version, please refer to the [v2-branch](https://github.com/sanity-io/sanity-plugin-documents-pane/tree/studio-v2).
55
66
Displays the results of a GROQ query in a View Pane. With the ability to use field values in the current document as query parameters.
@@ -34,7 +34,7 @@ S.view
3434
.options({
3535
query: `*[references($id)]`,
3636
params: {id: `_id`},
37-
options: {perspective: 'previewDrafts'}
37+
options: {perspective: 'previewDrafts'},
3838
})
3939
.title('Incoming References')
4040
```
@@ -49,6 +49,7 @@ The `.options()` configuration works as follows:
4949
- `debug` (bool, optional, default: `false`) In case of an error or the query returning no documents, setting to `true` will display the query and params that were used.
5050
- `initialValueTemplates` (function, optional) A function that receives the various displayed, draft, and published versions of the document, and returns a list of initial value templates. These will be used to define buttons at the top of the list so users can create new related documents.
5151
- `options` (object, optional) An object of options passed to the listening query. Includes support for `apiVersion` and `perspective`.
52+
- `duplicate` (bool, optional, default: `false`) Enables a duplicate action in the context of the document list of the document pane. Useful for retaining existing editing context when needing to create new incoming references.
5253

5354
## Resolving query parameters with a function and providing initial value templates
5455

@@ -118,4 +119,3 @@ Run ["CI & Release" workflow](https://github.com/sanity-io/sanity-plugin-documen
118119
Make sure to select the main branch and check "Release new version".
119120
120121
Semantic release will only release on configured branches, so it is safe to run release on any branch.
121-

package-lock.json

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Documents.tsx

+20-3
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,34 @@
11
import React, {useCallback} from 'react'
22
import {Box, Button, Stack, Flex, Spinner, Card} from '@sanity/ui'
33
import {fromString as pathFromString} from '@sanity/util/paths'
4-
import {Preview, useSchema, DefaultPreview, SanityDocument, ListenQueryOptions} from 'sanity'
4+
import {
5+
Preview,
6+
useSchema,
7+
DefaultPreview,
8+
SanityDocument,
9+
ListenQueryOptions,
10+
getPublishedId,
11+
} from 'sanity'
512
import {usePaneRouter} from 'sanity/structure'
613
import {WarningOutlineIcon} from '@sanity/icons'
714
import {Feedback, useListeningQuery} from 'sanity-plugin-utils'
815

916
import Debug from './Debug'
1017
import {DocumentsPaneInitialValueTemplate} from './types'
1118
import NewDocument from './NewDocument'
19+
import DuplicateDocument from './DuplicateDocument'
1220

1321
type DocumentsProps = {
1422
query: string
1523
params: {[key: string]: string}
1624
debug: boolean
1725
initialValueTemplates: DocumentsPaneInitialValueTemplate[]
1826
options: ListenQueryOptions
27+
duplicate: boolean
1928
}
2029

2130
export default function Documents(props: DocumentsProps) {
22-
const {query, params, options, debug, initialValueTemplates} = props
31+
const {query, params, options, debug, initialValueTemplates, duplicate} = props
2332
const {routerPanesState, groupIndex, handleEditReference} = usePaneRouter()
2433
const schema = useSchema()
2534

@@ -91,11 +100,19 @@ export default function Documents(props: DocumentsProps) {
91100
return schemaType ? (
92101
<Button
93102
key={doc._id}
103+
// eslint-disable-next-line react/jsx-no-bind
94104
onClick={() => handleClick(doc._id, doc._type)}
95105
padding={2}
96106
mode="bleed"
97107
>
98-
<Preview value={doc} schemaType={schemaType} />
108+
<Preview
109+
value={doc}
110+
schemaType={schemaType}
111+
actions={
112+
duplicate && <DuplicateDocument id={getPublishedId(doc._id)} type={doc._type} />
113+
}
114+
layout="block"
115+
/>
99116
</Button>
100117
) : (
101118
<Card radius={2} tone="caution" data-ui="Alert" padding={2} key={doc._id}>

src/DocumentsPane.tsx

+2
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export default function DocumentsPane(props: DocumentsPaneProps) {
1717
debug = false,
1818
initialValueTemplates: initialValueTemplatesResolver,
1919
options = {},
20+
duplicate = false,
2021
} = props.options
2122

2223
if (useDraft && typeof params === 'function') {
@@ -57,6 +58,7 @@ export default function DocumentsPane(props: DocumentsPaneProps) {
5758
options={options}
5859
debug={debug}
5960
initialValueTemplates={initialValueTemplates}
61+
duplicate={duplicate}
6062
/>
6163
)
6264
}

src/DuplicateDocument.tsx

+99
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import {Box, Button, Tooltip, Text} from '@sanity/ui'
2+
import React, {useState, useCallback} from 'react'
3+
import {filter, firstValueFrom} from 'rxjs'
4+
import {CopyIcon} from '@sanity/icons'
5+
import {
6+
useDocumentOperation,
7+
useDocumentPairPermissions,
8+
useDocumentStore,
9+
useTranslation,
10+
} from 'sanity'
11+
import {usePaneRouter} from 'sanity/structure'
12+
import {uuid} from '@sanity/uuid'
13+
import {fromString as pathFromString} from '@sanity/util/paths'
14+
15+
import {structureLocaleNamespace} from 'sanity/structure'
16+
17+
interface NewDocumentProps {
18+
id: string
19+
type: string
20+
}
21+
22+
export default function DuplicateDocument(props: NewDocumentProps) {
23+
const {id, type} = props
24+
25+
const documentStore = useDocumentStore()
26+
const {duplicate} = useDocumentOperation(id, type)
27+
const {routerPanesState, groupIndex, handleEditReference} = usePaneRouter()
28+
const [isDuplicating, setDuplicating] = useState(false)
29+
const [permissions, isPermissionsLoading] = useDocumentPairPermissions({
30+
id,
31+
type,
32+
permission: 'duplicate',
33+
})
34+
35+
const {t} = useTranslation(structureLocaleNamespace)
36+
37+
const handle = useCallback(
38+
async (event: React.MouseEvent<HTMLElement>) => {
39+
event.stopPropagation()
40+
const dupeId = uuid()
41+
42+
setDuplicating(true)
43+
44+
// set up the listener before executing
45+
const duplicateSuccess = firstValueFrom(
46+
documentStore.pair
47+
.operationEvents(id, type)
48+
.pipe(filter((e) => e.op === 'duplicate' && e.type === 'success'))
49+
)
50+
duplicate.execute(dupeId)
51+
52+
// only navigate to the duplicated document when the operation is successful
53+
await duplicateSuccess
54+
setDuplicating(false)
55+
56+
const childParams = routerPanesState[groupIndex + 1]?.[0].params || {}
57+
const {parentRefPath} = childParams
58+
59+
handleEditReference({
60+
id: dupeId,
61+
type,
62+
parentRefPath: parentRefPath ? pathFromString(parentRefPath) : [``],
63+
template: {id: dupeId},
64+
})
65+
},
66+
[documentStore.pair, duplicate, groupIndex, handleEditReference, id, routerPanesState, type]
67+
)
68+
69+
if (isPermissionsLoading || !permissions?.granted) {
70+
return <></>
71+
}
72+
73+
return (
74+
<Tooltip
75+
content={
76+
<Box>
77+
<Text muted size={1}>
78+
{t('action.duplicate.label')}
79+
</Text>
80+
</Box>
81+
}
82+
placement="left"
83+
portal
84+
>
85+
<Button
86+
onClick={handle}
87+
padding={2}
88+
fontSize={1}
89+
as={Box}
90+
icon={<CopyIcon />}
91+
mode="ghost"
92+
tone="default"
93+
aria-label={t('action.duplicate.label')}
94+
style={{cursor: 'pointer'}}
95+
disabled={isDuplicating || Boolean(duplicate.disabled) || isPermissionsLoading}
96+
/>
97+
</Tooltip>
98+
)
99+
}

src/NewDocument.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import {Button, Card, Flex} from '@sanity/ui'
22
import React from 'react'
33
import {DocumentsPaneInitialValueTemplate} from './types'
44
import {ComposeIcon} from '@sanity/icons'
5-
import {usePaneRouter} from 'sanity/desk'
5+
import {usePaneRouter} from 'sanity/structure'
66
import {uuid} from '@sanity/uuid'
77

88
interface NewDocumentProps {
@@ -16,7 +16,7 @@ export default function NewDocument(props: NewDocumentProps) {
1616
if (!initialValueTemplates.length) return null
1717

1818
return (
19-
<Card borderBottom={true} padding={2}>
19+
<Card borderBottom padding={2}>
2020
<Flex justify="flex-end" gap={1}>
2121
{initialValueTemplates.map((template) => {
2222
if (!template.template) {

src/types.ts

+7-2
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ import {UserViewComponent} from 'sanity/structure'
55
export type DocumentVersionsCollection = React.ComponentProps<UserViewComponent>['document']
66

77
// eslint-disable-next-line prettier/prettier
8-
export type DocumentsPaneQueryParams = (params: {document: DocumentVersionsCollection}) => ({[key: string]: string}) | {[key: string]: string}
8+
export type DocumentsPaneQueryParams = (params: {
9+
document: DocumentVersionsCollection
10+
}) => {[key: string]: string} | {[key: string]: string}
911

1012
export interface DocumentsPaneInitialValueTemplate {
1113
schemaType: string
@@ -15,7 +17,9 @@ export interface DocumentsPaneInitialValueTemplate {
1517
}
1618

1719
// eslint-disable-next-line prettier/prettier
18-
export type DocumentsPaneInitialValueTemplateResolver = (params: {document: DocumentVersionsCollection}) => DocumentsPaneInitialValueTemplate[]
20+
export type DocumentsPaneInitialValueTemplateResolver = (params: {
21+
document: DocumentVersionsCollection
22+
}) => DocumentsPaneInitialValueTemplate[]
1923

2024
export type DocumentsPaneOptions = {
2125
query: string
@@ -24,6 +28,7 @@ export type DocumentsPaneOptions = {
2428
useDraft?: boolean
2529
initialValueTemplates?: DocumentsPaneInitialValueTemplateResolver
2630
options?: ListenQueryOptions
31+
duplicate?: boolean
2732
}
2833

2934
export type DocumentsPaneProps = React.ComponentProps<UserViewComponent<DocumentsPaneOptions>>

0 commit comments

Comments
 (0)