Record filters - Introduced fieldMetadataItemUsedInDropdown instead of filterDefinitionUsedInDropdown (#10044)

This PR progressively introduces fieldMetadataItemUsedInDropdown instead
of filterDefinitionUsedInDropdown where most easy to replace.

This allows to use `fieldMetadataItemUsedInDropdown.id` instead of
`filterDefinition.fieldMetadataId`, which is one easy dependency to
remove on filter definition.

We still derive filterDefinition instead of fully replacing it, because
it will be easier to remove RecordFilterDefinition usage in a bottom-up
approach instead.

In multiple components of the filter dropdown, we try to replace
filterDefinition by fieldMetadataItem derivation : Icon, label, id,
type, etc.

We also introduce the usage of subFieldNameUsedInDropdown instead of
storing it dynamically on filterDefinition, for handling filtering on
composite sub fields.

The method `formatFieldMetadataItemAsFilterDefinition()` that is used to
derive filterDefinition from fieldMetadataItem is what was being used
originally to create the availableFilterDefinition state. (That is
already removed)

Fixed associated unit tests accordingly.
This commit is contained in:
Lucas Bordeau
2025-02-06 11:03:55 +01:00
committed by GitHub
parent e21cbb2fe2
commit 049a0118aa
34 changed files with 483 additions and 209 deletions

View File

@ -54,10 +54,8 @@ describe('useColumnDefinitionsFromFieldMetadata', () => {
);
expect(Array.isArray(result.current.columnDefinitions)).toBe(true);
expect(Array.isArray(result.current.filterDefinitions)).toBe(true);
expect(Array.isArray(result.current.sortDefinitions)).toBe(true);
expect(result.current.columnDefinitions.length).toBe(0);
expect(result.current.filterDefinitions.length).toBe(0);
expect(result.current.sortDefinitions.length).toBe(0);
});
@ -76,11 +74,9 @@ describe('useColumnDefinitionsFromFieldMetadata', () => {
},
);
const { columnDefinitions, filterDefinitions, sortDefinitions } =
result.current;
const { columnDefinitions, sortDefinitions } = result.current;
expect(columnDefinitions.length).toBe(21);
expect(filterDefinitions.length).toBe(17);
expect(sortDefinitions.length).toBe(14);
});
});

View File

@ -4,6 +4,7 @@ import {
RelationDefinitionType,
} from '~/generated-metadata/graphql';
import { FilterableFieldType } from '@/object-record/record-filter/types/FilterableFieldType';
import { ObjectMetadataItem } from '../types/ObjectMetadataItem';
export const formatFieldMetadataItemsAsFilterDefinitions = ({
@ -79,7 +80,9 @@ export const getRelationObjectMetadataNamePlural = ({
return field.relationDefinition?.targetObjectMetadata.namePlural;
};
export const getFilterTypeFromFieldType = (fieldType: FieldMetadataType) => {
export const getFilterTypeFromFieldType = (
fieldType: FieldMetadataType,
): FilterableFieldType => {
switch (fieldType) {
case FieldMetadataType.DATE_TIME:
return 'DATE_TIME';

View File

@ -1,8 +1,11 @@
import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById';
import { availableFieldMetadataItemsForFilterFamilySelector } from '@/object-metadata/states/availableFieldMetadataItemsForFilterFamilySelector';
import { formatFieldMetadataItemAsFilterDefinition } from '@/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions';
import {
formatFieldMetadataItemAsFilterDefinition,
getFilterTypeFromFieldType,
} from '@/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions';
import { useUpsertCombinedViewFilterGroup } from '@/object-record/advanced-filter/hooks/useUpsertCombinedViewFilterGroup';
import { getRecordFilterOperandsForRecordFilterDefinition } from '@/object-record/record-filter/utils/getRecordFilterOperandsForRecordFilterDefinition';
import { getRecordFilterOperands } from '@/object-record/record-filter/utils/getRecordFilterOperands';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
@ -86,12 +89,17 @@ export const AdvancedFilterAddFilterRuleSelect = ({
field: defaultFieldMetadataItem,
});
const filterType = getFilterTypeFromFieldType(
defaultFieldMetadataItem.type,
);
upsertCombinedViewFilter({
id: v4(),
fieldMetadataId: defaultFieldMetadataItem.id,
operand: getRecordFilterOperandsForRecordFilterDefinition(
defaultFilterDefinition,
)[0],
type: filterType,
operand: getRecordFilterOperands({
filterType,
})[0],
definition: defaultFilterDefinition,
value: '',
displayValue: '',
@ -123,12 +131,17 @@ export const AdvancedFilterAddFilterRuleSelect = ({
field: defaultFieldMetadataItem,
});
const filterType = getFilterTypeFromFieldType(
defaultFieldMetadataItem.type,
);
upsertCombinedViewFilter({
id: v4(),
fieldMetadataId: defaultFieldMetadataItem.id,
operand: getRecordFilterOperandsForRecordFilterDefinition(
defaultFilterDefinition,
)[0],
type: filterType,
operand: getRecordFilterOperands({
filterType,
})[0],
definition: defaultFilterDefinition,
value: '',
displayValue: '',

View File

@ -25,9 +25,9 @@ export const AdvancedFilterViewFilterFieldSelect = ({
}: AdvancedFilterViewFilterFieldSelectProps) => {
const { advancedFilterDropdownId } = useAdvancedFilterDropdown(viewFilterId);
const filter = useCurrentViewFilter({ viewFilterId });
const recordFilter = useCurrentViewFilter({ viewFilterId });
const selectedFieldLabel = filter?.definition.label ?? '';
const selectedFieldLabel = recordFilter?.label ?? '';
const setAdvancedFilterViewFilterId = useSetRecoilComponentStateV2(
advancedFilterViewFilterIdComponentState,
@ -58,8 +58,8 @@ export const AdvancedFilterViewFilterFieldSelect = ({
/>
}
onOpen={() => {
setAdvancedFilterViewFilterId(filter?.id);
setAdvancedFilterViewFilterGroupId(filter?.viewFilterGroupId);
setAdvancedFilterViewFilterId(recordFilter?.id);
setAdvancedFilterViewFilterGroupId(recordFilter?.viewFilterGroupId);
}}
dropdownComponents={
shouldShowCompositeSelectionSubMenu ? (

View File

@ -3,7 +3,8 @@ import styled from '@emotion/styled';
import { useEffect, useState } from 'react';
import { v4 } from 'uuid';
import { filterDefinitionUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/filterDefinitionUsedInDropdownComponentState';
import { formatFieldMetadataItemAsFilterDefinition } from '@/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions';
import { fieldMetadataItemUsedInDropdownComponentSelector } from '@/object-record/object-filter-dropdown/states/fieldMetadataItemUsedInDropdownComponentSelector';
import { selectedFilterComponentState } from '@/object-record/object-filter-dropdown/states/selectedFilterComponentState';
import { selectedOperandInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/selectedOperandInDropdownComponentState';
import { useApplyRecordFilter } from '@/object-record/record-filter/hooks/useApplyRecordFilter';
@ -39,8 +40,8 @@ export const ObjectFilterDropdownBooleanSelect = () => {
const theme = useTheme();
const options = [true, false];
const filterDefinitionUsedInDropdown = useRecoilComponentValueV2(
filterDefinitionUsedInDropdownComponentState,
const fieldMetadataItemUsedInDropdown = useRecoilComponentValueV2(
fieldMetadataItemUsedInDropdownComponentSelector,
);
const selectedOperandInDropdown = useRecoilComponentValueV2(
@ -65,18 +66,22 @@ export const ObjectFilterDropdownBooleanSelect = () => {
const handleOptionSelect = (value: boolean) => {
if (
!isDefined(filterDefinitionUsedInDropdown) ||
!isDefined(fieldMetadataItemUsedInDropdown) ||
!isDefined(selectedOperandInDropdown)
) {
return;
}
const filterDefinition = formatFieldMetadataItemAsFilterDefinition({
field: fieldMetadataItemUsedInDropdown,
});
applyRecordFilter({
id: selectedFilter?.id ?? v4(),
definition: filterDefinitionUsedInDropdown,
definition: filterDefinition,
operand: selectedOperandInDropdown,
displayValue: value ? 'True' : 'False',
fieldMetadataId: filterDefinitionUsedInDropdown.fieldMetadataId,
fieldMetadataId: fieldMetadataItemUsedInDropdown.id,
value: value.toString(),
viewFilterGroupId: selectedFilter?.viewFilterGroupId,
});

View File

@ -1,6 +1,7 @@
import { v4 } from 'uuid';
import { filterDefinitionUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/filterDefinitionUsedInDropdownComponentState';
import { formatFieldMetadataItemAsFilterDefinition } from '@/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions';
import { fieldMetadataItemUsedInDropdownComponentSelector } from '@/object-record/object-filter-dropdown/states/fieldMetadataItemUsedInDropdownComponentSelector';
import { selectedFilterComponentState } from '@/object-record/object-filter-dropdown/states/selectedFilterComponentState';
import { selectedOperandInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/selectedOperandInDropdownComponentState';
import { getRelativeDateDisplayValue } from '@/object-record/object-filter-dropdown/utils/getRelativeDateDisplayValue';
@ -19,8 +20,8 @@ import { isDefined } from 'twenty-shared';
import { FieldMetadataType } from '~/generated-metadata/graphql';
export const ObjectFilterDropdownDateInput = () => {
const filterDefinitionUsedInDropdown = useRecoilComponentValueV2(
filterDefinitionUsedInDropdownComponentState,
const fieldMetadataItemUsedInDropdown = useRecoilComponentValueV2(
fieldMetadataItemUsedInDropdownComponentSelector,
);
const selectedOperandInDropdown = useRecoilComponentValueV2(
@ -41,16 +42,21 @@ export const ObjectFilterDropdownDateInput = () => {
);
const isDateTimeInput =
filterDefinitionUsedInDropdown?.type === FieldMetadataType.DATE_TIME;
fieldMetadataItemUsedInDropdown?.type === FieldMetadataType.DATE_TIME;
const handleAbsoluteDateChange = (newDate: Date | null) => {
setInternalDate(newDate);
if (!filterDefinitionUsedInDropdown || !selectedOperandInDropdown) return;
if (!fieldMetadataItemUsedInDropdown || !selectedOperandInDropdown) return;
const filterDefinition = formatFieldMetadataItemAsFilterDefinition({
field: fieldMetadataItemUsedInDropdown,
});
applyRecordFilter({
id: selectedFilter?.id ? selectedFilter.id : v4(),
fieldMetadataId: filterDefinitionUsedInDropdown.fieldMetadataId,
definition: filterDefinition,
fieldMetadataId: fieldMetadataItemUsedInDropdown.id,
value: newDate?.toISOString() ?? '',
operand: selectedOperandInDropdown,
displayValue: isDefined(newDate)
@ -58,7 +64,6 @@ export const ObjectFilterDropdownDateInput = () => {
? newDate.toLocaleString()
: newDate.toLocaleDateString()
: '',
definition: filterDefinitionUsedInDropdown,
viewFilterGroupId: selectedFilter?.viewFilterGroupId,
});
};
@ -70,7 +75,11 @@ export const ObjectFilterDropdownDateInput = () => {
unit: VariableDateViewFilterValueUnit;
} | null,
) => {
if (!filterDefinitionUsedInDropdown || !selectedOperandInDropdown) return;
if (!fieldMetadataItemUsedInDropdown || !selectedOperandInDropdown) return;
const filterDefinition = formatFieldMetadataItemAsFilterDefinition({
field: fieldMetadataItemUsedInDropdown,
});
const value = relativeDate
? computeVariableDateViewFilterValue(
@ -82,11 +91,11 @@ export const ObjectFilterDropdownDateInput = () => {
applyRecordFilter({
id: selectedFilter?.id ? selectedFilter.id : v4(),
fieldMetadataId: filterDefinitionUsedInDropdown.fieldMetadataId,
fieldMetadataId: fieldMetadataItemUsedInDropdown.id,
value,
operand: selectedOperandInDropdown,
displayValue: getRelativeDateDisplayValue(relativeDate),
definition: filterDefinitionUsedInDropdown,
definition: filterDefinition,
viewFilterGroupId: selectedFilter?.viewFilterGroupId,
});
};

View File

@ -151,9 +151,7 @@ export const ObjectFilterDropdownFilterSelect = ({
filterDefinition: selectedFilterDefinition,
});
setFieldMetadataItemIdUsedInDropdown(
selectedFilterDefinition.fieldMetadataId,
);
setFieldMetadataItemIdUsedInDropdown(fieldMetadataItemId);
closeAdvancedFilterDropdown();
};

View File

@ -1,7 +1,10 @@
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { getFilterTypeFromFieldType } from '@/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions';
import { useAdvancedFilterDropdown } from '@/object-record/advanced-filter/hooks/useAdvancedFilterDropdown';
import { advancedFilterViewFilterGroupIdComponentState } from '@/object-record/object-filter-dropdown/states/advancedFilterViewFilterGroupIdComponentState';
import { advancedFilterViewFilterIdComponentState } from '@/object-record/object-filter-dropdown/states/advancedFilterViewFilterIdComponentState';
import { fieldMetadataItemIdUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/fieldMetadataItemIdUsedInDropdownComponentState';
import { fieldMetadataItemUsedInDropdownComponentSelector } from '@/object-record/object-filter-dropdown/states/fieldMetadataItemUsedInDropdownComponentSelector';
import { filterDefinitionUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/filterDefinitionUsedInDropdownComponentState';
import { objectFilterDropdownFilterIsSelectedComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownFilterIsSelectedComponentState';
import { objectFilterDropdownFirstLevelFilterDefinitionComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownFirstLevelFilterDefinitionComponentState';
@ -9,12 +12,13 @@ import { objectFilterDropdownIsSelectingCompositeFieldComponentState } from '@/o
import { objectFilterDropdownSearchInputComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownSearchInputComponentState';
import { objectFilterDropdownSubMenuFieldTypeComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownSubMenuFieldTypeComponentState';
import { selectedOperandInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/selectedOperandInDropdownComponentState';
import { subFieldNameUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/subFieldNameUsedInDropdownComponentState';
import { getCompositeSubFieldLabel } from '@/object-record/object-filter-dropdown/utils/getCompositeSubFieldLabel';
import { getFilterableFieldTypeLabel } from '@/object-record/object-filter-dropdown/utils/getFilterableFieldTypeLabel';
import { getInitialFilterValue } from '@/object-record/object-filter-dropdown/utils/getInitialFilterValue';
import { useApplyRecordFilter } from '@/object-record/record-filter/hooks/useApplyRecordFilter';
import { RecordFilterDefinition } from '@/object-record/record-filter/types/RecordFilterDefinition';
import { getRecordFilterOperandsForRecordFilterDefinition } from '@/object-record/record-filter/utils/getRecordFilterOperandsForRecordFilterDefinition';
import { getRecordFilterOperands } from '@/object-record/record-filter/utils/getRecordFilterOperands';
import { SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS } from '@/settings/data-model/constants/SettingsCompositeFieldTypeConfigs';
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
@ -37,6 +41,14 @@ export const ObjectFilterDropdownFilterSelectCompositeFieldSubMenu = () => {
objectFilterDropdownFirstLevelFilterDefinitionComponentState,
);
const fieldMetadataItemUsedInDropdown = useRecoilComponentValueV2(
fieldMetadataItemUsedInDropdownComponentSelector,
);
const setSubFieldNameUsedInDropdown = useSetRecoilComponentStateV2(
subFieldNameUsedInDropdownComponentState,
);
const [, setObjectFilterDropdownFilterIsSelected] = useRecoilComponentStateV2(
objectFilterDropdownFilterIsSelectedComponentState,
);
@ -83,39 +95,60 @@ export const ObjectFilterDropdownFilterSelectCompositeFieldSubMenu = () => {
advancedFilterViewFilterId,
);
const handleSelectFilter = (definition: RecordFilterDefinition | null) => {
if (definition !== null) {
const handleSelectFilter = (
fieldMetadataItem: FieldMetadataItem | null | undefined,
subFieldName?: string | null | undefined,
) => {
if (isDefined(fieldMetadataItem)) {
const filterDefinition: RecordFilterDefinition = {
fieldMetadataId: fieldMetadataItem.id,
type: getFilterTypeFromFieldType(fieldMetadataItem.type),
label: fieldMetadataItem.label,
iconName: fieldMetadataItem.icon ?? '',
compositeFieldName: subFieldName ?? undefined,
};
if (
isDefined(advancedFilterViewFilterId) &&
isDefined(advancedFilterViewFilterGroupId)
) {
closeAdvancedFilterDropdown();
const operand =
getRecordFilterOperandsForRecordFilterDefinition(definition)[0];
const { value, displayValue } = getInitialFilterValue(
definition.type,
operand,
);
const type = getFilterTypeFromFieldType(fieldMetadataItem.type);
const operand = getRecordFilterOperands({
filterType: type,
subFieldName: subFieldName,
})[0];
const { value, displayValue } = getInitialFilterValue(type, operand);
applyRecordFilter({
id: advancedFilterViewFilterId,
fieldMetadataId: definition.fieldMetadataId,
fieldMetadataId: fieldMetadataItem.id,
value,
operand,
displayValue,
definition,
definition: filterDefinition,
viewFilterGroupId: advancedFilterViewFilterGroupId,
subFieldName: subFieldName,
});
}
setFilterDefinitionUsedInDropdown(definition);
setFieldMetadataItemIdUsedInDropdown(definition.fieldMetadataId);
setFilterDefinitionUsedInDropdown(filterDefinition);
setFieldMetadataItemIdUsedInDropdown(fieldMetadataItem.id);
const type = getFilterTypeFromFieldType(fieldMetadataItem.type);
setSelectedOperandInDropdown(
getRecordFilterOperandsForRecordFilterDefinition(definition)[0],
getRecordFilterOperands({
filterType: type,
subFieldName: subFieldName,
})[0],
);
setSubFieldNameUsedInDropdown(subFieldName);
setObjectFilterDropdownSearchInput('');
setObjectFilterDropdownFilterIsSelected(true);
@ -167,36 +200,30 @@ export const ObjectFilterDropdownFilterSelectCompositeFieldSubMenu = () => {
key={`select-filter-${-1}`}
testId={`select-filter-${-1}`}
onClick={() => {
handleSelectFilter(objectFilterDropdownFirstLevelFilterDefinition);
handleSelectFilter(fieldMetadataItemUsedInDropdown);
}}
LeftIcon={IconApps}
text={`Any ${getFilterableFieldTypeLabel(objectFilterDropdownSubMenuFieldType)} field`}
/>
{/* TODO: fix this with a backend field on ViewFilter for composite field filter */}
{objectFilterDropdownFirstLevelFilterDefinition.type === 'ACTOR' &&
{fieldMetadataItemUsedInDropdown?.type === 'ACTOR' &&
options.map((subFieldName, index) => (
<MenuItem
key={`select-filter-${index}`}
testId={`select-filter-${index}`}
onClick={() => {
if (isDefined(objectFilterDropdownFirstLevelFilterDefinition)) {
handleSelectFilter({
...objectFilterDropdownFirstLevelFilterDefinition,
label: getCompositeSubFieldLabel(
objectFilterDropdownSubMenuFieldType,
subFieldName,
),
compositeFieldName: subFieldName,
});
if (isDefined(fieldMetadataItemUsedInDropdown)) {
handleSelectFilter(
fieldMetadataItemUsedInDropdown,
subFieldName,
);
}
}}
text={getCompositeSubFieldLabel(
objectFilterDropdownSubMenuFieldType,
subFieldName,
)}
LeftIcon={getIcon(
objectFilterDropdownFirstLevelFilterDefinition?.iconName,
)}
LeftIcon={getIcon(fieldMetadataItemUsedInDropdown?.icon)}
/>
))}
</DropdownMenuItemsContainer>

View File

@ -7,14 +7,15 @@ import { objectFilterDropdownFirstLevelFilterDefinitionComponentState } from '@/
import { objectFilterDropdownIsSelectingCompositeFieldComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownIsSelectingCompositeFieldComponentState';
import { objectFilterDropdownSubMenuFieldTypeComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownSubMenuFieldTypeComponentState';
import { selectedOperandInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/selectedOperandInDropdownComponentState';
import { CompositeFilterableFieldType } from '@/object-record/record-filter/types/CompositeFilterableFieldType';
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { formatFieldMetadataItemAsFilterDefinition } from '@/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions';
import {
formatFieldMetadataItemAsFilterDefinition,
getFilterTypeFromFieldType,
} from '@/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions';
import { filterDefinitionUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/filterDefinitionUsedInDropdownComponentState';
import { isCompositeField } from '@/object-record/object-filter-dropdown/utils/isCompositeField';
import { RecordFilterDefinition } from '@/object-record/record-filter/types/RecordFilterDefinition';
import { getRecordFilterOperandsForRecordFilterDefinition } from '@/object-record/record-filter/utils/getRecordFilterOperandsForRecordFilterDefinition';
import { getRecordFilterOperands } from '@/object-record/record-filter/utils/getRecordFilterOperands';
import { RelationPickerHotkeyScope } from '@/object-record/relation-picker/types/RelationPickerHotkeyScope';
import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList';
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
@ -57,16 +58,10 @@ export const ObjectFilterDropdownFilterSelectMenuItem = ({
OBJECT_FILTER_DROPDOWN_ID,
);
const filterDefinitionToSelect = formatFieldMetadataItemAsFilterDefinition({
field: fieldMetadataItemToSelect,
});
const isSelectedItem = useRecoilValue(
isSelectedItemIdSelector(fieldMetadataItemToSelect.id),
);
const isACompositeField = isCompositeField(fieldMetadataItemToSelect.type);
const setSelectedOperandInDropdown = useSetRecoilComponentStateV2(
selectedOperandInDropdownComponentState,
);
@ -85,27 +80,28 @@ export const ObjectFilterDropdownFilterSelectMenuItem = ({
advancedFilterViewFilterId,
);
const handleSelectFilterDefinition = (
availableFilterDefinition: RecordFilterDefinition,
) => {
const handleSelectFilter = (fieldMetadataItem: FieldMetadataItem) => {
closeAdvancedFilterDropdown();
setFieldMetadataItemIdUsedInDropdown(
availableFilterDefinition.fieldMetadataId,
);
setFilterDefinitionUsedInDropdown(availableFilterDefinition);
setFieldMetadataItemIdUsedInDropdown(fieldMetadataItem.id);
const filterDefinition = formatFieldMetadataItemAsFilterDefinition({
field: fieldMetadataItem,
});
setFilterDefinitionUsedInDropdown(filterDefinition);
if (
availableFilterDefinition.type === 'RELATION' ||
availableFilterDefinition.type === 'SELECT'
filterDefinition.type === 'RELATION' ||
filterDefinition.type === 'SELECT'
) {
setHotkeyScope(RelationPickerHotkeyScope.RelationPicker);
}
setSelectedOperandInDropdown(
getRecordFilterOperandsForRecordFilterDefinition(
availableFilterDefinition,
)[0],
getRecordFilterOperands({
filterType: filterDefinition.type,
})[0],
);
setObjectFilterDropdownFilterIsSelected(true);
@ -113,20 +109,29 @@ export const ObjectFilterDropdownFilterSelectMenuItem = ({
const { getIcon } = useIcons();
const Icon = getIcon(fieldMetadataItemToSelect.icon);
const shouldShowSubMenu = isCompositeField(fieldMetadataItemToSelect.type);
const handleClick = () => {
resetSelectedItem();
if (isACompositeField) {
// TODO: create isCompositeFilterableFieldType type guard
setObjectFilterDropdownSubMenuFieldType(
filterDefinitionToSelect.type as CompositeFilterableFieldType,
);
setObjectFilterDropdownFirstLevelFilterDefinition(
filterDefinitionToSelect,
);
const filterType = getFilterTypeFromFieldType(
fieldMetadataItemToSelect.type,
);
if (isCompositeField(filterType)) {
setObjectFilterDropdownSubMenuFieldType(filterType);
const filterDefinition = formatFieldMetadataItemAsFilterDefinition({
field: fieldMetadataItemToSelect,
});
setObjectFilterDropdownFirstLevelFilterDefinition(filterDefinition);
setFieldMetadataItemIdUsedInDropdown(fieldMetadataItemToSelect.id);
setObjectFilterDropdownIsSelectingCompositeField(true);
} else {
handleSelectFilterDefinition(filterDefinitionToSelect);
handleSelectFilter(fieldMetadataItemToSelect);
}
};
@ -135,9 +140,9 @@ export const ObjectFilterDropdownFilterSelectMenuItem = ({
selected={false}
hovered={isSelectedItem}
onClick={handleClick}
LeftIcon={getIcon(filterDefinitionToSelect.iconName)}
text={filterDefinitionToSelect.label}
hasSubMenu={isACompositeField}
LeftIcon={Icon}
text={fieldMetadataItemToSelect.label}
hasSubMenu={shouldShowSubMenu}
/>
);
};

View File

@ -1,7 +1,8 @@
import { ChangeEvent, useCallback, useState } from 'react';
import { v4 } from 'uuid';
import { filterDefinitionUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/filterDefinitionUsedInDropdownComponentState';
import { formatFieldMetadataItemAsFilterDefinition } from '@/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions';
import { fieldMetadataItemUsedInDropdownComponentSelector } from '@/object-record/object-filter-dropdown/states/fieldMetadataItemUsedInDropdownComponentSelector';
import { selectedFilterComponentState } from '@/object-record/object-filter-dropdown/states/selectedFilterComponentState';
import { selectedOperandInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/selectedOperandInDropdownComponentState';
import { useApplyRecordFilter } from '@/object-record/record-filter/hooks/useApplyRecordFilter';
@ -13,8 +14,8 @@ export const ObjectFilterDropdownNumberInput = () => {
selectedOperandInDropdownComponentState,
);
const filterDefinitionUsedInDropdown = useRecoilComponentValueV2(
filterDefinitionUsedInDropdownComponentState,
const fieldMetadataItemUsedInDropdown = useRecoilComponentValueV2(
fieldMetadataItemUsedInDropdownComponentSelector,
);
const selectedFilter = useRecoilComponentValueV2(
@ -39,27 +40,32 @@ export const ObjectFilterDropdownNumberInput = () => {
},
[hasFocused],
);
return (
filterDefinitionUsedInDropdown &&
fieldMetadataItemUsedInDropdown &&
selectedOperandInDropdown && (
<DropdownMenuInput
ref={handleInputRef}
value={inputValue}
autoFocus
type="number"
placeholder={filterDefinitionUsedInDropdown.label}
placeholder={fieldMetadataItemUsedInDropdown.label}
onChange={(event: ChangeEvent<HTMLInputElement>) => {
const newValue = event.target.value;
setInputValue(newValue);
const filterDefinition = formatFieldMetadataItemAsFilterDefinition({
field: fieldMetadataItemUsedInDropdown,
});
applyRecordFilter({
id: selectedFilter?.id ? selectedFilter.id : v4(),
fieldMetadataId: filterDefinitionUsedInDropdown.fieldMetadataId,
fieldMetadataId: fieldMetadataItemUsedInDropdown?.id ?? '',
value: newValue,
operand: selectedOperandInDropdown,
displayValue: newValue,
definition: filterDefinitionUsedInDropdown,
definition: filterDefinition,
viewFilterGroupId: selectedFilter?.viewFilterGroupId,
});
}}

View File

@ -1,10 +1,16 @@
import { v4 } from 'uuid';
import { filterDefinitionUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/filterDefinitionUsedInDropdownComponentState';
import {
formatFieldMetadataItemAsFilterDefinition,
getFilterTypeFromFieldType,
} from '@/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions';
import { fieldMetadataItemUsedInDropdownComponentSelector } from '@/object-record/object-filter-dropdown/states/fieldMetadataItemUsedInDropdownComponentSelector';
import { selectedFilterComponentState } from '@/object-record/object-filter-dropdown/states/selectedFilterComponentState';
import { selectedOperandInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/selectedOperandInDropdownComponentState';
import { subFieldNameUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/subFieldNameUsedInDropdownComponentState';
import { getInitialFilterValue } from '@/object-record/object-filter-dropdown/utils/getInitialFilterValue';
import { useApplyRecordFilter } from '@/object-record/record-filter/hooks/useApplyRecordFilter';
import { getRecordFilterOperands } from '@/object-record/record-filter/utils/getRecordFilterOperands';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
@ -13,7 +19,6 @@ import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
import styled from '@emotion/styled';
import { isDefined } from 'twenty-shared';
import { MenuItem } from 'twenty-ui';
import { getRecordFilterOperandsForRecordFilterDefinition } from '../../record-filter/utils/getRecordFilterOperandsForRecordFilterDefinition';
import { getOperandLabel } from '../utils/getOperandLabel';
const StyledDropdownMenuItemsContainer = styled(DropdownMenuItemsContainer)`
@ -22,8 +27,8 @@ const StyledDropdownMenuItemsContainer = styled(DropdownMenuItemsContainer)`
`;
export const ObjectFilterDropdownOperandSelect = () => {
const filterDefinitionUsedInDropdown = useRecoilComponentValueV2(
filterDefinitionUsedInDropdownComponentState,
const fieldMetadataItemUsedInDropdown = useRecoilComponentValueV2(
fieldMetadataItemUsedInDropdownComponentSelector,
);
const setSelectedOperandInDropdown = useSetRecoilComponentStateV2(
@ -34,14 +39,21 @@ export const ObjectFilterDropdownOperandSelect = () => {
selectedFilterComponentState,
);
const subFieldNameUsedInDropdown = useRecoilComponentValueV2(
subFieldNameUsedInDropdownComponentState,
);
const { applyRecordFilter } = useApplyRecordFilter();
const { closeDropdown } = useDropdown();
const operandsForFilterType = isDefined(filterDefinitionUsedInDropdown)
? getRecordFilterOperandsForRecordFilterDefinition(
filterDefinitionUsedInDropdown,
)
const operandsForFilterType = isDefined(fieldMetadataItemUsedInDropdown)
? getRecordFilterOperands({
filterType: getFilterTypeFromFieldType(
fieldMetadataItemUsedInDropdown.type,
),
subFieldName: subFieldNameUsedInDropdown,
})
: [];
const handleOperandChange = (newOperand: ViewFilterOperand) => {
@ -55,36 +67,48 @@ export const ObjectFilterDropdownOperandSelect = () => {
setSelectedOperandInDropdown(newOperand);
if (isValuelessOperand && isDefined(filterDefinitionUsedInDropdown)) {
if (isValuelessOperand && isDefined(fieldMetadataItemUsedInDropdown)) {
const filterDefinition = formatFieldMetadataItemAsFilterDefinition({
field: fieldMetadataItemUsedInDropdown,
});
applyRecordFilter({
id: v4(),
fieldMetadataId: filterDefinitionUsedInDropdown?.fieldMetadataId ?? '',
fieldMetadataId: fieldMetadataItemUsedInDropdown.id,
displayValue: '',
operand: newOperand,
value: '',
definition: filterDefinitionUsedInDropdown,
definition: filterDefinition,
});
return;
}
if (
isDefined(filterDefinitionUsedInDropdown) &&
isDefined(fieldMetadataItemUsedInDropdown) &&
isDefined(selectedFilter)
) {
const filterType = getFilterTypeFromFieldType(
fieldMetadataItemUsedInDropdown.type,
);
const { value, displayValue } = getInitialFilterValue(
filterDefinitionUsedInDropdown.type,
filterType,
newOperand,
selectedFilter.value,
selectedFilter.displayValue,
);
const filterDefinition = formatFieldMetadataItemAsFilterDefinition({
field: fieldMetadataItemUsedInDropdown,
});
applyRecordFilter({
id: selectedFilter.id ? selectedFilter.id : v4(),
fieldMetadataId: selectedFilter.fieldMetadataId,
displayValue,
operand: newOperand,
value,
definition: filterDefinitionUsedInDropdown,
definition: filterDefinition,
});
}
};

View File

@ -13,7 +13,8 @@ import { SelectableList } from '@/ui/layout/selectable-list/components/Selectabl
import { useSelectableListStates } from '@/ui/layout/selectable-list/hooks/internal/useSelectableListStates';
import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList';
import { filterDefinitionUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/filterDefinitionUsedInDropdownComponentState';
import { formatFieldMetadataItemAsFilterDefinition } from '@/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions';
import { fieldMetadataItemUsedInDropdownComponentSelector } from '@/object-record/object-filter-dropdown/states/fieldMetadataItemUsedInDropdownComponentSelector';
import { objectFilterDropdownSearchInputComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownSearchInputComponentState';
import { objectFilterDropdownSelectedOptionValuesComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownSelectedOptionValuesComponentState';
import { selectedFilterComponentState } from '@/object-record/object-filter-dropdown/states/selectedFilterComponentState';
@ -32,8 +33,8 @@ type SelectOptionForFilter = FieldMetadataItemOption & {
};
export const ObjectFilterDropdownOptionSelect = () => {
const filterDefinitionUsedInDropdown = useRecoilComponentValueV2(
filterDefinitionUsedInDropdownComponentState,
const fieldMetadataItemUsedInDropdown = useRecoilComponentValueV2(
fieldMetadataItemUsedInDropdownComponentSelector,
);
const objectFilterDropdownSelectedOptionValues = useRecoilComponentValueV2(
@ -66,7 +67,7 @@ export const ObjectFilterDropdownOptionSelect = () => {
const selectedItemId = useRecoilValue(selectedItemIdState);
const fieldMetaDataId = filterDefinitionUsedInDropdown?.fieldMetadataId ?? '';
const fieldMetaDataId = fieldMetadataItemUsedInDropdown?.id ?? '';
const { selectOptions } = useOptionsForSelect(fieldMetaDataId);
@ -125,7 +126,7 @@ export const ObjectFilterDropdownOptionSelect = () => {
: selectedOptions.map((option) => option.label).join(', ');
if (
isDefined(filterDefinitionUsedInDropdown) &&
isDefined(fieldMetadataItemUsedInDropdown) &&
isDefined(selectedOperandInDropdown)
) {
const newFilterValue =
@ -133,12 +134,16 @@ export const ObjectFilterDropdownOptionSelect = () => {
? JSON.stringify(selectedOptions.map((option) => option.value))
: EMPTY_FILTER_VALUE;
const filterDefinition = formatFieldMetadataItemAsFilterDefinition({
field: fieldMetadataItemUsedInDropdown,
});
applyRecordFilter({
id: selectedFilter?.id ? selectedFilter.id : v4(),
definition: filterDefinitionUsedInDropdown,
definition: filterDefinition,
operand: selectedOperandInDropdown,
displayValue: filterDisplayValue,
fieldMetadataId: filterDefinitionUsedInDropdown.fieldMetadataId,
fieldMetadataId: fieldMetadataItemUsedInDropdown.id,
value: newFilterValue,
viewFilterGroupId: selectedFilter?.viewFilterGroupId,
});

View File

@ -1,6 +1,5 @@
import { v4 } from 'uuid';
import { filterDefinitionUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/filterDefinitionUsedInDropdownComponentState';
import { selectedOperandInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/selectedOperandInDropdownComponentState';
import { RATING_VALUES } from '@/object-record/record-field/meta-types/constants/RatingValues';
import { FieldRatingValue } from '@/object-record/record-field/types/FieldMetadata';
@ -9,6 +8,8 @@ import { RatingInput } from '@/ui/field/input/components/RatingInput';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { formatFieldMetadataItemAsFilterDefinition } from '@/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions';
import { fieldMetadataItemUsedInDropdownComponentSelector } from '@/object-record/object-filter-dropdown/states/fieldMetadataItemUsedInDropdownComponentSelector';
import { selectedFilterComponentState } from '@/object-record/object-filter-dropdown/states/selectedFilterComponentState';
const convertFieldRatingValueToNumber = (
rating: Exclude<FieldRatingValue, null>,
@ -37,8 +38,8 @@ export const ObjectFilterDropdownRatingInput = () => {
selectedOperandInDropdownComponentState,
);
const filterDefinitionUsedInDropdown = useRecoilComponentValueV2(
filterDefinitionUsedInDropdownComponentState,
const fieldMetadataItemUsedInDropdown = useRecoilComponentValueV2(
fieldMetadataItemUsedInDropdownComponentSelector,
);
const selectedFilter = useRecoilComponentValueV2(
@ -48,7 +49,7 @@ export const ObjectFilterDropdownRatingInput = () => {
const { applyRecordFilter } = useApplyRecordFilter();
return (
filterDefinitionUsedInDropdown &&
fieldMetadataItemUsedInDropdown &&
selectedOperandInDropdown && (
<DropdownMenuItemsContainer>
<RatingInput
@ -58,13 +59,17 @@ export const ObjectFilterDropdownRatingInput = () => {
return;
}
const filterDefinition = formatFieldMetadataItemAsFilterDefinition({
field: fieldMetadataItemUsedInDropdown,
});
applyRecordFilter?.({
id: selectedFilter?.id ? selectedFilter.id : v4(),
fieldMetadataId: filterDefinitionUsedInDropdown.fieldMetadataId,
fieldMetadataId: fieldMetadataItemUsedInDropdown.id,
value: convertFieldRatingValueToNumber(newValue),
operand: selectedOperandInDropdown,
displayValue: convertFieldRatingValueToNumber(newValue),
definition: filterDefinitionUsedInDropdown,
definition: filterDefinition,
viewFilterGroupId: selectedFilter?.viewFilterGroupId,
});
}}

View File

@ -1,8 +1,8 @@
import { useState } from 'react';
import { v4 } from 'uuid';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { getRelationObjectMetadataNameSingular } from '@/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions';
import {
formatFieldMetadataItemAsFilterDefinition,
getRelationObjectMetadataNameSingular,
} from '@/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions';
import { ObjectFilterDropdownRecordPinnedItems } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownRecordPinnedItems';
import { CURRENT_WORKSPACE_MEMBER_SELECTABLE_ITEM_ID } from '@/object-record/object-filter-dropdown/constants/CurrentWorkspaceMemberSelectableItemId';
import { fieldMetadataItemUsedInDropdownComponentSelector } from '@/object-record/object-filter-dropdown/states/fieldMetadataItemUsedInDropdownComponentSelector';
@ -24,6 +24,7 @@ import { jsonRelationFilterValueSchema } from '@/views/view-filter-value/validat
import { simpleRelationFilterValueSchema } from '@/views/view-filter-value/validation-schemas/simpleRelationFilterValueSchema';
import { isDefined } from 'twenty-shared';
import { IconUserCircle } from 'twenty-ui';
import { v4 } from 'uuid';
export const EMPTY_FILTER_VALUE: string = JSON.stringify({
isCurrentWorkspaceMemberSelected: false,
@ -68,8 +69,6 @@ export const ObjectFilterDropdownRecordSelect = ({
const { currentViewWithCombinedFiltersAndSorts } =
useGetCurrentView(viewComponentId);
const [fieldId] = useState(v4());
const { isCurrentWorkspaceMemberSelected } = jsonRelationFilterValueSchema
.catch({
isCurrentWorkspaceMemberSelected: false,
@ -200,17 +199,21 @@ export const ObjectFilterDropdownRecordSelect = ({
currentViewWithCombinedFiltersAndSorts?.viewFilters.find(
(viewFilter) =>
viewFilter.fieldMetadataId ===
filterDefinitionUsedInDropdown.fieldMetadataId,
fieldMetadataItemUsedInFilterDropdown.id,
);
const filterId = viewFilter?.id ?? fieldId;
const filterId = viewFilter?.id ?? v4();
const filterDefinition = formatFieldMetadataItemAsFilterDefinition({
field: fieldMetadataItemUsedInFilterDropdown,
});
applyRecordFilter({
id: selectedFilter?.id ? selectedFilter.id : filterId,
definition: filterDefinitionUsedInDropdown,
definition: filterDefinition,
operand: selectedOperandInDropdown,
displayValue: filterDisplayValue,
fieldMetadataId: filterDefinitionUsedInDropdown.fieldMetadataId,
fieldMetadataId: fieldMetadataItemUsedInFilterDropdown.id,
value: newFilterValue,
viewFilterGroupId: selectedFilter?.viewFilterGroupId,
});

View File

@ -1,6 +1,6 @@
import { ChangeEvent, useCallback, useState } from 'react';
import { filterDefinitionUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/filterDefinitionUsedInDropdownComponentState';
import { fieldMetadataItemUsedInDropdownComponentSelector } from '@/object-record/object-filter-dropdown/states/fieldMetadataItemUsedInDropdownComponentSelector';
import { objectFilterDropdownSearchInputComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownSearchInputComponentState';
import { selectedOperandInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/selectedOperandInDropdownComponentState';
import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
@ -8,8 +8,8 @@ import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
export const ObjectFilterDropdownSearchInput = () => {
const filterDefinitionUsedInDropdown = useRecoilComponentValueV2(
filterDefinitionUsedInDropdownComponentState,
const fieldMetadataItemUsedInDropdown = useRecoilComponentValueV2(
fieldMetadataItemUsedInDropdownComponentSelector,
);
const selectedOperandInDropdown = useRecoilComponentValueV2(
@ -37,14 +37,14 @@ export const ObjectFilterDropdownSearchInput = () => {
[hasFocused],
);
return (
filterDefinitionUsedInDropdown &&
fieldMetadataItemUsedInDropdown &&
selectedOperandInDropdown && (
<DropdownMenuSearchInput
ref={handleInputRef}
autoFocus
type="text"
value={objectFilterDropdownSearchInput}
placeholder={filterDefinitionUsedInDropdown.label}
placeholder={fieldMetadataItemUsedInDropdown.label}
onChange={(event: ChangeEvent<HTMLInputElement>) => {
setObjectFilterDropdownSearchInput(event.target.value);
}}

View File

@ -1,8 +1,9 @@
import { useState } from 'react';
import { v4 } from 'uuid';
import { formatFieldMetadataItemAsFilterDefinition } from '@/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions';
import { useEmptyRecordFilter } from '@/object-record/object-filter-dropdown/hooks/useEmptyRecordFilter';
import { filterDefinitionUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/filterDefinitionUsedInDropdownComponentState';
import { fieldMetadataItemUsedInDropdownComponentSelector } from '@/object-record/object-filter-dropdown/states/fieldMetadataItemUsedInDropdownComponentSelector';
import { objectFilterDropdownSearchInputComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownSearchInputComponentState';
import { objectFilterDropdownSelectedRecordIdsComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownSelectedRecordIdsComponentState';
import { selectedFilterComponentState } from '@/object-record/object-filter-dropdown/states/selectedFilterComponentState';
@ -50,8 +51,8 @@ export const ObjectFilterDropdownSourceSelect = ({
selectedOperandInDropdownComponentState,
);
const filterDefinitionUsedInDropdown = useRecoilComponentValueV2(
filterDefinitionUsedInDropdownComponentState,
const fieldMetadataItemUsedInFilterDropdown = useRecoilComponentValueV2(
fieldMetadataItemUsedInDropdownComponentSelector,
);
const { applyRecordFilter } = useApplyRecordFilter(viewComponentId);
@ -87,13 +88,15 @@ export const ObjectFilterDropdownSourceSelect = ({
(id) => id !== itemToSelect.id,
);
if (!filterDefinitionUsedInDropdown) {
throw new Error('Filter definition used in dropdown should be defined');
if (!isDefined(fieldMetadataItemUsedInFilterDropdown)) {
throw new Error(
'Field metadata item used in filter dropdown should be defined',
);
}
if (newSelectedItemIds.length === 0) {
emptyRecordFilter();
removeRecordFilter(filterDefinitionUsedInDropdown.fieldMetadataId);
removeRecordFilter(fieldMetadataItemUsedInFilterDropdown.id);
deleteCombinedViewFilter(fieldId);
return;
}
@ -110,7 +113,7 @@ export const ObjectFilterDropdownSourceSelect = ({
: selectedItemNames.join(', ');
if (
isDefined(filterDefinitionUsedInDropdown) &&
isDefined(fieldMetadataItemUsedInFilterDropdown) &&
isDefined(selectedOperandInDropdown)
) {
const newFilterValue =
@ -122,17 +125,21 @@ export const ObjectFilterDropdownSourceSelect = ({
currentViewWithCombinedFiltersAndSorts?.viewFilters.find(
(viewFilter) =>
viewFilter.fieldMetadataId ===
filterDefinitionUsedInDropdown.fieldMetadataId,
fieldMetadataItemUsedInFilterDropdown.id,
);
const filterId = viewFilter?.id ?? fieldId;
const filterDefinition = formatFieldMetadataItemAsFilterDefinition({
field: fieldMetadataItemUsedInFilterDropdown,
});
applyRecordFilter({
id: selectedFilter?.id ? selectedFilter.id : filterId,
definition: filterDefinitionUsedInDropdown,
definition: filterDefinition,
operand: selectedOperandInDropdown || ViewFilterOperand.Is,
displayValue: filterDisplayValue,
fieldMetadataId: filterDefinitionUsedInDropdown.fieldMetadataId,
fieldMetadataId: fieldMetadataItemUsedInFilterDropdown.id,
value: newFilterValue,
viewFilterGroupId: selectedFilter?.viewFilterGroupId,
});

View File

@ -1,7 +1,7 @@
import { ChangeEvent, useCallback, useState } from 'react';
import { v4 } from 'uuid';
import { filterDefinitionUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/filterDefinitionUsedInDropdownComponentState';
import { formatFieldMetadataItemAsFilterDefinition } from '@/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions';
import { fieldMetadataItemUsedInDropdownComponentSelector } from '@/object-record/object-filter-dropdown/states/fieldMetadataItemUsedInDropdownComponentSelector';
import { objectFilterDropdownSearchInputComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownSearchInputComponentState';
import { selectedFilterComponentState } from '@/object-record/object-filter-dropdown/states/selectedFilterComponentState';
import { selectedOperandInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/selectedOperandInDropdownComponentState';
@ -9,13 +9,13 @@ import { useApplyRecordFilter } from '@/object-record/record-filter/hooks/useApp
import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
import { v4 } from 'uuid';
export const ObjectFilterDropdownTextSearchInput = () => {
const [filterId] = useState(v4());
const [hasFocused, setHasFocused] = useState(false);
const filterDefinitionUsedInDropdown = useRecoilComponentValueV2(
filterDefinitionUsedInDropdownComponentState,
const fieldMetadataItemUsedInDropdown = useRecoilComponentValueV2(
fieldMetadataItemUsedInDropdownComponentSelector,
);
const selectedOperandInDropdown = useRecoilComponentValueV2(
@ -47,24 +47,28 @@ export const ObjectFilterDropdownTextSearchInput = () => {
[hasFocused],
);
return (
filterDefinitionUsedInDropdown &&
fieldMetadataItemUsedInDropdown &&
selectedOperandInDropdown && (
<DropdownMenuSearchInput
ref={handleInputRef}
autoFocus
type="text"
placeholder={filterDefinitionUsedInDropdown.label}
placeholder={fieldMetadataItemUsedInDropdown.label}
value={selectedFilter?.value ?? objectFilterDropdownSearchInput}
onChange={(event: ChangeEvent<HTMLInputElement>) => {
setObjectFilterDropdownSearchInput(event.target.value);
const filterDefinition = formatFieldMetadataItemAsFilterDefinition({
field: fieldMetadataItemUsedInDropdown,
});
applyRecordFilter({
id: selectedFilter?.id ?? filterId,
fieldMetadataId: filterDefinitionUsedInDropdown.fieldMetadataId,
id: selectedFilter?.id ?? v4(),
fieldMetadataId: fieldMetadataItemUsedInDropdown.id,
value: event.target.value,
operand: selectedOperandInDropdown,
displayValue: event.target.value,
definition: filterDefinitionUsedInDropdown,
definition: filterDefinition,
viewFilterGroupId: selectedFilter?.viewFilterGroupId,
});
}}

View File

@ -2,7 +2,7 @@ import { ObjectFilterDropdownComponentInstanceContext } from '@/object-record/ob
import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2';
export const subFieldNameUsedInDropdownComponentState = createComponentStateV2<
string | null
string | null | undefined
>({
key: 'subFieldNameUsedInDropdownComponentState',
defaultValue: null,

View File

@ -0,0 +1,7 @@
import { FieldActorValue } from '@/object-record/record-field/types/FieldMetadata';
export const isFilterOnActorSourceSubField = (
subFieldName?: string | null | undefined,
) => {
return subFieldName === ('source' satisfies keyof FieldActorValue);
};

View File

@ -47,6 +47,8 @@ describe('useRemoveRecordFilter', () => {
label: 'Test Field',
iconName: 'IconText',
},
label: 'Test Field',
type: 'TEXT',
};
// First add a filter
@ -96,6 +98,8 @@ describe('useRemoveRecordFilter', () => {
label: 'Test Field',
iconName: 'IconText',
},
label: 'Test Field',
type: 'TEXT',
};
// Add a filter

View File

@ -41,6 +41,8 @@ describe('useUpsertRecordFilter', () => {
label: 'Test Field',
iconName: 'IconText',
},
label: 'Test Field',
type: 'TEXT',
};
act(() => {
@ -79,6 +81,8 @@ describe('useUpsertRecordFilter', () => {
label: 'Test Field',
iconName: 'IconText',
},
label: 'Test Field',
type: 'TEXT',
};
const updatedFilter: RecordFilter = {
@ -93,6 +97,8 @@ describe('useUpsertRecordFilter', () => {
label: 'Test Field',
iconName: 'IconText',
},
label: 'Test Field',
type: 'TEXT',
};
act(() => {

View File

@ -17,16 +17,25 @@ export const useUpsertRecordFilter = () => {
currentRecordFiltersCallbackState,
);
// TODO: This is a temporary solution to ensure that the record filter is compatible with filter definitions
// Label and type will be set without definition
const recordFilterToSet: RecordFilter = {
...filter,
label: filter.definition.label,
type: filter.definition.type,
};
const foundRecordFilterInCurrentRecordFilters =
currentRecordFilters.some(
(existingFilter) =>
existingFilter.fieldMetadataId === filter.fieldMetadataId,
existingFilter.fieldMetadataId ===
recordFilterToSet.fieldMetadataId,
);
if (!foundRecordFilterInCurrentRecordFilters) {
set(currentRecordFiltersCallbackState, [
...currentRecordFilters,
filter,
recordFilterToSet,
]);
} else {
set(currentRecordFiltersCallbackState, (currentRecordFilters) => {
@ -34,11 +43,12 @@ export const useUpsertRecordFilter = () => {
const indexOfFilterToUpdate = newCurrentRecordFilters.findIndex(
(existingFilter) =>
existingFilter.fieldMetadataId === filter.fieldMetadataId,
existingFilter.fieldMetadataId ===
recordFilterToSet.fieldMetadataId,
);
newCurrentRecordFilters[indexOfFilterToUpdate] = {
...filter,
...recordFilterToSet,
};
return newCurrentRecordFilters;

View File

@ -1,3 +1,4 @@
import { FilterableFieldType } from '@/object-record/record-filter/types/FilterableFieldType';
import { RecordFilterDefinition } from '@/object-record/record-filter/types/RecordFilterDefinition';
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
@ -7,11 +8,12 @@ export type RecordFilter = {
fieldMetadataId: string;
value: string;
displayValue: string;
type?: FilterableFieldType;
viewFilterGroupId?: string;
displayAvatarUrl?: string;
operand: ViewFilterOperand;
positionInViewFilterGroup?: number | null;
definition: RecordFilterDefinition;
label?: string;
subFieldName?: string;
subFieldName?: string | null | undefined;
};

View File

@ -795,7 +795,7 @@ const computeFilterRecordGqlOperationFilter = (
);
default:
throw new Error(
`Unknown operand ${filter.operand} for ${filter.definition.label} filter`,
`Unknown operand ${filter.operand} for ${filter.label} filter`,
);
}
}

View File

@ -0,0 +1,105 @@
import { isFilterOnActorSourceSubField } from '@/object-record/object-filter-dropdown/utils/isFilterOnActorSourceSubField';
import { FilterableFieldType } from '@/object-record/record-filter/types/FilterableFieldType';
import { ViewFilterOperand as RecordFilterOperand } from '@/views/types/ViewFilterOperand';
export type GetRecordFilterOperandsParams = {
filterType: FilterableFieldType;
subFieldName?: string | null | undefined;
};
export const getRecordFilterOperands = ({
filterType,
subFieldName,
}: GetRecordFilterOperandsParams): RecordFilterOperand[] => {
const emptyOperands = [
RecordFilterOperand.IsEmpty,
RecordFilterOperand.IsNotEmpty,
];
const relationOperands = [RecordFilterOperand.Is, RecordFilterOperand.IsNot];
switch (filterType) {
case 'TEXT':
case 'EMAILS':
case 'FULL_NAME':
case 'ADDRESS':
case 'LINKS':
case 'PHONES':
return [
RecordFilterOperand.Contains,
RecordFilterOperand.DoesNotContain,
...emptyOperands,
];
case 'CURRENCY':
case 'NUMBER':
return [
RecordFilterOperand.GreaterThan,
RecordFilterOperand.LessThan,
...emptyOperands,
];
case 'RAW_JSON':
return [
RecordFilterOperand.Contains,
RecordFilterOperand.DoesNotContain,
...emptyOperands,
];
case 'DATE_TIME':
case 'DATE':
return [
RecordFilterOperand.Is,
RecordFilterOperand.IsRelative,
RecordFilterOperand.IsInPast,
RecordFilterOperand.IsInFuture,
RecordFilterOperand.IsToday,
RecordFilterOperand.IsBefore,
RecordFilterOperand.IsAfter,
...emptyOperands,
];
case 'RATING':
return [
RecordFilterOperand.Is,
RecordFilterOperand.GreaterThan,
RecordFilterOperand.LessThan,
...emptyOperands,
];
case 'RELATION':
return [...relationOperands, ...emptyOperands];
case 'MULTI_SELECT':
return [
RecordFilterOperand.Contains,
RecordFilterOperand.DoesNotContain,
...emptyOperands,
];
case 'SELECT':
return [
RecordFilterOperand.Is,
RecordFilterOperand.IsNot,
...emptyOperands,
];
case 'ACTOR': {
if (isFilterOnActorSourceSubField(subFieldName)) {
return [
RecordFilterOperand.Is,
RecordFilterOperand.IsNot,
...emptyOperands,
];
}
return [
RecordFilterOperand.Contains,
RecordFilterOperand.DoesNotContain,
...emptyOperands,
];
}
case 'ARRAY':
return [
RecordFilterOperand.Contains,
RecordFilterOperand.DoesNotContain,
...emptyOperands,
];
case 'BOOLEAN':
return [RecordFilterOperand.Is];
default:
return [];
}
};

View File

@ -30,9 +30,7 @@ export const RecordTableEmptyStateSoftDelete = () => {
const handleButtonClick = async () => {
const deletedFilter = tableFilters.find(
(filter) =>
filter.definition.label === 'Deleted' &&
filter.operand === 'isNotEmpty',
(filter) => filter.label === 'Deleted' && filter.operand === 'isNotEmpty',
);
if (!deletedFilter) {

View File

@ -1,5 +1,6 @@
import { useIcons } from 'twenty-ui';
import { useFieldMetadataItemById } from '@/object-metadata/hooks/useFieldMetadataItemById';
import { getOperandLabelShort } from '@/object-record/object-filter-dropdown/utils/getOperandLabel';
import { RecordFilter } from '@/object-record/record-filter/types/RecordFilter';
import { SortOrFilterChip } from '@/views/components/SortOrFilterChip';
@ -14,13 +15,20 @@ export const EditableFilterChip = ({
onRemove,
}: EditableFilterChipProps) => {
const { getIcon } = useIcons();
const { fieldMetadataItem } = useFieldMetadataItemById(
viewFilter.fieldMetadataId,
);
const FieldMetadataItemIcon = getIcon(fieldMetadataItem.icon);
return (
<SortOrFilterChip
key={viewFilter.id}
testId={viewFilter.id}
labelKey={`${viewFilter.definition.label}${getOperandLabelShort(viewFilter.operand)}`}
labelKey={`${viewFilter.label}${getOperandLabelShort(viewFilter.operand)}`}
labelValue={viewFilter.displayValue}
Icon={getIcon(viewFilter.definition.iconName)}
Icon={FieldMetadataItemIcon}
onRemove={onRemove}
/>
);

View File

@ -1,5 +1,6 @@
import { useIcons } from 'twenty-ui';
import { useFieldMetadataItemById } from '@/object-metadata/hooks/useFieldMetadataItemById';
import { useObjectNameSingularFromPlural } from '@/object-metadata/hooks/useObjectNameSingularFromPlural';
import { useRemoveRecordFilter } from '@/object-record/record-filter/hooks/useRemoveRecordFilter';
import { RecordFilter } from '@/object-record/record-filter/types/RecordFilter';
@ -9,12 +10,12 @@ import { useDeleteCombinedViewFilters } from '@/views/hooks/useDeleteCombinedVie
import { useParams } from 'react-router-dom';
type VariantFilterChipProps = {
viewFilter: RecordFilter;
recordFilter: RecordFilter;
viewBarId: string;
};
export const VariantFilterChip = ({
viewFilter,
recordFilter,
viewBarId,
}: VariantFilterChipProps) => {
const { deleteCombinedViewFilter } = useDeleteCombinedViewFilters();
@ -30,17 +31,23 @@ export const VariantFilterChip = ({
viewBarId,
});
const { fieldMetadataItem } = useFieldMetadataItemById(
recordFilter.fieldMetadataId,
);
const { removeRecordFilter } = useRemoveRecordFilter();
const { getIcon } = useIcons();
const FieldMetadataItemIcon = getIcon(fieldMetadataItem.icon);
const handleRemoveClick = () => {
deleteCombinedViewFilter(viewFilter.id);
removeRecordFilter(viewFilter.fieldMetadataId);
deleteCombinedViewFilter(recordFilter.id);
removeRecordFilter(recordFilter.fieldMetadataId);
if (
viewFilter.definition.label === 'Deleted' &&
viewFilter.operand === 'isNotEmpty'
recordFilter.label === 'Deleted' &&
recordFilter.operand === 'isNotEmpty'
) {
toggleSoftDeleteFilterState(false);
}
@ -48,11 +55,11 @@ export const VariantFilterChip = ({
return (
<SortOrFilterChip
key={viewFilter.fieldMetadataId}
testId={viewFilter.fieldMetadataId}
variant={viewFilter.variant}
labelValue={viewFilter.definition.label}
Icon={getIcon(viewFilter.definition.iconName)}
key={recordFilter.fieldMetadataId}
testId={recordFilter.fieldMetadataId}
variant={recordFilter.variant}
labelValue={recordFilter.label ?? ''}
Icon={FieldMetadataItemIcon}
onRemove={handleRemoveClick}
/>
);

View File

@ -193,7 +193,7 @@ export const ViewBarDetails = ({
{otherViewFilters.map((viewFilter) => (
<VariantFilterChip
key={viewFilter.fieldMetadataId}
viewFilter={viewFilter}
recordFilter={viewFilter}
viewBarId={viewBarId}
/>
))}

View File

@ -4,7 +4,7 @@ import { useEffect } from 'react';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
import { filterDefinitionUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/filterDefinitionUsedInDropdownComponentState';
import { fieldMetadataItemUsedInDropdownComponentSelector } from '@/object-record/object-filter-dropdown/states/fieldMetadataItemUsedInDropdownComponentSelector';
import { objectFilterDropdownSelectedOptionValuesComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownSelectedOptionValuesComponentState';
import { objectFilterDropdownSelectedRecordIdsComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownSelectedRecordIdsComponentState';
import { useGetCurrentView } from '@/views/hooks/useGetCurrentView';
@ -21,8 +21,8 @@ export const ViewBarFilterEffect = ({
}: ViewBarFilterEffectProps) => {
const { currentViewWithCombinedFiltersAndSorts } = useGetCurrentView();
const filterDefinitionUsedInDropdown = useRecoilComponentValueV2(
filterDefinitionUsedInDropdownComponentState,
const fieldMetadataItemUsedInDropdown = useRecoilComponentValueV2(
fieldMetadataItemUsedInDropdownComponentSelector,
filterDropdownId,
);
@ -38,12 +38,11 @@ export const ViewBarFilterEffect = ({
);
useEffect(() => {
if (filterDefinitionUsedInDropdown?.type === 'RELATION') {
if (fieldMetadataItemUsedInDropdown?.type === 'RELATION') {
const viewFilterUsedInDropdown =
currentViewWithCombinedFiltersAndSorts?.viewFilters.find(
(filter) =>
filter.fieldMetadataId ===
filterDefinitionUsedInDropdown?.fieldMetadataId,
filter.fieldMetadataId === fieldMetadataItemUsedInDropdown?.id,
);
const { selectedRecordIds } = jsonRelationFilterValueSchema
@ -57,14 +56,13 @@ export const ViewBarFilterEffect = ({
setObjectFilterDropdownSelectedRecordIds(selectedRecordIds);
} else if (
isDefined(filterDefinitionUsedInDropdown) &&
['SELECT', 'MULTI_SELECT'].includes(filterDefinitionUsedInDropdown.type)
isDefined(fieldMetadataItemUsedInDropdown) &&
['SELECT', 'MULTI_SELECT'].includes(fieldMetadataItemUsedInDropdown.type)
) {
const viewFilterUsedInDropdown =
currentViewWithCombinedFiltersAndSorts?.viewFilters.find(
(filter) =>
filter.fieldMetadataId ===
filterDefinitionUsedInDropdown?.fieldMetadataId,
filter.fieldMetadataId === fieldMetadataItemUsedInDropdown?.id,
);
const viewFilterSelectedRecords = isNonEmptyString(
@ -75,7 +73,7 @@ export const ViewBarFilterEffect = ({
setObjectFilterDropdownSelectedOptionValues(viewFilterSelectedRecords);
}
}, [
filterDefinitionUsedInDropdown,
fieldMetadataItemUsedInDropdown,
setObjectFilterDropdownSelectedRecordIds,
setObjectFilterDropdownSelectedOptionValues,
currentViewWithCombinedFiltersAndSorts,

View File

@ -1,7 +1,11 @@
import { act, renderHook } from '@testing-library/react';
import { formatFieldMetadataItemAsFilterDefinition } from '@/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions';
import {
formatFieldMetadataItemAsFilterDefinition,
getFilterTypeFromFieldType,
} from '@/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions';
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
import { RecordFilter } from '@/object-record/record-filter/types/RecordFilter';
import { RecordFilterDefinition } from '@/object-record/record-filter/types/RecordFilterDefinition';
import { usePrefetchedData } from '@/prefetch/hooks/usePrefetchedData';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
@ -39,7 +43,7 @@ describe('useApplyCurrentViewFiltersToCurrentRecordFilters', () => {
fieldMetadataId: mockFieldMetadataItem.id,
operand: ViewFilterOperand.Contains,
value: 'test',
displayValue: 'test',
displayValue: mockFieldMetadataItem.label,
viewFilterGroupId: 'group-1',
positionInViewFilterGroup: 0,
definition: mockFilterDefinition,
@ -99,7 +103,9 @@ describe('useApplyCurrentViewFiltersToCurrentRecordFilters', () => {
viewFilterGroupId: mockViewFilter.viewFilterGroupId,
positionInViewFilterGroup: mockViewFilter.positionInViewFilterGroup,
definition: mockFilterDefinition,
},
label: mockViewFilter.displayValue,
type: getFilterTypeFromFieldType(mockFieldMetadataItem.type),
} satisfies RecordFilter,
]);
});

View File

@ -1,7 +1,11 @@
import { act, renderHook } from '@testing-library/react';
import { formatFieldMetadataItemAsFilterDefinition } from '@/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions';
import {
formatFieldMetadataItemAsFilterDefinition,
getFilterTypeFromFieldType,
} from '@/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions';
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
import { RecordFilter } from '@/object-record/record-filter/types/RecordFilter';
import { RecordFilterDefinition } from '@/object-record/record-filter/types/RecordFilterDefinition';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { ViewFilter } from '@/views/types/ViewFilter';
@ -35,7 +39,7 @@ describe('useApplyViewFiltersToCurrentRecordFilters', () => {
fieldMetadataId: mockFieldMetadataItem.id,
operand: ViewFilterOperand.Contains,
value: 'test',
displayValue: 'test',
displayValue: mockFieldMetadataItem.label,
viewFilterGroupId: 'group-1',
positionInViewFilterGroup: 0,
definition: mockAvailableFilterDefinition,
@ -72,7 +76,9 @@ describe('useApplyViewFiltersToCurrentRecordFilters', () => {
viewFilterGroupId: mockViewFilter.viewFilterGroupId,
positionInViewFilterGroup: mockViewFilter.positionInViewFilterGroup,
definition: mockAvailableFilterDefinition,
},
label: mockViewFilter.displayValue,
type: getFilterTypeFromFieldType(mockFieldMetadataItem.type),
} satisfies RecordFilter,
]);
});

View File

@ -10,6 +10,7 @@ import { mapColumnDefinitionsToViewFields } from '@/views/utils/mapColumnDefinit
import { mapViewFieldsToColumnDefinitions } from '@/views/utils/mapViewFieldsToColumnDefinitions';
import { mapViewFiltersToFilters } from '@/views/utils/mapViewFiltersToFilters';
import { mapViewSortsToSorts } from '@/views/utils/mapViewSortsToSorts';
import { FieldMetadataType } from '~/generated/graphql';
const baseDefinition = {
@ -65,6 +66,10 @@ describe('mapViewFiltersToFilters', () => {
...baseDefinition,
type: 'FULL_NAME',
},
label: baseDefinition.label,
type: 'FULL_NAME',
positionInViewFilterGroup: undefined,
viewFilterGroupId: undefined,
},
];
expect(

View File

@ -26,6 +26,8 @@ export const mapViewFiltersToFilters = (
viewFilterGroupId: viewFilter.viewFilterGroupId,
positionInViewFilterGroup: viewFilter.positionInViewFilterGroup,
definition: viewFilter.definition ?? availableFilterDefinition,
label: viewFilter.definition?.label ?? availableFilterDefinition.label,
type: viewFilter.definition?.type ?? availableFilterDefinition.type,
};
})
.filter(isDefined);