Skip to content

Commit

Permalink
Feature/show full url (#18)
Browse files Browse the repository at this point in the history
* show url below slug input

* reuse getsanitydocumentid logic

* add readme

* cleanup

* improve typing

* Update src/components/SlugField.tsx

Co-authored-by: Djovanni Tehubijuluw <[email protected]>

* use renderdefault

* fix typo

---------

Co-authored-by: Djovanni Tehubijuluw <[email protected]>
  • Loading branch information
tstikvoort and djohalo2 authored Feb 7, 2024
1 parent 5207418 commit 6322508
Show file tree
Hide file tree
Showing 10 changed files with 107 additions and 5 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"
};
```

Expand Down
1 change: 1 addition & 0 deletions examples/studio/page-tree.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ export const pageTreeConfig: PageTreeConfig = {
pageSchemaTypes: ['homePage', 'contentPage'],
apiVersion: '2023-12-08',
titleFieldName: 'title',
baseUrl: 'https://q42.nl'
};
5 changes: 3 additions & 2 deletions src/components/PageTreeField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<ReferenceValue> & {
Expand All @@ -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(
Expand Down
61 changes: 61 additions & 0 deletions src/components/SlugField.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Stack space={3}>
{renderDefault(props)}
{typeof id == 'string' && typeof type == 'string' && !!parentRef?._ref && (
<UrlExplanation id={id} type={type} parentId={parentRef?._ref} config={config} value={value} />
)}
</Stack>
);
};

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 && (
<a href={url} target="blank">
{url}
</a>
);

const content = isPublished ? <>Link to page: {linkToPage}</> : <>Page url once published: {url ?? path}</>;

return (
<Text muted size={1}>
{content}
</Text>
);
};
5 changes: 3 additions & 2 deletions src/helpers/page-tree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
PageMetadata,
} from '../types';
import { getLanguageFromConfig } from './config';
import { getSanityDocumentId } from '../utils/sanity';

export const DRAFTS_PREFIX = 'drafts.';

Expand Down Expand Up @@ -95,15 +96,15 @@ 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
.filter(page => isValidPage(config, page))
.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,
Expand Down
20 changes: 20 additions & 0 deletions src/hooks/usePageTreeItem.ts
Original file line number Diff line number Diff line change
@@ -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<RawPageMetadata[]>(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),
};
};
4 changes: 4 additions & 0 deletions src/schema/definePageType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
}),
Expand Down
2 changes: 2 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand Down
6 changes: 6 additions & 0 deletions src/utils/sanity.ts
Original file line number Diff line number Diff line change
@@ -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, '');
5 changes: 4 additions & 1 deletion src/validators/slug-validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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) {
Expand Down

0 comments on commit 6322508

Please sign in to comment.