diff --git a/.github/workflows/Deploy.yml b/.github/workflows/Deploy.yml index a3dd4b6a..32b4874d 100644 --- a/.github/workflows/Deploy.yml +++ b/.github/workflows/Deploy.yml @@ -51,9 +51,10 @@ jobs: - name: Transfer build files to server uses: appleboy/scp-action@master with: - host: 95.163.213.142 + host: ${{ secrets.HOST_IP }} username: ubuntu key: ${{ secrets.PRIVATE_KEY }} source: "dist/*" target: "/home/ubuntu/01-frontend/" + overwrite: true \ No newline at end of file diff --git a/src/actions/invite.js b/src/actions/invite.js new file mode 100644 index 00000000..fff4304a --- /dev/null +++ b/src/actions/invite.js @@ -0,0 +1,80 @@ +'use strict'; + +// Modules +import Dispatcher from '../modules/Dispatcher/Dispatcher.js'; + +/** + * Константа, содержащая в себе типы действий для ссылок-приглашений + */ +export const InviteActionTypes = { + GO_BOARD_INVITE: 'go/invite/board', + GO_CARD_INVITE: 'go/invite/card', + REFRESH_BOARD_LINK: 'refresh/invite/board', + REFRESH_CARD_LINK: 'refresh/invite/card', + COPY_BOARD_LINK: 'copy/invite/board', + COPY_CARD_LINK: 'copy/invite/card', +}; + +/** + * Объект, содержащий в себе действия в системе связанные с приглашениями. + */ +export const inviteActions = { + + /** + * Приглашает пользователя в доску + * @param {String} accessPath + */ + openBoardInvite(accessPath) { + Dispatcher.dispatch({ + actionName: InviteActionTypes.GO_BOARD_INVITE, + data: {accessPath}, + }); + }, + + /** + * Приглашает пользователя в карточку + * @param {String} accessPath + */ + openCardInvite(accessPath) { + Dispatcher.dispatch({ + actionName: InviteActionTypes.GO_CARD_INVITE, + data: {accessPath}, + }); + }, + + /** + * Обновляет ссылку приглашение на доску + */ + refreshBoardInvite() { + Dispatcher.dispatch({ + actionName: InviteActionTypes.REFRESH_BOARD_LINK, + }); + }, + + /** + * Обновляет ссылку приглашение на карточку + */ + refreshCardInvite() { + Dispatcher.dispatch({ + actionName: InviteActionTypes.REFRESH_CARD_LINK, + }); + }, + + /** + * Скопировать приглашение на доску + */ + copyBoardInvite() { + Dispatcher.dispatch({ + actionName: InviteActionTypes.COPY_BOARD_LINK, + }); + }, + + /** + * Скопировать приглашение на карточку + */ + copyCardInvite() { + Dispatcher.dispatch({ + actionName: InviteActionTypes.COPY_CARD_LINK, + }); + }, +}; diff --git a/src/constants/constants.js b/src/constants/constants.js index 4875deb8..f919f257 100644 --- a/src/constants/constants.js +++ b/src/constants/constants.js @@ -12,6 +12,12 @@ export const Urls = { Card: '/card/', Profile: '/profile', NotFound: '/404', + Invite: { + BoardPath: '/invite/board/', + Board: '/invite/board/', + CardPath: '/invite/card/', + Card: '/invite/card/', + }, }; /** @@ -91,6 +97,7 @@ export const ConstantMessages = { CardTitleTooLong: 'Название карточки слишком длинное', CardErrorOnServer: 'Не удалось создать карточку, попробуйте позднее', UnsuccessfulRequest: 'Неудачный запрос, попробуйте позднее :]', + CantCopyToClipBoard: 'Не удалось скопировать текст', WrongTagNameLength: 'Введите имя тега длиной от 1 до 40 символов', }; diff --git a/src/index.js b/src/index.js index c75f7e6c..a74a61c0 100644 --- a/src/index.js +++ b/src/index.js @@ -42,12 +42,15 @@ window.addEventListener('DOMContentLoaded', async () => { const root = document.getElementById(Html.Root); document.getElementById('no-connection').innerHTML = ''; - const boardView = new BoardsView(root); - Router.register(Urls.Root, boardView); - Router.register(Urls.Boards, boardView); + const boardsView = new BoardsView(root); + const boardView = new BoardView(root); + Router.register(Urls.Root, boardsView); + Router.register(Urls.Boards, boardsView); + Router.register(Urls.Invite.Board, boardView); + Router.register(Urls.Invite.Card, boardView); Router.register(Urls.Register, new RegisterView(root)); Router.register(Urls.Login, new LoginView(root)); - Router.register(Urls.Board, new BoardView(root)); + Router.register(Urls.Board, boardView); Router.register(Urls.Profile, new ProfileView(root)); UserStore.addListener(() => { diff --git a/src/modules/Network/Network.js b/src/modules/Network/Network.js index 8507d4ac..50eb87c1 100644 --- a/src/modules/Network/Network.js +++ b/src/modules/Network/Network.js @@ -653,6 +653,62 @@ class Network { `https://${this.BackendUrl}/${this._endpoints.tags}/${tgid}`, options); } + + /** + * Метод, реализующий запрос PUT /api/boards/access/:accessPath. + * @param {String} accessPath ключ доступа + * @return {Promise} промис запроса + */ + async useBoardInvite(accessPath) { + const options = { + method: 'put', + }; + return this.httpRequest( + `http://${this.BackendUrl}:${this.BackendPort}/${this._endpoints.board}/access/` + + `${accessPath}`, options); + } + + /** + * Метод, реализующий запрос PUT /api/boards/:bid/access. + * @param {Number} bid id доски + * @return {Promise} промис запроса + */ + async refreshBoardInvite(bid) { + const options = { + method: 'put', + }; + return this.httpRequest( + `http://${this.BackendUrl}:${this.BackendPort}/${this._endpoints.board}/${bid}/access`, + options); + } + + /** + * Метод, реализующий запрос PUT /api/card/access/tocard/:accessPath + * @param {String} accessPath ключ доступа + * @return {Promise} промис запроса + */ + async useCardInvite(accessPath) { + const options = { + method: 'put', + }; + return this.httpRequest( + `http://${this.BackendUrl}:${this.BackendPort}/${this._endpoints.card}/access/tocard/` + + `${accessPath}`, options); + } + + /** + * Метод, реализующий запрос PUT /api/boards/:bid/access. + * @param {Number} cid id карточки + * @return {Promise} промис запроса + */ + async refreshCardInvite(cid) { + const options = { + method: 'put', + }; + return this.httpRequest( + `http://${this.BackendUrl}:${this.BackendPort}/${this._endpoints.card}/access/${cid}`, + options); + } } export default new Network(); diff --git a/src/modules/Router/Router.js b/src/modules/Router/Router.js index 5cef03dd..5932f857 100644 --- a/src/modules/Router/Router.js +++ b/src/modules/Router/Router.js @@ -70,6 +70,7 @@ class Router { * (по умолчанию: false) */ go(url, replaceState = false) { + console.log(url); const {urlData, view} = this.processURL(url) || {}; if (!urlData || !view) { this.go(Urls.NotFound, true); diff --git a/src/popups/AddUser/AddUserPopUp.hbs b/src/popups/AddUser/AddUserPopUp.hbs index 599b40eb..2ef0e12f 100644 --- a/src/popups/AddUser/AddUserPopUp.hbs +++ b/src/popups/AddUser/AddUserPopUp.hbs @@ -25,6 +25,17 @@ {{/each}} + {{#if inviteLink}} + + {{/if}}
diff --git a/src/popups/AddUser/AddUserPopUp.js b/src/popups/AddUser/AddUserPopUp.js index 621f9180..2b03bb20 100644 --- a/src/popups/AddUser/AddUserPopUp.js +++ b/src/popups/AddUser/AddUserPopUp.js @@ -35,6 +35,9 @@ export default class AddUserPopUp extends BaseComponent { input: document.getElementById('addUserPopUpSearchInputId'), users: document.querySelectorAll('.search-result'), closeBtn: document.getElementById('addUserPopUpCloseId'), + inviteInput: document.getElementById('inviteLinkInputId'), + inviteRefreshBtn: document.getElementById('refreshLinkId'), + inviteCopyBtn: document.getElementById('copyLinkId'), }; } @@ -52,6 +55,8 @@ export default class AddUserPopUp extends BaseComponent { this._elements.users?.forEach((user)=>{ user.addEventListener('click', this._callbacks.onUserClick); }); + this._elements.inviteRefreshBtn?.addEventListener('click', this._callbacks.onRefreshInvite); + this._elements.inviteCopyBtn?.addEventListener('click', this._callbacks.onCopyInvite); }; /** @@ -66,6 +71,8 @@ export default class AddUserPopUp extends BaseComponent { this._elements.users?.forEach((user)=>{ user.removeEventListener('click', this._callbacks.onUserClick); }); + this._elements.inviteRefreshBtn?.removeEventListener('click', this._callbacks.onRefreshInvite); + this._elements.inviteCopyBtn?.removeEventListener('click', this._callbacks.onCopyInvite); } /** @@ -73,6 +80,11 @@ export default class AddUserPopUp extends BaseComponent { * @private */ _setUpSearchInput() { + if (this.context.selectInvite) { + this._elements.inviteInput?.focus(); + this._elements.inviteInput?.select(); + return; + } this._elements.input?.focus(); this._elements.input?.setSelectionRange(this._elements.input.value.length, this._elements.input.value.length); diff --git a/src/popups/AddUser/AddUserPopUp.scss b/src/popups/AddUser/AddUserPopUp.scss index c2e99d0f..65ec7ca9 100644 --- a/src/popups/AddUser/AddUserPopUp.scss +++ b/src/popups/AddUser/AddUserPopUp.scss @@ -54,3 +54,28 @@ padding: 0 10px; } } + +.invite-link-wrapper { + display: flex; + flex-direction: column; +} + +.invite-link { + display: flex; + flex-direction: row; + align-items: center; + column-gap: 5px; + + &__link { + flex-grow: 1; + } + + &__copy { + + } + + &__reload { + + } + +} diff --git a/src/stores/BoardStore/BoardStore.js b/src/stores/BoardStore/BoardStore.js index d8936277..8eb83d60 100644 --- a/src/stores/BoardStore/BoardStore.js +++ b/src/stores/BoardStore/BoardStore.js @@ -20,6 +20,7 @@ import { CheckLists, ConstantMessages, HttpStatusCodes, + SelfAddress, Urls, } from '../../constants/constants.js'; @@ -27,6 +28,7 @@ import { import UserStore from '../UserStore/UserStore.js'; import SettingsStore from '../SettingsStore/SettingsStore.js'; + /** * Класс, реализующий хранилище доски */ @@ -84,6 +86,8 @@ class BoardStore extends BaseStore { searchString: null, users: [], header: 'Добавить пользователя в доску', + inviteLink: null, + selectInvite: false, }); this._storage.set('add-card-member-popup', { @@ -92,6 +96,8 @@ class BoardStore extends BaseStore { searchString: null, users: [], header: 'Добавить пользователя в карточку', + inviteLink: null, + selectInvite: false, }); this._storage.set('tags-list-popup', { @@ -348,6 +354,36 @@ class BoardStore extends BaseStore { this._emitChange(); break; + /* Invite: */ + case InviteActionTypes.GO_BOARD_INVITE: + await this._openBoardInvite(action.data); + this._emitChange(); + break; + + case InviteActionTypes.GO_CARD_INVITE: + await this._openCardInvite(action.data); + this._emitChange(); + break; + + case InviteActionTypes.REFRESH_BOARD_LINK: + await this._refreshBoardInvite(); + this._emitChange(); + break; + + case InviteActionTypes.REFRESH_CARD_LINK: + await this._refreshCardInvite(); + this._emitChange(); + break; + + case InviteActionTypes.COPY_BOARD_LINK: + await this._copyBoardInvite(); + this._emitChange(); + break; + + case InviteActionTypes.COPY_CARD_LINK: + await this._copyCardInvite(); + this._emitChange(); + break; case TagsActionTypes.SHOW_LIST_POPUP_BOARD: this._showTagListPopUpBoard(); this._emitChange(); @@ -412,6 +448,26 @@ class BoardStore extends BaseStore { } } + /** + * Создает приглашение на карточку + * @param {String} accessPath - путь + * @private + */ + _setCardInvite(accessPath) { + this._storage.get('add-card-member-popup').inviteLink = + `http://${SelfAddress.Url}:${SelfAddress.Port}` + Urls.Invite.CardPath + accessPath; + } + + /** + * Создает приглашение на доску + * @param {String} accessPath - путь + * @private + */ + _setBoardInvite(accessPath) { + this._storage.get('add-board-member-popup').inviteLink = + `http://${SelfAddress.Url}:${SelfAddress.Port}` + Urls.Invite.BoardPath + accessPath; + } + /** * Ищет тег среди полученных вместе с доской тегов * @param {Number} tgid id тега @@ -486,6 +542,7 @@ class BoardStore extends BaseStore { }); this._storage.set('members', payload.data.members || []); // todo payload.data.members + this._setBoardInvite(payload.data.access_path); return; case HttpStatusCodes.Unauthorized: @@ -905,6 +962,7 @@ class BoardStore extends BaseStore { assignees: [], tags: [], check_lists: [], + access_path: payload.data.access_path, }); return; @@ -1462,6 +1520,7 @@ class BoardStore extends BaseStore { async _toggleCheckListItem(data) { const context = this._storage.get('card-popup'); context.errors = null; + context.selectInvite = false; let item = this._getCheckListItemById(data.chlid, data.chliid); const newItem = {...item}; @@ -1506,6 +1565,8 @@ class BoardStore extends BaseStore { context.users = card.assignees.map((assignee) => { return {...assignee, added: true}; }); + this._setCardInvite(card.access_path); + context.selectInvite = false; if (!context.users.length) { context.users = this._storage.get('members').slice(); @@ -1517,6 +1578,7 @@ class BoardStore extends BaseStore { * @private */ _hideAddCardAssigneePopUp() { + this._storage.get('add-card-member-popup').selectInvite = false; this._storage.get('add-card-member-popup').visible = false; } @@ -1527,6 +1589,7 @@ class BoardStore extends BaseStore { */ async _refreshCardAssigneeSearchList(data) { const context = this._storage.get('add-card-member-popup'); + context.selectInvite = false; context.errors = null; const {searchString} = data; context.searchString = searchString; @@ -1574,6 +1637,7 @@ class BoardStore extends BaseStore { async _toggleCardAssigneeInSearchList(data) { const context = this._storage.get('add-card-member-popup'); context.errors = null; + context.selectInvite = false; const card = this._getCardById(this._storage.get('card-popup').clid, this._storage.get('card-popup').cid); @@ -1625,6 +1689,7 @@ class BoardStore extends BaseStore { */ _showAddBoardMemberPopUp() { const context = this._storage.get('add-board-member-popup'); + context.selectInvite = false; context.visible = true; context.errors = null; context.searchString = null; @@ -1638,6 +1703,7 @@ class BoardStore extends BaseStore { * @private */ _hideAddBoardMemberPopUp() { + this._storage.get('add-board-member-popup').selectInvite = false; this._storage.get('add-board-member-popup').visible = false; } @@ -1666,6 +1732,7 @@ class BoardStore extends BaseStore { */ async _toggleBoardMemberInSearchList(data) { const context = this._storage.get('add-board-member-popup'); + context.selectInvite = false; context.errors = null; const members = this._storage.get('members').slice(); @@ -1722,6 +1789,7 @@ class BoardStore extends BaseStore { */ async _refreshBoardMemberSearchList(data) { const context = this._storage.get('add-board-member-popup'); + context.selectInvite = false; context.errors = null; const {searchString} = data; context.searchString = searchString; @@ -1916,6 +1984,146 @@ class BoardStore extends BaseStore { this._storage.get('card-popup').scroll = data.scrollValue; } + /** + * Приглашает пользователя в доску + * @param {Object} data инвайт + * @return {Promise} + */ + async _openBoardInvite(data) { + let payload; + + try { + payload = await Network.useBoardInvite(data.accessPath); + } catch (error) { + console.log('Unable to connect to backend, reason: ', error); + return; + } + + switch (payload.status) { + case HttpStatusCodes.Ok: + Router.go(`/board/${payload.data.bid}`, true); + return; + + default: + Router.go(Urls.Login, true); + return; + } + } + + /** + * Приглашает пользователя в карточку + * @param {Object} data инвайт + * @return {Promise} + */ + async _openCardInvite(data) { + let payload; + + try { + payload = await Network.useCardInvite(data.accessPath); + } catch (error) { + console.log('Unable to connect to backend, reason: ', error); + return; + } + + switch (payload.status) { + case HttpStatusCodes.Ok: + Router.go(`/board/${this._storage.get('bid')}`, true); + return; + + default: + Router.go(Urls.Login, true); + return; + } + } + + /** + * Обновляет приглашение на доску + * @return {Promise} + */ + async _refreshBoardInvite() { + const context = this._storage.get('add-board-member-popup'); + context.errors = null; + + let payload; + + try { + payload = await Network.refreshBoardInvite(this._storage.get('bid')); + } catch (error) { + console.log('Unable to connect to backend, reason: ', error); + return; + } + + switch (payload.status) { + case HttpStatusCodes.Ok: + this._setBoardInvite(payload.data.access_path); + return; + + default: + context.errors = ConstantMessages.UnsuccessfulRequest; + return; + } + } + + /** + * Обновляет приглашение на карточку + * @return {Promise} + */ + async _refreshCardInvite() { + const context = this._storage.get('add-card-member-popup'); + context.errors = null; + + let payload; + + try { + payload = await Network.refreshCardInvite(this._storage.get('card-popup').cid); + } catch (error) { + console.log('Unable to connect to backend, reason: ', error); + return; + } + + switch (payload.status) { + case HttpStatusCodes.Ok: + this._setCardInvite(payload.data.access_path); + return; + + default: + context.errors = ConstantMessages.UnsuccessfulRequest; + return; + } + } + + /** + * Скопировать приглашение на доску + */ + async _copyBoardInvite() { + const context = this._storage.get('add-board-member-popup'); + context.errors = null; + + try { + await navigator.clipboard.writeText(context.inviteLink); + } catch (error) { + context.errors = ConstantMessages.CantCopyToClipBoard; + } + + context.selectInvite = true; + } + + /** + * Скопировать приглашение на карточку + */ + async _copyCardInvite() { + const context = this._storage.get('add-card-member-popup'); + context.errors = null; + + try { + await navigator.clipboard.writeText(context.inviteLink); + } catch (error) { + context.errors = ConstantMessages.CantCopyToClipBoard; + } + + context.selectInvite = true; + } + /** * Отображает окно со списком тегов, при нажатии на кнопку тегов на доске * @private diff --git a/src/styles/scss/Common.scss b/src/styles/scss/Common.scss index 37836646..fe28916f 100644 --- a/src/styles/scss/Common.scss +++ b/src/styles/scss/Common.scss @@ -219,6 +219,22 @@ div#root { } } + &-copy { + @extend .material-icon; + + &::before { + content: 'content_copy'; + } + } + + &-reload { + @extend .material-icon; + + &::before { + content: 'autorenew'; + } + } + &-check { @extend .material-icon; @@ -228,12 +244,12 @@ div#root { } - &-remove { - @extend .material-icon; - &::before { - content: 'cancel'; + &-remove { + @extend .material-icon; + &::before { + content: 'cancel'; + } } - } } .horizontal-line { diff --git a/src/views/BoardView/BoardView.js b/src/views/BoardView/BoardView.js index 005338bf..b112de93 100644 --- a/src/views/BoardView/BoardView.js +++ b/src/views/BoardView/BoardView.js @@ -36,6 +36,7 @@ import './BoardView.scss'; // Шаблон import template from './BoardView.hbs'; +import {inviteActions} from '../../actions/invite'; import {tagsActions} from '../../actions/tags'; /** @@ -82,8 +83,19 @@ export default class BoardView extends BaseView { * @param {Object} urlData параметры адресной строки */ _onShow(urlData) { - this.urlData = urlData; - boardsActions.getBoard(urlData.pathParams.id); + if (!UserStore.getContext('isAuthorized')) { + Router.go(Urls.Login, true); + return; + } + + if ('accessPathBoard' in urlData.pathParams) { + inviteActions.openBoardInvite(urlData.pathParams.accessPathBoard); + } else if ('accessPathCard' in urlData.pathParams) { + inviteActions.openCardInvite(urlData.pathParams.accessPathCard); + } else { + boardsActions.getBoard(urlData.pathParams.id); + } + this.render(); } @@ -180,11 +192,15 @@ export default class BoardView extends BaseView { onInput: this._onAddCardMemberInput.bind(this), onUserClick: this._onAddCardMemberUserClick.bind(this), onClose: this._onAddCardMemberClose.bind(this), + onRefreshInvite: this._onRefreshCardInvite.bind(this), + onCopyInvite: this._onCopyCardInvite.bind(this), }, board: { onInput: this._onAddBoardMemberInput.bind(this), onUserClick: this._onAddBoardMemberUserClick.bind(this), onClose: this._onAddBoardMemberClose.bind(this), + onRefreshInvite: this._onRefreshBoardInvite.bind(this), + onCopyInvite: this._onCopyBoardInvite.bind(this), }, }; this._onAddBoardMemberShow = this._onAddBoardMemberShow.bind(this); @@ -404,6 +420,46 @@ export default class BoardView extends BaseView { } } + /** + * Callback, вызываемый при обновлении ссылки приглашение на доску + * @param {Event} event объект события + * @private + */ + _onRefreshBoardInvite(event) { + event.preventDefault(); + inviteActions.refreshBoardInvite(); + } + + /** + * Callback, вызываемый при обновлении ссылки приглашение на карточку + * @param {Event} event объект события + * @private + */ + _onRefreshCardInvite(event) { + event.preventDefault(); + inviteActions.refreshCardInvite(); + } + + /** + * Callback, вызываемый при копировании приглашения на доску + * @param {Event} event объект события + * @private + */ + _onCopyBoardInvite(event) { + event.preventDefault(); + inviteActions.copyBoardInvite(); + } + + /** + * Callback, вызываемый при копировании приглашения на карточку + * @param {Event} event объект события + * @private + */ + _onCopyCardInvite(event) { + event.preventDefault(); + inviteActions.copyCardInvite(); + } + /** * CallBack на отображение тегов * @param {Event} event - объект события diff --git a/src/views/BoardsView/BoardsView.js b/src/views/BoardsView/BoardsView.js index 6a4915e8..c8ae6608 100644 --- a/src/views/BoardsView/BoardsView.js +++ b/src/views/BoardsView/BoardsView.js @@ -73,6 +73,7 @@ export default class BoardsView extends BaseView { /** * Метод, вызывающийся по умолчанию при открытии страницы. + * @private */ _onShow() { this._setContext(new Map([