Skip to content

Commit

Permalink
feat(archive-viewer): Add search (#1121)
Browse files Browse the repository at this point in the history
* feat(archive-viewer): Add search

* feat(archive-viewer): address comments

* feat(archive-viewer): address comments

* feat(archive-viewer): address comments
  • Loading branch information
Mingze authored and mergify[bot] committed Dec 18, 2019
1 parent 8abfb53 commit 2897851
Show file tree
Hide file tree
Showing 7 changed files with 158 additions and 24 deletions.
4 changes: 4 additions & 0 deletions src/i18n/en-US.properties
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,12 @@ filename=Filename
last_modified_date=Last modified date
# Label for size column name
size=Size
# Shown as search accessibility label.
search=Search
# Shown as the title in the breadcrumbs while searching.
search_results=Search Results
# Shown as a placeholder in the search bar.
search_placeholder=Search files and folders

# Media Preview
# Label for autoplay in media player
Expand Down
58 changes: 43 additions & 15 deletions src/lib/viewers/archive/ArchiveExplorer.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import getProp from 'lodash/get';
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 fuzzySearch from 'box-ui-elements/es/utils/fuzzySearch';
import {
readableTimeCellRenderer,
sizeCellRenderer,
Expand All @@ -13,11 +14,13 @@ import VirtualizedTable from 'box-ui-elements/es/features/virtualized-table';
import { addLocaleData } from 'react-intl';
import { Column } from 'react-virtualized/dist/es/Table/index';
import Breadcrumbs from './Breadcrumbs';
import SearchBar from './SearchBar';
import { TABLE_COLUMNS, VIEWS } from './constants';
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 = {
Expand Down Expand Up @@ -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,
};
}

Expand Down Expand Up @@ -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,
Expand All @@ -112,25 +117,44 @@ class ArchiveExplorer extends React.Component {
};

/**
* Handle click event, update fullPath state
* Handle item 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}/`,
});
};
handleItemClick = ({ fullPath }) => this.setState({ view: VIEW_FOLDER, fullPath, searchQuery: '' });

/**
* Handle click event, update fullPath state
* Handle breadcrumb click event, update fullPath state
*
* @param {string} fullPath - target folder path
* @return {void}
*/
handleClickFullPath = fullPath => this.setState({ fullPath });
handleBreadcrumbClick = fullPath => this.setState({ fullPath });

/**
* Handle search input, update view state
*
* @param {string} query - raw query string in the search bar
* @return {void}
*/
handleSearch = query =>
this.setState({
searchQuery: query,
view: query.trim() ? VIEW_SEARCH : VIEW_FOLDER,
});

/**
* Filter item collection for search query
*
* @param {Array<Object>} itemCollection - raw data
* @param {string} searchQuery - user input
* @return {Array<Object>} filtered items for search query
*/
getSearchResult = (itemCollection, searchQuery) => {
const trimmedQuery = searchQuery.trim();
return itemCollection.filter(item => fuzzySearch(trimmedQuery, item.name, 0));
};

/**
* render data
Expand All @@ -139,18 +163,22 @@ 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 (
<Internationalize language={language} messages={elementsMessages}>
<div className="bp-ArchiveExplorer">
<Breadcrumbs fullPath={fullPath} onClick={this.handleClickFullPath} view={view} />
<SearchBar onSearch={this.handleSearch} searchQuery={searchQuery} />
<Breadcrumbs fullPath={fullPath} onClick={this.handleBreadcrumbClick} view={view} />
<VirtualizedTable rowData={itemList} rowGetter={this.getRowData(itemList)}>
{intl => [
<Column
key={KEY_NAME}
cellRenderer={itemNameCellRenderer(intl, this.handleClick)}
cellRenderer={itemNameCellRenderer(intl, this.handleItemClick)}
dataKey={KEY_NAME}
disableSort
flexGrow={3}
Expand Down
2 changes: 1 addition & 1 deletion src/lib/viewers/archive/Breadcrumbs.scss
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
flex: 0 0 50px;
align-items: center;
justify-content: space-between;
padding: 0 20px 0 25px;
padding: 0 20px;
border-bottom: 1px solid $bdl-gray-10;
box-shadow: 0 4px 6px -2px $transparent-black;
}
24 changes: 24 additions & 0 deletions src/lib/viewers/archive/SearchBar.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import React from 'react';
import PropTypes from 'prop-types';
import './SearchBar.scss';

const SearchBar = ({ onSearch, searchQuery }) => {
return (
<div className="bp-SearchBar">
<input
aria-label={__('search')}
onChange={({ currentTarget }) => onSearch(currentTarget.value)}
placeholder={__('search_placeholder')}
type="search"
value={searchQuery}
/>
</div>
);
};

SearchBar.propTypes = {
onSearch: PropTypes.func.isRequired,
searchQuery: PropTypes.string.isRequired,
};

export default SearchBar;
16 changes: 16 additions & 0 deletions src/lib/viewers/archive/SearchBar.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
@import '~box-ui-elements/es/styles/variables';

.bp-SearchBar {
display: flex;
flex: 0 0 70px;
align-items: center;
padding: 0 20px;
background: $almost-white;
border-bottom: 1px solid $bdl-gray-10;

// override .be input[type='serach']
input[type='search'] {
width: 100%;
font: inherit;
}
}
50 changes: 42 additions & 8 deletions src/lib/viewers/archive/__tests__/ArchiveExplorer-test-react.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -72,27 +72,30 @@ describe('lib/viewers/archive/ArchiveExplorer', () => {
const component = shallow(<ArchiveExplorer itemCollection={data} />);

expect(component.find('.bp-ArchiveExplorer').length).to.equal(1);
expect(component.find('SearchBar').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);
});
});

describe('handleClick()', () => {
it('should set state when handleClick() is called', () => {
describe('handleItemClick()', () => {
it('should set state when handleItemClick() is called', () => {
const component = shallow(<ArchiveExplorer itemCollection={data} />);

component.instance().handleClick({ name: 'subfolder' });
component.instance().handleItemClick({ 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('');
});
});

describe('handleClickFullPath()', () => {
it('should set state when handleClickFullPath() is called', () => {
describe('handleBreadcrumbClick()', () => {
it('should set state when handleBreadcrumbClick() is called', () => {
const component = shallow(<ArchiveExplorer itemCollection={data} />);

component.instance().handleClickFullPath('test/subfolder/');
component.instance().handleBreadcrumbClick('test/subfolder/');

expect(component.state().fullPath).to.equal('test/subfolder/');
});
Expand All @@ -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,
Expand All @@ -129,4 +133,34 @@ describe('lib/viewers/archive/ArchiveExplorer', () => {
expect(itemList).to.eql([data[1], data[2]]);
});
});

describe('handleSearch()', () => {
it('should set correct state when search query is not empty', () => {
const component = shallow(<ArchiveExplorer itemCollection={data} />);

component.instance().handleSearch('test');
expect(component.state().searchQuery).to.equal('test');
expect(component.state().view).to.equal(VIEWS.VIEW_SEARCH);

component.instance().handleSearch('');
expect(component.state().searchQuery).to.equal('');
expect(component.state().view).to.equal(VIEWS.VIEW_FOLDER);

component.instance().handleSearch(' ');
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(<ArchiveExplorer itemCollection={data} />);

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]]);
});
});
});
28 changes: 28 additions & 0 deletions src/lib/viewers/archive/__tests__/SearchBar-test-react.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import React from 'react';
import { shallow } from 'enzyme';
import sinon from 'sinon';
import SearchBar from '../SearchBar';

const sandbox = sinon.sandbox.create();
let searchQuery;
let onSearch;

describe('lib/viewers/archive/SearchBar', () => {
beforeEach(() => {
searchQuery = 'test';
onSearch = sandbox.stub();
});

afterEach(() => {
sandbox.verifyAndRestore();
});

describe('render()', () => {
it('should render correct components', () => {
const component = shallow(<SearchBar onSearch={onSearch} searchQuery={searchQuery} />);

expect(component.find('.bp-SearchBar').length).to.equal(1);
expect(component.find('input').length).to.equal(1);
});
});
});

0 comments on commit 2897851

Please sign in to comment.