diff --git a/appinfo/database.xml b/appinfo/database.xml index 3eb4495f9..91c9076ba 100644 --- a/appinfo/database.xml +++ b/appinfo/database.xml @@ -77,6 +77,14 @@ 8 false + + deleted_at + integer + 0 + 8 + false + true + deck_stacks_board_id_index @@ -167,6 +175,14 @@ boolean false + + deleted_at + integer + 0 + 8 + false + true + deck_cards_stack_id_index diff --git a/appinfo/info.xml b/appinfo/info.xml index f2ff32e48..1760084d6 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -14,7 +14,7 @@ - 🚀 Get your project organized - 0.5.0-dev1 + 0.5.0-dev2 agpl Julius Härtl Deck diff --git a/appinfo/routes.php b/appinfo/routes.php index cdd2b9d79..90dffb520 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -5,20 +5,20 @@ * @author Julius Härtl * * @license GNU AGPL version 3 or any later version - * + * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. - * + * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. - * + * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . - * + * */ return [ @@ -43,6 +43,7 @@ return [ ['name' => 'stack#update', 'url' => '/stacks/{stackId}', 'verb' => 'PUT'], ['name' => 'stack#reorder', 'url' => '/stacks/{stackId}/reorder', 'verb' => 'PUT'], ['name' => 'stack#delete', 'url' => '/stacks/{stackId}', 'verb' => 'DELETE'], + ['name' => 'stack#deleted', 'url' => '/{boardId}/stacks/deleted', 'verb' => 'GET'], ['name' => 'stack#archived', 'url' => '/stacks/{boardId}/archived', 'verb' => 'GET'], // cards @@ -50,6 +51,7 @@ return [ ['name' => 'card#create', 'url' => '/cards', 'verb' => 'POST'], ['name' => 'card#update', 'url' => '/cards/{cardId}', 'verb' => 'PUT'], ['name' => 'card#delete', 'url' => '/cards/{cardId}', 'verb' => 'DELETE'], + ['name' => 'card#deleted', 'url' => '/{boardId}/cards/deleted', 'verb' => 'GET'], ['name' => 'card#rename', 'url' => '/cards/{cardId}/rename', 'verb' => 'PUT'], ['name' => 'card#reorder', 'url' => '/cards/{cardId}/reorder', 'verb' => 'PUT'], ['name' => 'card#archive', 'url' => '/cards/{cardId}/archive', 'verb' => 'PUT'], diff --git a/css/style.scss b/css/style.scss index a94b60938..12504fe91 100644 --- a/css/style.scss +++ b/css/style.scss @@ -1170,6 +1170,16 @@ input.input-inline { position: relative; } +.board-detail__deleted-list__item { + display: flex; + flex-direction: row; + justify-content: space-between; + + * { + flex-basis: 20%; + } +} + #board-detail-labels { ul li { input { @@ -1215,12 +1225,18 @@ input.input-inline { .tabHeaders { clear: both; - overflow: hidden; + overflow: initial; margin-bottom: 0; } .tabsContainer { margin-top: 15px; + height: 100%; + + .tab { + height: 100%; + overflow: scroll; + } } .ui-select-offscreen { diff --git a/js/controller/BoardController.js b/js/controller/BoardController.js index d8b4c16b7..aa82d15f3 100644 --- a/js/controller/BoardController.js +++ b/js/controller/BoardController.js @@ -4,20 +4,20 @@ * @author Julius Härtl * * @license GNU AGPL version 3 or any later version - * + * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. - * + * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. - * + * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . - * + * */ import app from '../app/App.js'; @@ -42,6 +42,15 @@ app.controller('BoardController', function ($rootScope, $scope, $stateParams, St $scope.board = BoardService.getCurrent(); $scope.uploader = FileService.uploader; + $scope.$watch(function() { + return $state.current; + }, function(currentState) { + if(currentState.name === 'board.detail') { + CardService.fetchDeleted($scope.id); + StackService.fetchDeleted($scope.id); + } + }); + // workaround for $stateParams changes not being propagated $scope.$watch(function() { return $state.params; @@ -186,20 +195,58 @@ app.controller('BoardController', function ($rootScope, $scope, $stateParams, St }); }; + $scope.stackDelete = function (stack) { + $scope.stackservice.delete(stack.id); + }; + + $scope.stackUndoDelete = function (deletedStack) { + return StackService.undoDelete(deletedStack); + }; + $scope.cardDelete = function (card) { - OC.dialogs.confirm(t('deck', 'Are you sure you want to delete this card with all of its data?'), t('deck', 'Delete'), function(state) { - if (!state) { - return; - } - CardService.delete(card.id).then(function () { - StackService.removeCard(card); - }); + CardService.delete(card.id).then(function () { + StackService.removeCard(card); }); }; + + $scope.cardOrCardAndStackUndoDelete = function (deletedCard) { + var associatedDeletedStack = $scope.stackservice.deleted[deletedCard.stackId]; + if(associatedDeletedStack !== undefined) { + $scope.cardAndStackUndoDeleteAskForConfirmation(deletedCard, associatedDeletedStack); + } else { + $scope.cardUndoDelete(deletedCard); + } + }; + + $scope.cardAndStackUndoDeleteAskForConfirmation = function(deletedCard, associatedDeletedStack) { + OC.dialogs.confirm( + t('deck', 'The associated stack is deleted as well, it will be restored as well.'), + t('deck', 'Restore associated stack'), + function(state) { + if (state) { + $scope.cardAndStackUndoDelete(deletedCard, associatedDeletedStack); + } + } + ); + }; + + $scope.cardAndStackUndoDelete = function(deletedCard, associatedDeletedStack) { + $scope.stackUndoDelete(associatedDeletedStack).then(function() { + $scope.cardUndoDelete(deletedCard); + }); + }; + + $scope.cardUndoDelete = function(deletedCard) { + CardService.undoDelete(deletedCard).then(function() { + StackService.addCard(deletedCard); + }); + }; + $scope.cardArchive = function (card) { CardService.archive(card); StackService.removeCard(card); }; + $scope.isCurrentUserAssigned = function (card) { if (! CardService.get(card.id).assignedUsers) { return false; @@ -209,6 +256,7 @@ app.controller('BoardController', function ($rootScope, $scope, $stateParams, St }); return userList.length === 1; }; + $scope.cardAssignToMe = function (card) { CardService.assignUser(card, OC.getCurrentUser().uid) .then( @@ -217,6 +265,7 @@ app.controller('BoardController', function ($rootScope, $scope, $stateParams, St // TODO: remove this jquery call. Fix and use appPopoverMenuUtils instead $('.popovermenu').addClass('hidden'); }; + $scope.cardUnassignFromMe = function (card) { CardService.unassignUser(card, OC.getCurrentUser().uid); StackService.updateCard(card); @@ -235,6 +284,7 @@ app.controller('BoardController', function ($rootScope, $scope, $stateParams, St BoardService.getCurrent().labels.splice(i, 1); // TODO: remove from cards }; + $scope.labelCreate = function (label) { label.boardId = $scope.id; LabelService.create(label).then(function (data) { @@ -254,12 +304,14 @@ app.controller('BoardController', function ($rootScope, $scope, $stateParams, St BoardService.addAcl(sharee); $scope.status.addSharee = null; }; + $scope.aclDelete = function (acl) { BoardService.deleteAcl(acl).then(function(data) { $scope.loadDefault(); $scope.refreshData(); }); }; + $scope.aclUpdate = function (acl) { BoardService.updateAcl(acl); }; @@ -383,5 +435,4 @@ app.controller('BoardController', function ($rootScope, $scope, $stateParams, St } return card.attachmentCount; }; - }); diff --git a/js/service/ApiService.js b/js/service/ApiService.js index 051479417..7cf2c2a37 100644 --- a/js/service/ApiService.js +++ b/js/service/ApiService.js @@ -4,35 +4,48 @@ * @author Julius Härtl * * @license GNU AGPL version 3 or any later version - * + * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. - * + * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. - * + * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . - * + * */ import app from '../app/App.js'; - /** global: oc_defaults */ app.factory('ApiService', function ($http, $q) { var ApiService = function (http, endpoint) { + // Consider renaming endpoint to resource this.endpoint = endpoint; - this.baseUrl = OC.generateUrl('/apps/deck/' + endpoint); + this.baseUrl = this.generateUrl(this.endpoint); this.http = http; this.q = $q; this.data = {}; + this.deleted = {}; this.id = null; this.sorted = []; }; + ApiService.prototype.generateUrl = function(path) { + return OC.generateUrl('/apps/deck/' + path); + }; + + ApiService.prototype.tryAllThenDeleted = function(id) { + let object = this.data[id]; + if (object === undefined) { + object = this.deleted[id]; + } + return object; + }; + ApiService.prototype.fetchAll = function () { var deferred = $q.defer(); var self = this; @@ -48,6 +61,30 @@ app.factory('ApiService', function ($http, $q) { return deferred.promise; }; + ApiService.prototype.fetchDeleted = function (scopeId) { + var deferred = $q.defer(); + var self = this; + $http.get(this.generateUrl(scopeId + '/' + this.endpoint + '/deleted')).then(function (response) { + var objects = response.data; + objects.forEach(function (obj) { + if(self.deleted[obj.id] !== undefined) { + return; + } + + self.deleted[obj.id] = obj; + + if(self.afterFetch !== undefined) { + self.afterFetch(obj); + } + }); + deferred.resolve(objects); + }, function (error) { + deferred.reject('Fetching ' + self.endpoint + ' failed'); + }); + return deferred.promise; + }; + + ApiService.prototype.fetchOne = function (id) { this.id = id; @@ -104,21 +141,41 @@ app.factory('ApiService', function ($http, $q) { var self = this; $http.delete(this.baseUrl + '/' + id).then(function (response) { - self.remove(id); + self.deleted[id] = self.data[id]; + delete self.data[id]; + + let deletedAt = response.data.deletedAt; + if (deletedAt !== undefined) { + self.deleted[id].deletedAt = deletedAt; + } + deferred.resolve(response.data); }, function (error) { deferred.reject('Deleting ' + self.endpoint + ' failed'); }); return deferred.promise; - }; + ApiService.prototype.undoDelete = function(entity) { + var self = this; + entity.deletedAt = 0; + + var promise = this.update(entity); + + promise.then(() => { + self.data[entity.id] = entity; + delete this.deleted[entity.id]; + }); + + return promise; + }; // methods for managing data ApiService.prototype.clear = function () { this.data = {}; }; + ApiService.prototype.add = function (entity) { var element = this.data[entity.id]; if (element === undefined) { @@ -132,11 +189,7 @@ app.factory('ApiService', function ($http, $q) { element.status = {}; } }; - ApiService.prototype.remove = function (id) { - if (this.data[id] !== undefined) { - delete this.data[id]; - } - }; + ApiService.prototype.addAll = function (entities) { var self = this; angular.forEach(entities, function (entity) { diff --git a/js/service/CardService.js b/js/service/CardService.js index 51edc4e1d..eb62785f0 100644 --- a/js/service/CardService.js +++ b/js/service/CardService.js @@ -4,20 +4,20 @@ * @author Julius Härtl * * @license GNU AGPL version 3 or any later version - * + * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. - * + * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. - * + * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . - * + * */ import app from '../app/App.js'; diff --git a/js/service/StackService.js b/js/service/StackService.js index a4671a331..1a3224573 100644 --- a/js/service/StackService.js +++ b/js/service/StackService.js @@ -4,20 +4,20 @@ * @author Julius Härtl * * @license GNU AGPL version 3 or any later version - * + * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. - * + * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. - * + * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . - * + * */ import app from '../app/App.js'; @@ -27,6 +27,11 @@ app.factory('StackService', function (ApiService, CardService, $http, $q) { ApiService.call(this, $http, ep, $q); }; StackService.prototype = angular.copy(ApiService.prototype); + + StackService.prototype.afterFetch = function(stack) { + CardService.addAll(stack.cards); + }; + StackService.prototype.fetchAll = function (boardId) { var deferred = $q.defer(); var self = this; @@ -129,27 +134,6 @@ app.factory('StackService', function (ApiService, CardService, $http, $q) { } }; - // FIXME: Should not show popup but proper undo mechanism - StackService.prototype.delete = function (id) { - var deferred = $q.defer(); - var self = this; - - OC.dialogs.confirm(t('deck', 'Are you sure you want to delete the stack with all of its data?'), t('deck', 'Delete'), function(state) { - if (!state) { - return; - } - $http.delete(self.baseUrl + '/' + id).then(function (response) { - self.remove(id); - deferred.resolve(response.data); - - }, function (error) { - deferred.reject('Deleting ' + self.endpoint + ' failed'); - }); - }); - return deferred.promise; - }; - var service = new StackService($http, 'stacks', $q); return service; }); - diff --git a/lib/Controller/CardController.php b/lib/Controller/CardController.php index aeadcb6f9..22502f27e 100644 --- a/lib/Controller/CardController.php +++ b/lib/Controller/CardController.php @@ -89,10 +89,11 @@ class CardController extends Controller { * @param $order * @param $description * @param $duedate + * @param $deletedAt * @return \OCP\AppFramework\Db\Entity */ - public function update($id, $title, $stackId, $type, $order, $description, $duedate) { - return $this->cardService->update($id, $title, $stackId, $type, $order, $description, $this->userId, $duedate); + public function update($id, $title, $stackId, $type, $order, $description, $duedate, $deletedAt) { + return $this->cardService->update($id, $title, $stackId, $type, $order, $description, $this->userId, $duedate, $deletedAt); } /** @@ -104,6 +105,15 @@ class CardController extends Controller { return $this->cardService->delete($cardId); } + /** + * @NoAdminRequired + * @param $boardId + * @return \OCP\AppFramework\Db\Entity + */ + public function deleted($boardId) { + return $this->cardService->fetchDeleted($boardId); + } + /** * @NoAdminRequired * @param $cardId diff --git a/lib/Controller/StackController.php b/lib/Controller/StackController.php index 3e2e358ed..3063d858f 100644 --- a/lib/Controller/StackController.php +++ b/lib/Controller/StackController.php @@ -74,10 +74,11 @@ class StackController extends Controller { * @param $title * @param $boardId * @param $order + * @param $deletedAt * @return \OCP\AppFramework\Db\Entity */ - public function update($id, $title, $boardId, $order) { - return $this->stackService->update($id, $title, $boardId, $order); + public function update($id, $title, $boardId, $order, $deletedAt) { + return $this->stackService->update($id, $title, $boardId, $order, $deletedAt); } /** @@ -98,4 +99,14 @@ class StackController extends Controller { public function delete($stackId) { return $this->stackService->delete($stackId); } + + /** + * @NoAdminRequired + * @param $boardId + * @return \OCP\AppFramework\Db\Entity + */ + public function deleted($boardId) { + return $this->stackService->fetchDeleted($boardId); + } + } diff --git a/lib/Db/Card.php b/lib/Db/Card.php index 21f65d79e..eb5e93911 100644 --- a/lib/Db/Card.php +++ b/lib/Db/Card.php @@ -5,20 +5,20 @@ * @author Julius Härtl * * @license GNU AGPL version 3 or any later version - * + * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. - * + * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. - * + * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . - * + * */ namespace OCA\Deck\Db; @@ -42,6 +42,7 @@ class Card extends RelationalEntity { protected $archived = false; protected $duedate; protected $notified = false; + protected $deletedAt = 0; private $databaseType = 'sqlite'; @@ -58,6 +59,7 @@ class Card extends RelationalEntity { $this->addType('createdAt', 'integer'); $this->addType('archived', 'boolean'); $this->addType('notified', 'boolean'); + $this->addType('deletedAt', 'integer'); $this->addRelation('labels'); $this->addRelation('assignedUsers'); $this->addRelation('attachments'); diff --git a/lib/Db/CardMapper.php b/lib/Db/CardMapper.php index e0234fc43..31f363398 100644 --- a/lib/Db/CardMapper.php +++ b/lib/Db/CardMapper.php @@ -120,10 +120,17 @@ class CardMapper extends DeckMapper implements IPermissionMapper { public function findAll($stackId, $limit = null, $offset = null) { $sql = 'SELECT * FROM `*PREFIX*deck_cards` - WHERE `stack_id` = ? AND NOT archived ORDER BY `order`'; + WHERE `stack_id` = ? AND NOT archived AND deleted_at = 0 ORDER BY `order`'; return $this->findEntities($sql, [$stackId], $limit, $offset); } + public function findDeleted($boardId, $limit = null, $offset = null) { + $sql = 'SELECT c.* FROM `*PREFIX*deck_cards` c + INNER JOIN `*PREFIX*deck_stacks` s ON s.id = c.stack_id + WHERE `s`.`board_id` = ? AND NOT c.archived AND NOT c.deleted_at = 0 ORDER BY `c`.`order`'; + return $this->findEntities($sql, [$boardId], $limit, $offset); + } + public function findAllArchived($stackId, $limit = null, $offset = null) { $sql = 'SELECT * FROM `*PREFIX*deck_cards` WHERE `stack_id`=? AND archived ORDER BY `last_modified`'; return $this->findEntities($sql, [$stackId], $limit, $offset); @@ -197,4 +204,4 @@ class CardMapper extends DeckMapper implements IPermissionMapper { } -} \ No newline at end of file +} diff --git a/lib/Db/LabelMapper.php b/lib/Db/LabelMapper.php index 9ed47266b..3107b4310 100644 --- a/lib/Db/LabelMapper.php +++ b/lib/Db/LabelMapper.php @@ -5,20 +5,20 @@ * @author Julius Härtl * * @license GNU AGPL version 3 or any later version - * + * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. - * + * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. - * + * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . - * + * */ namespace OCA\Deck\Db; diff --git a/lib/Db/Stack.php b/lib/Db/Stack.php index 8a3d00ffb..f1ba66c9c 100644 --- a/lib/Db/Stack.php +++ b/lib/Db/Stack.php @@ -27,12 +27,14 @@ class Stack extends RelationalEntity { protected $title; protected $boardId; + protected $deletedAt = 0; protected $cards = array(); protected $order; public function __construct() { $this->addType('id', 'integer'); $this->addType('boardId', 'integer'); + $this->addType('deletedAt', 'integer'); $this->addType('order', 'integer'); } @@ -47,4 +49,4 @@ class Stack extends RelationalEntity { } return $json; } -} \ No newline at end of file +} diff --git a/lib/Db/StackMapper.php b/lib/Db/StackMapper.php index 51d8bf29c..c98d87cc0 100644 --- a/lib/Db/StackMapper.php +++ b/lib/Db/StackMapper.php @@ -51,10 +51,17 @@ class StackMapper extends DeckMapper implements IPermissionMapper { public function findAll($boardId, $limit = null, $offset = null) { - $sql = 'SELECT * FROM `*PREFIX*deck_stacks` WHERE `board_id` = ? ORDER BY `order`'; + $sql = 'SELECT * FROM `*PREFIX*deck_stacks` WHERE `board_id` = ? AND deleted_at = 0 ORDER BY `order`'; return $this->findEntities($sql, [$boardId], $limit, $offset); } - + + + public function findDeleted($boardId, $limit = null, $offset = null) { + $sql = 'SELECT * FROM `*PREFIX*deck_stacks` s + WHERE `s`.`board_id` = ? AND NOT s.deleted_at = 0'; + return $this->findEntities($sql, [$boardId], $limit, $offset); + } + public function delete(Entity $entity) { // delete cards on stack @@ -73,4 +80,4 @@ class StackMapper extends DeckMapper implements IPermissionMapper { $entity = $this->find($stackId); return $entity->getBoardId(); } -} \ No newline at end of file +} diff --git a/lib/Service/BoardService.php b/lib/Service/BoardService.php index 3f9a905cc..ab6baf225 100644 --- a/lib/Service/BoardService.php +++ b/lib/Service/BoardService.php @@ -256,4 +256,4 @@ class BoardService { return $this->aclMapper->delete($acl); } -} \ No newline at end of file +} diff --git a/lib/Service/CardService.php b/lib/Service/CardService.php index a9b7ae7a2..fcf691ecc 100644 --- a/lib/Service/CardService.php +++ b/lib/Service/CardService.php @@ -30,6 +30,8 @@ use OCA\Deck\Db\CardMapper; use OCA\Deck\Db\Acl; use OCA\Deck\Db\StackMapper; use OCA\Deck\Notification\NotificationHelper; +use OCA\Deck\Db\BoardMapper; +use OCA\Deck\Db\LabelMapper; use OCA\Deck\NotFoundException; use OCA\Deck\StatusException; @@ -38,6 +40,8 @@ class CardService { private $cardMapper; private $stackMapper; + private $boardMapper; + private $labelMapper; private $permissionService; private $boardService; private $notificationHelper; @@ -45,9 +49,22 @@ class CardService { private $attachmentService; private $currentUser; - public function __construct(CardMapper $cardMapper, StackMapper $stackMapper, PermissionService $permissionService, BoardService $boardService, NotificationHelper $notificationHelper, AssignedUsersMapper $assignedUsersMapper, AttachmentService $attachmentService, $userId) { + public function __construct( + CardMapper $cardMapper, + StackMapper $stackMapper, + BoardMapper $boardMapper, + LabelMapper $labelMapper, + PermissionService $permissionService, + BoardService $boardService, + NotificationHelper $notificationHelper, + AssignedUsersMapper $assignedUsersMapper, + AttachmentService $attachmentService, + $userId + ) { $this->cardMapper = $cardMapper; $this->stackMapper = $stackMapper; + $this->boardMapper = $boardMapper; + $this->labelMapper = $labelMapper; $this->permissionService = $permissionService; $this->boardService = $boardService; $this->notificationHelper = $notificationHelper; @@ -56,6 +73,22 @@ class CardService { $this->currentUser = $userId; } + public function enrich($card) { + $cardId = $card->getId(); + $card->setAssignedUsers($this->assignedUsersMapper->find($cardId)); + $card->setLabels($this->labelMapper->findAssignedLabelsForCard($cardId)); + $card->setAttachmentCount($this->attachmentService->count($cardId)); + } + + public function fetchDeleted($boardId) { + $this->permissionService->checkPermission($this->boardMapper, $boardId, Acl::PERMISSION_READ); + $cards = $this->cardMapper->findDeleted($boardId); + foreach ($cards as $card) { + $this->enrich($card); + } + return $cards; + } + public function find($cardId) { $this->permissionService->checkPermission($this->cardMapper, $cardId, Acl::PERMISSION_READ); $card = $this->cardMapper->find($cardId); @@ -89,10 +122,13 @@ class CardService { if ($this->boardService->isArchived($this->cardMapper, $id)) { throw new StatusException('Operation not allowed. This board is archived.'); } - return $this->cardMapper->delete($this->cardMapper->find($id)); + $card = $this->cardMapper->find($id); + $card->setDeletedAt(time()); + $this->cardMapper->update($card); + return $card; } - public function update($id, $title, $stackId, $type, $order, $description, $owner, $duedate) { + public function update($id, $title, $stackId, $type, $order, $description, $owner, $duedate, $deletedAt) { $this->permissionService->checkPermission($this->cardMapper, $id, Acl::PERMISSION_EDIT); if ($this->boardService->isArchived($this->cardMapper, $id)) { throw new StatusException('Operation not allowed. This board is archived.'); @@ -108,6 +144,7 @@ class CardService { $card->setOwner($owner); $card->setDescription($description); $card->setDuedate($duedate); + $card->setDeletedAt($deletedAt); return $this->cardMapper->update($card); } diff --git a/lib/Service/StackService.php b/lib/Service/StackService.php index 30e40cc6e..b64766e79 100644 --- a/lib/Service/StackService.php +++ b/lib/Service/StackService.php @@ -5,26 +5,27 @@ * @author Julius Härtl * * @license GNU AGPL version 3 or any later version - * + * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. - * + * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. - * + * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . - * + * */ namespace OCA\Deck\Service; use OCA\Deck\Db\Acl; use OCA\Deck\Db\CardMapper; +use OCA\Deck\Db\BoardMapper; use OCA\Deck\Db\LabelMapper; use OCA\Deck\Db\AssignedUsersMapper; use OCA\Deck\Db\Stack; @@ -38,46 +39,67 @@ class StackService { private $stackMapper; private $cardMapper; + private $boardMapper; private $labelMapper; private $permissionService; private $boardService; + private $cardService; private $assignedUsersMapper; private $attachmentService; public function __construct( StackMapper $stackMapper, + BoardMapper $boardMapper, CardMapper $cardMapper, LabelMapper $labelMapper, PermissionService $permissionService, BoardService $boardService, + CardService $cardService, AssignedUsersMapper $assignedUsersMapper, AttachmentService $attachmentService ) { $this->stackMapper = $stackMapper; + $this->boardMapper = $boardMapper; $this->cardMapper = $cardMapper; $this->labelMapper = $labelMapper; $this->permissionService = $permissionService; $this->boardService = $boardService; + $this->cardService = $cardService; $this->assignedUsersMapper = $assignedUsersMapper; $this->attachmentService = $attachmentService; } + private function enrichStackWithCards($stack) { + $cards = $this->cardMapper->findAll($stack->id); + + if(is_null($cards)) { + return; + } + + foreach ($cards as $card) { + $this->cardService->enrich($card); + } + + $stack->setCards($cards); + } + + private function enrichStacksWithCards($stacks) { + foreach ($stacks as $stack) { + $this->enrichStackWithCards($stack); + } + } + public function findAll($boardId) { $this->permissionService->checkPermission(null, $boardId, Acl::PERMISSION_READ); $stacks = $this->stackMapper->findAll($boardId); - $labels = $this->labelMapper->getAssignedLabelsForBoard($boardId); - foreach ($stacks as $stackIndex => $stack) { - $cards = $this->cardMapper->findAll($stack->id); - foreach ($cards as $cardIndex => $card) { - $assignedUsers = $this->assignedUsersMapper->find($card->getId()); - $card->setAssignedUsers($assignedUsers); - if (array_key_exists($card->id, $labels)) { - $cards[$cardIndex]->setLabels($labels[$card->id]); - } - $card->setAttachmentCount($this->attachmentService->count($card->getId())); - } - $stacks[$stackIndex]->setCards($cards); - } + $this->enrichStacksWithCards($stacks); + return $stacks; + } + + public function fetchDeleted($boardId) { + $this->permissionService->checkPermission($this->boardMapper, $boardId, Acl::PERMISSION_READ); + $stacks = $this->stackMapper->findDeleted($boardId); + $this->enrichStacksWithCards($stacks); return $stacks; } @@ -115,10 +137,18 @@ class StackService { public function delete($id) { $this->permissionService->checkPermission($this->stackMapper, $id, Acl::PERMISSION_MANAGE); - return $this->stackMapper->delete($this->stackMapper->find($id)); + + $stack = $this->stackMapper->find($id); + $stack->setDeletedAt(time()); + $this->stackMapper->update($stack); + + $this->enrichStackWithCards($stack); + + return $stack; } - public function update($id, $title, $boardId, $order) { + + public function update($id, $title, $boardId, $order, $deletedAt) { $this->permissionService->checkPermission($this->stackMapper, $id, Acl::PERMISSION_MANAGE); if ($this->boardService->isArchived($this->stackMapper, $id)) { throw new StatusException('Operation not allowed. This board is archived.'); @@ -127,6 +157,7 @@ class StackService { $stack->setTitle($title); $stack->setBoardId($boardId); $stack->setOrder($order); + $stack->setDeletedAt($deletedAt); return $this->stackMapper->update($stack); } @@ -154,4 +185,4 @@ class StackService { return $result; } -} \ No newline at end of file +} diff --git a/templates/part.board.mainView.php b/templates/part.board.mainView.php index bc5f483c2..26447c9c2 100644 --- a/templates/part.board.mainView.php +++ b/templates/part.board.mainView.php @@ -52,7 +52,7 @@ + ng-click="stackDelete(s)"> + +
+
    +
  • + + {{deletedStack.title}} + {{deletedStack.deletedAt | relativeDateFilter }} + + + +
  • +
+
+ +
+
    +
  • + + {{deletedCard.title}} + {{stackservice.tryAllThenDeleted(deletedCard.stackId).title}} + {{deletedCard.deletedAt | relativeDateFilter }} + + + +
  • +
+
diff --git a/tests/unit/Db/BoardMapperTest.php b/tests/unit/Db/BoardMapperTest.php index 64c878893..f11eb0363 100644 --- a/tests/unit/Db/BoardMapperTest.php +++ b/tests/unit/Db/BoardMapperTest.php @@ -5,20 +5,20 @@ * @author Julius Härtl * * @license GNU AGPL version 3 or any later version - * + * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. - * + * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. - * + * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . - * + * */ namespace OCA\Deck\Db; @@ -175,4 +175,4 @@ class BoardMapperTest extends MapperTestUtility { } } -} \ No newline at end of file +} diff --git a/tests/unit/Db/CardTest.php b/tests/unit/Db/CardTest.php index be6472b88..ba8ff7030 100644 --- a/tests/unit/Db/CardTest.php +++ b/tests/unit/Db/CardTest.php @@ -5,20 +5,20 @@ * @author Julius Härtl * * @license GNU AGPL version 3 or any later version - * + * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. - * + * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. - * + * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . - * + * */ namespace OCA\Deck\Db; @@ -81,6 +81,7 @@ class CardTest extends TestCase { 'attachments' => null, 'attachmentCount' => null, 'assignedUsers' => null, + 'deletedAt' => 0 ], $card->jsonSerialize()); } public function testJsonSerializeLabels() { @@ -103,6 +104,7 @@ class CardTest extends TestCase { 'attachments' => null, 'attachmentCount' => null, 'assignedUsers' => null, + 'deletedAt' => 0 ], $card->jsonSerialize()); } @@ -135,7 +137,8 @@ class CardTest extends TestCase { 'attachments' => null, 'attachmentCount' => null, 'assignedUsers' => ['user1'], + 'deletedAt' => 0 ], $card->jsonSerialize()); } -} \ No newline at end of file +} diff --git a/tests/unit/Db/StackTest.php b/tests/unit/Db/StackTest.php index a28a732c6..75873d350 100644 --- a/tests/unit/Db/StackTest.php +++ b/tests/unit/Db/StackTest.php @@ -5,20 +5,20 @@ * @author Julius Härtl * * @license GNU AGPL version 3 or any later version - * + * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. - * + * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. - * + * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . - * + * */ namespace OCA\Deck\Db; @@ -39,6 +39,7 @@ class StackTest extends \Test\TestCase { 'title' => "My Stack", 'order' => 1, 'boardId' => 1, + 'deletedAt' => 0 ], $board->jsonSerialize()); } public function testJsonSerializeWithCards() { @@ -51,6 +52,7 @@ class StackTest extends \Test\TestCase { 'order' => 1, 'boardId' => 1, 'cards' => array("foo", "bar"), + 'deletedAt' => 0 ], $board->jsonSerialize()); } -} \ No newline at end of file +} diff --git a/tests/unit/Service/CardServiceTest.php b/tests/unit/Service/CardServiceTest.php index 0f850c006..74f4ee0bf 100644 --- a/tests/unit/Service/CardServiceTest.php +++ b/tests/unit/Service/CardServiceTest.php @@ -29,6 +29,8 @@ use OCA\Deck\Db\AssignedUsersMapper; use OCA\Deck\Db\Card; use OCA\Deck\Db\CardMapper; use OCA\Deck\Db\StackMapper; +use OCA\Deck\Db\BoardMapper; +use OCA\Deck\Db\LabelMapper; use OCA\Deck\NotFoundException; use OCA\Deck\Notification\NotificationHelper; use OCA\Deck\StatusException; @@ -48,19 +50,36 @@ class CardServiceTest extends TestCase { private $notificationHelper; /** @var AssignedUsersMapper|\PHPUnit\Framework\MockObject\MockObject */ private $assignedUsersMapper; - /** @var BoardService|\PHPUnit\Framework\MockObject\MockObject */ - private $boardService; + /** @var BoardService|\PHPUnit\Framework\MockObject\MockObject */ + private $boardService; + /** @var LabelMapper|\PHPUnit\Framework\MockObject\MockObject */ + private $labelMapper; + private $boardMapper; + private $attachmentService; - public function setUp() { - parent::setUp(); + public function setUp() { + parent::setUp(); $this->cardMapper = $this->createMock(CardMapper::class); $this->stackMapper = $this->createMock(StackMapper::class); + $this->boardMapper = $this->createMock(BoardMapper::class); + $this->labelMapper = $this->createMock(LabelMapper::class); $this->permissionService = $this->createMock(PermissionService::class); $this->boardService = $this->createMock(BoardService::class); $this->notificationHelper = $this->createMock(NotificationHelper::class); $this->assignedUsersMapper = $this->createMock(AssignedUsersMapper::class); $this->attachmentService = $this->createMock(AttachmentService::class); - $this->cardService = new CardService($this->cardMapper, $this->stackMapper, $this->permissionService, $this->boardService, $this->notificationHelper, $this->assignedUsersMapper, $this->attachmentService, 'userXY'); + $this->cardService = new CardService( + $this->cardMapper, + $this->stackMapper, + $this->boardMapper, + $this->labelMapper, + $this->permissionService, + $this->boardService, + $this->notificationHelper, + $this->assignedUsersMapper, + $this->attachmentService, + 'user1' + ); } public function testFind() { @@ -100,13 +119,15 @@ class CardServiceTest extends TestCase { } public function testDelete() { + $cardToBeDeleted = new Card(); $this->cardMapper->expects($this->once()) ->method('find') - ->willReturn(new Card()); + ->willReturn($cardToBeDeleted); $this->cardMapper->expects($this->once()) - ->method('delete') - ->willReturn(1); - $this->assertEquals(1, $this->cardService->delete(123)); + ->method('update') + ->willReturn($cardToBeDeleted); + $this->cardService->delete(123); + $this->assertTrue($cardToBeDeleted->getDeletedAt() <= time(), 'deletedAt is in the past'); } public function testUpdate() { @@ -115,7 +136,7 @@ class CardServiceTest extends TestCase { $card->setArchived(false); $this->cardMapper->expects($this->once())->method('find')->willReturn($card); $this->cardMapper->expects($this->once())->method('update')->willReturnCallback(function($c) { return $c; }); - $actual = $this->cardService->update(123, 'newtitle', 234, 'text', 999, 'foo', 'admin', '2017-01-01 00:00:00'); + $actual = $this->cardService->update(123, 'newtitle', 234, 'text', 999, 'foo', 'admin', '2017-01-01 00:00:00', null); $this->assertEquals('newtitle', $actual->getTitle()); $this->assertEquals(234, $actual->getStackId()); $this->assertEquals('text', $actual->getType()); @@ -131,7 +152,7 @@ class CardServiceTest extends TestCase { $this->cardMapper->expects($this->once())->method('find')->willReturn($card); $this->cardMapper->expects($this->never())->method('update'); $this->setExpectedException(StatusException::class); - $this->cardService->update(123, 'newtitle', 234, 'text', 999, 'foo', 'admin', '2017-01-01 00:00:00'); + $this->cardService->update(123, 'newtitle', 234, 'text', 999, 'foo', 'admin', '2017-01-01 00:00:00', null); } public function testRename() { @@ -317,4 +338,4 @@ class CardServiceTest extends TestCase { } -} \ No newline at end of file +} diff --git a/tests/unit/Service/StackServiceTest.php b/tests/unit/Service/StackServiceTest.php index d9cc908fc..e013de63d 100644 --- a/tests/unit/Service/StackServiceTest.php +++ b/tests/unit/Service/StackServiceTest.php @@ -28,6 +28,7 @@ namespace OCA\Deck\Service; use OCA\Deck\Db\AssignedUsersMapper; use OCA\Deck\Db\Card; use OCA\Deck\Db\CardMapper; +use OCA\Deck\Db\BoardMapper; use OCA\Deck\Db\Label; use OCA\Deck\Db\LabelMapper; use OCA\Deck\Db\Stack; @@ -48,6 +49,8 @@ class StackServiceTest extends TestCase { private $stackMapper; /** @var \PHPUnit\Framework\MockObject\MockObject|CardMapper */ private $cardMapper; + /** @var \PHPUnit\Framework\MockObject\MockObject|BoardMapper */ + private $boardMapper; /** @var \PHPUnit\Framework\MockObject\MockObject|LabelMapper */ private $labelMapper; /** @var \PHPUnit\Framework\MockObject\MockObject|PermissionService */ @@ -58,23 +61,30 @@ class StackServiceTest extends TestCase { private $attachmentService; /** @var BoardService|\PHPUnit\Framework\MockObject\MockObject */ private $boardService; + /** @var CardService|\PHPUnit\Framework\MockObject\MockObject */ + private $cardService; public function setUp() { parent::setUp(); $this->stackMapper = $this->createMock(StackMapper::class); $this->cardMapper = $this->createMock(CardMapper::class); - $this->labelMapper = $this->createMock(LabelMapper::class); + $this->boardMapper = $this->createMock(BoardMapper::class); $this->permissionService = $this->createMock(PermissionService::class); $this->boardService = $this->createMock(BoardService::class); + $this->cardService = $this->createMock(CardService::class); $this->assignedUsersMapper = $this->createMock(AssignedUsersMapper::class); $this->attachmentService = $this->createMock(AttachmentService::class); + $this->labelMapper = $this->createMock(LabelMapper::class); $this->stackService = new StackService( $this->stackMapper, - $this->cardMapper, - $this->labelMapper, + $this->boardMapper, + $this->cardMapper, + $this->labelMapper, + $this->permissionService, $this->boardService, + $this->cardService, $this->assignedUsersMapper, $this->attachmentService ); @@ -83,9 +93,16 @@ class StackServiceTest extends TestCase { public function testFindAll() { $this->permissionService->expects($this->once())->method('checkPermission'); $this->stackMapper->expects($this->once())->method('findAll')->willReturn($this->getStacks()); - $this->labelMapper->expects($this->once())->method('getAssignedLabelsForBoard')->willReturn($this->getLabels()); + $this->cardService->expects($this->atLeastOnce())->method('enrich')->will( + $this->returnCallback( + function($card) { + $card->setLabels($this->getLabels()[$card->getId()]); + } + ) + ); $this->cardMapper->expects($this->any())->method('findAll')->willReturn($this->getCards(222)); + $actual = $this->stackService->findAll(123); for($stackId=0; $stackId<3; $stackId++) { for ($cardId=0;$cardId<10;$cardId++) { @@ -130,8 +147,10 @@ class StackServiceTest extends TestCase { private function getStacks() { $s1 = new Stack(); $s1->setId(222); + $s1->setBoardId(1); $s2 = new Stack(); $s2->setId(223); + $s1->setBoardId(1); return [$s1, $s2]; } private function getCards($stackId=0) { @@ -158,9 +177,12 @@ class StackServiceTest extends TestCase { public function testDelete() { $this->permissionService->expects($this->once())->method('checkPermission'); - $this->stackMapper->expects($this->once())->method('find')->willReturn(new Stack()); - $this->stackMapper->expects($this->once())->method('delete'); + $stackToBeDeleted = new Stack(); + $stackToBeDeleted->setId(1); + $this->stackMapper->expects($this->once())->method('find')->willReturn($stackToBeDeleted); + $this->stackMapper->expects($this->once())->method('update'); $this->stackService->delete(123); + $this->assertTrue($stackToBeDeleted->getDeletedAt() <= time(), "deletedAt is in the past"); } public function testUpdate() { @@ -172,7 +194,7 @@ class StackServiceTest extends TestCase { $stack->setTitle('Foo'); $stack->setBoardId(2); $stack->setOrder(1); - $result = $this->stackService->update(123, 'Foo', 2, 1); + $result = $this->stackService->update(123, 'Foo', 2, 1, null); $this->assertEquals($stack, $result); } @@ -207,4 +229,4 @@ class StackServiceTest extends TestCase { return $stack; } -} \ No newline at end of file +} diff --git a/tests/unit/controller/CardControllerTest.php b/tests/unit/controller/CardControllerTest.php index 6f19db775..904f7b03a 100644 --- a/tests/unit/controller/CardControllerTest.php +++ b/tests/unit/controller/CardControllerTest.php @@ -76,7 +76,7 @@ class CardControllerTest extends \Test\TestCase { ->method('update') ->with(1, 'title', 3, 'text', 5, 'foo', $this->userId, '2017-01-01 00:00:00') ->willReturn(1); - $this->assertEquals(1, $this->controller->update(1, 'title', 3, 'text', 5, 'foo', '2017-01-01 00:00:00')); + $this->assertEquals(1, $this->controller->update(1, 'title', 3, 'text', 5, 'foo', '2017-01-01 00:00:00', null)); } public function testDelete() { diff --git a/tests/unit/controller/StackControllerTest.php b/tests/unit/controller/StackControllerTest.php index 01f0f963e..b209178db 100644 --- a/tests/unit/controller/StackControllerTest.php +++ b/tests/unit/controller/StackControllerTest.php @@ -81,7 +81,7 @@ class StackControllerTest extends \Test\TestCase { ->method('update') ->with(1, 2, 3, 4) ->willReturn(1); - $this->assertEquals(1, $this->controller->update(1, 2, 3, 4)); + $this->assertEquals(1, $this->controller->update(1, 2, 3, 4, null)); } public function testReorder() {