From 72d92acbe5e8e506c9d01db9214d6bed9358bb67 Mon Sep 17 00:00:00 2001 From: GeorgiyX <34867130+GeorgiyX@users.noreply.github.com> Date: Thu, 16 Dec 2021 14:07:51 +0300 Subject: [PATCH] =?UTF-8?q?LDBR-4.12:=20=D0=92=D0=BB=D0=BE=D0=B6=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D1=8F=20(#49)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * LDBR-4.12: Реализовать функционал работы с вложенями * LDBR-4.7: Внести правки в работу с аттачами Co-authored-by: DPeshkoff --- .github/workflows/Deploy.yml | 1 + src/actions/attachments.js | 51 +++++++++++++++ src/constants/constants.js | 2 + src/modules/Network/Network.js | 35 ++++++++++ src/popups/Card/CardPopUp.hbs | 15 +++++ src/popups/Card/CardPopUp.js | 56 ++++++++++++++++ src/popups/Card/CardPopUp.scss | 45 +++++++++++++ src/stores/BoardStore/BoardStore.js | 99 +++++++++++++++++++++++++++++ src/styles/scss/Common.scss | 16 +++++ 9 files changed, 320 insertions(+) create mode 100644 src/actions/attachments.js diff --git a/.github/workflows/Deploy.yml b/.github/workflows/Deploy.yml index 32b4874d..231df935 100644 --- a/.github/workflows/Deploy.yml +++ b/.github/workflows/Deploy.yml @@ -57,4 +57,5 @@ jobs: source: "dist/*" target: "/home/ubuntu/01-frontend/" overwrite: true + rm: true \ No newline at end of file diff --git a/src/actions/attachments.js b/src/actions/attachments.js new file mode 100644 index 00000000..f001df24 --- /dev/null +++ b/src/actions/attachments.js @@ -0,0 +1,51 @@ +'use strict'; + +// Modules +import Dispatcher from '../modules/Dispatcher/Dispatcher.js'; + +/** + * Константа, содержащая в себе типы действий для списка досок. + */ +export const AttachmentsActionTypes = { + UPLOAD: 'attach/upload', + DELETE: 'attach/delete', + DOWNLOAD: 'attach/download', +}; + +/** + * Класс, содержащий в себе действия в системе. + */ +export const attachmentsActions = { + /** + * Загружает файл вложения + * @param {File} file файл вложения + */ + uploadAttachment(file) { + Dispatcher.dispatch({ + actionName: AttachmentsActionTypes.UPLOAD, + data: {file}, + }); + }, + + /** + * Удаляет файл вложения + * @param {Number} atid id вложения + */ + deleteAttachment(atid) { + Dispatcher.dispatch({ + actionName: AttachmentsActionTypes.DELETE, + data: {atid}, + }); + }, + + /** + * Скачивает файл вложения + * @param {Number} atid id вложения + */ + downloadAttachment(atid) { + Dispatcher.dispatch({ + actionName: AttachmentsActionTypes.DOWNLOAD, + data: {atid}, + }); + }, +}; diff --git a/src/constants/constants.js b/src/constants/constants.js index f919f257..8fc55d83 100644 --- a/src/constants/constants.js +++ b/src/constants/constants.js @@ -97,6 +97,7 @@ export const ConstantMessages = { CardTitleTooLong: 'Название карточки слишком длинное', CardErrorOnServer: 'Не удалось создать карточку, попробуйте позднее', UnsuccessfulRequest: 'Неудачный запрос, попробуйте позднее :]', + AttachmentSizeTooBig: 'Слишком большой размер вложения', CantCopyToClipBoard: 'Не удалось скопировать текст', WrongTagNameLength: 'Введите имя тега длиной от 1 до 40 символов', @@ -104,6 +105,7 @@ export const ConstantMessages = { export const BoardStoreConstants = { MinUserNameSearchLength: 3, + MaxAttachmentSize: 1024 * 1024 * 50, }; export const CheckLists = { diff --git a/src/modules/Network/Network.js b/src/modules/Network/Network.js index 50eb87c1..57370a77 100644 --- a/src/modules/Network/Network.js +++ b/src/modules/Network/Network.js @@ -28,6 +28,7 @@ class Network { team: 'api/teams', checklists: 'api/checkLists', checklistsItems: 'api/checkListItems', + attachments: 'api/attachments', tags: 'api/tags', }; @@ -709,6 +710,40 @@ class Network { `http://${this.BackendUrl}:${this.BackendPort}/${this._endpoints.card}/access/${cid}`, options); } + + /** + * Метод, реализующий запрос POST /api/attachments/:cid. + * @param {Object} data файл аттача + * @param {Number} cid id карточки + * @return {Promise} промис запроса + */ + async uploadAttachment(data, cid) { + const options = { + method: 'post', + body: data, + }; + return this.httpRequest( + `http://${this.BackendUrl}:${this.BackendPort}/` + + `${this._endpoints.attachments}/${cid}`, + options); + } + + /** + * Метод, реализующий запрос DELETE /api/attachments/:atid. + * @param {Number} atid id аттача + * @return {Promise} промис запроса + */ + async deleteAttachment(atid) { + const options = { + method: 'delete', + headers: { + 'Content-Type': 'application/json', + }, + }; + return this.httpRequest( + `http://${this.BackendUrl}:${this.BackendPort}/${this._endpoints.attachments}/${atid}`, + options); + } } export default new Network(); diff --git a/src/popups/Card/CardPopUp.hbs b/src/popups/Card/CardPopUp.hbs index 2d3db2f7..a6ff3369 100644 --- a/src/popups/Card/CardPopUp.hbs +++ b/src/popups/Card/CardPopUp.hbs @@ -102,6 +102,21 @@
{{errors}}
{{/if}} {{#if edit }} + {{#if attachments}}Вложения{{/if}} +
+ {{#each attachments}} +
+
+
{{this.file_pub_name}}
+
+
+
+ {{/each}} +
+ diff --git a/src/popups/Card/CardPopUp.js b/src/popups/Card/CardPopUp.js index 71d4d585..79437814 100644 --- a/src/popups/Card/CardPopUp.js +++ b/src/popups/Card/CardPopUp.js @@ -11,6 +11,7 @@ import {checkListAction} from '../../actions/checklist'; // Стили: import './CardPopUp.scss'; +import {attachmentsActions} from '../../actions/attachments'; import {tagsActions} from '../../actions/tags'; /** @@ -61,6 +62,9 @@ export default class CardPopUp extends BaseComponent { deleteBtn: document.querySelectorAll('.checklist-item-delete'), label: document.querySelectorAll('.checklist-item__label'), }, + attachInput: document.getElementById('attachmentInputId'), + downloadAttachBtn: document.querySelectorAll('.attachment__download-btn'), + deleteAttachBtn: document.querySelectorAll('.attachment__delete-btn'), scrollZone: document.getElementById('cardPopUpScrollZoneId'), tags: document.querySelectorAll('.card-popup-tag'), addTagBtn: document.getElementById('addTagCardPopUpId'), @@ -133,6 +137,15 @@ export default class CardPopUp extends BaseComponent { if (this._elements.scrollZone) { this._elements.scrollZone.scrollTop = this.context.get('card-popup').scroll; } + + /* Attachments */ + this._elements.attachInput?.addEventListener('change', this._onUploadAttachment); + this._elements.downloadAttachBtn?.forEach((element) => { + element.addEventListener('click', this._onDownloadAttachment); + }); + this._elements.deleteAttachBtn?.forEach((element) => { + element.addEventListener('click', this._onDeleteAttachment); + }); }; /** @@ -184,6 +197,14 @@ export default class CardPopUp extends BaseComponent { element.removeEventListener('click', this._onDeleteCheckListItem); }); + /* Attachments */ + this._elements.attachInput?.removeEventListener('change', this._onUploadAttachment); + this._elements.downloadAttachBtn?.forEach((element) => { + element.removeEventListener('click', this._onDownloadAttachment); + }); + this._elements.deleteAttachBtn?.forEach((element) => { + element.removeEventListener('click', this._onDeleteAttachment); + }); /* Tags */ this._elements.tags?.forEach((tag) => { tag.removeEventListener('click', this._onShowTagListPopUpCard); @@ -221,6 +242,10 @@ export default class CardPopUp extends BaseComponent { this._onSaveChekListItem = this._onSaveChekListItem.bind(this); this._onToggleChekListItem = this._onToggleChekListItem.bind(this); + /* Attachments */ + this._onDownloadAttachment = this._onDownloadAttachment.bind(this); + this._onUploadAttachment = this._onUploadAttachment.bind(this); + this._onDeleteAttachment = this._onDeleteAttachment.bind(this); /* Tags */ this._onShowTagListPopUpCard = this._onShowTagListPopUpCard.bind(this); } @@ -475,6 +500,37 @@ export default class CardPopUp extends BaseComponent { cardActions.changeScroll(this._elements.scrollZone.scrollTop); } + /** + * CallBack на загрузку вложения + * @param {Event} event - объект события + * @private + */ + _onUploadAttachment(event) { + event.preventDefault(); + attachmentsActions.uploadAttachment(event.target.files[0]); + } + + /** + * CallBack на удаление вложения + * @param {Event} event - объект события + * @private + */ + _onDeleteAttachment(event) { + event.preventDefault(); + const atid = Number.parseInt(event.target.closest('div.attachment').dataset.id, 10); + attachmentsActions.deleteAttachment(atid); + } + + /** + * CallBack на скачивание вложения + * @param {Event} event - объект события + * @private + */ + _onDownloadAttachment(event) { + event.preventDefault(); + const atid = Number.parseInt(event.target.closest('div.attachment').dataset.id, 10); + attachmentsActions.downloadAttachment(atid); + } /** * CallBack на добаление тега * @param {Event} event - объект события diff --git a/src/popups/Card/CardPopUp.scss b/src/popups/Card/CardPopUp.scss index 599e9412..9e2cb91e 100644 --- a/src/popups/Card/CardPopUp.scss +++ b/src/popups/Card/CardPopUp.scss @@ -84,6 +84,51 @@ input.checklist-item__input { } } + +.attachments { + display: flex; + flex-direction: column; + row-gap: 5px; +} + +.attachment { + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + + + &__icon { + } + + &__title { + margin-left: 5px; + flex-grow: 1; + display: flex; + flex-direction: row; + align-items: center; + } + + &__download-btn { + + } + + &__delete-btn { + + } + + &__button { + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + column-gap: 5px; + } +} + +#attachmentInputId { + display: none; +} .card-popup-tags { display: flex; flex-direction: row; diff --git a/src/stores/BoardStore/BoardStore.js b/src/stores/BoardStore/BoardStore.js index 8eb83d60..6b75619c 100644 --- a/src/stores/BoardStore/BoardStore.js +++ b/src/stores/BoardStore/BoardStore.js @@ -27,6 +27,7 @@ import { // Stores import UserStore from '../UserStore/UserStore.js'; import SettingsStore from '../SettingsStore/SettingsStore.js'; +import {AttachmentsActionTypes} from '../../actions/attachments'; /** @@ -354,6 +355,21 @@ class BoardStore extends BaseStore { this._emitChange(); break; + /* Attachments */ + case AttachmentsActionTypes.UPLOAD: + await this._uploadAttachment(action.data); + this._emitChange(); + break; + + case AttachmentsActionTypes.DELETE: + await this._deleteAttachment(action.data); + this._emitChange(); + break; + + case AttachmentsActionTypes.DOWNLOAD: + this._downloadAttachment(action.data); + break; + /* Invite: */ case InviteActionTypes.GO_BOARD_INVITE: await this._openBoardInvite(action.data); @@ -534,6 +550,7 @@ class BoardStore extends BaseStore { card.deadlineStatus = validator.validateDeadline(card.deadline, card.deadline_check); card.deadlineCheck = card.deadline_check; card.deadlineDate = (new Date(card.deadline)).toLocaleDateString('ru-RU', options); + card.attachments = card.attachments || []; // Сохраним в карточке ссылки на теже теги, что и в списке тегов card.tags = card.tags.map((tag) => { return this._getTagById(tag.tgid); @@ -1034,6 +1051,7 @@ class BoardStore extends BaseStore { return {...list, check_list_items: items, edit: false}; }), scroll: 0, + attachments: card.attachments, }); } @@ -1984,6 +2002,87 @@ class BoardStore extends BaseStore { this._storage.get('card-popup').scroll = data.scrollValue; } + /** + * Загружает файл вложения + * @param {Object} data данные + * @private + */ + async _uploadAttachment(data) { + const cardContext = this._storage.get('card-popup'); + cardContext.errors = null; + if (data.file.size > BoardStoreConstants.MaxAttachmentSize) { + cardContext.errors = ConstantMessages.AttachmentSizeTooBig; + return; + } + + const attachmentForm = new FormData(); + attachmentForm.append('attachment', data.file); + + let payload; + + try { + payload = await Network.uploadAttachment(attachmentForm, cardContext.cid); + } catch (error) { + console.log('Unable to connect to backend, reason: ', error); + return; + } + + switch (payload.status) { + case HttpStatusCodes.Ok: + const card = this._getCardById(cardContext.clid, cardContext.cid); + card.attachments.push(payload.data); + return; + + default: + cardContext.errors = ConstantMessages.UnsuccessfulRequest; + return; + } + } + + /** + * Удаляет файл вложения + * @param {Object} data данные + * @private + */ + async _deleteAttachment(data) { + const cardContext = this._storage.get('card-popup'); + + let payload; + + try { + payload = await Network.deleteAttachment(data.atid); + } catch (error) { + console.log('Unable to connect to backend, reason: ', error); + return; + } + + switch (payload.status) { + case HttpStatusCodes.Ok: + const attachment = cardContext.attachments.find((attach) => { + return attach.atid === data.atid; + }); + cardContext.attachments.splice(cardContext.attachments.indexOf(attachment), 1); + return; + + default: + cardContext.errors = ConstantMessages.UnsuccessfulRequest; + return; + } + } + + /** + * Скачивает файл вложения + * @param {Object} data данные + * @private + */ + _downloadAttachment(data) { + const cardContext = this._storage.get('card-popup'); + const attachment = cardContext.attachments.find((attach) => { + return attach.atid === data.atid; + }); + window.open(attachment.file_tech_name, `Download: ${attachment.file_pub_name}`); + } + /** * Приглашает пользователя в доску * @param {Object} data инвайт diff --git a/src/styles/scss/Common.scss b/src/styles/scss/Common.scss index fe28916f..78cd99d2 100644 --- a/src/styles/scss/Common.scss +++ b/src/styles/scss/Common.scss @@ -308,6 +308,22 @@ div#root { content: 'save'; } } + + &-download { + @extend .material-icon; + + &::before { + content: 'download'; + } + } + + &-file { + @extend .material-icon; + + &::before { + content: 'description'; + } + } }