@sanity/sdk-react 0.0.0-alpha.2 → 0.0.0-alpha.21
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 +38 -67
- package/dist/index.d.ts +4811 -2
- package/dist/index.js +1069 -2
- package/dist/index.js.map +1 -1
- package/package.json +27 -58
- package/src/_exports/index.ts +66 -10
- package/src/components/Login/LoginLinks.test.tsx +4 -14
- package/src/components/Login/LoginLinks.tsx +16 -31
- package/src/components/SDKProvider.test.tsx +79 -0
- package/src/components/SDKProvider.tsx +42 -0
- package/src/components/SanityApp.test.tsx +156 -0
- package/src/components/SanityApp.tsx +90 -0
- package/src/components/auth/AuthBoundary.test.tsx +6 -19
- package/src/components/auth/AuthBoundary.tsx +20 -4
- package/src/components/auth/Login.test.tsx +2 -16
- package/src/components/auth/Login.tsx +11 -30
- package/src/components/auth/LoginCallback.test.tsx +5 -20
- package/src/components/auth/LoginCallback.tsx +9 -14
- package/src/components/auth/LoginError.test.tsx +2 -17
- package/src/components/auth/LoginError.tsx +11 -16
- package/src/components/auth/LoginFooter.test.tsx +2 -16
- package/src/components/auth/LoginFooter.tsx +8 -24
- package/src/components/auth/LoginLayout.test.tsx +2 -16
- package/src/components/auth/LoginLayout.tsx +8 -38
- package/src/components/auth/authTestHelpers.tsx +11 -0
- package/src/components/utils.ts +22 -0
- package/src/context/SanityInstanceContext.ts +4 -0
- package/src/{components/context → context}/SanityProvider.test.tsx +2 -2
- package/src/context/SanityProvider.tsx +50 -0
- package/src/hooks/_synchronous-groq-js.mjs +4 -0
- package/src/hooks/auth/useAuthState.tsx +4 -5
- package/src/hooks/auth/useAuthToken.tsx +1 -1
- package/src/hooks/auth/useCurrentUser.tsx +28 -4
- package/src/hooks/auth/useDashboardOrganizationId.test.tsx +42 -0
- package/src/hooks/auth/useDashboardOrganizationId.tsx +29 -0
- package/src/hooks/auth/useHandleAuthCallback.test.tsx +16 -0
- package/src/hooks/auth/{useHandleCallback.tsx → useHandleAuthCallback.tsx} +7 -6
- package/src/hooks/auth/useLogOut.test.tsx +2 -2
- package/src/hooks/auth/useLogOut.tsx +1 -1
- package/src/hooks/auth/useLoginUrls.tsx +1 -0
- package/src/hooks/client/useClient.ts +9 -30
- package/src/hooks/comlink/useFrameConnection.test.tsx +167 -0
- package/src/hooks/comlink/useFrameConnection.ts +107 -0
- package/src/hooks/comlink/useManageFavorite.test.ts +111 -0
- package/src/hooks/comlink/useManageFavorite.ts +130 -0
- package/src/hooks/comlink/useRecordDocumentHistoryEvent.test.ts +81 -0
- package/src/hooks/comlink/useRecordDocumentHistoryEvent.ts +106 -0
- package/src/hooks/comlink/useWindowConnection.test.ts +135 -0
- package/src/hooks/comlink/useWindowConnection.ts +122 -0
- package/src/hooks/context/useSanityInstance.test.tsx +2 -2
- package/src/hooks/context/useSanityInstance.ts +24 -8
- package/src/hooks/dashboard/useNavigateToStudioDocument.test.ts +178 -0
- package/src/hooks/dashboard/useNavigateToStudioDocument.ts +123 -0
- package/src/hooks/dashboard/useStudioWorkspacesByResourceId.test.tsx +278 -0
- package/src/hooks/dashboard/useStudioWorkspacesByResourceId.ts +92 -0
- package/src/hooks/datasets/useDatasets.ts +40 -0
- package/src/hooks/document/useApplyDocumentActions.test.ts +25 -0
- package/src/hooks/document/useApplyDocumentActions.ts +75 -0
- package/src/hooks/document/useDocument.test.ts +81 -0
- package/src/hooks/document/useDocument.ts +107 -0
- package/src/hooks/document/useDocumentEvent.test.ts +63 -0
- package/src/hooks/document/useDocumentEvent.ts +54 -0
- package/src/hooks/document/useDocumentPermissions.ts +84 -0
- package/src/hooks/document/useDocumentSyncStatus.test.ts +16 -0
- package/src/hooks/document/useDocumentSyncStatus.ts +33 -0
- package/src/hooks/document/useEditDocument.test.ts +179 -0
- package/src/hooks/document/useEditDocument.ts +195 -0
- package/src/hooks/documents/useDocuments.test.tsx +152 -0
- package/src/hooks/documents/useDocuments.ts +174 -0
- package/src/hooks/helpers/createCallbackHook.tsx +3 -2
- package/src/hooks/helpers/createStateSourceHook.test.tsx +66 -0
- package/src/hooks/helpers/createStateSourceHook.tsx +29 -10
- package/src/hooks/paginatedDocuments/usePaginatedDocuments.test.tsx +259 -0
- package/src/hooks/paginatedDocuments/usePaginatedDocuments.ts +290 -0
- package/src/hooks/preview/usePreview.test.tsx +19 -10
- package/src/hooks/preview/usePreview.tsx +67 -13
- package/src/hooks/projection/useProjection.test.tsx +218 -0
- package/src/hooks/projection/useProjection.ts +147 -0
- package/src/hooks/projects/useProject.ts +48 -0
- package/src/hooks/projects/useProjects.ts +45 -0
- package/src/hooks/query/useQuery.test.tsx +188 -0
- package/src/hooks/query/useQuery.ts +103 -0
- package/src/hooks/users/useUsers.test.ts +163 -0
- package/src/hooks/users/useUsers.ts +107 -0
- package/src/utils/getEnv.ts +21 -0
- package/src/version.ts +8 -0
- package/src/vite-env.d.ts +10 -0
- package/dist/_chunks-es/useLogOut.js +0 -44
- package/dist/_chunks-es/useLogOut.js.map +0 -1
- package/dist/assets/bundle-CcAyERuZ.css +0 -11
- package/dist/components.d.ts +0 -257
- package/dist/components.js +0 -316
- package/dist/components.js.map +0 -1
- package/dist/hooks.d.ts +0 -187
- package/dist/hooks.js +0 -81
- package/dist/hooks.js.map +0 -1
- package/src/_exports/components.ts +0 -13
- package/src/_exports/hooks.ts +0 -9
- package/src/components/DocumentGridLayout/DocumentGridLayout.stories.tsx +0 -113
- package/src/components/DocumentGridLayout/DocumentGridLayout.test.tsx +0 -42
- package/src/components/DocumentGridLayout/DocumentGridLayout.tsx +0 -21
- package/src/components/DocumentListLayout/DocumentListLayout.stories.tsx +0 -105
- package/src/components/DocumentListLayout/DocumentListLayout.test.tsx +0 -42
- package/src/components/DocumentListLayout/DocumentListLayout.tsx +0 -12
- package/src/components/DocumentPreviewLayout/DocumentPreviewLayout.md +0 -49
- package/src/components/DocumentPreviewLayout/DocumentPreviewLayout.stories.tsx +0 -39
- package/src/components/DocumentPreviewLayout/DocumentPreviewLayout.test.tsx +0 -30
- package/src/components/DocumentPreviewLayout/DocumentPreviewLayout.tsx +0 -171
- package/src/components/context/SanityProvider.tsx +0 -42
- package/src/css/css.config.js +0 -220
- package/src/css/paramour.css +0 -2347
- package/src/css/styles.css +0 -11
- package/src/hooks/auth/useHandleCallback.test.tsx +0 -16
- package/src/hooks/client/useClient.test.tsx +0 -130
- package/src/hooks/documentCollection/useDocuments.test.ts +0 -130
- package/src/hooks/documentCollection/useDocuments.ts +0 -87
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import {type DatasetsResponse} from '@sanity/client'
|
|
2
|
+
import {getDatasetsState, resolveDatasets, type SanityInstance, type StateSource} from '@sanity/sdk'
|
|
3
|
+
|
|
4
|
+
import {createStateSourceHook} from '../helpers/createStateSourceHook'
|
|
5
|
+
|
|
6
|
+
type UseDatasets = {
|
|
7
|
+
/**
|
|
8
|
+
*
|
|
9
|
+
* Returns metadata for each dataset the current user has access to.
|
|
10
|
+
*
|
|
11
|
+
* @category Datasets
|
|
12
|
+
* @returns The metadata for your the datasets
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```tsx
|
|
16
|
+
* const datasets = useDatasets()
|
|
17
|
+
*
|
|
18
|
+
* return (
|
|
19
|
+
* <select>
|
|
20
|
+
* {datasets.map((dataset) => (
|
|
21
|
+
* <option key={dataset.name}>{dataset.name}</option>
|
|
22
|
+
* ))}
|
|
23
|
+
* </select>
|
|
24
|
+
* )
|
|
25
|
+
* ```
|
|
26
|
+
*
|
|
27
|
+
*/
|
|
28
|
+
(): DatasetsResponse
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @public
|
|
33
|
+
* @function
|
|
34
|
+
*/
|
|
35
|
+
export const useDatasets: UseDatasets = createStateSourceHook({
|
|
36
|
+
// remove `undefined` since we're suspending when that is the case
|
|
37
|
+
getState: getDatasetsState as (instance: SanityInstance) => StateSource<DatasetsResponse>,
|
|
38
|
+
shouldSuspend: (instance) => getDatasetsState(instance).getCurrent() === undefined,
|
|
39
|
+
suspender: resolveDatasets,
|
|
40
|
+
})
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import {applyDocumentActions, createDocument, type ResourceId} from '@sanity/sdk'
|
|
2
|
+
import {describe, it} from 'vitest'
|
|
3
|
+
|
|
4
|
+
import {createCallbackHook} from '../helpers/createCallbackHook'
|
|
5
|
+
|
|
6
|
+
vi.mock('../helpers/createCallbackHook', () => ({
|
|
7
|
+
createCallbackHook: vi.fn((cb) => () => cb),
|
|
8
|
+
}))
|
|
9
|
+
vi.mock('@sanity/sdk', async (importOriginal) => {
|
|
10
|
+
const original = await importOriginal<typeof import('@sanity/sdk')>()
|
|
11
|
+
return {...original, applyDocumentActions: vi.fn()}
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
describe('useApplyDocumentActions', () => {
|
|
15
|
+
it('calls `createCallbackHook` with `applyDocumentActions`', async () => {
|
|
16
|
+
const {useApplyDocumentActions} = await import('./useApplyDocumentActions')
|
|
17
|
+
const resourceId: ResourceId = 'project1.dataset1'
|
|
18
|
+
expect(createCallbackHook).not.toHaveBeenCalled()
|
|
19
|
+
|
|
20
|
+
expect(applyDocumentActions).not.toHaveBeenCalled()
|
|
21
|
+
const apply = useApplyDocumentActions(resourceId)
|
|
22
|
+
apply(createDocument({_type: 'author'}))
|
|
23
|
+
expect(applyDocumentActions).toHaveBeenCalledWith(createDocument({_type: 'author'}))
|
|
24
|
+
})
|
|
25
|
+
})
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type ActionsResult,
|
|
3
|
+
applyDocumentActions,
|
|
4
|
+
type ApplyDocumentActionsOptions,
|
|
5
|
+
type DocumentAction,
|
|
6
|
+
type ResourceId,
|
|
7
|
+
} from '@sanity/sdk'
|
|
8
|
+
import {type SanityDocument} from '@sanity/types'
|
|
9
|
+
|
|
10
|
+
import {createCallbackHook} from '../helpers/createCallbackHook'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
*
|
|
14
|
+
* @beta
|
|
15
|
+
*
|
|
16
|
+
* Provides a callback for applying one or more actions to a document.
|
|
17
|
+
*
|
|
18
|
+
* @category Documents
|
|
19
|
+
* @param resourceId - The resource ID of the document to apply actions to. If not provided, the document will use the default resource.
|
|
20
|
+
* @returns A function that takes one more more {@link DocumentAction}s and returns a promise that resolves to an {@link ActionsResult}.
|
|
21
|
+
* @example Publish or unpublish a document
|
|
22
|
+
* ```
|
|
23
|
+
* import { publishDocument, unpublishDocument } from '@sanity/sdk'
|
|
24
|
+
* import { useApplyDocumentActions } from '@sanity/sdk-react'
|
|
25
|
+
*
|
|
26
|
+
* const apply = useApplyDocumentActions()
|
|
27
|
+
* const myDocument = { _id: 'my-document-id', _type: 'my-document-type' }
|
|
28
|
+
*
|
|
29
|
+
* return (
|
|
30
|
+
* <button onClick={() => apply(publishDocument(myDocument))}>Publish</button>
|
|
31
|
+
* <button onClick={() => apply(unpublishDocument(myDocument))}>Unpublish</button>
|
|
32
|
+
* )
|
|
33
|
+
* ```
|
|
34
|
+
*
|
|
35
|
+
* @example Create and publish a new document
|
|
36
|
+
* ```
|
|
37
|
+
* import { createDocument, publishDocument } from '@sanity/sdk'
|
|
38
|
+
* import { useApplyDocumentActions } from '@sanity/sdk-react'
|
|
39
|
+
*
|
|
40
|
+
* const apply = useApplyDocumentActions()
|
|
41
|
+
*
|
|
42
|
+
* const handleCreateAndPublish = () => {
|
|
43
|
+
* const handle = { _id: window.crypto.randomUUID(), _type: 'my-document-type' }
|
|
44
|
+
* apply([
|
|
45
|
+
* createDocument(handle),
|
|
46
|
+
* publishDocument(handle),
|
|
47
|
+
* ])
|
|
48
|
+
* }
|
|
49
|
+
*
|
|
50
|
+
* return (
|
|
51
|
+
* <button onClick={handleCreateAndPublish}>
|
|
52
|
+
* I’m feeling lucky
|
|
53
|
+
* </button>
|
|
54
|
+
* )
|
|
55
|
+
* ```
|
|
56
|
+
*/
|
|
57
|
+
export function useApplyDocumentActions(
|
|
58
|
+
resourceId?: ResourceId,
|
|
59
|
+
): <TDocument extends SanityDocument>(
|
|
60
|
+
action: DocumentAction<TDocument> | DocumentAction<TDocument>[],
|
|
61
|
+
options?: ApplyDocumentActionsOptions,
|
|
62
|
+
) => Promise<ActionsResult<TDocument>>
|
|
63
|
+
|
|
64
|
+
/** @beta */
|
|
65
|
+
export function useApplyDocumentActions(
|
|
66
|
+
resourceId?: ResourceId,
|
|
67
|
+
): (
|
|
68
|
+
action: DocumentAction | DocumentAction[],
|
|
69
|
+
options?: ApplyDocumentActionsOptions,
|
|
70
|
+
) => Promise<ActionsResult> {
|
|
71
|
+
return _useApplyDocumentActions(resourceId)()
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const _useApplyDocumentActions = (resourceId?: ResourceId) =>
|
|
75
|
+
createCallbackHook(applyDocumentActions, resourceId)
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// tests/useDocument.test.ts
|
|
2
|
+
import {
|
|
3
|
+
createSanityInstance,
|
|
4
|
+
getDocumentState,
|
|
5
|
+
resolveDocument,
|
|
6
|
+
type StateSource,
|
|
7
|
+
} from '@sanity/sdk'
|
|
8
|
+
import {type SanityDocument} from '@sanity/types'
|
|
9
|
+
import {renderHook} from '@testing-library/react'
|
|
10
|
+
import {beforeEach, describe, expect, it, vi} from 'vitest'
|
|
11
|
+
|
|
12
|
+
import {useSanityInstance} from '../context/useSanityInstance'
|
|
13
|
+
import {useDocument} from './useDocument'
|
|
14
|
+
|
|
15
|
+
vi.mock('@sanity/sdk', async (importOriginal) => {
|
|
16
|
+
const original = await importOriginal<typeof import('@sanity/sdk')>()
|
|
17
|
+
return {...original, getDocumentState: vi.fn(), resolveDocument: vi.fn()}
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
vi.mock('../context/useSanityInstance', () => ({
|
|
21
|
+
useSanityInstance: vi.fn(),
|
|
22
|
+
}))
|
|
23
|
+
|
|
24
|
+
// Create a fake instance to be returned by useSanityInstance.
|
|
25
|
+
const instance = createSanityInstance({projectId: 'p', dataset: 'd'})
|
|
26
|
+
const doc: SanityDocument = {
|
|
27
|
+
_id: 'doc1',
|
|
28
|
+
foo: 'bar',
|
|
29
|
+
_type: 'book',
|
|
30
|
+
_rev: 'tx0',
|
|
31
|
+
_createdAt: '2025-02-06T00:11:00.000Z',
|
|
32
|
+
_updatedAt: '2025-02-06T00:11:00.000Z',
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
describe('useDocument hook', () => {
|
|
36
|
+
beforeEach(() => {
|
|
37
|
+
vi.resetAllMocks()
|
|
38
|
+
vi.mocked(useSanityInstance).mockReturnValue(instance)
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('returns the current document when ready (without a path)', () => {
|
|
42
|
+
const getCurrent = vi.fn().mockReturnValue(doc)
|
|
43
|
+
const subscribe = vi.fn().mockReturnValue(vi.fn())
|
|
44
|
+
vi.mocked(getDocumentState).mockReturnValue({
|
|
45
|
+
getCurrent,
|
|
46
|
+
subscribe,
|
|
47
|
+
} as unknown as StateSource<unknown>)
|
|
48
|
+
|
|
49
|
+
const {result} = renderHook(() => useDocument({_id: 'doc1', _type: 'book'}))
|
|
50
|
+
|
|
51
|
+
expect(result.current).toEqual(doc)
|
|
52
|
+
expect(getCurrent).toHaveBeenCalled()
|
|
53
|
+
expect(subscribe).toHaveBeenCalled()
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('throws a promise (suspends) when the document is not ready', () => {
|
|
57
|
+
const getCurrent = vi.fn().mockReturnValue(undefined)
|
|
58
|
+
const subscribe = vi.fn().mockReturnValue(vi.fn())
|
|
59
|
+
vi.mocked(getDocumentState).mockReturnValue({
|
|
60
|
+
getCurrent,
|
|
61
|
+
subscribe,
|
|
62
|
+
} as unknown as StateSource<unknown>)
|
|
63
|
+
|
|
64
|
+
const resolveDocPromise = Promise.resolve(doc)
|
|
65
|
+
|
|
66
|
+
// Also, simulate resolveDocument to return a known promise.
|
|
67
|
+
vi.mocked(resolveDocument).mockReturnValue(resolveDocPromise)
|
|
68
|
+
|
|
69
|
+
// Render the hook and capture the thrown promise.
|
|
70
|
+
const {result} = renderHook(() => {
|
|
71
|
+
try {
|
|
72
|
+
return useDocument({_id: 'doc1', _type: 'book'})
|
|
73
|
+
} catch (e) {
|
|
74
|
+
return e
|
|
75
|
+
}
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
// When the document is not ready, the hook throws the promise from resolveDocument.
|
|
79
|
+
expect(result.current).toBe(resolveDocPromise)
|
|
80
|
+
})
|
|
81
|
+
})
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type DocumentHandle,
|
|
3
|
+
getDocumentState,
|
|
4
|
+
getResourceId,
|
|
5
|
+
type JsonMatch,
|
|
6
|
+
type JsonMatchPath,
|
|
7
|
+
resolveDocument,
|
|
8
|
+
} from '@sanity/sdk'
|
|
9
|
+
import {type SanityDocument} from '@sanity/types'
|
|
10
|
+
|
|
11
|
+
import {createStateSourceHook} from '../helpers/createStateSourceHook'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @beta
|
|
15
|
+
*
|
|
16
|
+
* ## useDocument(doc, path)
|
|
17
|
+
* Read and subscribe to nested values in a document
|
|
18
|
+
* @category Documents
|
|
19
|
+
* @param doc - The document to read state from. If you pass a `DocumentHandle` with a `resourceId` in the DocumentResourceId format (`document:projectId.dataset:documentId`)
|
|
20
|
+
* the document will be read from the specified Sanity project and dataset that is included in the handle. If no `resourceId` is provided, the default project and dataset from your `SanityApp` configuration will be used.
|
|
21
|
+
* @param path - The path to the nested value to read from
|
|
22
|
+
* @returns The value at the specified path
|
|
23
|
+
* @example
|
|
24
|
+
* ```tsx
|
|
25
|
+
* import {type DocumentHandle, useDocument} from '@sanity/sdk-react'
|
|
26
|
+
*
|
|
27
|
+
* function OrderLink({documentHandle}: {documentHandle: DocumentHandle}) {
|
|
28
|
+
* const title = useDocument(documentHandle, 'title')
|
|
29
|
+
* const id = useDocument(documentHandle, '_id')
|
|
30
|
+
*
|
|
31
|
+
* return (
|
|
32
|
+
* <a href=`/order/${id}`>Order {title} today!</a>
|
|
33
|
+
* )
|
|
34
|
+
* }
|
|
35
|
+
* ```
|
|
36
|
+
*
|
|
37
|
+
*/
|
|
38
|
+
export function useDocument<
|
|
39
|
+
TDocument extends SanityDocument,
|
|
40
|
+
TPath extends JsonMatchPath<TDocument>,
|
|
41
|
+
>(doc: DocumentHandle<TDocument>, path: TPath): JsonMatch<TDocument, TPath> | undefined
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* @beta
|
|
45
|
+
* ## useDocument(doc)
|
|
46
|
+
* Read and subscribe to an entire document
|
|
47
|
+
* @param doc - The document to read state from
|
|
48
|
+
* @returns The document state as an object
|
|
49
|
+
* @example
|
|
50
|
+
* ```tsx
|
|
51
|
+
* import {type SanityDocument, type DocumentHandle, useDocument} from '@sanity/sdk-react'
|
|
52
|
+
*
|
|
53
|
+
* interface Book extends SanityDocument {
|
|
54
|
+
* title: string
|
|
55
|
+
* author: string
|
|
56
|
+
* summary: string
|
|
57
|
+
* }
|
|
58
|
+
*
|
|
59
|
+
* function DocumentView({documentHandle}: {documentHandle: DocumentHandle}) {
|
|
60
|
+
* const book = useDocument<Book>(documentHandle)
|
|
61
|
+
*
|
|
62
|
+
* return (
|
|
63
|
+
* <article>
|
|
64
|
+
* <h1>{book?.title}</h1>
|
|
65
|
+
* <address>By {book?.author}</address>
|
|
66
|
+
*
|
|
67
|
+
* <h2>Summary</h2>
|
|
68
|
+
* {book?.summary}
|
|
69
|
+
*
|
|
70
|
+
* <h2>Order</h2>
|
|
71
|
+
* <a href=`/order/${book._id}`>Order {book?.title} today!</a>
|
|
72
|
+
* </article>
|
|
73
|
+
* )
|
|
74
|
+
* }
|
|
75
|
+
* ```
|
|
76
|
+
*
|
|
77
|
+
*/
|
|
78
|
+
export function useDocument<TDocument extends SanityDocument>(
|
|
79
|
+
doc: DocumentHandle<TDocument>,
|
|
80
|
+
): TDocument | null
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* @beta
|
|
84
|
+
* Reads and subscribes to a document’s realtime state, incorporating both local and remote changes.
|
|
85
|
+
* When called with a `path` argument, the hook will return the nested value’s state.
|
|
86
|
+
* When called without a `path` argument, the entire document’s state will be returned.
|
|
87
|
+
*
|
|
88
|
+
* @remarks
|
|
89
|
+
* `useDocument` is designed to be used within a realtime context in which local updates to documents
|
|
90
|
+
* need to be displayed before they are persisted to the remote copy. This can be useful within a collaborative
|
|
91
|
+
* or realtime editing interface where local changes need to be reflected immediately.
|
|
92
|
+
*
|
|
93
|
+
* However, this hook can be too resource intensive for applications where static document values simply
|
|
94
|
+
* need to be displayed (or when changes to documents don’t need to be reflected immediately);
|
|
95
|
+
* consider using `usePreview` or `useQuery` for these use cases instead. These hooks leverage the Sanity
|
|
96
|
+
* Live Content API to provide a more efficient way to read and subscribe to document state.
|
|
97
|
+
*/
|
|
98
|
+
export function useDocument(doc: DocumentHandle, path?: string): unknown {
|
|
99
|
+
return _useDocument(doc, path)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const _useDocument = createStateSourceHook<[doc: DocumentHandle, path?: string], unknown>({
|
|
103
|
+
getState: getDocumentState,
|
|
104
|
+
shouldSuspend: (instance, doc) => getDocumentState(instance, doc._id).getCurrent() === undefined,
|
|
105
|
+
suspender: resolveDocument,
|
|
106
|
+
getResourceId: (doc) => getResourceId(doc.resourceId),
|
|
107
|
+
})
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// tests/useDocumentEvent.test.ts
|
|
2
|
+
import {
|
|
3
|
+
createSanityInstance,
|
|
4
|
+
type DocumentEvent,
|
|
5
|
+
type DocumentHandle,
|
|
6
|
+
subscribeDocumentEvents,
|
|
7
|
+
} from '@sanity/sdk'
|
|
8
|
+
import {renderHook} from '@testing-library/react'
|
|
9
|
+
import {beforeEach, describe, expect, it, vi} from 'vitest'
|
|
10
|
+
|
|
11
|
+
import {useSanityInstance} from '../context/useSanityInstance'
|
|
12
|
+
import {useDocumentEvent} from './useDocumentEvent'
|
|
13
|
+
|
|
14
|
+
vi.mock('@sanity/sdk', async (importOriginal) => {
|
|
15
|
+
const original = await importOriginal<typeof import('@sanity/sdk')>()
|
|
16
|
+
return {...original, subscribeDocumentEvents: vi.fn()}
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
vi.mock('../context/useSanityInstance', () => ({
|
|
20
|
+
useSanityInstance: vi.fn(),
|
|
21
|
+
}))
|
|
22
|
+
|
|
23
|
+
const instance = createSanityInstance({projectId: 'p', dataset: 'd'})
|
|
24
|
+
const docHandle: DocumentHandle = {
|
|
25
|
+
_id: 'doc1',
|
|
26
|
+
_type: 'book',
|
|
27
|
+
resourceId: 'document:p.d:doc1',
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
describe('useDocumentEvent hook', () => {
|
|
31
|
+
beforeEach(() => {
|
|
32
|
+
vi.resetAllMocks()
|
|
33
|
+
vi.mocked(useSanityInstance).mockReturnValue(instance)
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('calls subscribeDocumentEvents with instance and a stable handler', () => {
|
|
37
|
+
const handler = vi.fn()
|
|
38
|
+
const unsubscribe = vi.fn()
|
|
39
|
+
vi.mocked(subscribeDocumentEvents).mockReturnValue(unsubscribe)
|
|
40
|
+
|
|
41
|
+
renderHook(() => useDocumentEvent(handler, docHandle))
|
|
42
|
+
|
|
43
|
+
expect(vi.mocked(subscribeDocumentEvents)).toHaveBeenCalledTimes(1)
|
|
44
|
+
expect(vi.mocked(subscribeDocumentEvents).mock.calls[0][0]).toBe(instance)
|
|
45
|
+
|
|
46
|
+
const stableHandler = vi.mocked(subscribeDocumentEvents).mock.calls[0][1]
|
|
47
|
+
expect(typeof stableHandler).toBe('function')
|
|
48
|
+
|
|
49
|
+
const event = {type: 'edited', documentId: 'doc1', outgoing: {}} as DocumentEvent
|
|
50
|
+
stableHandler(event)
|
|
51
|
+
expect(handler).toHaveBeenCalledWith(event)
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('calls the unsubscribe function on unmount', () => {
|
|
55
|
+
const handler = vi.fn()
|
|
56
|
+
const unsubscribe = vi.fn()
|
|
57
|
+
vi.mocked(subscribeDocumentEvents).mockReturnValue(unsubscribe)
|
|
58
|
+
|
|
59
|
+
const {unmount} = renderHook(() => useDocumentEvent(handler, docHandle))
|
|
60
|
+
unmount()
|
|
61
|
+
expect(unsubscribe).toHaveBeenCalledTimes(1)
|
|
62
|
+
})
|
|
63
|
+
})
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type DocumentEvent,
|
|
3
|
+
type DocumentHandle,
|
|
4
|
+
getResourceId,
|
|
5
|
+
subscribeDocumentEvents,
|
|
6
|
+
} from '@sanity/sdk'
|
|
7
|
+
import {useCallback, useEffect, useInsertionEffect, useRef} from 'react'
|
|
8
|
+
|
|
9
|
+
import {useSanityInstance} from '../context/useSanityInstance'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
*
|
|
13
|
+
* @beta
|
|
14
|
+
*
|
|
15
|
+
* Subscribes an event handler to events in your application’s document store, such as document
|
|
16
|
+
* creation, deletion, and updates.
|
|
17
|
+
*
|
|
18
|
+
* @category Documents
|
|
19
|
+
* @param handler - The event handler to register.
|
|
20
|
+
* @param doc - The document to subscribe to events for. If you pass a `DocumentHandle` with a `resourceId` (in the format of `document:projectId.dataset:documentId`)
|
|
21
|
+
* the document will be read from the specified Sanity project and dataset that is included in the handle. If no `resourceId` is provided, the default project and dataset from your `SanityApp` configuration will be used.
|
|
22
|
+
* @example
|
|
23
|
+
* ```
|
|
24
|
+
* import {useDocumentEvent} from '@sanity/sdk-react'
|
|
25
|
+
* import {type DocumentEvent} from '@sanity/sdk'
|
|
26
|
+
*
|
|
27
|
+
* useDocumentEvent((event) => {
|
|
28
|
+
* if (event.type === DocumentEvent.DocumentDeletedEvent) {
|
|
29
|
+
* alert(`Document with ID ${event.documentId} deleted!`)
|
|
30
|
+
* } else {
|
|
31
|
+
* console.log(event)
|
|
32
|
+
* }
|
|
33
|
+
* })
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
export function useDocumentEvent(
|
|
37
|
+
handler: (documentEvent: DocumentEvent) => void,
|
|
38
|
+
doc: DocumentHandle,
|
|
39
|
+
): void {
|
|
40
|
+
const ref = useRef(handler)
|
|
41
|
+
|
|
42
|
+
useInsertionEffect(() => {
|
|
43
|
+
ref.current = handler
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
const stableHandler = useCallback((documentEvent: DocumentEvent) => {
|
|
47
|
+
return ref.current(documentEvent)
|
|
48
|
+
}, [])
|
|
49
|
+
|
|
50
|
+
const instance = useSanityInstance(getResourceId(doc.resourceId))
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
return subscribeDocumentEvents(instance, stableHandler)
|
|
53
|
+
}, [instance, stableHandler])
|
|
54
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type DocumentAction,
|
|
3
|
+
type DocumentPermissionsResult,
|
|
4
|
+
getPermissionsState,
|
|
5
|
+
getResourceId,
|
|
6
|
+
} from '@sanity/sdk'
|
|
7
|
+
import {useCallback, useMemo, useSyncExternalStore} from 'react'
|
|
8
|
+
import {filter, firstValueFrom} from 'rxjs'
|
|
9
|
+
|
|
10
|
+
import {useSanityInstance} from '../context/useSanityInstance'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
*
|
|
14
|
+
* @beta
|
|
15
|
+
*
|
|
16
|
+
* Check if the current user has the specified permissions for the given document actions.
|
|
17
|
+
*
|
|
18
|
+
* @category Permissions
|
|
19
|
+
* @param actions - One more more calls to a particular document action function for a given document
|
|
20
|
+
* @returns An object that specifies whether the action is allowed; if the action is not allowed, an explanatory message and list of reasons is also provided.
|
|
21
|
+
*
|
|
22
|
+
* @example Checking for permission to publish a document
|
|
23
|
+
* ```ts
|
|
24
|
+
* import {useDocumentPermissions, useApplyDocumentActions} from '@sanity/sdk-react'
|
|
25
|
+
* import {publishDocument} from '@sanity/sdk'
|
|
26
|
+
*
|
|
27
|
+
* export function PublishButton({doc}: {doc: DocumentHandle}) {
|
|
28
|
+
* const publishPermissions = useDocumentPermissions(publishDocument(doc))
|
|
29
|
+
* const applyAction = useApplyDocumentActions()
|
|
30
|
+
*
|
|
31
|
+
* return (
|
|
32
|
+
* <>
|
|
33
|
+
* <button
|
|
34
|
+
* disabled={!publishPermissions.allowed}
|
|
35
|
+
* onClick={() => applyAction(publishDocument(doc))}
|
|
36
|
+
* popoverTarget={`${publishPermissions.allowed ? undefined : 'publishButtonPopover'}`}
|
|
37
|
+
* >
|
|
38
|
+
* Publish
|
|
39
|
+
* </button>
|
|
40
|
+
* {!publishPermissions.allowed && (
|
|
41
|
+
* <div popover id="publishButtonPopover">
|
|
42
|
+
* {publishPermissions.message}
|
|
43
|
+
* </div>
|
|
44
|
+
* )}
|
|
45
|
+
* </>
|
|
46
|
+
* )
|
|
47
|
+
* }
|
|
48
|
+
* ```
|
|
49
|
+
*/
|
|
50
|
+
export function useDocumentPermissions(
|
|
51
|
+
actions: DocumentAction | DocumentAction[],
|
|
52
|
+
): DocumentPermissionsResult {
|
|
53
|
+
// if actions is an array, we need to check each action to see if the resourceId is the same
|
|
54
|
+
if (Array.isArray(actions)) {
|
|
55
|
+
const resourceIds = actions.map((action) => action.resourceId)
|
|
56
|
+
const uniqueResourceIds = new Set(resourceIds)
|
|
57
|
+
if (uniqueResourceIds.size !== 1) {
|
|
58
|
+
throw new Error('All actions must have the same resourceId')
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
const resourceId = Array.isArray(actions)
|
|
62
|
+
? getResourceId(actions[0].resourceId)
|
|
63
|
+
: getResourceId(actions.resourceId)
|
|
64
|
+
|
|
65
|
+
const instance = useSanityInstance(resourceId)
|
|
66
|
+
const isDocumentReady = useCallback(
|
|
67
|
+
() => getPermissionsState(instance, actions).getCurrent() !== undefined,
|
|
68
|
+
[actions, instance],
|
|
69
|
+
)
|
|
70
|
+
if (!isDocumentReady()) {
|
|
71
|
+
throw firstValueFrom(
|
|
72
|
+
getPermissionsState(instance, actions).observable.pipe(
|
|
73
|
+
filter((result) => result !== undefined),
|
|
74
|
+
),
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const {subscribe, getCurrent} = useMemo(
|
|
79
|
+
() => getPermissionsState(instance, actions),
|
|
80
|
+
[actions, instance],
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
return useSyncExternalStore(subscribe, getCurrent) as DocumentPermissionsResult
|
|
84
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import {getDocumentSyncStatus} from '@sanity/sdk'
|
|
2
|
+
import {identity} from 'rxjs'
|
|
3
|
+
import {describe, it} from 'vitest'
|
|
4
|
+
|
|
5
|
+
import {createStateSourceHook} from '../helpers/createStateSourceHook'
|
|
6
|
+
|
|
7
|
+
vi.mock('../helpers/createStateSourceHook', () => ({createStateSourceHook: vi.fn(identity)}))
|
|
8
|
+
vi.mock('@sanity/sdk', () => ({getDocumentSyncStatus: vi.fn()}))
|
|
9
|
+
|
|
10
|
+
describe('useDocumentSyncStatus', () => {
|
|
11
|
+
it('calls `createStateSourceHook` with `getTokenState`', async () => {
|
|
12
|
+
const {useDocumentSyncStatus} = await import('./useDocumentSyncStatus')
|
|
13
|
+
expect(createStateSourceHook).toHaveBeenCalledWith(getDocumentSyncStatus)
|
|
14
|
+
expect(useDocumentSyncStatus).toBe(getDocumentSyncStatus)
|
|
15
|
+
})
|
|
16
|
+
})
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import {type DocumentHandle, getDocumentSyncStatus} from '@sanity/sdk'
|
|
2
|
+
|
|
3
|
+
import {createStateSourceHook} from '../helpers/createStateSourceHook'
|
|
4
|
+
|
|
5
|
+
type UseDocumentSyncStatus = {
|
|
6
|
+
/**
|
|
7
|
+
* Exposes the document’s sync status between local and remote document states.
|
|
8
|
+
*
|
|
9
|
+
* @category Documents
|
|
10
|
+
* @param doc - The document handle to get sync status for. If you pass a `DocumentHandle` with a `resourceId` (in the format of `document:projectId.dataset:documentId`)
|
|
11
|
+
* the document will be read from the specified Sanity project and dataset that is included in the handle. If no `resourceId` is provided, the default project and dataset from your `SanityApp` configuration will be used.
|
|
12
|
+
* @returns `true` if local changes are synced with remote, `false` if the changes are not synced, and `undefined` if the document is not found
|
|
13
|
+
* @example Disable a Save button when there are no changes to sync
|
|
14
|
+
* ```
|
|
15
|
+
* const myDocumentHandle = { _id: 'documentId', _type: 'documentType', resourceId: 'document:projectId:dataset:documentId' }
|
|
16
|
+
* const documentSynced = useDocumentSyncStatus(myDocumentHandle)
|
|
17
|
+
*
|
|
18
|
+
* return (
|
|
19
|
+
* <button disabled={documentSynced}>
|
|
20
|
+
* Save Changes
|
|
21
|
+
* </button>
|
|
22
|
+
* )
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
(doc: DocumentHandle): boolean | undefined
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @beta
|
|
30
|
+
* @function
|
|
31
|
+
*/
|
|
32
|
+
export const useDocumentSyncStatus: UseDocumentSyncStatus =
|
|
33
|
+
createStateSourceHook(getDocumentSyncStatus)
|