Using the Sanity studio client to update documents in custom components

8 replies
Last updated: Jan 25, 2022
I have created a custom component, which takes complex array as a value, schema is defined like this:
    {
      name: 'sections',
      title: 'Sections',
      type: 'array',
      inputComponent: SectionItemList,
      of: [
        {
          type: DocumentTypes.workoutgroup,
          weak: true,
        },
      ],
    },
And type workoutgroup schema is:

export const workoutGroup = createDocumentSchema({
  name: DocumentTypes.workoutgroup,
  type: 'object',

  fields: [
    {
      name: 'label',
      title: 'Workout group name',
      type: 'string',
      validation: (Rule: any) => Rule.required(),
    },
    {
      name: 'startTime',
      title: 'Start time',
      type: 'string',
    },
    {
      name: 'exercises',
      title: 'Excercises',
      type: 'array',
      of: [
        {
          type: DocumentTypes.workoutgroupexercise,
          weak: true,
        },
      ],
    },
  ],
});
From within the custom component which is client side js code, I need to make queries to update
label
,
startTime
create new
DocumentTypes.workoutgroup
update
exercises
. I used
sanityClient
and made queries like this:
      await client
        .patch(document._id)
        .set({ [`sections[_key=="${key}"].label`]: newValue.toString() })
        .commit();
However when client is initialised it requires auth token, which has write permissions (admin) and I shouldn’t be keeping the auth token in my JS bundle for security reasons.

I have tried using PatchEvent to make these queries, but it doesn’t seem to work.

My current idea is to move client queries to separate node proxy server and do it this way: FE -> node proxy service (which has auth token set, privately) -> Sanity

Does that feel like the best approach?
Jan 25, 2022, 4:54 PM
since this is within the studio, you can use the studio's client instead:

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

const client = sanityClient.withConfig({apiVersion: '2022-01-26'})
You won't have to expose your key this way
Jan 25, 2022, 5:01 PM
My custom component is a huge component, with many sub components nested, but the core looks like this:
/* eslint-disable no-console */
import { TrashIcon, CopyIcon } from '@sanity/icons';
import {
  Stack, Label, Card, Flex, Inline, Button, useToast, Tooltip, Box, Text,
} from '@sanity/ui';
import debounce from 'lodash.debounce';
import { nanoid } from 'nanoid';
import { withDocument } from 'part:@sanity/form-builder';
import React, {
  FC, useRef, useCallback, useState,
} from 'react';

import { IWorkoutSectionItem, IDocument } from '../../interfaces/common';
import { DocumentTypes } from '../../schemas/common/baseDocument';
import { client } from '../../utils/browserClient';
import DeleteConfirmationModal from './DeleteConfirmationModal';
import WorkoutSectionExercises from './ExerciseItemList';
import WorkoutSectionLabelInput from './LabelInput';
import WorkoutSectionsTimeInput from './StartTimeInput';

interface IWorkoutSectionItemProps extends IWorkoutSectionItem {
  document: IDocument;
}

const DEBOUNCE_TIME = 500;

const WorkoutSectionItem: FC<IWorkoutSectionItemProps> = ({
  label,
  startTime,
  exercises,
  _key: key,
  document,
}) => {
  const [ confirmationIsOpen, setConfirmationIsOpen ] = useState(false);
  const onClose = useCallback(() => setConfirmationIsOpen(false), []);
  const onOpen = useCallback(() => setConfirmationIsOpen(true), []);

  const startDateRef = useRef();
  const labelRef = useRef();
  const toast = useToast();

  const onLabelChange = useCallback(debounce(async (newValue) => {
    try {
      await client
        .patch(document._id)
        .set({ [`sections[_key=="${key}"].label`]: newValue.toString() })
        .commit();

      toast.push({
        status: 'success',
        title: 'Successfully updated workout group title',
      });
    } catch (e) {
      toast.push({
        status: 'error',
        title: 'Failed to update workout group title',
      });
      console.log(e);
    }
  }, DEBOUNCE_TIME), [ key ]);

  const onStartTimeChange = useCallback(debounce(async (newValue) => {
    try {
      await client
        .patch(document._id)
        .set({ [`sections[_key=="${key}"].startTime`]: newValue.toString() })
        .commit();

      toast.push({
        status: 'success',
        title: 'Successfully updated workout group start time',
      });
    } catch (e) {
      toast.push({
        status: 'error',
        title: 'Failed to update workout group start time',
      });
      console.log(e);
    }
  }, DEBOUNCE_TIME), [ key ]);

  const deleteWorkoutGroup = useCallback(async () => {
    try {
      await client
        .patch(document._id)
        .unset([ `sections[_key=="${key}"]` ])
        .commit();

      toast.push({
        status: 'success',
        title: `Successfully removed "${label}"`,
      });
    } catch (e) {
      toast.push({
        status: 'error',
        title: 'Failed to remove workout group',
      });
      console.log(e);
    }

    onClose();
  }, [ key, label, onClose ]);

  const cloneWorkoutGroup = useCallback(async () => {
    const clonedWorkoutGroup = {
      _type: DocumentTypes.workoutgroup,
      _key: nanoid(),
      label: `${label} (copy)`,
      startTime,
      exercises,
    };

    try {
      await client
        .patch(document._id)
        .prepend('sections', [ clonedWorkoutGroup ])
        .commit();

      toast.push({
        status: 'success',
        title: `Successfully cloned "${label}"`,
      });
    } catch (e) {
      toast.push({
        status: 'error',
        title: 'Failed to clone workout group',
      });
      console.log(e);
    }
  }, [ exercises, label, startTime ]);

  return (
    <React.Fragment>
      <Card padding={[ 0, 0, 5, 5 ]} style={{ borderBottom: '1px dashed black' }} shadow={0}>
        <Flex>
          <Card flex={1} marginRight={4}>
            <WorkoutSectionLabelInput
              ref={labelRef.current}
              value={label}
              label="Workout group title"
              onChange={onLabelChange}
            />
          </Card>
          <Card marginRight={4}>
            <WorkoutSectionsTimeInput
              ref={startDateRef.current}
              value={startTime}
              onChange={onStartTimeChange}
              label="Start time"
            />
          </Card>
          <Card>
            <Stack space={2}>
              <Label>Actions</Label>
              <Inline space={[ 0, 0, 2 ]}>
                <Tooltip
                  content={(
                    <Box padding={2}>
                      <Text muted size={1}>
                        Delete workout group
                      </Text>
                    </Box>
              )}
                  fallbackPlacements={[ 'right', 'left' ]}
                  placement="top"
                  portal
                >
                  <Button
                    fontSize={0}
                    icon={TrashIcon}
                    tone="primary"
                    onClick={onOpen}
                    style={{ cursor: 'pointer' }}
                  />
                </Tooltip>
                <Tooltip
                  content={(
                    <Box padding={2}>
                      <Text muted size={1}>
                        Clone workout group
                      </Text>
                    </Box>
              )}
                  fallbackPlacements={[ 'right', 'left' ]}
                  placement="top"
                  portal
                >
                  <Button
                    fontSize={0}
                    icon={CopyIcon}
                    tone="secondary"
                    onClick={cloneWorkoutGroup}
                    style={{ cursor: 'pointer' }}
                  />
                </Tooltip>
              </Inline>
            </Stack>
          </Card>
        </Flex>
        <WorkoutSectionExercises exercises={exercises} workoutGroupKey={key} />
      </Card>
      <DeleteConfirmationModal
        isOpen={confirmationIsOpen}
        onClose={onClose}
        title={`Do you really want to delete workout group "${label}"?`}
        deleteCallback={deleteWorkoutGroup}
      />
    </React.Fragment>
  );
};

export default withDocument(WorkoutSectionItem);
Jan 25, 2022, 5:12 PM
Looks like
import sanityClient from 'part:@sanity/base/client'
Does the job!
Jan 25, 2022, 5:14 PM
Doing some further tests.
Jan 25, 2022, 5:14 PM
Just to confirm it uses sanity studio users loggin token and then it all depends on users permissions?
Jan 25, 2022, 5:14 PM
Correct.
Jan 25, 2022, 5:14 PM
Cool. Thank you guys!
Jan 25, 2022, 5:15 PM
Glad you got it sorted out!
Jan 25, 2022, 5:15 PM

Sanity– build remarkable experiences at scale

Sanity is a modern headless CMS that treats content as data to power your digital business. Free to get started, and pay-as-you-go on all plans.

Was this answer helpful?