mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2024-12-23 01:20:24 +03:00
Added search bar to tags list
This commit is contained in:
parent
843c121285
commit
96adb227d9
3 changed files with 48 additions and 10 deletions
|
@ -1,11 +1,12 @@
|
|||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { pick, splitEvery } from 'ramda';
|
||||
import { listTags } from './reducers/tagsList';
|
||||
import { filterTags, listTags } from './reducers/tagsList';
|
||||
import MuttedMessage from '../utils/MuttedMessage';
|
||||
import TagCard from './TagCard';
|
||||
import SearchField from '../utils/SearchField';
|
||||
|
||||
const { round } = Math;
|
||||
const { ceil } = Math;
|
||||
|
||||
export class TagsList extends React.Component {
|
||||
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) {
|
||||
return <MuttedMessage>No tags found</MuttedMessage>;
|
||||
}
|
||||
|
||||
const tagsGroups = splitEvery(round(tagsCount / 4), tagsList.tags);
|
||||
const tagsGroups = splitEvery(ceil(tagsCount / 4), tagsList.filteredTags);
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
|
@ -54,8 +55,17 @@ export class TagsList extends React.Component {
|
|||
}
|
||||
|
||||
render() {
|
||||
const { filterTags } = this.props;
|
||||
|
||||
return (
|
||||
<div className="shlink-container">
|
||||
{!this.props.tagsList.loading && (
|
||||
<SearchField
|
||||
onChange={filterTags}
|
||||
className="mb-3"
|
||||
placeholder="Search tags..."
|
||||
/>
|
||||
)}
|
||||
<div className="row">
|
||||
{this.renderContent()}
|
||||
</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);
|
||||
|
|
|
@ -6,9 +6,11 @@ import { TAG_EDITED } from './tagEdit';
|
|||
const LIST_TAGS_START = 'shlink/tagsList/LIST_TAGS_START';
|
||||
const LIST_TAGS_ERROR = 'shlink/tagsList/LIST_TAGS_ERROR';
|
||||
const LIST_TAGS = 'shlink/tagsList/LIST_TAGS';
|
||||
const FILTER_TAGS = 'shlink/tagsList/FILTER_TAGS';
|
||||
|
||||
const defaultState = {
|
||||
tags: [],
|
||||
filteredTags: [],
|
||||
loading: false,
|
||||
error: false,
|
||||
};
|
||||
|
@ -30,20 +32,31 @@ export default function reducer(state = defaultState, action) {
|
|||
case LIST_TAGS:
|
||||
return {
|
||||
tags: action.tags,
|
||||
filteredTags: action.tags,
|
||||
loading: false,
|
||||
error: false,
|
||||
};
|
||||
case TAG_DELETED:
|
||||
return {
|
||||
...state,
|
||||
// FIXME This should be optimized somehow...
|
||||
tags: reject(tag => tag === action.tag, state.tags),
|
||||
filteredTags: reject(tag => tag === action.tag, state.filteredTags),
|
||||
};
|
||||
case TAG_EDITED:
|
||||
const renameTag = tag => tag === action.oldName ? action.newName : tag;
|
||||
return {
|
||||
...state,
|
||||
tags: state.tags.map(
|
||||
tag => tag === action.oldName ? action.newName : tag
|
||||
).sort(),
|
||||
// FIXME This should be optimized somehow...
|
||||
tags: state.tags.map(renameTag).sort(),
|
||||
filteredTags: state.filteredTags.map(renameTag).sort(),
|
||||
};
|
||||
case FILTER_TAGS:
|
||||
return {
|
||||
...state,
|
||||
filteredTags: state.tags.filter(
|
||||
tag => tag.toLowerCase().match(action.searchTerm),
|
||||
),
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
|
@ -61,3 +74,8 @@ export const _listTags = ShlinkApiClient => async dispatch => {
|
|||
}
|
||||
};
|
||||
export const listTags = () => _listTags(ShlinkApiClient);
|
||||
|
||||
export const filterTags = searchTerm => ({
|
||||
type: FILTER_TAGS,
|
||||
searchTerm,
|
||||
});
|
||||
|
|
|
@ -2,10 +2,17 @@ import React from 'react';
|
|||
import FontAwesomeIcon from '@fortawesome/react-fontawesome';
|
||||
import searchIcon from '@fortawesome/fontawesome-free-solid/faSearch';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
import './SearchField.scss';
|
||||
|
||||
const propTypes = {
|
||||
onChange: PropTypes.func.isRequired,
|
||||
className: PropTypes.string,
|
||||
placeholder: PropTypes.string,
|
||||
};
|
||||
const defaultProps = {
|
||||
className: '',
|
||||
placeholder: 'Search...',
|
||||
};
|
||||
|
||||
export default class SearchField extends React.Component {
|
||||
|
@ -31,12 +38,14 @@ export default class SearchField extends React.Component {
|
|||
}
|
||||
|
||||
render() {
|
||||
const { className, placeholder } = this.props;
|
||||
|
||||
return (
|
||||
<div className="search-field">
|
||||
<div className={classnames('search-field', className)}>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control form-control-lg search-field__input"
|
||||
placeholder="Search..."
|
||||
placeholder={placeholder}
|
||||
onChange={e => this.searchTermChanged(e.target.value)}
|
||||
value={this.state.searchTerm}
|
||||
/>
|
||||
|
@ -55,3 +64,4 @@ export default class SearchField extends React.Component {
|
|||
}
|
||||
|
||||
SearchField.propTypes = propTypes;
|
||||
SearchField.defaultProps = defaultProps;
|
||||
|
|
Loading…
Reference in a new issue