Fix broken dropdown auto resize behavior (#11423)

This PR was originally about fixing advanced filter dropdown auto resize
to avoid breaking the app main container, but the regression is not
limited to advanced filter dropdown, so this PR fixes the regression for
every dropdown in the app.

This PR adds a max dropdown max width to allow resizing dropdowns
horizontally also, which can happen easily for the advanced filter
dropdown.

In this PR we also start removing `fieldMetadataItemUsedInDropdown` in
component `AdvancedFilterDropdownTextInput` because it has no impact
outside of this component which is used only once.

The autoresize behavior determines the right padding-bottom between
mobile and PC.

Mobile : 

<img width="604" alt="Capture d’écran 2025-04-07 à 16 03 12"
src="https://github.com/user-attachments/assets/fbdd8020-1bfc-4e01-8a05-3a9f114cdd40"
/>

PC :

<img width="757" alt="Capture d’écran 2025-04-07 à 16 03 30"
src="https://github.com/user-attachments/assets/f80a5967-8f60-40bb-ae3c-fa9eb4c65707"
/>

Fixes https://github.com/twentyhq/core-team-issues/issues/725
Fixes https://github.com/twentyhq/twenty/issues/11409

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
Lucas Bordeau
2025-04-08 11:03:10 +02:00
committed by GitHub
parent 89abf3db4f
commit 3d90eb4eb9
38 changed files with 174 additions and 213 deletions
@@ -72,7 +72,7 @@ export const RecordIndexActionMenuDropdown = () => {
scope: ActionMenuDropdownHotkeyScope.ActionMenuDropdown,
}}
data-select-disable
dropdownMenuWidth={width}
dropdownWidth={width}
dropdownPlacement="bottom-start"
dropdownStrategy="absolute"
dropdownOffset={{
@@ -48,7 +48,7 @@ export const AttachmentDropdown = ({
clickableComponent={
<LightIconButton Icon={IconDotsVertical} accent="tertiary" />
}
dropdownMenuWidth={160}
dropdownWidth={160}
dropdownComponents={
<DropdownMenuItemsContainer>
<MenuItem
@@ -5,43 +5,30 @@ import { ObjectFilterDropdownSearchInput } from '@/object-record/object-filter-d
import { ObjectFilterDropdownSourceSelect } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownSourceSelect';
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
import { getFilterTypeFromFieldType } from '@/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions';
import { AdvancedFilterDropdownDateInput } from '@/object-record/advanced-filter/components/AdvancedFilterDropdownDateInput';
import { ObjectFilterDropdownBooleanSelect } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownBooleanSelect';
import { ObjectFilterDropdownTextInput } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownTextInput';
import { DATE_FILTER_TYPES } from '@/object-record/object-filter-dropdown/constants/DateFilterTypes';
import { fieldMetadataItemUsedInDropdownComponentSelector } from '@/object-record/object-filter-dropdown/states/fieldMetadataItemUsedInDropdownComponentSelector';
import { subFieldNameUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/subFieldNameUsedInDropdownComponentState';
import { isFilterOnActorSourceSubField } from '@/object-record/object-filter-dropdown/utils/isFilterOnActorSourceSubField';
import { RecordFilter } from '@/object-record/record-filter/types/RecordFilter';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { isDefined } from 'twenty-shared/utils';
type AdvancedFilterDropdownFilterInputProps = {
filterDropdownId?: string;
recordFilterId?: string;
recordFilter: RecordFilter;
};
export const AdvancedFilterDropdownFilterInput = ({
filterDropdownId,
recordFilterId,
recordFilter,
}: AdvancedFilterDropdownFilterInputProps) => {
const fieldMetadataItemUsedInDropdown = useRecoilComponentValueV2(
fieldMetadataItemUsedInDropdownComponentSelector,
filterDropdownId,
);
const subFieldNameUsedInDropdown = useRecoilComponentValueV2(
subFieldNameUsedInDropdownComponentState,
filterDropdownId,
);
if (!isDefined(fieldMetadataItemUsedInDropdown)) {
return null;
}
const filterType = getFilterTypeFromFieldType(
fieldMetadataItemUsedInDropdown.type,
);
const filterType = recordFilter.type;
const isActorSourceCompositeFilter = isFilterOnActorSourceSubField(
subFieldNameUsedInDropdown,
@@ -57,7 +44,7 @@ export const AdvancedFilterDropdownFilterInput = ({
<>
<ObjectFilterDropdownSearchInput />
<DropdownMenuSeparator />
<ObjectFilterDropdownRecordSelect recordFilterId={recordFilterId} />
<ObjectFilterDropdownRecordSelect recordFilterId={recordFilter.id} />
</>
)}
{filterType === 'ACTOR' &&
@@ -1,57 +1,36 @@
import { useState } from 'react';
import { getFilterTypeFromFieldType } from '@/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions';
import { fieldMetadataItemUsedInDropdownComponentSelector } from '@/object-record/object-filter-dropdown/states/fieldMetadataItemUsedInDropdownComponentSelector';
import { selectedFilterComponentState } from '@/object-record/object-filter-dropdown/states/selectedFilterComponentState';
import { selectedOperandInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/selectedOperandInDropdownComponentState';
import { useApplyRecordFilter } from '@/object-record/record-filter/hooks/useApplyRecordFilter';
import { RecordFilter } from '@/object-record/record-filter/types/RecordFilter';
import { TextInputV2 } from '@/ui/input/components/TextInputV2';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { v4 } from 'uuid';
export const AdvancedFilterDropdownTextInput = () => {
const selectedOperandInDropdown = useRecoilComponentValueV2(
selectedOperandInDropdownComponentState,
);
const fieldMetadataItemUsedInDropdown = useRecoilComponentValueV2(
fieldMetadataItemUsedInDropdownComponentSelector,
);
const selectedFilter = useRecoilComponentValueV2(
selectedFilterComponentState,
);
type AdvancedFilterDropdownTextInputProps = {
recordFilter: RecordFilter;
};
export const AdvancedFilterDropdownTextInput = ({
recordFilter,
}: AdvancedFilterDropdownTextInputProps) => {
const { applyRecordFilter } = useApplyRecordFilter();
const [inputValue, setInputValue] = useState(
() => selectedFilter?.value || '',
);
const [inputValue, setInputValue] = useState(() => recordFilter?.value || '');
const handleChange = (newValue: string) => {
if (!fieldMetadataItemUsedInDropdown || !selectedOperandInDropdown) {
return;
}
setInputValue(newValue);
applyRecordFilter({
id: selectedFilter?.id ? selectedFilter.id : v4(),
fieldMetadataId: fieldMetadataItemUsedInDropdown?.id ?? '',
id: recordFilter.id,
fieldMetadataId: recordFilter?.fieldMetadataId ?? '',
value: newValue,
operand: selectedOperandInDropdown,
operand: recordFilter.operand,
displayValue: newValue,
type: getFilterTypeFromFieldType(fieldMetadataItemUsedInDropdown.type),
label: fieldMetadataItemUsedInDropdown.label,
recordFilterGroupId: selectedFilter?.recordFilterGroupId,
positionInRecordFilterGroup: selectedFilter?.positionInRecordFilterGroup,
type: recordFilter.type,
label: recordFilter.label,
recordFilterGroupId: recordFilter?.recordFilterGroupId,
positionInRecordFilterGroup: recordFilter?.positionInRecordFilterGroup,
});
};
if (!selectedOperandInDropdown || !fieldMetadataItemUsedInDropdown) {
return null;
}
return (
<TextInputV2
value={inputValue}
@@ -5,8 +5,10 @@ import { AdvancedFilterRecordFilterRow } from '@/object-record/advanced-filter/c
import { useChildRecordFiltersAndRecordFilterGroups } from '@/object-record/advanced-filter/hooks/useChildRecordFiltersAndRecordFilterGroups';
import { rootLevelRecordFilterGroupComponentSelector } from '@/object-record/advanced-filter/states/rootLevelRecordFilterGroupComponentSelector';
import { isRecordFilterGroupChildARecordFilterGroup } from '@/object-record/advanced-filter/utils/isRecordFilterGroupChildARecordFilterGroup';
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import styled from '@emotion/styled';
import { id } from 'date-fns/locale';
import { isDefined } from 'twenty-shared/utils';
const StyledContainer = styled.div<{ isGrayBackground?: boolean }>`
@@ -16,7 +18,7 @@ const StyledContainer = styled.div<{ isGrayBackground?: boolean }>`
flex-direction: column;
gap: ${({ theme }) => theme.spacing(2)};
padding: ${({ theme }) => theme.spacing(2)};
overflow: hidden;
min-width: 650px;
`;
export const AdvancedFilterRootRecordFilterGroup = () => {
@@ -34,28 +36,32 @@ export const AdvancedFilterRootRecordFilterGroup = () => {
}
return (
<StyledContainer>
{childRecordFiltersAndRecordFilterGroups.map(
(recordFilterGroupChild, recordFilterGroupChildIndex) =>
isRecordFilterGroupChildARecordFilterGroup(recordFilterGroupChild) ? (
<AdvancedFilterRecordFilterGroupRow
key={recordFilterGroupChild.id}
parentRecordFilterGroup={rootRecordFilterGroup}
recordFilterGroup={recordFilterGroupChild}
recordFilterGroupIndex={recordFilterGroupChildIndex}
/>
) : (
<AdvancedFilterRecordFilterRow
key={recordFilterGroupChild.id}
recordFilterGroup={rootRecordFilterGroup}
recordFilter={recordFilterGroupChild}
recordFilterIndex={recordFilterGroupChildIndex}
/>
),
)}
<AdvancedFilterAddFilterRuleSelect
recordFilterGroup={rootRecordFilterGroup}
/>
</StyledContainer>
<ScrollWrapper componentInstanceId={`scroll-wrapper-dropdown-menu-${id}`}>
<StyledContainer>
{childRecordFiltersAndRecordFilterGroups.map(
(recordFilterGroupChild, recordFilterGroupChildIndex) =>
isRecordFilterGroupChildARecordFilterGroup(
recordFilterGroupChild,
) ? (
<AdvancedFilterRecordFilterGroupRow
key={recordFilterGroupChild.id}
parentRecordFilterGroup={rootRecordFilterGroup}
recordFilterGroup={recordFilterGroupChild}
recordFilterGroupIndex={recordFilterGroupChildIndex}
/>
) : (
<AdvancedFilterRecordFilterRow
key={recordFilterGroupChild.id}
recordFilterGroup={rootRecordFilterGroup}
recordFilter={recordFilterGroupChild}
recordFilterIndex={recordFilterGroupChildIndex}
/>
),
)}
<AdvancedFilterAddFilterRuleSelect
recordFilterGroup={rootRecordFilterGroup}
/>
</StyledContainer>
</ScrollWrapper>
);
};
@@ -45,11 +45,15 @@ export const AdvancedFilterValueInput = ({
const operandHasNoInput =
recordFilter && !configurableViewFilterOperands.has(recordFilter.operand);
if (!isDefined(recordFilter)) {
return null;
}
const handleFilterValueDropdownClose = () => {
setObjectFilterDropdownSearchInput('');
};
const filterType = recordFilter?.type;
const filterType = recordFilter.type;
const dropdownContentOffset =
filterType === 'DATE' || filterType === 'DATE_TIME'
@@ -67,7 +71,7 @@ export const AdvancedFilterValueInput = ({
) : isDefined(filterType) &&
(TEXT_FILTER_TYPES.includes(filterType) ||
NUMBER_FILTER_TYPES.includes(filterType)) ? (
<AdvancedFilterDropdownTextInput />
<AdvancedFilterDropdownTextInput recordFilter={recordFilter} />
) : (
<Dropdown
dropdownId={dropdownId}
@@ -77,14 +81,12 @@ export const AdvancedFilterValueInput = ({
/>
}
dropdownComponents={
<AdvancedFilterDropdownFilterInput
recordFilterId={recordFilter.id}
/>
<AdvancedFilterDropdownFilterInput recordFilter={recordFilter} />
}
dropdownHotkeyScope={{ scope: dropdownId }}
dropdownOffset={dropdownContentOffset}
dropdownPlacement="bottom-start"
dropdownMenuWidth={280}
dropdownWidth={280}
onClose={handleFilterValueDropdownClose}
/>
)}
@@ -3,7 +3,6 @@ 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 { MultipleFiltersDropdownFilterOnFilterChangedEffect } from './MultipleFiltersDropdownFilterOnFilterChangedEffect';
import { ObjectFilterDropdownFilterSelect } from './ObjectFilterDropdownFilterSelect';
type MultipleFiltersDropdownContentProps = {
@@ -40,7 +39,6 @@ export const MultipleFiltersDropdownContent = ({
) : (
<ObjectFilterDropdownFilterSelect isAdvancedFilterButtonVisible />
)}
<MultipleFiltersDropdownFilterOnFilterChangedEffect />
</>
);
};
@@ -1,36 +0,0 @@
import { useEffect } from 'react';
import { getFilterTypeFromFieldType } from '@/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions';
import { fieldMetadataItemUsedInDropdownComponentSelector } from '@/object-record/object-filter-dropdown/states/fieldMetadataItemUsedInDropdownComponentSelector';
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { isDefined } from 'twenty-shared/utils';
export const MultipleFiltersDropdownFilterOnFilterChangedEffect = () => {
const { setDropdownWidth } = useDropdown();
const fieldMetadataItemUsedInDropdown = useRecoilComponentValueV2(
fieldMetadataItemUsedInDropdownComponentSelector,
);
useEffect(() => {
if (!isDefined(fieldMetadataItemUsedInDropdown)) {
return;
}
const filterType = getFilterTypeFromFieldType(
fieldMetadataItemUsedInDropdown.type,
);
switch (filterType) {
case 'DATE':
case 'DATE_TIME':
setDropdownWidth(280);
break;
default:
setDropdownWidth(200);
}
}, [fieldMetadataItemUsedInDropdown, setDropdownWidth]);
return null;
};
@@ -33,7 +33,7 @@ export const ObjectOptionsDropdown = ({
<Dropdown
dropdownId={OBJECT_OPTIONS_DROPDOWN_ID}
dropdownHotkeyScope={{ scope: TableOptionsHotkeyScope.Dropdown }}
dropdownMenuWidth={DROPDOWN_WIDTH}
dropdownWidth={DROPDOWN_WIDTH}
dropdownOffset={{ y: DROPDOWN_OFFSET_Y }}
clickableComponent={
<StyledHeaderDropdownButton isUnfolded={isDropdownOpen}>
@@ -26,8 +26,6 @@ describe('useOptionsDropdown', () => {
closeDropdown: mockCloseDropdown,
toggleDropdown: jest.fn(),
openDropdown: jest.fn(),
dropdownWidth: undefined,
setDropdownWidth: jest.fn(),
dropdownPlacement: null,
setDropdownPlacement: jest.fn(),
});
@@ -46,7 +46,7 @@ export const RecordBoardColumnHeaderAggregateDropdown = ({
dropdownHotkeyScope={{
scope: RecordBoardColumnHotkeyScope.ColumnHeader,
}}
dropdownMenuWidth={DROPDOWN_WIDTH}
dropdownWidth={DROPDOWN_WIDTH}
dropdownOffset={{ y: DROPDOWN_OFFSET_Y }}
clickableComponent={
<RecordBoardColumnHeaderAggregateDropdownButton
@@ -78,7 +78,7 @@ export const RecordIndexAddRecordInGroupDropdown = ({
return (
<Dropdown
dropdownMenuWidth="200px"
dropdownWidth="200px"
dropdownPlacement="bottom-start"
clickableComponent={clickableComponent}
dropdownId={dropdownId}
@@ -10,7 +10,6 @@ import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/Drop
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal';
import { Trans, useLingui } from '@lingui/react/macro';
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
import {
IconCalendarEvent,
IconDotsVertical,
@@ -20,6 +19,7 @@ import {
} from 'twenty-ui/display';
import { LightIconButton } from 'twenty-ui/input';
import { MenuItem } from 'twenty-ui/navigation';
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
type SettingsAccountsRowDropdownMenuProps = {
account: ConnectedAccount;
@@ -55,7 +55,7 @@ export const SettingsAccountsRowDropdownMenu = ({
clickableComponent={
<LightIconButton Icon={IconDotsVertical} accent="tertiary" />
}
dropdownMenuWidth={160}
dropdownWidth={160}
dropdownComponents={
<DropdownMenuItemsContainer>
<MenuItem
@@ -4,14 +4,14 @@ import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { phonesSchema as phonesFieldDefaultValueSchema } from '@/object-record/record-field/types/guards/isFieldPhonesValue';
import { SettingsOptionCardContentSelect } from '@/settings/components/SettingsOptions/SettingsOptionCardContentSelect';
import { countryCodeToCallingCode } from '@/settings/data-model/fields/preview/utils/getPhonesFieldPreviewValue';
import { useCountries } from '@/ui/input/components/internal/hooks/useCountries';
import { Select } from '@/ui/input/components/Select';
import { useCountries } from '@/ui/input/components/internal/hooks/useCountries';
import { useLingui } from '@lingui/react/macro';
import { CountryCode } from 'libphonenumber-js';
import { IconCircleOff, IconComponentProps, IconMap } from 'twenty-ui/display';
import { z } from 'zod';
import { applySimpleQuotesToString } from '~/utils/string/applySimpleQuotesToString';
import { stripSimpleQuotesFromString } from '~/utils/string/stripSimpleQuotesFromString';
import { useLingui } from '@lingui/react/macro';
import { IconCircleOff, IconComponentProps, IconMap } from 'twenty-ui/display';
type SettingsDataModelFieldPhonesFormProps = {
disabled?: boolean;
@@ -74,7 +74,6 @@ export const SettingsDataModelFieldPhonesForm = ({
description={t`The default country code for new phone numbers.`}
>
<Select<string>
dropdownWidth={'auto'}
dropdownId="selectDefaultCountryCode"
value={stripSimpleQuotesFromString(
value?.primaryPhoneCountryCode,
@@ -55,7 +55,7 @@ export const SettingsObjectFieldActiveActionDropdown = ({
accent="tertiary"
/>
}
dropdownMenuWidth={160}
dropdownWidth={160}
dropdownComponents={
<DropdownMenuItemsContainer>
<MenuItem
@@ -2,7 +2,6 @@ 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 { t } from '@lingui/core/macro';
import { FieldMetadataType } from '~/generated-metadata/graphql';
import {
IconArchiveOff,
IconDotsVertical,
@@ -12,6 +11,7 @@ import {
} from 'twenty-ui/display';
import { LightIconButton } from 'twenty-ui/input';
import { MenuItem } from 'twenty-ui/navigation';
import { FieldMetadataType } from '~/generated-metadata/graphql';
type SettingsObjectFieldInactiveActionDropdownProps = {
isCustomField?: boolean;
@@ -60,7 +60,7 @@ export const SettingsObjectFieldInactiveActionDropdown = ({
accent="tertiary"
/>
}
dropdownMenuWidth={160}
dropdownWidth={160}
dropdownComponents={
<DropdownMenuItemsContainer>
<MenuItem
@@ -77,7 +77,7 @@ export const SettingsObjectSummaryCard = ({
accent="tertiary"
/>
}
dropdownMenuWidth={160}
dropdownWidth={160}
dropdownComponents={
<DropdownMenuItemsContainer>
<MenuItem
@@ -42,7 +42,7 @@ export const SettingsObjectInactiveMenuDropDown = ({
accent="tertiary"
/>
}
dropdownMenuWidth={160}
dropdownWidth={160}
dropdownComponents={
<DropdownMenuItemsContainer>
<MenuItem
@@ -8,11 +8,11 @@ import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/Drop
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { useLingui } from '@lingui/react/macro';
import { UnwrapRecoilValue } from 'recoil';
import { SsoIdentityProviderStatus } from '~/generated/graphql';
import { isDefined } from 'twenty-shared/utils';
import { IconArchive, IconDotsVertical, IconTrash } from 'twenty-ui/display';
import { LightIconButton } from 'twenty-ui/input';
import { MenuItem } from 'twenty-ui/navigation';
import { SsoIdentityProviderStatus } from '~/generated/graphql';
type SettingsSecuritySSORowDropdownMenuProps = {
SSOIdp: UnwrapRecoilValue<typeof SSOIdentitiesProvidersState>[0];
@@ -72,7 +72,7 @@ export const SettingsSecuritySSORowDropdownMenu = ({
clickableComponent={
<LightIconButton Icon={IconDotsVertical} accent="tertiary" />
}
dropdownMenuWidth={160}
dropdownWidth={160}
dropdownComponents={
<DropdownMenuItemsContainer>
<MenuItem
@@ -1,15 +1,15 @@
import { approvedAccessDomainsState } from '@/settings/security/states/ApprovedAccessDomainsState';
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
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 { UnwrapRecoilValue, useSetRecoilState } from 'recoil';
import { useDeleteApprovedAccessDomainMutation } from '~/generated/graphql';
import { approvedAccessDomainsState } from '@/settings/security/states/ApprovedAccessDomainsState';
import { isDefined } from 'twenty-shared/utils';
import { IconDotsVertical, IconTrash } from 'twenty-ui/display';
import { LightIconButton } from 'twenty-ui/input';
import { MenuItem } from 'twenty-ui/navigation';
import { useDeleteApprovedAccessDomainMutation } from '~/generated/graphql';
type SettingsSecurityApprovedAccessDomainRowDropdownMenuProps = {
approvedAccessDomain: UnwrapRecoilValue<typeof approvedAccessDomainsState>[0];
@@ -61,7 +61,7 @@ export const SettingsSecurityApprovedAccessDomainRowDropdownMenu = ({
clickableComponent={
<LightIconButton Icon={IconDotsVertical} accent="tertiary" />
}
dropdownMenuWidth={160}
dropdownWidth={160}
dropdownComponents={
<DropdownMenuItemsContainer>
<MenuItem
@@ -26,7 +26,7 @@ export const SupportDropdown = () => {
dropdownPlacement="top-start"
dropdownOffset={{ x: 0, y: -28 }}
clickableComponent={<SupportButton />}
dropdownMenuWidth={160}
dropdownWidth={160}
dropdownComponents={
<DropdownMenuItemsContainer>
<MenuItem
@@ -14,7 +14,6 @@ import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousH
import { arrayToChunks } from '~/utils/array/arrayToChunks';
import { t } from '@lingui/core/macro';
import { IconPickerHotkeyScope } from '../types/IconPickerHotkeyScope';
import { IconApps, IconComponent, useIcons } from 'twenty-ui/display';
import {
IconButton,
@@ -22,6 +21,7 @@ import {
IconButtonVariant,
LightIconButton,
} from 'twenty-ui/input';
import { IconPickerHotkeyScope } from '../types/IconPickerHotkeyScope';
export type IconPickerProps = {
disabled?: boolean;
@@ -172,7 +172,7 @@ export const IconPicker = ({
size={size}
/>
}
dropdownMenuWidth={176}
dropdownWidth={176}
dropdownComponents={
<SelectableList
selectableListId="icon-list"
@@ -30,7 +30,7 @@ export type SelectProps<Value extends SelectValue> = {
disabled?: boolean;
selectSizeVariant?: SelectSizeVariant;
dropdownId: string;
dropdownWidth?: `${string}px` | 'auto' | number;
dropdownWidth?: number;
dropdownWidthAuto?: boolean;
emptyOption?: SelectOption<Value>;
fullWidth?: boolean;
@@ -128,7 +128,7 @@ export const Select = <Value extends SelectValue>({
) : (
<Dropdown
dropdownId={dropdownId}
dropdownMenuWidth={dropDownMenuWidth}
dropdownWidth={dropDownMenuWidth}
dropdownPlacement="bottom-start"
dropdownOffset={dropdownOffset}
clickableComponent={
@@ -152,7 +152,10 @@ export const Select = <Value extends SelectValue>({
<DropdownMenuSeparator />
)}
{!!filteredOptions.length && (
<DropdownMenuItemsContainer hasMaxHeight>
<DropdownMenuItemsContainer
hasMaxHeight
width={dropDownMenuWidth}
>
{filteredOptions.map((option) => (
<MenuItemSelect
key={`${option.value}-${option.label}`}
@@ -30,6 +30,8 @@ export const MONTH_AND_YEAR_DROPDOWN_YEAR_SELECT_ID =
'date-picker-month-and-year-dropdown-year-select';
const StyledContainer = styled.div<{ calendarDisabled?: boolean }>`
width: 280px;
& .react-datepicker {
border-color: ${({ theme }) => theme.border.color.light};
background: transparent;
@@ -3,9 +3,12 @@ import { DropdownOnToggleEffect } from '@/ui/layout/dropdown/components/Dropdown
import { DropdownComponentInstanceContext } from '@/ui/layout/dropdown/contexts/DropdownComponeInstanceContext';
import { DropdownScope } from '@/ui/layout/dropdown/scopes/DropdownScope';
import { dropdownHotkeyComponentState } from '@/ui/layout/dropdown/states/dropdownHotkeyComponentState';
import { dropdownMaxHeightComponentState } from '@/ui/layout/dropdown/states/internal/dropdownMaxHeightComponentState';
import { dropdownMaxWidthComponentState } from '@/ui/layout/dropdown/states/internal/dropdownMaxWidthComponentState';
import { DropdownOffset } from '@/ui/layout/dropdown/types/DropdownOffset';
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
import { getScopeIdFromComponentId } from '@/ui/utilities/recoil-scope/utils/getScopeIdFromComponentId';
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
import styled from '@emotion/styled';
import {
Placement,
@@ -20,6 +23,7 @@ import { flushSync } from 'react-dom';
import { Keys } from 'react-hotkeys-hook';
import { useRecoilCallback } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
import { useIsMobile } from 'twenty-ui/utilities';
import { useDropdown } from '../hooks/useDropdown';
const StyledDropdownFallbackAnchor = styled.div`
@@ -43,7 +47,7 @@ export type DropdownProps = {
dropdownHotkeyScope: HotkeyScope;
dropdownId: string;
dropdownPlacement?: Placement;
dropdownMenuWidth?: `${string}px` | `${number}%` | 'auto' | number;
dropdownWidth?: `${string}px` | `${number}%` | 'auto' | number;
dropdownOffset?: DropdownOffset;
dropdownStrategy?: 'fixed' | 'absolute';
onClickOutside?: () => void;
@@ -56,7 +60,7 @@ export const Dropdown = ({
className,
clickableComponent,
dropdownComponents,
dropdownMenuWidth,
dropdownWidth,
hotkey,
dropdownId,
dropdownHotkeyScope,
@@ -82,17 +86,39 @@ export const Dropdown = ({
]
: [];
const setDropdownMaxHeight = useSetRecoilComponentStateV2(
dropdownMaxHeightComponentState,
dropdownId,
);
const setDropdownMaxWidth = useSetRecoilComponentStateV2(
dropdownMaxWidthComponentState,
dropdownId,
);
const isMobile = useIsMobile();
const bottomAutoresizePadding = isMobile ? 64 : 32;
const { refs, floatingStyles, placement } = useFloating({
placement: dropdownPlacement,
middleware: [
...offsetMiddleware,
flip(),
size({
padding: 32,
apply: () => {
padding: {
right: 32,
bottom: bottomAutoresizePadding,
},
/**
* DO NOT TOUCH THIS apply() MIDDLEWARE PLEASE
* THIS IS MANDATORY FOR KEEPING AUTORESIZING FOR ALL DROPDOWNS
* IT'S THE STANDARD WAY OF WORKING RECOMMENDED BY THE LIBRARY
* See https://floating-ui.com/docs/size#usage
*/
apply: ({ availableHeight, availableWidth }) => {
flushSync(() => {
// TODO: I think this is not needed anymore let's remove it if not used for a few weeks
// setDropdownMaxHeight(availableHeight);
setDropdownMaxHeight(availableHeight);
setDropdownMaxWidth(availableWidth);
});
},
boundary: document.querySelector('#root') ?? undefined,
@@ -144,7 +170,7 @@ export const Dropdown = ({
<DropdownContent
className={className}
floatingStyles={floatingStyles}
dropdownMenuWidth={dropdownMenuWidth}
dropdownWidth={dropdownWidth}
dropdownComponents={dropdownComponents}
dropdownId={dropdownId}
dropdownPlacement={placement}
@@ -3,7 +3,8 @@ import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu';
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { useInternalHotkeyScopeManagement } from '@/ui/layout/dropdown/hooks/useInternalHotkeyScopeManagement';
import { activeDropdownFocusIdState } from '@/ui/layout/dropdown/states/activeDropdownFocusIdState';
import { dropdownMaxHeightComponentStateV2 } from '@/ui/layout/dropdown/states/dropdownMaxHeightComponentStateV2';
import { dropdownMaxHeightComponentState } from '@/ui/layout/dropdown/states/internal/dropdownMaxHeightComponentState';
import { dropdownMaxWidthComponentState } from '@/ui/layout/dropdown/states/internal/dropdownMaxWidthComponentState';
import { OverlayContainer } from '@/ui/layout/overlay/components/OverlayContainer';
import { HotkeyEffect } from '@/ui/utilities/hotkey/components/HotkeyEffect';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
@@ -40,7 +41,7 @@ export type DropdownContentProps = {
scope: string;
};
onHotkeyTriggered?: () => void;
dropdownMenuWidth?: `${string}px` | `${number}%` | 'auto' | number;
dropdownWidth?: `${string}px` | `${number}%` | 'auto' | number;
dropdownComponents: React.ReactNode;
parentDropdownId?: string;
avoidPortal?: boolean;
@@ -56,17 +57,22 @@ export const DropdownContent = ({
floatingStyles,
hotkey,
onHotkeyTriggered,
dropdownMenuWidth,
dropdownWidth,
dropdownComponents,
avoidPortal,
}: DropdownContentProps) => {
const { isDropdownOpen, closeDropdown, dropdownWidth, setDropdownPlacement } =
const { isDropdownOpen, closeDropdown, setDropdownPlacement } =
useDropdown(dropdownId);
const activeDropdownFocusId = useRecoilValue(activeDropdownFocusIdState);
const dropdownMaxHeight = useRecoilComponentValueV2(
dropdownMaxHeightComponentStateV2,
dropdownMaxHeightComponentState,
dropdownId,
);
const dropdownMaxWidth = useRecoilComponentValueV2(
dropdownMaxWidthComponentState,
dropdownId,
);
@@ -112,6 +118,7 @@ export const DropdownContent = ({
const dropdownMenuStyles = {
...floatingStyles,
maxHeight: dropdownMaxHeight,
maxWidth: dropdownMaxWidth,
};
return (
@@ -129,7 +136,7 @@ export const DropdownContent = ({
<OverlayContainer>
<DropdownMenu
className={className}
width={dropdownMenuWidth ?? dropdownWidth}
width={dropdownWidth}
data-select-disable
>
{dropdownComponents}
@@ -148,7 +155,7 @@ export const DropdownContent = ({
<DropdownMenu
id={dropdownId}
className={className}
width={dropdownMenuWidth ?? dropdownWidth}
width={dropdownWidth}
data-select-disable
>
{dropdownComponents}
@@ -1,4 +1,5 @@
import styled from '@emotion/styled';
import { isDefined } from 'twenty-shared/utils';
const StyledDropdownMenu = styled.div<{
width?: `${string}px` | `${number}%` | 'auto' | number;
@@ -7,8 +8,12 @@ const StyledDropdownMenu = styled.div<{
flex-direction: column;
height: 100%;
width: ${({ width = 200 }) =>
typeof width === 'number' ? `${width}px` : width};
width: ${({ width }) =>
isDefined(width)
? typeof width === 'number'
? `${width}px`
: width
: 'auto'};
`;
export const DropdownMenu = StyledDropdownMenu;
@@ -1,9 +1,12 @@
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
import { css } from '@emotion/react';
import styled from '@emotion/styled';
import { useId } from 'react';
import { isDefined } from 'twenty-shared/utils';
const StyledDropdownMenuItemsExternalContainer = styled.div<{
hasMaxHeight?: boolean;
width: number;
}>`
--padding: ${({ theme }) => theme.spacing(1)};
@@ -15,7 +18,11 @@ const StyledDropdownMenuItemsExternalContainer = styled.div<{
padding: var(--padding);
width: calc(100% - 2 * var(--padding));
${({ width }) =>
isDefined(width) &&
css`
width: ${width}px;
`}
`;
const StyledDropdownMenuItemsInternalContainer = styled.div`
@@ -38,12 +45,14 @@ export const DropdownMenuItemsContainer = ({
children,
hasMaxHeight,
className,
width = 200,
scrollable = true,
}: {
children: React.ReactNode;
hasMaxHeight?: boolean;
className?: string;
scrollable?: boolean;
width?: number;
}) => {
const id = useId();
@@ -52,6 +61,7 @@ export const DropdownMenuItemsContainer = ({
hasMaxHeight={hasMaxHeight}
className={className}
role="listbox"
width={width}
>
{hasMaxHeight ? (
<StyledScrollWrapper
@@ -73,6 +83,7 @@ export const DropdownMenuItemsContainer = ({
hasMaxHeight={hasMaxHeight}
className={className}
role="listbox"
width={width}
>
<StyledDropdownMenuItemsInternalContainer>
{children}
@@ -51,18 +51,4 @@ describe('useDropdown', () => {
expect(result.current.isDropdownOpen).toBe(false);
});
it('should change dropdownWidth', async () => {
const { result } = renderHook(() => useDropdown(dropdownId), {
wrapper: Wrapper,
});
expect(result.current.dropdownWidth).toBe(200);
await act(async () => {
result.current.setDropdownWidth(220);
});
expect(result.current.dropdownWidth).toEqual(220);
});
});
@@ -1,7 +1,6 @@
import { DropdownScopeInternalContext } from '@/ui/layout/dropdown/scopes/scope-internal-context/DropdownScopeInternalContext';
import { dropdownHotkeyComponentState } from '@/ui/layout/dropdown/states/dropdownHotkeyComponentState';
import { dropdownPlacementComponentState } from '@/ui/layout/dropdown/states/dropdownPlacementComponentState';
import { dropdownWidthComponentState } from '@/ui/layout/dropdown/states/dropdownWidthComponentState';
import { isDropdownOpenComponentState } from '@/ui/layout/dropdown/states/isDropdownOpenComponentState';
import { useAvailableScopeIdOrThrow } from '@/ui/utilities/recoil-scope/scopes-internal/hooks/useAvailableScopeId';
import { extractComponentState } from '@/ui/utilities/state/component-state/utils/extractComponentState';
@@ -28,10 +27,6 @@ export const useDropdownStates = ({
dropdownHotkeyComponentState,
scopeId,
),
dropdownWidthState: extractComponentState(
dropdownWidthComponentState,
scopeId,
),
isDropdownOpenState: extractComponentState(
isDropdownOpenComponentState,
scopeId,
@@ -12,14 +12,10 @@ import { useCallback } from 'react';
import { isDefined } from 'twenty-shared/utils';
export const useDropdown = (dropdownId?: string) => {
const {
scopeId,
dropdownWidthState,
isDropdownOpenState,
dropdownPlacementState,
} = useDropdownStates({
dropdownScopeId: getScopeIdOrUndefinedFromComponentId(dropdownId),
});
const { scopeId, isDropdownOpenState, dropdownPlacementState } =
useDropdownStates({
dropdownScopeId: getScopeIdOrUndefinedFromComponentId(dropdownId),
});
const { setActiveDropdownFocusIdAndMemorizePrevious } =
useSetActiveDropdownFocusIdAndMemorizePrevious();
@@ -32,8 +28,6 @@ export const useDropdown = (dropdownId?: string) => {
goBackToPreviousHotkeyScope,
} = usePreviousHotkeyScope();
const [dropdownWidth, setDropdownWidth] = useRecoilState(dropdownWidthState);
const [dropdownPlacement, setDropdownPlacement] = useRecoilState(
dropdownPlacementState,
);
@@ -103,8 +97,6 @@ export const useDropdown = (dropdownId?: string) => {
closeDropdown,
toggleDropdown,
openDropdown,
dropdownWidth,
setDropdownWidth,
dropdownPlacement,
setDropdownPlacement,
};
@@ -1,8 +0,0 @@
import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState';
export const dropdownWidthComponentState = createComponentState<
number | undefined
>({
key: 'dropdownWidthComponentState',
defaultValue: 200,
});
@@ -1,10 +1,10 @@
import { DropdownComponentInstanceContext } from '@/ui/layout/dropdown/contexts/DropdownComponeInstanceContext';
import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2';
export const dropdownMaxHeightComponentStateV2 = createComponentStateV2<
export const dropdownMaxHeightComponentState = createComponentStateV2<
number | undefined
>({
key: 'dropdownMaxHeightComponentStateV2',
key: 'dropdownMaxHeightComponentState',
componentInstanceContext: DropdownComponentInstanceContext,
defaultValue: undefined,
});
@@ -0,0 +1,10 @@
import { DropdownComponentInstanceContext } from '@/ui/layout/dropdown/contexts/DropdownComponeInstanceContext';
import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2';
export const dropdownMaxWidthComponentState = createComponentStateV2<
number | undefined
>({
key: 'dropdownMaxWidthComponentState',
componentInstanceContext: DropdownComponentInstanceContext,
defaultValue: undefined,
});
@@ -24,7 +24,7 @@ export const AdvancedFilterDropdownButton = () => {
dropdownHotkeyScope={{ scope: ADVANCED_FILTER_DROPDOWN_ID }}
dropdownOffset={{ y: 8, x: 0 }}
dropdownPlacement="bottom-start"
dropdownMenuWidth={650}
dropdownWidth="100%"
/>
);
};
@@ -74,7 +74,6 @@ export const ViewPickerDropdown = () => {
dropdownHotkeyScope={{ scope: ViewsHotkeyScope.ListDropdown }}
dropdownOffset={{ x: 0, y: 8 }}
dropdownPlacement="bottom-start"
dropdownMenuWidth={200}
onClickOutside={handleClickOutside}
clickableComponent={
<StyledDropdownButtonContainer isUnfolded={isViewsListDropdownOpen}>
@@ -174,7 +174,7 @@ export const WorkflowEditTriggerDatabaseEventForm = ({
<StyledLabel>Record Type</StyledLabel>
<Dropdown
dropdownId="workflow-edit-trigger-record-type"
dropdownMenuWidth={300}
dropdownWidth={300}
dropdownPlacement="bottom-start"
clickableComponent={
<SelectControl
@@ -92,7 +92,7 @@ export const WorkflowVariablesDropdown = ({
return (
<Dropdown
dropdownMenuWidth={320}
dropdownWidth={320}
dropdownId={dropdownId}
dropdownHotkeyScope={{
scope: dropdownId,