Compare commits

...

72 Commits

Author SHA1 Message Date
Julius Härtl
38921cade8 Merge pull request #3461 from nextcloud/backport/3459/stable1.4 2021-11-30 16:32:02 +01:00
Julius Härtl
8a3e679c33 Fix cursor generation if no results are found
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-11-30 12:22:12 +00:00
Jonas
d1b81e697f Merge pull request #3442 from nextcloud/backport/3428/stable1.4
[stable1.4] Allow to download an attachment without navigating to the files app
2021-11-22 19:11:06 +01:00
Julius Härtl
215fcf61bc Allow to download an attachment without navigating to the files app
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-11-22 17:40:47 +00:00
Julius Härtl
e418373503 Bump version to 1.4.7
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-11-10 20:42:09 +01:00
Julius Härtl
ca22b0ad2c Bump version to 1.4.6
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-11-10 16:12:11 +01:00
Julius Härtl
e4cbc694d4 Merge pull request #3408 from nextcloud/backport/3384/stable1.4
[stable1.4] Keep exceptions http response generic
2021-11-05 19:53:35 +01:00
Julius Härtl
f53e51fc4e Keep exceptions http response generic and return the request ID for further tracing
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-11-05 17:02:30 +00:00
Julius Härtl
dcbbb22dda Merge pull request #3393 from nextcloud/backport/3391/stable1.4 2021-10-27 14:16:23 +02:00
Paweł Kuffel
e85042e1b4 use displayname instead of uid for mentions
Signed-off-by: Paweł Kuffel <pawel@kuffel.io>
2021-10-27 11:59:25 +00:00
Julius Härtl
a720669354 Merge pull request #3379 from nextcloud/backport/3324/stable1.4 2021-10-22 20:24:34 +02:00
Julius Härtl
216b9445d3 Merge pull request #3385 from Artem4590/backport/3323/stable1.4 2021-10-22 20:24:17 +02:00
Artem Lavrukhin
b21faa8501 [stable1.4] Extend drag-and-drop zone in card sidebar
Signed-off-by: Artem Lavrukhin <lavryha4590@gmail.com>
2021-10-21 14:48:19 +03:00
Lera Dmitrieva
1bc28c68a5 Fix menu button position in card modal
Signed-off-by: Lera Dmitrieva <dmit.valerya@yandex.ru>
2021-10-12 10:25:31 +00:00
Julius Härtl
f78f8bfd7f Merge pull request #3367 from nextcloud/backport/3364/stable1.4 2021-10-06 11:40:26 +02:00
Julius Härtl
01bddf029e Fix optional parameter order
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-10-06 09:24:23 +00:00
Julius Härtl
bdead3cdd5 Merge pull request #3360 from nextcloud/enh/stable1.4-paginated-search-for-boards-and-cards 2021-10-05 08:24:10 +02:00
Julien Veyssier
88d164b411 use distinct pagination cursor for cards and boards, use cursor and limit in SearchService::searchBoards()
Signed-off-by: Julien Veyssier <eneiluj@posteo.net>
2021-10-04 17:30:45 +02:00
Julius Härtl
1638c3d350 Merge pull request #3359 from nextcloud/backport/stable1.4/2935 2021-10-04 16:25:10 +02:00
Joas Schilling
454d515192 Rich object string parameters for notifications
Signed-off-by: Joas Schilling <coding@schilljs.com>
2021-10-04 14:21:23 +02:00
Julius Härtl
e60219c9df Bump version to 1.4.5
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-09-14 21:10:51 +02:00
Julius Härtl
5c8c73f2ac Merge pull request #3318 from nextcloud/backport/3316/stable1.4
[stable1.4] Additional check for stacks
2021-09-14 21:08:34 +02:00
Julius Härtl
fad63ac6f5 Additional check for stacks
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-09-14 19:01:30 +00:00
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
59 changed files with 960 additions and 360 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,71 @@
# Changelog
All notable changes to this project will be documented in this file.
## 1.4.7
### Fixed
- Fix release asset build
## 1.4.6
### Fixed
- #3379 Fix menu button position in card modal
- #3360 Improve combined search @eneiluj
- #3367 Fix optional parameter order
- #3393 Use displayname instead of uid for mentions
- #3359 Rich object string parameters for notifications @juliushaertl
- #3385 Extend drag-and-drop zone in card sidebar @Artem4590
- #3408 Keep exceptions http response generic
## 1.4.5
### Fixed
- #3318 Additional check for stacks
## 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.7</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

@@ -36,8 +36,10 @@ class CommentsApiController extends OCSController {
private $commentService;
public function __construct(
$appName, IRequest $request, $corsMethods = 'PUT, POST, GET, DELETE, PATCH', $corsAllowedHeaders = 'Authorization, Content-Type, Accept', $corsMaxAge = 1728000,
CommentService $commentService
string $appName,
IRequest $request,
CommentService $commentService,
string $corsMethods = 'PUT, POST, GET, DELETE, PATCH', string $corsAllowedHeaders = 'Authorization, Content-Type, Accept', int $corsMaxAge = 1728000
) {
parent::__construct($appName, $request, $corsMethods, $corsAllowedHeaders, $corsMaxAge);
$this->commentService = $commentService;

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 {
$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);
$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));
$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

@@ -32,6 +32,7 @@ use OCP\AppFramework\Http\JSONResponse;
use OCP\AppFramework\OCS\OCSException;
use OCP\AppFramework\OCSController;
use OCP\ILogger;
use OCP\IRequest;
use OCP\Util;
use OCP\IConfig;
@@ -41,6 +42,8 @@ class ExceptionMiddleware extends Middleware {
private $logger;
/** @var IConfig */
private $config;
/** @var IRequest */
private $request;
/**
* SharingMiddleware constructor.
@@ -48,9 +51,10 @@ class ExceptionMiddleware extends Middleware {
* @param ILogger $logger
* @param IConfig $config
*/
public function __construct(ILogger $logger, IConfig $config) {
public function __construct(ILogger $logger, IConfig $config, IRequest $request) {
$this->logger = $logger;
$this->config = $config;
$this->request = $request;
}
/**
@@ -67,45 +71,10 @@ class ExceptionMiddleware extends Middleware {
throw $exception;
}
if ($exception instanceof ConflictException) {
if ($this->config->getSystemValue('loglevel', Util::WARN) === Util::DEBUG) {
$this->logger->logException($exception);
}
return new JSONResponse([
'status' => $exception->getStatus(),
'message' => $exception->getMessage(),
'data' => $exception->getData(),
], $exception->getStatus());
}
if ($exception instanceof StatusException) {
if ($this->config->getSystemValue('loglevel', Util::WARN) === Util::DEBUG) {
$this->logger->logException($exception);
}
if ($controller instanceof OCSController) {
$exception = new OCSException($exception->getMessage(), $exception->getStatus(), $exception);
throw $exception;
}
return new JSONResponse([
'status' => $exception->getStatus(),
'message' => $exception->getMessage()
], $exception->getStatus());
}
if (strpos(get_class($controller), 'OCA\\Deck\\Controller\\') === 0) {
$response = [
'status' => 500,
'message' => $exception->getMessage()
];
if ($this->config->getSystemValue('loglevel', Util::WARN) === Util::DEBUG) {
$this->logger->logException($exception);
}
if ($this->config->getSystemValue('debug', true) === true) {
$response['exception'] = (array) $exception;
}
return new JSONResponse($response, 500);
}
$debugMode = $this->config->getSystemValue('debug', false);
$exceptionMessage = $debugMode !== true
? 'Internal server error: Please contact the server administrator if this error reappears multiple times, please include the request ID "' . $this->request->getId() . '" below in your report.'
: $exception->getMessage();
// uncatched DoesNotExistExceptions will be thrown when the main entity is not found
// we return a 403 so we don't leak information over existing entries
@@ -117,6 +86,43 @@ class ExceptionMiddleware extends Middleware {
], 403);
}
if ($exception instanceof StatusException) {
if ($this->config->getSystemValue('loglevel', Util::WARN) === Util::DEBUG) {
$this->logger->logException($exception);
}
if ($exception instanceof ConflictException) {
return new JSONResponse([
'status' => $exception->getStatus(),
'message' => $exception->getMessage(),
'data' => $exception->getData(),
], $exception->getStatus());
}
if ($controller instanceof OCSController) {
$exception = new OCSException($exception->getMessage(), $exception->getStatus(), $exception);
throw $exception;
}
return new JSONResponse([
'status' => $exception->getStatus(),
'message' => $exception->getMessage(),
], $exception->getStatus());
}
if (strpos(get_class($controller), 'OCA\\Deck\\Controller\\') === 0) {
$response = [
'status' => 500,
'message' => $exceptionMessage,
'requestId' => $this->request->getId(),
];
$this->logger->logException($exception);
if ($debugMode === true) {
$response['exception'] = (array) $exception;
}
return new JSONResponse($response, 500);
}
throw $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());
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) {
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());
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());
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

@@ -25,6 +25,7 @@ namespace OCA\Deck\Notification;
use OCA\Deck\Db\BoardMapper;
use OCA\Deck\Db\CardMapper;
use OCA\Deck\Db\StackMapper;
use OCP\IURLGenerator;
use OCP\IUserManager;
use OCP\L10N\IFactory;
@@ -41,6 +42,8 @@ class Notifier implements INotifier {
protected $userManager;
/** @var CardMapper */
protected $cardMapper;
/** @var StackMapper */
protected $stackMapper;
/** @var BoardMapper */
protected $boardMapper;
@@ -49,12 +52,14 @@ class Notifier implements INotifier {
IURLGenerator $url,
IUserManager $userManager,
CardMapper $cardMapper,
StackMapper $stackMapper,
BoardMapper $boardMapper
) {
$this->l10nFactory = $l10nFactory;
$this->url = $url;
$this->userManager = $userManager;
$this->cardMapper = $cardMapper;
$this->stackMapper = $stackMapper;
$this->boardMapper = $boardMapper;
}
@@ -100,6 +105,11 @@ class Notifier implements INotifier {
if (!$boardId) {
throw new AlreadyProcessedException();
}
$card = $this->cardMapper->find($cardId);
$stackId = $card->getStackId();
$stack = $this->stackMapper->find($stackId);
$initiator = $this->userManager->get($params[2]);
if ($initiator !== null) {
$dn = $initiator->getDisplayName();
@@ -110,8 +120,22 @@ class Notifier implements INotifier {
(string) $l->t('The card "%s" on "%s" has been assigned to you by %s.', [$params[0], $params[1], $dn])
);
$notification->setRichSubject(
(string) $l->t('{user} has assigned the card "%s" on "%s" to you.', [$params[0], $params[1]]),
$l->t('{user} has assigned the card {deck-card} on {deck-board} to you.'),
[
'deck-card' => [
'type' => 'deck-card',
'id' => $cardId,
'name' => $params[0],
'boardname' => $params[1],
'stackname' => $stack->getTitle(),
'link' => $this->url->linkToRouteAbsolute('deck.page.index') . '#/board/' . $boardId . '/card/' . $cardId . '',
],
'deck-board' => [
'type' => 'deck-board',
'id' => $boardId,
'name' => $params[1],
'link' => $this->url->linkToRouteAbsolute('deck.page.index') . '#/board/' . $boardId,
],
'user' => [
'type' => 'user',
'id' => $params[2],
@@ -127,9 +151,33 @@ class Notifier implements INotifier {
if (!$boardId) {
throw new AlreadyProcessedException();
}
$card = $this->cardMapper->find($cardId);
$stackId = $card->getStackId();
$stack = $this->stackMapper->find($stackId);
$notification->setParsedSubject(
(string) $l->t('The card "%s" on "%s" has reached its due date.', $params)
);
$notification->setRichSubject(
$l->t('The card {deck-card} on {deck-board} has reached its due date.'),
[
'deck-card' => [
'type' => 'deck-card',
'id' => $cardId,
'name' => $params[0],
'boardname' => $params[1],
'stackname' => $stack->getTitle(),
'link' => $this->url->linkToRouteAbsolute('deck.page.index') . '#/board/' . $boardId . '/card/' . $cardId . '',
],
'deck-board' => [
'type' => 'deck-board',
'id' => $boardId,
'name' => $params[1],
'link' => $this->url->linkToRouteAbsolute('deck.page.index') . '#/board/' . $boardId,
],
]
);
$notification->setLink($this->url->linkToRouteAbsolute('deck.page.index') . '#/board/' . $boardId . '/card/' . $cardId . '');
break;
case 'card-comment-mentioned':
@@ -138,6 +186,11 @@ class Notifier implements INotifier {
if (!$boardId) {
throw new AlreadyProcessedException();
}
$card = $this->cardMapper->find($cardId);
$stackId = $card->getStackId();
$stack = $this->stackMapper->find($stackId);
$initiator = $this->userManager->get($params[2]);
if ($initiator !== null) {
$dn = $initiator->getDisplayName();
@@ -148,8 +201,16 @@ class Notifier implements INotifier {
(string) $l->t('%s has mentioned you in a comment on "%s".', [$dn, $params[0]])
);
$notification->setRichSubject(
(string) $l->t('{user} has mentioned you in a comment on "%s".', [$params[0]]),
$l->t('{user} has mentioned you in a comment on {deck-card}.'),
[
'deck-card' => [
'type' => 'deck-card',
'id' => $cardId,
'name' => $params[0],
'boardname' => $params[1],
'stackname' => $stack->getTitle(),
'link' => $this->url->linkToRouteAbsolute('deck.page.index') . '#/board/' . $boardId . '/card/' . $cardId . '',
],
'user' => [
'type' => 'user',
'id' => $params[2],
@@ -177,8 +238,14 @@ class Notifier implements INotifier {
(string) $l->t('The board "%s" has been shared with you by %s.', [$params[0], $dn])
);
$notification->setRichSubject(
(string) $l->t('{user} has shared the board %s with you.', [$params[0]]),
$l->t('{user} has shared {deck-board} with you.'),
[
'deck-board' => [
'type' => 'deck-board',
'id' => $boardId,
'name' => $params[0],
'link' => $this->url->linkToRouteAbsolute('deck.page.index') . '#/board/' . $boardId,
],
'user' => [
'type' => 'user',
'id' => $params[1],

View File

@@ -63,29 +63,52 @@ class DeckProvider implements IProvider {
}
public function search(IUser $user, ISearchQuery $query): SearchResult {
$cursor = $query->getCursor() !== null ? (int)$query->getCursor() : null;
$boardResults = $this->searchService->searchBoards($query->getTerm(), $query->getLimit(), $cursor);
$cardResults = $this->searchService->searchCards($query->getTerm(), $query->getLimit(), $cursor);
$results = array_merge(
array_map(function (Board $board) {
return new BoardSearchResultEntry($board, $this->urlGenerator);
}, $boardResults),
array_map(function (Card $card) {
return new CardSearchResultEntry($card->getRelatedBoard(), $card->getRelatedStack(), $card, $this->urlGenerator);
}, $cardResults)
);
$cursor = $query->getCursor();
[$boardCursor, $cardCursor] = $this->parseCursor($cursor);
if (count($cardResults) < $query->getLimit()) {
$boardObjects = $this->searchService->searchBoards($query->getTerm(), $query->getLimit(), $boardCursor);
$boardResults = array_map(function (Board $board) {
return [
'object' => $board,
'entry' => new BoardSearchResultEntry($board, $this->urlGenerator)
];
}, $boardObjects);
$cardObjects = $this->searchService->searchCards($query->getTerm(), $query->getLimit(), $cardCursor);
$cardResults = array_map(function (Card $card) {
return [
'object' => $card,
'entry' => new CardSearchResultEntry($card->getRelatedBoard(), $card->getRelatedStack(), $card, $this->urlGenerator)
];
}, $cardObjects);
$results = array_merge($boardResults, $cardResults);
usort($results, function ($a, $b) {
$ta = $a['object']->getLastModified();
$tb = $b['object']->getLastModified();
return $ta === $tb
? 0
: ($ta > $tb ? -1 : 1);
});
$resultEntries = array_map(function (array $result) {
return $result['entry'];
}, $results);
// if both cards and boards results are less then the limit, we know we won't get more
if (count($resultEntries) < $query->getLimit()) {
return SearchResult::complete(
'Deck',
$results
$resultEntries
);
}
$newCursor = $this->getNewCursor($boardObjects, $cardObjects);
return SearchResult::paginated(
'Deck',
$results,
$cardResults[count($results) - 1]->getLastModified()
$resultEntries,
$newCursor
);
}
@@ -95,4 +118,27 @@ class DeckProvider implements IProvider {
}
return 10;
}
private function parseCursor(?string $cursor): array {
$boardCursor = null;
$cardCursor = null;
if ($cursor !== null) {
$splitCursor = explode('|', $cursor);
if (count($splitCursor) >= 2) {
$boardCursor = (int)$splitCursor[0] ?: null;
$cardCursor = (int)$splitCursor[1] ?: null;
}
}
return [$boardCursor, $cardCursor];
}
private function getNewCursor(array $boards, array $cards): string {
$boardTimestamps = array_map(function (Board $board) {
return $board->getLastModified();
}, $boards);
$cardTimestamps = array_map(function (Card $card) {
return $card->getLastModified();
}, $cards);
return (count($boardTimestamps) > 0 ? min($boardTimestamps) : '') . '|' . (count($cardTimestamps) > 0 ? min($cardTimestamps) : '');
}
}

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 (\Exception $e) {
} catch (IMapperException $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);
}
}
$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) {
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,11 +153,14 @@ class PermissionService {
return true;
}
$acls = $this->aclMapper->findAll($boardId);
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
throw new NoPermissionException('Permission denied');
@@ -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

@@ -91,9 +91,24 @@ class SearchService {
public function searchBoards(string $term, ?int $limit, ?int $cursor): array {
$boards = $this->boardService->getUserBoards();
return array_filter($boards, static function (Board $board) use ($term) {
return mb_stripos(mb_strtolower($board->getTitle()), mb_strtolower($term)) > -1;
// get boards that have a lastmodified date which is lower than the cursor
// and which match the search term
$filteredBoards = array_filter($boards, static function (Board $board) use ($term, $cursor) {
return (
($cursor === null || $board->getLastModified() < $cursor)
&& mb_stripos(mb_strtolower($board->getTitle()), mb_strtolower($term)) > -1
);
});
// sort the boards, recently modified first
usort($filteredBoards, function ($boardA, $boardB) {
$ta = $boardA->getLastModified();
$tb = $boardB->getLastModified();
return $ta === $tb
? 0
: ($ta > $tb ? -1 : 1);
});
// limit the number of results
return array_slice($filteredBoards, 0, $limit);
}
public function searchComments(string $term, ?int $limit = null, ?int $cursor = null): array {

View File

@@ -48,7 +48,6 @@ class StackService {
private $assignedUsersMapper;
private $attachmentService;
private $activityManager;
private $symfonyAdapter;
private $changeHelper;
public function __construct(
@@ -110,6 +109,7 @@ class StackService {
throw new BadRequestException('stack id must be a number');
}
$this->permissionService->checkPermission($this->stackMapper, $stackId, Acl::PERMISSION_READ);
$stack = $this->stackMapper->find($stackId);
$cards = $this->cardMapper->findAll($stackId);
foreach ($cards as $cardIndex => $card) {

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

@@ -23,6 +23,12 @@
namespace OCA\Deck;
/**
* User facing exception that can be thrown with an error being reported to the frontend
* or consumers of the API
*
* This exception is catched in the ExceptionMiddleware
*/
class StatusException extends \Exception {
public function __construct($message) {
parent::__construct($message);

View File

@@ -1,7 +1,7 @@
{
"name": "deck",
"description": "",
"version": "1.0.0",
"version": "1.4.7",
"authors": [
{
"name": "Julius Härtl",

View File

@@ -131,6 +131,12 @@ export default {
position: relative;
}
.attachments-drag-zone.drop-upload--sidebar {
display: flex;
flex-direction: column;
flex-basis: 100%;
}
.dragover {
position: absolute;
background: var(--color-primary-light);

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 v-if="!isReadOnly" class="button-group">
<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>
<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,18 @@
{{ 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') }}
<ActionLink v-if="attachment.extendedData.fileid"
icon="icon-download"
:href="downloadLink(attachment)"
download>
{{ t('deck', 'Download') }}
</ActionLink>
<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)">
@@ -94,7 +107,8 @@ import { Actions, ActionButton, ActionLink } from '@nextcloud/vue'
import AttachmentDragAndDrop from '../AttachmentDragAndDrop'
import relativeDate from '../../mixins/relativeDate'
import { formatFileSize } from '@nextcloud/files'
import { generateUrl, generateOcsUrl } from '@nextcloud/router'
import { getCurrentUser } from '@nextcloud/auth'
import { generateUrl, generateOcsUrl, generateRemoteUrl } from '@nextcloud/router'
import { mapState } from 'vuex'
import { loadState } from '@nextcloud/initial-state'
import attachmentUpload from '../../mixins/attachmentUpload'
@@ -143,6 +157,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() {
@@ -166,6 +181,9 @@ export default {
internalLink() {
return (attachment) => generateUrl('/f/' + attachment.extendedData.fileid)
},
downloadLink() {
return (attachment) => generateRemoteUrl(`dav/files/${getCurrentUser().uid}/${attachment.extendedData.path}`)
},
formattedFileSize() {
return (filesize) => formatFileSize(filesize)
},
@@ -320,9 +338,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

@@ -185,6 +185,13 @@ export default {
<style lang="scss" scoped>
section.app-sidebar__tab--active {
min-height: auto;
display: flex;
flex-direction: column;
height: 100%;
}
// FIXME: Obivously we should at some point not randomly reuse the sidebar component
// since this is not oficially supported
.modal__card .app-sidebar {
@@ -202,7 +209,6 @@ export default {
.app-sidebar-header {
position: sticky;
top: 0;
padding-top: $modal-padding;
z-index: 100;
background-color: var(--color-main-background);
}
@@ -214,12 +220,6 @@ export default {
background-color: var(--color-main-background);
}
section.app-sidebar__tab--active {
min-height: auto;
display: flex;
flex-direction: column;
}
#emptycontent, .emptycontent {
margin-top: 88px;
}

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,20 +91,35 @@ export default {
},
methods: {
async infiniteHandler($state) {
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
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) {
const commentObj = {
@@ -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

@@ -26,7 +26,7 @@
<At ref="at"
v-model="commentText"
:members="members"
name-key="uid"
name-key="displayname"
:tab-select="true">
<template v-slot:item="s">
<Avatar class="atwho-li--avatar" :user="s.item.uid" :size="24" />
@@ -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,11 +1,23 @@
<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>
<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>
<div class="comment--header">
@@ -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

@@ -47,10 +47,12 @@ class ExceptionMiddlewareTest extends \Test\TestCase {
public function setUp(): void {
$this->logger = $this->createMock(ILogger::class);
$this->config = $this->createMock(IConfig::class);
$this->request = $this->createMock(IRequest::class);
$this->controller = $this->createMock(Controller::class);
$this->exceptionMiddleware = new ExceptionMiddleware(
$this->logger,
$this->config
$this->config,
$this->request
);
}
@@ -81,10 +83,11 @@ class ExceptionMiddlewareTest extends \Test\TestCase {
}
public function testAfterExceptionFail() {
$this->request->expects($this->any())->method('getId')->willReturn('abc123');
// BoardService $boardService, PermissionService $permissionService, $userId
$boardController = new BoardController('deck', $this->createMock(IRequest::class), $this->createMock(BoardService::class), $this->createMock(PermissionService::class), 'admin');
$result = $this->exceptionMiddleware->afterException($boardController, 'bar', new \Exception('failed hard'));
$this->assertEquals('failed hard', $result->getData()['message']);
$result = $this->exceptionMiddleware->afterException($boardController, 'bar', new \Exception('other exception message'));
$this->assertEquals('Internal server error: Please contact the server administrator if this error reappears multiple times, please include the request ID "abc123" below in your report.', $result->getData()['message']);
$this->assertEquals(500, $result->getData()['status']);
}
}

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

@@ -25,29 +25,33 @@ namespace OCA\Deck\Notification;
use OCA\Deck\Db\BoardMapper;
use OCA\Deck\Db\CardMapper;
use OCA\Deck\Db\StackMapper;
use OCP\IL10N;
use OCP\IURLGenerator;
use OCP\IUser;
use OCP\IUserManager;
use OCP\L10N\IFactory;
use OCP\Notification\INotification;
use PHPUnit\Framework\MockObject\MockObject;
class NotifierTest extends \Test\TestCase {
/** @var IFactory */
/** @var IFactory|MockObject */
protected $l10nFactory;
/** @var IURLGenerator */
/** @var IURLGenerator|MockObject */
protected $url;
/** @var IUserManager */
/** @var IUserManager|MockObject */
protected $userManager;
/** @var CardMapper */
/** @var CardMapper|MockObject */
protected $cardMapper;
/** @var StackMapper|MockObject */
protected $stackMapper;
/** @var BoardMapper */
protected $boardMapper;
/** @var IL10N|MockObject */
protected $l10n;
/** @var Notifier */
protected $notifier;
/** @var IL10N */
protected $l10n;
public function setUp(): void {
parent::setUp();
@@ -55,12 +59,14 @@ class NotifierTest extends \Test\TestCase {
$this->url = $this->createMock(IURLGenerator::class);
$this->userManager = $this->createMock(IUserManager::class);
$this->cardMapper = $this->createMock(CardMapper::class);
$this->stackMapper = $this->createMock(StackMapper::class);
$this->boardMapper = $this->createMock(BoardMapper::class);
$this->notifier = new Notifier(
$this->l10nFactory,
$this->url,
$this->userManager,
$this->cardMapper,
$this->stackMapper,
$this->boardMapper
);
$this->l10n = \OC::$server->getL10N('deck');
@@ -149,7 +155,7 @@ class NotifierTest extends \Test\TestCase {
->with($expectedMessage);
$notification->expects($this->once())
->method('setRichSubject')
->with('{user} has mentioned you in a comment on "Card title".');
->with('{user} has mentioned you in a comment on {deck-card}.');
$this->url->expects($this->once())
@@ -218,11 +224,25 @@ class NotifierTest extends \Test\TestCase {
->with($expectedMessage);
$notification->expects($this->once())
->method('setRichSubject')
->with('{user} has assigned the card "Card title" on "Board title" to you.', [
->with('{user} has assigned the card {deck-card} on {deck-board} to you.', [
'user' => [
'type' => 'user',
'id' => 'otheruser',
'name' => $dn,
],
'deck-card' => [
'type' => 'deck-card',
'id' => '123',
'name' => 'Card title',
'boardname' => 'Board title',
'stackname' => null,
'link' => '#/board/123/card/123',
],
'deck-board' => [
'type' => 'deck-board',
'id' => 123,
'name' => 'Board title',
'link' => '#/board/123',
]
]);
@@ -288,11 +308,17 @@ class NotifierTest extends \Test\TestCase {
->with($expectedMessage);
$notification->expects($this->once())
->method('setRichSubject')
->with('{user} has shared the board Board title with you.', [
->with('{user} has shared {deck-board} with you.', [
'user' => [
'type' => 'user',
'id' => 'otheruser',
'name' => $dn,
],
'deck-board' => [
'type' => 'deck-board',
'id' => 123,
'name' => 'Board title',
'link' => '#/board/123',
]
]);

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);