cards soft delete wip

Signed-off-by: Manuel Arno Korfmann <manu@korfmann.info>

cards: softdelete done; undo delete wip

Signed-off-by: Manuel Arno Korfmann <manu@korfmann.info>

show deleted cards in board settings sidebar wip

Signed-off-by: Manuel Arno Korfmann <manu@korfmann.info>

CardMapper#findDeleted: fix bug in entity property assigning

Signed-off-by: Manuel Arno Korfmann <manu@korfmann.info>
This commit is contained in:
Manuel Arno Korfmann
2018-07-09 23:49:42 +02:00
committed by Julius Härtl
parent 3e4dedf397
commit 2ef4b55af4
10 changed files with 88 additions and 2 deletions

View File

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

View File

@@ -50,6 +50,7 @@ return [
['name' => 'card#create', 'url' => '/cards', 'verb' => 'POST'], ['name' => 'card#create', 'url' => '/cards', 'verb' => 'POST'],
['name' => 'card#update', 'url' => '/cards/{cardId}', 'verb' => 'PUT'], ['name' => 'card#update', 'url' => '/cards/{cardId}', 'verb' => 'PUT'],
['name' => 'card#delete', 'url' => '/cards/{cardId}', 'verb' => 'DELETE'], ['name' => 'card#delete', 'url' => '/cards/{cardId}', 'verb' => 'DELETE'],
['name' => 'card#deleted', 'url' => '/cards/deleted/{boardId}', 'verb' => 'GET'],
['name' => 'card#rename', 'url' => '/cards/{cardId}/rename', 'verb' => 'PUT'], ['name' => 'card#rename', 'url' => '/cards/{cardId}/rename', 'verb' => 'PUT'],
['name' => 'card#reorder', 'url' => '/cards/{cardId}/reorder', 'verb' => 'PUT'], ['name' => 'card#reorder', 'url' => '/cards/{cardId}/reorder', 'verb' => 'PUT'],
['name' => 'card#archive', 'url' => '/cards/{cardId}/archive', 'verb' => 'PUT'], ['name' => 'card#archive', 'url' => '/cards/{cardId}/archive', 'verb' => 'PUT'],

View File

@@ -42,6 +42,8 @@ app.controller('BoardController', function ($rootScope, $scope, $stateParams, St
$scope.board = BoardService.getCurrent(); $scope.board = BoardService.getCurrent();
$scope.uploader = FileService.uploader; $scope.uploader = FileService.uploader;
$scope.deletedCards = [];
// workaround for $stateParams changes not being propagated // workaround for $stateParams changes not being propagated
$scope.$watch(function() { $scope.$watch(function() {
return $state.params; return $state.params;
@@ -136,6 +138,14 @@ app.controller('BoardController', function ($rootScope, $scope, $stateParams, St
}); });
}; };
$scope.loadDeletedCards = function() {
CardService.fetchDeleted($scope.id).then(function (data) {
$scope.deletedCards = data;
}, function (error) {
$scope.statusservice.setError('Error occured', error);
});
}
$scope.loadDefault = function () { $scope.loadDefault = function () {
StackService.fetchAll($scope.id).then(function (data) { StackService.fetchAll($scope.id).then(function (data) {
$scope.statusservice.releaseWaiting(); $scope.statusservice.releaseWaiting();
@@ -193,6 +203,7 @@ app.controller('BoardController', function ($rootScope, $scope, $stateParams, St
} }
CardService.delete(card.id).then(function () { CardService.delete(card.id).then(function () {
StackService.removeCard(card); StackService.removeCard(card);
$scope.loadDeletedCards();
}); });
}); });
}; };
@@ -384,4 +395,5 @@ app.controller('BoardController', function ($rootScope, $scope, $stateParams, St
return card.attachmentCount; return card.attachmentCount;
}; };
$scope.loadDeletedCards();
}); });

View File

@@ -114,6 +114,18 @@ app.factory('ApiService', function ($http, $q) {
}; };
ApiService.prototype.softDelete = function (id) {
var deferred = $q.defer();
var self = this;
$http.delete(this.baseUrl + '/' + id).then(function (response) {
self.data[id].deletedAt = response.data.deletedAt;
deferred.resolve(response.data);
}, function (error) {
deferred.reject('Deleting ' + self.endpoint + ' failed');
});
return deferred.promise;
};
// methods for managing data // methods for managing data
ApiService.prototype.clear = function () { ApiService.prototype.clear = function () {

View File

@@ -27,6 +27,8 @@ app.factory('CardService', function (ApiService, $http, $q) {
}; };
CardService.prototype = angular.copy(ApiService.prototype); CardService.prototype = angular.copy(ApiService.prototype);
CardService.prototype.delete = CardService.prototype.softDelete;
CardService.prototype.reorder = function (card, order) { CardService.prototype.reorder = function (card, order) {
var deferred = $q.defer(); var deferred = $q.defer();
var self = this; var self = this;
@@ -172,6 +174,22 @@ app.factory('CardService', function (ApiService, $http, $q) {
return deferred.promise; return deferred.promise;
}; };
CardService.prototype.fetchDeleted = function (boardId) {
var deferred = $q.defer();
var self = this;
$http.get(this.baseUrl + '/deleted/' + boardId).then(function (response) {
var objects = response.data;
return objects;
deferred.resolve(self.data);
}, function (error) {
deferred.reject('Fetching ' + self.endpoint + ' failed');
});
return deferred.promise;
};
var service = new CardService($http, 'cards', $q); var service = new CardService($http, 'cards', $q);
return service; return service;
}); });

View File

@@ -104,6 +104,15 @@ class CardController extends Controller {
return $this->cardService->delete($cardId); return $this->cardService->delete($cardId);
} }
/**
* @NoAdminRequired
* @param $boardId
* @return \OCP\AppFramework\Db\Entity
*/
public function deleted($boardId) {
return $this->cardService->fetchDeleted($boardId);
}
/** /**
* @NoAdminRequired * @NoAdminRequired
* @param $cardId * @param $cardId

View File

@@ -42,6 +42,7 @@ class Card extends RelationalEntity {
protected $archived = false; protected $archived = false;
protected $duedate; protected $duedate;
protected $notified = false; protected $notified = false;
protected $deletedAt;
private $databaseType = 'sqlite'; private $databaseType = 'sqlite';
@@ -58,6 +59,7 @@ class Card extends RelationalEntity {
$this->addType('createdAt', 'integer'); $this->addType('createdAt', 'integer');
$this->addType('archived', 'boolean'); $this->addType('archived', 'boolean');
$this->addType('notified', 'boolean'); $this->addType('notified', 'boolean');
$this->addType('deletedAt', 'integer');
$this->addRelation('labels'); $this->addRelation('labels');
$this->addRelation('assignedUsers'); $this->addRelation('assignedUsers');
$this->addRelation('attachments'); $this->addRelation('attachments');

View File

@@ -124,6 +124,13 @@ class CardMapper extends DeckMapper implements IPermissionMapper {
return $this->findEntities($sql, [$stackId], $limit, $offset); 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 AND c.deleted_at <= ? ORDER BY `c`.`order`';
return $this->findEntities($sql, [$boardId, time()], $limit, $offset);
}
public function findAllArchived($stackId, $limit = null, $offset = null) { public function findAllArchived($stackId, $limit = null, $offset = null) {
$sql = 'SELECT * FROM `*PREFIX*deck_cards` WHERE `stack_id`=? AND archived ORDER BY `last_modified`'; $sql = 'SELECT * FROM `*PREFIX*deck_cards` WHERE `stack_id`=? AND archived ORDER BY `last_modified`';
return $this->findEntities($sql, [$stackId], $limit, $offset); return $this->findEntities($sql, [$stackId], $limit, $offset);

View File

@@ -56,6 +56,10 @@ class CardService {
$this->currentUser = $userId; $this->currentUser = $userId;
} }
public function fetchDeleted($boardId) {
return $this->cardMapper->findDeleted($boardId);
}
public function find($cardId) { public function find($cardId) {
$this->permissionService->checkPermission($this->cardMapper, $cardId, Acl::PERMISSION_READ); $this->permissionService->checkPermission($this->cardMapper, $cardId, Acl::PERMISSION_READ);
$card = $this->cardMapper->find($cardId); $card = $this->cardMapper->find($cardId);
@@ -89,7 +93,10 @@ class CardService {
if ($this->boardService->isArchived($this->cardMapper, $id)) { if ($this->boardService->isArchived($this->cardMapper, $id)) {
throw new StatusException('Operation not allowed. This board is archived.'); 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) {

View File

@@ -14,6 +14,7 @@
<ul class="tabHeaders"> <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==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==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 Cards')); ?></a></li>
</ul> </ul>
<div class="tabsContainer"> <div class="tabsContainer">
<div id="tabBoardShare" class="tab" ng-if="params.tab==0 || !params.tab"> <div id="tabBoardShare" class="tab" ng-if="params.tab==0 || !params.tab">
@@ -118,4 +119,13 @@
</ul> </ul>
</div> </div>
<div id="board-detail-deleted-cards" class="tab deletedCardsTabView" ng-if="params.tab==2">
<ul>
<li ng-repeat="deletedCard in deletedCards">
{{123}}
{{deletedCard}}
</li>
</ul>
</div>
</div> </div>