fix(domains): show custom domain DNS records and activation status without a page reload (#22037)
## Problem
Setting up a custom domain had two confusing UX issues, both caused by
local state not being refreshed after the relevant mutation:
1. **DNS records didn't appear after saving.** After hitting save you
got the green "Custom domain updated" snackbar, but the "Domain Setup"
section (the Cloudflare/DNS records to configure) stayed empty. You had
to leave the page and come back for the records to show up.
2. **The "Custom Domain" card stayed "Inactive"** even after the DNS
records validated as "Success". Only a full page reload flipped it to
"Active".
## Root cause
**Issue 1 — stale closure.** In `useSettingsCustomDomain.handleSave`,
the `updateWorkspace` `onCompleted` callback called
`setCurrentWorkspace({ ...currentWorkspace, customDomain })` and then
`checkCustomDomainRecords()`. But `checkCustomDomainRecords` guarded on
the closed-over `currentWorkspace.customDomain`, which was still `null`
at that render. The `setCurrentWorkspace` call doesn't synchronously
update that captured value, so the guard returned early and the records
were never fetched. Remounting the page (navigate away/back) ran the
on-mount effect with a fresh workspace, which is why the trip "fixed"
it.
**Issue 2 — `isCustomDomainEnabled` never refreshed locally.** The
Active/Inactive badge is driven by
`currentWorkspace.isCustomDomainEnabled`. The backend flips this flag
inside `checkCustomDomainValidRecords`
(`custom-domain-manager.service.ts`), but the mutation didn't return it,
so the local `currentWorkspaceState` stayed stale until a full reload
re-ran the bootstrap query. The green "Success" DNS rows read from a
different source (`record.status`), which is why the rows and the badge
disagreed.
## Changes
**Issue 1**
- `checkCustomDomainRecords` now accepts the domain explicitly
(defaulting to the workspace value), so the freshly-saved domain can be
passed straight from `handleSave` instead of relying on the stale
closure. No new `useEffect` introduced.
- Fixed the Reload button so it no longer passes its click event as the
domain argument.
**Issue 2**
- Added a nullable `isCustomDomainEnabled` field to the
`DomainValidRecords` GraphQL type, populated only by the custom-domain
check (the shared public-domain flow leaves it null, so it's backward
compatible).
- The frontend now writes that value back into `currentWorkspaceState`
when the check completes, using a **functional** Jotai update so a
concurrent `customDomain` update is never clobbered. The badge flips to
"Active" as soon as validation passes — on mount, on Reload, and right
after save.
I deliberately kept this targeted rather than introducing real-time
workspace sync: `isCustomDomainEnabled` only changes server-side during
the on-demand DNS check (mount/Reload/cron), so returning it from that
mutation is sufficient and far lower risk.
## Notes
- `packages/twenty-front/src/generated-metadata/graphql.ts` was updated
to match what `graphql:generate` produces for the new schema field
(codegen requires a running backend, which isn't available in this
environment). Worth re-running codegen in CI to confirm it's
byte-identical.
- No existing unit or integration tests reference these paths.
## Test plan
- [ ] Set a custom domain → DNS records appear immediately (no
navigation needed).
- [ ] Once DNS validates, the "Custom Domain" card flips to "Active"
without a reload.
- [ ] Reload button still refreshes records.
- [ ] Public domain validation flow is unaffected.
https://claude.ai/code/session_01BB6C6bpPZMUbMzKSCydEaj
---
_Generated by [Claude
Code](https://claude.ai/code/session_01BB6C6bpPZMUbMzKSCydEaj)_
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22037?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:
@@ -1469,6 +1469,7 @@ export type DomainValidRecords = {
|
||||
__typename?: 'DomainValidRecords';
|
||||
domain: Scalars['String']['output'];
|
||||
id: Scalars['UUID']['output'];
|
||||
isCustomDomainEnabled?: Maybe<Scalars['Boolean']['output']>;
|
||||
records: Array<DomainRecord>;
|
||||
};
|
||||
|
||||
@@ -8613,7 +8614,7 @@ export type UploadWorkspaceLogoMutation = { __typename?: 'Mutation', uploadWorks
|
||||
export type CheckCustomDomainValidRecordsMutationVariables = Exact<{ [key: string]: never; }>;
|
||||
|
||||
|
||||
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 CheckCustomDomainValidRecordsMutation = { __typename?: 'Mutation', checkCustomDomainValidRecords?: { __typename?: 'DomainValidRecords', id: string, domain: string, isCustomDomainEnabled?: boolean | null, records: Array<{ __typename?: 'DomainRecord', type: string, key: string, value: string, validationType: string, status: string }> } | null };
|
||||
|
||||
export type GetAiSystemPromptPreviewQueryVariables = Exact<{ [key: string]: never; }>;
|
||||
|
||||
@@ -8953,7 +8954,7 @@ export const ActivateWorkspaceDocument = {"kind":"Document","definitions":[{"kin
|
||||
export const DeleteCurrentWorkspaceDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteCurrentWorkspace"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"deleteCurrentWorkspace"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]} as unknown as DocumentNode<DeleteCurrentWorkspaceMutation, DeleteCurrentWorkspaceMutationVariables>;
|
||||
export const UpdateWorkspaceDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateWorkspace"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UpdateWorkspaceInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateWorkspace"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"data"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"customDomain"}},{"kind":"Field","name":{"kind":"Name","value":"subdomain"}},{"kind":"Field","name":{"kind":"Name","value":"displayName"}},{"kind":"Field","name":{"kind":"Name","value":"logo"}},{"kind":"Field","name":{"kind":"Name","value":"allowImpersonation"}},{"kind":"Field","name":{"kind":"Name","value":"isPublicInviteLinkEnabled"}},{"kind":"Field","name":{"kind":"Name","value":"isGoogleAuthEnabled"}},{"kind":"Field","name":{"kind":"Name","value":"isMicrosoftAuthEnabled"}},{"kind":"Field","name":{"kind":"Name","value":"isPasswordAuthEnabled"}},{"kind":"Field","name":{"kind":"Name","value":"isTwoFactorAuthenticationEnforced"}},{"kind":"Field","name":{"kind":"Name","value":"isInternalMessagesImportEnabled"}},{"kind":"Field","name":{"kind":"Name","value":"defaultRole"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"RoleFragment"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"RoleFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Role"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"label"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"icon"}},{"kind":"Field","name":{"kind":"Name","value":"canUpdateAllSettings"}},{"kind":"Field","name":{"kind":"Name","value":"canAccessAllTools"}},{"kind":"Field","name":{"kind":"Name","value":"isEditable"}},{"kind":"Field","name":{"kind":"Name","value":"canReadAllObjectRecords"}},{"kind":"Field","name":{"kind":"Name","value":"canUpdateAllObjectRecords"}},{"kind":"Field","name":{"kind":"Name","value":"canSoftDeleteAllObjectRecords"}},{"kind":"Field","name":{"kind":"Name","value":"canDestroyAllObjectRecords"}},{"kind":"Field","name":{"kind":"Name","value":"canBeAssignedToUsers"}},{"kind":"Field","name":{"kind":"Name","value":"canBeAssignedToAgents"}},{"kind":"Field","name":{"kind":"Name","value":"canBeAssignedToApiKeys"}}]}}]} as unknown as DocumentNode<UpdateWorkspaceMutation, UpdateWorkspaceMutationVariables>;
|
||||
export const UploadWorkspaceLogoDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UploadWorkspaceLogo"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"file"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Upload"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"uploadWorkspaceLogo"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"file"},"value":{"kind":"Variable","name":{"kind":"Name","value":"file"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"url"}}]}}]}}]} as unknown as DocumentNode<UploadWorkspaceLogoMutation, UploadWorkspaceLogoMutationVariables>;
|
||||
export const CheckCustomDomainValidRecordsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CheckCustomDomainValidRecords"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"checkCustomDomainValidRecords"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"domain"}},{"kind":"Field","name":{"kind":"Name","value":"records"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"value"}},{"kind":"Field","name":{"kind":"Name","value":"validationType"}},{"kind":"Field","name":{"kind":"Name","value":"status"}}]}}]}}]}}]} as unknown as DocumentNode<CheckCustomDomainValidRecordsMutation, CheckCustomDomainValidRecordsMutationVariables>;
|
||||
export const CheckCustomDomainValidRecordsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CheckCustomDomainValidRecords"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"checkCustomDomainValidRecords"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"domain"}},{"kind":"Field","name":{"kind":"Name","value":"isCustomDomainEnabled"}},{"kind":"Field","name":{"kind":"Name","value":"records"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"value"}},{"kind":"Field","name":{"kind":"Name","value":"validationType"}},{"kind":"Field","name":{"kind":"Name","value":"status"}}]}}]}}]}}]} as unknown as DocumentNode<CheckCustomDomainValidRecordsMutation, CheckCustomDomainValidRecordsMutationVariables>;
|
||||
export const GetAiSystemPromptPreviewDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetAiSystemPromptPreview"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"getAiSystemPromptPreview"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"sections"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"content"}},{"kind":"Field","name":{"kind":"Name","value":"estimatedTokenCount"}}]}},{"kind":"Field","name":{"kind":"Name","value":"estimatedTokenCount"}}]}}]}}]} as unknown as DocumentNode<GetAiSystemPromptPreviewQuery, GetAiSystemPromptPreviewQueryVariables>;
|
||||
export const GetPublicWorkspaceDataByIdDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetPublicWorkspaceDataById"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"getPublicWorkspaceDataById"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"displayName"}},{"kind":"Field","name":{"kind":"Name","value":"logo"}}]}}]}}]} as unknown as DocumentNode<GetPublicWorkspaceDataByIdQuery, GetPublicWorkspaceDataByIdQueryVariables>;
|
||||
export const GetWorkspaceFromInviteHashDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetWorkspaceFromInviteHash"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"inviteHash"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"findWorkspaceFromInviteHash"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"inviteHash"},"value":{"kind":"Variable","name":{"kind":"Name","value":"inviteHash"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"displayName"}},{"kind":"Field","name":{"kind":"Name","value":"logo"}},{"kind":"Field","name":{"kind":"Name","value":"allowImpersonation"}}]}}]}}]} as unknown as DocumentNode<GetWorkspaceFromInviteHashQuery, GetWorkspaceFromInviteHashQueryVariables>;
|
||||
+1
-1
@@ -110,7 +110,7 @@ export const SettingsCustomDomain = () => {
|
||||
Icon={IconReload}
|
||||
title={t`Reload`}
|
||||
variant="primary"
|
||||
onClick={checkCustomDomainRecords}
|
||||
onClick={() => checkCustomDomainRecords()}
|
||||
type="button"
|
||||
/>
|
||||
</StyledButtonContainer>
|
||||
|
||||
+24
-6
@@ -5,21 +5,24 @@ import { isDefined } from 'twenty-shared/utils';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { customDomainRecordsState } from '@/settings/domains/states/customDomainRecordsState';
|
||||
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
|
||||
export const useCheckCustomDomainValidRecords = () => {
|
||||
const [checkCustomDomainValidRecords] = useMutation(
|
||||
CheckCustomDomainValidRecordsDocument,
|
||||
);
|
||||
const { enqueueErrorSnackBar } = useSnackBar();
|
||||
const currentWorkspace = useAtomStateValue(currentWorkspaceState);
|
||||
const [currentWorkspace, setCurrentWorkspace] = useAtomState(
|
||||
currentWorkspaceState,
|
||||
);
|
||||
|
||||
const [{ isLoading }, setCustomDomainRecords] = useAtomState(
|
||||
customDomainRecordsState,
|
||||
);
|
||||
|
||||
const checkCustomDomainRecords = () => {
|
||||
if (isLoading || !currentWorkspace?.customDomain) {
|
||||
const checkCustomDomainRecords = (
|
||||
customDomain: string | null | undefined = currentWorkspace?.customDomain,
|
||||
) => {
|
||||
if (isLoading || !customDomain) {
|
||||
return;
|
||||
}
|
||||
setCustomDomainRecords((currentState) => ({
|
||||
@@ -28,13 +31,28 @@ export const useCheckCustomDomainValidRecords = () => {
|
||||
}));
|
||||
checkCustomDomainValidRecords({
|
||||
onCompleted: (data) => {
|
||||
const validRecords = data.checkCustomDomainValidRecords;
|
||||
|
||||
setCustomDomainRecords((currentState) => ({
|
||||
...currentState,
|
||||
isLoading: false,
|
||||
...(isDefined(data.checkCustomDomainValidRecords)
|
||||
? { customDomainRecords: data.checkCustomDomainValidRecords }
|
||||
...(isDefined(validRecords)
|
||||
? { customDomainRecords: validRecords }
|
||||
: {}),
|
||||
}));
|
||||
|
||||
const nextIsCustomDomainEnabled = validRecords?.isCustomDomainEnabled;
|
||||
|
||||
if (isDefined(nextIsCustomDomainEnabled)) {
|
||||
setCurrentWorkspace((previousWorkspace) =>
|
||||
isDefined(previousWorkspace)
|
||||
? {
|
||||
...previousWorkspace,
|
||||
isCustomDomainEnabled: nextIsCustomDomainEnabled,
|
||||
}
|
||||
: previousWorkspace,
|
||||
);
|
||||
}
|
||||
},
|
||||
onError: (error) => {
|
||||
enqueueErrorSnackBar({ apolloError: error });
|
||||
|
||||
@@ -65,7 +65,7 @@ export const useSettingsCustomDomain = () => {
|
||||
});
|
||||
enqueueSuccessSnackBar({ message: t`Custom domain updated` });
|
||||
setIsSubmitting(false);
|
||||
checkCustomDomainRecords();
|
||||
checkCustomDomainRecords(domainValue);
|
||||
},
|
||||
onError: (mutationError) => {
|
||||
if (
|
||||
|
||||
+1
@@ -5,6 +5,7 @@ export const CHECK_CUSTOM_DOMAIN_VALID_RECORDS = gql`
|
||||
checkCustomDomainValidRecords {
|
||||
id
|
||||
domain
|
||||
isCustomDomainEnabled
|
||||
records {
|
||||
type
|
||||
key
|
||||
|
||||
+3
@@ -30,4 +30,7 @@ export class DomainValidRecords {
|
||||
|
||||
@Field(() => [DomainRecord])
|
||||
records: Array<DomainRecord>;
|
||||
|
||||
@Field(() => Boolean, { nullable: true })
|
||||
isCustomDomainEnabled?: boolean;
|
||||
}
|
||||
|
||||
+4
-1
@@ -124,6 +124,9 @@ export class CustomDomainManagerService {
|
||||
);
|
||||
}
|
||||
|
||||
return customDomainWithRecords;
|
||||
return {
|
||||
...customDomainWithRecords,
|
||||
isCustomDomainEnabled: workspace.isCustomDomainEnabled,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user