CreateComponentFamilyState -> createComponentFamilyStateV2 (#11546)

Refacto of createComponentFamilyState

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
Guillim
2025-04-14 10:31:30 +02:00
committed by GitHub
parent 27f542e132
commit 0de8140b3a
36 changed files with 311 additions and 299 deletions

View File

@ -1,7 +1,7 @@
import { ReactNode, useEffect, useRef } from 'react';
import { useRecoilValue } from 'recoil';
import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList';
import { isSelectedItemIdComponentFamilySelector } from '@/ui/layout/selectable-list/states/selectors/isSelectedItemIdComponentFamilySelector';
import { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValueV2';
import styled from '@emotion/styled';
const StyledContainer = styled.div`
@ -20,9 +20,10 @@ export const SelectableItem = ({
children,
className,
}: SelectableItemProps) => {
const { isSelectedItemIdSelector } = useSelectableList();
const isSelectedItemId = useRecoilValue(isSelectedItemIdSelector(itemId));
const isSelectedItemId = useRecoilComponentFamilyValueV2(
isSelectedItemIdComponentFamilySelector,
itemId,
);
const scrollRef = useRef<HTMLDivElement>(null);

View File

@ -1,34 +1,43 @@
import { ReactNode, useEffect } from 'react';
import { useSelectableListHotKeys } from '@/ui/layout/selectable-list/hooks/internal/useSelectableListHotKeys';
import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList';
import { SelectableListScope } from '@/ui/layout/selectable-list/scopes/SelectableListScope';
import { arrayToChunks } from '~/utils/array/arrayToChunks';
import { SelectableListComponentInstanceContext } from '@/ui/layout/selectable-list/states/contexts/SelectableListComponentInstanceContext';
import { selectableItemIdsComponentState } from '@/ui/layout/selectable-list/states/selectableItemIdsComponentState';
import { selectableListOnEnterComponentState } from '@/ui/layout/selectable-list/states/selectableListOnEnterComponentState';
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
import { isDefined } from 'twenty-shared/utils';
import { arrayToChunks } from '~/utils/array/arrayToChunks';
type SelectableListProps = {
children: ReactNode;
selectableListId: string;
selectableItemIdArray?: string[];
selectableItemIdMatrix?: string[][];
onSelect?: (selected: string) => void;
hotkeyScope: string;
onEnter?: (itemId: string) => void;
selectableListInstanceId: string;
};
export const SelectableList = ({
children,
selectableListId,
hotkeyScope,
selectableItemIdArray,
selectableItemIdMatrix,
selectableListInstanceId,
onEnter,
onSelect,
}: SelectableListProps) => {
useSelectableListHotKeys(selectableListId, hotkeyScope, onSelect);
useSelectableListHotKeys(selectableListInstanceId, hotkeyScope, onSelect);
const { setSelectableItemIds, setSelectableListOnEnter, setSelectedItemId } =
useSelectableList(selectableListId);
const setSelectableListOnEnter = useSetRecoilComponentStateV2(
selectableListOnEnterComponentState,
selectableListInstanceId,
);
const setSelectableItemIds = useSetRecoilComponentStateV2(
selectableItemIdsComponentState,
selectableListInstanceId,
);
useEffect(() => {
setSelectableListOnEnter(() => onEnter);
@ -48,16 +57,15 @@ export const SelectableList = ({
if (isDefined(selectableItemIdArray)) {
setSelectableItemIds(arrayToChunks(selectableItemIdArray, 1));
}
}, [
selectableItemIdArray,
selectableItemIdMatrix,
setSelectableItemIds,
setSelectedItemId,
]);
}, [selectableItemIdArray, selectableItemIdMatrix, setSelectableItemIds]);
return (
<SelectableListScope selectableListScopeId={selectableListId}>
<SelectableListComponentInstanceContext.Provider
value={{
instanceId: selectableListInstanceId,
}}
>
{children}
</SelectableListScope>
</SelectableListComponentInstanceContext.Provider>
);
};

View File

@ -1,9 +1,12 @@
import { act } from 'react-dom/test-utils';
import { renderHook } from '@testing-library/react';
import { RecoilRoot, useRecoilState, useRecoilValue } from 'recoil';
import { act } from 'react-dom/test-utils';
import { RecoilRoot } from 'recoil';
import { useSelectableListStates } from '@/ui/layout/selectable-list/hooks/internal/useSelectableListStates';
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 { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
const selectableListScopeId = 'testId';
const testArr = [['1'], ['2'], ['3']];
@ -13,15 +16,15 @@ describe('useSelectableList', () => {
it('Should setSelectableItemIds', async () => {
const { result } = renderHook(
() => {
const { setSelectableItemIds } = useSelectableList(
const setSelectableItemIds = useSetRecoilComponentStateV2(
selectableItemIdsComponentState,
selectableListScopeId,
);
const { selectableItemIdsState } = useSelectableListStates({
const selectableItemIds = useRecoilComponentValueV2(
selectableItemIdsComponentState,
selectableListScopeId,
});
const selectableItemIds = useRecoilValue(selectableItemIdsState);
);
return {
setSelectableItemIds,
@ -47,13 +50,14 @@ describe('useSelectableList', () => {
() => {
const { resetSelectedItem } = useSelectableList(selectableListScopeId);
const { selectedItemIdState } = useSelectableListStates({
const selectedItemId = useRecoilComponentValueV2(
selectedItemIdComponentState,
selectableListScopeId,
});
const [selectedItemId, setSelectedItemId] =
useRecoilState(selectedItemIdState);
);
const setSelectedItemId = useSetRecoilComponentStateV2(
selectedItemIdComponentState,
selectableListScopeId,
);
return {
resetSelectedItem,
selectedItemId,

View File

@ -2,14 +2,17 @@ import { isNonEmptyString } from '@sniptt/guards';
import { useRecoilCallback } from 'recoil';
import { Key } from 'ts-key-enum';
import { useSelectableListStates } from '@/ui/layout/selectable-list/hooks/internal/useSelectableListStates';
import { selectableItemIdsComponentState } from '@/ui/layout/selectable-list/states/selectableItemIdsComponentState';
import { selectableListOnEnterComponentState } from '@/ui/layout/selectable-list/states/selectableListOnEnterComponentState';
import { selectedItemIdComponentState } from '@/ui/layout/selectable-list/states/selectedItemIdComponentState';
import { isSelectedItemIdComponentFamilySelector } from '@/ui/layout/selectable-list/states/selectors/isSelectedItemIdComponentFamilySelector';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue';
type Direction = 'up' | 'down' | 'left' | 'right';
export const useSelectableListHotKeys = (
scopeId: string,
instanceId: string,
hotkeyScope: string,
onSelect?: (itemId: string) => void,
) => {
@ -29,22 +32,20 @@ export const useSelectableListHotKeys = (
}
};
const {
selectedItemIdState,
selectableItemIdsState,
isSelectedItemIdSelector,
selectableListOnEnterState,
} = useSelectableListStates({
selectableListScopeId: scopeId,
});
const handleSelect = useRecoilCallback(
({ snapshot, set }) =>
(direction: Direction) => {
const selectedItemId = getSnapshotValue(snapshot, selectedItemIdState);
const selectedItemId = getSnapshotValue(
snapshot,
selectedItemIdComponentState.atomFamily({
instanceId: instanceId,
}),
);
const selectableItemIds = getSnapshotValue(
snapshot,
selectableItemIdsState,
selectableItemIdsComponentState.atomFamily({
instanceId: instanceId,
}),
);
const currentPosition = findPosition(selectableItemIds, selectedItemId);
@ -104,22 +105,34 @@ export const useSelectableListHotKeys = (
if (selectedItemId !== nextId) {
if (isNonEmptyString(nextId)) {
set(isSelectedItemIdSelector(nextId), true);
set(selectedItemIdState, nextId);
set(
isSelectedItemIdComponentFamilySelector.selectorFamily({
instanceId: instanceId,
familyKey: nextId,
}),
true,
);
set(
selectedItemIdComponentState.atomFamily({
instanceId: instanceId,
}),
nextId,
);
onSelect?.(nextId);
}
if (isNonEmptyString(selectedItemId)) {
set(isSelectedItemIdSelector(selectedItemId), false);
set(
isSelectedItemIdComponentFamilySelector.selectorFamily({
instanceId: instanceId,
familyKey: selectedItemId,
}),
false,
);
}
}
},
[
isSelectedItemIdSelector,
onSelect,
selectableItemIdsState,
selectedItemIdState,
],
[instanceId, onSelect],
);
useScopedHotkeys(Key.ArrowUp, () => handleSelect('up'), hotkeyScope, []);
@ -142,18 +155,22 @@ export const useSelectableListHotKeys = (
() => {
const selectedItemId = getSnapshotValue(
snapshot,
selectedItemIdState,
selectedItemIdComponentState.atomFamily({
instanceId: instanceId,
}),
);
const onEnter = getSnapshotValue(
snapshot,
selectableListOnEnterState,
selectableListOnEnterComponentState.atomFamily({
instanceId: instanceId,
}),
);
if (isNonEmptyString(selectedItemId)) {
onEnter?.(selectedItemId);
}
},
[selectableListOnEnterState, selectedItemIdState],
[instanceId],
),
hotkeyScope,
[],

View File

@ -1,41 +0,0 @@
import { SelectableListScopeInternalContext } from '@/ui/layout/selectable-list/scopes/scope-internal-context/SelectableListScopeInternalContext';
import { selectableItemIdsComponentState } from '@/ui/layout/selectable-list/states/selectableItemIdsComponentState';
import { selectableListOnEnterComponentState } from '@/ui/layout/selectable-list/states/selectableListOnEnterComponentState';
import { selectedItemIdComponentState } from '@/ui/layout/selectable-list/states/selectedItemIdComponentState';
import { isSelectedItemIdFamilySelector } from '@/ui/layout/selectable-list/states/selectors/isSelectedItemIdFamilySelector';
import { useAvailableScopeIdOrThrow } from '@/ui/utilities/recoil-scope/scopes-internal/hooks/useAvailableScopeId';
import { extractComponentFamilyState } from '@/ui/utilities/state/component-state/utils/extractComponentFamilyState';
import { extractComponentState } from '@/ui/utilities/state/component-state/utils/extractComponentState';
type useSelectableListStatesProps = {
selectableListScopeId?: string;
};
export const useSelectableListStates = ({
selectableListScopeId,
}: useSelectableListStatesProps) => {
const scopeId = useAvailableScopeIdOrThrow(
SelectableListScopeInternalContext,
selectableListScopeId,
);
return {
scopeId,
isSelectedItemIdSelector: extractComponentFamilyState(
isSelectedItemIdFamilySelector,
scopeId,
),
selectableItemIdsState: extractComponentState(
selectableItemIdsComponentState,
scopeId,
),
selectableListOnEnterState: extractComponentState(
selectableListOnEnterComponentState,
scopeId,
),
selectedItemIdState: extractComponentState(
selectedItemIdComponentState,
scopeId,
),
};
};

View File

@ -1,4 +1,4 @@
import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList';
import { selectedItemIdComponentState } from '@/ui/layout/selectable-list/states/selectedItemIdComponentState';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue';
import { isNonEmptyString } from '@sniptt/guards';
@ -7,15 +7,15 @@ import { Key } from 'ts-key-enum';
export const useListenToEnterHotkeyOnListItem = ({
hotkeyScope,
itemId,
onEnter,
}: {
hotkeyScope: string;
itemId: string;
onEnter: () => void;
}) => {
const { selectedItemIdState } = useSelectableList();
useScopedHotkeys(
Key.Enter,
useRecoilCallback(
@ -23,17 +23,19 @@ export const useListenToEnterHotkeyOnListItem = ({
() => {
const selectedItemId = getSnapshotValue(
snapshot,
selectedItemIdState,
selectedItemIdComponentState.atomFamily({
instanceId: itemId,
}),
);
if (isNonEmptyString(selectedItemId) && selectedItemId === itemId) {
onEnter?.();
}
},
[itemId, onEnter, selectedItemIdState],
[itemId, onEnter],
),
hotkeyScope,
[selectedItemIdState, itemId, onEnter],
[itemId, onEnter],
{
preventDefault: false,
},

View File

@ -1,60 +1,85 @@
import { useRecoilCallback, useSetRecoilState } from 'recoil';
import { useRecoilCallback } from 'recoil';
import { useSelectableListStates } from '@/ui/layout/selectable-list/hooks/internal/useSelectableListStates';
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 { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { isDefined } from 'twenty-shared/utils';
export const useSelectableList = (selectableListId?: string) => {
const {
scopeId,
selectableItemIdsState,
selectableListOnEnterState,
isSelectedItemIdSelector,
selectedItemIdState,
} = useSelectableListStates({
selectableListScopeId: selectableListId,
});
const setSelectableItemIds = useSetRecoilState(selectableItemIdsState);
const setSelectableListOnEnter = useSetRecoilState(
selectableListOnEnterState,
export const useSelectableList = (instanceId?: string) => {
const selectableListInstanceId = useAvailableComponentInstanceIdOrThrow(
SelectableListComponentInstanceContext,
instanceId,
);
const resetSelectedItem = useRecoilCallback(
({ snapshot, set }) =>
() => {
const selectedItemId = getSnapshotValue(snapshot, selectedItemIdState);
const selectedItemId = getSnapshotValue(
snapshot,
selectedItemIdComponentState.atomFamily({
instanceId: selectableListInstanceId,
}),
);
if (isDefined(selectedItemId)) {
set(selectedItemIdState, null);
set(isSelectedItemIdSelector(selectedItemId), false);
set(
selectedItemIdComponentState.atomFamily({
instanceId: selectableListInstanceId,
}),
null,
);
set(
isSelectedItemIdComponentFamilySelector.selectorFamily({
instanceId: selectableListInstanceId,
familyKey: selectedItemId,
}),
false,
);
}
},
[selectedItemIdState, isSelectedItemIdSelector],
[selectableListInstanceId],
);
const setSelectedItemId = useRecoilCallback(
({ set, snapshot }) =>
(itemId: string) => {
const selectedItemId = getSnapshotValue(snapshot, selectedItemIdState);
const selectedItemId = getSnapshotValue(
snapshot,
selectedItemIdComponentState.atomFamily({
instanceId: selectableListInstanceId,
}),
);
if (isDefined(selectedItemId)) {
set(isSelectedItemIdSelector(selectedItemId), false);
set(
isSelectedItemIdComponentFamilySelector.selectorFamily({
instanceId: selectableListInstanceId,
familyKey: selectedItemId,
}),
false,
);
}
set(selectedItemIdState, itemId);
set(isSelectedItemIdSelector(itemId), true);
set(
selectedItemIdComponentState.atomFamily({
instanceId: selectableListInstanceId,
}),
itemId,
);
set(
isSelectedItemIdComponentFamilySelector.selectorFamily({
instanceId: selectableListInstanceId,
familyKey: itemId,
}),
true,
);
},
[selectedItemIdState, isSelectedItemIdSelector],
[selectableListInstanceId],
);
return {
selectableListId: scopeId,
setSelectableItemIds,
isSelectedItemIdSelector,
setSelectableListOnEnter,
resetSelectedItem,
setSelectedItemId,
selectedItemIdState,
};
};

View File

@ -1,21 +0,0 @@
import { ReactNode } from 'react';
import { SelectableListScopeInternalContext } from './scope-internal-context/SelectableListScopeInternalContext';
type SelectableListScopeProps = {
children: ReactNode;
selectableListScopeId: string;
};
export const SelectableListScope = ({
children,
selectableListScopeId,
}: SelectableListScopeProps) => {
return (
<SelectableListScopeInternalContext.Provider
value={{ scopeId: selectableListScopeId }}
>
{children}
</SelectableListScopeInternalContext.Provider>
);
};

View File

@ -1,7 +0,0 @@
import { createScopeInternalContext } from '@/ui/utilities/recoil-scope/scopes-internal/utils/createScopeInternalContext';
import { RecoilComponentStateKey } from '@/ui/utilities/state/component-state/types/RecoilComponentStateKey';
type SelectableListScopeInternalContextProps = RecoilComponentStateKey;
export const SelectableListScopeInternalContext =
createScopeInternalContext<SelectableListScopeInternalContextProps>();

View File

@ -0,0 +1,4 @@
import { createComponentInstanceContext } from '@/ui/utilities/state/component-state/utils/createComponentInstanceContext';
export const SelectableListComponentInstanceContext =
createComponentInstanceContext();

View File

@ -1,9 +1,9 @@
import { createComponentFamilyState } from '@/ui/utilities/state/component-state/utils/createComponentFamilyState';
import { SelectableListComponentInstanceContext } from '@/ui/layout/selectable-list/states/contexts/SelectableListComponentInstanceContext';
import { createComponentFamilyStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentFamilyStateV2';
export const isSelectedItemIdComponentFamilyState = createComponentFamilyState<
boolean,
string
>({
key: 'isSelectedItemIdComponentFamilyState',
defaultValue: false,
});
export const isSelectedItemIdComponentFamilyState =
createComponentFamilyStateV2<boolean, string>({
key: 'isSelectedItemIdComponentFamilyState',
defaultValue: false,
componentInstanceContext: SelectableListComponentInstanceContext,
});

View File

@ -1,8 +1,10 @@
import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState';
import { SelectableListComponentInstanceContext } from '@/ui/layout/selectable-list/states/contexts/SelectableListComponentInstanceContext';
import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2';
export const selectableItemIdsComponentState = createComponentState<string[][]>(
{
key: 'selectableItemIdsComponentState',
defaultValue: [[]],
},
);
export const selectableItemIdsComponentState = createComponentStateV2<
string[][]
>({
key: 'selectableItemIdsComponentState',
defaultValue: [[]],
componentInstanceContext: SelectableListComponentInstanceContext,
});

View File

@ -1,8 +1,10 @@
import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState';
import { SelectableListComponentInstanceContext } from '@/ui/layout/selectable-list/states/contexts/SelectableListComponentInstanceContext';
import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2';
export const selectableListOnEnterComponentState = createComponentState<
export const selectableListOnEnterComponentState = createComponentStateV2<
((itemId: string) => void) | undefined
>({
key: 'selectableListOnEnterComponentState',
defaultValue: undefined,
componentInstanceContext: SelectableListComponentInstanceContext,
});

View File

@ -1,8 +1,10 @@
import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState';
import { SelectableListComponentInstanceContext } from '@/ui/layout/selectable-list/states/contexts/SelectableListComponentInstanceContext';
import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2';
export const selectedItemIdComponentState = createComponentState<string | null>(
{
key: 'selectedItemIdComponentState',
defaultValue: null,
},
);
export const selectedItemIdComponentState = createComponentStateV2<
string | null
>({
key: 'selectedItemIdComponentState',
defaultValue: null,
componentInstanceContext: SelectableListComponentInstanceContext,
});

View File

@ -0,0 +1,28 @@
import { SelectableListComponentInstanceContext } from '@/ui/layout/selectable-list/states/contexts/SelectableListComponentInstanceContext';
import { isSelectedItemIdComponentFamilyState } from '@/ui/layout/selectable-list/states/isSelectedItemIdComponentFamilyState';
import { createComponentFamilySelectorV2 } from '@/ui/utilities/state/component-state/utils/createComponentFamilySelectorV2';
export const isSelectedItemIdComponentFamilySelector =
createComponentFamilySelectorV2<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,
),
});

View File

@ -1,28 +0,0 @@
import { isSelectedItemIdComponentFamilyState } from '@/ui/layout/selectable-list/states/isSelectedItemIdComponentFamilyState';
import { createComponentFamilySelector } from '@/ui/utilities/state/component-state/utils/createComponentFamilySelector';
export const isSelectedItemIdFamilySelector = createComponentFamilySelector<
boolean,
string
>({
key: 'isSelectedItemIdFamilySelector',
get:
({ scopeId, familyKey }: { scopeId: string; familyKey: string }) =>
({ get }) =>
get(
isSelectedItemIdComponentFamilyState({
scopeId: scopeId,
familyKey: familyKey,
}),
),
set:
({ scopeId, familyKey }: { scopeId: string; familyKey: string }) =>
({ set }, newValue) =>
set(
isSelectedItemIdComponentFamilyState({
scopeId: scopeId,
familyKey: familyKey,
}),
newValue,
),
});