From 36afac9effb0f2627ad216ce2ede6bdc72215c04 Mon Sep 17 00:00:00 2001 From: Jakob Date: Fri, 6 Sep 2019 09:32:52 +0200 Subject: [PATCH 1/5] clone board func with labels and stacks Signed-off-by: Jakob --- appinfo/routes.php | 1 + lib/Controller/BoardController.php | 9 ++++ lib/Service/BoardService.php | 44 +++++++++++++++++++ .../navigation/AppNavigationBoard.vue | 10 +++++ src/services/BoardApi.js | 15 +++++++ src/store/main.js | 19 ++++++++ 6 files changed, 98 insertions(+) diff --git a/appinfo/routes.php b/appinfo/routes.php index 7d6816b7a..f08c28ff7 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -40,6 +40,7 @@ return [ ['name' => 'board#addAcl', 'url' => '/boards/{boardId}/acl', 'verb' => 'POST'], ['name' => 'board#updateAcl', 'url' => '/boards/{boardId}/acl', 'verb' => 'PUT'], ['name' => 'board#deleteAcl', 'url' => '/boards/{boardId}/acl/{aclId}', 'verb' => 'DELETE'], + ['name' => 'board#clone', 'url' => '/boards/{boardId}/clone', 'verb' => 'POST'], // stacks ['name' => 'stack#index', 'url' => '/stacks/{boardId}', 'verb' => 'GET'], diff --git a/lib/Controller/BoardController.php b/lib/Controller/BoardController.php index 6cb65ff01..b397c63d8 100644 --- a/lib/Controller/BoardController.php +++ b/lib/Controller/BoardController.php @@ -150,4 +150,13 @@ class BoardController extends ApiController { return $this->boardService->deleteAcl($aclId); } + /** + * @NoAdminRequired + * @param $boardId + * @return \OCP\AppFramework\Db\Entity + */ + public function clone($boardId) { + return $this->boardService->clone($boardId); + } + } diff --git a/lib/Service/BoardService.php b/lib/Service/BoardService.php index 1e03b7445..8f5cc58af 100644 --- a/lib/Service/BoardService.php +++ b/lib/Service/BoardService.php @@ -33,6 +33,7 @@ use OCA\Deck\Db\AssignedUsersMapper; use OCA\Deck\Db\ChangeHelper; use OCA\Deck\Db\IPermissionMapper; use OCA\Deck\Db\Label; +use OCA\Deck\Db\Stack; use OCA\Deck\Db\StackMapper; use OCA\Deck\NoPermissionException; use OCA\Deck\Notification\NotificationHelper; @@ -605,6 +606,49 @@ class BoardService { return $delete; } + /** + * @param $id + * @return Board + * @throws DoesNotExistException + * @throws \OCA\Deck\NoPermissionException + * @throws \OCP\AppFramework\Db\MultipleObjectsReturnedException + * @throws BadRequestException + */ + public function clone($id) { + + if (is_numeric($id) === false) { + throw new BadRequestException('board id must be a number'); + } + + $this->permissionService->checkPermission($this->boardMapper, $id, Acl::PERMISSION_READ); + + $board = $this->boardMapper->find($id); + $newBoard = new Board(); + $newBoard->setTitle($board->getTitle() . ' (copy)'); + $newBoard->setOwner($board->getOwner()); + $newBoard->setColor($board->getColor()); + $this->boardMapper->insert($newBoard); + + $labels = $this->labelMapper->findAll($id); + foreach ($labels as $label) { + $newLabel = new Label(); + $newLabel->setTitle($label->getTitle()); + $newLabel->setColor($label->getColor()); + $newLabel->setBoardId($newBoard->getId()); + $this->labelMapper->insert($newLabel); + } + + $stacks = $this->stackMapper->findAll($id); + foreach ($stacks as $stack) { + $newStack = new Stack(); + $newStack->setTitle($stack->getTitle()); + $newStack->setBoardId($newBoard->getId()); + $this->stackMapper->insert($newStack); + } + + return $newBoard; + } + private function enrichWithStacks($board, $since = -1) { $stacks = $this->stackMapper->findAll($board->getId(), null, null, $since); diff --git a/src/components/navigation/AppNavigationBoard.vue b/src/components/navigation/AppNavigationBoard.vue index ae4eec965..d7d07f0ec 100644 --- a/src/components/navigation/AppNavigationBoard.vue +++ b/src/components/navigation/AppNavigationBoard.vue @@ -130,6 +130,16 @@ export default { text: t('deck', 'Edit board') }) + actions.push({ + action: () => { + this.hideMenu() + // this.boardApi.cloneBoard(this.board) + this.$store.dispatch('cloneBoard', this.board) + }, + icon: 'icon-clone', + text: t('deck', 'Clone board') + }) + if (!this.board.archived) { actions.push({ action: () => { diff --git a/src/services/BoardApi.js b/src/services/BoardApi.js index 66857269f..ff780ce5d 100644 --- a/src/services/BoardApi.js +++ b/src/services/BoardApi.js @@ -135,6 +135,21 @@ export class BoardApi { }) } + cloneBoard(board) { + return axios.post(this.url(`/boards/${board.id}/clone`)) + .then( + (response) => { + return Promise.resolve(response.data) + }, + (err) => { + return Promise.reject(err) + } + ) + .catch((err) => { + return Promise.reject(err) + }) + } + // Label API Calls deleteLabel(id) { return axios.delete(this.url(`/labels/${id}`)) diff --git a/src/store/main.js b/src/store/main.js index 93aaba30b..4ae00740b 100644 --- a/src/store/main.js +++ b/src/store/main.js @@ -115,6 +115,19 @@ export default new Vuex.Store({ state.boards.push(board) } }, + + cloneBoard(state, board) { + const indexExisting = state.boards.findIndex((b) => { + return board.id === b.id + }) + + if (indexExisting > -1) { + Vue.set(state.boards, indexExisting, board) + } else { + state.boards.push(board) + } + }, + /** * Removes the board from the store. * @@ -267,6 +280,12 @@ export default new Vuex.Store({ commit('addBoard', board) }) }, + cloneBoard({ commit }, boardData) { + apiClient.cloneBoard(boardData) + .then((board) => { + commit('cloneBoard', board) + }) + }, removeBoard({ commit }, board) { commit('removeBoard', board) }, From df0a8515a34d67c55a29cff1e89bcba5a0c5fae8 Mon Sep 17 00:00:00 2001 From: Jakob Date: Tue, 10 Sep 2019 12:51:59 +0200 Subject: [PATCH 2/5] added icon Signed-off-by: Jakob --- css/icons.scss | 4 +++- img/clone.svg | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 img/clone.svg diff --git a/css/icons.scss b/css/icons.scss index e8ca7e7f9..f2b90dbf7 100644 --- a/css/icons.scss +++ b/css/icons.scss @@ -53,10 +53,12 @@ background-image: url('../img/toggle-view-collapse.svg'); } + @if mixin-exists('icon-black-white') { @include icon-black-white('deck', 'deck', 1); @include icon-black-white('archive', 'deck', 1); - @include icon-black-white('circles', 'deck', 1); + @include icon-black-white('circles', 'deck', 1); + @include icon-black-white('clone', 'deck', 1); .icon-toggle-compact-collapsed { @include icon-color('toggle-view-expand', 'deck', $color-black); diff --git a/img/clone.svg b/img/clone.svg new file mode 100644 index 000000000..469fd1beb --- /dev/null +++ b/img/clone.svg @@ -0,0 +1 @@ + \ No newline at end of file From 31ae00708d0638daae5ee4296506448f5b502ac6 Mon Sep 17 00:00:00 2001 From: Jakob Date: Tue, 17 Sep 2019 12:37:43 +0200 Subject: [PATCH 3/5] loading indicator Signed-off-by: Jakob --- src/components/navigation/AppNavigationBoard.vue | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/components/navigation/AppNavigationBoard.vue b/src/components/navigation/AppNavigationBoard.vue index d7d07f0ec..474d7f81a 100644 --- a/src/components/navigation/AppNavigationBoard.vue +++ b/src/components/navigation/AppNavigationBoard.vue @@ -133,8 +133,14 @@ export default { actions.push({ action: () => { this.hideMenu() - // this.boardApi.cloneBoard(this.board) - this.$store.dispatch('cloneBoard', this.board) + this.loading = true + this.$store.dispatch('cloneBoard', this.board).then((newBoard) => { + this.loading = false + this.editTitle = this.board.title + this.editColor = '#' + this.board.color + this.editing = true + + }) }, icon: 'icon-clone', text: t('deck', 'Clone board') From 71e7c98fd6411f71a2cacf15525e965d4ced1712 Mon Sep 17 00:00:00 2001 From: Jakob Date: Wed, 9 Oct 2019 08:42:28 +0200 Subject: [PATCH 4/5] change route after board duplication Signed-off-by: Jakob --- css/icons.scss | 4 ++-- lib/Controller/BoardController.php | 2 +- lib/Service/BoardService.php | 2 +- .../navigation/AppNavigationBoard.vue | 18 +++++++++++------- src/store/main.js | 14 ++++++++++---- 5 files changed, 25 insertions(+), 15 deletions(-) diff --git a/css/icons.scss b/css/icons.scss index f2b90dbf7..ef9755890 100644 --- a/css/icons.scss +++ b/css/icons.scss @@ -55,8 +55,8 @@ @if mixin-exists('icon-black-white') { - @include icon-black-white('deck', 'deck', 1); - @include icon-black-white('archive', 'deck', 1); + @include icon-black-white('deck', 'deck', 1); + @include icon-black-white('archive', 'deck', 1); @include icon-black-white('circles', 'deck', 1); @include icon-black-white('clone', 'deck', 1); diff --git a/lib/Controller/BoardController.php b/lib/Controller/BoardController.php index b397c63d8..5a02477ab 100644 --- a/lib/Controller/BoardController.php +++ b/lib/Controller/BoardController.php @@ -153,7 +153,7 @@ class BoardController extends ApiController { /** * @NoAdminRequired * @param $boardId - * @return \OCP\AppFramework\Db\Entity + * @return \OCP\Deck\DB\Board */ public function clone($boardId) { return $this->boardService->clone($boardId); diff --git a/lib/Service/BoardService.php b/lib/Service/BoardService.php index 8f5cc58af..e87576a92 100644 --- a/lib/Service/BoardService.php +++ b/lib/Service/BoardService.php @@ -624,7 +624,7 @@ class BoardService { $board = $this->boardMapper->find($id); $newBoard = new Board(); - $newBoard->setTitle($board->getTitle() . ' (copy)'); + $newBoard->setTitle($board->getTitle() . ' (' . $this->l10n->t('copy') . ')'); $newBoard->setOwner($board->getOwner()); $newBoard->setColor($board->getColor()); $this->boardMapper->insert($newBoard); diff --git a/src/components/navigation/AppNavigationBoard.vue b/src/components/navigation/AppNavigationBoard.vue index 474d7f81a..7c27e7cb4 100644 --- a/src/components/navigation/AppNavigationBoard.vue +++ b/src/components/navigation/AppNavigationBoard.vue @@ -131,16 +131,20 @@ export default { }) actions.push({ - action: () => { + action: async () => { this.hideMenu() this.loading = true - this.$store.dispatch('cloneBoard', this.board).then((newBoard) => { + try { + const newBoard = await this.$store.dispatch('cloneBoard', this.board) + this.loading = false - this.editTitle = this.board.title - this.editColor = '#' + this.board.color - this.editing = true - - }) + const route = this.routeTo + route.params.id = newBoard.id + this.$router.push(route) + } + catch { + OC.Notification.showTemporary(t('deck', 'An error occurred')) + } }, icon: 'icon-clone', text: t('deck', 'Clone board') diff --git a/src/store/main.js b/src/store/main.js index 4ae00740b..fb67ff39c 100644 --- a/src/store/main.js +++ b/src/store/main.js @@ -281,10 +281,16 @@ export default new Vuex.Store({ }) }, cloneBoard({ commit }, boardData) { - apiClient.cloneBoard(boardData) - .then((board) => { - commit('cloneBoard', board) - }) + return new Promise((resolve, reject) => { + apiClient.cloneBoard(boardData) + .then((board) => { + commit('cloneBoard', board) + resolve(board) + }) + .catch((err) => { + return reject(err) + }) + }) }, removeBoard({ commit }, board) { commit('removeBoard', board) From 1c2c700593b5bf1d8b9c1ba49431587266fa9398 Mon Sep 17 00:00:00 2001 From: Jakob Date: Wed, 9 Oct 2019 12:45:33 +0200 Subject: [PATCH 5/5] use try/catch Signed-off-by: Jakob --- .../navigation/AppNavigationBoard.vue | 7 +++---- src/services/BoardApi.js | 20 +++++++------------ src/store/main.js | 19 ++++++++---------- 3 files changed, 18 insertions(+), 28 deletions(-) diff --git a/src/components/navigation/AppNavigationBoard.vue b/src/components/navigation/AppNavigationBoard.vue index 7c27e7cb4..73c24740e 100644 --- a/src/components/navigation/AppNavigationBoard.vue +++ b/src/components/navigation/AppNavigationBoard.vue @@ -131,19 +131,18 @@ export default { }) actions.push({ - action: async () => { + action: async() => { this.hideMenu() this.loading = true try { const newBoard = await this.$store.dispatch('cloneBoard', this.board) - this.loading = false const route = this.routeTo route.params.id = newBoard.id this.$router.push(route) - } - catch { + } catch (e) { OC.Notification.showTemporary(t('deck', 'An error occurred')) + console.error(e) } }, icon: 'icon-clone', diff --git a/src/services/BoardApi.js b/src/services/BoardApi.js index ff780ce5d..336b638ad 100644 --- a/src/services/BoardApi.js +++ b/src/services/BoardApi.js @@ -135,19 +135,13 @@ export class BoardApi { }) } - cloneBoard(board) { - return axios.post(this.url(`/boards/${board.id}/clone`)) - .then( - (response) => { - return Promise.resolve(response.data) - }, - (err) => { - return Promise.reject(err) - } - ) - .catch((err) => { - return Promise.reject(err) - }) + async cloneBoard(board) { + try { + let response = await axios.post(this.url(`/boards/${board.id}/clone`)) + return response.data + } catch (err) { + return err + } } // Label API Calls diff --git a/src/store/main.js b/src/store/main.js index fb67ff39c..82b22bd3e 100644 --- a/src/store/main.js +++ b/src/store/main.js @@ -280,17 +280,14 @@ export default new Vuex.Store({ commit('addBoard', board) }) }, - cloneBoard({ commit }, boardData) { - return new Promise((resolve, reject) => { - apiClient.cloneBoard(boardData) - .then((board) => { - commit('cloneBoard', board) - resolve(board) - }) - .catch((err) => { - return reject(err) - }) - }) + async cloneBoard({ commit }, boardData) { + try { + let newBoard = await apiClient.cloneBoard(boardData) + commit('cloneBoard', newBoard) + return newBoard + } catch (err) { + return err + } }, removeBoard({ commit }, board) { commit('removeBoard', board)