@nrk/sanity-plugin-nrkno-schema-structure
v1.0.15
Published
_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
Downloads
60
Maintainers
Keywords
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
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
});