diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d87be83e16a..189c91ed117 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -405,6 +405,9 @@ catalogs: jotai: specifier: 2.20.1 version: 2.20.1 + jotai-effect: + specifier: 2.3.1 + version: 2.3.1 jotai-scope: specifier: 0.11.0 version: 0.11.0 @@ -1252,6 +1255,9 @@ importers: jotai: specifier: 'catalog:' version: 2.20.1(@babel/core@7.29.7)(@babel/template@7.29.7)(@types/react@19.2.17)(react@19.2.7) + jotai-effect: + specifier: 'catalog:' + version: 2.3.1(jotai@2.20.1(@babel/core@7.29.7)(@babel/template@7.29.7)(@types/react@19.2.17)(react@19.2.7)) jotai-scope: specifier: 'catalog:' version: 0.11.0(jotai@2.20.1(@babel/core@7.29.7)(@babel/template@7.29.7)(@types/react@19.2.17)(react@19.2.7))(react@19.2.7) @@ -7364,6 +7370,12 @@ packages: resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} hasBin: true + jotai-effect@2.3.1: + resolution: {integrity: sha512-FBBVXDM2podbbxJsZ19+uDv45LxeXoVA5yh6CfkQ35AVCuRHj7Lanlcjiea3b67Q7+/MMGXWf8+GeB73JcbeMg==} + engines: {node: '>=12.20.0'} + peerDependencies: + jotai: '>=2.20.0' + jotai-scope@0.11.0: resolution: {integrity: sha512-ofiW0Z0i3lTw509Gx0+T6fqsDPMDxMn+AHmNs9iF9OA8CmK1/0xRprPxuZ89UZdBzt6jcrTYdunNZSF2255zJQ==} engines: {node: '>=14.0.0'} @@ -16663,6 +16675,10 @@ snapshots: jiti@2.7.0: {} + jotai-effect@2.3.1(jotai@2.20.1(@babel/core@7.29.7)(@babel/template@7.29.7)(@types/react@19.2.17)(react@19.2.7)): + dependencies: + jotai: 2.20.1(@babel/core@7.29.7)(@babel/template@7.29.7)(@types/react@19.2.17)(react@19.2.7) + jotai-scope@0.11.0(jotai@2.20.1(@babel/core@7.29.7)(@babel/template@7.29.7)(@types/react@19.2.17)(react@19.2.7))(react@19.2.7): dependencies: jotai: 2.20.1(@babel/core@7.29.7)(@babel/template@7.29.7)(@types/react@19.2.17)(react@19.2.7) @@ -20034,6 +20050,7 @@ time: i18next@26.3.3: '2026-06-26T11:23:58.220Z' iconify-import-svg@0.2.0: '2026-04-20T06:18:25.132Z' immer@11.1.8: '2026-05-08T15:09:33.021Z' + jotai-effect@2.3.1: '2026-05-13T18:51:15.911Z' jotai-scope@0.11.0: '2026-05-13T18:43:15.331Z' jotai-tanstack-query@0.11.0: '2025-08-01T02:55:49.826Z' jotai@2.20.1: '2026-06-11T06:30:45.782Z' diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index df50f9c4228..54eba537ae1 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -185,6 +185,7 @@ catalog: iconify-import-svg: 0.2.0 immer: 11.1.8 jotai: 2.20.1 + jotai-effect: 2.3.1 jotai-scope: 0.11.0 jotai-tanstack-query: 0.11.0 js-audio-recorder: 1.0.7 diff --git a/web/app/(commonLayout)/external-service-sync.tsx b/web/app/(commonLayout)/external-service-sync.tsx new file mode 100644 index 00000000000..b0ab3f166c5 --- /dev/null +++ b/web/app/(commonLayout)/external-service-sync.tsx @@ -0,0 +1,12 @@ +'use client' + +import { useAtomValue } from 'jotai' +import { amplitudeIdentitySyncAtom } from '@/context/amplitude-identity-sync' +import { zendeskConversationSyncAtom } from '@/context/zendesk-conversation-sync' + +export function ExternalServiceSync() { + useAtomValue(zendeskConversationSyncAtom) + useAtomValue(amplitudeIdentitySyncAtom) + + return null +} diff --git a/web/app/(commonLayout)/hydration-boundary.tsx b/web/app/(commonLayout)/hydration-boundary.tsx index 4f6156dd8a8..ea6d9a5cbed 100644 --- a/web/app/(commonLayout)/hydration-boundary.tsx +++ b/web/app/(commonLayout)/hydration-boundary.tsx @@ -61,28 +61,22 @@ export async function CommonLayoutHydrationBoundary({ children }: { children: Re const queryClient = getQueryClientServer() const accountProfileUrl = resolveServerConsoleApiUrl(ACCOUNT_PROFILE_PATH) - if (!accountProfileUrl) { - return ( - - {children} - - ) - } + if (accountProfileUrl) { + try { + const context = await getServerConsoleClientContext() - try { - const context = await getServerConsoleClientContext() - - await Promise.all([ - queryClient.fetchQuery(serverUserProfileQueryOptions()), - queryClient.prefetchQuery(serverSystemFeaturesQueryOptions()), - queryClient.prefetchQuery(serverConsoleQuery.workspaces.current.post.queryOptions({ - context, - retry: false, - })), - ]) - } - catch (error) { - await handleProfileError(error) + await Promise.all([ + queryClient.fetchQuery(serverUserProfileQueryOptions()), + queryClient.prefetchQuery(serverSystemFeaturesQueryOptions()), + queryClient.prefetchQuery(serverConsoleQuery.workspaces.current.post.queryOptions({ + context, + retry: false, + })), + ]) + } + catch (error) { + await handleProfileError(error) + } } return ( diff --git a/web/app/(commonLayout)/layout.tsx b/web/app/(commonLayout)/layout.tsx index e70e660f1c8..966ee0b4b43 100644 --- a/web/app/(commonLayout)/layout.tsx +++ b/web/app/(commonLayout)/layout.tsx @@ -1,18 +1,10 @@ import * as React from 'react' -import AmplitudeProvider from '@/app/components/base/amplitude' -import { GoogleAnalyticsScripts } from '@/app/components/base/ga' import Zendesk from '@/app/components/base/zendesk' -import { EducationVerifyActionRecorder } from '@/app/components/education-verify-action-recorder' import MaintenanceNotice from '@/app/components/header/maintenance-notice' import MainNavLayout from '@/app/components/main-nav/layout' import { NextRouteStateBridge } from '@/app/components/next-route-state' -import { OAuthRegistrationAnalytics } from '@/app/components/oauth-registration-analytics' -import { AppBootstrapEffects } from '@/context/app-bootstrap-effects' -import { EventEmitterContextProvider } from '@/context/event-emitter-provider' -import { ModalContextProvider } from '@/context/modal-context-provider' -import { ProviderContextProvider } from '@/context/provider-context-provider' import { CommonLayoutGlobalMounts } from './global-mounts' -import { CommonLayoutHydrationBoundary } from './hydration-boundary' +import { ConsoleContextProviders, ConsoleRuntimeProviders } from './providers' export default async function Layout({ children, @@ -23,28 +15,19 @@ export default async function Layout({ }) { return ( - - - - - +
- - - - - - {children} - - - - - + + + {children} + + +
-
+
) diff --git a/web/app/(commonLayout)/providers.tsx b/web/app/(commonLayout)/providers.tsx new file mode 100644 index 00000000000..6b0b579b2d0 --- /dev/null +++ b/web/app/(commonLayout)/providers.tsx @@ -0,0 +1,37 @@ +import type { ReactNode } from 'react' +import AmplitudeProvider from '@/app/components/base/amplitude' +import { GoogleAnalyticsScripts } from '@/app/components/base/ga' +import { EducationVerifyActionRecorder } from '@/app/components/education-verify-action-recorder' +import { OAuthRegistrationAnalytics } from '@/app/components/oauth-registration-analytics' +import { EventEmitterContextProvider } from '@/context/event-emitter-provider' +import { ModalContextProvider } from '@/context/modal-context-provider' +import { ProviderContextProvider } from '@/context/provider-context-provider' +import { ExternalServiceSync } from './external-service-sync' +import { CommonLayoutHydrationBoundary } from './hydration-boundary' + +export async function ConsoleRuntimeProviders({ children }: { children: ReactNode }) { + return ( + <> + + + + + + + {children} + + + ) +} + +export function ConsoleContextProviders({ children }: { children: ReactNode }) { + return ( + + + + {children} + + + + ) +} diff --git a/web/app/account/(commonLayout)/layout.tsx b/web/app/account/(commonLayout)/layout.tsx index 292010773c7..06ec2ae4e5d 100644 --- a/web/app/account/(commonLayout)/layout.tsx +++ b/web/app/account/(commonLayout)/layout.tsx @@ -1,42 +1,25 @@ import * as React from 'react' -import { CommonLayoutHydrationBoundary } from '@/app/(commonLayout)/hydration-boundary' -import AmplitudeProvider from '@/app/components/base/amplitude' -import { GoogleAnalyticsScripts } from '@/app/components/base/ga' -import { EducationVerifyActionRecorder } from '@/app/components/education-verify-action-recorder' +import { ConsoleContextProviders, ConsoleRuntimeProviders } from '@/app/(commonLayout)/providers' import HeaderWrapper from '@/app/components/header/header-wrapper' import MaintenanceNotice from '@/app/components/header/maintenance-notice' -import { OAuthRegistrationAnalytics } from '@/app/components/oauth-registration-analytics' -import { AppBootstrapEffects } from '@/context/app-bootstrap-effects' -import { EventEmitterContextProvider } from '@/context/event-emitter-provider' -import { ModalContextProvider } from '@/context/modal-context-provider' -import { ProviderContextProvider } from '@/context/provider-context-provider' import Header from './header' export default async function Layout({ children }: { children: React.ReactNode }) { return ( - - - - - +
- - - - - -
- -
- {children} -
- - - + + +
+ +
+ {children} +
+
-
+
) } diff --git a/web/app/components/oauth-registration-analytics.tsx b/web/app/components/oauth-registration-analytics.tsx index 2c0d84edd52..ff6b3b036c0 100644 --- a/web/app/components/oauth-registration-analytics.tsx +++ b/web/app/components/oauth-registration-analytics.tsx @@ -48,8 +48,8 @@ export function OAuthRegistrationAnalytics() { const eventName = utmInfo ? 'user_registration_success_with_utm' : 'user_registration_success' - // Defer the Amplitude event until the user ID is attached. It is flushed in - // AppBootstrapEffects after setUserId runs. Firing it here would record it under an + // Defer the Amplitude event until the user ID is attached. The app context + // external sync replays it after setUserId runs. Firing it here would record it under an // anonymous Amplitude profile (no user ID set yet). rememberRegistrationSuccess({ method: 'oauth', utmInfo }) diff --git a/web/app/signup/set-password/page.tsx b/web/app/signup/set-password/page.tsx index f0087156a85..697aa8e2bdf 100644 --- a/web/app/signup/set-password/page.tsx +++ b/web/app/signup/set-password/page.tsx @@ -78,8 +78,8 @@ const ChangePasswordForm = () => { if (result === 'success') { const utmInfo = parseUtmInfo() rememberCreateAppExternalAttribution({ utmInfo }) - // Defer the Amplitude event until the user ID is attached. It is flushed in - // AppBootstrapEffects after setUserId runs once the redirect lands on /apps. + // Defer the Amplitude event until the user ID is attached. The app context + // external sync replays it after setUserId runs once the redirect lands on /apps. // Firing it here would record it under an anonymous Amplitude profile. rememberRegistrationSuccess({ method: 'email', utmInfo }) diff --git a/web/context/__tests__/app-bootstrap-effects.spec.tsx b/web/context/__tests__/console-bootstrap.spec.tsx similarity index 96% rename from web/context/__tests__/app-bootstrap-effects.spec.tsx rename to web/context/__tests__/console-bootstrap.spec.tsx index 108c2ecf618..617ff9abdbb 100644 --- a/web/context/__tests__/app-bootstrap-effects.spec.tsx +++ b/web/context/__tests__/console-bootstrap.spec.tsx @@ -5,11 +5,11 @@ import { Provider as JotaiProvider, useAtomValue, useSetAtom } from 'jotai' import { queryClientAtom } from 'jotai-tanstack-query' import { useHydrateAtoms } from 'jotai/react/utils' import { Suspense } from 'react' +import { ExternalServiceSync } from '@/app/(commonLayout)/external-service-sync' import { setUserId, setUserProperties } from '@/app/components/base/amplitude' import { flushRegistrationSuccess } from '@/app/components/base/amplitude/registration-tracking' import { setZendeskConversationFields } from '@/app/components/base/zendesk/utils' import { ZENDESK_FIELD_IDS } from '@/config' -import { AppBootstrapEffects } from '../app-bootstrap-effects' import { initialWorkspaceInfo } from '../app-context-defaults' import { currentWorkspaceAtom, @@ -167,7 +167,7 @@ vi.mock('@/app/components/header/maintenance-notice', () => ({ default: () => null, })) -function AppContextProbe() { +function ConsoleBootstrapProbe() { const userProfile = useAtomValue(userProfileAtom) const currentWorkspace = useAtomValue(currentWorkspaceAtom) const roleFlags = useAtomValue(workspaceRoleFlagsAtom) @@ -257,15 +257,15 @@ function createTestQueryClient() { }) } -function renderBootstrapEffects() { +function renderConsoleBootstrap() { const queryClient = createTestQueryClient() const view = render( loading}> - - + + @@ -278,7 +278,7 @@ function renderBootstrapEffects() { } } -describe('AppBootstrapEffects', () => { +describe('Console bootstrap', () => { beforeEach(() => { vi.clearAllMocks() mockPermissionKeysState.isPending = false @@ -339,7 +339,7 @@ describe('AppBootstrapEffects', () => { describe('Bootstrap atoms', () => { it('should provide profile, workspace, permissions, loading state, and version metadata', async () => { - renderBootstrapEffects() + renderConsoleBootstrap() expect(await screen.findByText('user:user@example.com')).toBeInTheDocument() expect(await screen.findByText('workspace:Workspace')).toBeInTheDocument() @@ -360,7 +360,7 @@ describe('AppBootstrapEffects', () => { mockPermissionKeysState.permissionKeys = [] mockLangGeniusVersionState.data = undefined - renderBootstrapEffects() + renderConsoleBootstrap() expect(await screen.findByText('user:')).toBeInTheDocument() expect(screen.getByText(`workspace:${initialWorkspaceInfo.name}`)).toBeInTheDocument() @@ -375,7 +375,7 @@ describe('AppBootstrapEffects', () => { role: 'unsupported-role', } - renderBootstrapEffects() + renderConsoleBootstrap() expect(await screen.findByText(`role:${initialWorkspaceInfo.role}`)).toBeInTheDocument() }) @@ -386,7 +386,7 @@ describe('AppBootstrapEffects', () => { role: 'owner', } - renderBootstrapEffects() + renderConsoleBootstrap() expect(await screen.findByText('manager:true')).toBeInTheDocument() expect(screen.getByText('owner:true')).toBeInTheDocument() @@ -398,7 +398,7 @@ describe('AppBootstrapEffects', () => { mockPermissionKeysState.isPending = true mockCurrentWorkspaceQueryState.isPending = true - renderBootstrapEffects() + renderConsoleBootstrap() expect(await screen.findByText('workspace loading:true')).toBeInTheDocument() expect(screen.getByText('permission loading:true')).toBeInTheDocument() @@ -407,7 +407,7 @@ describe('AppBootstrapEffects', () => { describe('Refresh actions', () => { it('should invalidate the source queries when refresh actions are called', async () => { - const { queryClient } = renderBootstrapEffects() + const { queryClient } = renderConsoleBootstrap() const invalidateQueriesSpy = vi.spyOn(queryClient, 'invalidateQueries') fireEvent.click(await screen.findByRole('button', { name: /refresh user/i })) @@ -420,7 +420,7 @@ describe('AppBootstrapEffects', () => { describe('External side effects', () => { it('should sync Zendesk fields and Amplitude identity when bootstrap data is available', async () => { - renderBootstrapEffects() + renderConsoleBootstrap() await waitFor(() => { expect(setZendeskConversationFields).toHaveBeenCalledWith([{ @@ -469,7 +469,7 @@ describe('AppBootstrapEffects', () => { }, } - renderBootstrapEffects() + renderConsoleBootstrap() await screen.findByText('user:') expect(setUserId).not.toHaveBeenCalled() diff --git a/web/context/amplitude-identity-sync.ts b/web/context/amplitude-identity-sync.ts new file mode 100644 index 00000000000..7ccbf570fba --- /dev/null +++ b/web/context/amplitude-identity-sync.ts @@ -0,0 +1,65 @@ +'use client' + +import type { GetAccountProfileResponse } from '@dify/contracts/api/console/account/types.gen' +import type { ICurrentWorkspace } from '@/models/common' +import { atom } from 'jotai' +import { atomEffect } from 'jotai-effect' +import { setUserId, setUserProperties } from '@/app/components/base/amplitude' +import { flushRegistrationSuccess } from '@/app/components/base/amplitude/registration-tracking' +import { + currentWorkspaceAtom, + userProfileAtom, +} from './app-context-state' + +type AmplitudeProperties = Record + +const amplitudeIdentityAtom = atom(undefined) + +function buildAmplitudeProperties({ + currentWorkspace, + userProfile, +}: { + currentWorkspace: ICurrentWorkspace + userProfile: GetAccountProfileResponse +}) { + const properties: AmplitudeProperties = { + email: userProfile.email, + name: userProfile.name, + has_password: userProfile.is_password_set, + } + + if (currentWorkspace.id) { + properties.workspace_id = currentWorkspace.id + properties.workspace_name = currentWorkspace.name + properties.workspace_plan = currentWorkspace.plan + properties.workspace_status = currentWorkspace.status + properties.workspace_role = currentWorkspace.role + } + + return properties +} + +export const amplitudeIdentitySyncAtom = atomEffect((get, set) => { + const userProfile = get(userProfileAtom) + const currentWorkspace = get(currentWorkspaceAtom) + + if (!userProfile.id) + return + + const properties = buildAmplitudeProperties({ + currentWorkspace, + userProfile, + }) + const identity = JSON.stringify({ + userId: userProfile.email, + properties, + }) + + if (identity === get.peek(amplitudeIdentityAtom)) + return + + setUserId(userProfile.email) + setUserProperties(properties) + flushRegistrationSuccess() + set(amplitudeIdentityAtom, identity) +}) diff --git a/web/context/app-bootstrap-effects.tsx b/web/context/app-bootstrap-effects.tsx deleted file mode 100644 index 0493cfffc99..00000000000 --- a/web/context/app-bootstrap-effects.tsx +++ /dev/null @@ -1,13 +0,0 @@ -'use client' - -import { - useSyncAmplitudeIdentity, - useSyncZendeskFields, -} from './app-context-effects' - -export function AppBootstrapEffects() { - useSyncZendeskFields() - useSyncAmplitudeIdentity() - - return null -} diff --git a/web/context/app-context-effects.ts b/web/context/app-context-effects.ts deleted file mode 100644 index 875cc2ec638..00000000000 --- a/web/context/app-context-effects.ts +++ /dev/null @@ -1,82 +0,0 @@ -'use client' - -import { useAtomValue } from 'jotai' -import { useEffect } from 'react' -import { setUserId, setUserProperties } from '@/app/components/base/amplitude' -import { flushRegistrationSuccess } from '@/app/components/base/amplitude/registration-tracking' -import { setZendeskConversationFields } from '@/app/components/base/zendesk/utils' -import { ZENDESK_FIELD_IDS } from '@/config' -import { - currentWorkspaceAtom, - langGeniusVersionInfoAtom, - userProfileAtom, -} from './app-context-state' - -export function useSyncZendeskFields() { - const userProfile = useAtomValue(userProfileAtom) - const currentWorkspace = useAtomValue(currentWorkspaceAtom) - const langGeniusVersionInfo = useAtomValue(langGeniusVersionInfoAtom) - - useEffect(() => { - if (ZENDESK_FIELD_IDS.ENVIRONMENT && langGeniusVersionInfo?.current_env) { - setZendeskConversationFields([{ - id: ZENDESK_FIELD_IDS.ENVIRONMENT, - value: langGeniusVersionInfo.current_env.toLowerCase(), - }]) - } - }, [langGeniusVersionInfo?.current_env]) - - useEffect(() => { - if (ZENDESK_FIELD_IDS.VERSION && langGeniusVersionInfo?.version) { - setZendeskConversationFields([{ - id: ZENDESK_FIELD_IDS.VERSION, - value: langGeniusVersionInfo.version, - }]) - } - }, [langGeniusVersionInfo?.version]) - - useEffect(() => { - if (ZENDESK_FIELD_IDS.EMAIL && userProfile?.email) { - setZendeskConversationFields([{ - id: ZENDESK_FIELD_IDS.EMAIL, - value: userProfile.email, - }]) - } - }, [userProfile?.email]) - - useEffect(() => { - if (ZENDESK_FIELD_IDS.WORKSPACE_ID && currentWorkspace?.id) { - setZendeskConversationFields([{ - id: ZENDESK_FIELD_IDS.WORKSPACE_ID, - value: currentWorkspace.id, - }]) - } - }, [currentWorkspace?.id]) -} - -export function useSyncAmplitudeIdentity() { - const userProfile = useAtomValue(userProfileAtom) - const currentWorkspace = useAtomValue(currentWorkspaceAtom) - - useEffect(() => { - if (userProfile?.id) { - setUserId(userProfile.email) - const properties: Record = { - email: userProfile.email, - name: userProfile.name, - has_password: userProfile.is_password_set, - } - - if (currentWorkspace?.id) { - properties.workspace_id = currentWorkspace.id - properties.workspace_name = currentWorkspace.name - properties.workspace_plan = currentWorkspace.plan - properties.workspace_status = currentWorkspace.status - properties.workspace_role = currentWorkspace.role - } - - setUserProperties(properties) - flushRegistrationSuccess() - } - }, [userProfile, currentWorkspace]) -} diff --git a/web/context/zendesk-conversation-sync.ts b/web/context/zendesk-conversation-sync.ts new file mode 100644 index 00000000000..535731b4518 --- /dev/null +++ b/web/context/zendesk-conversation-sync.ts @@ -0,0 +1,88 @@ +'use client' + +import { atom } from 'jotai' +import { atomEffect } from 'jotai-effect' +import { setZendeskConversationFields } from '@/app/components/base/zendesk/utils' +import { ZENDESK_FIELD_IDS } from '@/config' +import { + currentWorkspaceAtom, + langGeniusVersionInfoAtom, + userProfileAtom, +} from './app-context-state' + +type ZendeskSyncState = { + email?: string + environment?: string + version?: string + workspaceId?: string +} + +const zendeskConversationSyncStateAtom = atom({}) + +function syncZendeskField({ + fieldId, + previousValue, + setNextValue, + value, +}: { + fieldId: string | undefined + previousValue: string | undefined + setNextValue: (value: string) => void + value: string +}) { + if (!fieldId || !value || value === previousValue) + return false + + setZendeskConversationFields([{ + id: fieldId, + value, + }]) + setNextValue(value) + + return true +} + +export const zendeskConversationSyncAtom = atomEffect((get, set) => { + const userProfile = get(userProfileAtom) + const currentWorkspace = get(currentWorkspaceAtom) + const langGeniusVersionInfo = get(langGeniusVersionInfoAtom) + const state = get.peek(zendeskConversationSyncStateAtom) + const nextState = { ...state } + + let didSync = false + didSync = syncZendeskField({ + fieldId: ZENDESK_FIELD_IDS.ENVIRONMENT, + value: langGeniusVersionInfo.current_env.toLowerCase(), + previousValue: state.environment, + setNextValue: (value) => { + nextState.environment = value + }, + }) || didSync + didSync = syncZendeskField({ + fieldId: ZENDESK_FIELD_IDS.VERSION, + value: langGeniusVersionInfo.version, + previousValue: state.version, + setNextValue: (value) => { + nextState.version = value + }, + }) || didSync + didSync = syncZendeskField({ + fieldId: ZENDESK_FIELD_IDS.EMAIL, + value: userProfile.email, + previousValue: state.email, + setNextValue: (value) => { + nextState.email = value + }, + }) || didSync + didSync = syncZendeskField({ + fieldId: ZENDESK_FIELD_IDS.WORKSPACE_ID, + value: currentWorkspace.id, + previousValue: state.workspaceId, + setNextValue: (value) => { + nextState.workspaceId = value + }, + }) || didSync + + if (didSync) + set(zendeskConversationSyncStateAtom, nextState) +}) diff --git a/web/package.json b/web/package.json index f4ff8e96e73..7f61d6ed3ab 100644 --- a/web/package.json +++ b/web/package.json @@ -109,6 +109,7 @@ "i18next-resources-to-backend": "catalog:", "immer": "catalog:", "jotai": "catalog:", + "jotai-effect": "catalog:", "jotai-scope": "catalog:", "jotai-tanstack-form": "workspace:*", "jotai-tanstack-query": "catalog:",