From ee64f3c39c8b06cb293134e87f5ec4069256b564 Mon Sep 17 00:00:00 2001 From: stilyan-tinloof Date: Fri, 14 Feb 2025 11:03:12 +0000 Subject: [PATCH 1/2] pages navigator filter --- .../components/DefaultPagesNavigator.tsx | 36 +++++++++++++------ .../src/plugins/navigator/index.ts | 15 ++++---- packages/sanity-studio/src/types.ts | 35 ++++++++++-------- 3 files changed, 55 insertions(+), 31 deletions(-) diff --git a/packages/sanity-studio/src/plugins/navigator/components/DefaultPagesNavigator.tsx b/packages/sanity-studio/src/plugins/navigator/components/DefaultPagesNavigator.tsx index 221c0aa5..281a3d11 100644 --- a/packages/sanity-studio/src/plugins/navigator/components/DefaultPagesNavigator.tsx +++ b/packages/sanity-studio/src/plugins/navigator/components/DefaultPagesNavigator.tsx @@ -1,13 +1,14 @@ -import React from "react"; +import React from 'react'; -import { PagesNavigatorOptions } from "../../../types"; -import { NavigatorProvider } from "../context"; -import { useSanityFetch } from "../utils"; -import Header from "./Header"; -import { List } from "./List"; -import LocaleSelect from "./LocaleSelect"; -import SearchBox from "./SearchBox"; -import ThemeProvider from "./ThemeProvider"; +import { PagesNavigatorOptions } from '../../../types'; +import { NavigatorProvider } from '../context'; +import { useSanityFetch } from '../utils'; +import Header from './Header'; +import { List } from './List'; +import LocaleSelect from './LocaleSelect'; +import SearchBox from './SearchBox'; +import ThemeProvider from './ThemeProvider'; +import { useCurrentUser } from 'sanity'; export function createPagesNavigator(props: PagesNavigatorOptions) { return function PagesNavigator() { @@ -16,8 +17,23 @@ export function createPagesNavigator(props: PagesNavigatorOptions) { } function DefaultPagesNavigator(props: PagesNavigatorOptions) { + const currentUser = useCurrentUser(); + const filterBasedOnRoles = props?.filterBasedOnRoles; + + const queryFilter = [`pathname.current != null`]; + // Allow additional filters + if (filterBasedOnRoles) { + for (const f of filterBasedOnRoles) { + if (f.role === 'all') { + queryFilter.push(`${f.filter}`); + } else if (currentUser?.roles.some((r) => r.name === f.role)) { + queryFilter.push(`${f.filter}`); + } + } + } + const pagesRoutesQuery = ` - *[pathname.current != null]{ + *[${queryFilter.join(' && ')}]{ _rev, _id, _originalId, diff --git a/packages/sanity-studio/src/plugins/navigator/index.ts b/packages/sanity-studio/src/plugins/navigator/index.ts index 43261682..e62c6b33 100644 --- a/packages/sanity-studio/src/plugins/navigator/index.ts +++ b/packages/sanity-studio/src/plugins/navigator/index.ts @@ -1,9 +1,9 @@ -import { definePlugin } from "sanity"; -import { presentationTool } from "sanity/presentation"; +import { definePlugin } from 'sanity'; +import { presentationTool } from 'sanity/presentation'; -import { PagesNavigatorPluginOptions } from "../../types"; -import { createPagesNavigator } from "./components/DefaultPagesNavigator"; -import { createPageTemplates, normalizeCreatablePages } from "./utils"; +import { PagesNavigatorPluginOptions } from '../../types'; +import { createPagesNavigator } from './components/DefaultPagesNavigator'; +import { createPageTemplates, normalizeCreatablePages } from './utils'; /** * The `pages` plugin is a wrapper around Sanity's `presentation` plugin. * When enabled, it will add Tinloof's pages navigator to the prensentation view. @@ -33,20 +33,21 @@ export const pages = definePlugin((config) => { config.creatablePages ); return { - name: "tinloof-pages-navigator", + name: 'tinloof-pages-navigator', schema: { templates: createPageTemplates(normalizedCreatablePages), }, plugins: [ presentationTool({ ...config, - title: config.title ?? "Pages", + title: config.title ?? 'Pages', components: { unstable_navigator: { component: createPagesNavigator({ i18n: config.i18n, creatablePages: normalizedCreatablePages, folders: config.folders, + filterBasedOnRoles: config.filterBasedOnRoles, }), minWidth: config.navigator?.minWidth ?? 320, maxWidth: config.navigator?.maxWidth ?? 480, diff --git a/packages/sanity-studio/src/types.ts b/packages/sanity-studio/src/types.ts index 43c5fccf..d8fb409e 100644 --- a/packages/sanity-studio/src/types.ts +++ b/packages/sanity-studio/src/types.ts @@ -1,5 +1,5 @@ -import { Language as Locale } from "@sanity/document-internationalization"; -import { LocalizePathnameFn } from "@tinloof/sanity-web"; +import { Language as Locale } from '@sanity/document-internationalization'; +import { LocalizePathnameFn } from '@tinloof/sanity-web'; import { FieldDefinitionBase, ObjectDefinition, @@ -14,13 +14,13 @@ import { StringDefinition, StringInputProps, StringSchemaType, -} from "sanity"; +} from 'sanity'; import { NavigatorOptions as PresentationNavigatorOptions, PresentationPluginOptions, -} from "sanity/presentation"; +} from 'sanity/presentation'; -import { SlugContext } from "./hooks/usePathnameContext"; +import { SlugContext } from './hooks/usePathnameContext'; export type NormalizedCreatablePage = { title: string; @@ -36,6 +36,11 @@ export type FoldersConfig = { }; }; +export type FilterBasedOnRoles = { + role: 'all' | 'contributor' | 'editor' | 'administrator' | 'viewer' | string; + filter: string; +}; + export type PagesNavigatorOptions = { i18n?: { locales: Locale[]; @@ -45,6 +50,7 @@ export type PagesNavigatorOptions = { }; creatablePages?: Array; folders?: FoldersConfig; + filterBasedOnRoles?: FilterBasedOnRoles[]; }; export type PagesNavigatorPluginOptions = PresentationPluginOptions & { @@ -54,17 +60,18 @@ export type PagesNavigatorPluginOptions = PresentationPluginOptions & { requireLocale?: boolean; localizePathname?: LocalizePathnameFn; }; - navigator?: Pick; + navigator?: Pick; creatablePages?: Array; folders?: FoldersConfig; title?: string; + filterBasedOnRoles?: FilterBasedOnRoles[]; }; export type Page = { _rev: string; _id: string; _originalId: string; - _type: Exclude<"string", "folder">; + _type: Exclude<'string', 'folder'>; _updatedAt: string; _createdAt: string; pathname: string | null; @@ -77,8 +84,8 @@ export type PageTreeNode = Page & { edited?: boolean; }; -export type FolderTreeNode = Omit & { - _type: "folder"; +export type FolderTreeNode = Omit & { + _type: 'folder'; title: string; children: Tree; }; @@ -131,7 +138,7 @@ export type ReducerAction = { }; export interface DocumentWithLocale extends SanityDocument { - locale: Locale["id"]; + locale: Locale['id']; } export interface SectionOptions extends ObjectOptions { @@ -143,7 +150,7 @@ export interface SectionOptions extends ObjectOptions { * * The `custom` property is strictly typed to include what the toolkit needs for scaffolding the website & studio. */ -export interface SectionSchema extends Omit { +export interface SectionSchema extends Omit { options: SectionOptions; } @@ -193,7 +200,7 @@ export type PathnameSourceFn = ( context: SlugContext ) => string | Promise; -export type PathnameOptions = Pick & { +export type PathnameOptions = Pick & { source?: string | Path | PathnameSourceFn; prefix?: PathnamePrefix; folder?: { @@ -209,7 +216,7 @@ export type PathnameOptions = Pick & { export type PathnameParams = Omit< SlugDefinition & FieldDefinitionBase, - "type" | "options" | "name" + 'type' | 'options' | 'name' > & { name?: string; options?: PathnameOptions; @@ -227,7 +234,7 @@ export type IconOptions = { export type IconParams = Omit< StringDefinition & FieldDefinitionBase, - "type" | "name" | "options" + 'type' | 'name' | 'options' > & { name?: string; options?: IconOptions; From 862b9cf394ae850832abce0558e8d81769fd501d Mon Sep 17 00:00:00 2001 From: stilyan-tinloof Date: Fri, 14 Feb 2025 11:10:54 +0000 Subject: [PATCH 2/2] filter readme and changeset --- .changeset/dirty-geckos-rush.md | 5 + packages/sanity-studio/README.md | 186 ++++++++++++++++++------------- 2 files changed, 113 insertions(+), 78 deletions(-) create mode 100644 .changeset/dirty-geckos-rush.md diff --git a/.changeset/dirty-geckos-rush.md b/.changeset/dirty-geckos-rush.md new file mode 100644 index 00000000..c5011162 --- /dev/null +++ b/.changeset/dirty-geckos-rush.md @@ -0,0 +1,5 @@ +--- +"@tinloof/sanity-studio": minor +--- + +Pages navigator filter diff --git a/packages/sanity-studio/README.md b/packages/sanity-studio/README.md index 9c42875c..e3d3d572 100644 --- a/packages/sanity-studio/README.md +++ b/packages/sanity-studio/README.md @@ -38,7 +38,7 @@ Pages is a plugin that wraps [Presentation](https://www.sanity.io/docs/presentat #### 1. Configure Pages: ```tsx -import { pages } from "@tinloof/sanity-studio"; +import { pages } from '@tinloof/sanity-studio'; export default defineConfig({ // ... other Sanity Studio config @@ -47,7 +47,7 @@ export default defineConfig({ // Presentation's configuration previewUrl: { previewMode: { - enable: "/api/draft", + enable: '/api/draft', }, }, }), @@ -58,14 +58,14 @@ export default defineConfig({ #### 2. Add a `pathname` field to page schemas using the `definePage` helper: ```tsx -import { definePathname } from "@tinloof/sanity-studio"; +import { definePathname } from '@tinloof/sanity-studio'; export default defineType({ - type: "document", - name: "modularPage", + type: 'document', + name: 'modularPage', fields: [ definePathname({ - name: "pathname", + name: 'pathname', }), ], }); @@ -76,16 +76,16 @@ Documents with a defined `pathname` field value are now recognized as pages and Like Sanity's native `slug` type, the `pathname` supports a `source` option which can be used to generate the pathname from another field on the document, eg. the title: ```tsx -import { definePathname } from "@tinloof/sanity-studio"; +import { definePathname } from '@tinloof/sanity-studio'; export default defineType({ - type: "document", - name: "modularPage", + type: 'document', + name: 'modularPage', fields: [ definePathname({ - name: "pathname", + name: 'pathname', options: { - source: "title", + source: 'title', }, }), ], @@ -104,17 +104,17 @@ When a page is created, it will automatically have the current folder in its pat ```tsx -import { pages } from "@tinloof/sanity-studio"; +import { pages } from '@tinloof/sanity-studio'; export default defineConfig({ // ... other Sanity Studio config plugins: [ pages({ // Add any documents you want to be creatable from the pages navigator - creatablePages: ["page"], + creatablePages: ['page'], previewUrl: { previewMode: { - enable: "/api/draft", + enable: '/api/draft', }, }, }), @@ -134,14 +134,14 @@ Pathnames are automatically validated to be unique accros locales. ```tsx -import { pages } from "@tinloof/sanity-studio"; +import { pages } from '@tinloof/sanity-studio'; const i18nConfig = { locales: [ - { id: "en", title: "English" }, - { id: "fr", title: "French" }, + { id: 'en', title: 'English' }, + { id: 'fr', title: 'French' }, ], - defaultLocaleId: "en", + defaultLocaleId: 'en', }; export default defineConfig({ @@ -151,7 +151,7 @@ export default defineConfig({ i18n: i18nConfig, previewUrl: { previewMode: { - enable: "/api/draft", + enable: '/api/draft', }, }, }), @@ -162,11 +162,11 @@ export default defineConfig({ * Don't forget to add i18n options and locale field to your document schema */ export default defineType({ - type: "document", - name: "page", + type: 'document', + name: 'page', fields: [ definePathname({ - name: "pathname", + name: 'pathname', options: { // Add i18n options i18n: { @@ -177,14 +177,44 @@ export default defineType({ }), // Add locale field defineField({ - type: "string", - name: "locale", + type: 'string', + name: 'locale', hidden: true, }), ], }); ``` +### Filtering pages based on user roles + +The `filterBasedOnRoles` option can be used to filter pages based on the current user's roles. + +```tsx +import { pages } from '@tinloof/sanity-studio'; + +export default defineConfig({ + // ... other Sanity Studio config + plugins: [ + pages({ + // Presentation's configuration + previewUrl: { + previewMode: { + enable: '/api/draft', + }, + }, + filterBasedOnRoles: [ + { role: 'all', filter: "!(_id match 'singleton*')" }, + { role: 'contributor', filter: "_type == 'blog.post'" }, + ], + }), + ], +}); +``` + +This allows you to build upon the base filter, `pathname.current != null`, to filter pages based on the current user's roles. + +Setting `role: "all"` will set the filter to all roles while anything else will filter based on the current user's roles. + #### Support documents without a locale By default, when internationalization is enabled, only pages whose `locale` field matches the currently selected locale will be shown in the list. If you have page types that are not translated but you still want them to show up in the list, you can set the `requireLocale` option to false in your `i18n` config: @@ -192,10 +222,10 @@ By default, when internationalization is enabled, only pages whose `locale` fiel ```ts const i18nConfig = { locales: [ - { id: "en", title: "English" }, - { id: "fr", title: "French" }, + { id: 'en', title: 'English' }, + { id: 'fr', title: 'French' }, ], - defaultLocaleId: "en", + defaultLocaleId: 'en', requireLocale: false, }; ``` @@ -207,14 +237,14 @@ Now all documents with a `pathname` field will show up in the list regardless of By default, folders can be renamed. Set the `folder.canUnlock` option to `false` to disable this. ```tsx -import { definePathname } from "@tinloof/sanity-studio"; +import { definePathname } from '@tinloof/sanity-studio'; export default defineType({ - type: "document", - name: "modularPage", + type: 'document', + name: 'modularPage', fields: [ definePathname({ - name: "pathname", + name: 'pathname', options: { folder: { canUnlock: false, @@ -231,25 +261,25 @@ Documents can have their preview customized on the pages navigator using the [Li ```tsx export default { - name: "movie", - type: "document", + name: 'movie', + type: 'document', fields: [ { - title: "Title", - name: "title", - type: "string", + title: 'Title', + name: 'title', + type: 'string', }, { - type: "image", - name: "image", - title: "Image", + type: 'image', + name: 'image', + title: 'Image', }, ], // Preview information preview: { select: { - title: "title", - media: "image", + title: 'title', + media: 'image', }, prepare({ title, image }) { return { @@ -272,12 +302,12 @@ export default defineConfig({ pages({ previewUrl: { previewMode: { - enable: "/api/draft", + enable: '/api/draft', }, }, folders: { - "/news": { - title: "Articles", + '/news': { + title: 'Articles', icon: NewspaperIcon, }, }, @@ -291,14 +321,14 @@ export default defineConfig({ By default, the `pathname` field comes with a "Preview" button which is used to navigate to the page within the Presentation iframe when the pathname changes. You can optionally disable this manual button and have the Presentation tool automatically navigate to the new pathname as it changes: ```tsx -import { definePathname } from "@tinloof/sanity-studio"; +import { definePathname } from '@tinloof/sanity-studio'; export default defineType({ - type: "document", - name: "modularPage", + type: 'document', + name: 'modularPage', fields: [ definePathname({ - name: "pathname", + name: 'pathname', options: { autoNavigate: true, }, @@ -321,9 +351,9 @@ The `defineSection` field lets you easily define a new section schema. Used in c ```tsx // @/sanity/schemas/sections/banner.tsx export const bannerSection = defineSection({ - name: "block.banner", - title: "Banner", - type: "object", + name: 'block.banner', + title: 'Banner', + type: 'object', options: { variants: [ { @@ -331,14 +361,14 @@ export const bannerSection = defineSection({ * Will be used to display a preview image * when opening the section picker */ - assetUrl: "/images/blocks/hero.png", + assetUrl: '/images/blocks/hero.png', }, ], }, fields: [ defineField({ - name: "bannerSection", - type: "string", + name: 'bannerSection', + type: 'string', }), ], }); @@ -349,7 +379,7 @@ export const bannerSection = defineSection({ ```tsx // @/sanity/schemas/sections/index.tsx -import { bannerSection } from "@/sanity/schemas/sections/banner"; +import { bannerSection } from '@/sanity/schemas/sections/banner'; export const sections = [bannerSection]; ``` @@ -390,8 +420,8 @@ export const sections = [bannerSection]; ```tsx // @/sanity/schemas/index.tsx -import { sections } from "@sanity/schemas/index"; -import page from "@/sanity/schemas/page"; +import { sections } from '@sanity/schemas/index'; +import page from '@/sanity/schemas/page'; const schemas = [page, ...sections]; @@ -443,11 +473,11 @@ You can include additional properties ```tsx const locales = [ - { id: "en", title: "English", countryCode: "US", isDefault: true }, - { id: "fr", title: "French", countryCode: "FR" }, + { id: 'en', title: 'English', countryCode: 'US', isDefault: true }, + { id: 'fr', title: 'French', countryCode: 'FR' }, ]; -localizedItem(S, "blog.post", "Blog posts", locales, BookIcon); +localizedItem(S, 'blog.post', 'Blog posts', locales, BookIcon); ``` The utility will create a nested structure with: @@ -463,27 +493,27 @@ Builds upon a string field with an options list to show a preview of the icon se ### Basic usage ```tsx -import { iconSchema } from "@tinloof/sanity-studio"; -import { defineType } from "sanity"; +import { iconSchema } from '@tinloof/sanity-studio'; +import { defineType } from 'sanity'; export default defineType({ - type: "document", - name: "page", + type: 'document', + name: 'page', fields: [ { - type: "string", - name: "title", + type: 'string', + name: 'title', }, { ...iconSchema, options: { list: [ - { title: "Calendar", value: "calendar" }, - { title: "Chat", value: "chat" }, - { title: "Clock", value: "clock" }, + { title: 'Calendar', value: 'calendar' }, + { title: 'Chat', value: 'chat' }, + { title: 'Clock', value: 'clock' }, ], - path: "/icons/select", - backgroundColor: "black", + path: '/icons/select', + backgroundColor: 'black', }, }, ], @@ -511,18 +541,18 @@ Plugin to disable the creation of doucments with the `disableCreation` option se ```tsx sanity.config.ts; -import { disableCreation } from "@tinloof/sanity-studio"; -import schemas from "@/sanity/schemas"; +import { disableCreation } from '@tinloof/sanity-studio'; +import schemas from '@/sanity/schemas'; export default defineConfig({ - name: "studio", - title: "Studio", - projectId: "12345678", - dataset: "production", + name: 'studio', + title: 'Studio', + projectId: '12345678', + dataset: 'production', schema: { types: schemas, }, - plugins: [disableCreation({ schemas: ["home", "header", "footer"] })], + plugins: [disableCreation({ schemas: ['home', 'header', 'footer'] })], }); ```