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
213 lines (185 loc) • 6.5 kB
text/typescript
import {find} from 'lodash'
import {isRecord} from 'sanity'
import {type ChildResolver, type ChildResolverOptions} from './ChildResolver'
import {isDocumentListItem} from './DocumentListItem'
import {
type BuildableGenericList,
type GenericList,
GenericListBuilder,
type GenericListInput,
shallowIntentChecker,
} from './GenericList'
import {type IntentChecker} from './Intent'
import {type ListItem, ListItemBuilder} from './ListItem'
import {HELP_URL, SerializeError} from './SerializeError'
import {type Divider, type SerializeOptions, type SerializePath} from './StructureNodes'
import {type StructureContext} from './types'
const getArgType = (thing: ListItem) => {
if (thing instanceof ListBuilder) {
return 'ListBuilder'
}
if (isPromise<ListItem>(thing)) {
return 'Promise'
}
return Array.isArray(thing) ? 'array' : typeof thing
}
const isListItem = (item: ListItem | Divider): item is ListItem => {
return item.type === 'listItem'
}
const defaultCanHandleIntent: IntentChecker = (intentName: string, params, context) => {
const pane = context.pane as List
const items = pane.items || []
return (
items
.filter(isDocumentListItem)
.some((item) => item.schemaType.name === params.type && item._id === params.id) ||
shallowIntentChecker(intentName, params, context)
)
}
const resolveChildForItem: ChildResolver = (itemId: string, options: ChildResolverOptions) => {
const parentItem = options.parent as List
const items = parentItem.items.filter(isListItem)
const target = (items.find((item) => item.id === itemId) || {child: undefined}).child
if (!target || typeof target !== 'function') {
return target
}
return typeof target === 'function' ? target(itemId, options) : target
}
function maybeSerializeListItem(
item: ListItem | ListItemBuilder | Divider,
index: number,
path: SerializePath,
): ListItem | Divider {
if (item instanceof ListItemBuilder) {
return item.serialize({path, index})
}
const listItem = item as ListItem
if (listItem && listItem.type === 'divider') {
return item as Divider
}
if (!listItem || listItem.type !== 'listItem') {
const gotWhat = (listItem && listItem.type) || getArgType(listItem)
const helpText = gotWhat === 'array' ? ' - did you forget to spread (...moreItems)?' : ''
throw new SerializeError(
`List items must be of type "listItem", got "${gotWhat}"${helpText}`,
path,
index,
).withHelpUrl(HELP_URL.INVALID_LIST_ITEM)
}
return item
}
function isPromise<T>(thing: unknown): thing is PromiseLike<T> {
return isRecord(thing) && typeof thing.then === 'function'
}
/**
* Interface for List
*
* @public
*/
export interface List extends GenericList {
type: 'list'
/** List items. See {@link ListItem} and {@link Divider} */
items: (ListItem | Divider)[]
}
/**
* Interface for list input
*
* @public
*/
export interface ListInput extends GenericListInput {
/** List input items array. See {@link ListItem}, {@link ListItemBuilder} and {@link Divider} */
items?: (ListItem | ListItemBuilder | Divider)[]
}
/**
* Interface for buildable list
*
* @public
*/
export interface BuildableList extends BuildableGenericList {
/** List items. See {@link ListItem}, {@link ListItemBuilder} and {@link Divider} */
items?: (ListItem | ListItemBuilder | Divider)[]
}
/**
* A `ListBuilder` is used to build a list of items in the structure tool.
*
* @public */
export class ListBuilder extends GenericListBuilder<BuildableList, ListBuilder> {
/** buildable list option object. See {@link BuildableList} */
protected spec: BuildableList
constructor(
/**
* Structure context. See {@link StructureContext}
*/
protected _context: StructureContext,
spec?: ListInput,
) {
super()
this.spec = spec ? spec : {}
this.initialValueTemplatesSpecified = Boolean(spec && spec.initialValueTemplates)
}
/**
* Set list builder based on items provided
* @param items - list items. See {@link ListItemBuilder}, {@link ListItem} and {@link Divider}
* @returns list builder based on items provided. See {@link ListBuilder}
*/
items(items: (ListItemBuilder | ListItem | Divider)[]): ListBuilder {
return this.clone({items})
}
/** Get list builder items
* @returns list items. See {@link BuildableList}
*/
getItems(): BuildableList['items'] {
return this.spec.items
}
/** Serialize list builder
* @param options - serialization options. See {@link SerializeOptions}
* @returns list based on path in options. See {@link List}
*/
serialize(options: SerializeOptions = {path: []}): List {
const id = this.spec.id
if (typeof id !== 'string' || !id) {
throw new SerializeError(
'`id` is required for lists',
options.path,
options.index,
).withHelpUrl(HELP_URL.ID_REQUIRED)
}
const items = typeof this.spec.items === 'undefined' ? [] : this.spec.items
if (!Array.isArray(items)) {
throw new SerializeError(
'`items` must be an array of items',
options.path,
options.index,
).withHelpUrl(HELP_URL.LIST_ITEMS_MUST_BE_ARRAY)
}
const path = (options.path || []).concat(id)
const serializedItems = items.map((item, index) => maybeSerializeListItem(item, index, path))
const dupes = serializedItems.filter((val, i) => find(serializedItems, {id: val.id}, i + 1))
if (dupes.length > 0) {
const dupeIds = dupes.map((item) => item.id).slice(0, 5)
const dupeDesc = dupes.length > 5 ? `${dupeIds.join(', ')}...` : dupeIds.join(', ')
throw new SerializeError(
`List items with same ID found (${dupeDesc})`,
options.path,
options.index,
).withHelpUrl(HELP_URL.LIST_ITEM_IDS_MUST_BE_UNIQUE)
}
return {
...super.serialize(options),
type: 'list',
canHandleIntent: this.spec.canHandleIntent || defaultCanHandleIntent,
child: this.spec.child || resolveChildForItem,
items: serializedItems,
}
}
/**
* Clone list builder and return new list builder based on context and spec provided
* @param withSpec - list options. See {@link BuildableList}
* @returns new list builder based on context and spec provided. See {@link ListBuilder}
*/
clone(withSpec?: BuildableList): ListBuilder {
const builder = new ListBuilder(this._context)
builder.spec = {...this.spec, ...(withSpec || {})}
return builder
}
}