Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(archive-viewer): Add search #1121

Merged
merged 5 commits into from
Dec 18, 2019
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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();
jstoffan marked this conversation as resolved.
Show resolved Hide resolved
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'] {
mxiao6 marked this conversation as resolved.
Show resolved Hide resolved
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);
});
});
});