diff --git a/appinfo/routes.php b/appinfo/routes.php index b641e1073..8834a7fbc 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -60,6 +60,8 @@ ['name' => 'card#reorder', 'url' => '/cards/{cardId}/reorder', 'verb' => 'PUT'], ['name' => 'card#archive', 'url' => '/cards/{cardId}/archive', 'verb' => 'PUT'], ['name' => 'card#unarchive', 'url' => '/cards/{cardId}/unarchive', 'verb' => 'PUT'], + ['name' => 'card#done', 'url' => '/cards/{cardId}/done', 'verb' => 'PUT'], + ['name' => 'card#undone', 'url' => '/cards/{cardId}/undone', 'verb' => 'PUT'], ['name' => 'card#assignLabel', 'url' => '/cards/{cardId}/label/{labelId}', 'verb' => 'POST'], ['name' => 'card#removeLabel', 'url' => '/cards/{cardId}/label/{labelId}', 'verb' => 'DELETE'], ['name' => 'card#assignUser', 'url' => '/cards/{cardId}/assign', 'verb' => 'POST'], diff --git a/docs/API.md b/docs/API.md index e81553e3b..7013c2bfc 100644 --- a/docs/API.md +++ b/docs/API.md @@ -601,6 +601,7 @@ The board list endpoint supports setting an `If-Modified-Since` header to limit "owner":"admin", "order":999, "archived":false, + "done":false, "duedate": "2019-12-24T19:29:30+00:00", "deletedAt":0, "commentsUnread":0, diff --git a/docs/User_documentation_en.md b/docs/User_documentation_en.md index 876b95f02..3d57b347d 100644 --- a/docs/User_documentation_en.md +++ b/docs/User_documentation_en.md @@ -12,11 +12,12 @@ Overall, Deck is easy to use. You can create boards, add users, share the Deck, 1. [Create my first board](#1-create-my-first-board) 2. [Create stacks and cards](#2-create-stacks-and-cards) 3. [Handle cards options](#3-handle-cards-options) -4. [Archive old tasks](#4-archive-old-tasks) -5. [Manage your board](#5-manage-your-board) -6. [Import boards](#6-import-boards) -7. [Search](#7-search) -8. [New owner for the deck entities](#8-new-owner-for-the-deck-entities) +4. [Mark task as done](#4-mark-as-done) +5. [Archive old tasks](#5-archive-old-tasks) +6. [Manage your board](#6-manage-your-board) +7. [Import boards](#7-import-boards) +8. [Search](#8-search) +9. [New owner for the deck entities](#9-new-owner-for-the-deck-entities) ### 1. Create my first board In this example, we're going to create a board and share it with an other nextcloud user. @@ -53,10 +54,16 @@ And even : ![Gif for puting infos on tasks 2](resources/gifs/EN_put_infos_2.gif) +### 3. Mark as done +Once a task has been completed, you can mark it as done. This will prevent it from becoming overdue and hide it from the upcoming cards. +You can mark it as not done at any time. + +![Gif for marking a card as done](resources/gifs/EN_done.gif) + ### 4. Archive old tasks -Once finished or obsolete, a task could be archived. The tasks is not deleted, it's just archived, and you can retrieve it later +Once obsolete, a task could be archived. The tasks is not deleted, it's just archived, and you can retrieve it later -![Gif for puting infos on tasks 2](resources/gifs/EN_archive.gif) +![Gif for archiving a task](resources/gifs/EN_archive.gif) ### 5. Manage your board You can manage the settings of your Deck once you are inside it, by clicking on the small wheel at the top right. diff --git a/docs/resources/gifs/EN_done.gif b/docs/resources/gifs/EN_done.gif new file mode 100644 index 000000000..e65cef47e Binary files /dev/null and b/docs/resources/gifs/EN_done.gif differ diff --git a/l10n/en_GB.js b/l10n/en_GB.js index 02508c1a5..f00b6f347 100644 --- a/l10n/en_GB.js +++ b/l10n/en_GB.js @@ -65,7 +65,12 @@ OC.L10N.register( "Description" : "Description", "Formatting help" : "Formatting help", "(group)" : "(group)", + "Done" : "Done", + "Mark card as done" : "Mark card as done", + "Mark card as not done" : "Mark card as not done", + "No due date" : "No due date", "Archive card" : "Archive card", + "Unarchive card" : "Unarchive card", "Delete card" : "Delete card", "seconds ago" : "seconds ago", "Archived boards" : "Archived boards", diff --git a/l10n/en_GB.json b/l10n/en_GB.json index 35e29efde..efc91ca24 100644 --- a/l10n/en_GB.json +++ b/l10n/en_GB.json @@ -63,7 +63,11 @@ "Description" : "Description", "Formatting help" : "Formatting help", "(group)" : "(group)", + "Mark card as done": "Mark card as done", + "Mark card as not done": "Mark card as not done", + "No due date": "No due date", "Archive card" : "Archive card", + "Anarchive card" : "Unarchive card", "Delete card" : "Delete card", "seconds ago" : "seconds ago", "Archived boards" : "Archived boards", diff --git a/lib/Activity/ActivityManager.php b/lib/Activity/ActivityManager.php index baedf109d..03c74ee0c 100644 --- a/lib/Activity/ActivityManager.php +++ b/lib/Activity/ActivityManager.php @@ -89,6 +89,8 @@ class ActivityManager { public const SUBJECT_CARD_UPDATE_DUEDATE = 'card_update_duedate'; public const SUBJECT_CARD_UPDATE_ARCHIVE = 'card_update_archive'; public const SUBJECT_CARD_UPDATE_UNARCHIVE = 'card_update_unarchive'; + public const SUBJECT_CARD_UPDATE_DONE = 'card_update_done'; + public const SUBJECT_CARD_UPDATE_UNDONE = 'card_update_undone'; public const SUBJECT_CARD_UPDATE_STACKID = 'card_update_stackId'; public const SUBJECT_CARD_USER_ASSIGN = 'card_user_assign'; public const SUBJECT_CARD_USER_UNASSIGN = 'card_user_unassign'; @@ -196,6 +198,12 @@ public function getActivityFormat($language, $subjectIdentifier, $subjectParams case self::SUBJECT_CARD_UPDATE_UNARCHIVE: $subject = $ownActivity ? $l->t('You have unarchived card {card} in list {stack} on board {board}') : $l->t('{user} has unarchived card {card} in list {stack} on board {board}'); break; + case self::SUBJECT_CARD_UPDATE_DONE: + $subject = $ownActivity ? $l->t('You have marked the card {card} as done in list {stack} on board {board}') : $l->t('{user} has marked card {card} as done in list {stack} on board {board}'); + break; + case self::SUBJECT_CARD_UPDATE_UNDONE: + $subject = $ownActivity ? $l->t('You have marked the card {card} as undone in list {stack} on board {board}') : $l->t('{user} has marked the card {card} as undone in list {stack} on board {board}'); + break; case self::SUBJECT_CARD_UPDATE_DUEDATE: if (!isset($subjectParams['after'])) { $subject = $ownActivity ? $l->t('You have removed the due date of card {card}') : $l->t('{user} has removed the due date of card {card}'); @@ -356,6 +364,8 @@ private function createEvent($objectType, $entity, $subject, $additionalParams = case self::SUBJECT_CARD_DELETE: case self::SUBJECT_CARD_UPDATE_ARCHIVE: case self::SUBJECT_CARD_UPDATE_UNARCHIVE: + case self::SUBJECT_CARD_UPDATE_DONE: + case self::SUBJECT_CARD_UPDATE_UNDONE: case self::SUBJECT_CARD_UPDATE_TITLE: case self::SUBJECT_CARD_UPDATE_DESCRIPTION: case self::SUBJECT_CARD_UPDATE_DUEDATE: diff --git a/lib/Controller/CardApiController.php b/lib/Controller/CardApiController.php index 1c9e87a18..612c6f620 100644 --- a/lib/Controller/CardApiController.php +++ b/lib/Controller/CardApiController.php @@ -94,8 +94,8 @@ public function create($title, $type = 'plain', $order = 999, $description = '', * * Update a card */ - public function update($title, $type, $owner, $description = '', $order = 0, $duedate = null, $archived = null) { - $card = $this->cardService->update($this->request->getParam('cardId'), $title, $this->request->getParam('stackId'), $type, $owner, $description, $order, $duedate, 0, $archived); + public function update($title, $type, $owner, $description = '', $order = 0, $duedate = null, $archived = null, $done = null) { + $card = $this->cardService->update($this->request->getParam('cardId'), $title, $this->request->getParam('stackId'), $type, $owner, $description, $order, $duedate, 0, $archived, $done); return new DataResponse($card, HTTP::STATUS_OK); } diff --git a/lib/Controller/CardController.php b/lib/Controller/CardController.php index b6f6e6958..00a9f6ef6 100644 --- a/lib/Controller/CardController.php +++ b/lib/Controller/CardController.php @@ -92,10 +92,12 @@ public function create($title, $stackId, $type = 'plain', $order = 999, string $ * @param $description * @param $duedate * @param $deletedAt + * @param $archived + * @param $done * @return \OCP\AppFramework\Db\Entity */ - public function update($id, $title, $stackId, $type, $order, $description, $duedate, $deletedAt) { - return $this->cardService->update($id, $title, $stackId, $type, $this->userId, $description, $order, $duedate, $deletedAt); + public function update($id, $title, $stackId, $type, $order, $description, $duedate, $deletedAt, $archived, $done) { + return $this->cardService->update($id, $title, $stackId, $type, $this->userId, $description, $order, $duedate, $deletedAt, $archived, $done); } /** @@ -134,6 +136,26 @@ public function unarchive($cardId) { return $this->cardService->unarchive($cardId); } + /** + * @NoAdminRequired + * @param $cardId + * @return \OCP\AppFramework\Db\Entity + */ + public function done($cardId) + { + return $this->cardService->done($cardId); + } + + /** + * @NoAdminRequired + * @param $cardId + * @return \OCP\AppFramework\Db\Entity + */ + public function undone($cardId) + { + return $this->cardService->undone($cardId); + } + /** * @NoAdminRequired * @param $cardId diff --git a/lib/Db/Card.php b/lib/Db/Card.php index 91bc6ab08..892e3e8f3 100644 --- a/lib/Db/Card.php +++ b/lib/Db/Card.php @@ -45,6 +45,7 @@ class Card extends RelationalEntity { protected $owner; protected $order; protected $archived = false; + protected $done = false; protected $duedate; protected $notified = false; protected $deletedAt = 0; @@ -61,13 +62,15 @@ class Card extends RelationalEntity { public const DUEDATE_NOW = 2; public const DUEDATE_OVERDUE = 3; - public function __construct() { + public function __construct() + { $this->addType('id', 'integer'); $this->addType('stackId', 'integer'); $this->addType('order', 'integer'); $this->addType('lastModified', 'integer'); $this->addType('createdAt', 'integer'); $this->addType('archived', 'boolean'); + $this->addType('done', 'boolean'); $this->addType('notified', 'boolean'); $this->addType('deletedAt', 'integer'); $this->addRelation('labels'); diff --git a/lib/Db/CardMapper.php b/lib/Db/CardMapper.php index 338560257..8290b8544 100644 --- a/lib/Db/CardMapper.php +++ b/lib/Db/CardMapper.php @@ -236,6 +236,7 @@ public function findAllWithDue($boardId) { ->where($qb->expr()->eq('s.board_id', $qb->createNamedParameter($boardId, IQueryBuilder::PARAM_INT))) ->andWhere($qb->expr()->isNotNull('c.duedate')) ->andWhere($qb->expr()->eq('c.archived', $qb->createNamedParameter(false, IQueryBuilder::PARAM_BOOL))) + ->andWhere($qb->expr()->eq('c.done', $qb->createNamedParameter(false, IQueryBuilder::PARAM_BOOL))) ->andWhere($qb->expr()->eq('c.deleted_at', $qb->createNamedParameter(0, IQueryBuilder::PARAM_INT))) ->andWhere($qb->expr()->eq('s.deleted_at', $qb->createNamedParameter(0, IQueryBuilder::PARAM_INT))) ->andWhere($qb->expr()->eq('b.archived', $qb->createNamedParameter(false, IQueryBuilder::PARAM_BOOL))) @@ -257,6 +258,7 @@ public function findToMeOrNotAssignedCards($boardId, $username) { ) // Filter out archived/deleted cards and board ->andWhere($qb->expr()->eq('c.archived', $qb->createNamedParameter(false, IQueryBuilder::PARAM_BOOL))) + ->andWhere($qb->expr()->eq('c.done', $qb->createNamedParameter(false, IQueryBuilder::PARAM_BOOL))) ->andWhere($qb->expr()->eq('c.deleted_at', $qb->createNamedParameter(0, IQueryBuilder::PARAM_INT))) ->andWhere($qb->expr()->eq('s.deleted_at', $qb->createNamedParameter(0, IQueryBuilder::PARAM_INT))) ->andWhere($qb->expr()->eq('b.archived', $qb->createNamedParameter(false, IQueryBuilder::PARAM_BOOL))) @@ -271,6 +273,7 @@ public function findOverdue() { ->where($qb->expr()->lt('duedate', $qb->createFunction('NOW()'))) ->andWhere($qb->expr()->eq('notified', $qb->createNamedParameter(false, IQueryBuilder::PARAM_BOOL))) ->andWhere($qb->expr()->eq('archived', $qb->createNamedParameter(false, IQueryBuilder::PARAM_BOOL))) + ->andWhere($qb->expr()->eq('done', $qb->createNamedParameter(false, IQueryBuilder::PARAM_BOOL))) ->andWhere($qb->expr()->eq('deleted_at', $qb->createNamedParameter(0, IQueryBuilder::PARAM_INT))); return $this->findEntities($qb); } diff --git a/lib/Migration/Version1000Date20200306161713.php b/lib/Migration/Version1000Date20200306161713.php index 62693a0fd..c68aeeb27 100644 --- a/lib/Migration/Version1000Date20200306161713.php +++ b/lib/Migration/Version1000Date20200306161713.php @@ -134,6 +134,10 @@ public function changeSchema(IOutput $output, Closure $schemaClosure, array $opt 'notnull' => false, 'length' => 8, ]); + $table->addColumn('done', 'boolean', [ + 'notnull' => false, + 'default' => false, + ]); $table->addColumn('archived', 'boolean', [ 'notnull' => false, 'default' => false, diff --git a/lib/Service/CardService.php b/lib/Service/CardService.php index 8b5f9a652..7b75a503a 100644 --- a/lib/Service/CardService.php +++ b/lib/Service/CardService.php @@ -1,4 +1,5 @@ * @@ -271,6 +272,9 @@ public function delete($id) { * @param $description * @param $order * @param $duedate + * @param $deletedAt + * @param $archived + * @param $done * @return \OCP\AppFramework\Db\Entity * @throws StatusException * @throws \OCA\Deck\NoPermissionException @@ -278,7 +282,8 @@ public function delete($id) { * @throws \OCP\AppFramework\Db\MultipleObjectsReturnedException * @throws BadRequestException */ - public function update($id, $title, $stackId, $type, $owner, $description = '', $order = 0, $duedate = null, $deletedAt = null, $archived = null) { + public function update($id, $title, $stackId, $type, $owner, $description = '', $order = 0, $duedate = null, $deletedAt = null, $archived = null, $done = null) + { if (is_numeric($id) === false) { throw new BadRequestException('card id must be a number'); } @@ -350,6 +355,9 @@ public function update($id, $title, $stackId, $type, $owner, $description = '', if ($archived !== null) { $card->setArchived($archived); } + if ($done !== null) { + $card->setDone($done); + } // Trigger update events before setting description as it is handled separately @@ -539,6 +547,67 @@ public function unarchive($id) { return $newCard; } + /** + * @param $id + * @return \OCP\AppFramework\Db\Entity + * @throws StatusException + * @throws \OCA\Deck\NoPermissionException + * @throws \OCP\AppFramework\Db\DoesNotExistException + * @throws \OCP\AppFramework\Db\ + * @throws BadRequestException + */ + public function done($id) + { + if (is_numeric($id) === false) { + throw new BadRequestException('id must be a number'); + } + + $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.'); + } + $card = $this->cardMapper->find($id); + $card->setDone(true); + $newCard = $this->cardMapper->update($card); + $this->notificationHelper->markDuedateAsRead($card); + $this->activityManager->triggerEvent(ActivityManager::DECK_OBJECT_CARD, $newCard, ActivityManager::SUBJECT_CARD_UPDATE_DONE); + $this->changeHelper->cardChanged($id, false); + + $this->eventDispatcher->dispatchTyped(new CardUpdatedEvent($card)); + + return $newCard; + } + + /** + * @param $id + * @return \OCP\AppFramework\Db\Entity + * @throws StatusException + * @throws \OCA\Deck\NoPermissionException + * @throws \OCP\AppFramework\Db\DoesNotExistException + * @throws \OCP\AppFramework\Db\MultipleObjectsReturnedException + * @throws BadRequestException + */ + public function undone($id) + { + if (is_numeric($id) === false) { + throw new BadRequestException('id must be a number'); + } + + $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.'); + } + $card = $this->cardMapper->find($id); + $card->setDone(false); + $newCard = $this->cardMapper->update($card); + $this->activityManager->triggerEvent(ActivityManager::DECK_OBJECT_CARD, $newCard, ActivityManager::SUBJECT_CARD_UPDATE_UNDONE); + $this->changeHelper->cardChanged($id, false); + + $this->eventDispatcher->dispatchTyped(new CardUpdatedEvent($card)); + + return $newCard; + } + /** * @param $cardId * @param $labelId diff --git a/src/components/card/CardSidebarTabDetails.vue b/src/components/card/CardSidebarTabDetails.vue index fe0624b8d..4f10af642 100644 --- a/src/components/card/CardSidebarTabDetails.vue +++ b/src/components/card/CardSidebarTabDetails.vue @@ -22,6 +22,19 @@