4689 multi workspace i should be able to accept an invite if im already logged in (#5454)

- split signInUp to separate Invitation from signInUp
- update redirection logic
- add a resolver for userWorkspace
- add a mutation to add a user to a workspace
- authorize /invite/hash while loggedIn
- add a button to join a workspace

### Base functionnality

https://github.com/twentyhq/twenty/assets/29927851/a1075a4e-a2af-4184-aa3e-e163711277a1

### Error handling

https://github.com/twentyhq/twenty/assets/29927851/1bdd78ce-933a-4860-a87a-3f1f7bda389e
This commit is contained in:
martmull
2024-05-20 12:11:38 +02:00
committed by GitHub
parent 1ceeb68da8
commit 88f5eb669e
17 changed files with 340 additions and 101 deletions

View File

@ -0,0 +1,122 @@
import { useEffect, useMemo } from 'react';
import { useNavigate } from 'react-router-dom';
import styled from '@emotion/styled';
import { useRecoilValue } from 'recoil';
import { Logo } from '@/auth/components/Logo';
import { Title } from '@/auth/components/Title';
import { FooterNote } from '@/auth/sign-in-up/components/FooterNote';
import { SignInUpForm } from '@/auth/sign-in-up/components/SignInUpForm';
import { useSignInUpForm } from '@/auth/sign-in-up/hooks/useSignInUpForm';
import { useWorkspaceFromInviteHash } from '@/auth/sign-in-up/hooks/useWorkspaceFromInviteHash';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { AppPath } from '@/types/AppPath';
import { Loader } from '@/ui/feedback/loader/components/Loader';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { MainButton } from '@/ui/input/button/components/MainButton';
import { useWorkspaceSwitching } from '@/ui/navigation/navigation-drawer/hooks/useWorkspaceSwitching';
import { AnimatedEaseIn } from '@/ui/utilities/animation/components/AnimatedEaseIn';
import { useAddUserToWorkspaceMutation } from '~/generated/graphql';
import { isDefined } from '~/utils/isDefined';
const StyledContentContainer = styled.div`
margin-bottom: ${({ theme }) => theme.spacing(8)};
margin-top: ${({ theme }) => theme.spacing(4)};
`;
export const Invite = () => {
const { enqueueSnackBar } = useSnackBar();
const navigate = useNavigate();
const {
workspace: workspaceFromInviteHash,
loading: workspaceFromInviteHashLoading,
workspaceInviteHash,
} = useWorkspaceFromInviteHash();
const { form } = useSignInUpForm();
const currentWorkspace = useRecoilValue(currentWorkspaceState);
const [addUserToWorkspace] = useAddUserToWorkspaceMutation();
const { switchWorkspace } = useWorkspaceSwitching();
const title = useMemo(() => {
return `Join ${workspaceFromInviteHash?.displayName ?? ''} team`;
}, [workspaceFromInviteHash?.displayName]);
const handleUserJoinWorkspace = async () => {
if (
!(isDefined(workspaceInviteHash) && isDefined(workspaceFromInviteHash))
) {
return;
}
await addUserToWorkspace({
variables: {
inviteHash: workspaceInviteHash,
},
});
await switchWorkspace(workspaceFromInviteHash.id);
};
useEffect(() => {
if (
!isDefined(workspaceFromInviteHash) &&
!workspaceFromInviteHashLoading
) {
enqueueSnackBar('workspace does not exist', {
variant: 'error',
});
if (isDefined(currentWorkspace)) {
navigate(AppPath.Index);
} else {
navigate(AppPath.SignInUp);
}
}
if (
isDefined(currentWorkspace) &&
currentWorkspace.id === workspaceFromInviteHash?.id
) {
enqueueSnackBar(
`You already belong to ${workspaceFromInviteHash?.displayName} workspace`,
{
variant: 'info',
},
);
navigate(AppPath.Index);
}
}, [
navigate,
enqueueSnackBar,
currentWorkspace,
workspaceFromInviteHash,
workspaceFromInviteHashLoading,
]);
return (
!workspaceFromInviteHashLoading && (
<>
<AnimatedEaseIn>
<Logo workspaceLogo={workspaceFromInviteHash?.logo} />
</AnimatedEaseIn>
<Title animate>{title}</Title>
{isDefined(currentWorkspace) && workspaceFromInviteHash ? (
<>
<StyledContentContainer>
<MainButton
variant="secondary"
title="Continue"
type="submit"
onClick={handleUserJoinWorkspace}
Icon={() => form.formState.isSubmitting && <Loader />}
fullWidth
/>
</StyledContentContainer>
<FooterNote>
By using Twenty, you agree to the Terms of Service and Privacy
Policy.
</FooterNote>
</>
) : (
<SignInUpForm />
)}
</>
)
);
};

View File

@ -1,3 +1,43 @@
import { SignInUpForm } from '../../modules/auth/sign-in-up/components/SignInUpForm';
import { useMemo } from 'react';
import { useRecoilValue } from 'recoil';
export const SignInUp = () => <SignInUpForm />;
import { Title } from '@/auth/components/Title';
import { SignInUpForm } from '@/auth/sign-in-up/components/SignInUpForm';
import {
SignInUpMode,
SignInUpStep,
useSignInUp,
} from '@/auth/sign-in-up/hooks/useSignInUp';
import { useSignInUpForm } from '@/auth/sign-in-up/hooks/useSignInUpForm';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { isDefined } from '~/utils/isDefined';
export const SignInUp = () => {
const { form } = useSignInUpForm();
const currentWorkspace = useRecoilValue(currentWorkspaceState);
const { signInUpStep, signInUpMode } = useSignInUp(form);
const title = useMemo(() => {
if (
signInUpStep === SignInUpStep.Init ||
signInUpStep === SignInUpStep.Email
) {
return 'Welcome to Twenty';
}
return signInUpMode === SignInUpMode.SignIn
? 'Sign in to Twenty'
: 'Sign up to Twenty';
}, [signInUpMode, signInUpStep]);
if (isDefined(currentWorkspace)) {
return <></>;
}
return (
<>
<Title animate>{title}</Title>
<SignInUpForm />
</>
);
};

View File

@ -10,7 +10,6 @@ import {
PageDecoratorArgs,
} from '~/testing/decorators/PageDecorator';
import { graphqlMocks } from '~/testing/graphqlMocks';
import { mockedOnboardingUsersData } from '~/testing/mock-data/users';
import { SignInUp } from '../SignInUp';
@ -24,14 +23,25 @@ const meta: Meta<PageDecoratorArgs> = {
handlers: [
graphql.query(getOperationName(GET_CURRENT_USER) ?? '', () => {
return HttpResponse.json({
data: {
currentUser: mockedOnboardingUsersData[0],
},
data: null,
errors: [
{
message: 'Unauthorized',
extensions: {
code: 'UNAUTHENTICATED',
response: {
statusCode: 401,
message: 'Unauthorized',
},
},
},
],
});
}),
graphqlMocks.handlers,
],
},
cookie: '',
},
};