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

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