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
@@ -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);
});
});
@@ -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';
@@ -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: '',
@@ -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 ? (
@@ -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,
});
@@ -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,
});
};
@@ -151,9 +151,7 @@ export const ObjectFilterDropdownFilterSelect = ({
filterDefinition: selectedFilterDefinition,
});
setFieldMetadataItemIdUsedInDropdown(
selectedFilterDefinition.fieldMetadataId,
);
setFieldMetadataItemIdUsedInDropdown(fieldMetadataItemId);
closeAdvancedFilterDropdown();
};
@@ -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>
@@ -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}
/>
);
};
@@ -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,
});
}}
@@ -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,
});
}
};
@@ -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,
});
@@ -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,
});
}}
@@ -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,
});
@@ -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);
}}
@@ -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,
});
@@ -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,
});
}}
@@ -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,
@@ -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);
};
@@ -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
@@ -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(() => {
@@ -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;
@@ -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;
};
@@ -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`,
);
}
}
@@ -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 [];
}
};
@@ -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) {
@@ -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}
/>
);
@@ -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}
/>
);
@@ -193,7 +193,7 @@ export const ViewBarDetails = ({
{otherViewFilters.map((viewFilter) => (
<VariantFilterChip
key={viewFilter.fieldMetadataId}
viewFilter={viewFilter}
recordFilter={viewFilter}
viewBarId={viewBarId}
/>
))}
@@ -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,
@@ -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,
]);
});
@@ -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,
]);
});
@@ -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(
@@ -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);