@sanity/sdk-react 3.11.0-next.20261029105031 → 3.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/README.md +13 -9
  2. package/dist/_exports/collaboration.d.ts +255 -0
  3. package/dist/_exports/collaboration.d.ts.map +1 -0
  4. package/dist/_exports/collaboration.js +244 -0
  5. package/dist/_exports/collaboration.js.map +1 -0
  6. package/dist/_exports/dashboard.js +2 -1
  7. package/dist/_exports/dashboard.js.map +1 -1
  8. package/dist/index.d.ts +16 -22
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +49 -159
  11. package/dist/index.js.map +1 -1
  12. package/dist/useCommentList-CXi_IIwA.js +167 -0
  13. package/dist/useCommentList-CXi_IIwA.js.map +1 -0
  14. package/dist/useNormalizedResourceOptions-BKg7aOKA.d.ts +22 -0
  15. package/dist/useNormalizedResourceOptions-BKg7aOKA.d.ts.map +1 -0
  16. package/dist/useSanityInstance-C0a_Ou_p.js +10 -0
  17. package/dist/useSanityInstance-C0a_Ou_p.js.map +1 -0
  18. package/dist/{useStudioWorkspacesByProjectIdDataset-FRofq7KL.js → useStudioWorkspacesByProjectIdDataset-Cx-Q44d0.js} +4 -8
  19. package/dist/useStudioWorkspacesByProjectIdDataset-Cx-Q44d0.js.map +1 -0
  20. package/package.json +3 -2
  21. package/src/_exports/collaboration.ts +13 -0
  22. package/src/_exports/sdk-react.ts +6 -3
  23. package/src/hooks/comments/legacy/noCommentsError.ts +20 -0
  24. package/src/hooks/comments/legacy/useCommentActions.test.tsx +150 -0
  25. package/src/hooks/comments/legacy/useCommentActions.ts +116 -0
  26. package/src/hooks/comments/legacy/useCommentThreads.test.tsx +107 -0
  27. package/src/hooks/comments/legacy/useCommentThreads.ts +83 -0
  28. package/src/hooks/comments/legacy/useComments.test.tsx +242 -0
  29. package/src/hooks/comments/legacy/useComments.ts +68 -0
  30. package/src/hooks/comments/useCommentActions.test.tsx +33 -13
  31. package/src/hooks/comments/useCommentActions.ts +31 -11
  32. package/src/hooks/comments/useCommentList.ts +34 -19
  33. package/src/hooks/comments/useCommentThreads.test.tsx +207 -19
  34. package/src/hooks/comments/useCommentThreads.ts +33 -27
  35. package/src/hooks/comments/useComments.test.tsx +33 -126
  36. package/src/hooks/comments/useComments.ts +31 -12
  37. package/src/hooks/comments/useCommentsQuery.test.tsx +259 -0
  38. package/src/hooks/comments/useCommentsQuery.ts +105 -0
  39. package/src/hooks/document/useEditDocument.ts +0 -1
  40. package/dist/useStudioWorkspacesByProjectIdDataset-FRofq7KL.js.map +0 -1
@@ -0,0 +1,167 @@
1
+ import { n as SanityInstanceContext, t as useSanityInstance } from "./useSanityInstance-C0a_Ou_p.js";
2
+ import { c } from "react-compiler-runtime";
3
+ import "@sanity/sdk";
4
+ import { trackHookMounted } from "@sanity/sdk/_internal";
5
+ import { createContext, useContext, useEffect, useMemo, useRef, useState, useSyncExternalStore, useTransition } from "react";
6
+ /** Context for resources.
7
+ * @beta
8
+ */
9
+ const ResourcesContext = createContext({}), ResourceContext = createContext(void 0), PerspectiveContext = createContext(void 0);
10
+ /**
11
+ * Pure function that normalizes options by resolving `resourceName` to a `DocumentResource`
12
+ * using the provided resources map. Use this when options are only available at call time
13
+ * (e.g. inside a callback) and you cannot call the {@link useNormalizedResourceOptions} hook.
14
+ *
15
+ * @typeParam T - The options type (must include optional resource field)
16
+ * @param options - Options that may include `resourceName` and/or `resource`
17
+ * @param resources - Map of resource names to DocumentResource (e.g. from ResourcesContext)
18
+ * @param contextResource - Resource from context (from ResourceContext)
19
+ * @param contextPerspective - Perspective from context (from PerspectiveContext)
20
+ * @returns Normalized options with `resourceName` removed and `resource` resolved
21
+ * @internal
22
+ */
23
+ function normalizeResourceOptions(options, resources, contextResource, contextPerspective) {
24
+ let { resourceName, projectId, dataset, ...rest } = options, resource = options.resource;
25
+ if (resourceName && resource) throw Error(`Resource name ${JSON.stringify(resourceName)} and resource ${JSON.stringify(resource)} cannot be used together.`);
26
+ let resolvedResource;
27
+ if (resource) resolvedResource = resource;
28
+ else if (resourceName) {
29
+ if (!Object.hasOwn(resources, resourceName)) throw Error(`There's no resource named ${JSON.stringify(resourceName)} in context. Please use <ResourceProvider>.`);
30
+ resolvedResource = resources[resourceName];
31
+ }
32
+ !resolvedResource && projectId && dataset && (resolvedResource = {
33
+ projectId,
34
+ dataset
35
+ }), resolvedResource ||= contextResource;
36
+ let resolvedPerspective = Object.hasOwn(options, "perspective") ? options.perspective : contextPerspective;
37
+ return {
38
+ ...rest,
39
+ ...resolvedResource !== void 0 && { resource: resolvedResource },
40
+ ...resolvedPerspective !== void 0 && { perspective: resolvedPerspective }
41
+ };
42
+ }
43
+ /**
44
+ * Returns the effective context resource: the `ResourceContext` value if set,
45
+ * otherwise a resource synthesized from the current `SanityInstance` config
46
+ * (tier-d fallback — returns `undefined` for studio-style configs with no project).
47
+ *
48
+ * @internal
49
+ */
50
+ function useEffectiveContextResource() {
51
+ let $ = c(5), contextResource = useContext(ResourceContext), instance = useContext(SanityInstanceContext), t0;
52
+ $[0] === instance?.config ? t0 = $[1] : (t0 = instance?.config ?? {}, $[0] = instance?.config, $[1] = t0);
53
+ let { projectId, dataset, resource: configResource } = t0, t1;
54
+ bb0: {
55
+ if (contextResource) {
56
+ t1 = contextResource;
57
+ break bb0;
58
+ }
59
+ if (configResource) {
60
+ t1 = configResource;
61
+ break bb0;
62
+ }
63
+ if (projectId && dataset) {
64
+ let t2;
65
+ $[2] !== dataset || $[3] !== projectId ? (t2 = {
66
+ projectId,
67
+ dataset
68
+ }, $[2] = dataset, $[3] = projectId, $[4] = t2) : t2 = $[4], t1 = t2;
69
+ break bb0;
70
+ }
71
+ t1 = void 0;
72
+ }
73
+ return t1;
74
+ }
75
+ /**
76
+ * Normalizes hook options by resolving `resourceName` to a `DocumentResource`.
77
+ *
78
+ * Resolution priority for resource:
79
+ * 1. Explicit `resource` or `resourceName` in options
80
+ * 2. Bare `projectId`/`dataset` pair in options → synthesized into a resource
81
+ * 3. `ResourceContext` value (set by `ResourceProvider` / `SDKProvider`)
82
+ * 4. Current `SanityInstance` config — falls back to `undefined` for studio configs
83
+ *
84
+ * Resolution priority for perspective:
85
+ * 1. Explicit `perspective` in options
86
+ * 2. `PerspectiveContext` value (set by `ResourceProvider`)
87
+ *
88
+ * @internal
89
+ */
90
+ function useNormalizedResourceOptions(options) {
91
+ let $ = c(5), resources = useContext(ResourcesContext), effectiveContextResource = useEffectiveContextResource(), contextPerspective = useContext(PerspectiveContext), t0;
92
+ return $[0] !== contextPerspective || $[1] !== effectiveContextResource || $[2] !== options || $[3] !== resources ? (t0 = normalizeResourceOptions(options, resources, effectiveContextResource, contextPerspective), $[0] = contextPerspective, $[1] = effectiveContextResource, $[2] = options, $[3] = resources, $[4] = t0) : t0 = $[4], t0;
93
+ }
94
+ /**
95
+ * Tracks the first usage of a named hook per SDK session.
96
+ * If the telemetry manager hasn't initialized yet, the hook
97
+ * name is buffered and flushed when it becomes available.
98
+ *
99
+ * Uses a ref to ensure the tracking call only happens once per
100
+ * component mount, avoiding repeated WeakMap lookups on re-renders.
101
+ *
102
+ * Call at the top of any public hook whose adoption we want to measure.
103
+ *
104
+ * @internal
105
+ */
106
+ function useTrackHookUsage(hookName) {
107
+ let instance = useSanityInstance(), tracked = useRef(null);
108
+ tracked.current === null && (tracked.current = !0, trackHookMounted(instance, hookName));
109
+ }
110
+ /**
111
+ * Non-hook variant for tracking hook usage when an instance is already
112
+ * available (avoids an extra `useSanityInstance` call in hooks that
113
+ * already have the instance).
114
+ *
115
+ * @internal
116
+ */
117
+ function trackHookUsage(instance, hookName) {
118
+ trackHookMounted(instance, hookName);
119
+ }
120
+ /**
121
+ * Shared body of the comment read hooks.
122
+ *
123
+ * Suspends until the first snapshot arrives. Changing documents or filters
124
+ * happens in a transition, so the list already on screen stays put and
125
+ * `isPending` reports the swap instead of the component suspending again. The
126
+ * previous read is aborted, which drops its listener when nothing else is
127
+ * reading it.
128
+ *
129
+ * A listener that fails after the list has loaded is reported through `error`
130
+ * rather than thrown: the list stays on screen, frozen as it last stood.
131
+ *
132
+ * @internal
133
+ */
134
+ function useCommentList(hookName, options, { getState, getErrorState, resolve, getKey, parseKey }) {
135
+ let instance = useSanityInstance();
136
+ trackHookUsage(instance, hookName);
137
+ let normalized = useNormalizedResourceOptions(options), [isPending, startTransition] = useTransition(), key = getKey(normalized), [deferredKey, setDeferredKey] = useState(key), abortRef = useRef(new AbortController());
138
+ useEffect(() => {
139
+ key !== deferredKey && startTransition(() => {
140
+ abortRef.current.signal.aborted || (abortRef.current.abort(), abortRef.current = new AbortController()), setDeferredKey(key);
141
+ });
142
+ }, [deferredKey, key]);
143
+ let deferred = useMemo(() => parseKey(deferredKey), [deferredKey, parseKey]), { getCurrent, subscribe } = useMemo(() => getState(instance, deferred), [
144
+ deferred,
145
+ getState,
146
+ instance
147
+ ]), errorSource = useMemo(() => getErrorState(instance, deferred), [
148
+ deferred,
149
+ getErrorState,
150
+ instance
151
+ ]);
152
+ if (getCurrent() === void 0) {
153
+ let currentSignal = abortRef.current.signal;
154
+ throw resolve(instance, {
155
+ ...deferred,
156
+ signal: currentSignal
157
+ });
158
+ }
159
+ return {
160
+ value: useSyncExternalStore(subscribe, getCurrent),
161
+ isPending,
162
+ error: useSyncExternalStore(errorSource.subscribe, errorSource.getCurrent)
163
+ };
164
+ }
165
+ export { useEffectiveContextResource as a, ResourceContext as c, normalizeResourceOptions as i, ResourcesContext as l, trackHookUsage as n, useNormalizedResourceOptions as o, useTrackHookUsage as r, PerspectiveContext as s, useCommentList as t };
166
+
167
+ //# sourceMappingURL=useCommentList-CXi_IIwA.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useCommentList-CXi_IIwA.js","names":["DocumentResource","createContext","ResourcesContext","Record","DocumentResource","createContext","ResourceContext","undefined","PerspectiveHandle","createContext","PerspectiveContext","undefined","DocumentResource","PerspectiveHandle","useContext","useMemo","ResourceContext","PerspectiveContext","ResourcesContext","SanityInstanceContext","NormalizedResourceFields","WithResourceNameSupport","resource","T","resourceName","normalizeResourceOptions","projectId","dataset","perspective","options","resources","Record","contextResource","contextPerspective","Omit","rest","Error","JSON","stringify","resolvedResource","Object","hasOwn","resolvedPerspective","undefined","useEffectiveContextResource","$","_c","instance","t0","config","configResource","t1","bb0","t2","useNormalizedResourceOptions","effectiveContextResource","_c","SanityInstance","trackHookMounted","useRef","useSanityInstance","useTrackHookUsage","hookName","instance","tracked","current","trackHookUsage","DocumentResource","SanityInstance","StateSource","useEffect","useMemo","useRef","useState","useSyncExternalStore","useTransition","useSanityInstance","useNormalizedResourceOptions","WithResourceNameSupport","trackHookUsage","CommentListSource","getState","instance","options","TOptions","TValue","getErrorState","resolve","signal","AbortSignal","Promise","getKey","parseKey","key","useCommentList","resource","hookName","value","isPending","error","normalized","startTransition","deferredKey","setDeferredKey","abortRef","AbortController","current","aborted","abort","deferred","getCurrent","subscribe","errorSource","undefined","currentSignal"],"sources":["../src/context/ResourcesContext.tsx","../src/context/DefaultResourceContext.ts","../src/context/PerspectiveContext.ts","../src/hooks/helpers/useNormalizedResourceOptions.ts","../src/hooks/helpers/useTrackHookUsage.ts","../src/hooks/comments/useCommentList.ts"],"sourcesContent":["import {type DocumentResource} from '@sanity/sdk'\nimport {createContext} from 'react'\n\n/** Context for resources.\n * @beta\n */\nexport const ResourcesContext = createContext<Record<string, DocumentResource>>({})\n","import {type DocumentResource} from '@sanity/sdk'\nimport {createContext} from 'react'\n\n/**\n * Provides the active DocumentResource for a subtree.\n * Set by ResourceProvider; read by useNormalizedResourceOptions as a fallback\n * when hooks receive no explicit resource or resourceName.\n * @internal\n */\nexport const ResourceContext = createContext<DocumentResource | undefined>(undefined)\n","import {type PerspectiveHandle} from '@sanity/sdk'\nimport {createContext} from 'react'\n\n/**\n * Provides the active perspective for a subtree.\n * Set by ResourceProvider; injected by useNormalizedResourceOptions when\n * the hook's options don't include an explicit perspective.\n * @internal\n */\nexport const PerspectiveContext = createContext<PerspectiveHandle['perspective'] | undefined>(\n undefined,\n)\n","import {type DocumentResource, type PerspectiveHandle} from '@sanity/sdk'\nimport {useContext, useMemo} from 'react'\n\nimport {ResourceContext} from '../../context/DefaultResourceContext'\nimport {PerspectiveContext} from '../../context/PerspectiveContext'\nimport {ResourcesContext} from '../../context/ResourcesContext'\nimport {SanityInstanceContext} from '../../context/SanityInstanceContext'\n\ntype NormalizedResourceFields = 'resourceName' | 'projectId' | 'dataset'\n\n/**\n * Adds React hook support (resourceName resolution) to core types.\n * Prefer using the React-layer handle types (ResourceHandle, DocumentHandle)\n * from `@sanity/sdk-react` — this wrapper is kept for cases where overloads\n * don't fit (e.g. non-handle options objects).\n *\n * @typeParam T - The core type to extend (must have optional `resource` field)\n * @beta\n */\nexport type WithResourceNameSupport<T extends {resource?: DocumentResource}> = T & {\n /**\n * Optional name of a resource to resolve from context.\n * If provided, will be resolved to a `DocumentResource` via `ResourcesContext`.\n * @beta\n */\n resourceName?: string\n}\n\n/**\n * Pure function that normalizes options by resolving `resourceName` to a `DocumentResource`\n * using the provided resources map. Use this when options are only available at call time\n * (e.g. inside a callback) and you cannot call the {@link useNormalizedResourceOptions} hook.\n *\n * @typeParam T - The options type (must include optional resource field)\n * @param options - Options that may include `resourceName` and/or `resource`\n * @param resources - Map of resource names to DocumentResource (e.g. from ResourcesContext)\n * @param contextResource - Resource from context (from ResourceContext)\n * @param contextPerspective - Perspective from context (from PerspectiveContext)\n * @returns Normalized options with `resourceName` removed and `resource` resolved\n * @internal\n */\nexport function normalizeResourceOptions<\n T extends {\n resource?: DocumentResource\n resourceName?: string\n projectId?: string\n dataset?: string\n perspective?: unknown\n },\n>(\n options: T,\n resources: Record<string, DocumentResource>,\n contextResource?: DocumentResource,\n contextPerspective?: PerspectiveHandle['perspective'],\n): Omit<T, NormalizedResourceFields> {\n const {resourceName, projectId, dataset, ...rest} = options\n const resource = options.resource\n\n if (resourceName && resource) {\n throw new Error(\n `Resource name ${JSON.stringify(resourceName)} and resource ${JSON.stringify(resource)} cannot be used together.`,\n )\n }\n\n let resolvedResource: DocumentResource | undefined\n\n // Tier (a): explicit resource object or resourceName lookup\n if (resource) {\n resolvedResource = resource\n } else if (resourceName) {\n if (!Object.hasOwn(resources, resourceName)) {\n throw new Error(\n `There's no resource named ${JSON.stringify(resourceName)} in context. Please use <ResourceProvider>.`,\n )\n }\n resolvedResource = resources[resourceName]\n }\n\n // Tier (b): projectId or dataset in options → synthesize a resource\n if (!resolvedResource && projectId && dataset) {\n resolvedResource = {\n projectId,\n dataset,\n }\n }\n\n // Tier (c): fall back to whatever ResourceContext provides\n if (!resolvedResource) {\n resolvedResource = contextResource\n }\n\n // Inject perspective from context when not explicitly provided in options\n const resolvedPerspective = Object.hasOwn(options, 'perspective')\n ? options.perspective\n : contextPerspective\n\n return {\n ...rest,\n ...(resolvedResource !== undefined && {resource: resolvedResource}),\n ...(resolvedPerspective !== undefined && {perspective: resolvedPerspective}),\n }\n}\n\n/**\n * Returns the effective context resource: the `ResourceContext` value if set,\n * otherwise a resource synthesized from the current `SanityInstance` config\n * (tier-d fallback — returns `undefined` for studio-style configs with no project).\n *\n * @internal\n */\nexport function useEffectiveContextResource(): DocumentResource | undefined {\n const contextResource = useContext(ResourceContext)\n const instance = useContext(SanityInstanceContext)\n const {projectId, dataset, resource: configResource} = instance?.config ?? {}\n\n return useMemo(() => {\n if (contextResource) return contextResource\n if (configResource) return configResource\n if (projectId && dataset) return {projectId, dataset}\n return undefined\n }, [contextResource, configResource, projectId, dataset])\n}\n\n/**\n * Normalizes hook options by resolving `resourceName` to a `DocumentResource`.\n *\n * Resolution priority for resource:\n * 1. Explicit `resource` or `resourceName` in options\n * 2. Bare `projectId`/`dataset` pair in options → synthesized into a resource\n * 3. `ResourceContext` value (set by `ResourceProvider` / `SDKProvider`)\n * 4. Current `SanityInstance` config — falls back to `undefined` for studio configs\n *\n * Resolution priority for perspective:\n * 1. Explicit `perspective` in options\n * 2. `PerspectiveContext` value (set by `ResourceProvider`)\n *\n * @internal\n */\nexport function useNormalizedResourceOptions<\n T extends {\n resource?: DocumentResource\n resourceName?: string\n projectId?: string\n dataset?: string\n perspective?: PerspectiveHandle['perspective']\n },\n>(\n options: T,\n): Omit<T, NormalizedResourceFields> & {\n resource?: DocumentResource\n perspective?: PerspectiveHandle['perspective']\n} {\n const resources = useContext(ResourcesContext)\n const effectiveContextResource = useEffectiveContextResource()\n const contextPerspective = useContext(PerspectiveContext)\n\n return normalizeResourceOptions(options, resources, effectiveContextResource, contextPerspective)\n}\n","import {type SanityInstance} from '@sanity/sdk'\nimport {trackHookMounted} from '@sanity/sdk/_internal'\nimport {useRef} from 'react'\n\nimport {useSanityInstance} from '../context/useSanityInstance'\n\n/**\n * Tracks the first usage of a named hook per SDK session.\n * If the telemetry manager hasn't initialized yet, the hook\n * name is buffered and flushed when it becomes available.\n *\n * Uses a ref to ensure the tracking call only happens once per\n * component mount, avoiding repeated WeakMap lookups on re-renders.\n *\n * Call at the top of any public hook whose adoption we want to measure.\n *\n * @internal\n */\nexport function useTrackHookUsage(hookName: string): void {\n const instance = useSanityInstance()\n const tracked = useRef<true | null>(null)\n if (tracked.current === null) {\n tracked.current = true\n trackHookMounted(instance, hookName)\n }\n}\n\n/**\n * Non-hook variant for tracking hook usage when an instance is already\n * available (avoids an extra `useSanityInstance` call in hooks that\n * already have the instance).\n *\n * @internal\n */\nexport function trackHookUsage(instance: SanityInstance, hookName: string): void {\n trackHookMounted(instance, hookName)\n}\n","import {type DocumentResource, type SanityInstance, type StateSource} from '@sanity/sdk'\nimport {useEffect, useMemo, useRef, useState, useSyncExternalStore, useTransition} from 'react'\n\nimport {useSanityInstance} from '../context/useSanityInstance'\nimport {\n useNormalizedResourceOptions,\n type WithResourceNameSupport,\n} from '../helpers/useNormalizedResourceOptions'\nimport {trackHookUsage} from '../helpers/useTrackHookUsage'\n\n/**\n * The core functions backing one read hook, plus the option-key pair that keeps\n * its state source steady across renders.\n *\n * @internal\n */\nexport interface CommentListSource<TOptions, TValue> {\n getState: (instance: SanityInstance, options: TOptions) => StateSource<TValue | undefined>\n getErrorState: (instance: SanityInstance, options: TOptions) => StateSource<unknown>\n resolve: (instance: SanityInstance, options: TOptions & {signal?: AbortSignal}) => Promise<TValue>\n getKey: (options: TOptions) => string\n parseKey: (key: string) => TOptions\n}\n\n/**\n * Shared body of the comment read hooks.\n *\n * Suspends until the first snapshot arrives. Changing documents or filters\n * happens in a transition, so the list already on screen stays put and\n * `isPending` reports the swap instead of the component suspending again. The\n * previous read is aborted, which drops its listener when nothing else is\n * reading it.\n *\n * A listener that fails after the list has loaded is reported through `error`\n * rather than thrown: the list stays on screen, frozen as it last stood.\n *\n * @internal\n */\nexport function useCommentList<TOptions extends {resource?: DocumentResource}, TValue>(\n hookName: string,\n options: WithResourceNameSupport<TOptions>,\n {getState, getErrorState, resolve, getKey, parseKey}: CommentListSource<TOptions, TValue>,\n): {value: TValue; isPending: boolean; error: unknown} {\n const instance = useSanityInstance()\n trackHookUsage(instance, hookName)\n\n const normalized = useNormalizedResourceOptions(options) as TOptions\n const [isPending, startTransition] = useTransition()\n\n const key = getKey(normalized)\n // Held one render behind `key`, so the swap can happen inside a transition.\n const [deferredKey, setDeferredKey] = useState(key)\n const abortRef = useRef<AbortController>(new AbortController())\n\n useEffect(() => {\n if (key === deferredKey) return\n\n startTransition(() => {\n if (!abortRef.current.signal.aborted) {\n abortRef.current.abort()\n abortRef.current = new AbortController()\n }\n setDeferredKey(key)\n })\n }, [deferredKey, key])\n\n const deferred = useMemo(() => parseKey(deferredKey), [deferredKey, parseKey])\n const {getCurrent, subscribe} = useMemo(\n () => getState(instance, deferred),\n [deferred, getState, instance],\n )\n const errorSource = useMemo(\n () => getErrorState(instance, deferred),\n [deferred, getErrorState, instance],\n )\n\n if (getCurrent() === undefined) {\n // Reading the ref mid-render is safe here: React runs no effects for a\n // render that suspends, so the signal captured now cannot be swapped\n // underneath this pass.\n const currentSignal = abortRef.current.signal\n\n // eslint-disable-next-line react-hooks/refs -- intentional during a suspended render; see above\n throw resolve(instance, {...deferred, signal: currentSignal})\n }\n\n // Not memoised: every caller destructures this immediately and memoises its\n // own result object, so a stable identity here would never be observed.\n return {\n value: useSyncExternalStore(subscribe, getCurrent) as TValue,\n isPending,\n error: useSyncExternalStore(errorSource.subscribe, errorSource.getCurrent),\n }\n}\n"],"mappings":";;;;;;;;AAMA,MAAaE,mBAAmBD,cAAgD,CAAC,CAAC,GCGrEK,kBAAkBD,cAA4CE,KAAAA,CAAS,GCAvEG,qBAAqBD,cAChCE,KAAAA,CACF;;;;;;;;;;;;;;AC8BA,SAAgBc,yBASdI,SACAC,WACAE,iBACAC,oBACmC;CACnC,IAAM,EAACT,cAAcE,WAAWC,SAAS,GAAGQ,SAAQN,SAC9CP,WAAWO,QAAQP;CAEzB,IAAIE,gBAAgBF,UAClB,MAAUc,MACR,iBAAiBC,KAAKC,UAAUd,YAAY,EAAC,gBAAiBa,KAAKC,UAAUhB,QAAQ,EAAC,0BACxF;CAGF,IAAIiB;CAGJ,IAAIjB,UACFiB,mBAAmBjB;MACd,IAAIE,cAAc;EACvB,IAAI,CAACgB,OAAOC,OAAOX,WAAWN,YAAY,GACxC,MAAUY,MACR,6BAA6BC,KAAKC,UAAUd,YAAY,EAAC,4CAC3D;EAEFe,mBAAmBT,UAAUN;CAC/B;CAWA,AARI,CAACe,oBAAoBb,aAAaC,YACpCY,mBAAmB;EACjBb;EACAC;CACF,IAIF,AACEY,qBAAmBP;CAIrB,IAAMU,sBAAsBF,OAAOC,OAAOZ,SAAS,aAAa,IAC5DA,QAAQD,cACRK;CAEJ,OAAO;EACL,GAAGE;EACH,GAAII,qBAAqBI,KAAAA,KAAa,EAACrB,UAAUiB,iBAAgB;EACjE,GAAIG,wBAAwBC,KAAAA,KAAa,EAACf,aAAac,oBAAmB;CAC5E;AACF;;;;;;;;AASA,SAAOE,8BAAA;CAAA,IAAAC,IAAAC,EAAA,CAAA,GACLd,kBAAwBlB,WAAWE,eAAe,GAClD+B,WAAiBjC,WAAWK,qBAAqB,GAAC6B;CAAA,AAAAH,EAAA,OAAAE,UAAAE,SAC2BD,KAAAH,EAAA,MAAtBG,KAAAD,UAAQE,UAAR,CAAqB,GAACJ,EAAA,KAAAE,UAAAE,QAAAJ,EAAA,KAAAG;CAA7E,IAAA,EAAAtB,WAAAC,SAAAL,UAAA4B,mBAAuDF,IAAsBG;CAAAC,KAAA;EAG3E,IAAIpB,iBAAe;GAAEmB,KAAOnB;GAAP,MAAAoB;EAAsB;EAC3C,IAAIF,gBAAc;GAAEC,KAAOD;GAAP,MAAAE;EAAqB;EACzC,IAAI1B,aAAAC,SAAoB;GAAA,IAAA0B;GAAEF,AAAFN,EAAA,OAAAlB,WAAAkB,EAAA,OAAAnB,aAAS2B,KAAA;IAAA3B;IAAAC;GAAmB,GAACkB,EAAA,KAAAlB,SAAAkB,EAAA,KAAAnB,WAAAmB,EAAA,KAAAQ,MAAAA,KAAAR,EAAA,IAA3BM,KAAOE;GAAP,MAAAD;EAA2B;EACrDD,KAAOR,KAAAA;CAAS;CAAA,OAJXQ;AAKkD;;;;;;;;;;;;;;;;AAkB3D,SAAOG,6BAAAzB,SAAA;CAAA,IAAAgB,IAAAC,EAAA,CAAA,GAcLhB,YAAkBhB,WAAWI,gBAAgB,GAC7CqC,2BAAiCX,4BAA4B,GAC7DX,qBAA2BnB,WAAWG,kBAAkB,GAAC+B;CAEwC,OAFxCH,EAAA,OAAAZ,sBAAAY,EAAA,OAAAU,4BAAAV,EAAA,OAAAhB,WAAAgB,EAAA,OAAAf,aAElDkB,KAAAvB,yBAAyBI,SAASC,WAAWyB,0BAA0BtB,kBAAkB,GAACY,EAAA,KAAAZ,oBAAAY,EAAA,KAAAU,0BAAAV,EAAA,KAAAhB,SAAAgB,EAAA,KAAAf,WAAAe,EAAA,KAAAG,MAAAA,KAAAH,EAAA,IAA1FG;AAA0F;;;;;;;;;;;;;AC1InG,SAAOa,kBAAAC,UAAA;CACL,IAAAC,WAAiBH,kBAAkB,GACnCI,UAAgBL,OAAoB,IAAI;CACxC,AAAIK,QAAOC,YAAa,SACtBD,QAAOC,UAAW,IAClBP,iBAAiBK,UAAUD,QAAQ;AACpC;;;;;;;;AAUH,SAAgBI,eAAeH,UAA0BD,UAAwB;CAC/EJ,iBAAiBK,UAAUD,QAAQ;AACrC;;;;;;;;;;;;;;;ACEA,SAAgBgC,eACdE,UACAb,SACA,EAACF,UAAUK,eAAeC,SAASI,QAAQC,YACU;CACrD,IAAMV,WAAWN,kBAAkB;CACnCG,eAAeG,UAAUc,QAAQ;CAEjC,IAAMI,aAAavB,6BAA6BM,OAAO,GACjD,CAACe,WAAWG,mBAAmB1B,cAAc,GAE7CkB,MAAMF,OAAOS,UAAU,GAEvB,CAACE,aAAaC,kBAAkB9B,SAASoB,GAAG,GAC5CW,WAAWhC,OAAwB,IAAIiC,gBAAgB,CAAC;CAE9DnC,gBAAgB;EACd,AAAIuB,QAAQS,eAEZD,sBAAsB;GAKpBE,AAJKC,SAASE,QAAQlB,OAAOmB,YAC3BH,SAASE,QAAQE,MAAM,GACvBJ,SAASE,UAAU,IAAID,gBAAgB,IAEzCF,eAAeV,GAAG;EACpB,CAAC;CACH,GAAG,CAACS,aAAaT,GAAG,CAAC;CAErB,IAAMgB,WAAWtC,cAAcqB,SAASU,WAAW,GAAG,CAACA,aAAaV,QAAQ,CAAC,GACvE,EAACkB,YAAYC,cAAaxC,cACxBU,SAASC,UAAU2B,QAAQ,GACjC;EAACA;EAAU5B;EAAUC;CAAQ,CAC/B,GACM8B,cAAczC,cACZe,cAAcJ,UAAU2B,QAAQ,GACtC;EAACA;EAAUvB;EAAeJ;CAAQ,CACpC;CAEA,IAAI4B,WAAW,MAAMG,KAAAA,GAAW;EAI9B,IAAMC,gBAAgBV,SAASE,QAAQlB;EAGvC,MAAMD,QAAQL,UAAU;GAAC,GAAG2B;GAAUrB,QAAQ0B;EAAa,CAAC;CAC9D;CAIA,OAAO;EACLjB,OAAOvB,qBAAqBqC,WAAWD,UAAU;EACjDZ;EACAC,OAAOzB,qBAAqBsC,YAAYD,WAAWC,YAAYF,UAAU;CAC3E;AACF"}
@@ -0,0 +1,22 @@
1
+ import { DocumentResource } from "@sanity/sdk";
2
+ /**
3
+ * Adds React hook support (resourceName resolution) to core types.
4
+ * Prefer using the React-layer handle types (ResourceHandle, DocumentHandle)
5
+ * from `@sanity/sdk-react` — this wrapper is kept for cases where overloads
6
+ * don't fit (e.g. non-handle options objects).
7
+ *
8
+ * @typeParam T - The core type to extend (must have optional `resource` field)
9
+ * @beta
10
+ */
11
+ type WithResourceNameSupport<T extends {
12
+ resource?: DocumentResource;
13
+ }> = T & {
14
+ /**
15
+ * Optional name of a resource to resolve from context.
16
+ * If provided, will be resolved to a `DocumentResource` via `ResourcesContext`.
17
+ * @beta
18
+ */
19
+ resourceName?: string;
20
+ };
21
+ export { WithResourceNameSupport as t };
22
+ //# sourceMappingURL=useNormalizedResourceOptions-BKg7aOKA.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useNormalizedResourceOptions-BKg7aOKA.d.ts","names":["DocumentResource","PerspectiveHandle","NormalizedResourceFields","WithResourceNameSupport","T","normalizeResourceOptions","Record","Omit","useEffectiveContextResource","useNormalizedResourceOptions"],"sources":["../src/hooks/helpers/useNormalizedResourceOptions.ts"],"mappings":";;;;;;;;;;KAmBYG,wBAAwB;EAAW,WAAWH;KAAqBI;;;;;;EAM7E"}
@@ -0,0 +1,10 @@
1
+ import "@sanity/sdk";
2
+ import { createContext, useContext } from "react";
3
+ const SanityInstanceContext = createContext(null), useSanityInstance = () => {
4
+ let instance = useContext(SanityInstanceContext);
5
+ if (!instance) throw Error("SanityInstance context not found. Please ensure that your component is wrapped in a ResourceProvider or a SanityApp component.");
6
+ return instance;
7
+ };
8
+ export { SanityInstanceContext as n, useSanityInstance as t };
9
+
10
+ //# sourceMappingURL=useSanityInstance-C0a_Ou_p.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useSanityInstance-C0a_Ou_p.js","names":["SanityInstance","createContext","SanityInstanceContext","SanityInstance","useContext","SanityInstanceContext","useSanityInstance","instance","Error"],"sources":["../src/context/SanityInstanceContext.ts","../src/hooks/context/useSanityInstance.ts"],"sourcesContent":["import {type SanityInstance} from '@sanity/sdk'\nimport {createContext} from 'react'\n\nexport const SanityInstanceContext = createContext<SanityInstance | null>(null)\n","import {type SanityInstance} from '@sanity/sdk'\nimport {useContext} from 'react'\n\nimport {SanityInstanceContext} from '../../context/SanityInstanceContext'\n\n/**\n * Retrieves the current Sanity instance from context\n *\n * @public\n *\n * @category Platform\n * @returns The current Sanity instance\n *\n * @remarks\n * This hook accesses the nearest Sanity instance from the React context.\n * The hook must be used within a component wrapped by a `ResourceProvider` or `SanityApp`.\n *\n * @example Get the current instance\n * ```tsx\n * const instance = useSanityInstance()\n * console.log(instance.config.projectId)\n * ```\n *\n * @throws Error if no SanityInstance is found in context\n */\nexport const useSanityInstance = (): SanityInstance => {\n const instance = useContext(SanityInstanceContext)\n\n if (!instance) {\n throw new Error(\n `SanityInstance context not found. Please ensure that your component is wrapped in a ResourceProvider or a SanityApp component.`,\n )\n }\n\n return instance\n}\n"],"mappings":";;AAGA,MAAaE,wBAAwBD,cAAqC,IAAI,GCsBjEK,0BAAoB;CAC/B,IAAAC,WAAiBH,WAAWC,qBAAqB;CAEjD,IAAI,CAACE,UACH,MAAUC,MACR,gIACF;CACD,OAEMD;AAAQ"}
@@ -1,20 +1,16 @@
1
+ import { t as useSanityInstance } from "./useSanityInstance-C0a_Ou_p.js";
1
2
  import { c } from "react-compiler-runtime";
2
3
  import "@sanity/client";
3
4
  import { AuthStateType, getAuthState, getDashboardOrganizationId, setAuthToken } from "@sanity/sdk";
4
5
  import { getApplicationOrigin, getDashboardMessageBus, getTopicState, isMessageBusEnvironment, requireDashboardMessageBus, resolveTopic } from "@sanity/sdk/_internal";
5
6
  import * as React$1 from "react";
6
- import { createContext, useContext, useEffect, useRef, useState, useSyncExternalStore } from "react";
7
+ import { useContext, useEffect, useRef, useState, useSyncExternalStore } from "react";
7
8
  import { SDK_CHANNEL_NAME, SDK_NODE_NAME } from "@sanity/message-protocol";
8
9
  import { getNodeState } from "@sanity/sdk/comlink";
9
10
  import { defer, filter, firstValueFrom, of } from "rxjs";
10
11
  import { jsx } from "react/jsx-runtime";
11
12
  import "@sanity/sdk/dashboard";
12
13
  import { catchError } from "rxjs/operators";
13
- const SanityInstanceContext = createContext(null), useSanityInstance = () => {
14
- let instance = useContext(SanityInstanceContext);
15
- if (!instance) throw Error("SanityInstance context not found. Please ensure that your component is wrapped in a ResourceProvider or a SanityApp component.");
16
- return instance;
17
- };
18
14
  function createStateSourceHook(options) {
19
15
  let getState = typeof options == "function" ? options : options.getState, suspense = "shouldSuspend" in options && "suspender" in options ? options : void 0;
20
16
  function useHook(...t0) {
@@ -465,6 +461,6 @@ function useComlinkStudioWorkspaces() {
465
461
  error
466
462
  }, $[5] = error, $[6] = workspacesByProjectIdAndDataset, $[7] = t4) : t4 = $[7], t4;
467
463
  }
468
- export { DashboardTokenRefreshProvider as a, useAuthState as c, SanityInstanceContext as d, useOrganizationId as i, createStateSourceHook as l, useTopic as n, getDashboardRootContext as o, useEmit as r, useWindowConnection as s, useStudioWorkspacesByProjectIdDataset as t, useSanityInstance as u };
464
+ export { DashboardTokenRefreshProvider as a, useAuthState as c, useOrganizationId as i, createStateSourceHook as l, useTopic as n, getDashboardRootContext as o, useEmit as r, useWindowConnection as s, useStudioWorkspacesByProjectIdDataset as t };
469
465
 
470
- //# sourceMappingURL=useStudioWorkspacesByProjectIdDataset-FRofq7KL.js.map
466
+ //# sourceMappingURL=useStudioWorkspacesByProjectIdDataset-Cx-Q44d0.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useStudioWorkspacesByProjectIdDataset-Cx-Q44d0.js","names":["SanityConfig","SanityInstance","StateSource","useSyncExternalStore","useSanityInstance","StateSourceFactory","instance","params","TParams","TState","CreateStateSourceHookOptions","getState","shouldSuspend","suspender","Promise","getConfig","createStateSourceHook","options","suspense","undefined","useHook","t0","$","_c","t1","state","subscribe","getCurrent","_c","AuthState","getAuthState","createStateSourceHook","useAuthState","MessageData","NodeInput","SanityInstance","StateSource","FrameMessage","getNodeState","NodeState","WindowMessage","useCallback","useEffect","useRef","filter","firstValueFrom","useSanityInstance","createStateSourceHook","WindowMessageHandler","event","TFrameMessage","UseWindowConnectionOptions","name","connectTo","onMessage","Record","TMessage","WindowConnection","sendMessage","type","TType","data","Extract","fetch","options","signal","AbortSignal","suppressWarnings","responseTimeout","Promise","TResponse","useNodeState","getState","instance","nodeInput","shouldSuspend","getCurrent","undefined","suspender","observable","pipe","Boolean","useWindowConnection","t0","$","_c","t1","node","t2","Symbol","for","messageUnsubscribers","t3","Object","entries","forEach","t4","handler","messageUnsubscribe","on","current","push","_temp","t5","type_0","post","t6","type_1","data_0","fetchOptions","t7","unsubscribe","_c","_temp","React","MODULE_SLOT_KEY","Symbol","for","ROOT_SLOT_KEY","DashboardRoot","signal","AbortSignal","ContextSlot","WeakMap","createContext","Context","T","getSlotContext","slotKey","globals","globalThis","Record","slot","key","context","get","undefined","set","getDashboardModuleContext","getDashboardRootContext","React","ClientError","AuthStateType","setAuthToken","getDashboardMessageBus","MessageBus","React","PropsWithChildren","useContext","useEffect","useRef","useState","defer","of","catchError","getDashboardModuleContext","useAuthState","useSanityInstance","DashboardTokenRefresh","t0","$","_c","children","messageBus","instance","authState","processed401ErrorRef","t1","t2","subscription","subscribe","pipe","_temp","token","unsubscribe","t3","error","type","has401Error","ERROR","statusCode","current","emit","undefined","catch","_temp2","t4","console","warn","DashboardTokenRefreshProvider","FC","Symbol","for","moduleId","_c","getDashboardOrganizationId","OrganizationBase","getTopicState","isMessageBusEnvironment","useMemo","useSyncExternalStore","useSanityInstance","CurrentOrganization","Pick","useOrganizationId","$","_c","instance","t0","bb0","t1","source","t2","getCurrent","id","undefined","t3","subscribe","_c","requireDashboardMessageBus","EventTopic","MessageBusEmitOptions","MessageBusEmitResult","PayloadOf","ReplyOf","useCallback","useSanityInstance","TopicEmitter","args","K","payload","options","useEmit","topic","$","_c","instance","t0","t1","messageBus","t2","t3","emit","_c","getTopicState","resolveTopic","StateTopic","TopicData","createStateSourceHook","useTopic","getState","shouldSuspend","instance","topic","getCurrent","undefined","suspender","K","SDK_CHANNEL_NAME","SDK_NODE_NAME","getApplicationOrigin","isMessageBusEnvironment","TopicData","useEffect","useMemo","useState","useWindowConnection","useTopic","DashboardResource","id","name","title","basePath","projectId","dataset","type","userApplicationId","userApplicationSlug","url","WorkspacesByProjectIdDataset","key","StudioWorkspacesResult","workspacesByProjectIdAndDataset","error","DashboardApplications","NonNullable","useStudioWorkspacesByProjectIdDataset","useBusStudioWorkspaces","useComlinkStudioWorkspaces","toResources","application","activeDeployment","workspaces","map","workspace","slug","toWorkspaceMap","applications","workspaceMap","resource","flatMap","const","push","$","_c","t0","t1","t2","Symbol","for","setWorkspacesByProjectIdAndDataset","setError","connectTo","fetch","t3","fetchWorkspaces","signal","data","undefined","noProjectIdAndDataset","context","availableResources","forEach","length","t4","err","Error","controller","AbortController","abort","_c"],"sources":["../src/hooks/helpers/createStateSourceHook.tsx","../src/hooks/auth/useAuthState.tsx","../src/hooks/comlink/useWindowConnection.ts","../src/dashboard/module.ts","../src/context/DashboardTokenRefresh.tsx","../src/hooks/dashboard/useOrganizationId.tsx","../src/hooks/dashboard/useEmit.ts","../src/hooks/dashboard/useTopic.ts","../src/hooks/dashboard/useStudioWorkspacesByProjectIdDataset.ts"],"sourcesContent":["import {type SanityConfig, type SanityInstance, type StateSource} from '@sanity/sdk'\nimport {useSyncExternalStore} from 'react'\n\nimport {useSanityInstance} from '../context/useSanityInstance'\n\ntype StateSourceFactory<TParams extends unknown[], TState> = (\n instance: SanityInstance,\n ...params: TParams\n) => StateSource<TState>\n\ninterface CreateStateSourceHookOptions<TParams extends unknown[], TState> {\n getState: StateSourceFactory<TParams, TState>\n shouldSuspend?: (instance: SanityInstance, ...params: TParams) => boolean\n suspender?: (instance: SanityInstance, ...params: TParams) => Promise<unknown>\n getConfig?: (...params: TParams) => SanityConfig | undefined\n}\n\nexport function createStateSourceHook<TParams extends unknown[], TState>(\n options: StateSourceFactory<TParams, TState> | CreateStateSourceHookOptions<TParams, TState>,\n): (...params: TParams) => TState {\n const getState = typeof options === 'function' ? options : options.getState\n const suspense = 'shouldSuspend' in options && 'suspender' in options ? options : undefined\n\n function useHook(...params: TParams) {\n const instance = useSanityInstance()\n\n if (suspense?.suspender && suspense?.shouldSuspend?.(instance, ...params)) {\n throw suspense.suspender(instance, ...params)\n }\n\n const state = getState(instance, ...params)\n return useSyncExternalStore(state.subscribe, state.getCurrent)\n }\n\n return useHook\n}\n","import {type AuthState, getAuthState} from '@sanity/sdk'\n\nimport {createStateSourceHook} from '../helpers/createStateSourceHook'\n\n/**\n * @internal\n * A React hook that subscribes to authentication state changes.\n *\n * This hook provides access to the current authentication state type from the Sanity auth store.\n * It automatically re-renders when the authentication state changes.\n *\n * @remarks\n * The hook uses `useSyncExternalStore` to safely subscribe to auth state changes\n * and ensure consistency between server and client rendering.\n *\n * @returns The current authentication state type\n *\n * @example\n * ```tsx\n * function AuthStatus() {\n * const authState = useAuthState()\n * return <div>Current auth state: {authState}</div>\n * }\n * ```\n */\nexport const useAuthState: () => AuthState = createStateSourceHook(getAuthState)\n","import {type MessageData, type NodeInput} from '@sanity/comlink'\nimport {type SanityInstance, type StateSource} from '@sanity/sdk'\nimport {\n type FrameMessage,\n getNodeState,\n type NodeState,\n type WindowMessage,\n} from '@sanity/sdk/comlink'\nimport {useCallback, useEffect, useRef} from 'react'\nimport {filter, firstValueFrom} from 'rxjs'\n\nimport {useSanityInstance} from '../context/useSanityInstance'\nimport {createStateSourceHook} from '../helpers/createStateSourceHook'\n\n/**\n * @internal\n */\nexport type WindowMessageHandler<TFrameMessage extends FrameMessage> = (\n event: TFrameMessage['data'],\n) => TFrameMessage['response']\n\n/**\n * @internal\n */\nexport interface UseWindowConnectionOptions<TMessage extends FrameMessage> {\n name: string\n connectTo: string\n onMessage?: Record<TMessage['type'], WindowMessageHandler<TMessage>>\n}\n\n/**\n * @internal\n */\nexport interface WindowConnection<TMessage extends WindowMessage> {\n sendMessage: <TType extends TMessage['type']>(\n type: TType,\n data?: Extract<TMessage, {type: TType}>['data'],\n ) => void\n fetch: <TResponse>(\n type: string,\n data?: MessageData,\n options?: {\n signal?: AbortSignal\n suppressWarnings?: boolean\n responseTimeout?: number\n },\n ) => Promise<TResponse>\n}\n\nconst useNodeState = createStateSourceHook({\n getState: getNodeState as (\n instance: SanityInstance,\n nodeInput: NodeInput,\n ) => StateSource<NodeState>,\n shouldSuspend: (instance: SanityInstance, nodeInput: NodeInput) =>\n getNodeState(instance, nodeInput).getCurrent() === undefined,\n suspender: (instance: SanityInstance, nodeInput: NodeInput) => {\n return firstValueFrom(getNodeState(instance, nodeInput).observable.pipe(filter(Boolean)))\n },\n})\n\n/**\n * @internal\n * Hook to wrap a Comlink node in a React hook.\n * Our store functionality takes care of the lifecycle of the node,\n * as well as sharing a single node between invocations if they share the same name.\n *\n * Generally not to be used directly, but to be used as a dependency of\n * Comlink-powered hooks like `useStudioWorkspacesByProjectIdDataset`.\n */\nexport function useWindowConnection<\n TWindowMessage extends WindowMessage,\n TFrameMessage extends FrameMessage,\n>({\n name,\n connectTo,\n onMessage,\n}: UseWindowConnectionOptions<TFrameMessage>): WindowConnection<TWindowMessage> {\n const {node} = useNodeState({name, connectTo})\n const messageUnsubscribers = useRef<(() => void)[]>([])\n const instance = useSanityInstance()\n\n useEffect(() => {\n if (onMessage) {\n Object.entries(onMessage).forEach(([type, handler]) => {\n const messageUnsubscribe = node.on(type, handler as WindowMessageHandler<TFrameMessage>)\n if (messageUnsubscribe) {\n messageUnsubscribers.current.push(messageUnsubscribe)\n }\n })\n }\n\n return () => {\n messageUnsubscribers.current.forEach((unsubscribe) => unsubscribe())\n messageUnsubscribers.current = []\n }\n }, [instance, name, onMessage, node])\n\n const sendMessage = useCallback(\n (type: TWindowMessage['type'], data?: Extract<TWindowMessage, {type: typeof type}>['data']) => {\n node.post(type, data)\n },\n [node],\n )\n\n const fetch = useCallback(\n <TResponse>(\n type: string,\n data?: MessageData,\n fetchOptions?: {\n responseTimeout?: number\n signal?: AbortSignal\n suppressWarnings?: boolean\n },\n ): Promise<TResponse> => {\n return node.fetch(type, data, fetchOptions ?? {}) as Promise<TResponse>\n },\n [node],\n )\n return {\n sendMessage,\n fetch,\n }\n}\n","import * as React from 'react'\n\nconst MODULE_SLOT_KEY = Symbol.for('sanity.os.module')\nconst ROOT_SLOT_KEY = Symbol.for('sanity.os.root')\n\n/** The root a CLI-generated wrapper rendered the app into. */\ninterface DashboardRoot {\n /** Aborts when the host disposes the root, never when it hides it. */\n signal: AbortSignal\n}\n\ntype ContextSlot<T> = WeakMap<typeof React.createContext, React.Context<T | undefined>>\n\nfunction getSlotContext<T>(slotKey: symbol): React.Context<T | undefined> {\n const globals = globalThis as Record<symbol, ContextSlot<T> | undefined>\n const slot = (globals[slotKey] ??= new WeakMap())\n const key = React.createContext\n let context = slot.get(key)\n if (!context) {\n context = React.createContext<T | undefined>(undefined)\n slot.set(key, context)\n }\n return context\n}\n\n/**\n * Returns the React context that carries the current federation module id.\n *\n * @remarks\n * This is the slot the CLI-generated wrapper populates with\n * `renderOptions.moduleId`. The context lives in a per-React-copy slot on\n * `globalThis` so the CLI wrapper and the SDK share the same context even\n * across module copies. Each React copy gets its own context, since a context\n * created by one copy is inert in another.\n *\n * The slot is keyed on `React.createContext` rather than the `React` namespace\n * object: `import * as React` and `import React from 'react'` can yield\n * different wrapper objects for the same React copy under bundler interop,\n * whereas the `createContext` function is the same reference under both.\n *\n * The provider side lives in the CLI, which must not import the SDK:\n * `packages/@sanity/workbench-cli/src/actions/build/render-remote.ts` in\n * `sanity-io/cli` reconstructs this accessor (same symbol, same key) and\n * wraps `App` in the context's `Provider`. Nothing in this repo exports it.\n * @internal\n */\nexport function getDashboardModuleContext(): React.Context<string | undefined> {\n return getSlotContext<string>(MODULE_SLOT_KEY)\n}\n\n/**\n * Returns the React context that carries the {@link DashboardRoot} the app renders in.\n *\n * @remarks\n * Shared with the CLI-generated wrapper the same way as\n * {@link getDashboardModuleContext}, under `Symbol.for('sanity.os.root')`. It is a\n * separate context because older SDKs read the module context as a plain string.\n * @internal\n */\nexport function getDashboardRootContext(): React.Context<DashboardRoot | undefined> {\n return getSlotContext<DashboardRoot>(ROOT_SLOT_KEY)\n}\n","import {type ClientError} from '@sanity/client'\nimport {AuthStateType, setAuthToken} from '@sanity/sdk'\nimport {getDashboardMessageBus} from '@sanity/sdk/_internal'\nimport {type MessageBus} from '@sanity/sdk/dashboard'\nimport React, {type PropsWithChildren, useContext, useEffect, useRef, useState} from 'react'\nimport {defer, of} from 'rxjs'\nimport {catchError} from 'rxjs/operators'\n\nimport {getDashboardModuleContext} from '../dashboard/module'\nimport {useAuthState} from '../hooks/auth/useAuthState'\nimport {useSanityInstance} from '../hooks/context/useSanityInstance'\n\n/**\n * Keeps the SDK auth token in sync with the dashboard \"OS\".\n *\n * When running inside the dashboard the OS owns the session, so we subscribe\n * to its `auth.token` stream and mirror each value into\n * the auth store — a token logs us in, `null` logs us out, and later OS\n * sign-in/out propagates automatically. When a request is rejected with a 401\n * (the token expired), we ask the OS to reissue rather than tearing the session\n * down; the new token arrives back through the same subscription.\n */\nfunction DashboardTokenRefresh({\n children,\n messageBus,\n}: PropsWithChildren<{messageBus: MessageBus}>) {\n const instance = useSanityInstance()\n const authState = useAuthState()\n const processed401ErrorRef = useRef<unknown | null>(null)\n\n useEffect(() => {\n const subscription = defer(() => messageBus.subscribe('auth.token'))\n .pipe(catchError(() => of(null)))\n .subscribe((token) => setAuthToken(instance, token))\n return () => subscription.unsubscribe()\n }, [instance, messageBus])\n\n useEffect(() => {\n const has401Error =\n authState.type === AuthStateType.ERROR && (authState.error as ClientError)?.statusCode === 401\n\n if (has401Error && processed401ErrorRef.current !== authState.error) {\n processed401ErrorRef.current = authState.error\n // Event topics have no replay, so a missing responder or timeout is otherwise dropped silently.\n messageBus.emit('auth.token.refresh', undefined).catch((error) => {\n // eslint-disable-next-line no-console\n console.warn('[sanity/sdk] Dashboard token refresh failed:', error)\n })\n } else if (!has401Error) {\n processed401ErrorRef.current = null\n }\n }, [authState, messageBus])\n\n return children\n}\n\n/**\n * Authenticates the SDK with the Sanity Dashboard's session when the app runs\n * inside the dashboard.\n *\n * The dashboard owns the session there: this provider subscribes to the token\n * the dashboard issues, writes each new value into the SDK's auth store (where\n * SDK hooks read it from), and asks the dashboard for a fresh token when a\n * request fails with a 401. Outside the dashboard it renders children\n * unchanged and the app's normal auth flow applies.\n *\n * @remarks\n * `AuthBoundary` mounts this automatically, so most apps never need it\n * directly. Mount it yourself only when your app runs inside the dashboard\n * without `AuthBoundary` — that is, the app renders its own loading and error\n * UI instead of the SDK's login flow — but still uses SDK hooks such as\n * `useQuery`, which need the dashboard's token in the auth store to\n * authenticate their requests.\n *\n * Mount it once, inside the provider that creates the Sanity instance whose\n * store should receive the token.\n *\n * @example\n * ```tsx\n * import {ResourceProvider} from '@sanity/sdk-react'\n * import {TokenRefreshProvider} from '@sanity/sdk-react/dashboard'\n *\n * function EmbeddedApp() {\n * return (\n * <ResourceProvider fallback={<Loading />}>\n * <TokenRefreshProvider>\n * <App />\n * </TokenRefreshProvider>\n * </ResourceProvider>\n * )\n * }\n * ```\n *\n * @public\n * @category Dashboard\n */\nexport const DashboardTokenRefreshProvider: React.FC<PropsWithChildren> = ({children}) => {\n const instance = useSanityInstance()\n const moduleId = useContext(getDashboardModuleContext())\n // The connection is first-caller-wins per instance, and hooks below read it during their\n // render, before any effect here could run. Connecting in the first render pins the module\n // identity before they do; nothing has subscribed to the store yet, so the write is safe.\n // The module id is read once: the CLI wrapper provides it statically above this tree.\n // No retry: the host installs the bus at module evaluation, before any remote renders, and\n // a standalone app has no host to wait for.\n const [messageBus] = useState(() => getDashboardMessageBus(instance, moduleId))\n if (messageBus) {\n return <DashboardTokenRefresh messageBus={messageBus}>{children}</DashboardTokenRefresh>\n }\n\n return children\n}\n","import {getDashboardOrganizationId, type OrganizationBase} from '@sanity/sdk'\nimport {getTopicState, isMessageBusEnvironment} from '@sanity/sdk/_internal'\nimport {useMemo, useSyncExternalStore} from 'react'\n\nimport {useSanityInstance} from '../context/useSanityInstance'\n\ntype CurrentOrganization = Pick<OrganizationBase, 'id' | 'name' | 'slug'> | null | undefined\n\n/**\n * @public\n *\n * A React hook that retrieves the dashboard organization ID that is currently selected in the Sanity Dashboard.\n *\n * Works in both Dashboard runtimes: it reads the `organizations.current` message bus topic when a\n * host has installed the bus, and falls back to the Comlink connection otherwise.\n *\n * @example\n * ```tsx\n * function DashboardComponent() {\n * const orgId = useOrganizationId()\n *\n * if (!orgId) return null\n *\n * return <div>Organization ID: {String(orgId)}</div>\n * }\n * ```\n *\n * @category Dashboard\n * @returns The dashboard organization ID (string | undefined)\n */\nexport function useOrganizationId(): string | undefined {\n const instance = useSanityInstance()\n const {subscribe, getCurrent} = useMemo(() => {\n if (!isMessageBusEnvironment()) return getDashboardOrganizationId(instance)\n const source = getTopicState(instance, 'organizations.current')\n return {\n subscribe: source.subscribe,\n getCurrent: () => (source.getCurrent() as CurrentOrganization)?.id ?? undefined,\n }\n }, [instance])\n\n return useSyncExternalStore(subscribe, getCurrent)\n}\n","import {requireDashboardMessageBus} from '@sanity/sdk/_internal'\nimport {\n type EventTopic,\n type MessageBusEmitOptions,\n type MessageBusEmitResult,\n type PayloadOf,\n type ReplyOf,\n} from '@sanity/sdk/dashboard'\nimport {useCallback} from 'react'\n\nimport {useSanityInstance} from '../context/useSanityInstance'\n\n/**\n * Emits a dashboard event topic, typed to the topic's payload and reply.\n * @public\n * @category Types\n * @category Dashboard\n */\nexport type TopicEmitter<K extends EventTopic> = (\n ...args: PayloadOf<K> extends void\n ? [payload?: void, options?: MessageBusEmitOptions]\n : [payload: PayloadOf<K>, options?: MessageBusEmitOptions]\n) => MessageBusEmitResult<ReplyOf<K>>\n\n/**\n * Returns a stable function that emits a dashboard event topic.\n *\n * The event is sent immediately. Ignore the lazy result for fire-and-forget delivery, await it\n * inside `useTransition` to track a reply, or read it with React `use` to suspend. A reply\n * that fails (`NO_RESPONDER`, `TIMEOUT`, `ABORTED`) rejects the result; when read with `use`\n * that reaches the nearest error boundary, so pair the `Suspense` with one.\n *\n * @example Fire and forget\n * ```tsx\n * function ExpandPanel() {\n * const setPanelMode = useEmit('panels.mode.set')\n * return (\n * <button onClick={() => setPanelMode({name: 'favorites', mode: 'full'})}>\n * Expand\n * </button>\n * )\n * }\n * ```\n *\n * @example Await a reply with Suspense\n * ```tsx\n * function Session({request}: {request: MessageBusEmitResult<string>}) {\n * use(request)\n * return <p>Ready</p>\n * }\n *\n * function SessionAccordion() {\n * const refreshToken = useEmit('auth.token.refresh')\n * const [request, setRequest] = useState<MessageBusEmitResult<string> | null>(null)\n *\n * return (\n * <details\n * onToggle={(event) => setRequest(event.currentTarget.open ? refreshToken() : null)}\n * >\n * <summary>Session</summary>\n * <ErrorBoundary fallback={<p>Could not refresh</p>}>\n * <Suspense fallback={<p>Refreshing...</p>}>\n * {request && <Session request={request} />}\n * </Suspense>\n * </ErrorBoundary>\n * </details>\n * )\n * }\n * ```\n *\n * @public\n * @category Dashboard\n */\nexport function useEmit<K extends EventTopic>(topic: K): TopicEmitter<K> {\n const instance = useSanityInstance()\n const messageBus = requireDashboardMessageBus(instance, `emit topic \"${topic}\"`)\n return useCallback<TopicEmitter<K>>(\n (...args) => messageBus.emit(topic, ...args),\n [messageBus, topic],\n )\n}\n","import {getTopicState, resolveTopic} from '@sanity/sdk/_internal'\nimport {type StateTopic, type TopicData} from '@sanity/sdk/dashboard'\n\nimport {createStateSourceHook} from '../helpers/createStateSourceHook'\n\n/**\n * Returns the current value of a dashboard state topic and follows later updates.\n *\n * The hook suspends until the topic publishes its first value, using the message bus query\n * deadline. A topic declared with `TopicResult` resolves to its successful value; a\n * failed result throws a `TopicError` to the nearest error boundary.\n *\n * @example\n * ```tsx\n * function ForegroundApplication() {\n * const foregroundId = useTopic('applications.foreground')\n * return <span>{foregroundId ?? 'No application in the foreground'}</span>\n * }\n * ```\n *\n * @public\n * @category Dashboard\n */\nexport const useTopic = createStateSourceHook({\n getState: getTopicState,\n // `getCurrent` throws a recorded read failure, which surfaces it from render like a thrown value.\n shouldSuspend: (instance, topic: StateTopic) =>\n getTopicState(instance, topic).getCurrent() === undefined,\n suspender: resolveTopic,\n}) as <K extends StateTopic>(topic: K) => TopicData<K>\n","/* eslint-disable react-compiler/react-compiler -- the transport branch in `useStudioWorkspacesByProjectIdDataset` is a deliberate rules-of-hooks exception; the compiler refuses files that disable it */\nimport {SDK_CHANNEL_NAME, SDK_NODE_NAME} from '@sanity/message-protocol'\nimport {getApplicationOrigin, isMessageBusEnvironment} from '@sanity/sdk/_internal'\nimport {type TopicData} from '@sanity/sdk/dashboard'\nimport {useEffect, useMemo, useState} from 'react'\n\nimport {useWindowConnection} from '../comlink/useWindowConnection'\nimport {useTopic} from './useTopic'\n\nexport interface DashboardResource {\n id: string\n name: string\n title: string\n basePath: string\n projectId: string\n dataset: string\n type: string\n userApplicationId: string\n userApplicationSlug?: string | null\n url: string\n}\n\ninterface WorkspacesByProjectIdDataset {\n [key: `${string}:${string}`]: DashboardResource[] // key format: `${projectId}:${dataset}`\n}\n\ninterface StudioWorkspacesResult {\n workspacesByProjectIdAndDataset: WorkspacesByProjectIdDataset\n error: string | null\n}\n\ntype DashboardApplications = NonNullable<TopicData<'applications.list'>>\n\n/**\n * Hook that fetches studio workspaces and organizes them by projectId:dataset\n *\n * Works in both Dashboard runtimes: it derives workspaces from the `applications.list` message\n * bus topic when a host has installed the bus, and falls back to the Comlink connection otherwise.\n * @internal\n *\n * @example\n * ```tsx\n * import {useStudioWorkspacesByProjectIdDataset} from '@sanity/sdk-react'\n * import {Card, Code, Button} from '@sanity/ui'\n * import {Suspense} from 'react'\n *\n * function WorkspacesCard() {\n * const {workspacesByProjectIdAndDataset, error} = useStudioWorkspacesByProjectIdDataset()\n * if (error) {\n * return <div>Error: {error}</div>\n * }\n * return (\n * <Card padding={4} radius={2} shadow={1}>\n * <Code language=\"json\">\n * {JSON.stringify(workspacesByProjectIdAndDataset, null, 2)}\n * </Code>\n * </Card>\n * )\n * }\n *\n * // Wrap the component with Suspense since the hook may suspend\n * function DashboardWorkspaces() {\n * return (\n * <Suspense fallback={<Button text=\"Loading...\" disabled />}>\n * <WorkspacesCard />\n * </Suspense>\n * )\n * }\n * ```\n */\nexport function useStudioWorkspacesByProjectIdDataset(): StudioWorkspacesResult {\n // The branch is stable: the transport is fixed for the page lifetime, so one set of hooks\n // always runs and the other never does.\n // eslint-disable-next-line react-hooks/rules-of-hooks -- transport is fixed for the page lifetime\n if (isMessageBusEnvironment()) return useBusStudioWorkspaces()\n // eslint-disable-next-line react-hooks/rules-of-hooks -- transport is fixed for the page lifetime\n return useComlinkStudioWorkspaces()\n}\n\n// The legacy Comlink protocol models studios at the workspace level, so each workspace of a\n// studio's active deployment becomes one resource, addressed by the studio's origin.\nfunction toResources(application: DashboardApplications[number]): DashboardResource[] {\n // Installations join the topic union without a `type`; only studios yield workspace resources.\n if (!('type' in application) || application.type !== 'studio') return []\n const url = getApplicationOrigin(application) ?? ''\n return (application.activeDeployment?.workspaces ?? []).map((workspace) => ({\n id: workspace.id,\n name: workspace.name,\n title: workspace.title ?? application.title,\n basePath: workspace.basePath ?? '',\n projectId: workspace.projectId,\n dataset: workspace.dataset,\n type: 'studio',\n userApplicationId: application.id,\n userApplicationSlug: application.slug,\n url,\n }))\n}\n\nfunction toWorkspaceMap(applications: DashboardApplications): WorkspacesByProjectIdDataset {\n const workspaceMap: WorkspacesByProjectIdDataset = {}\n for (const resource of applications.flatMap(toResources)) {\n const key = `${resource.projectId}:${resource.dataset}` as const\n workspaceMap[key] ??= []\n workspaceMap[key].push(resource)\n }\n return workspaceMap\n}\n\n// Suspends until the host publishes its application list and throws a `TopicError` on failure,\n// like every bus hook, so `error` is always `null` on this path.\nfunction useBusStudioWorkspaces(): StudioWorkspacesResult {\n const applications = useTopic('applications.list')\n const workspacesByProjectIdAndDataset = useMemo(\n () => toWorkspaceMap(applications ?? []),\n [applications],\n )\n return {workspacesByProjectIdAndDataset, error: null}\n}\n\nfunction useComlinkStudioWorkspaces(): StudioWorkspacesResult {\n const [workspacesByProjectIdAndDataset, setWorkspacesByProjectIdAndDataset] =\n useState<WorkspacesByProjectIdDataset>({})\n const [error, setError] = useState<string | null>(null)\n\n const {fetch} = useWindowConnection({\n name: SDK_NODE_NAME,\n connectTo: SDK_CHANNEL_NAME,\n })\n\n // Once computed, this should probably be in a store and poll for changes\n // However, our stores are currently being refactored\n useEffect(() => {\n if (!fetch) return\n\n async function fetchWorkspaces(signal: AbortSignal) {\n try {\n const data = await fetch<{\n context: {availableResources: Array<DashboardResource>}\n }>('dashboard/v1/context', undefined, {signal})\n\n const workspaceMap: WorkspacesByProjectIdDataset = {}\n const noProjectIdAndDataset: DashboardResource[] = []\n\n data.context.availableResources.forEach((resource) => {\n if (resource.type !== 'studio') return\n if (!resource.projectId || !resource.dataset) {\n noProjectIdAndDataset.push(resource)\n return\n }\n const key = `${resource.projectId}:${resource.dataset}` as const\n if (!workspaceMap[key]) {\n workspaceMap[key] = []\n }\n workspaceMap[key].push(resource)\n })\n\n if (noProjectIdAndDataset.length > 0) {\n workspaceMap['NO_PROJECT_ID:NO_DATASET'] = noProjectIdAndDataset\n }\n\n setWorkspacesByProjectIdAndDataset(workspaceMap)\n setError(null)\n } catch (err: unknown) {\n if (err instanceof Error) {\n if (err.name === 'AbortError') {\n return\n }\n setError('Failed to fetch workspaces')\n }\n }\n }\n\n const controller = new AbortController()\n fetchWorkspaces(controller.signal)\n\n return () => {\n controller.abort()\n }\n }, [fetch])\n\n return {\n workspacesByProjectIdAndDataset,\n error,\n }\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sanity/sdk-react",
3
- "version": "3.11.0-next.20261029105031",
3
+ "version": "3.11.0",
4
4
  "private": false,
5
5
  "description": "Sanity SDK React toolkit for Content OS",
6
6
  "keywords": [
@@ -35,6 +35,7 @@
35
35
  "exports": {
36
36
  ".": "./dist/index.js",
37
37
  "./package.json": "./package.json",
38
+ "./collaboration": "./dist/_exports/collaboration.js",
38
39
  "./dashboard": "./dist/_exports/dashboard.js",
39
40
  "./dashboard/_internal": "./dist/_exports/dashboard-internal.js"
40
41
  },
@@ -45,7 +46,7 @@
45
46
  "@module-federation/runtime": "^2.9.2",
46
47
  "@sanity/client": "^8.13.0",
47
48
  "@sanity/message-protocol": "^0.24.0",
48
- "@sanity/sdk": "3.11.0-next.20261029105031",
49
+ "@sanity/sdk": "3.11.0",
49
50
  "@sanity/types": "^6.21.0",
50
51
  "react-compiler-runtime": "^1.0.0",
51
52
  "react-error-boundary": "^6.1.6",
@@ -0,0 +1,13 @@
1
+ // Comment hooks backed by the organization collaboration API, in beta.
2
+ //
3
+ // The subpath exists so these can ship beside the deprecated add-on dataset
4
+ // comment hooks on the package root, which carry the same names. In the next
5
+ // major those go away and these become the root exports;
6
+ // `@sanity/sdk-react/collaboration` keeps re-exporting them in that major,
7
+ // marked `@deprecated`, and is removed in the major after.
8
+
9
+ export {type CommentActions, useCommentActions} from '../hooks/comments/useCommentActions'
10
+ export {useComments, type UseCommentsResult} from '../hooks/comments/useComments'
11
+ export {useCommentsQuery, type UseCommentsQueryResult} from '../hooks/comments/useCommentsQuery'
12
+ export {useCommentThreads, type UseCommentThreadsResult} from '../hooks/comments/useCommentThreads'
13
+ export * from '@sanity/sdk/collaboration'
@@ -48,9 +48,12 @@ export {
48
48
  type WindowConnection,
49
49
  type WindowMessageHandler,
50
50
  } from '../hooks/comlink/useWindowConnection'
51
- export {type CommentActions, useCommentActions} from '../hooks/comments/useCommentActions'
52
- export {useComments, type UseCommentsResult} from '../hooks/comments/useComments'
53
- export {useCommentThreads, type UseCommentThreadsResult} from '../hooks/comments/useCommentThreads'
51
+ export {type CommentActions, useCommentActions} from '../hooks/comments/legacy/useCommentActions'
52
+ export {useComments, type UseCommentsResult} from '../hooks/comments/legacy/useComments'
53
+ export {
54
+ useCommentThreads,
55
+ type UseCommentThreadsResult,
56
+ } from '../hooks/comments/legacy/useCommentThreads'
54
57
  export {useResource} from '../hooks/context/useResource'
55
58
  export {useSanityInstance} from '../hooks/context/useSanityInstance'
56
59
  export {useFavorite} from '../hooks/dashboard/useFavorite'
@@ -0,0 +1,20 @@
1
+ import {type StateSource} from '@sanity/sdk'
2
+ import {of} from 'rxjs'
3
+
4
+ const NEVER_ERRORS: StateSource<unknown> = {
5
+ subscribe: () => () => {},
6
+ getCurrent: () => undefined,
7
+ observable: of(undefined),
8
+ }
9
+
10
+ /**
11
+ * The error source {@link useCommentList} asks every read hook for.
12
+ *
13
+ * The add-on dataset store has no error state: a listener that fails throws at
14
+ * whoever is reading it rather than parking the error beside the list. So the
15
+ * deprecated hooks report no error, ever, and keep the behaviour they shipped
16
+ * with.
17
+ */
18
+ export function getNoCommentsErrorState(): StateSource<unknown> {
19
+ return NEVER_ERRORS
20
+ }
@@ -0,0 +1,150 @@
1
+ import {
2
+ createComment,
3
+ removeComment,
4
+ replyToComment,
5
+ setCommentStatus,
6
+ updateComment,
7
+ } from '@sanity/sdk'
8
+ import {renderHook} from '@testing-library/react'
9
+ import {beforeEach, describe, expect, it, vi} from 'vitest'
10
+
11
+ import {ResourceProvider} from '../../../context/ResourceProvider'
12
+ import {ResourcesContext} from '../../../context/ResourcesContext'
13
+ import {useCommentActions} from './useCommentActions'
14
+
15
+ vi.mock('@sanity/sdk', async (importOriginal) => {
16
+ const original = await importOriginal<typeof import('@sanity/sdk')>()
17
+ return {
18
+ ...original,
19
+ createComment: vi.fn(),
20
+ replyToComment: vi.fn(),
21
+ updateComment: vi.fn(),
22
+ setCommentStatus: vi.fn(),
23
+ removeComment: vi.fn(),
24
+ }
25
+ })
26
+
27
+ const HANDLE = {documentId: 'doc-1', documentType: 'author'}
28
+
29
+ /** Creates name a field, since a comment with no path is refused. */
30
+ const CREATE = {...HANDLE, fieldPath: 'name'}
31
+
32
+ // Hoisted: an inline object here would be a new value on every render, and the
33
+ // callbacks are memoised against it.
34
+ const RESOURCES = {other: {projectId: 'p2', dataset: 'd2'}}
35
+
36
+ /** Hoisted for the same reason as `RESOURCES`. */
37
+ const RELEASE_PERSPECTIVE = {releaseName: 'summer'}
38
+
39
+ function Wrapper({children}: {children: React.ReactNode}) {
40
+ return (
41
+ <ResourceProvider projectId="p" dataset="d" fallback={null}>
42
+ <ResourcesContext.Provider value={RESOURCES}>{children}</ResourcesContext.Provider>
43
+ </ResourceProvider>
44
+ )
45
+ }
46
+
47
+ function PerspectiveWrapper({children}: {children: React.ReactNode}) {
48
+ return (
49
+ <ResourceProvider projectId="p" dataset="d" perspective={RELEASE_PERSPECTIVE} fallback={null}>
50
+ <ResourcesContext.Provider value={RESOURCES}>{children}</ResourcesContext.Provider>
51
+ </ResourceProvider>
52
+ )
53
+ }
54
+
55
+ function setup(wrapper: typeof Wrapper = Wrapper) {
56
+ return renderHook(() => useCommentActions(), {wrapper})
57
+ }
58
+
59
+ describe('useCommentActions', () => {
60
+ beforeEach(() => {
61
+ vi.resetAllMocks()
62
+ })
63
+
64
+ it('keeps the callbacks stable across renders', () => {
65
+ const {result, rerender} = setup()
66
+ const first = result.current
67
+
68
+ rerender()
69
+
70
+ expect(result.current).toBe(first)
71
+ })
72
+
73
+ it('forwards each action to the store', () => {
74
+ const {result} = setup()
75
+
76
+ result.current.createComment({...CREATE, message: null})
77
+ result.current.replyToComment({...HANDLE, parentCommentId: 'p1', message: null})
78
+ result.current.updateComment({commentId: 'c1', message: null})
79
+ result.current.setCommentStatus({commentId: 'c1', status: 'resolved'})
80
+ result.current.removeComment({commentId: 'c1'})
81
+
82
+ expect(createComment).toHaveBeenCalledOnce()
83
+ expect(replyToComment).toHaveBeenCalledOnce()
84
+ expect(updateComment).toHaveBeenCalledOnce()
85
+ expect(setCommentStatus).toHaveBeenCalledOnce()
86
+ expect(removeComment).toHaveBeenCalledOnce()
87
+ })
88
+
89
+ it('fills in the resource from context', () => {
90
+ const {result} = setup()
91
+
92
+ result.current.createComment({...CREATE, message: null})
93
+
94
+ expect(createComment).toHaveBeenCalledWith(
95
+ expect.anything(),
96
+ expect.objectContaining({resource: {projectId: 'p', dataset: 'd'}}),
97
+ )
98
+ })
99
+
100
+ it('lets a call choose a different named resource', () => {
101
+ // Resolution happens per call, so one set of callbacks serves several
102
+ // resources.
103
+ const {result} = setup()
104
+
105
+ result.current.removeComment({commentId: 'c1', resourceName: 'other'})
106
+
107
+ expect(removeComment).toHaveBeenCalledWith(
108
+ expect.anything(),
109
+ expect.objectContaining({resource: {projectId: 'p2', dataset: 'd2'}}),
110
+ )
111
+ })
112
+
113
+ it('rejects a call naming a resource that is not in context', () => {
114
+ const {result} = setup()
115
+
116
+ expect(() => result.current.removeComment({commentId: 'c1', resourceName: 'missing'})).toThrow(
117
+ /no resource named "missing"/,
118
+ )
119
+ })
120
+
121
+ it('fills in the perspective from context', () => {
122
+ // Core turns a release perspective into `target.documentVersionId`, so
123
+ // losing it here files the comment against the wrong release and nothing
124
+ // reports an error.
125
+ const {result} = setup(PerspectiveWrapper)
126
+
127
+ result.current.createComment({...CREATE, message: null})
128
+ result.current.replyToComment({...HANDLE, parentCommentId: 'p1', message: null})
129
+
130
+ expect(createComment).toHaveBeenCalledWith(
131
+ expect.anything(),
132
+ expect.objectContaining({perspective: RELEASE_PERSPECTIVE}),
133
+ )
134
+ expect(replyToComment).toHaveBeenCalledWith(
135
+ expect.anything(),
136
+ expect.objectContaining({perspective: RELEASE_PERSPECTIVE}),
137
+ )
138
+ })
139
+
140
+ it('lets a call override the perspective from context', () => {
141
+ const {result} = setup(PerspectiveWrapper)
142
+
143
+ result.current.createComment({...CREATE, message: null, perspective: 'published'})
144
+
145
+ expect(createComment).toHaveBeenCalledWith(
146
+ expect.anything(),
147
+ expect.objectContaining({perspective: 'published'}),
148
+ )
149
+ })
150
+ })