Compare commits

...

49 Commits

Author SHA1 Message Date
Julius Härtl
31eb8d6698 Bump version to 1.4.4
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-09-09 14:25:50 +02:00
Julius Härtl
40967a4ee6 Merge pull request #3307 from nextcloud/backport/3299/stable1.4 2021-09-08 18:42:20 +02:00
Julius Härtl
bfe9b05d69 Return false instead of throwing when getting calendar integration setting
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-09-08 18:31:16 +02:00
Julius Härtl
82e3400162 Merge pull request #3304 from nextcloud/backport/3298/stable1.4
[stable1.4] Delete file shares through attachments API
2021-09-08 18:14:31 +02:00
Julius Härtl
a886b4ee78 Delete file shares through attachments API
Previously the file was deleted in the file structure of the user is not
expected as the file might not only be related to the card.

Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-09-08 17:58:01 +02:00
Julius Härtl
618fb50618 Merge pull request #3301 from nextcloud/backport/3294/stable1.4
[stable1.4] Fix print style issues
2021-09-07 13:14:53 +02:00
Michael Weimann
f7aae7912d fix print style issues
Signed-off-by: Michael Weimann <mail@michael-weimann.eu>
2021-09-06 13:58:44 +00:00
Julius Härtl
2976604b7b Merge pull request #3227 from nextcloud/backport/3217/stable1.4
[stable1.4] Additional circle level check
2021-08-04 18:41:06 +02:00
Julius Härtl
bbe482586b Check circle level
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-08-03 18:20:18 +02:00
Julius Härtl
ff61238487 Pin CI to mariadb 10.5
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-08-03 18:18:52 +02:00
Julius Härtl
9e2dcb686f Bump version to 1.4.3
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-07-09 12:05:19 +02:00
Julius Härtl
fcc96ca98d Merge pull request #3169 from nextcloud/backport/3161/stable1.4
[stable1.4] Reduce duplicate queries when fetching user boards an permissions
2021-07-06 07:54:18 +02:00
Julius Härtl
a43cee8a5d Reduce duplicate queries when fetching user boards an permissions
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-07-05 18:28:01 +00:00
Julius Härtl
f4ccc506af Merge pull request #3164 from nextcloud/backport/3151/stable1.4
[stable1.4] Always log generic exceptions
2021-07-05 16:21:19 +02:00
Julius Härtl
fee49f3699 Always log generic exceptions
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-07-02 15:55:32 +00:00
Julius Härtl
d43c7a48cc Merge pull request #3153 from nextcloud/backport/3152/stable1.4
[stable1.4] Only offer stack creation in emptycontent with proper permissions
2021-06-25 15:56:36 +02:00
Julius Härtl
c0fad295b5 Only offer stack creation in emptycontent with proper permissions
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-06-25 13:38:48 +00:00
Julius Härtl
cb1314f067 Merge pull request #3143 from nextcloud/backport/3142/stable1.4
[stable1.4] Always pass user id in share provider
2021-06-21 13:31:49 +02:00
Julius Härtl
ba68e4c2f7 Always pass user id in share provider
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-06-21 07:34:30 +00:00
Julius Härtl
bd8fd6a66b Bump version to 1.4.2
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-05-03 14:10:54 +02:00
Julius Härtl
0eba8d0840 Merge pull request #3040 from nextcloud/backport/3038/stable1.4
[stable1.4] Get attachment from the user node instead of the share source
2021-05-03 09:02:33 -01:00
Julius Härtl
8fc95dc40d Merge pull request #3039 from nextcloud/backport/3037/stable1.4
[stable1.4] Catch any error during circle detail fetching
2021-05-03 09:02:26 -01:00
Julius Härtl
ecd3e25588 Get attachment from the user node instead of the share source
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-05-03 09:44:20 +00:00
Julius Härtl
914f912612 Catch any error during circle detail fetching
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-05-03 11:43:43 +02:00
Christoph Wurst
e68f723095 Merge pull request #3031 from nextcloud/backport/3016/stable1.4
[stable1.4] Allow searching for filters without a query to match all that have a given filter set
2021-04-30 15:30:21 +02:00
Julius Härtl
5f71be2e7f Add test case for special characters
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-04-30 13:13:39 +00:00
Julius Härtl
bc2a72f035 Catch canceled requests and show better loading indication
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-04-30 13:13:39 +00:00
Julius Härtl
cf4be82827 Fix handling of quotes in queries
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-04-30 13:13:39 +00:00
Julius Härtl
23580705aa Allow searching for filters without a query to match all that have a given filter set
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-04-30 13:13:39 +00:00
Christoph Wurst
65c8c394a8 Merge pull request #3030 from nextcloud/backport/3014
Proper error handling when fetching comments fails
2021-04-30 15:12:10 +02:00
Julius Härtl
422788a6a3 Show comment counter and highlight if unread comments are available
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-04-30 10:39:22 +02:00
Julius Härtl
2d5e29de5d Allow to cancel repies and adapt comment ui to talk
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-04-30 10:39:22 +02:00
Julius Härtl
2a307b92a7 Wrap lines properly in comment text
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-04-30 10:38:09 +02:00
Julius Härtl
2d8dbc70ad Proper error handling when fetching comments fails
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-04-30 10:37:54 +02:00
Julius Härtl
cfee259b38 Bump version to 1.4.1
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-04-20 11:34:48 +02:00
Julius Härtl
f94cdb3ebb Merge pull request #2994 from nextcloud/backport/2950/stable1.4 2021-04-20 07:44:48 -01:00
Julius Härtl
1ed50fdca6 Fix tests
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-04-20 10:17:39 +02:00
Julius Härtl
56e460004f Filter out current user when emitting share notifications to groups
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-04-20 10:17:39 +02:00
Julius Härtl
a95f78d188 Remove notification on unshare/unassign and add type hints
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-04-20 10:17:38 +02:00
Julius Härtl
df09a9a7b2 Remove app code check
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-04-20 10:16:53 +02:00
Julius Härtl
990ee2aef9 Merge pull request #3008 from nextcloud/backport/3005/stable1.4
[stable1.4] Do not query the lookupserver when looking for sharees
2021-04-19 10:38:34 -01:00
Julius Härtl
486ecd12db Merge pull request #3006 from nextcloud/backport/3003/stable1.4
[stable1.4] Only import debounce
2021-04-19 09:02:42 -01:00
Julius Härtl
c9cdd7bb11 Do not query the lookupserver when looking for sharees
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-04-19 10:02:05 +00:00
Julius Härtl
2c753fd084 Only import debounce
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-04-19 09:22:50 +00:00
Julius Härtl
79d2d2f3f5 Merge pull request #2990 from nextcloud/backport/2989/stable1.4 2021-04-16 13:26:08 -01:00
Julius Härtl
24d9b55bfc Cast column when comparing comment object_id with the card id
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-04-16 13:54:19 +00:00
Julius Härtl
28cd9fcf77 Add test for unified comments search
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-04-16 13:54:19 +00:00
Christoph Wurst
d8a36f0602 Merge pull request #2984 from nextcloud/backport/2983/stable1.4
[stable1.4] Fix codemirror description width
2021-04-14 20:39:03 +02:00
Julius Härtl
de06033dcd Fix codemirror description width
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-04-14 18:08:19 +00:00
49 changed files with 662 additions and 277 deletions

View File

@@ -1,55 +0,0 @@
name: Nextcloud app code check
on:
pull_request:
push:
branches:
- master
- stable*
env:
APP_NAME: deck
jobs:
unit-tests:
runs-on: ubuntu-latest
strategy:
matrix:
php-versions: ['7.4']
server-versions: ['master', 'stable18', 'stable19', 'stable20']
name: AppCode check php${{ matrix.php-versions }}-${{ matrix.server-versions }}
steps:
- name: Checkout server
uses: actions/checkout@v2
with:
repository: nextcloud/server
ref: ${{ matrix.server-versions }}
- name: Checkout submodules
shell: bash
run: |
auth_header="$(git config --local --get http.https://github.com/.extraheader)"
git submodule sync --recursive
git -c "http.extraheader=$auth_header" -c protocol.version=2 submodule update --init --force --recursive --depth=1
- name: Checkout app
uses: actions/checkout@v2
with:
path: apps/${{ env.APP_NAME }}
- name: Set up php ${{ matrix.php-versions }}
uses: shivammathur/setup-php@v1
with:
php-version: ${{ matrix.php-versions }}
tools: phpunit
extensions: mbstring, iconv, fileinfo, intl, sqlite, pdo_sqlite
- name: Checkout app
uses: actions/checkout@v2
with:
path: apps/${{ env.APP_NAME }}
- name: App code check
run: php occ app:check-code ${{ env.APP_NAME }}

View File

@@ -34,7 +34,7 @@ jobs:
POSTGRES_DB: nextcloud
options: --health-cmd pg_isready --health-interval 5s --health-timeout 2s --health-retries 5
mysql:
image: mariadb
image: mariadb:10.5
ports:
- 4444:3306/tcp
env:

View File

@@ -35,7 +35,7 @@ jobs:
POSTGRES_DB: nextcloud
options: --health-cmd pg_isready --health-interval 5s --health-timeout 2s --health-retries 5
mysql:
image: mariadb
image: mariadb:10.5
ports:
- 4444:3306/tcp
env:

View File

@@ -1,6 +1,45 @@
# Changelog
All notable changes to this project will be documented in this file.
## 1.4.4
### Fixed
- #3301 Fix print style issues
- #3307 Return false instead of throwing when getting calendar setting
- #3227 Additional circle level check
- #3304 Delete file shares through attachments API
## 1.4.3 - 2021-07-09
### Fixed
* [#3143](https://github.com/nextcloud/deck/pull/3143) Always pass user id in share provider
* [#3153](https://github.com/nextcloud/deck/pull/3153) Only offer stack creation in emptycontent with proper permissions
* [#3164](https://github.com/nextcloud/deck/pull/3164) Always log generic exceptions
* [#3169](https://github.com/nextcloud/deck/pull/3169) Reduce duplicate queries when fetching user boards an permissions
## 1.4.2 - 2021-05-03
### Fixed
* [#3030](https://github.com/nextcloud/deck/pull/3030) Proper error handling when fetching comments fails
* [#3031](https://github.com/nextcloud/deck/pull/3031) Allow searching for filters without a query to match all that have a given filter set
* [#3039](https://github.com/nextcloud/deck/pull/3039) Catch any error during circle detail fetching
* [#3040](https://github.com/nextcloud/deck/pull/3040) Get attachment from the user node instead of the share source
## 1.4.1 - 2021-04-20
### Fixed
* [#2984](https://github.com/nextcloud/deck/pull/2984) Fix codemirror description width
* [#2990](https://github.com/nextcloud/deck/pull/2990) Fix unified comments search with postgres
* [#2994](https://github.com/nextcloud/deck/pull/2994) Remove notification on unshare and add type hints
* [#3006](https://github.com/nextcloud/deck/pull/3006) Only import debounce
* [#3008](https://github.com/nextcloud/deck/pull/3008) Do not query the lookupserver when looking for sharees
## 1.4.0 - 2021-04-13
### Added

View File

@@ -1,6 +1,5 @@
<?xml version="1.0"?>
<info xmlns:xsi= "http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://apps.nextcloud.com/schema/apps/info.xsd">
<?xml version="1.0" encoding="utf-8"?>
<info xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://apps.nextcloud.com/schema/apps/info.xsd">
<id>deck</id>
<name>Deck</name>
<summary>Personal planning and team project organization</summary>
@@ -17,12 +16,12 @@
- 🚀 Get your project organized
</description>
<version>1.4.0</version>
<version>1.4.4</version>
<licence>agpl</licence>
<author>Julius Härtl</author>
<namespace>Deck</namespace>
<types>
<dav />
<dav/>
</types>
<category>organization</category>
<category>office</category>
@@ -36,7 +35,7 @@
<database min-version="9.4">pgsql</database>
<database>sqlite</database>
<database min-version="5.5">mysql</database>
<nextcloud min-version="21" max-version="22" />
<nextcloud min-version="21" max-version="21"/>
</dependencies>
<background-jobs>
<job>OCA\Deck\Cron\DeleteCron</job>

View File

@@ -22,7 +22,9 @@
.icon-activity {
@include icon-color('activity-dark', 'activity', $color-black);
}
.icon-comment--unread {
@include icon-color('comment', 'actions', $color-primary, 1, true);
}
.avatardiv.circles {
background: var(--color-primary);

View File

@@ -2,21 +2,26 @@
/* hide stuff */
#body-user {
#header,
div#app-navigation,
div.board-header-controls,
.app-navigation,
.app-sidebar,
.board-header-controls,
.board-actions,
#app-navigation-toggle,
#app-navigation-toggle-custom,
div#controls.ng-scope div.crumb:not(.title),
div#controls.ng-scope div.crumb a.bullet,
a.ng-binding + a,
div.card.create,
.stack__header .action-item,
button.card-options {
display: none !important;
}
#content {
margin: 0;
padding: 0;
}
#app-content {
margin: 0 !important;
}
@@ -75,6 +80,11 @@
margin: 2cm;
}
.board {
max-height: none !important;
overflow: visible !important;
}
div#innerBoard {
display:flex;
flex-wrap: wrap;

View File

@@ -959,6 +959,7 @@ For now only `deck_file` is supported as an attachment type.
### DELETE /boards/{boardId}/stacks/{stackId}/cards/{cardId}/attachments/{attachmentId} - Delete an attachment
#### Request parameters
| Parameter | Type | Description |

View File

@@ -23,11 +23,12 @@
namespace OCA\Deck\Db;
use OC\Cache\CappedMemoryCache;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\IDBConnection;
use OCP\ILogger;
use OCP\IUserManager;
use OCP\IGroupManager;
use Psr\Log\LoggerInterface;
class BoardMapper extends DeckMapper implements IPermissionMapper {
private $labelMapper;
@@ -35,16 +36,20 @@ class BoardMapper extends DeckMapper implements IPermissionMapper {
private $stackMapper;
private $userManager;
private $groupManager;
private $logger;
private $circlesEnabled;
private $userBoardCache;
public function __construct(
IDBConnection $db,
LabelMapper $labelMapper,
AclMapper $aclMapper,
StackMapper $stackMapper,
IUserManager $userManager,
IGroupManager $groupManager
IGroupManager $groupManager,
LoggerInterface $logger
) {
parent::__construct($db, 'deck_boards', Board::class);
$this->labelMapper = $labelMapper;
@@ -52,6 +57,10 @@ class BoardMapper extends DeckMapper implements IPermissionMapper {
$this->stackMapper = $stackMapper;
$this->userManager = $userManager;
$this->groupManager = $groupManager;
$this->logger = $logger;
$this->userBoardCache = new CappedMemoryCache();
$this->circlesEnabled = \OC::$server->getAppManager()->isEnabledForUser('circles');
}
@@ -86,13 +95,21 @@ class BoardMapper extends DeckMapper implements IPermissionMapper {
}
public function findAllForUser(string $userId, int $since = -1, $includeArchived = true): array {
$groups = $this->groupManager->getUserGroupIds(
$this->userManager->get($userId)
);
$userBoards = $this->findAllByUser($userId, null, null, $since, $includeArchived);
$groupBoards = $this->findAllByGroups($userId, $groups,null, null, $since, $includeArchived);
$circleBoards = $this->findAllByCircles($userId, null, null, $since, $includeArchived);
return array_unique(array_merge($userBoards, $groupBoards, $circleBoards));
$useCache = ($since === -1 && $includeArchived === true);
if (!isset($this->userBoardCache[$userId]) || !$useCache) {
$groups = $this->groupManager->getUserGroupIds(
$this->userManager->get($userId)
);
$userBoards = $this->findAllByUser($userId, null, null, $since, $includeArchived);
$groupBoards = $this->findAllByGroups($userId, $groups, null, null, $since, $includeArchived);
$circleBoards = $this->findAllByCircles($userId, null, null, $since, $includeArchived);
$allBoards = array_unique(array_merge($userBoards, $groupBoards, $circleBoards));
if ($useCache) {
$this->userBoardCache[$userId] = $allBoards;
}
return $allBoards;
}
return $this->userBoardCache[$userId];
}
/**
@@ -248,7 +265,7 @@ class BoardMapper extends DeckMapper implements IPermissionMapper {
if ($user !== null) {
return new User($user);
}
\OC::$server->getLogger()->debug('User ' . $acl->getId() . ' not found when mapping acl ' . $acl->getParticipant());
$this->logger->debug('User ' . $acl->getId() . ' not found when mapping acl ' . $acl->getParticipant());
return null;
}
if ($acl->getType() === Acl::PERMISSION_TYPE_GROUP) {
@@ -256,7 +273,7 @@ class BoardMapper extends DeckMapper implements IPermissionMapper {
if ($group !== null) {
return new Group($group);
}
\OC::$server->getLogger()->debug('Group ' . $acl->getId() . ' not found when mapping acl ' . $acl->getParticipant());
$this->logger->debug('Group ' . $acl->getId() . ' not found when mapping acl ' . $acl->getParticipant());
return null;
}
if ($acl->getType() === Acl::PERMISSION_TYPE_CIRCLE) {
@@ -268,11 +285,12 @@ class BoardMapper extends DeckMapper implements IPermissionMapper {
if ($circle) {
return new Circle($circle);
}
} catch (\Exception $e) {
} catch (\Throwable $e) {
$this->logger->error('Failed to get circle details when building ACL', ['exception' => $e]);
}
return null;
}
\OC::$server->getLogger()->log(ILogger::WARN, 'Unknown permission type for mapping acl ' . $acl->getId());
$this->logger->warning('Unknown permission type for mapping acl ' . $acl->getId());
return null;
});
}

View File

@@ -49,6 +49,7 @@ class Card extends RelationalEntity {
protected $notified = false;
protected $deletedAt = 0;
protected $commentsUnread = 0;
protected $commentsCount = 0;
protected $relatedStack = null;
protected $relatedBoard = null;
@@ -75,6 +76,7 @@ class Card extends RelationalEntity {
$this->addRelation('attachmentCount');
$this->addRelation('participants');
$this->addRelation('commentsUnread');
$this->addRelation('commentsCount');
$this->addResolvable('owner');
$this->addRelation('relatedStack');

View File

@@ -321,7 +321,7 @@ class CardMapper extends QBMapper implements IPermissionMapper {
$this->extendQueryByFilter($qb, $query);
$qb->innerJoin('c', 'comments', 'comments', $qb->expr()->andX(
$qb->expr()->eq('comments.object_id', 'c.id', IQueryBuilder::PARAM_STR),
$qb->expr()->eq('comments.object_id', $qb->expr()->castColumn('c.id', IQueryBuilder::PARAM_STR)),
$qb->expr()->eq('comments.object_type', $qb->createNamedParameter(Application::COMMENT_ENTITY_TYPE, IQueryBuilder::PARAM_STR))
));
$qb->selectAlias('comments.id', 'comment_id');
@@ -339,7 +339,7 @@ class CardMapper extends QBMapper implements IPermissionMapper {
$tokenMatching
);
$qb->groupBy('comments.id');
$qb->groupBy('comments.id', 'c.id');
$qb->orderBy('comments.id', 'DESC');
if ($limit !== null) {
$qb->setMaxResults($limit);
@@ -383,6 +383,10 @@ class CardMapper extends QBMapper implements IPermissionMapper {
foreach ($query->getDuedate() as $duedate) {
$dueDateColumn = $this->databaseType === 'sqlite3' ? $qb->createFunction('DATETIME(`c`.`duedate`)') : 'c.duedate';
$date = $duedate->getValue();
if ($date === "") {
$qb->andWhere($qb->expr()->isNotNull('c.duedate'));
continue;
}
$supportedFilters = ['overdue', 'today', 'week', 'month', 'none'];
if (in_array($date, $supportedFilters, true)) {
$currentDate = new DateTime();
@@ -430,6 +434,10 @@ class CardMapper extends QBMapper implements IPermissionMapper {
foreach ($query->getAssigned() as $index => $assignment) {
$qb->innerJoin('c', 'deck_assigned_users', 'au' . $index, $qb->expr()->eq('c.id', 'au' . $index . '.card_id'));
$assignedQueryValue = $assignment->getValue();
if ($assignedQueryValue === "") {
$qb->andWhere($qb->expr()->isNotNull('au' . $index . '.participant'));
continue;
}
$searchUsers = $this->userManager->searchDisplayName($assignment->getValue());
$users = array_filter($searchUsers, function (IUser $user) use ($assignedQueryValue) {
return (mb_strtolower($user->getDisplayName()) === mb_strtolower($assignedQueryValue) || $user->getUID() === $assignedQueryValue);

View File

@@ -98,9 +98,7 @@ class ExceptionMiddleware extends Middleware {
'status' => 500,
'message' => $exception->getMessage()
];
if ($this->config->getSystemValue('loglevel', Util::WARN) === Util::DEBUG) {
$this->logger->logException($exception);
}
$this->logger->logException($exception);
if ($this->config->getSystemValue('debug', true) === true) {
$response['exception'] = (array) $exception;
}

View File

@@ -1,4 +1,7 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2017 Julius Härtl <jus@bitgrid.net>
*
@@ -24,19 +27,24 @@
namespace OCA\Deck\Notification;
use DateTime;
use Exception;
use OCA\Deck\AppInfo\Application;
use OCA\Deck\Db\Acl;
use OCA\Deck\Db\AssignmentMapper;
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\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Db\MultipleObjectsReturnedException;
use OCP\Comments\IComment;
use OCP\IConfig;
use OCP\IGroupManager;
use OCP\Notification\IManager;
use OCP\Notification\INotification;
class NotificationHelper {
@@ -80,10 +88,10 @@ class NotificationHelper {
}
/**
* @param $card
* @throws \OCP\AppFramework\Db\DoesNotExistException
* @throws DoesNotExistException
* @throws Exception thrown on invalid due date
*/
public function sendCardDuedate($card) {
public function sendCardDuedate(Card $card): void {
// check if notification has already been sent
// ideally notifications should not be deleted once seen by the user so we can
// also deliver due date notifications for users who have been added later to a board
@@ -117,7 +125,7 @@ class NotificationHelper {
$notification
->setApp('deck')
->setUser((string)$user->getUID())
->setObject('card', $card->getId())
->setObject('card', (string)$card->getId())
->setSubject('card-overdue', [
$card->getTitle(), $board->getTitle()
])
@@ -128,25 +136,29 @@ class NotificationHelper {
$this->cardMapper->markNotified($card);
}
public function markDuedateAsRead($card) {
public function markDuedateAsRead(Card $card): void {
$notification = $this->notificationManager->createNotification();
$notification
->setApp('deck')
->setObject('card', $card->getId())
->setObject('card', (string)$card->getId())
->setSubject('card-overdue', []);
$this->notificationManager->markProcessed($notification);
}
public function sendCardAssigned($card, $userId) {
public function sendCardAssigned(Card $card, string $userId): void {
$boardId = $this->cardMapper->findBoardId($card->getId());
$board = $this->getBoard($boardId);
try {
$board = $this->getBoard($boardId);
} catch (Exception $e) {
return;
}
$notification = $this->notificationManager->createNotification();
$notification
->setApp('deck')
->setUser((string) $userId)
->setUser($userId)
->setDateTime(new DateTime())
->setObject('card', $card->getId())
->setObject('card', (string)$card->getId())
->setSubject('card-assigned', [
$card->getTitle(),
$board->getTitle(),
@@ -155,29 +167,56 @@ class NotificationHelper {
$this->notificationManager->notify($notification);
}
public function markCardAssignedAsRead(Card $card, string $userId): void {
$notification = $this->notificationManager->createNotification();
$notification
->setApp('deck')
->setUser($userId)
->setObject('card', (string)$card->getId())
->setSubject('card-assigned', []);
$this->notificationManager->markProcessed($notification);
}
/**
* Send notifications that a board was shared with a user/group
*
* @param $boardId
* @param Acl $acl
* @throws \InvalidArgumentException
*/
public function sendBoardShared($boardId, $acl) {
$board = $this->getBoard($boardId);
public function sendBoardShared(int $boardId, Acl $acl, bool $markAsRead = false): void {
try {
$board = $this->getBoard($boardId);
} catch (Exception $e) {
return;
}
if ($acl->getType() === Acl::PERMISSION_TYPE_USER) {
$notification = $this->generateBoardShared($board, $acl->getParticipant());
$this->notificationManager->notify($notification);
if ($markAsRead) {
$this->notificationManager->markProcessed($notification);
} else {
$notification->setDateTime(new DateTime());
$this->notificationManager->notify($notification);
}
}
if ($acl->getType() === Acl::PERMISSION_TYPE_GROUP) {
$group = $this->groupManager->get($acl->getParticipant());
if ($group === null) {
return;
}
foreach ($group->getUsers() as $user) {
if ($user->getUID() === $this->currentUser) {
continue;
}
$notification = $this->generateBoardShared($board, $user->getUID());
$this->notificationManager->notify($notification);
if ($markAsRead) {
$this->notificationManager->markProcessed($notification);
} else {
$notification->setDateTime(new DateTime());
$this->notificationManager->notify($notification);
}
}
}
}
public function sendMention(IComment $comment) {
public function sendMention(IComment $comment): void {
foreach ($comment->getMentions() as $mention) {
$card = $this->cardMapper->find($comment->getObjectId());
$boardId = $this->cardMapper->findBoardId($card->getId());
@@ -194,27 +233,22 @@ class NotificationHelper {
}
/**
* @param $boardId
* @return Board
* @throws \OCP\AppFramework\Db\DoesNotExistException
* @throws DoesNotExistException
* @throws MultipleObjectsReturnedException
*/
private function getBoard($boardId, bool $withLabels = false, bool $withAcl = false) {
private function getBoard(int $boardId, bool $withLabels = false, bool $withAcl = false): Board {
if (!array_key_exists($boardId, $this->boards)) {
$this->boards[$boardId] = $this->boardMapper->find($boardId, $withLabels, $withAcl);
}
return $this->boards[$boardId];
}
/**
* @param Board $board
*/
private function generateBoardShared($board, $userId) {
private function generateBoardShared(Board $board, string $userId): INotification {
$notification = $this->notificationManager->createNotification();
$notification
->setApp('deck')
->setUser((string) $userId)
->setDateTime(new DateTime())
->setObject('board', $board->getId())
->setUser($userId)
->setObject('board', (string)$board->getId())
->setSubject('board-shared', [$board->getTitle(), $this->currentUser]);
return $notification;
}

View File

@@ -37,7 +37,7 @@ class AQueryParameter {
public function getValue() {
if (is_string($this->value) && mb_strlen($this->value) > 1) {
$param = ($this->value[0] === '"' && $this->value[mb_strlen($this->value) - 1] === '"') ? mb_substr($this->value, 1, -1): $this->value;
$param = (mb_substr($this->value, 0, 1) === '"' && mb_substr($this->value, -1, 1) === '"') ? mb_substr($this->value, 1, -1): $this->value;
return $param;
}
return $this->value;

View File

@@ -74,6 +74,8 @@ class AssignmentService {
* @var IEventDispatcher
*/
private $eventDispatcher;
/** @var string|null */
private $currentUser;
public function __construct(
PermissionService $permissionService,
@@ -138,8 +140,7 @@ class AssignmentService {
}
if ($userId !== $this->currentUser) {
/* Notifyuser about the card assignment */
if ($type === Assignment::TYPE_USER && $userId !== $this->currentUser) {
$this->notificationHelper->sendCardAssigned($card, $userId);
}
@@ -183,8 +184,12 @@ class AssignmentService {
$assignment = $this->assignedUsersMapper->delete($assignment);
$card = $this->cardMapper->find($cardId);
$this->activityManager->triggerEvent(ActivityManager::DECK_OBJECT_CARD, $card, ActivityManager::SUBJECT_CARD_USER_UNASSIGN, ['assigneduser' => $userId]);
if ($type === Assignment::TYPE_USER && $userId !== $this->currentUser) {
$this->notificationHelper->markCardAssignedAsRead($card, $userId);
}
$this->changeHelper->cardChanged($cardId);
$this->eventDispatcher->dispatchTyped(new CardUpdatedEvent($card));
return $assignment;

View File

@@ -35,6 +35,7 @@ use OCA\Deck\InvalidAttachmentType;
use OCA\Deck\NoPermissionException;
use OCA\Deck\NotFoundException;
use OCA\Deck\StatusException;
use OCP\AppFramework\Db\IMapperException;
use OCP\AppFramework\Http\Response;
use OCP\ICache;
use OCP\ICacheFactory;
@@ -320,14 +321,10 @@ class AttachmentService {
* Either mark an attachment as deleted for later removal or just remove it depending
* on the IAttachmentService implementation
*
* @param $attachmentId
* @return \OCP\AppFramework\Db\Entity
* @throws \OCA\Deck\NoPermissionException
* @throws \OCP\AppFramework\Db\DoesNotExistException
* @throws \OCP\AppFramework\Db\MultipleObjectsReturnedException
* @throws BadRequestException
* @throws NoPermissionException
* @throws NotFoundException
*/
public function delete($cardId, $attachmentId, $type = 'deck_file') {
public function delete(int $cardId, int $attachmentId, string $type = 'deck_file'): Attachment {
try {
$service = $this->getService($type);
} catch (InvalidAttachmentType $e) {
@@ -340,40 +337,32 @@ class AttachmentService {
$attachment->setType($type);
$attachment->setCardId($cardId);
$service->extendData($attachment);
$service->delete($attachment);
$this->changeHelper->cardChanged($attachment->getCardId());
$this->activityManager->triggerEvent(ActivityManager::DECK_OBJECT_CARD, $attachment, ActivityManager::SUBJECT_ATTACHMENT_DELETE);
return $attachment;
} else {
try {
$attachment = $this->attachmentMapper->find($attachmentId);
} catch (IMapperException $e) {
throw new NoPermissionException('Permission denied');
}
}
try {
$attachment = $this->attachmentMapper->find($attachmentId);
} catch (\Exception $e) {
throw new NoPermissionException('Permission denied');
}
$this->permissionService->checkPermission($this->cardMapper, $attachment->getCardId(), Acl::PERMISSION_EDIT);
$this->cache->clear('card-' . $attachment->getCardId());
if ($service->allowUndo()) {
$service->markAsDeleted($attachment);
$this->activityManager->triggerEvent(ActivityManager::DECK_OBJECT_CARD, $attachment, ActivityManager::SUBJECT_ATTACHMENT_DELETE);
$this->changeHelper->cardChanged($attachment->getCardId());
return $this->attachmentMapper->update($attachment);
$attachment = $this->attachmentMapper->update($attachment);
} else {
$service->delete($attachment);
if (!$service instanceof ICustomAttachmentService) {
$attachment = $this->attachmentMapper->delete($attachment);
}
}
$service->delete($attachment);
$attachment = $this->attachmentMapper->delete($attachment);
$this->cache->clear('card-' . $attachment->getCardId());
$this->changeHelper->cardChanged($attachment->getCardId());
$this->activityManager->triggerEvent(ActivityManager::DECK_OBJECT_CARD, $attachment, ActivityManager::SUBJECT_ATTACHMENT_DELETE);
return $attachment;
}
public function restore($cardId, $attachmentId, $type = 'deck_file') {
if (is_numeric($attachmentId) === false) {
throw new BadRequestException('attachment id must be a number');
}
public function restore(int $cardId, int $attachmentId, string $type = 'deck_file'): Attachment {
try {
$attachment = $this->attachmentMapper->find($attachmentId);
} catch (\Exception $e) {

View File

@@ -510,11 +510,10 @@ class BoardService {
$acl->setPermissionEdit($edit);
$acl->setPermissionShare($share);
$acl->setPermissionManage($manage);
$this->notificationHelper->sendBoardShared($boardId, $acl);
$newAcl = $this->aclMapper->insert($acl);
$this->activityManager->triggerEvent(ActivityManager::DECK_OBJECT_BOARD, $newAcl, ActivityManager::SUBJECT_BOARD_SHARE);
$this->notificationHelper->sendBoardShared((int)$boardId, $acl);
$this->boardMapper->mapAcl($newAcl);
$this->changeHelper->boardChanged($boardId);
@@ -599,9 +598,8 @@ class BoardService {
}
}
$this->notificationHelper->sendBoardShared($acl->getBoardId(), $acl);
$this->activityManager->triggerEvent(ActivityManager::DECK_OBJECT_BOARD, $acl, ActivityManager::SUBJECT_BOARD_UNSHARE);
$this->notificationHelper->sendBoardShared($acl->getBoardId(), $acl, true);
$this->changeHelper->boardChanged($acl->getBoardId());
$version = \OCP\Util::getVersion()[0];

View File

@@ -105,8 +105,10 @@ class CardService {
$card->setAttachmentCount($this->attachmentService->count($cardId));
$user = $this->userManager->get($this->currentUser);
$lastRead = $this->commentsManager->getReadMark('deckCard', (string)$card->getId(), $user);
$count = $this->commentsManager->getNumberOfCommentsForObject('deckCard', (string)$card->getId(), $lastRead);
$card->setCommentsUnread($count);
$countUnreadComments = $this->commentsManager->getNumberOfCommentsForObject('deckCard', (string)$card->getId(), $lastRead);
$countComments = $this->commentsManager->getNumberOfCommentsForObject('deckCard', (string)$card->getId());
$card->setCommentsUnread($countUnreadComments);
$card->setCommentsCount($countComments);
$stack = $this->stackMapper->find($card->getStackId());
$board = $this->boardService->find($stack->getBoardId());
@@ -243,6 +245,7 @@ class CardService {
$this->cardMapper->update($card);
$this->activityManager->triggerEvent(ActivityManager::DECK_OBJECT_CARD, $card, ActivityManager::SUBJECT_CARD_DELETE);
$this->notificationHelper->markDuedateAsRead($card);
$this->changeHelper->cardChanged($card->getId(), false);
$this->eventDispatcher->dispatchTyped(new CardDeletedEvent($card));
@@ -322,6 +325,15 @@ class CardService {
$card->setOrder($order);
$card->setOwner($owner);
$card->setDuedate($duedate);
$resetDuedateNotification = false;
if (
$card->getDuedate() === null ||
(new \DateTime($card->getDuedate())) != (new \DateTime($changes->getBefore()->getDuedate()))
) {
$card->setNotified(false);
$resetDuedateNotification = true;
}
if ($deletedAt !== null) {
$card->setDeletedAt($deletedAt);
}
@@ -341,6 +353,9 @@ class CardService {
$card = $this->cardMapper->update($card);
if ($resetDuedateNotification) {
$this->notificationHelper->markDuedateAsRead($card);
}
$this->changeHelper->cardChanged($card->getId(), true);
$this->eventDispatcher->dispatchTyped(new CardUpdatedEvent($card));

View File

@@ -26,6 +26,7 @@ declare(strict_types=1);
namespace OCA\Deck\Service;
use OCA\Circles\Api\v1\Circles;
use OCP\App\IAppManager;
/**
@@ -53,8 +54,8 @@ class CirclesService {
}
try {
\OCA\Circles\Api\v1\Circles::getMember($circleId, $userId, 1, true);
return true;
$member = \OCA\Circles\Api\v1\Circles::getMember($circleId, $userId, 1, true);
return $member->getLevel() >= Circles::LEVEL_MEMBER;
} catch (\Exception $e) {
}
return false;

View File

@@ -73,16 +73,20 @@ class ConfigService {
if (!$this->groupManager->isAdmin($this->userId)) {
throw new NoPermissionException('You must be admin to get the group limit');
}
$result = $this->getGroupLimit();
break;
return $this->getGroupLimit();
case 'calendar':
$result = (bool)$this->config->getUserValue($this->userId, Application::APP_ID, 'calendar', true);
break;
if ($this->userId === null) {
return false;
}
return (bool)$this->config->getUserValue($this->userId, Application::APP_ID, 'calendar', true);
}
return $result;
}
public function isCalendarEnabled(int $boardId = null): bool {
if ($this->userId === null) {
return false;
}
$defaultState = (bool)$this->config->getUserValue($this->userId, Application::APP_ID, 'calendar', true);
if ($boardId === null) {
return $defaultState;

View File

@@ -23,7 +23,10 @@
namespace OCA\Deck\Service;
use OCA\Deck\Db\Acl;
use OCA\Deck\Db\Attachment;
use OCA\Deck\Db\CardMapper;
use OCA\Deck\NoPermissionException;
use OCA\Deck\Sharing\DeckShareProvider;
use OCA\Deck\StatusException;
use OCP\AppFramework\Http\StreamResponse;
@@ -38,6 +41,7 @@ use OCP\IRequest;
use OCP\Share\Exceptions\ShareNotFound;
use OCP\Share\IManager;
use OCP\Share\IShare;
use Psr\Log\LoggerInterface;
class FilesAppService implements IAttachmentService, ICustomAttachmentService {
private $request;
@@ -48,8 +52,10 @@ class FilesAppService implements IAttachmentService, ICustomAttachmentService {
private $configService;
private $l10n;
private $preview;
private $permissionService;
private $mimeTypeDetector;
private $permissionService;
private $cardMapper;
private $logger;
public function __construct(
IRequest $request,
@@ -59,8 +65,10 @@ class FilesAppService implements IAttachmentService, ICustomAttachmentService {
ConfigService $configService,
DeckShareProvider $shareProvider,
IPreview $preview,
PermissionService $permissionService,
IMimeTypeDetector $mimeTypeDetector,
PermissionService $permissionService,
CardMapper $cardMapper,
LoggerInterface $logger,
string $userId = null
) {
$this->request = $request;
@@ -72,15 +80,20 @@ class FilesAppService implements IAttachmentService, ICustomAttachmentService {
$this->userId = $userId;
$this->preview = $preview;
$this->mimeTypeDetector = $mimeTypeDetector;
$this->permissionService = $permissionService;
$this->cardMapper = $cardMapper;
$this->logger = $logger;
}
public function listAttachments(int $cardId): array {
$shares = $this->shareProvider->getSharedWithByType($cardId, IShare::TYPE_DECK, -1, 0);
$shares = array_filter($shares, function ($share) {
return $share->getPermissions() > 0;
});
return array_map(function (IShare $share) use ($cardId) {
$file = $share->getNode();
return array_filter(array_map(function (IShare $share) use ($cardId) {
try {
$file = $share->getNode();
} catch (NotFoundException $e) {
$this->logger->debug('Unable to find node for share with ID ' . $share->getId());
return null;
}
$attachment = new Attachment();
$attachment->setType('file');
$attachment->setId((int)$share->getId());
@@ -89,9 +102,9 @@ class FilesAppService implements IAttachmentService, ICustomAttachmentService {
$attachment->setData($file->getName());
$attachment->setLastModified($file->getMTime());
$attachment->setCreatedAt($share->getShareTime()->getTimestamp());
$attachment->setDeletedAt(0);
$attachment->setDeletedAt($share->getPermissions() === 0 ? $share->getShareTime()->getTimestamp() : 0);
return $attachment;
}, $shares);
}, $shares));
}
public function getAttachmentCount(int $cardId): int {
@@ -125,7 +138,11 @@ class FilesAppService implements IAttachmentService, ICustomAttachmentService {
public function extendData(Attachment $attachment) {
$userFolder = $this->rootFolder->getUserFolder($this->userId);
$share = $this->shareProvider->getShareById($attachment->getId());
$file = $share->getNode();
$files = $userFolder->getById($share->getNode()->getId());
if (count($files) === 0) {
return $attachment;
}
$file = array_shift($files);
$attachment->setExtendedData([
'path' => $userFolder->getRelativePath($file->getPath()),
'fileid' => $file->getId(),
@@ -140,6 +157,7 @@ class FilesAppService implements IAttachmentService, ICustomAttachmentService {
}
public function display(Attachment $attachment) {
// Problem: Folders
/** @psalm-suppress InvalidCatch */
try {
$share = $this->shareProvider->getShareById($attachment->getId());
@@ -161,6 +179,9 @@ class FilesAppService implements IAttachmentService, ICustomAttachmentService {
$file = $this->getUploadedFile();
$fileName = $file['name'];
// get shares for current card
// check if similar filename already exists
$userFolder = $this->rootFolder->getUserFolder($this->userId);
try {
$folder = $userFolder->get($this->configService->getAttachmentFolder());
@@ -241,12 +262,16 @@ class FilesAppService implements IAttachmentService, ICustomAttachmentService {
$file = $share->getNode();
$attachment->setData($file->getName());
if ($file->getOwner() !== null && $file->getOwner()->getUID() === $this->userId) {
$file->delete();
// Deleting a Nextcloud file attachment will remove the share to the card, keeping the source file untouched
// Opt-out of individual shares per user is no longer performed within deck but can still be done through the files app
$canEdit = $this->permissionService->checkPermission($this->cardMapper, $attachment->getCardId(), Acl::PERMISSION_EDIT);
$isFileOwner = $file->getOwner() !== null && $file->getOwner()->getUID() === $this->userId;
if ($isFileOwner || $canEdit) {
$this->shareManager->deleteShare($share);
return;
}
$this->shareManager->deleteFromSelf($share, $this->userId);
throw new NoPermissionException('No permission to remove the attachment from the card');
}
public function allowUndo() {

View File

@@ -23,6 +23,8 @@
namespace OCA\Deck\Service;
use OC\Cache\CappedMemoryCache;
use OCA\Circles\Model\Member;
use OCA\Deck\Db\Acl;
use OCA\Deck\Db\AclMapper;
use OCA\Deck\Db\Board;
@@ -31,7 +33,6 @@ use OCA\Deck\Db\IPermissionMapper;
use OCA\Deck\Db\User;
use OCA\Deck\NoPermissionException;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Db\Entity;
use OCP\AppFramework\Db\MultipleObjectsReturnedException;
use OCP\IConfig;
use OCP\IGroupManager;
@@ -61,6 +62,7 @@ class PermissionService {
private $users = [];
private $circlesEnabled = false;
private $boardCache;
public function __construct(
ILogger $logger,
@@ -81,6 +83,8 @@ class PermissionService {
$this->config = $config;
$this->userId = $userId;
$this->boardCache = new CappedMemoryCache();
$this->circlesEnabled = \OC::$server->getAppManager()->isEnabledForUser('circles') &&
(version_compare(\OC::$server->getAppManager()->getAppVersion('circles'), '0.17.1') >= 0);
}
@@ -149,10 +153,13 @@ class PermissionService {
return true;
}
$acls = $this->aclMapper->findAll($boardId);
$result = $this->userCan($acls, $permission, $userId);
if ($result) {
return true;
try {
$acls = $this->getBoard($boardId)->getAcl();
$result = $this->userCan($acls, $permission, $userId);
if ($result) {
return true;
}
} catch (DoesNotExistException | MultipleObjectsReturnedException $e) {
}
// Throw NoPermission to not leak information about existing entries
@@ -168,13 +175,24 @@ class PermissionService {
$userId = $this->userId;
}
try {
$board = $this->boardMapper->find($boardId);
return $board && $userId === $board->getOwner();
$board = $this->getBoard($boardId);
return $userId === $board->getOwner();
} catch (DoesNotExistException | MultipleObjectsReturnedException $e) {
}
return false;
}
/**
* @throws MultipleObjectsReturnedException
* @throws DoesNotExistException
*/
private function getBoard($boardId): Board {
if (!isset($this->boardCache[$boardId])) {
$this->boardCache[$boardId] = $this->boardMapper->find($boardId, false, true);
}
return $this->boardCache[$boardId];
}
/**
* Check if permission matches the acl rules for current user and groups
*
@@ -194,8 +212,8 @@ class PermissionService {
if ($this->circlesEnabled && $acl->getType() === Acl::PERMISSION_TYPE_CIRCLE) {
try {
\OCA\Circles\Api\v1\Circles::getMember($acl->getParticipant(), $this->userId, 1, true);
return $acl->getPermission($permission);
$member = \OCA\Circles\Api\v1\Circles::getMember($acl->getParticipant(), $this->userId, 1, true);
return $member->getLevel() >= Member::LEVEL_MEMBER && $acl->getPermission($permission);
} catch (\Exception $e) {
$this->logger->info('Member not found in circle that was accessed. This should not happen.');
}

View File

@@ -271,9 +271,9 @@ class DeckShareProvider implements \OCP\Share\IShareProvider {
return $share;
}
private function applyBoardPermission($share, $permissions) {
private function applyBoardPermission($share, $permissions, $userId) {
try {
$this->permissionService->checkPermission($this->cardMapper, $share->getSharedWith(), Acl::PERMISSION_EDIT);
$this->permissionService->checkPermission($this->cardMapper, $share->getSharedWith(), Acl::PERMISSION_EDIT, $userId);
} catch (NoPermissionException $e) {
$permissions &= Constants::PERMISSION_ALL - Constants::PERMISSION_UPDATE;
$permissions &= Constants::PERMISSION_ALL - Constants::PERMISSION_CREATE;
@@ -281,7 +281,7 @@ class DeckShareProvider implements \OCP\Share\IShareProvider {
}
try {
$this->permissionService->checkPermission($this->cardMapper, $share->getSharedWith(), Acl::PERMISSION_SHARE);
$this->permissionService->checkPermission($this->cardMapper, $share->getSharedWith(), Acl::PERMISSION_SHARE, $userId);
} catch (NoPermissionException $e) {
$permissions &= Constants::PERMISSION_ALL - Constants::PERMISSION_SHARE;
}
@@ -646,7 +646,7 @@ class DeckShareProvider implements \OCP\Share\IShareProvider {
$stmt = $query->execute();
while ($data = $stmt->fetch()) {
$this->applyBoardPermission($shareMap[$data['parent']], (int)$data['permissions']);
$this->applyBoardPermission($shareMap[$data['parent']], (int)$data['permissions'], $userId);
$shareMap[$data['parent']]->setTarget($data['file_target']);
}

View File

@@ -1,7 +1,7 @@
{
"name": "deck",
"description": "",
"version": "1.0.0",
"version": "1.4.4",
"authors": [
{
"name": "Julius Härtl",
@@ -129,4 +129,4 @@
"<rootDir>/node_modules/jest-serializer-vue"
]
}
}
}

View File

@@ -32,7 +32,7 @@
</div>
<EmptyContent v-else-if="isEmpty" key="empty" icon="icon-deck">
{{ t('deck', 'No lists available') }}
<template #desc>
<template v-if="canManage" #desc>
{{ t('deck', 'Create a new list to add cards to this board') }}
<form @submit.prevent="addNewStack()">
<input id="new-stack-input-main"
@@ -110,6 +110,7 @@ export default {
}),
...mapGetters([
'canEdit',
'canManage',
]),
stacksByBoard() {
return this.$store.getters.stacksByBoard(this.board.id)

View File

@@ -73,7 +73,7 @@ import { CollectionList } from 'nextcloud-vue-collections'
import { mapGetters, mapState } from 'vuex'
import { getCurrentUser } from '@nextcloud/auth'
import { showError } from '@nextcloud/dialogs'
import { debounce } from 'lodash'
import debounce from 'lodash/debounce'
export default {
name: 'SharingTabSidebar',

View File

@@ -22,7 +22,7 @@
<template>
<AttachmentDragAndDrop :card-id="cardId" class="drop-upload--sidebar">
<div class="button-group">
<div class="button-group" v-if="!isReadOnly">
<button class="icon-upload" @click="uploadNewFile()">
{{ t('deck', 'Upload new files') }}
</button>
@@ -49,18 +49,25 @@
</li>
<li v-for="attachment in attachments"
:key="attachment.id"
class="attachment">
class="attachment"
:class="{ 'attachment--deleted': attachment.deletedAt > 0 }">
<a class="fileicon"
:href="internalLink(attachment)"
:style="mimetypeForAttachment(attachment)"
@click.prevent="showViewer(attachment)" />
<div class="details">
<a @click.prevent="showViewer(attachment)">
<a :href="internalLink(attachment)" @click.prevent="showViewer(attachment)">
<div class="filename">
<span class="basename">{{ attachment.data }}</span>
</div>
<span class="filesize">{{ formattedFileSize(attachment.extendedData.filesize) }}</span>
<span class="filedate">{{ relativeDate(attachment.createdAt*1000) }}</span>
<span class="filedate">{{ attachment.createdBy }}</span>
<div v-if="attachment.deletedAt === 0">
<span class="filesize">{{ formattedFileSize(attachment.extendedData.filesize) }}</span>
<span class="filedate">{{ relativeDate(attachment.createdAt*1000) }}</span>
<span class="filedate">{{ attachment.createdBy }}</span>
</div>
<div v-else>
<span class="attachment--info">{{ t('deck', 'Pending share') }}</span>
</div>
</a>
</div>
<Actions v-if="selectable">
@@ -68,12 +75,12 @@
{{ t('deck', 'Add this attachment') }}
</ActionButton>
</Actions>
<Actions v-if="removable" :force-menu="true">
<Actions v-if="removable && !isReadOnly" :force-menu="true">
<ActionLink v-if="attachment.extendedData.fileid" icon="icon-folder" :href="internalLink(attachment)">
{{ t('deck', 'Show in Files') }}
</ActionLink>
<ActionButton v-if="attachment.extendedData.fileid" icon="icon-delete" @click="unshareAttachment(attachment)">
{{ t('deck', 'Unshare file') }}
<ActionButton v-if="attachment.extendedData.fileid && !isReadOnly" icon="icon-delete" @click="unshareAttachment(attachment)">
{{ t('deck', 'Remove attachment') }}
</ActionButton>
<ActionButton v-if="!attachment.extendedData.fileid && attachment.deletedAt === 0" icon="icon-delete" @click="$emit('deleteAttachment', attachment)">
@@ -143,6 +150,7 @@ export default {
},
computed: {
attachments() {
// FIXME sort propertly by last modified / deleted at
return [...this.$store.getters.attachmentsByCard(this.cardId)].filter(attachment => attachment.deletedAt >= 0).sort((a, b) => b.id - a.id)
},
mimetypeForAttachment() {
@@ -320,9 +328,10 @@ export default {
opacity: 0.7;
}
}
.attachment--info,
.filesize, .filedate {
font-size: 90%;
color: darkgray;
color: var(--color-text-maxcontrast);
}
.app-popover-menu-utils {
position: relative;

View File

@@ -7,7 +7,11 @@
</span>
</div>
<CommentItem v-if="replyTo" :comment="replyTo" :reply="true" />
<CommentItem v-if="replyTo"
:comment="replyTo"
:reply="true"
:preview="true"
@cancel="cancelReply" />
<CommentForm v-model="newComment" @submit="createComment" />
<ul v-if="getCommentsForCard(card.id).length > 0" id="commentsFeed">
@@ -23,8 +27,8 @@
</ul>
<div v-else-if="isLoading" class="icon icon-loading" />
<div v-else class="emptycontent">
<div class="icon-comment" />
<p>{{ t('deck', 'No comments yet. Begin the discussion!') }}</p>
<div :class="{ 'icon-comment': !error, 'icon-error': error }" />
<p>{{ error || t('deck', 'No comments yet. Begin the discussion!') }}</p>
</div>
</div>
</template>
@@ -36,6 +40,7 @@ import CommentItem from './CommentItem'
import CommentForm from './CommentForm'
import InfiniteLoading from 'vue-infinite-loading'
import { getCurrentUser } from '@nextcloud/auth'
export default {
name: 'CardSidebarTabComments',
components: {
@@ -60,6 +65,7 @@ export default {
newComment: '',
isLoading: false,
currentUser: getCurrentUser(),
error: null,
}
},
computed: {
@@ -85,19 +91,34 @@ export default {
},
methods: {
async infiniteHandler($state) {
await this.loadMore()
if (this.hasMoreComments(this.card.id)) {
$state.loaded()
} else {
this.error = null
try {
await this.loadMore()
if (this.hasMoreComments(this.card.id)) {
$state.loaded()
} else {
$state.complete()
}
} catch (e) {
console.error('Failed to fetch more comments during infinite loading', e)
this.error = t('deck', 'Failed to load comments')
$state.complete()
}
},
async loadComments() {
this.$store.dispatch('setReplyTo', null)
this.error = null
this.isLoading = true
await this.$store.dispatch('fetchComments', { cardId: this.card.id })
this.isLoading = false
if (this.card.commentsUnread > 0) {
await this.$store.dispatch('markCommentsAsRead', this.card.id)
try {
await this.$store.dispatch('fetchComments', { cardId: this.card.id })
this.isLoading = false
if (this.card.commentsUnread > 0) {
await this.$store.dispatch('markCommentsAsRead', this.card.id)
}
} catch (e) {
this.isLoading = false
console.error('Failed to fetch more comments during infinite loading', e)
this.error = t('deck', 'Failed to load comments')
}
},
async createComment(content) {
@@ -115,6 +136,9 @@ export default {
await this.$store.dispatch('fetchMore', { cardId: this.card.id })
this.isLoading = false
},
cancelReply() {
this.$store.dispatch('setReplyTo', null)
},
},
}
</script>

View File

@@ -41,6 +41,7 @@
</span>
</template>
<div ref="contentEditable"
class="comment-form__contenteditable"
contenteditable
@keydown.enter="handleKeydown"
@paste="onPaste"
@@ -175,6 +176,11 @@ export default {
<style scoped lang="scss">
@import '../../css/comments';
.comment-form__contenteditable {
word-break: break-word;
border-radius: var(--border-radius-large)
}
.atwho-wrap {
width: 100%;
& > div[contenteditable] {

View File

@@ -1,10 +1,22 @@
<template>
<div v-if="reply" class="reply">
<span class="reply--hint">{{ t('deck', 'In reply to') }} <UserBubble :user="comment.actorId" :display-name="comment.actorDisplayName" /></span>
<RichText class="comment--content"
:text="richText(comment)"
:arguments="richArgs(comment)"
:autolink="true" />
<div v-if="reply" class="reply" :class="{ 'reply--preview': preview }">
<div class="reply--wrapper">
<div class="reply--header">
<div class="reply--hint">
{{ t('deck', 'In reply to') }}
<UserBubble :user="comment.actorId" :display-name="comment.actorDisplayName" />
</div>
<Actions v-if="preview" class="reply--cancel">
<ActionButton icon="icon-close" @click="$emit('cancel')">
{{ t('deck', 'Cancel reply') }}
</ActionButton>
</Actions>
</div>
<RichText class="comment--content"
:text="richText(comment)"
:arguments="richArgs(comment)"
:autolink="true" />
</div>
</div>
<li v-else class="comment">
<template>
@@ -14,13 +26,19 @@
{{ comment.actorDisplayName }}
</span>
<Actions v-show="!edit" :force-menu="true">
<ActionButton icon="icon-reply" @click="replyTo()">
<ActionButton icon="icon-reply" :close-after-click="true" @click="replyTo()">
{{ t('deck', 'Reply') }}
</ActionButton>
<ActionButton v-if="canEdit" icon="icon-rename" @click="showUpdateForm()">
<ActionButton v-if="canEdit"
icon="icon-rename"
:close-after-click="true"
@click="showUpdateForm()">
{{ t('deck', 'Update') }}
</ActionButton>
<ActionButton v-if="canEdit" icon="icon-delete" @click="deleteComment()">
<ActionButton v-if="canEdit"
icon="icon-delete"
:close-after-click="true"
@click="deleteComment()">
{{ t('deck', 'Delete') }}
</ActionButton>
</Actions>
@@ -86,6 +104,10 @@ export default {
type: Boolean,
default: false,
},
preview: {
type: Boolean,
default: false,
},
},
data() {
return {
@@ -175,20 +197,41 @@ export default {
@import '../../css/comments';
.reply {
border-left: 3px solid var(--color-primary-element);
padding-left: 5px;
margin-left: 2px;
margin-bottom: 5px;
margin: 0 0 0 44px;
&.reply--preview {
margin: 4px 0;
padding: 8px;
background-color: var(--color-background-hover);
border-radius: var(--border-radius-large);
.reply--wrapper {
margin: 8px;
}
.reply--cancel {
margin-right: -12px;
margin-top: -12px;
}
}
.reply--wrapper {
border-left: 4px solid var(--color-border-dark);
padding-left: 8px;
}
&::v-deep .rich-text--wrapper {
margin-top: -3px;
color: var(--color-text-light);
color: var(--color-text-lighter);
}
.reply--header {
display: flex;
}
.reply--hint {
font-size: 0.9em;
color: var(--color-text-lighter);
vertical-align: top;
flex-grow: 1;
}
.comment--content {

View File

@@ -306,6 +306,7 @@ h5 {
padding: 0;
background-color: var(--color-main-background);
color: var(--color-main-text);
width: 100%;
}
.CodeMirror-placeholder {

View File

@@ -22,7 +22,13 @@
<template>
<div v-if="card" class="badges">
<div v-if="card.commentsUnread > 0" class="icon icon-comment" />
<div v-if="card.commentsCount > 0"
v-tooltip="commentsHint"
class="icon icon-comment"
:class="{ 'icon-comment--unread': card.commentsUnread > 0 }"
@click.stop="openComments">
{{ card.commentsCount }}
</div>
<div v-if="card.description && checkListCount > 0" class="card-tasks icon icon-checkmark">
{{ checkListCheckedCount }}/{{ checkListCount }}
@@ -58,6 +64,21 @@ export default {
checkListCheckedCount() {
return (this.card.description.match(/^\s*([*+-]|(\d\.))\s+\[\s*x\s*\](.*)$/gim) || []).length
},
commentsHint() {
if (this.card.commentsUnread > 0) {
return t('deck', '{count} comments, {unread} unread', {
count: this.card.commentsCount,
unread: this.card.commentsUnread
})
}
return null
},
},
methods: {
openComments() {
const boardId = this.card && this.card.boardId ? this.card.boardId : this.$route.params.id
this.$router.push({ name: 'card', params: { id: boardId, cardId: this.card.id, tabId: 'comments' } })
},
},
}
</script>
@@ -70,7 +91,7 @@ export default {
.icon {
opacity: 0.5;
padding: 12px 18px;
padding: 10px 20px;
padding-right: 4px;
margin-right: 5px;
background-position: left;
@@ -78,8 +99,8 @@ export default {
span {
margin-left: 18px;
}
&.icon-edit {
opacity: 0.5;
&.icon-comment--unread {
opacity: 1;
}
}
}

View File

@@ -22,7 +22,10 @@
<template>
<div v-if="searchQuery!==''" class="global-search">
<h2><RichText :text="t('deck', 'Search for {searchQuery} in all boards')" :arguments="queryStringArgs" /></h2>
<h2>
<RichText :text="t('deck', 'Search for {searchQuery} in all boards')" :arguments="queryStringArgs" />
<div v-if="loading" class="icon-loading-small" />
</h2>
<Actions>
<ActionButton icon="icon-close" @click="$store.commit('setSearchQuery', '')" />
</Actions>
@@ -107,23 +110,38 @@ export default {
},
},
watch: {
searchQuery() {
async searchQuery() {
this.cursor = null
this.loading = true
this.search()
try {
await this.search()
this.loading = false
} catch (e) {
if (!axios.isCancel(e)) {
console.error('Search request failed', e)
this.loading = false
}
}
},
},
methods: {
infiniteHandler($state) {
async infiniteHandler($state) {
this.loading = true
this.search().then((data) => {
try {
const data = await this.search()
if (data.length) {
$state.loaded()
} else {
$state.complete()
}
this.loading = false
})
} catch (e) {
if (!axios.isCancel(e)) {
console.error('Search request failed', e)
$state.complete()
this.loading = false
}
}
},
async search() {
if (this.cancel) {
@@ -177,6 +195,13 @@ export default {
padding: 10px;
}
h2 > div {
display: inline-block;
&.icon-loading-small {
margin-right: 20px;
}
}
h2::v-deep span {
background-color: var(--color-background-dark);
padding: 3px;

View File

@@ -48,4 +48,5 @@
.comment--content {
margin-left: 44px;
word-break: break-word;
}

View File

@@ -92,24 +92,39 @@ export default {
const filterOutQuotes = (q) => {
if (q[0] === '"' && q[q.length - 1] === '"') {
return q.substr(1, -1)
return q.substr(1, q.length - 2)
}
return q
}
for (const match of matches) {
let [filter, query] = match.indexOf(':') !== -1 ? match.split(/:(.+)/) : [null, match]
let [filter, query] = match.indexOf(':') !== -1 ? match.split(/:(.*)/) : [null, match]
const isEmptyQuery = typeof query === 'undefined' || filterOutQuotes(query) === ''
if (filter === 'title') {
if (isEmptyQuery) {
continue
}
hasMatch = hasMatch && card.title.toLowerCase().includes(filterOutQuotes(query).toLowerCase())
} else if (filter === 'description') {
if (isEmptyQuery) {
hasMatch = hasMatch && !!card.description
continue
}
hasMatch = hasMatch && card.description.toLowerCase().includes(filterOutQuotes(query).toLowerCase())
} else if (filter === 'list') {
const stack = this.getters.stackById(card.stackId)
if (isEmptyQuery) {
continue
}
const stack = getters.stackById(card.stackId)
if (!stack) {
return false
}
hasMatch = hasMatch && stack.title.toLowerCase().includes(filterOutQuotes(query).toLowerCase())
} else if (filter === 'tag') {
if (isEmptyQuery) {
hasMatch = hasMatch && card.labels.length > 0
continue
}
hasMatch = hasMatch && card.labels.findIndex((label) => label.title.toLowerCase().includes(filterOutQuotes(query).toLowerCase())) !== -1
} else if (filter === 'date') {
const datediffHour = ((new Date(card.duedate) - new Date()) / 3600 / 1000)
@@ -158,6 +173,10 @@ export default {
}
} else if (filter === 'assigned') {
if (isEmptyQuery) {
hasMatch = hasMatch && card.assignedUsers.length > 0
continue
}
hasMatch = hasMatch && card.assignedUsers.findIndex((assignment) => {
return assignment.participant.primaryKey.toLowerCase() === filterOutQuotes(query).toLowerCase()
|| assignment.participant.displayname.toLowerCase() === filterOutQuotes(query).toLowerCase()

View File

@@ -420,6 +420,7 @@ export default new Vuex.Store({
params.append('format', 'json')
params.append('perPage', 20)
params.append('itemType', [0, 1, 4, 7])
params.append('lookup', false)
const response = await axios.get(generateOcsUrl('apps/files_sharing/api/v1') + 'sharees', { params })
commit('setSharees', response.data.ocs.data)

View File

@@ -8,4 +8,5 @@ default:
baseUrl: http://localhost:8080/
- RequestContext
- BoardContext
- CommentContext
- SearchContext

View File

@@ -27,6 +27,10 @@ class BoardContext implements Context {
$this->serverContext = $environment->getContext('ServerContext');
}
public function getLastUsedCard() {
return $this->card;
}
/**
* @Given /^creates a board named "([^"]*)" with color "([^"]*)"$/
*/

View File

@@ -0,0 +1,31 @@
<?php
use Behat\Behat\Context\Context;
use Behat\Behat\Hook\Scope\BeforeScenarioScope;
require_once __DIR__ . '/../../vendor/autoload.php';
class CommentContext implements Context {
use RequestTrait;
/** @var BoardContext */
protected $boardContext;
/** @BeforeScenario */
public function gatherContexts(BeforeScenarioScope $scope) {
$environment = $scope->getEnvironment();
$this->boardContext = $environment->getContext('BoardContext');
}
/**
* @Given /^post a comment with content "([^"]*)" on the card$/
*/
public function postACommentWithContentOnTheCard($content) {
$card = $this->boardContext->getLastUsedCard();
$this->requestContext->sendOCSRequest('POST', '/apps/deck/api/v1.0/cards/' . $card['id'] . '/comments', [
'message' => $content,
'parentId' => null
]);
}
}

View File

@@ -13,6 +13,7 @@ class SearchContext implements Context {
protected $boardContext;
private $searchResults;
private $unifiedSearchResult;
/** @BeforeScenario */
public function gatherContexts(BeforeScenarioScope $scope) {
@@ -32,6 +33,18 @@ class SearchContext implements Context {
$this->searchResults = json_decode($data, true);
}
/**
* @When /^searching for "([^"]*)" in comments in unified search$/
* @param string $term
* https://cloud.nextcloud.com/ocs/v2.php/search/providers/talk-conversations/search?term=an&from=%2Fapps%2Fdashboard%2F
*/
public function searchingForComments(string $term) {
$this->requestContext->sendOCSRequest('GET', '/search/providers/deck-comment/search?term=' . urlencode($term), []);
$this->requestContext->getResponse()->getBody()->seek(0);
$data = (string)$this->getResponse()->getBody();
$this->unifiedSearchResult = json_decode($data, true);
}
/**
* @When /^searching for '([^']*)'$/
* @param string $term
@@ -78,4 +91,33 @@ class SearchContext implements Context {
public function theCardIsNotFound($arg1) {
Assert::assertFalse($this->cardIsFound($arg1), 'Card can not be found');
}
/**
* @Then /^the comment with "([^"]*)" is found$/
*/
public function theCommentWithIsFound($arg1) {
$ocsData = $this->unifiedSearchResult['ocs']['data']['entries'];
$found = null;
foreach ($ocsData as $result) {
if ($result['subline'] === $arg1) {
$found = $result;
}
}
Assert::assertNotNull($found, 'Comment was expected but was not found');
Assert::assertEquals('admin on Card with comment', $found['title']);
}
/**
* @Then /^the comment with "([^"]*)" is not found$/
*/
public function theCommentWithIsNotFound($arg1) {
$ocsData = $this->unifiedSearchResult['ocs']['data']['entries'];
$found = null;
foreach ($ocsData as $result) {
if ($result['subline'] === $arg1) {
$found = $result;
}
}
Assert::assertNull($found, 'Comment was found but not expected');
}
}

View File

@@ -257,3 +257,10 @@ Feature: Searching for cards
Then the card "Example task 1" is not found
And the card "Labeled card" is not found
And the card "Multi labeled card" is found
Scenario: Search for a card comment
Given create a card named "Card with comment"
And post a comment with content "My first comment" on the card
When searching for "My first comment" in comments in unified search
Then the comment with "My first comment" is found
Then the comment with "Any other" is not found

View File

@@ -105,7 +105,8 @@
<ParamNameMismatch occurrences="1">
<code>$boardId</code>
</ParamNameMismatch>
<UndefinedClass occurrences="1">
<UndefinedClass occurrences="2">
<code>\OCA\Circles\Api\v1\Circles</code>
<code>\OCA\Circles\Api\v1\Circles</code>
</UndefinedClass>
</file>
@@ -170,11 +171,6 @@
<code>$stackId</code>
</ParamNameMismatch>
</file>
<file src="lib/Notification/NotificationHelper.php">
<InvalidScalarArgument occurrences="1">
<code>$board-&gt;getId()</code>
</InvalidScalarArgument>
</file>
<file src="lib/Notification/Notifier.php">
<RedundantCast occurrences="7">
<code>(string) $l-&gt;t('%s has mentioned you in a comment on "%s".', [$dn, $params[0]])</code>
@@ -196,12 +192,9 @@
<code>$cardId</code>
<code>$cardId</code>
</InvalidScalarArgument>
<UndefinedThisPropertyAssignment occurrences="1">
<code>$this-&gt;currentUser</code>
</UndefinedThisPropertyAssignment>
<UndefinedThisPropertyFetch occurrences="1">
<code>$this-&gt;currentUser</code>
</UndefinedThisPropertyFetch>
</file>
<file src="lib/Service/AttachmentService.php">
<InvalidCatch occurrences="1"/>
</file>
<file src="lib/Service/BoardService.php">
<TooManyArguments occurrences="2">
@@ -265,7 +258,6 @@
</RedundantCondition>
</file>
<file src="lib/Service/FilesAppService.php">
<InvalidCatch occurrences="1"/>
<MissingDependency occurrences="4">
<code>$this-&gt;rootFolder</code>
<code>$this-&gt;rootFolder</code>
@@ -274,18 +266,12 @@
</MissingDependency>
</file>
<file src="lib/Service/PermissionService.php">
<UndefinedClass occurrences="2">
<UndefinedClass occurrences="3">
<code>Member</code>
<code>\OCA\Circles\Api\v1\Circles</code>
<code>\OCA\Circles\Api\v1\Circles</code>
</UndefinedClass>
</file>
<file src="lib/Service/SearchService.php">
<UndefinedThisPropertyFetch occurrences="3">
<code>$this-&gt;l10n</code>
<code>$this-&gt;urlGenerator</code>
<code>$this-&gt;userManager</code>
</UndefinedThisPropertyFetch>
</file>
<file src="lib/Service/StackService.php">
<UndefinedClass occurrences="1">
<code>BadRquestException</code>

View File

@@ -25,6 +25,7 @@ namespace OCA\Deck\Db;
use OCP\IGroupManager;
use OCP\IUserManager;
use Psr\Log\LoggerInterface;
use Test\AppFramework\Db\MapperTestUtility;
/**
@@ -54,7 +55,8 @@ class AclMapperTest extends MapperTestUtility {
$this->aclMapper,
\OC::$server->query(StackMapper::class),
$this->userManager,
$this->groupManager
$this->groupManager,
$this->createMock(LoggerInterface::class)
);
$this->boards = [

View File

@@ -26,6 +26,7 @@ namespace OCA\Deck\Db;
use OCP\IDBConnection;
use OCP\IGroupManager;
use OCP\IUserManager;
use Psr\Log\LoggerInterface;
use Test\AppFramework\Db\MapperTestUtility;
/**
@@ -61,7 +62,8 @@ class BoardMapperTest extends MapperTestUtility {
\OC::$server->query(AclMapper::class),
\OC::$server->query(StackMapper::class),
$this->userManager,
$this->groupManager
$this->groupManager,
$this->createMock(LoggerInterface::class)
);
$this->aclMapper = \OC::$server->query(AclMapper::class);
$this->labelMapper = \OC::$server->query(LabelMapper::class);

View File

@@ -83,6 +83,7 @@ class CardTest extends TestCase {
'assignedUsers' => null,
'deletedAt' => 0,
'commentsUnread' => 0,
'commentsCount' => 0,
'lastEditor' => null,
'ETag' => $card->getETag(),
], $card->jsonSerialize());
@@ -109,6 +110,7 @@ class CardTest extends TestCase {
'assignedUsers' => null,
'deletedAt' => 0,
'commentsUnread' => 0,
'commentsCount' => 0,
'lastEditor' => null,
'ETag' => $card->getETag(),
], $card->jsonSerialize());
@@ -145,6 +147,7 @@ class CardTest extends TestCase {
'assignedUsers' => ['user1'],
'deletedAt' => 0,
'commentsUnread' => 0,
'commentsCount' => 0,
'lastEditor' => null,
'ETag' => $card->getETag(),
], $card->jsonSerialize());

View File

@@ -130,7 +130,8 @@ class NotificationHelperTest extends \Test\TestCase {
$card = Card::fromParams([
'notified' => false,
'id' => 123,
'title' => 'MyCardTitle'
'title' => 'MyCardTitle',
'duedate' => '2020-12-24'
]);
$this->cardMapper->expects($this->once())
->method('findBoardId')
@@ -225,7 +226,8 @@ class NotificationHelperTest extends \Test\TestCase {
$card = Card::fromParams([
'notified' => false,
'id' => 123,
'title' => 'MyCardTitle'
'title' => 'MyCardTitle',
'duedate' => '2020-12-24'
]);
$card->setAssignedUsers([
new User($users[0])
@@ -323,7 +325,8 @@ class NotificationHelperTest extends \Test\TestCase {
$card = Card::fromParams([
'notified' => false,
'id' => 123,
'title' => 'MyCardTitle'
'title' => 'MyCardTitle',
'duedate' => '2020-12-24'
]);
$card->setAssignedUsers([
new User($users[0])
@@ -470,7 +473,7 @@ class NotificationHelperTest extends \Test\TestCase {
->with(123)
->willReturn($board);
$user = $this->createMock(IUser::class);
$user->expects($this->once())
$user->expects($this->any())
->method('getUID')
->willReturn('userA');
$group = $this->createMock(IGroup::class);

View File

@@ -0,0 +1,47 @@
<?php
/*
* @copyright Copyright (c) 2021 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/>.
*
*/
declare(strict_types=1);
namespace OCA\Deck\Search\Query;
use PHPUnit\Framework\TestCase;
class AQueryParameterTest extends TestCase {
public function dataValue() {
return [
['foo', 'foo'],
['spätial character', 'spätial character'],
['"spätial character"', 'spätial character'],
['"spätial "character"', 'spätial "character'],
['"spätial 🐘"', 'spätial 🐘'],
['\'spätial character\'', '\'spätial character\''],
];
}
/** @dataProvider dataValue */
public function testValue($input, $expectedValue) {
$parameter = new StringQueryParameter('test', 0, $input);
$this->assertEquals($expectedValue, $parameter->getValue());
}
}

View File

@@ -128,7 +128,7 @@ class CardServiceTest extends TestCase {
$this->userManager->expects($this->once())
->method('get')
->willReturn($user);
$this->commentsManager->expects($this->once())
$this->commentsManager->expects($this->any())
->method('getNumberOfCommentsForObject')
->willReturn(0);
$boardMock = $this->createMock(Board::class);

View File

@@ -146,7 +146,7 @@ class PermissionServiceTest extends \Test\TestCase {
}
public function testUserIsBoardOwnerNull() {
$this->boardMapper->expects($this->once())->method('find')->willReturn(null);
$this->boardMapper->expects($this->once())->method('find')->willThrowException(new DoesNotExistException('board does not exist'));
$this->assertEquals(false, $this->service->userIsBoardOwner(123));
}
@@ -225,12 +225,9 @@ class PermissionServiceTest extends \Test\TestCase {
$board = new Board();
$board->setId($boardId);
$board->setOwner($owner);
$board->setAcl($this->getAcls($boardId));
$this->boardMapper->expects($this->any())->method('find')->willReturn($board);
// acl check
$acls = $this->getAcls($boardId);
$this->aclMapper->expects($this->any())->method('findAll')->willReturn($acls);
$this->shareManager->expects($this->any())
->method('sharingDisabledForUser')
->willReturn(false);
@@ -250,14 +247,12 @@ class PermissionServiceTest extends \Test\TestCase {
$board = new Board();
$board->setId($boardId);
$board->setOwner($owner);
$board->setAcl($this->getAcls($boardId));
if ($boardId === null) {
$this->boardMapper->expects($this->any())->method('find')->willThrowException(new DoesNotExistException('not found'));
} else {
$this->boardMapper->expects($this->any())->method('find')->willReturn($board);
}
$acls = $this->getAcls($boardId);
$this->aclMapper->expects($this->any())->method('findAll')->willReturn($acls);
if ($result) {
$actual = $this->service->checkPermission($mapper, 1234, $permission);