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
text/typescript
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
}