Merge pull request #557 from nextcloud/feature/undo-delete-cards-and-stacks

Feature/undo delete cards and stacks
This commit is contained in:
Julius Härtl
2018-08-15 20:52:01 +02:00
committed by GitHub
27 changed files with 448 additions and 142 deletions

View File

@@ -77,6 +77,14 @@
<length>8</length>
<notnull>false</notnull>
</field>
<field>
<name>deleted_at</name>
<type>integer</type>
<default>0</default>
<length>8</length>
<notnull>false</notnull>
<unsigned>true</unsigned>
</field>
<index>
<name>deck_stacks_board_id_index</name>
<field>
@@ -167,6 +175,14 @@
<type>boolean</type>
<default>false</default>
</field>
<field>
<name>deleted_at</name>
<type>integer</type>
<default>0</default>
<length>8</length>
<notnull>false</notnull>
<unsigned>true</unsigned>
</field>
<index>
<name>deck_cards_stack_id_index</name>
<field>

View File

@@ -14,7 +14,7 @@
- 🚀 Get your project organized
</description>
<version>0.5.0-dev1</version>
<version>0.5.0-dev2</version>
<licence>agpl</licence>
<author>Julius Härtl</author>
<namespace>Deck</namespace>

View File

@@ -5,20 +5,20 @@
* @author Julius Härtl <jus@bitgrid.net>
*
* @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 <http://www.gnu.org/licenses/>.
*
*
*/
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'],

View File

@@ -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 {

View File

@@ -4,20 +4,20 @@
* @author Julius Härtl <jus@bitgrid.net>
*
* @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 <http://www.gnu.org/licenses/>.
*
*
*/
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;
};
});

View File

@@ -4,35 +4,48 @@
* @author Julius Härtl <jus@bitgrid.net>
*
* @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 <http://www.gnu.org/licenses/>.
*
*
*/
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) {

View File

@@ -4,20 +4,20 @@
* @author Julius Härtl <jus@bitgrid.net>
*
* @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 <http://www.gnu.org/licenses/>.
*
*
*/
import app from '../app/App.js';

View File

@@ -4,20 +4,20 @@
* @author Julius Härtl <jus@bitgrid.net>
*
* @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 <http://www.gnu.org/licenses/>.
*
*
*/
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;
});

View File

@@ -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

View File

@@ -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);
}
}

View File

@@ -5,20 +5,20 @@
* @author Julius Härtl <jus@bitgrid.net>
*
* @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 <http://www.gnu.org/licenses/>.
*
*
*/
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');

View File

@@ -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 {
}
}
}

View File

@@ -5,20 +5,20 @@
* @author Julius Härtl <jus@bitgrid.net>
*
* @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 <http://www.gnu.org/licenses/>.
*
*
*/
namespace OCA\Deck\Db;

View File

@@ -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;
}
}
}

View File

@@ -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();
}
}
}

View File

@@ -256,4 +256,4 @@ class BoardService {
return $this->aclMapper->delete($acl);
}
}
}

View File

@@ -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);
}

View File

@@ -5,26 +5,27 @@
* @author Julius Härtl <jus@bitgrid.net>
*
* @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 <http://www.gnu.org/licenses/>.
*
*
*/
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;
}
}
}

View File

@@ -52,7 +52,7 @@
</form>
<button class="icon-delete button-inline stack-actions"
ng-if="!s.status.editStack"
ng-click="stackservice.delete(s.id)"></button>
ng-click="stackDelete(s)"></button>
</h3>
<ul data-as-sortable="sortOptions" is-disabled="!boardservice.canEdit() || filter==='archive'" data-ng-model="s.cards" class="card-list" ng-class="{emptyStack: !s.cards.length}">
<li class="card as-sortable-item"

View File

@@ -5,8 +5,8 @@
<p>{{ statusservice.text }}</p></div>
</div>
<div id="sidebar-header">
<a class="icon-close" ui-sref="board" ng-click="sidebar.show=!sidebar.show" title="<?php p($l->t('Close')); ?>"> &nbsp;<?php
?><span class="hidden-visually"><?php p($l->t('Close')); ?></span><?php
<a class="icon-close" ui-sref="board" ng-click="sidebar.show=!sidebar.show" title="<?php p($l->t('Close')); ?>"> &nbsp;<?php
?><span class="hidden-visually"><?php p($l->t('Close')); ?></span><?php
?></a>
<h3>{{ boardservice.getCurrent().title }}</h3>
</div>
@@ -14,6 +14,8 @@
<ul class="tabHeaders">
<li class="tabHeader" ng-class="{'selected': (params.tab==0 || !params.tab)}" ui-sref="{tab: 0}"><a><?php p($l->t('Sharing')); ?></a></li>
<li class="tabHeader" ng-class="{'selected': (params.tab==1)}" ui-sref="{tab: 1}"><a><?php p($l->t('Tags')); ?></a></li>
<li class="tabHeader" ng-class="{'selected': (params.tab==2)}" ui-sref="{tab: 2}"><a><?php p($l->t('Deleted Stacks')); ?></a></li>
<li class="tabHeader" ng-class="{'selected': (params.tab==3)}" ui-sref="{tab: 3}"><a><?php p($l->t('Deleted Cards')); ?></a></li>
</ul>
<div class="tabsContainer">
<div id="tabBoardShare" class="tab" ng-if="params.tab==0 || !params.tab">
@@ -118,4 +120,31 @@
</ul>
</div>
<div id="board-detail-deleted-stacks" class="tab deletedStacksTabView" ng-if="params.tab==2">
<ul class='board-detail__deleted-list'>
<li class='board-detail__deleted-list__item' ng-repeat="deletedStack in stackservice.deleted">
<span class="icon icon-deck"></span>
<span>{{deletedStack.title}}</span>
<span>{{deletedStack.deletedAt | relativeDateFilter }}</span>
<a ng-click="stackUndoDelete(deletedStack)">
<span class="icon icon-history"></span>
</a>
</li>
</ul>
</div>
<div id="board-detail-deleted-cards" class="tab deletedCardsTabView" ng-if="params.tab==3">
<ul class='board-detail__deleted-list'>
<li class='board-detail__deleted-list__item' ng-repeat="deletedCard in cardservice.deleted">
<span class="icon icon-deck"></span>
<span>{{deletedCard.title}}</span>
<span>{{stackservice.tryAllThenDeleted(deletedCard.stackId).title}}</span>
<span>{{deletedCard.deletedAt | relativeDateFilter }}</span>
<a ng-click="cardOrCardAndStackUndoDelete(deletedCard)">
<span class="icon icon-history"></span>
</a>
</li>
</ul>
</div>
</div>

View File

@@ -5,20 +5,20 @@
* @author Julius Härtl <jus@bitgrid.net>
*
* @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 <http://www.gnu.org/licenses/>.
*
*
*/
namespace OCA\Deck\Db;
@@ -175,4 +175,4 @@ class BoardMapperTest extends MapperTestUtility {
}
}
}
}

View File

@@ -5,20 +5,20 @@
* @author Julius Härtl <jus@bitgrid.net>
*
* @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 <http://www.gnu.org/licenses/>.
*
*
*/
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());
}
}
}

View File

@@ -5,20 +5,20 @@
* @author Julius Härtl <jus@bitgrid.net>
*
* @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 <http://www.gnu.org/licenses/>.
*
*
*/
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());
}
}
}

View File

@@ -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 {
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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() {

View File

@@ -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() {