Skip to content

Instantly share code, notes, and snippets.

@SimeonGriggs
Last active November 26, 2021 14:22
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save SimeonGriggs/decb322c0acdf4f10e723abd3643d2a2 to your computer and use it in GitHub Desktop.
Save SimeonGriggs/decb322c0acdf4f10e723abd3643d2a2 to your computer and use it in GitHub Desktop.
inputComponent drop-in replacement for `sanity-plugin-tabs`
/* eslint-disable no-negated-condition */
/* eslint-disable react/prop-types */
/* eslint-disable react/display-name */
/* eslint-disable max-nested-callbacks */
/**
* inputComponent drop-in replacement for `sanity-plugin-tabs`
* Provided as-is with no explicit or implied future support
* Addresses some UI issues with that plugin, but may introduce others
*
* It is mostly just this code...
* https://www.sanity.io/docs/custom-input-widgets#40b31a232a03
* ...with some Sanity UI tabs and logic for validation markers
*/
import React, {useState, useEffect} from 'react'
import {FormBuilderInput} from '@sanity/form-builder/lib/FormBuilderInput'
import Fieldset from 'part:@sanity/components/fieldsets/default'
import {withDocument} from 'part:@sanity/form-builder'
import {ErrorOutlineIcon, WarningOutlineIcon} from '@sanity/icons'
import {Card, Stack, Tab, TabList} from '@sanity/ui'
import {setIfMissing} from '@sanity/form-builder/PatchEvent'
const MARKER_TONES = {
error: `critical`,
warning: `caution`,
}
const MARKER_ICONS = {
error: ErrorOutlineIcon,
warning: WarningOutlineIcon,
}
const Tabs = React.forwardRef((props, ref) => {
// destructure props for easier use
const {
compareValue,
focusPath,
markers,
onBlur,
onChange,
onFocus,
presence,
type,
value,
level,
} = props
const [tabs, setTabs] = useState(type.fieldsets.map(({name, title}) => ({name, title})))
const [activeTab, setActiveTab] = useState(tabs[0].name)
// Loop over markers and update tabs if they contain issues
// This could likely be done better and safer but for this POC it appears to work
useEffect(() => {
const resetTabs = tabs.map(({name, title}) => ({name, title}))
// Remove all tones and icons
if (!markers.length) {
setTabs(resetTabs)
} else {
// Assume top-level of field path is where the fieldset is declared
markers.forEach((marker) => {
const tabNameOfMarker = type.fields.find((field) => field.name === marker.path[0]).fieldset
// Now we know what tab to target, add a status
if (tabNameOfMarker) {
const targetTab = resetTabs.findIndex((tab) => tab.name === tabNameOfMarker)
if (targetTab > -1) {
if (
// This marker is `warning`
marker.level === 'warning' &&
// But the existing tab is already `error`
resetTabs[targetTab]?.tone === MARKER_TONES?.error
) {
// Don't overwrite an "error" level with a "warning" level
} else {
resetTabs[targetTab] = {
...resetTabs[targetTab],
tone: MARKER_TONES?.[marker.level],
icon: MARKER_ICONS?.[marker.level],
}
}
}
}
})
setTabs(resetTabs)
}
}, [markers])
const handleFieldChange = React.useCallback(
(field, fieldPatchEvent) => {
// fieldPatchEvent is an array of patches
// Patches look like this:
/*
{
type: "set|unset|setIfMissing",
path: ["fieldName"], // An array of fields
value: "Some value" // a value to change to
}
*/
onChange(fieldPatchEvent.prefixAll(field.name).prepend(setIfMissing({_type: type.name})))
},
[onChange]
)
// Get an array of field names for use in a few instances in the code
const fieldNames = type.fields.map((f) => f.name)
// If Presence exist, get the presence as an array for the children of this field
const childPresence =
presence.length === 0 ? presence : presence.filter((item) => fieldNames.includes(item.path[0]))
// If Markers exist, get the markers as an array for the children of this field
const childMarkers =
markers.length === 0 ? markers : markers.filter((item) => fieldNames.includes(item.path[0]))
return (
<Stack space={3}>
{tabs.length > 0 ? (
<Card padding={1} radius={3} shadow={1} tone="inherit">
<TabList space={1}>
{tabs.map((tab) => (
<Tab
tone={tab?.tone ?? null}
icon={tab?.icon ?? null}
key={tab.name}
label={tab.title}
onClick={() => setActiveTab(tab.name)}
radius={2}
selected={tab.name === activeTab}
/>
))}
</TabList>
</Card>
) : null}
<Fieldset
// Deliberately removed title so it's not repeated
// legend={type.title} // schema title
description={type.description} // schema description
markers={childMarkers} // markers built above
presence={childPresence} // presence built above
>
{type.fields
.filter((field) => (activeTab ? field.fieldset === activeTab : true))
.map((field, i) => {
return (
<FormBuilderInput
level={level + 1}
ref={i === 0 ? ref : null}
key={field.name}
type={field.type}
value={value && value[field.name]}
onChange={(patchEvent) => handleFieldChange(field, patchEvent)}
path={[field.name]}
markers={markers}
focusPath={focusPath}
readOnly={field.readOnly}
presence={presence}
onFocus={onFocus}
onBlur={onBlur}
compareValue={compareValue}
/>
)
})}
</Fieldset>
</Stack>
)
})
export default withDocument(Tabs)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment