Refactored editable filter chip dropdown opening (#11765)
This PR is refactoring a part of the ongoing filter refactor that was blocking other refactor in that area. Precisely, the dropdown filter that was used with the editable filter chip was initialized by two conflicting useEffect, causing many unwanted and hard to tackle bugs when modifying other places in the code that used the same dropdown. We also remove a difficult to maintain pattern around onToggleColumnFilterComponentState, which was storing a click handler in a state, we want to avoid this pattern. The hook useHandleToggleColumnFilter is also removed and replaced by useOpenRecordFilterChipFromTableHeader. The code is now synchronous and starts from the user click event that is triggered on a table cell header filter button click. Also : - Created a useSetEditableFilterChipDropdownStates that allows to separate the code path of filter chip dropdown from the code path of view bar global filter dropdown (will be continued in another refactor) - Added useCreateEmptyFilterFromFieldMetadataItem to abstract empty filter creation when opening a filter dropdown (will be used for other refactor) - Created a useOpenDropdownFromOutside hook that will also be used for other refactor on filter - Deleted EditableFilterDropdownButtonEffect - Removed call to ViewBarFilterEffect (will be completely removed in other refactors)
This commit is contained in:
@ -8,6 +8,7 @@ import { RecordFilterGroup } from '@/object-record/record-filter-group/types/Rec
|
||||
import { RecordFilterGroupLogicalOperator } from '@/object-record/record-filter-group/types/RecordFilterGroupLogicalOperator';
|
||||
import { useUpsertRecordFilter } from '@/object-record/record-filter/hooks/useUpsertRecordFilter';
|
||||
import { RecordFilter } from '@/object-record/record-filter/types/RecordFilter';
|
||||
import { getDefaultSubFieldNameForCompositeFilterableFieldType } from '@/object-record/record-filter/utils/getDefaultSubFieldNameForCompositeFilterableFieldType';
|
||||
import { getRecordFilterOperands } from '@/object-record/record-filter/utils/getRecordFilterOperands';
|
||||
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
@ -61,6 +62,9 @@ export const AdvancedFilterAddFilterRuleSelect = ({
|
||||
defaultFieldMetadataItemForFilter.type,
|
||||
);
|
||||
|
||||
const defaultSubFieldName =
|
||||
getDefaultSubFieldNameForCompositeFilterableFieldType(filterType);
|
||||
|
||||
const newRecordFilter: RecordFilter = {
|
||||
id: v4(),
|
||||
fieldMetadataId: defaultFieldMetadataItemForFilter.id,
|
||||
@ -73,6 +77,7 @@ export const AdvancedFilterAddFilterRuleSelect = ({
|
||||
recordFilterGroupId: recordFilterGroup.id,
|
||||
positionInRecordFilterGroup: newPositionInRecordFilterGroup,
|
||||
label: defaultFieldMetadataItemForFilter.label,
|
||||
subFieldName: defaultSubFieldName,
|
||||
};
|
||||
|
||||
upsertRecordFilter(newRecordFilter);
|
||||
|
||||
@ -1,20 +1,17 @@
|
||||
import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById';
|
||||
import { availableFieldMetadataItemsForFilterFamilySelector } from '@/object-metadata/states/availableFieldMetadataItemsForFilterFamilySelector';
|
||||
import { getFilterTypeFromFieldType } from '@/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions';
|
||||
import { OBJECT_FILTER_DROPDOWN_ID } from '@/object-record/object-filter-dropdown/constants/ObjectFilterDropdownId';
|
||||
import { useUpsertRecordFilterGroup } from '@/object-record/record-filter-group/hooks/useUpsertRecordFilterGroup';
|
||||
import { currentRecordFilterGroupsComponentState } from '@/object-record/record-filter-group/states/currentRecordFilterGroupsComponentState';
|
||||
import { RecordFilterGroupLogicalOperator } from '@/object-record/record-filter-group/types/RecordFilterGroupLogicalOperator';
|
||||
import { useUpsertRecordFilter } from '@/object-record/record-filter/hooks/useUpsertRecordFilter';
|
||||
import { getRecordFilterOperands } from '@/object-record/record-filter/utils/getRecordFilterOperands';
|
||||
|
||||
import { useSetRecordFilterUsedInAdvancedFilterDropdownRow } from '@/object-record/advanced-filter/hooks/useSetRecordFilterUsedInAdvancedFilterDropdownRow';
|
||||
import { RecordFilter } from '@/object-record/record-filter/types/RecordFilter';
|
||||
import { RecordFilterGroupLogicalOperator } from '@/object-record/record-filter-group/types/RecordFilterGroupLogicalOperator';
|
||||
import { useCreateEmptyRecordFilterFromFieldMetadataItem } from '@/object-record/record-filter/hooks/useCreateEmptyRecordFilterFromFieldMetadataItem';
|
||||
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
import { ADVANCED_FILTER_DROPDOWN_ID } from '@/views/constants/AdvancedFilterDropdownId';
|
||||
import { useGetCurrentViewOnly } from '@/views/hooks/useGetCurrentViewOnly';
|
||||
import { ViewFilterGroupLogicalOperator } from '@/views/types/ViewFilterGroupLogicalOperator';
|
||||
import styled from '@emotion/styled';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
@ -85,6 +82,9 @@ export const AdvancedFilterButton = () => {
|
||||
const { setRecordFilterUsedInAdvancedFilterDropdownRow } =
|
||||
useSetRecordFilterUsedInAdvancedFilterDropdownRow();
|
||||
|
||||
const { createEmptyRecordFilterFromFieldMetadataItem } =
|
||||
useCreateEmptyRecordFilterFromFieldMetadataItem();
|
||||
|
||||
const handleClick = () => {
|
||||
if (!isDefined(currentView)) {
|
||||
throw new Error('Missing current view id');
|
||||
@ -96,13 +96,10 @@ export const AdvancedFilterButton = () => {
|
||||
const newRecordFilterGroup = {
|
||||
id: v4(),
|
||||
viewId: currentView.id,
|
||||
logicalOperator: ViewFilterGroupLogicalOperator.AND,
|
||||
logicalOperator: RecordFilterGroupLogicalOperator.AND,
|
||||
};
|
||||
|
||||
upsertRecordFilterGroup({
|
||||
id: newRecordFilterGroup.id,
|
||||
logicalOperator: RecordFilterGroupLogicalOperator.AND,
|
||||
});
|
||||
upsertRecordFilterGroup(newRecordFilterGroup);
|
||||
|
||||
const defaultFieldMetadataItem =
|
||||
availableFieldMetadataItemsForFilter.find(
|
||||
@ -115,25 +112,11 @@ export const AdvancedFilterButton = () => {
|
||||
throw new Error('Missing default filter definition');
|
||||
}
|
||||
|
||||
const filterType = getFilterTypeFromFieldType(
|
||||
defaultFieldMetadataItem.type,
|
||||
const { newRecordFilter } = createEmptyRecordFilterFromFieldMetadataItem(
|
||||
defaultFieldMetadataItem,
|
||||
);
|
||||
|
||||
const firstOperand = getRecordFilterOperands({
|
||||
filterType,
|
||||
})[0];
|
||||
|
||||
const newRecordFilter: RecordFilter = {
|
||||
id: v4(),
|
||||
fieldMetadataId: defaultFieldMetadataItem.id,
|
||||
operand: firstOperand,
|
||||
value: '',
|
||||
displayValue: '',
|
||||
recordFilterGroupId: newRecordFilterGroup.id,
|
||||
type: getFilterTypeFromFieldType(defaultFieldMetadataItem.type),
|
||||
label: defaultFieldMetadataItem.label,
|
||||
positionInRecordFilterGroup: 1,
|
||||
};
|
||||
newRecordFilter.recordFilterGroupId = newRecordFilterGroup.id;
|
||||
|
||||
upsertRecordFilter(newRecordFilter);
|
||||
|
||||
|
||||
@ -64,16 +64,9 @@ describe('getOperandsForFilterType', () => {
|
||||
['DATE', [...dateOperands, ...emptyOperands]],
|
||||
['DATE_TIME', [...dateOperands, ...emptyOperands]],
|
||||
['RELATION', [...relationOperand, ...emptyOperands]],
|
||||
[undefined, []],
|
||||
[null, []],
|
||||
['UNKNOWN_TYPE', []],
|
||||
] satisfies (
|
||||
| [
|
||||
FieldType | null | undefined | 'UNKNOWN_TYPE',
|
||||
RecordFilterOperand[],
|
||||
CompositeFieldSubFieldName,
|
||||
]
|
||||
| [FieldType | null | undefined | 'UNKNOWN_TYPE', RecordFilterOperand[]]
|
||||
| [FieldType, RecordFilterOperand[], CompositeFieldSubFieldName]
|
||||
| [FieldType, RecordFilterOperand[]]
|
||||
)[];
|
||||
|
||||
testCases.forEach(([filterType, expectedOperands, subFieldName]) => {
|
||||
|
||||
@ -0,0 +1,40 @@
|
||||
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { getFilterTypeFromFieldType } from '@/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions';
|
||||
import { RecordFilter } from '@/object-record/record-filter/types/RecordFilter';
|
||||
import { getDefaultSubFieldNameForCompositeFilterableFieldType } from '@/object-record/record-filter/utils/getDefaultSubFieldNameForCompositeFilterableFieldType';
|
||||
import { getRecordFilterOperands } from '@/object-record/record-filter/utils/getRecordFilterOperands';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
export const useCreateEmptyRecordFilterFromFieldMetadataItem = () => {
|
||||
const createEmptyRecordFilterFromFieldMetadataItem = (
|
||||
fieldMetadataItem: FieldMetadataItem,
|
||||
) => {
|
||||
const filterType = getFilterTypeFromFieldType(fieldMetadataItem.type);
|
||||
|
||||
const availableOperandsForFilter = getRecordFilterOperands({
|
||||
filterType,
|
||||
});
|
||||
|
||||
const defaultOperand = availableOperandsForFilter[0];
|
||||
|
||||
const defaultSubFieldName =
|
||||
getDefaultSubFieldNameForCompositeFilterableFieldType(filterType);
|
||||
|
||||
const newRecordFilter: RecordFilter = {
|
||||
id: v4(),
|
||||
fieldMetadataId: fieldMetadataItem.id,
|
||||
operand: defaultOperand,
|
||||
displayValue: '',
|
||||
label: fieldMetadataItem.label,
|
||||
type: filterType,
|
||||
value: '',
|
||||
subFieldName: defaultSubFieldName,
|
||||
};
|
||||
|
||||
return { newRecordFilter };
|
||||
};
|
||||
|
||||
return {
|
||||
createEmptyRecordFilterFromFieldMetadataItem,
|
||||
};
|
||||
};
|
||||
@ -4,6 +4,7 @@ import { FilterableFieldType } from '@/object-record/record-filter/types/Filtera
|
||||
import { CompositeFieldSubFieldName } from '@/settings/data-model/types/CompositeFieldSubFieldName';
|
||||
import { ViewFilterOperand as RecordFilterOperand } from '@/views/types/ViewFilterOperand';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { assertUnreachable } from 'twenty-shared/utils';
|
||||
|
||||
export type GetRecordFilterOperandsParams = {
|
||||
filterType: FilterableFieldType;
|
||||
@ -208,6 +209,6 @@ export const getRecordFilterOperands = ({
|
||||
case 'BOOLEAN':
|
||||
return FILTER_OPERANDS_MAP.BOOLEAN;
|
||||
default:
|
||||
return [];
|
||||
assertUnreachable(filterType, `Unknown filter type ${filterType}`);
|
||||
}
|
||||
};
|
||||
|
||||
@ -3,7 +3,6 @@ import { useEffect } from 'react';
|
||||
import { useColumnDefinitionsFromFieldMetadata } from '@/object-metadata/hooks/useColumnDefinitionsFromFieldMetadata';
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
|
||||
import { useHandleToggleColumnFilter } from '@/object-record/record-index/hooks/useHandleToggleColumnFilter';
|
||||
import { useHandleToggleColumnSort } from '@/object-record/record-index/hooks/useHandleToggleColumnSort';
|
||||
import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
|
||||
import { viewFieldAggregateOperationState } from '@/object-record/record-table/record-table-footer/states/viewFieldAggregateOperationState';
|
||||
@ -16,13 +15,7 @@ import { isDefined } from 'twenty-shared/utils';
|
||||
export const RecordIndexTableContainerEffect = () => {
|
||||
const { recordIndexId, objectNameSingular } = useRecordIndexContextOrThrow();
|
||||
|
||||
const viewBarId = recordIndexId;
|
||||
|
||||
const {
|
||||
setAvailableTableColumns,
|
||||
setOnToggleColumnFilter,
|
||||
setOnToggleColumnSort,
|
||||
} = useRecordTable({
|
||||
const { setAvailableTableColumns, setOnToggleColumnSort } = useRecordTable({
|
||||
recordTableId: recordIndexId,
|
||||
});
|
||||
|
||||
@ -37,24 +30,12 @@ export const RecordIndexTableContainerEffect = () => {
|
||||
setAvailableTableColumns(columnDefinitions);
|
||||
}, [columnDefinitions, setAvailableTableColumns]);
|
||||
|
||||
const handleToggleColumnFilter = useHandleToggleColumnFilter({
|
||||
objectNameSingular,
|
||||
viewBarId,
|
||||
});
|
||||
|
||||
const handleToggleColumnSort = useHandleToggleColumnSort({
|
||||
objectNameSingular,
|
||||
});
|
||||
|
||||
const { currentView } = useGetCurrentViewOnly();
|
||||
|
||||
useEffect(() => {
|
||||
setOnToggleColumnFilter(
|
||||
() => (fieldMetadataId: string) =>
|
||||
handleToggleColumnFilter(fieldMetadataId),
|
||||
);
|
||||
}, [setOnToggleColumnFilter, handleToggleColumnFilter]);
|
||||
|
||||
useEffect(() => {
|
||||
setOnToggleColumnSort(
|
||||
() => (fieldMetadataId: string) =>
|
||||
|
||||
@ -1,148 +0,0 @@
|
||||
import { useCallback } from 'react';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { useColumnDefinitionsFromFieldMetadata } from '@/object-metadata/hooks/useColumnDefinitionsFromFieldMetadata';
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
|
||||
import { availableFieldMetadataItemsForFilterFamilySelector } from '@/object-metadata/states/availableFieldMetadataItemsForFilterFamilySelector';
|
||||
import { getFilterTypeFromFieldType } from '@/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions';
|
||||
|
||||
import { useSelectFilterUsedInDropdown } from '@/object-record/object-filter-dropdown/hooks/useSelectFilterUsedInDropdown';
|
||||
import { useUpsertRecordFilter } from '@/object-record/record-filter/hooks/useUpsertRecordFilter';
|
||||
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
|
||||
import { RecordFilter } from '@/object-record/record-filter/types/RecordFilter';
|
||||
import { getDefaultSubFieldNameForCompositeFilterableFieldType } from '@/object-record/record-filter/utils/getDefaultSubFieldNameForCompositeFilterableFieldType';
|
||||
import { getRecordFilterOperands } from '@/object-record/record-filter/utils/getRecordFilterOperands';
|
||||
import { useSetActiveDropdownFocusIdAndMemorizePrevious } from '@/ui/layout/dropdown/hooks/useSetFocusedDropdownIdAndMemorizePrevious';
|
||||
import { isDropdownOpenComponentState } from '@/ui/layout/dropdown/states/isDropdownOpenComponentState';
|
||||
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
import { extractComponentState } from '@/ui/utilities/state/component-state/utils/extractComponentState';
|
||||
import { useRecoilCallback, useRecoilValue } from 'recoil';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
type UseHandleToggleColumnFilterProps = {
|
||||
objectNameSingular: string;
|
||||
viewBarId: string;
|
||||
};
|
||||
|
||||
export const useHandleToggleColumnFilter = ({
|
||||
objectNameSingular,
|
||||
viewBarId,
|
||||
}: UseHandleToggleColumnFilterProps) => {
|
||||
const { objectMetadataItem } = useObjectMetadataItem({
|
||||
objectNameSingular,
|
||||
});
|
||||
|
||||
const { columnDefinitions } =
|
||||
useColumnDefinitionsFromFieldMetadata(objectMetadataItem);
|
||||
|
||||
const { upsertRecordFilter } = useUpsertRecordFilter();
|
||||
|
||||
const { setActiveDropdownFocusIdAndMemorizePrevious } =
|
||||
useSetActiveDropdownFocusIdAndMemorizePrevious();
|
||||
|
||||
const { setHotkeyScopeAndMemorizePreviousScope } = usePreviousHotkeyScope();
|
||||
|
||||
const openDropdown = useRecoilCallback(
|
||||
({ set }) => {
|
||||
return (dropdownId: string) => {
|
||||
const dropdownOpenState = extractComponentState(
|
||||
isDropdownOpenComponentState,
|
||||
dropdownId,
|
||||
);
|
||||
|
||||
setActiveDropdownFocusIdAndMemorizePrevious(dropdownId);
|
||||
setHotkeyScopeAndMemorizePreviousScope(dropdownId);
|
||||
|
||||
set(dropdownOpenState, true);
|
||||
};
|
||||
},
|
||||
[
|
||||
setActiveDropdownFocusIdAndMemorizePrevious,
|
||||
setHotkeyScopeAndMemorizePreviousScope,
|
||||
],
|
||||
);
|
||||
|
||||
const availableFieldMetadataItemsForFilter = useRecoilValue(
|
||||
availableFieldMetadataItemsForFilterFamilySelector({
|
||||
objectMetadataItemId: objectMetadataItem.id,
|
||||
}),
|
||||
);
|
||||
|
||||
const { selectFilterUsedInDropdown } =
|
||||
useSelectFilterUsedInDropdown(viewBarId);
|
||||
|
||||
const currentRecordFilters = useRecoilComponentValueV2(
|
||||
currentRecordFiltersComponentState,
|
||||
);
|
||||
|
||||
const handleToggleColumnFilter = useCallback(
|
||||
async (fieldMetadataId: string) => {
|
||||
const correspondingColumnDefinition = columnDefinitions.find(
|
||||
(columnDefinition) =>
|
||||
columnDefinition.fieldMetadataId === fieldMetadataId,
|
||||
);
|
||||
|
||||
if (!isDefined(correspondingColumnDefinition)) return;
|
||||
|
||||
const newFilterId = v4();
|
||||
|
||||
const existingRecordFilter = currentRecordFilters.find(
|
||||
(recordFilter) => recordFilter.fieldMetadataId === fieldMetadataId,
|
||||
);
|
||||
|
||||
if (!isDefined(existingRecordFilter)) {
|
||||
const fieldMetadataItem = availableFieldMetadataItemsForFilter.find(
|
||||
(fieldMetadataItemToFind) =>
|
||||
fieldMetadataItemToFind.id === fieldMetadataId,
|
||||
);
|
||||
|
||||
if (!isDefined(fieldMetadataItem)) {
|
||||
throw new Error('Field metadata item not found');
|
||||
}
|
||||
|
||||
const filterType = getFilterTypeFromFieldType(fieldMetadataItem.type);
|
||||
|
||||
const defaultSubFieldName =
|
||||
getDefaultSubFieldNameForCompositeFilterableFieldType(
|
||||
fieldMetadataItem.type,
|
||||
);
|
||||
|
||||
const availableOperandsForFilter = getRecordFilterOperands({
|
||||
filterType,
|
||||
subFieldName: defaultSubFieldName,
|
||||
});
|
||||
|
||||
const defaultOperand = availableOperandsForFilter[0];
|
||||
|
||||
const newFilter: RecordFilter = {
|
||||
id: newFilterId,
|
||||
fieldMetadataId,
|
||||
operand: defaultOperand,
|
||||
displayValue: '',
|
||||
label: fieldMetadataItem.label,
|
||||
type: filterType,
|
||||
value: '',
|
||||
subFieldName: defaultSubFieldName,
|
||||
};
|
||||
|
||||
upsertRecordFilter(newFilter);
|
||||
|
||||
selectFilterUsedInDropdown({ fieldMetadataItemId: fieldMetadataId });
|
||||
}
|
||||
|
||||
openDropdown(existingRecordFilter?.id ?? newFilterId);
|
||||
},
|
||||
[
|
||||
openDropdown,
|
||||
columnDefinitions,
|
||||
selectFilterUsedInDropdown,
|
||||
currentRecordFilters,
|
||||
availableFieldMetadataItemsForFilter,
|
||||
upsertRecordFilter,
|
||||
],
|
||||
);
|
||||
|
||||
return handleToggleColumnFilter;
|
||||
};
|
||||
@ -19,7 +19,7 @@ import { RecordTableComponentInstanceContext } from '@/object-record/record-tabl
|
||||
import { isRecordTableInitialLoadingComponentState } from '@/object-record/record-table/states/isRecordTableInitialLoadingComponentState';
|
||||
import { onColumnsChangeComponentState } from '@/object-record/record-table/states/onColumnsChangeComponentState';
|
||||
import { onEntityCountChangeComponentState } from '@/object-record/record-table/states/onEntityCountChangeComponentState';
|
||||
import { onToggleColumnFilterComponentState } from '@/object-record/record-table/states/onToggleColumnFilterComponentState';
|
||||
|
||||
import { onToggleColumnSortComponentState } from '@/object-record/record-table/states/onToggleColumnSortComponentState';
|
||||
import { tableLastRowVisibleComponentState } from '@/object-record/record-table/states/tableLastRowVisibleComponentState';
|
||||
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
||||
@ -73,10 +73,6 @@ export const useRecordTable = (props?: useRecordTableProps) => {
|
||||
recordTableId,
|
||||
);
|
||||
|
||||
const setOnToggleColumnFilter = useSetRecoilComponentStateV2(
|
||||
onToggleColumnFilterComponentState,
|
||||
recordTableId,
|
||||
);
|
||||
const setOnToggleColumnSort = useSetRecoilComponentStateV2(
|
||||
onToggleColumnSortComponentState,
|
||||
recordTableId,
|
||||
@ -226,7 +222,6 @@ export const useRecordTable = (props?: useRecordTableProps) => {
|
||||
setRecordTableLastRowVisible,
|
||||
setFocusPosition,
|
||||
setHasUserSelectedAllRows,
|
||||
setOnToggleColumnFilter,
|
||||
setOnToggleColumnSort,
|
||||
};
|
||||
};
|
||||
|
||||
@ -3,7 +3,7 @@ import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/Drop
|
||||
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
|
||||
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
|
||||
|
||||
import { onToggleColumnFilterComponentState } from '@/object-record/record-table/states/onToggleColumnFilterComponentState';
|
||||
import { useOpenRecordFilterChipFromTableHeader } from '@/object-record/record-table/record-table-header/hooks/useOpenRecordFilterChipFromTableHeader';
|
||||
import { onToggleColumnSortComponentState } from '@/object-record/record-table/states/onToggleColumnSortComponentState';
|
||||
import { visibleTableColumnsComponentSelector } from '@/object-record/record-table/states/selectors/visibleTableColumnsComponentSelector';
|
||||
import { useToggleScrollWrapper } from '@/ui/utilities/scroll/hooks/useToggleScrollWrapper';
|
||||
@ -76,9 +76,6 @@ export const RecordTableColumnHeadDropdownMenu = ({
|
||||
handleColumnVisibilityChange(column);
|
||||
};
|
||||
|
||||
const onToggleColumnFilter = useRecoilComponentValueV2(
|
||||
onToggleColumnFilterComponentState,
|
||||
);
|
||||
const onToggleColumnSort = useRecoilComponentValueV2(
|
||||
onToggleColumnSortComponentState,
|
||||
);
|
||||
@ -89,10 +86,13 @@ export const RecordTableColumnHeadDropdownMenu = ({
|
||||
onToggleColumnSort?.(column.fieldMetadataId);
|
||||
};
|
||||
|
||||
const { openRecordFilterChipFromTableHeader } =
|
||||
useOpenRecordFilterChipFromTableHeader();
|
||||
|
||||
const handleFilterClick = () => {
|
||||
closeDropdownAndToggleScroll();
|
||||
|
||||
onToggleColumnFilter?.(column.fieldMetadataId);
|
||||
openRecordFilterChipFromTableHeader(column.fieldMetadataId);
|
||||
};
|
||||
|
||||
const isSortable = column.isSortable === true;
|
||||
|
||||
@ -0,0 +1,70 @@
|
||||
import { useSelectFilterUsedInDropdown } from '@/object-record/object-filter-dropdown/hooks/useSelectFilterUsedInDropdown';
|
||||
import { useCreateEmptyRecordFilterFromFieldMetadataItem } from '@/object-record/record-filter/hooks/useCreateEmptyRecordFilterFromFieldMetadataItem';
|
||||
import { useFilterableFieldMetadataItemsInRecordIndexContext } from '@/object-record/record-filter/hooks/useFilterableFieldMetadataItemsInRecordIndexContext';
|
||||
import { useUpsertRecordFilter } from '@/object-record/record-filter/hooks/useUpsertRecordFilter';
|
||||
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
|
||||
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
|
||||
import { useOpenDropdownFromOutside } from '@/ui/layout/dropdown/hooks/useOpenDropdownFromOutside';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
import { useSetEditableFilterChipDropdownStates } from '@/views/hooks/useSetEditableFilterChipDropdownStates';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const useOpenRecordFilterChipFromTableHeader = () => {
|
||||
const { recordIndexId } = useRecordIndexContextOrThrow();
|
||||
|
||||
const { filterableFieldMetadataItems } =
|
||||
useFilterableFieldMetadataItemsInRecordIndexContext();
|
||||
|
||||
const { selectFilterUsedInDropdown } =
|
||||
useSelectFilterUsedInDropdown(recordIndexId);
|
||||
|
||||
const currentRecordFilters = useRecoilComponentValueV2(
|
||||
currentRecordFiltersComponentState,
|
||||
);
|
||||
|
||||
const { createEmptyRecordFilterFromFieldMetadataItem } =
|
||||
useCreateEmptyRecordFilterFromFieldMetadataItem();
|
||||
|
||||
const { upsertRecordFilter } = useUpsertRecordFilter();
|
||||
|
||||
const { openDropdownFromOutside } = useOpenDropdownFromOutside();
|
||||
|
||||
const { setEditableFilterChipDropdownStates } =
|
||||
useSetEditableFilterChipDropdownStates();
|
||||
|
||||
const openRecordFilterChipFromTableHeader = (fieldMetadataItemId: string) => {
|
||||
const correspondingFieldMetadataItem = filterableFieldMetadataItems.find(
|
||||
(fieldMetadataItemToFind) =>
|
||||
fieldMetadataItemToFind.id === fieldMetadataItemId,
|
||||
);
|
||||
|
||||
if (!isDefined(correspondingFieldMetadataItem)) {
|
||||
throw new Error(
|
||||
`Cannot find field metadata item with id : ${fieldMetadataItemId}`,
|
||||
);
|
||||
}
|
||||
|
||||
const existingRecordFilter = currentRecordFilters.find(
|
||||
(recordFilter) => recordFilter.fieldMetadataId === fieldMetadataItemId,
|
||||
);
|
||||
|
||||
if (isDefined(existingRecordFilter)) {
|
||||
setEditableFilterChipDropdownStates(existingRecordFilter);
|
||||
openDropdownFromOutside(existingRecordFilter.id);
|
||||
return;
|
||||
}
|
||||
|
||||
const { newRecordFilter } = createEmptyRecordFilterFromFieldMetadataItem(
|
||||
correspondingFieldMetadataItem,
|
||||
);
|
||||
|
||||
upsertRecordFilter(newRecordFilter);
|
||||
|
||||
selectFilterUsedInDropdown({ fieldMetadataItemId });
|
||||
|
||||
setEditableFilterChipDropdownStates(newRecordFilter);
|
||||
openDropdownFromOutside(newRecordFilter.id);
|
||||
};
|
||||
|
||||
return { openRecordFilterChipFromTableHeader };
|
||||
};
|
||||
@ -1,10 +0,0 @@
|
||||
import { RecordTableComponentInstanceContext } from '@/object-record/record-table/states/context/RecordTableComponentInstanceContext';
|
||||
import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2';
|
||||
|
||||
export const onToggleColumnFilterComponentState = createComponentStateV2<
|
||||
((fieldMetadataId: string) => void) | undefined
|
||||
>({
|
||||
key: 'onToggleColumnFilterComponentState',
|
||||
defaultValue: undefined,
|
||||
componentInstanceContext: RecordTableComponentInstanceContext,
|
||||
});
|
||||
@ -0,0 +1,34 @@
|
||||
import { useSetActiveDropdownFocusIdAndMemorizePrevious } from '@/ui/layout/dropdown/hooks/useSetFocusedDropdownIdAndMemorizePrevious';
|
||||
import { isDropdownOpenComponentState } from '@/ui/layout/dropdown/states/isDropdownOpenComponentState';
|
||||
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
|
||||
import { extractComponentState } from '@/ui/utilities/state/component-state/utils/extractComponentState';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
export const useOpenDropdownFromOutside = () => {
|
||||
const { setActiveDropdownFocusIdAndMemorizePrevious } =
|
||||
useSetActiveDropdownFocusIdAndMemorizePrevious();
|
||||
|
||||
const { setHotkeyScopeAndMemorizePreviousScope } = usePreviousHotkeyScope();
|
||||
|
||||
const openDropdownFromOutside = useRecoilCallback(
|
||||
({ set }) => {
|
||||
return (dropdownId: string) => {
|
||||
const dropdownOpenState = extractComponentState(
|
||||
isDropdownOpenComponentState,
|
||||
dropdownId,
|
||||
);
|
||||
|
||||
setActiveDropdownFocusIdAndMemorizePrevious(dropdownId);
|
||||
setHotkeyScopeAndMemorizePreviousScope(dropdownId);
|
||||
|
||||
set(dropdownOpenState, true);
|
||||
};
|
||||
},
|
||||
[
|
||||
setActiveDropdownFocusIdAndMemorizePrevious,
|
||||
setHotkeyScopeAndMemorizePreviousScope,
|
||||
],
|
||||
);
|
||||
|
||||
return { openDropdownFromOutside };
|
||||
};
|
||||
@ -13,11 +13,13 @@ import { useIcons } from 'twenty-ui/display';
|
||||
type EditableFilterChipProps = {
|
||||
recordFilter: RecordFilter;
|
||||
onRemove: () => void;
|
||||
onClick?: () => void;
|
||||
};
|
||||
|
||||
export const EditableFilterChip = ({
|
||||
recordFilter,
|
||||
onRemove,
|
||||
onClick,
|
||||
}: EditableFilterChipProps) => {
|
||||
const { getIcon } = useIcons();
|
||||
|
||||
@ -58,6 +60,7 @@ export const EditableFilterChip = ({
|
||||
labelValue={recordFilter.displayValue}
|
||||
Icon={FieldMetadataItemIcon}
|
||||
onRemove={onRemove}
|
||||
onClick={onClick}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@ -9,7 +9,7 @@ import { EditableFilterChip } from '@/views/components/EditableFilterChip';
|
||||
import { ObjectFilterOperandSelectAndInput } from '@/object-record/object-filter-dropdown/components/ObjectFilterOperandSelectAndInput';
|
||||
import { useRemoveRecordFilter } from '@/object-record/record-filter/hooks/useRemoveRecordFilter';
|
||||
import { isRecordFilterConsideredEmpty } from '@/object-record/record-filter/utils/isRecordFilterConsideredEmpty';
|
||||
import { EditableFilterDropdownButtonEffect } from '@/views/components/EditableFilterDropdownButtonEffect';
|
||||
import { useSetEditableFilterChipDropdownStates } from '@/views/hooks/useSetEditableFilterChipDropdownStates';
|
||||
|
||||
type EditableFilterDropdownButtonProps = {
|
||||
recordFilter: RecordFilter;
|
||||
@ -38,15 +38,22 @@ export const EditableFilterDropdownButton = ({
|
||||
}
|
||||
}, [recordFilter, removeRecordFilter]);
|
||||
|
||||
const { setEditableFilterChipDropdownStates } =
|
||||
useSetEditableFilterChipDropdownStates();
|
||||
|
||||
const handleFilterChipClick = () => {
|
||||
setEditableFilterChipDropdownStates(recordFilter);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<EditableFilterDropdownButtonEffect recordFilter={recordFilter} />
|
||||
<Dropdown
|
||||
dropdownId={recordFilter.id}
|
||||
clickableComponent={
|
||||
<EditableFilterChip
|
||||
recordFilter={recordFilter}
|
||||
onRemove={handleRemove}
|
||||
onClick={handleFilterChipClick}
|
||||
/>
|
||||
}
|
||||
dropdownComponents={
|
||||
|
||||
@ -1,81 +0,0 @@
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { fieldMetadataItemIdUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/fieldMetadataItemIdUsedInDropdownComponentState';
|
||||
|
||||
import { objectFilterDropdownSelectedRecordIdsComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownSelectedRecordIdsComponentState';
|
||||
import { selectedFilterComponentState } from '@/object-record/object-filter-dropdown/states/selectedFilterComponentState';
|
||||
import { selectedOperandInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/selectedOperandInDropdownComponentState';
|
||||
import { subFieldNameUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/subFieldNameUsedInDropdownComponentState';
|
||||
import { useFilterableFieldMetadataItemsInRecordIndexContext } from '@/object-record/record-filter/hooks/useFilterableFieldMetadataItemsInRecordIndexContext';
|
||||
import { RecordFilter } from '@/object-record/record-filter/types/RecordFilter';
|
||||
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
type EditableFilterDropdownButtonEffectProps = {
|
||||
recordFilter: RecordFilter;
|
||||
};
|
||||
|
||||
export const EditableFilterDropdownButtonEffect = ({
|
||||
recordFilter,
|
||||
}: EditableFilterDropdownButtonEffectProps) => {
|
||||
const setFieldMetadataItemIdUsedInDropdown = useSetRecoilComponentStateV2(
|
||||
fieldMetadataItemIdUsedInDropdownComponentState,
|
||||
);
|
||||
|
||||
const setSelectedOperandInDropdown = useSetRecoilComponentStateV2(
|
||||
selectedOperandInDropdownComponentState,
|
||||
recordFilter.id,
|
||||
);
|
||||
|
||||
const setSubFieldNameUsedInDropdown = useSetRecoilComponentStateV2(
|
||||
subFieldNameUsedInDropdownComponentState,
|
||||
recordFilter.id,
|
||||
);
|
||||
|
||||
const setSelectedFilter = useSetRecoilComponentStateV2(
|
||||
selectedFilterComponentState,
|
||||
recordFilter.id,
|
||||
);
|
||||
|
||||
const setObjectFilterDropdownSelectedRecordIds = useSetRecoilComponentStateV2(
|
||||
objectFilterDropdownSelectedRecordIdsComponentState,
|
||||
recordFilter.id,
|
||||
);
|
||||
|
||||
const { filterableFieldMetadataItems } =
|
||||
useFilterableFieldMetadataItemsInRecordIndexContext();
|
||||
|
||||
useEffect(() => {
|
||||
const fieldMetadataItem = filterableFieldMetadataItems.find(
|
||||
(fieldMetadataItem) =>
|
||||
fieldMetadataItem.id === recordFilter.fieldMetadataId,
|
||||
);
|
||||
|
||||
if (!isDefined(fieldMetadataItem)) {
|
||||
return;
|
||||
}
|
||||
|
||||
setFieldMetadataItemIdUsedInDropdown(fieldMetadataItem.id);
|
||||
setSelectedOperandInDropdown(recordFilter.operand);
|
||||
setSelectedFilter(recordFilter);
|
||||
setSubFieldNameUsedInDropdown(recordFilter.subFieldName);
|
||||
|
||||
try {
|
||||
const selectedOptions = JSON.parse(recordFilter.value);
|
||||
|
||||
setObjectFilterDropdownSelectedRecordIds(selectedOptions);
|
||||
} catch {
|
||||
setObjectFilterDropdownSelectedRecordIds([]);
|
||||
}
|
||||
}, [
|
||||
filterableFieldMetadataItems,
|
||||
setFieldMetadataItemIdUsedInDropdown,
|
||||
recordFilter,
|
||||
setSelectedOperandInDropdown,
|
||||
setSelectedFilter,
|
||||
setSubFieldNameUsedInDropdown,
|
||||
setObjectFilterDropdownSelectedRecordIds,
|
||||
]);
|
||||
|
||||
return null;
|
||||
};
|
||||
@ -10,7 +10,6 @@ import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/
|
||||
import { AdvancedFilterDropdownButton } from '@/views/components/AdvancedFilterDropdownButton';
|
||||
import { EditableFilterDropdownButton } from '@/views/components/EditableFilterDropdownButton';
|
||||
import { EditableSortChip } from '@/views/components/EditableSortChip';
|
||||
import { ViewBarFilterEffect } from '@/views/components/ViewBarFilterEffect';
|
||||
import { useViewFromQueryParams } from '@/views/hooks/internal/useViewFromQueryParams';
|
||||
|
||||
import { useCheckIsSoftDeleteFilter } from '@/object-record/record-filter/hooks/useCheckIsSoftDeleteFilter';
|
||||
@ -232,7 +231,6 @@ export const ViewBarDetails = ({
|
||||
value={{ instanceId: recordFilter.id }}
|
||||
>
|
||||
<DropdownScope dropdownScopeId={recordFilter.id}>
|
||||
<ViewBarFilterEffect filterDropdownId={recordFilter.id} />
|
||||
<EditableFilterDropdownButton
|
||||
recordFilter={recordFilter}
|
||||
hotkeyScope={{
|
||||
|
||||
@ -0,0 +1,104 @@
|
||||
import { fieldMetadataItemIdUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/fieldMetadataItemIdUsedInDropdownComponentState';
|
||||
import { objectFilterDropdownSelectedOptionValuesComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownSelectedOptionValuesComponentState';
|
||||
import { objectFilterDropdownSelectedRecordIdsComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownSelectedRecordIdsComponentState';
|
||||
import { selectedFilterComponentState } from '@/object-record/object-filter-dropdown/states/selectedFilterComponentState';
|
||||
import { selectedOperandInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/selectedOperandInDropdownComponentState';
|
||||
import { subFieldNameUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/subFieldNameUsedInDropdownComponentState';
|
||||
import { useFilterableFieldMetadataItemsInRecordIndexContext } from '@/object-record/record-filter/hooks/useFilterableFieldMetadataItemsInRecordIndexContext';
|
||||
import { RecordFilter } from '@/object-record/record-filter/types/RecordFilter';
|
||||
import { jsonRelationFilterValueSchema } from '@/views/view-filter-value/validation-schemas/jsonRelationFilterValueSchema';
|
||||
import { simpleRelationFilterValueSchema } from '@/views/view-filter-value/validation-schemas/simpleRelationFilterValueSchema';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const useSetEditableFilterChipDropdownStates = () => {
|
||||
const { filterableFieldMetadataItems } =
|
||||
useFilterableFieldMetadataItemsInRecordIndexContext();
|
||||
|
||||
const setEditableFilterChipDropdownStates = useRecoilCallback(
|
||||
({ set }) =>
|
||||
(recordFilter: RecordFilter) => {
|
||||
const fieldMetadataItem = filterableFieldMetadataItems.find(
|
||||
(fieldMetadataItem) =>
|
||||
fieldMetadataItem.id === recordFilter.fieldMetadataId,
|
||||
);
|
||||
|
||||
if (!isDefined(fieldMetadataItem)) {
|
||||
return;
|
||||
}
|
||||
|
||||
set(
|
||||
fieldMetadataItemIdUsedInDropdownComponentState.atomFamily({
|
||||
instanceId: recordFilter.id,
|
||||
}),
|
||||
fieldMetadataItem.id,
|
||||
);
|
||||
|
||||
set(
|
||||
selectedOperandInDropdownComponentState.atomFamily({
|
||||
instanceId: recordFilter.id,
|
||||
}),
|
||||
recordFilter.operand,
|
||||
);
|
||||
|
||||
set(
|
||||
selectedFilterComponentState.atomFamily({
|
||||
instanceId: recordFilter.id,
|
||||
}),
|
||||
recordFilter,
|
||||
);
|
||||
|
||||
set(
|
||||
subFieldNameUsedInDropdownComponentState.atomFamily({
|
||||
instanceId: recordFilter.id,
|
||||
}),
|
||||
recordFilter.subFieldName,
|
||||
);
|
||||
|
||||
if (recordFilter.type === 'RELATION') {
|
||||
const { selectedRecordIds } = jsonRelationFilterValueSchema
|
||||
.catch({
|
||||
isCurrentWorkspaceMemberSelected: false,
|
||||
selectedRecordIds: simpleRelationFilterValueSchema.parse(
|
||||
recordFilter.value,
|
||||
),
|
||||
})
|
||||
.parse(recordFilter.value);
|
||||
|
||||
set(
|
||||
objectFilterDropdownSelectedRecordIdsComponentState.atomFamily({
|
||||
instanceId: recordFilter.id,
|
||||
}),
|
||||
selectedRecordIds,
|
||||
);
|
||||
} else if (['SELECT', 'MULTI_SELECT'].includes(recordFilter.type)) {
|
||||
try {
|
||||
const selectedOptions = JSON.parse(recordFilter.value);
|
||||
|
||||
set(
|
||||
objectFilterDropdownSelectedOptionValuesComponentState.atomFamily(
|
||||
{
|
||||
instanceId: recordFilter.id,
|
||||
},
|
||||
),
|
||||
selectedOptions,
|
||||
);
|
||||
} catch {
|
||||
set(
|
||||
objectFilterDropdownSelectedOptionValuesComponentState.atomFamily(
|
||||
{
|
||||
instanceId: recordFilter.id,
|
||||
},
|
||||
),
|
||||
[],
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
[filterableFieldMetadataItems],
|
||||
);
|
||||
|
||||
return {
|
||||
setEditableFilterChipDropdownStates,
|
||||
};
|
||||
};
|
||||
@ -9,7 +9,7 @@ export const areViewFilterGroupsEqual = (
|
||||
'positionInViewFilterGroup',
|
||||
'logicalOperator',
|
||||
'parentViewFilterGroupId',
|
||||
'viewId',
|
||||
'id',
|
||||
];
|
||||
|
||||
return propertiesToCompare.every((property) =>
|
||||
|
||||
Reference in New Issue
Block a user