From 7563b8b91974e1b30d1f2eaee5f372bbb39a08cb Mon Sep 17 00:00:00 2001 From: Lucas Bordeau Date: Mon, 28 Apr 2025 17:36:48 +0200 Subject: [PATCH] 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) --- .../AdvancedFilterAddFilterRuleSelect.tsx | 5 + .../components/AdvancedFilterButton.tsx | 37 ++--- .../getOperandsForFilterType.test.ts | 11 +- ...eEmptyRecordFilterFromFieldMetadataItem.ts | 40 +++++ .../utils/getRecordFilterOperands.ts | 3 +- .../RecordIndexTableContainerEffect.tsx | 21 +-- .../hooks/useHandleToggleColumnFilter.ts | 148 ------------------ .../record-table/hooks/useRecordTable.ts | 7 +- .../RecordTableColumnHeadDropdownMenu.tsx | 10 +- .../useOpenRecordFilterChipFromTableHeader.ts | 70 +++++++++ .../onToggleColumnFilterComponentState.ts | 10 -- .../hooks/useOpenDropdownFromOutside.ts | 34 ++++ .../views/components/EditableFilterChip.tsx | 3 + .../EditableFilterDropdownButton.tsx | 11 +- .../EditableFilterDropdownButtonEffect.tsx | 81 ---------- .../views/components/ViewBarDetails.tsx | 2 - .../useSetEditableFilterChipDropdownStates.ts | 104 ++++++++++++ .../views/utils/areViewFilterGroupsEqual.ts | 2 +- 18 files changed, 287 insertions(+), 312 deletions(-) create mode 100644 packages/twenty-front/src/modules/object-record/record-filter/hooks/useCreateEmptyRecordFilterFromFieldMetadataItem.ts delete mode 100644 packages/twenty-front/src/modules/object-record/record-index/hooks/useHandleToggleColumnFilter.ts create mode 100644 packages/twenty-front/src/modules/object-record/record-table/record-table-header/hooks/useOpenRecordFilterChipFromTableHeader.ts delete mode 100644 packages/twenty-front/src/modules/object-record/record-table/states/onToggleColumnFilterComponentState.ts create mode 100644 packages/twenty-front/src/modules/ui/layout/dropdown/hooks/useOpenDropdownFromOutside.ts delete mode 100644 packages/twenty-front/src/modules/views/components/EditableFilterDropdownButtonEffect.tsx create mode 100644 packages/twenty-front/src/modules/views/hooks/useSetEditableFilterChipDropdownStates.ts diff --git a/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterAddFilterRuleSelect.tsx b/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterAddFilterRuleSelect.tsx index 9bdedea1d..b1e75599e 100644 --- a/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterAddFilterRuleSelect.tsx +++ b/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterAddFilterRuleSelect.tsx @@ -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); diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/AdvancedFilterButton.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/AdvancedFilterButton.tsx index 4fde4314e..aeb598099 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/AdvancedFilterButton.tsx +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/AdvancedFilterButton.tsx @@ -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); diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/__tests__/getOperandsForFilterType.test.ts b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/__tests__/getOperandsForFilterType.test.ts index fc7c63e35..eaec15bd8 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/__tests__/getOperandsForFilterType.test.ts +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/__tests__/getOperandsForFilterType.test.ts @@ -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]) => { diff --git a/packages/twenty-front/src/modules/object-record/record-filter/hooks/useCreateEmptyRecordFilterFromFieldMetadataItem.ts b/packages/twenty-front/src/modules/object-record/record-filter/hooks/useCreateEmptyRecordFilterFromFieldMetadataItem.ts new file mode 100644 index 000000000..f3bf9d98d --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-filter/hooks/useCreateEmptyRecordFilterFromFieldMetadataItem.ts @@ -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, + }; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-filter/utils/getRecordFilterOperands.ts b/packages/twenty-front/src/modules/object-record/record-filter/utils/getRecordFilterOperands.ts index f7186e1e4..236ab6f35 100644 --- a/packages/twenty-front/src/modules/object-record/record-filter/utils/getRecordFilterOperands.ts +++ b/packages/twenty-front/src/modules/object-record/record-filter/utils/getRecordFilterOperands.ts @@ -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}`); } }; diff --git a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexTableContainerEffect.tsx b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexTableContainerEffect.tsx index 2a9a80d38..b5d3d902e 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexTableContainerEffect.tsx +++ b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexTableContainerEffect.tsx @@ -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) => diff --git a/packages/twenty-front/src/modules/object-record/record-index/hooks/useHandleToggleColumnFilter.ts b/packages/twenty-front/src/modules/object-record/record-index/hooks/useHandleToggleColumnFilter.ts deleted file mode 100644 index 1c41a3ae0..000000000 --- a/packages/twenty-front/src/modules/object-record/record-index/hooks/useHandleToggleColumnFilter.ts +++ /dev/null @@ -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; -}; diff --git a/packages/twenty-front/src/modules/object-record/record-table/hooks/useRecordTable.ts b/packages/twenty-front/src/modules/object-record/record-table/hooks/useRecordTable.ts index 941b47c0a..5d307d279 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/hooks/useRecordTable.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/hooks/useRecordTable.ts @@ -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, }; }; diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableColumnHeadDropdownMenu.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableColumnHeadDropdownMenu.tsx index 083467c79..cf134538f 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableColumnHeadDropdownMenu.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableColumnHeadDropdownMenu.tsx @@ -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; diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/hooks/useOpenRecordFilterChipFromTableHeader.ts b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/hooks/useOpenRecordFilterChipFromTableHeader.ts new file mode 100644 index 000000000..9aef27c50 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/hooks/useOpenRecordFilterChipFromTableHeader.ts @@ -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 }; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-table/states/onToggleColumnFilterComponentState.ts b/packages/twenty-front/src/modules/object-record/record-table/states/onToggleColumnFilterComponentState.ts deleted file mode 100644 index c0e0a972b..000000000 --- a/packages/twenty-front/src/modules/object-record/record-table/states/onToggleColumnFilterComponentState.ts +++ /dev/null @@ -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, -}); diff --git a/packages/twenty-front/src/modules/ui/layout/dropdown/hooks/useOpenDropdownFromOutside.ts b/packages/twenty-front/src/modules/ui/layout/dropdown/hooks/useOpenDropdownFromOutside.ts new file mode 100644 index 000000000..8ea2bae6d --- /dev/null +++ b/packages/twenty-front/src/modules/ui/layout/dropdown/hooks/useOpenDropdownFromOutside.ts @@ -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 }; +}; diff --git a/packages/twenty-front/src/modules/views/components/EditableFilterChip.tsx b/packages/twenty-front/src/modules/views/components/EditableFilterChip.tsx index a4a6606eb..1f8116448 100644 --- a/packages/twenty-front/src/modules/views/components/EditableFilterChip.tsx +++ b/packages/twenty-front/src/modules/views/components/EditableFilterChip.tsx @@ -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} /> ); }; diff --git a/packages/twenty-front/src/modules/views/components/EditableFilterDropdownButton.tsx b/packages/twenty-front/src/modules/views/components/EditableFilterDropdownButton.tsx index e33d38c54..b7225195e 100644 --- a/packages/twenty-front/src/modules/views/components/EditableFilterDropdownButton.tsx +++ b/packages/twenty-front/src/modules/views/components/EditableFilterDropdownButton.tsx @@ -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 ( <> - } dropdownComponents={ diff --git a/packages/twenty-front/src/modules/views/components/EditableFilterDropdownButtonEffect.tsx b/packages/twenty-front/src/modules/views/components/EditableFilterDropdownButtonEffect.tsx deleted file mode 100644 index 272f2628e..000000000 --- a/packages/twenty-front/src/modules/views/components/EditableFilterDropdownButtonEffect.tsx +++ /dev/null @@ -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; -}; diff --git a/packages/twenty-front/src/modules/views/components/ViewBarDetails.tsx b/packages/twenty-front/src/modules/views/components/ViewBarDetails.tsx index 9cf6bb322..449601e3f 100644 --- a/packages/twenty-front/src/modules/views/components/ViewBarDetails.tsx +++ b/packages/twenty-front/src/modules/views/components/ViewBarDetails.tsx @@ -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 }} > - { + 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, + }; +}; diff --git a/packages/twenty-front/src/modules/views/utils/areViewFilterGroupsEqual.ts b/packages/twenty-front/src/modules/views/utils/areViewFilterGroupsEqual.ts index 9f47fac16..752370d76 100644 --- a/packages/twenty-front/src/modules/views/utils/areViewFilterGroupsEqual.ts +++ b/packages/twenty-front/src/modules/views/utils/areViewFilterGroupsEqual.ts @@ -9,7 +9,7 @@ export const areViewFilterGroupsEqual = ( 'positionInViewFilterGroup', 'logicalOperator', 'parentViewFilterGroupId', - 'viewId', + 'id', ]; return propertiesToCompare.every((property) =>