diff --git a/packages/twenty-front/.env.example b/packages/twenty-front/.env.example index 381ac3fa1..755698a12 100644 --- a/packages/twenty-front/.env.example +++ b/packages/twenty-front/.env.example @@ -11,4 +11,5 @@ VITE_DISABLE_ESLINT_CHECKER=true # VITE_ENABLE_SSL=false # VITE_HOST=localhost.com # SSL_KEY_PATH="./certs/your-cert.key" -# SSL_CERT_PATH="./certs/your-cert.crt" \ No newline at end of file +# SSL_CERT_PATH="./certs/your-cert.crt" +# IS_DEBUG_MODE=false \ No newline at end of file diff --git a/packages/twenty-front/src/modules/apollo/hooks/useApolloFactory.ts b/packages/twenty-front/src/modules/apollo/hooks/useApolloFactory.ts index 0779de0a1..77cab6049 100644 --- a/packages/twenty-front/src/modules/apollo/hooks/useApolloFactory.ts +++ b/packages/twenty-front/src/modules/apollo/hooks/useApolloFactory.ts @@ -1,7 +1,7 @@ import { InMemoryCache, NormalizedCacheObject } from '@apollo/client'; import { useMemo, useRef } from 'react'; import { useLocation, useNavigate } from 'react-router-dom'; -import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil'; +import { useRecoilState, useSetRecoilState } from 'recoil'; import { currentUserState } from '@/auth/states/currentUserState'; import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState'; @@ -9,7 +9,6 @@ import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; import { previousUrlState } from '@/auth/states/previousUrlState'; import { tokenPairState } from '@/auth/states/tokenPairState'; import { workspacesState } from '@/auth/states/workspaces'; -import { isDebugModeState } from '@/client-config/states/isDebugModeState'; import { REACT_APP_SERVER_BASE_URL } from '~/config'; import { useUpdateEffect } from '~/hooks/useUpdateEffect'; import { isMatchingLocation } from '~/utils/isMatchingLocation'; @@ -22,18 +21,16 @@ import { ApolloFactory, Options } from '../services/apollo.factory'; export const useApolloFactory = (options: Partial> = {}) => { // eslint-disable-next-line @nx/workspace-no-state-useref const apolloRef = useRef | null>(null); - const [isDebugMode] = useRecoilState(isDebugModeState); const navigate = useNavigate(); const setTokenPair = useSetRecoilState(tokenPairState); const [currentWorkspace, setCurrentWorkspace] = useRecoilState( currentWorkspaceState, ); - const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState); - const setCurrentUser = useSetRecoilState(currentUserState); - const setCurrentWorkspaceMember = useSetRecoilState( + const [currentWorkspaceMember, setCurrentWorkspaceMember] = useRecoilState( currentWorkspaceMemberState, ); + const setCurrentUser = useSetRecoilState(currentUserState); const setCurrentUserWorkspace = useSetRecoilState(currentUserWorkspaceState); const setWorkspaces = useSetRecoilState(workspacesState); @@ -50,18 +47,15 @@ export const useApolloFactory = (options: Partial> = {}) => { }, }, }), - headers: { - ...(currentWorkspace?.metadataVersion && { - 'X-Schema-Version': `${currentWorkspace.metadataVersion}`, - }), - }, + defaultOptions: { watchQuery: { fetchPolicy: 'cache-and-network', }, }, - connectToDevTools: isDebugMode, + connectToDevTools: process.env.IS_DEBUG_MODE === 'true', currentWorkspaceMember: currentWorkspaceMember, + currentWorkspace: currentWorkspace, onTokenPairChange: (tokenPair) => { setTokenPair(tokenPair); }, @@ -83,7 +77,7 @@ export const useApolloFactory = (options: Partial> = {}) => { } }, extraLinks: [], - isDebugMode, + isDebugMode: process.env.IS_DEBUG_MODE === 'true', // Override options ...options, }); @@ -96,8 +90,6 @@ export const useApolloFactory = (options: Partial> = {}) => { setCurrentWorkspaceMember, setCurrentWorkspace, setWorkspaces, - isDebugMode, - currentWorkspace?.metadataVersion, setPreviousUrl, ]); @@ -107,5 +99,11 @@ export const useApolloFactory = (options: Partial> = {}) => { } }, [currentWorkspaceMember]); + useUpdateEffect(() => { + if (isDefined(apolloRef.current)) { + apolloRef.current.updateCurrentWorkspace(currentWorkspace); + } + }, [currentWorkspace]); + return apolloClient; }; diff --git a/packages/twenty-front/src/modules/apollo/services/__tests__/apollo.factory.test.ts b/packages/twenty-front/src/modules/apollo/services/__tests__/apollo.factory.test.ts index c5933ffbd..2e75e22f0 100644 --- a/packages/twenty-front/src/modules/apollo/services/__tests__/apollo.factory.test.ts +++ b/packages/twenty-front/src/modules/apollo/services/__tests__/apollo.factory.test.ts @@ -1,6 +1,7 @@ import { ApolloError, gql, InMemoryCache } from '@apollo/client'; import fetchMock, { enableFetchMocks } from 'jest-fetch-mock'; +import { WorkspaceActivationStatus } from '~/generated/graphql'; import { ApolloFactory, Options } from '../apollo.factory'; enableFetchMocks(); @@ -31,9 +32,32 @@ const mockWorkspaceMember = { colorScheme: 'Light' as const, }; +const mockWorkspace = { + id: 'workspace-id', + metadataVersion: 1, + allowImpersonation: false, + activationStatus: WorkspaceActivationStatus.ACTIVE, + billingSubscriptions: [], + currentBillingSubscription: null, + workspaceMembersCount: 0, + isPublicInviteLinkEnabled: false, + isGoogleAuthEnabled: false, + isMicrosoftAuthEnabled: false, + isPasswordAuthEnabled: false, + isCustomDomainEnabled: false, + hasValidEnterpriseKey: false, + subdomain: 'test', + customDomain: 'test.com', + workspaceUrls: { + subdomainUrl: 'test.com', + customUrl: 'test.com', + }, +}; + const createMockOptions = (): Options => ({ uri: 'http://localhost:3000', currentWorkspaceMember: mockWorkspaceMember, + currentWorkspace: mockWorkspace, cache: new InMemoryCache(), isDebugMode: true, onError: mockOnError, diff --git a/packages/twenty-front/src/modules/apollo/services/apollo.factory.ts b/packages/twenty-front/src/modules/apollo/services/apollo.factory.ts index fcceb9196..fedf01a11 100644 --- a/packages/twenty-front/src/modules/apollo/services/apollo.factory.ts +++ b/packages/twenty-front/src/modules/apollo/services/apollo.factory.ts @@ -13,6 +13,7 @@ import { createUploadLink } from 'apollo-upload-client'; import { renewToken } from '@/auth/services/AuthService'; import { CurrentWorkspaceMember } from '@/auth/states/currentWorkspaceMemberState'; +import { CurrentWorkspace } from '@/auth/states/currentWorkspaceState'; import { AuthTokenPair } from '~/generated/graphql'; import { logDebug } from '~/utils/logDebug'; @@ -34,6 +35,7 @@ export interface Options extends ApolloClientOptions { onTokenPairChange?: (tokenPair: AuthTokenPair) => void; onUnauthenticatedError?: () => void; currentWorkspaceMember: CurrentWorkspaceMember | null; + currentWorkspace: CurrentWorkspace | null; extraLinks?: ApolloLink[]; isDebugMode?: boolean; } @@ -41,6 +43,7 @@ export interface Options extends ApolloClientOptions { export class ApolloFactory implements ApolloManager { private client: ApolloClient; private currentWorkspaceMember: CurrentWorkspaceMember | null = null; + private currentWorkspace: CurrentWorkspace | null = null; constructor(opts: Options) { const { @@ -50,12 +53,14 @@ export class ApolloFactory implements ApolloManager { onTokenPairChange, onUnauthenticatedError, currentWorkspaceMember, + currentWorkspace, extraLinks, isDebugMode, ...options } = opts; this.currentWorkspaceMember = currentWorkspaceMember; + this.currentWorkspace = currentWorkspace; const buildApolloLink = (): ApolloLink => { const httpLink = createUploadLink({ @@ -84,6 +89,9 @@ export class ApolloFactory implements ApolloManager { ...(this.currentWorkspaceMember?.locale ? { 'x-locale': this.currentWorkspaceMember.locale } : { 'x-locale': i18n.locale }), + ...(this.currentWorkspace?.metadataVersion && { + 'X-Schema-Version': `${this.currentWorkspace.metadataVersion}`, + }), }, }; }); @@ -188,6 +196,10 @@ export class ApolloFactory implements ApolloManager { this.currentWorkspaceMember = workspaceMember; } + updateCurrentWorkspace(workspace: CurrentWorkspace | null) { + this.currentWorkspace = workspace; + } + getClient() { return this.client; } diff --git a/packages/twenty-front/src/modules/apollo/utils/hasTokenPair.ts b/packages/twenty-front/src/modules/apollo/utils/hasTokenPair.ts new file mode 100644 index 000000000..b6dbc0d6a --- /dev/null +++ b/packages/twenty-front/src/modules/apollo/utils/hasTokenPair.ts @@ -0,0 +1,7 @@ +import { getTokenPair } from '@/apollo/utils/getTokenPair'; +import { isDefined } from 'twenty-shared/utils'; + +export const hasTokenPair = () => { + const tokenPair = getTokenPair(); + return isDefined(tokenPair) && isDefined(tokenPair.accessToken?.token); +}; diff --git a/packages/twenty-front/src/modules/app/components/AppRouterProviders.tsx b/packages/twenty-front/src/modules/app/components/AppRouterProviders.tsx index 397cc7600..3ebca4b84 100644 --- a/packages/twenty-front/src/modules/app/components/AppRouterProviders.tsx +++ b/packages/twenty-front/src/modules/app/components/AppRouterProviders.tsx @@ -33,15 +33,15 @@ export const AppRouterProviders = () => { const pageTitle = getPageTitleFromPath(pathname); return ( - - - - - + + + + + + + - - @@ -71,9 +71,9 @@ export const AppRouterProviders = () => { - - - - + + + + ); }; diff --git a/packages/twenty-front/src/modules/auth/components/VerifyLoginTokenEffect.tsx b/packages/twenty-front/src/modules/auth/components/VerifyLoginTokenEffect.tsx index c869ee9a0..620636f46 100644 --- a/packages/twenty-front/src/modules/auth/components/VerifyLoginTokenEffect.tsx +++ b/packages/twenty-front/src/modules/auth/components/VerifyLoginTokenEffect.tsx @@ -17,7 +17,7 @@ export const VerifyLoginTokenEffect = () => { const navigate = useNavigateApp(); const { verifyLoginToken } = useVerifyLogin(); - const { isLoaded: clientConfigLoaded } = useRecoilValue( + const { isSaved: clientConfigLoaded } = useRecoilValue( clientConfigApiStatusState, ); diff --git a/packages/twenty-front/src/modules/auth/hooks/__tests__/useAuth.test.tsx b/packages/twenty-front/src/modules/auth/hooks/__tests__/useAuth.test.tsx index b04f39bc2..0cb9d1e0a 100644 --- a/packages/twenty-front/src/modules/auth/hooks/__tests__/useAuth.test.tsx +++ b/packages/twenty-front/src/modules/auth/hooks/__tests__/useAuth.test.tsx @@ -1,6 +1,5 @@ import { useAuth } from '@/auth/hooks/useAuth'; import { billingState } from '@/client-config/states/billingState'; -import { isDebugModeState } from '@/client-config/states/isDebugModeState'; import { isDeveloperDefaultSignInPrefilledState } from '@/client-config/states/isDeveloperDefaultSignInPrefilledState'; import { supportChatState } from '@/client-config/states/supportChatState'; import { workspaceAuthProvidersState } from '@/workspace/states/workspaceAuthProvidersState'; @@ -118,7 +117,6 @@ describe('useAuth', () => { isDeveloperDefaultSignInPrefilledState, ); const supportChat = useRecoilValue(supportChatState); - const isDebugMode = useRecoilValue(isDebugModeState); const isMultiWorkspaceEnabled = useRecoilValue( isMultiWorkspaceEnabledState, ); @@ -131,7 +129,6 @@ describe('useAuth', () => { billing, isDeveloperDefaultSignInPrefilled, supportChat, - isDebugMode, isMultiWorkspaceEnabled, }, }; @@ -160,7 +157,6 @@ describe('useAuth', () => { supportDriver: 'none', supportFrontChatId: null, }); - expect(state.isDebugMode).toBe(false); }); it('should handle credential sign-up', async () => { diff --git a/packages/twenty-front/src/modules/auth/hooks/useAuth.ts b/packages/twenty-front/src/modules/auth/hooks/useAuth.ts index ac3ed8ed7..beed55d37 100644 --- a/packages/twenty-front/src/modules/auth/hooks/useAuth.ts +++ b/packages/twenty-front/src/modules/auth/hooks/useAuth.ts @@ -11,11 +11,9 @@ import { import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState'; import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; -import { isCurrentUserLoadedState } from '@/auth/states/isCurrentUserLoadingState'; import { workspacesState } from '@/auth/states/workspaces'; import { billingState } from '@/client-config/states/billingState'; import { clientConfigApiStatusState } from '@/client-config/states/clientConfigApiStatusState'; -import { isDebugModeState } from '@/client-config/states/isDebugModeState'; import { supportChatState } from '@/client-config/states/supportChatState'; import { ColorScheme } from '@/workspace-member/types/WorkspaceMember'; import { REACT_APP_SERVER_BASE_URL } from '~/config'; @@ -42,25 +40,27 @@ import { currentUserState } from '../states/currentUserState'; import { tokenPairState } from '../states/tokenPairState'; import { currentUserWorkspaceState } from '@/auth/states/currentUserWorkspaceState'; +import { isCurrentUserLoadedState } from '@/auth/states/isCurrentUserLoadedState'; import { SignInUpStep, signInUpStepState, } from '@/auth/states/signInUpStepState'; import { workspacePublicDataState } from '@/auth/states/workspacePublicDataState'; import { BillingCheckoutSession } from '@/auth/types/billingCheckoutSession.type'; +import { apiConfigState } from '@/client-config/states/apiConfigState'; import { captchaState } from '@/client-config/states/captchaState'; import { isEmailVerificationRequiredState } from '@/client-config/states/isEmailVerificationRequiredState'; import { isMultiWorkspaceEnabledState } from '@/client-config/states/isMultiWorkspaceEnabledState'; +import { sentryConfigState } from '@/client-config/states/sentryConfigState'; import { useIsCurrentLocationOnAWorkspace } from '@/domain-manager/hooks/useIsCurrentLocationOnAWorkspace'; import { useLastAuthenticatedWorkspaceDomain } from '@/domain-manager/hooks/useLastAuthenticatedWorkspaceDomain'; import { useOrigin } from '@/domain-manager/hooks/useOrigin'; import { useRedirect } from '@/domain-manager/hooks/useRedirect'; import { useRedirectToWorkspaceDomain } from '@/domain-manager/hooks/useRedirectToWorkspaceDomain'; import { domainConfigurationState } from '@/domain-manager/states/domainConfigurationState'; -import { useRefreshObjectMetadataItems } from '@/object-metadata/hooks/useRefreshObjectMetadataItem'; import { workspaceAuthProvidersState } from '@/workspace/states/workspaceAuthProvidersState'; import { i18n } from '@lingui/core'; -import { useSearchParams } from 'react-router-dom'; +import { useNavigate, useSearchParams } from 'react-router-dom'; import { APP_LOCALES } from 'twenty-shared/translations'; import { isDefined } from 'twenty-shared/utils'; import { iconsState } from 'twenty-ui/display'; @@ -85,8 +85,6 @@ export const useAuth = () => { isEmailVerificationRequiredState, ); - const { refreshObjectMetadataItems } = useRefreshObjectMetadataItems(); - const setSignInUpStep = useSetRecoilState(signInUpStepState); const setCurrentWorkspace = useSetRecoilState(currentWorkspaceState); const setWorkspaces = useSetRecoilState(workspacesState); @@ -119,10 +117,13 @@ export const useAuth = () => { const [, setSearchParams] = useSearchParams(); + const navigate = useNavigate(); + const clearSession = useRecoilCallback( ({ snapshot }) => async () => { const emptySnapshot = snapshot_UNSTABLE(); + const iconsValue = snapshot.getLoadable(iconsState).getValue(); const authProvidersValue = snapshot .getLoadable(workspaceAuthProvidersState) @@ -132,7 +133,6 @@ export const useAuth = () => { .getLoadable(isDeveloperDefaultSignInPrefilledState) .getValue(); const supportChat = snapshot.getLoadable(supportChatState).getValue(); - const isDebugMode = snapshot.getLoadable(isDebugModeState).getValue(); const captcha = snapshot.getLoadable(captchaState).getValue(); const clientConfigApiStatus = snapshot .getLoadable(clientConfigApiStatusState) @@ -146,6 +146,12 @@ export const useAuth = () => { const domainConfiguration = snapshot .getLoadable(domainConfigurationState) .getValue(); + const apiConfig = snapshot.getLoadable(apiConfigState).getValue(); + const sentryConfig = snapshot.getLoadable(sentryConfigState).getValue(); + const workspacePublicData = snapshot + .getLoadable(workspacePublicDataState) + .getValue(); + const initialSnapshot = emptySnapshot.map(({ set }) => { set(iconsState, iconsValue); set(workspaceAuthProvidersState, authProvidersValue); @@ -155,22 +161,27 @@ export const useAuth = () => { isDeveloperDefaultSignInPrefilled, ); set(supportChatState, supportChat); - set(isDebugModeState, isDebugMode); set(captchaState, captcha); + set(apiConfigState, apiConfig); + set(sentryConfigState, sentryConfig); + set(workspacePublicDataState, workspacePublicData); set(clientConfigApiStatusState, clientConfigApiStatus); set(isCurrentUserLoadedState, isCurrentUserLoaded); set(isMultiWorkspaceEnabledState, isMultiWorkspaceEnabled); set(domainConfigurationState, domainConfiguration); return undefined; }); + goToRecoilSnapshot(initialSnapshot); - await client.clearStore(); + sessionStorage.clear(); localStorage.clear(); + await client.clearStore(); // We need to explicitly clear the state to trigger the cookie deletion which include the parent domain setLastAuthenticateWorkspaceDomain(null); + navigate(AppPath.SignInUp); }, - [client, goToRecoilSnapshot, setLastAuthenticateWorkspaceDomain], + [navigate, client, goToRecoilSnapshot, setLastAuthenticateWorkspaceDomain], ); const handleGetLoginTokenFromCredentials = useCallback( @@ -368,16 +379,9 @@ export const useAuth = () => { ), ); - await refreshObjectMetadataItems(); await loadCurrentUser(); }, - [ - getAuthTokensFromLoginToken, - setTokenPair, - refreshObjectMetadataItems, - loadCurrentUser, - origin, - ], + [getAuthTokensFromLoginToken, setTokenPair, loadCurrentUser, origin], ); const handleCredentialsSignIn = useCallback( diff --git a/packages/twenty-front/src/modules/auth/states/isCurrentUserLoadingState.ts b/packages/twenty-front/src/modules/auth/states/isCurrentUserLoadedState.ts similarity index 100% rename from packages/twenty-front/src/modules/auth/states/isCurrentUserLoadingState.ts rename to packages/twenty-front/src/modules/auth/states/isCurrentUserLoadedState.ts diff --git a/packages/twenty-front/src/modules/captcha/components/CaptchaProvider.tsx b/packages/twenty-front/src/modules/captcha/components/CaptchaProvider.tsx index 97599b6c4..4491151af 100644 --- a/packages/twenty-front/src/modules/captcha/components/CaptchaProvider.tsx +++ b/packages/twenty-front/src/modules/captcha/components/CaptchaProvider.tsx @@ -1,21 +1,30 @@ -import React from 'react'; +import React, { useMemo } from 'react'; import { CaptchaProviderScriptLoaderEffect } from '@/captcha/components/CaptchaProviderScriptLoaderEffect'; import { isCaptchaRequiredForPath } from '@/captcha/utils/isCaptchaRequiredForPath'; import { useLocation } from 'react-router-dom'; -export const CaptchaProvider = ({ children }: React.PropsWithChildren) => { - const location = useLocation(); +export const CaptchaProvider = React.memo( + ({ children }: React.PropsWithChildren) => { + const location = useLocation(); - if (!isCaptchaRequiredForPath(location.pathname)) { - return <>{children}; - } + const isCaptchaRequired = useMemo( + () => isCaptchaRequiredForPath(location.pathname), + [location.pathname], + ); - return ( - <> -
- - {children} - - ); -}; + return ( + <> + {isCaptchaRequired && ( + <> +
+ + + )} + {children} + + ); + }, +); + +CaptchaProvider.displayName = 'CaptchaProvider'; diff --git a/packages/twenty-front/src/modules/client-config/components/ClientConfigProviderEffect.tsx b/packages/twenty-front/src/modules/client-config/components/ClientConfigProviderEffect.tsx index 809716c44..4fc28b4f7 100644 --- a/packages/twenty-front/src/modules/client-config/components/ClientConfigProviderEffect.tsx +++ b/packages/twenty-front/src/modules/client-config/components/ClientConfigProviderEffect.tsx @@ -9,7 +9,6 @@ import { clientConfigApiStatusState } from '@/client-config/states/clientConfigA import { isAnalyticsEnabledState } from '@/client-config/states/isAnalyticsEnabledState'; import { isAttachmentPreviewEnabledState } from '@/client-config/states/isAttachmentPreviewEnabledState'; import { isConfigVariablesInDbEnabledState } from '@/client-config/states/isConfigVariablesInDbEnabledState'; -import { isDebugModeState } from '@/client-config/states/isDebugModeState'; import { isDeveloperDefaultSignInPrefilledState } from '@/client-config/states/isDeveloperDefaultSignInPrefilledState'; import { isEmailVerificationRequiredState } from '@/client-config/states/isEmailVerificationRequiredState'; import { isGoogleCalendarEnabledState } from '@/client-config/states/isGoogleCalendarEnabledState'; @@ -26,7 +25,6 @@ import { useRecoilState, useSetRecoilState } from 'recoil'; import { isDefined } from 'twenty-shared/utils'; export const ClientConfigProviderEffect = () => { - const setIsDebugMode = useSetRecoilState(isDebugModeState); const setIsAnalyticsEnabled = useSetRecoilState(isAnalyticsEnabledState); const setDomainConfiguration = useSetRecoilState(domainConfigurationState); const setAuthProviders = useSetRecoilState(authProvidersState); @@ -90,10 +88,17 @@ export const ClientConfigProviderEffect = () => { const { data, loading, error, fetchClientConfig } = useClientConfig(); useEffect(() => { - if (!clientConfigApiStatus.isLoaded) { + if ( + !clientConfigApiStatus.isLoadedOnce && + !clientConfigApiStatus.isLoading + ) { fetchClientConfig(); } - }, [clientConfigApiStatus.isLoaded, fetchClientConfig]); + }, [ + clientConfigApiStatus.isLoadedOnce, + clientConfigApiStatus.isLoading, + fetchClientConfig, + ]); useEffect(() => { if (loading) return; @@ -124,7 +129,6 @@ export const ClientConfigProviderEffect = () => { magicLink: false, sso: data?.clientConfig.authProviders.sso, }); - setIsDebugMode(data?.clientConfig.debugMode); setIsAnalyticsEnabled(data?.clientConfig.analyticsEnabled); setIsDeveloperDefaultSignInPrefilled(data?.clientConfig.signInPrefilled); setIsMultiWorkspaceEnabled(data?.clientConfig.isMultiWorkspaceEnabled); @@ -167,24 +171,23 @@ export const ClientConfigProviderEffect = () => { ); setClientConfigApiStatus((currentStatus) => ({ ...currentStatus, - isLoaded: true, + isSaved: true, })); }, [ data, - setIsDebugMode, + loading, + error, setIsDeveloperDefaultSignInPrefilled, setIsMultiWorkspaceEnabled, setIsEmailVerificationRequired, setSupportChat, setBilling, setSentryConfig, - loading, setClientConfigApiStatus, setCaptcha, setChromeExtensionId, setApiConfig, setIsAnalyticsEnabled, - error, setDomainConfiguration, setAuthProviders, setCanManageFeatureFlags, diff --git a/packages/twenty-front/src/modules/client-config/hooks/useClientConfig.ts b/packages/twenty-front/src/modules/client-config/hooks/useClientConfig.ts index 92e94cf98..165c4f090 100644 --- a/packages/twenty-front/src/modules/client-config/hooks/useClientConfig.ts +++ b/packages/twenty-front/src/modules/client-config/hooks/useClientConfig.ts @@ -1,4 +1,4 @@ -import { useRecoilCallback, useRecoilValue } from 'recoil'; +import { useRecoilValue, useSetRecoilState } from 'recoil'; import { ClientConfig } from '~/generated/graphql'; import { clientConfigApiStatusState } from '../states/clientConfigApiStatusState'; import { getClientConfig } from '../utils/getClientConfig'; @@ -13,41 +13,38 @@ type UseClientConfigResult = { export const useClientConfig = (): UseClientConfigResult => { const clientConfigApiStatus = useRecoilValue(clientConfigApiStatusState); - - const fetchClientConfig = useRecoilCallback( - ({ set }) => - async () => { - set(clientConfigApiStatusState, (prev) => ({ - ...prev, - isLoading: true, - isErrored: false, - error: undefined, - })); - - try { - const clientConfig = await getClientConfig(); - set(clientConfigApiStatusState, (prev) => ({ - ...prev, - isLoading: false, - isLoaded: true, - data: { clientConfig }, - })); - } catch (err) { - const error = - err instanceof Error - ? err - : new Error('Failed to fetch client config'); - set(clientConfigApiStatusState, (prev) => ({ - ...prev, - isLoading: false, - isErrored: true, - error, - })); - } - }, - [], + const setClientConfigApiStatus = useSetRecoilState( + clientConfigApiStatusState, ); + const fetchClientConfig = async () => { + setClientConfigApiStatus((prev) => ({ + ...prev, + isLoading: true, + })); + + try { + const clientConfig = await getClientConfig(); + setClientConfigApiStatus((prev) => ({ + ...prev, + isLoading: false, + isLoadedOnce: true, + isErrored: false, + error: undefined, + data: { clientConfig }, + })); + } catch (err) { + const error = + err instanceof Error ? err : new Error('Failed to fetch client config'); + setClientConfigApiStatus((prev) => ({ + ...prev, + isLoading: false, + isErrored: true, + error, + })); + } + }; + return { data: clientConfigApiStatus.data, loading: clientConfigApiStatus.isLoading || false, diff --git a/packages/twenty-front/src/modules/client-config/states/clientConfigApiStatusState.ts b/packages/twenty-front/src/modules/client-config/states/clientConfigApiStatusState.ts index 0f25510f0..de3f188e4 100644 --- a/packages/twenty-front/src/modules/client-config/states/clientConfigApiStatusState.ts +++ b/packages/twenty-front/src/modules/client-config/states/clientConfigApiStatusState.ts @@ -2,9 +2,10 @@ import { createState } from 'twenty-ui/utilities'; import { ClientConfig } from '~/generated/graphql'; type ClientConfigApiStatus = { - isLoaded: boolean; + isLoadedOnce: boolean; isLoading: boolean; isErrored: boolean; + isSaved: boolean; error?: Error; data?: { clientConfig: ClientConfig }; }; @@ -12,9 +13,10 @@ type ClientConfigApiStatus = { export const clientConfigApiStatusState = createState({ key: 'clientConfigApiStatus', defaultValue: { - isLoaded: false, + isLoadedOnce: false, isLoading: false, isErrored: false, + isSaved: false, error: undefined, data: undefined, }, diff --git a/packages/twenty-front/src/modules/client-config/states/isDebugModeState.ts b/packages/twenty-front/src/modules/client-config/states/isDebugModeState.ts deleted file mode 100644 index 094995765..000000000 --- a/packages/twenty-front/src/modules/client-config/states/isDebugModeState.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { createState } from 'twenty-ui/utilities'; -export const isDebugModeState = createState({ - key: 'isDebugModeState', - defaultValue: false, -}); diff --git a/packages/twenty-front/src/modules/context-store/components/MainContextStoreProviderEffect.tsx b/packages/twenty-front/src/modules/context-store/components/MainContextStoreProviderEffect.tsx index cd7d1e6b1..8fcaee758 100644 --- a/packages/twenty-front/src/modules/context-store/components/MainContextStoreProviderEffect.tsx +++ b/packages/twenty-front/src/modules/context-store/components/MainContextStoreProviderEffect.tsx @@ -2,14 +2,12 @@ import { MAIN_CONTEXT_STORE_INSTANCE_ID } from '@/context-store/constants/MainCo import { contextStoreCurrentObjectMetadataItemIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataItemIdComponentState'; import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState'; import { contextStoreCurrentViewTypeComponentState } from '@/context-store/states/contextStoreCurrentViewTypeComponentState'; -import { ContextStoreViewType } from '@/context-store/types/ContextStoreViewType'; +import { getViewType } from '@/context-store/utils/getViewType'; import { useSetLastVisitedObjectMetadataId } from '@/navigation/hooks/useSetLastVisitedObjectMetadataId'; import { useSetLastVisitedViewForObjectMetadataNamePlural } from '@/navigation/hooks/useSetLastVisitedViewForObjectMetadataNamePlural'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { prefetchViewFromViewIdFamilySelector } from '@/prefetch/states/selector/prefetchViewFromViewIdFamilySelector'; import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2'; -import { View } from '@/views/types/View'; -import { ViewType } from '@/views/types/ViewType'; import { useEffect } from 'react'; import { useRecoilValue } from 'recoil'; @@ -124,31 +122,3 @@ export const MainContextStoreProviderEffect = ({ return null; }; - -const getViewType = ({ - isSettingsPage, - isRecordShowPage, - isRecordIndexPage, - view, -}: { - isSettingsPage: boolean; - isRecordShowPage: boolean; - isRecordIndexPage: boolean; - view?: View; -}) => { - if (isSettingsPage) { - return null; - } - - if (isRecordIndexPage) { - return view?.type === ViewType.Kanban - ? ContextStoreViewType.Kanban - : ContextStoreViewType.Table; - } - - if (isRecordShowPage) { - return ContextStoreViewType.ShowPage; - } - - return null; -}; diff --git a/packages/twenty-front/src/modules/context-store/utils/getViewType.ts b/packages/twenty-front/src/modules/context-store/utils/getViewType.ts new file mode 100644 index 000000000..90790864e --- /dev/null +++ b/packages/twenty-front/src/modules/context-store/utils/getViewType.ts @@ -0,0 +1,31 @@ +import { ContextStoreViewType } from '@/context-store/types/ContextStoreViewType'; +import { View } from '@/views/types/View'; +import { ViewType } from '@/views/types/ViewType'; + +export const getViewType = ({ + isSettingsPage, + isRecordShowPage, + isRecordIndexPage, + view, +}: { + isSettingsPage: boolean; + isRecordShowPage: boolean; + isRecordIndexPage: boolean; + view?: View; +}) => { + if (isSettingsPage) { + return null; + } + + if (isRecordIndexPage) { + return view?.type === ViewType.Kanban + ? ContextStoreViewType.Kanban + : ContextStoreViewType.Table; + } + + if (isRecordShowPage) { + return ContextStoreViewType.ShowPage; + } + + return null; +}; diff --git a/packages/twenty-front/src/modules/debug/components/ApolloDevLogEffect.tsx b/packages/twenty-front/src/modules/debug/components/ApolloDevLogEffect.tsx index a1959a4b5..f705ba63e 100644 --- a/packages/twenty-front/src/modules/debug/components/ApolloDevLogEffect.tsx +++ b/packages/twenty-front/src/modules/debug/components/ApolloDevLogEffect.tsx @@ -1,11 +1,8 @@ import { loadDevMessages, loadErrorMessages } from '@apollo/client/dev'; import { useEffect } from 'react'; -import { useRecoilValue } from 'recoil'; - -import { isDebugModeState } from '@/client-config/states/isDebugModeState'; export const ApolloDevLogEffect = () => { - const isDebugMode = useRecoilValue(isDebugModeState); + const isDebugMode = process.env.IS_DEBUG_MODE === 'true'; useEffect(() => { if (isDebugMode) { diff --git a/packages/twenty-front/src/modules/debug/components/RecoilDebugObserver.tsx b/packages/twenty-front/src/modules/debug/components/RecoilDebugObserver.tsx index f6ce48693..5586cad1b 100644 --- a/packages/twenty-front/src/modules/debug/components/RecoilDebugObserver.tsx +++ b/packages/twenty-front/src/modules/debug/components/RecoilDebugObserver.tsx @@ -1,5 +1,4 @@ -import { isDebugModeState } from '@/client-config/states/isDebugModeState'; -import { useRecoilTransactionObserver_UNSTABLE, useRecoilValue } from 'recoil'; +import { useRecoilTransactionObserver_UNSTABLE } from 'recoil'; import { logDebug } from '~/utils/logDebug'; @@ -15,7 +14,7 @@ const formatTitle = (stateName: string) => { }; export const RecoilDebugObserverEffect = () => { - const isDebugMode = useRecoilValue(isDebugModeState); + const isDebugMode = process.env.IS_DEBUG_MODE === 'true'; useRecoilTransactionObserver_UNSTABLE(({ snapshot }) => { if (!isDebugMode) { diff --git a/packages/twenty-front/src/modules/domain-manager/hooks/useGetPublicWorkspaceDataByDomain.ts b/packages/twenty-front/src/modules/domain-manager/hooks/useGetPublicWorkspaceDataByDomain.ts index 57c4f0f88..201149ff2 100644 --- a/packages/twenty-front/src/modules/domain-manager/hooks/useGetPublicWorkspaceDataByDomain.ts +++ b/packages/twenty-front/src/modules/domain-manager/hooks/useGetPublicWorkspaceDataByDomain.ts @@ -28,7 +28,7 @@ export const useGetPublicWorkspaceDataByDomain = () => { origin, }, skip: - !clientConfigApiStatus.isLoaded || + !clientConfigApiStatus.isSaved || (isMultiWorkspaceEnabled && isDefaultDomain) || isDefined(workspacePublicData), onCompleted: (data) => { @@ -38,9 +38,17 @@ export const useGetPublicWorkspaceDataByDomain = () => { setWorkspacePublicDataState(data.getPublicWorkspaceDataByDomain); }, onError: (error) => { - // eslint-disable-next-line no-console - console.error(error); - redirectToDefaultDomain(); + // Only redirect to default domain if it's a workspace not found error + const isWorkspaceNotFoundError = error.graphQLErrors?.some( + (graphQLError) => graphQLError.extensions?.code === 'NOT_FOUND', + ); + + if (isWorkspaceNotFoundError) { + redirectToDefaultDomain(); + } else { + // eslint-disable-next-line no-console + console.error(error); + } }, }); diff --git a/packages/twenty-front/src/modules/object-metadata/components/ObjectMetadataItemsLoadEffect.tsx b/packages/twenty-front/src/modules/object-metadata/components/ObjectMetadataItemsLoadEffect.tsx index 001ec8284..e675caec6 100644 --- a/packages/twenty-front/src/modules/object-metadata/components/ObjectMetadataItemsLoadEffect.tsx +++ b/packages/twenty-front/src/modules/object-metadata/components/ObjectMetadataItemsLoadEffect.tsx @@ -5,8 +5,8 @@ import { currentUserState } from '@/auth/states/currentUserState'; import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; import { useLoadMockedObjectMetadataItems } from '@/object-metadata/hooks/useLoadMockedObjectMetadataItems'; import { useRefreshObjectMetadataItems } from '@/object-metadata/hooks/useRefreshObjectMetadataItem'; -import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull'; import { isWorkspaceActiveOrSuspended } from 'twenty-shared/workspace'; +import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull'; export const ObjectMetadataItemsLoadEffect = () => { const currentUser = useRecoilValue(currentUserState); diff --git a/packages/twenty-front/src/modules/users/components/UserProvider.tsx b/packages/twenty-front/src/modules/users/components/UserProvider.tsx index 81a48a216..796f59b4c 100644 --- a/packages/twenty-front/src/modules/users/components/UserProvider.tsx +++ b/packages/twenty-front/src/modules/users/components/UserProvider.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { useRecoilValue } from 'recoil'; -import { isCurrentUserLoadedState } from '@/auth/states/isCurrentUserLoadingState'; +import { isCurrentUserLoadedState } from '@/auth/states/isCurrentUserLoadedState'; import { dateTimeFormatState } from '@/localization/states/dateTimeFormatState'; import { AppPath } from '@/types/AppPath'; import { UserContext } from '@/users/contexts/UserContext'; diff --git a/packages/twenty-front/src/modules/users/components/UserProviderEffect.tsx b/packages/twenty-front/src/modules/users/components/UserProviderEffect.tsx index 7adbc8356..a6a3f4545 100644 --- a/packages/twenty-front/src/modules/users/components/UserProviderEffect.tsx +++ b/packages/twenty-front/src/modules/users/components/UserProviderEffect.tsx @@ -1,13 +1,13 @@ -import { useEffect, useState } from 'react'; import { useRecoilCallback, useRecoilState, useSetRecoilState } from 'recoil'; +import { useIsLogged } from '@/auth/hooks/useIsLogged'; import { currentUserState } from '@/auth/states/currentUserState'; import { currentUserWorkspaceState } from '@/auth/states/currentUserWorkspaceState'; import { currentWorkspaceDeletedMembersState } from '@/auth/states/currentWorkspaceDeletedMembersStates'; import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState'; import { currentWorkspaceMembersState } from '@/auth/states/currentWorkspaceMembersStates'; import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; -import { isCurrentUserLoadedState } from '@/auth/states/isCurrentUserLoadingState'; +import { isCurrentUserLoadedState } from '@/auth/states/isCurrentUserLoadedState'; import { workspacesState } from '@/auth/states/workspaces'; import { DateFormat } from '@/localization/constants/DateFormat'; import { TimeFormat } from '@/localization/constants/TimeFormat'; @@ -21,6 +21,7 @@ import { AppPath } from '@/types/AppPath'; import { getDateFnsLocale } from '@/ui/field/display/utils/getDateFnsLocale.util'; import { ColorScheme } from '@/workspace-member/types/WorkspaceMember'; import { enUS } from 'date-fns/locale'; +import { useEffect } from 'react'; import { useLocation } from 'react-router-dom'; import { APP_LOCALES, SOURCE_LOCALE } from 'twenty-shared/translations'; import { isDefined } from 'twenty-shared/utils'; @@ -31,7 +32,6 @@ import { dynamicActivate } from '~/utils/i18n/dynamicActivate'; import { isMatchingLocation } from '~/utils/isMatchingLocation'; export const UserProviderEffect = () => { - const [isLoading, setIsLoading] = useState(true); const location = useLocation(); const [isCurrentUserLoaded, setIsCurrentUserLoaded] = useRecoilState( @@ -42,6 +42,8 @@ export const UserProviderEffect = () => { const setCurrentUserWorkspace = useSetRecoilState(currentUserWorkspaceState); const setWorkspaces = useSetRecoilState(workspacesState); const setDateTimeFormat = useSetRecoilState(dateTimeFormatState); + const isLoggedIn = useIsLogged(); + const updateLocaleCatalog = useRecoilCallback( ({ snapshot, set }) => async (newLocale: keyof typeof APP_LOCALES) => { @@ -68,8 +70,9 @@ export const UserProviderEffect = () => { currentWorkspaceDeletedMembersState, ); - const { loading: queryLoading, data: queryData } = useGetCurrentUserQuery({ + const { data: queryData, loading: queryLoading } = useGetCurrentUserQuery({ skip: + !isLoggedIn || isCurrentUserLoaded || isMatchingLocation(location, AppPath.Verify) || isMatchingLocation(location, AppPath.VerifyEmail), @@ -77,7 +80,6 @@ export const UserProviderEffect = () => { useEffect(() => { if (!queryLoading) { - setIsLoading(false); setIsCurrentUserLoaded(true); } @@ -159,15 +161,14 @@ export const UserProviderEffect = () => { setWorkspaces(workspaces); } }, [ + queryLoading, + queryData?.currentUser, setCurrentUser, setCurrentUserWorkspace, setCurrentWorkspaceMembers, - isLoading, - queryLoading, setCurrentWorkspace, setCurrentWorkspaceMember, setWorkspaces, - queryData?.currentUser, setIsCurrentUserLoaded, setDateTimeFormat, setCurrentWorkspaceMembersWithDeleted, diff --git a/packages/twenty-front/src/pages/settings/profile/appearance/components/LocalePicker.tsx b/packages/twenty-front/src/pages/settings/profile/appearance/components/LocalePicker.tsx index 0a4dc08f7..e51a0d600 100644 --- a/packages/twenty-front/src/pages/settings/profile/appearance/components/LocalePicker.tsx +++ b/packages/twenty-front/src/pages/settings/profile/appearance/components/LocalePicker.tsx @@ -1,8 +1,7 @@ import styled from '@emotion/styled'; -import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil'; +import { useRecoilState, useSetRecoilState } from 'recoil'; import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState'; -import { isDebugModeState } from '@/client-config/states/isDebugModeState'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord'; import { getDateFnsLocale } from '@/ui/field/display/utils/getDateFnsLocale.util'; @@ -29,7 +28,6 @@ export const LocalePicker = () => { currentWorkspaceMemberState, ); const setDateLocale = useSetRecoilState(dateLocaleState); - const isDebugMode = useRecoilValue(isDebugModeState); const { updateOneRecord } = useUpdateOneRecord({ objectNameSingular: CoreObjectNameSingular.WorkspaceMember, @@ -204,7 +202,7 @@ export const LocalePicker = () => { }, ]; - if (isDebugMode) { + if (process.env.NODE_ENV === 'development') { unsortedLocaleOptions.push({ label: t`Pseudo-English`, value: APP_LOCALES['pseudo-en'], diff --git a/packages/twenty-front/vite.config.ts b/packages/twenty-front/vite.config.ts index 2706bf728..299d55dca 100644 --- a/packages/twenty-front/vite.config.ts +++ b/packages/twenty-front/vite.config.ts @@ -24,6 +24,7 @@ export default defineConfig(({ command, mode }) => { SSL_CERT_PATH, SSL_KEY_PATH, REACT_APP_PORT, + IS_DEBUG_MODE, } = env; const port = isNonEmptyString(REACT_APP_PORT) @@ -191,6 +192,7 @@ export default defineConfig(({ command, mode }) => { }, 'process.env': { REACT_APP_SERVER_BASE_URL, + IS_DEBUG_MODE, }, }, css: {