Headless action modal (#18270)
https://github.com/user-attachments/assets/809a281f-3c38-41df-99db-e780941acf9f
This commit is contained in:
+67
@@ -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
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
export const ACTION_MENU_CONFIRMATION_MODAL_INSTANCE_ID =
|
||||
'action-menu-confirmation-modal';
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
export const ACTION_MENU_CONFIRMATION_MODAL_RESULT_BROWSER_EVENT_NAME =
|
||||
'action-menu-confirmation-modal-result';
|
||||
+48
@@ -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 };
|
||||
};
|
||||
+18
@@ -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,
|
||||
});
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
export type ActionMenuConfirmationModalResult = 'confirm' | 'cancel';
|
||||
|
||||
export type ActionMenuConfirmationModalResultBrowserEventDetail = {
|
||||
frontComponentId: string;
|
||||
confirmationResult: ActionMenuConfirmationModalResult;
|
||||
};
|
||||
@@ -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>
|
||||
|
||||
+14
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user