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 <charles@twenty.com>
This commit is contained in:
@ -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 };
|
||||
|
||||
@ -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 };
|
||||
|
||||
@ -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 };
|
||||
|
||||
@ -0,0 +1,8 @@
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
const StyledRecordPickerInitialLoadingEmptyContainer = styled.div`
|
||||
height: 320px;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export { StyledRecordPickerInitialLoadingEmptyContainer as RecordPickerInitialLoadingEmptyContainer };
|
||||
@ -0,0 +1,13 @@
|
||||
import { DropdownMenuSkeletonItem } from '@/ui/input/relation-picker/components/skeletons/DropdownMenuSkeletonItem';
|
||||
|
||||
export const RecordPickerLoadingSkeletonList = () => {
|
||||
return (
|
||||
<>
|
||||
<DropdownMenuSkeletonItem width="53%" />
|
||||
<DropdownMenuSkeletonItem width="35%" />
|
||||
<DropdownMenuSkeletonItem width="48%" />
|
||||
<DropdownMenuSkeletonItem width="67%" />
|
||||
<DropdownMenuSkeletonItem width="75%" />
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -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;
|
||||
|
||||
@ -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 (
|
||||
<div>
|
||||
<>
|
||||
<StyledIntersectionObserver ref={ref} />
|
||||
{isLoading && <StyledText>Loading more...</StyledText>}
|
||||
</div>
|
||||
{multipleRecordPickerIsFetchingMore && (
|
||||
<StyledText>Loading more...</StyledText>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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 (
|
||||
<>
|
||||
<MultipleRecordPickerLoadingEffect />
|
||||
<DropdownMenuSeparator />
|
||||
{isLoading && itemsLength === 0 ? (
|
||||
<DropdownMenuSkeletonItem />
|
||||
) : (
|
||||
<MultipleRecordPickerMenuItems onChange={onChange} focusId={focusId} />
|
||||
)}
|
||||
<MultipleRecordPickerMenuItems onChange={onChange} focusId={focusId} />
|
||||
<DropdownMenuSeparator />
|
||||
</>
|
||||
);
|
||||
|
||||
@ -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;
|
||||
};
|
||||
@ -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 (
|
||||
<DropdownMenuItemsContainer hasMaxHeight>
|
||||
{pickableRecordIds.length === 0 ? (
|
||||
{multipleRecordPickerShouldShowInitialLoading ? (
|
||||
<RecordPickerInitialLoadingEmptyContainer />
|
||||
) : multipleRecordPickerShouldShowSkeleton ? (
|
||||
<RecordPickerLoadingSkeletonList />
|
||||
) : searchHasNoResults ? (
|
||||
<RecordPickerNoRecordFoundMenuItem />
|
||||
) : (
|
||||
<SelectableList
|
||||
|
||||
@ -0,0 +1,40 @@
|
||||
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 { useRecoilCallback } from 'recoil';
|
||||
|
||||
export const useMultipleRecordPickerOpen = () => {
|
||||
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,
|
||||
};
|
||||
};
|
||||
@ -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<boolean>({
|
||||
key: 'multipleRecordPickerIsFetchingMoreComponentState',
|
||||
defaultValue: false,
|
||||
componentInstanceContext: MultipleRecordPickerComponentInstanceContext,
|
||||
});
|
||||
@ -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<boolean>({
|
||||
key: 'multipleRecordPickerShouldShowInitialLoadingComponentState',
|
||||
defaultValue: false,
|
||||
componentInstanceContext: MultipleRecordPickerComponentInstanceContext,
|
||||
});
|
||||
@ -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<boolean>({
|
||||
key: 'multipleRecordPickerShouldShowSkeletonComponentState',
|
||||
defaultValue: false,
|
||||
componentInstanceContext: MultipleRecordPickerComponentInstanceContext,
|
||||
});
|
||||
@ -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;
|
||||
};
|
||||
@ -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 (
|
||||
<SelectableList
|
||||
selectableListInstanceId={selectableListComponentInstanceId}
|
||||
@ -100,49 +104,42 @@ export const SingleRecordPickerMenuItems = ({
|
||||
hotkeyScope={DropdownHotkeyScope.Dropdown}
|
||||
focusId={focusId}
|
||||
>
|
||||
{loading ? (
|
||||
<DropdownMenuSkeletonItem />
|
||||
) : (
|
||||
recordsInDropdown?.map((record) => {
|
||||
switch (record.id) {
|
||||
case 'select-none': {
|
||||
return (
|
||||
emptyLabel && (
|
||||
<SelectableListItem
|
||||
key={record.id}
|
||||
itemId={record.id}
|
||||
onEnter={() => {
|
||||
setSelectedRecordId(undefined);
|
||||
onRecordSelected();
|
||||
}}
|
||||
>
|
||||
<MenuItemSelect
|
||||
onClick={() => {
|
||||
setSelectedRecordId(undefined);
|
||||
onRecordSelected();
|
||||
}}
|
||||
LeftIcon={EmptyIcon}
|
||||
text={emptyLabel}
|
||||
selected={isUndefined(selectedRecordId)}
|
||||
focused={isSelectedSelectNoneButton}
|
||||
/>
|
||||
</SelectableListItem>
|
||||
)
|
||||
);
|
||||
}
|
||||
default: {
|
||||
return (
|
||||
<SingleRecordPickerMenuItem
|
||||
key={record.id}
|
||||
record={record}
|
||||
onRecordSelected={onRecordSelected}
|
||||
selectedRecord={selectedRecord}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
})
|
||||
{emptyLabel && (
|
||||
<SelectableListItem
|
||||
key={'select-none'}
|
||||
itemId={'select-none'}
|
||||
onEnter={() => {
|
||||
setSelectedRecordId(undefined);
|
||||
onRecordSelected();
|
||||
}}
|
||||
>
|
||||
<MenuItemSelect
|
||||
onClick={() => {
|
||||
setSelectedRecordId(undefined);
|
||||
onRecordSelected();
|
||||
}}
|
||||
LeftIcon={EmptyIcon}
|
||||
text={emptyLabel}
|
||||
selected={isUndefined(selectedRecordId)}
|
||||
focused={isSelectedSelectNoneButton}
|
||||
/>
|
||||
</SelectableListItem>
|
||||
)}
|
||||
{singleRecordPickerShouldShowInitialLoading ? (
|
||||
<RecordPickerInitialLoadingEmptyContainer />
|
||||
) : singleRecordPickerShouldShowSkeleton ? (
|
||||
<RecordPickerLoadingSkeletonList />
|
||||
) : (
|
||||
recordsInDropdown?.map((record) => (
|
||||
<SingleRecordPickerMenuItem
|
||||
key={record.id}
|
||||
record={record}
|
||||
onRecordSelected={onRecordSelected}
|
||||
selectedRecord={selectedRecord}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
{searchHasNoResults && <RecordPickerNoRecordFoundMenuItem />}
|
||||
</SelectableList>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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 (
|
||||
<>
|
||||
<SingleRecordPickerLoadingEffect loading={records.loading} />
|
||||
{layoutDirection === 'search-bar-on-bottom' && (
|
||||
<>
|
||||
{isDefined(onCreate) && hasObjectUpdatePermissions && (
|
||||
@ -101,12 +95,11 @@ export const SingleRecordPickerMenuItemsWithSearch = ({
|
||||
)}
|
||||
|
||||
<DropdownMenuItemsContainer hasMaxHeight>
|
||||
{searchHasNoResults && <RecordPickerNoRecordFoundMenuItem />}
|
||||
<SingleRecordPickerMenuItems
|
||||
focusId={focusId}
|
||||
recordsToSelect={records.recordsToSelect}
|
||||
loading={records.loading}
|
||||
selectedRecord={records.selectedRecords?.[0]}
|
||||
filteredSelectedRecords={records.filteredSelectedRecords}
|
||||
{...{
|
||||
EmptyIcon,
|
||||
emptyLabel,
|
||||
@ -130,8 +123,8 @@ export const SingleRecordPickerMenuItemsWithSearch = ({
|
||||
<SingleRecordPickerMenuItems
|
||||
focusId={focusId}
|
||||
recordsToSelect={records.recordsToSelect}
|
||||
loading={records.loading}
|
||||
selectedRecord={records.selectedRecords?.[0]}
|
||||
filteredSelectedRecords={records.filteredSelectedRecords}
|
||||
{...{
|
||||
EmptyIcon,
|
||||
emptyLabel,
|
||||
@ -139,7 +132,6 @@ export const SingleRecordPickerMenuItemsWithSearch = ({
|
||||
onRecordSelected,
|
||||
}}
|
||||
/>
|
||||
{searchHasNoResults && <RecordPickerNoRecordFoundMenuItem />}
|
||||
</DropdownMenuItemsContainer>
|
||||
{isDefined(onCreate) && hasObjectUpdatePermissions && (
|
||||
<>
|
||||
|
||||
@ -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,
|
||||
};
|
||||
};
|
||||
@ -23,6 +23,7 @@ export const useSingleRecordPickerSearch = (
|
||||
singleRecordPickerSelectedIdComponentState,
|
||||
recordPickerComponentInstanceId,
|
||||
);
|
||||
|
||||
const debouncedSetSearchFilter = useDebouncedCallback(
|
||||
setRecordPickerSearchFilter,
|
||||
100,
|
||||
|
||||
@ -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<boolean>({
|
||||
key: 'singleRecordPickerShouldShowInitialLoadingComponentState',
|
||||
defaultValue: false,
|
||||
componentInstanceContext: SingleRecordPickerComponentInstanceContext,
|
||||
});
|
||||
@ -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<boolean>({
|
||||
key: 'singleRecordPickerShouldShowSkeletonComponentState',
|
||||
defaultValue: false,
|
||||
componentInstanceContext: SingleRecordPickerComponentInstanceContext,
|
||||
});
|
||||
@ -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: '',
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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 (
|
||||
<StyledDropdownMenuSkeletonContainer>
|
||||
@ -22,7 +32,11 @@ export const DropdownMenuSkeletonItem = () => {
|
||||
baseColor={theme.background.quaternary}
|
||||
highlightColor={theme.background.secondary}
|
||||
>
|
||||
<Skeleton height={SKELETON_LOADER_HEIGHT_SIZES.standard.s} />
|
||||
<Skeleton
|
||||
height={SKELETON_LOADER_HEIGHT_SIZES.standard.s}
|
||||
style={{ lineHeight: 0 }}
|
||||
width={width}
|
||||
/>
|
||||
</SkeletonTheme>
|
||||
</StyledDropdownMenuSkeletonContainer>
|
||||
);
|
||||
|
||||
@ -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: (
|
||||
<StyledEmptyDropdownContent data-testid="dropdown-content" />
|
||||
<DropdownContent>
|
||||
<StyledEmptyDropdownContent data-testid="dropdown-content" />
|
||||
</DropdownContent>
|
||||
),
|
||||
},
|
||||
play: async () => {
|
||||
@ -155,26 +158,28 @@ const FakeSelectableMenuItemList = ({ hasAvatar }: { hasAvatar?: boolean }) => {
|
||||
const [selectedItem, setSelectedItem] = useState<string | null>(null);
|
||||
|
||||
return (
|
||||
<>
|
||||
{optionsMock.map((item) => (
|
||||
<MenuItemSelectAvatar
|
||||
key={item.id}
|
||||
selected={selectedItem === item.id}
|
||||
onClick={() => setSelectedItem(item.id)}
|
||||
avatar={
|
||||
hasAvatar ? (
|
||||
<Avatar
|
||||
placeholder="A"
|
||||
avatarUrl={item.avatarUrl}
|
||||
size="md"
|
||||
type="squared"
|
||||
/>
|
||||
) : undefined
|
||||
}
|
||||
text={item.name}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
<DropdownContent>
|
||||
<DropdownMenuItemsContainer hasMaxHeight>
|
||||
{optionsMock.map((item) => (
|
||||
<MenuItemSelectAvatar
|
||||
key={item.id}
|
||||
selected={selectedItem === item.id}
|
||||
onClick={() => setSelectedItem(item.id)}
|
||||
avatar={
|
||||
hasAvatar ? (
|
||||
<Avatar
|
||||
placeholder="A"
|
||||
avatarUrl={item.avatarUrl}
|
||||
size="md"
|
||||
type="squared"
|
||||
/>
|
||||
) : undefined
|
||||
}
|
||||
text={item.name}
|
||||
/>
|
||||
))}
|
||||
</DropdownMenuItemsContainer>
|
||||
</DropdownContent>
|
||||
);
|
||||
};
|
||||
|
||||
@ -184,31 +189,33 @@ const FakeCheckableMenuItemList = ({ hasAvatar }: { hasAvatar?: boolean }) => {
|
||||
>({});
|
||||
|
||||
return (
|
||||
<>
|
||||
{optionsMock.map((item) => (
|
||||
<MenuItemMultiSelectAvatar
|
||||
key={item.id}
|
||||
selected={selectedItemsById[item.id]}
|
||||
onSelectChange={(checked) =>
|
||||
setSelectedItemsById((previous) => ({
|
||||
...previous,
|
||||
[item.id]: checked,
|
||||
}))
|
||||
}
|
||||
avatar={
|
||||
hasAvatar ? (
|
||||
<Avatar
|
||||
placeholder="A"
|
||||
avatarUrl={item.avatarUrl}
|
||||
size="md"
|
||||
type="squared"
|
||||
/>
|
||||
) : undefined
|
||||
}
|
||||
text={item.name}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
<DropdownContent>
|
||||
<DropdownMenuItemsContainer hasMaxHeight>
|
||||
{optionsMock.map((item) => (
|
||||
<MenuItemMultiSelectAvatar
|
||||
key={item.id}
|
||||
selected={selectedItemsById[item.id]}
|
||||
onSelectChange={(checked) =>
|
||||
setSelectedItemsById((previous) => ({
|
||||
...previous,
|
||||
[item.id]: checked,
|
||||
}))
|
||||
}
|
||||
avatar={
|
||||
hasAvatar ? (
|
||||
<Avatar
|
||||
placeholder="A"
|
||||
avatarUrl={item.avatarUrl}
|
||||
size="md"
|
||||
type="squared"
|
||||
/>
|
||||
) : undefined
|
||||
}
|
||||
text={item.name}
|
||||
/>
|
||||
))}
|
||||
</DropdownMenuItemsContainer>
|
||||
</DropdownContent>
|
||||
);
|
||||
};
|
||||
|
||||
@ -227,7 +234,7 @@ export const WithHeaders: Story = {
|
||||
decorators: [WithContentBelowDecorator],
|
||||
args: {
|
||||
dropdownComponents: (
|
||||
<>
|
||||
<DropdownContent>
|
||||
<DropdownMenuHeader
|
||||
StartComponent={
|
||||
<DropdownMenuHeaderLeftComponent Icon={IconChevronLeft} />
|
||||
@ -250,7 +257,7 @@ export const WithHeaders: Story = {
|
||||
<MenuItem key={item.id} text={item.name} />
|
||||
))}
|
||||
</DropdownMenuItemsContainer>
|
||||
</>
|
||||
</DropdownContent>
|
||||
),
|
||||
},
|
||||
play: playInteraction,
|
||||
@ -260,13 +267,13 @@ export const SearchWithLoadingMenu: Story = {
|
||||
decorators: [WithContentBelowDecorator],
|
||||
args: {
|
||||
dropdownComponents: (
|
||||
<>
|
||||
<DropdownContent>
|
||||
<DropdownMenuSearchInput value="query" autoFocus />
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItemsContainer hasMaxHeight>
|
||||
<DropdownMenuSkeletonItem />
|
||||
</DropdownMenuItemsContainer>
|
||||
</>
|
||||
</DropdownContent>
|
||||
),
|
||||
},
|
||||
play: async () => {
|
||||
@ -292,7 +299,7 @@ export const WithInput: Story = {
|
||||
decorators: [WithContentBelowDecorator],
|
||||
args: {
|
||||
dropdownComponents: (
|
||||
<>
|
||||
<DropdownContent>
|
||||
<DropdownMenuInput value="Lorem ipsum" autoFocus />
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItemsContainer hasMaxHeight>
|
||||
@ -300,7 +307,7 @@ export const WithInput: Story = {
|
||||
<MenuItem key={name} text={name} />
|
||||
))}
|
||||
</DropdownMenuItemsContainer>
|
||||
</>
|
||||
</DropdownContent>
|
||||
),
|
||||
},
|
||||
play: playInteraction,
|
||||
@ -309,11 +316,7 @@ export const WithInput: Story = {
|
||||
export const SelectableMenuItemWithAvatar: Story = {
|
||||
decorators: [WithContentBelowDecorator],
|
||||
args: {
|
||||
dropdownComponents: (
|
||||
<DropdownMenuItemsContainer hasMaxHeight>
|
||||
<FakeSelectableMenuItemList hasAvatar />
|
||||
</DropdownMenuItemsContainer>
|
||||
),
|
||||
dropdownComponents: <FakeSelectableMenuItemList hasAvatar />,
|
||||
},
|
||||
play: playInteraction,
|
||||
};
|
||||
@ -321,11 +324,7 @@ export const SelectableMenuItemWithAvatar: Story = {
|
||||
export const CheckableMenuItemWithAvatar: Story = {
|
||||
decorators: [WithContentBelowDecorator],
|
||||
args: {
|
||||
dropdownComponents: (
|
||||
<DropdownMenuItemsContainer hasMaxHeight>
|
||||
<FakeCheckableMenuItemList hasAvatar />
|
||||
</DropdownMenuItemsContainer>
|
||||
),
|
||||
dropdownComponents: <FakeCheckableMenuItemList hasAvatar />,
|
||||
},
|
||||
play: playInteraction,
|
||||
};
|
||||
@ -354,11 +353,9 @@ const ModalWithDropdown = () => {
|
||||
dropdownId="modal-dropdown-test"
|
||||
isDropdownInModal={true}
|
||||
dropdownComponents={
|
||||
<DropdownMenuItemsContainer hasMaxHeight>
|
||||
<div data-testid="dropdown-content">
|
||||
<FakeSelectableMenuItemList hasAvatar />
|
||||
</div>
|
||||
</DropdownMenuItemsContainer>
|
||||
<div data-testid="dropdown-content">
|
||||
<FakeSelectableMenuItemList hasAvatar />
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
1
packages/twenty-front/src/modules/ui/types/CSSWidth.ts
Normal file
1
packages/twenty-front/src/modules/ui/types/CSSWidth.ts
Normal file
@ -0,0 +1 @@
|
||||
export type CSSWidth = `${number}%` | `${number}px`;
|
||||
Reference in New Issue
Block a user