@nrk/sanity-plugin-nrkno-schema-structure

_This document assumes familiarity with [Sanity StructureBuilder](https://www.sanity.io/docs/structure-builder-introduction)._ _It builds on the [principles of nrkno-sanity](https://github.com/nrkno/nrkno-sanity-libs/blob/master/docs/nrkno-sanity-principl

Usage no npm install needed!

<script type="module">
  import nrkSanityPluginNrknoSchemaStructure from 'https://cdn.skypack.dev/@nrk/sanity-plugin-nrkno-schema-structure';
</script>

README

@nrk/sanity-plugin-nrkno-schema-structure

This document assumes familiarity with Sanity StructureBuilder. It builds on the principles of nrkno-sanity and option driven design.

nrkno-schema-structure allows schemas to use a declarative approach to Sanity Studio structure, by configuring a customStructure field in document schemas.

This lib uses and extends DocumentSchema from nrkno-sanity-typesafe-schemas. It is recommended to use @nrk/nrkno-sanity-typesafe-schemas when creating schemas, so this lib can be used in a typesafe manner.

At-a-glance

structure.png

Figure 1: A document-list for schema type "Animasjonsscroll", placed in "Animasjoner" group, using the below schema-driven config.

schema('document', {
  type: 'animasjonsscroll',
  title: 'Animasjonsscroll',
+ customStructure: {
+  type: 'document-list',
+  group: 'animation',
+ },
  fields: [
   /* omitted */
  ]
})

At the time of writing, NRK organizes 60+ document schemas using this approach.

Overview

The basic idea is to have schemas declare what should be placed where in a directory-like structure, without knowing how it is done.

nrkno-schema-structure finds all schemas with customStructure and creates a structure-registry. Groups can be obtained by name, and contain everything that where declaratively added to them. Groups can then be composed into any Sanity StructureBuilder hierarchy.

Groups can contain subgroups (S.listItem), document-lists (S.documentTypeList), document-singletons (S.document), custom-builders (ad-hoc S.listItem builders) and dividers (S.divider).

All of these will be sorted by a sort-key (sortKey ?? title), making it possible to compose complex structure hierarchies locally from each schema.

The library provides support for managing the "Create new document" menu, by filtering out schemas that should not appear there.

All custom structures support the enabledForRoles option out-of-the-box, which makes it simple to hide schemas form users without access.

The declarative nature of this approach aligns well with the principles of nrkno-sanity and option driven design

nrkno-schema-structure also supports views (split panes) in a declarative manner, using customStructure.view.

The final structure is still fully customizable by each Studio, and the library can easily be composted with existing structure code. The API provides a list of all ungrouped schemas, so that they can be placed wherever it makes sense.

Installation

Yarn

In Sanity studio project run:

npx sanity install @nrk/sanity-plugin-nrkno-schema-structure

This will run yarn install & add the plugin to sanity.json plugin array.

npm

npm install --save @nrk/sanity-plugin-nrkno-schema-structure

Add "@nrk/sanity-plugin-nrkno-schema-structure" to "plugins" in sanity.json manually.

Usage

This lib requires some setup:

Create typesafe root groups

First we define typesafe groups. Create structure-registry.ts:

import {
  createCustomGroup,
  initStructureRegistry,
} from '@nrk/sanity-plugin-nrkno-schema-structure';

export const customGroups = {
  format: createCustomGroup({
    urlId: 'group1', // same as id in strucure builder
    title: 'Group 1',
   // schemas in this group (or subgroups) can be created in the top menu
    addToCreateMenu: true, 
  }),
  animation: createCustomGroup({
    urlId: 'group2',
    title: 'Group 2.',
    icon: () => 'II',
   // schemas under this group must be created via the document list
    addToCreateMenu: false,
  }),
} as const;

type CustomGroups = typeof customGroups;

declare module '@nrk/sanity-plugin-nrkno-schema-structure' {
  // Here we extend the GroupRegistry type with our groups.
  // This makes groupId typesafe whe using the structure registry
  // eslint-disable-next-line @typescript-eslint/no-empty-interface
  interface GroupRegistry extends CustomGroups {}
}

// part:@sanity/base/new-document-structure does not support promises.
// Until it does, this verbose setup is required so we can lazyload the registry.
// Unfortunatly, user cannot be resolved in time to support enabledForRoles
// in "Create new" menu, so we must expose createStructureRegistry() as well     

let structureRegistry: StructureRegistryApi;
export const getStructureRegistry = () => {
 if (!structureRegistry) {
  structureRegistry = createStructureRegistry();
 }
 return structureRegistry;
};

export function createStructureRegistry() {
 return initStructureRegistry({
  groups: Object.values(customGroups),
  locale: 'no-no' // locale used for sorting
 });
}

Configure "Create new" menu

Then configure the create new document menu :

import { createInitialValueTemplates } from '@nrk/sanity-plugin-nrkno-schema-structure';
import { createStructureRegistry } from './structure-registry';

// part:@sanity/base/new-document-structure does not support promises.
// Until it does, we have to init structureRegistry twice, once
// before user is resolved (like here), and lazily with user resolved in structure.ts

// Unfortunatly, user cannot be resolved in time to support enabledForRoles
// in "Create new" menu, so we must use createStructureRegistry() here.

const templates = createInitialValueTemplates(createStructureRegistry().getGroupRegistry());

export default [...templates];

Configure Studio Structure

Then configure the Studio structure:

import { StructureBuilder as S } from '@sanity/structure';
import { isDefined } from '../types/type-util';
import { authorStructure } from '../features/author/author';
import { getStructureRegistry } from './structure-registry';

export const getDefaultDocumentNode = ({ schemaType }: { schemaType: string }) => {
  // adds support for customStructure.views in schemas
  return getStructureRegistry().getSchemaViews({ schemaType }) ?? S.document();
};

// Sanity supports async structure
export default async () => {
  const { getGroupItems, getGroup, getUngrouped } = getStructureRegistry();
  
  // Compose the Sanity Structure.
  // Can be combind with any amount of manual S.itemList nodes.
  const items = [
    // schemas with customStructure.group: 'group1' are contained in this group. 
    structureRegistry.getGroup('group1'),
          
   // schemas without group is part of the ungrouped list, one listItem per schema (hence the spread)      
   S.divider(),
   structureRegistry.getUngrouped(),
   S.divider(),
          
   // schemas with customStructure.group: 'group2' are contained in this group
   // notice the use of getGroupItems (as opposed to getGroup). 
   // This inlines the direct children of the group.
   structureRegistry.getGroupItems('group2')
  ].flatMap(i => i); // flatmap to flatten everyting 

  return S.list().title('Content').items(items);
};

Use customStructure in schemas

Finally, we can start organizing schemas directly from the schema definition.

In your schema:

import { schema } from '@nrk/nrkno-sanity-typesafe-schemas';

export const mySchema = schema('document', {
  type: 'my-schema',
  title: 'My schema',
  customStructure: {
    type: 'document-list',
    // this group will be typesafe. Ie, autocomplete and 'group3' will give compileerror
    group: 'group1', 
  },
  fields: [
   /* omitted */
  ]
})

Supported structures

customStructure supports a handful of different usecases, most of them controlled by the optional type-field.

Standard documents

Document-schemas without options.customStructure are available directly in the root content list.

Documents without customStructure appear in structureRegistry.getUngrouped() using the default S.documentTypeList.

Group

Groups (root groups) contain every schema that has been configured to appear in them. Groups can have subgroups, which in turn can have subgroups.

These are static, and must be provided when initializing the structure registry. See the Usage section above for how to configure them in a typesafe manner.

Groups are accessed using structureRegistry.getGroup('groupId') and appear as S.listItems. Subgroups cannot be accessed directly.

Document list

Puts the schema in a S.documentTypeList, under the provided group.

const partialSchema = {
  customStructure: {
    type: 'document-list',
    group: 'group1', 
  }
}

This schema appears in structureRegistry.getGroup('group1') as a S.documentTypeList.

Document singleton

The schema will only list documents with the configured ids. Maps to S.document.

const partialSchema = {
  customStructure: {
   type: 'document-singleton',
   group: 'help',
   documents: [
    { documentId: 'user-help', title: 'Sanity-help' },
    { documentId: 'developer-help', icon: () => 'Dev' , title: 'Developer-help' },
   ],
  },
}

This schema appears in structureRegistry.getGroup('group1') as two S.document nodes.

Custom builder

Use this if you want a handwritten structure for the schema.

const partialSchema = {
   customStructure: {
      type: 'custom-builder',
      group: 'group1',
      listItem: () =>
        S.listItem()
          .id('url-path')
          .title('Some custom thing')
          .child(S.documentList().id('some-schema')),
    }
}

This schema appears in structureRegistry.getGroup('group1') as the provided structure.

Manual

Totally removes the schema from the structure registry.

This is useful if we want place the schema anywhere using regular S.builder functions, any way we want.

const partialSchema = {
  customStructure: {
    type: 'manual'
  }
}

This schema appears in the structureRegistry.getManualSchemas()

Subgroup

Subgroups are ad-hoc groups that can be provided to any other custom structure. Subgroups must be used alongside the group parameter in customStructure.

Create subgroups constants:

import {SubgroupSpec} from '@nrk/sanity-plugin-nrkno-schema-structure'

export const mySubgroup: SubgroupSpec = {
  urlId: 'mySubgroup',
  title: 'Subgroup',
};

export const nestedSubgroup: SubgroupSpec = {
 urlId: 'nested',
 title: 'Nested Subgroup',
 // its is also possible to prepopulate a subgroup with custom-builders
 // customItems: [] 
};

Subgroups appear as S.listItems.

Then use it in a schema.customStructure:

import { schema } from '@nrk/nrkno-sanity-typesafe-schemas';

export const mySchema = schema('document', {
  type: 'my-schema',
  title: 'My schema',
  customStructure: {
    type: 'document-list',
   // this document-list will be placed under Group 1 -> Subgroup -> Nested Subgroup -> My schema
   group: 'group1', 
   subgroup: [mySubgroup, nestedSubgroup]
  },
  fields: [
   /* omitted */
  ]
})

This schema appears nested in subgroups under structureRegistry.getGroup('group1') as S.documentTypeList

Subgroups can technically be defined inline, but its better to use a constant to avoid multiple subgroups using the same urlId within the same group (undefined behaviour).

customStructure without group

Some nrkno-sanity-structure features do not require a group.

In this case they will affect how the schema appears when accessed from getUngrouped().

const partialSchema = {
  customStructure: {
   type: 'document-list',
   title: 'Special thing',
   icon: () => 'Custom icon',
   omitFormView: true,
   views: [S.view.component(SpecialForm).title('HyperEdit')],
   enabledForRoles: ['developer'],
   addToCreateMenu: false,
   sortKey: 'xxxxxxWayLast',
   divider: 'below'
  }
}

This schema appears in structureRegistry.getUngrouped() as S.documentTypeList.

Dividers

Its possible to have dividers above or below a schema entry using customStrucutre.divider: 'over' | 'under' | 'over-under'

If exact location is required, playing around with sort key might be required.

const partialSchema = {
  customStructure: {
    type: 'document-list',
   divider: 'below', 
  }
}

This schema appears in structureRegistry.getUngrouped() as S.documentTypeList followed by S.divider.

Not supported at this time

Parametrized initial value templates.

Develop

This plugin is built with sanipack.

Test

In this directory

npm run build
npm link
cd /path/to/my-studio
npm link @nrk/sanity-plugin-nrkno-schema-structure

Note: due to potentially conflicting Sanity versions when linking, you should provide StructureBuilder as an argument to the api in the Studio:

import { StructureBuilder } from '@sanity/structure';

export const structureRegistry = initStructureRegistry({
  groups: Object.values(customGroups),
  StructureBuilder, // add this while testing with npm link
});