Skip to content

Commit

Permalink
[Curation] Add Result Manually flyout/search (#94887) (#94991)
Browse files Browse the repository at this point in the history
* Set up curation search server route

- not really sure which API endpoint to use, hedging my bets

* Set up AddResultLogic

- fairly simple, mostly concerned with flyout behavior & search query
- could likely be reused (or replaced with??) query tester logic in the future

* Add main AddResultFlyout component

- with custom isPromoted / isHidden logic & actions

* Update AddResultButton to open flyout

* Update Curation page to render the flyout

* PR feedback: reset search query on flyout re-open

Co-authored-by: Constance <[email protected]>
  • Loading branch information
kibanamachine and Constance authored Mar 19, 2021
1 parent 7946d27 commit 290f0cd
Show file tree
Hide file tree
Showing 11 changed files with 566 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ import { Loading } from '../../../../shared/loading';
jest.mock('./curation_logic', () => ({ CurationLogic: jest.fn() }));
import { CurationLogic } from './curation_logic';

import { AddResultFlyout } from './results';

import { Curation } from './';

describe('Curation', () => {
Expand All @@ -31,6 +33,7 @@ describe('Curation', () => {
const values = {
dataLoading: false,
queries: ['query A', 'query B'],
isFlyoutOpen: false,
};
const actions = {
loadCuration: jest.fn(),
Expand Down Expand Up @@ -60,6 +63,13 @@ describe('Curation', () => {
expect(wrapper.find(Loading)).toHaveLength(1);
});

it('renders the add result flyout when open', () => {
setMockValues({ ...values, isFlyoutOpen: true });
const wrapper = shallow(<Curation {...props} />);

expect(wrapper.find(AddResultFlyout)).toHaveLength(1);
});

it('initializes CurationLogic with a curationId prop from URL param', () => {
(useParams as jest.Mock).mockReturnValueOnce({ curationId: 'hello-world' });
shallow(<Curation {...props} />);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { MANAGE_CURATION_TITLE, RESTORE_CONFIRMATION } from '../constants';
import { CurationLogic } from './curation_logic';
import { PromotedDocuments, OrganicDocuments, HiddenDocuments } from './documents';
import { ActiveQuerySelect, ManageQueriesModal } from './queries';
import { AddResultLogic, AddResultFlyout } from './results';

interface Props {
curationsBreadcrumb: BreadcrumbTrail;
Expand All @@ -32,6 +33,7 @@ export const Curation: React.FC<Props> = ({ curationsBreadcrumb }) => {
const { curationId } = useParams() as { curationId: string };
const { loadCuration, resetCuration } = useActions(CurationLogic({ curationId }));
const { dataLoading, queries } = useValues(CurationLogic({ curationId }));
const { isFlyoutOpen } = useValues(AddResultLogic);

useEffect(() => {
loadCuration();
Expand Down Expand Up @@ -77,7 +79,7 @@ export const Curation: React.FC<Props> = ({ curationsBreadcrumb }) => {
<EuiSpacer />
<HiddenDocuments />

{/* TODO: AddResult flyout */}
{isFlyoutOpen && <AddResultFlyout />}
</>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
* 2.0.
*/

import { setMockActions } from '../../../../../__mocks__';

import React from 'react';

import { shallow, ShallowWrapper } from 'enzyme';
Expand All @@ -14,9 +16,14 @@ import { EuiButton } from '@elastic/eui';
import { AddResultButton } from './';

describe('AddResultButton', () => {
const actions = {
openFlyout: jest.fn(),
};

let wrapper: ShallowWrapper;

beforeAll(() => {
setMockActions(actions);
wrapper = shallow(<AddResultButton />);
});

Expand All @@ -26,6 +33,6 @@ describe('AddResultButton', () => {

it('opens the add result flyout on click', () => {
wrapper.find(EuiButton).simulate('click');
// TODO: assert on logic action
expect(actions.openFlyout).toHaveBeenCalled();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,18 @@

import React from 'react';

import { useActions } from 'kea';

import { EuiButton } from '@elastic/eui';
import { i18n } from '@kbn/i18n';

import { AddResultLogic } from './';

export const AddResultButton: React.FC = () => {
const { openFlyout } = useActions(AddResultLogic);

return (
<EuiButton onClick={() => {} /* TODO */} iconType="plusInCircle" size="s" fill>
<EuiButton onClick={openFlyout} iconType="plusInCircle" size="s" fill>
{i18n.translate('xpack.enterpriseSearch.appSearch.engine.curations.addResult.buttonLabel', {
defaultMessage: 'Add result manually',
})}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
/*
* 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 { setMockActions, setMockValues } from '../../../../../__mocks__';

import React from 'react';

import { shallow } from 'enzyme';

import { EuiFlyout, EuiFieldSearch, EuiEmptyPrompt } from '@elastic/eui';

import { CurationResult, AddResultFlyout } from './';

describe('AddResultFlyout', () => {
const values = {
dataLoading: false,
searchQuery: '',
searchResults: [],
promotedIds: [],
hiddenIds: [],
};
const actions = {
search: jest.fn(),
closeFlyout: jest.fn(),
addPromotedId: jest.fn(),
removePromotedId: jest.fn(),
addHiddenId: jest.fn(),
removeHiddenId: jest.fn(),
};

beforeEach(() => {
jest.clearAllMocks();
setMockValues(values);
setMockActions(actions);
});

it('renders a closeable flyout', () => {
const wrapper = shallow(<AddResultFlyout />);
expect(wrapper.find(EuiFlyout)).toHaveLength(1);

wrapper.find(EuiFlyout).simulate('close');
expect(actions.closeFlyout).toHaveBeenCalled();
});

describe('search input', () => {
it('renders isLoading state correctly', () => {
setMockValues({ ...values, dataLoading: true });
const wrapper = shallow(<AddResultFlyout />);

expect(wrapper.find(EuiFieldSearch).prop('isLoading')).toEqual(true);
});

it('renders value correctly', () => {
setMockValues({ ...values, searchQuery: 'hello world' });
const wrapper = shallow(<AddResultFlyout />);

expect(wrapper.find(EuiFieldSearch).prop('value')).toEqual('hello world');
});

it('calls search on input change', () => {
const wrapper = shallow(<AddResultFlyout />);
wrapper.find(EuiFieldSearch).simulate('change', { target: { value: 'lorem ipsum' } });

expect(actions.search).toHaveBeenCalledWith('lorem ipsum');
});
});

describe('search results', () => {
it('renders an empty state', () => {
setMockValues({ ...values, searchResults: [] });
const wrapper = shallow(<AddResultFlyout />);

expect(wrapper.find(EuiEmptyPrompt)).toHaveLength(1);
expect(wrapper.find(CurationResult)).toHaveLength(0);
});

it('renders a result component for each item in searchResults', () => {
setMockValues({
...values,
searchResults: [
{ id: { raw: 'doc-1' } },
{ id: { raw: 'doc-2' } },
{ id: { raw: 'doc-3' } },
],
});
const wrapper = shallow(<AddResultFlyout />);

expect(wrapper.find(CurationResult)).toHaveLength(3);
});

describe('actions', () => {
it('renders a hide result button if the document ID is not already in the hiddenIds list', () => {
setMockValues({
...values,
searchResults: [{ id: { raw: 'visible-document' } }],
hiddenIds: ['hidden-document'],
});
const wrapper = shallow(<AddResultFlyout />);
wrapper.find(CurationResult).prop('actions')[0].onClick();

expect(actions.addHiddenId).toHaveBeenCalledWith('visible-document');
});

it('renders a show result button if the document ID is already in the hiddenIds list', () => {
setMockValues({
...values,
searchResults: [{ id: { raw: 'hidden-document' } }],
hiddenIds: ['hidden-document'],
});
const wrapper = shallow(<AddResultFlyout />);
wrapper.find(CurationResult).prop('actions')[0].onClick();

expect(actions.removeHiddenId).toHaveBeenCalledWith('hidden-document');
});

it('renders a promote result button if the document ID is not already in the promotedIds list', () => {
setMockValues({
...values,
searchResults: [{ id: { raw: 'some-document' } }],
promotedIds: ['promoted-document'],
});
const wrapper = shallow(<AddResultFlyout />);
wrapper.find(CurationResult).prop('actions')[1].onClick();

expect(actions.addPromotedId).toHaveBeenCalledWith('some-document');
});

it('renders a demote result button if the document ID is already in the promotedIds list', () => {
setMockValues({
...values,
searchResults: [{ id: { raw: 'promoted-document' } }],
promotedIds: ['promoted-document'],
});
const wrapper = shallow(<AddResultFlyout />);
wrapper.find(CurationResult).prop('actions')[1].onClick();

expect(actions.removePromotedId).toHaveBeenCalledWith('promoted-document');
});
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
/*
* 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 { useValues, useActions } from 'kea';

import {
EuiPortal,
EuiFlyout,
EuiFlyoutHeader,
EuiFlyoutBody,
EuiTitle,
EuiText,
EuiSpacer,
EuiFieldSearch,
EuiEmptyPrompt,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';

import { FlashMessages } from '../../../../../shared/flash_messages';

import {
RESULT_ACTIONS_DIRECTIONS,
PROMOTE_DOCUMENT_ACTION,
DEMOTE_DOCUMENT_ACTION,
HIDE_DOCUMENT_ACTION,
SHOW_DOCUMENT_ACTION,
} from '../../constants';
import { CurationLogic } from '../curation_logic';

import { AddResultLogic, CurationResult } from './';

export const AddResultFlyout: React.FC = () => {
const { searchQuery, searchResults, dataLoading } = useValues(AddResultLogic);
const { search, closeFlyout } = useActions(AddResultLogic);

const { promotedIds, hiddenIds } = useValues(CurationLogic);
const { addPromotedId, removePromotedId, addHiddenId, removeHiddenId } = useActions(
CurationLogic
);

return (
<EuiPortal>
<EuiFlyout ownFocus onClose={closeFlyout} aria-labelledby="addResultFlyout">
<EuiFlyoutHeader hasBorder>
<EuiTitle size="m">
<h2 id="addResultFlyout">
{i18n.translate('xpack.enterpriseSearch.appSearch.engine.curations.addResult.title', {
defaultMessage: 'Add result to curation',
})}
</h2>
</EuiTitle>
<EuiText color="subdued">
<p>{RESULT_ACTIONS_DIRECTIONS}</p>
</EuiText>
</EuiFlyoutHeader>
<EuiFlyoutBody banner={<FlashMessages />}>
<EuiFieldSearch
value={searchQuery}
onChange={(e) => search(e.target.value)}
isLoading={dataLoading}
placeholder={i18n.translate(
'xpack.enterpriseSearch.appSearch.engine.curations.addResult.searchPlaceholder',
{ defaultMessage: 'Search engine documents' }
)}
fullWidth
autoFocus
/>
<EuiSpacer />

{searchResults.length > 0 ? (
searchResults.map((result) => {
const id = result.id.raw;
const isPromoted = promotedIds.includes(id);
const isHidden = hiddenIds.includes(id);

return (
<CurationResult
key={id}
result={result}
actions={[
isHidden
? {
...SHOW_DOCUMENT_ACTION,
onClick: () => removeHiddenId(id),
}
: {
...HIDE_DOCUMENT_ACTION,
onClick: () => addHiddenId(id),
},
isPromoted
? {
...DEMOTE_DOCUMENT_ACTION,
onClick: () => removePromotedId(id),
}
: {
...PROMOTE_DOCUMENT_ACTION,
onClick: () => addPromotedId(id),
},
]}
/>
);
})
) : (
<EuiEmptyPrompt
body={i18n.translate(
'xpack.enterpriseSearch.appSearch.engine.curations.addResult.searchEmptyDescription',
{ defaultMessage: 'No matching content found.' }
)}
/>
)}
</EuiFlyoutBody>
</EuiFlyout>
</EuiPortal>
);
};
Loading

0 comments on commit 290f0cd

Please sign in to comment.