-
Notifications
You must be signed in to change notification settings - Fork 394
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[field] Add "array of options" field diff
- Loading branch information
Showing
6 changed files
with
230 additions
and
0 deletions.
There are no files selected for viewing
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
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,4 @@ | ||
/* eslint-disable @typescript-eslint/ban-types */ | ||
import { | ||
Path, | ||
Block, | ||
|
16 changes: 16 additions & 0 deletions
16
packages/@sanity/field/src/types/array/diff/ArrayOfOptionsFieldDiff.css
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
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; | ||
} |
165 changes: 165 additions & 0 deletions
165
packages/@sanity/field/src/types/array/diff/ArrayOfOptionsFieldDiff.tsx
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
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 | ||
} |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from './ArrayOfOptionsFieldDiff' |
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
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])) | ||
} |