14240 extensibility ability to create multiple custom domains for each workspace 2 (#14307)

Adds a public-domain core-module. 
Reorganize custom-domain files properly
This commit is contained in:
martmull
2025-09-11 12:24:57 +02:00
committed by GitHub
parent ceffc82b9c
commit b84f4075e5
48 changed files with 1215 additions and 797 deletions
@@ -868,22 +868,6 @@ export type CursorPaging = {
last?: InputMaybe<Scalars['Int']>;
};
export type CustomDomainRecord = {
__typename?: 'CustomDomainRecord';
key: Scalars['String'];
status: Scalars['String'];
type: Scalars['String'];
validationType: Scalars['String'];
value: Scalars['String'];
};
export type CustomDomainValidRecords = {
__typename?: 'CustomDomainValidRecords';
customDomain: Scalars['String'];
id: Scalars['UUID'];
records: Array<CustomDomainRecord>;
};
/** Database Event Action */
export enum DatabaseEventAction {
CREATED = 'CREATED',
@@ -961,6 +945,22 @@ export enum DistantTableUpdate {
TABLE_DELETED = 'TABLE_DELETED'
}
export type DomainRecord = {
__typename?: 'DomainRecord';
key: Scalars['String'];
status: Scalars['String'];
type: Scalars['String'];
validationType: Scalars['String'];
value: Scalars['String'];
};
export type DomainValidRecords = {
__typename?: 'DomainValidRecords';
domain: Scalars['String'];
id: Scalars['UUID'];
records: Array<DomainRecord>;
};
export type DuplicateWorkflowVersionStepInput = {
stepId: Scalars['String'];
workflowVersionId: Scalars['String'];
@@ -1410,7 +1410,7 @@ export type Mutation = {
assignRoleToAgent: Scalars['Boolean'];
assignRoleToApiKey: Scalars['Boolean'];
authorizeApp: AuthorizeApp;
checkCustomDomainValidRecords?: Maybe<CustomDomainValidRecords>;
checkCustomDomainValidRecords?: Maybe<DomainValidRecords>;
checkoutSession: BillingSessionOutput;
computeStepOutputSchema: Scalars['JSON'];
createAgentChatThread: AgentChatThread;
@@ -1438,6 +1438,7 @@ export type Mutation = {
createPageLayout: PageLayout;
createPageLayoutTab: PageLayoutTab;
createPageLayoutWidget: PageLayoutWidget;
createPublicDomain: PublicDomain;
createSAMLIdentityProvider: SetupSsoOutput;
createWebhook: Webhook;
createWorkflowVersionEdge: WorkflowVersionStepChanges;
@@ -1462,6 +1463,7 @@ export type Mutation = {
deletePageLayout: PageLayout;
deletePageLayoutTab: Scalars['Boolean'];
deletePageLayoutWidget: PageLayoutWidget;
deletePublicDomain: Scalars['Boolean'];
deleteSSOIdentityProvider: DeleteSsoOutput;
deleteTwoFactorAuthenticationMethod: DeleteTwoFactorAuthenticationMethodOutput;
deleteUser: User;
@@ -1734,6 +1736,11 @@ export type MutationCreatePageLayoutWidgetArgs = {
};
export type MutationCreatePublicDomainArgs = {
domain: Scalars['String'];
};
export type MutationCreateSamlIdentityProviderArgs = {
input: SetupSamlSsoInput;
};
@@ -1849,6 +1856,11 @@ export type MutationDeletePageLayoutWidgetArgs = {
};
export type MutationDeletePublicDomainArgs = {
domain: Scalars['String'];
};
export type MutationDeleteSsoIdentityProviderArgs = {
input: DeleteSsoInput;
};
@@ -2593,6 +2605,14 @@ export type PostgresCredentials = {
workspaceId: Scalars['UUID'];
};
export type PublicDomain = {
__typename?: 'PublicDomain';
createdAt: Scalars['DateTime'];
domain: Scalars['String'];
id: Scalars['UUID'];
isValidated: Scalars['Boolean'];
};
export type PublicFeatureFlag = {
__typename?: 'PublicFeatureFlag';
key: FeatureFlagKey;
@@ -5403,7 +5423,7 @@ export type UploadWorkspaceLogoMutation = { __typename?: 'Mutation', uploadWorks
export type CheckCustomDomainValidRecordsMutationVariables = Exact<{ [key: string]: never; }>;
export type CheckCustomDomainValidRecordsMutation = { __typename?: 'Mutation', checkCustomDomainValidRecords?: { __typename?: 'CustomDomainValidRecords', id: string, customDomain: string, records: Array<{ __typename?: 'CustomDomainRecord', type: string, key: string, value: string, validationType: string, status: string }> } | null };
export type CheckCustomDomainValidRecordsMutation = { __typename?: 'Mutation', checkCustomDomainValidRecords?: { __typename?: 'DomainValidRecords', id: string, domain: string, records: Array<{ __typename?: 'DomainRecord', type: string, key: string, value: string, validationType: string, status: string }> } | null };
export type GetWorkspaceFromInviteHashQueryVariables = Exact<{
inviteHash: Scalars['String'];
@@ -12604,7 +12624,7 @@ export const CheckCustomDomainValidRecordsDocument = gql`
mutation CheckCustomDomainValidRecords {
checkCustomDomainValidRecords {
id
customDomain
domain
records {
type
key
+37 -17
View File
@@ -832,22 +832,6 @@ export type CursorPaging = {
last?: InputMaybe<Scalars['Int']>;
};
export type CustomDomainRecord = {
__typename?: 'CustomDomainRecord';
key: Scalars['String'];
status: Scalars['String'];
type: Scalars['String'];
validationType: Scalars['String'];
value: Scalars['String'];
};
export type CustomDomainValidRecords = {
__typename?: 'CustomDomainValidRecords';
customDomain: Scalars['String'];
id: Scalars['UUID'];
records: Array<CustomDomainRecord>;
};
/** Database Event Action */
export enum DatabaseEventAction {
CREATED = 'CREATED',
@@ -925,6 +909,22 @@ export enum DistantTableUpdate {
TABLE_DELETED = 'TABLE_DELETED'
}
export type DomainRecord = {
__typename?: 'DomainRecord';
key: Scalars['String'];
status: Scalars['String'];
type: Scalars['String'];
validationType: Scalars['String'];
value: Scalars['String'];
};
export type DomainValidRecords = {
__typename?: 'DomainValidRecords';
domain: Scalars['String'];
id: Scalars['UUID'];
records: Array<DomainRecord>;
};
export type DuplicateWorkflowVersionStepInput = {
stepId: Scalars['String'];
workflowVersionId: Scalars['String'];
@@ -1367,7 +1367,7 @@ export type Mutation = {
assignRoleToAgent: Scalars['Boolean'];
assignRoleToApiKey: Scalars['Boolean'];
authorizeApp: AuthorizeApp;
checkCustomDomainValidRecords?: Maybe<CustomDomainValidRecords>;
checkCustomDomainValidRecords?: Maybe<DomainValidRecords>;
checkoutSession: BillingSessionOutput;
computeStepOutputSchema: Scalars['JSON'];
createAgentChatThread: AgentChatThread;
@@ -1394,6 +1394,7 @@ export type Mutation = {
createPageLayout: PageLayout;
createPageLayoutTab: PageLayoutTab;
createPageLayoutWidget: PageLayoutWidget;
createPublicDomain: PublicDomain;
createSAMLIdentityProvider: SetupSsoOutput;
createWebhook: Webhook;
createWorkflowVersionEdge: WorkflowVersionStepChanges;
@@ -1417,6 +1418,7 @@ export type Mutation = {
deletePageLayout: PageLayout;
deletePageLayoutTab: Scalars['Boolean'];
deletePageLayoutWidget: PageLayoutWidget;
deletePublicDomain: Scalars['Boolean'];
deleteSSOIdentityProvider: DeleteSsoOutput;
deleteTwoFactorAuthenticationMethod: DeleteTwoFactorAuthenticationMethodOutput;
deleteUser: User;
@@ -1670,6 +1672,11 @@ export type MutationCreatePageLayoutWidgetArgs = {
};
export type MutationCreatePublicDomainArgs = {
domain: Scalars['String'];
};
export type MutationCreateSamlIdentityProviderArgs = {
input: SetupSamlSsoInput;
};
@@ -1780,6 +1787,11 @@ export type MutationDeletePageLayoutWidgetArgs = {
};
export type MutationDeletePublicDomainArgs = {
domain: Scalars['String'];
};
export type MutationDeleteSsoIdentityProviderArgs = {
input: DeleteSsoInput;
};
@@ -2504,6 +2516,14 @@ export type PostgresCredentials = {
workspaceId: Scalars['UUID'];
};
export type PublicDomain = {
__typename?: 'PublicDomain';
createdAt: Scalars['DateTime'];
domain: Scalars['String'];
id: Scalars['UUID'];
isValidated: Scalars['Boolean'];
};
export type PublicFeatureFlag = {
__typename?: 'PublicFeatureFlag';
key: FeatureFlagKey;
@@ -7,8 +7,8 @@ import styled from '@emotion/styled';
import { Button } from 'twenty-ui/input';
import { useDebouncedCallback } from 'use-debounce';
import {
type CustomDomainRecord,
type CustomDomainValidRecords,
type DomainRecord,
type DomainValidRecords,
} from '~/generated/graphql';
import { useCopyToClipboard } from '~/hooks/useCopyToClipboard';
import { capitalize } from 'twenty-shared/utils';
@@ -49,7 +49,7 @@ const StyledButton = styled(Button)`
export const SettingsCustomDomainRecords = ({
records,
}: {
records: CustomDomainValidRecords['records'];
records: DomainValidRecords['records'];
}) => {
const { customDomainRecords } = useRecoilValue(customDomainRecordsState);
const currentWorkspace = useRecoilValue(currentWorkspaceState);
@@ -67,7 +67,7 @@ export const SettingsCustomDomainRecords = ({
];
const defaultValues: { status: string; color: ThemeColor } =
currentWorkspace?.customDomain === customDomainRecords?.customDomain
currentWorkspace?.customDomain === customDomainRecords?.domain
? {
status: 'success',
color: 'green',
@@ -78,7 +78,7 @@ export const SettingsCustomDomainRecords = ({
};
const rows = rowsDefinitions.map<
{ name: string; status: string; color: ThemeColor } & CustomDomainRecord
{ name: string; status: string; color: ThemeColor } & DomainRecord
>((row) => {
const record = records.find(
({ validationType }) => validationType === row.validationType,
@@ -1,5 +1,5 @@
import { useCheckCustomDomainValidRecordsMutation } from '~/generated-metadata/graphql';
import { useSetRecoilState } from 'recoil';
import { useRecoilState } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { customDomainRecordsState } from '@/settings/domains/states/customDomainRecordsState';
@@ -9,9 +9,14 @@ export const useCheckCustomDomainValidRecords = () => {
useCheckCustomDomainValidRecordsMutation();
const { enqueueErrorSnackBar } = useSnackBar();
const setCustomDomainRecords = useSetRecoilState(customDomainRecordsState);
const [{ isLoading }, setCustomDomainRecords] = useRecoilState(
customDomainRecordsState,
);
const checkCustomDomainRecords = () => {
if (isLoading) {
return;
}
setCustomDomainRecords((currentState) => ({
...currentState,
isLoading: true,
@@ -1,8 +1,8 @@
import { type CustomDomainValidRecords } from '~/generated/graphql';
import { type DomainValidRecords } from '~/generated/graphql';
import { createState } from 'twenty-ui/utilities';
export const customDomainRecordsState = createState<{
customDomainRecords: CustomDomainValidRecords | null;
customDomainRecords: DomainValidRecords | null;
isLoading: boolean;
}>({
key: 'customDomainRecordsState',
@@ -4,7 +4,7 @@ export const CHECK_CUSTOM_DOMAIN_VALID_RECORDS = gql`
mutation CheckCustomDomainValidRecords {
checkCustomDomainValidRecords {
id
customDomain
domain
records {
type
key
@@ -5,8 +5,6 @@ import {
import { useRedirectToWorkspaceDomain } from '@/domain-manager/hooks/useRedirectToWorkspaceDomain';
import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons';
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
import { SettingsCustomDomain } from '@/settings/domains/components/SettingsCustomDomain';
import { SettingsSubdomain } from '@/settings/domains/components/SettingsSubdomain';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal';
import { useModal } from '@/ui/layout/modal/hooks/useModal';
@@ -21,6 +19,9 @@ import { getSettingsPath, isDefined } from 'twenty-shared/utils';
import { z } from 'zod';
import { useUpdateWorkspaceMutation } from '~/generated-metadata/graphql';
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
import { SettingsCustomDomain } from '@/settings/domains/components/SettingsCustomDomain';
import { SettingsSubdomain } from '@/settings/domains/components/SettingsSubdomain';
import { useState } from 'react';
export const SUBDOMAIN_CHANGE_CONFIRMATION_MODAL_ID =
'subdomain-change-confirmation-modal';
@@ -61,6 +62,7 @@ export const SettingsDomain = () => {
const { enqueueSuccessSnackBar, enqueueErrorSnackBar } = useSnackBar();
const [updateWorkspace] = useUpdateWorkspaceMutation();
const { redirectToWorkspaceDomain } = useRedirectToWorkspaceDomain();
const [isSubmitting, setIsSubmitting] = useState(false);
const [currentWorkspace, setCurrentWorkspace] = useRecoilState(
currentWorkspaceState,
@@ -88,6 +90,10 @@ export const SettingsDomain = () => {
customDomain: string | null,
currentWorkspace: CurrentWorkspace,
) => {
if (isSubmitting) {
return;
}
setIsSubmitting(true);
updateWorkspace({
variables: {
input: {
@@ -106,6 +112,7 @@ export const SettingsDomain = () => {
enqueueSuccessSnackBar({
message: t`Custom domain updated`,
});
setIsSubmitting(false);
},
onError: (error: ApolloError) => {
if (
@@ -120,6 +127,7 @@ export const SettingsDomain = () => {
enqueueErrorSnackBar({
apolloError: error,
});
setIsSubmitting(false);
},
});
};
@@ -128,6 +136,10 @@ export const SettingsDomain = () => {
subdomain: string,
currentWorkspace: CurrentWorkspace,
) => {
if (isSubmitting) {
return;
}
setIsSubmitting(true);
updateWorkspace({
variables: {
input: {
@@ -148,6 +160,7 @@ export const SettingsDomain = () => {
enqueueErrorSnackBar({
apolloError: error,
});
setIsSubmitting(false);
},
onCompleted: async () => {
const currentUrl = new URL(window.location.href);
@@ -164,6 +177,7 @@ export const SettingsDomain = () => {
enqueueSuccessSnackBar({
message: t`Subdomain updated`,
});
setIsSubmitting(false);
await redirectToWorkspaceDomain(currentUrl.toString());
},
@@ -222,7 +236,7 @@ export const SettingsDomain = () => {
actionButton={
<SaveAndCancelButtons
onCancel={() => navigate(SettingsPath.Domains)}
isSaveDisabled={form.formState.isSubmitting}
isSaveDisabled={isSubmitting}
/>
}
>