From 90d0594c57794ba8b2762d955bf3b0ccdd8e9b12 Mon Sep 17 00:00:00 2001 From: Dmitry Peshkov <31134795+DPeshkoff@users.noreply.github.com> Date: Thu, 25 Nov 2021 14:53:20 +0300 Subject: [PATCH] =?UTF-8?q?LDBR-3.18:=20=D0=9A=D0=BE=D0=BC=D0=BC=D0=B5?= =?UTF-8?q?=D0=BD=D1=82=D0=B0=D1=80=D0=B8=D0=B8=20(#38)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * LDBR-3.18: Добавить комментарии с плохой версткой. * LDBR-3.18: Интегрировать с бэкендом. * LDBR-3.18: Исправить комментарии. * LDBR-3.18: Переписать стиль у комментов * LDBR-3.18: Исправить undefined при добавлении комментария в новую карточку * LDBR-3.18: Починить редактирование комментария * LDBR-3.18: Предзапрос профиля для работы комментов * LDBR-3.18: Исправить замечания линтера * LDBR-3.18: Исправить авторство комментария при создании комментария. Co-authored-by: Georgiy --- src/actions/comments.js | 67 ++++++ src/modules/Helpers/IsAuthorHelper.js | 11 + src/modules/Network/Network.js | 53 +++++ src/popups/Card/CardPopUp.hbs | 46 +++- src/popups/Card/CardPopUp.js | 84 +++++++ src/popups/Card/CardPopUp.scss | 31 +++ src/stores/BoardStore/BoardStore.js | 329 +++++++++++++++++++++++++- src/styles/scss/Common.scss | 37 +++ src/styles/scss/PopUp.scss | 6 + 9 files changed, 661 insertions(+), 3 deletions(-) create mode 100644 src/actions/comments.js create mode 100644 src/modules/Helpers/IsAuthorHelper.js diff --git a/src/actions/comments.js b/src/actions/comments.js new file mode 100644 index 00000000..ed0a96ea --- /dev/null +++ b/src/actions/comments.js @@ -0,0 +1,67 @@ +'use strict'; + +// Modules +import Dispatcher from '../modules/Dispatcher/Dispatcher.js'; + +/** + * Константа, содержащая в себе типы действий для списка досок. + */ +export const CommentsActionTypes = { + CARD_ADD_COMMENT: 'card/comment/add', + CARD_EDIT_COMMENT: 'card/comment/edit', + CARD_UPDATE_COMMENT: 'card/comment/update', + CARD_DELETE_COMMENT: 'card/comment/delete', +}; + +/** + * Класс, содержащий в себе действия в системе. + */ +export const commentsActions = { + /** + * Создает комментарий + * @param {String} text текст комментария + */ + createComment(text) { + Dispatcher.dispatch({ + actionName: CommentsActionTypes.CARD_ADD_COMMENT, + data: {text}, + }); + }, + + /** + * Удалить комментарий (не спрашиваем, хотим ли) + * @param {Number} cmid id комментария + */ + deleteComment(cmid) { + Dispatcher.dispatch({ + actionName: CommentsActionTypes.CARD_DELETE_COMMENT, + data: {cmid}, + }); + }, + + /** + * Переключает комментарий в режим редактирования + * @param {Number} cmid id комментария + */ + editComment(cmid) { + Dispatcher.dispatch({ + actionName: CommentsActionTypes.CARD_EDIT_COMMENT, + data: {cmid}, + }); + }, + + /** + * Обновляет список карточек + * @param {String} text текст комментария + * @param {Number} cmid id комментария + */ + updateComment(text, cmid) { + Dispatcher.dispatch({ + actionName: CommentsActionTypes.CARD_UPDATE_COMMENT, + data: { + text, + cmid, + }, + }); + }, +}; diff --git a/src/modules/Helpers/IsAuthorHelper.js b/src/modules/Helpers/IsAuthorHelper.js new file mode 100644 index 00000000..a21d087d --- /dev/null +++ b/src/modules/Helpers/IsAuthorHelper.js @@ -0,0 +1,11 @@ +import UserStore from '../../stores/UserStore/UserStore.js'; + +/** + * Handelbars helper, проверяет авторство комментария + * @param {any} author - логин автора комментария + * @return {boolean} - результат сравнения + * @constructor + */ +export default function IsAuthorHelper(author) { + return author === UserStore.getContext('userName'); +}; diff --git a/src/modules/Network/Network.js b/src/modules/Network/Network.js index 11d84321..cabd6e6c 100644 --- a/src/modules/Network/Network.js +++ b/src/modules/Network/Network.js @@ -20,6 +20,7 @@ class Network { board: 'api/boards', card: 'api/cards', cardlist: 'api/cardLists', + comments: 'api/comments', usersearch: { card: 'api/usersearch/card', board: 'api/usersearch/board', @@ -538,6 +539,58 @@ class Network { return this.httpRequest(`http://${this.BackendUrl}:${this.BackendPort}` + `/${this._endpoints.checklistsItems}/${chliid}`, options); } + + + /** + * Метод, реализующий запрос POST /api/comments. + * @param {object} data полезная нагрузка запроса + * @return {Promise} промис запроса + */ + async createComment(data) { + const options = { + method: 'post', + body: JSON.stringify(data), + }; + return this.httpRequest( + `http://${this.BackendUrl}:${this.BackendPort}/${this._endpoints.comments}`, + options); + } + + /** + * Метод, реализующий запрос PUT /api/comments. + * @param {Object} data полезная нагрузка запроса + * @param {Number} cmid id обновляемого комментария + * @return {Promise} промис запроса + */ + async updateComment(data) { + const options = { + method: 'put', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(data), + }; + return this.httpRequest( + `http://${this.BackendUrl}:${this.BackendPort}/${this._endpoints.comments}/${data.cmid}`, + options); + } + + /** + * Метод, реализующий запрос DELETE /api/comments/:cmid. + * @param {Number} data данные запроса + * @return {Promise} промис запроса + */ + async deleteComment(data) { + const options = { + method: 'delete', + headers: { + 'Content-Type': 'application/json', + }, + }; + return this.httpRequest( + `http://${this.BackendUrl}:${this.BackendPort}/${this._endpoints.comments}/${data.cmid}`, + options); + } } export default new Network(); diff --git a/src/popups/Card/CardPopUp.hbs b/src/popups/Card/CardPopUp.hbs index 221e05c4..5e9b7175 100644 --- a/src/popups/Card/CardPopUp.hbs +++ b/src/popups/Card/CardPopUp.hbs @@ -88,14 +88,56 @@ {{#if errors}}
{{errors}}
{{/if}} - {{# if edit }} + {{#if edit }} - {{ else }} + {{else }} {{/if}} + {{#if edit}} +
+
+ +
+
+ +
+
+
+ {{#each comments}} +
+ +
+ {{#if this.edit}} + + {{else}} +
{{this.text}}
+
{{this.date}}
+ {{/if}} +
+
+ {{#if this.edit}} +
+ {{/if}} + {{#if (IsAuthorHelper this.user.userName)}} +
+
+ {{/if}} +
+
+ {{/each}} +
+ {{/if}} +
diff --git a/src/popups/Card/CardPopUp.js b/src/popups/Card/CardPopUp.js index 026eca57..cb3f1264 100644 --- a/src/popups/Card/CardPopUp.js +++ b/src/popups/Card/CardPopUp.js @@ -6,6 +6,7 @@ import template from './CardPopUp.hbs'; // Actions import {cardActions} from '../../actions/card.js'; +import {commentsActions} from '../../actions/comments.js'; import {checkListAction} from '../../actions/checklist'; // Стили: @@ -37,6 +38,13 @@ export default class CardPopUp extends BaseComponent { positionSelect: document.getElementById('cardPopUpPositionId'), card_name: document.getElementById('cardPopUpTitleId'), description: document.getElementById('cardPopUpDescriptionId'), + comments: { + editBtns: document.querySelectorAll('.editComment'), + saveBtns: document.querySelectorAll('.saveComment'), + deleteBtns: document.querySelectorAll('.deleteComment'), + }, + newCommentText: document.getElementById('newCommentTextId'), + addCommentBtn: document.getElementById('createCommentId'), deadline: document.getElementById('cardPopUpDeadlineId'), assigneeBtn: document.getElementById('cardPopUpAddAssigneeBtnId'), checkList: { @@ -66,6 +74,16 @@ export default class CardPopUp extends BaseComponent { this._elements.closeBtn?.addEventListener('click', this._onPopUpClose); this._elements.createBtn?.addEventListener('click', this._onCreate); this._elements.saveBtn?.addEventListener('click', this._onSave); + this._elements.comments?.editBtns?.forEach((editCommentBtn)=>{ + editCommentBtn.addEventListener('click', this._onEditComment); + }); + this._elements.comments?.saveBtns?.forEach((saveCommentBtn)=>{ + saveCommentBtn.addEventListener('click', this._onUpdateComment); + }); + this._elements.comments?.deleteBtns?.forEach((deleteCommentBtn)=>{ + deleteCommentBtn.addEventListener('click', this._onDeleteComment); + }); + this._elements.addCommentBtn?.addEventListener('click', this._onCreateComment); this._elements.deadline?.addEventListener('click', this._onDeadlineClick); this._elements.assigneeBtn?.addEventListener('click', this._onAssigneeClick); @@ -109,6 +127,16 @@ export default class CardPopUp extends BaseComponent { this._elements.closeBtn?.removeEventListener('click', this._onPopUpClose); this._elements.createBtn?.removeEventListener('click', this._onCreate); this._elements.saveBtn?.removeEventListener('click', this._onSave); + this._elements.comments?.editBtns?.forEach((editCommentBtn)=>{ + editCommentBtn.removeEventListener('click', this._onEditComment); + }); + this._elements.comments?.saveBtns?.forEach((saveCommentBtn)=>{ + saveCommentBtn.removeEventListener('click', this._onUpdateComment); + }); + this._elements.comments?.deleteBtns?.forEach((deleteCommentBtn)=>{ + deleteCommentBtn.removeEventListener('click', this._onDeleteComment); + }); + this._elements.addCommentBtn?.removeEventListener('click', this._onCreateComment); this._elements.deadline?.removeEventListener('click', this._onDeadlineClick); this._elements.assigneeBtn?.removeEventListener('click', this._onAssigneeClick); @@ -147,6 +175,12 @@ export default class CardPopUp extends BaseComponent { this._onPopUpClose = this._onPopUpClose.bind(this); this._onCreate = this._onCreate.bind(this); this._onSave = this._onSave.bind(this); + + /* Comments */ + this._onDeleteComment = this._onDeleteComment.bind(this); + this._onEditComment = this._onEditComment.bind(this); + this._onUpdateComment = this._onUpdateComment.bind(this); + this._onCreateComment = this._onCreateComment.bind(this); this._onDeadlineClick = this._onDeadlineClick.bind(this); this._onAssigneeClick = this._onAssigneeClick.bind(this); @@ -209,6 +243,56 @@ export default class CardPopUp extends BaseComponent { ); } + /** + * Callback, вызываемый при нажатии "Создать комментарий" + * @param {Event} event объект события + * @private + */ + _onCreateComment(event) { + event.preventDefault(); + commentsActions.createComment( + this._elements.newCommentText.value, + ); + } + + /** + * Callback, вызываемый при нажатии редактировании комментария + * @param {Event} event объект события + * @private + */ + _onEditComment(event) { + event.preventDefault(); + commentsActions.editComment( + parseInt(event.target.dataset.id, 10), + ); + } + + /** + * Callback, вызываемый при нажатии "Обновить комментарий" + * @param {Event} event объект события + * @private + */ + _onUpdateComment(event) { + event.preventDefault(); + const newComment = event.target.closest('.comment') + .querySelector('.commentInput').value; + commentsActions.updateComment( + newComment, + parseInt(event.target.dataset.id, 10), + ); + } + + /** + * Callback, вызываемый при удалении комментария + * @param {Event} event объект события + * @private + */ + _onDeleteComment(event) { + event.preventDefault(); + commentsActions.deleteComment( + parseInt(event.target.dataset.id, 10), + ); + } /** * Callback, вызываемый при редактировании дедлайна * @param {Event} event объект события diff --git a/src/popups/Card/CardPopUp.scss b/src/popups/Card/CardPopUp.scss index be225401..38bac613 100644 --- a/src/popups/Card/CardPopUp.scss +++ b/src/popups/Card/CardPopUp.scss @@ -1,4 +1,35 @@ @import '../../styles/scss/Constants'; +@import '../../styles/scss/Common'; + +.comments-wrapper { + display: flex; + flex-direction: column; +} + +.comment { + display: flex; + flex-direction: row; + + &__avatar { + @extend .avatar; + width: 25px; + height: 25px; + } + + &__content { + margin-left: 10px; + flex-grow: 1; + } + + &__text { + font-size: 16px; + } + + &__btns { + display: flex; + flex-direction: row; + } +} .check-list { display: flex; diff --git a/src/stores/BoardStore/BoardStore.js b/src/stores/BoardStore/BoardStore.js index 12793d00..68fc7123 100644 --- a/src/stores/BoardStore/BoardStore.js +++ b/src/stores/BoardStore/BoardStore.js @@ -6,6 +6,7 @@ import {CardListActionTypes} from '../../actions/cardlist.js'; import {CheckListActionTypes} from '../../actions/checklist'; import {CardActionTypes} from '../../actions/card.js'; import {BoardActionTypes} from '../../actions/board.js'; +import {CommentsActionTypes} from '../../actions/comments.js'; // Modules import Network from '../../modules/Network/Network.js'; @@ -18,6 +19,7 @@ import {CheckLists, ConstantMessages, // Stores import UserStore from '../UserStore/UserStore.js'; +import SettingsStore from '../SettingsStore/SettingsStore.js'; /** * Класс, реализующий хранилище доски @@ -205,6 +207,28 @@ class BoardStore extends BaseStore { this._emitChange(); break; + /* Card comments */ + + case CommentsActionTypes.CARD_ADD_COMMENT: + await this._createCardComment(action.data); + this._emitChange(); + break; + + case CommentsActionTypes.CARD_DELETE_COMMENT: + await this._deleteCardComment(action.data); + this._emitChange(); + break; + + case CommentsActionTypes.CARD_EDIT_COMMENT: + await this._editCardComment(action.data); + this._emitChange(); + break; + + case CommentsActionTypes.CARD_UPDATE_COMMENT: + await this._updateCardComment(action.data); + this._emitChange(); + break; + case CardActionTypes.CARD_UPDATE_STATUS: await this._updateDeadlineCheck(action.data); this._emitChange(); @@ -745,6 +769,7 @@ class BoardStore extends BaseStore { card_name: data.card_name, description: data.description, pos: this._getCardListById(data.clid).cards.length + 1, + comments: [], deadline: data.deadline, deadline_check: false, deadlineStatus: validator.validateDeadline( @@ -782,9 +807,14 @@ class BoardStore extends BaseStore { * @private */ _getCardById(clid, cid) { - return this._getCardListById(clid).cards.find((card) => { + const result = this._getCardListById(clid).cards.find((card) => { return card.cid === cid; }); + if (!result) { + throw new Error(`BoardStore: ошибка в функции _getCardById' + + '(карточка в столбце ${clid} с айди ${cid} не найдена)`); + } + return result; } /** @@ -806,6 +836,7 @@ class BoardStore extends BaseStore { (_, index) => index + 1), card_name: card.card_name, description: card.description, + comments: card.comments, deadline: card.deadline, deadline_check: card.deadline_check, errors: null, @@ -1553,6 +1584,302 @@ class BoardStore extends BaseStore { return; } } + + /** + * Создает комментарий + * @param {Object} data данные + * @return {Promise} + * @private + */ + async _createCardComment(data) { + const context = this._storage.get('card-popup'); + + const comment = { + cid: context.cid, + text: data.text, + }; + + let payload; + + try { + payload = await Network.createComment(comment); + } catch (error) { + console.log('Unable to connect to backend, reason: ', error); + return; + } + + switch (payload.status) { + case HttpStatusCodes.Ok: + context.comments.push({ + cmid: payload.data.cmid, + cid: this._storage.get('card-popup').cid, + user: { + userName: SettingsStore.getContext('login'), + avatar: SettingsStore.getContext('avatar'), + }, + text: data.text, + date: payload.data.date, + }); + + return; + + case HttpStatusCodes.Forbidden: + this._storage.get('card-popup').errors = ConstantMessages.BoardNoAccess; + return; + + default: + this._storage.get('card-popup').errors = ConstantMessages.CardListErrorOnServer; + return; + } + } + + /** + * Переключает комментарий в режим редактирования + * @param {Object} data - данные по комменту + * @private + */ + async _editCardComment(data) { + const comments = this._storage.get('card-popup').comments; + const comment = comments.find((comment) => { + return comment.cmid === data.cmid; + }); + if (!comment) { + throw new Error(`BoardStore: комментарий с индексом ${data.cmid} не найден`); + } + comment.edit = !comment.edit; + } + + /** + * Обновляет комментарий + * @param {Object} data новые данные + * @return {Promise} + * @private + */ + async _updateCardComment(data) { + let payload; + + try { + payload = await Network.updateComment(data); + } catch (error) { + console.log('Unable to connect to backend, reason: ', error); + return; + } + + switch (payload.status) { + case HttpStatusCodes.Ok: + const comments = this._storage.get('card-popup').comments; + const comment = comments.find((item) => { + return item.cmid === data.cmid; + }); + + if (!comment) { + throw new Error(`BoardStore: комментарий ${data.cmid} не найден.`); + } + + comment.text = data.text; + comment.edit = false; + + return; + + case HttpStatusCodes.Forbidden: + this._storage.get('card-popup').errors = ConstantMessages.BoardNoAccess; + return; + + default: + this._storage.get('card-popup').errors = ConstantMessages.CardListErrorOnServer; + return; + } + } + + /** + * Удаляет комментарий + * @param {Object} data данные + * @return {Promise} + * @private + */ + async _deleteCardComment(data) { + let payload; + + try { + payload = await Network.deleteComment(data); + } catch (error) { + console.log('Unable to connect to backend, reason: ', error); + return; + } + + switch (payload.status) { + case HttpStatusCodes.Ok: + const card = this._getCardById( + this._storage.get('card-popup').clid, + this._storage.get('card-popup').cid, + ); + + const cmid = card.comments.findIndex(({cmid}) => cmid === data.cmid); + if (cmid === -1) { + throw new Error(`BoardStore: комментарий ${data.cmid} не найден.`); + } + + card.comments.splice(cmid, 1); + + return; + + case HttpStatusCodes.Forbidden: + this._storage.get('card-popup').errors = ConstantMessages.BoardNoAccess; + return; + + default: + this._storage.get('card-popup').errors = ConstantMessages.CardListErrorOnServer; + return; + } + } + + /** + * Создает комментарий + * @param {Object} data данные + * @return {Promise} + * @private + */ + async _createCardComment(data) { + const context = this._storage.get('card-popup'); + + const comment = { + cid: context.cid, + text: data.text, + }; + + let payload; + + try { + payload = await Network.createComment(comment); + } catch (error) { + console.log('Unable to connect to backend, reason: ', error); + return; + } + + switch (payload.status) { + case HttpStatusCodes.Ok: + context.comments.push({ + cmid: payload.data.cmid, + cid: this._storage.get('card-popup').cid, + user: { + userName: SettingsStore.getContext('login'), + avatar: SettingsStore.getContext('avatar'), + }, + text: data.text, + date: payload.data.date, + }); + + return; + + case HttpStatusCodes.Forbidden: + this._storage.get('card-popup').errors = ConstantMessages.BoardNoAccess; + return; + + default: + this._storage.get('card-popup').errors = ConstantMessages.CardListErrorOnServer; + return; + } + } + + /** + * Переключает комментарий в режим редактирования + * @param {Object} data - данные по комменту + * @private + */ + async _editCardComment(data) { + const comments = this._storage.get('card-popup').comments; + const comment = comments.find((comment) => { + return comment.cmid === data.cmid; + }); + if (!comment) { + throw new Error(`BoardStore: комментарий с индексом ${data.cmid} не найден`); + } + comment.edit = !comment.edit; + } + + /** + * Обновляет комментарий + * @param {Object} data новые данные + * @return {Promise} + * @private + */ + async _updateCardComment(data) { + let payload; + + try { + payload = await Network.updateComment(data); + } catch (error) { + console.log('Unable to connect to backend, reason: ', error); + return; + } + + switch (payload.status) { + case HttpStatusCodes.Ok: + const comments = this._storage.get('card-popup').comments; + const comment = comments.find((item) => { + return item.cmid === data.cmid; + }); + + if (!comment) { + throw new Error(`BoardStore: комментарий ${data.cmid} не найден.`); + } + + comment.text = data.text; + comment.edit = false; + + return; + + case HttpStatusCodes.Forbidden: + this._storage.get('card-popup').errors = ConstantMessages.BoardNoAccess; + return; + + default: + this._storage.get('card-popup').errors = ConstantMessages.CardListErrorOnServer; + return; + } + } + + /** + * Удаляет комментарий + * @param {Object} data данные + * @return {Promise} + * @private + */ + async _deleteCardComment(data) { + let payload; + + try { + payload = await Network.deleteComment(data); + } catch (error) { + console.log('Unable to connect to backend, reason: ', error); + return; + } + + switch (payload.status) { + case HttpStatusCodes.Ok: + const card = this._getCardById( + this._storage.get('card-popup').clid, + this._storage.get('card-popup').cid, + ); + + const cmid = card.comments.findIndex(({cmid}) => cmid === data.cmid); + if (cmid === -1) { + throw new Error(`BoardStore: комментарий ${data.cmid} не найден.`); + } + + card.comments.splice(cmid, 1); + + return; + + case HttpStatusCodes.Forbidden: + this._storage.get('card-popup').errors = ConstantMessages.BoardNoAccess; + return; + + default: + this._storage.get('card-popup').errors = ConstantMessages.CardListErrorOnServer; + return; + } + } } export default new BoardStore(); diff --git a/src/styles/scss/Common.scss b/src/styles/scss/Common.scss index 1468b9d0..89395897 100644 --- a/src/styles/scss/Common.scss +++ b/src/styles/scss/Common.scss @@ -140,6 +140,43 @@ body { } } +.material-icon { + @extend .material-icons; + opacity: 0.3; + cursor: pointer; + + &:hover { + opacity: 1.0; + } + + &-delete { + @extend .material-icon; + &::before { + content: 'delete'; + } + } + + &-edit { + @extend .material-icon; + &::before { + content: 'edit'; + } + } + + &-add { + @extend .material-icon; + &::before { + content: 'add'; + } + } + + &-save { + @extend .material-icon; + &::before { + content: 'save'; + } + } +} .horizontal-line { border-bottom: solid 2px $tertiary-bg-color; diff --git a/src/styles/scss/PopUp.scss b/src/styles/scss/PopUp.scss index d7dc5871..04a716c0 100644 --- a/src/styles/scss/PopUp.scss +++ b/src/styles/scss/PopUp.scss @@ -55,3 +55,9 @@ padding: 15px 30px 0; } } + +.sub-text { + color:rgb(170, 170, 170); + font-family:verdana; + font-size:11px; +}