UNPKG

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 108 kB
{"version":3,"file":"index3.mjs","sources":["../../src/structure/components/paneRouter/PaneRouterProvider.tsx","../../src/structure/structureResolvers/PaneResolutionError.ts","../../src/structure/structureResolvers/assignId.ts","../../src/structure/structureResolvers/createPaneResolver.ts","../../src/structure/structureResolvers/memoBind.ts","../../src/structure/structureResolvers/resolveIntent.ts","../../src/structure/structureResolvers/createResolvedPaneNodeStream.ts","../../src/structure/structureResolvers/useResolvedPanes.ts","../../src/structure/components/structureTool/intentResolver/utils.ts","../../src/structure/components/structureTool/intentResolver/IntentResolver.tsx","../../src/structure/components/structureTool/StructureError.tsx","../../src/structure/panes/unknown/UnknownPaneType.tsx","../../src/structure/panes/StructureToolPane.tsx","../../src/structure/components/structureTool/NoDocumentTypesScreen.tsx","../../src/structure/components/structureTool/StructureTitle.tsx","../../src/structure/components/structureTool/StructureTool.tsx","../../src/structure/components/structureTool/StructureToolBoundary.tsx"],"sourcesContent":["import {toString as pathToString} from '@sanity/util/paths'\nimport {omit} from 'lodash'\nimport {type ReactNode, useCallback, useMemo} from 'react'\nimport {PaneRouterContext} from 'sanity/_singletons'\nimport {useRouter, useRouterState} from 'sanity/router'\n\nimport {type RouterPaneGroup, type RouterPanes, type RouterPaneSibling} from '../../types'\nimport {usePaneLayout} from '../pane/usePaneLayout'\nimport {BackLink} from './BackLink'\nimport {ChildLink} from './ChildLink'\nimport {ParameterizedLink} from './ParameterizedLink'\nimport {ReferenceChildLink} from './ReferenceChildLink'\nimport {type PaneRouterContextValue} from './types'\n\nconst emptyArray: never[] = []\n\n/**\n * @internal\n */\nexport function PaneRouterProvider(props: {\n children: ReactNode\n flatIndex: number\n index: number\n params: Record<string, string | undefined>\n payload: unknown\n siblingIndex: number\n}) {\n const {children, flatIndex, index, params, payload, siblingIndex} = props\n const {navigate, navigateIntent, resolvePathFromState} = useRouter()\n const routerState = useRouterState()\n const {panes, expand} = usePaneLayout()\n const routerPaneGroups: RouterPaneGroup[] = useMemo(\n () => (routerState?.panes || emptyArray) as RouterPanes,\n [routerState?.panes],\n )\n const lastPane = useMemo(() => panes?.[panes.length - 2], [panes])\n\n const groupIndex = index - 1\n\n const createNextRouterState = useCallback(\n (modifier: (siblings: RouterPaneGroup, item: RouterPaneSibling) => RouterPaneGroup) => {\n const currentGroup = routerPaneGroups[groupIndex] || []\n const currentItem = currentGroup[siblingIndex]\n const nextGroup = modifier(currentGroup, currentItem)\n const nextPanes = [\n ...routerPaneGroups.slice(0, groupIndex),\n nextGroup,\n ...routerPaneGroups.slice(groupIndex + 1),\n ]\n const nextRouterState = {...(routerState || {}), panes: nextPanes}\n\n return nextRouterState\n },\n [groupIndex, routerPaneGroups, routerState, siblingIndex],\n )\n\n const modifyCurrentGroup = useCallback(\n (modifier: (siblings: RouterPaneGroup, item: RouterPaneSibling) => RouterPaneGroup) => {\n const nextRouterState = createNextRouterState(modifier)\n setTimeout(() => navigate(nextRouterState), 0)\n return nextRouterState\n },\n [createNextRouterState, navigate],\n )\n\n const createPathWithParams: PaneRouterContextValue['createPathWithParams'] = useCallback(\n (nextParams) => {\n const nextRouterState = createNextRouterState((siblings, item) => [\n ...siblings.slice(0, siblingIndex),\n {...item, params: nextParams},\n ...siblings.slice(siblingIndex + 1),\n ])\n\n return resolvePathFromState(nextRouterState)\n },\n [createNextRouterState, resolvePathFromState, siblingIndex],\n )\n\n const setPayload: PaneRouterContextValue['setPayload'] = useCallback(\n (nextPayload) => {\n modifyCurrentGroup((siblings, item) => [\n ...siblings.slice(0, siblingIndex),\n {...item, payload: nextPayload},\n ...siblings.slice(siblingIndex + 1),\n ])\n },\n [modifyCurrentGroup, siblingIndex],\n )\n\n const setParams: PaneRouterContextValue['setParams'] = useCallback(\n (nextParams) => {\n modifyCurrentGroup((siblings, item) => [\n ...siblings.slice(0, siblingIndex),\n {...item, params: nextParams},\n ...siblings.slice(siblingIndex + 1),\n ])\n },\n [modifyCurrentGroup, siblingIndex],\n )\n\n const handleEditReference: PaneRouterContextValue['handleEditReference'] = useCallback(\n ({id, parentRefPath, type, template, version}) => {\n navigate({\n panes: [\n ...routerPaneGroups.slice(0, groupIndex + 1),\n [\n {\n id,\n params: {\n template: template.id,\n parentRefPath: pathToString(parentRefPath),\n type,\n version,\n },\n payload: template.params,\n },\n ],\n ],\n })\n },\n [groupIndex, navigate, routerPaneGroups],\n )\n\n const ctx: PaneRouterContextValue = useMemo(\n () => ({\n // Zero-based index (position) of pane, visually\n index: flatIndex,\n\n // Zero-based index of pane group (within URL structure)\n groupIndex,\n\n // Zero-based index of pane within sibling group\n siblingIndex,\n\n // Payload of the current pane\n payload,\n\n // Params of the current pane\n params,\n\n // Whether or not the pane has any siblings (within the same group)\n hasGroupSiblings: routerPaneGroups[groupIndex]\n ? routerPaneGroups[groupIndex].length > 1\n : false,\n\n // The length of the current group\n groupLength: routerPaneGroups[groupIndex] ? routerPaneGroups[groupIndex].length : 0,\n\n // Current router state for the \"panes\" property\n routerPanesState: routerPaneGroups,\n\n // Curried StateLink that passes the correct state automatically\n ChildLink,\n\n // Curried StateLink that pops off the last pane group\n // Only pass if this is not the first pane\n BackLink: flatIndex ? BackLink : undefined,\n\n // A specialized `ChildLink` that takes in the needed props to open a\n // referenced document to the right\n ReferenceChildLink,\n\n // Similar to `ReferenceChildLink` expect without the wrapping component\n handleEditReference,\n\n // Curried StateLink that passed the correct state, but merges params/payload\n ParameterizedLink,\n\n // Replaces the current pane with a new one\n replaceCurrent: (opts = {}): void => {\n modifyCurrentGroup(() => [\n {id: opts.id || '', payload: opts.payload, params: opts.params || {}},\n ])\n },\n\n // Removes the current pane from the group\n closeCurrent: (): void => {\n modifyCurrentGroup((siblings, item) =>\n siblings.length > 1 ? siblings.filter((sibling) => sibling !== item) : siblings,\n )\n },\n\n // Removes all panes to the right including current\n closeCurrentAndAfter: (expandLast = true): void => {\n if (expandLast && lastPane) {\n expand(lastPane.element)\n }\n navigate({\n panes: [...routerPaneGroups.slice(0, groupIndex)],\n })\n },\n\n // Duplicate the current pane, with optional overrides for payload, parameters\n duplicateCurrent: (options): void => {\n modifyCurrentGroup((siblings, item) => {\n const duplicatedItem = {\n ...item,\n payload: options?.payload || item.payload,\n params: options?.params || item.params,\n }\n\n return [\n ...siblings.slice(0, siblingIndex),\n duplicatedItem,\n ...siblings.slice(siblingIndex),\n ]\n })\n },\n\n // Set the view for the current pane\n setView: (viewId) => {\n const restParams = omit(params, 'view')\n return setParams(viewId ? {...restParams, view: viewId} : restParams)\n },\n\n // Set the parameters for the current pane\n setParams,\n\n // Set the payload for the current pane\n setPayload,\n\n // A function that returns a path with the given parameters\n createPathWithParams,\n\n // Proxied navigation to a given intent. Consider just exposing `router` instead?\n navigateIntent,\n }),\n [\n flatIndex,\n groupIndex,\n siblingIndex,\n payload,\n params,\n routerPaneGroups,\n handleEditReference,\n setParams,\n setPayload,\n createPathWithParams,\n navigateIntent,\n modifyCurrentGroup,\n lastPane,\n navigate,\n expand,\n ],\n )\n\n return <PaneRouterContext.Provider value={ctx}>{children}</PaneRouterContext.Provider>\n}\n","import {type RouterPaneSiblingContext} from '../types'\n\nexport interface PaneResolutionErrorOptions {\n message: string\n context?: RouterPaneSiblingContext\n helpId?: string\n cause?: Error\n}\n\n/**\n * An error thrown during pane resolving. This error is meant to be bubbled up\n * through react and handled in an error boundary. It includes a `cause`\n * property which is the original error caught\n */\nexport class PaneResolutionError extends Error {\n cause: Error | undefined\n context: RouterPaneSiblingContext | undefined\n helpId: string | undefined\n\n constructor({message, context, helpId, cause}: PaneResolutionErrorOptions) {\n super(message)\n this.name = 'PaneResolutionError'\n this.context = context\n this.helpId = helpId\n this.cause = cause\n }\n}\n","import {nanoid} from 'nanoid'\n\n// `WeakMap`s require the first type param to extend `object`\n// eslint-disable-next-line @typescript-eslint/ban-types\nconst randomIdCache = new WeakMap<object, string>()\n\n/**\n * given an object, this function randomly generates an ID and returns it. this\n * result is then saved in a WeakMap so subsequent requests for the same object\n * will receive the same ID\n */\n// eslint-disable-next-line @typescript-eslint/ban-types\nexport function assignId(obj: object): string {\n const cachedValue = randomIdCache.get(obj)\n if (cachedValue) return cachedValue\n\n const id = nanoid()\n randomIdCache.set(obj, id)\n return id\n}\n","import {from, isObservable, type Observable, of as observableOf} from 'rxjs'\nimport {publishReplay, refCount, switchMap} from 'rxjs/operators'\nimport {isRecord} from 'sanity'\n\nimport {type PaneNode, type RouterPaneSiblingContext, type UnresolvedPaneNode} from '../types'\nimport {PaneResolutionError} from './PaneResolutionError'\n\ninterface Serializable {\n serialize: (...args: never[]) => unknown\n}\n\nconst isPromise = (thing: any): thing is PromiseLike<unknown> => {\n return !!thing && typeof thing?.then === 'function'\n}\nconst isSerializable = (thing: unknown): thing is Serializable => {\n if (!isRecord(thing)) return false\n return typeof thing.serialize === 'function'\n}\n\n/**\n * The signature of the function used to take an `UnresolvedPaneNode` and turn\n * it into an `Observable<PaneNode>`.\n */\nexport type PaneResolver = (\n unresolvedPane: UnresolvedPaneNode | undefined,\n context: RouterPaneSiblingContext,\n flatIndex: number,\n) => Observable<PaneNode>\n\nexport type PaneResolverMiddleware = (paneResolveFn: PaneResolver) => PaneResolver\n\nconst rethrowWithPaneResolutionErrors: PaneResolverMiddleware =\n (next) => (unresolvedPane, context, flatIndex) => {\n try {\n return next(unresolvedPane, context, flatIndex)\n } catch (e) {\n // re-throw errors that are already `PaneResolutionError`s\n if (e instanceof PaneResolutionError) {\n throw e\n }\n\n // anything else, wrap with `PaneResolutionError` and set the underlying\n // error as a the `cause`\n throw new PaneResolutionError({\n message: typeof e?.message === 'string' ? e.message : '',\n context,\n cause: e,\n })\n }\n }\n\nconst wrapWithPublishReplay: PaneResolverMiddleware =\n (next) =>\n (...args) => {\n return next(...args).pipe(\n // need to add publishReplay + refCount to ensure new subscribers always\n // get an emission. without this, memoized observables may get stuck\n // waiting for their first emissions resulting in a loading pane\n publishReplay(1),\n refCount(),\n )\n }\n\nexport function createPaneResolver(middleware: PaneResolverMiddleware): PaneResolver {\n // note: this API includes a middleware/wrapper function because the function\n // is recursive. we want to call the wrapped version of the function always\n // (even inside of nested calls) so the identifier invoked for the recursion\n // should be the wrapped version\n const resolvePane = rethrowWithPaneResolutionErrors(\n wrapWithPublishReplay(\n middleware((unresolvedPane, context, flatIndex) => {\n if (!unresolvedPane) {\n throw new PaneResolutionError({\n message: 'Pane returned no child',\n context,\n helpId: 'structure-item-returned-no-child',\n })\n }\n\n if (isPromise(unresolvedPane) || isObservable(unresolvedPane)) {\n return from(unresolvedPane).pipe(\n switchMap((result) => resolvePane(result, context, flatIndex)),\n )\n }\n\n if (isSerializable(unresolvedPane)) {\n return resolvePane(unresolvedPane.serialize(context), context, flatIndex)\n }\n\n if (typeof unresolvedPane === 'function') {\n return resolvePane(unresolvedPane(context.id, context), context, flatIndex)\n }\n\n return observableOf(unresolvedPane)\n }),\n ),\n )\n\n return resolvePane\n}\n","/* eslint-disable @typescript-eslint/ban-types */\n\n// `WeakMap`s require the first type param to extend `object`\nconst bindCache = new WeakMap<object, Map<string, Function>>()\n\n/**\n * An alternative to `obj.method.bind(obj)` that utilizes a weakmap to return\n * the same memory reference for sequent binds.\n */\nexport function memoBind<\n T extends object,\n K extends keyof {[P in keyof T]: T[P] extends Function ? T[P] : never},\n>(obj: T, methodKey: K): T[K]\nexport function memoBind(obj: Record<string, unknown>, methodKey: string): Function {\n const boundMethods = bindCache.get(obj) || new Map<string, Function>()\n if (boundMethods) {\n const bound = boundMethods.get(methodKey)\n if (bound) return bound\n }\n\n const method = obj[methodKey]\n\n if (typeof method !== 'function') {\n throw new Error(\n `Expected property \\`${methodKey}\\` to be a function but got ${typeof method} instead.`,\n )\n }\n\n const bound = method.bind(obj)\n boundMethods.set(methodKey, bound)\n bindCache.set(obj, boundMethods)\n\n return bound\n}\n","import {omit} from 'lodash'\nimport {firstValueFrom, type Observable} from 'rxjs'\n\nimport {type StructureContext} from '../structureBuilder'\nimport {\n type PaneNode,\n type RouterPanes,\n type RouterPaneSiblingContext,\n type UnresolvedPaneNode,\n} from '../types'\nimport {assignId} from './assignId'\nimport {createPaneResolver, type PaneResolverMiddleware} from './createPaneResolver'\nimport {memoBind} from './memoBind'\n\ninterface TraverseOptions {\n unresolvedPane: UnresolvedPaneNode | undefined\n intent: string\n params: {type: string; id: string; [key: string]: string | undefined}\n payload: unknown\n parent: PaneNode | null\n path: string[]\n currentId: string\n flatIndex: number\n levelIndex: number\n structureContext: StructureContext\n}\n\nexport interface ResolveIntentOptions {\n rootPaneNode?: UnresolvedPaneNode\n intent: string\n params: {type: string; id: string; [key: string]: string | undefined}\n payload: unknown\n structureContext: StructureContext\n}\n\n/**\n * Resolves an intent request using breadth first search. If a match is not\n * found, the intent will resolve to the fallback editor.\n *\n * A match is found if:\n * 1. the `PaneNode` is of type `document` and the its ID matches the intent ID\n * 2. the `PaneNode` is of type `documentList` and the `schemaTypeName` matches\n * 3. the `PaneNode`'s `canHandleIntent` method returns true\n *\n * If a `PaneNode` of type `list` is found, it will be searched for a match.\n *\n * @see PaneNode\n */\nexport async function resolveIntent(options: ResolveIntentOptions): Promise<RouterPanes> {\n const resolvedPaneCache = new Map<string, Observable<PaneNode>>()\n\n // this is a simple version of the memoizer in `createResolvedPaneNodeStream`\n const memoize: PaneResolverMiddleware = (nextFn) => (unresolvedPane, context, flatIndex) => {\n const key = unresolvedPane && `${assignId(unresolvedPane)}-${context.path.join('__')}`\n const cachedResolvedPane = key && resolvedPaneCache.get(key)\n if (cachedResolvedPane) return cachedResolvedPane\n\n const result = nextFn(unresolvedPane, context, flatIndex)\n if (key) resolvedPaneCache.set(key, result)\n return result\n }\n\n const resolvePane = createPaneResolver(memoize)\n\n const fallbackEditorPanes: RouterPanes = [\n [\n {\n id: `__edit__${options.params.id}`,\n params: {...omit(options.params, ['id']), type: options.params.type},\n payload: options.payload,\n },\n ],\n ]\n\n async function traverse({\n currentId,\n flatIndex,\n intent,\n params,\n parent,\n path,\n payload,\n unresolvedPane,\n levelIndex,\n structureContext,\n }: TraverseOptions): Promise<\n Array<{panes: RouterPanes; depthIndex: number; levelIndex: number}>\n > {\n if (!unresolvedPane) return []\n\n const {id: targetId, type: schemaTypeName, ...otherParams} = params\n const context: RouterPaneSiblingContext = {\n id: currentId,\n splitIndex: 0,\n parent,\n path,\n index: flatIndex,\n params: {},\n payload: undefined,\n structureContext,\n }\n const resolvedPane = await firstValueFrom(resolvePane(unresolvedPane, context, flatIndex))\n\n // if the resolved pane is a document pane and the pane's ID matches then\n // resolve the intent to the current path\n if (resolvedPane.type === 'document' && resolvedPane.id === targetId) {\n return [\n {\n panes: [\n ...path.slice(0, path.length - 1).map((i) => [{id: i}]),\n [{id: targetId, params: otherParams, payload}],\n ],\n depthIndex: path.length,\n levelIndex,\n },\n ]\n }\n\n // NOTE: if you update this logic, please also update the similar handler in\n // `getIntentState.ts`\n if (\n // if the resolve pane's `canHandleIntent` returns true, then resolve\n resolvedPane.canHandleIntent?.(intent, params, {\n pane: resolvedPane,\n index: flatIndex,\n }) ||\n // if the pane's `canHandleIntent` did not return true, then match against\n // this default case. we will resolve the intent if:\n (resolvedPane.type === 'documentList' &&\n // 1. the schema type matches (this required for the document to render)\n resolvedPane.schemaTypeName === schemaTypeName &&\n // 2. the filter is the default filter.\n //\n // NOTE: this case is to prevent false positive matches where the user\n // has configured a more specific filter for a particular type. In that\n // case, the user can implement their own `canHandleIntent` function\n resolvedPane.options.filter === '_type == $type')\n ) {\n return [\n {\n panes: [\n // map the current path to router panes\n ...path.map((id) => [{id}]),\n // then augment with the intents IDs and params\n [{id: params.id, params: otherParams, payload}],\n ],\n depthIndex: path.length,\n levelIndex,\n },\n ]\n }\n\n if (resolvedPane.type === 'list' && resolvedPane.child && resolvedPane.items) {\n return (\n await Promise.all(\n resolvedPane.items.map((item, nextLevelIndex) => {\n if (item.type === 'divider') return Promise.resolve([])\n\n return traverse({\n currentId: item._id || item.id,\n flatIndex: flatIndex + 1,\n intent,\n params,\n parent: resolvedPane,\n path: [...path, item.id],\n payload,\n unresolvedPane:\n typeof resolvedPane.child === 'function'\n ? memoBind(resolvedPane, 'child')\n : resolvedPane.child,\n levelIndex: nextLevelIndex,\n structureContext,\n })\n }),\n )\n ).flat()\n }\n\n return []\n }\n\n const matchingPanes = await traverse({\n currentId: 'root',\n flatIndex: 0,\n levelIndex: 0,\n intent: options.intent,\n params: options.params,\n parent: null,\n path: [],\n payload: options.payload,\n unresolvedPane: options.rootPaneNode,\n structureContext: options.structureContext,\n })\n\n const closestPaneToRoot = matchingPanes.sort((a, b) => {\n // break ties with the level index\n if (a.depthIndex === b.depthIndex) return a.levelIndex - b.levelIndex\n return a.depthIndex - b.depthIndex\n })[0]\n\n if (closestPaneToRoot) {\n return closestPaneToRoot.panes\n }\n\n return fallbackEditorPanes\n}\n","import {generateHelpUrl} from '@sanity/generate-help-url'\nimport {isEqual} from 'lodash'\nimport {concat, NEVER, type Observable, of as observableOf} from 'rxjs'\nimport {distinctUntilChanged, map, pairwise, scan, startWith, switchMap} from 'rxjs/operators'\n\nimport {type StructureContext} from '../structureBuilder'\nimport {\n type DocumentPaneNode,\n type PaneNode,\n type PaneNodeResolver,\n type RouterPanes,\n type RouterPaneSibling,\n type RouterPaneSiblingContext,\n type UnresolvedPaneNode,\n} from '../types'\nimport {assignId} from './assignId'\nimport {\n createPaneResolver,\n type PaneResolver,\n type PaneResolverMiddleware,\n} from './createPaneResolver'\nimport {memoBind} from './memoBind'\nimport {PaneResolutionError} from './PaneResolutionError'\n\n/**\n * the fallback editor child that is implicitly inserted into the structure tree\n * if the id starts with `__edit__`\n */\nconst fallbackEditorChild: PaneNodeResolver = (nodeId, context): DocumentPaneNode => {\n const id = nodeId.replace(/^__edit__/, '')\n const {\n params,\n payload,\n structureContext: {resolveDocumentNode},\n } = context\n const {type, template} = params\n\n if (!type) {\n throw new Error(\n `Document type for document with ID ${id} was not provided in the router params.`,\n )\n }\n\n let defaultDocumentBuilder = resolveDocumentNode({schemaType: type, documentId: id}).id('editor')\n\n if (template) {\n defaultDocumentBuilder = defaultDocumentBuilder.initialValueTemplate(\n template,\n payload as {[key: string]: unknown},\n )\n }\n\n return defaultDocumentBuilder.serialize() as DocumentPaneNode\n}\n\n/**\n * takes in a `RouterPaneSiblingContext` and returns a normalized string\n * representation that can be used for comparisons\n */\nfunction hashContext(context: RouterPaneSiblingContext): string {\n return `contextHash(${JSON.stringify({\n id: context.id,\n parentId: parent && assignId(parent),\n path: context.path,\n index: context.index,\n splitIndex: context.splitIndex,\n serializeOptionsIndex: context.serializeOptions?.index,\n serializeOptionsPath: context.serializeOptions?.path,\n })})`\n}\n\n/**\n * takes in `ResolvedPaneMeta` and returns a normalized string representation\n * that can be used for comparisons\n */\nconst hashResolvedPaneMeta = (meta: ResolvedPaneMeta): string => {\n const normalized = {\n type: meta.type,\n id: meta.routerPaneSibling.id,\n params: meta.routerPaneSibling.params || {},\n payload: meta.routerPaneSibling.payload || null,\n flatIndex: meta.flatIndex,\n groupIndex: meta.groupIndex,\n siblingIndex: meta.siblingIndex,\n path: meta.path,\n paneNode: meta.type === 'resolvedMeta' ? assignId(meta.paneNode) : null,\n }\n\n return `metaHash(${JSON.stringify(normalized)})`\n}\n\n/**\n * Represents one flattened \"router pane\", including the source group and\n * sibling indexes.\n *\n * @see RouterPanes\n */\ninterface FlattenedRouterPane {\n routerPaneSibling: RouterPaneSibling\n flatIndex: number\n groupIndex: number\n siblingIndex: number\n}\n\n/**\n * The state of the accumulator used to store and manage memo cache state\n */\ninterface CacheState {\n /**\n * Holds the memoization results keyed by a combination of `assignId` and a\n * context hash.\n */\n resolvedPaneCache: Map<string, Observable<PaneNode>>\n /**\n * Acts as a dictionary that stores cache keys by their flat index. This is\n * used to clean up the cache between different branches in the pane\n * structure.\n *\n * @see createResolvedPaneNodeStream look inside the `scan` where `wrapFn` is\n * defined\n */\n cacheKeysByFlatIndex: Array<Set<string>>\n /**\n * The resulting memoized `PaneResolver` function. This function closes over\n * the `resolvedPaneCache`.\n */\n resolvePane: PaneResolver\n flattenedRouterPanes: FlattenedRouterPane[]\n}\n\nexport interface CreateResolvedPaneNodeStreamOptions {\n /**\n * an input stream of `RouterPanes`\n * @see RouterPanes\n */\n routerPanesStream: Observable<RouterPanes>\n /**\n * any `UnresolvedPaneNode` (could be an observable, promise, pane resolver etc)\n */\n rootPaneNode: UnresolvedPaneNode\n /** used primarily for testing */\n initialCacheState?: CacheState\n\n structureContext: StructureContext\n}\n\n/**\n * The result of pane resolving\n */\nexport type ResolvedPaneMeta = {\n groupIndex: number\n siblingIndex: number\n flatIndex: number\n routerPaneSibling: RouterPaneSibling\n path: string[]\n} & ({type: 'loading'; paneNode: null} | {type: 'resolvedMeta'; paneNode: PaneNode})\n\ninterface ResolvePaneTreeOptions {\n resolvePane: PaneResolver\n flattenedRouterPanes: FlattenedRouterPane[]\n unresolvedPane: UnresolvedPaneNode | undefined\n parent: PaneNode | null\n path: string[]\n structureContext: StructureContext\n}\n\n/**\n * A recursive pane resolving function. Starts at one unresolved pane node and\n * continues until there is no more flattened router panes that can be used as\n * input to the unresolved panes.\n */\nfunction resolvePaneTree({\n unresolvedPane,\n flattenedRouterPanes,\n parent,\n path,\n resolvePane,\n structureContext,\n}: ResolvePaneTreeOptions): Observable<ResolvedPaneMeta[]> {\n const [current, ...rest] = flattenedRouterPanes\n const next = rest[0] as FlattenedRouterPane | undefined\n\n const context: RouterPaneSiblingContext = {\n id: current.routerPaneSibling.id,\n splitIndex: current.siblingIndex,\n parent,\n path: [...path, current.routerPaneSibling.id],\n index: current.flatIndex,\n params: current.routerPaneSibling.params || {},\n payload: current.routerPaneSibling.payload,\n structureContext,\n }\n\n try {\n return resolvePane(unresolvedPane, context, current.flatIndex).pipe(\n // this switch map receives a resolved pane\n switchMap((paneNode) => {\n // we can create a `resolvedMeta` type using it\n const resolvedPaneMeta: ResolvedPaneMeta = {\n type: 'resolvedMeta',\n ...current,\n paneNode: paneNode,\n path: context.path,\n }\n\n // for the other unresolved panes, we can create \"loading panes\"\n const loadingPanes = rest.map((i, restIndex) => {\n const loadingPanePath = [\n ...context.path,\n ...rest.slice(restIndex).map((_, currentIndex) => `[${i.flatIndex + currentIndex}]`),\n ]\n\n const loadingPane: ResolvedPaneMeta = {\n type: 'loading',\n path: loadingPanePath,\n paneNode: null,\n ...i,\n }\n\n return loadingPane\n })\n\n if (!rest.length) {\n return observableOf([resolvedPaneMeta])\n }\n\n let nextStream\n\n if (\n // the fallback editor case\n next?.routerPaneSibling.id.startsWith('__edit__')\n ) {\n nextStream = resolvePaneTree({\n unresolvedPane: fallbackEditorChild,\n flattenedRouterPanes: rest,\n parent,\n path: context.path,\n resolvePane,\n structureContext,\n })\n } else if (current.groupIndex === next?.groupIndex) {\n // if the next flattened router pane has the same group index as the\n // current flattened router pane, then the next flattened router pane\n // belongs to the same group (i.e. it is a split pane)\n nextStream = resolvePaneTree({\n unresolvedPane,\n flattenedRouterPanes: rest,\n parent,\n path,\n resolvePane,\n structureContext,\n })\n } else {\n // normal children resolving\n nextStream = resolvePaneTree({\n unresolvedPane:\n typeof paneNode.child === 'function'\n ? (memoBind(paneNode, 'child') as PaneNodeResolver)\n : paneNode.child,\n flattenedRouterPanes: rest,\n parent: paneNode,\n path: context.path,\n resolvePane,\n structureContext,\n })\n }\n\n return concat(\n // we emit the loading panes first in a concat (this emits immediately)\n observableOf([resolvedPaneMeta, ...loadingPanes]),\n // then whenever the next stream is done, the results will be combined.\n nextStream.pipe(map((nextResolvedPanes) => [resolvedPaneMeta, ...nextResolvedPanes])),\n )\n }),\n )\n } catch (e) {\n if (e instanceof PaneResolutionError) {\n if (e.context) {\n console.warn(\n `Pane resolution error at index ${e.context.index}${\n e.context.splitIndex > 0 ? ` for split pane index ${e.context.splitIndex}` : ''\n }: ${e.message}${e.helpId ? ` - see ${generateHelpUrl(e.helpId)}` : ''}`,\n e,\n )\n }\n\n if (e.helpId === 'structure-item-returned-no-child') {\n // returning an observable of an empty array will remove loading panes\n // note: this one intentionally does not throw\n return observableOf([])\n }\n }\n\n throw e\n }\n}\n\n/**\n * Takes in a stream of `RouterPanes` and an unresolved root pane and returns\n * a stream of `ResolvedPaneMeta`\n */\nexport function createResolvedPaneNodeStream({\n routerPanesStream,\n rootPaneNode,\n initialCacheState = {\n cacheKeysByFlatIndex: [],\n flattenedRouterPanes: [],\n resolvedPaneCache: new Map(),\n resolvePane: () => NEVER,\n },\n structureContext,\n}: CreateResolvedPaneNodeStreamOptions): Observable<ResolvedPaneMeta[]> {\n const resolvedPanes$ = routerPanesStream.pipe(\n // add in implicit \"root\" router pane\n map((rawRouterPanes) => [[{id: 'root'}], ...rawRouterPanes]),\n // create flattened router panes\n map((routerPanes) => {\n const flattenedRouterPanes: FlattenedRouterPane[] = routerPanes\n .flatMap((routerPaneGroup, groupIndex) =>\n routerPaneGroup.map((routerPaneSibling, siblingIndex) => ({\n routerPaneSibling,\n groupIndex,\n siblingIndex,\n })),\n )\n // add in the flat index\n .map((i, index) => ({...i, flatIndex: index}))\n\n return flattenedRouterPanes\n }),\n // calculate a \"diffIndex\" used for clearing the memo cache\n startWith([] as FlattenedRouterPane[]),\n pairwise(),\n map(([prev, curr]) => {\n for (let i = 0; i < curr.length; i++) {\n const prevValue = prev[i]\n const currValue = curr[i]\n\n if (!isEqual(prevValue, currValue)) {\n return {\n flattenedRouterPanes: curr,\n diffIndex: i,\n }\n }\n }\n\n return {\n flattenedRouterPanes: curr,\n diffIndex: curr.length,\n }\n }),\n // create the memoized `resolvePane` function and manage the memo cache\n scan((acc, next) => {\n const {cacheKeysByFlatIndex, resolvedPaneCache} = acc\n const {flattenedRouterPanes, diffIndex} = next\n\n // use the `cacheKeysByFlatIndex` like a dictionary to find cache keys to\n // and cache keys to delete\n const beforeDiffIndex = cacheKeysByFlatIndex.slice(0, diffIndex + 1)\n const afterDiffIndex = cacheKeysByFlatIndex.slice(diffIndex + 1)\n\n const keysToKeep = new Set(beforeDiffIndex.flatMap((keySet) => Array.from(keySet)))\n const keysToDelete = afterDiffIndex\n .flatMap((keySet) => Array.from(keySet))\n .filter((key) => !keysToKeep.has(key))\n\n for (const key of keysToDelete) {\n resolvedPaneCache.delete(key)\n }\n\n // create a memoizing pane resolver middleware that utilizes the cache\n // maintained above. this keeps the cache from growing indefinitely\n const memoize: PaneResolverMiddleware = (nextFn) => (unresolvedPane, context, flatIndex) => {\n const key = unresolvedPane && `${assignId(unresolvedPane)}-${hashContext(context)}`\n const cachedResolvedPane = key && resolvedPaneCache.get(key)\n if (cachedResolvedPane) return cachedResolvedPane\n\n const result = nextFn(unresolvedPane, context, flatIndex)\n if (!key) return result\n\n const cacheKeySet = cacheKeysByFlatIndex[flatIndex] || new Set()\n cacheKeySet.add(key)\n cacheKeysByFlatIndex[flatIndex] = cacheKeySet\n resolvedPaneCache.set(key, result)\n return result\n }\n\n return {\n flattenedRouterPanes,\n cacheKeysByFlatIndex,\n resolvedPaneCache,\n resolvePane: createPaneResolver(memoize),\n }\n }, initialCacheState),\n // run the memoized, recursive resolving\n switchMap(({flattenedRouterPanes, resolvePane}) =>\n resolvePaneTree({\n unresolvedPane: rootPaneNode,\n flattenedRouterPanes,\n parent: null,\n path: [],\n resolvePane,\n structureContext,\n }),\n ),\n )\n\n // after we've created a stream of `ResolvedPaneMeta[]`, we need to clean up\n // the results to remove unwanted loading panes and prevent unnecessary\n // emissions\n return resolvedPanes$.pipe(\n // this diffs the previous emission with the current one. if there is a new\n // loading pane at the same position where a previous pane already had a\n // resolved value (looking at the IDs to compare), then return the previous\n // pane instead of the loading pane\n scan(\n (prev, next) =>\n next.map((nextPane, index) => {\n const prevPane = prev[index] as ResolvedPaneMeta | undefined\n if (!prevPane) return nextPane\n if (nextPane.type !== 'loading') return nextPane\n\n if (prevPane.routerPaneSibling.id === nextPane.routerPaneSibling.id) {\n return prevPane\n }\n return nextPane\n }),\n [] as ResolvedPaneMeta[],\n ),\n // this prevents duplicate emissions\n distinctUntilChanged((prev, next) => {\n if (prev.length !== next.length) return false\n\n for (let i = 0; i < next.length; i++) {\n const prevValue = prev[i]\n const nextValue = next[i]\n if (hashResolvedPaneMeta(prevValue) !== hashResolvedPaneMeta(nextValue)) {\n return false\n }\n }\n\n return true\n }),\n )\n}\n","import {useEffect, useMemo, useState} from 'react'\nimport {ReplaySubject} from 'rxjs'\nimport {map} from 'rxjs/operators'\nimport {type RouterState, useRouter} from 'sanity/router'\n\nimport {LOADING_PANE} from '../constants'\nimport {type PaneNode, type RouterPaneGroup, type RouterPanes} from '../types'\nimport {useStructureTool} from '../useStructureTool'\nimport {createResolvedPaneNodeStream} from './createResolvedPaneNodeStream'\n\ninterface PaneData {\n active: boolean\n childItemId: string | null\n groupIndex: number\n index: number\n itemId: string\n key: string\n pane: PaneNode | typeof LOADING_PANE\n params: Record<string, string | undefined> & {perspective?: string}\n path: string\n payload: unknown\n selected: boolean\n siblingIndex: number\n}\n\nexport interface Panes {\n paneDataItems: PaneData[]\n routerPanes: RouterPanes\n resolvedPanes: (PaneNode | typeof LOADING_PANE)[]\n}\n\nfunction useRouterPanesStream() {\n const [routerStateSubject] = useState(() => new ReplaySubject<RouterState>(1))\n const routerPanes$ = useMemo(\n () =>\n routerStateSubject\n .asObservable()\n .pipe(map((_routerState) => (_routerState?.panes || []) as RouterPanes)),\n [routerStateSubject],\n )\n const {state: routerState} = useRouter()\n useEffect(() => {\n routerStateSubject.next(routerState)\n }, [routerState, routerStateSubject])\n\n return routerPanes$\n}\n\nexport function useResolvedPanes(): Panes {\n // used to propagate errors from async effect. throwing inside of the render\n // will bubble the error to react where it can be picked up by standard error\n // boundaries\n const [error, setError] = useState<unknown>()\n if (error) throw error\n\n const {structureContext, rootPaneNode} = useStructureTool()\n\n const [data, setData] = useState<Panes>({\n paneDataItems: [],\n resolvedPanes: [],\n routerPanes: [],\n })\n\n const routerPanesStream = useRouterPanesStream()\n\n useEffect(() => {\n const resolvedPanes$ = createResolvedPaneNodeStream({\n rootPaneNode,\n routerPanesStream,\n structureContext,\n }).pipe(\n map((resolvedPanes) => {\n const routerPanes = resolvedPanes.reduce<RouterPanes>((acc, next) => {\n const currentGroup = acc[next.groupIndex] || []\n currentGroup[next.siblingIndex] = next.routerPaneSibling\n acc[next.groupIndex] = currentGroup\n return acc\n }, [])\n\n const groupsLen = routerPanes.length\n\n const paneDataItems = resolvedPanes.map((pane) => {\n const {groupIndex, flatIndex, siblingIndex, routerPaneSibling, path} = pane\n const itemId = routerPaneSibling.id\n const nextGroup = routerPanes[groupIndex + 1] as RouterPaneGroup | undefined\n\n const paneDataItem: PaneData = {\n active: groupIndex === groupsLen - 2,\n childItemId: nextGroup?.[0].id ?? null,\n index: flatIndex,\n itemId: routerPaneSibling.id,\n groupIndex,\n key: `${\n pane.type === 'loading' ? 'unknown' : pane.paneNode.id\n }-${itemId}-${siblingIndex}`,\n pane: pane.type === 'loading' ? LOADING_PANE : pane.paneNode,\n params: routerPaneSibling.params || {},\n path: path.join(';'),\n payload: routerPaneSibling.payload,\n selected: flatIndex === resolvedPanes.length - 1,\n siblingIndex,\n }\n\n return paneDataItem\n })\n\n return {\n paneDataItems,\n routerPanes,\n resolvedPanes: paneDataItems.map((pane) => pane.pane),\n }\n }),\n )\n\n const subscription = resolvedPanes$.subscribe({\n next: (result) => setData(result),\n error: (e) => setError(e),\n })\n\n return () => subscription.unsubscribe()\n }, [rootPaneNode, routerPanesStream, structureContext])\n\n return data\n}\n","import {uuid} from '@sanity/uuid'\nimport {firstValueFrom, type Observable} from 'rxjs'\nimport {type DocumentStore, getPublishedId} from 'sanity'\n\nimport {PaneResolutionError} from '../../../structureResolvers'\n\nexport function removeDraftPrefix(documentId: string): string {\n const publishedId = getPublishedId(documentId)\n\n if (publishedId !== documentId) {\n console.warn(\n 'Removed unexpected draft id in document link: All links to documents should have the ' +\n '`drafts.`-prefix removed and something appears to have made an intent link to `%s`',\n documentId,\n )\n }\n\n return publishedId\n}\n\nexport async function ensureDocumentIdAndType(\n documentStore: DocumentStore,\n id: string | undefined,\n type: string | undefined,\n): Promise<{id: string; type: string}> {\n if (id && type) return {id, type}\n if (!id && type) return {id: uuid(), type}\n if (id && !type) {\n const resolvedType = await firstValueFrom(\n documentStore.resolveTypeForDocument(id) as Observable<string>,\n )\n\n return {id, type: resolvedType}\n }\n\n throw new PaneResolutionError({\n message: 'Neither document `id` or `type` was provided when trying to resolve intent.',\n })\n}\n","import {memo, useCallback, useEffect, useState} from 'react'\nimport {isRecord, useDocumentStore} from 'sanity'\nimport {useRouter, useRouterState} from 'sanity/router'\n\nimport {resolveIntent} from '../../../structureResolvers'\nimport {useStructureTool} from '../../../useStructureTool'\nimport {ensureDocumentIdAndType} from './utils'\n\nconst EMPTY_RECORD: Record<string, unknown> = {}\n\n/**\n * A component that receives an intent from props and redirects to the resolved\n * intent location (while showing a loading spinner during the process)\n */\nexport const IntentResolver = memo(function IntentResolver() {\n const {navigate} = useRouter()\n const maybeIntent = useRouterState(\n useCallback((routerState) => {\n const intentName = typeof routerState.intent === 'string' ? routerState.intent : undefined\n return intentName\n ? {\n intent: intentName,\n params: isRecord(routerState.params) ? routerState.params : EMPTY_RECORD,\n payload: routerState.payload,\n }\n : undefined\n }, []),\n )\n const {rootPaneNode, structureContext} = useStructureTool()\n const documentStore = useDocumentStore()\n const [error, setError] = useState<unknown>(null)\n\n // this re-throws errors so that parent ErrorBoundary's can handle them properly\n if (error) throw error\n\n // eslint-disable-next-line consistent-return\n useEffect(() => {\n if (maybeIntent) {\n const {intent, params, payload} = maybeIntent\n\n let cancelled = false\n // eslint-disable-next-line no-inner-declarations\n async function effect() {\n const {id, type} = await ensureDocumentIdAndType(\n documentStore,\n typeof params.id === 'string' ? params.id : undefined,\n typeof params.type === 'string' ? params.type : undefined,\n )\n\n if (cancelled) return\n\n const panes = await resolveIntent({\n intent,\n params: {...params, id, type},\n payload,\n rootPaneNode,\n structureContext,\n })\n\n if (cancelled) return\n\n navigate({panes}, {replace: true})\n }\n\n effect().catch(setError)\n\n return () => {\n cancelled = true\n }\n }\n }, [documentStore, maybeIntent, navigate, rootPaneNode, structureContext])\n\n return null\n})\n","import {generateHelpUrl} from '@sanity/generate-help-url'\nimport {SyncIcon} from '@sanity/icons'\nimport {Box, Card, Code, Container, Heading, Stack, Text} from '@sanity/ui'\nimport {useCallback} from 'react'\nimport {useTranslation} from 'sanity'\nimport {styled} from 'styled-components'\n\nimport {Button} from '../../../ui-components'\nimport {structureLocaleNamespace} from '../../i18n'\nimport {SerializeError} from '../../structureBuilder'\nimport {PaneResolutionError} from '../../structureResolvers'\n\nconst PathSegment = styled.span`\n &:not(:last-child)::after {\n content: ' ➝ ';\n opacity: 0.5;\n }\n`\n\nfunction formatStack(stack: string) {\n return (\n stack\n // Prettify builder functions\n .replace(/\\(\\.\\.\\.\\)\\./g, '(...)\\n .')\n // Remove webpack cruft from function names\n .replace(/__WEBPACK_IMPORTED_MODULE_\\d+_+/g, '')\n // Remove default export postfix from function names\n .replace(/___default\\./g, '.')\n // Replace full host path, leave only path to JS-file\n .replace(new RegExp(` \\\\(https?:\\\\/\\\\/${window.location.host}`, 'g'), ' (')\n )\n}\n\ninterface StructureErrorProps {\n error: unknown\n}\n\nexport function StructureError({error}: StructureErrorProps) {\n if (!(error instanceof PaneResolutionError)) {\n throw error\n }\n const {cause} = error\n const {t} = useTranslation(structureLocaleNamespace)\n\n // Serialize errors are well-formatted and should be readable, in these cases a stack trace is\n // usually not helpful. Build errors in dev (with HMR) usually also contains a bunch of garbage\n // instead of an actual error message, so make sure we show the message in these cases as well\n const stack = cause?.stack || error.stack\n const showStack =\n stack && !(cause instanceof SerializeError) && !error.message.includes('Module build failed:')\n\n const path = cause instanceof SerializeError ? cause.path : []\n const helpId = (cause instanceof SerializeError && cause.helpId) || error.helpId\n\n const handleReload = useCallback(() => {\n window.location.reload()\n }, [])\n\n return (\n <Card height=\"fill\" overflow=\"auto\" padding={4} sizing=\"border\" tone=\"critical\">\n <Container>\n <Heading as=\"h2\">{t('structure-error.header.text')}</Heading>\n\n <Card marginTop={4} padding={4} radius={2} overflow=\"auto\" shadow={1} tone=\"inherit\">\n {path.length > 0 && (\n <Stack space={2}>\n <Text size={1} weight=\"medium\">\n {t('structure-error.structure-path.label')}\n </Text>\n <Code>\n {/* TODO: it seems like the path is off by one and includes */}\n {/* `root` twice */}\n {path.slice(1).map((segment, i) => (\n // eslint-disable-next-line react/no-array-index-key\n <PathSegment key={`${segment}-${i}`}>{segment}</PathSegment>\n ))}\n </Code>\n </Stack>\n )}\n\n <Stack marginTop={4} space={2}>\n <Text size={1} weight=\"medium\">\n {t('structure-error.error.label')}\n </Text>\n <Code>{showStack ? formatStack(stack) : error.message}</Code>\n </Stack>\n\n {helpId && (\n <Box marginTop={4}>\n <Text>\n <a href={generateHelpUrl(helpId)} rel=\"noopener noreferrer\" target=\"_blank\">\n {t('structure-error.docs-link.text')}\n </a>\n </Text>\n </Box>\n )}\n\n <Box marginTop={4}>\n <Button\n text={t('structure-error.reload-button.text')}\n icon={SyncIcon}\n tone=\"primary\"\n onClick={handleReload}\n />\n </Box>\n </Card>\n </Container>\n </Card>\n )\n}\n","import {Box, Text} from '@sanity/ui'\nimport {isRecord, Translate, useTranslation} from 'sanity'\n\nimport {Pane, PaneContent, PaneHeader} from '../../components/pane'\nimport {structureLocaleNamespace} from '../../i18n'\n\ninterface UnknownPaneProps {\n isSelected: boolean\n pane: unknown\n paneKey: string\n}\n\n/**\n * @internal\n */\nexport function UnknownPane(props: UnknownPaneProps) {\n const {isSelected, pane, paneKey} = props\n const type = (isRecord(pane) && pane.type) || null\n const {t} = useTranslation(structureLocaleNamespace)\n return (\n <Pane id={paneKey} selected={isSelected}>\n <PaneHeader title={t('panes.unknown-pane-type.title')} />\n <PaneContent>\n <Box padding={4}>\n {typeof type === 'string' ? (\n <Text as=\"p\" muted>\n <Translate\n t={t}\n i18nKey=\"panes.unknown-pane-type.unknown-type.text\"\n values={{type}}\n />\n </Text>\n ) : (\n <Text as=\"p\" muted>\n <Translate t={t} i18nKey=\"panes.unknown-pane-type.missing-type.text\" />\n </Text>\n )}\n </Box>\n </PaneContent>\n </Pane>\n )\n}\n","import {isEqual} from 'lodash'\nimport {lazy, memo, Suspense} from 'react'\n\nimport {PaneRouterProvider} from '../components/paneRouter'\nimport {type PaneNode} from '../types'\nimport {LoadingPane} from './loading'\nimport {UnknownPane} from './unknown'\n\ninterface StructureToolPaneProps {\n active: boolean\n childItemId: string | null\n groupIndex: number\n index: number\n itemId: string\n pane: PaneNode\n paneKey: string\n params: Record<string, string | undefined> & {perspective?: string}\n payload: unknown\n path: string\n selected: boolean\n siblingIndex: number\n}\n\n// TODO: audit this creates separate chunks\nconst paneMap = {\n component: lazy(() => import('./userComponent')),\n document: lazy(() => import('./document/pane')),\n documentList: lazy(() => import('./documentList/pane')),\n list: lazy(() => import('./list')),\n}\n\n/**\n * NOTE: The same pane might appear multiple times (split pane), so use index as tiebreaker\n *\n * @intern