Refactored table filters to consume new currentRecordFilters component state (#9652)
This PR implements a first real use case, now currentRecordFilters component state acts as the global record filter reference. It is set by the view initially and can be reset to view filters state at any point. This new state is also modified by two new upsertRecordFilter / removeRecordFilter hooks that will be drop-in replacement of the actual upsertCombinedViewFilter and removeCombinediewFilter hooks. This PR implements the logic to manipulate record filters but only reads it to make the table find many request, all other features are still relying on the old view filter implementation. Advanced filters are ignored because they are hidden and because this effort is made precisely to allow the completion of the advanced filters feature.
This commit is contained in:
@ -9,6 +9,7 @@ import { selectedFilterComponentState } from '@/object-record/object-filter-drop
|
||||
import { selectedOperandInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/selectedOperandInDropdownComponentState';
|
||||
import { getActorSourceMultiSelectOptions } from '@/object-record/object-filter-dropdown/utils/getActorSourceMultiSelectOptions';
|
||||
import { useApplyRecordFilter } from '@/object-record/record-filter/hooks/useApplyRecordFilter';
|
||||
import { useRemoveRecordFilter } from '@/object-record/record-filter/hooks/useRemoveRecordFilter';
|
||||
import { RelationPickerHotkeyScope } from '@/object-record/relation-picker/types/RelationPickerHotkeyScope';
|
||||
import { MultipleSelectDropdown } from '@/object-record/select/components/MultipleSelectDropdown';
|
||||
import { SelectableItem } from '@/object-record/select/types/SelectableItem';
|
||||
@ -61,6 +62,7 @@ export const ObjectFilterDropdownSourceSelect = ({
|
||||
const { currentViewWithCombinedFiltersAndSorts } =
|
||||
useGetCurrentView(viewComponentId);
|
||||
|
||||
// TODO: this should be removed as it is not consistent across re-renders
|
||||
const [fieldId] = useState(v4());
|
||||
|
||||
const sourceTypes = getActorSourceMultiSelectOptions(
|
||||
@ -73,6 +75,8 @@ export const ObjectFilterDropdownSourceSelect = ({
|
||||
|
||||
const { emptyRecordFilter } = useEmptyRecordFilter();
|
||||
|
||||
const { removeRecordFilter } = useRemoveRecordFilter();
|
||||
|
||||
const handleMultipleItemSelectChange = (
|
||||
itemToSelect: SelectableItem,
|
||||
newSelectedValue: boolean,
|
||||
@ -83,8 +87,13 @@ export const ObjectFilterDropdownSourceSelect = ({
|
||||
(id) => id !== itemToSelect.id,
|
||||
);
|
||||
|
||||
if (!filterDefinitionUsedInDropdown) {
|
||||
throw new Error('Filter definition used in dropdown should be defined');
|
||||
}
|
||||
|
||||
if (newSelectedItemIds.length === 0) {
|
||||
emptyRecordFilter();
|
||||
removeRecordFilter(filterDefinitionUsedInDropdown.fieldMetadataId);
|
||||
deleteCombinedViewFilter(fieldId);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -5,6 +5,7 @@ import { CoreObjectNamePlural } from '@/object-metadata/types/CoreObjectNamePlur
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { MultipleFiltersDropdownButton } from '@/object-record/object-filter-dropdown/components/MultipleFiltersDropdownButton';
|
||||
import { ObjectFilterDropdownComponentInstanceContext } from '@/object-record/object-filter-dropdown/states/contexts/ObjectFilterDropdownComponentInstanceContext';
|
||||
import { RecordFiltersComponentInstanceContext } from '@/object-record/record-filter/states/context/RecordFiltersComponentInstanceContext';
|
||||
import { RecordIndexContextProvider } from '@/object-record/record-index/contexts/RecordIndexContext';
|
||||
import { RecordTableComponentInstanceContext } from '@/object-record/record-table/states/context/RecordTableComponentInstanceContext';
|
||||
import { tableColumnsComponentState } from '@/object-record/record-table/states/tableColumnsComponentState';
|
||||
@ -107,17 +108,21 @@ const meta: Meta<typeof MultipleFiltersDropdownButton> = {
|
||||
recordIndexId: instanceId,
|
||||
}}
|
||||
>
|
||||
<ObjectFilterDropdownComponentInstanceContext.Provider
|
||||
<RecordFiltersComponentInstanceContext.Provider
|
||||
value={{ instanceId }}
|
||||
>
|
||||
<RecordTableComponentInstanceContext.Provider
|
||||
value={{ instanceId: instanceId, onColumnsChange: () => {} }}
|
||||
<ObjectFilterDropdownComponentInstanceContext.Provider
|
||||
value={{ instanceId }}
|
||||
>
|
||||
<ViewComponentInstanceContext.Provider value={{ instanceId }}>
|
||||
<Story />
|
||||
</ViewComponentInstanceContext.Provider>
|
||||
</RecordTableComponentInstanceContext.Provider>
|
||||
</ObjectFilterDropdownComponentInstanceContext.Provider>
|
||||
<RecordTableComponentInstanceContext.Provider
|
||||
value={{ instanceId: instanceId, onColumnsChange: () => {} }}
|
||||
>
|
||||
<ViewComponentInstanceContext.Provider value={{ instanceId }}>
|
||||
<Story />
|
||||
</ViewComponentInstanceContext.Provider>
|
||||
</RecordTableComponentInstanceContext.Provider>
|
||||
</ObjectFilterDropdownComponentInstanceContext.Provider>
|
||||
</RecordFiltersComponentInstanceContext.Provider>
|
||||
</RecordIndexContextProvider>
|
||||
);
|
||||
},
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { ObjectFilterDropdownComponentInstanceContext } from '@/object-record/object-filter-dropdown/states/contexts/ObjectFilterDropdownComponentInstanceContext';
|
||||
import { RecordFilterDefinition } from '@/object-record/record-filter/types/RecordFilterDefinition';
|
||||
|
||||
import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2';
|
||||
|
||||
export const availableFilterDefinitionsComponentState = createComponentStateV2<
|
||||
|
||||
@ -7,6 +7,7 @@ import { ObjectOptionsDropdownContent } from '@/object-record/object-options-dro
|
||||
import { OBJECT_OPTIONS_DROPDOWN_ID } from '@/object-record/object-options-dropdown/constants/ObjectOptionsDropdownId';
|
||||
import { ObjectOptionsDropdownContext } from '@/object-record/object-options-dropdown/states/contexts/ObjectOptionsDropdownContext';
|
||||
import { ObjectOptionsContentId } from '@/object-record/object-options-dropdown/types/ObjectOptionsContentId';
|
||||
import { RecordFiltersComponentInstanceContext } from '@/object-record/record-filter/states/context/RecordFiltersComponentInstanceContext';
|
||||
import { RecordIndexContextProvider } from '@/object-record/record-index/contexts/RecordIndexContext';
|
||||
import { RecordTableComponentInstanceContext } from '@/object-record/record-table/states/context/RecordTableComponentInstanceContext';
|
||||
import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu';
|
||||
@ -37,22 +38,26 @@ const meta: Meta<typeof ObjectOptionsDropdownContent> = {
|
||||
}, [setObjectMetadataItems]);
|
||||
|
||||
return (
|
||||
<RecordTableComponentInstanceContext.Provider
|
||||
value={{ instanceId, onColumnsChange: () => {} }}
|
||||
<RecordFiltersComponentInstanceContext.Provider
|
||||
value={{ instanceId: 'object-options-dropdown' }}
|
||||
>
|
||||
<ViewComponentInstanceContext.Provider value={{ instanceId }}>
|
||||
<ContextStoreComponentInstanceContext.Provider
|
||||
value={{ instanceId }}
|
||||
>
|
||||
<MemoryRouter
|
||||
initialEntries={['/one', '/two', { pathname: '/three' }]}
|
||||
initialIndex={1}
|
||||
<RecordTableComponentInstanceContext.Provider
|
||||
value={{ instanceId, onColumnsChange: () => {} }}
|
||||
>
|
||||
<ViewComponentInstanceContext.Provider value={{ instanceId }}>
|
||||
<ContextStoreComponentInstanceContext.Provider
|
||||
value={{ instanceId }}
|
||||
>
|
||||
<Story />
|
||||
</MemoryRouter>
|
||||
</ContextStoreComponentInstanceContext.Provider>
|
||||
</ViewComponentInstanceContext.Provider>
|
||||
</RecordTableComponentInstanceContext.Provider>
|
||||
<MemoryRouter
|
||||
initialEntries={['/one', '/two', { pathname: '/three' }]}
|
||||
initialIndex={1}
|
||||
>
|
||||
<Story />
|
||||
</MemoryRouter>
|
||||
</ContextStoreComponentInstanceContext.Provider>
|
||||
</ViewComponentInstanceContext.Provider>
|
||||
</RecordTableComponentInstanceContext.Provider>
|
||||
</RecordFiltersComponentInstanceContext.Provider>
|
||||
);
|
||||
},
|
||||
ObjectMetadataItemsDecorator,
|
||||
|
||||
@ -0,0 +1,117 @@
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
|
||||
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
|
||||
import { RecordFilter } from '@/object-record/record-filter/types/RecordFilter';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
|
||||
import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper';
|
||||
import { useRemoveRecordFilter } from '../useRemoveRecordFilter';
|
||||
import { useUpsertRecordFilter } from '../useUpsertRecordFilter';
|
||||
|
||||
const Wrapper = getJestMetadataAndApolloMocksWrapper({
|
||||
apolloMocks: [],
|
||||
});
|
||||
|
||||
describe('useRemoveRecordFilter', () => {
|
||||
it('should remove an existing filter', () => {
|
||||
const { result } = renderHook(
|
||||
() => {
|
||||
const currentRecordFilters = useRecoilComponentValueV2(
|
||||
currentRecordFiltersComponentState,
|
||||
);
|
||||
|
||||
const { upsertRecordFilter } = useUpsertRecordFilter();
|
||||
const { removeRecordFilter } = useRemoveRecordFilter();
|
||||
|
||||
return {
|
||||
upsertRecordFilter,
|
||||
removeRecordFilter,
|
||||
currentRecordFilters,
|
||||
};
|
||||
},
|
||||
{
|
||||
wrapper: Wrapper,
|
||||
},
|
||||
);
|
||||
|
||||
const filter: RecordFilter = {
|
||||
id: 'filter-1',
|
||||
fieldMetadataId: 'field-1',
|
||||
value: 'test-value',
|
||||
operand: ViewFilterOperand.Contains,
|
||||
displayValue: 'test-value',
|
||||
definition: {
|
||||
type: 'TEXT',
|
||||
fieldMetadataId: 'field-1',
|
||||
label: 'Test Field',
|
||||
iconName: 'IconText',
|
||||
},
|
||||
};
|
||||
|
||||
// First add a filter
|
||||
act(() => {
|
||||
result.current.upsertRecordFilter(filter);
|
||||
});
|
||||
|
||||
expect(result.current.currentRecordFilters).toHaveLength(1);
|
||||
expect(result.current.currentRecordFilters[0]).toEqual(filter);
|
||||
|
||||
// Then remove it
|
||||
act(() => {
|
||||
result.current.removeRecordFilter(filter.fieldMetadataId);
|
||||
});
|
||||
|
||||
expect(result.current.currentRecordFilters).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should not modify filters when trying to remove a non-existent filter', () => {
|
||||
const { result } = renderHook(
|
||||
() => {
|
||||
const currentRecordFilters = useRecoilComponentValueV2(
|
||||
currentRecordFiltersComponentState,
|
||||
);
|
||||
const { upsertRecordFilter } = useUpsertRecordFilter();
|
||||
const { removeRecordFilter } = useRemoveRecordFilter();
|
||||
return {
|
||||
upsertRecordFilter,
|
||||
removeRecordFilter,
|
||||
currentRecordFilters,
|
||||
};
|
||||
},
|
||||
{
|
||||
wrapper: Wrapper,
|
||||
},
|
||||
);
|
||||
|
||||
const filter: RecordFilter = {
|
||||
id: 'filter-1',
|
||||
fieldMetadataId: 'field-1',
|
||||
value: 'test-value',
|
||||
operand: ViewFilterOperand.Contains,
|
||||
displayValue: 'test-value',
|
||||
definition: {
|
||||
type: 'TEXT',
|
||||
fieldMetadataId: 'field-1',
|
||||
label: 'Test Field',
|
||||
iconName: 'IconText',
|
||||
},
|
||||
};
|
||||
|
||||
// Add a filter
|
||||
act(() => {
|
||||
result.current.upsertRecordFilter(filter);
|
||||
});
|
||||
|
||||
expect(result.current.currentRecordFilters).toHaveLength(1);
|
||||
|
||||
// Try to remove a non-existent filter
|
||||
act(() => {
|
||||
result.current.removeRecordFilter('non-existent-field');
|
||||
});
|
||||
|
||||
// Filter list should remain unchanged
|
||||
expect(result.current.currentRecordFilters).toHaveLength(1);
|
||||
expect(result.current.currentRecordFilters[0]).toEqual(filter);
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,112 @@
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
|
||||
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
|
||||
import { RecordFilter } from '@/object-record/record-filter/types/RecordFilter';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
|
||||
import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper';
|
||||
import { useUpsertRecordFilter } from '../useUpsertRecordFilter';
|
||||
|
||||
const Wrapper = getJestMetadataAndApolloMocksWrapper({
|
||||
apolloMocks: [],
|
||||
});
|
||||
|
||||
describe('useUpsertRecordFilter', () => {
|
||||
it('should add a new filter when fieldMetadataId does not exist', () => {
|
||||
const { result } = renderHook(
|
||||
() => {
|
||||
const currentRecordFilters = useRecoilComponentValueV2(
|
||||
currentRecordFiltersComponentState,
|
||||
);
|
||||
|
||||
const { upsertRecordFilter } = useUpsertRecordFilter();
|
||||
|
||||
return { upsertRecordFilter, currentRecordFilters };
|
||||
},
|
||||
{
|
||||
wrapper: Wrapper,
|
||||
},
|
||||
);
|
||||
|
||||
const newFilter: RecordFilter = {
|
||||
id: 'filter-1',
|
||||
fieldMetadataId: 'field-1',
|
||||
value: 'test-value',
|
||||
operand: ViewFilterOperand.Contains,
|
||||
displayValue: 'test-value',
|
||||
definition: {
|
||||
type: 'TEXT',
|
||||
fieldMetadataId: 'field-1',
|
||||
label: 'Test Field',
|
||||
iconName: 'IconText',
|
||||
},
|
||||
};
|
||||
|
||||
act(() => {
|
||||
result.current.upsertRecordFilter(newFilter);
|
||||
});
|
||||
|
||||
expect(result.current.currentRecordFilters).toHaveLength(1);
|
||||
expect(result.current.currentRecordFilters[0]).toEqual(newFilter);
|
||||
});
|
||||
|
||||
it('should update an existing filter when fieldMetadataId exists', () => {
|
||||
const { result } = renderHook(
|
||||
() => {
|
||||
const currentRecordFilters = useRecoilComponentValueV2(
|
||||
currentRecordFiltersComponentState,
|
||||
);
|
||||
|
||||
const { upsertRecordFilter } = useUpsertRecordFilter();
|
||||
|
||||
return { upsertRecordFilter, currentRecordFilters };
|
||||
},
|
||||
{
|
||||
wrapper: Wrapper,
|
||||
},
|
||||
);
|
||||
|
||||
const initialFilter: RecordFilter = {
|
||||
id: 'filter-1',
|
||||
fieldMetadataId: 'field-1',
|
||||
value: 'initial-value',
|
||||
operand: ViewFilterOperand.Contains,
|
||||
displayValue: 'initial-value',
|
||||
definition: {
|
||||
type: 'TEXT',
|
||||
fieldMetadataId: 'field-1',
|
||||
label: 'Test Field',
|
||||
iconName: 'IconText',
|
||||
},
|
||||
};
|
||||
|
||||
const updatedFilter: RecordFilter = {
|
||||
id: 'filter-1',
|
||||
fieldMetadataId: 'field-1',
|
||||
value: 'updated-value',
|
||||
operand: ViewFilterOperand.Contains,
|
||||
displayValue: 'updated-value',
|
||||
definition: {
|
||||
type: 'TEXT',
|
||||
fieldMetadataId: 'field-1',
|
||||
label: 'Test Field',
|
||||
iconName: 'IconText',
|
||||
},
|
||||
};
|
||||
|
||||
act(() => {
|
||||
result.current.upsertRecordFilter(initialFilter);
|
||||
});
|
||||
|
||||
expect(result.current.currentRecordFilters).toHaveLength(1);
|
||||
expect(result.current.currentRecordFilters[0]).toEqual(initialFilter);
|
||||
|
||||
act(() => {
|
||||
result.current.upsertRecordFilter(updatedFilter);
|
||||
});
|
||||
|
||||
expect(result.current.currentRecordFilters).toHaveLength(1);
|
||||
expect(result.current.currentRecordFilters[0]).toEqual(updatedFilter);
|
||||
});
|
||||
});
|
||||
@ -1,7 +1,6 @@
|
||||
import { onFilterSelectComponentState } from '@/object-record/object-filter-dropdown/states/onFilterSelectComponentState';
|
||||
import { selectedFilterComponentState } from '@/object-record/object-filter-dropdown/states/selectedFilterComponentState';
|
||||
import { useUpsertRecordFilter } from '@/object-record/record-filter/hooks/useUpsertRecordFilter';
|
||||
import { RecordFilter } from '@/object-record/record-filter/types/RecordFilter';
|
||||
import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue';
|
||||
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
|
||||
import { useUpsertCombinedViewFilters } from '@/views/hooks/useUpsertCombinedViewFilters';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
@ -14,32 +13,19 @@ export const useApplyRecordFilter = (componentInstanceId?: string) => {
|
||||
componentInstanceId,
|
||||
);
|
||||
|
||||
const onFilterSelectCallbackState = useRecoilComponentCallbackStateV2(
|
||||
onFilterSelectComponentState,
|
||||
componentInstanceId,
|
||||
);
|
||||
const { upsertRecordFilter } = useUpsertRecordFilter();
|
||||
|
||||
const applyRecordFilter = useRecoilCallback(
|
||||
({ set, snapshot }) =>
|
||||
({ set }) =>
|
||||
(filter: RecordFilter | null) => {
|
||||
set(selectedFilterCallbackState, filter);
|
||||
|
||||
const onFilterSelect = getSnapshotValue(
|
||||
snapshot,
|
||||
onFilterSelectCallbackState,
|
||||
);
|
||||
|
||||
if (isDefined(filter)) {
|
||||
upsertCombinedViewFilter(filter);
|
||||
upsertRecordFilter(filter);
|
||||
}
|
||||
|
||||
onFilterSelect?.(filter);
|
||||
},
|
||||
[
|
||||
selectedFilterCallbackState,
|
||||
onFilterSelectCallbackState,
|
||||
upsertCombinedViewFilter,
|
||||
],
|
||||
[selectedFilterCallbackState, upsertCombinedViewFilter, upsertRecordFilter],
|
||||
);
|
||||
|
||||
return {
|
||||
|
||||
@ -0,0 +1,46 @@
|
||||
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
|
||||
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
|
||||
import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
export const useRemoveRecordFilter = () => {
|
||||
const currentRecordFiltersCallbackState = useRecoilComponentCallbackStateV2(
|
||||
currentRecordFiltersComponentState,
|
||||
);
|
||||
|
||||
const removeRecordFilter = useRecoilCallback(
|
||||
({ set, snapshot }) =>
|
||||
(fieldMetadataId: string) => {
|
||||
const currentRecordFilters = getSnapshotValue(
|
||||
snapshot,
|
||||
currentRecordFiltersCallbackState,
|
||||
);
|
||||
|
||||
const foundRecordFilterInCurrentRecordFilters =
|
||||
currentRecordFilters.some(
|
||||
(existingFilter) =>
|
||||
existingFilter.fieldMetadataId === fieldMetadataId,
|
||||
);
|
||||
|
||||
if (foundRecordFilterInCurrentRecordFilters) {
|
||||
set(currentRecordFiltersCallbackState, (currentRecordFilters) => {
|
||||
const newCurrentRecordFilters = [...currentRecordFilters];
|
||||
|
||||
const indexOfFilterToRemove = newCurrentRecordFilters.findIndex(
|
||||
(existingFilter) =>
|
||||
existingFilter.fieldMetadataId === fieldMetadataId,
|
||||
);
|
||||
|
||||
newCurrentRecordFilters.splice(indexOfFilterToRemove, 1);
|
||||
|
||||
return newCurrentRecordFilters;
|
||||
});
|
||||
}
|
||||
},
|
||||
[currentRecordFiltersCallbackState],
|
||||
);
|
||||
|
||||
return {
|
||||
removeRecordFilter,
|
||||
};
|
||||
};
|
||||
@ -0,0 +1,54 @@
|
||||
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
|
||||
import { RecordFilter } from '@/object-record/record-filter/types/RecordFilter';
|
||||
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
|
||||
import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
export const useUpsertRecordFilter = () => {
|
||||
const currentRecordFiltersCallbackState = useRecoilComponentCallbackStateV2(
|
||||
currentRecordFiltersComponentState,
|
||||
);
|
||||
|
||||
const upsertRecordFilter = useRecoilCallback(
|
||||
({ set, snapshot }) =>
|
||||
(filter: RecordFilter) => {
|
||||
const currentRecordFilters = getSnapshotValue(
|
||||
snapshot,
|
||||
currentRecordFiltersCallbackState,
|
||||
);
|
||||
|
||||
const foundRecordFilterInCurrentRecordFilters =
|
||||
currentRecordFilters.some(
|
||||
(existingFilter) =>
|
||||
existingFilter.fieldMetadataId === filter.fieldMetadataId,
|
||||
);
|
||||
|
||||
if (!foundRecordFilterInCurrentRecordFilters) {
|
||||
set(currentRecordFiltersCallbackState, [
|
||||
...currentRecordFilters,
|
||||
filter,
|
||||
]);
|
||||
} else {
|
||||
set(currentRecordFiltersCallbackState, (currentRecordFilters) => {
|
||||
const newCurrentRecordFilters = [...currentRecordFilters];
|
||||
|
||||
const indexOfFilterToUpdate = newCurrentRecordFilters.findIndex(
|
||||
(existingFilter) =>
|
||||
existingFilter.fieldMetadataId === filter.fieldMetadataId,
|
||||
);
|
||||
|
||||
newCurrentRecordFilters[indexOfFilterToUpdate] = {
|
||||
...filter,
|
||||
};
|
||||
|
||||
return newCurrentRecordFilters;
|
||||
});
|
||||
}
|
||||
},
|
||||
[currentRecordFiltersCallbackState],
|
||||
);
|
||||
|
||||
return {
|
||||
upsertRecordFilter,
|
||||
};
|
||||
};
|
||||
@ -0,0 +1,4 @@
|
||||
import { createComponentInstanceContext } from '@/ui/utilities/state/component-state/utils/createComponentInstanceContext';
|
||||
|
||||
export const RecordFiltersComponentInstanceContext =
|
||||
createComponentInstanceContext();
|
||||
@ -0,0 +1,11 @@
|
||||
import { RecordFiltersComponentInstanceContext } from '@/object-record/record-filter/states/context/RecordFiltersComponentInstanceContext';
|
||||
import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2';
|
||||
import { RecordFilter } from '../../record-filter/types/RecordFilter';
|
||||
|
||||
export const currentRecordFiltersComponentState = createComponentStateV2<
|
||||
RecordFilter[]
|
||||
>({
|
||||
key: 'currentRecordFiltersComponentState',
|
||||
defaultValue: [],
|
||||
componentInstanceContext: RecordFiltersComponentInstanceContext,
|
||||
});
|
||||
@ -0,0 +1,128 @@
|
||||
import { isMatchingArrayFilter } from '../isMatchingArrayFilter';
|
||||
|
||||
describe('isMatchingArrayFilter', () => {
|
||||
describe('is filter', () => {
|
||||
it('should return true when checking for NULL and value is null', () => {
|
||||
expect(
|
||||
isMatchingArrayFilter({
|
||||
arrayFilter: { is: 'NULL' },
|
||||
value: null,
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when checking for NULL and value is not null', () => {
|
||||
expect(
|
||||
isMatchingArrayFilter({
|
||||
arrayFilter: { is: 'NULL' },
|
||||
value: ['test'],
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true when checking for NOT_NULL and value is not null', () => {
|
||||
expect(
|
||||
isMatchingArrayFilter({
|
||||
arrayFilter: { is: 'NOT_NULL' },
|
||||
value: ['test'],
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when checking for NOT_NULL and value is null', () => {
|
||||
expect(
|
||||
isMatchingArrayFilter({
|
||||
arrayFilter: { is: 'NOT_NULL' },
|
||||
value: null,
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isEmptyArray filter', () => {
|
||||
it('should return true when array is empty and checking for empty array', () => {
|
||||
expect(
|
||||
isMatchingArrayFilter({
|
||||
arrayFilter: { isEmptyArray: true },
|
||||
value: [],
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when array is not empty and checking for empty array', () => {
|
||||
expect(
|
||||
isMatchingArrayFilter({
|
||||
arrayFilter: { isEmptyArray: true },
|
||||
value: ['test'],
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when value is null and checking for empty array', () => {
|
||||
expect(
|
||||
isMatchingArrayFilter({
|
||||
arrayFilter: { isEmptyArray: true },
|
||||
value: null,
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('containsIlike filter', () => {
|
||||
it('should return true when array contains item matching case-insensitive search', () => {
|
||||
expect(
|
||||
isMatchingArrayFilter({
|
||||
arrayFilter: { containsIlike: 'TEST' },
|
||||
value: ['test item'],
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when array does not contain item matching search', () => {
|
||||
expect(
|
||||
isMatchingArrayFilter({
|
||||
arrayFilter: { containsIlike: 'missing' },
|
||||
value: ['test item'],
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when value is null and using containsIlike', () => {
|
||||
expect(
|
||||
isMatchingArrayFilter({
|
||||
arrayFilter: { containsIlike: 'test' },
|
||||
value: null,
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('should match partial strings case-insensitively', () => {
|
||||
expect(
|
||||
isMatchingArrayFilter({
|
||||
arrayFilter: { containsIlike: 'TE' },
|
||||
value: ['Test Item', 'Another Item'],
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('error handling', () => {
|
||||
it('should throw error for invalid filter', () => {
|
||||
expect(() =>
|
||||
isMatchingArrayFilter({
|
||||
arrayFilter: {},
|
||||
value: [],
|
||||
}),
|
||||
).toThrow('Unexpected value for array filter');
|
||||
});
|
||||
|
||||
it('should throw error for unknown filter type', () => {
|
||||
expect(() =>
|
||||
isMatchingArrayFilter({
|
||||
arrayFilter: { unknownFilter: 'test' } as any,
|
||||
value: [],
|
||||
}),
|
||||
).toThrow('Unexpected value for array filter');
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -1,10 +1,10 @@
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { turnSortsIntoOrderBy } from '@/object-record/object-sort-dropdown/utils/turnSortsIntoOrderBy';
|
||||
import { useFilterValueDependencies } from '@/object-record/record-filter/hooks/useFilterValueDependencies';
|
||||
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
|
||||
import { computeViewRecordGqlOperationFilter } from '@/object-record/record-filter/utils/computeViewRecordGqlOperationFilter';
|
||||
import { useCurrentRecordGroupDefinition } from '@/object-record/record-group/hooks/useCurrentRecordGroupDefinition';
|
||||
import { useRecordGroupFilter } from '@/object-record/record-group/hooks/useRecordGroupFilter';
|
||||
import { tableFiltersComponentState } from '@/object-record/record-table/states/tableFiltersComponentState';
|
||||
import { tableSortsComponentState } from '@/object-record/record-table/states/tableSortsComponentState';
|
||||
import { tableViewFilterGroupsComponentState } from '@/object-record/record-table/states/tableViewFilterGroupsComponentState';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
@ -27,20 +27,21 @@ export const useFindManyRecordIndexTableParams = (
|
||||
tableViewFilterGroupsComponentState,
|
||||
recordTableId,
|
||||
);
|
||||
const tableFilters = useRecoilComponentValueV2(
|
||||
tableFiltersComponentState,
|
||||
recordTableId,
|
||||
);
|
||||
|
||||
const tableSorts = useRecoilComponentValueV2(
|
||||
tableSortsComponentState,
|
||||
recordTableId,
|
||||
);
|
||||
|
||||
const currentRecordFilters = useRecoilComponentValueV2(
|
||||
currentRecordFiltersComponentState,
|
||||
);
|
||||
|
||||
const { filterValueDependencies } = useFilterValueDependencies();
|
||||
|
||||
const stateFilter = computeViewRecordGqlOperationFilter(
|
||||
filterValueDependencies,
|
||||
tableFilters,
|
||||
currentRecordFilters,
|
||||
objectMetadataItem?.fields ?? [],
|
||||
tableViewFilterGroups,
|
||||
);
|
||||
|
||||
@ -5,6 +5,7 @@ import { useColumnDefinitionsFromFieldMetadata } from '@/object-metadata/hooks/u
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { useSelectFilterDefinitionUsedInDropdown } from '@/object-record/object-filter-dropdown/hooks/useSelectFilterDefinitionUsedInDropdown';
|
||||
|
||||
import { useUpsertRecordFilter } from '@/object-record/record-filter/hooks/useUpsertRecordFilter';
|
||||
import { RecordFilter } from '@/object-record/record-filter/types/RecordFilter';
|
||||
import { getRecordFilterOperandsForRecordFilterDefinition } from '@/object-record/record-filter/utils/getRecordFilterOperandsForRecordFilterDefinition';
|
||||
import { isDropdownOpenComponentState } from '@/ui/layout/dropdown/states/isDropdownOpenComponentState';
|
||||
@ -33,6 +34,7 @@ export const useHandleToggleColumnFilter = ({
|
||||
useColumnDefinitionsFromFieldMetadata(objectMetadataItem);
|
||||
|
||||
const { upsertCombinedViewFilter } = useUpsertCombinedViewFilters(viewBarId);
|
||||
const { upsertRecordFilter } = useUpsertRecordFilter();
|
||||
|
||||
const openDropdown = useRecoilCallback(({ set }) => {
|
||||
return (dropdownId: string) => {
|
||||
@ -93,6 +95,8 @@ export const useHandleToggleColumnFilter = ({
|
||||
value: '',
|
||||
};
|
||||
|
||||
upsertRecordFilter(newFilter);
|
||||
|
||||
await upsertCombinedViewFilter(newFilter);
|
||||
|
||||
selectFilterDefinitionUsedInDropdown({ filterDefinition });
|
||||
@ -107,6 +111,7 @@ export const useHandleToggleColumnFilter = ({
|
||||
selectFilterDefinitionUsedInDropdown,
|
||||
currentViewWithCombinedFiltersAndSorts,
|
||||
availableFilterDefinitions,
|
||||
upsertRecordFilter,
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
@ -4,6 +4,7 @@ import { v4 } from 'uuid';
|
||||
import { useColumnDefinitionsFromFieldMetadata } from '@/object-metadata/hooks/useColumnDefinitionsFromFieldMetadata';
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { getFilterTypeFromFieldType } from '@/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions';
|
||||
import { useUpsertRecordFilter } from '@/object-record/record-filter/hooks/useUpsertRecordFilter';
|
||||
import { RecordFilter } from '@/object-record/record-filter/types/RecordFilter';
|
||||
import { isSoftDeleteFilterActiveComponentState } from '@/object-record/record-table/states/isSoftDeleteFilterActiveComponentState';
|
||||
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
|
||||
@ -36,6 +37,8 @@ export const useHandleToggleTrashColumnFilter = ({
|
||||
viewBarId,
|
||||
);
|
||||
|
||||
const { upsertRecordFilter } = useUpsertRecordFilter();
|
||||
|
||||
const handleToggleTrashColumnFilter = useCallback(() => {
|
||||
const trashFieldMetadata = objectMetadataItem.fields.find(
|
||||
(field: { name: string }) => field.name === 'deletedAt',
|
||||
@ -69,8 +72,14 @@ export const useHandleToggleTrashColumnFilter = ({
|
||||
value: '',
|
||||
};
|
||||
|
||||
upsertRecordFilter(newFilter);
|
||||
upsertCombinedViewFilter(newFilter);
|
||||
}, [columnDefinitions, objectMetadataItem, upsertCombinedViewFilter]);
|
||||
}, [
|
||||
columnDefinitions,
|
||||
objectMetadataItem,
|
||||
upsertCombinedViewFilter,
|
||||
upsertRecordFilter,
|
||||
]);
|
||||
|
||||
const toggleSoftDeleteFilterState = useRecoilCallback(
|
||||
({ set }) =>
|
||||
|
||||
@ -2,6 +2,7 @@ import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext';
|
||||
import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext';
|
||||
import { RecordFiltersComponentInstanceContext } from '@/object-record/record-filter/states/context/RecordFiltersComponentInstanceContext';
|
||||
import { isNewViewableRecordLoadingState } from '@/object-record/record-right-drawer/states/isNewViewableRecordLoading';
|
||||
import { viewableRecordIdState } from '@/object-record/record-right-drawer/states/viewableRecordIdState';
|
||||
import { viewableRecordNameSingularState } from '@/object-record/record-right-drawer/states/viewableRecordNameSingularState';
|
||||
@ -42,29 +43,33 @@ export const RightDrawerRecord = () => {
|
||||
);
|
||||
|
||||
return (
|
||||
<ContextStoreComponentInstanceContext.Provider
|
||||
value={{
|
||||
instanceId: `record-show-${objectRecordId}`,
|
||||
}}
|
||||
<RecordFiltersComponentInstanceContext.Provider
|
||||
value={{ instanceId: `record-show-${objectRecordId}` }}
|
||||
>
|
||||
<ActionMenuComponentInstanceContext.Provider
|
||||
value={{ instanceId: `record-show-${objectRecordId}` }}
|
||||
<ContextStoreComponentInstanceContext.Provider
|
||||
value={{
|
||||
instanceId: `record-show-${objectRecordId}`,
|
||||
}}
|
||||
>
|
||||
<StyledRightDrawerRecord isMobile={isMobile}>
|
||||
<RecordFieldValueSelectorContextProvider>
|
||||
{!isNewViewableRecordLoading && (
|
||||
<RecordValueSetterEffect recordId={objectRecordId} />
|
||||
)}
|
||||
<RecordShowContainer
|
||||
objectNameSingular={objectNameSingular}
|
||||
objectRecordId={objectRecordId}
|
||||
loading={false}
|
||||
isInRightDrawer={true}
|
||||
isNewRightDrawerItemLoading={isNewViewableRecordLoading}
|
||||
/>
|
||||
</RecordFieldValueSelectorContextProvider>
|
||||
</StyledRightDrawerRecord>
|
||||
</ActionMenuComponentInstanceContext.Provider>
|
||||
</ContextStoreComponentInstanceContext.Provider>
|
||||
<ActionMenuComponentInstanceContext.Provider
|
||||
value={{ instanceId: `record-show-${objectRecordId}` }}
|
||||
>
|
||||
<StyledRightDrawerRecord isMobile={isMobile}>
|
||||
<RecordFieldValueSelectorContextProvider>
|
||||
{!isNewViewableRecordLoading && (
|
||||
<RecordValueSetterEffect recordId={objectRecordId} />
|
||||
)}
|
||||
<RecordShowContainer
|
||||
objectNameSingular={objectNameSingular}
|
||||
objectRecordId={objectRecordId}
|
||||
loading={false}
|
||||
isInRightDrawer={true}
|
||||
isNewRightDrawerItemLoading={isNewViewableRecordLoading}
|
||||
/>
|
||||
</RecordFieldValueSelectorContextProvider>
|
||||
</StyledRightDrawerRecord>
|
||||
</ActionMenuComponentInstanceContext.Provider>
|
||||
</ContextStoreComponentInstanceContext.Provider>
|
||||
</RecordFiltersComponentInstanceContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { IconFilterOff } from 'twenty-ui';
|
||||
|
||||
import { useObjectLabel } from '@/object-metadata/hooks/useObjectLabel';
|
||||
import { useRemoveRecordFilter } from '@/object-record/record-filter/hooks/useRemoveRecordFilter';
|
||||
import { useHandleToggleTrashColumnFilter } from '@/object-record/record-index/hooks/useHandleToggleTrashColumnFilter';
|
||||
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
|
||||
import { RecordTableEmptyStateDisplay } from '@/object-record/record-table/empty-state/components/RecordTableEmptyStateDisplay';
|
||||
@ -25,14 +26,22 @@ export const RecordTableEmptyStateSoftDelete = () => {
|
||||
viewBarId: recordTableId,
|
||||
});
|
||||
|
||||
const { removeRecordFilter } = useRemoveRecordFilter();
|
||||
|
||||
const handleButtonClick = async () => {
|
||||
deleteCombinedViewFilter(
|
||||
tableFilters.find(
|
||||
(filter) =>
|
||||
filter.definition.label === 'Deleted' &&
|
||||
filter.operand === 'isNotEmpty',
|
||||
)?.id ?? '',
|
||||
const deletedFilter = tableFilters.find(
|
||||
(filter) =>
|
||||
filter.definition.label === 'Deleted' &&
|
||||
filter.operand === 'isNotEmpty',
|
||||
);
|
||||
|
||||
if (!deletedFilter) {
|
||||
throw new Error('Deleted filter not found');
|
||||
}
|
||||
|
||||
removeRecordFilter(deletedFilter.fieldMetadataId);
|
||||
deleteCombinedViewFilter(deletedFilter.id);
|
||||
|
||||
toggleSoftDeleteFilterState(false);
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user