feat: onboarding & profile edition (#507)
* feat: wip onboarding * fix: generate graphql front * wip: onboarding * feat: login/register and edit profile * fix: unused import * fix: test * Use DEBUG_MODE instead of STAGE and mute typescript depth exceed errors * Fix seeds * Fix onboarding when coming from google * Fix * Fix lint * Fix ci * Fix tests --------- Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
@ -1,14 +1,22 @@
|
||||
import { useCallback } from 'react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { getOperationName } from '@apollo/client/utilities';
|
||||
import styled from '@emotion/styled';
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import { SubTitle } from '@/auth/components/ui/SubTitle';
|
||||
import { Title } from '@/auth/components/ui/Title';
|
||||
import { useOnboardingStatus } from '@/auth/hooks/useOnboardingStatus';
|
||||
import { currentUserState } from '@/auth/states/currentUserState';
|
||||
import { OnboardingStatus } from '@/auth/utils/getOnboardingStatus';
|
||||
import { captureHotkeyTypeInFocusState } from '@/hotkeys/states/captureHotkeyTypeInFocusState';
|
||||
import { MainButton } from '@/ui/components/buttons/MainButton';
|
||||
import { ImageInput } from '@/ui/components/inputs/ImageInput';
|
||||
import { TextInput } from '@/ui/components/inputs/TextInput';
|
||||
import { SubSectionTitle } from '@/ui/components/section-titles/SubSectionTitle';
|
||||
import { GET_CURRENT_USER } from '@/users/services';
|
||||
import { useUpdateUserMutation } from '~/generated/graphql';
|
||||
|
||||
const StyledContentContainer = styled.div`
|
||||
width: 100%;
|
||||
@ -37,10 +45,58 @@ const StyledComboInputContainer = styled.div`
|
||||
|
||||
export function CreateProfile() {
|
||||
const navigate = useNavigate();
|
||||
const onboardingStatus = useOnboardingStatus();
|
||||
|
||||
const [currentUser] = useRecoilState(currentUserState);
|
||||
const [, setCaptureHotkeyTypeInFocus] = useRecoilState(
|
||||
captureHotkeyTypeInFocusState,
|
||||
);
|
||||
|
||||
const [updateUser] = useUpdateUserMutation();
|
||||
|
||||
const [firstName, setFirstName] = useState('');
|
||||
const [lastName, setLastName] = useState('');
|
||||
|
||||
const handleCreate = useCallback(async () => {
|
||||
navigate('/');
|
||||
}, [navigate]);
|
||||
try {
|
||||
if (!currentUser?.id) {
|
||||
throw new Error('User is not logged in');
|
||||
}
|
||||
|
||||
const { data, errors } = await updateUser({
|
||||
variables: {
|
||||
where: {
|
||||
id: currentUser?.id,
|
||||
},
|
||||
data: {
|
||||
firstName: {
|
||||
set: firstName,
|
||||
},
|
||||
lastName: {
|
||||
set: lastName,
|
||||
},
|
||||
},
|
||||
},
|
||||
refetchQueries: [getOperationName(GET_CURRENT_USER) ?? ''],
|
||||
});
|
||||
|
||||
if (errors || !data?.updateUser) {
|
||||
throw errors;
|
||||
}
|
||||
|
||||
setCaptureHotkeyTypeInFocus(false);
|
||||
navigate('/');
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}, [
|
||||
currentUser?.id,
|
||||
firstName,
|
||||
lastName,
|
||||
navigate,
|
||||
setCaptureHotkeyTypeInFocus,
|
||||
updateUser,
|
||||
]);
|
||||
|
||||
useHotkeys(
|
||||
'enter',
|
||||
@ -54,10 +110,20 @@ export function CreateProfile() {
|
||||
[handleCreate],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (onboardingStatus !== OnboardingStatus.OngoingProfileCreation) {
|
||||
navigate('/');
|
||||
}
|
||||
}, [onboardingStatus, navigate]);
|
||||
|
||||
useEffect(() => {
|
||||
setCaptureHotkeyTypeInFocus(true);
|
||||
}, [setCaptureHotkeyTypeInFocus]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Title>Create profile</Title>
|
||||
<SubTitle>How you'll be identify on the app.</SubTitle>
|
||||
<SubTitle>How you'll be identified on the app.</SubTitle>
|
||||
<StyledContentContainer>
|
||||
<StyledSectionContainer>
|
||||
<SubSectionTitle title="Picture" />
|
||||
@ -71,21 +137,28 @@ export function CreateProfile() {
|
||||
<StyledComboInputContainer>
|
||||
<TextInput
|
||||
label="First Name"
|
||||
value=""
|
||||
value={firstName}
|
||||
placeholder="Tim"
|
||||
onChange={setFirstName}
|
||||
fullWidth
|
||||
/>
|
||||
<TextInput
|
||||
label="Last Name"
|
||||
value=""
|
||||
value={lastName}
|
||||
placeholder="Cook"
|
||||
onChange={setLastName}
|
||||
fullWidth
|
||||
/>
|
||||
</StyledComboInputContainer>
|
||||
</StyledSectionContainer>
|
||||
</StyledContentContainer>
|
||||
<StyledButtonContainer>
|
||||
<MainButton title="Continue" onClick={handleCreate} fullWidth />
|
||||
<MainButton
|
||||
title="Continue"
|
||||
onClick={handleCreate}
|
||||
disabled={!firstName || !lastName}
|
||||
fullWidth
|
||||
/>
|
||||
</StyledButtonContainer>
|
||||
</>
|
||||
);
|
||||
|
||||
@ -1,14 +1,19 @@
|
||||
import { useCallback } from 'react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { getOperationName } from '@apollo/client/utilities';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { SubTitle } from '@/auth/components/ui/SubTitle';
|
||||
import { Title } from '@/auth/components/ui/Title';
|
||||
import { useOnboardingStatus } from '@/auth/hooks/useOnboardingStatus';
|
||||
import { OnboardingStatus } from '@/auth/utils/getOnboardingStatus';
|
||||
import { MainButton } from '@/ui/components/buttons/MainButton';
|
||||
import { ImageInput } from '@/ui/components/inputs/ImageInput';
|
||||
import { TextInput } from '@/ui/components/inputs/TextInput';
|
||||
import { SubSectionTitle } from '@/ui/components/section-titles/SubSectionTitle';
|
||||
import { GET_CURRENT_USER } from '@/users/services';
|
||||
import { useUpdateWorkspaceMutation } from '~/generated/graphql';
|
||||
|
||||
const StyledContentContainer = styled.div`
|
||||
width: 100%;
|
||||
@ -29,10 +34,38 @@ const StyledButtonContainer = styled.div`
|
||||
|
||||
export function CreateWorkspace() {
|
||||
const navigate = useNavigate();
|
||||
const onboardingStatus = useOnboardingStatus();
|
||||
|
||||
const [workspaceName, setWorkspaceName] = useState('');
|
||||
|
||||
const [updateWorkspace] = useUpdateWorkspaceMutation();
|
||||
|
||||
const handleCreate = useCallback(async () => {
|
||||
navigate('/auth/create/profile');
|
||||
}, [navigate]);
|
||||
try {
|
||||
if (!workspaceName) {
|
||||
throw new Error('Workspace name is required');
|
||||
}
|
||||
|
||||
const { data, errors } = await updateWorkspace({
|
||||
variables: {
|
||||
data: {
|
||||
displayName: {
|
||||
set: workspaceName,
|
||||
},
|
||||
},
|
||||
},
|
||||
refetchQueries: [getOperationName(GET_CURRENT_USER) ?? ''],
|
||||
});
|
||||
|
||||
if (errors || !data?.updateWorkspace) {
|
||||
throw errors;
|
||||
}
|
||||
|
||||
navigate('/auth/create/profile');
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}, [navigate, updateWorkspace, workspaceName]);
|
||||
|
||||
useHotkeys(
|
||||
'enter',
|
||||
@ -46,6 +79,12 @@ export function CreateWorkspace() {
|
||||
[handleCreate],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (onboardingStatus !== OnboardingStatus.OngoingWorkspaceCreation) {
|
||||
navigate('/auth/create/profile');
|
||||
}
|
||||
}, [onboardingStatus, navigate]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Title>Create your workspace</Title>
|
||||
@ -63,11 +102,21 @@ export function CreateWorkspace() {
|
||||
title="Workspace name"
|
||||
description="The name of your organization"
|
||||
/>
|
||||
<TextInput value="" placeholder="Apple" fullWidth />
|
||||
<TextInput
|
||||
value={workspaceName}
|
||||
placeholder="Apple"
|
||||
onChange={setWorkspaceName}
|
||||
fullWidth
|
||||
/>
|
||||
</StyledSectionContainer>
|
||||
</StyledContentContainer>
|
||||
<StyledButtonContainer>
|
||||
<MainButton title="Continue" onClick={handleCreate} fullWidth />
|
||||
<MainButton
|
||||
title="Continue"
|
||||
onClick={handleCreate}
|
||||
disabled={!workspaceName}
|
||||
fullWidth
|
||||
/>
|
||||
</StyledButtonContainer>
|
||||
</>
|
||||
);
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import styled from '@emotion/styled';
|
||||
import { motion } from 'framer-motion';
|
||||
import { useRecoilState } from 'recoil';
|
||||
@ -10,7 +11,6 @@ import { Title } from '@/auth/components/ui/Title';
|
||||
import { useAuth } from '@/auth/hooks/useAuth';
|
||||
import { authFlowUserEmailState } from '@/auth/states/authFlowUserEmailState';
|
||||
import { isMockModeState } from '@/auth/states/isMockModeState';
|
||||
import { captureHotkeyTypeInFocusState } from '@/hotkeys/states/captureHotkeyTypeInFocusState';
|
||||
import { MainButton } from '@/ui/components/buttons/MainButton';
|
||||
import { TextInput } from '@/ui/components/inputs/TextInput';
|
||||
import { SubSectionTitle } from '@/ui/components/section-titles/SubSectionTitle';
|
||||
@ -47,17 +47,15 @@ const StyledErrorContainer = styled.div`
|
||||
`;
|
||||
|
||||
export function PasswordLogin() {
|
||||
const [, setMockMode] = useRecoilState(isMockModeState);
|
||||
const [, setCaptureHotkeyTypeInFocus] = useRecoilState(
|
||||
captureHotkeyTypeInFocusState,
|
||||
);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const prefillPassword =
|
||||
process.env.NODE_ENV === 'development' ? 'applecar2025' : '';
|
||||
process.env.NODE_ENV === 'development' ? 'Applecar2025' : '';
|
||||
|
||||
const [authFlowUserEmail, setAuthFlowUserEmail] = useRecoilState(
|
||||
authFlowUserEmailState,
|
||||
);
|
||||
const [, setMockMode] = useRecoilState(isMockModeState);
|
||||
const [internalPassword, setInternalPassword] = useState(prefillPassword);
|
||||
const [formError, setFormError] = useState('');
|
||||
|
||||
@ -65,21 +63,15 @@ export function PasswordLogin() {
|
||||
|
||||
const handleLogin = useCallback(async () => {
|
||||
try {
|
||||
await login(authFlowUserEmail, internalPassword);
|
||||
setMockMode(false);
|
||||
setCaptureHotkeyTypeInFocus(false);
|
||||
// TODO: Navigate to the workspace selection page when it's ready
|
||||
// navigate('/auth/create/workspace');
|
||||
|
||||
await login(authFlowUserEmail, internalPassword);
|
||||
|
||||
navigate('/auth/create/workspace');
|
||||
} catch (err: any) {
|
||||
setFormError(err.message);
|
||||
}
|
||||
}, [
|
||||
authFlowUserEmail,
|
||||
internalPassword,
|
||||
login,
|
||||
setMockMode,
|
||||
setCaptureHotkeyTypeInFocus,
|
||||
]);
|
||||
}, [login, authFlowUserEmail, internalPassword, setMockMode, navigate]);
|
||||
|
||||
useHotkeys(
|
||||
'enter',
|
||||
|
||||
@ -23,7 +23,7 @@ export const FilterByName: Story = {
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const filterButton = canvas.getByText('Filter');
|
||||
const filterButton = await canvas.findByText('Filter');
|
||||
await userEvent.click(filterButton);
|
||||
|
||||
const nameFilterButton = canvas
|
||||
@ -60,7 +60,7 @@ export const FilterByAccountOwner: Story = {
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const filterButton = canvas.getByText('Filter');
|
||||
const filterButton = await canvas.findByText('Filter');
|
||||
await userEvent.click(filterButton);
|
||||
|
||||
const accountOwnerFilterButton = (
|
||||
@ -83,7 +83,6 @@ export const FilterByAccountOwner: Story = {
|
||||
const charlesChip = canvas
|
||||
.getAllByTestId('dropdown-menu-item')
|
||||
.find((item) => {
|
||||
console.log({ item });
|
||||
return item.textContent?.includes('Charles Test');
|
||||
});
|
||||
|
||||
|
||||
@ -21,7 +21,7 @@ export const SortByName: Story = {
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const sortButton = canvas.getByText('Sort');
|
||||
const sortButton = await canvas.findByText('Sort');
|
||||
await userEvent.click(sortButton);
|
||||
|
||||
const nameSortButton = canvas.getByText('Name', { selector: 'li' });
|
||||
|
||||
@ -23,7 +23,7 @@ export const Email: Story = {
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const filterButton = canvas.getByText('Filter');
|
||||
const filterButton = await canvas.findByText('Filter');
|
||||
await userEvent.click(filterButton);
|
||||
|
||||
const emailFilterButton = canvas
|
||||
@ -59,7 +59,7 @@ export const CompanyName: Story = {
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const filterButton = canvas.getByText('Filter');
|
||||
const filterButton = await canvas.findByText('Filter');
|
||||
await userEvent.click(filterButton);
|
||||
|
||||
const companyFilterButton = canvas
|
||||
|
||||
@ -22,7 +22,7 @@ export const Email: Story = {
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const sortButton = canvas.getByText('Sort');
|
||||
const sortButton = await canvas.findByText('Sort');
|
||||
await userEvent.click(sortButton);
|
||||
|
||||
const emailSortButton = canvas.getByText('Email', { selector: 'li' });
|
||||
@ -48,7 +48,7 @@ export const Cancel: Story = {
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const sortButton = canvas.getByText('Sort');
|
||||
const sortButton = await canvas.findByText('Sort');
|
||||
await userEvent.click(sortButton);
|
||||
|
||||
const emailSortButton = canvas.getByText('Email', { selector: 'li' });
|
||||
|
||||
@ -1,4 +1,7 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { getOperationName } from '@apollo/client/utilities';
|
||||
import styled from '@emotion/styled';
|
||||
import debounce from 'lodash.debounce';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { currentUserState } from '@/auth/states/currentUserState';
|
||||
@ -7,6 +10,8 @@ import { TextInput } from '@/ui/components/inputs/TextInput';
|
||||
import { MainSectionTitle } from '@/ui/components/section-titles/MainSectionTitle';
|
||||
import { SubSectionTitle } from '@/ui/components/section-titles/SubSectionTitle';
|
||||
import { NoTopBarContainer } from '@/ui/layout/containers/NoTopBarContainer';
|
||||
import { GET_CURRENT_USER } from '@/users/services';
|
||||
import { useUpdateUserMutation } from '~/generated/graphql';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
display: flex;
|
||||
@ -34,6 +39,57 @@ const StyledComboInputContainer = styled.div`
|
||||
|
||||
export function SettingsProfile() {
|
||||
const currentUser = useRecoilValue(currentUserState);
|
||||
|
||||
const [firstName, setFirstName] = useState(currentUser?.firstName ?? '');
|
||||
const [lastName, setLastName] = useState(currentUser?.lastName ?? '');
|
||||
|
||||
const [updateUser] = useUpdateUserMutation();
|
||||
|
||||
// TODO: Enhance this with react-hook-form (https://www.react-hook-form.com)
|
||||
const debouncedUpdate = debounce(async () => {
|
||||
try {
|
||||
if (!currentUser?.id) {
|
||||
throw new Error('User is not logged in');
|
||||
}
|
||||
|
||||
const { data, errors } = await updateUser({
|
||||
variables: {
|
||||
where: {
|
||||
id: currentUser?.id,
|
||||
},
|
||||
data: {
|
||||
firstName: {
|
||||
set: firstName,
|
||||
},
|
||||
lastName: {
|
||||
set: lastName,
|
||||
},
|
||||
},
|
||||
},
|
||||
refetchQueries: [getOperationName(GET_CURRENT_USER) ?? ''],
|
||||
});
|
||||
|
||||
if (errors || !data?.updateUser) {
|
||||
throw errors;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}, 500);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
currentUser?.firstName !== firstName ||
|
||||
currentUser?.lastName !== lastName
|
||||
) {
|
||||
debouncedUpdate();
|
||||
}
|
||||
|
||||
return () => {
|
||||
debouncedUpdate.cancel();
|
||||
};
|
||||
}, [firstName, lastName, currentUser, debouncedUpdate]);
|
||||
|
||||
return (
|
||||
<NoTopBarContainer>
|
||||
<StyledContainer>
|
||||
@ -50,13 +106,15 @@ export function SettingsProfile() {
|
||||
<StyledComboInputContainer>
|
||||
<TextInput
|
||||
label="First Name"
|
||||
value={currentUser?.displayName}
|
||||
value={firstName}
|
||||
onChange={setFirstName}
|
||||
placeholder="Tim"
|
||||
fullWidth
|
||||
/>
|
||||
<TextInput
|
||||
label="Last Name"
|
||||
value=""
|
||||
value={lastName}
|
||||
onChange={setLastName}
|
||||
placeholder="Cook"
|
||||
fullWidth
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user