Refactor useDropdownV2 (#12875)

# What this PR does

This PR introduces what’s needed to progressively refactor the
useDropdown hooks we have.

It removes useDropdownV2 and renames useDropdown used for multi-page
dropdowns to useDropdownContextStateManagement

## The 3 useDropdown hooks

There are currently 3 useDropdown hooks : 

One is used for managing states in some dropdowns that have multiple
inner pages with contexts and without recoil. It is limited and would
need a separate refactoring, thus I just renamed it.

Then we have useDropdown and useDropdownV2, which followed our multiple
versions of recoil state management wrappers.

Now that the state management has been stabilized, we can merge
everything on the last version.

## What this refactor will allow next

This refactor will allow to refactor the legacy recoil state management,
because useDropdown is depending on legacy recoil states with scopeId.

Because there are only a dozen of those legacy states left, and the
dropdown’s ones are the harder to refactor, because swapping them as-is
causes a big QA, and if we have a big QA to do on all dropdowns, better
refactor the whole dropdown management and have everything clean.

After this refactor, we will be able to delete the legacy dropdown
states, and proceed with the other legacy states, then removing all the
legacy recoil state mangament.

## How do we allow progressive refactoring ?

There are many places where useDropdown is used.

To have an easier refactoring process, we want to merge multiple small
pull requests so that it is easier to QA and review.

For this we will maintain both legacy component state and component
state V2 in parallel for isDropdownOpen, so that the new hooks
`useOpenDropdown` , `useCloseDropdown` are doing the same thing than
`useDropdown` and `useDropdownV2` .

Thus for the moment, whether we use the legacy hooks or the new ones,
the effects are the same.

And we can have dropdowns operating on the old states and the new states
living side by side in the app.

## QA

Component | Status | Comments
-- | -- | --
CommandMenuActionMenuDropdown | Ok |  
RecordIndexActionMenuDropdown | Ok |  
RecordShowRightDrawerOpenRecordButton | Ok |  
useCloseActionMenu | Ok |  
CommandMenuContextChipGroups | Ok |  
useCommandMenuCloseAnimationCompleteCleanup | Ok |  
ObjectOptionsDropdown | Ok |  
ObjectOptionsDropdownContent | Ok |  
ObjectOptionsDropdownFieldsContent | Ok |  
ObjectOptionsDropdownHiddenFieldsContent | Ok |  
ObjectOptionsDropdownHiddenRecordGroupsContent | Ok |  
ObjectOptionsDropdownLayoutContent | Ok |  
ObjectOptionsDropdownLayoutOpenInContent | Ok |  
ObjectOptionsDropdownMenuContent | Ok |  
ObjectOptionsDropdownRecordGroupFieldsContent | Ok |  
ObjectOptionsDropdownRecordGroupsContent | Ok |  
ObjectOptionsDropdownRecordGroupSortContent | Ok |  
RecordBoardColumnHeaderAggregateDropdown | Ok |  
AggregateDropdownContent | Ok |  
RecordBoardColumnHeaderAggregateDropdownFieldsContent | Ok |  
RecordBoardColumnHeaderAggregateDropdownMenuContent | Ok |  
RecordBoardColumnHeaderAggregateDropdownMenuContent | Ok |  
RecordBoardColumnHeaderAggregateDropdownOptionsContent | Ok |  
RecordBoard | Ok | Used closeAnyOpenDropdown instead for a better UX
RecordBoardCard | Ok |  
useRecordBoardSelection | Ok |  
RecordTableColumnAggregateFooterDropdownContent | Ok |  
RecordTableColumnFooterWithDropdown | Ok |  
useOpenRecordFilterChipFromTableHeader | Ok |  
useCloseAnyOpenDropdown | Ok |  
useCloseDropdownFromOutside | Removed | Removed because unused
useDropdownV2 | Removed | Removed because all calls have been removed
useOpenDropdownFromOutside | Removed | Removed because unused
useCloseAndResetViewPicker | Ok |  
WorkflowVariablesDropdown | Ok |
This commit is contained in:
Lucas Bordeau
2025-06-25 17:16:56 +02:00
committed by GitHub
parent 74f53cc759
commit 22b4595b0b
55 changed files with 399 additions and 267 deletions

View File

@ -8,7 +8,7 @@ import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { DropdownHotkeyScope } from '@/ui/layout/dropdown/constants/DropdownHotkeyScope';
import { useDropdownV2 } from '@/ui/layout/dropdown/hooks/useDropdownV2';
import { useToggleDropdown } from '@/ui/layout/dropdown/hooks/useToggleDropdown';
import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList';
import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList';
import { useHotkeysOnFocusedElement } from '@/ui/utilities/hotkey/hooks/useHotkeysOnFocusedElement';
@ -30,12 +30,14 @@ export const CommandMenuActionMenuDropdown = () => {
const dropdownId =
getRightDrawerActionMenuDropdownIdFromActionMenuId(actionMenuId);
const { toggleDropdown } = useDropdownV2();
const { toggleDropdown } = useToggleDropdown();
const hotkeysConfig = {
keys: ['ctrl+o', 'meta+o'],
callback: () => {
toggleDropdown(dropdownId);
toggleDropdown({
dropdownComponentInstanceIdFromProps: dropdownId,
});
},
scope: AppHotkeyScope.CommandMenuOpen,
dependencies: [toggleDropdown],

View File

@ -11,7 +11,7 @@ import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { DropdownHotkeyScope } from '@/ui/layout/dropdown/constants/DropdownHotkeyScope';
import { useDropdownV2 } from '@/ui/layout/dropdown/hooks/useDropdownV2';
import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList';
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
import { selectedItemIdComponentState } from '@/ui/layout/selectable-list/states/selectedItemIdComponentState';
@ -48,7 +48,7 @@ export const RecordIndexActionMenuDropdown = () => {
);
const dropdownId = getActionMenuDropdownIdFromActionMenuId(actionMenuId);
const { closeDropdown } = useDropdownV2();
const { closeDropdown } = useCloseDropdown();
const actionMenuDropdownPosition = useRecoilValue(
extractComponentState(

View File

@ -9,7 +9,7 @@ import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSi
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { AppPath } from '@/types/AppPath';
import { useDropdownV2 } from '@/ui/layout/dropdown/hooks/useDropdownV2';
import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
import { getShowPageTabListComponentId } from '@/ui/layout/show-page/utils/getShowPageTabListComponentId';
import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
import { useHotkeysOnFocusedElement } from '@/ui/utilities/hotkey/hooks/useHotkeysOnFocusedElement';
@ -74,7 +74,7 @@ export const RecordShowRightDrawerOpenRecordButton = ({
ActionMenuComponentInstanceContext,
);
const { closeDropdown } = useDropdownV2();
const { closeDropdown } = useCloseDropdown();
const handleOpenRecord = useRecoilCallback(
({ snapshot, reset }) =>

View File

@ -3,7 +3,7 @@ import { ActionMenuComponentInstanceContext } from '@/action-menu/states/context
import { getActionMenuDropdownIdFromActionMenuId } from '@/action-menu/utils/getActionMenuDropdownIdFromActionMenuId';
import { getRightDrawerActionMenuDropdownIdFromActionMenuId } from '@/action-menu/utils/getRightDrawerActionMenuDropdownIdFromActionMenuId';
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
import { useDropdownV2 } from '@/ui/layout/dropdown/hooks/useDropdownV2';
import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useContext } from 'react';
import { isDefined } from 'twenty-shared/utils';
@ -19,7 +19,7 @@ export const useCloseActionMenu = ({
const { closeCommandMenu } = useCommandMenu();
const { closeDropdown } = useDropdownV2();
const { closeDropdown } = useCloseDropdown();
const actionMenuId = useAvailableComponentInstanceIdOrThrow(
ActionMenuComponentInstanceContext,

View File

@ -2,7 +2,7 @@ import { COMMAND_MENU_CONTEXT_CHIP_GROUPS_DROPDOWN_ID } from '@/command-menu/con
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { useDropdownV2 } from '@/ui/layout/dropdown/hooks/useDropdownV2';
import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
import { isDefined } from 'twenty-shared/utils';
import { MenuItem } from 'twenty-ui/navigation';
import {
@ -15,7 +15,7 @@ export const CommandMenuContextChipGroups = ({
}: {
contextChips: CommandMenuContextChipProps[];
}) => {
const { closeDropdown } = useDropdownV2();
const { closeDropdown } = useCloseDropdown();
if (contextChips.length === 0) {
return null;

View File

@ -25,8 +25,8 @@ const mockResetContextStoreStates = jest.fn();
const mockResetSelectedItem = jest.fn();
const mockEmitSidePanelCloseEvent = jest.fn();
jest.mock('@/ui/layout/dropdown/hooks/useDropdownV2', () => ({
useDropdownV2: () => ({
jest.mock('@/ui/layout/dropdown/hooks/useCloseDropdown', () => ({
useCloseDropdown: () => ({
closeDropdown: mockCloseDropdown,
}),
}));

View File

@ -13,7 +13,7 @@ import { isCommandMenuClosingState } from '@/command-menu/states/isCommandMenuCl
import { isCommandMenuOpenedState } from '@/command-menu/states/isCommandMenuOpenedState';
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
import { viewableRecordIdState } from '@/object-record/record-right-drawer/states/viewableRecordIdState';
import { useDropdownV2 } from '@/ui/layout/dropdown/hooks/useDropdownV2';
import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
import { emitSidePanelCloseEvent } from '@/ui/layout/right-drawer/utils/emitSidePanelCloseEvent';
import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList';
import { getShowPageTabListComponentId } from '@/ui/layout/show-page/utils/getShowPageTabListComponentId';
@ -27,7 +27,7 @@ export const useCommandMenuCloseAnimationCompleteCleanup = () => {
const { resetContextStoreStates } = useResetContextStoreStates();
const { closeDropdown } = useDropdownV2();
const { closeDropdown } = useCloseDropdown();
const commandMenuCloseAnimationCompleteCleanup = useRecoilCallback(
({ snapshot, set }) =>

View File

@ -1,6 +1,6 @@
import { useCallback, useState } from 'react';
export const useCurrentContentId = <T>() => {
export const useDropdownContextCurrentContentId = <T>() => {
const [currentContentId, setCurrentContentId] = useState<T | null>(null);
const [previousContentId, setPreviousContentId] = useState<T | null>(null);

View File

@ -4,7 +4,13 @@ import { RecordTableColumnAggregateFooterDropdownContextValue } from '@/object-r
import { useDropdown as useDropdownUi } from '@/ui/layout/dropdown/hooks/useDropdown';
import { Context, useCallback, useContext } from 'react';
export const useDropdown = <
/**
*
* @deprecated This hook is deprecated because it uses context instead of recoil and synchronous hooks like we do in the application
*
* TODO: refactor this generic way to handle multiple pages in a dropdown with state management and specific code paths in a dedicated module, instead of using context with generic union types.
*/
export const useDropdownContextStateManagement = <
T extends
| RecordBoardColumnHeaderAggregateDropdownContextValue
| RecordTableColumnAggregateFooterDropdownContextValue
@ -18,7 +24,7 @@ export const useDropdown = <
if (!dropdownContext) {
throw new Error(
`useDropdown must be used within a context provider (${context.Provider.name})`,
`useDropdownContextStateManagement must be used within a context provider (${context.Provider.name})`,
);
}
const dropdownId = dropdownContext.dropdownId;

View File

@ -1,5 +1,4 @@
import { DROPDOWN_OFFSET_Y } from '@/dropdown/constants/DropdownOffsetY';
import { useCurrentContentId } from '@/dropdown/hooks/useCurrentContentId';
import { useDropdownContextCurrentContentId } from '@/dropdown-context-state-management/hooks/useDropdownContextCurrentContentId';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { ObjectOptionsDropdownContent } from '@/object-record/object-options-dropdown/components/ObjectOptionsDropdownContent';
import { OBJECT_OPTIONS_DROPDOWN_ID } from '@/object-record/object-options-dropdown/constants/ObjectOptionsDropdownId';
@ -9,6 +8,7 @@ import { RecordGroupReorderConfirmationModal } from '@/object-record/record-grou
import { useRecordGroupReorderConfirmationModal } from '@/object-record/record-group/hooks/useRecordGroupReorderConfirmationModal';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { StyledHeaderDropdownButton } from '@/ui/layout/dropdown/components/StyledHeaderDropdownButton';
import { DROPDOWN_OFFSET_Y } from '@/ui/layout/dropdown/constants/DropdownOffsetY';
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { ViewType } from '@/views/types/ViewType';
import { Trans } from '@lingui/react/macro';
@ -25,7 +25,7 @@ export const ObjectOptionsDropdown = ({
viewType,
}: ObjectOptionsDropdownProps) => {
const { currentContentId, handleContentChange, handleResetContent } =
useCurrentContentId<ObjectOptionsContentId>();
useDropdownContextCurrentContentId<ObjectOptionsContentId>();
const { isDropdownOpen } = useDropdown(OBJECT_OPTIONS_DROPDOWN_ID);
const {

View File

@ -7,10 +7,10 @@ import { ObjectOptionsDropdownMenuContent } from '@/object-record/object-options
import { ObjectOptionsDropdownRecordGroupFieldsContent } from '@/object-record/object-options-dropdown/components/ObjectOptionsDropdownRecordGroupFieldsContent';
import { ObjectOptionsDropdownRecordGroupsContent } from '@/object-record/object-options-dropdown/components/ObjectOptionsDropdownRecordGroupsContent';
import { ObjectOptionsDropdownRecordGroupSortContent } from '@/object-record/object-options-dropdown/components/ObjectOptionsDropdownRecordGroupSortContent';
import { useOptionsDropdown } from '@/object-record/object-options-dropdown/hooks/useOptionsDropdown';
import { useObjectOptionsDropdown } from '@/object-record/object-options-dropdown/hooks/useObjectOptionsDropdown';
export const ObjectOptionsDropdownContent = () => {
const { currentContentId } = useOptionsDropdown();
const { currentContentId } = useObjectOptionsDropdown();
switch (currentContentId) {
case 'layout':

View File

@ -1,6 +1,6 @@
import { useObjectOptionsDropdown } from '@/object-record/object-options-dropdown/hooks/useObjectOptionsDropdown';
import { useObjectOptionsForBoard } from '@/object-record/object-options-dropdown/hooks/useObjectOptionsForBoard';
import { useObjectOptionsForTable } from '@/object-record/object-options-dropdown/hooks/useObjectOptionsForTable';
import { useOptionsDropdown } from '@/object-record/object-options-dropdown/hooks/useOptionsDropdown';
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader/DropdownMenuHeader';
import { DropdownMenuHeaderLeftComponent } from '@/ui/layout/dropdown/components/DropdownMenuHeader/internal/DropdownMenuHeaderLeftComponent';
@ -20,7 +20,7 @@ export const ObjectOptionsDropdownFieldsContent = () => {
objectMetadataItem,
onContentChange,
resetContent,
} = useOptionsDropdown();
} = useObjectOptionsDropdown();
const {
handleColumnVisibilityChange,

View File

@ -3,9 +3,9 @@ import { useSetRecoilState } from 'recoil';
import { useObjectNamePluralFromSingular } from '@/object-metadata/hooks/useObjectNamePluralFromSingular';
import { useObjectOptionsDropdown } from '@/object-record/object-options-dropdown/hooks/useObjectOptionsDropdown';
import { useObjectOptionsForBoard } from '@/object-record/object-options-dropdown/hooks/useObjectOptionsForBoard';
import { useObjectOptionsForTable } from '@/object-record/object-options-dropdown/hooks/useObjectOptionsForTable';
import { useOptionsDropdown } from '@/object-record/object-options-dropdown/hooks/useOptionsDropdown';
import { SettingsPath } from '@/types/SettingsPath';
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader/DropdownMenuHeader';
@ -28,7 +28,7 @@ export const ObjectOptionsDropdownHiddenFieldsContent = () => {
objectMetadataItem,
onContentChange,
closeDropdown,
} = useOptionsDropdown();
} = useObjectOptionsDropdown();
const { objectNamePlural } = useObjectNamePluralFromSingular({
objectNameSingular: objectMetadataItem.nameSingular,

View File

@ -2,7 +2,7 @@ import { useEffect } from 'react';
import { useObjectNamePluralFromSingular } from '@/object-metadata/hooks/useObjectNamePluralFromSingular';
import { useOptionsDropdown } from '@/object-record/object-options-dropdown/hooks/useOptionsDropdown';
import { useObjectOptionsDropdown } from '@/object-record/object-options-dropdown/hooks/useObjectOptionsDropdown';
import { RecordGroupsVisibilityDropdownSection } from '@/object-record/record-group/components/RecordGroupsVisibilityDropdownSection';
import { useRecordGroupVisibility } from '@/object-record/record-group/hooks/useRecordGroupVisibility';
import { recordGroupFieldMetadataComponentState } from '@/object-record/record-group/states/recordGroupFieldMetadataComponentState';
@ -30,7 +30,7 @@ export const ObjectOptionsDropdownHiddenRecordGroupsContent = () => {
objectMetadataItem,
onContentChange,
closeDropdown,
} = useOptionsDropdown();
} = useObjectOptionsDropdown();
const recordGroupFieldMetadata = useRecoilComponentValueV2(
recordGroupFieldMetadataComponentState,

View File

@ -1,6 +1,6 @@
import { OBJECT_OPTIONS_DROPDOWN_ID } from '@/object-record/object-options-dropdown/constants/ObjectOptionsDropdownId';
import { useObjectOptionsDropdown } from '@/object-record/object-options-dropdown/hooks/useObjectOptionsDropdown';
import { useObjectOptionsForBoard } from '@/object-record/object-options-dropdown/hooks/useObjectOptionsForBoard';
import { useOptionsDropdown } from '@/object-record/object-options-dropdown/hooks/useOptionsDropdown';
import { useSetViewTypeFromLayoutOptionsMenu } from '@/object-record/object-options-dropdown/hooks/useSetViewTypeFromLayoutOptionsMenu';
import { recordGroupFieldMetadataComponentState } from '@/object-record/record-group/states/recordGroupFieldMetadataComponentState';
import { recordIndexOpenRecordInState } from '@/object-record/record-index/states/recordIndexOpenRecordInState';
@ -43,7 +43,7 @@ export const ObjectOptionsDropdownLayoutContent = () => {
resetContent,
onContentChange,
dropdownId,
} = useOptionsDropdown();
} = useObjectOptionsDropdown();
const { isCompactModeActive, setAndPersistIsCompactModeActive } =
useObjectOptionsForBoard({

View File

@ -1,5 +1,5 @@
import { OBJECT_OPTIONS_DROPDOWN_ID } from '@/object-record/object-options-dropdown/constants/ObjectOptionsDropdownId';
import { useOptionsDropdown } from '@/object-record/object-options-dropdown/hooks/useOptionsDropdown';
import { useObjectOptionsDropdown } from '@/object-record/object-options-dropdown/hooks/useObjectOptionsDropdown';
import { useUpdateObjectViewOptions } from '@/object-record/object-options-dropdown/hooks/useUpdateObjectViewOptions';
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
import { recordIndexOpenRecordInState } from '@/object-record/record-index/states/recordIndexOpenRecordInState';
@ -25,7 +25,7 @@ import {
import { MenuItemSelect } from 'twenty-ui/navigation';
export const ObjectOptionsDropdownLayoutOpenInContent = () => {
const { onContentChange } = useOptionsDropdown();
const { onContentChange } = useObjectOptionsDropdown();
const recordIndexOpenRecordIn = useRecoilValue(recordIndexOpenRecordInState);
const { currentView } = useGetCurrentViewOnly();
const { setAndPersistOpenRecordIn } = useUpdateObjectViewOptions();

View File

@ -1,7 +1,7 @@
import { ObjectOptionsDropdownMenuViewName } from '@/object-record/object-options-dropdown/components/ObjectOptionsDropdownMenuViewName';
import { OBJECT_OPTIONS_DROPDOWN_ID } from '@/object-record/object-options-dropdown/constants/ObjectOptionsDropdownId';
import { useObjectOptionsDropdown } from '@/object-record/object-options-dropdown/hooks/useObjectOptionsDropdown';
import { useObjectOptionsForBoard } from '@/object-record/object-options-dropdown/hooks/useObjectOptionsForBoard';
import { useOptionsDropdown } from '@/object-record/object-options-dropdown/hooks/useOptionsDropdown';
import { recordGroupFieldMetadataComponentState } from '@/object-record/record-group/states/recordGroupFieldMetadataComponentState';
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
@ -33,7 +33,7 @@ import { MenuItem } from 'twenty-ui/navigation';
export const ObjectOptionsDropdownMenuContent = () => {
const { t } = useLingui();
const { recordIndexId, objectMetadataItem, onContentChange, closeDropdown } =
useOptionsDropdown();
useObjectOptionsDropdown();
const { currentView } = useGetCurrentViewOnly();

View File

@ -3,7 +3,7 @@ import { useEffect } from 'react';
import { useObjectNamePluralFromSingular } from '@/object-metadata/hooks/useObjectNamePluralFromSingular';
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { useOptionsDropdown } from '@/object-record/object-options-dropdown/hooks/useOptionsDropdown';
import { useObjectOptionsDropdown } from '@/object-record/object-options-dropdown/hooks/useObjectOptionsDropdown';
import { useSearchRecordGroupField } from '@/object-record/object-options-dropdown/hooks/useSearchRecordGroupField';
import { recordGroupFieldMetadataComponentState } from '@/object-record/record-group/states/recordGroupFieldMetadataComponentState';
import { hiddenRecordGroupIdsComponentSelector } from '@/object-record/record-group/states/selectors/hiddenRecordGroupIdsComponentSelector';
@ -42,7 +42,7 @@ export const ObjectOptionsDropdownRecordGroupFieldsContent = () => {
onContentChange,
resetContent,
closeDropdown,
} = useOptionsDropdown();
} = useObjectOptionsDropdown();
const { objectNamePlural } = useObjectNamePluralFromSingular({
objectNameSingular: objectMetadataItem.nameSingular,

View File

@ -1,7 +1,7 @@
import { useEffect } from 'react';
import { OBJECT_OPTIONS_DROPDOWN_ID } from '@/object-record/object-options-dropdown/constants/ObjectOptionsDropdownId';
import { useOptionsDropdown } from '@/object-record/object-options-dropdown/hooks/useOptionsDropdown';
import { useObjectOptionsDropdown } from '@/object-record/object-options-dropdown/hooks/useObjectOptionsDropdown';
import { hiddenRecordGroupIdsComponentSelector } from '@/object-record/record-group/states/selectors/hiddenRecordGroupIdsComponentSelector';
import { RecordGroupSort } from '@/object-record/record-group/types/RecordGroupSort';
import { recordIndexRecordGroupSortComponentState } from '@/object-record/record-index/states/recordIndexRecordGroupSortComponentState';
@ -24,7 +24,7 @@ import {
import { MenuItemSelect } from 'twenty-ui/navigation';
export const ObjectOptionsDropdownRecordGroupSortContent = () => {
const { currentContentId, onContentChange } = useOptionsDropdown();
const { currentContentId, onContentChange } = useObjectOptionsDropdown();
const hiddenRecordGroupIds = useRecoilComponentValueV2(
hiddenRecordGroupIdsComponentSelector,

View File

@ -1,7 +1,7 @@
import { useEffect } from 'react';
import { OBJECT_OPTIONS_DROPDOWN_ID } from '@/object-record/object-options-dropdown/constants/ObjectOptionsDropdownId';
import { useOptionsDropdown } from '@/object-record/object-options-dropdown/hooks/useOptionsDropdown';
import { useObjectOptionsDropdown } from '@/object-record/object-options-dropdown/hooks/useObjectOptionsDropdown';
import { RecordGroupsVisibilityDropdownSection } from '@/object-record/record-group/components/RecordGroupsVisibilityDropdownSection';
import { useRecordGroupVisibility } from '@/object-record/record-group/hooks/useRecordGroupVisibility';
import { recordGroupFieldMetadataComponentState } from '@/object-record/record-group/states/recordGroupFieldMetadataComponentState';
@ -43,7 +43,7 @@ export const ObjectOptionsDropdownRecordGroupsContent = () => {
onContentChange,
resetContent,
handleRecordGroupOrderChangeWithModal,
} = useOptionsDropdown();
} = useObjectOptionsDropdown();
const { currentView } = useGetCurrentViewOnly();

View File

@ -3,7 +3,7 @@ import { act } from 'react';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { OBJECT_OPTIONS_DROPDOWN_ID } from '@/object-record/object-options-dropdown/constants/ObjectOptionsDropdownId';
import { useOptionsDropdown } from '@/object-record/object-options-dropdown/hooks/useOptionsDropdown';
import { useObjectOptionsDropdown } from '@/object-record/object-options-dropdown/hooks/useObjectOptionsDropdown';
import { ObjectOptionsDropdownContext } from '@/object-record/object-options-dropdown/states/contexts/ObjectOptionsDropdownContext';
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { ViewType } from '@/views/types/ViewType';
@ -61,7 +61,7 @@ describe('useOptionsDropdown', () => {
{children}
</ObjectOptionsDropdownContext.Provider>
);
return renderHook(() => useOptionsDropdown(), { wrapper });
return renderHook(() => useObjectOptionsDropdown(), { wrapper });
};
it('provides closeDropdown functionality from the context', () => {

View File

@ -1,15 +1,15 @@
import { useDropdown } from '@/dropdown/hooks/useDropdown';
import { useDropdownContextStateManagement } from '@/dropdown-context-state-management/hooks/useDropdownContextStateManagement';
import { ObjectOptionsDropdownContext } from '@/object-record/object-options-dropdown/states/contexts/ObjectOptionsDropdownContext';
import { useContext } from 'react';
export const useOptionsDropdown = () => {
export const useObjectOptionsDropdown = () => {
const context = useContext(ObjectOptionsDropdownContext);
if (!context) {
throw new Error('useOptionsDropdown must be used within a context');
throw new Error('useObjectOptionsDropdown must be used within a context');
}
const { closeDropdown } = useDropdown({
const { closeDropdown } = useDropdownContextStateManagement({
context: ObjectOptionsDropdownContext,
});

View File

@ -4,7 +4,6 @@ import { useContext, useRef } from 'react';
import { useRecoilCallback } from 'recoil';
import { ACTION_MENU_DROPDOWN_CLICK_OUTSIDE_ID } from '@/action-menu/constants/ActionMenuDropdownClickOutsideId';
import { getActionMenuIdFromRecordIndexId } from '@/action-menu/utils/getActionMenuIdFromRecordIndexId';
import { COMMAND_MENU_CLICK_OUTSIDE_ID } from '@/command-menu/constants/CommandMenuClickOutsideId';
import { RecordBoardHeader } from '@/object-record/record-board/components/RecordBoardHeader';
import { RecordBoardScrollToFocusedCardEffect } from '@/object-record/record-board/components/RecordBoardScrollToFocusedCardEffect';
@ -26,7 +25,7 @@ import { RECORD_INDEX_REMOVE_SORTING_MODAL_ID } from '@/object-record/record-ind
import { recordIndexRecordIdsByGroupComponentFamilyState } from '@/object-record/record-index/states/recordIndexRecordIdsByGroupComponentFamilyState';
import { currentRecordSortsComponentState } from '@/object-record/record-sort/states/currentRecordSortsComponentState';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { useDropdownV2 } from '@/ui/layout/dropdown/hooks/useDropdownV2';
import { useCloseAnyOpenDropdown } from '@/ui/layout/dropdown/hooks/useCloseAnyOpenDropdown';
import { MODAL_BACKDROP_CLICK_OUTSIDE_ID } from '@/ui/layout/modal/constants/ModalBackdropClickOutsideId';
import { useModal } from '@/ui/layout/modal/hooks/useModal';
import { PAGE_ACTION_CONTAINER_CLICK_OUTSIDE_ID } from '@/ui/layout/page/constants/PageActionContainerClickOutsideId';
@ -81,15 +80,13 @@ export const RecordBoard = () => {
RECORD_BOARD_CLICK_OUTSIDE_LISTENER_ID,
);
const actionMenuId = getActionMenuIdFromRecordIndexId(recordBoardId);
const { closeDropdown } = useDropdownV2();
const { closeAnyOpenDropdown } = useCloseAnyOpenDropdown();
const { deactivateBoardCard } = useActiveRecordBoardCard(recordBoardId);
const { unfocusBoardCard } = useFocusedRecordBoardCard(recordBoardId);
const handleDragSelectionStart = () => {
closeDropdown(actionMenuId);
closeAnyOpenDropdown();
toggleClickOutside(false);
};

View File

@ -5,7 +5,7 @@ import { getActionMenuIdFromRecordIndexId } from '@/action-menu/utils/getActionM
import { RecordBoardComponentInstanceContext } from '@/object-record/record-board/states/contexts/RecordBoardComponentInstanceContext';
import { isRecordBoardCardSelectedComponentFamilyState } from '@/object-record/record-board/states/isRecordBoardCardSelectedComponentFamilyState';
import { recordBoardSelectedRecordIdsComponentSelector } from '@/object-record/record-board/states/selectors/recordBoardSelectedRecordIdsComponentSelector';
import { useDropdownV2 } from '@/ui/layout/dropdown/hooks/useDropdownV2';
import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue';
@ -28,7 +28,7 @@ export const useRecordBoardSelection = (recordBoardId?: string) => {
recordBoardId,
);
const { closeDropdown } = useDropdownV2();
const { closeDropdown } = useCloseDropdown();
const dropdownId = getActionMenuDropdownIdFromActionMenuId(
getActionMenuIdFromRecordIndexId(instanceIdFromProps),

View File

@ -15,7 +15,7 @@ import { RecordBoardCardBody } from '@/object-record/record-board/record-board-c
import { RecordBoardCardHeader } from '@/object-record/record-board/record-board-card/components/RecordBoardCardHeader';
import { RECORD_BOARD_CARD_CLICK_OUTSIDE_ID } from '@/object-record/record-board/record-board-card/constants/RecordBoardCardClickOutsideId';
import { useOpenRecordFromIndexView } from '@/object-record/record-index/hooks/useOpenRecordFromIndexView';
import { useDropdownV2 } from '@/ui/layout/dropdown/hooks/useDropdownV2';
import { useOpenDropdown } from '@/ui/layout/dropdown/hooks/useOpenDropdown';
import { useAvailableScopeIdOrThrow } from '@/ui/utilities/recoil-scope/scopes-internal/hooks/useAvailableScopeId';
import { useScrollWrapperElement } from '@/ui/utilities/scroll/hooks/useScrollWrapperElement';
import { useRecoilComponentFamilyStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyStateV2';
@ -140,22 +140,25 @@ export const RecordBoardCard = () => {
),
);
const { openDropdown } = useDropdownV2();
const { openDropdown } = useOpenDropdown();
const { openRecordFromIndexView } = useOpenRecordFromIndexView();
const { activateBoardCard } = useActiveRecordBoardCard(recordBoardId);
const { unfocusBoardCard } = useFocusedRecordBoardCard(recordBoardId);
const handleActionMenuDropdown = (event: React.MouseEvent) => {
const handleContextMenuOpen = (event: React.MouseEvent) => {
event.preventDefault();
setIsCurrentCardSelected(true);
setActionMenuDropdownPosition({
x: event.clientX,
y: event.clientY,
});
openDropdown(actionMenuDropdownId, {
enableGlobalHotkeysWithModifiers: true,
enableGlobalHotkeysConflictingWithKeyboard: false,
openDropdown({
dropdownComponentInstanceIdFromProps: actionMenuDropdownId,
globalHotkeysConfig: {
enableGlobalHotkeysWithModifiers: true,
enableGlobalHotkeysConflictingWithKeyboard: false,
},
});
};
@ -185,7 +188,7 @@ export const RecordBoardCard = () => {
return (
<StyledBoardCardWrapper
data-click-outside-id={RECORD_BOARD_CARD_CLICK_OUTSIDE_ID}
onContextMenu={handleActionMenuDropdown}
onContextMenu={handleContextMenuOpen}
>
<InView>
<StyledBoardCard

View File

@ -1,5 +1,4 @@
import { DROPDOWN_OFFSET_Y } from '@/dropdown/constants/DropdownOffsetY';
import { useCurrentContentId } from '@/dropdown/hooks/useCurrentContentId';
import { useDropdownContextCurrentContentId } from '@/dropdown-context-state-management/hooks/useDropdownContextCurrentContentId';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { RecordBoardColumnHeaderAggregateDropdownComponentInstanceContext } from '@/object-record/record-board/contexts/RecordBoardColumnHeaderAggregateDropdownComponentInstanceContext';
import { RecordBoardColumnHeaderAggregateDropdownButton } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdownButton';
@ -7,6 +6,7 @@ import { AggregateDropdownContent } from '@/object-record/record-board/record-bo
import { RecordBoardColumnHeaderAggregateDropdownContext } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdownContext';
import { RecordBoardColumnHeaderAggregateContentId } from '@/object-record/record-board/types/RecordBoardColumnHeaderAggregateContentId';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { DROPDOWN_OFFSET_Y } from '@/ui/layout/dropdown/constants/DropdownOffsetY';
import styled from '@emotion/styled';
type RecordBoardColumnHeaderAggregateDropdownProps = {
@ -31,7 +31,8 @@ export const RecordBoardColumnHeaderAggregateDropdown = ({
handleContentChange,
handleResetContent,
previousContentId,
} = useCurrentContentId<RecordBoardColumnHeaderAggregateContentId>();
} =
useDropdownContextCurrentContentId<RecordBoardColumnHeaderAggregateContentId>();
return (
<RecordBoardColumnHeaderAggregateDropdownComponentInstanceContext.Provider

View File

@ -1,4 +1,4 @@
import { useDropdown } from '@/dropdown/hooks/useDropdown';
import { useDropdownContextStateManagement } from '@/dropdown-context-state-management/hooks/useDropdownContextStateManagement';
import { RecordBoardColumnHeaderAggregateDropdownContext } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdownContext';
import { RecordBoardColumnHeaderAggregateDropdownFieldsContent } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdownFieldsContent';
import { RecordBoardColumnHeaderAggregateDropdownMenuContent } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdownMenuContent';
@ -12,9 +12,10 @@ import { getAvailableFieldsIdsForAggregationFromObjectFields } from '@/object-re
import { t } from '@lingui/core/macro';
export const AggregateDropdownContent = () => {
const { currentContentId, objectMetadataItem } = useDropdown({
context: RecordBoardColumnHeaderAggregateDropdownContext,
});
const { currentContentId, objectMetadataItem } =
useDropdownContextStateManagement({
context: RecordBoardColumnHeaderAggregateDropdownContext,
});
switch (currentContentId) {
case 'countAggregateOperationsOptions': {

View File

@ -1,4 +1,4 @@
import { useDropdown } from '@/dropdown/hooks/useDropdown';
import { useDropdownContextStateManagement } from '@/dropdown-context-state-management/hooks/useDropdownContextStateManagement';
import { RecordBoardColumnHeaderAggregateDropdownContext } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdownContext';
import { aggregateOperationComponentState } from '@/object-record/record-board/record-board-column/states/aggregateOperationComponentState';
import { availableFieldIdsForAggregateOperationComponentState } from '@/object-record/record-board/record-board-column/states/availableFieldIdsForAggregateOperationComponentState';
@ -27,7 +27,7 @@ export const RecordBoardColumnHeaderAggregateDropdownFieldsContent = () => {
onContentChange,
resetContent,
previousContentId,
} = useDropdown({
} = useDropdownContextStateManagement({
context: RecordBoardColumnHeaderAggregateDropdownContext,
});

View File

@ -1,6 +1,6 @@
import { Key } from 'ts-key-enum';
import { useDropdown } from '@/dropdown/hooks/useDropdown';
import { useDropdownContextStateManagement } from '@/dropdown-context-state-management/hooks/useDropdownContextStateManagement';
import {
RecordBoardColumnHeaderAggregateDropdownContext,
RecordBoardColumnHeaderAggregateDropdownContextValue,
@ -17,9 +17,11 @@ export const RecordBoardColumnHeaderAggregateDropdownMenuContent = () => {
const { t } = useLingui();
const { onContentChange, closeDropdown } =
useDropdown<RecordBoardColumnHeaderAggregateDropdownContextValue>({
context: RecordBoardColumnHeaderAggregateDropdownContext,
});
useDropdownContextStateManagement<RecordBoardColumnHeaderAggregateDropdownContextValue>(
{
context: RecordBoardColumnHeaderAggregateDropdownContext,
},
);
useScopedHotkeys(
[Key.Escape],

View File

@ -1,4 +1,4 @@
import { useDropdown } from '@/dropdown/hooks/useDropdown';
import { useDropdownContextStateManagement } from '@/dropdown-context-state-management/hooks/useDropdownContextStateManagement';
import {
RecordBoardColumnHeaderAggregateDropdownContext,
RecordBoardColumnHeaderAggregateDropdownContextValue,
@ -32,9 +32,11 @@ export const RecordBoardColumnHeaderAggregateDropdownOptionsContent = ({
title: string;
}) => {
const { onContentChange, closeDropdown, resetContent } =
useDropdown<RecordBoardColumnHeaderAggregateDropdownContextValue>({
context: RecordBoardColumnHeaderAggregateDropdownContext,
});
useDropdownContextStateManagement<RecordBoardColumnHeaderAggregateDropdownContextValue>(
{
context: RecordBoardColumnHeaderAggregateDropdownContext,
},
);
useScopedHotkeys(
[Key.Escape],

View File

@ -1,4 +1,4 @@
import { useDropdown } from '@/dropdown/hooks/useDropdown';
import { useDropdownContextStateManagement } from '@/dropdown-context-state-management/hooks/useDropdownContextStateManagement';
import { AggregateOperations } from '@/object-record/record-table/constants/AggregateOperations';
import { DateAggregateOperations } from '@/object-record/record-table/constants/DateAggregateOperations';
import { RecordTableColumnAggregateFooterDropdownSubmenuContent } from '@/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateDropdownSubmenuContent';
@ -14,9 +14,10 @@ import { useLingui } from '@lingui/react/macro';
export const RecordTableColumnAggregateFooterDropdownContent = () => {
const { t } = useLingui();
const { currentContentId, fieldMetadataType } = useDropdown({
context: RecordTableColumnAggregateFooterDropdownContext,
});
const { currentContentId, fieldMetadataType } =
useDropdownContextStateManagement({
context: RecordTableColumnAggregateFooterDropdownContext,
});
const availableAggregateOperations =
getAvailableAggregateOperationsForFieldMetadataType({

View File

@ -1,4 +1,4 @@
import { useCurrentContentId } from '@/dropdown/hooks/useCurrentContentId';
import { useDropdownContextCurrentContentId } from '@/dropdown-context-state-management/hooks/useDropdownContextCurrentContentId';
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { RecordTableColumnAggregateFooterCellContext } from '@/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterCellContext';
import { RecordTableColumnAggregateFooterDropdownContent } from '@/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterDropdownContent';
@ -19,7 +19,7 @@ export const RecordTableColumnFooterWithDropdown = ({
isFirstCell,
}: RecordTableColumnFooterWithDropdownProps) => {
const { currentContentId, handleContentChange, handleResetContent } =
useCurrentContentId<RecordTableFooterAggregateContentId>();
useDropdownContextCurrentContentId<RecordTableFooterAggregateContentId>();
const { fieldMetadataId } = useContext(
RecordTableColumnAggregateFooterCellContext,

View File

@ -2,7 +2,7 @@ import { useCreateEmptyRecordFilterFromFieldMetadataItem } from '@/object-record
import { useFilterableFieldMetadataItemsInRecordIndexContext } from '@/object-record/record-filter/hooks/useFilterableFieldMetadataItemsInRecordIndexContext';
import { useUpsertRecordFilter } from '@/object-record/record-filter/hooks/useUpsertRecordFilter';
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
import { useDropdownV2 } from '@/ui/layout/dropdown/hooks/useDropdownV2';
import { useOpenDropdown } from '@/ui/layout/dropdown/hooks/useOpenDropdown';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useSetEditableFilterChipDropdownStates } from '@/views/hooks/useSetEditableFilterChipDropdownStates';
import { isDefined } from 'twenty-shared/utils';
@ -20,7 +20,7 @@ export const useOpenRecordFilterChipFromTableHeader = () => {
const { upsertRecordFilter } = useUpsertRecordFilter();
const { openDropdown } = useDropdownV2();
const { openDropdown } = useOpenDropdown();
const { setEditableFilterChipDropdownStates } =
useSetEditableFilterChipDropdownStates();
@ -45,7 +45,10 @@ export const useOpenRecordFilterChipFromTableHeader = () => {
if (isDefined(existingNonAdvancedRecordFilter)) {
setEditableFilterChipDropdownStates(existingNonAdvancedRecordFilter);
openDropdown(existingNonAdvancedRecordFilter.id);
openDropdown({
dropdownComponentInstanceIdFromProps:
existingNonAdvancedRecordFilter.id,
});
return;
}
@ -56,7 +59,7 @@ export const useOpenRecordFilterChipFromTableHeader = () => {
upsertRecordFilter(newRecordFilter);
setEditableFilterChipDropdownStates(newRecordFilter);
openDropdown(newRecordFilter.id);
openDropdown({ dropdownComponentInstanceIdFromProps: newRecordFilter.id });
};
return { openRecordFilterChipFromTableHeader };

View File

@ -2,7 +2,7 @@ import { DropdownOnToggleEffect } from '@/ui/layout/dropdown/components/Dropdown
import { DropdownInternalContainer } from '@/ui/layout/dropdown/components/internal/DropdownInternalContainer';
import { DROPDOWN_RESIZE_MIN_HEIGHT } from '@/ui/layout/dropdown/constants/DropdownResizeMinHeight';
import { DROPDOWN_RESIZE_MIN_WIDTH } from '@/ui/layout/dropdown/constants/DropdownResizeMinWidth';
import { DropdownComponentInstanceContext } from '@/ui/layout/dropdown/contexts/DropdownComponeInstanceContext';
import { DropdownComponentInstanceContext } from '@/ui/layout/dropdown/contexts/DropdownComponentInstanceContext';
import { DropdownScope } from '@/ui/layout/dropdown/scopes/DropdownScope';
import { dropdownMaxHeightComponentState } from '@/ui/layout/dropdown/states/internal/dropdownMaxHeightComponentState';
import { dropdownMaxWidthComponentState } from '@/ui/layout/dropdown/states/internal/dropdownMaxWidthComponentState';

View File

@ -3,20 +3,36 @@ import { renderHook } from '@testing-library/react';
import { act } from 'react';
import { RecoilRoot } from 'recoil';
import { DropdownComponentInstanceContext } from '@/ui/layout/dropdown/contexts/DropdownComponentInstanceContext';
import { useCloseAnyOpenDropdown } from '@/ui/layout/dropdown/hooks/useCloseAnyOpenDropdown';
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { useOpenDropdown } from '@/ui/layout/dropdown/hooks/useOpenDropdown';
import { isDropdownOpenComponentStateV2 } from '@/ui/layout/dropdown/states/isDropdownOpenComponentStateV2';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
const dropdownId = 'test-dropdown-id';
const Wrapper = ({ children }: { children: React.ReactNode }) => {
return <RecoilRoot>{children}</RecoilRoot>;
return (
<RecoilRoot>
<DropdownComponentInstanceContext.Provider
value={{ instanceId: dropdownId }}
>
{children}
</DropdownComponentInstanceContext.Provider>
</RecoilRoot>
);
};
describe('useCloseAnyOpenDropdown', () => {
it('should open dropdown and then close it with closeAnyOpenDropdown', async () => {
const { result } = renderHook(
() => {
const { openDropdown, isDropdownOpen } = useDropdown(dropdownId);
const isDropdownOpen = useRecoilComponentValueV2(
isDropdownOpenComponentStateV2,
dropdownId,
);
const { openDropdown } = useOpenDropdown();
const { closeAnyOpenDropdown } = useCloseAnyOpenDropdown();

View File

@ -1,43 +0,0 @@
import { expect } from '@storybook/test';
import { renderHook } from '@testing-library/react';
import { act } from 'react';
import { RecoilRoot } from 'recoil';
import { useCloseDropdownFromOutside } from '@/ui/layout/dropdown/hooks/useCloseDropdownFromOutside';
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
const dropdownId = 'test-dropdown-id';
const Wrapper = ({ children }: { children: React.ReactNode }) => {
return <RecoilRoot>{children}</RecoilRoot>;
};
describe('useCloseDropdownFromOutside', () => {
it('should close open dropdown', async () => {
const { result } = renderHook(
() => {
const { isDropdownOpen, openDropdown } = useDropdown(dropdownId);
const { closeDropdownFromOutside } = useCloseDropdownFromOutside();
return { closeDropdownFromOutside, isDropdownOpen, openDropdown };
},
{
wrapper: Wrapper,
},
);
expect(result.current.isDropdownOpen).toBe(false);
act(() => {
result.current.openDropdown();
});
expect(result.current.isDropdownOpen).toBe(true);
act(() => {
result.current.closeDropdownFromOutside(dropdownId);
});
expect(result.current.isDropdownOpen).toBe(false);
});
});

View File

@ -1,4 +1,4 @@
import { useCloseDropdownFromOutside } from '@/ui/layout/dropdown/hooks/useCloseDropdownFromOutside';
import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
import { activeDropdownFocusIdState } from '@/ui/layout/dropdown/states/activeDropdownFocusIdState';
import { previousDropdownFocusIdState } from '@/ui/layout/dropdown/states/previousDropdownFocusIdState';
import { useRemoveFocusItemFromFocusStackById } from '@/ui/utilities/focus/hooks/useRemoveFocusItemFromFocusStackById';
@ -6,7 +6,7 @@ import { useRecoilCallback } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
export const useCloseAnyOpenDropdown = () => {
const { closeDropdownFromOutside } = useCloseDropdownFromOutside();
const { closeDropdown } = useCloseDropdown();
const { removeFocusItemFromFocusStackById } =
useRemoveFocusItemFromFocusStackById();
@ -33,14 +33,14 @@ export const useCloseAnyOpenDropdown = () => {
const thereIsOneNestedDropdownOpen = isDefined(previousDropdownFocusId);
if (isDefined(activeDropdownFocusId)) {
closeDropdownFromOutside(activeDropdownFocusId);
closeDropdown(activeDropdownFocusId);
removeFocusItemFromFocusStackById({
focusId: activeDropdownFocusId,
});
}
if (thereIsOneNestedDropdownOpen) {
closeDropdownFromOutside(previousDropdownFocusId);
closeDropdown(previousDropdownFocusId);
removeFocusItemFromFocusStackById({
focusId: previousDropdownFocusId,
});
@ -49,7 +49,7 @@ export const useCloseAnyOpenDropdown = () => {
set(previousDropdownFocusIdState, null);
set(activeDropdownFocusIdState, null);
},
[closeDropdownFromOutside, removeFocusItemFromFocusStackById],
[closeDropdown, removeFocusItemFromFocusStackById],
);
return { closeAnyOpenDropdown };

View File

@ -0,0 +1,79 @@
import { DropdownComponentInstanceContext } from '@/ui/layout/dropdown/contexts/DropdownComponentInstanceContext';
import { useGoBackToPreviousDropdownFocusId } from '@/ui/layout/dropdown/hooks/useGoBackToPreviousDropdownFocusId';
import { isDropdownOpenComponentState } from '@/ui/layout/dropdown/states/isDropdownOpenComponentState';
import { isDropdownOpenComponentStateV2 } from '@/ui/layout/dropdown/states/isDropdownOpenComponentStateV2';
import { useRemoveFocusItemFromFocusStackById } from '@/ui/utilities/focus/hooks/useRemoveFocusItemFromFocusStackById';
import { useAvailableComponentInstanceId } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceId';
import { useRecoilCallback } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
export const useCloseDropdown = () => {
const { goBackToPreviousDropdownFocusId } =
useGoBackToPreviousDropdownFocusId();
const { removeFocusItemFromFocusStackById } =
useRemoveFocusItemFromFocusStackById();
const dropdownComponentInstanceIdFromContext =
useAvailableComponentInstanceId(DropdownComponentInstanceContext);
const closeDropdown = useRecoilCallback(
({ set, snapshot }) =>
(dropdownComponentInstanceIdFromProps?: string) => {
const dropdownComponentInstanceId =
dropdownComponentInstanceIdFromProps ??
dropdownComponentInstanceIdFromContext;
if (!isDefined(dropdownComponentInstanceId)) {
throw new Error('Dropdown component instance ID is not defined');
}
const isDropdownOpen = snapshot
.getLoadable(
isDropdownOpenComponentStateV2.atomFamily({
instanceId: dropdownComponentInstanceId,
}),
)
.getValue();
const isDropdownOpenLegacy = snapshot
.getLoadable(
isDropdownOpenComponentState({
scopeId: dropdownComponentInstanceId,
}),
)
.getValue();
if (isDropdownOpen || isDropdownOpenLegacy) {
removeFocusItemFromFocusStackById({
focusId: dropdownComponentInstanceId,
});
goBackToPreviousDropdownFocusId();
set(
isDropdownOpenComponentStateV2.atomFamily({
instanceId: dropdownComponentInstanceId,
}),
false,
);
set(
isDropdownOpenComponentState({
scopeId: dropdownComponentInstanceId,
}),
false,
);
}
},
[
removeFocusItemFromFocusStackById,
goBackToPreviousDropdownFocusId,
dropdownComponentInstanceIdFromContext,
],
);
return {
closeDropdown,
};
};

View File

@ -1,14 +0,0 @@
import { isDropdownOpenComponentState } from '@/ui/layout/dropdown/states/isDropdownOpenComponentState';
import { useRecoilCallback } from 'recoil';
export const useCloseDropdownFromOutside = () => {
const closeDropdownFromOutside = useRecoilCallback(
({ set }) =>
(dropdownId: string) => {
set(isDropdownOpenComponentState({ scopeId: dropdownId }), false);
},
[],
);
return { closeDropdownFromOutside };
};

View File

@ -10,6 +10,13 @@ import { GlobalHotkeysConfig } from '@/ui/utilities/hotkey/types/GlobalHotkeysCo
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
import { useCallback } from 'react';
/**
*
* @deprecated This hook is deprecated, use a specific hook instead :
* - `useOpenDropdown`
* - `useCloseDropdown`
* - `useToggleDropdown`
*/
export const useDropdown = (dropdownId?: string) => {
const { pushFocusItemToFocusStack } = usePushFocusItemToFocusStack();
const { removeFocusItemFromFocusStackById } =

View File

@ -1,106 +0,0 @@
import { useRecoilCallback } from 'recoil';
import { useGoBackToPreviousDropdownFocusId } from '@/ui/layout/dropdown/hooks/useGoBackToPreviousDropdownFocusId';
import { useSetActiveDropdownFocusIdAndMemorizePrevious } from '@/ui/layout/dropdown/hooks/useSetFocusedDropdownIdAndMemorizePrevious';
import { isDropdownOpenComponentState } from '@/ui/layout/dropdown/states/isDropdownOpenComponentState';
import { usePushFocusItemToFocusStack } from '@/ui/utilities/focus/hooks/usePushFocusItemToFocusStack';
import { useRemoveFocusItemFromFocusStackById } from '@/ui/utilities/focus/hooks/useRemoveFocusItemFromFocusStackById';
import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType';
import { GlobalHotkeysConfig } from '@/ui/utilities/hotkey/types/GlobalHotkeysConfig';
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
export const useDropdownV2 = () => {
const { goBackToPreviousDropdownFocusId } =
useGoBackToPreviousDropdownFocusId();
const { pushFocusItemToFocusStack } = usePushFocusItemToFocusStack();
const { removeFocusItemFromFocusStackById } =
useRemoveFocusItemFromFocusStackById();
const { setActiveDropdownFocusIdAndMemorizePrevious } =
useSetActiveDropdownFocusIdAndMemorizePrevious();
const closeDropdown = useRecoilCallback(
({ set, snapshot }) =>
(specificComponentId: string) => {
const scopeId = specificComponentId;
const isDropdownOpen = snapshot
.getLoadable(isDropdownOpenComponentState({ scopeId }))
.getValue();
if (isDropdownOpen) {
removeFocusItemFromFocusStackById({
focusId: scopeId,
});
goBackToPreviousDropdownFocusId();
set(
isDropdownOpenComponentState({
scopeId,
}),
false,
);
}
},
[removeFocusItemFromFocusStackById, goBackToPreviousDropdownFocusId],
);
const openDropdown = useRecoilCallback(
({ set }) =>
(
specificComponentId: string,
globalHotkeysConfig?: Partial<GlobalHotkeysConfig>,
) => {
const scopeId = specificComponentId;
set(
isDropdownOpenComponentState({
scopeId,
}),
true,
);
setActiveDropdownFocusIdAndMemorizePrevious(specificComponentId);
pushFocusItemToFocusStack({
focusId: scopeId,
component: {
type: FocusComponentType.DROPDOWN,
instanceId: scopeId,
},
globalHotkeysConfig,
// TODO: Remove this once we've fully migrated away from hotkey scopes
hotkeyScope: { scope: 'dropdown' } as HotkeyScope,
memoizeKey: 'global',
});
},
[pushFocusItemToFocusStack, setActiveDropdownFocusIdAndMemorizePrevious],
);
const toggleDropdown = useRecoilCallback(
({ snapshot }) =>
(
specificComponentId: string,
globalHotkeysConfig?: Partial<GlobalHotkeysConfig>,
) => {
const scopeId = specificComponentId;
const isDropdownOpen = snapshot
.getLoadable(isDropdownOpenComponentState({ scopeId }))
.getValue();
if (isDropdownOpen) {
closeDropdown(specificComponentId);
} else {
openDropdown(specificComponentId, globalHotkeysConfig);
}
},
[closeDropdown, openDropdown],
);
return {
closeDropdown,
openDropdown,
toggleDropdown,
};
};

View File

@ -0,0 +1,77 @@
import { DropdownComponentInstanceContext } from '@/ui/layout/dropdown/contexts/DropdownComponentInstanceContext';
import { useSetActiveDropdownFocusIdAndMemorizePrevious } from '@/ui/layout/dropdown/hooks/useSetFocusedDropdownIdAndMemorizePrevious';
import { isDropdownOpenComponentState } from '@/ui/layout/dropdown/states/isDropdownOpenComponentState';
import { isDropdownOpenComponentStateV2 } from '@/ui/layout/dropdown/states/isDropdownOpenComponentStateV2';
import { usePushFocusItemToFocusStack } from '@/ui/utilities/focus/hooks/usePushFocusItemToFocusStack';
import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType';
import { GlobalHotkeysConfig } from '@/ui/utilities/hotkey/types/GlobalHotkeysConfig';
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
import { useAvailableComponentInstanceId } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceId';
import { useRecoilCallback } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
type OpenDropdownArgs = {
dropdownComponentInstanceIdFromProps?: string;
globalHotkeysConfig?: Partial<GlobalHotkeysConfig>;
};
export const useOpenDropdown = () => {
const { pushFocusItemToFocusStack } = usePushFocusItemToFocusStack();
const { setActiveDropdownFocusIdAndMemorizePrevious } =
useSetActiveDropdownFocusIdAndMemorizePrevious();
const dropdownComponentInstanceIdFromContext =
useAvailableComponentInstanceId(DropdownComponentInstanceContext);
const openDropdown = useRecoilCallback(
({ set }) =>
(args?: OpenDropdownArgs | null | undefined) => {
const dropdownComponentInstanceId =
args?.dropdownComponentInstanceIdFromProps ??
dropdownComponentInstanceIdFromContext;
if (!isDefined(dropdownComponentInstanceId)) {
throw new Error('Dropdown component instance ID is not defined');
}
set(
isDropdownOpenComponentState({
scopeId: dropdownComponentInstanceId,
}),
true,
);
set(
isDropdownOpenComponentStateV2.atomFamily({
instanceId: dropdownComponentInstanceId,
}),
true,
);
setActiveDropdownFocusIdAndMemorizePrevious(
dropdownComponentInstanceId,
);
pushFocusItemToFocusStack({
focusId: dropdownComponentInstanceId,
component: {
type: FocusComponentType.DROPDOWN,
instanceId: dropdownComponentInstanceId,
},
globalHotkeysConfig: args?.globalHotkeysConfig ?? undefined,
// TODO: Remove this once we've fully migrated away from hotkey scopes
hotkeyScope: { scope: 'dropdown' } as HotkeyScope,
memoizeKey: 'global',
});
},
[
pushFocusItemToFocusStack,
setActiveDropdownFocusIdAndMemorizePrevious,
dropdownComponentInstanceIdFromContext,
],
);
return {
openDropdown,
};
};

View File

@ -0,0 +1,66 @@
import { DropdownComponentInstanceContext } from '@/ui/layout/dropdown/contexts/DropdownComponentInstanceContext';
import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
import { useOpenDropdown } from '@/ui/layout/dropdown/hooks/useOpenDropdown';
import { isDropdownOpenComponentState } from '@/ui/layout/dropdown/states/isDropdownOpenComponentState';
import { isDropdownOpenComponentStateV2 } from '@/ui/layout/dropdown/states/isDropdownOpenComponentStateV2';
import { GlobalHotkeysConfig } from '@/ui/utilities/hotkey/types/GlobalHotkeysConfig';
import { useAvailableComponentInstanceId } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceId';
import { useRecoilCallback } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
export const useToggleDropdown = () => {
const dropdownComponentInstanceIdFromContext =
useAvailableComponentInstanceId(DropdownComponentInstanceContext);
const { openDropdown } = useOpenDropdown();
const { closeDropdown } = useCloseDropdown();
const toggleDropdown = useRecoilCallback(
({ snapshot }) =>
({
dropdownComponentInstanceIdFromProps,
globalHotkeysConfig,
}: {
dropdownComponentInstanceIdFromProps?: string;
globalHotkeysConfig?: Partial<GlobalHotkeysConfig>;
}) => {
const dropdownComponentInstanceId =
dropdownComponentInstanceIdFromProps ??
dropdownComponentInstanceIdFromContext;
if (!isDefined(dropdownComponentInstanceId)) {
throw new Error('Dropdown component instance ID is not defined');
}
const isDropdownOpen = snapshot
.getLoadable(
isDropdownOpenComponentStateV2.atomFamily({
instanceId: dropdownComponentInstanceId,
}),
)
.getValue();
const isDropdownOpenLegacy = snapshot
.getLoadable(
isDropdownOpenComponentState({
scopeId: dropdownComponentInstanceId,
}),
)
.getValue();
if (isDropdownOpen || isDropdownOpenLegacy) {
closeDropdown(dropdownComponentInstanceId);
} else {
openDropdown({
dropdownComponentInstanceIdFromProps: dropdownComponentInstanceId,
globalHotkeysConfig,
});
}
},
[closeDropdown, openDropdown, dropdownComponentInstanceIdFromContext],
);
return {
toggleDropdown,
};
};

View File

@ -1,4 +1,5 @@
import { createState } from 'twenty-ui/utilities';
export const activeDropdownFocusIdState = createState<string | null>({
key: 'activeDropdownFocusIdState',
defaultValue: null,

View File

@ -1,4 +1,4 @@
import { DropdownComponentInstanceContext } from '@/ui/layout/dropdown/contexts/DropdownComponeInstanceContext';
import { DropdownComponentInstanceContext } from '@/ui/layout/dropdown/contexts/DropdownComponentInstanceContext';
import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2';
export const dropdownMaxHeightComponentState = createComponentStateV2<

View File

@ -1,4 +1,4 @@
import { DropdownComponentInstanceContext } from '@/ui/layout/dropdown/contexts/DropdownComponeInstanceContext';
import { DropdownComponentInstanceContext } from '@/ui/layout/dropdown/contexts/DropdownComponentInstanceContext';
import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2';
export const dropdownMaxWidthComponentState = createComponentStateV2<

View File

@ -1,5 +1,8 @@
import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState';
/**
* @deprecated Use `isDropdownOpenComponentStateV2` instead.
*/
export const isDropdownOpenComponentState = createComponentState<boolean>({
key: 'isDropdownOpenComponentState',
defaultValue: false,

View File

@ -0,0 +1,8 @@
import { DropdownComponentInstanceContext } from '@/ui/layout/dropdown/contexts/DropdownComponentInstanceContext';
import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2';
export const isDropdownOpenComponentStateV2 = createComponentStateV2<boolean>({
key: 'isDropdownOpenComponentStateV2',
defaultValue: false,
componentInstanceContext: DropdownComponentInstanceContext,
});

View File

@ -1,4 +1,5 @@
import { createState } from 'twenty-ui/utilities';
export const previousDropdownFocusIdState = createState<string | null>({
key: 'previousDropdownFocusIdState',
defaultValue: null,

View File

@ -0,0 +1,19 @@
import { useComponentInstanceStateContext } from '@/ui/utilities/state/component-state/hooks/useComponentInstanceStateContext';
import { ComponentInstanceStateContext } from '@/ui/utilities/state/component-state/types/ComponentInstanceStateContext';
import { isNonEmptyString } from '@sniptt/guards';
export const useAvailableComponentInstanceId = <
T extends { instanceId: string },
>(
Context: ComponentInstanceStateContext<T>,
): string | null => {
const instanceStateContext = useComponentInstanceStateContext(Context);
const instanceIdFromContext = instanceStateContext?.instanceId;
if (isNonEmptyString(instanceIdFromContext)) {
return instanceIdFromContext;
} else {
return null;
}
};

View File

@ -1,6 +1,6 @@
import { useCallback } from 'react';
import { useDropdownV2 } from '@/ui/layout/dropdown/hooks/useDropdownV2';
import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
import { VIEW_PICKER_DROPDOWN_ID } from '@/views/view-picker/constants/ViewPickerDropdownId';
import { VIEW_PICKER_KANBAN_FIELD_DROPDOWN_ID } from '@/views/view-picker/constants/ViewPickerKanbanFieldDropdownId';
@ -17,7 +17,7 @@ export const useCloseAndResetViewPicker = () => {
viewPickerIsPersistingComponentState,
);
const { closeDropdown } = useDropdownV2();
const { closeDropdown } = useCloseDropdown();
const closeAndResetViewPicker = useCallback(() => {
setViewPickerIsPersisting(false);

View File

@ -1,6 +1,6 @@
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { StyledDropdownButtonContainer } from '@/ui/layout/dropdown/components/StyledDropdownButtonContainer';
import { useDropdownV2 } from '@/ui/layout/dropdown/hooks/useDropdownV2';
import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
import { isDropdownOpenComponentState } from '@/ui/layout/dropdown/states/isDropdownOpenComponentState';
import { extractComponentState } from '@/ui/utilities/state/component-state/utils/extractComponentState';
import { WorkflowVariablesDropdownFieldItems } from '@/workflow/workflow-variables/components/WorkflowVariablesDropdownFieldItems';
@ -51,7 +51,7 @@ export const WorkflowVariablesDropdown = ({
const isDropdownOpen = useRecoilValue(
extractComponentState(isDropdownOpenComponentState, dropdownId),
);
const { closeDropdown } = useDropdownV2();
const { closeDropdown } = useCloseDropdown();
const availableVariablesInWorkflowStep = useAvailableVariablesInWorkflowStep({
objectNameSingularToSelect,
});