Progress on translations (#9703)

Start adding a few translations on setting pages, introduce
pseudo-locale, switch to dynamic import, add eslint rule
This commit is contained in:
Félix Malfait
2025-01-16 23:34:54 +01:00
committed by GitHub
parent f44b31573a
commit 7acb68929f
46 changed files with 3019 additions and 299 deletions

View File

@ -1,3 +1,4 @@
import { useLingui } from '@lingui/react/macro';
import { useState } from 'react';
import { useRecoilValue, useSetRecoilState } from 'recoil';
import {
@ -35,26 +36,28 @@ type SwitchInfo = {
impact: string;
};
const MONTHLY_SWITCH_INFO: SwitchInfo = {
newInterval: SubscriptionInterval.Year,
to: 'to yearly',
from: 'from monthly to yearly',
impact: 'You will be charged immediately for the full year.',
};
const YEARLY_SWITCH_INFO: SwitchInfo = {
newInterval: SubscriptionInterval.Month,
to: 'to monthly',
from: 'from yearly to monthly',
impact: 'Your credit balance will be used to pay the monthly bills.',
};
const SWITCH_INFOS = {
year: YEARLY_SWITCH_INFO,
month: MONTHLY_SWITCH_INFO,
};
export const SettingsBilling = () => {
const { t } = useLingui();
const MONTHLY_SWITCH_INFO: SwitchInfo = {
newInterval: SubscriptionInterval.Year,
to: t`to yearly`,
from: t`from monthly to yearly`,
impact: t`You will be charged immediately for the full year.`,
};
const YEARLY_SWITCH_INFO: SwitchInfo = {
newInterval: SubscriptionInterval.Month,
to: t`to monthly`,
from: t`from yearly to monthly`,
impact: t`Your credit balance will be used to pay the monthly bills.`,
};
const SWITCH_INFOS = {
year: YEARLY_SWITCH_INFO,
month: MONTHLY_SWITCH_INFO,
};
const { enqueueSnackBar } = useSnackBar();
const onboardingStatus = useOnboardingStatus();
const subscriptionStatus = useSubscriptionStatus();
@ -94,6 +97,10 @@ export const SettingsBilling = () => {
setIsSwitchingIntervalModalOpen(true);
};
const from = switchingInfo.from;
const to = switchingInfo.to;
const impact = switchingInfo.impact;
const switchInterval = async () => {
try {
await updateBillingSubscription();
@ -107,28 +114,25 @@ export const SettingsBilling = () => {
};
setCurrentWorkspace(newCurrentWorkspace);
}
enqueueSnackBar(`Subscription has been switched ${switchingInfo.to}`, {
enqueueSnackBar(t`Subscription has been switched ${to}`, {
variant: SnackBarVariant.Success,
});
} catch (error: any) {
enqueueSnackBar(
`Error while switching subscription ${switchingInfo.to}.`,
{
variant: SnackBarVariant.Error,
},
);
enqueueSnackBar(t`Error while switching subscription ${to}.`, {
variant: SnackBarVariant.Error,
});
}
};
return (
<SubMenuTopBarContainer
title="Billing"
title={t`Billing`}
links={[
{
children: 'Workspace',
children: t`Workspace`,
href: getSettingsPagePath(SettingsPath.Workspace),
},
{ children: 'Billing' },
{ children: t`Billing` },
]}
>
<SettingsPageContainer>
@ -137,12 +141,12 @@ export const SettingsBilling = () => {
<>
<Section>
<H2Title
title="Manage your subscription"
description="Edit payment method, see your invoices and more"
title={t`Manage your subscription`}
description={t`Edit payment method, see your invoices and more`}
/>
<Button
Icon={IconCreditCard}
title="View billing details"
title={t`View billing details`}
variant="secondary"
onClick={openBillingPortal}
disabled={billingPortalButtonDisabled}
@ -150,12 +154,12 @@ export const SettingsBilling = () => {
</Section>
<Section>
<H2Title
title="Edit billing interval"
description={`Switch ${switchingInfo.from}`}
title={t`Edit billing interval`}
description={t`Switch ${from}`}
/>
<Button
Icon={IconCalendarEvent}
title={`Switch ${switchingInfo.to}`}
title={t`Switch ${to}`}
variant="secondary"
onClick={openSwitchingIntervalModal}
disabled={switchIntervalButtonDisabled}
@ -163,12 +167,12 @@ export const SettingsBilling = () => {
</Section>
<Section>
<H2Title
title="Cancel your subscription"
description="Your workspace will be disabled"
title={t`Cancel your subscription`}
description={t`Your workspace will be disabled`}
/>
<Button
Icon={IconCircleX}
title="Cancel Plan"
title={t`Cancel Plan`}
variant="secondary"
accent="danger"
onClick={openBillingPortal}
@ -181,15 +185,10 @@ export const SettingsBilling = () => {
<ConfirmationModal
isOpen={isSwitchingIntervalModalOpen}
setIsOpen={setIsSwitchingIntervalModalOpen}
title={`Switch billing ${switchingInfo.to}`}
subtitle={
<>
{`Are you sure that you want to change your billing interval?
${switchingInfo.impact}`}
</>
}
title={t`Switch billing ${to}`}
subtitle={t`Are you sure that you want to change your billing interval? ${impact}`}
onConfirmClick={switchInterval}
deleteButtonText={`Change ${switchingInfo.to}`}
deleteButtonText={t`Change ${to}`}
confirmButtonAccent={'blue'}
/>
</SubMenuTopBarContainer>

View File

@ -1,3 +1,4 @@
import { useLingui } from '@lingui/react/macro';
import { H2Title, Section } from 'twenty-ui';
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
@ -10,39 +11,46 @@ import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath';
import { SettingsPath } from '@/types/SettingsPath';
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
export const SettingsProfile = () => (
<SubMenuTopBarContainer
title="Profile"
links={[
{
children: 'User',
href: getSettingsPagePath(SettingsPath.ProfilePage),
},
{ children: 'Profile' },
]}
>
<SettingsPageContainer>
<Section>
<H2Title title="Picture" />
<ProfilePictureUploader />
</Section>
<Section>
<H2Title title="Name" description="Your name as it will be displayed" />
<NameFields />
</Section>
<Section>
<H2Title
title="Email"
description="The email associated to your account"
/>
<EmailField />
</Section>
<Section>
<ChangePassword />
</Section>
<Section>
<DeleteAccount />
</Section>
</SettingsPageContainer>
</SubMenuTopBarContainer>
);
export const SettingsProfile = () => {
const { t } = useLingui();
return (
<SubMenuTopBarContainer
title={t`Profile`}
links={[
{
children: t`User`,
href: getSettingsPagePath(SettingsPath.ProfilePage),
},
{ children: t`Profile` },
]}
>
<SettingsPageContainer>
<Section>
<H2Title title={t`Picture`} />
<ProfilePictureUploader />
</Section>
<Section>
<H2Title
title={t`Name`}
description={t`Your name as it will be displayed`}
/>
<NameFields />
</Section>
<Section>
<H2Title
title={t`Email`}
description={t`The email associated to your account`}
/>
<EmailField />
</Section>
<Section>
<ChangePassword />
</Section>
<Section>
<DeleteAccount />
</Section>
</SettingsPageContainer>
</SubMenuTopBarContainer>
);
};

View File

@ -1,12 +1,15 @@
import { useLingui } from '@lingui/react/macro';
import { useRecoilValue } from 'recoil';
import {
GithubVersionLink,
H2Title,
Section,
IconWorld,
Section,
UndecoratedLink,
} from 'twenty-ui';
import { useRecoilValue } from 'recoil';
import { isMultiWorkspaceEnabledState } from '@/client-config/states/isMultiWorkspaceEnabledState';
import { SettingsCard } from '@/settings/components/SettingsCard';
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
import { DeleteWorkspace } from '@/settings/profile/components/DeleteWorkspace';
import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath';
@ -16,47 +19,50 @@ import { WorkspaceLogoUploader } from '@/settings/workspace/components/Workspace
import { SettingsPath } from '@/types/SettingsPath';
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
import packageJson from '../../../package.json';
import { SettingsCard } from '@/settings/components/SettingsCard';
import { isMultiWorkspaceEnabledState } from '@/client-config/states/isMultiWorkspaceEnabledState';
export const SettingsWorkspace = () => {
const isMultiWorkspaceEnabled = useRecoilValue(isMultiWorkspaceEnabledState);
const { t } = useLingui();
return (
<SubMenuTopBarContainer
title="General"
title={t`General`}
links={[
{
children: 'Workspace',
children: t`Workspace`,
href: getSettingsPagePath(SettingsPath.Workspace),
},
{ children: 'General' },
{ children: t`General` },
]}
>
<SettingsPageContainer>
<Section>
<H2Title title="Picture" />
<H2Title title={t`Picture`} />
<WorkspaceLogoUploader />
</Section>
<Section>
<H2Title title="Name" description="Name of your workspace" />
<H2Title title={t`Name`} description={t`Name of your workspace`} />
<NameField />
</Section>
{isMultiWorkspaceEnabled && (
<>
<Section>
<H2Title
title="Domain"
description="Edit your subdomain name or set a custom domain."
title={t`Domain`}
description={t`Edit your subdomain name or set a custom domain.`}
/>
<UndecoratedLink to={getSettingsPagePath(SettingsPath.Domain)}>
<SettingsCard title="Customize Domain" Icon={<IconWorld />} />
<SettingsCard
title={t`Customize Domain`}
Icon={<IconWorld />}
/>
</UndecoratedLink>
</Section>
<Section>
<H2Title
title="Support"
title={t`Support`}
adornment={<ToggleImpersonate />}
description="Grant Twenty support temporary access to your workspace so we can troubleshoot problems or recover content on your behalf. You can revoke access at any time."
description={t`Grant Twenty support temporary access to your workspace so we can troubleshoot problems or recover content on your behalf. You can revoke access at any time.`}
/>
</Section>
</>

View File

@ -1,4 +1,5 @@
import { zodResolver } from '@hookform/resolvers/zod';
import { useLingui } from '@lingui/react/macro';
import { FormProvider, useForm } from 'react-hook-form';
import { useNavigate } from 'react-router-dom';
import { H2Title, Section } from 'twenty-ui';
@ -24,78 +25,78 @@ type SettingsDataModelNewObjectFormValues = z.infer<typeof newObjectFormSchema>;
export const SettingsNewObject = () => {
const navigate = useNavigate();
const { t } = useLingui();
const { enqueueSnackBar } = useSnackBar();
const { createOneObjectMetadataItem, findManyRecordsCache } =
useCreateOneObjectMetadataItem();
const settingsObjectsPagePath = getSettingsPagePath(SettingsPath.Objects);
const formConfig = useForm<SettingsDataModelNewObjectFormValues>({
mode: 'onTouched',
const methods = useForm<SettingsDataModelNewObjectFormValues>({
defaultValues: {},
resolver: zodResolver(newObjectFormSchema),
});
const { isValid, isSubmitting } = formConfig.formState;
const canSave = isValid && !isSubmitting;
const { handleSubmit } = methods;
const handleSave = async (
formValues: SettingsDataModelNewObjectFormValues,
) => {
const { createOneObjectMetadataItem } = useCreateOneObjectMetadataItem();
const onSubmit = async (data: SettingsDataModelNewObjectFormValues) => {
try {
const { data: response } = await createOneObjectMetadataItem(
settingsCreateObjectInputSchema.parse(formValues),
);
const createObjectInput = settingsCreateObjectInputSchema.parse(data);
navigate(
response
? `${settingsObjectsPagePath}/${response.createOneObject.namePlural}`
: settingsObjectsPagePath,
);
await createOneObjectMetadataItem(createObjectInput);
await findManyRecordsCache();
} catch (error) {
enqueueSnackBar((error as Error).message, {
variant: SnackBarVariant.Error,
enqueueSnackBar(t`Object created successfully`, {
variant: SnackBarVariant.Success,
});
navigate(getSettingsPagePath(SettingsPath.Objects));
} catch (error) {
if (error instanceof z.ZodError) {
enqueueSnackBar(t`Invalid object data`, {
variant: SnackBarVariant.Error,
});
} else {
enqueueSnackBar(t`Failed to create object`, {
variant: SnackBarVariant.Error,
});
}
}
};
return (
// eslint-disable-next-line react/jsx-props-no-spreading
<FormProvider {...formConfig}>
<>
<SubMenuTopBarContainer
title="New Object"
title={t`New Object`}
links={[
{
children: 'Workspace',
children: t`Workspace`,
href: getSettingsPagePath(SettingsPath.Workspace),
},
{
children: 'Objects',
href: settingsObjectsPagePath,
children: t`Objects`,
href: getSettingsPagePath(SettingsPath.Objects),
},
{ children: 'New' },
{ children: t`New` },
]}
actionButton={
<SaveAndCancelButtons
isSaveDisabled={!canSave}
isCancelDisabled={isSubmitting}
onCancel={() => navigate(settingsObjectsPagePath)}
onSave={formConfig.handleSubmit(handleSave)}
/>
}
>
<SettingsPageContainer>
<Section>
<H2Title
title="About"
description="Name in both singular (e.g., 'Invoice') and plural (e.g., 'Invoices') forms."
/>
<SettingsDataModelObjectAboutForm />
</Section>
{/* eslint-disable-next-line react/jsx-props-no-spreading */}
<FormProvider {...methods}>
<form onSubmit={handleSubmit(onSubmit)}>
<Section>
<H2Title
title={t`About`}
description={t`Define the name and description of your object`}
/>
<SettingsDataModelObjectAboutForm />
</Section>
<SaveAndCancelButtons
onCancel={() =>
navigate(getSettingsPagePath(SettingsPath.Objects))
}
/>
</form>
</FormProvider>
</SettingsPageContainer>
</SubMenuTopBarContainer>
</FormProvider>
</>
);
};

View File

@ -21,6 +21,7 @@ import { TableSection } from '@/ui/layout/table/components/TableSection';
import { useSortedArray } from '@/ui/layout/table/hooks/useSortedArray';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { useLingui } from '@lingui/react/macro';
import { isNonEmptyArray } from '@sniptt/guards';
import { useMemo, useState } from 'react';
import {
@ -38,12 +39,15 @@ import { SettingsObjectTableItem } from '~/pages/settings/data-model/types/Setti
const StyledIconChevronRight = styled(IconChevronRight)`
color: ${({ theme }) => theme.font.color.tertiary};
`;
const StyledSearchInput = styled(TextInput)`
padding-bottom: ${({ theme }) => theme.spacing(2)};
width: 100%;
`;
export const SettingsObjects = () => {
const theme = useTheme();
const { t } = useLingui();
const [searchTerm, setSearchTerm] = useState('');
const { deleteOneObjectMetadataItem } = useDeleteOneObjectMetadataItem();
const { updateOneObjectMetadataItem } = useUpdateOneObjectMetadataItem();
@ -109,6 +113,7 @@ export const SettingsObjects = () => {
inactiveObjectSettingsArray,
SETTINGS_OBJECT_TABLE_METADATA,
);
const filteredActiveObjectSettingsItems = useMemo(
() =>
sortedActiveObjectSettingsItems.filter(
@ -128,14 +133,15 @@ export const SettingsObjects = () => {
),
[sortedInactiveObjectSettingsItems, searchTerm],
);
return (
<SubMenuTopBarContainer
title="Data model"
title={t`Data model`}
actionButton={
<UndecoratedLink to={getSettingsPagePath(SettingsPath.NewObject)}>
<Button
Icon={IconPlus}
title="Add object"
title={t`Add object`}
accent="blue"
size="small"
/>
@ -143,11 +149,11 @@ export const SettingsObjects = () => {
}
links={[
{
children: 'Workspace',
children: t`Workspace`,
href: getSettingsPagePath(SettingsPath.Workspace),
},
{
children: 'Objects',
children: t`Objects`,
},
]}
>
@ -155,11 +161,11 @@ export const SettingsObjects = () => {
<>
<SettingsObjectCoverImage />
<Section>
<H2Title title="Existing objects" />
<H2Title title={t`Existing objects`} />
<StyledSearchInput
LeftIcon={IconSearch}
placeholder="Search an object..."
placeholder={t`Search an object...`}
value={searchTerm}
onChange={setSearchTerm}
/>
@ -181,7 +187,7 @@ export const SettingsObjects = () => {
<TableHeader></TableHeader>
</StyledObjectTableRow>
{isNonEmptyArray(sortedActiveObjectSettingsItems) && (
<TableSection title="Active">
<TableSection title={t`Active`}>
{filteredActiveObjectSettingsItems.map(
(objectSettingsItem) => (
<SettingsObjectMetadataItemTableRow
@ -205,7 +211,7 @@ export const SettingsObjects = () => {
</TableSection>
)}
{isNonEmptyArray(inactiveObjectMetadataItems) && (
<TableSection title="Inactive">
<TableSection title={t`Inactive`}>
{filteredInactiveObjectSettingsItems.map(
(objectSettingsItem) => (
<SettingsObjectMetadataItemTableRow

View File

@ -7,6 +7,7 @@ import { SettingsPath } from '@/types/SettingsPath';
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
import styled from '@emotion/styled';
import { useLingui } from '@lingui/react/macro';
import { Button, H2Title, IconPlus, MOBILE_VIEWPORT, Section } from 'twenty-ui';
const StyledButtonContainer = styled.div`
@ -27,30 +28,32 @@ const StyledContainer = styled.div<{ isMobile: boolean }>`
export const SettingsDevelopers = () => {
const isMobile = useIsMobile();
const { t } = useLingui();
return (
<SubMenuTopBarContainer
title="Developers"
title={t`Developers`}
actionButton={<SettingsReadDocumentationButton />}
links={[
{
children: 'Workspace',
children: t`Workspace`,
href: getSettingsPagePath(SettingsPath.Workspace),
},
{ children: 'Developers' },
{ children: t`Developers` },
]}
>
<SettingsPageContainer>
<StyledContainer isMobile={isMobile}>
<Section>
<H2Title
title="API keys"
description="Active APIs keys created by you or your team."
title={t`API keys`}
description={t`Active APIs keys created by you or your team.`}
/>
<SettingsApiKeysTable />
<StyledButtonContainer>
<Button
Icon={IconPlus}
title="Create API key"
title={t`Create API key`}
size="small"
variant="secondary"
to={'/settings/developers/api-keys/new'}
@ -59,14 +62,14 @@ export const SettingsDevelopers = () => {
</Section>
<Section>
<H2Title
title="Webhooks"
description="Establish Webhook endpoints for notifications on asynchronous events."
title={t`Webhooks`}
description={t`Establish Webhook endpoints for notifications on asynchronous events.`}
/>
<SettingsWebhooksTable />
<StyledButtonContainer>
<Button
Icon={IconPlus}
title="Create Webhook"
title={t`Create Webhook`}
size="small"
variant="secondary"
to={'/settings/developers/webhooks/new'}

View File

@ -1,12 +1,14 @@
import styled from '@emotion/styled';
import { useRecoilState } from 'recoil';
import { useRecoilState, useRecoilValue } from 'recoil';
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { isDebugModeState } from '@/client-config/states/isDebugModeState';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
import { Select } from '@/ui/input/components/Select';
import { i18n } from '@lingui/core';
import { dynamicActivate } from '~/utils/i18n/dynamicActivate';
import { isDefined } from '~/utils/isDefined';
import { logError } from '~/utils/logError';
@ -20,6 +22,7 @@ export const LocalePicker = () => {
const [currentWorkspaceMember, setCurrentWorkspaceMember] = useRecoilState(
currentWorkspaceMemberState,
);
const isDebugMode = useRecoilValue(isDebugModeState);
const { updateOneRecord } = useUpdateOneRecord({
objectNameSingular: CoreObjectNameSingular.WorkspaceMember,
@ -49,9 +52,47 @@ export const LocalePicker = () => {
});
updateWorkspaceMember({ locale: value });
i18n.activate(value);
dynamicActivate(value);
};
const localeOptions = [
{
label: 'Portuguese',
value: 'pt',
},
{
label: 'French',
value: 'fr',
},
{
label: 'German',
value: 'de',
},
{
label: 'Italian',
value: 'it',
},
{
label: 'Spanish',
value: 'es',
},
{
label: 'English',
value: 'en',
},
{
label: 'Chinese',
value: 'zh',
},
];
if (isDebugMode) {
localeOptions.push({
label: 'Pseudo-English',
value: 'pseudo-en',
});
}
return (
<StyledContainer>
<Select
@ -59,36 +100,7 @@ export const LocalePicker = () => {
dropdownWidthAuto
fullWidth
value={i18n.locale}
options={[
{
label: 'Portuguese',
value: 'pt',
},
{
label: 'French',
value: 'fr',
},
{
label: 'German',
value: 'de',
},
{
label: 'Italian',
value: 'it',
},
{
label: 'Spanish',
value: 'es',
},
{
label: 'English',
value: 'en',
},
{
label: 'Chinese',
value: 'zh',
},
]}
options={localeOptions}
onChange={(value) => handleLocaleChange(value)}
/>
</StyledContainer>

View File

@ -1,4 +1,5 @@
import styled from '@emotion/styled';
import { useLingui } from '@lingui/react/macro';
import { H2Title, IconLock, Section, Tag } from 'twenty-ui';
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
@ -25,27 +26,29 @@ const StyledSSOSection = styled(Section)`
`;
export const SettingsSecurity = () => {
const { t } = useLingui();
return (
<SubMenuTopBarContainer
title="Security"
title={t`Security`}
actionButton={<SettingsReadDocumentationButton />}
links={[
{
children: 'Workspace',
children: t`Workspace`,
href: getSettingsPagePath(SettingsPath.Workspace),
},
{ children: 'Security' },
{ children: t`Security` },
]}
>
<SettingsPageContainer>
<StyledMainContent>
<StyledSSOSection>
<H2Title
title="SSO"
description="Configure an SSO connection"
title={t`SSO`}
description={t`Configure an SSO connection`}
adornment={
<Tag
text={'Enterprise'}
text={t`Enterprise`}
color={'transparent'}
Icon={IconLock}
variant={'border'}
@ -57,8 +60,8 @@ export const SettingsSecurity = () => {
<Section>
<StyledContainer>
<H2Title
title="Authentication"
description="Customize your workspace security"
title={t`Authentication`}
description={t`Customize your workspace security`}
/>
<SettingsSecurityOptionsList />
</StyledContainer>

View File

@ -1,38 +1,28 @@
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { useRedirectToWorkspaceDomain } from '@/domain-manager/hooks/useRedirectToWorkspaceDomain';
import { domainConfigurationState } from '@/domain-manager/states/domainConfigurationState';
import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons';
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath';
import { SettingsPath } from '@/types/SettingsPath';
import { zodResolver } from '@hookform/resolvers/zod';
import { H2Title, Section } from 'twenty-ui';
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
import { TextInputV2 } from '@/ui/input/components/TextInputV2';
import { Controller, useForm } from 'react-hook-form';
import { z } from 'zod';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { useRecoilState, useRecoilValue } from 'recoil';
import styled from '@emotion/styled';
import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons';
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
import { useNavigate } from 'react-router-dom';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { TextInputV2 } from '@/ui/input/components/TextInputV2';
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
import styled from '@emotion/styled';
import { zodResolver } from '@hookform/resolvers/zod';
import { useLingui } from '@lingui/react/macro';
import { Controller, useForm } from 'react-hook-form';
import { useNavigate } from 'react-router-dom';
import { useRecoilState, useRecoilValue } from 'recoil';
import { H2Title, Section } from 'twenty-ui';
import { z } from 'zod';
import { useUpdateWorkspaceMutation } from '~/generated/graphql';
import { domainConfigurationState } from '@/domain-manager/states/domainConfigurationState';
import { isDefined } from '~/utils/isDefined';
import { useRedirectToWorkspaceDomain } from '@/domain-manager/hooks/useRedirectToWorkspaceDomain';
const validationSchema = z
.object({
subdomain: z
.string()
.min(3, { message: 'Subdomain can not be shorter than 3 characters' })
.max(30, { message: 'Subdomain can not be longer than 30 characters' })
.regex(/^[a-z0-9][a-z0-9-]{1,28}[a-z0-9]$/, {
message:
'Use letter, number and dash only. Start and finish with a letter or a number',
}),
})
.required();
type Form = z.infer<typeof validationSchema>;
type Form = {
subdomain: string;
};
const StyledDomainFromWrapper = styled.div`
align-items: center;
@ -49,6 +39,19 @@ const StyledDomain = styled.h2`
export const SettingsDomain = () => {
const navigate = useNavigate();
const { t } = useLingui();
const validationSchema = z
.object({
subdomain: z
.string()
.min(3, { message: t`Subdomain can not be shorter than 3 characters` })
.max(30, { message: t`Subdomain can not be longer than 30 characters` })
.regex(/^[a-z0-9][a-z0-9-]{1,28}[a-z0-9]$/, {
message: t`Use letter, number and dash only. Start and finish with a letter or a number`,
}),
})
.required();
const domainConfiguration = useRecoilValue(domainConfigurationState);
@ -81,7 +84,7 @@ export const SettingsDomain = () => {
const values = getValues();
if (!values || !isValid || !currentWorkspace) {
throw new Error('Invalid form values');
throw new Error(t`Invalid form values`);
}
await updateWorkspace({
@ -101,8 +104,8 @@ export const SettingsDomain = () => {
} catch (error) {
if (
error instanceof Error &&
(error.message === 'Subdomain already taken' ||
error.message.endsWith('not allowed'))
(error.message === t`Subdomain already taken` ||
error.message.endsWith(t`not allowed`))
) {
control.setError('subdomain', {
type: 'manual',
@ -119,17 +122,17 @@ export const SettingsDomain = () => {
return (
<SubMenuTopBarContainer
title="General"
title={t`General`}
links={[
{
children: 'Workspace',
children: t`Workspace`,
href: getSettingsPagePath(SettingsPath.Workspace),
},
{
children: 'General',
children: t`General`,
href: getSettingsPagePath(SettingsPath.Workspace),
},
{ children: 'Domain' },
{ children: t`Domain` },
]}
actionButton={
<SaveAndCancelButtons
@ -144,8 +147,8 @@ export const SettingsDomain = () => {
<SettingsPageContainer>
<Section>
<H2Title
title="Domain"
description="Set the name of your subdomain"
title={t`Domain`}
description={t`Set the name of your subdomain`}
/>
{currentWorkspace?.subdomain && (
<StyledDomainFromWrapper>