@sanity/sdk-react 3.0.0-rc.0 → 3.0.0-rc.1
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.
- package/README.md +103 -155
- package/dist/index.d.ts +1564 -682
- package/dist/index.js +1118 -612
- package/dist/index.js.map +1 -1
- package/package.json +51 -54
- package/src/_exports/index.ts +2 -4
- package/src/_exports/sdk-react.ts +26 -17
- package/src/components/SDKProvider.test.tsx +173 -32
- package/src/components/SDKProvider.tsx +87 -28
- package/src/components/SanityApp.test.tsx +68 -12
- package/src/components/SanityApp.tsx +94 -87
- package/src/components/auth/AuthBoundary.recovery.test.tsx +86 -0
- package/src/components/auth/AuthBoundary.test.tsx +26 -8
- package/src/components/auth/AuthBoundary.tsx +28 -6
- package/src/components/auth/DashboardAccessRequest.tsx +37 -0
- package/src/components/auth/LoginError.test.tsx +191 -5
- package/src/components/auth/LoginError.tsx +103 -59
- package/src/components/errors/ChunkLoadError.test.tsx +59 -0
- package/src/components/errors/ChunkLoadError.tsx +56 -0
- package/src/components/errors/CorsErrorComponent.tsx +2 -2
- package/src/components/errors/chunkReloadStorage.ts +57 -0
- package/src/config/handles.ts +29 -27
- package/src/constants.ts +5 -0
- package/src/context/ComlinkTokenRefresh.test.tsx +10 -27
- package/src/context/DefaultResourceContext.ts +3 -3
- package/src/context/OrganizationResourcesProvider.test.tsx +189 -0
- package/src/context/OrganizationResourcesProvider.tsx +111 -0
- package/src/context/PerspectiveContext.ts +3 -3
- package/src/context/ProjectContext.ts +15 -0
- package/src/context/ResourceProvider.test.tsx +72 -90
- package/src/context/ResourceProvider.tsx +59 -93
- package/src/context/SDKStudioContext.test.tsx +28 -33
- package/src/context/SanityInstanceProvider.test.tsx +100 -0
- package/src/context/SanityInstanceProvider.tsx +71 -0
- package/src/context/WorkbenchTokenRefresh.test.tsx +106 -0
- package/src/context/WorkbenchTokenRefresh.tsx +61 -0
- package/src/context/renderSanityApp.test.tsx +151 -49
- package/src/context/renderSanityApp.tsx +12 -8
- package/src/context/workbenchToken.ts +63 -0
- package/src/hooks/access/useCheckPermissions.test-d.ts +15 -0
- package/src/hooks/access/useCheckPermissions.test.tsx +53 -0
- package/src/hooks/access/useCheckPermissions.ts +24 -0
- package/src/hooks/agent/agentActions.test.tsx +1 -1
- package/src/hooks/agent/agentActions.ts +51 -71
- package/src/hooks/applications/useApplication.test-d.ts +30 -0
- package/src/hooks/applications/useApplication.ts +22 -0
- package/src/hooks/applications/useApplications.test-d.ts +31 -0
- package/src/hooks/applications/useApplications.ts +25 -0
- package/src/hooks/applications/useCreateUserApplication.test-d.ts +14 -0
- package/src/hooks/applications/useCreateUserApplication.ts +11 -0
- package/src/hooks/applications/useDeleteApplication.test-d.ts +12 -0
- package/src/hooks/applications/useDeleteApplication.ts +11 -0
- package/src/hooks/applications/useDeleteUserApplication.test-d.ts +14 -0
- package/src/hooks/applications/useDeleteUserApplication.ts +11 -0
- package/src/hooks/applications/useUpdateApplication.test-d.ts +12 -0
- package/src/hooks/applications/useUpdateApplication.ts +11 -0
- package/src/hooks/applications/useUpdateUserApplication.test-d.ts +14 -0
- package/src/hooks/applications/useUpdateUserApplication.ts +11 -0
- package/src/hooks/applications/useUserApplication.test-d.ts +11 -0
- package/src/hooks/applications/useUserApplication.ts +14 -0
- package/src/hooks/applications/useUserApplications.test-d.ts +11 -0
- package/src/hooks/applications/useUserApplications.ts +14 -0
- package/src/hooks/auth/useDashboardOrganizationId.test.tsx +2 -8
- package/src/hooks/auth/useVerifyOrgProjects.test.tsx +8 -32
- package/src/hooks/auth/useVerifyOrgProjects.tsx +13 -6
- package/src/hooks/client/useClient.test.tsx +1 -4
- package/src/hooks/client/useClient.ts +1 -0
- package/src/hooks/context/useResource.test.tsx +32 -0
- package/src/hooks/context/useResource.ts +24 -0
- package/src/hooks/context/useSanityInstance.test.tsx +72 -3
- package/src/hooks/context/useSanityInstance.ts +30 -8
- package/src/hooks/dashboard/useManageFavorite.test.tsx +110 -46
- package/src/hooks/dashboard/useManageFavorite.ts +10 -16
- package/src/hooks/dashboard/useNavigateToStudioDocument.test.ts +5 -7
- package/src/hooks/dashboard/useNavigateToStudioDocument.ts +2 -6
- package/src/hooks/dashboard/useRecordDocumentHistoryEvent.test.ts +0 -2
- package/src/hooks/dashboard/useRecordDocumentHistoryEvent.ts +1 -2
- package/src/hooks/dashboard/useWindowTitle.test.ts +244 -0
- package/src/hooks/dashboard/useWindowTitle.ts +114 -0
- package/src/hooks/datasets/useDatasets.test.tsx +116 -0
- package/src/hooks/datasets/useDatasets.ts +33 -13
- package/src/hooks/document/useApplyDocumentActions.test.tsx +48 -0
- package/src/hooks/document/useApplyDocumentActions.ts +66 -48
- package/src/hooks/document/useCreateDocument.test.tsx +83 -0
- package/src/hooks/document/useCreateDocument.ts +117 -0
- package/src/hooks/document/useDocument.test.tsx +37 -8
- package/src/hooks/document/useDocument.ts +131 -66
- package/src/hooks/document/useDocumentEvent.test.tsx +16 -4
- package/src/hooks/document/useDocumentEvent.ts +16 -10
- package/src/hooks/document/useDocumentPermissions.test.tsx +8 -7
- package/src/hooks/document/useDocumentPermissions.ts +71 -39
- package/src/hooks/document/useDocumentSyncStatus.test.ts +1 -0
- package/src/hooks/document/useDocumentSyncStatus.ts +13 -9
- package/src/hooks/document/useEditDocument.test.tsx +21 -14
- package/src/hooks/document/useEditDocument.ts +163 -17
- package/src/hooks/documents/useDocuments.test.tsx +64 -44
- package/src/hooks/documents/useDocuments.ts +36 -20
- package/src/hooks/helpers/createCallbackHook.test.tsx +13 -19
- package/src/hooks/helpers/createFetcherHook.test.tsx +180 -0
- package/src/hooks/helpers/createFetcherHook.ts +69 -0
- package/src/hooks/helpers/createMutationHook.test.tsx +125 -0
- package/src/hooks/helpers/createMutationHook.tsx +93 -0
- package/src/hooks/helpers/createStateSourceHook.test.tsx +10 -10
- package/src/hooks/helpers/createStateSourceHook.tsx +2 -1
- package/src/hooks/{document/useApplyDocumentActions.test.ts → helpers/useApplyActions.test.tsx} +92 -53
- package/src/hooks/helpers/useApplyActions.ts +68 -0
- package/src/hooks/helpers/useNormalizedResourceOptions.test.tsx +253 -0
- package/src/hooks/helpers/useNormalizedResourceOptions.ts +92 -50
- package/src/hooks/helpers/useResolvedProjectId.test.tsx +59 -0
- package/src/hooks/helpers/useResolvedProjectId.ts +35 -0
- package/src/hooks/helpers/useTrackHookUsage.ts +37 -0
- package/src/hooks/installations/useInstallation.test-d.ts +19 -0
- package/src/hooks/installations/useInstallation.ts +22 -0
- package/src/hooks/installations/useInstallations.test-d.ts +26 -0
- package/src/hooks/installations/useInstallations.ts +25 -0
- package/src/hooks/organizations/useOrganization.test-d.ts +60 -0
- package/src/hooks/organizations/useOrganization.test.ts +63 -0
- package/src/hooks/organizations/useOrganization.ts +34 -0
- package/src/hooks/organizations/useOrganizations.test-d.ts +68 -0
- package/src/hooks/organizations/useOrganizations.test.ts +63 -0
- package/src/hooks/organizations/useOrganizations.ts +39 -0
- package/src/hooks/paginatedDocuments/usePaginatedDocuments.test.tsx +34 -27
- package/src/hooks/paginatedDocuments/usePaginatedDocuments.ts +40 -22
- package/src/hooks/presence/usePresence.test.tsx +0 -15
- package/src/hooks/presence/usePresence.ts +9 -20
- package/src/hooks/preview/useDocumentPreview.test.tsx +0 -1
- package/src/hooks/preview/useDocumentPreview.tsx +10 -8
- package/src/hooks/projection/useDocumentProjection.test.tsx +78 -9
- package/src/hooks/projection/useDocumentProjection.ts +81 -2
- package/src/hooks/projects/useProject.test-d.ts +56 -0
- package/src/hooks/projects/useProject.test.tsx +120 -0
- package/src/hooks/projects/useProject.ts +42 -43
- package/src/hooks/projects/useProjects.test-d.ts +60 -0
- package/src/hooks/projects/useProjects.test.ts +45 -98
- package/src/hooks/projects/useProjects.ts +19 -26
- package/src/hooks/query/useQuery.test.tsx +6 -5
- package/src/hooks/query/useQuery.ts +91 -9
- package/src/hooks/releases/useActiveReleases.test.tsx +13 -7
- package/src/hooks/releases/useActiveReleases.ts +28 -24
- package/src/hooks/releases/useAllReleases.test.tsx +93 -0
- package/src/hooks/releases/useAllReleases.ts +62 -0
- package/src/hooks/releases/useApplyReleaseActions.test.tsx +66 -0
- package/src/hooks/releases/useApplyReleaseActions.ts +82 -0
- package/src/hooks/releases/usePerspective.test.tsx +18 -10
- package/src/hooks/releases/usePerspective.ts +13 -17
- package/src/hooks/users/useUser.test.tsx +3 -9
- package/src/hooks/users/useUser.ts +2 -0
- package/src/hooks/users/useUsers.test.tsx +103 -7
- package/src/hooks/users/useUsers.ts +37 -3
- package/src/utils/resolveOrgResources.test.ts +111 -0
- package/src/utils/resolveOrgResources.ts +69 -0
- package/src/hooks/context/useDefaultResource.test.tsx +0 -25
- package/src/hooks/context/useDefaultResource.ts +0 -30
- package/src/hooks/dashboard/useDispatchIntent.test.ts +0 -254
- package/src/hooks/dashboard/useDispatchIntent.ts +0 -157
- package/src/hooks/dashboard/utils/useResourceIdFromDocumentHandle.test.ts +0 -107
- package/src/hooks/dashboard/utils/useResourceIdFromDocumentHandle.ts +0 -39
- package/src/hooks/datasets/useDatasets.test.ts +0 -66
- package/src/hooks/helpers/useNormalizedResourceOptions.test.ts +0 -65
- package/src/hooks/projects/useProject.test.ts +0 -79
|
@@ -23,39 +23,35 @@ import {useNormalizedResourceOptions} from '../helpers/useNormalizedResourceOpti
|
|
|
23
23
|
* @example
|
|
24
24
|
* ```tsx
|
|
25
25
|
* import {usePerspective, useQuery} from '@sanity/sdk-react'
|
|
26
|
-
|
|
27
|
-
* const perspective = usePerspective({
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* })
|
|
31
|
-
* const {data} = useQuery<Movie[]>({
|
|
32
|
-
* query: '*[_type == "movie"]',
|
|
33
|
-
* perspective,
|
|
26
|
+
|
|
27
|
+
* const perspective = usePerspective({perspective: 'rxg1346', projectId: 'abc123', dataset: 'production'})
|
|
28
|
+
* const {data} = useQuery<Movie[]>('*[_type == "movie"]', {
|
|
29
|
+
* perspective: perspective,
|
|
34
30
|
* })
|
|
35
31
|
* ```
|
|
36
32
|
*
|
|
37
33
|
* @returns The perspective for the given perspective handle.
|
|
38
34
|
*/
|
|
39
|
-
type
|
|
40
|
-
(perspectiveHandle?:
|
|
35
|
+
type UsePerspectiveValue = {
|
|
36
|
+
(perspectiveHandle: {resource?: DocumentResource}): string | string[]
|
|
41
37
|
}
|
|
42
38
|
|
|
43
|
-
const usePerspectiveValue = createStateSourceHook({
|
|
39
|
+
const usePerspectiveValue: UsePerspectiveValue = createStateSourceHook({
|
|
44
40
|
getState: getPerspectiveState as (
|
|
45
41
|
instance: SanityInstance,
|
|
46
|
-
perspectiveHandle
|
|
42
|
+
perspectiveHandle?: {resource?: DocumentResource},
|
|
47
43
|
) => StateSource<string | string[]>,
|
|
48
|
-
shouldSuspend: (instance: SanityInstance, options: {resource
|
|
44
|
+
shouldSuspend: (instance: SanityInstance, options: {resource?: DocumentResource}): boolean =>
|
|
49
45
|
getPerspectiveState(instance, options).getCurrent() === undefined,
|
|
50
|
-
suspender: (instance: SanityInstance, _options?: {resource
|
|
51
|
-
firstValueFrom(getPerspectiveState(instance, _options).observable.pipe(filter(Boolean))),
|
|
46
|
+
suspender: (instance: SanityInstance, _options?: {resource?: DocumentResource}) =>
|
|
47
|
+
firstValueFrom(getPerspectiveState(instance, _options ?? {}).observable.pipe(filter(Boolean))),
|
|
52
48
|
})
|
|
53
49
|
|
|
54
50
|
/**
|
|
55
51
|
* @public
|
|
56
52
|
* @function
|
|
57
53
|
*/
|
|
58
|
-
export
|
|
59
|
-
const normalizedOptions = useNormalizedResourceOptions(
|
|
54
|
+
export function usePerspective(perspectiveHandle?: ResourceHandle): string | string[] {
|
|
55
|
+
const normalizedOptions = useNormalizedResourceOptions(perspectiveHandle ?? {})
|
|
60
56
|
return usePerspectiveValue(normalizedOptions)
|
|
61
57
|
}
|
|
@@ -88,7 +88,7 @@ describe('useUser', () => {
|
|
|
88
88
|
}
|
|
89
89
|
|
|
90
90
|
render(
|
|
91
|
-
<ResourceProvider
|
|
91
|
+
<ResourceProvider projectId="p" fallback={null}>
|
|
92
92
|
<TestComponent />
|
|
93
93
|
</ResourceProvider>,
|
|
94
94
|
)
|
|
@@ -340,10 +340,7 @@ describe('useUser', () => {
|
|
|
340
340
|
}
|
|
341
341
|
|
|
342
342
|
render(
|
|
343
|
-
<ResourceProvider
|
|
344
|
-
resource={{projectId: 'test-project', dataset: 'production'}}
|
|
345
|
-
fallback={null}
|
|
346
|
-
>
|
|
343
|
+
<ResourceProvider projectId="test-project" fallback={null}>
|
|
347
344
|
<TestComponent />
|
|
348
345
|
</ResourceProvider>,
|
|
349
346
|
)
|
|
@@ -390,10 +387,7 @@ describe('useUser', () => {
|
|
|
390
387
|
}
|
|
391
388
|
|
|
392
389
|
render(
|
|
393
|
-
<ResourceProvider
|
|
394
|
-
resource={{projectId: 'test-project', dataset: 'production'}}
|
|
395
|
-
fallback={null}
|
|
396
|
-
>
|
|
390
|
+
<ResourceProvider projectId="test-project" fallback={null}>
|
|
397
391
|
<WrapperComponent />
|
|
398
392
|
</ResourceProvider>,
|
|
399
393
|
)
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
import {useEffect, useMemo, useState, useSyncExternalStore, useTransition} from 'react'
|
|
10
10
|
|
|
11
11
|
import {useSanityInstance} from '../context/useSanityInstance'
|
|
12
|
+
import {trackHookUsage} from '../helpers/useTrackHookUsage'
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* @public
|
|
@@ -59,6 +60,7 @@ export interface UserResult {
|
|
|
59
60
|
*/
|
|
60
61
|
export function useUser(options: GetUserOptions): UserResult {
|
|
61
62
|
const instance = useSanityInstance()
|
|
63
|
+
trackHookUsage(instance, 'useUser')
|
|
62
64
|
// Use React's useTransition to avoid UI jank when user options change
|
|
63
65
|
const [isPending, startTransition] = useTransition()
|
|
64
66
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
+
createSanityInstance,
|
|
2
3
|
getUsersState,
|
|
3
4
|
loadMoreUsers,
|
|
4
5
|
resolveUsers,
|
|
@@ -6,12 +7,14 @@ import {
|
|
|
6
7
|
type StateSource,
|
|
7
8
|
type UserProfile,
|
|
8
9
|
} from '@sanity/sdk'
|
|
9
|
-
import {act, fireEvent, render, screen} from '@testing-library/react'
|
|
10
|
-
import {useState} from 'react'
|
|
10
|
+
import {act, fireEvent, render, renderHook, screen} from '@testing-library/react'
|
|
11
|
+
import {type ReactNode, useState} from 'react'
|
|
11
12
|
import {type Observable, Subject} from 'rxjs'
|
|
12
13
|
import {describe, expect, it, vi} from 'vitest'
|
|
13
14
|
|
|
14
15
|
import {ResourceProvider} from '../../context/ResourceProvider'
|
|
16
|
+
import {SanityInstanceContext} from '../../context/SanityInstanceContext'
|
|
17
|
+
import {useSanityInstance} from '../context/useSanityInstance'
|
|
15
18
|
import {useUsers} from './useUsers'
|
|
16
19
|
|
|
17
20
|
// Mock the functions from '@sanity/sdk'
|
|
@@ -313,10 +316,7 @@ describe('useUsers', () => {
|
|
|
313
316
|
}
|
|
314
317
|
|
|
315
318
|
render(
|
|
316
|
-
<ResourceProvider
|
|
317
|
-
resource={{projectId: 'p', dataset: 'production'}}
|
|
318
|
-
fallback={<div data-testid="fallback">Loading...</div>}
|
|
319
|
-
>
|
|
319
|
+
<ResourceProvider projectId="p" fallback={<div data-testid="fallback">Loading...</div>}>
|
|
320
320
|
<TestComponent />
|
|
321
321
|
</ResourceProvider>,
|
|
322
322
|
)
|
|
@@ -330,7 +330,7 @@ describe('useUsers', () => {
|
|
|
330
330
|
|
|
331
331
|
// Verify that loadMoreUsers was called with the correct arguments
|
|
332
332
|
expect(loadMoreUsers).toHaveBeenCalledWith(
|
|
333
|
-
expect.objectContaining({
|
|
333
|
+
expect.objectContaining({config: {projectId: 'p'}}),
|
|
334
334
|
{
|
|
335
335
|
resourceType: 'organization',
|
|
336
336
|
organizationId: 'test-org',
|
|
@@ -338,4 +338,100 @@ describe('useUsers', () => {
|
|
|
338
338
|
},
|
|
339
339
|
)
|
|
340
340
|
})
|
|
341
|
+
|
|
342
|
+
it('resolves the projectId from the default resource for a project-scoped query', () => {
|
|
343
|
+
vi.mocked(getUsersState).mockReturnValue({
|
|
344
|
+
getCurrent: vi.fn().mockReturnValue({data: mockUsers, hasMore: false, totalCount: 2}),
|
|
345
|
+
subscribe: vi.fn(),
|
|
346
|
+
get observable(): Observable<unknown> {
|
|
347
|
+
throw new Error('Not implemented')
|
|
348
|
+
},
|
|
349
|
+
} as unknown as StateSource<
|
|
350
|
+
{data: SanityUser[]; totalCount: number; hasMore: boolean} | undefined
|
|
351
|
+
>)
|
|
352
|
+
|
|
353
|
+
const {
|
|
354
|
+
result: {current: instance},
|
|
355
|
+
} = renderHook(
|
|
356
|
+
() => {
|
|
357
|
+
useUsers({resourceType: 'project'})
|
|
358
|
+
return useSanityInstance()
|
|
359
|
+
},
|
|
360
|
+
{
|
|
361
|
+
wrapper: ({children}: {children: ReactNode}) => (
|
|
362
|
+
<ResourceProvider
|
|
363
|
+
resource={{projectId: 'resource-project', dataset: 'production'}}
|
|
364
|
+
fallback={null}
|
|
365
|
+
>
|
|
366
|
+
{children}
|
|
367
|
+
</ResourceProvider>
|
|
368
|
+
),
|
|
369
|
+
},
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
expect(getUsersState).toHaveBeenLastCalledWith(
|
|
373
|
+
instance,
|
|
374
|
+
expect.objectContaining({projectId: 'resource-project', resourceType: 'project'}),
|
|
375
|
+
)
|
|
376
|
+
})
|
|
377
|
+
|
|
378
|
+
it('does not inject a projectId for an organization-scoped query', () => {
|
|
379
|
+
vi.mocked(getUsersState).mockReturnValue({
|
|
380
|
+
getCurrent: vi.fn().mockReturnValue({data: mockUsers, hasMore: false, totalCount: 2}),
|
|
381
|
+
subscribe: vi.fn(),
|
|
382
|
+
get observable(): Observable<unknown> {
|
|
383
|
+
throw new Error('Not implemented')
|
|
384
|
+
},
|
|
385
|
+
} as unknown as StateSource<
|
|
386
|
+
{data: SanityUser[]; totalCount: number; hasMore: boolean} | undefined
|
|
387
|
+
>)
|
|
388
|
+
|
|
389
|
+
function TestComponent() {
|
|
390
|
+
useUsers({resourceType: 'organization', organizationId: 'test-org'})
|
|
391
|
+
return null
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
render(
|
|
395
|
+
<ResourceProvider
|
|
396
|
+
resource={{projectId: 'resource-project', dataset: 'production'}}
|
|
397
|
+
fallback={null}
|
|
398
|
+
>
|
|
399
|
+
<TestComponent />
|
|
400
|
+
</ResourceProvider>,
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
// Organization queries key off organizationId; the ambient project resource
|
|
404
|
+
// must not leak in as a projectId.
|
|
405
|
+
expect(vi.mocked(getUsersState).mock.lastCall?.[1]).not.toHaveProperty('projectId')
|
|
406
|
+
})
|
|
407
|
+
|
|
408
|
+
it('resolves a bare projectId from a ResourceProvider when the parent instance has no config', () => {
|
|
409
|
+
vi.mocked(getUsersState).mockReturnValue({
|
|
410
|
+
getCurrent: vi.fn().mockReturnValue({data: mockUsers, hasMore: false, totalCount: 2}),
|
|
411
|
+
subscribe: vi.fn(),
|
|
412
|
+
get observable(): Observable<unknown> {
|
|
413
|
+
throw new Error('Not implemented')
|
|
414
|
+
},
|
|
415
|
+
} as unknown as StateSource<
|
|
416
|
+
{data: SanityUser[]; totalCount: number; hasMore: boolean} | undefined
|
|
417
|
+
>)
|
|
418
|
+
|
|
419
|
+
// An instance with no project/dataset config, then a
|
|
420
|
+
// projectId-only ResourceProvider.
|
|
421
|
+
const emptyInstance = createSanityInstance({})
|
|
422
|
+
renderHook(() => useUsers(), {
|
|
423
|
+
wrapper: ({children}: {children: ReactNode}) => (
|
|
424
|
+
<SanityInstanceContext.Provider value={emptyInstance}>
|
|
425
|
+
<ResourceProvider projectId="bare-project" fallback={null}>
|
|
426
|
+
{children}
|
|
427
|
+
</ResourceProvider>
|
|
428
|
+
</SanityInstanceContext.Provider>
|
|
429
|
+
),
|
|
430
|
+
})
|
|
431
|
+
|
|
432
|
+
expect(getUsersState).toHaveBeenLastCalledWith(
|
|
433
|
+
emptyInstance,
|
|
434
|
+
expect.objectContaining({projectId: 'bare-project'}),
|
|
435
|
+
)
|
|
436
|
+
})
|
|
341
437
|
})
|
|
@@ -10,6 +10,8 @@ import {
|
|
|
10
10
|
import {useCallback, useEffect, useMemo, useState, useSyncExternalStore, useTransition} from 'react'
|
|
11
11
|
|
|
12
12
|
import {useSanityInstance} from '../context/useSanityInstance'
|
|
13
|
+
import {useResolvedProjectId} from '../helpers/useResolvedProjectId'
|
|
14
|
+
import {trackHookUsage} from '../helpers/useTrackHookUsage'
|
|
13
15
|
|
|
14
16
|
/**
|
|
15
17
|
* @public
|
|
@@ -35,6 +37,23 @@ export interface UsersResult {
|
|
|
35
37
|
loadMore: () => void
|
|
36
38
|
}
|
|
37
39
|
|
|
40
|
+
/**
|
|
41
|
+
* Injects a resolved `projectId` into project-scoped users options. A missing
|
|
42
|
+
* `projectId` is a no-op, and organization-scoped queries (explicit
|
|
43
|
+
* `resourceType`/`organizationId`) or options that already carry a `projectId`
|
|
44
|
+
* are returned unchanged.
|
|
45
|
+
*/
|
|
46
|
+
function withResolvedProjectId(
|
|
47
|
+
options: GetUsersOptions | undefined,
|
|
48
|
+
projectId: string | undefined,
|
|
49
|
+
): GetUsersOptions | undefined {
|
|
50
|
+
if (!projectId) return options
|
|
51
|
+
if (!options) return {projectId}
|
|
52
|
+
const isOrgScoped = options.resourceType === 'organization' || !!options.organizationId
|
|
53
|
+
if (isOrgScoped || options.projectId) return options
|
|
54
|
+
return {...options, projectId}
|
|
55
|
+
}
|
|
56
|
+
|
|
38
57
|
/**
|
|
39
58
|
*
|
|
40
59
|
* @public
|
|
@@ -66,14 +85,29 @@ export interface UsersResult {
|
|
|
66
85
|
* </div>
|
|
67
86
|
* )
|
|
68
87
|
* ```
|
|
88
|
+
* @remarks
|
|
89
|
+
* For project-scoped queries the `projectId` is resolved in order from:
|
|
90
|
+
* 1. an explicit `projectId` option
|
|
91
|
+
* 2. A legacy ProjectContext (e.g. a `<ResourceProvider projectId="…">` with no dataset), then
|
|
92
|
+
* 3. The active resource (`ResourceProvider`/`SDKProvider`)
|
|
93
|
+
* 4. `instance.config`.
|
|
69
94
|
*/
|
|
70
95
|
export function useUsers(options?: GetUsersOptions): UsersResult {
|
|
71
96
|
const instance = useSanityInstance()
|
|
97
|
+
trackHookUsage(instance, 'useUsers')
|
|
72
98
|
// Use React's useTransition to avoid UI jank when user options change
|
|
73
99
|
const [isPending, startTransition] = useTransition()
|
|
74
100
|
|
|
101
|
+
// Resolve the projectId from the ambient project/resource context so a
|
|
102
|
+
// project-scoped users request can pick it up rather than the top-level config.
|
|
103
|
+
const resolvedProjectId = useResolvedProjectId(options)
|
|
104
|
+
const effectiveOptions = useMemo(
|
|
105
|
+
() => withResolvedProjectId(options, resolvedProjectId),
|
|
106
|
+
[options, resolvedProjectId],
|
|
107
|
+
)
|
|
108
|
+
|
|
75
109
|
// Get the unique key for this users request and its options
|
|
76
|
-
const key = getUsersKey(instance,
|
|
110
|
+
const key = getUsersKey(instance, effectiveOptions)
|
|
77
111
|
// Use a deferred state to avoid immediate re-renders when the users request changes
|
|
78
112
|
const [deferredKey, setDeferredKey] = useState(key)
|
|
79
113
|
// Parse the deferred users key back into users options
|
|
@@ -113,8 +147,8 @@ export function useUsers(options?: GetUsersOptions): UsersResult {
|
|
|
113
147
|
const {data, hasMore} = useSyncExternalStore(subscribe, getCurrent)!
|
|
114
148
|
|
|
115
149
|
const loadMore = useCallback(() => {
|
|
116
|
-
loadMoreUsers(instance,
|
|
117
|
-
}, [instance,
|
|
150
|
+
loadMoreUsers(instance, effectiveOptions)
|
|
151
|
+
}, [instance, effectiveOptions])
|
|
118
152
|
|
|
119
153
|
return {data, hasMore, isPending, loadMore}
|
|
120
154
|
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import {type SanityClient} from '@sanity/client'
|
|
2
|
+
import {getClientState, type SanityInstance, type StateSource} from '@sanity/sdk'
|
|
3
|
+
import {firstValueFrom} from 'rxjs'
|
|
4
|
+
import {beforeEach, describe, expect, it, vi} from 'vitest'
|
|
5
|
+
|
|
6
|
+
import {resolveOrgResources} from './resolveOrgResources'
|
|
7
|
+
|
|
8
|
+
vi.mock('@sanity/sdk', () => ({
|
|
9
|
+
getClientState: vi.fn(),
|
|
10
|
+
}))
|
|
11
|
+
|
|
12
|
+
vi.mock('rxjs', () => ({
|
|
13
|
+
firstValueFrom: vi.fn(),
|
|
14
|
+
}))
|
|
15
|
+
|
|
16
|
+
const mockGetClientState = vi.mocked(getClientState)
|
|
17
|
+
const mockFirstValueFrom = vi.mocked(firstValueFrom)
|
|
18
|
+
|
|
19
|
+
const mockRequest = vi.fn()
|
|
20
|
+
const mockClient = {request: mockRequest}
|
|
21
|
+
const mockInstance = {} as SanityInstance
|
|
22
|
+
|
|
23
|
+
beforeEach(() => {
|
|
24
|
+
vi.clearAllMocks()
|
|
25
|
+
mockGetClientState.mockReturnValue({
|
|
26
|
+
observable: 'mock-observable',
|
|
27
|
+
} as unknown as StateSource<SanityClient>)
|
|
28
|
+
mockFirstValueFrom.mockResolvedValue(mockClient as unknown as SanityClient)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
const ORG_ID = 'org-1'
|
|
32
|
+
|
|
33
|
+
describe('resolveOrgResources', () => {
|
|
34
|
+
it('returns both mediaLibrary and canvas when both requests succeed', async () => {
|
|
35
|
+
mockRequest
|
|
36
|
+
.mockResolvedValueOnce({data: [{id: 'ml-123'}]})
|
|
37
|
+
.mockResolvedValueOnce({data: [{id: 'canvas-456'}]})
|
|
38
|
+
|
|
39
|
+
const result = await resolveOrgResources(mockInstance, ORG_ID)
|
|
40
|
+
|
|
41
|
+
expect(result).toEqual({
|
|
42
|
+
mediaLibrary: {mediaLibraryId: 'ml-123'},
|
|
43
|
+
canvas: {canvasId: 'canvas-456'},
|
|
44
|
+
})
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('scopes both requests to the organization', async () => {
|
|
48
|
+
mockRequest
|
|
49
|
+
.mockResolvedValueOnce({data: [{id: 'ml-123'}]})
|
|
50
|
+
.mockResolvedValueOnce({data: [{id: 'canvas-456'}]})
|
|
51
|
+
|
|
52
|
+
await resolveOrgResources(mockInstance, ORG_ID)
|
|
53
|
+
|
|
54
|
+
expect(mockRequest).toHaveBeenCalledWith(
|
|
55
|
+
expect.objectContaining({uri: '/media-libraries', query: {organizationId: ORG_ID}}),
|
|
56
|
+
)
|
|
57
|
+
expect(mockRequest).toHaveBeenCalledWith(
|
|
58
|
+
expect.objectContaining({uri: '/canvases', query: {organizationId: ORG_ID}}),
|
|
59
|
+
)
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('returns only mediaLibrary when canvas request fails', async () => {
|
|
63
|
+
mockRequest
|
|
64
|
+
.mockResolvedValueOnce({data: [{id: 'ml-123'}]})
|
|
65
|
+
.mockRejectedValueOnce(new Error('canvas not found'))
|
|
66
|
+
|
|
67
|
+
const result = await resolveOrgResources(mockInstance, ORG_ID)
|
|
68
|
+
|
|
69
|
+
expect(result).toEqual({
|
|
70
|
+
mediaLibrary: {mediaLibraryId: 'ml-123'},
|
|
71
|
+
canvas: undefined,
|
|
72
|
+
})
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('returns only canvas when mediaLibrary request fails', async () => {
|
|
76
|
+
mockRequest
|
|
77
|
+
.mockRejectedValueOnce(new Error('media library not found'))
|
|
78
|
+
.mockResolvedValueOnce({data: [{id: 'canvas-456'}]})
|
|
79
|
+
|
|
80
|
+
const result = await resolveOrgResources(mockInstance, ORG_ID)
|
|
81
|
+
|
|
82
|
+
expect(result).toEqual({
|
|
83
|
+
mediaLibrary: undefined,
|
|
84
|
+
canvas: {canvasId: 'canvas-456'},
|
|
85
|
+
})
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('returns undefined for both when both requests fail', async () => {
|
|
89
|
+
mockRequest
|
|
90
|
+
.mockRejectedValueOnce(new Error('error 1'))
|
|
91
|
+
.mockRejectedValueOnce(new Error('error 2'))
|
|
92
|
+
|
|
93
|
+
const result = await resolveOrgResources(mockInstance, ORG_ID)
|
|
94
|
+
|
|
95
|
+
expect(result).toEqual({
|
|
96
|
+
mediaLibrary: undefined,
|
|
97
|
+
canvas: undefined,
|
|
98
|
+
})
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it('returns undefined for both when data arrays are empty', async () => {
|
|
102
|
+
mockRequest.mockResolvedValueOnce({data: []}).mockResolvedValueOnce({data: []})
|
|
103
|
+
|
|
104
|
+
const result = await resolveOrgResources(mockInstance, ORG_ID)
|
|
105
|
+
|
|
106
|
+
expect(result).toEqual({
|
|
107
|
+
mediaLibrary: undefined,
|
|
108
|
+
canvas: undefined,
|
|
109
|
+
})
|
|
110
|
+
})
|
|
111
|
+
})
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type CanvasResource,
|
|
3
|
+
getClientState,
|
|
4
|
+
type MediaLibraryResource,
|
|
5
|
+
type SanityInstance,
|
|
6
|
+
} from '@sanity/sdk'
|
|
7
|
+
import {firstValueFrom} from 'rxjs'
|
|
8
|
+
|
|
9
|
+
const API_VERSION = 'v2026-07-09'
|
|
10
|
+
|
|
11
|
+
interface OrgResourcesApiItem {
|
|
12
|
+
id: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface OrgResourcesApiResponse {
|
|
16
|
+
data: OrgResourcesApiItem[]
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface OrgResources {
|
|
20
|
+
mediaLibrary?: MediaLibraryResource
|
|
21
|
+
canvas?: CanvasResource
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Gets the id of the first resource from a settled request result. The request
|
|
26
|
+
* is already scoped to a single organization, so the first item is the one we want.
|
|
27
|
+
*/
|
|
28
|
+
function getFirstResourceId(
|
|
29
|
+
result: PromiseSettledResult<OrgResourcesApiResponse>,
|
|
30
|
+
): string | undefined {
|
|
31
|
+
if (result.status !== 'fulfilled') return undefined
|
|
32
|
+
return result.value.data?.[0]?.id
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Fetches the media library and canvas resources for the given organization.
|
|
37
|
+
* Both requests are scoped to `organizationId` so each response contains only
|
|
38
|
+
* that org's resources. Each resource is fetched independently — a failure for
|
|
39
|
+
* one does not prevent the other from resolving.
|
|
40
|
+
*/
|
|
41
|
+
export async function resolveOrgResources(
|
|
42
|
+
instance: SanityInstance,
|
|
43
|
+
organizationId: string,
|
|
44
|
+
): Promise<OrgResources> {
|
|
45
|
+
const client = await firstValueFrom(
|
|
46
|
+
getClientState(instance, {apiVersion: API_VERSION, scope: 'global'}).observable,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
const [mediaLibrariesResult, canvasesResult] = await Promise.allSettled([
|
|
50
|
+
client.request<OrgResourcesApiResponse>({
|
|
51
|
+
uri: `/media-libraries`,
|
|
52
|
+
query: {organizationId},
|
|
53
|
+
tag: 'org-resources.media-libraries',
|
|
54
|
+
}),
|
|
55
|
+
client.request<OrgResourcesApiResponse>({
|
|
56
|
+
uri: `/canvases`,
|
|
57
|
+
query: {organizationId},
|
|
58
|
+
tag: 'org-resources.canvases',
|
|
59
|
+
}),
|
|
60
|
+
])
|
|
61
|
+
|
|
62
|
+
const mediaLibraryId = getFirstResourceId(mediaLibrariesResult)
|
|
63
|
+
const canvasId = getFirstResourceId(canvasesResult)
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
mediaLibrary: mediaLibraryId ? {mediaLibraryId} : undefined,
|
|
67
|
+
canvas: canvasId ? {canvasId} : undefined,
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import {renderHook} from '@testing-library/react'
|
|
2
|
-
import {describe, expect, it} from 'vitest'
|
|
3
|
-
|
|
4
|
-
import {ResourceContext} from '../../context/DefaultResourceContext'
|
|
5
|
-
import {useResource} from './useDefaultResource'
|
|
6
|
-
|
|
7
|
-
describe('useResource', () => {
|
|
8
|
-
it('returns the resource from context', () => {
|
|
9
|
-
const resource = {projectId: 'my-project', dataset: 'my-dataset'}
|
|
10
|
-
|
|
11
|
-
const {result} = renderHook(() => useResource(), {
|
|
12
|
-
wrapper: ({children}) => (
|
|
13
|
-
<ResourceContext.Provider value={resource}>{children}</ResourceContext.Provider>
|
|
14
|
-
),
|
|
15
|
-
})
|
|
16
|
-
|
|
17
|
-
expect(result.current).toBe(resource)
|
|
18
|
-
})
|
|
19
|
-
|
|
20
|
-
it('returns undefined when no resource is in context', () => {
|
|
21
|
-
const {result} = renderHook(() => useResource())
|
|
22
|
-
|
|
23
|
-
expect(result.current).toBeUndefined()
|
|
24
|
-
})
|
|
25
|
-
})
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import {type DocumentResource} from '@sanity/sdk'
|
|
2
|
-
import {useContext} from 'react'
|
|
3
|
-
|
|
4
|
-
import {ResourceContext} from '../../context/DefaultResourceContext'
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Returns the current {@link DocumentResource} from context.
|
|
8
|
-
*
|
|
9
|
-
* @public
|
|
10
|
-
*
|
|
11
|
-
* @category Platform
|
|
12
|
-
* @returns The resource set by the nearest `ResourceProvider`, or `undefined`
|
|
13
|
-
*
|
|
14
|
-
* @remarks
|
|
15
|
-
* With the flat instance model, nested `ResourceProvider`s override the
|
|
16
|
-
* active resource via React context rather than creating child instances.
|
|
17
|
-
* Use this hook when you need the current project/dataset/resource for a
|
|
18
|
-
* subtree instead of reading from `useSanityInstance().config`.
|
|
19
|
-
*
|
|
20
|
-
* @example Get the current resource
|
|
21
|
-
* ```tsx
|
|
22
|
-
* const resource = useResource()
|
|
23
|
-
* if (resource && 'projectId' in resource) {
|
|
24
|
-
* console.log(resource.projectId, resource.dataset)
|
|
25
|
-
* }
|
|
26
|
-
* ```
|
|
27
|
-
*/
|
|
28
|
-
export function useResource(): DocumentResource | undefined {
|
|
29
|
-
return useContext(ResourceContext)
|
|
30
|
-
}
|