Refactored editable filter chip dropdown opening (#11765)

This PR is refactoring a part of the ongoing filter refactor that was
blocking other refactor in that area.

Precisely, the dropdown filter that was used with the editable filter
chip was initialized by two conflicting useEffect, causing many unwanted
and hard to tackle bugs when modifying other places in the code that
used the same dropdown.

We also remove a difficult to maintain pattern around
onToggleColumnFilterComponentState, which was storing a click handler in
a state, we want to avoid this pattern.

The hook useHandleToggleColumnFilter is also removed and replaced by
useOpenRecordFilterChipFromTableHeader.

The code is now synchronous and starts from the user click event that is
triggered on a table cell header filter button click.

Also : 

- Created a useSetEditableFilterChipDropdownStates that allows to
separate the code path of filter chip dropdown from the code path of
view bar global filter dropdown (will be continued in another refactor)
- Added useCreateEmptyFilterFromFieldMetadataItem to abstract empty
filter creation when opening a filter dropdown (will be used for other
refactor)
- Created a useOpenDropdownFromOutside hook that will also be used for
other refactor on filter
- Deleted EditableFilterDropdownButtonEffect
- Removed call to ViewBarFilterEffect (will be completely removed in
other refactors)
This commit is contained in:
Lucas Bordeau
2025-04-28 17:36:48 +02:00
committed by GitHub
parent 6cf44ef3c2
commit 7563b8b919
18 changed files with 287 additions and 312 deletions

View File

@ -8,6 +8,7 @@ import { RecordFilterGroup } from '@/object-record/record-filter-group/types/Rec
import { RecordFilterGroupLogicalOperator } from '@/object-record/record-filter-group/types/RecordFilterGroupLogicalOperator';
import { useUpsertRecordFilter } from '@/object-record/record-filter/hooks/useUpsertRecordFilter';
import { RecordFilter } from '@/object-record/record-filter/types/RecordFilter';
import { getDefaultSubFieldNameForCompositeFilterableFieldType } from '@/object-record/record-filter/utils/getDefaultSubFieldNameForCompositeFilterableFieldType';
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';
@ -61,6 +62,9 @@ export const AdvancedFilterAddFilterRuleSelect = ({
defaultFieldMetadataItemForFilter.type,
);
const defaultSubFieldName =
getDefaultSubFieldNameForCompositeFilterableFieldType(filterType);
const newRecordFilter: RecordFilter = {
id: v4(),
fieldMetadataId: defaultFieldMetadataItemForFilter.id,
@ -73,6 +77,7 @@ export const AdvancedFilterAddFilterRuleSelect = ({
recordFilterGroupId: recordFilterGroup.id,
positionInRecordFilterGroup: newPositionInRecordFilterGroup,
label: defaultFieldMetadataItemForFilter.label,
subFieldName: defaultSubFieldName,
};
upsertRecordFilter(newRecordFilter);

View File

@ -1,20 +1,17 @@
import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById';
import { availableFieldMetadataItemsForFilterFamilySelector } from '@/object-metadata/states/availableFieldMetadataItemsForFilterFamilySelector';
import { getFilterTypeFromFieldType } from '@/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions';
import { OBJECT_FILTER_DROPDOWN_ID } from '@/object-record/object-filter-dropdown/constants/ObjectFilterDropdownId';
import { useUpsertRecordFilterGroup } from '@/object-record/record-filter-group/hooks/useUpsertRecordFilterGroup';
import { currentRecordFilterGroupsComponentState } from '@/object-record/record-filter-group/states/currentRecordFilterGroupsComponentState';
import { RecordFilterGroupLogicalOperator } from '@/object-record/record-filter-group/types/RecordFilterGroupLogicalOperator';
import { useUpsertRecordFilter } from '@/object-record/record-filter/hooks/useUpsertRecordFilter';
import { getRecordFilterOperands } from '@/object-record/record-filter/utils/getRecordFilterOperands';
import { useSetRecordFilterUsedInAdvancedFilterDropdownRow } from '@/object-record/advanced-filter/hooks/useSetRecordFilterUsedInAdvancedFilterDropdownRow';
import { RecordFilter } from '@/object-record/record-filter/types/RecordFilter';
import { RecordFilterGroupLogicalOperator } from '@/object-record/record-filter-group/types/RecordFilterGroupLogicalOperator';
import { useCreateEmptyRecordFilterFromFieldMetadataItem } from '@/object-record/record-filter/hooks/useCreateEmptyRecordFilterFromFieldMetadataItem';
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { ADVANCED_FILTER_DROPDOWN_ID } from '@/views/constants/AdvancedFilterDropdownId';
import { useGetCurrentViewOnly } from '@/views/hooks/useGetCurrentViewOnly';
import { ViewFilterGroupLogicalOperator } from '@/views/types/ViewFilterGroupLogicalOperator';
import styled from '@emotion/styled';
import { useLingui } from '@lingui/react/macro';
import { useRecoilValue } from 'recoil';
@ -85,6 +82,9 @@ export const AdvancedFilterButton = () => {
const { setRecordFilterUsedInAdvancedFilterDropdownRow } =
useSetRecordFilterUsedInAdvancedFilterDropdownRow();
const { createEmptyRecordFilterFromFieldMetadataItem } =
useCreateEmptyRecordFilterFromFieldMetadataItem();
const handleClick = () => {
if (!isDefined(currentView)) {
throw new Error('Missing current view id');
@ -96,13 +96,10 @@ export const AdvancedFilterButton = () => {
const newRecordFilterGroup = {
id: v4(),
viewId: currentView.id,
logicalOperator: ViewFilterGroupLogicalOperator.AND,
logicalOperator: RecordFilterGroupLogicalOperator.AND,
};
upsertRecordFilterGroup({
id: newRecordFilterGroup.id,
logicalOperator: RecordFilterGroupLogicalOperator.AND,
});
upsertRecordFilterGroup(newRecordFilterGroup);
const defaultFieldMetadataItem =
availableFieldMetadataItemsForFilter.find(
@ -115,25 +112,11 @@ export const AdvancedFilterButton = () => {
throw new Error('Missing default filter definition');
}
const filterType = getFilterTypeFromFieldType(
defaultFieldMetadataItem.type,
const { newRecordFilter } = createEmptyRecordFilterFromFieldMetadataItem(
defaultFieldMetadataItem,
);
const firstOperand = getRecordFilterOperands({
filterType,
})[0];
const newRecordFilter: RecordFilter = {
id: v4(),
fieldMetadataId: defaultFieldMetadataItem.id,
operand: firstOperand,
value: '',
displayValue: '',
recordFilterGroupId: newRecordFilterGroup.id,
type: getFilterTypeFromFieldType(defaultFieldMetadataItem.type),
label: defaultFieldMetadataItem.label,
positionInRecordFilterGroup: 1,
};
newRecordFilter.recordFilterGroupId = newRecordFilterGroup.id;
upsertRecordFilter(newRecordFilter);

View File

@ -64,16 +64,9 @@ describe('getOperandsForFilterType', () => {
['DATE', [...dateOperands, ...emptyOperands]],
['DATE_TIME', [...dateOperands, ...emptyOperands]],
['RELATION', [...relationOperand, ...emptyOperands]],
[undefined, []],
[null, []],
['UNKNOWN_TYPE', []],
] satisfies (
| [
FieldType | null | undefined | 'UNKNOWN_TYPE',
RecordFilterOperand[],
CompositeFieldSubFieldName,
]
| [FieldType | null | undefined | 'UNKNOWN_TYPE', RecordFilterOperand[]]
| [FieldType, RecordFilterOperand[], CompositeFieldSubFieldName]
| [FieldType, RecordFilterOperand[]]
)[];
testCases.forEach(([filterType, expectedOperands, subFieldName]) => {

View File

@ -0,0 +1,40 @@
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { getFilterTypeFromFieldType } from '@/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions';
import { RecordFilter } from '@/object-record/record-filter/types/RecordFilter';
import { getDefaultSubFieldNameForCompositeFilterableFieldType } from '@/object-record/record-filter/utils/getDefaultSubFieldNameForCompositeFilterableFieldType';
import { getRecordFilterOperands } from '@/object-record/record-filter/utils/getRecordFilterOperands';
import { v4 } from 'uuid';
export const useCreateEmptyRecordFilterFromFieldMetadataItem = () => {
const createEmptyRecordFilterFromFieldMetadataItem = (
fieldMetadataItem: FieldMetadataItem,
) => {
const filterType = getFilterTypeFromFieldType(fieldMetadataItem.type);
const availableOperandsForFilter = getRecordFilterOperands({
filterType,
});
const defaultOperand = availableOperandsForFilter[0];
const defaultSubFieldName =
getDefaultSubFieldNameForCompositeFilterableFieldType(filterType);
const newRecordFilter: RecordFilter = {
id: v4(),
fieldMetadataId: fieldMetadataItem.id,
operand: defaultOperand,
displayValue: '',
label: fieldMetadataItem.label,
type: filterType,
value: '',
subFieldName: defaultSubFieldName,
};
return { newRecordFilter };
};
return {
createEmptyRecordFilterFromFieldMetadataItem,
};
};

View File

@ -4,6 +4,7 @@ import { FilterableFieldType } from '@/object-record/record-filter/types/Filtera
import { CompositeFieldSubFieldName } from '@/settings/data-model/types/CompositeFieldSubFieldName';
import { ViewFilterOperand as RecordFilterOperand } from '@/views/types/ViewFilterOperand';
import { FieldMetadataType } from 'twenty-shared/types';
import { assertUnreachable } from 'twenty-shared/utils';
export type GetRecordFilterOperandsParams = {
filterType: FilterableFieldType;
@ -208,6 +209,6 @@ export const getRecordFilterOperands = ({
case 'BOOLEAN':
return FILTER_OPERANDS_MAP.BOOLEAN;
default:
return [];
assertUnreachable(filterType, `Unknown filter type ${filterType}`);
}
};

View File

@ -3,7 +3,6 @@ import { useEffect } from 'react';
import { useColumnDefinitionsFromFieldMetadata } from '@/object-metadata/hooks/useColumnDefinitionsFromFieldMetadata';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
import { useHandleToggleColumnFilter } from '@/object-record/record-index/hooks/useHandleToggleColumnFilter';
import { useHandleToggleColumnSort } from '@/object-record/record-index/hooks/useHandleToggleColumnSort';
import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
import { viewFieldAggregateOperationState } from '@/object-record/record-table/record-table-footer/states/viewFieldAggregateOperationState';
@ -16,13 +15,7 @@ import { isDefined } from 'twenty-shared/utils';
export const RecordIndexTableContainerEffect = () => {
const { recordIndexId, objectNameSingular } = useRecordIndexContextOrThrow();
const viewBarId = recordIndexId;
const {
setAvailableTableColumns,
setOnToggleColumnFilter,
setOnToggleColumnSort,
} = useRecordTable({
const { setAvailableTableColumns, setOnToggleColumnSort } = useRecordTable({
recordTableId: recordIndexId,
});
@ -37,24 +30,12 @@ export const RecordIndexTableContainerEffect = () => {
setAvailableTableColumns(columnDefinitions);
}, [columnDefinitions, setAvailableTableColumns]);
const handleToggleColumnFilter = useHandleToggleColumnFilter({
objectNameSingular,
viewBarId,
});
const handleToggleColumnSort = useHandleToggleColumnSort({
objectNameSingular,
});
const { currentView } = useGetCurrentViewOnly();
useEffect(() => {
setOnToggleColumnFilter(
() => (fieldMetadataId: string) =>
handleToggleColumnFilter(fieldMetadataId),
);
}, [setOnToggleColumnFilter, handleToggleColumnFilter]);
useEffect(() => {
setOnToggleColumnSort(
() => (fieldMetadataId: string) =>

View File

@ -1,148 +0,0 @@
import { useCallback } from 'react';
import { v4 } from 'uuid';
import { useColumnDefinitionsFromFieldMetadata } from '@/object-metadata/hooks/useColumnDefinitionsFromFieldMetadata';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { availableFieldMetadataItemsForFilterFamilySelector } from '@/object-metadata/states/availableFieldMetadataItemsForFilterFamilySelector';
import { getFilterTypeFromFieldType } from '@/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions';
import { useSelectFilterUsedInDropdown } from '@/object-record/object-filter-dropdown/hooks/useSelectFilterUsedInDropdown';
import { useUpsertRecordFilter } from '@/object-record/record-filter/hooks/useUpsertRecordFilter';
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
import { RecordFilter } from '@/object-record/record-filter/types/RecordFilter';
import { getDefaultSubFieldNameForCompositeFilterableFieldType } from '@/object-record/record-filter/utils/getDefaultSubFieldNameForCompositeFilterableFieldType';
import { getRecordFilterOperands } from '@/object-record/record-filter/utils/getRecordFilterOperands';
import { useSetActiveDropdownFocusIdAndMemorizePrevious } from '@/ui/layout/dropdown/hooks/useSetFocusedDropdownIdAndMemorizePrevious';
import { isDropdownOpenComponentState } from '@/ui/layout/dropdown/states/isDropdownOpenComponentState';
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { extractComponentState } from '@/ui/utilities/state/component-state/utils/extractComponentState';
import { useRecoilCallback, useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
type UseHandleToggleColumnFilterProps = {
objectNameSingular: string;
viewBarId: string;
};
export const useHandleToggleColumnFilter = ({
objectNameSingular,
viewBarId,
}: UseHandleToggleColumnFilterProps) => {
const { objectMetadataItem } = useObjectMetadataItem({
objectNameSingular,
});
const { columnDefinitions } =
useColumnDefinitionsFromFieldMetadata(objectMetadataItem);
const { upsertRecordFilter } = useUpsertRecordFilter();
const { setActiveDropdownFocusIdAndMemorizePrevious } =
useSetActiveDropdownFocusIdAndMemorizePrevious();
const { setHotkeyScopeAndMemorizePreviousScope } = usePreviousHotkeyScope();
const openDropdown = useRecoilCallback(
({ set }) => {
return (dropdownId: string) => {
const dropdownOpenState = extractComponentState(
isDropdownOpenComponentState,
dropdownId,
);
setActiveDropdownFocusIdAndMemorizePrevious(dropdownId);
setHotkeyScopeAndMemorizePreviousScope(dropdownId);
set(dropdownOpenState, true);
};
},
[
setActiveDropdownFocusIdAndMemorizePrevious,
setHotkeyScopeAndMemorizePreviousScope,
],
);
const availableFieldMetadataItemsForFilter = useRecoilValue(
availableFieldMetadataItemsForFilterFamilySelector({
objectMetadataItemId: objectMetadataItem.id,
}),
);
const { selectFilterUsedInDropdown } =
useSelectFilterUsedInDropdown(viewBarId);
const currentRecordFilters = useRecoilComponentValueV2(
currentRecordFiltersComponentState,
);
const handleToggleColumnFilter = useCallback(
async (fieldMetadataId: string) => {
const correspondingColumnDefinition = columnDefinitions.find(
(columnDefinition) =>
columnDefinition.fieldMetadataId === fieldMetadataId,
);
if (!isDefined(correspondingColumnDefinition)) return;
const newFilterId = v4();
const existingRecordFilter = currentRecordFilters.find(
(recordFilter) => recordFilter.fieldMetadataId === fieldMetadataId,
);
if (!isDefined(existingRecordFilter)) {
const fieldMetadataItem = availableFieldMetadataItemsForFilter.find(
(fieldMetadataItemToFind) =>
fieldMetadataItemToFind.id === fieldMetadataId,
);
if (!isDefined(fieldMetadataItem)) {
throw new Error('Field metadata item not found');
}
const filterType = getFilterTypeFromFieldType(fieldMetadataItem.type);
const defaultSubFieldName =
getDefaultSubFieldNameForCompositeFilterableFieldType(
fieldMetadataItem.type,
);
const availableOperandsForFilter = getRecordFilterOperands({
filterType,
subFieldName: defaultSubFieldName,
});
const defaultOperand = availableOperandsForFilter[0];
const newFilter: RecordFilter = {
id: newFilterId,
fieldMetadataId,
operand: defaultOperand,
displayValue: '',
label: fieldMetadataItem.label,
type: filterType,
value: '',
subFieldName: defaultSubFieldName,
};
upsertRecordFilter(newFilter);
selectFilterUsedInDropdown({ fieldMetadataItemId: fieldMetadataId });
}
openDropdown(existingRecordFilter?.id ?? newFilterId);
},
[
openDropdown,
columnDefinitions,
selectFilterUsedInDropdown,
currentRecordFilters,
availableFieldMetadataItemsForFilter,
upsertRecordFilter,
],
);
return handleToggleColumnFilter;
};

View File

@ -19,7 +19,7 @@ import { RecordTableComponentInstanceContext } from '@/object-record/record-tabl
import { isRecordTableInitialLoadingComponentState } from '@/object-record/record-table/states/isRecordTableInitialLoadingComponentState';
import { onColumnsChangeComponentState } from '@/object-record/record-table/states/onColumnsChangeComponentState';
import { onEntityCountChangeComponentState } from '@/object-record/record-table/states/onEntityCountChangeComponentState';
import { onToggleColumnFilterComponentState } from '@/object-record/record-table/states/onToggleColumnFilterComponentState';
import { onToggleColumnSortComponentState } from '@/object-record/record-table/states/onToggleColumnSortComponentState';
import { tableLastRowVisibleComponentState } from '@/object-record/record-table/states/tableLastRowVisibleComponentState';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
@ -73,10 +73,6 @@ export const useRecordTable = (props?: useRecordTableProps) => {
recordTableId,
);
const setOnToggleColumnFilter = useSetRecoilComponentStateV2(
onToggleColumnFilterComponentState,
recordTableId,
);
const setOnToggleColumnSort = useSetRecoilComponentStateV2(
onToggleColumnSortComponentState,
recordTableId,
@ -226,7 +222,6 @@ export const useRecordTable = (props?: useRecordTableProps) => {
setRecordTableLastRowVisible,
setFocusPosition,
setHasUserSelectedAllRows,
setOnToggleColumnFilter,
setOnToggleColumnSort,
};
};

View File

@ -3,7 +3,7 @@ import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/Drop
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { onToggleColumnFilterComponentState } from '@/object-record/record-table/states/onToggleColumnFilterComponentState';
import { useOpenRecordFilterChipFromTableHeader } from '@/object-record/record-table/record-table-header/hooks/useOpenRecordFilterChipFromTableHeader';
import { onToggleColumnSortComponentState } from '@/object-record/record-table/states/onToggleColumnSortComponentState';
import { visibleTableColumnsComponentSelector } from '@/object-record/record-table/states/selectors/visibleTableColumnsComponentSelector';
import { useToggleScrollWrapper } from '@/ui/utilities/scroll/hooks/useToggleScrollWrapper';
@ -76,9 +76,6 @@ export const RecordTableColumnHeadDropdownMenu = ({
handleColumnVisibilityChange(column);
};
const onToggleColumnFilter = useRecoilComponentValueV2(
onToggleColumnFilterComponentState,
);
const onToggleColumnSort = useRecoilComponentValueV2(
onToggleColumnSortComponentState,
);
@ -89,10 +86,13 @@ export const RecordTableColumnHeadDropdownMenu = ({
onToggleColumnSort?.(column.fieldMetadataId);
};
const { openRecordFilterChipFromTableHeader } =
useOpenRecordFilterChipFromTableHeader();
const handleFilterClick = () => {
closeDropdownAndToggleScroll();
onToggleColumnFilter?.(column.fieldMetadataId);
openRecordFilterChipFromTableHeader(column.fieldMetadataId);
};
const isSortable = column.isSortable === true;

View File

@ -0,0 +1,70 @@
import { useSelectFilterUsedInDropdown } from '@/object-record/object-filter-dropdown/hooks/useSelectFilterUsedInDropdown';
import { useCreateEmptyRecordFilterFromFieldMetadataItem } from '@/object-record/record-filter/hooks/useCreateEmptyRecordFilterFromFieldMetadataItem';
import { useFilterableFieldMetadataItemsInRecordIndexContext } from '@/object-record/record-filter/hooks/useFilterableFieldMetadataItemsInRecordIndexContext';
import { useUpsertRecordFilter } from '@/object-record/record-filter/hooks/useUpsertRecordFilter';
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
import { useOpenDropdownFromOutside } from '@/ui/layout/dropdown/hooks/useOpenDropdownFromOutside';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useSetEditableFilterChipDropdownStates } from '@/views/hooks/useSetEditableFilterChipDropdownStates';
import { isDefined } from 'twenty-shared/utils';
export const useOpenRecordFilterChipFromTableHeader = () => {
const { recordIndexId } = useRecordIndexContextOrThrow();
const { filterableFieldMetadataItems } =
useFilterableFieldMetadataItemsInRecordIndexContext();
const { selectFilterUsedInDropdown } =
useSelectFilterUsedInDropdown(recordIndexId);
const currentRecordFilters = useRecoilComponentValueV2(
currentRecordFiltersComponentState,
);
const { createEmptyRecordFilterFromFieldMetadataItem } =
useCreateEmptyRecordFilterFromFieldMetadataItem();
const { upsertRecordFilter } = useUpsertRecordFilter();
const { openDropdownFromOutside } = useOpenDropdownFromOutside();
const { setEditableFilterChipDropdownStates } =
useSetEditableFilterChipDropdownStates();
const openRecordFilterChipFromTableHeader = (fieldMetadataItemId: string) => {
const correspondingFieldMetadataItem = filterableFieldMetadataItems.find(
(fieldMetadataItemToFind) =>
fieldMetadataItemToFind.id === fieldMetadataItemId,
);
if (!isDefined(correspondingFieldMetadataItem)) {
throw new Error(
`Cannot find field metadata item with id : ${fieldMetadataItemId}`,
);
}
const existingRecordFilter = currentRecordFilters.find(
(recordFilter) => recordFilter.fieldMetadataId === fieldMetadataItemId,
);
if (isDefined(existingRecordFilter)) {
setEditableFilterChipDropdownStates(existingRecordFilter);
openDropdownFromOutside(existingRecordFilter.id);
return;
}
const { newRecordFilter } = createEmptyRecordFilterFromFieldMetadataItem(
correspondingFieldMetadataItem,
);
upsertRecordFilter(newRecordFilter);
selectFilterUsedInDropdown({ fieldMetadataItemId });
setEditableFilterChipDropdownStates(newRecordFilter);
openDropdownFromOutside(newRecordFilter.id);
};
return { openRecordFilterChipFromTableHeader };
};

View File

@ -1,10 +0,0 @@
import { RecordTableComponentInstanceContext } from '@/object-record/record-table/states/context/RecordTableComponentInstanceContext';
import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2';
export const onToggleColumnFilterComponentState = createComponentStateV2<
((fieldMetadataId: string) => void) | undefined
>({
key: 'onToggleColumnFilterComponentState',
defaultValue: undefined,
componentInstanceContext: RecordTableComponentInstanceContext,
});

View File

@ -0,0 +1,34 @@
import { useSetActiveDropdownFocusIdAndMemorizePrevious } from '@/ui/layout/dropdown/hooks/useSetFocusedDropdownIdAndMemorizePrevious';
import { isDropdownOpenComponentState } from '@/ui/layout/dropdown/states/isDropdownOpenComponentState';
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
import { extractComponentState } from '@/ui/utilities/state/component-state/utils/extractComponentState';
import { useRecoilCallback } from 'recoil';
export const useOpenDropdownFromOutside = () => {
const { setActiveDropdownFocusIdAndMemorizePrevious } =
useSetActiveDropdownFocusIdAndMemorizePrevious();
const { setHotkeyScopeAndMemorizePreviousScope } = usePreviousHotkeyScope();
const openDropdownFromOutside = useRecoilCallback(
({ set }) => {
return (dropdownId: string) => {
const dropdownOpenState = extractComponentState(
isDropdownOpenComponentState,
dropdownId,
);
setActiveDropdownFocusIdAndMemorizePrevious(dropdownId);
setHotkeyScopeAndMemorizePreviousScope(dropdownId);
set(dropdownOpenState, true);
};
},
[
setActiveDropdownFocusIdAndMemorizePrevious,
setHotkeyScopeAndMemorizePreviousScope,
],
);
return { openDropdownFromOutside };
};

View File

@ -13,11 +13,13 @@ import { useIcons } from 'twenty-ui/display';
type EditableFilterChipProps = {
recordFilter: RecordFilter;
onRemove: () => void;
onClick?: () => void;
};
export const EditableFilterChip = ({
recordFilter,
onRemove,
onClick,
}: EditableFilterChipProps) => {
const { getIcon } = useIcons();
@ -58,6 +60,7 @@ export const EditableFilterChip = ({
labelValue={recordFilter.displayValue}
Icon={FieldMetadataItemIcon}
onRemove={onRemove}
onClick={onClick}
/>
);
};

View File

@ -9,7 +9,7 @@ import { EditableFilterChip } from '@/views/components/EditableFilterChip';
import { ObjectFilterOperandSelectAndInput } from '@/object-record/object-filter-dropdown/components/ObjectFilterOperandSelectAndInput';
import { useRemoveRecordFilter } from '@/object-record/record-filter/hooks/useRemoveRecordFilter';
import { isRecordFilterConsideredEmpty } from '@/object-record/record-filter/utils/isRecordFilterConsideredEmpty';
import { EditableFilterDropdownButtonEffect } from '@/views/components/EditableFilterDropdownButtonEffect';
import { useSetEditableFilterChipDropdownStates } from '@/views/hooks/useSetEditableFilterChipDropdownStates';
type EditableFilterDropdownButtonProps = {
recordFilter: RecordFilter;
@ -38,15 +38,22 @@ export const EditableFilterDropdownButton = ({
}
}, [recordFilter, removeRecordFilter]);
const { setEditableFilterChipDropdownStates } =
useSetEditableFilterChipDropdownStates();
const handleFilterChipClick = () => {
setEditableFilterChipDropdownStates(recordFilter);
};
return (
<>
<EditableFilterDropdownButtonEffect recordFilter={recordFilter} />
<Dropdown
dropdownId={recordFilter.id}
clickableComponent={
<EditableFilterChip
recordFilter={recordFilter}
onRemove={handleRemove}
onClick={handleFilterChipClick}
/>
}
dropdownComponents={

View File

@ -1,81 +0,0 @@
import { useEffect } from 'react';
import { fieldMetadataItemIdUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/fieldMetadataItemIdUsedInDropdownComponentState';
import { objectFilterDropdownSelectedRecordIdsComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownSelectedRecordIdsComponentState';
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 { useFilterableFieldMetadataItemsInRecordIndexContext } from '@/object-record/record-filter/hooks/useFilterableFieldMetadataItemsInRecordIndexContext';
import { RecordFilter } from '@/object-record/record-filter/types/RecordFilter';
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
import { isDefined } from 'twenty-shared/utils';
type EditableFilterDropdownButtonEffectProps = {
recordFilter: RecordFilter;
};
export const EditableFilterDropdownButtonEffect = ({
recordFilter,
}: EditableFilterDropdownButtonEffectProps) => {
const setFieldMetadataItemIdUsedInDropdown = useSetRecoilComponentStateV2(
fieldMetadataItemIdUsedInDropdownComponentState,
);
const setSelectedOperandInDropdown = useSetRecoilComponentStateV2(
selectedOperandInDropdownComponentState,
recordFilter.id,
);
const setSubFieldNameUsedInDropdown = useSetRecoilComponentStateV2(
subFieldNameUsedInDropdownComponentState,
recordFilter.id,
);
const setSelectedFilter = useSetRecoilComponentStateV2(
selectedFilterComponentState,
recordFilter.id,
);
const setObjectFilterDropdownSelectedRecordIds = useSetRecoilComponentStateV2(
objectFilterDropdownSelectedRecordIdsComponentState,
recordFilter.id,
);
const { filterableFieldMetadataItems } =
useFilterableFieldMetadataItemsInRecordIndexContext();
useEffect(() => {
const fieldMetadataItem = filterableFieldMetadataItems.find(
(fieldMetadataItem) =>
fieldMetadataItem.id === recordFilter.fieldMetadataId,
);
if (!isDefined(fieldMetadataItem)) {
return;
}
setFieldMetadataItemIdUsedInDropdown(fieldMetadataItem.id);
setSelectedOperandInDropdown(recordFilter.operand);
setSelectedFilter(recordFilter);
setSubFieldNameUsedInDropdown(recordFilter.subFieldName);
try {
const selectedOptions = JSON.parse(recordFilter.value);
setObjectFilterDropdownSelectedRecordIds(selectedOptions);
} catch {
setObjectFilterDropdownSelectedRecordIds([]);
}
}, [
filterableFieldMetadataItems,
setFieldMetadataItemIdUsedInDropdown,
recordFilter,
setSelectedOperandInDropdown,
setSelectedFilter,
setSubFieldNameUsedInDropdown,
setObjectFilterDropdownSelectedRecordIds,
]);
return null;
};

View File

@ -10,7 +10,6 @@ import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/
import { AdvancedFilterDropdownButton } from '@/views/components/AdvancedFilterDropdownButton';
import { EditableFilterDropdownButton } from '@/views/components/EditableFilterDropdownButton';
import { EditableSortChip } from '@/views/components/EditableSortChip';
import { ViewBarFilterEffect } from '@/views/components/ViewBarFilterEffect';
import { useViewFromQueryParams } from '@/views/hooks/internal/useViewFromQueryParams';
import { useCheckIsSoftDeleteFilter } from '@/object-record/record-filter/hooks/useCheckIsSoftDeleteFilter';
@ -232,7 +231,6 @@ export const ViewBarDetails = ({
value={{ instanceId: recordFilter.id }}
>
<DropdownScope dropdownScopeId={recordFilter.id}>
<ViewBarFilterEffect filterDropdownId={recordFilter.id} />
<EditableFilterDropdownButton
recordFilter={recordFilter}
hotkeyScope={{

View File

@ -0,0 +1,104 @@
import { fieldMetadataItemIdUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/fieldMetadataItemIdUsedInDropdownComponentState';
import { objectFilterDropdownSelectedOptionValuesComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownSelectedOptionValuesComponentState';
import { objectFilterDropdownSelectedRecordIdsComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownSelectedRecordIdsComponentState';
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 { useFilterableFieldMetadataItemsInRecordIndexContext } from '@/object-record/record-filter/hooks/useFilterableFieldMetadataItemsInRecordIndexContext';
import { RecordFilter } from '@/object-record/record-filter/types/RecordFilter';
import { jsonRelationFilterValueSchema } from '@/views/view-filter-value/validation-schemas/jsonRelationFilterValueSchema';
import { simpleRelationFilterValueSchema } from '@/views/view-filter-value/validation-schemas/simpleRelationFilterValueSchema';
import { useRecoilCallback } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
export const useSetEditableFilterChipDropdownStates = () => {
const { filterableFieldMetadataItems } =
useFilterableFieldMetadataItemsInRecordIndexContext();
const setEditableFilterChipDropdownStates = useRecoilCallback(
({ set }) =>
(recordFilter: RecordFilter) => {
const fieldMetadataItem = filterableFieldMetadataItems.find(
(fieldMetadataItem) =>
fieldMetadataItem.id === recordFilter.fieldMetadataId,
);
if (!isDefined(fieldMetadataItem)) {
return;
}
set(
fieldMetadataItemIdUsedInDropdownComponentState.atomFamily({
instanceId: recordFilter.id,
}),
fieldMetadataItem.id,
);
set(
selectedOperandInDropdownComponentState.atomFamily({
instanceId: recordFilter.id,
}),
recordFilter.operand,
);
set(
selectedFilterComponentState.atomFamily({
instanceId: recordFilter.id,
}),
recordFilter,
);
set(
subFieldNameUsedInDropdownComponentState.atomFamily({
instanceId: recordFilter.id,
}),
recordFilter.subFieldName,
);
if (recordFilter.type === 'RELATION') {
const { selectedRecordIds } = jsonRelationFilterValueSchema
.catch({
isCurrentWorkspaceMemberSelected: false,
selectedRecordIds: simpleRelationFilterValueSchema.parse(
recordFilter.value,
),
})
.parse(recordFilter.value);
set(
objectFilterDropdownSelectedRecordIdsComponentState.atomFamily({
instanceId: recordFilter.id,
}),
selectedRecordIds,
);
} else if (['SELECT', 'MULTI_SELECT'].includes(recordFilter.type)) {
try {
const selectedOptions = JSON.parse(recordFilter.value);
set(
objectFilterDropdownSelectedOptionValuesComponentState.atomFamily(
{
instanceId: recordFilter.id,
},
),
selectedOptions,
);
} catch {
set(
objectFilterDropdownSelectedOptionValuesComponentState.atomFamily(
{
instanceId: recordFilter.id,
},
),
[],
);
}
}
},
[filterableFieldMetadataItems],
);
return {
setEditableFilterChipDropdownStates,
};
};

View File

@ -9,7 +9,7 @@ export const areViewFilterGroupsEqual = (
'positionInViewFilterGroup',
'logicalOperator',
'parentViewFilterGroupId',
'viewId',
'id',
];
return propertiesToCompare.every((property) =>