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.

<img width="512" alt="image"
src="https://github.com/user-attachments/assets/e9a2f8d2-a66f-4800-853a-4df5c6b627a9"
/>

<img width="495" alt="image"
src="https://github.com/user-attachments/assets/7542697b-0689-4095-9c3c-b5e47875355f"
/>

---------

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
This commit is contained in:
Lucas Bordeau
2025-04-17 17:03:56 +02:00
committed by GitHub
parent 1ba0c24071
commit caf44207fd
30 changed files with 193 additions and 48 deletions

View File

@ -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 (
<TextInputV2
value={inputValue}
value={recordFilter.value}
onChange={handleChange}
placeholder="Enter value"
fullWidth

View File

@ -1,5 +1,8 @@
import { useGetFieldMetadataItemById } from '@/object-metadata/hooks/useGetFieldMetadataItemById';
import { getCompositeSubFieldLabel } from '@/object-record/object-filter-dropdown/utils/getCompositeSubFieldLabel';
import { isCompositeField } from '@/object-record/object-filter-dropdown/utils/isCompositeField';
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
import { isValidSubFieldName } from '@/settings/data-model/utils/isValidSubFieldName';
import { SelectControl } from '@/ui/input/components/SelectControl';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { isNonEmptyString } from '@sniptt/guards';
@ -33,12 +36,25 @@ export const AdvancedFilterFieldSelectDropdownButtonClickableSelect = ({
? getIcon(fieldMetadataItem?.icon)
: undefined;
const selectedFieldLabel = recordFilter?.label ?? '';
const subFieldLabel =
isDefined(fieldMetadataItem) &&
isCompositeField(fieldMetadataItem.type) &&
isNonEmptyString(recordFilter?.subFieldName) &&
isValidSubFieldName(recordFilter.subFieldName)
? getCompositeSubFieldLabel(
fieldMetadataItem.type,
recordFilter.subFieldName,
)
: '';
const fieldNameLabel = isNonEmptyString(subFieldLabel)
? `${recordFilter?.label} / ${subFieldLabel}`
: (recordFilter?.label ?? '');
return (
<SelectControl
selectedOption={{
label: selectedFieldLabel,
label: fieldNameLabel,
value: null,
Icon: fieldIcon,
}}

View File

@ -11,6 +11,7 @@ import { objectFilterDropdownIsSelectingCompositeFieldComponentState } from '@/o
import { objectFilterDropdownSubMenuFieldTypeComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownSubMenuFieldTypeComponentState';
import { getCompositeSubFieldLabel } from '@/object-record/object-filter-dropdown/utils/getCompositeSubFieldLabel';
import { getFilterableFieldTypeLabel } from '@/object-record/object-filter-dropdown/utils/getFilterableFieldTypeLabel';
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';
@ -79,6 +80,12 @@ export const AdvancedFilterSubFieldSelectMenu = ({
objectFilterDropdownSubMenuFieldType
].filterableSubFields.sort((a, b) => a.localeCompare(b));
const subFieldsAreFilterable =
isDefined(fieldMetadataItemUsedInDropdown) &&
isCompositeFieldTypeSubFieldsFilterable(
fieldMetadataItemUsedInDropdown.type,
);
return (
<>
<DropdownMenuHeader
@ -101,8 +108,7 @@ export const AdvancedFilterSubFieldSelectMenu = ({
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) => (
<MenuItem
key={`select-filter-${index}`}

View File

@ -97,7 +97,7 @@ export const useSelectFieldUsedInAdvancedFilterDropdown = () => {
existingRecordFilter?.positionInRecordFilterGroup,
type: filterType,
label: fieldMetadataItem.label,
subFieldName,
subFieldName: subFieldName ?? null,
});
setSubFieldNameUsedInDropdown(subFieldName);

View File

@ -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 (
<Dropdown
@ -28,6 +48,7 @@ export const MultipleFiltersDropdownButton = ({
dropdownComponents={<MultipleFiltersDropdownContent />}
dropdownHotkeyScope={hotkeyScope}
dropdownOffset={{ y: 8 }}
onClickOutside={handleDropdownClickOutside}
/>
);
};

View File

@ -82,6 +82,7 @@ export const ObjectFilterDropdownBooleanSelect = () => {
positionInRecordFilterGroup: selectedFilter?.positionInRecordFilterGroup,
type: getFilterTypeFromFieldType(fieldMetadataItemUsedInDropdown.type),
label: fieldMetadataItemUsedInDropdown.label,
subFieldName: selectedFilter?.subFieldName,
});
setSelectedValue(value);

View File

@ -63,6 +63,7 @@ export const ObjectFilterDropdownDateInput = () => {
positionInRecordFilterGroup: selectedFilter?.positionInRecordFilterGroup,
type: getFilterTypeFromFieldType(fieldMetadataItemUsedInDropdown.type),
label: fieldMetadataItemUsedInDropdown.label,
subFieldName: selectedFilter?.subFieldName,
});
};

View File

@ -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 (
<>
<DropdownMenuHeader
@ -172,8 +179,7 @@ export const ObjectFilterDropdownFilterSelectCompositeFieldSubMenu = () => {
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) => (
<MenuItem
key={`select-filter-${index}`}

View File

@ -70,6 +70,7 @@ export const ObjectFilterDropdownNumberInput = () => {
recordFilterGroupId: selectedFilter?.recordFilterGroupId,
positionInRecordFilterGroup:
selectedFilter?.positionInRecordFilterGroup,
subFieldName: selectedFilter?.subFieldName,
});
}}
/>

View File

@ -147,6 +147,7 @@ export const ObjectFilterDropdownOptionSelect = () => {
recordFilterGroupId: selectedFilter?.recordFilterGroupId,
positionInRecordFilterGroup:
selectedFilter?.positionInRecordFilterGroup,
subFieldName: selectedFilter?.subFieldName,
});
}
resetSelectedItem();

View File

@ -77,6 +77,7 @@ export const ObjectFilterDropdownRatingInput = () => {
fieldMetadataItemUsedInDropdown.type,
),
label: fieldMetadataItemUsedInDropdown.label,
subFieldName: selectedFilter?.subFieldName,
});
}}
/>

View File

@ -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,
});
}
}

View File

@ -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,
});
}}
/>

View File

@ -71,6 +71,7 @@ export const ObjectFilterDropdownTextSearchInput = () => {
label: fieldMetadataItemUsedInDropdown.label,
positionInRecordFilterGroup:
selectedFilter?.positionInRecordFilterGroup,
subFieldName: selectedFilter?.subFieldName,
});
}}
/>

View File

@ -0,0 +1,11 @@
import { FieldType } from '@/settings/data-model/types/FieldType';
type CompositeFilterableFieldType = Extract<FieldType, 'ACTOR' | 'FULL_NAME'>;
export const isCompositeFieldTypeSubFieldsFilterable = (
fieldType: FieldType,
): fieldType is CompositeFilterableFieldType => {
return (
['ACTOR', 'FULL_NAME'] satisfies CompositeFilterableFieldType[]
).includes(fieldType as any);
};

View File

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

View File

@ -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,
);

View File

@ -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<T> = SettingsFieldTypeConfig<T> & {
subFields: (keyof T)[];

View File

@ -0,0 +1,3 @@
import { ALL_SUB_FIELDS } from '@/settings/data-model/constants/AllSubFields';
export type CompositeFieldSubFieldName = (typeof ALL_SUB_FIELDS)[number];

View File

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

View File

@ -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 (
<SortOrFilterChip
key={viewFilter.id}
testId={viewFilter.id}
key={recordFilter.id}
testId={recordFilter.id}
labelKey={labelKey}
labelValue={viewFilter.displayValue}
labelValue={recordFilter.displayValue}
Icon={FieldMetadataItemIcon}
onRemove={onRemove}
/>

View File

@ -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={
<EditableFilterChip
viewFilter={recordFilter}
recordFilter={recordFilter}
onRemove={handleRemove}
/>
}

View File

@ -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`

View File

@ -58,6 +58,7 @@ export const usePersistViewFilterRecords = () => {
operand: viewFilter.operand,
viewFilterGroupId: viewFilter.viewFilterGroupId,
positionInViewFilterGroup: viewFilter.positionInViewFilterGroup,
subFieldName: viewFilter.subFieldName ?? null,
} satisfies Partial<ViewFilter>,
},
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<ViewFilter>,
},
update: (cache, { data }) => {

View File

@ -13,4 +13,5 @@ export type ViewFilter = {
viewId?: string;
viewFilterGroupId?: string;
positionInViewFilterGroup?: number | null;
subFieldName?: string | null;
};

View File

@ -12,6 +12,7 @@ export const areViewFiltersEqual = (
'value',
'displayValue',
'operand',
'subFieldName',
];
return propertiesToCompare.every((property) =>

View File

@ -13,5 +13,6 @@ export const mapRecordFilterToViewFilter = (
value: recordFilter.value,
positionInViewFilterGroup: recordFilter.positionInRecordFilterGroup,
viewFilterGroupId: recordFilter.recordFilterGroupId,
subFieldName: recordFilter.subFieldName,
};
};

View File

@ -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);

View File

@ -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 = {

View File

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