7da8450075
## Description - Add `isHeadless` field to `FrontComponent` entity so front components can run without rendering UI in the command menu - Introduce headless front component mounting logic: `HeadlessFrontComponentMountRoot` at the application root, `useMountHeadlessFrontComponent`, and `useUnmountHeadlessFrontComponent` hooks to mount/unmount headless components - Expand the SDK with new action components (`Action`, `ActionLink`, `ActionOpenSidePanelPage`) and host communication functions (`openSidePanelPage`, `unmountFrontComponent`) - Move `CommandMenuPages` type to twenty-shared so the SDK can reference it for side panel navigation ## Video QA https://github.com/user-attachments/assets/4f9e3bb1-fcd1-42be-b3f4-a97e80c2add2
110 lines
3.7 KiB
TypeScript
110 lines
3.7 KiB
TypeScript
import '@remote-dom/core/polyfill';
|
|
import '@remote-dom/react/polyfill';
|
|
|
|
import '../generated/remote-elements';
|
|
|
|
import { ThreadWebWorker } from '@quilted/threads';
|
|
import {
|
|
BatchingRemoteConnection,
|
|
type RemoteConnection,
|
|
type RemoteRootElement,
|
|
} from '@remote-dom/core/elements';
|
|
|
|
import { isDefined } from 'twenty-shared/utils';
|
|
|
|
import { installStyleBridge } from '@/front-component-renderer/polyfills/installStyleBridge';
|
|
import { installStylePropertyOnRemoteElements } from '@/front-component-renderer/remote/utils/installStylePropertyOnRemoteElements';
|
|
import { patchRemoteElementSetAttribute } from '@/front-component-renderer/remote/utils/patchRemoteElementSetAttribute';
|
|
import { HTML_TAG_TO_CUSTOM_ELEMENT_TAG } from '@/sdk/front-component-api/constants/HtmlTagToRemoteComponent';
|
|
import { setFrontComponentExecutionContext } from '@/sdk/front-component-api/context/frontComponentContext';
|
|
|
|
import { type FrontComponentExecutionContext } from '../../types/FrontComponentExecutionContext';
|
|
import { type FrontComponentHostCommunicationApi } from '../../types/FrontComponentHostCommunicationApi';
|
|
import { type HostToWorkerRenderContext } from '../../types/HostToWorkerRenderContext';
|
|
import { type WorkerExports } from '../../types/WorkerExports';
|
|
import { exposeGlobals } from '../utils/exposeGlobals';
|
|
import { setWorkerEnv } from './utils/setWorkerEnv';
|
|
|
|
installStylePropertyOnRemoteElements();
|
|
patchRemoteElementSetAttribute();
|
|
|
|
exposeGlobals({
|
|
__HTML_TAG_TO_CUSTOM_ELEMENT_TAG__: HTML_TAG_TO_CUSTOM_ELEMENT_TAG,
|
|
});
|
|
|
|
const render: WorkerExports['render'] = async (
|
|
connection: RemoteConnection,
|
|
renderContext: HostToWorkerRenderContext,
|
|
) => {
|
|
const batchedConnection = new BatchingRemoteConnection(connection);
|
|
const root = document.createElement('remote-root') as RemoteRootElement;
|
|
const renderContainer = document.createElement('remote-fragment');
|
|
root.connect(batchedConnection);
|
|
root.append(renderContainer);
|
|
document.body.append(root);
|
|
installStyleBridge(root);
|
|
|
|
if (
|
|
isDefined(renderContext.applicationAccessToken) &&
|
|
isDefined(renderContext.apiUrl)
|
|
) {
|
|
setWorkerEnv({
|
|
TWENTY_APP_ACCESS_TOKEN: renderContext.applicationAccessToken,
|
|
TWENTY_API_URL: renderContext.apiUrl,
|
|
});
|
|
}
|
|
|
|
const response = await fetch(renderContext.componentUrl, {
|
|
headers: isDefined(renderContext.applicationAccessToken)
|
|
? { Authorization: `Bearer ${renderContext.applicationAccessToken}` }
|
|
: undefined,
|
|
});
|
|
|
|
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);
|
|
|
|
componentModule.default(renderContainer);
|
|
} finally {
|
|
URL.revokeObjectURL(importUrl);
|
|
}
|
|
};
|
|
|
|
const initializeHostCommunicationApi: WorkerExports['initializeHostCommunicationApi'] =
|
|
async () => {
|
|
const hostApi =
|
|
ThreadWebWorker.self.import<FrontComponentHostCommunicationApi>();
|
|
|
|
frontComponentHostCommunicationApi.navigate = hostApi.navigate;
|
|
frontComponentHostCommunicationApi.openSidePanelPage =
|
|
hostApi.openSidePanelPage;
|
|
frontComponentHostCommunicationApi.unmountFrontComponent =
|
|
hostApi.unmountFrontComponent;
|
|
};
|
|
|
|
const updateContext: WorkerExports['updateContext'] = async (
|
|
context: FrontComponentExecutionContext,
|
|
) => {
|
|
setFrontComponentExecutionContext(context);
|
|
};
|
|
|
|
ThreadWebWorker.self.export({
|
|
render,
|
|
initializeHostCommunicationApi,
|
|
updateContext,
|
|
});
|