Fixed many dropdown bugs (#8256)

Many dropdown bugs have been fixed, more refactoring is needed.

Dropdown fixed : 
- Filter select
- Sort select
- Visible field select
- Hidden field select
- Multi item picker (phones, links, emails, etc.)
- Phone country select
This commit is contained in:
Lucas Bordeau
2024-11-01 09:23:01 +01:00
committed by GitHub
parent a287edd91b
commit c93d2bcd5e
15 changed files with 276 additions and 182 deletions

View File

@ -5,15 +5,10 @@ import { ObjectFilterOperandSelectAndInput } from '@/object-record/object-filter
import { objectFilterDropdownFilterIsSelectedComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownFilterIsSelectedComponentState';
import { objectFilterDropdownIsSelectingCompositeFieldComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownIsSelectingCompositeFieldComponentState';
import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2';
import styled from '@emotion/styled';
import { useRecoilValue } from 'recoil';
import { MultipleFiltersDropdownFilterOnFilterChangedEffect } from './MultipleFiltersDropdownFilterOnFilterChangedEffect';
import { ObjectFilterDropdownFilterSelect } from './ObjectFilterDropdownFilterSelect';
const StyledContainer = styled.div`
position: relative;
`;
type MultipleFiltersDropdownContentProps = {
filterDropdownId?: string;
};
@ -46,7 +41,7 @@ export const MultipleFiltersDropdownContent = ({
const shoudShowFilterInput = objectFilterDropdownFilterIsSelected;
return (
<StyledContainer>
<>
{shoudShowFilterInput ? (
<ObjectFilterOperandSelectAndInput
filterDropdownId={filterDropdownId}
@ -61,6 +56,6 @@ export const MultipleFiltersDropdownContent = ({
filterDefinitionUsedInDropdown?.type
}
/>
</StyledContainer>
</>
);
};

View File

@ -28,8 +28,13 @@ export const ObjectFilterDropdownFilterInput = ({
const {
filterDefinitionUsedInDropdownState,
selectedOperandInDropdownState,
isObjectFilterDropdownOperandSelectUnfoldedState,
} = useFilterDropdown({ filterDropdownId });
const isObjectFilterDropdownOperandSelectUnfolded = useRecoilValue(
isObjectFilterDropdownOperandSelectUnfoldedState,
);
const filterDefinitionUsedInDropdown = useRecoilValue(
filterDefinitionUsedInDropdownState,
);
@ -53,7 +58,9 @@ export const ObjectFilterDropdownFilterInput = ({
ViewFilterOperand.IsRelative,
].includes(selectedOperandInDropdown);
if (!isDefined(filterDefinitionUsedInDropdown)) {
const shouldHide = isObjectFilterDropdownOperandSelectUnfolded;
if (shouldHide || !isDefined(filterDefinitionUsedInDropdown)) {
return null;
}

View File

@ -8,9 +8,7 @@ const StyledOperandSelectContainer = styled.div`
background: ${({ theme }) => theme.background.secondary};
box-shadow: ${({ theme }) => theme.boxShadow.light};
border-radius: ${({ theme }) => theme.border.radius.md};
left: 10px;
position: absolute;
top: 10px;
width: 100%;
z-index: 1000;
`;

View File

@ -16,6 +16,7 @@ import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownM
import { SelectableItem } from '@/ui/layout/selectable-list/components/SelectableItem';
import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList';
import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList';
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useGetCurrentView } from '@/views/hooks/useGetCurrentView';
import { availableFilterDefinitionsComponentState } from '@/views/states/availableFilterDefinitionsComponentState';
@ -35,7 +36,7 @@ export const StyledInput = styled.input`
margin: 0;
outline: none;
padding: ${({ theme }) => theme.spacing(2)};
height: 19px;
min-height: 19px;
font-family: inherit;
font-size: ${({ theme }) => theme.font.size.sm};
@ -160,43 +161,45 @@ export const ObjectFilterDropdownFilterSelect = ({
setObjectFilterDropdownSearchInput(event.target.value)
}
/>
<SelectableList
hotkeyScope={FiltersHotkeyScope.ObjectFilterDropdownButton}
selectableItemIdArray={selectableListItemIds}
selectableListId={OBJECT_FILTER_DROPDOWN_ID}
onEnter={handleEnter}
>
<DropdownMenuItemsContainer>
{visibleColumnsFilterDefinitions.map(
(visibleFilterDefinition, index) => (
<SelectableItem
itemId={visibleFilterDefinition.fieldMetadataId}
key={`visible-select-filter-${index}`}
>
<ObjectFilterDropdownFilterSelectMenuItem
filterDefinition={visibleFilterDefinition}
/>
</SelectableItem>
),
)}
</DropdownMenuItemsContainer>
{shoudShowSeparator && <DropdownMenuSeparator />}
<DropdownMenuItemsContainer>
{hiddenColumnsFilterDefinitions.map(
(hiddenFilterDefinition, index) => (
<SelectableItem
itemId={hiddenFilterDefinition.fieldMetadataId}
key={`hidden-select-filter-${index}`}
>
<ObjectFilterDropdownFilterSelectMenuItem
filterDefinition={hiddenFilterDefinition}
/>
</SelectableItem>
),
)}
</DropdownMenuItemsContainer>
</SelectableList>
{shouldShowAdvancedFilterButton && <AdvancedFilterButton />}
<ScrollWrapper contextProviderName="dropdownMenuItemsContainer">
<SelectableList
hotkeyScope={FiltersHotkeyScope.ObjectFilterDropdownButton}
selectableItemIdArray={selectableListItemIds}
selectableListId={OBJECT_FILTER_DROPDOWN_ID}
onEnter={handleEnter}
>
<DropdownMenuItemsContainer>
{visibleColumnsFilterDefinitions.map(
(visibleFilterDefinition, index) => (
<SelectableItem
itemId={visibleFilterDefinition.fieldMetadataId}
key={`visible-select-filter-${index}`}
>
<ObjectFilterDropdownFilterSelectMenuItem
filterDefinition={visibleFilterDefinition}
/>
</SelectableItem>
),
)}
</DropdownMenuItemsContainer>
{shoudShowSeparator && <DropdownMenuSeparator />}
<DropdownMenuItemsContainer>
{hiddenColumnsFilterDefinitions.map(
(hiddenFilterDefinition, index) => (
<SelectableItem
itemId={hiddenFilterDefinition.fieldMetadataId}
key={`hidden-select-filter-${index}`}
>
<ObjectFilterDropdownFilterSelectMenuItem
filterDefinition={hiddenFilterDefinition}
/>
</SelectableItem>
),
)}
</DropdownMenuItemsContainer>
</SelectableList>
{shouldShowAdvancedFilterButton && <AdvancedFilterButton />}
</ScrollWrapper>
</>
);
};

View File

@ -10,17 +10,28 @@ export const ObjectFilterDropdownOperandButton = () => {
const {
selectedOperandInDropdownState,
setIsObjectFilterDropdownOperandSelectUnfolded,
isObjectFilterDropdownOperandSelectUnfoldedState,
} = useFilterDropdown();
const selectedOperandInDropdown = useRecoilValue(
selectedOperandInDropdownState,
);
const isObjectFilterDropdownOperandSelectUnfolded = useRecoilValue(
isObjectFilterDropdownOperandSelectUnfoldedState,
);
const handleButtonClick = () => {
setIsObjectFilterDropdownOperandSelectUnfolded(
!isObjectFilterDropdownOperandSelectUnfolded,
);
};
return (
<DropdownMenuHeader
key={'selected-filter-operand'}
EndIcon={IconChevronDown}
onClick={() => setIsObjectFilterDropdownOperandSelectUnfolded(true)}
onClick={handleButtonClick}
>
{getOperandLabel(selectedOperandInDropdown)}
</DropdownMenuHeader>

View File

@ -16,6 +16,7 @@ import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectab
import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
import { MenuItemMultiSelect } from '@/ui/navigation/menu-item/components/MenuItemMultiSelect';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
import { isDefined } from '~/utils/isDefined';
export const EMPTY_FILTER_VALUE = '';
@ -162,22 +163,24 @@ export const ObjectFilterDropdownOptionSelect = () => {
}
}}
>
<DropdownMenuItemsContainer hasMaxHeight>
{optionsInDropdown?.map((option) => (
<MenuItemMultiSelect
key={option.id}
selected={option.isSelected}
isKeySelected={option.id === selectedItemId}
onSelectChange={(selected) =>
handleMultipleOptionSelectChange(option, selected)
}
text={option.label}
color={option.color}
className=""
/>
))}
</DropdownMenuItemsContainer>
{showNoResult && <MenuItem text="No result" />}
<ScrollWrapper contextProviderName="dropdownMenuItemsContainer">
<DropdownMenuItemsContainer hasMaxHeight>
{optionsInDropdown?.map((option) => (
<MenuItemMultiSelect
key={option.id}
selected={option.isSelected}
isKeySelected={option.id === selectedItemId}
onSelectChange={(selected) =>
handleMultipleOptionSelectChange(option, selected)
}
text={option.label}
color={option.color}
className=""
/>
))}
</DropdownMenuItemsContainer>
{showNoResult && <MenuItem text="No result" />}
</ScrollWrapper>
</SelectableList>
);
};

View File

@ -15,6 +15,7 @@ import { StyledHeaderDropdownButton } from '@/ui/layout/dropdown/components/Styl
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
import { useContext } from 'react';
import { SORT_DIRECTIONS } from '../types/SortDirection';
@ -42,17 +43,13 @@ export const StyledInput = styled.input`
}
`;
const StyledContainer = styled.div`
position: relative;
`;
const StyledSelectedSortDirectionContainer = styled.div`
background: ${({ theme }) => theme.background.secondary};
box-shadow: ${({ theme }) => theme.boxShadow.light};
border-radius: ${({ theme }) => theme.border.radius.md};
left: 10px;
position: absolute;
top: 10px;
top: 32px;
width: 100%;
z-index: 1000;
`;
@ -166,21 +163,23 @@ export const ObjectSortDropdownButton = ({
</DropdownMenuItemsContainer>
</StyledSelectedSortDirectionContainer>
)}
<StyledContainer>
<DropdownMenuHeader
EndIcon={IconChevronDown}
onClick={() => setIsSortDirectionMenuUnfolded(true)}
>
{selectedSortDirection === 'asc' ? 'Ascending' : 'Descending'}
</DropdownMenuHeader>
<StyledInput
autoFocus
value={objectSortDropdownSearchInput}
placeholder="Search fields"
onChange={(event) =>
setObjectSortDropdownSearchInput(event.target.value)
}
/>
<DropdownMenuHeader
EndIcon={IconChevronDown}
onClick={() =>
setIsSortDirectionMenuUnfolded(!isSortDirectionMenuUnfolded)
}
>
{selectedSortDirection === 'asc' ? 'Ascending' : 'Descending'}
</DropdownMenuHeader>
<StyledInput
autoFocus
value={objectSortDropdownSearchInput}
placeholder="Search fields"
onChange={(event) =>
setObjectSortDropdownSearchInput(event.target.value)
}
/>
<ScrollWrapper contextProviderName="dropdownMenuItemsContainer">
<DropdownMenuItemsContainer>
{visibleColumnsSortDefinitions.map(
(visibleSortDefinition, index) => (
@ -214,7 +213,7 @@ export const ObjectSortDropdownButton = ({
),
)}
</DropdownMenuItemsContainer>
</StyledContainer>
</ScrollWrapper>
</>
}
onClose={handleDropdownButtonClose}

View File

@ -21,7 +21,6 @@ import { turnIntoEmptyStringIfWhitespacesOnly } from '~/utils/string/turnIntoEmp
const StyledDropdownMenu = styled(DropdownMenu)`
left: -1px;
position: absolute;
top: -1px;
`;
@ -46,6 +45,7 @@ type MultiItemFieldInputProps<T> = {
};
// Todo: the API of this component does not look healthy: we have renderInput, renderItem, formatInput, ...
// This should be refactored with a hook instead that exposes those events in a context around this component and its children.
export const MultiItemFieldInput = <T,>({
items,
onPersist,

View File

@ -1,14 +1,11 @@
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
import styled from '@emotion/styled';
import { useEffect, useState } from 'react';
import { MenuItemWithOptionDropdown } from '@/ui/navigation/menu-item/components/MenuItemWithOptionDropdown';
import { useState } from 'react';
import {
IconBookmark,
IconBookmarkPlus,
IconComponent,
IconDotsVertical,
IconPencil,
IconTrash,
} from 'twenty-ui';
@ -24,12 +21,6 @@ type MultiItemFieldMenuItemProps<T> = {
hasPrimaryButton?: boolean;
};
const StyledIconBookmark = styled(IconBookmark)`
color: ${({ theme }) => theme.font.color.light};
height: ${({ theme }) => theme.icon.size.sm}px;
width: ${({ theme }) => theme.icon.size.sm}px;
`;
export const MultiItemFieldMenuItem = <T,>({
dropdownId,
isPrimary,
@ -47,66 +38,51 @@ export const MultiItemFieldMenuItem = <T,>({
const handleMouseLeave = () => setIsHovered(false);
const handleDeleteClick = () => {
closeDropdown();
setIsHovered(false);
onDelete?.();
};
useEffect(() => {
if (isDropdownOpen) {
return () => closeDropdown();
}
}, [closeDropdown, isDropdownOpen]);
const handleSetAsPrimaryClick = () => {
closeDropdown();
onSetAsPrimary?.();
};
const handleEditClick = () => {
closeDropdown();
onEdit?.();
};
return (
<MenuItem
<MenuItemWithOptionDropdown
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
text={<DisplayComponent value={value} />}
isIconDisplayedOnHoverOnly={!isPrimary && !isDropdownOpen}
iconButtons={[
{
Wrapper: isHovered
? ({ iconButton }) => (
<Dropdown
dropdownId={dropdownId}
dropdownHotkeyScope={{ scope: dropdownId }}
dropdownPlacement="right-start"
dropdownStrategy="fixed"
disableBlur
clickableComponent={iconButton}
dropdownComponents={
<DropdownMenuItemsContainer>
{hasPrimaryButton && !isPrimary && (
<MenuItem
LeftIcon={IconBookmarkPlus}
text="Set as Primary"
onClick={onSetAsPrimary}
/>
)}
<MenuItem
LeftIcon={IconPencil}
text="Edit"
onClick={onEdit}
/>
<MenuItem
accent="danger"
LeftIcon={IconTrash}
text="Delete"
onClick={handleDeleteClick}
/>
</DropdownMenuItemsContainer>
}
/>
)
: undefined,
Icon:
isPrimary && !isHovered
? (StyledIconBookmark as IconComponent)
: IconDotsVertical,
accent: 'tertiary',
onClick: isHovered ? () => {} : undefined,
},
]}
RightIcon={isHovered ? null : IconBookmark}
dropdownId={dropdownId}
dropdownContent={
<DropdownMenuItemsContainer>
{hasPrimaryButton && !isPrimary && (
<MenuItem
LeftIcon={IconBookmarkPlus}
text="Set as Primary"
onClick={handleSetAsPrimaryClick}
/>
)}
<MenuItem
LeftIcon={IconPencil}
text="Edit"
onClick={handleEditClick}
/>
<MenuItem
accent="danger"
LeftIcon={IconTrash}
text="Delete"
onClick={handleDeleteClick}
/>
</DropdownMenuItemsContainer>
}
/>
);
};

View File

@ -40,6 +40,7 @@ import { MenuItemNavigate } from '@/ui/navigation/menu-item/components/MenuItemN
import { MenuItemToggle } from '@/ui/navigation/menu-item/components/MenuItemToggle';
import { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMemorizedUrlState';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
import { ViewFieldsVisibilityDropdownSection } from '@/views/components/ViewFieldsVisibilityDropdownSection';
import { ViewGroupsVisibilityDropdownSection } from '@/views/components/ViewGroupsVisibilityDropdownSection';
import { useGetCurrentView } from '@/views/hooks/useGetCurrentView';
@ -259,15 +260,17 @@ export const RecordIndexOptionsDropdownContent = ({
<DropdownMenuHeader StartIcon={IconChevronLeft} onClick={resetMenu}>
Fields
</DropdownMenuHeader>
<ViewFieldsVisibilityDropdownSection
title="Visible"
fields={visibleRecordFields}
isDraggable
onDragEnd={handleReorderFields}
onVisibilityChange={handleChangeFieldVisibility}
showSubheader={false}
showDragGrip={true}
/>
<ScrollWrapper contextProviderName="dropdownMenuItemsContainer">
<ViewFieldsVisibilityDropdownSection
title="Visible"
fields={visibleRecordFields}
isDraggable
onDragEnd={handleReorderFields}
onVisibilityChange={handleChangeFieldVisibility}
showSubheader={false}
showDragGrip={true}
/>
</ScrollWrapper>
<DropdownMenuSeparator />
<DropdownMenuItemsContainer>
<MenuItemNavigate
@ -317,7 +320,7 @@ export const RecordIndexOptionsDropdownContent = ({
Hidden Fields
</DropdownMenuHeader>
{hiddenRecordFields.length > 0 && (
<>
<ScrollWrapper contextProviderName="dropdownMenuItemsContainer">
<ViewFieldsVisibilityDropdownSection
title="Hidden"
fields={hiddenRecordFields}
@ -326,7 +329,7 @@ export const RecordIndexOptionsDropdownContent = ({
showSubheader={false}
showDragGrip={false}
/>
</>
</ScrollWrapper>
)}
<DropdownMenuSeparator />