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
324 lines (288 loc) • 9.23 kB
text/typescript
import {SortIcon} from '@sanity/icons'
import {type SchemaType, type SortOrdering, type SortOrderingItem} from '@sanity/types'
import {type I18nTextRecord} from 'sanity'
import {type Intent} from './Intent'
import {HELP_URL, SerializeError} from './SerializeError'
import {DEFAULT_ORDERING_OPTIONS} from './Sort'
import {type Serializable, type SerializeOptions, type SerializePath} from './StructureNodes'
import {type StructureContext} from './types'
import {getExtendedProjection} from './util/getExtendedProjection'
/** @internal */
export function maybeSerializeMenuItem(
item: MenuItem | MenuItemBuilder,
index: number,
path: SerializePath,
): MenuItem {
return item instanceof MenuItemBuilder ? item.serialize({path, index}) : item
}
/**
* Menu item action type
* @public */
export type MenuItemActionType =
| string
| ((params: Record<string, string> | undefined, scope?: any) => void)
/**
* Menu items parameters
*
* @public */
export type MenuItemParamsType = Record<string, string | unknown | undefined>
/**
* Interface for menu items
*
* @public */
export interface MenuItem {
/**
* 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'>
/**
* Menu Item title. Note that the `i18n` configuration will take
* precedence and this title is left here as a fallback if no i18n key is
* provided and compatibility with older plugins
*/
title: string
/** Menu Item action */
action?: MenuItemActionType
/** Menu Item intent */
intent?: Intent
/** Menu Item group */
group?: string
// TODO: align these with TemplateItem['icon']
/** Menu Item icon */
icon?: React.ComponentType | React.ReactNode
/** Menu Item parameters. See {@link MenuItemParamsType} */
params?: MenuItemParamsType
/** Determine if it will show the MenuItem as action */
showAsAction?: boolean
}
/**
* Partial menu items
* @public
*/
export type PartialMenuItem = Partial<MenuItem>
/**
* Class for building menu items.
*
* @public */
export class MenuItemBuilder implements Serializable<MenuItem> {
/** menu item option object. See {@link PartialMenuItem} */
protected spec: PartialMenuItem
constructor(
/**
* Structure context. See {@link StructureContext}
*/
protected _context: StructureContext,
spec?: MenuItem,
) {
this.spec = spec ? spec : {}
}
/**
* Set menu item action
* @param action - menu item action. See {@link MenuItemActionType}
* @returns menu item builder based on action provided. See {@link MenuItemBuilder}
*/
action(action: MenuItemActionType): MenuItemBuilder {
return this.clone({action})
}
/**
* Get menu item action
* @returns menu item builder action. See {@link PartialMenuItem}
*/
getAction(): PartialMenuItem['action'] {
return this.spec.action
}
/**
* Set menu item intent
* @param intent - menu item intent. See {@link Intent}
* @returns menu item builder based on intent provided. See {@link MenuItemBuilder}
*/
intent(intent: Intent): MenuItemBuilder {
return this.clone({intent})
}
/**
* Get menu item intent
* @returns menu item intent. See {@link PartialMenuItem}
*/
getIntent(): PartialMenuItem['intent'] {
return this.spec.intent
}
/**
* Set menu item title
* @param title - menu item title
* @returns menu item builder based on title provided. See {@link MenuItemBuilder}
*/
title(title: string): MenuItemBuilder {
return this.clone({title})
}
/**
* Get menu item title. Note that the `i18n` configuration will take
* precedence and this title is left here for compatibility.
* @returns menu item title
*/
getTitle(): string | undefined {
return this.spec.title
}
/**
* Set the i18n key and namespace used to populate the localized title.
* @param i18n - object with i18n key and related namespace
* @returns menu item builder based on i18n config provided. See {@link MenuItemBuilder}
*/
i18n(i18n: I18nTextRecord<'title'>): MenuItemBuilder {
return this.clone({i18n})
}
/**
* Get the 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 menu item group
* @param group - menu item group
* @returns menu item builder based on group provided. See {@link MenuItemBuilder}
*/
group(group: string): MenuItemBuilder {
return this.clone({group})
}
/**
* Get menu item group
* @returns menu item group. See {@link PartialMenuItem}
*/
getGroup(): PartialMenuItem['group'] {
return this.spec.group
}
/**
* Set menu item icon
* @param icon - menu item icon
* @returns menu item builder based on icon provided. See {@link MenuItemBuilder}
*/
icon(icon: React.ComponentType | React.ReactNode): MenuItemBuilder {
return this.clone({icon})
}
/**
* Get menu item icon
* @returns menu item icon. See {@link PartialMenuItem}
*/
getIcon(): PartialMenuItem['icon'] {
return this.spec.icon
}
/**
* Set menu item parameters
* @param params - menu item parameters. See {@link MenuItemParamsType}
* @returns menu item builder based on parameters provided. See {@link MenuItemBuilder}
*/
params(params: MenuItemParamsType): MenuItemBuilder {
return this.clone({params})
}
/**
* Get meny item parameters
* @returns menu item parameters. See {@link PartialMenuItem}
*/
getParams(): PartialMenuItem['params'] {
return this.spec.params
}
/**
* Set menu item to show as action
* @param showAsAction - determine if menu item should show as action
* @returns menu item builder based on if it should show as action. See {@link MenuItemBuilder}
*/
showAsAction(showAsAction = true): MenuItemBuilder {
return this.clone({showAsAction: Boolean(showAsAction)})
}
/**
* Check if menu item should show as action
* @returns true if menu item should show as action, false if not. See {@link PartialMenuItem}
*/
getShowAsAction(): PartialMenuItem['showAsAction'] {
return this.spec.showAsAction
}
/** Serialize menu item builder
* @param options - serialization options. See {@link SerializeOptions}
* @returns menu item node based on path provided in options. See {@link MenuItem}
*/
serialize(options: SerializeOptions = {path: []}): MenuItem {
const {title, action, intent} = this.spec
if (!title) {
const hint = typeof action === 'string' ? `action: "${action}"` : undefined
throw new SerializeError(
'`title` is required for menu item',
options.path,
options.index,
hint,
).withHelpUrl(HELP_URL.TITLE_REQUIRED)
}
if (!action && !intent) {
throw new SerializeError(
`\`action\` or \`intent\` required for menu item with title ${this.spec.title}`,
options.path,
options.index,
`"${title}"`,
).withHelpUrl(HELP_URL.ACTION_OR_INTENT_REQUIRED)
}
if (intent && action) {
throw new SerializeError(
'cannot set both `action` AND `intent`',
options.path,
options.index,
`"${title}"`,
).withHelpUrl(HELP_URL.ACTION_AND_INTENT_MUTUALLY_EXCLUSIVE)
}
return {...this.spec, title}
}
/** Clone menu item builder
* @param withSpec - menu item options. See {@link PartialMenuItem}
* @returns menu item builder based on context and spec provided. See {@link MenuItemBuilder}
*/
clone(withSpec?: PartialMenuItem): MenuItemBuilder {
const builder = new MenuItemBuilder(this._context)
builder.spec = {...this.spec, ...(withSpec || {})}
return builder
}
}
/** @internal */
export interface SortMenuItem extends MenuItem {
params: {
by: SortOrderingItem[]
}
}
/** @internal */
export function getOrderingMenuItem(
context: StructureContext,
{by, title, i18n}: SortOrdering,
extendedProjection?: string,
): MenuItemBuilder {
let builder = new MenuItemBuilder(context)
.group('sorting')
.title(
context.i18n.t('default-menu-item.fallback-title', {
// note this lives in the `studio` bundle because that one is loaded by default
ns: 'studio',
replace: {title}, // replaces the `{{title}}` option
}),
) // fallback title
.icon(SortIcon)
.action('setSortOrder')
.params({by, extendedProjection})
if (i18n) {
builder = builder.i18n(i18n)
}
return builder
}
/** @internal */
export function getOrderingMenuItemsForSchemaType(
context: StructureContext,
typeName: SchemaType | string,
): MenuItemBuilder[] {
const {schema} = context
const type = typeof typeName === 'string' ? schema.get(typeName) : typeName
if (!type || !('orderings' in type)) {
return []
}
return (
type.orderings ? type.orderings.concat(DEFAULT_ORDERING_OPTIONS) : DEFAULT_ORDERING_OPTIONS
).map((ordering: SortOrdering) =>
getOrderingMenuItem(context, ordering, getExtendedProjection(type, ordering.by)),
)
}