Migrate dropdown to jotai (#18063)

Here we go again
This commit is contained in:
Charles Bochet
2026-02-19 00:58:26 +01:00
committed by GitHub
parent 66deb8be63
commit 36c2b0e23b
54 changed files with 599 additions and 370 deletions
@@ -13,7 +13,7 @@ import { SELECT_COUNTRY_DROPDOWN_ID } from '@/ui/input/components/internal/count
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { activeDropdownFocusIdState } from '@/ui/layout/dropdown/states/activeDropdownFocusIdState';
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
import { useRecoilValue } from 'recoil';
import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2';
import { isDefined } from 'twenty-shared/utils';
import { MOBILE_VIEWPORT } from 'twenty-ui/theme';
import { v4 } from 'uuid';
@@ -236,7 +236,7 @@ export const AddressInput = ({
onShiftTab: handleShiftTab,
});
const activeDropdownFocusId = useRecoilValue(activeDropdownFocusIdState);
const activeDropdownFocusId = useRecoilValueV2(activeDropdownFocusIdState);
useListenClickOutside({
refs: [wrapperRef],
@@ -10,7 +10,7 @@ import { PhoneCountryPickerDropdownSelect } from './PhoneCountryPickerDropdownSe
import { PHONE_COUNTRY_CODE_PICKER_DROPDOWN_ID } from '@/ui/input/components/internal/phone/constants/PhoneCountryCodePickerDropdownId';
import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
import { isDropdownOpenComponentState } from '@/ui/layout/dropdown/states/isDropdownOpenComponentState';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilComponentValueV2';
import 'react-phone-number-input/style.css';
import { isDefined } from 'twenty-shared/utils';
import { IconChevronDown, IconWorld } from 'twenty-ui/display';
@@ -78,7 +78,7 @@ export const PhoneCountryPickerDropdownButton = ({
const [selectedCountry, setSelectedCountry] = useState<Country>();
const isDropdownOpen = useRecoilComponentValue(
const isDropdownOpen = useRecoilComponentValueV2(
isDropdownOpenComponentState,
PHONE_COUNTRY_CODE_PICKER_DROPDOWN_ID,
);
@@ -13,8 +13,8 @@ import { dropdownYPositionComponentState } from '@/ui/layout/dropdown/states/int
import { isDropdownOpenComponentState } from '@/ui/layout/dropdown/states/isDropdownOpenComponentState';
import { type DropdownOffset } from '@/ui/layout/dropdown/types/DropdownOffset';
import { type GlobalHotkeysConfig } from '@/ui/utilities/hotkey/types/GlobalHotkeysConfig';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilComponentValueV2';
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/jotai/hooks/useSetRecoilComponentStateV2';
import styled from '@emotion/styled';
import {
type Placement,
@@ -24,10 +24,9 @@ import {
size,
useFloating,
} from '@floating-ui/react';
import { type MouseEvent, type ReactNode } from 'react';
import { type MouseEvent, type ReactNode, useCallback } from 'react';
import { flushSync } from 'react-dom';
import { type Keys } from 'react-hotkeys-hook';
import { useRecoilCallback } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
import { useIsMobile } from 'twenty-ui/utilities';
@@ -89,7 +88,7 @@ export const Dropdown = ({
disableClickForClickableComponent = false,
middlewareBoundaryPadding = {},
}: DropdownProps) => {
const isDropdownOpen = useRecoilComponentValue(
const isDropdownOpen = useRecoilComponentValueV2(
isDropdownOpenComponentState,
dropdownId,
);
@@ -108,17 +107,17 @@ export const Dropdown = ({
]
: [];
const setDropdownMaxHeight = useSetRecoilComponentState(
const setDropdownMaxHeight = useSetRecoilComponentStateV2(
dropdownMaxHeightComponentState,
dropdownId,
);
const setDropdownMaxWidth = useSetRecoilComponentState(
const setDropdownMaxWidth = useSetRecoilComponentStateV2(
dropdownMaxWidthComponentState,
dropdownId,
);
const setDropdownYPosition = useSetRecoilComponentState(
const setDropdownYPosition = useSetRecoilComponentStateV2(
dropdownYPositionComponentState,
dropdownId,
);
@@ -173,8 +172,8 @@ export const Dropdown = ({
strategy: dropdownStrategy,
});
const handleClickableComponentClick = useRecoilCallback(
() => async (event: MouseEvent) => {
const handleClickableComponentClick = useCallback(
async (event: MouseEvent) => {
if (disableClickForClickableComponent) return;
event.stopPropagation();
event.preventDefault();
@@ -1,7 +1,7 @@
import { useEffect, useState } from 'react';
import { isDropdownOpenComponentState } from '@/ui/layout/dropdown/states/isDropdownOpenComponentState';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilComponentValueV2';
export const DropdownOnToggleEffect = ({
onDropdownClose,
@@ -10,7 +10,9 @@ export const DropdownOnToggleEffect = ({
onDropdownClose?: () => void;
onDropdownOpen?: () => void;
}) => {
const isDropdownOpen = useRecoilComponentValue(isDropdownOpenComponentState);
const isDropdownOpen = useRecoilComponentValueV2(
isDropdownOpenComponentState,
);
const [currentIsDropdownOpen, setCurrentIsDropdownOpen] =
useState(isDropdownOpen);
@@ -11,8 +11,9 @@ import { HotkeyEffect } from '@/ui/utilities/hotkey/components/HotkeyEffect';
import { useHotkeysOnFocusedElement } from '@/ui/utilities/hotkey/hooks/useHotkeysOnFocusedElement';
import { ClickOutsideListenerContext } from '@/ui/utilities/pointer-event/contexts/ClickOutsideListenerContext';
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilComponentValueV2';
import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2';
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/jotai/hooks/useSetRecoilComponentStateV2';
import styled from '@emotion/styled';
import {
FloatingPortal,
@@ -21,7 +22,6 @@ import {
} from '@floating-ui/react';
import { useContext, useEffect } from 'react';
import { type Keys } from 'react-hotkeys-hook';
import { useRecoilValue } from 'recoil';
import { Key } from 'ts-key-enum';
export const StyledDropdownContentContainer = styled.div<{
@@ -70,23 +70,25 @@ export const DropdownInternalContainer = ({
excludedClickOutsideIds,
isDropdownInModal = false,
}: DropdownInternalContainerProps) => {
const isDropdownOpen = useRecoilComponentValue(isDropdownOpenComponentState);
const isDropdownOpen = useRecoilComponentValueV2(
isDropdownOpenComponentState,
);
const { closeDropdown } = useCloseDropdown();
const activeDropdownFocusId = useRecoilValue(activeDropdownFocusIdState);
const activeDropdownFocusId = useRecoilValueV2(activeDropdownFocusIdState);
const dropdownMaxHeight = useRecoilComponentValue(
const dropdownMaxHeight = useRecoilComponentValueV2(
dropdownMaxHeightComponentState,
dropdownId,
);
const dropdownMaxWidth = useRecoilComponentValue(
const dropdownMaxWidth = useRecoilComponentValueV2(
dropdownMaxWidthComponentState,
dropdownId,
);
const setDropdownPlacement = useSetRecoilComponentState(
const setDropdownPlacement = useSetRecoilComponentStateV2(
dropdownPlacementComponentState,
dropdownId,
);
@@ -1,4 +1,5 @@
import { renderHook } from '@testing-library/react';
import { Provider as JotaiProvider } from 'jotai';
import { act } from 'react';
import { RecoilRoot } from 'recoil';
@@ -6,27 +7,37 @@ import { DropdownComponentInstanceContext } from '@/ui/layout/dropdown/contexts/
import { useCloseAnyOpenDropdown } from '@/ui/layout/dropdown/hooks/useCloseAnyOpenDropdown';
import { useOpenDropdown } from '@/ui/layout/dropdown/hooks/useOpenDropdown';
import { isDropdownOpenComponentState } from '@/ui/layout/dropdown/states/isDropdownOpenComponentState';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilComponentValueV2';
import { jotaiStore } from '@/ui/utilities/state/jotai/jotaiStore';
const dropdownId = 'test-dropdown-id';
const Wrapper = ({ children }: { children: React.ReactNode }) => {
return (
<RecoilRoot>
<DropdownComponentInstanceContext.Provider
value={{ instanceId: dropdownId }}
>
{children}
</DropdownComponentInstanceContext.Provider>
</RecoilRoot>
<JotaiProvider store={jotaiStore}>
<RecoilRoot>
<DropdownComponentInstanceContext.Provider
value={{ instanceId: dropdownId }}
>
{children}
</DropdownComponentInstanceContext.Provider>
</RecoilRoot>
</JotaiProvider>
);
};
describe('useCloseAnyOpenDropdown', () => {
beforeEach(() => {
jotaiStore.set(
isDropdownOpenComponentState.atomFamily({ instanceId: dropdownId }),
false,
);
});
it('should open dropdown and then close it with closeAnyOpenDropdown', async () => {
const { result } = renderHook(
() => {
const isDropdownOpen = useRecoilComponentValue(
const isDropdownOpen = useRecoilComponentValueV2(
isDropdownOpenComponentState,
dropdownId,
);
@@ -1,4 +1,5 @@
import { renderHook } from '@testing-library/react';
import { Provider as JotaiProvider } from 'jotai';
import { act } from 'react';
import { RecoilRoot } from 'recoil';
@@ -6,36 +7,52 @@ import { DropdownComponentInstanceContext } from '@/ui/layout/dropdown/contexts/
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 { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilComponentValueV2';
import { jotaiStore } from '@/ui/utilities/state/jotai/jotaiStore';
const dropdownId = 'test-dropdown-id';
const outsideDropdownId = 'test-dropdown-id-outside';
const Wrapper = ({ children }: { children: React.ReactNode }) => {
return (
<RecoilRoot>
<DropdownComponentInstanceContext.Provider
value={{ instanceId: dropdownId }}
>
{children}
</DropdownComponentInstanceContext.Provider>
<DropdownComponentInstanceContext.Provider
value={{ instanceId: outsideDropdownId }}
></DropdownComponentInstanceContext.Provider>
</RecoilRoot>
<JotaiProvider store={jotaiStore}>
<RecoilRoot>
<DropdownComponentInstanceContext.Provider
value={{ instanceId: dropdownId }}
>
{children}
</DropdownComponentInstanceContext.Provider>
<DropdownComponentInstanceContext.Provider
value={{ instanceId: outsideDropdownId }}
></DropdownComponentInstanceContext.Provider>
</RecoilRoot>
</JotaiProvider>
);
};
describe('useCloseDropdown', () => {
beforeEach(() => {
jotaiStore.set(
isDropdownOpenComponentState.atomFamily({ instanceId: dropdownId }),
false,
);
jotaiStore.set(
isDropdownOpenComponentState.atomFamily({
instanceId: outsideDropdownId,
}),
false,
);
});
it('should close dropdown from inside component instance context', async () => {
const { result } = renderHook(
() => {
const isOutsideDropdownOpen = useRecoilComponentValue(
const isOutsideDropdownOpen = useRecoilComponentValueV2(
isDropdownOpenComponentState,
outsideDropdownId,
);
const isInsideDropdownOpen = useRecoilComponentValue(
const isInsideDropdownOpen = useRecoilComponentValueV2(
isDropdownOpenComponentState,
);
@@ -72,12 +89,12 @@ describe('useCloseDropdown', () => {
it('should close dropdown from outside component instance context', async () => {
const { result } = renderHook(
() => {
const isOutsideDropdownOpen = useRecoilComponentValue(
const isOutsideDropdownOpen = useRecoilComponentValueV2(
isDropdownOpenComponentState,
outsideDropdownId,
);
const isInsideDropdownOpen = useRecoilComponentValue(
const isInsideDropdownOpen = useRecoilComponentValueV2(
isDropdownOpenComponentState,
);
@@ -1,40 +1,57 @@
import { renderHook } from '@testing-library/react';
import { Provider as JotaiProvider } from 'jotai';
import { act } from 'react';
import { RecoilRoot } from 'recoil';
import { DropdownComponentInstanceContext } from '@/ui/layout/dropdown/contexts/DropdownComponentInstanceContext';
import { useOpenDropdown } from '@/ui/layout/dropdown/hooks/useOpenDropdown';
import { isDropdownOpenComponentState } from '@/ui/layout/dropdown/states/isDropdownOpenComponentState';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilComponentValueV2';
import { jotaiStore } from '@/ui/utilities/state/jotai/jotaiStore';
const dropdownId = 'test-dropdown-id';
const outsideDropdownId = 'test-dropdown-id-outside';
const Wrapper = ({ children }: { children: React.ReactNode }) => {
return (
<RecoilRoot>
<DropdownComponentInstanceContext.Provider
value={{ instanceId: dropdownId }}
>
{children}
</DropdownComponentInstanceContext.Provider>
<DropdownComponentInstanceContext.Provider
value={{ instanceId: outsideDropdownId }}
></DropdownComponentInstanceContext.Provider>
</RecoilRoot>
<JotaiProvider store={jotaiStore}>
<RecoilRoot>
<DropdownComponentInstanceContext.Provider
value={{ instanceId: dropdownId }}
>
{children}
</DropdownComponentInstanceContext.Provider>
<DropdownComponentInstanceContext.Provider
value={{ instanceId: outsideDropdownId }}
></DropdownComponentInstanceContext.Provider>
</RecoilRoot>
</JotaiProvider>
);
};
describe('useOpenDropdown', () => {
beforeEach(() => {
jotaiStore.set(
isDropdownOpenComponentState.atomFamily({ instanceId: dropdownId }),
false,
);
jotaiStore.set(
isDropdownOpenComponentState.atomFamily({
instanceId: outsideDropdownId,
}),
false,
);
});
it('should open dropdown from inside component instance context', async () => {
const { result } = renderHook(
() => {
const isOutsideDropdownOpen = useRecoilComponentValue(
const isOutsideDropdownOpen = useRecoilComponentValueV2(
isDropdownOpenComponentState,
outsideDropdownId,
);
const isInsideDropdownOpen = useRecoilComponentValue(
const isInsideDropdownOpen = useRecoilComponentValueV2(
isDropdownOpenComponentState,
);
@@ -61,12 +78,12 @@ describe('useOpenDropdown', () => {
it('should open dropdown from outside component instance context', async () => {
const { result } = renderHook(
() => {
const isOutsideDropdownOpen = useRecoilComponentValue(
const isOutsideDropdownOpen = useRecoilComponentValueV2(
isDropdownOpenComponentState,
outsideDropdownId,
);
const isInsideDropdownOpen = useRecoilComponentValue(
const isInsideDropdownOpen = useRecoilComponentValueV2(
isDropdownOpenComponentState,
);
@@ -1,40 +1,57 @@
import { renderHook } from '@testing-library/react';
import { Provider as JotaiProvider } from 'jotai';
import { act } from 'react';
import { RecoilRoot } from 'recoil';
import { DropdownComponentInstanceContext } from '@/ui/layout/dropdown/contexts/DropdownComponentInstanceContext';
import { useToggleDropdown } from '@/ui/layout/dropdown/hooks/useToggleDropdown';
import { isDropdownOpenComponentState } from '@/ui/layout/dropdown/states/isDropdownOpenComponentState';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilComponentValueV2';
import { jotaiStore } from '@/ui/utilities/state/jotai/jotaiStore';
const dropdownId = 'test-dropdown-id';
const outsideDropdownId = 'test-dropdown-id-outside';
const Wrapper = ({ children }: { children: React.ReactNode }) => {
return (
<RecoilRoot>
<DropdownComponentInstanceContext.Provider
value={{ instanceId: dropdownId }}
>
{children}
</DropdownComponentInstanceContext.Provider>
<DropdownComponentInstanceContext.Provider
value={{ instanceId: outsideDropdownId }}
></DropdownComponentInstanceContext.Provider>
</RecoilRoot>
<JotaiProvider store={jotaiStore}>
<RecoilRoot>
<DropdownComponentInstanceContext.Provider
value={{ instanceId: dropdownId }}
>
{children}
</DropdownComponentInstanceContext.Provider>
<DropdownComponentInstanceContext.Provider
value={{ instanceId: outsideDropdownId }}
></DropdownComponentInstanceContext.Provider>
</RecoilRoot>
</JotaiProvider>
);
};
describe('useToggleDropdown', () => {
beforeEach(() => {
jotaiStore.set(
isDropdownOpenComponentState.atomFamily({ instanceId: dropdownId }),
false,
);
jotaiStore.set(
isDropdownOpenComponentState.atomFamily({
instanceId: outsideDropdownId,
}),
false,
);
});
it('should toggle dropdown from inside component instance context', async () => {
const { result } = renderHook(
() => {
const isOutsideDropdownOpen = useRecoilComponentValue(
const isOutsideDropdownOpen = useRecoilComponentValueV2(
isDropdownOpenComponentState,
outsideDropdownId,
);
const isInsideDropdownOpen = useRecoilComponentValue(
const isInsideDropdownOpen = useRecoilComponentValueV2(
isDropdownOpenComponentState,
);
const { toggleDropdown } = useToggleDropdown();
@@ -74,12 +91,12 @@ describe('useToggleDropdown', () => {
it('should toggle dropdown from outside component instance context', async () => {
const { result } = renderHook(
() => {
const isOutsideDropdownOpen = useRecoilComponentValue(
const isOutsideDropdownOpen = useRecoilComponentValueV2(
isDropdownOpenComponentState,
outsideDropdownId,
);
const isInsideDropdownOpen = useRecoilComponentValue(
const isInsideDropdownOpen = useRecoilComponentValueV2(
isDropdownOpenComponentState,
);
const { toggleDropdown } = useToggleDropdown();
@@ -1,8 +1,10 @@
import { useCallback } from 'react';
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';
import { useRecoilCallback } from 'recoil';
import { jotaiStore } from '@/ui/utilities/state/jotai/jotaiStore';
import { isDefined } from 'twenty-shared/utils';
export const useCloseAnyOpenDropdown = () => {
@@ -11,46 +13,41 @@ export const useCloseAnyOpenDropdown = () => {
const { removeFocusItemFromFocusStackById } =
useRemoveFocusItemFromFocusStackById();
const closeAnyOpenDropdown = useRecoilCallback(
({ snapshot, set }) =>
() => {
const previousDropdownFocusId = snapshot
.getLoadable(previousDropdownFocusIdState)
.getValue();
const closeAnyOpenDropdown = useCallback(() => {
const previousDropdownFocusId = jotaiStore.get(
previousDropdownFocusIdState.atom,
);
const activeDropdownFocusId = snapshot
.getLoadable(activeDropdownFocusIdState)
.getValue();
const activeDropdownFocusId = jotaiStore.get(
activeDropdownFocusIdState.atom,
);
const thereIsNoDropdownOpen =
!isDefined(activeDropdownFocusId) &&
!isDefined(previousDropdownFocusId);
const thereIsNoDropdownOpen =
!isDefined(activeDropdownFocusId) && !isDefined(previousDropdownFocusId);
if (thereIsNoDropdownOpen) {
return;
}
if (thereIsNoDropdownOpen) {
return;
}
const thereIsOneNestedDropdownOpen = isDefined(previousDropdownFocusId);
const thereIsOneNestedDropdownOpen = isDefined(previousDropdownFocusId);
if (isDefined(activeDropdownFocusId)) {
closeDropdown(activeDropdownFocusId);
removeFocusItemFromFocusStackById({
focusId: activeDropdownFocusId,
});
}
if (isDefined(activeDropdownFocusId)) {
closeDropdown(activeDropdownFocusId);
removeFocusItemFromFocusStackById({
focusId: activeDropdownFocusId,
});
}
if (thereIsOneNestedDropdownOpen) {
closeDropdown(previousDropdownFocusId);
removeFocusItemFromFocusStackById({
focusId: previousDropdownFocusId,
});
}
if (thereIsOneNestedDropdownOpen) {
closeDropdown(previousDropdownFocusId);
removeFocusItemFromFocusStackById({
focusId: previousDropdownFocusId,
});
}
set(previousDropdownFocusIdState, null);
set(activeDropdownFocusIdState, null);
},
[closeDropdown, removeFocusItemFromFocusStackById],
);
jotaiStore.set(previousDropdownFocusIdState.atom, null);
jotaiStore.set(activeDropdownFocusIdState.atom, null);
}, [closeDropdown, removeFocusItemFromFocusStackById]);
return { closeAnyOpenDropdown };
};
@@ -1,10 +1,12 @@
import { useCallback } from 'react';
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 { useRemoveFocusItemFromFocusStackById } from '@/ui/utilities/focus/hooks/useRemoveFocusItemFromFocusStackById';
import { useAvailableComponentInstanceId } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceId';
import { useRecoilCallback } from 'recoil';
import { jotaiStore } from '@/ui/utilities/state/jotai/jotaiStore';
import { isDefined } from 'twenty-shared/utils';
export const useCloseDropdown = () => {
@@ -17,40 +19,37 @@ export const useCloseDropdown = () => {
const dropdownComponentInstanceIdFromContext =
useAvailableComponentInstanceId(DropdownComponentInstanceContext);
const closeDropdown = useRecoilCallback(
({ set, snapshot }) =>
(dropdownComponentInstanceIdFromProps?: string) => {
const dropdownComponentInstanceId =
dropdownComponentInstanceIdFromProps ??
dropdownComponentInstanceIdFromContext;
const closeDropdown = useCallback(
(dropdownComponentInstanceIdFromProps?: string) => {
const dropdownComponentInstanceId =
dropdownComponentInstanceIdFromProps ??
dropdownComponentInstanceIdFromContext;
if (!isDefined(dropdownComponentInstanceId)) {
throw new Error('Dropdown component instance ID is not defined');
}
if (!isDefined(dropdownComponentInstanceId)) {
throw new Error('Dropdown component instance ID is not defined');
}
const isDropdownOpen = snapshot
.getLoadable(
isDropdownOpenComponentState.atomFamily({
instanceId: dropdownComponentInstanceId,
}),
)
.getValue();
const isDropdownOpen = jotaiStore.get(
isDropdownOpenComponentState.atomFamily({
instanceId: dropdownComponentInstanceId,
}),
);
if (isDropdownOpen) {
removeFocusItemFromFocusStackById({
focusId: dropdownComponentInstanceId,
});
if (isDropdownOpen) {
removeFocusItemFromFocusStackById({
focusId: dropdownComponentInstanceId,
});
goBackToPreviousDropdownFocusId();
goBackToPreviousDropdownFocusId();
set(
isDropdownOpenComponentState.atomFamily({
instanceId: dropdownComponentInstanceId,
}),
false,
);
}
},
jotaiStore.set(
isDropdownOpenComponentState.atomFamily({
instanceId: dropdownComponentInstanceId,
}),
false,
);
}
},
[
removeFocusItemFromFocusStackById,
goBackToPreviousDropdownFocusId,
@@ -1,22 +1,22 @@
import { useRecoilCallback } from 'recoil';
import { useCallback } from 'react';
import { activeDropdownFocusIdState } from '@/ui/layout/dropdown/states/activeDropdownFocusIdState';
import { previousDropdownFocusIdState } from '@/ui/layout/dropdown/states/previousDropdownFocusIdState';
import { jotaiStore } from '@/ui/utilities/state/jotai/jotaiStore';
// TODO: this won't work for more than 1 nested dropdown
export const useGoBackToPreviousDropdownFocusId = () => {
const goBackToPreviousDropdownFocusId = useRecoilCallback(
({ snapshot, set }) =>
() => {
const previouslyFocusedDropdownId = snapshot
.getLoadable(previousDropdownFocusIdState)
.getValue();
const goBackToPreviousDropdownFocusId = useCallback(() => {
const previouslyFocusedDropdownId = jotaiStore.get(
previousDropdownFocusIdState.atom,
);
set(activeDropdownFocusIdState, previouslyFocusedDropdownId);
set(previousDropdownFocusIdState, null);
},
[],
);
jotaiStore.set(
activeDropdownFocusIdState.atom,
previouslyFocusedDropdownId,
);
jotaiStore.set(previousDropdownFocusIdState.atom, null);
}, []);
return {
goBackToPreviousDropdownFocusId,
@@ -1,3 +1,5 @@
import { useCallback } from 'react';
import { DropdownComponentInstanceContext } from '@/ui/layout/dropdown/contexts/DropdownComponentInstanceContext';
import { useSetActiveDropdownFocusIdAndMemorizePrevious } from '@/ui/layout/dropdown/hooks/useSetFocusedDropdownIdAndMemorizePrevious';
@@ -6,7 +8,7 @@ import { usePushFocusItemToFocusStack } from '@/ui/utilities/focus/hooks/usePush
import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType';
import { type GlobalHotkeysConfig } from '@/ui/utilities/hotkey/types/GlobalHotkeysConfig';
import { useAvailableComponentInstanceId } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceId';
import { useRecoilCallback } from 'recoil';
import { jotaiStore } from '@/ui/utilities/state/jotai/jotaiStore';
import { isDefined } from 'twenty-shared/utils';
type OpenDropdownArgs = {
@@ -22,44 +24,41 @@ export const useOpenDropdown = () => {
const dropdownComponentInstanceIdFromContext =
useAvailableComponentInstanceId(DropdownComponentInstanceContext);
const openDropdown = useRecoilCallback(
({ set }) =>
(args?: OpenDropdownArgs | null | undefined) => {
const dropdownComponentInstanceId =
args?.dropdownComponentInstanceIdFromProps ??
dropdownComponentInstanceIdFromContext;
const openDropdown = useCallback(
(args?: OpenDropdownArgs | null | undefined) => {
const dropdownComponentInstanceId =
args?.dropdownComponentInstanceIdFromProps ??
dropdownComponentInstanceIdFromContext;
if (!isDefined(dropdownComponentInstanceId)) {
throw new Error('Dropdown component instance ID is not defined');
}
if (!isDefined(dropdownComponentInstanceId)) {
throw new Error('Dropdown component instance ID is not defined');
}
set(
isDropdownOpenComponentState.atomFamily({
instanceId: dropdownComponentInstanceId,
}),
true,
);
jotaiStore.set(
isDropdownOpenComponentState.atomFamily({
instanceId: dropdownComponentInstanceId,
}),
true,
);
setActiveDropdownFocusIdAndMemorizePrevious(
dropdownComponentInstanceId,
);
setActiveDropdownFocusIdAndMemorizePrevious(dropdownComponentInstanceId);
pushFocusItemToFocusStack({
focusId: dropdownComponentInstanceId,
component: {
type: FocusComponentType.DROPDOWN,
instanceId: dropdownComponentInstanceId,
},
globalHotkeysConfig: {
enableGlobalHotkeysConflictingWithKeyboard:
args?.globalHotkeysConfig
?.enableGlobalHotkeysConflictingWithKeyboard ?? false,
enableGlobalHotkeysWithModifiers:
args?.globalHotkeysConfig?.enableGlobalHotkeysWithModifiers ??
false,
},
});
},
pushFocusItemToFocusStack({
focusId: dropdownComponentInstanceId,
component: {
type: FocusComponentType.DROPDOWN,
instanceId: dropdownComponentInstanceId,
},
globalHotkeysConfig: {
enableGlobalHotkeysConflictingWithKeyboard:
args?.globalHotkeysConfig
?.enableGlobalHotkeysConflictingWithKeyboard ?? false,
enableGlobalHotkeysWithModifiers:
args?.globalHotkeysConfig?.enableGlobalHotkeysWithModifiers ??
false,
},
});
},
[
pushFocusItemToFocusStack,
setActiveDropdownFocusIdAndMemorizePrevious,
@@ -1,23 +1,23 @@
import { useRecoilCallback } from 'recoil';
import { useCallback } from 'react';
import { activeDropdownFocusIdState } from '@/ui/layout/dropdown/states/activeDropdownFocusIdState';
import { previousDropdownFocusIdState } from '@/ui/layout/dropdown/states/previousDropdownFocusIdState';
import { jotaiStore } from '@/ui/utilities/state/jotai/jotaiStore';
export const useSetActiveDropdownFocusIdAndMemorizePrevious = () => {
const setActiveDropdownFocusIdAndMemorizePrevious = useRecoilCallback(
({ snapshot, set }) =>
(dropdownId: string | null) => {
const activeDropdownFocusId = snapshot
.getLoadable(activeDropdownFocusIdState)
.getValue();
const setActiveDropdownFocusIdAndMemorizePrevious = useCallback(
(dropdownId: string | null) => {
const activeDropdownFocusId = jotaiStore.get(
activeDropdownFocusIdState.atom,
);
if (activeDropdownFocusId === dropdownId) {
return;
}
if (activeDropdownFocusId === dropdownId) {
return;
}
set(previousDropdownFocusIdState, activeDropdownFocusId);
set(activeDropdownFocusIdState, dropdownId);
},
jotaiStore.set(previousDropdownFocusIdState.atom, activeDropdownFocusId);
jotaiStore.set(activeDropdownFocusIdState.atom, dropdownId);
},
[],
);
@@ -1,10 +1,12 @@
import { useCallback } from 'react';
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 { type GlobalHotkeysConfig } from '@/ui/utilities/hotkey/types/GlobalHotkeysConfig';
import { useAvailableComponentInstanceId } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceId';
import { useRecoilCallback } from 'recoil';
import { jotaiStore } from '@/ui/utilities/state/jotai/jotaiStore';
import { isDefined } from 'twenty-shared/utils';
type ToggleDropdownArgs = {
@@ -19,34 +21,31 @@ export const useToggleDropdown = () => {
const { openDropdown } = useOpenDropdown();
const { closeDropdown } = useCloseDropdown();
const toggleDropdown = useRecoilCallback(
({ snapshot }) =>
(args?: ToggleDropdownArgs | null | undefined) => {
const dropdownComponentInstanceId =
args?.dropdownComponentInstanceIdFromProps ??
dropdownComponentInstanceIdFromContext;
const toggleDropdown = useCallback(
(args?: ToggleDropdownArgs | null | undefined) => {
const dropdownComponentInstanceId =
args?.dropdownComponentInstanceIdFromProps ??
dropdownComponentInstanceIdFromContext;
if (!isDefined(dropdownComponentInstanceId)) {
throw new Error('Dropdown component instance ID is not defined');
}
if (!isDefined(dropdownComponentInstanceId)) {
throw new Error('Dropdown component instance ID is not defined');
}
const isDropdownOpen = snapshot
.getLoadable(
isDropdownOpenComponentState.atomFamily({
instanceId: dropdownComponentInstanceId,
}),
)
.getValue();
const isDropdownOpen = jotaiStore.get(
isDropdownOpenComponentState.atomFamily({
instanceId: dropdownComponentInstanceId,
}),
);
if (isDropdownOpen) {
closeDropdown(dropdownComponentInstanceId);
} else {
openDropdown({
dropdownComponentInstanceIdFromProps: dropdownComponentInstanceId,
globalHotkeysConfig: args?.globalHotkeysConfig,
});
}
},
if (isDropdownOpen) {
closeDropdown(dropdownComponentInstanceId);
} else {
openDropdown({
dropdownComponentInstanceIdFromProps: dropdownComponentInstanceId,
globalHotkeysConfig: args?.globalHotkeysConfig,
});
}
},
[closeDropdown, openDropdown, dropdownComponentInstanceIdFromContext],
);
@@ -1,6 +1,6 @@
import { createState } from '@/ui/utilities/state/utils/createState';
import { createStateV2 } from '@/ui/utilities/state/jotai/utils/createStateV2';
export const activeDropdownFocusIdState = createState<string | null>({
export const activeDropdownFocusIdState = createStateV2<string | null>({
key: 'activeDropdownFocusIdState',
defaultValue: null,
});
@@ -1,10 +1,10 @@
import { DropdownComponentInstanceContext } from '@/ui/layout/dropdown/contexts/DropdownComponentInstanceContext';
import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState';
import { createComponentStateV2 } from '@/ui/utilities/state/jotai/utils/createComponentStateV2';
import { type Placement } from '@floating-ui/react';
export const dropdownPlacementComponentState =
createComponentState<Placement | null>({
createComponentStateV2<Placement | null>({
key: 'dropdownPlacementComponentState',
componentInstanceContext: DropdownComponentInstanceContext,
defaultValue: null,
@@ -1,7 +1,7 @@
import { DropdownComponentInstanceContext } from '@/ui/layout/dropdown/contexts/DropdownComponentInstanceContext';
import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState';
import { createComponentStateV2 } from '@/ui/utilities/state/jotai/utils/createComponentStateV2';
export const dropdownMaxHeightComponentState = createComponentState<
export const dropdownMaxHeightComponentState = createComponentStateV2<
number | undefined
>({
key: 'dropdownMaxHeightComponentState',
@@ -1,7 +1,7 @@
import { DropdownComponentInstanceContext } from '@/ui/layout/dropdown/contexts/DropdownComponentInstanceContext';
import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState';
import { createComponentStateV2 } from '@/ui/utilities/state/jotai/utils/createComponentStateV2';
export const dropdownMaxWidthComponentState = createComponentState<
export const dropdownMaxWidthComponentState = createComponentStateV2<
number | undefined
>({
key: 'dropdownMaxWidthComponentState',
@@ -1,7 +1,7 @@
import { DropdownComponentInstanceContext } from '@/ui/layout/dropdown/contexts/DropdownComponentInstanceContext';
import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState';
import { createComponentStateV2 } from '@/ui/utilities/state/jotai/utils/createComponentStateV2';
export const dropdownYPositionComponentState = createComponentState<
export const dropdownYPositionComponentState = createComponentStateV2<
number | undefined
>({
key: 'dropdownYPositionComponentState',
@@ -1,7 +1,7 @@
import { DropdownComponentInstanceContext } from '@/ui/layout/dropdown/contexts/DropdownComponentInstanceContext';
import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState';
import { createComponentStateV2 } from '@/ui/utilities/state/jotai/utils/createComponentStateV2';
export const isDropdownOpenComponentState = createComponentState<boolean>({
export const isDropdownOpenComponentState = createComponentStateV2<boolean>({
key: 'isDropdownOpenComponentState',
defaultValue: false,
componentInstanceContext: DropdownComponentInstanceContext,
@@ -1,6 +1,6 @@
import { createState } from '@/ui/utilities/state/utils/createState';
import { createStateV2 } from '@/ui/utilities/state/jotai/utils/createStateV2';
export const previousDropdownFocusIdState = createState<string | null>({
export const previousDropdownFocusIdState = createStateV2<string | null>({
key: 'previousDropdownFocusIdState',
defaultValue: null,
});
@@ -0,0 +1,30 @@
import { useAtom } from 'jotai';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { globalComponentInstanceContextMap } from '@/ui/utilities/state/component-state/utils/globalComponentInstanceContextMap';
import { type ComponentStateV2 } from '@/ui/utilities/state/jotai/types/ComponentStateV2';
export const useRecoilComponentStateV2 = <StateType>(
componentState: ComponentStateV2<StateType>,
instanceIdFromProps?: string,
): [
StateType,
(value: StateType | ((prev: StateType) => StateType)) => void,
] => {
const componentInstanceContext = globalComponentInstanceContextMap.get(
componentState.key,
);
if (!componentInstanceContext) {
throw new Error(
`Instance context for key "${componentState.key}" is not defined`,
);
}
const instanceId = useAvailableComponentInstanceIdOrThrow(
componentInstanceContext,
instanceIdFromProps,
);
return useAtom(componentState.atomFamily({ instanceId }));
};
@@ -0,0 +1,27 @@
import { useAtomValue } from 'jotai';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { globalComponentInstanceContextMap } from '@/ui/utilities/state/component-state/utils/globalComponentInstanceContextMap';
import { type ComponentStateV2 } from '@/ui/utilities/state/jotai/types/ComponentStateV2';
export const useRecoilComponentValueV2 = <StateType>(
componentState: ComponentStateV2<StateType>,
instanceIdFromProps?: string,
): StateType => {
const componentInstanceContext = globalComponentInstanceContextMap.get(
componentState.key,
);
if (!componentInstanceContext) {
throw new Error(
`Instance context for key "${componentState.key}" is not defined`,
);
}
const instanceId = useAvailableComponentInstanceIdOrThrow(
componentInstanceContext,
instanceIdFromProps,
);
return useAtomValue(componentState.atomFamily({ instanceId }));
};
@@ -0,0 +1,27 @@
import { useSetAtom } from 'jotai';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { globalComponentInstanceContextMap } from '@/ui/utilities/state/component-state/utils/globalComponentInstanceContextMap';
import { type ComponentStateV2 } from '@/ui/utilities/state/jotai/types/ComponentStateV2';
export const useSetRecoilComponentStateV2 = <ValueType>(
componentState: ComponentStateV2<ValueType>,
instanceIdFromProps?: string,
): ((value: ValueType | ((prev: ValueType) => ValueType)) => void) => {
const componentInstanceContext = globalComponentInstanceContextMap.get(
componentState.key,
);
if (!componentInstanceContext) {
throw new Error(
`Instance context for key "${componentState.key}" is not defined`,
);
}
const instanceId = useAvailableComponentInstanceIdOrThrow(
componentInstanceContext,
instanceIdFromProps,
);
return useSetAtom(componentState.atomFamily({ instanceId }));
};
@@ -1,3 +1,8 @@
import { createStore } from 'jotai';
export const jotaiStore = createStore();
export let jotaiStore = createStore();
export const resetJotaiStore = () => {
jotaiStore = createStore();
return jotaiStore;
};
@@ -0,0 +1,15 @@
import { type WritableAtom } from 'jotai';
import { type ComponentStateKey } from '@/ui/utilities/state/component-state/types/ComponentStateKey';
type JotaiWritableAtom<ValueType> = WritableAtom<
ValueType,
[ValueType | ((prev: ValueType) => ValueType)],
void
>;
export type ComponentStateV2<ValueType> = {
type: 'ComponentStateV2';
key: string;
atomFamily: (key: ComponentStateKey) => JotaiWritableAtom<ValueType>;
};
@@ -0,0 +1,50 @@
import { atom } from 'jotai';
import { type ComponentInstanceStateContext } from '@/ui/utilities/state/component-state/types/ComponentInstanceStateContext';
import { type ComponentStateKey } from '@/ui/utilities/state/component-state/types/ComponentStateKey';
import { globalComponentInstanceContextMap } from '@/ui/utilities/state/component-state/utils/globalComponentInstanceContextMap';
import { type ComponentStateV2 } from '@/ui/utilities/state/jotai/types/ComponentStateV2';
import { isDefined } from 'twenty-shared/utils';
export const createComponentStateV2 = <ValueType>({
key,
defaultValue,
componentInstanceContext,
}: {
key: string;
defaultValue: ValueType;
componentInstanceContext: ComponentInstanceStateContext<any> | null;
}): ComponentStateV2<ValueType> => {
if (isDefined(componentInstanceContext)) {
globalComponentInstanceContextMap.set(key, componentInstanceContext);
}
const atomCache = new Map<
string,
ReturnType<ComponentStateV2<ValueType>['atomFamily']>
>();
const familyFunction = ({
instanceId,
}: ComponentStateKey): ReturnType<
ComponentStateV2<ValueType>['atomFamily']
> => {
const existing = atomCache.get(instanceId);
if (existing !== undefined) {
return existing;
}
const baseAtom = atom(defaultValue);
baseAtom.debugLabel = `${key}__${instanceId}`;
atomCache.set(instanceId, baseAtom);
return baseAtom;
};
return {
type: 'ComponentStateV2',
key,
atomFamily: familyFunction,
};
};