Merge pull request #4137 from TehThanos/master
This commit is contained in:
@@ -16,7 +16,7 @@
|
|||||||
- 🚀 Get your project organized
|
- 🚀 Get your project organized
|
||||||
|
|
||||||
</description>
|
</description>
|
||||||
<version>1.11.0-dev.1</version>
|
<version>1.11.0-dev.2</version>
|
||||||
<licence>agpl</licence>
|
<licence>agpl</licence>
|
||||||
<author>Julius Härtl</author>
|
<author>Julius Härtl</author>
|
||||||
<documentation>
|
<documentation>
|
||||||
|
|||||||
@@ -61,6 +61,8 @@ return [
|
|||||||
['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'],
|
||||||
['name' => 'card#unarchive', 'url' => '/cards/{cardId}/unarchive', '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#assignLabel', 'url' => '/cards/{cardId}/label/{labelId}', 'verb' => 'POST'],
|
||||||
['name' => 'card#removeLabel', 'url' => '/cards/{cardId}/label/{labelId}', 'verb' => 'DELETE'],
|
['name' => 'card#removeLabel', 'url' => '/cards/{cardId}/label/{labelId}', 'verb' => 'DELETE'],
|
||||||
['name' => 'card#assignUser', 'url' => '/cards/{cardId}/assign', 'verb' => 'POST'],
|
['name' => 'card#assignUser', 'url' => '/cards/{cardId}/assign', 'verb' => 'POST'],
|
||||||
|
|||||||
22
docs/API.md
22
docs/API.md
@@ -117,6 +117,7 @@ This API version has become available with **Deck 1.3.0**.
|
|||||||
- [GET /boards/import/getSystems - Import a board](#get-boardsimportgetsystems-import-a-board)
|
- [GET /boards/import/getSystems - Import a board](#get-boardsimportgetsystems-import-a-board)
|
||||||
- [GET /boards/import/config/system/{schema} - Import a board](#get-boardsimportconfigsystemschema-import-a-board)
|
- [GET /boards/import/config/system/{schema} - Import a board](#get-boardsimportconfigsystemschema-import-a-board)
|
||||||
- [POST /boards/import - Import a board](#post-boardsimport-import-a-board)
|
- [POST /boards/import - Import a board](#post-boardsimport-import-a-board)
|
||||||
|
- The `done` property was added to cards
|
||||||
|
|
||||||
# Endpoints
|
# Endpoints
|
||||||
|
|
||||||
@@ -601,6 +602,7 @@ The board list endpoint supports setting an `If-Modified-Since` header to limit
|
|||||||
"owner":"admin",
|
"owner":"admin",
|
||||||
"order":999,
|
"order":999,
|
||||||
"archived":false,
|
"archived":false,
|
||||||
|
"done":null,
|
||||||
"duedate": "2019-12-24T19:29:30+00:00",
|
"duedate": "2019-12-24T19:29:30+00:00",
|
||||||
"deletedAt":0,
|
"deletedAt":0,
|
||||||
"commentsUnread":0,
|
"commentsUnread":0,
|
||||||
@@ -623,13 +625,16 @@ The board list endpoint supports setting an `If-Modified-Since` header to limit
|
|||||||
|
|
||||||
#### Request data
|
#### Request data
|
||||||
|
|
||||||
| Parameter | Type | Description |
|
| Parameter | Type | Description |
|
||||||
|-------------|-----------|------------------------------------------------------|
|
|-------------|-----------------|-----------------------------------------------------------------------------------------------------|
|
||||||
| title | String | The title of the card, maximum length is limited to 255 characters |
|
| title | String | The title of the card, maximum length is limited to 255 characters |
|
||||||
| description | String | The markdown description of the card |
|
| description | String | The markdown description of the card |
|
||||||
| type | String | Type of the card (for later use) use 'plain' for now |
|
| type | String | Type of the card (for later use) use 'plain' for now |
|
||||||
| order | Integer | Order for sorting the stacks |
|
| owner | String | The user that owns the card |
|
||||||
| duedate | timestamp | The ISO-8601 formatted duedate of the card or null |
|
| order | Integer | Order for sorting the stacks |
|
||||||
|
| duedate | timestamp | The ISO-8601 formatted duedate of the card or null |
|
||||||
|
| archived | bool | Whether the card is archived or not |
|
||||||
|
| done | timestamp\|null | The ISO-8601 formatted date when the card is marked as done (optional, null indicates undone state) |
|
||||||
|
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -637,8 +642,11 @@ The board list endpoint supports setting an `If-Modified-Since` header to limit
|
|||||||
"title": "Test card",
|
"title": "Test card",
|
||||||
"description": "A card description",
|
"description": "A card description",
|
||||||
"type": "plain",
|
"type": "plain",
|
||||||
|
"owner": "admin",
|
||||||
"order": 999,
|
"order": 999,
|
||||||
"duedate": "2019-12-24T19:29:30+00:00",
|
"duedate": "2019-12-24T19:29:30+00:00",
|
||||||
|
"archived": false,
|
||||||
|
"done": null,
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
1. [Create my first board](#1-create-my-first-board)
|
||||||
2. [Create stacks and cards](#2-create-stacks-and-cards)
|
2. [Create stacks and cards](#2-create-stacks-and-cards)
|
||||||
3. [Handle cards options](#3-handle-cards-options)
|
3. [Handle cards options](#3-handle-cards-options)
|
||||||
4. [Archive old tasks](#4-archive-old-tasks)
|
4. [Mark task as done](#4-mark-as-done)
|
||||||
5. [Manage your board](#5-manage-your-board)
|
5. [Archive old tasks](#5-archive-old-tasks)
|
||||||
6. [Import boards](#6-import-boards)
|
6. [Manage your board](#6-manage-your-board)
|
||||||
7. [Search](#7-search)
|
7. [Import boards](#7-import-boards)
|
||||||
8. [New owner for the deck entities](#8-new-owner-for-the-deck-entities)
|
8. [Search](#8-search)
|
||||||
|
9. [New owner for the deck entities](#9-new-owner-for-the-deck-entities)
|
||||||
|
|
||||||
### 1. Create my first board
|
### 1. Create my first board
|
||||||
In this example, we're going to create a board and share it with an other nextcloud user.
|
In this example, we're going to create a board and share it with an other nextcloud user.
|
||||||
@@ -53,12 +54,18 @@ And even :
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
### 4. Archive old tasks
|
### 4. Mark as done
|
||||||
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 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.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
### 5. Manage your board
|
### 5. Archive old tasks
|
||||||
|
Once obsolete, a task could be archived. The task is not deleted, it's just archived, and you can retrieve it later
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### 6. 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.
|
You can manage the settings of your Deck once you are inside it, by clicking on the small wheel at the top right.
|
||||||
Once in this menu, you have access to several things:
|
Once in this menu, you have access to several things:
|
||||||
|
|
||||||
@@ -72,7 +79,7 @@ The **sharing tab** allows you to add users or even groups to your boards.
|
|||||||
**Deleted objects** allows you to return previously deleted stacks or cards.
|
**Deleted objects** allows you to return previously deleted stacks or cards.
|
||||||
The **Timeline** allows you to see everything that happened in your boards. Everything!
|
The **Timeline** allows you to see everything that happened in your boards. Everything!
|
||||||
|
|
||||||
### 6. Import boards
|
### 7. Import boards
|
||||||
|
|
||||||
Importing can be done using the API or the `occ` `deck:import` command.
|
Importing can be done using the API or the `occ` `deck:import` command.
|
||||||
|
|
||||||
@@ -138,7 +145,7 @@ Example configuration file:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### 7. Search
|
### 8. Search
|
||||||
|
|
||||||
Deck provides a global search either through the unified search in the Nextcloud header or with the inline search next to the board controls.
|
Deck provides a global search either through the unified search in the Nextcloud header or with the inline search next to the board controls.
|
||||||
This search allows advanced filtering of cards across all board of the logged in user.
|
This search allows advanced filtering of cards across all board of the logged in user.
|
||||||
@@ -161,7 +168,7 @@ Other text tokens will be used to perform a case-insensitive search on the card
|
|||||||
|
|
||||||
In addition, quotes can be used to pass a query with spaces, e.g. `"Exact match with spaces"` or `title:"My card"`.
|
In addition, quotes can be used to pass a query with spaces, e.g. `"Exact match with spaces"` or `title:"My card"`.
|
||||||
|
|
||||||
### 8. New owner for the deck entities
|
### 9. New owner for the deck entities
|
||||||
You can transfer ownership of boards, cards, etc to a new user, using `occ` command `deck:transfer-ownership`
|
You can transfer ownership of boards, cards, etc to a new user, using `occ` command `deck:transfer-ownership`
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
BIN
docs/resources/gifs/EN_done.gif
Normal file
BIN
docs/resources/gifs/EN_done.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 88 KiB |
@@ -91,6 +91,8 @@ class ActivityManager {
|
|||||||
public const SUBJECT_CARD_UPDATE_DUEDATE = 'card_update_duedate';
|
public const SUBJECT_CARD_UPDATE_DUEDATE = 'card_update_duedate';
|
||||||
public const SUBJECT_CARD_UPDATE_ARCHIVE = 'card_update_archive';
|
public const SUBJECT_CARD_UPDATE_ARCHIVE = 'card_update_archive';
|
||||||
public const SUBJECT_CARD_UPDATE_UNARCHIVE = 'card_update_unarchive';
|
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_UPDATE_STACKID = 'card_update_stackId';
|
||||||
public const SUBJECT_CARD_USER_ASSIGN = 'card_user_assign';
|
public const SUBJECT_CARD_USER_ASSIGN = 'card_user_assign';
|
||||||
public const SUBJECT_CARD_USER_UNASSIGN = 'card_user_unassign';
|
public const SUBJECT_CARD_USER_UNASSIGN = 'card_user_unassign';
|
||||||
@@ -198,6 +200,12 @@ class ActivityManager {
|
|||||||
case self::SUBJECT_CARD_UPDATE_UNARCHIVE:
|
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}');
|
$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;
|
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:
|
case self::SUBJECT_CARD_UPDATE_DUEDATE:
|
||||||
if (!isset($subjectParams['after'])) {
|
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}');
|
$subject = $ownActivity ? $l->t('You have removed the due date of card {card}') : $l->t('{user} has removed the due date of card {card}');
|
||||||
@@ -357,6 +365,8 @@ class ActivityManager {
|
|||||||
case self::SUBJECT_CARD_DELETE:
|
case self::SUBJECT_CARD_DELETE:
|
||||||
case self::SUBJECT_CARD_UPDATE_ARCHIVE:
|
case self::SUBJECT_CARD_UPDATE_ARCHIVE:
|
||||||
case self::SUBJECT_CARD_UPDATE_UNARCHIVE:
|
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_TITLE:
|
||||||
case self::SUBJECT_CARD_UPDATE_DESCRIPTION:
|
case self::SUBJECT_CARD_UPDATE_DESCRIPTION:
|
||||||
case self::SUBJECT_CARD_UPDATE_DUEDATE:
|
case self::SUBJECT_CARD_UPDATE_DUEDATE:
|
||||||
|
|||||||
@@ -25,6 +25,7 @@
|
|||||||
|
|
||||||
namespace OCA\Deck\Controller;
|
namespace OCA\Deck\Controller;
|
||||||
|
|
||||||
|
use OCA\Deck\Model\OptionalNullableValue;
|
||||||
use OCA\Deck\Service\AssignmentService;
|
use OCA\Deck\Service\AssignmentService;
|
||||||
use OCA\Deck\Service\CardService;
|
use OCA\Deck\Service\CardService;
|
||||||
use OCP\AppFramework\ApiController;
|
use OCP\AppFramework\ApiController;
|
||||||
@@ -105,7 +106,8 @@ class CardApiController extends ApiController {
|
|||||||
* Update a card
|
* Update a card
|
||||||
*/
|
*/
|
||||||
public function update($title, $type, $owner, $description = '', $order = 0, $duedate = null, $archived = null) {
|
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);
|
$done = array_key_exists('done', $this->request->getParams()) ? new OptionalNullableValue($this->request->getParam('done', null)) : 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);
|
return new DataResponse($card, HTTP::STATUS_OK);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -143,6 +143,24 @@ class CardController extends Controller {
|
|||||||
return $this->cardService->unarchive($cardId);
|
return $this->cardService->unarchive($cardId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @NoAdminRequired
|
||||||
|
* @param $cardId
|
||||||
|
* @return \OCP\AppFramework\Db\Entity
|
||||||
|
*/
|
||||||
|
public function done(int $cardId) {
|
||||||
|
return $this->cardService->done($cardId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @NoAdminRequired
|
||||||
|
* @param $cardId
|
||||||
|
* @return \OCP\AppFramework\Db\Entity
|
||||||
|
*/
|
||||||
|
public function undone(int $cardId) {
|
||||||
|
return $this->cardService->undone($cardId);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @NoAdminRequired
|
* @NoAdminRequired
|
||||||
* @param $cardId
|
* @param $cardId
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @copyright Copyright (c) 2016 Julius Härtl <jus@bitgrid.net>
|
* @copyright Copyright (c) 2016 Julius Härtl <jus@bitgrid.net>
|
||||||
*
|
*
|
||||||
@@ -37,6 +40,8 @@ use Sabre\VObject\Component\VCalendar;
|
|||||||
* @method int getCreatedAt()
|
* @method int getCreatedAt()
|
||||||
* @method bool getArchived()
|
* @method bool getArchived()
|
||||||
* @method bool getNotified()
|
* @method bool getNotified()
|
||||||
|
* @method ?DateTime getDone()
|
||||||
|
* @method void setDone(?DateTime $done)
|
||||||
*
|
*
|
||||||
* @method void setLabels(Label[] $labels)
|
* @method void setLabels(Label[] $labels)
|
||||||
* @method null|Label[] getLabels()
|
* @method null|Label[] getLabels()
|
||||||
@@ -83,6 +88,7 @@ class Card extends RelationalEntity {
|
|||||||
protected $owner;
|
protected $owner;
|
||||||
protected $order;
|
protected $order;
|
||||||
protected $archived = false;
|
protected $archived = false;
|
||||||
|
protected $done = null;
|
||||||
protected $duedate;
|
protected $duedate;
|
||||||
protected $notified = false;
|
protected $notified = false;
|
||||||
protected $deletedAt = 0;
|
protected $deletedAt = 0;
|
||||||
@@ -106,6 +112,7 @@ class Card extends RelationalEntity {
|
|||||||
$this->addType('lastModified', 'integer');
|
$this->addType('lastModified', 'integer');
|
||||||
$this->addType('createdAt', 'integer');
|
$this->addType('createdAt', 'integer');
|
||||||
$this->addType('archived', 'boolean');
|
$this->addType('archived', 'boolean');
|
||||||
|
$this->addType('done', 'datetime');
|
||||||
$this->addType('notified', 'boolean');
|
$this->addType('notified', 'boolean');
|
||||||
$this->addType('deletedAt', 'integer');
|
$this->addType('deletedAt', 'integer');
|
||||||
$this->addType('duedate', 'datetime');
|
$this->addType('duedate', 'datetime');
|
||||||
@@ -139,19 +146,22 @@ class Card extends RelationalEntity {
|
|||||||
$event->add('RELATED-TO', 'deck-stack-' . $this->getStackId());
|
$event->add('RELATED-TO', 'deck-stack-' . $this->getStackId());
|
||||||
|
|
||||||
// FIXME: For write support: CANCELLED / IN-PROCESS handling
|
// FIXME: For write support: CANCELLED / IN-PROCESS handling
|
||||||
$event->STATUS = $this->getArchived() ? "COMPLETED" : "NEEDS-ACTION";
|
if ($this->getDone() || $this->getArchived()) {
|
||||||
if ($this->getArchived()) {
|
|
||||||
$date = new DateTime();
|
$date = new DateTime();
|
||||||
$date->setTimestamp($this->getLastModified());
|
$date->setTimestamp($this->getLastModified());
|
||||||
$event->COMPLETED = $date;
|
$event->STATUS = 'COMPLETED';
|
||||||
//$event->add('PERCENT-COMPLETE', 100);
|
$event->COMPLETED = $this->getDone() ? $this->$this->getDone() : $this->getArchived();
|
||||||
}
|
} else {
|
||||||
if (count($this->getLabels()) > 0) {
|
$event->STATUS = 'NEEDS-ACTION';
|
||||||
$event->CATEGORIES = array_map(function ($label) {
|
|
||||||
return $label->getTitle();
|
|
||||||
}, $this->getLabels());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// $event->add('PERCENT-COMPLETE', 100);
|
||||||
|
|
||||||
|
$labels = $this->getLabels() ?? [];
|
||||||
|
$event->CATEGORIES = array_map(function ($label): string {
|
||||||
|
return $label->getTitle();
|
||||||
|
}, $labels);
|
||||||
|
|
||||||
$event->SUMMARY = $this->getTitle();
|
$event->SUMMARY = $this->getTitle();
|
||||||
$event->DESCRIPTION = $this->getDescription();
|
$event->DESCRIPTION = $this->getDescription();
|
||||||
$calendar->add($event);
|
$calendar->add($event);
|
||||||
@@ -177,7 +187,7 @@ class Card extends RelationalEntity {
|
|||||||
return 'card';
|
return 'card';
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getETag() {
|
public function getETag(): string {
|
||||||
return md5((string)$this->getLastModified());
|
return md5((string)$this->getLastModified());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -263,6 +263,7 @@ class CardMapper extends QBMapper implements IPermissionMapper {
|
|||||||
->where($qb->expr()->in('s.board_id', $qb->createNamedParameter($boardIds, IQueryBuilder::PARAM_INT_ARRAY)))
|
->where($qb->expr()->in('s.board_id', $qb->createNamedParameter($boardIds, IQueryBuilder::PARAM_INT_ARRAY)))
|
||||||
->andWhere($qb->expr()->isNotNull('c.duedate'))
|
->andWhere($qb->expr()->isNotNull('c.duedate'))
|
||||||
->andWhere($qb->expr()->eq('c.archived', $qb->createNamedParameter(false, IQueryBuilder::PARAM_BOOL)))
|
->andWhere($qb->expr()->eq('c.archived', $qb->createNamedParameter(false, IQueryBuilder::PARAM_BOOL)))
|
||||||
|
->andWhere($qb->expr()->isNull('done'))
|
||||||
->andWhere($qb->expr()->eq('c.deleted_at', $qb->createNamedParameter(0, IQueryBuilder::PARAM_INT)))
|
->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('s.deleted_at', $qb->createNamedParameter(0, IQueryBuilder::PARAM_INT)))
|
||||||
->andWhere($qb->expr()->eq('b.archived', $qb->createNamedParameter(false, IQueryBuilder::PARAM_BOOL)))
|
->andWhere($qb->expr()->eq('b.archived', $qb->createNamedParameter(false, IQueryBuilder::PARAM_BOOL)))
|
||||||
@@ -284,6 +285,7 @@ class CardMapper extends QBMapper implements IPermissionMapper {
|
|||||||
)
|
)
|
||||||
// Filter out archived/deleted cards and board
|
// Filter out archived/deleted cards and board
|
||||||
->andWhere($qb->expr()->eq('c.archived', $qb->createNamedParameter(false, IQueryBuilder::PARAM_BOOL)))
|
->andWhere($qb->expr()->eq('c.archived', $qb->createNamedParameter(false, IQueryBuilder::PARAM_BOOL)))
|
||||||
|
->andWhere($qb->expr()->isNull('done'))
|
||||||
->andWhere($qb->expr()->eq('c.deleted_at', $qb->createNamedParameter(0, IQueryBuilder::PARAM_INT)))
|
->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('s.deleted_at', $qb->createNamedParameter(0, IQueryBuilder::PARAM_INT)))
|
||||||
->andWhere($qb->expr()->eq('b.archived', $qb->createNamedParameter(false, IQueryBuilder::PARAM_BOOL)))
|
->andWhere($qb->expr()->eq('b.archived', $qb->createNamedParameter(false, IQueryBuilder::PARAM_BOOL)))
|
||||||
@@ -298,6 +300,7 @@ class CardMapper extends QBMapper implements IPermissionMapper {
|
|||||||
->where($qb->expr()->lt('duedate', $qb->createFunction('NOW()')))
|
->where($qb->expr()->lt('duedate', $qb->createFunction('NOW()')))
|
||||||
->andWhere($qb->expr()->eq('notified', $qb->createNamedParameter(false, IQueryBuilder::PARAM_BOOL)))
|
->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('archived', $qb->createNamedParameter(false, IQueryBuilder::PARAM_BOOL)))
|
||||||
|
->andWhere($qb->expr()->isNull('done'))
|
||||||
->andWhere($qb->expr()->eq('deleted_at', $qb->createNamedParameter(0, IQueryBuilder::PARAM_INT)));
|
->andWhere($qb->expr()->eq('deleted_at', $qb->createNamedParameter(0, IQueryBuilder::PARAM_INT)));
|
||||||
return $this->findEntities($qb);
|
return $this->findEntities($qb);
|
||||||
}
|
}
|
||||||
|
|||||||
61
lib/Migration/Version1011Date20230901010840.php
Normal file
61
lib/Migration/Version1011Date20230901010840.php
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @copyright Copyright (c) 2023 Thanos kamber <thanos.kamber@gmail.com>
|
||||||
|
*
|
||||||
|
* @author Thanos kamber <thanos.kamber@gmail.com>
|
||||||
|
*
|
||||||
|
* @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\Migration;
|
||||||
|
|
||||||
|
use Closure;
|
||||||
|
use OCP\DB\ISchemaWrapper;
|
||||||
|
use OCP\DB\Types;
|
||||||
|
use OCP\Migration\IOutput;
|
||||||
|
use OCP\Migration\SimpleMigrationStep;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-generated migration step: Please modify to your needs!
|
||||||
|
*/
|
||||||
|
class Version1011Date20230901010840 extends SimpleMigrationStep {
|
||||||
|
/**
|
||||||
|
* @param IOutput $output
|
||||||
|
* @param Closure $schemaClosure
|
||||||
|
* @param array $options
|
||||||
|
* @return null|ISchemaWrapper
|
||||||
|
*/
|
||||||
|
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper {
|
||||||
|
/** @var ISchemaWrapper $schema */
|
||||||
|
$schema = $schemaClosure();
|
||||||
|
|
||||||
|
$table = $schema->getTable('deck_cards');
|
||||||
|
if (!$table->hasColumn('done')) {
|
||||||
|
$table->addColumn('done', Types::DATETIME, [
|
||||||
|
'default' => null,
|
||||||
|
'notnull' => false,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $schema;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
51
lib/Model/OptionalNullableValue.php
Normal file
51
lib/Model/OptionalNullableValue.php
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* @copyright Copyright (c) 2023 Julius Härtl <jus@bitgrid.net>
|
||||||
|
*
|
||||||
|
* @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\Model;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is a helper abstraction to allow usage of optional parameters
|
||||||
|
* which hold a nullable value. The actual null value of the parameter
|
||||||
|
* is used to indicate if it has been set or not. The containing value
|
||||||
|
* will then still allow having null as a value
|
||||||
|
*
|
||||||
|
* Example use case: Have a nullable database column,
|
||||||
|
* but only update it if it is passed
|
||||||
|
*
|
||||||
|
* @template T
|
||||||
|
*/
|
||||||
|
class OptionalNullableValue {
|
||||||
|
|
||||||
|
/** @var ?T */
|
||||||
|
private mixed $value;
|
||||||
|
|
||||||
|
/** @param ?T $value */
|
||||||
|
public function __construct(mixed $value) {
|
||||||
|
$this->value = $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return ?T */
|
||||||
|
public function getValue(): mixed {
|
||||||
|
return $this->value;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -43,6 +43,7 @@ use OCA\Deck\Event\CardCreatedEvent;
|
|||||||
use OCA\Deck\Event\CardDeletedEvent;
|
use OCA\Deck\Event\CardDeletedEvent;
|
||||||
use OCA\Deck\Event\CardUpdatedEvent;
|
use OCA\Deck\Event\CardUpdatedEvent;
|
||||||
use OCA\Deck\Model\CardDetails;
|
use OCA\Deck\Model\CardDetails;
|
||||||
|
use OCA\Deck\Model\OptionalNullableValue;
|
||||||
use OCA\Deck\NoPermissionException;
|
use OCA\Deck\NoPermissionException;
|
||||||
use OCA\Deck\Notification\NotificationHelper;
|
use OCA\Deck\Notification\NotificationHelper;
|
||||||
use OCA\Deck\StatusException;
|
use OCA\Deck\StatusException;
|
||||||
@@ -284,6 +285,9 @@ class CardService {
|
|||||||
* @param $description
|
* @param $description
|
||||||
* @param $order
|
* @param $order
|
||||||
* @param $duedate
|
* @param $duedate
|
||||||
|
* @param $deletedAt
|
||||||
|
* @param $archived
|
||||||
|
* @param $done
|
||||||
* @return \OCP\AppFramework\Db\Entity
|
* @return \OCP\AppFramework\Db\Entity
|
||||||
* @throws StatusException
|
* @throws StatusException
|
||||||
* @throws \OCA\Deck\NoPermissionException
|
* @throws \OCA\Deck\NoPermissionException
|
||||||
@@ -291,7 +295,7 @@ class CardService {
|
|||||||
* @throws \OCP\AppFramework\Db\MultipleObjectsReturnedException
|
* @throws \OCP\AppFramework\Db\MultipleObjectsReturnedException
|
||||||
* @throws BadRequestException
|
* @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, ?OptionalNullableValue $done = null) {
|
||||||
$this->cardServiceValidator->check(compact('id', 'title', 'stackId', 'type', 'owner', 'order'));
|
$this->cardServiceValidator->check(compact('id', 'title', 'stackId', 'type', 'owner', 'order'));
|
||||||
|
|
||||||
$this->permissionService->checkPermission($this->cardMapper, $id, Acl::PERMISSION_EDIT);
|
$this->permissionService->checkPermission($this->cardMapper, $id, Acl::PERMISSION_EDIT);
|
||||||
@@ -341,6 +345,9 @@ class CardService {
|
|||||||
if ($archived !== null) {
|
if ($archived !== null) {
|
||||||
$card->setArchived($archived);
|
$card->setArchived($archived);
|
||||||
}
|
}
|
||||||
|
if ($done !== null) {
|
||||||
|
$card->setDone($done->getValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// Trigger update events before setting description as it is handled separately
|
// Trigger update events before setting description as it is handled separately
|
||||||
@@ -511,6 +518,57 @@ class CardService {
|
|||||||
return $newCard;
|
return $newCard;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param $id
|
||||||
|
* @return \OCA\Deck\Db\Card
|
||||||
|
* @throws StatusException
|
||||||
|
* @throws \OCA\Deck\NoPermissionException
|
||||||
|
* @throws \OCP\AppFramework\Db\DoesNotExistException
|
||||||
|
* @throws \OCP\AppFramework\Db\MultipleObjectsReturnedException
|
||||||
|
* @throws BadRequestException
|
||||||
|
*/
|
||||||
|
public function done(int $id): Card {
|
||||||
|
$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(new \DateTime());
|
||||||
|
$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 \OCA\Deck\Db\Card
|
||||||
|
* @throws StatusException
|
||||||
|
* @throws \OCA\Deck\NoPermissionException
|
||||||
|
* @throws \OCP\AppFramework\Db\DoesNotExistException
|
||||||
|
* @throws \OCP\AppFramework\Db\MultipleObjectsReturnedException
|
||||||
|
* @throws BadRequestException
|
||||||
|
*/
|
||||||
|
public function undone(int $id): Card {
|
||||||
|
$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(null);
|
||||||
|
$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 $cardId
|
||||||
* @param $labelId
|
* @param $labelId
|
||||||
|
|||||||
27
src/components/card/CardDetailEntry.vue
Normal file
27
src/components/card/CardDetailEntry.vue
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
<template>
|
||||||
|
<div class="selector-wrapper" :aria-label="label">
|
||||||
|
<div class="selector-wrapper--icon">
|
||||||
|
<slot name="icon" />
|
||||||
|
</div>
|
||||||
|
<div class="selector-wrapper--content">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { defineComponent } from 'vue'
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'CardDetailEntry',
|
||||||
|
props: {
|
||||||
|
label: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
<style lang="scss">
|
||||||
|
@import '../../css/selector';
|
||||||
|
</style>
|
||||||
@@ -35,7 +35,9 @@
|
|||||||
@select="assignUserToCard"
|
@select="assignUserToCard"
|
||||||
@remove="removeUserFromCard" />
|
@remove="removeUserFromCard" />
|
||||||
|
|
||||||
<DueDateSelector :card="card" :can-edit="canEdit" @change="updateCardDue" />
|
<DueDateSelector :card="card"
|
||||||
|
:can-edit="canEdit"
|
||||||
|
@change="updateCardDue" />
|
||||||
|
|
||||||
<div v-if="projectsEnabled" class="section-wrapper">
|
<div v-if="projectsEnabled" class="section-wrapper">
|
||||||
<CollectionList v-if="card.id"
|
<CollectionList v-if="card.id"
|
||||||
@@ -218,12 +220,36 @@ export default {
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
|
||||||
button.action-item--single {
|
.remove-due-button{
|
||||||
margin-top: -3px;
|
margin-top: -2px;
|
||||||
|
margin-left: 6px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.button-group {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
button {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.done {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag {
|
||||||
|
flex-grow: 0;
|
||||||
|
flex-shrink: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 0px 5px;
|
||||||
|
border-radius: 15px;
|
||||||
|
font-size: 85%;
|
||||||
|
margin-right: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
.avatarLabel {
|
.avatarLabel {
|
||||||
padding: 6px
|
padding: 6px
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="selector-wrapper" :aria-label="t('deck', 'Assign a due date to this card…')">
|
<CardDetailEntry :label="t('deck', 'Assign a due date to this card…')">
|
||||||
<div class="selector-wrapper--icon">
|
<Calendar v-if="!card.done" slot="icon" :size="20" />
|
||||||
<Calendar :size="20" />
|
<CalendarCheck v-else slot="icon" :size="20" />
|
||||||
</div>
|
<template v-if="!card.done && !card.archived">
|
||||||
<div class="duedate-selector">
|
|
||||||
<NcDateTimePickerNative v-if="duedate"
|
<NcDateTimePickerNative v-if="duedate"
|
||||||
id="card-duedate-picker"
|
id="card-duedate-picker"
|
||||||
v-model="duedate"
|
v-model="duedate"
|
||||||
@@ -43,28 +42,86 @@
|
|||||||
{{ t('deck', 'Remove due date') }}
|
{{ t('deck', 'Remove due date') }}
|
||||||
</NcActionButton>
|
</NcActionButton>
|
||||||
</NcActions>
|
</NcActions>
|
||||||
</div>
|
|
||||||
</div>
|
<NcButton v-if="!card.done"
|
||||||
|
type="secondary"
|
||||||
|
class="completed-button"
|
||||||
|
@click="changeCardDoneStatus()">
|
||||||
|
<template #icon>
|
||||||
|
<CheckIcon :size="20" />
|
||||||
|
</template>
|
||||||
|
{{ t('deck', 'Completed') }}
|
||||||
|
</NcButton>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<div class="done-info">
|
||||||
|
<span v-if="card.done" class="done-info--done">
|
||||||
|
{{ formatReadableDate(card.done) }}
|
||||||
|
</span>
|
||||||
|
<span v-if="duedate" class="done-info--duedate" :class="{ 'dimmed': card.done }">
|
||||||
|
{{ t('deck', 'Due at:') }}
|
||||||
|
{{ formatReadableDate(duedate) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="due-actions">
|
||||||
|
<NcButton v-if="!card.archived"
|
||||||
|
type="tertiary"
|
||||||
|
:title="t('deck', 'Not completed')"
|
||||||
|
@click="changeCardDoneStatus()">
|
||||||
|
<template #icon>
|
||||||
|
<ClearIcon :size="20" />
|
||||||
|
</template>
|
||||||
|
</NcButton>
|
||||||
|
<NcButton type="secondary" @click="archiveUnarchiveCard()">
|
||||||
|
<template #icon>
|
||||||
|
<ArchiveIcon :size="20" />
|
||||||
|
</template>
|
||||||
|
{{ card.archived ? t('deck', 'Unarchive card') : t('deck', 'Archive card') }}
|
||||||
|
</NcButton>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</CardDetailEntry>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { defineComponent } from 'vue'
|
import { defineComponent } from 'vue'
|
||||||
import { NcActionButton, NcActions, NcActionSeparator, NcDateTimePickerNative } from '@nextcloud/vue'
|
import {
|
||||||
|
NcActionButton,
|
||||||
|
NcActions,
|
||||||
|
NcActionSeparator,
|
||||||
|
NcButton,
|
||||||
|
NcDateTimePickerNative,
|
||||||
|
} from '@nextcloud/vue'
|
||||||
|
import readableDate from '../../mixins/readableDate.js'
|
||||||
import { getDayNamesMin, getFirstDay, getMonthNamesShort } from '@nextcloud/l10n'
|
import { getDayNamesMin, getFirstDay, getMonthNamesShort } from '@nextcloud/l10n'
|
||||||
|
import moment from '@nextcloud/moment'
|
||||||
|
import ArchiveIcon from 'vue-material-design-icons/Archive.vue'
|
||||||
import Plus from 'vue-material-design-icons/Plus.vue'
|
import Plus from 'vue-material-design-icons/Plus.vue'
|
||||||
import Calendar from 'vue-material-design-icons/Calendar.vue'
|
import Calendar from 'vue-material-design-icons/Calendar.vue'
|
||||||
import moment from '@nextcloud/moment'
|
import CalendarCheck from 'vue-material-design-icons/CalendarCheck.vue'
|
||||||
|
import CheckIcon from 'vue-material-design-icons/Check.vue'
|
||||||
|
import ClearIcon from 'vue-material-design-icons/Close.vue'
|
||||||
|
import CardDetailEntry from './CardDetailEntry.vue'
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'DueDateSelector',
|
name: 'DueDateSelector',
|
||||||
components: {
|
components: {
|
||||||
|
NcButton,
|
||||||
|
ArchiveIcon,
|
||||||
|
ClearIcon,
|
||||||
|
CardDetailEntry,
|
||||||
Plus,
|
Plus,
|
||||||
Calendar,
|
Calendar,
|
||||||
|
CalendarCheck,
|
||||||
|
CheckIcon,
|
||||||
NcActions,
|
NcActions,
|
||||||
NcActionButton,
|
NcActionButton,
|
||||||
NcActionSeparator,
|
NcActionSeparator,
|
||||||
NcDateTimePickerNative,
|
NcDateTimePickerNative,
|
||||||
},
|
},
|
||||||
|
mixins: [
|
||||||
|
readableDate,
|
||||||
|
],
|
||||||
props: {
|
props: {
|
||||||
card: {
|
card: {
|
||||||
type: Object,
|
type: Object,
|
||||||
@@ -166,13 +223,35 @@ export default defineComponent({
|
|||||||
getTimestamp(momentObject) {
|
getTimestamp(momentObject) {
|
||||||
return momentObject?.minute(0).second(0).millisecond(0).toDate() || null
|
return momentObject?.minute(0).second(0).millisecond(0).toDate() || null
|
||||||
},
|
},
|
||||||
|
changeCardDoneStatus() {
|
||||||
|
this.$store.dispatch('changeCardDoneStatus', { ...this.card, done: !this.card.done })
|
||||||
|
},
|
||||||
|
archiveUnarchiveCard() {
|
||||||
|
this.$store.dispatch('archiveUnarchiveCard', { ...this.card, archived: !this.card.archived })
|
||||||
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss">
|
<style scoped lang="scss">
|
||||||
@import '../../css/selector';
|
.done-info {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
.duedate-selector {
|
.done-info--duedate,
|
||||||
|
.done-info--done {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
padding-top: 10px;
|
||||||
|
&.dimmed {
|
||||||
|
color: var(--color-text-maxcontrast);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.completed-button {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.due-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -64,7 +64,8 @@
|
|||||||
<input type="submit" value="" class="icon-confirm">
|
<input type="submit" value="" class="icon-confirm">
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<DueDate v-if="!editing" :card="card" />
|
<DueDate v-if="!editing && !card.done" :card="card" />
|
||||||
|
<Done v-else-if="!editing && card.done" :card="card" />
|
||||||
|
|
||||||
<CardMenu v-if="!editing && compactMode" :card="card" class="right" />
|
<CardMenu v-if="!editing && compactMode" :card="card" class="right" />
|
||||||
</div>
|
</div>
|
||||||
@@ -94,12 +95,13 @@ import Color from '../../mixins/color.js'
|
|||||||
import labelStyle from '../../mixins/labelStyle.js'
|
import labelStyle from '../../mixins/labelStyle.js'
|
||||||
import AttachmentDragAndDrop from '../AttachmentDragAndDrop.vue'
|
import AttachmentDragAndDrop from '../AttachmentDragAndDrop.vue'
|
||||||
import CardMenu from './CardMenu.vue'
|
import CardMenu from './CardMenu.vue'
|
||||||
|
import Done from './badges/Done.vue'
|
||||||
import DueDate from './badges/DueDate.vue'
|
import DueDate from './badges/DueDate.vue'
|
||||||
import CardCover from './CardCover.vue'
|
import CardCover from './CardCover.vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'CardItem',
|
name: 'CardItem',
|
||||||
components: { CardBadges, AttachmentDragAndDrop, CardMenu, DueDate, CardCover },
|
components: { CardBadges, AttachmentDragAndDrop, CardMenu, DueDate, CardCover, Done },
|
||||||
directives: {
|
directives: {
|
||||||
ClickOutside,
|
ClickOutside,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -36,6 +36,9 @@
|
|||||||
@click="unassignCardFromMe()">
|
@click="unassignCardFromMe()">
|
||||||
{{ t('deck', 'Unassign myself') }}
|
{{ t('deck', 'Unassign myself') }}
|
||||||
</NcActionButton>
|
</NcActionButton>
|
||||||
|
<NcActionButton icon="icon-checkmark" :close-after-click="true" @click="changeCardDoneStatus()">
|
||||||
|
{{ card.done ? t('deck', 'Mark as not done') : t('deck', 'Mark as done') }}
|
||||||
|
</NcActionButton>
|
||||||
<NcActionButton icon="icon-external" :close-after-click="true" @click="modalShow=true">
|
<NcActionButton icon="icon-external" :close-after-click="true" @click="modalShow=true">
|
||||||
{{ t('deck', 'Move card') }}
|
{{ t('deck', 'Move card') }}
|
||||||
</NcActionButton>
|
</NcActionButton>
|
||||||
@@ -157,6 +160,9 @@ export default {
|
|||||||
this.$store.dispatch('deleteCard', this.card)
|
this.$store.dispatch('deleteCard', this.card)
|
||||||
showUndo(t('deck', 'Card deleted'), () => this.$store.dispatch('cardUndoDelete', this.card))
|
showUndo(t('deck', 'Card deleted'), () => this.$store.dispatch('cardUndoDelete', this.card))
|
||||||
},
|
},
|
||||||
|
changeCardDoneStatus() {
|
||||||
|
this.$store.dispatch('changeCardDoneStatus', { ...this.card, done: !this.card.done })
|
||||||
|
},
|
||||||
archiveUnarchiveCard() {
|
archiveUnarchiveCard() {
|
||||||
this.$store.dispatch('archiveUnarchiveCard', { ...this.card, archived: !this.card.archived })
|
this.$store.dispatch('archiveUnarchiveCard', { ...this.card, archived: !this.card.archived })
|
||||||
},
|
},
|
||||||
|
|||||||
72
src/components/cards/badges/Done.vue
Normal file
72
src/components/cards/badges/Done.vue
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
<!--
|
||||||
|
- @copyright Copyright (c) 2022 Thanos Kamber <thanos.kamber@gmail.com>
|
||||||
|
-
|
||||||
|
- @author Thanos Kamber <thanos.kamber@gmail.com>
|
||||||
|
-
|
||||||
|
- @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/>.
|
||||||
|
-
|
||||||
|
-->
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div v-if="card" class="done">
|
||||||
|
<transition name="zoom">
|
||||||
|
<div class="icon-check-circle">
|
||||||
|
<CheckCircle :size="20" :title="formatReadableDate(card.done)" />
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import CheckCircle from 'vue-material-design-icons/CheckCircle.vue'
|
||||||
|
import readableDate from '../../../mixins/readableDate.js'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'Done',
|
||||||
|
components: {
|
||||||
|
CheckCircle,
|
||||||
|
},
|
||||||
|
mixins: [
|
||||||
|
readableDate,
|
||||||
|
],
|
||||||
|
props: {
|
||||||
|
card: {
|
||||||
|
type: Object,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.icon-check-circle {
|
||||||
|
color: var(--color-success);
|
||||||
|
margin: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media print {
|
||||||
|
.icon-check-circle {
|
||||||
|
span {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
color: var(--color-text-lighter);
|
||||||
|
content: 'Done';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -33,4 +33,9 @@
|
|||||||
&--selector {
|
&--selector {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&--content {
|
||||||
|
display: flex;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
33
src/mixins/readableDate.js
Normal file
33
src/mixins/readableDate.js
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
/*
|
||||||
|
* @copyright Copyright (c) 2020 Julius Härtl <jus@bitgrid.net>
|
||||||
|
*
|
||||||
|
* @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 moment from '@nextcloud/moment'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
computed: {
|
||||||
|
formatReadableDate() {
|
||||||
|
return (timestamp) => {
|
||||||
|
return moment(timestamp).format('lll')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -165,6 +165,36 @@ export class CardApi {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
markCardAsDone(card) {
|
||||||
|
return axios.put(this.url(`/cards/${card.id}/done`))
|
||||||
|
.then(
|
||||||
|
(response) => {
|
||||||
|
return Promise.resolve(response.data)
|
||||||
|
},
|
||||||
|
(err) => {
|
||||||
|
return Promise.reject(err)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.catch((err) => {
|
||||||
|
return Promise.reject(err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
markCardAsUndone(card) {
|
||||||
|
return axios.put(this.url(`/cards/${card.id}/undone`))
|
||||||
|
.then(
|
||||||
|
(response) => {
|
||||||
|
return Promise.resolve(response.data)
|
||||||
|
},
|
||||||
|
(err) => {
|
||||||
|
return Promise.reject(err)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.catch((err) => {
|
||||||
|
return Promise.reject(err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
assignLabelToCard(data) {
|
assignLabelToCard(data) {
|
||||||
return axios.post(this.url(`/cards/${data.card.id}/label/${data.labelId}`))
|
return axios.post(this.url(`/cards/${data.card.id}/label/${data.labelId}`))
|
||||||
.then(
|
.then(
|
||||||
|
|||||||
@@ -334,6 +334,15 @@ export default {
|
|||||||
const updatedCard = await apiClient[call](card)
|
const updatedCard = await apiClient[call](card)
|
||||||
commit('updateCard', updatedCard)
|
commit('updateCard', updatedCard)
|
||||||
},
|
},
|
||||||
|
async changeCardDoneStatus({ commit }, card) {
|
||||||
|
let call = 'markCardAsDone'
|
||||||
|
if (card.done === false) {
|
||||||
|
call = 'markCardAsUndone'
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedCard = await apiClient[call](card)
|
||||||
|
commit('updateCardProperty', { property: 'done', card: updatedCard })
|
||||||
|
},
|
||||||
async assignCardToUser({ commit }, { card, assignee }) {
|
async assignCardToUser({ commit }, { card, assignee }) {
|
||||||
const user = await apiClient.assignUser(card.id, assignee.userId, assignee.type)
|
const user = await apiClient.assignUser(card.id, assignee.userId, assignee.type)
|
||||||
commit('assignCardToUser', user)
|
commit('assignCardToUser', user)
|
||||||
|
|||||||
@@ -133,7 +133,7 @@ class ImportExportTest extends \Test\TestCase {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function writeArrayStructure(string $prefix = '', array $array = [], array $skipKeyList = ['id', 'boardId', 'cardId', 'stackId', 'ETag', 'permissions', 'shared', 'version']): string {
|
public static function writeArrayStructure(string $prefix = '', array $array = [], array $skipKeyList = ['id', 'boardId', 'cardId', 'stackId', 'ETag', 'permissions', 'shared', 'version', 'done']): string {
|
||||||
$output = '';
|
$output = '';
|
||||||
$arrayIsList = array_keys($array) === range(0, count($array) - 1);
|
$arrayIsList = array_keys($array) === range(0, count($array) - 1);
|
||||||
foreach ($array as $key => $value) {
|
foreach ($array as $key => $value) {
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ class CardTest extends TestCase {
|
|||||||
$card->setOwner("admin");
|
$card->setOwner("admin");
|
||||||
$card->setOrder(12);
|
$card->setOrder(12);
|
||||||
$card->setArchived(false);
|
$card->setArchived(false);
|
||||||
|
$card->setDone(null);
|
||||||
// TODO: relation shared labels acl
|
// TODO: relation shared labels acl
|
||||||
return $card;
|
return $card;
|
||||||
}
|
}
|
||||||
@@ -87,6 +88,7 @@ class CardTest extends TestCase {
|
|||||||
'commentsCount' => 0,
|
'commentsCount' => 0,
|
||||||
'lastEditor' => null,
|
'lastEditor' => null,
|
||||||
'ETag' => $card->getETag(),
|
'ETag' => $card->getETag(),
|
||||||
|
'done' => null,
|
||||||
], (new CardDetails($card))->jsonSerialize());
|
], (new CardDetails($card))->jsonSerialize());
|
||||||
}
|
}
|
||||||
public function testJsonSerializeLabels() {
|
public function testJsonSerializeLabels() {
|
||||||
@@ -114,6 +116,7 @@ class CardTest extends TestCase {
|
|||||||
'commentsCount' => 0,
|
'commentsCount' => 0,
|
||||||
'lastEditor' => null,
|
'lastEditor' => null,
|
||||||
'ETag' => $card->getETag(),
|
'ETag' => $card->getETag(),
|
||||||
|
'done' => false,
|
||||||
], (new CardDetails($card))->jsonSerialize());
|
], (new CardDetails($card))->jsonSerialize());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,6 +146,7 @@ class CardTest extends TestCase {
|
|||||||
'commentsCount' => 0,
|
'commentsCount' => 0,
|
||||||
'lastEditor' => null,
|
'lastEditor' => null,
|
||||||
'ETag' => $card->getETag(),
|
'ETag' => $card->getETag(),
|
||||||
|
'done' => false,
|
||||||
], (new CardDetails($card))->jsonSerialize());
|
], (new CardDetails($card))->jsonSerialize());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user