Files
twenty/packages/twenty-front/src/pages/settings/workspace/SettingsDomain.tsx
Antoine Moreaux 7abc67c905 refactor(forms): simplify form handling and button behavior (#10441)
Removed redundant handleSave and handleSubmit props in domain settings.
Integrated form submission logic directly into form components, ensuring
consistent behavior and reducing complexity. Updated button components
to explicitly support the "type" attribute for improved accessibility
and functionality.

---------

Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
2025-02-25 10:37:36 +00:00

244 lines
7.8 KiB
TypeScript

import { ApolloError } from '@apollo/client';
import {
CurrentWorkspace,
currentWorkspaceState,
} from '@/auth/states/currentWorkspaceState';
import { useRecoilState } from 'recoil';
import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons';
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import {
FeatureFlagKey,
useUpdateWorkspaceMutation,
} from '~/generated/graphql';
import { useRedirectToWorkspaceDomain } from '@/domain-manager/hooks/useRedirectToWorkspaceDomain';
import { SettingsCustomDomain } from '~/pages/settings/workspace/SettingsCustomDomain';
import { SettingsSubdomain } from '~/pages/settings/workspace/SettingsSubdomain';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
import { Trans, useLingui } from '@lingui/react/macro';
import { z } from 'zod';
import { FormProvider, useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
import { SettingsPath } from '@/types/SettingsPath';
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
import { SettingsCustomDomainEffect } from '~/pages/settings/workspace/SettingsCustomDomainEffect';
import { isDefined } from 'twenty-shared';
export const SettingsDomain = () => {
const navigate = useNavigateSettings();
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`,
}),
customDomain: z
.string()
.regex(
/^([a-zA-Z0-9][a-zA-Z0-9-]*\.)+[a-zA-Z0-9][a-zA-Z0-9-]*\.[a-zA-Z]{2,}$/,
{
message: t`Invalid custom domain. Please include at least one subdomain (e.g., sub.example.com).`,
},
)
.regex(
/^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9-]*[A-Za-z0-9])$/,
{
message: t`Invalid domain. Domains have to be smaller than 256 characters in length, cannot be IP addresses, cannot contain spaces, cannot contain any special characters such as _~\`!@#$%^*()=+{}[]|\\;:'",<>/? and cannot begin or end with a '-' character.`,
},
)
.max(256)
.optional()
.or(z.literal('')),
})
.required();
const { enqueueSnackBar } = useSnackBar();
const [updateWorkspace] = useUpdateWorkspaceMutation();
const { redirectToWorkspaceDomain } = useRedirectToWorkspaceDomain();
const isCustomDomainEnabled = useIsFeatureEnabled(
FeatureFlagKey.IsCustomDomainEnabled,
);
const [currentWorkspace, setCurrentWorkspace] = useRecoilState(
currentWorkspaceState,
);
const form = useForm<{
subdomain: string;
customDomain: string | null;
}>({
mode: 'onSubmit',
delayError: 500,
defaultValues: {
subdomain: currentWorkspace?.subdomain ?? '',
customDomain: currentWorkspace?.customDomain ?? '',
},
resolver: zodResolver(validationSchema),
});
const subdomainValue = form.watch('subdomain');
const customDomainValue = form.watch('customDomain');
const updateCustomDomain = (
customDomain: string | null,
currentWorkspace: CurrentWorkspace,
) => {
updateWorkspace({
variables: {
input: {
customDomain:
isDefined(customDomain) && customDomain.length > 0
? customDomain
: null,
},
},
onCompleted: () => {
setCurrentWorkspace({
...currentWorkspace,
customDomain:
customDomain && customDomain.length > 0 ? customDomain : null,
});
enqueueSnackBar(t`Custom domain updated`, {
variant: SnackBarVariant.Success,
});
},
onError: (error) => {
if (
error instanceof ApolloError &&
error.graphQLErrors[0]?.extensions?.code === 'CONFLICT'
) {
return form.control.setError('subdomain', {
type: 'manual',
message: t`Subdomain already taken`,
});
}
enqueueSnackBar((error as Error).message, {
variant: SnackBarVariant.Error,
});
},
});
};
const updateSubdomain = (
subdomain: string,
currentWorkspace: CurrentWorkspace,
) => {
updateWorkspace({
variables: {
input: {
subdomain,
},
},
onError: (error) => {
if (
error instanceof ApolloError &&
error.graphQLErrors[0]?.extensions?.code === 'CONFLICT'
) {
return form.control.setError('subdomain', {
type: 'manual',
message: t`Subdomain already taken`,
});
}
enqueueSnackBar((error as Error).message, {
variant: SnackBarVariant.Error,
});
},
onCompleted: () => {
const currentUrl = new URL(window.location.href);
currentUrl.hostname = new URL(
currentWorkspace.workspaceUrls.subdomainUrl,
).hostname.replace(currentWorkspace.subdomain, subdomain);
setCurrentWorkspace({
...currentWorkspace,
subdomain,
});
enqueueSnackBar(t`Subdomain updated`, {
variant: SnackBarVariant.Success,
});
redirectToWorkspaceDomain(currentUrl.toString());
},
});
};
const handleSave = async () => {
const values = form.getValues();
if (
subdomainValue === currentWorkspace?.subdomain &&
customDomainValue === currentWorkspace?.customDomain
) {
return enqueueSnackBar(t`No change detected`, {
variant: SnackBarVariant.Error,
});
}
if (!values || !currentWorkspace) {
return enqueueSnackBar(t`Invalid form values`, {
variant: SnackBarVariant.Error,
});
}
if (
isDefined(values.subdomain) &&
values.subdomain !== currentWorkspace.subdomain
) {
return updateSubdomain(values.subdomain, currentWorkspace);
}
if (values.customDomain !== currentWorkspace.customDomain) {
return updateCustomDomain(values.customDomain, currentWorkspace);
}
};
return (
<form onSubmit={form.handleSubmit(handleSave)}>
{/* eslint-disable-next-line react/jsx-props-no-spreading */}
<FormProvider {...form}>
<SubMenuTopBarContainer
title={t`Domain`}
links={[
{
children: <Trans>Workspace</Trans>,
href: getSettingsPath(SettingsPath.Workspace),
},
{
children: <Trans>General</Trans>,
href: getSettingsPath(SettingsPath.Workspace),
},
{ children: <Trans>Domain</Trans> },
]}
actionButton={
<SaveAndCancelButtons
onCancel={() => navigate(SettingsPath.Workspace)}
isSaveDisabled={form.formState.isSubmitting}
/>
}
>
<SettingsPageContainer>
<SettingsSubdomain />
{isCustomDomainEnabled && (
<>
<SettingsCustomDomainEffect />
<SettingsCustomDomain />
</>
)}
</SettingsPageContainer>
</SubMenuTopBarContainer>
</FormProvider>
</form>
);
};