This commit is contained in:
Lucas Bordeau 2024-10-31 15:25:15 +01:00
parent bc7144c111
commit 43e7b2d664
11 changed files with 182 additions and 96 deletions

View File

@ -161,7 +161,7 @@ export const ObjectFilterDropdownFilterSelect = ({
setObjectFilterDropdownSearchInput(event.target.value)
}
/>
<ScrollWrapper contextProviderName="filterSelectDropdown">
<ScrollWrapper contextProviderName="dropdownMenuItemsContainer">
<SelectableList
hotkeyScope={FiltersHotkeyScope.ObjectFilterDropdownButton}
selectableItemIdArray={selectableListItemIds}

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

@ -179,7 +179,7 @@ export const ObjectSortDropdownButton = ({
setObjectSortDropdownSearchInput(event.target.value)
}
/>
<ScrollWrapper contextProviderName="sortSelectDropdown">
<ScrollWrapper contextProviderName="dropdownMenuItemsContainer">
<DropdownMenuItemsContainer>
{visibleColumnsSortDefinitions.map(
(visibleSortDefinition, index) => (

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

View File

@ -74,8 +74,8 @@ export const PhoneCountryPickerDropdownButton = ({
);
const handleChange = (countryCode: string) => {
onChange(countryCode);
closeDropdown();
onChange(countryCode);
};
const countries = useCountries();
@ -89,7 +89,6 @@ export const PhoneCountryPickerDropdownButton = ({
return (
<Dropdown
dropdownMenuWidth={'100%'}
dropdownId="country-picker-dropdown-id"
dropdownHotkeyScope={{ scope: CountryPickerHotkeyScope.CountryPicker }}
clickableComponent={

View File

@ -7,7 +7,7 @@ import {
size,
useFloating,
} from '@floating-ui/react';
import { MouseEvent, useRef } from 'react';
import { MouseEvent, ReactNode, useRef } from 'react';
import { Keys } from 'react-hotkeys-hook';
import { Key } from 'ts-key-enum';
@ -27,8 +27,8 @@ import { DropdownOnToggleEffect } from './DropdownOnToggleEffect';
type DropdownProps = {
className?: string;
clickableComponent?: JSX.Element | JSX.Element[];
dropdownComponents: JSX.Element | JSX.Element[];
clickableComponent?: ReactNode;
dropdownComponents: ReactNode;
hotkey?: {
key: Keys;
scope: string;
@ -92,6 +92,7 @@ export const Dropdown = ({
elements.floating.style.height = 'auto';
},
boundary: document.querySelector('#root') ?? undefined,
}),
...offsetMiddlewares,
],

View File

@ -24,7 +24,6 @@ const StyledDropdownMenu = styled.div<{
display: flex;
height: 100%;
overflow: hidden;
flex-direction: column;
z-index: 30;

View File

@ -13,7 +13,6 @@ const StyledDropdownMenuItemsExternalContainer = styled.div<{
flex-direction: column;
gap: 2px;
max-height: ${({ hasMaxHeight }) => (hasMaxHeight ? '188px' : 'none')};
overflow-y: auto;
padding: var(--padding);
@ -34,6 +33,8 @@ const StyledDropdownMenuItemsInternalContainer = styled.div`
width: 100%;
`;
// TODO: refactor this, the dropdown should handle the max height behavior + scroll with the size middleware
// We should instead create a DropdownMenuItemsContainerScrollable or take for granted that it is the default behavior
export const DropdownMenuItemsContainer = ({
children,
hasMaxHeight,

View File

@ -0,0 +1,104 @@
import { useTheme } from '@emotion/react';
import { FunctionComponent, MouseEvent, ReactElement, ReactNode } from 'react';
import {
IconChevronRight,
IconComponent,
IconDotsVertical,
LightIconButton,
LightIconButtonProps,
} from 'twenty-ui';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { MenuItemLeftContent } from '../internals/components/MenuItemLeftContent';
import {
StyledHoverableMenuItemBase,
StyledMenuItemLeftContent,
} from '../internals/components/StyledMenuItemBase';
import { MenuItemAccent } from '../types/MenuItemAccent';
export type MenuItemIconButton = {
Wrapper?: FunctionComponent<{ iconButton: ReactElement }>;
Icon: IconComponent;
accent?: LightIconButtonProps['accent'];
onClick?: (event: MouseEvent<any>) => void;
};
export type MenuItemWithOptionDropdownProps = {
accent?: MenuItemAccent;
className?: string;
dropdownContent: ReactNode;
dropdownId: string;
isIconDisplayedOnHoverOnly?: boolean;
isTooltipOpen?: boolean;
LeftIcon?: IconComponent | null;
RightIcon?: IconComponent | null;
onClick?: (event: MouseEvent<HTMLDivElement>) => void;
onMouseEnter?: (event: MouseEvent<HTMLDivElement>) => void;
onMouseLeave?: (event: MouseEvent<HTMLDivElement>) => void;
testId?: string;
text: ReactNode;
hasSubMenu?: boolean;
};
// TODO: refactor this
export const MenuItemWithOptionDropdown = ({
accent = 'default',
className,
isIconDisplayedOnHoverOnly = true,
dropdownContent,
dropdownId,
LeftIcon,
RightIcon,
onClick,
onMouseEnter,
onMouseLeave,
testId,
text,
hasSubMenu = false,
}: MenuItemWithOptionDropdownProps) => {
const theme = useTheme();
const handleMenuItemClick = (event: MouseEvent<HTMLDivElement>) => {
if (!onClick) return;
event.preventDefault();
event.stopPropagation();
onClick?.(event);
};
return (
<StyledHoverableMenuItemBase
data-testid={testId ?? undefined}
onClick={handleMenuItemClick}
className={className}
accent={accent}
isIconDisplayedOnHoverOnly={isIconDisplayedOnHoverOnly}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
>
<StyledMenuItemLeftContent>
<MenuItemLeftContent LeftIcon={LeftIcon ?? undefined} text={text} />
</StyledMenuItemLeftContent>
<div className="hoverable-buttons">
<Dropdown
clickableComponent={
<LightIconButton
Icon={RightIcon ?? IconDotsVertical}
size="small"
/>
}
dropdownComponents={dropdownContent}
dropdownId={dropdownId}
dropdownHotkeyScope={{ scope: 'sd' }}
disableBlur
/>
</div>
{hasSubMenu && (
<IconChevronRight
size={theme.icon.size.sm}
color={theme.font.color.tertiary}
/>
)}
</StyledHoverableMenuItemBase>
);
};