Last active
April 20, 2020 10:15
-
-
Save zanedev/bdf13642c6611c558cd7759780288342 to your computer and use it in GitHub Desktop.
Sanity Conditional Field Component
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | |
} | |
] | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'; | |
}) | |
); | |
} |
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
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