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 723bf617cd..394433083c 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 2ed3ece59c..9c694b6435 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 9cb5f1980a..6fb77ac0b2 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 6e6205d4d7..9d0ebc99e8 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 e951842865..14e5e27c4a 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 5cfdfc5266..ca899d4077 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 55b7e03a2c..85590df546 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 412172f4b2..85f4194562 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 72b61368dd..eaba930737 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 c08d552789..46871720c6 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 0000000000..560886e799 --- /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 0000000000..97538adf97 --- /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 0000000000..307d935dfc --- /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 d75810e7b8..0c60eaea7e 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 0000000000..47f19dbc5e --- /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 0000000000..c6ef93a209 --- /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 9cd6c25452..99b6bd0c5e 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 09b7d67bd8..e33d38c54c 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 04e18cbec9..9cf6bb322f 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 8cb3e360bb..07884e83f7 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 23e1647e1c..b5e5160bad 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 2ceee15f00..52dd8c661b 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 7e905ac197..eaaa963be7 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 533002d8b0..521991d87e 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 c0989cc64e..809ffa5238 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 506ab0ef7e..3c469f5430 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; }