@sanity/sdk-react 2.10.0 → 2.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +257 -200
- package/dist/index.js +364 -253
- package/dist/index.js.map +1 -1
- package/package.json +6 -9
- package/src/_exports/index.ts +2 -0
- package/src/_exports/sdk-react.ts +4 -0
- package/src/components/SDKProvider.test.tsx +5 -12
- package/src/components/SDKProvider.tsx +26 -24
- package/src/config/handles.ts +55 -0
- package/src/constants.ts +5 -0
- package/src/context/DefaultResourceContext.ts +10 -0
- package/src/context/PerspectiveContext.ts +12 -0
- package/src/context/ResourceProvider.test.tsx +2 -2
- package/src/context/ResourceProvider.tsx +53 -49
- package/src/hooks/agent/agentActions.ts +55 -38
- package/src/hooks/context/useResource.test.tsx +32 -0
- package/src/hooks/context/useResource.ts +24 -0
- package/src/hooks/context/useSanityInstance.test.tsx +42 -111
- package/src/hooks/context/useSanityInstance.ts +28 -50
- package/src/hooks/dashboard/useDispatchIntent.test.ts +5 -1
- package/src/hooks/dashboard/useDispatchIntent.ts +3 -3
- package/src/hooks/dashboard/useManageFavorite.test.tsx +16 -12
- package/src/hooks/dashboard/utils/useResourceIdFromDocumentHandle.ts +1 -5
- package/src/hooks/document/{useApplyDocumentActions.test.ts → useApplyDocumentActions.test.tsx} +42 -77
- package/src/hooks/document/useApplyDocumentActions.ts +28 -62
- package/src/hooks/document/useDocument.ts +3 -5
- package/src/hooks/document/useDocumentEvent.ts +4 -3
- package/src/hooks/document/useDocumentPermissions.test.tsx +58 -150
- package/src/hooks/document/useDocumentPermissions.ts +78 -55
- package/src/hooks/document/useEditDocument.test.tsx +25 -60
- package/src/hooks/document/useEditDocument.ts +1 -1
- package/src/hooks/documents/useDocuments.ts +13 -8
- package/src/hooks/helpers/createStateSourceHook.tsx +1 -2
- package/src/hooks/helpers/useNormalizedResourceOptions.test.tsx +253 -0
- package/src/hooks/helpers/useNormalizedResourceOptions.ts +85 -47
- package/src/hooks/organizations/useOrganization.test-d.ts +53 -0
- package/src/hooks/organizations/useOrganization.test.ts +65 -0
- package/src/hooks/organizations/useOrganization.ts +40 -0
- package/src/hooks/organizations/useOrganizations.test-d.ts +55 -0
- package/src/hooks/organizations/useOrganizations.test.ts +85 -0
- package/src/hooks/organizations/useOrganizations.ts +45 -0
- package/src/hooks/paginatedDocuments/usePaginatedDocuments.ts +23 -9
- package/src/hooks/presence/usePresence.ts +4 -11
- package/src/hooks/preview/useDocumentPreview.tsx +4 -7
- package/src/hooks/projection/useDocumentProjection.ts +5 -7
- package/src/hooks/projects/useProject.test-d.ts +49 -0
- package/src/hooks/projects/useProject.ts +33 -41
- package/src/hooks/projects/useProjects.test-d.ts +49 -0
- package/src/hooks/projects/useProjects.ts +17 -23
- package/src/hooks/query/useQuery.ts +1 -1
- package/src/hooks/releases/useActiveReleases.ts +6 -6
- package/src/hooks/releases/usePerspective.ts +7 -12
- package/src/hooks/users/useUser.ts +1 -1
- package/src/hooks/users/useUsers.ts +1 -1
|
@@ -1,14 +1,18 @@
|
|
|
1
|
-
import {type DocumentResource} from '@sanity/sdk'
|
|
2
|
-
import {useContext} from 'react'
|
|
1
|
+
import {type DocumentResource, type PerspectiveHandle} from '@sanity/sdk'
|
|
2
|
+
import {useContext, useMemo} from 'react'
|
|
3
3
|
|
|
4
|
+
import {ResourceContext} from '../../context/DefaultResourceContext'
|
|
5
|
+
import {PerspectiveContext} from '../../context/PerspectiveContext'
|
|
4
6
|
import {ResourcesContext} from '../../context/ResourcesContext'
|
|
7
|
+
import {SanityInstanceContext} from '../../context/SanityInstanceContext'
|
|
8
|
+
|
|
9
|
+
type NormalizedResourceFields = 'resourceName' | 'source' | 'sourceName' | 'projectId' | 'dataset'
|
|
5
10
|
|
|
6
11
|
/**
|
|
7
12
|
* Adds React hook support (resourceName resolution) to core types.
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* functions having resources explicitly passed will reduce complexity.
|
|
13
|
+
* Prefer using the React-layer handle types (ResourceHandle, DocumentHandle)
|
|
14
|
+
* from `@sanity/sdk-react` — this wrapper is kept for cases where overloads
|
|
15
|
+
* don't fit (e.g. non-handle options objects).
|
|
12
16
|
*
|
|
13
17
|
* @typeParam T - The core type to extend (must have optional `resource` field)
|
|
14
18
|
* @beta
|
|
@@ -35,6 +39,8 @@ export type WithResourceNameSupport<T extends {resource?: DocumentResource}> = T
|
|
|
35
39
|
* @typeParam T - The options type (must include optional resource field)
|
|
36
40
|
* @param options - Options that may include `resourceName` and/or `resource`
|
|
37
41
|
* @param resources - Map of resource names to DocumentResource (e.g. from ResourcesContext)
|
|
42
|
+
* @param contextResource - Resource from context (from ResourceContext)
|
|
43
|
+
* @param contextPerspective - Perspective from context (from PerspectiveContext)
|
|
38
44
|
* @returns Normalized options with `resourceName` removed and `resource` resolved
|
|
39
45
|
* @internal
|
|
40
46
|
*/
|
|
@@ -44,25 +50,23 @@ export function normalizeResourceOptions<
|
|
|
44
50
|
resourceName?: string
|
|
45
51
|
source?: DocumentResource
|
|
46
52
|
sourceName?: string
|
|
53
|
+
projectId?: string
|
|
54
|
+
dataset?: string
|
|
55
|
+
perspective?: unknown
|
|
47
56
|
},
|
|
48
57
|
>(
|
|
49
58
|
options: T,
|
|
50
59
|
resources: Record<string, DocumentResource>,
|
|
51
|
-
|
|
52
|
-
|
|
60
|
+
contextResource?: DocumentResource,
|
|
61
|
+
contextPerspective?: PerspectiveHandle['perspective'],
|
|
62
|
+
): Omit<T, NormalizedResourceFields> {
|
|
63
|
+
const {resourceName, sourceName, source, projectId, dataset, ...rest} = options
|
|
53
64
|
|
|
54
65
|
// Coalesce deprecated aliases to their canonical equivalents
|
|
55
66
|
const effectiveResourceName = resourceName ?? sourceName
|
|
56
67
|
const effectiveResource = options.resource ?? source
|
|
57
68
|
|
|
58
|
-
if (
|
|
59
|
-
return rest as Omit<T, 'resourceName' | 'source' | 'sourceName'>
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const hasNameKey = Object.hasOwn(options, 'resourceName') || Object.hasOwn(options, 'sourceName')
|
|
63
|
-
const hasResourceKey = Object.hasOwn(options, 'resource') || Object.hasOwn(options, 'source')
|
|
64
|
-
|
|
65
|
-
if (hasNameKey && hasResourceKey) {
|
|
69
|
+
if (effectiveResourceName && effectiveResource) {
|
|
66
70
|
throw new Error(
|
|
67
71
|
`Resource name ${JSON.stringify(effectiveResourceName)} and resource ${JSON.stringify(effectiveResource)} cannot be used together.`,
|
|
68
72
|
)
|
|
@@ -70,53 +74,76 @@ export function normalizeResourceOptions<
|
|
|
70
74
|
|
|
71
75
|
let resolvedResource: DocumentResource | undefined
|
|
72
76
|
|
|
77
|
+
// Tier (a): explicit resource object or resourceName lookup
|
|
73
78
|
if (effectiveResource) {
|
|
74
79
|
resolvedResource = effectiveResource
|
|
80
|
+
} else if (effectiveResourceName) {
|
|
81
|
+
if (!Object.hasOwn(resources, effectiveResourceName)) {
|
|
82
|
+
throw new Error(
|
|
83
|
+
`There's no resource named ${JSON.stringify(effectiveResourceName)} in context. Please use <ResourceProvider>.`,
|
|
84
|
+
)
|
|
85
|
+
}
|
|
86
|
+
resolvedResource = resources[effectiveResourceName]
|
|
75
87
|
}
|
|
76
88
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
89
|
+
// Tier (b): projectId or dataset in options → synthesize a resource
|
|
90
|
+
if (!resolvedResource && projectId && dataset) {
|
|
91
|
+
resolvedResource = {
|
|
92
|
+
projectId,
|
|
93
|
+
dataset,
|
|
94
|
+
}
|
|
81
95
|
}
|
|
82
96
|
|
|
83
|
-
|
|
84
|
-
|
|
97
|
+
// Tier (c): fall back to whatever ResourceContext provides
|
|
98
|
+
if (!resolvedResource) {
|
|
99
|
+
resolvedResource = contextResource
|
|
85
100
|
}
|
|
86
101
|
|
|
102
|
+
// Inject perspective from context when not explicitly provided in options
|
|
103
|
+
const resolvedPerspective = Object.hasOwn(options, 'perspective')
|
|
104
|
+
? options.perspective
|
|
105
|
+
: contextPerspective
|
|
106
|
+
|
|
87
107
|
return {
|
|
88
108
|
...rest,
|
|
89
|
-
resource: resolvedResource,
|
|
109
|
+
...(resolvedResource !== undefined && {resource: resolvedResource}),
|
|
110
|
+
...(resolvedPerspective !== undefined && {perspective: resolvedPerspective}),
|
|
90
111
|
}
|
|
91
112
|
}
|
|
92
113
|
|
|
93
114
|
/**
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
* clean separation between React and core layers.
|
|
115
|
+
* Returns the effective context resource: the `ResourceContext` value if set,
|
|
116
|
+
* otherwise a resource synthesized from the current `SanityInstance` config
|
|
117
|
+
* (tier-d fallback — returns `undefined` for studio-style configs with no project).
|
|
98
118
|
*
|
|
99
|
-
* @
|
|
100
|
-
|
|
101
|
-
|
|
119
|
+
* @internal
|
|
120
|
+
*/
|
|
121
|
+
export function useEffectiveContextResource(): DocumentResource | undefined {
|
|
122
|
+
const contextResource = useContext(ResourceContext)
|
|
123
|
+
const instance = useContext(SanityInstanceContext)
|
|
124
|
+
const {projectId, dataset} = instance?.config ?? {}
|
|
125
|
+
|
|
126
|
+
return useMemo(() => {
|
|
127
|
+
if (contextResource) return contextResource
|
|
128
|
+
if (projectId && dataset) return {projectId, dataset}
|
|
129
|
+
return undefined
|
|
130
|
+
}, [contextResource, projectId, dataset])
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Normalizes hook options by resolving `resourceName` to a `DocumentResource`.
|
|
102
135
|
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
136
|
+
* Resolution priority for resource:
|
|
137
|
+
* 1. Explicit `resource` or `resourceName` in options
|
|
138
|
+
* 2. Bare `projectId`/`dataset` pair in options → synthesized into a resource
|
|
139
|
+
* 3. `ResourceContext` value (set by `ResourceProvider` / `SDKProvider`)
|
|
140
|
+
* 4. Current `SanityInstance` config — falls back to `undefined` for studio configs
|
|
108
141
|
*
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
* const instance = useSanityInstance(options)
|
|
113
|
-
* const normalized = useNormalizedOptions(options)
|
|
114
|
-
* // normalized now has resource but never resourceName
|
|
115
|
-
* const queryKey = getQueryKey(normalized)
|
|
116
|
-
* }
|
|
117
|
-
* ```
|
|
142
|
+
* Resolution priority for perspective:
|
|
143
|
+
* 1. Explicit `perspective` in options
|
|
144
|
+
* 2. `PerspectiveContext` value (set by `ResourceProvider`)
|
|
118
145
|
*
|
|
119
|
-
* @
|
|
146
|
+
* @internal
|
|
120
147
|
*/
|
|
121
148
|
export function useNormalizedResourceOptions<
|
|
122
149
|
T extends {
|
|
@@ -124,8 +151,19 @@ export function useNormalizedResourceOptions<
|
|
|
124
151
|
resourceName?: string
|
|
125
152
|
source?: DocumentResource
|
|
126
153
|
sourceName?: string
|
|
154
|
+
projectId?: string
|
|
155
|
+
dataset?: string
|
|
156
|
+
perspective?: PerspectiveHandle['perspective']
|
|
127
157
|
},
|
|
128
|
-
>(
|
|
158
|
+
>(
|
|
159
|
+
options: T,
|
|
160
|
+
): Omit<T, NormalizedResourceFields> & {
|
|
161
|
+
resource?: DocumentResource
|
|
162
|
+
perspective?: PerspectiveHandle['perspective']
|
|
163
|
+
} {
|
|
129
164
|
const resources = useContext(ResourcesContext)
|
|
130
|
-
|
|
165
|
+
const effectiveContextResource = useEffectiveContextResource()
|
|
166
|
+
const contextPerspective = useContext(PerspectiveContext)
|
|
167
|
+
|
|
168
|
+
return normalizeResourceOptions(options, resources, effectiveContextResource, contextPerspective)
|
|
131
169
|
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import {type Organization, type OrganizationMember} from '@sanity/sdk'
|
|
2
|
+
import {expectTypeOf, test} from 'vitest'
|
|
3
|
+
|
|
4
|
+
import {useOrganization} from './useOrganization'
|
|
5
|
+
|
|
6
|
+
test('useOrganization — no flags: members and features both omitted', () => {
|
|
7
|
+
expectTypeOf(useOrganization({organizationId: 'org_1'})).toEqualTypeOf<
|
|
8
|
+
Organization<false, false>
|
|
9
|
+
>()
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
test('useOrganization — includeMembers: true adds members to the type', () => {
|
|
13
|
+
expectTypeOf(useOrganization({organizationId: 'org_1', includeMembers: true})).toEqualTypeOf<
|
|
14
|
+
Organization<true, false>
|
|
15
|
+
>()
|
|
16
|
+
type Result = ReturnType<typeof useOrganization<true, false>>
|
|
17
|
+
expectTypeOf<Result['members']>().toEqualTypeOf<OrganizationMember[]>()
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
test('useOrganization — includeFeatures: true adds features to the type', () => {
|
|
21
|
+
expectTypeOf(useOrganization({organizationId: 'org_1', includeFeatures: true})).toEqualTypeOf<
|
|
22
|
+
Organization<false, true>
|
|
23
|
+
>()
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
test('useOrganization — both flags true → both arrays present', () => {
|
|
27
|
+
expectTypeOf(
|
|
28
|
+
useOrganization({organizationId: 'org_1', includeMembers: true, includeFeatures: true}),
|
|
29
|
+
).toEqualTypeOf<Organization<true, true>>()
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
test('useOrganization — both flags false → bare base shape', () => {
|
|
33
|
+
expectTypeOf(
|
|
34
|
+
useOrganization({organizationId: 'org_1', includeMembers: false, includeFeatures: false}),
|
|
35
|
+
).toEqualTypeOf<Organization<false, false>>()
|
|
36
|
+
type Result = ReturnType<typeof useOrganization<false, false>>
|
|
37
|
+
expectTypeOf<Result['id']>().toEqualTypeOf<string>()
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
test('useOrganization — rejects non-boolean flag values', () => {
|
|
41
|
+
// @ts-expect-error — includeMembers must be a boolean
|
|
42
|
+
void useOrganization({organizationId: 'org_1', includeMembers: 'yes'})
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
test('useOrganization — non-literal boolean flag makes members optional', () => {
|
|
46
|
+
const includeMembers = false as boolean
|
|
47
|
+
expectTypeOf(useOrganization({organizationId: 'org_1', includeMembers})).toEqualTypeOf<
|
|
48
|
+
Organization<boolean, false>
|
|
49
|
+
>()
|
|
50
|
+
type Result = ReturnType<typeof useOrganization<boolean, false>>
|
|
51
|
+
expectTypeOf<Result['members']>().toEqualTypeOf<OrganizationMember[] | undefined>()
|
|
52
|
+
expectTypeOf<Pick<Result, 'members'>>().toEqualTypeOf<{members?: OrganizationMember[]}>()
|
|
53
|
+
})
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import {getOrganizationState, type OrganizationOptions, type SanityInstance} from '@sanity/sdk'
|
|
2
|
+
import {beforeEach, describe, expect, it, vi} from 'vitest'
|
|
3
|
+
|
|
4
|
+
import {createStateSourceHook} from '../helpers/createStateSourceHook'
|
|
5
|
+
|
|
6
|
+
vi.mock('@sanity/sdk', () => ({
|
|
7
|
+
getOrganizationState: vi.fn(() => ({
|
|
8
|
+
getCurrent: vi.fn(() => undefined),
|
|
9
|
+
})),
|
|
10
|
+
resolveOrganization: vi.fn(),
|
|
11
|
+
}))
|
|
12
|
+
vi.mock('../helpers/createStateSourceHook', () => ({
|
|
13
|
+
createStateSourceHook: vi.fn(),
|
|
14
|
+
}))
|
|
15
|
+
|
|
16
|
+
describe('useOrganization', () => {
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
vi.resetModules()
|
|
19
|
+
vi.mock('@sanity/sdk', () => ({
|
|
20
|
+
getOrganizationState: vi.fn(() => ({
|
|
21
|
+
getCurrent: vi.fn(() => undefined),
|
|
22
|
+
})),
|
|
23
|
+
resolveOrganization: vi.fn(),
|
|
24
|
+
}))
|
|
25
|
+
vi.mock('../helpers/createStateSourceHook', () => ({
|
|
26
|
+
createStateSourceHook: vi.fn(),
|
|
27
|
+
}))
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('should call createStateSourceHook with correct arguments on import', async () => {
|
|
31
|
+
await import('./useOrganization')
|
|
32
|
+
|
|
33
|
+
expect(createStateSourceHook).toHaveBeenCalled()
|
|
34
|
+
expect(createStateSourceHook).toHaveBeenCalledWith(
|
|
35
|
+
expect.objectContaining({
|
|
36
|
+
getState: expect.any(Function),
|
|
37
|
+
shouldSuspend: expect.any(Function),
|
|
38
|
+
suspender: expect.any(Function),
|
|
39
|
+
}),
|
|
40
|
+
)
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('shouldSuspend should call getOrganizationState and getCurrent', async () => {
|
|
44
|
+
await import('./useOrganization')
|
|
45
|
+
|
|
46
|
+
const mockCreateStateSourceHook = createStateSourceHook as ReturnType<typeof vi.fn>
|
|
47
|
+
expect(mockCreateStateSourceHook.mock.calls.length).toBeGreaterThan(0)
|
|
48
|
+
const createStateSourceHookArgs = mockCreateStateSourceHook.mock.calls[0][0]
|
|
49
|
+
const shouldSuspend = createStateSourceHookArgs.shouldSuspend
|
|
50
|
+
|
|
51
|
+
const mockInstance = {} as SanityInstance
|
|
52
|
+
const mockOptions: OrganizationOptions = {organizationId: 'org_1'}
|
|
53
|
+
|
|
54
|
+
const result = shouldSuspend(mockInstance, mockOptions)
|
|
55
|
+
|
|
56
|
+
const mockGetOrganizationState = getOrganizationState as ReturnType<typeof vi.fn>
|
|
57
|
+
expect(mockGetOrganizationState).toHaveBeenCalledWith(mockInstance, mockOptions)
|
|
58
|
+
|
|
59
|
+
expect(mockGetOrganizationState.mock.results.length).toBeGreaterThan(0)
|
|
60
|
+
const getOrganizationStateMockResult = mockGetOrganizationState.mock.results[0].value
|
|
61
|
+
expect(getOrganizationStateMockResult.getCurrent).toHaveBeenCalled()
|
|
62
|
+
|
|
63
|
+
expect(result).toBe(true)
|
|
64
|
+
})
|
|
65
|
+
})
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getOrganizationState,
|
|
3
|
+
type Organization,
|
|
4
|
+
type OrganizationOptions,
|
|
5
|
+
resolveOrganization,
|
|
6
|
+
} from '@sanity/sdk'
|
|
7
|
+
|
|
8
|
+
import {createStateSourceHook} from '../helpers/createStateSourceHook'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Returns metadata for a given organisation.
|
|
12
|
+
*
|
|
13
|
+
* @category Organizations
|
|
14
|
+
* @param options - Configuration options
|
|
15
|
+
* @returns The metadata for the organisation. `members` is included only when
|
|
16
|
+
* `includeMembers: true`; `features` is included only when `includeFeatures: true`.
|
|
17
|
+
* @example
|
|
18
|
+
* ```tsx
|
|
19
|
+
* function OrganizationName({organizationId}: {organizationId: string}) {
|
|
20
|
+
* const organization = useOrganization({organizationId})
|
|
21
|
+
*
|
|
22
|
+
* return <h1>{organization.name}</h1>
|
|
23
|
+
* }
|
|
24
|
+
* ```
|
|
25
|
+
* @example
|
|
26
|
+
* ```tsx
|
|
27
|
+
* const organizationWithMembers = useOrganization({organizationId, includeMembers: true})
|
|
28
|
+
* const organizationWithFeatures = useOrganization({organizationId, includeFeatures: true})
|
|
29
|
+
* ```
|
|
30
|
+
* @public
|
|
31
|
+
* @function
|
|
32
|
+
*/
|
|
33
|
+
export const useOrganization = createStateSourceHook({
|
|
34
|
+
getState: getOrganizationState,
|
|
35
|
+
shouldSuspend: (instance, ...params) =>
|
|
36
|
+
getOrganizationState(instance, ...params).getCurrent() === undefined,
|
|
37
|
+
suspender: resolveOrganization,
|
|
38
|
+
}) as <IncludeMembers extends boolean = false, IncludeFeatures extends boolean = false>(
|
|
39
|
+
options: OrganizationOptions<IncludeMembers, IncludeFeatures>,
|
|
40
|
+
) => Organization<IncludeMembers, IncludeFeatures>
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import {type OrganizationMember, type Organizations} from '@sanity/sdk'
|
|
2
|
+
import {expectTypeOf, test} from 'vitest'
|
|
3
|
+
|
|
4
|
+
import {useOrganizations} from './useOrganizations'
|
|
5
|
+
|
|
6
|
+
test('useOrganizations — no args: members and features both omitted', () => {
|
|
7
|
+
expectTypeOf(useOrganizations()).toEqualTypeOf<Organizations<false, false>>()
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
test('useOrganizations — includeMembers: true adds members to the type', () => {
|
|
11
|
+
expectTypeOf(useOrganizations({includeMembers: true})).toEqualTypeOf<Organizations<true, false>>()
|
|
12
|
+
type Result = ReturnType<typeof useOrganizations<true, false>>
|
|
13
|
+
expectTypeOf<Result[number]['members']>().toEqualTypeOf<OrganizationMember[]>()
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
test('useOrganizations — includeFeatures: true adds features to the type', () => {
|
|
17
|
+
expectTypeOf(useOrganizations({includeFeatures: true})).toEqualTypeOf<
|
|
18
|
+
Organizations<false, true>
|
|
19
|
+
>()
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
test('useOrganizations — both flags true → both arrays present', () => {
|
|
23
|
+
expectTypeOf(useOrganizations({includeMembers: true, includeFeatures: true})).toEqualTypeOf<
|
|
24
|
+
Organizations<true, true>
|
|
25
|
+
>()
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
test('useOrganizations — both flags false → bare base shape', () => {
|
|
29
|
+
expectTypeOf(useOrganizations({includeMembers: false, includeFeatures: false})).toEqualTypeOf<
|
|
30
|
+
Organizations<false, false>
|
|
31
|
+
>()
|
|
32
|
+
type Result = ReturnType<typeof useOrganizations<false, false>>
|
|
33
|
+
expectTypeOf<Result[number]['id']>().toEqualTypeOf<string>()
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
test('useOrganizations — rejects non-boolean flag values', () => {
|
|
37
|
+
// @ts-expect-error — includeMembers must be a boolean
|
|
38
|
+
void useOrganizations({includeMembers: 'yes'})
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
test('useOrganizations — includeImplicitMemberships does not change the data shape', () => {
|
|
42
|
+
expectTypeOf(useOrganizations({includeImplicitMemberships: true})).toEqualTypeOf<
|
|
43
|
+
Organizations<false, false>
|
|
44
|
+
>()
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
test('useOrganizations — non-literal boolean flag makes members optional', () => {
|
|
48
|
+
const includeMembers = false as boolean
|
|
49
|
+
expectTypeOf(useOrganizations({includeMembers})).toEqualTypeOf<Organizations<boolean, false>>()
|
|
50
|
+
type Result = ReturnType<typeof useOrganizations<boolean, false>>
|
|
51
|
+
expectTypeOf<Result[number]['members']>().toEqualTypeOf<OrganizationMember[] | undefined>()
|
|
52
|
+
expectTypeOf<Pick<Result[number], 'members'>>().toEqualTypeOf<{
|
|
53
|
+
members?: OrganizationMember[]
|
|
54
|
+
}>()
|
|
55
|
+
})
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import {getOrganizationsState, type SanityInstance} from '@sanity/sdk'
|
|
2
|
+
import {beforeEach, describe, expect, it, vi} from 'vitest'
|
|
3
|
+
|
|
4
|
+
import {createStateSourceHook} from '../helpers/createStateSourceHook'
|
|
5
|
+
|
|
6
|
+
vi.mock('@sanity/sdk', () => ({
|
|
7
|
+
getOrganizationsState: vi.fn(() => ({
|
|
8
|
+
getCurrent: vi.fn(() => undefined),
|
|
9
|
+
})),
|
|
10
|
+
resolveOrganizations: vi.fn(),
|
|
11
|
+
}))
|
|
12
|
+
vi.mock('../helpers/createStateSourceHook', () => ({
|
|
13
|
+
createStateSourceHook: vi.fn(),
|
|
14
|
+
}))
|
|
15
|
+
|
|
16
|
+
describe('useOrganizations', () => {
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
vi.resetModules()
|
|
19
|
+
vi.mock('@sanity/sdk', () => ({
|
|
20
|
+
getOrganizationsState: vi.fn(() => ({
|
|
21
|
+
getCurrent: vi.fn(() => undefined),
|
|
22
|
+
})),
|
|
23
|
+
resolveOrganizations: vi.fn(),
|
|
24
|
+
}))
|
|
25
|
+
vi.mock('../helpers/createStateSourceHook', () => ({
|
|
26
|
+
createStateSourceHook: vi.fn(),
|
|
27
|
+
}))
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('should call createStateSourceHook with correct arguments on import', async () => {
|
|
31
|
+
await import('./useOrganizations')
|
|
32
|
+
|
|
33
|
+
expect(createStateSourceHook).toHaveBeenCalled()
|
|
34
|
+
expect(createStateSourceHook).toHaveBeenCalledWith(
|
|
35
|
+
expect.objectContaining({
|
|
36
|
+
getState: expect.any(Function),
|
|
37
|
+
shouldSuspend: expect.any(Function),
|
|
38
|
+
suspender: expect.any(Function),
|
|
39
|
+
}),
|
|
40
|
+
)
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('shouldSuspend should call getOrganizationsState and getCurrent', async () => {
|
|
44
|
+
await import('./useOrganizations')
|
|
45
|
+
|
|
46
|
+
const mockCreateStateSourceHook = createStateSourceHook as ReturnType<typeof vi.fn>
|
|
47
|
+
expect(mockCreateStateSourceHook.mock.calls.length).toBeGreaterThan(0)
|
|
48
|
+
const createStateSourceHookArgs = mockCreateStateSourceHook.mock.calls[0][0]
|
|
49
|
+
const shouldSuspend = createStateSourceHookArgs.shouldSuspend
|
|
50
|
+
|
|
51
|
+
const mockInstance = {} as SanityInstance
|
|
52
|
+
|
|
53
|
+
const result = shouldSuspend(mockInstance, undefined)
|
|
54
|
+
|
|
55
|
+
const mockGetOrganizationsState = getOrganizationsState as ReturnType<typeof vi.fn>
|
|
56
|
+
expect(mockGetOrganizationsState).toHaveBeenCalledWith(mockInstance, undefined)
|
|
57
|
+
|
|
58
|
+
expect(mockGetOrganizationsState.mock.results.length).toBeGreaterThan(0)
|
|
59
|
+
const getOrganizationsStateMockResult = mockGetOrganizationsState.mock.results[0].value
|
|
60
|
+
expect(getOrganizationsStateMockResult.getCurrent).toHaveBeenCalled()
|
|
61
|
+
|
|
62
|
+
expect(result).toBe(true)
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('should handle different parameter combinations in shouldSuspend', async () => {
|
|
66
|
+
await import('./useOrganizations')
|
|
67
|
+
|
|
68
|
+
const mockCreateStateSourceHook = createStateSourceHook as ReturnType<typeof vi.fn>
|
|
69
|
+
const createStateSourceHookArgs = mockCreateStateSourceHook.mock.calls[0][0]
|
|
70
|
+
const shouldSuspend = createStateSourceHookArgs.shouldSuspend
|
|
71
|
+
|
|
72
|
+
const mockInstance = {} as SanityInstance
|
|
73
|
+
|
|
74
|
+
expect(() => shouldSuspend(mockInstance, undefined)).not.toThrow()
|
|
75
|
+
expect(() => shouldSuspend(mockInstance, {includeMembers: true})).not.toThrow()
|
|
76
|
+
expect(() => shouldSuspend(mockInstance, {includeFeatures: true})).not.toThrow()
|
|
77
|
+
expect(() =>
|
|
78
|
+
shouldSuspend(mockInstance, {
|
|
79
|
+
includeMembers: true,
|
|
80
|
+
includeFeatures: true,
|
|
81
|
+
includeImplicitMemberships: true,
|
|
82
|
+
}),
|
|
83
|
+
).not.toThrow()
|
|
84
|
+
})
|
|
85
|
+
})
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getOrganizationsState,
|
|
3
|
+
type Organizations,
|
|
4
|
+
type OrganizationsOptions,
|
|
5
|
+
resolveOrganizations,
|
|
6
|
+
} from '@sanity/sdk'
|
|
7
|
+
|
|
8
|
+
import {createStateSourceHook} from '../helpers/createStateSourceHook'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Returns metadata for each organisation the current user has access to.
|
|
12
|
+
*
|
|
13
|
+
* @category Organizations
|
|
14
|
+
* @param options - Configuration options
|
|
15
|
+
* @returns An array of organisation metadata. `members` is included only when
|
|
16
|
+
* `includeMembers: true`; `features` is included only when `includeFeatures: true`.
|
|
17
|
+
* @example
|
|
18
|
+
* ```tsx
|
|
19
|
+
* const organizations = useOrganizations()
|
|
20
|
+
*
|
|
21
|
+
* return (
|
|
22
|
+
* <select>
|
|
23
|
+
* {organizations.map((organization) => (
|
|
24
|
+
* <option key={organization.id}>{organization.name}</option>
|
|
25
|
+
* ))}
|
|
26
|
+
* </select>
|
|
27
|
+
* )
|
|
28
|
+
* ```
|
|
29
|
+
* @example
|
|
30
|
+
* ```tsx
|
|
31
|
+
* const organizationsWithMembers = useOrganizations({includeMembers: true})
|
|
32
|
+
* const organizationsWithFeatures = useOrganizations({includeFeatures: true})
|
|
33
|
+
* const organizationsIncludingImplicit = useOrganizations({includeImplicitMemberships: true})
|
|
34
|
+
* ```
|
|
35
|
+
* @public
|
|
36
|
+
* @function
|
|
37
|
+
*/
|
|
38
|
+
export const useOrganizations = createStateSourceHook({
|
|
39
|
+
getState: getOrganizationsState,
|
|
40
|
+
shouldSuspend: (instance, ...params) =>
|
|
41
|
+
getOrganizationsState(instance, ...params).getCurrent() === undefined,
|
|
42
|
+
suspender: resolveOrganizations,
|
|
43
|
+
}) as <IncludeMembers extends boolean = false, IncludeFeatures extends boolean = false>(
|
|
44
|
+
options?: OrganizationsOptions<IncludeMembers, IncludeFeatures>,
|
|
45
|
+
) => Organizations<IncludeMembers, IncludeFeatures>
|
|
@@ -1,8 +1,17 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
createGroqSearchFilter,
|
|
3
|
+
type DocumentHandle,
|
|
4
|
+
isDatasetResource,
|
|
5
|
+
type QueryOptions,
|
|
6
|
+
} from '@sanity/sdk'
|
|
7
|
+
import {pickProperties} from '@sanity/sdk/_internal'
|
|
2
8
|
import {type SortOrderingItem} from '@sanity/types'
|
|
3
9
|
import {useCallback, useMemo, useState} from 'react'
|
|
4
10
|
|
|
5
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
useNormalizedResourceOptions,
|
|
13
|
+
type WithResourceNameSupport,
|
|
14
|
+
} from '../helpers/useNormalizedResourceOptions'
|
|
6
15
|
import {useTrackHookUsage} from '../helpers/useTrackHookUsage'
|
|
7
16
|
import {useQuery} from '../query/useQuery'
|
|
8
17
|
|
|
@@ -16,7 +25,9 @@ export interface PaginatedDocumentsOptions<
|
|
|
16
25
|
TDocumentType extends string = string,
|
|
17
26
|
TDataset extends string = string,
|
|
18
27
|
TProjectId extends string = string,
|
|
19
|
-
> extends
|
|
28
|
+
> extends WithResourceNameSupport<
|
|
29
|
+
Omit<QueryOptions<TDocumentType, TDataset, TProjectId>, 'query'>
|
|
30
|
+
> {
|
|
20
31
|
documentType?: TDocumentType | TDocumentType[]
|
|
21
32
|
/**
|
|
22
33
|
* GROQ filter expression to apply to the query
|
|
@@ -232,16 +243,16 @@ export function usePaginatedDocuments<
|
|
|
232
243
|
params = {},
|
|
233
244
|
orderings,
|
|
234
245
|
search,
|
|
235
|
-
...
|
|
246
|
+
...rawOptions
|
|
236
247
|
}: PaginatedDocumentsOptions<TDocumentType, TDataset, TProjectId>): PaginatedDocumentsResponse<
|
|
237
248
|
TDocumentType,
|
|
238
249
|
TDataset,
|
|
239
250
|
TProjectId
|
|
240
251
|
> {
|
|
241
252
|
useTrackHookUsage('usePaginatedDocuments')
|
|
242
|
-
const
|
|
253
|
+
const options = useNormalizedResourceOptions(rawOptions)
|
|
243
254
|
const [pageIndex, setPageIndex] = useState(0)
|
|
244
|
-
const key = JSON.stringify({filter, search, params, orderings, pageSize})
|
|
255
|
+
const key = JSON.stringify({filter, search, params, orderings, pageSize, ...options})
|
|
245
256
|
// Reset pageIndex to 0 whenever any query parameter changes.
|
|
246
257
|
const [prevKey, setPrevKey] = useState(key)
|
|
247
258
|
if (prevKey !== key) {
|
|
@@ -303,9 +314,12 @@ export function usePaginatedDocuments<
|
|
|
303
314
|
...params,
|
|
304
315
|
__types: documentTypes,
|
|
305
316
|
__handle: {
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
317
|
+
// keep projectId/dataset for backward compat until v4; resource is added
|
|
318
|
+
// intentionally so that hook consumers can resolve the correct resource
|
|
319
|
+
...(options.resource && isDatasetResource(options.resource)
|
|
320
|
+
? pickProperties(options.resource, ['projectId', 'dataset'])
|
|
321
|
+
: {}),
|
|
322
|
+
...pickProperties(options, ['perspective', 'resource']),
|
|
309
323
|
},
|
|
310
324
|
},
|
|
311
325
|
})
|
|
@@ -1,23 +1,16 @@
|
|
|
1
|
-
import {
|
|
2
|
-
type DatasetHandle,
|
|
3
|
-
getPresence,
|
|
4
|
-
isMediaLibraryResource,
|
|
5
|
-
type UserPresence,
|
|
6
|
-
} from '@sanity/sdk'
|
|
1
|
+
import {getPresence, isMediaLibraryResource, type UserPresence} from '@sanity/sdk'
|
|
7
2
|
import {useCallback, useMemo, useSyncExternalStore} from 'react'
|
|
8
3
|
|
|
4
|
+
import {type ResourceHandle} from '../../config/handles'
|
|
9
5
|
import {useSanityInstance} from '../context/useSanityInstance'
|
|
10
|
-
import {
|
|
11
|
-
useNormalizedResourceOptions,
|
|
12
|
-
type WithResourceNameSupport,
|
|
13
|
-
} from '../helpers/useNormalizedResourceOptions'
|
|
6
|
+
import {useNormalizedResourceOptions} from '../helpers/useNormalizedResourceOptions'
|
|
14
7
|
import {trackHookUsage} from '../helpers/useTrackHookUsage'
|
|
15
8
|
|
|
16
9
|
/**
|
|
17
10
|
* A hook for subscribing to presence information for the current project or Canvas.
|
|
18
11
|
* @public
|
|
19
12
|
*/
|
|
20
|
-
export function usePresence(options:
|
|
13
|
+
export function usePresence(options: ResourceHandle = {}): {
|
|
21
14
|
locations: UserPresence[]
|
|
22
15
|
} {
|
|
23
16
|
const normalizedOptions = useNormalizedResourceOptions(options)
|