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 9bdedea1da1..b1e75599e4e 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 4fde4314ed0..aeb5980992e 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 fc7c63e35eb..eaec15bd8cc 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 00000000000..f3bf9d98dd7 --- /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 f7186e1e402..236ab6f357b 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 2a9a80d38d1..b5d3d902e47 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 1c41a3ae054..00000000000 --- 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 941b47c0abd..5d307d2793f 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 083467c7990..cf134538fb5 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 00000000000..9aef27c505c --- /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 c0e0a972bf6..00000000000 --- 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 00000000000..8ea2bae6d7e --- /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 a4a6606eb9f..1f8116448e4 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 e33d38c54c5..b7225195e85 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 272f2628ed4..00000000000 --- 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 9cf6bb322f1..449601e3fb0 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 9f47fac168b..752370d76b8 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) =>