Skip to content

Commit

Permalink
Merge pull request #24 from Q42/feature/page-tree-input-component
Browse files Browse the repository at this point in the history
Page tree input component
  • Loading branch information
djohalo2 authored Apr 8, 2024
2 parents 7f70aa1 + 91ba1de commit 1aae192
Show file tree
Hide file tree
Showing 4 changed files with 145 additions and 103 deletions.
16 changes: 15 additions & 1 deletion examples/studio/schemas/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ObjectRule, defineArrayMember, defineField, defineType } from 'sanity';
import { PageTreeField, definePageType } from '@q42/sanity-plugin-page-tree';
import { PageTreeField, PageTreeInput, definePageType } from '@q42/sanity-plugin-page-tree';
import { pageTreeConfig } from '../page-tree.config';

const _homePageType = defineType({
Expand All @@ -18,6 +18,20 @@ const _homePageType = defineType({
type: 'text',
validation: Rule => Rule.required(),
}),
defineField({
name: 'links',
title: 'Links',
type: 'array',
of: [
defineArrayMember({
type: 'reference',
to: [{ type: 'contentPage' }, { type: 'homePage' }],
components: {
input: props => PageTreeInput({ ...props, config: pageTreeConfig }),
},
}),
],
}),
defineField({
name: 'link',
title: 'Link',
Expand Down
112 changes: 10 additions & 102 deletions src/components/PageTreeField.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,7 @@
import { Stack, Flex, Spinner, Card, Dialog, Box } from '@sanity/ui';
import { useMemo, useState } from 'react';
import { ObjectFieldProps, ReferenceValue, FormField, set, useFormValue, SanityDocument } from 'sanity';
import styled from 'styled-components';
import { ObjectFieldProps, ReferenceValue, FormField } from 'sanity';

import { PageTreeEditor } from './PageTreeEditor';
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';
import { PageTreeConfig } from '../types';
import { PageTreeInput } from './PageTreeInput';

export const PageTreeField = (
props: ObjectFieldProps<ReferenceValue> & {
Expand All @@ -18,99 +10,15 @@ export const PageTreeField = (
inputProps: { schemaType: { to?: { name: string }[] } };
},
) => {
const mode = props.mode ?? 'select-page';
const form = useFormValue([]) as SanityDocument;
const { pageTree } = usePageTree(props.config);

const allowedPageTypes = props.inputProps.schemaType.to?.map(t => t.name);

const [isPageTreeDialogOpen, setIsPageTreeDialogOpen] = useState(false);

const parentId = props.inputProps.value?._ref;
const pageId = getSanityDocumentId(form._id);

const fieldPage = useMemo(() => (pageTree ? findPageTreeItemById(pageTree, pageId) : undefined), [pageTree, pageId]);
const parentPage = useMemo(
() => (pageTree && parentId ? findPageTreeItemById(pageTree, parentId) : undefined),
[pageTree, parentId],
);

const flatFieldPages = useMemo(() => (fieldPage ? flatMapPageTree([fieldPage]) : []), [fieldPage]);

const [parentPath, setOptimisticParentPath] = useOptimisticState<string | undefined>(parentPage?.path);

// Some page tree items are not suitable options for a new parent reference.
// Disable the current parent page, the current page and all of its children.
const disabledParentIds =
mode !== 'select-parent' ? [] : [...(parentId ? [parentId] : []), ...flatFieldPages.map(page => page._id)];
// Initially open the current page and all of its parents
const openItemIds = fieldPage?._id ? [fieldPage?._id] : undefined;

const openDialog = () => {
setIsPageTreeDialogOpen(true);
};

const closeDialog = () => {
setIsPageTreeDialogOpen(false);
};

const selectParentPage = (page: PageTreeItem) => {
props.inputProps.onChange(
set({
_ref: page._id,
_type: 'reference',
_weak: page.isDraft,
...(page.isDraft ? { _strengthenOnPublish: { type: page._type } } : {}),
}),
);
setOptimisticParentPath(page.path);
closeDialog();
const inputProps = {
config: props.config,
mode: props.mode,
...props.inputProps,
};

return (
<PageTreeConfigProvider config={props.config}>
<FormField title={props.title} inputId={props.inputId} validation={props.validation}>
<Stack space={3}>
{!pageTree ? (
<Flex paddingY={4} justify="center" align="center">
<Spinner />
</Flex>
) : (
<Card padding={1} shadow={1} radius={2}>
<SelectedItemCard padding={3} radius={2} onClick={openDialog}>
{parentId ? parentPath ?? 'Select page' : 'Select page'}
</SelectedItemCard>
</Card>
)}
</Stack>
{pageTree && isPageTreeDialogOpen && (
<Dialog
header={'Select page'}
id="parent-page-tree"
zOffset={1000}
width={1}
onClose={closeDialog}
onClickOutside={closeDialog}>
<Box padding={4}>
<PageTreeEditor
allowedPageTypes={allowedPageTypes}
pageTree={pageTree}
onItemClick={selectParentPage}
disabledItemIds={disabledParentIds}
initialOpenItemIds={openItemIds}
/>
</Box>
</Dialog>
)}
</FormField>
</PageTreeConfigProvider>
<FormField title={props.title} inputId={props.inputId} validation={props.validation}>
<PageTreeInput {...inputProps} />
</FormField>
);
};

const SelectedItemCard = styled(Card)`
cursor: pointer;
&:hover {
background-color: ${({ theme }) => theme.sanity.color.card.hovered.bg};
}
`;
119 changes: 119 additions & 0 deletions src/components/PageTreeInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import { Stack, Flex, Spinner, Card, Dialog, Box, Text } from '@sanity/ui';
import { useMemo, useState } from 'react';
import { ReferenceValue, set, useFormValue, SanityDocument, ObjectInputProps } from 'sanity';
import styled from 'styled-components';

import { PageTreeEditor } from './PageTreeEditor';
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 PageTreeInput = (
props: ObjectInputProps<ReferenceValue> & {
config: PageTreeConfig;
mode?: 'select-parent' | 'select-page';
schemaType: { to?: { name: string }[] };
},
) => {
const mode = props.mode ?? 'select-page';
const form = useFormValue([]) as SanityDocument;
const { pageTree } = usePageTree(props.config);

const allowedPageTypes = props.schemaType.to?.map(t => t.name);

const [isPageTreeDialogOpen, setIsPageTreeDialogOpen] = useState(false);

const parentId = props.value?._ref;
const pageId = getSanityDocumentId(form._id);

const fieldPage = useMemo(() => (pageTree ? findPageTreeItemById(pageTree, pageId) : undefined), [pageTree, pageId]);
const parentPage = useMemo(
() => (pageTree && parentId ? findPageTreeItemById(pageTree, parentId) : undefined),
[pageTree, parentId],
);

const flatFieldPages = useMemo(() => (fieldPage ? flatMapPageTree([fieldPage]) : []), [fieldPage]);

const [parentPath, setOptimisticParentPath] = useOptimisticState<string | undefined>(parentPage?.path);

// Some page tree items are not suitable options for a new parent reference.
// Disable the current parent page, the current page and all of its children.
const disabledParentIds =
mode !== 'select-parent' ? [] : [...(parentId ? [parentId] : []), ...flatFieldPages.map(page => page._id)];
// Initially open the current page and all of its parents
const openItemIds = fieldPage?._id ? [fieldPage?._id] : undefined;

const openDialog = () => {
setIsPageTreeDialogOpen(true);
};

const closeDialog = () => {
setIsPageTreeDialogOpen(false);
};

const selectParentPage = (page: PageTreeItem) => {
// In the case of an array of references, we need to find the last path in the array and extract the _key
const lastPath = props.path[props.path.length - 1];
const _key = typeof lastPath === 'object' && '_key' in lastPath ? lastPath._key : undefined;

props.onChange(
set({
...(_key ? { _key } : {}),
_ref: page._id,
_type: 'reference',
_weak: page.isDraft,
...(page.isDraft ? { _strengthenOnPublish: { type: page._type } } : {}),
}),
);
setOptimisticParentPath(page.path);
closeDialog();
};

return (
<PageTreeConfigProvider config={props.config}>
<Stack space={3}>
{!pageTree ? (
<Flex paddingY={4} justify="center" align="center">
<Spinner />
</Flex>
) : (
<Card padding={1} shadow={1} radius={2}>
<SelectedItemCard padding={3} radius={2} onClick={openDialog}>
<Text size={2}>{parentId ? parentPath ?? 'Select page' : 'Select page'}</Text>
</SelectedItemCard>
</Card>
)}
</Stack>
{pageTree && isPageTreeDialogOpen && (
<Dialog
header={'Select page'}
id="parent-page-tree"
zOffset={1000}
width={1}
onClose={closeDialog}
onClickOutside={closeDialog}>
<Box padding={4}>
<PageTreeEditor
allowedPageTypes={allowedPageTypes}
pageTree={pageTree}
onItemClick={selectParentPage}
disabledItemIds={disabledParentIds}
initialOpenItemIds={openItemIds}
/>
</Box>
</Dialog>
)}
</PageTreeConfigProvider>
);
};

const SelectedItemCard = styled(Card)`
cursor: pointer;
&:hover {
background-color: ${({ theme }) => theme.sanity.color.card.hovered.bg};
}
`;
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { createPageTreeView } from './components/PageTreeView';

export { definePageType } from './schema/definePageType';
export { PageTreeField } from './components/PageTreeField';
export { PageTreeInput } from './components/PageTreeInput';

export type { PageTreeConfig, PageTreeDocumentListOptions } from './types';

Expand Down

0 comments on commit 1aae192

Please sign in to comment.