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
@@ -10,6 +10,7 @@ import { getActionBarIdFromActionMenuId } from '@/action-menu/utils/getActionBar
import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState';
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext';
import { RecordFiltersComponentInstanceContext } from '@/object-record/record-filter/states/context/RecordFiltersComponentInstanceContext';
import { isBottomBarOpenedComponentState } from '@/ui/layout/bottom-bar/states/isBottomBarOpenedComponentState';
import { expect, jest } from '@storybook/jest';
import { Meta, StoryObj } from '@storybook/react';
@@ -25,58 +26,63 @@ const meta: Meta<typeof RecordIndexActionMenuBar> = {
decorators: [
RouterDecorator,
(Story) => (
<ContextStoreComponentInstanceContext.Provider
<RecordFiltersComponentInstanceContext.Provider
value={{ instanceId: 'story-action-menu' }}
>
<RecoilRoot
initializeState={({ set }) => {
set(
contextStoreTargetedRecordsRuleComponentState.atomFamily({
instanceId: 'story-action-menu',
}),
{
mode: 'selection',
selectedRecordIds: ['1', '2', '3'],
},
);
set(
contextStoreNumberOfSelectedRecordsComponentState.atomFamily({
instanceId: 'story-action-menu',
}),
3,
);
const map = new Map<string, ActionMenuEntry>();
map.set('delete', {
isPinned: true,
scope: ActionMenuEntryScope.RecordSelection,
type: ActionMenuEntryType.Standard,
key: 'delete',
label: 'Delete',
position: 0,
Icon: IconTrash,
onClick: deleteMock,
});
set(
actionMenuEntriesComponentState.atomFamily({
instanceId: 'story-action-menu',
}),
map,
);
set(
isBottomBarOpenedComponentState.atomFamily({
instanceId: getActionBarIdFromActionMenuId('story-action-menu'),
}),
true,
);
}}
<ContextStoreComponentInstanceContext.Provider
value={{ instanceId: 'story-action-menu' }}
>
<ActionMenuComponentInstanceContext.Provider
value={{ instanceId: 'story-action-menu' }}
<RecoilRoot
initializeState={({ set }) => {
set(
contextStoreTargetedRecordsRuleComponentState.atomFamily({
instanceId: 'story-action-menu',
}),
{
mode: 'selection',
selectedRecordIds: ['1', '2', '3'],
},
);
set(
contextStoreNumberOfSelectedRecordsComponentState.atomFamily({
instanceId: 'story-action-menu',
}),
3,
);
const map = new Map<string, ActionMenuEntry>();
map.set('delete', {
isPinned: true,
scope: ActionMenuEntryScope.RecordSelection,
type: ActionMenuEntryType.Standard,
key: 'delete',
label: 'Delete',
position: 0,
Icon: IconTrash,
onClick: deleteMock,
});
set(
actionMenuEntriesComponentState.atomFamily({
instanceId: 'story-action-menu',
}),
map,
);
set(
isBottomBarOpenedComponentState.atomFamily({
instanceId:
getActionBarIdFromActionMenuId('story-action-menu'),
}),
true,
);
}}
>
<Story />
</ActionMenuComponentInstanceContext.Provider>
</RecoilRoot>
</ContextStoreComponentInstanceContext.Provider>
<ActionMenuComponentInstanceContext.Provider
value={{ instanceId: 'story-action-menu' }}
>
<Story />
</ActionMenuComponentInstanceContext.Provider>
</RecoilRoot>
</ContextStoreComponentInstanceContext.Provider>
</RecordFiltersComponentInstanceContext.Provider>
),
],
args: {
@@ -9,6 +9,7 @@ import { useCommandMenuHotKeys } from '@/command-menu/hooks/useCommandMenuHotKey
import { isCommandMenuOpenedState } from '@/command-menu/states/isCommandMenuOpenedState';
import { CommandMenuAnimationVariant } from '@/command-menu/types/CommandMenuAnimationVariant';
import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext';
import { RecordFiltersComponentInstanceContext } from '@/object-record/record-filter/states/context/RecordFiltersComponentInstanceContext';
import { AppHotkeyScope } from '@/ui/utilities/hotkey/types/AppHotkeyScope';
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
import { workflowReactFlowRefState } from '@/workflow/workflow-diagram/states/workflowReactFlowRefState';
@@ -74,37 +75,41 @@ export const CommandMenuContainer = ({
const theme = useTheme();
return (
<ContextStoreComponentInstanceContext.Provider
<RecordFiltersComponentInstanceContext.Provider
value={{ instanceId: 'command-menu' }}
>
<ActionMenuComponentInstanceContext.Provider
<ContextStoreComponentInstanceContext.Provider
value={{ instanceId: 'command-menu' }}
>
<ActionMenuContext.Provider
value={{
isInRightDrawer: false,
onActionExecutedCallback: toggleCommandMenu,
}}
<ActionMenuComponentInstanceContext.Provider
value={{ instanceId: 'command-menu' }}
>
<RecordActionMenuEntriesSetter />
{isWorkflowEnabled && <RecordAgnosticActionsSetterEffect />}
<ActionMenuConfirmationModals />
{isCommandMenuOpened && (
<StyledCommandMenu
data-testid="command-menu"
ref={commandMenuRef}
className="command-menu"
animate={targetVariantForAnimation}
initial="closed"
exit="closed"
variants={COMMAND_MENU_ANIMATION_VARIANTS}
transition={{ duration: theme.animation.duration.normal }}
>
{children}
</StyledCommandMenu>
)}
</ActionMenuContext.Provider>
</ActionMenuComponentInstanceContext.Provider>
</ContextStoreComponentInstanceContext.Provider>
<ActionMenuContext.Provider
value={{
isInRightDrawer: false,
onActionExecutedCallback: toggleCommandMenu,
}}
>
<RecordActionMenuEntriesSetter />
{isWorkflowEnabled && <RecordAgnosticActionsSetterEffect />}
<ActionMenuConfirmationModals />
{isCommandMenuOpened && (
<StyledCommandMenu
data-testid="command-menu"
ref={commandMenuRef}
className="command-menu"
animate={targetVariantForAnimation}
initial="closed"
exit="closed"
variants={COMMAND_MENU_ANIMATION_VARIANTS}
transition={{ duration: theme.animation.duration.normal }}
>
{children}
</StyledCommandMenu>
)}
</ActionMenuContext.Provider>
</ActionMenuComponentInstanceContext.Provider>
</ContextStoreComponentInstanceContext.Provider>
</RecordFiltersComponentInstanceContext.Provider>
);
};
@@ -19,6 +19,7 @@ import { ActionMenuComponentInstanceContext } from '@/action-menu/states/context
import { CommandMenuRouter } from '@/command-menu/components/CommandMenuRouter';
import { isCommandMenuOpenedState } from '@/command-menu/states/isCommandMenuOpenedState';
import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext';
import { RecordFiltersComponentInstanceContext } from '@/object-record/record-filter/states/context/RecordFiltersComponentInstanceContext';
import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator';
import { JestContextStoreSetter } from '~/testing/jest/JestContextStoreSetter';
import { CommandMenu } from '../CommandMenu';
@@ -29,17 +30,21 @@ const openTimeout = 50;
const ContextStoreDecorator: Decorator = (Story) => {
return (
<ContextStoreComponentInstanceContext.Provider
<RecordFiltersComponentInstanceContext.Provider
value={{ instanceId: 'command-menu' }}
>
<ActionMenuComponentInstanceContext.Provider
<ContextStoreComponentInstanceContext.Provider
value={{ instanceId: 'command-menu' }}
>
<JestContextStoreSetter contextStoreCurrentObjectMetadataNameSingular="company">
<Story />
</JestContextStoreSetter>
</ActionMenuComponentInstanceContext.Provider>
</ContextStoreComponentInstanceContext.Provider>
<ActionMenuComponentInstanceContext.Provider
value={{ instanceId: 'command-menu' }}
>
<JestContextStoreSetter contextStoreCurrentObjectMetadataNameSingular="company">
<Story />
</JestContextStoreSetter>
</ActionMenuComponentInstanceContext.Provider>
</ContextStoreComponentInstanceContext.Provider>
</RecordFiltersComponentInstanceContext.Provider>
);
};
@@ -4,7 +4,7 @@ import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSi
import { useReadFindManyRecordsQueryInCache } from '@/object-record/cache/hooks/useReadFindManyRecordsQueryInCache';
import { useDeleteOneRecord } from '@/object-record/hooks/useDeleteOneRecord';
import { PREFETCH_CONFIG } from '@/prefetch/constants/PrefetchConfig';
import { usePrefetchRunQuery } from '@/prefetch/hooks/internal/usePrefetchRunQuery';
import { useUpsertRecordsInCacheForPrefetchKey } from '@/prefetch/hooks/internal/useUpsertRecordsInCacheForPrefetchKey';
import { PrefetchKey } from '@/prefetch/types/PrefetchKey';
export const useDeleteFavoriteFolder = () => {
@@ -12,9 +12,10 @@ export const useDeleteFavoriteFolder = () => {
objectNameSingular: CoreObjectNameSingular.FavoriteFolder,
});
const { upsertRecordsInCache } = usePrefetchRunQuery<Favorite>({
prefetchKey: PrefetchKey.AllFavorites,
});
const { upsertRecordsInCache } =
useUpsertRecordsInCacheForPrefetchKey<Favorite>({
prefetchKey: PrefetchKey.AllFavorites,
});
const { objectMetadataItem } = useObjectMetadataItem({
objectNameSingular:
@@ -1,6 +1,6 @@
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { Favorite } from '@/favorites/types/Favorite';
import { usePrefetchRunQuery } from '@/prefetch/hooks/internal/usePrefetchRunQuery';
import { useUpsertRecordsInCacheForPrefetchKey } from '@/prefetch/hooks/internal/useUpsertRecordsInCacheForPrefetchKey';
import { usePrefetchedData } from '@/prefetch/hooks/usePrefetchedData';
import { PrefetchKey } from '@/prefetch/types/PrefetchKey';
import { useRecoilValue } from 'recoil';
@@ -33,7 +33,7 @@ export const usePrefetchedFavoritesData = (): PrefetchedFavoritesData => {
);
const { upsertRecordsInCache: upsertFavorites } =
usePrefetchRunQuery<Favorite>({
useUpsertRecordsInCacheForPrefetchKey<Favorite>({
prefetchKey: PrefetchKey.AllFavorites,
});
@@ -1,6 +1,6 @@
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { FavoriteFolder } from '@/favorites/types/FavoriteFolder';
import { usePrefetchRunQuery } from '@/prefetch/hooks/internal/usePrefetchRunQuery';
import { useUpsertRecordsInCacheForPrefetchKey } from '@/prefetch/hooks/internal/useUpsertRecordsInCacheForPrefetchKey';
import { usePrefetchedData } from '@/prefetch/hooks/usePrefetchedData';
import { PrefetchKey } from '@/prefetch/types/PrefetchKey';
import { useRecoilValue } from 'recoil';
@@ -26,7 +26,7 @@ export const usePrefetchedFavoritesFoldersData =
);
const { upsertRecordsInCache: upsertFavoriteFolders } =
usePrefetchRunQuery<FavoriteFolder>({
useUpsertRecordsInCacheForPrefetchKey<FavoriteFolder>({
prefetchKey: PrefetchKey.AllFavoritesFolders,
});
@@ -9,6 +9,7 @@ import { selectedFilterComponentState } from '@/object-record/object-filter-drop
import { selectedOperandInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/selectedOperandInDropdownComponentState';
import { getActorSourceMultiSelectOptions } from '@/object-record/object-filter-dropdown/utils/getActorSourceMultiSelectOptions';
import { useApplyRecordFilter } from '@/object-record/record-filter/hooks/useApplyRecordFilter';
import { useRemoveRecordFilter } from '@/object-record/record-filter/hooks/useRemoveRecordFilter';
import { RelationPickerHotkeyScope } from '@/object-record/relation-picker/types/RelationPickerHotkeyScope';
import { MultipleSelectDropdown } from '@/object-record/select/components/MultipleSelectDropdown';
import { SelectableItem } from '@/object-record/select/types/SelectableItem';
@@ -61,6 +62,7 @@ export const ObjectFilterDropdownSourceSelect = ({
const { currentViewWithCombinedFiltersAndSorts } =
useGetCurrentView(viewComponentId);
// TODO: this should be removed as it is not consistent across re-renders
const [fieldId] = useState(v4());
const sourceTypes = getActorSourceMultiSelectOptions(
@@ -73,6 +75,8 @@ export const ObjectFilterDropdownSourceSelect = ({
const { emptyRecordFilter } = useEmptyRecordFilter();
const { removeRecordFilter } = useRemoveRecordFilter();
const handleMultipleItemSelectChange = (
itemToSelect: SelectableItem,
newSelectedValue: boolean,
@@ -83,8 +87,13 @@ export const ObjectFilterDropdownSourceSelect = ({
(id) => id !== itemToSelect.id,
);
if (!filterDefinitionUsedInDropdown) {
throw new Error('Filter definition used in dropdown should be defined');
}
if (newSelectedItemIds.length === 0) {
emptyRecordFilter();
removeRecordFilter(filterDefinitionUsedInDropdown.fieldMetadataId);
deleteCombinedViewFilter(fieldId);
return;
}
@@ -5,6 +5,7 @@ import { CoreObjectNamePlural } from '@/object-metadata/types/CoreObjectNamePlur
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { MultipleFiltersDropdownButton } from '@/object-record/object-filter-dropdown/components/MultipleFiltersDropdownButton';
import { ObjectFilterDropdownComponentInstanceContext } from '@/object-record/object-filter-dropdown/states/contexts/ObjectFilterDropdownComponentInstanceContext';
import { RecordFiltersComponentInstanceContext } from '@/object-record/record-filter/states/context/RecordFiltersComponentInstanceContext';
import { RecordIndexContextProvider } from '@/object-record/record-index/contexts/RecordIndexContext';
import { RecordTableComponentInstanceContext } from '@/object-record/record-table/states/context/RecordTableComponentInstanceContext';
import { tableColumnsComponentState } from '@/object-record/record-table/states/tableColumnsComponentState';
@@ -107,17 +108,21 @@ const meta: Meta<typeof MultipleFiltersDropdownButton> = {
recordIndexId: instanceId,
}}
>
<ObjectFilterDropdownComponentInstanceContext.Provider
<RecordFiltersComponentInstanceContext.Provider
value={{ instanceId }}
>
<RecordTableComponentInstanceContext.Provider
value={{ instanceId: instanceId, onColumnsChange: () => {} }}
<ObjectFilterDropdownComponentInstanceContext.Provider
value={{ instanceId }}
>
<ViewComponentInstanceContext.Provider value={{ instanceId }}>
<Story />
</ViewComponentInstanceContext.Provider>
</RecordTableComponentInstanceContext.Provider>
</ObjectFilterDropdownComponentInstanceContext.Provider>
<RecordTableComponentInstanceContext.Provider
value={{ instanceId: instanceId, onColumnsChange: () => {} }}
>
<ViewComponentInstanceContext.Provider value={{ instanceId }}>
<Story />
</ViewComponentInstanceContext.Provider>
</RecordTableComponentInstanceContext.Provider>
</ObjectFilterDropdownComponentInstanceContext.Provider>
</RecordFiltersComponentInstanceContext.Provider>
</RecordIndexContextProvider>
);
},
@@ -1,5 +1,6 @@
import { ObjectFilterDropdownComponentInstanceContext } from '@/object-record/object-filter-dropdown/states/contexts/ObjectFilterDropdownComponentInstanceContext';
import { RecordFilterDefinition } from '@/object-record/record-filter/types/RecordFilterDefinition';
import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2';
export const availableFilterDefinitionsComponentState = createComponentStateV2<
@@ -7,6 +7,7 @@ import { ObjectOptionsDropdownContent } from '@/object-record/object-options-dro
import { OBJECT_OPTIONS_DROPDOWN_ID } from '@/object-record/object-options-dropdown/constants/ObjectOptionsDropdownId';
import { ObjectOptionsDropdownContext } from '@/object-record/object-options-dropdown/states/contexts/ObjectOptionsDropdownContext';
import { ObjectOptionsContentId } from '@/object-record/object-options-dropdown/types/ObjectOptionsContentId';
import { RecordFiltersComponentInstanceContext } from '@/object-record/record-filter/states/context/RecordFiltersComponentInstanceContext';
import { RecordIndexContextProvider } from '@/object-record/record-index/contexts/RecordIndexContext';
import { RecordTableComponentInstanceContext } from '@/object-record/record-table/states/context/RecordTableComponentInstanceContext';
import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu';
@@ -37,22 +38,26 @@ const meta: Meta<typeof ObjectOptionsDropdownContent> = {
}, [setObjectMetadataItems]);
return (
<RecordTableComponentInstanceContext.Provider
value={{ instanceId, onColumnsChange: () => {} }}
<RecordFiltersComponentInstanceContext.Provider
value={{ instanceId: 'object-options-dropdown' }}
>
<ViewComponentInstanceContext.Provider value={{ instanceId }}>
<ContextStoreComponentInstanceContext.Provider
value={{ instanceId }}
>
<MemoryRouter
initialEntries={['/one', '/two', { pathname: '/three' }]}
initialIndex={1}
<RecordTableComponentInstanceContext.Provider
value={{ instanceId, onColumnsChange: () => {} }}
>
<ViewComponentInstanceContext.Provider value={{ instanceId }}>
<ContextStoreComponentInstanceContext.Provider
value={{ instanceId }}
>
<Story />
</MemoryRouter>
</ContextStoreComponentInstanceContext.Provider>
</ViewComponentInstanceContext.Provider>
</RecordTableComponentInstanceContext.Provider>
<MemoryRouter
initialEntries={['/one', '/two', { pathname: '/three' }]}
initialIndex={1}
>
<Story />
</MemoryRouter>
</ContextStoreComponentInstanceContext.Provider>
</ViewComponentInstanceContext.Provider>
</RecordTableComponentInstanceContext.Provider>
</RecordFiltersComponentInstanceContext.Provider>
);
},
ObjectMetadataItemsDecorator,
@@ -0,0 +1,117 @@
import { renderHook } from '@testing-library/react';
import { act } from 'react-dom/test-utils';
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
import { RecordFilter } from '@/object-record/record-filter/types/RecordFilter';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper';
import { useRemoveRecordFilter } from '../useRemoveRecordFilter';
import { useUpsertRecordFilter } from '../useUpsertRecordFilter';
const Wrapper = getJestMetadataAndApolloMocksWrapper({
apolloMocks: [],
});
describe('useRemoveRecordFilter', () => {
it('should remove an existing filter', () => {
const { result } = renderHook(
() => {
const currentRecordFilters = useRecoilComponentValueV2(
currentRecordFiltersComponentState,
);
const { upsertRecordFilter } = useUpsertRecordFilter();
const { removeRecordFilter } = useRemoveRecordFilter();
return {
upsertRecordFilter,
removeRecordFilter,
currentRecordFilters,
};
},
{
wrapper: Wrapper,
},
);
const filter: RecordFilter = {
id: 'filter-1',
fieldMetadataId: 'field-1',
value: 'test-value',
operand: ViewFilterOperand.Contains,
displayValue: 'test-value',
definition: {
type: 'TEXT',
fieldMetadataId: 'field-1',
label: 'Test Field',
iconName: 'IconText',
},
};
// First add a filter
act(() => {
result.current.upsertRecordFilter(filter);
});
expect(result.current.currentRecordFilters).toHaveLength(1);
expect(result.current.currentRecordFilters[0]).toEqual(filter);
// Then remove it
act(() => {
result.current.removeRecordFilter(filter.fieldMetadataId);
});
expect(result.current.currentRecordFilters).toHaveLength(0);
});
it('should not modify filters when trying to remove a non-existent filter', () => {
const { result } = renderHook(
() => {
const currentRecordFilters = useRecoilComponentValueV2(
currentRecordFiltersComponentState,
);
const { upsertRecordFilter } = useUpsertRecordFilter();
const { removeRecordFilter } = useRemoveRecordFilter();
return {
upsertRecordFilter,
removeRecordFilter,
currentRecordFilters,
};
},
{
wrapper: Wrapper,
},
);
const filter: RecordFilter = {
id: 'filter-1',
fieldMetadataId: 'field-1',
value: 'test-value',
operand: ViewFilterOperand.Contains,
displayValue: 'test-value',
definition: {
type: 'TEXT',
fieldMetadataId: 'field-1',
label: 'Test Field',
iconName: 'IconText',
},
};
// Add a filter
act(() => {
result.current.upsertRecordFilter(filter);
});
expect(result.current.currentRecordFilters).toHaveLength(1);
// Try to remove a non-existent filter
act(() => {
result.current.removeRecordFilter('non-existent-field');
});
// Filter list should remain unchanged
expect(result.current.currentRecordFilters).toHaveLength(1);
expect(result.current.currentRecordFilters[0]).toEqual(filter);
});
});
@@ -0,0 +1,112 @@
import { renderHook } from '@testing-library/react';
import { act } from 'react-dom/test-utils';
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
import { RecordFilter } from '@/object-record/record-filter/types/RecordFilter';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper';
import { useUpsertRecordFilter } from '../useUpsertRecordFilter';
const Wrapper = getJestMetadataAndApolloMocksWrapper({
apolloMocks: [],
});
describe('useUpsertRecordFilter', () => {
it('should add a new filter when fieldMetadataId does not exist', () => {
const { result } = renderHook(
() => {
const currentRecordFilters = useRecoilComponentValueV2(
currentRecordFiltersComponentState,
);
const { upsertRecordFilter } = useUpsertRecordFilter();
return { upsertRecordFilter, currentRecordFilters };
},
{
wrapper: Wrapper,
},
);
const newFilter: RecordFilter = {
id: 'filter-1',
fieldMetadataId: 'field-1',
value: 'test-value',
operand: ViewFilterOperand.Contains,
displayValue: 'test-value',
definition: {
type: 'TEXT',
fieldMetadataId: 'field-1',
label: 'Test Field',
iconName: 'IconText',
},
};
act(() => {
result.current.upsertRecordFilter(newFilter);
});
expect(result.current.currentRecordFilters).toHaveLength(1);
expect(result.current.currentRecordFilters[0]).toEqual(newFilter);
});
it('should update an existing filter when fieldMetadataId exists', () => {
const { result } = renderHook(
() => {
const currentRecordFilters = useRecoilComponentValueV2(
currentRecordFiltersComponentState,
);
const { upsertRecordFilter } = useUpsertRecordFilter();
return { upsertRecordFilter, currentRecordFilters };
},
{
wrapper: Wrapper,
},
);
const initialFilter: RecordFilter = {
id: 'filter-1',
fieldMetadataId: 'field-1',
value: 'initial-value',
operand: ViewFilterOperand.Contains,
displayValue: 'initial-value',
definition: {
type: 'TEXT',
fieldMetadataId: 'field-1',
label: 'Test Field',
iconName: 'IconText',
},
};
const updatedFilter: RecordFilter = {
id: 'filter-1',
fieldMetadataId: 'field-1',
value: 'updated-value',
operand: ViewFilterOperand.Contains,
displayValue: 'updated-value',
definition: {
type: 'TEXT',
fieldMetadataId: 'field-1',
label: 'Test Field',
iconName: 'IconText',
},
};
act(() => {
result.current.upsertRecordFilter(initialFilter);
});
expect(result.current.currentRecordFilters).toHaveLength(1);
expect(result.current.currentRecordFilters[0]).toEqual(initialFilter);
act(() => {
result.current.upsertRecordFilter(updatedFilter);
});
expect(result.current.currentRecordFilters).toHaveLength(1);
expect(result.current.currentRecordFilters[0]).toEqual(updatedFilter);
});
});
@@ -1,7 +1,6 @@
import { onFilterSelectComponentState } from '@/object-record/object-filter-dropdown/states/onFilterSelectComponentState';
import { selectedFilterComponentState } from '@/object-record/object-filter-dropdown/states/selectedFilterComponentState';
import { useUpsertRecordFilter } from '@/object-record/record-filter/hooks/useUpsertRecordFilter';
import { RecordFilter } from '@/object-record/record-filter/types/RecordFilter';
import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue';
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
import { useUpsertCombinedViewFilters } from '@/views/hooks/useUpsertCombinedViewFilters';
import { useRecoilCallback } from 'recoil';
@@ -14,32 +13,19 @@ export const useApplyRecordFilter = (componentInstanceId?: string) => {
componentInstanceId,
);
const onFilterSelectCallbackState = useRecoilComponentCallbackStateV2(
onFilterSelectComponentState,
componentInstanceId,
);
const { upsertRecordFilter } = useUpsertRecordFilter();
const applyRecordFilter = useRecoilCallback(
({ set, snapshot }) =>
({ set }) =>
(filter: RecordFilter | null) => {
set(selectedFilterCallbackState, filter);
const onFilterSelect = getSnapshotValue(
snapshot,
onFilterSelectCallbackState,
);
if (isDefined(filter)) {
upsertCombinedViewFilter(filter);
upsertRecordFilter(filter);
}
onFilterSelect?.(filter);
},
[
selectedFilterCallbackState,
onFilterSelectCallbackState,
upsertCombinedViewFilter,
],
[selectedFilterCallbackState, upsertCombinedViewFilter, upsertRecordFilter],
);
return {
@@ -0,0 +1,46 @@
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue';
import { useRecoilCallback } from 'recoil';
export const useRemoveRecordFilter = () => {
const currentRecordFiltersCallbackState = useRecoilComponentCallbackStateV2(
currentRecordFiltersComponentState,
);
const removeRecordFilter = useRecoilCallback(
({ set, snapshot }) =>
(fieldMetadataId: string) => {
const currentRecordFilters = getSnapshotValue(
snapshot,
currentRecordFiltersCallbackState,
);
const foundRecordFilterInCurrentRecordFilters =
currentRecordFilters.some(
(existingFilter) =>
existingFilter.fieldMetadataId === fieldMetadataId,
);
if (foundRecordFilterInCurrentRecordFilters) {
set(currentRecordFiltersCallbackState, (currentRecordFilters) => {
const newCurrentRecordFilters = [...currentRecordFilters];
const indexOfFilterToRemove = newCurrentRecordFilters.findIndex(
(existingFilter) =>
existingFilter.fieldMetadataId === fieldMetadataId,
);
newCurrentRecordFilters.splice(indexOfFilterToRemove, 1);
return newCurrentRecordFilters;
});
}
},
[currentRecordFiltersCallbackState],
);
return {
removeRecordFilter,
};
};
@@ -0,0 +1,54 @@
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
import { RecordFilter } from '@/object-record/record-filter/types/RecordFilter';
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue';
import { useRecoilCallback } from 'recoil';
export const useUpsertRecordFilter = () => {
const currentRecordFiltersCallbackState = useRecoilComponentCallbackStateV2(
currentRecordFiltersComponentState,
);
const upsertRecordFilter = useRecoilCallback(
({ set, snapshot }) =>
(filter: RecordFilter) => {
const currentRecordFilters = getSnapshotValue(
snapshot,
currentRecordFiltersCallbackState,
);
const foundRecordFilterInCurrentRecordFilters =
currentRecordFilters.some(
(existingFilter) =>
existingFilter.fieldMetadataId === filter.fieldMetadataId,
);
if (!foundRecordFilterInCurrentRecordFilters) {
set(currentRecordFiltersCallbackState, [
...currentRecordFilters,
filter,
]);
} else {
set(currentRecordFiltersCallbackState, (currentRecordFilters) => {
const newCurrentRecordFilters = [...currentRecordFilters];
const indexOfFilterToUpdate = newCurrentRecordFilters.findIndex(
(existingFilter) =>
existingFilter.fieldMetadataId === filter.fieldMetadataId,
);
newCurrentRecordFilters[indexOfFilterToUpdate] = {
...filter,
};
return newCurrentRecordFilters;
});
}
},
[currentRecordFiltersCallbackState],
);
return {
upsertRecordFilter,
};
};
@@ -0,0 +1,4 @@
import { createComponentInstanceContext } from '@/ui/utilities/state/component-state/utils/createComponentInstanceContext';
export const RecordFiltersComponentInstanceContext =
createComponentInstanceContext();
@@ -0,0 +1,11 @@
import { RecordFiltersComponentInstanceContext } from '@/object-record/record-filter/states/context/RecordFiltersComponentInstanceContext';
import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2';
import { RecordFilter } from '../../record-filter/types/RecordFilter';
export const currentRecordFiltersComponentState = createComponentStateV2<
RecordFilter[]
>({
key: 'currentRecordFiltersComponentState',
defaultValue: [],
componentInstanceContext: RecordFiltersComponentInstanceContext,
});
@@ -0,0 +1,128 @@
import { isMatchingArrayFilter } from '../isMatchingArrayFilter';
describe('isMatchingArrayFilter', () => {
describe('is filter', () => {
it('should return true when checking for NULL and value is null', () => {
expect(
isMatchingArrayFilter({
arrayFilter: { is: 'NULL' },
value: null,
}),
).toBe(true);
});
it('should return false when checking for NULL and value is not null', () => {
expect(
isMatchingArrayFilter({
arrayFilter: { is: 'NULL' },
value: ['test'],
}),
).toBe(false);
});
it('should return true when checking for NOT_NULL and value is not null', () => {
expect(
isMatchingArrayFilter({
arrayFilter: { is: 'NOT_NULL' },
value: ['test'],
}),
).toBe(true);
});
it('should return false when checking for NOT_NULL and value is null', () => {
expect(
isMatchingArrayFilter({
arrayFilter: { is: 'NOT_NULL' },
value: null,
}),
).toBe(false);
});
});
describe('isEmptyArray filter', () => {
it('should return true when array is empty and checking for empty array', () => {
expect(
isMatchingArrayFilter({
arrayFilter: { isEmptyArray: true },
value: [],
}),
).toBe(true);
});
it('should return false when array is not empty and checking for empty array', () => {
expect(
isMatchingArrayFilter({
arrayFilter: { isEmptyArray: true },
value: ['test'],
}),
).toBe(false);
});
it('should return false when value is null and checking for empty array', () => {
expect(
isMatchingArrayFilter({
arrayFilter: { isEmptyArray: true },
value: null,
}),
).toBe(false);
});
});
describe('containsIlike filter', () => {
it('should return true when array contains item matching case-insensitive search', () => {
expect(
isMatchingArrayFilter({
arrayFilter: { containsIlike: 'TEST' },
value: ['test item'],
}),
).toBe(true);
});
it('should return false when array does not contain item matching search', () => {
expect(
isMatchingArrayFilter({
arrayFilter: { containsIlike: 'missing' },
value: ['test item'],
}),
).toBe(false);
});
it('should return false when value is null and using containsIlike', () => {
expect(
isMatchingArrayFilter({
arrayFilter: { containsIlike: 'test' },
value: null,
}),
).toBe(false);
});
it('should match partial strings case-insensitively', () => {
expect(
isMatchingArrayFilter({
arrayFilter: { containsIlike: 'TE' },
value: ['Test Item', 'Another Item'],
}),
).toBe(true);
});
});
describe('error handling', () => {
it('should throw error for invalid filter', () => {
expect(() =>
isMatchingArrayFilter({
arrayFilter: {},
value: [],
}),
).toThrow('Unexpected value for array filter');
});
it('should throw error for unknown filter type', () => {
expect(() =>
isMatchingArrayFilter({
arrayFilter: { unknownFilter: 'test' } as any,
value: [],
}),
).toThrow('Unexpected value for array filter');
});
});
});
@@ -1,10 +1,10 @@
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { turnSortsIntoOrderBy } from '@/object-record/object-sort-dropdown/utils/turnSortsIntoOrderBy';
import { useFilterValueDependencies } from '@/object-record/record-filter/hooks/useFilterValueDependencies';
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
import { computeViewRecordGqlOperationFilter } from '@/object-record/record-filter/utils/computeViewRecordGqlOperationFilter';
import { useCurrentRecordGroupDefinition } from '@/object-record/record-group/hooks/useCurrentRecordGroupDefinition';
import { useRecordGroupFilter } from '@/object-record/record-group/hooks/useRecordGroupFilter';
import { tableFiltersComponentState } from '@/object-record/record-table/states/tableFiltersComponentState';
import { tableSortsComponentState } from '@/object-record/record-table/states/tableSortsComponentState';
import { tableViewFilterGroupsComponentState } from '@/object-record/record-table/states/tableViewFilterGroupsComponentState';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
@@ -27,20 +27,21 @@ export const useFindManyRecordIndexTableParams = (
tableViewFilterGroupsComponentState,
recordTableId,
);
const tableFilters = useRecoilComponentValueV2(
tableFiltersComponentState,
recordTableId,
);
const tableSorts = useRecoilComponentValueV2(
tableSortsComponentState,
recordTableId,
);
const currentRecordFilters = useRecoilComponentValueV2(
currentRecordFiltersComponentState,
);
const { filterValueDependencies } = useFilterValueDependencies();
const stateFilter = computeViewRecordGqlOperationFilter(
filterValueDependencies,
tableFilters,
currentRecordFilters,
objectMetadataItem?.fields ?? [],
tableViewFilterGroups,
);
@@ -5,6 +5,7 @@ import { useColumnDefinitionsFromFieldMetadata } from '@/object-metadata/hooks/u
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useSelectFilterDefinitionUsedInDropdown } from '@/object-record/object-filter-dropdown/hooks/useSelectFilterDefinitionUsedInDropdown';
import { useUpsertRecordFilter } from '@/object-record/record-filter/hooks/useUpsertRecordFilter';
import { RecordFilter } from '@/object-record/record-filter/types/RecordFilter';
import { getRecordFilterOperandsForRecordFilterDefinition } from '@/object-record/record-filter/utils/getRecordFilterOperandsForRecordFilterDefinition';
import { isDropdownOpenComponentState } from '@/ui/layout/dropdown/states/isDropdownOpenComponentState';
@@ -33,6 +34,7 @@ export const useHandleToggleColumnFilter = ({
useColumnDefinitionsFromFieldMetadata(objectMetadataItem);
const { upsertCombinedViewFilter } = useUpsertCombinedViewFilters(viewBarId);
const { upsertRecordFilter } = useUpsertRecordFilter();
const openDropdown = useRecoilCallback(({ set }) => {
return (dropdownId: string) => {
@@ -93,6 +95,8 @@ export const useHandleToggleColumnFilter = ({
value: '',
};
upsertRecordFilter(newFilter);
await upsertCombinedViewFilter(newFilter);
selectFilterDefinitionUsedInDropdown({ filterDefinition });
@@ -107,6 +111,7 @@ export const useHandleToggleColumnFilter = ({
selectFilterDefinitionUsedInDropdown,
currentViewWithCombinedFiltersAndSorts,
availableFilterDefinitions,
upsertRecordFilter,
],
);
@@ -4,6 +4,7 @@ import { v4 } from 'uuid';
import { useColumnDefinitionsFromFieldMetadata } from '@/object-metadata/hooks/useColumnDefinitionsFromFieldMetadata';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { getFilterTypeFromFieldType } from '@/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions';
import { useUpsertRecordFilter } from '@/object-record/record-filter/hooks/useUpsertRecordFilter';
import { RecordFilter } from '@/object-record/record-filter/types/RecordFilter';
import { isSoftDeleteFilterActiveComponentState } from '@/object-record/record-table/states/isSoftDeleteFilterActiveComponentState';
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
@@ -36,6 +37,8 @@ export const useHandleToggleTrashColumnFilter = ({
viewBarId,
);
const { upsertRecordFilter } = useUpsertRecordFilter();
const handleToggleTrashColumnFilter = useCallback(() => {
const trashFieldMetadata = objectMetadataItem.fields.find(
(field: { name: string }) => field.name === 'deletedAt',
@@ -69,8 +72,14 @@ export const useHandleToggleTrashColumnFilter = ({
value: '',
};
upsertRecordFilter(newFilter);
upsertCombinedViewFilter(newFilter);
}, [columnDefinitions, objectMetadataItem, upsertCombinedViewFilter]);
}, [
columnDefinitions,
objectMetadataItem,
upsertCombinedViewFilter,
upsertRecordFilter,
]);
const toggleSoftDeleteFilterState = useRecoilCallback(
({ set }) =>
@@ -2,6 +2,7 @@ import { useRecoilValue } from 'recoil';
import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext';
import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext';
import { RecordFiltersComponentInstanceContext } from '@/object-record/record-filter/states/context/RecordFiltersComponentInstanceContext';
import { isNewViewableRecordLoadingState } from '@/object-record/record-right-drawer/states/isNewViewableRecordLoading';
import { viewableRecordIdState } from '@/object-record/record-right-drawer/states/viewableRecordIdState';
import { viewableRecordNameSingularState } from '@/object-record/record-right-drawer/states/viewableRecordNameSingularState';
@@ -42,29 +43,33 @@ export const RightDrawerRecord = () => {
);
return (
<ContextStoreComponentInstanceContext.Provider
value={{
instanceId: `record-show-${objectRecordId}`,
}}
<RecordFiltersComponentInstanceContext.Provider
value={{ instanceId: `record-show-${objectRecordId}` }}
>
<ActionMenuComponentInstanceContext.Provider
value={{ instanceId: `record-show-${objectRecordId}` }}
<ContextStoreComponentInstanceContext.Provider
value={{
instanceId: `record-show-${objectRecordId}`,
}}
>
<StyledRightDrawerRecord isMobile={isMobile}>
<RecordFieldValueSelectorContextProvider>
{!isNewViewableRecordLoading && (
<RecordValueSetterEffect recordId={objectRecordId} />
)}
<RecordShowContainer
objectNameSingular={objectNameSingular}
objectRecordId={objectRecordId}
loading={false}
isInRightDrawer={true}
isNewRightDrawerItemLoading={isNewViewableRecordLoading}
/>
</RecordFieldValueSelectorContextProvider>
</StyledRightDrawerRecord>
</ActionMenuComponentInstanceContext.Provider>
</ContextStoreComponentInstanceContext.Provider>
<ActionMenuComponentInstanceContext.Provider
value={{ instanceId: `record-show-${objectRecordId}` }}
>
<StyledRightDrawerRecord isMobile={isMobile}>
<RecordFieldValueSelectorContextProvider>
{!isNewViewableRecordLoading && (
<RecordValueSetterEffect recordId={objectRecordId} />
)}
<RecordShowContainer
objectNameSingular={objectNameSingular}
objectRecordId={objectRecordId}
loading={false}
isInRightDrawer={true}
isNewRightDrawerItemLoading={isNewViewableRecordLoading}
/>
</RecordFieldValueSelectorContextProvider>
</StyledRightDrawerRecord>
</ActionMenuComponentInstanceContext.Provider>
</ContextStoreComponentInstanceContext.Provider>
</RecordFiltersComponentInstanceContext.Provider>
);
};
@@ -1,6 +1,7 @@
import { IconFilterOff } from 'twenty-ui';
import { useObjectLabel } from '@/object-metadata/hooks/useObjectLabel';
import { useRemoveRecordFilter } from '@/object-record/record-filter/hooks/useRemoveRecordFilter';
import { useHandleToggleTrashColumnFilter } from '@/object-record/record-index/hooks/useHandleToggleTrashColumnFilter';
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { RecordTableEmptyStateDisplay } from '@/object-record/record-table/empty-state/components/RecordTableEmptyStateDisplay';
@@ -25,14 +26,22 @@ export const RecordTableEmptyStateSoftDelete = () => {
viewBarId: recordTableId,
});
const { removeRecordFilter } = useRemoveRecordFilter();
const handleButtonClick = async () => {
deleteCombinedViewFilter(
tableFilters.find(
(filter) =>
filter.definition.label === 'Deleted' &&
filter.operand === 'isNotEmpty',
)?.id ?? '',
const deletedFilter = tableFilters.find(
(filter) =>
filter.definition.label === 'Deleted' &&
filter.operand === 'isNotEmpty',
);
if (!deletedFilter) {
throw new Error('Deleted filter not found');
}
removeRecordFilter(deletedFilter.fieldMetadataId);
deleteCombinedViewFilter(deletedFilter.id);
toggleSoftDeleteFilterState(false);
};
@@ -7,7 +7,7 @@ import { FavoriteFolder } from '@/favorites/types/FavoriteFolder';
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
import { useCombinedFindManyRecords } from '@/object-record/multiple-objects/hooks/useCombinedFindManyRecords';
import { PREFETCH_CONFIG } from '@/prefetch/constants/PrefetchConfig';
import { usePrefetchRunQuery } from '@/prefetch/hooks/internal/usePrefetchRunQuery';
import { useUpsertRecordsInCacheForPrefetchKey } from '@/prefetch/hooks/internal/useUpsertRecordsInCacheForPrefetchKey';
import { PrefetchKey } from '@/prefetch/types/PrefetchKey';
import { View } from '@/views/types/View';
import { useIsWorkspaceActivationStatusSuspended } from '@/workspace/hooks/useIsWorkspaceActivationStatusSuspended';
@@ -19,16 +19,16 @@ export const PrefetchRunQueriesEffect = () => {
const isWorkspaceSuspended = useIsWorkspaceActivationStatusSuspended();
const { upsertRecordsInCache: upsertViewsInCache } =
usePrefetchRunQuery<View>({
useUpsertRecordsInCacheForPrefetchKey<View>({
prefetchKey: PrefetchKey.AllViews,
});
const { upsertRecordsInCache: upsertFavoritesInCache } =
usePrefetchRunQuery<Favorite>({
useUpsertRecordsInCacheForPrefetchKey<Favorite>({
prefetchKey: PrefetchKey.AllFavorites,
});
const { upsertRecordsInCache: upsertFavoritesFoldersInCache } =
usePrefetchRunQuery<FavoriteFolder>({
useUpsertRecordsInCacheForPrefetchKey<FavoriteFolder>({
prefetchKey: PrefetchKey.AllFavoritesFolders,
});
const { objectMetadataItems } = useObjectMetadataItems();
@@ -11,7 +11,7 @@ export type UsePrefetchRunQuery = {
prefetchKey: PrefetchKey;
};
export const usePrefetchRunQuery = <T extends ObjectRecord>({
export const useUpsertRecordsInCacheForPrefetchKey = <T extends ObjectRecord>({
prefetchKey,
}: UsePrefetchRunQuery) => {
const setPrefetchDataIsLoaded = useSetRecoilState(
@@ -45,7 +45,6 @@ export const usePrefetchRunQuery = <T extends ObjectRecord>({
return {
objectMetadataItem,
setPrefetchDataIsLoaded,
upsertRecordsInCache,
};
};
@@ -3,6 +3,7 @@ import styled from '@emotion/styled';
import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext';
import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { RecordFiltersComponentInstanceContext } from '@/object-record/record-filter/states/context/RecordFiltersComponentInstanceContext';
import { RecordIndexContextProvider } from '@/object-record/record-index/contexts/RecordIndexContext';
import { RecordTableWithWrappers } from '@/object-record/record-table/components/RecordTableWithWrappers';
import { SignInBackgroundMockContainerEffect } from '@/sign-in-background-mock/components/SignInBackgroundMockContainerEffect';
@@ -41,30 +42,36 @@ export const SignInBackgroundMockContainer = () => {
<ViewComponentInstanceContext.Provider
value={{ instanceId: recordIndexId }}
>
<ContextStoreComponentInstanceContext.Provider
<RecordFiltersComponentInstanceContext.Provider
value={{ instanceId: recordIndexId }}
>
<ActionMenuComponentInstanceContext.Provider
value={{ instanceId: recordIndexId }}
<ContextStoreComponentInstanceContext.Provider
value={{
instanceId: recordIndexId,
}}
>
<ViewBar
viewBarId={viewBarId}
onCurrentViewChange={() => {}}
optionsDropdownButton={<></>}
/>
<SignInBackgroundMockContainerEffect
objectNamePlural={objectNamePlural}
recordTableId={recordIndexId}
viewId={viewBarId}
/>
<RecordTableWithWrappers
objectNameSingular={objectNameSingular}
recordTableId={recordIndexId}
viewBarId={viewBarId}
updateRecordMutation={() => {}}
/>
</ActionMenuComponentInstanceContext.Provider>
</ContextStoreComponentInstanceContext.Provider>
<ActionMenuComponentInstanceContext.Provider
value={{ instanceId: recordIndexId }}
>
<ViewBar
viewBarId={viewBarId}
onCurrentViewChange={() => {}}
optionsDropdownButton={<></>}
/>
<SignInBackgroundMockContainerEffect
objectNamePlural={objectNamePlural}
recordTableId={recordIndexId}
viewId={viewBarId}
/>
<RecordTableWithWrappers
objectNameSingular={objectNameSingular}
recordTableId={recordIndexId}
viewBarId={viewBarId}
updateRecordMutation={() => {}}
/>
</ActionMenuComponentInstanceContext.Provider>
</ContextStoreComponentInstanceContext.Provider>
</RecordFiltersComponentInstanceContext.Provider>
</ViewComponentInstanceContext.Provider>
</RecordIndexContextProvider>
</StyledContainer>
@@ -11,6 +11,7 @@ import { ObjectFilterOperandSelectAndInput } from '@/object-record/object-filter
import { filterDefinitionUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/filterDefinitionUsedInDropdownComponentState';
import { selectedFilterComponentState } from '@/object-record/object-filter-dropdown/states/selectedFilterComponentState';
import { selectedOperandInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/selectedOperandInDropdownComponentState';
import { useRemoveRecordFilter } from '@/object-record/record-filter/hooks/useRemoveRecordFilter';
import { RecordFilterOperand } from '@/object-record/record-filter/types/RecordFilterOperand';
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
import { useDeleteCombinedViewFilters } from '@/views/hooks/useDeleteCombinedViewFilters';
@@ -73,14 +74,17 @@ export const EditableFilterDropdownButton = ({
viewFilterDropdownId,
]);
const { removeRecordFilter } = useRemoveRecordFilter();
const handleRemove = () => {
closeDropdown();
deleteCombinedViewFilter(viewFilter.id);
removeRecordFilter(viewFilter.fieldMetadataId);
};
const handleDropdownClickOutside = useCallback(() => {
const { id: fieldId, value, operand } = viewFilter;
const { id: fieldId, value, operand, fieldMetadataId } = viewFilter;
if (
!value &&
![
@@ -91,9 +95,10 @@ export const EditableFilterDropdownButton = ({
RecordFilterOperand.IsToday,
].includes(operand)
) {
removeRecordFilter(fieldMetadataId);
deleteCombinedViewFilter(fieldId);
}
}, [viewFilter, deleteCombinedViewFilter]);
}, [viewFilter, deleteCombinedViewFilter, removeRecordFilter]);
return (
<Dropdown
@@ -3,6 +3,7 @@ import { useEffect } from 'react';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useSetRecoilComponentFamilyStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentFamilyStateV2';
import { useViewFromQueryParams } from '@/views/hooks/internal/useViewFromQueryParams';
import { useApplyViewFiltersToCurrentRecordFilters } from '@/views/hooks/useApplyViewFiltersToCurrentRecordFilters';
import { useResetUnsavedViewStates } from '@/views/hooks/useResetUnsavedViewStates';
import { currentViewIdComponentState } from '@/views/states/currentViewIdComponentState';
import { unsavedToUpsertViewFiltersComponentFamilyState } from '@/views/states/unsavedToUpsertViewFiltersComponentFamilyState';
@@ -20,6 +21,9 @@ export const QueryParamsFiltersEffect = () => {
const { resetUnsavedViewStates } = useResetUnsavedViewStates();
const { applyViewFiltersToCurrentRecordFilters } =
useApplyViewFiltersToCurrentRecordFilters();
useEffect(() => {
if (!hasFiltersQueryParams) {
return;
@@ -27,10 +31,12 @@ export const QueryParamsFiltersEffect = () => {
getFiltersFromQueryParams().then((filtersFromParams) => {
if (Array.isArray(filtersFromParams)) {
applyViewFiltersToCurrentRecordFilters(filtersFromParams);
setUnsavedViewFilter(filtersFromParams);
}
});
}, [
applyViewFiltersToCurrentRecordFilters,
getFiltersFromQueryParams,
hasFiltersQueryParams,
resetUnsavedViewStates,
@@ -1,6 +1,7 @@
import { useIcons } from 'twenty-ui';
import { useObjectNameSingularFromPlural } from '@/object-metadata/hooks/useObjectNameSingularFromPlural';
import { useRemoveRecordFilter } from '@/object-record/record-filter/hooks/useRemoveRecordFilter';
import { RecordFilter } from '@/object-record/record-filter/types/RecordFilter';
import { useHandleToggleTrashColumnFilter } from '@/object-record/record-index/hooks/useHandleToggleTrashColumnFilter';
import { SortOrFilterChip } from '@/views/components/SortOrFilterChip';
@@ -29,10 +30,14 @@ export const VariantFilterChip = ({
viewBarId,
});
const { removeRecordFilter } = useRemoveRecordFilter();
const { getIcon } = useIcons();
const handleRemoveClick = () => {
deleteCombinedViewFilter(viewFilter.id);
removeRecordFilter(viewFilter.fieldMetadataId);
if (
viewFilter.definition.label === 'Deleted' &&
viewFilter.operand === 'isNotEmpty'
@@ -21,6 +21,7 @@ import { ViewsHotkeyScope } from '../types/ViewsHotkeyScope';
import { FiltersHotkeyScope } from '@/object-record/object-filter-dropdown/types/FiltersHotkeyScope';
import { VIEW_SORT_DROPDOWN_ID } from '@/object-record/object-sort-dropdown/constants/ViewSortDropdownId';
import { ObjectSortDropdownComponentInstanceContext } from '@/object-record/object-sort-dropdown/states/context/ObjectSortDropdownComponentInstanceContext';
import { ViewBarRecordFilterEffect } from '@/views/components/ViewBarRecordFilterEffect';
import { ViewEventContext } from '@/views/events/contexts/ViewEventContext';
import { UpdateViewButtonGroup } from './UpdateViewButtonGroup';
import { ViewBarDetails } from './ViewBarDetails';
@@ -53,6 +54,7 @@ export const ViewBar = ({
value={{ instanceId: VIEW_SORT_DROPDOWN_ID }}
>
<ViewEventContext.Provider value={{ onCurrentViewChange }}>
<ViewBarRecordFilterEffect />
<ViewBarEffect viewBarId={viewBarId} />
<ViewBarFilterEffect filterDropdownId={filterDropdownId} />
<ViewBarSortEffect />
@@ -14,6 +14,8 @@ import { EditableFilterDropdownButton } from '@/views/components/EditableFilterD
import { EditableSortChip } from '@/views/components/EditableSortChip';
import { ViewBarFilterEffect } from '@/views/components/ViewBarFilterEffect';
import { useViewFromQueryParams } from '@/views/hooks/internal/useViewFromQueryParams';
import { useApplyCurrentViewFiltersToCurrentRecordFilters } from '@/views/hooks/useApplyCurrentViewFiltersToCurrentRecordFilters';
import { useGetCurrentView } from '@/views/hooks/useGetCurrentView';
import { useResetUnsavedViewStates } from '@/views/hooks/useResetUnsavedViewStates';
import { availableFilterDefinitionsComponentState } from '@/views/states/availableFilterDefinitionsComponentState';
@@ -167,9 +169,13 @@ export const ViewBarDetails = ({
};
}, [currentViewWithCombinedFiltersAndSorts]);
const { applyCurrentViewFiltersToCurrentRecordFilters } =
useApplyCurrentViewFiltersToCurrentRecordFilters();
const handleCancelClick = () => {
if (isDefined(viewId)) {
resetUnsavedViewStates(viewId);
applyCurrentViewFiltersToCurrentRecordFilters();
toggleSoftDeleteFilterState(false);
}
};
@@ -1,16 +1,13 @@
import { isNonEmptyString } from '@sniptt/guards';
import { useEffect } from 'react';
import { RecordFilter } from '@/object-record/record-filter/types/RecordFilter';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
import { filterDefinitionUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/filterDefinitionUsedInDropdownComponentState';
import { objectFilterDropdownSelectedOptionValuesComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownSelectedOptionValuesComponentState';
import { objectFilterDropdownSelectedRecordIdsComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownSelectedRecordIdsComponentState';
import { onFilterSelectComponentState } from '@/object-record/object-filter-dropdown/states/onFilterSelectComponentState';
import { useGetCurrentView } from '@/views/hooks/useGetCurrentView';
import { useUpsertCombinedViewFilters } from '@/views/hooks/useUpsertCombinedViewFilters';
import { availableFilterDefinitionsComponentState } from '@/views/states/availableFilterDefinitionsComponentState';
import { jsonRelationFilterValueSchema } from '@/views/view-filter-value/validation-schemas/jsonRelationFilterValueSchema';
import { simpleRelationFilterValueSchema } from '@/views/view-filter-value/validation-schemas/simpleRelationFilterValueSchema';
@@ -23,19 +20,12 @@ type ViewBarFilterEffectProps = {
export const ViewBarFilterEffect = ({
filterDropdownId,
}: ViewBarFilterEffectProps) => {
const { upsertCombinedViewFilter } = useUpsertCombinedViewFilters();
const { currentViewWithCombinedFiltersAndSorts } = useGetCurrentView();
const availableFilterDefinitions = useRecoilComponentValueV2(
availableFilterDefinitionsComponentState,
);
const setOnFilterSelect = useSetRecoilComponentStateV2(
onFilterSelectComponentState,
filterDropdownId,
);
const filterDefinitionUsedInDropdown = useRecoilComponentValueV2(
filterDefinitionUsedInDropdownComponentState,
filterDropdownId,
@@ -62,17 +52,7 @@ export const ViewBarFilterEffect = ({
if (isDefined(availableFilterDefinitions)) {
setAvailableFilterDefinitions(availableFilterDefinitions);
}
setOnFilterSelect(() => (filter: RecordFilter | null) => {
if (isDefined(filter)) {
upsertCombinedViewFilter(filter);
}
});
}, [
availableFilterDefinitions,
setAvailableFilterDefinitions,
setOnFilterSelect,
upsertCombinedViewFilter,
]);
}, [availableFilterDefinitions, setAvailableFilterDefinitions]);
useEffect(() => {
if (filterDefinitionUsedInDropdown?.type === 'RELATION') {
@@ -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;
};
@@ -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([]);
});
});
@@ -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([]);
});
});
@@ -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,
};
};
@@ -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,
};
};
@@ -9,6 +9,7 @@ import { ContextStoreComponentInstanceContext } from '@/context-store/states/con
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useObjectNameSingularFromPlural } from '@/object-metadata/hooks/useObjectNameSingularFromPlural';
import { lastShowPageRecordIdState } from '@/object-record/record-field/states/lastShowPageRecordId';
import { RecordFiltersComponentInstanceContext } from '@/object-record/record-filter/states/context/RecordFiltersComponentInstanceContext';
import { RecordIndexContainer } from '@/object-record/record-index/components/RecordIndexContainer';
import { RecordIndexContainerContextStoreNumberOfSelectedRecordsEffect } from '@/object-record/record-index/components/RecordIndexContainerContextStoreNumberOfSelectedRecordsEffect';
import { RecordIndexContainerContextStoreObjectMetadataEffect } from '@/object-record/record-index/components/RecordIndexContainerContextStoreObjectMetadataEffect';
@@ -81,28 +82,32 @@ export const RecordIndexPage = () => {
<ViewComponentInstanceContext.Provider
value={{ instanceId: recordIndexId }}
>
<ContextStoreComponentInstanceContext.Provider
value={{
instanceId: getActionMenuIdFromRecordIndexId(recordIndexId),
}}
<RecordFiltersComponentInstanceContext.Provider
value={{ instanceId: recordIndexId }}
>
<ActionMenuComponentInstanceContext.Provider
<ContextStoreComponentInstanceContext.Provider
value={{
instanceId: getActionMenuIdFromRecordIndexId(recordIndexId),
}}
>
<PageTitle title={`${capitalize(objectNamePlural)}`} />
<RecordIndexPageHeader />
<PageBody>
<StyledIndexContainer>
<RecordIndexContainerContextStoreObjectMetadataEffect />
<RecordIndexContainerContextStoreNumberOfSelectedRecordsEffect />
<MainContextStoreComponentInstanceIdSetterEffect />
<RecordIndexContainer />
</StyledIndexContainer>
</PageBody>
</ActionMenuComponentInstanceContext.Provider>
</ContextStoreComponentInstanceContext.Provider>
<ActionMenuComponentInstanceContext.Provider
value={{
instanceId: getActionMenuIdFromRecordIndexId(recordIndexId),
}}
>
<PageTitle title={`${capitalize(objectNamePlural)}`} />
<RecordIndexPageHeader />
<PageBody>
<StyledIndexContainer>
<RecordIndexContainerContextStoreObjectMetadataEffect />
<RecordIndexContainerContextStoreNumberOfSelectedRecordsEffect />
<MainContextStoreComponentInstanceIdSetterEffect />
<RecordIndexContainer />
</StyledIndexContainer>
</PageBody>
</ActionMenuComponentInstanceContext.Provider>
</ContextStoreComponentInstanceContext.Provider>
</RecordFiltersComponentInstanceContext.Provider>
</ViewComponentInstanceContext.Provider>
</RecordIndexContextProvider>
</PageContainer>
@@ -5,6 +5,7 @@ import { ActionMenuComponentInstanceContext } from '@/action-menu/states/context
import { TimelineActivityContext } from '@/activities/timeline-activities/contexts/TimelineActivityContext';
import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { RecordFiltersComponentInstanceContext } from '@/object-record/record-filter/states/context/RecordFiltersComponentInstanceContext';
import { RecordShowContainer } from '@/object-record/record-show/components/RecordShowContainer';
import { useRecordShowPage } from '@/object-record/record-show/hooks/useRecordShowPage';
import { RecordValueSetterEffect } from '@/object-record/record-store/components/RecordValueSetterEffect';
@@ -46,62 +47,68 @@ export const RecordShowPage = () => {
return (
<RecordFieldValueSelectorContextProvider>
<ContextStoreComponentInstanceContext.Provider
<RecordFiltersComponentInstanceContext.Provider
value={{ instanceId: `record-show-${objectRecordId}` }}
>
<ActionMenuComponentInstanceContext.Provider
<ContextStoreComponentInstanceContext.Provider
value={{ instanceId: `record-show-${objectRecordId}` }}
>
<RecordValueSetterEffect recordId={objectRecordId} />
<PageContainer>
<PageTitle title={pageTitle} />
<RecordShowPageHeader
objectNameSingular={objectNameSingular}
objectRecordId={objectRecordId}
headerIcon={headerIcon}
>
<>
{!isCommandMenuV2Enabled &&
objectNameSingular === CoreObjectNameSingular.Workflow && (
<RecordShowPageWorkflowHeader workflowId={objectRecordId} />
)}
{!isCommandMenuV2Enabled &&
objectNameSingular ===
CoreObjectNameSingular.WorkflowVersion && (
<RecordShowPageWorkflowVersionHeader
workflowVersionId={objectRecordId}
<ActionMenuComponentInstanceContext.Provider
value={{ instanceId: `record-show-${objectRecordId}` }}
>
<RecordValueSetterEffect recordId={objectRecordId} />
<PageContainer>
<PageTitle title={pageTitle} />
<RecordShowPageHeader
objectNameSingular={objectNameSingular}
objectRecordId={objectRecordId}
headerIcon={headerIcon}
>
<>
{!isCommandMenuV2Enabled &&
objectNameSingular === CoreObjectNameSingular.Workflow && (
<RecordShowPageWorkflowHeader
workflowId={objectRecordId}
/>
)}
{!isCommandMenuV2Enabled &&
objectNameSingular ===
CoreObjectNameSingular.WorkflowVersion && (
<RecordShowPageWorkflowVersionHeader
workflowVersionId={objectRecordId}
/>
)}
{(isCommandMenuV2Enabled ||
(objectNameSingular !== CoreObjectNameSingular.Workflow &&
objectNameSingular !==
CoreObjectNameSingular.WorkflowVersion)) && (
<RecordShowActionMenu
{...{
isFavorite,
record,
handleFavoriteButtonClick,
objectMetadataItem,
objectNameSingular,
}}
/>
)}
{(isCommandMenuV2Enabled ||
(objectNameSingular !== CoreObjectNameSingular.Workflow &&
objectNameSingular !==
CoreObjectNameSingular.WorkflowVersion)) && (
<RecordShowActionMenu
{...{
isFavorite,
record,
handleFavoriteButtonClick,
objectMetadataItem,
objectNameSingular,
}}
</>
</RecordShowPageHeader>
<PageBody>
<TimelineActivityContext.Provider
value={{ labelIdentifierValue: pageName }}
>
<RecordShowContainer
objectNameSingular={objectNameSingular}
objectRecordId={objectRecordId}
loading={loading}
/>
)}
</>
</RecordShowPageHeader>
<PageBody>
<TimelineActivityContext.Provider
value={{ labelIdentifierValue: pageName }}
>
<RecordShowContainer
objectNameSingular={objectNameSingular}
objectRecordId={objectRecordId}
loading={loading}
/>
</TimelineActivityContext.Provider>
</PageBody>
</PageContainer>
</ActionMenuComponentInstanceContext.Provider>
</ContextStoreComponentInstanceContext.Provider>
</TimelineActivityContext.Provider>
</PageBody>
</PageContainer>
</ActionMenuComponentInstanceContext.Provider>
</ContextStoreComponentInstanceContext.Provider>
</RecordFiltersComponentInstanceContext.Provider>
</RecordFieldValueSelectorContextProvider>
);
};
@@ -22,6 +22,7 @@ import { mockedApolloClient } from '~/testing/mockedApolloClient';
import { RecoilDebugObserverEffect } from '@/debug/components/RecoilDebugObserver';
import { ObjectMetadataItemsProvider } from '@/object-metadata/components/ObjectMetadataItemsProvider';
import { RecordFiltersComponentInstanceContext } from '@/object-record/record-filter/states/context/RecordFiltersComponentInstanceContext';
import { PrefetchDataProvider } from '@/prefetch/components/PrefetchDataProvider';
import { WorkspaceProviderEffect } from '@/workspace/components/WorkspaceProviderEffect';
import { i18n } from '@lingui/core';
@@ -88,7 +89,13 @@ const Providers = () => {
<SnackBarProviderScope snackBarManagerScopeId="snack-bar-manager">
<IconsProvider>
<PrefetchDataProvider>
<Outlet />
<RecordFiltersComponentInstanceContext.Provider
value={{
instanceId: 'storybook-test-record-filters',
}}
>
<Outlet />
</RecordFiltersComponentInstanceContext.Provider>
</PrefetchDataProvider>
</IconsProvider>
</SnackBarProviderScope>
@@ -1,6 +1,7 @@
import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext';
import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext';
import { ContextStoreTargetedRecordsRule } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
import { RecordFiltersComponentInstanceContext } from '@/object-record/record-filter/states/context/RecordFiltersComponentInstanceContext';
import { MockedResponse } from '@apollo/client/testing';
import { ReactNode } from 'react';
import { MutableSnapshot } from 'recoil';
@@ -33,25 +34,33 @@ export const getJestMetadataAndApolloMocksAndActionMenuWrapper = ({
return ({ children }: { children: ReactNode }) => (
<Wrapper>
<ContextStoreComponentInstanceContext.Provider
value={{ instanceId: componentInstanceId }}
<RecordFiltersComponentInstanceContext.Provider
value={{
instanceId: componentInstanceId,
}}
>
<ActionMenuComponentInstanceContext.Provider
<ContextStoreComponentInstanceContext.Provider
value={{ instanceId: componentInstanceId }}
>
<JestContextStoreSetter
contextStoreTargetedRecordsRule={contextStoreTargetedRecordsRule}
contextStoreNumberOfSelectedRecords={
contextStoreNumberOfSelectedRecords
}
contextStoreCurrentObjectMetadataNameSingular={
contextStoreCurrentObjectMetadataNameSingular
}
<ActionMenuComponentInstanceContext.Provider
value={{
instanceId: componentInstanceId,
}}
>
{children}
</JestContextStoreSetter>
</ActionMenuComponentInstanceContext.Provider>
</ContextStoreComponentInstanceContext.Provider>
<JestContextStoreSetter
contextStoreTargetedRecordsRule={contextStoreTargetedRecordsRule}
contextStoreNumberOfSelectedRecords={
contextStoreNumberOfSelectedRecords
}
contextStoreCurrentObjectMetadataNameSingular={
contextStoreCurrentObjectMetadataNameSingular
}
>
{children}
</JestContextStoreSetter>
</ActionMenuComponentInstanceContext.Provider>
</ContextStoreComponentInstanceContext.Provider>
</RecordFiltersComponentInstanceContext.Provider>
</Wrapper>
);
};
@@ -2,14 +2,16 @@ import { MockedProvider, MockedResponse } from '@apollo/client/testing';
import { ReactNode } from 'react';
import { MutableSnapshot, RecoilRoot } from 'recoil';
import { RecordFiltersComponentInstanceContext } from '@/object-record/record-filter/states/context/RecordFiltersComponentInstanceContext';
import { SnackBarProviderScope } from '@/ui/feedback/snack-bar-manager/scopes/SnackBarProviderScope';
import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext';
import { JestObjectMetadataItemSetter } from '~/testing/jest/JestObjectMetadataItemSetter';
export const getJestMetadataAndApolloMocksWrapper = ({
apolloMocks,
onInitializeRecoilSnapshot,
}: {
apolloMocks:
apolloMocks?:
| readonly MockedResponse<Record<string, any>, Record<string, any>>[]
| undefined;
onInitializeRecoilSnapshot?: (snapshot: MutableSnapshot) => void;
@@ -18,9 +20,17 @@ export const getJestMetadataAndApolloMocksWrapper = ({
<RecoilRoot initializeState={onInitializeRecoilSnapshot}>
<SnackBarProviderScope snackBarManagerScopeId="snack-bar-manager">
<MockedProvider mocks={apolloMocks} addTypename={false}>
<JestObjectMetadataItemSetter>
{children}
</JestObjectMetadataItemSetter>
<RecordFiltersComponentInstanceContext.Provider
value={{ instanceId: 'instanceId' }}
>
<ViewComponentInstanceContext.Provider
value={{ instanceId: 'instanceId' }}
>
<JestObjectMetadataItemSetter>
{children}
</JestObjectMetadataItemSetter>
</ViewComponentInstanceContext.Provider>
</RecordFiltersComponentInstanceContext.Provider>
</MockedProvider>
</SnackBarProviderScope>
</RecoilRoot>