Index
Edit

How it works

How to configure the Desk tool using structure builder

To explain how the structure works, it’s usually easiest to think of how the desk tool navigation is laid out into columns, or “panes” as we call them.

The desk tool build-up

Each pane can host either a list, an editor or a custom component. A list consists of list items, which can be custom made, or automatically be set up from for example a type name. You can nest as many panes you want, although to deep structures tend to be not so user friendly. A pane can also hold a menu or a clickable action element.

Each pane in the structure can have items that tells the desk tool what the next pane should be, the contents of the next pane is then considered the item’s child. Which panes to display is identified by an ID, which is pushed to the URL and separated by semicolons (paneItem1;paneItem2;paneItem3 etc). The desk tool will then resolve what those IDs mean by calling a function on the pane prior to it. This enable you to share and return to the desk-tool state from the url.

Customizing structure

To use the structure feature, you will need to have Sanity version 0.134.0 or higher installed in your studio. You can upgrade by doing sanity upgrade in your studio folder from the terminal.

Defining the part

In order to customize structure you’ll have to implement it as a part. In your content studio’s sanity.json (at the root of the project), you have an array named parts. Your schema is most probably already an entry in this list.

In the same way, we’ll define a part for the structure. The definition looks like this:

{
  "name": "part:@sanity/desk-tool/structure",
  "path": "./deskStructure.js"
}

In this example, the structure definition file is located at the root of the project folder, but you can name it and place it wherever you want. Just make sure to get the path correct.

If you had the studio running locally while doing this, you should restart the process.

Configuring a structure

Hello structure!

The structure definition file we pointed at above should export a function which will return a structure definition. We’ll go into detail about what the definition looks like in a bit, but for now, let’s start by creating an empty javascript file in the referenced location (in the example above, <content-studio>/deskStructure.js).

Next, you will want to import a module to help you define the structure. We’ll use it build a list with a single item, just to get started:

import S from "@sanity/desk-tool/structure-builder";
 
export default () =>
  S.list()
    .title("Content")
    .items([
      S.listItem()
        .title("Your first structure!")
    ]);
Pro-tip

You can of course import the individual structure methods (import { list, listItem } from '@sanity/desk-tool/structure-builder'), but we prefer to import the whole package as S to make the code a bit easier to read.

If you reload your studio, you should now see the default navigation structure having been replaced with a single column holding an item with the title “Structure”. We have not defined what should happen when it is clicked, so for now it doesn’t actually lead anywhere.

Gotcha

If you are not seeing the expected structure, you might have to restart the studio for the part to be picked up.

Pro-tip

Each segment of the structure can return a plain structure definition (as above), a promise, or an observable. This will allow you to do things like providing different navigation and workflows for different users or change content based on external APIs, in real time.

Adding child items

Global config in the candy store

In order to make something happen when clicking an item in the root pane, you have to add a child to the listItems.

A child takes one of the structure methods, just as with the root pane. Here you can for example choose to render the editor for a certain document type with a predefined _id, what we sometimes call “a singleton”:

import S from "@sanity/desk-tool/structure-builder";
 
export default () =>
  S.list()
    .title("Content")
    .items([
      S.listItem()
        .title("Config")
        .child(
          S.editor()
            .id('config')
            .schemaType("config")
            .documentId("global-config")
        )
    ]);

When the user now clicks on the config list item in the root pane, a new document with the _id "global-config" will be created, and load the editor with with the fields for a document type called "config". If this document type doesn’t exist (because you haven’t made it yet), the editor will be empty.

Perhaps this was just the one thing you needed, and you want to just list out the document types and their documents like the studio does out of the box. You’ll find this list in a method called documentTypeList, just add it to the items-array, and make sure that you spread them out. Since we have a document type called config, we want to remove that from this list. We can do that by adding filter to the documentTypeList-array and remove the list item with the id config.

import S from "@sanity/desk-tool/structure-builder";

const hiddenDocTypes = listItem => ![
  "config"
].includes(listItem.getId())

export default () =>
  S.list()
    .title("Content")
    .items([
      S.listItem()
        .title("Config")
        .child(
          S.editor()
            .id('config')
            .schemaType("config")
            .documentId("global-config")
        ),
      ...S.documentTypeListItems()
        .filter(hiddenDocTypes)
    ]);

Segmented content

Products segmented by categories

Another common usecase is to group documents in different ways. Since structure builder is “just JavaScript” and most of the methods returns arrays of documents, there are almost endless possibilities for how you can organize these lists.

A thing you might want to do, however, is to make a segmented list based on some references between documents. For example, in the candy store we have a type for categories, and it would be nice to be able to group the products based on their relationship to a category. We do that by adding a listItem to the root, and give it a title and a child. In the child (i.e. the second pane) we add the method documentTypeList that takes a document type name as a parameter and returns a list of the respective documents in this pane.

In the this pane we put a documentList that has a filter and a params method. The filter method takes a GROQ filter expression, where we can use variables that we send in with the params-method. In the example we filter documents by the type “category”:

import S from '@sanity/desk-tool/structure-builder'

export default () =>
  S.list()
    .title("Content")
    .items([
      S.listItem()
        .title('Products by categories')
        .child(
          S.documentList()
            .title('Parent categories')
            .menuItems(S.documentTypeList('category').getMenuItems())
            .filter('_type == $type && !defined(parents)')
            .params({ type: 'category' })
        )
      // The rest of the structure
    ]);

This will list out categories with no parent categories. S.documentList() will automatically render the editor if the document items for the categories are clicked. But we're only half way!

Let's add a child with the child categories. In the child-method for documentList we can add a function that takes the id of each the documents in the document type. In the example underneath this will be the documents in the type category. In other words, when you click on a category, we'll send its _id to the next pane, where we can use this id to decide what should be listed there:

import S from '@sanity/desk-tool/structure-builder'

export default () =>
  S.list()
    .title("Content")
    .items([
      S.listItem()
        .title('Products by categories')
        .child(
          S.documentList()
            .title('Products')
            .menuItems(S.documentTypeList("category").getMenuItems())
            .filter('_type == $type && !defined(parents)')
            .params({ type: 'category' })
+           .child(categoryId =>
+             S.documentList()
+             .title('Child categories')
+             .menuItems(S.documentTypeList("category").getMenuItems())
+             .filter('_type == $type && $categoryId in parents[]._ref')
+             .params({ type: 'category', categoryId })
+           )  
          ),
      // The rest of the structure
    ]);


And then the products, works pretty much the same way:

import S from '@sanity/desk-tool/structure-builder'

const preview = () =>
  S.list()
    .title('Content')
    .items([
      S.listItem()
          .title('Products by categories')
          .child(
            S.documentList()
              .title('Parent categories')
              .menuItems(S.documentTypeList('category').getMenuItems())
              .filter('_type == $type && !defined(parents)')
              .params({ type: 'category' })
              .child(categoryId =>
                S.documentList()
                .title('Child categories')
                .menuItems(S.documentTypeList('category').getMenuItems())
                .filter('_type == $type && $categoryId in parents[]._ref')
                .params({ type: 'category', categoryId })
+               .child(categoryId =>
+                S.documentList()
+                   .title('Products')
+                   .menuItems(S.documentTypeList('product').getMenuItems())
+                   .filter('_type == $type && $categoryId in categories[]._ref')
+                   .params({ type: 'product', categoryId })
                )
              )
            ),
    ])

This is also the way you can express nested hiearchies in the desk tool.

Using filters to make editoral workflows

You can also use structure builder to facilitate simple workflows in the studio. Let’s say you have added a document type called article to the e-commerce studio (you know, for marketing), and you want to enable an editoral workflow where writers can tag the articles as “needs review", "awaiting publication", and "published".

Let’s begin with adding the editorial states to the article schema. It doesn't need to be more complex than a simple string field with some predefined options:

{
  name: "editorialState",
  type: "string",
  options: {
    list: [
      { title: "Needs review", value: "review" },
      { title: "Awaiting publication", value: "awaiting" },
      { title: "Published", value: "published" }
    ],
    layout: "radio"
  }
}
Workflow radio buttons

Now, let's put everything that has to do with these articles under “Marketing” in the root pane. Working with the structure builder, it's often easiest to configure one pane at a time. Let’s put the listItems "Up for review", "Awaiting publication", "Published", and "Drafts" in the second pane:

export default () =>
  S.list()
    .title('Content')
    .items([
      S.listItem()
        .title('Marketing')
        .child(
          S.list()
            .title('Marketing')
            .items([
              S.listItem()
                .title('Up for review'),
              S.listItem()
                .title('Awaiting publication'),
              S.listItem()
                .title('Published'),
              S.listItem()
                .title('Drafts'),
              S.listItem()
                .title('All articles')
            ])
        ),
      // the rest of the structure
    ])
This will give those Oompa Loompas something to work with

Now we need to add child items to each of these menuItems, the first one is “Up for review":

export default () =>
  S.list()
    .title('Content')
    .items([
      S.listItem()
.title('Marketing')
.child( S.list() .title('Marketing') .items([ S.listItem() .title('Up for review') + .child( + S.documentList() + .title('For review') + .filter('_type == $type && editorialState == $state') + .params({ type: 'article', state: 'review' }) + ), S.listItem() .title('Awaiting publication'), S.listItem() .title('Published'), S.listItem() .title('Drafts'), S.listItem() .title('All articles') ]) ), // the rest of the structure ])

The same pattern goes for the next two menuItems in the marketing pane, with only the param value changed:

export default () =>
  S.list()
    .title('Content')
    .items([
      S.listItem()
        .title('Marketing')
        .child(
          S.list()
            .title('Marketing')
            .items([
              S.listItem()
                .title('Up for review')
                .child(
                  S.documentList()
                    .title('For review')
                    .filter('_type == $type && editorialState == $state')
                    .params({ type: 'article', state: 'review' })
                ),
              S.listItem()
                .title('Awaiting publication'),
                .child(
                  S.documentList()
                    .title('For review')
                    .filter('_type == $type && editorialState == $state')
                    .params({ type: 'article', state: 'awaiting' })
                ),
              S.listItem()
                .title('Published'),
+               .child(
+                 S.documentList()
+                   .title('For review')
+                   .filter('_type == $type && editorialState == $state')
+                   .params({ type: 'article', state: 'published' })
+               ),
              S.listItem()
                .title('Drafts'),
              S.listItem()
                .title('All articles')
            ])
        ),
      // the rest of the structure
    ])

In this case, we will let “Drafts” be articles where the editorial status haven't been set yet (!defined(editorialState)) and the last item will be just a list of all articles using documentTypeList('article'):

export default () =>
  S.list()
    .title('Content')
    .items([
      S.listItem()
        .title('Marketing')
        .child(
          S.list()
            .title('Marketing')
            .items([
              S.listItem()
                .title('Up for review')
                .child(
                  S.documentList()
                    .title('For review')
                    .filter('_type == $type && editorialState == $state')
                    .params({ type: 'article', state: 'review' })
                ),
              S.listItem()
                .title('Awaiting publication')
                .child(
                  S.documentList()
                    .title('For review')
                    .filter('_type == $type && editorialState == $state')
                    .params({ type: 'article', state: 'awaiting' })
                ),
              S.listItem()
                .title('Published')
                .child(
                  S.documentList()
                    .title('For review')
                    .filter('_type == $type && editorialState == $state')
                    .params({ type: 'article', state: 'published' })
                ),
              S.listItem()
                .title('Drafts')
+ .child(
+ S.documentList()
+ .title('Awaiting publication')
+ .filter('_type == $type && !defined(editorialState)')
+ .params({ type: 'article' })
+ ),
S.listItem() .title('All articles')
+ .child(S.documentTypeList('article'))
]) ), // the rest of the structure ])

Previous: Structure builderNext: Methods Reference