From bcf5268f7f197088f1e4b76ab54e1c75181f039c Mon Sep 17 00:00:00 2001 From: Kanav Arora Date: Thu, 4 Apr 2024 04:13:44 +0530 Subject: [PATCH] 3886 - Shortcut Sort/Filter (#3901) Closes #3886 --------- Co-authored-by: Lucas Bordeau --- .../useColumnDefinitionsFromFieldMetadata.ts | 38 +++++++-- ...atFieldMetadataItemsAsFilterDefinitions.ts | 4 +- .../components/ObjectSortDropdownButton.tsx | 57 +++---------- .../constants/ObjectSortDropdownId.ts | 6 +- .../hooks/useObjectSortDropdown.ts | 77 +++++++++++++++++ .../isSortDirectionMenuUnfoldedState.ts | 8 ++ .../states/selectedSortDirectionState.ts | 10 +++ .../RecordIndexTableContainerEffect.tsx | 28 ++++++ .../hooks/useHandleToggleColumnFilter.ts | 71 ++++++++++++++++ .../hooks/useHandleToggleColumnSort.ts | 52 ++++++++++++ .../RecordTableColumnDropdownMenu.tsx | 49 ++++++++++- .../hooks/internal/useRecordTableStates.ts | 10 +++ .../record-table/hooks/useRecordTable.ts | 7 ++ .../onToggleColumnFilterComponentState.ts | 8 ++ .../onToggleColumnSortComponentState.ts | 8 ++ .../record-table/types/ColumnDefinition.ts | 2 + .../layout/dropdown/components/Dropdown.tsx | 5 +- .../ui/layout/dropdown/hooks/useDropdownV2.ts | 85 +++++++++++++++++++ .../EditableFilterDropdownButton.tsx | 2 +- .../views/components/ViewBarDetails.tsx | 3 +- .../utils/mapViewFieldsToColumnDefinitions.ts | 4 +- .../display/icon/components/TablerIcons.ts | 2 + 22 files changed, 473 insertions(+), 63 deletions(-) create mode 100644 packages/twenty-front/src/modules/object-record/object-sort-dropdown/hooks/useObjectSortDropdown.ts create mode 100644 packages/twenty-front/src/modules/object-record/object-sort-dropdown/states/isSortDirectionMenuUnfoldedState.ts create mode 100644 packages/twenty-front/src/modules/object-record/object-sort-dropdown/states/selectedSortDirectionState.ts create 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-index/hooks/useHandleToggleColumnSort.ts create mode 100644 packages/twenty-front/src/modules/object-record/record-table/states/onToggleColumnFilterComponentState.ts create mode 100644 packages/twenty-front/src/modules/object-record/record-table/states/onToggleColumnSortComponentState.ts create mode 100644 packages/twenty-front/src/modules/ui/layout/dropdown/hooks/useDropdownV2.ts diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/useColumnDefinitionsFromFieldMetadata.ts b/packages/twenty-front/src/modules/object-metadata/hooks/useColumnDefinitionsFromFieldMetadata.ts index 5f516def51..f673adee7e 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/useColumnDefinitionsFromFieldMetadata.ts +++ b/packages/twenty-front/src/modules/object-metadata/hooks/useColumnDefinitionsFromFieldMetadata.ts @@ -23,6 +23,14 @@ export const useColumnDefinitionsFromFieldMetadata = ( [objectMetadataItem], ); + const filterDefinitions = formatFieldMetadataItemsAsFilterDefinitions({ + fields: activeFieldMetadataItems, + }); + + const sortDefinitions = formatFieldMetadataItemsAsSortDefinitions({ + fields: activeFieldMetadataItems, + }); + const columnDefinitions: ColumnDefinition[] = useMemo( () => objectMetadataItem @@ -35,18 +43,30 @@ export const useColumnDefinitionsFromFieldMetadata = ( }), ) .filter(filterAvailableTableColumns) + .map((column) => { + const existsInFilterDefinitions = filterDefinitions.some( + (filter) => filter.fieldMetadataId === column.fieldMetadataId, + ); + + const existsInSortDefinitions = sortDefinitions.some( + (sort) => sort.fieldMetadataId === column.fieldMetadataId, + ); + + return { + ...column, + isFilterable: existsInFilterDefinitions, + isSortable: existsInSortDefinitions, + }; + }) : [], - [activeFieldMetadataItems, objectMetadataItem], + [ + activeFieldMetadataItems, + objectMetadataItem, + filterDefinitions, + sortDefinitions, + ], ); - const filterDefinitions = formatFieldMetadataItemsAsFilterDefinitions({ - fields: activeFieldMetadataItems, - }); - - const sortDefinitions = formatFieldMetadataItemsAsSortDefinitions({ - fields: activeFieldMetadataItems, - }); - return { columnDefinitions, filterDefinitions, diff --git a/packages/twenty-front/src/modules/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions.ts b/packages/twenty-front/src/modules/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions.ts index 359b153962..69092bf39d 100644 --- a/packages/twenty-front/src/modules/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions.ts +++ b/packages/twenty-front/src/modules/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions.ts @@ -53,10 +53,10 @@ export const formatFieldMetadataItemAsFilterDefinition = ({ field.toRelationMetadata?.fromObjectMetadata.namePlural, relationObjectMetadataNameSingular: field.toRelationMetadata?.fromObjectMetadata.nameSingular, - type: getFilterType(field.type), + type: getFilterTypeFromFieldType(field.type), }); -const getFilterType = (fieldType: FieldMetadataType) => { +export const getFilterTypeFromFieldType = (fieldType: FieldMetadataType) => { switch (fieldType) { case FieldMetadataType.DateTime: return 'DATE_TIME'; diff --git a/packages/twenty-front/src/modules/object-record/object-sort-dropdown/components/ObjectSortDropdownButton.tsx b/packages/twenty-front/src/modules/object-record/object-sort-dropdown/components/ObjectSortDropdownButton.tsx index e550fda40a..89da251bcd 100644 --- a/packages/twenty-front/src/modules/object-record/object-sort-dropdown/components/ObjectSortDropdownButton.tsx +++ b/packages/twenty-front/src/modules/object-record/object-sort-dropdown/components/ObjectSortDropdownButton.tsx @@ -1,9 +1,7 @@ -import { useCallback, useState } from 'react'; -import { useRecoilValue } from 'recoil'; import { IconChevronDown } from 'twenty-ui'; import { OBJECT_SORT_DROPDOWN_ID } from '@/object-record/object-sort-dropdown/constants/ObjectSortDropdownId'; -import { useSortDropdown } from '@/object-record/object-sort-dropdown/hooks/useSortDropdown'; +import { useObjectSortDropdown } from '@/object-record/object-sort-dropdown/hooks/useObjectSortDropdown'; import { ObjectSortDropdownScope } from '@/object-record/object-sort-dropdown/scopes/ObjectSortDropdownScope'; import { useIcons } from '@/ui/display/icon/hooks/useIcons'; import { LightButton } from '@/ui/input/button/components/LightButton'; @@ -11,12 +9,10 @@ import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader'; import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator'; -import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown'; import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem'; import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope'; -import { SortDefinition } from '../types/SortDefinition'; -import { SORT_DIRECTIONS, SortDirection } from '../types/SortDirection'; +import { SORT_DIRECTIONS } from '../types/SortDirection'; export type ObjectSortDropdownButtonProps = { sortDropdownId: string; @@ -27,45 +23,20 @@ export const ObjectSortDropdownButton = ({ sortDropdownId, hotkeyScope, }: ObjectSortDropdownButtonProps) => { - const [isSortDirectionMenuUnfolded, setIsSortDirectionMenuUnfolded] = - useState(false); - - const [selectedSortDirection, setSelectedSortDirection] = - useState('asc'); - - const resetState = useCallback(() => { - setIsSortDirectionMenuUnfolded(false); - setSelectedSortDirection('asc'); - }, []); - - const { toggleDropdown } = useDropdown(OBJECT_SORT_DROPDOWN_ID); + const { + isSortDirectionMenuUnfolded, + setIsSortDirectionMenuUnfolded, + selectedSortDirection, + setSelectedSortDirection, + toggleSortDropdown, + resetState, + isSortSelected, + availableSortDefinitions, + handleAddSort, + } = useObjectSortDropdown(); const handleButtonClick = () => { - toggleDropdown(); - resetState(); - }; - - const { - availableSortDefinitionsState, - onSortSelectState, - isSortSelectedState, - } = useSortDropdown({ - sortDropdownId: sortDropdownId, - }); - - const isSortSelected = useRecoilValue(isSortSelectedState); - const availableSortDefinitions = useRecoilValue( - availableSortDefinitionsState, - ); - const onSortSelect = useRecoilValue(onSortSelectState); - - const handleAddSort = (selectedSortDefinition: SortDefinition) => { - toggleDropdown(); - onSortSelect?.({ - fieldMetadataId: selectedSortDefinition.fieldMetadataId, - direction: selectedSortDirection, - definition: selectedSortDefinition, - }); + toggleSortDropdown(); }; const handleDropdownButtonClose = () => { diff --git a/packages/twenty-front/src/modules/object-record/object-sort-dropdown/constants/ObjectSortDropdownId.ts b/packages/twenty-front/src/modules/object-record/object-sort-dropdown/constants/ObjectSortDropdownId.ts index 5cc1f9c8fc..18e7cdb321 100644 --- a/packages/twenty-front/src/modules/object-record/object-sort-dropdown/constants/ObjectSortDropdownId.ts +++ b/packages/twenty-front/src/modules/object-record/object-sort-dropdown/constants/ObjectSortDropdownId.ts @@ -1 +1,5 @@ -export const OBJECT_SORT_DROPDOWN_ID = 'sort-dropdown'; +/* eslint-disable @nx/workspace-max-consts-per-file */ +const OBJECT_SORT_DROPDOWN_ID = 'sort-dropdown'; +const VIEW_SORT_DROPDOWN_ID = 'view-sort'; + +export { OBJECT_SORT_DROPDOWN_ID, VIEW_SORT_DROPDOWN_ID }; diff --git a/packages/twenty-front/src/modules/object-record/object-sort-dropdown/hooks/useObjectSortDropdown.ts b/packages/twenty-front/src/modules/object-record/object-sort-dropdown/hooks/useObjectSortDropdown.ts new file mode 100644 index 0000000000..85322c628b --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/object-sort-dropdown/hooks/useObjectSortDropdown.ts @@ -0,0 +1,77 @@ +import { useCallback } from 'react'; +import { useRecoilState, useRecoilValue } from 'recoil'; + +import { useSortDropdown } from '@/object-record/object-sort-dropdown/hooks/useSortDropdown'; +import isSortDirectionMenuUnfoldedState from '@/object-record/object-sort-dropdown/states/isSortDirectionMenuUnfoldedState'; +import selectedSortDirectionState from '@/object-record/object-sort-dropdown/states/selectedSortDirectionState'; +import { SortDefinition } from '@/object-record/object-sort-dropdown/types/SortDefinition'; +import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown'; + +import { + OBJECT_SORT_DROPDOWN_ID, + VIEW_SORT_DROPDOWN_ID, +} from '../constants/ObjectSortDropdownId'; + +// TODO: merge this with useSortDropdown +export const useObjectSortDropdown = () => { + const [isSortDirectionMenuUnfolded, setIsSortDirectionMenuUnfolded] = + useRecoilState(isSortDirectionMenuUnfoldedState); + + const [selectedSortDirection, setSelectedSortDirection] = useRecoilState( + selectedSortDirectionState, + ); + + const resetState = useCallback(() => { + setIsSortDirectionMenuUnfolded(false); + setSelectedSortDirection('asc'); + }, [setIsSortDirectionMenuUnfolded, setSelectedSortDirection]); + + const { toggleDropdown, closeDropdown } = useDropdown( + OBJECT_SORT_DROPDOWN_ID, + ); + + const toggleSortDropdown = () => { + toggleDropdown(); + resetState(); + }; + + const closeSortDropdown = () => { + closeDropdown(); + resetState(); + }; + + const { + availableSortDefinitionsState, + onSortSelectState, + isSortSelectedState, + } = useSortDropdown({ + sortDropdownId: VIEW_SORT_DROPDOWN_ID, + }); + + const isSortSelected = useRecoilValue(isSortSelectedState); + const availableSortDefinitions = useRecoilValue( + availableSortDefinitionsState, + ); + const onSortSelect = useRecoilValue(onSortSelectState); + + const handleAddSort = (selectedSortDefinition: SortDefinition) => { + closeSortDropdown(); + onSortSelect?.({ + fieldMetadataId: selectedSortDefinition.fieldMetadataId, + direction: selectedSortDirection, + definition: selectedSortDefinition, + }); + }; + + return { + isSortDirectionMenuUnfolded, + setIsSortDirectionMenuUnfolded, + selectedSortDirection, + setSelectedSortDirection, + toggleSortDropdown, + resetState, + isSortSelected, + availableSortDefinitions, + handleAddSort, + }; +}; diff --git a/packages/twenty-front/src/modules/object-record/object-sort-dropdown/states/isSortDirectionMenuUnfoldedState.ts b/packages/twenty-front/src/modules/object-record/object-sort-dropdown/states/isSortDirectionMenuUnfoldedState.ts new file mode 100644 index 0000000000..ebf779b94a --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/object-sort-dropdown/states/isSortDirectionMenuUnfoldedState.ts @@ -0,0 +1,8 @@ +import { atom } from 'recoil'; + +const isSortDirectionMenuUnfoldedState = atom({ + key: 'isSortDirectionMenuUnfoldedState', + default: false, +}); + +export default isSortDirectionMenuUnfoldedState; diff --git a/packages/twenty-front/src/modules/object-record/object-sort-dropdown/states/selectedSortDirectionState.ts b/packages/twenty-front/src/modules/object-record/object-sort-dropdown/states/selectedSortDirectionState.ts new file mode 100644 index 0000000000..327f969175 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/object-sort-dropdown/states/selectedSortDirectionState.ts @@ -0,0 +1,10 @@ +import { atom } from 'recoil'; + +import { SortDirection } from '@/object-record/object-sort-dropdown/types/SortDirection'; + +const selectedSortDirectionState = atom({ + key: 'selectedSortDirectionState', + default: 'asc', +}); + +export default selectedSortDirectionState; 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 dbd8289464..6677d04266 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 @@ -4,6 +4,8 @@ import { useRecoilValue } from 'recoil'; import { useColumnDefinitionsFromFieldMetadata } from '@/object-metadata/hooks/useColumnDefinitionsFromFieldMetadata'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { useRecordActionBar } from '@/object-record/record-action-bar/hooks/useRecordActionBar'; +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 { useSetRecordCountInCurrentView } from '@/views/hooks/useSetRecordCountInCurrentView'; @@ -23,6 +25,8 @@ export const RecordIndexTableContainerEffect = ({ setOnEntityCountChange, resetTableRowSelection, selectedRowIdsSelector, + setOnToggleColumnFilter, + setOnToggleColumnSort, } = useRecordTable({ recordTableId, }); @@ -49,6 +53,30 @@ export const RecordIndexTableContainerEffect = ({ callback: resetTableRowSelection, }); + const handleToggleColumnFilter = useHandleToggleColumnFilter({ + objectNameSingular, + viewBarId, + }); + + const handleToggleColumnSort = useHandleToggleColumnSort({ + objectNameSingular, + viewBarId, + }); + + useEffect(() => { + setOnToggleColumnFilter( + () => (fieldMetadataId: string) => + handleToggleColumnFilter(fieldMetadataId), + ); + }, [setOnToggleColumnFilter, handleToggleColumnFilter]); + + useEffect(() => { + setOnToggleColumnSort( + () => (fieldMetadataId: string) => + handleToggleColumnSort(fieldMetadataId), + ); + }, [setOnToggleColumnSort, handleToggleColumnSort]); + useEffect(() => { setActionBarEntries?.(); setContextMenuEntries?.(); 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 new file mode 100644 index 0000000000..774e604178 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-index/hooks/useHandleToggleColumnFilter.ts @@ -0,0 +1,71 @@ +import { useCallback } from 'react'; + +import { useColumnDefinitionsFromFieldMetadata } from '@/object-metadata/hooks/useColumnDefinitionsFromFieldMetadata'; +import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; +import { getFilterTypeFromFieldType } from '@/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions'; +import { Filter } from '@/object-record/object-filter-dropdown/types/Filter'; +import { getOperandsForFilterType } from '@/object-record/object-filter-dropdown/utils/getOperandsForFilterType'; +import { useDropdownV2 } from '@/ui/layout/dropdown/hooks/useDropdownV2'; +import { useCombinedViewFilters } from '@/views/hooks/useCombinedViewFilters'; +import { isDefined } from '~/utils/isDefined'; + +type UseHandleToggleColumnFilterProps = { + objectNameSingular: string; + viewBarId: string; +}; + +export const useHandleToggleColumnFilter = ({ + viewBarId, + objectNameSingular, +}: UseHandleToggleColumnFilterProps) => { + const { objectMetadataItem } = useObjectMetadataItem({ + objectNameSingular, + }); + + const { columnDefinitions } = + useColumnDefinitionsFromFieldMetadata(objectMetadataItem); + + const { upsertCombinedViewFilter } = useCombinedViewFilters(viewBarId); + const { openDropdown } = useDropdownV2(); + + const handleToggleColumnFilter = useCallback( + (fieldMetadataId: string) => { + const correspondingColumnDefinition = columnDefinitions.find( + (columnDefinition) => + columnDefinition.fieldMetadataId === fieldMetadataId, + ); + + if (!isDefined(correspondingColumnDefinition)) return; + + const filterType = getFilterTypeFromFieldType( + correspondingColumnDefinition?.type, + ); + + const availableOperandsForFilter = getOperandsForFilterType(filterType); + + const defaultOperand = availableOperandsForFilter[0]; + + const newFilter: Filter = { + fieldMetadataId, + operand: defaultOperand, + displayValue: '', + definition: { + label: correspondingColumnDefinition.label, + iconName: correspondingColumnDefinition.iconName, + fieldMetadataId, + type: filterType, + }, + value: '', + }; + + upsertCombinedViewFilter(newFilter); + + openDropdown(fieldMetadataId, { + scope: fieldMetadataId, + }); + }, + [columnDefinitions, upsertCombinedViewFilter, openDropdown], + ); + + return handleToggleColumnFilter; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-index/hooks/useHandleToggleColumnSort.ts b/packages/twenty-front/src/modules/object-record/record-index/hooks/useHandleToggleColumnSort.ts new file mode 100644 index 0000000000..a5253a49e8 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-index/hooks/useHandleToggleColumnSort.ts @@ -0,0 +1,52 @@ +import { useCallback } from 'react'; + +import { useColumnDefinitionsFromFieldMetadata } from '@/object-metadata/hooks/useColumnDefinitionsFromFieldMetadata'; +import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; +import { Sort } from '@/object-record/object-sort-dropdown/types/Sort'; +import { useCombinedViewSorts } from '@/views/hooks/useCombinedViewSorts'; +import { isDefined } from '~/utils/isDefined'; + +type UseHandleToggleColumnSortProps = { + objectNameSingular: string; + viewBarId: string; +}; + +export const useHandleToggleColumnSort = ({ + viewBarId, + objectNameSingular, +}: UseHandleToggleColumnSortProps) => { + const { objectMetadataItem } = useObjectMetadataItem({ + objectNameSingular, + }); + + const { columnDefinitions } = + useColumnDefinitionsFromFieldMetadata(objectMetadataItem); + + const { upsertCombinedViewSort } = useCombinedViewSorts(viewBarId); + + const handleToggleColumnSort = useCallback( + (fieldMetadataId: string) => { + const correspondingColumnDefinition = columnDefinitions.find( + (columnDefinition) => + columnDefinition.fieldMetadataId === fieldMetadataId, + ); + + if (!isDefined(correspondingColumnDefinition)) return; + + const newSort: Sort = { + fieldMetadataId, + definition: { + fieldMetadataId, + label: correspondingColumnDefinition.label, + iconName: correspondingColumnDefinition.iconName, + }, + direction: 'asc', + }; + + upsertCombinedViewSort(newSort); + }, + [columnDefinitions, upsertCombinedViewSort], + ); + + return handleToggleColumnSort; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableColumnDropdownMenu.tsx b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableColumnDropdownMenu.tsx index 98fab89208..96a257041f 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableColumnDropdownMenu.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableColumnDropdownMenu.tsx @@ -1,9 +1,16 @@ import { useRecoilValue } from 'recoil'; -import { IconArrowLeft, IconArrowRight, IconEyeOff } from 'twenty-ui'; +import { + IconArrowLeft, + IconArrowRight, + IconEyeOff, + IconFilter, + IconSortDescending, +} from 'twenty-ui'; import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata'; import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates'; import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; +import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator'; import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown'; import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem'; @@ -17,7 +24,11 @@ export type RecordTableColumnDropdownMenuProps = { export const RecordTableColumnDropdownMenu = ({ column, }: RecordTableColumnDropdownMenuProps) => { - const { visibleTableColumnsSelector } = useRecordTableStates(); + const { + visibleTableColumnsSelector, + onToggleColumnFilterState, + onToggleColumnSortState, + } = useRecordTableStates(); const visibleTableColumns = useRecoilValue(visibleTableColumnsSelector()); @@ -55,8 +66,42 @@ export const RecordTableColumnDropdownMenu = ({ handleColumnVisibilityChange(column); }; + const onToggleColumnFilter = useRecoilValue(onToggleColumnFilterState); + const onToggleColumnSort = useRecoilValue(onToggleColumnSortState); + + const handleSortClick = () => { + closeDropdown(); + + onToggleColumnSort?.(column.fieldMetadataId); + }; + + const handleFilterClick = () => { + closeDropdown(); + + onToggleColumnFilter?.(column.fieldMetadataId); + }; + + const isSortable = column.isSortable === true; + const isFilterable = column.isFilterable === true; + const showSeparator = isFilterable || isSortable; + return ( + {isFilterable && ( + + )} + {isSortable && ( + + )} + {showSeparator && } {canMoveLeft && ( { tableColumnsComponentState, scopeId, ), + onToggleColumnFilterState: extractComponentState( + onToggleColumnFilterComponentState, + scopeId, + ), + onToggleColumnSortState: extractComponentState( + onToggleColumnSortComponentState, + scopeId, + ), onColumnsChangeState: extractComponentState( onColumnsChangeComponentState, scopeId, 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 78e5f0cefb..648b0f4405 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 @@ -42,6 +42,8 @@ export const useRecordTable = (props?: useRecordTableProps) => { isRecordTableInitialLoadingState, tableLastRowVisibleState, selectedRowIdsSelector, + onToggleColumnFilterState, + onToggleColumnSortState, } = useRecordTableStates(recordTableId); const setAvailableTableColumns = useRecoilCallback( @@ -70,6 +72,9 @@ export const useRecordTable = (props?: useRecordTableProps) => { const setOnColumnsChange = useSetRecoilState(onColumnsChangeState); + const setOnToggleColumnFilter = useSetRecoilState(onToggleColumnFilterState); + const setOnToggleColumnSort = useSetRecoilState(onToggleColumnSortState); + const setIsRecordTableInitialLoading = useSetRecoilState( isRecordTableInitialLoadingState, ); @@ -215,5 +220,7 @@ export const useRecordTable = (props?: useRecordTableProps) => { isSomeCellInEditModeState, selectedRowIdsSelector, setHasUserSelectedAllRows, + setOnToggleColumnFilter, + setOnToggleColumnSort, }; }; 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 new file mode 100644 index 0000000000..75a602331c --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-table/states/onToggleColumnFilterComponentState.ts @@ -0,0 +1,8 @@ +import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState'; + +export const onToggleColumnFilterComponentState = createComponentState< + ((fieldMetadataId: string) => void) | undefined +>({ + key: 'onToggleColumnFilterComponentState', + defaultValue: undefined, +}); diff --git a/packages/twenty-front/src/modules/object-record/record-table/states/onToggleColumnSortComponentState.ts b/packages/twenty-front/src/modules/object-record/record-table/states/onToggleColumnSortComponentState.ts new file mode 100644 index 0000000000..73f0a924a9 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-table/states/onToggleColumnSortComponentState.ts @@ -0,0 +1,8 @@ +import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState'; + +export const onToggleColumnSortComponentState = createComponentState< + ((fieldMetadataId: string) => void) | undefined +>({ + key: 'onToggleColumnSortComponentState', + defaultValue: undefined, +}); diff --git a/packages/twenty-front/src/modules/object-record/record-table/types/ColumnDefinition.ts b/packages/twenty-front/src/modules/object-record/record-table/types/ColumnDefinition.ts index d65d838e49..c14b745ef5 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/types/ColumnDefinition.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/types/ColumnDefinition.ts @@ -7,4 +7,6 @@ export type ColumnDefinition = FieldDefinition & { isLabelIdentifier?: boolean; isVisible?: boolean; viewFieldId?: string; + isFilterable?: boolean; + isSortable?: boolean; }; diff --git a/packages/twenty-front/src/modules/ui/layout/dropdown/components/Dropdown.tsx b/packages/twenty-front/src/modules/ui/layout/dropdown/components/Dropdown.tsx index 88d8479be2..70c58355d4 100644 --- a/packages/twenty-front/src/modules/ui/layout/dropdown/components/Dropdown.tsx +++ b/packages/twenty-front/src/modules/ui/layout/dropdown/components/Dropdown.tsx @@ -14,6 +14,7 @@ import { HotkeyEffect } from '@/ui/utilities/hotkey/components/HotkeyEffect'; import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope'; import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside'; +import { getScopeIdFromComponentId } from '@/ui/utilities/recoil-scope/utils/getScopeIdFromComponentId'; import { isDefined } from '~/utils/isDefined'; import { useDropdown } from '../hooks/useDropdown'; @@ -92,7 +93,7 @@ export const Dropdown = ({ }); useInternalHotkeyScopeManagement({ - dropdownScopeId: `${dropdownId}-scope`, + dropdownScopeId: getScopeIdFromComponentId(dropdownId), dropdownHotkeyScopeFromParent: dropdownHotkeyScope, }); @@ -106,7 +107,7 @@ export const Dropdown = ({ ); return ( - +
{clickableComponent && (
{ + const { + setHotkeyScopeAndMemorizePreviousScope, + goBackToPreviousHotkeyScope, + } = usePreviousHotkeyScope(); + + const closeDropdown = useRecoilCallback( + ({ set }) => + (specificComponentId: string) => { + const scopeId = getScopeIdFromComponentId(specificComponentId); + + goBackToPreviousHotkeyScope(); + set( + isDropdownOpenComponentState({ + scopeId, + }), + false, + ); + }, + [goBackToPreviousHotkeyScope], + ); + + const openDropdown = useRecoilCallback( + ({ set, snapshot }) => + (specificComponentId: string, customHotkeyScope?: HotkeyScope) => { + const scopeId = getScopeIdFromComponentId(specificComponentId); + + const dropdownHotkeyScope = snapshot + .getLoadable(dropdownHotkeyComponentState({ scopeId })) + .getValue(); + + set( + isDropdownOpenComponentState({ + scopeId, + }), + true, + ); + + if (isDefined(customHotkeyScope)) { + setHotkeyScopeAndMemorizePreviousScope( + customHotkeyScope.scope, + customHotkeyScope.customScopes, + ); + } else if (isDefined(dropdownHotkeyScope)) { + setHotkeyScopeAndMemorizePreviousScope( + dropdownHotkeyScope.scope, + dropdownHotkeyScope.customScopes, + ); + } + }, + [setHotkeyScopeAndMemorizePreviousScope], + ); + + const toggleDropdown = useRecoilCallback( + ({ snapshot }) => + (specificComponentId: string) => { + const scopeId = getScopeIdFromComponentId(specificComponentId); + const isDropdownOpen = snapshot + .getLoadable(isDropdownOpenComponentState({ scopeId })) + .getValue(); + + if (isDropdownOpen) { + closeDropdown(specificComponentId); + } else { + openDropdown(specificComponentId); + } + }, + [closeDropdown, openDropdown], + ); + + return { + closeDropdown, + openDropdown, + toggleDropdown, + }; +}; diff --git a/packages/twenty-front/src/modules/views/components/EditableFilterDropdownButton.tsx b/packages/twenty-front/src/modules/views/components/EditableFilterDropdownButton.tsx index aa10156cdc..ee7f4dedb1 100644 --- a/packages/twenty-front/src/modules/views/components/EditableFilterDropdownButton.tsx +++ b/packages/twenty-front/src/modules/views/components/EditableFilterDropdownButton.tsx @@ -74,7 +74,7 @@ export const EditableFilterDropdownButton = ({ return ( } diff --git a/packages/twenty-front/src/modules/views/components/ViewBarDetails.tsx b/packages/twenty-front/src/modules/views/components/ViewBarDetails.tsx index 4ebe64e417..f2f60d1810 100644 --- a/packages/twenty-front/src/modules/views/components/ViewBarDetails.tsx +++ b/packages/twenty-front/src/modules/views/components/ViewBarDetails.tsx @@ -4,7 +4,6 @@ import { useRecoilValue } from 'recoil'; import { AddObjectFilterFromDetailsButton } from '@/object-record/object-filter-dropdown/components/AddObjectFilterFromDetailsButton'; import { ObjectFilterDropdownScope } from '@/object-record/object-filter-dropdown/scopes/ObjectFilterDropdownScope'; -import { FiltersHotkeyScope } from '@/object-record/object-filter-dropdown/types/FiltersHotkeyScope'; import { DropdownScope } from '@/ui/layout/dropdown/scopes/DropdownScope'; import { EditableFilterDropdownButton } from '@/views/components/EditableFilterDropdownButton'; import { EditableSortChip } from '@/views/components/EditableSortChip'; @@ -161,7 +160,7 @@ export const ViewBarDetails = ({ diff --git a/packages/twenty-front/src/modules/views/utils/mapViewFieldsToColumnDefinitions.ts b/packages/twenty-front/src/modules/views/utils/mapViewFieldsToColumnDefinitions.ts index 0357997607..c27b8045d5 100644 --- a/packages/twenty-front/src/modules/views/utils/mapViewFieldsToColumnDefinitions.ts +++ b/packages/twenty-front/src/modules/views/utils/mapViewFieldsToColumnDefinitions.ts @@ -47,7 +47,9 @@ export const mapViewFieldsToColumnDefinitions = ({ isLabelIdentifier, isVisible: isLabelIdentifier || viewField.isVisible, viewFieldId: viewField.id, - }; + isSortable: correspondingColumnDefinition.isSortable, + isFilterable: correspondingColumnDefinition.isFilterable, + } as ColumnDefinition; }) .filter(isDefined); diff --git a/packages/twenty-ui/src/display/icon/components/TablerIcons.ts b/packages/twenty-ui/src/display/icon/components/TablerIcons.ts index bf12bede93..25ec8383fa 100644 --- a/packages/twenty-ui/src/display/icon/components/TablerIcons.ts +++ b/packages/twenty-ui/src/display/icon/components/TablerIcons.ts @@ -67,6 +67,7 @@ export { IconFileText, IconFileUpload, IconFileZip, + IconFilter, IconFilterOff, IconFocusCentered, IconForbid, @@ -119,6 +120,7 @@ export { IconSearch, IconSend, IconSettings, + IconSortDescending, IconTable, IconTag, IconTarget,