[FRONT COMPONENTS] Add loader to command menu items (#18165)
## PR description - Add a loader to the command menu items when the action is running. - Add a new method `closeSidePanel` to the front component host api. ## Video QA https://github.com/user-attachments/assets/c20b592a-6e4c-4cab-b5da-4cb2316acc7c
This commit is contained in:
+10
-1
@@ -23,9 +23,11 @@ export type ActionDisplayProps = {
|
||||
export const ActionDisplay = ({
|
||||
onClick,
|
||||
to,
|
||||
disabled,
|
||||
}: {
|
||||
onClick?: (event?: React.MouseEvent<HTMLElement>) => void;
|
||||
to?: string;
|
||||
disabled?: boolean;
|
||||
}) => {
|
||||
const action = useContext(ActionConfigContext);
|
||||
const { displayType } = useContext(ActionMenuContext);
|
||||
@@ -39,7 +41,14 @@ export const ActionDisplay = ({
|
||||
}
|
||||
|
||||
if (displayType === 'listItem') {
|
||||
return <ActionListItem action={action} onClick={onClick} to={to} />;
|
||||
return (
|
||||
<ActionListItem
|
||||
action={action}
|
||||
onClick={onClick}
|
||||
to={to}
|
||||
disabled={disabled}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (displayType === 'dropdownItem') {
|
||||
|
||||
+10
-1
@@ -4,19 +4,26 @@ import { CommandMenuItem } from '@/command-menu/components/CommandMenuItem';
|
||||
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { Loader } from 'twenty-ui/feedback';
|
||||
|
||||
export const ActionListItem = ({
|
||||
action,
|
||||
onClick,
|
||||
to,
|
||||
disabled,
|
||||
}: {
|
||||
action: ActionDisplayProps;
|
||||
onClick?: () => void;
|
||||
to?: string;
|
||||
disabled?: boolean;
|
||||
}) => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleClick = () => {
|
||||
if (disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
onClick?.();
|
||||
if (isDefined(to)) {
|
||||
navigate(to);
|
||||
@@ -31,8 +38,10 @@ export const ActionListItem = ({
|
||||
label={getActionLabel(action.label)}
|
||||
description={getActionLabel(action.description ?? '')}
|
||||
to={to}
|
||||
onClick={onClick}
|
||||
onClick={disabled ? undefined : onClick}
|
||||
hotKeys={action.hotKeys}
|
||||
disabled={disabled}
|
||||
RightComponent={disabled ? <Loader /> : undefined}
|
||||
/>
|
||||
</SelectableListItem>
|
||||
);
|
||||
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
import { ActionConfigContext } from '@/action-menu/contexts/ActionConfigContext';
|
||||
import { useCloseActionMenu } from '@/action-menu/hooks/useCloseActionMenu';
|
||||
import { isHeadlessFrontComponentMountedFamilySelector } from '@/front-components/selectors/isHeadlessFrontComponentMountedFamilySelector';
|
||||
import { useContext } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { ActionDisplay } from './ActionDisplay';
|
||||
|
||||
export const HeadlessFrontComponentAction = ({
|
||||
frontComponentId,
|
||||
onClick,
|
||||
}: {
|
||||
frontComponentId: string;
|
||||
onClick: () => void;
|
||||
}) => {
|
||||
const actionConfig = useContext(ActionConfigContext);
|
||||
|
||||
const { closeActionMenu } = useCloseActionMenu({
|
||||
closeSidePanelOnShowPageOptionsActionExecution: false,
|
||||
closeSidePanelOnCommandMenuListActionExecution: false,
|
||||
});
|
||||
|
||||
const isMounted = useRecoilValue(
|
||||
isHeadlessFrontComponentMountedFamilySelector(frontComponentId),
|
||||
);
|
||||
|
||||
if (!actionConfig) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const handleClick = () => {
|
||||
if (isMounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
closeActionMenu();
|
||||
onClick();
|
||||
};
|
||||
|
||||
return <ActionDisplay onClick={handleClick} disabled={isMounted} />;
|
||||
};
|
||||
+6
-3
@@ -2,6 +2,7 @@ import { Action } from '@/action-menu/actions/components/Action';
|
||||
import { ActionScope } from '@/action-menu/actions/types/ActionScope';
|
||||
import { ActionType } from '@/action-menu/actions/types/ActionType';
|
||||
import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext';
|
||||
import { HeadlessFrontComponentAction } from '@/action-menu/actions/display/components/HeadlessFrontComponentAction';
|
||||
import { useOpenFrontComponentInCommandMenu } from '@/command-menu/hooks/useOpenFrontComponentInCommandMenu';
|
||||
import { contextStoreCurrentObjectMetadataItemIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataItemIdComponentState';
|
||||
import { contextStoreIsPageInEditModeComponentState } from '@/context-store/states/contextStoreIsPageInEditModeComponentState';
|
||||
@@ -76,11 +77,13 @@ const buildActionFromItem = ({
|
||||
isPinned,
|
||||
Icon,
|
||||
shouldBeRegistered: () => true,
|
||||
component: (
|
||||
<Action
|
||||
component: isHeadless ? (
|
||||
<HeadlessFrontComponentAction
|
||||
frontComponentId={item.frontComponentId}
|
||||
onClick={handleClick}
|
||||
closeSidePanelOnCommandMenuListActionExecution={isHeadless}
|
||||
/>
|
||||
) : (
|
||||
<Action onClick={handleClick} />
|
||||
),
|
||||
};
|
||||
};
|
||||
|
||||
+8
@@ -6,6 +6,7 @@ import {
|
||||
import { type AppPath } from 'twenty-shared/types';
|
||||
|
||||
import { currentUserState } from '@/auth/states/currentUserState';
|
||||
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
|
||||
import { useNavigateCommandMenu } from '@/command-menu/hooks/useNavigateCommandMenu';
|
||||
import { commandMenuSearchState } from '@/command-menu/states/commandMenuSearchState';
|
||||
import { useUnmountHeadlessFrontComponent } from '@/front-components/hooks/useUnmountHeadlessFrontComponent';
|
||||
@@ -26,6 +27,7 @@ export const useFrontComponentExecutionContext = ({
|
||||
const setCommandMenuSearchState = useSetRecoilState(commandMenuSearchState);
|
||||
const { getIcon } = useIcons();
|
||||
const unmountHeadlessFrontComponent = useUnmountHeadlessFrontComponent();
|
||||
const { closeCommandMenu } = useCommandMenu();
|
||||
|
||||
const navigate: FrontComponentHostCommunicationApi['navigate'] = async (
|
||||
to,
|
||||
@@ -63,11 +65,17 @@ export const useFrontComponentExecutionContext = ({
|
||||
unmountHeadlessFrontComponent(frontComponentId);
|
||||
};
|
||||
|
||||
const closeSidePanel: FrontComponentHostCommunicationApi['closeSidePanel'] =
|
||||
async () => {
|
||||
closeCommandMenu();
|
||||
};
|
||||
|
||||
const frontComponentHostCommunicationApi: FrontComponentHostCommunicationApi =
|
||||
{
|
||||
navigate,
|
||||
openSidePanelPage,
|
||||
unmountFrontComponent,
|
||||
closeSidePanel,
|
||||
};
|
||||
|
||||
return {
|
||||
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
import { selectorFamily } from 'recoil';
|
||||
|
||||
import { mountedHeadlessFrontComponentIdsState } from '@/front-components/states/mountedHeadlessFrontComponentIdsState';
|
||||
|
||||
export const isHeadlessFrontComponentMountedFamilySelector = selectorFamily<
|
||||
boolean,
|
||||
string
|
||||
>({
|
||||
key: 'isHeadlessFrontComponentMountedFamilySelector',
|
||||
get:
|
||||
(frontComponentId: string) =>
|
||||
({ get }) => {
|
||||
const mountedIds = get(mountedHeadlessFrontComponentIdsState);
|
||||
|
||||
return mountedIds.has(frontComponentId);
|
||||
},
|
||||
});
|
||||
@@ -95,6 +95,7 @@ const initializeHostCommunicationApi: WorkerExports['initializeHostCommunication
|
||||
hostApi.openSidePanelPage;
|
||||
frontComponentHostCommunicationApi.unmountFrontComponent =
|
||||
hostApi.unmountFrontComponent;
|
||||
frontComponentHostCommunicationApi.closeSidePanel = hostApi.closeSidePanel;
|
||||
};
|
||||
|
||||
const updateContext: WorkerExports['updateContext'] = async (
|
||||
|
||||
+2
@@ -1,4 +1,5 @@
|
||||
import {
|
||||
type CloseSidePanelFunction,
|
||||
type NavigateFunction,
|
||||
type OpenSidePanelPageFunction,
|
||||
type UnmountFrontComponentFunction,
|
||||
@@ -8,4 +9,5 @@ export type FrontComponentHostCommunicationApi = {
|
||||
navigate: NavigateFunction;
|
||||
openSidePanelPage: OpenSidePanelPageFunction;
|
||||
unmountFrontComponent: UnmountFrontComponentFunction;
|
||||
closeSidePanel: CloseSidePanelFunction;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import {
|
||||
type CloseSidePanelFunction,
|
||||
frontComponentHostCommunicationApi,
|
||||
} from '../globals/frontComponentHostCommunicationApi';
|
||||
|
||||
export const closeSidePanel: CloseSidePanelFunction = () => {
|
||||
const closeSidePanelFunction =
|
||||
frontComponentHostCommunicationApi.closeSidePanel;
|
||||
|
||||
if (!isDefined(closeSidePanelFunction)) {
|
||||
throw new Error('closeSidePanelFunction is not set');
|
||||
}
|
||||
|
||||
return closeSidePanelFunction();
|
||||
};
|
||||
+3
@@ -21,10 +21,13 @@ export type OpenSidePanelPageFunction = (params: {
|
||||
|
||||
export type UnmountFrontComponentFunction = () => Promise<void>;
|
||||
|
||||
export type CloseSidePanelFunction = () => Promise<void>;
|
||||
|
||||
export type FrontComponentHostCommunicationApiStore = {
|
||||
navigate?: NavigateFunction;
|
||||
openSidePanelPage?: OpenSidePanelPageFunction;
|
||||
unmountFrontComponent?: UnmountFrontComponentFunction;
|
||||
closeSidePanel?: CloseSidePanelFunction;
|
||||
};
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export { setFrontComponentExecutionContext } from './context/frontComponentContext';
|
||||
export { closeSidePanel } from './functions/closeSidePanel';
|
||||
export { navigate } from './functions/navigate';
|
||||
export { openSidePanelPage } from './functions/openSidePanelPage';
|
||||
export { unmountFrontComponent } from './functions/unmountFrontComponent';
|
||||
|
||||
@@ -74,6 +74,7 @@ export type { ActionOpenSidePanelPageProps } from './action';
|
||||
|
||||
// Front Component API exports
|
||||
export {
|
||||
closeSidePanel,
|
||||
navigate,
|
||||
openSidePanelPage,
|
||||
unmountFrontComponent,
|
||||
|
||||
Reference in New Issue
Block a user