Migrate to a monorepo structure (#2909)

This commit is contained in:
Charles Bochet
2023-12-10 18:10:54 +01:00
committed by GitHub
parent a70a9281eb
commit 5bdca9de6c
2304 changed files with 37152 additions and 25869 deletions

View File

@ -0,0 +1,212 @@
import { useCallback } from 'react';
import { Controller, SubmitHandler, useForm } from 'react-hook-form';
import { useNavigate } from 'react-router-dom';
import styled from '@emotion/styled';
import { zodResolver } from '@hookform/resolvers/zod';
import { useRecoilState } from 'recoil';
import { Key } from 'ts-key-enum';
import { z } from 'zod';
import { SubTitle } from '@/auth/components/SubTitle';
import { Title } from '@/auth/components/Title';
import { useOnboardingStatus } from '@/auth/hooks/useOnboardingStatus';
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { OnboardingStatus } from '@/auth/utils/getOnboardingStatus';
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
import { ProfilePictureUploader } from '@/settings/profile/components/ProfilePictureUploader';
import { PageHotkeyScope } from '@/types/PageHotkeyScope';
import { H2Title } from '@/ui/display/typography/components/H2Title';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { MainButton } from '@/ui/input/button/components/MainButton';
import { TextInput } from '@/ui/input/components/TextInput';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { WorkspaceMember } from '@/workspace-member/types/WorkspaceMember';
const StyledContentContainer = styled.div`
width: 100%;
`;
const StyledSectionContainer = styled.div`
margin-top: ${({ theme }) => theme.spacing(8)};
`;
const StyledButtonContainer = styled.div`
margin-top: ${({ theme }) => theme.spacing(8)};
width: 200px;
`;
const StyledComboInputContainer = styled.div`
display: flex;
flex-direction: row;
> * + * {
margin-left: ${({ theme }) => theme.spacing(4)};
}
`;
const validationSchema = z
.object({
firstName: z.string().min(1, { message: 'First name can not be empty' }),
lastName: z.string().min(1, { message: 'Last name can not be empty' }),
})
.required();
type Form = z.infer<typeof validationSchema>;
export const CreateProfile = () => {
const navigate = useNavigate();
const onboardingStatus = useOnboardingStatus();
const { enqueueSnackBar } = useSnackBar();
const [currentWorkspaceMember, setCurrentWorkspaceMember] = useRecoilState(
currentWorkspaceMemberState,
);
const { updateOneRecord } = useUpdateOneRecord<WorkspaceMember>({
objectNameSingular: 'workspaceMember',
});
// Form
const {
control,
handleSubmit,
formState: { isValid, isSubmitting },
getValues,
} = useForm<Form>({
mode: 'onChange',
defaultValues: {
firstName: currentWorkspaceMember?.name?.firstName ?? '',
lastName: currentWorkspaceMember?.name?.lastName ?? '',
},
resolver: zodResolver(validationSchema),
});
const onSubmit: SubmitHandler<Form> = useCallback(
async (data) => {
try {
if (!currentWorkspaceMember?.id) {
throw new Error('User is not logged in');
}
if (!data.firstName || !data.lastName) {
throw new Error('First name or last name is missing');
}
await updateOneRecord({
idToUpdate: currentWorkspaceMember?.id,
input: {
name: {
firstName: data.firstName,
lastName: data.lastName,
},
},
});
setCurrentWorkspaceMember(
(current) =>
({
...current,
name: {
firstName: data.firstName,
lastName: data.lastName,
},
}) as any,
);
navigate('/');
} catch (error: any) {
enqueueSnackBar(error?.message, {
variant: 'error',
});
}
},
[
currentWorkspaceMember?.id,
enqueueSnackBar,
navigate,
setCurrentWorkspaceMember,
updateOneRecord,
],
);
useScopedHotkeys(
Key.Enter,
() => {
onSubmit(getValues());
},
PageHotkeyScope.CreateProfile,
[onSubmit],
);
if (onboardingStatus !== OnboardingStatus.OngoingProfileCreation) {
return null;
}
return (
<>
<Title>Create profile</Title>
<SubTitle>How you'll be identified on the app.</SubTitle>
<StyledContentContainer>
<StyledSectionContainer>
<H2Title title="Picture" />
<ProfilePictureUploader />
</StyledSectionContainer>
<StyledSectionContainer>
<H2Title
title="Name"
description="Your name as it will be displayed on the app"
/>
{/* TODO: When react-web-hook-form is added to edit page we should create a dedicated component with context */}
<StyledComboInputContainer>
<Controller
name="firstName"
control={control}
render={({
field: { onChange, onBlur, value },
fieldState: { error },
}) => (
<TextInput
autoFocus
label="First Name"
value={value}
onBlur={onBlur}
onChange={onChange}
placeholder="Tim"
error={error?.message}
fullWidth
disableHotkeys
/>
)}
/>
<Controller
name="lastName"
control={control}
render={({
field: { onChange, onBlur, value },
fieldState: { error },
}) => (
<TextInput
label="Last Name"
value={value}
onBlur={onBlur}
onChange={onChange}
placeholder="Cook"
error={error?.message}
fullWidth
disableHotkeys
/>
)}
/>
</StyledComboInputContainer>
</StyledSectionContainer>
</StyledContentContainer>
<StyledButtonContainer>
<MainButton
title="Continue"
onClick={handleSubmit(onSubmit)}
disabled={!isValid || isSubmitting}
fullWidth
/>
</StyledButtonContainer>
</>
);
};

View File

@ -0,0 +1,169 @@
import { useCallback } from 'react';
import { Controller, SubmitHandler, useForm } from 'react-hook-form';
import { useNavigate } from 'react-router-dom';
import styled from '@emotion/styled';
import { zodResolver } from '@hookform/resolvers/zod';
import { useSetRecoilState } from 'recoil';
import { z } from 'zod';
import { SubTitle } from '@/auth/components/SubTitle';
import { Title } from '@/auth/components/Title';
import { useOnboardingStatus } from '@/auth/hooks/useOnboardingStatus';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { OnboardingStatus } from '@/auth/utils/getOnboardingStatus';
import { WorkspaceLogoUploader } from '@/settings/workspace/components/WorkspaceLogoUploader';
import { PageHotkeyScope } from '@/types/PageHotkeyScope';
import { H2Title } from '@/ui/display/typography/components/H2Title';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { MainButton } from '@/ui/input/button/components/MainButton';
import { TextInput } from '@/ui/input/components/TextInput';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { useUpdateWorkspaceMutation } from '~/generated/graphql';
const StyledContentContainer = styled.div`
width: 100%;
`;
const StyledSectionContainer = styled.div`
margin-top: ${({ theme }) => theme.spacing(8)};
`;
const StyledButtonContainer = styled.div`
margin-top: ${({ theme }) => theme.spacing(8)};
width: 200px;
`;
const validationSchema = z
.object({
name: z.string().min(1, { message: 'Name can not be empty' }),
})
.required();
type Form = z.infer<typeof validationSchema>;
export const CreateWorkspace = () => {
const navigate = useNavigate();
const { enqueueSnackBar } = useSnackBar();
const onboardingStatus = useOnboardingStatus();
const setCurrentWorkspace = useSetRecoilState(currentWorkspaceState);
const [updateWorkspace] = useUpdateWorkspaceMutation();
// Form
const {
control,
handleSubmit,
formState: { isValid, isSubmitting },
getValues,
} = useForm<Form>({
mode: 'onChange',
defaultValues: {
name: '',
},
resolver: zodResolver(validationSchema),
});
const onSubmit: SubmitHandler<Form> = useCallback(
async (data) => {
try {
const result = await updateWorkspace({
variables: {
input: {
displayName: data.name,
},
},
});
setCurrentWorkspace({
id: result.data?.updateWorkspace?.id ?? '',
displayName: data.name,
allowImpersonation:
result.data?.updateWorkspace?.allowImpersonation ?? false,
});
if (result.errors || !result.data?.updateWorkspace) {
throw result.errors ?? new Error('Unknown error');
}
setTimeout(() => {
navigate('/create/profile');
}, 20);
} catch (error: any) {
enqueueSnackBar(error?.message, {
variant: 'error',
});
}
},
[enqueueSnackBar, navigate, setCurrentWorkspace, updateWorkspace],
);
const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
if (event.key === 'Enter') {
event.preventDefault();
handleSubmit(onSubmit)();
}
};
useScopedHotkeys(
'enter',
() => {
onSubmit(getValues());
},
PageHotkeyScope.CreateWokspace,
[onSubmit],
);
if (onboardingStatus !== OnboardingStatus.OngoingWorkspaceCreation) {
return null;
}
return (
<>
<Title>Create your workspace</Title>
<SubTitle>
A shared environment where you will be able to manage your customer
relations with your team.
</SubTitle>
<StyledContentContainer>
<StyledSectionContainer>
<H2Title title="Workspace logo" />
<WorkspaceLogoUploader />
</StyledSectionContainer>
<StyledSectionContainer>
<H2Title
title="Workspace name"
description="The name of your organization"
/>
<Controller
name="name"
control={control}
render={({
field: { onChange, onBlur, value },
fieldState: { error },
}) => (
<TextInput
autoFocus
value={value}
placeholder="Apple"
onBlur={onBlur}
onChange={onChange}
error={error?.message}
onKeyDown={handleKeyDown}
fullWidth
disableHotkeys
/>
)}
/>
</StyledSectionContainer>
</StyledContentContainer>
<StyledButtonContainer>
<MainButton
title="Continue"
onClick={handleSubmit(onSubmit)}
disabled={!isValid || isSubmitting}
fullWidth
/>
</StyledButtonContainer>
</>
);
};

View File

@ -0,0 +1,3 @@
import { SignInUpForm } from '../../modules/auth/sign-in-up/components/SignInUpForm';
export const SignInUp = () => <SignInUpForm />;

View File

@ -0,0 +1,45 @@
import { useEffect } from 'react';
import { useNavigate, useSearchParams } from 'react-router-dom';
import { isNonEmptyString } from '@sniptt/guards';
import { useRecoilValue } from 'recoil';
import { useAuth } from '@/auth/hooks/useAuth';
import { useIsLogged } from '@/auth/hooks/useIsLogged';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { AppPath } from '../../modules/types/AppPath';
export const VerifyEffect = () => {
const [searchParams] = useSearchParams();
const loginToken = searchParams.get('loginToken');
const currentWorkspace = useRecoilValue(currentWorkspaceState);
const isLogged = useIsLogged();
const navigate = useNavigate();
const { verify } = useAuth();
useEffect(() => {
const getTokens = async () => {
if (!loginToken) {
navigate(AppPath.SignIn);
} else {
await verify(loginToken);
if (isNonEmptyString(currentWorkspace?.displayName)) {
navigate(AppPath.Index);
} else {
navigate(AppPath.CreateWorkspace);
}
}
};
if (!isLogged) {
getTokens();
}
// Verify only needs to run once at mount
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return <></>;
};

View File

@ -0,0 +1,46 @@
import { getOperationName } from '@apollo/client/utilities';
import { Meta, StoryObj } from '@storybook/react';
import { within } from '@storybook/test';
import { graphql } from 'msw';
import { AppPath } from '@/types/AppPath';
import {
PageDecorator,
PageDecoratorArgs,
} from '~/testing/decorators/PageDecorator';
import { mockedOnboardingUsersData } from '~/testing/mock-data/users';
import { GET_CURRENT_USER } from '../../../modules/users/graphql/queries/getCurrentUser';
import { CreateProfile } from '../CreateProfile';
const meta: Meta<PageDecoratorArgs> = {
title: 'Pages/Auth/CreateProfile',
component: CreateProfile,
decorators: [PageDecorator],
args: { routePath: AppPath.CreateProfile },
parameters: {
msw: [
graphql.query(
getOperationName(GET_CURRENT_USER) ?? '',
(req, res, ctx) => {
return res(
ctx.data({
currentUser: mockedOnboardingUsersData[1],
}),
);
},
),
],
},
};
export default meta;
export type Story = StoryObj<typeof CreateProfile>;
export const Default: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await canvas.findByText('Create profile');
},
};

View File

@ -0,0 +1,46 @@
import { getOperationName } from '@apollo/client/utilities';
import { Meta, StoryObj } from '@storybook/react';
import { within } from '@storybook/test';
import { graphql } from 'msw';
import { AppPath } from '@/types/AppPath';
import {
PageDecorator,
PageDecoratorArgs,
} from '~/testing/decorators/PageDecorator';
import { mockedOnboardingUsersData } from '~/testing/mock-data/users';
import { GET_CURRENT_USER } from '../../../modules/users/graphql/queries/getCurrentUser';
import { CreateWorkspace } from '../CreateWorkspace';
const meta: Meta<PageDecoratorArgs> = {
title: 'Pages/Auth/CreateWorkspace',
component: CreateWorkspace,
decorators: [PageDecorator],
args: { routePath: AppPath.CreateWorkspace },
parameters: {
msw: [
graphql.query(
getOperationName(GET_CURRENT_USER) ?? '',
(req, res, ctx) => {
return res(
ctx.data({
currentUser: mockedOnboardingUsersData[0],
}),
);
},
),
],
},
};
export default meta;
export type Story = StoryObj<typeof CreateWorkspace>;
export const Default: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await canvas.findByText('Create your workspace');
},
};

View File

@ -0,0 +1,39 @@
import { Meta, StoryObj } from '@storybook/react';
import { fireEvent, within } from '@storybook/test';
import { AppPath } from '@/types/AppPath';
import {
PageDecorator,
PageDecoratorArgs,
} from '~/testing/decorators/PageDecorator';
import { graphqlMocks } from '~/testing/graphqlMocks';
import { SignInUp } from '../SignInUp';
const meta: Meta<PageDecoratorArgs> = {
title: 'Pages/Auth/SignInUp',
component: SignInUp,
decorators: [PageDecorator],
args: { routePath: AppPath.SignIn },
parameters: {
msw: graphqlMocks,
cookie: {
tokenPair: '{}',
},
},
};
export default meta;
export type Story = StoryObj<typeof SignInUp>;
export const Default: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const continueWithEmailButton = await canvas.findByText(
'Continue With Email',
);
await fireEvent.click(continueWithEmailButton);
},
};