@sanity/sdk-react 2.9.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 +338 -215
- package/dist/index.js +564 -342
- package/dist/index.js.map +1 -1
- package/package.json +9 -14
- package/src/_exports/index.ts +2 -0
- package/src/_exports/sdk-react.ts +8 -0
- package/src/components/SDKProvider.test.tsx +5 -12
- package/src/components/SDKProvider.tsx +58 -28
- package/src/components/SanityApp.tsx +2 -2
- package/src/components/auth/AuthBoundary.tsx +8 -1
- package/src/components/auth/DashboardAccessRequest.tsx +37 -0
- package/src/components/auth/LoginError.test.tsx +191 -5
- package/src/components/auth/LoginError.tsx +100 -56
- package/src/components/errors/ChunkLoadError.test.tsx +59 -0
- package/src/components/errors/ChunkLoadError.tsx +56 -0
- package/src/components/errors/chunkReloadStorage.ts +57 -0
- 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 +56 -51
- package/src/context/ResourcesContext.tsx +7 -0
- package/src/context/SanityInstanceProvider.test.tsx +100 -0
- package/src/context/SanityInstanceProvider.tsx +71 -0
- package/src/hooks/agent/agentActions.ts +55 -38
- package/src/hooks/auth/useVerifyOrgProjects.tsx +13 -6
- 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 +11 -7
- package/src/hooks/dashboard/useDispatchIntent.ts +7 -7
- package/src/hooks/dashboard/useManageFavorite.test.tsx +16 -12
- package/src/hooks/dashboard/utils/useResourceIdFromDocumentHandle.test.ts +15 -15
- package/src/hooks/dashboard/utils/useResourceIdFromDocumentHandle.ts +13 -17
- package/src/hooks/document/{useApplyDocumentActions.test.ts → useApplyDocumentActions.test.tsx} +46 -81
- package/src/hooks/document/useApplyDocumentActions.ts +33 -67
- package/src/hooks/document/useDocument.ts +4 -6
- package/src/hooks/document/useDocumentEvent.ts +8 -7
- package/src/hooks/document/useDocumentPermissions.test.tsx +60 -152
- package/src/hooks/document/useDocumentPermissions.ts +78 -55
- package/src/hooks/document/useDocumentSyncStatus.ts +2 -2
- package/src/hooks/document/useEditDocument.test.tsx +25 -60
- package/src/hooks/document/useEditDocument.ts +3 -3
- package/src/hooks/documents/useDocuments.ts +19 -11
- package/src/hooks/helpers/createStateSourceHook.tsx +1 -2
- package/src/hooks/helpers/useNormalizedResourceOptions.test.tsx +253 -0
- package/src/hooks/helpers/useNormalizedResourceOptions.ts +169 -0
- package/src/hooks/helpers/useTrackHookUsage.ts +2 -2
- 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 +29 -14
- package/src/hooks/presence/usePresence.test.tsx +56 -9
- package/src/hooks/presence/usePresence.ts +16 -4
- package/src/hooks/preview/useDocumentPreview.tsx +8 -10
- package/src/hooks/projection/useDocumentProjection.ts +7 -9
- 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 +11 -10
- package/src/hooks/releases/useActiveReleases.ts +14 -14
- package/src/hooks/releases/usePerspective.ts +11 -16
- package/src/hooks/users/useUser.ts +1 -1
- package/src/hooks/users/useUsers.ts +1 -1
- package/src/context/SourcesContext.tsx +0 -7
- package/src/hooks/helpers/useNormalizedSourceOptions.ts +0 -107
|
@@ -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,9 +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
|
-
import {
|
|
4
|
-
import {useCallback, useEffect, useMemo, useState} from 'react'
|
|
9
|
+
import {useCallback, useMemo, useState} from 'react'
|
|
5
10
|
|
|
6
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
useNormalizedResourceOptions,
|
|
13
|
+
type WithResourceNameSupport,
|
|
14
|
+
} from '../helpers/useNormalizedResourceOptions'
|
|
7
15
|
import {useTrackHookUsage} from '../helpers/useTrackHookUsage'
|
|
8
16
|
import {useQuery} from '../query/useQuery'
|
|
9
17
|
|
|
@@ -17,7 +25,9 @@ export interface PaginatedDocumentsOptions<
|
|
|
17
25
|
TDocumentType extends string = string,
|
|
18
26
|
TDataset extends string = string,
|
|
19
27
|
TProjectId extends string = string,
|
|
20
|
-
> extends
|
|
28
|
+
> extends WithResourceNameSupport<
|
|
29
|
+
Omit<QueryOptions<TDocumentType, TDataset, TProjectId>, 'query'>
|
|
30
|
+
> {
|
|
21
31
|
documentType?: TDocumentType | TDocumentType[]
|
|
22
32
|
/**
|
|
23
33
|
* GROQ filter expression to apply to the query
|
|
@@ -233,21 +243,22 @@ export function usePaginatedDocuments<
|
|
|
233
243
|
params = {},
|
|
234
244
|
orderings,
|
|
235
245
|
search,
|
|
236
|
-
...
|
|
246
|
+
...rawOptions
|
|
237
247
|
}: PaginatedDocumentsOptions<TDocumentType, TDataset, TProjectId>): PaginatedDocumentsResponse<
|
|
238
248
|
TDocumentType,
|
|
239
249
|
TDataset,
|
|
240
250
|
TProjectId
|
|
241
251
|
> {
|
|
242
252
|
useTrackHookUsage('usePaginatedDocuments')
|
|
243
|
-
const
|
|
253
|
+
const options = useNormalizedResourceOptions(rawOptions)
|
|
244
254
|
const [pageIndex, setPageIndex] = useState(0)
|
|
245
|
-
const key = JSON.stringify({filter, search, params, orderings, pageSize})
|
|
246
|
-
// Reset
|
|
247
|
-
|
|
248
|
-
|
|
255
|
+
const key = JSON.stringify({filter, search, params, orderings, pageSize, ...options})
|
|
256
|
+
// Reset pageIndex to 0 whenever any query parameter changes.
|
|
257
|
+
const [prevKey, setPrevKey] = useState(key)
|
|
258
|
+
if (prevKey !== key) {
|
|
259
|
+
setPrevKey(key)
|
|
249
260
|
setPageIndex(0)
|
|
250
|
-
}
|
|
261
|
+
}
|
|
251
262
|
|
|
252
263
|
const startIndex = pageIndex * pageSize
|
|
253
264
|
const endIndex = (pageIndex + 1) * pageSize
|
|
@@ -303,8 +314,12 @@ export function usePaginatedDocuments<
|
|
|
303
314
|
...params,
|
|
304
315
|
__types: documentTypes,
|
|
305
316
|
__handle: {
|
|
306
|
-
|
|
307
|
-
|
|
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']),
|
|
308
323
|
},
|
|
309
324
|
},
|
|
310
325
|
})
|
|
@@ -6,14 +6,17 @@ import {describe, expect, it, vi} from 'vitest'
|
|
|
6
6
|
import {ResourceProvider} from '../../context/ResourceProvider'
|
|
7
7
|
import {usePresence} from './usePresence'
|
|
8
8
|
|
|
9
|
-
vi.mock('@sanity/sdk', () =>
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
9
|
+
vi.mock('@sanity/sdk', async (importOriginal) => {
|
|
10
|
+
const actual = await importOriginal<typeof import('@sanity/sdk')>()
|
|
11
|
+
return {
|
|
12
|
+
...actual,
|
|
13
|
+
getPresence: vi.fn(),
|
|
14
|
+
createSanityInstance: vi.fn(() => ({
|
|
15
|
+
isDisposed: vi.fn(() => false),
|
|
16
|
+
dispose: vi.fn(),
|
|
17
|
+
})),
|
|
18
|
+
}
|
|
19
|
+
})
|
|
17
20
|
|
|
18
21
|
describe('usePresence', () => {
|
|
19
22
|
it('should return presence locations and update when the store changes', () => {
|
|
@@ -59,7 +62,10 @@ describe('usePresence', () => {
|
|
|
59
62
|
|
|
60
63
|
const {result, unmount} = renderHook(() => usePresence(), {
|
|
61
64
|
wrapper: ({children}) => (
|
|
62
|
-
<ResourceProvider
|
|
65
|
+
<ResourceProvider
|
|
66
|
+
resource={{projectId: 'test-project', dataset: 'test-dataset'}}
|
|
67
|
+
fallback={null}
|
|
68
|
+
>
|
|
63
69
|
{children}
|
|
64
70
|
</ResourceProvider>
|
|
65
71
|
),
|
|
@@ -80,4 +86,45 @@ describe('usePresence', () => {
|
|
|
80
86
|
expect(result.current.locations).toEqual(updatedLocations)
|
|
81
87
|
unmount()
|
|
82
88
|
})
|
|
89
|
+
|
|
90
|
+
it('should throw an error when used with a media library resource', () => {
|
|
91
|
+
expect(() => {
|
|
92
|
+
renderHook(() => usePresence({resource: {mediaLibraryId: 'ml123'}}), {
|
|
93
|
+
wrapper: ({children}) => (
|
|
94
|
+
<ResourceProvider
|
|
95
|
+
resource={{projectId: 'test-project', dataset: 'test-dataset'}}
|
|
96
|
+
fallback={null}
|
|
97
|
+
>
|
|
98
|
+
{children}
|
|
99
|
+
</ResourceProvider>
|
|
100
|
+
),
|
|
101
|
+
})
|
|
102
|
+
}).toThrow('usePresence() does not support media library resources')
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it('should work with a dataset resource', () => {
|
|
106
|
+
const mockPresenceSource = {
|
|
107
|
+
getCurrent: vi.fn().mockReturnValue([]),
|
|
108
|
+
subscribe: vi.fn(() => () => {}),
|
|
109
|
+
observable: NEVER,
|
|
110
|
+
}
|
|
111
|
+
vi.mocked(getPresence).mockReturnValue(mockPresenceSource)
|
|
112
|
+
|
|
113
|
+
const {result, unmount} = renderHook(
|
|
114
|
+
() => usePresence({resource: {projectId: 'test-project', dataset: 'test-dataset'}}),
|
|
115
|
+
{
|
|
116
|
+
wrapper: ({children}) => (
|
|
117
|
+
<ResourceProvider
|
|
118
|
+
resource={{projectId: 'test-project', dataset: 'test-dataset'}}
|
|
119
|
+
fallback={null}
|
|
120
|
+
>
|
|
121
|
+
{children}
|
|
122
|
+
</ResourceProvider>
|
|
123
|
+
),
|
|
124
|
+
},
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
expect(result.current.locations).toEqual([])
|
|
128
|
+
unmount()
|
|
129
|
+
})
|
|
83
130
|
})
|
|
@@ -1,19 +1,31 @@
|
|
|
1
|
-
import {getPresence, type UserPresence} from '@sanity/sdk'
|
|
1
|
+
import {getPresence, isMediaLibraryResource, type UserPresence} from '@sanity/sdk'
|
|
2
2
|
import {useCallback, useMemo, useSyncExternalStore} from 'react'
|
|
3
3
|
|
|
4
|
+
import {type ResourceHandle} from '../../config/handles'
|
|
4
5
|
import {useSanityInstance} from '../context/useSanityInstance'
|
|
6
|
+
import {useNormalizedResourceOptions} from '../helpers/useNormalizedResourceOptions'
|
|
5
7
|
import {trackHookUsage} from '../helpers/useTrackHookUsage'
|
|
6
8
|
|
|
7
9
|
/**
|
|
8
|
-
* A hook for subscribing to presence information for the current project.
|
|
10
|
+
* A hook for subscribing to presence information for the current project or Canvas.
|
|
9
11
|
* @public
|
|
10
12
|
*/
|
|
11
|
-
export function usePresence(): {
|
|
13
|
+
export function usePresence(options: ResourceHandle = {}): {
|
|
12
14
|
locations: UserPresence[]
|
|
13
15
|
} {
|
|
16
|
+
const normalizedOptions = useNormalizedResourceOptions(options)
|
|
17
|
+
if (normalizedOptions.resource && isMediaLibraryResource(normalizedOptions.resource)) {
|
|
18
|
+
throw new Error(
|
|
19
|
+
'usePresence() does not support media library resources. Presence tracking requires a canvas or dataset resource.',
|
|
20
|
+
)
|
|
21
|
+
}
|
|
22
|
+
|
|
14
23
|
const sanityInstance = useSanityInstance()
|
|
15
24
|
trackHookUsage(sanityInstance, 'usePresence')
|
|
16
|
-
const source = useMemo(
|
|
25
|
+
const source = useMemo(
|
|
26
|
+
() => getPresence(sanityInstance, normalizedOptions),
|
|
27
|
+
[sanityInstance, normalizedOptions],
|
|
28
|
+
)
|
|
17
29
|
const subscribe = useCallback((callback: () => void) => source.subscribe(callback), [source])
|
|
18
30
|
const locations = useSyncExternalStore(
|
|
19
31
|
subscribe,
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import {
|
|
2
|
-
type DocumentHandle,
|
|
3
2
|
PREVIEW_PROJECTION,
|
|
4
3
|
type PreviewQueryResult,
|
|
5
4
|
type PreviewValue,
|
|
@@ -7,11 +6,9 @@ import {
|
|
|
7
6
|
} from '@sanity/sdk'
|
|
8
7
|
import {useMemo} from 'react'
|
|
9
8
|
|
|
9
|
+
import {type DocumentHandle} from '../../config/handles'
|
|
10
10
|
import {useSanityInstance} from '../context/useSanityInstance'
|
|
11
|
-
import {
|
|
12
|
-
useNormalizedSourceOptions,
|
|
13
|
-
type WithSourceNameSupport,
|
|
14
|
-
} from '../helpers/useNormalizedSourceOptions'
|
|
11
|
+
import {useNormalizedResourceOptions} from '../helpers/useNormalizedResourceOptions'
|
|
15
12
|
import {trackHookUsage} from '../helpers/useTrackHookUsage'
|
|
16
13
|
import {useDocumentProjection} from '../projection/useDocumentProjection'
|
|
17
14
|
|
|
@@ -19,7 +16,7 @@ import {useDocumentProjection} from '../projection/useDocumentProjection'
|
|
|
19
16
|
* @public
|
|
20
17
|
* @category Types
|
|
21
18
|
*/
|
|
22
|
-
export interface useDocumentPreviewOptions extends
|
|
19
|
+
export interface useDocumentPreviewOptions extends DocumentHandle {
|
|
23
20
|
/**
|
|
24
21
|
* Optional ref object to track visibility. When provided, preview resolution
|
|
25
22
|
* only occurs when the referenced element is visible in the viewport.
|
|
@@ -98,9 +95,9 @@ export function useDocumentPreview({
|
|
|
98
95
|
ref,
|
|
99
96
|
...docHandle
|
|
100
97
|
}: useDocumentPreviewOptions): useDocumentPreviewResults {
|
|
101
|
-
const instance = useSanityInstance(
|
|
98
|
+
const instance = useSanityInstance()
|
|
102
99
|
trackHookUsage(instance, 'useDocumentPreview')
|
|
103
|
-
const normalizedDocHandle =
|
|
100
|
+
const normalizedDocHandle = useNormalizedResourceOptions(docHandle)
|
|
104
101
|
|
|
105
102
|
// Use the projection hook with the fixed preview projection
|
|
106
103
|
const projectionResult = useDocumentProjection<PreviewQueryResult>({
|
|
@@ -112,8 +109,9 @@ export function useDocumentPreview({
|
|
|
112
109
|
// Contract: useDocumentProjection suspends while data is null, so data is always available here.
|
|
113
110
|
// Keep this non-null assumption aligned with useDocumentPreviewResults.data.
|
|
114
111
|
const previewValue = useMemo(
|
|
115
|
-
() =>
|
|
116
|
-
|
|
112
|
+
() =>
|
|
113
|
+
transformProjectionToPreview(instance, projectionResult.data, normalizedDocHandle.resource),
|
|
114
|
+
[projectionResult.data, instance, normalizedDocHandle.resource],
|
|
117
115
|
)
|
|
118
116
|
|
|
119
117
|
return {
|
|
@@ -1,13 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {getProjectionState, resolveProjection} from '@sanity/sdk'
|
|
2
2
|
import {type SanityProjectionResult} from 'groq'
|
|
3
3
|
import {useCallback, useMemo, useSyncExternalStore} from 'react'
|
|
4
4
|
import {distinctUntilChanged, EMPTY, Observable, startWith, switchMap} from 'rxjs'
|
|
5
5
|
|
|
6
|
+
import {type DocumentHandle} from '../../config/handles'
|
|
6
7
|
import {useSanityInstance} from '../context/useSanityInstance'
|
|
7
|
-
import {
|
|
8
|
-
useNormalizedSourceOptions,
|
|
9
|
-
type WithSourceNameSupport,
|
|
10
|
-
} from '../helpers/useNormalizedSourceOptions'
|
|
8
|
+
import {useNormalizedResourceOptions} from '../helpers/useNormalizedResourceOptions'
|
|
11
9
|
import {trackHookUsage} from '../helpers/useTrackHookUsage'
|
|
12
10
|
|
|
13
11
|
/**
|
|
@@ -19,7 +17,7 @@ export interface useDocumentProjectionOptions<
|
|
|
19
17
|
TDocumentType extends string = string,
|
|
20
18
|
TDataset extends string = string,
|
|
21
19
|
TProjectId extends string = string,
|
|
22
|
-
> extends
|
|
20
|
+
> extends DocumentHandle<TDocumentType, TDataset, TProjectId> {
|
|
23
21
|
/** The GROQ projection string */
|
|
24
22
|
projection: TProjection
|
|
25
23
|
/** Optional parameters for the projection query */
|
|
@@ -181,7 +179,7 @@ export function useDocumentProjection<TData extends object>({
|
|
|
181
179
|
projection,
|
|
182
180
|
...docHandle
|
|
183
181
|
}: useDocumentProjectionOptions): useDocumentProjectionResults<TData> {
|
|
184
|
-
const instance = useSanityInstance(
|
|
182
|
+
const instance = useSanityInstance()
|
|
185
183
|
trackHookUsage(instance, 'useDocumentProjection')
|
|
186
184
|
|
|
187
185
|
// Normalize projection string to handle template literals with whitespace
|
|
@@ -189,8 +187,8 @@ export function useDocumentProjection<TData extends object>({
|
|
|
189
187
|
// even if the string reference changes (e.g., from inline template literals)
|
|
190
188
|
const normalizedProjection = useMemo(() => projection.trim(), [projection])
|
|
191
189
|
|
|
192
|
-
// Normalize options: resolve
|
|
193
|
-
const normalizedDocHandle =
|
|
190
|
+
// Normalize options: resolve resourceName to resource and strip resourceName
|
|
191
|
+
const normalizedDocHandle = useNormalizedResourceOptions(docHandle)
|
|
194
192
|
|
|
195
193
|
// Memoize stateSource based on normalized projection and docHandle properties
|
|
196
194
|
// This prevents creating a new StateSource on every render when projection content is the same
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import {type Project, type ProjectMember} from '@sanity/sdk'
|
|
2
|
+
import {expectTypeOf, test} from 'vitest'
|
|
3
|
+
|
|
4
|
+
import {useProject} from './useProject'
|
|
5
|
+
|
|
6
|
+
test('useProject — no args: members and features both included by default', () => {
|
|
7
|
+
expectTypeOf(useProject()).toEqualTypeOf<Project<true, true>>()
|
|
8
|
+
type Result = ReturnType<typeof useProject<true, true>>
|
|
9
|
+
expectTypeOf<Result['members']>().toEqualTypeOf<ProjectMember[]>()
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
test('useProject — includeMembers: false drops members from the type', () => {
|
|
13
|
+
expectTypeOf(useProject({includeMembers: false})).toEqualTypeOf<Project<false, true>>()
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
test('useProject — includeFeatures: false drops features from the type', () => {
|
|
17
|
+
expectTypeOf(useProject({includeFeatures: false})).toEqualTypeOf<Project<true, false>>()
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
test('useProject — both flags true → both arrays present', () => {
|
|
21
|
+
expectTypeOf(useProject({includeMembers: true, includeFeatures: true})).toEqualTypeOf<
|
|
22
|
+
Project<true, true>
|
|
23
|
+
>()
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
test('useProject — both flags false → bare base shape', () => {
|
|
27
|
+
expectTypeOf(useProject({includeMembers: false, includeFeatures: false})).toEqualTypeOf<
|
|
28
|
+
Project<false, false>
|
|
29
|
+
>()
|
|
30
|
+
type Result = ReturnType<typeof useProject<false, false>>
|
|
31
|
+
expectTypeOf<Result['id']>().toEqualTypeOf<string>()
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
test('useProject — rejects non-boolean flag values', () => {
|
|
35
|
+
// @ts-expect-error — includeMembers must be a boolean
|
|
36
|
+
void useProject({includeMembers: 'yes'})
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
test('useProject — projectId alone does not change the data shape', () => {
|
|
40
|
+
expectTypeOf(useProject({projectId: 'p'})).toEqualTypeOf<Project<true, true>>()
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
test('useProject — non-literal boolean flag makes members optional', () => {
|
|
44
|
+
const includeMembers = false as boolean
|
|
45
|
+
expectTypeOf(useProject({includeMembers})).toEqualTypeOf<Project<boolean, true>>()
|
|
46
|
+
type Result = ReturnType<typeof useProject<boolean, true>>
|
|
47
|
+
expectTypeOf<Result['members']>().toEqualTypeOf<ProjectMember[] | undefined>()
|
|
48
|
+
expectTypeOf<Pick<Result, 'members'>>().toEqualTypeOf<{members?: ProjectMember[]}>()
|
|
49
|
+
})
|
|
@@ -1,51 +1,43 @@
|
|
|
1
|
-
import {
|
|
2
|
-
getProjectState,
|
|
3
|
-
type ProjectHandle,
|
|
4
|
-
resolveProject,
|
|
5
|
-
type SanityInstance,
|
|
6
|
-
type SanityProject,
|
|
7
|
-
type StateSource,
|
|
8
|
-
} from '@sanity/sdk'
|
|
1
|
+
import {getProjectState, type Project, type ProjectOptions, resolveProject} from '@sanity/sdk'
|
|
9
2
|
import {identity} from 'rxjs'
|
|
10
3
|
|
|
11
4
|
import {createStateSourceHook} from '../helpers/createStateSourceHook'
|
|
12
5
|
|
|
13
|
-
type UseProject = {
|
|
14
|
-
/**
|
|
15
|
-
*
|
|
16
|
-
* Returns metadata for a given project
|
|
17
|
-
*
|
|
18
|
-
* @category Projects
|
|
19
|
-
* @param projectId - The ID of the project to retrieve metadata for
|
|
20
|
-
* @returns The metadata for the project
|
|
21
|
-
* @example
|
|
22
|
-
* ```tsx
|
|
23
|
-
* function ProjectMetadata({ projectId }: { projectId: string }) {
|
|
24
|
-
* const project = useProject(projectId)
|
|
25
|
-
*
|
|
26
|
-
* return (
|
|
27
|
-
* <figure style={{ backgroundColor: project.metadata.color || 'lavender'}}>
|
|
28
|
-
* <h1>{project.displayName}</h1>
|
|
29
|
-
* </figure>
|
|
30
|
-
* )
|
|
31
|
-
* }
|
|
32
|
-
* ```
|
|
33
|
-
*/
|
|
34
|
-
(projectHandle?: ProjectHandle): SanityProject
|
|
35
|
-
}
|
|
36
|
-
|
|
37
6
|
/**
|
|
7
|
+
* Returns metadata for a given project.
|
|
8
|
+
*
|
|
9
|
+
* @category Projects
|
|
10
|
+
* @param options - Configuration options
|
|
11
|
+
* @returns The metadata for the project. `members` is included only when
|
|
12
|
+
* `includeMembers: true`; `features` is included unless `includeFeatures: false`.
|
|
13
|
+
* @example
|
|
14
|
+
* ```tsx
|
|
15
|
+
* function ProjectMetadata({projectId}: {projectId: string}) {
|
|
16
|
+
* const project = useProject({projectId})
|
|
17
|
+
*
|
|
18
|
+
* return (
|
|
19
|
+
* <figure style={{backgroundColor: project.metadata.color || 'lavender'}}>
|
|
20
|
+
* <h1>{project.displayName}</h1>
|
|
21
|
+
* </figure>
|
|
22
|
+
* )
|
|
23
|
+
* }
|
|
24
|
+
* ```
|
|
25
|
+
* @example
|
|
26
|
+
* ```tsx
|
|
27
|
+
* const projectWithMembersAndFeatures = useProject({projectId})
|
|
28
|
+
* const projectWithMembers = useProject({projectId, includeMembers: true})
|
|
29
|
+
* const projectWithoutMembers = useProject({projectId, includeMembers: false})
|
|
30
|
+
* const projectWithoutFeatures = useProject({projectId, includeFeatures: false})
|
|
31
|
+
* ```
|
|
38
32
|
* @public
|
|
39
33
|
* @function
|
|
40
34
|
*/
|
|
41
|
-
export const useProject
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
instance
|
|
45
|
-
projectHandle?: ProjectHandle,
|
|
46
|
-
) => StateSource<SanityProject>,
|
|
47
|
-
shouldSuspend: (instance, projectHandle) =>
|
|
48
|
-
getProjectState(instance, projectHandle).getCurrent() === undefined,
|
|
35
|
+
export const useProject = createStateSourceHook({
|
|
36
|
+
getState: getProjectState,
|
|
37
|
+
shouldSuspend: (instance, ...params) =>
|
|
38
|
+
getProjectState(instance, ...params).getCurrent() === undefined,
|
|
49
39
|
suspender: resolveProject,
|
|
50
40
|
getConfig: identity,
|
|
51
|
-
})
|
|
41
|
+
}) as <IncludeMembers extends boolean = true, IncludeFeatures extends boolean = true>(
|
|
42
|
+
options?: ProjectOptions<IncludeMembers, IncludeFeatures>,
|
|
43
|
+
) => Project<IncludeMembers, IncludeFeatures>
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import {type Project, type ProjectMember} from '@sanity/sdk'
|
|
2
|
+
import {expectTypeOf, test} from 'vitest'
|
|
3
|
+
|
|
4
|
+
import {useProjects} from './useProjects'
|
|
5
|
+
|
|
6
|
+
test('useProjects — no args: features included, members omitted', () => {
|
|
7
|
+
expectTypeOf(useProjects()).toEqualTypeOf<Project<false, true>[]>()
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
test('useProjects — includeMembers: true adds members to the type', () => {
|
|
11
|
+
expectTypeOf(useProjects({includeMembers: true})).toEqualTypeOf<Project<true, true>[]>()
|
|
12
|
+
type Result = ReturnType<typeof useProjects<true, true>>
|
|
13
|
+
expectTypeOf<Result[number]['members']>().toEqualTypeOf<ProjectMember[]>()
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
test('useProjects — includeFeatures: false drops features from the type', () => {
|
|
17
|
+
expectTypeOf(useProjects({includeFeatures: false})).toEqualTypeOf<Project<false, false>[]>()
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
test('useProjects — both flags true → both arrays present', () => {
|
|
21
|
+
expectTypeOf(useProjects({includeMembers: true, includeFeatures: true})).toEqualTypeOf<
|
|
22
|
+
Project<true, true>[]
|
|
23
|
+
>()
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
test('useProjects — both flags false → bare base shape', () => {
|
|
27
|
+
expectTypeOf(useProjects({includeMembers: false, includeFeatures: false})).toEqualTypeOf<
|
|
28
|
+
Project<false, false>[]
|
|
29
|
+
>()
|
|
30
|
+
type Result = ReturnType<typeof useProjects<false, false>>
|
|
31
|
+
expectTypeOf<Result[number]['id']>().toEqualTypeOf<string>()
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
test('useProjects — rejects non-boolean flag values', () => {
|
|
35
|
+
// @ts-expect-error — includeMembers must be a boolean
|
|
36
|
+
void useProjects({includeMembers: 'yes'})
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
test('useProjects — organizationId alone does not change the data shape', () => {
|
|
40
|
+
expectTypeOf(useProjects({organizationId: 'org_123'})).toEqualTypeOf<Project<false, true>[]>()
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
test('useProjects — non-literal boolean flag makes members optional', () => {
|
|
44
|
+
const includeMembers = false as boolean
|
|
45
|
+
expectTypeOf(useProjects({includeMembers})).toEqualTypeOf<Project<boolean, true>[]>()
|
|
46
|
+
type Result = ReturnType<typeof useProjects<boolean, true>>
|
|
47
|
+
expectTypeOf<Result[number]['members']>().toEqualTypeOf<ProjectMember[] | undefined>()
|
|
48
|
+
expectTypeOf<Pick<Result[number], 'members'>>().toEqualTypeOf<{members?: ProjectMember[]}>()
|
|
49
|
+
})
|