From d541543ab347c755c255ac1a883be6684ea04156 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 18 Aug 2018 22:59:53 +0200 Subject: [PATCH] Implemented edition of tags --- package.json | 1 + src/api/ShlinkApiClient.js | 5 +++ src/common/AsideMenu.js | 2 +- src/reducers/index.js | 2 + src/tags/TagCard.js | 15 ++++++- src/tags/TagCard.scss | 4 +- src/tags/helpers/EditTagModal.js | 75 ++++++++++++++++++++++++++++++++ src/tags/reducers/tagEdit.js | 65 +++++++++++++++++++++++++++ src/tags/reducers/tagsList.js | 8 ++++ yarn.lock | 26 ++++++++++- 10 files changed, 197 insertions(+), 6 deletions(-) create mode 100644 src/tags/helpers/EditTagModal.js create mode 100644 src/tags/reducers/tagEdit.js diff --git a/package.json b/package.json index 53e04ed3..2e7b35c0 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "ramda": "^0.25.0", "react": "^16.3.2", "react-chartjs-2": "^2.7.4", + "react-color": "^2.14.1", "react-copy-to-clipboard": "^5.0.1", "react-datepicker": "^1.5.0", "react-dom": "^16.3.2", diff --git a/src/api/ShlinkApiClient.js b/src/api/ShlinkApiClient.js index 767fc742..ee12e094 100644 --- a/src/api/ShlinkApiClient.js +++ b/src/api/ShlinkApiClient.js @@ -59,6 +59,11 @@ export class ShlinkApiClient { .then(() => ({ tags })) .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 = {}) => { if (isEmpty(this._token)) { this._token = await this._authenticate(); diff --git a/src/common/AsideMenu.js b/src/common/AsideMenu.js index 6283277a..92db9013 100644 --- a/src/common/AsideMenu.js +++ b/src/common/AsideMenu.js @@ -52,7 +52,7 @@ export default function AsideMenu({ selectedServer, className, showOnMobile }) { to={`/server/${serverId}/tags`} > - List tags + Manage tags this.setState({ isDeleteModalOpen: !this.state.isDeleteModalOpen }); + const toggleEdit = () => + this.setState({ isEditModalOpen: !this.state.isEditModalOpen }); return ( -
@@ -53,6 +59,11 @@ export default class TagCard extends React.Component { toggle={toggleDelete} isOpen={this.state.isDeleteModalOpen} /> + ); } diff --git a/src/tags/TagCard.scss b/src/tags/TagCard.scss index a2861ff6..3a369dda 100644 --- a/src/tags/TagCard.scss +++ b/src/tags/TagCard.scss @@ -28,6 +28,6 @@ .tag-card__btn { float: right; } -.tag-card__btn:not(:last-child) { - margin-left: 2px; +.tag-card__btn--last { + margin-left: 3px; } diff --git a/src/tags/helpers/EditTagModal.js b/src/tags/helpers/EditTagModal.js new file mode 100644 index 00000000..b7b03f6d --- /dev/null +++ b/src/tags/helpers/EditTagModal.js @@ -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 ( + +
+ Edit tag + + this.setState({ tag: e.target.value })} + placeholder="Tag" + required + className="form-control" + /> + {tagEdit.error && ( +
+ Something went wrong while editing the tag :( +
+ )} +
+ + + + +
+
+ ); + } +} + +export default connect(pick(['tagEdit']), { editTag, tagEdited })(EditTagModal); diff --git a/src/tags/reducers/tagEdit.js b/src/tags/reducers/tagEdit.js new file mode 100644 index 00000000..bfb940f5 --- /dev/null +++ b/src/tags/reducers/tagEdit.js @@ -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, +}); diff --git a/src/tags/reducers/tagsList.js b/src/tags/reducers/tagsList.js index 80b9e448..e75b3bd1 100644 --- a/src/tags/reducers/tagsList.js +++ b/src/tags/reducers/tagsList.js @@ -1,6 +1,7 @@ import ShlinkApiClient from '../../api/ShlinkApiClient'; import { TAG_DELETED } from './tagDelete'; import { reject } from 'ramda'; +import { TAG_EDITED } from './tagEdit'; const LIST_TAGS_START = 'shlink/tagsList/LIST_TAGS_START'; const LIST_TAGS_ERROR = 'shlink/tagsList/LIST_TAGS_ERROR'; @@ -37,6 +38,13 @@ export default function reducer(state = defaultState, action) { ...state, 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: return state; } diff --git a/yarn.lock b/yarn.lock index 0991aac1..eb2340f8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4667,7 +4667,7 @@ lodash.uniq@^4.5.0: version "4.5.0" 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" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.10.tgz#1b7793cf7259ea38fb3661d4d38b3260af8ae4e7" @@ -4737,6 +4737,10 @@ map-visit@^1.0.0: dependencies: 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: version "1.2.17" 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" 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: version "5.0.1" 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" 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: version "6.3.1" resolved "https://registry.yarnpkg.com/reactstrap/-/reactstrap-6.3.1.tgz#263924e4c73ee239f446180d40d9f09cc40607e9" @@ -7338,6 +7358,10 @@ timers-browserify@^2.0.4: dependencies: 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: version "0.0.33" resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9"