From caf44207fd139f2efe5cde53d87ac70415386116 Mon Sep 17 00:00:00 2001 From: Lucas Bordeau Date: Thu, 17 Apr 2025 17:03:56 +0200 Subject: [PATCH] Implemented filter on FULL_NAME sub-fields (#11628) This PR implements what's missing to have sub-field filtering. There is a backend modification to save subFieldName, we just add this field on view filter workspace entity. This PR adds subFieldName where missing in frontend, notably in applyFilter calls, that will be refactored soon. Also fixes a bug in ViewBar where Add Filter button was at the right side of the ViewBar, while it should be right after the chips section. Another bug fixed where we wouldn't delete an empty record filter on dropdown click outside from the view bar, which was already the case where using the filter chip dropdown. image image --------- Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --- .../AdvancedFilterDropdownTextInput.tsx | 15 +++----- ...eldSelectDropdownButtonClickableSelect.tsx | 20 +++++++++-- .../AdvancedFilterSubFieldSelectMenu.tsx | 10 ++++-- ...SelectFieldUsedInAdvancedFilterDropdown.ts | 2 +- .../MultipleFiltersDropdownButton.tsx | 27 ++++++++++++-- .../ObjectFilterDropdownBooleanSelect.tsx | 1 + .../ObjectFilterDropdownDateInput.tsx | 1 + ...pdownFilterSelectCompositeFieldSubMenu.tsx | 10 ++++-- .../ObjectFilterDropdownNumberInput.tsx | 1 + .../ObjectFilterDropdownOptionSelect.tsx | 1 + .../ObjectFilterDropdownRatingInput.tsx | 1 + .../ObjectFilterDropdownRecordSelect.tsx | 4 ++- .../ObjectFilterDropdownTextInput.tsx | 6 ++++ .../ObjectFilterDropdownTextSearchInput.tsx | 1 + .../utils/isCompositeFieldTypeFilterable.ts | 11 ++++++ .../utils/isRecordFilterConsideredEmpty.ts | 24 +++++++++++++ .../data-model/constants/AllSubFields.ts | 7 ++++ .../SettingsCompositeFieldTypeConfigs.ts | 2 +- .../types/CompositeFieldSubFieldName.ts | 3 ++ .../data-model/utils/isValidSubFieldName.ts | 14 ++++++++ .../views/components/EditableFilterChip.tsx | 35 ++++++++++++++----- .../EditableFilterDropdownButton.tsx | 18 +++------- .../views/components/ViewBarDetails.tsx | 7 ++-- .../internal/usePersistViewFilterRecords.ts | 2 ++ .../src/modules/views/types/ViewFilter.ts | 1 + .../views/utils/areViewFiltersEqual.ts | 1 + .../utils/mapRecordFilterToViewFilter.ts | 1 + .../views/utils/mapViewFiltersToFilters.ts | 3 +- .../constants/standard-field-ids.ts | 1 + .../view-filter.workspace-entity.ts | 11 ++++++ 30 files changed, 193 insertions(+), 48 deletions(-) create mode 100644 packages/twenty-front/src/modules/object-record/record-filter/utils/isCompositeFieldTypeFilterable.ts create mode 100644 packages/twenty-front/src/modules/object-record/record-filter/utils/isRecordFilterConsideredEmpty.ts create mode 100644 packages/twenty-front/src/modules/settings/data-model/constants/AllSubFields.ts create mode 100644 packages/twenty-front/src/modules/settings/data-model/types/CompositeFieldSubFieldName.ts create mode 100644 packages/twenty-front/src/modules/settings/data-model/utils/isValidSubFieldName.ts 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 index 723bf617c..394433083 100644 --- 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 @@ -1,5 +1,3 @@ -import { useState } from 'react'; - 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'; @@ -13,27 +11,24 @@ export const AdvancedFilterDropdownTextInput = ({ }: AdvancedFilterDropdownTextInputProps) => { const { applyRecordFilter } = useApplyRecordFilter(); - const [inputValue, setInputValue] = useState(() => recordFilter?.value || ''); - const handleChange = (newValue: string) => { - setInputValue(newValue); - applyRecordFilter({ id: recordFilter.id, - fieldMetadataId: recordFilter?.fieldMetadataId ?? '', + fieldMetadataId: recordFilter.fieldMetadataId, value: newValue, operand: recordFilter.operand, displayValue: newValue, type: recordFilter.type, label: recordFilter.label, - recordFilterGroupId: recordFilter?.recordFilterGroupId, - positionInRecordFilterGroup: recordFilter?.positionInRecordFilterGroup, + recordFilterGroupId: recordFilter.recordFilterGroupId, + positionInRecordFilterGroup: recordFilter.positionInRecordFilterGroup, + subFieldName: recordFilter.subFieldName, }); }; return ( a.localeCompare(b)); + const subFieldsAreFilterable = + isDefined(fieldMetadataItemUsedInDropdown) && + isCompositeFieldTypeSubFieldsFilterable( + fieldMetadataItemUsedInDropdown.type, + ); + return ( <> - {/* TODO: fix this with a backend field on ViewFilter for composite field filter */} - {fieldMetadataItemUsedInDropdown?.type === 'ACTOR' && + {subFieldsAreFilterable && options.map((subFieldName, index) => ( { existingRecordFilter?.positionInRecordFilterGroup, type: filterType, label: fieldMetadataItem.label, - subFieldName, + subFieldName: subFieldName ?? null, }); setSubFieldNameUsedInDropdown(subFieldName); diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/MultipleFiltersDropdownButton.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/MultipleFiltersDropdownButton.tsx index 2ed3ece59..9c694b643 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/MultipleFiltersDropdownButton.tsx +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/MultipleFiltersDropdownButton.tsx @@ -2,8 +2,12 @@ import { OBJECT_FILTER_DROPDOWN_ID } from '@/object-record/object-filter-dropdow import { useResetFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useResetFilterDropdown'; import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope'; -import { useCallback } from 'react'; +import { selectedFilterComponentState } from '@/object-record/object-filter-dropdown/states/selectedFilterComponentState'; +import { useRemoveRecordFilter } from '@/object-record/record-filter/hooks/useRemoveRecordFilter'; +import { isRecordFilterConsideredEmpty } from '@/object-record/record-filter/utils/isRecordFilterConsideredEmpty'; +import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; +import { isDefined } from 'twenty-shared/utils'; import { MultipleFiltersButton } from './MultipleFiltersButton'; import { MultipleFiltersDropdownContent } from './MultipleFiltersDropdownContent'; @@ -16,9 +20,25 @@ export const MultipleFiltersDropdownButton = ({ }: MultipleFiltersDropdownButtonProps) => { const { resetFilterDropdown } = useResetFilterDropdown(); - const handleDropdownClose = useCallback(() => { + const { removeRecordFilter } = useRemoveRecordFilter(); + + const selectedFilter = useRecoilComponentValueV2( + selectedFilterComponentState, + ); + + const handleDropdownClickOutside = () => { + const recordFilterIsEmpty = + isDefined(selectedFilter) && + isRecordFilterConsideredEmpty(selectedFilter); + + if (recordFilterIsEmpty) { + removeRecordFilter({ recordFilterId: selectedFilter.id }); + } + }; + + const handleDropdownClose = () => { resetFilterDropdown(); - }, [resetFilterDropdown]); + }; return ( } dropdownHotkeyScope={hotkeyScope} dropdownOffset={{ y: 8 }} + onClickOutside={handleDropdownClickOutside} /> ); }; diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownBooleanSelect.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownBooleanSelect.tsx index 9cb5f1980..6fb77ac0b 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownBooleanSelect.tsx +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownBooleanSelect.tsx @@ -82,6 +82,7 @@ export const ObjectFilterDropdownBooleanSelect = () => { positionInRecordFilterGroup: selectedFilter?.positionInRecordFilterGroup, type: getFilterTypeFromFieldType(fieldMetadataItemUsedInDropdown.type), label: fieldMetadataItemUsedInDropdown.label, + subFieldName: selectedFilter?.subFieldName, }); setSelectedValue(value); diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownDateInput.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownDateInput.tsx index 6e6205d4d..9d0ebc99e 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownDateInput.tsx +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownDateInput.tsx @@ -63,6 +63,7 @@ export const ObjectFilterDropdownDateInput = () => { positionInRecordFilterGroup: selectedFilter?.positionInRecordFilterGroup, type: getFilterTypeFromFieldType(fieldMetadataItemUsedInDropdown.type), label: fieldMetadataItemUsedInDropdown.label, + subFieldName: selectedFilter?.subFieldName, }); }; diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelectCompositeFieldSubMenu.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelectCompositeFieldSubMenu.tsx index e95184286..14e5e27c4 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelectCompositeFieldSubMenu.tsx +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelectCompositeFieldSubMenu.tsx @@ -14,6 +14,7 @@ import { getFilterableFieldTypeLabel } from '@/object-record/object-filter-dropd import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState'; import { findDuplicateRecordFilterInNonAdvancedRecordFilters } from '@/object-record/record-filter/utils/findDuplicateRecordFilterInNonAdvancedRecordFilters'; import { getRecordFilterOperands } from '@/object-record/record-filter/utils/getRecordFilterOperands'; +import { isCompositeFieldTypeSubFieldsFilterable } from '@/object-record/record-filter/utils/isCompositeFieldTypeFilterable'; import { SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS } from '@/settings/data-model/constants/SettingsCompositeFieldTypeConfigs'; import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader/DropdownMenuHeader'; import { DropdownMenuHeaderLeftComponent } from '@/ui/layout/dropdown/components/DropdownMenuHeader/internal/DropdownMenuHeaderLeftComponent'; @@ -142,6 +143,12 @@ export const ObjectFilterDropdownFilterSelectCompositeFieldSubMenu = () => { item.toLocaleLowerCase().includes(searchText.toLocaleLowerCase()), ); + const subFieldsAreFilterable = + isDefined(fieldMetadataItemUsedInDropdown) && + isCompositeFieldTypeSubFieldsFilterable( + fieldMetadataItemUsedInDropdown.type, + ); + return ( <> { LeftIcon={IconApps} text={`Any ${getFilterableFieldTypeLabel(objectFilterDropdownSubMenuFieldType)} field`} /> - {/* TODO: fix this with a backend field on ViewFilter for composite field filter */} - {fieldMetadataItemUsedInDropdown?.type === 'ACTOR' && + {subFieldsAreFilterable && options.map((subFieldName, index) => ( { recordFilterGroupId: selectedFilter?.recordFilterGroupId, positionInRecordFilterGroup: selectedFilter?.positionInRecordFilterGroup, + subFieldName: selectedFilter?.subFieldName, }); }} /> diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownOptionSelect.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownOptionSelect.tsx index 5cfdfc526..ca899d407 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownOptionSelect.tsx +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownOptionSelect.tsx @@ -147,6 +147,7 @@ export const ObjectFilterDropdownOptionSelect = () => { recordFilterGroupId: selectedFilter?.recordFilterGroupId, positionInRecordFilterGroup: selectedFilter?.positionInRecordFilterGroup, + subFieldName: selectedFilter?.subFieldName, }); } resetSelectedItem(); diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownRatingInput.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownRatingInput.tsx index 55b7e03a2..85590df54 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownRatingInput.tsx +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownRatingInput.tsx @@ -77,6 +77,7 @@ export const ObjectFilterDropdownRatingInput = () => { fieldMetadataItemUsedInDropdown.type, ), label: fieldMetadataItemUsedInDropdown.label, + subFieldName: selectedFilter?.subFieldName, }); }} /> diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownRecordSelect.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownRecordSelect.tsx index 412172f4b..85f419456 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownRecordSelect.tsx +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownRecordSelect.tsx @@ -22,8 +22,8 @@ import { RelationFilterValue } from '@/views/view-filter-value/types/RelationFil import { jsonRelationFilterValueSchema } from '@/views/view-filter-value/validation-schemas/jsonRelationFilterValueSchema'; import { simpleRelationFilterValueSchema } from '@/views/view-filter-value/validation-schemas/simpleRelationFilterValueSchema'; import { isDefined } from 'twenty-shared/utils'; -import { v4 } from 'uuid'; import { IconUserCircle } from 'twenty-ui/display'; +import { v4 } from 'uuid'; export const EMPTY_FILTER_VALUE: string = JSON.stringify({ isCurrentWorkspaceMemberSelected: false, @@ -235,6 +235,7 @@ export const ObjectFilterDropdownRecordSelect = ({ duplicateFilterInCurrentRecordFilters.recordFilterGroupId, positionInRecordFilterGroup: duplicateFilterInCurrentRecordFilters.positionInRecordFilterGroup, + subFieldName: duplicateFilterInCurrentRecordFilters.subFieldName, }); } else { applyRecordFilter({ @@ -250,6 +251,7 @@ export const ObjectFilterDropdownRecordSelect = ({ recordFilterGroupId: selectedFilter?.recordFilterGroupId, positionInRecordFilterGroup: selectedFilter?.positionInRecordFilterGroup, + subFieldName: selectedFilter?.subFieldName, }); } } diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownTextInput.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownTextInput.tsx index 72b61368d..eaba93073 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownTextInput.tsx +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownTextInput.tsx @@ -5,6 +5,7 @@ import { getFilterTypeFromFieldType } from '@/object-metadata/utils/formatFieldM 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 { subFieldNameUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/subFieldNameUsedInDropdownComponentState'; import { useApplyRecordFilter } from '@/object-record/record-filter/hooks/useApplyRecordFilter'; import { DropdownMenuInput } from '@/ui/layout/dropdown/components/DropdownMenuInput'; import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; @@ -19,6 +20,10 @@ export const ObjectFilterDropdownTextInput = () => { fieldMetadataItemUsedInDropdownComponentSelector, ); + const subFieldNameUsedInDropdown = useRecoilComponentValueV2( + subFieldNameUsedInDropdownComponentState, + ); + const selectedFilter = useRecoilComponentValueV2( selectedFilterComponentState, ); @@ -70,6 +75,7 @@ export const ObjectFilterDropdownTextInput = () => { recordFilterGroupId: selectedFilter?.recordFilterGroupId, positionInRecordFilterGroup: selectedFilter?.positionInRecordFilterGroup, + subFieldName: subFieldNameUsedInDropdown, }); }} /> diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownTextSearchInput.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownTextSearchInput.tsx index c08d55278..46871720c 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownTextSearchInput.tsx +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownTextSearchInput.tsx @@ -71,6 +71,7 @@ export const ObjectFilterDropdownTextSearchInput = () => { label: fieldMetadataItemUsedInDropdown.label, positionInRecordFilterGroup: selectedFilter?.positionInRecordFilterGroup, + subFieldName: selectedFilter?.subFieldName, }); }} /> diff --git a/packages/twenty-front/src/modules/object-record/record-filter/utils/isCompositeFieldTypeFilterable.ts b/packages/twenty-front/src/modules/object-record/record-filter/utils/isCompositeFieldTypeFilterable.ts new file mode 100644 index 000000000..560886e79 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-filter/utils/isCompositeFieldTypeFilterable.ts @@ -0,0 +1,11 @@ +import { FieldType } from '@/settings/data-model/types/FieldType'; + +type CompositeFilterableFieldType = Extract; + +export const isCompositeFieldTypeSubFieldsFilterable = ( + fieldType: FieldType, +): fieldType is CompositeFilterableFieldType => { + return ( + ['ACTOR', 'FULL_NAME'] satisfies CompositeFilterableFieldType[] + ).includes(fieldType as any); +}; diff --git a/packages/twenty-front/src/modules/object-record/record-filter/utils/isRecordFilterConsideredEmpty.ts b/packages/twenty-front/src/modules/object-record/record-filter/utils/isRecordFilterConsideredEmpty.ts new file mode 100644 index 000000000..97538adf9 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-filter/utils/isRecordFilterConsideredEmpty.ts @@ -0,0 +1,24 @@ +import { RecordFilter } from '@/object-record/record-filter/types/RecordFilter'; +import { RecordFilterOperand } from '@/object-record/record-filter/types/RecordFilterOperand'; +import { isDefined } from 'twenty-shared/utils'; + +export const isRecordFilterConsideredEmpty = ( + recordFilter: RecordFilter, +): boolean => { + const { value, operand } = recordFilter; + + if ( + (!isDefined(value) || value === '') && + ![ + RecordFilterOperand.IsEmpty, + RecordFilterOperand.IsNotEmpty, + RecordFilterOperand.IsInPast, + RecordFilterOperand.IsInFuture, + RecordFilterOperand.IsToday, + ].includes(operand) + ) { + return true; + } + + return false; +}; diff --git a/packages/twenty-front/src/modules/settings/data-model/constants/AllSubFields.ts b/packages/twenty-front/src/modules/settings/data-model/constants/AllSubFields.ts new file mode 100644 index 000000000..307d935df --- /dev/null +++ b/packages/twenty-front/src/modules/settings/data-model/constants/AllSubFields.ts @@ -0,0 +1,7 @@ +import { SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS } from '@/settings/data-model/constants/SettingsCompositeFieldTypeConfigs'; +import { COMPOSITE_FIELD_TYPES } from '@/settings/data-model/types/CompositeFieldType'; + +export const ALL_SUB_FIELDS = COMPOSITE_FIELD_TYPES.flatMap( + (compositeFieldType) => + SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS[compositeFieldType].subFields, +); diff --git a/packages/twenty-front/src/modules/settings/data-model/constants/SettingsCompositeFieldTypeConfigs.ts b/packages/twenty-front/src/modules/settings/data-model/constants/SettingsCompositeFieldTypeConfigs.ts index d75810e7b..0c60eaea7 100644 --- a/packages/twenty-front/src/modules/settings/data-model/constants/SettingsCompositeFieldTypeConfigs.ts +++ b/packages/twenty-front/src/modules/settings/data-model/constants/SettingsCompositeFieldTypeConfigs.ts @@ -11,7 +11,6 @@ import { } from '@/object-record/record-field/types/FieldMetadata'; import { SettingsFieldTypeConfig } from '@/settings/data-model/constants/SettingsNonCompositeFieldTypeConfigs'; import { CompositeFieldType } from '@/settings/data-model/types/CompositeFieldType'; -import { FieldMetadataType } from '~/generated-metadata/graphql'; import { ConnectedAccountProvider } from 'twenty-shared/types'; import { IllustrationIconCurrency, @@ -23,6 +22,7 @@ import { IllustrationIconText, IllustrationIconUser, } from 'twenty-ui/display'; +import { FieldMetadataType } from '~/generated-metadata/graphql'; export type SettingsCompositeFieldTypeConfig = SettingsFieldTypeConfig & { subFields: (keyof T)[]; diff --git a/packages/twenty-front/src/modules/settings/data-model/types/CompositeFieldSubFieldName.ts b/packages/twenty-front/src/modules/settings/data-model/types/CompositeFieldSubFieldName.ts new file mode 100644 index 000000000..47f19dbc5 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/data-model/types/CompositeFieldSubFieldName.ts @@ -0,0 +1,3 @@ +import { ALL_SUB_FIELDS } from '@/settings/data-model/constants/AllSubFields'; + +export type CompositeFieldSubFieldName = (typeof ALL_SUB_FIELDS)[number]; diff --git a/packages/twenty-front/src/modules/settings/data-model/utils/isValidSubFieldName.ts b/packages/twenty-front/src/modules/settings/data-model/utils/isValidSubFieldName.ts new file mode 100644 index 000000000..c6ef93a20 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/data-model/utils/isValidSubFieldName.ts @@ -0,0 +1,14 @@ +import { SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS } from '@/settings/data-model/constants/SettingsCompositeFieldTypeConfigs'; +import { CompositeFieldSubFieldName } from '@/settings/data-model/types/CompositeFieldSubFieldName'; +import { COMPOSITE_FIELD_TYPES } from '@/settings/data-model/types/CompositeFieldType'; + +export const isValidSubFieldName = ( + subFieldName: string, +): subFieldName is CompositeFieldSubFieldName => { + const allSubFields = COMPOSITE_FIELD_TYPES.flatMap( + (compositeFieldType) => + SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS[compositeFieldType].subFields, + ); + + return allSubFields.includes(subFieldName as any); +}; diff --git a/packages/twenty-front/src/modules/views/components/EditableFilterChip.tsx b/packages/twenty-front/src/modules/views/components/EditableFilterChip.tsx index 9cd6c2545..99b6bd0c5 100644 --- a/packages/twenty-front/src/modules/views/components/EditableFilterChip.tsx +++ b/packages/twenty-front/src/modules/views/components/EditableFilterChip.tsx @@ -1,37 +1,56 @@ import { useFieldMetadataItemById } from '@/object-metadata/hooks/useFieldMetadataItemById'; +import { getCompositeSubFieldLabel } from '@/object-record/object-filter-dropdown/utils/getCompositeSubFieldLabel'; import { getOperandLabelShort } from '@/object-record/object-filter-dropdown/utils/getOperandLabel'; +import { isCompositeField } from '@/object-record/object-filter-dropdown/utils/isCompositeField'; import { RecordFilter } from '@/object-record/record-filter/types/RecordFilter'; +import { isValidSubFieldName } from '@/settings/data-model/utils/isValidSubFieldName'; import { SortOrFilterChip } from '@/views/components/SortOrFilterChip'; import { isNonEmptyString } from '@sniptt/guards'; import { useIcons } from 'twenty-ui/display'; type EditableFilterChipProps = { - viewFilter: RecordFilter; + recordFilter: RecordFilter; onRemove: () => void; }; export const EditableFilterChip = ({ - viewFilter, + recordFilter, onRemove, }: EditableFilterChipProps) => { const { getIcon } = useIcons(); const { fieldMetadataItem } = useFieldMetadataItemById( - viewFilter.fieldMetadataId, + recordFilter.fieldMetadataId, ); const FieldMetadataItemIcon = getIcon(fieldMetadataItem.icon); - const operandLabelShort = getOperandLabelShort(viewFilter.operand); + const operandLabelShort = getOperandLabelShort(recordFilter.operand); - const labelKey = `${viewFilter.label}${isNonEmptyString(viewFilter.value) ? operandLabelShort : ''}`; + const recordFilterSubFieldName = recordFilter.subFieldName; + + const subFieldLabel = + isCompositeField(fieldMetadataItem.type) && + isNonEmptyString(recordFilterSubFieldName) && + isValidSubFieldName(recordFilterSubFieldName) + ? getCompositeSubFieldLabel( + fieldMetadataItem.type, + recordFilterSubFieldName, + ) + : ''; + + const fieldNameLabel = isNonEmptyString(subFieldLabel) + ? `${recordFilter.label} / ${subFieldLabel}` + : recordFilter.label; + + const labelKey = `${fieldNameLabel}${isNonEmptyString(recordFilter.value) ? operandLabelShort : ''}`; return ( diff --git a/packages/twenty-front/src/modules/views/components/EditableFilterDropdownButton.tsx b/packages/twenty-front/src/modules/views/components/EditableFilterDropdownButton.tsx index 09b7d67bd..e33d38c54 100644 --- a/packages/twenty-front/src/modules/views/components/EditableFilterDropdownButton.tsx +++ b/packages/twenty-front/src/modules/views/components/EditableFilterDropdownButton.tsx @@ -8,7 +8,7 @@ import { EditableFilterChip } from '@/views/components/EditableFilterChip'; import { ObjectFilterOperandSelectAndInput } from '@/object-record/object-filter-dropdown/components/ObjectFilterOperandSelectAndInput'; import { useRemoveRecordFilter } from '@/object-record/record-filter/hooks/useRemoveRecordFilter'; -import { RecordFilterOperand } from '@/object-record/record-filter/types/RecordFilterOperand'; +import { isRecordFilterConsideredEmpty } from '@/object-record/record-filter/utils/isRecordFilterConsideredEmpty'; import { EditableFilterDropdownButtonEffect } from '@/views/components/EditableFilterDropdownButtonEffect'; type EditableFilterDropdownButtonProps = { @@ -31,17 +31,9 @@ export const EditableFilterDropdownButton = ({ }; const handleDropdownClickOutside = useCallback(() => { - const { value, operand } = recordFilter; - if ( - !value && - ![ - RecordFilterOperand.IsEmpty, - RecordFilterOperand.IsNotEmpty, - RecordFilterOperand.IsInPast, - RecordFilterOperand.IsInFuture, - RecordFilterOperand.IsToday, - ].includes(operand) - ) { + const recordFilterIsEmpty = isRecordFilterConsideredEmpty(recordFilter); + + if (recordFilterIsEmpty) { removeRecordFilter({ recordFilterId: recordFilter.id }); } }, [recordFilter, removeRecordFilter]); @@ -53,7 +45,7 @@ export const EditableFilterDropdownButton = ({ dropdownId={recordFilter.id} clickableComponent={ } diff --git a/packages/twenty-front/src/modules/views/components/ViewBarDetails.tsx b/packages/twenty-front/src/modules/views/components/ViewBarDetails.tsx index 04e18cbec..9cf6bb322 100644 --- a/packages/twenty-front/src/modules/views/components/ViewBarDetails.tsx +++ b/packages/twenty-front/src/modules/views/components/ViewBarDetails.tsx @@ -70,12 +70,11 @@ const StyledActionButtonContainer = styled.div` `; const StyledFilterContainer = styled.div` - display: flex; align-items: center; - flex: 1; - overflow-x: hidden; - + display: flex; gap: ${({ theme }) => theme.spacing(1)}; + + overflow-x: hidden; `; const StyledSeperatorContainer = styled.div` diff --git a/packages/twenty-front/src/modules/views/hooks/internal/usePersistViewFilterRecords.ts b/packages/twenty-front/src/modules/views/hooks/internal/usePersistViewFilterRecords.ts index 8cb3e360b..07884e83f 100644 --- a/packages/twenty-front/src/modules/views/hooks/internal/usePersistViewFilterRecords.ts +++ b/packages/twenty-front/src/modules/views/hooks/internal/usePersistViewFilterRecords.ts @@ -58,6 +58,7 @@ export const usePersistViewFilterRecords = () => { operand: viewFilter.operand, viewFilterGroupId: viewFilter.viewFilterGroupId, positionInViewFilterGroup: viewFilter.positionInViewFilterGroup, + subFieldName: viewFilter.subFieldName ?? null, } satisfies Partial, }, update: (cache, { data }) => { @@ -98,6 +99,7 @@ export const usePersistViewFilterRecords = () => { operand: viewFilter.operand, positionInViewFilterGroup: viewFilter.positionInViewFilterGroup, viewFilterGroupId: viewFilter.viewFilterGroupId, + subFieldName: viewFilter.subFieldName ?? null, } satisfies Partial, }, update: (cache, { data }) => { diff --git a/packages/twenty-front/src/modules/views/types/ViewFilter.ts b/packages/twenty-front/src/modules/views/types/ViewFilter.ts index 23e1647e1..b5e5160ba 100644 --- a/packages/twenty-front/src/modules/views/types/ViewFilter.ts +++ b/packages/twenty-front/src/modules/views/types/ViewFilter.ts @@ -13,4 +13,5 @@ export type ViewFilter = { viewId?: string; viewFilterGroupId?: string; positionInViewFilterGroup?: number | null; + subFieldName?: string | null; }; diff --git a/packages/twenty-front/src/modules/views/utils/areViewFiltersEqual.ts b/packages/twenty-front/src/modules/views/utils/areViewFiltersEqual.ts index 2ceee15f0..52dd8c661 100644 --- a/packages/twenty-front/src/modules/views/utils/areViewFiltersEqual.ts +++ b/packages/twenty-front/src/modules/views/utils/areViewFiltersEqual.ts @@ -12,6 +12,7 @@ export const areViewFiltersEqual = ( 'value', 'displayValue', 'operand', + 'subFieldName', ]; return propertiesToCompare.every((property) => diff --git a/packages/twenty-front/src/modules/views/utils/mapRecordFilterToViewFilter.ts b/packages/twenty-front/src/modules/views/utils/mapRecordFilterToViewFilter.ts index 7e905ac19..eaaa963be 100644 --- a/packages/twenty-front/src/modules/views/utils/mapRecordFilterToViewFilter.ts +++ b/packages/twenty-front/src/modules/views/utils/mapRecordFilterToViewFilter.ts @@ -13,5 +13,6 @@ export const mapRecordFilterToViewFilter = ( value: recordFilter.value, positionInViewFilterGroup: recordFilter.positionInRecordFilterGroup, viewFilterGroupId: recordFilter.recordFilterGroupId, + subFieldName: recordFilter.subFieldName, }; }; diff --git a/packages/twenty-front/src/modules/views/utils/mapViewFiltersToFilters.ts b/packages/twenty-front/src/modules/views/utils/mapViewFiltersToFilters.ts index 533002d8b..521991d87 100644 --- a/packages/twenty-front/src/modules/views/utils/mapViewFiltersToFilters.ts +++ b/packages/twenty-front/src/modules/views/utils/mapViewFiltersToFilters.ts @@ -3,8 +3,8 @@ import { RecordFilter } from '@/object-record/record-filter/types/RecordFilter'; import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; import { getFilterTypeFromFieldType } from '@/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions'; -import { ViewFilter } from '../types/ViewFilter'; import { isDefined } from 'twenty-shared/utils'; +import { ViewFilter } from '../types/ViewFilter'; export const mapViewFiltersToFilters = ( viewFilters: ViewFilter[], @@ -36,6 +36,7 @@ export const mapViewFiltersToFilters = ( positionInRecordFilterGroup: viewFilter.positionInViewFilterGroup, label: availableFieldMetadataItem.label, type: filterType, + subFieldName: viewFilter.subFieldName, } satisfies RecordFilter; }) .filter(isDefined); diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids.ts index c0989cc64..809ffa523 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids.ts @@ -408,6 +408,7 @@ export const VIEW_FILTER_STANDARD_FIELD_IDS = { view: '20202020-4f5b-487e-829c-3d881c163611', viewFilterGroupId: '20202020-2580-420a-8328-cab1635c0296', positionInViewFilterGroup: '20202020-3bb0-4f66-a537-a46fe0dc468f', + subFieldName: '20202020-3bb0-4f66-a537-a46fe0dc469a', }; export const VIEW_FILTER_GROUP_STANDARD_FIELD_IDS = { diff --git a/packages/twenty-server/src/modules/view/standard-objects/view-filter.workspace-entity.ts b/packages/twenty-server/src/modules/view/standard-objects/view-filter.workspace-entity.ts index 506ab0ef7..3c469f543 100644 --- a/packages/twenty-server/src/modules/view/standard-objects/view-filter.workspace-entity.ts +++ b/packages/twenty-server/src/modules/view/standard-objects/view-filter.workspace-entity.ts @@ -94,4 +94,15 @@ export class ViewFilterWorkspaceEntity extends BaseWorkspaceEntity { }) @WorkspaceIsNullable() positionInViewFilterGroup: number | null; + + @WorkspaceField({ + standardId: VIEW_FILTER_STANDARD_FIELD_IDS.subFieldName, + type: FieldMetadataType.TEXT, + label: msg`Sub field name`, + description: msg`Sub field name`, + icon: 'IconSubtask', + defaultValue: null, + }) + @WorkspaceIsNullable() + subFieldName: string | null; }