refactor(auth): add workspaces selection (#12098)
This commit is contained in:
@ -8,7 +8,6 @@ import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMembe
|
||||
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 { REACT_APP_SERVER_BASE_URL } from '~/config';
|
||||
import { useUpdateEffect } from '~/hooks/useUpdateEffect';
|
||||
import { isMatchingLocation } from '~/utils/isMatchingLocation';
|
||||
@ -33,8 +32,7 @@ export const useApolloFactory = (options: Partial<Options<any>> = {}) => {
|
||||
const setCurrentUser = useSetRecoilState(currentUserState);
|
||||
const setCurrentUserWorkspace = useSetRecoilState(currentUserWorkspaceState);
|
||||
|
||||
const setWorkspaces = useSetRecoilState(workspacesState);
|
||||
const [, setPreviousUrl] = useRecoilState(previousUrlState);
|
||||
const setPreviousUrl = useSetRecoilState(previousUrlState);
|
||||
const location = useLocation();
|
||||
|
||||
const apolloClient = useMemo(() => {
|
||||
@ -65,7 +63,6 @@ export const useApolloFactory = (options: Partial<Options<any>> = {}) => {
|
||||
setCurrentWorkspaceMember(null);
|
||||
setCurrentWorkspace(null);
|
||||
setCurrentUserWorkspace(null);
|
||||
setWorkspaces([]);
|
||||
if (
|
||||
!isMatchingLocation(location, AppPath.Verify) &&
|
||||
!isMatchingLocation(location, AppPath.SignInUp) &&
|
||||
@ -89,7 +86,6 @@ export const useApolloFactory = (options: Partial<Options<any>> = {}) => {
|
||||
setCurrentUser,
|
||||
setCurrentWorkspaceMember,
|
||||
setCurrentWorkspace,
|
||||
setWorkspaces,
|
||||
setPreviousUrl,
|
||||
]);
|
||||
|
||||
|
||||
@ -11,6 +11,7 @@ type LogoProps = {
|
||||
primaryLogo?: string | null;
|
||||
secondaryLogo?: string | null;
|
||||
placeholder?: string | null;
|
||||
onClick?: () => void;
|
||||
};
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
@ -53,6 +54,7 @@ export const Logo = ({
|
||||
primaryLogo,
|
||||
secondaryLogo,
|
||||
placeholder,
|
||||
onClick,
|
||||
}: LogoProps) => {
|
||||
const { redirectToDefaultDomain } = useRedirectToDefaultDomain();
|
||||
const defaultPrimaryLogoUrl = `${window.location.origin}/images/icons/android/android-launchericon-192-192.png`;
|
||||
@ -72,7 +74,7 @@ export const Logo = ({
|
||||
const isUsingDefaultLogo = !isDefined(primaryLogo);
|
||||
|
||||
return (
|
||||
<StyledContainer>
|
||||
<StyledContainer onClick={() => onClick?.()}>
|
||||
{isUsingDefaultLogo ? (
|
||||
<UndecoratedLink
|
||||
to={AppPath.SignInUp}
|
||||
|
||||
@ -17,3 +17,36 @@ export const AUTH_TOKENS = gql`
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const AVAILABLE_WORKSPACE_FOR_AUTH_FRAGMENT = gql`
|
||||
fragment AvailableWorkspaceFragment on AvailableWorkspace {
|
||||
id
|
||||
displayName
|
||||
loginToken
|
||||
inviteHash
|
||||
personalInviteToken
|
||||
workspaceUrls {
|
||||
subdomainUrl
|
||||
customUrl
|
||||
}
|
||||
logo
|
||||
sso {
|
||||
type
|
||||
id
|
||||
issuer
|
||||
name
|
||||
status
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const AVAILABLE_WORKSPACES_FOR_AUTH_FRAGMENT = gql`
|
||||
fragment AvailableWorkspacesFragment on AvailableWorkspaces {
|
||||
availableWorkspacesForSignIn {
|
||||
...AvailableWorkspaceFragment
|
||||
}
|
||||
availableWorkspacesForSignUp {
|
||||
...AvailableWorkspaceFragment
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@ -17,8 +17,7 @@ export const GET_LOGIN_TOKEN_FROM_EMAIL_VERIFICATION_TOKEN = gql`
|
||||
...AuthTokenFragment
|
||||
}
|
||||
workspaceUrls {
|
||||
subdomainUrl
|
||||
customUrl
|
||||
...WorkspaceUrlsFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,8 +6,7 @@ export const IMPERSONATE = gql`
|
||||
impersonate(userId: $userId, workspaceId: $workspaceId) {
|
||||
workspace {
|
||||
workspaceUrls {
|
||||
subdomainUrl
|
||||
customUrl
|
||||
...WorkspaceUrlsFragment
|
||||
}
|
||||
id
|
||||
}
|
||||
|
||||
@ -0,0 +1,14 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const SIGN_IN = gql`
|
||||
mutation SignIn($email: String!, $password: String!, $captchaToken: String) {
|
||||
signIn(email: $email, password: $password, captchaToken: $captchaToken) {
|
||||
availableWorkspaces {
|
||||
...AvailableWorkspacesFragment
|
||||
}
|
||||
tokens {
|
||||
...AuthTokensFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
@ -1,35 +1,13 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const SIGN_UP = gql`
|
||||
mutation SignUp(
|
||||
$email: String!
|
||||
$password: String!
|
||||
$workspaceInviteHash: String
|
||||
$workspacePersonalInviteToken: String = null
|
||||
$captchaToken: String
|
||||
$workspaceId: String
|
||||
$locale: String
|
||||
$verifyEmailNextPath: String
|
||||
) {
|
||||
signUp(
|
||||
email: $email
|
||||
password: $password
|
||||
workspaceInviteHash: $workspaceInviteHash
|
||||
workspacePersonalInviteToken: $workspacePersonalInviteToken
|
||||
captchaToken: $captchaToken
|
||||
workspaceId: $workspaceId
|
||||
locale: $locale
|
||||
verifyEmailNextPath: $verifyEmailNextPath
|
||||
) {
|
||||
loginToken {
|
||||
...AuthTokenFragment
|
||||
mutation SignUp($email: String!, $password: String!, $captchaToken: String) {
|
||||
signUp(email: $email, password: $password, captchaToken: $captchaToken) {
|
||||
availableWorkspaces {
|
||||
...AvailableWorkspacesFragment
|
||||
}
|
||||
workspace {
|
||||
id
|
||||
workspaceUrls {
|
||||
subdomainUrl
|
||||
customUrl
|
||||
}
|
||||
tokens {
|
||||
...AuthTokensFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -9,8 +9,7 @@ export const SIGN_UP_IN_NEW_WORKSPACE = gql`
|
||||
workspace {
|
||||
id
|
||||
workspaceUrls {
|
||||
subdomainUrl
|
||||
customUrl
|
||||
...WorkspaceUrlsFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,36 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const SIGN_UP_IN_WORKSPACE = gql`
|
||||
mutation SignUpInWorkspace(
|
||||
$email: String!
|
||||
$password: String!
|
||||
$workspaceInviteHash: String
|
||||
$workspacePersonalInviteToken: String = null
|
||||
$captchaToken: String
|
||||
$workspaceId: String
|
||||
$locale: String
|
||||
$verifyEmailNextPath: String
|
||||
) {
|
||||
signUpInWorkspace(
|
||||
email: $email
|
||||
password: $password
|
||||
workspaceInviteHash: $workspaceInviteHash
|
||||
workspacePersonalInviteToken: $workspacePersonalInviteToken
|
||||
captchaToken: $captchaToken
|
||||
workspaceId: $workspaceId
|
||||
locale: $locale
|
||||
verifyEmailNextPath: $verifyEmailNextPath
|
||||
) {
|
||||
loginToken {
|
||||
...AuthTokenFragment
|
||||
}
|
||||
workspace {
|
||||
id
|
||||
workspaceUrls {
|
||||
subdomainUrl
|
||||
customUrl
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
@ -3,30 +3,9 @@ import { gql } from '@apollo/client';
|
||||
export const CHECK_USER_EXISTS = gql`
|
||||
query CheckUserExists($email: String!, $captchaToken: String) {
|
||||
checkUserExists(email: $email, captchaToken: $captchaToken) {
|
||||
__typename
|
||||
... on UserExists {
|
||||
exists
|
||||
availableWorkspaces {
|
||||
id
|
||||
displayName
|
||||
workspaceUrls {
|
||||
subdomainUrl
|
||||
customUrl
|
||||
}
|
||||
logo
|
||||
sso {
|
||||
type
|
||||
id
|
||||
issuer
|
||||
name
|
||||
status
|
||||
}
|
||||
}
|
||||
isEmailVerified
|
||||
}
|
||||
... on UserNotExists {
|
||||
exists
|
||||
}
|
||||
exists
|
||||
availableWorkspacesCount
|
||||
isEmailVerified
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { gql } from '@apollo/client';
|
||||
import { WORKSPACE_URLS_FRAGMENT } from '@/users/graphql/fragments/workspaceUrlsFragment';
|
||||
|
||||
export const GET_PUBLIC_WORKSPACE_DATA_BY_DOMAIN = gql`
|
||||
query GetPublicWorkspaceDataByDomain($origin: String!) {
|
||||
@ -7,8 +8,7 @@ export const GET_PUBLIC_WORKSPACE_DATA_BY_DOMAIN = gql`
|
||||
logo
|
||||
displayName
|
||||
workspaceUrls {
|
||||
subdomainUrl
|
||||
customUrl
|
||||
...WorkspaceUrlsFragment
|
||||
}
|
||||
authProviders {
|
||||
sso {
|
||||
@ -25,4 +25,5 @@ export const GET_PUBLIC_WORKSPACE_DATA_BY_DOMAIN = gql`
|
||||
}
|
||||
}
|
||||
}
|
||||
${WORKSPACE_URLS_FRAGMENT}
|
||||
`;
|
||||
|
||||
@ -3,6 +3,7 @@ import {
|
||||
GetCurrentUserDocument,
|
||||
GetLoginTokenFromCredentialsDocument,
|
||||
SignUpDocument,
|
||||
SignUpInWorkspaceDocument,
|
||||
} from '~/generated/graphql';
|
||||
|
||||
export const queries = {
|
||||
@ -10,6 +11,7 @@ export const queries = {
|
||||
getAuthTokensFromLoginToken: GetAuthTokensFromLoginTokenDocument,
|
||||
signup: SignUpDocument,
|
||||
getCurrentUser: GetCurrentUserDocument,
|
||||
signUpInWorkspace: SignUpInWorkspaceDocument,
|
||||
};
|
||||
|
||||
export const email = 'test@test.com';
|
||||
@ -29,7 +31,13 @@ export const variables = {
|
||||
email,
|
||||
password,
|
||||
workspacePersonalInviteToken: null,
|
||||
locale: "",
|
||||
locale: '',
|
||||
},
|
||||
signUpInWorkspace: {
|
||||
email,
|
||||
password,
|
||||
workspacePersonalInviteToken: null,
|
||||
locale: '',
|
||||
},
|
||||
getCurrentUser: {},
|
||||
};
|
||||
@ -48,6 +56,16 @@ export const results = {
|
||||
},
|
||||
},
|
||||
signUp: { loginToken: { token, expiresAt: 'expiresAt' } },
|
||||
signUpInWorkspace: {
|
||||
loginToken: { token, expiresAt: 'expiresAt' },
|
||||
workspace: {
|
||||
id: 'workspace-id',
|
||||
workspaceUrls: {
|
||||
subdomainUrl: 'https://subdomain.twenty.com',
|
||||
customUrl: 'https://custom.twenty.com',
|
||||
},
|
||||
},
|
||||
},
|
||||
getCurrentUser: {
|
||||
currentUser: {
|
||||
id: 'id',
|
||||
@ -67,6 +85,7 @@ export const results = {
|
||||
avatarUrl: 'avatarUrl',
|
||||
locale: 'locale',
|
||||
},
|
||||
availableWorkspaces: [],
|
||||
currentWorkspace: {
|
||||
id: 'id',
|
||||
displayName: 'displayName',
|
||||
@ -74,6 +93,11 @@ export const results = {
|
||||
inviteHash: 'inviteHash',
|
||||
allowImpersonation: true,
|
||||
subscriptionStatus: 'subscriptionStatus',
|
||||
customDomain: null,
|
||||
workspaceUrls: {
|
||||
customUrl: undefined,
|
||||
subdomainUrl: 'https://twenty.com',
|
||||
},
|
||||
featureFlags: {
|
||||
id: 'id',
|
||||
key: 'key',
|
||||
@ -85,8 +109,8 @@ export const results = {
|
||||
},
|
||||
};
|
||||
|
||||
export const mocks = [
|
||||
{
|
||||
export const mocks = {
|
||||
getLoginTokenFromCredentials: {
|
||||
request: {
|
||||
query: queries.getLoginTokenFromCredentials,
|
||||
variables: variables.getLoginTokenFromCredentials,
|
||||
@ -97,7 +121,7 @@ export const mocks = [
|
||||
},
|
||||
})),
|
||||
},
|
||||
{
|
||||
getAuthTokensFromLoginToken: {
|
||||
request: {
|
||||
query: queries.getAuthTokensFromLoginToken,
|
||||
variables: variables.getAuthTokensFromLoginToken,
|
||||
@ -108,7 +132,7 @@ export const mocks = [
|
||||
},
|
||||
})),
|
||||
},
|
||||
{
|
||||
signup: {
|
||||
request: {
|
||||
query: queries.signup,
|
||||
variables: variables.signup,
|
||||
@ -119,7 +143,7 @@ export const mocks = [
|
||||
},
|
||||
})),
|
||||
},
|
||||
{
|
||||
getCurrentUser: {
|
||||
request: {
|
||||
query: queries.getCurrentUser,
|
||||
variables: variables.getCurrentUser,
|
||||
@ -128,4 +152,15 @@ export const mocks = [
|
||||
data: results.getCurrentUser,
|
||||
})),
|
||||
},
|
||||
];
|
||||
signUpInWorkspace: {
|
||||
request: {
|
||||
query: queries.signUpInWorkspace,
|
||||
variables: variables.signUpInWorkspace,
|
||||
},
|
||||
result: jest.fn(() => ({
|
||||
data: {
|
||||
signUpInWorkspace: results.signUpInWorkspace,
|
||||
},
|
||||
})),
|
||||
},
|
||||
};
|
||||
|
||||
@ -2,6 +2,7 @@ import { useAuth } from '@/auth/hooks/useAuth';
|
||||
import { billingState } from '@/client-config/states/billingState';
|
||||
import { isDeveloperDefaultSignInPrefilledState } from '@/client-config/states/isDeveloperDefaultSignInPrefilledState';
|
||||
import { supportChatState } from '@/client-config/states/supportChatState';
|
||||
import { SnackBarProviderScope } from '@/ui/feedback/snack-bar-manager/scopes/SnackBarProviderScope';
|
||||
import { workspaceAuthProvidersState } from '@/workspace/states/workspaceAuthProvidersState';
|
||||
import { useApolloClient } from '@apollo/client';
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
@ -30,9 +31,13 @@ jest.mock('@/object-metadata/hooks/useRefreshObjectMetadataItem', () => ({
|
||||
}));
|
||||
|
||||
const Wrapper = ({ children }: { children: ReactNode }) => (
|
||||
<MockedProvider mocks={mocks} addTypename={false}>
|
||||
<MockedProvider mocks={Object.values(mocks)} addTypename={false}>
|
||||
<RecoilRoot>
|
||||
<MemoryRouter>{children}</MemoryRouter>
|
||||
<MemoryRouter>
|
||||
<SnackBarProviderScope snackBarManagerScopeId="test-scope-id">
|
||||
{children}
|
||||
</SnackBarProviderScope>
|
||||
</MemoryRouter>
|
||||
</RecoilRoot>
|
||||
</MockedProvider>
|
||||
);
|
||||
@ -63,7 +68,7 @@ describe('useAuth', () => {
|
||||
).toStrictEqual(results.getLoginTokenFromCredentials);
|
||||
});
|
||||
|
||||
expect(mocks[0].result).toHaveBeenCalled();
|
||||
expect(mocks.getLoginTokenFromCredentials.result).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should verify user', async () => {
|
||||
@ -73,19 +78,19 @@ describe('useAuth', () => {
|
||||
await result.current.getAuthTokensFromLoginToken(token);
|
||||
});
|
||||
|
||||
expect(mocks[1].result).toHaveBeenCalled();
|
||||
expect(mocks[3].result).toHaveBeenCalled();
|
||||
expect(mocks.getAuthTokensFromLoginToken.result).toHaveBeenCalled();
|
||||
expect(mocks.getCurrentUser.result).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle credential sign-in', async () => {
|
||||
const { result } = renderHooks();
|
||||
|
||||
await act(async () => {
|
||||
await result.current.signInWithCredentials(email, password);
|
||||
await result.current.signInWithCredentialsInWorkspace(email, password);
|
||||
});
|
||||
|
||||
expect(mocks[0].result).toHaveBeenCalled();
|
||||
expect(mocks[1].result).toHaveBeenCalled();
|
||||
expect(mocks.getLoginTokenFromCredentials.result).toHaveBeenCalled();
|
||||
expect(mocks.getAuthTokensFromLoginToken.result).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle google sign-in', async () => {
|
||||
@ -94,6 +99,7 @@ describe('useAuth', () => {
|
||||
await act(async () => {
|
||||
await result.current.signInWithGoogle({
|
||||
workspaceInviteHash: 'workspaceInviteHash',
|
||||
action: 'join-workspace',
|
||||
});
|
||||
});
|
||||
|
||||
@ -163,9 +169,12 @@ describe('useAuth', () => {
|
||||
const { result } = renderHooks();
|
||||
|
||||
await act(async () => {
|
||||
await result.current.signUpWithCredentials({ email, password });
|
||||
await result.current.signUpWithCredentialsInWorkspace({
|
||||
email,
|
||||
password,
|
||||
});
|
||||
});
|
||||
|
||||
expect(mocks[2].result).toHaveBeenCalled();
|
||||
expect(mocks.signUpInWorkspace.result).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@ -11,19 +11,21 @@ import {
|
||||
|
||||
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
||||
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
||||
import { workspacesState } from '@/auth/states/workspaces';
|
||||
import { billingState } from '@/client-config/states/billingState';
|
||||
import { clientConfigApiStatusState } from '@/client-config/states/clientConfigApiStatusState';
|
||||
import { supportChatState } from '@/client-config/states/supportChatState';
|
||||
import { ColorScheme } from '@/workspace-member/types/WorkspaceMember';
|
||||
import { REACT_APP_SERVER_BASE_URL } from '~/config';
|
||||
import {
|
||||
AuthTokenPair,
|
||||
useCheckUserExistsLazyQuery,
|
||||
useGetAuthTokensFromLoginTokenMutation,
|
||||
useGetCurrentUserLazyQuery,
|
||||
useGetLoginTokenFromCredentialsMutation,
|
||||
useGetLoginTokenFromEmailVerificationTokenMutation,
|
||||
useSignUpMutation,
|
||||
useSignInMutation,
|
||||
useSignUpInWorkspaceMutation,
|
||||
} from '~/generated/graphql';
|
||||
|
||||
import { currentWorkspaceMembersState } from '@/auth/states/currentWorkspaceMembersStates';
|
||||
@ -68,10 +70,17 @@ import { iconsState } from 'twenty-ui/display';
|
||||
import { cookieStorage } from '~/utils/cookie-storage';
|
||||
import { getWorkspaceUrl } from '~/utils/getWorkspaceUrl';
|
||||
import { dynamicActivate } from '~/utils/i18n/dynamicActivate';
|
||||
import { availableWorkspacesState } from '@/auth/states/availableWorkspacesState';
|
||||
import { useSignUpInNewWorkspace } from '@/auth/sign-in-up/hooks/useSignUpInNewWorkspace';
|
||||
import {
|
||||
countAvailableWorkspaces,
|
||||
getFirstAvailableWorkspaces,
|
||||
} from '@/auth/utils/availableWorkspacesUtils';
|
||||
|
||||
export const useAuth = () => {
|
||||
const setTokenPair = useSetRecoilState(tokenPairState);
|
||||
const setCurrentUser = useSetRecoilState(currentUserState);
|
||||
const setAvailableWorkspaces = useSetRecoilState(availableWorkspacesState);
|
||||
const setCurrentWorkspaceMember = useSetRecoilState(
|
||||
currentWorkspaceMemberState,
|
||||
);
|
||||
@ -87,16 +96,18 @@ export const useAuth = () => {
|
||||
);
|
||||
|
||||
const { refreshObjectMetadataItems } = useRefreshObjectMetadataItems();
|
||||
const { createWorkspace } = useSignUpInNewWorkspace();
|
||||
|
||||
const setSignInUpStep = useSetRecoilState(signInUpStepState);
|
||||
const setCurrentWorkspace = useSetRecoilState(currentWorkspaceState);
|
||||
const setWorkspaces = useSetRecoilState(workspacesState);
|
||||
const { redirect } = useRedirect();
|
||||
const { redirectToWorkspaceDomain } = useRedirectToWorkspaceDomain();
|
||||
|
||||
const [getLoginTokenFromCredentials] =
|
||||
useGetLoginTokenFromCredentialsMutation();
|
||||
const [signIn] = useSignInMutation();
|
||||
const [signUp] = useSignUpMutation();
|
||||
const [signUpInWorkspace] = useSignUpInWorkspaceMutation();
|
||||
const [getAuthTokensFromLoginToken] =
|
||||
useGetAuthTokensFromLoginTokenMutation();
|
||||
const [getLoginTokenFromEmailVerificationToken] =
|
||||
@ -280,6 +291,10 @@ export const useAuth = () => {
|
||||
setCurrentWorkspaceMembers(workspaceMembers);
|
||||
}
|
||||
|
||||
if (isDefined(user.availableWorkspaces)) {
|
||||
setAvailableWorkspaces(user.availableWorkspaces);
|
||||
}
|
||||
|
||||
if (isDefined(user.currentUserWorkspace)) {
|
||||
setCurrentUserWorkspace(user.currentUserWorkspace);
|
||||
}
|
||||
@ -326,17 +341,6 @@ export const useAuth = () => {
|
||||
});
|
||||
}
|
||||
|
||||
if (isDefined(user.workspaces)) {
|
||||
const validWorkspaces = user.workspaces
|
||||
.filter(
|
||||
({ workspace }) => workspace !== null && workspace !== undefined,
|
||||
)
|
||||
.map((validWorkspace) => validWorkspace.workspace)
|
||||
.filter(isDefined);
|
||||
|
||||
setWorkspaces(validWorkspaces);
|
||||
}
|
||||
|
||||
return {
|
||||
user,
|
||||
workspaceMember,
|
||||
@ -352,9 +356,17 @@ export const useAuth = () => {
|
||||
setCurrentWorkspaceMembers,
|
||||
setDateTimeFormat,
|
||||
setLastAuthenticateWorkspaceDomain,
|
||||
setWorkspaces,
|
||||
setAvailableWorkspaces,
|
||||
]);
|
||||
|
||||
const handleSetAuthTokens = useCallback(
|
||||
(tokens: AuthTokenPair) => {
|
||||
setTokenPair(tokens);
|
||||
cookieStorage.setItem('tokenPair', JSON.stringify(tokens));
|
||||
},
|
||||
[setTokenPair],
|
||||
);
|
||||
|
||||
const handleGetAuthTokensFromLoginToken = useCallback(
|
||||
async (loginToken: string) => {
|
||||
const getAuthTokensResult = await getAuthTokensFromLoginToken({
|
||||
@ -372,14 +384,8 @@ export const useAuth = () => {
|
||||
throw new Error('No getAuthTokensFromLoginToken result');
|
||||
}
|
||||
|
||||
setTokenPair(
|
||||
getAuthTokensResult.data?.getAuthTokensFromLoginToken.tokens,
|
||||
);
|
||||
cookieStorage.setItem(
|
||||
'tokenPair',
|
||||
JSON.stringify(
|
||||
getAuthTokensResult.data?.getAuthTokensFromLoginToken.tokens,
|
||||
),
|
||||
handleSetAuthTokens(
|
||||
getAuthTokensResult.data.getAuthTokensFromLoginToken.tokens,
|
||||
);
|
||||
|
||||
// TODO: We can't parallelize this yet because when loadCurrentUSer is loaded
|
||||
@ -390,14 +396,97 @@ export const useAuth = () => {
|
||||
},
|
||||
[
|
||||
getAuthTokensFromLoginToken,
|
||||
setTokenPair,
|
||||
loadCurrentUser,
|
||||
origin,
|
||||
handleSetAuthTokens,
|
||||
refreshObjectMetadataItems,
|
||||
],
|
||||
);
|
||||
|
||||
const handleCredentialsSignIn = useCallback(
|
||||
async (email: string, password: string, captchaToken?: string) => {
|
||||
signIn({
|
||||
variables: { email, password, captchaToken },
|
||||
onCompleted: async (data) => {
|
||||
handleSetAuthTokens(data.signIn.tokens);
|
||||
const { user } = await loadCurrentUser();
|
||||
|
||||
const availableWorkspacesCount = countAvailableWorkspaces(
|
||||
user.availableWorkspaces,
|
||||
);
|
||||
|
||||
if (availableWorkspacesCount === 0) {
|
||||
return createWorkspace();
|
||||
}
|
||||
|
||||
if (availableWorkspacesCount === 1) {
|
||||
const targetWorkspace = getFirstAvailableWorkspaces(
|
||||
user.availableWorkspaces,
|
||||
);
|
||||
return await redirectToWorkspaceDomain(
|
||||
getWorkspaceUrl(targetWorkspace.workspaceUrls),
|
||||
targetWorkspace.loginToken ? AppPath.Verify : AppPath.SignInUp,
|
||||
{
|
||||
...(targetWorkspace.loginToken && {
|
||||
loginToken: targetWorkspace.loginToken,
|
||||
}),
|
||||
email: user.email,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
setSignInUpStep(SignInUpStep.WorkspaceSelection);
|
||||
},
|
||||
onError: (error) => {
|
||||
if (
|
||||
error instanceof ApolloError &&
|
||||
error.graphQLErrors[0]?.extensions?.subCode === 'EMAIL_NOT_VERIFIED'
|
||||
) {
|
||||
setSearchParams({ email });
|
||||
setSignInUpStep(SignInUpStep.EmailVerification);
|
||||
throw error;
|
||||
}
|
||||
throw error;
|
||||
},
|
||||
});
|
||||
},
|
||||
[
|
||||
handleSetAuthTokens,
|
||||
redirectToWorkspaceDomain,
|
||||
signIn,
|
||||
loadCurrentUser,
|
||||
setSearchParams,
|
||||
setSignInUpStep,
|
||||
createWorkspace,
|
||||
],
|
||||
);
|
||||
|
||||
const handleCredentialsSignUp = useCallback(
|
||||
async (email: string, password: string, captchaToken?: string) => {
|
||||
signUp({
|
||||
variables: { email, password, captchaToken },
|
||||
onCompleted: async (data) => {
|
||||
handleSetAuthTokens(data.signUp.tokens);
|
||||
const { user } = await loadCurrentUser();
|
||||
|
||||
if (countAvailableWorkspaces(user.availableWorkspaces) === 0) {
|
||||
return createWorkspace();
|
||||
}
|
||||
|
||||
setSignInUpStep(SignInUpStep.WorkspaceSelection);
|
||||
},
|
||||
});
|
||||
},
|
||||
[
|
||||
handleSetAuthTokens,
|
||||
signUp,
|
||||
loadCurrentUser,
|
||||
setSignInUpStep,
|
||||
createWorkspace,
|
||||
],
|
||||
);
|
||||
|
||||
const handleCredentialsSignInInWorkspace = useCallback(
|
||||
async (email: string, password: string, captchaToken?: string) => {
|
||||
const { loginToken } = await handleGetLoginTokenFromCredentials(
|
||||
email,
|
||||
@ -413,7 +502,7 @@ export const useAuth = () => {
|
||||
await clearSession();
|
||||
}, [clearSession]);
|
||||
|
||||
const handleCredentialsSignUp = useCallback(
|
||||
const handleCredentialsSignUpInWorkspace = useCallback(
|
||||
async ({
|
||||
email,
|
||||
password,
|
||||
@ -429,7 +518,7 @@ export const useAuth = () => {
|
||||
captchaToken?: string;
|
||||
verifyEmailNextPath?: string;
|
||||
}) => {
|
||||
const signUpResult = await signUp({
|
||||
const signUpInWorkspaceResult = await signUpInWorkspace({
|
||||
variables: {
|
||||
email,
|
||||
password,
|
||||
@ -444,11 +533,11 @@ export const useAuth = () => {
|
||||
},
|
||||
});
|
||||
|
||||
if (isDefined(signUpResult.errors)) {
|
||||
throw signUpResult.errors;
|
||||
if (isDefined(signUpInWorkspaceResult.errors)) {
|
||||
throw signUpInWorkspaceResult.errors;
|
||||
}
|
||||
|
||||
if (!signUpResult.data?.signUp) {
|
||||
if (!signUpInWorkspaceResult.data?.signUpInWorkspace) {
|
||||
throw new Error('No login token');
|
||||
}
|
||||
|
||||
@ -460,11 +549,15 @@ export const useAuth = () => {
|
||||
|
||||
if (isMultiWorkspaceEnabled) {
|
||||
return await redirectToWorkspaceDomain(
|
||||
getWorkspaceUrl(signUpResult.data.signUp.workspace.workspaceUrls),
|
||||
getWorkspaceUrl(
|
||||
signUpInWorkspaceResult.data.signUpInWorkspace.workspace
|
||||
.workspaceUrls,
|
||||
),
|
||||
isEmailVerificationRequired ? AppPath.SignInUp : AppPath.Verify,
|
||||
{
|
||||
...(!isEmailVerificationRequired && {
|
||||
loginToken: signUpResult.data.signUp.loginToken.token,
|
||||
loginToken:
|
||||
signUpInWorkspaceResult.data.signUpInWorkspace.loginToken.token,
|
||||
}),
|
||||
email,
|
||||
},
|
||||
@ -472,11 +565,11 @@ export const useAuth = () => {
|
||||
}
|
||||
|
||||
await handleGetAuthTokensFromLoginToken(
|
||||
signUpResult.data?.signUp.loginToken.token,
|
||||
signUpInWorkspaceResult.data?.signUpInWorkspace.loginToken.token,
|
||||
);
|
||||
},
|
||||
[
|
||||
signUp,
|
||||
signUpInWorkspace,
|
||||
workspacePublicData,
|
||||
isMultiWorkspaceEnabled,
|
||||
handleGetAuthTokensFromLoginToken,
|
||||
@ -494,6 +587,7 @@ export const useAuth = () => {
|
||||
workspacePersonalInviteToken?: string;
|
||||
workspaceInviteHash?: string;
|
||||
billingCheckoutSession?: BillingCheckoutSession;
|
||||
action?: string;
|
||||
},
|
||||
) => {
|
||||
const url = new URL(`${REACT_APP_SERVER_BASE_URL}${path}`);
|
||||
@ -513,6 +607,10 @@ export const useAuth = () => {
|
||||
);
|
||||
}
|
||||
|
||||
if (isDefined(params.action)) {
|
||||
url.searchParams.set('action', params.action);
|
||||
}
|
||||
|
||||
if (isDefined(workspacePublicData)) {
|
||||
url.searchParams.set('workspaceId', workspacePublicData.id);
|
||||
}
|
||||
@ -527,6 +625,7 @@ export const useAuth = () => {
|
||||
workspacePersonalInviteToken?: string;
|
||||
workspaceInviteHash?: string;
|
||||
billingCheckoutSession?: BillingCheckoutSession;
|
||||
action: string;
|
||||
}) => {
|
||||
redirect(buildRedirectUrl('/auth/google', params));
|
||||
},
|
||||
@ -538,6 +637,7 @@ export const useAuth = () => {
|
||||
workspacePersonalInviteToken?: string;
|
||||
workspaceInviteHash?: string;
|
||||
billingCheckoutSession?: BillingCheckoutSession;
|
||||
action: string;
|
||||
}) => {
|
||||
redirect(buildRedirectUrl('/auth/microsoft', params));
|
||||
},
|
||||
@ -556,8 +656,11 @@ export const useAuth = () => {
|
||||
clearSession,
|
||||
signOut: handleSignOut,
|
||||
signUpWithCredentials: handleCredentialsSignUp,
|
||||
signUpWithCredentialsInWorkspace: handleCredentialsSignUpInWorkspace,
|
||||
signInWithCredentialsInWorkspace: handleCredentialsSignInInWorkspace,
|
||||
signInWithCredentials: handleCredentialsSignIn,
|
||||
signInWithGoogle: handleGoogleLogin,
|
||||
signInWithMicrosoft: handleMicrosoftLogin,
|
||||
setAuthTokens: handleSetAuthTokens,
|
||||
};
|
||||
};
|
||||
|
||||
@ -2,14 +2,17 @@ import styled from '@emotion/styled';
|
||||
import { motion } from 'framer-motion';
|
||||
import { useState } from 'react';
|
||||
import { FormProvider } from 'react-hook-form';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { UndecoratedLink } from 'twenty-ui/navigation';
|
||||
import { useBuildWorkspaceUrl } from '@/domain-manager/hooks/useBuildWorkspaceUrl';
|
||||
import { availableWorkspacesState } from '@/auth/states/availableWorkspacesState';
|
||||
|
||||
import { useAuth } from '@/auth/hooks/useAuth';
|
||||
import { SignInUpEmailField } from '@/auth/sign-in-up/components/SignInUpEmailField';
|
||||
import { SignInUpPasswordField } from '@/auth/sign-in-up/components/SignInUpPasswordField';
|
||||
import { SignInUpWithGoogle } from '@/auth/sign-in-up/components/SignInUpWithGoogle';
|
||||
import { SignInUpWithMicrosoft } from '@/auth/sign-in-up/components/SignInUpWithMicrosoft';
|
||||
import { SignInUpEmailField } from '@/auth/sign-in-up/components/internal/SignInUpEmailField';
|
||||
import { SignInUpPasswordField } from '@/auth/sign-in-up/components/internal/SignInUpPasswordField';
|
||||
import { SignInUpWithGoogle } from '@/auth/sign-in-up/components/internal/SignInUpWithGoogle';
|
||||
import { SignInUpWithMicrosoft } from '@/auth/sign-in-up/components/internal/SignInUpWithMicrosoft';
|
||||
import { useSignInUp } from '@/auth/sign-in-up/hooks/useSignInUp';
|
||||
import { useSignInUpForm } from '@/auth/sign-in-up/hooks/useSignInUpForm';
|
||||
import { signInUpModeState } from '@/auth/states/signInUpModeState';
|
||||
@ -17,19 +20,23 @@ import {
|
||||
SignInUpStep,
|
||||
signInUpStepState,
|
||||
} from '@/auth/states/signInUpStepState';
|
||||
import { getAvailableWorkspacePathAndSearchParams } from '@/auth/utils/availableWorkspacesUtils';
|
||||
import { SignInUpMode } from '@/auth/types/signInUpMode';
|
||||
import { useReadCaptchaToken } from '@/captcha/hooks/useReadCaptchaToken';
|
||||
import { useRequestFreshCaptchaToken } from '@/captcha/hooks/useRequestFreshCaptchaToken';
|
||||
import { isRequestingCaptchaTokenState } from '@/captcha/states/isRequestingCaptchaTokenState';
|
||||
import { authProvidersState } from '@/client-config/states/authProvidersState';
|
||||
import { useRedirectToWorkspaceDomain } from '@/domain-manager/hooks/useRedirectToWorkspaceDomain';
|
||||
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { HorizontalSeparator } from 'twenty-ui/display';
|
||||
import {
|
||||
HorizontalSeparator,
|
||||
IconChevronRight,
|
||||
IconPlus,
|
||||
Avatar,
|
||||
} from 'twenty-ui/display';
|
||||
import { Loader } from 'twenty-ui/feedback';
|
||||
import { MainButton } from 'twenty-ui/input';
|
||||
import { getWorkspaceUrl } from '~/utils/getWorkspaceUrl';
|
||||
import { DEFAULT_WORKSPACE_LOGO } from '@/ui/navigation/navigation-drawer/constants/DefaultWorkspaceLogo';
|
||||
import { useSignUpInNewWorkspace } from '@/auth/sign-in-up/hooks/useSignUpInNewWorkspace';
|
||||
import { AvailableWorkspace } from '~/generated/graphql';
|
||||
|
||||
const StyledContentContainer = styled(motion.div)`
|
||||
margin-bottom: ${({ theme }) => theme.spacing(8)};
|
||||
@ -44,29 +51,101 @@ const StyledForm = styled.form`
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledWorkspaceContainer = styled.div`
|
||||
border: 1px solid ${({ theme }) => theme.border.color.light};
|
||||
border-radius: ${({ theme }) => theme.border.radius.md};
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: ${({ theme }) => theme.spacing(8)};
|
||||
margin-top: ${({ theme }) => theme.spacing(4)};
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledWorkspaceItem = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: ${({ theme }) => theme.spacing(15)};
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
|
||||
border-bottom: 1px solid ${({ theme }) => theme.border.color.light};
|
||||
cursor: pointer;
|
||||
justify-content: space-between;
|
||||
|
||||
&:hover {
|
||||
background-color: ${({ theme }) => theme.background.transparent.light};
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledWorkspaceContent = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: ${({ theme }) => theme.spacing(4)};
|
||||
width: 100%;
|
||||
padding: 0 ${({ theme }) => theme.spacing(4)};
|
||||
`;
|
||||
|
||||
const StyledWorkspaceTextContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
`;
|
||||
|
||||
const StyledWorkspaceLogo = styled.div`
|
||||
border-radius: ${({ theme }) => theme.border.radius.sm};
|
||||
height: ${({ theme }) => theme.spacing(6)};
|
||||
width: ${({ theme }) => theme.spacing(6)};
|
||||
background-color: ${({ theme }) => theme.background.transparent.light};
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
const StyledWorkspaceName = styled.div`
|
||||
color: ${({ theme }) => theme.font.color.primary};
|
||||
font-weight: ${({ theme }) => theme.font.weight.medium};
|
||||
padding-bottom: ${({ theme }) => theme.spacing(1)};
|
||||
`;
|
||||
|
||||
const StyledWorkspaceUrl = styled.div`
|
||||
color: ${({ theme }) => theme.font.color.tertiary};
|
||||
font-size: ${({ theme }) => theme.font.size.xs};
|
||||
`;
|
||||
|
||||
const StyledChevronIcon = styled.div`
|
||||
align-items: center;
|
||||
color: ${({ theme }) => theme.font.color.tertiary};
|
||||
display: flex;
|
||||
`;
|
||||
|
||||
export const SignInUpGlobalScopeForm = () => {
|
||||
const authProviders = useRecoilValue(authProvidersState);
|
||||
const signInUpStep = useRecoilValue(signInUpStepState);
|
||||
const { buildWorkspaceUrl } = useBuildWorkspaceUrl();
|
||||
|
||||
const { checkUserExists } = useAuth();
|
||||
const { readCaptchaToken } = useReadCaptchaToken();
|
||||
const { redirectToWorkspaceDomain } = useRedirectToWorkspaceDomain();
|
||||
const { createWorkspace } = useSignUpInNewWorkspace();
|
||||
const setSignInUpStep = useSetRecoilState(signInUpStepState);
|
||||
const [signInUpMode, setSignInUpMode] = useRecoilState(signInUpModeState);
|
||||
const [signInUpMode] = useRecoilState(signInUpModeState);
|
||||
const availableWorkspaces = useRecoilValue(availableWorkspacesState);
|
||||
const theme = useTheme();
|
||||
const { t } = useLingui();
|
||||
|
||||
const isRequestingCaptchaToken = useRecoilValue(
|
||||
isRequestingCaptchaTokenState,
|
||||
);
|
||||
|
||||
const { enqueueSnackBar } = useSnackBar();
|
||||
const { requestFreshCaptchaToken } = useRequestFreshCaptchaToken();
|
||||
|
||||
const [showErrors, setShowErrors] = useState(false);
|
||||
|
||||
const { form } = useSignInUpForm();
|
||||
const { pathname } = useLocation();
|
||||
|
||||
const { submitCredentials } = useSignInUp(form);
|
||||
const { submitCredentials, continueWithCredentials } = useSignInUp(form);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (isDefined(form?.formState?.errors?.email)) {
|
||||
@ -79,38 +158,7 @@ export const SignInUpGlobalScopeForm = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
const token = await readCaptchaToken();
|
||||
await checkUserExists.checkUserExistsQuery({
|
||||
variables: {
|
||||
email: form.getValues('email').toLowerCase().trim(),
|
||||
captchaToken: token,
|
||||
},
|
||||
onError: (error) => {
|
||||
enqueueSnackBar(`${error.message}`, {
|
||||
variant: SnackBarVariant.Error,
|
||||
});
|
||||
},
|
||||
onCompleted: async (data) => {
|
||||
requestFreshCaptchaToken();
|
||||
const response = data.checkUserExists;
|
||||
if (response.__typename === 'UserExists') {
|
||||
if (response.availableWorkspaces.length >= 1) {
|
||||
const workspace = response.availableWorkspaces[0];
|
||||
return await redirectToWorkspaceDomain(
|
||||
getWorkspaceUrl(workspace.workspaceUrls),
|
||||
pathname,
|
||||
{
|
||||
email: form.getValues('email'),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
if (response.__typename === 'UserNotExists') {
|
||||
setSignInUpMode(SignInUpMode.SignUp);
|
||||
setSignInUpStep(SignInUpStep.Password);
|
||||
}
|
||||
},
|
||||
});
|
||||
continueWithCredentials();
|
||||
};
|
||||
|
||||
const onEmailChange = (email: string) => {
|
||||
@ -119,42 +167,118 @@ export const SignInUpGlobalScopeForm = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const getAvailableWorkspaceUrl = (availableWorkspace: AvailableWorkspace) => {
|
||||
const { pathname, searchParams } = getAvailableWorkspacePathAndSearchParams(
|
||||
availableWorkspace,
|
||||
{ email: form.getValues('email') },
|
||||
);
|
||||
|
||||
return buildWorkspaceUrl(
|
||||
getWorkspaceUrl(availableWorkspace.workspaceUrls),
|
||||
pathname,
|
||||
searchParams,
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<StyledContentContainer>
|
||||
{authProviders.google && <SignInUpWithGoogle />}
|
||||
{authProviders.microsoft && <SignInUpWithMicrosoft />}
|
||||
{(authProviders.google || authProviders.microsoft) && (
|
||||
<HorizontalSeparator />
|
||||
)}
|
||||
{/* eslint-disable-next-line react/jsx-props-no-spreading */}
|
||||
<FormProvider {...form}>
|
||||
<StyledForm onSubmit={form.handleSubmit(handleSubmit)}>
|
||||
<SignInUpEmailField
|
||||
showErrors={showErrors}
|
||||
onInputChange={onEmailChange}
|
||||
/>
|
||||
{signInUpStep === SignInUpStep.Password && (
|
||||
<SignInUpPasswordField
|
||||
{signInUpStep === SignInUpStep.WorkspaceSelection && (
|
||||
<StyledWorkspaceContainer>
|
||||
{[
|
||||
...availableWorkspaces.availableWorkspacesForSignIn,
|
||||
...availableWorkspaces.availableWorkspacesForSignUp,
|
||||
].map((availableWorkspace) => (
|
||||
<UndecoratedLink
|
||||
key={availableWorkspace.id}
|
||||
to={getAvailableWorkspaceUrl(availableWorkspace)}
|
||||
>
|
||||
<StyledWorkspaceItem>
|
||||
<StyledWorkspaceContent>
|
||||
<Avatar
|
||||
placeholder={availableWorkspace.displayName || ''}
|
||||
avatarUrl={
|
||||
availableWorkspace.logo ?? DEFAULT_WORKSPACE_LOGO
|
||||
}
|
||||
size="lg"
|
||||
/>
|
||||
<StyledWorkspaceTextContainer>
|
||||
<StyledWorkspaceName>
|
||||
{availableWorkspace.displayName || availableWorkspace.id}
|
||||
</StyledWorkspaceName>
|
||||
<StyledWorkspaceUrl>
|
||||
{
|
||||
new URL(
|
||||
getWorkspaceUrl(availableWorkspace.workspaceUrls),
|
||||
).hostname
|
||||
}
|
||||
</StyledWorkspaceUrl>
|
||||
</StyledWorkspaceTextContainer>
|
||||
<StyledChevronIcon>
|
||||
<IconChevronRight size={theme.icon.size.md} />
|
||||
</StyledChevronIcon>
|
||||
</StyledWorkspaceContent>
|
||||
</StyledWorkspaceItem>
|
||||
</UndecoratedLink>
|
||||
))}
|
||||
<StyledWorkspaceItem onClick={() => createWorkspace()}>
|
||||
<StyledWorkspaceContent>
|
||||
<StyledWorkspaceLogo>
|
||||
<IconPlus size={theme.icon.size.lg} />
|
||||
</StyledWorkspaceLogo>
|
||||
<StyledWorkspaceTextContainer>
|
||||
<StyledWorkspaceName>{t`Create a workspace`}</StyledWorkspaceName>
|
||||
</StyledWorkspaceTextContainer>
|
||||
<StyledChevronIcon>
|
||||
<IconChevronRight size={theme.icon.size.md} />
|
||||
</StyledChevronIcon>
|
||||
</StyledWorkspaceContent>
|
||||
</StyledWorkspaceItem>
|
||||
</StyledWorkspaceContainer>
|
||||
)}
|
||||
{signInUpStep !== SignInUpStep.WorkspaceSelection && (
|
||||
<StyledContentContainer>
|
||||
{authProviders.google && (
|
||||
<SignInUpWithGoogle action="list-available-workspaces" />
|
||||
)}
|
||||
{authProviders.microsoft && (
|
||||
<SignInUpWithMicrosoft action="list-available-workspaces" />
|
||||
)}
|
||||
{(authProviders.google || authProviders.microsoft) && (
|
||||
<HorizontalSeparator />
|
||||
)}
|
||||
{/* eslint-disable-next-line react/jsx-props-no-spreading */}
|
||||
<FormProvider {...form}>
|
||||
<StyledForm onSubmit={form.handleSubmit(handleSubmit)}>
|
||||
<SignInUpEmailField
|
||||
showErrors={showErrors}
|
||||
signInUpMode={signInUpMode}
|
||||
onInputChange={onEmailChange}
|
||||
/>
|
||||
)}
|
||||
<MainButton
|
||||
disabled={isRequestingCaptchaToken}
|
||||
title={
|
||||
signInUpStep === SignInUpStep.Password ? 'Sign Up' : 'Continue'
|
||||
}
|
||||
type="submit"
|
||||
variant={
|
||||
signInUpStep === SignInUpStep.Init ? 'secondary' : 'primary'
|
||||
}
|
||||
Icon={() => (form.formState.isSubmitting ? <Loader /> : null)}
|
||||
fullWidth
|
||||
/>
|
||||
</StyledForm>
|
||||
</FormProvider>
|
||||
</StyledContentContainer>
|
||||
{signInUpStep === SignInUpStep.Password && (
|
||||
<SignInUpPasswordField
|
||||
showErrors={showErrors}
|
||||
signInUpMode={signInUpMode}
|
||||
/>
|
||||
)}
|
||||
<MainButton
|
||||
disabled={isRequestingCaptchaToken}
|
||||
title={
|
||||
signInUpStep === SignInUpStep.Password
|
||||
? signInUpMode === SignInUpMode.SignIn
|
||||
? t`Sign In`
|
||||
: t`Sign Up`
|
||||
: t`Continue`
|
||||
}
|
||||
type="submit"
|
||||
variant={
|
||||
signInUpStep === SignInUpStep.Init ? 'secondary' : 'primary'
|
||||
}
|
||||
Icon={() => (form.formState.isSubmitting ? <Loader /> : null)}
|
||||
fullWidth
|
||||
/>
|
||||
</StyledForm>
|
||||
</FormProvider>
|
||||
</StyledContentContainer>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { SignInUpWithCredentials } from '@/auth/sign-in-up/components/SignInUpWithCredentials';
|
||||
import { SignInUpWithGoogle } from '@/auth/sign-in-up/components/SignInUpWithGoogle';
|
||||
import { SignInUpWithMicrosoft } from '@/auth/sign-in-up/components/SignInUpWithMicrosoft';
|
||||
import { SignInUpWithSSO } from '@/auth/sign-in-up/components/SignInUpWithSSO';
|
||||
import { SignInUpWithCredentials } from '@/auth/sign-in-up/components/internal/SignInUpWithCredentials';
|
||||
import { SignInUpWithGoogle } from '@/auth/sign-in-up/components/internal/SignInUpWithGoogle';
|
||||
import { SignInUpWithMicrosoft } from '@/auth/sign-in-up/components/internal/SignInUpWithMicrosoft';
|
||||
import { SignInUpWithSSO } from '@/auth/sign-in-up/components/internal/SignInUpWithSSO';
|
||||
import { useHandleResetPassword } from '@/auth/sign-in-up/hooks/useHandleResetPassword';
|
||||
import { useSignInUp } from '@/auth/sign-in-up/hooks/useSignInUp';
|
||||
import { useSignInUpForm } from '@/auth/sign-in-up/hooks/useSignInUpForm';
|
||||
@ -36,9 +36,13 @@ export const SignInUpWorkspaceScopeForm = () => {
|
||||
return (
|
||||
<>
|
||||
<StyledContentContainer>
|
||||
{workspaceAuthProviders.google && <SignInUpWithGoogle />}
|
||||
{workspaceAuthProviders.google && (
|
||||
<SignInUpWithGoogle action="join-workspace" />
|
||||
)}
|
||||
|
||||
{workspaceAuthProviders.microsoft && <SignInUpWithMicrosoft />}
|
||||
{workspaceAuthProviders.microsoft && (
|
||||
<SignInUpWithMicrosoft action="join-workspace" />
|
||||
)}
|
||||
|
||||
{workspaceAuthProviders.sso.length > 0 && <SignInUpWithSSO />}
|
||||
|
||||
|
||||
@ -0,0 +1,34 @@
|
||||
import {
|
||||
SignInUpStep,
|
||||
signInUpStepState,
|
||||
} from '@/auth/states/signInUpStepState';
|
||||
import { useEffect } from 'react';
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { useAuth } from '@/auth/hooks/useAuth';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
|
||||
export const SignInUpGlobalScopeFormEffect = () => {
|
||||
const setSignInUpStep = useSetRecoilState(signInUpStepState);
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const { setAuthTokens, loadCurrentUser } = useAuth();
|
||||
|
||||
useEffect(() => {
|
||||
const tokenPair = searchParams.get('tokenPair');
|
||||
if (isDefined(tokenPair)) {
|
||||
setAuthTokens(JSON.parse(tokenPair));
|
||||
searchParams.delete('tokenPair');
|
||||
setSearchParams(searchParams);
|
||||
loadCurrentUser();
|
||||
setSignInUpStep(SignInUpStep.WorkspaceSelection);
|
||||
}
|
||||
}, [
|
||||
searchParams,
|
||||
setSearchParams,
|
||||
setSignInUpStep,
|
||||
loadCurrentUser,
|
||||
setAuthTokens,
|
||||
]);
|
||||
|
||||
return <></>;
|
||||
};
|
||||
@ -5,8 +5,8 @@ import {
|
||||
signInUpStepState,
|
||||
} from '@/auth/states/signInUpStepState';
|
||||
|
||||
import { SignInUpEmailField } from '@/auth/sign-in-up/components/SignInUpEmailField';
|
||||
import { SignInUpPasswordField } from '@/auth/sign-in-up/components/SignInUpPasswordField';
|
||||
import { SignInUpEmailField } from '@/auth/sign-in-up/components/internal/SignInUpEmailField';
|
||||
import { SignInUpPasswordField } from '@/auth/sign-in-up/components/internal/SignInUpPasswordField';
|
||||
import { SignInUpMode } from '@/auth/types/signInUpMode';
|
||||
import { isRequestingCaptchaTokenState } from '@/captcha/states/isRequestingCaptchaTokenState';
|
||||
import { captchaState } from '@/client-config/states/captchaState';
|
||||
@ -9,23 +9,27 @@ import { memo } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { HorizontalSeparator, IconGoogle } from 'twenty-ui/display';
|
||||
import { MainButton } from 'twenty-ui/input';
|
||||
import { SocialSSOSignInUpActionType } from '@/auth/types/socialSSOSignInUp.type';
|
||||
|
||||
const GoogleIcon = memo(() => {
|
||||
const theme = useTheme();
|
||||
return <IconGoogle size={theme.icon.size.md} />;
|
||||
});
|
||||
|
||||
export const SignInUpWithGoogle = () => {
|
||||
export const SignInUpWithGoogle = ({
|
||||
action,
|
||||
}: {
|
||||
action: SocialSSOSignInUpActionType;
|
||||
}) => {
|
||||
const { t } = useLingui();
|
||||
const signInUpStep = useRecoilValue(signInUpStepState);
|
||||
const { signInWithGoogle } = useSignInWithGoogle();
|
||||
|
||||
return (
|
||||
<>
|
||||
<MainButton
|
||||
Icon={GoogleIcon}
|
||||
title={t`Continue with Google`}
|
||||
onClick={signInWithGoogle}
|
||||
onClick={() => signInWithGoogle({ action })}
|
||||
variant={signInUpStep === SignInUpStep.Init ? undefined : 'secondary'}
|
||||
fullWidth
|
||||
/>
|
||||
@ -8,8 +8,13 @@ import { useLingui } from '@lingui/react/macro';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { HorizontalSeparator, IconMicrosoft } from 'twenty-ui/display';
|
||||
import { MainButton } from 'twenty-ui/input';
|
||||
import { SocialSSOSignInUpActionType } from '@/auth/types/socialSSOSignInUp.type';
|
||||
|
||||
export const SignInUpWithMicrosoft = () => {
|
||||
export const SignInUpWithMicrosoft = ({
|
||||
action,
|
||||
}: {
|
||||
action: SocialSSOSignInUpActionType;
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const { t } = useLingui();
|
||||
|
||||
@ -21,7 +26,7 @@ export const SignInUpWithMicrosoft = () => {
|
||||
<MainButton
|
||||
Icon={() => <IconMicrosoft size={theme.icon.size.md} />}
|
||||
title={t`Continue with Microsoft`}
|
||||
onClick={signInWithMicrosoft}
|
||||
onClick={() => signInWithMicrosoft({ action })}
|
||||
variant={signInUpStep === SignInUpStep.Init ? undefined : 'secondary'}
|
||||
fullWidth
|
||||
/>
|
||||
@ -43,9 +43,12 @@ describe('useSignInWithGoogle', () => {
|
||||
const { result } = renderHook(() => useSignInWithGoogle(), {
|
||||
wrapper: Wrapper,
|
||||
});
|
||||
result.current.signInWithGoogle();
|
||||
result.current.signInWithGoogle({
|
||||
action: 'join-workspace',
|
||||
});
|
||||
|
||||
expect(signInWithGoogleMock).toHaveBeenCalledWith({
|
||||
action: 'join-workspace',
|
||||
workspaceInviteHash: 'testHash',
|
||||
workspacePersonalInviteToken: 'testToken',
|
||||
billingCheckoutSession: mockBillingCheckoutSession,
|
||||
@ -66,9 +69,12 @@ describe('useSignInWithGoogle', () => {
|
||||
const { result } = renderHook(() => useSignInWithGoogle(), {
|
||||
wrapper: Wrapper,
|
||||
});
|
||||
result.current.signInWithGoogle();
|
||||
result.current.signInWithGoogle({
|
||||
action: 'join-workspace',
|
||||
});
|
||||
|
||||
expect(signInWithGoogleMock).toHaveBeenCalledWith({
|
||||
action: 'join-workspace',
|
||||
workspaceInviteHash: 'testHash',
|
||||
workspacePersonalInviteToken: undefined,
|
||||
billingCheckoutSession: mockBillingCheckoutSession,
|
||||
|
||||
@ -43,9 +43,12 @@ describe('useSignInWithMicrosoft', () => {
|
||||
const { result } = renderHook(() => useSignInWithMicrosoft(), {
|
||||
wrapper: Wrapper,
|
||||
});
|
||||
result.current.signInWithMicrosoft();
|
||||
result.current.signInWithMicrosoft({
|
||||
action: 'join-workspace',
|
||||
});
|
||||
|
||||
expect(signInWithMicrosoftMock).toHaveBeenCalledWith({
|
||||
action: 'join-workspace',
|
||||
workspaceInviteHash: workspaceInviteHashMock,
|
||||
workspacePersonalInviteToken: inviteTokenMock,
|
||||
billingCheckoutSession: mockBillingCheckoutSession,
|
||||
@ -67,9 +70,12 @@ describe('useSignInWithMicrosoft', () => {
|
||||
const { result } = renderHook(() => useSignInWithMicrosoft(), {
|
||||
wrapper: Wrapper,
|
||||
});
|
||||
result.current.signInWithMicrosoft();
|
||||
result.current.signInWithMicrosoft({
|
||||
action: 'join-workspace',
|
||||
});
|
||||
|
||||
expect(signInWithMicrosoftMock).toHaveBeenCalledWith({
|
||||
action: 'join-workspace',
|
||||
billingCheckoutSession: mockBillingCheckoutSession,
|
||||
workspaceInviteHash: workspaceInviteHashMock,
|
||||
workspacePersonalInviteToken: undefined,
|
||||
|
||||
@ -19,12 +19,14 @@ import { useRecoilState } from 'recoil';
|
||||
import { buildAppPathWithQueryParams } from '~/utils/buildAppPathWithQueryParams';
|
||||
import { isMatchingLocation } from '~/utils/isMatchingLocation';
|
||||
import { useAuth } from '../../hooks/useAuth';
|
||||
import { useIsCurrentLocationOnAWorkspace } from '@/domain-manager/hooks/useIsCurrentLocationOnAWorkspace';
|
||||
|
||||
export const useSignInUp = (form: UseFormReturn<Form>) => {
|
||||
const { enqueueSnackBar } = useSnackBar();
|
||||
|
||||
const [signInUpStep, setSignInUpStep] = useRecoilState(signInUpStepState);
|
||||
const [signInUpMode, setSignInUpMode] = useRecoilState(signInUpModeState);
|
||||
const { isOnAWorkspace } = useIsCurrentLocationOnAWorkspace();
|
||||
|
||||
const location = useLocation();
|
||||
|
||||
@ -38,7 +40,9 @@ export const useSignInUp = (form: UseFormReturn<Form>) => {
|
||||
);
|
||||
|
||||
const {
|
||||
signInWithCredentialsInWorkspace,
|
||||
signInWithCredentials,
|
||||
signUpWithCredentialsInWorkspace,
|
||||
signUpWithCredentials,
|
||||
checkUserExists: { checkUserExistsQuery },
|
||||
} = useAuth();
|
||||
@ -71,11 +75,11 @@ export const useSignInUp = (form: UseFormReturn<Form>) => {
|
||||
},
|
||||
onCompleted: (data) => {
|
||||
requestFreshCaptchaToken();
|
||||
if (data?.checkUserExists.exists) {
|
||||
setSignInUpMode(SignInUpMode.SignIn);
|
||||
} else {
|
||||
setSignInUpMode(SignInUpMode.SignUp);
|
||||
}
|
||||
setSignInUpMode(
|
||||
data?.checkUserExists.exists
|
||||
? SignInUpMode.SignIn
|
||||
: SignInUpMode.SignUp,
|
||||
);
|
||||
setSignInUpStep(SignInUpStep.Password);
|
||||
},
|
||||
});
|
||||
@ -97,31 +101,60 @@ export const useSignInUp = (form: UseFormReturn<Form>) => {
|
||||
throw new Error('Email and password are required');
|
||||
}
|
||||
|
||||
if (signInUpMode === SignInUpMode.SignIn && !isInviteMode) {
|
||||
await signInWithCredentials(
|
||||
if (
|
||||
!isInviteMode &&
|
||||
signInUpMode === SignInUpMode.SignIn &&
|
||||
isOnAWorkspace
|
||||
) {
|
||||
return await signInWithCredentialsInWorkspace(
|
||||
data.email.toLowerCase().trim(),
|
||||
data.password,
|
||||
token,
|
||||
);
|
||||
} else {
|
||||
const verifyEmailNextPath = buildAppPathWithQueryParams(
|
||||
AppPath.PlanRequired,
|
||||
await buildSearchParamsFromUrlSyncedStates(),
|
||||
);
|
||||
|
||||
await signUpWithCredentials({
|
||||
email: data.email.toLowerCase().trim(),
|
||||
password: data.password,
|
||||
workspaceInviteHash,
|
||||
workspacePersonalInviteToken,
|
||||
captchaToken: token,
|
||||
verifyEmailNextPath,
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
!isInviteMode &&
|
||||
signInUpMode === SignInUpMode.SignIn &&
|
||||
!isOnAWorkspace
|
||||
) {
|
||||
return await signInWithCredentials(
|
||||
data.email.toLowerCase().trim(),
|
||||
data.password,
|
||||
token,
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
!isInviteMode &&
|
||||
signInUpMode === SignInUpMode.SignUp &&
|
||||
!isOnAWorkspace
|
||||
) {
|
||||
return await signUpWithCredentials(
|
||||
data.email.toLowerCase().trim(),
|
||||
data.password,
|
||||
token,
|
||||
);
|
||||
}
|
||||
|
||||
const verifyEmailNextPath = buildAppPathWithQueryParams(
|
||||
AppPath.PlanRequired,
|
||||
await buildSearchParamsFromUrlSyncedStates(),
|
||||
);
|
||||
|
||||
await signUpWithCredentialsInWorkspace({
|
||||
email: data.email.toLowerCase().trim(),
|
||||
password: data.password,
|
||||
workspaceInviteHash,
|
||||
workspacePersonalInviteToken,
|
||||
captchaToken: token,
|
||||
verifyEmailNextPath,
|
||||
});
|
||||
} catch (err: any) {
|
||||
enqueueSnackBar(err?.message, {
|
||||
variant: SnackBarVariant.Error,
|
||||
});
|
||||
} finally {
|
||||
requestFreshCaptchaToken();
|
||||
}
|
||||
},
|
||||
@ -129,13 +162,16 @@ export const useSignInUp = (form: UseFormReturn<Form>) => {
|
||||
readCaptchaToken,
|
||||
signInUpMode,
|
||||
isInviteMode,
|
||||
signInWithCredentialsInWorkspace,
|
||||
signInWithCredentials,
|
||||
signUpWithCredentials,
|
||||
signUpWithCredentialsInWorkspace,
|
||||
workspaceInviteHash,
|
||||
workspacePersonalInviteToken,
|
||||
enqueueSnackBar,
|
||||
requestFreshCaptchaToken,
|
||||
buildSearchParamsFromUrlSyncedStates,
|
||||
isOnAWorkspace,
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useEffect } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useLocation, useSearchParams } from 'react-router-dom';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { z } from 'zod';
|
||||
|
||||
@ -30,7 +30,6 @@ const makeValidationSchema = (signInUpStep: SignInUpStep) =>
|
||||
|
||||
export type Form = z.infer<ReturnType<typeof makeValidationSchema>>;
|
||||
export const useSignInUpForm = () => {
|
||||
const location = useLocation();
|
||||
const signInUpStep = useRecoilValue(signInUpStepState);
|
||||
|
||||
const validationSchema = makeValidationSchema(signInUpStep); // Create schema based on the current step
|
||||
@ -61,11 +60,6 @@ export const useSignInUpForm = () => {
|
||||
form.setValue('email', prefilledEmail ?? 'tim@apple.dev');
|
||||
form.setValue('password', 'tim@apple.dev');
|
||||
}
|
||||
}, [
|
||||
form,
|
||||
isDeveloperDefaultSignInPrefilled,
|
||||
prefilledEmail,
|
||||
location.search,
|
||||
]);
|
||||
}, [form, isDeveloperDefaultSignInPrefilled, prefilledEmail]);
|
||||
return { form: form };
|
||||
};
|
||||
|
||||
@ -2,6 +2,7 @@ import { useParams, useSearchParams } from 'react-router-dom';
|
||||
|
||||
import { useAuth } from '@/auth/hooks/useAuth';
|
||||
import { BillingCheckoutSession } from '@/auth/types/billingCheckoutSession.type';
|
||||
import { SocialSSOSignInUpActionType } from '@/auth/types/socialSSOSignInUp.type';
|
||||
|
||||
export const useSignInWithGoogle = () => {
|
||||
const workspaceInviteHash = useParams().workspaceInviteHash;
|
||||
@ -15,12 +16,14 @@ export const useSignInWithGoogle = () => {
|
||||
} as BillingCheckoutSession;
|
||||
|
||||
const { signInWithGoogle } = useAuth();
|
||||
|
||||
return {
|
||||
signInWithGoogle: () =>
|
||||
signInWithGoogle: ({ action }: { action: SocialSSOSignInUpActionType }) =>
|
||||
signInWithGoogle({
|
||||
workspaceInviteHash,
|
||||
workspacePersonalInviteToken,
|
||||
billingCheckoutSession,
|
||||
action,
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
@ -3,6 +3,7 @@ import { useParams, useSearchParams } from 'react-router-dom';
|
||||
import { useAuth } from '@/auth/hooks/useAuth';
|
||||
import { billingCheckoutSessionState } from '@/auth/states/billingCheckoutSessionState';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { SocialSSOSignInUpActionType } from '@/auth/types/socialSSOSignInUp.type';
|
||||
|
||||
export const useSignInWithMicrosoft = () => {
|
||||
const workspaceInviteHash = useParams().workspaceInviteHash;
|
||||
@ -13,11 +14,16 @@ export const useSignInWithMicrosoft = () => {
|
||||
|
||||
const { signInWithMicrosoft } = useAuth();
|
||||
return {
|
||||
signInWithMicrosoft: () =>
|
||||
signInWithMicrosoft: ({
|
||||
action,
|
||||
}: {
|
||||
action: SocialSSOSignInUpActionType;
|
||||
}) =>
|
||||
signInWithMicrosoft({
|
||||
workspaceInviteHash,
|
||||
workspacePersonalInviteToken,
|
||||
billingCheckoutSession,
|
||||
action,
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
@ -0,0 +1,37 @@
|
||||
import { getWorkspaceUrl } from '~/utils/getWorkspaceUrl';
|
||||
import { AppPath } from '@/types/AppPath';
|
||||
import { useSignUpInNewWorkspaceMutation } from '~/generated/graphql';
|
||||
import { useRedirectToWorkspaceDomain } from '@/domain-manager/hooks/useRedirectToWorkspaceDomain';
|
||||
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
|
||||
export const useSignUpInNewWorkspace = () => {
|
||||
const { redirectToWorkspaceDomain } = useRedirectToWorkspaceDomain();
|
||||
const { enqueueSnackBar } = useSnackBar();
|
||||
|
||||
const [signUpInNewWorkspaceMutation] = useSignUpInNewWorkspaceMutation();
|
||||
|
||||
const createWorkspace = () => {
|
||||
signUpInNewWorkspaceMutation({
|
||||
onCompleted: async (data) => {
|
||||
return await redirectToWorkspaceDomain(
|
||||
getWorkspaceUrl(data.signUpInNewWorkspace.workspace.workspaceUrls),
|
||||
AppPath.Verify,
|
||||
{
|
||||
loginToken: data.signUpInNewWorkspace.loginToken.token,
|
||||
},
|
||||
'_blank',
|
||||
);
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
enqueueSnackBar(error.message, {
|
||||
variant: SnackBarVariant.Error,
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
createWorkspace,
|
||||
};
|
||||
};
|
||||
@ -1,9 +0,0 @@
|
||||
import { UserExists } from '~/generated/graphql';
|
||||
import { createState } from 'twenty-ui/utilities';
|
||||
|
||||
export const availableSSOIdentityProvidersForAuthState = createState<
|
||||
NonNullable<UserExists['availableWorkspaces']>[0]['sso']
|
||||
>({
|
||||
key: 'availableSSOIdentityProvidersForAuth',
|
||||
defaultValue: [],
|
||||
});
|
||||
@ -0,0 +1,10 @@
|
||||
import { createState } from 'twenty-ui/utilities';
|
||||
import { AvailableWorkspaces } from '~/generated/graphql';
|
||||
|
||||
export const availableWorkspacesState = createState<AvailableWorkspaces>({
|
||||
key: 'availableWorkspacesState',
|
||||
defaultValue: {
|
||||
availableWorkspacesForSignIn: [],
|
||||
availableWorkspacesForSignUp: [],
|
||||
},
|
||||
});
|
||||
@ -1,12 +0,0 @@
|
||||
import { Workspace } from '~/generated/graphql';
|
||||
import { createState } from 'twenty-ui/utilities';
|
||||
|
||||
export type Workspaces = Pick<
|
||||
Workspace,
|
||||
'id' | 'logo' | 'displayName' | 'workspaceUrls'
|
||||
>[];
|
||||
|
||||
export const workspacesState = createState<Workspaces>({
|
||||
key: 'workspacesState',
|
||||
defaultValue: [],
|
||||
});
|
||||
@ -0,0 +1,4 @@
|
||||
export type SocialSSOSignInUpActionType =
|
||||
| 'create-new-workspace'
|
||||
| 'list-available-workspaces'
|
||||
| 'join-workspace';
|
||||
@ -0,0 +1,70 @@
|
||||
import { AvailableWorkspaces, AvailableWorkspace } from '~/generated/graphql';
|
||||
import { AppPath } from '@/types/AppPath';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { generatePath } from 'react-router-dom';
|
||||
|
||||
export const countAvailableWorkspaces = ({
|
||||
availableWorkspacesForSignIn,
|
||||
availableWorkspacesForSignUp,
|
||||
}: AvailableWorkspaces): number => {
|
||||
return (
|
||||
availableWorkspacesForSignIn.length + availableWorkspacesForSignUp.length
|
||||
);
|
||||
};
|
||||
|
||||
export const getFirstAvailableWorkspaces = ({
|
||||
availableWorkspacesForSignIn,
|
||||
availableWorkspacesForSignUp,
|
||||
}: AvailableWorkspaces): AvailableWorkspace => {
|
||||
return availableWorkspacesForSignIn[0] ?? availableWorkspacesForSignUp[0];
|
||||
};
|
||||
|
||||
const getAvailableWorkspacePathname = (
|
||||
availableWorkspace: AvailableWorkspace,
|
||||
) => {
|
||||
if (isDefined(availableWorkspace.loginToken)) {
|
||||
return AppPath.Verify;
|
||||
}
|
||||
|
||||
if (
|
||||
isDefined(availableWorkspace.personalInviteToken) &&
|
||||
isDefined(availableWorkspace.inviteHash)
|
||||
) {
|
||||
return generatePath(AppPath.Invite, {
|
||||
workspaceInviteHash: availableWorkspace.inviteHash,
|
||||
});
|
||||
}
|
||||
|
||||
return AppPath.SignInUp;
|
||||
};
|
||||
|
||||
const getAvailableWorkspaceSearchParams = (
|
||||
availableWorkspace: AvailableWorkspace,
|
||||
defaultSearchParams: Record<string, string> = {},
|
||||
) => {
|
||||
const searchParams: Record<string, string> = defaultSearchParams;
|
||||
|
||||
if (isDefined(availableWorkspace.loginToken)) {
|
||||
searchParams.loginToken = availableWorkspace.loginToken;
|
||||
return searchParams;
|
||||
}
|
||||
|
||||
if (isDefined(availableWorkspace.personalInviteToken)) {
|
||||
searchParams.inviteToken = availableWorkspace.personalInviteToken;
|
||||
}
|
||||
|
||||
return searchParams;
|
||||
};
|
||||
|
||||
export const getAvailableWorkspacePathAndSearchParams = (
|
||||
availableWorkspace: AvailableWorkspace,
|
||||
defaultSearchParams: Record<string, string> = {},
|
||||
): { pathname: string; searchParams: Record<string, string> } => {
|
||||
return {
|
||||
pathname: getAvailableWorkspacePathname(availableWorkspace),
|
||||
searchParams: getAvailableWorkspaceSearchParams(
|
||||
availableWorkspace,
|
||||
defaultSearchParams,
|
||||
),
|
||||
};
|
||||
};
|
||||
@ -0,0 +1,43 @@
|
||||
import { act, renderHook } from '@testing-library/react';
|
||||
import { RecoilRoot } from 'recoil';
|
||||
|
||||
import { captchaTokenState } from '@/captcha/states/captchaTokenState';
|
||||
import { useReadCaptchaToken } from '@/captcha/hooks/useReadCaptchaToken';
|
||||
|
||||
describe('useReadCaptchaToken', () => {
|
||||
it('should return undefined when no token exists', async () => {
|
||||
const { result } = renderHook(() => useReadCaptchaToken(), {
|
||||
wrapper: RecoilRoot,
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
const token = await result.current.readCaptchaToken();
|
||||
expect(token).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
it('should return the token when it exists', async () => {
|
||||
const { result } = renderHook(
|
||||
() => {
|
||||
const hook = useReadCaptchaToken();
|
||||
return hook;
|
||||
},
|
||||
{
|
||||
wrapper: ({ children }) => (
|
||||
<RecoilRoot
|
||||
initializeState={({ set }) => {
|
||||
set(captchaTokenState, 'test-token');
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</RecoilRoot>
|
||||
),
|
||||
},
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
const token = await result.current.readCaptchaToken();
|
||||
expect(token).toBe('test-token');
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,66 @@
|
||||
import { act, renderHook } from '@testing-library/react';
|
||||
import { RecoilRoot } from 'recoil';
|
||||
import * as ReactRouterDom from 'react-router-dom';
|
||||
|
||||
import { useRequestFreshCaptchaToken } from '@/captcha/hooks/useRequestFreshCaptchaToken';
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useLocation: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('useRequestFreshCaptchaToken', () => {
|
||||
beforeEach(() => {
|
||||
// Mock useLocation to return a path that requires captcha
|
||||
(ReactRouterDom.useLocation as jest.Mock).mockReturnValue({
|
||||
pathname: '/sign-in',
|
||||
});
|
||||
|
||||
// Mock window.grecaptcha
|
||||
window.grecaptcha = {
|
||||
execute: jest.fn().mockImplementation(() => {
|
||||
return Promise.resolve('google-recaptcha-token');
|
||||
}),
|
||||
};
|
||||
|
||||
// Mock window.turnstile
|
||||
window.turnstile = {
|
||||
render: jest.fn().mockReturnValue('turnstile-widget-id'),
|
||||
execute: jest.fn().mockImplementation((widgetId, options) => {
|
||||
return options?.callback('turnstile-token');
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
delete window.grecaptcha;
|
||||
delete window.turnstile;
|
||||
});
|
||||
|
||||
it('should not request a token if captcha is not required for the path', async () => {
|
||||
const { result } = renderHook(() => useRequestFreshCaptchaToken(), {
|
||||
wrapper: RecoilRoot,
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await result.current.requestFreshCaptchaToken();
|
||||
});
|
||||
|
||||
expect(window.grecaptcha.execute).not.toHaveBeenCalled();
|
||||
expect(window.turnstile.execute).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not request a token if captcha provider is not defined', async () => {
|
||||
const { result } = renderHook(() => useRequestFreshCaptchaToken(), {
|
||||
wrapper: RecoilRoot,
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await result.current.requestFreshCaptchaToken();
|
||||
});
|
||||
|
||||
expect(window.grecaptcha.execute).not.toHaveBeenCalled();
|
||||
expect(window.turnstile.execute).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@ -18,8 +18,9 @@ export const useIsCurrentLocationOnAWorkspace = () => {
|
||||
throw new Error('frontDomain and defaultSubdomain are required');
|
||||
}
|
||||
|
||||
const isOnAWorkspace =
|
||||
isMultiWorkspaceEnabled && window.location.hostname !== defaultDomain;
|
||||
const isOnAWorkspace = !isMultiWorkspaceEnabled
|
||||
? true
|
||||
: window.location.hostname !== defaultDomain;
|
||||
|
||||
return {
|
||||
isOnAWorkspace,
|
||||
|
||||
@ -150,8 +150,7 @@ export const queries = {
|
||||
hasValidEnterpriseKey
|
||||
customDomain
|
||||
workspaceUrls {
|
||||
subdomainUrl
|
||||
customUrl
|
||||
...WorkspaceUrlsFragment
|
||||
}
|
||||
featureFlags {
|
||||
id
|
||||
@ -179,8 +178,7 @@ export const queries = {
|
||||
subdomain
|
||||
customDomain
|
||||
workspaceUrls {
|
||||
subdomainUrl
|
||||
customUrl
|
||||
...WorkspaceUrlsFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -14,6 +14,7 @@ import { currentUserState } from '@/auth/states/currentUserState';
|
||||
import { billingState } from '@/client-config/states/billingState';
|
||||
import { labPublicFeatureFlagsState } from '@/client-config/states/labPublicFeatureFlagsState';
|
||||
import { useSettingsPermissionMap } from '@/settings/roles/hooks/useSettingsPermissionMap';
|
||||
import { SnackBarProviderScope } from '@/ui/feedback/snack-bar-manager/scopes/SnackBarProviderScope';
|
||||
|
||||
const mockCurrentUser = {
|
||||
id: 'fake-user-id',
|
||||
@ -41,7 +42,11 @@ const initializeState = ({ set }: MutableSnapshot) => {
|
||||
const Wrapper = ({ children }: { children: ReactNode }) => (
|
||||
<MockedProvider>
|
||||
<RecoilRoot initializeState={initializeState}>
|
||||
<MemoryRouter>{children}</MemoryRouter>
|
||||
<MemoryRouter>
|
||||
<SnackBarProviderScope snackBarManagerScopeId="test-scope-id">
|
||||
{children}
|
||||
</SnackBarProviderScope>
|
||||
</MemoryRouter>
|
||||
</RecoilRoot>
|
||||
</MockedProvider>
|
||||
);
|
||||
|
||||
@ -2,7 +2,6 @@ import { DEFAULT_WORKSPACE_LOGO } from '@/ui/navigation/navigation-drawer/consta
|
||||
|
||||
import { useAuth } from '@/auth/hooks/useAuth';
|
||||
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
||||
import { Workspaces, workspacesState } from '@/auth/states/workspaces';
|
||||
import { useBuildWorkspaceUrl } from '@/domain-manager/hooks/useBuildWorkspaceUrl';
|
||||
import { useRedirectToWorkspaceDomain } from '@/domain-manager/hooks/useRedirectToWorkspaceDomain';
|
||||
import { AppPath } from '@/types/AppPath';
|
||||
@ -37,9 +36,14 @@ import {
|
||||
MenuItemSelectAvatar,
|
||||
UndecoratedLink,
|
||||
} from 'twenty-ui/navigation';
|
||||
import { useSignUpInNewWorkspaceMutation } from '~/generated/graphql';
|
||||
import {
|
||||
useSignUpInNewWorkspaceMutation,
|
||||
AvailableWorkspace,
|
||||
} from '~/generated/graphql';
|
||||
import { getWorkspaceUrl } from '~/utils/getWorkspaceUrl';
|
||||
import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
|
||||
import { availableWorkspacesState } from '@/auth/states/availableWorkspacesState';
|
||||
import { countAvailableWorkspaces } from '@/auth/utils/availableWorkspacesUtils';
|
||||
|
||||
const StyledDescription = styled.div`
|
||||
color: ${({ theme }) => theme.font.color.light};
|
||||
@ -50,7 +54,9 @@ export const MultiWorkspaceDropdownDefaultComponents = () => {
|
||||
const currentWorkspace = useRecoilValue(currentWorkspaceState);
|
||||
const { t } = useLingui();
|
||||
const { redirectToWorkspaceDomain } = useRedirectToWorkspaceDomain();
|
||||
const workspaces = useRecoilValue(workspacesState);
|
||||
const availableWorkspaces = useRecoilValue(availableWorkspacesState);
|
||||
const availableWorkspacesCount =
|
||||
countAvailableWorkspaces(availableWorkspaces);
|
||||
const { buildWorkspaceUrl } = useBuildWorkspaceUrl();
|
||||
const { closeDropdown } = useDropdown(MULTI_WORKSPACE_DROPDOWN_ID);
|
||||
const { signOut } = useAuth();
|
||||
@ -63,8 +69,10 @@ export const MultiWorkspaceDropdownDefaultComponents = () => {
|
||||
multiWorkspaceDropdownState,
|
||||
);
|
||||
|
||||
const handleChange = async (workspace: Workspaces[0]) => {
|
||||
redirectToWorkspaceDomain(getWorkspaceUrl(workspace.workspaceUrls));
|
||||
const handleChange = async (availableWorkspace: AvailableWorkspace) => {
|
||||
redirectToWorkspaceDomain(
|
||||
getWorkspaceUrl(availableWorkspace.workspaceUrls),
|
||||
);
|
||||
};
|
||||
|
||||
const createWorkspace = () => {
|
||||
@ -127,36 +135,41 @@ export const MultiWorkspaceDropdownDefaultComponents = () => {
|
||||
>
|
||||
{currentWorkspace?.displayName}
|
||||
</DropdownMenuHeader>
|
||||
{workspaces.length > 1 && (
|
||||
{availableWorkspacesCount > 1 && (
|
||||
<>
|
||||
<DropdownMenuItemsContainer>
|
||||
{workspaces
|
||||
{[
|
||||
...availableWorkspaces.availableWorkspacesForSignIn,
|
||||
...availableWorkspaces.availableWorkspacesForSignUp,
|
||||
]
|
||||
.filter(({ id }) => id !== currentWorkspace?.id)
|
||||
.slice(0, 3)
|
||||
.map((workspace) => (
|
||||
.map((availableWorkspace) => (
|
||||
<UndecoratedLink
|
||||
key={workspace.id}
|
||||
key={availableWorkspace.id}
|
||||
to={buildWorkspaceUrl(
|
||||
getWorkspaceUrl(workspace.workspaceUrls),
|
||||
getWorkspaceUrl(availableWorkspace.workspaceUrls),
|
||||
)}
|
||||
onClick={(event) => {
|
||||
event?.preventDefault();
|
||||
handleChange(workspace);
|
||||
handleChange(availableWorkspace);
|
||||
}}
|
||||
>
|
||||
<MenuItemSelectAvatar
|
||||
text={workspace.displayName ?? '(No name)'}
|
||||
text={availableWorkspace.displayName ?? '(No name)'}
|
||||
avatar={
|
||||
<Avatar
|
||||
placeholder={workspace.displayName || ''}
|
||||
avatarUrl={workspace.logo ?? DEFAULT_WORKSPACE_LOGO}
|
||||
placeholder={availableWorkspace.displayName || ''}
|
||||
avatarUrl={
|
||||
availableWorkspace.logo ?? DEFAULT_WORKSPACE_LOGO
|
||||
}
|
||||
/>
|
||||
}
|
||||
selected={false}
|
||||
/>
|
||||
</UndecoratedLink>
|
||||
))}
|
||||
{workspaces.length > 4 && (
|
||||
{availableWorkspacesCount > 4 && (
|
||||
<MenuItem
|
||||
LeftIcon={IconSwitchHorizontal}
|
||||
text={t`Other workspaces`}
|
||||
|
||||
@ -1,32 +1,23 @@
|
||||
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
||||
import { Workspaces, workspacesState } from '@/auth/states/workspaces';
|
||||
import { useBuildWorkspaceUrl } from '@/domain-manager/hooks/useBuildWorkspaceUrl';
|
||||
import { useRedirectToWorkspaceDomain } from '@/domain-manager/hooks/useRedirectToWorkspaceDomain';
|
||||
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
|
||||
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader/DropdownMenuHeader';
|
||||
import { DropdownMenuHeaderLeftComponent } from '@/ui/layout/dropdown/components/DropdownMenuHeader/internal/DropdownMenuHeaderLeftComponent';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
|
||||
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
|
||||
import { DEFAULT_WORKSPACE_LOGO } from '@/ui/navigation/navigation-drawer/constants/DefaultWorkspaceLogo';
|
||||
import { multiWorkspaceDropdownState } from '@/ui/navigation/navigation-drawer/states/multiWorkspaceDropdownState';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { useState } from 'react';
|
||||
import { useRecoilValue, useSetRecoilState } from 'recoil';
|
||||
import { Avatar, IconChevronLeft } from 'twenty-ui/display';
|
||||
import { MenuItemSelectAvatar, UndecoratedLink } from 'twenty-ui/navigation';
|
||||
import { getWorkspaceUrl } from '~/utils/getWorkspaceUrl';
|
||||
import { IconChevronLeft } from 'twenty-ui/display';
|
||||
|
||||
import { WorkspacesForSignIn } from './components/WorkspacesForSignIn';
|
||||
import { WorkspacesForSignUp } from './components/WorkspacesForSignUp';
|
||||
import { availableWorkspacesState } from '@/auth/states/availableWorkspacesState';
|
||||
|
||||
export const MultiWorkspaceDropdownWorkspacesListComponents = () => {
|
||||
const currentWorkspace = useRecoilValue(currentWorkspaceState);
|
||||
const workspaces = useRecoilValue(workspacesState);
|
||||
const { redirectToWorkspaceDomain } = useRedirectToWorkspaceDomain();
|
||||
const { buildWorkspaceUrl } = useBuildWorkspaceUrl();
|
||||
const { t } = useLingui();
|
||||
|
||||
const handleChange = async (workspace: Workspaces[0]) => {
|
||||
await redirectToWorkspaceDomain(getWorkspaceUrl(workspace.workspaceUrls));
|
||||
};
|
||||
const availableWorkspaces = useRecoilValue(availableWorkspacesState);
|
||||
|
||||
const setMultiWorkspaceDropdownState = useSetRecoilState(
|
||||
multiWorkspaceDropdownState,
|
||||
);
|
||||
@ -52,37 +43,10 @@ export const MultiWorkspaceDropdownWorkspacesListComponents = () => {
|
||||
}}
|
||||
/>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItemsContainer>
|
||||
{workspaces
|
||||
.filter(
|
||||
(workspace) =>
|
||||
workspace.id !== currentWorkspace?.id &&
|
||||
workspace.displayName
|
||||
?.toLowerCase()
|
||||
.includes(searchValue.toLowerCase()),
|
||||
)
|
||||
.map((workspace) => (
|
||||
<UndecoratedLink
|
||||
key={workspace.id}
|
||||
to={buildWorkspaceUrl(getWorkspaceUrl(workspace.workspaceUrls))}
|
||||
onClick={(event) => {
|
||||
event?.preventDefault();
|
||||
handleChange(workspace);
|
||||
}}
|
||||
>
|
||||
<MenuItemSelectAvatar
|
||||
text={workspace.displayName ?? '(No name)'}
|
||||
avatar={
|
||||
<Avatar
|
||||
placeholder={workspace.displayName || ''}
|
||||
avatarUrl={workspace.logo ?? DEFAULT_WORKSPACE_LOGO}
|
||||
/>
|
||||
}
|
||||
selected={currentWorkspace?.id === workspace.id}
|
||||
/>
|
||||
</UndecoratedLink>
|
||||
))}
|
||||
</DropdownMenuItemsContainer>
|
||||
<WorkspacesForSignIn searchValue={searchValue} />
|
||||
{availableWorkspaces.availableWorkspacesForSignUp.length > 0 && (
|
||||
<WorkspacesForSignUp searchValue={searchValue} />
|
||||
)}
|
||||
</DropdownContent>
|
||||
);
|
||||
};
|
||||
|
||||
@ -0,0 +1,58 @@
|
||||
import { Avatar } from 'twenty-ui/display';
|
||||
import { MenuItemSelectAvatar, UndecoratedLink } from 'twenty-ui/navigation';
|
||||
import { DEFAULT_WORKSPACE_LOGO } from '@/ui/navigation/navigation-drawer/constants/DefaultWorkspaceLogo';
|
||||
import { AvailableWorkspace } from '~/generated/graphql';
|
||||
import { useRedirectToWorkspaceDomain } from '@/domain-manager/hooks/useRedirectToWorkspaceDomain';
|
||||
import { getWorkspaceUrl } from '~/utils/getWorkspaceUrl';
|
||||
import { getAvailableWorkspacePathAndSearchParams } from '@/auth/utils/availableWorkspacesUtils';
|
||||
import React from 'react';
|
||||
import { useBuildWorkspaceUrl } from '@/domain-manager/hooks/useBuildWorkspaceUrl';
|
||||
|
||||
export const AvailableWorkspaceItem = ({
|
||||
availableWorkspace,
|
||||
isSelected,
|
||||
}: {
|
||||
availableWorkspace: AvailableWorkspace;
|
||||
isSelected: boolean;
|
||||
}) => {
|
||||
const { buildWorkspaceUrl } = useBuildWorkspaceUrl();
|
||||
|
||||
const { redirectToWorkspaceDomain } = useRedirectToWorkspaceDomain();
|
||||
|
||||
const { pathname, searchParams } =
|
||||
getAvailableWorkspacePathAndSearchParams(availableWorkspace);
|
||||
|
||||
const handleChange = async () => {
|
||||
await redirectToWorkspaceDomain(
|
||||
getWorkspaceUrl(availableWorkspace.workspaceUrls),
|
||||
pathname,
|
||||
searchParams,
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<UndecoratedLink
|
||||
key={availableWorkspace.id}
|
||||
to={buildWorkspaceUrl(
|
||||
getWorkspaceUrl(availableWorkspace.workspaceUrls),
|
||||
pathname,
|
||||
searchParams,
|
||||
)}
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
handleChange();
|
||||
}}
|
||||
>
|
||||
<MenuItemSelectAvatar
|
||||
text={availableWorkspace.displayName ?? '(No name)'}
|
||||
avatar={
|
||||
<Avatar
|
||||
placeholder={availableWorkspace.displayName || ''}
|
||||
avatarUrl={availableWorkspace.logo ?? DEFAULT_WORKSPACE_LOGO}
|
||||
/>
|
||||
}
|
||||
selected={isSelected}
|
||||
/>
|
||||
</UndecoratedLink>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,39 @@
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { StyledDropdownMenuSubheader } from '@/ui/layout/dropdown/components/StyledDropdownMenuSubheader';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
import { useFilteredAvailableWorkspaces } from '@/ui/navigation/navigation-drawer/hooks/useFilteredAvailableWorkspaces';
|
||||
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { availableWorkspacesState } from '@/auth/states/availableWorkspacesState';
|
||||
import { AvailableWorkspaceItem } from '@/ui/navigation/navigation-drawer/components/MultiWorkspaceDropdown/internal/components/AvailableWorkspaceItem';
|
||||
|
||||
export const WorkspacesForSignIn = ({
|
||||
searchValue,
|
||||
}: {
|
||||
searchValue: string;
|
||||
}) => {
|
||||
const { t } = useLingui();
|
||||
|
||||
const availableWorkspaces = useRecoilValue(availableWorkspacesState);
|
||||
const currentWorkspace = useRecoilValue(currentWorkspaceState);
|
||||
|
||||
const { searchAvailableWorkspaces } = useFilteredAvailableWorkspaces();
|
||||
|
||||
return (
|
||||
<>
|
||||
<StyledDropdownMenuSubheader>{t`Member of`}</StyledDropdownMenuSubheader>
|
||||
<DropdownMenuItemsContainer>
|
||||
{searchAvailableWorkspaces(
|
||||
searchValue,
|
||||
availableWorkspaces.availableWorkspacesForSignIn,
|
||||
).map((availableWorkspace) => (
|
||||
<AvailableWorkspaceItem
|
||||
key={availableWorkspace.id}
|
||||
availableWorkspace={availableWorkspace}
|
||||
isSelected={currentWorkspace?.id === availableWorkspace.id}
|
||||
/>
|
||||
))}
|
||||
</DropdownMenuItemsContainer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,39 @@
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { StyledDropdownMenuSubheader } from '@/ui/layout/dropdown/components/StyledDropdownMenuSubheader';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
import { useFilteredAvailableWorkspaces } from '@/ui/navigation/navigation-drawer/hooks/useFilteredAvailableWorkspaces';
|
||||
import { AvailableWorkspaceItem } from '@/ui/navigation/navigation-drawer/components/MultiWorkspaceDropdown/internal/components/AvailableWorkspaceItem';
|
||||
import { availableWorkspacesState } from '@/auth/states/availableWorkspacesState';
|
||||
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
export const WorkspacesForSignUp = ({
|
||||
searchValue,
|
||||
}: {
|
||||
searchValue: string;
|
||||
}) => {
|
||||
const { t } = useLingui();
|
||||
|
||||
const availableWorkspaces = useRecoilValue(availableWorkspacesState);
|
||||
const currentWorkspace = useRecoilValue(currentWorkspaceState);
|
||||
|
||||
const { searchAvailableWorkspaces } = useFilteredAvailableWorkspaces();
|
||||
|
||||
return (
|
||||
<>
|
||||
<StyledDropdownMenuSubheader>{t`Invitations`}</StyledDropdownMenuSubheader>
|
||||
<DropdownMenuItemsContainer scrollable={false}>
|
||||
{searchAvailableWorkspaces(
|
||||
searchValue,
|
||||
availableWorkspaces.availableWorkspacesForSignUp,
|
||||
).map((availableWorkspace) => (
|
||||
<AvailableWorkspaceItem
|
||||
key={availableWorkspace.id}
|
||||
availableWorkspace={availableWorkspace}
|
||||
isSelected={currentWorkspace?.id === availableWorkspace.id}
|
||||
/>
|
||||
))}
|
||||
</DropdownMenuItemsContainer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,25 @@
|
||||
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { AvailableWorkspace } from '~/generated/graphql';
|
||||
|
||||
export const useFilteredAvailableWorkspaces = () => {
|
||||
const currentWorkspace = useRecoilValue(currentWorkspaceState);
|
||||
|
||||
const searchAvailableWorkspaces = (
|
||||
searchValue: string,
|
||||
availableWorkspaces: Array<AvailableWorkspace>,
|
||||
) => {
|
||||
return availableWorkspaces.filter(
|
||||
(availableWorkspace) =>
|
||||
currentWorkspace?.id &&
|
||||
availableWorkspace.id !== currentWorkspace.id &&
|
||||
availableWorkspace.displayName
|
||||
?.toLowerCase()
|
||||
.includes(searchValue.toLowerCase()),
|
||||
);
|
||||
};
|
||||
|
||||
return {
|
||||
searchAvailableWorkspaces,
|
||||
};
|
||||
};
|
||||
@ -8,7 +8,6 @@ import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMembe
|
||||
import { currentWorkspaceMembersState } from '@/auth/states/currentWorkspaceMembersStates';
|
||||
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
||||
import { isCurrentUserLoadedState } from '@/auth/states/isCurrentUserLoadedState';
|
||||
import { workspacesState } from '@/auth/states/workspaces';
|
||||
import { DateFormat } from '@/localization/constants/DateFormat';
|
||||
import { TimeFormat } from '@/localization/constants/TimeFormat';
|
||||
import { dateTimeFormatState } from '@/localization/states/dateTimeFormatState';
|
||||
@ -30,6 +29,7 @@ import { useGetCurrentUserQuery } from '~/generated/graphql';
|
||||
import { dateLocaleState } from '~/localization/states/dateLocaleState';
|
||||
import { dynamicActivate } from '~/utils/i18n/dynamicActivate';
|
||||
import { isMatchingLocation } from '~/utils/isMatchingLocation';
|
||||
import { availableWorkspacesState } from '@/auth/states/availableWorkspacesState';
|
||||
|
||||
export const UserProviderEffect = () => {
|
||||
const location = useLocation();
|
||||
@ -40,7 +40,7 @@ export const UserProviderEffect = () => {
|
||||
const setCurrentUser = useSetRecoilState(currentUserState);
|
||||
const setCurrentWorkspace = useSetRecoilState(currentWorkspaceState);
|
||||
const setCurrentUserWorkspace = useSetRecoilState(currentUserWorkspaceState);
|
||||
const setWorkspaces = useSetRecoilState(workspacesState);
|
||||
const setAvailableWorkspaces = useSetRecoilState(availableWorkspacesState);
|
||||
const setDateTimeFormat = useSetRecoilState(dateTimeFormatState);
|
||||
const isLoggedIn = useIsLogged();
|
||||
|
||||
@ -102,7 +102,7 @@ export const UserProviderEffect = () => {
|
||||
workspaceMember,
|
||||
workspaceMembers,
|
||||
deletedWorkspaceMembers,
|
||||
workspaces: userWorkspaces,
|
||||
availableWorkspaces,
|
||||
} = queryData.currentUser;
|
||||
|
||||
const affectDefaultValuesOnEmptyWorkspaceMemberFields = (
|
||||
@ -153,12 +153,8 @@ export const UserProviderEffect = () => {
|
||||
setCurrentWorkspaceMembersWithDeleted(deletedWorkspaceMembers);
|
||||
}
|
||||
|
||||
if (isDefined(userWorkspaces)) {
|
||||
const workspaces = userWorkspaces
|
||||
.map(({ workspace }) => workspace)
|
||||
.filter(isDefined);
|
||||
|
||||
setWorkspaces(workspaces);
|
||||
if (isDefined(availableWorkspaces)) {
|
||||
setAvailableWorkspaces(availableWorkspaces);
|
||||
}
|
||||
}, [
|
||||
queryLoading,
|
||||
@ -166,9 +162,9 @@ export const UserProviderEffect = () => {
|
||||
setCurrentUser,
|
||||
setCurrentUserWorkspace,
|
||||
setCurrentWorkspaceMembers,
|
||||
setAvailableWorkspaces,
|
||||
setCurrentWorkspace,
|
||||
setCurrentWorkspaceMember,
|
||||
setWorkspaces,
|
||||
setIsCurrentUserLoaded,
|
||||
setDateTimeFormat,
|
||||
setCurrentWorkspaceMembersWithDeleted,
|
||||
|
||||
@ -3,6 +3,7 @@ import { ROLE_FRAGMENT } from '@/settings/roles/graphql/fragments/roleFragment';
|
||||
import { DELETED_WORKSPACE_MEMBER_QUERY_FRAGMENT } from '@/workspace-member/graphql/fragments/deletedWorkspaceMemberQueryFragment';
|
||||
import { WORKSPACE_MEMBER_QUERY_FRAGMENT } from '@/workspace-member/graphql/fragments/workspaceMemberQueryFragment';
|
||||
import { gql } from '@apollo/client';
|
||||
import { AVAILABLE_WORKSPACES_FOR_AUTH_FRAGMENT } from '@/auth/graphql/fragments/authFragments';
|
||||
|
||||
export const USER_QUERY_FRAGMENT = gql`
|
||||
${ROLE_FRAGMENT}
|
||||
@ -48,8 +49,7 @@ export const USER_QUERY_FRAGMENT = gql`
|
||||
customDomain
|
||||
isCustomDomainEnabled
|
||||
workspaceUrls {
|
||||
subdomainUrl
|
||||
customUrl
|
||||
...WorkspaceUrlsFragment
|
||||
}
|
||||
featureFlags {
|
||||
key
|
||||
@ -86,22 +86,13 @@ export const USER_QUERY_FRAGMENT = gql`
|
||||
...RoleFragment
|
||||
}
|
||||
}
|
||||
workspaces {
|
||||
workspace {
|
||||
id
|
||||
logo
|
||||
displayName
|
||||
subdomain
|
||||
customDomain
|
||||
workspaceUrls {
|
||||
subdomainUrl
|
||||
customUrl
|
||||
}
|
||||
}
|
||||
availableWorkspaces {
|
||||
...AvailableWorkspacesFragment
|
||||
}
|
||||
userVars
|
||||
}
|
||||
|
||||
${AVAILABLE_WORKSPACES_FOR_AUTH_FRAGMENT}
|
||||
${WORKSPACE_MEMBER_QUERY_FRAGMENT}
|
||||
${DELETED_WORKSPACE_MEMBER_QUERY_FRAGMENT}
|
||||
`;
|
||||
|
||||
@ -0,0 +1,8 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const WORKSPACE_URLS_FRAGMENT = gql`
|
||||
fragment WorkspaceUrlsFragment on WorkspaceUrls {
|
||||
subdomainUrl
|
||||
customUrl
|
||||
}
|
||||
`;
|
||||
Reference in New Issue
Block a user