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
@@ -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);
@@ -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);
@@ -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]) => {
@@ -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,
};
};
@@ -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}`);
}
};
@@ -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) =>
@@ -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;
};
@@ -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,
};
};
@@ -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;
@@ -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 };
};
@@ -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,
});
@@ -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 };
};
@@ -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}
/>
);
};
@@ -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={
@@ -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;
};
@@ -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={{
@@ -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,
};
};
@@ -9,7 +9,7 @@ export const areViewFilterGroupsEqual = (
'positionInViewFilterGroup',
'logicalOperator',
'parentViewFilterGroupId',
'viewId',
'id',
];
return propertiesToCompare.every((property) =>