diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result.scss b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result.scss index 3132894ddc7a14..93bace1d77775c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result.scss +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result.scss @@ -1,10 +1,10 @@ .appSearchResult { display: grid; - grid-template-columns: auto 1fr auto; - grid-template-rows: auto 1fr auto; + grid-template-columns: auto 1fr; + grid-template-rows: auto 1fr; grid-template-areas: - 'drag content actions' - 'drag toggle actions'; + 'drag content' + 'drag toggle'; overflow: hidden; // Prevents child background-colors from clipping outside of panel border-radius border: $euiBorderThin; // TODO: Remove after EUI version is bumped beyond 31.8.0 @@ -35,29 +35,6 @@ } } - &__actionButtons { - grid-area: actions; - display: flex; - flex-wrap: no-wrap; - } - - &__actionButton { - display: flex; - justify-content: center; - align-items: center; - width: $euiSize * 2; - border-left: $euiBorderThin; - - &:first-child { - border-left: none; - } - - &:hover, - &:focus { - background-color: $euiPageBackgroundColor; - } - } - &__dragHandle { grid-area: drag; display: flex; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result.test.tsx index 3e83717bf93559..ba9944744e5c79 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result.test.tsx @@ -5,12 +5,14 @@ * 2.0. */ +import { mockKibanaValues } from '../../../__mocks__'; + import React from 'react'; import { DraggableProvidedDragHandleProps } from 'react-beautiful-dnd'; import { shallow, ShallowWrapper } from 'enzyme'; -import { EuiButtonIcon, EuiPanel, EuiButtonIconColor } from '@elastic/eui'; +import { EuiPanel } from '@elastic/eui'; import { SchemaTypes } from '../../../shared/types'; @@ -63,18 +65,28 @@ describe('Result', () => { ]); }); - it('renders a header', () => { - const wrapper = shallow(); - const header = wrapper.find(ResultHeader); - expect(header.exists()).toBe(true); - expect(header.prop('isMetaEngine')).toBe(true); // passed through from props - expect(header.prop('showScore')).toBe(true); // passed through from props - expect(header.prop('shouldLinkToDetailPage')).toBe(false); // passed through from props - expect(header.prop('resultMeta')).toEqual({ - id: '1', - score: 100, - engine: 'my-engine', - }); // passed through from meta in result prop + describe('header', () => { + it('renders a header', () => { + const wrapper = shallow(); + const header = wrapper.find(ResultHeader); + + expect(header.exists()).toBe(true); + expect(header.prop('isMetaEngine')).toBe(true); // passed through from props + expect(header.prop('showScore')).toBe(true); // passed through from props + expect(header.prop('resultMeta')).toEqual({ + id: '1', + score: 100, + engine: 'my-engine', + }); // passed through from meta in result prop + expect(header.prop('documentLink')).toBe(undefined); // based on shouldLinkToDetailPage prop + }); + + it('passes documentLink when shouldLinkToDetailPage is true', () => { + const wrapper = shallow(); + const header = wrapper.find(ResultHeader); + + expect(header.prop('documentLink')).toBe('/engines/my-engine/documents/1'); + }); }); describe('actions', () => { @@ -83,53 +95,30 @@ describe('Result', () => { title: 'Hide', onClick: jest.fn(), iconType: 'eyeClosed', - iconColor: 'danger' as EuiButtonIconColor, }, { title: 'Bookmark', onClick: jest.fn(), iconType: 'starFilled', - iconColor: undefined, }, ]; - it('will render an action button in the header for each action passed', () => { + it('passes actions to the header', () => { const wrapper = shallow(); - const header = wrapper.find(ResultHeader); - const renderedActions = shallow(header.prop('actions') as any); - const buttons = renderedActions.find(EuiButtonIcon); - expect(buttons).toHaveLength(2); - - expect(buttons.first().prop('iconType')).toEqual('eyeClosed'); - expect(buttons.first().prop('color')).toEqual('danger'); - buttons.first().simulate('click'); - expect(actions[0].onClick).toHaveBeenCalled(); - - expect(buttons.last().prop('iconType')).toEqual('starFilled'); - // Note that no iconColor was passed so it was defaulted to primary - expect(buttons.last().prop('color')).toEqual('primary'); - buttons.last().simulate('click'); - expect(actions[1].onClick).toHaveBeenCalled(); + expect(wrapper.find(ResultHeader).prop('actions')).toEqual(actions); }); - it('will render a document detail link as the first action if shouldLinkToDetailPage is passed', () => { + it('adds a link action to the start of the actions array if shouldLinkToDetailPage is passed', () => { const wrapper = shallow(); - const header = wrapper.find(ResultHeader); - const renderedActions = shallow(header.prop('actions') as any); - const buttons = renderedActions.find(EuiButtonIcon); - // In addition to the 2 actions passed, we also have a link action - expect(buttons).toHaveLength(3); + const passedActions = wrapper.find(ResultHeader).prop('actions'); + expect(passedActions.length).toEqual(3); // In addition to the 2 actions passed, we also have a link action - expect(buttons.first().prop('data-test-subj')).toEqual('DocumentDetailLink'); - }); + const linkAction = passedActions[0]; + expect(linkAction.title).toEqual('Visit document details'); - it('will not render anything if no actions are passed and shouldLinkToDetailPage is false', () => { - const wrapper = shallow(); - const header = wrapper.find(ResultHeader); - const renderedActions = shallow(header.prop('actions') as any); - const buttons = renderedActions.find(EuiButtonIcon); - expect(buttons).toHaveLength(0); + linkAction.onClick(); + expect(mockKibanaValues.navigateToUrl).toHaveBeenCalledWith('/engines/my-engine/documents/1'); }); }); @@ -148,9 +137,7 @@ describe('Result', () => { }); it('will render field details with type highlights if schemaForTypeHighlights has been provided', () => { - const wrapper = shallow( - - ); + const wrapper = shallow(); expect(wrapper.find(ResultField).map((rf) => rf.prop('type'))).toEqual([ 'text', 'text', diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result.tsx index 71d9f39d802d5b..d9c16a877dc59a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result.tsx @@ -10,12 +10,11 @@ import { DraggableProvidedDragHandleProps } from 'react-beautiful-dnd'; import './result.scss'; -import { EuiButtonIcon, EuiPanel, EuiFlexGroup, EuiFlexItem, EuiIcon } from '@elastic/eui'; +import { EuiPanel, EuiIcon } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { ReactRouterHelper } from '../../../shared/react_router_helpers/eui_components'; - +import { KibanaLogic } from '../../../shared/kibana'; import { Schema } from '../../../shared/types'; import { ENGINE_DOCUMENT_DETAIL_PATH } from '../../routes'; @@ -56,48 +55,27 @@ export const Result: React.FC = ({ [result] ); const numResults = resultFields.length; - const documentLink = generateEncodedPath(ENGINE_DOCUMENT_DETAIL_PATH, { - engineName: resultMeta.engine, - documentId: resultMeta.id, - }); const typeForField = (fieldName: string) => { if (schemaForTypeHighlights) return schemaForTypeHighlights[fieldName]; }; - const ResultActions = () => { - if (!shouldLinkToDetailPage && !actions.length) return null; - return ( - - - {shouldLinkToDetailPage && ( - - - - - - )} - {actions.map(({ onClick, title, iconType, iconColor }) => ( - - - - ))} - - - ); - }; + const documentLink = shouldLinkToDetailPage + ? generateEncodedPath(ENGINE_DOCUMENT_DETAIL_PATH, { + engineName: resultMeta.engine, + documentId: resultMeta.id, + }) + : undefined; + if (shouldLinkToDetailPage && documentLink) { + const linkAction = { + onClick: () => KibanaLogic.values.navigateToUrl(documentLink), + title: i18n.translate('xpack.enterpriseSearch.appSearch.result.documentDetailLink', { + defaultMessage: 'Visit document details', + }), + iconType: 'eye', + }; + actions = [linkAction, ...actions]; + } return ( = ({ resultMeta={resultMeta} showScore={!!showScore} isMetaEngine={isMetaEngine} - shouldLinkToDetailPage={shouldLinkToDetailPage} - actions={} + documentLink={documentLink} + actions={actions} /> {resultFields .slice(0, isOpen ? resultFields.length : RESULT_CUTOFF) diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_actions.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_actions.test.tsx new file mode 100644 index 00000000000000..4aae1e07f0f8c5 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_actions.test.tsx @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { EuiButtonIcon, EuiButtonIconColor } from '@elastic/eui'; + +import { ResultActions } from './result_actions'; + +describe('ResultActions', () => { + const actions = [ + { + title: 'Hide', + onClick: jest.fn(), + iconType: 'eyeClosed', + iconColor: 'danger' as EuiButtonIconColor, + }, + { + title: 'Bookmark', + onClick: jest.fn(), + iconType: 'starFilled', + iconColor: undefined, + }, + ]; + + const wrapper = shallow(); + const buttons = wrapper.find(EuiButtonIcon); + + it('renders an action button for each action passed', () => { + expect(buttons).toHaveLength(2); + }); + + it('passes icon props correctly', () => { + expect(buttons.first().prop('iconType')).toEqual('eyeClosed'); + expect(buttons.first().prop('color')).toEqual('danger'); + + expect(buttons.last().prop('iconType')).toEqual('starFilled'); + // Note that no iconColor was passed so it was defaulted to primary + expect(buttons.last().prop('color')).toEqual('primary'); + }); + + it('passes click events', () => { + buttons.first().simulate('click'); + expect(actions[0].onClick).toHaveBeenCalled(); + + buttons.last().simulate('click'); + expect(actions[1].onClick).toHaveBeenCalled(); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_actions.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_actions.tsx new file mode 100644 index 00000000000000..52fbee90fe31af --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_actions.tsx @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; + +import { ResultAction } from './types'; + +interface Props { + actions: ResultAction[]; +} + +export const ResultActions: React.FC = ({ actions }) => { + return ( + + {actions.map(({ onClick, title, iconType, iconColor }) => ( + + + + ))} + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_header.scss b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_header.scss index cd1042998dd347..ebae11ee8ad330 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_header.scss +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_header.scss @@ -1,26 +1,3 @@ .appSearchResultHeader { - display: flex; - margin-bottom: $euiSizeS; - - @include euiBreakpoint('xs') { - flex-direction: column; - } - - &__column { - display: flex; - flex-wrap: wrap; - - @include euiBreakpoint('xs') { - flex-direction: column; - } - - & + &, - .appSearchResultHeaderItem + .appSearchResultHeaderItem { - margin-left: $euiSizeL; - - @include euiBreakpoint('xs') { - margin-left: 0; - } - } - } + margin-bottom: $euiSizeM; } diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_header.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_header.test.tsx index 80cff9b96a3cad..cdd43c3efd97a9 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_header.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_header.test.tsx @@ -9,6 +9,7 @@ import React from 'react'; import { shallow } from 'enzyme'; +import { ResultActions } from './result_actions'; import { ResultHeader } from './result_header'; describe('ResultHeader', () => { @@ -17,30 +18,27 @@ describe('ResultHeader', () => { score: 100, engine: 'my-engine', }; + const props = { + showScore: false, + isMetaEngine: false, + resultMeta, + actions: [], + }; it('renders', () => { - const wrapper = shallow( - - ); + const wrapper = shallow(); expect(wrapper.isEmptyRender()).toBe(false); }); it('always renders an id', () => { - const wrapper = shallow( - - ); + const wrapper = shallow(); expect(wrapper.find('[data-test-subj="ResultId"]').prop('value')).toEqual('1'); expect(wrapper.find('[data-test-subj="ResultId"]').prop('href')).toBeUndefined(); }); - it('renders id as a link if shouldLinkToDetailPage is true', () => { + it('renders id as a link if a documentLink has been passed', () => { const wrapper = shallow( - + ); expect(wrapper.find('[data-test-subj="ResultId"]').prop('value')).toEqual('1'); expect(wrapper.find('[data-test-subj="ResultId"]').prop('href')).toEqual( @@ -50,47 +48,39 @@ describe('ResultHeader', () => { describe('score', () => { it('renders score if showScore is true ', () => { - const wrapper = shallow( - - ); + const wrapper = shallow(); expect(wrapper.find('[data-test-subj="ResultScore"]').prop('value')).toEqual(100); }); it('does not render score if showScore is false', () => { - const wrapper = shallow( - - ); + const wrapper = shallow(); expect(wrapper.find('[data-test-subj="ResultScore"]').exists()).toBe(false); }); }); describe('engine', () => { it('renders engine name if this is a meta engine', () => { - const wrapper = shallow( - - ); + const wrapper = shallow(); expect(wrapper.find('[data-test-subj="ResultEngine"]').prop('value')).toBe('my-engine'); }); it('does not render an engine if this is not a meta engine', () => { - const wrapper = shallow( - - ); + const wrapper = shallow(); expect(wrapper.find('[data-test-subj="ResultEngine"]').exists()).toBe(false); }); }); + + describe('actions', () => { + const actions = [{ title: 'View document', onClick: () => {}, iconType: 'eye' }]; + + it('renders ResultActions if actions have been passed', () => { + const wrapper = shallow(); + expect(wrapper.find(ResultActions).exists()).toBe(true); + }); + + it('does not render ResultActions if no actions are passed', () => { + const wrapper = shallow(); + expect(wrapper.find(ResultActions).exists()).toBe(false); + }); + }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_header.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_header.tsx index 93a684b1968a2e..f577b481b39cf3 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_header.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_header.tsx @@ -9,11 +9,9 @@ import React from 'react'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { ENGINE_DOCUMENT_DETAIL_PATH } from '../../routes'; -import { generateEncodedPath } from '../../utils/encode_path_params'; - +import { ResultActions } from './result_actions'; import { ResultHeaderItem } from './result_header_item'; -import { ResultMeta } from './types'; +import { ResultMeta, ResultAction } from './types'; import './result_header.scss'; @@ -21,8 +19,8 @@ interface Props { showScore: boolean; isMetaEngine: boolean; resultMeta: ResultMeta; - actions?: React.ReactNode; - shouldLinkToDetailPage?: boolean; + actions: ResultAction[]; + documentLink?: string; } export const ResultHeader: React.FC = ({ @@ -30,19 +28,20 @@ export const ResultHeader: React.FC = ({ resultMeta, isMetaEngine, actions, - shouldLinkToDetailPage = false, + documentLink, }) => { - const documentLink = generateEncodedPath(ENGINE_DOCUMENT_DETAIL_PATH, { - engineName: resultMeta.engine, - documentId: resultMeta.id, - }); - return ( -
- +
+ = ({ /> )} - {actions} + {actions.length > 0 && ( + + + + )}
); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_header_item.scss b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_header_item.scss index df3e2ec2411065..94367ae634b7c8 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_header_item.scss +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_header_item.scss @@ -1,12 +1,12 @@ -.euiFlexItem:not(:first-child):not(:last-child) .appSearchResultHeaderItem { - padding-right: .75rem; - box-shadow: inset -1px 0 0 0 $euiBorderColor; -} - .appSearchResultHeaderItem { @include euiCodeFont; &__score { color: $euiColorSuccessText; } + + .euiFlexItem:not(:first-child):not(:last-child) & { + padding-right: $euiSizeS; + box-shadow: inset (-$euiBorderWidthThin) 0 0 0 $euiBorderColor; + } } diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_header_item.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_header_item.test.tsx index e0407b4db7f251..d45eb8856d118b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_header_item.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_header_item.test.tsx @@ -69,7 +69,7 @@ describe('ResultHeaderItem', () => { const wrapper = shallow( ); - expect(wrapper.find('ReactRouterHelper').exists()).toBe(true); - expect(wrapper.find('ReactRouterHelper').prop('to')).toBe('http://www.example.com'); + expect(wrapper.find('EuiLinkTo').exists()).toBe(true); + expect(wrapper.find('EuiLinkTo').prop('to')).toBe('http://www.example.com'); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_header_item.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_header_item.tsx index 545b85c17a529d..cf3b385fd92577 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_header_item.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_header_item.tsx @@ -9,7 +9,7 @@ import React from 'react'; import './result_header_item.scss'; -import { ReactRouterHelper } from '../../../shared/react_router_helpers/eui_components'; +import { EuiLinkTo } from '../../../shared/react_router_helpers/eui_components'; import { TruncatedContent } from '../../../shared/truncate'; @@ -48,11 +48,9 @@ export const ResultHeaderItem: React.FC = ({ field, type, value, href })   {href ? ( - - - - - + + + ) : ( )}