UNPKG

sanity

Version:

Sanity is a real-time content infrastructure with a scalable, hosted backend featuring a Graph Oriented Query Language (GROQ), asset pipelines and fast edge caches

327 lines (285 loc) • 8.95 kB
import { type ArraySchemaType, type MultiFieldSet, type ObjectField, type ObjectSchemaType, type Path, type SchemaType, } from '@sanity/types' import {getItemKeySegment, pathsAreEqual, pathToString} from '../../paths' import {getArrayDiffItemType} from '../../schema/helpers' import { type ArrayDiff, type ChangeNode, type ChangeTitlePath, type Diff, type DiffComponent, type FieldChangeNode, type ItemDiff, type ObjectDiff, } from '../../types' import {hasPTMemberType} from '../../types/portableText/diff/helpers' import {getValueError} from '../../validation' import {isFieldChange} from '../helpers' import {resolveDiffComponent} from '../resolve/resolveDiffComponent' interface DiffContext { itemDiff?: ItemDiff parentDiff?: ArrayDiff | ObjectDiff parentSchema?: ArraySchemaType | ObjectSchemaType fieldFilter?: string[] } export function buildChangeList( schemaType: SchemaType, diff: Diff, path: Path = [], titlePath: ChangeTitlePath = [], context: DiffContext = {}, ): ChangeNode[] { const diffComponent = resolveDiffComponent(schemaType, context.parentSchema) if (!diffComponent) { if (schemaType.jsonType === 'object' && diff.type === 'object') { return buildObjectChangeList(schemaType as ObjectSchemaType, diff, path, titlePath, context) } if (schemaType.jsonType === 'array' && diff.type === 'array') { return buildArrayChangeList(schemaType, diff, path, titlePath) } } return getFieldChange(schemaType, diff, path, titlePath, context) } export function buildObjectChangeList( schemaType: ObjectSchemaType, diff: ObjectDiff, path: Path = [], titlePath: ChangeTitlePath = [], diffContext: DiffContext = {}, ): ChangeNode[] { const changes: ChangeNode[] = [] const childContext: DiffContext = {...diffContext, parentSchema: schemaType} const fieldSets = schemaType.fieldsets || schemaType.fields.map((field) => ({single: true, field})) for (const fieldSet of fieldSets) { if (fieldSet.single) { changes.push(...buildFieldChange(fieldSet.field, diff, path, titlePath, childContext)) } else { changes.push( ...buildFieldsetChangeList(fieldSet as MultiFieldSet, diff, path, titlePath, childContext), ) } } if (changes.length < 2) { return changes } return [ { type: 'group', key: pathToString(path) || 'root', path, titlePath, changes: reduceTitlePaths(changes, titlePath.length), schemaType, }, ] } export function buildFieldChange( field: ObjectField, diff: ObjectDiff, path: Path, titlePath: ChangeTitlePath, diffContext: DiffContext & {fieldFilter?: string[]} = {}, ): ChangeNode[] { const {fieldFilter, ...context} = diffContext const fieldDiff = diff.fields[field.name] if (!fieldDiff || !fieldDiff.isChanged || (fieldFilter && !fieldFilter.includes(field.name))) { return [] } const fieldPath = path.concat([field.name]) const fieldTitlePath = titlePath.concat([field.type.title || field.name]) return buildChangeList(field.type as any, fieldDiff, fieldPath, fieldTitlePath, context) } export function buildFieldsetChangeList( fieldSet: MultiFieldSet, diff: ObjectDiff, path: Path, titlePath: ChangeTitlePath, diffContext: DiffContext & {fieldFilter?: string[]} = {}, ): ChangeNode[] { const {fields, name, title, readOnly, hidden} = fieldSet const {fieldFilter, ...context} = diffContext const fieldSetHidden = hidden const fieldsetReadOnly = readOnly const fieldSetTitlePath = titlePath.concat([title || name]) const changes: ChangeNode[] = [] for (const field of fields) { const fieldDiff = diff.fields[field.name] if (!fieldDiff || !fieldDiff.isChanged || (fieldFilter && !fieldFilter.includes(field.name))) { continue } const fieldPath = path.concat([field.name]) const fieldTitlePath = fieldSetTitlePath.concat([field.type.title || field.name]) changes.push( ...buildChangeList( { readOnly: fieldsetReadOnly, hidden: fieldSetHidden, ...field.type, } as any, fieldDiff, fieldPath, fieldTitlePath, context, ), ) } if (changes.length < 2) { return changes } return [ { type: 'group', key: pathToString(path) || 'root', fieldsetName: name, path, titlePath: fieldSetTitlePath, changes: reduceTitlePaths(changes, fieldSetTitlePath.length), readOnly: fieldsetReadOnly, hidden: fieldSetHidden, }, ] } export function buildArrayChangeList( schemaType: ArraySchemaType, diff: ArrayDiff, path: Path = [], titlePath: ChangeTitlePath = [], ): ChangeNode[] { const changedOrMoved = diff.items.filter( (item) => (item.hasMoved && item.fromIndex !== item.toIndex) || item.diff.action !== 'unchanged', ) if (changedOrMoved.length === 0) { return [] } const isPortableText = hasPTMemberType(schemaType) const list: ChangeNode[] = [] const changes = changedOrMoved.reduce((acc, itemDiff) => { const memberTypes = getArrayDiffItemType(itemDiff.diff, schemaType) const memberType = memberTypes.toType || memberTypes.fromType if (!memberType) { // eslint-disable-next-line no-console console.warn('Could not determine schema type for item at %s', pathToString(path)) return acc } const segment = getItemKeySegment(itemDiff.diff.fromValue) || getItemKeySegment(itemDiff.diff.toValue) || diff.items.indexOf(itemDiff) const itemPath = path.concat(segment) const itemContext: DiffContext = {itemDiff, parentDiff: diff, parentSchema: schemaType} const itemTitlePath = titlePath.concat({ hasMoved: itemDiff.hasMoved, toIndex: itemDiff.toIndex, fromIndex: itemDiff.fromIndex, annotation: itemDiff.diff.action === 'unchanged' ? itemDiff.annotation : itemDiff.diff.annotation, }) const attachItemDiff = (change: ChangeNode): ChangeNode => { if (change.type === 'field' && pathsAreEqual(itemPath, change.path)) { change.itemDiff = itemDiff } return change } const children = buildChangeList( memberType, itemDiff.diff, itemPath, itemTitlePath, itemContext, ).map(attachItemDiff) if (isPortableText) { children.filter(isFieldChange).forEach((field, index, siblings) => { field.showHeader = siblings.length === 1 field.showIndex = itemDiff.fromIndex !== itemDiff.toIndex && itemDiff.hasMoved }) } if (children.length === 0) { // This can happen when there are no changes to the actual element, it's just been moved acc.push(...getFieldChange(memberType, itemDiff.diff, itemPath, itemTitlePath, itemContext)) } else { acc.push(...children) } return acc }, list) if (changes.length > 1) { return [ { type: 'group', key: pathToString(path) || 'root', path, titlePath, changes: reduceTitlePaths(changes, titlePath.length), schemaType, }, ] } return changes } function getFieldChange( schemaType: SchemaType, diff: Diff, path: Path, titlePath: ChangeTitlePath, {itemDiff, parentDiff, parentSchema}: DiffContext = {}, ): FieldChangeNode[] { const {fromValue, toValue, type} = diff // Treat undefined => [] as no change if (type === 'array' && isEmpty(fromValue) && isEmpty(toValue)) { return [] } let error if (typeof fromValue !== 'undefined') { error = getValueError(fromValue, schemaType) } if (!error && typeof toValue !== 'undefined') { error = getValueError(toValue, schemaType) } let showHeader = true let component: DiffComponent | undefined const diffComponent = resolveDiffComponent(schemaType, parentSchema) if (diffComponent && typeof diffComponent === 'function') { // Just a diff component with default options component = diffComponent } else if (diffComponent) { // Diff component with options component = (diffComponent as any).component showHeader = typeof (diffComponent as any).showHeader === 'undefined' ? showHeader : (diffComponent as any).showHeader } return [ { type: 'field', diff, path, error, itemDiff, parentDiff, titlePath, schemaType, showHeader, showIndex: true, key: pathToString(path) || 'root', diffComponent: error ? undefined : component, parentSchema, }, ] } function reduceTitlePaths(changes: ChangeNode[], byLength = 1): ChangeNode[] { return changes.map((change) => { change.titlePath = change.titlePath.slice(byLength) return change }) } function isEmpty(item: unknown): boolean { return (Array.isArray(item) && item.length === 0) || item === null || typeof item === 'undefined' }