feat: Search bar for Query Builder (#2517)

This commit is contained in:
Chintan Sudani 2023-04-15 16:08:17 +05:30 committed by GitHub
parent 2c206e8bf4
commit 23081996c4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 826 additions and 12 deletions

View File

@ -0,0 +1,63 @@
import { ApiV3Instance } from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
export type TagKeyValueProps = {
dataSource: string;
aggregateOperator?: string;
aggregateAttribute?: string;
searchText?: string;
attributeKey?: string;
};
export interface AttributeKeyOptions {
key: string;
type: string;
dataType: 'string' | 'boolean' | 'number';
isColumn: boolean;
}
export const getAttributesKeys = async (
props: TagKeyValueProps,
): Promise<SuccessResponse<AttributeKeyOptions[]> | ErrorResponse> => {
try {
const response = await ApiV3Instance.get(
`/autocomplete/attribute_keys?aggregateOperator=${props.aggregateOperator}&dataSource=${props.dataSource}&aggregateAttribute=${props.aggregateAttribute}&searchText=${props.searchText}`,
);
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data.data.attributeKeys,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};
export interface TagValuePayloadProps {
boolAttributeValues: null | string[];
numberAttributeValues: null | string[];
stringAttributeValues: null | string[];
}
export const getAttributesValues = async (
props: TagKeyValueProps,
): Promise<SuccessResponse<TagValuePayloadProps> | ErrorResponse> => {
try {
const response = await ApiV3Instance.get(
`/autocomplete/attribute_values?aggregateOperator=${props.aggregateOperator}&dataSource=${props.dataSource}&aggregateAttribute=${props.aggregateAttribute}&searchText=${props.searchText}&attributeKey=${props.attributeKey}`,
);
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};

View File

@ -61,3 +61,78 @@ export const operatorsByTypes: Record<LocalDataType, string[]> = {
number: Object.values(NumberOperators),
bool: Object.values(BoolOperators),
};
export type IQueryBuilderState = 'search';
export const QUERY_BUILDER_SEARCH_VALUES = {
MULTIPLY: 'MULTIPLY_VALUE',
SINGLE: 'SINGLE_VALUE',
NON: 'NON_VALUE',
NOT_VALID: 'NOT_VALID',
};
export const OPERATORS = {
IN: 'IN',
NIN: 'NOT_IN',
LIKE: 'LIKE',
NLIKE: 'NOT_LIKE',
EQUALS: '=',
NOT_EQUALS: '!=',
EXISTS: 'EXISTS',
NOT_EXISTS: 'NOT_EXISTS',
CONTAINS: 'CONTAINS',
NOT_CONTAINS: 'NOT_CONTAINS',
GTE: '>=',
GT: '>',
LTE: '<=',
LT: '<',
};
export const QUERY_BUILDER_OPERATORS_BY_TYPES = {
string: [
OPERATORS.EQUALS,
OPERATORS.NOT_EQUALS,
OPERATORS.IN,
OPERATORS.NIN,
OPERATORS.LIKE,
OPERATORS.NLIKE,
OPERATORS.CONTAINS,
OPERATORS.NOT_CONTAINS,
OPERATORS.EXISTS,
OPERATORS.NOT_EXISTS,
],
number: [
OPERATORS.EQUALS,
OPERATORS.NOT_EQUALS,
OPERATORS.IN,
OPERATORS.NIN,
OPERATORS.EXISTS,
OPERATORS.NOT_EXISTS,
OPERATORS.GTE,
OPERATORS.GT,
OPERATORS.LTE,
OPERATORS.LT,
],
boolean: [
OPERATORS.EQUALS,
OPERATORS.NOT_EQUALS,
OPERATORS.EXISTS,
OPERATORS.NOT_EXISTS,
],
universal: [
OPERATORS.EQUALS,
OPERATORS.NOT_EQUALS,
OPERATORS.IN,
OPERATORS.NIN,
OPERATORS.EXISTS,
OPERATORS.NOT_EXISTS,
OPERATORS.LIKE,
OPERATORS.NLIKE,
OPERATORS.GTE,
OPERATORS.GT,
OPERATORS.LTE,
OPERATORS.LT,
OPERATORS.CONTAINS,
OPERATORS.NOT_CONTAINS,
],
};

View File

@ -11,12 +11,14 @@ export const Container = styled.div`
export const RightContainerWrapper = styled(Col)`
&&& {
min-width: 200px;
margin-bottom: 1rem;
}
`;
export const LeftContainerWrapper = styled(Col)`
&&& {
margin-right: 1rem;
margin-bottom: 1rem;
max-width: 70%;
}
`;

View File

@ -7,7 +7,7 @@ export const StyledButton = styled(Button)<{ $isAvailableToDisable: boolean }>`
padding: ${(props): string =>
props.$isAvailableToDisable ? '0.43rem' : '0.43rem 0.68rem'};
border-radius: 0.375rem;
margin-right: 0.1rem;
margin-right: 0.5rem;
pointer-events: ${(props): string =>
props.$isAvailableToDisable ? 'default' : 'none'};
`;

View File

@ -2,10 +2,10 @@ import { Col, Input, Row } from 'antd';
// ** Constants
import {
initialAggregateAttribute,
initialQueryBuilderFormValues,
mapOfFilters,
mapOfOperators,
} from 'constants/queryBuilder';
import { initialQueryBuilderFormValues } from 'constants/queryBuilder';
// ** Components
import {
AdditionalFiltersToggler,
@ -19,7 +19,7 @@ import {
OperatorsSelect,
ReduceToFilter,
} from 'container/QueryBuilder/filters';
// Context
import QueryBuilderSearch from 'container/QueryBuilder/filters/QueryBuilderSearch';
import { useQueryBuilder } from 'hooks/useQueryBuilder';
import { findDataTypeOfOperator } from 'lib/query/findDataTypeOfOperator';
// ** Hooks
@ -186,12 +186,17 @@ export const Query = memo(function Query({
removeEntityByIndex('queryData', index);
}, [removeEntityByIndex, index]);
const isMatricsDataSource = useMemo(
() => query.dataSource === DataSource.METRICS,
[query.dataSource],
);
return (
<StyledRow gutter={[0, 15]}>
<StyledDeleteEntity onClick={handleDeleteQuery} />
<Col span={24}>
<Row wrap={false} align="middle">
<Col span={24}>
<Row align="middle" justify="space-between">
<Col>
<ListMarker
isDisabled={query.disabled}
toggleDisabled={handleToggleDisableQuery}
@ -203,11 +208,15 @@ export const Query = memo(function Query({
<DataSourceDropdown
onChange={handleChangeDataSource}
value={query.dataSource}
style={{ marginRight: '0.5rem' }}
/>
) : (
<FilterLabel label={transformToUpperCase(query.dataSource)} />
)}
{/* TODO: here will be search */}
{isMatricsDataSource && <FilterLabel label="WHERE" />}
</Col>
<Col span={isMatricsDataSource ? 17 : 20}>
<QueryBuilderSearch query={query} />
</Col>
</Row>
</Col>

View File

@ -9,6 +9,7 @@ import { useQuery } from 'react-query';
import { SelectOption } from 'types/common/select';
import { transformToUpperCase } from 'utils/transformToUpperCase';
import { selectStyle } from '../QueryBuilderSearch/config';
// ** Types
import { AgregatorFilterProps } from './AggregatorFilter.intefaces';
@ -27,16 +28,15 @@ export const AggregatorFilter = memo(function AggregatorFilter({
],
async () =>
getAggregateAttribute({
searchText,
aggregateOperator: query.aggregateOperator,
dataSource: query.dataSource,
searchText,
}),
{ enabled: !!query.aggregateOperator && !!query.dataSource },
);
const handleSearchAttribute = (searchText: string): void => {
const handleSearchAttribute = (searchText: string): void =>
setSearchText(searchText);
};
const optionsData: SelectOption<string, string>[] =
data?.payload?.attributeKeys?.map((item) => ({
@ -70,7 +70,7 @@ export const AggregatorFilter = memo(function AggregatorFilter({
<AutoComplete
showSearch
placeholder={`${transformToUpperCase(query.dataSource)} name`}
style={{ width: '100%' }}
style={selectStyle}
showArrow={false}
filterOption={false}
onSearch={handleSearchAttribute}

View File

@ -0,0 +1 @@
export const selectStyle = { width: '100%' };

View File

@ -0,0 +1,96 @@
import { Select, Spin, Tag, Tooltip } from 'antd';
import { useAutoComplete } from 'hooks/queryBuilder/useAutoComplete';
import React from 'react';
import { IBuilderQueryForm } from 'types/api/queryBuilder/queryBuilderData';
import { selectStyle } from './config';
import { StyledCheckOutlined, TypographyText } from './style';
import { isInNotInOperator } from './utils';
function QueryBuilderSearch({ query }: QueryBuilderSearchProps): JSX.Element {
const {
handleClearTag,
handleKeyDown,
handleSearch,
handleSelect,
tags,
options,
searchValue,
isMulti,
isFetching,
} = useAutoComplete(query);
const onTagRender = ({
value,
closable,
onClose,
}: CustomTagProps): React.ReactElement => {
const isInNin = isInNotInOperator(value);
const onCloseHandler = (): void => {
onClose();
handleSearch('');
};
return (
<Tag closable={closable} onClose={onCloseHandler}>
<Tooltip title={value}>
<TypographyText ellipsis isInNin={isInNin}>
{value}
</TypographyText>
</Tooltip>
</Tag>
);
};
const onChangeHandler = (value: string[]): void => {
if (!isMulti) handleSearch(value[value.length - 1]);
};
const onInputKeyDownHandler = (event: React.KeyboardEvent<Element>): void => {
if (isMulti || event.key === 'Backspace') handleKeyDown(event);
};
return (
<Select
virtual
showSearch
tagRender={onTagRender}
filterOption={!isMulti}
autoClearSearchValue={false}
mode="multiple"
placeholder="Search Filter"
value={tags}
searchValue={searchValue}
disabled={!query.aggregateAttribute.key}
style={selectStyle}
onSearch={handleSearch}
onChange={onChangeHandler}
onSelect={handleSelect}
onDeselect={handleClearTag}
onInputKeyDown={onInputKeyDownHandler}
notFoundContent={isFetching ? <Spin size="small" /> : null}
>
{options?.map((option) => (
<Select.Option key={option.value} value={option.value}>
{option.value}
{option.selected && <StyledCheckOutlined />}
</Select.Option>
))}
</Select>
);
}
interface QueryBuilderSearchProps {
query: IBuilderQueryForm;
}
export interface CustomTagProps {
label: React.ReactNode;
value: string;
disabled: boolean;
onClose: (event?: React.MouseEvent<HTMLElement, MouseEvent>) => void;
closable: boolean;
}
export default QueryBuilderSearch;

View File

@ -0,0 +1,11 @@
import { CheckOutlined } from '@ant-design/icons';
import { Typography } from 'antd';
import styled from 'styled-components';
export const TypographyText = styled(Typography.Text)<{ isInNin: boolean }>`
width: ${({ isInNin }): string => (isInNin ? '10rem' : 'auto')};
`;
export const StyledCheckOutlined = styled(CheckOutlined)`
float: right;
`;

View File

@ -0,0 +1,9 @@
import { OPERATORS } from 'constants/queryBuilder';
export function isInNotInOperator(value: string): boolean {
return value?.includes(OPERATORS.IN || OPERATORS.NIN);
}
export function isExistsNotExistsOperator(value: string): boolean {
return value?.includes(OPERATORS.EXISTS || OPERATORS.NOT_EXISTS);
}

View File

@ -0,0 +1,16 @@
import { IQueryBuilderState } from 'constants/queryBuilder';
export interface InitialStateI {
search: string;
}
export interface ContextValueI {
values: InitialStateI;
onChangeHandler: (type: IQueryBuilderState) => (value: string) => void;
onSubmitHandler: VoidFunction;
}
export type Option = {
value: string;
selected?: boolean;
};

View File

@ -0,0 +1,131 @@
import { isExistsNotExistsOperator } from 'container/QueryBuilder/filters/QueryBuilderSearch/utils';
import { Option } from 'container/QueryBuilder/type';
import { useCallback, useState } from 'react';
import { IBuilderQueryForm } from 'types/api/queryBuilder/queryBuilderData';
import { checkStringEndsWithSpace } from 'utils/checkStringEndsWithSpace';
import { useFetchKeysAndValues } from './useFetchKeysAndValues';
import { useOptions } from './useOptions';
import { useSetCurrentKeyAndOperator } from './useSetCurrentKeyAndOperator';
import { useTag } from './useTag';
import { useTagValidation } from './useTagValidation';
interface IAutoComplete {
handleSearch: (value: string) => void;
handleClearTag: (value: string) => void;
handleSelect: (value: string) => void;
handleKeyDown: (event: React.KeyboardEvent) => void;
options: Option[];
tags: string[];
searchValue: string;
isMulti: boolean;
isFetching: boolean;
}
export const useAutoComplete = (query: IBuilderQueryForm): IAutoComplete => {
const [searchValue, setSearchValue] = useState<string>('');
const handleSearch = (value: string): void => setSearchValue(value);
const { keys, results, isFetching } = useFetchKeysAndValues(
searchValue,
query,
);
const [key, operator, result] = useSetCurrentKeyAndOperator(searchValue, keys);
const {
isValidTag,
isExist,
isValidOperator,
isMulti,
isFreeText,
} = useTagValidation(searchValue, operator, result);
const { handleAddTag, handleClearTag, tags } = useTag(
key,
isValidTag,
isFreeText,
handleSearch,
);
const handleSelect = useCallback(
(value: string): void => {
if (isMulti) {
setSearchValue((prev: string) => {
if (prev.includes(value)) {
return prev.replace(` ${value}`, '');
}
return checkStringEndsWithSpace(prev)
? `${prev} ${value}`
: `${prev}, ${value}`;
});
}
if (!isMulti && isValidTag && !isExistsNotExistsOperator(value)) {
handleAddTag(value);
}
if (!isMulti && isExistsNotExistsOperator(value)) {
handleAddTag(value);
}
},
[handleAddTag, isMulti, isValidTag],
);
const handleKeyDown = useCallback(
(event: React.KeyboardEvent): void => {
if (
event.key === ' ' &&
(searchValue.endsWith(' ') || searchValue.length === 0)
) {
event.preventDefault();
}
if (event.key === 'Enter' && searchValue && isValidTag) {
if (isMulti || isFreeText) {
event.stopPropagation();
}
event.preventDefault();
handleAddTag(searchValue);
}
if (event.key === 'Backspace' && !searchValue) {
event.stopPropagation();
const last = tags[tags.length - 1];
handleClearTag(last);
}
},
[
handleAddTag,
handleClearTag,
isFreeText,
isMulti,
isValidTag,
searchValue,
tags,
],
);
const options = useOptions(
key,
keys,
operator,
searchValue,
isMulti,
isValidOperator,
isExist,
results,
result,
);
return {
handleSearch,
handleClearTag,
handleSelect,
handleKeyDown,
options,
tags,
searchValue,
isMulti,
isFetching,
};
};

View File

@ -0,0 +1,106 @@
import {
AttributeKeyOptions,
getAttributesKeys,
getAttributesValues,
} from 'api/queryBuilder/getAttributesKeysValues';
import { useEffect, useRef, useState } from 'react';
import { useQuery } from 'react-query';
import { useDebounce } from 'react-use';
import { IBuilderQueryForm } from 'types/api/queryBuilder/queryBuilderData';
import { separateSearchValue } from 'utils/separateSearchValue';
type UseFetchKeysAndValuesReturnValues = {
keys: AttributeKeyOptions[];
results: string[];
isFetching: boolean;
};
/**
* Custom hook to fetch attribute keys and values from an API
* @param searchValue - the search query value
* @param query - an object containing data for the query
* @returns an object containing the fetched attribute keys, results, and the status of the fetch
*/
export const useFetchKeysAndValues = (
searchValue: string,
query: IBuilderQueryForm,
): UseFetchKeysAndValuesReturnValues => {
const [keys, setKeys] = useState<AttributeKeyOptions[]>([]);
const [results, setResults] = useState<string[]>([]);
const { data, isFetching, status } = useQuery(
[
'GET_ATTRIBUTE_KEY',
searchValue,
query.dataSource,
query.aggregateOperator,
query.aggregateAttribute.key,
],
async () =>
getAttributesKeys({
searchText: searchValue,
dataSource: query.dataSource,
aggregateOperator: query.aggregateOperator,
aggregateAttribute: query.aggregateAttribute.key,
}),
{ enabled: !!query.aggregateOperator && !!query.dataSource },
);
/**
* Fetches the options to be displayed based on the selected value
* @param value - the selected value
* @param query - an object containing data for the query
*/
const handleFetchOption = async (
value: string,
query: IBuilderQueryForm,
): Promise<void> => {
if (value) {
// separate the search value into the attribute key and the operator
const [tKey, operator] = separateSearchValue(value);
setResults([]);
if (tKey && operator) {
const { payload } = await getAttributesValues({
searchText: searchValue,
dataSource: query.dataSource,
aggregateOperator: query.aggregateOperator,
aggregateAttribute: query.aggregateAttribute.key,
attributeKey: tKey,
});
if (payload) {
const values = Object.values(payload).find((el) => !!el);
if (values) {
setResults(values);
} else {
setResults([]);
}
}
}
}
};
// creates a ref to the fetch function so that it doesn't change on every render
const clearFetcher = useRef(handleFetchOption).current;
// debounces the fetch function to avoid excessive API calls
useDebounce(() => clearFetcher(searchValue, query), 500, [
clearFetcher,
searchValue,
query,
]);
// update the fetched keys when the fetch status changes
useEffect(() => {
if (status === 'success' && data?.payload) {
setKeys(data?.payload);
} else {
setKeys([]);
}
}, [data?.payload, status]);
return {
keys,
results,
isFetching,
};
};

View File

@ -0,0 +1,22 @@
import { useMemo } from 'react';
import { OperatorType } from './useOperatorType';
const validationMapper: Record<
OperatorType,
(resultLength: number) => boolean
> = {
SINGLE_VALUE: (resultLength: number) => resultLength === 1,
MULTIPLY_VALUE: (resultLength: number) => resultLength >= 1,
NON_VALUE: (resultLength: number) => resultLength === 0,
NOT_VALID: () => false,
};
export const useIsValidTag = (
operatorType: OperatorType,
resultLength: number,
): boolean =>
useMemo(() => validationMapper[operatorType]?.(resultLength), [
operatorType,
resultLength,
]);

View File

@ -0,0 +1,27 @@
import { OPERATORS } from 'constants/queryBuilder';
export type OperatorType =
| 'SINGLE_VALUE'
| 'MULTIPLY_VALUE'
| 'NON_VALUE'
| 'NOT_VALID';
const operatorTypeMapper: Record<string, OperatorType> = {
[OPERATORS.IN]: 'MULTIPLY_VALUE',
[OPERATORS.NIN]: 'MULTIPLY_VALUE',
[OPERATORS.EXISTS]: 'NON_VALUE',
[OPERATORS.NOT_EXISTS]: 'NON_VALUE',
[OPERATORS.LTE]: 'SINGLE_VALUE',
[OPERATORS.LT]: 'SINGLE_VALUE',
[OPERATORS.GTE]: 'SINGLE_VALUE',
[OPERATORS.GT]: 'SINGLE_VALUE',
[OPERATORS.LIKE]: 'SINGLE_VALUE',
[OPERATORS.NLIKE]: 'SINGLE_VALUE',
[OPERATORS.CONTAINS]: 'SINGLE_VALUE',
[OPERATORS.NOT_CONTAINS]: 'SINGLE_VALUE',
[OPERATORS.EQUALS]: 'SINGLE_VALUE',
[OPERATORS.NOT_EQUALS]: 'SINGLE_VALUE',
};
export const useOperatorType = (operator: string): OperatorType =>
operatorTypeMapper[operator] || 'NOT_VALID';

View File

@ -0,0 +1,20 @@
import { AttributeKeyOptions } from 'api/queryBuilder/getAttributesKeysValues';
import { QUERY_BUILDER_OPERATORS_BY_TYPES } from 'constants/queryBuilder';
import { useMemo } from 'react';
type IOperators =
| typeof QUERY_BUILDER_OPERATORS_BY_TYPES.universal
| typeof QUERY_BUILDER_OPERATORS_BY_TYPES.string
| typeof QUERY_BUILDER_OPERATORS_BY_TYPES.boolean
| typeof QUERY_BUILDER_OPERATORS_BY_TYPES.number;
export const useOperators = (
key: string,
keys: AttributeKeyOptions[],
): IOperators =>
useMemo(() => {
const currentKey = keys?.find((el) => el.key === key);
return currentKey
? QUERY_BUILDER_OPERATORS_BY_TYPES[currentKey.dataType]
: QUERY_BUILDER_OPERATORS_BY_TYPES.universal;
}, [keys, key]);

View File

@ -0,0 +1,78 @@
import { AttributeKeyOptions } from 'api/queryBuilder/getAttributesKeysValues';
import { Option } from 'container/QueryBuilder/type';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useOperators } from './useOperators';
export const useOptions = (
key: string,
keys: AttributeKeyOptions[],
operator: string,
searchValue: string,
isMulti: boolean,
isValidOperator: boolean,
isExist: boolean,
results: string[],
result: string[],
): Option[] => {
const [options, setOptions] = useState<Option[]>([]);
const operators = useOperators(key, keys);
const updateOptions = useCallback(() => {
if (!key) {
setOptions(
searchValue
? [{ value: searchValue }, ...keys.map((k) => ({ value: k.key }))]
: keys?.map((k) => ({ value: k.key })),
);
} else if (key && !operator) {
setOptions(
operators.map((o) => ({
value: `${key} ${o}`,
label: `${key} ${o.replace('_', ' ')}`,
})),
);
} else if (key && operator) {
if (isMulti) {
setOptions(results.map((r) => ({ value: `${r}` })));
} else if (isExist) {
setOptions([]);
} else if (isValidOperator) {
const hasAllResults = result.every((val) => results.includes(val));
const values = results.map((r) => ({
value: `${key} ${operator} ${r}`,
}));
const options = hasAllResults
? values
: [{ value: searchValue }, ...values];
setOptions(options);
}
}
}, [
isExist,
isMulti,
isValidOperator,
key,
keys,
operator,
operators,
result,
results,
searchValue,
]);
useEffect(() => {
updateOptions();
}, [updateOptions]);
return useMemo(
() =>
options?.map((option) => {
if (isMulti) {
return { ...option, selected: searchValue.includes(option.value) };
}
return option;
}),
[isMulti, options, searchValue],
);
};

View File

@ -0,0 +1,32 @@
import { AttributeKeyOptions } from 'api/queryBuilder/getAttributesKeysValues';
import { useMemo } from 'react';
import { getCountOfSpace } from 'utils/getCountOfSpace';
import { separateSearchValue } from 'utils/separateSearchValue';
type ICurrentKeyAndOperator = [string, string, string[]];
export const useSetCurrentKeyAndOperator = (
value: string,
keys: AttributeKeyOptions[],
): ICurrentKeyAndOperator => {
const [key, operator, result] = useMemo(() => {
let key = '';
let operator = '';
let result: string[] = [];
if (value) {
const [tKey, tOperator, tResult] = separateSearchValue(value);
const isSuggestKey = keys?.some((el) => el.key === tKey);
if (getCountOfSpace(value) >= 1 || isSuggestKey) {
key = tKey || '';
operator = tOperator || '';
result = tResult.filter((el) => el);
}
}
return [key, operator, result];
}, [value, keys]);
return [key, operator, result];
};

View File

@ -0,0 +1,53 @@
import { isExistsNotExistsOperator } from 'container/QueryBuilder/filters/QueryBuilderSearch/utils';
import { useCallback, useState } from 'react';
type IUseTag = {
handleAddTag: (value: string) => void;
handleClearTag: (value: string) => void;
tags: string[];
};
/**
* A custom React hook for handling tags.
* @param {string} key - A string value to identify tags.
* @param {boolean} isValidTag - A boolean value to indicate whether the tag is valid.
* @param {boolean} isFreeText - A boolean value to indicate whether free text is allowed.
* @param {function} handleSearch - A callback function to handle search.
* @returns {IUseTag} The return object containing handlers and tags.
*/
export const useTag = (
key: string,
isValidTag: boolean,
isFreeText: boolean,
handleSearch: (value: string) => void,
): IUseTag => {
const [tags, setTags] = useState<string[]>([]);
/**
* Adds a new tag to the tag list.
* @param {string} value - The tag value to be added.
*/
const handleAddTag = useCallback(
(value: string): void => {
if (
(value && key && isValidTag) ||
isFreeText ||
isExistsNotExistsOperator(value)
) {
setTags((prevTags) => [...prevTags, value]);
handleSearch('');
}
},
[key, isValidTag, isFreeText, handleSearch],
);
/**
* Removes a tag from the tag list.
* @param {string} value - The tag value to be removed.
*/
const handleClearTag = useCallback((value: string): void => {
setTags((prevTags) => prevTags.filter((v) => v !== value));
}, []);
return { handleAddTag, handleClearTag, tags };
};

View File

@ -0,0 +1,35 @@
import { QUERY_BUILDER_SEARCH_VALUES } from 'constants/queryBuilder';
import { useMemo } from 'react';
import { checkStringEndsWithSpace } from 'utils/checkStringEndsWithSpace';
import { useIsValidTag } from './useIsValidTag';
import { useOperatorType } from './useOperatorType';
type ITagValidation = {
isValidTag: boolean;
isExist: boolean;
isValidOperator: boolean;
isMulti: boolean;
isFreeText: boolean;
};
export const useTagValidation = (
value: string,
operator: string,
result: string[],
): ITagValidation => {
const operatorType = useOperatorType(operator);
const isValidTag = useIsValidTag(operatorType, result.length);
const { isExist, isValidOperator, isMulti, isFreeText } = useMemo(() => {
const isExist = operatorType === QUERY_BUILDER_SEARCH_VALUES.NON;
const isValidOperator =
operatorType !== QUERY_BUILDER_SEARCH_VALUES.NOT_VALID;
const isMulti = operatorType === QUERY_BUILDER_SEARCH_VALUES.MULTIPLY;
const isFreeText = operator === '' && !checkStringEndsWithSpace(value);
return { isExist, isValidOperator, isMulti, isFreeText };
}, [operator, operatorType, value]);
return { isValidTag, isExist, isValidOperator, isMulti, isFreeText };
};

View File

@ -1,7 +1,9 @@
// ** Helpers
// ** Constants
import { initialQueryBuilderFormValues } from 'constants/queryBuilder';
import { mapOfOperators } from 'constants/queryBuilder';
import {
initialQueryBuilderFormValues,
mapOfOperators,
} from 'constants/queryBuilder';
import {
createNewQueryName,
MAX_QUERIES,

View File

@ -0,0 +1,4 @@
export const checkStringEndsWithSpace = (str: string): boolean => {
const endSpace = / $/;
return endSpace.test(str);
};

View File

@ -0,0 +1 @@
export const getCountOfSpace = (s: string): number => s.split(' ').length - 1;

View File

@ -0,0 +1,9 @@
export const getSearchParams = (newParams: {
[key: string]: string;
}): URLSearchParams => {
const params = new URLSearchParams();
Object.entries(newParams).forEach(([key, value]) => {
params.set(key, value);
});
return params;
};

View File

@ -0,0 +1,12 @@
import { OPERATORS } from 'constants/queryBuilder';
export const separateSearchValue = (
value: string,
): [string, string, string[]] => {
const separatedString = value.split(' ');
const [key, operator, ...result] = separatedString;
if (operator === OPERATORS.IN || operator === OPERATORS.NIN) {
return [key, operator, result];
}
return [key, operator, Array(result.join(' '))];
};