Revert "Make subdomain minimum length configurable via env var" (#23871)

Instead, reduce the subdomain minimum length to 1 char

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23871?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
This commit is contained in:
Marie
2026-08-07 12:27:00 +02:00
committed by GitHub
parent 47f82b9121
commit 4dbaafc65d
27 changed files with 214 additions and 312 deletions
@@ -993,6 +993,7 @@ export type ClientConfig = {
publicFunctionDomain?: Maybe<Scalars['String']['output']>;
sentry: Sentry;
signInPrefilled: Scalars['Boolean']['output'];
/** @deprecated Subdomain minimum length is no longer configurable; kept one release for API compatibility, no longer read by the frontend. */
subdomainMinLength: Scalars['Float']['output'];
support: Support;
};
@@ -1,6 +1,4 @@
import { domainConfigurationState } from '@/domain-manager/states/domainConfigurationState';
import { getSubdomainValidationSchema } from '@/settings/domains/utils/getSubdomainValidationSchema';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { useApolloClient, useLazyQuery } from '@apollo/client/react';
import { useMemo, useState } from 'react';
import {
@@ -27,11 +25,7 @@ export const useWorkspaceSubdomainField = ({
isSubdomainEnabled = true,
}: { isSubdomainEnabled?: boolean } = {}) => {
const apolloClient = useApolloClient();
const { subdomainMinLength } = useAtomStateValue(domainConfigurationState);
const subdomainSchema = useMemo(
() => getSubdomainValidationSchema(subdomainMinLength),
[subdomainMinLength],
);
const subdomainSchema = getSubdomainValidationSchema();
const defaults = useMemo(
() =>
@@ -208,7 +208,6 @@ export const useClientConfig = (): UseClientConfigResult => {
defaultSubdomain: clientConfig?.defaultSubdomain,
frontDomain: clientConfig?.frontDomain,
publicFunctionDomain: clientConfig?.publicFunctionDomain,
subdomainMinLength: clientConfig?.subdomainMinLength,
});
setCanManageFeatureFlags(clientConfig?.canManageFeatureFlags);
setLabPublicFeatureFlags(clientConfig?.publicFeatureFlags);
@@ -25,7 +25,6 @@ export type ClientConfig = {
canManageFeatureFlags: boolean;
captcha: Captcha;
defaultSubdomain?: string;
subdomainMinLength: number;
frontDomain: string;
publicFunctionDomain?: string | null;
isAttachmentPreviewEnabled: boolean;
@@ -1,14 +1,10 @@
import { type ClientConfig } from '@/client-config/types/ClientConfig';
import { createAtomState } from '@/ui/utilities/state/jotai/utils/createAtomState';
import { DEFAULT_SUBDOMAIN_MIN_LENGTH } from 'twenty-shared/constants';
export const domainConfigurationState = createAtomState<
Pick<
ClientConfig,
| 'frontDomain'
| 'defaultSubdomain'
| 'publicFunctionDomain'
| 'subdomainMinLength'
'frontDomain' | 'defaultSubdomain' | 'publicFunctionDomain'
>
>({
key: 'domainConfiguration',
@@ -16,6 +12,5 @@ export const domainConfigurationState = createAtomState<
frontDomain: '',
defaultSubdomain: undefined,
publicFunctionDomain: undefined,
subdomainMinLength: DEFAULT_SUBDOMAIN_MIN_LENGTH,
},
});
@@ -1,11 +1,9 @@
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { useRedirectToWorkspaceDomain } from '@/domain-manager/hooks/useRedirectToWorkspaceDomain';
import { domainConfigurationState } from '@/domain-manager/states/domainConfigurationState';
import { getSubdomainValidationSchema } from '@/settings/domains/utils/getSubdomainValidationSchema';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { useModal } from '@/ui/layout/modal/hooks/useModal';
import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { CombinedGraphQLErrors } from '@apollo/client/errors';
import { useMutation } from '@apollo/client/react';
import { useLingui } from '@lingui/react/macro';
@@ -18,8 +16,7 @@ export const SUBDOMAIN_CHANGE_CONFIRMATION_MODAL_ID =
export const useSettingsSubdomain = () => {
const { t } = useLingui();
const { subdomainMinLength } = useAtomStateValue(domainConfigurationState);
const subdomainSchema = getSubdomainValidationSchema(subdomainMinLength);
const subdomainSchema = getSubdomainValidationSchema();
const { enqueueSuccessSnackBar, enqueueErrorSnackBar } = useSnackBar();
const [updateWorkspace] = useMutation(UpdateWorkspaceDocument);
@@ -1,20 +1,15 @@
import { t } from '@lingui/core/macro';
import {
DEFAULT_SUBDOMAIN_MIN_LENGTH,
RESERVED_SUBDOMAINS,
SUBDOMAIN_PATTERN,
} from 'twenty-shared/constants';
import { z } from 'zod';
export const getSubdomainValidationSchema = (
minLength = DEFAULT_SUBDOMAIN_MIN_LENGTH,
) =>
export const getSubdomainValidationSchema = () =>
z
.string()
.min(minLength, {
message: t`Subdomain cannot be shorter than ${minLength} characters`,
})
.max(30, { message: t`Subdomain can not be longer than 30 characters` })
.min(1, { message: t`Subdomain cannot be empty` })
.max(30, { message: t`Subdomain cannot be longer than 30 characters` })
.regex(SUBDOMAIN_PATTERN, {
message: t`Use letter, number and dash only. Start and finish with a letter or a number`,
})
@@ -1,11 +1,14 @@
import { type Meta, type StoryObj } from '@storybook/react-vite';
import { expect, userEvent, within } from 'storybook/test';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { jotaiStore } from '@/ui/utilities/state/jotai/jotaiStore';
import {
PageDecorator,
type PageDecoratorArgs,
} from '~/testing/decorators/PageDecorator';
import { graphqlMocks } from '~/testing/graphqlMocks';
import { mockCurrentWorkspace } from '~/testing/mock-data/users';
import { SettingsSubdomainPage } from '~/pages/settings/domains/SettingsSubdomainPage';
@@ -14,6 +17,13 @@ const meta: Meta<PageDecoratorArgs> = {
component: SettingsSubdomainPage,
decorators: [PageDecorator],
args: { routePath: '/settings/domains/subdomain' },
beforeEach: () => {
jotaiStore.set(currentWorkspaceState.atom, mockCurrentWorkspace);
return () => {
jotaiStore.set(currentWorkspaceState.atom, null);
};
},
parameters: {
msw: graphqlMocks,
},
@@ -25,20 +35,17 @@ export type Story = StoryObj<typeof SettingsSubdomainPage>;
export const Default: Story = {};
export const TooShortSubdomain: Story = {
export const EmptySubdomain: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const input = await canvas.findByRole('textbox', {}, { timeout: 5000 });
await expect(input).toHaveValue(mockCurrentWorkspace.subdomain);
await userEvent.clear(input);
await userEvent.type(input, 'ab');
const errorMessage = await canvas.findByText(
'Subdomain cannot be shorter than 3 characters',
);
await expect(errorMessage).toBeVisible();
await canvas.findByText('Subdomain cannot be empty');
const saveButton = canvas.getByText('Save');
@@ -16,7 +16,6 @@ export const mockedClientConfig: ClientConfig = {
},
frontDomain: 'localhost',
defaultSubdomain: 'app',
subdomainMinLength: 3,
analyticsEnabled: true,
support: {
supportDriver: SupportDriver.FRONT,