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
1 lines • 176 kB
Source Map (JSON)
{"version":3,"file":"StructureToolProvider.mjs","sources":["../../src/structure/i18n/index.ts","../../src/structure/structureBuilder/util/getExtendedProjection.ts","../../src/structure/structureBuilder/SerializeError.ts","../../src/structure/structureBuilder/Sort.ts","../../src/structure/structureBuilder/MenuItem.ts","../../src/structure/structureBuilder/MenuItemGroup.ts","../../src/structure/structureBuilder/util/validateId.ts","../../src/structure/structureBuilder/util/getStructureNodeId.ts","../../src/structure/structureBuilder/Component.ts","../../src/structure/structureBuilder/Divider.ts","../../src/structure/structureBuilder/util/resolveTypeForDocument.ts","../../src/structure/structureBuilder/views/View.ts","../../src/structure/structureBuilder/views/ComponentView.ts","../../src/structure/structureBuilder/views/FormView.ts","../../src/structure/structureBuilder/views/index.ts","../../src/structure/structureBuilder/Document.ts","../../src/structure/structureBuilder/InitialValueTemplateItem.ts","../../src/structure/structureBuilder/Intent.ts","../../src/structure/structureBuilder/Layout.ts","../../src/structure/structureBuilder/GenericList.ts","../../src/structure/structureBuilder/DocumentList.ts","../../src/structure/structureBuilder/List.ts","../../src/structure/structureBuilder/ListItem.ts","../../src/structure/structureBuilder/DocumentListItem.ts","../../src/structure/structureBuilder/DocumentTypeList.ts","../../src/structure/structureBuilder/documentTypeListItems.ts","../../src/structure/structureBuilder/createStructureBuilder.ts","../../src/structure/StructureToolProvider.tsx"],"sourcesContent":["import {defineLocaleResourceBundle} from 'sanity'\n\n/**\n * The locale namespace for the structure tool\n *\n * @public\n */\nexport const structureLocaleNamespace = 'structure' as const\n\n/**\n * The default locale bundle for the structure tool, which is US English.\n *\n * @internal\n */\nexport const structureUsEnglishLocaleBundle = defineLocaleResourceBundle({\n locale: 'en-US',\n namespace: structureLocaleNamespace,\n resources: () => import('./resources'),\n})\n\n/**\n * The locale resource keys for the structure tool.\n *\n * @alpha\n * @hidden\n */\nexport type {StructureLocaleResourceKeys} from './resources'\n","import {type SchemaType, type SortOrderingItem} from '@sanity/types'\n\nconst IMPLICIT_SCHEMA_TYPE_FIELDS = ['_id', '_type', '_createdAt', '_updatedAt', '_rev']\n\n// Takes a path array and a schema type and builds a GROQ join every time it enters a reference field\nfunction joinReferences(schemaType: SchemaType, path: string[], strict: boolean = false): string {\n const [head, ...tail] = path\n\n if (!('fields' in schemaType)) {\n return ''\n }\n\n const schemaField = schemaType.fields.find((field) => field.name === head)\n if (!schemaField) {\n if (!IMPLICIT_SCHEMA_TYPE_FIELDS.includes(head)) {\n const errorMessage = `The current ordering config targeted the nonexistent field \"${head}\" on schema type \"${schemaType.name}\". It should be one of ${schemaType.fields.map((field) => field.name).join(', ')}`\n if (strict) {\n throw new Error(errorMessage)\n } else {\n console.warn(errorMessage)\n }\n }\n return ''\n }\n\n if ('to' in schemaField.type && schemaField.type.name === 'reference') {\n const refTypes = schemaField.type.to\n return `${head}->{${refTypes.map((refType) => joinReferences(refType, tail)).join(',')}}`\n }\n\n const tailFields = tail.length > 0 && joinReferences(schemaField.type, tail)\n const tailWrapper = tailFields ? `{${tailFields}}` : ''\n return tail.length > 0 ? `${head}${tailWrapper}` : head\n}\n\nexport function getExtendedProjection(\n schemaType: SchemaType,\n orderBy: SortOrderingItem[],\n strict: boolean = false,\n): string {\n return orderBy\n .map((ordering) => joinReferences(schemaType, ordering.field.split('.'), strict))\n .join(', ')\n}\n","import {type SerializePath} from './StructureNodes'\n\n/** @internal */\nexport class SerializeError extends Error {\n public readonly path: SerializePath\n public helpId?: (typeof HELP_URL)[keyof typeof HELP_URL]\n\n constructor(\n message: string,\n parentPath: SerializePath,\n pathSegment: string | number | undefined,\n hint?: string,\n ) {\n super(message)\n this.name = 'SerializeError'\n const segment = typeof pathSegment === 'undefined' ? '<unknown>' : `${pathSegment}`\n this.path = (parentPath || []).concat(hint ? `${segment} (${hint})` : segment)\n }\n\n withHelpUrl(id: (typeof HELP_URL)[keyof typeof HELP_URL]): SerializeError {\n this.helpId = id\n return this\n }\n}\n\n/** @internal */\nexport const HELP_URL = {\n ID_REQUIRED: 'structure-node-id-required' as const,\n TITLE_REQUIRED: 'structure-title-required' as const,\n FILTER_REQUIRED: 'structure-filter-required' as const,\n INVALID_LIST_ITEM: 'structure-invalid-list-item' as const,\n COMPONENT_REQUIRED: 'structure-view-component-required' as const,\n DOCUMENT_ID_REQUIRED: 'structure-document-id-required' as const,\n DOCUMENT_TYPE_REQUIRED: 'structure-document-type-required' as const,\n SCHEMA_TYPE_REQUIRED: 'structure-schema-type-required' as const,\n SCHEMA_TYPE_NOT_FOUND: 'structure-schema-type-not-found' as const,\n LIST_ITEMS_MUST_BE_ARRAY: 'structure-list-items-must-be-array' as const,\n QUERY_PROVIDED_FOR_FILTER: 'structure-query-provided-for-filter' as const,\n ACTION_OR_INTENT_REQUIRED: 'structure-action-or-intent-required' as const,\n LIST_ITEM_IDS_MUST_BE_UNIQUE: 'structure-list-item-ids-must-be-unique' as const,\n ACTION_AND_INTENT_MUTUALLY_EXCLUSIVE: 'structure-action-and-intent-mutually-exclusive' as const,\n API_VERSION_REQUIRED_FOR_CUSTOM_FILTER:\n 'structure-api-version-required-for-custom-filter' as const,\n}\n","import {type SortOrdering} from '@sanity/types'\n\nimport {structureLocaleNamespace} from '../i18n'\n\nexport const ORDER_BY_UPDATED_AT: SortOrdering = {\n title: 'Last edited',\n i18n: {\n title: {\n key: 'menu-items.sort-by.last-edited',\n ns: structureLocaleNamespace,\n },\n },\n name: 'lastEditedDesc',\n by: [{field: '_updatedAt', direction: 'desc'}],\n}\n\nexport const ORDER_BY_CREATED_AT: SortOrdering = {\n title: 'Created',\n i18n: {\n title: {\n key: 'menu-items.sort-by.created',\n ns: structureLocaleNamespace,\n },\n },\n name: 'lastCreatedDesc',\n by: [{field: '_createdAt', direction: 'desc'}],\n}\n\nexport const DEFAULT_SELECTED_ORDERING_OPTION = ORDER_BY_UPDATED_AT\n\nexport const DEFAULT_ORDERING_OPTIONS: SortOrdering[] = [\n ORDER_BY_UPDATED_AT, // _updatedAt\n ORDER_BY_CREATED_AT, // _createdAt\n]\n","import {SortIcon} from '@sanity/icons'\nimport {type SchemaType, type SortOrdering, type SortOrderingItem} from '@sanity/types'\nimport {type I18nTextRecord} from 'sanity'\n\nimport {type Intent} from './Intent'\nimport {HELP_URL, SerializeError} from './SerializeError'\nimport {DEFAULT_ORDERING_OPTIONS} from './Sort'\nimport {type Serializable, type SerializeOptions, type SerializePath} from './StructureNodes'\nimport {type StructureContext} from './types'\nimport {getExtendedProjection} from './util/getExtendedProjection'\n\n/** @internal */\nexport function maybeSerializeMenuItem(\n item: MenuItem | MenuItemBuilder,\n index: number,\n path: SerializePath,\n): MenuItem {\n return item instanceof MenuItemBuilder ? item.serialize({path, index}) : item\n}\n\n/**\n * Menu item action type\n * @public */\nexport type MenuItemActionType =\n | string\n | ((params: Record<string, string> | undefined, scope?: any) => void)\n\n/**\n * Menu items parameters\n *\n * @public */\nexport type MenuItemParamsType = Record<string, string | unknown | undefined>\n\n/**\n * Interface for menu items\n *\n * @public */\nexport interface MenuItem {\n /**\n * The i18n key and namespace used to populate the localized title. This is\n * the recommend way to set the title if you are localizing your studio.\n */\n i18n?: I18nTextRecord<'title'>\n /**\n * Menu Item title. Note that the `i18n` configuration will take\n * precedence and this title is left here as a fallback if no i18n key is\n * provided and compatibility with older plugins\n */\n title: string\n /** Menu Item action */\n action?: MenuItemActionType\n /** Menu Item intent */\n intent?: Intent\n /** Menu Item group */\n group?: string\n // TODO: align these with TemplateItem['icon']\n /** Menu Item icon */\n icon?: React.ComponentType | React.ReactNode\n /** Menu Item parameters. See {@link MenuItemParamsType} */\n params?: MenuItemParamsType\n /** Determine if it will show the MenuItem as action */\n showAsAction?: boolean\n}\n\n/**\n * Partial menu items\n * @public\n */\nexport type PartialMenuItem = Partial<MenuItem>\n\n/**\n * Class for building menu items.\n *\n * @public */\nexport class MenuItemBuilder implements Serializable<MenuItem> {\n /** menu item option object. See {@link PartialMenuItem} */\n protected spec: PartialMenuItem\n\n protected _context: StructureContext\n\n constructor(\n /**\n * Structure context. See {@link StructureContext}\n */\n _context: StructureContext,\n spec?: MenuItem,\n ) {\n this._context = _context\n this.spec = spec ? spec : {}\n }\n\n /**\n * Set menu item action\n * @param action - menu item action. See {@link MenuItemActionType}\n * @returns menu item builder based on action provided. See {@link MenuItemBuilder}\n */\n action(action: MenuItemActionType): MenuItemBuilder {\n return this.clone({action})\n }\n\n /**\n * Get menu item action\n * @returns menu item builder action. See {@link PartialMenuItem}\n */\n getAction(): PartialMenuItem['action'] {\n return this.spec.action\n }\n\n /**\n * Set menu item intent\n * @param intent - menu item intent. See {@link Intent}\n * @returns menu item builder based on intent provided. See {@link MenuItemBuilder}\n */\n intent(intent: Intent): MenuItemBuilder {\n return this.clone({intent})\n }\n\n /**\n * Get menu item intent\n * @returns menu item intent. See {@link PartialMenuItem}\n */\n getIntent(): PartialMenuItem['intent'] {\n return this.spec.intent\n }\n\n /**\n * Set menu item title\n * @param title - menu item title\n * @returns menu item builder based on title provided. See {@link MenuItemBuilder}\n */\n title(title: string): MenuItemBuilder {\n return this.clone({title})\n }\n\n /**\n * Get menu item title. Note that the `i18n` configuration will take\n * precedence and this title is left here for compatibility.\n * @returns menu item title\n */\n getTitle(): string | undefined {\n return this.spec.title\n }\n\n /**\n * Set the i18n key and namespace used to populate the localized title.\n * @param i18n - object with i18n key and related namespace\n * @returns menu item builder based on i18n config provided. See {@link MenuItemBuilder}\n */\n i18n(i18n: I18nTextRecord<'title'>): MenuItemBuilder {\n return this.clone({i18n})\n }\n\n /**\n * Get the i18n key and namespace used to populate the localized title.\n * @returns the i18n key and namespace used to populate the localized title.\n */\n getI18n(): I18nTextRecord<'title'> | undefined {\n return this.spec.i18n\n }\n\n /**\n * Set menu item group\n * @param group - menu item group\n * @returns menu item builder based on group provided. See {@link MenuItemBuilder}\n */\n group(group: string): MenuItemBuilder {\n return this.clone({group})\n }\n\n /**\n * Get menu item group\n * @returns menu item group. See {@link PartialMenuItem}\n */\n getGroup(): PartialMenuItem['group'] {\n return this.spec.group\n }\n\n /**\n * Set menu item icon\n * @param icon - menu item icon\n * @returns menu item builder based on icon provided. See {@link MenuItemBuilder}\n */\n icon(icon: React.ComponentType | React.ReactNode): MenuItemBuilder {\n return this.clone({icon})\n }\n\n /**\n * Get menu item icon\n * @returns menu item icon. See {@link PartialMenuItem}\n */\n getIcon(): PartialMenuItem['icon'] {\n return this.spec.icon\n }\n\n /**\n * Set menu item parameters\n * @param params - menu item parameters. See {@link MenuItemParamsType}\n * @returns menu item builder based on parameters provided. See {@link MenuItemBuilder}\n */\n params(params: MenuItemParamsType): MenuItemBuilder {\n return this.clone({params})\n }\n\n /**\n * Get meny item parameters\n * @returns menu item parameters. See {@link PartialMenuItem}\n */\n getParams(): PartialMenuItem['params'] {\n return this.spec.params\n }\n\n /**\n * Set menu item to show as action\n * @param showAsAction - determine if menu item should show as action\n * @returns menu item builder based on if it should show as action. See {@link MenuItemBuilder}\n */\n showAsAction(showAsAction = true): MenuItemBuilder {\n return this.clone({showAsAction: Boolean(showAsAction)})\n }\n\n /**\n * Check if menu item should show as action\n * @returns true if menu item should show as action, false if not. See {@link PartialMenuItem}\n */\n getShowAsAction(): PartialMenuItem['showAsAction'] {\n return this.spec.showAsAction\n }\n\n /** Serialize menu item builder\n * @param options - serialization options. See {@link SerializeOptions}\n * @returns menu item node based on path provided in options. See {@link MenuItem}\n */\n serialize(options: SerializeOptions = {path: []}): MenuItem {\n const {title, action, intent} = this.spec\n if (!title) {\n const hint = typeof action === 'string' ? `action: \"${action}\"` : undefined\n throw new SerializeError(\n '`title` is required for menu item',\n options.path,\n options.index,\n hint,\n ).withHelpUrl(HELP_URL.TITLE_REQUIRED)\n }\n\n if (!action && !intent) {\n throw new SerializeError(\n `\\`action\\` or \\`intent\\` required for menu item with title ${this.spec.title}`,\n options.path,\n options.index,\n `\"${title}\"`,\n ).withHelpUrl(HELP_URL.ACTION_OR_INTENT_REQUIRED)\n }\n\n if (intent && action) {\n throw new SerializeError(\n 'cannot set both `action` AND `intent`',\n options.path,\n options.index,\n `\"${title}\"`,\n ).withHelpUrl(HELP_URL.ACTION_AND_INTENT_MUTUALLY_EXCLUSIVE)\n }\n\n return {...this.spec, title}\n }\n\n /** Clone menu item builder\n * @param withSpec - menu item options. See {@link PartialMenuItem}\n * @returns menu item builder based on context and spec provided. See {@link MenuItemBuilder}\n */\n clone(withSpec?: PartialMenuItem): MenuItemBuilder {\n const builder = new MenuItemBuilder(this._context)\n builder.spec = {...this.spec, ...withSpec}\n return builder\n }\n}\n\n/** @internal */\nexport interface SortMenuItem extends MenuItem {\n params: {\n by: SortOrderingItem[]\n }\n}\n\n/** @internal */\nexport function getOrderingMenuItem(\n context: StructureContext,\n {by, title, i18n}: SortOrdering,\n extendedProjection?: string,\n): MenuItemBuilder {\n let builder = new MenuItemBuilder(context)\n .group('sorting')\n .title(\n context.i18n.t('default-menu-item.fallback-title', {\n // note this lives in the `studio` bundle because that one is loaded by default\n ns: 'studio',\n replace: {title}, // replaces the `{{title}}` option\n }),\n ) // fallback title\n .icon(SortIcon)\n .action('setSortOrder')\n .params({by, extendedProjection})\n\n if (i18n) {\n builder = builder.i18n(i18n)\n }\n\n return builder\n}\n\n/** @internal */\nexport function getOrderingMenuItemsForSchemaType(\n context: StructureContext,\n typeName: SchemaType | string,\n): MenuItemBuilder[] {\n const {schema} = context\n const type = typeof typeName === 'string' ? schema.get(typeName) : typeName\n if (!type || !('orderings' in type)) {\n return []\n }\n\n return (\n type.orderings ? type.orderings.concat(DEFAULT_ORDERING_OPTIONS) : DEFAULT_ORDERING_OPTIONS\n ).map((ordering: SortOrdering) =>\n getOrderingMenuItem(context, ordering, getExtendedProjection(type, ordering.by)),\n )\n}\n","import {type I18nTextRecord} from 'sanity'\n\nimport {HELP_URL, SerializeError} from './SerializeError'\nimport {type Serializable, type SerializeOptions, type SerializePath} from './StructureNodes'\nimport {type StructureContext} from './types'\n\n/** @internal */\nexport function maybeSerializeMenuItemGroup(\n item: MenuItemGroup | MenuItemGroupBuilder,\n index: number,\n path: SerializePath,\n): MenuItemGroup {\n return item instanceof MenuItemGroupBuilder ? item.serialize({path, index}) : item\n}\n\n/**\n * Interface for menu item groups\n * @public\n */\nexport interface MenuItemGroup {\n /** Menu group Id */\n id: string\n /** Menu group title */\n title: string\n i18n?: I18nTextRecord<'title'>\n}\n\n/**\n * Class for building menu item groups.\n *\n * @public\n */\nexport class MenuItemGroupBuilder implements Serializable<MenuItemGroup> {\n /** Menu item group ID */\n protected _id: string\n /** Menu item group title */\n protected _title: string\n\n protected _i18n?: I18nTextRecord<'title'>\n\n protected _context: StructureContext\n\n constructor(\n /**\n * Structure context. See {@link StructureContext}\n */\n _context: StructureContext,\n spec?: MenuItemGroup,\n ) {\n this._context = _context\n this._id = spec ? spec.id : ''\n this._title = spec ? spec.title : ''\n this._i18n = spec ? spec.i18n : undefined\n }\n\n /**\n * Set menu item group ID\n * @param id - menu item group ID\n * @returns menu item group builder based on ID provided. See {@link MenuItemGroupBuilder}\n */\n id(id: string): MenuItemGroupBuilder {\n return new MenuItemGroupBuilder(this._context, {id, title: this._title, i18n: this._i18n})\n }\n\n /**\n * Get menu item group ID\n * @returns menu item group ID\n */\n getId(): string {\n return this._id\n }\n\n /**\n * Set menu item group title\n * @param title - menu item group title\n * @returns menu item group builder based on title provided. See {@link MenuItemGroupBuilder}\n */\n title(title: string): MenuItemGroupBuilder {\n return new MenuItemGroupBuilder(this._context, {title, id: this._id, i18n: this._i18n})\n }\n\n /**\n * Get menu item group title\n * @returns menu item group title\n */\n getTitle(): string {\n return this._title\n }\n\n /**\n * Set the i18n key and namespace used to populate the localized title.\n * @param i18n - object with i18n key and related namespace\n * @returns menu item group builder based on i18n info provided. See {@link MenuItemGroupBuilder}\n */\n i18n(i18n: I18nTextRecord<'title'>): MenuItemGroupBuilder {\n return new MenuItemGroupBuilder(this._context, {i18n, id: this._id, title: this._title})\n }\n\n /**\n * Get the i18n key and namespace used to populate the localized title.\n * @returns the i18n key and namespace used to populate the localized title.\n */\n getI18n(): I18nTextRecord<'title'> | undefined {\n return this._i18n\n }\n\n /**\n * Serialize menu item group builder\n * @param options - serialization options (path). See {@link SerializeOptions}\n * @returns menu item group based on path provided in options. See {@link MenuItemGroup}\n */\n serialize(options: SerializeOptions = {path: []}): MenuItemGroup {\n if (!this._id) {\n throw new SerializeError(\n '`id` is required for a menu item group',\n options.path,\n options.index,\n this._title,\n ).withHelpUrl(HELP_URL.ID_REQUIRED)\n }\n\n if (!this._title) {\n throw new SerializeError(\n '`title` is required for a menu item group',\n options.path,\n this._id,\n ).withHelpUrl(HELP_URL.TITLE_REQUIRED)\n }\n\n return {\n id: this._id,\n title: this._title,\n i18n: this._i18n,\n }\n }\n}\n","import {SerializeError} from '../SerializeError'\nimport {type SerializePath} from '../StructureNodes'\n\nexport const disallowedPattern = /([^A-Za-z0-9-_.])/\n\nexport function validateId(\n id: string,\n parentPath: SerializePath,\n pathSegment: string | number | undefined,\n): string {\n if (typeof id !== 'string') {\n throw new SerializeError(\n `Structure node id must be of type string, got ${typeof id}`,\n parentPath,\n pathSegment,\n )\n }\n\n const [disallowedChar] = id.match(disallowedPattern) || []\n if (disallowedChar) {\n throw new SerializeError(\n `Structure node id cannot contain character \"${disallowedChar}\"`,\n parentPath,\n pathSegment,\n )\n }\n\n if (id.startsWith('__edit__')) {\n throw new SerializeError(\n `Structure node id cannot start with __edit__`,\n parentPath,\n pathSegment,\n )\n }\n\n return id\n}\n","import {camelCase} from 'lodash'\nimport getSlug from 'speakingurl'\n\nimport {disallowedPattern} from './validateId'\n\nexport function getStructureNodeId(title: string, id?: string): string {\n if (id) {\n return id\n }\n\n const camelCased = camelCase(title)\n\n return disallowedPattern.test(camelCased) ? camelCase(getSlug(title)) : camelCased\n}\n","import {type I18nTextRecord} from 'sanity'\n\nimport {type IntentChecker} from './Intent'\nimport {maybeSerializeMenuItem, type MenuItem, type MenuItemBuilder} from './MenuItem'\nimport {\n maybeSerializeMenuItemGroup,\n type MenuItemGroup,\n type MenuItemGroupBuilder,\n} from './MenuItemGroup'\nimport {HELP_URL, SerializeError} from './SerializeError'\nimport {\n type Child,\n type Serializable,\n type SerializeOptions,\n type StructureNode,\n} from './StructureNodes'\nimport {type UserComponent} from './types'\nimport {getStructureNodeId} from './util/getStructureNodeId'\nimport {validateId} from './util/validateId'\n\n/**\n * Interface for component\n *\n * @public\n */\n// TODO: rename to `StructureComponent` since it clashes with React?\nexport interface Component extends StructureNode {\n /** Component of type {@link UserComponent} */\n component: UserComponent\n /** Component child of type {@link Child} */\n child?: Child\n /** Component menu items, array of type {@link MenuItem} */\n menuItems: MenuItem[]\n /** Component menu item group, array of type {@link MenuItemGroup} */\n menuItemGroups: MenuItemGroup[]\n /** Component options */\n options: {[key: string]: unknown}\n canHandleIntent?: IntentChecker\n}\n\n/**\n * Interface for component input\n *\n * @public\n */\nexport interface ComponentInput extends StructureNode {\n /** Component of type {@link UserComponent} */\n component: UserComponent\n /** Component child of type {@link Child} */\n child?: Child\n /** Component options */\n options?: {[key: string]: unknown}\n /** Component menu items. See {@link MenuItem} and {@link MenuItemBuilder} */\n menuItems?: (MenuItem | MenuItemBuilder)[]\n /** Component menu item groups. See {@link MenuItemGroup} and {@link MenuItemGroupBuilder} */\n menuItemGroups?: (MenuItemGroup | MenuItemGroupBuilder)[]\n}\n\n/**\n * Interface for buildable component\n *\n * @public\n */\nexport interface BuildableComponent extends Partial<StructureNode> {\n /** Component of type {@link UserComponent} */\n component?: UserComponent\n /** Component child of type {@link Child} */\n child?: Child\n /** Component options */\n options?: {[key: string]: unknown}\n /** Component menu items. See {@link MenuItem} and {@link MenuItemBuilder} */\n menuItems?: (MenuItem | MenuItemBuilder)[]\n /** Component menu item groups. See {@link MenuItemGroup} and {@link MenuItemGroupBuilder} */\n menuItemGroups?: (MenuItemGroup | MenuItemGroupBuilder)[]\n canHandleIntent?: IntentChecker\n}\n\n/**\n * Class for building components\n *\n * @public\n */\nexport class ComponentBuilder implements Serializable<Component> {\n /** component builder option object */\n protected spec: BuildableComponent\n\n constructor(spec?: ComponentInput) {\n this.spec = {options: {}, ...(spec ? spec : {})}\n }\n\n /** Set Component ID\n * @param id - component ID\n * @returns component builder based on ID provided\n */\n id(id: string): ComponentBuilder {\n return this.clone({id})\n }\n\n /** Get ID\n * @returns ID\n */\n getId(): BuildableComponent['id'] {\n return this.spec.id\n }\n\n /** Set Component title\n * @param title - component title\n * @returns component builder based on title provided (and ID)\n */\n title(title: string): ComponentBuilder {\n return this.clone({title, id: getStructureNodeId(title, this.spec.id)})\n }\n\n /** Get Component title\n * @returns title\n */\n getTitle(): BuildableComponent['title'] {\n return this.spec.title\n }\n\n /** Set the i18n key and namespace used to populate the localized title.\n * @param i18n - the key and namespaced used to populate the localized title.\n * @returns component builder based on i18n key and ns provided\n */\n i18n(i18n: I18nTextRecord<'title'>): ComponentBuilder {\n return this.clone({i18n})\n }\n\n /** Get i18n key and namespace used to populate the localized title\n * @returns the i18n key and namespace used to populate the localized title\n */\n getI18n(): I18nTextRecord<'title'> | undefined {\n return this.spec.i18n\n }\n\n /** Set Component child\n * @param child - child component\n * @returns component builder based on child component provided\n */\n child(child: Child): ComponentBuilder {\n return this.clone({child})\n }\n\n /** Get Component child\n * @returns child component\n */\n getChild(): BuildableComponent['child'] {\n return this.spec.child\n }\n\n /** Set component\n * @param component - user built component\n * @returns component builder based on component provided\n */\n component(component: UserComponent): ComponentBuilder {\n return this.clone({component})\n }\n\n /** Get Component\n * @returns component\n */\n getComponent(): BuildableComponent['component'] {\n return this.spec.component\n }\n\n /** Set Component options\n * @param options - component options\n * @returns component builder based on options provided\n */\n options(options: {[key: string]: unknown}): ComponentBuilder {\n return this.clone({options})\n }\n\n /** Get Component options\n * @returns component options\n */\n getOptions(): NonNullable<BuildableComponent['options']> {\n return this.spec.options || {}\n }\n\n /** Set Component menu items\n * @param menuItems - component menu items\n * @returns component builder based on menuItems provided\n */\n menuItems(menuItems: (MenuItem | MenuItemBuilder)[]): ComponentBuilder {\n return this.clone({menuItems})\n }\n\n /** Get Component menu items\n * @returns menu items\n */\n getMenuItems(): BuildableComponent['menuItems'] {\n return this.spec.menuItems\n }\n\n /** Set Component menu item groups\n * @param menuItemGroups - component menu item groups\n * @returns component builder based on menuItemGroups provided\n */\n menuItemGroups(menuItemGroups: (MenuItemGroup | MenuItemGroupBuilder)[]): ComponentBuilder {\n return this.clone({menuItemGroups})\n }\n\n /** Get Component menu item groups\n * @returns menu item groups\n */\n getMenuItemGroups(): BuildableComponent['menuItemGroups'] {\n return this.spec.menuItemGroups\n }\n\n canHandleIntent(canHandleIntent: IntentChecker): ComponentBuilder {\n return this.clone({canHandleIntent})\n }\n\n /** Serialize component\n * @param options - serialization options\n * @returns component object based on path provided in options\n *\n */\n serialize(options: SerializeOptions = {path: []}): Component {\n const {id, title, child, options: componentOptions, component} = this.spec\n if (!id) {\n throw new SerializeError(\n '`id` is required for `component` structure item',\n options.path,\n options.index,\n ).withHelpUrl(HELP_URL.ID_REQUIRED)\n }\n\n if (!component) {\n throw new SerializeError(\n '`component` is required for `component` structure item',\n options.path,\n options.index,\n ).withHelpUrl(HELP_URL.ID_REQUIRED)\n }\n\n return {\n id: validateId(id, options.path, options.index),\n title,\n type: 'component',\n child,\n component,\n canHandleIntent: this.spec.canHandleIntent,\n options: componentOptions || {},\n menuItems: (this.spec.menuItems || []).map((item, i) =>\n maybeSerializeMenuItem(item, i, options.path),\n ),\n menuItemGroups: (this.spec.menuItemGroups || []).map((item, i) =>\n maybeSerializeMenuItemGroup(item, i, options.path),\n ),\n }\n }\n\n /** Clone component builder (allows for options overriding)\n * @param withSpec - component builder options\n * @returns cloned builder\n */\n clone(withSpec?: BuildableComponent): ComponentBuilder {\n const builder = new ComponentBuilder()\n builder.spec = {...this.spec, ...withSpec}\n return builder\n }\n}\n","import {uniqueId} from 'lodash'\nimport {type I18nTextRecord} from 'sanity'\n\nimport {type Divider, type Serializable} from './StructureNodes'\n\nexport class DividerBuilder implements Serializable<Divider> {\n protected spec: Divider\n\n constructor(spec?: Divider) {\n this.spec = {\n id: uniqueId('__divider__'),\n type: 'divider',\n ...spec,\n }\n }\n\n /** Set the title of the divider\n * @param title - the title of the divider\n * @returns divider builder based on title provided\n */\n title(title: string): DividerBuilder {\n return this.clone({\n title,\n })\n }\n\n /** Get the title of the divider\n * @returns the title of the divider\n */\n getTitle(): Divider['title'] {\n return this.spec.title\n }\n\n /** Set the i18n key and namespace used to populate the localized title.\n * @param i18n - the key and namespaced used to populate the localized title.\n * @returns divider builder based on i18n key and ns provided\n */\n i18n(i18n: I18nTextRecord<'title'>): DividerBuilder {\n return this.clone({\n i18n,\n })\n }\n\n /** Get i18n key and namespace used to populate the localized title\n * @returns the i18n key and namespace used to populate the localized title\n */\n getI18n(): I18nTextRecord<'title'> | undefined {\n return this.spec.i18n\n }\n\n /** Serialize the divider\n * @returns the serialized divider\n */\n serialize(): Divider {\n return {...this.spec}\n }\n\n /** Clone divider builder (allows for options overriding)\n * @param withSpec - divider builder options\n * @returns cloned builder\n */\n clone(withSpec?: Partial<Divider>): DividerBuilder {\n const builder = new DividerBuilder()\n builder.spec = {...this.spec, ...withSpec}\n return builder\n }\n}\n","import {type SanityClient} from '@sanity/client'\nimport {DEFAULT_STUDIO_CLIENT_OPTIONS, getPublishedId, type SourceClientOptions} from 'sanity'\n\nexport async function resolveTypeForDocument(\n getClient: (options: SourceClientOptions) => SanityClient,\n id: string,\n): Promise<string | undefined> {\n const query = '*[sanity::versionOf($publishedId)][0]._type'\n\n const type = await getClient(DEFAULT_STUDIO_CLIENT_OPTIONS).fetch(\n query,\n {publishedId: getPublishedId(id)},\n {tag: 'structure.resolve-type'},\n )\n\n return type\n}\n","import {kebabCase} from 'lodash'\n\nimport {HELP_URL, SerializeError} from '../SerializeError'\nimport {type Serializable, type SerializeOptions, type SerializePath} from '../StructureNodes'\nimport {type View} from '../types'\nimport {validateId} from '../util/validateId'\nimport {type ComponentViewBuilder} from './ComponentView'\nimport {type FormViewBuilder} from './FormView'\n\n/**\n * Interface for base view\n *\n * @public */\nexport interface BaseView {\n /** View id */\n id: string\n /** View Title */\n title: string\n /** View Icon */\n icon?: React.ComponentType | React.ReactNode\n}\n\n/**\n * Class for building generic views.\n *\n * @public\n */\nexport abstract class GenericViewBuilder<TView extends Partial<BaseView>, ConcreteImpl>\n implements Serializable<BaseView>\n{\n /** Generic view option object */\n protected spec: TView = {} as TView\n\n /** Set generic view ID\n * @param id - generic view ID\n * @returns generic view builder based on ID provided.\n */\n id(id: string): ConcreteImpl {\n return this.clone({id})\n }\n /** Get generic view ID\n * @returns generic view ID\n */\n getId(): TView['id'] {\n return this.spec.id\n }\n\n /** Set generic view title\n * @param title - generic view title\n * @returns generic view builder based on title provided and (if provided) its ID.\n */\n title(title: string): ConcreteImpl {\n return this.clone({title, id: this.spec.id || kebabCase(title)})\n }\n\n /** Get generic view title\n * @returns generic view title\n */\n getTitle(): TView['title'] {\n return this.spec.title\n }\n\n /** Set generic view icon\n * @param icon - generic view icon\n * @returns generic view builder based on icon provided.\n */\n icon(icon: React.ComponentType | React.ReactNode): ConcreteImpl {\n return this.clone({icon})\n }\n\n /** Get generic view icon\n * @returns generic view icon\n */\n getIcon(): TView['icon'] {\n return this.spec.icon\n }\n\n /** Serialize generic view\n * @param options - serialization options. See {@link SerializeOptions}\n * @returns generic view object based on path provided in options. See {@link BaseView}\n */\n serialize(options: SerializeOptions = {path: []}): BaseView {\n const {id, title, icon} = this.spec\n if (!id) {\n throw new SerializeError(\n '`id` is required for view item',\n options.path,\n options.index,\n ).withHelpUrl(HELP_URL.ID_REQUIRED)\n }\n\n if (!title) {\n throw new SerializeError(\n '`title` is required for view item',\n options.path,\n options.index,\n ).withHelpUrl(HELP_URL.TITLE_REQUIRED)\n }\n\n return {\n id: validateId(id, options.path, options.index),\n title,\n icon,\n }\n }\n\n /** Clone generic view builder (allows for options overriding)\n * @param withSpec - Partial generic view builder options. See {@link BaseView}\n * @returns Generic view builder.\n */\n abstract clone(withSpec?: Partial<BaseView>): ConcreteImpl\n}\n\nfunction isSerializable(view: BaseView | Serializable<BaseView>): view is Serializable<BaseView> {\n return typeof (view as Serializable<BaseView>).serialize === 'function'\n}\n\n/** @internal */\nexport function maybeSerializeView(\n item: View | Serializable<View>,\n index: number,\n path: SerializePath,\n): View {\n return isSerializable(item) ? item.serialize({path, index}) : item\n}\n\n/**\n * View builder. See {@link ComponentViewBuilder} and {@link FormViewBuilder}\n *\n * @public\n */\nexport type ViewBuilder = ComponentViewBuilder | FormViewBuilder\n","import {isRecord} from 'sanity'\n\nimport {HELP_URL, SerializeError} from '../SerializeError'\nimport {type SerializeOptions} from '../StructureNodes'\nimport {type UserViewComponent} from '../types'\nimport {type BaseView, GenericViewBuilder} from './View'\n\n/**\n * Interface for component views.\n *\n * @public */\nexport interface ComponentView<TOptions = Record<string, any>> extends BaseView {\n type: 'component'\n /** Component view components. See {@link UserViewComponent} */\n component: UserViewComponent\n /** Component view options */\n options: TOptions\n}\n\nconst isComponentSpec = (spec: unknown): spec is ComponentView =>\n isRecord(spec) && spec.type === 'component'\n\n/**\n * Class for building a component view.\n *\n * @public */\nexport class ComponentViewBuilder extends GenericViewBuilder<\n Partial<ComponentView>,\n ComponentViewBuilder\n> {\n /** Partial Component view option object. See {@link ComponentView} */\n protected spec: Partial<ComponentView>\n\n constructor(\n /**\n * Component view component or spec\n * @param componentOrSpec - user view component or partial component view. See {@link UserViewComponent} and {@link ComponentView}\n */\n componentOrSpec?: UserViewComponent | Partial<ComponentView>,\n ) {\n const spec = isComponentSpec(componentOrSpec) ? {...componentOrSpec} : {options: {}}\n\n super()\n this.spec = spec\n\n const userComponent =\n typeof componentOrSpec === 'function' ? componentOrSpec : this.spec.component\n\n if (userComponent) {\n // Because we're cloning, this'll return a new instance, so grab the spec from it\n this.spec = this.component(userComponent).spec\n }\n }\n\n /** Set view Component\n * @param component - component view component. See {@link UserViewComponent}\n * @returns component view builder based on component view provided. See {@link ComponentViewBuilder}\n */\n component(component: UserViewComponent): ComponentViewBuilder {\n return this.clone({component})\n }\n\n /** Get view Component\n * @returns Partial component view. See {@link ComponentView}\n */\n getComponent(): Partial<ComponentView>['component'] {\n return this.spec.component\n }\n\n /** Set view Component options\n * @param options - component view options\n * @returns component view builder based on options provided. See {@link ComponentViewBuilder}\n */\n options(options: {[key: string]: any}): ComponentViewBuilder {\n return this.clone({options})\n }\n\n /** Get view Component options\n * @returns component view options. See {@link ComponentView}\n */\n getOptions(): ComponentView['options'] {\n return this.spec.options || {}\n }\n\n /** Serialize view Component\n * @param options - serialization options. See {@link SerializeOptions}\n * @returns component view based on path provided in options. See {@link ComponentView}\n *\n */\n serialize(options: SerializeOptions = {path: []}): ComponentView {\n const base = super.serialize(options)\n\n const component = this.spec.component\n if (typeof component !== 'function') {\n throw new SerializeError(\n '`component` is required and must be a function for `component()` view item',\n options.path,\n options.index,\n ).withHelpUrl(HELP_URL.COMPONENT_REQUIRED)\n }\n\n return {\n ...base,\n component,\n options: this.spec.options || {},\n type: 'component',\n }\n }\n\n /** Clone Component view builder (allows for options overriding)\n * @param withSpec - partial for component view option. See {@link ComponentView}\n * @returns component view builder. See {@link ComponentViewBuilder}\n */\n clone(withSpec?: Partial<ComponentView>): ComponentViewBuilder {\n const builder = new ComponentViewBuilder()\n builder.spec = {...this.spec, ...withSpec}\n return builder\n }\n}\n","import {type SerializeOptions} from '../StructureNodes'\nimport {type BaseView, GenericViewBuilder} from './View'\n\n/**\n * Interface for form views.\n *\n * @public */\nexport interface FormView extends BaseView {\n type: 'form'\n}\n\n/**\n * Class for building a form view.\n *\n * @public */\nexport class FormViewBuilder extends GenericViewBuilder<Partial<BaseView>, FormViewBuilder> {\n /** Document list options. See {@link FormView} */\n protected spec: Partial<FormView>\n\n constructor(spec?: Partial<FormView>) {\n super()\n this.spec = {id: 'editor', title: 'Editor', ...(spec ? spec : {})}\n }\n\n /**\n * Serialize Form view builder\n * @param options - Serialize options. See {@link SerializeOptions}\n * @returns form view builder based on path provided in options. See {@link FormView}\n */\n serialize(options: SerializeOptions = {path: []}): FormView {\n return {\n ...super.serialize(options),\n type: 'form',\n }\n }\n\n /**\n * Clone Form view builder (allows for options overriding)\n * @param withSpec - Partial form view builder options. See {@link FormView}\n * @returns form view builder. See {@link FormViewBuilder}\n */\n clone(withSpec?: Partial<FormView>): FormViewBuilder {\n const builder = new FormViewBuilder()\n builder.spec = {...this.spec, ...withSpec}\n return builder\n }\n}\n","import {type UserViewComponent} from '../types'\nimport {type ComponentView, ComponentViewBuilder} from './ComponentView'\nimport {type FormView, FormViewBuilder} from './FormView'\n\nexport * from './ComponentView'\nexport * from './FormView'\nexport * from './View'\n\n/** @internal */\nexport const form = (spec?: Partial<FormView>): FormViewBuilder => new FormViewBuilder(spec)\n\n/** @internal */\nexport const component = (\n componentOrSpec?: UserViewComponent | Partial<ComponentView>,\n): ComponentViewBuilder => new ComponentViewBuilder(componentOrSpec)\n","import {type SchemaType} from '@sanity/types'\nimport {uniq} from 'lodash'\nimport {type I18nTextRecord} from 'sanity'\n\nimport {type ChildResolver} from './ChildResolver'\nimport {HELP_URL, SerializeError} from './SerializeError'\nimport {\n type Child,\n type DocumentNode,\n type EditorNode,\n type Serializable,\n type SerializeOptions,\n} from './StructureNodes'\nimport {type StructureContext, type View} from './types'\nimport {getStructureNodeId} from './util/getStructureNodeId'\nimport {resolveTypeForDocument} from './util/resolveTypeForDocument'\nimport {validateId} from './util/validateId'\nimport {form} from './views'\nimport {maybeSerializeView, type ViewBuilder} from './views/View'\n\nconst createDocumentChildResolver =\n ({resolveDocumentNode, getClient}: StructureContext): ChildResolver =>\n async (itemId, {params, path}) => {\n let type = params.type\n\n const parentPath = path.slice(0, path.length - 1)\n const currentSegment = path[path.length - 1]\n\n if (!type) {\n type = await resolveTypeForDocument(getClient, itemId)\n }\n\n if (!type) {\n throw new SerializeError(\n `Failed to resolve document, and no type provided in parameters.`,\n parentPath,\n currentSegment,\n )\n }\n\n return resolveDocumentNode({documentId: itemId, schemaType: type})\n }\n\n/**\n * Interface for options of Partial Documents. See {@link PartialDocumentNode}\n *\n * @public */\nexport interface DocumentOptions {\n /** Document Id */\n id: string\n /** Document Type */\n type: string\n /** Document Template */\n template?: string\n /** Template parameters */\n templateParameters?: Record<string, unknown>\n}\n\n/**\n * Interface for partial document (focused on the document pane)\n *\n * @public */\nexport interface PartialDocumentNode {\n /** Document Id */\n id?: string\n /** Document title */\n title?: string\n /** I18n key and namespace used to populate the localized title */\n i18n?: I18nTextRecord<'title'>\n /** Document children of type {@link Child} */\n child?: Child\n /**\n * Views for the document pane. See {@link ViewBuilder} and {@link View}\n */\n views?: (View | ViewBuilder)[]\n /**\n * Document options. See {@link DocumentOptions}\n */\n options?: Partial<DocumentOptions>\n}\n\n/**\n * A `DocumentBuilder` is used to build a document node.\n *\n * @public */\nexport class DocumentBuilder implements Serializable<DocumentNode> {\n /** Component builder option object See {@link PartialDocumentNode} */\n protected spec: PartialDocumentNode\n\n protected _context: StructureContext\n\n constructor(\n /**\n * Structure context. See {@link StructureContext}\n */\n _context: StructureContext,\n spec?: PartialDocumentNode,\n ) {\n this._context = _context\n this.spec = spec ? spec : {}\n }\n\n /** Set Document Builder ID\n * @param id - document builder ID\n * @returns document builder based on ID provided. See {@link DocumentBuilder}\n */\n id(id: string): DocumentBuilder {\n return this.clone({id})\n }\n\n /** Get Document Builder ID\n * @returns document ID. See {@link PartialDocumentNode}\n */\n getId(): PartialDocumentNode['id'] {\n return this.spec.id\n }\n\n /** Set Document title\n * @param title - document title\n * @returns document builder based on title provided (and ID). See {@link DocumentBuilder}\n */\n title(title: string): DocumentBuilder {\n return this.clone({title, id: getStructureNodeId(title, this.spec.id)})\n }\n\n /** Get Document title\n * @returns document title. See {@link PartialDocumentNode}\n */\n getTitle(): PartialDocumentNode['title'] {\n return this.spec.title\n }\n\n /** Set the i18n key and namespace used to populate the localized title.\n * @param i18n - the key and namespaced used to populate the localized title.\n * @returns component builder based on i18n key and ns provided\n */\n i18n(i18n: I18nTextRecord<'title'>): DocumentBuilder {\n return this.clone({i18n})\n }\n\n /** Get i18n key and namespace used to populate the localized title\n * @returns the i18n key and namespace used to populate the localized title\n */\n getI18n(): I18nTextRecord<'title'> | undefined {\n return this.spec.i18n\n }\n\n /** Set Document child\n * @param child - document child\n * @returns document builder based on child provided. See {@link DocumentBuilder}\n */\n child(child: Child): DocumentBuilder {\n return this.clone({child})\n }\n\n /** Get Document child\n * @returns document child. See {@link PartialDocumentNode}\n */\n getChild(): PartialDocumentNode['child'] {\n return this.spec.child\n }\n\n /** Set Document ID\n * @param documentId - document ID\n * @returns document builder with document based on ID provided. See {@link DocumentBuilder}\n */\n documentId(documentId: string): DocumentBuilder {\n // Let's try to be a bit helpful and assign an ID from document ID if none is specified\n const paneId = this.spec.id || documentId\n return this.clone({\n id: paneId,\n options: {\n ...this.spec.options,\n id: documentId,\n },\n })\n }\n\n /** Get Document ID\n * @returns document ID. See {@link DocumentOptions}\n */\n getDocumentId(): Partial<DocumentOptions>['id'] {\n return this.spec.options?.id\n }\n\n /** Set Document Type\n * @param documentType - document type\n * @returns document builder with document based on type provided. See {@link DocumentBuilder}\n */\n schemaType(documentType: SchemaType | string): DocumentBuilder {\n return this.clone({\n options: {\n ...this.spec.options,\n type: typeof documentType === 'string' ? documentType : documentType.name,\n },\n })\n }\n\n /** Get Document Type\n * @returns document type. See {@link DocumentOptions}\n */\n getSchemaType(): Partial<DocumentOptions>['type'] {\n return this.spec.options?.type\n }\n\n /** Set Document Template\n * @param templateId - document template ID\n * @param parameters - document template parameters\n * @returns document builder with document based on template provided. See {@link DocumentBuilder}\n */\n initialValueTemplate(templateId: string, parameters?: Record<string, unknown>): DocumentBuilder {\n return this.clone({\n options: {\n ...this.spec.options,\n template: templateId,\n templateParameters: parameters,\n },\n })\n }\n\n /** Get Document Template\n * @returns document template. See {@link DocumentOptions}\n */\n getInitialValueTemplate(): Partial<DocumentOptions>['template'] {\n return this.spec.options?.template\n }\n\n /** Get Document's initial value Template parameters\n * @returns document template parameters. See {@link DocumentOptions}\n */\n getInitialValueTemplateParameters(): Partial<DocumentOptions>['templateParameters'] {\n return this.spec.options?.templateParameters\n }\n\n /** Set Document views\n * @param views - document views. See {@link ViewBuilder} and {@link View}\n * @returns document builder with document based on views provided. See {@link DocumentBuilder}\n */\n views(views: (View | ViewBuilder)[]): DocumentBuilder {\n return this.clone({views})\n }\n\n /** Get Document views\n * @returns document views. See {@link ViewBuilder} and {@link View}\n */\n getViews(): (View | ViewBuilder)[] {\n return this.spec.views || []\n }\n\n /** Serialize Document builder\n * @param options - serialization options. See {@link SerializeOptions}\n * @returns document node based on path, index and hint provided in options. See {@link DocumentNode}\n */\n serialize({path = [], index, hint}: SerializeOptions = {path: []}): DocumentNode {\n const urlId = path[index || path.length - 1]\n\n // Try to grab document ID / editor ID from URL if not defined\n const id = this.spec.id || (urlId && `${urlId}`) || ''\n const options: Partial<DocumentOptions> = {\n id,\n type: undefined,\n template: undefined,\n templateParameters: undefined,\n ...this.spec.options,\n }\n\n if (typeof id !== 'string' || !id) {\n throw new SerializeError(\n '`id` is required for document nodes',\n path,\n index,\n hint,\n ).withHelpUrl(HELP_URL.ID_REQUIRED)\n }\n\n if (!options || !options.id) {\n throw new SerializeError(\n 'document id (`id`) is required for document nodes',\n path,\n id,\n hint,\n ).withHelpUrl(HELP_URL.DOCUMENT_ID_REQUIRED)\n }\n\n if (!options || !options.type) {\n throw new SerializeError(\n 'document type (`schemaType`) is required for document nodes',\n path,\n id,\n