Skip to content

Instantly share code, notes, and snippets.

@zanedev
Last active April 20, 2020 10:15
Show Gist options
  • Save zanedev/bdf13642c6611c558cd7759780288342 to your computer and use it in GitHub Desktop.
Save zanedev/bdf13642c6611c558cd7759780288342 to your computer and use it in GitHub Desktop.
Sanity Conditional Field Component
import PropTypes from 'prop-types';
import React from 'react';
import { setIfMissing } from 'part:@sanity/form-builder/patch-event';
import { FormBuilderInput, withDocument, withValuePath } from 'part:@sanity/form-builder';
import fieldStyle from '@sanity/form-builder/lib/inputs/ObjectInput/styles/Field.css';
import { isAdminUser } from '../../lib/user';
import { take } from 'rxjs/operators';
const isFunction = (obj) => !!(obj && obj.constructor && obj.call && obj.apply);
/**
* This component will render a field based on a condition passed in from the schema.
* It also does an isAdmin check so we can selectively render fields based on role.
*
* This is a workaround for not being able to hide or mark fields read only or hidden in sanity.
* See https://github.com/sanity-io/sanity/issues/1224
*
* Optionally you can hide it based on other conditions in the options array on a schema
*
* Adopted from https://gist.github.com/michaeland/f41aef54d46588fff27651cd0d35212f
*
* The optional condition comes from a field in the document schema like:
* {
* name: 'objectTitle',
* title: 'object Title'
* type: 'object',
* options: {
* condition: (document: obj, context: func) => {readOnly, hidden}
* }
* fields : []
* }
*
*/
class ConditionalFields extends React.PureComponent {
static propTypes = {
type: PropTypes.shape({
title: PropTypes.string,
name: PropTypes.string.isRequired,
fields: PropTypes.array.isRequired,
options: PropTypes.shape({
condition: PropTypes.func.isRequired
}).isRequired
}).isRequired,
level: PropTypes.number,
value: PropTypes.shape({
_type: PropTypes.string
}),
onFocus: PropTypes.func.isRequired,
onChange: PropTypes.func.isRequired,
onBlur: PropTypes.func.isRequired
};
firstFieldInput = React.createRef();
focus() {
this.firstFieldInput.current && this.firstFieldInput.current.focus();
}
getContext(level = 1) {
// gets value path from withValuePath HOC, and applies path to document
// we remove the last 𝑥 elements from the valuePath
const valuePath = this.props.getValuePath();
const removeItems = -Math.abs(level);
return (valuePath.length + removeItems <= 0)
? this.props.document
: valuePath
.slice(0, removeItems)
.reduce((context, current) => {
// basic string path
if (typeof current === 'string') {
return context[current] || {};
}
// object path with key used on arrays
if (
typeof current === 'object' &&
Array.isArray(context) &&
current._key
) {
return context.filter(item => item._key && item._key === current._key)[0] || {};
}
}, this.props.document);
}
handleFieldChange = (field, fieldPatchEvent) => {
// Whenever the field input emits a patch event, we need to make sure to each of the included patches
// are prefixed with its field name, e.g. going from:
// {path: [], set: <nextvalue>} to {path: [<fieldName>], set: <nextValue>}
// and ensure this input's value exists
const { onChange, type } = this.props;
const event = fieldPatchEvent
.prefixAll(field.name)
.prepend(setIfMissing({ _type: type.name }));
onChange(event);
};
render() {
// console.log('ConditionalFields props: ', this.props);
// get admin user value from user stream
// must be done in render fn
let isAdmin = false;
// eslint-disable-next-line no-return-assign
isAdminUser().pipe(take(1)).subscribe(value => isAdmin = value);
// console.log('conditional field component isAdmin: ', isAdmin);
const { document, type, value, level, onFocus, onBlur } = this.props;
const condition = (isFunction(type.options.condition) && type.options.condition) || function() {
return {
hidden: false,
readOnly: false,
};
};
const showFields = condition({ document, isAdmin, options: type.options });
if (showFields.hidden) {
return null;
}
return <>
{type.fields ? type.fields
.map((field, i) => (
// Delegate to the generic FormBuilderInput. It will resolve and insert the actual input component
// for the given field type
// todo: why does FormBuilderInput not render the slug with the generate button?
<div className={fieldStyle.root} key={i}>
<FormBuilderInput
level={level + 1}
ref={i === 0 ? this.firstFieldInput : null}
key={field.name}
type={field.type}
value={value && value[field.name]}
onChange={patchEvent => this.handleFieldChange(field, patchEvent)}
path={[field.name]}
onFocus={onFocus}
onBlur={onBlur}
readOnly={showFields.readOnly}
/>
</div>
)) : null}
</>;
}
}
export default withValuePath(withDocument(ConditionalFields));
import ConditionalFields from './ConditionalFields.js'
import { isAdminUser } from './user';
/**
* Helper callback to check if admin from conditional custom field args
* @param {Object} document Sanity doc/item being edited
* @param {Boolean} isAdmin Actual value for is admin detected at field render time
* @param {Array} options Field options passed in
* @returns {Object} {readOnly, hidden}
*/
function adminConditionalDisplay({ document, isAdmin, options }) {
// console.log('document: ', document);
// console.log('isAdmin: ', isAdmin);
// console.log('options: ', options);
// if not an admin, check for non admin read only / hidden settings
return !isAdmin ? {
readOnly: options.nonAdminReadOnly ? options.nonAdminReadOnly : false,
hidden: options.nonAdminHidden ? options.nonAdminHidden : false,
} : {
// else fallback to regular readonly and hidden settings regardless of admin or not
readOnly: options.readOnly ? options.readOnly : false,
hidden: options.hidden ? options.hidden : false,
};
}
export default {
title: 'Link',
name: 'link',
type: 'object',
fields: [
{
title: 'Link type',
name: 'linkType',
type: 'string',
options: {
list: [
{ title: 'Internal', value: 'internal'},
{ title: 'External', value: 'external'}
],
layout: 'radio',
direction: 'horizontal'
}
},
{
name : 'internal',
type : 'object',
inputComponent : ConditionalFields,
fields : [
{
title: 'Page',
name: 'reference',
type: 'reference',
to: [{ type: 'movie' }]
}
],
options : {
readOnly: false,
hidden: false,
nonAdminReadOnly: false,
nonAdminHidden: true, // we dont want staff seeing this field
condition: adminConditionalDisplay,
},
inputComponent: ConditionalFieldComponent,
}
]
}
import userStore from 'part:@sanity/base/user';
import { map } from 'rxjs/operators';
export function isAdminUser() {
return userStore.currentUser.pipe(
map(({ user }) => {
const { role } = user;
return role === 'administrator';
})
);
}
@zanedev
Copy link
Author

zanedev commented Apr 12, 2020

This is a variant of https://gist.github.com/michaeland/f41aef54d46588fff27651cd0d35212f that allows passing a condition but also readonly or hidden and ability to toggle on hidden or not

@coxmi
Copy link

coxmi commented Apr 20, 2020

Nice work, and glad I could help!

I noticed in https://github.com/bjornwang/sanity-conditional-fields/blob/master/plugins/conditionalField.js#L54-L55 that sanity already provides a PathUtils.get(document, parentPath), so all of the stuff in getContext is probably a little overkill.

This should also work in various contexts too, like within a blockContent annotation (in a modal window).
A lot of the other methods I tried didn't seem to do the trick.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment