mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2024-12-23 09:30:31 +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 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);
|
||||||
|
|
|
@ -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,
|
||||||
|
});
|
||||||
|
|
|
@ -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;
|
||||||
|
|
Loading…
Reference in a new issue