Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: added duplicate action to doc list #68

Merged
merged 1 commit into from
May 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# sanity-plugin-documents-pane

>This is a **Sanity Studio v3** plugin.
> This is a **Sanity Studio v3** plugin.
> For the v2 version, please refer to the [v2-branch](https://github.com/sanity-io/sanity-plugin-documents-pane/tree/studio-v2).

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.
Expand Down Expand Up @@ -34,7 +34,7 @@ S.view
.options({
query: `*[references($id)]`,
params: {id: `_id`},
options: {perspective: 'previewDrafts'}
options: {perspective: 'previewDrafts'},
})
.title('Incoming References')
```
Expand All @@ -49,6 +49,7 @@ The `.options()` configuration works as follows:
- `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.
- `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.
- `options` (object, optional) An object of options passed to the listening query. Includes support for `apiVersion` and `perspective`.
- `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.

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

Expand Down Expand Up @@ -118,4 +119,3 @@ Run ["CI & Release" workflow](https://github.com/sanity-io/sanity-plugin-documen
Make sure to select the main branch and check "Release new version".

Semantic release will only release on configured branches, so it is safe to run release on any branch.

2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

23 changes: 20 additions & 3 deletions src/Documents.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,34 @@
import React, {useCallback} from 'react'
import {Box, Button, Stack, Flex, Spinner, Card} from '@sanity/ui'
import {fromString as pathFromString} from '@sanity/util/paths'
import {Preview, useSchema, DefaultPreview, SanityDocument, ListenQueryOptions} from 'sanity'
import {
Preview,
useSchema,
DefaultPreview,
SanityDocument,
ListenQueryOptions,
getPublishedId,
} from 'sanity'
import {usePaneRouter} from 'sanity/structure'
import {WarningOutlineIcon} from '@sanity/icons'
import {Feedback, useListeningQuery} from 'sanity-plugin-utils'

import Debug from './Debug'
import {DocumentsPaneInitialValueTemplate} from './types'
import NewDocument from './NewDocument'
import DuplicateDocument from './DuplicateDocument'

type DocumentsProps = {
query: string
params: {[key: string]: string}
debug: boolean
initialValueTemplates: DocumentsPaneInitialValueTemplate[]
options: ListenQueryOptions
duplicate: boolean
}

export default function Documents(props: DocumentsProps) {
const {query, params, options, debug, initialValueTemplates} = props
const {query, params, options, debug, initialValueTemplates, duplicate} = props
const {routerPanesState, groupIndex, handleEditReference} = usePaneRouter()
const schema = useSchema()

Expand Down Expand Up @@ -91,11 +100,19 @@ export default function Documents(props: DocumentsProps) {
return schemaType ? (
<Button
key={doc._id}
// eslint-disable-next-line react/jsx-no-bind
onClick={() => handleClick(doc._id, doc._type)}
padding={2}
mode="bleed"
>
<Preview value={doc} schemaType={schemaType} />
<Preview
value={doc}
schemaType={schemaType}
actions={
duplicate && <DuplicateDocument id={getPublishedId(doc._id)} type={doc._type} />
}
layout="block"
/>
</Button>
) : (
<Card radius={2} tone="caution" data-ui="Alert" padding={2} key={doc._id}>
Expand Down
2 changes: 2 additions & 0 deletions src/DocumentsPane.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export default function DocumentsPane(props: DocumentsPaneProps) {
debug = false,
initialValueTemplates: initialValueTemplatesResolver,
options = {},
duplicate = false,
} = props.options

if (useDraft && typeof params === 'function') {
Expand Down Expand Up @@ -57,6 +58,7 @@ export default function DocumentsPane(props: DocumentsPaneProps) {
options={options}
debug={debug}
initialValueTemplates={initialValueTemplates}
duplicate={duplicate}
/>
)
}
99 changes: 99 additions & 0 deletions src/DuplicateDocument.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import {Box, Button, Tooltip, Text} from '@sanity/ui'
import React, {useState, useCallback} from 'react'
import {filter, firstValueFrom} from 'rxjs'
import {CopyIcon} from '@sanity/icons'
import {
useDocumentOperation,
useDocumentPairPermissions,
useDocumentStore,
useTranslation,
} from 'sanity'
import {usePaneRouter} from 'sanity/structure'
import {uuid} from '@sanity/uuid'
import {fromString as pathFromString} from '@sanity/util/paths'

import {structureLocaleNamespace} from 'sanity/structure'

interface NewDocumentProps {
id: string
type: string
}

export default function DuplicateDocument(props: NewDocumentProps) {
const {id, type} = props

const documentStore = useDocumentStore()
const {duplicate} = useDocumentOperation(id, type)
const {routerPanesState, groupIndex, handleEditReference} = usePaneRouter()
const [isDuplicating, setDuplicating] = useState(false)
const [permissions, isPermissionsLoading] = useDocumentPairPermissions({
id,
type,
permission: 'duplicate',
})

const {t} = useTranslation(structureLocaleNamespace)

const handle = useCallback(
async (event: React.MouseEvent<HTMLElement>) => {
event.stopPropagation()
const dupeId = uuid()

setDuplicating(true)

// set up the listener before executing
const duplicateSuccess = firstValueFrom(
documentStore.pair
.operationEvents(id, type)
.pipe(filter((e) => e.op === 'duplicate' && e.type === 'success'))
)
duplicate.execute(dupeId)

// only navigate to the duplicated document when the operation is successful
await duplicateSuccess
setDuplicating(false)

const childParams = routerPanesState[groupIndex + 1]?.[0].params || {}
const {parentRefPath} = childParams

handleEditReference({
id: dupeId,
type,
parentRefPath: parentRefPath ? pathFromString(parentRefPath) : [``],
template: {id: dupeId},
})
},
[documentStore.pair, duplicate, groupIndex, handleEditReference, id, routerPanesState, type]
)

if (isPermissionsLoading || !permissions?.granted) {
return <></>
}

return (
<Tooltip
content={
<Box>
<Text muted size={1}>
{t('action.duplicate.label')}
</Text>
</Box>
}
placement="left"
portal
>
<Button
onClick={handle}
padding={2}
fontSize={1}
as={Box}
icon={<CopyIcon />}
mode="ghost"
tone="default"
aria-label={t('action.duplicate.label')}
style={{cursor: 'pointer'}}
disabled={isDuplicating || Boolean(duplicate.disabled) || isPermissionsLoading}
/>
</Tooltip>
)
}
4 changes: 2 additions & 2 deletions src/NewDocument.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import {Button, Card, Flex} from '@sanity/ui'
import React from 'react'
import {DocumentsPaneInitialValueTemplate} from './types'
import {ComposeIcon} from '@sanity/icons'
import {usePaneRouter} from 'sanity/desk'
import {usePaneRouter} from 'sanity/structure'
import {uuid} from '@sanity/uuid'

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

return (
<Card borderBottom={true} padding={2}>
<Card borderBottom padding={2}>
<Flex justify="flex-end" gap={1}>
{initialValueTemplates.map((template) => {
if (!template.template) {
Expand Down
9 changes: 7 additions & 2 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import {UserViewComponent} from 'sanity/structure'
export type DocumentVersionsCollection = React.ComponentProps<UserViewComponent>['document']

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

export interface DocumentsPaneInitialValueTemplate {
schemaType: string
Expand All @@ -15,7 +17,9 @@ export interface DocumentsPaneInitialValueTemplate {
}

// eslint-disable-next-line prettier/prettier
export type DocumentsPaneInitialValueTemplateResolver = (params: {document: DocumentVersionsCollection}) => DocumentsPaneInitialValueTemplate[]
export type DocumentsPaneInitialValueTemplateResolver = (params: {
document: DocumentVersionsCollection
}) => DocumentsPaneInitialValueTemplate[]

export type DocumentsPaneOptions = {
query: string
Expand All @@ -24,6 +28,7 @@ export type DocumentsPaneOptions = {
useDraft?: boolean
initialValueTemplates?: DocumentsPaneInitialValueTemplateResolver
options?: ListenQueryOptions
duplicate?: boolean
}

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