@sanity/sdk-react 2.14.1 → 2.16.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/README.md +40 -30
- package/dist/index.d.ts +512 -335
- package/dist/index.js +540 -332
- package/dist/index.js.map +1 -1
- package/package.json +37 -39
- package/src/_exports/sdk-react.ts +1 -0
- package/src/components/SDKProvider.test.tsx +135 -22
- package/src/components/SDKProvider.tsx +54 -17
- package/src/components/SanityApp.tsx +29 -0
- package/src/components/auth/AuthBoundary.recovery.test.tsx +86 -0
- package/src/components/auth/AuthBoundary.tsx +11 -1
- package/src/context/OrganizationResourcesProvider.test.tsx +189 -0
- package/src/context/OrganizationResourcesProvider.tsx +111 -0
- package/src/context/ProjectContext.ts +15 -0
- package/src/context/ResourceProvider.test.tsx +57 -1
- package/src/context/ResourceProvider.tsx +30 -9
- package/src/hooks/datasets/useDatasets.test.tsx +116 -0
- package/src/hooks/datasets/useDatasets.ts +20 -8
- package/src/hooks/document/useApplyDocumentActions.ts +1 -1
- package/src/hooks/document/useCreateDocument.test.tsx +83 -0
- package/src/hooks/document/useCreateDocument.ts +117 -0
- package/src/hooks/document/useDocument.ts +2 -2
- package/src/hooks/helpers/useResolvedProjectId.test.tsx +59 -0
- package/src/hooks/helpers/useResolvedProjectId.ts +35 -0
- package/src/hooks/projects/useProject.test.tsx +114 -0
- package/src/hooks/projects/useProject.ts +17 -7
- package/src/hooks/users/useUsers.test.tsx +101 -2
- package/src/hooks/users/useUsers.ts +35 -3
- package/src/utils/resolveOrgResources.test.ts +111 -0
- package/src/utils/resolveOrgResources.ts +69 -0
- package/src/hooks/datasets/useDatasets.test.ts +0 -80
- package/src/hooks/projects/useProject.test.ts +0 -80
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import {type SanityClient} from '@sanity/client'
|
|
2
|
+
import {getClientState, type SanityInstance, type StateSource} from '@sanity/sdk'
|
|
3
|
+
import {firstValueFrom} from 'rxjs'
|
|
4
|
+
import {beforeEach, describe, expect, it, vi} from 'vitest'
|
|
5
|
+
|
|
6
|
+
import {resolveOrgResources} from './resolveOrgResources'
|
|
7
|
+
|
|
8
|
+
vi.mock('@sanity/sdk', () => ({
|
|
9
|
+
getClientState: vi.fn(),
|
|
10
|
+
}))
|
|
11
|
+
|
|
12
|
+
vi.mock('rxjs', () => ({
|
|
13
|
+
firstValueFrom: vi.fn(),
|
|
14
|
+
}))
|
|
15
|
+
|
|
16
|
+
const mockGetClientState = vi.mocked(getClientState)
|
|
17
|
+
const mockFirstValueFrom = vi.mocked(firstValueFrom)
|
|
18
|
+
|
|
19
|
+
const mockRequest = vi.fn()
|
|
20
|
+
const mockClient = {request: mockRequest}
|
|
21
|
+
const mockInstance = {} as SanityInstance
|
|
22
|
+
|
|
23
|
+
beforeEach(() => {
|
|
24
|
+
vi.clearAllMocks()
|
|
25
|
+
mockGetClientState.mockReturnValue({
|
|
26
|
+
observable: 'mock-observable',
|
|
27
|
+
} as unknown as StateSource<SanityClient>)
|
|
28
|
+
mockFirstValueFrom.mockResolvedValue(mockClient as unknown as SanityClient)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
const ORG_ID = 'org-1'
|
|
32
|
+
|
|
33
|
+
describe('resolveOrgResources', () => {
|
|
34
|
+
it('returns both mediaLibrary and canvas when both requests succeed', async () => {
|
|
35
|
+
mockRequest
|
|
36
|
+
.mockResolvedValueOnce({data: [{id: 'ml-123'}]})
|
|
37
|
+
.mockResolvedValueOnce({data: [{id: 'canvas-456'}]})
|
|
38
|
+
|
|
39
|
+
const result = await resolveOrgResources(mockInstance, ORG_ID)
|
|
40
|
+
|
|
41
|
+
expect(result).toEqual({
|
|
42
|
+
mediaLibrary: {mediaLibraryId: 'ml-123'},
|
|
43
|
+
canvas: {canvasId: 'canvas-456'},
|
|
44
|
+
})
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('scopes both requests to the organization', async () => {
|
|
48
|
+
mockRequest
|
|
49
|
+
.mockResolvedValueOnce({data: [{id: 'ml-123'}]})
|
|
50
|
+
.mockResolvedValueOnce({data: [{id: 'canvas-456'}]})
|
|
51
|
+
|
|
52
|
+
await resolveOrgResources(mockInstance, ORG_ID)
|
|
53
|
+
|
|
54
|
+
expect(mockRequest).toHaveBeenCalledWith(
|
|
55
|
+
expect.objectContaining({uri: '/media-libraries', query: {organizationId: ORG_ID}}),
|
|
56
|
+
)
|
|
57
|
+
expect(mockRequest).toHaveBeenCalledWith(
|
|
58
|
+
expect.objectContaining({uri: '/canvases', query: {organizationId: ORG_ID}}),
|
|
59
|
+
)
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('returns only mediaLibrary when canvas request fails', async () => {
|
|
63
|
+
mockRequest
|
|
64
|
+
.mockResolvedValueOnce({data: [{id: 'ml-123'}]})
|
|
65
|
+
.mockRejectedValueOnce(new Error('canvas not found'))
|
|
66
|
+
|
|
67
|
+
const result = await resolveOrgResources(mockInstance, ORG_ID)
|
|
68
|
+
|
|
69
|
+
expect(result).toEqual({
|
|
70
|
+
mediaLibrary: {mediaLibraryId: 'ml-123'},
|
|
71
|
+
canvas: undefined,
|
|
72
|
+
})
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('returns only canvas when mediaLibrary request fails', async () => {
|
|
76
|
+
mockRequest
|
|
77
|
+
.mockRejectedValueOnce(new Error('media library not found'))
|
|
78
|
+
.mockResolvedValueOnce({data: [{id: 'canvas-456'}]})
|
|
79
|
+
|
|
80
|
+
const result = await resolveOrgResources(mockInstance, ORG_ID)
|
|
81
|
+
|
|
82
|
+
expect(result).toEqual({
|
|
83
|
+
mediaLibrary: undefined,
|
|
84
|
+
canvas: {canvasId: 'canvas-456'},
|
|
85
|
+
})
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('returns undefined for both when both requests fail', async () => {
|
|
89
|
+
mockRequest
|
|
90
|
+
.mockRejectedValueOnce(new Error('error 1'))
|
|
91
|
+
.mockRejectedValueOnce(new Error('error 2'))
|
|
92
|
+
|
|
93
|
+
const result = await resolveOrgResources(mockInstance, ORG_ID)
|
|
94
|
+
|
|
95
|
+
expect(result).toEqual({
|
|
96
|
+
mediaLibrary: undefined,
|
|
97
|
+
canvas: undefined,
|
|
98
|
+
})
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it('returns undefined for both when data arrays are empty', async () => {
|
|
102
|
+
mockRequest.mockResolvedValueOnce({data: []}).mockResolvedValueOnce({data: []})
|
|
103
|
+
|
|
104
|
+
const result = await resolveOrgResources(mockInstance, ORG_ID)
|
|
105
|
+
|
|
106
|
+
expect(result).toEqual({
|
|
107
|
+
mediaLibrary: undefined,
|
|
108
|
+
canvas: undefined,
|
|
109
|
+
})
|
|
110
|
+
})
|
|
111
|
+
})
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type CanvasResource,
|
|
3
|
+
getClientState,
|
|
4
|
+
type MediaLibraryResource,
|
|
5
|
+
type SanityInstance,
|
|
6
|
+
} from '@sanity/sdk'
|
|
7
|
+
import {firstValueFrom} from 'rxjs'
|
|
8
|
+
|
|
9
|
+
const API_VERSION = 'v2026-07-09'
|
|
10
|
+
|
|
11
|
+
interface OrgResourcesApiItem {
|
|
12
|
+
id: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface OrgResourcesApiResponse {
|
|
16
|
+
data: OrgResourcesApiItem[]
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface OrgResources {
|
|
20
|
+
mediaLibrary?: MediaLibraryResource
|
|
21
|
+
canvas?: CanvasResource
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Gets the id of the first resource from a settled request result. The request
|
|
26
|
+
* is already scoped to a single organization, so the first item is the one we want.
|
|
27
|
+
*/
|
|
28
|
+
function getFirstResourceId(
|
|
29
|
+
result: PromiseSettledResult<OrgResourcesApiResponse>,
|
|
30
|
+
): string | undefined {
|
|
31
|
+
if (result.status !== 'fulfilled') return undefined
|
|
32
|
+
return result.value.data?.[0]?.id
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Fetches the media library and canvas resources for the given organization.
|
|
37
|
+
* Both requests are scoped to `organizationId` so each response contains only
|
|
38
|
+
* that org's resources. Each resource is fetched independently — a failure for
|
|
39
|
+
* one does not prevent the other from resolving.
|
|
40
|
+
*/
|
|
41
|
+
export async function resolveOrgResources(
|
|
42
|
+
instance: SanityInstance,
|
|
43
|
+
organizationId: string,
|
|
44
|
+
): Promise<OrgResources> {
|
|
45
|
+
const client = await firstValueFrom(
|
|
46
|
+
getClientState(instance, {apiVersion: API_VERSION, scope: 'global'}).observable,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
const [mediaLibrariesResult, canvasesResult] = await Promise.allSettled([
|
|
50
|
+
client.request<OrgResourcesApiResponse>({
|
|
51
|
+
uri: `/media-libraries`,
|
|
52
|
+
query: {organizationId},
|
|
53
|
+
tag: 'org-resources.media-libraries',
|
|
54
|
+
}),
|
|
55
|
+
client.request<OrgResourcesApiResponse>({
|
|
56
|
+
uri: `/canvases`,
|
|
57
|
+
query: {organizationId},
|
|
58
|
+
tag: 'org-resources.canvases',
|
|
59
|
+
}),
|
|
60
|
+
])
|
|
61
|
+
|
|
62
|
+
const mediaLibraryId = getFirstResourceId(mediaLibrariesResult)
|
|
63
|
+
const canvasId = getFirstResourceId(canvasesResult)
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
mediaLibrary: mediaLibraryId ? {mediaLibraryId} : undefined,
|
|
67
|
+
canvas: canvasId ? {canvasId} : undefined,
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
import {getDatasetsState, type ProjectHandle, type SanityInstance} from '@sanity/sdk'
|
|
2
|
-
import {beforeEach, describe, expect, it, vi} from 'vitest'
|
|
3
|
-
|
|
4
|
-
import {createStateSourceHook} from '../helpers/createStateSourceHook'
|
|
5
|
-
|
|
6
|
-
// Mock dependencies
|
|
7
|
-
vi.mock('@sanity/sdk', () => ({
|
|
8
|
-
getDatasetsState: vi.fn(() => ({
|
|
9
|
-
getCurrent: vi.fn(() => undefined), // Mocking getCurrent to satisfy the call within shouldSuspend
|
|
10
|
-
})),
|
|
11
|
-
resolveDatasets: vi.fn(),
|
|
12
|
-
}))
|
|
13
|
-
vi.mock('../helpers/createStateSourceHook', () => ({
|
|
14
|
-
createStateSourceHook: vi.fn(),
|
|
15
|
-
}))
|
|
16
|
-
|
|
17
|
-
describe('useDatasets', () => {
|
|
18
|
-
// Use beforeEach to reset modules and ensure mocks are fresh for each test
|
|
19
|
-
beforeEach(() => {
|
|
20
|
-
vi.resetModules()
|
|
21
|
-
// Re-mock dependencies for each test after resetModules
|
|
22
|
-
vi.mock('@sanity/sdk', () => ({
|
|
23
|
-
getDatasetsState: vi.fn(() => ({
|
|
24
|
-
getCurrent: vi.fn(() => undefined),
|
|
25
|
-
})),
|
|
26
|
-
resolveDatasets: vi.fn(),
|
|
27
|
-
}))
|
|
28
|
-
vi.mock('../helpers/createStateSourceHook', () => ({
|
|
29
|
-
createStateSourceHook: vi.fn(),
|
|
30
|
-
}))
|
|
31
|
-
})
|
|
32
|
-
|
|
33
|
-
it('should call createStateSourceHook with correct arguments on import', async () => {
|
|
34
|
-
// Dynamically import the hook *after* mocks are set up and modules reset
|
|
35
|
-
await import('./useDatasets')
|
|
36
|
-
|
|
37
|
-
// Check if createStateSourceHook was called during the module evaluation (import)
|
|
38
|
-
expect(createStateSourceHook).toHaveBeenCalled()
|
|
39
|
-
expect(createStateSourceHook).toHaveBeenCalledWith(
|
|
40
|
-
expect.objectContaining({
|
|
41
|
-
getState: expect.any(Function),
|
|
42
|
-
shouldSuspend: expect.any(Function),
|
|
43
|
-
suspender: expect.any(Function), // Actual function reference doesn't matter here as it's mocked
|
|
44
|
-
getConfig: expect.any(Function), // Actual function reference doesn't matter here
|
|
45
|
-
}),
|
|
46
|
-
)
|
|
47
|
-
})
|
|
48
|
-
|
|
49
|
-
it('shouldSuspend should call getDatasetsState and getCurrent', async () => {
|
|
50
|
-
// Dynamically import the hook *after* mocks are set up and modules reset
|
|
51
|
-
await import('./useDatasets')
|
|
52
|
-
|
|
53
|
-
// Get the arguments passed to createStateSourceHook
|
|
54
|
-
// Need to ensure createStateSourceHook mock is correctly typed for access
|
|
55
|
-
const mockCreateStateSourceHook = createStateSourceHook as ReturnType<typeof vi.fn>
|
|
56
|
-
expect(mockCreateStateSourceHook.mock.calls.length).toBeGreaterThan(0)
|
|
57
|
-
const createStateSourceHookArgs = mockCreateStateSourceHook.mock.calls[0][0]
|
|
58
|
-
const shouldSuspend = createStateSourceHookArgs.shouldSuspend
|
|
59
|
-
|
|
60
|
-
// Mock instance and projectHandle for the test call
|
|
61
|
-
const mockInstance = {} as SanityInstance // Use specific type
|
|
62
|
-
const mockProjectHandle = {} as ProjectHandle // Use specific type
|
|
63
|
-
|
|
64
|
-
// Call the shouldSuspend function
|
|
65
|
-
const result = shouldSuspend(mockInstance, mockProjectHandle)
|
|
66
|
-
|
|
67
|
-
// Assert that getDatasetsState was called with the correct arguments
|
|
68
|
-
// Need to ensure getDatasetsState mock is correctly typed for access
|
|
69
|
-
const mockGetDatasetsState = getDatasetsState as ReturnType<typeof vi.fn>
|
|
70
|
-
expect(mockGetDatasetsState).toHaveBeenCalledWith(mockInstance, mockProjectHandle)
|
|
71
|
-
|
|
72
|
-
// Assert that getCurrent was called on the result of getDatasetsState
|
|
73
|
-
expect(mockGetDatasetsState.mock.results.length).toBeGreaterThan(0)
|
|
74
|
-
const getDatasetsStateMockResult = mockGetDatasetsState.mock.results[0].value
|
|
75
|
-
expect(getDatasetsStateMockResult.getCurrent).toHaveBeenCalled()
|
|
76
|
-
|
|
77
|
-
// Assert the result of shouldSuspend based on the mocked getCurrent value
|
|
78
|
-
expect(result).toBe(true) // Since getCurrent is mocked to return undefined
|
|
79
|
-
})
|
|
80
|
-
})
|
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
import {getProjectState, type ProjectHandle, type SanityInstance} from '@sanity/sdk'
|
|
2
|
-
import {beforeEach, describe, expect, it, vi} from 'vitest'
|
|
3
|
-
|
|
4
|
-
import {createStateSourceHook} from '../helpers/createStateSourceHook'
|
|
5
|
-
|
|
6
|
-
// Mock dependencies
|
|
7
|
-
vi.mock('@sanity/sdk', () => ({
|
|
8
|
-
getProjectState: vi.fn(() => ({
|
|
9
|
-
getCurrent: vi.fn(() => undefined), // Mocking getCurrent to satisfy the call within shouldSuspend
|
|
10
|
-
})),
|
|
11
|
-
resolveProject: vi.fn(),
|
|
12
|
-
}))
|
|
13
|
-
vi.mock('../helpers/createStateSourceHook', () => ({
|
|
14
|
-
createStateSourceHook: vi.fn(),
|
|
15
|
-
}))
|
|
16
|
-
|
|
17
|
-
describe('useProject', () => {
|
|
18
|
-
// Use beforeEach to reset modules and ensure mocks are fresh for each test
|
|
19
|
-
beforeEach(() => {
|
|
20
|
-
vi.resetModules()
|
|
21
|
-
// Re-mock dependencies for each test after resetModules
|
|
22
|
-
vi.mock('@sanity/sdk', () => ({
|
|
23
|
-
getProjectState: vi.fn(() => ({
|
|
24
|
-
getCurrent: vi.fn(() => undefined),
|
|
25
|
-
})),
|
|
26
|
-
resolveProject: vi.fn(),
|
|
27
|
-
}))
|
|
28
|
-
vi.mock('../helpers/createStateSourceHook', () => ({
|
|
29
|
-
createStateSourceHook: vi.fn(),
|
|
30
|
-
}))
|
|
31
|
-
})
|
|
32
|
-
|
|
33
|
-
it('should call createStateSourceHook with correct arguments on import', async () => {
|
|
34
|
-
// Dynamically import the hook *after* mocks are set up and modules reset
|
|
35
|
-
await import('./useProject')
|
|
36
|
-
|
|
37
|
-
// Check if createStateSourceHook was called during the module evaluation (import)
|
|
38
|
-
expect(createStateSourceHook).toHaveBeenCalled()
|
|
39
|
-
expect(createStateSourceHook).toHaveBeenCalledWith(
|
|
40
|
-
expect.objectContaining({
|
|
41
|
-
getState: expect.any(Function),
|
|
42
|
-
shouldSuspend: expect.any(Function),
|
|
43
|
-
suspender: expect.any(Function), // Actual function reference doesn't matter here as it's mocked
|
|
44
|
-
getConfig: expect.any(Function), // Actual function reference doesn't matter here
|
|
45
|
-
}),
|
|
46
|
-
)
|
|
47
|
-
})
|
|
48
|
-
|
|
49
|
-
it('shouldSuspend should call getProjectState and getCurrent', async () => {
|
|
50
|
-
// Dynamically import the hook *after* mocks are set up and modules reset
|
|
51
|
-
await import('./useProject')
|
|
52
|
-
|
|
53
|
-
// Get the arguments passed to createStateSourceHook
|
|
54
|
-
// Need to ensure createStateSourceHook mock is correctly typed for access
|
|
55
|
-
const mockCreateStateSourceHook = createStateSourceHook as ReturnType<typeof vi.fn>
|
|
56
|
-
expect(mockCreateStateSourceHook.mock.calls.length).toBeGreaterThan(0)
|
|
57
|
-
const createStateSourceHookArgs = mockCreateStateSourceHook.mock.calls[0][0]
|
|
58
|
-
const shouldSuspend = createStateSourceHookArgs.shouldSuspend
|
|
59
|
-
|
|
60
|
-
// Mock instance and projectHandle for the test call
|
|
61
|
-
const mockInstance = {} as SanityInstance // Use specific type
|
|
62
|
-
const mockProjectHandle = {} as ProjectHandle // Use specific type
|
|
63
|
-
|
|
64
|
-
// Call the shouldSuspend function
|
|
65
|
-
const result = shouldSuspend(mockInstance, mockProjectHandle)
|
|
66
|
-
|
|
67
|
-
// Assert that getProjectState was called with the correct arguments
|
|
68
|
-
// Need to ensure getProjectState mock is correctly typed for access
|
|
69
|
-
const mockGetProjectState = getProjectState as ReturnType<typeof vi.fn>
|
|
70
|
-
expect(mockGetProjectState).toHaveBeenCalledWith(mockInstance, mockProjectHandle)
|
|
71
|
-
|
|
72
|
-
// Assert that getCurrent was called on the result of getProjectState
|
|
73
|
-
expect(mockGetProjectState.mock.results.length).toBeGreaterThan(0)
|
|
74
|
-
const getProjectStateMockResult = mockGetProjectState.mock.results[0].value
|
|
75
|
-
expect(getProjectStateMockResult.getCurrent).toHaveBeenCalled()
|
|
76
|
-
|
|
77
|
-
// Assert the result of shouldSuspend based on the mocked getCurrent value
|
|
78
|
-
expect(result).toBe(true) // Since getCurrent is mocked to return undefined
|
|
79
|
-
})
|
|
80
|
-
})
|