@sanity/sdk-react 3.0.0-rc.0 → 3.0.0-rc.2
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 +2488 -2019
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4435 -1529
- package/dist/index.js.map +1 -1
- package/package.json +55 -59
- package/src/_exports/index.ts +2 -4
- package/src/_exports/sdk-react.ts +30 -18
- 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 +93 -87
- package/src/components/auth/AuthBoundary.recovery.test.tsx +86 -0
- package/src/components/auth/AuthBoundary.test.tsx +7 -7
- package/src/components/auth/AuthBoundary.tsx +29 -6
- package/src/components/auth/DashboardAccessRequest.tsx +37 -0
- package/src/components/auth/LoginError.test.tsx +191 -5
- package/src/components/auth/LoginError.tsx +104 -61
- 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/ComlinkTokenRefresh.tsx +3 -5
- 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 +93 -90
- package/src/context/ResourceProvider.tsx +62 -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 +2 -2
- package/src/hooks/agent/agentActions.ts +52 -72
- package/src/hooks/agent/useAgentResourceContext.ts +1 -1
- 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/useDeleteApplication.test-d.ts +12 -0
- package/src/hooks/applications/useDeleteApplication.ts +11 -0
- package/src/hooks/applications/useUpdateApplication.test-d.ts +12 -0
- package/src/hooks/applications/useUpdateApplication.ts +11 -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/comlink/useFrameConnection.test.tsx +3 -2
- package/src/hooks/comlink/useFrameConnection.ts +1 -1
- package/src/hooks/comlink/useWindowConnection.test.tsx +4 -3
- package/src/hooks/comlink/useWindowConnection.ts +2 -3
- package/src/hooks/comments/useCommentActions.test.tsx +150 -0
- package/src/hooks/comments/useCommentActions.ts +109 -0
- package/src/hooks/comments/useCommentList.ts +79 -0
- package/src/hooks/comments/useCommentThreads.test.tsx +107 -0
- package/src/hooks/comments/useCommentThreads.ts +73 -0
- package/src/hooks/comments/useComments.test.tsx +242 -0
- package/src/hooks/comments/useComments.ts +59 -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 +18 -1
- package/src/hooks/context/useSanityInstance.ts +6 -6
- package/src/hooks/dashboard/useManageFavorite.test.tsx +110 -46
- package/src/hooks/dashboard/useManageFavorite.ts +11 -17
- 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 +2 -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 +31 -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 +226 -0
- package/src/hooks/helpers/useNormalizedResourceOptions.ts +79 -48
- 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 +35 -22
- package/src/hooks/presence/usePresence.test.tsx +0 -15
- package/src/hooks/presence/usePresence.ts +22 -20
- package/src/hooks/presence/usePresenceForDocument.test.tsx +141 -0
- package/src/hooks/presence/usePresenceForDocument.ts +104 -0
- package/src/hooks/presence/useReportPresence.test.tsx +202 -0
- package/src/hooks/presence/useReportPresence.ts +170 -0
- package/src/hooks/preview/useDocumentPreview.test.tsx +0 -1
- package/src/hooks/preview/useDocumentPreview.tsx +12 -14
- 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 +17 -32
- package/src/hooks/query/useQuery.test.tsx +6 -5
- package/src/hooks/query/useQuery.ts +94 -17
- 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 +4 -8
- package/src/hooks/users/useUsers.test.tsx +103 -7
- package/src/hooks/users/useUsers.ts +38 -5
- 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
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type DatasetHandle,
|
|
3
|
+
type DocumentResource,
|
|
4
|
+
getAllReleasesState,
|
|
5
|
+
type ReleaseDocument,
|
|
6
|
+
type SanityInstance,
|
|
7
|
+
type StateSource,
|
|
8
|
+
} from '@sanity/sdk'
|
|
9
|
+
import {filter, firstValueFrom} from 'rxjs'
|
|
10
|
+
|
|
11
|
+
import {createStateSourceHook} from '../helpers/createStateSourceHook'
|
|
12
|
+
import {
|
|
13
|
+
useNormalizedResourceOptions,
|
|
14
|
+
type WithResourceNameSupport,
|
|
15
|
+
} from '../helpers/useNormalizedResourceOptions'
|
|
16
|
+
|
|
17
|
+
type UseAllReleasesValue = {
|
|
18
|
+
(options?: {resource?: DocumentResource}): ReleaseDocument[]
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const useAllReleasesValue: UseAllReleasesValue = createStateSourceHook({
|
|
22
|
+
getState: getAllReleasesState as (
|
|
23
|
+
instance: SanityInstance,
|
|
24
|
+
options?: {resource?: DocumentResource},
|
|
25
|
+
) => StateSource<ReleaseDocument[]>,
|
|
26
|
+
shouldSuspend: (instance: SanityInstance, options?: {resource?: DocumentResource}) =>
|
|
27
|
+
getAllReleasesState(instance, options ?? {}).getCurrent() === undefined,
|
|
28
|
+
suspender: (instance: SanityInstance, options?: {resource?: DocumentResource}) =>
|
|
29
|
+
firstValueFrom(getAllReleasesState(instance, options ?? {}).observable.pipe(filter(Boolean))),
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @public
|
|
34
|
+
* @function
|
|
35
|
+
*
|
|
36
|
+
* Returns every release the dataset has — including `archived`, `published`,
|
|
37
|
+
* and mid-transition states (`archiving`, `unarchiving`, `publishing`,
|
|
38
|
+
* `scheduling`).
|
|
39
|
+
*
|
|
40
|
+
* Use this hook when you're building a release-management UI (listing
|
|
41
|
+
* releases, surfacing lifecycle controls, etc.) so a release stays visible
|
|
42
|
+
* across its full lifecycle — including after it's been published or
|
|
43
|
+
* archived. For perspective / content queries, prefer
|
|
44
|
+
* {@link useActiveReleases}, which filters to releases that still affect
|
|
45
|
+
* what's queryable.
|
|
46
|
+
*
|
|
47
|
+
* @returns Every release for the current project, sorted to match the order
|
|
48
|
+
* used by {@link useActiveReleases}.
|
|
49
|
+
* @category Releases
|
|
50
|
+
* @example
|
|
51
|
+
* ```tsx
|
|
52
|
+
* import {useAllReleases} from '@sanity/sdk-react'
|
|
53
|
+
*
|
|
54
|
+
* const releases = useAllReleases()
|
|
55
|
+
* ```
|
|
56
|
+
*/
|
|
57
|
+
export function useAllReleases(
|
|
58
|
+
options?: WithResourceNameSupport<DatasetHandle> | undefined,
|
|
59
|
+
): ReleaseDocument[] {
|
|
60
|
+
const normalizedOptions = useNormalizedResourceOptions(options ?? {})
|
|
61
|
+
return useAllReleasesValue(normalizedOptions)
|
|
62
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import {applyDocumentActions, createSanityInstance} from '@sanity/sdk'
|
|
2
|
+
import {describe, it} from 'vitest'
|
|
3
|
+
|
|
4
|
+
import {renderHook} from '../../../test/test-utils'
|
|
5
|
+
import {useSanityInstance} from '../context/useSanityInstance'
|
|
6
|
+
import {useApplyReleaseActions} from './useApplyReleaseActions'
|
|
7
|
+
|
|
8
|
+
// Resource resolution, mismatch detection, and context fallback are covered
|
|
9
|
+
// by hooks/helpers/useApplyActions.test.tsx — both this hook and
|
|
10
|
+
// useApplyDocumentActions are typed wrappers over that shared implementation.
|
|
11
|
+
// These tests just verify the wrapper forwards release actions through and
|
|
12
|
+
// supports batching them in a single transaction.
|
|
13
|
+
|
|
14
|
+
vi.mock('@sanity/sdk', async (importOriginal) => {
|
|
15
|
+
const original = await importOriginal<typeof import('@sanity/sdk')>()
|
|
16
|
+
return {...original, applyDocumentActions: vi.fn()}
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
vi.mock('../context/useSanityInstance')
|
|
20
|
+
|
|
21
|
+
const instance = createSanityInstance({projectId: 'p', dataset: 'd'})
|
|
22
|
+
|
|
23
|
+
describe('useApplyReleaseActions', () => {
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
vi.resetAllMocks()
|
|
26
|
+
vi.mocked(useSanityInstance).mockReturnValueOnce(instance)
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('forwards a release action to applyDocumentActions with the resolved resource', () => {
|
|
30
|
+
const {result} = renderHook(() => useApplyReleaseActions())
|
|
31
|
+
result.current({type: 'release.create', releaseId: 'r1', metadata: {releaseType: 'asap'}})
|
|
32
|
+
|
|
33
|
+
expect(applyDocumentActions).toHaveBeenCalledExactlyOnceWith(instance, {
|
|
34
|
+
actions: [
|
|
35
|
+
{
|
|
36
|
+
type: 'release.create',
|
|
37
|
+
releaseId: 'r1',
|
|
38
|
+
metadata: {releaseType: 'asap'},
|
|
39
|
+
resource: {projectId: 'test', dataset: 'test'},
|
|
40
|
+
},
|
|
41
|
+
],
|
|
42
|
+
resource: {projectId: 'test', dataset: 'test'},
|
|
43
|
+
})
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('forwards an array of release actions as a single transaction', () => {
|
|
47
|
+
const {result} = renderHook(() => useApplyReleaseActions())
|
|
48
|
+
result.current([
|
|
49
|
+
{type: 'release.create', releaseId: 'r1', metadata: {releaseType: 'asap'}},
|
|
50
|
+
{type: 'release.publish', releaseId: 'r1'},
|
|
51
|
+
])
|
|
52
|
+
|
|
53
|
+
expect(applyDocumentActions).toHaveBeenCalledExactlyOnceWith(instance, {
|
|
54
|
+
actions: [
|
|
55
|
+
{
|
|
56
|
+
type: 'release.create',
|
|
57
|
+
releaseId: 'r1',
|
|
58
|
+
metadata: {releaseType: 'asap'},
|
|
59
|
+
resource: {projectId: 'test', dataset: 'test'},
|
|
60
|
+
},
|
|
61
|
+
{type: 'release.publish', releaseId: 'r1', resource: {projectId: 'test', dataset: 'test'}},
|
|
62
|
+
],
|
|
63
|
+
resource: {projectId: 'test', dataset: 'test'},
|
|
64
|
+
})
|
|
65
|
+
})
|
|
66
|
+
})
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import {type ActionsResult, type ReleaseAction} from '@sanity/sdk'
|
|
2
|
+
|
|
3
|
+
import {type ResourceHandle} from '../../config/handles'
|
|
4
|
+
import {useApplyActions} from '../helpers/useApplyActions'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @public
|
|
8
|
+
*/
|
|
9
|
+
interface UseApplyReleaseActions {
|
|
10
|
+
(): (action: ReleaseAction | ReleaseAction[], options?: ResourceHandle) => Promise<ActionsResult>
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @public
|
|
15
|
+
*
|
|
16
|
+
* Provides a stable callback function for applying one or more release actions.
|
|
17
|
+
*
|
|
18
|
+
* This hook wraps the core `applyDocumentActions` functionality from `@sanity/sdk`,
|
|
19
|
+
* integrating it with the React component lifecycle and {@link SanityInstance}.
|
|
20
|
+
* It accepts release-lifecycle actions generated by {@link createRelease},
|
|
21
|
+
* {@link editRelease}, {@link publishRelease}, {@link scheduleRelease},
|
|
22
|
+
* {@link unscheduleRelease}, {@link archiveRelease}, {@link unarchiveRelease},
|
|
23
|
+
* and {@link deleteRelease}.
|
|
24
|
+
*
|
|
25
|
+
* Note that actions submitted via this hook will cascade to the documents in the release.
|
|
26
|
+
* For example, if you create a release and then publish it, the documents in the release will be published.
|
|
27
|
+
* If you delete a published release, the version documents will be deleted.
|
|
28
|
+
*
|
|
29
|
+
* Features:
|
|
30
|
+
* - Applies one or multiple `ReleaseAction` objects.
|
|
31
|
+
* - Supports optimistic updates for create/edit/delete: local release state
|
|
32
|
+
* reflects changes immediately while in-flight.
|
|
33
|
+
* - Handles batching: multiple actions passed together are submitted as a
|
|
34
|
+
* single atomic transaction.
|
|
35
|
+
*
|
|
36
|
+
* Release actions cannot be combined with `liveEdit` document actions in the
|
|
37
|
+
* same transaction. Submit them as separate transactions if you need both.
|
|
38
|
+
*
|
|
39
|
+
* @category Releases
|
|
40
|
+
* @returns A stable callback. When called with a single `ReleaseAction` or an
|
|
41
|
+
* array of `ReleaseAction`s, it returns a promise that resolves to an
|
|
42
|
+
* {@link ActionsResult}.
|
|
43
|
+
*
|
|
44
|
+
* @example Create and schedule a release
|
|
45
|
+
* ```tsx
|
|
46
|
+
* import {
|
|
47
|
+
* createRelease,
|
|
48
|
+
* scheduleRelease,
|
|
49
|
+
* useApplyReleaseActions,
|
|
50
|
+
* type ReleaseHandle,
|
|
51
|
+
* } from '@sanity/sdk-react'
|
|
52
|
+
*
|
|
53
|
+
* function ScheduleReleaseButton({release}: {release: ReleaseHandle}) {
|
|
54
|
+
* const applyRelease = useApplyReleaseActions()
|
|
55
|
+
*
|
|
56
|
+
* const handleSchedule = () =>
|
|
57
|
+
* applyRelease([
|
|
58
|
+
* createRelease(release, {title: 'Summer drop', releaseType: 'asap'}),
|
|
59
|
+
* scheduleRelease(release, '2026-06-01T00:00:00Z'),
|
|
60
|
+
* ])
|
|
61
|
+
*
|
|
62
|
+
* return <button onClick={handleSchedule}>Schedule</button>
|
|
63
|
+
* }
|
|
64
|
+
* ```
|
|
65
|
+
*
|
|
66
|
+
* @example Publish a release
|
|
67
|
+
* ```tsx
|
|
68
|
+
* import {publishRelease, useApplyReleaseActions} from '@sanity/sdk-react'
|
|
69
|
+
*
|
|
70
|
+
* function PublishButton({releaseId}: {releaseId: string}) {
|
|
71
|
+
* const applyRelease = useApplyReleaseActions()
|
|
72
|
+
* return (
|
|
73
|
+
* <button onClick={() => applyRelease(publishRelease({releaseId}))}>
|
|
74
|
+
* Publish all documents in the release
|
|
75
|
+
* </button>
|
|
76
|
+
* )
|
|
77
|
+
* }
|
|
78
|
+
* ```
|
|
79
|
+
*/
|
|
80
|
+
export const useApplyReleaseActions: UseApplyReleaseActions = () => {
|
|
81
|
+
return useApplyActions() as ReturnType<UseApplyReleaseActions>
|
|
82
|
+
}
|
|
@@ -5,10 +5,11 @@ import {
|
|
|
5
5
|
type PerspectiveHandle,
|
|
6
6
|
type ReleaseDocument,
|
|
7
7
|
} from '@sanity/sdk'
|
|
8
|
+
import {renderHook} from '@testing-library/react'
|
|
8
9
|
import {BehaviorSubject} from 'rxjs'
|
|
9
10
|
import {describe, expect, it, vi} from 'vitest'
|
|
10
11
|
|
|
11
|
-
import {
|
|
12
|
+
import {ResourceProvider} from '../../context/ResourceProvider'
|
|
12
13
|
import {usePerspective} from './usePerspective'
|
|
13
14
|
|
|
14
15
|
// Mock the SDK functions
|
|
@@ -46,7 +47,7 @@ describe('usePerspective', () => {
|
|
|
46
47
|
// Mock the active releases observable for the suspender
|
|
47
48
|
const mockReleaseDoc: ReleaseDocument = {
|
|
48
49
|
_id: 'release1',
|
|
49
|
-
_type: 'release',
|
|
50
|
+
_type: 'system.release',
|
|
50
51
|
_createdAt: '2021-01-01T00:00:00Z',
|
|
51
52
|
_updatedAt: '2021-01-01T00:00:00Z',
|
|
52
53
|
_rev: 'rev1',
|
|
@@ -67,13 +68,18 @@ describe('usePerspective', () => {
|
|
|
67
68
|
vi.mocked(getPerspectiveState).mockReturnValue(mockStateSource)
|
|
68
69
|
vi.mocked(getActiveReleasesState).mockReturnValue(mockReleasesStateSource)
|
|
69
70
|
|
|
70
|
-
const {result} = renderHook(
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
71
|
+
const {result} = renderHook(
|
|
72
|
+
() => {
|
|
73
|
+
try {
|
|
74
|
+
return usePerspective(perspectiveHandle)
|
|
75
|
+
} catch (e) {
|
|
76
|
+
return e
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
wrapper: ({children}) => <ResourceProvider fallback={null}>{children}</ResourceProvider>,
|
|
81
|
+
},
|
|
82
|
+
)
|
|
77
83
|
|
|
78
84
|
// Verify that the hook threw a promise (suspended)
|
|
79
85
|
expect(result.current).toBeInstanceOf(Promise)
|
|
@@ -98,7 +104,9 @@ describe('usePerspective', () => {
|
|
|
98
104
|
|
|
99
105
|
vi.mocked(getPerspectiveState).mockReturnValue(mockStateSource)
|
|
100
106
|
|
|
101
|
-
const {result} = renderHook(() => usePerspective(perspectiveHandle)
|
|
107
|
+
const {result} = renderHook(() => usePerspective(perspectiveHandle), {
|
|
108
|
+
wrapper: ({children}) => <ResourceProvider fallback={null}>{children}</ResourceProvider>,
|
|
109
|
+
})
|
|
102
110
|
|
|
103
111
|
// Verify that the hook returned the perspective without suspending
|
|
104
112
|
expect(result.current).toEqual(mockPerspective)
|
|
@@ -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
|
)
|
|
@@ -1,14 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
getUsersKey,
|
|
4
|
-
getUsersState,
|
|
5
|
-
parseUsersKey,
|
|
6
|
-
resolveUsers,
|
|
7
|
-
type SanityUser,
|
|
8
|
-
} from '@sanity/sdk'
|
|
1
|
+
import {type GetUserOptions, getUsersState, resolveUsers, type SanityUser} from '@sanity/sdk'
|
|
2
|
+
import {getUsersKey, parseUsersKey} from '@sanity/sdk/_internal'
|
|
9
3
|
import {useEffect, useMemo, useState, useSyncExternalStore, useTransition} from 'react'
|
|
10
4
|
|
|
11
5
|
import {useSanityInstance} from '../context/useSanityInstance'
|
|
6
|
+
import {trackHookUsage} from '../helpers/useTrackHookUsage'
|
|
12
7
|
|
|
13
8
|
/**
|
|
14
9
|
* @public
|
|
@@ -59,6 +54,7 @@ export interface UserResult {
|
|
|
59
54
|
*/
|
|
60
55
|
export function useUser(options: GetUserOptions): UserResult {
|
|
61
56
|
const instance = useSanityInstance()
|
|
57
|
+
trackHookUsage(instance, 'useUser')
|
|
62
58
|
// Use React's useTransition to avoid UI jank when user options change
|
|
63
59
|
const [isPending, startTransition] = useTransition()
|
|
64
60
|
|
|
@@ -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
|
})
|
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
import {
|
|
2
|
-
getUsersKey,
|
|
3
2
|
type GetUsersOptions,
|
|
4
3
|
getUsersState,
|
|
5
4
|
loadMoreUsers,
|
|
6
|
-
parseUsersKey,
|
|
7
5
|
resolveUsers,
|
|
8
6
|
type SanityUser,
|
|
9
7
|
} from '@sanity/sdk'
|
|
8
|
+
import {getUsersKey, parseUsersKey} from '@sanity/sdk/_internal'
|
|
10
9
|
import {useCallback, useEffect, useMemo, useState, useSyncExternalStore, useTransition} from 'react'
|
|
11
10
|
|
|
12
11
|
import {useSanityInstance} from '../context/useSanityInstance'
|
|
12
|
+
import {useResolvedProjectId} from '../helpers/useResolvedProjectId'
|
|
13
|
+
import {trackHookUsage} from '../helpers/useTrackHookUsage'
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
16
|
* @public
|
|
@@ -35,6 +36,23 @@ export interface UsersResult {
|
|
|
35
36
|
loadMore: () => void
|
|
36
37
|
}
|
|
37
38
|
|
|
39
|
+
/**
|
|
40
|
+
* Injects a resolved `projectId` into project-scoped users options. A missing
|
|
41
|
+
* `projectId` is a no-op, and organization-scoped queries (explicit
|
|
42
|
+
* `resourceType`/`organizationId`) or options that already carry a `projectId`
|
|
43
|
+
* are returned unchanged.
|
|
44
|
+
*/
|
|
45
|
+
function withResolvedProjectId(
|
|
46
|
+
options: GetUsersOptions | undefined,
|
|
47
|
+
projectId: string | undefined,
|
|
48
|
+
): GetUsersOptions | undefined {
|
|
49
|
+
if (!projectId) return options
|
|
50
|
+
if (!options) return {projectId}
|
|
51
|
+
const isOrgScoped = options.resourceType === 'organization' || !!options.organizationId
|
|
52
|
+
if (isOrgScoped || options.projectId) return options
|
|
53
|
+
return {...options, projectId}
|
|
54
|
+
}
|
|
55
|
+
|
|
38
56
|
/**
|
|
39
57
|
*
|
|
40
58
|
* @public
|
|
@@ -66,14 +84,29 @@ export interface UsersResult {
|
|
|
66
84
|
* </div>
|
|
67
85
|
* )
|
|
68
86
|
* ```
|
|
87
|
+
* @remarks
|
|
88
|
+
* For project-scoped queries the `projectId` is resolved in order from:
|
|
89
|
+
* 1. an explicit `projectId` option
|
|
90
|
+
* 2. A legacy ProjectContext (e.g. a `<ResourceProvider projectId="…">` with no dataset), then
|
|
91
|
+
* 3. The active resource (`ResourceProvider`/`SDKProvider`)
|
|
92
|
+
* 4. `instance.config`.
|
|
69
93
|
*/
|
|
70
94
|
export function useUsers(options?: GetUsersOptions): UsersResult {
|
|
71
95
|
const instance = useSanityInstance()
|
|
96
|
+
trackHookUsage(instance, 'useUsers')
|
|
72
97
|
// Use React's useTransition to avoid UI jank when user options change
|
|
73
98
|
const [isPending, startTransition] = useTransition()
|
|
74
99
|
|
|
100
|
+
// Resolve the projectId from the ambient project/resource context so a
|
|
101
|
+
// project-scoped users request can pick it up rather than the top-level config.
|
|
102
|
+
const resolvedProjectId = useResolvedProjectId(options)
|
|
103
|
+
const effectiveOptions = useMemo(
|
|
104
|
+
() => withResolvedProjectId(options, resolvedProjectId),
|
|
105
|
+
[options, resolvedProjectId],
|
|
106
|
+
)
|
|
107
|
+
|
|
75
108
|
// Get the unique key for this users request and its options
|
|
76
|
-
const key = getUsersKey(instance,
|
|
109
|
+
const key = getUsersKey(instance, effectiveOptions)
|
|
77
110
|
// Use a deferred state to avoid immediate re-renders when the users request changes
|
|
78
111
|
const [deferredKey, setDeferredKey] = useState(key)
|
|
79
112
|
// Parse the deferred users key back into users options
|
|
@@ -113,8 +146,8 @@ export function useUsers(options?: GetUsersOptions): UsersResult {
|
|
|
113
146
|
const {data, hasMore} = useSyncExternalStore(subscribe, getCurrent)!
|
|
114
147
|
|
|
115
148
|
const loadMore = useCallback(() => {
|
|
116
|
-
loadMoreUsers(instance,
|
|
117
|
-
}, [instance,
|
|
149
|
+
loadMoreUsers(instance, effectiveOptions)
|
|
150
|
+
}, [instance, effectiveOptions])
|
|
118
151
|
|
|
119
152
|
return {data, hasMore, isPending, loadMore}
|
|
120
153
|
}
|