Last active
November 26, 2021 14:22
-
-
Save SimeonGriggs/decb322c0acdf4f10e723abd3643d2a2 to your computer and use it in GitHub Desktop.
inputComponent drop-in replacement for `sanity-plugin-tabs`
This file contains 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
/* 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