feat: implement TS vector search filter (#12392)

Closes #12427 

This PR introduces a comprehensive search filter system that enhances
the application's data filtering capabilities. At its core, the
implementation leverages a custom useSearchFilter hook that manages
search state and operations, providing a consistent search experience
across different components. The search functionality is optimized for
performance through debounced operations (500ms) and efficient state
management using Recoil. Users can trigger search through keyboard
shortcuts (Ctrl/Cmd + F) or UI interactions, with the system maintaining
search state persistence and providing clear visual feedback. The
implementation integrates seamlessly with the existing record filtering
system, view bar components, and advanced filter system, while ensuring
good performance through optimized re-renders and component state
isolation.


https://github.com/user-attachments/assets/12936189-fba8-44b3-a30c-d8cb6d6bd514

---------

Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
Co-authored-by: Félix Malfait <felix@twenty.com>
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Co-authored-by: Marie <51697796+ijreilly@users.noreply.github.com>
Co-authored-by: Charles Bochet <charlesBochet@users.noreply.github.com>
Co-authored-by: Jordan Chalupka <9794216+jordan-chalupka@users.noreply.github.com>
Co-authored-by: Charles Bochet <charles@twenty.com>
Co-authored-by: Thomas Trompette <thomas.trompette@sfr.fr>
Co-authored-by: Guillim <guillim@users.noreply.github.com>
Co-authored-by: Raphaël Bosi <71827178+bosiraphael@users.noreply.github.com>
Co-authored-by: jaspass04 <147055860+jaspass04@users.noreply.github.com>
Co-authored-by: martmull <martmull@hotmail.fr>
Co-authored-by: Thomas des Francs <tdesfrancs@gmail.com>
Co-authored-by: Etienne <45695613+etiennejouan@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@twenty.com>
Co-authored-by: Weiko <corentin@twenty.com>
Co-authored-by: Matt Dvertola <64113801+mdvertola@users.noreply.github.com>
Co-authored-by: guillim <guigloo@msn.com>
Co-authored-by: Zeroday BYTE <github@zerodaysec.org>
This commit is contained in:
Abdul Rahman
2025-06-04 18:37:52 +05:30
committed by GitHub
parent 7046965496
commit 63c9af54f5
43 changed files with 656 additions and 132 deletions

View File

@ -2,14 +2,12 @@ import { ReactNode } from 'react';
import { useParams } from 'react-router-dom';
import { ObjectSortDropdownButton } from '@/object-record/object-sort-dropdown/components/ObjectSortDropdownButton';
import { useIsPrefetchLoading } from '@/prefetch/hooks/useIsPrefetchLoading';
import { TopBar } from '@/ui/layout/top-bar/components/TopBar';
import { QueryParamsFiltersEffect } from '@/views/components/QueryParamsFiltersEffect';
import { ViewBarPageTitle } from '@/views/components/ViewBarPageTitle';
import { ViewBarSkeletonLoader } from '@/views/components/ViewBarSkeletonLoader';
import { ViewPickerDropdown } from '@/views/view-picker/components/ViewPickerDropdown';
import { ViewsHotkeyScope } from '../types/ViewsHotkeyScope';
import { ObjectFilterDropdownComponentInstanceContext } from '@/object-record/object-filter-dropdown/states/contexts/ObjectFilterDropdownComponentInstanceContext';
@ -24,7 +22,7 @@ import { VIEW_BAR_FILTER_DROPDOWN_ID } from '@/views/constants/ViewBarFilterDrop
import { UpdateViewButtonGroup } from './UpdateViewButtonGroup';
import { ViewBarDetails } from './ViewBarDetails';
export type ViewBarProps = {
type ViewBarProps = {
viewBarId: string;
className?: string;
optionsDropdownButton: ReactNode;
@ -36,7 +34,6 @@ export const ViewBar = ({
optionsDropdownButton,
}: ViewBarProps) => {
const { objectNamePlural } = useParams();
const loading = useIsPrefetchLoading();
if (!objectNamePlural) {

View File

@ -2,6 +2,7 @@ import { useResetFilterDropdown } from '@/object-record/object-filter-dropdown/h
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
import { VIEW_BAR_FILTER_DROPDOWN_ID } from '@/views/constants/ViewBarFilterDropdownId';
import { useVectorSearchFilterActions } from '@/views/hooks/useVectorSearchFilterActions';
import { OPERAND_DROPDOWN_CLICK_OUTSIDE_ID } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownOperandDropdown';
import { objectFilterDropdownCurrentRecordFilterComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownCurrentRecordFilterComponentState';
@ -20,7 +21,7 @@ export const ViewBarFilterDropdown = ({
hotkeyScope,
}: ViewBarFilterDropdownProps) => {
const { resetFilterDropdown } = useResetFilterDropdown();
const { removeEmptyVectorSearchFilter } = useVectorSearchFilterActions();
const { removeRecordFilter } = useRemoveRecordFilter();
const objectFilterDropdownCurrentRecordFilter = useRecoilComponentValueV2(
@ -37,10 +38,13 @@ export const ViewBarFilterDropdown = ({
recordFilterId: objectFilterDropdownCurrentRecordFilter.id,
});
}
removeEmptyVectorSearchFilter();
};
const handleDropdownClose = () => {
resetFilterDropdown();
removeEmptyVectorSearchFilter();
};
return (

View File

@ -3,6 +3,10 @@ import { availableFieldMetadataItemsForFilterFamilySelector } from '@/object-met
import { useUpsertRecordFilterGroup } from '@/object-record/record-filter-group/hooks/useUpsertRecordFilterGroup';
import { currentRecordFilterGroupsComponentState } from '@/object-record/record-filter-group/states/currentRecordFilterGroupsComponentState';
import { useUpsertRecordFilter } from '@/object-record/record-filter/hooks/useUpsertRecordFilter';
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
import { isSelectedItemIdComponentFamilySelector } from '@/ui/layout/selectable-list/states/selectors/isSelectedItemIdComponentFamilySelector';
import { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValueV2';
import { VIEW_BAR_FILTER_BOTTOM_MENU_ITEM_IDS } from '@/views/constants/ViewBarFilterBottomMenuItemIds';
import { VIEW_BAR_FILTER_DROPDOWN_ID } from '@/views/constants/ViewBarFilterDropdownId';
import { useSetRecordFilterUsedInAdvancedFilterDropdownRow } from '@/object-record/advanced-filter/hooks/useSetRecordFilterUsedInAdvancedFilterDropdownRow';
@ -18,24 +22,10 @@ import { useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
import { Pill } from 'twenty-ui/components';
import { IconFilter } from 'twenty-ui/display';
import { MenuItemLeftContent, StyledMenuItemBase } from 'twenty-ui/navigation';
import { MenuItem } from 'twenty-ui/navigation';
import { v4 } from 'uuid';
export const StyledContainer = styled.div`
align-items: center;
display: flex;
justify-content: space-between;
padding: ${({ theme }) => theme.spacing(1)};
border-top: 1px solid ${({ theme }) => theme.border.color.light};
`;
export const StyledMenuItemSelect = styled(StyledMenuItemBase)`
&:hover {
background: ${({ theme }) => theme.background.transparent.light};
}
`;
export const StyledPill = styled(Pill)`
const StyledPill = styled(Pill)`
background: ${({ theme }) => theme.color.blueAccent10};
color: ${({ theme }) => theme.color.blue};
`;
@ -45,6 +35,11 @@ export const ViewBarFilterDropdownAdvancedFilterButton = () => {
const { t } = useLingui();
const isSelected = useRecoilComponentFamilyValueV2(
isSelectedItemIdComponentFamilySelector,
VIEW_BAR_FILTER_BOTTOM_MENU_ITEM_IDS.ADVANCED_FILTER,
);
const { openDropdown: openAdvancedFilterDropdown } = useDropdown(
ADVANCED_FILTER_DROPDOWN_ID,
);
@ -130,13 +125,19 @@ export const ViewBarFilterDropdownAdvancedFilterButton = () => {
};
return (
<StyledContainer>
<StyledMenuItemSelect onClick={handleClick}>
<MenuItemLeftContent LeftIcon={IconFilter} text={t`Advanced filter`} />
{advancedFilterQuerySubFilterCount > 0 && (
<StyledPill label={advancedFilterQuerySubFilterCount.toString()} />
)}
</StyledMenuItemSelect>
</StyledContainer>
<SelectableListItem
itemId={VIEW_BAR_FILTER_BOTTOM_MENU_ITEM_IDS.ADVANCED_FILTER}
onEnter={handleClick}
>
<MenuItem
text={t`Advanced filter`}
onClick={handleClick}
LeftIcon={IconFilter}
focused={isSelected}
/>
{advancedFilterQuerySubFilterCount > 0 && (
<StyledPill label={advancedFilterQuerySubFilterCount.toString()} />
)}
</SelectableListItem>
);
};

View File

@ -0,0 +1,20 @@
import { ViewBarFilterDropdownAdvancedFilterButton } from '@/views/components/ViewBarFilterDropdownAdvancedFilterButton';
import { ViewBarFilterDropdownVectorSearchButton } from '@/views/components/ViewBarFilterDropdownVectorSearchButton';
import styled from '@emotion/styled';
const StyledContainer = styled.div`
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-between;
padding: ${({ theme }) => theme.spacing(1)};
`;
export const ViewBarFilterDropdownBottomMenu = () => {
return (
<StyledContainer>
<ViewBarFilterDropdownVectorSearchButton />
<ViewBarFilterDropdownAdvancedFilterButton />
</StyledContainer>
);
};

View File

@ -1,4 +1,5 @@
import styled from '@emotion/styled';
import React from 'react';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
@ -12,8 +13,10 @@ import { useFilterDropdownSelectableFieldMetadataItems } from '@/object-record/o
import { FiltersHotkeyScope } from '@/object-record/object-filter-dropdown/types/FiltersHotkeyScope';
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2';
import { ViewBarFilterDropdownAdvancedFilterButton } from '@/views/components/ViewBarFilterDropdownAdvancedFilterButton';
import { ViewBarFilterDropdownBottomMenu } from '@/views/components/ViewBarFilterDropdownBottomMenu';
import { ViewBarFilterDropdownFieldSelectMenuItem } from '@/views/components/ViewBarFilterDropdownFieldSelectMenuItem';
import { VIEW_BAR_FILTER_BOTTOM_MENU_ITEM_IDS } from '@/views/constants/ViewBarFilterBottomMenuItemIds';
import { useLingui } from '@lingui/react/macro';
export const StyledInput = styled.input`
@ -58,12 +61,18 @@ export const ViewBarFilterDropdownFieldSelectMenu = () => {
...selectableHiddenFieldMetadataItems.map(
(fieldMetadataItem) => fieldMetadataItem.id,
),
VIEW_BAR_FILTER_BOTTOM_MENU_ITEM_IDS.SEARCH,
VIEW_BAR_FILTER_BOTTOM_MENU_ITEM_IDS.ADVANCED_FILTER,
];
const shouldShowSeparator =
selectableVisibleFieldMetadataItems.length > 0 &&
selectableHiddenFieldMetadataItems.length > 0;
const hasSelectableItems =
selectableVisibleFieldMetadataItems.length > 0 ||
selectableHiddenFieldMetadataItems.length > 0;
const { t } = useLingui();
return (
@ -81,25 +90,30 @@ export const ViewBarFilterDropdownFieldSelectMenu = () => {
selectableItemIdArray={selectableFieldMetadataItemIds}
selectableListInstanceId={FILTER_FIELD_LIST_ID}
>
<DropdownMenuItemsContainer>
{selectableVisibleFieldMetadataItems.map(
(visibleFieldMetadataItem) => (
<ViewBarFilterDropdownFieldSelectMenuItem
key={visibleFieldMetadataItem.id}
fieldMetadataItemToSelect={visibleFieldMetadataItem}
/>
),
)}
{shouldShowSeparator && <DropdownMenuSeparator />}
{selectableHiddenFieldMetadataItems.map((hiddenFieldMetadataItem) => (
<ViewBarFilterDropdownFieldSelectMenuItem
key={hiddenFieldMetadataItem.id}
fieldMetadataItemToSelect={hiddenFieldMetadataItem}
/>
))}
</DropdownMenuItemsContainer>
{hasSelectableItems && (
<DropdownMenuItemsContainer>
{selectableVisibleFieldMetadataItems.map(
(visibleFieldMetadataItem) => (
<ViewBarFilterDropdownFieldSelectMenuItem
key={visibleFieldMetadataItem.id}
fieldMetadataItemToSelect={visibleFieldMetadataItem}
/>
),
)}
{shouldShowSeparator && <DropdownMenuSeparator />}
{selectableHiddenFieldMetadataItems.map(
(hiddenFieldMetadataItem) => (
<ViewBarFilterDropdownFieldSelectMenuItem
key={hiddenFieldMetadataItem.id}
fieldMetadataItemToSelect={hiddenFieldMetadataItem}
/>
),
)}
</DropdownMenuItemsContainer>
)}
{hasSelectableItems && <DropdownMenuSeparator />}
<ViewBarFilterDropdownBottomMenu />
</SelectableList>
<ViewBarFilterDropdownAdvancedFilterButton />
</DropdownContent>
);
};

View File

@ -0,0 +1,80 @@
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
import { isSelectedItemIdComponentFamilySelector } from '@/ui/layout/selectable-list/states/selectors/isSelectedItemIdComponentFamilySelector';
import { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValueV2';
import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import styled from '@emotion/styled';
import { useLingui } from '@lingui/react/macro';
import { IconSearch } from 'twenty-ui/display';
import { MenuItem } from 'twenty-ui/navigation';
import { VIEW_BAR_FILTER_BOTTOM_MENU_ITEM_IDS } from '@/views/constants/ViewBarFilterBottomMenuItemIds';
import { VIEW_BAR_FILTER_DROPDOWN_ID } from '@/views/constants/ViewBarFilterDropdownId';
import { objectFilterDropdownSearchInputComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownSearchInputComponentState';
import { useOpenVectorSearchFilter } from '@/views/hooks/useOpenVectorSearchFilter';
import { useSetVectorSearchInputValueFromExistingFilter } from '@/views/hooks/useSetVectorSearchInputValueFromExistingFilter';
import { useVectorSearchFilterActions } from '@/views/hooks/useVectorSearchFilterActions';
import { vectorSearchInputComponentState } from '@/views/states/vectorSearchInputComponentState';
const StyledSearchText = styled.span`
color: ${({ theme }) => theme.font.color.light};
margin-left: ${({ theme }) => theme.spacing(1)};
`;
export const ViewBarFilterDropdownVectorSearchButton = () => {
const { t } = useLingui();
const [, setVectorSearchInputValue] = useRecoilComponentStateV2(
vectorSearchInputComponentState,
VIEW_BAR_FILTER_DROPDOWN_ID,
);
const { setVectorSearchInputValueFromExistingFilter } =
useSetVectorSearchInputValueFromExistingFilter(VIEW_BAR_FILTER_DROPDOWN_ID);
const fieldSearchInputValue = useRecoilComponentValueV2(
objectFilterDropdownSearchInputComponentState,
VIEW_BAR_FILTER_DROPDOWN_ID,
);
const { applyVectorSearchFilter } = useVectorSearchFilterActions();
const { openVectorSearchFilter } = useOpenVectorSearchFilter(
VIEW_BAR_FILTER_DROPDOWN_ID,
);
const isSelected = useRecoilComponentFamilyValueV2(
isSelectedItemIdComponentFamilySelector,
VIEW_BAR_FILTER_BOTTOM_MENU_ITEM_IDS.SEARCH,
);
const handleSearchClick = () => {
openVectorSearchFilter();
if (fieldSearchInputValue.length > 0) {
setVectorSearchInputValue(fieldSearchInputValue);
applyVectorSearchFilter(fieldSearchInputValue);
} else {
setVectorSearchInputValueFromExistingFilter();
}
};
return (
<SelectableListItem
itemId={VIEW_BAR_FILTER_BOTTOM_MENU_ITEM_IDS.SEARCH}
onEnter={handleSearchClick}
>
<MenuItem
focused={isSelected}
onClick={handleSearchClick}
LeftIcon={IconSearch}
text={
<>
{t`Search`}
{fieldSearchInputValue && (
<StyledSearchText>{t`· ${fieldSearchInputValue}`}</StyledSearchText>
)}
</>
}
/>
</SelectableListItem>
);
};

View File

@ -0,0 +1,47 @@
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
import { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth';
import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2';
import { useVectorSearchFilterActions } from '@/views/hooks/useVectorSearchFilterActions';
import { vectorSearchInputComponentState } from '@/views/states/vectorSearchInputComponentState';
import { useLingui } from '@lingui/react/macro';
import { useDebouncedCallback } from 'use-debounce';
export const ViewBarFilterDropdownVectorSearchInput = ({
filterDropdownId,
}: {
filterDropdownId: string;
}) => {
const { t } = useLingui();
const [vectorSearchInputValue, setVectorSearchInputValue] =
useRecoilComponentStateV2(
vectorSearchInputComponentState,
filterDropdownId,
);
const { applyVectorSearchFilter } = useVectorSearchFilterActions();
const debouncedApplyVectorSearchFilter = useDebouncedCallback(
(value: string) => {
applyVectorSearchFilter(value);
},
500,
);
const handleSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const inputValue = e.target.value;
setVectorSearchInputValue(inputValue);
debouncedApplyVectorSearchFilter(inputValue);
};
return (
<DropdownContent widthInPixels={GenericDropdownContentWidth.Medium}>
<DropdownMenuSearchInput
autoFocus
type="text"
value={vectorSearchInputValue}
placeholder={t`Search`}
onChange={handleSearchChange}
/>
</DropdownContent>
);
};

View File

@ -1,13 +1,12 @@
import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState';
import { useFilterableFieldMetadataItems } from '@/object-record/record-filter/hooks/useFilterableFieldMetadataItems';
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
import { prefetchViewFromViewIdFamilySelector } from '@/prefetch/states/selector/prefetchViewFromViewIdFamilySelector';
import { useRecoilComponentFamilyStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyStateV2';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
import { useMapViewFiltersToFilters } from '@/views/hooks/useMapViewFiltersToFilters';
import { hasInitializedCurrentRecordFiltersComponentFamilyState } from '@/views/states/hasInitializedCurrentRecordFiltersComponentFamilyState';
import { mapViewFiltersToFilters } from '@/views/utils/mapViewFiltersToFilters';
import { useEffect } from 'react';
import { useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
@ -39,9 +38,7 @@ export const ViewBarRecordFilterEffect = () => {
currentRecordFiltersComponentState,
);
const { filterableFieldMetadataItems } = useFilterableFieldMetadataItems(
objectMetadataItem.id,
);
const { mapViewFiltersToRecordFilters } = useMapViewFiltersToFilters();
useEffect(() => {
if (!hasInitializedCurrentRecordFilters && isDefined(currentView)) {
@ -50,10 +47,7 @@ export const ViewBarRecordFilterEffect = () => {
}
setCurrentRecordFilters(
mapViewFiltersToFilters(
currentView.viewFilters,
filterableFieldMetadataItems,
),
mapViewFiltersToRecordFilters(currentView.viewFilters),
);
setHasInitializedCurrentRecordFilters(true);
@ -61,7 +55,7 @@ export const ViewBarRecordFilterEffect = () => {
}, [
currentViewId,
setCurrentRecordFilters,
filterableFieldMetadataItems,
mapViewFiltersToRecordFilters,
hasInitializedCurrentRecordFilters,
setHasInitializedCurrentRecordFilters,
currentView,

View File

@ -0,0 +1,4 @@
export const VIEW_BAR_FILTER_BOTTOM_MENU_ITEM_IDS = {
SEARCH: 'search-button',
ADVANCED_FILTER: 'advanced-filter-button',
};

View File

@ -0,0 +1 @@
export const SEARCH_VECTOR_FIELD_NAME = 'searchVector';

View File

@ -1,12 +1,11 @@
import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState';
import { useFilterableFieldMetadataItemsInRecordIndexContext } from '@/object-record/record-filter/hooks/useFilterableFieldMetadataItemsInRecordIndexContext';
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
import { prefetchViewFromViewIdFamilySelector } from '@/prefetch/states/selector/prefetchViewFromViewIdFamilySelector';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
import { mapViewFiltersToFilters } from '@/views/utils/mapViewFiltersToFilters';
import { useRecoilCallback } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
import { useMapViewFiltersToFilters } from './useMapViewFiltersToFilters';
export const useApplyCurrentViewFiltersToCurrentRecordFilters = () => {
const currentViewId = useRecoilComponentValueV2(
@ -17,8 +16,7 @@ export const useApplyCurrentViewFiltersToCurrentRecordFilters = () => {
currentRecordFiltersComponentState,
);
const { filterableFieldMetadataItems } =
useFilterableFieldMetadataItemsInRecordIndexContext();
const { mapViewFiltersToRecordFilters } = useMapViewFiltersToFilters();
const applyCurrentViewFiltersToCurrentRecordFilters = useRecoilCallback(
({ snapshot }) =>
@ -33,14 +31,11 @@ export const useApplyCurrentViewFiltersToCurrentRecordFilters = () => {
if (isDefined(currentView)) {
setCurrentRecordFilters(
mapViewFiltersToFilters(
currentView.viewFilters,
filterableFieldMetadataItems,
),
mapViewFiltersToRecordFilters(currentView.viewFilters),
);
}
},
[currentViewId, filterableFieldMetadataItems, setCurrentRecordFilters],
[currentViewId, mapViewFiltersToRecordFilters, setCurrentRecordFilters],
);
return {

View File

@ -1,28 +1,19 @@
import { useFilterableFieldMetadataItems } from '@/object-record/record-filter/hooks/useFilterableFieldMetadataItems';
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
import { ViewFilter } from '@/views/types/ViewFilter';
import { mapViewFiltersToFilters } from '@/views/utils/mapViewFiltersToFilters';
import { useMapViewFiltersToFilters } from './useMapViewFiltersToFilters';
export const useApplyViewFiltersToCurrentRecordFilters = () => {
const setCurrentRecordFilters = useSetRecoilComponentStateV2(
currentRecordFiltersComponentState,
);
const { objectMetadataItem } = useRecordIndexContextOrThrow();
const { filterableFieldMetadataItems } = useFilterableFieldMetadataItems(
objectMetadataItem.id,
);
const { mapViewFiltersToRecordFilters } = useMapViewFiltersToFilters();
const applyViewFiltersToCurrentRecordFilters = (
viewFilters: ViewFilter[],
) => {
const recordFiltersToApply = mapViewFiltersToFilters(
viewFilters,
filterableFieldMetadataItems,
);
const recordFiltersToApply = mapViewFiltersToRecordFilters(viewFilters);
setCurrentRecordFilters(recordFiltersToApply);
};

View File

@ -0,0 +1,16 @@
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
import { ViewFilter } from '../types/ViewFilter';
import { getFilterableFieldsWithVectorSearch } from '../utils/getFilterableFieldsWithVectorSearch';
import { mapViewFiltersToFilters } from '../utils/mapViewFiltersToFilters';
export const useMapViewFiltersToFilters = () => {
const { objectMetadataItem } = useRecordIndexContextOrThrow();
const mapViewFiltersToRecordFilters = (viewFilters: ViewFilter[]) => {
const filterableFieldMetadataItems =
getFilterableFieldsWithVectorSearch(objectMetadataItem);
return mapViewFiltersToFilters(viewFilters, filterableFieldMetadataItems);
};
return { mapViewFiltersToRecordFilters };
};

View File

@ -0,0 +1,25 @@
import { objectFilterDropdownFilterIsSelectedComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownFilterIsSelectedComponentState';
import { selectedOperandInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/selectedOperandInDropdownComponentState';
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
export const useOpenVectorSearchFilter = (filterDropdownId?: string) => {
const setSelectedOperandInDropdown = useSetRecoilComponentStateV2(
selectedOperandInDropdownComponentState,
filterDropdownId,
);
const setObjectFilterDropdownFilterIsSelected = useSetRecoilComponentStateV2(
objectFilterDropdownFilterIsSelectedComponentState,
filterDropdownId,
);
const openVectorSearchFilter = () => {
setObjectFilterDropdownFilterIsSelected(true);
setSelectedOperandInDropdown(ViewFilterOperand.VectorSearch);
};
return {
openVectorSearchFilter,
};
};

View File

@ -1,8 +1,7 @@
import { useActiveFieldMetadataItems } from '@/object-metadata/hooks/useActiveFieldMetadataItems';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { useFilterValueDependencies } from '@/object-record/record-filter/hooks/useFilterValueDependencies';
import { useViewOrDefaultViewFromPrefetchedViews } from '@/views/hooks/useViewOrDefaultViewFromPrefetchedViews';
import { getQueryVariablesFromView } from '@/views/utils/getQueryVariablesFromView';
import { useQueryVariablesFromView } from './useQueryVariablesFromView';
export const useQueryVariablesFromActiveFieldsOfViewOrDefaultView = ({
objectMetadataItem,
@ -13,14 +12,9 @@ export const useQueryVariablesFromActiveFieldsOfViewOrDefaultView = ({
objectMetadataItemId: objectMetadataItem.id,
});
const { activeFieldMetadataItems } = useActiveFieldMetadataItems({
objectMetadataItem,
});
const { filterValueDependencies } = useFilterValueDependencies();
const { filter, orderBy } = getQueryVariablesFromView({
fieldMetadataItems: activeFieldMetadataItems,
const { filter, orderBy } = useQueryVariablesFromView({
objectMetadataItem,
view,
filterValueDependencies,

View File

@ -1,24 +1,20 @@
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { turnSortsIntoOrderBy } from '@/object-record/object-sort-dropdown/utils/turnSortsIntoOrderBy';
import { RecordFilterValueDependencies } from '@/object-record/record-filter/types/RecordFilterValueDependencies';
import { computeRecordGqlOperationFilter } from '@/object-record/record-filter/utils/computeRecordGqlOperationFilter';
import { View } from '@/views/types/View';
import { getFilterableFieldsWithVectorSearch } from '@/views/utils/getFilterableFieldsWithVectorSearch';
import { mapViewFilterGroupsToRecordFilterGroups } from '@/views/utils/mapViewFilterGroupsToRecordFilterGroups';
import { mapViewFiltersToFilters } from '@/views/utils/mapViewFiltersToFilters';
import { mapViewSortsToSorts } from '@/views/utils/mapViewSortsToSorts';
import { isDefined } from 'twenty-shared/utils';
export const getQueryVariablesFromView = ({
export const useQueryVariablesFromView = ({
view,
fieldMetadataItems,
objectMetadataItem,
filterValueDependencies,
}: {
view: View | null | undefined;
fieldMetadataItems: FieldMetadataItem[];
objectMetadataItem: ObjectMetadataItem;
filterValueDependencies: RecordFilterValueDependencies;
}) => {
@ -35,9 +31,12 @@ export const getQueryVariablesFromView = ({
viewFilterGroups ?? [],
);
const filterableFieldMetadataItems =
getFilterableFieldsWithVectorSearch(objectMetadataItem);
const recordFilters = mapViewFiltersToFilters(
viewFilters,
fieldMetadataItems,
filterableFieldMetadataItems,
);
const filter = computeRecordGqlOperationFilter({

View File

@ -4,6 +4,9 @@ import { selectedOperandInDropdownComponentState } from '@/object-record/object-
import { subFieldNameUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/subFieldNameUsedInDropdownComponentState';
import { useFilterableFieldMetadataItemsInRecordIndexContext } from '@/object-record/record-filter/hooks/useFilterableFieldMetadataItemsInRecordIndexContext';
import { RecordFilter } from '@/object-record/record-filter/types/RecordFilter';
import { useVectorSearchFieldInRecordIndexContextOrThrow } from '@/views/hooks/useVectorSearchFieldInRecordIndexContextOrThrow';
import { vectorSearchInputComponentState } from '@/views/states/vectorSearchInputComponentState';
import { isVectorSearchFilter } from '@/views/utils/isVectorSearchFilter';
import { useRecoilCallback } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
@ -11,10 +14,17 @@ export const useSetEditableFilterChipDropdownStates = () => {
const { filterableFieldMetadataItems } =
useFilterableFieldMetadataItemsInRecordIndexContext();
const { vectorSearchField } =
useVectorSearchFieldInRecordIndexContextOrThrow();
const setEditableFilterChipDropdownStates = useRecoilCallback(
({ set }) =>
(recordFilter: RecordFilter) => {
const fieldMetadataItem = filterableFieldMetadataItems.find(
const filterableFieldsWithVector = vectorSearchField
? filterableFieldMetadataItems.concat(vectorSearchField)
: filterableFieldMetadataItems;
const fieldMetadataItem = filterableFieldsWithVector.find(
(fieldMetadataItem) =>
fieldMetadataItem.id === recordFilter.fieldMetadataId,
);
@ -23,6 +33,15 @@ export const useSetEditableFilterChipDropdownStates = () => {
return;
}
if (isVectorSearchFilter(recordFilter)) {
set(
vectorSearchInputComponentState.atomFamily({
instanceId: recordFilter.id,
}),
recordFilter.value,
);
}
set(
fieldMetadataItemIdUsedInDropdownComponentState.atomFamily({
instanceId: recordFilter.id,
@ -51,7 +70,7 @@ export const useSetEditableFilterChipDropdownStates = () => {
recordFilter.subFieldName,
);
},
[filterableFieldMetadataItems],
[filterableFieldMetadataItems, vectorSearchField],
);
return {

View File

@ -0,0 +1,23 @@
import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2';
import { vectorSearchInputComponentState } from '@/views/states/vectorSearchInputComponentState';
import { isDefined } from 'twenty-shared/utils';
import { useVectorSearchFilterState } from './useVectorSearchFilterState';
export const useSetVectorSearchInputValueFromExistingFilter = (
filterDropdownId: string,
) => {
const [, setVectorSearchInputValue] = useRecoilComponentStateV2(
vectorSearchInputComponentState,
filterDropdownId,
);
const { getExistingVectorSearchFilter } = useVectorSearchFilterState();
const setVectorSearchInputValueFromExistingFilter = () => {
const existingVectorSearchFilter = getExistingVectorSearchFilter();
if (isDefined(existingVectorSearchFilter)) {
setVectorSearchInputValue(existingVectorSearchFilter.value);
}
};
return { setVectorSearchInputValueFromExistingFilter };
};

View File

@ -0,0 +1,13 @@
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
import { SEARCH_VECTOR_FIELD_NAME } from '../constants/ViewFieldConstants';
export const useVectorSearchFieldInRecordIndexContextOrThrow = () => {
const { objectMetadataItem } = useRecordIndexContextOrThrow();
const vectorSearchField = objectMetadataItem.fields.find(
(field) =>
field.type === 'TS_VECTOR' && field.name === SEARCH_VECTOR_FIELD_NAME,
);
return { vectorSearchField };
};

View File

@ -0,0 +1,55 @@
import { getFilterTypeFromFieldType } from '@/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions';
import { useRemoveRecordFilter } from '@/object-record/record-filter/hooks/useRemoveRecordFilter';
import { useUpsertRecordFilter } from '@/object-record/record-filter/hooks/useUpsertRecordFilter';
import { isRecordFilterConsideredEmpty } from '@/object-record/record-filter/utils/isRecordFilterConsideredEmpty';
import { useVectorSearchFieldInRecordIndexContextOrThrow } from '@/views/hooks/useVectorSearchFieldInRecordIndexContextOrThrow';
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
import { isDefined } from 'twenty-shared/utils';
import { v4 } from 'uuid';
import { useVectorSearchFilterState } from './useVectorSearchFilterState';
export const useVectorSearchFilterActions = () => {
const { vectorSearchField } =
useVectorSearchFieldInRecordIndexContextOrThrow();
const { getExistingVectorSearchFilter } = useVectorSearchFilterState();
const { upsertRecordFilter } = useUpsertRecordFilter();
const { removeRecordFilter } = useRemoveRecordFilter();
const applyVectorSearchFilter = (value: string) => {
if (!vectorSearchField) {
return;
}
const existingVectorSearchFilter = getExistingVectorSearchFilter();
const vectorSearchRecordFilter = {
id: existingVectorSearchFilter?.id ?? v4(),
fieldMetadataId: vectorSearchField.id,
value: value,
displayValue: value,
operand: ViewFilterOperand.VectorSearch,
type: getFilterTypeFromFieldType(vectorSearchField.type),
label: 'Search',
};
upsertRecordFilter(vectorSearchRecordFilter);
};
const removeEmptyVectorSearchFilter = () => {
const vectorSearchFilter = getExistingVectorSearchFilter();
if (
isDefined(vectorSearchFilter) &&
isRecordFilterConsideredEmpty(vectorSearchFilter)
) {
removeRecordFilter({
recordFilterId: vectorSearchFilter.id,
});
}
};
return {
applyVectorSearchFilter,
removeEmptyVectorSearchFilter,
};
};

View File

@ -0,0 +1,17 @@
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { isVectorSearchFilter } from '@/views/utils/isVectorSearchFilter';
export const useVectorSearchFilterState = () => {
const currentRecordFilters = useRecoilComponentValueV2(
currentRecordFiltersComponentState,
);
const getExistingVectorSearchFilter = () => {
return currentRecordFilters.find(isVectorSearchFilter);
};
return {
getExistingVectorSearchFilter,
};
};

View File

@ -0,0 +1,8 @@
import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2';
import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext';
export const vectorSearchInputComponentState = createComponentStateV2<string>({
key: 'vectorSearchInputComponentState',
defaultValue: '',
componentInstanceContext: ViewComponentInstanceContext,
});

View File

@ -14,4 +14,5 @@ export enum ViewFilterOperand {
IsInPast = 'isInPast',
IsInFuture = 'isInFuture',
IsToday = 'isToday',
VectorSearch = 'search',
}

View File

@ -0,0 +1,19 @@
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { getFilterFilterableFieldMetadataItems } from '@/object-metadata/utils/getFilterFilterableFieldMetadataItems';
import { SEARCH_VECTOR_FIELD_NAME } from '../constants/ViewFieldConstants';
export const getFilterableFieldsWithVectorSearch = (
objectMetadataItem: ObjectMetadataItem,
) => {
const vectorSearchField = objectMetadataItem.fields.find(
(field) =>
field.type === 'TS_VECTOR' && field.name === SEARCH_VECTOR_FIELD_NAME,
);
return [
...objectMetadataItem.fields.filter(
getFilterFilterableFieldMetadataItems({ isJsonFilterEnabled: true }),
),
...(vectorSearchField ? [vectorSearchField] : []),
];
};

View File

@ -0,0 +1,6 @@
import { RecordFilter } from '@/object-record/record-filter/types/RecordFilter';
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
export const isVectorSearchFilter = (filter: RecordFilter) => {
return filter.operand === ViewFilterOperand.VectorSearch;
};

View File

@ -3,6 +3,7 @@ import { RecordFilter } from '@/object-record/record-filter/types/RecordFilter';
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { getFilterTypeFromFieldType } from '@/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions';
import { isSystemSearchVectorField } from '@/object-record/utils/isSystemSearchVectorField';
import { isDefined } from 'twenty-shared/utils';
import { ViewFilter } from '../types/ViewFilter';
@ -26,6 +27,10 @@ export const mapViewFiltersToFilters = (
availableFieldMetadataItem.type,
);
const label = isSystemSearchVectorField(availableFieldMetadataItem.name)
? 'Search'
: availableFieldMetadataItem.label;
return {
id: viewFilter.id,
fieldMetadataId: viewFilter.fieldMetadataId,
@ -34,7 +39,7 @@ export const mapViewFiltersToFilters = (
operand: viewFilter.operand,
recordFilterGroupId: viewFilter.viewFilterGroupId,
positionInRecordFilterGroup: viewFilter.positionInViewFilterGroup,
label: availableFieldMetadataItem.label,
label,
type: filterType,
subFieldName: viewFilter.subFieldName,
} satisfies RecordFilter;