[FRONT COMPONENTS] Retrieve the front components from the backend (#17813)

- Pass the auth token to worker
- Fetch the file from the rest API with the auth token
- Create the blob url
- Update the stories to pass a fake token which will be ignored

---------

Co-authored-by: Charles Bochet <charlesBochet@users.noreply.github.com>
This commit is contained in:
Raphaël Bosi
2026-02-09 19:10:29 +01:00
committed by GitHub
parent 37b9a55382
commit f51291704d
7 changed files with 47 additions and 54 deletions
@@ -1,4 +1,5 @@
import { getMockFrontComponentUrl } from '@/front-components/utils/mockFrontComponent';
import { REST_API_BASE_URL } from '@/apollo/constant/rest-api-base-url';
import { getTokenPair } from '@/apollo/utils/getTokenPair';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { useTheme } from '@emotion/react';
import { t } from '@lingui/core/macro';
@@ -13,7 +14,7 @@ type FrontComponentRendererProps = {
};
export const FrontComponentRenderer = ({
frontComponentId: _frontComponentId,
frontComponentId,
}: FrontComponentRendererProps) => {
const theme = useTheme();
const [hasError, setHasError] = useState(false);
@@ -22,6 +23,9 @@ export const FrontComponentRenderer = ({
const { executionContext, frontComponentHostCommunicationApi } =
useFrontComponentExecutionContext();
const componentUrl = `${REST_API_BASE_URL}/front-components/${frontComponentId}`;
const authToken = getTokenPair()?.accessOrWorkspaceAgnosticToken?.token;
const handleError = (error?: Error) => {
if (isDefined(error)) {
const errorMessage = error.message;
@@ -33,14 +37,16 @@ export const FrontComponentRenderer = ({
setHasError(true);
};
if (hasError) {
if (hasError || !isDefined(authToken)) {
// TODO: Add an error display component here
return null;
}
return (
<SharedFrontComponentRenderer
theme={theme}
componentUrl={getMockFrontComponentUrl()}
componentUrl={componentUrl}
authToken={authToken}
executionContext={executionContext}
frontComponentHostCommunicationApi={frontComponentHostCommunicationApi}
onError={handleError}
@@ -1,43 +0,0 @@
import { isDefined } from 'twenty-shared/utils';
const mockFrontComponentCode = `
var navigate = /* @__PURE__ */ (() => globalThis.TwentySdk.navigate)();
var AppPath = /* @__PURE__ */ (() => globalThis.TwentyShared["types"].AppPath)();
var Button = /* @__PURE__ */ (() => globalThis.RemoteComponents.TwentyUiButton)();
var jsx = /* @__PURE__ */ (() => globalThis.jsx)();
var TestComponent = () => {
return /* @__PURE__ */ jsx(
Button,
{
title: "Navigate to people index page",
onClick: () => navigate?.(AppPath.RecordIndexPage, {
objectNamePlural: "people"
})
}
);
};
var test_component_front_component_default = globalThis.jsx(TestComponent, {});
export {
test_component_front_component_default as default
};
`;
let cachedMockBlobUrl: string | null = null;
export const getMockFrontComponentUrl = (): string => {
if (isDefined(cachedMockBlobUrl)) {
return cachedMockBlobUrl;
}
const blob = new Blob([mockFrontComponentCode], {
type: 'application/javascript',
});
cachedMockBlobUrl = URL.createObjectURL(blob);
return cachedMockBlobUrl;
};
@@ -15,6 +15,7 @@ const meta: Meta<typeof FrontComponentRenderer> = {
},
args: {
onError: errorHandler,
authToken: 'fake-token',
},
beforeEach: () => {
errorHandler.mockClear();
@@ -19,6 +19,7 @@ import { componentRegistry } from '../generated/host-component-registry';
type FrontComponentContentProps = {
componentUrl: string;
authToken: string;
executionContext: FrontComponentExecutionContext;
frontComponentHostCommunicationApi: FrontComponentHostCommunicationApi;
onError: (error?: Error) => void;
@@ -27,6 +28,7 @@ type FrontComponentContentProps = {
export const FrontComponentRenderer = ({
componentUrl,
authToken,
executionContext,
frontComponentHostCommunicationApi,
onError,
@@ -43,6 +45,7 @@ export const FrontComponentRenderer = ({
return (
<FrontComponentWorkerEffect
componentUrl={componentUrl}
authToken={authToken}
frontComponentHostCommunicationApi={frontComponentHostCommunicationApi}
setReceiver={setReceiver}
setThread={setThread}
@@ -51,6 +54,7 @@ export const FrontComponentRenderer = ({
);
}, [
componentUrl,
authToken,
frontComponentHostCommunicationApi,
setError,
setReceiver,
@@ -7,6 +7,7 @@ import { createRemoteWorker } from '../worker/createRemoteWorker';
type FrontComponentWorkerEffectProps = {
componentUrl: string;
authToken: string;
frontComponentHostCommunicationApi: FrontComponentHostCommunicationApi;
setReceiver: React.Dispatch<React.SetStateAction<RemoteReceiver | null>>;
setThread: React.Dispatch<
@@ -20,6 +21,7 @@ type FrontComponentWorkerEffectProps = {
export const FrontComponentWorkerEffect = ({
componentUrl,
authToken,
frontComponentHostCommunicationApi,
setReceiver,
setThread,
@@ -56,7 +58,7 @@ export const FrontComponentWorkerEffect = ({
setThread(thread);
thread.imports
.render(newReceiver.connection, { componentUrl })
.render(newReceiver.connection, { componentUrl, authToken })
.catch((error: Error) => {
setError(error);
});
@@ -67,7 +69,7 @@ export const FrontComponentWorkerEffect = ({
setThread(null);
worker.terminate();
};
}, [componentUrl, setError, setReceiver, setThread]);
}, [componentUrl, authToken, setError, setReceiver, setThread]);
return null;
};
@@ -15,9 +15,9 @@ import { jsx, jsxs } from 'react/jsx-runtime';
import * as TwentySharedTypes from 'twenty-shared/types';
import * as TwentySharedUtils from 'twenty-shared/utils';
import * as TwentySdk from '@/sdk';
import { setFrontComponentExecutionContext } from '@/sdk/front-component-api/context/frontComponentContext';
import { setNavigate } from '@/sdk/front-component-api/functions/navigate';
import * as TwentySdk from '@/sdk';
import { type FrontComponentExecutionContext } from '../../types/FrontComponentExecutionContext';
import { type FrontComponentHostCommunicationApi } from '../../types/FrontComponentHostCommunicationApi';
@@ -47,11 +47,33 @@ const render: WorkerExports['render'] = async (
root.connect(batchedConnection);
document.body.append(root);
/* @vite-ignore */
const componentModule = await import(renderContext.componentUrl);
const response = await fetch(renderContext.componentUrl, {
headers: { Authorization: `Bearer ${renderContext.authToken}` },
});
const reactRoot = createRoot(root);
reactRoot.render(componentModule.default);
if (!response.ok) {
throw new Error(
`Failed to fetch front component from ${renderContext.componentUrl}: ${response.status} ${response.statusText}`,
);
}
const responseText = await response.text();
const blob = new Blob([responseText], {
type: 'application/javascript',
});
const importUrl = URL.createObjectURL(blob);
try {
/* @vite-ignore */
const componentModule = await import(importUrl);
const reactRoot = createRoot(root);
reactRoot.render(componentModule.default);
} finally {
URL.revokeObjectURL(importUrl);
}
};
const initializeHostCommunicationApi: WorkerExports['initializeHostCommunicationApi'] =
@@ -1,3 +1,4 @@
export type HostToWorkerRenderContext = {
componentUrl: string;
authToken: string;
};