nitin 2026-03-05 15:43:46 +05:30 committed by GitHub
parent 0e89c96170
commit 4cfd738312
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 429 additions and 15 deletions

View file

@ -48,6 +48,7 @@
"search.exclude": {
"**/.yarn": true
},
"eslint.useFlatConfig": true,
"eslint.debug": true,
"files.associations": {
".cursorrules": "markdown"

View file

@ -0,0 +1,67 @@
import { ACTION_MENU_CONFIRMATION_MODAL_RESULT_BROWSER_EVENT_NAME } from '@/action-menu/confirmation-modal/constants/ActionMenuConfirmationModalResultBrowserEventName';
import { ACTION_MENU_CONFIRMATION_MODAL_INSTANCE_ID } from '@/action-menu/confirmation-modal/constants/ActionMenuConfirmationModalId';
import { actionMenuConfirmationModalConfigState } from '@/action-menu/confirmation-modal/states/actionMenuConfirmationModalState';
import {
type ActionMenuConfirmationModalResult,
type ActionMenuConfirmationModalResultBrowserEventDetail,
} from '@/action-menu/confirmation-modal/types/ActionMenuConfirmationModalResultBrowserEventDetail';
import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal';
import { isModalOpenedComponentState } from '@/ui/layout/modal/states/isModalOpenedComponentState';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
export const ActionMenuConfirmationModalManager = () => {
const actionMenuConfirmationModalConfig = useAtomStateValue(
actionMenuConfirmationModalConfigState,
);
const isModalOpened = useAtomComponentStateValue(
isModalOpenedComponentState,
ACTION_MENU_CONFIRMATION_MODAL_INSTANCE_ID,
);
const setActionMenuConfirmationModalConfig = useSetAtomState(
actionMenuConfirmationModalConfigState,
);
const callerId = actionMenuConfirmationModalConfig?.frontComponentId;
const emitConfirmationResult = (
confirmationResult: ActionMenuConfirmationModalResult,
) => {
if (!callerId) {
return;
}
window.dispatchEvent(
new CustomEvent<ActionMenuConfirmationModalResultBrowserEventDetail>(
ACTION_MENU_CONFIRMATION_MODAL_RESULT_BROWSER_EVENT_NAME,
{
detail: {
frontComponentId: callerId,
confirmationResult,
},
},
),
);
setActionMenuConfirmationModalConfig(null);
};
if (!actionMenuConfirmationModalConfig || !isModalOpened) {
return null;
}
return (
<ConfirmationModal
modalInstanceId={ACTION_MENU_CONFIRMATION_MODAL_INSTANCE_ID}
title={actionMenuConfirmationModalConfig.title}
subtitle={actionMenuConfirmationModalConfig.subtitle}
onConfirmClick={() => emitConfirmationResult('confirm')}
onClose={() => emitConfirmationResult('cancel')}
confirmButtonText={actionMenuConfirmationModalConfig.confirmButtonText}
confirmButtonAccent={
actionMenuConfirmationModalConfig.confirmButtonAccent
}
/>
);
};

View file

@ -0,0 +1,2 @@
export const ACTION_MENU_CONFIRMATION_MODAL_INSTANCE_ID =
'action-menu-confirmation-modal';

View file

@ -0,0 +1,2 @@
export const ACTION_MENU_CONFIRMATION_MODAL_RESULT_BROWSER_EVENT_NAME =
'action-menu-confirmation-modal-result';

View file

@ -0,0 +1,48 @@
import { useStore } from 'jotai';
import { useCallback } from 'react';
import { ACTION_MENU_CONFIRMATION_MODAL_INSTANCE_ID } from '@/action-menu/confirmation-modal/constants/ActionMenuConfirmationModalId';
import {
type ActionMenuConfirmationModalConfig,
actionMenuConfirmationModalConfigState,
} from '@/action-menu/confirmation-modal/states/actionMenuConfirmationModalState';
import { useModal } from '@/ui/layout/modal/hooks/useModal';
import { isModalOpenedComponentState } from '@/ui/layout/modal/states/isModalOpenedComponentState';
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
export const useActionMenuConfirmationModal = () => {
const store = useStore();
const setActionMenuConfirmationModalConfig = useSetAtomState(
actionMenuConfirmationModalConfigState,
);
const { openModal } = useModal();
const openConfirmationModal = useCallback(
(config: ActionMenuConfirmationModalConfig) => {
const existingActionMenuConfirmationModalConfig = store.get(
actionMenuConfirmationModalConfigState.atom,
);
const isActionMenuConfirmationModalOpened = store.get(
isModalOpenedComponentState.atomFamily({
instanceId: ACTION_MENU_CONFIRMATION_MODAL_INSTANCE_ID,
}),
);
if (
existingActionMenuConfirmationModalConfig !== null ||
isActionMenuConfirmationModalOpened
) {
throw new Error(
'Action menu confirmation modal is already active for another front component',
);
}
setActionMenuConfirmationModalConfig(config);
openModal(ACTION_MENU_CONFIRMATION_MODAL_INSTANCE_ID);
},
[store, setActionMenuConfirmationModalConfig, openModal],
);
return { openConfirmationModal };
};

View file

@ -0,0 +1,18 @@
import { type ReactNode } from 'react';
import { createAtomState } from '@/ui/utilities/state/jotai/utils/createAtomState';
import { type ButtonAccent } from 'twenty-ui/input';
export type ActionMenuConfirmationModalConfig = {
frontComponentId: string;
title: string;
subtitle: ReactNode;
confirmButtonText?: string;
confirmButtonAccent?: ButtonAccent;
};
export const actionMenuConfirmationModalConfigState =
createAtomState<ActionMenuConfirmationModalConfig | null>({
key: 'actionMenuConfirmationModalConfigState',
defaultValue: null,
});

View file

@ -0,0 +1,6 @@
export type ActionMenuConfirmationModalResult = 'confirm' | 'cancel';
export type ActionMenuConfirmationModalResultBrowserEventDetail = {
frontComponentId: string;
confirmationResult: ActionMenuConfirmationModalResult;
};

View file

@ -1,4 +1,5 @@
import { AgentChatProvider } from '@/ai/components/AgentChatProvider';
import { ActionMenuConfirmationModalManager } from '@/action-menu/confirmation-modal/components/ActionMenuConfirmationModalManager';
import { ApolloProvider } from '@/apollo/components/ApolloProvider';
import { MetadataGater } from '@/metadata-store/components/MetadataGater';
import { IsAppMetadataReadyEffect } from '@/metadata-store/effect-components/IsAppMetadataReadyEffect';
@ -71,6 +72,7 @@ export const AppRouterProviders = () => {
<PageFavicon />
<Outlet />
<GlobalFilePreviewModal />
<ActionMenuConfirmationModalManager />
<HeadlessFrontComponentMountRoot />
</StrictMode>
</DialogManager>

View file

@ -5,6 +5,7 @@ import {
} from 'twenty-sdk/front-component-renderer';
import { type AppPath, type EnqueueSnackbarParams } from 'twenty-shared/types';
import { useActionMenuConfirmationModal } from '@/action-menu/confirmation-modal/hooks/useActionMenuConfirmationModal';
import { currentUserState } from '@/auth/states/currentUserState';
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
import { useNavigateCommandMenu } from '@/command-menu/hooks/useNavigateCommandMenu';
@ -31,6 +32,7 @@ export const useFrontComponentExecutionContext = ({
const { requestAccessTokenRefresh } = useRequestApplicationTokenRefresh({
frontComponentId,
});
const { openConfirmationModal } = useActionMenuConfirmationModal();
const { navigateCommandMenu } = useNavigateCommandMenu();
const setCommandMenuSearch = useSetAtomState(commandMenuSearchState);
const { getIcon } = useIcons();
@ -70,6 +72,17 @@ export const useFrontComponentExecutionContext = ({
}
};
const openActionConfirmationModal: FrontComponentHostCommunicationApi['openActionConfirmationModal'] =
async ({ title, subtitle, confirmButtonText, confirmButtonAccent }) => {
openConfirmationModal({
frontComponentId,
title,
subtitle,
confirmButtonText,
confirmButtonAccent,
});
};
const enqueueSnackbar: FrontComponentHostCommunicationApi['enqueueSnackbar'] =
async ({
message,
@ -125,6 +138,7 @@ export const useFrontComponentExecutionContext = ({
navigate,
requestAccessTokenRefresh,
openSidePanelPage,
openActionConfirmationModal,
enqueueSnackbar,
unmountFrontComponent,
closeSidePanel,

View file

@ -16,6 +16,7 @@ const meta: Meta<typeof FrontComponentRenderer> = {
args: {
onError: errorHandler,
applicationAccessToken: 'fake-token',
executionContext: { frontComponentId: 'storybook-test', userId: null },
},
beforeEach: () => {
errorHandler.mockClear();
@ -117,16 +118,15 @@ export const SdkContext: Story = {
...createComponentStory('sdk-context-example'),
args: {
...createComponentStory('sdk-context-example').args,
executionContext: { userId: 'test-user-abc-123' },
executionContext: {
frontComponentId: 'sdk-context-test',
userId: 'test-user-abc-123',
},
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await canvas.findByTestId(
'sdk-context-component',
{},
{ timeout: 30000 },
);
await canvas.findByTestId('sdk-context-component', {}, { timeout: 30000 });
const userIdElement = await canvas.findByTestId('sdk-context-user-id');
expect(userIdElement).toBeVisible();
@ -138,9 +138,7 @@ export const SdkContext: Story = {
const button = await canvas.findByTestId('sdk-context-button');
await userEvent.click(button);
const renderCount = await canvas.findByTestId(
'sdk-context-render-count',
);
const renderCount = await canvas.findByTestId('sdk-context-render-count');
expect(renderCount).toHaveTextContent('Renders: 1');
expect(userIdElement).toHaveTextContent('test-user-abc-123');

View file

@ -16,6 +16,7 @@ const meta: Meta<typeof FrontComponentRenderer> = {
args: {
onError: errorHandler,
applicationAccessToken: 'fake-token',
executionContext: { frontComponentId: 'storybook-test', userId: null },
},
beforeEach: () => {
errorHandler.mockClear();
@ -138,7 +139,7 @@ const twentyUiTest: Story['play'] = async ({ canvasElement }) => {
export const TwentyUiReact: Story = createComponentStory('twenty-ui-example', {
play: twentyUiTest,
});
export const TwentyUiPreact: Story = createComponentStory(
'twenty-ui-example',
{ runtime: 'preact', play: twentyUiTest },
);
export const TwentyUiPreact: Story = createComponentStory('twenty-ui-example', {
runtime: 'preact',
play: twentyUiTest,
});

View file

@ -50,6 +50,7 @@ export const FrontComponentRenderer = ({
componentUrl={componentUrl}
applicationAccessToken={applicationAccessToken}
apiUrl={apiUrl}
frontComponentId={executionContext.frontComponentId}
frontComponentHostCommunicationApi={frontComponentHostCommunicationApi}
setReceiver={setReceiver}
setThread={setThread}
@ -64,6 +65,7 @@ export const FrontComponentRenderer = ({
setThread,
applicationAccessToken,
apiUrl,
executionContext.frontComponentId,
]);
return (

View file

@ -1,14 +1,25 @@
import { ThreadWebWorker, release, retain } from '@quilted/threads';
import { RemoteReceiver } from '@remote-dom/core/receivers';
import { useEffect, useRef } from 'react';
import { type ActionConfirmationModalResult } from '../../../sdk/front-component-api/globals/frontComponentHostCommunicationApi';
import { type FrontComponentHostCommunicationApi } from '../../types/FrontComponentHostCommunicationApi';
import { type WorkerExports } from '../../types/WorkerExports';
import { createRemoteWorker } from '../worker/utils/createRemoteWorker';
// Must match ACTION_MENU_CONFIRMATION_MODAL_RESULT_BROWSER_EVENT_NAME in twenty-front
const ACTION_MENU_CONFIRMATION_MODAL_RESULT_BROWSER_EVENT_NAME =
'action-menu-confirmation-modal-result';
type ActionMenuConfirmationModalResultBrowserEventDetail = {
frontComponentId: string;
confirmationResult: ActionConfirmationModalResult;
};
type FrontComponentWorkerEffectProps = {
componentUrl: string;
applicationAccessToken?: string;
apiUrl?: string;
frontComponentId: string;
frontComponentHostCommunicationApi: FrontComponentHostCommunicationApi;
setReceiver: React.Dispatch<React.SetStateAction<RemoteReceiver | null>>;
setThread: React.Dispatch<
@ -24,6 +35,7 @@ export const FrontComponentWorkerEffect = ({
componentUrl,
applicationAccessToken,
apiUrl,
frontComponentId,
frontComponentHostCommunicationApi,
setReceiver,
setThread,
@ -55,6 +67,32 @@ export const FrontComponentWorkerEffect = ({
exports: frontComponentHostCommunicationApi,
});
const handleActionMenuConfirmationModalResultBrowserEvent = (
event: CustomEvent<ActionMenuConfirmationModalResultBrowserEventDetail>,
) => {
const actionMenuConfirmationModalResultBrowserEventDetail = event.detail;
if (
actionMenuConfirmationModalResultBrowserEventDetail.frontComponentId !==
frontComponentId
) {
return;
}
thread.imports
.onConfirmationModalResult(
actionMenuConfirmationModalResultBrowserEventDetail.confirmationResult,
)
.catch((error: Error) => {
setError(error);
});
};
window.addEventListener(
ACTION_MENU_CONFIRMATION_MODAL_RESULT_BROWSER_EVENT_NAME,
handleActionMenuConfirmationModalResultBrowserEvent as EventListener,
);
setThread(thread);
thread.imports
@ -71,6 +109,10 @@ export const FrontComponentWorkerEffect = ({
isInitializedRef.current = true;
return () => {
window.removeEventListener(
ACTION_MENU_CONFIRMATION_MODAL_RESULT_BROWSER_EVENT_NAME,
handleActionMenuConfirmationModalResultBrowserEvent as EventListener,
);
setThread(null);
worker.terminate();
isInitializedRef.current = false;
@ -79,6 +121,7 @@ export const FrontComponentWorkerEffect = ({
componentUrl,
applicationAccessToken,
apiUrl,
frontComponentId,
setError,
setReceiver,
setThread,

View file

@ -24,6 +24,10 @@ import { type FrontComponentHostCommunicationApi } from '../../types/FrontCompon
import { type HostToWorkerRenderContext } from '../../types/HostToWorkerRenderContext';
import { type WorkerExports } from '../../types/WorkerExports';
import { exposeGlobals } from '../utils/exposeGlobals';
import {
createOpenActionConfirmationModalAdapter,
handleActionConfirmationModalResult,
} from './utils/createActionConfirmationModalBridge';
import { setWorkerEnv } from './utils/setWorkerEnv';
installStylePropertyOnRemoteElements();
@ -97,6 +101,8 @@ const initializeHostCommunicationApi: WorkerExports['initializeHostCommunication
hostApi.requestAccessTokenRefresh;
frontComponentHostCommunicationApi.openSidePanelPage =
hostApi.openSidePanelPage;
frontComponentHostCommunicationApi.openActionConfirmationModal =
createOpenActionConfirmationModalAdapter(hostApi);
frontComponentHostCommunicationApi.unmountFrontComponent =
hostApi.unmountFrontComponent;
frontComponentHostCommunicationApi.enqueueSnackbar =
@ -104,6 +110,11 @@ const initializeHostCommunicationApi: WorkerExports['initializeHostCommunication
frontComponentHostCommunicationApi.closeSidePanel = hostApi.closeSidePanel;
};
const onConfirmationModalResult: WorkerExports['onConfirmationModalResult'] =
async (result) => {
await handleActionConfirmationModalResult(result);
};
const updateContext: WorkerExports['updateContext'] = async (
context: FrontComponentExecutionContext,
) => {
@ -113,5 +124,6 @@ const updateContext: WorkerExports['updateContext'] = async (
ThreadWebWorker.self.export({
render,
initializeHostCommunicationApi,
onConfirmationModalResult,
updateContext,
});

View file

@ -0,0 +1,65 @@
import {
type ActionConfirmationModalResult,
type OpenActionConfirmationModalFunction,
} from '@/sdk/front-component-api/globals/frontComponentHostCommunicationApi';
import { type FrontComponentHostCommunicationApi } from '../../../types/FrontComponentHostCommunicationApi';
type ActionConfirmationModalPromiseCallbacks = {
resolve: (result: ActionConfirmationModalResult) => void;
reject: (error: Error) => void;
};
let pendingActionConfirmationModalPromiseCallbacks: ActionConfirmationModalPromiseCallbacks | null =
null;
const clearPendingActionConfirmationModalPromiseCallbacks = () => {
pendingActionConfirmationModalPromiseCallbacks = null;
};
export const createOpenActionConfirmationModalAdapter = (
hostApi: Pick<
FrontComponentHostCommunicationApi,
'openActionConfirmationModal'
>,
): OpenActionConfirmationModalFunction => {
return async (params) => {
if (pendingActionConfirmationModalPromiseCallbacks !== null) {
throw new Error(
'A confirmation modal is already pending for this front component',
);
}
let rejectActionConfirmationModalPromise: (error: Error) => void = () => {};
const actionConfirmationModalResultPromise =
new Promise<ActionConfirmationModalResult>((resolve, reject) => {
rejectActionConfirmationModalPromise = reject;
pendingActionConfirmationModalPromiseCallbacks = { resolve, reject };
});
try {
await hostApi.openActionConfirmationModal(params);
} catch (error) {
clearPendingActionConfirmationModalPromiseCallbacks();
rejectActionConfirmationModalPromise(
error instanceof Error ? error : new Error(String(error)),
);
}
return actionConfirmationModalResultPromise;
};
};
export const handleActionConfirmationModalResult = async (
result: ActionConfirmationModalResult,
) => {
if (pendingActionConfirmationModalPromiseCallbacks === null) {
return;
}
const currentActionConfirmationModalPromiseCallbacks =
pendingActionConfirmationModalPromiseCallbacks;
clearPendingActionConfirmationModalPromiseCallbacks();
currentActionConfirmationModalPromiseCallbacks.resolve(result);
};

View file

@ -2,6 +2,7 @@ import {
type CloseSidePanelFunction,
type EnqueueSnackbarFunction,
type NavigateFunction,
type OpenActionConfirmationModalHostFunction,
type OpenSidePanelPageFunction,
type RequestAccessTokenRefreshFunction,
type UnmountFrontComponentFunction,
@ -11,6 +12,7 @@ export type FrontComponentHostCommunicationApi = {
navigate: NavigateFunction;
requestAccessTokenRefresh: RequestAccessTokenRefreshFunction;
openSidePanelPage: OpenSidePanelPageFunction;
openActionConfirmationModal: OpenActionConfirmationModalHostFunction;
unmountFrontComponent: UnmountFrontComponentFunction;
enqueueSnackbar: EnqueueSnackbarFunction;
closeSidePanel: CloseSidePanelFunction;

View file

@ -9,4 +9,5 @@ export type WorkerExports = {
) => Promise<void>;
initializeHostCommunicationApi: () => Promise<void>;
updateContext: (context: FrontComponentExecutionContext) => Promise<void>;
onConfirmationModalResult: (result: 'confirm' | 'cancel') => Promise<void>;
};

View file

@ -0,0 +1,78 @@
import { useEffect, useState } from 'react';
import {
enqueueSnackbar,
getFrontComponentActionErrorDedupeKey,
openActionConfirmationModal,
unmountFrontComponent,
useFrontComponentId,
type ActionConfirmationModalAccent,
} from '../front-component-api';
export type ActionModalProps = {
title: string;
subtitle: string;
execute: () => void | Promise<void>;
confirmButtonText?: string;
confirmButtonAccent?: ActionConfirmationModalAccent;
};
export const ActionModal = ({
title,
subtitle,
execute,
confirmButtonText,
confirmButtonAccent,
}: ActionModalProps) => {
const [hasExecuted, setHasExecuted] = useState(false);
const frontComponentId = useFrontComponentId();
useEffect(() => {
if (hasExecuted) {
return;
}
setHasExecuted(true);
const run = async () => {
try {
const actionConfirmationModalResult = await openActionConfirmationModal(
{
title,
subtitle,
confirmButtonText,
confirmButtonAccent,
},
);
if (actionConfirmationModalResult === 'confirm') {
await execute();
}
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
await enqueueSnackbar({
message: 'Action failed',
detailedMessage: message,
variant: 'error',
dedupeKey: getFrontComponentActionErrorDedupeKey(frontComponentId),
});
} finally {
await unmountFrontComponent();
}
};
run();
}, [
title,
subtitle,
execute,
confirmButtonText,
confirmButtonAccent,
hasExecuted,
frontComponentId,
]);
return null;
};

View file

@ -4,3 +4,5 @@ export { ActionLink } from './ActionLink';
export type { ActionLinkProps } from './ActionLink';
export { ActionOpenSidePanelPage } from './ActionOpenSidePanelPage';
export type { ActionOpenSidePanelPageProps } from './ActionOpenSidePanelPage';
export { ActionModal } from './ActionModal';
export type { ActionModalProps } from './ActionModal';

View file

@ -0,0 +1,18 @@
import { isDefined } from 'twenty-shared/utils';
import {
frontComponentHostCommunicationApi,
type OpenActionConfirmationModalFunction,
} from '../globals/frontComponentHostCommunicationApi';
export const openActionConfirmationModal: OpenActionConfirmationModalFunction =
(params) => {
const openActionConfirmationModalFunction =
frontComponentHostCommunicationApi.openActionConfirmationModal;
if (!isDefined(openActionConfirmationModalFunction)) {
throw new Error('openActionConfirmationModalFunction is not set');
}
return openActionConfirmationModalFunction(params);
};

View file

@ -20,6 +20,17 @@ export type OpenSidePanelPageFunction = (params: {
shouldResetSearchState?: boolean;
}) => Promise<void>;
export type ActionConfirmationModalResult = 'confirm' | 'cancel';
export type ActionConfirmationModalAccent = 'default' | 'blue' | 'danger';
export type OpenActionConfirmationModalFunction = (params: {
title: string;
subtitle: string;
confirmButtonText?: string;
confirmButtonAccent?: ActionConfirmationModalAccent;
}) => Promise<ActionConfirmationModalResult>;
export type UnmountFrontComponentFunction = () => Promise<void>;
export type EnqueueSnackbarFunction = (
@ -30,10 +41,15 @@ export type CloseSidePanelFunction = () => Promise<void>;
export type RequestAccessTokenRefreshFunction = () => Promise<string>;
export type OpenActionConfirmationModalHostFunction = (
params: Parameters<OpenActionConfirmationModalFunction>[0],
) => Promise<void>;
export type FrontComponentHostCommunicationApiStore = {
navigate?: NavigateFunction;
requestAccessTokenRefresh?: RequestAccessTokenRefreshFunction;
openSidePanelPage?: OpenSidePanelPageFunction;
openActionConfirmationModal?: OpenActionConfirmationModalFunction;
unmountFrontComponent?: UnmountFrontComponentFunction;
enqueueSnackbar?: EnqueueSnackbarFunction;
closeSidePanel?: CloseSidePanelFunction;

View file

@ -19,6 +19,7 @@ export { setFrontComponentExecutionContext } from './context/frontComponentConte
export { closeSidePanel } from './functions/closeSidePanel';
export { enqueueSnackbar } from './functions/enqueueSnackbar';
export { navigate } from './functions/navigate';
export { openActionConfirmationModal } from './functions/openActionConfirmationModal';
export { openSidePanelPage } from './functions/openSidePanelPage';
export { unmountFrontComponent } from './functions/unmountFrontComponent';
export { useFrontComponentExecutionContext } from './hooks/useFrontComponentExecutionContext';
@ -27,6 +28,10 @@ export { useRecordId } from './hooks/useRecordId';
export { useUserId } from './hooks/useUserId';
export type { FrontComponentExecutionContext } from './types/FrontComponentExecutionContext';
export { getFrontComponentActionErrorDedupeKey } from './utils/getFrontComponentActionErrorDedupeKey';
export type {
ActionConfirmationModalAccent,
ActionConfirmationModalResult,
} from './globals/frontComponentHostCommunicationApi';
export { ALLOWED_HTML_ELEMENTS } from './constants/AllowedHtmlElements';
export type { AllowedHtmlElement } from './constants/AllowedHtmlElements';

View file

@ -73,9 +73,15 @@ export { defineView } from './views/define-view';
export type { ViewConfig } from './views/view-config';
// Action components for front components
export { Action, ActionLink, ActionOpenSidePanelPage } from './action';
export {
Action,
ActionLink,
ActionModal,
ActionOpenSidePanelPage,
} from './action';
export type {
ActionLinkProps,
ActionModalProps,
ActionOpenSidePanelPageProps,
ActionProps,
} from './action';
@ -105,6 +111,7 @@ export {
enqueueSnackbar,
getFrontComponentActionErrorDedupeKey,
navigate,
openActionConfirmationModal,
openSidePanelPage,
unmountFrontComponent,
useFrontComponentExecutionContext,
@ -112,7 +119,11 @@ export {
useRecordId,
useUserId,
} from './front-component-api';
export type { FrontComponentExecutionContext } from './front-component-api';
export type {
ActionConfirmationModalAccent,
ActionConfirmationModalResult,
FrontComponentExecutionContext,
} from './front-component-api';
export { AppPath, CommandMenuPages } from 'twenty-shared/types';
export type {