Various fixes (#11448)

# Scrollbar fix

Fixes https://github.com/twentyhq/twenty/issues/11403

<img width="1512" alt="image"
src="https://github.com/user-attachments/assets/b13fe0f2-8c61-4ea8-9ea1-e61e571a90da"
/>

---------

Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
This commit is contained in:
Charles Bochet
2025-04-09 01:03:43 +02:00
committed by GitHub
parent ab63214efa
commit b6e344e7be
41 changed files with 265 additions and 470 deletions

View File

@ -60,7 +60,7 @@
} }
</script> </script>
</head> </head>
<body style="width: 100%; height: 100vh"> <body>
<noscript>You need to enable JavaScript to run this app.</noscript> <noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div> <div id="root"></div>
<script <script

View File

@ -91,23 +91,23 @@ const testCases: {
{ loc: AppPath.SignInUp, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.INVITE_TEAM, res: AppPath.InviteTeam }, { loc: AppPath.SignInUp, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.INVITE_TEAM, res: AppPath.InviteTeam },
{ loc: AppPath.SignInUp, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.COMPLETED, res: defaultHomePagePath }, { loc: AppPath.SignInUp, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.COMPLETED, res: defaultHomePagePath },
{ loc: AppPath.Invite, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PLAN_REQUIRED, res: undefined }, { loc: AppPath.Invite, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PLAN_REQUIRED, res: '/plan-required' },
{ loc: AppPath.Invite, isLoggedIn: true, isWorkspaceSuspended: true, onboardingStatus: OnboardingStatus.COMPLETED, res: undefined }, { loc: AppPath.Invite, isLoggedIn: true, isWorkspaceSuspended: true, onboardingStatus: OnboardingStatus.COMPLETED, res: '/settings/billing' },
{ loc: AppPath.Invite, isLoggedIn: false, isWorkspaceSuspended: false, onboardingStatus: undefined, res: undefined }, { loc: AppPath.Invite, isLoggedIn: false, isWorkspaceSuspended: false, onboardingStatus: undefined, res: undefined },
{ loc: AppPath.Invite, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WORKSPACE_ACTIVATION, res: undefined }, { loc: AppPath.Invite, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WORKSPACE_ACTIVATION, res: '/create/workspace' },
{ loc: AppPath.Invite, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PROFILE_CREATION, res: undefined }, { loc: AppPath.Invite, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PROFILE_CREATION, res: '/create/profile' },
{ loc: AppPath.Invite, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.SYNC_EMAIL, res: undefined }, { loc: AppPath.Invite, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.SYNC_EMAIL, res: '/sync/emails' },
{ loc: AppPath.Invite, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.INVITE_TEAM, res: undefined }, { loc: AppPath.Invite, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.INVITE_TEAM, res: '/invite-team' },
{ loc: AppPath.Invite, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.COMPLETED, res: undefined }, { loc: AppPath.Invite, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.COMPLETED, res: '/objects/companies' },
{ loc: AppPath.ResetPassword, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PLAN_REQUIRED, res: undefined }, { loc: AppPath.ResetPassword, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PLAN_REQUIRED, res: '/plan-required' },
{ loc: AppPath.ResetPassword, isLoggedIn: true, isWorkspaceSuspended: true, onboardingStatus: OnboardingStatus.COMPLETED, res: undefined }, { loc: AppPath.ResetPassword, isLoggedIn: true, isWorkspaceSuspended: true, onboardingStatus: OnboardingStatus.COMPLETED, res: '/settings/billing' },
{ loc: AppPath.ResetPassword, isLoggedIn: false, isWorkspaceSuspended: false, onboardingStatus: undefined, res: undefined }, { loc: AppPath.ResetPassword, isLoggedIn: false, isWorkspaceSuspended: false, onboardingStatus: undefined, res: undefined },
{ loc: AppPath.ResetPassword, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WORKSPACE_ACTIVATION, res: undefined }, { loc: AppPath.ResetPassword, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WORKSPACE_ACTIVATION, res: '/create/workspace' },
{ loc: AppPath.ResetPassword, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PROFILE_CREATION, res: undefined }, { loc: AppPath.ResetPassword, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PROFILE_CREATION, res: '/create/profile' },
{ loc: AppPath.ResetPassword, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.SYNC_EMAIL, res: undefined }, { loc: AppPath.ResetPassword, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.SYNC_EMAIL, res: '/sync/emails' },
{ loc: AppPath.ResetPassword, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.INVITE_TEAM, res: undefined }, { loc: AppPath.ResetPassword, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.INVITE_TEAM, res: '/invite-team' },
{ loc: AppPath.ResetPassword, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.COMPLETED, res: undefined }, { loc: AppPath.ResetPassword, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.COMPLETED, res: '/objects/companies' },
{ loc: AppPath.VerifyEmail, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PLAN_REQUIRED, res: AppPath.PlanRequired }, { loc: AppPath.VerifyEmail, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PLAN_REQUIRED, res: AppPath.PlanRequired },
{ loc: AppPath.VerifyEmail, isLoggedIn: true, isWorkspaceSuspended: true, onboardingStatus: OnboardingStatus.COMPLETED, res: '/settings/billing' }, { loc: AppPath.VerifyEmail, isLoggedIn: true, isWorkspaceSuspended: true, onboardingStatus: OnboardingStatus.COMPLETED, res: '/settings/billing' },

View File

@ -7,10 +7,10 @@ import { SettingsPath } from '@/types/SettingsPath';
import { useIsWorkspaceActivationStatusEqualsTo } from '@/workspace/hooks/useIsWorkspaceActivationStatusEqualsTo'; import { useIsWorkspaceActivationStatusEqualsTo } from '@/workspace/hooks/useIsWorkspaceActivationStatusEqualsTo';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import { useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
import { WorkspaceActivationStatus } from 'twenty-shared/workspace';
import { OnboardingStatus } from '~/generated/graphql'; import { OnboardingStatus } from '~/generated/graphql';
import { useIsMatchingLocation } from '~/hooks/useIsMatchingLocation'; import { useIsMatchingLocation } from '~/hooks/useIsMatchingLocation';
import { WorkspaceActivationStatus } from 'twenty-shared/workspace';
import { isDefined } from 'twenty-shared/utils';
export const usePageChangeEffectNavigateLocation = () => { export const usePageChangeEffectNavigateLocation = () => {
const { isMatchingLocation } = useIsMatchingLocation(); const { isMatchingLocation } = useIsMatchingLocation();
@ -47,10 +47,6 @@ export const usePageChangeEffectNavigateLocation = () => {
(objectMetadataItem) => objectMetadataItem.namePlural === objectNamePlural, (objectMetadataItem) => objectMetadataItem.namePlural === objectNamePlural,
); );
if (isMatchingOpenRoute) {
return;
}
if (!isLoggedIn && !isMatchingOngoingUserCreationRoute) { if (!isLoggedIn && !isMatchingOngoingUserCreationRoute) {
return AppPath.SignInUp; return AppPath.SignInUp;
} }

View File

@ -1,9 +1,11 @@
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { useShowAuthModal } from '@/ui/layout/hooks/useShowAuthModal';
import { Modal } from '@/ui/layout/modal/components/Modal';
import { NAV_DRAWER_WIDTHS } from '@/ui/navigation/navigation-drawer/constants/NavDrawerWidths'; import { NAV_DRAWER_WIDTHS } from '@/ui/navigation/navigation-drawer/constants/NavDrawerWidths';
import { MOBILE_VIEWPORT } from 'twenty-ui/theme';
import { LeftPanelSkeletonLoader } from '~/loading/components/LeftPanelSkeletonLoader'; import { LeftPanelSkeletonLoader } from '~/loading/components/LeftPanelSkeletonLoader';
import { RightPanelSkeletonLoader } from '~/loading/components/RightPanelSkeletonLoader'; import { RightPanelSkeletonLoader } from '~/loading/components/RightPanelSkeletonLoader';
import { MOBILE_VIEWPORT } from 'twenty-ui/theme';
const StyledContainer = styled.div` const StyledContainer = styled.div`
background: ${({ theme }) => theme.background.noisy}; background: ${({ theme }) => theme.background.noisy};
@ -23,8 +25,11 @@ const StyledContainer = styled.div`
`; `;
export const UserOrMetadataLoader = () => { export const UserOrMetadataLoader = () => {
const showAuthModal = useShowAuthModal();
return ( return (
<StyledContainer> <StyledContainer>
{showAuthModal && <Modal.Backdrop modalVariant="primary" />}
<LeftPanelSkeletonLoader /> <LeftPanelSkeletonLoader />
<RightPanelSkeletonLoader /> <RightPanelSkeletonLoader />
</StyledContainer> </StyledContainer>

View File

@ -10,7 +10,7 @@ import { ClientConfigProviderEffect } from '@/client-config/components/ClientCon
import { MainContextStoreProvider } from '@/context-store/components/MainContextStoreProvider'; import { MainContextStoreProvider } from '@/context-store/components/MainContextStoreProvider';
import { PromiseRejectionEffect } from '@/error-handler/components/PromiseRejectionEffect'; import { PromiseRejectionEffect } from '@/error-handler/components/PromiseRejectionEffect';
import { ApolloMetadataClientProvider } from '@/object-metadata/components/ApolloMetadataClientProvider'; import { ApolloMetadataClientProvider } from '@/object-metadata/components/ApolloMetadataClientProvider';
import { ObjectMetadataItemsGater } from '@/object-metadata/components/ObjectMetadataItemsGater'; import { ObjectMetadataItemsLoadEffect } from '@/object-metadata/components/ObjectMetadataItemsLoadEffect';
import { ObjectMetadataItemsProvider } from '@/object-metadata/components/ObjectMetadataItemsProvider'; import { ObjectMetadataItemsProvider } from '@/object-metadata/components/ObjectMetadataItemsProvider';
import { PrefetchDataProvider } from '@/prefetch/components/PrefetchDataProvider'; import { PrefetchDataProvider } from '@/prefetch/components/PrefetchDataProvider';
import { DialogManager } from '@/ui/feedback/dialog-manager/components/DialogManager'; import { DialogManager } from '@/ui/feedback/dialog-manager/components/DialogManager';
@ -45,28 +45,27 @@ export const AppRouterProviders = () => {
<UserProvider> <UserProvider>
<AuthProvider> <AuthProvider>
<ApolloMetadataClientProvider> <ApolloMetadataClientProvider>
<ObjectMetadataItemsLoadEffect />
<ObjectMetadataItemsProvider> <ObjectMetadataItemsProvider>
<ObjectMetadataItemsGater> <PrefetchDataProvider>
<PrefetchDataProvider> <UserThemeProviderEffect />
<UserThemeProviderEffect /> <SnackBarProvider>
<SnackBarProvider> <DialogManagerScope dialogManagerScopeId="dialog-manager">
<DialogManagerScope dialogManagerScopeId="dialog-manager"> <DialogManager>
<DialogManager> <StrictMode>
<StrictMode> <PromiseRejectionEffect />
<PromiseRejectionEffect /> <GotoHotkeysEffectsProvider />
<GotoHotkeysEffectsProvider /> <ServerPreconnect />
<ServerPreconnect /> <PageTitle title={pageTitle} />
<PageTitle title={pageTitle} /> <PageFavicon />
<PageFavicon /> <Outlet />
<Outlet /> </StrictMode>
</StrictMode> </DialogManager>
</DialogManager> </DialogManagerScope>
</DialogManagerScope> </SnackBarProvider>
</SnackBarProvider> <MainContextStoreProvider />
<MainContextStoreProvider /> </PrefetchDataProvider>
</PrefetchDataProvider> <PageChangeEffect />
<PageChangeEffect />
</ObjectMetadataItemsGater>
</ObjectMetadataItemsProvider> </ObjectMetadataItemsProvider>
</ApolloMetadataClientProvider> </ApolloMetadataClientProvider>
</AuthProvider> </AuthProvider>

View File

@ -1,5 +1,6 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { import {
createSearchParams,
matchPath, matchPath,
useLocation, useLocation,
useNavigate, useNavigate,
@ -60,15 +61,27 @@ export const PageChangeEffect = () => {
}, [location, previousLocation]); }, [location, previousLocation]);
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const navigationParams = searchParams.get('animateModal')
? `?animateModal=${searchParams.get('animateModal')}`
: '';
useEffect(() => { useEffect(() => {
if (isDefined(pageChangeEffectNavigateLocation)) { if (isDefined(pageChangeEffectNavigateLocation)) {
navigate(pageChangeEffectNavigateLocation + navigationParams); const hasQueryParams = pageChangeEffectNavigateLocation.includes('?');
const navigationParams = createSearchParams({
...(searchParams.get('animateModal')
? { animateModal: searchParams.get('animateModal') ?? 'false' }
: {}),
});
if (hasQueryParams) {
navigate(pageChangeEffectNavigateLocation);
} else {
navigate({
pathname: pageChangeEffectNavigateLocation,
search: navigationParams.toString(),
});
}
} }
}, [navigate, pageChangeEffectNavigateLocation, navigationParams]); }, [navigate, pageChangeEffectNavigateLocation, searchParams]);
useEffect(() => { useEffect(() => {
const isLeavingRecordIndexPage = !!matchPath( const isLeavingRecordIndexPage = !!matchPath(

View File

@ -1,20 +1,20 @@
import { useApolloClient } from '@apollo/client';
import { MockedProvider } from '@apollo/client/testing';
import { expect } from '@storybook/test';
import { ReactNode, act } from 'react';
import { MemoryRouter } from 'react-router-dom';
import { RecoilRoot, useRecoilValue } from 'recoil';
import { useAuth } from '@/auth/hooks/useAuth'; import { useAuth } from '@/auth/hooks/useAuth';
import { billingState } from '@/client-config/states/billingState'; import { billingState } from '@/client-config/states/billingState';
import { isDebugModeState } from '@/client-config/states/isDebugModeState'; import { isDebugModeState } from '@/client-config/states/isDebugModeState';
import { isDeveloperDefaultSignInPrefilledState } from '@/client-config/states/isDeveloperDefaultSignInPrefilledState'; import { isDeveloperDefaultSignInPrefilledState } from '@/client-config/states/isDeveloperDefaultSignInPrefilledState';
import { supportChatState } from '@/client-config/states/supportChatState'; import { supportChatState } from '@/client-config/states/supportChatState';
import { workspaceAuthProvidersState } from '@/workspace/states/workspaceAuthProvidersState'; import { workspaceAuthProvidersState } from '@/workspace/states/workspaceAuthProvidersState';
import { useApolloClient } from '@apollo/client';
import { MockedProvider } from '@apollo/client/testing';
import { expect } from '@storybook/test';
import { ReactNode, act } from 'react';
import { MemoryRouter } from 'react-router-dom';
import { RecoilRoot, useRecoilValue } from 'recoil';
import { isMultiWorkspaceEnabledState } from '@/client-config/states/isMultiWorkspaceEnabledState'; import { isMultiWorkspaceEnabledState } from '@/client-config/states/isMultiWorkspaceEnabledState';
import { renderHook } from '@testing-library/react'; import { renderHook } from '@testing-library/react';
import { email, mocks, password, results, token } from '../__mocks__/useAuth';
import { iconsState } from 'twenty-ui/display'; import { iconsState } from 'twenty-ui/display';
import { email, mocks, password, results, token } from '../__mocks__/useAuth';
const redirectSpy = jest.fn(); const redirectSpy = jest.fn();
@ -24,6 +24,12 @@ jest.mock('@/domain-manager/hooks/useRedirect', () => ({
})), })),
})); }));
jest.mock('@/object-metadata/hooks/useRefreshObjectMetadataItem', () => ({
useRefreshObjectMetadataItems: jest.fn().mockImplementation(() => ({
refreshObjectMetadataItems: jest.fn(),
})),
}));
const Wrapper = ({ children }: { children: ReactNode }) => ( const Wrapper = ({ children }: { children: ReactNode }) => (
<MockedProvider mocks={mocks} addTypename={false}> <MockedProvider mocks={mocks} addTypename={false}>
<RecoilRoot> <RecoilRoot>

View File

@ -56,15 +56,15 @@ import { useLastAuthenticatedWorkspaceDomain } from '@/domain-manager/hooks/useL
import { useRedirect } from '@/domain-manager/hooks/useRedirect'; import { useRedirect } from '@/domain-manager/hooks/useRedirect';
import { useRedirectToWorkspaceDomain } from '@/domain-manager/hooks/useRedirectToWorkspaceDomain'; import { useRedirectToWorkspaceDomain } from '@/domain-manager/hooks/useRedirectToWorkspaceDomain';
import { domainConfigurationState } from '@/domain-manager/states/domainConfigurationState'; import { domainConfigurationState } from '@/domain-manager/states/domainConfigurationState';
import { isAppWaitingForFreshObjectMetadataState } from '@/object-metadata/states/isAppWaitingForFreshObjectMetadataState'; import { useRefreshObjectMetadataItems } from '@/object-metadata/hooks/useRefreshObjectMetadataItem';
import { workspaceAuthProvidersState } from '@/workspace/states/workspaceAuthProvidersState'; import { workspaceAuthProvidersState } from '@/workspace/states/workspaceAuthProvidersState';
import { i18n } from '@lingui/core'; import { i18n } from '@lingui/core';
import { useSearchParams } from 'react-router-dom'; import { useSearchParams } from 'react-router-dom';
import { APP_LOCALES } from 'twenty-shared/translations'; import { APP_LOCALES } from 'twenty-shared/translations';
import { isDefined } from 'twenty-shared/utils'; import { isDefined } from 'twenty-shared/utils';
import { iconsState } from 'twenty-ui/display';
import { getWorkspaceUrl } from '~/utils/getWorkspaceUrl'; import { getWorkspaceUrl } from '~/utils/getWorkspaceUrl';
import { dynamicActivate } from '~/utils/i18n/dynamicActivate'; import { dynamicActivate } from '~/utils/i18n/dynamicActivate';
import { iconsState } from 'twenty-ui/display';
export const useAuth = () => { export const useAuth = () => {
const setTokenPair = useSetRecoilState(tokenPairState); const setTokenPair = useSetRecoilState(tokenPairState);
@ -73,9 +73,7 @@ export const useAuth = () => {
currentWorkspaceMemberState, currentWorkspaceMemberState,
); );
const setCurrentUserWorkspace = useSetRecoilState(currentUserWorkspaceState); const setCurrentUserWorkspace = useSetRecoilState(currentUserWorkspaceState);
const setIsAppWaitingForFreshObjectMetadataState = useSetRecoilState(
isAppWaitingForFreshObjectMetadataState,
);
const setCurrentWorkspaceMembers = useSetRecoilState( const setCurrentWorkspaceMembers = useSetRecoilState(
currentWorkspaceMembersState, currentWorkspaceMembersState,
); );
@ -84,6 +82,8 @@ export const useAuth = () => {
isEmailVerificationRequiredState, isEmailVerificationRequiredState,
); );
const { refreshObjectMetadataItems } = useRefreshObjectMetadataItems();
const setSignInUpStep = useSetRecoilState(signInUpStepState); const setSignInUpStep = useSetRecoilState(signInUpStepState);
const setCurrentWorkspace = useSetRecoilState(currentWorkspaceState); const setCurrentWorkspace = useSetRecoilState(currentWorkspaceState);
const setWorkspaces = useSetRecoilState(workspacesState); const setWorkspaces = useSetRecoilState(workspacesState);
@ -312,7 +312,6 @@ export const useAuth = () => {
setWorkspaces(validWorkspaces); setWorkspaces(validWorkspaces);
} }
setIsAppWaitingForFreshObjectMetadataState(true);
return { return {
user, user,
@ -328,7 +327,6 @@ export const useAuth = () => {
setCurrentWorkspaceMember, setCurrentWorkspaceMember,
setCurrentWorkspaceMembers, setCurrentWorkspaceMembers,
setDateTimeFormat, setDateTimeFormat,
setIsAppWaitingForFreshObjectMetadataState,
setLastAuthenticateWorkspaceDomain, setLastAuthenticateWorkspaceDomain,
setWorkspaces, setWorkspaces,
]); ]);
@ -351,9 +349,15 @@ export const useAuth = () => {
getAuthTokensResult.data?.getAuthTokensFromLoginToken.tokens, getAuthTokensResult.data?.getAuthTokensFromLoginToken.tokens,
); );
await refreshObjectMetadataItems();
await loadCurrentUser(); await loadCurrentUser();
}, },
[getAuthTokensFromLoginToken, setTokenPair, loadCurrentUser], [
getAuthTokensFromLoginToken,
setTokenPair,
refreshObjectMetadataItems,
loadCurrentUser,
],
); );
const handleCredentialsSignIn = useCallback( const handleCredentialsSignIn = useCallback(

View File

@ -1,11 +1,9 @@
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar'; import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
import { useAuth } from '@/auth/hooks/useAuth'; import { useAuth } from '@/auth/hooks/useAuth';
import { isAppWaitingForFreshObjectMetadataState } from '@/object-metadata/states/isAppWaitingForFreshObjectMetadataState';
import { AppPath } from '@/types/AppPath'; import { AppPath } from '@/types/AppPath';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { useLingui } from '@lingui/react/macro'; import { useLingui } from '@lingui/react/macro';
import { useSetRecoilState } from 'recoil';
import { useNavigateApp } from '~/hooks/useNavigateApp'; import { useNavigateApp } from '~/hooks/useNavigateApp';
export const useVerifyLogin = () => { export const useVerifyLogin = () => {
@ -14,21 +12,14 @@ export const useVerifyLogin = () => {
const { getAuthTokensFromLoginToken } = useAuth(); const { getAuthTokensFromLoginToken } = useAuth();
const { t } = useLingui(); const { t } = useLingui();
const setIsAppWaitingForFreshObjectMetadata = useSetRecoilState(
isAppWaitingForFreshObjectMetadataState,
);
const verifyLoginToken = async (loginToken: string) => { const verifyLoginToken = async (loginToken: string) => {
try { try {
setIsAppWaitingForFreshObjectMetadata(true);
await getAuthTokensFromLoginToken(loginToken); await getAuthTokensFromLoginToken(loginToken);
} catch (error) { } catch (error) {
enqueueSnackBar(t`Authentication failed`, { enqueueSnackBar(t`Authentication failed`, {
variant: SnackBarVariant.Error, variant: SnackBarVariant.Error,
}); });
navigate(AppPath.SignInUp); navigate(AppPath.SignInUp);
} finally {
setIsAppWaitingForFreshObjectMetadata(false);
} }
}; };

View File

@ -11,12 +11,13 @@ import styled from '@emotion/styled';
import { Trans } from '@lingui/react/macro'; import { Trans } from '@lingui/react/macro';
import { FormProvider } from 'react-hook-form'; import { FormProvider } from 'react-hook-form';
import { useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
import { ActionLink } from 'twenty-ui/navigation';
import { HorizontalSeparator } from 'twenty-ui/display'; import { HorizontalSeparator } from 'twenty-ui/display';
import { ActionLink } from 'twenty-ui/navigation';
const StyledContentContainer = styled.div` const StyledContentContainer = styled.div`
margin-bottom: ${({ theme }) => theme.spacing(8)}; margin-bottom: ${({ theme }) => theme.spacing(8)};
margin-top: ${({ theme }) => theme.spacing(4)}; margin-top: ${({ theme }) => theme.spacing(4)};
width: 200px;
`; `;
export const SignInUpWorkspaceScopeForm = () => { export const SignInUpWorkspaceScopeForm = () => {

View File

@ -1,17 +1,16 @@
import { useState } from 'react'; import { useState } from 'react';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import { useRecoilValue, useSetRecoilState } from 'recoil'; import { useRecoilValue } from 'recoil';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar'; import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { isDefaultLayoutAuthModalVisibleState } from '@/ui/layout/states/isDefaultLayoutAuthModalVisibleState';
import { AppPath } from '@/types/AppPath'; import { AppPath } from '@/types/AppPath';
import { isDefined } from 'twenty-shared/utils';
import { useGetWorkspaceFromInviteHashQuery } from '~/generated/graphql'; import { useGetWorkspaceFromInviteHashQuery } from '~/generated/graphql';
import { useNavigateApp } from '~/hooks/useNavigateApp'; import { useNavigateApp } from '~/hooks/useNavigateApp';
import { isDefined } from 'twenty-shared/utils';
export const useWorkspaceFromInviteHash = () => { export const useWorkspaceFromInviteHash = () => {
const { enqueueSnackBar } = useSnackBar(); const { enqueueSnackBar } = useSnackBar();
@ -19,9 +18,7 @@ export const useWorkspaceFromInviteHash = () => {
const workspaceInviteHash = useParams().workspaceInviteHash; const workspaceInviteHash = useParams().workspaceInviteHash;
const currentWorkspace = useRecoilValue(currentWorkspaceState); const currentWorkspace = useRecoilValue(currentWorkspaceState);
const [initiallyLoggedIn] = useState(isDefined(currentWorkspace)); const [initiallyLoggedIn] = useState(isDefined(currentWorkspace));
const setIsDefaultLayoutAuthModalVisible = useSetRecoilState(
isDefaultLayoutAuthModalVisibleState,
);
const { data: workspaceFromInviteHash, loading } = const { data: workspaceFromInviteHash, loading } =
useGetWorkspaceFromInviteHashQuery({ useGetWorkspaceFromInviteHashQuery({
skip: !workspaceInviteHash, skip: !workspaceInviteHash,
@ -46,8 +43,6 @@ export const useWorkspaceFromInviteHash = () => {
}, },
); );
navigate(AppPath.Index); navigate(AppPath.Index);
} else {
setIsDefaultLayoutAuthModalVisible(true);
} }
}, },
}); });

View File

@ -4,6 +4,7 @@ import { useLastVisitedView } from '@/navigation/hooks/useLastVisitedView';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { prefetchIndexViewIdFromObjectMetadataItemFamilySelector } from '@/prefetch/states/selector/prefetchIndexViewIdFromObjectMetadataItemFamilySelector'; import { prefetchIndexViewIdFromObjectMetadataItemFamilySelector } from '@/prefetch/states/selector/prefetchIndexViewIdFromObjectMetadataItemFamilySelector';
import { AppPath } from '@/types/AppPath'; import { AppPath } from '@/types/AppPath';
import { useShowAuthModal } from '@/ui/layout/hooks/useShowAuthModal';
import { useParams, useSearchParams } from 'react-router-dom'; import { useParams, useSearchParams } from 'react-router-dom';
import { useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-shared/utils'; import { isDefined } from 'twenty-shared/utils';
@ -62,9 +63,10 @@ export const MainContextStoreProvider = () => {
); );
const viewId = getViewId(viewIdQueryParam, indexViewId, lastVisitedViewId); const viewId = getViewId(viewIdQueryParam, indexViewId, lastVisitedViewId);
const showAuthModal = useShowAuthModal();
const shouldComputeContextStore = const shouldComputeContextStore =
isRecordIndexPage || isRecordShowPage || isSettingsPage; (isRecordIndexPage || isRecordShowPage || isSettingsPage) && !showAuthModal;
if (!shouldComputeContextStore) { if (!shouldComputeContextStore) {
return null; return null;

View File

@ -61,13 +61,12 @@ export const MainContextStoreProviderEffect = ({
); );
useEffect(() => { useEffect(() => {
if (!objectMetadataItem) { if (contextStoreCurrentObjectMetadataItemId !== objectMetadataItem?.id) {
setContextStoreCurrentObjectMetadataItemId(undefined); setContextStoreCurrentObjectMetadataItemId(objectMetadataItem?.id);
return;
} }
if (contextStoreCurrentObjectMetadataItemId !== objectMetadataItem.id) { if (!objectMetadataItem) {
setContextStoreCurrentObjectMetadataItemId(objectMetadataItem.id); return;
} }
setLastVisitedViewForObjectMetadataNamePlural({ setLastVisitedViewForObjectMetadataNamePlural({

View File

@ -1,19 +0,0 @@
import React from 'react';
import { useRecoilValue } from 'recoil';
import { isAppWaitingForFreshObjectMetadataState } from '@/object-metadata/states/isAppWaitingForFreshObjectMetadataState';
import { UserOrMetadataLoader } from '~/loading/components/UserOrMetadataLoader';
export const ObjectMetadataItemsGater = ({
children,
}: React.PropsWithChildren) => {
const isAppWaitingForFreshObjectMetadata = useRecoilValue(
isAppWaitingForFreshObjectMetadataState,
);
const shouldDisplayChildren = !isAppWaitingForFreshObjectMetadata;
return (
<>{shouldDisplayChildren ? <>{children}</> : <UserOrMetadataLoader />}</>
);
};

View File

@ -1,8 +1,8 @@
import React from 'react'; import React from 'react';
import { useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
import { ObjectMetadataItemsLoadEffect } from '@/object-metadata/components/ObjectMetadataItemsLoadEffect';
import { PreComputedChipGeneratorsProvider } from '@/object-metadata/components/PreComputedChipGeneratorsProvider'; import { PreComputedChipGeneratorsProvider } from '@/object-metadata/components/PreComputedChipGeneratorsProvider';
import { isAppWaitingForFreshObjectMetadataState } from '@/object-metadata/states/isAppWaitingForFreshObjectMetadataState';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { UserOrMetadataLoader } from '~/loading/components/UserOrMetadataLoader'; import { UserOrMetadataLoader } from '~/loading/components/UserOrMetadataLoader';
@ -10,11 +10,16 @@ export const ObjectMetadataItemsProvider = ({
children, children,
}: React.PropsWithChildren) => { }: React.PropsWithChildren) => {
const objectMetadataItems = useRecoilValue(objectMetadataItemsState); const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
const shouldDisplayChildren = objectMetadataItems.length > 0;
const isAppWaitingForFreshObjectMetadata = useRecoilValue(
isAppWaitingForFreshObjectMetadataState,
);
const shouldDisplayChildren =
!isAppWaitingForFreshObjectMetadata && objectMetadataItems.length > 0;
return ( return (
<> <>
<ObjectMetadataItemsLoadEffect />
{shouldDisplayChildren ? ( {shouldDisplayChildren ? (
<PreComputedChipGeneratorsProvider> <PreComputedChipGeneratorsProvider>
{children} {children}

View File

@ -39,7 +39,8 @@ export const useRefreshObjectMetadataItems = (
!isDeeplyEqual( !isDeeplyEqual(
snapshot.getLoadable(objectMetadataItemsState).getValue(), snapshot.getLoadable(objectMetadataItemsState).getValue(),
toSetObjectMetadataItems, toSetObjectMetadataItems,
) ) &&
toSetObjectMetadataItems.length > 0
) { ) {
set(objectMetadataItemsState, toSetObjectMetadataItems); set(objectMetadataItemsState, toSetObjectMetadataItems);
set(isAppWaitingForFreshObjectMetadataState, false); set(isAppWaitingForFreshObjectMetadataState, false);

View File

@ -433,6 +433,17 @@ const HookMockWrapper = getJestMetadataAndApolloMocksWrapper({
apolloMocks: mocks, apolloMocks: mocks,
}); });
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useLocation: jest.fn().mockReturnValue({
pathname: '/',
search: '',
hash: '',
state: null,
key: 'default',
}),
}));
const Wrapper = ({ children }: { children: ReactNode }) => { const Wrapper = ({ children }: { children: ReactNode }) => {
return ( return (
<HookMockWrapper> <HookMockWrapper>

View File

@ -1,4 +1,6 @@
import { RecordIndexActionMenu } from '@/action-menu/components/RecordIndexActionMenu'; import { RecordIndexActionMenu } from '@/action-menu/components/RecordIndexActionMenu';
import { MAIN_CONTEXT_STORE_INSTANCE_ID } from '@/context-store/constants/MainContextStoreInstanceId';
import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState';
import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState'; import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState';
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems'; import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext'; import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
@ -7,7 +9,7 @@ import { PageHeader } from '@/ui/layout/page/components/PageHeader';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { t } from '@lingui/core/macro'; import { t } from '@lingui/core/macro';
import { capitalize } from 'twenty-shared/utils'; import { capitalize, isDefined } from 'twenty-shared/utils';
import { useIcons } from 'twenty-ui/display'; import { useIcons } from 'twenty-ui/display';
const StyledTitleWithSelectedRecords = styled.div` const StyledTitleWithSelectedRecords = styled.div`
@ -59,10 +61,19 @@ export const RecordIndexPageHeader = () => {
label label
); );
const contextStoreCurrentViewId = useRecoilComponentValueV2(
contextStoreCurrentViewIdComponentState,
MAIN_CONTEXT_STORE_INSTANCE_ID,
);
return ( return (
<PageHeader title={pageHeaderTitle} Icon={Icon}> <PageHeader title={pageHeaderTitle} Icon={Icon}>
<RecordIndexActionMenu indexId={recordIndexId} /> {isDefined(contextStoreCurrentViewId) && (
<PageHeaderToggleCommandMenuButton /> <>
<RecordIndexActionMenu indexId={recordIndexId} />
<PageHeaderToggleCommandMenuButton />
</>
)}
</PageHeader> </PageHeader>
); );
}; };

View File

@ -1,15 +1,14 @@
import { useRecoilValue } from 'recoil';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useLazyFindManyRecords } from '@/object-record/hooks/useLazyFindManyRecords'; import { useLazyFindManyRecords } from '@/object-record/hooks/useLazyFindManyRecords';
import { useFindManyRecordIndexTableParams } from '@/object-record/record-index/hooks/useFindManyRecordIndexTableParams'; import { useFindManyRecordIndexTableParams } from '@/object-record/record-index/hooks/useFindManyRecordIndexTableParams';
import { useRecordTableRecordGqlFields } from '@/object-record/record-index/hooks/useRecordTableRecordGqlFields'; import { useRecordTableRecordGqlFields } from '@/object-record/record-index/hooks/useRecordTableRecordGqlFields';
import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable'; import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
import { SIGN_IN_BACKGROUND_MOCK_COMPANIES } from '@/sign-in-background-mock/constants/SignInBackgroundMockCompanies'; import { SIGN_IN_BACKGROUND_MOCK_COMPANIES } from '@/sign-in-background-mock/constants/SignInBackgroundMockCompanies';
import { isWorkspaceActiveOrSuspended } from 'twenty-shared/workspace'; import { useShowAuthModal } from '@/ui/layout/hooks/useShowAuthModal';
export const useLazyLoadRecordIndexTable = (objectNameSingular: string) => { export const useLazyLoadRecordIndexTable = (objectNameSingular: string) => {
const showAuthModal = useShowAuthModal();
const { objectMetadataItem } = useObjectMetadataItem({ const { objectMetadataItem } = useObjectMetadataItem({
objectNameSingular, objectNameSingular,
}); });
@ -17,8 +16,6 @@ export const useLazyLoadRecordIndexTable = (objectNameSingular: string) => {
const { setRecordTableData, setIsRecordTableInitialLoading } = const { setRecordTableData, setIsRecordTableInitialLoading } =
useRecordTable(); useRecordTable();
const currentWorkspace = useRecoilValue(currentWorkspaceState);
const params = useFindManyRecordIndexTableParams(objectNameSingular); const params = useFindManyRecordIndexTableParams(objectNameSingular);
const recordGqlFields = useRecordTableRecordGqlFields({ objectMetadataItem }); const recordGqlFields = useRecordTableRecordGqlFields({ objectMetadataItem });
@ -44,9 +41,7 @@ export const useLazyLoadRecordIndexTable = (objectNameSingular: string) => {
return { return {
findManyRecords, findManyRecords,
records: isWorkspaceActiveOrSuspended(currentWorkspace) records: !showAuthModal ? records : SIGN_IN_BACKGROUND_MOCK_COMPANIES,
? records
: SIGN_IN_BACKGROUND_MOCK_COMPANIES,
totalCount: totalCount, totalCount: totalCount,
loading, loading,
fetchMoreRecords, fetchMoreRecords,

View File

@ -2,25 +2,28 @@ import { useEffect, useState } from 'react';
import { useRecoilState, useRecoilValue } from 'recoil'; import { useRecoilState, useRecoilValue } from 'recoil';
import { useDebouncedCallback } from 'use-debounce'; import { useDebouncedCallback } from 'use-debounce';
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { lastShowPageRecordIdState } from '@/object-record/record-field/states/lastShowPageRecordId'; import { lastShowPageRecordIdState } from '@/object-record/record-field/states/lastShowPageRecordId';
import { useLazyLoadRecordIndexTable } from '@/object-record/record-index/hooks/useLazyLoadRecordIndexTable'; import { useLazyLoadRecordIndexTable } from '@/object-record/record-index/hooks/useLazyLoadRecordIndexTable';
import { ROW_HEIGHT } from '@/object-record/record-table/constants/RowHeight'; import { ROW_HEIGHT } from '@/object-record/record-table/constants/RowHeight';
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext'; import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
import { hasRecordTableFetchedAllRecordsComponentStateV2 } from '@/object-record/record-table/states/hasRecordTableFetchedAllRecordsComponentStateV2'; import { hasRecordTableFetchedAllRecordsComponentStateV2 } from '@/object-record/record-table/states/hasRecordTableFetchedAllRecordsComponentStateV2';
import { tableEncounteredUnrecoverableErrorComponentState } from '@/object-record/record-table/states/tableEncounteredUnrecoverableErrorComponentState'; import { tableEncounteredUnrecoverableErrorComponentState } from '@/object-record/record-table/states/tableEncounteredUnrecoverableErrorComponentState';
import { tableLastRowVisibleComponentState } from '@/object-record/record-table/states/tableLastRowVisibleComponentState'; import { tableLastRowVisibleComponentState } from '@/object-record/record-table/states/tableLastRowVisibleComponentState';
import { isFetchingMoreRecordsFamilyState } from '@/object-record/states/isFetchingMoreRecordsFamilyState'; import { isFetchingMoreRecordsFamilyState } from '@/object-record/states/isFetchingMoreRecordsFamilyState';
import { useShowAuthModal } from '@/ui/layout/hooks/useShowAuthModal';
import { useScrollToPosition } from '@/ui/utilities/scroll/hooks/useScrollToPosition'; import { useScrollToPosition } from '@/ui/utilities/scroll/hooks/useScrollToPosition';
import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2'; import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2'; import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
import { isNonEmptyString, isNull } from '@sniptt/guards'; import { isNonEmptyString } from '@sniptt/guards';
export const RecordTableNoRecordGroupBodyEffect = () => { export const RecordTableNoRecordGroupBodyEffect = () => {
const { objectNameSingular } = useRecordTableContextOrThrow(); const { objectNameSingular } = useRecordTableContextOrThrow();
const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState); const { setIsRecordTableInitialLoading } = useRecordTable();
const showAuthModal = useShowAuthModal();
const [hasInitializedScroll, setHasInitializedScroll] = useState(false); const [hasInitializedScroll, setHasInitializedScroll] = useState(false);
@ -139,7 +142,8 @@ export const RecordTableNoRecordGroupBodyEffect = () => {
]); ]);
useEffect(() => { useEffect(() => {
if (isNull(currentWorkspaceMember)) { if (showAuthModal) {
setIsRecordTableInitialLoading(false);
return; return;
} }
@ -147,7 +151,12 @@ export const RecordTableNoRecordGroupBodyEffect = () => {
findManyRecords(); findManyRecords();
setHasInitialized(true); setHasInitialized(true);
} }
}, [currentWorkspaceMember, findManyRecords, hasInitialized]); }, [
findManyRecords,
hasInitialized,
setIsRecordTableInitialLoading,
showAuthModal,
]);
return <></>; return <></>;
}; };

View File

@ -1,21 +1,22 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useRecoilState, useRecoilValue } from 'recoil'; import { useRecoilState } from 'recoil';
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { lastShowPageRecordIdState } from '@/object-record/record-field/states/lastShowPageRecordId'; import { lastShowPageRecordIdState } from '@/object-record/record-field/states/lastShowPageRecordId';
import { useCurrentRecordGroupId } from '@/object-record/record-group/hooks/useCurrentRecordGroupId'; import { useCurrentRecordGroupId } from '@/object-record/record-group/hooks/useCurrentRecordGroupId';
import { useLazyLoadRecordIndexTable } from '@/object-record/record-index/hooks/useLazyLoadRecordIndexTable'; import { useLazyLoadRecordIndexTable } from '@/object-record/record-index/hooks/useLazyLoadRecordIndexTable';
import { recordIndexHasFetchedAllRecordsByGroupComponentState } from '@/object-record/record-index/states/recordIndexHasFetchedAllRecordsByGroupComponentState'; import { recordIndexHasFetchedAllRecordsByGroupComponentState } from '@/object-record/record-index/states/recordIndexHasFetchedAllRecordsByGroupComponentState';
import { ROW_HEIGHT } from '@/object-record/record-table/constants/RowHeight'; import { ROW_HEIGHT } from '@/object-record/record-table/constants/RowHeight';
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext'; import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { useOnboardingStatus } from '@/onboarding/hooks/useOnboardingStatus';
import { useScrollToPosition } from '@/ui/utilities/scroll/hooks/useScrollToPosition'; import { useScrollToPosition } from '@/ui/utilities/scroll/hooks/useScrollToPosition';
import { useSetRecoilComponentFamilyStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentFamilyStateV2'; import { useSetRecoilComponentFamilyStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentFamilyStateV2';
import { isNonEmptyString, isNull } from '@sniptt/guards'; import { isNonEmptyString } from '@sniptt/guards';
import { OnboardingStatus } from '~/generated-metadata/graphql';
export const RecordTableRecordGroupBodyEffect = () => { export const RecordTableRecordGroupBodyEffect = () => {
const { objectNameSingular } = useRecordTableContextOrThrow(); const { objectNameSingular } = useRecordTableContextOrThrow();
const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState); const onboardingStatus = useOnboardingStatus();
const recordGroupId = useCurrentRecordGroupId(); const recordGroupId = useCurrentRecordGroupId();
@ -79,12 +80,12 @@ export const RecordTableRecordGroupBodyEffect = () => {
}, [hasNextPage, setHasRecordFetchedAllRecordsComponents]); }, [hasNextPage, setHasRecordFetchedAllRecordsComponents]);
useEffect(() => { useEffect(() => {
if (isNull(currentWorkspaceMember)) { if (onboardingStatus !== OnboardingStatus.COMPLETED) {
return; return;
} }
findManyRecords(); findManyRecords();
}, [currentWorkspaceMember, findManyRecords]); }, [onboardingStatus, findManyRecords]);
return <></>; return <></>;
}; };

View File

@ -2,7 +2,6 @@ import { ReactElement, useContext } from 'react';
import { RecordTableCellContext } from '@/object-record/record-table/contexts/RecordTableCellContext'; import { RecordTableCellContext } from '@/object-record/record-table/contexts/RecordTableCellContext';
import { RecordTableCellBaseContainer } from '@/object-record/record-table/record-table-cell/components/RecordTableCellBaseContainer'; import { RecordTableCellBaseContainer } from '@/object-record/record-table/record-table-cell/components/RecordTableCellBaseContainer';
import { RecordTableCellSoftFocusMode } from '@/object-record/record-table/record-table-cell/components/RecordTableCellSoftFocusMode';
import { RecordTableCellSoftFocusModeHotkeysSetterEffect } from '@/object-record/record-table/record-table-cell/components/RecordTableCellSoftFocusModeHotkeysSetterEffect'; import { RecordTableCellSoftFocusModeHotkeysSetterEffect } from '@/object-record/record-table/record-table-cell/components/RecordTableCellSoftFocusModeHotkeysSetterEffect';
import { RecordTableCellDisplayMode } from './RecordTableCellDisplayMode'; import { RecordTableCellDisplayMode } from './RecordTableCellDisplayMode';
@ -27,19 +26,14 @@ export const RecordTableCellContainer = ({
<RecordTableCellBaseContainer> <RecordTableCellBaseContainer>
{isInEditMode ? ( {isInEditMode ? (
<RecordTableCellEditMode>{editModeContent}</RecordTableCellEditMode> <RecordTableCellEditMode>{editModeContent}</RecordTableCellEditMode>
) : hasSoftFocus ? (
<>
<RecordTableCellSoftFocusModeHotkeysSetterEffect />
<RecordTableCellSoftFocusMode
editModeContent={editModeContent}
nonEditModeContent={nonEditModeContent}
/>
</>
) : ( ) : (
<RecordTableCellDisplayMode> <RecordTableCellDisplayMode>
{nonEditModeContent} {nonEditModeContent}
</RecordTableCellDisplayMode> </RecordTableCellDisplayMode>
)} )}
{hasSoftFocus ? (
<RecordTableCellSoftFocusModeHotkeysSetterEffect />
) : null}
</RecordTableCellBaseContainer> </RecordTableCellBaseContainer>
); );
}; };

View File

@ -41,11 +41,11 @@ export const RecordTableCellFieldContextWrapper = ({
return ( return (
<RecordFieldComponentInstanceContext.Provider value={{ instanceId }}> <RecordFieldComponentInstanceContext.Provider value={{ instanceId }}>
{isLabelIdentifier ? ( {isLabelIdentifier ? (
<RecordTableCellFieldContextLabelIdentifier> <RecordTableCellFieldContextLabelIdentifier key={instanceId}>
{children} {children}
</RecordTableCellFieldContextLabelIdentifier> </RecordTableCellFieldContextLabelIdentifier>
) : ( ) : (
<RecordTableCellFieldContextGeneric> <RecordTableCellFieldContextGeneric key={instanceId}>
{children} {children}
</RecordTableCellFieldContextGeneric> </RecordTableCellFieldContextGeneric>
)} )}

View File

@ -1,117 +0,0 @@
import { ReactElement, useContext, useEffect, useRef } from 'react';
import { useRecoilValue } from 'recoil';
import { useGetButtonIcon } from '@/object-record/record-field/hooks/useGetButtonIcon';
import { useIsFieldEmpty } from '@/object-record/record-field/hooks/useIsFieldEmpty';
import { useIsFieldInputOnly } from '@/object-record/record-field/hooks/useIsFieldInputOnly';
import { RecordTableCellContext } from '@/object-record/record-table/contexts/RecordTableCellContext';
import { RecordTableCellButton } from '@/object-record/record-table/record-table-cell/components/RecordTableCellButton';
import { useOpenRecordTableCellFromCell } from '@/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellFromCell';
import { isSoftFocusUsingMouseState } from '@/object-record/record-table/states/isSoftFocusUsingMouseState';
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
import { useRecordTableBodyContextOrThrow } from '@/object-record/record-table/contexts/RecordTableBodyContext';
import { isDefined } from 'twenty-shared/utils';
import { IconArrowUpRight } from 'twenty-ui/display';
import { useIsMobile } from 'twenty-ui/utilities';
import { RecordTableCellDisplayContainer } from './RecordTableCellDisplayContainer';
type RecordTableCellSoftFocusModeProps = {
editModeContent: ReactElement;
nonEditModeContent: ReactElement;
};
export const RecordTableCellSoftFocusMode = ({
editModeContent,
nonEditModeContent,
}: RecordTableCellSoftFocusModeProps) => {
const { columnIndex, columnDefinition } = useContext(RecordTableCellContext);
const { recordId, isReadOnly } = useContext(FieldContext);
const { onActionMenuDropdownOpened } = useRecordTableBodyContextOrThrow();
const { openTableCell } = useOpenRecordTableCellFromCell();
const editModeContentOnly = useIsFieldInputOnly();
const isFieldInputOnly = useIsFieldInputOnly();
const isEmpty = useIsFieldEmpty();
const scrollRef = useRef<HTMLDivElement>(null);
const isSoftFocusUsingMouse = useRecoilValue(isSoftFocusUsingMouseState);
useEffect(() => {
if (!isSoftFocusUsingMouse) {
scrollRef.current?.scrollIntoView({ block: 'nearest' });
}
}, [isSoftFocusUsingMouse]);
const handleClick = () => {
if (!isFieldInputOnly && !isReadOnly) {
openTableCell();
}
};
const handleButtonClick = () => {
if (!isFieldInputOnly && isFirstColumn) {
openTableCell(undefined, false, true);
} else {
openTableCell();
}
/*
Disabling sidepanel access for now, TODO: launch
if (!isFieldInputOnly) {
openTableCell(undefined, true);
}
*/
};
const handleActionMenuDropdown = (event: React.MouseEvent) => {
onActionMenuDropdownOpened(event, recordId);
};
const isFirstColumn = columnIndex === 0;
const customButtonIcon = useGetButtonIcon();
const buttonIcon = isFirstColumn
? IconArrowUpRight // IconLayoutSidebarRightExpand - Disabling sidepanel access for now
: customButtonIcon;
const isMobile = useIsMobile();
const showButton =
isDefined(buttonIcon) &&
!editModeContentOnly &&
!isReadOnly &&
!(isMobile && isFirstColumn);
const dontShowContent = isEmpty && isReadOnly;
const showPlaceholder =
!editModeContentOnly && !isReadOnly && isFirstColumn && isEmpty;
return (
<>
<RecordTableCellDisplayContainer
onClick={handleClick}
scrollRef={scrollRef}
softFocus
onContextMenu={handleActionMenuDropdown}
placeholderForEmptyCell={
showPlaceholder ? columnDefinition.label : undefined
}
>
{dontShowContent ? (
<></>
) : editModeContentOnly ? (
editModeContent
) : (
nonEditModeContent
)}
</RecordTableCellDisplayContainer>
{showButton && (
<RecordTableCellButton onClick={handleButtonClick} Icon={buttonIcon} />
)}
</>
);
};

View File

@ -1,7 +1,6 @@
import { useEffect } from 'react'; import { useEffect } from 'react';
import { useRecoilCallback, useRecoilValue, useSetRecoilState } from 'recoil'; import { useRecoilCallback, useSetRecoilState } from 'recoil';
import { currentUserState } from '@/auth/states/currentUserState';
import { Favorite } from '@/favorites/types/Favorite'; import { Favorite } from '@/favorites/types/Favorite';
import { FavoriteFolder } from '@/favorites/types/FavoriteFolder'; import { FavoriteFolder } from '@/favorites/types/FavoriteFolder';
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems'; import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
@ -13,17 +12,12 @@ import { prefetchFavoriteFoldersState } from '@/prefetch/states/prefetchFavorite
import { prefetchFavoritesState } from '@/prefetch/states/prefetchFavoritesState'; import { prefetchFavoritesState } from '@/prefetch/states/prefetchFavoritesState';
import { prefetchIsLoadedFamilyState } from '@/prefetch/states/prefetchIsLoadedFamilyState'; import { prefetchIsLoadedFamilyState } from '@/prefetch/states/prefetchIsLoadedFamilyState';
import { PrefetchKey } from '@/prefetch/types/PrefetchKey'; import { PrefetchKey } from '@/prefetch/types/PrefetchKey';
import { useIsWorkspaceActivationStatusEqualsTo } from '@/workspace/hooks/useIsWorkspaceActivationStatusEqualsTo'; import { useShowAuthModal } from '@/ui/layout/hooks/useShowAuthModal';
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
import { WorkspaceActivationStatus } from 'twenty-shared/workspace';
import { isDefined } from 'twenty-shared/utils'; import { isDefined } from 'twenty-shared/utils';
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
export const PrefetchRunFavoriteQueriesEffect = () => { export const PrefetchRunFavoriteQueriesEffect = () => {
const currentUser = useRecoilValue(currentUserState); const showAuthModal = useShowAuthModal();
const isWorkspaceActive = useIsWorkspaceActivationStatusEqualsTo(
WorkspaceActivationStatus.ACTIVE,
);
const { objectMetadataItems } = useObjectMetadataItems(); const { objectMetadataItems } = useObjectMetadataItems();
@ -53,14 +47,14 @@ export const PrefetchRunFavoriteQueriesEffect = () => {
objectNameSingular: CoreObjectNameSingular.Favorite, objectNameSingular: CoreObjectNameSingular.Favorite,
filter: findAllFavoritesOperationSignature.variables.filter, filter: findAllFavoritesOperationSignature.variables.filter,
recordGqlFields: findAllFavoritesOperationSignature.fields, recordGqlFields: findAllFavoritesOperationSignature.fields,
skip: !currentUser || !isWorkspaceActive, skip: showAuthModal,
}); });
const { records: favoriteFolders } = useFindManyRecords({ const { records: favoriteFolders } = useFindManyRecords({
objectNameSingular: CoreObjectNameSingular.FavoriteFolder, objectNameSingular: CoreObjectNameSingular.FavoriteFolder,
filter: findAllFavoriteFoldersOperationSignature.variables.filter, filter: findAllFavoriteFoldersOperationSignature.variables.filter,
recordGqlFields: findAllFavoriteFoldersOperationSignature.fields, recordGqlFields: findAllFavoriteFoldersOperationSignature.fields,
skip: !currentUser || !isWorkspaceActive, skip: showAuthModal,
}); });
const setPrefetchFavoritesState = useRecoilCallback( const setPrefetchFavoritesState = useRecoilCallback(

View File

@ -8,7 +8,7 @@ import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTabl
import { useSetTableColumns } from '@/object-record/record-table/hooks/useSetTableColumns'; import { useSetTableColumns } from '@/object-record/record-table/hooks/useSetTableColumns';
import { SIGN_IN_BACKGROUND_MOCK_COLUMN_DEFINITIONS } from '@/sign-in-background-mock/constants/SignInBackgroundMockColumnDefinitions'; import { SIGN_IN_BACKGROUND_MOCK_COLUMN_DEFINITIONS } from '@/sign-in-background-mock/constants/SignInBackgroundMockColumnDefinitions';
import { SIGN_IN_BACKGROUND_MOCK_VIEW_FIELDS } from '@/sign-in-background-mock/constants/SignInBackgroundMockViewFields'; import { SIGN_IN_BACKGROUND_MOCK_VIEW_FIELDS } from '@/sign-in-background-mock/constants/SignInBackgroundMockViewFields';
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2'; import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2';
import { useInitViewBar } from '@/views/hooks/useInitViewBar'; import { useInitViewBar } from '@/views/hooks/useInitViewBar';
import { mapViewFieldsToColumnDefinitions } from '@/views/utils/mapViewFieldsToColumnDefinitions'; import { mapViewFieldsToColumnDefinitions } from '@/views/utils/mapViewFieldsToColumnDefinitions';
@ -23,11 +23,13 @@ export const SignInBackgroundMockContainerEffect = ({
recordTableId, recordTableId,
viewId, viewId,
}: SignInBackgroundMockContainerEffectProps) => { }: SignInBackgroundMockContainerEffectProps) => {
const setContextStoreCurrentObjectMetadataItemId = const [
useSetRecoilComponentStateV2( contextStoreCurrentObjectMetadataItemId,
contextStoreCurrentObjectMetadataItemIdComponentState, setContextStoreCurrentObjectMetadataItemId,
MAIN_CONTEXT_STORE_INSTANCE_ID, ] = useRecoilComponentStateV2(
); contextStoreCurrentObjectMetadataItemIdComponentState,
MAIN_CONTEXT_STORE_INSTANCE_ID,
);
const { setAvailableTableColumns } = useRecordTable({ const { setAvailableTableColumns } = useRecordTable({
recordTableId, recordTableId,
@ -61,7 +63,9 @@ export const SignInBackgroundMockContainerEffect = ({
recordTableId, recordTableId,
); );
setContextStoreCurrentObjectMetadataItemId(objectMetadataItem.id); if (contextStoreCurrentObjectMetadataItemId !== objectMetadataItem.id) {
setContextStoreCurrentObjectMetadataItemId(objectMetadataItem.id);
}
}, [ }, [
setViewObjectMetadataId, setViewObjectMetadataId,
setAvailableFieldDefinitions, setAvailableFieldDefinitions,
@ -70,6 +74,7 @@ export const SignInBackgroundMockContainerEffect = ({
setTableColumns, setTableColumns,
recordTableId, recordTableId,
setContextStoreCurrentObjectMetadataItemId, setContextStoreCurrentObjectMetadataItemId,
contextStoreCurrentObjectMetadataItemId,
]); ]);
return <></>; return <></>;

View File

@ -4,8 +4,6 @@ import React from 'react';
const StyledInputErrorHelper = styled.div` const StyledInputErrorHelper = styled.div`
color: ${({ theme }) => theme.color.red}; color: ${({ theme }) => theme.color.red};
font-size: ${({ theme }) => theme.font.size.xs}; font-size: ${({ theme }) => theme.font.size.xs};
position: absolute;
margin-top: ${({ theme }) => theme.spacing(0.25)};
`; `;
export const InputErrorHelper = ({ export const InputErrorHelper = ({

View File

@ -1,11 +1,10 @@
import { renderHook } from '@testing-library/react'; import { renderHook } from '@testing-library/react';
import { RecoilRoot, useSetRecoilState } from 'recoil'; import { RecoilRoot } from 'recoil';
import { useIsLogged } from '@/auth/hooks/useIsLogged'; import { useIsLogged } from '@/auth/hooks/useIsLogged';
import { useOnboardingStatus } from '@/onboarding/hooks/useOnboardingStatus'; import { useOnboardingStatus } from '@/onboarding/hooks/useOnboardingStatus';
import { AppPath } from '@/types/AppPath'; import { AppPath } from '@/types/AppPath';
import { useShowAuthModal } from '@/ui/layout/hooks/useShowAuthModal'; import { useShowAuthModal } from '@/ui/layout/hooks/useShowAuthModal';
import { isDefaultLayoutAuthModalVisibleState } from '@/ui/layout/states/isDefaultLayoutAuthModalVisibleState';
import { useSubscriptionStatus } from '@/workspace/hooks/useSubscriptionStatus'; import { useSubscriptionStatus } from '@/workspace/hooks/useSubscriptionStatus';
import { OnboardingStatus, SubscriptionStatus } from '~/generated/graphql'; import { OnboardingStatus, SubscriptionStatus } from '~/generated/graphql';
import { useIsMatchingLocation } from '~/hooks/useIsMatchingLocation'; import { useIsMatchingLocation } from '~/hooks/useIsMatchingLocation';
@ -39,14 +38,9 @@ const setupMockIsMatchingLocation = (pathname: string) => {
}); });
}; };
const getResult = (isDefaultLayoutAuthModalVisible = true) => const getResult = () =>
renderHook( renderHook(
() => { () => {
const setIsDefaultLayoutAuthModalVisible = useSetRecoilState(
isDefaultLayoutAuthModalVisibleState,
);
setIsDefaultLayoutAuthModalVisible(isDefaultLayoutAuthModalVisible);
return useShowAuthModal(); return useShowAuthModal();
}, },
{ {
@ -112,15 +106,15 @@ const testCases = [
{ loc: AppPath.ResetPassword, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.COMPLETED, res: true }, { loc: AppPath.ResetPassword, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.COMPLETED, res: true },
{ loc: AppPath.CreateWorkspace, isLogged: true, subscriptionStatus: undefined, onboardingStatus: OnboardingStatus.PLAN_REQUIRED, res: true }, { loc: AppPath.CreateWorkspace, isLogged: true, subscriptionStatus: undefined, onboardingStatus: OnboardingStatus.PLAN_REQUIRED, res: true },
{ loc: AppPath.CreateWorkspace, isLogged: true, subscriptionStatus: SubscriptionStatus.Canceled, onboardingStatus: OnboardingStatus.COMPLETED, res: false }, { loc: AppPath.CreateWorkspace, isLogged: true, subscriptionStatus: SubscriptionStatus.Canceled, onboardingStatus: OnboardingStatus.COMPLETED, res: true },
{ loc: AppPath.CreateWorkspace, isLogged: true, subscriptionStatus: SubscriptionStatus.Unpaid, onboardingStatus: OnboardingStatus.COMPLETED, res: false }, { loc: AppPath.CreateWorkspace, isLogged: true, subscriptionStatus: SubscriptionStatus.Unpaid, onboardingStatus: OnboardingStatus.COMPLETED, res: true },
{ loc: AppPath.CreateWorkspace, isLogged: true, subscriptionStatus: SubscriptionStatus.PastDue, onboardingStatus: OnboardingStatus.COMPLETED, res: false }, { loc: AppPath.CreateWorkspace, isLogged: true, subscriptionStatus: SubscriptionStatus.PastDue, onboardingStatus: OnboardingStatus.COMPLETED, res: true },
{ loc: AppPath.CreateWorkspace, isLogged: false, subscriptionStatus: undefined, onboardingStatus: undefined, res: true }, { loc: AppPath.CreateWorkspace, isLogged: false, subscriptionStatus: undefined, onboardingStatus: undefined, res: true },
{ loc: AppPath.CreateWorkspace, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.WORKSPACE_ACTIVATION, res: true }, { loc: AppPath.CreateWorkspace, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.WORKSPACE_ACTIVATION, res: true },
{ loc: AppPath.CreateWorkspace, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.PROFILE_CREATION, res: true }, { loc: AppPath.CreateWorkspace, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.PROFILE_CREATION, res: true },
{ loc: AppPath.CreateWorkspace, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.SYNC_EMAIL, res: true }, { loc: AppPath.CreateWorkspace, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.SYNC_EMAIL, res: true },
{ loc: AppPath.CreateWorkspace, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.INVITE_TEAM, res: true }, { loc: AppPath.CreateWorkspace, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.INVITE_TEAM, res: true },
{ loc: AppPath.CreateWorkspace, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.COMPLETED, res: false }, { loc: AppPath.CreateWorkspace, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.COMPLETED, res: true },
{ loc: AppPath.CreateProfile, isLogged: true, subscriptionStatus: undefined, onboardingStatus: OnboardingStatus.PLAN_REQUIRED, res: true }, { loc: AppPath.CreateProfile, isLogged: true, subscriptionStatus: undefined, onboardingStatus: OnboardingStatus.PLAN_REQUIRED, res: true },
{ loc: AppPath.CreateProfile, isLogged: true, subscriptionStatus: SubscriptionStatus.Canceled, onboardingStatus: OnboardingStatus.COMPLETED, res: false }, { loc: AppPath.CreateProfile, isLogged: true, subscriptionStatus: SubscriptionStatus.Canceled, onboardingStatus: OnboardingStatus.COMPLETED, res: false },
@ -157,14 +151,14 @@ const testCases = [
{ loc: AppPath.PlanRequired, isLogged: true, subscriptionStatus: undefined, onboardingStatus: OnboardingStatus.PLAN_REQUIRED, res: true }, { loc: AppPath.PlanRequired, isLogged: true, subscriptionStatus: undefined, onboardingStatus: OnboardingStatus.PLAN_REQUIRED, res: true },
{ loc: AppPath.PlanRequired, isLogged: true, subscriptionStatus: SubscriptionStatus.Canceled, onboardingStatus: OnboardingStatus.COMPLETED, res: true }, { loc: AppPath.PlanRequired, isLogged: true, subscriptionStatus: SubscriptionStatus.Canceled, onboardingStatus: OnboardingStatus.COMPLETED, res: true },
{ loc: AppPath.PlanRequired, isLogged: true, subscriptionStatus: SubscriptionStatus.Unpaid, onboardingStatus: OnboardingStatus.COMPLETED, res: false }, { loc: AppPath.PlanRequired, isLogged: true, subscriptionStatus: SubscriptionStatus.Unpaid, onboardingStatus: OnboardingStatus.COMPLETED, res: true },
{ loc: AppPath.PlanRequired, isLogged: true, subscriptionStatus: SubscriptionStatus.PastDue, onboardingStatus: OnboardingStatus.COMPLETED, res: false }, { loc: AppPath.PlanRequired, isLogged: true, subscriptionStatus: SubscriptionStatus.PastDue, onboardingStatus: OnboardingStatus.COMPLETED, res: true },
{ loc: AppPath.PlanRequired, isLogged: false, subscriptionStatus: undefined, onboardingStatus: undefined, res: true }, { loc: AppPath.PlanRequired, isLogged: false, subscriptionStatus: undefined, onboardingStatus: undefined, res: true },
{ loc: AppPath.PlanRequired, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.WORKSPACE_ACTIVATION, res: true }, { loc: AppPath.PlanRequired, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.WORKSPACE_ACTIVATION, res: true },
{ loc: AppPath.PlanRequired, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.PROFILE_CREATION, res: true }, { loc: AppPath.PlanRequired, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.PROFILE_CREATION, res: true },
{ loc: AppPath.PlanRequired, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.SYNC_EMAIL, res: true }, { loc: AppPath.PlanRequired, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.SYNC_EMAIL, res: true },
{ loc: AppPath.PlanRequired, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.INVITE_TEAM, res: true }, { loc: AppPath.PlanRequired, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.INVITE_TEAM, res: true },
{ loc: AppPath.PlanRequired, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.COMPLETED, res: false }, { loc: AppPath.PlanRequired, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.COMPLETED, res: true },
{ loc: AppPath.PlanRequiredSuccess, isLogged: true, subscriptionStatus: undefined, onboardingStatus: OnboardingStatus.PLAN_REQUIRED, res: true }, { loc: AppPath.PlanRequiredSuccess, isLogged: true, subscriptionStatus: undefined, onboardingStatus: OnboardingStatus.PLAN_REQUIRED, res: true },
{ loc: AppPath.PlanRequiredSuccess, isLogged: true, subscriptionStatus: SubscriptionStatus.Canceled, onboardingStatus: OnboardingStatus.COMPLETED, res: false }, { loc: AppPath.PlanRequiredSuccess, isLogged: true, subscriptionStatus: SubscriptionStatus.Canceled, onboardingStatus: OnboardingStatus.COMPLETED, res: false },
@ -290,7 +284,7 @@ const testCases = [
describe('useShowAuthModal', () => { describe('useShowAuthModal', () => {
testCases.forEach((testCase) => { testCases.forEach((testCase) => {
it(`testCase for location ${testCase.loc} with onboardingStatus ${testCase.onboardingStatus} should return ${testCase.res}`, () => { it(`testCase for location ${testCase.loc} with onboardingStatus ${testCase.onboardingStatus} and subscriptionStatus ${testCase.subscriptionStatus} should return ${testCase.res}`, () => {
setupMockOnboardingStatus(testCase.onboardingStatus); setupMockOnboardingStatus(testCase.onboardingStatus);
setupMockSubscriptionStatus(testCase.subscriptionStatus); setupMockSubscriptionStatus(testCase.subscriptionStatus);
setupMockIsMatchingLocation(testCase.loc); setupMockIsMatchingLocation(testCase.loc);
@ -305,21 +299,21 @@ describe('useShowAuthModal', () => {
}); });
describe('test with token validation loading', () => { describe('test with token validation loading', () => {
it(`with appPath ${AppPath.Invite} and isDefaultLayoutAuthModalVisible=false`, () => { it(`with appPath ${AppPath.Invite} `, () => {
setupMockOnboardingStatus(OnboardingStatus.COMPLETED); setupMockOnboardingStatus(OnboardingStatus.COMPLETED);
setupMockSubscriptionStatus(SubscriptionStatus.Active); setupMockSubscriptionStatus(SubscriptionStatus.Active);
setupMockIsMatchingLocation(AppPath.Invite); setupMockIsMatchingLocation(AppPath.Invite);
setupMockIsLogged(true); setupMockIsLogged(true);
const { result } = getResult(false); const { result } = getResult();
expect(result.current).toBeFalsy(); expect(result.current).toBeTruthy();
}); });
it(`with appPath ${AppPath.ResetPassword} and isDefaultLayoutAuthModalVisible=false`, () => { it(`with appPath ${AppPath.ResetPassword} `, () => {
setupMockOnboardingStatus(OnboardingStatus.COMPLETED); setupMockOnboardingStatus(OnboardingStatus.COMPLETED);
setupMockSubscriptionStatus(SubscriptionStatus.Active); setupMockSubscriptionStatus(SubscriptionStatus.Active);
setupMockIsMatchingLocation(AppPath.ResetPassword); setupMockIsMatchingLocation(AppPath.ResetPassword);
setupMockIsLogged(true); setupMockIsLogged(true);
const { result } = getResult(false); const { result } = getResult();
expect(result.current).toBeFalsy(); expect(result.current).toBeTruthy();
}); });
}); });

View File

@ -1,40 +1,27 @@
import { useMemo } from 'react'; import { useMemo } from 'react';
import { useRecoilValue } from 'recoil';
import { useIsLogged } from '@/auth/hooks/useIsLogged'; import { useIsLogged } from '@/auth/hooks/useIsLogged';
import { useOnboardingStatus } from '@/onboarding/hooks/useOnboardingStatus'; import { useOnboardingStatus } from '@/onboarding/hooks/useOnboardingStatus';
import { AppPath } from '@/types/AppPath'; import { AppPath } from '@/types/AppPath';
import { isDefaultLayoutAuthModalVisibleState } from '@/ui/layout/states/isDefaultLayoutAuthModalVisibleState'; import { OnboardingStatus } from '~/generated/graphql';
import { useSubscriptionStatus } from '@/workspace/hooks/useSubscriptionStatus';
import { isDefined } from 'twenty-shared/utils';
import { OnboardingStatus, SubscriptionStatus } from '~/generated/graphql';
import { useIsMatchingLocation } from '~/hooks/useIsMatchingLocation'; import { useIsMatchingLocation } from '~/hooks/useIsMatchingLocation';
export const useShowAuthModal = () => { export const useShowAuthModal = () => {
const { isMatchingLocation } = useIsMatchingLocation(); const { isMatchingLocation } = useIsMatchingLocation();
const isLoggedIn = useIsLogged(); const isLoggedIn = useIsLogged();
const onboardingStatus = useOnboardingStatus(); const onboardingStatus = useOnboardingStatus();
const subscriptionStatus = useSubscriptionStatus();
const isDefaultLayoutAuthModalVisible = useRecoilValue(
isDefaultLayoutAuthModalVisibleState,
);
return useMemo(() => { return useMemo(() => {
if (
isMatchingLocation(AppPath.Verify) ||
isMatchingLocation(AppPath.VerifyEmail)
) {
return true;
}
if ( if (
isMatchingLocation(AppPath.Invite) || isMatchingLocation(AppPath.Invite) ||
isMatchingLocation(AppPath.ResetPassword) || isMatchingLocation(AppPath.ResetPassword) ||
isMatchingLocation(AppPath.VerifyEmail) || isMatchingLocation(AppPath.VerifyEmail) ||
isMatchingLocation(AppPath.SignInUp) isMatchingLocation(AppPath.Verify) ||
isMatchingLocation(AppPath.SignInUp) ||
isMatchingLocation(AppPath.CreateWorkspace) ||
isMatchingLocation(AppPath.PlanRequired)
) { ) {
return isDefaultLayoutAuthModalVisible; return true;
} }
if ( if (
@ -48,20 +35,6 @@ export const useShowAuthModal = () => {
return true; return true;
} }
if (isMatchingLocation(AppPath.PlanRequired)) {
return (
(onboardingStatus === OnboardingStatus.COMPLETED &&
!isDefined(subscriptionStatus)) ||
subscriptionStatus === SubscriptionStatus.Canceled
);
}
return false; return false;
}, [ }, [isLoggedIn, isMatchingLocation, onboardingStatus]);
isLoggedIn,
isDefaultLayoutAuthModalVisible,
isMatchingLocation,
onboardingStatus,
subscriptionStatus,
]);
}; };

View File

@ -257,3 +257,4 @@ export const Modal = ({
Modal.Header = ModalHeader; Modal.Header = ModalHeader;
Modal.Content = ModalContent; Modal.Content = ModalContent;
Modal.Footer = ModalFooter; Modal.Footer = ModalFooter;
Modal.Backdrop = StyledBackDrop;

View File

@ -26,7 +26,7 @@ const StyledLayout = styled.div`
flex-direction: column; flex-direction: column;
height: 100dvh; height: 100dvh;
position: relative; position: relative;
scrollbar-color: ${({ theme }) => theme.border.color.medium}; scrollbar-color: ${({ theme }) => theme.border.color.medium} transparent;
scrollbar-width: 4px; scrollbar-width: 4px;
width: 100%; width: 100%;
@ -105,7 +105,9 @@ export const DefaultLayout = () => {
)} )}
{showAuthModal ? ( {showAuthModal ? (
<> <>
<SignInBackgroundMockPage /> <StyledMainContainer>
<SignInBackgroundMockPage />
</StyledMainContainer>
<AnimatePresence mode="wait"> <AnimatePresence mode="wait">
<LayoutGroup> <LayoutGroup>
<AuthModal isOpenAnimated={animateModal}> <AuthModal isOpenAnimated={animateModal}>

View File

@ -1,5 +0,0 @@
import { createState } from 'twenty-ui/utilities';
export const isDefaultLayoutAuthModalVisibleState = createState<boolean>({
key: 'isDefaultLayoutAuthModalVisibleState',
defaultValue: true,
});

View File

@ -9,10 +9,10 @@ import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-sta
const StyledScrollWrapper = styled.div` const StyledScrollWrapper = styled.div`
&.scroll-wrapper-x-enabled { &.scroll-wrapper-x-enabled {
overflow-x: scroll; overflow-x: overlay;
} }
&.scroll-wrapper-y-enabled { &.scroll-wrapper-y-enabled {
overflow-y: scroll; overflow-y: overlay;
} }
overflow-x: hidden; overflow-x: hidden;
overflow-y: hidden; overflow-y: hidden;

View File

@ -10,7 +10,6 @@ import { AppPath } from '@/types/AppPath';
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar'; import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { TextInputV2 } from '@/ui/input/components/TextInputV2'; import { TextInputV2 } from '@/ui/input/components/TextInputV2';
import { isDefaultLayoutAuthModalVisibleState } from '@/ui/layout/states/isDefaultLayoutAuthModalVisibleState';
import { useTheme } from '@emotion/react'; import { useTheme } from '@emotion/react';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
@ -21,7 +20,9 @@ import { useState } from 'react';
import { Controller, useForm } from 'react-hook-form'; import { Controller, useForm } from 'react-hook-form';
import Skeleton, { SkeletonTheme } from 'react-loading-skeleton'; import Skeleton, { SkeletonTheme } from 'react-loading-skeleton';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import { useRecoilValue, useSetRecoilState } from 'recoil'; import { useRecoilValue } from 'recoil';
import { MainButton } from 'twenty-ui/input';
import { AnimatedEaseIn } from 'twenty-ui/utilities';
import { z } from 'zod'; import { z } from 'zod';
import { import {
useUpdatePasswordViaResetTokenMutation, useUpdatePasswordViaResetTokenMutation,
@ -29,8 +30,6 @@ import {
} from '~/generated/graphql'; } from '~/generated/graphql';
import { useNavigateApp } from '~/hooks/useNavigateApp'; import { useNavigateApp } from '~/hooks/useNavigateApp';
import { logError } from '~/utils/logError'; import { logError } from '~/utils/logError';
import { AnimatedEaseIn } from 'twenty-ui/utilities';
import { MainButton } from 'twenty-ui/input';
const validationSchema = z const validationSchema = z
.object({ .object({
@ -89,9 +88,6 @@ export const PasswordReset = () => {
const isLoggedIn = useIsLogged(); const isLoggedIn = useIsLogged();
const setIsDefaultLayoutAuthModalVisibleState = useSetRecoilState(
isDefaultLayoutAuthModalVisibleState,
);
const { control, handleSubmit } = useForm<Form>({ const { control, handleSubmit } = useForm<Form>({
mode: 'onChange', mode: 'onChange',
defaultValues: { defaultValues: {
@ -105,7 +101,7 @@ export const PasswordReset = () => {
variables: { variables: {
token: passwordResetToken ?? '', token: passwordResetToken ?? '',
}, },
skip: !passwordResetToken, skip: !passwordResetToken || isTokenValid,
onError: (error) => { onError: (error) => {
enqueueSnackBar(error?.message ?? 'Token Invalid', { enqueueSnackBar(error?.message ?? 'Token Invalid', {
variant: SnackBarVariant.Error, variant: SnackBarVariant.Error,
@ -114,7 +110,6 @@ export const PasswordReset = () => {
}, },
onCompleted: (data) => { onCompleted: (data) => {
setIsTokenValid(true); setIsTokenValid(true);
setIsDefaultLayoutAuthModalVisibleState(true);
if (isNonEmptyString(data?.validatePasswordResetToken?.email)) { if (isNonEmptyString(data?.validatePasswordResetToken?.email)) {
setEmail(data.validatePasswordResetToken.email); setEmail(data.validatePasswordResetToken.email);
} }

View File

@ -1,100 +1,12 @@
import { workspacePublicDataState } from '@/auth/states/workspacePublicDataState';
import { useRecoilValue } from 'recoil';
import { Logo } from '@/auth/components/Logo';
import { Title } from '@/auth/components/Title';
import { DEFAULT_WORKSPACE_NAME } from '@/ui/navigation/navigation-drawer/constants/DefaultWorkspaceName';
import { useMemo } from 'react';
import { useWorkspaceFromInviteHash } from '@/auth/sign-in-up/hooks/useWorkspaceFromInviteHash';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { useLingui } from '@lingui/react/macro';
import { isNonEmptyString } from '@sniptt/guards';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import { isDefined } from 'twenty-shared/utils';
import { Loader } from 'twenty-ui/feedback';
import { MainButton } from 'twenty-ui/input';
import { PublicWorkspaceDataOutput } from '~/generated/graphql';
const StyledContentContainer = styled(motion.div)` const StyledContentContainer = styled(motion.div)`
height: 300px;
margin-bottom: ${({ theme }) => theme.spacing(8)}; margin-bottom: ${({ theme }) => theme.spacing(8)};
margin-top: ${({ theme }) => theme.spacing(4)}; margin-top: ${({ theme }) => theme.spacing(4)};
`; `;
const StyledForm = styled.form`
align-items: center;
display: flex;
flex-direction: column;
width: 100%;
margin-top: ${({ theme }) => theme.spacing(10)};
`;
const StandardContent = ({
workspacePublicData,
signInUpForm,
title,
}: {
workspacePublicData: PublicWorkspaceDataOutput | null;
signInUpForm: JSX.Element | null;
title: string;
}) => {
return (
<>
<Logo secondaryLogo={workspacePublicData?.logo} />
<Title animate={false}>{title}</Title>
{signInUpForm}
</>
);
};
export const SignInUpLoading = () => { export const SignInUpLoading = () => {
const { t } = useLingui(); return <StyledContentContainer />;
const workspacePublicData = useRecoilValue(workspacePublicDataState);
const { workspaceInviteHash, workspace: workspaceFromInviteHash } =
useWorkspaceFromInviteHash();
const title = useMemo(() => {
if (isDefined(workspaceInviteHash)) {
return `Join ${workspaceFromInviteHash?.displayName ?? ''} team`;
}
const workspaceName = !isDefined(workspacePublicData?.displayName)
? DEFAULT_WORKSPACE_NAME
: !isNonEmptyString(workspacePublicData?.displayName)
? t`Your Workspace`
: workspacePublicData?.displayName;
return t`Welcome to ${workspaceName}`;
}, [
workspaceFromInviteHash?.displayName,
workspaceInviteHash,
workspacePublicData?.displayName,
t,
]);
return (
<StandardContent
workspacePublicData={workspacePublicData}
signInUpForm={
<>
<p style={{ color: 'red', backgroundColor: 'blue' }}>
SignInUpLoading
</p>
<StyledContentContainer>
<StyledForm>
<MainButton
disabled={true}
title={t`Continue`}
type="submit"
variant={'primary'}
Icon={() => <Loader />}
fullWidth
/>
</StyledForm>
</StyledContentContainer>
</>
}
title={title}
/>
);
}; };

View File

@ -1,30 +1,32 @@
import { MAIN_CONTEXT_STORE_INSTANCE_ID } from '@/context-store/constants/MainContextStoreInstanceId'; import { MAIN_CONTEXT_STORE_INSTANCE_ID } from '@/context-store/constants/MainContextStoreInstanceId';
import { contextStoreCurrentObjectMetadataItemIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataItemIdComponentState'; import { contextStoreCurrentObjectMetadataItemIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataItemIdComponentState';
import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState';
import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext'; import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext';
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
import { RecordIndexContainerGater } from '@/object-record/record-index/components/RecordIndexContainerGater'; import { RecordIndexContainerGater } from '@/object-record/record-index/components/RecordIndexContainerGater';
import { PageContainer } from '@/ui/layout/page/components/PageContainer'; import { PageContainer } from '@/ui/layout/page/components/PageContainer';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { isNonEmptyString, isUndefined } from '@sniptt/guards'; import { isUndefined } from '@sniptt/guards';
export const RecordIndexPage = () => { export const RecordIndexPage = () => {
const contextStoreCurrentViewId = useRecoilComponentValueV2(
contextStoreCurrentViewIdComponentState,
MAIN_CONTEXT_STORE_INSTANCE_ID,
);
const contextStoreCurrentObjectMetadataItemId = useRecoilComponentValueV2( const contextStoreCurrentObjectMetadataItemId = useRecoilComponentValueV2(
contextStoreCurrentObjectMetadataItemIdComponentState, contextStoreCurrentObjectMetadataItemIdComponentState,
MAIN_CONTEXT_STORE_INSTANCE_ID, MAIN_CONTEXT_STORE_INSTANCE_ID,
); );
if ( const { objectMetadataItems } = useObjectMetadataItems();
isUndefined(contextStoreCurrentObjectMetadataItemId) ||
!isNonEmptyString(contextStoreCurrentViewId) if (isUndefined(contextStoreCurrentObjectMetadataItemId)) {
) { return <></>;
return null;
} }
const objectMetadataItem = objectMetadataItems.find(
(objectMetadataItem) =>
objectMetadataItem.id === contextStoreCurrentObjectMetadataItemId,
);
if (isUndefined(objectMetadataItem)) {
return <></>;
}
return ( return (
<PageContainer> <PageContainer>
<ContextStoreComponentInstanceContext.Provider <ContextStoreComponentInstanceContext.Provider

View File

@ -8,6 +8,7 @@ import { z } from 'zod';
import { SubTitle } from '@/auth/components/SubTitle'; import { SubTitle } from '@/auth/components/SubTitle';
import { Title } from '@/auth/components/Title'; import { Title } from '@/auth/components/Title';
import { useAuth } from '@/auth/hooks/useAuth'; import { useAuth } from '@/auth/hooks/useAuth';
import { useRefreshObjectMetadataItems } from '@/object-metadata/hooks/useRefreshObjectMetadataItem';
import { useOnboardingStatus } from '@/onboarding/hooks/useOnboardingStatus'; import { useOnboardingStatus } from '@/onboarding/hooks/useOnboardingStatus';
import { useSetNextOnboardingStatus } from '@/onboarding/hooks/useSetNextOnboardingStatus'; import { useSetNextOnboardingStatus } from '@/onboarding/hooks/useSetNextOnboardingStatus';
import { WorkspaceLogoUploader } from '@/settings/workspace/components/WorkspaceLogoUploader'; import { WorkspaceLogoUploader } from '@/settings/workspace/components/WorkspaceLogoUploader';
@ -15,14 +16,14 @@ import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/Snac
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { TextInputV2 } from '@/ui/input/components/TextInputV2'; import { TextInputV2 } from '@/ui/input/components/TextInputV2';
import { Trans, useLingui } from '@lingui/react/macro'; import { Trans, useLingui } from '@lingui/react/macro';
import {
OnboardingStatus,
useActivateWorkspaceMutation,
} from '~/generated/graphql';
import { isDefined } from 'twenty-shared/utils'; import { isDefined } from 'twenty-shared/utils';
import { H2Title } from 'twenty-ui/display'; import { H2Title } from 'twenty-ui/display';
import { Loader } from 'twenty-ui/feedback'; import { Loader } from 'twenty-ui/feedback';
import { MainButton } from 'twenty-ui/input'; import { MainButton } from 'twenty-ui/input';
import {
OnboardingStatus,
useActivateWorkspaceMutation,
} from '~/generated/graphql';
const StyledContentContainer = styled.div` const StyledContentContainer = styled.div`
width: 100%; width: 100%;
@ -42,6 +43,7 @@ export const CreateWorkspace = () => {
const { enqueueSnackBar } = useSnackBar(); const { enqueueSnackBar } = useSnackBar();
const onboardingStatus = useOnboardingStatus(); const onboardingStatus = useOnboardingStatus();
const setNextOnboardingStatus = useSetNextOnboardingStatus(); const setNextOnboardingStatus = useSetNextOnboardingStatus();
const { refreshObjectMetadataItems } = useRefreshObjectMetadataItems();
const { loadCurrentUser } = useAuth(); const { loadCurrentUser } = useAuth();
const [activateWorkspace] = useActivateWorkspaceMutation(); const [activateWorkspace] = useActivateWorkspaceMutation();
@ -81,6 +83,8 @@ export const CreateWorkspace = () => {
if (isDefined(result.errors)) { if (isDefined(result.errors)) {
throw result.errors ?? new Error(t`Unknown error`); throw result.errors ?? new Error(t`Unknown error`);
} }
await refreshObjectMetadataItems();
await loadCurrentUser(); await loadCurrentUser();
setNextOnboardingStatus(); setNextOnboardingStatus();
} catch (error: any) { } catch (error: any) {
@ -93,6 +97,7 @@ export const CreateWorkspace = () => {
activateWorkspace, activateWorkspace,
enqueueSnackBar, enqueueSnackBar,
loadCurrentUser, loadCurrentUser,
refreshObjectMetadataItems,
setNextOnboardingStatus, setNextOnboardingStatus,
t, t,
], ],

View File

@ -21,6 +21,7 @@ import { UserProvider } from '~/modules/users/components/UserProvider';
import { mockedApolloClient } from '~/testing/mockedApolloClient'; import { mockedApolloClient } from '~/testing/mockedApolloClient';
import { RecoilDebugObserverEffect } from '@/debug/components/RecoilDebugObserver'; import { RecoilDebugObserverEffect } from '@/debug/components/RecoilDebugObserver';
import { ObjectMetadataItemsLoadEffect } from '@/object-metadata/components/ObjectMetadataItemsLoadEffect';
import { ObjectMetadataItemsProvider } from '@/object-metadata/components/ObjectMetadataItemsProvider'; import { ObjectMetadataItemsProvider } from '@/object-metadata/components/ObjectMetadataItemsProvider';
import { RecordFilterGroupsComponentInstanceContext } from '@/object-record/record-filter-group/states/context/RecordFilterGroupsComponentInstanceContext'; import { RecordFilterGroupsComponentInstanceContext } from '@/object-record/record-filter-group/states/context/RecordFilterGroupsComponentInstanceContext';
import { RecordFiltersComponentInstanceContext } from '@/object-record/record-filter/states/context/RecordFiltersComponentInstanceContext'; import { RecordFiltersComponentInstanceContext } from '@/object-record/record-filter/states/context/RecordFiltersComponentInstanceContext';
@ -29,10 +30,10 @@ import { PrefetchDataProvider } from '@/prefetch/components/PrefetchDataProvider
import { WorkspaceProviderEffect } from '@/workspace/components/WorkspaceProviderEffect'; import { WorkspaceProviderEffect } from '@/workspace/components/WorkspaceProviderEffect';
import { i18n } from '@lingui/core'; import { i18n } from '@lingui/core';
import { I18nProvider } from '@lingui/react'; import { I18nProvider } from '@lingui/react';
import { dynamicActivate } from '~/utils/i18n/dynamicActivate';
import { FullHeightStorybookLayout } from '../FullHeightStorybookLayout';
import { SOURCE_LOCALE } from 'twenty-shared/translations'; import { SOURCE_LOCALE } from 'twenty-shared/translations';
import { IconsProvider } from 'twenty-ui/display'; import { IconsProvider } from 'twenty-ui/display';
import { dynamicActivate } from '~/utils/i18n/dynamicActivate';
import { FullHeightStorybookLayout } from '../FullHeightStorybookLayout';
export type PageDecoratorArgs = { export type PageDecoratorArgs = {
routePath: string; routePath: string;
@ -86,6 +87,7 @@ const Providers = () => {
<WorkspaceProviderEffect /> <WorkspaceProviderEffect />
<UserProvider> <UserProvider>
<ApolloMetadataClientMockedProvider> <ApolloMetadataClientMockedProvider>
<ObjectMetadataItemsLoadEffect />
<ObjectMetadataItemsProvider> <ObjectMetadataItemsProvider>
<FullHeightStorybookLayout> <FullHeightStorybookLayout>
<HelmetProvider> <HelmetProvider>

View File

@ -12,10 +12,14 @@ export const emailModuleFactory = (
switch (driver) { switch (driver) {
case EmailDriver.Logger: case EmailDriver.Logger:
return {}; return {
type: EmailDriver.Logger,
};
case EmailDriver.Smtp: { case EmailDriver.Smtp: {
const options: EmailModuleOptions = {}; const options: EmailModuleOptions = {
type: EmailDriver.Smtp,
};
const host = environmentService.get('EMAIL_SMTP_HOST'); const host = environmentService.get('EMAIL_SMTP_HOST');
const port = environmentService.get('EMAIL_SMTP_PORT'); const port = environmentService.get('EMAIL_SMTP_PORT');

View File

@ -1,6 +1,9 @@
import { DynamicModule, Global } from '@nestjs/common'; import { DynamicModule, Global } from '@nestjs/common';
import { EmailModuleAsyncOptions } from 'src/engine/core-modules/email/interfaces/email.interface'; import {
EmailDriver,
EmailModuleAsyncOptions,
} from 'src/engine/core-modules/email/interfaces/email.interface';
import { LoggerDriver } from 'src/engine/core-modules/email/drivers/logger.driver'; import { LoggerDriver } from 'src/engine/core-modules/email/drivers/logger.driver';
import { SmtpDriver } from 'src/engine/core-modules/email/drivers/smtp.driver'; import { SmtpDriver } from 'src/engine/core-modules/email/drivers/smtp.driver';
@ -16,7 +19,9 @@ export class EmailModule {
useFactory: (...args: any[]) => { useFactory: (...args: any[]) => {
const config = options.useFactory(...args); const config = options.useFactory(...args);
return config ? new SmtpDriver(config) : new LoggerDriver(); return config.type === EmailDriver.Smtp
? new SmtpDriver(config)
: new LoggerDriver();
}, },
inject: options.inject || [], inject: options.inject || [],
}; };

View File

@ -7,7 +7,13 @@ export enum EmailDriver {
Smtp = 'smtp', Smtp = 'smtp',
} }
export type EmailModuleOptions = SMTPConnection.Options | undefined; export type EmailModuleOptions =
| (SMTPConnection.Options & {
type: EmailDriver.Smtp;
})
| {
type: EmailDriver.Logger;
};
export type EmailModuleAsyncOptions = { export type EmailModuleAsyncOptions = {
useFactory: (...args: any[]) => EmailModuleOptions; useFactory: (...args: any[]) => EmailModuleOptions;