Skip to content

Commit 3cee2b7

Browse files
Fixed record picker loading flickering (#12736)
This PR solves a flickering effect on record pickers on the different loading state they can be in. It was designed with @Bonapara to settle on a nice UX feeling. ## Before With fast network (local) : https://github.com/user-attachments/assets/58899934-c705-4b44-b7f6-289045032c11 With slow network : https://github.com/user-attachments/assets/9fb18d86-9da6-4e5d-a83f-00c810fab2dc ## After https://github.com/user-attachments/assets/f4abb40f-5d42-4c46-88ab-aaef4f883f7f Fixes #12680 --------- Co-authored-by: Charles Bochet <[email protected]>
1 parent 9aaa104 commit 3cee2b7

26 files changed

+474
-195
lines changed

packages/twenty-front/src/modules/activities/inline-cell/hooks/useOpenActivityTargetCellEditMode.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { ActivityTargetWithTargetRecord } from '@/activities/types/ActivityTargetObject';
22
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
33
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
4+
import { useMultipleRecordPickerOpen } from '@/object-record/record-picker/multiple-record-picker/hooks/useMultipleRecordPickerOpen';
45
import { useMultipleRecordPickerPerformSearch } from '@/object-record/record-picker/multiple-record-picker/hooks/useMultipleRecordPickerPerformSearch';
56
import { multipleRecordPickerPickableMorphItemsComponentState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerPickableMorphItemsComponentState';
67
import { multipleRecordPickerSearchFilterComponentState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerSearchFilterComponentState';
@@ -19,6 +20,7 @@ type OpenActivityTargetCellEditModeProps = {
1920
export const useOpenActivityTargetCellEditMode = () => {
2021
const { performSearch: multipleRecordPickerPerformSearch } =
2122
useMultipleRecordPickerPerformSearch();
23+
const { openMultipleRecordPicker } = useMultipleRecordPickerOpen();
2224

2325
const { pushFocusItemToFocusStack } = usePushFocusItemToFocusStack();
2426

@@ -70,6 +72,8 @@ export const useOpenActivityTargetCellEditMode = () => {
7072
'',
7173
);
7274

75+
openMultipleRecordPicker(recordPickerInstanceId);
76+
7377
multipleRecordPickerPerformSearch({
7478
multipleRecordPickerInstanceId: recordPickerInstanceId,
7579
forceSearchFilter: '',
@@ -97,7 +101,11 @@ export const useOpenActivityTargetCellEditMode = () => {
97101
memoizeKey: recordPickerInstanceId,
98102
});
99103
},
100-
[multipleRecordPickerPerformSearch, pushFocusItemToFocusStack],
104+
[
105+
multipleRecordPickerPerformSearch,
106+
openMultipleRecordPicker,
107+
pushFocusItemToFocusStack,
108+
],
101109
);
102110

103111
return { openActivityTargetCellEditMode };

packages/twenty-front/src/modules/object-record/record-field/meta-types/input/hooks/useOpenRelationFromManyFieldInput.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
FieldRelationFromManyValue,
55
FieldRelationValue,
66
} from '@/object-record/record-field/types/FieldMetadata';
7+
import { useMultipleRecordPickerOpen } from '@/object-record/record-picker/multiple-record-picker/hooks/useMultipleRecordPickerOpen';
78
import { useMultipleRecordPickerPerformSearch } from '@/object-record/record-picker/multiple-record-picker/hooks/useMultipleRecordPickerPerformSearch';
89
import { multipleRecordPickerPickableMorphItemsComponentState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerPickableMorphItemsComponentState';
910
import { multipleRecordPickerSearchableObjectMetadataItemsComponentState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerSearchableObjectMetadataItemsComponentState';
@@ -17,6 +18,7 @@ import { useRecoilCallback } from 'recoil';
1718

1819
export const useOpenRelationFromManyFieldInput = () => {
1920
const { performSearch } = useMultipleRecordPickerPerformSearch();
21+
const { openMultipleRecordPicker } = useMultipleRecordPickerOpen();
2022

2123
const { pushFocusItemToFocusStack } = usePushFocusItemToFocusStack();
2224

@@ -58,6 +60,8 @@ export const useOpenRelationFromManyFieldInput = () => {
5860
return;
5961
}
6062

63+
openMultipleRecordPicker(recordPickerInstanceId);
64+
6165
const pickableMorphItems: RecordPickerPickableMorphItem[] =
6266
fieldValue.map((record) => {
6367
return {
@@ -105,7 +109,7 @@ export const useOpenRelationFromManyFieldInput = () => {
105109
memoizeKey: recordPickerInstanceId,
106110
});
107111
},
108-
[performSearch, pushFocusItemToFocusStack],
112+
[openMultipleRecordPicker, performSearch, pushFocusItemToFocusStack],
109113
);
110114

111115
return { openRelationFromManyFieldInput };

packages/twenty-front/src/modules/object-record/record-field/meta-types/input/hooks/useOpenRelationToOneFieldInput.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
FieldRelationToOneValue,
44
FieldRelationValue,
55
} from '@/object-record/record-field/types/FieldMetadata';
6+
import { useSingleRecordPickerOpen } from '@/object-record/record-picker/single-record-picker/hooks/useSingleRecordPickerOpen';
67
import { singleRecordPickerSelectedIdComponentState } from '@/object-record/record-picker/single-record-picker/states/singleRecordPickerSelectedIdComponentState';
78
import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector';
89
import { DropdownHotkeyScope } from '@/ui/layout/dropdown/constants/DropdownHotkeyScope';
@@ -13,6 +14,7 @@ import { isDefined } from 'twenty-shared/utils';
1314

1415
export const useOpenRelationToOneFieldInput = () => {
1516
const { pushFocusItemToFocusStack } = usePushFocusItemToFocusStack();
17+
const { openSingleRecordPicker } = useSingleRecordPickerOpen();
1618

1719
const openRelationToOneFieldInput = useRecoilCallback(
1820
({ set, snapshot }) =>
@@ -39,6 +41,8 @@ export const useOpenRelationToOneFieldInput = () => {
3941
);
4042
}
4143

44+
openSingleRecordPicker(recordPickerInstanceId);
45+
4246
pushFocusItemToFocusStack({
4347
focusId: recordPickerInstanceId,
4448
component: {
@@ -50,7 +54,7 @@ export const useOpenRelationToOneFieldInput = () => {
5054
memoizeKey: recordPickerInstanceId,
5155
});
5256
},
53-
[pushFocusItemToFocusStack],
57+
[openSingleRecordPicker, pushFocusItemToFocusStack],
5458
);
5559

5660
return { openRelationToOneFieldInput };
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import styled from '@emotion/styled';
2+
3+
const StyledRecordPickerInitialLoadingEmptyContainer = styled.div`
4+
height: 320px;
5+
width: 100%;
6+
`;
7+
8+
export { StyledRecordPickerInitialLoadingEmptyContainer as RecordPickerInitialLoadingEmptyContainer };
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { DropdownMenuSkeletonItem } from '@/ui/input/relation-picker/components/skeletons/DropdownMenuSkeletonItem';
2+
3+
export const RecordPickerLoadingSkeletonList = () => {
4+
return (
5+
<>
6+
<DropdownMenuSkeletonItem width="53%" />
7+
<DropdownMenuSkeletonItem width="35%" />
8+
<DropdownMenuSkeletonItem width="48%" />
9+
<DropdownMenuSkeletonItem width="67%" />
10+
<DropdownMenuSkeletonItem width="75%" />
11+
</>
12+
);
13+
};

packages/twenty-front/src/modules/object-record/record-picker/multiple-record-picker/components/MultipleRecordPicker.tsx

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,23 +12,16 @@ import { CreateNewButton } from '@/ui/input/relation-picker/components/CreateNew
1212
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
1313
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
1414
import { DropdownHotkeyScope } from '@/ui/layout/dropdown/constants/DropdownHotkeyScope';
15-
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
1615
import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList';
1716
import { useHotkeysOnFocusedElement } from '@/ui/utilities/hotkey/hooks/useHotkeysOnFocusedElement';
1817
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
1918
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
20-
import styled from '@emotion/styled';
2119
import { useRef } from 'react';
2220
import { useRecoilCallback } from 'recoil';
2321
import { Key } from 'ts-key-enum';
2422
import { isDefined } from 'twenty-shared/utils';
2523
import { IconPlus } from 'twenty-ui/display';
2624

27-
export const StyledSelectableItem = styled(SelectableListItem)`
28-
height: 100%;
29-
width: 100%;
30-
`;
31-
3225
type MultipleRecordPickerProps = {
3326
onChange?: (morphItem: RecordPickerPickableMorphItem) => void;
3427
onSubmit?: () => void;

packages/twenty-front/src/modules/object-record/record-picker/multiple-record-picker/components/MultipleRecordPickerFetchMoreLoader.tsx

Lines changed: 42 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
11
import { useMultipleRecordPickerPerformSearch } from '@/object-record/record-picker/multiple-record-picker/hooks/useMultipleRecordPickerPerformSearch';
22
import { MultipleRecordPickerComponentInstanceContext } from '@/object-record/record-picker/multiple-record-picker/states/contexts/MultipleRecordPickerComponentInstanceContext';
3+
4+
import { multipleRecordPickerIsFetchingMoreComponentState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerIsFetchingMoreComponentState';
35
import { multipleRecordPickerIsLoadingComponentState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerIsLoadingComponentState';
6+
47
import { multipleRecordPickerPaginationState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerPaginationState';
58
import { multipleRecordPickerSearchFilterComponentState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerSearchFilterComponentState';
9+
import { multipleRecordPickerShouldShowInitialLoadingComponentState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerShouldShowInitialLoadingComponentState';
10+
import { multipleRecordPickerShouldShowSkeletonComponentState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerShouldShowSkeletonComponentState';
611
import { multipleRecordPickerPaginationSelector } from '@/object-record/record-picker/multiple-record-picker/states/selectors/multipleRecordPickerPaginationSelector';
712
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
13+
import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2';
814
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
915
import styled from '@emotion/styled';
1016
import { useCallback } from 'react';
@@ -23,10 +29,17 @@ const StyledText = styled.div`
2329
`;
2430

2531
const StyledIntersectionObserver = styled.div`
26-
height: 1px;
32+
height: 0px;
2733
`;
2834

2935
export const MultipleRecordPickerFetchMoreLoader = () => {
36+
const [
37+
multipleRecordPickerIsFetchingMore,
38+
setMultipleRecordPickerIsFetchingMore,
39+
] = useRecoilComponentStateV2(
40+
multipleRecordPickerIsFetchingMoreComponentState,
41+
);
42+
3043
const componentInstanceId = useAvailableComponentInstanceIdOrThrow(
3144
MultipleRecordPickerComponentInstanceContext,
3245
);
@@ -46,6 +59,15 @@ export const MultipleRecordPickerFetchMoreLoader = () => {
4659
componentInstanceId,
4760
);
4861

62+
const multipleRecordPickerShouldShowInitialLoading =
63+
useRecoilComponentValueV2(
64+
multipleRecordPickerShouldShowInitialLoadingComponentState,
65+
);
66+
67+
const multipleRecordPickerShouldShowSkeleton = useRecoilComponentValueV2(
68+
multipleRecordPickerShouldShowSkeletonComponentState,
69+
);
70+
4971
const { performSearch } = useMultipleRecordPickerPerformSearch();
5072

5173
const fetchMore = useRecoilCallback(
@@ -63,7 +85,7 @@ export const MultipleRecordPickerFetchMoreLoader = () => {
6385
return;
6486
}
6587

66-
performSearch({
88+
await performSearch({
6789
multipleRecordPickerInstanceId: componentInstanceId,
6890
forceSearchFilter: searchFilter,
6991
loadMore: true,
@@ -74,23 +96,34 @@ export const MultipleRecordPickerFetchMoreLoader = () => {
7496

7597
const { ref } = useInView({
7698
onChange: useCallback(
77-
(inView: boolean) => {
99+
async (inView: boolean) => {
78100
if (inView) {
79-
fetchMore();
101+
setMultipleRecordPickerIsFetchingMore(true);
102+
103+
await fetchMore();
104+
105+
setMultipleRecordPickerIsFetchingMore(false);
80106
}
81107
},
82-
[fetchMore],
108+
[fetchMore, setMultipleRecordPickerIsFetchingMore],
83109
),
84110
});
85111

86-
if (!paginationState.hasNextPage) {
112+
if (
113+
!paginationState.hasNextPage ||
114+
multipleRecordPickerShouldShowInitialLoading ||
115+
multipleRecordPickerShouldShowSkeleton ||
116+
(isLoading && !multipleRecordPickerIsFetchingMore)
117+
) {
87118
return null;
88119
}
89120

90121
return (
91-
<div>
122+
<>
92123
<StyledIntersectionObserver ref={ref} />
93-
{isLoading && <StyledText>Loading more...</StyledText>}
94-
</div>
124+
{multipleRecordPickerIsFetchingMore && (
125+
<StyledText>Loading more...</StyledText>
126+
)}
127+
</>
95128
);
96129
};

packages/twenty-front/src/modules/object-record/record-picker/multiple-record-picker/components/MultipleRecordPickerItemsDisplay.tsx

Lines changed: 3 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,7 @@
1+
import { MultipleRecordPickerLoadingEffect } from '@/object-record/record-picker/multiple-record-picker/components/MultipleRecordPickerLoadingEffect';
12
import { MultipleRecordPickerMenuItems } from '@/object-record/record-picker/multiple-record-picker/components/MultipleRecordPickerMenuItems';
2-
import { MultipleRecordPickerComponentInstanceContext } from '@/object-record/record-picker/multiple-record-picker/states/contexts/MultipleRecordPickerComponentInstanceContext';
3-
import { multipleRecordPickerIsLoadingComponentState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerIsLoadingComponentState';
4-
import { multipleRecordPickerPickableMorphItemsLengthComponentSelector } from '@/object-record/record-picker/multiple-record-picker/states/selectors/multipleRecordPickerPickableMorphItemsLengthComponentSelector';
53
import { RecordPickerPickableMorphItem } from '@/object-record/record-picker/types/RecordPickerPickableMorphItem';
6-
import { DropdownMenuSkeletonItem } from '@/ui/input/relation-picker/components/skeletons/DropdownMenuSkeletonItem';
74
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
8-
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
9-
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
105

116
export const MultipleRecordPickerItemsDisplay = ({
127
onChange,
@@ -15,28 +10,11 @@ export const MultipleRecordPickerItemsDisplay = ({
1510
onChange?: (morphItem: RecordPickerPickableMorphItem) => void;
1611
focusId: string;
1712
}) => {
18-
const componentInstanceId = useAvailableComponentInstanceIdOrThrow(
19-
MultipleRecordPickerComponentInstanceContext,
20-
);
21-
22-
const isLoading = useRecoilComponentValueV2(
23-
multipleRecordPickerIsLoadingComponentState,
24-
componentInstanceId,
25-
);
26-
27-
const itemsLength = useRecoilComponentValueV2(
28-
multipleRecordPickerPickableMorphItemsLengthComponentSelector,
29-
componentInstanceId,
30-
);
31-
3213
return (
3314
<>
15+
<MultipleRecordPickerLoadingEffect />
3416
<DropdownMenuSeparator />
35-
{isLoading && itemsLength === 0 ? (
36-
<DropdownMenuSkeletonItem />
37-
) : (
38-
<MultipleRecordPickerMenuItems onChange={onChange} focusId={focusId} />
39-
)}
17+
<MultipleRecordPickerMenuItems onChange={onChange} focusId={focusId} />
4018
<DropdownMenuSeparator />
4119
</>
4220
);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { multipleRecordPickerIsFetchingMoreComponentState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerIsFetchingMoreComponentState';
2+
import { multipleRecordPickerIsLoadingComponentState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerIsLoadingComponentState';
3+
import { multipleRecordPickerShouldShowSkeletonComponentState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerShouldShowSkeletonComponentState';
4+
import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2';
5+
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
6+
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
7+
import { useEffect, useState } from 'react';
8+
import { useDebouncedCallback } from 'use-debounce';
9+
10+
export const MultipleRecordPickerLoadingEffect = () => {
11+
const [previousLoading, setPreviousLoading] = useState(false);
12+
13+
const loading = useRecoilComponentValueV2(
14+
multipleRecordPickerIsLoadingComponentState,
15+
);
16+
17+
const setMultipleRecordPickerShowSkeleton = useSetRecoilComponentStateV2(
18+
multipleRecordPickerShouldShowSkeletonComponentState,
19+
);
20+
21+
const [multipleRecordPickerIsFetchingMore] = useRecoilComponentStateV2(
22+
multipleRecordPickerIsFetchingMoreComponentState,
23+
);
24+
25+
const debouncedShowPickerSearchSkeleton = useDebouncedCallback(
26+
() => setMultipleRecordPickerShowSkeleton(true),
27+
350,
28+
);
29+
30+
useEffect(() => {
31+
if (previousLoading !== loading) {
32+
setPreviousLoading(loading);
33+
34+
if (loading) {
35+
if (!multipleRecordPickerIsFetchingMore) {
36+
debouncedShowPickerSearchSkeleton();
37+
}
38+
} else {
39+
debouncedShowPickerSearchSkeleton.cancel();
40+
setMultipleRecordPickerShowSkeleton(false);
41+
}
42+
}
43+
}, [
44+
loading,
45+
previousLoading,
46+
setMultipleRecordPickerShowSkeleton,
47+
multipleRecordPickerIsFetchingMore,
48+
debouncedShowPickerSearchSkeleton,
49+
]);
50+
51+
return null;
52+
};

0 commit comments

Comments
 (0)