diff --git a/packages/root-cms/ui/components/CopyDocModal/CopyDocModal.tsx b/packages/root-cms/ui/components/CopyDocModal/CopyDocModal.tsx index c0231460..9242b6ee 100644 --- a/packages/root-cms/ui/components/CopyDocModal/CopyDocModal.tsx +++ b/packages/root-cms/ui/components/CopyDocModal/CopyDocModal.tsx @@ -1,11 +1,12 @@ import {Button} from '@mantine/core'; import {ContextModalProps, useModals} from '@mantine/modals'; import {showNotification} from '@mantine/notifications'; -import {useState} from 'preact/hooks'; +import {useState, useEffect} from 'preact/hooks'; import {useLocation} from 'preact-iso'; import {getSlugError, normalizeSlug} from '../../../shared/slug.js'; import {useModalTheme} from '../../hooks/useModalTheme.js'; import {cmsCopyDoc, cmsCreateDoc} from '../../utils/doc.js'; +import {batchUpdateTags, loadTranslations} from '../../utils/l10n.js'; import {SlugInput} from '../SlugInput/SlugInput.js'; import {Text} from '../Text/Text.js'; import './CopyDocModal.css'; @@ -38,14 +39,29 @@ export function CopyDocModal(modalProps: ContextModalProps) { const {route} = useLocation(); const [toCollectionId, setToCollectionId] = useState(''); const [toSlug, setToSlug] = useState(''); + const [slugError, setSlugError] = useState(''); const [error, setError] = useState(''); const [confirmOverwrite, setConfirmOverwrite] = useState(false); const [loading, setLoading] = useState(false); + const [translationsMap, setTranslationsMap] = useState< + Record + >({}); const fromDocId = props.fromDocId; const fromCollectionId = fromDocId.split('/')[0]; const sourceLabel = props.fromLabel || fromDocId; + // Load translations tagged with the source doc ID. + useEffect(() => { + loadTranslations({tags: [fromDocId]}).then(setTranslationsMap); + }, [fromDocId]); + + function validateSlug(slug: string, collectionId: string) { + const cleanSlug = normalizeSlug(slug); + const slugRegex = window.__ROOT_CTX.collections[collectionId]?.slugRegex; + return cleanSlug ? getSlugError(cleanSlug, slugRegex) : ''; + } + async function onSubmit(e: Event) { e.preventDefault(); setLoading(true); @@ -56,15 +72,14 @@ export function CopyDocModal(modalProps: ContextModalProps) { setLoading(false); return; } - const cleanSlug = normalizeSlug(toSlug); - const slugRegex = window.__ROOT_CTX.collections[toCollectionId]?.slugRegex; - const slugValidationError = getSlugError(cleanSlug, slugRegex); + const slugValidationError = validateSlug(toSlug, toCollectionId); if (slugValidationError) { - setError(slugValidationError); + // SlugInput already displays this error, just return early. setLoading(false); return; } + const cleanSlug = normalizeSlug(toSlug); const toDocId = `${toCollectionId}/${cleanSlug}`; try { if (props.fields) { @@ -75,6 +90,18 @@ export function CopyDocModal(modalProps: ContextModalProps) { } else { await cmsCopyDoc(fromDocId, toDocId, {overwrite: confirmOverwrite}); } + + // Auto-copy translations by adding the new doc ID tag to all strings + // tagged with the old doc ID. + const translationHashes = Object.keys(translationsMap); + if (translationHashes.length > 0) { + const updates = translationHashes.map((hash) => ({ + hash, + tags: [toDocId], + })); + await batchUpdateTags(updates, {mode: 'union'}); + } + context.closeModal(id); showNotification({ title: 'Copied!', @@ -123,6 +150,9 @@ export function CopyDocModal(modalProps: ContextModalProps) { onChange={(newValue: {collectionId: string; slug: string}) => { setToCollectionId(newValue.collectionId); setToSlug(newValue.slug); + setSlugError(validateSlug(newValue.slug, newValue.collectionId)); + setError(''); + setConfirmOverwrite(false); }} /> @@ -145,6 +175,7 @@ export function CopyDocModal(modalProps: ContextModalProps) { size="xs" color="dark" loading={loading} + disabled={!!error || !!slugError} > {confirmOverwrite ? 'Overwrite?' : 'Submit'} diff --git a/packages/root-cms/ui/components/NewDocModal/NewDocModal.tsx b/packages/root-cms/ui/components/NewDocModal/NewDocModal.tsx index 69d600d0..670e6915 100644 --- a/packages/root-cms/ui/components/NewDocModal/NewDocModal.tsx +++ b/packages/root-cms/ui/components/NewDocModal/NewDocModal.tsx @@ -21,6 +21,7 @@ export function NewDocModal(props: NewDocModalProps) { const [slug, setSlug] = useState(''); const [rpcLoading, setRpcLoading] = useState(false); const [slugError, setSlugError] = useState(''); + const [error, setError] = useState(''); const theme = useMantineTheme(); const collectionId = props.collection; const rootCollection = window.__ROOT_CTX.collections[collectionId]; @@ -29,6 +30,11 @@ export function NewDocModal(props: NewDocModalProps) { } const collection = useCollectionSchema(collectionId); + function validateSlug(slug: string) { + const cleanSlug = normalizeSlug(slug); + return cleanSlug ? getSlugError(cleanSlug, rootCollection.slugRegex) : ''; + } + function onClose() { if (props.onClose) { props.onClose(); @@ -40,15 +46,14 @@ export function NewDocModal(props: NewDocModalProps) { setRpcLoading(true); setSlugError(''); - const cleanSlug = normalizeSlug(slug); - const slugRegex = rootCollection.slugRegex; - const slugValidationError = getSlugError(cleanSlug, slugRegex); + const slugValidationError = validateSlug(slug); if (slugValidationError) { - setSlugError(slugValidationError); + // SlugInput already displays this error, just return early. setRpcLoading(false); return; } + const cleanSlug = normalizeSlug(slug); const docId = `${collectionId}/${cleanSlug}`; try { // Save the doc using the default value defined in the collection's @@ -59,7 +64,7 @@ export function NewDocModal(props: NewDocModalProps) { } await cmsCreateDoc(docId, {fields: defaultValue}); } catch (err) { - setSlugError(String(err)); + setError(String(err)); setRpcLoading(false); return; } @@ -96,10 +101,12 @@ export function NewDocModal(props: NewDocModalProps) { collectionId={collectionId} onChange={(newValue: {collectionId: string; slug: string}) => { setSlug(newValue.slug); + setSlugError(validateSlug(newValue.slug)); + setError(''); }} /> - {slugError &&
{slugError}
} + {error &&
{error}
}