Skip to content

Commit

Permalink
[field] Add "array of options" field diff
Browse files Browse the repository at this point in the history
  • Loading branch information
rexxars committed Oct 6, 2020
1 parent b5fd8bc commit 748ac7b
Show file tree
Hide file tree
Showing 6 changed files with 230 additions and 0 deletions.
5 changes: 5 additions & 0 deletions packages/@sanity/field/src/diff/resolve/diffResolver.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import {ArrayOfOptionsFieldDiff} from '../../types/array/diff'
import {DatetimeFieldDiff} from '../../types/datetime/diff'
import {UrlFieldDiff} from '../../types/url/diff'
import {SlugFieldDiff} from '../../types/slug/diff'
Expand All @@ -17,5 +18,9 @@ export const diffResolver: DiffComponentResolver = function diffResolver({schema
return SlugFieldDiff
}

if (schemaType.jsonType === 'array' && Array.isArray(schemaType.options?.list)) {
return ArrayOfOptionsFieldDiff
}

return undefined
}
1 change: 1 addition & 0 deletions packages/@sanity/field/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/ban-types */
import {
Path,
Block,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
@import 'part:@sanity/base/theme/variables-style';

.item {
display: flex;
align-items: center;
}

.label {
display: flex;
align-items: center;
}

.itemPreview {
margin-left: var(--small-padding);
line-height: 1;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
import Preview from 'part:@sanity/base/preview'
import {useUserColorManager} from '@sanity/base/user-color'
import {isKeyedObject, TypedObject} from '@sanity/types'
import React from 'react'
import {
Annotation,
ArrayDiff,
ArraySchemaType,
Diff,
DiffComponent,
FromToArrow,
getAnnotationColor,
ItemDiff,
SchemaType
} from '../../../diff'
import {Checkbox} from '../../boolean/preview'
import {isEqual} from '../util/arrayUtils'
import styles from './ArrayOfOptionsFieldDiff.css'

interface NamedListOption {
title?: string
value: unknown
}

interface NormalizedListOption {
title?: string
value: unknown
memberType?: Exclude<SchemaType, ArraySchemaType>
isPresent: boolean
annotation: Annotation
itemIndex: number
}

export const ArrayOfOptionsFieldDiff: DiffComponent<ArrayDiff> = ({diff, schemaType}) => {
const options = schemaType.options?.list
const colorManager = useUserColorManager()
if (!Array.isArray(options)) {
// Shouldn't happen, because the resolver should only resolve here if it does
return null
}

return (
<div>
{diff.items
.map(item => normalizeItems(item, diff, schemaType))
.filter((item): item is NormalizedListOption => item !== null)
.sort(sortItems)
.map((item, index) => {
const {annotation, isPresent, value, memberType, title} = item
const color = getAnnotationColor(colorManager, annotation)
return (
<div className={styles.item} key={getItemKey(diff, index)}>
<Checkbox checked={!isPresent} color={color} />
<FromToArrow />
<div className={styles.label}>
<Checkbox checked={isPresent} color={color} />
<ItemPreview value={title || value} memberType={memberType} />
</div>
</div>
)
})}
</div>
)
}

function normalizeItems(
item: ItemDiff,
parentDiff: ArrayDiff,
schemaType: ArraySchemaType
): NormalizedListOption | null {
if (item.diff.action === 'unchanged') {
return null
}

const {fromValue, toValue} = parentDiff
const value = getValue(item.diff)
const wasPresent = isInArray(value, fromValue)
const isPresent = isInArray(value, toValue)
if (wasPresent === isPresent) {
return null
}

return {
title: getItemTitle(value, schemaType),
memberType: resolveMemberType(getValue(item.diff), schemaType),
itemIndex: getOptionIndex(value, schemaType),
annotation: item.annotation,
isPresent,
value
}
}

function sortItems(itemA: NormalizedListOption, itemB: NormalizedListOption): number {
return itemA.itemIndex - itemB.itemIndex
}

function ItemPreview({value, memberType}: {memberType?: SchemaType; value: unknown}) {
return (
<div className={styles.itemPreview}>
{typeof value === 'string' || typeof value === 'number' ? (
value
) : (
<Preview type={memberType} value={value} layout="default" />
)}
</div>
)
}

function isInArray(value: unknown, parent?: unknown[] | null): boolean {
const array = parent || []
return typeof value === 'object' && value !== null
? array.some(item => isEqual(item, value))
: array.includes(value)
}

function getItemKey(diff: Diff, index: number): string | number {
const value = diff.toValue || diff.fromValue
return isKeyedObject(value) ? value._key : index
}

function getValue(diff: Diff) {
return typeof diff.toValue === 'undefined' ? diff.fromValue : diff.toValue
}

function resolveMemberType(item: unknown, schemaType: ArraySchemaType) {
const itemTypeName = resolveTypeName(item)
return schemaType.of.find(memberType => memberType.name === itemTypeName)
}

function resolveTypeName(value: unknown): string {
const jsType = resolveJSType(value)
if (jsType !== 'object') {
return jsType
}

const obj = value as TypedObject
return ('_type' in obj && obj._type) || jsType
}

function resolveJSType(val: unknown) {
if (val === null) {
return 'null'
}

if (Array.isArray(val)) {
return 'array'
}

return typeof val
}

function isNamedOption(item: unknown | NamedListOption): item is NamedListOption {
return typeof item === 'object' && item !== null && 'title' in item
}

function getOptionIndex(item: unknown, schemaType: ArraySchemaType): number {
const list = schemaType.options?.list || []
return list.findIndex(opt => isEqual(isNamedOption(opt) ? opt.value : opt, item))
}

function getItemTitle(item: unknown, schemaType: ArraySchemaType): string | undefined {
const list = (schemaType.options?.list || []) as NamedListOption[]
const index = getOptionIndex(item, schemaType)
return index === -1 ? undefined : list[index].title || undefined
}
1 change: 1 addition & 0 deletions packages/@sanity/field/src/types/array/diff/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './ArrayOfOptionsFieldDiff'
42 changes: 42 additions & 0 deletions packages/@sanity/field/src/types/array/util/arrayUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import {isKeyedObject} from '@sanity/types'

export function isEqual(item: unknown, otherItem: unknown): boolean {
if (item === otherItem) {
return true
}

if (typeof item !== typeof otherItem) {
return false
}

if (typeof item !== 'object' && !Array.isArray(item)) {
return item === otherItem
}

if (isKeyedObject(item) && isKeyedObject(otherItem) && item._key === otherItem._key) {
return true
}

if (Array.isArray(item) && Array.isArray(otherItem)) {
if (item.length !== otherItem.length) {
return false
}

return item.every((child, i) => isEqual(child, otherItem[i]))
}

if (item === null || otherItem === null) {
return item === otherItem
}

const obj = item as Record<string, unknown>
const otherObj = otherItem as Record<string, unknown>

const keys = Object.keys(obj)
const otherKeys = Object.keys(otherObj)
if (keys.length !== otherKeys.length) {
return false
}

return keys.every(keyName => isEqual(item[keyName], otherObj[keyName]))
}

0 comments on commit 748ac7b

Please sign in to comment.