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 { ActivityTargetWithTargetRecord } from '@/activities/types/ActivityTargetObject';
|
||||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
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 { useMultipleRecordPickerPerformSearch } from '@/object-record/record-picker/multiple-record-picker/hooks/useMultipleRecordPickerPerformSearch';
|
||||||
import { multipleRecordPickerPickableMorphItemsComponentState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerPickableMorphItemsComponentState';
|
import { multipleRecordPickerPickableMorphItemsComponentState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerPickableMorphItemsComponentState';
|
||||||
import { multipleRecordPickerSearchFilterComponentState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerSearchFilterComponentState';
|
import { multipleRecordPickerSearchFilterComponentState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerSearchFilterComponentState';
|
||||||
@ -19,6 +20,7 @@ type OpenActivityTargetCellEditModeProps = {
|
|||||||
export const useOpenActivityTargetCellEditMode = () => {
|
export const useOpenActivityTargetCellEditMode = () => {
|
||||||
const { performSearch: multipleRecordPickerPerformSearch } =
|
const { performSearch: multipleRecordPickerPerformSearch } =
|
||||||
useMultipleRecordPickerPerformSearch();
|
useMultipleRecordPickerPerformSearch();
|
||||||
|
const { openMultipleRecordPicker } = useMultipleRecordPickerOpen();
|
||||||
|
|
||||||
const { pushFocusItemToFocusStack } = usePushFocusItemToFocusStack();
|
const { pushFocusItemToFocusStack } = usePushFocusItemToFocusStack();
|
||||||
|
|
||||||
@ -70,6 +72,8 @@ export const useOpenActivityTargetCellEditMode = () => {
|
|||||||
'',
|
'',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
openMultipleRecordPicker(recordPickerInstanceId);
|
||||||
|
|
||||||
multipleRecordPickerPerformSearch({
|
multipleRecordPickerPerformSearch({
|
||||||
multipleRecordPickerInstanceId: recordPickerInstanceId,
|
multipleRecordPickerInstanceId: recordPickerInstanceId,
|
||||||
forceSearchFilter: '',
|
forceSearchFilter: '',
|
||||||
@ -97,7 +101,11 @@ export const useOpenActivityTargetCellEditMode = () => {
|
|||||||
memoizeKey: recordPickerInstanceId,
|
memoizeKey: recordPickerInstanceId,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[multipleRecordPickerPerformSearch, pushFocusItemToFocusStack],
|
[
|
||||||
|
multipleRecordPickerPerformSearch,
|
||||||
|
openMultipleRecordPicker,
|
||||||
|
pushFocusItemToFocusStack,
|
||||||
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
return { openActivityTargetCellEditMode };
|
return { openActivityTargetCellEditMode };
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import {
|
|||||||
FieldRelationFromManyValue,
|
FieldRelationFromManyValue,
|
||||||
FieldRelationValue,
|
FieldRelationValue,
|
||||||
} from '@/object-record/record-field/types/FieldMetadata';
|
} 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 { useMultipleRecordPickerPerformSearch } from '@/object-record/record-picker/multiple-record-picker/hooks/useMultipleRecordPickerPerformSearch';
|
||||||
import { multipleRecordPickerPickableMorphItemsComponentState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerPickableMorphItemsComponentState';
|
import { multipleRecordPickerPickableMorphItemsComponentState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerPickableMorphItemsComponentState';
|
||||||
import { multipleRecordPickerSearchableObjectMetadataItemsComponentState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerSearchableObjectMetadataItemsComponentState';
|
import { multipleRecordPickerSearchableObjectMetadataItemsComponentState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerSearchableObjectMetadataItemsComponentState';
|
||||||
@ -17,6 +18,7 @@ import { useRecoilCallback } from 'recoil';
|
|||||||
|
|
||||||
export const useOpenRelationFromManyFieldInput = () => {
|
export const useOpenRelationFromManyFieldInput = () => {
|
||||||
const { performSearch } = useMultipleRecordPickerPerformSearch();
|
const { performSearch } = useMultipleRecordPickerPerformSearch();
|
||||||
|
const { openMultipleRecordPicker } = useMultipleRecordPickerOpen();
|
||||||
|
|
||||||
const { pushFocusItemToFocusStack } = usePushFocusItemToFocusStack();
|
const { pushFocusItemToFocusStack } = usePushFocusItemToFocusStack();
|
||||||
|
|
||||||
@ -58,6 +60,8 @@ export const useOpenRelationFromManyFieldInput = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
openMultipleRecordPicker(recordPickerInstanceId);
|
||||||
|
|
||||||
const pickableMorphItems: RecordPickerPickableMorphItem[] =
|
const pickableMorphItems: RecordPickerPickableMorphItem[] =
|
||||||
fieldValue.map((record) => {
|
fieldValue.map((record) => {
|
||||||
return {
|
return {
|
||||||
@ -105,7 +109,7 @@ export const useOpenRelationFromManyFieldInput = () => {
|
|||||||
memoizeKey: recordPickerInstanceId,
|
memoizeKey: recordPickerInstanceId,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[performSearch, pushFocusItemToFocusStack],
|
[openMultipleRecordPicker, performSearch, pushFocusItemToFocusStack],
|
||||||
);
|
);
|
||||||
|
|
||||||
return { openRelationFromManyFieldInput };
|
return { openRelationFromManyFieldInput };
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import {
|
|||||||
FieldRelationToOneValue,
|
FieldRelationToOneValue,
|
||||||
FieldRelationValue,
|
FieldRelationValue,
|
||||||
} from '@/object-record/record-field/types/FieldMetadata';
|
} 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 { singleRecordPickerSelectedIdComponentState } from '@/object-record/record-picker/single-record-picker/states/singleRecordPickerSelectedIdComponentState';
|
||||||
import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector';
|
import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector';
|
||||||
import { DropdownHotkeyScope } from '@/ui/layout/dropdown/constants/DropdownHotkeyScope';
|
import { DropdownHotkeyScope } from '@/ui/layout/dropdown/constants/DropdownHotkeyScope';
|
||||||
@ -13,6 +14,7 @@ import { isDefined } from 'twenty-shared/utils';
|
|||||||
|
|
||||||
export const useOpenRelationToOneFieldInput = () => {
|
export const useOpenRelationToOneFieldInput = () => {
|
||||||
const { pushFocusItemToFocusStack } = usePushFocusItemToFocusStack();
|
const { pushFocusItemToFocusStack } = usePushFocusItemToFocusStack();
|
||||||
|
const { openSingleRecordPicker } = useSingleRecordPickerOpen();
|
||||||
|
|
||||||
const openRelationToOneFieldInput = useRecoilCallback(
|
const openRelationToOneFieldInput = useRecoilCallback(
|
||||||
({ set, snapshot }) =>
|
({ set, snapshot }) =>
|
||||||
@ -39,6 +41,8 @@ export const useOpenRelationToOneFieldInput = () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
openSingleRecordPicker(recordPickerInstanceId);
|
||||||
|
|
||||||
pushFocusItemToFocusStack({
|
pushFocusItemToFocusStack({
|
||||||
focusId: recordPickerInstanceId,
|
focusId: recordPickerInstanceId,
|
||||||
component: {
|
component: {
|
||||||
@ -50,7 +54,7 @@ export const useOpenRelationToOneFieldInput = () => {
|
|||||||
memoizeKey: recordPickerInstanceId,
|
memoizeKey: recordPickerInstanceId,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[pushFocusItemToFocusStack],
|
[openSingleRecordPicker, pushFocusItemToFocusStack],
|
||||||
);
|
);
|
||||||
|
|
||||||
return { openRelationToOneFieldInput };
|
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 { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
|
||||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||||
import { DropdownHotkeyScope } from '@/ui/layout/dropdown/constants/DropdownHotkeyScope';
|
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 { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList';
|
||||||
import { useHotkeysOnFocusedElement } from '@/ui/utilities/hotkey/hooks/useHotkeysOnFocusedElement';
|
import { useHotkeysOnFocusedElement } from '@/ui/utilities/hotkey/hooks/useHotkeysOnFocusedElement';
|
||||||
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
|
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
|
||||||
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
|
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
|
||||||
import styled from '@emotion/styled';
|
|
||||||
import { useRef } from 'react';
|
import { useRef } from 'react';
|
||||||
import { useRecoilCallback } from 'recoil';
|
import { useRecoilCallback } from 'recoil';
|
||||||
import { Key } from 'ts-key-enum';
|
import { Key } from 'ts-key-enum';
|
||||||
import { isDefined } from 'twenty-shared/utils';
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
import { IconPlus } from 'twenty-ui/display';
|
import { IconPlus } from 'twenty-ui/display';
|
||||||
|
|
||||||
export const StyledSelectableItem = styled(SelectableListItem)`
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
`;
|
|
||||||
|
|
||||||
type MultipleRecordPickerProps = {
|
type MultipleRecordPickerProps = {
|
||||||
onChange?: (morphItem: RecordPickerPickableMorphItem) => void;
|
onChange?: (morphItem: RecordPickerPickableMorphItem) => void;
|
||||||
onSubmit?: () => void;
|
onSubmit?: () => void;
|
||||||
|
|||||||
@ -1,10 +1,16 @@
|
|||||||
import { useMultipleRecordPickerPerformSearch } from '@/object-record/record-picker/multiple-record-picker/hooks/useMultipleRecordPickerPerformSearch';
|
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 { 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 { multipleRecordPickerIsLoadingComponentState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerIsLoadingComponentState';
|
||||||
|
|
||||||
import { multipleRecordPickerPaginationState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerPaginationState';
|
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 { 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 { multipleRecordPickerPaginationSelector } from '@/object-record/record-picker/multiple-record-picker/states/selectors/multipleRecordPickerPaginationSelector';
|
||||||
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
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 { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
@ -23,10 +29,17 @@ const StyledText = styled.div`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledIntersectionObserver = styled.div`
|
const StyledIntersectionObserver = styled.div`
|
||||||
height: 1px;
|
height: 0px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const MultipleRecordPickerFetchMoreLoader = () => {
|
export const MultipleRecordPickerFetchMoreLoader = () => {
|
||||||
|
const [
|
||||||
|
multipleRecordPickerIsFetchingMore,
|
||||||
|
setMultipleRecordPickerIsFetchingMore,
|
||||||
|
] = useRecoilComponentStateV2(
|
||||||
|
multipleRecordPickerIsFetchingMoreComponentState,
|
||||||
|
);
|
||||||
|
|
||||||
const componentInstanceId = useAvailableComponentInstanceIdOrThrow(
|
const componentInstanceId = useAvailableComponentInstanceIdOrThrow(
|
||||||
MultipleRecordPickerComponentInstanceContext,
|
MultipleRecordPickerComponentInstanceContext,
|
||||||
);
|
);
|
||||||
@ -46,6 +59,15 @@ export const MultipleRecordPickerFetchMoreLoader = () => {
|
|||||||
componentInstanceId,
|
componentInstanceId,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const multipleRecordPickerShouldShowInitialLoading =
|
||||||
|
useRecoilComponentValueV2(
|
||||||
|
multipleRecordPickerShouldShowInitialLoadingComponentState,
|
||||||
|
);
|
||||||
|
|
||||||
|
const multipleRecordPickerShouldShowSkeleton = useRecoilComponentValueV2(
|
||||||
|
multipleRecordPickerShouldShowSkeletonComponentState,
|
||||||
|
);
|
||||||
|
|
||||||
const { performSearch } = useMultipleRecordPickerPerformSearch();
|
const { performSearch } = useMultipleRecordPickerPerformSearch();
|
||||||
|
|
||||||
const fetchMore = useRecoilCallback(
|
const fetchMore = useRecoilCallback(
|
||||||
@ -63,7 +85,7 @@ export const MultipleRecordPickerFetchMoreLoader = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
performSearch({
|
await performSearch({
|
||||||
multipleRecordPickerInstanceId: componentInstanceId,
|
multipleRecordPickerInstanceId: componentInstanceId,
|
||||||
forceSearchFilter: searchFilter,
|
forceSearchFilter: searchFilter,
|
||||||
loadMore: true,
|
loadMore: true,
|
||||||
@ -74,23 +96,34 @@ export const MultipleRecordPickerFetchMoreLoader = () => {
|
|||||||
|
|
||||||
const { ref } = useInView({
|
const { ref } = useInView({
|
||||||
onChange: useCallback(
|
onChange: useCallback(
|
||||||
(inView: boolean) => {
|
async (inView: boolean) => {
|
||||||
if (inView) {
|
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 null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<>
|
||||||
<StyledIntersectionObserver ref={ref} />
|
<StyledIntersectionObserver ref={ref} />
|
||||||
{isLoading && <StyledText>Loading more...</StyledText>}
|
{multipleRecordPickerIsFetchingMore && (
|
||||||
</div>
|
<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 { 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 { 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 { 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 = ({
|
export const MultipleRecordPickerItemsDisplay = ({
|
||||||
onChange,
|
onChange,
|
||||||
@ -15,28 +10,11 @@ export const MultipleRecordPickerItemsDisplay = ({
|
|||||||
onChange?: (morphItem: RecordPickerPickableMorphItem) => void;
|
onChange?: (morphItem: RecordPickerPickableMorphItem) => void;
|
||||||
focusId: string;
|
focusId: string;
|
||||||
}) => {
|
}) => {
|
||||||
const componentInstanceId = useAvailableComponentInstanceIdOrThrow(
|
|
||||||
MultipleRecordPickerComponentInstanceContext,
|
|
||||||
);
|
|
||||||
|
|
||||||
const isLoading = useRecoilComponentValueV2(
|
|
||||||
multipleRecordPickerIsLoadingComponentState,
|
|
||||||
componentInstanceId,
|
|
||||||
);
|
|
||||||
|
|
||||||
const itemsLength = useRecoilComponentValueV2(
|
|
||||||
multipleRecordPickerPickableMorphItemsLengthComponentSelector,
|
|
||||||
componentInstanceId,
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<MultipleRecordPickerLoadingEffect />
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
{isLoading && itemsLength === 0 ? (
|
<MultipleRecordPickerMenuItems onChange={onChange} focusId={focusId} />
|
||||||
<DropdownMenuSkeletonItem />
|
|
||||||
) : (
|
|
||||||
<MultipleRecordPickerMenuItems onChange={onChange} focusId={focusId} />
|
|
||||||
)}
|
|
||||||
<DropdownMenuSeparator />
|
<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 { RecordPickerNoRecordFoundMenuItem } from '@/object-record/record-picker/components/RecordPickerNoRecordFoundMenuItem';
|
||||||
import { MultipleRecordPickerFetchMoreLoader } from '@/object-record/record-picker/multiple-record-picker/components/MultipleRecordPickerFetchMoreLoader';
|
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 { 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 { 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 { 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 { 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 { getMultipleRecordPickerSelectableListId } from '@/object-record/record-picker/multiple-record-picker/utils/getMultipleRecordPickerSelectableListId';
|
||||||
import { RecordPickerPickableMorphItem } from '@/object-record/record-picker/types/RecordPickerPickableMorphItem';
|
import { RecordPickerPickableMorphItem } from '@/object-record/record-picker/types/RecordPickerPickableMorphItem';
|
||||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||||
import { DropdownHotkeyScope } from '@/ui/layout/dropdown/constants/DropdownHotkeyScope';
|
import { DropdownHotkeyScope } from '@/ui/layout/dropdown/constants/DropdownHotkeyScope';
|
||||||
import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList';
|
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 { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList';
|
||||||
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
||||||
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
|
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
|
||||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||||
import { useRecoilCallback } from 'recoil';
|
import { useRecoilCallback } from 'recoil';
|
||||||
|
|
||||||
export const StyledSelectableItem = styled(SelectableListItem)`
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
`;
|
|
||||||
|
|
||||||
type MultipleRecordPickerMenuItemsProps = {
|
type MultipleRecordPickerMenuItemsProps = {
|
||||||
onChange?: (morphItem: RecordPickerPickableMorphItem) => void;
|
onChange?: (morphItem: RecordPickerPickableMorphItem) => void;
|
||||||
focusId: string;
|
focusId: string;
|
||||||
@ -79,9 +75,24 @@ export const MultipleRecordPickerMenuItems = ({
|
|||||||
[multipleRecordPickerPickableMorphItemsState],
|
[multipleRecordPickerPickableMorphItemsState],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const multipleRecordPickerShouldShowInitialLoading =
|
||||||
|
useRecoilComponentValueV2(
|
||||||
|
multipleRecordPickerShouldShowInitialLoadingComponentState,
|
||||||
|
);
|
||||||
|
|
||||||
|
const multipleRecordPickerShouldShowSkeleton = useRecoilComponentValueV2(
|
||||||
|
multipleRecordPickerShouldShowSkeletonComponentState,
|
||||||
|
);
|
||||||
|
|
||||||
|
const searchHasNoResults = pickableRecordIds.length === 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownMenuItemsContainer hasMaxHeight>
|
<DropdownMenuItemsContainer hasMaxHeight>
|
||||||
{pickableRecordIds.length === 0 ? (
|
{multipleRecordPickerShouldShowInitialLoading ? (
|
||||||
|
<RecordPickerInitialLoadingEmptyContainer />
|
||||||
|
) : multipleRecordPickerShouldShowSkeleton ? (
|
||||||
|
<RecordPickerLoadingSkeletonList />
|
||||||
|
) : searchHasNoResults ? (
|
||||||
<RecordPickerNoRecordFoundMenuItem />
|
<RecordPickerNoRecordFoundMenuItem />
|
||||||
) : (
|
) : (
|
||||||
<SelectableList
|
<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 { isNonEmptyString, isUndefined } from '@sniptt/guards';
|
||||||
import { Key } from 'ts-key-enum';
|
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 { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList';
|
||||||
import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList';
|
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 { 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 { SingleRecordPickerComponentInstanceContext } from '@/object-record/record-picker/single-record-picker/states/contexts/SingleRecordPickerComponentInstanceContext';
|
||||||
import { singleRecordPickerSelectedIdComponentState } from '@/object-record/record-picker/single-record-picker/states/singleRecordPickerSelectedIdComponentState';
|
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 { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
||||||
import { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValueV2';
|
import { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValueV2';
|
||||||
import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2';
|
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 { isDefined } from 'twenty-shared/utils';
|
||||||
import { IconComponent } from 'twenty-ui/display';
|
import { IconComponent } from 'twenty-ui/display';
|
||||||
import { MenuItemSelect } from 'twenty-ui/navigation';
|
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 = {
|
export type SingleRecordPickerMenuItemsProps = {
|
||||||
EmptyIcon?: IconComponent;
|
EmptyIcon?: IconComponent;
|
||||||
emptyLabel?: string;
|
emptyLabel?: string;
|
||||||
recordsToSelect: SingleRecordPickerRecord[];
|
recordsToSelect: SingleRecordPickerRecord[];
|
||||||
loading?: boolean;
|
|
||||||
onCancel?: () => void;
|
onCancel?: () => void;
|
||||||
onRecordSelected: (entity?: SingleRecordPickerRecord) => void;
|
onRecordSelected: (entity?: SingleRecordPickerRecord) => void;
|
||||||
selectedRecord?: SingleRecordPickerRecord;
|
selectedRecord?: SingleRecordPickerRecord;
|
||||||
focusId: string;
|
focusId: string;
|
||||||
|
filteredSelectedRecords: SingleRecordPickerRecord[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SingleRecordPickerMenuItems = ({
|
export const SingleRecordPickerMenuItems = ({
|
||||||
EmptyIcon,
|
EmptyIcon,
|
||||||
emptyLabel,
|
emptyLabel,
|
||||||
recordsToSelect,
|
recordsToSelect,
|
||||||
loading,
|
|
||||||
onCancel,
|
onCancel,
|
||||||
onRecordSelected,
|
onRecordSelected,
|
||||||
|
filteredSelectedRecords,
|
||||||
selectedRecord,
|
selectedRecord,
|
||||||
focusId,
|
focusId,
|
||||||
}: SingleRecordPickerMenuItemsProps) => {
|
}: SingleRecordPickerMenuItemsProps) => {
|
||||||
const selectNone = emptyLabel
|
const recordsInDropdown = [selectedRecord, ...recordsToSelect].filter(
|
||||||
? {
|
|
||||||
__typename: '',
|
|
||||||
id: 'select-none',
|
|
||||||
name: emptyLabel,
|
|
||||||
}
|
|
||||||
: null;
|
|
||||||
|
|
||||||
const recordsInDropdown = [
|
|
||||||
selectNone,
|
|
||||||
selectedRecord,
|
|
||||||
...recordsToSelect,
|
|
||||||
].filter(
|
|
||||||
(entity): entity is SingleRecordPickerRecord =>
|
(entity): entity is SingleRecordPickerRecord =>
|
||||||
isDefined(entity) && isNonEmptyString(entity.name),
|
isDefined(entity) && isNonEmptyString(entity.name),
|
||||||
);
|
);
|
||||||
@ -93,6 +86,17 @@ export const SingleRecordPickerMenuItems = ({
|
|||||||
singleRecordPickerSelectedIdComponentState,
|
singleRecordPickerSelectedIdComponentState,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const singleRecordPickerShouldShowSkeleton = useRecoilComponentValueV2(
|
||||||
|
singleRecordPickerShouldShowSkeletonComponentState,
|
||||||
|
);
|
||||||
|
|
||||||
|
const singleRecordPickerShouldShowInitialLoading = useRecoilComponentValueV2(
|
||||||
|
singleRecordPickerShouldShowInitialLoadingComponentState,
|
||||||
|
);
|
||||||
|
|
||||||
|
const searchHasNoResults =
|
||||||
|
recordsToSelect.length === 0 && filteredSelectedRecords?.length === 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SelectableList
|
<SelectableList
|
||||||
selectableListInstanceId={selectableListComponentInstanceId}
|
selectableListInstanceId={selectableListComponentInstanceId}
|
||||||
@ -100,49 +104,42 @@ export const SingleRecordPickerMenuItems = ({
|
|||||||
hotkeyScope={DropdownHotkeyScope.Dropdown}
|
hotkeyScope={DropdownHotkeyScope.Dropdown}
|
||||||
focusId={focusId}
|
focusId={focusId}
|
||||||
>
|
>
|
||||||
{loading ? (
|
{emptyLabel && (
|
||||||
<DropdownMenuSkeletonItem />
|
<SelectableListItem
|
||||||
) : (
|
key={'select-none'}
|
||||||
recordsInDropdown?.map((record) => {
|
itemId={'select-none'}
|
||||||
switch (record.id) {
|
onEnter={() => {
|
||||||
case 'select-none': {
|
setSelectedRecordId(undefined);
|
||||||
return (
|
onRecordSelected();
|
||||||
emptyLabel && (
|
}}
|
||||||
<SelectableListItem
|
>
|
||||||
key={record.id}
|
<MenuItemSelect
|
||||||
itemId={record.id}
|
onClick={() => {
|
||||||
onEnter={() => {
|
setSelectedRecordId(undefined);
|
||||||
setSelectedRecordId(undefined);
|
onRecordSelected();
|
||||||
onRecordSelected();
|
}}
|
||||||
}}
|
LeftIcon={EmptyIcon}
|
||||||
>
|
text={emptyLabel}
|
||||||
<MenuItemSelect
|
selected={isUndefined(selectedRecordId)}
|
||||||
onClick={() => {
|
focused={isSelectedSelectNoneButton}
|
||||||
setSelectedRecordId(undefined);
|
/>
|
||||||
onRecordSelected();
|
</SelectableListItem>
|
||||||
}}
|
|
||||||
LeftIcon={EmptyIcon}
|
|
||||||
text={emptyLabel}
|
|
||||||
selected={isUndefined(selectedRecordId)}
|
|
||||||
focused={isSelectedSelectNoneButton}
|
|
||||||
/>
|
|
||||||
</SelectableListItem>
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
default: {
|
|
||||||
return (
|
|
||||||
<SingleRecordPickerMenuItem
|
|
||||||
key={record.id}
|
|
||||||
record={record}
|
|
||||||
onRecordSelected={onRecordSelected}
|
|
||||||
selectedRecord={selectedRecord}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
)}
|
)}
|
||||||
|
{singleRecordPickerShouldShowInitialLoading ? (
|
||||||
|
<RecordPickerInitialLoadingEmptyContainer />
|
||||||
|
) : singleRecordPickerShouldShowSkeleton ? (
|
||||||
|
<RecordPickerLoadingSkeletonList />
|
||||||
|
) : (
|
||||||
|
recordsInDropdown?.map((record) => (
|
||||||
|
<SingleRecordPickerMenuItem
|
||||||
|
key={record.id}
|
||||||
|
record={record}
|
||||||
|
onRecordSelected={onRecordSelected}
|
||||||
|
selectedRecord={selectedRecord}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
{searchHasNoResults && <RecordPickerNoRecordFoundMenuItem />}
|
||||||
</SelectableList>
|
</SelectableList>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||||
import { useObjectPermissionsForObject } from '@/object-record/hooks/useObjectPermissionsForObject';
|
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 {
|
import {
|
||||||
SingleRecordPickerMenuItems,
|
SingleRecordPickerMenuItems,
|
||||||
SingleRecordPickerMenuItemsProps,
|
SingleRecordPickerMenuItemsProps,
|
||||||
@ -16,7 +16,6 @@ import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/Dropdow
|
|||||||
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
|
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
|
||||||
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
||||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||||
import { isNonEmptyString } from '@sniptt/guards';
|
|
||||||
import { isDefined } from 'twenty-shared/utils';
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
import { IconPlus } from 'twenty-ui/display';
|
import { IconPlus } from 'twenty-ui/display';
|
||||||
|
|
||||||
@ -73,18 +72,13 @@ export const SingleRecordPickerMenuItemsWithSearch = ({
|
|||||||
|
|
||||||
const hasObjectUpdatePermissions = objectPermissions.canUpdateObjectRecords;
|
const hasObjectUpdatePermissions = objectPermissions.canUpdateObjectRecords;
|
||||||
|
|
||||||
const searchHasNoResults =
|
|
||||||
isNonEmptyString(recordPickerSearchFilter) &&
|
|
||||||
records.recordsToSelect.length === 0 &&
|
|
||||||
records.filteredSelectedRecords.length === 0 &&
|
|
||||||
!records.loading;
|
|
||||||
|
|
||||||
const handleCreateNew = () => {
|
const handleCreateNew = () => {
|
||||||
onCreate?.(recordPickerSearchFilter);
|
onCreate?.(recordPickerSearchFilter);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<SingleRecordPickerLoadingEffect loading={records.loading} />
|
||||||
{layoutDirection === 'search-bar-on-bottom' && (
|
{layoutDirection === 'search-bar-on-bottom' && (
|
||||||
<>
|
<>
|
||||||
{isDefined(onCreate) && hasObjectUpdatePermissions && (
|
{isDefined(onCreate) && hasObjectUpdatePermissions && (
|
||||||
@ -101,12 +95,11 @@ export const SingleRecordPickerMenuItemsWithSearch = ({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<DropdownMenuItemsContainer hasMaxHeight>
|
<DropdownMenuItemsContainer hasMaxHeight>
|
||||||
{searchHasNoResults && <RecordPickerNoRecordFoundMenuItem />}
|
|
||||||
<SingleRecordPickerMenuItems
|
<SingleRecordPickerMenuItems
|
||||||
focusId={focusId}
|
focusId={focusId}
|
||||||
recordsToSelect={records.recordsToSelect}
|
recordsToSelect={records.recordsToSelect}
|
||||||
loading={records.loading}
|
|
||||||
selectedRecord={records.selectedRecords?.[0]}
|
selectedRecord={records.selectedRecords?.[0]}
|
||||||
|
filteredSelectedRecords={records.filteredSelectedRecords}
|
||||||
{...{
|
{...{
|
||||||
EmptyIcon,
|
EmptyIcon,
|
||||||
emptyLabel,
|
emptyLabel,
|
||||||
@ -130,8 +123,8 @@ export const SingleRecordPickerMenuItemsWithSearch = ({
|
|||||||
<SingleRecordPickerMenuItems
|
<SingleRecordPickerMenuItems
|
||||||
focusId={focusId}
|
focusId={focusId}
|
||||||
recordsToSelect={records.recordsToSelect}
|
recordsToSelect={records.recordsToSelect}
|
||||||
loading={records.loading}
|
|
||||||
selectedRecord={records.selectedRecords?.[0]}
|
selectedRecord={records.selectedRecords?.[0]}
|
||||||
|
filteredSelectedRecords={records.filteredSelectedRecords}
|
||||||
{...{
|
{...{
|
||||||
EmptyIcon,
|
EmptyIcon,
|
||||||
emptyLabel,
|
emptyLabel,
|
||||||
@ -139,7 +132,6 @@ export const SingleRecordPickerMenuItemsWithSearch = ({
|
|||||||
onRecordSelected,
|
onRecordSelected,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{searchHasNoResults && <RecordPickerNoRecordFoundMenuItem />}
|
|
||||||
</DropdownMenuItemsContainer>
|
</DropdownMenuItemsContainer>
|
||||||
{isDefined(onCreate) && hasObjectUpdatePermissions && (
|
{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,
|
singleRecordPickerSelectedIdComponentState,
|
||||||
recordPickerComponentInstanceId,
|
recordPickerComponentInstanceId,
|
||||||
);
|
);
|
||||||
|
|
||||||
const debouncedSetSearchFilter = useDebouncedCallback(
|
const debouncedSetSearchFilter = useDebouncedCallback(
|
||||||
setRecordPickerSearchFilter,
|
setRecordPickerSearchFilter,
|
||||||
100,
|
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 { useUpdateRelationFromManyFieldInput } from '@/object-record/record-field/meta-types/input/hooks/useUpdateRelationFromManyFieldInput';
|
||||||
import { FieldRelationMetadata } from '@/object-record/record-field/types/FieldMetadata';
|
import { FieldRelationMetadata } from '@/object-record/record-field/types/FieldMetadata';
|
||||||
import { MultipleRecordPicker } from '@/object-record/record-picker/multiple-record-picker/components/MultipleRecordPicker';
|
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 { useMultipleRecordPickerPerformSearch } from '@/object-record/record-picker/multiple-record-picker/hooks/useMultipleRecordPickerPerformSearch';
|
||||||
import { multipleRecordPickerPickableMorphItemsComponentState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerPickableMorphItemsComponentState';
|
import { multipleRecordPickerPickableMorphItemsComponentState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerPickableMorphItemsComponentState';
|
||||||
import { multipleRecordPickerSearchFilterComponentState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerSearchFilterComponentState';
|
import { multipleRecordPickerSearchFilterComponentState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerSearchFilterComponentState';
|
||||||
@ -71,6 +72,8 @@ export const RecordDetailRelationSectionDropdownToMany = () => {
|
|||||||
const { performSearch: multipleRecordPickerPerformSearch } =
|
const { performSearch: multipleRecordPickerPerformSearch } =
|
||||||
useMultipleRecordPickerPerformSearch();
|
useMultipleRecordPickerPerformSearch();
|
||||||
|
|
||||||
|
const { openMultipleRecordPicker } = useMultipleRecordPickerOpen();
|
||||||
|
|
||||||
const handleCloseRelationPickerDropdown = useCallback(() => {
|
const handleCloseRelationPickerDropdown = useCallback(() => {
|
||||||
setMultipleRecordPickerSearchFilter('');
|
setMultipleRecordPickerSearchFilter('');
|
||||||
}, [setMultipleRecordPickerSearchFilter]);
|
}, [setMultipleRecordPickerSearchFilter]);
|
||||||
@ -99,6 +102,8 @@ export const RecordDetailRelationSectionDropdownToMany = () => {
|
|||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
openMultipleRecordPicker(dropdownId);
|
||||||
|
|
||||||
multipleRecordPickerPerformSearch({
|
multipleRecordPickerPerformSearch({
|
||||||
multipleRecordPickerInstanceId: dropdownId,
|
multipleRecordPickerInstanceId: dropdownId,
|
||||||
forceSearchFilter: '',
|
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 { useAddNewRecordAndOpenRightDrawer } from '@/object-record/record-field/meta-types/input/hooks/useAddNewRecordAndOpenRightDrawer';
|
||||||
import { FieldRelationMetadata } from '@/object-record/record-field/types/FieldMetadata';
|
import { FieldRelationMetadata } from '@/object-record/record-field/types/FieldMetadata';
|
||||||
import { SingleRecordPicker } from '@/object-record/record-picker/single-record-picker/components/SingleRecordPicker';
|
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 { singleRecordPickerSearchFilterComponentState } from '@/object-record/record-picker/single-record-picker/states/singleRecordPickerSearchFilterComponentState';
|
||||||
import { singleRecordPickerSelectedIdComponentState } from '@/object-record/record-picker/single-record-picker/states/singleRecordPickerSelectedIdComponentState';
|
import { singleRecordPickerSelectedIdComponentState } from '@/object-record/record-picker/single-record-picker/states/singleRecordPickerSelectedIdComponentState';
|
||||||
import { SingleRecordPickerRecord } from '@/object-record/record-picker/single-record-picker/types/SingleRecordPickerRecord';
|
import { SingleRecordPickerRecord } from '@/object-record/record-picker/single-record-picker/types/SingleRecordPickerRecord';
|
||||||
@ -87,8 +88,12 @@ export const RecordDetailRelationSectionDropdownToOne = () => {
|
|||||||
recordId,
|
recordId,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { openSingleRecordPicker } = useSingleRecordPickerOpen();
|
||||||
|
|
||||||
const handleOpenRelationPickerDropdown = () => {
|
const handleOpenRelationPickerDropdown = () => {
|
||||||
setSingleRecordPickerSearchFilter('');
|
setSingleRecordPickerSearchFilter('');
|
||||||
|
openSingleRecordPicker(dropdownId);
|
||||||
|
|
||||||
if (relationRecords.length > 0) {
|
if (relationRecords.length > 0) {
|
||||||
setSingleRecordPickerSelectedId(relationRecords[0]?.id);
|
setSingleRecordPickerSelectedId(relationRecords[0]?.id);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { SKELETON_LOADER_HEIGHT_SIZES } from '@/activities/components/SkeletonLoader';
|
import { SKELETON_LOADER_HEIGHT_SIZES } from '@/activities/components/SkeletonLoader';
|
||||||
|
import { CSSWidth } from '@/ui/types/CSSWidth';
|
||||||
import { useTheme } from '@emotion/react';
|
import { useTheme } from '@emotion/react';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import Skeleton, { SkeletonTheme } from 'react-loading-skeleton';
|
import Skeleton, { SkeletonTheme } from 'react-loading-skeleton';
|
||||||
@ -6,15 +7,24 @@ import Skeleton, { SkeletonTheme } from 'react-loading-skeleton';
|
|||||||
const StyledDropdownMenuSkeletonContainer = styled.div`
|
const StyledDropdownMenuSkeletonContainer = styled.div`
|
||||||
--horizontal-padding: ${({ theme }) => theme.spacing(1)};
|
--horizontal-padding: ${({ theme }) => theme.spacing(1)};
|
||||||
--vertical-padding: ${({ theme }) => theme.spacing(2)};
|
--vertical-padding: ${({ theme }) => theme.spacing(2)};
|
||||||
align-items: center;
|
|
||||||
border-radius: ${({ theme }) => theme.border.radius.sm};
|
border-radius: ${({ theme }) => theme.border.radius.sm};
|
||||||
gap: ${({ theme }) => theme.spacing(2)};
|
gap: ${({ theme }) => theme.spacing(2)};
|
||||||
height: calc(32px - 2 * var(--vertical-padding));
|
box-sizing: border-box;
|
||||||
padding: var(--vertical-padding) var(--horizontal-padding);
|
|
||||||
width: calc(100% - 2 * var(--horizontal-padding));
|
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();
|
const theme = useTheme();
|
||||||
return (
|
return (
|
||||||
<StyledDropdownMenuSkeletonContainer>
|
<StyledDropdownMenuSkeletonContainer>
|
||||||
@ -22,7 +32,11 @@ export const DropdownMenuSkeletonItem = () => {
|
|||||||
baseColor={theme.background.quaternary}
|
baseColor={theme.background.quaternary}
|
||||||
highlightColor={theme.background.secondary}
|
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>
|
</SkeletonTheme>
|
||||||
</StyledDropdownMenuSkeletonContainer>
|
</StyledDropdownMenuSkeletonContainer>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import { useState } from 'react';
|
|||||||
|
|
||||||
import { DropdownMenuSkeletonItem } from '@/ui/input/relation-picker/components/skeletons/DropdownMenuSkeletonItem';
|
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 { DropdownMenuHeaderLeftComponent } from '@/ui/layout/dropdown/components/DropdownMenuHeader/internal/DropdownMenuHeaderLeftComponent';
|
||||||
import { Modal } from '@/ui/layout/modal/components/Modal';
|
import { Modal } from '@/ui/layout/modal/components/Modal';
|
||||||
import { ModalHotkeyScope } from '@/ui/layout/modal/components/types/ModalHotkeyScope';
|
import { ModalHotkeyScope } from '@/ui/layout/modal/components/types/ModalHotkeyScope';
|
||||||
@ -84,7 +85,9 @@ const StyledEmptyDropdownContent = styled.div`
|
|||||||
export const Empty: Story = {
|
export const Empty: Story = {
|
||||||
args: {
|
args: {
|
||||||
dropdownComponents: (
|
dropdownComponents: (
|
||||||
<StyledEmptyDropdownContent data-testid="dropdown-content" />
|
<DropdownContent>
|
||||||
|
<StyledEmptyDropdownContent data-testid="dropdown-content" />
|
||||||
|
</DropdownContent>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
play: async () => {
|
play: async () => {
|
||||||
@ -155,26 +158,28 @@ const FakeSelectableMenuItemList = ({ hasAvatar }: { hasAvatar?: boolean }) => {
|
|||||||
const [selectedItem, setSelectedItem] = useState<string | null>(null);
|
const [selectedItem, setSelectedItem] = useState<string | null>(null);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<DropdownContent>
|
||||||
{optionsMock.map((item) => (
|
<DropdownMenuItemsContainer hasMaxHeight>
|
||||||
<MenuItemSelectAvatar
|
{optionsMock.map((item) => (
|
||||||
key={item.id}
|
<MenuItemSelectAvatar
|
||||||
selected={selectedItem === item.id}
|
key={item.id}
|
||||||
onClick={() => setSelectedItem(item.id)}
|
selected={selectedItem === item.id}
|
||||||
avatar={
|
onClick={() => setSelectedItem(item.id)}
|
||||||
hasAvatar ? (
|
avatar={
|
||||||
<Avatar
|
hasAvatar ? (
|
||||||
placeholder="A"
|
<Avatar
|
||||||
avatarUrl={item.avatarUrl}
|
placeholder="A"
|
||||||
size="md"
|
avatarUrl={item.avatarUrl}
|
||||||
type="squared"
|
size="md"
|
||||||
/>
|
type="squared"
|
||||||
) : undefined
|
/>
|
||||||
}
|
) : undefined
|
||||||
text={item.name}
|
}
|
||||||
/>
|
text={item.name}
|
||||||
))}
|
/>
|
||||||
</>
|
))}
|
||||||
|
</DropdownMenuItemsContainer>
|
||||||
|
</DropdownContent>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -184,31 +189,33 @@ const FakeCheckableMenuItemList = ({ hasAvatar }: { hasAvatar?: boolean }) => {
|
|||||||
>({});
|
>({});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<DropdownContent>
|
||||||
{optionsMock.map((item) => (
|
<DropdownMenuItemsContainer hasMaxHeight>
|
||||||
<MenuItemMultiSelectAvatar
|
{optionsMock.map((item) => (
|
||||||
key={item.id}
|
<MenuItemMultiSelectAvatar
|
||||||
selected={selectedItemsById[item.id]}
|
key={item.id}
|
||||||
onSelectChange={(checked) =>
|
selected={selectedItemsById[item.id]}
|
||||||
setSelectedItemsById((previous) => ({
|
onSelectChange={(checked) =>
|
||||||
...previous,
|
setSelectedItemsById((previous) => ({
|
||||||
[item.id]: checked,
|
...previous,
|
||||||
}))
|
[item.id]: checked,
|
||||||
}
|
}))
|
||||||
avatar={
|
}
|
||||||
hasAvatar ? (
|
avatar={
|
||||||
<Avatar
|
hasAvatar ? (
|
||||||
placeholder="A"
|
<Avatar
|
||||||
avatarUrl={item.avatarUrl}
|
placeholder="A"
|
||||||
size="md"
|
avatarUrl={item.avatarUrl}
|
||||||
type="squared"
|
size="md"
|
||||||
/>
|
type="squared"
|
||||||
) : undefined
|
/>
|
||||||
}
|
) : undefined
|
||||||
text={item.name}
|
}
|
||||||
/>
|
text={item.name}
|
||||||
))}
|
/>
|
||||||
</>
|
))}
|
||||||
|
</DropdownMenuItemsContainer>
|
||||||
|
</DropdownContent>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -227,7 +234,7 @@ export const WithHeaders: Story = {
|
|||||||
decorators: [WithContentBelowDecorator],
|
decorators: [WithContentBelowDecorator],
|
||||||
args: {
|
args: {
|
||||||
dropdownComponents: (
|
dropdownComponents: (
|
||||||
<>
|
<DropdownContent>
|
||||||
<DropdownMenuHeader
|
<DropdownMenuHeader
|
||||||
StartComponent={
|
StartComponent={
|
||||||
<DropdownMenuHeaderLeftComponent Icon={IconChevronLeft} />
|
<DropdownMenuHeaderLeftComponent Icon={IconChevronLeft} />
|
||||||
@ -250,7 +257,7 @@ export const WithHeaders: Story = {
|
|||||||
<MenuItem key={item.id} text={item.name} />
|
<MenuItem key={item.id} text={item.name} />
|
||||||
))}
|
))}
|
||||||
</DropdownMenuItemsContainer>
|
</DropdownMenuItemsContainer>
|
||||||
</>
|
</DropdownContent>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
play: playInteraction,
|
play: playInteraction,
|
||||||
@ -260,13 +267,13 @@ export const SearchWithLoadingMenu: Story = {
|
|||||||
decorators: [WithContentBelowDecorator],
|
decorators: [WithContentBelowDecorator],
|
||||||
args: {
|
args: {
|
||||||
dropdownComponents: (
|
dropdownComponents: (
|
||||||
<>
|
<DropdownContent>
|
||||||
<DropdownMenuSearchInput value="query" autoFocus />
|
<DropdownMenuSearchInput value="query" autoFocus />
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuItemsContainer hasMaxHeight>
|
<DropdownMenuItemsContainer hasMaxHeight>
|
||||||
<DropdownMenuSkeletonItem />
|
<DropdownMenuSkeletonItem />
|
||||||
</DropdownMenuItemsContainer>
|
</DropdownMenuItemsContainer>
|
||||||
</>
|
</DropdownContent>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
play: async () => {
|
play: async () => {
|
||||||
@ -292,7 +299,7 @@ export const WithInput: Story = {
|
|||||||
decorators: [WithContentBelowDecorator],
|
decorators: [WithContentBelowDecorator],
|
||||||
args: {
|
args: {
|
||||||
dropdownComponents: (
|
dropdownComponents: (
|
||||||
<>
|
<DropdownContent>
|
||||||
<DropdownMenuInput value="Lorem ipsum" autoFocus />
|
<DropdownMenuInput value="Lorem ipsum" autoFocus />
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuItemsContainer hasMaxHeight>
|
<DropdownMenuItemsContainer hasMaxHeight>
|
||||||
@ -300,7 +307,7 @@ export const WithInput: Story = {
|
|||||||
<MenuItem key={name} text={name} />
|
<MenuItem key={name} text={name} />
|
||||||
))}
|
))}
|
||||||
</DropdownMenuItemsContainer>
|
</DropdownMenuItemsContainer>
|
||||||
</>
|
</DropdownContent>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
play: playInteraction,
|
play: playInteraction,
|
||||||
@ -309,11 +316,7 @@ export const WithInput: Story = {
|
|||||||
export const SelectableMenuItemWithAvatar: Story = {
|
export const SelectableMenuItemWithAvatar: Story = {
|
||||||
decorators: [WithContentBelowDecorator],
|
decorators: [WithContentBelowDecorator],
|
||||||
args: {
|
args: {
|
||||||
dropdownComponents: (
|
dropdownComponents: <FakeSelectableMenuItemList hasAvatar />,
|
||||||
<DropdownMenuItemsContainer hasMaxHeight>
|
|
||||||
<FakeSelectableMenuItemList hasAvatar />
|
|
||||||
</DropdownMenuItemsContainer>
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
play: playInteraction,
|
play: playInteraction,
|
||||||
};
|
};
|
||||||
@ -321,11 +324,7 @@ export const SelectableMenuItemWithAvatar: Story = {
|
|||||||
export const CheckableMenuItemWithAvatar: Story = {
|
export const CheckableMenuItemWithAvatar: Story = {
|
||||||
decorators: [WithContentBelowDecorator],
|
decorators: [WithContentBelowDecorator],
|
||||||
args: {
|
args: {
|
||||||
dropdownComponents: (
|
dropdownComponents: <FakeCheckableMenuItemList hasAvatar />,
|
||||||
<DropdownMenuItemsContainer hasMaxHeight>
|
|
||||||
<FakeCheckableMenuItemList hasAvatar />
|
|
||||||
</DropdownMenuItemsContainer>
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
play: playInteraction,
|
play: playInteraction,
|
||||||
};
|
};
|
||||||
@ -354,11 +353,9 @@ const ModalWithDropdown = () => {
|
|||||||
dropdownId="modal-dropdown-test"
|
dropdownId="modal-dropdown-test"
|
||||||
isDropdownInModal={true}
|
isDropdownInModal={true}
|
||||||
dropdownComponents={
|
dropdownComponents={
|
||||||
<DropdownMenuItemsContainer hasMaxHeight>
|
<div data-testid="dropdown-content">
|
||||||
<div data-testid="dropdown-content">
|
<FakeSelectableMenuItemList hasAvatar />
|
||||||
<FakeSelectableMenuItemList hasAvatar />
|
</div>
|
||||||
</div>
|
|
||||||
</DropdownMenuItemsContainer>
|
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</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