Skip to content

Commit

Permalink
Merge pull request #4 from tinloof/tinloof-pages-navigator
Browse files Browse the repository at this point in the history
Use document schema preview to render pages navigator Icon
  • Loading branch information
thomasKn authored Mar 25, 2024
2 parents 6b532d9 + 44d3302 commit 89b6581
Show file tree
Hide file tree
Showing 10 changed files with 253 additions and 32 deletions.
1 change: 1 addition & 0 deletions examples/hello-world/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"@tailwindcss/typography": "0.5.10",
"@tinloof/sanity-studio": "workspace:*",
"classnames": "2.5.1",
"lucide-react": "^0.360.0",
"next": "14.1.0",
"next-sanity": "^8.0.0",
"react": "18.2.0",
Expand Down
14 changes: 0 additions & 14 deletions examples/hello-world/sanity/schemas/page.ts

This file was deleted.

33 changes: 33 additions & 0 deletions examples/hello-world/sanity/schemas/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { definePathname } from '@tinloof/sanity-studio'
import { defineType } from 'sanity'
import { StickyNote } from 'lucide-react'

export default defineType({
type: 'document',
name: 'page',
fields: [
{
type: 'string',
name: 'title',
},
{
type: 'image',
name: 'image',
},
definePathname({ name: 'pathname' }),
],
preview: {
select: {
title: 'title',
image: 'image.asset.url',
},
prepare({ title, image }) {
const Image = () => <img src={image} alt="" />
const Icon = () => <StickyNote size={16} />
return {
title,
media: image ? Image : Icon,
}
},
},
})
4 changes: 3 additions & 1 deletion packages/sanity-studio/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,9 @@
}
},
"dependencies": {
"@sanity/icons": "^2.10.2",
"@sanity/asset-utils": "^1.3.0",
"@sanity/icons": "^2.11.2",
"@sanity/image-url": "^1.0.2",
"@sanity/incompatible-plugin": "^1.0.4",
"@sanity/ui": "^2.0.1",
"@tanstack/react-virtual": "^3.0.4",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React from "react";

import { PagesNavigatorOptions } from "../../../types";
import { NavigatorProvider } from "../context";
import { useSanityFetch } from "../utils";
Expand All @@ -17,6 +18,7 @@ export function createPagesNavigator(props: PagesNavigatorOptions) {
function DefaultPagesNavigator(props: PagesNavigatorOptions) {
const pagesRoutesQuery = `
*[pathname.current != null]{
_rev,
_id,
_originalId,
_type,
Expand Down
32 changes: 19 additions & 13 deletions packages/sanity-studio/src/plugins/navigator/components/List.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,16 @@ import { Badge, Box, Card, Flex, Stack, Text, Tooltip } from "@sanity/ui";
import { useVirtualizer } from "@tanstack/react-virtual";
import { localizePathname } from "@tinloof/sanity-web";
import React, { useRef } from "react";
import { useColorSchemeValue, useSchema } from "sanity";
import { useColorSchemeValue } from "sanity";
import {
usePresentationNavigate,
usePresentationParams,
} from "sanity/presentation";
import styled from "styled-components";

import { ListItemProps, PageTreeNode } from "../../../types";
import { ListItemProps, PageTreeNode, TreeNode } from "../../../types";
import { useNavigator } from "../context";
import { PreviewElement } from "./Preview";

type PreviewStyleProps = {
isPreviewed?: boolean;
Expand Down Expand Up @@ -218,7 +219,6 @@ const ListItem = ({
const scheme = useColorSchemeValue();
const { preview } = usePresentationParams();
const navigate = usePresentationNavigate();

const previewed = preview === path;

const handleClick = (e: React.MouseEvent<HTMLLIElement>) => {
Expand Down Expand Up @@ -293,7 +293,7 @@ const ListItem = ({
justify="center"
style={{ position: "relative", width: 33, height: 33, flexShrink: 0 }}
>
<ItemIcon type={item._type} />
<ItemIcon item={item} />
<div
style={{
boxShadow: "inset 0 0 0 1px var(--card-fg-color)",
Expand All @@ -316,7 +316,11 @@ const ListItem = ({
currentScheme={scheme}
weight="medium"
>
{item.title}
{item._type !== "folder" ? (
<PreviewElement fallback={item.title} type="title" item={item} />
) : (
item.title
)}
</TextElement>
<TextElement
size={1}
Expand All @@ -325,7 +329,11 @@ const ListItem = ({
isPreviewed={previewed}
currentScheme={scheme}
>
{path}
{item._type !== "folder" ? (
<PreviewElement fallback={path} type="subtitle" item={item} />
) : (
path
)}
</TextElement>
</TextContainer>
</Flex>
Expand Down Expand Up @@ -411,21 +419,19 @@ const ListItem = ({
);
};

const ItemIcon = ({ type }: { type: string }) => {
const schema = useSchema();
const ItemIcon = ({ item }: { item: TreeNode }) => {
const iconProps = {
fontSize: "calc(21 / 16 * 1em)",
color: "var(--card-icon-color)",
};

if (type === "folder") {
if (item._type === "folder") {
return <FolderIcon {...iconProps} />;
}

const fullSchema = schema.get(type);
const Icon = fullSchema?.icon ?? DocumentIcon;

return <Icon {...iconProps} />;
return (
<PreviewElement fallback={<DocumentIcon />} type="media" item={item} />
);
};

const SkeletonListItems = ({ items }: { items: number }) => {
Expand Down
174 changes: 174 additions & 0 deletions packages/sanity-studio/src/plugins/navigator/components/Preview.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
import { isImageSource, SanityImageSource } from "@sanity/asset-utils";
import { DocumentIcon } from "@sanity/icons";
import imageUrlBuilder from "@sanity/image-url";
import React from "react";
import { isValidElementType } from "react-is";
import { useMemoObservable } from "react-rx";
import {
getPreviewStateObservable,
getPreviewValueWithFallback,
ImageUrlFitMode,
isString,
SanityDefaultPreviewProps,
useClient,
useDocumentPreviewStore,
useSchema,
} from "sanity";

import { FolderTreeNode, TreeNode } from "../../../types";

const PreviewElement = ({
item,
type,
fallback,
}: {
item: Exclude<TreeNode, FolderTreeNode>; // Only accepts a PageTreeNode, FolderTreeNode is forbidden
type: "media" | "title" | "subtitle";
fallback?: React.ReactNode | string;
}): React.ReactElement => {
const schema = useSchema();
const { _id, _type } = item;

const documentPreviewStore = useDocumentPreviewStore();
const schemaType = schema.get(_type);

const { draft, published, isLoading } = useMemoObservable(
() => getPreviewStateObservable(documentPreviewStore, schemaType, _id, ""),
[_id, documentPreviewStore, schemaType]
)!;

const previewValues = getPreviewValueWithFallback({
draft,
published,
value: { ...item },
});

const showPreview =
typeof schemaType.preview?.prepare === "function" && !isLoading;

if (type === "media") {
return showPreview ? (
<PreviewMedia
{...previewValues}
isPlaceholder={isLoading ?? true}
layout="default"
icon={schemaType.icon}
/>
) : (
<>{!isLoading ? fallback : null}</>
);
}

if (type === "title") {
return showPreview && previewValues?.title ? (
<>{previewValues?.title}</>
) : (
<>{fallback}</>
);
}

if (type === "subtitle") {
return showPreview && previewValues?.subtitle ? (
<>{previewValues?.subtitle}</>
) : (
<>{fallback}</>
);
}

return null;
};

PreviewElement.displayName = "PreviewElement";

const PreviewMedia = (props: SanityDefaultPreviewProps): React.ReactElement => {
const { icon, media: mediaProp, imageUrl, title } = props;

const client = useClient({
apiVersion: "2024-03-12",
});
const imageBuilder = React.useMemo(() => imageUrlBuilder(client), [client]);

// NOTE: This function exists because the previews provides options
// for the rendering of the media (dimensions)
const renderMedia = React.useCallback(
(options: {
dimensions: {
width?: number;
height?: number;
fit: ImageUrlFitMode;
dpr?: number;
};
}) => {
const { dimensions } = options;

// Handle sanity image
return (
<img
alt={isString(title) ? title : undefined}
referrerPolicy="strict-origin-when-cross-origin"
src={
imageBuilder
.image(
mediaProp as SanityImageSource /*will only enter this code path if it's compatible*/
)
.width(dimensions.width || 100)
.height(dimensions.height || 100)
.fit(dimensions.fit)
.dpr(dimensions.dpr || 1)
.url() || ""
}
/>
);
},
[imageBuilder, mediaProp, title]
);

const renderIcon = React.useCallback(() => {
return React.createElement(icon || DocumentIcon);
}, [icon]);

const media = React.useMemo(() => {
if (icon === false) {
// Explicitly disabled
return false;
}

if (isValidElementType(mediaProp)) {
return mediaProp;
}

if (React.isValidElement(mediaProp)) {
return mediaProp;
}

if (isImageSource(mediaProp)) {
return renderMedia;
}

// Handle image urls
if (isString(imageUrl)) {
return (
<img
src={imageUrl}
alt={isString(title) ? title : undefined}
referrerPolicy="strict-origin-when-cross-origin"
/>
);
}

// Render fallback icon
return renderIcon;
}, [icon, imageUrl, mediaProp, renderIcon, renderMedia, title]);

if (typeof media === "number" || typeof media === "string") {
return <>{media}</>;
}

const Media = media as React.ComponentType<any>;

return <Media />;
};

PreviewMedia.displayName = "PreviewMedia";

export { PreviewElement };
2 changes: 1 addition & 1 deletion packages/sanity-studio/src/plugins/navigator/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { definePlugin } from "sanity";

import { presentationTool } from "sanity/presentation";

import { PagesNavigatorPluginOptions } from "../../types";
import { createPagesNavigator } from "./components/DefaultPagesNavigator";
import { createPageTemplates, normalizeCreatablePages } from "./utils";
Expand Down
4 changes: 2 additions & 2 deletions packages/sanity-studio/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import {
SlugDefinition,
SlugOptions,
} from "sanity";

import {
NavigatorOptions as PresentationNavigatorOptions,
PresentationPluginOptions,
Expand Down Expand Up @@ -37,9 +36,10 @@ export type PagesNavigatorPluginOptions = PresentationPluginOptions & {
};

export type Page = {
_rev: string;
_id: string;
_originalId: string;
_type: string;
_type: Exclude<"string", "folder">;
_updatedAt: string;
_createdAt: string;
pathname: string | null;
Expand Down
Loading

0 comments on commit 89b6581

Please sign in to comment.