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,375 lines • 667 kB
TypeScript
import { CSSProperties, Component, ComponentProps, ComponentType, Dispatch, ElementType, ErrorInfo, FC, FocusEvent, FocusEventHandler, FormEvent, FormEventHandler, HTMLAttributes, HTMLProps, KeyboardEvent, MutableRefObject, PropsWithChildren, PureComponent, ReactElement, ReactNode, Ref, RefAttributes, RefObject, SVGProps, SetStateAction } from "react";
import { ArraySchemaType, AssetMetadataType, AssetSource, AssetSourceSpec, BlockDecoratorDefinition, BlockListDefinition, BlockStyleDefinition, BooleanSchemaType, ConditionalProperty, CrossDatasetReferenceSchemaType, CrossDatasetReferenceValue, CrossDatasetType, CurrentUser, DeprecatedProperty, DocumentSystem, FieldGroup, File as File$1, FileAsset, FileSchemaType, FileValue, FormNodeValidation, GeopointValue, GlobalDocumentReferenceType, I18nTextRecord, Image, ImageAsset, ImageSchemaType, ImageUrlFitMode, ImageValue, IndexTuple, InitialValueProperty, InitialValueResolverContext, IntrinsicTypeName, KeyedObject, KeyedSegment, MendozaEffectPair, NumberSchemaType, ObjectField, ObjectFieldType, ObjectSchemaType, PatchOperations, Path, PathSegment, PortableTextBlock, PortableTextObject, PortableTextTextBlock, PrepareViewOptions, PreviewValue, Reference, ReferenceSchemaType, ReferenceValue, Role, RuleClass, SanityDocument, SanityDocumentLike, Schema, SchemaType, SchemaTypeDefinition, SearchStrategy, SlugSchemaType, SlugValue, SortOrdering, StrictVersionLayeringOptions, StringSchemaType, TextSchemaType, TransactionLogEventWithEffects, TransactionLogEventWithMutations, UploadState, User, ValidationMarker } from "@sanity/types";
import { BifurClient } from "@sanity/bifur-client";
import { ClientConfig, ClientError, ClientPerspective, QueryParams, ReconnectEvent, ReleaseDocument, ReleaseDocument as ReleaseDocument$1, ReleaseType, RequestHandler, ResetEvent, SanityClient, SanityDocument as SanityDocument$1, SingleActionResult, SingleMutationResult, StackablePerspective, WelcomeBackEvent, WelcomeEvent } from "@sanity/client";
import { AvatarPosition, AvatarProps, AvatarSize, AvatarStatus, BadgeProps, BadgeTone, BoxProps, ButtonProps, ButtonTone, DialogProps, HotkeysProps, MenuButtonProps, MenuItem, Placement, PopoverProps, ResponsivePaddingProps, ResponsiveWidthProps, TextProps, ThemeColorSchemeKey, ToastParams, TooltipProps } from "@sanity/ui";
import { FlatNamespace, KeyPrefix, Namespace, TFunction, TFunction as TFunction$1, i18n as i18n$1 } from "i18next";
import { MonoTypeOperatorFunction, Observable, OperatorFunction, Subject } from "rxjs";
import { IntentLinkProps, IntentParameters, Router, RouterState, SearchParam } from "sanity/router";
import { ScrollToOptions } from "@tanstack/react-virtual";
import { DRAFTS_FOLDER, DraftId, PublishedId, VERSION_FOLDER, getDraftId, getPublishedId, getVersionFromId, getVersionId, isDraftId, isPublishedId, isVersionId } from "@sanity/client/csm";
import { Mutation } from "@sanity/mutator";
import { CanvasResource, FrameMessages, MediaResource, StudioResource, WindowMessages } from "@sanity/message-protocol";
import { DocumentHandle } from "@sanity/sdk";
import { DocumentDivergencesContextValue, DocumentLimitUpsellContextValue, FeedbackContext, FeedbackContextValue, FieldActionsContextValue, HoveredFieldContextValue, LocaleContextValue, SchedulesContext, SingleDocReleaseContextValue } from "sanity/_singletons";
import { FallbackNs } from "react-i18next";
import { BrowserHistory, HashHistory, History, MemoryHistory } from "history";
import { RootTheme, ThemeColorSchemeKey as ThemeColorSchemeKey$1 } from "@sanity/ui/theme";
import { EditorSelection, HotkeyOptions, InvalidValueResolution, OnCopyFn, OnPasteResultOrPromise, PasteData, Patch, PortableTextEditor, RangeDecoration, RangeDecorationOnMovedDetails, RenderBlockFunction } from "@portabletext/editor";
import { ColorHueKey, ColorTintKey, ColorTints } from "@sanity/color";
import { Subscriber } from "nano-pubsub";
import { ArrayDiff, BooleanDiff, Diff, ItemDiff, NullDiff, NumberDiff, ObjectDiff, StringDiff, StringSegmentChanged, StringSegmentUnchanged, TypeChangeDiff } from "@sanity/diff";
import { CSSProperties as CSSProperties$1, ExecutionProps } from "styled-components";
import { ThrottleSettings } from "lodash-es/throttle.js";
import { DEFAULT_ANNOTATIONS, DEFAULT_DECORATORS } from "@sanity/schema";
import { SanityImageSource } from "@sanity/asset-utils";
import { PortableTextMemberSchemaTypes } from "@portabletext/sanity-bridge";
import { ImageUrlBuilder, ImageUrlBuilder as ImageUrlBuilder$1 } from "@sanity/image-url";
import { MarkdownShortcutsPluginProps } from "@portabletext/plugin-markdown-shortcuts";
import { PasteLinkPluginProps } from "@portabletext/plugin-paste-link";
import { TypographyPluginProps } from "@portabletext/plugin-typography";
import { Node } from "@sanity/comlink";
interface CreateSanityMediaLibrarySourceProps {
i18nKey?: string;
icon?: React.ComponentType;
libraryId: string | null;
name?: string;
}
/**
* Create a new image asset source for the Media Library
*
* @beta
*/
declare function createSanityMediaLibraryImageSource(props: CreateSanityMediaLibrarySourceProps): AssetSource;
/**
* Create a new file asset source for the Media Library
*
* @beta
*/
declare function createSanityMediaLibraryFileSource(props: CreateSanityMediaLibrarySourceProps): AssetSource;
interface SharedProps$1 {
children?: ReactNode;
header: string;
width: ResponsiveWidthProps['width'];
}
interface DialogProps$2 extends SharedProps$1 {
type: 'dialog';
id?: string;
autofocus?: boolean;
onClose?: () => void;
}
interface PopoverProps$3 extends SharedProps$1 {
type: 'popover';
legacy_referenceElement: HTMLElement | null;
onClose: () => void;
}
/**
* @beta
* Creates a dialog or a popover for editing content.
* Handles presence and virtual scrolling.
*/
declare function EditPortal(props: PopoverProps$3 | DialogProps$2): React.JSX.Element;
interface SharedProps {
children?: ReactNode;
header: string;
width: ResponsiveWidthProps['width'];
}
interface DialogProps$1 extends SharedProps {
type: 'dialog';
id?: string;
autofocus?: boolean;
onClose?: () => void;
}
interface PopoverProps$2 extends SharedProps {
type: 'popover';
id?: string;
legacy_referenceElement: HTMLElement | null;
onClose: () => void;
}
/**
* @beta
* Creates a dialog or a popover for editing content.
* Handles presence and virtual scrolling.
*
* When multiple dialogs are open, only the top-most dialog is visible.
* Non-top dialogs are hidden via CSS while preserving their state.
*/
declare function EnhancedObjectDialog(props: PopoverProps$2 | DialogProps$1): React.JSX.Element;
/**
* Returns whether the given version name (e.g. from `getVersionFromId`) is an
* agent bundle name (starts with `agent-`).
*
* @internal
*/
declare function isAgentBundleName(versionName: unknown): boolean;
/**
* Display overrides for an agent bundle version chip.
*
* @internal
*/
type AgentVersionDisplay = {
displayName: string;
tone: BadgeTone;
};
/**
* Filters a list of version document IDs and provides display metadata for
* agent bundles.
*
* - Other users' `agent-*` versions are removed from the returned list.
* - The current user's `agent-*` versions are kept and can be resolved to
* display overrides via `getVersionDisplay`.
* - Non-agent versions pass through unchanged.
* - While the SSE connection is loading, only the currently active agent
* bundle (if any) is kept; all others are hidden until ownership is confirmed.
*
* Call this hook once and thread the results down — don't call it per-item.
*
* @internal
*/
declare function useAgentVersionDisplay(versionIds: string[], activeBundleId?: string): {
/** Version IDs with other users' agent bundles removed. */filteredVersionIds: string[];
/**
* Returns display overrides for a version document ID if it's the current
* user's agent bundle, or `null` for all other versions.
*/
getVersionDisplay: (versionDocumentId: string) => AgentVersionDisplay | null;
};
/**
* Login methods that may be used for Studio authentication.
*
* @public
*/
type LoginMethod = 'dual' | 'cookie' | 'token';
/**
* Login methods that acknowledge cookieless authentication tokens.
*
* @internal
* @hidden
*/
type CookielessCompatibleLoginMethod = Extract<LoginMethod, 'dual' | 'token'>;
/**
* Authentication options
*
* @public
*/
interface AuthConfig {
/**
* Login method to use for the studio. Can be one of:
* - `dual` (default) - attempt to use cookies where possible, falling back to
* storing authentication token in `localStorage` otherwise
* - `cookie` - explicitly disable `localStorage` method, relying only on cookies. May fail due
* to cookies being treated as third-party cookies in some browsers, thus the default is `dual`.
* - `token` - explicitly disable cookies, relying only on `localStorage` method
*/
loginMethod?: LoginMethod;
/**
* If true, the "Choose login provider" (eg "Google, "GitHub", "E-mail/password") screen
* will be skipped if only a single provider is configured in the `providers` array -
* instead it will redirect unauthenticated users straight to the authentication URL.
*/
redirectOnSingle?: boolean;
/**
* Array of authentication providers to use, or a function that takes an array of default
* authentication providers (fetched from the Sanity API) and should return a new list of
* providers. This can be used to selectively replace, add or remove providers from the
* list of choices.
*
* @remarks If a static array of providers is provided it will replace the default
* providers with the ones specified.
*
* If not set, the default providers will be used.
*/
providers?: AuthProvider[] | ((prev: AuthProvider[]) => AuthProvider[] | Promise<AuthProvider[]>);
/**
* The API hostname for requests. Should usually be left undefined,
* but can be set if using custom cname for API domain.
*/
apiHost?: string;
}
/**
* A provider of authentication.
*
* By default, a list of providers for a project will be fetched from the
* {@link https://api.sanity.io/v1/auth/providers | Sanity API}, but you may choose to limit this
* list by explicitly defining the providers you want to allow, or add additional custom providers
* that conforms to the authentication provider specification outlined in
* {@link https://www.sanity.io/docs/third-party-login | the documentation}.
*
* @public
*/
interface AuthProvider {
/**
* URL-friendly identifier/name for the provider, eg `github`
*/
name: string;
/**
* Human friendly title for the provider, eg `GitHub`
*/
title: string;
/**
* URL for the authentication endpoint that will trigger the authentication flow
*/
url: string;
/**
* URL for a logo to display next to the provider in the login screen
*/
logo?: string;
}
/** @internal */
interface WorkspacesProviderProps {
config: Config;
children: ReactNode;
basePath?: string;
LoadingComponent: ComponentType;
/**
* Project ID of the first workspace in the Studio config. Forwarded to
* `CorsOriginErrorScreen` so it can surface the "Register Studio" option
* when the failing project matches the studio's primary project.
*/
primaryProjectId?: string;
}
/**
* Verdict from `/check/cors` for the current origin against the project's
* CORS allowlist. Both fields come straight from the endpoint:
*
* - `allowed`: whether the origin is in the allowlist at all.
* - `withCredentials`: whether the allowlist entry permits credentialed
* requests (the studio always sends them).
*
* The studio needs both `true` to function. A `null` probe result means
* the probe couldn't reach a conclusion (network down, endpoint down, no
* projectId) — callers should treat that as "not CORS" and let the
* original error flow to its normal branch.
*
* @internal
*/
type CorsCheckResult = {
allowed: boolean;
withCredentials: boolean;
};
/**
* Outcome of the `/check/cors` probe:
* - a {@link CorsCheckResult} — the allow/credentials verdict
* - `'project-not-found'` — the probe itself 404s because the project
* doesn't exist (`errorCode: "SIO-404-PNF"`). This is the one place we
* can detect a missing project: the CORS endpoint answers with CORS
* headers (so the `fetch` resolves) but a 404 body, whereas the data
* API requests fail as opaque network/CORS errors that can't tell us
* why.
* - `null` — inconclusive (network down, endpoint down, no projectId)
*
* @internal
*/
type CorsProbeOutcome = CorsCheckResult | 'project-not-found';
/** @internal */
declare function WorkspacesProvider({
config,
children,
basePath,
LoadingComponent,
primaryProjectId
}: WorkspacesProviderProps): import("react").JSX.Element;
/**
* A diagnosed request failure — the reason a request the studio can't recover
* from locally failed. Produced by {@link RequestFailureProbe} from an
* otherwise-opaque error.
*
* - `project-not-found` / `dataset-not-found` — a configuration error: the
* project or dataset the studio points at doesn't exist. Unrecoverable by
* retry; the studio takes over the screen with guidance.
* - `cors` — the origin isn't allowed (or can't send credentials) by the
* project's CORS allowlist. The studio shows the CORS-origin screen. This
* can begin at any time (e.g. an admin editing the allowlist in Manage),
* not only at boot.
* - `unknown` — couldn't be diagnosed (a transient network error, a timeout,
* a CORS-allowed origin, …). The caller falls back to its normal handling
* (e.g. the generic network-error dialog).
*
* @internal
*/
type RequestFailureResult = {
type: 'project-not-found';
} | {
type: 'dataset-not-found';
} | {
type: 'cors';
allowed: boolean;
withCredentials: boolean;
} | {
type: 'unknown';
};
/**
* Diagnoses why a request failed. Pure classifier: it inspects the error (and,
* for opaque network failures, probes `/check/cors`) and returns a
* {@link RequestFailureResult}. It performs no side effects — the caller
* decides what to do with the result (take over the screen, re-throw, …).
*
* Used in two places that must agree: the studio's request handler (for
* requests that flow through it) and the auth store's `/users/me` probe (which
* runs on a client with the request handler stripped, so it needs to diagnose
* its own failures).
*
* @internal
*/
type RequestFailureProbe = (err: unknown) => Promise<RequestFailureResult>;
/**
* Options for delegating a request error to the studio's error UI.
*
* @beta
*/
interface RequestErrorReportOptions {
/**
* Whether the failed request is safe to re-run. Only set this to `true`
* when re-running cannot cause harm: the request is idempotent (reads),
* or it is guaranteed to have been rejected before processing (429s).
*
* When `true`, the studio's error dialog offers a "Try again" button
* that re-runs the request. When `false` (the default), the dialog only
* offers "Reload Studio" and uses more conservative copy, warning that
* the last change may or may not have been applied.
*
* Note: retry requires a re-runnable request, so it only takes effect
* with `attempt()` (which has the thunk to re-invoke). The `handle`
* rejection-handler form cannot re-run the request.
*/
retryable?: boolean;
}
/**
* An error claimed by the studio's request-error UI, as rendered by the
* studio dialog. Mirrors the classification plus the caller's retry
* assertion.
*
* @internal
*/
type RequestErrorClaim = {
type: 'networkError';
error: Error;
retryable: boolean;
} | {
type: 'serverError';
error: Error;
retryable: boolean;
} | {
type: 'rateLimited';
error: Error;
retryAfterSeconds?: number;
retryable: boolean;
} | {
type: 'unauthorized';
error: Error;
projectId?: string;
};
/**
* Call-site API for delegating unrecoverable request errors to the
* studio's built-in error UI (dialogs, verified forced logout).
*
* The studio never intercepts your requests — errors only reach this
* channel when you hand them over. Handle what you can locally (inline
* errors, toasts, fallbacks); delegate what you can't.
*
* Two shapes, both feeding the same dialog:
*
* ```ts
* const {attempt, handle} = useStudioErrorHandler()
*
* // 1. Thunk wrapper — the dialog's "Try again" re-invokes the thunk.
* // The thunk may return a promise or a (single-shot) observable:
* const user = await attempt(() => client.request({uri: '/users/me'}), {
* retryable: true,
* })
*
* // 2. Promise rejection handler — fire-and-surface, no retry:
* client.create(doc).catch(handle)
* ```
*
* Unclaimable errors (4xx other than 429, parse errors, ...) are
* re-thrown unchanged, so downstream `.catch` / `catchError` handlers
* still see them. 401s are claimed only when the API explicitly tags them
* as session expiry (`SIO-401-AEX`), in which case a forced logout
* follows; untagged resource-level 401s are re-thrown to the caller.
*
* @beta
*/
interface StudioErrorHandler {
/**
* Promise rejection handler — use directly in `.catch(handle)`.
* Claimable errors surface the studio dialog and leave the promise
* pending (the request is in limbo until the user reloads);
* unclaimable errors are re-thrown to the next `.catch`:
*
* ```ts
* client
* .create(doc)
* .catch(handle)
* .catch((err) => {
* // caller-domain errors (validation, permissions, 404, ...)
* })
* ```
*/
handle(err: unknown): Promise<never>;
/**
* Runs `thunk` and delegates claimable failures to the studio dialog.
* With `retryable: true`, the dialog's "Try again" re-invokes the thunk;
* the returned promise resolves with the first successful attempt.
* Unclaimable failures reject the returned promise.
*
* The thunk receives the attempt number (starting at 1), useful for
* logging or cache-busting. It may return a promise or an observable —
* an observable is drained to its last emitted value (single-shot
* request observables emit once and complete, so this is their value).
*
* The observable MUST be finite. `attempt` waits for completion, so a
* long-lived or multicast source (`client.listen()`, a `Subject`) would
* never resolve and would leak its subscription for the life of the
* returned promise. An observable that completes without emitting
* rejects with rxjs's `EmptyError`. Pass request-style observables only.
*
* Promises are eager, so the thunk must CREATE the request when called
* — retry works by invoking it again for a fresh request:
*
* ```ts
* // ✓ each invocation issues a new request
* attempt(() => client.fetch(query), {retryable: true})
*
* // ✗ the request already happened; retry re-awaits the same
* // settled rejection and the dialog just reappears
* const promise = client.fetch(query)
* attempt(() => promise, {retryable: true})
* ```
*/
attempt<T>(thunk: (attemptNumber: number) => PromiseLike<T> | Observable<T>, options?: RequestErrorReportOptions): Promise<T>;
}
/**
* The full channel, as owned by `WorkspacesProvider`: the reporter
* surface plus the state stream and retry trigger the dialog needs.
*
* @internal
*/
interface RequestErrorChannel extends StudioErrorHandler {
/** Currently active claim (undefined → no dialog). */
claim$: Observable<RequestErrorClaim | undefined>;
/** Re-runs all parked retryable requests and clears the claim. */
retry(): void;
}
type AuthProbeResult = {
authenticated: false;
} | {
authenticated: true;
id: string;
};
/**
* The interface used by the Studio that produces a `SanityClient` and
* `CurrentUser` that gets passed to the resulting `Workspace`s and `Source`s.
*
* NOTE: This interface is primarily for internal use. Refer to
* `createAuthStore` instead.
*
* @beta
* @hidden
*/
interface AuthStore {
/**
* Emits `AuthState`s. This should update when the user's auth state changes.
* E.g. After a login, a new `AuthState` could be emitted with a non-null
* `currentUser` and `authenticated: true`
*
* NOTE: all auth store implementations should emit on subscribe using
* something like shareReplay(1) to ensure all new subscribers get an
* `AuthState` value on subscribe
*/
state: Observable<AuthState>;
/**
* Emits auth tokens, or `null` if not configured to use them or they do not exist
*/
token?: Observable<string | null>;
/**
* Custom auth stores are expected to implement a UI that initiates the user's
* authentication. For the typical case in `createAuthStore`, this means
* loading the providers and showing them as options to the user.
*/
LoginComponent?: ComponentType<LoginComponentProps>;
/**
* Custom auth stores can implement a function that runs when the user logs
* out. The implementation is expected to remove all credentials both locally
* and on the server.
*/
logout?: () => Promise<void>;
/**
* Custom auth stores can implement a function that is designated to run when
* the Studio loads (e.g. to trade a session ID for a token in cookie-less
* mode). Within the Studio, this is called within the `AuthBoundary`.
*/
handleCallbackUrl?: () => Promise<HandleCallbackResult>;
}
/**
* The unit an `AuthStore` emits to determine the user's authentication state.
*
* @beta
* @hidden
*/
interface AuthState {
/**
* Similar to a logged-in flag. This state is used in places like the
* `AuthBoundary` to determine whether or not it should render the
* `NotAuthenticatedComponent`. Implementers may choose to set this to `true`
* while also also emitting a `currentUser` of `null` if a `null` user is
* accepted (e.g. a project that doesn't require a login)
*/
authenticated: boolean;
/**
* The value of the user logged in or `null` if none is provided
*/
currentUser: CurrentUser | null;
/**
* A client that is expected to be pre-configured to allow for any downstream
* requests in the Studio
*/
client: SanityClient;
}
/**
* @beta
* @hidden
*/
type LoginComponentProps = {
projectId: string; /** @deprecated use redirectPath instead */
basePath: string;
redirectPath?: string;
/**
* Invoked when the user chooses to switch workspaces from the login screen
* (e.g. when the active workspace has no usable login providers). Only
* provided when more than one workspace is available to switch to.
*/
onChooseAnotherWorkspace?: () => void;
} | {
projectId: string;
redirectPath: string; /** @deprecated use redirectPath instead */
basePath?: string;
/**
* Invoked when the user chooses to switch workspaces from the login screen
* (e.g. when the active workspace has no usable login providers). Only
* provided when more than one workspace is available to switch to.
*/
onChooseAnotherWorkspace?: () => void;
};
/**
* Result returned from `handleCallbackUrl` describing what happened during
* the auth callback flow. Used for telemetry, diagnostics, and error handling
* in the AuthBoundary.
*
* @internal
*/
interface HandleCallbackResult {
/** The login method configured for this auth store (e.g. `'cookie'` or `'token'`). */
loginMethod: LoginMethod;
/**
* Which auth flow was taken:
* - `'already-authenticated'`: No sid in hash - user was already authenticated or not.
* - `'exchange'`: sid was present, went through /auth/exchange + probe flow.
*/
flow: 'already-authenticated' | 'exchange';
/** Whether the auth flow completed successfully. */
success: boolean;
/** Total wall-clock time for the callback handling, in milliseconds. */
durationMs: number;
/** Time spent on the /auth/exchange call specifically. Only set for `'exchange'` flow. */
exchangeDurationMs?: number;
/** Time spent on the /users/me probe calls. Only set for `'exchange'` flow. */
probeDurationMs?: number;
/** Which auth method was selected by the probes. Only set when `success` is `true` and flow is `'exchange'`. */
authMethod?: 'cookie' | 'token';
/** Human-readable reason for failure. Only set when `success` is `false`. */
failureReason?: string;
/**
* Structured error for the AuthBoundary to render appropriate UI.
* Only set when `success` is `false` and flow is `'exchange'`.
*
* - `'cookie-blocked'`: cookie-only mode and the cookie probe failed.
* The browser likely has strict cookie policies.
* - `'auth-failed'`: all probe methods failed. The user should retry.
*/
error?: {
type: 'cookie-blocked' | 'auth-failed';
message: string;
};
}
/** @internal */
interface AuthStoreOptions extends AuthConfig {
clientFactory?: (options: ClientConfig) => SanityClient;
projectId: string;
dataset: string;
/**
* Lazily resolves the channel for delegating unrecoverable boot-time
* request errors (network / 5xx on the initial `/users/me` probe) to the
* studio's error dialog instead of crashing the boot sequence.
*
* @internal
*/
getRequestErrorHandler?: () => StudioErrorHandler | undefined;
/**
* Lazily resolves diagnostics for the `/users/me` probe, so it can detect
* and report the CORS / missing-project-or-dataset failures the studio
* request handler would otherwise catch (the probe bypasses that handler).
* Resolved lazily for the same reason as `getRequestErrorHandler`: it's
* unhashable runtime wiring that must stay out of the auth-store memo key.
*
* @internal
*/
getRequestFailureDiagnostics?: () => RequestFailureDiagnostics | undefined;
/**
* Retrieves the session ID from the URL hash for the auth callback flow.
* Called by `handleCallbackUrl` to obtain the session ID that is exchanged
* for a token or cookie.
* @internal
*/
getSessionId: () => string | undefined;
/**
* Extracts and consumes a `#token=…` fragment from the URL hash.
* Called at init to pick up hash tokens and on `hashchange` events.
* @internal
*/
consumeHashToken: () => string | undefined;
}
/**
* Lets the auth store's `/users/me` probe diagnose and report the failures the
* studio request handler would normally catch — but can't here, because the
* probe runs on a client with that handler stripped (see {@link getCurrentUser}).
*
* - `diagnose` — the shared classifier. The client is passed per call rather
* than bound up front, because the auth store builds its own clients
* internally (so there's no single client to bind the probe to).
* - `onRequestFailure` — reports a non-`unknown` result to the studio so it
* can take over the screen (CORS / missing project or dataset). Must be
* idempotent: the probe runs inside a retryable thunk, so a recurring
* failure can report the same result more than once.
*
* @internal
*/
interface RequestFailureDiagnostics {
diagnose: (err: unknown, client: SanityClient) => ReturnType<RequestFailureProbe>;
onRequestFailure: (result: Exclude<RequestFailureResult, {
type: 'unknown';
}>, client: SanityClient) => void;
}
/**
* @internal
*/
declare function _createAuthStore({
clientFactory: clientFactoryOption,
projectId,
dataset,
apiHost,
loginMethod,
getSessionId,
consumeHashToken,
getRequestErrorHandler,
getRequestFailureDiagnostics,
...providerOptions
}: AuthStoreOptions): AuthStore;
/**
* Public options for `createAuthStore`. The `getSessionId` and `consumeHashToken`
* dependencies are wired automatically using the default implementations.
* @internal
*/
type CreateAuthStoreOptions = Omit<AuthStoreOptions, 'getSessionId' | 'consumeHashToken'>;
/**
* @internal
*/
declare const createAuthStore: (options: CreateAuthStoreOptions) => AuthStore;
/** @internal */
interface MockAuthStoreOptions {
currentUser: CurrentUser | null;
client: SanityClient;
}
/**
* Creates a mock `AuthStore` (for testing) that emits an `AuthState` derived
* from the `client` and `currentUser` given.
*
* @internal
*/
declare function createMockAuthStore({
client,
currentUser
}: MockAuthStoreOptions): AuthStore;
/** @internal */
declare function getProviderTitle(provider?: string): string | undefined;
/**
* Duck-type check for whether or not this looks like an auth store
*
* @param maybeStore - Item to check if matches the AuthStore interface
* @returns True if auth store, false otherwise
* @internal
*/
declare function isAuthStore(maybeStore: unknown): maybeStore is AuthStore;
/**
* Check whether the provided login method is compatible with cookieless auth, e.g. whether any
* authentication token found in localStorage should be acknowledged.
*
* @internal
*/
declare function isCookielessCompatibleLoginMethod(loginMethod: LoginMethod): loginMethod is CookielessCompatibleLoginMethod;
/** @internal */
interface ConnectionStatusStore {
connectionStatus$: Observable<ConnectionStatus>;
}
/** @internal */
type ConnectingStatus = {
type: 'connecting';
};
/** @internal */
type ErrorStatus = {
type: 'error';
error: Error;
attemptNo: number;
isOffline: boolean;
retryAt: Date;
};
/** @internal */
type RetryingStatus = {
type: 'retrying';
};
/** @internal */
type ConnectedStatus = {
type: 'connected';
lastHeartbeat: Date;
};
/** @internal */
declare const CONNECTING: ConnectingStatus;
/** @internal */
type ConnectionStatus = ConnectingStatus | ErrorStatus | ConnectedStatus | RetryingStatus;
/** @internal */
declare const onRetry: () => void;
/** @internal */
interface ConnectionStatusStoreOptions {
bifur: BifurClient;
}
/**
* This is the beginning of what should be the data store tracking connection status in the Sanity studio.
*
* @internal
*/
declare function createConnectionStatusStore({
bifur
}: ConnectionStatusStoreOptions): ConnectionStatusStore;
/** @internal */
type BetaBadgeProps = Omit<BadgeProps, 'mode' | 'tone'>;
/** @internal */
declare function BetaBadge(props: BetaBadgeProps & Omit<HTMLProps<HTMLDivElement>, 'ref'>): import("react").JSX.Element;
/** @internal */
type CommandListElementType = 'input' | 'list';
/** @internal */
type CommandListGetItemDisabledCallback = (virtualIndex: number) => boolean;
/** @internal */
type CommandListGetItemKeyCallback = (virtualIndex: number) => number | string;
/** @internal */
type CommandListGetItemSelectedCallback = (virtualIndex: number) => boolean;
/** @internal */
type CommandListItemContext = {
activeIndex: number | null;
disabled?: boolean;
selected?: boolean;
virtualIndex: number;
};
/** @internal */
type CommandListRenderItemCallback<T> = (item: T, context: CommandListItemContext) => ReactNode;
/** @internal */
interface CommandListHandle {
focusInputElement: () => void;
focusListElement: () => void;
getTopIndex: () => number;
scrollToIndex: (index: number) => void;
}
/** @internal */
interface CommandListProps<T = any> extends ResponsivePaddingProps {
/** The data attribute to apply to any active virtual list items */
activeItemDataAttr?: string;
/** `aria-label` to apply to the virtual list container element */
ariaLabel: string;
/** Whether `aria-multiselectable` is enabled on the virtual list container element */
ariaMultiselectable?: boolean;
/** Automatically focus the input or virtual list */
autoFocus?: CommandListElementType;
/** Whether the virtual list can receive focus */
canReceiveFocus?: boolean;
/** Pixel offset of the virtual list focus ring. Negative values will cause the focus ring to appear inset */
focusRingOffset?: number;
/** Force a fixed height for all virtual list children and skip measurement (faster). */
fixedHeight?: boolean;
/** Custom function to map disabled items */
getItemDisabled?: CommandListGetItemDisabledCallback;
/** Custom function to map virtual list items to custom keys */
getItemKey?: CommandListGetItemKeyCallback;
/** Custom function to map selected items */
getItemSelected?: CommandListGetItemSelectedCallback;
/** Scroll alignment of the initial active index */
initialScrollAlign?: ScrollToOptions['align'];
/** Initial active index on mount */
initialIndex?: number;
/** Input element to associate with this virtual list. Associated inputs will receive focus and handle key events */
inputElement?: HTMLInputElement | null;
/** Estimated height for each list item */
itemHeight: number;
/** Virtual list item values, accessible to all rendered item components */
items: T[];
/** Callback fired when the virtual list is within `onEndReachedIndexThreshold` of rendered content */
onEndReached?: () => void;
/** Number of items from the end of the virtual list before which `onEndReached` is triggered */
onEndReachedIndexOffset?: number;
/** Only show selection state when the virtual list is active (is hovered or has focus) */
onlyShowSelectionWhenActive?: boolean;
/** Number of items to render above and below the visible area*/
overscan?: number;
/** Rendered component in virtual lists */
renderItem: CommandListRenderItemCallback<T>;
/** `data-testid` to apply to outermost container */
testId?: string;
/** Allow wraparound keyboard navigation between first and last items */
wrapAround?: boolean;
}
/**
* Renders a Command List with support for the following:
*
* - Keyboard navigation (↑ / ↓ / ENTER) to children with a specified container (`childContainerRef`)
* - Focus redirection when clicking child elements
* - Pointer blocking when navigating with arrow keys (to ensure that only one active state is visible at any given time)
* - ARIA attributes to define a `combobox` input that controls a separate `listbox`
*
* @internal
*/
declare const CommandList: import("react").NamedExoticComponent<CommandListProps<any> & import("react").RefAttributes<CommandListHandle>>;
type BaseButtonProps = Pick<ButtonProps<ElementType>, 'as' | 'icon' | 'iconRight' | 'justify' | 'loading' | 'mode' | 'paddingY' | 'paddingLeft' | 'selected' | 'tone' | 'type' | 'width'> & {
size?: 'default' | 'large';
radius?: 'full';
};
type ButtonWithText = {
text: string;
tooltipProps?: TooltipProps$1 | null;
icon?: ButtonProps['icon'];
};
type IconButton = {
text?: undefined;
icon?: ButtonProps['icon'];
/**
* When using a button with an icon, tooltipProps are required to enforce consistency in UI.
*/
tooltipProps: TooltipProps$1 | null;
};
/** @internal */
type ButtonProps$1 = BaseButtonProps & (ButtonWithText | IconButton);
/**
* Customized Sanity UI <Button> with pre-defined layout options.
*
* @internal
*/
declare const Button: import("react").ForwardRefExoticComponent<(Omit<Pick<ButtonProps<ElementType>, "as" | "icon" | "iconRight" | "justify" | "loading" | "mode" | "paddingLeft" | "paddingY" | "selected" | "tone" | "type" | "width"> & {
size?: 'default' | 'large';
radius?: 'full';
} & ButtonWithText & Omit<HTMLProps<HTMLButtonElement>, "as" | "size" | "title">, "ref"> | Omit<Pick<ButtonProps<ElementType>, "as" | "icon" | "iconRight" | "justify" | "loading" | "mode" | "paddingLeft" | "paddingY" | "selected" | "tone" | "type" | "width"> & {
size?: 'default' | 'large';
radius?: 'full';
} & IconButton & Omit<HTMLProps<HTMLButtonElement>, "as" | "size" | "title">, "ref">) & import("react").RefAttributes<HTMLButtonElement>>;
/** @internal */
type MenuButtonProps$1 = Omit<MenuButtonProps, 'popover'> & {
popover?: Omit<PopoverProps, 'animate' | 'content' | 'open'>;
};
/** @internal */
type TooltipProps$1 = Omit<TooltipProps, 'arrow' | 'padding' | 'shadow'> & {
hotkeys?: HotkeysProps['keys'];
};
/** @internal */
type PopoverProps$1 = PopoverProps;
type ContextMenuButtonProps = Pick<ButtonProps$1, 'mode' | 'selected' | 'size' | 'tone' | 'tooltipProps' | 'loading'>;
/**
* Simple context menu button (with horizontal ellipsis icon) with shared localization.
*
* @internal
*/
declare const ContextMenuButton: import("react").ForwardRefExoticComponent<ContextMenuButtonProps & Pick<HTMLProps<HTMLButtonElement>, "disabled" | "hidden" | "onClick"> & import("react").RefAttributes<HTMLButtonElement>>;
/**
* Indicates the type of document variant, either `draft`, `version` or `published`.
* Draft documents are prefixed with `drafts.`.
* Version documents are prefixed with `versions.<versionName>`
* The rest are considered published documents.
* @public
*/
type DocumentVariantType = 'draft' | 'version' | 'published';
/**
* Takes a document id and returns the variant type for that document
* If it's a document that starts with `version.` it's a `version` document.
* If it's a document that starts with `drafts.` it's a `draft` document.
* Otherwise, it's a `published` document.
* @public
* */
declare function getDocumentVariantType(documentId: string): DocumentVariantType;
/**
* @internal
*/
declare const Chip: import("react").ForwardRefExoticComponent<Omit<Omit<any, "as" | keyof import("@sanity/ui").ButtonOwnProps> & import("@sanity/ui").ButtonOwnProps & {
as?: import("@sanity/ui").ElementType<any> | undefined;
}, "ref"> & import("react").RefAttributes<unknown>>;
/**
*
* Checks if the document ID `documentId` has the same ID as `equalsDocumentId`,
* ignoring the draft prefix.
*
* @public
*
* @param documentId - The document ID to check
* @param equalsDocumentId - The document ID to check against
*
* @example
* Draft vs published document ID, but representing the same document:
* ```
* // Prints "true":
* console.log(documentIdEquals('drafts.agot', 'agot'));
* ```
* @example
* Different documents:
* ```
* // Prints "false":
* console.log(documentIdEquals('hp-tcos', 'hp-hbp'));
* ```
*
* @returns `true` if the document IDs are equal, `false` otherwise
*/
declare function documentIdEquals(documentId: string, equalsDocumentId: string): boolean;
/** @internal */
declare function isDraft(document: SanityDocumentLike): boolean;
/**
* TODO: Improve return type based on presence of `version` option.
*
* @internal
*/
declare function getIdPair(id: string, {
version
}?: {
version?: string;
}): {
draftId: DraftId;
publishedId: PublishedId;
versionId?: string;
};
/**
* System bundles are sets of documents owned by the system.
*
* - Draft documents contain data that has not yet been published. These documents all exist in the "drafts" path.
* - Published documents contain data that has been published. These documents all exist in the root path.
*
* These differ to user bundles, which are created when a user establishes a custom set of documents
* (e.g. by creating a release).
*
* @public
*/
declare const systemBundles: readonly ['drafts', 'published'];
/**
* System bundles are sets of documents owned by the system.
*
* - Draft documents contain data that has not yet been published. These documents all exist in the "drafts" path.
* - Published documents contain data that has been published. These documents all exist in the root path.
*
* These differ to user bundles, which are created when a user establishes a custom set of documents
* (e.g. by creating a release).
*
* @public
*/
type SystemBundle = 'drafts' | 'published';
/** @internal */
declare function isSystemBundle(maybeSystemBundle: unknown): maybeSystemBundle is SystemBundle;
/** @internal */
type SystemBundleName = 'draft' | 'published';
/**
* `isSystemBundle` should be preferred, but some parts of the codebase currently use the singular
* "draft" name instead of the plural "drafts".
*
* @internal
*/
declare function isSystemBundleName(maybeSystemBundleName: unknown): maybeSystemBundleName is SystemBundleName;
/**
* @internal
* Given a perspective stack and a document id, returns true if the document id matches any of the provided perspectives
* e.g. `idMatchesPerspective('['summer'], 'versions.summer.foo') === true`
* e.g. `idMatchesPerspective('['drafts', 'summer'], 'versions.summer.foo') === true`
* e.g. `idMatchesPerspective('['drafts'], 'versions.summer.foo') === false`
* e.g. `idMatchesPerspective('['drafts', 'summer'], 'versions.winter.foo') === false`
*
* Note: a published id will match any perspective
* e.g. `idMatchesPerspective('['drafts', 'summer'], 'foo') === true`
*/
declare function idMatchesPerspective(perspectiveStack: StackablePerspective[], documentId: string): boolean;
/** @internal */
declare function createDraftFrom(document: SanityDocument): SanityDocument;
/** @internal */
declare function newDraftFrom(document: SanityDocument): SanityDocument;
/** @internal */
declare function createPublishedFrom(document: SanityDocument): SanityDocument;
/**
* Takes a list of documents and collates draft/published pairs into single entries
* `{id: <published id>, draft?: <draft document>, published?: <published document>}`
*
* Note: because Map is ordered by insertion key the resulting array will be ordered by whichever
* version appeared first
*
* @internal
*/
interface CollatedHit<T extends {
_id: string;
} = {
_id: string;
}> {
id: string;
type: string;
draft?: T;
published?: T;
versions: T[];
}
/** @internal */
declare function collate<T extends {
_id: string;
_type: string;
}>(documents: T[]): CollatedHit<T>[];
/** @internal */
declare function removeDupes(documents: SanityDocumentLike[]): SanityDocumentLike[];
declare const VARIANT_DOCUMENT_TYPE: 'system.variant';
declare const VARIANT_DOCUMENTS_PATH: '_.variants';
interface SystemVariant extends SanityDocument {
_type: typeof VARIANT_DOCUMENT_TYPE;
_id: `${typeof VARIANT_DOCUMENTS_PATH}.${string}`;
name?: string;
conditions: Record<string, string>;
priority: number;
metadata?: {
title?: string;
description?: PortableTextBlock[];
[key: string]: unknown;
};
}
/**
* @beta
*/
type ReleaseId = string;
/**
* A value representing a perspective, including the data describing it. This is either the name of a
* system bundle, or a document describing a release.
*
* @public
*/
type TargetPerspective = ReleaseDocument | SystemBundle | string;
/**
* @beta
* @deprecated Use `TargetPerspective` instead.
*/
type SelectedPerspective = TargetPerspective;
/**
* @beta
*/
type PerspectiveStack = ExtractArray<ClientPerspective>;
type PerspectiveBundle = 'published' | 'drafts' | (string & {});
/**
* @beta
*/
interface PerspectiveContextValue {
selectedPerspectiveName: 'published' | ReleaseId | undefined;
/**
* The releaseId as `r<string>`; it will be undefined if the selected perspective is `published` or `drafts`
*/
selectedReleaseId: ReleaseId | undefined;
selectedPerspective: TargetPerspective;
/**
* The stacked array of perspectives ids ordered chronologically to represent the state of documents at the given point in time.
* It can be used as the perspective param in the client to get the correct view of the documents.
* @returns ["published"] | ["drafts"] | ["releaseId2", "releaseId1", "drafts"]
*/
perspectiveStack: PerspectiveStack;
excludedPerspectives: string[];
/**
* Resolved variant definition; undefined = default (all users) or still loading
* @beta
* @internal
*/
selectedVariant: SystemVariant | undefined;
/**
* The selected bundle, either `published`, `drafts` or a release id or the bundle id for anonymous bundles like agent documents.
*/
bundle: PerspectiveBundle;
}
/**
* @internal
*/
type ExtractArray<Union> = Union extends unknown[] ? Union : never;
/**
* @internal
*/
type ReleasesNavMenuItemPropsGetter = (content: {
perspective: TargetPerspective;
}) => Partial<ComponentProps<typeof MenuItem>>;
/**
* @internal
*/
declare const VersionChip: import("react").MemoExoticComponent<(props: {
disabled?: boolean;
selected: boolean;
tooltipContent?: ReactNode;
onClick: () => void;
text: string;
contextMenuPortal?: boolean;
tone: BadgeTone;
locked?: boolean;
onCopyToDraftsNavigate: () => void;
contextValues: {
documentId: string;
documentType: string;
releases: ReleaseDocument[];
releasesLoading: boolean;
bundleId: string;
isVersion: boolean;
disabled?: boolean;
isGoingToUnpublish?: boolean;
release?: ReleaseDocument;
};
}) => import("react").JSX.Element>;
/** @internal */
type ReleaseAvatarIconProps = {
release: TargetPerspective;
tone?: never;
releaseType?: never;
} | {
releaseType: ReleaseType;
tone?: never;
release?: never;
} | {
/**
* @deprecated - Prefer `release` or `releaseType`.
*/
tone: BadgeTone;
release?: never;
releaseType?: never;
};
declare const ReleaseAvatarIcon: ({
tone,
release,
releaseType
}: ReleaseAvatarIconProps) => import("react").JSX.Element;
declare function ReleaseAvatar({
fontSize,
padding,
...iconProps
}: ReleaseAvatarIconProps & {
fontSize?: number;
padding?: number;
}): React.JSX.Element;
/** @internal */
interface ReleaseTitleDetails {
displayTitle: string;
fullTitle: string;
isTruncated: boolean;
}
/** @internal */
interface ReleaseTitleProps {
title: string | undefined;
fallback: string;
enableTooltip?: boolean;
tooltipMaxWidth?: string;
children?: (details: ReleaseTitleDetails) => ReactElement;
textProps?: Omit<TextProps, 'children'> & {
style?: CSSProperties;
};
}
/** @internal */
declare function ReleaseTitle(props: ReleaseTitleProps): ReactNode;
/**
* @internal
*/
declare const VersionInlineBadge: ({
children,
$tone
}: PropsWithChildren<{
$tone?: BadgeTone;
}>) => import("react").JSX.Element;
/**
* @internal
*/
declare const getVersionInlineBadge: (release?: TargetPerspective) => FC<{
children?: import("react").ReactNode | undefined;
}>;
interface ObserveDocumentAPIConfig {
dataset?: string;
projectId?: string;
apiVersion?: string;
}
type DocumentIdSetObserverState = {
status: 'reconnecting' | 'connected';
documentIds: string[];
};
/** @internal */
type Id = string;
/**
* @hidden
* @beta */
type Previewable = ({
_id: string;
} | {
_type: string;
} | {
_system?: {
delete: boolean;
};
} | {
_ref: string;
_dataset?: string;
_projectId?: string;
}) & {
/**
* optional object used to attach meta data to the prepared result.
* currently used to add a flag for the invalid preview error fallback and
* insufficient permissions fallback
* @internal
*/
_internalMeta?: {
type?: string;
};
};
/**
* TODO: unify with content path from `@sanity/types`
*
*
* @hidden
* @beta
*/
type PreviewPath = FieldName[];
/** @internal */
type Selection = [id: Id, fields: FieldName[]];
/**
* @hidden
* @beta */
type FieldName = string;
/** @internal */
interface AvailabilityResponse {
omitted: {
id: string;
reason: 'existence' | 'permission';
}[];
}
/** @internal */
type AvailabilityReason = 'READABLE' | 'PERMISSION_DENIED' | 'NOT_FOUND';
/**
* @hidden
* @beta */
type PreviewableType = SchemaType | CrossDatasetType | GlobalDocumentReferenceType;
/**
* @hidden
* @beta */
interface ApiConfig {
projectId: string;
dataset: string;
}
/**
* @hidden
* @beta */
type DocumentAvailability = {
available: true;
reason: 'READABLE';
} | {
available: false;
reason: 'PERMISSION_DENIED' | 'NOT_FOUND' | 'VERSION_DELETED';
};
/**
* @hidden
* @beta */
interface DraftsModelDocumentAvailability {
/**
* document readability for the published document
*/
published: DocumentAvailability;
/**
* document readability for the draft document
*/
draft: DocumentAvailability;
/**
* document readability for the version document
*/
version?: DocumentAvailability;
}
/**
* @hidden
* @beta */
interface DocumentStackAvailability {
/**
* Document id
*/
id: string;
/**
* Availability for the document in this stack
*/
availability: DocumentAvailability;
}
/**
* @hidden
* @beta */
interface DraftsModelDocument<T extends SanityDocumentLike = SanityDocumentLike> {
id: string;
type: string | null;
draft: {
availability: DocumentAvailability;
snapshot: T | undefined;
};
published: {
availability: DocumentAvailability;
snapshot: T | undefined;
};
version?: {
availability: DocumentAvailability;
snapshot: T | undefined;
};
}
/**
* Event emitted to notify preview subscribers when they need to refetch a document being previewed
* - 'connected' will happen when the store is connected to the invalidation channel, both initially and after a reconnect after a connection loss
* - 'mutation' will happen when a document has been mutated and the store needs to refetch a document
* @hidden
* @beta
*/
type InvalidationChannelEvent = {
type: 'connected';
} | {
type: 'mutation';
documentId: string;
visibility: string;
};
/**
* @hidden
* @beta */
interface PreparedSnapshot {
type?: PreviewableType;
snapshot: PreviewValue | null | undefined;
}
/** @internal */
type ObserveDocumentTypeFromIdFn = (id: string, apiConfig?: ApiConfig, perspective?: StackablePerspective[]) => Observable<string | undefined>;
/**
* @hidden
* @beta */
interface ObservePathsFn {
(value: Previewable, paths: (string | PreviewPath)[], apiConfig?: ApiConfig, perspective?: StackablePerspective[]): Observable<PreviewValue | SanityDocumentLike | Reference | string | null>;
}
/**
* @hidden
* @beta */
interface ObserveDocumentAvailabilityFn {
(id: string, options?: {
version?: string;
}): Observable<{
draft: DocumentAvailability;
published: DocumentAvailability;
version?: DocumentAvailability;
}>;
}
/**
* @hidden
* @beta */
type ObserveForPreviewFn = (value: Previewable, type: PreviewableType, options?: {
viewOptions?: PrepareViewOptions;
perspective?: StackablePerspective[];
apiConfig?: ApiConfig;
}) => Observable<PreparedSnapshot>;
/**
* The document preview store supports subscribing to content for previewing purposes.
* Documents observed by this store will be kept in sync and receive real-time updates from all collaborators,
* but has no support for optimistic updates, so a