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
161 lines (132 loc) • 4.58 kB
text/typescript
import {type Router, type RouterState} from 'sanity/router'
import {type Tool} from '../../config'
import {isRecord} from '../../util/isRecord'
import {type RouterEvent, type RouterStateEvent} from './types'
import {getOrderedTools} from './util/getOrderedTools'
const WEIGHTED_CREATE_INTENT_PARAMS = ['template']
const WEIGHTED_EDIT_INTENT_PARAMS = ['mode']
function resolveUrlStateWithDefaultTool(tools: Tool[], state: Record<string, unknown> | null) {
const orderedTools = getOrderedTools(tools)
const defaultTool = orderedTools[0]
if (!state || state.tool || !defaultTool) {
return state
}
return Object.assign({}, state, {
tool: defaultTool.name,
})
}
function makeBackwardsCompatible(
tools: Tool[],
state: Record<string, unknown> | null,
): Record<string, unknown> | null {
if (!state) {
return state
}
if (getOrderedTools(tools).find((tool) => tool.name === state.space)) {
return {...state, tool: state.space, space: undefined}
}
return state
}
export function resolveDefaultState(
tools: Tool[],
state: Record<string, unknown> | null,
): RouterState | null {
const urlStateWithDefaultTool = resolveUrlStateWithDefaultTool(
tools,
makeBackwardsCompatible(tools, state),
)
return urlStateWithDefaultTool
}
export function resolveIntentState(
tools: Tool[],
prevState: RouterState | null,
nextState: RouterState,
): RouterEvent {
const {intent, params, payload} = nextState
if (typeof intent !== 'string') {
throw new Error('intent must be a string')
}
if (!isRecord(params)) {
throw new Error('intent params must be a string')
}
const orderedTools = getOrderedTools(tools)
const currentTool = prevState?.tool
? orderedTools.find((tool) => tool.name === prevState.tool)
: null
const otherTools = currentTool
? orderedTools.filter((tool) => tool !== currentTool)
: orderedTools
let weightedParams: string[] = []
if (intent === 'create') {
weightedParams = WEIGHTED_CREATE_INTENT_PARAMS
} else if (intent === 'edit') {
weightedParams = WEIGHTED_EDIT_INTENT_PARAMS
}
// Rank tools by how well they can handle the intent, based on the params they support.
// Only the ones defined in `WEIGHTED_*_INTENT_PARAMS` are considered, and on ties in score,
// the first tool wins. Any active tool is considered first, then the rest.
const initialMatch: {score: number; tool: Tool<any> | null} = {score: -1, tool: null}
const {tool: matchingTool} = (currentTool ? [currentTool, ...otherTools] : orderedTools).reduce(
(prev, tool) => {
if (!tool || typeof tool.canHandleIntent !== 'function') {
return prev
}
const canHandle = tool.canHandleIntent(intent, params, prevState && prevState[tool.name])
if (typeof canHandle === 'boolean') {
// Treat `true` as a score of `0`, since an empty object also has that score
return canHandle && prev.score < 0 ? {score: 0, tool} : prev
}
// Skip unknown return values
if (!isRecord(canHandle)) {
return prev
}
// Rank by number of supported, weighted values
const score = weightedParams.reduce((prevScore, weightedParam) => {
return weightedParam in params && canHandle[weightedParam] === true
? prevScore + 1
: prevScore
}, 0)
return score > prev.score ? {score, tool} : prev
},
initialMatch,
)
if (matchingTool?.getIntentState) {
const _toolState = matchingTool.getIntentState(
intent,
params as any,
prevState && (prevState[matchingTool.name] as any),
payload,
) as Record<string, unknown>
const {_searchParams, ...toolState} = _toolState
const nextUrlState: Record<string, unknown> = {
...prevState,
tool: matchingTool.name,
[matchingTool.name]: toolState,
}
if (matchingTool.router?.__unsafe_disableScopedSearchParams) {
nextUrlState._searchParams = _searchParams
} else {
toolState._searchParams = _searchParams
}
return {
type: 'state',
isNotFound: false,
state: nextUrlState,
}
}
return {
type: 'intent',
isNotFound: true,
intent: {name: intent, params},
}
}
export function decodeUrlState(rootRouter: Router, pathname: string): RouterStateEvent {
return {
type: 'state',
state: rootRouter.decode(pathname) || {},
isNotFound: rootRouter.isNotFound(pathname),
}
}
export function isNonNullable<T>(value: T): value is NonNullable<T> {
return value !== null && value !== undefined
}