Email translation and snackbar translation (#10395)
This pull request focuses on improving localization by replacing hardcoded strings with translatable strings using the `Trans` component from `@lingui/react/macro`. Additionally, it introduces locale support to several email components. Here are the most important changes: ### Localization Improvements: * Replaced hardcoded strings with `Trans` components in various email templates to support localization. (`packages/twenty-emails/src/emails/clean-suspended-workspace.email.tsx`, `packages/twenty-emails/src/emails/password-reset-link.email.tsx`, `packages/twenty-emails/src/emails/password-update-notify.email.tsx`, `packages/twenty-emails/src/emails/send-email-verification-link.email.tsx`, `packages/twenty-emails/src/emails/send-invite-link.email.tsx`, `packages/twenty-emails/src/emails/warn-suspended-workspace.email.tsx`) [[1]](diffhunk://#diff-ca227a03c0aa66428daff938c743435e8a4dc3ffa960c0952f2697a23e280fdbR6-R25) [[2]](diffhunk://#diff-ca227a03c0aa66428daff938c743435e8a4dc3ffa960c0952f2697a23e280fdbL42-R45) [[3]](diffhunk://#diff-523cd37f5680ce418450946f62b7804b6586158efb190ced73920ef0fdf96bc8L1) [[4]](diffhunk://#diff-523cd37f5680ce418450946f62b7804b6586158efb190ced73920ef0fdf96bc8L23-R23) [[5]](diffhunk://#diff-cf16aa55d3eeb6be606bbe93de4c83b6f146c49b60d6f512d4b87e49fe14338cL29-R29) [[6]](diffhunk://#diff-cf16aa55d3eeb6be606bbe93de4c83b6f146c49b60d6f512d4b87e49fe14338cL46-R46) [[7]](diffhunk://#diff-16b613160f937563ec108176f595d8f275a1d87a5b8245d84df60d775f3efebeL1) [[8]](diffhunk://#diff-16b613160f937563ec108176f595d8f275a1d87a5b8245d84df60d775f3efebeL22-R22) [[9]](diffhunk://#diff-0da62e7cc5cfcb32cc25f067fa1d50123047c239af210398f065455ab6700886L1) [[10]](diffhunk://#diff-0da62e7cc5cfcb32cc25f067fa1d50123047c239af210398f065455ab6700886L42-R41) [[11]](diffhunk://#diff-0da62e7cc5cfcb32cc25f067fa1d50123047c239af210398f065455ab6700886L57-R56) [[12]](diffhunk://#diff-483346065c074946a43c18492334bd680422a1d4cb994dc8c3cd39d0208e6016L1-R21) [[13]](diffhunk://#diff-483346065c074946a43c18492334bd680422a1d4cb994dc8c3cd39d0208e6016L28-R31) [[14]](diffhunk://#diff-483346065c074946a43c18492334bd680422a1d4cb994dc8c3cd39d0208e6016L53-R55) ### Locale Support: * Added `locale` prop to email components to dynamically set the locale. (`packages/twenty-emails/src/emails/clean-suspended-workspace.email.tsx`, `packages/twenty-emails/src/emails/warn-suspended-workspace.email.tsx`) [[1]](diffhunk://#diff-ca227a03c0aa66428daff938c743435e8a4dc3ffa960c0952f2697a23e280fdbR6-R25) [[2]](diffhunk://#diff-483346065c074946a43c18492334bd680422a1d4cb994dc8c3cd39d0208e6016L1-R21) ### SnackBar Messages: * Replaced hardcoded SnackBar messages with translatable strings using the `t` function from `@lingui/react/macro`. (`packages/twenty-front/src/modules/auth/components/VerifyEmailEffect.tsx`, `packages/twenty-front/src/modules/auth/hooks/useVerifyLogin.ts`, `packages/twenty-front/src/modules/auth/sign-in-up/hooks/useHandleResendEmailVerificationToken.ts`, `packages/twenty-front/src/modules/auth/sign-in-up/hooks/useHandleResetPassword.ts`, `packages/twenty-front/src/modules/object-record/record-field/components/LightCopyIconButton.tsx`, `packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/PhonesFieldDisplay.tsx`) [[1]](diffhunk://#diff-551f2f94eacd8856d22bab7e63dd3ad693f87e9fa9b289864802ebc387f72b42R7) [[2]](diffhunk://#diff-551f2f94eacd8856d22bab7e63dd3ad693f87e9fa9b289864802ebc387f72b42L24-R29) [[3]](diffhunk://#diff-551f2f94eacd8856d22bab7e63dd3ad693f87e9fa9b289864802ebc387f72b42L43-R51) [[4]](diffhunk://#diff-428199461992a01325159f5fbf826d845f05f3361279eccd3f1ce416e0114845R7-R15) [[5]](diffhunk://#diff-428199461992a01325159f5fbf826d845f05f3361279eccd3f1ce416e0114845L24-R26) [[6]](diffhunk://#diff-cde42d6abfed63e52c2bda09d537a6577148d0baf957fde75ceaa8657ed58403R5) [[7]](diffhunk://#diff-cde42d6abfed63e52c2bda09d537a6577148d0baf957fde75ceaa8657ed58403L16-R17) [[8]](diffhunk://#diff-cde42d6abfed63e52c2bda09d537a6577148d0baf957fde75ceaa8657ed58403L28-R33) [[9]](diffhunk://#diff-9332c1988864863f12516c2fb77e814af60bedb37c36ffa094f49afc335d5457R5-R17) [[10]](diffhunk://#diff-9332c1988864863f12516c2fb77e814af60bedb37c36ffa094f49afc335d5457L27-R33) [[11]](diffhunk://#diff-9332c1988864863f12516c2fb77e814af60bedb37c36ffa094f49afc335d5457L42-R44) [[12]](diffhunk://#diff-8d64afa825b47ab71d18e3e284408e2097f5fd2365eae84d9d25d3568c48e49cR7) [[13]](diffhunk://#diff-8d64afa825b47ab71d18e3e284408e2097f5fd2365eae84d9d25d3568c48e49cR20-R28) [[14]](diffhunk://#diff-6e4361ded2b5656afaeb1befa8b1d23a45b490a1118550da290e27cdb8ebcdceR6) [[15]](diffhunk://#diff-6e4361ded2b5656afaeb1befa8b1d23a45b490a1118550da290e27cdb8ebcdceR19-R20) [[16]](diffhunk://#diff-6e4361ded2b5656afaeb1befa8b1d23a45b490a1118550da290e27cdb8ebcdceL29-R38)
This commit is contained in:
@ -3,25 +3,30 @@ import { BaseEmail } from 'src/components/BaseEmail';
|
|||||||
import { CallToAction } from 'src/components/CallToAction';
|
import { CallToAction } from 'src/components/CallToAction';
|
||||||
import { MainText } from 'src/components/MainText';
|
import { MainText } from 'src/components/MainText';
|
||||||
import { Title } from 'src/components/Title';
|
import { Title } from 'src/components/Title';
|
||||||
|
import { APP_LOCALES } from 'twenty-shared';
|
||||||
|
|
||||||
type CleanSuspendedWorkspaceEmailProps = {
|
type CleanSuspendedWorkspaceEmailProps = {
|
||||||
daysSinceInactive: number;
|
daysSinceInactive: number;
|
||||||
userName: string;
|
userName: string;
|
||||||
workspaceDisplayName: string | undefined;
|
workspaceDisplayName: string | undefined;
|
||||||
|
locale: keyof typeof APP_LOCALES;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CleanSuspendedWorkspaceEmail = ({
|
export const CleanSuspendedWorkspaceEmail = ({
|
||||||
daysSinceInactive,
|
daysSinceInactive,
|
||||||
userName,
|
userName,
|
||||||
workspaceDisplayName,
|
workspaceDisplayName,
|
||||||
|
locale,
|
||||||
}: CleanSuspendedWorkspaceEmailProps) => {
|
}: CleanSuspendedWorkspaceEmailProps) => {
|
||||||
const helloString = userName?.length > 1 ? `Hello ${userName}` : 'Hello';
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseEmail width={333} locale="en">
|
<BaseEmail width={333} locale={locale}>
|
||||||
<Title value="Deleted Workspace 🥺" />
|
<Title value={<Trans>Deleted Workspace</Trans>} />
|
||||||
<MainText>
|
<MainText>
|
||||||
{helloString},
|
{userName?.length > 1 ? (
|
||||||
|
<Trans>Dear {userName},</Trans>
|
||||||
|
) : (
|
||||||
|
<Trans>Hello,</Trans>
|
||||||
|
)}
|
||||||
<br />
|
<br />
|
||||||
<br />
|
<br />
|
||||||
<Trans>
|
<Trans>
|
||||||
@ -39,7 +44,7 @@ export const CleanSuspendedWorkspaceEmail = ({
|
|||||||
</MainText>
|
</MainText>
|
||||||
<CallToAction
|
<CallToAction
|
||||||
href="https://app.twenty.com/"
|
href="https://app.twenty.com/"
|
||||||
value="Create a new workspace"
|
value={<Trans>Create a new workspace</Trans>}
|
||||||
/>
|
/>
|
||||||
</BaseEmail>
|
</BaseEmail>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
import { t } from '@lingui/core/macro';
|
|
||||||
import { Trans } from '@lingui/react/macro';
|
import { Trans } from '@lingui/react/macro';
|
||||||
import { BaseEmail } from 'src/components/BaseEmail';
|
import { BaseEmail } from 'src/components/BaseEmail';
|
||||||
import { CallToAction } from 'src/components/CallToAction';
|
import { CallToAction } from 'src/components/CallToAction';
|
||||||
@ -20,8 +19,8 @@ export const PasswordResetLinkEmail = ({
|
|||||||
}: PasswordResetLinkEmailProps) => {
|
}: PasswordResetLinkEmailProps) => {
|
||||||
return (
|
return (
|
||||||
<BaseEmail locale={locale}>
|
<BaseEmail locale={locale}>
|
||||||
<Title value={t`Reset your password 🗝`} />
|
<Title value={<Trans>Reset your password 🗝</Trans>} />
|
||||||
<CallToAction href={link} value={t`Reset`} />
|
<CallToAction href={link} value={<Trans>Reset</Trans>} />
|
||||||
<MainText>
|
<MainText>
|
||||||
<Trans>
|
<Trans>
|
||||||
This link is only valid for the next {duration}. If the link does not
|
This link is only valid for the next {duration}. If the link does not
|
||||||
|
|||||||
@ -26,7 +26,7 @@ export const PasswordUpdateNotifyEmail = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseEmail locale={locale}>
|
<BaseEmail locale={locale}>
|
||||||
<Title value={t`Password updated`} />
|
<Title value={<Trans>Password updated</Trans>} />
|
||||||
<MainText>
|
<MainText>
|
||||||
{helloString},
|
{helloString},
|
||||||
<br />
|
<br />
|
||||||
@ -43,7 +43,7 @@ export const PasswordUpdateNotifyEmail = ({
|
|||||||
</Trans>
|
</Trans>
|
||||||
<br />
|
<br />
|
||||||
</MainText>
|
</MainText>
|
||||||
<CallToAction value={t`Connect to Twenty`} href={link} />
|
<CallToAction value={<Trans>Connect to Twenty</Trans>} href={link} />
|
||||||
</BaseEmail>
|
</BaseEmail>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
import { t } from '@lingui/core/macro';
|
|
||||||
import { Trans } from '@lingui/react/macro';
|
import { Trans } from '@lingui/react/macro';
|
||||||
|
|
||||||
import { BaseEmail } from 'src/components/BaseEmail';
|
import { BaseEmail } from 'src/components/BaseEmail';
|
||||||
@ -19,8 +18,8 @@ export const SendEmailVerificationLinkEmail = ({
|
|||||||
}: SendEmailVerificationLinkEmailProps) => {
|
}: SendEmailVerificationLinkEmailProps) => {
|
||||||
return (
|
return (
|
||||||
<BaseEmail width={333} locale={locale}>
|
<BaseEmail width={333} locale={locale}>
|
||||||
<Title value={t`Confirm your email address`} />
|
<Title value={<Trans>Confirm your email address</Trans>} />
|
||||||
<CallToAction href={link} value={t`Verify Email`} />
|
<CallToAction href={link} value={<Trans>Verify Email</Trans>} />
|
||||||
<br />
|
<br />
|
||||||
<br />
|
<br />
|
||||||
<MainText>
|
<MainText>
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
import { t } from '@lingui/core/macro';
|
|
||||||
import { Trans } from '@lingui/react/macro';
|
import { Trans } from '@lingui/react/macro';
|
||||||
import { Img } from '@react-email/components';
|
import { Img } from '@react-email/components';
|
||||||
import { emailTheme } from 'src/common-style';
|
import { emailTheme } from 'src/common-style';
|
||||||
@ -39,7 +38,7 @@ export const SendInviteLinkEmail = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseEmail width={333} locale={locale}>
|
<BaseEmail width={333} locale={locale}>
|
||||||
<Title value={t`Join your team on Twenty`} />
|
<Title value={<Trans>Join your team on Twenty</Trans>} />
|
||||||
<MainText>
|
<MainText>
|
||||||
{capitalize(sender.firstName)} (
|
{capitalize(sender.firstName)} (
|
||||||
<Link
|
<Link
|
||||||
@ -54,7 +53,7 @@ export const SendInviteLinkEmail = ({
|
|||||||
<HighlightedContainer>
|
<HighlightedContainer>
|
||||||
{workspaceLogo && <Img src={workspaceLogo} width={40} height={40} />}
|
{workspaceLogo && <Img src={workspaceLogo} width={40} height={40} />}
|
||||||
{workspace.name && <HighlightedText value={workspace.name} />}
|
{workspace.name && <HighlightedText value={workspace.name} />}
|
||||||
<CallToAction href={link} value={t`Accept invite`} />
|
<CallToAction href={link} value={<Trans>Accept invite</Trans>} />
|
||||||
</HighlightedContainer>
|
</HighlightedContainer>
|
||||||
<WhatIsTwenty />
|
<WhatIsTwenty />
|
||||||
</BaseEmail>
|
</BaseEmail>
|
||||||
|
|||||||
@ -1,15 +1,16 @@
|
|||||||
import { t } from '@lingui/core/macro';
|
|
||||||
import { Trans } from '@lingui/react/macro';
|
import { Trans } from '@lingui/react/macro';
|
||||||
import { BaseEmail } from 'src/components/BaseEmail';
|
import { BaseEmail } from 'src/components/BaseEmail';
|
||||||
import { CallToAction } from 'src/components/CallToAction';
|
import { CallToAction } from 'src/components/CallToAction';
|
||||||
import { MainText } from 'src/components/MainText';
|
import { MainText } from 'src/components/MainText';
|
||||||
import { Title } from 'src/components/Title';
|
import { Title } from 'src/components/Title';
|
||||||
|
import { APP_LOCALES } from 'twenty-shared';
|
||||||
|
|
||||||
type WarnSuspendedWorkspaceEmailProps = {
|
type WarnSuspendedWorkspaceEmailProps = {
|
||||||
daysSinceInactive: number;
|
daysSinceInactive: number;
|
||||||
inactiveDaysBeforeDelete: number;
|
inactiveDaysBeforeDelete: number;
|
||||||
userName: string;
|
userName: string;
|
||||||
workspaceDisplayName: string | undefined;
|
workspaceDisplayName: string | undefined;
|
||||||
|
locale: keyof typeof APP_LOCALES;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const WarnSuspendedWorkspaceEmail = ({
|
export const WarnSuspendedWorkspaceEmail = ({
|
||||||
@ -17,6 +18,7 @@ export const WarnSuspendedWorkspaceEmail = ({
|
|||||||
inactiveDaysBeforeDelete,
|
inactiveDaysBeforeDelete,
|
||||||
userName,
|
userName,
|
||||||
workspaceDisplayName,
|
workspaceDisplayName,
|
||||||
|
locale,
|
||||||
}: WarnSuspendedWorkspaceEmailProps) => {
|
}: WarnSuspendedWorkspaceEmailProps) => {
|
||||||
const daysLeft = inactiveDaysBeforeDelete - daysSinceInactive;
|
const daysLeft = inactiveDaysBeforeDelete - daysSinceInactive;
|
||||||
const dayOrDays = daysLeft > 1 ? 'days' : 'day';
|
const dayOrDays = daysLeft > 1 ? 'days' : 'day';
|
||||||
@ -25,8 +27,8 @@ export const WarnSuspendedWorkspaceEmail = ({
|
|||||||
const helloString = userName?.length > 1 ? `Hello ${userName}` : 'Hello';
|
const helloString = userName?.length > 1 ? `Hello ${userName}` : 'Hello';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseEmail width={333} locale="en">
|
<BaseEmail width={333} locale={locale}>
|
||||||
<Title value="Suspended Workspace 😴" />
|
<Title value={<Trans>Suspended Workspace </Trans>} />
|
||||||
<MainText>
|
<MainText>
|
||||||
{helloString},
|
{helloString},
|
||||||
<br />
|
<br />
|
||||||
@ -50,7 +52,7 @@ export const WarnSuspendedWorkspaceEmail = ({
|
|||||||
</MainText>
|
</MainText>
|
||||||
<CallToAction
|
<CallToAction
|
||||||
href="https://app.twenty.com/settings/billing"
|
href="https://app.twenty.com/settings/billing"
|
||||||
value={t`Update your subscription`}
|
value={<Trans>Update your subscription</Trans>}
|
||||||
/>
|
/>
|
||||||
</BaseEmail>
|
</BaseEmail>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/Snac
|
|||||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||||
|
|
||||||
import { useReadCaptchaToken } from '@/captcha/hooks/useReadCaptchaToken';
|
import { useReadCaptchaToken } from '@/captcha/hooks/useReadCaptchaToken';
|
||||||
|
import { useLingui } from '@lingui/react/macro';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useSearchParams } from 'react-router-dom';
|
import { useSearchParams } from 'react-router-dom';
|
||||||
import { useNavigateApp } from '~/hooks/useNavigateApp';
|
import { useNavigateApp } from '~/hooks/useNavigateApp';
|
||||||
@ -21,11 +22,11 @@ export const VerifyEmailEffect = () => {
|
|||||||
|
|
||||||
const navigate = useNavigateApp();
|
const navigate = useNavigateApp();
|
||||||
const { readCaptchaToken } = useReadCaptchaToken();
|
const { readCaptchaToken } = useReadCaptchaToken();
|
||||||
|
const { t } = useLingui();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const verifyEmailToken = async () => {
|
const verifyEmailToken = async () => {
|
||||||
if (!email || !emailVerificationToken) {
|
if (!email || !emailVerificationToken) {
|
||||||
enqueueSnackBar(`Invalid email verification link.`, {
|
enqueueSnackBar(t`Invalid email verification link.`, {
|
||||||
dedupeKey: 'email-verification-link-dedupe-key',
|
dedupeKey: 'email-verification-link-dedupe-key',
|
||||||
variant: SnackBarVariant.Error,
|
variant: SnackBarVariant.Error,
|
||||||
});
|
});
|
||||||
@ -40,14 +41,14 @@ export const VerifyEmailEffect = () => {
|
|||||||
captchaToken,
|
captchaToken,
|
||||||
);
|
);
|
||||||
|
|
||||||
enqueueSnackBar('Email verified.', {
|
enqueueSnackBar(t`Email verified.`, {
|
||||||
dedupeKey: 'email-verification-dedupe-key',
|
dedupeKey: 'email-verification-dedupe-key',
|
||||||
variant: SnackBarVariant.Success,
|
variant: SnackBarVariant.Success,
|
||||||
});
|
});
|
||||||
|
|
||||||
navigate(AppPath.Verify, undefined, { loginToken: loginToken.token });
|
navigate(AppPath.Verify, undefined, { loginToken: loginToken.token });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
enqueueSnackBar('Email verification failed.', {
|
enqueueSnackBar(t`Email verification failed.`, {
|
||||||
dedupeKey: 'email-verification-dedupe-key',
|
dedupeKey: 'email-verification-dedupe-key',
|
||||||
variant: SnackBarVariant.Error,
|
variant: SnackBarVariant.Error,
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,5 +1,8 @@
|
|||||||
|
import { i18n } from '@lingui/core';
|
||||||
|
import { I18nProvider } from '@lingui/react';
|
||||||
import { renderHook } from '@testing-library/react';
|
import { renderHook } from '@testing-library/react';
|
||||||
import { RecoilRoot } from 'recoil';
|
import { RecoilRoot } from 'recoil';
|
||||||
|
import { SOURCE_LOCALE } from 'twenty-shared';
|
||||||
|
|
||||||
import { AppPath } from '@/types/AppPath';
|
import { AppPath } from '@/types/AppPath';
|
||||||
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
||||||
@ -8,6 +11,7 @@ import { useNavigateApp } from '~/hooks/useNavigateApp';
|
|||||||
import { useAuth } from '../useAuth';
|
import { useAuth } from '../useAuth';
|
||||||
import { useVerifyLogin } from '../useVerifyLogin';
|
import { useVerifyLogin } from '../useVerifyLogin';
|
||||||
|
|
||||||
|
import { dynamicActivate } from '~/utils/i18n/dynamicActivate';
|
||||||
jest.mock('../useAuth', () => ({
|
jest.mock('../useAuth', () => ({
|
||||||
useAuth: jest.fn(),
|
useAuth: jest.fn(),
|
||||||
}));
|
}));
|
||||||
@ -20,9 +24,12 @@ jest.mock('~/hooks/useNavigateApp', () => ({
|
|||||||
useNavigateApp: jest.fn(),
|
useNavigateApp: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
dynamicActivate(SOURCE_LOCALE);
|
||||||
|
|
||||||
const renderHooks = () => {
|
const renderHooks = () => {
|
||||||
const { result } = renderHook(() => useVerifyLogin(), {
|
const { result } = renderHook(() => useVerifyLogin(), {
|
||||||
wrapper: RecoilRoot,
|
wrapper: ({ children }) =>
|
||||||
|
RecoilRoot({ children: I18nProvider({ i18n, children }) }),
|
||||||
});
|
});
|
||||||
return { result };
|
return { result };
|
||||||
};
|
};
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import { useAuth } from '@/auth/hooks/useAuth';
|
|||||||
import { isAppWaitingForFreshObjectMetadataState } from '@/object-metadata/states/isAppWaitingForFreshObjectMetadataState';
|
import { isAppWaitingForFreshObjectMetadataState } from '@/object-metadata/states/isAppWaitingForFreshObjectMetadataState';
|
||||||
import { AppPath } from '@/types/AppPath';
|
import { AppPath } from '@/types/AppPath';
|
||||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||||
|
import { useLingui } from '@lingui/react/macro';
|
||||||
import { useSetRecoilState } from 'recoil';
|
import { useSetRecoilState } from 'recoil';
|
||||||
import { useNavigateApp } from '~/hooks/useNavigateApp';
|
import { useNavigateApp } from '~/hooks/useNavigateApp';
|
||||||
|
|
||||||
@ -11,6 +12,7 @@ export const useVerifyLogin = () => {
|
|||||||
const { enqueueSnackBar } = useSnackBar();
|
const { enqueueSnackBar } = useSnackBar();
|
||||||
const navigate = useNavigateApp();
|
const navigate = useNavigateApp();
|
||||||
const { getAuthTokensFromLoginToken } = useAuth();
|
const { getAuthTokensFromLoginToken } = useAuth();
|
||||||
|
const { t } = useLingui();
|
||||||
|
|
||||||
const setIsAppWaitingForFreshObjectMetadata = useSetRecoilState(
|
const setIsAppWaitingForFreshObjectMetadata = useSetRecoilState(
|
||||||
isAppWaitingForFreshObjectMetadataState,
|
isAppWaitingForFreshObjectMetadataState,
|
||||||
@ -21,7 +23,7 @@ export const useVerifyLogin = () => {
|
|||||||
setIsAppWaitingForFreshObjectMetadata(true);
|
setIsAppWaitingForFreshObjectMetadata(true);
|
||||||
await getAuthTokensFromLoginToken(loginToken);
|
await getAuthTokensFromLoginToken(loginToken);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
enqueueSnackBar('Authentication failed', {
|
enqueueSnackBar(t`Authentication failed`, {
|
||||||
variant: SnackBarVariant.Error,
|
variant: SnackBarVariant.Error,
|
||||||
});
|
});
|
||||||
navigate(AppPath.SignInUp);
|
navigate(AppPath.SignInUp);
|
||||||
|
|||||||
@ -1,29 +1,46 @@
|
|||||||
|
import { i18n } from '@lingui/core';
|
||||||
|
import { I18nProvider } from '@lingui/react';
|
||||||
import { act, renderHook } from '@testing-library/react';
|
import { act, renderHook } from '@testing-library/react';
|
||||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
import { RecoilRoot } from 'recoil';
|
||||||
import { useEmailPasswordResetLinkMutation } from '~/generated/graphql';
|
import { SOURCE_LOCALE } from 'twenty-shared';
|
||||||
|
|
||||||
import { useHandleResetPassword } from '@/auth/sign-in-up/hooks/useHandleResetPassword';
|
import { useHandleResetPassword } from '@/auth/sign-in-up/hooks/useHandleResetPassword';
|
||||||
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
||||||
|
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||||
|
import { useEmailPasswordResetLinkMutation } from '~/generated/graphql';
|
||||||
|
import { dynamicActivate } from '~/utils/i18n/dynamicActivate';
|
||||||
|
|
||||||
// Mocks
|
// Mocks
|
||||||
jest.mock('@/ui/feedback/snack-bar-manager/hooks/useSnackBar');
|
jest.mock('@/ui/feedback/snack-bar-manager/hooks/useSnackBar');
|
||||||
jest.mock('~/generated/graphql');
|
jest.mock('~/generated/graphql');
|
||||||
|
|
||||||
|
dynamicActivate(SOURCE_LOCALE);
|
||||||
|
|
||||||
|
const renderHooks = () => {
|
||||||
|
const { result } = renderHook(() => useHandleResetPassword(), {
|
||||||
|
wrapper: ({ children }) =>
|
||||||
|
RecoilRoot({ children: I18nProvider({ i18n, children }) }),
|
||||||
|
});
|
||||||
|
return { result };
|
||||||
|
};
|
||||||
|
|
||||||
describe('useHandleResetPassword', () => {
|
describe('useHandleResetPassword', () => {
|
||||||
const enqueueSnackBarMock = jest.fn();
|
const enqueueSnackBarMock = jest.fn();
|
||||||
const emailPasswordResetLinkMock = jest.fn();
|
const emailPasswordResetLinkMock = jest.fn();
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
|
||||||
(useSnackBar as jest.Mock).mockReturnValue({
|
(useSnackBar as jest.Mock).mockReturnValue({
|
||||||
enqueueSnackBar: enqueueSnackBarMock,
|
enqueueSnackBar: enqueueSnackBarMock,
|
||||||
});
|
});
|
||||||
(useEmailPasswordResetLinkMutation as jest.Mock).mockReturnValue([
|
(useEmailPasswordResetLinkMutation as jest.Mock).mockReturnValue([
|
||||||
emailPasswordResetLinkMock,
|
emailPasswordResetLinkMock,
|
||||||
]);
|
]);
|
||||||
jest.clearAllMocks();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should show error message if email is invalid', async () => {
|
it('should show error message if email is invalid', async () => {
|
||||||
const { result } = renderHook(() => useHandleResetPassword());
|
const { result } = renderHooks();
|
||||||
await act(() => result.current.handleResetPassword('')());
|
await act(() => result.current.handleResetPassword('')());
|
||||||
|
|
||||||
expect(enqueueSnackBarMock).toHaveBeenCalledWith('Invalid email', {
|
expect(enqueueSnackBarMock).toHaveBeenCalledWith('Invalid email', {
|
||||||
@ -36,7 +53,7 @@ describe('useHandleResetPassword', () => {
|
|||||||
data: { emailPasswordResetLink: { success: true } },
|
data: { emailPasswordResetLink: { success: true } },
|
||||||
});
|
});
|
||||||
|
|
||||||
const { result } = renderHook(() => useHandleResetPassword());
|
const { result } = renderHooks();
|
||||||
await act(() => result.current.handleResetPassword('test@example.com')());
|
await act(() => result.current.handleResetPassword('test@example.com')());
|
||||||
|
|
||||||
expect(enqueueSnackBarMock).toHaveBeenCalledWith(
|
expect(enqueueSnackBarMock).toHaveBeenCalledWith(
|
||||||
@ -50,10 +67,10 @@ describe('useHandleResetPassword', () => {
|
|||||||
data: { emailPasswordResetLink: { success: false } },
|
data: { emailPasswordResetLink: { success: false } },
|
||||||
});
|
});
|
||||||
|
|
||||||
const { result } = renderHook(() => useHandleResetPassword());
|
const { result } = renderHooks();
|
||||||
await act(() => result.current.handleResetPassword('test@example.com')());
|
await act(() => result.current.handleResetPassword('test@example.com')());
|
||||||
|
|
||||||
expect(enqueueSnackBarMock).toHaveBeenCalledWith('There was some issue', {
|
expect(enqueueSnackBarMock).toHaveBeenCalledWith('There was an issue', {
|
||||||
variant: SnackBarVariant.Error,
|
variant: SnackBarVariant.Error,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -62,7 +79,7 @@ describe('useHandleResetPassword', () => {
|
|||||||
const errorMessage = 'Network Error';
|
const errorMessage = 'Network Error';
|
||||||
emailPasswordResetLinkMock.mockRejectedValue(new Error(errorMessage));
|
emailPasswordResetLinkMock.mockRejectedValue(new Error(errorMessage));
|
||||||
|
|
||||||
const { result } = renderHook(() => useHandleResetPassword());
|
const { result } = renderHooks();
|
||||||
await act(() => result.current.handleResetPassword('test@example.com')());
|
await act(() => result.current.handleResetPassword('test@example.com')());
|
||||||
|
|
||||||
expect(enqueueSnackBarMock).toHaveBeenCalledWith(errorMessage, {
|
expect(enqueueSnackBarMock).toHaveBeenCalledWith(errorMessage, {
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import { useCallback } from 'react';
|
|||||||
|
|
||||||
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
||||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||||
|
import { t } from '@lingui/core/macro';
|
||||||
import { useResendEmailVerificationTokenMutation } from '~/generated/graphql';
|
import { useResendEmailVerificationTokenMutation } from '~/generated/graphql';
|
||||||
|
|
||||||
export const useHandleResendEmailVerificationToken = () => {
|
export const useHandleResendEmailVerificationToken = () => {
|
||||||
@ -13,7 +14,7 @@ export const useHandleResendEmailVerificationToken = () => {
|
|||||||
(email: string | null) => {
|
(email: string | null) => {
|
||||||
return async () => {
|
return async () => {
|
||||||
if (!email) {
|
if (!email) {
|
||||||
enqueueSnackBar('Invalid email', {
|
enqueueSnackBar(t`Invalid email`, {
|
||||||
variant: SnackBarVariant.Error,
|
variant: SnackBarVariant.Error,
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
@ -25,11 +26,11 @@ export const useHandleResendEmailVerificationToken = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (data?.resendEmailVerificationToken?.success === true) {
|
if (data?.resendEmailVerificationToken?.success === true) {
|
||||||
enqueueSnackBar('Email verification link resent!', {
|
enqueueSnackBar(t`Email verification link resent!`, {
|
||||||
variant: SnackBarVariant.Success,
|
variant: SnackBarVariant.Success,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
enqueueSnackBar('There was some issue', {
|
enqueueSnackBar(t`There was an issue`, {
|
||||||
variant: SnackBarVariant.Error,
|
variant: SnackBarVariant.Error,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,17 +2,19 @@ import { useCallback } from 'react';
|
|||||||
|
|
||||||
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
||||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||||
|
import { useLingui } from '@lingui/react/macro';
|
||||||
import { useEmailPasswordResetLinkMutation } from '~/generated/graphql';
|
import { useEmailPasswordResetLinkMutation } from '~/generated/graphql';
|
||||||
|
|
||||||
export const useHandleResetPassword = () => {
|
export const useHandleResetPassword = () => {
|
||||||
const { enqueueSnackBar } = useSnackBar();
|
const { enqueueSnackBar } = useSnackBar();
|
||||||
const [emailPasswordResetLink] = useEmailPasswordResetLinkMutation();
|
const [emailPasswordResetLink] = useEmailPasswordResetLinkMutation();
|
||||||
|
const { t } = useLingui();
|
||||||
|
|
||||||
const handleResetPassword = useCallback(
|
const handleResetPassword = useCallback(
|
||||||
(email: string) => {
|
(email: string) => {
|
||||||
return async () => {
|
return async () => {
|
||||||
if (!email) {
|
if (!email) {
|
||||||
enqueueSnackBar('Invalid email', {
|
enqueueSnackBar(t`Invalid email`, {
|
||||||
variant: SnackBarVariant.Error,
|
variant: SnackBarVariant.Error,
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
@ -24,11 +26,11 @@ export const useHandleResetPassword = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (data?.emailPasswordResetLink?.success === true) {
|
if (data?.emailPasswordResetLink?.success === true) {
|
||||||
enqueueSnackBar('Password reset link has been sent to the email', {
|
enqueueSnackBar(t`Password reset link has been sent to the email`, {
|
||||||
variant: SnackBarVariant.Success,
|
variant: SnackBarVariant.Success,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
enqueueSnackBar('There was some issue', {
|
enqueueSnackBar(t`There was an issue`, {
|
||||||
variant: SnackBarVariant.Error,
|
variant: SnackBarVariant.Error,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -39,7 +41,7 @@ export const useHandleResetPassword = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
[enqueueSnackBar, emailPasswordResetLink],
|
[enqueueSnackBar, emailPasswordResetLink, t],
|
||||||
);
|
);
|
||||||
|
|
||||||
return { handleResetPassword };
|
return { handleResetPassword };
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import { IconCopy, LightIconButton } from 'twenty-ui';
|
|||||||
|
|
||||||
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
||||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||||
|
import { useLingui } from '@lingui/react/macro';
|
||||||
|
|
||||||
const StyledButtonContainer = styled.div`
|
const StyledButtonContainer = styled.div`
|
||||||
padding: 0 ${({ theme }) => theme.spacing(1)};
|
padding: 0 ${({ theme }) => theme.spacing(1)};
|
||||||
@ -16,6 +17,7 @@ export type LightCopyIconButtonProps = {
|
|||||||
export const LightCopyIconButton = ({ copyText }: LightCopyIconButtonProps) => {
|
export const LightCopyIconButton = ({ copyText }: LightCopyIconButtonProps) => {
|
||||||
const { enqueueSnackBar } = useSnackBar();
|
const { enqueueSnackBar } = useSnackBar();
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
const { t } = useLingui();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledButtonContainer>
|
<StyledButtonContainer>
|
||||||
@ -23,7 +25,7 @@ export const LightCopyIconButton = ({ copyText }: LightCopyIconButtonProps) => {
|
|||||||
className="copy-button"
|
className="copy-button"
|
||||||
Icon={IconCopy}
|
Icon={IconCopy}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
enqueueSnackBar('Text copied to clipboard', {
|
enqueueSnackBar(t`Text copied to clipboard`, {
|
||||||
variant: SnackBarVariant.Success,
|
variant: SnackBarVariant.Success,
|
||||||
icon: <IconCopy size={theme.icon.size.md} />,
|
icon: <IconCopy size={theme.icon.size.md} />,
|
||||||
duration: 2000,
|
duration: 2000,
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { usePhonesFieldDisplay } from '@/object-record/record-field/meta-types/h
|
|||||||
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
||||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||||
import { PhonesDisplay } from '@/ui/field/display/components/PhonesDisplay';
|
import { PhonesDisplay } from '@/ui/field/display/components/PhonesDisplay';
|
||||||
|
import { useLingui } from '@lingui/react/macro';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useIcons } from 'twenty-ui';
|
import { useIcons } from 'twenty-ui';
|
||||||
|
|
||||||
@ -15,6 +16,8 @@ export const PhonesFieldDisplay = () => {
|
|||||||
|
|
||||||
const { getIcon } = useIcons();
|
const { getIcon } = useIcons();
|
||||||
|
|
||||||
|
const { t } = useLingui();
|
||||||
|
|
||||||
const IconCircleCheck = getIcon('IconCircleCheck');
|
const IconCircleCheck = getIcon('IconCircleCheck');
|
||||||
const IconExclamationCircle = getIcon('IconExclamationCircle');
|
const IconExclamationCircle = getIcon('IconExclamationCircle');
|
||||||
|
|
||||||
@ -26,13 +29,13 @@ export const PhonesFieldDisplay = () => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await navigator.clipboard.writeText(phoneNumber);
|
await navigator.clipboard.writeText(phoneNumber);
|
||||||
enqueueSnackBar('Phone number copied to clipboard', {
|
enqueueSnackBar(t`Phone number copied to clipboard`, {
|
||||||
variant: SnackBarVariant.Success,
|
variant: SnackBarVariant.Success,
|
||||||
icon: <IconCircleCheck size={16} color="green" />,
|
icon: <IconCircleCheck size={16} color="green" />,
|
||||||
duration: 2000,
|
duration: 2000,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
enqueueSnackBar('Error copying to clipboard', {
|
enqueueSnackBar(t`Error copying to clipboard`, {
|
||||||
variant: SnackBarVariant.Error,
|
variant: SnackBarVariant.Error,
|
||||||
icon: <IconExclamationCircle size={16} color="red" />,
|
icon: <IconExclamationCircle size={16} color="red" />,
|
||||||
duration: 2000,
|
duration: 2000,
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { ComponentDecorator } from 'twenty-ui';
|
|||||||
|
|
||||||
import { PhonesFieldDisplay } from '@/object-record/record-field/meta-types/display/components/PhonesFieldDisplay';
|
import { PhonesFieldDisplay } from '@/object-record/record-field/meta-types/display/components/PhonesFieldDisplay';
|
||||||
import { getFieldDecorator } from '~/testing/decorators/getFieldDecorator';
|
import { getFieldDecorator } from '~/testing/decorators/getFieldDecorator';
|
||||||
|
import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator';
|
||||||
import { MemoryRouterDecorator } from '~/testing/decorators/MemoryRouterDecorator';
|
import { MemoryRouterDecorator } from '~/testing/decorators/MemoryRouterDecorator';
|
||||||
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
|
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
|
||||||
import { getProfilingStory } from '~/testing/profiling/utils/getProfilingStory';
|
import { getProfilingStory } from '~/testing/profiling/utils/getProfilingStory';
|
||||||
@ -13,6 +14,7 @@ const meta: Meta = {
|
|||||||
MemoryRouterDecorator,
|
MemoryRouterDecorator,
|
||||||
getFieldDecorator('person', 'phones'),
|
getFieldDecorator('person', 'phones'),
|
||||||
ComponentDecorator,
|
ComponentDecorator,
|
||||||
|
I18nFrontDecorator,
|
||||||
SnackBarDecorator,
|
SnackBarDecorator,
|
||||||
],
|
],
|
||||||
component: PhonesFieldDisplay,
|
component: PhonesFieldDisplay,
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
|
|||||||
|
|
||||||
import { FieldContextProvider } from '@/object-record/record-field/meta-types/components/FieldContextProvider';
|
import { FieldContextProvider } from '@/object-record/record-field/meta-types/components/FieldContextProvider';
|
||||||
import { StorybookFieldInputDropdownFocusIdSetterEffect } from '~/testing/components/StorybookFieldInputDropdownFocusIdSetterEffect';
|
import { StorybookFieldInputDropdownFocusIdSetterEffect } from '~/testing/components/StorybookFieldInputDropdownFocusIdSetterEffect';
|
||||||
|
import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator';
|
||||||
import { useNumberField } from '../../../hooks/useNumberField';
|
import { useNumberField } from '../../../hooks/useNumberField';
|
||||||
import { NumberFieldInput, NumberFieldInputProps } from '../NumberFieldInput';
|
import { NumberFieldInput, NumberFieldInputProps } from '../NumberFieldInput';
|
||||||
|
|
||||||
@ -108,7 +109,7 @@ const meta: Meta = {
|
|||||||
onTab: { control: false },
|
onTab: { control: false },
|
||||||
onShiftTab: { control: false },
|
onShiftTab: { control: false },
|
||||||
},
|
},
|
||||||
decorators: [clearMocksDecorator, SnackBarDecorator],
|
decorators: [clearMocksDecorator, SnackBarDecorator, I18nFrontDecorator],
|
||||||
parameters: {
|
parameters: {
|
||||||
clearMocks: true,
|
clearMocks: true,
|
||||||
},
|
},
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import { FieldMetadataType } from '~/generated/graphql';
|
|||||||
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
|
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
|
||||||
|
|
||||||
import { StorybookFieldInputDropdownFocusIdSetterEffect } from '~/testing/components/StorybookFieldInputDropdownFocusIdSetterEffect';
|
import { StorybookFieldInputDropdownFocusIdSetterEffect } from '~/testing/components/StorybookFieldInputDropdownFocusIdSetterEffect';
|
||||||
|
import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator';
|
||||||
import { FieldContextProvider } from '../../../components/FieldContextProvider';
|
import { FieldContextProvider } from '../../../components/FieldContextProvider';
|
||||||
import { useTextField } from '../../../hooks/useTextField';
|
import { useTextField } from '../../../hooks/useTextField';
|
||||||
import { TextFieldInput, TextFieldInputProps } from '../TextFieldInput';
|
import { TextFieldInput, TextFieldInputProps } from '../TextFieldInput';
|
||||||
@ -107,7 +108,7 @@ const meta: Meta = {
|
|||||||
onTab: { control: false },
|
onTab: { control: false },
|
||||||
onShiftTab: { control: false },
|
onShiftTab: { control: false },
|
||||||
},
|
},
|
||||||
decorators: [clearMocksDecorator, SnackBarDecorator],
|
decorators: [clearMocksDecorator, SnackBarDecorator, I18nFrontDecorator],
|
||||||
parameters: {
|
parameters: {
|
||||||
clearMocks: true,
|
clearMocks: true,
|
||||||
},
|
},
|
||||||
|
|||||||
@ -15,6 +15,7 @@ import { TableCell } from '@/ui/layout/table/components/TableCell';
|
|||||||
import { TableHeader } from '@/ui/layout/table/components/TableHeader';
|
import { TableHeader } from '@/ui/layout/table/components/TableHeader';
|
||||||
import { TableRow } from '@/ui/layout/table/components/TableRow';
|
import { TableRow } from '@/ui/layout/table/components/TableRow';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
import { useLingui } from '@lingui/react/macro';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useRecoilState, useRecoilValue } from 'recoil';
|
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||||
import { isDefined } from 'twenty-shared';
|
import { isDefined } from 'twenty-shared';
|
||||||
@ -49,9 +50,11 @@ export const SettingsAdminWorkspaceContent = ({
|
|||||||
const { updateFeatureFlagState } = useFeatureFlagState();
|
const { updateFeatureFlagState } = useFeatureFlagState();
|
||||||
const userLookupResult = useRecoilValue(userLookupResultState);
|
const userLookupResult = useRecoilValue(userLookupResultState);
|
||||||
|
|
||||||
|
const { t } = useLingui();
|
||||||
|
|
||||||
const handleImpersonate = async (workspaceId: string) => {
|
const handleImpersonate = async (workspaceId: string) => {
|
||||||
if (!userLookupResult?.user.id) {
|
if (!userLookupResult?.user.id) {
|
||||||
enqueueSnackBar('Please search for a user first', {
|
enqueueSnackBar(t`Please search for a user first`, {
|
||||||
variant: SnackBarVariant.Error,
|
variant: SnackBarVariant.Error,
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import { Button, IconCopy } from 'twenty-ui';
|
|||||||
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
||||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||||
import { TextInput } from '@/ui/input/components/TextInput';
|
import { TextInput } from '@/ui/input/components/TextInput';
|
||||||
|
import { useLingui } from '@lingui/react/macro';
|
||||||
const StyledContainer = styled.div`
|
const StyledContainer = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
@ -20,6 +20,7 @@ type ApiKeyInputProps = { apiKey: string };
|
|||||||
|
|
||||||
export const ApiKeyInput = ({ apiKey }: ApiKeyInputProps) => {
|
export const ApiKeyInput = ({ apiKey }: ApiKeyInputProps) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
const { t } = useLingui();
|
||||||
|
|
||||||
const { enqueueSnackBar } = useSnackBar();
|
const { enqueueSnackBar } = useSnackBar();
|
||||||
return (
|
return (
|
||||||
@ -31,7 +32,7 @@ export const ApiKeyInput = ({ apiKey }: ApiKeyInputProps) => {
|
|||||||
Icon={IconCopy}
|
Icon={IconCopy}
|
||||||
title="Copy"
|
title="Copy"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
enqueueSnackBar('API Key copied to clipboard', {
|
enqueueSnackBar(t`API Key copied to clipboard`, {
|
||||||
variant: SnackBarVariant.Success,
|
variant: SnackBarVariant.Success,
|
||||||
icon: <IconCopy size={theme.icon.size.md} />,
|
icon: <IconCopy size={theme.icon.size.md} />,
|
||||||
duration: 2000,
|
duration: 2000,
|
||||||
|
|||||||
@ -2,12 +2,13 @@ import { Meta, StoryObj } from '@storybook/react';
|
|||||||
import { ComponentDecorator } from 'twenty-ui';
|
import { ComponentDecorator } from 'twenty-ui';
|
||||||
|
|
||||||
import { ApiKeyInput } from '@/settings/developers/components/ApiKeyInput';
|
import { ApiKeyInput } from '@/settings/developers/components/ApiKeyInput';
|
||||||
|
import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator';
|
||||||
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
|
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
|
||||||
|
|
||||||
const meta: Meta<typeof ApiKeyInput> = {
|
const meta: Meta<typeof ApiKeyInput> = {
|
||||||
title: 'Modules/Settings/Developers/ApiKeys/ApiKeyInput',
|
title: 'Modules/Settings/Developers/ApiKeys/ApiKeyInput',
|
||||||
component: ApiKeyInput,
|
component: ApiKeyInput,
|
||||||
decorators: [ComponentDecorator, SnackBarDecorator],
|
decorators: [ComponentDecorator, SnackBarDecorator, I18nFrontDecorator],
|
||||||
args: {
|
args: {
|
||||||
apiKey:
|
apiKey:
|
||||||
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ0d2VudHktN2VkOWQyMTItMWMyNS00ZDAyLWJmMjUtNmFlY2NmN2VhNDE5IiwiaWF0IjoxNjk4MTQyODgyLCJleHAiOjE2OTk0MDE1OTksImp0aSI6ImMyMmFiNjQxLTVhOGYtNGQwMC1iMDkzLTk3MzUwYTM2YzZkOSJ9.JIe2TX5IXrdNl3n-kRFp3jyfNUE7unzXZLAzm2Gxl98',
|
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ0d2VudHktN2VkOWQyMTItMWMyNS00ZDAyLWJmMjUtNmFlY2NmN2VhNDE5IiwiaWF0IjoxNjk4MTQyODgyLCJleHAiOjE2OTk0MDE1OTksImp0aSI6ImMyMmFiNjQxLTVhOGYtNGQwMC1iMDkzLTk3MzUwYTM2YzZkOSJ9.JIe2TX5IXrdNl3n-kRFp3jyfNUE7unzXZLAzm2Gxl98',
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
|||||||
import { TextInput } from '@/ui/input/components/TextInput';
|
import { TextInput } from '@/ui/input/components/TextInput';
|
||||||
import { useTheme } from '@emotion/react';
|
import { useTheme } from '@emotion/react';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
import { useLingui } from '@lingui/react/macro';
|
||||||
import { Controller, useFormContext } from 'react-hook-form';
|
import { Controller, useFormContext } from 'react-hook-form';
|
||||||
import { Button, H2Title, IconCopy, Section } from 'twenty-ui';
|
import { Button, H2Title, IconCopy, Section } from 'twenty-ui';
|
||||||
import { REACT_APP_SERVER_BASE_URL } from '~/config';
|
import { REACT_APP_SERVER_BASE_URL } from '~/config';
|
||||||
@ -35,6 +36,7 @@ export const SettingsSSOOIDCForm = () => {
|
|||||||
const { control } = useFormContext();
|
const { control } = useFormContext();
|
||||||
const { enqueueSnackBar } = useSnackBar();
|
const { enqueueSnackBar } = useSnackBar();
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
const { t } = useLingui();
|
||||||
|
|
||||||
const authorizedUrl = window.location.origin;
|
const authorizedUrl = window.location.origin;
|
||||||
const redirectionUrl = `${REACT_APP_SERVER_BASE_URL}/auth/oidc/callback`;
|
const redirectionUrl = `${REACT_APP_SERVER_BASE_URL}/auth/oidc/callback`;
|
||||||
@ -43,8 +45,8 @@ export const SettingsSSOOIDCForm = () => {
|
|||||||
<>
|
<>
|
||||||
<Section>
|
<Section>
|
||||||
<H2Title
|
<H2Title
|
||||||
title="Client Settings"
|
title={t`Client Settings`}
|
||||||
description="Provide your OIDC provider details"
|
description={t`Provide your OIDC provider details`}
|
||||||
/>
|
/>
|
||||||
<StyledInputsContainer>
|
<StyledInputsContainer>
|
||||||
<StyledContainer>
|
<StyledContainer>
|
||||||
@ -59,7 +61,7 @@ export const SettingsSSOOIDCForm = () => {
|
|||||||
<StyledButtonCopy>
|
<StyledButtonCopy>
|
||||||
<Button
|
<Button
|
||||||
Icon={IconCopy}
|
Icon={IconCopy}
|
||||||
title="Copy"
|
title={t`Copy`}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
enqueueSnackBar('Authorized Url copied to clipboard', {
|
enqueueSnackBar('Authorized Url copied to clipboard', {
|
||||||
variant: SnackBarVariant.Success,
|
variant: SnackBarVariant.Success,
|
||||||
@ -75,7 +77,7 @@ export const SettingsSSOOIDCForm = () => {
|
|||||||
<StyledLinkContainer>
|
<StyledLinkContainer>
|
||||||
<TextInput
|
<TextInput
|
||||||
readOnly={true}
|
readOnly={true}
|
||||||
label="Redirection URI"
|
label={t`Redirection URI`}
|
||||||
value={redirectionUrl}
|
value={redirectionUrl}
|
||||||
fullWidth
|
fullWidth
|
||||||
/>
|
/>
|
||||||
@ -83,9 +85,9 @@ export const SettingsSSOOIDCForm = () => {
|
|||||||
<StyledButtonCopy>
|
<StyledButtonCopy>
|
||||||
<Button
|
<Button
|
||||||
Icon={IconCopy}
|
Icon={IconCopy}
|
||||||
title="Copy"
|
title={t`Copy`}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
enqueueSnackBar('Redirect Url copied to clipboard', {
|
enqueueSnackBar(t`Redirect Url copied to clipboard`, {
|
||||||
variant: SnackBarVariant.Success,
|
variant: SnackBarVariant.Success,
|
||||||
icon: <IconCopy size={theme.icon.size.md} />,
|
icon: <IconCopy size={theme.icon.size.md} />,
|
||||||
duration: 2000,
|
duration: 2000,
|
||||||
@ -99,8 +101,8 @@ export const SettingsSSOOIDCForm = () => {
|
|||||||
</Section>
|
</Section>
|
||||||
<Section>
|
<Section>
|
||||||
<H2Title
|
<H2Title
|
||||||
title="Identity Provider"
|
title={t`Identity Provider`}
|
||||||
description="Enter the credentials to set the connection"
|
description={t`Enter the credentials to set the connection`}
|
||||||
/>
|
/>
|
||||||
<StyledInputsContainer>
|
<StyledInputsContainer>
|
||||||
<Controller
|
<Controller
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
|||||||
import { TextInput } from '@/ui/input/components/TextInput';
|
import { TextInput } from '@/ui/input/components/TextInput';
|
||||||
import { useTheme } from '@emotion/react';
|
import { useTheme } from '@emotion/react';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
import { useLingui } from '@lingui/react/macro';
|
||||||
import { ChangeEvent, useRef } from 'react';
|
import { ChangeEvent, useRef } from 'react';
|
||||||
import { useFormContext } from 'react-hook-form';
|
import { useFormContext } from 'react-hook-form';
|
||||||
import { isDefined } from 'twenty-shared';
|
import { isDefined } from 'twenty-shared';
|
||||||
@ -57,6 +58,7 @@ export const SettingsSSOSAMLForm = () => {
|
|||||||
const { enqueueSnackBar } = useSnackBar();
|
const { enqueueSnackBar } = useSnackBar();
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const { setValue, getValues, watch, trigger } = useFormContext();
|
const { setValue, getValues, watch, trigger } = useFormContext();
|
||||||
|
const { t } = useLingui();
|
||||||
|
|
||||||
const handleFileChange = async (e: ChangeEvent<HTMLInputElement>) => {
|
const handleFileChange = async (e: ChangeEvent<HTMLInputElement>) => {
|
||||||
if (isDefined(e.target.files)) {
|
if (isDefined(e.target.files)) {
|
||||||
@ -64,7 +66,7 @@ export const SettingsSSOSAMLForm = () => {
|
|||||||
const samlMetadataParsed = parseSAMLMetadataFromXMLFile(text);
|
const samlMetadataParsed = parseSAMLMetadataFromXMLFile(text);
|
||||||
e.target.value = '';
|
e.target.value = '';
|
||||||
if (!samlMetadataParsed.success) {
|
if (!samlMetadataParsed.success) {
|
||||||
return enqueueSnackBar('Invalid File', {
|
return enqueueSnackBar(t`Invalid File`, {
|
||||||
variant: SnackBarVariant.Error,
|
variant: SnackBarVariant.Error,
|
||||||
duration: 2000,
|
duration: 2000,
|
||||||
});
|
});
|
||||||
@ -100,7 +102,7 @@ export const SettingsSSOSAMLForm = () => {
|
|||||||
`${REACT_APP_SERVER_BASE_URL}/auth/saml/metadata/${getValues('id')}`,
|
`${REACT_APP_SERVER_BASE_URL}/auth/saml/metadata/${getValues('id')}`,
|
||||||
);
|
);
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
return enqueueSnackBar('Metadata file generation failed', {
|
return enqueueSnackBar(t`Metadata file generation failed`, {
|
||||||
variant: SnackBarVariant.Error,
|
variant: SnackBarVariant.Error,
|
||||||
duration: 2000,
|
duration: 2000,
|
||||||
});
|
});
|
||||||
@ -120,8 +122,8 @@ export const SettingsSSOSAMLForm = () => {
|
|||||||
<>
|
<>
|
||||||
<Section>
|
<Section>
|
||||||
<H2Title
|
<H2Title
|
||||||
title="Identity Provider Metadata XML"
|
title={t`Identity Provider Metadata XML`}
|
||||||
description="Upload the XML file with your connection infos"
|
description={t`Upload the XML file with your connection infos`}
|
||||||
/>
|
/>
|
||||||
<StyledUploadFileContainer>
|
<StyledUploadFileContainer>
|
||||||
<StyledFileInput
|
<StyledFileInput
|
||||||
@ -133,7 +135,7 @@ export const SettingsSSOSAMLForm = () => {
|
|||||||
<Button
|
<Button
|
||||||
Icon={IconUpload}
|
Icon={IconUpload}
|
||||||
onClick={handleUploadFileClick}
|
onClick={handleUploadFileClick}
|
||||||
title="Upload file"
|
title={t`Upload file`}
|
||||||
></Button>
|
></Button>
|
||||||
{isXMLMetadataValid() && (
|
{isXMLMetadataValid() && (
|
||||||
<IconCheck
|
<IconCheck
|
||||||
@ -146,15 +148,15 @@ export const SettingsSSOSAMLForm = () => {
|
|||||||
</Section>
|
</Section>
|
||||||
<Section>
|
<Section>
|
||||||
<H2Title
|
<H2Title
|
||||||
title="Service Provider Details"
|
title={t`Service Provider Details`}
|
||||||
description="Enter the infos to set the connection"
|
description={t`Enter the infos to set the connection`}
|
||||||
/>
|
/>
|
||||||
<StyledInputsContainer>
|
<StyledInputsContainer>
|
||||||
<StyledContainer>
|
<StyledContainer>
|
||||||
<Button
|
<Button
|
||||||
Icon={IconDownload}
|
Icon={IconDownload}
|
||||||
onClick={downloadMetadata}
|
onClick={downloadMetadata}
|
||||||
title="Download file"
|
title={t`Download file`}
|
||||||
></Button>
|
></Button>
|
||||||
</StyledContainer>
|
</StyledContainer>
|
||||||
<HorizontalSeparator text={'Or'} />
|
<HorizontalSeparator text={'Or'} />
|
||||||
@ -194,9 +196,9 @@ export const SettingsSSOSAMLForm = () => {
|
|||||||
<StyledButtonCopy>
|
<StyledButtonCopy>
|
||||||
<Button
|
<Button
|
||||||
Icon={IconCopy}
|
Icon={IconCopy}
|
||||||
title="Copy"
|
title={t`Copy`}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
enqueueSnackBar('Entity ID copied to clipboard', {
|
enqueueSnackBar(t`Entity ID copied to clipboard`, {
|
||||||
variant: SnackBarVariant.Success,
|
variant: SnackBarVariant.Success,
|
||||||
icon: <IconCopy size={theme.icon.size.md} />,
|
icon: <IconCopy size={theme.icon.size.md} />,
|
||||||
duration: 2000,
|
duration: 2000,
|
||||||
|
|||||||
@ -16,6 +16,7 @@ import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
|||||||
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
|
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
|
||||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||||
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
|
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
|
||||||
|
import { useLingui } from '@lingui/react/macro';
|
||||||
import { UnwrapRecoilValue } from 'recoil';
|
import { UnwrapRecoilValue } from 'recoil';
|
||||||
import { isDefined } from 'twenty-shared';
|
import { isDefined } from 'twenty-shared';
|
||||||
import { SsoIdentityProviderStatus } from '~/generated/graphql';
|
import { SsoIdentityProviderStatus } from '~/generated/graphql';
|
||||||
@ -36,6 +37,8 @@ export const SettingsSecuritySSORowDropdownMenu = ({
|
|||||||
const { deleteSSOIdentityProvider } = useDeleteSSOIdentityProvider();
|
const { deleteSSOIdentityProvider } = useDeleteSSOIdentityProvider();
|
||||||
const { updateSSOIdentityProvider } = useUpdateSSOIdentityProvider();
|
const { updateSSOIdentityProvider } = useUpdateSSOIdentityProvider();
|
||||||
|
|
||||||
|
const { t } = useLingui();
|
||||||
|
|
||||||
const handleDeleteSSOIdentityProvider = async (
|
const handleDeleteSSOIdentityProvider = async (
|
||||||
identityProviderId: string,
|
identityProviderId: string,
|
||||||
) => {
|
) => {
|
||||||
@ -43,7 +46,7 @@ export const SettingsSecuritySSORowDropdownMenu = ({
|
|||||||
identityProviderId,
|
identityProviderId,
|
||||||
});
|
});
|
||||||
if (isDefined(result.errors)) {
|
if (isDefined(result.errors)) {
|
||||||
enqueueSnackBar('Error deleting SSO Identity Provider', {
|
enqueueSnackBar(t`Error deleting SSO Identity Provider`, {
|
||||||
variant: SnackBarVariant.Error,
|
variant: SnackBarVariant.Error,
|
||||||
duration: 2000,
|
duration: 2000,
|
||||||
});
|
});
|
||||||
@ -61,7 +64,7 @@ export const SettingsSecuritySSORowDropdownMenu = ({
|
|||||||
: SsoIdentityProviderStatus.Active,
|
: SsoIdentityProviderStatus.Active,
|
||||||
});
|
});
|
||||||
if (isDefined(result.errors)) {
|
if (isDefined(result.errors)) {
|
||||||
enqueueSnackBar('Error editing SSO Identity Provider', {
|
enqueueSnackBar(t`Error editing SSO Identity Provider`, {
|
||||||
variant: SnackBarVariant.Error,
|
variant: SnackBarVariant.Error,
|
||||||
duration: 2000,
|
duration: 2000,
|
||||||
});
|
});
|
||||||
@ -82,7 +85,7 @@ export const SettingsSecuritySSORowDropdownMenu = ({
|
|||||||
<MenuItem
|
<MenuItem
|
||||||
accent="default"
|
accent="default"
|
||||||
LeftIcon={IconArchive}
|
LeftIcon={IconArchive}
|
||||||
text={SSOIdp.status === 'Active' ? 'Deactivate' : 'Activate'}
|
text={SSOIdp.status === 'Active' ? t`Deactivate` : t`Activate`}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
toggleSSOIdentityProviderStatus(SSOIdp.id);
|
toggleSSOIdentityProviderStatus(SSOIdp.id);
|
||||||
closeDropdown();
|
closeDropdown();
|
||||||
@ -91,7 +94,7 @@ export const SettingsSecuritySSORowDropdownMenu = ({
|
|||||||
<MenuItem
|
<MenuItem
|
||||||
accent="danger"
|
accent="danger"
|
||||||
LeftIcon={IconTrash}
|
LeftIcon={IconTrash}
|
||||||
text="Delete"
|
text={t`Delete`}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
handleDeleteSSOIdentityProvider(SSOIdp.id);
|
handleDeleteSSOIdentityProvider(SSOIdp.id);
|
||||||
closeDropdown();
|
closeDropdown();
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
|||||||
import { RUN_WORKFLOW_VERSION } from '@/workflow/graphql/mutations/runWorkflowVersion';
|
import { RUN_WORKFLOW_VERSION } from '@/workflow/graphql/mutations/runWorkflowVersion';
|
||||||
import { useApolloClient, useMutation } from '@apollo/client';
|
import { useApolloClient, useMutation } from '@apollo/client';
|
||||||
import { useTheme } from '@emotion/react';
|
import { useTheme } from '@emotion/react';
|
||||||
|
import { useLingui } from '@lingui/react/macro';
|
||||||
import { IconSettingsAutomation } from 'twenty-ui';
|
import { IconSettingsAutomation } from 'twenty-ui';
|
||||||
import {
|
import {
|
||||||
RunWorkflowVersionMutation,
|
RunWorkflowVersionMutation,
|
||||||
@ -21,6 +22,7 @@ export const useRunWorkflowVersion = () => {
|
|||||||
const { enqueueSnackBar } = useSnackBar();
|
const { enqueueSnackBar } = useSnackBar();
|
||||||
|
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
const { t } = useLingui();
|
||||||
|
|
||||||
const runWorkflowVersion = async ({
|
const runWorkflowVersion = async ({
|
||||||
workflowVersionId,
|
workflowVersionId,
|
||||||
@ -36,7 +38,7 @@ export const useRunWorkflowVersion = () => {
|
|||||||
const workflowRunId = data?.runWorkflowVersion?.workflowRunId;
|
const workflowRunId = data?.runWorkflowVersion?.workflowRunId;
|
||||||
|
|
||||||
if (!workflowRunId) {
|
if (!workflowRunId) {
|
||||||
enqueueSnackBar('Workflow run failed', {
|
enqueueSnackBar(t`Workflow run failed`, {
|
||||||
variant: SnackBarVariant.Error,
|
variant: SnackBarVariant.Error,
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
@ -44,7 +46,7 @@ export const useRunWorkflowVersion = () => {
|
|||||||
|
|
||||||
const link = `/object/workflowRun/${workflowRunId}`;
|
const link = `/object/workflowRun/${workflowRunId}`;
|
||||||
|
|
||||||
enqueueSnackBar('Workflow is running...', {
|
enqueueSnackBar(t`Workflow is running...`, {
|
||||||
variant: SnackBarVariant.Success,
|
variant: SnackBarVariant.Success,
|
||||||
icon: (
|
icon: (
|
||||||
<IconSettingsAutomation
|
<IconSettingsAutomation
|
||||||
@ -54,7 +56,7 @@ export const useRunWorkflowVersion = () => {
|
|||||||
),
|
),
|
||||||
link: {
|
link: {
|
||||||
href: link,
|
href: link,
|
||||||
text: 'View execution details',
|
text: t`View execution details`,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@ -114,7 +114,7 @@ export const SettingsWorkspaceMembers = () => {
|
|||||||
const handleRemoveWorkspaceInvitation = async (appTokenId: string) => {
|
const handleRemoveWorkspaceInvitation = async (appTokenId: string) => {
|
||||||
const result = await deleteWorkspaceInvitation({ appTokenId });
|
const result = await deleteWorkspaceInvitation({ appTokenId });
|
||||||
if (isDefined(result.errors)) {
|
if (isDefined(result.errors)) {
|
||||||
enqueueSnackBar('Error deleting invitation', {
|
enqueueSnackBar(t`Error deleting invitation`, {
|
||||||
variant: SnackBarVariant.Error,
|
variant: SnackBarVariant.Error,
|
||||||
duration: 2000,
|
duration: 2000,
|
||||||
});
|
});
|
||||||
@ -124,7 +124,7 @@ export const SettingsWorkspaceMembers = () => {
|
|||||||
const handleResendWorkspaceInvitation = async (appTokenId: string) => {
|
const handleResendWorkspaceInvitation = async (appTokenId: string) => {
|
||||||
const result = await resendInvitation({ appTokenId });
|
const result = await resendInvitation({ appTokenId });
|
||||||
if (isDefined(result.errors)) {
|
if (isDefined(result.errors)) {
|
||||||
enqueueSnackBar('Error resending invitation', {
|
enqueueSnackBar(t`Error resending invitation`, {
|
||||||
variant: SnackBarVariant.Error,
|
variant: SnackBarVariant.Error,
|
||||||
duration: 2000,
|
duration: 2000,
|
||||||
});
|
});
|
||||||
@ -134,7 +134,7 @@ export const SettingsWorkspaceMembers = () => {
|
|||||||
const getExpiresAtText = (expiresAt: string) => {
|
const getExpiresAtText = (expiresAt: string) => {
|
||||||
const expiresAtDate = new Date(expiresAt);
|
const expiresAtDate = new Date(expiresAt);
|
||||||
return expiresAtDate < new Date()
|
return expiresAtDate < new Date()
|
||||||
? 'Expired'
|
? t`Expired`
|
||||||
: formatDistanceToNow(new Date(expiresAt));
|
: formatDistanceToNow(new Date(expiresAt));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -1,14 +1,15 @@
|
|||||||
import { TableRow } from '@/ui/layout/table/components/TableRow';
|
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
||||||
import { TableHeader } from '@/ui/layout/table/components/TableHeader';
|
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||||
|
import { Table } from '@/ui/layout/table/components/Table';
|
||||||
import { TableBody } from '@/ui/layout/table/components/TableBody';
|
import { TableBody } from '@/ui/layout/table/components/TableBody';
|
||||||
import { TableCell } from '@/ui/layout/table/components/TableCell';
|
import { TableCell } from '@/ui/layout/table/components/TableCell';
|
||||||
import { Button } from 'twenty-ui';
|
import { TableHeader } from '@/ui/layout/table/components/TableHeader';
|
||||||
import { Table } from '@/ui/layout/table/components/Table';
|
import { TableRow } from '@/ui/layout/table/components/TableRow';
|
||||||
import { CustomDomainValidRecords } from '~/generated/graphql';
|
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
import { useLingui } from '@lingui/react/macro';
|
||||||
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
import { Button } from 'twenty-ui';
|
||||||
import { useDebouncedCallback } from 'use-debounce';
|
import { useDebouncedCallback } from 'use-debounce';
|
||||||
|
import { CustomDomainValidRecords } from '~/generated/graphql';
|
||||||
|
|
||||||
const StyledTable = styled(Table)`
|
const StyledTable = styled(Table)`
|
||||||
border-bottom: 1px solid ${({ theme }) => theme.border.color.light};
|
border-bottom: 1px solid ${({ theme }) => theme.border.color.light};
|
||||||
@ -41,9 +42,11 @@ export const SettingsCustomDomainRecords = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const { enqueueSnackBar } = useSnackBar();
|
const { enqueueSnackBar } = useSnackBar();
|
||||||
|
|
||||||
|
const { t } = useLingui();
|
||||||
|
|
||||||
const copyToClipboard = (value: string) => {
|
const copyToClipboard = (value: string) => {
|
||||||
navigator.clipboard.writeText(value);
|
navigator.clipboard.writeText(value);
|
||||||
enqueueSnackBar('Copied to clipboard!', {
|
enqueueSnackBar(t`Copied to clipboard!`, {
|
||||||
variant: SnackBarVariant.Success,
|
variant: SnackBarVariant.Success,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@ -2,9 +2,9 @@ import { UseFilters, UseGuards } from '@nestjs/common';
|
|||||||
import { Args, Context, Mutation, Query, Resolver } from '@nestjs/graphql';
|
import { Args, Context, Mutation, Query, Resolver } from '@nestjs/graphql';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
|
||||||
import { SettingsPermissions, SOURCE_LOCALE } from 'twenty-shared';
|
|
||||||
import { Repository } from 'typeorm';
|
|
||||||
import omit from 'lodash.omit';
|
import omit from 'lodash.omit';
|
||||||
|
import { SOURCE_LOCALE, SettingsPermissions } from 'twenty-shared';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
|
||||||
import { ApiKeyTokenInput } from 'src/engine/core-modules/auth/dto/api-key-token.input';
|
import { ApiKeyTokenInput } from 'src/engine/core-modules/auth/dto/api-key-token.input';
|
||||||
import { AppTokenInput } from 'src/engine/core-modules/auth/dto/app-token.input';
|
import { AppTokenInput } from 'src/engine/core-modules/auth/dto/app-token.input';
|
||||||
@ -25,6 +25,8 @@ import {
|
|||||||
AuthExceptionCode,
|
AuthExceptionCode,
|
||||||
} from 'src/engine/core-modules/auth/auth.exception';
|
} from 'src/engine/core-modules/auth/auth.exception';
|
||||||
import { AvailableWorkspaceOutput } from 'src/engine/core-modules/auth/dto/available-workspaces.output';
|
import { AvailableWorkspaceOutput } from 'src/engine/core-modules/auth/dto/available-workspaces.output';
|
||||||
|
import { GetAuthorizationUrlForSSOInput } from 'src/engine/core-modules/auth/dto/get-authorization-url-for-sso.input';
|
||||||
|
import { GetAuthorizationUrlForSSOOutput } from 'src/engine/core-modules/auth/dto/get-authorization-url-for-sso.output';
|
||||||
import { GetLoginTokenFromEmailVerificationTokenInput } from 'src/engine/core-modules/auth/dto/get-login-token-from-email-verification-token.input';
|
import { GetLoginTokenFromEmailVerificationTokenInput } from 'src/engine/core-modules/auth/dto/get-login-token-from-email-verification-token.input';
|
||||||
import { SignUpOutput } from 'src/engine/core-modules/auth/dto/sign-up.output';
|
import { SignUpOutput } from 'src/engine/core-modules/auth/dto/sign-up.output';
|
||||||
import { ResetPasswordService } from 'src/engine/core-modules/auth/services/reset-password.service';
|
import { ResetPasswordService } from 'src/engine/core-modules/auth/services/reset-password.service';
|
||||||
@ -36,6 +38,7 @@ import { CaptchaGuard } from 'src/engine/core-modules/captcha/captcha.guard';
|
|||||||
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
|
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
|
||||||
import { EmailVerificationService } from 'src/engine/core-modules/email-verification/services/email-verification.service';
|
import { EmailVerificationService } from 'src/engine/core-modules/email-verification/services/email-verification.service';
|
||||||
import { I18nContext } from 'src/engine/core-modules/i18n/types/i18n-context.type';
|
import { I18nContext } from 'src/engine/core-modules/i18n/types/i18n-context.type';
|
||||||
|
import { SSOService } from 'src/engine/core-modules/sso/services/sso.service';
|
||||||
import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service';
|
import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service';
|
||||||
import { UserService } from 'src/engine/core-modules/user/services/user.service';
|
import { UserService } from 'src/engine/core-modules/user/services/user.service';
|
||||||
import { User } from 'src/engine/core-modules/user/user.entity';
|
import { User } from 'src/engine/core-modules/user/user.entity';
|
||||||
@ -48,9 +51,6 @@ import { SettingsPermissionsGuard } from 'src/engine/guards/settings-permissions
|
|||||||
import { UserAuthGuard } from 'src/engine/guards/user-auth.guard';
|
import { UserAuthGuard } from 'src/engine/guards/user-auth.guard';
|
||||||
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
||||||
import { PermissionsGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-graphql-api-exception.filter';
|
import { PermissionsGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-graphql-api-exception.filter';
|
||||||
import { SSOService } from 'src/engine/core-modules/sso/services/sso.service';
|
|
||||||
import { GetAuthorizationUrlForSSOOutput } from 'src/engine/core-modules/auth/dto/get-authorization-url-for-sso.output';
|
|
||||||
import { GetAuthorizationUrlForSSOInput } from 'src/engine/core-modules/auth/dto/get-authorization-url-for-sso.input';
|
|
||||||
|
|
||||||
import { GetAuthTokensFromLoginTokenInput } from './dto/get-auth-tokens-from-login-token.input';
|
import { GetAuthTokensFromLoginTokenInput } from './dto/get-auth-tokens-from-login-token.input';
|
||||||
import { GetLoginTokenFromCredentialsInput } from './dto/get-login-token-from-credentials.input';
|
import { GetLoginTokenFromCredentialsInput } from './dto/get-login-token-from-credentials.input';
|
||||||
@ -241,6 +241,7 @@ export class AuthResolver {
|
|||||||
user.id,
|
user.id,
|
||||||
user.email,
|
user.email,
|
||||||
workspace,
|
workspace,
|
||||||
|
signUpInput.locale ?? SOURCE_LOCALE,
|
||||||
);
|
);
|
||||||
|
|
||||||
const loginToken = await this.loginTokenService.generateLoginToken(
|
const loginToken = await this.loginTokenService.generateLoginToken(
|
||||||
|
|||||||
@ -3,6 +3,8 @@ import { InjectRepository } from '@nestjs/typeorm';
|
|||||||
|
|
||||||
import crypto from 'node:crypto';
|
import crypto from 'node:crypto';
|
||||||
|
|
||||||
|
import { i18n } from '@lingui/core';
|
||||||
|
import { t } from '@lingui/core/macro';
|
||||||
import { render } from '@react-email/render';
|
import { render } from '@react-email/render';
|
||||||
import { addMilliseconds } from 'date-fns';
|
import { addMilliseconds } from 'date-fns';
|
||||||
import ms from 'ms';
|
import ms from 'ms';
|
||||||
@ -45,6 +47,7 @@ import {
|
|||||||
SignInUpBaseParams,
|
SignInUpBaseParams,
|
||||||
SignInUpNewUserPayload,
|
SignInUpNewUserPayload,
|
||||||
} from 'src/engine/core-modules/auth/types/signInUp.type';
|
} from 'src/engine/core-modules/auth/types/signInUp.type';
|
||||||
|
import { WorkspaceSubdomainCustomDomainAndIsCustomDomainEnabledType } from 'src/engine/core-modules/domain-manager/domain-manager.type';
|
||||||
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
|
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
|
||||||
import { EmailService } from 'src/engine/core-modules/email/email.service';
|
import { EmailService } from 'src/engine/core-modules/email/email.service';
|
||||||
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
||||||
@ -56,7 +59,6 @@ import { WorkspaceInvitationService } from 'src/engine/core-modules/workspace-in
|
|||||||
import { WorkspaceAuthProvider } from 'src/engine/core-modules/workspace/types/workspace.type';
|
import { WorkspaceAuthProvider } from 'src/engine/core-modules/workspace/types/workspace.type';
|
||||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||||
import { workspaceValidator } from 'src/engine/core-modules/workspace/workspace.validate';
|
import { workspaceValidator } from 'src/engine/core-modules/workspace/workspace.validate';
|
||||||
import { WorkspaceSubdomainCustomDomainAndIsCustomDomainEnabledType } from 'src/engine/core-modules/domain-manager/domain-manager.type';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
// eslint-disable-next-line @nx/workspace-inject-workspace-repository
|
// eslint-disable-next-line @nx/workspace-inject-workspace-repository
|
||||||
@ -424,12 +426,14 @@ export class AuthService {
|
|||||||
const html = render(emailTemplate, { pretty: true });
|
const html = render(emailTemplate, { pretty: true });
|
||||||
const text = render(emailTemplate, { plainText: true });
|
const text = render(emailTemplate, { plainText: true });
|
||||||
|
|
||||||
|
i18n.activate(locale);
|
||||||
|
|
||||||
this.emailService.send({
|
this.emailService.send({
|
||||||
from: `${this.environmentService.get(
|
from: `${this.environmentService.get(
|
||||||
'EMAIL_FROM_NAME',
|
'EMAIL_FROM_NAME',
|
||||||
)} <${this.environmentService.get('EMAIL_FROM_ADDRESS')}>`,
|
)} <${this.environmentService.get('EMAIL_FROM_ADDRESS')}>`,
|
||||||
to: user.email,
|
to: user.email,
|
||||||
subject: 'Your Password Has Been Successfully Changed',
|
subject: t`Your Password Has Been Successfully Changed`,
|
||||||
text,
|
text,
|
||||||
html,
|
html,
|
||||||
});
|
});
|
||||||
|
|||||||
@ -3,6 +3,8 @@ import { InjectRepository } from '@nestjs/typeorm';
|
|||||||
|
|
||||||
import crypto from 'crypto';
|
import crypto from 'crypto';
|
||||||
|
|
||||||
|
import { i18n } from '@lingui/core';
|
||||||
|
import { t } from '@lingui/core/macro';
|
||||||
import { render } from '@react-email/render';
|
import { render } from '@react-email/render';
|
||||||
import { addMilliseconds, differenceInMilliseconds } from 'date-fns';
|
import { addMilliseconds, differenceInMilliseconds } from 'date-fns';
|
||||||
import ms from 'ms';
|
import ms from 'ms';
|
||||||
@ -143,12 +145,14 @@ export class ResetPasswordService {
|
|||||||
const html = render(emailTemplate, { pretty: true });
|
const html = render(emailTemplate, { pretty: true });
|
||||||
const text = render(emailTemplate, { plainText: true });
|
const text = render(emailTemplate, { plainText: true });
|
||||||
|
|
||||||
|
i18n.activate(locale);
|
||||||
|
|
||||||
this.emailService.send({
|
this.emailService.send({
|
||||||
from: `${this.environmentService.get(
|
from: `${this.environmentService.get(
|
||||||
'EMAIL_FROM_NAME',
|
'EMAIL_FROM_NAME',
|
||||||
)} <${this.environmentService.get('EMAIL_FROM_ADDRESS')}>`,
|
)} <${this.environmentService.get('EMAIL_FROM_ADDRESS')}>`,
|
||||||
to: email,
|
to: email,
|
||||||
subject: 'Action Needed to Reset Password',
|
subject: t`Action Needed to Reset Password`,
|
||||||
text,
|
text,
|
||||||
html,
|
html,
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,9 +1,12 @@
|
|||||||
import { Args, Mutation, Resolver } from '@nestjs/graphql';
|
import { Args, Context, Mutation, Resolver } from '@nestjs/graphql';
|
||||||
|
|
||||||
|
import { SOURCE_LOCALE } from 'twenty-shared';
|
||||||
|
|
||||||
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
|
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
|
||||||
import { ResendEmailVerificationTokenInput } from 'src/engine/core-modules/email-verification/dtos/resend-email-verification-token.input';
|
import { ResendEmailVerificationTokenInput } from 'src/engine/core-modules/email-verification/dtos/resend-email-verification-token.input';
|
||||||
import { ResendEmailVerificationTokenOutput } from 'src/engine/core-modules/email-verification/dtos/resend-email-verification-token.output';
|
import { ResendEmailVerificationTokenOutput } from 'src/engine/core-modules/email-verification/dtos/resend-email-verification-token.output';
|
||||||
import { EmailVerificationService } from 'src/engine/core-modules/email-verification/services/email-verification.service';
|
import { EmailVerificationService } from 'src/engine/core-modules/email-verification/services/email-verification.service';
|
||||||
|
import { I18nContext } from 'src/engine/core-modules/i18n/types/i18n-context.type';
|
||||||
import { workspaceValidator } from 'src/engine/core-modules/workspace/workspace.validate';
|
import { workspaceValidator } from 'src/engine/core-modules/workspace/workspace.validate';
|
||||||
import { OriginHeader } from 'src/engine/decorators/auth/origin-header.decorator';
|
import { OriginHeader } from 'src/engine/decorators/auth/origin-header.decorator';
|
||||||
|
|
||||||
@ -19,6 +22,7 @@ export class EmailVerificationResolver {
|
|||||||
@Args()
|
@Args()
|
||||||
resendEmailVerificationTokenInput: ResendEmailVerificationTokenInput,
|
resendEmailVerificationTokenInput: ResendEmailVerificationTokenInput,
|
||||||
@OriginHeader() origin: string,
|
@OriginHeader() origin: string,
|
||||||
|
@Context() context: I18nContext,
|
||||||
): Promise<ResendEmailVerificationTokenOutput> {
|
): Promise<ResendEmailVerificationTokenOutput> {
|
||||||
const workspace =
|
const workspace =
|
||||||
await this.domainManagerService.getWorkspaceByOriginOrDefaultWorkspace(
|
await this.domainManagerService.getWorkspaceByOriginOrDefaultWorkspace(
|
||||||
@ -30,6 +34,7 @@ export class EmailVerificationResolver {
|
|||||||
return await this.emailVerificationService.resendEmailVerificationToken(
|
return await this.emailVerificationService.resendEmailVerificationToken(
|
||||||
resendEmailVerificationTokenInput.email,
|
resendEmailVerificationTokenInput.email,
|
||||||
workspace,
|
workspace,
|
||||||
|
context.req.headers['x-locale'] ?? SOURCE_LOCALE,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
|
||||||
|
import { i18n } from '@lingui/core';
|
||||||
|
import { t } from '@lingui/core/macro';
|
||||||
import { render } from '@react-email/render';
|
import { render } from '@react-email/render';
|
||||||
import { addMilliseconds, differenceInMilliseconds } from 'date-fns';
|
import { addMilliseconds, differenceInMilliseconds } from 'date-fns';
|
||||||
import ms from 'ms';
|
import ms from 'ms';
|
||||||
@ -13,6 +15,7 @@ import {
|
|||||||
AppTokenType,
|
AppTokenType,
|
||||||
} from 'src/engine/core-modules/app-token/app-token.entity';
|
} from 'src/engine/core-modules/app-token/app-token.entity';
|
||||||
import { EmailVerificationTokenService } from 'src/engine/core-modules/auth/token/services/email-verification-token.service';
|
import { EmailVerificationTokenService } from 'src/engine/core-modules/auth/token/services/email-verification-token.service';
|
||||||
|
import { WorkspaceSubdomainCustomDomainAndIsCustomDomainEnabledType } from 'src/engine/core-modules/domain-manager/domain-manager.type';
|
||||||
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
|
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
|
||||||
import {
|
import {
|
||||||
EmailVerificationException,
|
EmailVerificationException,
|
||||||
@ -21,7 +24,6 @@ import {
|
|||||||
import { EmailService } from 'src/engine/core-modules/email/email.service';
|
import { EmailService } from 'src/engine/core-modules/email/email.service';
|
||||||
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
||||||
import { UserService } from 'src/engine/core-modules/user/services/user.service';
|
import { UserService } from 'src/engine/core-modules/user/services/user.service';
|
||||||
import { WorkspaceSubdomainCustomDomainAndIsCustomDomainEnabledType } from 'src/engine/core-modules/domain-manager/domain-manager.type';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
// eslint-disable-next-line @nx/workspace-inject-workspace-repository
|
// eslint-disable-next-line @nx/workspace-inject-workspace-repository
|
||||||
@ -40,6 +42,7 @@ export class EmailVerificationService {
|
|||||||
userId: string,
|
userId: string,
|
||||||
email: string,
|
email: string,
|
||||||
workspace: WorkspaceSubdomainCustomDomainAndIsCustomDomainEnabledType,
|
workspace: WorkspaceSubdomainCustomDomainAndIsCustomDomainEnabledType,
|
||||||
|
locale: keyof typeof APP_LOCALES,
|
||||||
) {
|
) {
|
||||||
if (!this.environmentService.get('IS_EMAIL_VERIFICATION_REQUIRED')) {
|
if (!this.environmentService.get('IS_EMAIL_VERIFICATION_REQUIRED')) {
|
||||||
return { success: false };
|
return { success: false };
|
||||||
@ -57,7 +60,7 @@ export class EmailVerificationService {
|
|||||||
|
|
||||||
const emailData = {
|
const emailData = {
|
||||||
link: verificationLink.toString(),
|
link: verificationLink.toString(),
|
||||||
locale: 'en' as keyof typeof APP_LOCALES,
|
locale,
|
||||||
};
|
};
|
||||||
|
|
||||||
const emailTemplate = SendEmailVerificationLinkEmail(emailData);
|
const emailTemplate = SendEmailVerificationLinkEmail(emailData);
|
||||||
@ -68,12 +71,14 @@ export class EmailVerificationService {
|
|||||||
plainText: true,
|
plainText: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
i18n.activate(locale);
|
||||||
|
|
||||||
await this.emailService.send({
|
await this.emailService.send({
|
||||||
from: `${this.environmentService.get(
|
from: `${this.environmentService.get(
|
||||||
'EMAIL_FROM_NAME',
|
'EMAIL_FROM_NAME',
|
||||||
)} <${this.environmentService.get('EMAIL_FROM_ADDRESS')}>`,
|
)} <${this.environmentService.get('EMAIL_FROM_ADDRESS')}>`,
|
||||||
to: email,
|
to: email,
|
||||||
subject: 'Welcome to Twenty: Please Confirm Your Email',
|
subject: t`Welcome to Twenty: Please Confirm Your Email`,
|
||||||
text,
|
text,
|
||||||
html,
|
html,
|
||||||
});
|
});
|
||||||
@ -84,6 +89,7 @@ export class EmailVerificationService {
|
|||||||
async resendEmailVerificationToken(
|
async resendEmailVerificationToken(
|
||||||
email: string,
|
email: string,
|
||||||
workspace: WorkspaceSubdomainCustomDomainAndIsCustomDomainEnabledType,
|
workspace: WorkspaceSubdomainCustomDomainAndIsCustomDomainEnabledType,
|
||||||
|
locale: keyof typeof APP_LOCALES,
|
||||||
) {
|
) {
|
||||||
if (!this.environmentService.get('IS_EMAIL_VERIFICATION_REQUIRED')) {
|
if (!this.environmentService.get('IS_EMAIL_VERIFICATION_REQUIRED')) {
|
||||||
throw new EmailVerificationException(
|
throw new EmailVerificationException(
|
||||||
@ -125,7 +131,7 @@ export class EmailVerificationService {
|
|||||||
await this.appTokenRepository.delete(existingToken.id);
|
await this.appTokenRepository.delete(existingToken.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.sendVerificationEmail(user.id, email, workspace);
|
await this.sendVerificationEmail(user.id, email, workspace, locale);
|
||||||
|
|
||||||
return { success: true };
|
return { success: true };
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,6 +3,8 @@ import { InjectRepository } from '@nestjs/typeorm';
|
|||||||
|
|
||||||
import crypto from 'crypto';
|
import crypto from 'crypto';
|
||||||
|
|
||||||
|
import { i18n } from '@lingui/core';
|
||||||
|
import { t } from '@lingui/core/macro';
|
||||||
import { render } from '@react-email/render';
|
import { render } from '@react-email/render';
|
||||||
import { addMilliseconds } from 'date-fns';
|
import { addMilliseconds } from 'date-fns';
|
||||||
import ms from 'ms';
|
import ms from 'ms';
|
||||||
@ -288,6 +290,8 @@ export class WorkspaceInvitationService {
|
|||||||
}
|
}
|
||||||
: {},
|
: {},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Todo: sender name and locale should come from workspace member not user!
|
||||||
const emailData = {
|
const emailData = {
|
||||||
link: link.toString(),
|
link: link.toString(),
|
||||||
workspace: { name: workspace.displayName, logo: workspace.logo },
|
workspace: { name: workspace.displayName, logo: workspace.logo },
|
||||||
@ -297,7 +301,7 @@ export class WorkspaceInvitationService {
|
|||||||
lastName: sender.lastName,
|
lastName: sender.lastName,
|
||||||
},
|
},
|
||||||
serverUrl: this.environmentService.get('SERVER_URL'),
|
serverUrl: this.environmentService.get('SERVER_URL'),
|
||||||
locale: 'en' as keyof typeof APP_LOCALES,
|
locale: sender.locale as keyof typeof APP_LOCALES,
|
||||||
};
|
};
|
||||||
|
|
||||||
const emailTemplate = SendInviteLinkEmail(emailData);
|
const emailTemplate = SendInviteLinkEmail(emailData);
|
||||||
@ -306,10 +310,12 @@ export class WorkspaceInvitationService {
|
|||||||
plainText: true,
|
plainText: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
i18n.activate(sender.locale);
|
||||||
|
|
||||||
await this.emailService.send({
|
await this.emailService.send({
|
||||||
from: `${sender.firstName} ${sender.lastName} (via Twenty) <${this.environmentService.get('EMAIL_FROM_ADDRESS')}>`,
|
from: `${sender.firstName} ${sender.lastName} (via Twenty) <${this.environmentService.get('EMAIL_FROM_ADDRESS')}>`,
|
||||||
to: invitation.value.email,
|
to: invitation.value.email,
|
||||||
subject: 'Join your team on Twenty',
|
subject: t`Join your team on Twenty`,
|
||||||
text,
|
text,
|
||||||
html,
|
html,
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
import { Injectable, Logger } from '@nestjs/common';
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
|
||||||
|
import { i18n } from '@lingui/core';
|
||||||
|
import { t } from '@lingui/core/macro';
|
||||||
import { render } from '@react-email/render';
|
import { render } from '@react-email/render';
|
||||||
import { differenceInDays } from 'date-fns';
|
import { differenceInDays } from 'date-fns';
|
||||||
import {
|
import {
|
||||||
@ -112,18 +114,21 @@ export class CleanerWorkspaceService {
|
|||||||
inactiveDaysBeforeDelete: this.inactiveDaysBeforeSoftDelete,
|
inactiveDaysBeforeDelete: this.inactiveDaysBeforeSoftDelete,
|
||||||
userName: `${workspaceMember.name.firstName} ${workspaceMember.name.lastName}`,
|
userName: `${workspaceMember.name.firstName} ${workspaceMember.name.lastName}`,
|
||||||
workspaceDisplayName: `${workspaceDisplayName}`,
|
workspaceDisplayName: `${workspaceDisplayName}`,
|
||||||
|
locale: workspaceMember.locale,
|
||||||
};
|
};
|
||||||
const emailTemplate = WarnSuspendedWorkspaceEmail(emailData);
|
const emailTemplate = WarnSuspendedWorkspaceEmail(emailData);
|
||||||
const html = render(emailTemplate, { pretty: true });
|
const html = render(emailTemplate, { pretty: true });
|
||||||
const text = render(emailTemplate, { plainText: true });
|
const text = render(emailTemplate, { plainText: true });
|
||||||
|
|
||||||
|
i18n.activate(workspaceMember.locale);
|
||||||
|
|
||||||
this.emailService.send({
|
this.emailService.send({
|
||||||
to: workspaceMember.userEmail,
|
to: workspaceMember.userEmail,
|
||||||
bcc: this.environmentService.get('EMAIL_SYSTEM_ADDRESS'),
|
bcc: this.environmentService.get('EMAIL_SYSTEM_ADDRESS'),
|
||||||
from: `${this.environmentService.get(
|
from: `${this.environmentService.get(
|
||||||
'EMAIL_FROM_NAME',
|
'EMAIL_FROM_NAME',
|
||||||
)} <${this.environmentService.get('EMAIL_FROM_ADDRESS')}>`,
|
)} <${this.environmentService.get('EMAIL_FROM_ADDRESS')}>`,
|
||||||
subject: 'Action needed to prevent workspace deletion',
|
subject: t`Action needed to prevent workspace deletion`,
|
||||||
html,
|
html,
|
||||||
text,
|
text,
|
||||||
});
|
});
|
||||||
@ -186,6 +191,7 @@ export class CleanerWorkspaceService {
|
|||||||
daysSinceInactive: daysSinceInactive,
|
daysSinceInactive: daysSinceInactive,
|
||||||
userName: `${workspaceMember.name.firstName} ${workspaceMember.name.lastName}`,
|
userName: `${workspaceMember.name.firstName} ${workspaceMember.name.lastName}`,
|
||||||
workspaceDisplayName,
|
workspaceDisplayName,
|
||||||
|
locale: workspaceMember.locale,
|
||||||
};
|
};
|
||||||
const emailTemplate = CleanSuspendedWorkspaceEmail(emailData);
|
const emailTemplate = CleanSuspendedWorkspaceEmail(emailData);
|
||||||
const html = render(emailTemplate, { pretty: true });
|
const html = render(emailTemplate, { pretty: true });
|
||||||
|
|||||||
Reference in New Issue
Block a user