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
346 lines (310 loc) • 10 kB
text/typescript
import {type SchemaType} from '@sanity/types'
import {type Observable} from 'rxjs'
import {type I18nTextRecord} from 'sanity'
import {type ChildResolver, type ItemChild} from './ChildResolver'
import {ComponentBuilder} from './Component'
import {DocumentBuilder} from './Document'
import {DocumentListBuilder} from './DocumentList'
import {ListBuilder} from './List'
import {HELP_URL, SerializeError} from './SerializeError'
import {
type Collection,
type CollectionBuilder,
type Serializable,
type SerializeOptions,
} from './StructureNodes'
import {type StructureContext} from './types'
import {getStructureNodeId} from './util/getStructureNodeId'
import {validateId} from './util/validateId'
/**
* Unserialized list item child.
* See {@link Collection}, {@link CollectionBuilder}, {@link ChildResolver} and {@link ItemChild}
*
* @public
*/
export type UnserializedListItemChild =
| Collection
| CollectionBuilder
| ChildResolver
| Observable<ItemChild>
/**
* Child of List Item
* See {@link Collection}, {@link ChildResolver}, {@link ItemChild}
* @public
*/
export type ListItemChild = Collection | ChildResolver | Observable<ItemChild> | undefined
/**
* Interface for serialize list item options
*
* @public
*/
export interface ListItemSerializeOptions extends SerializeOptions {
/** Check if list item title is optional */
titleIsOptional?: boolean
}
/**
* Interface for ist item display options
*
* @public */
export interface ListItemDisplayOptions {
/** Check if list item display should show icon */
showIcon?: boolean
}
/**
* interface for list item input
*
* @public */
export interface ListItemInput {
/** List item id */
id: string
/** List item title */
title?: string
/** List item icon */
icon?: React.ComponentType | React.ReactNode
/** List item child. See {@link ListItemChild} */
child?: ListItemChild
/** List item display options. See {@link ListItemDisplayOptions} */
displayOptions?: ListItemDisplayOptions
/** List item schema type. See {@link SchemaType} */
schemaType?: SchemaType | string
}
/**
* Interface for List Item
*
* @public */
export interface ListItem {
/** List item id */
id: string
/** List item type */
type: string
/**
* The i18n key and namespace used to populate the localized title. This is
* the recommend way to set the title if you are localizing your studio.
*/
i18n?: I18nTextRecord<'title'>
/** List item title. Note that the `i18n` key and namespace will take precedence. */
title?: string
/** List item icon */
icon?: React.ComponentType | React.ReactNode
/** List item child. See {@link ListItemChild} */
child?: ListItemChild
/** List item display options. See {@link ListItemDisplayOptions} */
displayOptions?: ListItemDisplayOptions
/** List item schema type. See {@link SchemaType} */
schemaType?: SchemaType
}
/**
* Interface for unserialized list items.
*
* @public
*/
export interface UnserializedListItem {
/** List item ID */
id: string
/** List item title */
title: string
/**
* The i18n key and namespace used to populate the localized title. This is
* the recommend way to set the title if you are localizing your studio.
*/
i18n?: I18nTextRecord<'title'>
/** List item icon */
icon?: React.ComponentType | React.ReactNode
/** List item child. See {@link UnserializedListItemChild} */
child?: UnserializedListItemChild
/** List item display options. See {@link ListItemDisplayOptions} */
displayOptions?: ListItemDisplayOptions
/** List item schema. See {@link SchemaType} */
schemaType?: SchemaType | string
}
/**
* Partial list item. See {@link UnserializedListItem}
*
* @public */
export type PartialListItem = Partial<UnserializedListItem>
/**
* Class for building list items
*
* @public */
export class ListItemBuilder implements Serializable<ListItem> {
/** List item option object. See {@link PartialListItem} */
protected spec: PartialListItem
constructor(
/**
* Structure context. See {@link StructureContext}
*/
protected _context: StructureContext,
spec?: ListItemInput,
) {
this.spec = spec ? spec : {}
}
/**
* Set list item ID
* @returns list item builder based on ID provided. See {@link ListItemBuilder}
*/
id(id: string): ListItemBuilder {
return this.clone({id})
}
/**
* Get list item ID
* @returns list item ID. See {@link PartialListItem}
*/
getId(): PartialListItem['id'] {
return this.spec.id
}
/**
* Set list item title
* @returns list item builder based on title provided. See {@link ListItemBuilder}
*/
title(title: string): ListItemBuilder {
return this.clone({title, id: getStructureNodeId(title, this.spec.id)})
}
/**
* Get list item title
* @returns list item title. See {@link PartialListItem}
*/
getTitle(): PartialListItem['title'] {
return this.spec.title
}
/** Set the i18n key and namespace used to populate the localized title.
* @param i18n - the key and namespaced used to populate the localized title.
* @returns component builder based on i18n key and ns provided
*/
i18n(i18n: I18nTextRecord<'title'>): ListItemBuilder {
return this.clone({i18n})
}
/** Get i18n key and namespace used to populate the localized title
* @returns the i18n key and namespace used to populate the localized title
*/
getI18n(): I18nTextRecord<'title'> | undefined {
return this.spec.i18n
}
/**
* Set list item icon
* @returns list item builder based on icon provided. See {@link ListItemBuilder}
*/
icon(icon: React.ComponentType | React.ReactNode): ListItemBuilder {
return this.clone({icon})
}
/**
* Set if list item should show icon
* @returns list item builder based on showIcon provided. See {@link ListItemBuilder}
*/
showIcon(enabled = true): ListItemBuilder {
return this.clone({
displayOptions: {...(this.spec.displayOptions || {}), showIcon: enabled},
})
}
/**
* Check if list item should show icon
* @returns true if it should show the icon, false if not, undefined if not set
*/
getShowIcon(): boolean | undefined {
return this.spec.displayOptions ? this.spec.displayOptions.showIcon : undefined
}
/**
*Get list item icon
* @returns list item icon. See {@link PartialListItem}
*/
getIcon(): PartialListItem['icon'] {
return this.spec.icon
}
/**
* Set list item child
* @param child - list item child. See {@link UnserializedListItemChild}
* @returns list item builder based on child provided. See {@link ListItemBuilder}
*/
child(child: UnserializedListItemChild): ListItemBuilder {
return this.clone({child})
}
/**
* Get list item child
* @returns list item child. See {@link PartialListItem}
*/
getChild(): PartialListItem['child'] {
return this.spec.child
}
/**
* Set list item schema type
* @param schemaType - list item schema type. See {@link SchemaType}
* @returns list item builder based on schema type provided. See {@link ListItemBuilder}
*/
schemaType(schemaType: SchemaType | string): ListItemBuilder {
return this.clone({schemaType})
}
/**
* Get list item schema type
* @returns list item schema type. See {@link PartialListItem}
*/
getSchemaType(): PartialListItem['schemaType'] {
const schemaType = this.spec.schemaType
if (typeof schemaType === 'string') {
return this._context.schema.get(schemaType)
}
return this.spec.schemaType
}
/** Serialize list item builder
* @param options - serialization options. See {@link ListItemSerializeOptions}
* @returns list item node based on path provided in options. See {@link ListItem}
*/
serialize(options: ListItemSerializeOptions = {path: []}): ListItem {
const {id, title, child} = this.spec
if (typeof id !== 'string' || !id) {
throw new SerializeError(
'`id` is required for list items',
options.path,
options.index,
).withHelpUrl(HELP_URL.ID_REQUIRED)
}
if (!options.titleIsOptional && (typeof title !== 'string' || !title)) {
throw new SerializeError('`title` is required for list items', options.path, id).withHelpUrl(
HELP_URL.TITLE_REQUIRED,
)
}
let schemaType = this.spec.schemaType
if (typeof schemaType === 'string') {
const type = this._context.schema.get(schemaType)
if (!type) {
throw new SerializeError(
`Could not find type "${schemaType}" in schema`,
options.path,
id,
).withHelpUrl(HELP_URL.SCHEMA_TYPE_NOT_FOUND)
}
schemaType = type
}
const serializeOptions = {path: options.path.concat(id), hint: 'child'}
let listChild =
child instanceof ComponentBuilder ||
child instanceof DocumentListBuilder ||
child instanceof DocumentBuilder ||
child instanceof ListBuilder
? child.serialize(serializeOptions)
: child
// In the case of a function, create a bound version that will pass the correct serialize
// context, so we may lazily resolve it at some point in the future without losing context
if (typeof listChild === 'function') {
const originalChild = listChild
listChild = (itemId, childOptions) => {
return originalChild(itemId, {...childOptions, serializeOptions})
}
}
return {
...this.spec,
id: validateId(id, options.path, options.index),
schemaType,
child: listChild,
title,
type: 'listItem',
}
}
/** Clone list item builder
* @param withSpec - partial list item options. See {@link PartialListItem}
* @returns list item builder based on context and spec provided. See {@link ListItemBuilder}
*/
clone(withSpec?: PartialListItem): ListItemBuilder {
const builder = new ListItemBuilder(this._context)
builder.spec = {...this.spec, ...(withSpec || {})}
return builder
}
}