From 15f05b51ff2b6a732b6b28f9af82f3e14a60665c Mon Sep 17 00:00:00 2001 From: Jason Stoltzfus Date: Wed, 20 Jan 2021 14:57:37 -0500 Subject: [PATCH] [App Search] Wired up configurable Sort and Facets in Documents View (#88764) --- .../build_search_ui_config.test.ts | 55 ++++++--- .../build_search_ui_config.ts | 13 +- .../build_sort_options.test.ts | 63 ++++++++++ .../search_experience/build_sort_options.ts | 29 +++++ .../documents/search_experience/constants.ts | 25 ++++ .../search_experience/search_experience.scss | 8 ++ .../search_experience.test.tsx | 78 +++++++++--- .../search_experience/search_experience.tsx | 69 ++++++++--- .../documents/search_experience/types.ts | 18 +++ .../search_experience/views/index.ts | 1 + .../views/multi_checkbox_facets_view.test.tsx | 99 +++++++++++++++ .../views/multi_checkbox_facets_view.tsx | 114 ++++++++++++++++++ .../translations/translations/ja-JP.json | 2 - .../translations/translations/zh-CN.json | 2 - 14 files changed, 524 insertions(+), 52 deletions(-) create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/build_sort_options.test.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/build_sort_options.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/constants.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/types.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/views/multi_checkbox_facets_view.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/views/multi_checkbox_facets_view.tsx diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/build_search_ui_config.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/build_search_ui_config.test.ts index dd52f6b8227b..a87b73bd4e14 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/build_search_ui_config.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/build_search_ui_config.test.ts @@ -15,24 +15,49 @@ 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({ - bar: { - raw: {}, - snippet: { - fallback: true, - size: 300, - }, - }, - foo: { - raw: {}, - snippet: { - fallback: true, - size: 300, + 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: { + fallback: true, + size: 300, + }, + }, + foo: { + raw: {}, + snippet: { + fallback: true, + size: 300, + }, + }, }, }, + trackUrlState: false, }); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/build_search_ui_config.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/build_search_ui_config.ts index 78e1fa9e7f3a..ac10e2db7191 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/build_search_ui_config.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/build_search_ui_config.ts @@ -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: { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/build_sort_options.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/build_sort_options.test.ts new file mode 100644 index 000000000000..e95f91f6f6f8 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/build_sort_options.test.ts @@ -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', + }, + ]); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/build_sort_options.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/build_sort_options.ts new file mode 100644 index 000000000000..8b80e557e4b6 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/build_sort_options.ts @@ -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; +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/constants.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/constants.ts new file mode 100644 index 000000000000..1c7c3f5a65bd --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/constants.ts @@ -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 }, + } + ); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience.scss b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience.scss index ba9931dc90fd..d2e0a8155fa5 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience.scss +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience.scss @@ -25,4 +25,12 @@ background-color: $euiPageBackgroundColor; padding: $euiSizeL; } + + .documentsSearchExperience__facet { + line-height: 0; + + .euiCheckbox__label { + @include euiTextTruncate; + } + } } diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience.test.tsx index 5df132a27bbb..410a4ea5bab7 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience.test.tsx @@ -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(); + }); + + 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(); + }); + + 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(); + expect(wrapper.find(Facet).length).toBe(3); + }); + describe('customization modal', () => { it('has a customization modal which can be opened and closed', () => { const wrapper = shallow(); 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(); - 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(); - wrapper.find(CustomizationCallout).simulate('click'); + wrapper.find('[data-test-subj="customize"]').simulate('click'); wrapper.find(CustomizationModal).prop('onSave')({ filterFields: ['new', 'filters'], sortFields: ['new', 'sorts'], diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience.tsx index e80ab2e18b2d..d829042bef11 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience.tsx @@ -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( `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 (
@@ -101,7 +105,36 @@ export const SearchExperience: React.FC = () => { view={SortingView} /> - + {fields.filterFields.length > 0 ? ( + <> + {fields.filterFields.map((fieldName) => ( +
+ + +
+ ))} + + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.documents.search.customizationButton', + { + defaultMessage: 'Customize filters and sort', + } + )} + + + ) : ( + + )} diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/types.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/types.ts new file mode 100644 index 000000000000..0cde0f94b773 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/types.ts @@ -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; +} diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/views/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/views/index.ts index 8c88fc81d3a3..7032fa1a9a06 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/views/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/views/index.ts @@ -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'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/views/multi_checkbox_facets_view.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/views/multi_checkbox_facets_view.test.tsx new file mode 100644 index 000000000000..7f43ca12652c --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/views/multi_checkbox_facets_view.test.tsx @@ -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(); + expect(wrapper.isEmptyRender()).toBe(false); + }); + + it('calls onMoreClick when more button is clicked', () => { + const wrapper = shallow(); + wrapper.find('[data-test-subj="more"]').simulate('click'); + expect(props.onMoreClick).toHaveBeenCalled(); + }); + + it('calls onSelect when an option is selected', () => { + const wrapper = shallow(); + 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( + + ); + 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( + + ); + 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: '' }, + ]); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/views/multi_checkbox_facets_view.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/views/multi_checkbox_facets_view.tsx new file mode 100644 index 000000000000..df61e6e3dcc0 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/views/multi_checkbox_facets_view.tsx @@ -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 = ({ + 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: '', + } + ), + }); + + 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 ( + <> + + {showMore && ( + <> + + + + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.documents.search.multiCheckboxFacetsView.showMore', + { + defaultMessage: 'Show more', + } + )} + + + + )} + + ); +}; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 07befe8a26b2..1bbf4b803375 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -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": "並べ替え基準", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 87af04f7dec8..51205a3420be 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -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": "排序依据",