@sanity/sdk-react 2.11.1 → 2.13.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.
@@ -20,14 +20,14 @@ describe('useActiveReleases', () => {
20
20
  vi.clearAllMocks()
21
21
  })
22
22
 
23
- it('should suspend when initial state is undefined', () => {
23
+ it('suspends until the releases state source emits, then resolves with the data', async () => {
24
24
  const mockSubject = new BehaviorSubject<ReleaseDocument[] | undefined>(undefined)
25
25
  const mockStateSource = {
26
26
  subscribe: vi.fn((callback) => {
27
27
  const subscription = mockSubject.subscribe(callback)
28
28
  return () => subscription.unsubscribe()
29
29
  }),
30
- getCurrent: vi.fn(() => undefined),
30
+ getCurrent: vi.fn(() => mockSubject.getValue()),
31
31
  observable: mockSubject,
32
32
  }
33
33
 
@@ -50,15 +50,21 @@ describe('useActiveReleases', () => {
50
50
  },
51
51
  )
52
52
 
53
- // Verify that the hook threw a promise (suspended)
54
53
  expect(result.current).toBeInstanceOf(Promise)
55
54
  expect(mockStateSource.getCurrent).toHaveBeenCalled()
55
+
56
+ const resolved: ReleaseDocument[] = [
57
+ {_id: 'release1', _type: 'release'} as unknown as ReleaseDocument,
58
+ ]
59
+ mockSubject.next(resolved)
60
+
61
+ await expect(result.current).resolves.toEqual(resolved)
56
62
  })
57
63
 
58
64
  it('should resolve with releases when data is available', () => {
59
65
  const mockReleases: ReleaseDocument[] = [
60
- {_id: 'release1', _type: 'release'} as ReleaseDocument,
61
- {_id: 'release2', _type: 'release'} as ReleaseDocument,
66
+ {_id: 'release1', _type: 'release'} as unknown as ReleaseDocument,
67
+ {_id: 'release2', _type: 'release'} as unknown as ReleaseDocument,
62
68
  ]
63
69
 
64
70
  const mockSubject = new BehaviorSubject<ReleaseDocument[]>(mockReleases)
@@ -14,21 +14,6 @@ import {
14
14
  type WithResourceNameSupport,
15
15
  } from '../helpers/useNormalizedResourceOptions'
16
16
 
17
- /**
18
- * @public
19
-
20
- * Returns the active releases for the current project,
21
- * represented as a list of release documents.
22
- *
23
- * @returns The active releases for the current project.
24
- * @category Projects
25
- * @example
26
- * ```tsx
27
- * import {useActiveReleases} from '@sanity/sdk-react'
28
- *
29
- * const activeReleases = useActiveReleases()
30
- * ```
31
- */
32
17
  type UseActiveReleasesValue = {
33
18
  (options?: {resource?: DocumentResource}): ReleaseDocument[]
34
19
  }
@@ -49,6 +34,18 @@ const useActiveReleasesValue: UseActiveReleasesValue = createStateSourceHook({
49
34
  /**
50
35
  * @public
51
36
  * @function
37
+ *
38
+ * Returns the active releases for the current project,
39
+ * represented as a list of release documents.
40
+ *
41
+ * @returns The active releases for the current project.
42
+ * @category Releases
43
+ * @example
44
+ * ```tsx
45
+ * import {useActiveReleases} from '@sanity/sdk-react'
46
+ *
47
+ * const activeReleases = useActiveReleases()
48
+ * ```
52
49
  */
53
50
  export function useActiveReleases(
54
51
  options?: WithResourceNameSupport<SanityConfig> | undefined,
@@ -0,0 +1,93 @@
1
+ import {getAllReleasesState, type ReleaseDocument} from '@sanity/sdk'
2
+ import {renderHook} from '@testing-library/react'
3
+ import {BehaviorSubject} from 'rxjs'
4
+ import {describe, expect, it, vi} from 'vitest'
5
+
6
+ import {ResourceProvider} from '../../context/ResourceProvider'
7
+ import {useAllReleases} from './useAllReleases'
8
+
9
+ vi.mock('@sanity/sdk', async () => {
10
+ const actual = await vi.importActual('@sanity/sdk')
11
+ return {
12
+ ...actual,
13
+ getAllReleasesState: vi.fn(),
14
+ }
15
+ })
16
+
17
+ describe('useAllReleases', () => {
18
+ beforeEach(() => {
19
+ vi.clearAllMocks()
20
+ })
21
+
22
+ it('suspends until the releases state source emits, then resolves with the data', async () => {
23
+ const mockSubject = new BehaviorSubject<ReleaseDocument[] | undefined>(undefined)
24
+ const mockStateSource = {
25
+ subscribe: vi.fn((callback) => {
26
+ const subscription = mockSubject.subscribe(callback)
27
+ return () => subscription.unsubscribe()
28
+ }),
29
+ getCurrent: vi.fn(() => mockSubject.getValue()),
30
+ observable: mockSubject,
31
+ }
32
+
33
+ vi.mocked(getAllReleasesState).mockReturnValue(mockStateSource)
34
+
35
+ const {result} = renderHook(
36
+ () => {
37
+ try {
38
+ return useAllReleases()
39
+ } catch (e) {
40
+ return e
41
+ }
42
+ },
43
+ {
44
+ wrapper: ({children}) => (
45
+ <ResourceProvider projectId="p" dataset="d" fallback={<p>Loading...</p>}>
46
+ {children}
47
+ </ResourceProvider>
48
+ ),
49
+ },
50
+ )
51
+
52
+ expect(result.current).toBeInstanceOf(Promise)
53
+ expect(mockStateSource.getCurrent).toHaveBeenCalled()
54
+
55
+ const resolved: ReleaseDocument[] = [
56
+ {_id: 'r-active', _type: 'system.release', state: 'active'} as ReleaseDocument,
57
+ ]
58
+ mockSubject.next(resolved)
59
+
60
+ await expect(result.current).resolves.toEqual(resolved)
61
+ })
62
+
63
+ it('returns every release including archived and published once loaded', () => {
64
+ const mockReleases: ReleaseDocument[] = [
65
+ {_id: 'r-active', _type: 'system.release', state: 'active'} as ReleaseDocument,
66
+ {_id: 'r-archived', _type: 'system.release', state: 'archived'} as ReleaseDocument,
67
+ {_id: 'r-published', _type: 'system.release', state: 'published'} as ReleaseDocument,
68
+ ]
69
+
70
+ const mockSubject = new BehaviorSubject<ReleaseDocument[]>(mockReleases)
71
+ const mockStateSource = {
72
+ subscribe: vi.fn((callback) => {
73
+ const subscription = mockSubject.subscribe(callback)
74
+ return () => subscription.unsubscribe()
75
+ }),
76
+ getCurrent: vi.fn(() => mockReleases),
77
+ observable: mockSubject,
78
+ }
79
+
80
+ vi.mocked(getAllReleasesState).mockReturnValue(mockStateSource)
81
+
82
+ const {result} = renderHook(() => useAllReleases(), {
83
+ wrapper: ({children}) => (
84
+ <ResourceProvider projectId="p" dataset="d" fallback={<p>Loading...</p>}>
85
+ {children}
86
+ </ResourceProvider>
87
+ ),
88
+ })
89
+
90
+ expect(result.current).toEqual(mockReleases)
91
+ expect(mockStateSource.getCurrent).toHaveBeenCalled()
92
+ })
93
+ })
@@ -0,0 +1,62 @@
1
+ import {
2
+ type DocumentResource,
3
+ getAllReleasesState,
4
+ type ReleaseDocument,
5
+ type SanityConfig,
6
+ type SanityInstance,
7
+ type StateSource,
8
+ } from '@sanity/sdk'
9
+ import {filter, firstValueFrom} from 'rxjs'
10
+
11
+ import {createStateSourceHook} from '../helpers/createStateSourceHook'
12
+ import {
13
+ useNormalizedResourceOptions,
14
+ type WithResourceNameSupport,
15
+ } from '../helpers/useNormalizedResourceOptions'
16
+
17
+ type UseAllReleasesValue = {
18
+ (options?: {resource?: DocumentResource}): ReleaseDocument[]
19
+ }
20
+
21
+ const useAllReleasesValue: UseAllReleasesValue = createStateSourceHook({
22
+ getState: getAllReleasesState as (
23
+ instance: SanityInstance,
24
+ options?: {resource?: DocumentResource},
25
+ ) => StateSource<ReleaseDocument[]>,
26
+ shouldSuspend: (instance: SanityInstance, options?: {resource?: DocumentResource}) =>
27
+ getAllReleasesState(instance, options ?? {}).getCurrent() === undefined,
28
+ suspender: (instance: SanityInstance, options?: {resource?: DocumentResource}) =>
29
+ firstValueFrom(getAllReleasesState(instance, options ?? {}).observable.pipe(filter(Boolean))),
30
+ })
31
+
32
+ /**
33
+ * @public
34
+ * @function
35
+ *
36
+ * Returns every release the dataset has — including `archived`, `published`,
37
+ * and mid-transition states (`archiving`, `unarchiving`, `publishing`,
38
+ * `scheduling`).
39
+ *
40
+ * Use this hook when you're building a release-management UI (listing
41
+ * releases, surfacing lifecycle controls, etc.) so a release stays visible
42
+ * across its full lifecycle — including after it's been published or
43
+ * archived. For perspective / content queries, prefer
44
+ * {@link useActiveReleases}, which filters to releases that still affect
45
+ * what's queryable.
46
+ *
47
+ * @returns Every release for the current project, sorted to match the order
48
+ * used by {@link useActiveReleases}.
49
+ * @category Releases
50
+ * @example
51
+ * ```tsx
52
+ * import {useAllReleases} from '@sanity/sdk-react'
53
+ *
54
+ * const releases = useAllReleases()
55
+ * ```
56
+ */
57
+ export function useAllReleases(
58
+ options?: WithResourceNameSupport<SanityConfig> | undefined,
59
+ ): ReleaseDocument[] {
60
+ const normalizedOptions = useNormalizedResourceOptions(options ?? {})
61
+ return useAllReleasesValue(normalizedOptions)
62
+ }
@@ -0,0 +1,66 @@
1
+ import {applyDocumentActions, createSanityInstance} from '@sanity/sdk'
2
+ import {describe, it} from 'vitest'
3
+
4
+ import {renderHook} from '../../../test/test-utils'
5
+ import {useSanityInstance} from '../context/useSanityInstance'
6
+ import {useApplyReleaseActions} from './useApplyReleaseActions'
7
+
8
+ // Resource resolution, mismatch detection, and context fallback are covered
9
+ // by hooks/helpers/useApplyActions.test.tsx — both this hook and
10
+ // useApplyDocumentActions are typed wrappers over that shared implementation.
11
+ // These tests just verify the wrapper forwards release actions through and
12
+ // supports batching them in a single transaction.
13
+
14
+ vi.mock('@sanity/sdk', async (importOriginal) => {
15
+ const original = await importOriginal<typeof import('@sanity/sdk')>()
16
+ return {...original, applyDocumentActions: vi.fn()}
17
+ })
18
+
19
+ vi.mock('../context/useSanityInstance')
20
+
21
+ const instance = createSanityInstance({projectId: 'p', dataset: 'd'})
22
+
23
+ describe('useApplyReleaseActions', () => {
24
+ beforeEach(() => {
25
+ vi.resetAllMocks()
26
+ vi.mocked(useSanityInstance).mockReturnValueOnce(instance)
27
+ })
28
+
29
+ it('forwards a release action to applyDocumentActions with the resolved resource', () => {
30
+ const {result} = renderHook(() => useApplyReleaseActions())
31
+ result.current({type: 'release.create', releaseId: 'r1', metadata: {releaseType: 'asap'}})
32
+
33
+ expect(applyDocumentActions).toHaveBeenCalledExactlyOnceWith(instance, {
34
+ actions: [
35
+ {
36
+ type: 'release.create',
37
+ releaseId: 'r1',
38
+ metadata: {releaseType: 'asap'},
39
+ resource: {projectId: 'test', dataset: 'test'},
40
+ },
41
+ ],
42
+ resource: {projectId: 'test', dataset: 'test'},
43
+ })
44
+ })
45
+
46
+ it('forwards an array of release actions as a single transaction', () => {
47
+ const {result} = renderHook(() => useApplyReleaseActions())
48
+ result.current([
49
+ {type: 'release.create', releaseId: 'r1', metadata: {releaseType: 'asap'}},
50
+ {type: 'release.publish', releaseId: 'r1'},
51
+ ])
52
+
53
+ expect(applyDocumentActions).toHaveBeenCalledExactlyOnceWith(instance, {
54
+ actions: [
55
+ {
56
+ type: 'release.create',
57
+ releaseId: 'r1',
58
+ metadata: {releaseType: 'asap'},
59
+ resource: {projectId: 'test', dataset: 'test'},
60
+ },
61
+ {type: 'release.publish', releaseId: 'r1', resource: {projectId: 'test', dataset: 'test'}},
62
+ ],
63
+ resource: {projectId: 'test', dataset: 'test'},
64
+ })
65
+ })
66
+ })
@@ -0,0 +1,82 @@
1
+ import {type ActionsResult, type ReleaseAction} from '@sanity/sdk'
2
+
3
+ import {type ResourceHandle} from '../../config/handles'
4
+ import {useApplyActions} from '../helpers/useApplyActions'
5
+
6
+ /**
7
+ * @public
8
+ */
9
+ interface UseApplyReleaseActions {
10
+ (): (action: ReleaseAction | ReleaseAction[], options?: ResourceHandle) => Promise<ActionsResult>
11
+ }
12
+
13
+ /**
14
+ * @public
15
+ *
16
+ * Provides a stable callback function for applying one or more release actions.
17
+ *
18
+ * This hook wraps the core `applyDocumentActions` functionality from `@sanity/sdk`,
19
+ * integrating it with the React component lifecycle and {@link SanityInstance}.
20
+ * It accepts release-lifecycle actions generated by {@link createRelease},
21
+ * {@link editRelease}, {@link publishRelease}, {@link scheduleRelease},
22
+ * {@link unscheduleRelease}, {@link archiveRelease}, {@link unarchiveRelease},
23
+ * and {@link deleteRelease}.
24
+ *
25
+ * Note that actions submitted via this hook will cascade to the documents in the release.
26
+ * For example, if you create a release and then publish it, the documents in the release will be published.
27
+ * If you delete a published release, the version documents will be deleted.
28
+ *
29
+ * Features:
30
+ * - Applies one or multiple `ReleaseAction` objects.
31
+ * - Supports optimistic updates for create/edit/delete: local release state
32
+ * reflects changes immediately while in-flight.
33
+ * - Handles batching: multiple actions passed together are submitted as a
34
+ * single atomic transaction.
35
+ *
36
+ * Release actions cannot be combined with `liveEdit` document actions in the
37
+ * same transaction. Submit them as separate transactions if you need both.
38
+ *
39
+ * @category Releases
40
+ * @returns A stable callback. When called with a single `ReleaseAction` or an
41
+ * array of `ReleaseAction`s, it returns a promise that resolves to an
42
+ * {@link ActionsResult}.
43
+ *
44
+ * @example Create and schedule a release
45
+ * ```tsx
46
+ * import {
47
+ * createRelease,
48
+ * scheduleRelease,
49
+ * useApplyReleaseActions,
50
+ * type ReleaseHandle,
51
+ * } from '@sanity/sdk-react'
52
+ *
53
+ * function ScheduleReleaseButton({release}: {release: ReleaseHandle}) {
54
+ * const applyRelease = useApplyReleaseActions()
55
+ *
56
+ * const handleSchedule = () =>
57
+ * applyRelease([
58
+ * createRelease(release, {title: 'Summer drop', releaseType: 'asap'}),
59
+ * scheduleRelease(release, '2026-06-01T00:00:00Z'),
60
+ * ])
61
+ *
62
+ * return <button onClick={handleSchedule}>Schedule</button>
63
+ * }
64
+ * ```
65
+ *
66
+ * @example Publish a release
67
+ * ```tsx
68
+ * import {publishRelease, useApplyReleaseActions} from '@sanity/sdk-react'
69
+ *
70
+ * function PublishButton({releaseId}: {releaseId: string}) {
71
+ * const applyRelease = useApplyReleaseActions()
72
+ * return (
73
+ * <button onClick={() => applyRelease(publishRelease({releaseId}))}>
74
+ * Publish all documents in the release
75
+ * </button>
76
+ * )
77
+ * }
78
+ * ```
79
+ */
80
+ export const useApplyReleaseActions: UseApplyReleaseActions = () => {
81
+ return useApplyActions() as ReturnType<UseApplyReleaseActions>
82
+ }
@@ -47,7 +47,7 @@ describe('usePerspective', () => {
47
47
  // Mock the active releases observable for the suspender
48
48
  const mockReleaseDoc: ReleaseDocument = {
49
49
  _id: 'release1',
50
- _type: 'release',
50
+ _type: 'system.release',
51
51
  _createdAt: '2021-01-01T00:00:00Z',
52
52
  _updatedAt: '2021-01-01T00:00:00Z',
53
53
  _rev: 'rev1',