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; }