Files
twenty_crm/packages/twenty-front/src/pages/onboarding/InviteTeam.tsx

234 lines
6.9 KiB
TypeScript

import { SubTitle } from '@/auth/components/SubTitle';
import { Title } from '@/auth/components/Title';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { calendarBookingPageIdState } from '@/client-config/states/calendarBookingPageIdState';
import { useSetNextOnboardingStatus } from '@/onboarding/hooks/useSetNextOnboardingStatus';
import { PageHotkeyScope } from '@/types/PageHotkeyScope';
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { TextInputV2 } from '@/ui/input/components/TextInputV2';
import { Modal } from '@/ui/layout/modal/components/Modal';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { zodResolver } from '@hookform/resolvers/zod';
import { Trans, useLingui } from '@lingui/react/macro';
import { useCallback } from 'react';
import {
Controller,
SubmitHandler,
useFieldArray,
useForm,
} from 'react-hook-form';
import { useRecoilValue } from 'recoil';
import { Key } from 'ts-key-enum';
import { isDefined } from 'twenty-shared/utils';
import { IconCopy, SeparatorLineText } from 'twenty-ui/display';
import { LightButton, MainButton } from 'twenty-ui/input';
import { ClickToActionLink } from 'twenty-ui/navigation';
import { z } from 'zod';
import { useCreateWorkspaceInvitation } from '../../modules/workspace-invitation/hooks/useCreateWorkspaceInvitation';
const StyledAnimatedContainer = styled.div`
display: flex;
flex-direction: column;
padding: ${({ theme }) => theme.spacing(8)} 0;
gap: ${({ theme }) => theme.spacing(4)};
overflow-y: scroll;
overflow-x: hidden;
width: 100%;
`;
const StyledActionLinkContainer = styled.div`
display: flex;
justify-content: center;
`;
const StyledButtonContainer = styled.div`
display: flex;
justify-content: center;
width: 200px;
`;
const StyledActionSkipLinkContainer = styled.div`
margin: ${({ theme }) => theme.spacing(3)} 0 0;
`;
const validationSchema = z.object({
emails: z.array(
z.object({ email: z.union([z.literal(''), z.string().email()]) }),
),
});
type FormInput = z.infer<typeof validationSchema>;
export const InviteTeam = () => {
const { t } = useLingui();
const theme = useTheme();
const { enqueueSnackBar } = useSnackBar();
const { sendInvitation } = useCreateWorkspaceInvitation();
const setNextOnboardingStatus = useSetNextOnboardingStatus();
const currentWorkspace = useRecoilValue(currentWorkspaceState);
const calendarBookingPageId = useRecoilValue(calendarBookingPageIdState);
const hasCalendarBooking = isDefined(calendarBookingPageId);
const {
control,
handleSubmit,
watch,
formState: { isValid, isSubmitting },
} = useForm<FormInput>({
mode: 'onChange',
defaultValues: {
emails: [{ email: '' }, { email: '' }, { email: '' }],
},
resolver: zodResolver(validationSchema),
});
const { fields, append, remove } = useFieldArray({
control,
name: 'emails',
});
watch(({ emails }) => {
if (!emails) {
return;
}
const emailValues = emails.map((email) => email?.email);
if (emailValues[emailValues.length - 1] !== '') {
append({ email: '' });
}
if (emailValues.length > 3 && emailValues[emailValues.length - 2] === '') {
remove(emailValues.length - 1);
}
});
const getPlaceholder = (emailIndex: number) => {
if (emailIndex === 0) {
return 'tim@apple.com';
}
if (emailIndex === 1) {
return 'phil@apple.com';
}
if (emailIndex === 2) {
return 'jony@apple.com';
}
return 'craig@apple.com';
};
const copyInviteLink = () => {
if (isDefined(currentWorkspace?.inviteHash)) {
const inviteLink = `${window.location.origin}/invite/${currentWorkspace?.inviteHash}`;
navigator.clipboard.writeText(inviteLink);
enqueueSnackBar(t`Link copied to clipboard`, {
variant: SnackBarVariant.Success,
icon: <IconCopy size={theme.icon.size.md} />,
duration: 2000,
});
}
};
const onSubmit: SubmitHandler<FormInput> = useCallback(
async (data) => {
const emails = Array.from(
new Set(
data.emails
.map((emailData) => emailData.email.trim())
.filter((email) => email.length > 0),
),
);
const result = await sendInvitation({ emails });
if (isDefined(result.errors)) {
throw result.errors;
}
if (emails.length > 0) {
enqueueSnackBar(t`Invite link sent to email addresses`, {
variant: SnackBarVariant.Success,
duration: 2000,
});
}
setNextOnboardingStatus();
},
[enqueueSnackBar, sendInvitation, setNextOnboardingStatus, t],
);
const handleSkip = async () => {
await onSubmit({ emails: [] });
};
useScopedHotkeys(
[Key.Enter],
() => {
handleSubmit(onSubmit)();
},
PageHotkeyScope.InviteTeam,
[handleSubmit],
);
return (
<Modal.Content isVerticalCentered isHorizontalCentered>
<Title>
<Trans>Invite your team</Trans>
</Title>
<SubTitle>
<Trans>Get the most out of your workspace by inviting your team.</Trans>
</SubTitle>
<StyledAnimatedContainer>
{fields.map((field, index) => (
<Controller
key={index}
name={`emails.${index}.email`}
control={control}
render={({
field: { onChange, onBlur, value },
fieldState: { error },
}) => (
<TextInputV2
autoFocus={index === 0}
type="email"
value={value}
placeholder={getPlaceholder(index)}
onBlur={onBlur}
error={error?.message}
onChange={onChange}
noErrorHelper
fullWidth
/>
)}
/>
))}
{isDefined(currentWorkspace?.inviteHash) && (
<>
<SeparatorLineText>
<Trans>or</Trans>
</SeparatorLineText>
<StyledActionLinkContainer>
<LightButton
title={t`Copy invitation link`}
accent="tertiary"
onClick={copyInviteLink}
Icon={IconCopy}
/>
</StyledActionLinkContainer>
</>
)}
</StyledAnimatedContainer>
<StyledButtonContainer>
<MainButton
title={hasCalendarBooking ? t`Continue` : t`Finish`}
disabled={!isValid || isSubmitting}
onClick={handleSubmit(onSubmit)}
fullWidth
/>
</StyledButtonContainer>
<StyledActionSkipLinkContainer>
<ClickToActionLink onClick={handleSkip}>
<Trans>Skip</Trans>
</ClickToActionLink>
</StyledActionSkipLinkContainer>
</Modal.Content>
);
};