diff --git a/packages/sanity/src/core/comments/plugin/studio-layout/CommentsStudioLayout.tsx b/packages/sanity/src/core/comments/plugin/studio-layout/CommentsStudioLayout.tsx
index e7dd42ce88d..2a4c6a51d57 100644
--- a/packages/sanity/src/core/comments/plugin/studio-layout/CommentsStudioLayout.tsx
+++ b/packages/sanity/src/core/comments/plugin/studio-layout/CommentsStudioLayout.tsx
@@ -1,23 +1,20 @@
import {ConditionalWrapper} from '../../../../ui-components'
import {type LayoutProps} from '../../../config'
import {useFeatureEnabled} from '../../../hooks'
-import {AddonDatasetProvider} from '../../../studio'
import {CommentsOnboardingProvider, CommentsUpsellProvider} from '../../context'
export function CommentsStudioLayout(props: LayoutProps) {
const {enabled, isLoading} = useFeatureEnabled('studioComments')
return (
-
-
- {children}}
- >
- {props.renderDefault(props)}
-
-
-
+
+ {children}}
+ >
+ {props.renderDefault(props)}
+
+
)
}
diff --git a/packages/sanity/src/core/store/_legacy/datastores.ts b/packages/sanity/src/core/store/_legacy/datastores.ts
index c86b76bcede..192e1d53854 100644
--- a/packages/sanity/src/core/store/_legacy/datastores.ts
+++ b/packages/sanity/src/core/store/_legacy/datastores.ts
@@ -5,7 +5,12 @@ import {of} from 'rxjs'
import {useClient, useSchema, useTemplates} from '../../hooks'
import {createDocumentPreviewStore, type DocumentPreviewStore} from '../../preview'
-import {useSource, useWorkspace} from '../../studio'
+import {
+ type AddonDatasetStore,
+ createAddonDatasetStore,
+ useSource,
+ useWorkspace,
+} from '../../studio'
import {DEFAULT_STUDIO_CLIENT_OPTIONS} from '../../studioClient'
import {createKeyValueStore, type KeyValueStore} from '../key-value'
import {useCurrentUser} from '../user'
@@ -273,3 +278,27 @@ export function useKeyValueStore(): KeyValueStore {
return keyValueStore
}, [client, resourceCache, workspace])
}
+
+/**
+ * @internal
+ */
+export function useAddonDatasetStore(): AddonDatasetStore {
+ const client = useClient(DEFAULT_STUDIO_CLIENT_OPTIONS)
+ const resourceCache = useResourceCache()
+
+ return useMemo(() => {
+ const addonDatasetStore =
+ resourceCache.get
({
+ namespace: 'addonDatasetStore',
+ dependencies: [client],
+ }) || createAddonDatasetStore({client})
+
+ resourceCache.set({
+ namespace: 'addonDatasetStore',
+ dependencies: [client],
+ value: addonDatasetStore,
+ })
+
+ return addonDatasetStore
+ }, [client, resourceCache])
+}
diff --git a/packages/sanity/src/core/studio/addonDataset/AddonDatasetProvider.tsx b/packages/sanity/src/core/studio/addonDataset/AddonDatasetProvider.tsx
deleted file mode 100644
index eba4aa6c528..00000000000
--- a/packages/sanity/src/core/studio/addonDataset/AddonDatasetProvider.tsx
+++ /dev/null
@@ -1,136 +0,0 @@
-import {type SanityClient} from '@sanity/client'
-import {useCallback, useEffect, useMemo, useState} from 'react'
-import {AddonDatasetContext} from 'sanity/_singletons'
-
-import {useClient} from '../../hooks'
-import {DEFAULT_STUDIO_CLIENT_OPTIONS} from '../../studioClient'
-import {useWorkspace} from '../workspace'
-import {type AddonDatasetContextValue} from './types'
-
-const API_VERSION = 'v2023-11-13'
-
-interface AddonDatasetSetupProviderProps {
- children: React.ReactNode
-}
-
-/**
- * This provider sets the addon dataset client, currently called `comments` dataset.
- * It also exposes a `createAddonDataset` function that can be used to create the addon dataset if it does not exist.
- * @beta
- * @hidden
- */
-export function AddonDatasetProvider(props: AddonDatasetSetupProviderProps) {
- const {children} = props
- const {dataset, projectId} = useWorkspace()
- const originalClient = useClient(DEFAULT_STUDIO_CLIENT_OPTIONS)
- const [addonDatasetClient, setAddonDatasetClient] = useState(null)
- const [isCreatingDataset, setIsCreatingDataset] = useState(false)
- const [ready, setReady] = useState(false)
-
- const getAddonDatasetName = useCallback(async (): Promise => {
- const res = await originalClient.withConfig({apiVersion: API_VERSION}).request({
- uri: `/projects/${projectId}/datasets?datasetProfile=comments&addonFor=${dataset}`,
- tag: 'sanity.studio',
- })
-
- // The response is an array containing the addon dataset. We only expect
- // one addon dataset to be returned, so we return the name of the first
- // addon dataset in the array.
- return res?.[0]?.name
- }, [dataset, originalClient, projectId])
-
- const handleCreateClient = useCallback(
- (addonDatasetName: string) => {
- const client = originalClient.withConfig({
- apiVersion: API_VERSION,
- dataset: addonDatasetName,
- projectId,
- requestTagPrefix: 'sanity.studio',
- useCdn: false,
- withCredentials: true,
- })
-
- return client
- },
- [originalClient, projectId],
- )
-
- const handleCreateAddonDataset = useCallback(async (): Promise => {
- setIsCreatingDataset(true)
-
- // Before running the setup, we check if the addon dataset already exists.
- // The addon dataset might already exist if another user has already run
- // the setup, but the current user has not refreshed the page yet and
- // therefore don't have a client for the addon dataset yet.
- try {
- const addonDatasetName = await getAddonDatasetName()
-
- if (addonDatasetName) {
- const client = handleCreateClient(addonDatasetName)
- setAddonDatasetClient(client)
- setIsCreatingDataset(false)
- return client
- }
- } catch (_) {
- // If the dataset does not exist we will get an error, but we can ignore
- // it since we will create the dataset in the next step.
- }
-
- try {
- // 1. Create the addon dataset
- const res = await originalClient.withConfig({apiVersion: API_VERSION}).request({
- uri: `/comments/${dataset}/setup`,
- method: 'POST',
- })
-
- const datasetName = res?.datasetName
-
- // 2. We can't continue if the addon dataset name is not returned
- if (!datasetName) {
- setIsCreatingDataset(false)
- return null
- }
-
- // 3. Create a client for the addon dataset and set it in the context value
- // so that the consumers can use it to execute comment operations and set up
- // the real time listener for the addon dataset.
- const client = handleCreateClient(datasetName)
- setAddonDatasetClient(client)
-
- // 4. Return the client so that the caller can use it to execute operations
- return client
- } catch (err) {
- throw err
- } finally {
- setIsCreatingDataset(false)
- }
- }, [dataset, getAddonDatasetName, handleCreateClient, originalClient])
-
- useEffect(() => {
- // On mount, we check if the addon dataset already exists. If it does, we create
- // a client for it and set it in the context value so that the consumers can use
- // it to execute comment operations and set up the real time listener for the addon
- // dataset.
- getAddonDatasetName()
- .then((addonDatasetName) => {
- if (!addonDatasetName) return
- const client = handleCreateClient(addonDatasetName)
- setAddonDatasetClient(client)
- })
- .finally(() => {
- setReady(true)
- })
- }, [getAddonDatasetName, handleCreateClient])
-
- const ctxValue = useMemo(
- (): AddonDatasetContextValue => ({
- client: addonDatasetClient,
- createAddonDataset: handleCreateAddonDataset,
- isCreatingDataset,
- ready,
- }),
- [addonDatasetClient, handleCreateAddonDataset, isCreatingDataset, ready],
- )
-
- return {children}
-}
diff --git a/packages/sanity/src/core/studio/addonDataset/createAddonDatasetStore.ts b/packages/sanity/src/core/studio/addonDataset/createAddonDatasetStore.ts
new file mode 100644
index 00000000000..c0421e9c0c1
--- /dev/null
+++ b/packages/sanity/src/core/studio/addonDataset/createAddonDatasetStore.ts
@@ -0,0 +1,157 @@
+import {type SanityClient} from '@sanity/client'
+import {map, mergeWith, type Observable, of, shareReplay, startWith, Subject, switchMap} from 'rxjs'
+
+const API_VERSION = 'v2023-11-13'
+
+interface Context {
+ client: SanityClient
+}
+
+type AddonDatasetError = string
+
+// TODO: Make client `never` when it isn't available (changes needed downstream).
+type ClientStore =
+ | {
+ state: 'setupRequired'
+ // client?: never
+ client: null
+ error?: never
+ }
+ | {
+ state: 'initialising'
+ // client?: never
+ client: null
+ error?: never
+ }
+ | {
+ state: 'ready'
+ client: SanityClient
+ error?: never
+ }
+ | {
+ state: 'error'
+ // client?: never
+ client: null
+ error: AddonDatasetError
+ }
+
+export interface AddonDatasetStore {
+ /**
+ * Get a client instance for the addon dataset, always ensuring first that the addon dataset has
+ * been created.
+ *
+ * TODO: `client$` will never be in state `setupRequired`.
+ */
+ client$: Observable
+
+ /**
+ * Get a client instance for the addon dataset without automatically creating the addon dataset.
+ */
+ lazyClient$: Observable
+}
+
+export function createAddonDatasetStore({client}: Context): AddonDatasetStore {
+ const {dataset, projectId} = client.config()
+
+ const newAddonDatasetName$ = new Subject()
+
+ // The response is an array containing the addon dataset. We only expect
+ // one addon dataset to be returned, so we return the name of the first
+ // addon dataset in the array.
+ const addonDatasetName$: Observable = newAddonDatasetName$
+ .pipe(
+ mergeWith(
+ client
+ .withConfig({
+ apiVersion: API_VERSION,
+ })
+ .observable.request<{name?: string}[]>({
+ uri: `/projects/${projectId}/datasets?datasetProfile=comments&addonFor=${dataset}`,
+ tag: 'sanity.studio',
+ })
+ .pipe(map((response) => response?.[0]?.name)),
+ ),
+ )
+ .pipe(shareReplay(1))
+
+ const handleCreateClient = (addonDatasetName: string): Observable => {
+ return of({
+ state: 'ready',
+ client: client.withConfig({
+ apiVersion: API_VERSION,
+ dataset: addonDatasetName,
+ projectId,
+ requestTagPrefix: 'sanity.studio',
+ useCdn: false,
+ withCredentials: true,
+ }),
+ })
+ }
+
+ const createAddonDataset$ = client.observable
+ .withConfig({
+ apiVersion: API_VERSION,
+ })
+ .request<{datasetName: string | null}>({
+ uri: `/comments/${dataset}/setup`,
+ method: 'POST',
+ })
+ .pipe(
+ switchMap(({datasetName}) => {
+ // 2. We can't continue if the addon dataset name is not returned
+ if (!datasetName) {
+ return of({
+ state: 'error',
+ error: 'No addon dataset',
+ client: null,
+ })
+ }
+ // 3. Create a client for the addon dataset and set it in the context value
+ // so that the consumers can use it to execute comment operations and set up
+ // the real time listener for the addon dataset.
+ newAddonDatasetName$.next(datasetName)
+ return handleCreateClient(datasetName)
+ }),
+ shareReplay(1),
+ )
+
+ // TODO: Abstract duplicate code.
+ const client$: Observable = addonDatasetName$.pipe(
+ switchMap((addonDatasetName) => {
+ if (typeof addonDatasetName === 'string') {
+ return handleCreateClient(addonDatasetName)
+ }
+
+ return createAddonDataset$
+ }),
+ startWith({
+ state: 'initialising',
+ client: null,
+ }),
+ shareReplay(1),
+ )
+
+ // TODO: Abstract duplicate code.
+ const lazyClient$: Observable = addonDatasetName$.pipe(
+ switchMap((addonDatasetName) => {
+ if (typeof addonDatasetName === 'string') {
+ return handleCreateClient(addonDatasetName)
+ }
+
+ return of({
+ state: 'setupRequired',
+ client: null,
+ })
+ }),
+ startWith({
+ state: 'initialising',
+ client: null,
+ }),
+ shareReplay(1),
+ )
+
+ return {
+ client$,
+ lazyClient$,
+ }
+}
diff --git a/packages/sanity/src/core/studio/addonDataset/index.ts b/packages/sanity/src/core/studio/addonDataset/index.ts
index a21b9ccffd0..e68df77659d 100644
--- a/packages/sanity/src/core/studio/addonDataset/index.ts
+++ b/packages/sanity/src/core/studio/addonDataset/index.ts
@@ -1,3 +1,2 @@
-export * from './AddonDatasetProvider'
+export * from './createAddonDatasetStore'
export * from './types'
-export * from './useAddonDataset'
diff --git a/packages/sanity/src/core/studio/addonDataset/useAddonDataset.ts b/packages/sanity/src/core/studio/addonDataset/useAddonDataset.ts
deleted file mode 100644
index 3da5046a251..00000000000
--- a/packages/sanity/src/core/studio/addonDataset/useAddonDataset.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import {useContext} from 'react'
-import {AddonDatasetContext} from 'sanity/_singletons'
-
-import {type AddonDatasetContextValue} from './types'
-
-/**
- * @beta
- * @hidden
- */
-export function useAddonDataset(): AddonDatasetContextValue {
- const ctx = useContext(AddonDatasetContext)
-
- if (!ctx) {
- throw new Error('useAddonDataset: missing context value')
- }
-
- return ctx
-}
diff --git a/packages/sanity/src/core/tasks/__workshop__/TasksCreateStory.tsx b/packages/sanity/src/core/tasks/__workshop__/TasksCreateStory.tsx
index 827fad1813e..8eb0b1642c7 100644
--- a/packages/sanity/src/core/tasks/__workshop__/TasksCreateStory.tsx
+++ b/packages/sanity/src/core/tasks/__workshop__/TasksCreateStory.tsx
@@ -1,32 +1,29 @@
import {TasksNavigationContext} from 'sanity/_singletons'
-import {AddonDatasetProvider} from '../../studio'
import {TasksFormBuilder} from '../components'
import {TasksProvider} from '../context'
export default function TasksCreateStory() {
return (
-
-
- null,
- setActiveTab: () => null,
- handleCloseTasks: () => null,
- handleOpenTasks: () => null,
- handleCopyLinkToTask: () => null,
- }}
- >
-
-
-
-
+
+ null,
+ setActiveTab: () => null,
+ handleCloseTasks: () => null,
+ handleOpenTasks: () => null,
+ handleCopyLinkToTask: () => null,
+ }}
+ >
+
+
+
)
}
diff --git a/packages/sanity/src/core/tasks/__workshop__/TasksLayoutStory.tsx b/packages/sanity/src/core/tasks/__workshop__/TasksLayoutStory.tsx
index 3a47199e745..b42da81faca 100644
--- a/packages/sanity/src/core/tasks/__workshop__/TasksLayoutStory.tsx
+++ b/packages/sanity/src/core/tasks/__workshop__/TasksLayoutStory.tsx
@@ -1,6 +1,5 @@
import {useState} from 'react'
-import {AddonDatasetProvider} from '../../studio'
import {TaskSidebarContent, TasksSidebarHeader} from '../components'
import {type SidebarTabsIds, TasksNavigationProvider, TasksProvider} from '../context'
@@ -11,18 +10,16 @@ export default function TasksLayoutStory() {
const [activeTabId, setActiveTabId] = useState('assigned')
return (
-
-
-
-
-
-
-
-
+
+
+
+
+
+
)
}
diff --git a/packages/sanity/src/core/tasks/components/form/addonWorkspace/TasksAddOnWorkspaceProvider.tsx b/packages/sanity/src/core/tasks/components/form/addonWorkspace/TasksAddOnWorkspaceProvider.tsx
index 96b71febe2c..ac778df4f9c 100644
--- a/packages/sanity/src/core/tasks/components/form/addonWorkspace/TasksAddOnWorkspaceProvider.tsx
+++ b/packages/sanity/src/core/tasks/components/form/addonWorkspace/TasksAddOnWorkspaceProvider.tsx
@@ -1,16 +1,11 @@
-import {useEffect, useMemo} from 'react'
+import {useMemo} from 'react'
+import {useObservable} from 'react-rx'
import {LoadingBlock} from '../../../../components'
import {type Config, prepareConfig} from '../../../../config'
import {useClient} from '../../../../hooks'
-import {ResourceCacheProvider} from '../../../../store'
-import {
- SourceProvider,
- useAddonDataset,
- useSource,
- useWorkspaceLoader,
- WorkspaceProvider,
-} from '../../../../studio'
+import {ResourceCacheProvider, useAddonDatasetStore} from '../../../../store'
+import {SourceProvider, useSource, useWorkspaceLoader, WorkspaceProvider} from '../../../../studio'
import {API_VERSION} from '../../../constants'
import {type FormMode} from '../../../types'
import {taskSchema} from './taskSchema'
@@ -65,17 +60,10 @@ function TasksAddonWorkspaceProviderInner({
* It also, creates the addon dataset if it doesn't exist.
*/
export function TasksAddonWorkspaceProvider(props: {children: React.ReactNode; mode: FormMode}) {
- const {client: addonDatasetClient, ready, createAddonDataset} = useAddonDataset()
+ const {client$} = useAddonDatasetStore()
+ const {client: addonDatasetClient} = useObservable(client$)!
const addonDataset = addonDatasetClient?.config().dataset
- useEffect(() => {
- if (!addonDataset && ready) {
- // The user is trying to use the addon dataset form, but it hasn't been created yet.
- // We should create it.
- createAddonDataset()
- }
- }, [addonDataset, ready, createAddonDataset])
-
if (!addonDataset) {
return
}
diff --git a/packages/sanity/src/core/tasks/hooks/useTaskOperations.ts b/packages/sanity/src/core/tasks/hooks/useTaskOperations.ts
index 4bd810bb9ca..0821eb82247 100644
--- a/packages/sanity/src/core/tasks/hooks/useTaskOperations.ts
+++ b/packages/sanity/src/core/tasks/hooks/useTaskOperations.ts
@@ -1,7 +1,7 @@
import {useCallback, useMemo} from 'react'
+import {filter, firstValueFrom, map} from 'rxjs'
-import {useCurrentUser} from '../../store'
-import {useAddonDataset} from '../../studio'
+import {useAddonDatasetStore, useCurrentUser} from '../../store'
import {type TaskCreatePayload, type TaskDocument, type TaskEditPayload} from '../types'
/**
@@ -19,7 +19,7 @@ export interface TaskOperations {
* @hidden
*/
export function useTaskOperations(): TaskOperations {
- const {client, createAddonDataset} = useAddonDataset()
+ const {client$} = useAddonDatasetStore()
const currentUser = useCurrentUser()
const handleCreate = useCallback(
@@ -34,19 +34,14 @@ export function useTaskOperations(): TaskOperations {
_type: 'tasks.task',
} satisfies Partial
- if (!client) {
- try {
- const newCreatedClient = await createAddonDataset()
- if (!newCreatedClient) throw new Error('No addon client found. Unable to create task.')
- const created = await newCreatedClient.create(task)
- return created
- } catch (err) {
- // TODO: Handle error
- throw err
- }
- }
-
try {
+ const client = await firstValueFrom(
+ client$.pipe(
+ filter((clientStore) => clientStore.state === 'ready'),
+ map((clientStore) => clientStore.client),
+ ),
+ )
+
const created = await client.create(task)
return created
} catch (err) {
@@ -54,15 +49,19 @@ export function useTaskOperations(): TaskOperations {
throw err
}
},
- [client, createAddonDataset, currentUser],
+ [client$, currentUser],
)
const handleEdit = useCallback(
async (id: string, set: TaskEditPayload) => {
try {
- if (!client) {
- throw new Error('No client. Unable to create task.')
- }
+ const client = await firstValueFrom(
+ client$.pipe(
+ filter((clientStore) => clientStore.state === 'ready'),
+ map((clientStore) => clientStore.client),
+ ),
+ )
+
const edited = (await client.patch(id).set(set).commit()) as TaskDocument
return edited
} catch (e) {
@@ -70,21 +69,25 @@ export function useTaskOperations(): TaskOperations {
throw e
}
},
- [client],
+ [client$],
)
const handleRemove = useCallback(
async (id: string) => {
try {
- if (!client) {
- throw new Error('No client. Unable to create task.')
- }
+ const client = await firstValueFrom(
+ client$.pipe(
+ filter((clientStore) => clientStore.state === 'ready'),
+ map((clientStore) => clientStore.client),
+ ),
+ )
+
await client.delete(id)
} catch (e) {
// TODO: Handle error
throw e
}
},
- [client],
+ [client$],
)
const operations: TaskOperations = useMemo(
diff --git a/packages/sanity/src/core/tasks/plugin/TasksStudioLayout.tsx b/packages/sanity/src/core/tasks/plugin/TasksStudioLayout.tsx
index e87bd2d132c..ceaf3704579 100644
--- a/packages/sanity/src/core/tasks/plugin/TasksStudioLayout.tsx
+++ b/packages/sanity/src/core/tasks/plugin/TasksStudioLayout.tsx
@@ -1,6 +1,5 @@
import {ConditionalWrapper} from '../../../ui-components'
import {type LayoutProps} from '../../config'
-import {AddonDatasetProvider} from '../../studio'
import {
TasksEnabledProvider,
TasksNavigationProvider,
@@ -16,17 +15,15 @@ const TasksStudioLayoutInner = (props: LayoutProps) => {
return props.renderDefault(props)
}
return (
-
- {children}}
- >
-
- {props.renderDefault(props)}
-
-
-
+ {children}}
+ >
+
+ {props.renderDefault(props)}
+
+
)
}
diff --git a/packages/sanity/src/core/tasks/store/useTasksStore.ts b/packages/sanity/src/core/tasks/store/useTasksStore.ts
index f91287c9a26..b680173541b 100644
--- a/packages/sanity/src/core/tasks/store/useTasksStore.ts
+++ b/packages/sanity/src/core/tasks/store/useTasksStore.ts
@@ -1,8 +1,9 @@
import {type ListenEvent, type ListenOptions} from '@sanity/client'
import {useCallback, useEffect, useMemo, useReducer, useState} from 'react'
+import {useObservable} from 'react-rx'
import {catchError, of} from 'rxjs'
+import {useAddonDatasetStore} from 'sanity'
-import {useAddonDataset} from '../../studio'
import {getPublishedId} from '../../util'
import {type Loadable, type TaskDocument} from '../types'
import {tasksReducer, type TasksReducerAction, type TasksReducerState} from './reducer'
@@ -40,7 +41,8 @@ const QUERY_SORT_ORDER = `order(${SORT_FIELD} ${SORT_ORDER})`
const QUERY = `*[${QUERY_FILTERS.join(' && ')}] ${QUERY_PROJECTION} | ${QUERY_SORT_ORDER}`
export function useTasksStore(opts: TasksStoreOptions): TasksStoreReturnType {
- const {client} = useAddonDataset()
+ const {lazyClient$} = useAddonDatasetStore()
+ const {client} = useObservable(lazyClient$)!
const {documentId} = opts
const [state, dispatch] = useReducer(tasksReducer, INITIAL_STATE)