Compare commits

..
Author SHA1 Message Date
Sonarly Claude CodeandClaude Opus 4.6 0e081fe590 Fix TypeError when fieldMetadataItem is undefined in RecordTableCellFieldContextWrapper
Add null guard for fieldMetadataItem lookup from fieldMetadataItemByFieldMetadataItemId
map. When a recordField's fieldMetadataItemId doesn't exist in the map (e.g. during
navigation away from a record table view), the lookup returns undefined and accessing
.name on it throws "Cannot read properties of undefined (reading 'name')".

Sentry issue: https://sentry.io/issues/6918487847/

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 03:25:43 +00:00
271 changed files with 1028 additions and 1492 deletions
@@ -5,7 +5,6 @@ import { useIsCurrentLocationOnAWorkspace } from '@/domain-manager/hooks/useIsCu
import { useDefaultHomePagePath } from '@/navigation/hooks/useDefaultHomePagePath';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { useOnboardingStatus } from '@/onboarding/hooks/useOnboardingStatus';
import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2';
import { useIsWorkspaceActivationStatusEqualsTo } from '@/workspace/hooks/useIsWorkspaceActivationStatusEqualsTo';
import { useLocation, useParams } from 'react-router-dom';
import { useRecoilValue } from 'recoil';
@@ -50,9 +49,7 @@ export const usePageChangeEffectNavigateLocation = () => {
const objectMetadataItem = objectMetadataItems?.find(
(objectMetadataItem) => objectMetadataItem.namePlural === objectNamePlural,
);
const verifyEmailRedirectPath = useRecoilValueV2(
verifyEmailRedirectPathState,
);
const verifyEmailRedirectPath = useRecoilValue(verifyEmailRedirectPathState);
if (
(!isLoggedIn || (isLoggedIn && !isOnAWorkspace)) &&
@@ -17,7 +17,6 @@ import { hasAnySoftDeleteFilterOnViewComponentSelector } from '@/object-record/r
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2';
import { useContext, useMemo } from 'react';
import { useRecoilCallback, useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
@@ -137,7 +136,7 @@ export const useShouldActionBeRegisteredParams = ({
forceRegisteredActionsByKeyState,
);
const currentWorkspace = useRecoilValueV2(currentWorkspaceState);
const currentWorkspace = useRecoilValue(currentWorkspaceState);
const isFeatureFlagEnabled = (featureFlagKey: FeatureFlagKey) => {
const featureFlag = currentWorkspace?.featureFlags?.find(
@@ -2,7 +2,7 @@ import { css, useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { t } from '@lingui/core/macro';
import { format } from 'date-fns';
import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2';
import { useRecoilValue } from 'recoil';
import {
CalendarChannelVisibility,
type TimelineCalendarEvent,
@@ -85,7 +85,7 @@ export const CalendarEventRow = ({
className,
}: CalendarEventRowProps) => {
const theme = useTheme();
const currentWorkspaceMember = useRecoilValueV2(currentWorkspaceMemberState);
const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
const { openCalendarEventInCommandMenu } =
useOpenCalendarEventInCommandMenu();
@@ -2,14 +2,12 @@ import { gql, InMemoryCache } from '@apollo/client';
import { MockedProvider } from '@apollo/client/testing';
import { act, renderHook } from '@testing-library/react';
import { type ReactNode } from 'react';
import { Provider as JotaiProvider } from 'jotai';
import { RecoilRoot, useSetRecoilState } from 'recoil';
import { useActivityTargetObjectRecords } from '@/activities/hooks/useActivityTargetObjectRecords';
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { jotaiStore } from '@/ui/utilities/state/jotai/jotaiStore';
import { SnackBarComponentInstanceContext } from '@/ui/feedback/snack-bar-manager/contexts/SnackBarComponentInstanceContext';
import { JestObjectMetadataItemSetter } from '~/testing/jest/JestObjectMetadataItemSetter';
import { mockWorkspaceMembers } from '~/testing/mock-data/workspace-members';
@@ -113,27 +111,26 @@ const task = {
};
const Wrapper = ({ children }: { children: ReactNode }) => (
<JotaiProvider store={jotaiStore}>
<RecoilRoot>
<MockedProvider cache={cache}>
<JestObjectMetadataItemSetter>
<SnackBarComponentInstanceContext.Provider
value={{ instanceId: 'snack-bar-manager' }}
>
{children}
</SnackBarComponentInstanceContext.Provider>
</JestObjectMetadataItemSetter>
</MockedProvider>
</RecoilRoot>
</JotaiProvider>
<RecoilRoot>
<MockedProvider cache={cache}>
<JestObjectMetadataItemSetter>
<SnackBarComponentInstanceContext.Provider
value={{ instanceId: 'snack-bar-manager' }}
>
{children}
</SnackBarComponentInstanceContext.Provider>
</JestObjectMetadataItemSetter>
</MockedProvider>
</RecoilRoot>
);
describe('useActivityTargetObjectRecords', () => {
it('return targetObjects', async () => {
jotaiStore.set(currentWorkspaceMemberState.atom, mockWorkspaceMembers[0]);
const { result } = renderHook(
() => {
const setCurrentWorkspaceMember = useSetRecoilState(
currentWorkspaceMemberState,
);
const setObjectMetadataItems = useSetRecoilState(
objectMetadataItemsState,
);
@@ -148,6 +145,7 @@ describe('useActivityTargetObjectRecords', () => {
return {
activityTargetObjectRecords,
setCurrentWorkspaceMember,
setObjectMetadataItems,
setRecordFromStore,
};
@@ -156,6 +154,7 @@ describe('useActivityTargetObjectRecords', () => {
);
act(() => {
result.current.setCurrentWorkspaceMember(mockWorkspaceMembers[0]);
result.current.setObjectMetadataItems(generatedMockObjectMetadataItems);
result.current.setRecordFromStore(task);
});
@@ -1,5 +1,4 @@
import styled from '@emotion/styled';
import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2';
import { useContext } from 'react';
import { useRecoilValue } from 'recoil';
@@ -93,7 +92,7 @@ export const EventRow = ({
event,
mainObjectMetadataItem,
}: EventRowProps) => {
const currentWorkspaceMember = useRecoilValueV2(currentWorkspaceMemberState);
const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
const allowRequestsToTwentyIcons = useRecoilValue(
allowRequestsToTwentyIconsState,
@@ -1,6 +1,7 @@
import styled from '@emotion/styled';
import { EditorContent } from '@tiptap/react';
import { useState } from 'react';
import { useRecoilValue } from 'recoil';
import { LightButton } from 'twenty-ui/input';
import { DropZone } from '@/activities/files/components/DropZone';
@@ -22,7 +23,6 @@ import { useAIChatFileUpload } from '@/ai/hooks/useAIChatFileUpload';
import { useAgentChatContextOrThrow } from '@/ai/hooks/useAgentChatContextOrThrow';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2';
const StyledContainer = styled.div<{ isDraggingFile: boolean }>`
background: ${({ theme }) => theme.background.primary};
@@ -144,7 +144,7 @@ export const AIChatTab = () => {
const hasMessages = messages.length > 0;
const { uploadFiles } = useAIChatFileUpload();
const currentWorkspace = useRecoilValueV2(currentWorkspaceState);
const currentWorkspace = useRecoilValue(currentWorkspaceState);
const smartModelLabel = useAiModelLabel(currentWorkspace?.smartModel, false);
const { editor, handleSendAndClear } = useAIChatEditor({
@@ -1,3 +1,5 @@
import { useSetRecoilState } from 'recoil';
import { useGetBrowsingContext } from '@/ai/hooks/useBrowsingContext';
import { agentChatSelectedFilesStateV2 } from '@/ai/states/agentChatSelectedFilesStateV2';
import { agentChatUploadedFilesStateV2 } from '@/ai/states/agentChatUploadedFilesStateV2';
@@ -21,7 +23,7 @@ import { REACT_APP_SERVER_BASE_URL } from '~/config';
import { cookieStorage } from '~/utils/cookie-storage';
export const useAgentChat = (uiMessages: ExtendedUIMessage[]) => {
const setTokenPair = useSetRecoilStateV2(tokenPairState);
const setTokenPair = useSetRecoilState(tokenPairState);
const setAgentChatUsage = useSetRecoilStateV2(agentChatUsageStateV2);
const { getBrowsingContext } = useGetBrowsingContext();
@@ -1,42 +1,41 @@
import { InMemoryCache, type NormalizedCacheObject } from '@apollo/client';
import { useMemo, useRef } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import { useRecoilState, useSetRecoilState } from 'recoil';
import { ApolloFactory, type Options } from '@/apollo/services/apollo.factory';
import { currentUserState } from '@/auth/states/currentUserState';
import { currentUserWorkspaceState } from '@/auth/states/currentUserWorkspaceState';
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { previousUrlState } from '@/auth/states/previousUrlState';
import { tokenPairState } from '@/auth/states/tokenPairState';
import { appVersionState } from '@/client-config/states/appVersionState';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { useRecoilStateV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilStateV2';
import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2';
import { useSetRecoilStateV2 } from '@/ui/utilities/state/jotai/hooks/useSetRecoilStateV2';
import { AppPath } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { REACT_APP_SERVER_BASE_URL } from '~/config';
import { useUpdateEffect } from '~/hooks/useUpdateEffect';
import { isMatchingLocation } from '~/utils/isMatchingLocation';
import { ApolloFactory, type Options } from '@/apollo/services/apollo.factory';
import { currentUserWorkspaceState } from '@/auth/states/currentUserWorkspaceState';
import { appVersionState } from '@/client-config/states/appVersionState';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { AppPath } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { useSetRecoilStateV2 } from '@/ui/utilities/state/jotai/hooks/useSetRecoilStateV2';
import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2';
export const useApolloFactory = (options: Partial<Options<any>> = {}) => {
// eslint-disable-next-line twenty/no-state-useref
const apolloRef = useRef<ApolloFactory<NormalizedCacheObject> | null>(null);
const navigate = useNavigate();
const setTokenPair = useSetRecoilStateV2(tokenPairState);
const [currentWorkspace, setCurrentWorkspace] = useRecoilStateV2(
const setTokenPair = useSetRecoilState(tokenPairState);
const [currentWorkspace, setCurrentWorkspace] = useRecoilState(
currentWorkspaceState,
);
const appVersion = useRecoilValueV2(appVersionState);
const [currentWorkspaceMember, setCurrentWorkspaceMember] = useRecoilStateV2(
const [currentWorkspaceMember, setCurrentWorkspaceMember] = useRecoilState(
currentWorkspaceMemberState,
);
const setCurrentUser = useSetRecoilStateV2(currentUserState);
const setCurrentUserWorkspace = useSetRecoilStateV2(
currentUserWorkspaceState,
);
const setCurrentUser = useSetRecoilState(currentUserState);
const setCurrentUserWorkspace = useSetRecoilState(currentUserWorkspaceState);
const setPreviousUrl = useSetRecoilStateV2(previousUrlState);
const location = useLocation();
@@ -1,13 +1,13 @@
import { useCreateAppRouter } from '@/app/hooks/useCreateAppRouter';
import { currentUserState } from '@/auth/states/currentUserState';
import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2';
import { RouterProvider } from 'react-router-dom';
import { useRecoilValue } from 'recoil';
export const AppRouter = () => {
// We want to disable logic function settings but keep the code for now
const isFunctionSettingsEnabled = false;
const currentUser = useRecoilValueV2(currentUserState);
const currentUser = useRecoilValue(currentUserState);
const isAdminPageEnabled =
(currentUser?.canImpersonate || currentUser?.canAccessFullAdminPanel) ??
@@ -1,3 +1,12 @@
import { useEffect, useState } from 'react';
import {
matchPath,
useLocation,
useNavigate,
useParams,
} from 'react-router-dom';
import { useRecoilCallback, useRecoilValue } from 'recoil';
import {
setSessionId,
useEventTracker,
@@ -6,15 +15,6 @@ import { useExecuteTasksOnAnyLocationChange } from '@/app/hooks/useExecuteTasksO
import { isAppEffectRedirectEnabledState } from '@/app/states/isAppEffectRedirectEnabledState';
import { useRequestFreshCaptchaToken } from '@/captcha/hooks/useRequestFreshCaptchaToken';
import { isCaptchaScriptLoadedState } from '@/captcha/states/isCaptchaScriptLoadedState';
import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2';
import { useEffect, useState } from 'react';
import {
matchPath,
useLocation,
useNavigate,
useParams,
} from 'react-router-dom';
import { useRecoilCallback } from 'recoil';
import { isCaptchaRequiredForPath } from '@/captcha/utils/isCaptchaRequiredForPath';
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState';
@@ -93,7 +93,7 @@ export const PageChangeEffect = () => {
const { executeTasksOnAnyLocationChange } =
useExecuteTasksOnAnyLocationChange();
const isAppEffectRedirectEnabled = useRecoilValueV2(
const isAppEffectRedirectEnabled = useRecoilValue(
isAppEffectRedirectEnabledState,
);
@@ -366,7 +366,7 @@ export const PageChangeEffect = () => {
}, [eventTracker, location.pathname]);
const { requestFreshCaptchaToken } = useRequestFreshCaptchaToken();
const isCaptchaScriptLoaded = useRecoilValueV2(isCaptchaScriptLoadedState);
const isCaptchaScriptLoaded = useRecoilValue(isCaptchaScriptLoadedState);
useEffect(() => {
if (isCaptchaScriptLoaded && isCaptchaRequiredForPath(location.pathname)) {
@@ -1,60 +1,63 @@
import { useCallback } from 'react';
import { useRecoilCallback } from 'recoil';
import { billingCheckoutSessionState } from '@/auth/states/billingCheckoutSessionState';
import { type BillingCheckoutSession } from '@/auth/types/billingCheckoutSession.type';
import { BILLING_CHECKOUT_SESSION_DEFAULT_VALUE } from '@/billing/constants/BillingCheckoutSessionDefaultValue';
import { jotaiStore } from '@/ui/utilities/state/jotai/jotaiStore';
import deepEqual from 'deep-equal';
// Initialize state that are hydrated from query parameters
// We used to use recoil-sync to do this, but it was causing issues with Firefox
export const useInitializeQueryParamState = () => {
const initializeQueryParamState = useCallback(() => {
const handlers = {
billingCheckoutSession: (value: string) => {
const billingCheckoutSession = jotaiStore.get(
billingCheckoutSessionState.atom,
);
const initializeQueryParamState = useRecoilCallback(
({ set, snapshot }) =>
() => {
const handlers = {
billingCheckoutSession: (value: string) => {
const billingCheckoutSession = snapshot
.getLoadable(billingCheckoutSessionState)
.getValue();
try {
const parsedValue = JSON.parse(decodeURIComponent(value));
try {
const parsedValue = JSON.parse(decodeURIComponent(value));
if (
typeof parsedValue === 'object' &&
parsedValue !== null &&
'plan' in parsedValue &&
'interval' in parsedValue &&
'requirePaymentMethod' in parsedValue &&
!deepEqual(billingCheckoutSession, parsedValue)
) {
jotaiStore.set(
billingCheckoutSessionState.atom,
parsedValue as BillingCheckoutSession,
);
if (
typeof parsedValue === 'object' &&
parsedValue !== null &&
'plan' in parsedValue &&
'interval' in parsedValue &&
'requirePaymentMethod' in parsedValue &&
!deepEqual(billingCheckoutSession, parsedValue)
) {
set(
billingCheckoutSessionState,
parsedValue as BillingCheckoutSession,
);
}
} catch (error) {
// eslint-disable-next-line no-console
console.error(
'Failed to parse billingCheckoutSession from URL',
error,
);
set(
billingCheckoutSessionState,
BILLING_CHECKOUT_SESSION_DEFAULT_VALUE,
);
}
},
};
const queryParams = new URLSearchParams(window.location.search);
for (const [paramName, handler] of Object.entries(handlers)) {
const value = queryParams.get(paramName);
if (value !== null) {
handler(value);
}
} catch (error) {
// eslint-disable-next-line no-console
console.error(
'Failed to parse billingCheckoutSession from URL',
error,
);
jotaiStore.set(
billingCheckoutSessionState.atom,
BILLING_CHECKOUT_SESSION_DEFAULT_VALUE,
);
}
},
};
const queryParams = new URLSearchParams(window.location.search);
for (const [paramName, handler] of Object.entries(handlers)) {
const value = queryParams.get(paramName);
if (value !== null) {
handler(value);
}
}
}, []);
[],
);
return { initializeQueryParamState };
};
@@ -1,5 +1,5 @@
import { createStateV2 } from '@/ui/utilities/state/jotai/utils/createStateV2';
export const isAppEffectRedirectEnabledState = createStateV2<boolean>({
import { createState } from '@/ui/utilities/state/utils/createState';
export const isAppEffectRedirectEnabledState = createState<boolean>({
key: 'isAppEffectRedirectEnabledState',
defaultValue: true,
});
@@ -1,6 +1,6 @@
import { createStateV2 } from '@/ui/utilities/state/jotai/utils/createStateV2';
import { createState } from '@/ui/utilities/state/utils/createState';
export const verifyEmailRedirectPathState = createStateV2<string | undefined>({
export const verifyEmailRedirectPathState = createState<string | undefined>({
key: 'verifyEmailRedirectPathState',
defaultValue: undefined,
});
@@ -1,15 +1,13 @@
import React from 'react';
import { useRecoilValue } from 'recoil';
import { AuthContext } from '@/auth/contexts/AuthContext';
import { currentWorkspaceDeletedMembersState } from '@/auth/states/currentWorkspaceDeletedMembersState';
import { currentWorkspaceMembersState } from '@/auth/states/currentWorkspaceMembersState';
import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2';
export const AuthProvider = ({ children }: React.PropsWithChildren) => {
const currentWorkspaceMembers = useRecoilValueV2(
currentWorkspaceMembersState,
);
const currentWorkspaceDeletedMembers = useRecoilValueV2(
const currentWorkspaceMembers = useRecoilValue(currentWorkspaceMembersState);
const currentWorkspaceDeletedMembers = useRecoilValue(
currentWorkspaceDeletedMembersState,
);
@@ -5,6 +5,7 @@ import { useCurrentUserWorkspaceTwoFactorAuthentication } from '@/settings/two-f
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { useLingui } from '@lingui/react/macro';
import { useEffect } from 'react';
import { useRecoilValue } from 'recoil';
import { useSetRecoilStateV2 } from '@/ui/utilities/state/jotai/hooks/useSetRecoilStateV2';
import { AppPath } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
@@ -18,7 +19,7 @@ export const TwoFactorAuthenticationSetupEffect = () => {
const navigate = useNavigateApp();
const { origin } = useOrigin();
const loginToken = useRecoilValueV2(loginTokenState);
const loginToken = useRecoilValue(loginTokenState);
const qrCode = useRecoilValueV2(qrCodeState);
const setQrCodeState = useSetRecoilStateV2(qrCodeState);
@@ -12,12 +12,11 @@ import { Modal } from '@/ui/layout/modal/components/Modal';
import { useLingui } from '@lingui/react/macro';
import { useEffect, useState } from 'react';
import { useSearchParams } from 'react-router-dom';
import { useRecoilValue } from 'recoil';
import { useRecoilValue, useSetRecoilState } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
import { useNavigateApp } from '~/hooks/useNavigateApp';
import { getWorkspaceUrl } from '~/utils/getWorkspaceUrl';
import { EmailVerificationSent } from '@/auth/sign-in-up/components/EmailVerificationSent';
import { useSetRecoilStateV2 } from '@/ui/utilities/state/jotai/hooks/useSetRecoilStateV2';
export const VerifyEmailEffect = () => {
const {
@@ -30,7 +29,7 @@ export const VerifyEmailEffect = () => {
const [searchParams] = useSearchParams();
const [isError, setIsError] = useState(false);
const setVerifyEmailRedirectPath = useSetRecoilStateV2(
const setVerifyEmailRedirectPath = useSetRecoilState(
verifyEmailRedirectPathState,
);
@@ -4,7 +4,6 @@ import { isDeveloperDefaultSignInPrefilledState } from '@/client-config/states/i
import { supportChatState } from '@/client-config/states/supportChatState';
import { workspaceAuthProvidersState } from '@/workspace/states/workspaceAuthProvidersState';
import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2';
import { useApolloClient } from '@apollo/client';
import { MockedProvider } from '@apollo/client/testing';
import { type ReactNode, act } from 'react';
@@ -159,7 +158,7 @@ describe('useAuth', () => {
const { result } = renderHook(
() => {
const client = useApolloClient();
const workspaceAuthProviders = useRecoilValueV2(
const workspaceAuthProviders = useRecoilValue(
workspaceAuthProvidersState,
);
const billing = useRecoilValue(billingState);
@@ -1,15 +1,14 @@
import { act, renderHook } from '@testing-library/react';
import { RecoilRoot } from 'recoil';
import { RecoilRoot, useSetRecoilState } from 'recoil';
import { useIsLogged } from '@/auth/hooks/useIsLogged';
import { tokenPairState } from '@/auth/states/tokenPairState';
import { useSetRecoilStateV2 } from '@/ui/utilities/state/jotai/hooks/useSetRecoilStateV2';
const renderHooks = () => {
const { result } = renderHook(
() => {
const isLogged = useIsLogged();
const setTokenPair = useSetRecoilStateV2(tokenPairState);
const setTokenPair = useSetRecoilState(tokenPairState);
return {
isLogged,
@@ -5,6 +5,7 @@ import {
useGotoRecoilSnapshot,
useRecoilCallback,
useRecoilValue,
useSetRecoilState,
} from 'recoil';
import { AppPath } from 'twenty-shared/types';
@@ -28,9 +29,6 @@ import {
import { tokenPairState } from '@/auth/states/tokenPairState';
import { isDeveloperDefaultSignInPrefilledState } from '@/client-config/states/isDeveloperDefaultSignInPrefilledState';
import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2';
import { useSetRecoilStateV2 } from '@/ui/utilities/state/jotai/hooks/useSetRecoilStateV2';
import { jotaiStore } from '@/ui/utilities/state/jotai/jotaiStore';
import { isAppEffectRedirectEnabledState } from '@/app/states/isAppEffectRedirectEnabledState';
import { useSignUpInNewWorkspace } from '@/auth/sign-in-up/hooks/useSignUpInNewWorkspace';
@@ -73,15 +71,15 @@ import { cookieStorage } from '~/utils/cookie-storage';
import { getWorkspaceUrl } from '~/utils/getWorkspaceUrl';
export const useAuth = () => {
const setTokenPair = useSetRecoilStateV2(tokenPairState);
const setLoginToken = useSetRecoilStateV2(loginTokenState);
const setIsAppEffectRedirectEnabled = useSetRecoilStateV2(
const setTokenPair = useSetRecoilState(tokenPairState);
const setLoginToken = useSetRecoilState(loginTokenState);
const setIsAppEffectRedirectEnabled = useSetRecoilState(
isAppEffectRedirectEnabledState,
);
const { origin } = useOrigin();
const { requestFreshCaptchaToken } = useRequestFreshCaptchaToken();
const isCaptchaScriptLoaded = useRecoilValueV2(isCaptchaScriptLoadedState);
const isCaptchaScriptLoaded = useRecoilValue(isCaptchaScriptLoadedState);
const isMultiWorkspaceEnabled = useRecoilValue(isMultiWorkspaceEnabledState);
const isEmailVerificationRequired = useRecoilValue(
isEmailVerificationRequiredState,
@@ -91,7 +89,7 @@ export const useAuth = () => {
const { refreshObjectMetadataItems } = useRefreshObjectMetadataItems();
const { createWorkspace } = useSignUpInNewWorkspace();
const setSignInUpStep = useSetRecoilStateV2(signInUpStepState);
const setSignInUpStep = useSetRecoilState(signInUpStepState);
const { redirect } = useRedirect();
const { redirectToWorkspaceDomain } = useRedirectToWorkspaceDomain();
@@ -108,7 +106,7 @@ export const useAuth = () => {
useVerifyEmailAndGetWorkspaceAgnosticTokenMutation();
const [getAuthTokensFromOtp] = useGetAuthTokensFromOtpMutation();
const workspacePublicData = useRecoilValueV2(workspacePublicDataState);
const workspacePublicData = useRecoilValue(workspacePublicDataState);
const { setLastAuthenticateWorkspaceDomain } =
useLastAuthenticatedWorkspaceDomain();
@@ -125,7 +123,7 @@ export const useAuth = () => {
const { loadMockedObjectMetadataItems } = useLoadMockedObjectMetadataItems();
const clearSession = useRecoilCallback(
({ snapshot }) =>
({ snapshot, set }) =>
async () => {
const sseClient = getSnapshotValue(snapshot, sseClientState);
@@ -133,9 +131,9 @@ export const useAuth = () => {
const emptySnapshot = snapshot_UNSTABLE();
const authProvidersValue = jotaiStore.get(
workspaceAuthProvidersState.atom,
);
const authProvidersValue = snapshot
.getLoadable(workspaceAuthProvidersState)
.getValue();
const billing = snapshot.getLoadable(billingState).getValue();
const isDeveloperDefaultSignInPrefilled = snapshot
.getLoadable(isDeveloperDefaultSignInPrefilledState)
@@ -145,28 +143,26 @@ export const useAuth = () => {
const clientConfigApiStatus = snapshot
.getLoadable(clientConfigApiStatusState)
.getValue();
const isCurrentUserLoadedValue = jotaiStore.get(
isCurrentUserLoadedState.atom,
);
const isCurrentUserLoaded = snapshot
.getLoadable(isCurrentUserLoadedState)
.getValue();
const isMultiWorkspaceEnabled = snapshot
.getLoadable(isMultiWorkspaceEnabledState)
.getValue();
const domainConfigurationValue = jotaiStore.get(
domainConfigurationState.atom,
);
const domainConfiguration = snapshot
.getLoadable(domainConfigurationState)
.getValue();
const apiConfig = snapshot.getLoadable(apiConfigState).getValue();
const sentryConfig = snapshot.getLoadable(sentryConfigState).getValue();
const workspacePublicDataValue = jotaiStore.get(
workspacePublicDataState.atom,
);
const lastAuthenticatedMethod = jotaiStore.get(
lastAuthenticatedMethodState.atom,
);
const isCaptchaScriptLoadedValue = jotaiStore.get(
isCaptchaScriptLoadedState.atom,
);
const workspacePublicData = snapshot
.getLoadable(workspacePublicDataState)
.getValue();
const lastAuthenticatedMethod = snapshot
.getLoadable(lastAuthenticatedMethodState)
.getValue();
const initialSnapshot = emptySnapshot.map(({ set }) => {
set(workspaceAuthProvidersState, authProvidersValue);
set(billingState, billing);
set(
isDeveloperDefaultSignInPrefilledState,
@@ -176,8 +172,12 @@ export const useAuth = () => {
set(captchaState, captcha);
set(apiConfigState, apiConfig);
set(sentryConfigState, sentryConfig);
set(workspacePublicDataState, workspacePublicData);
set(clientConfigApiStatusState, clientConfigApiStatus);
set(isCurrentUserLoadedState, isCurrentUserLoaded);
set(isMultiWorkspaceEnabledState, isMultiWorkspaceEnabled);
set(domainConfigurationState, domainConfiguration);
set(isCaptchaScriptLoadedState, isCaptchaScriptLoaded);
return undefined;
});
@@ -186,18 +186,7 @@ export const useAuth = () => {
goToRecoilSnapshot(initialSnapshot);
jotaiStore.set(workspaceAuthProvidersState.atom, authProvidersValue);
jotaiStore.set(workspacePublicDataState.atom, workspacePublicDataValue);
jotaiStore.set(isCurrentUserLoadedState.atom, isCurrentUserLoadedValue);
jotaiStore.set(domainConfigurationState.atom, domainConfigurationValue);
jotaiStore.set(
isCaptchaScriptLoadedState.atom,
isCaptchaScriptLoadedValue,
);
jotaiStore.set(
lastAuthenticatedMethodState.atom,
lastAuthenticatedMethod,
);
set(lastAuthenticatedMethodState, lastAuthenticatedMethod);
await client.clearStore();
setLastAuthenticateWorkspaceDomain(null);
@@ -210,6 +199,7 @@ export const useAuth = () => {
setLastAuthenticateWorkspaceDomain,
loadMockedObjectMetadataItems,
navigate,
isCaptchaScriptLoaded,
],
);
@@ -1,7 +1,8 @@
import { useRecoilState } from 'recoil';
import { tokenPairState } from '@/auth/states/tokenPairState';
import { useRecoilStateV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilStateV2';
export const useIsLogged = (): boolean => {
const [tokenPair] = useRecoilStateV2(tokenPairState);
const [tokenPair] = useRecoilState(tokenPairState);
return !!tokenPair;
};
@@ -9,7 +9,7 @@ import {
} from '@/auth/states/signInUpStepState';
import { OnboardingModalCircularIcon } from '@/onboarding/components/OnboardingModalCircularIcon';
import { t } from '@lingui/core/macro';
import { useSetRecoilStateV2 } from '@/ui/utilities/state/jotai/hooks/useSetRecoilStateV2';
import { useSetRecoilState } from 'recoil';
import {
IconGmail,
IconMail,
@@ -89,7 +89,7 @@ export const EmailVerificationSent = ({
email: string | null;
isError?: boolean;
}) => {
const setSignInUpStep = useSetRecoilStateV2(signInUpStepState);
const setSignInUpStep = useSetRecoilState(signInUpStepState);
const { handleResendEmailVerificationToken, loading: isLoading } =
useHandleResendEmailVerificationToken();
@@ -5,6 +5,7 @@ import styled from '@emotion/styled';
import { Trans, useLingui } from '@lingui/react/macro';
import { motion } from 'framer-motion';
import { FormProvider } from 'react-hook-form';
import { useRecoilValue } from 'recoil';
import { ClickToActionLink, UndecoratedLink } from 'twenty-ui/navigation';
import { useAuth } from '@/auth/hooks/useAuth';
@@ -125,12 +126,12 @@ const StyledActionLinkContainer = styled.div`
export const SignInUpGlobalScopeForm = () => {
const authProviders = useRecoilValueV2(authProvidersState);
const signInUpStep = useRecoilValueV2(signInUpStepState);
const signInUpStep = useRecoilValue(signInUpStepState);
const { buildWorkspaceUrl } = useBuildWorkspaceUrl();
const { signOut } = useAuth();
const { createWorkspace } = useSignUpInNewWorkspace();
const availableWorkspaces = useRecoilValueV2(availableWorkspacesState);
const availableWorkspaces = useRecoilValue(availableWorkspacesState);
const theme = useTheme();
const { t } = useLingui();
@@ -12,9 +12,9 @@ import { workspaceAuthProvidersState } from '@/workspace/states/workspaceAuthPro
import styled from '@emotion/styled';
import { Trans } from '@lingui/react/macro';
import { FormProvider } from 'react-hook-form';
import { useRecoilValue } from 'recoil';
import { HorizontalSeparator } from 'twenty-ui/display';
import { ClickToActionLink } from 'twenty-ui/navigation';
import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2';
const StyledContentContainer = styled.div`
margin-bottom: ${({ theme }) => theme.spacing(8)};
@@ -23,8 +23,8 @@ const StyledContentContainer = styled.div`
`;
export const SignInUpWorkspaceScopeForm = () => {
const workspaceAuthProviders = useRecoilValueV2(workspaceAuthProvidersState);
const workspaceAuthBypassProviders = useRecoilValueV2(
const workspaceAuthProviders = useRecoilValue(workspaceAuthProvidersState);
const workspaceAuthBypassProviders = useRecoilValue(
workspaceAuthBypassProvidersState,
);
const { shouldOfferBypass, shouldUseBypass } = useWorkspaceBypass();
@@ -6,11 +6,11 @@ import {
import { useLoadCurrentUser } from '@/users/hooks/useLoadCurrentUser';
import { useEffect } from 'react';
import { useSearchParams } from 'react-router-dom';
import { useSetRecoilStateV2 } from '@/ui/utilities/state/jotai/hooks/useSetRecoilStateV2';
import { useSetRecoilState } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
export const SignInUpGlobalScopeFormEffect = () => {
const setSignInUpStep = useSetRecoilStateV2(signInUpStepState);
const setSignInUpStep = useSetRecoilState(signInUpStepState);
const [searchParams, setSearchParams] = useSearchParams();
const { setAuthTokens } = useAuth();
const { loadCurrentUser } = useLoadCurrentUser();
@@ -3,13 +3,13 @@
import { useSSO } from '@/auth/sign-in-up/hooks/useSSO';
import { guessSSOIdentityProviderIconByUrl } from '@/settings/security/utils/guessSSOIdentityProviderIconByUrl';
import styled from '@emotion/styled';
import { useRecoilValue } from 'recoil';
import { workspaceAuthProvidersState } from '@/workspace/states/workspaceAuthProvidersState';
import React from 'react';
import { isDefined } from 'twenty-shared/utils';
import { HorizontalSeparator } from 'twenty-ui/display';
import { MainButton } from 'twenty-ui/input';
import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2';
const StyledContentContainer = styled.div`
margin-bottom: ${({ theme }) => theme.spacing(8)};
@@ -17,7 +17,7 @@ const StyledContentContainer = styled.div`
`;
export const SignInUpSSOIdentityProviderSelection = () => {
const workspaceAuthProviders = useRecoilValueV2(workspaceAuthProvidersState);
const workspaceAuthProviders = useRecoilValue(workspaceAuthProvidersState);
const { redirectToSSOLoginPage } = useSSO();
@@ -9,7 +9,7 @@ import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { Trans, useLingui } from '@lingui/react/macro';
import QRCode from 'react-qr-code';
import { useSetRecoilStateV2 } from '@/ui/utilities/state/jotai/hooks/useSetRecoilStateV2';
import { useSetRecoilState } from 'recoil';
import { IconCopy } from 'twenty-ui/display';
import { Loader } from 'twenty-ui/feedback';
import { MainButton } from 'twenty-ui/input';
@@ -71,7 +71,7 @@ export const SignInUpTwoFactorAuthenticationProvision = () => {
const theme = useTheme();
const { copyToClipboard } = useCopyToClipboard();
const qrCode = useRecoilValueV2(qrCodeState);
const setSignInUpStep = useSetRecoilStateV2(signInUpStepState);
const setSignInUpStep = useSetRecoilState(signInUpStepState);
const handleClick = () => {
setSignInUpStep(SignInUpStep.TwoFactorAuthenticationVerification);
@@ -18,12 +18,11 @@ import { Trans, useLingui } from '@lingui/react/macro';
import { OTPInput, type SlotProps } from 'input-otp';
import { useState } from 'react';
import { Controller } from 'react-hook-form';
import { useRecoilValue, useSetRecoilState } from 'recoil';
import { AppPath } from 'twenty-shared/types';
import { MainButton } from 'twenty-ui/input';
import { ClickToActionLink } from 'twenty-ui/navigation';
import { useNavigateApp } from '~/hooks/useNavigateApp';
import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2';
import { useSetRecoilStateV2 } from '@/ui/utilities/state/jotai/hooks/useSetRecoilStateV2';
const StyledMainContentContainer = styled.div`
margin-bottom: ${({ theme }) => theme.spacing(8)};
@@ -187,8 +186,8 @@ export const SignInUpTOTPVerification = () => {
const navigate = useNavigateApp();
const { readCaptchaToken } = useReadCaptchaToken();
const { isCaptchaReady } = useCaptcha();
const loginToken = useRecoilValueV2(loginTokenState);
const setSignInUpStep = useSetRecoilStateV2(signInUpStepState);
const loginToken = useRecoilValue(loginTokenState);
const setSignInUpStep = useSetRecoilState(signInUpStepState);
const { t } = useLingui();
const { form } = useTwoFactorAuthenticationForm();
@@ -19,12 +19,10 @@ import styled from '@emotion/styled';
import { useLingui } from '@lingui/react/macro';
import { useMemo, useState } from 'react';
import { useFormContext } from 'react-hook-form';
import { useRecoilValue } from 'recoil';
import { useRecoilState, useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
import { Loader } from 'twenty-ui/feedback';
import { MainButton } from 'twenty-ui/input';
import { useRecoilStateV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilStateV2';
import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2';
const StyledForm = styled.form`
align-items: center;
@@ -41,15 +39,13 @@ export const SignInUpWithCredentials = ({
const { t } = useLingui();
const form = useFormContext<Form>();
const [signInUpStep, setSignInUpStep] = useRecoilStateV2(signInUpStepState);
const [signInUpStep, setSignInUpStep] = useRecoilState(signInUpStepState);
const [showErrors, setShowErrors] = useState(false);
const captcha = useRecoilValue(captchaState);
const isRequestingCaptchaToken = useRecoilValueV2(
const isRequestingCaptchaToken = useRecoilValue(
isRequestingCaptchaTokenState,
);
const lastAuthenticatedMethod = useRecoilValueV2(
lastAuthenticatedMethodState,
);
const lastAuthenticatedMethod = useRecoilValue(lastAuthenticatedMethodState);
const hasMultipleAuthMethods = useHasMultipleAuthMethods();
const {
@@ -10,10 +10,9 @@ import { type SocialSSOSignInUpActionType } from '@/auth/types/socialSSOSignInUp
import { useTheme } from '@emotion/react';
import { useLingui } from '@lingui/react/macro';
import { memo } from 'react';
import { useRecoilState, useRecoilValue } from 'recoil';
import { HorizontalSeparator, IconGoogle } from 'twenty-ui/display';
import { MainButton } from 'twenty-ui/input';
import { useRecoilStateV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilStateV2';
import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2';
import { LastUsedPill } from './LastUsedPill';
import { StyledSSOButtonContainer } from './SignInUpSSOButtonStyles';
@@ -30,9 +29,10 @@ export const SignInUpWithGoogle = ({
isGlobalScope?: boolean;
}) => {
const { t } = useLingui();
const signInUpStep = useRecoilValueV2(signInUpStepState);
const [lastAuthenticatedMethod, setLastAuthenticatedMethod] =
useRecoilStateV2(lastAuthenticatedMethodState);
const signInUpStep = useRecoilValue(signInUpStepState);
const [lastAuthenticatedMethod, setLastAuthenticatedMethod] = useRecoilState(
lastAuthenticatedMethodState,
);
const { signInWithGoogle } = useSignInWithGoogle();
const hasMultipleAuthMethods = useHasMultipleAuthMethods();
@@ -9,10 +9,9 @@ import { AuthenticatedMethod } from '@/auth/types/AuthenticatedMethod.enum';
import { type SocialSSOSignInUpActionType } from '@/auth/types/socialSSOSignInUp.type';
import { useTheme } from '@emotion/react';
import { useLingui } from '@lingui/react/macro';
import { useRecoilState, useRecoilValue } from 'recoil';
import { HorizontalSeparator, IconMicrosoft } from 'twenty-ui/display';
import { MainButton } from 'twenty-ui/input';
import { useRecoilStateV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilStateV2';
import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2';
import { LastUsedPill } from './LastUsedPill';
import { StyledSSOButtonContainer } from './SignInUpSSOButtonStyles';
@@ -26,9 +25,10 @@ export const SignInUpWithMicrosoft = ({
const theme = useTheme();
const { t } = useLingui();
const signInUpStep = useRecoilValueV2(signInUpStepState);
const [lastAuthenticatedMethod, setLastAuthenticatedMethod] =
useRecoilStateV2(lastAuthenticatedMethodState);
const signInUpStep = useRecoilValue(signInUpStepState);
const [lastAuthenticatedMethod, setLastAuthenticatedMethod] = useRecoilState(
lastAuthenticatedMethodState,
);
const { signInWithMicrosoft } = useSignInWithMicrosoft();
const hasMultipleAuthMethods = useHasMultipleAuthMethods();
@@ -7,11 +7,9 @@ import {
} from '@/auth/states/signInUpStepState';
import { AuthenticatedMethod } from '@/auth/types/AuthenticatedMethod.enum';
import { workspaceAuthProvidersState } from '@/workspace/states/workspaceAuthProvidersState';
import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2';
import { useRecoilStateV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilStateV2';
import { useSetRecoilStateV2 } from '@/ui/utilities/state/jotai/hooks/useSetRecoilStateV2';
import { useTheme } from '@emotion/react';
import { useLingui } from '@lingui/react/macro';
import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
import { HorizontalSeparator, IconLock } from 'twenty-ui/display';
import { MainButton } from 'twenty-ui/input';
@@ -21,11 +19,12 @@ import { StyledSSOButtonContainer } from './SignInUpSSOButtonStyles';
export const SignInUpWithSSO = () => {
const theme = useTheme();
const { t } = useLingui();
const setSignInUpStep = useSetRecoilStateV2(signInUpStepState);
const workspaceAuthProviders = useRecoilValueV2(workspaceAuthProvidersState);
const signInUpStep = useRecoilValueV2(signInUpStepState);
const [lastAuthenticatedMethod, setLastAuthenticatedMethod] =
useRecoilStateV2(lastAuthenticatedMethodState);
const setSignInUpStep = useSetRecoilState(signInUpStepState);
const workspaceAuthProviders = useRecoilValue(workspaceAuthProvidersState);
const signInUpStep = useRecoilValue(signInUpStepState);
const [lastAuthenticatedMethod, setLastAuthenticatedMethod] = useRecoilState(
lastAuthenticatedMethodState,
);
const hasMultipleAuthMethods = useHasMultipleAuthMethods();
const { redirectToSSOLoginPage } = useSSO();
@@ -7,10 +7,8 @@ import {
import { isRequestingCaptchaTokenState } from '@/captcha/states/isRequestingCaptchaTokenState';
import { captchaState } from '@/client-config/states/captchaState';
import { workspaceAuthProvidersState } from '@/workspace/states/workspaceAuthProvidersState';
import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2';
import { useSetRecoilStateV2 } from '@/ui/utilities/state/jotai/hooks/useSetRecoilStateV2';
import { useEffect, useState } from 'react';
import { useRecoilValue } from 'recoil';
import { useRecoilValue, useSetRecoilState } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
const searchParams = new URLSearchParams(window.location.search);
@@ -23,9 +21,9 @@ enum LoadingStatus {
}
export const SignInUpWorkspaceScopeFormEffect = () => {
const workspaceAuthProviders = useRecoilValueV2(workspaceAuthProvidersState);
const workspaceAuthProviders = useRecoilValue(workspaceAuthProvidersState);
const isRequestingCaptchaToken = useRecoilValueV2(
const isRequestingCaptchaToken = useRecoilValue(
isRequestingCaptchaTokenState,
);
@@ -40,7 +38,7 @@ export const SignInUpWorkspaceScopeFormEffect = () => {
const { signInUpStep, continueWithEmail, continueWithCredentials } =
useSignInUp(form);
const setSignInUpStep = useSetRecoilStateV2(signInUpStepState);
const setSignInUpStep = useSetRecoilState(signInUpStepState);
useEffect(() => {
if (!workspaceAuthProviders) {
@@ -1,14 +1,11 @@
import { i18n } from '@lingui/core';
import { I18nProvider } from '@lingui/react';
import { act, renderHook } from '@testing-library/react';
import { type ReactNode, createElement } from 'react';
import { Provider as JotaiProvider } from 'jotai';
import { RecoilRoot } from 'recoil';
import { useHandleResetPassword } from '@/auth/sign-in-up/hooks/useHandleResetPassword';
import { workspacePublicDataState } from '@/auth/states/workspacePublicDataState';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { jotaiStore } from '@/ui/utilities/state/jotai/jotaiStore';
import { SOURCE_LOCALE } from 'twenty-shared/translations';
import {
type PublicWorkspaceDataOutput,
@@ -23,21 +20,16 @@ jest.mock('~/generated-metadata/graphql');
dynamicActivate(SOURCE_LOCALE);
const renderHooks = () => {
jotaiStore.set(workspacePublicDataState.atom, {
id: 'workspace-id',
} as PublicWorkspaceDataOutput);
const { result } = renderHook(() => useHandleResetPassword(), {
wrapper: ({ children }: { children: ReactNode }) =>
createElement(
JotaiProvider,
{ store: jotaiStore },
createElement(
RecoilRoot,
null as any,
createElement(I18nProvider, { i18n }, children),
),
),
wrapper: ({ children }) =>
RecoilRoot({
initializeState: ({ set }) => {
set(workspacePublicDataState, {
id: 'workspace-id',
} as PublicWorkspaceDataOutput);
},
children: I18nProvider({ i18n, children }),
}),
});
return { result };
};
@@ -5,14 +5,14 @@ import { workspacePublicDataState } from '@/auth/states/workspacePublicDataState
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { ApolloError } from '@apollo/client';
import { useLingui } from '@lingui/react/macro';
import { useRecoilValue } from 'recoil';
import { useEmailPasswordResetLinkMutation } from '~/generated-metadata/graphql';
import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2';
export const useHandleResetPassword = () => {
const { enqueueErrorSnackBar, enqueueSuccessSnackBar } = useSnackBar();
const [emailPasswordResetLink] = useEmailPasswordResetLinkMutation();
const workspacePublicData = useRecoilValueV2(workspacePublicDataState);
const currentUser = useRecoilValueV2(currentUserState);
const workspacePublicData = useRecoilValue(workspacePublicDataState);
const currentUser = useRecoilValue(currentUserState);
const { t } = useLingui();
@@ -1,8 +1,8 @@
import { workspaceAuthProvidersState } from '@/workspace/states/workspaceAuthProvidersState';
import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2';
import { useRecoilValue } from 'recoil';
export const useHasMultipleAuthMethods = () => {
const workspaceAuthProviders = useRecoilValueV2(workspaceAuthProvidersState);
const workspaceAuthProviders = useRecoilValue(workspaceAuthProvidersState);
if (!workspaceAuthProviders) {
return false;
@@ -18,23 +18,23 @@ import { useIsCurrentLocationOnAWorkspace } from '@/domain-manager/hooks/useIsCu
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { ApolloError } from '@apollo/client';
import { useLingui } from '@lingui/react/macro';
import { useRecoilState, useSetRecoilState } from 'recoil';
import { AppPath } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { buildAppPathWithQueryParams } from '~/utils/buildAppPathWithQueryParams';
import { isMatchingLocation } from '~/utils/isMatchingLocation';
import { useAuth } from '@/auth/hooks/useAuth';
import { useRecoilStateV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilStateV2';
import { useSetRecoilStateV2 } from '@/ui/utilities/state/jotai/hooks/useSetRecoilStateV2';
export const useSignInUp = (form: UseFormReturn<Form>) => {
const { enqueueErrorSnackBar } = useSnackBar();
const { t } = useLingui();
const [signInUpStep, setSignInUpStep] = useRecoilStateV2(signInUpStepState);
const [signInUpStep, setSignInUpStep] = useRecoilState(signInUpStepState);
const [signInUpMode, setSignInUpMode] = useRecoilStateV2(signInUpModeState);
const { isOnAWorkspace } = useIsCurrentLocationOnAWorkspace();
const { isCaptchaReady } = useCaptcha();
const setLastAuthenticatedMethod = useSetRecoilStateV2(
const setLastAuthenticatedMethod = useSetRecoilState(
lastAuthenticatedMethodState,
);
@@ -13,7 +13,6 @@ import {
import { PASSWORD_REGEX } from '@/auth/utils/passwordRegex';
import { isDeveloperDefaultSignInPrefilledState } from '@/client-config/states/isDeveloperDefaultSignInPrefilledState';
import { isDefined } from 'twenty-shared/utils';
import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2';
const makeValidationSchema = (signInUpStep: SignInUpStep) =>
z
@@ -35,7 +34,7 @@ const makeValidationSchema = (signInUpStep: SignInUpStep) =>
export type Form = z.infer<ReturnType<typeof makeValidationSchema>>;
export const useSignInUpForm = () => {
const signInUpStep = useRecoilValueV2(signInUpStepState);
const signInUpStep = useRecoilValue(signInUpStepState);
const validationSchema = makeValidationSchema(signInUpStep); // Create schema based on the current step
@@ -2,15 +2,15 @@ import { useParams, useSearchParams } from 'react-router-dom';
import { useAuth } from '@/auth/hooks/useAuth';
import { billingCheckoutSessionState } from '@/auth/states/billingCheckoutSessionState';
import { useRecoilValue } from 'recoil';
import { type SocialSSOSignInUpActionType } from '@/auth/types/socialSSOSignInUp.type';
import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2';
export const useSignInWithMicrosoft = () => {
const workspaceInviteHash = useParams().workspaceInviteHash;
const [searchParams] = useSearchParams();
const workspacePersonalInviteToken =
searchParams.get('inviteToken') ?? undefined;
const billingCheckoutSession = useRecoilValueV2(billingCheckoutSessionState);
const billingCheckoutSession = useRecoilValue(billingCheckoutSessionState);
const { signInWithMicrosoft } = useAuth();
return {
@@ -1,16 +1,16 @@
import { useRecoilState, useRecoilValue } from 'recoil';
import { useIsCurrentLocationOnAWorkspace } from '@/domain-manager/hooks/useIsCurrentLocationOnAWorkspace';
import { workspaceAuthBypassProvidersState } from '@/workspace/states/workspaceAuthBypassProvidersState';
import { workspaceAuthProvidersState } from '@/workspace/states/workspaceAuthProvidersState';
import { workspaceBypassModeState } from '@/workspace/states/workspaceBypassModeState';
import { useRecoilStateV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilStateV2';
import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2';
export const useWorkspaceBypass = () => {
const workspaceAuthProviders = useRecoilValueV2(workspaceAuthProvidersState);
const workspaceAuthBypassProviders = useRecoilValueV2(
const workspaceAuthProviders = useRecoilValue(workspaceAuthProvidersState);
const workspaceAuthBypassProviders = useRecoilValue(
workspaceAuthBypassProvidersState,
);
const [workspaceBypassMode, setWorkspaceBypassMode] = useRecoilStateV2(
const [workspaceBypassMode, setWorkspaceBypassMode] = useRecoilState(
workspaceBypassModeState,
);
@@ -1,8 +1,8 @@
import { useState } from 'react';
import { useParams } from 'react-router-dom';
import { useRecoilValue } from 'recoil';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
@@ -16,7 +16,7 @@ export const useWorkspaceFromInviteHash = () => {
const { enqueueErrorSnackBar, enqueueInfoSnackBar } = useSnackBar();
const navigate = useNavigateApp();
const workspaceInviteHash = useParams().workspaceInviteHash;
const currentWorkspace = useRecoilValueV2(currentWorkspaceState);
const currentWorkspace = useRecoilValue(currentWorkspaceState);
const [initiallyLoggedIn] = useState(isDefined(currentWorkspace));
const { data: workspaceFromInviteHash, loading } =
@@ -1,7 +1,7 @@
import { createStateV2 } from '@/ui/utilities/state/jotai/utils/createStateV2';
import { createState } from '@/ui/utilities/state/utils/createState';
import { type AvailableWorkspaces } from '~/generated-metadata/graphql';
export const availableWorkspacesState = createStateV2<AvailableWorkspaces>({
export const availableWorkspacesState = createState<AvailableWorkspaces>({
key: 'availableWorkspacesState',
defaultValue: {
availableWorkspacesForSignIn: [],
@@ -1,9 +1,8 @@
import { type BillingCheckoutSession } from '@/auth/types/billingCheckoutSession.type';
import { BILLING_CHECKOUT_SESSION_DEFAULT_VALUE } from '@/billing/constants/BillingCheckoutSessionDefaultValue';
import { createStateV2 } from '@/ui/utilities/state/jotai/utils/createStateV2';
import { createState } from '@/ui/utilities/state/utils/createState';
export const billingCheckoutSessionState =
createStateV2<BillingCheckoutSession>({
key: 'billingCheckoutSessionState',
defaultValue: BILLING_CHECKOUT_SESSION_DEFAULT_VALUE,
});
export const billingCheckoutSessionState = createState<BillingCheckoutSession>({
key: 'billingCheckoutSessionState',
defaultValue: BILLING_CHECKOUT_SESSION_DEFAULT_VALUE,
});
@@ -1,4 +1,4 @@
import { createStateV2 } from '@/ui/utilities/state/jotai/utils/createStateV2';
import { createState } from '@/ui/utilities/state/utils/createState';
import { type User } from '~/generated-metadata/graphql';
export type CurrentUser = Pick<
@@ -15,7 +15,7 @@ export type CurrentUser = Pick<
| 'hasPassword'
>;
export const currentUserState = createStateV2<CurrentUser | null>({
export const currentUserState = createState<CurrentUser | null>({
key: 'currentUserState',
defaultValue: null,
});
@@ -1,5 +1,5 @@
import { type ObjectPermissions } from 'twenty-shared/types';
import { createStateV2 } from '@/ui/utilities/state/jotai/utils/createStateV2';
import { createState } from '@/ui/utilities/state/utils/createState';
import { type UserWorkspace } from '~/generated-metadata/graphql';
export type CurrentUserWorkspace = Pick<
@@ -10,7 +10,7 @@ export type CurrentUserWorkspace = Pick<
};
export const currentUserWorkspaceState =
createStateV2<CurrentUserWorkspace | null>({
createState<CurrentUserWorkspace | null>({
key: 'currentUserWorkspaceState',
defaultValue: null,
});
@@ -1,7 +1,7 @@
import { createStateV2 } from '@/ui/utilities/state/jotai/utils/createStateV2';
import { createState } from '@/ui/utilities/state/utils/createState';
import { type DeletedWorkspaceMember } from '~/generated-metadata/graphql';
export const currentWorkspaceDeletedMembersState = createStateV2<
export const currentWorkspaceDeletedMembersState = createState<
DeletedWorkspaceMember[]
>({
key: 'currentWorkspaceDeletedMembersState',
@@ -1,5 +1,5 @@
import { type WorkspaceMember } from '@/workspace-member/types/WorkspaceMember';
import { createStateV2 } from '@/ui/utilities/state/jotai/utils/createStateV2';
import { createState } from '@/ui/utilities/state/utils/createState';
export type CurrentWorkspaceMember = Omit<
WorkspaceMember,
@@ -7,7 +7,7 @@ export type CurrentWorkspaceMember = Omit<
>;
export const currentWorkspaceMemberState =
createStateV2<CurrentWorkspaceMember | null>({
createState<CurrentWorkspaceMember | null>({
key: 'currentWorkspaceMemberState',
defaultValue: null,
});
@@ -1,7 +1,7 @@
import { type PartialWorkspaceMember } from '@/settings/roles/types/RoleWithPartialMembers';
import { createStateV2 } from '@/ui/utilities/state/jotai/utils/createStateV2';
import { createState } from '@/ui/utilities/state/utils/createState';
export const currentWorkspaceMembersState = createStateV2<
export const currentWorkspaceMembersState = createState<
PartialWorkspaceMember[]
>({
key: 'currentWorkspaceMembersState',
@@ -1,4 +1,4 @@
import { createStateV2 } from '@/ui/utilities/state/jotai/utils/createStateV2';
import { createState } from '@/ui/utilities/state/utils/createState';
import {
type Application,
type Role,
@@ -43,7 +43,7 @@ export type CurrentWorkspace = Pick<
workspaceCustomApplication: Pick<Application, 'id'> | null;
};
export const currentWorkspaceState = createStateV2<CurrentWorkspace | null>({
export const currentWorkspaceState = createState<CurrentWorkspace | null>({
key: 'currentWorkspaceState',
defaultValue: null,
});
@@ -1,5 +1,5 @@
import { createStateV2 } from '@/ui/utilities/state/jotai/utils/createStateV2';
export const isCurrentUserLoadedState = createStateV2<boolean>({
import { createState } from '@/ui/utilities/state/utils/createState';
export const isCurrentUserLoadedState = createState<boolean>({
key: 'isCurrentUserLoadedState',
defaultValue: false,
});
@@ -1,10 +1,9 @@
import { jwtDecode } from 'jwt-decode';
import { selector } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
import { tokenPairState } from './tokenPairState';
import { tokenPairState } from '@/auth/states/tokenPairState';
import { createSelectorV2 } from '@/ui/utilities/state/jotai/utils/createSelectorV2';
export const isImpersonatingState = createSelectorV2<boolean>({
export const isImpersonatingState = selector<boolean>({
key: 'isImpersonatingState',
get: ({ get }) => {
const tokenPair = get(tokenPairState);
@@ -1,9 +1,12 @@
import { type AuthenticatedMethod } from '@/auth/types/AuthenticatedMethod.enum';
import { createStateV2 } from '@/ui/utilities/state/jotai/utils/createStateV2';
import { atom } from 'recoil';
export const lastAuthenticatedMethodState =
createStateV2<AuthenticatedMethod | null>({
key: 'lastAuthenticatedMethodState',
defaultValue: null,
useLocalStorage: true,
});
import { type AuthenticatedMethod } from '@/auth/types/AuthenticatedMethod.enum';
import { localStorageEffect } from '~/utils/recoil/localStorageEffect';
const LAST_AUTHENTICATED_METHOD_STORAGE_KEY = 'lastAuthenticatedMethodState';
export const lastAuthenticatedMethodState = atom<AuthenticatedMethod | null>({
key: LAST_AUTHENTICATED_METHOD_STORAGE_KEY,
default: null,
effects: [localStorageEffect()],
});
@@ -1,7 +1,7 @@
import { createStateV2 } from '@/ui/utilities/state/jotai/utils/createStateV2';
import { createState } from '@/ui/utilities/state/utils/createState';
import { type AuthToken } from '~/generated-metadata/graphql';
export const loginTokenState = createStateV2<AuthToken['token'] | null>({
export const loginTokenState = createState<AuthToken['token'] | null>({
key: 'loginTokenState',
defaultValue: null,
});
@@ -1,10 +1,7 @@
import { currentUserWorkspaceState } from '@/auth/states/currentUserWorkspaceState';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { jotaiStore } from '@/ui/utilities/state/jotai/jotaiStore';
import { selectorFamily } from 'recoil';
// Temporary bridge: reads from Jotai store for migrated state.
// Will be fully migrated to Jotai in PR 6.
export const objectPermissionsFamilySelector = selectorFamily<
{
canRead: boolean;
@@ -16,9 +13,7 @@ export const objectPermissionsFamilySelector = selectorFamily<
get:
({ objectNameSingular }) =>
({ get }) => {
const currentUserWorkspace = jotaiStore.get(
currentUserWorkspaceState.atom,
);
const currentUserWorkspace = get(currentUserWorkspaceState);
const objectMetadataItems = get(objectMetadataItemsState);
const objectMetadataItem = objectMetadataItems.find(
@@ -1,4 +1,4 @@
import { createStateV2 } from '@/ui/utilities/state/jotai/utils/createStateV2';
import { createState } from '@/ui/utilities/state/utils/createState';
export enum SignInUpStep {
Init = 'init',
Email = 'email',
@@ -10,7 +10,7 @@ export enum SignInUpStep {
TwoFactorAuthenticationProvision = 'TwoFactorAuthenticationProvision',
}
export const signInUpStepState = createStateV2<SignInUpStep>({
export const signInUpStepState = createState<SignInUpStep>({
key: 'signInUpStepState',
defaultValue: SignInUpStep.Init,
});
@@ -1,12 +1,18 @@
import { createStateV2 } from '@/ui/utilities/state/jotai/utils/createStateV2';
import { createState } from '@/ui/utilities/state/utils/createState';
import { type AuthTokenPair } from '~/generated-metadata/graphql';
import { cookieStorageEffect } from '~/utils/recoil/cookieStorageEffect';
export const tokenPairState = createStateV2<AuthTokenPair | null>({
export const tokenPairState = createState<AuthTokenPair | null>({
key: 'tokenPairState',
defaultValue: null,
useCookieStorage: {
cookieKey: 'tokenPair',
validateInitFn: (payload: AuthTokenPair) =>
Boolean(payload['accessOrWorkspaceAgnosticToken']),
},
effects: [
cookieStorageEffect(
'tokenPair',
{},
{
validateInitFn: (payload: AuthTokenPair) =>
Boolean(payload['accessOrWorkspaceAgnosticToken']),
},
),
],
});
@@ -1,8 +1,8 @@
import { type PublicWorkspaceDataOutput } from '~/generated-metadata/graphql';
import { createStateV2 } from '@/ui/utilities/state/jotai/utils/createStateV2';
import { createState } from '@/ui/utilities/state/utils/createState';
export const workspacePublicDataState =
createStateV2<PublicWorkspaceDataOutput | null>({
createState<PublicWorkspaceDataOutput | null>({
key: 'workspacePublicDataState',
defaultValue: null,
});
@@ -1,4 +1,5 @@
import { useLingui } from '@lingui/react/macro';
import { useRecoilValue } from 'recoil';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { SettingsBillingCreditsSection } from '@/billing/components/SettingsBillingCreditsSection';
@@ -15,14 +16,13 @@ import {
useBillingPortalSessionQuery,
} from '~/generated-metadata/graphql';
import { useGetWorkflowNodeExecutionUsage } from '@/billing/hooks/useGetWorkflowNodeExecutionUsage';
import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2';
export const SettingsBillingContent = () => {
const { t } = useLingui();
const { redirect } = useRedirect();
const currentWorkspace = useRecoilValueV2(currentWorkspaceState);
const currentWorkspace = useRecoilValue(currentWorkspaceState);
const subscriptions = currentWorkspace?.billingSubscriptions;
@@ -29,8 +29,8 @@ import { useModal } from '@/ui/layout/modal/hooks/useModal';
import { useSubscriptionStatus } from '@/workspace/hooks/useSubscriptionStatus';
import styled from '@emotion/styled';
import { useLingui } from '@lingui/react/macro';
import { useSetRecoilStateV2 } from '@/ui/utilities/state/jotai/hooks/useSetRecoilStateV2';
import { useMemo, useState } from 'react';
import { useSetRecoilState } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
import {
H2Title,
@@ -144,7 +144,7 @@ export const SettingsBillingSubscriptionInfo = ({
const [cancelSwitchMeteredPrice] = useCancelSwitchMeteredPriceMutation();
const setCurrentWorkspace = useSetRecoilStateV2(currentWorkspaceState);
const setCurrentWorkspace = useSetRecoilState(currentWorkspaceState);
const isTrialPeriod = subscriptionStatus === SubscriptionStatus.Trialing;
@@ -7,7 +7,6 @@ import {
type MeteredBillingPrice,
} from '@/billing/types/billing-price-tiers.type';
import { useNumberFormat } from '@/localization/hooks/useNumberFormat';
import { useRecoilStateV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilStateV2';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { Select } from '@/ui/input/components/Select';
import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal';
@@ -15,6 +14,7 @@ import { useModal } from '@/ui/layout/modal/hooks/useModal';
import styled from '@emotion/styled';
import { t } from '@lingui/core/macro';
import { useState } from 'react';
import { useRecoilState } from 'recoil';
import { findOrThrow, isDefined } from 'twenty-shared/utils';
import { H2Title } from 'twenty-ui/display';
import { Button } from 'twenty-ui/input';
@@ -48,7 +48,7 @@ export const MeteredPriceSelector = ({
const { currentMeteredBillingPrice } = useCurrentMetered();
const { formatNumber } = useNumberFormat();
const [currentWorkspace, setCurrentWorkspace] = useRecoilStateV2(
const [currentWorkspace, setCurrentWorkspace] = useRecoilState(
currentWorkspaceState,
);
@@ -12,12 +12,12 @@ import { beautifyExactDate } from '~/utils/date-utils';
import { useCurrentPlan } from '@/billing/hooks/useCurrentPlan';
import { useCurrentMetered } from '@/billing/hooks/useCurrentMetered';
import { useCurrentBillingFlags } from '@/billing/hooks/useCurrentBillingFlags';
import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2';
import { useRecoilValue } from 'recoil';
export const useBillingWording = () => {
const { t } = useLingui();
const currentWorkspace = useRecoilValueV2(currentWorkspaceState);
const currentWorkspace = useRecoilValue(currentWorkspaceState);
assertIsDefinedOrThrow(currentWorkspace);
@@ -3,11 +3,11 @@ import {
BillingPlanKey,
} from '~/generated-metadata/graphql';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2';
import { useRecoilValue } from 'recoil';
import { assertIsDefinedOrThrow } from 'twenty-shared/utils';
export const useCurrentBillingFlags = () => {
const currentWorkspace = useRecoilValueV2(currentWorkspaceState);
const currentWorkspace = useRecoilValue(currentWorkspaceState);
assertIsDefinedOrThrow(currentWorkspace);
@@ -1,7 +1,7 @@
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { useCurrentPlan } from '@/billing/hooks/useCurrentPlan';
import type { MeteredBillingPrice } from '@/billing/types/billing-price-tiers.type';
import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2';
import { useRecoilValue } from 'recoil';
import { assertIsDefinedOrThrow, findOrThrow } from 'twenty-shared/utils';
import {
BillingProductKey,
@@ -11,7 +11,7 @@ import {
export const useCurrentMetered = () => {
const { currentPlan } = useCurrentPlan();
const currentWorkspace = useRecoilValueV2(currentWorkspaceState);
const currentWorkspace = useRecoilValue(currentWorkspaceState);
assertIsDefinedOrThrow(currentWorkspace);
@@ -2,12 +2,12 @@ import { BillingPlanKey } from '~/generated-metadata/graphql';
import { assertIsDefinedOrThrow, findOrThrow } from 'twenty-shared/utils';
import { usePlans } from './usePlans';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2';
import { useRecoilValue } from 'recoil';
export const useCurrentPlan = () => {
const { listPlans } = usePlans();
const currentWorkspace = useRecoilValueV2(currentWorkspaceState);
const currentWorkspace = useRecoilValue(currentWorkspaceState);
assertIsDefinedOrThrow(currentWorkspace);
@@ -1,16 +1,16 @@
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { useRedirect } from '@/domain-manager/hooks/useRedirect';
import { useRecoilStateV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilStateV2';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { t } from '@lingui/core/macro';
import { useState } from 'react';
import { useRecoilState } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
import { useEndSubscriptionTrialPeriodMutation } from '~/generated-metadata/graphql';
export const useEndSubscriptionTrialPeriod = () => {
const { enqueueSuccessSnackBar, enqueueErrorSnackBar } = useSnackBar();
const [endSubscriptionTrialPeriod] = useEndSubscriptionTrialPeriodMutation();
const [currentWorkspace, setCurrentWorkspace] = useRecoilStateV2(
const [currentWorkspace, setCurrentWorkspace] = useRecoilState(
currentWorkspaceState,
);
const [isLoading, setIsLoading] = useState(false);
@@ -1,8 +1,8 @@
import { useRecoilValue } from 'recoil';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2';
export const useHasNextBillingPhase = () => {
const currentWorkspace = useRecoilValueV2(currentWorkspaceState);
const currentWorkspace = useRecoilValue(currentWorkspaceState);
const hasNextBillingPhase =
currentWorkspace?.currentBillingSubscription?.phases.length === 2;
@@ -1,8 +1,8 @@
import { useRecoilValue } from 'recoil';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2';
export const useNextBillingPhase = () => {
const currentWorkspace = useRecoilValueV2(currentWorkspaceState);
const currentWorkspace = useRecoilValue(currentWorkspaceState);
const nextBillingPhase =
currentWorkspace?.currentBillingSubscription?.phases[1];
@@ -1,19 +1,19 @@
import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';
import { useRecoilValue, useSetRecoilState } from 'recoil';
import { useRequestFreshCaptchaToken } from '@/captcha/hooks/useRequestFreshCaptchaToken';
import { isCaptchaScriptLoadedState } from '@/captcha/states/isCaptchaScriptLoadedState';
import { getCaptchaUrlByProvider } from '@/captcha/utils/getCaptchaUrlByProvider';
import { isCaptchaRequiredForPath } from '@/captcha/utils/isCaptchaRequiredForPath';
import { useCaptcha } from '@/client-config/hooks/useCaptcha';
import { captchaState } from '@/client-config/states/captchaState';
import { useSetRecoilStateV2 } from '@/ui/utilities/state/jotai/hooks/useSetRecoilStateV2';
import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';
import { useRecoilValue } from 'recoil';
import { assertIsDefinedOrThrow } from 'twenty-shared/utils';
import { CaptchaDriverType } from '~/generated-metadata/graphql';
export const CaptchaProviderScriptLoaderEffect = () => {
const captcha = useRecoilValue(captchaState);
const setIsCaptchaScriptLoaded = useSetRecoilStateV2(
const setIsCaptchaScriptLoaded = useSetRecoilState(
isCaptchaScriptLoadedState,
);
const { isCaptchaScriptLoaded, isCaptchaConfigured } = useCaptcha();
@@ -1,26 +1,13 @@
import { act, renderHook } from '@testing-library/react';
import { type ReactNode } from 'react';
import { Provider as JotaiProvider } from 'jotai';
import { RecoilRoot } from 'recoil';
import { captchaTokenState } from '@/captcha/states/captchaTokenState';
import { useReadCaptchaToken } from '@/captcha/hooks/useReadCaptchaToken';
import { jotaiStore } from '@/ui/utilities/state/jotai/jotaiStore';
const Wrapper = ({ children }: { children: ReactNode }) => (
<JotaiProvider store={jotaiStore}>
<RecoilRoot>{children}</RecoilRoot>
</JotaiProvider>
);
describe('useReadCaptchaToken', () => {
beforeEach(() => {
jotaiStore.set(captchaTokenState.atom, undefined);
});
it('should return undefined when no token exists', async () => {
const { result } = renderHook(() => useReadCaptchaToken(), {
wrapper: Wrapper,
wrapper: RecoilRoot,
});
await act(async () => {
@@ -30,11 +17,23 @@ describe('useReadCaptchaToken', () => {
});
it('should return the token when it exists', async () => {
jotaiStore.set(captchaTokenState.atom, 'test-token');
const { result } = renderHook(() => useReadCaptchaToken(), {
wrapper: Wrapper,
});
const { result } = renderHook(
() => {
const hook = useReadCaptchaToken();
return hook;
},
{
wrapper: ({ children }) => (
<RecoilRoot
initializeState={({ set }) => {
set(captchaTokenState, 'test-token');
}}
>
{children}
</RecoilRoot>
),
},
);
await act(async () => {
const token = await result.current.readCaptchaToken();
@@ -1,27 +1,14 @@
import { act, renderHook, waitFor } from '@testing-library/react';
import { type ReactNode } from 'react';
import { type MutableSnapshot, RecoilRoot } from 'recoil';
import { Provider as JotaiProvider } from 'jotai';
import { RecoilRoot } from 'recoil';
import { useRequestFreshCaptchaToken } from '@/captcha/hooks/useRequestFreshCaptchaToken';
import { isRequestingCaptchaTokenState } from '@/captcha/states/isRequestingCaptchaTokenState';
import { isCaptchaRequiredForPath } from '@/captcha/utils/isCaptchaRequiredForPath';
import { captchaState } from '@/client-config/states/captchaState';
import { jotaiStore } from '@/ui/utilities/state/jotai/jotaiStore';
import { type Captcha, CaptchaDriverType } from '~/generated-metadata/graphql';
import { CaptchaDriverType } from '~/generated-metadata/graphql';
jest.mock('@/captcha/utils/isCaptchaRequiredForPath');
const createWrapper =
(initializeRecoilState?: (mutableSnapshot: MutableSnapshot) => void) =>
({ children }: { children: ReactNode }) => (
<JotaiProvider store={jotaiStore}>
<RecoilRoot initializeState={initializeRecoilState}>
{children}
</RecoilRoot>
</JotaiProvider>
);
describe('useRequestFreshCaptchaToken', () => {
const mockGrecaptchaExecute = jest.fn();
const mockTurnstileRender = jest.fn();
@@ -60,7 +47,7 @@ describe('useRequestFreshCaptchaToken', () => {
(isCaptchaRequiredForPath as jest.Mock).mockReturnValue(false);
const { result } = renderHook(() => useRequestFreshCaptchaToken(), {
wrapper: createWrapper(),
wrapper: RecoilRoot,
});
await act(async () => {
@@ -74,7 +61,7 @@ describe('useRequestFreshCaptchaToken', () => {
it('should not request a token if captcha provider is undefined', async () => {
const { result } = renderHook(() => useRequestFreshCaptchaToken(), {
wrapper: createWrapper(),
wrapper: RecoilRoot,
});
await act(async () => {
@@ -87,15 +74,20 @@ describe('useRequestFreshCaptchaToken', () => {
});
it('should request a token from Google reCAPTCHA when provider is GOOGLE_RECAPTCHA', async () => {
jotaiStore.set(isRequestingCaptchaTokenState.atom, false);
const { result } = renderHook(() => useRequestFreshCaptchaToken(), {
wrapper: createWrapper(({ set }) => {
set(captchaState, {
provider: CaptchaDriverType.GOOGLE_RECAPTCHA,
siteKey: 'google-site-key',
} as Captcha);
}),
wrapper: ({ children }) => (
<RecoilRoot
initializeState={({ set }) => {
set(captchaState, {
provider: CaptchaDriverType.GOOGLE_RECAPTCHA,
siteKey: 'google-site-key',
});
set(isRequestingCaptchaTokenState, false);
}}
>
{children}
</RecoilRoot>
),
});
await act(async () => {
@@ -112,15 +104,20 @@ describe('useRequestFreshCaptchaToken', () => {
});
it('should request a token from Turnstile when provider is TURNSTILE', async () => {
jotaiStore.set(isRequestingCaptchaTokenState.atom, false);
const { result } = renderHook(() => useRequestFreshCaptchaToken(), {
wrapper: createWrapper(({ set }) => {
set(captchaState, {
provider: CaptchaDriverType.TURNSTILE,
siteKey: 'turnstile-site-key',
} as Captcha);
}),
wrapper: ({ children }) => (
<RecoilRoot
initializeState={({ set }) => {
set(captchaState, {
provider: CaptchaDriverType.TURNSTILE,
siteKey: 'turnstile-site-key',
});
set(isRequestingCaptchaTokenState, false);
}}
>
{children}
</RecoilRoot>
),
});
await act(async () => {
@@ -1,16 +1,22 @@
import { useRecoilCallback } from 'recoil';
import { captchaTokenState } from '@/captcha/states/captchaTokenState';
import { jotaiStore } from '@/ui/utilities/state/jotai/jotaiStore';
import { useCallback } from 'react';
import { isDefined } from 'twenty-shared/utils';
export const useReadCaptchaToken = () => {
const readCaptchaToken = useCallback(() => {
const existingCaptchaToken = jotaiStore.get(captchaTokenState.atom);
const readCaptchaToken = useRecoilCallback(
({ snapshot }) =>
() => {
const existingCaptchaToken = snapshot
.getLoadable(captchaTokenState)
.getValue();
if (isDefined(existingCaptchaToken)) {
return existingCaptchaToken;
}
}, []);
if (isDefined(existingCaptchaToken)) {
return existingCaptchaToken;
}
},
[],
);
return { readCaptchaToken };
};
@@ -1,15 +1,15 @@
import { useRecoilCallback, useSetRecoilState } from 'recoil';
import { captchaTokenState } from '@/captcha/states/captchaTokenState';
import { isRequestingCaptchaTokenState } from '@/captcha/states/isRequestingCaptchaTokenState';
import { isCaptchaRequiredForPath } from '@/captcha/utils/isCaptchaRequiredForPath';
import { captchaState } from '@/client-config/states/captchaState';
import { useSetRecoilStateV2 } from '@/ui/utilities/state/jotai/hooks/useSetRecoilStateV2';
import { useRecoilCallback } from 'recoil';
import { assertIsDefinedOrThrow, isDefined } from 'twenty-shared/utils';
import { CaptchaDriverType } from '~/generated-metadata/graphql';
export const useRequestFreshCaptchaToken = () => {
const setCaptchaToken = useSetRecoilStateV2(captchaTokenState);
const setIsRequestingCaptchaToken = useSetRecoilStateV2(
const setCaptchaToken = useSetRecoilState(captchaTokenState);
const setIsRequestingCaptchaToken = useSetRecoilState(
isRequestingCaptchaTokenState,
);
@@ -1,5 +1,5 @@
import { createStateV2 } from '@/ui/utilities/state/jotai/utils/createStateV2';
export const captchaTokenState = createStateV2<string | undefined>({
import { createState } from '@/ui/utilities/state/utils/createState';
export const captchaTokenState = createState<string | undefined>({
key: 'captchaTokenState',
defaultValue: undefined,
});
@@ -1,5 +1,5 @@
import { createStateV2 } from '@/ui/utilities/state/jotai/utils/createStateV2';
export const isCaptchaScriptLoadedState = createStateV2<boolean>({
import { createState } from '@/ui/utilities/state/utils/createState';
export const isCaptchaScriptLoadedState = createState<boolean>({
key: 'isCaptchaScriptLoadedState',
defaultValue: false,
});
@@ -1,5 +1,5 @@
import { createStateV2 } from '@/ui/utilities/state/jotai/utils/createStateV2';
export const isRequestingCaptchaTokenState = createStateV2<boolean>({
import { createState } from '@/ui/utilities/state/utils/createState';
export const isRequestingCaptchaTokenState = createState<boolean>({
key: 'isRequestingCaptchaTokenState',
defaultValue: false,
});
@@ -1,18 +1,19 @@
import { useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { useSetRecoilState } from 'recoil';
import { tokenPairState } from '@/auth/states/tokenPairState';
import { isLoadingTokensFromExtensionState } from '@/chrome-extension-sidecar/states/isLoadingTokensFromExtensionState';
import { chromeExtensionIdState } from '@/client-config/states/chromeExtensionIdState';
import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2';
import { useSetRecoilStateV2 } from '@/ui/utilities/state/jotai/hooks/useSetRecoilStateV2';
import { useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { isDefined } from 'twenty-shared/utils';
import { isInFrame } from '~/utils/isInIframe';
import { isDefined } from 'twenty-shared/utils';
import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2';
export const ChromeExtensionSidecarEffect = () => {
const navigate = useNavigate();
const setTokenPair = useSetRecoilStateV2(tokenPairState);
const setTokenPair = useSetRecoilState(tokenPairState);
const chromeExtensionId = useRecoilValueV2(chromeExtensionIdState);
const setIsLoadingTokensFromExtension = useSetRecoilStateV2(
const setIsLoadingTokensFromExtension = useSetRecoilState(
isLoadingTokensFromExtensionState,
);
@@ -1,5 +1,5 @@
import { createStateV2 } from '@/ui/utilities/state/jotai/utils/createStateV2';
export const isLoadingTokensFromExtensionState = createStateV2<boolean | null>({
import { createState } from '@/ui/utilities/state/utils/createState';
export const isLoadingTokensFromExtensionState = createState<boolean | null>({
key: 'isLoadingTokensFromExtensionState',
defaultValue: null,
});
@@ -2,16 +2,15 @@ import { captchaTokenState } from '@/captcha/states/captchaTokenState';
import { isCaptchaScriptLoadedState } from '@/captcha/states/isCaptchaScriptLoadedState';
import { captchaState } from '@/client-config/states/captchaState';
import { clientConfigApiStatusState } from '@/client-config/states/clientConfigApiStatusState';
import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2';
import { useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
export const useCaptcha = () => {
const captcha = useRecoilValue(captchaState);
const captchaToken = useRecoilValueV2(captchaTokenState);
const captchaToken = useRecoilValue(captchaTokenState);
const clientConfigApiStatus = useRecoilValue(clientConfigApiStatusState);
const isCaptchaScriptLoaded = useRecoilValueV2(isCaptchaScriptLoadedState);
const isCaptchaScriptLoaded = useRecoilValue(isCaptchaScriptLoadedState);
const isClientConfigLoaded = clientConfigApiStatus.isLoadedOnce;
const isSiteKeyDefined = isDefined(captcha?.siteKey);
@@ -45,7 +45,7 @@ type UseClientConfigResult = {
export const useClientConfig = (): UseClientConfigResult => {
const setIsAnalyticsEnabled = useSetRecoilStateV2(isAnalyticsEnabledState);
const setDomainConfiguration = useSetRecoilStateV2(domainConfigurationState);
const setDomainConfiguration = useSetRecoilState(domainConfigurationState);
const setAuthProviders = useSetRecoilStateV2(authProvidersState);
const setAiModels = useSetRecoilStateV2(aiModelsState);
@@ -8,7 +8,6 @@ import { expect, userEvent, within } from 'storybook/test';
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { useSetRecoilStateV2 } from '@/ui/utilities/state/jotai/hooks/useSetRecoilStateV2';
import { ComponentWithRouterDecorator } from '~/testing/decorators/ComponentWithRouterDecorator';
import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator';
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
@@ -73,11 +72,11 @@ const meta: Meta<typeof CommandMenu> = {
component: CommandMenuRouter,
decorators: [
(Story) => {
const setCurrentWorkspace = useSetRecoilStateV2(currentWorkspaceState);
const setCurrentUserWorkspace = useSetRecoilStateV2(
const setCurrentWorkspace = useSetRecoilState(currentWorkspaceState);
const setCurrentUserWorkspace = useSetRecoilState(
currentUserWorkspaceState,
);
const setCurrentWorkspaceMember = useSetRecoilStateV2(
const setCurrentWorkspaceMember = useSetRecoilState(
currentWorkspaceMemberState,
);
const setIsCommandMenuOpened = useSetRecoilState(
@@ -139,7 +138,7 @@ export const LimitedPermissions: Story = {
},
decorators: [
(Story) => {
const setCurrentUserWorkspace = useSetRecoilStateV2(
const setCurrentUserWorkspace = useSetRecoilState(
currentUserWorkspaceState,
);
setCurrentUserWorkspace(
@@ -1,25 +1,27 @@
import { useCallback } from 'react';
import { billingCheckoutSessionState } from '@/auth/states/billingCheckoutSessionState';
import { BILLING_CHECKOUT_SESSION_DEFAULT_VALUE } from '@/billing/constants/BillingCheckoutSessionDefaultValue';
import { jotaiStore } from '@/ui/utilities/state/jotai/jotaiStore';
import { useRecoilCallback } from 'recoil';
export const useBuildSearchParamsFromUrlSyncedStates = () => {
const buildSearchParamsFromUrlSyncedStates = useCallback(async () => {
const billingCheckoutSession = jotaiStore.get(
billingCheckoutSessionState.atom,
);
const buildSearchParamsFromUrlSyncedStates = useRecoilCallback(
({ snapshot }) =>
async () => {
const billingCheckoutSession = snapshot
.getLoadable(billingCheckoutSessionState)
.getValue();
const output = {
...(billingCheckoutSession !== BILLING_CHECKOUT_SESSION_DEFAULT_VALUE
? {
billingCheckoutSession: JSON.stringify(billingCheckoutSession),
}
: {}),
};
const output = {
...(billingCheckoutSession !== BILLING_CHECKOUT_SESSION_DEFAULT_VALUE
? {
billingCheckoutSession: JSON.stringify(billingCheckoutSession),
}
: {}),
};
return output;
}, []);
return output;
},
[],
);
return {
buildSearchParamsFromUrlSyncedStates,
@@ -4,11 +4,9 @@ import { isMultiWorkspaceEnabledState } from '@/client-config/states/isMultiWork
import { useIsCurrentLocationOnDefaultDomain } from '@/domain-manager/hooks/useIsCurrentLocationOnDefaultDomain';
import { useOrigin } from '@/domain-manager/hooks/useOrigin';
import { useRedirectToDefaultDomain } from '@/domain-manager/hooks/useRedirectToDefaultDomain';
import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2';
import { useSetRecoilStateV2 } from '@/ui/utilities/state/jotai/hooks/useSetRecoilStateV2';
import { workspaceAuthBypassProvidersState } from '@/workspace/states/workspaceAuthBypassProvidersState';
import { workspaceAuthProvidersState } from '@/workspace/states/workspaceAuthProvidersState';
import { useRecoilValue } from 'recoil';
import { workspaceAuthBypassProvidersState } from '@/workspace/states/workspaceAuthBypassProvidersState';
import { useRecoilValue, useSetRecoilState } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
import { useGetPublicWorkspaceDataByDomainQuery } from '~/generated-metadata/graphql';
@@ -16,15 +14,15 @@ export const useGetPublicWorkspaceDataByDomain = () => {
const { isDefaultDomain } = useIsCurrentLocationOnDefaultDomain();
const isMultiWorkspaceEnabled = useRecoilValue(isMultiWorkspaceEnabledState);
const { origin } = useOrigin();
const setWorkspaceAuthProviders = useSetRecoilStateV2(
const setWorkspaceAuthProviders = useSetRecoilState(
workspaceAuthProvidersState,
);
const setWorkspaceAuthBypassProviders = useSetRecoilStateV2(
const setWorkspaceAuthBypassProviders = useSetRecoilState(
workspaceAuthBypassProvidersState,
);
const workspacePublicData = useRecoilValueV2(workspacePublicDataState);
const workspacePublicData = useRecoilValue(workspacePublicDataState);
const { redirectToDefaultDomain } = useRedirectToDefaultDomain();
const setWorkspacePublicDataState = useSetRecoilStateV2(
const setWorkspacePublicDataState = useSetRecoilState(
workspacePublicDataState,
);
const clientConfigApiStatus = useRecoilValue(clientConfigApiStatusState);
@@ -1,7 +1,6 @@
import { isMultiWorkspaceEnabledState } from '@/client-config/states/isMultiWorkspaceEnabledState';
import { useReadDefaultDomainFromConfiguration } from '@/domain-manager/hooks/useReadDefaultDomainFromConfiguration';
import { domainConfigurationState } from '@/domain-manager/states/domainConfigurationState';
import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2';
import { useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
@@ -9,7 +8,7 @@ export const useIsCurrentLocationOnAWorkspace = () => {
const { defaultDomain } = useReadDefaultDomainFromConfiguration();
const isMultiWorkspaceEnabled = useRecoilValue(isMultiWorkspaceEnabledState);
const domainConfiguration = useRecoilValueV2(domainConfigurationState);
const domainConfiguration = useRecoilValue(domainConfigurationState);
if (
isMultiWorkspaceEnabled &&
@@ -1,11 +1,10 @@
import { domainConfigurationState } from '@/domain-manager/states/domainConfigurationState';
import { lastAuthenticatedWorkspaceDomainState } from '@/domain-manager/states/lastAuthenticatedWorkspaceDomainState';
import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2';
import { useSetRecoilStateV2 } from '@/ui/utilities/state/jotai/hooks/useSetRecoilStateV2';
import { useRecoilValue, useSetRecoilState } from 'recoil';
export const useLastAuthenticatedWorkspaceDomain = () => {
const domainConfiguration = useRecoilValueV2(domainConfigurationState);
const setLastAuthenticatedWorkspaceDomain = useSetRecoilStateV2(
const domainConfiguration = useRecoilValue(domainConfigurationState);
const setLastAuthenticatedWorkspaceDomain = useSetRecoilState(
lastAuthenticatedWorkspaceDomainState,
);
const setLastAuthenticateWorkspaceDomainWithCookieAttributes = (
@@ -1,10 +1,9 @@
import { isMultiWorkspaceEnabledState } from '@/client-config/states/isMultiWorkspaceEnabledState';
import { domainConfigurationState } from '@/domain-manager/states/domainConfigurationState';
import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2';
import { useRecoilValue } from 'recoil';
export const useReadDefaultDomainFromConfiguration = () => {
const domainConfiguration = useRecoilValueV2(domainConfigurationState);
const domainConfiguration = useRecoilValue(domainConfigurationState);
const isMultiWorkspaceEnabled = useRecoilValue(isMultiWorkspaceEnabledState);
const defaultDomain = isMultiWorkspaceEnabled
@@ -1,7 +1,7 @@
import { type ClientConfig } from '@/client-config/types/ClientConfig';
import { createStateV2 } from '@/ui/utilities/state/jotai/utils/createStateV2';
import { createState } from '@/ui/utilities/state/utils/createState';
export const domainConfigurationState = createStateV2<
export const domainConfigurationState = createState<
Pick<ClientConfig, 'frontDomain' | 'defaultSubdomain'>
>({
key: 'domainConfiguration',
@@ -1,6 +1,7 @@
import { createStateV2 } from '@/ui/utilities/state/jotai/utils/createStateV2';
import { cookieStorageEffect } from '~/utils/recoil/cookieStorageEffect';
import { createState } from '@/ui/utilities/state/utils/createState';
export const lastAuthenticatedWorkspaceDomainState = createStateV2<
export const lastAuthenticatedWorkspaceDomainState = createState<
| {
workspaceUrl: string;
workspaceId: string;
@@ -12,10 +13,9 @@ export const lastAuthenticatedWorkspaceDomainState = createStateV2<
>({
key: 'lastAuthenticateWorkspaceDomain',
defaultValue: null,
useCookieStorage: {
cookieKey: 'lastAuthenticateWorkspaceDomain',
attributes: {
expires: new Date(Date.now() + 1000 * 60 * 60 * 24 * 365),
},
},
effects: [
cookieStorageEffect('lastAuthenticateWorkspaceDomain', {
expires: new Date(Date.now() + 1000 * 60 * 60 * 24 * 365), // 1 year
}),
],
});
@@ -1,10 +1,10 @@
import { useEffect, useState } from 'react';
import { useRecoilValue } from 'recoil';
import { currentUserState } from '@/auth/states/currentUserState';
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { sentryConfigState } from '@/client-config/states/sentryConfigState';
import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2';
import { useEffect, useState } from 'react';
import { useRecoilValue } from 'recoil';
import { isNonEmptyString } from '@sniptt/guards';
import { isDefined } from 'twenty-shared/utils';
import { REACT_APP_SERVER_BASE_URL } from '~/config';
@@ -12,9 +12,9 @@ import { REACT_APP_SERVER_BASE_URL } from '~/config';
export const SentryInitEffect = () => {
const sentryConfig = useRecoilValue(sentryConfigState);
const currentUser = useRecoilValueV2(currentUserState);
const currentWorkspace = useRecoilValueV2(currentWorkspaceState);
const currentWorkspaceMember = useRecoilValueV2(currentWorkspaceMemberState);
const currentUser = useRecoilValue(currentUserState);
const currentWorkspace = useRecoilValue(currentWorkspaceState);
const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
const [isSentryInitialized, setIsSentryInitialized] = useState(false);
const [isSentryInitializing, setIsSentryInitializing] = useState(false);
@@ -13,14 +13,14 @@ import { NavigationDrawerSectionTitle } from '@/ui/navigation/navigation-drawer/
import { useNavigationSection } from '@/ui/navigation/navigation-drawer/hooks/useNavigationSection';
import { isNavigationSectionOpenFamilyState } from '@/ui/navigation/navigation-drawer/states/isNavigationSectionOpenFamilyState';
import { useFamilyRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useFamilyRecoilValueV2';
import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2';
import { useLingui } from '@lingui/react/macro';
import { useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
import { IconFolderPlus } from 'twenty-ui/display';
import { LightIconButton } from 'twenty-ui/input';
export const CurrentWorkspaceMemberFavoritesFolders = () => {
const currentWorkspaceMember = useRecoilValueV2(currentWorkspaceMemberState);
const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
const { sortedFavorites: favorites } = useFavorites();
const { favoritesByFolder } = useFavoritesByFolder();
@@ -5,7 +5,6 @@ import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMembe
import { useCreateFavorite } from '@/favorites/hooks/useCreateFavorite';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { jotaiStore } from '@/ui/utilities/state/jotai/jotaiStore';
import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper';
import { generatedMockObjectMetadataItems } from '~/testing/utils/generatedMockObjectMetadataItems';
import {
@@ -47,10 +46,13 @@ const Wrapper = getJestMetadataAndApolloMocksWrapper({
describe('useCreateFavorite', () => {
it('should create favorite successfully', async () => {
jotaiStore.set(currentWorkspaceMemberState.atom, mockWorkspaceMember);
const { result } = renderHook(
() => {
const setCurrentWorkspaceMember = useSetRecoilState(
currentWorkspaceMemberState,
);
setCurrentWorkspaceMember(mockWorkspaceMember);
const setMetadataItems = useSetRecoilState(objectMetadataItemsState);
setMetadataItems(generatedMockObjectMetadataItems);
@@ -4,7 +4,6 @@ import { useSetRecoilState } from 'recoil';
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { useDeleteFavorite } from '@/favorites/hooks/useDeleteFavorite';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { jotaiStore } from '@/ui/utilities/state/jotai/jotaiStore';
import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper';
import { generatedMockObjectMetadataItems } from '~/testing/utils/generatedMockObjectMetadataItems';
import {
@@ -24,10 +23,13 @@ const Wrapper = getJestMetadataAndApolloMocksWrapper({
describe('useDeleteFavorite', () => {
it('should delete favorite successfully', async () => {
jotaiStore.set(currentWorkspaceMemberState.atom, mockWorkspaceMember);
const { result } = renderHook(
() => {
const setCurrentWorkspaceMember = useSetRecoilState(
currentWorkspaceMemberState,
);
setCurrentWorkspaceMember(mockWorkspaceMember);
const setMetadataItems = useSetRecoilState(objectMetadataItemsState);
setMetadataItems(generatedMockObjectMetadataItems);
@@ -6,7 +6,6 @@ import { useFavorites } from '@/favorites/hooks/useFavorites';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { prefetchFavoritesState } from '@/prefetch/states/prefetchFavoritesState';
import { jotaiStore } from '@/ui/utilities/state/jotai/jotaiStore';
import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper';
import { generatedMockObjectMetadataItems } from '~/testing/utils/generatedMockObjectMetadataItems';
import {
@@ -21,13 +20,16 @@ const Wrapper = getJestMetadataAndApolloMocksWrapper({
describe('useFavorites', () => {
it('should fetch and sort favorites successfully', () => {
jotaiStore.set(currentWorkspaceMemberState.atom, mockWorkspaceMember);
const { result } = renderHook(
() => {
const setPrefetchFavorites = useSetRecoilState(prefetchFavoritesState);
setPrefetchFavorites(initialFavorites);
const setCurrentWorkspaceMember = useSetRecoilState(
currentWorkspaceMemberState,
);
setCurrentWorkspaceMember(mockWorkspaceMember);
const setMetadataItems = useSetRecoilState(objectMetadataItemsState);
setMetadataItems(generatedMockObjectMetadataItems);
@@ -11,7 +11,6 @@ import { createFolderDroppableId } from '@/favorites/utils/createFolderDroppable
import { createFolderHeaderDroppableId } from '@/favorites/utils/createFolderHeaderDroppableId';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { prefetchFavoritesState } from '@/prefetch/states/prefetchFavoritesState';
import { jotaiStore } from '@/ui/utilities/state/jotai/jotaiStore';
import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper';
import { generatedMockObjectMetadataItems } from '~/testing/utils/generatedMockObjectMetadataItems';
import {
@@ -34,14 +33,17 @@ describe('useHandleFavoriteDragAndDrop', () => {
};
const setupHook = () => {
jotaiStore.set(currentWorkspaceMemberState.atom, mockWorkspaceMember);
return renderHook(
() => {
const setPrefetchFavorites = useSetRecoilState(prefetchFavoritesState);
setPrefetchFavorites(initialFavorites as Favorite[]);
const setCurrentWorkspaceMember = useSetRecoilState(
currentWorkspaceMemberState,
);
const setMetadataItems = useSetRecoilState(objectMetadataItemsState);
setCurrentWorkspaceMember(mockWorkspaceMember);
setMetadataItems(generatedMockObjectMetadataItems);
return {
@@ -1,7 +1,6 @@
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { type Favorite } from '@/favorites/types/Favorite';
import { prefetchFavoritesState } from '@/prefetch/states/prefetchFavoritesState';
import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2';
import { useRecoilValue } from 'recoil';
type PrefetchedFavoritesData = {
@@ -11,7 +10,7 @@ type PrefetchedFavoritesData = {
};
export const usePrefetchedFavoritesData = (): PrefetchedFavoritesData => {
const currentWorkspaceMember = useRecoilValueV2(currentWorkspaceMemberState);
const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
const currentWorkspaceMemberId = currentWorkspaceMember?.id;
const prefetchFavorites = useRecoilValue(prefetchFavoritesState);
@@ -1,7 +1,6 @@
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { type FavoriteFolder } from '@/favorites/types/FavoriteFolder';
import { prefetchFavoriteFoldersState } from '@/prefetch/states/prefetchFavoriteFoldersState';
import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2';
import { useRecoilValue } from 'recoil';
type PrefetchedFavoritesFoldersData = {
@@ -11,9 +10,7 @@ type PrefetchedFavoritesFoldersData = {
export const usePrefetchedFavoritesFoldersData =
(): PrefetchedFavoritesFoldersData => {
const currentWorkspaceMember = useRecoilValueV2(
currentWorkspaceMemberState,
);
const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
const currentWorkspaceMemberId = currentWorkspaceMember?.id;
const prefetchFavoriteFolders = useRecoilValue(
@@ -1,3 +1,4 @@
import { useRecoilValue } from 'recoil';
import {
type FrontComponentExecutionContext,
type FrontComponentHostCommunicationApi,
@@ -5,14 +6,13 @@ import {
import { type AppPath } from 'twenty-shared/types';
import { currentUserState } from '@/auth/states/currentUserState';
import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2';
import { useNavigateApp } from '~/hooks/useNavigateApp';
export const useFrontComponentExecutionContext = (): {
executionContext: FrontComponentExecutionContext;
frontComponentHostCommunicationApi: FrontComponentHostCommunicationApi;
} => {
const currentUser = useRecoilValueV2(currentUserState);
const currentUser = useRecoilValue(currentUserState);
const navigateApp = useNavigateApp();
const navigate: FrontComponentHostCommunicationApi['navigate'] = async (
@@ -3,13 +3,13 @@ import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMembe
import { isImpersonatingState } from '@/auth/states/isImpersonatingState';
import { InformationBanner } from '@/information-banner/components/InformationBanner';
import { t } from '@lingui/core/macro';
import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2';
import { useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
import { IconLogout } from 'twenty-ui/display';
export const InformationBannerIsImpersonating = () => {
const currentWorkspaceMember = useRecoilValueV2(currentWorkspaceMemberState);
const isImpersonating = useRecoilValueV2(isImpersonatingState);
const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
const isImpersonating = useRecoilValue(isImpersonatingState);
const { signOut } = useAuth();
@@ -3,10 +3,10 @@ import { currentUserState } from '@/auth/states/currentUserState';
import { type InformationBannerKeys } from '@/information-banner/types/InformationBannerKeys';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2';
import { useRecoilValue } from 'recoil';
export const useAccountToReconnect = (key: InformationBannerKeys) => {
const currentUser = useRecoilValueV2(currentUserState);
const currentUser = useRecoilValue(currentUserState);
const userVars = currentUser?.userVars;
@@ -1,7 +1,6 @@
import { act, renderHook } from '@testing-library/react';
import { type ReactNode } from 'react';
import { type MutableSnapshot, RecoilRoot } from 'recoil';
import { Provider as JotaiProvider } from 'jotai';
import { RecoilRoot, type MutableSnapshot } from 'recoil';
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { DateFormat } from '@/localization/constants/DateFormat';
@@ -9,7 +8,6 @@ import { NumberFormat } from '@/localization/constants/NumberFormat';
import { TimeFormat } from '@/localization/constants/TimeFormat';
import { useFormatPreferences } from '@/localization/hooks/useFormatPreferences';
import { workspaceMemberFormatPreferencesState } from '@/localization/states/workspaceMemberFormatPreferencesState';
import { jotaiStore } from '@/ui/utilities/state/jotai/jotaiStore';
import { detectCalendarStartDay } from '@/localization/utils/detection/detectCalendarStartDay';
import { detectDateFormat } from '@/localization/utils/detection/detectDateFormat';
import { detectNumberFormat } from '@/localization/utils/detection/detectNumberFormat';
@@ -75,30 +73,19 @@ const mockInitialPreferences = {
calendarStartDay: CalendarStartDay.MONDAY,
};
const createWrapper =
(initializeRecoilState?: (mutableSnapshot: MutableSnapshot) => void) =>
({ children }: { children: ReactNode }) => {
const defaultInitState = ({ set }: MutableSnapshot) => {
set(workspaceMemberFormatPreferencesState, mockInitialPreferences);
initializeRecoilState?.({ set } as MutableSnapshot);
};
return (
<JotaiProvider store={jotaiStore}>
<RecoilRoot initializeState={defaultInitState}>{children}</RecoilRoot>
</JotaiProvider>
);
const Wrapper = ({ children }: { children: ReactNode }) => {
const initializeState = ({ set }: MutableSnapshot) => {
set(currentWorkspaceMemberState, mockCurrentWorkspaceMember);
set(workspaceMemberFormatPreferencesState, mockInitialPreferences);
};
return <RecoilRoot initializeState={initializeState}>{children}</RecoilRoot>;
};
describe('useFormatPreferences', () => {
beforeEach(() => {
jest.clearAllMocks();
jotaiStore.set(
currentWorkspaceMemberState.atom,
mockCurrentWorkspaceMember,
);
mockUseUpdateOneRecord.mockReturnValue({
updateOneRecord: mockUpdateOneRecord,
});
@@ -119,7 +106,7 @@ describe('useFormatPreferences', () => {
it('should return format preferences and update functions', () => {
const { result } = renderHook(() => useFormatPreferences(), {
wrapper: createWrapper(),
wrapper: Wrapper,
});
expect(result.current).toHaveProperty('formatPreferences');
@@ -136,7 +123,7 @@ describe('useFormatPreferences', () => {
it('should return current format preferences', () => {
const { result } = renderHook(() => useFormatPreferences(), {
wrapper: createWrapper(),
wrapper: Wrapper,
});
expect(result.current.formatPreferences).toEqual(mockInitialPreferences);
@@ -144,7 +131,7 @@ describe('useFormatPreferences', () => {
it('should update single format preference successfully', async () => {
const { result } = renderHook(() => useFormatPreferences(), {
wrapper: createWrapper(),
wrapper: Wrapper,
});
const newTimeZone = 'Europe/London';
@@ -165,7 +152,7 @@ describe('useFormatPreferences', () => {
it('should handle SYSTEM values by detecting actual format', async () => {
const { result } = renderHook(() => useFormatPreferences(), {
wrapper: createWrapper(),
wrapper: Wrapper,
});
mockDetectTimeZone.mockReturnValue('America/Chicago');
@@ -187,7 +174,7 @@ describe('useFormatPreferences', () => {
it('should update multiple format preferences successfully', async () => {
const { result } = renderHook(() => useFormatPreferences(), {
wrapper: createWrapper(),
wrapper: Wrapper,
});
const updates = {
@@ -211,10 +198,17 @@ describe('useFormatPreferences', () => {
});
it('should not update preferences when user is not logged in', async () => {
jotaiStore.set(currentWorkspaceMemberState.atom, null);
const { result } = renderHook(() => useFormatPreferences(), {
wrapper: createWrapper(),
wrapper: ({ children }: { children: ReactNode }) => {
const initializeState = ({ set }: MutableSnapshot) => {
set(currentWorkspaceMemberState, null);
set(workspaceMemberFormatPreferencesState, mockInitialPreferences);
};
return (
<RecoilRoot initializeState={initializeState}>{children}</RecoilRoot>
);
},
});
await act(async () => {
@@ -226,7 +220,7 @@ describe('useFormatPreferences', () => {
it('should handle update errors gracefully', async () => {
const { result } = renderHook(() => useFormatPreferences(), {
wrapper: createWrapper(),
wrapper: Wrapper,
});
const error = new Error('Update failed');
@@ -244,7 +238,7 @@ describe('useFormatPreferences', () => {
it('should initialize format preferences from workspace member', () => {
const { result } = renderHook(() => useFormatPreferences(), {
wrapper: createWrapper(),
wrapper: Wrapper,
});
act(() => {
@@ -256,7 +250,7 @@ describe('useFormatPreferences', () => {
it('should not initialize format preferences when workspace member is null', () => {
const { result } = renderHook(() => useFormatPreferences(), {
wrapper: createWrapper(),
wrapper: Wrapper,
});
act(() => {
@@ -1,5 +1,5 @@
import { useCallback } from 'react';
import { useRecoilState } from 'recoil';
import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil';
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { DateFormat } from '@/localization/constants/DateFormat';
@@ -18,8 +18,6 @@ import { getFormatPreferencesFromWorkspaceMember } from '@/localization/utils/fo
import { getWorkspaceMemberUpdateFromFormatPreferences } from '@/localization/utils/format-preferences/getWorkspaceMemberUpdateFromFormatPreferences';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2';
import { useSetRecoilStateV2 } from '@/ui/utilities/state/jotai/hooks/useSetRecoilStateV2';
import { CalendarStartDay } from 'twenty-shared/constants';
import { logError } from '~/utils/logError';
@@ -30,8 +28,8 @@ export const useFormatPreferences = () => {
workspaceMemberFormatPreferences,
setWorkspaceMemberFormatPreferences,
] = useRecoilState(workspaceMemberFormatPreferencesState);
const currentWorkspaceMember = useRecoilValueV2(currentWorkspaceMemberState);
const setCurrentWorkspaceMember = useSetRecoilStateV2(
const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
const setCurrentWorkspaceMember = useSetRecoilState(
currentWorkspaceMemberState,
);
@@ -1,8 +1,10 @@
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { useLingui } from '@lingui/react/macro';
import { useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
import { IconFolderPlus } from 'twenty-ui/display';
import { LightIconButton } from 'twenty-ui/input';
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { CurrentWorkspaceMemberOrphanNavigationMenuItems } from '@/navigation-menu-item/components/CurrentWorkspaceMemberOrphanNavigationMenuItems';
import { NavigationMenuItemFolders } from '@/navigation-menu-item/components/NavigationMenuItemFolders';
import { NavigationMenuItemSkeletonLoader } from '@/navigation-menu-item/components/NavigationMenuItemSkeletonLoader';
@@ -17,10 +19,9 @@ import { NavigationDrawerSectionTitle } from '@/ui/navigation/navigation-drawer/
import { useNavigationSection } from '@/ui/navigation/navigation-drawer/hooks/useNavigationSection';
import { isNavigationSectionOpenFamilyState } from '@/ui/navigation/navigation-drawer/states/isNavigationSectionOpenFamilyState';
import { useFamilyRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useFamilyRecoilValueV2';
import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2';
export const CurrentWorkspaceMemberNavigationMenuItemFolders = () => {
const currentWorkspaceMember = useRecoilValueV2(currentWorkspaceMemberState);
const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
const { navigationMenuItemsSorted } = useSortedNavigationMenuItems();
const { userNavigationMenuItemsByFolder } = useNavigationMenuItemsByFolder();

Some files were not shown because too many files have changed in this diff Show More