@sanity/sdk-react 3.0.0-rc.2 → 3.1.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 (43) hide show
  1. package/dist/_exports/dashboard.d.ts +250 -0
  2. package/dist/_exports/dashboard.d.ts.map +1 -0
  3. package/dist/_exports/dashboard.js +278 -0
  4. package/dist/_exports/dashboard.js.map +1 -0
  5. package/dist/index.d.ts +86 -261
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +114 -616
  8. package/dist/index.js.map +1 -1
  9. package/dist/useStudioWorkspacesByProjectIdDataset-DxUlukmF.js +317 -0
  10. package/dist/useStudioWorkspacesByProjectIdDataset-DxUlukmF.js.map +1 -0
  11. package/package.json +12 -11
  12. package/src/_exports/dashboard.ts +12 -0
  13. package/src/_exports/sdk-react.ts +2 -12
  14. package/src/components/SDKProvider.test.tsx +5 -5
  15. package/src/components/auth/AuthBoundary.tsx +5 -5
  16. package/src/context/{WorkbenchTokenRefresh.test.tsx → DashboardTokenRefresh.test.tsx} +26 -26
  17. package/src/context/DashboardTokenRefresh.tsx +95 -0
  18. package/src/context/OrganizationResourcesProvider.test.tsx +9 -9
  19. package/src/context/OrganizationResourcesProvider.tsx +2 -2
  20. package/src/context/dashboardToken.ts +63 -0
  21. package/src/hooks/comlink/useWindowConnection.ts +1 -1
  22. package/src/hooks/{agent → dashboard}/useAgentResourceContext.ts +1 -1
  23. package/src/hooks/dashboard/useFavorite.test.tsx +101 -0
  24. package/src/hooks/dashboard/useFavorite.ts +34 -0
  25. package/src/hooks/dashboard/useFavoriteContext.ts +61 -0
  26. package/src/hooks/dashboard/{useDashboardNavigate.test.ts → useNavigate.test.ts} +3 -3
  27. package/src/hooks/dashboard/{useDashboardNavigate.ts → useNavigate.ts} +5 -5
  28. package/src/hooks/dashboard/useNavigateToStudioDocument.ts +2 -1
  29. package/src/hooks/{auth/useDashboardOrganizationId.test.tsx → dashboard/useOrganizationId.test.tsx} +4 -4
  30. package/src/hooks/{auth/useDashboardOrganizationId.tsx → dashboard/useOrganizationId.tsx} +2 -2
  31. package/src/hooks/dashboard/useUpdateFavorite.test.tsx +146 -0
  32. package/src/hooks/dashboard/useUpdateFavorite.ts +74 -0
  33. package/src/hooks/dashboard/useWindowTitle.ts +1 -1
  34. package/src/hooks/datasets/useDatasets.test.tsx +29 -22
  35. package/src/hooks/datasets/useDatasets.ts +31 -53
  36. package/src/hooks/document/useCreateDocument.ts +2 -1
  37. package/src/utils/resolveOrgResources.test.ts +2 -2
  38. package/src/utils/resolveOrgResources.ts +2 -2
  39. package/src/context/WorkbenchTokenRefresh.tsx +0 -61
  40. package/src/context/workbenchToken.ts +0 -63
  41. package/src/hooks/dashboard/useManageFavorite.test.tsx +0 -379
  42. package/src/hooks/dashboard/useManageFavorite.ts +0 -173
  43. /package/src/hooks/{agent → dashboard}/useAgentResourceContext.test.tsx +0 -0
@@ -4,14 +4,14 @@ import {throwError} from 'rxjs'
4
4
  import {describe, expect, it, vi} from 'vitest'
5
5
 
6
6
  import {ResourceProvider} from '../../context/ResourceProvider'
7
- import {useDashboardOrganizationId} from './useDashboardOrganizationId'
7
+ import {useOrganizationId} from './useOrganizationId'
8
8
 
9
9
  vi.mock('@sanity/sdk', async (importOriginal) => {
10
10
  const actual = await importOriginal()
11
11
  return {...(actual || {}), getDashboardOrganizationId: vi.fn()}
12
12
  })
13
13
 
14
- describe('useDashboardOrganizationId', () => {
14
+ describe('useOrganizationId', () => {
15
15
  it('should return undefined when no organization ID is set', () => {
16
16
  const subscribe = vi.fn()
17
17
  vi.mocked(getDashboardOrganizationId).mockReturnValue({
@@ -20,7 +20,7 @@ describe('useDashboardOrganizationId', () => {
20
20
  observable: throwError(() => new Error('Unexpected usage of observable')),
21
21
  })
22
22
 
23
- const {result} = renderHook(() => useDashboardOrganizationId(), {
23
+ const {result} = renderHook(() => useOrganizationId(), {
24
24
  wrapper: ({children}) => (
25
25
  <ResourceProvider projectId="test-project" dataset="test-dataset" fallback={null}>
26
26
  {children}
@@ -39,7 +39,7 @@ describe('useDashboardOrganizationId', () => {
39
39
  observable: throwError(() => new Error('Unexpected usage of observable')),
40
40
  })
41
41
 
42
- const {result} = renderHook(() => useDashboardOrganizationId(), {
42
+ const {result} = renderHook(() => useOrganizationId(), {
43
43
  wrapper: ({children}) => (
44
44
  <ResourceProvider projectId="test-project" dataset="test-dataset" fallback={null}>
45
45
  {children}
@@ -11,7 +11,7 @@ import {useSanityInstance} from '../context/useSanityInstance'
11
11
  * @example
12
12
  * ```tsx
13
13
  * function DashboardComponent() {
14
- * const orgId = useDashboardOrganizationId()
14
+ * const orgId = useOrganizationId()
15
15
  *
16
16
  * if (!orgId) return null
17
17
  *
@@ -22,7 +22,7 @@ import {useSanityInstance} from '../context/useSanityInstance'
22
22
  * @category Dashboard
23
23
  * @returns The dashboard organization ID (string | undefined)
24
24
  */
25
- export function useDashboardOrganizationId(): string | undefined {
25
+ export function useOrganizationId(): string | undefined {
26
26
  const instance = useSanityInstance()
27
27
  const {subscribe, getCurrent} = useMemo(() => getDashboardOrganizationId(instance), [instance])
28
28
 
@@ -0,0 +1,146 @@
1
+ import {type FavoriteStatusResponse, setFavorite} from '@sanity/sdk'
2
+ import {type MutationResult} from '@sanity/sdk/_internal'
3
+ import {act, renderHook, waitFor} from '@testing-library/react'
4
+ import {type ReactNode} from 'react'
5
+ import {afterEach, beforeEach, describe, expect, it, type Mock, vi} from 'vitest'
6
+
7
+ import {ResourceProvider} from '../../context/ResourceProvider'
8
+ import {useUpdateFavorite} from './useUpdateFavorite'
9
+
10
+ vi.mock(import('@sanity/sdk'), async (importOriginal) => {
11
+ const actual = await importOriginal()
12
+ return {
13
+ ...actual,
14
+ setFavorite: vi.fn(),
15
+ }
16
+ })
17
+
18
+ type SetFavorite = typeof setFavorite
19
+
20
+ describe('useUpdateFavorite', () => {
21
+ const mockSetFavorite = setFavorite as Mock
22
+
23
+ const handle = {
24
+ documentId: 'mock-id',
25
+ documentType: 'mock-type',
26
+ resourceType: 'studio' as const,
27
+ }
28
+
29
+ const makeWrapper = (projectId?: string, dataset?: string) => {
30
+ return function Wrapper({children}: {children: ReactNode}) {
31
+ return (
32
+ <ResourceProvider projectId={projectId} dataset={dataset} fallback={null}>
33
+ {children}
34
+ </ResourceProvider>
35
+ )
36
+ }
37
+ }
38
+ const wrapper = makeWrapper('test', 'test')
39
+
40
+ beforeEach(() => {
41
+ mockSetFavorite.mockImplementation((async (_instance, input) => ({
42
+ data: {isFavorited: input.isFavorited},
43
+ invalidated: Promise.resolve(),
44
+ })) as SetFavorite)
45
+ })
46
+
47
+ afterEach(() => {
48
+ vi.clearAllMocks()
49
+ })
50
+
51
+ it('sends the added event with the resolved studio resourceId', async () => {
52
+ const {result} = renderHook(() => useUpdateFavorite(handle), {wrapper})
53
+
54
+ await act(async () => {
55
+ await result.current.favorite()
56
+ })
57
+
58
+ expect(setFavorite).toHaveBeenCalledWith(
59
+ expect.anything(),
60
+ expect.objectContaining({
61
+ documentId: 'mock-id',
62
+ documentType: 'mock-type',
63
+ resourceId: 'test.test',
64
+ resourceType: 'studio',
65
+ isFavorited: true,
66
+ }),
67
+ )
68
+ })
69
+
70
+ it('sends the removed event when unfavoriting', async () => {
71
+ const {result} = renderHook(() => useUpdateFavorite(handle), {wrapper})
72
+
73
+ await act(async () => {
74
+ await result.current.unfavorite()
75
+ })
76
+
77
+ expect(setFavorite).toHaveBeenCalledWith(
78
+ expect.anything(),
79
+ expect.objectContaining({isFavorited: false}),
80
+ )
81
+ })
82
+
83
+ it('passes schemaName through when provided', async () => {
84
+ const {result} = renderHook(() => useUpdateFavorite({...handle, schemaName: 'testSchema'}), {
85
+ wrapper,
86
+ })
87
+
88
+ await act(async () => {
89
+ await result.current.favorite()
90
+ })
91
+
92
+ expect(setFavorite).toHaveBeenCalledWith(
93
+ expect.anything(),
94
+ expect.objectContaining({schemaName: 'testSchema', isFavorited: true}),
95
+ )
96
+ })
97
+
98
+ it('tracks isPending across the mutation lifecycle', async () => {
99
+ let resolveMutation!: (value: MutationResult<FavoriteStatusResponse>) => void
100
+ mockSetFavorite.mockReturnValue(
101
+ new Promise<MutationResult<FavoriteStatusResponse>>((resolve) => {
102
+ resolveMutation = resolve
103
+ }),
104
+ )
105
+
106
+ const {result} = renderHook(() => useUpdateFavorite(handle), {wrapper})
107
+ expect(result.current.isPending).toBe(false)
108
+
109
+ act(() => {
110
+ void result.current.favorite()
111
+ })
112
+ expect(result.current.isPending).toBe(true)
113
+
114
+ await act(async () => {
115
+ resolveMutation({data: {isFavorited: true}, invalidated: Promise.resolve()})
116
+ })
117
+ expect(result.current.isPending).toBe(false)
118
+ })
119
+
120
+ it('surfaces mutation failures on error', async () => {
121
+ mockSetFavorite.mockRejectedValue(new Error('mutate failed'))
122
+
123
+ const {result} = renderHook(() => useUpdateFavorite(handle), {wrapper})
124
+
125
+ await act(async () => {
126
+ await expect(result.current.favorite()).rejects.toThrow('mutate failed')
127
+ })
128
+
129
+ await waitFor(() => expect(result.current.error).toBeInstanceOf(Error))
130
+ })
131
+
132
+ it('throws when a studio resource is missing projectId or dataset', () => {
133
+ expect(() =>
134
+ renderHook(() => useUpdateFavorite(handle), {wrapper: makeWrapper(undefined, undefined)}),
135
+ ).toThrow('projectId and dataset are required for studio resources')
136
+ })
137
+
138
+ it('throws when resourceId is missing for non-studio resources', () => {
139
+ expect(() =>
140
+ renderHook(
141
+ () => useUpdateFavorite({...handle, resourceType: 'media-library', resourceId: undefined}),
142
+ {wrapper},
143
+ ),
144
+ ).toThrow('resourceId is required for media-library and canvas resources')
145
+ })
146
+ })
@@ -0,0 +1,74 @@
1
+ import {type FavoriteStatusResponse, setFavorite} from '@sanity/sdk'
2
+ import {useCallback} from 'react'
3
+
4
+ import {createMutationHook} from '../helpers/createMutationHook'
5
+ import {useFavoriteContext, type UseFavoriteProps} from './useFavoriteContext'
6
+
7
+ const useSetFavorite = createMutationHook(setFavorite)
8
+
9
+ /**
10
+ * The value returned by {@link useUpdateFavorite}.
11
+ *
12
+ * @internal
13
+ */
14
+ export interface UpdateFavorite {
15
+ /** Adds the document to favorites. */
16
+ favorite: () => Promise<FavoriteStatusResponse>
17
+ /** Removes the document from favorites. */
18
+ unfavorite: () => Promise<FavoriteStatusResponse>
19
+ /** A favorite or unfavorite mutation is currently in flight. */
20
+ isPending: boolean
21
+ /** The most recent failure; cleared by the next call or `reset`. */
22
+ error: unknown
23
+ /** Clears error and pending state back to idle. */
24
+ reset: () => void
25
+ }
26
+
27
+ /**
28
+ * @internal
29
+ *
30
+ * Adds or removes a document from favorites. The read-side counterpart is
31
+ * {@link useFavorite}, which reflects the change once the mutation settles.
32
+ *
33
+ * Unlike {@link useFavorite}, this hook does not suspend.
34
+ *
35
+ * @param props - The document handle plus the resource it lives in.
36
+ * @returns `favorite`/`unfavorite` actions and the `{isPending, error, reset}`
37
+ * mutation state.
38
+ *
39
+ * @example
40
+ * ```tsx
41
+ * function FavoriteButton(props: DocumentActionProps) {
42
+ * const {documentId, documentType} = props
43
+ * const handle = {documentId, documentType, resourceType: 'studio'} as const
44
+ * const isFavorited = useFavorite(handle)
45
+ * const {favorite, unfavorite, isPending} = useUpdateFavorite(handle)
46
+ *
47
+ * return (
48
+ * <Button
49
+ * disabled={isPending}
50
+ * onClick={() => (isFavorited ? unfavorite() : favorite())}
51
+ * text={isFavorited ? 'Remove from favorites' : 'Add to favorites'}
52
+ * />
53
+ * )
54
+ * }
55
+ *
56
+ * // Wrap the component with Suspense since useFavorite suspends
57
+ * function MyDocumentAction(props: DocumentActionProps) {
58
+ * return (
59
+ * <Suspense fallback={<Button text="Loading..." disabled />}>
60
+ * <FavoriteButton {...props} />
61
+ * </Suspense>
62
+ * )
63
+ * }
64
+ * ```
65
+ */
66
+ export function useUpdateFavorite(props: UseFavoriteProps): UpdateFavorite {
67
+ const context = useFavoriteContext(props)
68
+ const {mutate, isPending, error, reset} = useSetFavorite()
69
+
70
+ const favorite = useCallback(() => mutate({...context, isFavorited: true}), [mutate, context])
71
+ const unfavorite = useCallback(() => mutate({...context, isFavorited: false}), [mutate, context])
72
+
73
+ return {favorite, unfavorite, isPending, error, reset}
74
+ }
@@ -42,7 +42,7 @@ function resolveAppTitle(resource: ContextResource): string | undefined {
42
42
  *
43
43
  * @example
44
44
  * ```tsx
45
- * import {useWindowTitle} from '@sanity/sdk-react'
45
+ * import {useWindowTitle} from '@sanity/sdk-react/dashboard'
46
46
  *
47
47
  * function MoviesList() {
48
48
  * useWindowTitle('Movies')
@@ -1,10 +1,6 @@
1
1
  import {type DatasetsResponse} from '@sanity/client'
2
- import {
3
- createSanityInstance,
4
- getDatasetsState,
5
- resolveDatasets,
6
- type StateSource,
7
- } from '@sanity/sdk'
2
+ import {createSanityInstance, datasets, type StateSource} from '@sanity/sdk'
3
+ import {type FetcherSnapshot} from '@sanity/sdk/_internal'
8
4
  import {type ReactNode} from 'react'
9
5
  import {type Observable} from 'rxjs'
10
6
  import {beforeEach, describe, expect, it, vi} from 'vitest'
@@ -16,32 +12,43 @@ import {useDatasets} from './useDatasets'
16
12
 
17
13
  vi.mock('@sanity/sdk', async (importOriginal) => {
18
14
  const original = await importOriginal<typeof import('@sanity/sdk')>()
19
- return {...original, getDatasetsState: vi.fn(), resolveDatasets: vi.fn()}
15
+ return {...original, datasets: {getState: vi.fn(), resolveState: vi.fn()}}
20
16
  })
21
17
 
22
18
  const stateSource = (
23
19
  current: DatasetsResponse | undefined,
24
- ): StateSource<DatasetsResponse | undefined> =>
25
- ({
26
- getCurrent: vi.fn(() => current),
27
- subscribe: vi.fn(),
20
+ ): StateSource<FetcherSnapshot<DatasetsResponse>> => {
21
+ // Cache the snapshot: useSyncExternalStore requires a referentially stable current value.
22
+ const snapshot = current
23
+ ? {status: 'success', data: current, error: undefined, isFetching: false, dataUpdatedAt: 1}
24
+ : {
25
+ status: 'pending',
26
+ data: undefined,
27
+ error: undefined,
28
+ isFetching: true,
29
+ dataUpdatedAt: undefined,
30
+ }
31
+ return {
32
+ getCurrent: vi.fn(() => snapshot),
33
+ subscribe: vi.fn(() => () => {}),
28
34
  get observable(): Observable<unknown> {
29
35
  throw new Error('Not implemented')
30
36
  },
31
- }) as unknown as StateSource<DatasetsResponse | undefined>
37
+ } as unknown as StateSource<FetcherSnapshot<DatasetsResponse>>
38
+ }
32
39
 
33
40
  const sanityInstance = expect.objectContaining({config: expect.any(Object)})
34
41
 
35
42
  describe('useDatasets', () => {
36
43
  beforeEach(() => {
37
44
  vi.clearAllMocks()
38
- vi.mocked(getDatasetsState).mockReturnValue(stateSource([] as unknown as DatasetsResponse))
45
+ vi.mocked(datasets.getState).mockReturnValue(stateSource([] as unknown as DatasetsResponse))
39
46
  })
40
47
 
41
48
  it('resolves the projectId from the instance config resource', () => {
42
49
  // test-utils wraps with ResourceProvider projectId="test" dataset="test".
43
50
  renderHook(() => useDatasets())
44
- expect(getDatasetsState).toHaveBeenCalledWith(
51
+ expect(datasets.getState).toHaveBeenCalledWith(
45
52
  sanityInstance,
46
53
  expect.objectContaining({projectId: 'test'}),
47
54
  )
@@ -49,7 +56,7 @@ describe('useDatasets', () => {
49
56
 
50
57
  it('lets an explicit projectId override the ambient resource', () => {
51
58
  renderHook(() => useDatasets({projectId: 'explicit-project'}))
52
- expect(getDatasetsState).toHaveBeenCalledWith(
59
+ expect(datasets.getState).toHaveBeenCalledWith(
53
60
  sanityInstance,
54
61
  expect.objectContaining({projectId: 'explicit-project'}),
55
62
  )
@@ -66,7 +73,7 @@ describe('useDatasets', () => {
66
73
  </ResourceProvider>
67
74
  ),
68
75
  })
69
- expect(getDatasetsState).toHaveBeenCalledWith(
76
+ expect(datasets.getState).toHaveBeenCalledWith(
70
77
  sanityInstance,
71
78
  expect.objectContaining({projectId: 'resource-project'}),
72
79
  )
@@ -82,7 +89,7 @@ describe('useDatasets', () => {
82
89
  })
83
90
  // A dataset-less config can't form a DatasetResource; the projectId is carried
84
91
  // via ProjectContext and injected so project-scoped reads still resolve it.
85
- expect(getDatasetsState).toHaveBeenCalledWith(
92
+ expect(datasets.getState).toHaveBeenCalledWith(
86
93
  sanityInstance,
87
94
  expect.objectContaining({projectId: 'config-project'}),
88
95
  )
@@ -101,16 +108,16 @@ describe('useDatasets', () => {
101
108
  </SanityInstanceContext.Provider>
102
109
  ),
103
110
  })
104
- expect(getDatasetsState).toHaveBeenCalledWith(
111
+ expect(datasets.getState).toHaveBeenCalledWith(
105
112
  emptyInstance,
106
113
  expect.objectContaining({projectId: 'bare-project'}),
107
114
  )
108
115
  })
109
116
 
110
- it('suspends via resolveDatasets until dataset data is available', () => {
111
- vi.mocked(getDatasetsState).mockReturnValue(stateSource(undefined))
112
- vi.mocked(resolveDatasets).mockReturnValue(new Promise(() => {}))
117
+ it('suspends via the datasets fetcher until dataset data is available', () => {
118
+ vi.mocked(datasets.getState).mockReturnValue(stateSource(undefined))
119
+ vi.mocked(datasets.resolveState).mockReturnValue(new Promise(() => {}))
113
120
  renderHook(() => useDatasets())
114
- expect(resolveDatasets).toHaveBeenCalled()
121
+ expect(datasets.resolveState).toHaveBeenCalled()
115
122
  })
116
123
  })
@@ -1,64 +1,42 @@
1
1
  import {type DatasetsResponse} from '@sanity/client'
2
- import {
3
- getDatasetsState,
4
- type ProjectHandle,
5
- resolveDatasets,
6
- type SanityInstance,
7
- type StateSource,
8
- } from '@sanity/sdk'
2
+ import {datasets, type ProjectHandle} from '@sanity/sdk'
9
3
 
10
- import {createStateSourceHook} from '../helpers/createStateSourceHook'
4
+ import {createFetcherHook, type FetcherHookResult} from '../helpers/createFetcherHook'
11
5
  import {useResolvedProjectId} from '../helpers/useResolvedProjectId'
12
6
 
13
- type UseDatasets = {
14
- /**
15
- *
16
- * Returns metadata for each dataset the current user has access to.
17
- *
18
- * @category Datasets
19
- * @param options - Optional project/resource to read datasets for. Defaults to
20
- * the resource named in `ResourceProvider`/`SDKProvider`.
21
- * @returns The metadata for your the datasets
22
- *
23
- * @example
24
- * ```tsx
25
- * const datasets = useDatasets()
26
- *
27
- * return (
28
- * <select>
29
- * {datasets.map((dataset) => (
30
- * <option key={dataset.name}>{dataset.name}</option>
31
- * ))}
32
- * </select>
33
- * )
34
- * ```
35
- *
36
- * @remarks
37
- * The `projectId` is resolved in order from:
38
- * 1. an explicit `projectId` option
39
- * 2. A legacy ProjectContext (e.g. a `<ResourceProvider projectId="…">` with no dataset), then
40
- * 3. The active resource (`ResourceProvider`/`SDKProvider`)
41
- * 4. `instance.config`.
42
- */
43
- (options?: ProjectHandle): DatasetsResponse
44
- }
45
-
46
- const useDatasetsBase = createStateSourceHook({
47
- getState: getDatasetsState as (
48
- instance: SanityInstance,
49
- projectHandle?: ProjectHandle,
50
- ) => StateSource<DatasetsResponse>,
51
- shouldSuspend: (instance, projectHandle?: ProjectHandle) =>
52
- // remove `undefined` since we're suspending when that is the case
53
- getDatasetsState(instance, projectHandle).getCurrent() === undefined,
54
- suspender: resolveDatasets,
55
- })
7
+ const useDatasetsBase = createFetcherHook(datasets)
56
8
 
57
9
  /**
10
+ * Returns metadata for each dataset the current user has access to.
11
+ *
12
+ * @category Datasets
13
+ * @param options - Optional project/resource to read datasets for. Defaults to
14
+ * the resource named in `ResourceProvider`/`SDKProvider`.
15
+ * @returns A {@link FetcherHookResult} whose `data` is the metadata for the
16
+ * datasets.
17
+ *
18
+ * @example
19
+ * ```tsx
20
+ * const {data: datasets} = useDatasets()
21
+ *
22
+ * return (
23
+ * <select>
24
+ * {datasets.map((dataset) => (
25
+ * <option key={dataset.name}>{dataset.name}</option>
26
+ * ))}
27
+ * </select>
28
+ * )
29
+ * ```
30
+ *
31
+ * @remarks
32
+ * The `projectId` is resolved in order from:
33
+ * 1. an explicit `projectId` option
34
+ * 2. A legacy ProjectContext (e.g. a `<ResourceProvider projectId="…">` with no dataset), then
35
+ * 3. The active resource (`ResourceProvider`/`SDKProvider`)
36
+ * 4. `instance.config`.
58
37
  * @public
59
- * @function
60
38
  */
61
- export const useDatasets: UseDatasets = (options) => {
39
+ export function useDatasets(options?: ProjectHandle): FetcherHookResult<DatasetsResponse> {
62
40
  const projectId = useResolvedProjectId(options)
63
41
  return useDatasetsBase(projectId ? {...options, projectId} : options)
64
42
  }
@@ -1,4 +1,5 @@
1
1
  import {createDocument} from '@sanity/sdk'
2
+ import {randomUuid} from '@sanity/sdk/_internal'
2
3
  import {type SanityDocument} from 'groq'
3
4
 
4
5
  import {type DocumentHandle, type DocumentTypeHandle} from '../../config/handles'
@@ -109,7 +110,7 @@ export function useCreateDocument(
109
110
  const apply = useApplyDocumentActions()
110
111
 
111
112
  return async (initialValue, overrides) => {
112
- const documentId = overrides?.documentId ?? options.documentId ?? crypto.randomUUID()
113
+ const documentId = overrides?.documentId ?? options.documentId ?? randomUuid()
113
114
  const handle: DocumentHandle = {...options, documentId}
114
115
  await apply(createDocument(handle, initialValue))
115
116
  return handle
@@ -52,10 +52,10 @@ describe('resolveOrgResources', () => {
52
52
  await resolveOrgResources(mockInstance, ORG_ID)
53
53
 
54
54
  expect(mockRequest).toHaveBeenCalledWith(
55
- expect.objectContaining({uri: '/media-libraries', query: {organizationId: ORG_ID}}),
55
+ expect.objectContaining({url: '/media-libraries', query: {organizationId: ORG_ID}}),
56
56
  )
57
57
  expect(mockRequest).toHaveBeenCalledWith(
58
- expect.objectContaining({uri: '/canvases', query: {organizationId: ORG_ID}}),
58
+ expect.objectContaining({url: '/canvases', query: {organizationId: ORG_ID}}),
59
59
  )
60
60
  })
61
61
 
@@ -48,12 +48,12 @@ export async function resolveOrgResources(
48
48
 
49
49
  const [mediaLibrariesResult, canvasesResult] = await Promise.allSettled([
50
50
  client.request<OrgResourcesApiResponse>({
51
- uri: `/media-libraries`,
51
+ url: `/media-libraries`,
52
52
  query: {organizationId},
53
53
  tag: 'org-resources.media-libraries',
54
54
  }),
55
55
  client.request<OrgResourcesApiResponse>({
56
- uri: `/canvases`,
56
+ url: `/canvases`,
57
57
  query: {organizationId},
58
58
  tag: 'org-resources.canvases',
59
59
  }),
@@ -1,61 +0,0 @@
1
- import {type ClientError} from '@sanity/client'
2
- import {AuthStateType, setAuthToken} from '@sanity/sdk'
3
- import React, {type PropsWithChildren, useEffect, useRef} from 'react'
4
-
5
- import {useAuthState} from '../hooks/auth/useAuthState'
6
- import {useSanityInstance} from '../hooks/context/useSanityInstance'
7
- import {
8
- isWorkbenchEnvironment,
9
- observeWorkbenchToken,
10
- refreshWorkbenchToken,
11
- } from './workbenchToken'
12
-
13
- /**
14
- * Keeps the SDK auth token in sync with the workbench "OS".
15
- *
16
- * When running as a federated remote inside the workbench the OS owns the
17
- * session, so we subscribe to its `auth.token` stream and mirror each value into
18
- * the auth store — a token logs us in, `null` logs us out, and later OS
19
- * sign-in/out propagates automatically. When a request is rejected with a 401
20
- * (the token expired), we ask the OS to reissue rather than tearing the session
21
- * down; the new token arrives back through the same subscription.
22
- */
23
- function WorkbenchTokenRefresh({children}: PropsWithChildren) {
24
- const instance = useSanityInstance()
25
- const authState = useAuthState()
26
- const processed401ErrorRef = useRef<unknown | null>(null)
27
-
28
- useEffect(() => {
29
- const token$ = observeWorkbenchToken()
30
- if (!token$) return undefined
31
- const subscription = token$.subscribe((token) => setAuthToken(instance, token))
32
- return () => subscription.unsubscribe()
33
- }, [instance])
34
-
35
- useEffect(() => {
36
- const has401Error =
37
- authState.type === AuthStateType.ERROR && (authState.error as ClientError)?.statusCode === 401
38
-
39
- if (has401Error && processed401ErrorRef.current !== authState.error) {
40
- processed401ErrorRef.current = authState.error
41
- refreshWorkbenchToken()
42
- } else if (!has401Error) {
43
- processed401ErrorRef.current = null
44
- }
45
- }, [authState])
46
-
47
- return children
48
- }
49
-
50
- /**
51
- * Provides workbench OS token refresh. No-op outside the workbench, where the
52
- * app uses its normal auth flow.
53
- * @public
54
- */
55
- export const WorkbenchTokenRefreshProvider: React.FC<PropsWithChildren> = ({children}) => {
56
- if (isWorkbenchEnvironment()) {
57
- return <WorkbenchTokenRefresh>{children}</WorkbenchTokenRefresh>
58
- }
59
-
60
- return children
61
- }
@@ -1,63 +0,0 @@
1
- import {from, type Observable, of} from 'rxjs'
2
- import {catchError, switchMap} from 'rxjs/operators'
3
-
4
- // The workbench host installs its shared message bus on this well-known global
5
- // symbol before it loads federated remotes. It must match the key used by
6
- // `@sanity/workbench` (`Symbol.for('sanity.os.bus')`).
7
- const OS_BUS_KEY = Symbol.for('sanity.os.bus')
8
-
9
- /**
10
- * Whether this app is running as a federated remote inside the workbench.
11
- *
12
- * Federation shares the host's realm, so the installed bus is visible on
13
- * `globalThis`. This is `false` in a standalone app, where we must never import
14
- * `@sanity/workbench` (it would install a bus and add bundle weight for no
15
- * reason). Note: this is a different embedding model to the Core UI iframe,
16
- * which is detected separately via the dashboard context — that signal is not
17
- * set on the federation path.
18
- *
19
- * @internal
20
- */
21
- export function isWorkbenchEnvironment(): boolean {
22
- return typeof globalThis === 'object' && OS_BUS_KEY in globalThis
23
- }
24
-
25
- /**
26
- * Observes the session token issued by the workbench "OS", tracking the OS auth
27
- * state over time.
28
- *
29
- * Returns `undefined` when the app is not embedded in the workbench, so the
30
- * caller uses its normal auth flow. Inside the workbench, subscribes to the
31
- * `auth.token` state topic, emitting the current token — or `null` when the OS
32
- * is signed out — and re-emitting as the OS auth state changes, so sign-in/out
33
- * propagates instead of being captured once. Any bus error is treated as "no
34
- * token" (`null`). The token is used in-memory only and never persisted.
35
- *
36
- * @internal
37
- */
38
- export function observeWorkbenchToken(): Observable<string | null> | undefined {
39
- if (!isWorkbenchEnvironment()) return undefined
40
-
41
- return from(import('@sanity/workbench')).pipe(
42
- switchMap(({os}) => os.subscribe('auth.token')),
43
- // Any failure (importing the host bundle, or the subscription) means "no OS token".
44
- catchError(() => of(null)),
45
- )
46
- }
47
-
48
- /**
49
- * Asks the workbench "OS" to reissue the session token, e.g. after its current
50
- * one was rejected with a 401. Fire-and-forget: the reissued token arrives via
51
- * the `auth.token` subscription in {@link observeWorkbenchToken}. No-op outside
52
- * the workbench.
53
- *
54
- * @internal
55
- */
56
- export function refreshWorkbenchToken(): void {
57
- if (!isWorkbenchEnvironment()) return
58
-
59
- void import('@sanity/workbench').then(
60
- ({os}) => os.emit('auth.token.refresh', undefined),
61
- () => {},
62
- )
63
- }