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:
Lucas Bordeau
2025-01-23 11:09:44 +01:00
committed by GitHub
parent 3ab193f298
commit bddca09451
42 changed files with 1303 additions and 302 deletions

View File

@ -10,6 +10,7 @@ import { getActionBarIdFromActionMenuId } from '@/action-menu/utils/getActionBar
import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState';
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext';
import { RecordFiltersComponentInstanceContext } from '@/object-record/record-filter/states/context/RecordFiltersComponentInstanceContext';
import { isBottomBarOpenedComponentState } from '@/ui/layout/bottom-bar/states/isBottomBarOpenedComponentState';
import { expect, jest } from '@storybook/jest';
import { Meta, StoryObj } from '@storybook/react';
@ -25,58 +26,63 @@ const meta: Meta<typeof RecordIndexActionMenuBar> = {
decorators: [
RouterDecorator,
(Story) => (
<ContextStoreComponentInstanceContext.Provider
<RecordFiltersComponentInstanceContext.Provider
value={{ instanceId: 'story-action-menu' }}
>
<RecoilRoot
initializeState={({ set }) => {
set(
contextStoreTargetedRecordsRuleComponentState.atomFamily({
instanceId: 'story-action-menu',
}),
{
mode: 'selection',
selectedRecordIds: ['1', '2', '3'],
},
);
set(
contextStoreNumberOfSelectedRecordsComponentState.atomFamily({
instanceId: 'story-action-menu',
}),
3,
);
const map = new Map<string, ActionMenuEntry>();
map.set('delete', {
isPinned: true,
scope: ActionMenuEntryScope.RecordSelection,
type: ActionMenuEntryType.Standard,
key: 'delete',
label: 'Delete',
position: 0,
Icon: IconTrash,
onClick: deleteMock,
});
set(
actionMenuEntriesComponentState.atomFamily({
instanceId: 'story-action-menu',
}),
map,
);
set(
isBottomBarOpenedComponentState.atomFamily({
instanceId: getActionBarIdFromActionMenuId('story-action-menu'),
}),
true,
);
}}
<ContextStoreComponentInstanceContext.Provider
value={{ instanceId: 'story-action-menu' }}
>
<ActionMenuComponentInstanceContext.Provider
value={{ instanceId: 'story-action-menu' }}
<RecoilRoot
initializeState={({ set }) => {
set(
contextStoreTargetedRecordsRuleComponentState.atomFamily({
instanceId: 'story-action-menu',
}),
{
mode: 'selection',
selectedRecordIds: ['1', '2', '3'],
},
);
set(
contextStoreNumberOfSelectedRecordsComponentState.atomFamily({
instanceId: 'story-action-menu',
}),
3,
);
const map = new Map<string, ActionMenuEntry>();
map.set('delete', {
isPinned: true,
scope: ActionMenuEntryScope.RecordSelection,
type: ActionMenuEntryType.Standard,
key: 'delete',
label: 'Delete',
position: 0,
Icon: IconTrash,
onClick: deleteMock,
});
set(
actionMenuEntriesComponentState.atomFamily({
instanceId: 'story-action-menu',
}),
map,
);
set(
isBottomBarOpenedComponentState.atomFamily({
instanceId:
getActionBarIdFromActionMenuId('story-action-menu'),
}),
true,
);
}}
>
<Story />
</ActionMenuComponentInstanceContext.Provider>
</RecoilRoot>
</ContextStoreComponentInstanceContext.Provider>
<ActionMenuComponentInstanceContext.Provider
value={{ instanceId: 'story-action-menu' }}
>
<Story />
</ActionMenuComponentInstanceContext.Provider>
</RecoilRoot>
</ContextStoreComponentInstanceContext.Provider>
</RecordFiltersComponentInstanceContext.Provider>
),
],
args: {

View File

@ -9,6 +9,7 @@ import { useCommandMenuHotKeys } from '@/command-menu/hooks/useCommandMenuHotKey
import { isCommandMenuOpenedState } from '@/command-menu/states/isCommandMenuOpenedState';
import { CommandMenuAnimationVariant } from '@/command-menu/types/CommandMenuAnimationVariant';
import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext';
import { RecordFiltersComponentInstanceContext } from '@/object-record/record-filter/states/context/RecordFiltersComponentInstanceContext';
import { AppHotkeyScope } from '@/ui/utilities/hotkey/types/AppHotkeyScope';
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
import { workflowReactFlowRefState } from '@/workflow/workflow-diagram/states/workflowReactFlowRefState';
@ -74,37 +75,41 @@ export const CommandMenuContainer = ({
const theme = useTheme();
return (
<ContextStoreComponentInstanceContext.Provider
<RecordFiltersComponentInstanceContext.Provider
value={{ instanceId: 'command-menu' }}
>
<ActionMenuComponentInstanceContext.Provider
<ContextStoreComponentInstanceContext.Provider
value={{ instanceId: 'command-menu' }}
>
<ActionMenuContext.Provider
value={{
isInRightDrawer: false,
onActionExecutedCallback: toggleCommandMenu,
}}
<ActionMenuComponentInstanceContext.Provider
value={{ instanceId: 'command-menu' }}
>
<RecordActionMenuEntriesSetter />
{isWorkflowEnabled && <RecordAgnosticActionsSetterEffect />}
<ActionMenuConfirmationModals />
{isCommandMenuOpened && (
<StyledCommandMenu
data-testid="command-menu"
ref={commandMenuRef}
className="command-menu"
animate={targetVariantForAnimation}
initial="closed"
exit="closed"
variants={COMMAND_MENU_ANIMATION_VARIANTS}
transition={{ duration: theme.animation.duration.normal }}
>
{children}
</StyledCommandMenu>
)}
</ActionMenuContext.Provider>
</ActionMenuComponentInstanceContext.Provider>
</ContextStoreComponentInstanceContext.Provider>
<ActionMenuContext.Provider
value={{
isInRightDrawer: false,
onActionExecutedCallback: toggleCommandMenu,
}}
>
<RecordActionMenuEntriesSetter />
{isWorkflowEnabled && <RecordAgnosticActionsSetterEffect />}
<ActionMenuConfirmationModals />
{isCommandMenuOpened && (
<StyledCommandMenu
data-testid="command-menu"
ref={commandMenuRef}
className="command-menu"
animate={targetVariantForAnimation}
initial="closed"
exit="closed"
variants={COMMAND_MENU_ANIMATION_VARIANTS}
transition={{ duration: theme.animation.duration.normal }}
>
{children}
</StyledCommandMenu>
)}
</ActionMenuContext.Provider>
</ActionMenuComponentInstanceContext.Provider>
</ContextStoreComponentInstanceContext.Provider>
</RecordFiltersComponentInstanceContext.Provider>
);
};

View File

@ -19,6 +19,7 @@ import { ActionMenuComponentInstanceContext } from '@/action-menu/states/context
import { CommandMenuRouter } from '@/command-menu/components/CommandMenuRouter';
import { isCommandMenuOpenedState } from '@/command-menu/states/isCommandMenuOpenedState';
import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext';
import { RecordFiltersComponentInstanceContext } from '@/object-record/record-filter/states/context/RecordFiltersComponentInstanceContext';
import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator';
import { JestContextStoreSetter } from '~/testing/jest/JestContextStoreSetter';
import { CommandMenu } from '../CommandMenu';
@ -29,17 +30,21 @@ const openTimeout = 50;
const ContextStoreDecorator: Decorator = (Story) => {
return (
<ContextStoreComponentInstanceContext.Provider
<RecordFiltersComponentInstanceContext.Provider
value={{ instanceId: 'command-menu' }}
>
<ActionMenuComponentInstanceContext.Provider
<ContextStoreComponentInstanceContext.Provider
value={{ instanceId: 'command-menu' }}
>
<JestContextStoreSetter contextStoreCurrentObjectMetadataNameSingular="company">
<Story />
</JestContextStoreSetter>
</ActionMenuComponentInstanceContext.Provider>
</ContextStoreComponentInstanceContext.Provider>
<ActionMenuComponentInstanceContext.Provider
value={{ instanceId: 'command-menu' }}
>
<JestContextStoreSetter contextStoreCurrentObjectMetadataNameSingular="company">
<Story />
</JestContextStoreSetter>
</ActionMenuComponentInstanceContext.Provider>
</ContextStoreComponentInstanceContext.Provider>
</RecordFiltersComponentInstanceContext.Provider>
);
};

View File

@ -4,7 +4,7 @@ import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSi
import { useReadFindManyRecordsQueryInCache } from '@/object-record/cache/hooks/useReadFindManyRecordsQueryInCache';
import { useDeleteOneRecord } from '@/object-record/hooks/useDeleteOneRecord';
import { PREFETCH_CONFIG } from '@/prefetch/constants/PrefetchConfig';
import { usePrefetchRunQuery } from '@/prefetch/hooks/internal/usePrefetchRunQuery';
import { useUpsertRecordsInCacheForPrefetchKey } from '@/prefetch/hooks/internal/useUpsertRecordsInCacheForPrefetchKey';
import { PrefetchKey } from '@/prefetch/types/PrefetchKey';
export const useDeleteFavoriteFolder = () => {
@ -12,9 +12,10 @@ export const useDeleteFavoriteFolder = () => {
objectNameSingular: CoreObjectNameSingular.FavoriteFolder,
});
const { upsertRecordsInCache } = usePrefetchRunQuery<Favorite>({
prefetchKey: PrefetchKey.AllFavorites,
});
const { upsertRecordsInCache } =
useUpsertRecordsInCacheForPrefetchKey<Favorite>({
prefetchKey: PrefetchKey.AllFavorites,
});
const { objectMetadataItem } = useObjectMetadataItem({
objectNameSingular:

View File

@ -1,6 +1,6 @@
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { Favorite } from '@/favorites/types/Favorite';
import { usePrefetchRunQuery } from '@/prefetch/hooks/internal/usePrefetchRunQuery';
import { useUpsertRecordsInCacheForPrefetchKey } from '@/prefetch/hooks/internal/useUpsertRecordsInCacheForPrefetchKey';
import { usePrefetchedData } from '@/prefetch/hooks/usePrefetchedData';
import { PrefetchKey } from '@/prefetch/types/PrefetchKey';
import { useRecoilValue } from 'recoil';
@ -33,7 +33,7 @@ export const usePrefetchedFavoritesData = (): PrefetchedFavoritesData => {
);
const { upsertRecordsInCache: upsertFavorites } =
usePrefetchRunQuery<Favorite>({
useUpsertRecordsInCacheForPrefetchKey<Favorite>({
prefetchKey: PrefetchKey.AllFavorites,
});

View File

@ -1,6 +1,6 @@
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { FavoriteFolder } from '@/favorites/types/FavoriteFolder';
import { usePrefetchRunQuery } from '@/prefetch/hooks/internal/usePrefetchRunQuery';
import { useUpsertRecordsInCacheForPrefetchKey } from '@/prefetch/hooks/internal/useUpsertRecordsInCacheForPrefetchKey';
import { usePrefetchedData } from '@/prefetch/hooks/usePrefetchedData';
import { PrefetchKey } from '@/prefetch/types/PrefetchKey';
import { useRecoilValue } from 'recoil';
@ -26,7 +26,7 @@ export const usePrefetchedFavoritesFoldersData =
);
const { upsertRecordsInCache: upsertFavoriteFolders } =
usePrefetchRunQuery<FavoriteFolder>({
useUpsertRecordsInCacheForPrefetchKey<FavoriteFolder>({
prefetchKey: PrefetchKey.AllFavoritesFolders,
});

View File

@ -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;
}

View File

@ -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>
);
},

View File

@ -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<

View File

@ -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,

View File

@ -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);
});
});

View File

@ -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);
});
});

View File

@ -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 {

View File

@ -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,
};
};

View File

@ -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,
};
};

View File

@ -0,0 +1,4 @@
import { createComponentInstanceContext } from '@/ui/utilities/state/component-state/utils/createComponentInstanceContext';
export const RecordFiltersComponentInstanceContext =
createComponentInstanceContext();

View File

@ -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,
});

View File

@ -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');
});
});
});

View File

@ -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,
);

View File

@ -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,
],
);

View File

@ -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 }) =>

View File

@ -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>
);
};

View File

@ -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);
};

View File

@ -7,7 +7,7 @@ import { FavoriteFolder } from '@/favorites/types/FavoriteFolder';
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
import { useCombinedFindManyRecords } from '@/object-record/multiple-objects/hooks/useCombinedFindManyRecords';
import { PREFETCH_CONFIG } from '@/prefetch/constants/PrefetchConfig';
import { usePrefetchRunQuery } from '@/prefetch/hooks/internal/usePrefetchRunQuery';
import { useUpsertRecordsInCacheForPrefetchKey } from '@/prefetch/hooks/internal/useUpsertRecordsInCacheForPrefetchKey';
import { PrefetchKey } from '@/prefetch/types/PrefetchKey';
import { View } from '@/views/types/View';
import { useIsWorkspaceActivationStatusSuspended } from '@/workspace/hooks/useIsWorkspaceActivationStatusSuspended';
@ -19,16 +19,16 @@ export const PrefetchRunQueriesEffect = () => {
const isWorkspaceSuspended = useIsWorkspaceActivationStatusSuspended();
const { upsertRecordsInCache: upsertViewsInCache } =
usePrefetchRunQuery<View>({
useUpsertRecordsInCacheForPrefetchKey<View>({
prefetchKey: PrefetchKey.AllViews,
});
const { upsertRecordsInCache: upsertFavoritesInCache } =
usePrefetchRunQuery<Favorite>({
useUpsertRecordsInCacheForPrefetchKey<Favorite>({
prefetchKey: PrefetchKey.AllFavorites,
});
const { upsertRecordsInCache: upsertFavoritesFoldersInCache } =
usePrefetchRunQuery<FavoriteFolder>({
useUpsertRecordsInCacheForPrefetchKey<FavoriteFolder>({
prefetchKey: PrefetchKey.AllFavoritesFolders,
});
const { objectMetadataItems } = useObjectMetadataItems();

View File

@ -11,7 +11,7 @@ export type UsePrefetchRunQuery = {
prefetchKey: PrefetchKey;
};
export const usePrefetchRunQuery = <T extends ObjectRecord>({
export const useUpsertRecordsInCacheForPrefetchKey = <T extends ObjectRecord>({
prefetchKey,
}: UsePrefetchRunQuery) => {
const setPrefetchDataIsLoaded = useSetRecoilState(
@ -45,7 +45,6 @@ export const usePrefetchRunQuery = <T extends ObjectRecord>({
return {
objectMetadataItem,
setPrefetchDataIsLoaded,
upsertRecordsInCache,
};
};

View File

@ -3,6 +3,7 @@ import styled from '@emotion/styled';
import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext';
import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { RecordFiltersComponentInstanceContext } from '@/object-record/record-filter/states/context/RecordFiltersComponentInstanceContext';
import { RecordIndexContextProvider } from '@/object-record/record-index/contexts/RecordIndexContext';
import { RecordTableWithWrappers } from '@/object-record/record-table/components/RecordTableWithWrappers';
import { SignInBackgroundMockContainerEffect } from '@/sign-in-background-mock/components/SignInBackgroundMockContainerEffect';
@ -41,30 +42,36 @@ export const SignInBackgroundMockContainer = () => {
<ViewComponentInstanceContext.Provider
value={{ instanceId: recordIndexId }}
>
<ContextStoreComponentInstanceContext.Provider
<RecordFiltersComponentInstanceContext.Provider
value={{ instanceId: recordIndexId }}
>
<ActionMenuComponentInstanceContext.Provider
value={{ instanceId: recordIndexId }}
<ContextStoreComponentInstanceContext.Provider
value={{
instanceId: recordIndexId,
}}
>
<ViewBar
viewBarId={viewBarId}
onCurrentViewChange={() => {}}
optionsDropdownButton={<></>}
/>
<SignInBackgroundMockContainerEffect
objectNamePlural={objectNamePlural}
recordTableId={recordIndexId}
viewId={viewBarId}
/>
<RecordTableWithWrappers
objectNameSingular={objectNameSingular}
recordTableId={recordIndexId}
viewBarId={viewBarId}
updateRecordMutation={() => {}}
/>
</ActionMenuComponentInstanceContext.Provider>
</ContextStoreComponentInstanceContext.Provider>
<ActionMenuComponentInstanceContext.Provider
value={{ instanceId: recordIndexId }}
>
<ViewBar
viewBarId={viewBarId}
onCurrentViewChange={() => {}}
optionsDropdownButton={<></>}
/>
<SignInBackgroundMockContainerEffect
objectNamePlural={objectNamePlural}
recordTableId={recordIndexId}
viewId={viewBarId}
/>
<RecordTableWithWrappers
objectNameSingular={objectNameSingular}
recordTableId={recordIndexId}
viewBarId={viewBarId}
updateRecordMutation={() => {}}
/>
</ActionMenuComponentInstanceContext.Provider>
</ContextStoreComponentInstanceContext.Provider>
</RecordFiltersComponentInstanceContext.Provider>
</ViewComponentInstanceContext.Provider>
</RecordIndexContextProvider>
</StyledContainer>

View File

@ -11,6 +11,7 @@ import { ObjectFilterOperandSelectAndInput } from '@/object-record/object-filter
import { filterDefinitionUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/filterDefinitionUsedInDropdownComponentState';
import { selectedFilterComponentState } from '@/object-record/object-filter-dropdown/states/selectedFilterComponentState';
import { selectedOperandInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/selectedOperandInDropdownComponentState';
import { useRemoveRecordFilter } from '@/object-record/record-filter/hooks/useRemoveRecordFilter';
import { RecordFilterOperand } from '@/object-record/record-filter/types/RecordFilterOperand';
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
import { useDeleteCombinedViewFilters } from '@/views/hooks/useDeleteCombinedViewFilters';
@ -73,14 +74,17 @@ export const EditableFilterDropdownButton = ({
viewFilterDropdownId,
]);
const { removeRecordFilter } = useRemoveRecordFilter();
const handleRemove = () => {
closeDropdown();
deleteCombinedViewFilter(viewFilter.id);
removeRecordFilter(viewFilter.fieldMetadataId);
};
const handleDropdownClickOutside = useCallback(() => {
const { id: fieldId, value, operand } = viewFilter;
const { id: fieldId, value, operand, fieldMetadataId } = viewFilter;
if (
!value &&
![
@ -91,9 +95,10 @@ export const EditableFilterDropdownButton = ({
RecordFilterOperand.IsToday,
].includes(operand)
) {
removeRecordFilter(fieldMetadataId);
deleteCombinedViewFilter(fieldId);
}
}, [viewFilter, deleteCombinedViewFilter]);
}, [viewFilter, deleteCombinedViewFilter, removeRecordFilter]);
return (
<Dropdown

View File

@ -3,6 +3,7 @@ import { useEffect } from 'react';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useSetRecoilComponentFamilyStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentFamilyStateV2';
import { useViewFromQueryParams } from '@/views/hooks/internal/useViewFromQueryParams';
import { useApplyViewFiltersToCurrentRecordFilters } from '@/views/hooks/useApplyViewFiltersToCurrentRecordFilters';
import { useResetUnsavedViewStates } from '@/views/hooks/useResetUnsavedViewStates';
import { currentViewIdComponentState } from '@/views/states/currentViewIdComponentState';
import { unsavedToUpsertViewFiltersComponentFamilyState } from '@/views/states/unsavedToUpsertViewFiltersComponentFamilyState';
@ -20,6 +21,9 @@ export const QueryParamsFiltersEffect = () => {
const { resetUnsavedViewStates } = useResetUnsavedViewStates();
const { applyViewFiltersToCurrentRecordFilters } =
useApplyViewFiltersToCurrentRecordFilters();
useEffect(() => {
if (!hasFiltersQueryParams) {
return;
@ -27,10 +31,12 @@ export const QueryParamsFiltersEffect = () => {
getFiltersFromQueryParams().then((filtersFromParams) => {
if (Array.isArray(filtersFromParams)) {
applyViewFiltersToCurrentRecordFilters(filtersFromParams);
setUnsavedViewFilter(filtersFromParams);
}
});
}, [
applyViewFiltersToCurrentRecordFilters,
getFiltersFromQueryParams,
hasFiltersQueryParams,
resetUnsavedViewStates,

View File

@ -1,6 +1,7 @@
import { useIcons } from 'twenty-ui';
import { useObjectNameSingularFromPlural } from '@/object-metadata/hooks/useObjectNameSingularFromPlural';
import { useRemoveRecordFilter } from '@/object-record/record-filter/hooks/useRemoveRecordFilter';
import { RecordFilter } from '@/object-record/record-filter/types/RecordFilter';
import { useHandleToggleTrashColumnFilter } from '@/object-record/record-index/hooks/useHandleToggleTrashColumnFilter';
import { SortOrFilterChip } from '@/views/components/SortOrFilterChip';
@ -29,10 +30,14 @@ export const VariantFilterChip = ({
viewBarId,
});
const { removeRecordFilter } = useRemoveRecordFilter();
const { getIcon } = useIcons();
const handleRemoveClick = () => {
deleteCombinedViewFilter(viewFilter.id);
removeRecordFilter(viewFilter.fieldMetadataId);
if (
viewFilter.definition.label === 'Deleted' &&
viewFilter.operand === 'isNotEmpty'

View File

@ -21,6 +21,7 @@ import { ViewsHotkeyScope } from '../types/ViewsHotkeyScope';
import { FiltersHotkeyScope } from '@/object-record/object-filter-dropdown/types/FiltersHotkeyScope';
import { VIEW_SORT_DROPDOWN_ID } from '@/object-record/object-sort-dropdown/constants/ViewSortDropdownId';
import { ObjectSortDropdownComponentInstanceContext } from '@/object-record/object-sort-dropdown/states/context/ObjectSortDropdownComponentInstanceContext';
import { ViewBarRecordFilterEffect } from '@/views/components/ViewBarRecordFilterEffect';
import { ViewEventContext } from '@/views/events/contexts/ViewEventContext';
import { UpdateViewButtonGroup } from './UpdateViewButtonGroup';
import { ViewBarDetails } from './ViewBarDetails';
@ -53,6 +54,7 @@ export const ViewBar = ({
value={{ instanceId: VIEW_SORT_DROPDOWN_ID }}
>
<ViewEventContext.Provider value={{ onCurrentViewChange }}>
<ViewBarRecordFilterEffect />
<ViewBarEffect viewBarId={viewBarId} />
<ViewBarFilterEffect filterDropdownId={filterDropdownId} />
<ViewBarSortEffect />

View File

@ -14,6 +14,8 @@ import { EditableFilterDropdownButton } from '@/views/components/EditableFilterD
import { EditableSortChip } from '@/views/components/EditableSortChip';
import { ViewBarFilterEffect } from '@/views/components/ViewBarFilterEffect';
import { useViewFromQueryParams } from '@/views/hooks/internal/useViewFromQueryParams';
import { useApplyCurrentViewFiltersToCurrentRecordFilters } from '@/views/hooks/useApplyCurrentViewFiltersToCurrentRecordFilters';
import { useGetCurrentView } from '@/views/hooks/useGetCurrentView';
import { useResetUnsavedViewStates } from '@/views/hooks/useResetUnsavedViewStates';
import { availableFilterDefinitionsComponentState } from '@/views/states/availableFilterDefinitionsComponentState';
@ -167,9 +169,13 @@ export const ViewBarDetails = ({
};
}, [currentViewWithCombinedFiltersAndSorts]);
const { applyCurrentViewFiltersToCurrentRecordFilters } =
useApplyCurrentViewFiltersToCurrentRecordFilters();
const handleCancelClick = () => {
if (isDefined(viewId)) {
resetUnsavedViewStates(viewId);
applyCurrentViewFiltersToCurrentRecordFilters();
toggleSoftDeleteFilterState(false);
}
};

View File

@ -1,16 +1,13 @@
import { isNonEmptyString } from '@sniptt/guards';
import { useEffect } from 'react';
import { RecordFilter } from '@/object-record/record-filter/types/RecordFilter';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
import { filterDefinitionUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/filterDefinitionUsedInDropdownComponentState';
import { objectFilterDropdownSelectedOptionValuesComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownSelectedOptionValuesComponentState';
import { objectFilterDropdownSelectedRecordIdsComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownSelectedRecordIdsComponentState';
import { onFilterSelectComponentState } from '@/object-record/object-filter-dropdown/states/onFilterSelectComponentState';
import { useGetCurrentView } from '@/views/hooks/useGetCurrentView';
import { useUpsertCombinedViewFilters } from '@/views/hooks/useUpsertCombinedViewFilters';
import { availableFilterDefinitionsComponentState } from '@/views/states/availableFilterDefinitionsComponentState';
import { jsonRelationFilterValueSchema } from '@/views/view-filter-value/validation-schemas/jsonRelationFilterValueSchema';
import { simpleRelationFilterValueSchema } from '@/views/view-filter-value/validation-schemas/simpleRelationFilterValueSchema';
@ -23,19 +20,12 @@ type ViewBarFilterEffectProps = {
export const ViewBarFilterEffect = ({
filterDropdownId,
}: ViewBarFilterEffectProps) => {
const { upsertCombinedViewFilter } = useUpsertCombinedViewFilters();
const { currentViewWithCombinedFiltersAndSorts } = useGetCurrentView();
const availableFilterDefinitions = useRecoilComponentValueV2(
availableFilterDefinitionsComponentState,
);
const setOnFilterSelect = useSetRecoilComponentStateV2(
onFilterSelectComponentState,
filterDropdownId,
);
const filterDefinitionUsedInDropdown = useRecoilComponentValueV2(
filterDefinitionUsedInDropdownComponentState,
filterDropdownId,
@ -62,17 +52,7 @@ export const ViewBarFilterEffect = ({
if (isDefined(availableFilterDefinitions)) {
setAvailableFilterDefinitions(availableFilterDefinitions);
}
setOnFilterSelect(() => (filter: RecordFilter | null) => {
if (isDefined(filter)) {
upsertCombinedViewFilter(filter);
}
});
}, [
availableFilterDefinitions,
setAvailableFilterDefinitions,
setOnFilterSelect,
upsertCombinedViewFilter,
]);
}, [availableFilterDefinitions, setAvailableFilterDefinitions]);
useEffect(() => {
if (filterDefinitionUsedInDropdown?.type === 'RELATION') {

View File

@ -0,0 +1,50 @@
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
import { usePrefetchedData } from '@/prefetch/hooks/usePrefetchedData';
import { PrefetchKey } from '@/prefetch/types/PrefetchKey';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
import { availableFilterDefinitionsComponentState } from '@/views/states/availableFilterDefinitionsComponentState';
import { currentViewIdComponentState } from '@/views/states/currentViewIdComponentState';
import { View } from '@/views/types/View';
import { mapViewFiltersToFilters } from '@/views/utils/mapViewFiltersToFilters';
import { useEffect } from 'react';
import { isDefined } from 'twenty-ui';
export const ViewBarRecordFilterEffect = () => {
const { records: views, isDataPrefetched } = usePrefetchedData<View>(
PrefetchKey.AllViews,
);
const currentViewId = useRecoilComponentValueV2(currentViewIdComponentState);
const setCurrentRecordFilters = useSetRecoilComponentStateV2(
currentRecordFiltersComponentState,
);
const availableFilterDefinitions = useRecoilComponentValueV2(
availableFilterDefinitionsComponentState,
);
useEffect(() => {
if (isDataPrefetched) {
const currentView = views.find((view) => view.id === currentViewId);
if (isDefined(currentView)) {
setCurrentRecordFilters(
mapViewFiltersToFilters(
currentView.viewFilters,
availableFilterDefinitions,
),
);
}
}
}, [
isDataPrefetched,
views,
availableFilterDefinitions,
currentViewId,
setCurrentRecordFilters,
]);
return null;
};

View File

@ -0,0 +1,198 @@
import { act, renderHook } from '@testing-library/react';
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
import { RecordFilterDefinition } from '@/object-record/record-filter/types/RecordFilterDefinition';
import { usePrefetchedData } from '@/prefetch/hooks/usePrefetchedData';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { availableFilterDefinitionsComponentState } from '@/views/states/availableFilterDefinitionsComponentState';
import { currentViewIdComponentState } from '@/views/states/currentViewIdComponentState';
import { ViewFilter } from '@/views/types/ViewFilter';
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper';
import { useApplyCurrentViewFiltersToCurrentRecordFilters } from '../useApplyCurrentViewFiltersToCurrentRecordFilters';
jest.mock('@/prefetch/hooks/usePrefetchedData');
describe('useApplyCurrentViewFiltersToCurrentRecordFilters', () => {
const mockFilterDefinition: RecordFilterDefinition = {
fieldMetadataId: 'field-1',
label: 'Test Field',
type: 'TEXT',
iconName: 'IconText',
};
const mockViewFilter: ViewFilter = {
__typename: 'ViewFilter',
id: 'filter-1',
fieldMetadataId: 'field-1',
operand: ViewFilterOperand.Contains,
value: 'test',
displayValue: 'test',
viewFilterGroupId: 'group-1',
positionInViewFilterGroup: 0,
definition: mockFilterDefinition,
};
const mockView = {
id: 'view-1',
name: 'Test View',
objectMetadataId: 'object-1',
viewFilters: [mockViewFilter],
};
beforeEach(() => {
(usePrefetchedData as jest.Mock).mockReturnValue({
records: [mockView],
});
});
it('should apply filters from current view', () => {
const { result } = renderHook(
() => {
const { applyCurrentViewFiltersToCurrentRecordFilters } =
useApplyCurrentViewFiltersToCurrentRecordFilters();
const currentFilters = useRecoilComponentValueV2(
currentRecordFiltersComponentState,
);
return {
applyCurrentViewFiltersToCurrentRecordFilters,
currentFilters,
};
},
{
wrapper: getJestMetadataAndApolloMocksWrapper({
onInitializeRecoilSnapshot: (snapshot) => {
snapshot.set(
currentViewIdComponentState.atomFamily({
instanceId: 'instanceId',
}),
mockView.id,
);
snapshot.set(
availableFilterDefinitionsComponentState.atomFamily({
instanceId: 'instanceId',
}),
[mockFilterDefinition],
);
},
}),
},
);
act(() => {
result.current.applyCurrentViewFiltersToCurrentRecordFilters();
});
expect(result.current.currentFilters).toEqual([
{
id: mockViewFilter.id,
fieldMetadataId: mockViewFilter.fieldMetadataId,
value: mockViewFilter.value,
displayValue: mockViewFilter.displayValue,
operand: mockViewFilter.operand,
viewFilterGroupId: mockViewFilter.viewFilterGroupId,
positionInViewFilterGroup: mockViewFilter.positionInViewFilterGroup,
definition: mockFilterDefinition,
},
]);
});
it('should not apply filters when current view is not found', () => {
(usePrefetchedData as jest.Mock).mockReturnValue({
records: [],
});
const { result } = renderHook(
() => {
const { applyCurrentViewFiltersToCurrentRecordFilters } =
useApplyCurrentViewFiltersToCurrentRecordFilters();
const currentFilters = useRecoilComponentValueV2(
currentRecordFiltersComponentState,
);
return {
applyCurrentViewFiltersToCurrentRecordFilters,
currentFilters,
};
},
{
wrapper: getJestMetadataAndApolloMocksWrapper({
onInitializeRecoilSnapshot: (snapshot) => {
snapshot.set(
currentViewIdComponentState.atomFamily({
instanceId: 'instanceId',
}),
mockView.id,
);
snapshot.set(
availableFilterDefinitionsComponentState.atomFamily({
instanceId: 'instanceId',
}),
[mockFilterDefinition],
);
},
}),
},
);
act(() => {
result.current.applyCurrentViewFiltersToCurrentRecordFilters();
});
expect(result.current.currentFilters).toEqual([]);
});
it('should handle view with empty filters', () => {
const viewWithNoFilters = {
...mockView,
viewFilters: [],
};
(usePrefetchedData as jest.Mock).mockReturnValue({
records: [viewWithNoFilters],
});
const { result } = renderHook(
() => {
const { applyCurrentViewFiltersToCurrentRecordFilters } =
useApplyCurrentViewFiltersToCurrentRecordFilters();
const currentFilters = useRecoilComponentValueV2(
currentRecordFiltersComponentState,
);
return {
applyCurrentViewFiltersToCurrentRecordFilters,
currentFilters,
};
},
{
wrapper: getJestMetadataAndApolloMocksWrapper({
onInitializeRecoilSnapshot: (snapshot) => {
snapshot.set(
currentViewIdComponentState.atomFamily({
instanceId: 'instanceId',
}),
mockView.id,
);
snapshot.set(
availableFilterDefinitionsComponentState.atomFamily({
instanceId: 'instanceId',
}),
[mockFilterDefinition],
);
},
}),
},
);
act(() => {
result.current.applyCurrentViewFiltersToCurrentRecordFilters();
});
expect(result.current.currentFilters).toEqual([]);
});
});

View File

@ -0,0 +1,108 @@
import { act, renderHook } from '@testing-library/react';
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
import { RecordFilterDefinition } from '@/object-record/record-filter/types/RecordFilterDefinition';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { availableFilterDefinitionsComponentState } from '@/views/states/availableFilterDefinitionsComponentState';
import { ViewFilter } from '@/views/types/ViewFilter';
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper';
import { useApplyViewFiltersToCurrentRecordFilters } from '../useApplyViewFiltersToCurrentRecordFilters';
describe('useApplyViewFiltersToCurrentRecordFilters', () => {
const mockAvailableFilterDefinition: RecordFilterDefinition = {
fieldMetadataId: 'field-1',
label: 'Test Field',
type: 'TEXT',
iconName: 'IconText',
};
const mockViewFilter: ViewFilter = {
__typename: 'ViewFilter',
id: 'filter-1',
fieldMetadataId: 'field-1',
operand: ViewFilterOperand.Contains,
value: 'test',
displayValue: 'test',
viewFilterGroupId: 'group-1',
positionInViewFilterGroup: 0,
definition: mockAvailableFilterDefinition,
};
it('should apply view filters to current record filters', () => {
const { result } = renderHook(
() => {
const { applyViewFiltersToCurrentRecordFilters } =
useApplyViewFiltersToCurrentRecordFilters();
const currentFilters = useRecoilComponentValueV2(
currentRecordFiltersComponentState,
);
return { applyViewFiltersToCurrentRecordFilters, currentFilters };
},
{
wrapper: getJestMetadataAndApolloMocksWrapper({
onInitializeRecoilSnapshot: (snapshot) => {
snapshot.set(
availableFilterDefinitionsComponentState.atomFamily({
instanceId: 'instanceId',
}),
[mockAvailableFilterDefinition],
);
},
}),
},
);
act(() => {
result.current.applyViewFiltersToCurrentRecordFilters([mockViewFilter]);
});
expect(result.current.currentFilters).toEqual([
{
id: mockViewFilter.id,
fieldMetadataId: mockViewFilter.fieldMetadataId,
value: mockViewFilter.value,
displayValue: mockViewFilter.displayValue,
operand: mockViewFilter.operand,
viewFilterGroupId: mockViewFilter.viewFilterGroupId,
positionInViewFilterGroup: mockViewFilter.positionInViewFilterGroup,
definition: mockAvailableFilterDefinition,
},
]);
});
it('should handle empty view filters array', () => {
const { result } = renderHook(
() => {
const { applyViewFiltersToCurrentRecordFilters } =
useApplyViewFiltersToCurrentRecordFilters();
const currentFilters = useRecoilComponentValueV2(
currentRecordFiltersComponentState,
);
return { applyViewFiltersToCurrentRecordFilters, currentFilters };
},
{
wrapper: getJestMetadataAndApolloMocksWrapper({
onInitializeRecoilSnapshot: (snapshot) => {
snapshot.set(
availableFilterDefinitionsComponentState.atomFamily({
instanceId: 'instanceId',
}),
[mockAvailableFilterDefinition],
);
},
}),
},
);
act(() => {
result.current.applyViewFiltersToCurrentRecordFilters([]);
});
expect(result.current.currentFilters).toEqual([]);
});
});

View File

@ -0,0 +1,42 @@
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
import { usePrefetchedData } from '@/prefetch/hooks/usePrefetchedData';
import { PrefetchKey } from '@/prefetch/types/PrefetchKey';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
import { availableFilterDefinitionsComponentState } from '@/views/states/availableFilterDefinitionsComponentState';
import { currentViewIdComponentState } from '@/views/states/currentViewIdComponentState';
import { View } from '@/views/types/View';
import { mapViewFiltersToFilters } from '@/views/utils/mapViewFiltersToFilters';
import { isDefined } from 'twenty-ui';
export const useApplyCurrentViewFiltersToCurrentRecordFilters = () => {
const { records: views } = usePrefetchedData<View>(PrefetchKey.AllViews);
const currentViewId = useRecoilComponentValueV2(currentViewIdComponentState);
const setCurrentRecordFilters = useSetRecoilComponentStateV2(
currentRecordFiltersComponentState,
);
const availableFilterDefinitions = useRecoilComponentValueV2(
availableFilterDefinitionsComponentState,
);
const applyCurrentViewFiltersToCurrentRecordFilters = () => {
const currentView = views.find((view) => view.id === currentViewId);
if (isDefined(currentView)) {
setCurrentRecordFilters(
mapViewFiltersToFilters(
currentView.viewFilters,
availableFilterDefinitions,
),
);
}
};
return {
applyCurrentViewFiltersToCurrentRecordFilters,
};
};

View File

@ -0,0 +1,31 @@
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
import { availableFilterDefinitionsComponentState } from '@/views/states/availableFilterDefinitionsComponentState';
import { ViewFilter } from '@/views/types/ViewFilter';
import { mapViewFiltersToFilters } from '@/views/utils/mapViewFiltersToFilters';
export const useApplyViewFiltersToCurrentRecordFilters = () => {
const setCurrentRecordFilters = useSetRecoilComponentStateV2(
currentRecordFiltersComponentState,
);
const availableFilterDefinitions = useRecoilComponentValueV2(
availableFilterDefinitionsComponentState,
);
const applyViewFiltersToCurrentRecordFilters = (
viewFilters: ViewFilter[],
) => {
const recordFiltersToApply = mapViewFiltersToFilters(
viewFilters,
availableFilterDefinitions,
);
setCurrentRecordFilters(recordFiltersToApply);
};
return {
applyViewFiltersToCurrentRecordFilters,
};
};

View File

@ -9,6 +9,7 @@ import { ContextStoreComponentInstanceContext } from '@/context-store/states/con
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useObjectNameSingularFromPlural } from '@/object-metadata/hooks/useObjectNameSingularFromPlural';
import { lastShowPageRecordIdState } from '@/object-record/record-field/states/lastShowPageRecordId';
import { RecordFiltersComponentInstanceContext } from '@/object-record/record-filter/states/context/RecordFiltersComponentInstanceContext';
import { RecordIndexContainer } from '@/object-record/record-index/components/RecordIndexContainer';
import { RecordIndexContainerContextStoreNumberOfSelectedRecordsEffect } from '@/object-record/record-index/components/RecordIndexContainerContextStoreNumberOfSelectedRecordsEffect';
import { RecordIndexContainerContextStoreObjectMetadataEffect } from '@/object-record/record-index/components/RecordIndexContainerContextStoreObjectMetadataEffect';
@ -81,28 +82,32 @@ export const RecordIndexPage = () => {
<ViewComponentInstanceContext.Provider
value={{ instanceId: recordIndexId }}
>
<ContextStoreComponentInstanceContext.Provider
value={{
instanceId: getActionMenuIdFromRecordIndexId(recordIndexId),
}}
<RecordFiltersComponentInstanceContext.Provider
value={{ instanceId: recordIndexId }}
>
<ActionMenuComponentInstanceContext.Provider
<ContextStoreComponentInstanceContext.Provider
value={{
instanceId: getActionMenuIdFromRecordIndexId(recordIndexId),
}}
>
<PageTitle title={`${capitalize(objectNamePlural)}`} />
<RecordIndexPageHeader />
<PageBody>
<StyledIndexContainer>
<RecordIndexContainerContextStoreObjectMetadataEffect />
<RecordIndexContainerContextStoreNumberOfSelectedRecordsEffect />
<MainContextStoreComponentInstanceIdSetterEffect />
<RecordIndexContainer />
</StyledIndexContainer>
</PageBody>
</ActionMenuComponentInstanceContext.Provider>
</ContextStoreComponentInstanceContext.Provider>
<ActionMenuComponentInstanceContext.Provider
value={{
instanceId: getActionMenuIdFromRecordIndexId(recordIndexId),
}}
>
<PageTitle title={`${capitalize(objectNamePlural)}`} />
<RecordIndexPageHeader />
<PageBody>
<StyledIndexContainer>
<RecordIndexContainerContextStoreObjectMetadataEffect />
<RecordIndexContainerContextStoreNumberOfSelectedRecordsEffect />
<MainContextStoreComponentInstanceIdSetterEffect />
<RecordIndexContainer />
</StyledIndexContainer>
</PageBody>
</ActionMenuComponentInstanceContext.Provider>
</ContextStoreComponentInstanceContext.Provider>
</RecordFiltersComponentInstanceContext.Provider>
</ViewComponentInstanceContext.Provider>
</RecordIndexContextProvider>
</PageContainer>

View File

@ -5,6 +5,7 @@ import { ActionMenuComponentInstanceContext } from '@/action-menu/states/context
import { TimelineActivityContext } from '@/activities/timeline-activities/contexts/TimelineActivityContext';
import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { RecordFiltersComponentInstanceContext } from '@/object-record/record-filter/states/context/RecordFiltersComponentInstanceContext';
import { RecordShowContainer } from '@/object-record/record-show/components/RecordShowContainer';
import { useRecordShowPage } from '@/object-record/record-show/hooks/useRecordShowPage';
import { RecordValueSetterEffect } from '@/object-record/record-store/components/RecordValueSetterEffect';
@ -46,62 +47,68 @@ export const RecordShowPage = () => {
return (
<RecordFieldValueSelectorContextProvider>
<ContextStoreComponentInstanceContext.Provider
<RecordFiltersComponentInstanceContext.Provider
value={{ instanceId: `record-show-${objectRecordId}` }}
>
<ActionMenuComponentInstanceContext.Provider
<ContextStoreComponentInstanceContext.Provider
value={{ instanceId: `record-show-${objectRecordId}` }}
>
<RecordValueSetterEffect recordId={objectRecordId} />
<PageContainer>
<PageTitle title={pageTitle} />
<RecordShowPageHeader
objectNameSingular={objectNameSingular}
objectRecordId={objectRecordId}
headerIcon={headerIcon}
>
<>
{!isCommandMenuV2Enabled &&
objectNameSingular === CoreObjectNameSingular.Workflow && (
<RecordShowPageWorkflowHeader workflowId={objectRecordId} />
)}
{!isCommandMenuV2Enabled &&
objectNameSingular ===
CoreObjectNameSingular.WorkflowVersion && (
<RecordShowPageWorkflowVersionHeader
workflowVersionId={objectRecordId}
<ActionMenuComponentInstanceContext.Provider
value={{ instanceId: `record-show-${objectRecordId}` }}
>
<RecordValueSetterEffect recordId={objectRecordId} />
<PageContainer>
<PageTitle title={pageTitle} />
<RecordShowPageHeader
objectNameSingular={objectNameSingular}
objectRecordId={objectRecordId}
headerIcon={headerIcon}
>
<>
{!isCommandMenuV2Enabled &&
objectNameSingular === CoreObjectNameSingular.Workflow && (
<RecordShowPageWorkflowHeader
workflowId={objectRecordId}
/>
)}
{!isCommandMenuV2Enabled &&
objectNameSingular ===
CoreObjectNameSingular.WorkflowVersion && (
<RecordShowPageWorkflowVersionHeader
workflowVersionId={objectRecordId}
/>
)}
{(isCommandMenuV2Enabled ||
(objectNameSingular !== CoreObjectNameSingular.Workflow &&
objectNameSingular !==
CoreObjectNameSingular.WorkflowVersion)) && (
<RecordShowActionMenu
{...{
isFavorite,
record,
handleFavoriteButtonClick,
objectMetadataItem,
objectNameSingular,
}}
/>
)}
{(isCommandMenuV2Enabled ||
(objectNameSingular !== CoreObjectNameSingular.Workflow &&
objectNameSingular !==
CoreObjectNameSingular.WorkflowVersion)) && (
<RecordShowActionMenu
{...{
isFavorite,
record,
handleFavoriteButtonClick,
objectMetadataItem,
objectNameSingular,
}}
</>
</RecordShowPageHeader>
<PageBody>
<TimelineActivityContext.Provider
value={{ labelIdentifierValue: pageName }}
>
<RecordShowContainer
objectNameSingular={objectNameSingular}
objectRecordId={objectRecordId}
loading={loading}
/>
)}
</>
</RecordShowPageHeader>
<PageBody>
<TimelineActivityContext.Provider
value={{ labelIdentifierValue: pageName }}
>
<RecordShowContainer
objectNameSingular={objectNameSingular}
objectRecordId={objectRecordId}
loading={loading}
/>
</TimelineActivityContext.Provider>
</PageBody>
</PageContainer>
</ActionMenuComponentInstanceContext.Provider>
</ContextStoreComponentInstanceContext.Provider>
</TimelineActivityContext.Provider>
</PageBody>
</PageContainer>
</ActionMenuComponentInstanceContext.Provider>
</ContextStoreComponentInstanceContext.Provider>
</RecordFiltersComponentInstanceContext.Provider>
</RecordFieldValueSelectorContextProvider>
);
};

View File

@ -22,6 +22,7 @@ import { mockedApolloClient } from '~/testing/mockedApolloClient';
import { RecoilDebugObserverEffect } from '@/debug/components/RecoilDebugObserver';
import { ObjectMetadataItemsProvider } from '@/object-metadata/components/ObjectMetadataItemsProvider';
import { RecordFiltersComponentInstanceContext } from '@/object-record/record-filter/states/context/RecordFiltersComponentInstanceContext';
import { PrefetchDataProvider } from '@/prefetch/components/PrefetchDataProvider';
import { WorkspaceProviderEffect } from '@/workspace/components/WorkspaceProviderEffect';
import { i18n } from '@lingui/core';
@ -88,7 +89,13 @@ const Providers = () => {
<SnackBarProviderScope snackBarManagerScopeId="snack-bar-manager">
<IconsProvider>
<PrefetchDataProvider>
<Outlet />
<RecordFiltersComponentInstanceContext.Provider
value={{
instanceId: 'storybook-test-record-filters',
}}
>
<Outlet />
</RecordFiltersComponentInstanceContext.Provider>
</PrefetchDataProvider>
</IconsProvider>
</SnackBarProviderScope>

View File

@ -1,6 +1,7 @@
import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext';
import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext';
import { ContextStoreTargetedRecordsRule } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
import { RecordFiltersComponentInstanceContext } from '@/object-record/record-filter/states/context/RecordFiltersComponentInstanceContext';
import { MockedResponse } from '@apollo/client/testing';
import { ReactNode } from 'react';
import { MutableSnapshot } from 'recoil';
@ -33,25 +34,33 @@ export const getJestMetadataAndApolloMocksAndActionMenuWrapper = ({
return ({ children }: { children: ReactNode }) => (
<Wrapper>
<ContextStoreComponentInstanceContext.Provider
value={{ instanceId: componentInstanceId }}
<RecordFiltersComponentInstanceContext.Provider
value={{
instanceId: componentInstanceId,
}}
>
<ActionMenuComponentInstanceContext.Provider
<ContextStoreComponentInstanceContext.Provider
value={{ instanceId: componentInstanceId }}
>
<JestContextStoreSetter
contextStoreTargetedRecordsRule={contextStoreTargetedRecordsRule}
contextStoreNumberOfSelectedRecords={
contextStoreNumberOfSelectedRecords
}
contextStoreCurrentObjectMetadataNameSingular={
contextStoreCurrentObjectMetadataNameSingular
}
<ActionMenuComponentInstanceContext.Provider
value={{
instanceId: componentInstanceId,
}}
>
{children}
</JestContextStoreSetter>
</ActionMenuComponentInstanceContext.Provider>
</ContextStoreComponentInstanceContext.Provider>
<JestContextStoreSetter
contextStoreTargetedRecordsRule={contextStoreTargetedRecordsRule}
contextStoreNumberOfSelectedRecords={
contextStoreNumberOfSelectedRecords
}
contextStoreCurrentObjectMetadataNameSingular={
contextStoreCurrentObjectMetadataNameSingular
}
>
{children}
</JestContextStoreSetter>
</ActionMenuComponentInstanceContext.Provider>
</ContextStoreComponentInstanceContext.Provider>
</RecordFiltersComponentInstanceContext.Provider>
</Wrapper>
);
};

View File

@ -2,14 +2,16 @@ import { MockedProvider, MockedResponse } from '@apollo/client/testing';
import { ReactNode } from 'react';
import { MutableSnapshot, RecoilRoot } from 'recoil';
import { RecordFiltersComponentInstanceContext } from '@/object-record/record-filter/states/context/RecordFiltersComponentInstanceContext';
import { SnackBarProviderScope } from '@/ui/feedback/snack-bar-manager/scopes/SnackBarProviderScope';
import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext';
import { JestObjectMetadataItemSetter } from '~/testing/jest/JestObjectMetadataItemSetter';
export const getJestMetadataAndApolloMocksWrapper = ({
apolloMocks,
onInitializeRecoilSnapshot,
}: {
apolloMocks:
apolloMocks?:
| readonly MockedResponse<Record<string, any>, Record<string, any>>[]
| undefined;
onInitializeRecoilSnapshot?: (snapshot: MutableSnapshot) => void;
@ -18,9 +20,17 @@ export const getJestMetadataAndApolloMocksWrapper = ({
<RecoilRoot initializeState={onInitializeRecoilSnapshot}>
<SnackBarProviderScope snackBarManagerScopeId="snack-bar-manager">
<MockedProvider mocks={apolloMocks} addTypename={false}>
<JestObjectMetadataItemSetter>
{children}
</JestObjectMetadataItemSetter>
<RecordFiltersComponentInstanceContext.Provider
value={{ instanceId: 'instanceId' }}
>
<ViewComponentInstanceContext.Provider
value={{ instanceId: 'instanceId' }}
>
<JestObjectMetadataItemSetter>
{children}
</JestObjectMetadataItemSetter>
</ViewComponentInstanceContext.Provider>
</RecordFiltersComponentInstanceContext.Provider>
</MockedProvider>
</SnackBarProviderScope>
</RecoilRoot>