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:
@ -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={
|
||||
|
||||
@ -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,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -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={() => {
|
||||
|
||||
@ -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'}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -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';
|
||||
}
|
||||
};
|
||||
@ -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
|
||||
|
||||
Reference in New Issue
Block a user