diff --git a/README.md b/README.md index 9913565..3b4f2fb 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,9 @@ export const pageTreeConfig: PageTreeConfig = { apiVersion: '2023-12-08', /* Optionally provide the field name of the title field of your page documents, to be used to generate a slug automatically for example. */ titleFieldName: 'title', + /* Used for showing the full url for a document and linking to it. */ + /* optional, otherwise the path is shown */ + baseUrl: "https://example.com" }; ``` diff --git a/examples/studio/page-tree.config.ts b/examples/studio/page-tree.config.ts index 411a88b..4990136 100644 --- a/examples/studio/page-tree.config.ts +++ b/examples/studio/page-tree.config.ts @@ -5,4 +5,5 @@ export const pageTreeConfig: PageTreeConfig = { pageSchemaTypes: ['homePage', 'contentPage'], apiVersion: '2023-12-08', titleFieldName: 'title', + baseUrl: 'https://q42.nl' }; diff --git a/src/components/PageTreeField.tsx b/src/components/PageTreeField.tsx index 06ab759..3df4fb6 100644 --- a/src/components/PageTreeField.tsx +++ b/src/components/PageTreeField.tsx @@ -4,11 +4,12 @@ import { ObjectFieldProps, ReferenceValue, FormField, set, useFormValue, SanityD import styled from 'styled-components'; import { PageTreeEditor } from './PageTreeEditor'; -import { DRAFTS_PREFIX, findPageTreeItemById, flatMapPageTree } from '../helpers/page-tree'; +import { findPageTreeItemById, flatMapPageTree } from '../helpers/page-tree'; import { useOptimisticState } from '../hooks/useOptimisticState'; import { usePageTree } from '../hooks/usePageTree'; import { PageTreeConfigProvider } from '../hooks/usePageTreeConfig'; import { PageTreeConfig, PageTreeItem } from '../types'; +import { getSanityDocumentId } from '../utils/sanity'; export const PageTreeField = ( props: ObjectFieldProps & { @@ -26,7 +27,7 @@ export const PageTreeField = ( const [isPageTreeDialogOpen, setIsPageTreeDialogOpen] = useState(false); const parentId = props.inputProps.value?._ref; - const pageId = form._id?.replace(DRAFTS_PREFIX, ''); + const pageId = getSanityDocumentId(form._id); const fieldPage = useMemo(() => (pageTree ? findPageTreeItemById(pageTree, pageId) : undefined), [pageTree, pageId]); const parentPage = useMemo( diff --git a/src/components/SlugField.tsx b/src/components/SlugField.tsx new file mode 100644 index 0000000..438fe5f --- /dev/null +++ b/src/components/SlugField.tsx @@ -0,0 +1,61 @@ +import { Stack, Text } from '@sanity/ui'; +import { Reference, SlugInputProps, SlugValue, useEditState, useFormValue } from 'sanity'; +import { PageTreeConfig } from '../types'; +import { usePageTreeItem } from '../hooks/usePageTreeItem'; +import { getSanityDocumentId } from '../utils/sanity'; + +export type SlugFieldProps = { + config: PageTreeConfig; +} & SlugInputProps; + +export const SlugField = (props: SlugFieldProps) => { + const id = useFormValue(['_id']); + const type = useFormValue(['_type']); + // eslint-disable-next-line no-warning-comments + // TODO ideally this would be more type safe. + const parentRef = useFormValue(['parent']) as Reference | undefined; + + const { config, value, renderDefault } = props; + return ( + + {renderDefault(props)} + {typeof id == 'string' && typeof type == 'string' && !!parentRef?._ref && ( + + )} + + ); +}; + +type UrlExplanationProps = { + id: string; + type: string; + parentId: string; + value: SlugValue | undefined; + config: PageTreeConfig; +}; + +const UrlExplanation = ({ id, type, parentId, value, config }: UrlExplanationProps) => { + const state = useEditState(getSanityDocumentId(id), type ?? ''); + const isPublished = !!state.published; + + // we use published perspective so we don't get a draft version of the slug that has been changed of a parent page. + const { page, isLoading } = usePageTreeItem(parentId, config, 'published'); + if (isLoading) return null; + + const path = page?.path == '/' ? `${page?.path}${value?.current}` : `${page?.path}/${value?.current}`; + + const url = config.baseUrl ? `${config.baseUrl}${path}` : null; + const linkToPage = url && ( + + {url} + + ); + + const content = isPublished ? <>Link to page: {linkToPage} : <>Page url once published: {url ?? path}; + + return ( + + {content} + + ); +}; diff --git a/src/helpers/page-tree.ts b/src/helpers/page-tree.ts index 2912dbb..2f2c532 100644 --- a/src/helpers/page-tree.ts +++ b/src/helpers/page-tree.ts @@ -8,6 +8,7 @@ import { PageMetadata, } from '../types'; import { getLanguageFromConfig } from './config'; +import { getSanityDocumentId } from '../utils/sanity'; export const DRAFTS_PREFIX = 'drafts.'; @@ -95,7 +96,7 @@ const getPublishedAndDraftRawPageMetdadata = ( ); const draftPages = groupBy( pages.filter(p => p._id.startsWith(DRAFTS_PREFIX)), - p => p._id.replace(DRAFTS_PREFIX, ''), + p => getSanityDocumentId(p._id), ); return pages @@ -103,7 +104,7 @@ const getPublishedAndDraftRawPageMetdadata = ( .filter(p => !draftPages[p._id]) // filter out published versions for pages which have a draft .map(p => { const isDraft = p._id.startsWith(DRAFTS_PREFIX); - const _idWithoutDraft = p._id.replace(DRAFTS_PREFIX, ''); + const _idWithoutDraft = getSanityDocumentId(p._id); const newPage: RawPageMetadataWithPublishedState = { ...p, _id: isDraft ? _idWithoutDraft : p._id, diff --git a/src/hooks/usePageTreeItem.ts b/src/hooks/usePageTreeItem.ts new file mode 100644 index 0000000..5f76c35 --- /dev/null +++ b/src/hooks/usePageTreeItem.ts @@ -0,0 +1,20 @@ +import { useMemo } from 'react'; + +import { getRawPageMetadataQuery } from '../queries'; +import { RawPageMetadata, PageTreeConfig } from '../types'; +import { getAllPageMetadata } from '../helpers/page-tree'; +import { useListeningQuery } from 'sanity-plugin-utils'; +import { ClientPerspective } from 'next-sanity'; + +export const usePageTreeItem = (documentId: string, config: PageTreeConfig, perspective?: ClientPerspective) => { + const { data, loading } = useListeningQuery(getRawPageMetadataQuery(config), { + options: { apiVersion: config.apiVersion, perspective }, + }); + + const pageTree = useMemo(() => (data ? getAllPageMetadata(config, data) : undefined), [config, data]); + + return { + isLoading: loading, + page: pageTree?.find(page => page._id === documentId), + }; +}; diff --git a/src/schema/definePageType.ts b/src/schema/definePageType.ts index 7295d97..ac38507 100644 --- a/src/schema/definePageType.ts +++ b/src/schema/definePageType.ts @@ -3,6 +3,7 @@ import { DocumentDefinition, defineField, defineType, SlugOptions } from 'sanity import { PageTreeField } from '../components/PageTreeField'; import { PageTreeConfig } from '../types'; import { slugValidator } from '../validators/slug-validator'; +import { SlugField } from '../components/SlugField'; type Options = { isRoot?: boolean; @@ -32,6 +33,9 @@ const basePageFields = (config: PageTreeConfig, options: Options) => [ source: config.titleFieldName ?? options.slugSource, isUnique: () => true, }, + components: { + input: props => SlugField({ ...props, config }), + }, validation: Rule => Rule.required().custom(slugValidator(config)), group: options.fieldsGroupName, }), diff --git a/src/types.ts b/src/types.ts index 38bcb18..d777c47 100644 --- a/src/types.ts +++ b/src/types.ts @@ -47,6 +47,8 @@ export type PageTreeConfig = { pageSchemaTypes: string[]; /* Field name of your page documents */ titleFieldName?: string; + /* Used for creating page link on the editor page */ + baseUrl?: string; /* This plugin supports the document-internationalization plugin. To use it properly, provide the supported languages. */ documentInternationalization?: { /* Array of supported language code strings, e.g. ["en", "nl"]. These will be used in root pages and when creating a new child page it will set the language field based on the parent page. */ diff --git a/src/utils/sanity.ts b/src/utils/sanity.ts new file mode 100644 index 0000000..6ff68f7 --- /dev/null +++ b/src/utils/sanity.ts @@ -0,0 +1,6 @@ +export const DRAFTS_PREFIX = 'drafts.'; + +/** + * Strips draft id prefix from Sanity document id when draft id is provided. + */ +export const getSanityDocumentId = (val: string) => val.replace(DRAFTS_PREFIX, ''); diff --git a/src/validators/slug-validator.ts b/src/validators/slug-validator.ts index 7be68fa..6d83c6a 100644 --- a/src/validators/slug-validator.ts +++ b/src/validators/slug-validator.ts @@ -3,6 +3,7 @@ import { SlugValue, ValidationContext } from 'sanity'; import { DRAFTS_PREFIX } from '../helpers/page-tree'; import { getRawPageMetadataQuery } from '../queries'; import { RawPageMetadata, PageTreeConfig, SanityRef } from '../types'; +import { getSanityDocumentId } from '../utils/sanity'; /** * Validates that the slug is unique within the parent page and therefore that entire the path is unique. @@ -20,7 +21,9 @@ export const slugValidator = const siblingPages = allPages.filter(page => page.parent?._ref === parentRef._ref); const siblingPagesWithSameSlug = siblingPages - .filter(page => page._id.replace(DRAFTS_PREFIX, '') !== context.document?._id.replace(DRAFTS_PREFIX, '')) + .filter( + page => getSanityDocumentId(page._id) !== (context.document?._id && getSanityDocumentId(context.document._id)), + ) .filter(page => page.slug?.current === slug?.current); if (siblingPagesWithSameSlug.length) {