2.0.0-v3-studio.0 • Published 2 years ago

@snorreeb/sanity-plugin-nrkno-schema-structure v2.0.0-v3-studio.0

Weekly downloads
-
License
MIT
Repository
github
Last release
2 years ago

@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 DocumentDefinition from nrkno-sanity-typesafe-schemas. It is recommended to use sanity 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.views.

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

yarn add @snorreeb/sanity-plugin-nrkno-schema-structure

npm

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

Usage

This lib requires some setup:

Create typesafe root groups

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

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

export const customGroups = {
  group1: 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,
  }),
  group2: 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 '@snorreeb/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 {}
}

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

Configure "Create new" menu

Then configure the create new document menu :

import { createTemplates } from '@snorreeb/sanity-plugin-nrkno-schema-structure';
import { customGroups } from './structure-registry';

export default defineConfig({
  /*...*/
  schema: {
    /*...*/
    templates: (prev, context) => {
      return createTemplates(prev, context, Object.values(customGroups));
    },
  },
});

Configure Studio Structure

Then configure the Studio structure:

import { createStructureRegistry } from './structure-registry';

export default defineConfig({
  /*...*/
  plugins: [
    deskTool({
      structure: async (S, Context) => {
        const structureRegistry = createStructureRegistry(S, Context);

        // 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);
      },
      defaultDocumentNode: (S, context) =>
        createStructureRegistry(S, context).getSchemaViews({ S, context }) ?? S.document(),
    }),
  ],
});

Use customStructure in schemas

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

In your schema:

import { schema } from 'sanity';

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 'sanity';

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, context) => [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
});

Develop & test

This plugin uses @sanity/plugin-kit with default configuration for build & watch scripts.

See Testing a plugin in Sanity Studio on how to run this plugin with hotreload in the studio.