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

232 lines (189 loc) • 7.42 kB
import {type SanityDocument} from '@sanity/client' import { type ArraySchemaType, type ConditionalPropertyCallbackContext, type CurrentUser, isArraySchemaType, isObjectSchemaType, type ObjectField, type ObjectFieldType, type ObjectSchemaType, type PathSegment, type SchemaType, } from '@sanity/types' import * as PathUtils from '@sanity/util/paths' import {findIndex} from 'lodash' import {getValueAtPath} from '../../field' import {resolveConditionalProperty} from '../../form' import {getSchemaTypeTitle} from '../../schema' import {type CommentListBreadcrumbs} from '../types' function getSchemaField( schemaType: SchemaType, fieldPath: string, ): ObjectField<SchemaType> | undefined { const paths = PathUtils.fromString(fieldPath) const firstPath = paths[0] if (firstPath && isObjectSchemaType(schemaType)) { const field = schemaType?.fields?.find((f) => f.name === firstPath) if (field) { const nextPath = PathUtils.toString(paths.slice(1)) if (nextPath) { return getSchemaField(field.type, nextPath) } return field } } return undefined } function findArrayItemIndex(array: unknown[], pathSegment: PathSegment): number | false { if (typeof pathSegment === 'number') { return pathSegment } const index = findIndex(array, pathSegment) return index === -1 ? false : index } interface BuildCommentBreadcrumbsProps { documentValue: Partial<SanityDocument> | null fieldPath: string schemaType: SchemaType currentUser: CurrentUser } /** * @beta * @hidden * * This function builds a breadcrumb trail for a given comment using its field path. * It will validate each segment of the path against the document value and/or schema type. * The path is invalid if: * - The field is hidden by a conditional field * - The field is not found in the schema type * - The field is not found in the document value (array items only) */ export function buildCommentBreadcrumbs( props: BuildCommentBreadcrumbsProps, ): CommentListBreadcrumbs { const {currentUser, schemaType, fieldPath, documentValue} = props const paths = PathUtils.fromString(fieldPath) const fieldPaths: CommentListBreadcrumbs = [] let currentSchemaType: ArraySchemaType<SchemaType> | ObjectFieldType<SchemaType> | null = null paths.forEach((seg, index) => { const currentPath = paths.slice(0, index + 1) const previousPath = paths.slice(0, index) const field = getSchemaField(schemaType, PathUtils.toString(currentPath)) const isKeySegment = seg.hasOwnProperty('_key') const parentValue = getValueAtPath(documentValue, previousPath) const currentValue = getValueAtPath(documentValue, currentPath) const conditionalContext: ConditionalPropertyCallbackContext = { document: documentValue as SanityDocument, currentUser, parent: parentValue, value: currentValue, } // If the field is a key segment and the parent value is an array, we'll // try to find the index of the array item in the parent value. // If the index is not found, we'll mark it as invalid. // This can happen if the array item has been removed from the document value. if (isKeySegment && Array.isArray(parentValue)) { const arrayItemIndex = findArrayItemIndex(parentValue, seg) const isNumber = typeof arrayItemIndex === 'number' fieldPaths.push({ invalid: arrayItemIndex === false, isArrayItem: true, title: isNumber ? `#${Number(arrayItemIndex) + 1}` : 'Unknown array item', }) return } // If we find a field in the schema type, we'll add it to the breadcrumb trail. if (field?.type) { const hidden = resolveConditionalProperty(field.type.hidden, conditionalContext) fieldPaths.push({ invalid: hidden, isArrayItem: false, title: getSchemaTypeTitle(field.type), }) // Store the current schema type so we can use it in the next iteration. currentSchemaType = field.type return } if (isArraySchemaType(currentSchemaType)) { // Get the value of the array field in the document value const arrayValue: any = getValueAtPath(documentValue, previousPath) // Get the object type of the array field in the schema type // from the array field's `_type` property in the document value. const objectType = arrayValue?._type // Find the object field in the array field's `of` array using // the object type from the document value. const objectField = currentSchemaType?.of?.find( (type) => type.name === objectType, ) as ObjectSchemaType // Find the field in the object field's `fields` array // using the field name from the path segment. const currentField = objectField?.fields?.find( (f) => f.name === seg, ) as ObjectField<SchemaType> // If we don't find the `_type` property in the document value, that // means that the field is an anonymous object field and don't have a // name defined in the schema type. // In this case, we try to find all the fields of the current schema type // and check if the field name from the path segment matches any of them. // If we find a match, we'll use the field's type to get the title. if (!objectType && currentValue) { const allCurrentFields = currentSchemaType?.of ?.map((o: any) => o?.fields) .filter(Boolean) .flat() const anonymousField = allCurrentFields?.find((f) => f?.name === seg) const hidden = resolveConditionalProperty(anonymousField?.type?.hidden, conditionalContext) if (anonymousField) { fieldPaths.push({ invalid: hidden, isArrayItem: false, title: getSchemaTypeTitle(anonymousField?.type), }) currentSchemaType = anonymousField?.type } return } if (!currentField) { fieldPaths.push({ invalid: true, isArrayItem: false, title: 'Unknown field', }) return } // Get the title of the current field const currentTitle = getSchemaTypeTitle(currentField?.type) // Resolve the hidden property of the object field const objectFieldHidden = resolveConditionalProperty( objectField?.type?.hidden, conditionalContext, ) // Resolve the hidden property of the current field const currentFieldHidden = resolveConditionalProperty( currentField?.type.hidden, conditionalContext, ) // If the object field or the current field is hidden, we'll mark it as invalid. const isHidden = objectFieldHidden || currentFieldHidden // Add the field to the breadcrumb trail fieldPaths.push({ invalid: isHidden, isArrayItem: false, title: currentTitle, }) // If the current field is an object field, we'll set it as the current schema type // so we can use it in the next iteration. currentSchemaType = currentField?.type return } // If we get here, the field is not found in the schema type // or the document value so we'll mark it as invalid. fieldPaths.push({ invalid: true, isArrayItem: false, title: 'Unknown field', }) }) return fieldPaths }