Added search bar to tags list

This commit is contained in:
Alejandro Celaya 2018-08-19 20:52:33 +02:00
parent 843c121285
commit 96adb227d9
3 changed files with 48 additions and 10 deletions

View file

@ -1,11 +1,12 @@
import React from 'react'; import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { pick, splitEvery } from 'ramda'; import { pick, splitEvery } from 'ramda';
import { listTags } from './reducers/tagsList'; import { filterTags, listTags } from './reducers/tagsList';
import MuttedMessage from '../utils/MuttedMessage'; import MuttedMessage from '../utils/MuttedMessage';
import TagCard from './TagCard'; import TagCard from './TagCard';
import SearchField from '../utils/SearchField';
const { round } = Math; const { ceil } = Math;
export class TagsList extends React.Component { export class TagsList extends React.Component {
state = { isDeleteModalOpen: false }; state = { isDeleteModalOpen: false };
@ -29,12 +30,12 @@ export class TagsList extends React.Component {
); );
} }
const tagsCount = tagsList.tags.length; const tagsCount = tagsList.filteredTags.length;
if (tagsCount < 1) { if (tagsCount < 1) {
return <MuttedMessage>No tags found</MuttedMessage>; return <MuttedMessage>No tags found</MuttedMessage>;
} }
const tagsGroups = splitEvery(round(tagsCount / 4), tagsList.tags); const tagsGroups = splitEvery(ceil(tagsCount / 4), tagsList.filteredTags);
return ( return (
<React.Fragment> <React.Fragment>
@ -54,8 +55,17 @@ export class TagsList extends React.Component {
} }
render() { render() {
const { filterTags } = this.props;
return ( return (
<div className="shlink-container"> <div className="shlink-container">
{!this.props.tagsList.loading && (
<SearchField
onChange={filterTags}
className="mb-3"
placeholder="Search tags..."
/>
)}
<div className="row"> <div className="row">
{this.renderContent()} {this.renderContent()}
</div> </div>
@ -64,4 +74,4 @@ export class TagsList extends React.Component {
} }
} }
export default connect(pick(['tagsList']), { listTags })(TagsList); export default connect(pick(['tagsList']), { listTags, filterTags })(TagsList);

View file

@ -6,9 +6,11 @@ import { TAG_EDITED } from './tagEdit';
const LIST_TAGS_START = 'shlink/tagsList/LIST_TAGS_START'; const LIST_TAGS_START = 'shlink/tagsList/LIST_TAGS_START';
const LIST_TAGS_ERROR = 'shlink/tagsList/LIST_TAGS_ERROR'; const LIST_TAGS_ERROR = 'shlink/tagsList/LIST_TAGS_ERROR';
const LIST_TAGS = 'shlink/tagsList/LIST_TAGS'; const LIST_TAGS = 'shlink/tagsList/LIST_TAGS';
const FILTER_TAGS = 'shlink/tagsList/FILTER_TAGS';
const defaultState = { const defaultState = {
tags: [], tags: [],
filteredTags: [],
loading: false, loading: false,
error: false, error: false,
}; };
@ -30,20 +32,31 @@ export default function reducer(state = defaultState, action) {
case LIST_TAGS: case LIST_TAGS:
return { return {
tags: action.tags, tags: action.tags,
filteredTags: action.tags,
loading: false, loading: false,
error: false, error: false,
}; };
case TAG_DELETED: case TAG_DELETED:
return { return {
...state, ...state,
// FIXME This should be optimized somehow...
tags: reject(tag => tag === action.tag, state.tags), tags: reject(tag => tag === action.tag, state.tags),
filteredTags: reject(tag => tag === action.tag, state.filteredTags),
}; };
case TAG_EDITED: case TAG_EDITED:
const renameTag = tag => tag === action.oldName ? action.newName : tag;
return { return {
...state, ...state,
tags: state.tags.map( // FIXME This should be optimized somehow...
tag => tag === action.oldName ? action.newName : tag tags: state.tags.map(renameTag).sort(),
).sort(), filteredTags: state.filteredTags.map(renameTag).sort(),
};
case FILTER_TAGS:
return {
...state,
filteredTags: state.tags.filter(
tag => tag.toLowerCase().match(action.searchTerm),
),
}; };
default: default:
return state; return state;
@ -61,3 +74,8 @@ export const _listTags = ShlinkApiClient => async dispatch => {
} }
}; };
export const listTags = () => _listTags(ShlinkApiClient); export const listTags = () => _listTags(ShlinkApiClient);
export const filterTags = searchTerm => ({
type: FILTER_TAGS,
searchTerm,
});

View file

@ -2,10 +2,17 @@ import React from 'react';
import FontAwesomeIcon from '@fortawesome/react-fontawesome'; import FontAwesomeIcon from '@fortawesome/react-fontawesome';
import searchIcon from '@fortawesome/fontawesome-free-solid/faSearch'; import searchIcon from '@fortawesome/fontawesome-free-solid/faSearch';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import classnames from 'classnames';
import './SearchField.scss'; import './SearchField.scss';
const propTypes = { const propTypes = {
onChange: PropTypes.func.isRequired, onChange: PropTypes.func.isRequired,
className: PropTypes.string,
placeholder: PropTypes.string,
};
const defaultProps = {
className: '',
placeholder: 'Search...',
}; };
export default class SearchField extends React.Component { export default class SearchField extends React.Component {
@ -31,12 +38,14 @@ export default class SearchField extends React.Component {
} }
render() { render() {
const { className, placeholder } = this.props;
return ( return (
<div className="search-field"> <div className={classnames('search-field', className)}>
<input <input
type="text" type="text"
className="form-control form-control-lg search-field__input" className="form-control form-control-lg search-field__input"
placeholder="Search..." placeholder={placeholder}
onChange={e => this.searchTermChanged(e.target.value)} onChange={e => this.searchTermChanged(e.target.value)}
value={this.state.searchTerm} value={this.state.searchTerm}
/> />
@ -55,3 +64,4 @@ export default class SearchField extends React.Component {
} }
SearchField.propTypes = propTypes; SearchField.propTypes = propTypes;
SearchField.defaultProps = defaultProps;