Applying arbitrary order to documents in Sanity using reference fields and custom ID generation

77 replies
Last updated: May 10, 2020
Hi Sanity folks! I can't find an existing, usable solution for applying an arbitrary order to documents? The best I can come up with is an
order
field of type
number
and manually tweak those, but that's a real pain. Any ideas or recommendations for existing pluings that might handle this?
AI Update

Good news! There's an official plugin that solves exactly this problem: @sanity/orderable-document-list.

This plugin gives you drag-and-drop ordering directly in the Studio editing interface, so you don't have to manually fiddle with number fields anymore. It's maintained by the Sanity team and works by adding an orderRank field to your documents behind the scenes.

Installation:

npm i @sanity/orderable-document-list

The plugin is designed for Studio v3 (for Studio v2, you'd need the @studio-v2 tag, but that's deprecated now). You can find the full setup instructions in the GitHub README.

Why this is better than a manual order field:

  • No more manually adjusting numbers when you want to reorder
  • Drag-and-drop interface is way more intuitive for content editors
  • The ordering persists and can be queried from your frontend
  • Handles the complexity of maintaining order ranks automatically

Alternative approaches (if the plugin doesn't fit your use case):

  • Create a singleton document with an array of references to control order
  • Use wrapper documents containing reference arrays
  • Stick with the numerical order field approach (though as you've found, it's tedious)

The orderable document list plugin works great for small to medium-sized collections where manual ordering makes sense. For very large datasets, you might want to consider query-based sorting instead.

If its a nested list, I think you can use the _key property (it will yell at you if you don't have it)
The usual approach is to make an “order” document that holds an array of references to the document type you want to order. You can use structure builder to make this a singleton and organize it so that it is located alongside an “all documents of this type” list.
The advantage is that you don’t lock your documents into one specific order, although we recognize that sometimes that’s what you want. Affordances for that type of ordering is in the plan of things to look at.
I think I'm in a good position to do that. I have categories and people, with each people having a reference to a category. I want to order the people.
So what I should do, is reverse the reference and instead reference people from the category?
...and this way, those can be ordered within the scope of the category?
That should work if I understand you correctly
Great, I'm going to give it a try.
Thank you for your help, much appreciated.
👍 let us know if you encounter more hurdles!
Hurdle:
{
      name: 'signees',
      title: 'Signees',
      type: 'array',
      of: [{ type: 'signee' }]
    }
Using this, I can add signees into the array, but I cannot select pre-existing signees (of type 'signee')
I have an actual document type
signee
with existing data. I probably have to find documentation how to use reference and not just "type"?
You need to put a reference field “in between”
Array of reference to the type signee
Got it.

{
      name: 'signees',
      title: 'Signees',
      type: 'array',
      of: [{
        type: 'reference',
        to: [
          {type: 'signee'}
        ]
      }]
    }
Working now, thanks!
Sorry for being a little erratic. Very much hurry and very little sleep.
No worries
const signee_categories = await client.fetch(`*[_type == "signee_category"]{
    ...,
    "title": coalesce(title[$language], <http://title.fi|title.fi>),
    signees->{
      ...,
      "work": coalesce(work[$language], <http://work.fi|work.fi>),
      "title": coalesce(title[$language], <http://title.fi|title.fi>)
    }
  }`, { language });
...this didn't quite work as I expected. The signees are still just references in the result, and for some reason I got each category twice in the result set.
The duplicates seem to be some sort of "drafts" based on the _id:
_id": "drafts.f1796bf3-cd29-4ee7-9b3a-b202cb947a23"
oh, they were not published in the studio. But how can I prevent drafts from being included in the query response?
(and I still can't expand the references items in the array, they're still just references)
signees[]-&gt;{ ... }
&amp;&amp; !(_id in path(“drafts.*”))
Working perfectly now. I'd hug you if I could.
One (hopefully) final question:
Is there a way to import data (
signees
) from the command line so that they are automatically added to one of the
signee_categories
as a reference?
I'm assuming I can import the signees, but I have to one-by-one add each to the categories (that already exist).
There is, but it’s hard to write the code on the phone - could you share a zip of your schemas folder, and I’ll be on my laptop in a bit?
Sure!
how does the
signee
data look like that you're importing? That is, how do you want to determine which category it should end up in?
I have three separate JSON files currently
each file contains only the signees for one specific category
aha!
Also, the schema has the fields
title
and
work
translated with
localeString
, but my data doesn’t. The translations will be added afterwards.
So, each JSON file is an array of objects
{ name, title, work }
I haven't actually tested this, but it should be something like this: https://gist.github.com/kmelve/743b73cfa540415803ab4a9f32c6d849
I’ll give it a shot!
(updated it to make it a bit clearer)
there’s no logging here either, so a bit of a black box 🙂
ClientError: The mutation(s) failed: Malformed document ID: “-5_jTMsGxI3yJG-xGKhef”
Apparently nanoid produces invalid ids
?
Do I have to provide an _id, won’t Sanity create one for me if it’s missing?
You need to know the id to assign it to the category reference
Ah, of course.
You can restrict the characters nanoid uses: https://www.npmjs.com/package/nanoid#custom-alphabet-or-size
I think I could just slugify the name of the person and add a tiny randomized string to it.
…Or that.
you generally don't want hypens or dots in there
It worked! Yei.
Some items in this list are missing their keys. We need to fix this before the list can be edited.Fix missing keys
▼*Why is this happening?*
This usually happens when items are created through the API client from outside the Content Studio and someone forgets to set the 
_key
-property of list items.The value of the 
_key
 can be any string as long as it is unique for each element within the array
Greeted with this error in the studio, need to worry?
not really - but I thought I had included the _key in the script?
signees.forEach(async signee => {
  let _id = nanoid()

  // Add the new document
  await queue.add(() => client.create({
    _id,
    _type: 'signee',
    ...signee
  }))

  // insert a new reference object at the end of the signees array
  await queue.add(() => client.patch(CATEGORY_ID).insert('after', 'signees[-1]', [
    { _key: nanoid(), _type: 'reference', _ref: _id }
  ]).commit())
})
Here’s the beef of the scripts. There is a _key in the second queue.
Actually it didn’t work out with the references 😞
The signees are added though.
what does the data look like? inside of the array?
I just fixed that, I hope, by doing this:
await queue.add(() => client.create({
    _id,
    _type: 'signee',
    name: signee.name,
    '<http://work.fi|work.fi>': signee.work,
    '<http://title.fi|title.fi>': signee.title
  }))
I was using the
localeString
in the schema, hope that works?
My data isn’t yet localised.
await queue.add(() => client.create({
  _id,
  _type: 'signee',
  name: signee.name,
  work: {
    fi: signee.work,
  },
  title: {
    fi: signee.title
  }
}))
It should be something like this+
Half my brain is still working with MongoDB it seems
🙂
Ok, the signee is now created correctly, but the reference is still broken inside category.
but there’s data there, how does that look like?
first it complains about missing _key:s, then I have to manually pick the reference.
Here’s a sample of my array of objects:
export default [
  {
    name: 'John C. Ensored',
    title: 'Professor',
    work: 'Hospital'
  }
]
Hold on, I’ll see what the data looks like in category
{
  "_createdAt": "2020-05-09T08:27:16Z",
  "_id": "drafts.f00ca6bc-b1bc-4045-9172-4f42f84f90c1",
  "_rev": "zojct8-h1c-s3u-h08-n8l5a9g08",
  "_type": "signee_category",
  "_updatedAt": "2020-05-10T09:15:06Z",
  "order": 3,
  "signees": [
    {
      "_key": "6516d233c5a4a29483607f7a5d51b5f7",
      "_type": "reference"
    }
  ],
  "title": {
    "_type": "localeString",
    "en": "In support of",
    "fi": "Kannatamme tätä aloitetta"
  }
}
…so the
_ref
field is missing
yup
updated the gist a bit btw
Relevant bits seem to be ok here, about the _id generation:
*const* nanoid = () *=>* customAlphabet('1234567890abcdefghijklmnopqrstuvwz', 20);
and that’s the bug I see
try with the updates I made? (but adapt it to your localized) https://gist.github.com/kmelve/743b73cfa540415803ab4a9f32c6d849
Just got it working already:
signees.forEach(signee => {
  // Add the new document

  queue.add(async () => {
    let _id = nanoid()
    console.log('Adding', signee.name);
    await client.create({
      _id,
      _type: 'signee',
      name: signee.name,
      work: {
        fi: signee.work,
      },
      title: {
        fi: signee.title
      }
    })
    console.log('Updating category to include _ref', _id);
    await client.patch(CATEGORY_ID).insert('after', 'signees[-1]', [
      { _key: nanoid(), _type: 'reference', _ref: _id }
    ]).commit();
  })
})
First I introduced a bug with my botched up nanoid, and the original gist failed because the reference was attempted before the doc was available, I deduced from the error message.
Thank you so much for working with me on this. I think I’m all set. Sanity is great btw.

Sanity – Build the way you think, not the way your CMS thinks

Sanity is the developer-first content operating system that gives you complete control. Schema-as-code, GROQ queries, and real-time APIs mean no more workarounds or waiting for deployments. Free to start, scale as you grow.

Was this answer helpful?