diff --git a/frontend/src/components/QueryBuilderV2/QueryV2/QuerySearch/QuerySearch.tsx b/frontend/src/components/QueryBuilderV2/QueryV2/QuerySearch/QuerySearch.tsx index f4576ad50a6b..6691f3fc6533 100644 --- a/frontend/src/components/QueryBuilderV2/QueryV2/QuerySearch/QuerySearch.tsx +++ b/frontend/src/components/QueryBuilderV2/QueryV2/QuerySearch/QuerySearch.tsx @@ -26,9 +26,9 @@ import { queryOperatorSuggestions, } from 'constants/antlrQueryConstants'; import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; -import { isNull } from 'lodash-es'; +import { debounce, isNull } from 'lodash-es'; import { TriangleAlert } from 'lucide-react'; -import { useCallback, useEffect, useRef, useState } from 'react'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { IDetailedError, IQueryContext, @@ -42,6 +42,7 @@ import { getCurrentValueIndexAtCursor, getQueryContextAtCursor, } from 'utils/queryContextUtils'; +import { unquote } from 'utils/stringUtils'; import { queryExamples } from './constants'; @@ -109,7 +110,6 @@ function QuerySearch({ useEffect(() => { setQuery(queryData.filter?.expression || ''); - handleQueryValidation(queryData.filter?.expression || ''); }, [queryData.filter?.expression]); const [keySuggestions, setKeySuggestions] = useState< @@ -400,6 +400,11 @@ function QuerySearch({ [activeKey, dataSource, isLoadingSuggestions], ); + const debouncedFetchValueSuggestions = useMemo( + () => debounce(fetchValueSuggestions, 300), + [fetchValueSuggestions], + ); + const handleUpdate = useCallback((viewUpdate: { view: EditorView }): void => { if (!isMountedRef.current) return; @@ -465,15 +470,18 @@ function QuerySearch({ const handleBlur = (): void => { handleQueryValidation(query); setIsFocused(false); - if (editorRef.current) { - closeCompletion(editorRef.current); - } }; useEffect(() => { if (query) { handleQueryValidation(query); } + + return (): void => { + if (debouncedFetchValueSuggestions) { + debouncedFetchValueSuggestions.cancel(); + } + }; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); @@ -656,27 +664,38 @@ function QuerySearch({ return null; } - const searchText = word?.text.toLowerCase().trim() ?? ''; + let searchText = ''; + + if ( + queryContext.currentPair && + queryContext.currentPair.valuesPosition && + queryContext.currentPair.valueList + ) { + const { valuesPosition, valueList } = queryContext.currentPair; + const idx = getCurrentValueIndexAtCursor(valuesPosition, cursorPos.ch); + searchText = isNull(idx) + ? '' + : unquote(valueList[idx]).toLowerCase().trim(); + } + options = (valueSuggestions || []).filter((option) => option.label.toLowerCase().includes(searchText), ); if ( keyName && - ((options.length === 0 && + (((options.length === 0 || searchText === '') && (!isCompleteValuesList || lastValueRef.current !== searchText) && !isFetchingCompleteValuesList) || keyName !== activeKey || isLoadingSuggestions) && !(isLoadingSuggestions && lastKeyRef.current === keyName) ) { - setTimeout(() => { - fetchValueSuggestions({ - key: keyName, - searchText, - fetchingComplete: true, - }); - }, 300); + debouncedFetchValueSuggestions({ + key: keyName, + searchText, + fetchingComplete: true, + }); } // For values in bracket list, just add quotes without enclosing in brackets @@ -846,7 +865,11 @@ function QuerySearch({ if (!keyName) { return null; } - const searchText = word?.text.toLowerCase().trim() ?? ''; + let searchText = ''; + + if (queryContext.currentPair && queryContext.currentPair.value) { + searchText = unquote(queryContext.currentPair.value).toLowerCase().trim(); + } options = (valueSuggestions || []).filter((option) => option.label.toLowerCase().includes(searchText), @@ -855,7 +878,7 @@ function QuerySearch({ // Trigger fetch only if needed if ( keyName && - ((options.length === 0 && + (((options.length === 0 || searchText === '') && (!isCompleteValuesList || lastValueRef.current !== searchText) && !isFetchingCompleteValuesList) || keyName !== activeKey || @@ -863,13 +886,11 @@ function QuerySearch({ !(isLoadingSuggestions && lastKeyRef.current === keyName) ) { // eslint-disable-next-line sonarjs/no-identical-functions - setTimeout(() => { - fetchValueSuggestions({ - key: keyName, - searchText, - fetchingComplete: true, - }); - }, 300); + debouncedFetchValueSuggestions({ + key: keyName, + searchText, + fetchingComplete: true, + }); } // Process options to add appropriate formatting when selected @@ -1018,22 +1039,6 @@ function QuerySearch({ } } - // // If no specific context is detected, provide general suggestions - // options = [ - // ...(keySuggestions || []), - // { label: 'AND', type: 'conjunction', boost: -10 }, - // { label: 'OR', type: 'conjunction', boost: -10 }, - // { label: '(', type: 'parenthesis', info: 'Open group', boost: -20 }, - // ]; - - // // Add space after selection for general context - // const optionsWithSpace = addSpaceToOptions(options); - - // return { - // from: word?.from ?? 0, - // options: optionsWithSpace, - // }; - // Don't show anything if no context detected return { from: word?.from ?? 0, @@ -1043,8 +1048,12 @@ function QuerySearch({ // Effect to handle focus state and trigger suggestions useEffect(() => { - if (isFocused && editorRef.current) { - startCompletion(editorRef.current); + if (editorRef.current) { + if (!isFocused) { + closeCompletion(editorRef.current); + } else { + startCompletion(editorRef.current); + } } }, [isFocused]); @@ -1174,9 +1183,6 @@ function QuerySearch({ }} onFocus={(): void => { setIsFocused(true); - if (editorRef.current) { - startCompletion(editorRef.current); - } }} onBlur={handleBlur} /> diff --git a/frontend/src/components/QueryBuilderV2/utils.ts b/frontend/src/components/QueryBuilderV2/utils.ts index 51dcf50e4eae..2615fbe0bdc6 100644 --- a/frontend/src/components/QueryBuilderV2/utils.ts +++ b/frontend/src/components/QueryBuilderV2/utils.ts @@ -20,6 +20,7 @@ import { import { EQueryType } from 'types/common/dashboard'; import { DataSource } from 'types/common/queryBuilder'; import { extractQueryPairs } from 'utils/queryContextUtils'; +import { unquote } from 'utils/stringUtils'; import { v4 as uuid } from 'uuid'; /** @@ -95,21 +96,6 @@ export const convertFiltersToExpression = ( }; }; -function unquote(str: string): string { - if (typeof str !== 'string') return str; - - const startsWithQuote = str.startsWith('"') || str.startsWith("'"); - const endsWithSameQuote = - (str.endsWith('"') && str[0] === '"') || - (str.endsWith("'") && str[0] === "'"); - - if (startsWithQuote && endsWithSameQuote && str.length >= 2) { - return str.slice(1, -1); - } - - return str; -} - const formatValuesForFilter = (value: string | string[]): string | string[] => { if (Array.isArray(value)) { return value.map((v) => (typeof v === 'string' ? unquote(v) : String(v))); @@ -398,7 +384,7 @@ export const removeKeysFromExpression = ( currentQueryPair.position.operatorEnd ?? currentQueryPair.position.keyEnd; // Get the part of the expression that comes after the current query pair - const expressionAfterPair = `${expression.slice(queryPairEnd + 1)}`; + const expressionAfterPair = `${updatedExpression.slice(queryPairEnd + 1)}`; // Match optional spaces and an optional conjunction (AND/OR), case-insensitive const conjunctionOrSpacesRegex = /^(\s*((AND|OR)\s+)?)/i; const match = expressionAfterPair.match(conjunctionOrSpacesRegex); @@ -407,10 +393,10 @@ export const removeKeysFromExpression = ( queryPairEnd += match[0].length; } // Remove the full query pair (including any conjunction/whitespace) from the expression - updatedExpression = `${expression.slice( + updatedExpression = `${updatedExpression.slice( 0, queryPairStart, - )}${expression.slice(queryPairEnd + 1)}`.trim(); + )}${updatedExpression.slice(queryPairEnd + 1)}`.trim(); } } }); diff --git a/frontend/src/components/QuickFilters/QuickFilters.tsx b/frontend/src/components/QuickFilters/QuickFilters.tsx index 01b88c475160..c36efdfa883e 100644 --- a/frontend/src/components/QuickFilters/QuickFilters.tsx +++ b/frontend/src/components/QuickFilters/QuickFilters.tsx @@ -90,6 +90,10 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element { ...currentQuery.builder, queryData: currentQuery.builder.queryData.map((item, idx) => ({ ...item, + filter: { + ...item.filter, + expression: '', + }, filters: { ...item.filters, items: idx === lastUsedQuery ? [] : [...(item.filters?.items || [])], diff --git a/frontend/src/providers/QueryBuilder.tsx b/frontend/src/providers/QueryBuilder.tsx index 6be3a28deaab..66408ff57a27 100644 --- a/frontend/src/providers/QueryBuilder.tsx +++ b/frontend/src/providers/QueryBuilder.tsx @@ -865,6 +865,13 @@ export function QueryBuilderProvider({ ...currentQueryData.builder, queryData: currentQueryData.builder.queryData.map((item) => ({ ...item, + filter: { + ...item.filter, + expression: + item.filter?.expression.trim() === '' + ? '' + : item.filter?.expression ?? '', + }, filters: { items: [], op: 'AND', diff --git a/frontend/src/utils/stringUtils.ts b/frontend/src/utils/stringUtils.ts new file mode 100644 index 000000000000..f2e90bd23382 --- /dev/null +++ b/frontend/src/utils/stringUtils.ts @@ -0,0 +1,13 @@ +export function unquote(str: string): string { + if (typeof str !== 'string') return str; + + const trimmed = str.trim(); + const firstChar = trimmed[0]; + const lastChar = trimmed[trimmed.length - 1]; + + if ((firstChar === '"' || firstChar === "'") && firstChar === lastChar) { + return trimmed.slice(1, -1); + } + + return trimmed; +}