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 { AdvancedFilterFieldSelectDropdownContent } from '@/object-record/advanced-filter/components/AdvancedFilterFieldSelectDropdownContent';
|
||||||
import { DEFAULT_ADVANCED_FILTER_DROPDOWN_OFFSET } from '@/object-record/advanced-filter/constants/DefaultAdvancedFilterDropdownOffset';
|
import { DEFAULT_ADVANCED_FILTER_DROPDOWN_OFFSET } from '@/object-record/advanced-filter/constants/DefaultAdvancedFilterDropdownOffset';
|
||||||
import { useAdvancedFilterFieldSelectDropdown } from '@/object-record/advanced-filter/hooks/useAdvancedFilterFieldSelectDropdown';
|
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 { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
|
||||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
const StyledContainer = styled.div`
|
const StyledContainer = styled.div`
|
||||||
@ -22,26 +20,13 @@ export const AdvancedFilterFieldSelectDropdownButton = ({
|
|||||||
const { advancedFilterFieldSelectDropdownId } =
|
const { advancedFilterFieldSelectDropdownId } =
|
||||||
useAdvancedFilterFieldSelectDropdown(recordFilterId);
|
useAdvancedFilterFieldSelectDropdown(recordFilterId);
|
||||||
|
|
||||||
const currentRecordFilters = useRecoilComponentValueV2(
|
|
||||||
currentRecordFiltersComponentState,
|
|
||||||
);
|
|
||||||
|
|
||||||
const recordFilter = currentRecordFilters.find(
|
|
||||||
(recordFilter) => recordFilter.id === recordFilterId,
|
|
||||||
);
|
|
||||||
|
|
||||||
const selectedFieldLabel = recordFilter?.label ?? '';
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledContainer>
|
<StyledContainer>
|
||||||
<Dropdown
|
<Dropdown
|
||||||
dropdownId={advancedFilterFieldSelectDropdownId}
|
dropdownId={advancedFilterFieldSelectDropdownId}
|
||||||
clickableComponent={
|
clickableComponent={
|
||||||
<SelectControl
|
<AdvancedFilterFieldSelectDropdownButtonClickableSelect
|
||||||
selectedOption={{
|
recordFilterId={recordFilterId}
|
||||||
label: selectedFieldLabel,
|
|
||||||
value: null,
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
dropdownComponents={
|
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 { DEFAULT_ADVANCED_FILTER_DROPDOWN_OFFSET } from '@/object-record/advanced-filter/constants/DefaultAdvancedFilterDropdownOffset';
|
||||||
import { ObjectFilterDropdownFilterInput } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterInput';
|
import { ObjectFilterDropdownFilterInput } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterInput';
|
||||||
import { fieldMetadataItemIdUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/fieldMetadataItemIdUsedInDropdownComponentState';
|
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 { selectedOperandInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/selectedOperandInDropdownComponentState';
|
||||||
import { configurableViewFilterOperands } from '@/object-record/object-filter-dropdown/utils/configurableViewFilterOperands';
|
import { configurableViewFilterOperands } from '@/object-record/object-filter-dropdown/utils/configurableViewFilterOperands';
|
||||||
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
|
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 { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
|
||||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||||
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
|
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
|
||||||
@ -55,22 +55,15 @@ export const AdvancedFilterValueInputDropdownButton = ({
|
|||||||
{operandHasNoInput ? (
|
{operandHasNoInput ? (
|
||||||
<></>
|
<></>
|
||||||
) : isDisabled ? (
|
) : isDisabled ? (
|
||||||
<SelectControl
|
<AdvancedFilterValueInputDropdownButtonClickableSelect
|
||||||
isDisabled
|
recordFilterId={recordFilterId}
|
||||||
selectedOption={{
|
|
||||||
label: filter?.displayValue ?? '',
|
|
||||||
value: null,
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Dropdown
|
<Dropdown
|
||||||
dropdownId={dropdownId}
|
dropdownId={dropdownId}
|
||||||
clickableComponent={
|
clickableComponent={
|
||||||
<SelectControl
|
<AdvancedFilterValueInputDropdownButtonClickableSelect
|
||||||
selectedOption={{
|
recordFilterId={recordFilterId}
|
||||||
label: filter?.displayValue ?? '',
|
|
||||||
value: null,
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
onOpen={() => {
|
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,
|
SelectOption,
|
||||||
} from 'twenty-ui';
|
} from 'twenty-ui';
|
||||||
|
|
||||||
|
export type SelectControlTextAccent = 'default' | 'placeholder';
|
||||||
|
|
||||||
const StyledControlContainer = styled.div<{
|
const StyledControlContainer = styled.div<{
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
hasIcon: boolean;
|
hasIcon: boolean;
|
||||||
selectSizeVariant?: SelectSizeVariant;
|
selectSizeVariant?: SelectSizeVariant;
|
||||||
|
textAccent: SelectControlTextAccent;
|
||||||
}>`
|
}>`
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: ${({ hasIcon }) =>
|
grid-template-columns: ${({ hasIcon }) =>
|
||||||
@ -26,8 +29,12 @@ const StyledControlContainer = styled.div<{
|
|||||||
background-color: ${({ theme }) => theme.background.transparent.lighter};
|
background-color: ${({ theme }) => theme.background.transparent.lighter};
|
||||||
border: 1px solid ${({ theme }) => theme.border.color.medium};
|
border: 1px solid ${({ theme }) => theme.border.color.medium};
|
||||||
border-radius: ${({ theme }) => theme.border.radius.sm};
|
border-radius: ${({ theme }) => theme.border.radius.sm};
|
||||||
color: ${({ disabled, theme }) =>
|
color: ${({ disabled, theme, textAccent }) =>
|
||||||
disabled ? theme.font.color.tertiary : theme.font.color.primary};
|
disabled
|
||||||
|
? theme.font.color.tertiary
|
||||||
|
: textAccent === 'default'
|
||||||
|
? theme.font.color.primary
|
||||||
|
: theme.font.color.secondary};
|
||||||
cursor: ${({ disabled }) => (disabled ? 'not-allowed' : 'pointer')};
|
cursor: ${({ disabled }) => (disabled ? 'not-allowed' : 'pointer')};
|
||||||
text-align: left;
|
text-align: left;
|
||||||
`;
|
`;
|
||||||
@ -43,12 +50,14 @@ type SelectControlProps = {
|
|||||||
selectedOption: SelectOption<string | number | boolean | null>;
|
selectedOption: SelectOption<string | number | boolean | null>;
|
||||||
isDisabled?: boolean;
|
isDisabled?: boolean;
|
||||||
selectSizeVariant?: SelectSizeVariant;
|
selectSizeVariant?: SelectSizeVariant;
|
||||||
|
textAccent?: SelectControlTextAccent;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SelectControl = ({
|
export const SelectControl = ({
|
||||||
selectedOption,
|
selectedOption,
|
||||||
isDisabled,
|
isDisabled,
|
||||||
selectSizeVariant,
|
selectSizeVariant,
|
||||||
|
textAccent = 'default',
|
||||||
}: SelectControlProps) => {
|
}: SelectControlProps) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
|
||||||
@ -57,6 +66,7 @@ export const SelectControl = ({
|
|||||||
disabled={isDisabled}
|
disabled={isDisabled}
|
||||||
hasIcon={isDefined(selectedOption.Icon)}
|
hasIcon={isDefined(selectedOption.Icon)}
|
||||||
selectSizeVariant={selectSizeVariant}
|
selectSizeVariant={selectSizeVariant}
|
||||||
|
textAccent={textAccent}
|
||||||
>
|
>
|
||||||
{isDefined(selectedOption.Icon) ? (
|
{isDefined(selectedOption.Icon) ? (
|
||||||
<selectedOption.Icon
|
<selectedOption.Icon
|
||||||
|
|||||||
Reference in New Issue
Block a user