mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-12-05 03:54:23 +00:00
Improvements(UI) #9003: Improved explore page advanced search quick filters styling and functionality (#9237)
* Added util functions to use in AdvancedSearchUtils * Replaced ExploreQuickFilter component with SearchDropdown Styling improvements Localization changes * Fixed failing unit tests * Improved filters styling * Advanced search quick filter dropdown styling changes * Fixed failing unit tests * Added unit tests for newly added functions in AdvancedSearchUtils * UI : fix add owner and tier functionality on Translated entity page (#9244) * fix add owner and tier functionality on Translated entity page * get entity info keys from enum * remove unwanted translation keys * minor changes * Removed repetitions in en-us * Added update and close button feature to search dropdown * Stylings updated * Fixed failing unit tests and added tests for additional features * - Handled possible failure case for util functions in AdvancedSearchUtils functions - Added tests for negative scenarios Co-authored-by: Ashish Gupta <ashish@getcollate.io> Co-authored-by: Chirag Madlani <12962843+chirag-madlani@users.noreply.github.com>
This commit is contained in:
parent
c7d0fede2f
commit
930361a49d
@ -0,0 +1,3 @@
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6.00002 8.65912C5.8954 8.65912 5.79067 8.61915 5.71079 8.53927L1.61989 4.44834C1.46004 4.28849 1.46004 4.02964 1.61989 3.86989C1.77974 3.71014 2.03859 3.71004 2.19834 3.86989L6.00002 7.6716L9.80167 3.86989C9.96157 3.71004 10.2204 3.71004 10.3802 3.86989C10.5399 4.02974 10.54 4.28859 10.3802 4.44834L6.28925 8.53927C6.20937 8.61915 6.10464 8.65912 6.00002 8.65912Z" fill="#37352F" stroke="#37352F" stroke-width="0.6"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 532 B |
@ -248,6 +248,20 @@ export const getAdvancedFieldOptions = (
|
||||
});
|
||||
};
|
||||
|
||||
export const getAdvancedFieldDefaultOptions = (
|
||||
index: SearchIndex,
|
||||
field: string
|
||||
) => {
|
||||
const params = { index, field };
|
||||
|
||||
return APIClient.get<SearchResponse<ExploreSearchIndex>>(
|
||||
`/search/aggregate`,
|
||||
{
|
||||
params,
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export const getEntityCount = async (
|
||||
path: string,
|
||||
database?: string
|
||||
|
||||
@ -44,6 +44,7 @@ import { TabSpecificField } from '../../enums/entity.enum';
|
||||
import { SearchIndex } from '../../enums/search.enum';
|
||||
import { Table } from '../../generated/entity/data/table';
|
||||
import { Include } from '../../generated/type/include';
|
||||
import { getDropDownItems } from '../../utils/AdvancedSearchUtils';
|
||||
import {
|
||||
formatNumberWithComma,
|
||||
formTwoDigitNmber,
|
||||
@ -237,36 +238,25 @@ const Explore: React.FC<ExploreProps> = ({
|
||||
};
|
||||
|
||||
const handleAdvanceSearchFilter = (data: ExploreQuickFilterField[]) => {
|
||||
const term = {} as Record<string, unknown>;
|
||||
const terms = [] as Array<Record<string, unknown>>;
|
||||
|
||||
data.forEach((filter) => {
|
||||
if (filter.key) {
|
||||
term[filter.key] = filter.value;
|
||||
}
|
||||
filter.value?.map((val) => {
|
||||
if (filter.key) {
|
||||
terms.push({ term: { [filter.key]: val } });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
onChangeAdvancedSearchQueryFilter(
|
||||
isEmpty(term)
|
||||
isEmpty(terms)
|
||||
? undefined
|
||||
: {
|
||||
query: { bool: { must: [{ term }] } },
|
||||
query: { bool: { must: terms } },
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const handleAdvanceFieldClear = () => {
|
||||
setSelectedQuickFilters([]);
|
||||
};
|
||||
|
||||
const handleAdvanceFieldRemove = (value: string) => {
|
||||
setSelectedQuickFilters((prev) => {
|
||||
const data = prev.filter((p) => p.key !== value);
|
||||
handleAdvanceSearchFilter(data);
|
||||
|
||||
return data;
|
||||
});
|
||||
};
|
||||
|
||||
const handleAdvanceFieldValueSelect = (field: ExploreQuickFilterField) => {
|
||||
setSelectedQuickFilters((pre) => {
|
||||
const data = pre.map((preField) => {
|
||||
@ -283,16 +273,6 @@ const Explore: React.FC<ExploreProps> = ({
|
||||
});
|
||||
};
|
||||
|
||||
const handleAdvancedFieldSelect = (value: string) => {
|
||||
const flag = selectedQuickFilters.some((field) => field.key === value);
|
||||
if (!flag) {
|
||||
setSelectedQuickFilters((pre) => [
|
||||
...pre,
|
||||
{ key: value, value: undefined },
|
||||
]);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const escapeKeyHandler = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') {
|
||||
@ -306,6 +286,14 @@ const Explore: React.FC<ExploreProps> = ({
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const dropdownItems = getDropDownItems(searchIndex);
|
||||
|
||||
setSelectedQuickFilters(
|
||||
dropdownItems.map((item) => ({ ...item, value: undefined }))
|
||||
);
|
||||
}, [searchIndex]);
|
||||
|
||||
return (
|
||||
<PageLayoutV1
|
||||
leftPanel={
|
||||
@ -393,9 +381,6 @@ const Explore: React.FC<ExploreProps> = ({
|
||||
fields={selectedQuickFilters}
|
||||
index={searchIndex}
|
||||
onAdvanceSearch={() => setShowAdvanceSearchModal(true)}
|
||||
onClear={handleAdvanceFieldClear}
|
||||
onFieldRemove={handleAdvanceFieldRemove}
|
||||
onFieldSelect={handleAdvancedFieldSelect}
|
||||
onFieldValueSelect={handleAdvanceFieldValueSelect}
|
||||
/>
|
||||
</Col>
|
||||
|
||||
@ -1,179 +0,0 @@
|
||||
/*
|
||||
* Copyright 2021 Collate
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { act, fireEvent, render } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { SearchIndex } from '../../enums/search.enum';
|
||||
import { ExploreQuickFilterField } from './explore.interface';
|
||||
import AdvancedField from './ExploreQuickFilter';
|
||||
|
||||
const mockData = {
|
||||
'metadata-suggest': [
|
||||
{
|
||||
text: 'clou',
|
||||
offset: 0,
|
||||
length: 4,
|
||||
options: [
|
||||
{
|
||||
text: 'Cloud_Infra',
|
||||
_index: 'team_search_index',
|
||||
_type: '_doc',
|
||||
_id: '267a4dd4-df64-400d-b4d1-3925d6f23885',
|
||||
_score: 10,
|
||||
_source: {
|
||||
suggest: [
|
||||
{
|
||||
input: 'Cloud_Infra',
|
||||
weight: 5,
|
||||
},
|
||||
{
|
||||
input: 'Cloud_Infra',
|
||||
weight: 10,
|
||||
},
|
||||
],
|
||||
deleted: false,
|
||||
// eslint-disable-next-line @typescript-eslint/camelcase
|
||||
team_id: '267a4dd4-df64-400d-b4d1-3925d6f23885',
|
||||
name: 'Cloud_Infra',
|
||||
// eslint-disable-next-line @typescript-eslint/camelcase
|
||||
display_name: 'Cloud_Infra',
|
||||
// eslint-disable-next-line @typescript-eslint/camelcase
|
||||
entityType: 'team',
|
||||
users: [],
|
||||
owns: [],
|
||||
// eslint-disable-next-line @typescript-eslint/camelcase
|
||||
default_roles: [],
|
||||
// eslint-disable-next-line @typescript-eslint/camelcase
|
||||
last_updated_timestamp: 1654838173854,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
jest.mock('../../axiosAPIs/miscAPI', () => ({
|
||||
getAdvancedFieldOptions: jest
|
||||
.fn()
|
||||
.mockImplementation(() => Promise.resolve()),
|
||||
getUserSuggestions: jest
|
||||
.fn()
|
||||
.mockImplementation(() => Promise.resolve({ data: { suggest: mockData } })),
|
||||
}));
|
||||
|
||||
jest.mock('../../utils/AdvancedSearchUtils', () => ({
|
||||
getItemLabel: jest.fn().mockImplementation(() => 'owner'),
|
||||
getAdvancedField: jest.fn(),
|
||||
}));
|
||||
|
||||
const index = SearchIndex.TABLE;
|
||||
const field = {
|
||||
key: 'owner.name',
|
||||
value: undefined,
|
||||
} as ExploreQuickFilterField;
|
||||
const onFieldRemove = jest.fn();
|
||||
const onFieldValueSelect = jest.fn();
|
||||
|
||||
const mockProps = {
|
||||
index,
|
||||
field,
|
||||
onFieldRemove,
|
||||
onFieldValueSelect,
|
||||
};
|
||||
|
||||
describe('Test AdvancedField Component', () => {
|
||||
it('Should render AdvancedField component', async () => {
|
||||
await act(async () => {
|
||||
const { findByTestId } = render(<AdvancedField {...mockProps} />);
|
||||
|
||||
const label = await findByTestId('field-label');
|
||||
|
||||
expect(label).toBeInTheDocument();
|
||||
|
||||
expect(label).toHaveTextContent('Owner:');
|
||||
|
||||
const searchSelect = await findByTestId('field-select');
|
||||
|
||||
expect(searchSelect).toBeInTheDocument();
|
||||
|
||||
const removeButton = await findByTestId('field-remove-button');
|
||||
|
||||
expect(removeButton).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('Should call remove method on click of remove button', async () => {
|
||||
await act(async () => {
|
||||
const { findByTestId } = render(<AdvancedField {...mockProps} />);
|
||||
|
||||
const label = await findByTestId('field-label');
|
||||
|
||||
expect(label).toBeInTheDocument();
|
||||
|
||||
expect(label).toHaveTextContent('Owner:');
|
||||
|
||||
const searchSelect = await findByTestId('field-select');
|
||||
|
||||
expect(searchSelect).toBeInTheDocument();
|
||||
|
||||
const removeButton = await findByTestId('field-remove-button');
|
||||
|
||||
expect(removeButton).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(removeButton);
|
||||
|
||||
expect(onFieldRemove).toHaveBeenCalledWith(field.key);
|
||||
});
|
||||
});
|
||||
|
||||
it('Should call select method on click of option', async () => {
|
||||
await act(async () => {
|
||||
const { findByTestId, findByRole, findAllByTestId } = render(
|
||||
<AdvancedField {...mockProps} />
|
||||
);
|
||||
|
||||
const label = await findByTestId('field-label');
|
||||
|
||||
expect(label).toBeInTheDocument();
|
||||
|
||||
expect(label).toHaveTextContent('Owner:');
|
||||
|
||||
const searchSelect = await findByTestId('field-select');
|
||||
|
||||
expect(searchSelect).toBeInTheDocument();
|
||||
|
||||
const removeButton = await findByTestId('field-remove-button');
|
||||
|
||||
expect(removeButton).toBeInTheDocument();
|
||||
|
||||
const searchInput = await findByRole('combobox');
|
||||
|
||||
expect(searchInput).toBeInTheDocument();
|
||||
|
||||
fireEvent.change(searchInput, { target: { value: 'cloud' } });
|
||||
|
||||
const fieldOptions = await findAllByTestId('field-option');
|
||||
|
||||
expect(fieldOptions).toHaveLength(
|
||||
mockData['metadata-suggest'][0].options.length
|
||||
);
|
||||
|
||||
fireEvent.click(fieldOptions[0]);
|
||||
|
||||
expect(onFieldValueSelect).toHaveBeenCalledWith({
|
||||
key: 'owner.name',
|
||||
value: 'Cloud_Infra',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -1,187 +0,0 @@
|
||||
/*
|
||||
* Copyright 2021 Collate
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { Select } from 'antd';
|
||||
import { DefaultOptionType } from 'antd/lib/select';
|
||||
import { AxiosError } from 'axios';
|
||||
import { startCase } from 'lodash';
|
||||
import React, { FC, useState } from 'react';
|
||||
import {
|
||||
getAdvancedFieldOptions,
|
||||
getTagSuggestions,
|
||||
getUserSuggestions,
|
||||
} from '../../axiosAPIs/miscAPI';
|
||||
import { MISC_FIELDS } from '../../constants/AdvancedSearch.constants';
|
||||
import {
|
||||
getAdvancedField,
|
||||
getItemLabel,
|
||||
} from '../../utils/AdvancedSearchUtils';
|
||||
import { showErrorToast } from '../../utils/ToastUtils';
|
||||
import {
|
||||
ExploreQuickFilterProps,
|
||||
SearchInputProps,
|
||||
} from '../Explore/explore.interface';
|
||||
|
||||
const SearchInput = ({
|
||||
options,
|
||||
value,
|
||||
handleChange,
|
||||
handleSearch,
|
||||
handleSelect,
|
||||
handleClear,
|
||||
}: SearchInputProps) => {
|
||||
const { Option } = Select;
|
||||
|
||||
const optionsElement = options.map((d) => (
|
||||
<Option data-testid="field-option" key={d.value}>
|
||||
{d.label}
|
||||
</Option>
|
||||
));
|
||||
|
||||
return (
|
||||
<Select
|
||||
allowClear
|
||||
showSearch
|
||||
bordered={false}
|
||||
className="ant-advaced-field-select"
|
||||
data-testid="field-select"
|
||||
defaultActiveFirstOption={false}
|
||||
dropdownClassName="ant-suggestion-dropdown"
|
||||
filterOption={false}
|
||||
notFoundContent={null}
|
||||
placeholder="Search to Select"
|
||||
showArrow={false}
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
onClear={handleClear}
|
||||
onSearch={handleSearch}
|
||||
onSelect={handleSelect}>
|
||||
{optionsElement}
|
||||
</Select>
|
||||
);
|
||||
};
|
||||
|
||||
const ExploreQuickFilter: FC<ExploreQuickFilterProps> = ({
|
||||
field,
|
||||
onFieldRemove,
|
||||
index,
|
||||
onFieldValueSelect,
|
||||
}) => {
|
||||
const advancedField = getAdvancedField(field.key);
|
||||
|
||||
const [options, setOptions] = useState<DefaultOptionType[]>([]);
|
||||
const [value, setValue] = useState<string | undefined>(field.value);
|
||||
|
||||
const fetchOptions = (query: string) => {
|
||||
if (!MISC_FIELDS.includes(field.key)) {
|
||||
getAdvancedFieldOptions(query, index, advancedField)
|
||||
.then((res) => {
|
||||
const suggestOptions =
|
||||
res.data.suggest['metadata-suggest'][0].options ?? [];
|
||||
const uniqueOptions = [
|
||||
...new Set(suggestOptions.map((op) => op.text)),
|
||||
];
|
||||
setOptions(
|
||||
uniqueOptions.map((op: unknown) => ({
|
||||
label: op as string,
|
||||
value: op as string,
|
||||
}))
|
||||
);
|
||||
})
|
||||
.catch((err: AxiosError) => showErrorToast(err));
|
||||
} else {
|
||||
if (field.key === 'tags.tagFQN') {
|
||||
getTagSuggestions(query)
|
||||
.then((res) => {
|
||||
const suggestOptions =
|
||||
res.data.suggest['metadata-suggest'][0].options ?? [];
|
||||
const uniqueOptions = [
|
||||
...new Set(
|
||||
// eslint-disable-next-line
|
||||
suggestOptions.map((op: any) => op._source.fullyQualifiedName)
|
||||
),
|
||||
];
|
||||
setOptions(
|
||||
uniqueOptions.map((op: unknown) => ({
|
||||
label: op as string,
|
||||
value: op as string,
|
||||
}))
|
||||
);
|
||||
})
|
||||
.catch((err: AxiosError) => showErrorToast(err));
|
||||
} else {
|
||||
getUserSuggestions(query)
|
||||
.then((res) => {
|
||||
const suggestOptions =
|
||||
res.data.suggest['metadata-suggest'][0].options ?? [];
|
||||
const uniqueOptions = [
|
||||
// eslint-disable-next-line
|
||||
...new Set(suggestOptions.map((op: any) => op._source.name)),
|
||||
];
|
||||
setOptions(
|
||||
uniqueOptions.map((op: unknown) => ({
|
||||
label: op as string,
|
||||
value: op as string,
|
||||
}))
|
||||
);
|
||||
})
|
||||
.catch((err: AxiosError) => showErrorToast(err));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleSearch = (newValue: string) => {
|
||||
if (newValue) {
|
||||
fetchOptions(newValue);
|
||||
} else {
|
||||
setOptions([]);
|
||||
}
|
||||
};
|
||||
|
||||
const handleChange = (newValue: string) => {
|
||||
setValue(newValue);
|
||||
};
|
||||
|
||||
const handleOnSelect = (newValue: string) => {
|
||||
onFieldValueSelect({ ...field, value: newValue });
|
||||
};
|
||||
|
||||
const handleOnClear = () => {
|
||||
onFieldValueSelect({ ...field, value: undefined });
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="tw-bg-white tw-border tw-border-main tw-rounded tw-p-1 tw-px-2 tw-flex tw-justify-between">
|
||||
<span className="tw-self-center" data-testid="field-label">
|
||||
{startCase(getItemLabel(field.key))}:
|
||||
</span>
|
||||
<SearchInput
|
||||
handleChange={handleChange}
|
||||
handleClear={handleOnClear}
|
||||
handleSearch={handleSearch}
|
||||
handleSelect={handleOnSelect}
|
||||
options={options}
|
||||
value={value}
|
||||
/>
|
||||
<span
|
||||
className="tw-cursor-pointer tw-self-center"
|
||||
data-testid="field-remove-button"
|
||||
onClick={() => onFieldRemove(field.key)}>
|
||||
<FontAwesomeIcon className="tw-text-primary" icon="times" />
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ExploreQuickFilter;
|
||||
@ -0,0 +1,28 @@
|
||||
/*
|
||||
* Copyright 2022 Collate
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { SearchIndex } from '../../enums/search.enum';
|
||||
import { ExploreQuickFilterField } from './explore.interface';
|
||||
|
||||
export interface ExploreQuickFiltersProps {
|
||||
index: SearchIndex;
|
||||
fields: Array<ExploreQuickFilterField>;
|
||||
onFieldValueSelect: (field: ExploreQuickFilterField) => void;
|
||||
onAdvanceSearch: () => void;
|
||||
}
|
||||
|
||||
export interface FilterFieldsMenuItem {
|
||||
key: string;
|
||||
label: string;
|
||||
defaultField: boolean;
|
||||
}
|
||||
@ -17,10 +17,18 @@ import { SearchIndex } from '../../enums/search.enum';
|
||||
import { ExploreQuickFilterField } from '../Explore/explore.interface';
|
||||
import ExploreQuickFilters from './ExploreQuickFilters';
|
||||
|
||||
jest.mock('./ExploreQuickFilter', () =>
|
||||
const mockOnFieldRemove = jest.fn();
|
||||
const mockOnAdvanceSearch = jest.fn();
|
||||
const mockOnClear = jest.fn();
|
||||
const mockOnFieldValueSelect = jest.fn();
|
||||
const mockOnFieldSelect = jest.fn();
|
||||
const mockOnClearSelection = jest.fn();
|
||||
const mockOnUpdateFilterValues = jest.fn();
|
||||
|
||||
jest.mock('../SearchDropdown/SearchDropdown', () =>
|
||||
jest
|
||||
.fn()
|
||||
.mockReturnValue(<div data-testid="advanced-field">ExploreQuickFilter</div>)
|
||||
.mockReturnValue(<div data-testid="search-dropdown">SearchDropdown</div>)
|
||||
);
|
||||
|
||||
jest.mock('./AdvanceSearchModal.component', () => ({
|
||||
@ -33,11 +41,13 @@ const fields = [
|
||||
{ key: 'column_names', value: undefined },
|
||||
] as ExploreQuickFilterField[];
|
||||
|
||||
const onFieldRemove = jest.fn();
|
||||
const onAdvanceSearch = jest.fn();
|
||||
const onClear = jest.fn();
|
||||
const onFieldValueSelect = jest.fn();
|
||||
const onFieldSelect = jest.fn();
|
||||
const onFieldRemove = mockOnFieldRemove;
|
||||
const onAdvanceSearch = mockOnAdvanceSearch;
|
||||
const onClear = mockOnClear;
|
||||
const onFieldValueSelect = mockOnFieldValueSelect;
|
||||
const onFieldSelect = mockOnFieldSelect;
|
||||
const onClearSelection = mockOnClearSelection;
|
||||
const onUpdateFilterValues = mockOnUpdateFilterValues;
|
||||
|
||||
const mockProps = {
|
||||
index,
|
||||
@ -45,39 +55,19 @@ const mockProps = {
|
||||
onFieldRemove,
|
||||
onAdvanceSearch,
|
||||
onClear,
|
||||
onClearSelection,
|
||||
onFieldValueSelect,
|
||||
onFieldSelect,
|
||||
onUpdateFilterValues,
|
||||
};
|
||||
|
||||
describe('Test ExploreQuickFilters component', () => {
|
||||
it('Should render ExploreQuickFilters component', async () => {
|
||||
const { findByTestId, findAllByTestId } = render(
|
||||
<ExploreQuickFilters {...mockProps} />
|
||||
);
|
||||
const { findAllByTestId } = render(<ExploreQuickFilters {...mockProps} />);
|
||||
|
||||
const fields = await findAllByTestId('advanced-field');
|
||||
const clearButton = await findByTestId('clear-all-button');
|
||||
const fields = await findAllByTestId('search-dropdown');
|
||||
|
||||
expect(fields).toHaveLength(fields.length);
|
||||
|
||||
expect(clearButton).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Should call onClear method on click of Clear All button', async () => {
|
||||
const { findByTestId, findAllByTestId } = render(
|
||||
<ExploreQuickFilters {...mockProps} />
|
||||
);
|
||||
|
||||
const fields = await findAllByTestId('advanced-field');
|
||||
const clearButton = await findByTestId('clear-all-button');
|
||||
|
||||
expect(fields).toHaveLength(fields.length);
|
||||
|
||||
expect(clearButton).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(clearButton);
|
||||
|
||||
expect(onClear).toBeCalledWith();
|
||||
});
|
||||
|
||||
it('Should call onAdvanceSearch method on click of Advance Search button', async () => {
|
||||
@ -85,7 +75,7 @@ describe('Test ExploreQuickFilters component', () => {
|
||||
<ExploreQuickFilters {...mockProps} />
|
||||
);
|
||||
|
||||
const fields = await findAllByTestId('advanced-field');
|
||||
const fields = await findAllByTestId('search-dropdown');
|
||||
const advanceSearchButton = await findByTestId('advance-search-button');
|
||||
|
||||
expect(fields).toHaveLength(fields.length);
|
||||
|
||||
@ -11,93 +11,101 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Badge, Divider, Dropdown, Menu, Space } from 'antd';
|
||||
import { isEmpty, isNil, uniqueId } from 'lodash';
|
||||
import React, { FC, useCallback, useEffect, useMemo } from 'react';
|
||||
import { SearchIndex } from '../../enums/search.enum';
|
||||
import { getDropDownItems } from '../../utils/AdvancedSearchUtils';
|
||||
import SVGIcons, { Icons } from '../../utils/SvgUtils';
|
||||
import { ExploreQuickFilterField } from './explore.interface';
|
||||
import AdvancedField from './ExploreQuickFilter';
|
||||
import { Divider, Space } from 'antd';
|
||||
import { AxiosError } from 'axios';
|
||||
import { isUndefined } from 'lodash';
|
||||
import React, { FC, useState } from 'react';
|
||||
import {
|
||||
getAdvancedFieldDefaultOptions,
|
||||
getAdvancedFieldOptions,
|
||||
getTagSuggestions,
|
||||
getUserSuggestions,
|
||||
} from '../../axiosAPIs/miscAPI';
|
||||
import { MISC_FIELDS } from '../../constants/AdvancedSearch.constants';
|
||||
import { getAdvancedField } from '../../utils/AdvancedSearchUtils';
|
||||
import { showErrorToast } from '../../utils/ToastUtils';
|
||||
import SearchDropdown from '../SearchDropdown/SearchDropdown';
|
||||
import { ExploreQuickFiltersProps } from './ExploreQuickFilters.interface';
|
||||
|
||||
interface Props {
|
||||
index: SearchIndex;
|
||||
fields: Array<ExploreQuickFilterField>;
|
||||
onFieldRemove: (value: string) => void;
|
||||
onClear: () => void;
|
||||
onFieldValueSelect: (field: ExploreQuickFilterField) => void;
|
||||
onFieldSelect: (value: string) => void;
|
||||
onAdvanceSearch: () => void;
|
||||
}
|
||||
|
||||
const ExploreQuickFilters: FC<Props> = ({
|
||||
const ExploreQuickFilters: FC<ExploreQuickFiltersProps> = ({
|
||||
fields,
|
||||
onFieldRemove,
|
||||
onClear,
|
||||
onAdvanceSearch,
|
||||
index,
|
||||
onFieldValueSelect,
|
||||
onFieldSelect,
|
||||
}) => {
|
||||
const handleMenuItemClick = useCallback((menuInfo) => {
|
||||
onFieldSelect(menuInfo.key);
|
||||
}, []);
|
||||
const [options, setOptions] = useState<string[]>();
|
||||
const [isOptionsLoading, setIsOptionsLoading] = useState<boolean>(false);
|
||||
|
||||
const menuItems = useMemo(() => getDropDownItems(index), [index]);
|
||||
const fetchOptions = async (query: string, fieldKey: string) => {
|
||||
const advancedField = getAdvancedField(fieldKey);
|
||||
if (!MISC_FIELDS.includes(fieldKey)) {
|
||||
const res = await getAdvancedFieldOptions(query, index, advancedField);
|
||||
|
||||
const menu = useMemo(() => {
|
||||
return (
|
||||
<Menu
|
||||
items={menuItems.map((option) => ({
|
||||
...option,
|
||||
disabled: Boolean(fields.find((f) => f.key === option.key)),
|
||||
onClick: handleMenuItemClick,
|
||||
'data-testid': 'dropdown-menu-item',
|
||||
}))}
|
||||
/>
|
||||
);
|
||||
}, [onFieldSelect, fields, menuItems]);
|
||||
const suggestOptions =
|
||||
res.data.suggest['metadata-suggest'][0].options ?? [];
|
||||
const uniqueOptions = [...new Set(suggestOptions.map((op) => op.text))];
|
||||
setOptions(uniqueOptions);
|
||||
} else {
|
||||
if (fieldKey === 'tags.tagFQN') {
|
||||
const res = await getTagSuggestions(query);
|
||||
|
||||
useEffect(() => {
|
||||
onClear();
|
||||
handleMenuItemClick(menuItems[0]);
|
||||
handleMenuItemClick(menuItems[1]);
|
||||
}, [menuItems]);
|
||||
const suggestOptions =
|
||||
res.data.suggest['metadata-suggest'][0].options ?? [];
|
||||
const uniqueOptions = [
|
||||
...new Set(
|
||||
suggestOptions
|
||||
.filter((op) => !isUndefined(op._source.fullyQualifiedName))
|
||||
.map((op) => op._source.fullyQualifiedName as string)
|
||||
),
|
||||
];
|
||||
setOptions(uniqueOptions);
|
||||
} else {
|
||||
const res = await getUserSuggestions(query);
|
||||
|
||||
const filterCount = useMemo(
|
||||
() => fields.filter((field) => !isNil(field.value)).length,
|
||||
[fields]
|
||||
);
|
||||
const suggestOptions =
|
||||
res.data.suggest['metadata-suggest'][0].options ?? [];
|
||||
const uniqueOptions = [
|
||||
...new Set(suggestOptions.map((op) => op._source.name)),
|
||||
];
|
||||
setOptions(uniqueOptions);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const getFilterOptions = async (value: string, key: string) => {
|
||||
setIsOptionsLoading(true);
|
||||
try {
|
||||
if (value) {
|
||||
await fetchOptions(value, key);
|
||||
} else {
|
||||
const res = await getAdvancedFieldDefaultOptions(index, key);
|
||||
const buckets = res.data.aggregations[`sterms#${key}`].buckets;
|
||||
setOptions(buckets.map((option) => option.key));
|
||||
}
|
||||
} catch (error) {
|
||||
showErrorToast(error as AxiosError);
|
||||
} finally {
|
||||
setIsOptionsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Space wrap size={[16, 16]}>
|
||||
<Space wrap className="explore-quick-filters-container" size={[16, 16]}>
|
||||
{fields.map((field) => (
|
||||
<AdvancedField
|
||||
field={field}
|
||||
index={index}
|
||||
key={uniqueId()}
|
||||
onFieldRemove={onFieldRemove}
|
||||
onFieldValueSelect={onFieldValueSelect}
|
||||
<SearchDropdown
|
||||
isSuggestionsLoading={isOptionsLoading}
|
||||
key={field.key}
|
||||
label={field.label}
|
||||
options={options || []}
|
||||
searchKey={field.key}
|
||||
selectedKeys={field.value || []}
|
||||
onChange={(updatedValues) => {
|
||||
onFieldValueSelect({ ...field, value: updatedValues });
|
||||
}}
|
||||
onSearch={getFilterOptions}
|
||||
/>
|
||||
))}
|
||||
<Dropdown
|
||||
className="cursor-pointer"
|
||||
data-testid="quick-filter-dropdown"
|
||||
overlay={menu}
|
||||
trigger={['click']}>
|
||||
<Badge count={filterCount} size="small">
|
||||
<SVGIcons alt="filter" icon={Icons.FILTER_PRIMARY} />
|
||||
</Badge>
|
||||
</Dropdown>
|
||||
<Divider type="vertical" />
|
||||
{!isEmpty(fields) && (
|
||||
<span
|
||||
className="tw-text-primary tw-self-center tw-cursor-pointer"
|
||||
data-testid="clear-all-button"
|
||||
onClick={onClear}>
|
||||
Clear All
|
||||
</span>
|
||||
)}
|
||||
<Divider className="m-0" type="vertical" />
|
||||
<span
|
||||
className="tw-text-primary tw-self-center tw-cursor-pointer"
|
||||
data-testid="advance-search-button"
|
||||
|
||||
@ -72,7 +72,8 @@ export interface ExploreProps {
|
||||
|
||||
export interface ExploreQuickFilterField {
|
||||
key: string;
|
||||
value: string | undefined;
|
||||
label: string;
|
||||
value: string[] | undefined;
|
||||
}
|
||||
|
||||
export interface ExploreQuickFilterProps {
|
||||
|
||||
@ -11,17 +11,12 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export interface DropDownOption {
|
||||
key: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export interface SearchDropdownProps {
|
||||
label: string;
|
||||
options: DropDownOption[];
|
||||
isSuggestionsLoading?: boolean;
|
||||
options: string[];
|
||||
searchKey: string;
|
||||
selectedKeys: string[];
|
||||
showClear?: boolean;
|
||||
onChange: (values: string[], searchKey: string) => void;
|
||||
onSearch: (searchText: string, searchKey: string) => void;
|
||||
}
|
||||
|
||||
@ -11,6 +11,11 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
@trigger-btn-hover-bg: #efefef;
|
||||
@remove-icon-color: #bbb;
|
||||
@remove-icon-hover-color: #373737;
|
||||
@update-btn-hover-bg: #e2e2e2;
|
||||
|
||||
.custom-dropdown-render {
|
||||
// this is taken from antd dropdown menu box shadow
|
||||
box-shadow: 0 3px 6px -4px rgb(0 0 0 / 12%), 0 6px 16px 0 rgb(0 0 0 / 8%),
|
||||
@ -19,8 +24,37 @@
|
||||
.ant-dropdown-menu {
|
||||
box-shadow: none;
|
||||
padding: 0px;
|
||||
max-height: 200px;
|
||||
overflow-y: scroll;
|
||||
margin-bottom: 8px;
|
||||
|
||||
.ant-dropdown-menu-item {
|
||||
padding: 4px 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-option-label {
|
||||
max-width: 200px;
|
||||
}
|
||||
|
||||
.update-btn {
|
||||
background-color: @trigger-btn-hover-bg;
|
||||
border-color: @trigger-btn-hover-bg;
|
||||
}
|
||||
|
||||
.update-btn:hover {
|
||||
background-color: @update-btn-hover-bg;
|
||||
border-color: @update-btn-hover-bg;
|
||||
color: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
.quick-filter-dropdown-trigger-btn {
|
||||
.remove-field-icon {
|
||||
color: @remove-icon-color;
|
||||
padding-bottom: 2px;
|
||||
}
|
||||
.remove-field-icon:hover {
|
||||
color: @remove-icon-hover-color;
|
||||
}
|
||||
}
|
||||
|
||||
@ -20,20 +20,14 @@ import { SearchDropdownProps } from './SearchDropdown.interface';
|
||||
const mockOnChange = jest.fn();
|
||||
const mockOnSearch = jest.fn();
|
||||
|
||||
const searchOptions = [
|
||||
{ key: 'user.1', label: 'User 1' },
|
||||
{ key: 'user.2', label: 'User 2' },
|
||||
{ key: 'user.3', label: 'User 3' },
|
||||
{ key: 'user.4', label: 'User 4' },
|
||||
{ key: 'user.5', label: 'User 5' },
|
||||
];
|
||||
const searchOptions = ['User 1', 'User 2', 'User 3', 'User 4', 'User 5'];
|
||||
|
||||
const mockProps: SearchDropdownProps = {
|
||||
label: 'Owner',
|
||||
isSuggestionsLoading: false,
|
||||
options: searchOptions,
|
||||
searchKey: 'owner.name',
|
||||
selectedKeys: ['user.1'],
|
||||
showClear: true,
|
||||
selectedKeys: ['User 1'],
|
||||
onChange: mockOnChange,
|
||||
onSearch: mockOnSearch,
|
||||
};
|
||||
@ -72,9 +66,15 @@ describe('Search DropDown Component', () => {
|
||||
|
||||
expect(searchInput).toBeInTheDocument();
|
||||
|
||||
const clearButton = await screen.findByTestId('clear-button');
|
||||
const clearButton = screen.queryByTestId('clear-button');
|
||||
|
||||
expect(clearButton).toBeInTheDocument();
|
||||
expect(clearButton).not.toBeInTheDocument();
|
||||
|
||||
const updateButton = await screen.findByTestId('update-btn');
|
||||
const closeButton = await screen.findByTestId('update-btn');
|
||||
|
||||
expect(updateButton).toBeInTheDocument();
|
||||
expect(closeButton).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Selected keys option should be checked', async () => {
|
||||
@ -90,8 +90,8 @@ describe('Search DropDown Component', () => {
|
||||
|
||||
expect(await screen.findByTestId('drop-down-menu')).toBeInTheDocument();
|
||||
|
||||
// user.1 is selected key so should be checked
|
||||
expect(await screen.findByTestId('user.1')).toBeChecked();
|
||||
// User 1 is selected key so should be checked
|
||||
expect(await screen.findByTestId('User 1-checkbox')).toBeChecked();
|
||||
});
|
||||
|
||||
it('UnSelected keys option should not be checked', async () => {
|
||||
@ -107,13 +107,13 @@ describe('Search DropDown Component', () => {
|
||||
|
||||
expect(await screen.findByTestId('drop-down-menu')).toBeInTheDocument();
|
||||
|
||||
expect(await screen.findByTestId('user.2')).not.toBeChecked();
|
||||
expect(await screen.findByTestId('user.3')).not.toBeChecked();
|
||||
expect(await screen.findByTestId('user.4')).not.toBeChecked();
|
||||
expect(await screen.findByTestId('user.5')).not.toBeChecked();
|
||||
expect(await screen.findByTestId('User 2-checkbox')).not.toBeChecked();
|
||||
expect(await screen.findByTestId('User 3-checkbox')).not.toBeChecked();
|
||||
expect(await screen.findByTestId('User 4-checkbox')).not.toBeChecked();
|
||||
expect(await screen.findByTestId('User 5-checkbox')).not.toBeChecked();
|
||||
});
|
||||
|
||||
it('Should render the clear all button and click should work', async () => {
|
||||
it('Should render the clear all button after more than one options are selected and click should work', async () => {
|
||||
render(<SearchDropdown {...mockProps} />);
|
||||
|
||||
const container = await screen.findByTestId('search-dropdown');
|
||||
@ -126,6 +126,18 @@ describe('Search DropDown Component', () => {
|
||||
|
||||
expect(await screen.findByTestId('drop-down-menu')).toBeInTheDocument();
|
||||
|
||||
const option2 = await screen.findByTestId('User 2');
|
||||
|
||||
await act(async () => {
|
||||
userEvent.click(option2);
|
||||
});
|
||||
|
||||
let option1Checkbox = await screen.findByTestId('User 1-checkbox');
|
||||
let option2Checkbox = await screen.findByTestId('User 2-checkbox');
|
||||
|
||||
expect(option1Checkbox).toBeChecked();
|
||||
expect(option2Checkbox).toBeChecked();
|
||||
|
||||
const clearButton = await screen.findByTestId('clear-button');
|
||||
|
||||
expect(clearButton).toBeInTheDocument();
|
||||
@ -134,25 +146,11 @@ describe('Search DropDown Component', () => {
|
||||
userEvent.click(clearButton);
|
||||
});
|
||||
|
||||
expect(mockOnChange).toHaveBeenCalledWith([], 'owner.name');
|
||||
});
|
||||
option1Checkbox = await screen.findByTestId('User 1-checkbox');
|
||||
option2Checkbox = await screen.findByTestId('User 2-checkbox');
|
||||
|
||||
it('Should not render the clear all button if showClear is false/undefined', async () => {
|
||||
render(<SearchDropdown {...mockProps} showClear={false} />);
|
||||
|
||||
const container = await screen.findByTestId('search-dropdown');
|
||||
|
||||
expect(container).toBeInTheDocument();
|
||||
|
||||
await act(async () => {
|
||||
userEvent.click(container);
|
||||
});
|
||||
|
||||
expect(await screen.findByTestId('drop-down-menu')).toBeInTheDocument();
|
||||
|
||||
const clearButton = screen.queryByTestId('clear-button');
|
||||
|
||||
expect(clearButton).not.toBeInTheDocument();
|
||||
expect(option1Checkbox).not.toBeChecked();
|
||||
expect(option2Checkbox).not.toBeChecked();
|
||||
});
|
||||
|
||||
it('Search should work', async () => {
|
||||
@ -179,7 +177,7 @@ describe('Search DropDown Component', () => {
|
||||
expect(mockOnSearch).toHaveBeenCalledWith('user', 'owner.name');
|
||||
});
|
||||
|
||||
it('On Change should work', async () => {
|
||||
it('Update button should work properly', async () => {
|
||||
render(<SearchDropdown {...mockProps} />);
|
||||
|
||||
const container = await screen.findByTestId('search-dropdown');
|
||||
@ -198,9 +196,15 @@ describe('Search DropDown Component', () => {
|
||||
userEvent.click(option2);
|
||||
});
|
||||
|
||||
const updateButton = await screen.findByTestId('update-btn');
|
||||
|
||||
await act(async () => {
|
||||
userEvent.click(updateButton);
|
||||
});
|
||||
|
||||
// onChange should be called with previous selected keys and current selected keys
|
||||
expect(mockOnChange).toHaveBeenCalledWith(
|
||||
['user.1', 'user.2'],
|
||||
['User 1', 'User 2'],
|
||||
'owner.name'
|
||||
);
|
||||
});
|
||||
@ -218,12 +222,50 @@ describe('Search DropDown Component', () => {
|
||||
|
||||
expect(await screen.findByTestId('drop-down-menu')).toBeInTheDocument();
|
||||
|
||||
let option1Checkbox = await screen.findByTestId('User 1-checkbox');
|
||||
|
||||
expect(option1Checkbox).toBeChecked();
|
||||
|
||||
const option1 = await screen.findByTestId('User 1');
|
||||
|
||||
await act(async () => {
|
||||
userEvent.click(option1);
|
||||
});
|
||||
|
||||
expect(mockOnChange).toHaveBeenCalledWith([], 'owner.name');
|
||||
option1Checkbox = await screen.findByTestId('User 1-checkbox');
|
||||
|
||||
expect(option1Checkbox).not.toBeChecked();
|
||||
});
|
||||
|
||||
it('Close button should work properly', async () => {
|
||||
render(<SearchDropdown {...mockProps} />);
|
||||
|
||||
const container = await screen.findByTestId('search-dropdown');
|
||||
|
||||
expect(container).toBeInTheDocument();
|
||||
|
||||
let dropdownMenu = screen.queryByTestId('drop-down-menu');
|
||||
|
||||
expect(dropdownMenu).toBeNull();
|
||||
|
||||
await act(async () => {
|
||||
userEvent.click(container);
|
||||
});
|
||||
|
||||
dropdownMenu = await screen.findByTestId('drop-down-menu');
|
||||
|
||||
expect(dropdownMenu).toBeInTheDocument();
|
||||
|
||||
const closeButton = await screen.findByTestId('update-btn');
|
||||
|
||||
expect(closeButton).toBeInTheDocument();
|
||||
|
||||
await act(async () => {
|
||||
userEvent.click(closeButton);
|
||||
});
|
||||
|
||||
dropdownMenu = screen.queryByTestId('drop-down-menu');
|
||||
|
||||
expect(dropdownMenu).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@ -11,67 +11,79 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { DownOutlined } from '@ant-design/icons';
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Checkbox,
|
||||
Divider,
|
||||
Dropdown,
|
||||
Input,
|
||||
MenuItemProps,
|
||||
MenuProps,
|
||||
Row,
|
||||
Space,
|
||||
Typography,
|
||||
} from 'antd';
|
||||
import classNames from 'classnames';
|
||||
import React, { ChangeEvent, FC, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ReactComponent as DropDown } from '../../assets/svg/DropDown.svg';
|
||||
import {
|
||||
getSearchDropdownLabels,
|
||||
getSelectedOptionLabelString,
|
||||
} from '../../utils/AdvancedSearchUtils';
|
||||
import Loader from '../Loader/Loader';
|
||||
import { SearchDropdownProps } from './SearchDropdown.interface';
|
||||
import './SearchDropdown.less';
|
||||
|
||||
const SearchDropdown: FC<SearchDropdownProps> = ({
|
||||
isSuggestionsLoading,
|
||||
label,
|
||||
options,
|
||||
searchKey,
|
||||
selectedKeys,
|
||||
showClear,
|
||||
onChange,
|
||||
onSearch,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [isDropDownOpen, setIsDropDownOpen] = useState<boolean>(false);
|
||||
const [selectedOptions, setSelectedOptions] =
|
||||
useState<string[]>(selectedKeys);
|
||||
|
||||
// derive menu props from options and selected keys
|
||||
const menuOptions: MenuProps['items'] = useMemo(() => {
|
||||
return options.map((option) => {
|
||||
const isSelected = selectedKeys.includes(option.key);
|
||||
// Separating selected options to show on top
|
||||
const selectedOptionKeys =
|
||||
getSearchDropdownLabels(selectedOptions, true) || [];
|
||||
|
||||
return {
|
||||
key: option.key,
|
||||
label: (
|
||||
<Space data-testid={option.label} size={6}>
|
||||
<Checkbox checked={isSelected} data-testid={option.key} />
|
||||
{option.label}
|
||||
</Space>
|
||||
),
|
||||
};
|
||||
});
|
||||
}, [options, selectedKeys]);
|
||||
// Filtering out unselected options
|
||||
const unselectedOptions = options.filter(
|
||||
(option) => !selectedOptions.includes(option)
|
||||
);
|
||||
|
||||
// Labels for unselected options
|
||||
const otherOptions =
|
||||
getSearchDropdownLabels(unselectedOptions, false) || [];
|
||||
|
||||
return [...selectedOptionKeys, ...otherOptions];
|
||||
}, [options, selectedOptions]);
|
||||
|
||||
// handle menu item click
|
||||
const handleMenuItemClick: MenuItemProps['onClick'] = (info) => {
|
||||
const currentKey = info.key;
|
||||
const isSelected = selectedKeys.includes(currentKey);
|
||||
const isSelected = selectedOptions.includes(currentKey);
|
||||
|
||||
const updatedValues = isSelected
|
||||
? selectedKeys.filter((v) => v !== currentKey)
|
||||
: [...selectedKeys, currentKey];
|
||||
? selectedOptions.filter((v) => v !== currentKey)
|
||||
: [...selectedOptions, currentKey];
|
||||
|
||||
// call on change with updated value
|
||||
onChange(updatedValues, searchKey);
|
||||
setSelectedOptions(updatedValues);
|
||||
};
|
||||
|
||||
// handle clear all
|
||||
const handleClear = () => onChange([], searchKey);
|
||||
const handleClear = () => {
|
||||
setSelectedOptions([]);
|
||||
};
|
||||
|
||||
// handle search
|
||||
const handleSearch = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
@ -80,41 +92,111 @@ const SearchDropdown: FC<SearchDropdownProps> = ({
|
||||
onSearch(value, searchKey);
|
||||
};
|
||||
|
||||
// Handle dropdown close
|
||||
const handleDropdownClose = () => {
|
||||
setIsDropDownOpen(false);
|
||||
};
|
||||
|
||||
// Handle update button click
|
||||
const handleUpdate = () => {
|
||||
// call on change with updated value
|
||||
onChange(selectedOptions, searchKey);
|
||||
handleDropdownClose();
|
||||
};
|
||||
|
||||
const showClearAllBtn = useMemo(
|
||||
() => selectedOptions.length > 1,
|
||||
[selectedOptions]
|
||||
);
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
destroyPopupOnHide
|
||||
data-testid={searchKey}
|
||||
dropdownRender={(menuNode) => {
|
||||
return (
|
||||
<Card className="custom-dropdown-render" data-testid="drop-down-menu">
|
||||
<Space direction="vertical" size={4}>
|
||||
dropdownRender={(menuNode) => (
|
||||
<Card
|
||||
bodyStyle={{ padding: 0 }}
|
||||
className="custom-dropdown-render"
|
||||
data-testid="drop-down-menu">
|
||||
<Space direction="vertical" size={0}>
|
||||
<div className="p-t-sm p-x-sm">
|
||||
<Input
|
||||
data-testid="search-input"
|
||||
placeholder={`Search ${label}...`}
|
||||
placeholder={`${t('label.search-entity', {
|
||||
entity: label,
|
||||
})}...`}
|
||||
onChange={handleSearch}
|
||||
/>
|
||||
{showClear && (
|
||||
</div>
|
||||
{showClearAllBtn && (
|
||||
<>
|
||||
<Divider className="m-t-xs m-b-0" />
|
||||
<Button
|
||||
className="p-0"
|
||||
className="p-0 m-l-sm"
|
||||
data-testid="clear-button"
|
||||
type="link"
|
||||
onClick={handleClear}>
|
||||
{t('label.clear-all')}
|
||||
</Button>
|
||||
)}
|
||||
{menuNode}
|
||||
</>
|
||||
)}
|
||||
<Divider
|
||||
className={classNames(showClearAllBtn ? 'm-y-0' : 'm-t-xs m-b-0')}
|
||||
/>
|
||||
{isSuggestionsLoading ? (
|
||||
<Row align="middle" className="p-y-sm" justify="center">
|
||||
<Loader size="small" />
|
||||
</Row>
|
||||
) : options.length > 0 || selectedOptions.length > 0 ? (
|
||||
menuNode
|
||||
) : (
|
||||
<Row className="m-y-sm" justify="center">
|
||||
<Typography.Text>
|
||||
{t('label.no-data-available')}
|
||||
</Typography.Text>
|
||||
</Row>
|
||||
)}
|
||||
<Space className="p-sm p-t-xss">
|
||||
<Button
|
||||
className="update-btn"
|
||||
data-testid="update-btn"
|
||||
size="small"
|
||||
onClick={handleUpdate}>
|
||||
{t('label.update')}
|
||||
</Button>
|
||||
<Button
|
||||
data-testid="close-btn"
|
||||
size="small"
|
||||
type="link"
|
||||
onClick={handleDropdownClose}>
|
||||
{t('label.close')}
|
||||
</Button>
|
||||
</Space>
|
||||
</Card>
|
||||
);
|
||||
}}
|
||||
</Space>
|
||||
</Card>
|
||||
)}
|
||||
key={searchKey}
|
||||
menu={{ items: menuOptions, onClick: handleMenuItemClick }}
|
||||
open={isDropDownOpen}
|
||||
trigger={['click']}
|
||||
visible={isDropDownOpen}
|
||||
onVisibleChange={(visible) => setIsDropDownOpen(visible)}>
|
||||
<Button>
|
||||
<Space data-testid="search-dropdown">
|
||||
{label}
|
||||
<DownOutlined />
|
||||
onOpenChange={(visible) => {
|
||||
visible && onSearch('', searchKey);
|
||||
setIsDropDownOpen(visible);
|
||||
}}>
|
||||
<Button className="quick-filter-dropdown-trigger-btn">
|
||||
<Space data-testid="search-dropdown" size={4}>
|
||||
<Space size={0}>
|
||||
<Typography.Text>{label}</Typography.Text>
|
||||
{selectedKeys.length > 0 && (
|
||||
<span>
|
||||
{': '}
|
||||
<Typography.Text className="text-primary font-medium">
|
||||
{getSelectedOptionLabelString(selectedKeys)}
|
||||
</Typography.Text>
|
||||
</span>
|
||||
)}
|
||||
</Space>
|
||||
<DropDown className="flex self-center" />
|
||||
</Space>
|
||||
</Button>
|
||||
</Dropdown>
|
||||
|
||||
@ -474,7 +474,10 @@
|
||||
"no-of-test": " No. of Test",
|
||||
"test-suite": "Test Suite",
|
||||
"enter-entity": "Enter {{entity}}",
|
||||
"test-suite-status": "Test Suite Status"
|
||||
"test-suite-status": "Test Suite Status",
|
||||
"search-entity": "Search {{entity}}",
|
||||
"more": "More",
|
||||
"update": "Update"
|
||||
},
|
||||
"message": {
|
||||
"service-email-required": "Service account Email is required",
|
||||
|
||||
@ -19,6 +19,7 @@
|
||||
@white: #fff;
|
||||
@gray: #dde3ea;
|
||||
@grey-body: rgb(55, 53, 47);
|
||||
@trigger-btn-hover-bg: #efefef;
|
||||
|
||||
//font weight
|
||||
.font-medium {
|
||||
@ -92,6 +93,9 @@
|
||||
.border-1 {
|
||||
border-width: 1px;
|
||||
}
|
||||
.border-l {
|
||||
border-left-width: 1px;
|
||||
}
|
||||
.border-l-2 {
|
||||
border-left: 2px;
|
||||
}
|
||||
@ -280,3 +284,15 @@
|
||||
margin-bottom: 16px !important;
|
||||
color: @grey-body !important;
|
||||
}
|
||||
|
||||
.quick-filter-dropdown-trigger-btn {
|
||||
padding: 4px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.quick-filter-dropdown-trigger-btn:hover,
|
||||
.quick-filter-dropdown-trigger-btn:focus {
|
||||
background-color: @trigger-btn-hover-bg;
|
||||
}
|
||||
|
||||
@ -288,6 +288,9 @@
|
||||
.p-xs {
|
||||
padding: @padding-xs !important;
|
||||
}
|
||||
.p-xss {
|
||||
padding: @padding-xss;
|
||||
}
|
||||
.p-sm {
|
||||
padding: @padding-sm;
|
||||
}
|
||||
|
||||
@ -0,0 +1,62 @@
|
||||
/*
|
||||
* Copyright 2022 Collate
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
getSearchDropdownLabels,
|
||||
getSelectedOptionLabelString,
|
||||
} from './AdvancedSearchUtils';
|
||||
import {
|
||||
mockLongOptionsArray,
|
||||
mockOptionsArray,
|
||||
mockShortOptionsArray,
|
||||
} from './mocks/AdvancedSearchUtils.mock';
|
||||
|
||||
describe('AdvancedSearchUtils tests', () => {
|
||||
it('Function getSearchDropdownLabels should return menuItems for passed options', () => {
|
||||
const resultMenuItems = getSearchDropdownLabels(mockOptionsArray, true);
|
||||
|
||||
expect(resultMenuItems).toHaveLength(4);
|
||||
});
|
||||
|
||||
it('Function getSearchDropdownLabels should return an empty array if passed 1st argument as other than array', () => {
|
||||
const resultMenuItems = getSearchDropdownLabels(
|
||||
'' as unknown as string[],
|
||||
true
|
||||
);
|
||||
|
||||
expect(resultMenuItems).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('Function getSelectedOptionLabelString should return all options if the length of resultant string is less than 15', () => {
|
||||
const resultOptionsString = getSelectedOptionLabelString(
|
||||
mockShortOptionsArray
|
||||
);
|
||||
|
||||
expect(resultOptionsString).toEqual('str1, str2');
|
||||
});
|
||||
|
||||
it('Function getSelectedOptionLabelString should return string with ellipsis if the length of resultant string is more than 15', () => {
|
||||
const resultOptionsString =
|
||||
getSelectedOptionLabelString(mockLongOptionsArray);
|
||||
|
||||
expect(resultOptionsString).toEqual('string1, st...');
|
||||
});
|
||||
|
||||
it('Function getSelectedOptionLabelString should return an empty string when passed anything else than string array as an argument', () => {
|
||||
const resultOptionsString = getSelectedOptionLabelString(
|
||||
'invalidInput' as unknown as string[]
|
||||
);
|
||||
|
||||
expect(resultOptionsString).toEqual('');
|
||||
});
|
||||
});
|
||||
@ -12,9 +12,9 @@
|
||||
*/
|
||||
|
||||
import Icon, { CloseCircleOutlined, PlusOutlined } from '@ant-design/icons';
|
||||
import { Button } from 'antd';
|
||||
import { Button, Checkbox, MenuProps, Space, Typography } from 'antd';
|
||||
import i18next from 'i18next';
|
||||
import { isUndefined } from 'lodash';
|
||||
import { isArray, isUndefined } from 'lodash';
|
||||
import React from 'react';
|
||||
import { RenderSettings } from 'react-awesome-query-builder';
|
||||
import {
|
||||
@ -136,3 +136,40 @@ export const renderAdvanceSearchButtons: RenderSettings['renderButton'] = (
|
||||
|
||||
return <></>;
|
||||
};
|
||||
|
||||
export const getSearchDropdownLabels = (
|
||||
optionsArray: string[],
|
||||
checked: boolean
|
||||
): MenuProps['items'] => {
|
||||
if (isArray(optionsArray)) {
|
||||
return optionsArray.map((option) => ({
|
||||
key: option,
|
||||
label: (
|
||||
<Space className="m-x-sm" data-testid={option} size={6}>
|
||||
<Checkbox checked={checked} data-testid={`${option}-checkbox`} />
|
||||
<Typography.Text
|
||||
ellipsis
|
||||
className="dropdown-option-label"
|
||||
title={option}>
|
||||
{option}
|
||||
</Typography.Text>
|
||||
</Space>
|
||||
),
|
||||
}));
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
export const getSelectedOptionLabelString = (selectedOptions: string[]) => {
|
||||
if (isArray(selectedOptions)) {
|
||||
const stringifiedOptions = selectedOptions.join(', ');
|
||||
if (stringifiedOptions.length < 15) {
|
||||
return stringifiedOptions;
|
||||
} else {
|
||||
return `${stringifiedOptions.slice(0, 11)}...`;
|
||||
}
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
@ -0,0 +1,28 @@
|
||||
/*
|
||||
* Copyright 2022 Collate
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export const mockOptionsArray = [
|
||||
'option_1',
|
||||
'option_2',
|
||||
'option_3',
|
||||
'option_4',
|
||||
];
|
||||
|
||||
export const mockShortOptionsArray = ['str1', 'str2'];
|
||||
|
||||
export const mockLongOptionsArray = [
|
||||
'string1',
|
||||
'string2',
|
||||
'string3',
|
||||
'string4',
|
||||
];
|
||||
Loading…
x
Reference in New Issue
Block a user