From 3cee2b796f5df2b6649222433f55d2521662f94e Mon Sep 17 00:00:00 2001 From: Lucas Bordeau Date: Tue, 24 Jun 2025 12:15:50 +0200 Subject: [PATCH] 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 https://github.com/twentyhq/twenty/issues/12680 --------- Co-authored-by: Charles Bochet --- .../useOpenActivityTargetCellEditMode.ts | 10 +- .../useOpenRelationFromManyFieldInput.tsx | 6 +- .../hooks/useOpenRelationToOneFieldInput.tsx | 6 +- ...cordPickerInitialLoadingEmptyContainer.tsx | 8 ++ .../RecordPickerLoadingSkeletonList.tsx | 13 ++ .../components/MultipleRecordPicker.tsx | 7 - .../MultipleRecordPickerFetchMoreLoader.tsx | 51 +++++-- .../MultipleRecordPickerItemsDisplay.tsx | 28 +--- .../MultipleRecordPickerLoadingEffect.tsx | 52 +++++++ .../MultipleRecordPickerMenuItems.tsx | 29 ++-- .../hooks/useMultipleRecordPickerOpen.ts | 40 ++++++ ...ecordPickerIsFetchingMoreComponentState.ts | 9 ++ ...rShouldShowInitialLoadingComponentState.ts | 9 ++ ...dPickerShouldShowSkeletonComponentState.ts | 9 ++ .../SingleRecordPickerLoadingEffect.tsx | 40 ++++++ .../SingleRecordPickerMenuItems.tsx | 113 ++++++++------- .../SingleRecordPickerMenuItemsWithSearch.tsx | 16 +-- .../hooks/useSingleRecordPickerOpen.ts | 38 +++++ .../hooks/useSingleRecordPickerSearch.ts | 1 + ...rShouldShowInitialLoadingComponentState.ts | 9 ++ ...dPickerShouldShowSkeletonComponentState.ts | 9 ++ ...ordDetailRelationSectionDropdownToMany.tsx | 5 + ...cordDetailRelationSectionDropdownToOne.tsx | 5 + .../skeletons/DropdownMenuSkeletonItem.tsx | 26 +++- .../__stories__/Dropdown.stories.tsx | 131 +++++++++--------- .../src/modules/ui/types/CSSWidth.ts | 1 + 26 files changed, 475 insertions(+), 196 deletions(-) create mode 100644 packages/twenty-front/src/modules/object-record/record-picker/components/RecordPickerInitialLoadingEmptyContainer.tsx create mode 100644 packages/twenty-front/src/modules/object-record/record-picker/components/RecordPickerLoadingSkeletonList.tsx create mode 100644 packages/twenty-front/src/modules/object-record/record-picker/multiple-record-picker/components/MultipleRecordPickerLoadingEffect.tsx create mode 100644 packages/twenty-front/src/modules/object-record/record-picker/multiple-record-picker/hooks/useMultipleRecordPickerOpen.ts create mode 100644 packages/twenty-front/src/modules/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerIsFetchingMoreComponentState.ts create mode 100644 packages/twenty-front/src/modules/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerShouldShowInitialLoadingComponentState.ts create mode 100644 packages/twenty-front/src/modules/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerShouldShowSkeletonComponentState.ts create mode 100644 packages/twenty-front/src/modules/object-record/record-picker/single-record-picker/components/SingleRecordPickerLoadingEffect.tsx create mode 100644 packages/twenty-front/src/modules/object-record/record-picker/single-record-picker/hooks/useSingleRecordPickerOpen.ts create mode 100644 packages/twenty-front/src/modules/object-record/record-picker/single-record-picker/states/singleRecordPickerShouldShowInitialLoadingComponentState.ts create mode 100644 packages/twenty-front/src/modules/object-record/record-picker/single-record-picker/states/singleRecordPickerShouldShowSkeletonComponentState.ts create mode 100644 packages/twenty-front/src/modules/ui/types/CSSWidth.ts diff --git a/packages/twenty-front/src/modules/activities/inline-cell/hooks/useOpenActivityTargetCellEditMode.ts b/packages/twenty-front/src/modules/activities/inline-cell/hooks/useOpenActivityTargetCellEditMode.ts index fd8cb2893..cfadbfc69 100644 --- a/packages/twenty-front/src/modules/activities/inline-cell/hooks/useOpenActivityTargetCellEditMode.ts +++ b/packages/twenty-front/src/modules/activities/inline-cell/hooks/useOpenActivityTargetCellEditMode.ts @@ -1,6 +1,7 @@ import { ActivityTargetWithTargetRecord } from '@/activities/types/ActivityTargetObject'; import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; +import { useMultipleRecordPickerOpen } from '@/object-record/record-picker/multiple-record-picker/hooks/useMultipleRecordPickerOpen'; import { useMultipleRecordPickerPerformSearch } from '@/object-record/record-picker/multiple-record-picker/hooks/useMultipleRecordPickerPerformSearch'; import { multipleRecordPickerPickableMorphItemsComponentState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerPickableMorphItemsComponentState'; import { multipleRecordPickerSearchFilterComponentState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerSearchFilterComponentState'; @@ -19,6 +20,7 @@ type OpenActivityTargetCellEditModeProps = { export const useOpenActivityTargetCellEditMode = () => { const { performSearch: multipleRecordPickerPerformSearch } = useMultipleRecordPickerPerformSearch(); + const { openMultipleRecordPicker } = useMultipleRecordPickerOpen(); const { pushFocusItemToFocusStack } = usePushFocusItemToFocusStack(); @@ -70,6 +72,8 @@ export const useOpenActivityTargetCellEditMode = () => { '', ); + openMultipleRecordPicker(recordPickerInstanceId); + multipleRecordPickerPerformSearch({ multipleRecordPickerInstanceId: recordPickerInstanceId, forceSearchFilter: '', @@ -97,7 +101,11 @@ export const useOpenActivityTargetCellEditMode = () => { memoizeKey: recordPickerInstanceId, }); }, - [multipleRecordPickerPerformSearch, pushFocusItemToFocusStack], + [ + multipleRecordPickerPerformSearch, + openMultipleRecordPicker, + pushFocusItemToFocusStack, + ], ); return { openActivityTargetCellEditMode }; diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/hooks/useOpenRelationFromManyFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/hooks/useOpenRelationFromManyFieldInput.tsx index 10504aa69..1f1d8128c 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/hooks/useOpenRelationFromManyFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/hooks/useOpenRelationFromManyFieldInput.tsx @@ -4,6 +4,7 @@ import { FieldRelationFromManyValue, FieldRelationValue, } from '@/object-record/record-field/types/FieldMetadata'; +import { useMultipleRecordPickerOpen } from '@/object-record/record-picker/multiple-record-picker/hooks/useMultipleRecordPickerOpen'; import { useMultipleRecordPickerPerformSearch } from '@/object-record/record-picker/multiple-record-picker/hooks/useMultipleRecordPickerPerformSearch'; import { multipleRecordPickerPickableMorphItemsComponentState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerPickableMorphItemsComponentState'; import { multipleRecordPickerSearchableObjectMetadataItemsComponentState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerSearchableObjectMetadataItemsComponentState'; @@ -17,6 +18,7 @@ import { useRecoilCallback } from 'recoil'; export const useOpenRelationFromManyFieldInput = () => { const { performSearch } = useMultipleRecordPickerPerformSearch(); + const { openMultipleRecordPicker } = useMultipleRecordPickerOpen(); const { pushFocusItemToFocusStack } = usePushFocusItemToFocusStack(); @@ -58,6 +60,8 @@ export const useOpenRelationFromManyFieldInput = () => { return; } + openMultipleRecordPicker(recordPickerInstanceId); + const pickableMorphItems: RecordPickerPickableMorphItem[] = fieldValue.map((record) => { return { @@ -105,7 +109,7 @@ export const useOpenRelationFromManyFieldInput = () => { memoizeKey: recordPickerInstanceId, }); }, - [performSearch, pushFocusItemToFocusStack], + [openMultipleRecordPicker, performSearch, pushFocusItemToFocusStack], ); return { openRelationFromManyFieldInput }; diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/hooks/useOpenRelationToOneFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/hooks/useOpenRelationToOneFieldInput.tsx index b163f83c8..170d72524 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/hooks/useOpenRelationToOneFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/hooks/useOpenRelationToOneFieldInput.tsx @@ -3,6 +3,7 @@ import { FieldRelationToOneValue, FieldRelationValue, } from '@/object-record/record-field/types/FieldMetadata'; +import { useSingleRecordPickerOpen } from '@/object-record/record-picker/single-record-picker/hooks/useSingleRecordPickerOpen'; import { singleRecordPickerSelectedIdComponentState } from '@/object-record/record-picker/single-record-picker/states/singleRecordPickerSelectedIdComponentState'; import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector'; import { DropdownHotkeyScope } from '@/ui/layout/dropdown/constants/DropdownHotkeyScope'; @@ -13,6 +14,7 @@ import { isDefined } from 'twenty-shared/utils'; export const useOpenRelationToOneFieldInput = () => { const { pushFocusItemToFocusStack } = usePushFocusItemToFocusStack(); + const { openSingleRecordPicker } = useSingleRecordPickerOpen(); const openRelationToOneFieldInput = useRecoilCallback( ({ set, snapshot }) => @@ -39,6 +41,8 @@ export const useOpenRelationToOneFieldInput = () => { ); } + openSingleRecordPicker(recordPickerInstanceId); + pushFocusItemToFocusStack({ focusId: recordPickerInstanceId, component: { @@ -50,7 +54,7 @@ export const useOpenRelationToOneFieldInput = () => { memoizeKey: recordPickerInstanceId, }); }, - [pushFocusItemToFocusStack], + [openSingleRecordPicker, pushFocusItemToFocusStack], ); return { openRelationToOneFieldInput }; diff --git a/packages/twenty-front/src/modules/object-record/record-picker/components/RecordPickerInitialLoadingEmptyContainer.tsx b/packages/twenty-front/src/modules/object-record/record-picker/components/RecordPickerInitialLoadingEmptyContainer.tsx new file mode 100644 index 000000000..b7e895c1a --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-picker/components/RecordPickerInitialLoadingEmptyContainer.tsx @@ -0,0 +1,8 @@ +import styled from '@emotion/styled'; + +const StyledRecordPickerInitialLoadingEmptyContainer = styled.div` + height: 320px; + width: 100%; +`; + +export { StyledRecordPickerInitialLoadingEmptyContainer as RecordPickerInitialLoadingEmptyContainer }; diff --git a/packages/twenty-front/src/modules/object-record/record-picker/components/RecordPickerLoadingSkeletonList.tsx b/packages/twenty-front/src/modules/object-record/record-picker/components/RecordPickerLoadingSkeletonList.tsx new file mode 100644 index 000000000..64c47ccbc --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-picker/components/RecordPickerLoadingSkeletonList.tsx @@ -0,0 +1,13 @@ +import { DropdownMenuSkeletonItem } from '@/ui/input/relation-picker/components/skeletons/DropdownMenuSkeletonItem'; + +export const RecordPickerLoadingSkeletonList = () => { + return ( + <> + + + + + + + ); +}; diff --git a/packages/twenty-front/src/modules/object-record/record-picker/multiple-record-picker/components/MultipleRecordPicker.tsx b/packages/twenty-front/src/modules/object-record/record-picker/multiple-record-picker/components/MultipleRecordPicker.tsx index c2cb1b64a..b2884318f 100644 --- a/packages/twenty-front/src/modules/object-record/record-picker/multiple-record-picker/components/MultipleRecordPicker.tsx +++ b/packages/twenty-front/src/modules/object-record/record-picker/multiple-record-picker/components/MultipleRecordPicker.tsx @@ -12,23 +12,16 @@ import { CreateNewButton } from '@/ui/input/relation-picker/components/CreateNew import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent'; import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; import { DropdownHotkeyScope } from '@/ui/layout/dropdown/constants/DropdownHotkeyScope'; -import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem'; import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList'; import { useHotkeysOnFocusedElement } from '@/ui/utilities/hotkey/hooks/useHotkeysOnFocusedElement'; import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope'; import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2'; -import styled from '@emotion/styled'; import { useRef } from 'react'; import { useRecoilCallback } from 'recoil'; import { Key } from 'ts-key-enum'; import { isDefined } from 'twenty-shared/utils'; import { IconPlus } from 'twenty-ui/display'; -export const StyledSelectableItem = styled(SelectableListItem)` - height: 100%; - width: 100%; -`; - type MultipleRecordPickerProps = { onChange?: (morphItem: RecordPickerPickableMorphItem) => void; onSubmit?: () => void; diff --git a/packages/twenty-front/src/modules/object-record/record-picker/multiple-record-picker/components/MultipleRecordPickerFetchMoreLoader.tsx b/packages/twenty-front/src/modules/object-record/record-picker/multiple-record-picker/components/MultipleRecordPickerFetchMoreLoader.tsx index 7f4de8a5a..0c087c744 100644 --- a/packages/twenty-front/src/modules/object-record/record-picker/multiple-record-picker/components/MultipleRecordPickerFetchMoreLoader.tsx +++ b/packages/twenty-front/src/modules/object-record/record-picker/multiple-record-picker/components/MultipleRecordPickerFetchMoreLoader.tsx @@ -1,10 +1,16 @@ import { useMultipleRecordPickerPerformSearch } from '@/object-record/record-picker/multiple-record-picker/hooks/useMultipleRecordPickerPerformSearch'; import { MultipleRecordPickerComponentInstanceContext } from '@/object-record/record-picker/multiple-record-picker/states/contexts/MultipleRecordPickerComponentInstanceContext'; + +import { multipleRecordPickerIsFetchingMoreComponentState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerIsFetchingMoreComponentState'; import { multipleRecordPickerIsLoadingComponentState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerIsLoadingComponentState'; + import { multipleRecordPickerPaginationState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerPaginationState'; import { multipleRecordPickerSearchFilterComponentState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerSearchFilterComponentState'; +import { multipleRecordPickerShouldShowInitialLoadingComponentState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerShouldShowInitialLoadingComponentState'; +import { multipleRecordPickerShouldShowSkeletonComponentState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerShouldShowSkeletonComponentState'; import { multipleRecordPickerPaginationSelector } from '@/object-record/record-picker/multiple-record-picker/states/selectors/multipleRecordPickerPaginationSelector'; import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow'; +import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import styled from '@emotion/styled'; import { useCallback } from 'react'; @@ -23,10 +29,17 @@ const StyledText = styled.div` `; const StyledIntersectionObserver = styled.div` - height: 1px; + height: 0px; `; export const MultipleRecordPickerFetchMoreLoader = () => { + const [ + multipleRecordPickerIsFetchingMore, + setMultipleRecordPickerIsFetchingMore, + ] = useRecoilComponentStateV2( + multipleRecordPickerIsFetchingMoreComponentState, + ); + const componentInstanceId = useAvailableComponentInstanceIdOrThrow( MultipleRecordPickerComponentInstanceContext, ); @@ -46,6 +59,15 @@ export const MultipleRecordPickerFetchMoreLoader = () => { componentInstanceId, ); + const multipleRecordPickerShouldShowInitialLoading = + useRecoilComponentValueV2( + multipleRecordPickerShouldShowInitialLoadingComponentState, + ); + + const multipleRecordPickerShouldShowSkeleton = useRecoilComponentValueV2( + multipleRecordPickerShouldShowSkeletonComponentState, + ); + const { performSearch } = useMultipleRecordPickerPerformSearch(); const fetchMore = useRecoilCallback( @@ -63,7 +85,7 @@ export const MultipleRecordPickerFetchMoreLoader = () => { return; } - performSearch({ + await performSearch({ multipleRecordPickerInstanceId: componentInstanceId, forceSearchFilter: searchFilter, loadMore: true, @@ -74,23 +96,34 @@ export const MultipleRecordPickerFetchMoreLoader = () => { const { ref } = useInView({ onChange: useCallback( - (inView: boolean) => { + async (inView: boolean) => { if (inView) { - fetchMore(); + setMultipleRecordPickerIsFetchingMore(true); + + await fetchMore(); + + setMultipleRecordPickerIsFetchingMore(false); } }, - [fetchMore], + [fetchMore, setMultipleRecordPickerIsFetchingMore], ), }); - if (!paginationState.hasNextPage) { + if ( + !paginationState.hasNextPage || + multipleRecordPickerShouldShowInitialLoading || + multipleRecordPickerShouldShowSkeleton || + (isLoading && !multipleRecordPickerIsFetchingMore) + ) { return null; } return ( -
+ <> - {isLoading && Loading more...} -
+ {multipleRecordPickerIsFetchingMore && ( + Loading more... + )} + ); }; diff --git a/packages/twenty-front/src/modules/object-record/record-picker/multiple-record-picker/components/MultipleRecordPickerItemsDisplay.tsx b/packages/twenty-front/src/modules/object-record/record-picker/multiple-record-picker/components/MultipleRecordPickerItemsDisplay.tsx index de5a6b6ae..d095dd247 100644 --- a/packages/twenty-front/src/modules/object-record/record-picker/multiple-record-picker/components/MultipleRecordPickerItemsDisplay.tsx +++ b/packages/twenty-front/src/modules/object-record/record-picker/multiple-record-picker/components/MultipleRecordPickerItemsDisplay.tsx @@ -1,12 +1,7 @@ +import { MultipleRecordPickerLoadingEffect } from '@/object-record/record-picker/multiple-record-picker/components/MultipleRecordPickerLoadingEffect'; import { MultipleRecordPickerMenuItems } from '@/object-record/record-picker/multiple-record-picker/components/MultipleRecordPickerMenuItems'; -import { MultipleRecordPickerComponentInstanceContext } from '@/object-record/record-picker/multiple-record-picker/states/contexts/MultipleRecordPickerComponentInstanceContext'; -import { multipleRecordPickerIsLoadingComponentState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerIsLoadingComponentState'; -import { multipleRecordPickerPickableMorphItemsLengthComponentSelector } from '@/object-record/record-picker/multiple-record-picker/states/selectors/multipleRecordPickerPickableMorphItemsLengthComponentSelector'; import { RecordPickerPickableMorphItem } from '@/object-record/record-picker/types/RecordPickerPickableMorphItem'; -import { DropdownMenuSkeletonItem } from '@/ui/input/relation-picker/components/skeletons/DropdownMenuSkeletonItem'; import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator'; -import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow'; -import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; export const MultipleRecordPickerItemsDisplay = ({ onChange, @@ -15,28 +10,11 @@ export const MultipleRecordPickerItemsDisplay = ({ onChange?: (morphItem: RecordPickerPickableMorphItem) => void; focusId: string; }) => { - const componentInstanceId = useAvailableComponentInstanceIdOrThrow( - MultipleRecordPickerComponentInstanceContext, - ); - - const isLoading = useRecoilComponentValueV2( - multipleRecordPickerIsLoadingComponentState, - componentInstanceId, - ); - - const itemsLength = useRecoilComponentValueV2( - multipleRecordPickerPickableMorphItemsLengthComponentSelector, - componentInstanceId, - ); - return ( <> + - {isLoading && itemsLength === 0 ? ( - - ) : ( - - )} + ); diff --git a/packages/twenty-front/src/modules/object-record/record-picker/multiple-record-picker/components/MultipleRecordPickerLoadingEffect.tsx b/packages/twenty-front/src/modules/object-record/record-picker/multiple-record-picker/components/MultipleRecordPickerLoadingEffect.tsx new file mode 100644 index 000000000..22738daf5 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-picker/multiple-record-picker/components/MultipleRecordPickerLoadingEffect.tsx @@ -0,0 +1,52 @@ +import { multipleRecordPickerIsFetchingMoreComponentState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerIsFetchingMoreComponentState'; +import { multipleRecordPickerIsLoadingComponentState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerIsLoadingComponentState'; +import { multipleRecordPickerShouldShowSkeletonComponentState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerShouldShowSkeletonComponentState'; +import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2'; +import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; +import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2'; +import { useEffect, useState } from 'react'; +import { useDebouncedCallback } from 'use-debounce'; + +export const MultipleRecordPickerLoadingEffect = () => { + const [previousLoading, setPreviousLoading] = useState(false); + + const loading = useRecoilComponentValueV2( + multipleRecordPickerIsLoadingComponentState, + ); + + const setMultipleRecordPickerShowSkeleton = useSetRecoilComponentStateV2( + multipleRecordPickerShouldShowSkeletonComponentState, + ); + + const [multipleRecordPickerIsFetchingMore] = useRecoilComponentStateV2( + multipleRecordPickerIsFetchingMoreComponentState, + ); + + const debouncedShowPickerSearchSkeleton = useDebouncedCallback( + () => setMultipleRecordPickerShowSkeleton(true), + 350, + ); + + useEffect(() => { + if (previousLoading !== loading) { + setPreviousLoading(loading); + + if (loading) { + if (!multipleRecordPickerIsFetchingMore) { + debouncedShowPickerSearchSkeleton(); + } + } else { + debouncedShowPickerSearchSkeleton.cancel(); + setMultipleRecordPickerShowSkeleton(false); + } + } + }, [ + loading, + previousLoading, + setMultipleRecordPickerShowSkeleton, + multipleRecordPickerIsFetchingMore, + debouncedShowPickerSearchSkeleton, + ]); + + return null; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-picker/multiple-record-picker/components/MultipleRecordPickerMenuItems.tsx b/packages/twenty-front/src/modules/object-record/record-picker/multiple-record-picker/components/MultipleRecordPickerMenuItems.tsx index ede0533e3..d5ad65ab4 100644 --- a/packages/twenty-front/src/modules/object-record/record-picker/multiple-record-picker/components/MultipleRecordPickerMenuItems.tsx +++ b/packages/twenty-front/src/modules/object-record/record-picker/multiple-record-picker/components/MultipleRecordPickerMenuItems.tsx @@ -1,28 +1,24 @@ -import styled from '@emotion/styled'; - +import { RecordPickerInitialLoadingEmptyContainer } from '@/object-record/record-picker/components/RecordPickerInitialLoadingEmptyContainer'; +import { RecordPickerLoadingSkeletonList } from '@/object-record/record-picker/components/RecordPickerLoadingSkeletonList'; import { RecordPickerNoRecordFoundMenuItem } from '@/object-record/record-picker/components/RecordPickerNoRecordFoundMenuItem'; import { MultipleRecordPickerFetchMoreLoader } from '@/object-record/record-picker/multiple-record-picker/components/MultipleRecordPickerFetchMoreLoader'; import { MultipleRecordPickerMenuItem } from '@/object-record/record-picker/multiple-record-picker/components/MultipleRecordPickerMenuItem'; import { MultipleRecordPickerComponentInstanceContext } from '@/object-record/record-picker/multiple-record-picker/states/contexts/MultipleRecordPickerComponentInstanceContext'; import { multipleRecordPickerPickableMorphItemsComponentState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerPickableMorphItemsComponentState'; +import { multipleRecordPickerShouldShowInitialLoadingComponentState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerShouldShowInitialLoadingComponentState'; +import { multipleRecordPickerShouldShowSkeletonComponentState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerShouldShowSkeletonComponentState'; import { multipleRecordPickerPickableRecordIdsMatchingSearchComponentSelector } from '@/object-record/record-picker/multiple-record-picker/states/selectors/multipleRecordPickerPickableRecordIdsMatchingSearchComponentSelector'; import { getMultipleRecordPickerSelectableListId } from '@/object-record/record-picker/multiple-record-picker/utils/getMultipleRecordPickerSelectableListId'; import { RecordPickerPickableMorphItem } from '@/object-record/record-picker/types/RecordPickerPickableMorphItem'; import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; import { DropdownHotkeyScope } from '@/ui/layout/dropdown/constants/DropdownHotkeyScope'; import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList'; -import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem'; import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList'; import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow'; import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { useRecoilCallback } from 'recoil'; -export const StyledSelectableItem = styled(SelectableListItem)` - height: 100%; - width: 100%; -`; - type MultipleRecordPickerMenuItemsProps = { onChange?: (morphItem: RecordPickerPickableMorphItem) => void; focusId: string; @@ -79,9 +75,24 @@ export const MultipleRecordPickerMenuItems = ({ [multipleRecordPickerPickableMorphItemsState], ); + const multipleRecordPickerShouldShowInitialLoading = + useRecoilComponentValueV2( + multipleRecordPickerShouldShowInitialLoadingComponentState, + ); + + const multipleRecordPickerShouldShowSkeleton = useRecoilComponentValueV2( + multipleRecordPickerShouldShowSkeletonComponentState, + ); + + const searchHasNoResults = pickableRecordIds.length === 0; + return ( - {pickableRecordIds.length === 0 ? ( + {multipleRecordPickerShouldShowInitialLoading ? ( + + ) : multipleRecordPickerShouldShowSkeleton ? ( + + ) : searchHasNoResults ? ( ) : ( { + const openMultipleRecordPicker = useRecoilCallback( + ({ set }) => + (recordPickerComponentInstanceId: string) => { + set( + multipleRecordPickerShouldShowInitialLoadingComponentState.atomFamily( + { + instanceId: recordPickerComponentInstanceId, + }, + ), + true, + ); + set( + multipleRecordPickerShouldShowSkeletonComponentState.atomFamily({ + instanceId: recordPickerComponentInstanceId, + }), + true, + ); + setTimeout(() => { + set( + multipleRecordPickerShouldShowInitialLoadingComponentState.atomFamily( + { + instanceId: recordPickerComponentInstanceId, + }, + ), + false, + ); + }, 100); + }, + [], + ); + + return { + openMultipleRecordPicker, + }; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerIsFetchingMoreComponentState.ts b/packages/twenty-front/src/modules/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerIsFetchingMoreComponentState.ts new file mode 100644 index 000000000..d07837ebe --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerIsFetchingMoreComponentState.ts @@ -0,0 +1,9 @@ +import { MultipleRecordPickerComponentInstanceContext } from '@/object-record/record-picker/multiple-record-picker/states/contexts/MultipleRecordPickerComponentInstanceContext'; +import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2'; + +export const multipleRecordPickerIsFetchingMoreComponentState = + createComponentStateV2({ + key: 'multipleRecordPickerIsFetchingMoreComponentState', + defaultValue: false, + componentInstanceContext: MultipleRecordPickerComponentInstanceContext, + }); diff --git a/packages/twenty-front/src/modules/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerShouldShowInitialLoadingComponentState.ts b/packages/twenty-front/src/modules/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerShouldShowInitialLoadingComponentState.ts new file mode 100644 index 000000000..e7d2f3a11 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerShouldShowInitialLoadingComponentState.ts @@ -0,0 +1,9 @@ +import { MultipleRecordPickerComponentInstanceContext } from '@/object-record/record-picker/multiple-record-picker/states/contexts/MultipleRecordPickerComponentInstanceContext'; +import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2'; + +export const multipleRecordPickerShouldShowInitialLoadingComponentState = + createComponentStateV2({ + key: 'multipleRecordPickerShouldShowInitialLoadingComponentState', + defaultValue: false, + componentInstanceContext: MultipleRecordPickerComponentInstanceContext, + }); diff --git a/packages/twenty-front/src/modules/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerShouldShowSkeletonComponentState.ts b/packages/twenty-front/src/modules/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerShouldShowSkeletonComponentState.ts new file mode 100644 index 000000000..431cd5f05 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerShouldShowSkeletonComponentState.ts @@ -0,0 +1,9 @@ +import { MultipleRecordPickerComponentInstanceContext } from '@/object-record/record-picker/multiple-record-picker/states/contexts/MultipleRecordPickerComponentInstanceContext'; +import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2'; + +export const multipleRecordPickerShouldShowSkeletonComponentState = + createComponentStateV2({ + key: 'multipleRecordPickerShouldShowSkeletonComponentState', + defaultValue: false, + componentInstanceContext: MultipleRecordPickerComponentInstanceContext, + }); diff --git a/packages/twenty-front/src/modules/object-record/record-picker/single-record-picker/components/SingleRecordPickerLoadingEffect.tsx b/packages/twenty-front/src/modules/object-record/record-picker/single-record-picker/components/SingleRecordPickerLoadingEffect.tsx new file mode 100644 index 000000000..ed6256c19 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-picker/single-record-picker/components/SingleRecordPickerLoadingEffect.tsx @@ -0,0 +1,40 @@ +import { singleRecordPickerShouldShowSkeletonComponentState } from '@/object-record/record-picker/single-record-picker/states/singleRecordPickerShouldShowSkeletonComponentState'; +import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2'; +import { useEffect, useState } from 'react'; +import { useDebouncedCallback } from 'use-debounce'; + +export const SingleRecordPickerLoadingEffect = ({ + loading, +}: { + loading: boolean; +}) => { + const [previousLoading, setPreviousLoading] = useState(false); + + const setSingleRecordPickerShouldShowSkeleton = useSetRecoilComponentStateV2( + singleRecordPickerShouldShowSkeletonComponentState, + ); + + const debouncedShowPickerSearchSkeleton = useDebouncedCallback(() => { + setSingleRecordPickerShouldShowSkeleton(true); + }, 350); + + useEffect(() => { + if (previousLoading !== loading) { + setPreviousLoading(loading); + + if (loading) { + debouncedShowPickerSearchSkeleton(); + } else { + debouncedShowPickerSearchSkeleton.cancel(); + setSingleRecordPickerShouldShowSkeleton(false); + } + } + }, [ + loading, + previousLoading, + debouncedShowPickerSearchSkeleton, + setSingleRecordPickerShouldShowSkeleton, + ]); + + return null; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-picker/single-record-picker/components/SingleRecordPickerMenuItems.tsx b/packages/twenty-front/src/modules/object-record/record-picker/single-record-picker/components/SingleRecordPickerMenuItems.tsx index fbda69791..2588a24cd 100644 --- a/packages/twenty-front/src/modules/object-record/record-picker/single-record-picker/components/SingleRecordPickerMenuItems.tsx +++ b/packages/twenty-front/src/modules/object-record/record-picker/single-record-picker/components/SingleRecordPickerMenuItems.tsx @@ -1,10 +1,12 @@ import { isNonEmptyString, isUndefined } from '@sniptt/guards'; import { Key } from 'ts-key-enum'; -import { DropdownMenuSkeletonItem } from '@/ui/input/relation-picker/components/skeletons/DropdownMenuSkeletonItem'; import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList'; import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList'; +import { RecordPickerInitialLoadingEmptyContainer } from '@/object-record/record-picker/components/RecordPickerInitialLoadingEmptyContainer'; +import { RecordPickerLoadingSkeletonList } from '@/object-record/record-picker/components/RecordPickerLoadingSkeletonList'; +import { RecordPickerNoRecordFoundMenuItem } from '@/object-record/record-picker/components/RecordPickerNoRecordFoundMenuItem'; import { SingleRecordPickerMenuItem } from '@/object-record/record-picker/single-record-picker/components/SingleRecordPickerMenuItem'; import { SingleRecordPickerComponentInstanceContext } from '@/object-record/record-picker/single-record-picker/states/contexts/SingleRecordPickerComponentInstanceContext'; import { singleRecordPickerSelectedIdComponentState } from '@/object-record/record-picker/single-record-picker/states/singleRecordPickerSelectedIdComponentState'; @@ -17,44 +19,35 @@ import { useHotkeysOnFocusedElement } from '@/ui/utilities/hotkey/hooks/useHotke import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow'; import { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValueV2'; import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2'; +import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { isDefined } from 'twenty-shared/utils'; import { IconComponent } from 'twenty-ui/display'; import { MenuItemSelect } from 'twenty-ui/navigation'; +import { singleRecordPickerShouldShowInitialLoadingComponentState } from '@/object-record/record-picker/single-record-picker/states/singleRecordPickerShouldShowInitialLoadingComponentState'; +import { singleRecordPickerShouldShowSkeletonComponentState } from '@/object-record/record-picker/single-record-picker/states/singleRecordPickerShouldShowSkeletonComponentState'; export type SingleRecordPickerMenuItemsProps = { EmptyIcon?: IconComponent; emptyLabel?: string; recordsToSelect: SingleRecordPickerRecord[]; - loading?: boolean; onCancel?: () => void; onRecordSelected: (entity?: SingleRecordPickerRecord) => void; selectedRecord?: SingleRecordPickerRecord; focusId: string; + filteredSelectedRecords: SingleRecordPickerRecord[]; }; export const SingleRecordPickerMenuItems = ({ EmptyIcon, emptyLabel, recordsToSelect, - loading, onCancel, onRecordSelected, + filteredSelectedRecords, selectedRecord, focusId, }: SingleRecordPickerMenuItemsProps) => { - const selectNone = emptyLabel - ? { - __typename: '', - id: 'select-none', - name: emptyLabel, - } - : null; - - const recordsInDropdown = [ - selectNone, - selectedRecord, - ...recordsToSelect, - ].filter( + const recordsInDropdown = [selectedRecord, ...recordsToSelect].filter( (entity): entity is SingleRecordPickerRecord => isDefined(entity) && isNonEmptyString(entity.name), ); @@ -93,6 +86,17 @@ export const SingleRecordPickerMenuItems = ({ singleRecordPickerSelectedIdComponentState, ); + const singleRecordPickerShouldShowSkeleton = useRecoilComponentValueV2( + singleRecordPickerShouldShowSkeletonComponentState, + ); + + const singleRecordPickerShouldShowInitialLoading = useRecoilComponentValueV2( + singleRecordPickerShouldShowInitialLoadingComponentState, + ); + + const searchHasNoResults = + recordsToSelect.length === 0 && filteredSelectedRecords?.length === 0; + return ( - {loading ? ( - - ) : ( - recordsInDropdown?.map((record) => { - switch (record.id) { - case 'select-none': { - return ( - emptyLabel && ( - { - setSelectedRecordId(undefined); - onRecordSelected(); - }} - > - { - setSelectedRecordId(undefined); - onRecordSelected(); - }} - LeftIcon={EmptyIcon} - text={emptyLabel} - selected={isUndefined(selectedRecordId)} - focused={isSelectedSelectNoneButton} - /> - - ) - ); - } - default: { - return ( - - ); - } - } - }) + {emptyLabel && ( + { + setSelectedRecordId(undefined); + onRecordSelected(); + }} + > + { + setSelectedRecordId(undefined); + onRecordSelected(); + }} + LeftIcon={EmptyIcon} + text={emptyLabel} + selected={isUndefined(selectedRecordId)} + focused={isSelectedSelectNoneButton} + /> + )} + {singleRecordPickerShouldShowInitialLoading ? ( + + ) : singleRecordPickerShouldShowSkeleton ? ( + + ) : ( + recordsInDropdown?.map((record) => ( + + )) + )} + {searchHasNoResults && } ); }; diff --git a/packages/twenty-front/src/modules/object-record/record-picker/single-record-picker/components/SingleRecordPickerMenuItemsWithSearch.tsx b/packages/twenty-front/src/modules/object-record/record-picker/single-record-picker/components/SingleRecordPickerMenuItemsWithSearch.tsx index 01e1b7452..b419f869c 100644 --- a/packages/twenty-front/src/modules/object-record/record-picker/single-record-picker/components/SingleRecordPickerMenuItemsWithSearch.tsx +++ b/packages/twenty-front/src/modules/object-record/record-picker/single-record-picker/components/SingleRecordPickerMenuItemsWithSearch.tsx @@ -1,6 +1,6 @@ import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { useObjectPermissionsForObject } from '@/object-record/hooks/useObjectPermissionsForObject'; -import { RecordPickerNoRecordFoundMenuItem } from '@/object-record/record-picker/components/RecordPickerNoRecordFoundMenuItem'; +import { SingleRecordPickerLoadingEffect } from '@/object-record/record-picker/single-record-picker/components/SingleRecordPickerLoadingEffect'; import { SingleRecordPickerMenuItems, SingleRecordPickerMenuItemsProps, @@ -16,7 +16,6 @@ import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/Dropdow import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator'; import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; -import { isNonEmptyString } from '@sniptt/guards'; import { isDefined } from 'twenty-shared/utils'; import { IconPlus } from 'twenty-ui/display'; @@ -73,18 +72,13 @@ export const SingleRecordPickerMenuItemsWithSearch = ({ const hasObjectUpdatePermissions = objectPermissions.canUpdateObjectRecords; - const searchHasNoResults = - isNonEmptyString(recordPickerSearchFilter) && - records.recordsToSelect.length === 0 && - records.filteredSelectedRecords.length === 0 && - !records.loading; - const handleCreateNew = () => { onCreate?.(recordPickerSearchFilter); }; return ( <> + {layoutDirection === 'search-bar-on-bottom' && ( <> {isDefined(onCreate) && hasObjectUpdatePermissions && ( @@ -101,12 +95,11 @@ export const SingleRecordPickerMenuItemsWithSearch = ({ )} - {searchHasNoResults && } - {searchHasNoResults && } {isDefined(onCreate) && hasObjectUpdatePermissions && ( <> diff --git a/packages/twenty-front/src/modules/object-record/record-picker/single-record-picker/hooks/useSingleRecordPickerOpen.ts b/packages/twenty-front/src/modules/object-record/record-picker/single-record-picker/hooks/useSingleRecordPickerOpen.ts new file mode 100644 index 000000000..6a7990c0c --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-picker/single-record-picker/hooks/useSingleRecordPickerOpen.ts @@ -0,0 +1,38 @@ +import { singleRecordPickerShouldShowInitialLoadingComponentState } from '@/object-record/record-picker/single-record-picker/states/singleRecordPickerShouldShowInitialLoadingComponentState'; +import { singleRecordPickerShouldShowSkeletonComponentState } from '@/object-record/record-picker/single-record-picker/states/singleRecordPickerShouldShowSkeletonComponentState'; +import { useRecoilCallback } from 'recoil'; + +export const useSingleRecordPickerOpen = () => { + const openSingleRecordPicker = useRecoilCallback( + ({ set }) => + (recordPickerComponentInstanceId: string) => { + set( + singleRecordPickerShouldShowInitialLoadingComponentState.atomFamily({ + instanceId: recordPickerComponentInstanceId, + }), + true, + ); + set( + singleRecordPickerShouldShowSkeletonComponentState.atomFamily({ + instanceId: recordPickerComponentInstanceId, + }), + true, + ); + setTimeout(() => { + set( + singleRecordPickerShouldShowInitialLoadingComponentState.atomFamily( + { + instanceId: recordPickerComponentInstanceId, + }, + ), + false, + ); + }, 100); + }, + [], + ); + + return { + openSingleRecordPicker, + }; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-picker/single-record-picker/hooks/useSingleRecordPickerSearch.ts b/packages/twenty-front/src/modules/object-record/record-picker/single-record-picker/hooks/useSingleRecordPickerSearch.ts index 16aa4a28d..dc24e964d 100644 --- a/packages/twenty-front/src/modules/object-record/record-picker/single-record-picker/hooks/useSingleRecordPickerSearch.ts +++ b/packages/twenty-front/src/modules/object-record/record-picker/single-record-picker/hooks/useSingleRecordPickerSearch.ts @@ -23,6 +23,7 @@ export const useSingleRecordPickerSearch = ( singleRecordPickerSelectedIdComponentState, recordPickerComponentInstanceId, ); + const debouncedSetSearchFilter = useDebouncedCallback( setRecordPickerSearchFilter, 100, diff --git a/packages/twenty-front/src/modules/object-record/record-picker/single-record-picker/states/singleRecordPickerShouldShowInitialLoadingComponentState.ts b/packages/twenty-front/src/modules/object-record/record-picker/single-record-picker/states/singleRecordPickerShouldShowInitialLoadingComponentState.ts new file mode 100644 index 000000000..81dd2f46e --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-picker/single-record-picker/states/singleRecordPickerShouldShowInitialLoadingComponentState.ts @@ -0,0 +1,9 @@ +import { SingleRecordPickerComponentInstanceContext } from '@/object-record/record-picker/single-record-picker/states/contexts/SingleRecordPickerComponentInstanceContext'; +import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2'; + +export const singleRecordPickerShouldShowInitialLoadingComponentState = + createComponentStateV2({ + key: 'singleRecordPickerShouldShowInitialLoadingComponentState', + defaultValue: false, + componentInstanceContext: SingleRecordPickerComponentInstanceContext, + }); diff --git a/packages/twenty-front/src/modules/object-record/record-picker/single-record-picker/states/singleRecordPickerShouldShowSkeletonComponentState.ts b/packages/twenty-front/src/modules/object-record/record-picker/single-record-picker/states/singleRecordPickerShouldShowSkeletonComponentState.ts new file mode 100644 index 000000000..073c86517 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-picker/single-record-picker/states/singleRecordPickerShouldShowSkeletonComponentState.ts @@ -0,0 +1,9 @@ +import { SingleRecordPickerComponentInstanceContext } from '@/object-record/record-picker/single-record-picker/states/contexts/SingleRecordPickerComponentInstanceContext'; +import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2'; + +export const singleRecordPickerShouldShowSkeletonComponentState = + createComponentStateV2({ + key: 'singleRecordPickerShouldShowSkeletonComponentState', + defaultValue: false, + componentInstanceContext: SingleRecordPickerComponentInstanceContext, + }); diff --git a/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/RecordDetailRelationSectionDropdownToMany.tsx b/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/RecordDetailRelationSectionDropdownToMany.tsx index 979635b2a..21cad55d8 100644 --- a/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/RecordDetailRelationSectionDropdownToMany.tsx +++ b/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/RecordDetailRelationSectionDropdownToMany.tsx @@ -7,6 +7,7 @@ import { useAddNewRecordAndOpenRightDrawer } from '@/object-record/record-field/ import { useUpdateRelationFromManyFieldInput } from '@/object-record/record-field/meta-types/input/hooks/useUpdateRelationFromManyFieldInput'; import { FieldRelationMetadata } from '@/object-record/record-field/types/FieldMetadata'; import { MultipleRecordPicker } from '@/object-record/record-picker/multiple-record-picker/components/MultipleRecordPicker'; +import { useMultipleRecordPickerOpen } from '@/object-record/record-picker/multiple-record-picker/hooks/useMultipleRecordPickerOpen'; import { useMultipleRecordPickerPerformSearch } from '@/object-record/record-picker/multiple-record-picker/hooks/useMultipleRecordPickerPerformSearch'; import { multipleRecordPickerPickableMorphItemsComponentState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerPickableMorphItemsComponentState'; import { multipleRecordPickerSearchFilterComponentState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerSearchFilterComponentState'; @@ -71,6 +72,8 @@ export const RecordDetailRelationSectionDropdownToMany = () => { const { performSearch: multipleRecordPickerPerformSearch } = useMultipleRecordPickerPerformSearch(); + const { openMultipleRecordPicker } = useMultipleRecordPickerOpen(); + const handleCloseRelationPickerDropdown = useCallback(() => { setMultipleRecordPickerSearchFilter(''); }, [setMultipleRecordPickerSearchFilter]); @@ -99,6 +102,8 @@ export const RecordDetailRelationSectionDropdownToMany = () => { })), ); + openMultipleRecordPicker(dropdownId); + multipleRecordPickerPerformSearch({ multipleRecordPickerInstanceId: dropdownId, forceSearchFilter: '', diff --git a/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/RecordDetailRelationSectionDropdownToOne.tsx b/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/RecordDetailRelationSectionDropdownToOne.tsx index f6f5c0589..2343e32ad 100644 --- a/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/RecordDetailRelationSectionDropdownToOne.tsx +++ b/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/RecordDetailRelationSectionDropdownToOne.tsx @@ -8,6 +8,7 @@ import { usePersistField } from '@/object-record/record-field/hooks/usePersistFi import { useAddNewRecordAndOpenRightDrawer } from '@/object-record/record-field/meta-types/input/hooks/useAddNewRecordAndOpenRightDrawer'; import { FieldRelationMetadata } from '@/object-record/record-field/types/FieldMetadata'; import { SingleRecordPicker } from '@/object-record/record-picker/single-record-picker/components/SingleRecordPicker'; +import { useSingleRecordPickerOpen } from '@/object-record/record-picker/single-record-picker/hooks/useSingleRecordPickerOpen'; import { singleRecordPickerSearchFilterComponentState } from '@/object-record/record-picker/single-record-picker/states/singleRecordPickerSearchFilterComponentState'; import { singleRecordPickerSelectedIdComponentState } from '@/object-record/record-picker/single-record-picker/states/singleRecordPickerSelectedIdComponentState'; import { SingleRecordPickerRecord } from '@/object-record/record-picker/single-record-picker/types/SingleRecordPickerRecord'; @@ -87,8 +88,12 @@ export const RecordDetailRelationSectionDropdownToOne = () => { recordId, }); + const { openSingleRecordPicker } = useSingleRecordPickerOpen(); + const handleOpenRelationPickerDropdown = () => { setSingleRecordPickerSearchFilter(''); + openSingleRecordPicker(dropdownId); + if (relationRecords.length > 0) { setSingleRecordPickerSelectedId(relationRecords[0]?.id); } diff --git a/packages/twenty-front/src/modules/ui/input/relation-picker/components/skeletons/DropdownMenuSkeletonItem.tsx b/packages/twenty-front/src/modules/ui/input/relation-picker/components/skeletons/DropdownMenuSkeletonItem.tsx index 820831f7c..6f495fa0b 100644 --- a/packages/twenty-front/src/modules/ui/input/relation-picker/components/skeletons/DropdownMenuSkeletonItem.tsx +++ b/packages/twenty-front/src/modules/ui/input/relation-picker/components/skeletons/DropdownMenuSkeletonItem.tsx @@ -1,4 +1,5 @@ import { SKELETON_LOADER_HEIGHT_SIZES } from '@/activities/components/SkeletonLoader'; +import { CSSWidth } from '@/ui/types/CSSWidth'; import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; import Skeleton, { SkeletonTheme } from 'react-loading-skeleton'; @@ -6,15 +7,24 @@ import Skeleton, { SkeletonTheme } from 'react-loading-skeleton'; const StyledDropdownMenuSkeletonContainer = styled.div` --horizontal-padding: ${({ theme }) => theme.spacing(1)}; --vertical-padding: ${({ theme }) => theme.spacing(2)}; - align-items: center; + border-radius: ${({ theme }) => theme.border.radius.sm}; gap: ${({ theme }) => theme.spacing(2)}; - height: calc(32px - 2 * var(--vertical-padding)); - padding: var(--vertical-padding) var(--horizontal-padding); - width: calc(100% - 2 * var(--horizontal-padding)); + box-sizing: border-box; + + flex-shrink: 0; + + padding-left: var(--horizontal-padding); + padding-right: var(--horizontal-padding); + + height: ${({ theme }) => theme.spacing(8)}; `; -export const DropdownMenuSkeletonItem = () => { +export const DropdownMenuSkeletonItem = ({ + width = '100%', +}: { + width?: CSSWidth; +}) => { const theme = useTheme(); return ( @@ -22,7 +32,11 @@ export const DropdownMenuSkeletonItem = () => { baseColor={theme.background.quaternary} highlightColor={theme.background.secondary} > - + ); diff --git a/packages/twenty-front/src/modules/ui/layout/dropdown/components/__stories__/Dropdown.stories.tsx b/packages/twenty-front/src/modules/ui/layout/dropdown/components/__stories__/Dropdown.stories.tsx index cec0b9c4e..e5cbb3273 100644 --- a/packages/twenty-front/src/modules/ui/layout/dropdown/components/__stories__/Dropdown.stories.tsx +++ b/packages/twenty-front/src/modules/ui/layout/dropdown/components/__stories__/Dropdown.stories.tsx @@ -6,6 +6,7 @@ import { useState } from 'react'; import { DropdownMenuSkeletonItem } from '@/ui/input/relation-picker/components/skeletons/DropdownMenuSkeletonItem'; +import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent'; import { DropdownMenuHeaderLeftComponent } from '@/ui/layout/dropdown/components/DropdownMenuHeader/internal/DropdownMenuHeaderLeftComponent'; import { Modal } from '@/ui/layout/modal/components/Modal'; import { ModalHotkeyScope } from '@/ui/layout/modal/components/types/ModalHotkeyScope'; @@ -84,7 +85,9 @@ const StyledEmptyDropdownContent = styled.div` export const Empty: Story = { args: { dropdownComponents: ( - + + + ), }, play: async () => { @@ -155,26 +158,28 @@ const FakeSelectableMenuItemList = ({ hasAvatar }: { hasAvatar?: boolean }) => { const [selectedItem, setSelectedItem] = useState(null); return ( - <> - {optionsMock.map((item) => ( - setSelectedItem(item.id)} - avatar={ - hasAvatar ? ( - - ) : undefined - } - text={item.name} - /> - ))} - + + + {optionsMock.map((item) => ( + setSelectedItem(item.id)} + avatar={ + hasAvatar ? ( + + ) : undefined + } + text={item.name} + /> + ))} + + ); }; @@ -184,31 +189,33 @@ const FakeCheckableMenuItemList = ({ hasAvatar }: { hasAvatar?: boolean }) => { >({}); return ( - <> - {optionsMock.map((item) => ( - - setSelectedItemsById((previous) => ({ - ...previous, - [item.id]: checked, - })) - } - avatar={ - hasAvatar ? ( - - ) : undefined - } - text={item.name} - /> - ))} - + + + {optionsMock.map((item) => ( + + setSelectedItemsById((previous) => ({ + ...previous, + [item.id]: checked, + })) + } + avatar={ + hasAvatar ? ( + + ) : undefined + } + text={item.name} + /> + ))} + + ); }; @@ -227,7 +234,7 @@ export const WithHeaders: Story = { decorators: [WithContentBelowDecorator], args: { dropdownComponents: ( - <> + @@ -250,7 +257,7 @@ export const WithHeaders: Story = { ))} - + ), }, play: playInteraction, @@ -260,13 +267,13 @@ export const SearchWithLoadingMenu: Story = { decorators: [WithContentBelowDecorator], args: { dropdownComponents: ( - <> + - + ), }, play: async () => { @@ -292,7 +299,7 @@ export const WithInput: Story = { decorators: [WithContentBelowDecorator], args: { dropdownComponents: ( - <> + @@ -300,7 +307,7 @@ export const WithInput: Story = { ))} - + ), }, play: playInteraction, @@ -309,11 +316,7 @@ export const WithInput: Story = { export const SelectableMenuItemWithAvatar: Story = { decorators: [WithContentBelowDecorator], args: { - dropdownComponents: ( - - - - ), + dropdownComponents: , }, play: playInteraction, }; @@ -321,11 +324,7 @@ export const SelectableMenuItemWithAvatar: Story = { export const CheckableMenuItemWithAvatar: Story = { decorators: [WithContentBelowDecorator], args: { - dropdownComponents: ( - - - - ), + dropdownComponents: , }, play: playInteraction, }; @@ -354,11 +353,9 @@ const ModalWithDropdown = () => { dropdownId="modal-dropdown-test" isDropdownInModal={true} dropdownComponents={ - -
- -
-
+
+ +
} /> diff --git a/packages/twenty-front/src/modules/ui/types/CSSWidth.ts b/packages/twenty-front/src/modules/ui/types/CSSWidth.ts new file mode 100644 index 000000000..1f2524b16 --- /dev/null +++ b/packages/twenty-front/src/modules/ui/types/CSSWidth.ts @@ -0,0 +1 @@ +export type CSSWidth = `${number}%` | `${number}px`;