mirror of
https://github.com/twentyhq/twenty.git
synced 2024-12-24 04:23:57 +03:00
3886 - Shortcut Sort/Filter (#3901)
Closes #3886 --------- Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
This commit is contained in:
parent
b65d82c274
commit
bcf5268f7f
@ -23,6 +23,14 @@ export const useColumnDefinitionsFromFieldMetadata = (
|
|||||||
[objectMetadataItem],
|
[objectMetadataItem],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const filterDefinitions = formatFieldMetadataItemsAsFilterDefinitions({
|
||||||
|
fields: activeFieldMetadataItems,
|
||||||
|
});
|
||||||
|
|
||||||
|
const sortDefinitions = formatFieldMetadataItemsAsSortDefinitions({
|
||||||
|
fields: activeFieldMetadataItems,
|
||||||
|
});
|
||||||
|
|
||||||
const columnDefinitions: ColumnDefinition<FieldMetadata>[] = useMemo(
|
const columnDefinitions: ColumnDefinition<FieldMetadata>[] = useMemo(
|
||||||
() =>
|
() =>
|
||||||
objectMetadataItem
|
objectMetadataItem
|
||||||
@ -35,18 +43,30 @@ export const useColumnDefinitionsFromFieldMetadata = (
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.filter(filterAvailableTableColumns)
|
.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 {
|
return {
|
||||||
columnDefinitions,
|
columnDefinitions,
|
||||||
filterDefinitions,
|
filterDefinitions,
|
||||||
|
@ -53,10 +53,10 @@ export const formatFieldMetadataItemAsFilterDefinition = ({
|
|||||||
field.toRelationMetadata?.fromObjectMetadata.namePlural,
|
field.toRelationMetadata?.fromObjectMetadata.namePlural,
|
||||||
relationObjectMetadataNameSingular:
|
relationObjectMetadataNameSingular:
|
||||||
field.toRelationMetadata?.fromObjectMetadata.nameSingular,
|
field.toRelationMetadata?.fromObjectMetadata.nameSingular,
|
||||||
type: getFilterType(field.type),
|
type: getFilterTypeFromFieldType(field.type),
|
||||||
});
|
});
|
||||||
|
|
||||||
const getFilterType = (fieldType: FieldMetadataType) => {
|
export const getFilterTypeFromFieldType = (fieldType: FieldMetadataType) => {
|
||||||
switch (fieldType) {
|
switch (fieldType) {
|
||||||
case FieldMetadataType.DateTime:
|
case FieldMetadataType.DateTime:
|
||||||
return 'DATE_TIME';
|
return 'DATE_TIME';
|
||||||
|
@ -1,9 +1,7 @@
|
|||||||
import { useCallback, useState } from 'react';
|
|
||||||
import { useRecoilValue } from 'recoil';
|
|
||||||
import { IconChevronDown } from 'twenty-ui';
|
import { IconChevronDown } from 'twenty-ui';
|
||||||
|
|
||||||
import { OBJECT_SORT_DROPDOWN_ID } from '@/object-record/object-sort-dropdown/constants/ObjectSortDropdownId';
|
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 { ObjectSortDropdownScope } from '@/object-record/object-sort-dropdown/scopes/ObjectSortDropdownScope';
|
||||||
import { useIcons } from '@/ui/display/icon/hooks/useIcons';
|
import { useIcons } from '@/ui/display/icon/hooks/useIcons';
|
||||||
import { LightButton } from '@/ui/input/button/components/LightButton';
|
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 { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader';
|
||||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||||
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
|
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 { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
|
||||||
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
|
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
|
||||||
|
|
||||||
import { SortDefinition } from '../types/SortDefinition';
|
import { SORT_DIRECTIONS } from '../types/SortDirection';
|
||||||
import { SORT_DIRECTIONS, SortDirection } from '../types/SortDirection';
|
|
||||||
|
|
||||||
export type ObjectSortDropdownButtonProps = {
|
export type ObjectSortDropdownButtonProps = {
|
||||||
sortDropdownId: string;
|
sortDropdownId: string;
|
||||||
@ -27,45 +23,20 @@ export const ObjectSortDropdownButton = ({
|
|||||||
sortDropdownId,
|
sortDropdownId,
|
||||||
hotkeyScope,
|
hotkeyScope,
|
||||||
}: ObjectSortDropdownButtonProps) => {
|
}: ObjectSortDropdownButtonProps) => {
|
||||||
const [isSortDirectionMenuUnfolded, setIsSortDirectionMenuUnfolded] =
|
const {
|
||||||
useState(false);
|
isSortDirectionMenuUnfolded,
|
||||||
|
setIsSortDirectionMenuUnfolded,
|
||||||
const [selectedSortDirection, setSelectedSortDirection] =
|
selectedSortDirection,
|
||||||
useState<SortDirection>('asc');
|
setSelectedSortDirection,
|
||||||
|
toggleSortDropdown,
|
||||||
const resetState = useCallback(() => {
|
resetState,
|
||||||
setIsSortDirectionMenuUnfolded(false);
|
isSortSelected,
|
||||||
setSelectedSortDirection('asc');
|
availableSortDefinitions,
|
||||||
}, []);
|
handleAddSort,
|
||||||
|
} = useObjectSortDropdown();
|
||||||
const { toggleDropdown } = useDropdown(OBJECT_SORT_DROPDOWN_ID);
|
|
||||||
|
|
||||||
const handleButtonClick = () => {
|
const handleButtonClick = () => {
|
||||||
toggleDropdown();
|
toggleSortDropdown();
|
||||||
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,
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDropdownButtonClose = () => {
|
const handleDropdownButtonClose = () => {
|
||||||
|
@ -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 };
|
||||||
|
@ -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,
|
||||||
|
};
|
||||||
|
};
|
@ -0,0 +1,8 @@
|
|||||||
|
import { atom } from 'recoil';
|
||||||
|
|
||||||
|
const isSortDirectionMenuUnfoldedState = atom({
|
||||||
|
key: 'isSortDirectionMenuUnfoldedState',
|
||||||
|
default: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default isSortDirectionMenuUnfoldedState;
|
@ -0,0 +1,10 @@
|
|||||||
|
import { atom } from 'recoil';
|
||||||
|
|
||||||
|
import { SortDirection } from '@/object-record/object-sort-dropdown/types/SortDirection';
|
||||||
|
|
||||||
|
const selectedSortDirectionState = atom<SortDirection>({
|
||||||
|
key: 'selectedSortDirectionState',
|
||||||
|
default: 'asc',
|
||||||
|
});
|
||||||
|
|
||||||
|
export default selectedSortDirectionState;
|
@ -4,6 +4,8 @@ import { useRecoilValue } from 'recoil';
|
|||||||
import { useColumnDefinitionsFromFieldMetadata } from '@/object-metadata/hooks/useColumnDefinitionsFromFieldMetadata';
|
import { useColumnDefinitionsFromFieldMetadata } from '@/object-metadata/hooks/useColumnDefinitionsFromFieldMetadata';
|
||||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||||
import { useRecordActionBar } from '@/object-record/record-action-bar/hooks/useRecordActionBar';
|
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 { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
|
||||||
import { useSetRecordCountInCurrentView } from '@/views/hooks/useSetRecordCountInCurrentView';
|
import { useSetRecordCountInCurrentView } from '@/views/hooks/useSetRecordCountInCurrentView';
|
||||||
|
|
||||||
@ -23,6 +25,8 @@ export const RecordIndexTableContainerEffect = ({
|
|||||||
setOnEntityCountChange,
|
setOnEntityCountChange,
|
||||||
resetTableRowSelection,
|
resetTableRowSelection,
|
||||||
selectedRowIdsSelector,
|
selectedRowIdsSelector,
|
||||||
|
setOnToggleColumnFilter,
|
||||||
|
setOnToggleColumnSort,
|
||||||
} = useRecordTable({
|
} = useRecordTable({
|
||||||
recordTableId,
|
recordTableId,
|
||||||
});
|
});
|
||||||
@ -49,6 +53,30 @@ export const RecordIndexTableContainerEffect = ({
|
|||||||
callback: resetTableRowSelection,
|
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(() => {
|
useEffect(() => {
|
||||||
setActionBarEntries?.();
|
setActionBarEntries?.();
|
||||||
setContextMenuEntries?.();
|
setContextMenuEntries?.();
|
||||||
|
@ -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;
|
||||||
|
};
|
@ -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;
|
||||||
|
};
|
@ -1,9 +1,16 @@
|
|||||||
import { useRecoilValue } from 'recoil';
|
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 { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
|
||||||
import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates';
|
import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates';
|
||||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
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 { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
|
||||||
import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
|
import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
|
||||||
|
|
||||||
@ -17,7 +24,11 @@ export type RecordTableColumnDropdownMenuProps = {
|
|||||||
export const RecordTableColumnDropdownMenu = ({
|
export const RecordTableColumnDropdownMenu = ({
|
||||||
column,
|
column,
|
||||||
}: RecordTableColumnDropdownMenuProps) => {
|
}: RecordTableColumnDropdownMenuProps) => {
|
||||||
const { visibleTableColumnsSelector } = useRecordTableStates();
|
const {
|
||||||
|
visibleTableColumnsSelector,
|
||||||
|
onToggleColumnFilterState,
|
||||||
|
onToggleColumnSortState,
|
||||||
|
} = useRecordTableStates();
|
||||||
|
|
||||||
const visibleTableColumns = useRecoilValue(visibleTableColumnsSelector());
|
const visibleTableColumns = useRecoilValue(visibleTableColumnsSelector());
|
||||||
|
|
||||||
@ -55,8 +66,42 @@ export const RecordTableColumnDropdownMenu = ({
|
|||||||
handleColumnVisibilityChange(column);
|
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 (
|
return (
|
||||||
<DropdownMenuItemsContainer>
|
<DropdownMenuItemsContainer>
|
||||||
|
{isFilterable && (
|
||||||
|
<MenuItem
|
||||||
|
LeftIcon={IconFilter}
|
||||||
|
onClick={handleFilterClick}
|
||||||
|
text="Filter"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{isSortable && (
|
||||||
|
<MenuItem
|
||||||
|
LeftIcon={IconSortDescending}
|
||||||
|
onClick={handleSortClick}
|
||||||
|
text="Sort"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{showSeparator && <DropdownMenuSeparator />}
|
||||||
{canMoveLeft && (
|
{canMoveLeft && (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
LeftIcon={IconArrowLeft}
|
LeftIcon={IconArrowLeft}
|
||||||
|
@ -10,6 +10,8 @@ import { isTableCellInEditModeComponentFamilyState } from '@/object-record/recor
|
|||||||
import { numberOfTableRowsComponentState } from '@/object-record/record-table/states/numberOfTableRowsComponentState';
|
import { numberOfTableRowsComponentState } from '@/object-record/record-table/states/numberOfTableRowsComponentState';
|
||||||
import { onColumnsChangeComponentState } from '@/object-record/record-table/states/onColumnsChangeComponentState';
|
import { onColumnsChangeComponentState } from '@/object-record/record-table/states/onColumnsChangeComponentState';
|
||||||
import { onEntityCountChangeComponentState } from '@/object-record/record-table/states/onEntityCountChangeComponentState';
|
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 { resizeFieldOffsetComponentState } from '@/object-record/record-table/states/resizeFieldOffsetComponentState';
|
import { resizeFieldOffsetComponentState } from '@/object-record/record-table/states/resizeFieldOffsetComponentState';
|
||||||
import { allRowsSelectedStatusComponentSelector } from '@/object-record/record-table/states/selectors/allRowsSelectedStatusComponentSelector';
|
import { allRowsSelectedStatusComponentSelector } from '@/object-record/record-table/states/selectors/allRowsSelectedStatusComponentSelector';
|
||||||
import { hiddenTableColumnsComponentSelector } from '@/object-record/record-table/states/selectors/hiddenTableColumnsComponentSelector';
|
import { hiddenTableColumnsComponentSelector } from '@/object-record/record-table/states/selectors/hiddenTableColumnsComponentSelector';
|
||||||
@ -49,6 +51,14 @@ export const useRecordTableStates = (recordTableId?: string) => {
|
|||||||
tableColumnsComponentState,
|
tableColumnsComponentState,
|
||||||
scopeId,
|
scopeId,
|
||||||
),
|
),
|
||||||
|
onToggleColumnFilterState: extractComponentState(
|
||||||
|
onToggleColumnFilterComponentState,
|
||||||
|
scopeId,
|
||||||
|
),
|
||||||
|
onToggleColumnSortState: extractComponentState(
|
||||||
|
onToggleColumnSortComponentState,
|
||||||
|
scopeId,
|
||||||
|
),
|
||||||
onColumnsChangeState: extractComponentState(
|
onColumnsChangeState: extractComponentState(
|
||||||
onColumnsChangeComponentState,
|
onColumnsChangeComponentState,
|
||||||
scopeId,
|
scopeId,
|
||||||
|
@ -42,6 +42,8 @@ export const useRecordTable = (props?: useRecordTableProps) => {
|
|||||||
isRecordTableInitialLoadingState,
|
isRecordTableInitialLoadingState,
|
||||||
tableLastRowVisibleState,
|
tableLastRowVisibleState,
|
||||||
selectedRowIdsSelector,
|
selectedRowIdsSelector,
|
||||||
|
onToggleColumnFilterState,
|
||||||
|
onToggleColumnSortState,
|
||||||
} = useRecordTableStates(recordTableId);
|
} = useRecordTableStates(recordTableId);
|
||||||
|
|
||||||
const setAvailableTableColumns = useRecoilCallback(
|
const setAvailableTableColumns = useRecoilCallback(
|
||||||
@ -70,6 +72,9 @@ export const useRecordTable = (props?: useRecordTableProps) => {
|
|||||||
|
|
||||||
const setOnColumnsChange = useSetRecoilState(onColumnsChangeState);
|
const setOnColumnsChange = useSetRecoilState(onColumnsChangeState);
|
||||||
|
|
||||||
|
const setOnToggleColumnFilter = useSetRecoilState(onToggleColumnFilterState);
|
||||||
|
const setOnToggleColumnSort = useSetRecoilState(onToggleColumnSortState);
|
||||||
|
|
||||||
const setIsRecordTableInitialLoading = useSetRecoilState(
|
const setIsRecordTableInitialLoading = useSetRecoilState(
|
||||||
isRecordTableInitialLoadingState,
|
isRecordTableInitialLoadingState,
|
||||||
);
|
);
|
||||||
@ -215,5 +220,7 @@ export const useRecordTable = (props?: useRecordTableProps) => {
|
|||||||
isSomeCellInEditModeState,
|
isSomeCellInEditModeState,
|
||||||
selectedRowIdsSelector,
|
selectedRowIdsSelector,
|
||||||
setHasUserSelectedAllRows,
|
setHasUserSelectedAllRows,
|
||||||
|
setOnToggleColumnFilter,
|
||||||
|
setOnToggleColumnSort,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -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,
|
||||||
|
});
|
@ -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,
|
||||||
|
});
|
@ -7,4 +7,6 @@ export type ColumnDefinition<T extends FieldMetadata> = FieldDefinition<T> & {
|
|||||||
isLabelIdentifier?: boolean;
|
isLabelIdentifier?: boolean;
|
||||||
isVisible?: boolean;
|
isVisible?: boolean;
|
||||||
viewFieldId?: string;
|
viewFieldId?: string;
|
||||||
|
isFilterable?: boolean;
|
||||||
|
isSortable?: boolean;
|
||||||
};
|
};
|
||||||
|
@ -14,6 +14,7 @@ import { HotkeyEffect } from '@/ui/utilities/hotkey/components/HotkeyEffect';
|
|||||||
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||||
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
|
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
|
||||||
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
|
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
|
||||||
|
import { getScopeIdFromComponentId } from '@/ui/utilities/recoil-scope/utils/getScopeIdFromComponentId';
|
||||||
import { isDefined } from '~/utils/isDefined';
|
import { isDefined } from '~/utils/isDefined';
|
||||||
|
|
||||||
import { useDropdown } from '../hooks/useDropdown';
|
import { useDropdown } from '../hooks/useDropdown';
|
||||||
@ -92,7 +93,7 @@ export const Dropdown = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
useInternalHotkeyScopeManagement({
|
useInternalHotkeyScopeManagement({
|
||||||
dropdownScopeId: `${dropdownId}-scope`,
|
dropdownScopeId: getScopeIdFromComponentId(dropdownId),
|
||||||
dropdownHotkeyScopeFromParent: dropdownHotkeyScope,
|
dropdownHotkeyScopeFromParent: dropdownHotkeyScope,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -106,7 +107,7 @@ export const Dropdown = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownScope dropdownScopeId={`${dropdownId}-scope`}>
|
<DropdownScope dropdownScopeId={getScopeIdFromComponentId(dropdownId)}>
|
||||||
<div ref={containerRef} className={className}>
|
<div ref={containerRef} className={className}>
|
||||||
{clickableComponent && (
|
{clickableComponent && (
|
||||||
<div
|
<div
|
||||||
|
@ -0,0 +1,85 @@
|
|||||||
|
import { useRecoilCallback } from 'recoil';
|
||||||
|
|
||||||
|
import { dropdownHotkeyComponentState } from '@/ui/layout/dropdown/states/dropdownHotkeyComponentState';
|
||||||
|
import { isDropdownOpenComponentState } from '@/ui/layout/dropdown/states/isDropdownOpenComponentState';
|
||||||
|
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
|
||||||
|
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
|
||||||
|
import { getScopeIdFromComponentId } from '@/ui/utilities/recoil-scope/utils/getScopeIdFromComponentId';
|
||||||
|
import { isDefined } from '~/utils/isDefined';
|
||||||
|
|
||||||
|
export const useDropdownV2 = () => {
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
};
|
@ -74,7 +74,7 @@ export const EditableFilterDropdownButton = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Dropdown
|
<Dropdown
|
||||||
dropdownId={viewFilter.fieldMetadataId}
|
dropdownId={viewFilterDropdownId}
|
||||||
clickableComponent={
|
clickableComponent={
|
||||||
<EditableFilterChip viewFilter={viewFilter} onRemove={handleRemove} />
|
<EditableFilterChip viewFilter={viewFilter} onRemove={handleRemove} />
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,6 @@ import { useRecoilValue } from 'recoil';
|
|||||||
|
|
||||||
import { AddObjectFilterFromDetailsButton } from '@/object-record/object-filter-dropdown/components/AddObjectFilterFromDetailsButton';
|
import { AddObjectFilterFromDetailsButton } from '@/object-record/object-filter-dropdown/components/AddObjectFilterFromDetailsButton';
|
||||||
import { ObjectFilterDropdownScope } from '@/object-record/object-filter-dropdown/scopes/ObjectFilterDropdownScope';
|
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 { DropdownScope } from '@/ui/layout/dropdown/scopes/DropdownScope';
|
||||||
import { EditableFilterDropdownButton } from '@/views/components/EditableFilterDropdownButton';
|
import { EditableFilterDropdownButton } from '@/views/components/EditableFilterDropdownButton';
|
||||||
import { EditableSortChip } from '@/views/components/EditableSortChip';
|
import { EditableSortChip } from '@/views/components/EditableSortChip';
|
||||||
@ -161,7 +160,7 @@ export const ViewBarDetails = ({
|
|||||||
<EditableFilterDropdownButton
|
<EditableFilterDropdownButton
|
||||||
viewFilter={viewFilter}
|
viewFilter={viewFilter}
|
||||||
hotkeyScope={{
|
hotkeyScope={{
|
||||||
scope: FiltersHotkeyScope.ObjectFilterDropdownButton,
|
scope: viewFilter.fieldMetadataId,
|
||||||
}}
|
}}
|
||||||
viewFilterDropdownId={viewFilter.fieldMetadataId}
|
viewFilterDropdownId={viewFilter.fieldMetadataId}
|
||||||
/>
|
/>
|
||||||
|
@ -47,7 +47,9 @@ export const mapViewFieldsToColumnDefinitions = ({
|
|||||||
isLabelIdentifier,
|
isLabelIdentifier,
|
||||||
isVisible: isLabelIdentifier || viewField.isVisible,
|
isVisible: isLabelIdentifier || viewField.isVisible,
|
||||||
viewFieldId: viewField.id,
|
viewFieldId: viewField.id,
|
||||||
};
|
isSortable: correspondingColumnDefinition.isSortable,
|
||||||
|
isFilterable: correspondingColumnDefinition.isFilterable,
|
||||||
|
} as ColumnDefinition<FieldMetadata>;
|
||||||
})
|
})
|
||||||
.filter(isDefined);
|
.filter(isDefined);
|
||||||
|
|
||||||
|
@ -67,6 +67,7 @@ export {
|
|||||||
IconFileText,
|
IconFileText,
|
||||||
IconFileUpload,
|
IconFileUpload,
|
||||||
IconFileZip,
|
IconFileZip,
|
||||||
|
IconFilter,
|
||||||
IconFilterOff,
|
IconFilterOff,
|
||||||
IconFocusCentered,
|
IconFocusCentered,
|
||||||
IconForbid,
|
IconForbid,
|
||||||
@ -119,6 +120,7 @@ export {
|
|||||||
IconSearch,
|
IconSearch,
|
||||||
IconSend,
|
IconSend,
|
||||||
IconSettings,
|
IconSettings,
|
||||||
|
IconSortDescending,
|
||||||
IconTable,
|
IconTable,
|
||||||
IconTag,
|
IconTag,
|
||||||
IconTarget,
|
IconTarget,
|
||||||
|
Loading…
Reference in New Issue
Block a user