[App Search] Wired up configurable Sort and Facets in Documents View (#88764)
This commit is contained in:
parent
f4f6cb687c
commit
15f05b51ff
|
@ -15,10 +15,32 @@ describe('buildSearchUIConfig', () => {
|
|||
foo: 'text' as SchemaTypes,
|
||||
bar: 'number' as SchemaTypes,
|
||||
};
|
||||
const fields = {
|
||||
filterFields: ['fieldA', 'fieldB'],
|
||||
sortFields: [],
|
||||
};
|
||||
|
||||
const config = buildSearchUIConfig(connector, schema);
|
||||
expect(config.apiConnector).toEqual(connector);
|
||||
expect(config.searchQuery.result_fields).toEqual({
|
||||
const config = buildSearchUIConfig(connector, schema, fields);
|
||||
expect(config).toEqual({
|
||||
alwaysSearchOnInitialLoad: true,
|
||||
apiConnector: connector,
|
||||
initialState: {
|
||||
sortDirection: 'desc',
|
||||
sortField: 'id',
|
||||
},
|
||||
searchQuery: {
|
||||
disjunctiveFacets: ['fieldA', 'fieldB'],
|
||||
facets: {
|
||||
fieldA: {
|
||||
size: 30,
|
||||
type: 'value',
|
||||
},
|
||||
fieldB: {
|
||||
size: 30,
|
||||
type: 'value',
|
||||
},
|
||||
},
|
||||
result_fields: {
|
||||
bar: {
|
||||
raw: {},
|
||||
snippet: {
|
||||
|
@ -33,6 +55,9 @@ describe('buildSearchUIConfig', () => {
|
|||
size: 300,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
trackUrlState: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,8 +5,17 @@
|
|||
*/
|
||||
|
||||
import { Schema } from '../../../../shared/types';
|
||||
import { Fields } from './types';
|
||||
|
||||
export const buildSearchUIConfig = (apiConnector: object, schema: Schema, fields: Fields) => {
|
||||
const facets = fields.filterFields.reduce(
|
||||
(facetsConfig, fieldName) => ({
|
||||
...facetsConfig,
|
||||
[fieldName]: { type: 'value', size: 30 },
|
||||
}),
|
||||
{}
|
||||
);
|
||||
|
||||
export const buildSearchUIConfig = (apiConnector: object, schema: Schema) => {
|
||||
return {
|
||||
alwaysSearchOnInitialLoad: true,
|
||||
apiConnector,
|
||||
|
@ -16,6 +25,8 @@ export const buildSearchUIConfig = (apiConnector: object, schema: Schema) => {
|
|||
sortField: 'id',
|
||||
},
|
||||
searchQuery: {
|
||||
disjunctiveFacets: fields.filterFields,
|
||||
facets,
|
||||
result_fields: Object.keys(schema).reduce((acc: { [key: string]: object }, key: string) => {
|
||||
acc[key] = {
|
||||
snippet: {
|
||||
|
|
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { buildSortOptions } from './build_sort_options';
|
||||
|
||||
describe('buildSortOptions', () => {
|
||||
it('builds sort options from a list of field names', () => {
|
||||
const sortOptions = buildSortOptions(
|
||||
{
|
||||
filterFields: [],
|
||||
sortFields: ['fieldA', 'fieldB'],
|
||||
},
|
||||
[
|
||||
{
|
||||
name: 'Relevance (asc)',
|
||||
value: 'id',
|
||||
direction: 'desc',
|
||||
},
|
||||
{
|
||||
name: 'Relevance (desc)',
|
||||
value: 'id',
|
||||
direction: 'asc',
|
||||
},
|
||||
]
|
||||
);
|
||||
|
||||
expect(sortOptions).toEqual([
|
||||
{
|
||||
name: 'Relevance (asc)',
|
||||
value: 'id',
|
||||
direction: 'desc',
|
||||
},
|
||||
{
|
||||
name: 'Relevance (desc)',
|
||||
value: 'id',
|
||||
direction: 'asc',
|
||||
},
|
||||
{
|
||||
direction: 'asc',
|
||||
name: 'fieldA (asc)',
|
||||
value: 'fieldA',
|
||||
},
|
||||
{
|
||||
direction: 'desc',
|
||||
name: 'fieldA (desc)',
|
||||
value: 'fieldA',
|
||||
},
|
||||
{
|
||||
direction: 'asc',
|
||||
name: 'fieldB (asc)',
|
||||
value: 'fieldB',
|
||||
},
|
||||
{
|
||||
direction: 'desc',
|
||||
name: 'fieldB (desc)',
|
||||
value: 'fieldB',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { flatten } from 'lodash';
|
||||
|
||||
import { Fields, SortOption, SortDirection } from './types';
|
||||
import { ASCENDING, DESCENDING } from './constants';
|
||||
|
||||
const fieldNameToSortOptions = (fieldName: string): SortOption[] =>
|
||||
['asc', 'desc'].map((direction) => ({
|
||||
name: direction === 'asc' ? ASCENDING(fieldName) : DESCENDING(fieldName),
|
||||
value: fieldName,
|
||||
direction: direction as SortDirection,
|
||||
}));
|
||||
|
||||
/**
|
||||
* Adds two sort options for a given field, a "desc" and an "asc" option.
|
||||
*/
|
||||
export const buildSortOptions = (
|
||||
fields: Fields,
|
||||
defaultSortOptions: SortOption[]
|
||||
): SortOption[] => {
|
||||
const sortFieldsOptions = flatten(fields.sortFields.map(fieldNameToSortOptions));
|
||||
const sortingOptions = [...defaultSortOptions, ...sortFieldsOptions];
|
||||
return sortingOptions;
|
||||
};
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export const ASCENDING = (fieldName: string) =>
|
||||
i18n.translate(
|
||||
'xpack.enterpriseSearch.appSearch.documents.search.sortBy.option.ascendingDropDownOptionLabel',
|
||||
{
|
||||
defaultMessage: '{fieldName} (asc)',
|
||||
values: { fieldName },
|
||||
}
|
||||
);
|
||||
|
||||
export const DESCENDING = (fieldName: string) =>
|
||||
i18n.translate(
|
||||
'xpack.enterpriseSearch.appSearch.documents.search.sortBy.option.descendingDropDownOptionLabel',
|
||||
{
|
||||
defaultMessage: '{fieldName} (desc)',
|
||||
values: { fieldName },
|
||||
}
|
||||
);
|
|
@ -25,4 +25,12 @@
|
|||
background-color: $euiPageBackgroundColor;
|
||||
padding: $euiSizeL;
|
||||
}
|
||||
|
||||
.documentsSearchExperience__facet {
|
||||
line-height: 0;
|
||||
|
||||
.euiCheckbox__label {
|
||||
@include euiTextTruncate;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,25 +7,19 @@
|
|||
import '../../../../__mocks__/enterprise_search_url.mock';
|
||||
import { setMockValues } from '../../../../__mocks__';
|
||||
|
||||
const mockSetFields = jest.fn();
|
||||
|
||||
jest.mock('../../../../shared/use_local_storage', () => ({
|
||||
useLocalStorage: jest.fn(() => [
|
||||
{
|
||||
filterFields: ['a', 'b', 'c'],
|
||||
sortFields: ['d', 'c'],
|
||||
},
|
||||
mockSetFields,
|
||||
]),
|
||||
useLocalStorage: jest.fn(),
|
||||
}));
|
||||
import { useLocalStorage } from '../../../../shared/use_local_storage';
|
||||
|
||||
import React from 'react';
|
||||
// @ts-expect-error types are not available for this package yet
|
||||
import { SearchProvider } from '@elastic/react-search-ui';
|
||||
import { shallow } from 'enzyme';
|
||||
import { SearchProvider, Facet } from '@elastic/react-search-ui';
|
||||
import { shallow, ShallowWrapper } from 'enzyme';
|
||||
|
||||
import { CustomizationCallout } from './customization_callout';
|
||||
import { CustomizationModal } from './customization_modal';
|
||||
import { Fields } from './types';
|
||||
|
||||
import { SearchExperience } from './search_experience';
|
||||
|
||||
|
@ -36,8 +30,16 @@ describe('SearchExperience', () => {
|
|||
apiKey: '1234',
|
||||
},
|
||||
};
|
||||
const mockSetFields = jest.fn();
|
||||
const setFieldsInLocalStorage = (fields: Fields) => {
|
||||
(useLocalStorage as jest.Mock).mockImplementation(() => [fields, mockSetFields]);
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
setFieldsInLocalStorage({
|
||||
filterFields: ['a', 'b', 'c'],
|
||||
sortFields: ['d', 'c'],
|
||||
});
|
||||
jest.clearAllMocks();
|
||||
setMockValues(values);
|
||||
});
|
||||
|
@ -47,12 +49,60 @@ describe('SearchExperience', () => {
|
|||
expect(wrapper.find(SearchProvider).length).toBe(1);
|
||||
});
|
||||
|
||||
describe('when there are no selected filter fields', () => {
|
||||
let wrapper: ShallowWrapper;
|
||||
beforeEach(() => {
|
||||
setFieldsInLocalStorage({
|
||||
filterFields: [],
|
||||
sortFields: ['a', 'b'],
|
||||
});
|
||||
wrapper = shallow(<SearchExperience />);
|
||||
});
|
||||
|
||||
it('shows a customize callout instead of a button if no fields are yet selected', () => {
|
||||
expect(wrapper.find(CustomizationCallout).exists()).toBe(true);
|
||||
expect(wrapper.find('[data-test-subj="customize"]').exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('will show the customization modal when clicked', () => {
|
||||
expect(wrapper.find(CustomizationModal).exists()).toBe(false);
|
||||
wrapper.find(CustomizationCallout).simulate('click');
|
||||
|
||||
expect(wrapper.find(CustomizationModal).exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when there are selected filter fields', () => {
|
||||
let wrapper: ShallowWrapper;
|
||||
beforeEach(() => {
|
||||
setFieldsInLocalStorage({
|
||||
filterFields: ['a', 'b'],
|
||||
sortFields: ['a', 'b'],
|
||||
});
|
||||
wrapper = shallow(<SearchExperience />);
|
||||
});
|
||||
|
||||
it('shows a customize button', () => {
|
||||
expect(wrapper.find(CustomizationCallout).exists()).toBe(false);
|
||||
expect(wrapper.find('[data-test-subj="customize"]').exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it('renders Facet components for filter fields', () => {
|
||||
setFieldsInLocalStorage({
|
||||
filterFields: ['a', 'b', 'c'],
|
||||
sortFields: [],
|
||||
});
|
||||
const wrapper = shallow(<SearchExperience />);
|
||||
expect(wrapper.find(Facet).length).toBe(3);
|
||||
});
|
||||
|
||||
describe('customization modal', () => {
|
||||
it('has a customization modal which can be opened and closed', () => {
|
||||
const wrapper = shallow(<SearchExperience />);
|
||||
expect(wrapper.find(CustomizationModal).exists()).toBe(false);
|
||||
|
||||
wrapper.find(CustomizationCallout).simulate('click');
|
||||
wrapper.find('[data-test-subj="customize"]').simulate('click');
|
||||
expect(wrapper.find(CustomizationModal).exists()).toBe(true);
|
||||
|
||||
wrapper.find(CustomizationModal).prop('onClose')();
|
||||
|
@ -61,14 +111,14 @@ describe('SearchExperience', () => {
|
|||
|
||||
it('passes values from localStorage to the customization modal', () => {
|
||||
const wrapper = shallow(<SearchExperience />);
|
||||
wrapper.find(CustomizationCallout).simulate('click');
|
||||
wrapper.find('[data-test-subj="customize"]').simulate('click');
|
||||
expect(wrapper.find(CustomizationModal).prop('filterFields')).toEqual(['a', 'b', 'c']);
|
||||
expect(wrapper.find(CustomizationModal).prop('sortFields')).toEqual(['d', 'c']);
|
||||
});
|
||||
|
||||
it('updates selected fields in localStorage and closes modal on save', () => {
|
||||
const wrapper = shallow(<SearchExperience />);
|
||||
wrapper.find(CustomizationCallout).simulate('click');
|
||||
wrapper.find('[data-test-subj="customize"]').simulate('click');
|
||||
wrapper.find(CustomizationModal).prop('onSave')({
|
||||
filterFields: ['new', 'filters'],
|
||||
sortFields: ['new', 'sorts'],
|
||||
|
|
|
@ -7,36 +7,41 @@ import React, { useState } from 'react';
|
|||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { useValues } from 'kea';
|
||||
import { EuiSpacer, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import { EuiButton, EuiSpacer, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
// @ts-expect-error types are not available for this package yet;
|
||||
import { SearchProvider, SearchBox, Sorting } from '@elastic/react-search-ui';
|
||||
import { SearchProvider, SearchBox, Sorting, Facet } from '@elastic/react-search-ui';
|
||||
// @ts-expect-error types are not available for this package yet
|
||||
import AppSearchAPIConnector from '@elastic/search-ui-app-search-connector';
|
||||
|
||||
import './search_experience.scss';
|
||||
|
||||
import { EngineLogic } from '../../engine';
|
||||
import { externalUrl } from '../../../../shared/enterprise_search_url';
|
||||
import { useLocalStorage } from '../../../../shared/use_local_storage';
|
||||
import { EngineLogic } from '../../engine';
|
||||
|
||||
import { SearchBoxView, SortingView } from './views';
|
||||
import { Fields, SortOption } from './types';
|
||||
import { SearchBoxView, SortingView, MultiCheckboxFacetsView } from './views';
|
||||
import { SearchExperienceContent } from './search_experience_content';
|
||||
import { buildSearchUIConfig } from './build_search_ui_config';
|
||||
import { CustomizationCallout } from './customization_callout';
|
||||
import { CustomizationModal } from './customization_modal';
|
||||
import { buildSortOptions } from './build_sort_options';
|
||||
import { ASCENDING, DESCENDING } from './constants';
|
||||
|
||||
const DEFAULT_SORT_OPTIONS = [
|
||||
const RECENTLY_UPLOADED = i18n.translate(
|
||||
'xpack.enterpriseSearch.appSearch.documents.search.sortBy.option.recentlyUploaded',
|
||||
{
|
||||
name: i18n.translate('xpack.enterpriseSearch.appSearch.documents.search.recentlyUploadedDesc', {
|
||||
defaultMessage: 'Recently Uploaded (desc)',
|
||||
}),
|
||||
defaultMessage: 'Recently Uploaded',
|
||||
}
|
||||
);
|
||||
const DEFAULT_SORT_OPTIONS: SortOption[] = [
|
||||
{
|
||||
name: DESCENDING(RECENTLY_UPLOADED),
|
||||
value: 'id',
|
||||
direction: 'desc',
|
||||
},
|
||||
{
|
||||
name: i18n.translate('xpack.enterpriseSearch.appSearch.documents.search.recentlyUploadedAsc', {
|
||||
defaultMessage: 'Recently Uploaded (asc)',
|
||||
}),
|
||||
name: ASCENDING(RECENTLY_UPLOADED),
|
||||
value: 'id',
|
||||
direction: 'asc',
|
||||
},
|
||||
|
@ -50,16 +55,15 @@ export const SearchExperience: React.FC = () => {
|
|||
const openCustomizationModal = () => setShowCustomizationModal(true);
|
||||
const closeCustomizationModal = () => setShowCustomizationModal(false);
|
||||
|
||||
const [fields, setFields] = useLocalStorage(
|
||||
const [fields, setFields] = useLocalStorage<Fields>(
|
||||
`documents-search-experience-customization--${engine.name}`,
|
||||
{
|
||||
filterFields: [] as string[],
|
||||
sortFields: [] as string[],
|
||||
filterFields: [],
|
||||
sortFields: [],
|
||||
}
|
||||
);
|
||||
|
||||
// TODO const sortFieldsOptions = _flatten(fields.sortFields.map(fieldNameToSortOptions)) // we need to flatten this array since fieldNameToSortOptions returns an array of two sorting options
|
||||
const sortingOptions = [...DEFAULT_SORT_OPTIONS /* TODO ...sortFieldsOptions*/];
|
||||
const sortingOptions = buildSortOptions(fields, DEFAULT_SORT_OPTIONS);
|
||||
|
||||
const connector = new AppSearchAPIConnector({
|
||||
cacheResponses: false,
|
||||
|
@ -68,7 +72,7 @@ export const SearchExperience: React.FC = () => {
|
|||
searchKey: engine.apiKey,
|
||||
});
|
||||
|
||||
const searchProviderConfig = buildSearchUIConfig(connector, engine.schema || {});
|
||||
const searchProviderConfig = buildSearchUIConfig(connector, engine.schema || {}, fields);
|
||||
|
||||
return (
|
||||
<div className="documentsSearchExperience">
|
||||
|
@ -101,7 +105,36 @@ export const SearchExperience: React.FC = () => {
|
|||
view={SortingView}
|
||||
/>
|
||||
<EuiSpacer />
|
||||
{fields.filterFields.length > 0 ? (
|
||||
<>
|
||||
{fields.filterFields.map((fieldName) => (
|
||||
<section key={fieldName}>
|
||||
<Facet
|
||||
field={fieldName}
|
||||
label={fieldName}
|
||||
view={MultiCheckboxFacetsView}
|
||||
filterType="any"
|
||||
/>
|
||||
<EuiSpacer size="l" />
|
||||
</section>
|
||||
))}
|
||||
<EuiButton
|
||||
data-test-subj="customize"
|
||||
color="primary"
|
||||
iconType="gear"
|
||||
onClick={openCustomizationModal}
|
||||
>
|
||||
{i18n.translate(
|
||||
'xpack.enterpriseSearch.appSearch.documents.search.customizationButton',
|
||||
{
|
||||
defaultMessage: 'Customize filters and sort',
|
||||
}
|
||||
)}
|
||||
</EuiButton>
|
||||
</>
|
||||
) : (
|
||||
<CustomizationCallout onClick={openCustomizationModal} />
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem className="documentsSearchExperience__content">
|
||||
<SearchExperienceContent />
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export interface Fields {
|
||||
filterFields: string[];
|
||||
sortFields: string[];
|
||||
}
|
||||
|
||||
export type SortDirection = 'asc' | 'desc';
|
||||
|
||||
export interface SortOption {
|
||||
name: string;
|
||||
value: string;
|
||||
direction: SortDirection;
|
||||
}
|
|
@ -9,3 +9,4 @@ export { SortingView } from './sorting_view';
|
|||
export { ResultView } from './result_view';
|
||||
export { ResultsPerPageView } from './results_per_page_view';
|
||||
export { PagingView } from './paging_view';
|
||||
export { MultiCheckboxFacetsView } from './multi_checkbox_facets_view';
|
||||
|
|
|
@ -0,0 +1,99 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { MultiCheckboxFacetsView } from './multi_checkbox_facets_view';
|
||||
|
||||
describe('MultiCheckboxFacetsView', () => {
|
||||
const props = {
|
||||
label: 'foo',
|
||||
options: [
|
||||
{
|
||||
value: 'value1',
|
||||
selected: false,
|
||||
},
|
||||
{
|
||||
value: 'value2',
|
||||
selected: false,
|
||||
},
|
||||
],
|
||||
showMore: true,
|
||||
onMoreClick: jest.fn(),
|
||||
onRemove: jest.fn(),
|
||||
onSelect: jest.fn(),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('renders', () => {
|
||||
const wrapper = shallow(<MultiCheckboxFacetsView {...props} />);
|
||||
expect(wrapper.isEmptyRender()).toBe(false);
|
||||
});
|
||||
|
||||
it('calls onMoreClick when more button is clicked', () => {
|
||||
const wrapper = shallow(<MultiCheckboxFacetsView {...props} />);
|
||||
wrapper.find('[data-test-subj="more"]').simulate('click');
|
||||
expect(props.onMoreClick).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('calls onSelect when an option is selected', () => {
|
||||
const wrapper = shallow(<MultiCheckboxFacetsView {...props} />);
|
||||
wrapper.find('[data-test-subj="checkbox-group"]').simulate('change', 'generated-id_1');
|
||||
expect(props.onSelect).toHaveBeenCalledWith('value2');
|
||||
});
|
||||
|
||||
it('calls onRemove if the option was already selected', () => {
|
||||
const wrapper = shallow(
|
||||
<MultiCheckboxFacetsView
|
||||
{...{
|
||||
...props,
|
||||
options: [
|
||||
{
|
||||
value: 'value1',
|
||||
selected: false,
|
||||
},
|
||||
{
|
||||
value: 'value2',
|
||||
selected: true,
|
||||
},
|
||||
],
|
||||
}}
|
||||
/>
|
||||
);
|
||||
wrapper.find('[data-test-subj="checkbox-group"]').simulate('change', 'generated-id_1');
|
||||
expect(props.onRemove).toHaveBeenCalledWith('value2');
|
||||
});
|
||||
|
||||
it('it passes options to EuiCheckboxGroup, converting no values to the text "No Value"', () => {
|
||||
const wrapper = shallow(
|
||||
<MultiCheckboxFacetsView
|
||||
{...{
|
||||
...props,
|
||||
options: [
|
||||
{
|
||||
value: 'value1',
|
||||
selected: true,
|
||||
},
|
||||
{
|
||||
value: '',
|
||||
selected: false,
|
||||
},
|
||||
],
|
||||
}}
|
||||
/>
|
||||
);
|
||||
const options = wrapper.find('[data-test-subj="checkbox-group"]').prop('options');
|
||||
expect(options).toEqual([
|
||||
{ id: 'generated-id_0', label: 'value1' },
|
||||
{ id: 'generated-id_1', label: '<No value>' },
|
||||
]);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,114 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import {
|
||||
htmlIdGenerator,
|
||||
EuiCheckboxGroup,
|
||||
EuiFlexGroup,
|
||||
EuiButtonEmpty,
|
||||
EuiSpacer,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
interface Option {
|
||||
value: string;
|
||||
selected: boolean;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
label: string;
|
||||
options: Option[];
|
||||
showMore: boolean;
|
||||
onMoreClick(): void;
|
||||
onRemove(id: string): void;
|
||||
onSelect(id: string): void;
|
||||
}
|
||||
|
||||
const getIndexFromId = (id: string) => parseInt(id.split('_')[1], 10);
|
||||
|
||||
export const MultiCheckboxFacetsView: React.FC<Props> = ({
|
||||
label,
|
||||
onMoreClick,
|
||||
onRemove,
|
||||
onSelect,
|
||||
options,
|
||||
showMore,
|
||||
}) => {
|
||||
const getId = htmlIdGenerator();
|
||||
|
||||
const optionToCheckBoxGroupOption = (option: Option, index: number) => ({
|
||||
id: getId(String(index)),
|
||||
label:
|
||||
option.value ||
|
||||
i18n.translate(
|
||||
'xpack.enterpriseSearch.appSearch.documents.search.multiCheckboxFacetsView.noValue.selectOption',
|
||||
{
|
||||
defaultMessage: '<No value>',
|
||||
}
|
||||
),
|
||||
});
|
||||
|
||||
const optionToSelectedMapReducer = (
|
||||
selectedMap: { [name: string]: boolean },
|
||||
option: Option,
|
||||
index: number
|
||||
) => {
|
||||
if (option.selected) {
|
||||
selectedMap[getId(String(index))] = true;
|
||||
}
|
||||
return selectedMap;
|
||||
};
|
||||
|
||||
const checkboxGroupOptions = options.map(optionToCheckBoxGroupOption);
|
||||
const idToSelectedMap = options.reduce(optionToSelectedMapReducer, {});
|
||||
|
||||
const onChange = (checkboxId: string) => {
|
||||
const index = getIndexFromId(checkboxId);
|
||||
const option = options[index];
|
||||
if (option.selected) {
|
||||
onRemove(option.value);
|
||||
return;
|
||||
}
|
||||
onSelect(option.value);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiCheckboxGroup
|
||||
data-test-subj="checkbox-group"
|
||||
className="documentsSearchExperience__facet"
|
||||
legend={{ children: label }}
|
||||
options={checkboxGroupOptions}
|
||||
idToSelectedMap={idToSelectedMap}
|
||||
onChange={onChange}
|
||||
compressed={true}
|
||||
/>
|
||||
{showMore && (
|
||||
<>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiFlexGroup direction="row" justifyContent="center">
|
||||
<EuiButtonEmpty
|
||||
data-test-subj="more"
|
||||
onClick={onMoreClick}
|
||||
iconSide="right"
|
||||
iconType="arrowDown"
|
||||
size="xs"
|
||||
>
|
||||
{i18n.translate(
|
||||
'xpack.enterpriseSearch.appSearch.documents.search.multiCheckboxFacetsView.showMore',
|
||||
{
|
||||
defaultMessage: 'Show more',
|
||||
}
|
||||
)}
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexGroup>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -7208,8 +7208,6 @@
|
|||
"xpack.enterpriseSearch.appSearch.documents.search.indexingGuide": "インデックスガイドをお読みください",
|
||||
"xpack.enterpriseSearch.appSearch.documents.search.noResults": "「{resultSearchTerm}」の結果がありません。",
|
||||
"xpack.enterpriseSearch.appSearch.documents.search.placeholder": "ドキュメントのフィルター...",
|
||||
"xpack.enterpriseSearch.appSearch.documents.search.recentlyUploadedAsc": "最近アップロードされたドキュメント(昇順)",
|
||||
"xpack.enterpriseSearch.appSearch.documents.search.recentlyUploadedDesc": "最近アップロードされたドキュメント(降順)",
|
||||
"xpack.enterpriseSearch.appSearch.documents.search.resultsPerPage.ariaLabel": "1 ページに表示する結果数",
|
||||
"xpack.enterpriseSearch.appSearch.documents.search.resultsPerPage.show": "表示:",
|
||||
"xpack.enterpriseSearch.appSearch.documents.search.sortBy": "並べ替え基準",
|
||||
|
|
|
@ -7227,8 +7227,6 @@
|
|||
"xpack.enterpriseSearch.appSearch.documents.search.indexingGuide": "请阅读索引指南",
|
||||
"xpack.enterpriseSearch.appSearch.documents.search.noResults": "还没有匹配“{resultSearchTerm}”的结果!",
|
||||
"xpack.enterpriseSearch.appSearch.documents.search.placeholder": "筛选文档......",
|
||||
"xpack.enterpriseSearch.appSearch.documents.search.recentlyUploadedAsc": "最近上传(升序)",
|
||||
"xpack.enterpriseSearch.appSearch.documents.search.recentlyUploadedDesc": "最近上传(降序)",
|
||||
"xpack.enterpriseSearch.appSearch.documents.search.resultsPerPage.ariaLabel": "每页要显示的结果数",
|
||||
"xpack.enterpriseSearch.appSearch.documents.search.resultsPerPage.show": "显示:",
|
||||
"xpack.enterpriseSearch.appSearch.documents.search.sortBy": "排序依据",
|
||||
|
|
Loading…
Reference in a new issue