mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-08-11 12:58:59 +08:00
feat: Search bar for Query Builder (#2517)
This commit is contained in:
parent
2c206e8bf4
commit
23081996c4
63
frontend/src/api/queryBuilder/getAttributesKeysValues.ts
Normal file
63
frontend/src/api/queryBuilder/getAttributesKeysValues.ts
Normal 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);
|
||||||
|
}
|
||||||
|
};
|
@ -61,3 +61,78 @@ export const operatorsByTypes: Record<LocalDataType, string[]> = {
|
|||||||
number: Object.values(NumberOperators),
|
number: Object.values(NumberOperators),
|
||||||
bool: Object.values(BoolOperators),
|
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,
|
||||||
|
],
|
||||||
|
};
|
||||||
|
@ -11,12 +11,14 @@ export const Container = styled.div`
|
|||||||
export const RightContainerWrapper = styled(Col)`
|
export const RightContainerWrapper = styled(Col)`
|
||||||
&&& {
|
&&& {
|
||||||
min-width: 200px;
|
min-width: 200px;
|
||||||
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const LeftContainerWrapper = styled(Col)`
|
export const LeftContainerWrapper = styled(Col)`
|
||||||
&&& {
|
&&& {
|
||||||
margin-right: 1rem;
|
margin-right: 1rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
max-width: 70%;
|
max-width: 70%;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
@ -7,7 +7,7 @@ export const StyledButton = styled(Button)<{ $isAvailableToDisable: boolean }>`
|
|||||||
padding: ${(props): string =>
|
padding: ${(props): string =>
|
||||||
props.$isAvailableToDisable ? '0.43rem' : '0.43rem 0.68rem'};
|
props.$isAvailableToDisable ? '0.43rem' : '0.43rem 0.68rem'};
|
||||||
border-radius: 0.375rem;
|
border-radius: 0.375rem;
|
||||||
margin-right: 0.1rem;
|
margin-right: 0.5rem;
|
||||||
pointer-events: ${(props): string =>
|
pointer-events: ${(props): string =>
|
||||||
props.$isAvailableToDisable ? 'default' : 'none'};
|
props.$isAvailableToDisable ? 'default' : 'none'};
|
||||||
`;
|
`;
|
||||||
|
@ -2,10 +2,10 @@ import { Col, Input, Row } from 'antd';
|
|||||||
// ** Constants
|
// ** Constants
|
||||||
import {
|
import {
|
||||||
initialAggregateAttribute,
|
initialAggregateAttribute,
|
||||||
|
initialQueryBuilderFormValues,
|
||||||
mapOfFilters,
|
mapOfFilters,
|
||||||
mapOfOperators,
|
mapOfOperators,
|
||||||
} from 'constants/queryBuilder';
|
} from 'constants/queryBuilder';
|
||||||
import { initialQueryBuilderFormValues } from 'constants/queryBuilder';
|
|
||||||
// ** Components
|
// ** Components
|
||||||
import {
|
import {
|
||||||
AdditionalFiltersToggler,
|
AdditionalFiltersToggler,
|
||||||
@ -19,7 +19,7 @@ import {
|
|||||||
OperatorsSelect,
|
OperatorsSelect,
|
||||||
ReduceToFilter,
|
ReduceToFilter,
|
||||||
} from 'container/QueryBuilder/filters';
|
} from 'container/QueryBuilder/filters';
|
||||||
// Context
|
import QueryBuilderSearch from 'container/QueryBuilder/filters/QueryBuilderSearch';
|
||||||
import { useQueryBuilder } from 'hooks/useQueryBuilder';
|
import { useQueryBuilder } from 'hooks/useQueryBuilder';
|
||||||
import { findDataTypeOfOperator } from 'lib/query/findDataTypeOfOperator';
|
import { findDataTypeOfOperator } from 'lib/query/findDataTypeOfOperator';
|
||||||
// ** Hooks
|
// ** Hooks
|
||||||
@ -186,12 +186,17 @@ export const Query = memo(function Query({
|
|||||||
removeEntityByIndex('queryData', index);
|
removeEntityByIndex('queryData', index);
|
||||||
}, [removeEntityByIndex, index]);
|
}, [removeEntityByIndex, index]);
|
||||||
|
|
||||||
|
const isMatricsDataSource = useMemo(
|
||||||
|
() => query.dataSource === DataSource.METRICS,
|
||||||
|
[query.dataSource],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledRow gutter={[0, 15]}>
|
<StyledRow gutter={[0, 15]}>
|
||||||
<StyledDeleteEntity onClick={handleDeleteQuery} />
|
<StyledDeleteEntity onClick={handleDeleteQuery} />
|
||||||
<Col span={24}>
|
<Col span={24}>
|
||||||
<Row wrap={false} align="middle">
|
<Row align="middle" justify="space-between">
|
||||||
<Col span={24}>
|
<Col>
|
||||||
<ListMarker
|
<ListMarker
|
||||||
isDisabled={query.disabled}
|
isDisabled={query.disabled}
|
||||||
toggleDisabled={handleToggleDisableQuery}
|
toggleDisabled={handleToggleDisableQuery}
|
||||||
@ -203,11 +208,15 @@ export const Query = memo(function Query({
|
|||||||
<DataSourceDropdown
|
<DataSourceDropdown
|
||||||
onChange={handleChangeDataSource}
|
onChange={handleChangeDataSource}
|
||||||
value={query.dataSource}
|
value={query.dataSource}
|
||||||
|
style={{ marginRight: '0.5rem' }}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<FilterLabel label={transformToUpperCase(query.dataSource)} />
|
<FilterLabel label={transformToUpperCase(query.dataSource)} />
|
||||||
)}
|
)}
|
||||||
{/* TODO: here will be search */}
|
{isMatricsDataSource && <FilterLabel label="WHERE" />}
|
||||||
|
</Col>
|
||||||
|
<Col span={isMatricsDataSource ? 17 : 20}>
|
||||||
|
<QueryBuilderSearch query={query} />
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
</Col>
|
</Col>
|
||||||
|
@ -9,6 +9,7 @@ import { useQuery } from 'react-query';
|
|||||||
import { SelectOption } from 'types/common/select';
|
import { SelectOption } from 'types/common/select';
|
||||||
import { transformToUpperCase } from 'utils/transformToUpperCase';
|
import { transformToUpperCase } from 'utils/transformToUpperCase';
|
||||||
|
|
||||||
|
import { selectStyle } from '../QueryBuilderSearch/config';
|
||||||
// ** Types
|
// ** Types
|
||||||
import { AgregatorFilterProps } from './AggregatorFilter.intefaces';
|
import { AgregatorFilterProps } from './AggregatorFilter.intefaces';
|
||||||
|
|
||||||
@ -27,16 +28,15 @@ export const AggregatorFilter = memo(function AggregatorFilter({
|
|||||||
],
|
],
|
||||||
async () =>
|
async () =>
|
||||||
getAggregateAttribute({
|
getAggregateAttribute({
|
||||||
|
searchText,
|
||||||
aggregateOperator: query.aggregateOperator,
|
aggregateOperator: query.aggregateOperator,
|
||||||
dataSource: query.dataSource,
|
dataSource: query.dataSource,
|
||||||
searchText,
|
|
||||||
}),
|
}),
|
||||||
{ enabled: !!query.aggregateOperator && !!query.dataSource },
|
{ enabled: !!query.aggregateOperator && !!query.dataSource },
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleSearchAttribute = (searchText: string): void => {
|
const handleSearchAttribute = (searchText: string): void =>
|
||||||
setSearchText(searchText);
|
setSearchText(searchText);
|
||||||
};
|
|
||||||
|
|
||||||
const optionsData: SelectOption<string, string>[] =
|
const optionsData: SelectOption<string, string>[] =
|
||||||
data?.payload?.attributeKeys?.map((item) => ({
|
data?.payload?.attributeKeys?.map((item) => ({
|
||||||
@ -70,7 +70,7 @@ export const AggregatorFilter = memo(function AggregatorFilter({
|
|||||||
<AutoComplete
|
<AutoComplete
|
||||||
showSearch
|
showSearch
|
||||||
placeholder={`${transformToUpperCase(query.dataSource)} name`}
|
placeholder={`${transformToUpperCase(query.dataSource)} name`}
|
||||||
style={{ width: '100%' }}
|
style={selectStyle}
|
||||||
showArrow={false}
|
showArrow={false}
|
||||||
filterOption={false}
|
filterOption={false}
|
||||||
onSearch={handleSearchAttribute}
|
onSearch={handleSearchAttribute}
|
||||||
|
@ -0,0 +1 @@
|
|||||||
|
export const selectStyle = { width: '100%' };
|
@ -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;
|
@ -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;
|
||||||
|
`;
|
@ -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);
|
||||||
|
}
|
16
frontend/src/container/QueryBuilder/type.ts
Normal file
16
frontend/src/container/QueryBuilder/type.ts
Normal 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;
|
||||||
|
};
|
131
frontend/src/hooks/queryBuilder/useAutoComplete.ts
Normal file
131
frontend/src/hooks/queryBuilder/useAutoComplete.ts
Normal 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,
|
||||||
|
};
|
||||||
|
};
|
106
frontend/src/hooks/queryBuilder/useFetchKeysAndValues.ts
Normal file
106
frontend/src/hooks/queryBuilder/useFetchKeysAndValues.ts
Normal 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,
|
||||||
|
};
|
||||||
|
};
|
22
frontend/src/hooks/queryBuilder/useIsValidTag.ts
Normal file
22
frontend/src/hooks/queryBuilder/useIsValidTag.ts
Normal 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,
|
||||||
|
]);
|
27
frontend/src/hooks/queryBuilder/useOperatorType.ts
Normal file
27
frontend/src/hooks/queryBuilder/useOperatorType.ts
Normal 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';
|
20
frontend/src/hooks/queryBuilder/useOperators.ts
Normal file
20
frontend/src/hooks/queryBuilder/useOperators.ts
Normal 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]);
|
78
frontend/src/hooks/queryBuilder/useOptions.ts
Normal file
78
frontend/src/hooks/queryBuilder/useOptions.ts
Normal 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],
|
||||||
|
);
|
||||||
|
};
|
@ -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];
|
||||||
|
};
|
53
frontend/src/hooks/queryBuilder/useTag.ts
Normal file
53
frontend/src/hooks/queryBuilder/useTag.ts
Normal 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 };
|
||||||
|
};
|
35
frontend/src/hooks/queryBuilder/useTagValidation.ts
Normal file
35
frontend/src/hooks/queryBuilder/useTagValidation.ts
Normal 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 };
|
||||||
|
};
|
@ -1,7 +1,9 @@
|
|||||||
// ** Helpers
|
// ** Helpers
|
||||||
// ** Constants
|
// ** Constants
|
||||||
import { initialQueryBuilderFormValues } from 'constants/queryBuilder';
|
import {
|
||||||
import { mapOfOperators } from 'constants/queryBuilder';
|
initialQueryBuilderFormValues,
|
||||||
|
mapOfOperators,
|
||||||
|
} from 'constants/queryBuilder';
|
||||||
import {
|
import {
|
||||||
createNewQueryName,
|
createNewQueryName,
|
||||||
MAX_QUERIES,
|
MAX_QUERIES,
|
||||||
|
4
frontend/src/utils/checkStringEndsWithSpace.ts
Normal file
4
frontend/src/utils/checkStringEndsWithSpace.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export const checkStringEndsWithSpace = (str: string): boolean => {
|
||||||
|
const endSpace = / $/;
|
||||||
|
return endSpace.test(str);
|
||||||
|
};
|
1
frontend/src/utils/getCountOfSpace.ts
Normal file
1
frontend/src/utils/getCountOfSpace.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export const getCountOfSpace = (s: string): number => s.split(' ').length - 1;
|
9
frontend/src/utils/getSearchParams.ts
Normal file
9
frontend/src/utils/getSearchParams.ts
Normal 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;
|
||||||
|
};
|
12
frontend/src/utils/separateSearchValue.ts
Normal file
12
frontend/src/utils/separateSearchValue.ts
Normal 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(' '))];
|
||||||
|
};
|
Loading…
x
Reference in New Issue
Block a user