fix(sdk): link dev UI to workspace application detail page (#20849)

sdk handle auth of one workspace per session -- but server could be
configured as multi or single -- hence for multi get subdomain -- and
for single the localhost fallback!
also: link includes applicationId so it opens the app detail page
directly (not the list)

## QA
multi workspace flag on -

<img width="2996" height="1712" alt="CleanShot 2026-05-22 at 18 21
31@2x"
src="https://github.com/user-attachments/assets/8499b9f3-b22e-45e2-8b97-4b27fadc3c94"
/>

multi workspace flag off - 

<img width="3012" height="1734" alt="CleanShot 2026-05-22 at 18 14
37@2x"
src="https://github.com/user-attachments/assets/3af2f492-5e2d-4a4b-8251-c3343d79ae9e"
/>
This commit is contained in:
nitin
2026-05-22 19:49:27 +05:30
committed by GitHub
parent 4554dbe3c9
commit 59d69e2f5c
8 changed files with 182 additions and 11 deletions
@@ -1,6 +1,8 @@
import { ConfigService } from '@/cli/utilities/config/config-service';
import { isNonEmptyString } from '@sniptt/guards';
import axios, { type AxiosInstance } from 'axios';
import chalk from 'chalk';
import { isDefined } from 'twenty-shared/utils';
export class ApiClient {
readonly client: AxiosInstance;
@@ -70,6 +72,74 @@ export class ApiClient {
);
}
async getFrontendUrl(): Promise<string | null> {
try {
const response = await this.client.get(
'/.well-known/oauth-authorization-server',
{ headers: { Accept: 'application/json' } },
);
const authorizationEndpoint = response.data?.authorization_endpoint;
if (!isNonEmptyString(authorizationEndpoint)) {
return null;
}
return new URL(authorizationEndpoint).origin;
} catch {
return null;
}
}
async getWorkspaceFrontendUrl(): Promise<string | null> {
const workspaceFrontendUrl = await this.getCurrentWorkspaceFrontendUrl();
if (isDefined(workspaceFrontendUrl)) {
return workspaceFrontendUrl;
}
return this.getFrontendUrl();
}
private async getCurrentWorkspaceFrontendUrl(): Promise<string | null> {
try {
const query = `
query CurrentWorkspaceForFrontendUrl {
currentWorkspace {
workspaceUrls {
subdomainUrl
customUrl
}
}
}
`;
const response = await this.client.post(
'/metadata',
{ query },
{
headers: {
'Content-Type': 'application/json',
Accept: '*/*',
},
},
);
const workspaceUrls =
response.data?.data?.currentWorkspace?.workspaceUrls;
const workspaceFrontendUrl = isNonEmptyString(workspaceUrls?.customUrl)
? workspaceUrls.customUrl
: workspaceUrls?.subdomainUrl;
if (!isNonEmptyString(workspaceFrontendUrl)) {
return null;
}
return new URL(workspaceFrontendUrl).origin;
} catch {
return null;
}
}
async validateAuth(): Promise<{ authValid: boolean; serverUp: boolean }> {
try {
const query = `