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 { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState';
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState'; import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext'; 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 { isBottomBarOpenedComponentState } from '@/ui/layout/bottom-bar/states/isBottomBarOpenedComponentState';
import { expect, jest } from '@storybook/jest'; import { expect, jest } from '@storybook/jest';
import { Meta, StoryObj } from '@storybook/react'; import { Meta, StoryObj } from '@storybook/react';
@ -25,58 +26,63 @@ const meta: Meta<typeof RecordIndexActionMenuBar> = {
decorators: [ decorators: [
RouterDecorator, RouterDecorator,
(Story) => ( (Story) => (
<ContextStoreComponentInstanceContext.Provider <RecordFiltersComponentInstanceContext.Provider
value={{ instanceId: 'story-action-menu' }} value={{ instanceId: 'story-action-menu' }}
> >
<RecoilRoot <ContextStoreComponentInstanceContext.Provider
initializeState={({ set }) => { value={{ instanceId: 'story-action-menu' }}
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,
);
}}
> >
<ActionMenuComponentInstanceContext.Provider <RecoilRoot
value={{ instanceId: 'story-action-menu' }} 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
</ActionMenuComponentInstanceContext.Provider> value={{ instanceId: 'story-action-menu' }}
</RecoilRoot> >
</ContextStoreComponentInstanceContext.Provider> <Story />
</ActionMenuComponentInstanceContext.Provider>
</RecoilRoot>
</ContextStoreComponentInstanceContext.Provider>
</RecordFiltersComponentInstanceContext.Provider>
), ),
], ],
args: { args: {

View File

@ -9,6 +9,7 @@ import { useCommandMenuHotKeys } from '@/command-menu/hooks/useCommandMenuHotKey
import { isCommandMenuOpenedState } from '@/command-menu/states/isCommandMenuOpenedState'; import { isCommandMenuOpenedState } from '@/command-menu/states/isCommandMenuOpenedState';
import { CommandMenuAnimationVariant } from '@/command-menu/types/CommandMenuAnimationVariant'; import { CommandMenuAnimationVariant } from '@/command-menu/types/CommandMenuAnimationVariant';
import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext'; 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 { AppHotkeyScope } from '@/ui/utilities/hotkey/types/AppHotkeyScope';
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside'; import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
import { workflowReactFlowRefState } from '@/workflow/workflow-diagram/states/workflowReactFlowRefState'; import { workflowReactFlowRefState } from '@/workflow/workflow-diagram/states/workflowReactFlowRefState';
@ -74,37 +75,41 @@ export const CommandMenuContainer = ({
const theme = useTheme(); const theme = useTheme();
return ( return (
<ContextStoreComponentInstanceContext.Provider <RecordFiltersComponentInstanceContext.Provider
value={{ instanceId: 'command-menu' }} value={{ instanceId: 'command-menu' }}
> >
<ActionMenuComponentInstanceContext.Provider <ContextStoreComponentInstanceContext.Provider
value={{ instanceId: 'command-menu' }} value={{ instanceId: 'command-menu' }}
> >
<ActionMenuContext.Provider <ActionMenuComponentInstanceContext.Provider
value={{ value={{ instanceId: 'command-menu' }}
isInRightDrawer: false,
onActionExecutedCallback: toggleCommandMenu,
}}
> >
<RecordActionMenuEntriesSetter /> <ActionMenuContext.Provider
{isWorkflowEnabled && <RecordAgnosticActionsSetterEffect />} value={{
<ActionMenuConfirmationModals /> isInRightDrawer: false,
{isCommandMenuOpened && ( onActionExecutedCallback: toggleCommandMenu,
<StyledCommandMenu }}
data-testid="command-menu" >
ref={commandMenuRef} <RecordActionMenuEntriesSetter />
className="command-menu" {isWorkflowEnabled && <RecordAgnosticActionsSetterEffect />}
animate={targetVariantForAnimation} <ActionMenuConfirmationModals />
initial="closed" {isCommandMenuOpened && (
exit="closed" <StyledCommandMenu
variants={COMMAND_MENU_ANIMATION_VARIANTS} data-testid="command-menu"
transition={{ duration: theme.animation.duration.normal }} ref={commandMenuRef}
> className="command-menu"
{children} animate={targetVariantForAnimation}
</StyledCommandMenu> initial="closed"
)} exit="closed"
</ActionMenuContext.Provider> variants={COMMAND_MENU_ANIMATION_VARIANTS}
</ActionMenuComponentInstanceContext.Provider> transition={{ duration: theme.animation.duration.normal }}
</ContextStoreComponentInstanceContext.Provider> >
{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 { CommandMenuRouter } from '@/command-menu/components/CommandMenuRouter';
import { isCommandMenuOpenedState } from '@/command-menu/states/isCommandMenuOpenedState'; import { isCommandMenuOpenedState } from '@/command-menu/states/isCommandMenuOpenedState';
import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext'; 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 { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator';
import { JestContextStoreSetter } from '~/testing/jest/JestContextStoreSetter'; import { JestContextStoreSetter } from '~/testing/jest/JestContextStoreSetter';
import { CommandMenu } from '../CommandMenu'; import { CommandMenu } from '../CommandMenu';
@ -29,17 +30,21 @@ const openTimeout = 50;
const ContextStoreDecorator: Decorator = (Story) => { const ContextStoreDecorator: Decorator = (Story) => {
return ( return (
<ContextStoreComponentInstanceContext.Provider <RecordFiltersComponentInstanceContext.Provider
value={{ instanceId: 'command-menu' }} value={{ instanceId: 'command-menu' }}
> >
<ActionMenuComponentInstanceContext.Provider <ContextStoreComponentInstanceContext.Provider
value={{ instanceId: 'command-menu' }} value={{ instanceId: 'command-menu' }}
> >
<JestContextStoreSetter contextStoreCurrentObjectMetadataNameSingular="company"> <ActionMenuComponentInstanceContext.Provider
<Story /> value={{ instanceId: 'command-menu' }}
</JestContextStoreSetter> >
</ActionMenuComponentInstanceContext.Provider> <JestContextStoreSetter contextStoreCurrentObjectMetadataNameSingular="company">
</ContextStoreComponentInstanceContext.Provider> <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 { useReadFindManyRecordsQueryInCache } from '@/object-record/cache/hooks/useReadFindManyRecordsQueryInCache';
import { useDeleteOneRecord } from '@/object-record/hooks/useDeleteOneRecord'; import { useDeleteOneRecord } from '@/object-record/hooks/useDeleteOneRecord';
import { PREFETCH_CONFIG } from '@/prefetch/constants/PrefetchConfig'; 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 { PrefetchKey } from '@/prefetch/types/PrefetchKey';
export const useDeleteFavoriteFolder = () => { export const useDeleteFavoriteFolder = () => {
@ -12,9 +12,10 @@ export const useDeleteFavoriteFolder = () => {
objectNameSingular: CoreObjectNameSingular.FavoriteFolder, objectNameSingular: CoreObjectNameSingular.FavoriteFolder,
}); });
const { upsertRecordsInCache } = usePrefetchRunQuery<Favorite>({ const { upsertRecordsInCache } =
prefetchKey: PrefetchKey.AllFavorites, useUpsertRecordsInCacheForPrefetchKey<Favorite>({
}); prefetchKey: PrefetchKey.AllFavorites,
});
const { objectMetadataItem } = useObjectMetadataItem({ const { objectMetadataItem } = useObjectMetadataItem({
objectNameSingular: objectNameSingular:

View File

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

View File

@ -1,6 +1,6 @@
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState'; import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { FavoriteFolder } from '@/favorites/types/FavoriteFolder'; 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 { usePrefetchedData } from '@/prefetch/hooks/usePrefetchedData';
import { PrefetchKey } from '@/prefetch/types/PrefetchKey'; import { PrefetchKey } from '@/prefetch/types/PrefetchKey';
import { useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
@ -26,7 +26,7 @@ export const usePrefetchedFavoritesFoldersData =
); );
const { upsertRecordsInCache: upsertFavoriteFolders } = const { upsertRecordsInCache: upsertFavoriteFolders } =
usePrefetchRunQuery<FavoriteFolder>({ useUpsertRecordsInCacheForPrefetchKey<FavoriteFolder>({
prefetchKey: PrefetchKey.AllFavoritesFolders, 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 { selectedOperandInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/selectedOperandInDropdownComponentState';
import { getActorSourceMultiSelectOptions } from '@/object-record/object-filter-dropdown/utils/getActorSourceMultiSelectOptions'; import { getActorSourceMultiSelectOptions } from '@/object-record/object-filter-dropdown/utils/getActorSourceMultiSelectOptions';
import { useApplyRecordFilter } from '@/object-record/record-filter/hooks/useApplyRecordFilter'; 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 { RelationPickerHotkeyScope } from '@/object-record/relation-picker/types/RelationPickerHotkeyScope';
import { MultipleSelectDropdown } from '@/object-record/select/components/MultipleSelectDropdown'; import { MultipleSelectDropdown } from '@/object-record/select/components/MultipleSelectDropdown';
import { SelectableItem } from '@/object-record/select/types/SelectableItem'; import { SelectableItem } from '@/object-record/select/types/SelectableItem';
@ -61,6 +62,7 @@ export const ObjectFilterDropdownSourceSelect = ({
const { currentViewWithCombinedFiltersAndSorts } = const { currentViewWithCombinedFiltersAndSorts } =
useGetCurrentView(viewComponentId); useGetCurrentView(viewComponentId);
// TODO: this should be removed as it is not consistent across re-renders
const [fieldId] = useState(v4()); const [fieldId] = useState(v4());
const sourceTypes = getActorSourceMultiSelectOptions( const sourceTypes = getActorSourceMultiSelectOptions(
@ -73,6 +75,8 @@ export const ObjectFilterDropdownSourceSelect = ({
const { emptyRecordFilter } = useEmptyRecordFilter(); const { emptyRecordFilter } = useEmptyRecordFilter();
const { removeRecordFilter } = useRemoveRecordFilter();
const handleMultipleItemSelectChange = ( const handleMultipleItemSelectChange = (
itemToSelect: SelectableItem, itemToSelect: SelectableItem,
newSelectedValue: boolean, newSelectedValue: boolean,
@ -83,8 +87,13 @@ export const ObjectFilterDropdownSourceSelect = ({
(id) => id !== itemToSelect.id, (id) => id !== itemToSelect.id,
); );
if (!filterDefinitionUsedInDropdown) {
throw new Error('Filter definition used in dropdown should be defined');
}
if (newSelectedItemIds.length === 0) { if (newSelectedItemIds.length === 0) {
emptyRecordFilter(); emptyRecordFilter();
removeRecordFilter(filterDefinitionUsedInDropdown.fieldMetadataId);
deleteCombinedViewFilter(fieldId); deleteCombinedViewFilter(fieldId);
return; return;
} }

View File

@ -5,6 +5,7 @@ import { CoreObjectNamePlural } from '@/object-metadata/types/CoreObjectNamePlur
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { MultipleFiltersDropdownButton } from '@/object-record/object-filter-dropdown/components/MultipleFiltersDropdownButton'; import { MultipleFiltersDropdownButton } from '@/object-record/object-filter-dropdown/components/MultipleFiltersDropdownButton';
import { ObjectFilterDropdownComponentInstanceContext } from '@/object-record/object-filter-dropdown/states/contexts/ObjectFilterDropdownComponentInstanceContext'; 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 { RecordIndexContextProvider } from '@/object-record/record-index/contexts/RecordIndexContext';
import { RecordTableComponentInstanceContext } from '@/object-record/record-table/states/context/RecordTableComponentInstanceContext'; import { RecordTableComponentInstanceContext } from '@/object-record/record-table/states/context/RecordTableComponentInstanceContext';
import { tableColumnsComponentState } from '@/object-record/record-table/states/tableColumnsComponentState'; import { tableColumnsComponentState } from '@/object-record/record-table/states/tableColumnsComponentState';
@ -107,17 +108,21 @@ const meta: Meta<typeof MultipleFiltersDropdownButton> = {
recordIndexId: instanceId, recordIndexId: instanceId,
}} }}
> >
<ObjectFilterDropdownComponentInstanceContext.Provider <RecordFiltersComponentInstanceContext.Provider
value={{ instanceId }} value={{ instanceId }}
> >
<RecordTableComponentInstanceContext.Provider <ObjectFilterDropdownComponentInstanceContext.Provider
value={{ instanceId: instanceId, onColumnsChange: () => {} }} value={{ instanceId }}
> >
<ViewComponentInstanceContext.Provider value={{ instanceId }}> <RecordTableComponentInstanceContext.Provider
<Story /> value={{ instanceId: instanceId, onColumnsChange: () => {} }}
</ViewComponentInstanceContext.Provider> >
</RecordTableComponentInstanceContext.Provider> <ViewComponentInstanceContext.Provider value={{ instanceId }}>
</ObjectFilterDropdownComponentInstanceContext.Provider> <Story />
</ViewComponentInstanceContext.Provider>
</RecordTableComponentInstanceContext.Provider>
</ObjectFilterDropdownComponentInstanceContext.Provider>
</RecordFiltersComponentInstanceContext.Provider>
</RecordIndexContextProvider> </RecordIndexContextProvider>
); );
}, },

View File

@ -1,5 +1,6 @@
import { ObjectFilterDropdownComponentInstanceContext } from '@/object-record/object-filter-dropdown/states/contexts/ObjectFilterDropdownComponentInstanceContext'; import { ObjectFilterDropdownComponentInstanceContext } from '@/object-record/object-filter-dropdown/states/contexts/ObjectFilterDropdownComponentInstanceContext';
import { RecordFilterDefinition } from '@/object-record/record-filter/types/RecordFilterDefinition'; import { RecordFilterDefinition } from '@/object-record/record-filter/types/RecordFilterDefinition';
import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2'; import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2';
export const availableFilterDefinitionsComponentState = 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 { OBJECT_OPTIONS_DROPDOWN_ID } from '@/object-record/object-options-dropdown/constants/ObjectOptionsDropdownId';
import { ObjectOptionsDropdownContext } from '@/object-record/object-options-dropdown/states/contexts/ObjectOptionsDropdownContext'; import { ObjectOptionsDropdownContext } from '@/object-record/object-options-dropdown/states/contexts/ObjectOptionsDropdownContext';
import { ObjectOptionsContentId } from '@/object-record/object-options-dropdown/types/ObjectOptionsContentId'; 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 { RecordIndexContextProvider } from '@/object-record/record-index/contexts/RecordIndexContext';
import { RecordTableComponentInstanceContext } from '@/object-record/record-table/states/context/RecordTableComponentInstanceContext'; import { RecordTableComponentInstanceContext } from '@/object-record/record-table/states/context/RecordTableComponentInstanceContext';
import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu'; import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu';
@ -37,22 +38,26 @@ const meta: Meta<typeof ObjectOptionsDropdownContent> = {
}, [setObjectMetadataItems]); }, [setObjectMetadataItems]);
return ( return (
<RecordTableComponentInstanceContext.Provider <RecordFiltersComponentInstanceContext.Provider
value={{ instanceId, onColumnsChange: () => {} }} value={{ instanceId: 'object-options-dropdown' }}
> >
<ViewComponentInstanceContext.Provider value={{ instanceId }}> <RecordTableComponentInstanceContext.Provider
<ContextStoreComponentInstanceContext.Provider value={{ instanceId, onColumnsChange: () => {} }}
value={{ instanceId }} >
> <ViewComponentInstanceContext.Provider value={{ instanceId }}>
<MemoryRouter <ContextStoreComponentInstanceContext.Provider
initialEntries={['/one', '/two', { pathname: '/three' }]} value={{ instanceId }}
initialIndex={1}
> >
<Story /> <MemoryRouter
</MemoryRouter> initialEntries={['/one', '/two', { pathname: '/three' }]}
</ContextStoreComponentInstanceContext.Provider> initialIndex={1}
</ViewComponentInstanceContext.Provider> >
</RecordTableComponentInstanceContext.Provider> <Story />
</MemoryRouter>
</ContextStoreComponentInstanceContext.Provider>
</ViewComponentInstanceContext.Provider>
</RecordTableComponentInstanceContext.Provider>
</RecordFiltersComponentInstanceContext.Provider>
); );
}, },
ObjectMetadataItemsDecorator, 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 { 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 { 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 { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
import { useUpsertCombinedViewFilters } from '@/views/hooks/useUpsertCombinedViewFilters'; import { useUpsertCombinedViewFilters } from '@/views/hooks/useUpsertCombinedViewFilters';
import { useRecoilCallback } from 'recoil'; import { useRecoilCallback } from 'recoil';
@ -14,32 +13,19 @@ export const useApplyRecordFilter = (componentInstanceId?: string) => {
componentInstanceId, componentInstanceId,
); );
const onFilterSelectCallbackState = useRecoilComponentCallbackStateV2( const { upsertRecordFilter } = useUpsertRecordFilter();
onFilterSelectComponentState,
componentInstanceId,
);
const applyRecordFilter = useRecoilCallback( const applyRecordFilter = useRecoilCallback(
({ set, snapshot }) => ({ set }) =>
(filter: RecordFilter | null) => { (filter: RecordFilter | null) => {
set(selectedFilterCallbackState, filter); set(selectedFilterCallbackState, filter);
const onFilterSelect = getSnapshotValue(
snapshot,
onFilterSelectCallbackState,
);
if (isDefined(filter)) { if (isDefined(filter)) {
upsertCombinedViewFilter(filter); upsertCombinedViewFilter(filter);
upsertRecordFilter(filter);
} }
onFilterSelect?.(filter);
}, },
[ [selectedFilterCallbackState, upsertCombinedViewFilter, upsertRecordFilter],
selectedFilterCallbackState,
onFilterSelectCallbackState,
upsertCombinedViewFilter,
],
); );
return { 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 { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { turnSortsIntoOrderBy } from '@/object-record/object-sort-dropdown/utils/turnSortsIntoOrderBy'; import { turnSortsIntoOrderBy } from '@/object-record/object-sort-dropdown/utils/turnSortsIntoOrderBy';
import { useFilterValueDependencies } from '@/object-record/record-filter/hooks/useFilterValueDependencies'; 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 { computeViewRecordGqlOperationFilter } from '@/object-record/record-filter/utils/computeViewRecordGqlOperationFilter';
import { useCurrentRecordGroupDefinition } from '@/object-record/record-group/hooks/useCurrentRecordGroupDefinition'; import { useCurrentRecordGroupDefinition } from '@/object-record/record-group/hooks/useCurrentRecordGroupDefinition';
import { useRecordGroupFilter } from '@/object-record/record-group/hooks/useRecordGroupFilter'; 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 { tableSortsComponentState } from '@/object-record/record-table/states/tableSortsComponentState';
import { tableViewFilterGroupsComponentState } from '@/object-record/record-table/states/tableViewFilterGroupsComponentState'; import { tableViewFilterGroupsComponentState } from '@/object-record/record-table/states/tableViewFilterGroupsComponentState';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
@ -27,20 +27,21 @@ export const useFindManyRecordIndexTableParams = (
tableViewFilterGroupsComponentState, tableViewFilterGroupsComponentState,
recordTableId, recordTableId,
); );
const tableFilters = useRecoilComponentValueV2(
tableFiltersComponentState,
recordTableId,
);
const tableSorts = useRecoilComponentValueV2( const tableSorts = useRecoilComponentValueV2(
tableSortsComponentState, tableSortsComponentState,
recordTableId, recordTableId,
); );
const currentRecordFilters = useRecoilComponentValueV2(
currentRecordFiltersComponentState,
);
const { filterValueDependencies } = useFilterValueDependencies(); const { filterValueDependencies } = useFilterValueDependencies();
const stateFilter = computeViewRecordGqlOperationFilter( const stateFilter = computeViewRecordGqlOperationFilter(
filterValueDependencies, filterValueDependencies,
tableFilters, currentRecordFilters,
objectMetadataItem?.fields ?? [], objectMetadataItem?.fields ?? [],
tableViewFilterGroups, tableViewFilterGroups,
); );

View File

@ -5,6 +5,7 @@ import { useColumnDefinitionsFromFieldMetadata } from '@/object-metadata/hooks/u
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useSelectFilterDefinitionUsedInDropdown } from '@/object-record/object-filter-dropdown/hooks/useSelectFilterDefinitionUsedInDropdown'; 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 { RecordFilter } from '@/object-record/record-filter/types/RecordFilter';
import { getRecordFilterOperandsForRecordFilterDefinition } from '@/object-record/record-filter/utils/getRecordFilterOperandsForRecordFilterDefinition'; import { getRecordFilterOperandsForRecordFilterDefinition } from '@/object-record/record-filter/utils/getRecordFilterOperandsForRecordFilterDefinition';
import { isDropdownOpenComponentState } from '@/ui/layout/dropdown/states/isDropdownOpenComponentState'; import { isDropdownOpenComponentState } from '@/ui/layout/dropdown/states/isDropdownOpenComponentState';
@ -33,6 +34,7 @@ export const useHandleToggleColumnFilter = ({
useColumnDefinitionsFromFieldMetadata(objectMetadataItem); useColumnDefinitionsFromFieldMetadata(objectMetadataItem);
const { upsertCombinedViewFilter } = useUpsertCombinedViewFilters(viewBarId); const { upsertCombinedViewFilter } = useUpsertCombinedViewFilters(viewBarId);
const { upsertRecordFilter } = useUpsertRecordFilter();
const openDropdown = useRecoilCallback(({ set }) => { const openDropdown = useRecoilCallback(({ set }) => {
return (dropdownId: string) => { return (dropdownId: string) => {
@ -93,6 +95,8 @@ export const useHandleToggleColumnFilter = ({
value: '', value: '',
}; };
upsertRecordFilter(newFilter);
await upsertCombinedViewFilter(newFilter); await upsertCombinedViewFilter(newFilter);
selectFilterDefinitionUsedInDropdown({ filterDefinition }); selectFilterDefinitionUsedInDropdown({ filterDefinition });
@ -107,6 +111,7 @@ export const useHandleToggleColumnFilter = ({
selectFilterDefinitionUsedInDropdown, selectFilterDefinitionUsedInDropdown,
currentViewWithCombinedFiltersAndSorts, currentViewWithCombinedFiltersAndSorts,
availableFilterDefinitions, availableFilterDefinitions,
upsertRecordFilter,
], ],
); );

View File

@ -4,6 +4,7 @@ import { v4 } from 'uuid';
import { useColumnDefinitionsFromFieldMetadata } from '@/object-metadata/hooks/useColumnDefinitionsFromFieldMetadata'; import { useColumnDefinitionsFromFieldMetadata } from '@/object-metadata/hooks/useColumnDefinitionsFromFieldMetadata';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { getFilterTypeFromFieldType } from '@/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions'; 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 { RecordFilter } from '@/object-record/record-filter/types/RecordFilter';
import { isSoftDeleteFilterActiveComponentState } from '@/object-record/record-table/states/isSoftDeleteFilterActiveComponentState'; import { isSoftDeleteFilterActiveComponentState } from '@/object-record/record-table/states/isSoftDeleteFilterActiveComponentState';
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2'; import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
@ -36,6 +37,8 @@ export const useHandleToggleTrashColumnFilter = ({
viewBarId, viewBarId,
); );
const { upsertRecordFilter } = useUpsertRecordFilter();
const handleToggleTrashColumnFilter = useCallback(() => { const handleToggleTrashColumnFilter = useCallback(() => {
const trashFieldMetadata = objectMetadataItem.fields.find( const trashFieldMetadata = objectMetadataItem.fields.find(
(field: { name: string }) => field.name === 'deletedAt', (field: { name: string }) => field.name === 'deletedAt',
@ -69,8 +72,14 @@ export const useHandleToggleTrashColumnFilter = ({
value: '', value: '',
}; };
upsertRecordFilter(newFilter);
upsertCombinedViewFilter(newFilter); upsertCombinedViewFilter(newFilter);
}, [columnDefinitions, objectMetadataItem, upsertCombinedViewFilter]); }, [
columnDefinitions,
objectMetadataItem,
upsertCombinedViewFilter,
upsertRecordFilter,
]);
const toggleSoftDeleteFilterState = useRecoilCallback( const toggleSoftDeleteFilterState = useRecoilCallback(
({ set }) => ({ set }) =>

View File

@ -2,6 +2,7 @@ import { useRecoilValue } from 'recoil';
import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext'; import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext';
import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext'; 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 { isNewViewableRecordLoadingState } from '@/object-record/record-right-drawer/states/isNewViewableRecordLoading';
import { viewableRecordIdState } from '@/object-record/record-right-drawer/states/viewableRecordIdState'; import { viewableRecordIdState } from '@/object-record/record-right-drawer/states/viewableRecordIdState';
import { viewableRecordNameSingularState } from '@/object-record/record-right-drawer/states/viewableRecordNameSingularState'; import { viewableRecordNameSingularState } from '@/object-record/record-right-drawer/states/viewableRecordNameSingularState';
@ -42,29 +43,33 @@ export const RightDrawerRecord = () => {
); );
return ( return (
<ContextStoreComponentInstanceContext.Provider <RecordFiltersComponentInstanceContext.Provider
value={{ value={{ instanceId: `record-show-${objectRecordId}` }}
instanceId: `record-show-${objectRecordId}`,
}}
> >
<ActionMenuComponentInstanceContext.Provider <ContextStoreComponentInstanceContext.Provider
value={{ instanceId: `record-show-${objectRecordId}` }} value={{
instanceId: `record-show-${objectRecordId}`,
}}
> >
<StyledRightDrawerRecord isMobile={isMobile}> <ActionMenuComponentInstanceContext.Provider
<RecordFieldValueSelectorContextProvider> value={{ instanceId: `record-show-${objectRecordId}` }}
{!isNewViewableRecordLoading && ( >
<RecordValueSetterEffect recordId={objectRecordId} /> <StyledRightDrawerRecord isMobile={isMobile}>
)} <RecordFieldValueSelectorContextProvider>
<RecordShowContainer {!isNewViewableRecordLoading && (
objectNameSingular={objectNameSingular} <RecordValueSetterEffect recordId={objectRecordId} />
objectRecordId={objectRecordId} )}
loading={false} <RecordShowContainer
isInRightDrawer={true} objectNameSingular={objectNameSingular}
isNewRightDrawerItemLoading={isNewViewableRecordLoading} objectRecordId={objectRecordId}
/> loading={false}
</RecordFieldValueSelectorContextProvider> isInRightDrawer={true}
</StyledRightDrawerRecord> isNewRightDrawerItemLoading={isNewViewableRecordLoading}
</ActionMenuComponentInstanceContext.Provider> />
</ContextStoreComponentInstanceContext.Provider> </RecordFieldValueSelectorContextProvider>
</StyledRightDrawerRecord>
</ActionMenuComponentInstanceContext.Provider>
</ContextStoreComponentInstanceContext.Provider>
</RecordFiltersComponentInstanceContext.Provider>
); );
}; };

View File

@ -1,6 +1,7 @@
import { IconFilterOff } from 'twenty-ui'; import { IconFilterOff } from 'twenty-ui';
import { useObjectLabel } from '@/object-metadata/hooks/useObjectLabel'; 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 { useHandleToggleTrashColumnFilter } from '@/object-record/record-index/hooks/useHandleToggleTrashColumnFilter';
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext'; import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { RecordTableEmptyStateDisplay } from '@/object-record/record-table/empty-state/components/RecordTableEmptyStateDisplay'; import { RecordTableEmptyStateDisplay } from '@/object-record/record-table/empty-state/components/RecordTableEmptyStateDisplay';
@ -25,14 +26,22 @@ export const RecordTableEmptyStateSoftDelete = () => {
viewBarId: recordTableId, viewBarId: recordTableId,
}); });
const { removeRecordFilter } = useRemoveRecordFilter();
const handleButtonClick = async () => { const handleButtonClick = async () => {
deleteCombinedViewFilter( const deletedFilter = tableFilters.find(
tableFilters.find( (filter) =>
(filter) => filter.definition.label === 'Deleted' &&
filter.definition.label === 'Deleted' && filter.operand === 'isNotEmpty',
filter.operand === 'isNotEmpty',
)?.id ?? '',
); );
if (!deletedFilter) {
throw new Error('Deleted filter not found');
}
removeRecordFilter(deletedFilter.fieldMetadataId);
deleteCombinedViewFilter(deletedFilter.id);
toggleSoftDeleteFilterState(false); toggleSoftDeleteFilterState(false);
}; };

View File

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

View File

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

View File

@ -3,6 +3,7 @@ import styled from '@emotion/styled';
import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext'; import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext';
import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext'; import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; 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 { RecordIndexContextProvider } from '@/object-record/record-index/contexts/RecordIndexContext';
import { RecordTableWithWrappers } from '@/object-record/record-table/components/RecordTableWithWrappers'; import { RecordTableWithWrappers } from '@/object-record/record-table/components/RecordTableWithWrappers';
import { SignInBackgroundMockContainerEffect } from '@/sign-in-background-mock/components/SignInBackgroundMockContainerEffect'; import { SignInBackgroundMockContainerEffect } from '@/sign-in-background-mock/components/SignInBackgroundMockContainerEffect';
@ -41,30 +42,36 @@ export const SignInBackgroundMockContainer = () => {
<ViewComponentInstanceContext.Provider <ViewComponentInstanceContext.Provider
value={{ instanceId: recordIndexId }} value={{ instanceId: recordIndexId }}
> >
<ContextStoreComponentInstanceContext.Provider <RecordFiltersComponentInstanceContext.Provider
value={{ instanceId: recordIndexId }} value={{ instanceId: recordIndexId }}
> >
<ActionMenuComponentInstanceContext.Provider <ContextStoreComponentInstanceContext.Provider
value={{ instanceId: recordIndexId }} value={{
instanceId: recordIndexId,
}}
> >
<ViewBar <ActionMenuComponentInstanceContext.Provider
viewBarId={viewBarId} value={{ instanceId: recordIndexId }}
onCurrentViewChange={() => {}} >
optionsDropdownButton={<></>} <ViewBar
/> viewBarId={viewBarId}
<SignInBackgroundMockContainerEffect onCurrentViewChange={() => {}}
objectNamePlural={objectNamePlural} optionsDropdownButton={<></>}
recordTableId={recordIndexId} />
viewId={viewBarId} <SignInBackgroundMockContainerEffect
/> objectNamePlural={objectNamePlural}
<RecordTableWithWrappers recordTableId={recordIndexId}
objectNameSingular={objectNameSingular} viewId={viewBarId}
recordTableId={recordIndexId} />
viewBarId={viewBarId} <RecordTableWithWrappers
updateRecordMutation={() => {}} objectNameSingular={objectNameSingular}
/> recordTableId={recordIndexId}
</ActionMenuComponentInstanceContext.Provider> viewBarId={viewBarId}
</ContextStoreComponentInstanceContext.Provider> updateRecordMutation={() => {}}
/>
</ActionMenuComponentInstanceContext.Provider>
</ContextStoreComponentInstanceContext.Provider>
</RecordFiltersComponentInstanceContext.Provider>
</ViewComponentInstanceContext.Provider> </ViewComponentInstanceContext.Provider>
</RecordIndexContextProvider> </RecordIndexContextProvider>
</StyledContainer> </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 { filterDefinitionUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/filterDefinitionUsedInDropdownComponentState';
import { selectedFilterComponentState } from '@/object-record/object-filter-dropdown/states/selectedFilterComponentState'; import { selectedFilterComponentState } from '@/object-record/object-filter-dropdown/states/selectedFilterComponentState';
import { selectedOperandInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/selectedOperandInDropdownComponentState'; 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 { RecordFilterOperand } from '@/object-record/record-filter/types/RecordFilterOperand';
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2'; import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
import { useDeleteCombinedViewFilters } from '@/views/hooks/useDeleteCombinedViewFilters'; import { useDeleteCombinedViewFilters } from '@/views/hooks/useDeleteCombinedViewFilters';
@ -73,14 +74,17 @@ export const EditableFilterDropdownButton = ({
viewFilterDropdownId, viewFilterDropdownId,
]); ]);
const { removeRecordFilter } = useRemoveRecordFilter();
const handleRemove = () => { const handleRemove = () => {
closeDropdown(); closeDropdown();
deleteCombinedViewFilter(viewFilter.id); deleteCombinedViewFilter(viewFilter.id);
removeRecordFilter(viewFilter.fieldMetadataId);
}; };
const handleDropdownClickOutside = useCallback(() => { const handleDropdownClickOutside = useCallback(() => {
const { id: fieldId, value, operand } = viewFilter; const { id: fieldId, value, operand, fieldMetadataId } = viewFilter;
if ( if (
!value && !value &&
![ ![
@ -91,9 +95,10 @@ export const EditableFilterDropdownButton = ({
RecordFilterOperand.IsToday, RecordFilterOperand.IsToday,
].includes(operand) ].includes(operand)
) { ) {
removeRecordFilter(fieldMetadataId);
deleteCombinedViewFilter(fieldId); deleteCombinedViewFilter(fieldId);
} }
}, [viewFilter, deleteCombinedViewFilter]); }, [viewFilter, deleteCombinedViewFilter, removeRecordFilter]);
return ( return (
<Dropdown <Dropdown

View File

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

View File

@ -1,6 +1,7 @@
import { useIcons } from 'twenty-ui'; import { useIcons } from 'twenty-ui';
import { useObjectNameSingularFromPlural } from '@/object-metadata/hooks/useObjectNameSingularFromPlural'; 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 { RecordFilter } from '@/object-record/record-filter/types/RecordFilter';
import { useHandleToggleTrashColumnFilter } from '@/object-record/record-index/hooks/useHandleToggleTrashColumnFilter'; import { useHandleToggleTrashColumnFilter } from '@/object-record/record-index/hooks/useHandleToggleTrashColumnFilter';
import { SortOrFilterChip } from '@/views/components/SortOrFilterChip'; import { SortOrFilterChip } from '@/views/components/SortOrFilterChip';
@ -29,10 +30,14 @@ export const VariantFilterChip = ({
viewBarId, viewBarId,
}); });
const { removeRecordFilter } = useRemoveRecordFilter();
const { getIcon } = useIcons(); const { getIcon } = useIcons();
const handleRemoveClick = () => { const handleRemoveClick = () => {
deleteCombinedViewFilter(viewFilter.id); deleteCombinedViewFilter(viewFilter.id);
removeRecordFilter(viewFilter.fieldMetadataId);
if ( if (
viewFilter.definition.label === 'Deleted' && viewFilter.definition.label === 'Deleted' &&
viewFilter.operand === 'isNotEmpty' 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 { FiltersHotkeyScope } from '@/object-record/object-filter-dropdown/types/FiltersHotkeyScope';
import { VIEW_SORT_DROPDOWN_ID } from '@/object-record/object-sort-dropdown/constants/ViewSortDropdownId'; 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 { ObjectSortDropdownComponentInstanceContext } from '@/object-record/object-sort-dropdown/states/context/ObjectSortDropdownComponentInstanceContext';
import { ViewBarRecordFilterEffect } from '@/views/components/ViewBarRecordFilterEffect';
import { ViewEventContext } from '@/views/events/contexts/ViewEventContext'; import { ViewEventContext } from '@/views/events/contexts/ViewEventContext';
import { UpdateViewButtonGroup } from './UpdateViewButtonGroup'; import { UpdateViewButtonGroup } from './UpdateViewButtonGroup';
import { ViewBarDetails } from './ViewBarDetails'; import { ViewBarDetails } from './ViewBarDetails';
@ -53,6 +54,7 @@ export const ViewBar = ({
value={{ instanceId: VIEW_SORT_DROPDOWN_ID }} value={{ instanceId: VIEW_SORT_DROPDOWN_ID }}
> >
<ViewEventContext.Provider value={{ onCurrentViewChange }}> <ViewEventContext.Provider value={{ onCurrentViewChange }}>
<ViewBarRecordFilterEffect />
<ViewBarEffect viewBarId={viewBarId} /> <ViewBarEffect viewBarId={viewBarId} />
<ViewBarFilterEffect filterDropdownId={filterDropdownId} /> <ViewBarFilterEffect filterDropdownId={filterDropdownId} />
<ViewBarSortEffect /> <ViewBarSortEffect />

View File

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

View File

@ -1,16 +1,13 @@
import { isNonEmptyString } from '@sniptt/guards'; import { isNonEmptyString } from '@sniptt/guards';
import { useEffect } from 'react'; import { useEffect } from 'react';
import { RecordFilter } from '@/object-record/record-filter/types/RecordFilter';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2'; import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
import { filterDefinitionUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/filterDefinitionUsedInDropdownComponentState'; import { filterDefinitionUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/filterDefinitionUsedInDropdownComponentState';
import { objectFilterDropdownSelectedOptionValuesComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownSelectedOptionValuesComponentState'; import { objectFilterDropdownSelectedOptionValuesComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownSelectedOptionValuesComponentState';
import { objectFilterDropdownSelectedRecordIdsComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownSelectedRecordIdsComponentState'; 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 { useGetCurrentView } from '@/views/hooks/useGetCurrentView';
import { useUpsertCombinedViewFilters } from '@/views/hooks/useUpsertCombinedViewFilters';
import { availableFilterDefinitionsComponentState } from '@/views/states/availableFilterDefinitionsComponentState'; import { availableFilterDefinitionsComponentState } from '@/views/states/availableFilterDefinitionsComponentState';
import { jsonRelationFilterValueSchema } from '@/views/view-filter-value/validation-schemas/jsonRelationFilterValueSchema'; import { jsonRelationFilterValueSchema } from '@/views/view-filter-value/validation-schemas/jsonRelationFilterValueSchema';
import { simpleRelationFilterValueSchema } from '@/views/view-filter-value/validation-schemas/simpleRelationFilterValueSchema'; import { simpleRelationFilterValueSchema } from '@/views/view-filter-value/validation-schemas/simpleRelationFilterValueSchema';
@ -23,19 +20,12 @@ type ViewBarFilterEffectProps = {
export const ViewBarFilterEffect = ({ export const ViewBarFilterEffect = ({
filterDropdownId, filterDropdownId,
}: ViewBarFilterEffectProps) => { }: ViewBarFilterEffectProps) => {
const { upsertCombinedViewFilter } = useUpsertCombinedViewFilters();
const { currentViewWithCombinedFiltersAndSorts } = useGetCurrentView(); const { currentViewWithCombinedFiltersAndSorts } = useGetCurrentView();
const availableFilterDefinitions = useRecoilComponentValueV2( const availableFilterDefinitions = useRecoilComponentValueV2(
availableFilterDefinitionsComponentState, availableFilterDefinitionsComponentState,
); );
const setOnFilterSelect = useSetRecoilComponentStateV2(
onFilterSelectComponentState,
filterDropdownId,
);
const filterDefinitionUsedInDropdown = useRecoilComponentValueV2( const filterDefinitionUsedInDropdown = useRecoilComponentValueV2(
filterDefinitionUsedInDropdownComponentState, filterDefinitionUsedInDropdownComponentState,
filterDropdownId, filterDropdownId,
@ -62,17 +52,7 @@ export const ViewBarFilterEffect = ({
if (isDefined(availableFilterDefinitions)) { if (isDefined(availableFilterDefinitions)) {
setAvailableFilterDefinitions(availableFilterDefinitions); setAvailableFilterDefinitions(availableFilterDefinitions);
} }
setOnFilterSelect(() => (filter: RecordFilter | null) => { }, [availableFilterDefinitions, setAvailableFilterDefinitions]);
if (isDefined(filter)) {
upsertCombinedViewFilter(filter);
}
});
}, [
availableFilterDefinitions,
setAvailableFilterDefinitions,
setOnFilterSelect,
upsertCombinedViewFilter,
]);
useEffect(() => { useEffect(() => {
if (filterDefinitionUsedInDropdown?.type === 'RELATION') { 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 { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useObjectNameSingularFromPlural } from '@/object-metadata/hooks/useObjectNameSingularFromPlural'; import { useObjectNameSingularFromPlural } from '@/object-metadata/hooks/useObjectNameSingularFromPlural';
import { lastShowPageRecordIdState } from '@/object-record/record-field/states/lastShowPageRecordId'; 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 { RecordIndexContainer } from '@/object-record/record-index/components/RecordIndexContainer';
import { RecordIndexContainerContextStoreNumberOfSelectedRecordsEffect } from '@/object-record/record-index/components/RecordIndexContainerContextStoreNumberOfSelectedRecordsEffect'; import { RecordIndexContainerContextStoreNumberOfSelectedRecordsEffect } from '@/object-record/record-index/components/RecordIndexContainerContextStoreNumberOfSelectedRecordsEffect';
import { RecordIndexContainerContextStoreObjectMetadataEffect } from '@/object-record/record-index/components/RecordIndexContainerContextStoreObjectMetadataEffect'; import { RecordIndexContainerContextStoreObjectMetadataEffect } from '@/object-record/record-index/components/RecordIndexContainerContextStoreObjectMetadataEffect';
@ -81,28 +82,32 @@ export const RecordIndexPage = () => {
<ViewComponentInstanceContext.Provider <ViewComponentInstanceContext.Provider
value={{ instanceId: recordIndexId }} value={{ instanceId: recordIndexId }}
> >
<ContextStoreComponentInstanceContext.Provider <RecordFiltersComponentInstanceContext.Provider
value={{ value={{ instanceId: recordIndexId }}
instanceId: getActionMenuIdFromRecordIndexId(recordIndexId),
}}
> >
<ActionMenuComponentInstanceContext.Provider <ContextStoreComponentInstanceContext.Provider
value={{ value={{
instanceId: getActionMenuIdFromRecordIndexId(recordIndexId), instanceId: getActionMenuIdFromRecordIndexId(recordIndexId),
}} }}
> >
<PageTitle title={`${capitalize(objectNamePlural)}`} /> <ActionMenuComponentInstanceContext.Provider
<RecordIndexPageHeader /> value={{
<PageBody> instanceId: getActionMenuIdFromRecordIndexId(recordIndexId),
<StyledIndexContainer> }}
<RecordIndexContainerContextStoreObjectMetadataEffect /> >
<RecordIndexContainerContextStoreNumberOfSelectedRecordsEffect /> <PageTitle title={`${capitalize(objectNamePlural)}`} />
<MainContextStoreComponentInstanceIdSetterEffect /> <RecordIndexPageHeader />
<RecordIndexContainer /> <PageBody>
</StyledIndexContainer> <StyledIndexContainer>
</PageBody> <RecordIndexContainerContextStoreObjectMetadataEffect />
</ActionMenuComponentInstanceContext.Provider> <RecordIndexContainerContextStoreNumberOfSelectedRecordsEffect />
</ContextStoreComponentInstanceContext.Provider> <MainContextStoreComponentInstanceIdSetterEffect />
<RecordIndexContainer />
</StyledIndexContainer>
</PageBody>
</ActionMenuComponentInstanceContext.Provider>
</ContextStoreComponentInstanceContext.Provider>
</RecordFiltersComponentInstanceContext.Provider>
</ViewComponentInstanceContext.Provider> </ViewComponentInstanceContext.Provider>
</RecordIndexContextProvider> </RecordIndexContextProvider>
</PageContainer> </PageContainer>

View File

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

View File

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

View File

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

View File

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