mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2025-01-03 23:07:26 +03:00
Implemented edition of tags
This commit is contained in:
parent
878e336ba1
commit
d541543ab3
10 changed files with 197 additions and 6 deletions
|
@ -24,6 +24,7 @@
|
||||||
"ramda": "^0.25.0",
|
"ramda": "^0.25.0",
|
||||||
"react": "^16.3.2",
|
"react": "^16.3.2",
|
||||||
"react-chartjs-2": "^2.7.4",
|
"react-chartjs-2": "^2.7.4",
|
||||||
|
"react-color": "^2.14.1",
|
||||||
"react-copy-to-clipboard": "^5.0.1",
|
"react-copy-to-clipboard": "^5.0.1",
|
||||||
"react-datepicker": "^1.5.0",
|
"react-datepicker": "^1.5.0",
|
||||||
"react-dom": "^16.3.2",
|
"react-dom": "^16.3.2",
|
||||||
|
|
|
@ -59,6 +59,11 @@ export class ShlinkApiClient {
|
||||||
.then(() => ({ tags }))
|
.then(() => ({ tags }))
|
||||||
.catch(e => this._handleAuthError(e, this.deleteTag, []));
|
.catch(e => this._handleAuthError(e, this.deleteTag, []));
|
||||||
|
|
||||||
|
editTag = (oldName, newName) =>
|
||||||
|
this._performRequest('/tags', 'PUT', {}, { oldName, newName })
|
||||||
|
.then(() => ({ oldName, newName }))
|
||||||
|
.catch(e => this._handleAuthError(e, this.editTag, [oldName, newName]));
|
||||||
|
|
||||||
_performRequest = async (url, method = 'GET', query = {}, body = {}) => {
|
_performRequest = async (url, method = 'GET', query = {}, body = {}) => {
|
||||||
if (isEmpty(this._token)) {
|
if (isEmpty(this._token)) {
|
||||||
this._token = await this._authenticate();
|
this._token = await this._authenticate();
|
||||||
|
|
|
@ -52,7 +52,7 @@ export default function AsideMenu({ selectedServer, className, showOnMobile }) {
|
||||||
to={`/server/${serverId}/tags`}
|
to={`/server/${serverId}/tags`}
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={tagsIcon} />
|
<FontAwesomeIcon icon={tagsIcon} />
|
||||||
<span className="aside-menu__item-text">List tags</span>
|
<span className="aside-menu__item-text">Manage tags</span>
|
||||||
</NavLink>
|
</NavLink>
|
||||||
|
|
||||||
<DeleteServerButton
|
<DeleteServerButton
|
||||||
|
|
|
@ -9,6 +9,7 @@ import shortUrlVisitsReducer from '../short-urls/reducers/shortUrlVisits';
|
||||||
import shortUrlTagsReducer from '../short-urls/reducers/shortUrlTags';
|
import shortUrlTagsReducer from '../short-urls/reducers/shortUrlTags';
|
||||||
import tagsListReducer from '../tags/reducers/tagsList';
|
import tagsListReducer from '../tags/reducers/tagsList';
|
||||||
import tagDeleteReducer from '../tags/reducers/tagDelete';
|
import tagDeleteReducer from '../tags/reducers/tagDelete';
|
||||||
|
import tagEditReducer from '../tags/reducers/tagEdit';
|
||||||
|
|
||||||
export default combineReducers({
|
export default combineReducers({
|
||||||
servers: serversReducer,
|
servers: serversReducer,
|
||||||
|
@ -20,4 +21,5 @@ export default combineReducers({
|
||||||
shortUrlTags: shortUrlTagsReducer,
|
shortUrlTags: shortUrlTagsReducer,
|
||||||
tagsList: tagsListReducer,
|
tagsList: tagsListReducer,
|
||||||
tagDelete: tagDeleteReducer,
|
tagDelete: tagDeleteReducer,
|
||||||
|
tagEdit: tagEditReducer,
|
||||||
});
|
});
|
||||||
|
|
|
@ -8,6 +8,7 @@ import React from 'react';
|
||||||
import ColorGenerator, { colorGeneratorType } from '../utils/ColorGenerator';
|
import ColorGenerator, { colorGeneratorType } from '../utils/ColorGenerator';
|
||||||
import './TagCard.scss';
|
import './TagCard.scss';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
import EditTagModal from './helpers/EditTagModal';
|
||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
tag: PropTypes.string,
|
tag: PropTypes.string,
|
||||||
|
@ -24,17 +25,22 @@ export default class TagCard extends React.Component {
|
||||||
const { tag, colorGenerator, currentServerId } = this.props;
|
const { tag, colorGenerator, currentServerId } = this.props;
|
||||||
const toggleDelete = () =>
|
const toggleDelete = () =>
|
||||||
this.setState({ isDeleteModalOpen: !this.state.isDeleteModalOpen });
|
this.setState({ isDeleteModalOpen: !this.state.isDeleteModalOpen });
|
||||||
|
const toggleEdit = () =>
|
||||||
|
this.setState({ isEditModalOpen: !this.state.isEditModalOpen });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="tag-card">
|
<Card className="tag-card">
|
||||||
<CardBody className="tag-card__body">
|
<CardBody className="tag-card__body">
|
||||||
<button
|
<button
|
||||||
className="btn btn-light btn-sm tag-card__btn"
|
className="btn btn-light btn-sm tag-card__btn tag-card__btn--last"
|
||||||
onClick={toggleDelete}
|
onClick={toggleDelete}
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={deleteIcon}/>
|
<FontAwesomeIcon icon={deleteIcon}/>
|
||||||
</button>
|
</button>
|
||||||
<button className="btn btn-light btn-sm tag-card__btn">
|
<button
|
||||||
|
className="btn btn-light btn-sm tag-card__btn"
|
||||||
|
onClick={toggleEdit}
|
||||||
|
>
|
||||||
<FontAwesomeIcon icon={editIcon}/>
|
<FontAwesomeIcon icon={editIcon}/>
|
||||||
</button>
|
</button>
|
||||||
<h5 className="tag-card__tag-title">
|
<h5 className="tag-card__tag-title">
|
||||||
|
@ -53,6 +59,11 @@ export default class TagCard extends React.Component {
|
||||||
toggle={toggleDelete}
|
toggle={toggleDelete}
|
||||||
isOpen={this.state.isDeleteModalOpen}
|
isOpen={this.state.isDeleteModalOpen}
|
||||||
/>
|
/>
|
||||||
|
<EditTagModal
|
||||||
|
tag={tag}
|
||||||
|
toggle={toggleEdit}
|
||||||
|
isOpen={this.state.isEditModalOpen}
|
||||||
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,6 +28,6 @@
|
||||||
.tag-card__btn {
|
.tag-card__btn {
|
||||||
float: right;
|
float: right;
|
||||||
}
|
}
|
||||||
.tag-card__btn:not(:last-child) {
|
.tag-card__btn--last {
|
||||||
margin-left: 2px;
|
margin-left: 3px;
|
||||||
}
|
}
|
||||||
|
|
75
src/tags/helpers/EditTagModal.js
Normal file
75
src/tags/helpers/EditTagModal.js
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { Modal, ModalBody, ModalFooter, ModalHeader } from 'reactstrap';
|
||||||
|
import { pick } from 'ramda';
|
||||||
|
import { editTag, tagEdited } from '../reducers/tagEdit';
|
||||||
|
|
||||||
|
export class EditTagModal extends React.Component {
|
||||||
|
saveTag = e => {
|
||||||
|
e.preventDefault();
|
||||||
|
const { tag: oldName, editTag, toggle } = this.props;
|
||||||
|
const { tag: newName } = this.state;
|
||||||
|
|
||||||
|
editTag(oldName, newName)
|
||||||
|
.then(() => {
|
||||||
|
this.tagWasEdited = true;
|
||||||
|
toggle();
|
||||||
|
})
|
||||||
|
.catch(() => {});
|
||||||
|
};
|
||||||
|
onClosed = () => {
|
||||||
|
if (!this.tagWasEdited) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { tag: oldName, tagEdited } = this.props;
|
||||||
|
const { tag: newName } = this.state;
|
||||||
|
tagEdited(oldName, newName);
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
tag: props.tag,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.tagWasEdited = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { isOpen, toggle, tagEdit } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal isOpen={isOpen} toggle={toggle} centered onClosed={this.onClosed}>
|
||||||
|
<form onSubmit={this.saveTag}>
|
||||||
|
<ModalHeader toggle={toggle}>Edit tag</ModalHeader>
|
||||||
|
<ModalBody>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={this.state.tag}
|
||||||
|
onChange={e => this.setState({ tag: e.target.value })}
|
||||||
|
placeholder="Tag"
|
||||||
|
required
|
||||||
|
className="form-control"
|
||||||
|
/>
|
||||||
|
{tagEdit.error && (
|
||||||
|
<div className="p-2 mt-2 bg-danger text-white text-center">
|
||||||
|
Something went wrong while editing the tag :(
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</ModalBody>
|
||||||
|
<ModalFooter>
|
||||||
|
<button type="button" className="btn btn-link" onClick={toggle}>Cancel</button>
|
||||||
|
<button type="submit" className="btn btn-primary" disabled={tagEdit.editing}>
|
||||||
|
{tagEdit.editing ? 'Saving...' : 'Save'}
|
||||||
|
</button>
|
||||||
|
</ModalFooter>
|
||||||
|
</form>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(pick(['tagEdit']), { editTag, tagEdited })(EditTagModal);
|
65
src/tags/reducers/tagEdit.js
Normal file
65
src/tags/reducers/tagEdit.js
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
import ShlinkApiClient from '../../api/ShlinkApiClient';
|
||||||
|
import ColorGenerator from '../../utils/ColorGenerator';
|
||||||
|
import { curry } from 'ramda';
|
||||||
|
|
||||||
|
const EDIT_TAG_START = 'shlink/editTag/EDIT_TAG_START';
|
||||||
|
const EDIT_TAG_ERROR = 'shlink/editTag/EDIT_TAG_ERROR';
|
||||||
|
const EDIT_TAG = 'shlink/editTag/EDIT_TAG';
|
||||||
|
export const TAG_EDITED = 'shlink/editTag/TAG_EDITED';
|
||||||
|
|
||||||
|
const defaultState = {
|
||||||
|
oldName: '',
|
||||||
|
newName: '',
|
||||||
|
editing: false,
|
||||||
|
error: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function reducer(state = defaultState, action) {
|
||||||
|
switch (action.type) {
|
||||||
|
case EDIT_TAG_START:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
editing: true,
|
||||||
|
error: false,
|
||||||
|
};
|
||||||
|
case EDIT_TAG_ERROR:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
editing: false,
|
||||||
|
error: true,
|
||||||
|
};
|
||||||
|
case EDIT_TAG:
|
||||||
|
return {
|
||||||
|
oldName: action.oldName,
|
||||||
|
newName: action.newName,
|
||||||
|
editing: false,
|
||||||
|
error: false,
|
||||||
|
};
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const _editTag = (ShlinkApiClient, ColorGenerator, oldName, newName) => async dispatch => {
|
||||||
|
dispatch({ type: EDIT_TAG_START });
|
||||||
|
|
||||||
|
try {
|
||||||
|
await ShlinkApiClient.editTag(oldName, newName);
|
||||||
|
|
||||||
|
// Make new tag name use the same color as the old one
|
||||||
|
const color = ColorGenerator.getColorForKey(oldName);
|
||||||
|
ColorGenerator.setColorForKey(newName, color);
|
||||||
|
|
||||||
|
dispatch({ type: EDIT_TAG, oldName, newName });
|
||||||
|
} catch (e) {
|
||||||
|
dispatch({ type: EDIT_TAG_ERROR });
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
export const editTag = curry(_editTag)(ShlinkApiClient, ColorGenerator);
|
||||||
|
|
||||||
|
export const tagEdited = (oldName, newName) => ({
|
||||||
|
type: TAG_EDITED,
|
||||||
|
oldName,
|
||||||
|
newName,
|
||||||
|
});
|
|
@ -1,6 +1,7 @@
|
||||||
import ShlinkApiClient from '../../api/ShlinkApiClient';
|
import ShlinkApiClient from '../../api/ShlinkApiClient';
|
||||||
import { TAG_DELETED } from './tagDelete';
|
import { TAG_DELETED } from './tagDelete';
|
||||||
import { reject } from 'ramda';
|
import { reject } from 'ramda';
|
||||||
|
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';
|
||||||
|
@ -37,6 +38,13 @@ export default function reducer(state = defaultState, action) {
|
||||||
...state,
|
...state,
|
||||||
tags: reject(tag => tag === action.tag, state.tags),
|
tags: reject(tag => tag === action.tag, state.tags),
|
||||||
};
|
};
|
||||||
|
case TAG_EDITED:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
tags: state.tags.map(
|
||||||
|
tag => tag === action.oldName ? action.newName : tag
|
||||||
|
),
|
||||||
|
};
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
26
yarn.lock
26
yarn.lock
|
@ -4667,7 +4667,7 @@ lodash.uniq@^4.5.0:
|
||||||
version "4.5.0"
|
version "4.5.0"
|
||||||
resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
|
resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
|
||||||
|
|
||||||
"lodash@>=3.5 <5", lodash@^4.0.0, lodash@^4.15.0, lodash@^4.17.10, lodash@^4.17.2, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.3.0, lodash@~4.17.10:
|
"lodash@>=3.5 <5", lodash@^4.0.0, lodash@^4.0.1, lodash@^4.15.0, lodash@^4.17.10, lodash@^4.17.2, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.3.0, lodash@~4.17.10:
|
||||||
version "4.17.10"
|
version "4.17.10"
|
||||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.10.tgz#1b7793cf7259ea38fb3661d4d38b3260af8ae4e7"
|
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.10.tgz#1b7793cf7259ea38fb3661d4d38b3260af8ae4e7"
|
||||||
|
|
||||||
|
@ -4737,6 +4737,10 @@ map-visit@^1.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
object-visit "^1.0.0"
|
object-visit "^1.0.0"
|
||||||
|
|
||||||
|
material-colors@^1.2.1:
|
||||||
|
version "1.2.6"
|
||||||
|
resolved "https://registry.yarnpkg.com/material-colors/-/material-colors-1.2.6.tgz#6d1958871126992ceecc72f4bcc4d8f010865f46"
|
||||||
|
|
||||||
math-expression-evaluator@^1.2.14:
|
math-expression-evaluator@^1.2.14:
|
||||||
version "1.2.17"
|
version "1.2.17"
|
||||||
resolved "https://registry.yarnpkg.com/math-expression-evaluator/-/math-expression-evaluator-1.2.17.tgz#de819fdbcd84dccd8fae59c6aeb79615b9d266ac"
|
resolved "https://registry.yarnpkg.com/math-expression-evaluator/-/math-expression-evaluator-1.2.17.tgz#de819fdbcd84dccd8fae59c6aeb79615b9d266ac"
|
||||||
|
@ -6107,6 +6111,16 @@ react-chartjs-2@^2.7.4:
|
||||||
lodash "^4.17.4"
|
lodash "^4.17.4"
|
||||||
prop-types "^15.5.8"
|
prop-types "^15.5.8"
|
||||||
|
|
||||||
|
react-color@^2.14.1:
|
||||||
|
version "2.14.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-color/-/react-color-2.14.1.tgz#db8ad4f45d81e74896fc2e1c99508927c6d084e0"
|
||||||
|
dependencies:
|
||||||
|
lodash "^4.0.1"
|
||||||
|
material-colors "^1.2.1"
|
||||||
|
prop-types "^15.5.10"
|
||||||
|
reactcss "^1.2.0"
|
||||||
|
tinycolor2 "^1.4.1"
|
||||||
|
|
||||||
react-copy-to-clipboard@^5.0.1:
|
react-copy-to-clipboard@^5.0.1:
|
||||||
version "5.0.1"
|
version "5.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/react-copy-to-clipboard/-/react-copy-to-clipboard-5.0.1.tgz#8eae107bb400be73132ed3b6a7b4fb156090208e"
|
resolved "https://registry.yarnpkg.com/react-copy-to-clipboard/-/react-copy-to-clipboard-5.0.1.tgz#8eae107bb400be73132ed3b6a7b4fb156090208e"
|
||||||
|
@ -6270,6 +6284,12 @@ react@^16.3.2:
|
||||||
object-assign "^4.1.1"
|
object-assign "^4.1.1"
|
||||||
prop-types "^15.6.0"
|
prop-types "^15.6.0"
|
||||||
|
|
||||||
|
reactcss@^1.2.0:
|
||||||
|
version "1.2.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/reactcss/-/reactcss-1.2.3.tgz#c00013875e557b1cf0dfd9a368a1c3dab3b548dd"
|
||||||
|
dependencies:
|
||||||
|
lodash "^4.0.1"
|
||||||
|
|
||||||
reactstrap@^6.0.1:
|
reactstrap@^6.0.1:
|
||||||
version "6.3.1"
|
version "6.3.1"
|
||||||
resolved "https://registry.yarnpkg.com/reactstrap/-/reactstrap-6.3.1.tgz#263924e4c73ee239f446180d40d9f09cc40607e9"
|
resolved "https://registry.yarnpkg.com/reactstrap/-/reactstrap-6.3.1.tgz#263924e4c73ee239f446180d40d9f09cc40607e9"
|
||||||
|
@ -7338,6 +7358,10 @@ timers-browserify@^2.0.4:
|
||||||
dependencies:
|
dependencies:
|
||||||
setimmediate "^1.0.4"
|
setimmediate "^1.0.4"
|
||||||
|
|
||||||
|
tinycolor2@^1.4.1:
|
||||||
|
version "1.4.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/tinycolor2/-/tinycolor2-1.4.1.tgz#f4fad333447bc0b07d4dc8e9209d8f39a8ac77e8"
|
||||||
|
|
||||||
tmp@^0.0.33:
|
tmp@^0.0.33:
|
||||||
version "0.0.33"
|
version "0.0.33"
|
||||||
resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9"
|
resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9"
|
||||||
|
|
Loading…
Reference in a new issue