Next.js, Headless CMS and eCommerce expert agency

Three ways to refer a current user in Sanity documents

There are pretty much situations when you'd like to have a reference to a Sanity studio user from your documents. In this article, we will consider scenarios when you need to refer a current user somehow related to a document. Before we dive into a technical implementation let's see why you might need it at all.

The most obvious cases are when you’d like to have fields like createdBy and updatedBy in your documents. That could be useful to keep article authors when creating a blog site. Many other CMS have such things out of the box. In Sanity, you need to implement it yourselves but at the same time, you have the flexibility to have more complex scenarios in your document flow. For example, you can add approvedBy, assignedTo, and publishedBy fields to your documents and use them for creating complex publishing flows. Other cases could be:

  • Role-based document access model
  • Improved Sanity studio UX
  • Extending document history, creating dashboards, and so on.

Content-type for storing user details

Let’s first create a new content type called user. In it, we will store the necessary information about the user.

Script 1. User content type

const user = {
 name: 'user',
 title: 'Authenticated User',
 type: 'object',
 fields: [
   {
     name: 'id',
     type: 'string',
   },
   {
     name: 'name',
     type: 'string',
   },
   {
     name: 'email',
     type: 'string',
   },
   {
     name: 'profileImage',
     type: 'string',
   },
   {
     name: 'role',
     type: 'string',
   },
   {
     name: 'provider',
     type: 'string',
   },
 ],
};

The id field will store the Santy user ID. The purpose of the fields name and email is obvious. The profileImage field will contain the URL with the user’s avatar if one exists. The role field is the user’s role for this project, specified in the project settings. provider contains information about the user’s authentication service, for example, google if the user is registered with a Google account.

Getting the current user

When a user opens Sanity Studio, they must log in to it. Then all data operations are performed on behalf of the given user. Interaction with documents is provided by the Sanity Client module. Authentication of requests is achieved through an individual authorization token that the Sanity Client adds to the headers of each request. It all happens under the hood of Sanity Studio. Thus, any request made through the Sanity Client automatically occurs on behalf of the current user. You can get the Sanity Client access the following way:

Script 2. Import Sanity client

import client from 'part:@sanity/base/client';

To get information about the current user, we need to call the following request.

Script 3. Request current user

const resp = await client.request({ uri: 'users/me' });

As a result of such a request, we will receive an object of the following shape.

Script 3. User fields example

{
 id: 'pXXXXXX',
 name: 'Oleg Proskurin',
 email: '[email protected]',
 profileImage: 'https://lh3.googleusercontent.com/...',
 role: 'administrator',
 provider: 'google',
};

As you can see, we have received all the data we need and all that is left is to save them in the appropriate field of a document. To do this, we need to execute the request at the right time and transfer the data to the currently open document. There are several ways to implement this logic.

Utilizing initialValue API

The easiest way to request data during document creation is to add an initialValue property to our content-type definition. Below I put the complete source code with the type, which can be used in your projects.

Script 4. Content-type with an initial value

import client from 'part:@sanity/base/client';

export const user = {
 name: 'user',
 title: 'Authenticated User',
 type: 'object',
 fields: [
   {
     name: 'id',
     type: 'string',
     readOnly: true,
     hidden: true,
   },
   {
     name: 'name',
     type: 'string',
     readOnly: true,
   },
   {
     name: 'email',
     type: 'string',
     readOnly: true,
   },
   {
     name: 'profileImage',
     readOnly: true,
     hidden: true,
     type: 'string',
   },
   {
     name: 'role',
     type: 'string',
     readOnly: true,
     hidden: true,
   },
   {
     name: 'provider',
     type: 'string',
     readOnly: true,
     hidden: true,
   },
 ],
 initialValue: () => client.request({ uri: 'users/me' }),
};

In order to start using this, you just need to add the createdBy field of the user type to the document’s schema.

Script 5. Adding createdBy field

const Article = {
 name: 'Article',
 type: 'document',
 fields: [
   {
     name: 'createdBy',
     title: 'Created By',
     type: 'user',
   },

   /* ...other document fields  */
 ],
};

As soon as the user creates a new document of this type, the function specified in the initialValue of this type will make an asynchronous request and return a result with the user’s data. Thus, the document will be pre-filled with the value we need. Note that I have added read-only properties to all fields of type user. This is necessary because we do not want the user to manually change this data. Also, using the hidden property, we can display only human-readable information in the Sanity Studio editor.

Custom input component

More control and possibilities can be gained by creating your own component to display user data. A component can provide a user interface for interacting with data. Let’s illustrate this by creating an assignedTo field that allows any CMS user to assign themself to whatever is described in the document. In order to set a React Component as a field input to our content type, let’s add an inputComponent property to it and create a React component to use for this, respectively.

Script 6. Adding custom React Component

inputComponent: User,

The minimal implementation of a component might look like this:

Script 7. Custom input implementation

const User = ({ value, type }) => {
 const { profileImage, name, email } = value;
 const { title } = type;

 return (
   <div>
     <div className="title">{title}</div>
     <img src={profileImage} />
     <div>{`${name} / ${email}`}</div>
   </div>
 );
};

Please note that here I deliberately provide only the minimum necessary component implementation. In reality, what you want to do is add styling, checks, and validation. However, we already have a component that shows us the avatar of the user who created the document, their name, and their email. Let’s add the ability for other users to assign themselves to this document. To do this, add a “Join” button and its handler.

Script 8. Adding a button handler

const User = ({ value, type }) => {
 const { profileImage, name, email } = value;
 const { title } = type;

 const handleClick = () => {
// todo: switch user to the current one
}
 return (
   <div>
     <div className="title">{title}</div>
     <img src={profileImage} />
     <div>{`${name} / ${email}`}</div>
     <button onClick={handleClick}>Join</button>
   </div>
 );
};

To implement handleClick, let’s use helpful utilities from the Sanity form-builder package.

Script 9. Import patching utils

import PatchEvent, { set } from '@sanity/form-builder/PatchEvent';

As a result, our component will look like this.

Script 10. Component patching the document

import PatchEvent, { set } from ‘@Qsanity/form-builder/PatchEvent' ;

const User = ({ value, type, onChange }) = {
const { profileImage, name, email } = value;
const { title } = type;

const handleClick = async () => {
const user = await client.request({ uri:'users/me' });
onChange(PatchEvent.from(set(user)));
};

return (
<div>
<div className="title">{title}</div>
<img src={profileImage} />
<div>{`${name} / ${email}`}</div>
<button onClick={handleClick}>Join</button>
</div>
);
};

Now, having added the assignedTo field of the user type to our document, we initially will have a user who created the document, but later other users will be able to re-assign themselves.

The complete example of the user content-type implementation

Script 10. Content-type with an initial value and a custom input component

import React from 'react';
import client from 'part:@sanity/base/client';
import PatchEvent, { set } from '@sanity/form-builder/PatchEvent';

const User = ({ value = {}, type, onChange }) => {
 const { profileImage, name, email } = value;
 const { title } = type;

 const handleClick = async () => {
   const user = await client.request({ uri: 'users/me' });
   onChange(PatchEvent.from(set(user)));
 };

 return (
   <div>
     <div className="title">{title}</div>
     <img src={profileImage} />
     <div>{`${name} / ${email}`}</div>
     <button onClick={handleClick}>Join</button>
   </div>
 );
};

export const user = {
 name: 'user',
 title: 'Authenticated User',
 type: 'object',
 fields: [
   {
     name: 'id',
     type: 'string',
     readOnly: true,
     hidden: true,
   },
   {
     name: 'name',
     type: 'string',
     readOnly: true,
   },
   {
     name: 'email',
     type: 'string',
     readOnly: true,
   },
   {
     name: 'profileImage',
     readOnly: true,
     hidden: true,
     type: 'string',
   },
   {
     name: 'role',
     type: 'string',
     readOnly: true,
     hidden: true,
   },
   {
     name: 'provider',
     type: 'string',
     readOnly: true,
     hidden: true,
   },
 ],
 initialValue: () => client.request({ uri: 'users/me' }),
 inputComponent: User,
};

Custom action

Another operation where user data can be involved is a custom action command. For example, it can be an approval of a document by an authorized person. The process of creating and connecting custom actions is described in the documentation and we will skip them, switching directly to the “Approve” action, which will be used by users with appropriate permissions, to approve documents. Suppose in our document there is an approved field, the type of which is user that we created above, and we have an agreement that an empty value of the field means that the document is not approved, and the field in which there is user data - that it is approved by this user. This is what a custom action that implements this operation might look like.

Script 11. Approve action function

import client from 'part:@sanity/base/client';

export const approveAction = (docInfo) => {
 const { id, approved } = docInfo.draft;

 // only for not approved documents
 if (approved) {
   return null;
 }

 const onHandle = async () => {
   const user = await client.request({ uri: 'users/me' });

   // only administrators can approve documents
   if (user.role !== 'administrator') {
     return;
   }
   await client.patch(id).set({ approved: user }).commit();
 };

 return {
   icon: () => '👍',
   label: 'Approve',
   onHandle,
 };
};

When you click on a button, we request user data and check their permissions. If the user is authorized to do this, we patch the document by adding details about the user who approved it. This action should be hidden for already approved documents - for this, it is enough to return null from the action function. Ideally, we would like to similarly hide this action from users who do not have the appropriate permissions. Unfortunately, this requires making an asynchronous request from the body of the function itself, which means that the approveAction function itself would have to be asynchronous, which is no longer supported by Sanity Studio.

Thank you for your time, I hope you learned something useful and interesting for you. More articles about headless CMS and Sanity, in particular, can be found on our blog

#Sanity#Headless CMS#Content management#customization

WRITTEN BY

Oleg Proskurin

Oleg Proskurin

@UsulPro