Skip to content

Commit

Permalink
[#3] Add support for tags in filters
Browse files Browse the repository at this point in the history
  • Loading branch information
amir-hadzic committed Nov 2, 2016
1 parent 7252f62 commit 98f9cc4
Show file tree
Hide file tree
Showing 9 changed files with 103 additions and 12 deletions.
1 change: 1 addition & 0 deletions .storybook/config.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { configure } from '@kadira/storybook';
import '../node_modules/bootstrap/dist/css/bootstrap.min.css';
import '../node_modules/react-select/dist/react-select.min.css';

function loadStories() {
require('../stories');
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
"react": "^15.1.0",
"react-dom": "^15.3.2",
"react-redux": "^4.4.5",
"react-select": "^1.0.0-rc.1",
"redux": "^3.4.0",
"redux-actions": "^0.11.0",
"reselect": "^2.5.3"
Expand Down
24 changes: 19 additions & 5 deletions src/Filter.js
Original file line number Diff line number Diff line change
@@ -1,22 +1,36 @@
import React, { Component, PropTypes } from 'react';
import { Creatable } from 'react-select';

const propTypes = {
value: PropTypes.string,
value: PropTypes.array,
onChange: PropTypes.func.isRequired,
options: PropTypes.array.isRequired,
};

class Filter extends Component {
render() {
const {
value,
onChange,
options,
} = this.props;
return (
<input
className="form-control"
onChange={(e) => onChange(e.target.value)}
<Creatable
options={options}
noResultsText="No tags found, type text and press Enter to search"
placeholder="Search by text or tags"
promptTextCreator={(txt) => `Search for '${txt}'`}
onChange={(selected) => onChange(selected)}
newOptionCreator={({ label, labelKey, valueKey }) => {
const option = {};
option[valueKey] = label.toLowerCase();
option[labelKey] = label.toLowerCase();
option.textFilter = true;
option.className = 'Select-create-option-placeholder';
return option;
}}
value={value}
placeholder="Filter data with any text"
multi
style={{
margin: '1rem 0 1rem 0',
}}
Expand Down
4 changes: 2 additions & 2 deletions src/reducer.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {
const defaultState = (configs = {}) => ({
page: 0,
pageSize: configs.defaultPageSize || 5,
filter: '',
filter: [],
sortKey: null,
direction: null,
selectAll: false,
Expand Down Expand Up @@ -72,7 +72,7 @@ const behaviours = {
[TABLE_FILTER_CHANGED]: (state, { payload }) => ({
...state,
page: 0,
filter: payload.filter.toLowerCase(),
filter: payload.filter,
}),
[TABLE_SELECT_ALL_CHANGED]: (state) => ({
...state,
Expand Down
65 changes: 61 additions & 4 deletions src/selectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,30 @@ function paginate(rows, { page, pageSize }) {
return rows.slice(start, start + pageSize);
}

function filter(rows = [], textFilter, columns) {
if (!textFilter) {
function filter(rows = [], filters = [], columns) {
if (filters.length === 0) {
return rows.slice(0);
}
return _.filter(rows, (row) => _.some(columns, (column) => {

// apply text filter across all columns
let filteredRows = _.filter(rows, row => _.some(columns, (column) => {
if (!column.filterable) {
return false;
}
const normalized = String(_.get(row, column.key)).toLowerCase();
return normalized.indexOf(textFilter) > -1;
return _.every(filters, f => !f.textFilter || normalized.indexOf(f.value) > -1);
}));

// apply value filters on taggable columns
filteredRows = _.filter(filteredRows, row => _.every(columns, column => {
if (!column.taggable) {
return true;
}
const value = _.get(row, column.key);
return _.every(filters, f => !f.valueFilter || f.key !== column.key || f.value === value);
}));

return filteredRows;
}

function sort(rows, { sortKey, direction }) {
Expand Down Expand Up @@ -71,6 +84,49 @@ export default (tableName) => {
(initialData, textFilter, columns) => filter(initialData, textFilter, columns)
);

const getFilterOptions = createSelector(
getInitialData,
getColumns,
(initialData, columns) => {
const options = [];
const values = {};
const columnMap = _.keyBy(columns, 'key');

initialData.forEach(row => {
columns.forEach(column => {
if (!column.taggable) {
return;
}
if (!values[column.key]) {
values[column.key] = [];
}
const columnValues = values[column.key];
const value = _.get(row, column.key);
if (!columnValues.includes(value)) {
columnValues.push(value);
}
});
});

_.forOwn(values, (columnValues, key) => {
columnValues.forEach(value => {
let labelValue = value;
if (_.isBoolean(value)) {
labelValue = value ? 'Yes' : 'No';
}
options.push({
label: `${columnMap[key].header}:${labelValue}`,
valueFilter: true,
value,
key,
});
});
});

return options;
}
);

const getPageInfo = createSelector(
getPage,
getPageSize,
Expand Down Expand Up @@ -151,5 +207,6 @@ export default (tableName) => {
getSelectedRows,
getSelectAll,
getPrimaryKey,
getFilterOptions,
};
};
6 changes: 5 additions & 1 deletion src/sematable.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,13 @@ const propTypes = {

isInitialized: PropTypes.bool.isRequired,
visibleRows: PropTypes.array,
filter: PropTypes.string,
filter: PropTypes.array,
sortInfo: PropTypes.object,
pageInfo: PropTypes.object,
selectAll: PropTypes.bool,
selectedRows: PropTypes.array,
primaryKey: PropTypes.string,
filterOptions: PropTypes.array,

onPageChange: PropTypes.func.isRequired,
onPageSizeChange: PropTypes.func.isRequired,
Expand Down Expand Up @@ -71,6 +72,7 @@ const sematable = (tableName, TableComponent, columns, configs = {}) => {
selectAll: selectors.getSelectAll(state),
selectedRows: selectors.getSelectedRows(state),
primaryKey: selectors.getPrimaryKey(state),
filterOptions: selectors.getFilterOptions(state),
};
};

Expand Down Expand Up @@ -109,6 +111,7 @@ const sematable = (tableName, TableComponent, columns, configs = {}) => {
selectAll,
selectedRows,
primaryKey,
filterOptions,

/* actions */
onPageChange,
Expand Down Expand Up @@ -164,6 +167,7 @@ const sematable = (tableName, TableComponent, columns, configs = {}) => {
<div className="col-md-6">
{showFilter && <Filter
value={filter}
options={filterOptions}
onChange={(f) => onFilterChange(f)}
/>}
</div>
Expand Down
2 changes: 2 additions & 0 deletions stories/UsersTable.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import React, { Component } from 'react';
import sematable, { Table } from '../src';
import YesNo from './YesNo.js';

export const USERS_TABLE = 'usersTable';
const columns = [
{ key: 'id', primaryKey: true, header: 'ID' },
{ key: 'firstName', header: 'First name', filterable: true, sortable: true },
{ key: 'lastName', header: 'Last name', filterable: true, sortable: true },
{ key: 'status', header: 'Status', taggable: true },
{ key: 'confirmed', header: 'Confirmed', taggable: true, Component: YesNo },
];

class UsersTable extends Component {
Expand Down
10 changes: 10 additions & 0 deletions stories/YesNo.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import React, { PropTypes } from 'react';

const YesNo = ({ children }) => (
<span>{children ? 'Yes' : 'No'}</span>
);

YesNo.propTypes = {
children: PropTypes.bool.isRequired,
};
export default YesNo;
2 changes: 2 additions & 0 deletions stories/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,14 @@ const users = [
firstName: 'John',
lastName: 'Doe',
status: 'UNKNOWN',
confirmed: true,
},
{
id: 2,
firstName: 'Bob',
lastName: 'McBobber',
status: 'ACTIVE',
confirmed: false,
},
];

Expand Down

0 comments on commit 98f9cc4

Please sign in to comment.