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/css/icons.scss b/css/icons.scss index e8ca7e7f9..ef9755890 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('deck', 'deck', 1); + @include icon-black-white('archive', '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 diff --git a/lib/Controller/BoardController.php b/lib/Controller/BoardController.php index 6cb65ff01..5a02477ab 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\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 1e03b7445..e87576a92 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() . ' (' . $this->l10n->t('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..73c24740e 100644 --- a/src/components/navigation/AppNavigationBoard.vue +++ b/src/components/navigation/AppNavigationBoard.vue @@ -130,6 +130,25 @@ export default { text: t('deck', 'Edit board') }) + actions.push({ + 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 (e) { + OC.Notification.showTemporary(t('deck', 'An error occurred')) + console.error(e) + } + }, + 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..336b638ad 100644 --- a/src/services/BoardApi.js +++ b/src/services/BoardApi.js @@ -135,6 +135,15 @@ export class BoardApi { }) } + 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 deleteLabel(id) { return axios.delete(this.url(`/labels/${id}`)) diff --git a/src/store/main.js b/src/store/main.js index c1e049f3c..92d50ff5a 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. * @@ -268,6 +281,15 @@ export default new Vuex.Store({ commit('addBoard', board) }) }, + 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) },