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:
@ -1,6 +1,6 @@
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
|
||||
import { FilterableFieldType } from '@/object-record/record-filter/types/FilterableFieldType';
|
||||
import { FilterableAndTSVectorFieldType } from '@/object-record/record-filter/types/FilterableFieldType';
|
||||
import { ObjectMetadataItem } from '../types/ObjectMetadataItem';
|
||||
|
||||
export const getRelationObjectMetadataNameSingular = ({
|
||||
@ -13,7 +13,7 @@ export const getRelationObjectMetadataNameSingular = ({
|
||||
|
||||
export const getFilterTypeFromFieldType = (
|
||||
fieldType: FieldMetadataType,
|
||||
): FilterableFieldType => {
|
||||
): FilterableAndTSVectorFieldType => {
|
||||
switch (fieldType) {
|
||||
case FieldMetadataType.DATE_TIME:
|
||||
return 'DATE_TIME';
|
||||
@ -49,6 +49,8 @@ export const getFilterTypeFromFieldType = (
|
||||
return 'RAW_JSON';
|
||||
case FieldMetadataType.BOOLEAN:
|
||||
return 'BOOLEAN';
|
||||
case FieldMetadataType.TS_VECTOR:
|
||||
return 'TS_VECTOR';
|
||||
default:
|
||||
return 'TEXT';
|
||||
}
|
||||
|
||||
@ -142,6 +142,10 @@ export type RichTextV2Filter = {
|
||||
markdown?: RichTextV2LeafFilter;
|
||||
};
|
||||
|
||||
export type TSVectorFilter = {
|
||||
search: string;
|
||||
};
|
||||
|
||||
export type LeafFilter =
|
||||
| UUIDFilter
|
||||
| StringFilter
|
||||
@ -158,6 +162,7 @@ export type LeafFilter =
|
||||
| ArrayFilter
|
||||
| RawJsonFilter
|
||||
| RichTextV2Filter
|
||||
| TSVectorFilter
|
||||
| undefined;
|
||||
|
||||
export type AndObjectRecordFilter = {
|
||||
|
||||
@ -5,6 +5,7 @@ import { ObjectFilterDropdownRatingInput } from '@/object-record/object-filter-d
|
||||
import { ObjectFilterDropdownRecordSelect } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownRecordSelect';
|
||||
import { ObjectFilterDropdownSearchInput } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownSearchInput';
|
||||
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
|
||||
import { ViewBarFilterDropdownVectorSearchInput } from '@/views/components/ViewBarFilterDropdownVectorSearchInput';
|
||||
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
|
||||
|
||||
import { getFilterTypeFromFieldType } from '@/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions';
|
||||
@ -55,6 +56,17 @@ export const ObjectFilterDropdownFilterInput = ({
|
||||
ViewFilterOperand.IsRelative,
|
||||
].includes(selectedOperandInDropdown);
|
||||
|
||||
const isVectorSearchFilter =
|
||||
selectedOperandInDropdown === ViewFilterOperand.VectorSearch;
|
||||
|
||||
if (isVectorSearchFilter && isDefined(filterDropdownId)) {
|
||||
return (
|
||||
<ViewBarFilterDropdownVectorSearchInput
|
||||
filterDropdownId={filterDropdownId}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (!isDefined(fieldMetadataItemUsedInDropdown)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
import { FilterableFieldType } from '@/object-record/record-filter/types/FilterableFieldType';
|
||||
import { FilterableAndTSVectorFieldType } from '@/object-record/record-filter/types/FilterableFieldType';
|
||||
import { RecordFilter } from '@/object-record/record-filter/types/RecordFilter';
|
||||
import { RecordFilterOperand } from '@/object-record/record-filter/types/RecordFilterOperand';
|
||||
import { z } from 'zod';
|
||||
|
||||
export const getInitialFilterValue = (
|
||||
newType: FilterableFieldType,
|
||||
newType: FilterableAndTSVectorFieldType,
|
||||
newOperand: RecordFilterOperand,
|
||||
oldValue?: string,
|
||||
oldDisplayValue?: string,
|
||||
|
||||
@ -1,7 +1,11 @@
|
||||
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 { VIEW_BAR_FILTER_DROPDOWN_ID } from '@/views/constants/ViewBarFilterDropdownId';
|
||||
import { vectorSearchInputComponentState } from '@/views/states/vectorSearchInputComponentState';
|
||||
import { isVectorSearchFilter } from '@/views/utils/isVectorSearchFilter';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const useRemoveRecordFilter = () => {
|
||||
const currentRecordFiltersCallbackState = useRecoilComponentCallbackStateV2(
|
||||
@ -16,24 +20,34 @@ export const useRemoveRecordFilter = () => {
|
||||
currentRecordFiltersCallbackState,
|
||||
);
|
||||
|
||||
const foundRecordFilterInCurrentRecordFilters =
|
||||
currentRecordFilters.some(
|
||||
const filterToRemove = currentRecordFilters.find(
|
||||
(existingFilter) => existingFilter.id === recordFilterId,
|
||||
);
|
||||
|
||||
if (!isDefined(filterToRemove)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isVectorSearchFilter(filterToRemove)) {
|
||||
set(
|
||||
vectorSearchInputComponentState.atomFamily({
|
||||
instanceId: VIEW_BAR_FILTER_DROPDOWN_ID,
|
||||
}),
|
||||
'',
|
||||
);
|
||||
}
|
||||
|
||||
set(currentRecordFiltersCallbackState, (currentRecordFilters) => {
|
||||
const newCurrentRecordFilters = [...currentRecordFilters];
|
||||
|
||||
const indexOfFilterToRemove = newCurrentRecordFilters.findIndex(
|
||||
(existingFilter) => existingFilter.id === recordFilterId,
|
||||
);
|
||||
|
||||
if (foundRecordFilterInCurrentRecordFilters) {
|
||||
set(currentRecordFiltersCallbackState, (currentRecordFilters) => {
|
||||
const newCurrentRecordFilters = [...currentRecordFilters];
|
||||
newCurrentRecordFilters.splice(indexOfFilterToRemove, 1);
|
||||
|
||||
const indexOfFilterToRemove = newCurrentRecordFilters.findIndex(
|
||||
(existingFilter) => existingFilter.id === recordFilterId,
|
||||
);
|
||||
|
||||
newCurrentRecordFilters.splice(indexOfFilterToRemove, 1);
|
||||
|
||||
return newCurrentRecordFilters;
|
||||
});
|
||||
}
|
||||
return newCurrentRecordFilters;
|
||||
});
|
||||
},
|
||||
[currentRecordFiltersCallbackState],
|
||||
);
|
||||
|
||||
@ -28,3 +28,5 @@ export type FilterableFieldType = PickLiteral<
|
||||
FieldType,
|
||||
FilterableFieldTypeBaseLiteral
|
||||
>;
|
||||
|
||||
export type FilterableAndTSVectorFieldType = FilterableFieldType | 'TS_VECTOR';
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { FilterableFieldType } from '@/object-record/record-filter/types/FilterableFieldType';
|
||||
import { FilterableAndTSVectorFieldType } from '@/object-record/record-filter/types/FilterableFieldType';
|
||||
import { FILTER_OPERANDS_MAP } from '@/object-record/record-filter/utils/getRecordFilterOperands';
|
||||
import { CompositeFieldSubFieldName } from '@/settings/data-model/types/CompositeFieldSubFieldName';
|
||||
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
|
||||
@ -8,7 +8,7 @@ export type RecordFilter = {
|
||||
fieldMetadataId: string;
|
||||
value: string;
|
||||
displayValue: string;
|
||||
type: FilterableFieldType;
|
||||
type: FilterableAndTSVectorFieldType;
|
||||
recordFilterGroupId?: string;
|
||||
displayAvatarUrl?: string;
|
||||
operand: ViewFilterOperand;
|
||||
@ -17,5 +17,6 @@ export type RecordFilter = {
|
||||
subFieldName?: CompositeFieldSubFieldName | null | undefined;
|
||||
};
|
||||
|
||||
export type RecordFilterToRecordInputOperand<T extends FilterableFieldType> =
|
||||
(typeof FILTER_OPERANDS_MAP)[T][number];
|
||||
export type RecordFilterToRecordInputOperand<
|
||||
T extends FilterableAndTSVectorFieldType,
|
||||
> = (typeof FILTER_OPERANDS_MAP)[T][number];
|
||||
|
||||
@ -0,0 +1,61 @@
|
||||
import { isMatchingTSVectorFilter } from '@/object-record/record-filter/utils/isMatchingTSVectorFilter';
|
||||
|
||||
describe('isMatchingTSVectorFilter', () => {
|
||||
describe('search', () => {
|
||||
it('value matches search filter', () => {
|
||||
expect(
|
||||
isMatchingTSVectorFilter({
|
||||
tsVectorFilter: { search: 'test' },
|
||||
value: 'test document',
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('value does not match search filter', () => {
|
||||
expect(
|
||||
isMatchingTSVectorFilter({
|
||||
tsVectorFilter: { search: 'missing' },
|
||||
value: 'test document',
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('search is case insensitive', () => {
|
||||
expect(
|
||||
isMatchingTSVectorFilter({
|
||||
tsVectorFilter: { search: 'TEST' },
|
||||
value: 'test document',
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('search matches partial words', () => {
|
||||
expect(
|
||||
isMatchingTSVectorFilter({
|
||||
tsVectorFilter: { search: 'doc' },
|
||||
value: 'test document',
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('search matches multiple words', () => {
|
||||
expect(
|
||||
isMatchingTSVectorFilter({
|
||||
tsVectorFilter: { search: 'test doc' },
|
||||
value: 'test document',
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('error handling', () => {
|
||||
it('should throw error for unknown filter type', () => {
|
||||
expect(() =>
|
||||
isMatchingTSVectorFilter({
|
||||
tsVectorFilter: { unknownFilter: 'test' } as any,
|
||||
value: 'test document',
|
||||
}),
|
||||
).toThrow('Unexpected value for ts_vector filter');
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -1,6 +1,6 @@
|
||||
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { getFilterTypeFromFieldType } from '@/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions';
|
||||
import { FilterableFieldType } from '@/object-record/record-filter/types/FilterableFieldType';
|
||||
import { FilterableAndTSVectorFieldType } from '@/object-record/record-filter/types/FilterableFieldType';
|
||||
import { RecordFilter } from '@/object-record/record-filter/types/RecordFilter';
|
||||
import { isEmptinessOperand } from '@/object-record/record-filter/utils/isEmptinessOperand';
|
||||
|
||||
@ -13,9 +13,8 @@ export const checkIfShouldComputeEmptinessFilter = ({
|
||||
}) => {
|
||||
const isAnEmptinessOperand = isEmptinessOperand(recordFilter.operand);
|
||||
|
||||
const filterTypesThatHaveNoEmptinessOperand: FilterableFieldType[] = [
|
||||
'BOOLEAN',
|
||||
];
|
||||
const filterTypesThatHaveNoEmptinessOperand: FilterableAndTSVectorFieldType[] =
|
||||
['BOOLEAN', 'TS_VECTOR'];
|
||||
|
||||
const filterType = getFilterTypeFromFieldType(
|
||||
correspondingFieldMetadataItem.type,
|
||||
|
||||
@ -16,6 +16,7 @@ import {
|
||||
RelationFilter,
|
||||
SelectFilter,
|
||||
StringFilter,
|
||||
TSVectorFilter,
|
||||
} from '@/object-record/graphql/types/RecordGqlOperationFilter';
|
||||
import { Field } from '~/generated/graphql';
|
||||
import { generateILikeFiltersForCompositeFields } from '~/utils/array/generateILikeFiltersForCompositeFields';
|
||||
@ -118,6 +119,19 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
|
||||
`Unknown operand ${recordFilter.operand} for ${filterType} filter`,
|
||||
);
|
||||
}
|
||||
case 'TS_VECTOR':
|
||||
switch (recordFilter.operand) {
|
||||
case RecordFilterOperand.VectorSearch:
|
||||
return {
|
||||
[correspondingFieldMetadataItem.name]: {
|
||||
search: recordFilter.value,
|
||||
} as TSVectorFilter,
|
||||
};
|
||||
default:
|
||||
throw new Error(
|
||||
`Unknown operand ${recordFilter.operand} for ${filterType} filter`,
|
||||
);
|
||||
}
|
||||
case 'RAW_JSON':
|
||||
switch (recordFilter.operand) {
|
||||
case RecordFilterOperand.Contains:
|
||||
|
||||
@ -1,13 +1,16 @@
|
||||
import { isExpectedSubFieldName } from '@/object-record/object-filter-dropdown/utils/isExpectedSubFieldName';
|
||||
import { isFilterOnActorSourceSubField } from '@/object-record/object-filter-dropdown/utils/isFilterOnActorSourceSubField';
|
||||
import { FilterableFieldType } from '@/object-record/record-filter/types/FilterableFieldType';
|
||||
import {
|
||||
FilterableAndTSVectorFieldType,
|
||||
FilterableFieldType,
|
||||
} from '@/object-record/record-filter/types/FilterableFieldType';
|
||||
import { CompositeFieldSubFieldName } from '@/settings/data-model/types/CompositeFieldSubFieldName';
|
||||
import { ViewFilterOperand as RecordFilterOperand } from '@/views/types/ViewFilterOperand';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { assertUnreachable } from 'twenty-shared/utils';
|
||||
|
||||
export type GetRecordFilterOperandsParams = {
|
||||
filterType: FilterableFieldType;
|
||||
filterType: FilterableAndTSVectorFieldType;
|
||||
subFieldName?: string | null | undefined;
|
||||
};
|
||||
|
||||
@ -22,7 +25,7 @@ const relationOperands = [
|
||||
] as const;
|
||||
|
||||
type FilterOperandMap = {
|
||||
[K in FilterableFieldType]: readonly RecordFilterOperand[];
|
||||
[K in FilterableAndTSVectorFieldType]: readonly RecordFilterOperand[];
|
||||
};
|
||||
|
||||
// TODO: we would need to refactor the typing of SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS first
|
||||
@ -124,6 +127,7 @@ export const FILTER_OPERANDS_MAP = {
|
||||
...emptyOperands,
|
||||
],
|
||||
BOOLEAN: [RecordFilterOperand.Is],
|
||||
TS_VECTOR: [RecordFilterOperand.VectorSearch],
|
||||
} as const satisfies FilterOperandMap;
|
||||
|
||||
export const COMPOSITE_FIELD_FILTER_OPERANDS_MAP = {
|
||||
@ -198,6 +202,8 @@ export const getRecordFilterOperands = ({
|
||||
return FILTER_OPERANDS_MAP.ARRAY;
|
||||
case 'BOOLEAN':
|
||||
return FILTER_OPERANDS_MAP.BOOLEAN;
|
||||
case 'TS_VECTOR':
|
||||
return FILTER_OPERANDS_MAP.TS_VECTOR;
|
||||
default:
|
||||
assertUnreachable(filterType, `Unknown filter type ${filterType}`);
|
||||
}
|
||||
|
||||
@ -0,0 +1,28 @@
|
||||
import { TSVectorFilter } from '@/object-record/graphql/types/RecordGqlOperationFilter';
|
||||
|
||||
export const isMatchingTSVectorFilter = ({
|
||||
tsVectorFilter,
|
||||
value,
|
||||
}: {
|
||||
tsVectorFilter: TSVectorFilter;
|
||||
value: string | undefined;
|
||||
}) => {
|
||||
// For optimistic updates where value is undefined, skip filtering
|
||||
if (value === undefined) {
|
||||
return true;
|
||||
}
|
||||
|
||||
switch (true) {
|
||||
case tsVectorFilter.search !== undefined: {
|
||||
const searchQuery = tsVectorFilter.search.toLowerCase();
|
||||
const searchValue = value.toLowerCase();
|
||||
const searchWords = searchQuery.split(/\s+/).filter(Boolean);
|
||||
return searchWords.every((word) => searchValue.includes(word));
|
||||
}
|
||||
default: {
|
||||
throw new Error(
|
||||
`Unexpected value for ts_vector filter : ${JSON.stringify(tsVectorFilter)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -24,6 +24,7 @@ import {
|
||||
RichTextV2Filter,
|
||||
SelectFilter,
|
||||
StringFilter,
|
||||
TSVectorFilter,
|
||||
UUIDFilter,
|
||||
} from '@/object-record/graphql/types/RecordGqlOperationFilter';
|
||||
import { isMatchingArrayFilter } from '@/object-record/record-filter/utils/isMatchingArrayFilter';
|
||||
@ -37,6 +38,7 @@ import { isMatchingRawJsonFilter } from '@/object-record/record-filter/utils/isM
|
||||
import { isMatchingRichTextV2Filter } from '@/object-record/record-filter/utils/isMatchingRichTextV2Filter';
|
||||
import { isMatchingSelectFilter } from '@/object-record/record-filter/utils/isMatchingSelectFilter';
|
||||
import { isMatchingStringFilter } from '@/object-record/record-filter/utils/isMatchingStringFilter';
|
||||
import { isMatchingTSVectorFilter } from '@/object-record/record-filter/utils/isMatchingTSVectorFilter';
|
||||
import { isMatchingUUIDFilter } from '@/object-record/record-filter/utils/isMatchingUUIDFilter';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
@ -374,10 +376,15 @@ export const isRecordMatchingFilter = ({
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`Not implemented yet, use UUID filter instead on the corredponding "${filterKey}Id" field`,
|
||||
`Not implemented yet, use UUID filter instead on the corresponding "${filterKey}Id" field`,
|
||||
);
|
||||
}
|
||||
|
||||
case FieldMetadataType.TS_VECTOR: {
|
||||
return isMatchingTSVectorFilter({
|
||||
tsVectorFilter: filterValue as TSVectorFilter,
|
||||
value: record[filterKey],
|
||||
});
|
||||
}
|
||||
default: {
|
||||
throw new Error(
|
||||
`Not implemented yet for field type "${objectMetadataField.type}"`,
|
||||
|
||||
@ -66,6 +66,11 @@ export const buildValueFromFilter = ({
|
||||
filter.operand as (typeof FILTER_OPERANDS_MAP)['BOOLEAN'][number],
|
||||
filter.value,
|
||||
);
|
||||
case 'TS_VECTOR':
|
||||
return computeValueFromFilterTSVector(
|
||||
filter.operand as (typeof FILTER_OPERANDS_MAP)['TS_VECTOR'][number],
|
||||
filter.value,
|
||||
);
|
||||
case 'ARRAY':
|
||||
return computeValueFromFilterArray(
|
||||
filter.operand as (typeof FILTER_OPERANDS_MAP)['ARRAY'][number],
|
||||
@ -296,3 +301,15 @@ const computeValueFromFilterRelation = (
|
||||
assertUnreachable(operand);
|
||||
}
|
||||
};
|
||||
|
||||
const computeValueFromFilterTSVector = (
|
||||
operand: RecordFilterToRecordInputOperand<'TS_VECTOR'>,
|
||||
value: string,
|
||||
) => {
|
||||
switch (operand) {
|
||||
case ViewFilterOperand.VectorSearch:
|
||||
return value;
|
||||
default:
|
||||
assertUnreachable(operand);
|
||||
}
|
||||
};
|
||||
|
||||
@ -0,0 +1,5 @@
|
||||
import { SEARCH_VECTOR_FIELD_NAME } from '@/views/constants/ViewFieldConstants';
|
||||
|
||||
export const isSystemSearchVectorField = (fieldName: string): boolean => {
|
||||
return fieldName === SEARCH_VECTOR_FIELD_NAME;
|
||||
};
|
||||
@ -1,5 +1,6 @@
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { isSystemSearchVectorField } from '@/object-record/utils/isSystemSearchVectorField';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { RelationDefinitionType } from '~/generated-metadata/graphql';
|
||||
import { FieldMetadataType } from '~/generated/graphql';
|
||||
@ -14,6 +15,10 @@ export const sanitizeRecordInput = ({
|
||||
const filteredResultRecord = Object.fromEntries(
|
||||
Object.entries(recordInput)
|
||||
.map<[string, unknown] | undefined>(([fieldName, fieldValue]) => {
|
||||
if (isSystemSearchVectorField(fieldName)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const fieldMetadataItem = objectMetadataItem.fields.find(
|
||||
(field) => field.name === fieldName,
|
||||
);
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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 (
|
||||
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
@ -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,
|
||||
|
||||
@ -0,0 +1,4 @@
|
||||
export const VIEW_BAR_FILTER_BOTTOM_MENU_ITEM_IDS = {
|
||||
SEARCH: 'search-button',
|
||||
ADVANCED_FILTER: 'advanced-filter-button',
|
||||
};
|
||||
@ -0,0 +1 @@
|
||||
export const SEARCH_VECTOR_FIELD_NAME = 'searchVector';
|
||||
@ -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 {
|
||||
|
||||
@ -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);
|
||||
};
|
||||
|
||||
@ -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 };
|
||||
};
|
||||
@ -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,
|
||||
};
|
||||
};
|
||||
@ -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,
|
||||
|
||||
@ -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({
|
||||
@ -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 {
|
||||
|
||||
@ -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 };
|
||||
};
|
||||
@ -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 };
|
||||
};
|
||||
@ -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,
|
||||
};
|
||||
};
|
||||
@ -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,
|
||||
};
|
||||
};
|
||||
@ -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,
|
||||
});
|
||||
@ -14,4 +14,5 @@ export enum ViewFilterOperand {
|
||||
IsInPast = 'isInPast',
|
||||
IsInFuture = 'isInFuture',
|
||||
IsToday = 'isToday',
|
||||
VectorSearch = 'search',
|
||||
}
|
||||
|
||||
@ -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] : []),
|
||||
];
|
||||
};
|
||||
@ -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;
|
||||
};
|
||||
@ -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;
|
||||
|
||||
@ -4,6 +4,7 @@ import {
|
||||
GraphqlQueryRunnerException,
|
||||
GraphqlQueryRunnerExceptionCode,
|
||||
} from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception';
|
||||
import { formatSearchTerms } from 'src/engine/core-modules/search/utils/format-search-terms';
|
||||
|
||||
type WhereConditionParts = {
|
||||
sql: string;
|
||||
@ -96,10 +97,7 @@ export const computeWhereConditionParts = ({
|
||||
params: { [`${key}${uuid}`]: value },
|
||||
};
|
||||
case 'search': {
|
||||
const tsQuery = value
|
||||
.split(/\s+/)
|
||||
.map((term: string) => `${term}:*`)
|
||||
.join(' & ');
|
||||
const tsQuery = formatSearchTerms(value, 'and');
|
||||
|
||||
return {
|
||||
sql: `(
|
||||
|
||||
Reference in New Issue
Block a user