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
218 lines (183 loc) • 7.65 kB
text/typescript
import {
type ArraySchemaType,
isArrayOfBlocksSchemaType,
isArrayOfObjectsSchemaType,
isObjectSchemaType,
isPrimitiveSchemaType,
isReferenceSchemaType,
type ObjectField,
type ObjectSchemaType,
type Path,
type SchemaType,
} from '@sanity/types'
import {toString} from '@sanity/util/paths'
import {getValueAtPath} from '../../../../../field/paths/helpers'
import {EMPTY_ARRAY} from '../../../../../util/empty'
import {getItemType} from '../../../../store/utils/getItemType'
import {type TreeEditingBreadcrumb, type TreeEditingMenuItem} from '../../types'
import {findArrayTypePaths} from '../findArrayTypePaths'
import {getSchemaField} from '../getSchemaField'
import {buildBreadcrumbsState} from './buildBreadcrumbsState'
import {type RecursiveProps, type TreeEditingState} from './buildTreeEditingState'
import {getRelativePath, isArrayItemSelected, shouldBeInBreadcrumb} from './utils'
interface BuildArrayState {
/** The schema type of the array field */
arraySchemaType: ArraySchemaType
/** The value of the array field */
arrayValue: Record<string, unknown>[]
/** The value of the document */
documentValue: unknown
/** The current `openPath` */
openPath: Path
/** The recursive function to build the tree editing state for nested fields */
recursive: (props: RecursiveProps) => TreeEditingState
/** The root path of the array */
rootPath: Path
}
/**
* Build the tree editing state for an array field.
*/
export function buildArrayState(props: BuildArrayState): TreeEditingState {
const {arraySchemaType, arrayValue, documentValue, openPath, rootPath, recursive} = props
let relativePath: Path = []
const menuItems: TreeEditingMenuItem[] = []
const breadcrumbs: TreeEditingBreadcrumb[] = []
// If tree editing is disabled for the array field, return early.
if (arraySchemaType.options?.treeEditing === false) {
return {
breadcrumbs,
menuItems,
relativePath,
rootTitle: '',
}
}
// Iterate over the values of the array field.
arrayValue.forEach((item) => {
// Construct the path to the array item.
const itemPath = [...rootPath, {_key: item._key}] as Path
// Get the schema field for the array item.
const itemSchemaField = getItemType(arraySchemaType, item) as ObjectSchemaType
if (!itemSchemaField) return
if (isReferenceSchemaType(itemSchemaField)) return
if (itemSchemaField?.options?.treeEditing === false) return
const childrenFields = itemSchemaField?.fields || []
const childrenMenuItems: TreeEditingMenuItem[] = []
if (shouldBeInBreadcrumb(itemPath, openPath)) {
const breadcrumbsResult = buildBreadcrumbsState({
arraySchemaType,
arrayValue,
itemPath,
parentPath: rootPath,
})
breadcrumbs.push(breadcrumbsResult)
}
// Iterate over the fields of the array item to resolve any nested fields.
childrenFields.forEach((childField) => {
if (childField?.type?.options?.treeEditing === false) return
// Construct the path to the child field.
const childPath = [...itemPath, childField.name] as Path
// Get the value of the child field.
const childValue = getValueAtPath(documentValue, childPath)
if (isArrayItemSelected(childPath, openPath)) {
relativePath = getRelativePath(childPath)
}
// If the child field is an object field, check if it contains any array fields.
// If there are array fields within the child field, recursively build the tree
// editing state for them.
if (isObjectSchemaType(childField.type)) {
// Find the paths of any array fields within the child field.
const arrayPaths = findArrayTypePaths(childField.type.fields)
// If there are no array fields within the child field, return early.
if (arrayPaths.length === 0) return
// Iterate over the paths of the array fields within the child field.
arrayPaths.forEach((arrayPath) => {
// Construct the path to the array field.
const fieldPath = [...childPath, ...arrayPath] as Path
// Get the schema field for the array field.
const nestedArrayField = getSchemaField(
childField.type,
toString(arrayPath),
) as ObjectField<SchemaType>
// Get the value of the array field.
const arrayFieldValue_ = getValueAtPath(documentValue, fieldPath)
const arrayFieldValue = Array.isArray(arrayFieldValue_) ? arrayFieldValue_ : []
// If the array field has no value or tree editing is disabled, return early.
if (!arrayFieldValue.length) return
if (nestedArrayField.type.options?.treeEditing === false) return
// Update the relative path if the array field is selected.
if (isArrayItemSelected(fieldPath, openPath)) {
relativePath = getRelativePath(fieldPath)
}
// Recursively build the tree editing state for the array field.
const nestedArrayState = recursive({
documentValue,
path: fieldPath,
schemaType: nestedArrayField as ObjectSchemaType,
})
// Add the state of the array field to the children menu items.
childrenMenuItems.push({
children: nestedArrayState.menuItems,
parentSchemaType: childField.type,
path: fieldPath,
schemaType: nestedArrayField.type,
value: arrayFieldValue,
})
})
}
const isPortableText = isArrayOfBlocksSchemaType(childField.type)
const isValid = isArrayOfObjectsSchemaType(childField.type) && childValue && !isPortableText
if (isValid) {
if (shouldBeInBreadcrumb(childPath, openPath)) {
const breadcrumbsResult = buildBreadcrumbsState({
arraySchemaType: childField.type as ArraySchemaType,
arrayValue: childValue as Record<string, unknown>[],
itemPath: childPath,
parentPath: itemPath,
})
breadcrumbs.push(breadcrumbsResult)
}
const childState = recursive({
documentValue,
path: childPath,
schemaType: childField as ObjectSchemaType,
})
childrenMenuItems.push({
children: childState?.menuItems || EMPTY_ARRAY,
parentSchemaType: itemSchemaField,
path: childPath,
schemaType: childField as ObjectSchemaType,
value: childValue,
})
return
}
// If `openPath` points to an array field within a portable text field,
// set `relativePath` to the parent of the portable text field.
// This ensures that the tree editing dialog opens at the parent level
// of the portable text field.
// Portable text fields manage their own dialogs, so we open the tree editing
// dialog for the parent item and let the portable text field handle its
// dialogs via `openPath`.
if (isPortableText && toString(openPath).startsWith(toString(childPath))) {
relativePath = getRelativePath(childPath)
}
})
if (isArrayItemSelected(itemPath, openPath)) {
relativePath = getRelativePath(itemPath)
}
if (!isPrimitiveSchemaType(itemSchemaField?.type)) {
menuItems.push({
children: childrenMenuItems,
parentSchemaType: arraySchemaType,
path: itemPath as Path,
schemaType: itemSchemaField as ObjectSchemaType,
value: item,
})
}
})
return {
breadcrumbs,
menuItems,
relativePath,
rootTitle: '',
}
}