Action menu refactoring (#11454)
# Description Closes [#696](https://github.com/twentyhq/core-team-issues/issues/696) - `useAction` hooks have been removed for all actions - Every action can now declare a react component - Some standard action components have been introduced: `Action`, `ActionLink` and `ActionModal` - The `ActionDisplay` component uses the new `displayType` prop of the `ActionMenuContext` to render the right component for the action according to its container: `ActionButton`, `ActionDropdownItem` or `ActionListItem` - The `ActionDisplayer` wraps the action component inside a context which gives it all the information about the action -`actionMenuEntriesComponenState` has been removed and now all actions are computed directly using `useRegisteredAction` - This computation is done inside `ActionMenuContextProvider` and the actions are passed inside a context - `actionMenuType` gives information about the container of the action, so the action can know wether or not to close this container upon execution
This commit is contained in:
@ -1,23 +0,0 @@
|
||||
import { actionMenuEntriesComponentSelector } from '@/action-menu/states/actionMenuEntriesComponentSelector';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
const StyledActionMenuConfirmationModals = styled.div`
|
||||
position: absolute;
|
||||
`;
|
||||
|
||||
export const ActionMenuConfirmationModals = () => {
|
||||
const actionMenuEntries = useRecoilComponentValueV2(
|
||||
actionMenuEntriesComponentSelector,
|
||||
);
|
||||
|
||||
return (
|
||||
<StyledActionMenuConfirmationModals data-select-disable>
|
||||
{actionMenuEntries.map((actionMenuEntry, index) =>
|
||||
actionMenuEntry.ConfirmationModal ? (
|
||||
<div key={index}>{actionMenuEntry.ConfirmationModal}</div>
|
||||
) : null,
|
||||
)}
|
||||
</StyledActionMenuConfirmationModals>
|
||||
);
|
||||
};
|
||||
@ -1,6 +1,7 @@
|
||||
import { actionMenuEntriesComponentSelector } from '@/action-menu/states/actionMenuEntriesComponentSelector';
|
||||
import { ActionComponent } from '@/action-menu/actions/display/components/ActionComponent';
|
||||
import { ActionScope } from '@/action-menu/actions/types/ActionScope';
|
||||
import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext';
|
||||
import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext';
|
||||
import { ActionMenuEntryScope } from '@/action-menu/types/ActionMenuEntry';
|
||||
import { CommandMenuActionMenuDropdownHotkeyScope } from '@/action-menu/types/CommandMenuActionMenuDropdownHotkeyScope';
|
||||
import { getRightDrawerActionMenuDropdownIdFromActionMenuId } from '@/action-menu/utils/getRightDrawerActionMenuDropdownIdFromActionMenuId';
|
||||
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
|
||||
@ -9,17 +10,13 @@ import { useDropdownV2 } from '@/ui/layout/dropdown/hooks/useDropdownV2';
|
||||
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||
import { AppHotkeyScope } from '@/ui/utilities/hotkey/types/AppHotkeyScope';
|
||||
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import { i18n } from '@lingui/core';
|
||||
import { useContext } from 'react';
|
||||
import { Button } from 'twenty-ui/input';
|
||||
import { getOsControlSymbol } from 'twenty-ui/utilities';
|
||||
import { MenuItem } from 'twenty-ui/navigation';
|
||||
|
||||
export const CommandMenuActionMenuDropdown = () => {
|
||||
const actionMenuEntries = useRecoilComponentValueV2(
|
||||
actionMenuEntriesComponentSelector,
|
||||
);
|
||||
const { actions } = useContext(ActionMenuContext);
|
||||
|
||||
const actionMenuId = useAvailableComponentInstanceIdOrThrow(
|
||||
ActionMenuComponentInstanceContext,
|
||||
@ -61,25 +58,10 @@ export const CommandMenuActionMenuDropdown = () => {
|
||||
dropdownOffset={{ y: parseInt(theme.spacing(2), 10) }}
|
||||
dropdownComponents={
|
||||
<DropdownMenuItemsContainer>
|
||||
{actionMenuEntries
|
||||
.filter(
|
||||
(actionMenuEntry) =>
|
||||
actionMenuEntry.scope === ActionMenuEntryScope.RecordSelection,
|
||||
)
|
||||
.map((actionMenuEntry, index) => (
|
||||
<MenuItem
|
||||
key={index}
|
||||
LeftIcon={actionMenuEntry.Icon}
|
||||
onClick={() => {
|
||||
toggleDropdown(
|
||||
getRightDrawerActionMenuDropdownIdFromActionMenuId(
|
||||
actionMenuId,
|
||||
),
|
||||
);
|
||||
actionMenuEntry.onClick?.();
|
||||
}}
|
||||
text={i18n._(actionMenuEntry.label)}
|
||||
/>
|
||||
{actions
|
||||
.filter((action) => action.scope === ActionScope.RecordSelection)
|
||||
.map((action) => (
|
||||
<ActionComponent action={action} key={action.key} />
|
||||
))}
|
||||
</DropdownMenuItemsContainer>
|
||||
}
|
||||
|
||||
@ -1,59 +1,13 @@
|
||||
import { actionMenuEntriesComponentSelector } from '@/action-menu/states/actionMenuEntriesComponentSelector';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
import styled from '@emotion/styled';
|
||||
import { i18n } from '@lingui/core';
|
||||
import { AppTooltip, TooltipDelay, TooltipPosition } from 'twenty-ui/display';
|
||||
import { Button, IconButton } from 'twenty-ui/input';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
font-size: ${({ theme }) => theme.font.size.md};
|
||||
`;
|
||||
import { ActionComponent } from '@/action-menu/actions/display/components/ActionComponent';
|
||||
import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext';
|
||||
|
||||
import { useContext } from 'react';
|
||||
export const PageHeaderActionMenuButtons = () => {
|
||||
const actionMenuEntries = useRecoilComponentValueV2(
|
||||
actionMenuEntriesComponentSelector,
|
||||
);
|
||||
const { actions } = useContext(ActionMenuContext);
|
||||
|
||||
const pinnedEntries = actionMenuEntries.filter((entry) => entry.isPinned);
|
||||
const pinnedActions = actions.filter((entry) => entry.isPinned);
|
||||
|
||||
return (
|
||||
<>
|
||||
{pinnedEntries.map((entry) =>
|
||||
entry.shortLabel ? (
|
||||
<Button
|
||||
key={entry.key}
|
||||
Icon={entry.Icon}
|
||||
size="small"
|
||||
variant="secondary"
|
||||
accent="default"
|
||||
title={entry.shortLabel ? i18n._(entry.shortLabel) : ''}
|
||||
onClick={() => entry.onClick?.()}
|
||||
ariaLabel={i18n._(entry.label)}
|
||||
/>
|
||||
) : (
|
||||
<div id={`action-menu-entry-${entry.key}`} key={entry.key}>
|
||||
<IconButton
|
||||
Icon={entry.Icon}
|
||||
size="small"
|
||||
variant="secondary"
|
||||
accent="default"
|
||||
onClick={() => entry.onClick?.()}
|
||||
ariaLabel={i18n._(entry.label)}
|
||||
/>
|
||||
<StyledWrapper>
|
||||
<AppTooltip
|
||||
// eslint-disable-next-line
|
||||
anchorSelect={`#action-menu-entry-${entry.key}`}
|
||||
content={i18n._(entry.label)}
|
||||
delay={TooltipDelay.longDelay}
|
||||
place={TooltipPosition.Bottom}
|
||||
offset={5}
|
||||
noArrow
|
||||
/>
|
||||
</StyledWrapper>
|
||||
</div>
|
||||
),
|
||||
)}
|
||||
</>
|
||||
);
|
||||
return pinnedActions.map((action) => (
|
||||
<ActionComponent key={action.key} action={action} />
|
||||
));
|
||||
};
|
||||
|
||||
@ -1,62 +1,36 @@
|
||||
import { RecordActionMenuEntriesSetter } from '@/action-menu/actions/record-actions/components/RecordActionMenuEntriesSetter';
|
||||
import { MultipleRecordsActionKeys } from '@/action-menu/actions/record-actions/multiple-records/types/MultipleRecordsActionKeys';
|
||||
import { RecordAgnosticActionMenuEntriesSetter } from '@/action-menu/actions/record-agnostic-actions/components/RecordAgnosticActionMenuEntriesSetter';
|
||||
import { RunWorkflowRecordAgnosticActionMenuEntriesSetter } from '@/action-menu/actions/record-agnostic-actions/components/RunWorkflowRecordAgnosticActionMenuEntriesSetter';
|
||||
import { ActionMenuConfirmationModals } from '@/action-menu/components/ActionMenuConfirmationModals';
|
||||
import { PageHeaderActionMenuButtons } from '@/action-menu/components/PageHeaderActionMenuButtons';
|
||||
import { RecordIndexActionMenuDropdown } from '@/action-menu/components/RecordIndexActionMenuDropdown';
|
||||
import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext';
|
||||
import { ActionMenuContextProvider } from '@/action-menu/contexts/ActionMenuContextProvider';
|
||||
import { contextStoreCurrentObjectMetadataItemIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataItemIdComponentState';
|
||||
import { isRecordIndexLoadMoreLockedComponentState } from '@/object-record/record-index/states/isRecordIndexLoadMoreLockedComponentState';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
import { FeatureFlagKey } from '~/generated/graphql';
|
||||
import { useIsMobile } from 'twenty-ui/utilities';
|
||||
|
||||
export const RecordIndexActionMenu = ({ indexId }: { indexId: string }) => {
|
||||
export const RecordIndexActionMenu = () => {
|
||||
const contextStoreCurrentObjectMetadataItemId = useRecoilComponentValueV2(
|
||||
contextStoreCurrentObjectMetadataItemIdComponentState,
|
||||
);
|
||||
|
||||
const isWorkflowEnabled = useIsFeatureEnabled(
|
||||
FeatureFlagKey.IsWorkflowEnabled,
|
||||
);
|
||||
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
const setIsLoadMoreLocked = useSetRecoilComponentStateV2(
|
||||
isRecordIndexLoadMoreLockedComponentState,
|
||||
indexId,
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{contextStoreCurrentObjectMetadataItemId && (
|
||||
<ActionMenuContext.Provider
|
||||
value={{
|
||||
isInRightDrawer: false,
|
||||
onActionStartedCallback: (action) => {
|
||||
if (action.key === MultipleRecordsActionKeys.DELETE) {
|
||||
setIsLoadMoreLocked(true);
|
||||
}
|
||||
},
|
||||
onActionExecutedCallback: (action) => {
|
||||
if (action.key === MultipleRecordsActionKeys.DELETE) {
|
||||
setIsLoadMoreLocked(false);
|
||||
}
|
||||
},
|
||||
}}
|
||||
>
|
||||
{!isMobile && <PageHeaderActionMenuButtons />}
|
||||
<RecordIndexActionMenuDropdown />
|
||||
<ActionMenuConfirmationModals />
|
||||
<RecordActionMenuEntriesSetter />
|
||||
<RecordAgnosticActionMenuEntriesSetter />
|
||||
{isWorkflowEnabled && (
|
||||
<RunWorkflowRecordAgnosticActionMenuEntriesSetter />
|
||||
)}
|
||||
</ActionMenuContext.Provider>
|
||||
<>
|
||||
<ActionMenuContextProvider
|
||||
isInRightDrawer={false}
|
||||
displayType="button"
|
||||
actionMenuType="index-page-action-menu"
|
||||
>
|
||||
{!isMobile && <PageHeaderActionMenuButtons />}
|
||||
</ActionMenuContextProvider>
|
||||
<ActionMenuContextProvider
|
||||
isInRightDrawer={false}
|
||||
displayType="dropdownItem"
|
||||
actionMenuType="index-page-action-menu-dropdown"
|
||||
>
|
||||
<RecordIndexActionMenuDropdown />
|
||||
</ActionMenuContextProvider>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
@ -1,54 +0,0 @@
|
||||
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { IconLayoutSidebarRightExpand } from 'twenty-ui/display';
|
||||
import { getOsControlSymbol } from 'twenty-ui/utilities';
|
||||
|
||||
const StyledButton = styled.div`
|
||||
border-radius: ${({ theme }) => theme.border.radius.sm};
|
||||
color: ${({ theme }) => theme.font.color.secondary};
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
padding: ${({ theme }) => theme.spacing(2)};
|
||||
transition: background ${({ theme }) => theme.animation.duration.fast} ease;
|
||||
user-select: none;
|
||||
|
||||
&:hover {
|
||||
background: ${({ theme }) => theme.background.tertiary};
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledButtonLabel = styled.div`
|
||||
font-weight: ${({ theme }) => theme.font.weight.medium};
|
||||
margin-left: ${({ theme }) => theme.spacing(1)};
|
||||
`;
|
||||
|
||||
const StyledShortcutLabel = styled.div`
|
||||
color: ${({ theme }) => theme.font.color.light};
|
||||
font-weight: ${({ theme }) => theme.font.weight.medium};
|
||||
`;
|
||||
|
||||
const StyledSeparator = styled.div<{ size: 'sm' | 'md' }>`
|
||||
background: ${({ theme }) => theme.border.color.light};
|
||||
height: ${({ theme, size }) => theme.spacing(size === 'sm' ? 4 : 8)};
|
||||
margin: 0 ${({ theme }) => theme.spacing(1)};
|
||||
width: 1px;
|
||||
`;
|
||||
|
||||
export const RecordIndexActionMenuBarAllActionsButton = () => {
|
||||
const theme = useTheme();
|
||||
const { openCommandMenu } = useCommandMenu();
|
||||
return (
|
||||
<>
|
||||
<StyledSeparator size="md" />
|
||||
<StyledButton onClick={openCommandMenu}>
|
||||
<IconLayoutSidebarRightExpand size={theme.icon.size.md} />
|
||||
<StyledButtonLabel>All Actions</StyledButtonLabel>
|
||||
<StyledSeparator size="sm" />
|
||||
<StyledShortcutLabel>{getOsControlSymbol()}K</StyledShortcutLabel>
|
||||
</StyledButton>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -1,40 +0,0 @@
|
||||
import { ActionMenuEntry } from '@/action-menu/types/ActionMenuEntry';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { i18n } from '@lingui/core';
|
||||
|
||||
type RecordIndexActionMenuBarEntryProps = {
|
||||
entry: ActionMenuEntry;
|
||||
};
|
||||
|
||||
const StyledButton = styled.div`
|
||||
border-radius: ${({ theme }) => theme.border.radius.sm};
|
||||
color: ${({ theme }) => theme.font.color.secondary};
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: ${({ theme }) => theme.spacing(2)};
|
||||
transition: background ${({ theme }) => theme.animation.duration.fast} ease;
|
||||
user-select: none;
|
||||
&:hover {
|
||||
background: ${({ theme }) => theme.background.tertiary};
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledButtonLabel = styled.div`
|
||||
font-weight: ${({ theme }) => theme.font.weight.medium};
|
||||
margin-left: ${({ theme }) => theme.spacing(1)};
|
||||
`;
|
||||
|
||||
export const RecordIndexActionMenuBarEntry = ({
|
||||
entry,
|
||||
}: RecordIndexActionMenuBarEntryProps) => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<StyledButton onClick={() => entry.onClick?.()}>
|
||||
{entry.Icon && <entry.Icon size={theme.icon.size.md} />}
|
||||
<StyledButtonLabel>{i18n._(entry.label)}</StyledButtonLabel>
|
||||
</StyledButton>
|
||||
);
|
||||
};
|
||||
@ -1,21 +1,19 @@
|
||||
import { actionMenuEntriesComponentSelector } from '@/action-menu/states/actionMenuEntriesComponentSelector';
|
||||
import { ActionComponent } from '@/action-menu/actions/display/components/ActionComponent';
|
||||
import { ActionScope } from '@/action-menu/actions/types/ActionScope';
|
||||
import { ActionType } from '@/action-menu/actions/types/ActionType';
|
||||
import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext';
|
||||
import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext';
|
||||
import { recordIndexActionMenuDropdownPositionComponentState } from '@/action-menu/states/recordIndexActionMenuDropdownPositionComponentState';
|
||||
import { ActionMenuDropdownHotkeyScope } from '@/action-menu/types/ActionMenuDropdownHotKeyScope';
|
||||
import {
|
||||
ActionMenuEntryScope,
|
||||
ActionMenuEntryType,
|
||||
} from '@/action-menu/types/ActionMenuEntry';
|
||||
import { getActionMenuDropdownIdFromActionMenuId } from '@/action-menu/utils/getActionMenuDropdownIdFromActionMenuId';
|
||||
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
|
||||
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
|
||||
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';
|
||||
import styled from '@emotion/styled';
|
||||
import { i18n } from '@lingui/core';
|
||||
import { useContext } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { IconLayoutSidebarRightExpand } from 'twenty-ui/display';
|
||||
import { MenuItem } from 'twenty-ui/navigation';
|
||||
@ -31,14 +29,12 @@ const StyledDropdownMenuContainer = styled.div`
|
||||
`;
|
||||
|
||||
export const RecordIndexActionMenuDropdown = () => {
|
||||
const actionMenuEntries = useRecoilComponentValueV2(
|
||||
actionMenuEntriesComponentSelector,
|
||||
);
|
||||
const { actions } = useContext(ActionMenuContext);
|
||||
|
||||
const recordIndexActions = actionMenuEntries.filter(
|
||||
(actionMenuEntry) =>
|
||||
actionMenuEntry.type === ActionMenuEntryType.Standard &&
|
||||
actionMenuEntry.scope === ActionMenuEntryScope.RecordSelection,
|
||||
const recordIndexActions = actions.filter(
|
||||
(action) =>
|
||||
action.type === ActionType.Standard &&
|
||||
action.scope === ActionScope.RecordSelection,
|
||||
);
|
||||
|
||||
const actionMenuId = useAvailableComponentInstanceIdOrThrow(
|
||||
@ -57,14 +53,6 @@ export const RecordIndexActionMenuDropdown = () => {
|
||||
|
||||
const { openCommandMenu } = useCommandMenu();
|
||||
|
||||
//TODO: remove this
|
||||
const width = recordIndexActions.some(
|
||||
(actionMenuEntry) =>
|
||||
i18n._(actionMenuEntry.label) === 'Remove from favorites',
|
||||
)
|
||||
? 200
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
dropdownId={dropdownId}
|
||||
@ -72,7 +60,6 @@ export const RecordIndexActionMenuDropdown = () => {
|
||||
scope: ActionMenuDropdownHotkeyScope.ActionMenuDropdown,
|
||||
}}
|
||||
data-select-disable
|
||||
dropdownWidth={width}
|
||||
dropdownPlacement="bottom-start"
|
||||
dropdownStrategy="absolute"
|
||||
dropdownOffset={{
|
||||
@ -82,17 +69,8 @@ export const RecordIndexActionMenuDropdown = () => {
|
||||
dropdownComponents={
|
||||
<StyledDropdownMenuContainer className="action-menu-dropdown">
|
||||
<DropdownMenuItemsContainer>
|
||||
{recordIndexActions.map((item) => (
|
||||
<MenuItem
|
||||
key={item.key}
|
||||
LeftIcon={item.Icon}
|
||||
onClick={() => {
|
||||
closeDropdown();
|
||||
item.onClick?.();
|
||||
}}
|
||||
accent={item.accent}
|
||||
text={i18n._(item.label)}
|
||||
/>
|
||||
{recordIndexActions.map((action) => (
|
||||
<ActionComponent action={action} key={action.key} />
|
||||
))}
|
||||
<MenuItem
|
||||
key="more-actions"
|
||||
|
||||
@ -1,14 +1,8 @@
|
||||
import { RecordActionMenuEntriesSetter } from '@/action-menu/actions/record-actions/components/RecordActionMenuEntriesSetter';
|
||||
import { RecordAgnosticActionMenuEntriesSetter } from '@/action-menu/actions/record-agnostic-actions/components/RecordAgnosticActionMenuEntriesSetter';
|
||||
import { RunWorkflowRecordAgnosticActionMenuEntriesSetter } from '@/action-menu/actions/record-agnostic-actions/components/RunWorkflowRecordAgnosticActionMenuEntriesSetter';
|
||||
import { ActionMenuConfirmationModals } from '@/action-menu/components/ActionMenuConfirmationModals';
|
||||
import { PageHeaderActionMenuButtons } from '@/action-menu/components/PageHeaderActionMenuButtons';
|
||||
import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext';
|
||||
import { ActionMenuContextProvider } from '@/action-menu/contexts/ActionMenuContextProvider';
|
||||
import { contextStoreCurrentObjectMetadataItemIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataItemIdComponentState';
|
||||
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
import { FeatureFlagKey } from '~/generated/graphql';
|
||||
import { useIsMobile } from 'twenty-ui/utilities';
|
||||
|
||||
export const RecordShowActionMenu = () => {
|
||||
@ -24,29 +18,18 @@ export const RecordShowActionMenu = () => {
|
||||
contextStoreTargetedRecordsRule.mode === 'selection' &&
|
||||
contextStoreTargetedRecordsRule.selectedRecordIds.length === 1;
|
||||
|
||||
const isWorkflowEnabled = useIsFeatureEnabled(
|
||||
FeatureFlagKey.IsWorkflowEnabled,
|
||||
);
|
||||
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
return (
|
||||
<>
|
||||
{hasSelectedRecord && contextStoreCurrentObjectMetadataItemId && (
|
||||
<ActionMenuContext.Provider
|
||||
value={{
|
||||
isInRightDrawer: false,
|
||||
onActionExecutedCallback: () => {},
|
||||
}}
|
||||
<ActionMenuContextProvider
|
||||
isInRightDrawer={false}
|
||||
displayType="button"
|
||||
actionMenuType="show-page-action-menu"
|
||||
>
|
||||
{!isMobile && <PageHeaderActionMenuButtons />}
|
||||
<ActionMenuConfirmationModals />
|
||||
<RecordActionMenuEntriesSetter />
|
||||
<RecordAgnosticActionMenuEntriesSetter />
|
||||
{isWorkflowEnabled && (
|
||||
<RunWorkflowRecordAgnosticActionMenuEntriesSetter />
|
||||
)}
|
||||
</ActionMenuContext.Provider>
|
||||
</ActionMenuContextProvider>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
@ -1,62 +1,23 @@
|
||||
import { RecordActionMenuEntriesSetter } from '@/action-menu/actions/record-actions/components/RecordActionMenuEntriesSetter';
|
||||
import { SingleRecordActionKeys } from '@/action-menu/actions/record-actions/single-record/types/SingleRecordActionsKey';
|
||||
import { RecordAgnosticActionMenuEntriesSetter } from '@/action-menu/actions/record-agnostic-actions/components/RecordAgnosticActionMenuEntriesSetter';
|
||||
import { RunWorkflowRecordAgnosticActionMenuEntriesSetter } from '@/action-menu/actions/record-agnostic-actions/components/RunWorkflowRecordAgnosticActionMenuEntriesSetter';
|
||||
import { ActionMenuConfirmationModals } from '@/action-menu/components/ActionMenuConfirmationModals';
|
||||
import { CommandMenuActionMenuDropdown } from '@/action-menu/components/CommandMenuActionMenuDropdown';
|
||||
import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext';
|
||||
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
|
||||
import { ActionMenuContextProvider } from '@/action-menu/contexts/ActionMenuContextProvider';
|
||||
import { contextStoreCurrentObjectMetadataItemIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataItemIdComponentState';
|
||||
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
import { FeatureFlagKey } from '~/generated-metadata/graphql';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const RecordShowRightDrawerActionMenu = () => {
|
||||
const contextStoreCurrentObjectMetadataItemId = useRecoilComponentValueV2(
|
||||
contextStoreCurrentObjectMetadataItemIdComponentState,
|
||||
);
|
||||
|
||||
const contextStoreTargetedRecordsRule = useRecoilComponentValueV2(
|
||||
contextStoreTargetedRecordsRuleComponentState,
|
||||
);
|
||||
|
||||
const isWorkflowEnabled = useIsFeatureEnabled(
|
||||
FeatureFlagKey.IsWorkflowEnabled,
|
||||
);
|
||||
|
||||
const { toggleCommandMenu } = useCommandMenu();
|
||||
|
||||
return (
|
||||
<>
|
||||
{contextStoreCurrentObjectMetadataItemId && (
|
||||
<ActionMenuContext.Provider
|
||||
value={{
|
||||
isInRightDrawer: true,
|
||||
onActionExecutedCallback: ({ key }) => {
|
||||
if (
|
||||
key === SingleRecordActionKeys.DELETE ||
|
||||
key === SingleRecordActionKeys.DESTROY
|
||||
) {
|
||||
toggleCommandMenu();
|
||||
}
|
||||
},
|
||||
}}
|
||||
<ActionMenuContextProvider
|
||||
isInRightDrawer={true}
|
||||
displayType="dropdownItem"
|
||||
actionMenuType="command-menu-show-page-action-menu-dropdown"
|
||||
>
|
||||
<CommandMenuActionMenuDropdown />
|
||||
<ActionMenuConfirmationModals />
|
||||
|
||||
{isDefined(contextStoreTargetedRecordsRule) &&
|
||||
contextStoreTargetedRecordsRule.mode === 'selection' &&
|
||||
contextStoreTargetedRecordsRule.selectedRecordIds.length > 0 && (
|
||||
<RecordActionMenuEntriesSetter />
|
||||
)}
|
||||
<RecordAgnosticActionMenuEntriesSetter />
|
||||
{isWorkflowEnabled && (
|
||||
<RunWorkflowRecordAgnosticActionMenuEntriesSetter />
|
||||
)}
|
||||
</ActionMenuContext.Provider>
|
||||
</ActionMenuContextProvider>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
@ -3,25 +3,19 @@ import { Meta, StoryObj } from '@storybook/react';
|
||||
import { RecoilRoot } from 'recoil';
|
||||
|
||||
import { CommandMenuActionMenuDropdown } from '@/action-menu/components/CommandMenuActionMenuDropdown';
|
||||
import { actionMenuEntriesComponentState } from '@/action-menu/states/actionMenuEntriesComponentState';
|
||||
import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext';
|
||||
import { createMockActionMenuActions } from '@/action-menu/mock/action-menu-actions.mock';
|
||||
import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext';
|
||||
import {
|
||||
ActionMenuEntry,
|
||||
ActionMenuEntryScope,
|
||||
ActionMenuEntryType,
|
||||
} from '@/action-menu/types/ActionMenuEntry';
|
||||
import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState';
|
||||
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { userEvent, waitFor, within } from '@storybook/test';
|
||||
import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator';
|
||||
import {
|
||||
ComponentDecorator,
|
||||
RouterDecorator,
|
||||
getCanvasElementForDropdownTesting,
|
||||
} from 'twenty-ui/testing';
|
||||
import { IconFileExport, IconHeart, IconTrash } from 'twenty-ui/display';
|
||||
import { MenuItemAccent } from 'twenty-ui/navigation';
|
||||
|
||||
import { ContextStoreDecorator } from '~/testing/decorators/ContextStoreDecorator';
|
||||
import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator';
|
||||
const deleteMock = jest.fn();
|
||||
const addToFavoritesMock = jest.fn();
|
||||
const exportMock = jest.fn();
|
||||
@ -49,56 +43,31 @@ const meta: Meta<typeof CommandMenuActionMenuDropdown> = {
|
||||
}),
|
||||
1,
|
||||
);
|
||||
|
||||
const map = new Map<string, ActionMenuEntry>();
|
||||
|
||||
set(
|
||||
actionMenuEntriesComponentState.atomFamily({
|
||||
instanceId: 'story-action-menu',
|
||||
}),
|
||||
map,
|
||||
);
|
||||
|
||||
map.set('addToFavorites', {
|
||||
type: ActionMenuEntryType.Standard,
|
||||
scope: ActionMenuEntryScope.RecordSelection,
|
||||
key: 'addToFavorites',
|
||||
label: msg`Add to favorites`,
|
||||
position: 0,
|
||||
Icon: IconHeart,
|
||||
onClick: addToFavoritesMock,
|
||||
});
|
||||
|
||||
map.set('export', {
|
||||
type: ActionMenuEntryType.Standard,
|
||||
scope: ActionMenuEntryScope.RecordSelection,
|
||||
key: 'export',
|
||||
label: msg`Export`,
|
||||
position: 1,
|
||||
Icon: IconFileExport,
|
||||
onClick: exportMock,
|
||||
});
|
||||
|
||||
map.set('delete', {
|
||||
type: ActionMenuEntryType.Standard,
|
||||
scope: ActionMenuEntryScope.RecordSelection,
|
||||
key: 'delete',
|
||||
label: msg`Delete`,
|
||||
position: 2,
|
||||
Icon: IconTrash,
|
||||
onClick: deleteMock,
|
||||
accent: 'danger' as MenuItemAccent,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<ActionMenuComponentInstanceContext.Provider
|
||||
value={{ instanceId: 'story-action-menu' }}
|
||||
>
|
||||
<Story />
|
||||
<ActionMenuContext.Provider
|
||||
value={{
|
||||
isInRightDrawer: true,
|
||||
displayType: 'dropdownItem',
|
||||
actionMenuType: 'command-menu-show-page-action-menu-dropdown',
|
||||
actions: createMockActionMenuActions({
|
||||
deleteMock,
|
||||
addToFavoritesMock,
|
||||
exportMock,
|
||||
}),
|
||||
}}
|
||||
>
|
||||
<Story />
|
||||
</ActionMenuContext.Provider>
|
||||
</ActionMenuComponentInstanceContext.Provider>
|
||||
</RecoilRoot>
|
||||
),
|
||||
ComponentDecorator,
|
||||
ContextStoreDecorator,
|
||||
RouterDecorator,
|
||||
],
|
||||
args: {
|
||||
actionMenuId: 'story-action-menu',
|
||||
|
||||
@ -1,73 +0,0 @@
|
||||
import { RecordIndexActionMenuBarEntry } from '@/action-menu/components/RecordIndexActionMenuBarEntry';
|
||||
import {
|
||||
ActionMenuEntryScope,
|
||||
ActionMenuEntryType,
|
||||
} from '@/action-menu/types/ActionMenuEntry';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { expect, jest } from '@storybook/jest';
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
import { userEvent, within } from '@storybook/testing-library';
|
||||
import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator';
|
||||
import { ComponentDecorator } from 'twenty-ui/testing';
|
||||
import { IconCheckbox, IconTrash } from 'twenty-ui/display';
|
||||
|
||||
const meta: Meta<typeof RecordIndexActionMenuBarEntry> = {
|
||||
title: 'Modules/ActionMenu/RecordIndexActionMenuBarEntry',
|
||||
component: RecordIndexActionMenuBarEntry,
|
||||
decorators: [ComponentDecorator, I18nFrontDecorator],
|
||||
};
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof RecordIndexActionMenuBarEntry>;
|
||||
|
||||
const deleteMock = jest.fn();
|
||||
const markAsDoneMock = jest.fn();
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
entry: {
|
||||
type: ActionMenuEntryType.Standard,
|
||||
scope: ActionMenuEntryScope.RecordSelection,
|
||||
key: 'delete',
|
||||
label: msg`Delete`,
|
||||
position: 0,
|
||||
Icon: IconTrash,
|
||||
onClick: deleteMock,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const WithDangerAccent: Story = {
|
||||
args: {
|
||||
entry: {
|
||||
type: ActionMenuEntryType.Standard,
|
||||
scope: ActionMenuEntryScope.RecordSelection,
|
||||
key: 'delete',
|
||||
label: msg`Delete`,
|
||||
position: 0,
|
||||
Icon: IconTrash,
|
||||
onClick: deleteMock,
|
||||
accent: 'danger',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const WithInteraction: Story = {
|
||||
args: {
|
||||
entry: {
|
||||
type: ActionMenuEntryType.Standard,
|
||||
scope: ActionMenuEntryScope.RecordSelection,
|
||||
key: 'markAsDone',
|
||||
label: msg`Mark as done`,
|
||||
position: 0,
|
||||
Icon: IconCheckbox,
|
||||
onClick: markAsDoneMock,
|
||||
},
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
const button = await canvas.findByText('Mark as done');
|
||||
await userEvent.click(button);
|
||||
expect(markAsDoneMock).toHaveBeenCalled();
|
||||
},
|
||||
};
|
||||
@ -1,27 +1,25 @@
|
||||
import { expect, jest } from '@storybook/jest';
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
import { userEvent, within } from '@storybook/testing-library';
|
||||
import { userEvent, waitFor, within } from '@storybook/testing-library';
|
||||
import { RecoilRoot } from 'recoil';
|
||||
|
||||
import { RecordIndexActionMenuDropdown } from '@/action-menu/components/RecordIndexActionMenuDropdown';
|
||||
import { actionMenuEntriesComponentState } from '@/action-menu/states/actionMenuEntriesComponentState';
|
||||
import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext';
|
||||
import { createMockActionMenuActions } from '@/action-menu/mock/action-menu-actions.mock';
|
||||
import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext';
|
||||
import { recordIndexActionMenuDropdownPositionComponentState } from '@/action-menu/states/recordIndexActionMenuDropdownPositionComponentState';
|
||||
import {
|
||||
ActionMenuEntry,
|
||||
ActionMenuEntryScope,
|
||||
ActionMenuEntryType,
|
||||
} from '@/action-menu/types/ActionMenuEntry';
|
||||
import { isDropdownOpenComponentState } from '@/ui/layout/dropdown/states/isDropdownOpenComponentState';
|
||||
import { extractComponentState } from '@/ui/utilities/state/component-state/utils/extractComponentState';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import {
|
||||
RouterDecorator,
|
||||
getCanvasElementForDropdownTesting,
|
||||
} from 'twenty-ui/testing';
|
||||
import { ContextStoreDecorator } from '~/testing/decorators/ContextStoreDecorator';
|
||||
import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator';
|
||||
import { IconCheckbox, IconHeart, IconTrash } from 'twenty-ui/display';
|
||||
import { getCanvasElementForDropdownTesting } from 'twenty-ui/testing';
|
||||
|
||||
const deleteMock = jest.fn();
|
||||
const markAsDoneMock = jest.fn();
|
||||
const addToFavoritesMock = jest.fn();
|
||||
const exportMock = jest.fn();
|
||||
|
||||
const meta: Meta<typeof RecordIndexActionMenuDropdown> = {
|
||||
title: 'Modules/ActionMenu/RecordIndexActionMenuDropdown',
|
||||
@ -39,45 +37,6 @@ const meta: Meta<typeof RecordIndexActionMenuDropdown> = {
|
||||
{ x: 10, y: 10 },
|
||||
);
|
||||
|
||||
const map = new Map<string, ActionMenuEntry>();
|
||||
|
||||
set(
|
||||
actionMenuEntriesComponentState.atomFamily({
|
||||
instanceId: 'story-action-menu',
|
||||
}),
|
||||
map,
|
||||
);
|
||||
|
||||
map.set('delete', {
|
||||
type: ActionMenuEntryType.Standard,
|
||||
scope: ActionMenuEntryScope.RecordSelection,
|
||||
key: 'delete',
|
||||
label: msg`Delete`,
|
||||
position: 0,
|
||||
Icon: IconTrash,
|
||||
onClick: deleteMock,
|
||||
});
|
||||
|
||||
map.set('markAsDone', {
|
||||
type: ActionMenuEntryType.Standard,
|
||||
scope: ActionMenuEntryScope.RecordSelection,
|
||||
key: 'markAsDone',
|
||||
label: msg`Mark as done`,
|
||||
position: 1,
|
||||
Icon: IconCheckbox,
|
||||
onClick: markAsDoneMock,
|
||||
});
|
||||
|
||||
map.set('addToFavorites', {
|
||||
type: ActionMenuEntryType.Standard,
|
||||
scope: ActionMenuEntryScope.RecordSelection,
|
||||
key: 'addToFavorites',
|
||||
label: msg`Add to favorites`,
|
||||
position: 2,
|
||||
Icon: IconHeart,
|
||||
onClick: addToFavoritesMock,
|
||||
});
|
||||
|
||||
set(
|
||||
extractComponentState(
|
||||
isDropdownOpenComponentState,
|
||||
@ -90,10 +49,25 @@ const meta: Meta<typeof RecordIndexActionMenuDropdown> = {
|
||||
<ActionMenuComponentInstanceContext.Provider
|
||||
value={{ instanceId: 'story-action-menu' }}
|
||||
>
|
||||
<Story />
|
||||
<ActionMenuContext.Provider
|
||||
value={{
|
||||
isInRightDrawer: true,
|
||||
displayType: 'dropdownItem',
|
||||
actionMenuType: 'index-page-action-menu-dropdown',
|
||||
actions: createMockActionMenuActions({
|
||||
deleteMock,
|
||||
addToFavoritesMock,
|
||||
exportMock,
|
||||
}),
|
||||
}}
|
||||
>
|
||||
<Story />
|
||||
</ActionMenuContext.Provider>
|
||||
</ActionMenuComponentInstanceContext.Provider>
|
||||
</RecoilRoot>
|
||||
),
|
||||
ContextStoreDecorator,
|
||||
RouterDecorator,
|
||||
],
|
||||
};
|
||||
|
||||
@ -112,12 +86,24 @@ export const WithInteractions: Story = {
|
||||
actionMenuId: 'story',
|
||||
},
|
||||
play: async () => {
|
||||
const canvasElement = getCanvasElementForDropdownTesting();
|
||||
|
||||
const canvas = within(canvasElement);
|
||||
const canvas = within(getCanvasElementForDropdownTesting());
|
||||
|
||||
const deleteButton = await canvas.findByText('Delete');
|
||||
await userEvent.click(deleteButton);
|
||||
expect(deleteMock).toHaveBeenCalled();
|
||||
|
||||
const addToFavoritesButton = await canvas.findByText('Add to favorites');
|
||||
await userEvent.click(addToFavoritesButton);
|
||||
|
||||
const exportButton = await canvas.findByText('Export');
|
||||
await userEvent.click(exportButton);
|
||||
|
||||
const moreActionsButton = await canvas.findByText('More actions');
|
||||
|
||||
await waitFor(() => {
|
||||
expect(deleteMock).toHaveBeenCalled();
|
||||
expect(addToFavoritesMock).toHaveBeenCalled();
|
||||
expect(exportMock).toHaveBeenCalled();
|
||||
expect(moreActionsButton).toBeInTheDocument();
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user