fix: validation link for access-domains deeplinks to Invite tab (#22845)

The "validate domain" link sent in the email when adding an Access
Domain wasn't working because the link didn't deep link to the Invite
tab

URLs are now built to include the that hash property to deep link to the
target tab.

Before:
```
https://example.com/settings/members?wtdId=<id>&validationToken=<token>
```

After:
```
https://example.com/settings/members?wtdId=<id>&validationToken=<token>#invite
```

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22845?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:
Brahm Lower
2026-07-13 07:55:38 -07:00
committed by GitHub
parent ca437d374f
commit 2b0b62235e
5 changed files with 43 additions and 0 deletions
@@ -86,6 +86,7 @@ export class ApprovedAccessDomainService {
workspaceId: workspace.id,
}),
},
hash: 'invite',
});
if (!isDefined(sender.userEmail)) {
@@ -344,6 +344,7 @@ describe('ApprovedAccessDomainService', () => {
wtdId: approvedAccessDomain.id,
validationToken: 'signed.jwt.token',
},
hash: 'invite',
});
expect(emailService.send).toHaveBeenCalledWith({
@@ -4,12 +4,14 @@ type BuildUrlWithPathnameAndSearchParamsProps = {
baseUrl: URL;
pathname?: string;
searchParams?: Record<string, string | number | boolean>;
hash?: string;
};
export const buildUrlWithPathnameAndSearchParams = ({
baseUrl,
pathname,
searchParams,
hash,
}: BuildUrlWithPathnameAndSearchParamsProps) => {
const url = baseUrl;
@@ -21,5 +23,9 @@ export const buildUrlWithPathnameAndSearchParams = ({
appendSearchParamsToUrl(url, searchParams);
}
if (hash) {
url.hash = hash;
}
return url;
};
@@ -183,6 +183,38 @@ describe('WorkspaceDomainsService', () => {
expect(result.searchParams.get('foo')).toBe('bar');
expect(result.searchParams.get('baz')).toBe('123');
});
it('should set the hash if provided', () => {
jest
.spyOn(twentyConfigService, 'get')
.mockImplementation((key: string) => {
const env = {
FRONTEND_URL: 'https://example.com',
};
// @ts-expect-error legacy noImplicitAny
return env[key];
});
const result = workspaceDomainsService.buildWorkspaceURL({
workspace: {
subdomain: 'test',
customDomain: null,
isCustomDomainEnabled: false,
},
pathname: '/settings/members',
searchParams: {
wtdId: 'domain-id',
},
hash: 'invite',
});
expect(result.hash).toBe('#invite');
expect(result.searchParams.get('wtdId')).toBe('domain-id');
expect(result.toString()).toBe(
'https://example.com/settings/members?wtdId=domain-id#invite',
);
});
});
describe('getWorkspaceByOriginOrDefaultWorkspace', () => {
@@ -31,10 +31,12 @@ export class WorkspaceDomainsService {
workspace,
pathname,
searchParams,
hash,
}: {
workspace: WorkspaceDomainConfig;
pathname?: string;
searchParams?: Record<string, string | number | boolean>;
hash?: string;
}) {
const workspaceUrls = this.getWorkspaceUrls(workspace);
@@ -42,6 +44,7 @@ export class WorkspaceDomainsService {
baseUrl: new URL(workspaceUrls.customUrl ?? workspaceUrls.subdomainUrl),
pathname,
searchParams,
hash,
});
return url;