Skip to content

Commit

Permalink
[form-builder] Show warning for array items without _type (#2020)
Browse files Browse the repository at this point in the history
  • Loading branch information
rexxars committed Sep 22, 2020
1 parent a171a4b commit 78d88e8
Show file tree
Hide file tree
Showing 5 changed files with 153 additions and 45 deletions.
49 changes: 20 additions & 29 deletions packages/@sanity/form-builder/src/inputs/ArrayInput/InvalidItem.tsx
Original file line number Diff line number Diff line change
@@ -1,33 +1,24 @@
import React from 'react'
import {resolveTypeName} from '../../utils/resolveTypeName'
import InvalidValue from '../InvalidValueInput'
import PatchEvent from '../../PatchEvent'
type Type = {
name: string
of: Array<Type>
import {resolveTypeName} from '../../utils/resolveTypeName'
import InvalidValueInput from '../InvalidValueInput'
import {ArrayType, ItemValue} from './typedefs'

interface Props {
type: ArrayType
value: unknown
onChange: (event: PatchEvent, valueOverride?: ItemValue) => void
}
export default class Item extends React.PureComponent<{}, {}> {
props: {
// note: type here is the *array* type
type: Type
value: any
onChange: (arg0: PatchEvent, value: any) => void
}
handleChange = (event: PatchEvent) => {
const {onChange, value} = this.props
onChange(event, value)
}
render() {
const {type, value} = this.props
const actualType = resolveTypeName(value)
const validTypes = type.of.map(ofType => ofType.name)
return (
<InvalidValue
actualType={actualType}
validTypes={validTypes}
onChange={this.handleChange}
value={value}
/>
)
}

export default function InvalidItem({value, type, onChange}: Props) {
const actualType = resolveTypeName(value)
const validTypes = type.of.map(ofType => ofType.name)
return (
<InvalidValueInput
actualType={actualType}
validTypes={validTypes}
onChange={onChange}
value={value}
/>
)
}
17 changes: 10 additions & 7 deletions packages/@sanity/form-builder/src/inputs/ArrayInput/ItemValue.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {Presence, Marker, Type} from '../../typedefs'
import ConfirmButton from './ConfirmButton'
import styles from './styles/ItemValue.css'
import {ArrayType, ItemValue} from './typedefs'
import InvalidItem from './InvalidItem'

const DragHandle = createDragHandle(() => (
<span className={styles.dragHandle}>
Expand Down Expand Up @@ -113,15 +114,16 @@ export default class RenderItemValue extends React.PureComponent<Props> {
const {onRemove, value} = this.props
onRemove(value)
}
handleChange = (event: PatchEvent) => {
handleChange = (event: PatchEvent, valueOverride?: ItemValue) => {
const {onChange, value} = this.props
onChange(event, value)
onChange(event, typeof valueOverride === 'undefined' ? value : valueOverride)
}
getMemberType(): Type | null {
const {value, type} = this.props
const itemTypeName = resolveTypeName(value)
const memberType = type.of.find(memberType => memberType.name === itemTypeName)
return memberType
return itemTypeName === 'object' && type.of.length === 1
? type.of[0]
: type.of.find(memberType => memberType.name === itemTypeName)
}
getTitle(): string {
const {readOnly} = this.props
Expand Down Expand Up @@ -160,7 +162,7 @@ export default class RenderItemValue extends React.PureComponent<Props> {
const options = type.options || {}
const memberType = this.getMemberType()
const childMarkers = markers.filter(marker => marker.path.length > 1)
const childPresence = presence.filter(presence => presence.path.length > 1)
const childPresence = presence.filter(child => child.path.length > 1)
const content = (
<FormBuilderInput
type={memberType}
Expand Down Expand Up @@ -238,7 +240,7 @@ export default class RenderItemValue extends React.PureComponent<Props> {
)
}
renderItem() {
const {value, markers, type, readOnly, presence, focusPath} = this.props
const {value, markers, type, readOnly, presence, focusPath, onChange} = this.props
const options = type.options || {}
const isGrid = options.layout === 'grid'
const isSortable = !readOnly && !type.readOnly && options.sortable !== false
Expand All @@ -259,8 +261,9 @@ export default class RenderItemValue extends React.PureComponent<Props> {
const hasItemFocus = PathUtils.isExpanded(pathSegmentFrom(value), focusPath)
const memberType = this.getMemberType()
if (!memberType) {
return null
return <InvalidItem onChange={this.handleChange} type={type} value={value} />
}

return (
<div className={styles.inner}>
{!isGrid && isSortable && <DragHandle />}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,16 @@ import React from 'react'
import DefaultButton from 'part:@sanity/components/buttons/default'
import styles from '../ObjectInput/styles/UnknownFields.css'
import PatchEvent, {set, unset} from '../../PatchEvent'
import {ItemValue} from '../ArrayInput/typedefs'
import Warning from '../Warning'
import CONVERTERS from './converters'
import {UntypedValueInput} from './UntypedValueInput'

function getConverters(value, actualType, validTypes) {
if (!(actualType in CONVERTERS)) {
return []
}

return Object.keys(CONVERTERS[actualType])
.filter(targetType => validTypes.includes(targetType))
.map(targetType => ({
Expand All @@ -18,15 +21,14 @@ function getConverters(value, actualType, validTypes) {
}))
.filter(converter => converter.test(value))
}

type InvalidValueProps = {
actualType?: string
validTypes?: any[]
value?: any
onChange?: (...args: any[]) => any
validTypes?: string[]
value?: unknown
onChange?: (event: PatchEvent, valueOverride?: ItemValue) => void
}
export default class InvalidValue extends React.PureComponent<InvalidValueProps, {}> {
element: any

export default class InvalidValueInput extends React.PureComponent<InvalidValueProps, {}> {
handleClearClick = () => {
this.props.onChange(PatchEvent.from(unset()))
}
Expand All @@ -46,6 +48,7 @@ export default class InvalidValue extends React.PureComponent<InvalidValueProps,
</div>
)
}

return (
<div>
Only the following types are valid here according to schema:{' '}
Expand All @@ -57,8 +60,20 @@ export default class InvalidValue extends React.PureComponent<InvalidValueProps,
</div>
)
}

render() {
const {value, actualType, validTypes} = this.props
const {value, actualType, validTypes, onChange} = this.props

if (typeof value === 'object' && value !== null && !('_type' in value)) {
return (
<UntypedValueInput
value={value as Record<string, unknown>}
validTypes={validTypes}
onChange={onChange}
/>
)
}

const converters = getConverters(value, actualType, validTypes)
const message = (
<>
Expand All @@ -81,6 +96,7 @@ export default class InvalidValue extends React.PureComponent<InvalidValueProps,
</div>
</>
)

return <Warning heading="Content has invalid type" message={message} />
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import React from 'react'
import schema from 'part:@sanity/base/schema'
import DefaultButton from 'part:@sanity/components/buttons/default'
import PatchEvent, {setIfMissing, unset} from '../../PatchEvent'
import styles from '../ObjectInput/styles/UnknownFields.css'
import Warning from '../Warning'

type Props = {
validTypes?: string[]
value?: Record<string, unknown>
onChange?: (event: PatchEvent, value?: Record<string, unknown>) => void
}

function SetMissingTypeButton({
value,
targetType,
onChange
}: {
value: Record<string, unknown>
targetType: string
onChange: Props['onChange']
}) {
const itemValue = {...value, _type: targetType}
return (
<DefaultButton
onClick={() => onChange(PatchEvent.from(setIfMissing(targetType, ['_type'])), itemValue)}
color="primary"
>
Set <code>_type</code> to <code>{targetType}</code>
</DefaultButton>
)
}

function UnsetItemButton({
value,
onChange,
validTypes
}: {
value: Record<string, unknown>
validTypes: string[]
onChange: Props['onChange']
}) {
// Doesn't matter which `_type` we use as long as it's allowed by the array
const itemValue = {...value, _type: validTypes[0]}
return (
<DefaultButton onClick={() => onChange(PatchEvent.from(unset()), itemValue)} color="danger">
Remove value
</DefaultButton>
)
}

/**
* When the value does not have an `_type` property,
* but the schema has a named type
*/
export function UntypedValueInput({validTypes, value, onChange}: Props) {
const isSingleValidType = validTypes.length === 1
const isHoistedType = schema.has(validTypes[0])
const fix = isSingleValidType ? (
<SetMissingTypeButton onChange={onChange} targetType={validTypes[0]} value={value} />
) : null

const message = (
<>
Encountered an object value without a <code>_type</code> property.
{isSingleValidType && !isHoistedType && (
<div>
Either remove the <code>name</code> property of the object declaration, or set{' '}
<code>_type</code> property on items.
</div>
)}
{!isSingleValidType && (
<div>
The following types are valid here according to schema:{' '}
<ul>
{validTypes.map(validType => (
<li key={validType}>
<code>{validType}</code>
</li>
))}
</ul>
</div>
)}
<h4>object</h4>
<pre className={styles.inspectValue}>{JSON.stringify(value, null, 2)}</pre>
{fix}
{fix && ' '}
<UnsetItemButton onChange={onChange} validTypes={validTypes} value={value} />
</>
)

return <Warning heading="Content is missing _type" message={message} />
}
9 changes: 7 additions & 2 deletions packages/@sanity/form-builder/src/utils/resolveTypeName.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import {resolveJSType} from './resolveJSType'

export function resolveTypeName(value) {
export function resolveTypeName(value: unknown): string {
const jsType = resolveJSType(value)
return (jsType === 'object' && '_type' in value && value._type) || jsType
if (jsType !== 'object') {
return jsType
}

const obj = value as Record<string, unknown> & {_type?: string}
return ('_type' in obj && obj._type) || jsType
}

1 comment on commit 78d88e8

@vercel
Copy link

@vercel vercel bot commented on 78d88e8 Sep 22, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.