7339 implement contextual actions inside the commandmenu (#8000)

Closes #7339


https://github.com/user-attachments/assets/b623caa4-c1b3-448e-8880-4a8301802ba8
This commit is contained in:
Raphaël Bosi
2024-10-29 15:10:45 +01:00
committed by GitHub
parent 8bb07c4a4f
commit fe2c8bb43b
30 changed files with 399 additions and 237 deletions

View File

@ -1,5 +1,5 @@
import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext';
import { useActionMenuEntries } from '@/action-menu/hooks/useActionMenuEntries';
import { ActionMenuType } from '@/action-menu/types/ActionMenuType';
import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState';
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
import { computeContextStoreFilters } from '@/context-store/utils/computeContextStoreFilters';
@ -12,17 +12,15 @@ import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTabl
import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal';
import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useCallback, useEffect, useState } from 'react';
import { useCallback, useContext, useEffect, useState } from 'react';
import { IconTrash, isDefined } from 'twenty-ui';
export const DeleteRecordsActionEffect = ({
position,
objectMetadataItem,
actionMenuType,
}: {
position: number;
objectMetadataItem: ObjectMetadataItem;
actionMenuType: ActionMenuType;
}) => {
const { addActionMenuEntry, removeActionMenuEntry } = useActionMenuEntries();
@ -93,6 +91,9 @@ export const DeleteRecordsActionEffect = ({
contextStoreNumberOfSelectedRecords < DELETE_MAX_COUNT &&
contextStoreNumberOfSelectedRecords > 0;
const { isInRightDrawer, onActionExecutedCallback } =
useContext(ActionMenuContext);
useEffect(() => {
if (canDelete) {
addActionMenuEntry({
@ -120,17 +121,14 @@ export const DeleteRecordsActionEffect = ({
} can be recovered from the Options menu.`}
onConfirmClick={() => {
handleDeleteClick();
if (actionMenuType === 'recordShow') {
onActionExecutedCallback?.();
if (isInRightDrawer) {
closeRightDrawer();
}
}}
deleteButtonText={`Delete ${
contextStoreNumberOfSelectedRecords > 1 ? 'Records' : 'Record'
}`}
modalVariant={
actionMenuType === 'recordShow' ? 'tertiary' : 'primary'
}
/>
),
});
@ -142,13 +140,14 @@ export const DeleteRecordsActionEffect = ({
removeActionMenuEntry('delete');
};
}, [
actionMenuType,
addActionMenuEntry,
canDelete,
closeRightDrawer,
contextStoreNumberOfSelectedRecords,
handleDeleteClick,
isDeleteRecordsModalOpen,
isInRightDrawer,
onActionExecutedCallback,
position,
removeActionMenuEntry,
]);

View File

@ -1,27 +0,0 @@
import { DeleteRecordsActionEffect } from '@/action-menu/actions/record-actions/components/DeleteRecordsActionEffect';
import { ExportRecordsActionEffect } from '@/action-menu/actions/record-actions/components/ExportRecordsActionEffect';
import { ActionMenuType } from '@/action-menu/types/ActionMenuType';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
const actionEffects = [ExportRecordsActionEffect, DeleteRecordsActionEffect];
export const MultipleRecordsActionMenuEntriesSetter = ({
objectMetadataItem,
actionMenuType,
}: {
objectMetadataItem: ObjectMetadataItem;
actionMenuType: ActionMenuType;
}) => {
return (
<>
{actionEffects.map((ActionEffect, index) => (
<ActionEffect
key={index}
position={index}
objectMetadataItem={objectMetadataItem}
actionMenuType={actionMenuType}
/>
))}
</>
);
};

View File

@ -1,16 +1,23 @@
import { MultipleRecordsActionMenuEntriesSetter } from '@/action-menu/actions/record-actions/components/MultipleRecordsActionMenuEntriesSetter';
import { SingleRecordActionMenuEntriesSetter } from '@/action-menu/actions/record-actions/components/SingleRecordActionMenuEntriesSetter';
import { ActionMenuType } from '@/action-menu/types/ActionMenuType';
import { DeleteRecordsActionEffect } from '@/action-menu/actions/record-actions/components/DeleteRecordsActionEffect';
import { ExportRecordsActionEffect } from '@/action-menu/actions/record-actions/components/ExportRecordsActionEffect';
import { ManageFavoritesActionEffect } from '@/action-menu/actions/record-actions/components/ManageFavoritesActionEffect';
import { contextStoreCurrentObjectMetadataIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdComponentState';
import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState';
import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
export const RecordActionMenuEntriesSetter = ({
actionMenuType,
}: {
actionMenuType: ActionMenuType;
}) => {
const singleRecordActionEffects = [
ManageFavoritesActionEffect,
ExportRecordsActionEffect,
DeleteRecordsActionEffect,
];
const multipleRecordActionEffects = [
ExportRecordsActionEffect,
DeleteRecordsActionEffect,
];
export const RecordActionMenuEntriesSetter = () => {
const contextStoreNumberOfSelectedRecords = useRecoilComponentValueV2(
contextStoreNumberOfSelectedRecordsComponentState,
);
@ -33,19 +40,20 @@ export const RecordActionMenuEntriesSetter = ({
return null;
}
if (contextStoreNumberOfSelectedRecords === 1) {
return (
<SingleRecordActionMenuEntriesSetter
objectMetadataItem={objectMetadataItem}
actionMenuType={actionMenuType}
/>
);
}
const actions =
contextStoreNumberOfSelectedRecords === 1
? singleRecordActionEffects
: multipleRecordActionEffects;
return (
<MultipleRecordsActionMenuEntriesSetter
objectMetadataItem={objectMetadataItem}
actionMenuType={actionMenuType}
/>
<>
{actions.map((ActionEffect, index) => (
<ActionEffect
key={index}
position={index}
objectMetadataItem={objectMetadataItem}
/>
))}
</>
);
};

View File

@ -1,31 +0,0 @@
import { DeleteRecordsActionEffect } from '@/action-menu/actions/record-actions/components/DeleteRecordsActionEffect';
import { ExportRecordsActionEffect } from '@/action-menu/actions/record-actions/components/ExportRecordsActionEffect';
import { ManageFavoritesActionEffect } from '@/action-menu/actions/record-actions/components/ManageFavoritesActionEffect';
import { ActionMenuType } from '@/action-menu/types/ActionMenuType';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
export const SingleRecordActionMenuEntriesSetter = ({
objectMetadataItem,
actionMenuType,
}: {
objectMetadataItem: ObjectMetadataItem;
actionMenuType: ActionMenuType;
}) => {
const actionEffects = [
ManageFavoritesActionEffect,
ExportRecordsActionEffect,
DeleteRecordsActionEffect,
];
return (
<>
{actionEffects.map((ActionEffect, index) => (
<ActionEffect
key={index}
position={index}
objectMetadataItem={objectMetadataItem}
actionMenuType={actionMenuType}
/>
))}
</>
);
};

View File

@ -3,16 +3,12 @@ import { ActionMenuConfirmationModals } from '@/action-menu/components/ActionMen
import { RecordIndexActionMenuBar } from '@/action-menu/components/RecordIndexActionMenuBar';
import { RecordIndexActionMenuDropdown } from '@/action-menu/components/RecordIndexActionMenuDropdown';
import { RecordIndexActionMenuEffect } from '@/action-menu/components/RecordIndexActionMenuEffect';
import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext';
import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext';
import { contextStoreCurrentObjectMetadataIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdComponentState';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
export const RecordIndexActionMenu = ({
actionMenuId,
}: {
actionMenuId: string;
}) => {
export const RecordIndexActionMenu = () => {
const contextStoreCurrentObjectMetadataId = useRecoilComponentValueV2(
contextStoreCurrentObjectMetadataIdComponentState,
);
@ -20,15 +16,18 @@ export const RecordIndexActionMenu = ({
return (
<>
{contextStoreCurrentObjectMetadataId && (
<ActionMenuComponentInstanceContext.Provider
value={{ instanceId: actionMenuId }}
<ActionMenuContext.Provider
value={{
isInRightDrawer: false,
onActionExecutedCallback: () => {},
}}
>
<RecordIndexActionMenuBar />
<RecordIndexActionMenuDropdown />
<ActionMenuConfirmationModals />
<RecordIndexActionMenuEffect />
<RecordActionMenuEntriesSetter actionMenuType="recordIndex" />
</ActionMenuComponentInstanceContext.Provider>
<RecordActionMenuEntriesSetter />
</ActionMenuContext.Provider>
)}
</>
);

View File

@ -1,7 +1,9 @@
import { useActionMenu } from '@/action-menu/hooks/useActionMenu';
import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext';
import { isCommandMenuOpenedState } from '@/command-menu/states/isCommandMenuOpenedState';
import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState';
import { isDropdownOpenComponentState } from '@/ui/layout/dropdown/states/isDropdownOpenComponentState';
import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { extractComponentState } from '@/ui/utilities/state/component-state/utils/extractComponentState';
@ -25,6 +27,9 @@ export const RecordIndexActionMenuEffect = () => {
`action-menu-dropdown-${actionMenuId}`,
),
);
const { isRightDrawerOpen } = useRightDrawer();
const isCommandMenuOpened = useRecoilValue(isCommandMenuOpenedState);
useEffect(() => {
if (contextStoreNumberOfSelectedRecords > 0 && !isDropdownOpen) {
@ -43,5 +48,11 @@ export const RecordIndexActionMenuEffect = () => {
isDropdownOpen,
]);
useEffect(() => {
if (isRightDrawerOpen || isCommandMenuOpened) {
closeActionBar();
}
}, [closeActionBar, isRightDrawerOpen, isCommandMenuOpened]);
return null;
};

View File

@ -1,30 +1,53 @@
import { RecordActionMenuEntriesSetter } from '@/action-menu/actions/record-actions/components/RecordActionMenuEntriesSetter';
import { ActionMenuConfirmationModals } from '@/action-menu/components/ActionMenuConfirmationModals';
import { RecordShowActionMenuBar } from '@/action-menu/components/RecordShowActionMenuBar';
import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext';
import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext';
import { contextStoreCurrentObjectMetadataIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdComponentState';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { RecordShowPageBaseHeader } from '~/pages/object-record/RecordShowPageBaseHeader';
export const RecordShowActionMenu = ({
actionMenuId,
isFavorite,
handleFavoriteButtonClick,
record,
objectMetadataItem,
objectNameSingular,
}: {
actionMenuId: string;
isFavorite: boolean;
handleFavoriteButtonClick: () => void;
record: ObjectRecord | undefined;
objectMetadataItem: ObjectMetadataItem;
objectNameSingular: string;
}) => {
const contextStoreCurrentObjectMetadataId = useRecoilComponentValueV2(
contextStoreCurrentObjectMetadataIdComponentState,
);
// TODO: refactor RecordShowPageBaseHeader to use the context store
return (
<>
{contextStoreCurrentObjectMetadataId && (
<ActionMenuComponentInstanceContext.Provider
value={{ instanceId: actionMenuId }}
<ActionMenuContext.Provider
value={{
isInRightDrawer: false,
onActionExecutedCallback: () => {},
}}
>
<RecordShowActionMenuBar />
<RecordShowPageBaseHeader
{...{
isFavorite,
handleFavoriteButtonClick,
record,
objectMetadataItem,
objectNameSingular,
}}
/>
<ActionMenuConfirmationModals />
<RecordActionMenuEntriesSetter actionMenuType="recordShow" />
</ActionMenuComponentInstanceContext.Provider>
<RecordActionMenuEntriesSetter />
</ActionMenuContext.Provider>
)}
</>
);

View File

@ -0,0 +1,30 @@
import { RecordActionMenuEntriesSetter } from '@/action-menu/actions/record-actions/components/RecordActionMenuEntriesSetter';
import { ActionMenuConfirmationModals } from '@/action-menu/components/ActionMenuConfirmationModals';
import { RecordShowRightDrawerActionMenuBar } from '@/action-menu/components/RecordShowRightDrawerActionMenuBar';
import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext';
import { contextStoreCurrentObjectMetadataIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdComponentState';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
export const RecordShowRightDrawerActionMenu = () => {
const contextStoreCurrentObjectMetadataId = useRecoilComponentValueV2(
contextStoreCurrentObjectMetadataIdComponentState,
);
return (
<>
{contextStoreCurrentObjectMetadataId && (
<ActionMenuContext.Provider
value={{
isInRightDrawer: true,
onActionExecutedCallback: () => {},
}}
>
<RecordShowRightDrawerActionMenuBar />
<ActionMenuConfirmationModals />
<RecordActionMenuEntriesSetter />
</ActionMenuContext.Provider>
)}
</>
);
};

View File

@ -2,7 +2,7 @@ import { RecordShowActionMenuBarEntry } from '@/action-menu/components/RecordSho
import { actionMenuEntriesComponentSelector } from '@/action-menu/states/actionMenuEntriesComponentSelector';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
export const RecordShowActionMenuBar = () => {
export const RecordShowRightDrawerActionMenuBar = () => {
const actionMenuEntries = useRecoilComponentValueV2(
actionMenuEntriesComponentSelector,
);

View File

@ -2,7 +2,7 @@ import { expect, jest } from '@storybook/jest';
import { Meta, StoryObj } from '@storybook/react';
import { RecoilRoot } from 'recoil';
import { RecordShowActionMenuBar } from '@/action-menu/components/RecordShowActionMenuBar';
import { RecordShowRightDrawerActionMenuBar } from '@/action-menu/components/RecordShowRightDrawerActionMenuBar';
import { actionMenuEntriesComponentState } from '@/action-menu/states/actionMenuEntriesComponentState';
import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext';
import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState';
@ -20,9 +20,9 @@ const deleteMock = jest.fn();
const addToFavoritesMock = jest.fn();
const exportMock = jest.fn();
const meta: Meta<typeof RecordShowActionMenuBar> = {
title: 'Modules/ActionMenu/RecordShowActionMenuBar',
component: RecordShowActionMenuBar,
const meta: Meta<typeof RecordShowRightDrawerActionMenuBar> = {
title: 'Modules/ActionMenu/RecordShowRightDrawerActionMenuBar',
component: RecordShowRightDrawerActionMenuBar,
decorators: [
(Story) => (
<RecoilRoot
@ -98,7 +98,7 @@ const meta: Meta<typeof RecordShowActionMenuBar> = {
export default meta;
type Story = StoryObj<typeof RecordShowActionMenuBar>;
type Story = StoryObj<typeof RecordShowRightDrawerActionMenuBar>;
export const Default: Story = {
args: {

View File

@ -0,0 +1,11 @@
import { createContext } from 'react';
type ActionMenuContextType = {
isInRightDrawer: boolean;
onActionExecutedCallback: () => void;
};
export const ActionMenuContext = createContext<ActionMenuContextType>({
isInRightDrawer: false,
onActionExecutedCallback: () => {},
});

View File

@ -1 +0,0 @@
export type ActionMenuType = 'recordIndex' | 'recordShow';