feat: multi-workspace (frontend) (#4232)

* select workspace component

* generateJWT mutation

* workspaces state and hooks

* requested changes

* mutation fix

* requested changes

* user workpsace delete call

* migration to drop and createt user workspace

* revert select props

* add DropdownMenu

* seperate multi-workspace dropdown as component

* Signup button displayed accurately

* update seed data for multi-workspace

* lint fix

* lint fix

* css fix

* lint fix

* state fix

* isDefined check

* refactor

* add default workspace constants for logo and name

* update migration

* lint fix

* isInviteMode check on sign-in/up

* removeWorkspaceMember mutation

* import fixes

* prop name fix

* backfill migration

* handle edge cases

* refactor

* remove migration query

* delete user on no-workspace found condition

* emit workspaceMember.deleted

* Fix event class and unrelated fix linked to a previously missing dependency

* Edit migration (I did it in prod manually)

* Revert changes

* Fix tests

* Fix conflicts

---------

Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
This commit is contained in:
Aditya Pimpalkar
2024-03-20 13:43:41 +00:00
committed by GitHub
parent 352192a63f
commit da12710fe9
29 changed files with 726 additions and 134 deletions

View File

@ -0,0 +1,11 @@
import { gql } from '@apollo/client';
export const GENERATE_JWT = gql`
mutation GenerateJWT($workspaceId: String!) {
generateJWT(workspaceId: $workspaceId) {
tokens {
...AuthTokensFragment
}
}
}
`;

View File

@ -11,6 +11,7 @@ import {
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { isVerifyPendingState } from '@/auth/states/isVerifyPendingState';
import { workspacesState } from '@/auth/states/workspaces';
import { authProvidersState } from '@/client-config/states/authProvidersState';
import { billingState } from '@/client-config/states/billingState';
import { isDebugModeState } from '@/client-config/states/isDebugModeState';
@ -40,6 +41,7 @@ export const useAuth = () => {
const setCurrentWorkspace = useSetRecoilState(currentWorkspaceState);
const setIsVerifyPendingState = useSetRecoilState(isVerifyPendingState);
const setWorkspaces = useSetRecoilState(workspacesState);
const [challenge] = useChallengeMutation();
const [signUp] = useSignUpMutation();
@ -101,6 +103,15 @@ export const useAuth = () => {
}
const workspace = user.defaultWorkspace ?? null;
setCurrentWorkspace(workspace);
if (isDefined(verifyResult.data?.verify.user.workspaces)) {
const validWorkspaces = verifyResult.data?.verify.user.workspaces
.filter(
({ workspace }) => workspace !== null && workspace !== undefined,
)
.map((validWorkspace) => validWorkspace.workspace!);
setWorkspaces(validWorkspaces);
}
return {
user,
workspaceMember,
@ -114,6 +125,7 @@ export const useAuth = () => {
setCurrentUser,
setCurrentWorkspaceMember,
setCurrentWorkspace,
setWorkspaces,
],
);

View File

@ -54,6 +54,7 @@ export const SignInUpForm = () => {
const { form } = useSignInUpForm();
const {
isInviteMode,
signInUpStep,
signInUpMode,
continueWithCredentials,
@ -89,14 +90,14 @@ export const SignInUpForm = () => {
}, [signInUpMode, signInUpStep]);
const title = useMemo(() => {
if (signInUpMode === SignInUpMode.Invite) {
if (isInviteMode) {
return `Join ${workspace?.displayName ?? ''} team`;
}
return signInUpMode === SignInUpMode.SignIn
? 'Sign in to Twenty'
: 'Sign up to Twenty';
}, [signInUpMode, workspace?.displayName]);
}, [signInUpMode, workspace?.displayName, isInviteMode]);
const theme = useTheme();

View File

@ -15,7 +15,6 @@ import { useAuth } from '../../hooks/useAuth';
export enum SignInUpMode {
SignIn = 'sign-in',
SignUp = 'sign-up',
Invite = 'invite',
}
export enum SignInUpStep {
@ -33,15 +32,13 @@ export const useSignInUp = (form: UseFormReturn<Form>) => {
const { navigateAfterSignInUp } = useNavigateAfterSignInUp();
const [isInviteMode] = useState(() => isMatchingLocation(AppPath.Invite));
const [signInUpStep, setSignInUpStep] = useState<SignInUpStep>(
SignInUpStep.Init,
);
const [signInUpMode, setSignInUpMode] = useState<SignInUpMode>(() => {
if (isMatchingLocation(AppPath.Invite)) {
return SignInUpMode.Invite;
}
return isMatchingLocation(AppPath.SignIn)
? SignInUpMode.SignIn
: SignInUpMode.SignUp;
@ -72,24 +69,14 @@ export const useSignInUp = (form: UseFormReturn<Form>) => {
},
onCompleted: (data) => {
if (data?.checkUserExists.exists) {
isMatchingLocation(AppPath.Invite)
? setSignInUpMode(SignInUpMode.Invite)
: setSignInUpMode(SignInUpMode.SignIn);
setSignInUpMode(SignInUpMode.SignIn);
} else {
isMatchingLocation(AppPath.Invite)
? setSignInUpMode(SignInUpMode.Invite)
: setSignInUpMode(SignInUpMode.SignUp);
setSignInUpMode(SignInUpMode.SignUp);
}
setSignInUpStep(SignInUpStep.Password);
},
});
}, [
isMatchingLocation,
setSignInUpStep,
checkUserExistsQuery,
form,
setSignInUpMode,
]);
}, [setSignInUpStep, checkUserExistsQuery, form, setSignInUpMode]);
const submitCredentials: SubmitHandler<Form> = useCallback(
async (data) => {
@ -102,7 +89,7 @@ export const useSignInUp = (form: UseFormReturn<Form>) => {
workspace: currentWorkspace,
workspaceMember: currentWorkspaceMember,
} =
signInUpMode === SignInUpMode.SignIn
signInUpMode === SignInUpMode.SignIn && !isInviteMode
? await signInWithCredentials(
data.email.toLowerCase().trim(),
data.password,
@ -122,6 +109,7 @@ export const useSignInUp = (form: UseFormReturn<Form>) => {
},
[
signInUpMode,
isInviteMode,
signInWithCredentials,
signUpWithCredentials,
workspaceInviteHash,
@ -156,6 +144,7 @@ export const useSignInUp = (form: UseFormReturn<Form>) => {
);
return {
isInviteMode,
signInUpStep,
signInUpMode,
continueWithCredentials,

View File

@ -0,0 +1,9 @@
import { createState } from '@/ui/utilities/state/utils/createState';
import { Workspace } from '~/generated/graphql';
export type Workspaces = Pick<Workspace, 'id' | 'logo' | 'displayName'>;
export const workspacesState = createState<Workspaces[] | null>({
key: 'workspacesState',
defaultValue: [],
});