From 59e8e0633bfc1c9f625b4aab1017ccf0f70ee556 Mon Sep 17 00:00:00 2001 From: Lucas Bordeau Date: Fri, 4 Apr 2025 14:45:21 +0200 Subject: [PATCH] Fix advanced filter dropdown input components (#11381) This PR fixes the filter value input components that are used in advanced dropdown filter, which slightly differ from the classic object filter dropdown in the view bar. We notably needed the same experience as other text inputs in the application, for entering filter values. ## New text and number filter experience : image ## New date filter experience : image To obtain the same experience for date input as in workflow forms, it would require to duplicate or factorize a lot of complex code that manipulates dates and user events with the input, it would be better tackled in another issue related to a larger refactor effort : https://github.com/twentyhq/core-team-issues/issues/736 Fixes https://github.com/twentyhq/core-team-issues/issues/674 --- .../AdvancedFilterAddFilterRuleSelect.tsx | 23 +++- .../AdvancedFilterDropdownDateInput.tsx | 122 ++++++++++++++++++ .../AdvancedFilterDropdownFilterInput.tsx | 83 ++++++++++++ .../AdvancedFilterDropdownNumberInput.tsx | 64 +++++++++ .../AdvancedFilterDropdownTextInput.tsx | 63 +++++++++ .../AdvancedFilterRecordFilterRow.tsx | 13 +- .../AdvancedFilterSubFieldSelectMenu.tsx | 19 --- ...utton.tsx => AdvancedFilterValueInput.tsx} | 56 ++++---- ...alueInputDropdownButtonClickableSelect.tsx | 28 +++- ...rdFilterUsedInAdvancedFilterDropdownRow.ts | 47 +++++++ ...ObjectFilterDropdownComponentInstanceId.ts | 5 + .../components/AdvancedFilterButton.tsx | 17 ++- .../ui/input/components/SelectControl.tsx | 1 + 13 files changed, 478 insertions(+), 63 deletions(-) create mode 100644 packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterDropdownDateInput.tsx create mode 100644 packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterDropdownFilterInput.tsx create mode 100644 packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterDropdownNumberInput.tsx create mode 100644 packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterDropdownTextInput.tsx rename packages/twenty-front/src/modules/object-record/advanced-filter/components/{AdvancedFilterValueInputDropdownButton.tsx => AdvancedFilterValueInput.tsx} (60%) create mode 100644 packages/twenty-front/src/modules/object-record/advanced-filter/hooks/useSetRecordFilterUsedInAdvancedFilterDropdownRow.ts create mode 100644 packages/twenty-front/src/modules/object-record/advanced-filter/utils/getAdvancedFilterObjectFilterDropdownComponentInstanceId.ts diff --git a/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterAddFilterRuleSelect.tsx b/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterAddFilterRuleSelect.tsx index da22e0d22..9bdedea1d 100644 --- a/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterAddFilterRuleSelect.tsx +++ b/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterAddFilterRuleSelect.tsx @@ -1,21 +1,23 @@ import { getFilterTypeFromFieldType } from '@/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions'; import { useChildRecordFiltersAndRecordFilterGroups } from '@/object-record/advanced-filter/hooks/useChildRecordFiltersAndRecordFilterGroups'; import { useDefaultFieldMetadataItemForFilter } from '@/object-record/advanced-filter/hooks/useDefaultFieldMetadataItemForFilter'; +import { useSetRecordFilterUsedInAdvancedFilterDropdownRow } from '@/object-record/advanced-filter/hooks/useSetRecordFilterUsedInAdvancedFilterDropdownRow'; import { getAdvancedFilterAddFilterRuleSelectDropdownId } from '@/object-record/advanced-filter/utils/getAdvancedFilterAddFilterRuleSelectDropdownId'; import { useUpsertRecordFilterGroup } from '@/object-record/record-filter-group/hooks/useUpsertRecordFilterGroup'; import { RecordFilterGroup } from '@/object-record/record-filter-group/types/RecordFilterGroup'; import { RecordFilterGroupLogicalOperator } from '@/object-record/record-filter-group/types/RecordFilterGroupLogicalOperator'; import { useUpsertRecordFilter } from '@/object-record/record-filter/hooks/useUpsertRecordFilter'; +import { RecordFilter } from '@/object-record/record-filter/types/RecordFilter'; import { getRecordFilterOperands } from '@/object-record/record-filter/utils/getRecordFilterOperands'; 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 { useGetCurrentViewOnly } from '@/views/hooks/useGetCurrentViewOnly'; -import { v4 } from 'uuid'; import { isDefined } from 'twenty-shared/utils'; import { IconLibraryPlus, IconPlus } from 'twenty-ui/display'; import { LightButton } from 'twenty-ui/input'; import { MenuItem } from 'twenty-ui/navigation'; +import { v4 } from 'uuid'; type AdvancedFilterAddFilterRuleSelectProps = { recordFilterGroup: RecordFilterGroup; @@ -45,6 +47,9 @@ export const AdvancedFilterAddFilterRuleSelect = ({ const { defaultFieldMetadataItemForFilter } = useDefaultFieldMetadataItemForFilter(); + const { setRecordFilterUsedInAdvancedFilterDropdownRow } = + useSetRecordFilterUsedInAdvancedFilterDropdownRow(); + const handleAddFilter = () => { if (!isDefined(defaultFieldMetadataItemForFilter)) { throw new Error('Missing default field metadata item for filter'); @@ -56,7 +61,7 @@ export const AdvancedFilterAddFilterRuleSelect = ({ defaultFieldMetadataItemForFilter.type, ); - upsertRecordFilter({ + const newRecordFilter: RecordFilter = { id: v4(), fieldMetadataId: defaultFieldMetadataItemForFilter.id, type: filterType, @@ -68,7 +73,11 @@ export const AdvancedFilterAddFilterRuleSelect = ({ recordFilterGroupId: recordFilterGroup.id, positionInRecordFilterGroup: newPositionInRecordFilterGroup, label: defaultFieldMetadataItemForFilter.label, - }); + }; + + upsertRecordFilter(newRecordFilter); + + setRecordFilterUsedInAdvancedFilterDropdownRow(newRecordFilter); }; const handleAddFilterGroup = () => { @@ -97,7 +106,7 @@ export const AdvancedFilterAddFilterRuleSelect = ({ defaultFieldMetadataItemForFilter.type, ); - upsertRecordFilter({ + const newRecordFilter: RecordFilter = { id: v4(), fieldMetadataId: defaultFieldMetadataItemForFilter.id, type: filterType, @@ -109,7 +118,11 @@ export const AdvancedFilterAddFilterRuleSelect = ({ recordFilterGroupId: newRecordFilterGroupId, positionInRecordFilterGroup: 1, label: defaultFieldMetadataItemForFilter.label, - }); + }; + + upsertRecordFilter(newRecordFilter); + + setRecordFilterUsedInAdvancedFilterDropdownRow(newRecordFilter); }; const isFilterRuleGroupOptionVisible = !isDefined( diff --git a/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterDropdownDateInput.tsx b/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterDropdownDateInput.tsx new file mode 100644 index 000000000..1a3f4005f --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterDropdownDateInput.tsx @@ -0,0 +1,122 @@ +import { v4 } from 'uuid'; + +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 { getRelativeDateDisplayValue } from '@/object-record/object-filter-dropdown/utils/getRelativeDateDisplayValue'; +import { useApplyRecordFilter } from '@/object-record/record-filter/hooks/useApplyRecordFilter'; +import { DateTimePicker } from '@/ui/input/components/internal/date/components/InternalDatePicker'; +import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; +import { ViewFilterOperand } from '@/views/types/ViewFilterOperand'; +import { computeVariableDateViewFilterValue } from '@/views/view-filter-value/utils/computeVariableDateViewFilterValue'; +import { + resolveDateViewFilterValue, + VariableDateViewFilterValueDirection, + VariableDateViewFilterValueUnit, +} from '@/views/view-filter-value/utils/resolveDateViewFilterValue'; +import { useState } from 'react'; +import { isDefined } from 'twenty-shared/utils'; +import { FieldMetadataType } from '~/generated-metadata/graphql'; + +export const AdvancedFilterDropdownDateInput = () => { + const fieldMetadataItemUsedInDropdown = useRecoilComponentValueV2( + fieldMetadataItemUsedInDropdownComponentSelector, + ); + + const selectedOperandInDropdown = useRecoilComponentValueV2( + selectedOperandInDropdownComponentState, + ); + + const selectedFilter = useRecoilComponentValueV2( + selectedFilterComponentState, + ); + + const { applyRecordFilter } = useApplyRecordFilter(); + + const initialFilterValue = selectedFilter + ? resolveDateViewFilterValue(selectedFilter) + : null; + const [internalDate, setInternalDate] = useState( + initialFilterValue instanceof Date ? initialFilterValue : null, + ); + + const isDateTimeInput = + fieldMetadataItemUsedInDropdown?.type === FieldMetadataType.DATE_TIME; + + const handleAbsoluteDateChange = (newDate: Date | null) => { + setInternalDate(newDate); + + if (!fieldMetadataItemUsedInDropdown || !selectedOperandInDropdown) return; + + const newDisplayValue = isDefined(newDate) + ? newDate.toLocaleDateString() + : ''; + + applyRecordFilter({ + id: selectedFilter?.id ? selectedFilter.id : v4(), + fieldMetadataId: fieldMetadataItemUsedInDropdown.id, + value: newDate?.toISOString() ?? '', + operand: selectedOperandInDropdown, + displayValue: newDisplayValue, + recordFilterGroupId: selectedFilter?.recordFilterGroupId, + positionInRecordFilterGroup: selectedFilter?.positionInRecordFilterGroup, + type: getFilterTypeFromFieldType(fieldMetadataItemUsedInDropdown.type), + label: fieldMetadataItemUsedInDropdown.label, + }); + }; + + const handleRelativeDateChange = ( + relativeDate: { + direction: VariableDateViewFilterValueDirection; + amount?: number; + unit: VariableDateViewFilterValueUnit; + } | null, + ) => { + if (!fieldMetadataItemUsedInDropdown || !selectedOperandInDropdown) return; + + const value = relativeDate + ? computeVariableDateViewFilterValue( + relativeDate.direction, + relativeDate.amount, + relativeDate.unit, + ) + : ''; + + applyRecordFilter({ + id: selectedFilter?.id ? selectedFilter.id : v4(), + fieldMetadataId: fieldMetadataItemUsedInDropdown.id, + value, + operand: selectedOperandInDropdown, + displayValue: getRelativeDateDisplayValue(relativeDate), + recordFilterGroupId: selectedFilter?.recordFilterGroupId, + positionInRecordFilterGroup: selectedFilter?.positionInRecordFilterGroup, + type: getFilterTypeFromFieldType(fieldMetadataItemUsedInDropdown.type), + label: fieldMetadataItemUsedInDropdown.label, + }); + }; + + const isRelativeOperand = + selectedOperandInDropdown === ViewFilterOperand.IsRelative; + + const resolvedValue = selectedFilter + ? resolveDateViewFilterValue(selectedFilter) + : null; + + const relativeDate = + resolvedValue && !(resolvedValue instanceof Date) + ? resolvedValue + : undefined; + + return ( + + ); +}; diff --git a/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterDropdownFilterInput.tsx b/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterDropdownFilterInput.tsx new file mode 100644 index 000000000..d94cea8e6 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterDropdownFilterInput.tsx @@ -0,0 +1,83 @@ +import { ObjectFilterDropdownOptionSelect } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownOptionSelect'; +import { ObjectFilterDropdownRatingInput } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownRatingInput'; +import { ObjectFilterDropdownRecordSelect } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownRecordSelect'; +import { ObjectFilterDropdownSearchInput } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownSearchInput'; +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 { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; +import { isDefined } from 'twenty-shared/utils'; + +type AdvancedFilterDropdownFilterInputProps = { + filterDropdownId?: string; + recordFilterId?: string; +}; + +export const AdvancedFilterDropdownFilterInput = ({ + filterDropdownId, + recordFilterId, +}: AdvancedFilterDropdownFilterInputProps) => { + const fieldMetadataItemUsedInDropdown = useRecoilComponentValueV2( + fieldMetadataItemUsedInDropdownComponentSelector, + filterDropdownId, + ); + + const subFieldNameUsedInDropdown = useRecoilComponentValueV2( + subFieldNameUsedInDropdownComponentState, + filterDropdownId, + ); + + if (!isDefined(fieldMetadataItemUsedInDropdown)) { + return null; + } + + const filterType = getFilterTypeFromFieldType( + fieldMetadataItemUsedInDropdown.type, + ); + + const isActorSourceCompositeFilter = isFilterOnActorSourceSubField( + subFieldNameUsedInDropdown, + ); + + return ( + <> + {filterType === 'RATING' && } + {DATE_FILTER_TYPES.includes(filterType) && ( + + )} + {filterType === 'RELATION' && ( + <> + + + + + )} + {filterType === 'ACTOR' && + (isActorSourceCompositeFilter ? ( + <> + + + ) : ( + <> + + + ))} + {['SELECT', 'MULTI_SELECT'].includes(filterType) && ( + <> + + + + + )} + {filterType === 'BOOLEAN' && } + + ); +}; diff --git a/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterDropdownNumberInput.tsx b/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterDropdownNumberInput.tsx new file mode 100644 index 000000000..e8cf61cae --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterDropdownNumberInput.tsx @@ -0,0 +1,64 @@ +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 { TextInputV2 } from '@/ui/input/components/TextInputV2'; +import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; +import { v4 } from 'uuid'; + +export const AdvancedFilterDropdownNumberInput = () => { + const selectedOperandInDropdown = useRecoilComponentValueV2( + selectedOperandInDropdownComponentState, + ); + + const fieldMetadataItemUsedInDropdown = useRecoilComponentValueV2( + fieldMetadataItemUsedInDropdownComponentSelector, + ); + + const selectedFilter = useRecoilComponentValueV2( + selectedFilterComponentState, + ); + + const { applyRecordFilter } = useApplyRecordFilter(); + + const [inputValue, setInputValue] = useState( + () => selectedFilter?.value || '', + ); + + const handleChange = (newValue: string) => { + if (!fieldMetadataItemUsedInDropdown || !selectedOperandInDropdown) { + return; + } + + setInputValue(newValue); + + applyRecordFilter({ + id: selectedFilter?.id ? selectedFilter.id : v4(), + fieldMetadataId: fieldMetadataItemUsedInDropdown?.id ?? '', + value: newValue, + operand: selectedOperandInDropdown, + displayValue: newValue, + type: getFilterTypeFromFieldType(fieldMetadataItemUsedInDropdown.type), + label: fieldMetadataItemUsedInDropdown.label, + recordFilterGroupId: selectedFilter?.recordFilterGroupId, + positionInRecordFilterGroup: selectedFilter?.positionInRecordFilterGroup, + }); + }; + + if (!selectedOperandInDropdown || !fieldMetadataItemUsedInDropdown) { + return null; + } + + return ( + + ); +}; diff --git a/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterDropdownTextInput.tsx b/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterDropdownTextInput.tsx new file mode 100644 index 000000000..37d03684e --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterDropdownTextInput.tsx @@ -0,0 +1,63 @@ +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 { 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, + ); + + const { applyRecordFilter } = useApplyRecordFilter(); + + const [inputValue, setInputValue] = useState( + () => selectedFilter?.value || '', + ); + + const handleChange = (newValue: string) => { + if (!fieldMetadataItemUsedInDropdown || !selectedOperandInDropdown) { + return; + } + + setInputValue(newValue); + + applyRecordFilter({ + id: selectedFilter?.id ? selectedFilter.id : v4(), + fieldMetadataId: fieldMetadataItemUsedInDropdown?.id ?? '', + value: newValue, + operand: selectedOperandInDropdown, + displayValue: newValue, + type: getFilterTypeFromFieldType(fieldMetadataItemUsedInDropdown.type), + label: fieldMetadataItemUsedInDropdown.label, + recordFilterGroupId: selectedFilter?.recordFilterGroupId, + positionInRecordFilterGroup: selectedFilter?.positionInRecordFilterGroup, + }); + }; + + if (!selectedOperandInDropdown || !fieldMetadataItemUsedInDropdown) { + return null; + } + + return ( + + ); +}; diff --git a/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterRecordFilterRow.tsx b/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterRecordFilterRow.tsx index 7c57f6c5d..7fab53937 100644 --- a/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterRecordFilterRow.tsx +++ b/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterRecordFilterRow.tsx @@ -3,7 +3,8 @@ import { AdvancedFilterFieldSelectDropdownButton } from '@/object-record/advance import { AdvancedFilterLogicalOperatorCell } from '@/object-record/advanced-filter/components/AdvancedFilterLogicalOperatorCell'; import { AdvancedFilterRecordFilterOperandSelect } from '@/object-record/advanced-filter/components/AdvancedFilterRecordFilterOperandSelect'; import { AdvancedFilterRecordFilterOptionsDropdown } from '@/object-record/advanced-filter/components/AdvancedFilterRecordFilterOptionsDropdown'; -import { AdvancedFilterValueInputDropdownButton } from '@/object-record/advanced-filter/components/AdvancedFilterValueInputDropdownButton'; +import { AdvancedFilterValueInput } from '@/object-record/advanced-filter/components/AdvancedFilterValueInput'; +import { getAdvancedFilterObjectFilterDropdownComponentInstanceId } from '@/object-record/advanced-filter/utils/getAdvancedFilterObjectFilterDropdownComponentInstanceId'; import { ObjectFilterDropdownComponentInstanceContext } from '@/object-record/object-filter-dropdown/states/contexts/ObjectFilterDropdownComponentInstanceContext'; import { RecordFilterGroup } from '@/object-record/record-filter-group/types/RecordFilterGroup'; import { RecordFilter } from '@/object-record/record-filter/types/RecordFilter'; @@ -19,7 +20,11 @@ export const AdvancedFilterRecordFilterRow = ({ }) => { return ( - + diff --git a/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterSubFieldSelectMenu.tsx b/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterSubFieldSelectMenu.tsx index 5b4353c56..0db3aef9f 100644 --- a/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterSubFieldSelectMenu.tsx +++ b/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterSubFieldSelectMenu.tsx @@ -1,18 +1,14 @@ import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; -import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2'; import { isDefined } from 'twenty-shared/utils'; import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; import { useAdvancedFilterFieldSelectDropdown } from '@/object-record/advanced-filter/hooks/useAdvancedFilterFieldSelectDropdown'; import { useSelectFieldUsedInAdvancedFilterDropdown } from '@/object-record/advanced-filter/hooks/useSelectFieldUsedInAdvancedFilterDropdown'; -import { fieldMetadataItemIdUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/fieldMetadataItemIdUsedInDropdownComponentState'; import { fieldMetadataItemUsedInDropdownComponentSelector } from '@/object-record/object-filter-dropdown/states/fieldMetadataItemUsedInDropdownComponentSelector'; -import { objectFilterDropdownFilterIsSelectedComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownFilterIsSelectedComponentState'; import { objectFilterDropdownIsSelectingCompositeFieldComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownIsSelectingCompositeFieldComponentState'; import { objectFilterDropdownSubMenuFieldTypeComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownSubMenuFieldTypeComponentState'; -import { subFieldNameUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/subFieldNameUsedInDropdownComponentState'; import { getCompositeSubFieldLabel } from '@/object-record/object-filter-dropdown/utils/getCompositeSubFieldLabel'; import { getFilterableFieldTypeLabel } from '@/object-record/object-filter-dropdown/utils/getFilterableFieldTypeLabel'; import { SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS } from '@/settings/data-model/constants/SettingsCompositeFieldTypeConfigs'; @@ -35,14 +31,6 @@ export const AdvancedFilterSubFieldSelectMenu = ({ fieldMetadataItemUsedInDropdownComponentSelector, ); - const [, setObjectFilterDropdownFilterIsSelected] = useRecoilComponentStateV2( - objectFilterDropdownFilterIsSelectedComponentState, - ); - - const [, setSubFieldNameUsedInDropdown] = useRecoilComponentStateV2( - subFieldNameUsedInDropdownComponentState, - ); - const [, setObjectFilterDropdownIsSelectingCompositeField] = useRecoilComponentStateV2( objectFilterDropdownIsSelectingCompositeFieldComponentState, @@ -55,10 +43,6 @@ export const AdvancedFilterSubFieldSelectMenu = ({ objectFilterDropdownSubMenuFieldTypeComponentState, ); - const setFieldMetadataItemIdUsedInDropdown = useSetRecoilComponentStateV2( - fieldMetadataItemIdUsedInDropdownComponentState, - ); - const { closeAdvancedFilterFieldSelectDropdown } = useAdvancedFilterFieldSelectDropdown(recordFilterId); @@ -83,11 +67,8 @@ export const AdvancedFilterSubFieldSelectMenu = ({ }; const handleSubMenuBack = () => { - setFieldMetadataItemIdUsedInDropdown(null); setObjectFilterDropdownSubMenuFieldType(null); setObjectFilterDropdownIsSelectingCompositeField(false); - setObjectFilterDropdownFilterIsSelected(false); - setSubFieldNameUsedInDropdown(null); }; if (!isDefined(objectFilterDropdownSubMenuFieldType)) { diff --git a/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterValueInputDropdownButton.tsx b/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterValueInput.tsx similarity index 60% rename from packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterValueInputDropdownButton.tsx rename to packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterValueInput.tsx index 224b720c3..b804e2055 100644 --- a/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterValueInputDropdownButton.tsx +++ b/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterValueInput.tsx @@ -1,64 +1,61 @@ +import { AdvancedFilterDropdownFilterInput } from '@/object-record/advanced-filter/components/AdvancedFilterDropdownFilterInput'; +import { AdvancedFilterDropdownTextInput } from '@/object-record/advanced-filter/components/AdvancedFilterDropdownTextInput'; 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'; +import { NUMBER_FILTER_TYPES } from '@/object-record/object-filter-dropdown/constants/NumberFilterTypes'; +import { TEXT_FILTER_TYPES } from '@/object-record/object-filter-dropdown/constants/TextFilterTypes'; import { objectFilterDropdownSearchInputComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownSearchInputComponentState'; -import { selectedFilterComponentState } from '@/object-record/object-filter-dropdown/states/selectedFilterComponentState'; -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 { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; +import { DropdownOffset } from '@/ui/layout/dropdown/types/DropdownOffset'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2'; import styled from '@emotion/styled'; +import { isDefined } from 'twenty-shared/utils'; const StyledValueDropdownContainer = styled.div` flex: 3; `; -type AdvancedFilterValueInputDropdownButtonProps = { +type AdvancedFilterValueInputProps = { recordFilterId: string; }; -export const AdvancedFilterValueInputDropdownButton = ({ +export const AdvancedFilterValueInput = ({ recordFilterId, -}: AdvancedFilterValueInputDropdownButtonProps) => { +}: AdvancedFilterValueInputProps) => { const dropdownId = `advanced-filter-view-filter-value-input-${recordFilterId}`; const currentRecordFilters = useRecoilComponentValueV2( currentRecordFiltersComponentState, ); - const filter = currentRecordFilters.find( + const recordFilter = currentRecordFilters.find( (recordFilter) => recordFilter.id === recordFilterId, ); - const isDisabled = !filter?.fieldMetadataId || !filter.operand; + const isDisabled = !recordFilter?.fieldMetadataId || !recordFilter.operand; const setObjectFilterDropdownSearchInput = useSetRecoilComponentStateV2( objectFilterDropdownSearchInputComponentState, ); - const setFieldMetadataItemIdUsedInDropdown = useSetRecoilComponentStateV2( - fieldMetadataItemIdUsedInDropdownComponentState, - ); - - const setSelectedOperandInDropdown = useSetRecoilComponentStateV2( - selectedOperandInDropdownComponentState, - ); - - const setSelectedFilter = useSetRecoilComponentStateV2( - selectedFilterComponentState, - ); - const operandHasNoInput = - filter && !configurableViewFilterOperands.has(filter.operand); + recordFilter && !configurableViewFilterOperands.has(recordFilter.operand); const handleFilterValueDropdownClose = () => { setObjectFilterDropdownSearchInput(''); }; + const filterType = recordFilter?.type; + + const dropdownContentOffset = + filterType === 'DATE' || filterType === 'DATE_TIME' + ? ({ y: -33, x: 0 } satisfies DropdownOffset) + : DEFAULT_ADVANCED_FILTER_DROPDOWN_OFFSET; + return ( {operandHasNoInput ? ( @@ -67,6 +64,10 @@ export const AdvancedFilterValueInputDropdownButton = ({ + ) : isDefined(filterType) && + (TEXT_FILTER_TYPES.includes(filterType) || + NUMBER_FILTER_TYPES.includes(filterType)) ? ( + ) : ( } - onOpen={() => { - setFieldMetadataItemIdUsedInDropdown(filter.fieldMetadataId); - setSelectedOperandInDropdown(filter.operand); - setSelectedFilter(filter); - }} dropdownComponents={ - + } dropdownHotkeyScope={{ scope: dropdownId }} - dropdownOffset={DEFAULT_ADVANCED_FILTER_DROPDOWN_OFFSET} + dropdownOffset={dropdownContentOffset} dropdownPlacement="bottom-start" dropdownMenuWidth={280} onClose={handleFilterValueDropdownClose} diff --git a/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterValueInputDropdownButtonClickableSelect.tsx b/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterValueInputDropdownButtonClickableSelect.tsx index 62648bb2f..18d1871d4 100644 --- a/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterValueInputDropdownButtonClickableSelect.tsx +++ b/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterValueInputDropdownButtonClickableSelect.tsx @@ -3,10 +3,30 @@ import { getAdvancedFilterInputPlaceholderText } from '@/object-record/advanced- 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 styled from '@emotion/styled'; + import { isDefined } from 'twenty-shared/utils'; +// TODO: factorize this with https://github.com/twentyhq/core-team-issues/issues/752 +const StyledControlContainer = styled.div` + display: flex; + align-items: center; + gap: ${({ theme }) => theme.spacing(1)}; + box-sizing: border-box; + height: ${({ theme }) => theme.spacing(8)}; + max-width: 100%; + padding: 0 ${({ theme }) => theme.spacing(2)}; + background-color: ${({ theme }) => theme.background.transparent.lighter}; + border: 1px solid ${({ theme }) => theme.border.color.medium}; + border-radius: ${({ theme }) => theme.border.radius.sm}; + color: ${({ theme }) => theme.font.color.primary}; + cursor: pointer; + text-align: left; +`; + type AdvancedFilterValueInputDropdownButtonClickableSelectProps = { recordFilterId: string; }; @@ -42,7 +62,12 @@ export const AdvancedFilterValueInputDropdownButtonClickableSelect = ({ ? placeholderText : (recordFilter?.displayValue ?? ''); - return ( + const isDateTimeType = + recordFilter?.type === 'DATE' || recordFilter?.type === 'DATE_TIME'; + + return isDateTimeType ? ( + {advancedFilterInputText} + ) : ( ); }; diff --git a/packages/twenty-front/src/modules/object-record/advanced-filter/hooks/useSetRecordFilterUsedInAdvancedFilterDropdownRow.ts b/packages/twenty-front/src/modules/object-record/advanced-filter/hooks/useSetRecordFilterUsedInAdvancedFilterDropdownRow.ts new file mode 100644 index 000000000..f9a00b8fd --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/advanced-filter/hooks/useSetRecordFilterUsedInAdvancedFilterDropdownRow.ts @@ -0,0 +1,47 @@ +import { getAdvancedFilterObjectFilterDropdownComponentInstanceId } from '@/object-record/advanced-filter/utils/getAdvancedFilterObjectFilterDropdownComponentInstanceId'; +import { fieldMetadataItemIdUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/fieldMetadataItemIdUsedInDropdownComponentState'; +import { selectedFilterComponentState } from '@/object-record/object-filter-dropdown/states/selectedFilterComponentState'; +import { selectedOperandInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/selectedOperandInDropdownComponentState'; +import { RecordFilter } from '@/object-record/record-filter/types/RecordFilter'; +import { useRecoilCallback } from 'recoil'; + +export const useSetRecordFilterUsedInAdvancedFilterDropdownRow = () => { + const setRecordFilterUsedInAdvancedFilterDropdownRow = useRecoilCallback( + ({ set }) => + (recordFilter: RecordFilter) => { + const advancedFilterRowObjectFilterDropdownComponentInstanceId = + getAdvancedFilterObjectFilterDropdownComponentInstanceId( + recordFilter.id, + ); + + set( + fieldMetadataItemIdUsedInDropdownComponentState.atomFamily({ + instanceId: + advancedFilterRowObjectFilterDropdownComponentInstanceId, + }), + recordFilter.fieldMetadataId, + ); + + set( + selectedOperandInDropdownComponentState.atomFamily({ + instanceId: + advancedFilterRowObjectFilterDropdownComponentInstanceId, + }), + recordFilter.operand, + ); + + set( + selectedFilterComponentState.atomFamily({ + instanceId: + advancedFilterRowObjectFilterDropdownComponentInstanceId, + }), + recordFilter, + ); + }, + [], + ); + + return { + setRecordFilterUsedInAdvancedFilterDropdownRow, + }; +}; diff --git a/packages/twenty-front/src/modules/object-record/advanced-filter/utils/getAdvancedFilterObjectFilterDropdownComponentInstanceId.ts b/packages/twenty-front/src/modules/object-record/advanced-filter/utils/getAdvancedFilterObjectFilterDropdownComponentInstanceId.ts new file mode 100644 index 000000000..f42c2e4ae --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/advanced-filter/utils/getAdvancedFilterObjectFilterDropdownComponentInstanceId.ts @@ -0,0 +1,5 @@ +export const getAdvancedFilterObjectFilterDropdownComponentInstanceId = ( + recordFilterId: string, +) => { + return `advanced-filter-${recordFilterId}`; +}; diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/AdvancedFilterButton.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/AdvancedFilterButton.tsx index db9a50cd2..4fde4314e 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/AdvancedFilterButton.tsx +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/AdvancedFilterButton.tsx @@ -8,6 +8,8 @@ import { RecordFilterGroupLogicalOperator } from '@/object-record/record-filter- import { useUpsertRecordFilter } from '@/object-record/record-filter/hooks/useUpsertRecordFilter'; import { getRecordFilterOperands } from '@/object-record/record-filter/utils/getRecordFilterOperands'; +import { useSetRecordFilterUsedInAdvancedFilterDropdownRow } from '@/object-record/advanced-filter/hooks/useSetRecordFilterUsedInAdvancedFilterDropdownRow'; +import { RecordFilter } from '@/object-record/record-filter/types/RecordFilter'; import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { ADVANCED_FILTER_DROPDOWN_ID } from '@/views/constants/AdvancedFilterDropdownId'; @@ -16,11 +18,11 @@ import { ViewFilterGroupLogicalOperator } from '@/views/types/ViewFilterGroupLog import styled from '@emotion/styled'; import { useLingui } from '@lingui/react/macro'; import { useRecoilValue } from 'recoil'; -import { v4 } from 'uuid'; import { isDefined } from 'twenty-shared/utils'; +import { Pill } from 'twenty-ui/components'; import { IconFilter } from 'twenty-ui/display'; import { MenuItemLeftContent, StyledMenuItemBase } from 'twenty-ui/navigation'; -import { Pill } from 'twenty-ui/components'; +import { v4 } from 'uuid'; export const StyledContainer = styled.div` align-items: center; @@ -80,6 +82,9 @@ export const AdvancedFilterButton = () => { currentRecordFilterGroupsComponentState, ); + const { setRecordFilterUsedInAdvancedFilterDropdownRow } = + useSetRecordFilterUsedInAdvancedFilterDropdownRow(); + const handleClick = () => { if (!isDefined(currentView)) { throw new Error('Missing current view id'); @@ -118,7 +123,7 @@ export const AdvancedFilterButton = () => { filterType, })[0]; - upsertRecordFilter({ + const newRecordFilter: RecordFilter = { id: v4(), fieldMetadataId: defaultFieldMetadataItem.id, operand: firstOperand, @@ -128,7 +133,11 @@ export const AdvancedFilterButton = () => { type: getFilterTypeFromFieldType(defaultFieldMetadataItem.type), label: defaultFieldMetadataItem.label, positionInRecordFilterGroup: 1, - }); + }; + + upsertRecordFilter(newRecordFilter); + + setRecordFilterUsedInAdvancedFilterDropdownRow(newRecordFilter); } closeObjectFilterDropdown(); diff --git a/packages/twenty-front/src/modules/ui/input/components/SelectControl.tsx b/packages/twenty-front/src/modules/ui/input/components/SelectControl.tsx index 4eb3a23f0..d5078f666 100644 --- a/packages/twenty-front/src/modules/ui/input/components/SelectControl.tsx +++ b/packages/twenty-front/src/modules/ui/input/components/SelectControl.tsx @@ -7,6 +7,7 @@ import { SelectOption } from 'twenty-ui/input'; export type SelectControlTextAccent = 'default' | 'placeholder'; +// TODO: factorize this with https://github.com/twentyhq/core-team-issues/issues/752 const StyledControlContainer = styled.div<{ disabled?: boolean; hasIcon: boolean;