@sanity/sdk-react 2.10.0 → 2.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 (54) hide show
  1. package/dist/index.d.ts +257 -200
  2. package/dist/index.js +364 -253
  3. package/dist/index.js.map +1 -1
  4. package/package.json +6 -9
  5. package/src/_exports/index.ts +2 -0
  6. package/src/_exports/sdk-react.ts +4 -0
  7. package/src/components/SDKProvider.test.tsx +5 -12
  8. package/src/components/SDKProvider.tsx +26 -24
  9. package/src/config/handles.ts +55 -0
  10. package/src/constants.ts +5 -0
  11. package/src/context/DefaultResourceContext.ts +10 -0
  12. package/src/context/PerspectiveContext.ts +12 -0
  13. package/src/context/ResourceProvider.test.tsx +2 -2
  14. package/src/context/ResourceProvider.tsx +53 -49
  15. package/src/hooks/agent/agentActions.ts +55 -38
  16. package/src/hooks/context/useResource.test.tsx +32 -0
  17. package/src/hooks/context/useResource.ts +24 -0
  18. package/src/hooks/context/useSanityInstance.test.tsx +42 -111
  19. package/src/hooks/context/useSanityInstance.ts +28 -50
  20. package/src/hooks/dashboard/useDispatchIntent.test.ts +5 -1
  21. package/src/hooks/dashboard/useDispatchIntent.ts +3 -3
  22. package/src/hooks/dashboard/useManageFavorite.test.tsx +16 -12
  23. package/src/hooks/dashboard/utils/useResourceIdFromDocumentHandle.ts +1 -5
  24. package/src/hooks/document/{useApplyDocumentActions.test.ts → useApplyDocumentActions.test.tsx} +42 -77
  25. package/src/hooks/document/useApplyDocumentActions.ts +28 -62
  26. package/src/hooks/document/useDocument.ts +3 -5
  27. package/src/hooks/document/useDocumentEvent.ts +4 -3
  28. package/src/hooks/document/useDocumentPermissions.test.tsx +58 -150
  29. package/src/hooks/document/useDocumentPermissions.ts +78 -55
  30. package/src/hooks/document/useEditDocument.test.tsx +25 -60
  31. package/src/hooks/document/useEditDocument.ts +1 -1
  32. package/src/hooks/documents/useDocuments.ts +13 -8
  33. package/src/hooks/helpers/createStateSourceHook.tsx +1 -2
  34. package/src/hooks/helpers/useNormalizedResourceOptions.test.tsx +253 -0
  35. package/src/hooks/helpers/useNormalizedResourceOptions.ts +85 -47
  36. package/src/hooks/organizations/useOrganization.test-d.ts +53 -0
  37. package/src/hooks/organizations/useOrganization.test.ts +65 -0
  38. package/src/hooks/organizations/useOrganization.ts +40 -0
  39. package/src/hooks/organizations/useOrganizations.test-d.ts +55 -0
  40. package/src/hooks/organizations/useOrganizations.test.ts +85 -0
  41. package/src/hooks/organizations/useOrganizations.ts +45 -0
  42. package/src/hooks/paginatedDocuments/usePaginatedDocuments.ts +23 -9
  43. package/src/hooks/presence/usePresence.ts +4 -11
  44. package/src/hooks/preview/useDocumentPreview.tsx +4 -7
  45. package/src/hooks/projection/useDocumentProjection.ts +5 -7
  46. package/src/hooks/projects/useProject.test-d.ts +49 -0
  47. package/src/hooks/projects/useProject.ts +33 -41
  48. package/src/hooks/projects/useProjects.test-d.ts +49 -0
  49. package/src/hooks/projects/useProjects.ts +17 -23
  50. package/src/hooks/query/useQuery.ts +1 -1
  51. package/src/hooks/releases/useActiveReleases.ts +6 -6
  52. package/src/hooks/releases/usePerspective.ts +7 -12
  53. package/src/hooks/users/useUser.ts +1 -1
  54. package/src/hooks/users/useUsers.ts +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sanity/sdk-react",
3
- "version": "2.10.0",
3
+ "version": "2.11.0",
4
4
  "private": false,
5
5
  "description": "Sanity SDK React toolkit for Content OS",
6
6
  "keywords": [
@@ -50,7 +50,7 @@
50
50
  "react-compiler-runtime": "19.1.0-rc.2",
51
51
  "react-error-boundary": "^5.0.0",
52
52
  "rxjs": "^7.8.2",
53
- "@sanity/sdk": "2.10.0"
53
+ "@sanity/sdk": "2.11.0"
54
54
  },
55
55
  "devDependencies": {
56
56
  "@sanity/browserslist-config": "^1.0.5",
@@ -63,7 +63,7 @@
63
63
  "@types/react": "^19.2.7",
64
64
  "@types/react-dom": "^19.2.3",
65
65
  "@vitejs/plugin-react": "^4.7.0",
66
- "@vitest/coverage-v8": "3.2.4",
66
+ "@vitest/coverage-v8": "4.1.5",
67
67
  "babel-plugin-react-compiler": "19.1.0-rc.1",
68
68
  "eslint": "^9.22.0",
69
69
  "groq-js": "^1.22.0",
@@ -74,20 +74,17 @@
74
74
  "rollup-plugin-visualizer": "^5.14.0",
75
75
  "typescript": "^5.8.3",
76
76
  "vite": "^7.0.0",
77
- "vitest": "^3.2.4",
78
- "@repo/config-eslint": "0.0.0",
77
+ "vitest": "^4.1.4",
79
78
  "@repo/config-test": "0.0.1",
79
+ "@repo/package.bundle": "3.82.0",
80
80
  "@repo/package.config": "0.0.1",
81
81
  "@repo/tsconfig": "0.0.1",
82
- "@repo/package.bundle": "3.82.0"
82
+ "@repo/config-eslint": "0.0.0"
83
83
  },
84
84
  "peerDependencies": {
85
85
  "react": "^18.0.0 || ^19.0.0",
86
86
  "react-dom": "^18.0.0 || ^19.0.0"
87
87
  },
88
- "engines": {
89
- "node": ">=20.19"
90
- },
91
88
  "publishConfig": {
92
89
  "access": "public"
93
90
  },
@@ -1,2 +1,4 @@
1
1
  export * from './sdk-react.ts'
2
2
  export * from '@sanity/sdk'
3
+ // React-layer handles shadow core equivalents when imported from @sanity/sdk-react
4
+ export type {DocumentHandle, DocumentTypeHandle, ResourceHandle} from './sdk-react.ts'
@@ -4,6 +4,7 @@
4
4
  export {AuthBoundary, type AuthBoundaryProps} from '../components/auth/AuthBoundary'
5
5
  export {SanityApp, type SanityAppProps} from '../components/SanityApp'
6
6
  export {SDKProvider, type SDKProviderProps} from '../components/SDKProvider'
7
+ export {type DocumentHandle, type DocumentTypeHandle, type ResourceHandle} from '../config/handles'
7
8
  export {ComlinkTokenRefreshProvider} from '../context/ComlinkTokenRefresh'
8
9
  export {renderSanityApp} from '../context/renderSanityApp'
9
10
  export {ResourceProvider, type ResourceProviderProps} from '../context/ResourceProvider'
@@ -44,6 +45,7 @@ export {
44
45
  type WindowConnection,
45
46
  type WindowMessageHandler,
46
47
  } from '../hooks/comlink/useWindowConnection'
48
+ export {useResource} from '../hooks/context/useResource'
47
49
  export {useSanityInstance} from '../hooks/context/useSanityInstance'
48
50
  export {useDashboardNavigate} from '../hooks/dashboard/useDashboardNavigate'
49
51
  export {useDispatchIntent} from '../hooks/dashboard/useDispatchIntent'
@@ -67,6 +69,8 @@ export {
67
69
  type DocumentsResponse,
68
70
  useDocuments,
69
71
  } from '../hooks/documents/useDocuments'
72
+ export {useOrganization} from '../hooks/organizations/useOrganization'
73
+ export {useOrganizations} from '../hooks/organizations/useOrganizations'
70
74
  export {
71
75
  type PaginatedDocumentsOptions,
72
76
  type PaginatedDocumentsResponse,
@@ -62,7 +62,7 @@ describe('SDKProvider', () => {
62
62
  })
63
63
  })
64
64
 
65
- it('renders nested ResourceProviders with AuthBoundary for multiple configs', () => {
65
+ it('renders a single ResourceProvider using the first config when multiple configs are provided', () => {
66
66
  const configs = [
67
67
  {
68
68
  projectId: 'project-1',
@@ -80,22 +80,15 @@ describe('SDKProvider', () => {
80
80
  </SDKProvider>,
81
81
  )
82
82
 
83
- // Should create two nested ResourceProviders
83
+ // Should create a single ResourceProvider using the first config
84
84
  const providers = getAllByTestId('resource-provider')
85
- expect(providers.length).toBe(2)
85
+ expect(providers.length).toBe(1)
86
86
 
87
- // Should create an AuthBoundary inside the innermost provider
87
+ // Should create an AuthBoundary inside
88
88
  expect(getByTestId('auth-boundary')).toBeInTheDocument()
89
89
 
90
- // Verify each provider has the correct config - order is based on how SDKProvider creates nestings
91
- // The first provider contains config[1]
90
+ // Verify the provider uses the first config
92
91
  expect(JSON.parse(providers[0].getAttribute('data-config') || '{}')).toEqual({
93
- projectId: 'project-2',
94
- dataset: 'staging',
95
- })
96
-
97
- // The second provider contains config[0]
98
- expect(JSON.parse(providers[1].getAttribute('data-config') || '{}')).toEqual({
99
92
  projectId: 'project-1',
100
93
  dataset: 'production',
101
94
  })
@@ -2,6 +2,7 @@ import {type DocumentResource, isImportError, type SanityConfig} from '@sanity/s
2
2
  import {type ReactElement, type ReactNode, useEffect, useMemo} from 'react'
3
3
  import {ErrorBoundary, type FallbackProps} from 'react-error-boundary'
4
4
 
5
+ import {DEFAULT_RESOURCE_NAME} from '../constants'
5
6
  import {ResourceProvider} from '../context/ResourceProvider'
6
7
  import {ResourcesContext} from '../context/ResourcesContext'
7
8
  import {AuthBoundary, type AuthBoundaryProps} from './auth/AuthBoundary'
@@ -34,8 +35,6 @@ function ResetChunkReloadFlagOnMount(): null {
34
35
  * @internal
35
36
  *
36
37
  * Top-level context provider that provides access to the Sanity SDK.
37
- * Creates a hierarchy of ResourceProviders, each providing a SanityInstance that can be
38
- * accessed by hooks. The first configuration in the array becomes the default instance.
39
38
  */
40
39
  export function SDKProvider({
41
40
  children,
@@ -43,35 +42,38 @@ export function SDKProvider({
43
42
  fallback,
44
43
  ...props
45
44
  }: SDKProviderProps): ReactElement {
46
- // reverse because we want the first config to be the default, but the
47
- // ResourceProvider nesting makes the last one the default
48
- const configs = (Array.isArray(config) ? config : [config]).slice().reverse()
49
- const projectIds = configs.map((c) => c.projectId).filter((id): id is string => !!id)
45
+ const allConfigs = Array.isArray(config) ? config : [config]
46
+ const resolvedConfig = allConfigs[0]
47
+ const projectIds = allConfigs.map((c) => c.projectId).filter((id): id is string => !!id)
50
48
 
51
- // Memoize resources to prevent creating a new empty object on every render
52
- const resourcesValue = useMemo(() => props.resources ?? {}, [props.resources])
49
+ // Extract static fields so the memo below doesn't take a reference dependency
50
+ // on `config` inline config objects change identity on every render.
51
+ const singleConfig = Array.isArray(config) ? null : config
52
+ const defaultProjectId = singleConfig?.projectId
53
+ const defaultDataset = singleConfig?.dataset
53
54
 
54
- // Create a nested structure of ResourceProviders for each config
55
- const createNestedProviders = (index: number): ReactElement => {
56
- if (index >= configs.length) {
57
- return (
58
- <AuthBoundary {...props} projectIds={projectIds}>
59
- <ResourcesContext.Provider value={resourcesValue}>{children}</ResourcesContext.Provider>
60
- </AuthBoundary>
61
- )
55
+ // For a single config, synthesize a 'default' resource from its projectId/dataset
56
+ // so that hooks can resolve it via resourceName: 'default' or fall back to it
57
+ // automatically when no resource info is provided.
58
+ const resourcesValue = useMemo(() => {
59
+ const explicit = props.resources ?? {}
60
+ if (defaultProjectId && defaultDataset && !Object.hasOwn(explicit, DEFAULT_RESOURCE_NAME)) {
61
+ return {
62
+ [DEFAULT_RESOURCE_NAME]: {projectId: defaultProjectId, dataset: defaultDataset},
63
+ ...explicit,
64
+ }
62
65
  }
63
-
64
- return (
65
- <ResourceProvider {...configs[index]} fallback={fallback}>
66
- {createNestedProviders(index + 1)}
67
- </ResourceProvider>
68
- )
69
- }
66
+ return explicit
67
+ }, [defaultProjectId, defaultDataset, props.resources])
70
68
 
71
69
  return (
72
70
  <ErrorBoundary FallbackComponent={ChunkAwareFallback}>
73
71
  <ResetChunkReloadFlagOnMount />
74
- {createNestedProviders(0)}
72
+ <ResourceProvider {...resolvedConfig} fallback={fallback}>
73
+ <AuthBoundary {...props} projectIds={projectIds}>
74
+ <ResourcesContext.Provider value={resourcesValue}>{children}</ResourcesContext.Provider>
75
+ </AuthBoundary>
76
+ </ResourceProvider>
75
77
  </ErrorBoundary>
76
78
  )
77
79
  }
@@ -0,0 +1,55 @@
1
+ import {
2
+ type DatasetHandle,
3
+ type DocumentHandle as CoreDocumentHandle,
4
+ type DocumentTypeHandle as CoreDocumentTypeHandle,
5
+ } from '@sanity/sdk'
6
+
7
+ /**
8
+ * React SDK resource handle — extends the core DatasetHandle with `resourceName`
9
+ * for context-based resource resolution.
10
+ *
11
+ * Use this (or its subtypes) as the options type for custom hooks that need to
12
+ * accept a resource. It accepts a `resource` object, a `resourceName` registered
13
+ * via the `resources` prop on `<SanityApp>`, or a bare `projectId`/`dataset` pair
14
+ * for backward compatibility.
15
+ *
16
+ * @public
17
+ */
18
+ export interface ResourceHandle<
19
+ TDataset extends string = string,
20
+ TProjectId extends string = string,
21
+ > extends DatasetHandle<TDataset, TProjectId> {
22
+ /**
23
+ * Name of a resource registered via the `resources` prop on `<SanityApp>`.
24
+ * Resolved to a `DocumentResource` at the React layer.
25
+ */
26
+ resourceName?: string
27
+ }
28
+
29
+ /**
30
+ * React SDK document-type handle. Adds `resourceName` to the core `DocumentTypeHandle`.
31
+ * @public
32
+ */
33
+ export interface DocumentTypeHandle<
34
+ TDocumentType extends string = string,
35
+ TDataset extends string = string,
36
+ TProjectId extends string = string,
37
+ > extends CoreDocumentTypeHandle<TDocumentType, TDataset, TProjectId> {
38
+ resourceName?: string
39
+ }
40
+
41
+ /**
42
+ * React SDK document handle. Adds `resourceName` to the core `DocumentHandle`.
43
+ *
44
+ * Import from `@sanity/sdk-react` (not `@sanity/sdk`) when writing option types
45
+ * for hooks — this version understands `resourceName` resolution.
46
+ *
47
+ * @public
48
+ */
49
+ export interface DocumentHandle<
50
+ TDocumentType extends string = string,
51
+ TDataset extends string = string,
52
+ TProjectId extends string = string,
53
+ > extends CoreDocumentHandle<TDocumentType, TDataset, TProjectId> {
54
+ resourceName?: string
55
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * The name of the resource automatically registered from a single-config `<SanityApp>`.
3
+ * Hooks that receive no resource information fall back to this named resource.
4
+ */
5
+ export const DEFAULT_RESOURCE_NAME = 'default'
@@ -0,0 +1,10 @@
1
+ import {type DocumentResource} from '@sanity/sdk'
2
+ import {createContext} from 'react'
3
+
4
+ /**
5
+ * Provides the active DocumentResource for a subtree.
6
+ * Set by ResourceProvider; read by useNormalizedResourceOptions as a fallback
7
+ * when hooks receive no explicit resource or resourceName.
8
+ * @internal
9
+ */
10
+ export const ResourceContext = createContext<DocumentResource | undefined>(undefined)
@@ -0,0 +1,12 @@
1
+ import {type PerspectiveHandle} from '@sanity/sdk'
2
+ import {createContext} from 'react'
3
+
4
+ /**
5
+ * Provides the active perspective for a subtree.
6
+ * Set by ResourceProvider; injected by useNormalizedResourceOptions when
7
+ * the hook's options don't include an explicit perspective.
8
+ * @internal
9
+ */
10
+ export const PerspectiveContext = createContext<PerspectiveHandle['perspective'] | undefined>(
11
+ undefined,
12
+ )
@@ -78,7 +78,7 @@ describe('ResourceProvider', () => {
78
78
  })
79
79
  })
80
80
 
81
- it('creates child instance when parent context exists', async () => {
81
+ it('reuses instance when parent context exists', async () => {
82
82
  const parentConfig: SanityConfig = {...testConfig, dataset: 'parent-dataset'}
83
83
  const child = promiseWithResolvers<SanityInstance | null>()
84
84
 
@@ -97,7 +97,7 @@ describe('ResourceProvider', () => {
97
97
  )
98
98
 
99
99
  const childInstance = await child.promise
100
- expect(childInstance?.config).toEqual(testConfig)
100
+ expect(childInstance?.config).toEqual(parentConfig)
101
101
  expect(childInstance?.isDisposed()).toBe(false)
102
102
  })
103
103
 
@@ -1,7 +1,16 @@
1
- import {createSanityInstance, type SanityConfig, type SanityInstance} from '@sanity/sdk'
1
+ import {
2
+ createSanityInstance,
3
+ type DatasetResource,
4
+ type DocumentResource,
5
+ isDatasetResource,
6
+ type SanityConfig,
7
+ type SanityInstance,
8
+ } from '@sanity/sdk'
2
9
  import {initTelemetry} from '@sanity/sdk/_internal'
3
- import {useContext, useEffect, useMemo, useRef} from 'react'
10
+ import {useContext, useEffect, useMemo, useRef, useState} from 'react'
4
11
 
12
+ import {ResourceContext} from './DefaultResourceContext'
13
+ import {PerspectiveContext} from './PerspectiveContext'
5
14
  import {SanityInstanceContext} from './SanityInstanceContext'
6
15
  import {SanityInstanceProvider} from './SanityInstanceProvider'
7
16
 
@@ -17,73 +26,63 @@ const DEFAULT_FALLBACK = (
17
26
  */
18
27
  export interface ResourceProviderProps extends SanityConfig {
19
28
  /**
20
- * React node to show while content is loading
21
- * Used as the fallback for the internal Suspense boundary
29
+ * The document resource (project/dataset, media library, or canvas)
30
+ * for this subtree. Hooks that don't specify an explicit resource will
31
+ * use this value.
32
+ */
33
+ resource?: DocumentResource
34
+ /**
35
+ * React node to show while content is loading.
36
+ * Used as the fallback for the internal Suspense boundary.
22
37
  */
23
38
  fallback: React.ReactNode
24
39
  children: React.ReactNode
25
40
  }
26
41
 
27
42
  /**
28
- * Provides a Sanity instance to child components through React Context
43
+ * Provides Sanity configuration to child components through React Context.
29
44
  *
30
45
  * @internal
31
46
  *
32
- * @remarks
33
- * The ResourceProvider creates a hierarchical structure of Sanity instances:
34
- * - When used as a root provider, it creates a new Sanity instance with the given config
35
- * - When nested inside another ResourceProvider, it creates a child instance that
36
- * inherits and extends the parent's configuration
37
- *
38
- * Features:
39
- * - Automatically manages the lifecycle of Sanity instances
40
- * - Disposes instances when the component unmounts
41
- * - Includes a Suspense boundary for data loading
42
- * - Enables hierarchical configuration inheritance
43
- *
44
- * Use this component to:
45
- * - Set up project/dataset configuration for an application
46
- * - Override specific configuration values in a section of your app
47
- * - Create isolated instance hierarchies for different features
48
- *
49
- * @example Creating a root provider
47
+ * @example
50
48
  * ```tsx
51
49
  * <ResourceProvider
52
- * projectId="your-project-id"
53
- * dataset="production"
50
+ * resource={{ projectId: 'your-project-id', dataset: 'production' }}
54
51
  * fallback={<LoadingSpinner />}
55
52
  * >
56
53
  * <YourApp />
57
54
  * </ResourceProvider>
58
55
  * ```
59
- *
60
- * @example Creating nested providers with configuration inheritance
61
- * ```tsx
62
- * // Root provider with production config with nested provider for preview features with custom dataset
63
- * <ResourceProvider projectId="abc123" dataset="production" fallback={<Loading />}>
64
- * <div>...Main app content</div>
65
- * <Dashboard />
66
- * <ResourceProvider dataset="preview" fallback={<Loading />}>
67
- * <PreviewFeatures />
68
- * </ResourceProvider>
69
- * </ResourceProvider>
70
- * ```
71
56
  */
72
57
  export function ResourceProvider({
73
58
  children,
74
59
  fallback,
60
+ resource,
75
61
  ...config
76
62
  }: ResourceProviderProps): React.ReactNode {
77
- const parent = useContext(SanityInstanceContext)
78
- const instance = useMemo(
79
- () => (parent ? parent.createChild(config) : createSanityInstance(config)),
80
- [config, parent],
81
- )
63
+ const parentPerspective = useContext(PerspectiveContext)
64
+ const parentResource = useContext(ResourceContext)
65
+ const parentInstance = useContext(SanityInstanceContext)
66
+
67
+ const {projectId, dataset, perspective} = config
68
+
69
+ const [instance] = useState<SanityInstance>(() => parentInstance ?? createSanityInstance(config))
82
70
 
83
- const projectId = config.projectId ?? ''
84
- useMemo(() => {
85
- if (projectId && !parent) initTelemetry(instance, projectId)
86
- }, [instance, projectId, parent])
71
+ const configResource: DatasetResource | undefined = useMemo(() => {
72
+ if (projectId && dataset) {
73
+ return {projectId, dataset}
74
+ }
75
+ return undefined
76
+ }, [projectId, dataset])
77
+
78
+ const effectiveResource = useMemo(() => {
79
+ return resource ?? configResource ?? parentResource
80
+ }, [resource, configResource, parentResource])
81
+
82
+ useEffect(() => {
83
+ if (effectiveResource && isDatasetResource(effectiveResource))
84
+ initTelemetry(instance, effectiveResource.projectId)
85
+ }, [instance, effectiveResource])
87
86
 
88
87
  // Ref to hold the scheduled disposal timer.
89
88
  const disposal = useRef<{
@@ -102,17 +101,22 @@ export function ResourceProvider({
102
101
  disposal.current = {
103
102
  instance,
104
103
  timeoutId: setTimeout(() => {
105
- if (!instance.isDisposed()) {
104
+ // don't dispose the parent instance when this unmounts
105
+ if (!instance.isDisposed() && instance !== parentInstance) {
106
106
  instance.dispose()
107
107
  }
108
108
  }, 0),
109
109
  }
110
110
  }
111
- }, [instance])
111
+ }, [instance, parentInstance])
112
112
 
113
113
  return (
114
114
  <SanityInstanceProvider instance={instance} fallback={fallback ?? DEFAULT_FALLBACK}>
115
- {children}
115
+ <ResourceContext.Provider value={effectiveResource}>
116
+ <PerspectiveContext.Provider value={perspective ?? parentPerspective}>
117
+ {children}
118
+ </PerspectiveContext.Provider>
119
+ </ResourceContext.Provider>
116
120
  </SanityInstanceProvider>
117
121
  )
118
122
  }
@@ -11,11 +11,13 @@ import {
11
11
  type AgentTransformOptions,
12
12
  agentTranslate,
13
13
  type AgentTranslateOptions,
14
- type SanityInstance,
15
14
  } from '@sanity/sdk'
15
+ import {useCallback} from 'react'
16
16
  import {firstValueFrom} from 'rxjs'
17
17
 
18
- import {createCallbackHook} from '../helpers/createCallbackHook'
18
+ import {type ResourceHandle} from '../../config/handles'
19
+ import {useSanityInstance} from '../context/useSanityInstance'
20
+ import {useNormalizedResourceOptions} from '../helpers/useNormalizedResourceOptions'
19
21
 
20
22
  interface Subscription {
21
23
  unsubscribe(): void
@@ -103,10 +105,17 @@ interface Subscribable<T> {
103
105
  *
104
106
  * @category Agent Actions
105
107
  */
106
- export const useAgentGenerate: () => (options: AgentGenerateOptions) => Subscribable<unknown> =
107
- createCallbackHook(agentGenerate) as unknown as () => (
108
- options: AgentGenerateOptions,
109
- ) => Subscribable<unknown>
108
+ export function useAgentGenerate(
109
+ resourceHandle?: ResourceHandle,
110
+ ): (options: AgentGenerateOptions) => Subscribable<unknown> {
111
+ const instance = useSanityInstance()
112
+ const {resource} = useNormalizedResourceOptions(resourceHandle ?? {})
113
+ return useCallback(
114
+ (options: AgentGenerateOptions) =>
115
+ agentGenerate(instance, options, resource) as unknown as Subscribable<unknown>,
116
+ [instance, resource],
117
+ )
118
+ }
110
119
 
111
120
  /**
112
121
  * @alpha
@@ -179,10 +188,17 @@ export const useAgentGenerate: () => (options: AgentGenerateOptions) => Subscrib
179
188
  *
180
189
  * @category Agent Actions
181
190
  */
182
- export const useAgentTransform: () => (options: AgentTransformOptions) => Subscribable<unknown> =
183
- createCallbackHook(agentTransform) as unknown as () => (
184
- options: AgentTransformOptions,
185
- ) => Subscribable<unknown>
191
+ export function useAgentTransform(
192
+ resourceHandle?: ResourceHandle,
193
+ ): (options: AgentTransformOptions) => Subscribable<unknown> {
194
+ const instance = useSanityInstance()
195
+ const {resource} = useNormalizedResourceOptions(resourceHandle ?? {})
196
+ return useCallback(
197
+ (options: AgentTransformOptions) =>
198
+ agentTransform(instance, options, resource) as unknown as Subscribable<unknown>,
199
+ [instance, resource],
200
+ )
201
+ }
186
202
 
187
203
  /**
188
204
  * @alpha
@@ -274,20 +290,16 @@ export const useAgentTransform: () => (options: AgentTransformOptions) => Subscr
274
290
  *
275
291
  * @category Agent Actions
276
292
  */
277
- export const useAgentTranslate: () => (options: AgentTranslateOptions) => Subscribable<unknown> =
278
- createCallbackHook(agentTranslate) as unknown as () => (
279
- options: AgentTranslateOptions,
280
- ) => Subscribable<unknown>
281
-
282
- /**
283
- * @internal
284
- * Adapter to convert the agentPrompt observable to a Promise.
285
- */
286
- function promptAdapter(
287
- instance: SanityInstance,
288
- options: AgentPromptOptions,
289
- ): Promise<AgentPromptResult> {
290
- return firstValueFrom(agentPrompt(instance, options))
293
+ export function useAgentTranslate(
294
+ resourceHandle?: ResourceHandle,
295
+ ): (options: AgentTranslateOptions) => Subscribable<unknown> {
296
+ const instance = useSanityInstance()
297
+ const {resource} = useNormalizedResourceOptions(resourceHandle ?? {})
298
+ return useCallback(
299
+ (options: AgentTranslateOptions) =>
300
+ agentTranslate(instance, options, resource) as unknown as Subscribable<unknown>,
301
+ [instance, resource],
302
+ )
291
303
  }
292
304
 
293
305
  /**
@@ -384,18 +396,15 @@ function promptAdapter(
384
396
  *
385
397
  * @category Agent Actions
386
398
  */
387
- export const useAgentPrompt: () => (options: AgentPromptOptions) => Promise<AgentPromptResult> =
388
- createCallbackHook(promptAdapter)
389
-
390
- /**
391
- * @internal
392
- * Adapter to convert the agentPatch observable to a Promise.
393
- */
394
- function patchAdapter(
395
- instance: SanityInstance,
396
- options: AgentPatchOptions,
397
- ): Promise<AgentPatchResult> {
398
- return firstValueFrom(agentPatch(instance, options))
399
+ export function useAgentPrompt(
400
+ resourceHandle?: ResourceHandle,
401
+ ): (options: AgentPromptOptions) => Promise<AgentPromptResult> {
402
+ const instance = useSanityInstance()
403
+ const {resource} = useNormalizedResourceOptions(resourceHandle ?? {})
404
+ return useCallback(
405
+ (options: AgentPromptOptions) => firstValueFrom(agentPrompt(instance, options, resource)),
406
+ [instance, resource],
407
+ )
399
408
  }
400
409
 
401
410
  /**
@@ -547,5 +556,13 @@ function patchAdapter(
547
556
  *
548
557
  * @category Agent Actions
549
558
  */
550
- export const useAgentPatch: () => (options: AgentPatchOptions) => Promise<AgentPatchResult> =
551
- createCallbackHook(patchAdapter)
559
+ export function useAgentPatch(
560
+ resourceHandle?: ResourceHandle,
561
+ ): (options: AgentPatchOptions) => Promise<AgentPatchResult> {
562
+ const instance = useSanityInstance()
563
+ const {resource} = useNormalizedResourceOptions(resourceHandle ?? {})
564
+ return useCallback(
565
+ (options: AgentPatchOptions) => firstValueFrom(agentPatch(instance, options, resource)),
566
+ [instance, resource],
567
+ )
568
+ }
@@ -0,0 +1,32 @@
1
+ import {renderHook as reactRenderHook} from '@testing-library/react'
2
+ import {type ReactNode} from 'react'
3
+ import {describe, expect, it} from 'vitest'
4
+
5
+ import {renderHook} from '../../../test/test-utils'
6
+ import {ResourceProvider} from '../../context/ResourceProvider'
7
+ import {useResource} from './useResource'
8
+
9
+ describe('useResource', () => {
10
+ it('returns the resource from the instance config when no explicit resource is set', () => {
11
+ // test-utils wraps with ResourceProvider projectId="test" dataset="test"
12
+ const {result} = renderHook(() => useResource())
13
+ expect(result.current).toEqual({projectId: 'test', dataset: 'test'})
14
+ })
15
+
16
+ it('returns the explicit resource when ResourceProvider has a resource prop', () => {
17
+ const resource = {projectId: 'explicit-project', dataset: 'explicit-dataset'}
18
+ const {result} = reactRenderHook(() => useResource(), {
19
+ wrapper: ({children}: {children: ReactNode}) => (
20
+ <ResourceProvider resource={resource} fallback={null}>
21
+ {children}
22
+ </ResourceProvider>
23
+ ),
24
+ })
25
+ expect(result.current).toEqual(resource)
26
+ })
27
+
28
+ it('returns undefined when no resource or instance config is available', () => {
29
+ const {result} = reactRenderHook(() => useResource())
30
+ expect(result.current).toBeUndefined()
31
+ })
32
+ })
@@ -0,0 +1,24 @@
1
+ import {type DocumentResource} from '@sanity/sdk'
2
+
3
+ import {useEffectiveContextResource} from '../helpers/useNormalizedResourceOptions'
4
+
5
+ /**
6
+ * Returns the currently active `DocumentResource` for the nearest resource context.
7
+ *
8
+ * Resolves in priority order:
9
+ * 1. A `resource` prop on the nearest `<ResourceProvider>`
10
+ * 2. The `projectId`/`dataset` from the current `SanityInstance` config
11
+ * 3. `undefined` when neither is available
12
+ *
13
+ * @public
14
+ * @category Platform
15
+ *
16
+ * @example
17
+ * ```tsx
18
+ * const resource = useResource()
19
+ * console.log(resource?.projectId, resource?.dataset)
20
+ * ```
21
+ */
22
+ export function useResource(): DocumentResource | undefined {
23
+ return useEffectiveContextResource()
24
+ }