@@ -1,4 +1,4 @@
|
||||
import { useRef, useState } from 'react';
|
||||
import { useCallback, useRef, useState } from 'react';
|
||||
|
||||
import { useRegisterInputEvents } from '@/object-record/record-field/ui/meta-types/input/hooks/useRegisterInputEvents';
|
||||
import { DatePicker } from '@/ui/input/components/internal/date/components/DatePicker';
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
} from '@/ui/input/components/internal/date/components/DateTimePicker';
|
||||
import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
|
||||
import { currentFocusIdSelector } from '@/ui/utilities/focus/states/currentFocusIdSelector';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
import { useStore } from 'jotai';
|
||||
import { type Nullable } from 'twenty-ui/utilities';
|
||||
|
||||
export type DateInputProps = {
|
||||
@@ -39,6 +39,8 @@ export const DateInput = ({
|
||||
onSubmit,
|
||||
hideHeaderInput,
|
||||
}: DateInputProps) => {
|
||||
const store = useStore();
|
||||
|
||||
const [internalValue, setInternalValue] = useState(value);
|
||||
|
||||
const wrapperRef = useRef<HTMLDivElement>(null);
|
||||
@@ -75,25 +77,23 @@ export const DateInput = ({
|
||||
onEscape(internalValue);
|
||||
};
|
||||
|
||||
const handleClickOutside = useRecoilCallback(
|
||||
({ snapshot }) =>
|
||||
(event: MouseEvent | TouchEvent) => {
|
||||
const currentFocusId = snapshot
|
||||
.getLoadable(currentFocusIdSelector)
|
||||
.getValue();
|
||||
const handleClickOutside = useCallback(
|
||||
(event: MouseEvent | TouchEvent) => {
|
||||
const currentFocusId = store.get(currentFocusIdSelector.atom);
|
||||
|
||||
if (currentFocusId === instanceId) {
|
||||
closeDropdownYearSelect(MONTH_AND_YEAR_DROPDOWN_YEAR_SELECT_ID);
|
||||
closeDropdownMonthSelect(MONTH_AND_YEAR_DROPDOWN_MONTH_SELECT_ID);
|
||||
onClickOutside(event, internalValue);
|
||||
}
|
||||
},
|
||||
if (currentFocusId === instanceId) {
|
||||
closeDropdownYearSelect(MONTH_AND_YEAR_DROPDOWN_YEAR_SELECT_ID);
|
||||
closeDropdownMonthSelect(MONTH_AND_YEAR_DROPDOWN_MONTH_SELECT_ID);
|
||||
onClickOutside(event, internalValue);
|
||||
}
|
||||
},
|
||||
[
|
||||
instanceId,
|
||||
closeDropdownYearSelect,
|
||||
closeDropdownMonthSelect,
|
||||
onClickOutside,
|
||||
internalValue,
|
||||
store,
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useRef, useState } from 'react';
|
||||
import { useCallback, useRef, useState } from 'react';
|
||||
|
||||
import { useRegisterInputEvents } from '@/object-record/record-field/ui/meta-types/input/hooks/useRegisterInputEvents';
|
||||
import {
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
import { useUserTimezone } from '@/ui/input/components/internal/date/hooks/useUserTimezone';
|
||||
import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
|
||||
import { currentFocusIdSelector } from '@/ui/utilities/focus/states/currentFocusIdSelector';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
import { useStore } from 'jotai';
|
||||
import { type Temporal } from 'temporal-polyfill';
|
||||
import { type Nullable } from 'twenty-ui/utilities';
|
||||
|
||||
@@ -41,6 +41,8 @@ export const DateTimeInput = ({
|
||||
onSubmit,
|
||||
hideHeaderInput,
|
||||
}: DateTimeInputProps) => {
|
||||
const store = useStore();
|
||||
|
||||
const [internalValue, setInternalValue] = useState(value);
|
||||
const { userTimezone } = useUserTimezone();
|
||||
|
||||
@@ -78,25 +80,23 @@ export const DateTimeInput = ({
|
||||
onEscape(internalValue);
|
||||
};
|
||||
|
||||
const handleClickOutside = useRecoilCallback(
|
||||
({ snapshot }) =>
|
||||
(event: MouseEvent | TouchEvent) => {
|
||||
const currentFocusId = snapshot
|
||||
.getLoadable(currentFocusIdSelector)
|
||||
.getValue();
|
||||
const handleClickOutside = useCallback(
|
||||
(event: MouseEvent | TouchEvent) => {
|
||||
const currentFocusId = store.get(currentFocusIdSelector.atom);
|
||||
|
||||
if (currentFocusId === instanceId) {
|
||||
closeDropdownYearSelect(MONTH_AND_YEAR_DROPDOWN_YEAR_SELECT_ID);
|
||||
closeDropdownMonthSelect(MONTH_AND_YEAR_DROPDOWN_MONTH_SELECT_ID);
|
||||
onClickOutside(event, internalValue);
|
||||
}
|
||||
},
|
||||
if (currentFocusId === instanceId) {
|
||||
closeDropdownYearSelect(MONTH_AND_YEAR_DROPDOWN_YEAR_SELECT_ID);
|
||||
closeDropdownMonthSelect(MONTH_AND_YEAR_DROPDOWN_MONTH_SELECT_ID);
|
||||
onClickOutside(event, internalValue);
|
||||
}
|
||||
},
|
||||
[
|
||||
instanceId,
|
||||
closeDropdownYearSelect,
|
||||
closeDropdownMonthSelect,
|
||||
onClickOutside,
|
||||
internalValue,
|
||||
store,
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectab
|
||||
import { selectedItemIdComponentState } from '@/ui/layout/selectable-list/states/selectedItemIdComponentState';
|
||||
import { useHotkeysOnFocusedElement } from '@/ui/utilities/hotkey/hooks/useHotkeysOnFocusedElement';
|
||||
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilComponentValueV2';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { type SelectOption } from 'twenty-ui/input';
|
||||
@@ -47,7 +47,7 @@ export const MultiSelectInput = ({
|
||||
selectableListComponentInstanceId,
|
||||
);
|
||||
|
||||
const selectedItemId = useRecoilComponentValue(
|
||||
const selectedItemId = useRecoilComponentValueV2(
|
||||
selectedItemIdComponentState,
|
||||
selectableListComponentInstanceId,
|
||||
);
|
||||
|
||||
@@ -17,7 +17,7 @@ import { type DropdownOffset } from '@/ui/layout/dropdown/types/DropdownOffset';
|
||||
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
|
||||
import { selectedItemIdComponentState } from '@/ui/layout/selectable-list/states/selectedItemIdComponentState';
|
||||
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilComponentValueV2';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { useRecoilValue, useResetRecoilState } from 'recoil';
|
||||
import { IconApps, type IconComponent, useIcons } from 'twenty-ui/display';
|
||||
@@ -99,7 +99,7 @@ const IconPickerIcon = ({
|
||||
Icon,
|
||||
focusedIconKey,
|
||||
}: IconPickerIconProps) => {
|
||||
const isSelectedItemId = useRecoilComponentValue(
|
||||
const isSelectedItemId = useRecoilComponentValueV2(
|
||||
selectedItemIdComponentState,
|
||||
iconKey,
|
||||
);
|
||||
@@ -242,7 +242,7 @@ export const IconPicker = ({
|
||||
const selectableListInstanceId = 'icon-list';
|
||||
|
||||
const focusedIconKey =
|
||||
useRecoilComponentValue(
|
||||
useRecoilComponentValueV2(
|
||||
selectedItemIdComponentState,
|
||||
selectableListInstanceId,
|
||||
) ?? undefined;
|
||||
|
||||
@@ -16,7 +16,7 @@ import { SelectableList } from '@/ui/layout/selectable-list/components/Selectabl
|
||||
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
|
||||
import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList';
|
||||
import { selectedItemIdComponentState } from '@/ui/layout/selectable-list/states/selectedItemIdComponentState';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilComponentValueV2';
|
||||
import { isNonEmptyArray, isNonEmptyString } from '@sniptt/guards';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { type IconComponent } from 'twenty-ui/display';
|
||||
@@ -140,7 +140,7 @@ export const Select = <Value extends SelectValue>({
|
||||
|
||||
const selectableItemIdArray = filteredOptions.map((option) => option.label);
|
||||
|
||||
const selectedItemId = useRecoilComponentValue(
|
||||
const selectedItemId = useRecoilComponentValueV2(
|
||||
selectedItemIdComponentState,
|
||||
dropdownId,
|
||||
);
|
||||
|
||||
@@ -8,7 +8,7 @@ import { SelectableListComponentInstanceContext } from '@/ui/layout/selectable-l
|
||||
import { selectedItemIdComponentState } from '@/ui/layout/selectable-list/states/selectedItemIdComponentState';
|
||||
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
|
||||
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilComponentValueV2';
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
@@ -46,7 +46,7 @@ export const SelectInput = ({
|
||||
SelectableListComponentInstanceContext,
|
||||
);
|
||||
|
||||
const selectedItemId = useRecoilComponentValue(
|
||||
const selectedItemId = useRecoilComponentValueV2(
|
||||
selectedItemIdComponentState,
|
||||
selectableListInstanceId,
|
||||
);
|
||||
|
||||
+8
-8
@@ -4,7 +4,7 @@ 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 { jotaiStore } from '@/ui/utilities/state/jotai/jotaiStore';
|
||||
import { useStore } from 'jotai';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const useCloseAnyOpenDropdown = () => {
|
||||
@@ -13,14 +13,14 @@ export const useCloseAnyOpenDropdown = () => {
|
||||
const { removeFocusItemFromFocusStackById } =
|
||||
useRemoveFocusItemFromFocusStackById();
|
||||
|
||||
const store = useStore();
|
||||
|
||||
const closeAnyOpenDropdown = useCallback(() => {
|
||||
const previousDropdownFocusId = jotaiStore.get(
|
||||
const previousDropdownFocusId = store.get(
|
||||
previousDropdownFocusIdState.atom,
|
||||
);
|
||||
|
||||
const activeDropdownFocusId = jotaiStore.get(
|
||||
activeDropdownFocusIdState.atom,
|
||||
);
|
||||
const activeDropdownFocusId = store.get(activeDropdownFocusIdState.atom);
|
||||
|
||||
const thereIsNoDropdownOpen =
|
||||
!isDefined(activeDropdownFocusId) && !isDefined(previousDropdownFocusId);
|
||||
@@ -45,9 +45,9 @@ export const useCloseAnyOpenDropdown = () => {
|
||||
});
|
||||
}
|
||||
|
||||
jotaiStore.set(previousDropdownFocusIdState.atom, null);
|
||||
jotaiStore.set(activeDropdownFocusIdState.atom, null);
|
||||
}, [closeDropdown, removeFocusItemFromFocusStackById]);
|
||||
store.set(previousDropdownFocusIdState.atom, null);
|
||||
store.set(activeDropdownFocusIdState.atom, null);
|
||||
}, [closeDropdown, removeFocusItemFromFocusStackById, store]);
|
||||
|
||||
return { closeAnyOpenDropdown };
|
||||
};
|
||||
|
||||
@@ -6,7 +6,7 @@ import { isDropdownOpenComponentState } from '@/ui/layout/dropdown/states/isDrop
|
||||
|
||||
import { useRemoveFocusItemFromFocusStackById } from '@/ui/utilities/focus/hooks/useRemoveFocusItemFromFocusStackById';
|
||||
import { useAvailableComponentInstanceId } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceId';
|
||||
import { jotaiStore } from '@/ui/utilities/state/jotai/jotaiStore';
|
||||
import { useStore } from 'jotai';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const useCloseDropdown = () => {
|
||||
@@ -16,6 +16,8 @@ export const useCloseDropdown = () => {
|
||||
const { removeFocusItemFromFocusStackById } =
|
||||
useRemoveFocusItemFromFocusStackById();
|
||||
|
||||
const store = useStore();
|
||||
|
||||
const dropdownComponentInstanceIdFromContext =
|
||||
useAvailableComponentInstanceId(DropdownComponentInstanceContext);
|
||||
|
||||
@@ -29,7 +31,7 @@ export const useCloseDropdown = () => {
|
||||
throw new Error('Dropdown component instance ID is not defined');
|
||||
}
|
||||
|
||||
const isDropdownOpen = jotaiStore.get(
|
||||
const isDropdownOpen = store.get(
|
||||
isDropdownOpenComponentState.atomFamily({
|
||||
instanceId: dropdownComponentInstanceId,
|
||||
}),
|
||||
@@ -42,7 +44,7 @@ export const useCloseDropdown = () => {
|
||||
|
||||
goBackToPreviousDropdownFocusId();
|
||||
|
||||
jotaiStore.set(
|
||||
store.set(
|
||||
isDropdownOpenComponentState.atomFamily({
|
||||
instanceId: dropdownComponentInstanceId,
|
||||
}),
|
||||
@@ -54,6 +56,7 @@ export const useCloseDropdown = () => {
|
||||
removeFocusItemFromFocusStackById,
|
||||
goBackToPreviousDropdownFocusId,
|
||||
dropdownComponentInstanceIdFromContext,
|
||||
store,
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
+7
-8
@@ -2,21 +2,20 @@ 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';
|
||||
import { useStore } from 'jotai';
|
||||
|
||||
// TODO: this won't work for more than 1 nested dropdown
|
||||
export const useGoBackToPreviousDropdownFocusId = () => {
|
||||
const store = useStore();
|
||||
|
||||
const goBackToPreviousDropdownFocusId = useCallback(() => {
|
||||
const previouslyFocusedDropdownId = jotaiStore.get(
|
||||
const previouslyFocusedDropdownId = store.get(
|
||||
previousDropdownFocusIdState.atom,
|
||||
);
|
||||
|
||||
jotaiStore.set(
|
||||
activeDropdownFocusIdState.atom,
|
||||
previouslyFocusedDropdownId,
|
||||
);
|
||||
jotaiStore.set(previousDropdownFocusIdState.atom, null);
|
||||
}, []);
|
||||
store.set(activeDropdownFocusIdState.atom, previouslyFocusedDropdownId);
|
||||
store.set(previousDropdownFocusIdState.atom, null);
|
||||
}, [store]);
|
||||
|
||||
return {
|
||||
goBackToPreviousDropdownFocusId,
|
||||
|
||||
@@ -8,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 { jotaiStore } from '@/ui/utilities/state/jotai/jotaiStore';
|
||||
import { useStore } from 'jotai';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
type OpenDropdownArgs = {
|
||||
@@ -21,6 +21,8 @@ export const useOpenDropdown = () => {
|
||||
|
||||
const { setActiveDropdownFocusIdAndMemorizePrevious } =
|
||||
useSetActiveDropdownFocusIdAndMemorizePrevious();
|
||||
const store = useStore();
|
||||
|
||||
const dropdownComponentInstanceIdFromContext =
|
||||
useAvailableComponentInstanceId(DropdownComponentInstanceContext);
|
||||
|
||||
@@ -34,7 +36,7 @@ export const useOpenDropdown = () => {
|
||||
throw new Error('Dropdown component instance ID is not defined');
|
||||
}
|
||||
|
||||
jotaiStore.set(
|
||||
store.set(
|
||||
isDropdownOpenComponentState.atomFamily({
|
||||
instanceId: dropdownComponentInstanceId,
|
||||
}),
|
||||
@@ -63,6 +65,7 @@ export const useOpenDropdown = () => {
|
||||
pushFocusItemToFocusStack,
|
||||
setActiveDropdownFocusIdAndMemorizePrevious,
|
||||
dropdownComponentInstanceIdFromContext,
|
||||
store,
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
+7
-7
@@ -2,23 +2,23 @@ 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';
|
||||
import { useStore } from 'jotai';
|
||||
|
||||
export const useSetActiveDropdownFocusIdAndMemorizePrevious = () => {
|
||||
const store = useStore();
|
||||
|
||||
const setActiveDropdownFocusIdAndMemorizePrevious = useCallback(
|
||||
(dropdownId: string | null) => {
|
||||
const activeDropdownFocusId = jotaiStore.get(
|
||||
activeDropdownFocusIdState.atom,
|
||||
);
|
||||
const activeDropdownFocusId = store.get(activeDropdownFocusIdState.atom);
|
||||
|
||||
if (activeDropdownFocusId === dropdownId) {
|
||||
return;
|
||||
}
|
||||
|
||||
jotaiStore.set(previousDropdownFocusIdState.atom, activeDropdownFocusId);
|
||||
jotaiStore.set(activeDropdownFocusIdState.atom, dropdownId);
|
||||
store.set(previousDropdownFocusIdState.atom, activeDropdownFocusId);
|
||||
store.set(activeDropdownFocusIdState.atom, dropdownId);
|
||||
},
|
||||
[],
|
||||
[store],
|
||||
);
|
||||
|
||||
return {
|
||||
|
||||
@@ -6,7 +6,7 @@ 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 { jotaiStore } from '@/ui/utilities/state/jotai/jotaiStore';
|
||||
import { useStore } from 'jotai';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
type ToggleDropdownArgs = {
|
||||
@@ -21,6 +21,8 @@ export const useToggleDropdown = () => {
|
||||
const { openDropdown } = useOpenDropdown();
|
||||
const { closeDropdown } = useCloseDropdown();
|
||||
|
||||
const store = useStore();
|
||||
|
||||
const toggleDropdown = useCallback(
|
||||
(args?: ToggleDropdownArgs | null | undefined) => {
|
||||
const dropdownComponentInstanceId =
|
||||
@@ -31,7 +33,7 @@ export const useToggleDropdown = () => {
|
||||
throw new Error('Dropdown component instance ID is not defined');
|
||||
}
|
||||
|
||||
const isDropdownOpen = jotaiStore.get(
|
||||
const isDropdownOpen = store.get(
|
||||
isDropdownOpenComponentState.atomFamily({
|
||||
instanceId: dropdownComponentInstanceId,
|
||||
}),
|
||||
@@ -46,7 +48,12 @@ export const useToggleDropdown = () => {
|
||||
});
|
||||
}
|
||||
},
|
||||
[closeDropdown, openDropdown, dropdownComponentInstanceIdFromContext],
|
||||
[
|
||||
closeDropdown,
|
||||
openDropdown,
|
||||
dropdownComponentInstanceIdFromContext,
|
||||
store,
|
||||
],
|
||||
);
|
||||
|
||||
return {
|
||||
|
||||
@@ -9,7 +9,7 @@ import { MODAL_CLICK_OUTSIDE_LISTENER_EXCLUDED_ID } from '@/ui/layout/modal/cons
|
||||
import { useModal } from '@/ui/layout/modal/hooks/useModal';
|
||||
import { ClickOutsideListenerContext } from '@/ui/utilities/pointer-event/contexts/ClickOutsideListenerContext';
|
||||
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilComponentValueV2';
|
||||
import { css, useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
@@ -243,7 +243,7 @@ export const Modal = ({
|
||||
e.stopPropagation();
|
||||
};
|
||||
|
||||
const isModalOpened = useRecoilComponentValue(
|
||||
const isModalOpened = useRecoilComponentValueV2(
|
||||
isModalOpenedComponentState,
|
||||
modalId,
|
||||
);
|
||||
|
||||
+11
-8
@@ -1,24 +1,27 @@
|
||||
import { type Meta, type StoryObj } from '@storybook/react-vite';
|
||||
import {
|
||||
type Decorator,
|
||||
type Meta,
|
||||
type StoryObj,
|
||||
} from '@storybook/react-vite';
|
||||
import { expect, fn, userEvent, waitFor, within } from 'storybook/test';
|
||||
|
||||
import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal';
|
||||
import { isModalOpenedComponentState } from '@/ui/layout/modal/states/isModalOpenedComponentState';
|
||||
import { focusStackState } from '@/ui/utilities/focus/states/focusStackState';
|
||||
import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType';
|
||||
import { type SetRecoilState } from 'recoil';
|
||||
import { jotaiStore } from '@/ui/utilities/state/jotai/jotaiStore';
|
||||
import { ComponentDecorator } from 'twenty-ui/testing';
|
||||
import { RootDecorator } from '~/testing/decorators/RootDecorator';
|
||||
import { sleep } from '~/utils/sleep';
|
||||
|
||||
const initializeState = ({ set }: { set: SetRecoilState }) => {
|
||||
set(
|
||||
const JotaiInitDecorator: Decorator = (Story) => {
|
||||
jotaiStore.set(
|
||||
isModalOpenedComponentState.atomFamily({
|
||||
instanceId: 'confirmation-modal',
|
||||
}),
|
||||
true,
|
||||
);
|
||||
|
||||
set(focusStackState, [
|
||||
jotaiStore.set(focusStackState.atom, [
|
||||
{
|
||||
focusId: 'confirmation-modal',
|
||||
componentInstance: {
|
||||
@@ -31,14 +34,14 @@ const initializeState = ({ set }: { set: SetRecoilState }) => {
|
||||
},
|
||||
},
|
||||
]);
|
||||
return <Story />;
|
||||
};
|
||||
|
||||
const meta: Meta<typeof ConfirmationModal> = {
|
||||
title: 'UI/Layout/Modal/ConfirmationModal',
|
||||
component: ConfirmationModal,
|
||||
decorators: [RootDecorator, ComponentDecorator],
|
||||
decorators: [JotaiInitDecorator, RootDecorator, ComponentDecorator],
|
||||
parameters: {
|
||||
initializeState,
|
||||
disableHotkeyInitialization: true,
|
||||
},
|
||||
};
|
||||
|
||||
+11
-8
@@ -1,24 +1,27 @@
|
||||
import { type Meta, type StoryObj } from '@storybook/react-vite';
|
||||
import {
|
||||
type Decorator,
|
||||
type Meta,
|
||||
type StoryObj,
|
||||
} from '@storybook/react-vite';
|
||||
import { expect, fn, userEvent, waitFor, within } from 'storybook/test';
|
||||
|
||||
import { Modal } from '@/ui/layout/modal/components/Modal';
|
||||
import { isModalOpenedComponentState } from '@/ui/layout/modal/states/isModalOpenedComponentState';
|
||||
import { focusStackState } from '@/ui/utilities/focus/states/focusStackState';
|
||||
import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType';
|
||||
import { type SetRecoilState } from 'recoil';
|
||||
import { jotaiStore } from '@/ui/utilities/state/jotai/jotaiStore';
|
||||
import { ComponentDecorator } from 'twenty-ui/testing';
|
||||
import { RootDecorator } from '~/testing/decorators/RootDecorator';
|
||||
import { sleep } from '~/utils/sleep';
|
||||
|
||||
const initializeState = ({ set }: { set: SetRecoilState }) => {
|
||||
set(
|
||||
const JotaiInitDecorator: Decorator = (Story) => {
|
||||
jotaiStore.set(
|
||||
isModalOpenedComponentState.atomFamily({
|
||||
instanceId: 'modal-id',
|
||||
}),
|
||||
true,
|
||||
);
|
||||
|
||||
set(focusStackState, [
|
||||
jotaiStore.set(focusStackState.atom, [
|
||||
{
|
||||
focusId: 'modal-id',
|
||||
componentInstance: {
|
||||
@@ -31,14 +34,14 @@ const initializeState = ({ set }: { set: SetRecoilState }) => {
|
||||
},
|
||||
},
|
||||
]);
|
||||
return <Story />;
|
||||
};
|
||||
|
||||
const meta: Meta<typeof Modal> = {
|
||||
title: 'UI/Layout/Modal/Modal',
|
||||
component: Modal,
|
||||
decorators: [RootDecorator, ComponentDecorator],
|
||||
decorators: [JotaiInitDecorator, RootDecorator, ComponentDecorator],
|
||||
parameters: {
|
||||
initializeState,
|
||||
disableHotkeyInitialization: true,
|
||||
},
|
||||
};
|
||||
|
||||
+32
-18
@@ -1,28 +1,39 @@
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { RecoilRoot, useRecoilValue } from 'recoil';
|
||||
import { createStore, Provider } from 'jotai';
|
||||
import { type ReactNode, act } from 'react';
|
||||
import { RecoilRoot } from 'recoil';
|
||||
|
||||
import { useModal } from '@/ui/layout/modal/hooks/useModal';
|
||||
import { isModalOpenedComponentState } from '@/ui/layout/modal/states/isModalOpenedComponentState';
|
||||
import { act } from 'react';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilComponentValueV2';
|
||||
|
||||
const modalId = 'test-modal-id';
|
||||
|
||||
describe('useModal', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
const createTestWrapper = () => {
|
||||
const store = createStore();
|
||||
|
||||
const TestWrapper = ({ children }: { children: ReactNode }) => (
|
||||
<RecoilRoot>
|
||||
<Provider store={store}>{children}</Provider>
|
||||
</RecoilRoot>
|
||||
);
|
||||
|
||||
return TestWrapper;
|
||||
};
|
||||
|
||||
describe('useModal', () => {
|
||||
it('should open a modal', () => {
|
||||
const { result } = renderHook(
|
||||
() => {
|
||||
const modal = useModal();
|
||||
const isModalOpened = useRecoilValue(
|
||||
isModalOpenedComponentState.atomFamily({ instanceId: modalId }),
|
||||
const isModalOpened = useRecoilComponentValueV2(
|
||||
isModalOpenedComponentState,
|
||||
modalId,
|
||||
);
|
||||
return { modal, isModalOpened };
|
||||
},
|
||||
{
|
||||
wrapper: RecoilRoot,
|
||||
wrapper: createTestWrapper(),
|
||||
},
|
||||
);
|
||||
|
||||
@@ -37,13 +48,14 @@ describe('useModal', () => {
|
||||
const { result } = renderHook(
|
||||
() => {
|
||||
const modal = useModal();
|
||||
const isModalOpened = useRecoilValue(
|
||||
isModalOpenedComponentState.atomFamily({ instanceId: modalId }),
|
||||
const isModalOpened = useRecoilComponentValueV2(
|
||||
isModalOpenedComponentState,
|
||||
modalId,
|
||||
);
|
||||
return { modal, isModalOpened };
|
||||
},
|
||||
{
|
||||
wrapper: RecoilRoot,
|
||||
wrapper: createTestWrapper(),
|
||||
},
|
||||
);
|
||||
|
||||
@@ -63,13 +75,14 @@ describe('useModal', () => {
|
||||
const { result } = renderHook(
|
||||
() => {
|
||||
const modal = useModal();
|
||||
const isModalOpened = useRecoilValue(
|
||||
isModalOpenedComponentState.atomFamily({ instanceId: modalId }),
|
||||
const isModalOpened = useRecoilComponentValueV2(
|
||||
isModalOpenedComponentState,
|
||||
modalId,
|
||||
);
|
||||
return { modal, isModalOpened };
|
||||
},
|
||||
{
|
||||
wrapper: RecoilRoot,
|
||||
wrapper: createTestWrapper(),
|
||||
},
|
||||
);
|
||||
|
||||
@@ -86,13 +99,14 @@ describe('useModal', () => {
|
||||
const { result } = renderHook(
|
||||
() => {
|
||||
const modal = useModal();
|
||||
const isModalOpened = useRecoilValue(
|
||||
isModalOpenedComponentState.atomFamily({ instanceId: modalId }),
|
||||
const isModalOpened = useRecoilComponentValueV2(
|
||||
isModalOpenedComponentState,
|
||||
modalId,
|
||||
);
|
||||
return { modal, isModalOpened };
|
||||
},
|
||||
{
|
||||
wrapper: RecoilRoot,
|
||||
wrapper: createTestWrapper(),
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1,88 +1,83 @@
|
||||
import { useCallback } from 'react';
|
||||
import { useStore } from 'jotai';
|
||||
|
||||
import { isModalOpenedComponentState } from '@/ui/layout/modal/states/isModalOpenedComponentState';
|
||||
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 { useRecoilCallback } from 'recoil';
|
||||
|
||||
export const useModal = () => {
|
||||
const { pushFocusItemToFocusStack } = usePushFocusItemToFocusStack();
|
||||
const { removeFocusItemFromFocusStackById } =
|
||||
useRemoveFocusItemFromFocusStackById();
|
||||
|
||||
const closeModal = useRecoilCallback(
|
||||
({ set, snapshot }) =>
|
||||
(modalId: string) => {
|
||||
const isModalOpen = snapshot
|
||||
.getLoadable(
|
||||
isModalOpenedComponentState.atomFamily({ instanceId: modalId }),
|
||||
)
|
||||
.getValue();
|
||||
const store = useStore();
|
||||
|
||||
if (!isModalOpen) {
|
||||
return;
|
||||
}
|
||||
const closeModal = useCallback(
|
||||
(modalId: string) => {
|
||||
const isModalOpen = store.get(
|
||||
isModalOpenedComponentState.atomFamily({ instanceId: modalId }),
|
||||
);
|
||||
|
||||
removeFocusItemFromFocusStackById({
|
||||
focusId: modalId,
|
||||
});
|
||||
if (!isModalOpen) {
|
||||
return;
|
||||
}
|
||||
|
||||
set(
|
||||
isModalOpenedComponentState.atomFamily({ instanceId: modalId }),
|
||||
false,
|
||||
);
|
||||
},
|
||||
[removeFocusItemFromFocusStackById],
|
||||
removeFocusItemFromFocusStackById({
|
||||
focusId: modalId,
|
||||
});
|
||||
|
||||
store.set(
|
||||
isModalOpenedComponentState.atomFamily({ instanceId: modalId }),
|
||||
false,
|
||||
);
|
||||
},
|
||||
[store, removeFocusItemFromFocusStackById],
|
||||
);
|
||||
|
||||
const openModal = useRecoilCallback(
|
||||
({ set, snapshot }) =>
|
||||
(modalId: string) => {
|
||||
const isModalOpened = snapshot
|
||||
.getLoadable(
|
||||
isModalOpenedComponentState.atomFamily({ instanceId: modalId }),
|
||||
)
|
||||
.getValue();
|
||||
const openModal = useCallback(
|
||||
(modalId: string) => {
|
||||
const isModalOpened = store.get(
|
||||
isModalOpenedComponentState.atomFamily({ instanceId: modalId }),
|
||||
);
|
||||
|
||||
if (isModalOpened) {
|
||||
return;
|
||||
}
|
||||
if (isModalOpened) {
|
||||
return;
|
||||
}
|
||||
|
||||
set(
|
||||
isModalOpenedComponentState.atomFamily({ instanceId: modalId }),
|
||||
true,
|
||||
);
|
||||
store.set(
|
||||
isModalOpenedComponentState.atomFamily({ instanceId: modalId }),
|
||||
true,
|
||||
);
|
||||
|
||||
pushFocusItemToFocusStack({
|
||||
focusId: modalId,
|
||||
component: {
|
||||
type: FocusComponentType.MODAL,
|
||||
instanceId: modalId,
|
||||
},
|
||||
globalHotkeysConfig: {
|
||||
enableGlobalHotkeysWithModifiers: false,
|
||||
enableGlobalHotkeysConflictingWithKeyboard: false,
|
||||
},
|
||||
});
|
||||
},
|
||||
[pushFocusItemToFocusStack],
|
||||
pushFocusItemToFocusStack({
|
||||
focusId: modalId,
|
||||
component: {
|
||||
type: FocusComponentType.MODAL,
|
||||
instanceId: modalId,
|
||||
},
|
||||
globalHotkeysConfig: {
|
||||
enableGlobalHotkeysWithModifiers: false,
|
||||
enableGlobalHotkeysConflictingWithKeyboard: false,
|
||||
},
|
||||
});
|
||||
},
|
||||
[store, pushFocusItemToFocusStack],
|
||||
);
|
||||
|
||||
const toggleModal = useRecoilCallback(
|
||||
({ snapshot }) =>
|
||||
(modalId: string) => {
|
||||
const isModalOpen = snapshot
|
||||
.getLoadable(
|
||||
isModalOpenedComponentState.atomFamily({ instanceId: modalId }),
|
||||
)
|
||||
.getValue();
|
||||
const toggleModal = useCallback(
|
||||
(modalId: string) => {
|
||||
const isModalOpen = store.get(
|
||||
isModalOpenedComponentState.atomFamily({ instanceId: modalId }),
|
||||
);
|
||||
|
||||
if (isModalOpen) {
|
||||
closeModal(modalId);
|
||||
} else {
|
||||
openModal(modalId);
|
||||
}
|
||||
},
|
||||
[closeModal, openModal],
|
||||
if (isModalOpen) {
|
||||
closeModal(modalId);
|
||||
} else {
|
||||
openModal(modalId);
|
||||
}
|
||||
},
|
||||
[store, closeModal, openModal],
|
||||
);
|
||||
|
||||
return {
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState';
|
||||
import { createComponentStateV2 } from '@/ui/utilities/state/jotai/utils/createComponentStateV2';
|
||||
import { ModalComponentInstanceContext } from '@/ui/layout/modal/contexts/ModalComponentInstanceContext';
|
||||
|
||||
export const isModalOpenedComponentState = createComponentState<boolean>({
|
||||
export const isModalOpenedComponentState = createComponentStateV2<boolean>({
|
||||
key: 'isModalOpenedComponentState',
|
||||
defaultValue: false,
|
||||
componentInstanceContext: ModalComponentInstanceContext,
|
||||
|
||||
+2
-2
@@ -4,7 +4,7 @@ import { useSelectableListHotKeys } from '@/ui/layout/selectable-list/hooks/inte
|
||||
import { SelectableListComponentInstanceContext } from '@/ui/layout/selectable-list/states/contexts/SelectableListComponentInstanceContext';
|
||||
import { SelectableListContextProvider } from '@/ui/layout/selectable-list/states/contexts/SelectableListContext';
|
||||
import { selectableItemIdsComponentState } from '@/ui/layout/selectable-list/states/selectableItemIdsComponentState';
|
||||
import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState';
|
||||
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/jotai/hooks/useSetRecoilComponentStateV2';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { arrayToChunks } from '~/utils/array/arrayToChunks';
|
||||
|
||||
@@ -27,7 +27,7 @@ export const SelectableList = ({
|
||||
}: SelectableListProps) => {
|
||||
useSelectableListHotKeys(selectableListInstanceId, focusId, onSelect);
|
||||
|
||||
const setSelectableItemIds = useSetRecoilComponentState(
|
||||
const setSelectableItemIds = useSetRecoilComponentStateV2(
|
||||
selectableItemIdsComponentState,
|
||||
selectableListInstanceId,
|
||||
);
|
||||
|
||||
+4
-4
@@ -1,8 +1,8 @@
|
||||
import { type ReactNode, useEffect, useRef } from 'react';
|
||||
|
||||
import { SelectableListItemHotkeyEffect } from '@/ui/layout/selectable-list/components/SelectableListItemHotkeyEffect';
|
||||
import { isSelectedItemIdComponentFamilySelector } from '@/ui/layout/selectable-list/states/selectors/isSelectedItemIdComponentFamilySelector';
|
||||
import { useRecoilComponentFamilyValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValue';
|
||||
import { isSelectedItemIdComponentFamilyState } from '@/ui/layout/selectable-list/states/isSelectedItemIdComponentFamilyState';
|
||||
import { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilComponentFamilyValueV2';
|
||||
import styled from '@emotion/styled';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
@@ -25,8 +25,8 @@ export const SelectableListItem = ({
|
||||
children,
|
||||
onEnter,
|
||||
}: SelectableListItemProps) => {
|
||||
const isSelectedItemId = useRecoilComponentFamilyValue(
|
||||
isSelectedItemIdComponentFamilySelector,
|
||||
const isSelectedItemId = useRecoilComponentFamilyValueV2(
|
||||
isSelectedItemIdComponentFamilyState,
|
||||
itemId,
|
||||
);
|
||||
|
||||
|
||||
+9
-9
@@ -1,12 +1,12 @@
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { act } from 'react';
|
||||
import { RecoilRoot } from 'recoil';
|
||||
import { Provider } from 'jotai';
|
||||
|
||||
import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList';
|
||||
import { selectableItemIdsComponentState } from '@/ui/layout/selectable-list/states/selectableItemIdsComponentState';
|
||||
import { selectedItemIdComponentState } from '@/ui/layout/selectable-list/states/selectedItemIdComponentState';
|
||||
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';
|
||||
|
||||
const selectableListComponentInstanceId = 'testId';
|
||||
const testArr = [['1'], ['2'], ['3']];
|
||||
@@ -16,12 +16,12 @@ describe('useSelectableList', () => {
|
||||
it('Should setSelectableItemIds', async () => {
|
||||
const { result } = renderHook(
|
||||
() => {
|
||||
const setSelectableItemIds = useSetRecoilComponentState(
|
||||
const setSelectableItemIds = useSetRecoilComponentStateV2(
|
||||
selectableItemIdsComponentState,
|
||||
selectableListComponentInstanceId,
|
||||
);
|
||||
|
||||
const selectableItemIds = useRecoilComponentValue(
|
||||
const selectableItemIds = useRecoilComponentValueV2(
|
||||
selectableItemIdsComponentState,
|
||||
selectableListComponentInstanceId,
|
||||
);
|
||||
@@ -32,7 +32,7 @@ describe('useSelectableList', () => {
|
||||
};
|
||||
},
|
||||
{
|
||||
wrapper: RecoilRoot,
|
||||
wrapper: Provider,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -52,11 +52,11 @@ describe('useSelectableList', () => {
|
||||
selectableListComponentInstanceId,
|
||||
);
|
||||
|
||||
const selectedItemId = useRecoilComponentValue(
|
||||
const selectedItemId = useRecoilComponentValueV2(
|
||||
selectedItemIdComponentState,
|
||||
selectableListComponentInstanceId,
|
||||
);
|
||||
const setSelectedItemId = useSetRecoilComponentState(
|
||||
const setSelectedItemId = useSetRecoilComponentStateV2(
|
||||
selectedItemIdComponentState,
|
||||
selectableListComponentInstanceId,
|
||||
);
|
||||
@@ -67,7 +67,7 @@ describe('useSelectableList', () => {
|
||||
};
|
||||
},
|
||||
{
|
||||
wrapper: RecoilRoot,
|
||||
wrapper: Provider,
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
+102
-103
@@ -1,12 +1,12 @@
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
import { useCallback } from 'react';
|
||||
import { useStore } from 'jotai';
|
||||
import { Key } from 'ts-key-enum';
|
||||
|
||||
import { selectableItemIdsComponentState } from '@/ui/layout/selectable-list/states/selectableItemIdsComponentState';
|
||||
import { selectedItemIdComponentState } from '@/ui/layout/selectable-list/states/selectedItemIdComponentState';
|
||||
import { isSelectedItemIdComponentFamilySelector } from '@/ui/layout/selectable-list/states/selectors/isSelectedItemIdComponentFamilySelector';
|
||||
import { isSelectedItemIdComponentFamilyState } from '@/ui/layout/selectable-list/states/isSelectedItemIdComponentFamilyState';
|
||||
import { useHotkeysOnFocusedElement } from '@/ui/utilities/hotkey/hooks/useHotkeysOnFocusedElement';
|
||||
import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue';
|
||||
|
||||
type Direction = 'up' | 'down' | 'left' | 'right';
|
||||
|
||||
@@ -31,110 +31,109 @@ export const useSelectableListHotKeys = (
|
||||
}
|
||||
};
|
||||
|
||||
const handleSelect = useRecoilCallback(
|
||||
({ snapshot, set }) =>
|
||||
(direction: Direction) => {
|
||||
const selectedItemId = getSnapshotValue(
|
||||
snapshot,
|
||||
selectedItemIdComponentState.atomFamily({
|
||||
instanceId: instanceId,
|
||||
}),
|
||||
);
|
||||
const selectableItemIds = getSnapshotValue(
|
||||
snapshot,
|
||||
selectableItemIdsComponentState.atomFamily({
|
||||
instanceId: instanceId,
|
||||
}),
|
||||
);
|
||||
const store = useStore();
|
||||
|
||||
const currentPosition = findPosition(selectableItemIds, selectedItemId);
|
||||
const handleSelect = useCallback(
|
||||
(direction: Direction) => {
|
||||
const selectedItemId = store.get(
|
||||
selectedItemIdComponentState.atomFamily({
|
||||
instanceId: instanceId,
|
||||
}),
|
||||
);
|
||||
const selectableItemIds = store.get(
|
||||
selectableItemIdsComponentState.atomFamily({
|
||||
instanceId: instanceId,
|
||||
}),
|
||||
);
|
||||
|
||||
const computeNextId = (direction: Direction) => {
|
||||
if (
|
||||
selectableItemIds.length === 0 ||
|
||||
selectableItemIds[0]?.length === 0
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const currentPosition = findPosition(selectableItemIds, selectedItemId);
|
||||
|
||||
if (!selectedItemId || !currentPosition) {
|
||||
return selectableItemIds[0][0];
|
||||
}
|
||||
|
||||
const { row: currentRow, col: currentCol } = currentPosition;
|
||||
|
||||
const isSingleRow = selectableItemIds.length === 1;
|
||||
|
||||
let nextRow: number;
|
||||
let nextCol: number;
|
||||
|
||||
switch (direction) {
|
||||
case 'up':
|
||||
nextRow = isSingleRow ? currentRow : Math.max(0, currentRow - 1);
|
||||
nextCol = isSingleRow ? Math.max(0, currentCol - 1) : currentCol;
|
||||
break;
|
||||
case 'down':
|
||||
nextRow = isSingleRow
|
||||
? currentRow
|
||||
: Math.min(selectableItemIds.length - 1, currentRow + 1);
|
||||
nextCol = isSingleRow
|
||||
? Math.min(
|
||||
selectableItemIds[currentRow].length - 1,
|
||||
currentCol + 1,
|
||||
)
|
||||
: currentCol;
|
||||
break;
|
||||
case 'left':
|
||||
nextRow = currentRow;
|
||||
nextCol = Math.max(0, currentCol - 1);
|
||||
break;
|
||||
case 'right':
|
||||
nextRow = currentRow;
|
||||
nextCol = Math.min(
|
||||
selectableItemIds[currentRow].length - 1,
|
||||
currentCol + 1,
|
||||
);
|
||||
break;
|
||||
default:
|
||||
nextRow = currentRow;
|
||||
nextCol = currentCol;
|
||||
}
|
||||
|
||||
return selectableItemIds[nextRow][nextCol];
|
||||
};
|
||||
|
||||
const nextId = computeNextId(direction);
|
||||
|
||||
if (selectedItemId !== nextId) {
|
||||
if (isNonEmptyString(nextId)) {
|
||||
set(
|
||||
isSelectedItemIdComponentFamilySelector.selectorFamily({
|
||||
instanceId: instanceId,
|
||||
familyKey: nextId,
|
||||
}),
|
||||
true,
|
||||
);
|
||||
set(
|
||||
selectedItemIdComponentState.atomFamily({
|
||||
instanceId: instanceId,
|
||||
}),
|
||||
nextId,
|
||||
);
|
||||
onSelect?.(nextId);
|
||||
}
|
||||
|
||||
if (isNonEmptyString(selectedItemId)) {
|
||||
set(
|
||||
isSelectedItemIdComponentFamilySelector.selectorFamily({
|
||||
instanceId: instanceId,
|
||||
familyKey: selectedItemId,
|
||||
}),
|
||||
false,
|
||||
);
|
||||
}
|
||||
const computeNextId = (direction: Direction) => {
|
||||
if (
|
||||
selectableItemIds.length === 0 ||
|
||||
selectableItemIds[0]?.length === 0
|
||||
) {
|
||||
return;
|
||||
}
|
||||
},
|
||||
[instanceId, onSelect],
|
||||
|
||||
if (!selectedItemId || !currentPosition) {
|
||||
return selectableItemIds[0][0];
|
||||
}
|
||||
|
||||
const { row: currentRow, col: currentCol } = currentPosition;
|
||||
|
||||
const isSingleRow = selectableItemIds.length === 1;
|
||||
|
||||
let nextRow: number;
|
||||
let nextCol: number;
|
||||
|
||||
switch (direction) {
|
||||
case 'up':
|
||||
nextRow = isSingleRow ? currentRow : Math.max(0, currentRow - 1);
|
||||
nextCol = isSingleRow ? Math.max(0, currentCol - 1) : currentCol;
|
||||
break;
|
||||
case 'down':
|
||||
nextRow = isSingleRow
|
||||
? currentRow
|
||||
: Math.min(selectableItemIds.length - 1, currentRow + 1);
|
||||
nextCol = isSingleRow
|
||||
? Math.min(
|
||||
selectableItemIds[currentRow].length - 1,
|
||||
currentCol + 1,
|
||||
)
|
||||
: currentCol;
|
||||
break;
|
||||
case 'left':
|
||||
nextRow = currentRow;
|
||||
nextCol = Math.max(0, currentCol - 1);
|
||||
break;
|
||||
case 'right':
|
||||
nextRow = currentRow;
|
||||
nextCol = Math.min(
|
||||
selectableItemIds[currentRow].length - 1,
|
||||
currentCol + 1,
|
||||
);
|
||||
break;
|
||||
default:
|
||||
nextRow = currentRow;
|
||||
nextCol = currentCol;
|
||||
}
|
||||
|
||||
return selectableItemIds[nextRow][nextCol];
|
||||
};
|
||||
|
||||
const nextId = computeNextId(direction);
|
||||
|
||||
if (selectedItemId !== nextId) {
|
||||
if (isNonEmptyString(nextId)) {
|
||||
store.set(
|
||||
isSelectedItemIdComponentFamilyState.atomFamily({
|
||||
instanceId: instanceId,
|
||||
familyKey: nextId,
|
||||
}),
|
||||
true,
|
||||
);
|
||||
store.set(
|
||||
selectedItemIdComponentState.atomFamily({
|
||||
instanceId: instanceId,
|
||||
}),
|
||||
nextId,
|
||||
);
|
||||
onSelect?.(nextId);
|
||||
}
|
||||
|
||||
if (isNonEmptyString(selectedItemId)) {
|
||||
store.set(
|
||||
isSelectedItemIdComponentFamilyState.atomFamily({
|
||||
instanceId: instanceId,
|
||||
familyKey: selectedItemId,
|
||||
}),
|
||||
false,
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
[store, instanceId, onSelect],
|
||||
);
|
||||
|
||||
useHotkeysOnFocusedElement({
|
||||
|
||||
+56
-60
@@ -1,10 +1,10 @@
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
import { useCallback } from 'react';
|
||||
import { useStore } from 'jotai';
|
||||
|
||||
import { SelectableListComponentInstanceContext } from '@/ui/layout/selectable-list/states/contexts/SelectableListComponentInstanceContext';
|
||||
import { selectedItemIdComponentState } from '@/ui/layout/selectable-list/states/selectedItemIdComponentState';
|
||||
import { isSelectedItemIdComponentFamilySelector } from '@/ui/layout/selectable-list/states/selectors/isSelectedItemIdComponentFamilySelector';
|
||||
import { isSelectedItemIdComponentFamilyState } from '@/ui/layout/selectable-list/states/isSelectedItemIdComponentFamilyState';
|
||||
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
||||
import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const useSelectableList = (instanceId?: string) => {
|
||||
@@ -12,70 +12,66 @@ export const useSelectableList = (instanceId?: string) => {
|
||||
SelectableListComponentInstanceContext,
|
||||
instanceId,
|
||||
);
|
||||
const resetSelectedItem = useRecoilCallback(
|
||||
({ snapshot, set }) =>
|
||||
() => {
|
||||
const selectedItemId = getSnapshotValue(
|
||||
snapshot,
|
||||
selectedItemIdComponentState.atomFamily({
|
||||
instanceId: selectableListInstanceId,
|
||||
}),
|
||||
);
|
||||
|
||||
if (isDefined(selectedItemId)) {
|
||||
set(
|
||||
selectedItemIdComponentState.atomFamily({
|
||||
instanceId: selectableListInstanceId,
|
||||
}),
|
||||
null,
|
||||
);
|
||||
set(
|
||||
isSelectedItemIdComponentFamilySelector.selectorFamily({
|
||||
instanceId: selectableListInstanceId,
|
||||
familyKey: selectedItemId,
|
||||
}),
|
||||
false,
|
||||
);
|
||||
}
|
||||
},
|
||||
[selectableListInstanceId],
|
||||
);
|
||||
const store = useStore();
|
||||
|
||||
const setSelectedItemId = useRecoilCallback(
|
||||
({ set, snapshot }) =>
|
||||
(itemId: string) => {
|
||||
const selectedItemId = getSnapshotValue(
|
||||
snapshot,
|
||||
selectedItemIdComponentState.atomFamily({
|
||||
instanceId: selectableListInstanceId,
|
||||
}),
|
||||
);
|
||||
const resetSelectedItem = useCallback(() => {
|
||||
const selectedItemId = store.get(
|
||||
selectedItemIdComponentState.atomFamily({
|
||||
instanceId: selectableListInstanceId,
|
||||
}),
|
||||
);
|
||||
|
||||
if (isDefined(selectedItemId)) {
|
||||
set(
|
||||
isSelectedItemIdComponentFamilySelector.selectorFamily({
|
||||
instanceId: selectableListInstanceId,
|
||||
familyKey: selectedItemId,
|
||||
}),
|
||||
false,
|
||||
);
|
||||
}
|
||||
if (isDefined(selectedItemId)) {
|
||||
store.set(
|
||||
selectedItemIdComponentState.atomFamily({
|
||||
instanceId: selectableListInstanceId,
|
||||
}),
|
||||
null,
|
||||
);
|
||||
store.set(
|
||||
isSelectedItemIdComponentFamilyState.atomFamily({
|
||||
instanceId: selectableListInstanceId,
|
||||
familyKey: selectedItemId,
|
||||
}),
|
||||
false,
|
||||
);
|
||||
}
|
||||
}, [store, selectableListInstanceId]);
|
||||
|
||||
set(
|
||||
selectedItemIdComponentState.atomFamily({
|
||||
const setSelectedItemId = useCallback(
|
||||
(itemId: string) => {
|
||||
const selectedItemId = store.get(
|
||||
selectedItemIdComponentState.atomFamily({
|
||||
instanceId: selectableListInstanceId,
|
||||
}),
|
||||
);
|
||||
|
||||
if (isDefined(selectedItemId)) {
|
||||
store.set(
|
||||
isSelectedItemIdComponentFamilyState.atomFamily({
|
||||
instanceId: selectableListInstanceId,
|
||||
familyKey: selectedItemId,
|
||||
}),
|
||||
itemId,
|
||||
false,
|
||||
);
|
||||
set(
|
||||
isSelectedItemIdComponentFamilySelector.selectorFamily({
|
||||
instanceId: selectableListInstanceId,
|
||||
familyKey: itemId,
|
||||
}),
|
||||
true,
|
||||
);
|
||||
},
|
||||
[selectableListInstanceId],
|
||||
}
|
||||
|
||||
store.set(
|
||||
selectedItemIdComponentState.atomFamily({
|
||||
instanceId: selectableListInstanceId,
|
||||
}),
|
||||
itemId,
|
||||
);
|
||||
store.set(
|
||||
isSelectedItemIdComponentFamilyState.atomFamily({
|
||||
instanceId: selectableListInstanceId,
|
||||
familyKey: itemId,
|
||||
}),
|
||||
true,
|
||||
);
|
||||
},
|
||||
[store, selectableListInstanceId],
|
||||
);
|
||||
|
||||
return {
|
||||
|
||||
+17
-18
@@ -2,9 +2,9 @@ import { SelectableListComponentInstanceContext } from '@/ui/layout/selectable-l
|
||||
import { selectedItemIdComponentState } from '@/ui/layout/selectable-list/states/selectedItemIdComponentState';
|
||||
import { useHotkeysOnFocusedElement } from '@/ui/utilities/hotkey/hooks/useHotkeysOnFocusedElement';
|
||||
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
||||
import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
import { useCallback } from 'react';
|
||||
import { useStore } from 'jotai';
|
||||
import { Key } from 'ts-key-enum';
|
||||
|
||||
export const useSelectableListListenToEnterHotkeyOnItem = ({
|
||||
@@ -20,24 +20,23 @@ export const useSelectableListListenToEnterHotkeyOnItem = ({
|
||||
SelectableListComponentInstanceContext,
|
||||
);
|
||||
|
||||
const store = useStore();
|
||||
|
||||
const handleEnterKey = useCallback(() => {
|
||||
const selectedItemId = store.get(
|
||||
selectedItemIdComponentState.atomFamily({
|
||||
instanceId,
|
||||
}),
|
||||
);
|
||||
|
||||
if (isNonEmptyString(selectedItemId) && selectedItemId === itemId) {
|
||||
onEnter?.();
|
||||
}
|
||||
}, [store, instanceId, itemId, onEnter]);
|
||||
|
||||
useHotkeysOnFocusedElement({
|
||||
keys: Key.Enter,
|
||||
callback: useRecoilCallback(
|
||||
({ snapshot }) =>
|
||||
() => {
|
||||
const selectedItemId = getSnapshotValue(
|
||||
snapshot,
|
||||
selectedItemIdComponentState.atomFamily({
|
||||
instanceId,
|
||||
}),
|
||||
);
|
||||
|
||||
if (isNonEmptyString(selectedItemId) && selectedItemId === itemId) {
|
||||
onEnter?.();
|
||||
}
|
||||
},
|
||||
[instanceId, itemId, onEnter],
|
||||
),
|
||||
callback: handleEnterKey,
|
||||
focusId,
|
||||
dependencies: [itemId, onEnter],
|
||||
});
|
||||
|
||||
+7
-9
@@ -1,11 +1,9 @@
|
||||
import { SelectableListComponentInstanceContext } from '@/ui/layout/selectable-list/states/contexts/SelectableListComponentInstanceContext';
|
||||
import { createComponentFamilyState } from '@/ui/utilities/state/component-state/utils/createComponentFamilyState';
|
||||
import { createComponentFamilyStateV2 } from '@/ui/utilities/state/jotai/utils/createComponentFamilyStateV2';
|
||||
|
||||
export const isSelectedItemIdComponentFamilyState = createComponentFamilyState<
|
||||
boolean,
|
||||
string
|
||||
>({
|
||||
key: 'isSelectedItemIdComponentFamilyState',
|
||||
defaultValue: false,
|
||||
componentInstanceContext: SelectableListComponentInstanceContext,
|
||||
});
|
||||
export const isSelectedItemIdComponentFamilyState =
|
||||
createComponentFamilyStateV2<boolean, string>({
|
||||
key: 'isSelectedItemIdComponentFamilyState',
|
||||
defaultValue: false,
|
||||
componentInstanceContext: SelectableListComponentInstanceContext,
|
||||
});
|
||||
|
||||
+8
-8
@@ -1,10 +1,10 @@
|
||||
import { SelectableListComponentInstanceContext } from '@/ui/layout/selectable-list/states/contexts/SelectableListComponentInstanceContext';
|
||||
import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState';
|
||||
import { createComponentStateV2 } from '@/ui/utilities/state/jotai/utils/createComponentStateV2';
|
||||
|
||||
export const selectableItemIdsComponentState = createComponentState<string[][]>(
|
||||
{
|
||||
key: 'selectableItemIdsComponentState',
|
||||
defaultValue: [[]],
|
||||
componentInstanceContext: SelectableListComponentInstanceContext,
|
||||
},
|
||||
);
|
||||
export const selectableItemIdsComponentState = createComponentStateV2<
|
||||
string[][]
|
||||
>({
|
||||
key: 'selectableItemIdsComponentState',
|
||||
defaultValue: [[]],
|
||||
componentInstanceContext: SelectableListComponentInstanceContext,
|
||||
});
|
||||
|
||||
+8
-8
@@ -1,10 +1,10 @@
|
||||
import { SelectableListComponentInstanceContext } from '@/ui/layout/selectable-list/states/contexts/SelectableListComponentInstanceContext';
|
||||
import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState';
|
||||
import { createComponentStateV2 } from '@/ui/utilities/state/jotai/utils/createComponentStateV2';
|
||||
|
||||
export const selectedItemIdComponentState = createComponentState<string | null>(
|
||||
{
|
||||
key: 'selectedItemIdComponentState',
|
||||
defaultValue: null,
|
||||
componentInstanceContext: SelectableListComponentInstanceContext,
|
||||
},
|
||||
);
|
||||
export const selectedItemIdComponentState = createComponentStateV2<
|
||||
string | null
|
||||
>({
|
||||
key: 'selectedItemIdComponentState',
|
||||
defaultValue: null,
|
||||
componentInstanceContext: SelectableListComponentInstanceContext,
|
||||
});
|
||||
|
||||
-28
@@ -1,28 +0,0 @@
|
||||
import { SelectableListComponentInstanceContext } from '@/ui/layout/selectable-list/states/contexts/SelectableListComponentInstanceContext';
|
||||
import { isSelectedItemIdComponentFamilyState } from '@/ui/layout/selectable-list/states/isSelectedItemIdComponentFamilyState';
|
||||
import { createComponentFamilySelector } from '@/ui/utilities/state/component-state/utils/createComponentFamilySelector';
|
||||
|
||||
export const isSelectedItemIdComponentFamilySelector =
|
||||
createComponentFamilySelector<boolean, string>({
|
||||
key: 'isSelectedItemIdComponentFamilySelector',
|
||||
componentInstanceContext: SelectableListComponentInstanceContext,
|
||||
get:
|
||||
({ instanceId, familyKey }: { instanceId: string; familyKey: string }) =>
|
||||
({ get }) =>
|
||||
get(
|
||||
isSelectedItemIdComponentFamilyState.atomFamily({
|
||||
instanceId: instanceId,
|
||||
familyKey: familyKey,
|
||||
}),
|
||||
),
|
||||
set:
|
||||
({ instanceId, familyKey }: { instanceId: string; familyKey: string }) =>
|
||||
({ set }, newValue) =>
|
||||
set(
|
||||
isSelectedItemIdComponentFamilyState.atomFamily({
|
||||
instanceId: instanceId,
|
||||
familyKey: familyKey,
|
||||
}),
|
||||
newValue,
|
||||
),
|
||||
});
|
||||
@@ -6,7 +6,7 @@ import { type TabListProps } from '@/ui/layout/tab-list/types/TabListProps';
|
||||
import { NodeDimension } from '@/ui/utilities/dimensions/components/NodeDimension';
|
||||
import { TabListHiddenMeasurements } from '@/ui/layout/tab-list/components/TabListHiddenMeasurements';
|
||||
import { useTabListMeasurements } from '@/ui/layout/tab-list/hooks/useTabListMeasurements';
|
||||
import { useRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentState';
|
||||
import { useRecoilComponentStateV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilComponentStateV2';
|
||||
import styled from '@emotion/styled';
|
||||
import { useCallback, useEffect, useMemo } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
@@ -53,7 +53,7 @@ export const TabList = ({
|
||||
const visibleTabs = tabs.filter((tab) => !tab.hide);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [activeTabId, setActiveTabId] = useRecoilComponentState(
|
||||
const [activeTabId, setActiveTabId] = useRecoilComponentStateV2(
|
||||
activeTabIdComponentState,
|
||||
componentInstanceId,
|
||||
);
|
||||
|
||||
+6
-4
@@ -1,6 +1,6 @@
|
||||
import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
|
||||
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 { useEffect } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
@@ -14,8 +14,10 @@ export const TabListFromUrlOptionalEffect = ({
|
||||
isInRightDrawer,
|
||||
}: TabListFromUrlOptionalEffectProps) => {
|
||||
const location = useLocation();
|
||||
const activeTabId = useRecoilComponentValue(activeTabIdComponentState);
|
||||
const setActiveTabId = useSetRecoilComponentState(activeTabIdComponentState);
|
||||
const activeTabId = useRecoilComponentValueV2(activeTabIdComponentState);
|
||||
const setActiveTabId = useSetRecoilComponentStateV2(
|
||||
activeTabIdComponentState,
|
||||
);
|
||||
|
||||
const hash = location.hash.replace('#', '');
|
||||
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
import { TabListComponentInstanceContext } from '@/ui/layout/tab-list/states/contexts/TabListComponentInstanceContext';
|
||||
import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState';
|
||||
import { createComponentStateV2 } from '@/ui/utilities/state/jotai/utils/createComponentStateV2';
|
||||
|
||||
export const activeTabIdComponentState = createComponentState<string | null>({
|
||||
export const activeTabIdComponentState = createComponentStateV2<string | null>({
|
||||
key: 'activeTabIdComponentState',
|
||||
defaultValue: null,
|
||||
componentInstanceContext: TabListComponentInstanceContext,
|
||||
|
||||
+9
-3
@@ -2,7 +2,8 @@ import { TableHeader } from '@/ui/layout/table/components/TableHeader';
|
||||
import { TableHeaderText } from '@/ui/layout/table/components/TableHeaderText';
|
||||
import { sortedFieldByTableFamilyState } from '@/ui/layout/table/states/sortedFieldByTableFamilyState';
|
||||
import { type TableSortValue } from '@/ui/layout/table/types/TableSortValue';
|
||||
import { useRecoilState } from 'recoil';
|
||||
import { useFamilyRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useFamilyRecoilValueV2';
|
||||
import { useSetFamilyRecoilStateV2 } from '@/ui/utilities/state/jotai/hooks/useSetFamilyRecoilStateV2';
|
||||
import {
|
||||
IconArrowDown,
|
||||
IconArrowUp,
|
||||
@@ -24,8 +25,13 @@ export const SortableTableHeader = ({
|
||||
initialSort?: TableSortValue;
|
||||
Icon?: IconComponent;
|
||||
}) => {
|
||||
const [sortedFieldByTable, setSortedFieldByTable] = useRecoilState(
|
||||
sortedFieldByTableFamilyState({ tableId }),
|
||||
const sortedFieldByTable = useFamilyRecoilValueV2(
|
||||
sortedFieldByTableFamilyState,
|
||||
{ tableId },
|
||||
);
|
||||
const setSortedFieldByTable = useSetFamilyRecoilStateV2(
|
||||
sortedFieldByTableFamilyState,
|
||||
{ tableId },
|
||||
);
|
||||
|
||||
const sortValue = sortedFieldByTable ?? initialSort;
|
||||
|
||||
+24
-51
@@ -1,6 +1,6 @@
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import React, { type ReactNode } from 'react';
|
||||
import { type MutableSnapshot, RecoilRoot } from 'recoil';
|
||||
import { createStore, Provider } from 'jotai';
|
||||
|
||||
import {
|
||||
mockedTableMetadata,
|
||||
@@ -17,41 +17,32 @@ import { sortedFieldByTableFamilyState } from '@/ui/layout/table/states/sortedFi
|
||||
|
||||
import { useSortedArray } from '@/ui/layout/table/hooks/useSortedArray';
|
||||
|
||||
interface WrapperProps {
|
||||
children: ReactNode;
|
||||
initializeState?: (mutableSnapshot: MutableSnapshot) => void;
|
||||
}
|
||||
|
||||
const Wrapper: React.FC<WrapperProps> = ({ children, initializeState }) => (
|
||||
<RecoilRoot initializeState={initializeState}>{children}</RecoilRoot>
|
||||
);
|
||||
const createSortedWrapper = (
|
||||
fieldName: keyof MockedTableType,
|
||||
orderBy: OrderBy,
|
||||
) => {
|
||||
const store = createStore();
|
||||
store.set(
|
||||
sortedFieldByTableFamilyState.atomFamily({
|
||||
tableId: mockedTableMetadata.tableId,
|
||||
}),
|
||||
{
|
||||
fieldName: fieldName as string,
|
||||
orderBy,
|
||||
},
|
||||
);
|
||||
const Wrapper = ({ children }: { children: ReactNode }) => (
|
||||
<Provider store={store}>{children}</Provider>
|
||||
);
|
||||
return Wrapper;
|
||||
};
|
||||
|
||||
describe('useSortedArray hook', () => {
|
||||
const initializeState =
|
||||
(fieldName: keyof MockedTableType, orderBy: OrderBy) =>
|
||||
({ set }: MutableSnapshot) => {
|
||||
set(
|
||||
sortedFieldByTableFamilyState({
|
||||
tableId: mockedTableMetadata.tableId,
|
||||
}),
|
||||
{
|
||||
fieldName,
|
||||
orderBy,
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
test('initial sorting behavior for string fields - Ascending', () => {
|
||||
const { result } = renderHook(
|
||||
() => useSortedArray(tableData, mockedTableMetadata),
|
||||
{
|
||||
wrapper: ({ children }: { children: ReactNode }) => (
|
||||
<Wrapper
|
||||
initializeState={initializeState('labelPlural', 'AscNullsLast')}
|
||||
>
|
||||
{children}
|
||||
</Wrapper>
|
||||
),
|
||||
wrapper: createSortedWrapper('labelPlural', 'AscNullsLast'),
|
||||
},
|
||||
);
|
||||
|
||||
@@ -64,13 +55,7 @@ describe('useSortedArray hook', () => {
|
||||
const { result } = renderHook(
|
||||
() => useSortedArray(tableData, mockedTableMetadata),
|
||||
{
|
||||
wrapper: ({ children }: { children: ReactNode }) => (
|
||||
<Wrapper
|
||||
initializeState={initializeState('labelPlural', 'DescNullsLast')}
|
||||
>
|
||||
{children}
|
||||
</Wrapper>
|
||||
),
|
||||
wrapper: createSortedWrapper('labelPlural', 'DescNullsLast'),
|
||||
},
|
||||
);
|
||||
|
||||
@@ -83,13 +68,7 @@ describe('useSortedArray hook', () => {
|
||||
const { result } = renderHook(
|
||||
() => useSortedArray(tableData, mockedTableMetadata),
|
||||
{
|
||||
wrapper: ({ children }: { children: ReactNode }) => (
|
||||
<Wrapper
|
||||
initializeState={initializeState('fieldsCount', 'AscNullsLast')}
|
||||
>
|
||||
{children}
|
||||
</Wrapper>
|
||||
),
|
||||
wrapper: createSortedWrapper('fieldsCount', 'AscNullsLast'),
|
||||
},
|
||||
);
|
||||
|
||||
@@ -102,13 +81,7 @@ describe('useSortedArray hook', () => {
|
||||
const { result } = renderHook(
|
||||
() => useSortedArray(tableData, mockedTableMetadata),
|
||||
{
|
||||
wrapper: ({ children }: { children: ReactNode }) => (
|
||||
<Wrapper
|
||||
initializeState={initializeState('fieldsCount', 'DescNullsLast')}
|
||||
>
|
||||
{children}
|
||||
</Wrapper>
|
||||
),
|
||||
wrapper: createSortedWrapper('fieldsCount', 'DescNullsLast'),
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import { sortedFieldByTableFamilyState } from '@/ui/layout/table/states/sortedFieldByTableFamilyState';
|
||||
import { type TableMetadata } from '@/ui/layout/table/types/TableMetadata';
|
||||
import { useFamilyRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useFamilyRecoilValueV2';
|
||||
import { useMemo } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const useSortedArray = <T>(
|
||||
arrayToSort: T[],
|
||||
tableMetadata: TableMetadata<T>,
|
||||
): T[] => {
|
||||
const sortedFieldByTable = useRecoilValue(
|
||||
sortedFieldByTableFamilyState({ tableId: tableMetadata.tableId }),
|
||||
const sortedFieldByTable = useFamilyRecoilValueV2(
|
||||
sortedFieldByTableFamilyState,
|
||||
{ tableId: tableMetadata.tableId },
|
||||
);
|
||||
|
||||
const initialSort = tableMetadata.initialSort;
|
||||
|
||||
+2
-2
@@ -1,11 +1,11 @@
|
||||
import { type TableSortValue } from '@/ui/layout/table/types/TableSortValue';
|
||||
import { createFamilyState } from '@/ui/utilities/state/utils/createFamilyState';
|
||||
import { createFamilyStateV2 } from '@/ui/utilities/state/jotai/utils/createFamilyStateV2';
|
||||
|
||||
export type SortedFieldByTableFamilyStateKey = {
|
||||
tableId: string;
|
||||
};
|
||||
|
||||
export const sortedFieldByTableFamilyState = createFamilyState<
|
||||
export const sortedFieldByTableFamilyState = createFamilyStateV2<
|
||||
TableSortValue | null,
|
||||
SortedFieldByTableFamilyStateKey
|
||||
>({
|
||||
|
||||
@@ -2,9 +2,11 @@ import { useCallback } from 'react';
|
||||
|
||||
import { isDragSelectionStartEnabledStateV2 } from '@/ui/utilities/drag-select/states/internal/isDragSelectionStartEnabledStateV2';
|
||||
import { useSetRecoilStateV2 } from '@/ui/utilities/state/jotai/hooks/useSetRecoilStateV2';
|
||||
import { jotaiStore } from '@/ui/utilities/state/jotai/jotaiStore';
|
||||
import { useStore } from 'jotai';
|
||||
|
||||
export const useDragSelect = () => {
|
||||
const store = useStore();
|
||||
|
||||
const setIsDragSelectionStartEnabled = useSetRecoilStateV2(
|
||||
isDragSelectionStartEnabledStateV2,
|
||||
);
|
||||
@@ -14,8 +16,8 @@ export const useDragSelect = () => {
|
||||
};
|
||||
|
||||
const isDragSelectionStartEnabled = useCallback(
|
||||
() => jotaiStore.get(isDragSelectionStartEnabledStateV2.atom),
|
||||
[],
|
||||
() => store.get(isDragSelectionStartEnabledStateV2.atom),
|
||||
[store],
|
||||
);
|
||||
|
||||
return {
|
||||
|
||||
+15
-4
@@ -2,16 +2,27 @@ import { usePushFocusItemToFocusStack } from '@/ui/utilities/focus/hooks/usePush
|
||||
import { currentFocusIdSelector } from '@/ui/utilities/focus/states/currentFocusIdSelector';
|
||||
import { focusStackState } from '@/ui/utilities/focus/states/focusStackState';
|
||||
import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType';
|
||||
import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { createStore, Provider as JotaiProvider } from 'jotai';
|
||||
import { act } from 'react';
|
||||
import { RecoilRoot, useRecoilValue } from 'recoil';
|
||||
|
||||
const createTestWrapper = () => {
|
||||
const store = createStore();
|
||||
|
||||
const Wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||
<JotaiProvider store={store}>{children}</JotaiProvider>
|
||||
);
|
||||
|
||||
return Wrapper;
|
||||
};
|
||||
|
||||
const renderHooks = () => {
|
||||
const { result } = renderHook(
|
||||
() => {
|
||||
const { pushFocusItemToFocusStack } = usePushFocusItemToFocusStack();
|
||||
const focusStack = useRecoilValue(focusStackState);
|
||||
const currentFocusId = useRecoilValue(currentFocusIdSelector);
|
||||
const focusStack = useRecoilValueV2(focusStackState);
|
||||
const currentFocusId = useRecoilValueV2(currentFocusIdSelector);
|
||||
|
||||
return {
|
||||
pushFocusItemToFocusStack,
|
||||
@@ -20,7 +31,7 @@ const renderHooks = () => {
|
||||
};
|
||||
},
|
||||
{
|
||||
wrapper: RecoilRoot,
|
||||
wrapper: createTestWrapper(),
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
+17
-4
@@ -3,9 +3,18 @@ import { useRemoveFocusItemFromFocusStackById } from '@/ui/utilities/focus/hooks
|
||||
import { currentFocusIdSelector } from '@/ui/utilities/focus/states/currentFocusIdSelector';
|
||||
import { focusStackState } from '@/ui/utilities/focus/states/focusStackState';
|
||||
import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType';
|
||||
import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2';
|
||||
import {
|
||||
jotaiStore,
|
||||
resetJotaiStore,
|
||||
} from '@/ui/utilities/state/jotai/jotaiStore';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { Provider as JotaiProvider } from 'jotai';
|
||||
import { act } from 'react';
|
||||
import { RecoilRoot, useRecoilValue } from 'recoil';
|
||||
|
||||
const Wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||
<JotaiProvider store={jotaiStore}>{children}</JotaiProvider>
|
||||
);
|
||||
|
||||
const renderHooks = () => {
|
||||
const { result } = renderHook(
|
||||
@@ -13,8 +22,8 @@ const renderHooks = () => {
|
||||
const { pushFocusItemToFocusStack } = usePushFocusItemToFocusStack();
|
||||
const { removeFocusItemFromFocusStackById } =
|
||||
useRemoveFocusItemFromFocusStackById();
|
||||
const focusStack = useRecoilValue(focusStackState);
|
||||
const currentFocusId = useRecoilValue(currentFocusIdSelector);
|
||||
const focusStack = useRecoilValueV2(focusStackState);
|
||||
const currentFocusId = useRecoilValueV2(currentFocusIdSelector);
|
||||
|
||||
return {
|
||||
pushFocusItemToFocusStack,
|
||||
@@ -24,7 +33,7 @@ const renderHooks = () => {
|
||||
};
|
||||
},
|
||||
{
|
||||
wrapper: RecoilRoot,
|
||||
wrapper: Wrapper,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -56,6 +65,10 @@ const secondFocusItem = {
|
||||
};
|
||||
|
||||
describe('useRemoveFocusItemFromFocusStackById', () => {
|
||||
beforeEach(() => {
|
||||
resetJotaiStore();
|
||||
});
|
||||
|
||||
it('should remove focus item from the stack', async () => {
|
||||
const { result } = renderHooks();
|
||||
|
||||
|
||||
+17
-4
@@ -3,17 +3,26 @@ import { useResetFocusStack } from '@/ui/utilities/focus/hooks/useResetFocusStac
|
||||
import { currentFocusIdSelector } from '@/ui/utilities/focus/states/currentFocusIdSelector';
|
||||
import { focusStackState } from '@/ui/utilities/focus/states/focusStackState';
|
||||
import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType';
|
||||
import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2';
|
||||
import {
|
||||
jotaiStore,
|
||||
resetJotaiStore,
|
||||
} from '@/ui/utilities/state/jotai/jotaiStore';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { Provider as JotaiProvider } from 'jotai';
|
||||
import { act } from 'react';
|
||||
import { RecoilRoot, useRecoilValue } from 'recoil';
|
||||
|
||||
const Wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||
<JotaiProvider store={jotaiStore}>{children}</JotaiProvider>
|
||||
);
|
||||
|
||||
const renderHooks = () => {
|
||||
const { result } = renderHook(
|
||||
() => {
|
||||
const { pushFocusItemToFocusStack } = usePushFocusItemToFocusStack();
|
||||
const { resetFocusStack } = useResetFocusStack();
|
||||
const focusStack = useRecoilValue(focusStackState);
|
||||
const currentFocusId = useRecoilValue(currentFocusIdSelector);
|
||||
const focusStack = useRecoilValueV2(focusStackState);
|
||||
const currentFocusId = useRecoilValueV2(currentFocusIdSelector);
|
||||
|
||||
return {
|
||||
pushFocusItemToFocusStack,
|
||||
@@ -23,7 +32,7 @@ const renderHooks = () => {
|
||||
};
|
||||
},
|
||||
{
|
||||
wrapper: RecoilRoot,
|
||||
wrapper: Wrapper,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -31,6 +40,10 @@ const renderHooks = () => {
|
||||
};
|
||||
|
||||
describe('useResetFocusStack', () => {
|
||||
beforeEach(() => {
|
||||
resetJotaiStore();
|
||||
});
|
||||
|
||||
it('should reset the focus stack', async () => {
|
||||
const { result } = renderHooks();
|
||||
|
||||
|
||||
+17
-4
@@ -3,17 +3,26 @@ import { useResetFocusStackToFocusItem } from '@/ui/utilities/focus/hooks/useRes
|
||||
import { currentFocusIdSelector } from '@/ui/utilities/focus/states/currentFocusIdSelector';
|
||||
import { focusStackState } from '@/ui/utilities/focus/states/focusStackState';
|
||||
import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType';
|
||||
import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2';
|
||||
import {
|
||||
jotaiStore,
|
||||
resetJotaiStore,
|
||||
} from '@/ui/utilities/state/jotai/jotaiStore';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { Provider as JotaiProvider } from 'jotai';
|
||||
import { act } from 'react';
|
||||
import { RecoilRoot, useRecoilValue } from 'recoil';
|
||||
|
||||
const Wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||
<JotaiProvider store={jotaiStore}>{children}</JotaiProvider>
|
||||
);
|
||||
|
||||
const renderHooks = () => {
|
||||
const { result } = renderHook(
|
||||
() => {
|
||||
const { pushFocusItemToFocusStack } = usePushFocusItemToFocusStack();
|
||||
const { resetFocusStackToFocusItem } = useResetFocusStackToFocusItem();
|
||||
const focusStack = useRecoilValue(focusStackState);
|
||||
const currentFocusId = useRecoilValue(currentFocusIdSelector);
|
||||
const focusStack = useRecoilValueV2(focusStackState);
|
||||
const currentFocusId = useRecoilValueV2(currentFocusIdSelector);
|
||||
|
||||
return {
|
||||
pushFocusItemToFocusStack,
|
||||
@@ -23,7 +32,7 @@ const renderHooks = () => {
|
||||
};
|
||||
},
|
||||
{
|
||||
wrapper: RecoilRoot,
|
||||
wrapper: Wrapper,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -31,6 +40,10 @@ const renderHooks = () => {
|
||||
};
|
||||
|
||||
describe('useResetFocusStackToFocusItem', () => {
|
||||
beforeEach(() => {
|
||||
resetJotaiStore();
|
||||
});
|
||||
|
||||
it('should reset the focus stack to a specific focus item', async () => {
|
||||
const { result } = renderHooks();
|
||||
|
||||
|
||||
+45
-44
@@ -1,9 +1,11 @@
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { DEBUG_FOCUS_STACK } from '@/ui/utilities/focus/constants/DebugFocusStack';
|
||||
import { focusStackState } from '@/ui/utilities/focus/states/focusStackState';
|
||||
import { type FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType';
|
||||
import { type FocusStackItem } from '@/ui/utilities/focus/types/FocusStackItem';
|
||||
import { type GlobalHotkeysConfig } from '@/ui/utilities/hotkey/types/GlobalHotkeysConfig';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
import { useStore } from 'jotai';
|
||||
import { logDebug } from '~/utils/logDebug';
|
||||
|
||||
const addOrMoveItemToTheTopOfTheStack = ({
|
||||
@@ -21,54 +23,53 @@ const addOrMoveItemToTheTopOfTheStack = ({
|
||||
];
|
||||
|
||||
export const usePushFocusItemToFocusStack = () => {
|
||||
const pushFocusItemToFocusStack = useRecoilCallback(
|
||||
({ snapshot, set }) =>
|
||||
({
|
||||
const store = useStore();
|
||||
|
||||
const pushFocusItemToFocusStack = useCallback(
|
||||
({
|
||||
focusId,
|
||||
component,
|
||||
globalHotkeysConfig,
|
||||
}: {
|
||||
focusId: string;
|
||||
component: {
|
||||
type: FocusComponentType;
|
||||
instanceId: string;
|
||||
};
|
||||
globalHotkeysConfig?: Partial<GlobalHotkeysConfig>;
|
||||
}) => {
|
||||
const focusStackItem: FocusStackItem = {
|
||||
focusId,
|
||||
component,
|
||||
globalHotkeysConfig,
|
||||
}: {
|
||||
focusId: string;
|
||||
component: {
|
||||
type: FocusComponentType;
|
||||
instanceId: string;
|
||||
};
|
||||
globalHotkeysConfig?: Partial<GlobalHotkeysConfig>;
|
||||
}) => {
|
||||
const focusStackItem: FocusStackItem = {
|
||||
focusId,
|
||||
componentInstance: {
|
||||
componentType: component.type,
|
||||
componentInstanceId: component.instanceId,
|
||||
},
|
||||
globalHotkeysConfig: {
|
||||
enableGlobalHotkeysWithModifiers:
|
||||
globalHotkeysConfig?.enableGlobalHotkeysWithModifiers ?? true,
|
||||
enableGlobalHotkeysConflictingWithKeyboard:
|
||||
globalHotkeysConfig?.enableGlobalHotkeysConflictingWithKeyboard ??
|
||||
true,
|
||||
},
|
||||
};
|
||||
componentInstance: {
|
||||
componentType: component.type,
|
||||
componentInstanceId: component.instanceId,
|
||||
},
|
||||
globalHotkeysConfig: {
|
||||
enableGlobalHotkeysWithModifiers:
|
||||
globalHotkeysConfig?.enableGlobalHotkeysWithModifiers ?? true,
|
||||
enableGlobalHotkeysConflictingWithKeyboard:
|
||||
globalHotkeysConfig?.enableGlobalHotkeysConflictingWithKeyboard ??
|
||||
true,
|
||||
},
|
||||
};
|
||||
|
||||
const currentFocusStack = snapshot
|
||||
.getLoadable(focusStackState)
|
||||
.getValue();
|
||||
const currentFocusStack = store.get(focusStackState.atom);
|
||||
|
||||
const newFocusStack = addOrMoveItemToTheTopOfTheStack({
|
||||
const newFocusStack = addOrMoveItemToTheTopOfTheStack({
|
||||
focusStackItem,
|
||||
currentFocusStack,
|
||||
});
|
||||
|
||||
store.set(focusStackState.atom, newFocusStack);
|
||||
|
||||
if (DEBUG_FOCUS_STACK) {
|
||||
logDebug(`DEBUG: pushFocusItemToFocusStack ${focusId}`, {
|
||||
focusStackItem,
|
||||
currentFocusStack,
|
||||
newFocusStack,
|
||||
});
|
||||
|
||||
set(focusStackState, newFocusStack);
|
||||
|
||||
if (DEBUG_FOCUS_STACK) {
|
||||
logDebug(`DEBUG: pushFocusItemToFocusStack ${focusId}`, {
|
||||
focusStackItem,
|
||||
newFocusStack,
|
||||
});
|
||||
}
|
||||
},
|
||||
[],
|
||||
}
|
||||
},
|
||||
[store],
|
||||
);
|
||||
|
||||
return { pushFocusItemToFocusStack };
|
||||
|
||||
+35
-32
@@ -1,48 +1,51 @@
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { DEBUG_FOCUS_STACK } from '@/ui/utilities/focus/constants/DebugFocusStack';
|
||||
import { focusStackState } from '@/ui/utilities/focus/states/focusStackState';
|
||||
import { type FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
import { useStore } from 'jotai';
|
||||
import { logDebug } from '~/utils/logDebug';
|
||||
|
||||
export const useRemoveLastFocusItemFromFocusStackByComponentType = () => {
|
||||
const removeLastFocusItemFromFocusStackByComponentType = useRecoilCallback(
|
||||
({ snapshot, set }) =>
|
||||
({ componentType }: { componentType: FocusComponentType }) => {
|
||||
const focusStack = snapshot.getLoadable(focusStackState).getValue();
|
||||
const store = useStore();
|
||||
|
||||
const lastMatchingIndex = focusStack.findLastIndex(
|
||||
(focusStackItem) =>
|
||||
focusStackItem.componentInstance.componentType === componentType,
|
||||
);
|
||||
const removeLastFocusItemFromFocusStackByComponentType = useCallback(
|
||||
({ componentType }: { componentType: FocusComponentType }) => {
|
||||
const focusStack = store.get(focusStackState.atom);
|
||||
|
||||
if (lastMatchingIndex === -1) {
|
||||
if (DEBUG_FOCUS_STACK) {
|
||||
logDebug(
|
||||
`DEBUG: removeFocusItemFromFocusStackByComponentType - no item found for type ${componentType}`,
|
||||
{ focusStack },
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const removedFocusItem = focusStack[lastMatchingIndex];
|
||||
const newFocusStack = focusStack.filter(
|
||||
(_, index) => index !== lastMatchingIndex,
|
||||
);
|
||||
|
||||
set(focusStackState, newFocusStack);
|
||||
const lastMatchingIndex = focusStack.findLastIndex(
|
||||
(focusStackItem) =>
|
||||
focusStackItem.componentInstance.componentType === componentType,
|
||||
);
|
||||
|
||||
if (lastMatchingIndex === -1) {
|
||||
if (DEBUG_FOCUS_STACK) {
|
||||
logDebug(
|
||||
`DEBUG: removeFocusItemFromFocusStackByComponentType ${componentType}`,
|
||||
{
|
||||
removedFocusItem,
|
||||
newFocusStack,
|
||||
},
|
||||
`DEBUG: removeFocusItemFromFocusStackByComponentType - no item found for type ${componentType}`,
|
||||
{ focusStack },
|
||||
);
|
||||
}
|
||||
},
|
||||
[],
|
||||
return;
|
||||
}
|
||||
|
||||
const removedFocusItem = focusStack[lastMatchingIndex];
|
||||
const newFocusStack = focusStack.filter(
|
||||
(_, index) => index !== lastMatchingIndex,
|
||||
);
|
||||
|
||||
store.set(focusStackState.atom, newFocusStack);
|
||||
|
||||
if (DEBUG_FOCUS_STACK) {
|
||||
logDebug(
|
||||
`DEBUG: removeFocusItemFromFocusStackByComponentType ${componentType}`,
|
||||
{
|
||||
removedFocusItem,
|
||||
newFocusStack,
|
||||
},
|
||||
);
|
||||
}
|
||||
},
|
||||
[store],
|
||||
);
|
||||
|
||||
return { removeLastFocusItemFromFocusStackByComponentType };
|
||||
|
||||
+25
-22
@@ -1,35 +1,38 @@
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { DEBUG_FOCUS_STACK } from '@/ui/utilities/focus/constants/DebugFocusStack';
|
||||
import { focusStackState } from '@/ui/utilities/focus/states/focusStackState';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
import { useStore } from 'jotai';
|
||||
import { logDebug } from '~/utils/logDebug';
|
||||
|
||||
export const useRemoveFocusItemFromFocusStackById = () => {
|
||||
const removeFocusItemFromFocusStackById = useRecoilCallback(
|
||||
({ snapshot, set }) =>
|
||||
({ focusId }: { focusId: string }) => {
|
||||
const focusStack = snapshot.getLoadable(focusStackState).getValue();
|
||||
const store = useStore();
|
||||
|
||||
const removedFocusItem = focusStack.find(
|
||||
(focusStackItem) => focusStackItem.focusId === focusId,
|
||||
);
|
||||
const removeFocusItemFromFocusStackById = useCallback(
|
||||
({ focusId }: { focusId: string }) => {
|
||||
const focusStack = store.get(focusStackState.atom);
|
||||
|
||||
if (!removedFocusItem) {
|
||||
return;
|
||||
}
|
||||
const removedFocusItem = focusStack.find(
|
||||
(focusStackItem) => focusStackItem.focusId === focusId,
|
||||
);
|
||||
|
||||
const newFocusStack = focusStack.filter(
|
||||
(focusStackItem) => focusStackItem.focusId !== focusId,
|
||||
);
|
||||
if (!removedFocusItem) {
|
||||
return;
|
||||
}
|
||||
|
||||
set(focusStackState, newFocusStack);
|
||||
const newFocusStack = focusStack.filter(
|
||||
(focusStackItem) => focusStackItem.focusId !== focusId,
|
||||
);
|
||||
|
||||
if (DEBUG_FOCUS_STACK) {
|
||||
logDebug(`DEBUG: removeFocusItemFromFocusStack ${focusId}`, {
|
||||
newFocusStack,
|
||||
});
|
||||
}
|
||||
},
|
||||
[],
|
||||
store.set(focusStackState.atom, newFocusStack);
|
||||
|
||||
if (DEBUG_FOCUS_STACK) {
|
||||
logDebug(`DEBUG: removeFocusItemFromFocusStack ${focusId}`, {
|
||||
newFocusStack,
|
||||
});
|
||||
}
|
||||
},
|
||||
[store],
|
||||
);
|
||||
|
||||
return { removeFocusItemFromFocusStackById };
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { DEBUG_FOCUS_STACK } from '@/ui/utilities/focus/constants/DebugFocusStack';
|
||||
import { focusStackState } from '@/ui/utilities/focus/states/focusStackState';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
import { useStore } from 'jotai';
|
||||
import { logDebug } from '~/utils/logDebug';
|
||||
|
||||
export const useResetFocusStack = () => {
|
||||
const resetFocusStack = useRecoilCallback(
|
||||
({ reset }) =>
|
||||
() => {
|
||||
reset(focusStackState);
|
||||
const store = useStore();
|
||||
|
||||
if (DEBUG_FOCUS_STACK) {
|
||||
logDebug(`DEBUG: reset focus stack`);
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
const resetFocusStack = useCallback(() => {
|
||||
store.set(focusStackState.atom, []);
|
||||
|
||||
if (DEBUG_FOCUS_STACK) {
|
||||
logDebug(`DEBUG: reset focus stack`);
|
||||
}
|
||||
}, [store]);
|
||||
|
||||
return { resetFocusStack };
|
||||
};
|
||||
|
||||
+15
-12
@@ -1,22 +1,25 @@
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { DEBUG_FOCUS_STACK } from '@/ui/utilities/focus/constants/DebugFocusStack';
|
||||
import { focusStackState } from '@/ui/utilities/focus/states/focusStackState';
|
||||
import { type FocusStackItem } from '@/ui/utilities/focus/types/FocusStackItem';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
import { useStore } from 'jotai';
|
||||
import { logDebug } from '~/utils/logDebug';
|
||||
|
||||
export const useResetFocusStackToFocusItem = () => {
|
||||
const resetFocusStackToFocusItem = useRecoilCallback(
|
||||
({ set }) =>
|
||||
({ focusStackItem }: { focusStackItem: FocusStackItem }) => {
|
||||
set(focusStackState, [focusStackItem]);
|
||||
const store = useStore();
|
||||
|
||||
if (DEBUG_FOCUS_STACK) {
|
||||
logDebug(`DEBUG: reset focus stack to focus item`, {
|
||||
focusStackItem,
|
||||
});
|
||||
}
|
||||
},
|
||||
[],
|
||||
const resetFocusStackToFocusItem = useCallback(
|
||||
({ focusStackItem }: { focusStackItem: FocusStackItem }) => {
|
||||
store.set(focusStackState.atom, [focusStackItem]);
|
||||
|
||||
if (DEBUG_FOCUS_STACK) {
|
||||
logDebug(`DEBUG: reset focus stack to focus item`, {
|
||||
focusStackItem,
|
||||
});
|
||||
}
|
||||
},
|
||||
[store],
|
||||
);
|
||||
|
||||
return { resetFocusStackToFocusItem };
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
import { selector } from 'recoil';
|
||||
import { createSelectorV2 } from '@/ui/utilities/state/jotai/utils/createSelectorV2';
|
||||
import { focusStackState } from './focusStackState';
|
||||
|
||||
export const currentFocusIdSelector = selector<string | undefined>({
|
||||
export const currentFocusIdSelector = createSelectorV2<string | undefined>({
|
||||
key: 'currentFocusIdSelector',
|
||||
get: ({ get }) => {
|
||||
const focusStack = get(focusStackState);
|
||||
|
||||
+4
-2
@@ -1,8 +1,10 @@
|
||||
import { type FocusStackItem } from '@/ui/utilities/focus/types/FocusStackItem';
|
||||
import { selector } from 'recoil';
|
||||
import { createSelectorV2 } from '@/ui/utilities/state/jotai/utils/createSelectorV2';
|
||||
import { focusStackState } from './focusStackState';
|
||||
|
||||
export const currentFocusedItemSelector = selector<FocusStackItem | undefined>({
|
||||
export const currentFocusedItemSelector = createSelectorV2<
|
||||
FocusStackItem | undefined
|
||||
>({
|
||||
key: 'currentFocusedItemSelector',
|
||||
get: ({ get }) => {
|
||||
const focusStack = get(focusStackState);
|
||||
|
||||
+4
-5
@@ -1,11 +1,11 @@
|
||||
import { focusStackState } from '@/ui/utilities/focus/states/focusStackState';
|
||||
import { DEFAULT_GLOBAL_HOTKEYS_CONFIG } from '@/ui/utilities/hotkey/constants/DefaultGlobalHotkeysConfig';
|
||||
import { type GlobalHotkeysConfig } from '@/ui/utilities/hotkey/types/GlobalHotkeysConfig';
|
||||
import { selector } from 'recoil';
|
||||
import { createSelectorV2 } from '@/ui/utilities/state/jotai/utils/createSelectorV2';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const currentGlobalHotkeysConfigSelector = selector<GlobalHotkeysConfig>(
|
||||
{
|
||||
export const currentGlobalHotkeysConfigSelector =
|
||||
createSelectorV2<GlobalHotkeysConfig>({
|
||||
key: 'currentGlobalHotkeysConfigSelector',
|
||||
get: ({ get }) => {
|
||||
const focusStack = get(focusStackState);
|
||||
@@ -17,5 +17,4 @@ export const currentGlobalHotkeysConfigSelector = selector<GlobalHotkeysConfig>(
|
||||
|
||||
return lastFocusStackItem.globalHotkeysConfig;
|
||||
},
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { type FocusStackItem } from '@/ui/utilities/focus/types/FocusStackItem';
|
||||
import { createState } from '@/ui/utilities/state/utils/createState';
|
||||
import { createStateV2 } from '@/ui/utilities/state/jotai/utils/createStateV2';
|
||||
|
||||
export const focusStackState = createState<FocusStackItem[]>({
|
||||
export const focusStackState = createStateV2<FocusStackItem[]>({
|
||||
key: 'focusStackState',
|
||||
defaultValue: [],
|
||||
});
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { useGlobalHotkeysCallback } from '@/ui/utilities/hotkey/hooks/useGlobalHotkeysCallback';
|
||||
import { pendingHotkeyState } from '@/ui/utilities/hotkey/states/internal/pendingHotkeysState';
|
||||
import { useStore } from 'jotai';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import {
|
||||
type HotkeyCallback,
|
||||
type Keys,
|
||||
type Options,
|
||||
} from 'react-hotkeys-hook/dist/types';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
type UseHotkeysOptionsWithoutBuggyOptions = Omit<Options, 'enabled'>;
|
||||
@@ -24,6 +26,8 @@ export const useGlobalHotkeys = ({
|
||||
dependencies?: unknown[];
|
||||
options?: UseHotkeysOptionsWithoutBuggyOptions;
|
||||
}) => {
|
||||
const store = useStore();
|
||||
|
||||
const callGlobalHotkeysCallback = useGlobalHotkeysCallback(dependencies);
|
||||
|
||||
const enableOnContentEditable = isDefined(options?.enableOnContentEditable)
|
||||
@@ -42,20 +46,17 @@ export const useGlobalHotkeys = ({
|
||||
? options.ignoreModifiers === true
|
||||
: false;
|
||||
|
||||
const handleCallback = useRecoilCallback(
|
||||
({ snapshot, set }) =>
|
||||
async (keyboardEvent: KeyboardEvent, hotkeysEvent: any) => {
|
||||
const pendingHotkey = snapshot
|
||||
.getLoadable(pendingHotkeyState)
|
||||
.getValue();
|
||||
const handleCallback = useCallback(
|
||||
async (keyboardEvent: KeyboardEvent, hotkeysEvent: any) => {
|
||||
const pendingHotkey = store.get(pendingHotkeyState.atom);
|
||||
|
||||
if (!pendingHotkey) {
|
||||
callback(keyboardEvent, hotkeysEvent);
|
||||
}
|
||||
if (!pendingHotkey) {
|
||||
callback(keyboardEvent, hotkeysEvent);
|
||||
}
|
||||
|
||||
set(pendingHotkeyState, null);
|
||||
},
|
||||
[callback],
|
||||
store.set(pendingHotkeyState.atom, null);
|
||||
},
|
||||
[callback, store],
|
||||
);
|
||||
|
||||
return useHotkeys(
|
||||
|
||||
+64
-61
@@ -1,83 +1,86 @@
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { DEBUG_FOCUS_STACK } from '@/ui/utilities/focus/constants/DebugFocusStack';
|
||||
import { currentGlobalHotkeysConfigSelector } from '@/ui/utilities/focus/states/currentGlobalHotkeysConfigSelector';
|
||||
import { useStore } from 'jotai';
|
||||
import {
|
||||
type Hotkey,
|
||||
type OptionsOrDependencyArray,
|
||||
} from 'react-hotkeys-hook/dist/types';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
import { logDebug } from '~/utils/logDebug';
|
||||
|
||||
export const useGlobalHotkeysCallback = (
|
||||
dependencies?: OptionsOrDependencyArray,
|
||||
) => {
|
||||
const store = useStore();
|
||||
|
||||
const dependencyArray = Array.isArray(dependencies) ? dependencies : [];
|
||||
|
||||
return useRecoilCallback(
|
||||
({ snapshot }) =>
|
||||
({
|
||||
callback,
|
||||
containsModifier,
|
||||
hotkeysEvent,
|
||||
keyboardEvent,
|
||||
preventDefault,
|
||||
}: {
|
||||
keyboardEvent: KeyboardEvent;
|
||||
hotkeysEvent: Hotkey;
|
||||
containsModifier: boolean;
|
||||
callback: (keyboardEvent: KeyboardEvent, hotkeysEvent: Hotkey) => void;
|
||||
preventDefault?: boolean;
|
||||
}) => {
|
||||
const currentGlobalHotkeysConfig = snapshot
|
||||
.getLoadable(currentGlobalHotkeysConfigSelector)
|
||||
.getValue();
|
||||
return useCallback(
|
||||
({
|
||||
callback,
|
||||
containsModifier,
|
||||
hotkeysEvent,
|
||||
keyboardEvent,
|
||||
preventDefault,
|
||||
}: {
|
||||
keyboardEvent: KeyboardEvent;
|
||||
hotkeysEvent: Hotkey;
|
||||
containsModifier: boolean;
|
||||
callback: (keyboardEvent: KeyboardEvent, hotkeysEvent: Hotkey) => void;
|
||||
preventDefault?: boolean;
|
||||
}) => {
|
||||
const currentGlobalHotkeysConfig = store.get(
|
||||
currentGlobalHotkeysConfigSelector.atom,
|
||||
);
|
||||
|
||||
if (
|
||||
containsModifier &&
|
||||
!currentGlobalHotkeysConfig.enableGlobalHotkeysWithModifiers
|
||||
) {
|
||||
if (DEBUG_FOCUS_STACK) {
|
||||
logDebug(
|
||||
`DEBUG: %cI can't call hotkey (${
|
||||
hotkeysEvent.keys
|
||||
}) because global hotkeys with modifiers are disabled`,
|
||||
'color: gray; ',
|
||||
);
|
||||
}
|
||||
|
||||
return;
|
||||
if (
|
||||
containsModifier &&
|
||||
!currentGlobalHotkeysConfig.enableGlobalHotkeysWithModifiers
|
||||
) {
|
||||
if (DEBUG_FOCUS_STACK) {
|
||||
logDebug(
|
||||
`DEBUG: %cI can't call hotkey (${
|
||||
hotkeysEvent.keys
|
||||
}) because global hotkeys with modifiers are disabled`,
|
||||
'color: gray; ',
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
!containsModifier &&
|
||||
!currentGlobalHotkeysConfig.enableGlobalHotkeysConflictingWithKeyboard
|
||||
) {
|
||||
if (DEBUG_FOCUS_STACK) {
|
||||
logDebug(
|
||||
`DEBUG: %cI can't call hotkey (${
|
||||
hotkeysEvent.keys
|
||||
}) because global hotkeys conflicting with keyboard are disabled`,
|
||||
'color: gray; ',
|
||||
);
|
||||
}
|
||||
return;
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
!containsModifier &&
|
||||
!currentGlobalHotkeysConfig.enableGlobalHotkeysConflictingWithKeyboard
|
||||
) {
|
||||
if (DEBUG_FOCUS_STACK) {
|
||||
logDebug(
|
||||
`DEBUG: %cI can't call hotkey (${
|
||||
hotkeysEvent.keys
|
||||
}) because global hotkeys conflicting with keyboard are disabled`,
|
||||
'color: gray; ',
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (preventDefault === true) {
|
||||
if (DEBUG_FOCUS_STACK) {
|
||||
logDebug(
|
||||
`DEBUG: %cI prevent global default for hotkey (${hotkeysEvent.keys})`,
|
||||
'color: gray;',
|
||||
);
|
||||
}
|
||||
|
||||
if (preventDefault === true) {
|
||||
if (DEBUG_FOCUS_STACK) {
|
||||
logDebug(
|
||||
`DEBUG: %cI prevent global default for hotkey (${hotkeysEvent.keys})`,
|
||||
'color: gray;',
|
||||
);
|
||||
}
|
||||
keyboardEvent.stopPropagation();
|
||||
keyboardEvent.preventDefault();
|
||||
keyboardEvent.stopImmediatePropagation();
|
||||
}
|
||||
|
||||
keyboardEvent.stopPropagation();
|
||||
keyboardEvent.preventDefault();
|
||||
keyboardEvent.stopImmediatePropagation();
|
||||
}
|
||||
|
||||
return callback(keyboardEvent, hotkeysEvent);
|
||||
},
|
||||
return callback(keyboardEvent, hotkeysEvent);
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
dependencyArray,
|
||||
[...dependencyArray, store],
|
||||
);
|
||||
};
|
||||
|
||||
+3
-2
@@ -1,10 +1,10 @@
|
||||
import { type Options, useHotkeys } from 'react-hotkeys-hook';
|
||||
import { type Keys } from 'react-hotkeys-hook/dist/types';
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import { pendingHotkeyState } from '@/ui/utilities/hotkey/states/internal/pendingHotkeysState';
|
||||
|
||||
import { useGlobalHotkeysCallback } from '@/ui/utilities/hotkey/hooks/useGlobalHotkeysCallback';
|
||||
import { useRecoilStateV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilStateV2';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const useGlobalHotkeysSequence = (
|
||||
@@ -18,7 +18,8 @@ export const useGlobalHotkeysSequence = (
|
||||
},
|
||||
deps: any[] = [],
|
||||
) => {
|
||||
const [pendingHotkey, setPendingHotkey] = useRecoilState(pendingHotkeyState);
|
||||
const [pendingHotkey, setPendingHotkey] =
|
||||
useRecoilStateV2(pendingHotkeyState);
|
||||
|
||||
const callGlobalHotkeysCallback = useGlobalHotkeysCallback();
|
||||
|
||||
|
||||
+3
-2
@@ -1,12 +1,12 @@
|
||||
import { useHotkeysOnFocusedElementCallback } from '@/ui/utilities/hotkey/hooks/useHotkeysOnFocusedElementCallback';
|
||||
import { pendingHotkeyState } from '@/ui/utilities/hotkey/states/internal/pendingHotkeysState';
|
||||
import { useRecoilStateV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilStateV2';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import {
|
||||
type HotkeyCallback,
|
||||
type Keys,
|
||||
type Options,
|
||||
} from 'react-hotkeys-hook/dist/types';
|
||||
import { useRecoilState } from 'recoil';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
type UseHotkeysOptionsWithoutBuggyOptions = Omit<Options, 'enabled'>;
|
||||
@@ -24,7 +24,8 @@ export const useHotkeysOnFocusedElement = ({
|
||||
dependencies?: unknown[];
|
||||
options?: UseHotkeysOptionsWithoutBuggyOptions;
|
||||
}) => {
|
||||
const [pendingHotkey, setPendingHotkey] = useRecoilState(pendingHotkeyState);
|
||||
const [pendingHotkey, setPendingHotkey] =
|
||||
useRecoilStateV2(pendingHotkeyState);
|
||||
|
||||
const callScopedHotkeyCallback =
|
||||
useHotkeysOnFocusedElementCallback(dependencies);
|
||||
|
||||
+49
-48
@@ -1,74 +1,75 @@
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { DEBUG_FOCUS_STACK } from '@/ui/utilities/focus/constants/DebugFocusStack';
|
||||
import { currentFocusIdSelector } from '@/ui/utilities/focus/states/currentFocusIdSelector';
|
||||
import { useStore } from 'jotai';
|
||||
import {
|
||||
type Hotkey,
|
||||
type OptionsOrDependencyArray,
|
||||
} from 'react-hotkeys-hook/dist/types';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
import { logDebug } from '~/utils/logDebug';
|
||||
import { currentFocusIdSelector } from '@/ui/utilities/focus/states/currentFocusIdSelector';
|
||||
|
||||
export const useHotkeysOnFocusedElementCallback = (
|
||||
dependencies?: OptionsOrDependencyArray,
|
||||
) => {
|
||||
const store = useStore();
|
||||
|
||||
const dependencyArray = Array.isArray(dependencies) ? dependencies : [];
|
||||
|
||||
return useRecoilCallback(
|
||||
({ snapshot }) =>
|
||||
({
|
||||
callback,
|
||||
hotkeysEvent,
|
||||
keyboardEvent,
|
||||
focusId,
|
||||
preventDefault,
|
||||
}: {
|
||||
keyboardEvent: KeyboardEvent;
|
||||
hotkeysEvent: Hotkey;
|
||||
callback: (keyboardEvent: KeyboardEvent, hotkeysEvent: Hotkey) => void;
|
||||
focusId: string;
|
||||
preventDefault?: boolean;
|
||||
}) => {
|
||||
const currentFocusId = snapshot
|
||||
.getLoadable(currentFocusIdSelector)
|
||||
.getValue();
|
||||
|
||||
if (currentFocusId !== focusId) {
|
||||
if (DEBUG_FOCUS_STACK) {
|
||||
logDebug(
|
||||
`DEBUG: %cI can't call hotkey (${
|
||||
hotkeysEvent.keys
|
||||
}) because I'm in [${focusId}] and the current focus identifier is [${currentFocusId}]`,
|
||||
'color: gray; ',
|
||||
);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
return useCallback(
|
||||
({
|
||||
callback,
|
||||
hotkeysEvent,
|
||||
keyboardEvent,
|
||||
focusId,
|
||||
preventDefault,
|
||||
}: {
|
||||
keyboardEvent: KeyboardEvent;
|
||||
hotkeysEvent: Hotkey;
|
||||
callback: (keyboardEvent: KeyboardEvent, hotkeysEvent: Hotkey) => void;
|
||||
focusId: string;
|
||||
preventDefault?: boolean;
|
||||
}) => {
|
||||
const currentFocusId = store.get(currentFocusIdSelector.atom);
|
||||
|
||||
if (currentFocusId !== focusId) {
|
||||
if (DEBUG_FOCUS_STACK) {
|
||||
logDebug(
|
||||
`DEBUG: %cI can call hotkey (${
|
||||
`DEBUG: %cI can't call hotkey (${
|
||||
hotkeysEvent.keys
|
||||
}) because I'm in [${focusId}] and the current focus identifier is [${currentFocusId}]`,
|
||||
'color: green;',
|
||||
'color: gray; ',
|
||||
);
|
||||
}
|
||||
|
||||
if (preventDefault === true) {
|
||||
if (DEBUG_FOCUS_STACK) {
|
||||
logDebug(
|
||||
`DEBUG: %cI prevent default for hotkey (${hotkeysEvent.keys})`,
|
||||
'color: gray;',
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
keyboardEvent.stopPropagation();
|
||||
keyboardEvent.preventDefault();
|
||||
keyboardEvent.stopImmediatePropagation();
|
||||
if (DEBUG_FOCUS_STACK) {
|
||||
logDebug(
|
||||
`DEBUG: %cI can call hotkey (${
|
||||
hotkeysEvent.keys
|
||||
}) because I'm in [${focusId}] and the current focus identifier is [${currentFocusId}]`,
|
||||
'color: green;',
|
||||
);
|
||||
}
|
||||
|
||||
if (preventDefault === true) {
|
||||
if (DEBUG_FOCUS_STACK) {
|
||||
logDebug(
|
||||
`DEBUG: %cI prevent default for hotkey (${hotkeysEvent.keys})`,
|
||||
'color: gray;',
|
||||
);
|
||||
}
|
||||
|
||||
return callback(keyboardEvent, hotkeysEvent);
|
||||
},
|
||||
keyboardEvent.stopPropagation();
|
||||
keyboardEvent.preventDefault();
|
||||
keyboardEvent.stopImmediatePropagation();
|
||||
}
|
||||
|
||||
return callback(keyboardEvent, hotkeysEvent);
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
dependencyArray,
|
||||
[...dependencyArray, store],
|
||||
);
|
||||
};
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
import { type Keys } from 'react-hotkeys-hook/dist/types';
|
||||
import { createState } from '@/ui/utilities/state/utils/createState';
|
||||
import { createStateV2 } from '@/ui/utilities/state/jotai/utils/createStateV2';
|
||||
|
||||
export const pendingHotkeyState = createState<Keys | null>({
|
||||
export const pendingHotkeyState = createStateV2<Keys | null>({
|
||||
key: 'pendingHotkeyState',
|
||||
defaultValue: null,
|
||||
});
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
import { createState } from '@/ui/utilities/state/utils/createState';
|
||||
export const currentPageLocationState = createState<string>({
|
||||
import { createStateV2 } from '@/ui/utilities/state/jotai/utils/createStateV2';
|
||||
export const currentPageLocationState = createStateV2<string>({
|
||||
key: 'currentPageLocationState',
|
||||
defaultValue: '',
|
||||
});
|
||||
|
||||
+16
-4
@@ -1,26 +1,38 @@
|
||||
import { act, renderHook } from '@testing-library/react';
|
||||
import { RecoilRoot } from 'recoil';
|
||||
import { Provider as JotaiProvider } from 'jotai';
|
||||
|
||||
import { useClickOutsideListener } from '@/ui/utilities/pointer-event/hooks/useClickOutsideListener';
|
||||
import { clickOutsideListenerIsActivatedComponentState } from '@/ui/utilities/pointer-event/states/clickOutsideListenerIsActivatedComponentState';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilComponentValueV2';
|
||||
import {
|
||||
jotaiStore,
|
||||
resetJotaiStore,
|
||||
} from '@/ui/utilities/state/jotai/jotaiStore';
|
||||
|
||||
const componentId = 'componentId';
|
||||
|
||||
const Wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||
<JotaiProvider store={jotaiStore}>{children}</JotaiProvider>
|
||||
);
|
||||
|
||||
describe('useClickOutsideListener', () => {
|
||||
beforeEach(() => {
|
||||
resetJotaiStore();
|
||||
});
|
||||
|
||||
it('should toggle the click outside listener activation state', async () => {
|
||||
const { result } = renderHook(
|
||||
() => {
|
||||
return {
|
||||
useClickOutside: useClickOutsideListener(componentId),
|
||||
isActivated: useRecoilComponentValue(
|
||||
isActivated: useRecoilComponentValueV2(
|
||||
clickOutsideListenerIsActivatedComponentState,
|
||||
componentId,
|
||||
),
|
||||
};
|
||||
},
|
||||
{
|
||||
wrapper: RecoilRoot,
|
||||
wrapper: Wrapper,
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
+21
-24
@@ -1,34 +1,31 @@
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { clickOutsideListenerIsActivatedComponentState } from '@/ui/utilities/pointer-event/states/clickOutsideListenerIsActivatedComponentState';
|
||||
import { clickOutsideListenerMouseDownHappenedComponentState } from '@/ui/utilities/pointer-event/states/clickOutsideListenerMouseDownHappenedComponentState';
|
||||
import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState';
|
||||
import { useStore } from 'jotai';
|
||||
|
||||
export const useClickOutsideListener = (instanceId: string) => {
|
||||
const clickOutsideListenerIsActivatedState = useRecoilComponentCallbackState(
|
||||
clickOutsideListenerIsActivatedComponentState,
|
||||
instanceId,
|
||||
);
|
||||
const store = useStore();
|
||||
|
||||
const clickOutsideListenerMouseDownHappenedState =
|
||||
useRecoilComponentCallbackState(
|
||||
clickOutsideListenerMouseDownHappenedComponentState,
|
||||
instanceId,
|
||||
);
|
||||
const toggleClickOutside = useCallback(
|
||||
(activated: boolean) => {
|
||||
store.set(
|
||||
clickOutsideListenerIsActivatedComponentState.atomFamily({
|
||||
instanceId,
|
||||
}),
|
||||
activated,
|
||||
);
|
||||
|
||||
const toggleClickOutside = useRecoilCallback(
|
||||
({ set }) =>
|
||||
(activated: boolean) => {
|
||||
set(clickOutsideListenerIsActivatedState, activated);
|
||||
|
||||
if (!activated) {
|
||||
set(clickOutsideListenerMouseDownHappenedState, false);
|
||||
}
|
||||
},
|
||||
[
|
||||
clickOutsideListenerIsActivatedState,
|
||||
clickOutsideListenerMouseDownHappenedState,
|
||||
],
|
||||
if (!activated) {
|
||||
store.set(
|
||||
clickOutsideListenerMouseDownHappenedComponentState.atomFamily({
|
||||
instanceId,
|
||||
}),
|
||||
false,
|
||||
);
|
||||
}
|
||||
},
|
||||
[instanceId, store],
|
||||
);
|
||||
|
||||
return {
|
||||
|
||||
+96
-108
@@ -1,9 +1,9 @@
|
||||
import { useCallback, useEffect, type RefObject } from 'react';
|
||||
|
||||
import { clickOutsideListenerIsActivatedComponentState } from '@/ui/utilities/pointer-event/states/clickOutsideListenerIsActivatedComponentState';
|
||||
import { clickOutsideListenerIsMouseDownInsideComponentState } from '@/ui/utilities/pointer-event/states/clickOutsideListenerIsMouseDownInsideComponentState';
|
||||
import { clickOutsideListenerMouseDownHappenedComponentState } from '@/ui/utilities/pointer-event/states/clickOutsideListenerMouseDownHappenedComponentState';
|
||||
import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState';
|
||||
import { useEffect, type RefObject } from 'react';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
import { useStore } from 'jotai';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
const CLICK_OUTSIDE_DEBUG_MODE = false;
|
||||
@@ -23,132 +23,120 @@ export const useListenClickOutside = <T extends Element>({
|
||||
listenerId,
|
||||
enabled = true,
|
||||
}: ClickOutsideListenerProps<T>) => {
|
||||
const clickOutsideListenerIsMouseDownInsideState =
|
||||
useRecoilComponentCallbackState(
|
||||
clickOutsideListenerIsMouseDownInsideComponentState,
|
||||
listenerId,
|
||||
);
|
||||
const clickOutsideListenerIsActivatedState = useRecoilComponentCallbackState(
|
||||
clickOutsideListenerIsActivatedComponentState,
|
||||
listenerId,
|
||||
);
|
||||
const clickOutsideListenerMouseDownHappenedState =
|
||||
useRecoilComponentCallbackState(
|
||||
clickOutsideListenerMouseDownHappenedComponentState,
|
||||
listenerId,
|
||||
);
|
||||
const store = useStore();
|
||||
|
||||
const handleMouseDown = useRecoilCallback(
|
||||
({ snapshot, set }) =>
|
||||
(event: MouseEvent | TouchEvent) => {
|
||||
const clickOutsideListenerIsActivated = snapshot
|
||||
.getLoadable(clickOutsideListenerIsActivatedState)
|
||||
.getValue();
|
||||
const handleMouseDown = useCallback(
|
||||
(event: MouseEvent | TouchEvent) => {
|
||||
const clickOutsideListenerIsActivated = store.get(
|
||||
clickOutsideListenerIsActivatedComponentState.atomFamily({
|
||||
instanceId: listenerId,
|
||||
}),
|
||||
);
|
||||
|
||||
set(clickOutsideListenerMouseDownHappenedState, true);
|
||||
store.set(
|
||||
clickOutsideListenerMouseDownHappenedComponentState.atomFamily({
|
||||
instanceId: listenerId,
|
||||
}),
|
||||
true,
|
||||
);
|
||||
|
||||
const isListening = clickOutsideListenerIsActivated && enabled;
|
||||
const isListening = clickOutsideListenerIsActivated && enabled;
|
||||
|
||||
if (!isListening) {
|
||||
return;
|
||||
}
|
||||
if (!isListening) {
|
||||
return;
|
||||
}
|
||||
|
||||
const clickedOnAtLeastOneRef = refs
|
||||
.filter((ref) => !!ref.current)
|
||||
.some((ref) => ref.current?.contains(event.target as Node));
|
||||
const clickedOnAtLeastOneRef = refs
|
||||
.filter((ref) => !!ref.current)
|
||||
.some((ref) => ref.current?.contains(event.target as Node));
|
||||
|
||||
set(clickOutsideListenerIsMouseDownInsideState, clickedOnAtLeastOneRef);
|
||||
},
|
||||
[
|
||||
clickOutsideListenerIsActivatedState,
|
||||
clickOutsideListenerMouseDownHappenedState,
|
||||
enabled,
|
||||
refs,
|
||||
clickOutsideListenerIsMouseDownInsideState,
|
||||
],
|
||||
store.set(
|
||||
clickOutsideListenerIsMouseDownInsideComponentState.atomFamily({
|
||||
instanceId: listenerId,
|
||||
}),
|
||||
clickedOnAtLeastOneRef,
|
||||
);
|
||||
},
|
||||
[listenerId, enabled, refs, store],
|
||||
);
|
||||
|
||||
const handleClickOutside = useRecoilCallback(
|
||||
({ snapshot }) =>
|
||||
(event: MouseEvent | TouchEvent) => {
|
||||
const clickOutsideListenerIsActivated = snapshot
|
||||
.getLoadable(clickOutsideListenerIsActivatedState)
|
||||
.getValue();
|
||||
const handleClickOutside = useCallback(
|
||||
(event: MouseEvent | TouchEvent) => {
|
||||
const clickOutsideListenerIsActivated = store.get(
|
||||
clickOutsideListenerIsActivatedComponentState.atomFamily({
|
||||
instanceId: listenerId,
|
||||
}),
|
||||
);
|
||||
|
||||
const isListening = clickOutsideListenerIsActivated && enabled;
|
||||
const isListening = clickOutsideListenerIsActivated && enabled;
|
||||
|
||||
const isMouseDownInside = snapshot
|
||||
.getLoadable(clickOutsideListenerIsMouseDownInsideState)
|
||||
.getValue();
|
||||
const isMouseDownInside = store.get(
|
||||
clickOutsideListenerIsMouseDownInsideComponentState.atomFamily({
|
||||
instanceId: listenerId,
|
||||
}),
|
||||
);
|
||||
|
||||
const hasMouseDownHappened = snapshot
|
||||
.getLoadable(clickOutsideListenerMouseDownHappenedState)
|
||||
.getValue();
|
||||
const hasMouseDownHappened = store.get(
|
||||
clickOutsideListenerMouseDownHappenedComponentState.atomFamily({
|
||||
instanceId: listenerId,
|
||||
}),
|
||||
);
|
||||
|
||||
const clickedElement = event.target as HTMLElement;
|
||||
let isClickedOnExcluded = false;
|
||||
let currentElement: HTMLElement | null = clickedElement;
|
||||
const clickedElement = event.target as HTMLElement;
|
||||
let isClickedOnExcluded = false;
|
||||
let currentElement: HTMLElement | null = clickedElement;
|
||||
|
||||
while (currentElement) {
|
||||
const currentDataAttributes = currentElement.dataset;
|
||||
const isGloballyExcluded =
|
||||
currentDataAttributes?.globallyPreventClickOutside === 'true';
|
||||
while (currentElement) {
|
||||
const currentDataAttributes = currentElement.dataset;
|
||||
const isGloballyExcluded =
|
||||
currentDataAttributes?.globallyPreventClickOutside === 'true';
|
||||
|
||||
const clickOutsideId = currentDataAttributes?.clickOutsideId;
|
||||
const clickOutsideId = currentDataAttributes?.clickOutsideId;
|
||||
|
||||
isClickedOnExcluded =
|
||||
isGloballyExcluded ||
|
||||
(isDefined(clickOutsideId) &&
|
||||
isDefined(excludedClickOutsideIds) &&
|
||||
excludedClickOutsideIds.includes(clickOutsideId));
|
||||
isClickedOnExcluded =
|
||||
isGloballyExcluded ||
|
||||
(isDefined(clickOutsideId) &&
|
||||
isDefined(excludedClickOutsideIds) &&
|
||||
excludedClickOutsideIds.includes(clickOutsideId));
|
||||
|
||||
if (isClickedOnExcluded) {
|
||||
break;
|
||||
}
|
||||
|
||||
currentElement = currentElement.parentElement;
|
||||
if (isClickedOnExcluded) {
|
||||
break;
|
||||
}
|
||||
|
||||
const clickedOnAtLeastOneRef = refs
|
||||
.filter((ref) => !!ref.current)
|
||||
.some((ref) => ref.current?.contains(event.target as Node));
|
||||
currentElement = currentElement.parentElement;
|
||||
}
|
||||
|
||||
const shouldTrigger =
|
||||
isListening &&
|
||||
hasMouseDownHappened &&
|
||||
!clickedOnAtLeastOneRef &&
|
||||
!isMouseDownInside &&
|
||||
!isClickedOnExcluded;
|
||||
const clickedOnAtLeastOneRef = refs
|
||||
.filter((ref) => !!ref.current)
|
||||
.some((ref) => ref.current?.contains(event.target as Node));
|
||||
|
||||
if (CLICK_OUTSIDE_DEBUG_MODE) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('click outside compare ref', {
|
||||
listenerId,
|
||||
shouldTrigger,
|
||||
clickedOnAtLeastOneRef,
|
||||
isMouseDownInside,
|
||||
isListening,
|
||||
hasMouseDownHappened,
|
||||
isClickedOnExcluded,
|
||||
enabled,
|
||||
event,
|
||||
});
|
||||
}
|
||||
const shouldTrigger =
|
||||
isListening &&
|
||||
hasMouseDownHappened &&
|
||||
!clickedOnAtLeastOneRef &&
|
||||
!isMouseDownInside &&
|
||||
!isClickedOnExcluded;
|
||||
|
||||
if (shouldTrigger) {
|
||||
callback(event);
|
||||
}
|
||||
},
|
||||
[
|
||||
clickOutsideListenerIsActivatedState,
|
||||
enabled,
|
||||
clickOutsideListenerIsMouseDownInsideState,
|
||||
clickOutsideListenerMouseDownHappenedState,
|
||||
refs,
|
||||
excludedClickOutsideIds,
|
||||
callback,
|
||||
listenerId,
|
||||
],
|
||||
if (CLICK_OUTSIDE_DEBUG_MODE) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('click outside compare ref', {
|
||||
listenerId,
|
||||
shouldTrigger,
|
||||
clickedOnAtLeastOneRef,
|
||||
isMouseDownInside,
|
||||
isListening,
|
||||
hasMouseDownHappened,
|
||||
isClickedOnExcluded,
|
||||
enabled,
|
||||
event,
|
||||
});
|
||||
}
|
||||
|
||||
if (shouldTrigger) {
|
||||
callback(event);
|
||||
}
|
||||
},
|
||||
[listenerId, enabled, refs, excludedClickOutsideIds, callback, store],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
import { ClickOutsideListenerComponentInstanceContext } from '@/ui/utilities/pointer-event/states/contexts/ClickOutsideListenerComponentInstanceContext';
|
||||
import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState';
|
||||
import { createComponentStateV2 } from '@/ui/utilities/state/jotai/utils/createComponentStateV2';
|
||||
|
||||
export const clickOutsideListenerIsActivatedComponentState =
|
||||
createComponentState<boolean>({
|
||||
createComponentStateV2<boolean>({
|
||||
key: 'clickOutsideListenerIsActivatedComponentState',
|
||||
defaultValue: true,
|
||||
componentInstanceContext: ClickOutsideListenerComponentInstanceContext,
|
||||
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
import { ClickOutsideListenerComponentInstanceContext } from '@/ui/utilities/pointer-event/states/contexts/ClickOutsideListenerComponentInstanceContext';
|
||||
import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState';
|
||||
import { createComponentStateV2 } from '@/ui/utilities/state/jotai/utils/createComponentStateV2';
|
||||
|
||||
export const clickOutsideListenerIsMouseDownInsideComponentState =
|
||||
createComponentState<boolean>({
|
||||
createComponentStateV2<boolean>({
|
||||
key: 'clickOutsideListenerIsMouseDownInsideComponentState',
|
||||
defaultValue: false,
|
||||
componentInstanceContext: ClickOutsideListenerComponentInstanceContext,
|
||||
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
import { ClickOutsideListenerComponentInstanceContext } from '@/ui/utilities/pointer-event/states/contexts/ClickOutsideListenerComponentInstanceContext';
|
||||
import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState';
|
||||
import { createComponentStateV2 } from '@/ui/utilities/state/jotai/utils/createComponentStateV2';
|
||||
|
||||
export const clickOutsideListenerMouseDownHappenedComponentState =
|
||||
createComponentState<boolean>({
|
||||
createComponentStateV2<boolean>({
|
||||
key: 'clickOutsideListenerMouseDownHappenedComponentState',
|
||||
defaultValue: false,
|
||||
componentInstanceContext: ClickOutsideListenerComponentInstanceContext,
|
||||
|
||||
@@ -5,7 +5,7 @@ import { ScrollWrapperComponentInstanceContext } from '@/ui/utilities/scroll/sta
|
||||
import { scrollWrapperScrollBottomComponentState } from '@/ui/utilities/scroll/states/scrollWrapperScrollBottomComponentState';
|
||||
import { scrollWrapperScrollLeftComponentState } from '@/ui/utilities/scroll/states/scrollWrapperScrollLeftComponentState';
|
||||
import { scrollWrapperScrollTopComponentState } from '@/ui/utilities/scroll/states/scrollWrapperScrollTopComponentState';
|
||||
import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState';
|
||||
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/jotai/hooks/useSetRecoilComponentStateV2';
|
||||
|
||||
const StyledScrollWrapper = styled.div`
|
||||
&.scroll-wrapper-x-enabled {
|
||||
@@ -35,17 +35,17 @@ export const ScrollWrapper = ({
|
||||
defaultEnableXScroll = true,
|
||||
defaultEnableYScroll = true,
|
||||
}: ScrollWrapperProps) => {
|
||||
const setScrollTop = useSetRecoilComponentState(
|
||||
const setScrollTop = useSetRecoilComponentStateV2(
|
||||
scrollWrapperScrollTopComponentState,
|
||||
componentInstanceId,
|
||||
);
|
||||
|
||||
const setScrollLeft = useSetRecoilComponentState(
|
||||
const setScrollLeft = useSetRecoilComponentStateV2(
|
||||
scrollWrapperScrollLeftComponentState,
|
||||
componentInstanceId,
|
||||
);
|
||||
|
||||
const setScrollBottom = useSetRecoilComponentState(
|
||||
const setScrollBottom = useSetRecoilComponentStateV2(
|
||||
scrollWrapperScrollBottomComponentState,
|
||||
componentInstanceId,
|
||||
);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { SCROLL_RESTORATION_TOP_THRESHOLD_PX } from '@/ui/utilities/scroll/constants/ScrollRestorationTopThreshold';
|
||||
import { scrollWrapperScrollTopComponentState } from '@/ui/utilities/scroll/states/scrollWrapperScrollTopComponentState';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilComponentValueV2';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
@@ -10,7 +10,7 @@ export const useScrollRestoration = (componentInstanceId: string) => {
|
||||
const storageKey = `scroll-${location.pathname}`;
|
||||
const [isRestoring, setIsRestoring] = useState(false);
|
||||
|
||||
const scrollTop = useRecoilComponentValue(
|
||||
const scrollTop = useRecoilComponentValueV2(
|
||||
scrollWrapperScrollTopComponentState,
|
||||
componentInstanceId,
|
||||
);
|
||||
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
import { ScrollWrapperComponentInstanceContext } from '@/ui/utilities/scroll/states/contexts/ScrollWrapperComponentInstanceContext';
|
||||
import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState';
|
||||
import { createComponentStateV2 } from '@/ui/utilities/state/jotai/utils/createComponentStateV2';
|
||||
|
||||
export const scrollWrapperScrollBottomComponentState =
|
||||
createComponentState<number>({
|
||||
createComponentStateV2<number>({
|
||||
key: 'scrollWrapperScrollBottomComponentState',
|
||||
defaultValue: 0,
|
||||
componentInstanceContext: ScrollWrapperComponentInstanceContext,
|
||||
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
import { ScrollWrapperComponentInstanceContext } from '@/ui/utilities/scroll/states/contexts/ScrollWrapperComponentInstanceContext';
|
||||
import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState';
|
||||
import { createComponentStateV2 } from '@/ui/utilities/state/jotai/utils/createComponentStateV2';
|
||||
|
||||
export const scrollWrapperScrollLeftComponentState =
|
||||
createComponentState<number>({
|
||||
createComponentStateV2<number>({
|
||||
key: 'scrollWrapperScrollLeftComponentState',
|
||||
defaultValue: 0,
|
||||
componentInstanceContext: ScrollWrapperComponentInstanceContext,
|
||||
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
import { ScrollWrapperComponentInstanceContext } from '@/ui/utilities/scroll/states/contexts/ScrollWrapperComponentInstanceContext';
|
||||
import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState';
|
||||
import { createComponentStateV2 } from '@/ui/utilities/state/jotai/utils/createComponentStateV2';
|
||||
|
||||
export const scrollWrapperScrollTopComponentState =
|
||||
createComponentState<number>({
|
||||
createComponentStateV2<number>({
|
||||
key: 'scrollWrapperScrollTopComponentState',
|
||||
defaultValue: 0,
|
||||
componentInstanceContext: ScrollWrapperComponentInstanceContext,
|
||||
|
||||
@@ -0,0 +1,427 @@
|
||||
# Recoil → Jotai Migration Plan
|
||||
|
||||
## Overview
|
||||
|
||||
- **429 state files** + **69 selector files** = ~500 total Recoil-based state definitions
|
||||
- **5 already migrated** (dropdown component states)
|
||||
- Organized into **14 PRs** by module area, starting from lowest-level (fewest dependencies) to highest-level
|
||||
|
||||
## State Creation Patterns (Recoil → Jotai)
|
||||
|
||||
| Recoil Pattern | Count | Jotai Equivalent |
|
||||
|---|---|---|
|
||||
| `createState()` | ~100 global states | `createStateV2()` → Jotai `atom()` |
|
||||
| `createComponentState()` | ~200 component states | `createComponentStateV2()` → Jotai `atom()` with instance cache |
|
||||
| `createComponentFamilyState()` | ~27 component family states | New `createComponentFamilyStateV2()` → Jotai `atom()` with two-level cache |
|
||||
| `createComponentSelector()` | ~28 component selectors | New `createComponentSelectorV2()` → Jotai derived `atom()` |
|
||||
| `createComponentFamilySelector()` | ~9 component family selectors | New `createComponentFamilySelectorV2()` |
|
||||
| Raw `atom()` from recoil | 3 | Jotai `atom()` via `createStateV2()` |
|
||||
| Raw `atomFamily()` from recoil | 3 | Jotai `atom()` via `createFamilyStateV2()` |
|
||||
| Raw `selector()` from recoil | 11 | Jotai derived `atom()` via `createSelectorV2()` |
|
||||
| Raw `selectorFamily()` from recoil | ~21 | Jotai derived `atom()` via family pattern |
|
||||
|
||||
## Hook Migration
|
||||
|
||||
| Recoil Hook | Jotai Equivalent |
|
||||
|---|---|
|
||||
| `useRecoilValue()` | `useRecoilValueV2()` (already exists, wraps `useAtomValue`) |
|
||||
| `useRecoilState()` | `useRecoilStateV2()` (already exists, wraps `useAtom`) |
|
||||
| `useSetRecoilState()` | `useSetRecoilStateV2()` (already exists, wraps `useSetAtom`) |
|
||||
| `useResetRecoilState()` | `jotaiStore.set(atom, defaultValue)` or new `useResetAtom` wrapper |
|
||||
| `useRecoilComponentValue()` | `useRecoilComponentValueV2()` (already exists) |
|
||||
| `useSetRecoilComponentState()` | `useSetRecoilComponentStateV2()` (already exists) |
|
||||
| `useRecoilComponentState()` | `useRecoilComponentStateV2()` (already exists) |
|
||||
| `useRecoilCallback()` | `useCallback()` + `jotaiStore.get/set` |
|
||||
|
||||
## Infrastructure Needed Before Migration
|
||||
|
||||
Before starting the PRs below, these Jotai V2 utilities need to be created (if not already):
|
||||
|
||||
- [x] `createStateV2()` — global atom
|
||||
- [x] `createFamilyStateV2()` — global atom family
|
||||
- [x] `createSelectorV2()` — global derived atom
|
||||
- [x] `createComponentStateV2()` — instance-scoped atom
|
||||
- [ ] `createComponentFamilyStateV2()` — instance-scoped atom family
|
||||
- [ ] `createComponentSelectorV2()` — instance-scoped derived atom
|
||||
- [ ] `createComponentFamilySelectorV2()` — instance-scoped derived atom family
|
||||
- [ ] `createFamilySelectorV2()` — global derived atom family (for raw selectorFamily replacements)
|
||||
|
||||
---
|
||||
|
||||
## PR 1: UI Utilities — Focus, Hotkeys, Loading, Pointer Events, Scroll
|
||||
|
||||
Low-level utilities used across the entire app. No dependencies on other app states.
|
||||
|
||||
### States (16 files)
|
||||
```
|
||||
ui/utilities/focus/states/focusStackState.ts
|
||||
ui/utilities/hotkey/states/internal/pendingHotkeysState.ts
|
||||
ui/utilities/loading-state/states/currentPageLocationState.ts
|
||||
ui/utilities/pointer-event/states/clickOutsideListenerIsActivatedComponentState.ts
|
||||
ui/utilities/pointer-event/states/clickOutsideListenerIsMouseDownInsideComponentState.ts
|
||||
ui/utilities/pointer-event/states/clickOutsideListenerMouseDownHappenedComponentState.ts
|
||||
ui/utilities/scroll/states/scrollWrapperScrollBottomComponentState.ts
|
||||
ui/utilities/scroll/states/scrollWrapperScrollLeftComponentState.ts
|
||||
ui/utilities/scroll/states/scrollWrapperScrollTopComponentState.ts
|
||||
```
|
||||
|
||||
### Selectors (3 files)
|
||||
```
|
||||
ui/utilities/focus/states/currentFocusIdSelector.ts
|
||||
ui/utilities/focus/states/currentFocusedItemSelector.ts
|
||||
ui/utilities/focus/states/currentGlobalHotkeysConfigSelector.ts
|
||||
```
|
||||
|
||||
### Notes
|
||||
- `focusStackState` is used by the dropdown hooks — needs to be migrated to unblock removing `useRecoilCallback` from dropdown hooks
|
||||
- Focus selectors depend on `focusStackState`
|
||||
|
||||
---
|
||||
|
||||
## PR 2: UI Layout — Selectable List, Modal, Tab List, Table
|
||||
|
||||
Core layout primitives used by record-table, record-board, and many other modules.
|
||||
|
||||
### States (7 files)
|
||||
```
|
||||
ui/layout/selectable-list/states/isSelectedItemIdComponentFamilyState.ts
|
||||
ui/layout/selectable-list/states/selectableItemIdsComponentState.ts
|
||||
ui/layout/selectable-list/states/selectedItemIdComponentState.ts
|
||||
ui/layout/modal/states/isModalOpenedComponentState.ts
|
||||
ui/layout/tab-list/states/activeTabIdComponentState.ts
|
||||
ui/layout/table/states/sortedFieldByTableFamilyState.ts
|
||||
```
|
||||
|
||||
### Selectors (1 file)
|
||||
```
|
||||
ui/layout/selectable-list/states/selectors/isSelectedItemIdComponentFamilySelector.ts
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## PR 3: UI Navigation, Theme, Input, Feedback, Field
|
||||
|
||||
### States (12 files)
|
||||
```
|
||||
ui/navigation/navigation-drawer/states/isAdvancedModeEnabledState.ts
|
||||
ui/navigation/navigation-drawer/states/isNavigationSectionOpenFamilyState.ts
|
||||
ui/navigation/states/isNavigationDrawerExpanded.ts (raw Recoil atom)
|
||||
ui/navigation/states/navigationDrawerWidthState.ts
|
||||
ui/navigation/states/navigationMemorizedUrlState.ts
|
||||
ui/navigation/states/shouldNavigateBackToMemorizedUrlOnSaveState.ts
|
||||
ui/navigation/step-bar/states/stepBarInternalState.ts
|
||||
ui/theme/states/persistedColorSchemeState.ts
|
||||
ui/input/states/iconPickerState.ts
|
||||
ui/input/states/iconPickerVisibleCountState.ts
|
||||
ui/feedback/dialog-manager/states/dialogInternalComponentState.ts
|
||||
ui/feedback/snack-bar-manager/states/snackBarInternalComponentState.ts
|
||||
ui/field/display/states/filePreviewState.ts
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## PR 4: Auth, Domain Manager, App, Captcha, Chrome Extension
|
||||
|
||||
Core authentication and app-level states. Very widely imported (~195 external imports for auth).
|
||||
|
||||
### States (25 files)
|
||||
```
|
||||
auth/states/availableWorkspacesState.ts
|
||||
auth/states/billingCheckoutSessionState.ts
|
||||
auth/states/currentUserState.ts
|
||||
auth/states/currentUserWorkspaceState.ts
|
||||
auth/states/currentWorkspaceDeletedMembersState.ts
|
||||
auth/states/currentWorkspaceMemberState.ts
|
||||
auth/states/currentWorkspaceMembersState.ts
|
||||
auth/states/currentWorkspaceState.ts
|
||||
auth/states/isCurrentUserLoadedState.ts
|
||||
auth/states/lastAuthenticatedMethodState.ts
|
||||
auth/states/loginTokenState.ts
|
||||
auth/states/previousUrlState.ts
|
||||
auth/states/signInUpModeState.ts
|
||||
auth/states/signInUpStepState.ts
|
||||
auth/states/tokenPairState.ts
|
||||
auth/states/workspacePublicDataState.ts
|
||||
domain-manager/states/domainConfigurationState.ts
|
||||
domain-manager/states/lastAuthenticatedWorkspaceDomainState.ts
|
||||
app/states/isAppEffectRedirectEnabledState.ts
|
||||
app/states/verifyEmailRedirectPathState.ts
|
||||
captcha/states/captchaTokenState.ts
|
||||
captcha/states/isCaptchaScriptLoadedState.ts
|
||||
captcha/states/isRequestingCaptchaTokenState.ts
|
||||
chrome-extension-sidecar/states/isLoadingTokensFromExtensionState.ts
|
||||
workspace/states/workspaceAuthBypassProvidersState.ts
|
||||
workspace/states/workspaceAuthProvidersState.ts
|
||||
workspace/states/workspaceBypassModeState.ts
|
||||
```
|
||||
|
||||
### Selectors (2 files)
|
||||
```
|
||||
auth/states/isImpersonatingState.ts (raw Recoil selector)
|
||||
auth/states/objectPermissionsFamilySelector.ts
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## PR 5: Client Config
|
||||
|
||||
26 simple global states, mostly boolean feature flags. Low risk.
|
||||
|
||||
### States (26 files)
|
||||
```
|
||||
client-config/states/aiModelsState.ts
|
||||
client-config/states/apiConfigState.ts
|
||||
client-config/states/appVersionState.ts
|
||||
client-config/states/authProvidersState.ts
|
||||
client-config/states/billingState.ts
|
||||
client-config/states/calendarBookingPageIdState.ts
|
||||
client-config/states/canManageFeatureFlagsState.ts
|
||||
client-config/states/captchaState.ts
|
||||
client-config/states/chromeExtensionIdState.ts
|
||||
client-config/states/clientConfigApiStatusState.ts
|
||||
client-config/states/isAnalyticsEnabledState.ts
|
||||
client-config/states/isAttachmentPreviewEnabledState.ts
|
||||
client-config/states/isClickHouseConfiguredState.ts
|
||||
client-config/states/isCloudflareIntegrationEnabledState.ts
|
||||
client-config/states/isConfigVariablesInDbEnabledState.ts
|
||||
client-config/states/isDeveloperDefaultSignInPrefilledState.ts
|
||||
client-config/states/isEmailVerificationRequiredState.ts
|
||||
client-config/states/isEmailingDomainsEnabledState.ts
|
||||
client-config/states/isGoogleCalendarEnabledState.ts
|
||||
client-config/states/isGoogleMessagingEnabledState.ts
|
||||
client-config/states/isImapSmtpCaldavEnabledState.ts
|
||||
client-config/states/isMicrosoftCalendarEnabledState.ts
|
||||
client-config/states/isMicrosoftMessagingEnabledState.ts
|
||||
client-config/states/isMultiWorkspaceEnabledState.ts
|
||||
client-config/states/sentryConfigState.ts
|
||||
client-config/states/supportChatState.ts
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## PR 6: Object Metadata
|
||||
|
||||
Core metadata states used by record-table, record-board, views, etc.
|
||||
|
||||
### States (3 files)
|
||||
```
|
||||
object-metadata/states/lastFieldMetadataItemUpdateState.ts
|
||||
object-metadata/states/objectMetadataItemsState.ts
|
||||
object-metadata/states/shouldAppBeLoadingState.ts
|
||||
```
|
||||
|
||||
### Selectors (12 files)
|
||||
```
|
||||
object-metadata/states/availableFieldMetadataItemsForFilterFamilySelector.ts
|
||||
object-metadata/states/availableFieldMetadataItemsForSortFamilySelector.ts
|
||||
object-metadata/states/fieldMetadataItemByIdSelector.ts
|
||||
object-metadata/states/flattenedFieldMetadataItemsSelector.ts
|
||||
object-metadata/states/flattenedReadableFieldMetadataItemIdsSelector.ts
|
||||
object-metadata/states/isFieldMetadataItemFilterableAndSortableSelector.ts
|
||||
object-metadata/states/isFieldMetadataItemLabelIdentifierSelector.ts
|
||||
object-metadata/states/labelIdentifierFieldMetadataItemSelector.ts
|
||||
object-metadata/states/objectMetadataItemFamilySelector.ts
|
||||
object-metadata/states/objectMetadataItemsByNamePluralMapSelector.ts
|
||||
object-metadata/states/objectMetadataItemsByNameSingularMapSelector.ts
|
||||
object-metadata/states/objectMetadataItemsBySingularNameSelector.ts
|
||||
```
|
||||
|
||||
### Notes
|
||||
- Object metadata selectors are raw `selectorFamily()` from Recoil — needs `createFamilySelectorV2()` utility
|
||||
- Very widely depended on (~135 external imports)
|
||||
|
||||
---
|
||||
|
||||
## PR 7: Object Record — Record Store
|
||||
|
||||
The central record store, depended on by almost everything (~190 external imports).
|
||||
|
||||
### States (2 files)
|
||||
```
|
||||
object-record/states/recordStoreFamilyState.ts (raw atomFamily)
|
||||
object-record/record-store/states/recordStoreFamilyState.ts
|
||||
```
|
||||
|
||||
### Selectors (6 files)
|
||||
```
|
||||
object-record/record-store/states/selectors/recordStoreFamilySelector.ts
|
||||
object-record/record-store/states/selectors/recordStoreFieldValueSelector.ts
|
||||
object-record/record-store/states/selectors/recordStoreIdentifierSelector.ts
|
||||
object-record/record-store/states/selectors/recordStoreIdentifiersSelector.ts
|
||||
object-record/record-store/states/selectors/recordStoreMorphOneToManyValueWithObjectNameFamilySelector.ts
|
||||
object-record/record-store/states/selectors/recordStoreRecordsSelector.ts
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## PR 8: Object Record — Filters, Sorts, Groups, Drag, Field, Calendar, Board, Picker
|
||||
|
||||
### States (~80 files)
|
||||
```
|
||||
object-record/object-filter-dropdown/states/ (12 files)
|
||||
object-record/object-sort-dropdown/states/ (3 files)
|
||||
object-record/record-filter/states/ (2 files)
|
||||
object-record/record-filter-group/states/ (1 file)
|
||||
object-record/record-sort/states/ (1 file)
|
||||
object-record/record-group/states/ (3 files)
|
||||
object-record/record-drag/states/ (7 files)
|
||||
object-record/record-field/states/ (7 files)
|
||||
object-record/record-field-list/states/ (2 files)
|
||||
object-record/record-calendar/states/ (6 files)
|
||||
object-record/record-board/states/ (18 files)
|
||||
object-record/record-picker/states/ (15 files)
|
||||
object-record/record-right-drawer/states/ (2 files)
|
||||
object-record/record-merge/states/ (2 files)
|
||||
object-record/record-title-cell/states/ (1 file)
|
||||
object-record/advanced-filter/states/ (1 selector)
|
||||
```
|
||||
|
||||
### Selectors (~20 files)
|
||||
```
|
||||
object-record/record-group/states/selectors/ (6 files)
|
||||
object-record/record-index/states/selectors/ (5 files)
|
||||
object-record/record-board/states/selectors/ (2 files)
|
||||
object-record/record-calendar/states/selectors/ (2 files)
|
||||
object-record/record-picker/states/selectors/ (5 files)
|
||||
object-record/record-field/states/ (1 selector)
|
||||
object-record/object-filter-dropdown/states/ (1 selector)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## PR 9: Object Record — Record Table
|
||||
|
||||
Largest single module (40 states + 10 selectors).
|
||||
|
||||
### States (~40 files)
|
||||
```
|
||||
object-record/record-table/states/ (all component states)
|
||||
object-record/record-table/record-table-row/states/
|
||||
object-record/record-table/record-table-footer/states/
|
||||
object-record/record-table/record-table-section/states/
|
||||
object-record/record-table/virtualization/states/
|
||||
```
|
||||
|
||||
### Selectors (~10 files)
|
||||
```
|
||||
object-record/record-table/states/selectors/
|
||||
object-record/record-table/record-table-row/states/
|
||||
object-record/record-table/virtualization/states/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## PR 10: Object Record — Record Index
|
||||
|
||||
### States (~17 files)
|
||||
```
|
||||
object-record/record-index/states/ (all files)
|
||||
```
|
||||
|
||||
### Selectors (~5 files)
|
||||
```
|
||||
object-record/record-index/states/selectors/ (all files)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## PR 11: Views, Favorites, Navigation, Prefetch, Localization
|
||||
|
||||
### States (~30 files)
|
||||
```
|
||||
views/states/ (10 files)
|
||||
views/view-picker/states/ (13 files)
|
||||
favorites/favorite-folder-picker/states/ (4 files)
|
||||
navigation/states/ (3 files)
|
||||
prefetch/states/ (4 files)
|
||||
localization/states/ (1 file)
|
||||
information-banner/states/ (1 file)
|
||||
```
|
||||
|
||||
### Selectors (~8 files)
|
||||
```
|
||||
views/states/selectors/ (6 files)
|
||||
favorites/states/selectors/ (1 file)
|
||||
favorites/favorite-folder-picker/states/selectors/ (1 file)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## PR 12: Context Store, Command Menu, Action Menu
|
||||
|
||||
### States (~22 files)
|
||||
```
|
||||
context-store/states/ (10 files)
|
||||
command-menu/states/ (10 files)
|
||||
command-menu/pages/*/states/ (10 files)
|
||||
action-menu/states/ (1 file)
|
||||
action-menu/actions/states/ (1 file)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## PR 13: Workflow, Page Layout, AI, Activities
|
||||
|
||||
### States (~45 files)
|
||||
```
|
||||
workflow/states/ (6 files)
|
||||
workflow/workflow-diagram/states/ (12 files)
|
||||
workflow/workflow-steps/states/ (7 files)
|
||||
workflow/workflow-variables/states/ (2 files)
|
||||
page-layout/states/ (17 files)
|
||||
page-layout/widgets/states/ (10 files)
|
||||
ai/states/ (2 files)
|
||||
activities/states/ (5 files)
|
||||
blocknote-editor/states/ (1 file)
|
||||
```
|
||||
|
||||
### Selectors (~3 files)
|
||||
```
|
||||
workflow/states/selectors/ (2 files)
|
||||
workflow/workflow-steps/filters/states/ (1 selector)
|
||||
page-layout/states/selectors/ (1 file)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## PR 14: Settings, SSE, Spreadsheet Import, Misc
|
||||
|
||||
Leaf modules with few external consumers.
|
||||
|
||||
### States (~30 files)
|
||||
```
|
||||
settings/admin-panel/ (4 files)
|
||||
settings/data-model/ (2 files)
|
||||
settings/developers/ (1 file)
|
||||
settings/domains/ (3 files)
|
||||
settings/logic-functions/ (1 file)
|
||||
settings/playground/ (1 file)
|
||||
settings/roles/ (4 files)
|
||||
settings/security/ (2 files)
|
||||
sse-db-event/states/ (9 files)
|
||||
spreadsheet-import/states/ (4 files)
|
||||
```
|
||||
|
||||
### Selectors (1 file)
|
||||
```
|
||||
settings/roles/states/settingsAllRolesSelector.ts
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
Each PR should:
|
||||
1. Migrate state definitions (swap `createState` → `createStateV2`, etc.)
|
||||
2. Update all consumer hooks (swap `useRecoilValue` → `useRecoilValueV2`, etc.)
|
||||
3. Update `useRecoilCallback` → `useCallback` + `jotaiStore.get/set`
|
||||
4. Run `npx nx typecheck twenty-front`
|
||||
5. Run `npx nx test twenty-front` for affected test files
|
||||
6. Verify affected Storybook stories
|
||||
|
||||
## Notes
|
||||
|
||||
- The `beforeEach: resetJotaiStore()` in `.storybook/preview.tsx` handles Storybook isolation globally
|
||||
- Unit tests need `JotaiProvider store={jotaiStore}` in their wrappers and `beforeEach` resets for affected atoms
|
||||
- Some states use `effects` (e.g., `cookieStorageEffect`, `localStorageEffect`) — these need Jotai `onMount` equivalents
|
||||
- States with `useResetRecoilState` need a wrapper or direct `jotaiStore.set(atom, defaultValue)`
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
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 ComponentFamilyStateV2 } from '@/ui/utilities/state/jotai/types/ComponentFamilyStateV2';
|
||||
|
||||
export const useRecoilComponentFamilyStateV2 = <StateType, FamilyKey>(
|
||||
componentState: ComponentFamilyStateV2<StateType, FamilyKey>,
|
||||
familyKey: FamilyKey,
|
||||
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, familyKey }));
|
||||
};
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
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 ComponentFamilyStateV2 } from '@/ui/utilities/state/jotai/types/ComponentFamilyStateV2';
|
||||
|
||||
export const useRecoilComponentFamilyValueV2 = <StateType, FamilyKey>(
|
||||
componentState: ComponentFamilyStateV2<StateType, FamilyKey>,
|
||||
familyKey: FamilyKey,
|
||||
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, familyKey }));
|
||||
};
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
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 ComponentFamilyStateV2 } from '@/ui/utilities/state/jotai/types/ComponentFamilyStateV2';
|
||||
|
||||
export const useSetRecoilComponentFamilyStateV2 = <ValueType, FamilyKey>(
|
||||
componentState: ComponentFamilyStateV2<ValueType, FamilyKey>,
|
||||
familyKey: FamilyKey,
|
||||
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, familyKey }));
|
||||
};
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
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 ComponentFamilyStateKey<FamilyKey> = ComponentStateKey & {
|
||||
familyKey: FamilyKey;
|
||||
};
|
||||
|
||||
export type ComponentFamilyStateV2<ValueType, FamilyKey> = {
|
||||
type: 'ComponentFamilyStateV2';
|
||||
key: string;
|
||||
atomFamily: (
|
||||
key: ComponentFamilyStateKey<FamilyKey>,
|
||||
) => JotaiWritableAtom<ValueType>;
|
||||
};
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
import { atom } from 'jotai';
|
||||
|
||||
import { type ComponentInstanceStateContext } from '@/ui/utilities/state/component-state/types/ComponentInstanceStateContext';
|
||||
import { globalComponentInstanceContextMap } from '@/ui/utilities/state/component-state/utils/globalComponentInstanceContextMap';
|
||||
import {
|
||||
type ComponentFamilyStateKey,
|
||||
type ComponentFamilyStateV2,
|
||||
} from '@/ui/utilities/state/jotai/types/ComponentFamilyStateV2';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const createComponentFamilyStateV2 = <ValueType, FamilyKey>({
|
||||
key,
|
||||
defaultValue,
|
||||
componentInstanceContext,
|
||||
}: {
|
||||
key: string;
|
||||
defaultValue: ValueType;
|
||||
componentInstanceContext: ComponentInstanceStateContext<any> | null;
|
||||
}): ComponentFamilyStateV2<ValueType, FamilyKey> => {
|
||||
if (isDefined(componentInstanceContext)) {
|
||||
globalComponentInstanceContextMap.set(key, componentInstanceContext);
|
||||
}
|
||||
|
||||
const atomCache = new Map<
|
||||
string,
|
||||
ReturnType<ComponentFamilyStateV2<ValueType, FamilyKey>['atomFamily']>
|
||||
>();
|
||||
|
||||
const familyFunction = ({
|
||||
instanceId,
|
||||
familyKey,
|
||||
}: ComponentFamilyStateKey<FamilyKey>): ReturnType<
|
||||
ComponentFamilyStateV2<ValueType, FamilyKey>['atomFamily']
|
||||
> => {
|
||||
const familyKeyStr =
|
||||
typeof familyKey === 'string' ? familyKey : JSON.stringify(familyKey);
|
||||
|
||||
const cacheKey = `${instanceId}__${familyKeyStr}`;
|
||||
const existing = atomCache.get(cacheKey);
|
||||
|
||||
if (existing !== undefined) {
|
||||
return existing;
|
||||
}
|
||||
|
||||
const baseAtom = atom(defaultValue);
|
||||
baseAtom.debugLabel = `${key}__${cacheKey}`;
|
||||
atomCache.set(cacheKey, baseAtom);
|
||||
|
||||
return baseAtom;
|
||||
};
|
||||
|
||||
return {
|
||||
type: 'ComponentFamilyStateV2',
|
||||
key,
|
||||
atomFamily: familyFunction,
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user