From 41cc0a75a37fdac4355f76b907d9aa6be41a389d Mon Sep 17 00:00:00 2001 From: Mingze Xiao Date: Mon, 16 Dec 2019 17:04:32 -0800 Subject: [PATCH] feat(archive-viewer): Add search --- package.json | 1 + src/lib/viewers/archive/ArchiveExplorer.js | 68 ++++++++++++++++--- src/lib/viewers/archive/ArchiveExplorer.scss | 4 ++ .../__tests__/ArchiveExplorer-test-react.js | 40 ++++++++++- yarn.lock | 5 ++ 5 files changed, 104 insertions(+), 14 deletions(-) diff --git a/package.json b/package.json index ffb60265f..ca91bcb71 100644 --- a/package.json +++ b/package.json @@ -55,6 +55,7 @@ "eslint-plugin-react-hooks": "^1.7.0", "file-loader": "^4.1.0", "fscreen": "^1.0.2", + "fuzzysearch": "^1.0.3", "husky": "^3.0.5", "i18n-webpack-plugin": "^1.0.0", "jsuri": "^1.3.1", diff --git a/src/lib/viewers/archive/ArchiveExplorer.js b/src/lib/viewers/archive/ArchiveExplorer.js index 894bff8ad..e3d5244c6 100644 --- a/src/lib/viewers/archive/ArchiveExplorer.js +++ b/src/lib/viewers/archive/ArchiveExplorer.js @@ -1,9 +1,11 @@ import * as React from 'react'; import PropTypes from 'prop-types'; import getProp from 'lodash/get'; +import fuzzysearch from 'fuzzysearch'; import elementsMessages from 'box-elements-messages'; // eslint-disable-line import intlLocaleData from 'react-intl-locale-data'; // eslint-disable-line import Internationalize from 'box-ui-elements/es/elements/common/Internationalize'; +import SearchBar from 'box-ui-elements/es/elements/common/header'; import { readableTimeCellRenderer, sizeCellRenderer, @@ -18,6 +20,7 @@ import './ArchiveExplorer.scss'; const language = __LANGUAGE__; // eslint-disable-line const { KEY_NAME, KEY_MODIFIED_AT, KEY_SIZE } = TABLE_COLUMNS; +const { VIEW_FOLDER, VIEW_SEARCH } = VIEWS; class ArchiveExplorer extends React.Component { static propTypes = { @@ -65,7 +68,8 @@ class ArchiveExplorer extends React.Component { this.state = { fullPath: props.itemCollection.find(info => !info.parent).absolute_path, - view: VIEWS.VIEW_FOLDER, + searchQuery: '', + view: VIEW_FOLDER, }; } @@ -96,10 +100,11 @@ class ArchiveExplorer extends React.Component { * @return {Object} formatted data */ getRowData = itemList => ({ index }) => { - const { modified_at: modifiedAt, name, size, type, ...rest } = itemList[index]; + const { absolute_path: fullPath, modified_at: modifiedAt, name, size, type, ...rest } = itemList[index]; return { [KEY_NAME]: { + fullPath, isExternal: false, name, type, @@ -112,17 +117,12 @@ class ArchiveExplorer extends React.Component { }; /** - * Handle click event, update fullPath state + * Handle click event, update fullPath state, reset search and view * * @param {Object} cellValue - the cell being clicked * @return {void} */ - handleClick = ({ name }) => { - const { fullPath } = this.state; - this.setState({ - fullPath: `${fullPath}${name}/`, - }); - }; + handleClick = ({ fullPath }) => this.setState({ view: VIEW_FOLDER, fullPath, searchQuery: '' }); /** * Handle click event, update fullPath state @@ -132,6 +132,48 @@ class ArchiveExplorer extends React.Component { */ handleClickFullPath = fullPath => this.setState({ fullPath }); + /** + * Handle search input, update view state + * + * @param {string} query - raw query string in the search bar + * @return {void} + */ + search = query => { + const trimmedQuery = query.trim(); + + if (!query) { + this.setState({ + searchQuery: query, + view: VIEW_FOLDER, + }); + return; + } + + if (!trimmedQuery) { + this.setState({ + searchQuery: query, + }); + return; + } + + this.setState({ + searchQuery: query, + view: VIEW_SEARCH, + }); + }; + + /** + * Filter item collection for search query + * + * @param {Array} itemCollection - raw data + * @param {string} searchQuery - user input + * @return {Array} filtered items for search query + */ + getSearchResult = (itemCollection, searchQuery) => { + const trimmedQuery = searchQuery.trim(); + return itemCollection.filter(item => fuzzysearch(trimmedQuery, item.name)); + }; + /** * render data * @@ -139,12 +181,16 @@ class ArchiveExplorer extends React.Component { */ render() { const { itemCollection } = this.props; - const { fullPath, view } = this.state; - const itemList = this.getItemList(itemCollection, fullPath); + const { fullPath, searchQuery, view } = this.state; + const itemList = + view === VIEW_SEARCH + ? this.getSearchResult(itemCollection, searchQuery) + : this.getItemList(itemCollection, fullPath); return (
+ {intl => [ diff --git a/src/lib/viewers/archive/ArchiveExplorer.scss b/src/lib/viewers/archive/ArchiveExplorer.scss index 8eaac2ed4..7e1baaa43 100644 --- a/src/lib/viewers/archive/ArchiveExplorer.scss +++ b/src/lib/viewers/archive/ArchiveExplorer.scss @@ -3,4 +3,8 @@ flex-direction: column; width: 100%; height: 100%; + + .be-logo { + display: none; + } } diff --git a/src/lib/viewers/archive/__tests__/ArchiveExplorer-test-react.js b/src/lib/viewers/archive/__tests__/ArchiveExplorer-test-react.js index 2ba5a019e..d040dcd2c 100644 --- a/src/lib/viewers/archive/__tests__/ArchiveExplorer-test-react.js +++ b/src/lib/viewers/archive/__tests__/ArchiveExplorer-test-react.js @@ -2,7 +2,7 @@ import React from 'react'; import { shallow } from 'enzyme'; import sinon from 'sinon'; import ArchiveExplorer from '../ArchiveExplorer'; -import { TABLE_COLUMNS } from '../constants'; +import { TABLE_COLUMNS, VIEWS } from '../constants'; const sandbox = sinon.sandbox.create(); let data; @@ -72,6 +72,7 @@ describe('lib/viewers/archive/ArchiveExplorer', () => { const component = shallow(); expect(component.find('.bp-ArchiveExplorer').length).to.equal(1); + expect(component.find('InjectIntl(Header)').length).to.equal(1); expect(component.find('Breadcrumbs').length).to.equal(1); expect(component.find('Internationalize').length).to.equal(1); expect(component.find('InjectIntl(VirtualizedTable)').length).to.equal(1); @@ -82,9 +83,11 @@ describe('lib/viewers/archive/ArchiveExplorer', () => { it('should set state when handleClick() is called', () => { const component = shallow(); - component.instance().handleClick({ name: 'subfolder' }); + component.instance().handleClick({ fullPath: 'test/subfolder/' }); expect(component.state().fullPath).to.equal('test/subfolder/'); + expect(component.state().view).to.equal(VIEWS.VIEW_FOLDER); + expect(component.state().searchQuery).to.equal(''); }); }); @@ -105,10 +108,11 @@ describe('lib/viewers/archive/ArchiveExplorer', () => { const rowData = component.instance().getRowData(data)({ index: 0 }); const { KEY_NAME, KEY_MODIFIED_AT, KEY_SIZE } = TABLE_COLUMNS; - const { modified_at: modifiedAt, name, size, type, ...rest } = data[0]; + const { absolute_path: fullPath, modified_at: modifiedAt, name, size, type, ...rest } = data[0]; expect(rowData).to.eql({ [KEY_NAME]: { + fullPath, isExternal: false, name, type, @@ -129,4 +133,34 @@ describe('lib/viewers/archive/ArchiveExplorer', () => { expect(itemList).to.eql([data[1], data[2]]); }); }); + + describe('search()', () => { + it('should set correct state when search query is not empty', () => { + const component = shallow(); + + component.instance().search('test'); + expect(component.state().searchQuery).to.equal('test'); + expect(component.state().view).to.equal(VIEWS.VIEW_SEARCH); + + component.instance().search(''); + expect(component.state().searchQuery).to.equal(''); + expect(component.state().view).to.equal(VIEWS.VIEW_FOLDER); + + component.instance().search(' '); + expect(component.state().searchQuery).to.equal(' '); + expect(component.state().view).to.equal(VIEWS.VIEW_FOLDER); + }); + }); + + describe('getSearchResult()', () => { + it('should return correct item list', () => { + const component = shallow(); + + const itemList = component.instance().getSearchResult(data, 'level-1'); + const fuzzyList = component.instance().getSearchResult(data, 'leel1'); + + expect(itemList).to.eql([data[1], data[2]]); + expect(fuzzyList).to.eql([data[1], data[2]]); + }); + }); }); diff --git a/yarn.lock b/yarn.lock index 32b2e624f..27f7c6ae7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5662,6 +5662,11 @@ functions-have-names@^1.1.1: resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.0.tgz#83da7583e4ea0c9ac5ff530f73394b033e0bf77d" integrity sha512-zKXyzksTeaCSw5wIX79iCA40YAa6CJMJgNg9wdkU/ERBrIdPSimPICYiLp65lRbSBqtiHql/HZfS2DyI/AH6tQ== +fuzzysearch@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/fuzzysearch/-/fuzzysearch-1.0.3.tgz#dffc80f6d6b04223f2226aa79dd194231096d008" + integrity sha1-3/yA9tawQiPyImqnndGUIxCW0Ag= + gather-stream@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/gather-stream/-/gather-stream-1.0.0.tgz#b33994af457a8115700d410f317733cbe7a0904b"