From c243962f1d64e252f1124045ee6581a0ca18ccf9 Mon Sep 17 00:00:00 2001 From: Tom Smith Date: Thu, 18 Apr 2024 16:55:05 +0100 Subject: [PATCH] feat: added duplicate action to doc list --- README.md | 6 +-- package-lock.json | 2 +- src/Documents.tsx | 23 +++++++-- src/DocumentsPane.tsx | 2 + src/DuplicateDocument.tsx | 99 +++++++++++++++++++++++++++++++++++++++ src/NewDocument.tsx | 4 +- src/types.ts | 9 +++- 7 files changed, 134 insertions(+), 11 deletions(-) create mode 100644 src/DuplicateDocument.tsx diff --git a/README.md b/README.md index e93c718..4ad8230 100644 --- a/README.md +++ b/README.md @@ -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. @@ -34,7 +34,7 @@ S.view .options({ query: `*[references($id)]`, params: {id: `_id`}, - options: {perspective: 'previewDrafts'} + options: {perspective: 'previewDrafts'}, }) .title('Incoming References') ``` @@ -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 @@ -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. - diff --git a/package-lock.json b/package-lock.json index d9db065..eb06d89 100644 --- a/package-lock.json +++ b/package-lock.json @@ -54,7 +54,7 @@ "@sanity/ui": "^1.0 || ^2.0", "react": "^18", "react-dom": "^18", - "sanity": "^3.0.0", + "sanity": "^3.24.1", "styled-components": "^5.0 || ^6.0" } }, diff --git a/src/Documents.tsx b/src/Documents.tsx index 7c6c9f8..8e55c9a 100644 --- a/src/Documents.tsx +++ b/src/Documents.tsx @@ -1,7 +1,14 @@ 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' @@ -9,6 +16,7 @@ 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 @@ -16,10 +24,11 @@ type DocumentsProps = { 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() @@ -91,11 +100,19 @@ export default function Documents(props: DocumentsProps) { return schemaType ? ( ) : ( diff --git a/src/DocumentsPane.tsx b/src/DocumentsPane.tsx index f68403b..7b20a96 100644 --- a/src/DocumentsPane.tsx +++ b/src/DocumentsPane.tsx @@ -17,6 +17,7 @@ export default function DocumentsPane(props: DocumentsPaneProps) { debug = false, initialValueTemplates: initialValueTemplatesResolver, options = {}, + duplicate = false, } = props.options if (useDraft && typeof params === 'function') { @@ -57,6 +58,7 @@ export default function DocumentsPane(props: DocumentsPaneProps) { options={options} debug={debug} initialValueTemplates={initialValueTemplates} + duplicate={duplicate} /> ) } diff --git a/src/DuplicateDocument.tsx b/src/DuplicateDocument.tsx new file mode 100644 index 0000000..df3b4b8 --- /dev/null +++ b/src/DuplicateDocument.tsx @@ -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) => { + 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 ( + + + {t('action.duplicate.label')} + + + } + placement="left" + portal + > +