Advanced filter fix placeholder and icon in dropdown buttons (#11286)

This PR fixes a UI issue that brings a lot more robustness to the
advanced filter look and feel.

It adds the icon of the field metadata item in the filter field select.

It adds a custom placeholder in the filter input select, depending on
the field metadata item type.

<img width="661" alt="image"
src="https://github.com/user-attachments/assets/8bf2044f-52cf-447d-909d-3312089c0df5"
/>
This commit is contained in:
Lucas Bordeau
2025-03-31 10:32:04 +02:00
committed by GitHub
parent 98475ee63e
commit b13be7bd2e
6 changed files with 157 additions and 32 deletions

View File

@ -1,11 +1,9 @@
import { AdvancedFilterFieldSelectDropdownButtonClickableSelect } from '@/object-record/advanced-filter/components/AdvancedFilterFieldSelectDropdownButtonClickableSelect';
import { AdvancedFilterFieldSelectDropdownContent } from '@/object-record/advanced-filter/components/AdvancedFilterFieldSelectDropdownContent';
import { DEFAULT_ADVANCED_FILTER_DROPDOWN_OFFSET } from '@/object-record/advanced-filter/constants/DefaultAdvancedFilterDropdownOffset';
import { useAdvancedFilterFieldSelectDropdown } from '@/object-record/advanced-filter/hooks/useAdvancedFilterFieldSelectDropdown';
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
import { SelectControl } from '@/ui/input/components/SelectControl';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import styled from '@emotion/styled';
const StyledContainer = styled.div`
@ -22,26 +20,13 @@ export const AdvancedFilterFieldSelectDropdownButton = ({
const { advancedFilterFieldSelectDropdownId } =
useAdvancedFilterFieldSelectDropdown(recordFilterId);
const currentRecordFilters = useRecoilComponentValueV2(
currentRecordFiltersComponentState,
);
const recordFilter = currentRecordFilters.find(
(recordFilter) => recordFilter.id === recordFilterId,
);
const selectedFieldLabel = recordFilter?.label ?? '';
return (
<StyledContainer>
<Dropdown
dropdownId={advancedFilterFieldSelectDropdownId}
clickableComponent={
<SelectControl
selectedOption={{
label: selectedFieldLabel,
value: null,
}}
<AdvancedFilterFieldSelectDropdownButtonClickableSelect
recordFilterId={recordFilterId}
/>
}
dropdownComponents={

View File

@ -0,0 +1,47 @@
import { useGetFieldMetadataItemById } from '@/object-metadata/hooks/useGetFieldMetadataItemById';
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
import { SelectControl } from '@/ui/input/components/SelectControl';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { isNonEmptyString } from '@sniptt/guards';
import { isDefined } from 'twenty-shared/utils';
import { useIcons } from 'twenty-ui';
type AdvancedFilterFieldSelectDropdownButtonClickableSelectProps = {
recordFilterId: string;
};
export const AdvancedFilterFieldSelectDropdownButtonClickableSelect = ({
recordFilterId,
}: AdvancedFilterFieldSelectDropdownButtonClickableSelectProps) => {
const currentRecordFilters = useRecoilComponentValueV2(
currentRecordFiltersComponentState,
);
const recordFilter = currentRecordFilters.find(
(recordFilter) => recordFilter.id === recordFilterId,
);
const { getFieldMetadataItemById } = useGetFieldMetadataItemById();
const fieldMetadataItem = isNonEmptyString(recordFilter?.fieldMetadataId)
? getFieldMetadataItemById(recordFilter?.fieldMetadataId)
: undefined;
const { getIcon } = useIcons();
const fieldIcon = isDefined(fieldMetadataItem?.icon)
? getIcon(fieldMetadataItem?.icon)
: undefined;
const selectedFieldLabel = recordFilter?.label ?? '';
return (
<SelectControl
selectedOption={{
label: selectedFieldLabel,
value: null,
Icon: fieldIcon,
}}
/>
);
};

View File

@ -1,3 +1,4 @@
import { AdvancedFilterValueInputDropdownButtonClickableSelect } from '@/object-record/advanced-filter/components/AdvancedFilterValueInputDropdownButtonClickableSelect';
import { DEFAULT_ADVANCED_FILTER_DROPDOWN_OFFSET } from '@/object-record/advanced-filter/constants/DefaultAdvancedFilterDropdownOffset';
import { ObjectFilterDropdownFilterInput } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterInput';
import { fieldMetadataItemIdUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/fieldMetadataItemIdUsedInDropdownComponentState';
@ -5,7 +6,6 @@ import { selectedFilterComponentState } from '@/object-record/object-filter-drop
import { selectedOperandInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/selectedOperandInDropdownComponentState';
import { configurableViewFilterOperands } from '@/object-record/object-filter-dropdown/utils/configurableViewFilterOperands';
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
import { SelectControl } from '@/ui/input/components/SelectControl';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
@ -55,22 +55,15 @@ export const AdvancedFilterValueInputDropdownButton = ({
{operandHasNoInput ? (
<></>
) : isDisabled ? (
<SelectControl
isDisabled
selectedOption={{
label: filter?.displayValue ?? '',
value: null,
}}
<AdvancedFilterValueInputDropdownButtonClickableSelect
recordFilterId={recordFilterId}
/>
) : (
<Dropdown
dropdownId={dropdownId}
clickableComponent={
<SelectControl
selectedOption={{
label: filter?.displayValue ?? '',
value: null,
}}
<AdvancedFilterValueInputDropdownButtonClickableSelect
recordFilterId={recordFilterId}
/>
}
onOpen={() => {

View File

@ -0,0 +1,55 @@
import { useGetFieldMetadataItemById } from '@/object-metadata/hooks/useGetFieldMetadataItemById';
import { getAdvancedFilterInputPlaceholderText } from '@/object-record/advanced-filter/utils/getAdvancedFilterInputPlacedholderText';
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
import { SelectControl } from '@/ui/input/components/SelectControl';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { isNonEmptyString } from '@sniptt/guards';
import { isDefined } from 'twenty-shared/utils';
type AdvancedFilterValueInputDropdownButtonClickableSelectProps = {
recordFilterId: string;
};
export const AdvancedFilterValueInputDropdownButtonClickableSelect = ({
recordFilterId,
}: AdvancedFilterValueInputDropdownButtonClickableSelectProps) => {
const currentRecordFilters = useRecoilComponentValueV2(
currentRecordFiltersComponentState,
);
const recordFilter = currentRecordFilters.find(
(recordFilter) => recordFilter.id === recordFilterId,
);
const isDisabled =
!isDefined(recordFilter?.fieldMetadataId) ||
!isDefined(recordFilter.operand);
const shouldUsePlaceholder = !isNonEmptyString(recordFilter?.value);
const { getFieldMetadataItemById } = useGetFieldMetadataItemById();
const fieldMetadataItem = isNonEmptyString(recordFilter?.fieldMetadataId)
? getFieldMetadataItemById(recordFilter?.fieldMetadataId)
: undefined;
const placeholderText = isDefined(fieldMetadataItem)
? getAdvancedFilterInputPlaceholderText(fieldMetadataItem)
: 'Enter filter';
const advancedFilterInputText = shouldUsePlaceholder
? placeholderText
: (recordFilter?.displayValue ?? '');
return (
<SelectControl
selectedOption={{
label: advancedFilterInputText,
value: null,
disabled: isDisabled,
}}
textAccent={shouldUsePlaceholder ? 'placeholder' : 'default'}
/>
);
};

View File

@ -0,0 +1,35 @@
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { FieldMetadataType } from '~/generated-metadata/graphql';
// TODO: Refactor with composite filters
export const getAdvancedFilterInputPlaceholderText = (
fieldMetadataItem: FieldMetadataItem,
) => {
switch (fieldMetadataItem.type) {
case FieldMetadataType.TEXT:
case FieldMetadataType.ADDRESS:
case FieldMetadataType.LINKS:
case FieldMetadataType.EMAILS:
case FieldMetadataType.NUMERIC:
case FieldMetadataType.RATING:
case FieldMetadataType.PHONES:
case FieldMetadataType.ARRAY:
case FieldMetadataType.FULL_NAME:
return `Enter value for ${fieldMetadataItem.label}`;
case FieldMetadataType.NUMBER:
return 'Enter number';
case FieldMetadataType.DATE:
case FieldMetadataType.DATE_TIME:
return 'Enter date';
case FieldMetadataType.ACTOR:
return 'Select actor';
case FieldMetadataType.RELATION:
return `Select ${fieldMetadataItem.relationDefinition?.targetObjectMetadata.nameSingular}`;
case FieldMetadataType.SELECT:
case FieldMetadataType.MULTI_SELECT:
return `Select ${fieldMetadataItem.label}`;
default:
return 'Enter value';
}
};

View File

@ -8,10 +8,13 @@ import {
SelectOption,
} from 'twenty-ui';
export type SelectControlTextAccent = 'default' | 'placeholder';
const StyledControlContainer = styled.div<{
disabled?: boolean;
hasIcon: boolean;
selectSizeVariant?: SelectSizeVariant;
textAccent: SelectControlTextAccent;
}>`
display: grid;
grid-template-columns: ${({ hasIcon }) =>
@ -26,8 +29,12 @@ const StyledControlContainer = styled.div<{
background-color: ${({ theme }) => theme.background.transparent.lighter};
border: 1px solid ${({ theme }) => theme.border.color.medium};
border-radius: ${({ theme }) => theme.border.radius.sm};
color: ${({ disabled, theme }) =>
disabled ? theme.font.color.tertiary : theme.font.color.primary};
color: ${({ disabled, theme, textAccent }) =>
disabled
? theme.font.color.tertiary
: textAccent === 'default'
? theme.font.color.primary
: theme.font.color.secondary};
cursor: ${({ disabled }) => (disabled ? 'not-allowed' : 'pointer')};
text-align: left;
`;
@ -43,12 +50,14 @@ type SelectControlProps = {
selectedOption: SelectOption<string | number | boolean | null>;
isDisabled?: boolean;
selectSizeVariant?: SelectSizeVariant;
textAccent?: SelectControlTextAccent;
};
export const SelectControl = ({
selectedOption,
isDisabled,
selectSizeVariant,
textAccent = 'default',
}: SelectControlProps) => {
const theme = useTheme();
@ -57,6 +66,7 @@ export const SelectControl = ({
disabled={isDisabled}
hasIcon={isDefined(selectedOption.Icon)}
selectSizeVariant={selectSizeVariant}
textAccent={textAccent}
>
{isDefined(selectedOption.Icon) ? (
<selectedOption.Icon