-
Notifications
You must be signed in to change notification settings - Fork 39
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: add duplicate document action #194
Draft
nkgentile
wants to merge
10
commits into
sanity-io:main
Choose a base branch
from
nkgentile:feature/add-duplicate-action
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+319
−0
Draft
Changes from 4 commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
0222cdf
feat: add document action
nkgentile 6f087ff
feat: add i18n resource bundle
nkgentile b60bfc1
refactor: use metadata document hook
nkgentile f86fd18
docs: add documentation
nkgentile f3b6df0
docs: adjust documentation
nkgentile 6a7ae3a
fix: use copy icon
nkgentile 2ee472b
style: adjust `t`
nkgentile 6528922
style: rename t variables
nkgentile 5c7b6f7
perf: parallelize translation copy
nkgentile 0a18f9a
style: reorder hook deps
nkgentile File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
# Duplicating translated documents | ||
|
||
This plugin includes a custom document action that will duplicate a localized document, its translations, and the metadata document that relates them. | ||
|
||
![Duplicate document action](./img/duplicate-document-action.png) | ||
|
||
Import the document action and configure which document types will use it: | ||
|
||
```ts | ||
import { | ||
documentInternationalization, | ||
DuplicateWithTranslationsAction, | ||
} from '@sanity/document-internationalization' | ||
|
||
export default defineConfig({ | ||
// ...all other config | ||
document: { | ||
actions(prev, { schemaType }) { | ||
return schemaTypes.includes(schemaType) | ||
? prev.map((action) => | ||
action.action === "duplicate" | ||
? DuplicateWithTranslationsAction | ||
: action, | ||
) | ||
: prev; | ||
}, | ||
}, | ||
}) | ||
``` |
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think I'd keep the same |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,247 @@ | ||
import {CopyIcon, TranslateIcon} from '@sanity/icons' | ||
import {useToast} from '@sanity/ui' | ||
import {uuid} from '@sanity/uuid' | ||
import {useCallback, useMemo, useState} from 'react' | ||
import {filter, firstValueFrom} from 'rxjs' | ||
import { | ||
DEFAULT_STUDIO_CLIENT_OPTIONS, | ||
type DocumentActionComponent, | ||
type Id, | ||
InsufficientPermissionsMessage, | ||
type PatchOperations, | ||
useClient, | ||
useCurrentUser, | ||
useDocumentOperation, | ||
useDocumentPairPermissions, | ||
useDocumentStore, | ||
useTranslation, | ||
} from 'sanity' | ||
import {useRouter} from 'sanity/router' | ||
import {structureLocaleNamespace} from 'sanity/structure' | ||
|
||
import {METADATA_SCHEMA_NAME, TRANSLATIONS_ARRAY_NAME} from '../constants' | ||
import {useTranslationMetadata} from '../hooks/useLanguageMetadata' | ||
import {documenti18nLocaleNamespace} from '../i18n' | ||
|
||
const DISABLED_REASON_KEY = { | ||
METADATA_NOT_FOUND: 'action.duplicate.disabled.missing-metadata', | ||
MULTIPLE_METADATA: 'action.duplicate.disabled.multiple-metadata', | ||
NOTHING_TO_DUPLICATE: 'action.duplicate.disabled.nothing-to-duplicate', | ||
NOT_READY: 'action.duplicate.disabled.not-ready', | ||
} | ||
|
||
export const DuplicateWithTranslationsAction: DocumentActionComponent = ({ | ||
id, | ||
type, | ||
onComplete, | ||
}) => { | ||
const documentStore = useDocumentStore() | ||
const {duplicate} = useDocumentOperation(id, type) | ||
const {navigateIntent} = useRouter() | ||
const [isDuplicating, setDuplicating] = useState(false) | ||
const [permissions, isPermissionsLoading] = useDocumentPairPermissions({ | ||
id, | ||
type, | ||
permission: 'duplicate', | ||
}) | ||
const {data, loading: isMetadataDocumentLoading} = useTranslationMetadata(id) | ||
const hasOneMetadataDocument = useMemo(() => { | ||
return Array.isArray(data) && data.length <= 1 | ||
}, [data]) | ||
const metadataDocument = Array.isArray(data) && data.length ? data[0] : null | ||
const client = useClient(DEFAULT_STUDIO_CLIENT_OPTIONS) | ||
const toast = useToast() | ||
const {t: structureT} = useTranslation(structureLocaleNamespace) | ||
const {t: documenti18nT} = useTranslation(documenti18nLocaleNamespace) | ||
const currentUser = useCurrentUser() | ||
|
||
const handle = useCallback(async () => { | ||
setDuplicating(true) | ||
|
||
try { | ||
if (!metadataDocument) { | ||
throw new Error('Metadata document not found') | ||
} | ||
|
||
// 1. Duplicate the document and its localized versions | ||
const translations = new Map<string, Id>() | ||
for (const translation of metadataDocument[TRANSLATIONS_ARRAY_NAME]) { | ||
const dupeId = uuid() | ||
const translationLocale = translation._key | ||
const translationId = translation.value?._ref | ||
|
||
if (!translationId) { | ||
throw new Error('Translation document not found') | ||
} | ||
|
||
const {duplicate: duplicateTranslation} = await firstValueFrom( | ||
documentStore.pair | ||
.editOperations(translationId, type) | ||
.pipe(filter((op) => op.duplicate.disabled !== 'NOT_READY')) | ||
) | ||
|
||
if (duplicateTranslation.disabled) { | ||
throw new Error('Cannot duplicate document') | ||
} | ||
|
||
const duplicateTranslationSuccess = firstValueFrom( | ||
documentStore.pair | ||
.operationEvents(translationId, type) | ||
.pipe(filter((e) => e.op === 'duplicate' && e.type === 'success')) | ||
) | ||
|
||
duplicateTranslation.execute(dupeId) | ||
await duplicateTranslationSuccess | ||
|
||
translations.set(translationLocale, dupeId) | ||
} | ||
|
||
// 2. Duplicate the metadata document | ||
const {duplicate: duplicateMetadata} = await firstValueFrom( | ||
documentStore.pair | ||
.editOperations(metadataDocument._id, METADATA_SCHEMA_NAME) | ||
.pipe(filter((op) => op.duplicate.disabled !== 'NOT_READY')) | ||
) | ||
|
||
if (duplicateMetadata.disabled) { | ||
throw new Error('Cannot duplicate document') | ||
} | ||
|
||
const duplicateMetadataSuccess = firstValueFrom( | ||
documentStore.pair | ||
.operationEvents(metadataDocument._id, METADATA_SCHEMA_NAME) | ||
.pipe(filter((e) => e.op === 'duplicate' && e.type === 'success')) | ||
) | ||
|
||
const dupeId = uuid() | ||
|
||
duplicateMetadata.execute(dupeId) | ||
await duplicateMetadataSuccess | ||
|
||
// 3. Patch the duplicated metadata document to update the references | ||
// TODO: use document store | ||
// const { patch } = await firstValueFrom( | ||
// documentStore.pair | ||
// .editOperations(dupeId, METADATA_SCHEMA_NAME) | ||
// .pipe(filter((op) => op.patch.disabled !== "NOT_READY")), | ||
// ); | ||
|
||
// if (patch.disabled) { | ||
// throw new Error("Cannot patch document"); | ||
// } | ||
|
||
// const patchSuccess = firstValueFrom( | ||
// documentStore.pair | ||
// .operationEvents(dupeId, METADATA_SCHEMA_NAME) | ||
// .pipe(filter((e) => e.op === "patch" && e.type === "success")), | ||
// ); | ||
|
||
const patch: PatchOperations = { | ||
set: Object.fromEntries( | ||
Array.from(translations.entries()).map(([locale, documentId]) => [ | ||
`${TRANSLATIONS_ARRAY_NAME}[_key == "${locale}"].value._ref`, | ||
documentId, | ||
]) | ||
), | ||
} | ||
|
||
// patch.execute(patches); | ||
// await patchSuccess; | ||
await client.transaction().patch(dupeId, patch).commit() | ||
|
||
// 4. Navigate to the duplicated document | ||
navigateIntent('edit', { | ||
id: Array.from(translations.values()).at(0), | ||
type, | ||
}) | ||
|
||
onComplete() | ||
} catch (error) { | ||
console.error(error) | ||
toast.push({ | ||
status: 'error', | ||
title: 'Error duplicating document', | ||
description: | ||
error instanceof Error | ||
? error.message | ||
: 'Failed to duplicate document', | ||
}) | ||
} finally { | ||
setDuplicating(false) | ||
} | ||
}, [ | ||
client, | ||
documentStore.pair, | ||
metadataDocument, | ||
navigateIntent, | ||
onComplete, | ||
toast, | ||
type, | ||
]) | ||
|
||
return useMemo(() => { | ||
if (!isPermissionsLoading && !permissions?.granted) { | ||
return { | ||
icon: CopyIcon, | ||
disabled: true, | ||
label: documenti18nT('action.duplicate.label'), | ||
title: ( | ||
<InsufficientPermissionsMessage | ||
context="duplicate-document" | ||
currentUser={currentUser} | ||
/> | ||
), | ||
} | ||
} | ||
|
||
if (!isMetadataDocumentLoading && !metadataDocument) { | ||
return { | ||
icon: TranslateIcon, | ||
disabled: true, | ||
label: documenti18nT('action.duplicate.label'), | ||
title: documenti18nT(DISABLED_REASON_KEY.METADATA_NOT_FOUND), | ||
} | ||
} | ||
|
||
if (!hasOneMetadataDocument) { | ||
return { | ||
icon: TranslateIcon, | ||
disabled: true, | ||
label: documenti18nT('action.duplicate.label'), | ||
title: documenti18nT(DISABLED_REASON_KEY.MULTIPLE_METADATA), | ||
} | ||
} | ||
|
||
return { | ||
icon: TranslateIcon, | ||
disabled: | ||
isDuplicating || | ||
Boolean(duplicate.disabled) || | ||
isPermissionsLoading || | ||
isMetadataDocumentLoading, | ||
label: isDuplicating | ||
? structureT('action.duplicate.running.label') | ||
: documenti18nT('action.duplicate.label'), | ||
title: duplicate.disabled | ||
? structureT(DISABLED_REASON_KEY[duplicate.disabled]) | ||
: '', | ||
onHandle: handle, | ||
} | ||
}, [ | ||
currentUser, | ||
documenti18nT, | ||
duplicate.disabled, | ||
handle, | ||
hasOneMetadataDocument, | ||
isDuplicating, | ||
isMetadataDocumentLoading, | ||
isPermissionsLoading, | ||
metadataDocument, | ||
permissions?.granted, | ||
structureT, | ||
]) | ||
} | ||
|
||
DuplicateWithTranslationsAction.action = 'duplicate' | ||
// @ts-expect-error `displayName` is used by React DevTools | ||
DuplicateWithTranslationsAction.displayName = 'DuplicateWithTranslationsAction' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
import {defineLocaleResourceBundle} from 'sanity' | ||
|
||
/** | ||
* The locale namespace for the document internationalization plugin. | ||
* | ||
* @public | ||
*/ | ||
export const documenti18nLocaleNamespace = | ||
'document-internationalization' as const | ||
|
||
/** | ||
* The default locale bundle for the document internationalization plugin, which is US English. | ||
* | ||
* @internal | ||
*/ | ||
export const documentInternationalizationUsEnglishLocaleBundle = | ||
defineLocaleResourceBundle({ | ||
locale: 'en-US', | ||
namespace: documenti18nLocaleNamespace, | ||
resources: () => import('./resources'), | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
export default { | ||
'action.duplicate.label': 'Duplicate with translations', | ||
'action.duplicate.disabled.missing-metadata': | ||
'The document cannot be duplicated because the metadata document is missing', | ||
'action.duplicate.disabled.multiple-metadata': | ||
'The document cannot be duplicated because there are multiple metadata documents', | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe add a comment here that
schemaTypes
would be the same array that you add to thedocumentInternationalization
plugin config. When you copy-paste this snippet it's likely an undefined variable.