diff --git a/css/icons.scss b/css/icons.scss
index 3c9af26cf..7d1ed1034 100644
--- a/css/icons.scss
+++ b/css/icons.scss
@@ -9,6 +9,7 @@
@include icon-black-white('filter_set', 'deck', 1);
@include icon-black-white('attach', 'deck', 1);
@include icon-black-white('reply', 'deck', 1);
+@include icon-black-white('notifications-dark', 'deck', 1);
.icon-toggle-compact-collapsed {
@include icon-color('toggle-view-expand', 'deck', $color-black);
diff --git a/docs/API.md b/docs/API.md
index dfa6d6c66..2ec2d7285 100644
--- a/docs/API.md
+++ b/docs/API.md
@@ -115,6 +115,10 @@ Returns an array of board items
"deletedAt": 0,
"id": 10,
"lastModified": 1586269585,
+ "settings": {
+ "notify-due": "off",
+ "calendar": true
+ }
}
]
```
@@ -952,6 +956,77 @@ For now only `deck_file` is supported as an attachment type.
The following endpoints are available through the Nextcloud OCS endpoint, which is available at `/ocs/v2.php/apps/deck/api/v1.0/`.
This has the benefit that both the web UI as well as external integrations can use the same API.
+## Config
+
+Deck stores user and app configuration values globally and per board. The GET endpoint allows to fetch the current global configuration while board settings will be exposed through the board element on the regular API endpoints.
+
+### GET /api/v1.0/config - Fetch app configuration values
+
+#### Response
+
+| Config key | Description | Value |
+| --- | --- |
+| calendar | Determines if the calendar/tasks integration through the CalDAV backend is enabled for the user (boolean) |
+| groupLimit | Determines if creating new boards is limited to certain groups of the instance. The resulting output is an array of group objects with the id and the displayname (Admin only)|
+
+```
+{
+ "ocs": {
+ "meta": {
+ "status": "ok",
+ "statuscode": 200,
+ "message": "OK"
+ },
+ "data": {
+ "calendar": true,
+ "groupLimit": [
+ {
+ "id": "admin",
+ "displayname": "admin"
+ }
+ ]
+ }
+ }
+}
+
+```
+
+### POST /api/v1.0/config/{id}/{key} - Set a config value
+
+
+#### Request parameters
+
+| Parameter | Type | Description |
+| --------- | ------- | --------------------------------------- |
+| id | Integer | The id of the board |
+| key | String | The config key to set, prefixed with `board:{boardId}:` for board specific settings |
+| value | String | The value that should be stored for the config key |
+
+##### Board configuration
+
+| Key | Value |
+| --- | ----- |
+| notify-due | `off`, `assigned` or `all` |
+| calendar | Boolean |
+
+#### Example request
+
+```
+curl -X POST 'https://admin:admin@nextcloud.local/ocs/v2.php/apps/deck/api/v1.0/config/calendar' -H 'Accept: application/json' -H "Content-Type: application/json" -H 'OCS-APIRequest: true' --data-raw '{"value":false}'
+
+{
+ "ocs": {
+ "meta": {
+ "status": "ok",
+ "statuscode": 200,
+ "message": "OK"
+ },
+ "data": false
+ }
+}
+
+```
+
## Comments
### GET /cards/{cardId}/comments - List comments
diff --git a/img/notifications-dark.svg b/img/notifications-dark.svg
new file mode 100644
index 000000000..1bc88f4c3
--- /dev/null
+++ b/img/notifications-dark.svg
@@ -0,0 +1,4 @@
+
+
diff --git a/lib/DAV/CalendarPlugin.php b/lib/DAV/CalendarPlugin.php
index 76da1929a..c3f8834d7 100644
--- a/lib/DAV/CalendarPlugin.php
+++ b/lib/DAV/CalendarPlugin.php
@@ -33,11 +33,14 @@ class CalendarPlugin implements ICalendarProvider {
/** @var DeckCalendarBackend */
private $backend;
+ /** @var ConfigService */
+ private $configService;
/** @var bool */
private $calendarIntegrationEnabled;
public function __construct(DeckCalendarBackend $backend, ConfigService $configService) {
$this->backend = $backend;
+ $this->configService = $configService;
$this->calendarIntegrationEnabled = $configService->get('calendar');
}
@@ -50,9 +53,12 @@ class CalendarPlugin implements ICalendarProvider {
return [];
}
+ $configService = $this->configService;
return array_map(function (Board $board) use ($principalUri) {
return new Calendar($principalUri, 'board-' . $board->getId(), $board, $this->backend);
- }, $this->backend->getBoards());
+ }, array_filter($this->backend->getBoards(), function ($board) use ($configService) {
+ return $configService->isCalendarEnabled($board->getId());
+ }));
}
public function hasCalendarInCalendarHome(string $principalUri, string $calendarUri): bool {
diff --git a/lib/Db/AssignedUsersMapper.php b/lib/Db/AssignedUsersMapper.php
index bd18f46cd..025570055 100644
--- a/lib/Db/AssignedUsersMapper.php
+++ b/lib/Db/AssignedUsersMapper.php
@@ -21,27 +21,35 @@
*
*/
+declare(strict_types=1);
namespace OCA\Deck\Db;
+use OCA\Deck\NotFoundException;
+use OCA\Deck\Service\CirclesService;
use OCP\AppFramework\Db\Entity;
+use OCP\AppFramework\Db\QBMapper;
use OCP\IDBConnection;
use OCP\IGroupManager;
use OCP\IUserManager;
-class AssignedUsersMapper extends DeckMapper implements IPermissionMapper {
- private $cardMapper;
- private $userManager;
- /**
- * @var IGroupManager
- */
- private $groupManager;
+class AssignedUsersMapper extends QBMapper implements IPermissionMapper {
- public function __construct(IDBConnection $db, CardMapper $cardMapper, IUserManager $userManager, IGroupManager $groupManager) {
+ /** @var CardMapper */
+ private $cardMapper;
+ /** @var IUserManager */
+ private $userManager;
+ /** @var IGroupManager */
+ private $groupManager;
+ /** @var CirclesService */
+ private $circleService;
+
+ public function __construct(IDBConnection $db, CardMapper $cardMapper, IUserManager $userManager, IGroupManager $groupManager, CirclesService $circleService) {
parent::__construct($db, 'deck_assigned_users', AssignedUsers::class);
$this->cardMapper = $cardMapper;
$this->userManager = $userManager;
$this->groupManager = $groupManager;
+ $this->circleService = $circleService;
}
/**
@@ -51,9 +59,12 @@ class AssignedUsersMapper extends DeckMapper implements IPermissionMapper {
* @return array|Entity
*/
public function find($cardId) {
- $sql = 'SELECT * FROM `*PREFIX*deck_assigned_users` ' .
- 'WHERE `card_id` = ?';
- $users = $this->findEntities($sql, [$cardId]);
+ $qb = $this->db->getQueryBuilder();
+ $qb->select('*')
+ ->from('deck_assigned_users')
+ ->where($qb->expr()->eq('card_id', $qb->createNamedParameter($cardId)));
+ /** @var AssignedUsers[] $users */
+ $users = $this->findEntities($qb);
foreach ($users as &$user) {
$this->mapParticipant($user);
}
@@ -61,9 +72,12 @@ class AssignedUsersMapper extends DeckMapper implements IPermissionMapper {
}
public function findByUserId($uid) {
- $sql = 'SELECT * FROM `*PREFIX*deck_assigned_users` ' .
- 'WHERE `participant` = ?';
- return $this->findEntities($sql, [$uid]);
+ $qb = $this->db->getQueryBuilder();
+ $qb->select('*')
+ ->from('deck_assigned_users')
+ ->where($qb->expr()->eq('participant', $qb->createNamedParameter($uid)));
+ /** @var AssignedUsers[] $users */
+ return $this->findEntities($qb);
}
@@ -81,24 +95,43 @@ class AssignedUsersMapper extends DeckMapper implements IPermissionMapper {
* @param Entity $entity
* @return null|Entity
*/
- public function insert(Entity $entity) {
+ public function insert(Entity $entity): Entity {
$origin = $this->getOrigin($entity);
- if ($origin !== null) {
- /** @var AssignedUsers $assignment */
- $assignment = parent::insert($entity);
- $this->mapParticipant($assignment);
- return $assignment;
+ if ($origin === null) {
+ throw new NotFoundException('No origin found for assignment');
}
- return null;
+ /** @var AssignedUsers $assignment */
+ $assignment = parent::insert($entity);
+ $this->mapParticipant($assignment);
+ return $assignment;
}
- public function mapParticipant(AssignedUsers &$assignment) {
+ public function mapParticipant(AssignedUsers $assignment): void {
$self = $this;
$assignment->resolveRelation('participant', function () use (&$self, &$assignment) {
return $self->getOrigin($assignment);
});
}
+ public function isUserAssigned($cardId, $userId): bool {
+ $assignments = $this->find($cardId);
+ /** @var AssignedUsers $assignment */
+ foreach ($assignments as $assignment) {
+ $origin = $this->getOrigin($assignment);
+ if ($origin instanceof User && $assignment->getParticipant() === $userId) {
+ return true;
+ }
+ if ($origin instanceof Group && $this->groupManager->isInGroup($userId, $assignment->getParticipant())) {
+ return true;
+ }
+ if ($origin instanceof Circle && $this->circleService->isUserInCircle($assignment->getParticipant(), $userId)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
private function getOrigin(AssignedUsers $assignment) {
if ($assignment->getType() === AssignedUsers::TYPE_USER) {
$origin = $this->userManager->get($assignment->getParticipant());
@@ -109,7 +142,7 @@ class AssignedUsersMapper extends DeckMapper implements IPermissionMapper {
return $origin ? new Group($origin) : null;
}
if ($assignment->getType() === AssignedUsers::TYPE_CIRCLE) {
- $origin = $this->groupManager->get($assignment->getParticipant());
+ $origin = $this->circleService->getCircle($assignment->getParticipant());
return $origin ? new Circle($origin) : null;
}
return null;
diff --git a/lib/Db/Board.php b/lib/Db/Board.php
index 28129b2b2..eb5ec1d07 100644
--- a/lib/Db/Board.php
+++ b/lib/Db/Board.php
@@ -37,6 +37,8 @@ class Board extends RelationalEntity {
protected $deletedAt = 0;
protected $lastModified = 0;
+ protected $settings = [];
+
public function __construct() {
$this->addType('id', 'integer');
$this->addType('shared', 'integer');
@@ -49,6 +51,7 @@ class Board extends RelationalEntity {
$this->addRelation('users');
$this->addRelation('permissions');
$this->addRelation('stacks');
+ $this->addRelation('settings');
$this->addResolvable('owner');
$this->shared = -1;
}
diff --git a/lib/Db/CardMapper.php b/lib/Db/CardMapper.php
index 1eff81145..c8d8f4d2a 100644
--- a/lib/Db/CardMapper.php
+++ b/lib/Db/CardMapper.php
@@ -25,7 +25,6 @@ namespace OCA\Deck\Db;
use Exception;
use OCP\AppFramework\Db\Entity;
-
use OCP\AppFramework\Db\QBMapper;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\IDBConnection;
@@ -82,7 +81,8 @@ class CardMapper extends QBMapper implements IPermissionMapper {
}
// make sure we only reset the notification flag if the duedate changes
- if (in_array('duedate', $entity->getUpdatedFields(), true)) {
+ $updatedFields = $entity->getUpdatedFields();
+ if (isset($updatedFields['duedate']) && $updatedFields['duedate']) {
try {
/** @var Card $existing */
$existing = $this->find($entity->getId());
@@ -243,6 +243,7 @@ class CardMapper extends QBMapper implements IPermissionMapper {
$qb->select('id','title','duedate','notified')
->from('deck_cards')
->where($qb->expr()->lt('duedate', $qb->createFunction('NOW()')))
+ ->andWhere($qb->expr()->eq('notified', $qb->createNamedParameter(false)))
->andWhere($qb->expr()->eq('archived', $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/Db/RelationalObject.php b/lib/Db/RelationalObject.php
index 9ca4d596c..e9f0ed667 100644
--- a/lib/Db/RelationalObject.php
+++ b/lib/Db/RelationalObject.php
@@ -57,4 +57,8 @@ class RelationalObject implements \JsonSerializable {
throw new \Exception('jsonSerialize is not implemented on ' . get_class($this));
}
}
+
+ public function getPrimaryKey(): string {
+ return $this->primaryKey;
+ }
}
diff --git a/lib/Notification/NotificationHelper.php b/lib/Notification/NotificationHelper.php
index 83bf67606..299e1332d 100644
--- a/lib/Notification/NotificationHelper.php
+++ b/lib/Notification/NotificationHelper.php
@@ -24,14 +24,18 @@
namespace OCA\Deck\Notification;
use DateTime;
+use OCA\Deck\AppInfo\Application;
use OCA\Deck\Db\Acl;
+use OCA\Deck\Db\AssignedUsersMapper;
use OCA\Deck\Db\Board;
use OCA\Deck\Db\BoardMapper;
use OCA\Deck\Db\CardMapper;
+use OCA\Deck\Db\User;
+use OCA\Deck\Service\ConfigService;
use OCA\Deck\Service\PermissionService;
use OCP\Comments\IComment;
+use OCP\IConfig;
use OCP\IGroupManager;
-use OCP\IUser;
use OCP\Notification\IManager;
class NotificationHelper {
@@ -40,8 +44,12 @@ class NotificationHelper {
protected $cardMapper;
/** @var BoardMapper */
protected $boardMapper;
+ /** @var AssignedUsersMapper */
+ protected $assignedUsersMapper;
/** @var PermissionService */
protected $permissionService;
+ /** @var IConfig */
+ protected $config;
/** @var IManager */
protected $notificationManager;
/** @var IGroupManager */
@@ -54,14 +62,18 @@ class NotificationHelper {
public function __construct(
CardMapper $cardMapper,
BoardMapper $boardMapper,
+ AssignedUsersMapper $assignedUsersMapper,
PermissionService $permissionService,
+ IConfig $config,
IManager $notificationManager,
IGroupManager $groupManager,
$userId
) {
$this->cardMapper = $cardMapper;
$this->boardMapper = $boardMapper;
+ $this->assignedUsersMapper = $assignedUsersMapper;
$this->permissionService = $permissionService;
+ $this->config = $config;
$this->notificationManager = $notificationManager;
$this->groupManager = $groupManager;
$this->currentUser = $userId;
@@ -80,21 +92,38 @@ class NotificationHelper {
return;
}
- // TODO: Once assigning users is possible, those should be notified instead of all users of the board
$boardId = $this->cardMapper->findBoardId($card->getId());
- $board = $this->getBoard($boardId);
- /** @var IUser $user */
+ $board = $this->getBoard($boardId, false, true);
+ /** @var User $user */
foreach ($this->permissionService->findUsers($boardId) as $user) {
- $notification = $this->notificationManager->createNotification();
- $notification
- ->setApp('deck')
- ->setUser((string) $user->getUID())
- ->setObject('card', $card->getId())
- ->setSubject('card-overdue', [
- $card->getTitle(), $board->getTitle()
- ])
- ->setDateTime(new DateTime($card->getDuedate()));
- $this->notificationManager->notify($notification);
+ $notificationSetting = $this->config->getUserValue($user->getUID(), Application::APP_ID, 'board:' . $boardId . ':notify-due', ConfigService::SETTING_BOARD_NOTIFICATION_DUE_DEFAULT);
+
+ if ($notificationSetting === ConfigService::SETTING_BOARD_NOTIFICATION_DUE_OFF) {
+ continue;
+ }
+
+ $shouldNotify = $notificationSetting === ConfigService::SETTING_BOARD_NOTIFICATION_DUE_ALL;
+
+ if ($user->getUID() === $board->getOwner() && count($board->getAcl()) === 0) {
+ // Notify if all or assigned is configured for unshared boards
+ $shouldNotify = true;
+ } elseif ($notificationSetting === ConfigService::SETTING_BOARD_NOTIFICATION_DUE_ASSIGNED && $this->assignedUsersMapper->isUserAssigned($card->getId(), $user->getUID())) {
+ // Notify if the user is assigned and has the assigned setting selected
+ $shouldNotify = true;
+ }
+
+ if ($shouldNotify) {
+ $notification = $this->notificationManager->createNotification();
+ $notification
+ ->setApp('deck')
+ ->setUser((string)$user->getUID())
+ ->setObject('card', $card->getId())
+ ->setSubject('card-overdue', [
+ $card->getTitle(), $board->getTitle()
+ ])
+ ->setDateTime(new DateTime($card->getDuedate()));
+ $this->notificationManager->notify($notification);
+ }
}
$this->cardMapper->markNotified($card);
}
@@ -169,9 +198,9 @@ class NotificationHelper {
* @return Board
* @throws \OCP\AppFramework\Db\DoesNotExistException
*/
- private function getBoard($boardId) {
+ private function getBoard($boardId, bool $withLabels = false, bool $withAcl = false) {
if (!array_key_exists($boardId, $this->boards)) {
- $this->boards[$boardId] = $this->boardMapper->find($boardId);
+ $this->boards[$boardId] = $this->boardMapper->find($boardId, $withLabels, $withAcl);
}
return $this->boards[$boardId];
}
diff --git a/lib/Service/BoardService.php b/lib/Service/BoardService.php
index 516ca158a..cdc348119 100644
--- a/lib/Service/BoardService.php
+++ b/lib/Service/BoardService.php
@@ -26,6 +26,7 @@ namespace OCA\Deck\Service;
use OCA\Deck\Activity\ActivityManager;
use OCA\Deck\Activity\ChangeSet;
+use OCA\Deck\AppInfo\Application;
use OCA\Deck\Db\Acl;
use OCA\Deck\Db\AclMapper;
use OCA\Deck\Db\AssignedUsersMapper;
@@ -37,6 +38,7 @@ use OCA\Deck\Db\StackMapper;
use OCA\Deck\NoPermissionException;
use OCA\Deck\Notification\NotificationHelper;
use OCP\AppFramework\Db\DoesNotExistException;
+use OCP\IConfig;
use OCP\IGroupManager;
use OCP\IL10N;
use OCA\Deck\Db\Board;
@@ -52,6 +54,8 @@ class BoardService {
private $stackMapper;
private $labelMapper;
private $aclMapper;
+ /** @var IConfig */
+ private $config;
private $l10n;
private $permissionService;
private $notificationHelper;
@@ -66,9 +70,11 @@ class BoardService {
private $boardsCache = null;
+
public function __construct(
BoardMapper $boardMapper,
StackMapper $stackMapper,
+ IConfig $config,
IL10N $l10n,
LabelMapper $labelMapper,
AclMapper $aclMapper,
@@ -85,6 +91,7 @@ class BoardService {
$this->boardMapper = $boardMapper;
$this->stackMapper = $stackMapper;
$this->labelMapper = $labelMapper;
+ $this->config = $config;
$this->aclMapper = $aclMapper;
$this->l10n = $l10n;
$this->permissionService = $permissionService;
@@ -151,6 +158,7 @@ class BoardService {
'PERMISSION_MANAGE' => $permissions[Acl::PERMISSION_MANAGE] ?? false,
'PERMISSION_SHARE' => $permissions[Acl::PERMISSION_SHARE] ?? false
]);
+ $this->enrichWithBoardSettings($item);
$result[$item->getId()] = $item;
}
$this->boardsCache = $result;
@@ -190,6 +198,7 @@ class BoardService {
'PERMISSION_SHARE' => $permissions[Acl::PERMISSION_SHARE] ?? false
]);
$this->enrichWithUsers($board);
+ $this->enrichWithBoardSettings($board);
$this->boardsCache[$board->getId()] = $board;
return $board;
}
@@ -476,6 +485,15 @@ class BoardService {
return [$edit, $share, $manage];
}
+ public function enrichWithBoardSettings(Board $board) {
+ $globalCalendarConfig = (bool)$this->config->getUserValue($this->userId, Application::APP_ID, 'calendar', true);
+ $settings = [
+ 'notify-due' => $this->config->getUserValue($this->userId, Application::APP_ID, 'board:' . $board->getId() . ':notify-due', ConfigService::SETTING_BOARD_NOTIFICATION_DUE_ASSIGNED),
+ 'calendar' => $this->config->getUserValue($this->userId, Application::APP_ID, 'board:' . $board->getId() . ':calendar', $globalCalendarConfig),
+ ];
+ $board->setSettings($settings);
+ }
+
/**
* @param $boardId
* @param $type
diff --git a/lib/Service/CirclesService.php b/lib/Service/CirclesService.php
new file mode 100644
index 000000000..4df7a7987
--- /dev/null
+++ b/lib/Service/CirclesService.php
@@ -0,0 +1,62 @@
+
+ *
+ * @author Julius Härtl
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+declare(strict_types=1);
+
+
+namespace OCA\Deck\Service;
+
+use OCP\App\IAppManager;
+
+/**
+ * Wrapper around circles app API since it is not in a public namespace so we need to make sure that
+ * having the app disabled is properly handled
+ */
+class CirclesService {
+ private $circlesEnabled;
+
+ public function __construct(IAppManager $appManager) {
+ $this->circlesEnabled = $appManager->isEnabledForUser('circles');
+ }
+
+ public function getCircle($circleId) {
+ if (!$this->circlesEnabled) {
+ return null;
+ }
+
+ return \OCA\Circles\Api\v1\Circles::detailsCircle($circleId, true);
+ }
+
+ public function isUserInCircle($circleId, $userId): bool {
+ if (!$this->circlesEnabled) {
+ return false;
+ }
+
+ try {
+ \OCA\Circles\Api\v1\Circles::getMember($circleId, $userId, 1, true);
+ return true;
+ } catch (\Exception $e) {
+ }
+ return false;
+ }
+}
diff --git a/lib/Service/ConfigService.php b/lib/Service/ConfigService.php
index baf1e4b8f..d917740a2 100644
--- a/lib/Service/ConfigService.php
+++ b/lib/Service/ConfigService.php
@@ -27,12 +27,18 @@ declare(strict_types=1);
namespace OCA\Deck\Service;
use OCA\Deck\AppInfo\Application;
+use OCA\Deck\BadRequestException;
use OCA\Deck\NoPermissionException;
use OCP\IConfig;
use OCP\IGroup;
use OCP\IGroupManager;
class ConfigService {
+ public const SETTING_BOARD_NOTIFICATION_DUE_OFF = 'off';
+ public const SETTING_BOARD_NOTIFICATION_DUE_ASSIGNED = 'assigned';
+ public const SETTING_BOARD_NOTIFICATION_DUE_ALL = 'all';
+ public const SETTING_BOARD_NOTIFICATION_DUE_DEFAULT = self::SETTING_BOARD_NOTIFICATION_DUE_ASSIGNED;
+
private $config;
private $userId;
private $groupManager;
@@ -49,19 +55,18 @@ class ConfigService {
public function getAll(): array {
$data = [
- 'calendar' => $this->get('calendar')
+ 'calendar' => $this->isCalendarEnabled()
];
if ($this->groupManager->isAdmin($this->userId)) {
- $data = [
- 'groupLimit' => $this->get('groupLimit'),
- ];
+ $data['groupLimit'] = $this->get('groupLimit');
}
return $data;
}
public function get($key) {
$result = null;
- switch ($key) {
+ [$scope, $id] = explode(':', $key, 2);
+ switch ($scope) {
case 'groupLimit':
if (!$this->groupManager->isAdmin($this->userId)) {
throw new NoPermissionException('You must be admin to get the group limit');
@@ -75,9 +80,19 @@ class ConfigService {
return $result;
}
+ public function isCalendarEnabled(int $boardId = null): bool {
+ $defaultState = (bool)$this->config->getUserValue($this->userId, Application::APP_ID, 'calendar', true);
+ if ($boardId === null) {
+ return $defaultState;
+ }
+
+ return (bool)$this->config->getUserValue($this->userId, Application::APP_ID, 'board:' . $boardId . ':calendar', $defaultState);
+ }
+
public function set($key, $value) {
$result = null;
- switch ($key) {
+ [$scope, $id] = explode(':', $key, 2);
+ switch ($scope) {
case 'groupLimit':
if (!$this->groupManager->isAdmin($this->userId)) {
throw new NoPermissionException('You must be admin to set the group limit');
@@ -88,6 +103,13 @@ class ConfigService {
$this->config->setUserValue($this->userId, Application::APP_ID, 'calendar', (int)$value);
$result = $value;
break;
+ case 'board':
+ [$boardId, $boardConfigKey] = explode(':', $key);
+ if ($boardConfigKey === 'notify-due' && !in_array($value, [self::SETTING_BOARD_NOTIFICATION_DUE_ALL, self::SETTING_BOARD_NOTIFICATION_DUE_ASSIGNED, self::SETTING_BOARD_NOTIFICATION_DUE_OFF], true)) {
+ throw new BadRequestException('Board notification option must be one of: off, assigned, all');
+ }
+ $this->config->setUserValue($this->userId, Application::APP_ID, $key, $value);
+ $result = $value;
}
return $result;
}
diff --git a/src/components/navigation/AppNavigationBoard.vue b/src/components/navigation/AppNavigationBoard.vue
index fa03da162..0041d7992 100644
--- a/src/components/navigation/AppNavigationBoard.vue
+++ b/src/components/navigation/AppNavigationBoard.vue
@@ -34,38 +34,89 @@
style="opacity: 0.5" />
-
- {{ t('deck', 'Edit board') }}
+
+
+ {{ t('deck', 'Board details') }}
+
+
+ {{ t('deck', 'Edit board') }}
+
+
+ {{ t('deck', 'Clone board') }}
+
+
+ {{ t('deck', 'Unarchive board') }}
+
+
+ {{ t('deck', 'Archive board') }}
+
+
+
+ {{ board.settings['notify-due'] === 'off' ? t('deck', 'Turn on due date reminders') : t('deck', 'Turn off due date reminders') }}
+
+
+
+
+
+
+ {{ t('deck', 'Due date reminders') }}
+
+
+
+ {{ t('deck', 'All cards') }}
+
+
+ {{ t('deck', 'Assigned cards') }}
+
+
+ {{ t('deck', 'No notifications') }}
+
+
+
+ {{ dueDateReminderText }}
-
- {{ t('deck', 'Clone board ') }}
-
-
- {{ t('deck', 'Unarchive board ') }}
-
-
- {{ t('deck', 'Archive board ') }}
-
-
- {{ t('deck', 'Delete board ') }}
-
-
- {{ t('deck', 'Board details') }}
+ {{ t('deck', 'Delete board') }}
@@ -114,6 +165,8 @@ export default {
undoTimeoutHandle: null,
editTitle: '',
editColor: '',
+ isDueSubmenuActive: false,
+ updateDueSetting: null,
}
},
computed: {
@@ -133,7 +186,27 @@ export default {
}
},
canManage() {
- return this.board.permissions.PERMISSION_MANAGE
+ return this.board.permissions.PERMISSION_MANAGE && !this.board.archived
+ },
+ dueDateReminderIcon() {
+ if (this.board.settings['notify-due'] === 'all') {
+ return 'icon-sound'
+ } else if (this.board.settings['notify-due'] === 'assigned') {
+ return 'icon-user'
+ } else if (this.board.settings['notify-due'] === 'off') {
+ return 'icon-sound-off'
+ }
+ return ''
+ },
+ dueDateReminderText() {
+ if (this.board.settings['notify-due'] === 'all') {
+ return t('deck', 'All cards')
+ } else if (this.board.settings['notify-due'] === 'assigned') {
+ return t('deck', 'Only assigned cards')
+ } else if (this.board.settings['notify-due'] === 'off') {
+ return t('deck', 'No reminder')
+ }
+ return ''
},
},
watch: {},
@@ -230,6 +303,14 @@ export default {
route.name = 'board.details'
this.$router.push(route)
},
+ async updateSetting(key, value) {
+ this.updateDueSetting = value
+ const setting = {}
+ setting['board:' + this.board.id + ':' + key] = value
+ await this.$store.dispatch('setConfig', setting)
+ this.isDueSubmenuActive = false
+ this.updateDueSetting = null
+ },
},
inject: [
'boardApi',
@@ -266,4 +347,8 @@ export default {
background-size: 14px;
}
}
+
+ .forced-active {
+ box-shadow: inset 4px 0 var(--color-primary-element);
+ }
diff --git a/src/store/main.js b/src/store/main.js
index 1acec6466..d6ee6f5e7 100644
--- a/src/store/main.js
+++ b/src/store/main.js
@@ -138,7 +138,21 @@ export default new Vuex.Store({
},
mutations: {
SET_CONFIG(state, { key, value }) {
- Vue.set(state.config, key, value)
+ const [scope, id, configKey] = key.split(':', 3)
+ let indexExisting = -1
+ switch (scope) {
+ case 'board':
+ indexExisting = state.boards.findIndex((b) => {
+ return id === '' + b.id
+ })
+
+ if (indexExisting > -1) {
+ Vue.set(state.boards[indexExisting].settings, configKey, value)
+ }
+ break
+ default:
+ Vue.set(state.config, key, value)
+ }
},
setSearchQuery(state, searchQuery) {
state.searchQuery = searchQuery
diff --git a/tests/integration/database/AssignedUsersMapperTest.php b/tests/integration/database/AssignedUsersMapperTest.php
index 37a709c65..5a6a949ce 100644
--- a/tests/integration/database/AssignedUsersMapperTest.php
+++ b/tests/integration/database/AssignedUsersMapperTest.php
@@ -23,6 +23,7 @@
namespace OCA\Deck\Db;
+use OCA\Deck\NotFoundException;
use OCA\Deck\Service\AssignmentService;
use OCA\Deck\Service\BoardService;
use OCA\Deck\Service\StackService;
@@ -165,8 +166,8 @@ class AssignedUsersMapperTest extends \Test\TestCase {
$assignment->setCardId($this->cards[1]->getId());
$assignment->setParticipant('invalid-username');
$assignment->setType(AssignedUsers::TYPE_USER);
- $actual = $this->assignedUsersMapper->insert($assignment);
- $this->assertNull($actual);
+ $this->expectException(NotFoundException::class);
+ $this->assignedUsersMapper->insert($assignment);
}
/**
@@ -188,6 +189,44 @@ class AssignedUsersMapperTest extends \Test\TestCase {
$this->assertEquals('invalid-username', $assignment->resolveParticipant());
}
+ public function testIsUserAssigned() {
+ $assignment = new AssignedUsers();
+ $assignment->setCardId($this->cards[1]->getId());
+ $assignment->setParticipant(self::TEST_USER4);
+ $assignment->setType(AssignedUsers::TYPE_USER);
+ $this->assertFalse($this->assignedUsersMapper->isUserAssigned($this->cards[1]->getId(), self::TEST_USER4));
+
+ $assignment = $this->assignedUsersMapper->insert($assignment);
+ $actual = $this->assignedUsersMapper->isUserAssigned($this->cards[1]->getId(), self::TEST_USER4);
+ $this->assignedUsersMapper->delete($assignment);
+ $this->assertTrue($actual);
+
+ $this->assertFalse($this->assignedUsersMapper->isUserAssigned($this->cards[1]->getId(), self::TEST_USER4));
+ }
+
+ public function testIsUserAssignedGroup() {
+ $assignment = new AssignedUsers();
+ $assignment->setCardId($this->cards[1]->getId());
+ $assignment->setParticipant('group');
+ $assignment->setType(AssignedUsers::TYPE_GROUP);
+ $this->assertFalse($this->assignedUsersMapper->isUserAssigned($this->cards[1]->getId(), self::TEST_USER1));
+ $this->assertFalse($this->assignedUsersMapper->isUserAssigned($this->cards[1]->getId(), self::TEST_USER2));
+ $this->assertFalse($this->assignedUsersMapper->isUserAssigned($this->cards[1]->getId(), self::TEST_USER3));
+ $this->assertFalse($this->assignedUsersMapper->isUserAssigned($this->cards[1]->getId(), self::TEST_USER4));
+
+ $assignment = $this->assignedUsersMapper->insert($assignment);
+ $this->assertTrue($this->assignedUsersMapper->isUserAssigned($this->cards[1]->getId(), self::TEST_USER1));
+ $this->assertTrue($this->assignedUsersMapper->isUserAssigned($this->cards[1]->getId(), self::TEST_USER2));
+ $this->assertTrue($this->assignedUsersMapper->isUserAssigned($this->cards[1]->getId(), self::TEST_USER3));
+ $this->assertFalse($this->assignedUsersMapper->isUserAssigned($this->cards[1]->getId(), self::TEST_USER4));
+ $this->assignedUsersMapper->delete($assignment);
+
+ $this->assertFalse($this->assignedUsersMapper->isUserAssigned($this->cards[1]->getId(), self::TEST_USER1));
+ $this->assertFalse($this->assignedUsersMapper->isUserAssigned($this->cards[1]->getId(), self::TEST_USER2));
+ $this->assertFalse($this->assignedUsersMapper->isUserAssigned($this->cards[1]->getId(), self::TEST_USER3));
+ $this->assertFalse($this->assignedUsersMapper->isUserAssigned($this->cards[1]->getId(), self::TEST_USER4));
+ }
+
public function tearDown(): void {
$this->boardService->deleteForce($this->board->getId());
parent::tearDown();
diff --git a/tests/unit/Db/BoardTest.php b/tests/unit/Db/BoardTest.php
index 63c7f2b82..369e13a61 100644
--- a/tests/unit/Db/BoardTest.php
+++ b/tests/unit/Db/BoardTest.php
@@ -31,6 +31,7 @@ class BoardTest extends TestCase {
'acl' => [],
'archived' => false,
'users' => ['user1', 'user2'],
+ 'settings' => [],
], $board->jsonSerialize());
}
@@ -50,6 +51,7 @@ class BoardTest extends TestCase {
'acl' => [],
'archived' => false,
'users' => [],
+ 'settings' => [],
], $board->jsonSerialize());
}
public function testSetAcl() {
@@ -77,6 +79,7 @@ class BoardTest extends TestCase {
'archived' => false,
'shared' => 1,
'users' => [],
+ 'settings' => [],
], $board->jsonSerialize());
}
}
diff --git a/tests/unit/Notification/NotificationHelperTest.php b/tests/unit/Notification/NotificationHelperTest.php
index a1d771da4..80d7997e6 100644
--- a/tests/unit/Notification/NotificationHelperTest.php
+++ b/tests/unit/Notification/NotificationHelperTest.php
@@ -24,30 +24,50 @@
namespace OCA\Deck\Notification;
use OCA\Deck\Db\Acl;
+use OCA\Deck\Db\AssignedUsersMapper;
use OCA\Deck\Db\Board;
use OCA\Deck\Db\BoardMapper;
use OCA\Deck\Db\Card;
use OCA\Deck\Db\CardMapper;
use OCA\Deck\Db\User;
+use OCA\Deck\Service\ConfigService;
use OCA\Deck\Service\PermissionService;
use OCP\Comments\IComment;
+use OCP\IConfig;
use OCP\IGroup;
use OCP\IGroupManager;
use OCP\IUser;
use OCP\Notification\IManager;
use OCP\Notification\INotification;
+use PHPUnit\Framework\MockObject\MockObject;
+
+class DummyUser extends \OC\User\User {
+ private $uid;
+
+ public function __construct($uid) {
+ $this->uid = $uid;
+ }
+
+ public function getUID() {
+ return $this->uid;
+ }
+}
class NotificationHelperTest extends \Test\TestCase {
- /** @var CardMapper */
+ /** @var CardMapper|MockObject */
protected $cardMapper;
- /** @var BoardMapper */
+ /** @var BoardMapper|MockObject */
protected $boardMapper;
- /** @var PermissionService */
+ /** @var AssignedUsersMapper|MockObject */
+ protected $assignedUsersMapper;
+ /** @var PermissionService|MockObject */
protected $permissionService;
- /** @var IManager */
+ /** @var IConfig|MockObject */
+ protected $config;
+ /** @var IManager|MockObject */
protected $notificationManager;
- /** @var IGroupManager */
+ /** @var IGroupManager|MockObject */
protected $groupManager;
/** @var string */
protected $currentUser;
@@ -58,14 +78,18 @@ class NotificationHelperTest extends \Test\TestCase {
parent::setUp();
$this->cardMapper = $this->createMock(CardMapper::class);
$this->boardMapper = $this->createMock(BoardMapper::class);
+ $this->assignedUsersMapper = $this->createMock(AssignedUsersMapper::class);
$this->permissionService = $this->createMock(PermissionService::class);
+ $this->config = $this->createMock(IConfig::class);
$this->notificationManager = $this->createMock(IManager::class);
$this->groupManager = $this->createMock(IGroupManager::class);
$this->currentUser = 'admin';
$this->notificationHelper = new NotificationHelper(
$this->cardMapper,
$this->boardMapper,
+ $this->assignedUsersMapper,
$this->permissionService,
+ $this->config,
$this->notificationManager,
$this->groupManager,
$this->currentUser
@@ -90,34 +114,32 @@ class NotificationHelperTest extends \Test\TestCase {
}
public function testSendCardDuedate() {
- $card = $this->createMock(Card::class);
- $card->expects($this->at(0))
- ->method('__call')
- ->with('getNotified', [])
- ->willReturn(false);
- $card->expects($this->at(1))
- ->method('__call')
- ->with('getId', [])
- ->willReturn(123);
- for ($i=0; $i<3; $i++) {
- $card->expects($this->at($i*3+2))
- ->method('__call')
- ->with('getId', [])
- ->willReturn(123);
- $card->expects($this->at($i*3+3))
- ->method('__call', [])
- ->with('getTitle')
- ->willReturn('MyCardTitle');
- }
+ $this->config->expects($this->at(0))
+ ->method('getUserValue')
+ ->with('foo', 'deck', 'board:234:notify-due', ConfigService::SETTING_BOARD_NOTIFICATION_DUE_ASSIGNED)
+ ->willReturn(ConfigService::SETTING_BOARD_NOTIFICATION_DUE_ALL);
+ $this->config->expects($this->at(1))
+ ->method('getUserValue')
+ ->with('bar', 'deck', 'board:234:notify-due', ConfigService::SETTING_BOARD_NOTIFICATION_DUE_ASSIGNED)
+ ->willReturn(ConfigService::SETTING_BOARD_NOTIFICATION_DUE_ALL);
+ $this->config->expects($this->at(2))
+ ->method('getUserValue')
+ ->with('asd', 'deck', 'board:234:notify-due', ConfigService::SETTING_BOARD_NOTIFICATION_DUE_ASSIGNED)
+ ->willReturn(ConfigService::SETTING_BOARD_NOTIFICATION_DUE_ALL);
+
+ $card = Card::fromParams([
+ 'notified' => false,
+ 'id' => 123,
+ 'title' => 'MyCardTitle'
+ ]);
$this->cardMapper->expects($this->once())
->method('findBoardId')
->with(123)
->willReturn(234);
- $board = $this->createMock(Board::class);
- $board->expects($this->any())
- ->method('__call')
- ->with('getTitle', [])
- ->willReturn('MyBoardTitle');
+ $board = Board::fromParams([
+ 'id' => 123,
+ 'title' => 'MyBoardTitle'
+ ]);
$this->boardMapper->expects($this->once())
->method('find')
->with(234)
@@ -183,6 +205,188 @@ class NotificationHelperTest extends \Test\TestCase {
$this->notificationHelper->sendCardDuedate($card);
}
+ public function testSendCardDuedateAssigned() {
+ $this->config->expects($this->at(0))
+ ->method('getUserValue')
+ ->with('foo', 'deck', 'board:234:notify-due', ConfigService::SETTING_BOARD_NOTIFICATION_DUE_ASSIGNED)
+ ->willReturn(ConfigService::SETTING_BOARD_NOTIFICATION_DUE_ASSIGNED);
+ $this->config->expects($this->at(1))
+ ->method('getUserValue')
+ ->with('bar', 'deck', 'board:234:notify-due', ConfigService::SETTING_BOARD_NOTIFICATION_DUE_ASSIGNED)
+ ->willReturn(ConfigService::SETTING_BOARD_NOTIFICATION_DUE_ASSIGNED);
+ $this->config->expects($this->at(2))
+ ->method('getUserValue')
+ ->with('asd', 'deck', 'board:234:notify-due', ConfigService::SETTING_BOARD_NOTIFICATION_DUE_ASSIGNED)
+ ->willReturn(ConfigService::SETTING_BOARD_NOTIFICATION_DUE_ASSIGNED);
+
+ $users = [
+ new DummyUser('foo'), new DummyUser('bar'), new DummyUser('asd')
+ ];
+ $card = Card::fromParams([
+ 'notified' => false,
+ 'id' => 123,
+ 'title' => 'MyCardTitle'
+ ]);
+ $card->setAssignedUsers([
+ new User($users[0])
+ ]);
+ $this->cardMapper->expects($this->once())
+ ->method('findBoardId')
+ ->with(123)
+ ->willReturn(234);
+ $board = Board::fromParams([
+ 'id' => 123,
+ 'title' => 'MyBoardTitle'
+ ]);
+ $this->boardMapper->expects($this->once())
+ ->method('find')
+ ->with(234)
+ ->willReturn($board);
+
+
+ $this->permissionService->expects($this->once())
+ ->method('findUsers')
+ ->with(234)
+ ->willReturn($users);
+
+ $this->assignedUsersMapper->expects($this->exactly(3))
+ ->method('isUserAssigned')
+ ->willReturn(true);
+
+
+ $n1 = $this->createMock(INotification::class);
+ $n2 = $this->createMock(INotification::class);
+ $n3 = $this->createMock(INotification::class);
+
+ $n1->expects($this->once())->method('setApp')->with('deck')->willReturn($n1);
+ $n1->expects($this->once())->method('setUser')->with('foo')->willReturn($n1);
+ $n1->expects($this->once())->method('setObject')->with('card', 123)->willReturn($n1);
+ $n1->expects($this->once())->method('setSubject')->with('card-overdue', ['MyCardTitle', 'MyBoardTitle'])->willReturn($n1);
+ $n1->expects($this->once())->method('setDateTime')->willReturn($n1);
+
+ $n2->expects($this->once())->method('setApp')->with('deck')->willReturn($n2);
+ $n2->expects($this->once())->method('setUser')->with('bar')->willReturn($n2);
+ $n2->expects($this->once())->method('setObject')->with('card', 123)->willReturn($n2);
+ $n2->expects($this->once())->method('setSubject')->with('card-overdue', ['MyCardTitle', 'MyBoardTitle'])->willReturn($n2);
+ $n2->expects($this->once())->method('setDateTime')->willReturn($n2);
+
+ $n3->expects($this->once())->method('setApp')->with('deck')->willReturn($n3);
+ $n3->expects($this->once())->method('setUser')->with('asd')->willReturn($n3);
+ $n3->expects($this->once())->method('setObject')->with('card', 123)->willReturn($n3);
+ $n3->expects($this->once())->method('setSubject')->with('card-overdue', ['MyCardTitle', 'MyBoardTitle'])->willReturn($n3);
+ $n3->expects($this->once())->method('setDateTime')->willReturn($n3);
+
+ $this->notificationManager->expects($this->at(0))
+ ->method('createNotification')
+ ->willReturn($n1);
+ $this->notificationManager->expects($this->at(1))
+ ->method('notify')
+ ->with($n1);
+ $this->notificationManager->expects($this->at(2))
+ ->method('createNotification')
+ ->willReturn($n2);
+ $this->notificationManager->expects($this->at(3))
+ ->method('notify')
+ ->with($n2);
+ $this->notificationManager->expects($this->at(4))
+ ->method('createNotification')
+ ->willReturn($n3);
+ $this->notificationManager->expects($this->at(5))
+ ->method('notify')
+ ->with($n3);
+
+ $this->cardMapper->expects($this->once())
+ ->method('markNotified')
+ ->with($card);
+
+ $this->notificationHelper->sendCardDuedate($card);
+ }
+
+
+ public function testSendCardDuedateNever() {
+ $this->config->expects($this->at(0))
+ ->method('getUserValue')
+ ->with('foo', 'deck', 'board:234:notify-due', ConfigService::SETTING_BOARD_NOTIFICATION_DUE_ASSIGNED)
+ ->willReturn(ConfigService::SETTING_BOARD_NOTIFICATION_DUE_ASSIGNED);
+ $this->config->expects($this->at(1))
+ ->method('getUserValue')
+ ->with('bar', 'deck', 'board:234:notify-due', ConfigService::SETTING_BOARD_NOTIFICATION_DUE_ASSIGNED)
+ ->willReturn(ConfigService::SETTING_BOARD_NOTIFICATION_DUE_ASSIGNED);
+ $this->config->expects($this->at(2))
+ ->method('getUserValue')
+ ->with('asd', 'deck', 'board:234:notify-due', ConfigService::SETTING_BOARD_NOTIFICATION_DUE_ASSIGNED)
+ ->willReturn(ConfigService::SETTING_BOARD_NOTIFICATION_DUE_OFF);
+
+ $users = [
+ new DummyUser('foo'), new DummyUser('bar'), new DummyUser('asd')
+ ];
+ $card = Card::fromParams([
+ 'notified' => false,
+ 'id' => 123,
+ 'title' => 'MyCardTitle'
+ ]);
+ $card->setAssignedUsers([
+ new User($users[0])
+ ]);
+ $this->cardMapper->expects($this->once())
+ ->method('findBoardId')
+ ->with(123)
+ ->willReturn(234);
+ $board = Board::fromParams([
+ 'id' => 123,
+ 'title' => 'MyBoardTitle'
+ ]);
+ $this->boardMapper->expects($this->once())
+ ->method('find')
+ ->with(234)
+ ->willReturn($board);
+
+
+ $this->permissionService->expects($this->once())
+ ->method('findUsers')
+ ->with(234)
+ ->willReturn($users);
+
+ $this->assignedUsersMapper->expects($this->exactly(2))
+ ->method('isUserAssigned')
+ ->willReturn(true);
+
+
+ $n1 = $this->createMock(INotification::class);
+ $n2 = $this->createMock(INotification::class);
+
+ $n1->expects($this->once())->method('setApp')->with('deck')->willReturn($n1);
+ $n1->expects($this->once())->method('setUser')->with('foo')->willReturn($n1);
+ $n1->expects($this->once())->method('setObject')->with('card', 123)->willReturn($n1);
+ $n1->expects($this->once())->method('setSubject')->with('card-overdue', ['MyCardTitle', 'MyBoardTitle'])->willReturn($n1);
+ $n1->expects($this->once())->method('setDateTime')->willReturn($n1);
+
+ $n2->expects($this->once())->method('setApp')->with('deck')->willReturn($n2);
+ $n2->expects($this->once())->method('setUser')->with('bar')->willReturn($n2);
+ $n2->expects($this->once())->method('setObject')->with('card', 123)->willReturn($n2);
+ $n2->expects($this->once())->method('setSubject')->with('card-overdue', ['MyCardTitle', 'MyBoardTitle'])->willReturn($n2);
+ $n2->expects($this->once())->method('setDateTime')->willReturn($n2);
+
+ $this->notificationManager->expects($this->at(0))
+ ->method('createNotification')
+ ->willReturn($n1);
+ $this->notificationManager->expects($this->at(1))
+ ->method('notify')
+ ->with($n1);
+ $this->notificationManager->expects($this->at(2))
+ ->method('createNotification')
+ ->willReturn($n2);
+ $this->notificationManager->expects($this->at(3))
+ ->method('notify')
+ ->with($n2);
+
+ $this->cardMapper->expects($this->once())
+ ->method('markNotified')
+ ->with($card);
+
+ $this->notificationHelper->sendCardDuedate($card);
+ }
+
public function testSendCardAssignedUser() {
$board = new Board();
$board->setId(123);
diff --git a/tests/unit/Service/BoardServiceTest.php b/tests/unit/Service/BoardServiceTest.php
index 6553a6245..ec4920c10 100644
--- a/tests/unit/Service/BoardServiceTest.php
+++ b/tests/unit/Service/BoardServiceTest.php
@@ -36,6 +36,7 @@ use OCA\Deck\Db\LabelMapper;
use OCA\Deck\Db\StackMapper;
use OCA\Deck\NoPermissionException;
use OCA\Deck\Notification\NotificationHelper;
+use OCP\IConfig;
use OCP\IUser;
use OCP\IUserManager;
use OCP\IGroupManager;
@@ -80,6 +81,7 @@ class BoardServiceTest extends TestCase {
$this->aclMapper = $this->createMock(AclMapper::class);
$this->boardMapper = $this->createMock(BoardMapper::class);
$this->stackMapper = $this->createMock(StackMapper::class);
+ $this->config = $this->createMock(IConfig::class);
$this->labelMapper = $this->createMock(LabelMapper::class);
$this->permissionService = $this->createMock(PermissionService::class);
$this->notificationHelper = $this->createMock(NotificationHelper::class);
@@ -93,6 +95,7 @@ class BoardServiceTest extends TestCase {
$this->service = new BoardService(
$this->boardMapper,
$this->stackMapper,
+ $this->config,
$this->l10n,
$this->labelMapper,
$this->aclMapper,