Compare commits
68 Commits
bugfix/noi
...
v1.4.7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e418373503 | ||
|
|
ca22b0ad2c | ||
|
|
e4cbc694d4 | ||
|
|
f53e51fc4e | ||
|
|
dcbbb22dda | ||
|
|
e85042e1b4 | ||
|
|
a720669354 | ||
|
|
216b9445d3 | ||
|
|
b21faa8501 | ||
|
|
1bc28c68a5 | ||
|
|
f78f8bfd7f | ||
|
|
01bddf029e | ||
|
|
bdead3cdd5 | ||
|
|
88d164b411 | ||
|
|
1638c3d350 | ||
|
|
454d515192 | ||
|
|
e60219c9df | ||
|
|
5c8c73f2ac | ||
|
|
fad63ac6f5 | ||
|
|
31eb8d6698 | ||
|
|
40967a4ee6 | ||
|
|
bfe9b05d69 | ||
|
|
82e3400162 | ||
|
|
a886b4ee78 | ||
|
|
618fb50618 | ||
|
|
f7aae7912d | ||
|
|
2976604b7b | ||
|
|
bbe482586b | ||
|
|
ff61238487 | ||
|
|
9e2dcb686f | ||
|
|
fcc96ca98d | ||
|
|
a43cee8a5d | ||
|
|
f4ccc506af | ||
|
|
fee49f3699 | ||
|
|
d43c7a48cc | ||
|
|
c0fad295b5 | ||
|
|
cb1314f067 | ||
|
|
ba68e4c2f7 | ||
|
|
bd8fd6a66b | ||
|
|
0eba8d0840 | ||
|
|
8fc95dc40d | ||
|
|
ecd3e25588 | ||
|
|
914f912612 | ||
|
|
e68f723095 | ||
|
|
5f71be2e7f | ||
|
|
bc2a72f035 | ||
|
|
cf4be82827 | ||
|
|
23580705aa | ||
|
|
65c8c394a8 | ||
|
|
422788a6a3 | ||
|
|
2d5e29de5d | ||
|
|
2a307b92a7 | ||
|
|
2d8dbc70ad | ||
|
|
cfee259b38 | ||
|
|
f94cdb3ebb | ||
|
|
1ed50fdca6 | ||
|
|
56e460004f | ||
|
|
a95f78d188 | ||
|
|
df09a9a7b2 | ||
|
|
990ee2aef9 | ||
|
|
486ecd12db | ||
|
|
c9cdd7bb11 | ||
|
|
2c753fd084 | ||
|
|
79d2d2f3f5 | ||
|
|
24d9b55bfc | ||
|
|
28cd9fcf77 | ||
|
|
d8a36f0602 | ||
|
|
de06033dcd |
55
.github/workflows/app-code-check.yml
vendored
55
.github/workflows/app-code-check.yml
vendored
@@ -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 }}
|
|
||||||
2
.github/workflows/integration.yml
vendored
2
.github/workflows/integration.yml
vendored
@@ -34,7 +34,7 @@ jobs:
|
|||||||
POSTGRES_DB: nextcloud
|
POSTGRES_DB: nextcloud
|
||||||
options: --health-cmd pg_isready --health-interval 5s --health-timeout 2s --health-retries 5
|
options: --health-cmd pg_isready --health-interval 5s --health-timeout 2s --health-retries 5
|
||||||
mysql:
|
mysql:
|
||||||
image: mariadb
|
image: mariadb:10.5
|
||||||
ports:
|
ports:
|
||||||
- 4444:3306/tcp
|
- 4444:3306/tcp
|
||||||
env:
|
env:
|
||||||
|
|||||||
2
.github/workflows/phpunit.yml
vendored
2
.github/workflows/phpunit.yml
vendored
@@ -35,7 +35,7 @@ jobs:
|
|||||||
POSTGRES_DB: nextcloud
|
POSTGRES_DB: nextcloud
|
||||||
options: --health-cmd pg_isready --health-interval 5s --health-timeout 2s --health-retries 5
|
options: --health-cmd pg_isready --health-interval 5s --health-timeout 2s --health-retries 5
|
||||||
mysql:
|
mysql:
|
||||||
image: mariadb
|
image: mariadb:10.5
|
||||||
ports:
|
ports:
|
||||||
- 4444:3306/tcp
|
- 4444:3306/tcp
|
||||||
env:
|
env:
|
||||||
|
|||||||
65
CHANGELOG.md
65
CHANGELOG.md
@@ -1,6 +1,71 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
All notable changes to this project will be documented in this file.
|
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
|
## 1.4.0 - 2021-04-13
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
<?xml version="1.0"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<info xmlns:xsi= "http://www.w3.org/2001/XMLSchema-instance"
|
<info xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://apps.nextcloud.com/schema/apps/info.xsd">
|
||||||
xsi:noNamespaceSchemaLocation="https://apps.nextcloud.com/schema/apps/info.xsd">
|
|
||||||
<id>deck</id>
|
<id>deck</id>
|
||||||
<name>Deck</name>
|
<name>Deck</name>
|
||||||
<summary>Personal planning and team project organization</summary>
|
<summary>Personal planning and team project organization</summary>
|
||||||
@@ -17,12 +16,12 @@
|
|||||||
- 🚀 Get your project organized
|
- 🚀 Get your project organized
|
||||||
|
|
||||||
</description>
|
</description>
|
||||||
<version>1.4.0</version>
|
<version>1.4.7</version>
|
||||||
<licence>agpl</licence>
|
<licence>agpl</licence>
|
||||||
<author>Julius Härtl</author>
|
<author>Julius Härtl</author>
|
||||||
<namespace>Deck</namespace>
|
<namespace>Deck</namespace>
|
||||||
<types>
|
<types>
|
||||||
<dav />
|
<dav/>
|
||||||
</types>
|
</types>
|
||||||
<category>organization</category>
|
<category>organization</category>
|
||||||
<category>office</category>
|
<category>office</category>
|
||||||
@@ -36,7 +35,7 @@
|
|||||||
<database min-version="9.4">pgsql</database>
|
<database min-version="9.4">pgsql</database>
|
||||||
<database>sqlite</database>
|
<database>sqlite</database>
|
||||||
<database min-version="5.5">mysql</database>
|
<database min-version="5.5">mysql</database>
|
||||||
<nextcloud min-version="21" max-version="22" />
|
<nextcloud min-version="21" max-version="21"/>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
<background-jobs>
|
<background-jobs>
|
||||||
<job>OCA\Deck\Cron\DeleteCron</job>
|
<job>OCA\Deck\Cron\DeleteCron</job>
|
||||||
|
|||||||
@@ -22,7 +22,9 @@
|
|||||||
.icon-activity {
|
.icon-activity {
|
||||||
@include icon-color('activity-dark', 'activity', $color-black);
|
@include icon-color('activity-dark', 'activity', $color-black);
|
||||||
}
|
}
|
||||||
|
.icon-comment--unread {
|
||||||
|
@include icon-color('comment', 'actions', $color-primary, 1, true);
|
||||||
|
}
|
||||||
|
|
||||||
.avatardiv.circles {
|
.avatardiv.circles {
|
||||||
background: var(--color-primary);
|
background: var(--color-primary);
|
||||||
|
|||||||
@@ -2,21 +2,26 @@
|
|||||||
/* hide stuff */
|
/* hide stuff */
|
||||||
#body-user {
|
#body-user {
|
||||||
#header,
|
#header,
|
||||||
div#app-navigation,
|
.app-navigation,
|
||||||
div.board-header-controls,
|
.app-sidebar,
|
||||||
|
.board-header-controls,
|
||||||
|
.board-actions,
|
||||||
#app-navigation-toggle,
|
#app-navigation-toggle,
|
||||||
#app-navigation-toggle-custom,
|
#app-navigation-toggle-custom,
|
||||||
div#controls.ng-scope div.crumb:not(.title),
|
div#controls.ng-scope div.crumb:not(.title),
|
||||||
div#controls.ng-scope div.crumb a.bullet,
|
div#controls.ng-scope div.crumb a.bullet,
|
||||||
a.ng-binding + a,
|
a.ng-binding + a,
|
||||||
div.card.create,
|
div.card.create,
|
||||||
|
.stack__header .action-item,
|
||||||
button.card-options {
|
button.card-options {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
#content {
|
#content {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
#app-content {
|
#app-content {
|
||||||
margin: 0 !important;
|
margin: 0 !important;
|
||||||
}
|
}
|
||||||
@@ -75,6 +80,11 @@
|
|||||||
margin: 2cm;
|
margin: 2cm;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.board {
|
||||||
|
max-height: none !important;
|
||||||
|
overflow: visible !important;
|
||||||
|
}
|
||||||
|
|
||||||
div#innerBoard {
|
div#innerBoard {
|
||||||
display:flex;
|
display:flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
|||||||
@@ -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
|
### DELETE /boards/{boardId}/stacks/{stackId}/cards/{cardId}/attachments/{attachmentId} - Delete an attachment
|
||||||
|
|
||||||
|
|
||||||
#### Request parameters
|
#### Request parameters
|
||||||
|
|
||||||
| Parameter | Type | Description |
|
| Parameter | Type | Description |
|
||||||
|
|||||||
@@ -36,8 +36,10 @@ class CommentsApiController extends OCSController {
|
|||||||
private $commentService;
|
private $commentService;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
$appName, IRequest $request, $corsMethods = 'PUT, POST, GET, DELETE, PATCH', $corsAllowedHeaders = 'Authorization, Content-Type, Accept', $corsMaxAge = 1728000,
|
string $appName,
|
||||||
CommentService $commentService
|
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);
|
parent::__construct($appName, $request, $corsMethods, $corsAllowedHeaders, $corsMaxAge);
|
||||||
$this->commentService = $commentService;
|
$this->commentService = $commentService;
|
||||||
|
|||||||
@@ -23,11 +23,12 @@
|
|||||||
|
|
||||||
namespace OCA\Deck\Db;
|
namespace OCA\Deck\Db;
|
||||||
|
|
||||||
|
use OC\Cache\CappedMemoryCache;
|
||||||
use OCP\AppFramework\Db\DoesNotExistException;
|
use OCP\AppFramework\Db\DoesNotExistException;
|
||||||
use OCP\IDBConnection;
|
use OCP\IDBConnection;
|
||||||
use OCP\ILogger;
|
|
||||||
use OCP\IUserManager;
|
use OCP\IUserManager;
|
||||||
use OCP\IGroupManager;
|
use OCP\IGroupManager;
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
|
||||||
class BoardMapper extends DeckMapper implements IPermissionMapper {
|
class BoardMapper extends DeckMapper implements IPermissionMapper {
|
||||||
private $labelMapper;
|
private $labelMapper;
|
||||||
@@ -35,16 +36,20 @@ class BoardMapper extends DeckMapper implements IPermissionMapper {
|
|||||||
private $stackMapper;
|
private $stackMapper;
|
||||||
private $userManager;
|
private $userManager;
|
||||||
private $groupManager;
|
private $groupManager;
|
||||||
|
private $logger;
|
||||||
|
|
||||||
private $circlesEnabled;
|
private $circlesEnabled;
|
||||||
|
|
||||||
|
private $userBoardCache;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
IDBConnection $db,
|
IDBConnection $db,
|
||||||
LabelMapper $labelMapper,
|
LabelMapper $labelMapper,
|
||||||
AclMapper $aclMapper,
|
AclMapper $aclMapper,
|
||||||
StackMapper $stackMapper,
|
StackMapper $stackMapper,
|
||||||
IUserManager $userManager,
|
IUserManager $userManager,
|
||||||
IGroupManager $groupManager
|
IGroupManager $groupManager,
|
||||||
|
LoggerInterface $logger
|
||||||
) {
|
) {
|
||||||
parent::__construct($db, 'deck_boards', Board::class);
|
parent::__construct($db, 'deck_boards', Board::class);
|
||||||
$this->labelMapper = $labelMapper;
|
$this->labelMapper = $labelMapper;
|
||||||
@@ -52,6 +57,10 @@ class BoardMapper extends DeckMapper implements IPermissionMapper {
|
|||||||
$this->stackMapper = $stackMapper;
|
$this->stackMapper = $stackMapper;
|
||||||
$this->userManager = $userManager;
|
$this->userManager = $userManager;
|
||||||
$this->groupManager = $groupManager;
|
$this->groupManager = $groupManager;
|
||||||
|
$this->logger = $logger;
|
||||||
|
|
||||||
|
$this->userBoardCache = new CappedMemoryCache();
|
||||||
|
|
||||||
|
|
||||||
$this->circlesEnabled = \OC::$server->getAppManager()->isEnabledForUser('circles');
|
$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 {
|
public function findAllForUser(string $userId, int $since = -1, $includeArchived = true): array {
|
||||||
$groups = $this->groupManager->getUserGroupIds(
|
$useCache = ($since === -1 && $includeArchived === true);
|
||||||
$this->userManager->get($userId)
|
if (!isset($this->userBoardCache[$userId]) || !$useCache) {
|
||||||
);
|
$groups = $this->groupManager->getUserGroupIds(
|
||||||
$userBoards = $this->findAllByUser($userId, null, null, $since, $includeArchived);
|
$this->userManager->get($userId)
|
||||||
$groupBoards = $this->findAllByGroups($userId, $groups,null, null, $since, $includeArchived);
|
);
|
||||||
$circleBoards = $this->findAllByCircles($userId, null, null, $since, $includeArchived);
|
$userBoards = $this->findAllByUser($userId, null, null, $since, $includeArchived);
|
||||||
return array_unique(array_merge($userBoards, $groupBoards, $circleBoards));
|
$groupBoards = $this->findAllByGroups($userId, $groups, null, null, $since, $includeArchived);
|
||||||
|
$circleBoards = $this->findAllByCircles($userId, null, null, $since, $includeArchived);
|
||||||
|
$allBoards = array_unique(array_merge($userBoards, $groupBoards, $circleBoards));
|
||||||
|
if ($useCache) {
|
||||||
|
$this->userBoardCache[$userId] = $allBoards;
|
||||||
|
}
|
||||||
|
return $allBoards;
|
||||||
|
}
|
||||||
|
return $this->userBoardCache[$userId];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -248,7 +265,7 @@ class BoardMapper extends DeckMapper implements IPermissionMapper {
|
|||||||
if ($user !== null) {
|
if ($user !== null) {
|
||||||
return new User($user);
|
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;
|
return null;
|
||||||
}
|
}
|
||||||
if ($acl->getType() === Acl::PERMISSION_TYPE_GROUP) {
|
if ($acl->getType() === Acl::PERMISSION_TYPE_GROUP) {
|
||||||
@@ -256,7 +273,7 @@ class BoardMapper extends DeckMapper implements IPermissionMapper {
|
|||||||
if ($group !== null) {
|
if ($group !== null) {
|
||||||
return new Group($group);
|
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;
|
return null;
|
||||||
}
|
}
|
||||||
if ($acl->getType() === Acl::PERMISSION_TYPE_CIRCLE) {
|
if ($acl->getType() === Acl::PERMISSION_TYPE_CIRCLE) {
|
||||||
@@ -268,11 +285,12 @@ class BoardMapper extends DeckMapper implements IPermissionMapper {
|
|||||||
if ($circle) {
|
if ($circle) {
|
||||||
return new Circle($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;
|
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;
|
return null;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ class Card extends RelationalEntity {
|
|||||||
protected $notified = false;
|
protected $notified = false;
|
||||||
protected $deletedAt = 0;
|
protected $deletedAt = 0;
|
||||||
protected $commentsUnread = 0;
|
protected $commentsUnread = 0;
|
||||||
|
protected $commentsCount = 0;
|
||||||
|
|
||||||
protected $relatedStack = null;
|
protected $relatedStack = null;
|
||||||
protected $relatedBoard = null;
|
protected $relatedBoard = null;
|
||||||
@@ -75,6 +76,7 @@ class Card extends RelationalEntity {
|
|||||||
$this->addRelation('attachmentCount');
|
$this->addRelation('attachmentCount');
|
||||||
$this->addRelation('participants');
|
$this->addRelation('participants');
|
||||||
$this->addRelation('commentsUnread');
|
$this->addRelation('commentsUnread');
|
||||||
|
$this->addRelation('commentsCount');
|
||||||
$this->addResolvable('owner');
|
$this->addResolvable('owner');
|
||||||
|
|
||||||
$this->addRelation('relatedStack');
|
$this->addRelation('relatedStack');
|
||||||
|
|||||||
@@ -321,7 +321,7 @@ class CardMapper extends QBMapper implements IPermissionMapper {
|
|||||||
$this->extendQueryByFilter($qb, $query);
|
$this->extendQueryByFilter($qb, $query);
|
||||||
|
|
||||||
$qb->innerJoin('c', 'comments', 'comments', $qb->expr()->andX(
|
$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->expr()->eq('comments.object_type', $qb->createNamedParameter(Application::COMMENT_ENTITY_TYPE, IQueryBuilder::PARAM_STR))
|
||||||
));
|
));
|
||||||
$qb->selectAlias('comments.id', 'comment_id');
|
$qb->selectAlias('comments.id', 'comment_id');
|
||||||
@@ -339,7 +339,7 @@ class CardMapper extends QBMapper implements IPermissionMapper {
|
|||||||
$tokenMatching
|
$tokenMatching
|
||||||
);
|
);
|
||||||
|
|
||||||
$qb->groupBy('comments.id');
|
$qb->groupBy('comments.id', 'c.id');
|
||||||
$qb->orderBy('comments.id', 'DESC');
|
$qb->orderBy('comments.id', 'DESC');
|
||||||
if ($limit !== null) {
|
if ($limit !== null) {
|
||||||
$qb->setMaxResults($limit);
|
$qb->setMaxResults($limit);
|
||||||
@@ -383,6 +383,10 @@ class CardMapper extends QBMapper implements IPermissionMapper {
|
|||||||
foreach ($query->getDuedate() as $duedate) {
|
foreach ($query->getDuedate() as $duedate) {
|
||||||
$dueDateColumn = $this->databaseType === 'sqlite3' ? $qb->createFunction('DATETIME(`c`.`duedate`)') : 'c.duedate';
|
$dueDateColumn = $this->databaseType === 'sqlite3' ? $qb->createFunction('DATETIME(`c`.`duedate`)') : 'c.duedate';
|
||||||
$date = $duedate->getValue();
|
$date = $duedate->getValue();
|
||||||
|
if ($date === "") {
|
||||||
|
$qb->andWhere($qb->expr()->isNotNull('c.duedate'));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
$supportedFilters = ['overdue', 'today', 'week', 'month', 'none'];
|
$supportedFilters = ['overdue', 'today', 'week', 'month', 'none'];
|
||||||
if (in_array($date, $supportedFilters, true)) {
|
if (in_array($date, $supportedFilters, true)) {
|
||||||
$currentDate = new DateTime();
|
$currentDate = new DateTime();
|
||||||
@@ -430,6 +434,10 @@ class CardMapper extends QBMapper implements IPermissionMapper {
|
|||||||
foreach ($query->getAssigned() as $index => $assignment) {
|
foreach ($query->getAssigned() as $index => $assignment) {
|
||||||
$qb->innerJoin('c', 'deck_assigned_users', 'au' . $index, $qb->expr()->eq('c.id', 'au' . $index . '.card_id'));
|
$qb->innerJoin('c', 'deck_assigned_users', 'au' . $index, $qb->expr()->eq('c.id', 'au' . $index . '.card_id'));
|
||||||
$assignedQueryValue = $assignment->getValue();
|
$assignedQueryValue = $assignment->getValue();
|
||||||
|
if ($assignedQueryValue === "") {
|
||||||
|
$qb->andWhere($qb->expr()->isNotNull('au' . $index . '.participant'));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
$searchUsers = $this->userManager->searchDisplayName($assignment->getValue());
|
$searchUsers = $this->userManager->searchDisplayName($assignment->getValue());
|
||||||
$users = array_filter($searchUsers, function (IUser $user) use ($assignedQueryValue) {
|
$users = array_filter($searchUsers, function (IUser $user) use ($assignedQueryValue) {
|
||||||
return (mb_strtolower($user->getDisplayName()) === mb_strtolower($assignedQueryValue) || $user->getUID() === $assignedQueryValue);
|
return (mb_strtolower($user->getDisplayName()) === mb_strtolower($assignedQueryValue) || $user->getUID() === $assignedQueryValue);
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ use OCP\AppFramework\Http\JSONResponse;
|
|||||||
use OCP\AppFramework\OCS\OCSException;
|
use OCP\AppFramework\OCS\OCSException;
|
||||||
use OCP\AppFramework\OCSController;
|
use OCP\AppFramework\OCSController;
|
||||||
use OCP\ILogger;
|
use OCP\ILogger;
|
||||||
|
use OCP\IRequest;
|
||||||
use OCP\Util;
|
use OCP\Util;
|
||||||
use OCP\IConfig;
|
use OCP\IConfig;
|
||||||
|
|
||||||
@@ -41,6 +42,8 @@ class ExceptionMiddleware extends Middleware {
|
|||||||
private $logger;
|
private $logger;
|
||||||
/** @var IConfig */
|
/** @var IConfig */
|
||||||
private $config;
|
private $config;
|
||||||
|
/** @var IRequest */
|
||||||
|
private $request;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* SharingMiddleware constructor.
|
* SharingMiddleware constructor.
|
||||||
@@ -48,9 +51,10 @@ class ExceptionMiddleware extends Middleware {
|
|||||||
* @param ILogger $logger
|
* @param ILogger $logger
|
||||||
* @param IConfig $config
|
* @param IConfig $config
|
||||||
*/
|
*/
|
||||||
public function __construct(ILogger $logger, IConfig $config) {
|
public function __construct(ILogger $logger, IConfig $config, IRequest $request) {
|
||||||
$this->logger = $logger;
|
$this->logger = $logger;
|
||||||
$this->config = $config;
|
$this->config = $config;
|
||||||
|
$this->request = $request;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -67,45 +71,10 @@ class ExceptionMiddleware extends Middleware {
|
|||||||
throw $exception;
|
throw $exception;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($exception instanceof ConflictException) {
|
$debugMode = $this->config->getSystemValue('debug', false);
|
||||||
if ($this->config->getSystemValue('loglevel', Util::WARN) === Util::DEBUG) {
|
$exceptionMessage = $debugMode !== true
|
||||||
$this->logger->logException($exception);
|
? '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();
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
// uncatched DoesNotExistExceptions will be thrown when the main entity is not found
|
// 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
|
// we return a 403 so we don't leak information over existing entries
|
||||||
@@ -116,6 +85,43 @@ class ExceptionMiddleware extends Middleware {
|
|||||||
'message' => 'Permission denied'
|
'message' => 'Permission denied'
|
||||||
], 403);
|
], 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;
|
throw $exception;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @copyright Copyright (c) 2017 Julius Härtl <jus@bitgrid.net>
|
* @copyright Copyright (c) 2017 Julius Härtl <jus@bitgrid.net>
|
||||||
*
|
*
|
||||||
@@ -24,19 +27,24 @@
|
|||||||
namespace OCA\Deck\Notification;
|
namespace OCA\Deck\Notification;
|
||||||
|
|
||||||
use DateTime;
|
use DateTime;
|
||||||
|
use Exception;
|
||||||
use OCA\Deck\AppInfo\Application;
|
use OCA\Deck\AppInfo\Application;
|
||||||
use OCA\Deck\Db\Acl;
|
use OCA\Deck\Db\Acl;
|
||||||
use OCA\Deck\Db\AssignmentMapper;
|
use OCA\Deck\Db\AssignmentMapper;
|
||||||
use OCA\Deck\Db\Board;
|
use OCA\Deck\Db\Board;
|
||||||
use OCA\Deck\Db\BoardMapper;
|
use OCA\Deck\Db\BoardMapper;
|
||||||
|
use OCA\Deck\Db\Card;
|
||||||
use OCA\Deck\Db\CardMapper;
|
use OCA\Deck\Db\CardMapper;
|
||||||
use OCA\Deck\Db\User;
|
use OCA\Deck\Db\User;
|
||||||
use OCA\Deck\Service\ConfigService;
|
use OCA\Deck\Service\ConfigService;
|
||||||
use OCA\Deck\Service\PermissionService;
|
use OCA\Deck\Service\PermissionService;
|
||||||
|
use OCP\AppFramework\Db\DoesNotExistException;
|
||||||
|
use OCP\AppFramework\Db\MultipleObjectsReturnedException;
|
||||||
use OCP\Comments\IComment;
|
use OCP\Comments\IComment;
|
||||||
use OCP\IConfig;
|
use OCP\IConfig;
|
||||||
use OCP\IGroupManager;
|
use OCP\IGroupManager;
|
||||||
use OCP\Notification\IManager;
|
use OCP\Notification\IManager;
|
||||||
|
use OCP\Notification\INotification;
|
||||||
|
|
||||||
class NotificationHelper {
|
class NotificationHelper {
|
||||||
|
|
||||||
@@ -80,10 +88,10 @@ class NotificationHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param $card
|
* @throws DoesNotExistException
|
||||||
* @throws \OCP\AppFramework\Db\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
|
// check if notification has already been sent
|
||||||
// ideally notifications should not be deleted once seen by the user so we can
|
// 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
|
// also deliver due date notifications for users who have been added later to a board
|
||||||
@@ -117,7 +125,7 @@ class NotificationHelper {
|
|||||||
$notification
|
$notification
|
||||||
->setApp('deck')
|
->setApp('deck')
|
||||||
->setUser((string)$user->getUID())
|
->setUser((string)$user->getUID())
|
||||||
->setObject('card', $card->getId())
|
->setObject('card', (string)$card->getId())
|
||||||
->setSubject('card-overdue', [
|
->setSubject('card-overdue', [
|
||||||
$card->getTitle(), $board->getTitle()
|
$card->getTitle(), $board->getTitle()
|
||||||
])
|
])
|
||||||
@@ -128,25 +136,29 @@ class NotificationHelper {
|
|||||||
$this->cardMapper->markNotified($card);
|
$this->cardMapper->markNotified($card);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function markDuedateAsRead($card) {
|
public function markDuedateAsRead(Card $card): void {
|
||||||
$notification = $this->notificationManager->createNotification();
|
$notification = $this->notificationManager->createNotification();
|
||||||
$notification
|
$notification
|
||||||
->setApp('deck')
|
->setApp('deck')
|
||||||
->setObject('card', $card->getId())
|
->setObject('card', (string)$card->getId())
|
||||||
->setSubject('card-overdue', []);
|
->setSubject('card-overdue', []);
|
||||||
$this->notificationManager->markProcessed($notification);
|
$this->notificationManager->markProcessed($notification);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function sendCardAssigned($card, $userId) {
|
public function sendCardAssigned(Card $card, string $userId): void {
|
||||||
$boardId = $this->cardMapper->findBoardId($card->getId());
|
$boardId = $this->cardMapper->findBoardId($card->getId());
|
||||||
$board = $this->getBoard($boardId);
|
try {
|
||||||
|
$board = $this->getBoard($boardId);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
$notification = $this->notificationManager->createNotification();
|
$notification = $this->notificationManager->createNotification();
|
||||||
$notification
|
$notification
|
||||||
->setApp('deck')
|
->setApp('deck')
|
||||||
->setUser((string) $userId)
|
->setUser($userId)
|
||||||
->setDateTime(new DateTime())
|
->setDateTime(new DateTime())
|
||||||
->setObject('card', $card->getId())
|
->setObject('card', (string)$card->getId())
|
||||||
->setSubject('card-assigned', [
|
->setSubject('card-assigned', [
|
||||||
$card->getTitle(),
|
$card->getTitle(),
|
||||||
$board->getTitle(),
|
$board->getTitle(),
|
||||||
@@ -155,29 +167,56 @@ class NotificationHelper {
|
|||||||
$this->notificationManager->notify($notification);
|
$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
|
* 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 {
|
||||||
$board = $this->getBoard($boardId);
|
try {
|
||||||
|
$board = $this->getBoard($boardId);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if ($acl->getType() === Acl::PERMISSION_TYPE_USER) {
|
if ($acl->getType() === Acl::PERMISSION_TYPE_USER) {
|
||||||
$notification = $this->generateBoardShared($board, $acl->getParticipant());
|
$notification = $this->generateBoardShared($board, $acl->getParticipant());
|
||||||
$this->notificationManager->notify($notification);
|
if ($markAsRead) {
|
||||||
|
$this->notificationManager->markProcessed($notification);
|
||||||
|
} else {
|
||||||
|
$notification->setDateTime(new DateTime());
|
||||||
|
$this->notificationManager->notify($notification);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if ($acl->getType() === Acl::PERMISSION_TYPE_GROUP) {
|
if ($acl->getType() === Acl::PERMISSION_TYPE_GROUP) {
|
||||||
$group = $this->groupManager->get($acl->getParticipant());
|
$group = $this->groupManager->get($acl->getParticipant());
|
||||||
|
if ($group === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
foreach ($group->getUsers() as $user) {
|
foreach ($group->getUsers() as $user) {
|
||||||
|
if ($user->getUID() === $this->currentUser) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
$notification = $this->generateBoardShared($board, $user->getUID());
|
$notification = $this->generateBoardShared($board, $user->getUID());
|
||||||
$this->notificationManager->notify($notification);
|
if ($markAsRead) {
|
||||||
|
$this->notificationManager->markProcessed($notification);
|
||||||
|
} else {
|
||||||
|
$notification->setDateTime(new DateTime());
|
||||||
|
$this->notificationManager->notify($notification);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function sendMention(IComment $comment) {
|
public function sendMention(IComment $comment): void {
|
||||||
foreach ($comment->getMentions() as $mention) {
|
foreach ($comment->getMentions() as $mention) {
|
||||||
$card = $this->cardMapper->find($comment->getObjectId());
|
$card = $this->cardMapper->find($comment->getObjectId());
|
||||||
$boardId = $this->cardMapper->findBoardId($card->getId());
|
$boardId = $this->cardMapper->findBoardId($card->getId());
|
||||||
@@ -194,27 +233,22 @@ class NotificationHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param $boardId
|
* @throws DoesNotExistException
|
||||||
* @return Board
|
* @throws MultipleObjectsReturnedException
|
||||||
* @throws \OCP\AppFramework\Db\DoesNotExistException
|
|
||||||
*/
|
*/
|
||||||
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)) {
|
if (!array_key_exists($boardId, $this->boards)) {
|
||||||
$this->boards[$boardId] = $this->boardMapper->find($boardId, $withLabels, $withAcl);
|
$this->boards[$boardId] = $this->boardMapper->find($boardId, $withLabels, $withAcl);
|
||||||
}
|
}
|
||||||
return $this->boards[$boardId];
|
return $this->boards[$boardId];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private function generateBoardShared(Board $board, string $userId): INotification {
|
||||||
* @param Board $board
|
|
||||||
*/
|
|
||||||
private function generateBoardShared($board, $userId) {
|
|
||||||
$notification = $this->notificationManager->createNotification();
|
$notification = $this->notificationManager->createNotification();
|
||||||
$notification
|
$notification
|
||||||
->setApp('deck')
|
->setApp('deck')
|
||||||
->setUser((string) $userId)
|
->setUser($userId)
|
||||||
->setDateTime(new DateTime())
|
->setObject('board', (string)$board->getId())
|
||||||
->setObject('board', $board->getId())
|
|
||||||
->setSubject('board-shared', [$board->getTitle(), $this->currentUser]);
|
->setSubject('board-shared', [$board->getTitle(), $this->currentUser]);
|
||||||
return $notification;
|
return $notification;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ namespace OCA\Deck\Notification;
|
|||||||
|
|
||||||
use OCA\Deck\Db\BoardMapper;
|
use OCA\Deck\Db\BoardMapper;
|
||||||
use OCA\Deck\Db\CardMapper;
|
use OCA\Deck\Db\CardMapper;
|
||||||
|
use OCA\Deck\Db\StackMapper;
|
||||||
use OCP\IURLGenerator;
|
use OCP\IURLGenerator;
|
||||||
use OCP\IUserManager;
|
use OCP\IUserManager;
|
||||||
use OCP\L10N\IFactory;
|
use OCP\L10N\IFactory;
|
||||||
@@ -41,6 +42,8 @@ class Notifier implements INotifier {
|
|||||||
protected $userManager;
|
protected $userManager;
|
||||||
/** @var CardMapper */
|
/** @var CardMapper */
|
||||||
protected $cardMapper;
|
protected $cardMapper;
|
||||||
|
/** @var StackMapper */
|
||||||
|
protected $stackMapper;
|
||||||
/** @var BoardMapper */
|
/** @var BoardMapper */
|
||||||
protected $boardMapper;
|
protected $boardMapper;
|
||||||
|
|
||||||
@@ -49,12 +52,14 @@ class Notifier implements INotifier {
|
|||||||
IURLGenerator $url,
|
IURLGenerator $url,
|
||||||
IUserManager $userManager,
|
IUserManager $userManager,
|
||||||
CardMapper $cardMapper,
|
CardMapper $cardMapper,
|
||||||
|
StackMapper $stackMapper,
|
||||||
BoardMapper $boardMapper
|
BoardMapper $boardMapper
|
||||||
) {
|
) {
|
||||||
$this->l10nFactory = $l10nFactory;
|
$this->l10nFactory = $l10nFactory;
|
||||||
$this->url = $url;
|
$this->url = $url;
|
||||||
$this->userManager = $userManager;
|
$this->userManager = $userManager;
|
||||||
$this->cardMapper = $cardMapper;
|
$this->cardMapper = $cardMapper;
|
||||||
|
$this->stackMapper = $stackMapper;
|
||||||
$this->boardMapper = $boardMapper;
|
$this->boardMapper = $boardMapper;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,6 +105,11 @@ class Notifier implements INotifier {
|
|||||||
if (!$boardId) {
|
if (!$boardId) {
|
||||||
throw new AlreadyProcessedException();
|
throw new AlreadyProcessedException();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$card = $this->cardMapper->find($cardId);
|
||||||
|
$stackId = $card->getStackId();
|
||||||
|
$stack = $this->stackMapper->find($stackId);
|
||||||
|
|
||||||
$initiator = $this->userManager->get($params[2]);
|
$initiator = $this->userManager->get($params[2]);
|
||||||
if ($initiator !== null) {
|
if ($initiator !== null) {
|
||||||
$dn = $initiator->getDisplayName();
|
$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])
|
(string) $l->t('The card "%s" on "%s" has been assigned to you by %s.', [$params[0], $params[1], $dn])
|
||||||
);
|
);
|
||||||
$notification->setRichSubject(
|
$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' => [
|
'user' => [
|
||||||
'type' => 'user',
|
'type' => 'user',
|
||||||
'id' => $params[2],
|
'id' => $params[2],
|
||||||
@@ -127,9 +151,33 @@ class Notifier implements INotifier {
|
|||||||
if (!$boardId) {
|
if (!$boardId) {
|
||||||
throw new AlreadyProcessedException();
|
throw new AlreadyProcessedException();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$card = $this->cardMapper->find($cardId);
|
||||||
|
$stackId = $card->getStackId();
|
||||||
|
$stack = $this->stackMapper->find($stackId);
|
||||||
|
|
||||||
$notification->setParsedSubject(
|
$notification->setParsedSubject(
|
||||||
(string) $l->t('The card "%s" on "%s" has reached its due date.', $params)
|
(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 . '');
|
$notification->setLink($this->url->linkToRouteAbsolute('deck.page.index') . '#/board/' . $boardId . '/card/' . $cardId . '');
|
||||||
break;
|
break;
|
||||||
case 'card-comment-mentioned':
|
case 'card-comment-mentioned':
|
||||||
@@ -138,6 +186,11 @@ class Notifier implements INotifier {
|
|||||||
if (!$boardId) {
|
if (!$boardId) {
|
||||||
throw new AlreadyProcessedException();
|
throw new AlreadyProcessedException();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$card = $this->cardMapper->find($cardId);
|
||||||
|
$stackId = $card->getStackId();
|
||||||
|
$stack = $this->stackMapper->find($stackId);
|
||||||
|
|
||||||
$initiator = $this->userManager->get($params[2]);
|
$initiator = $this->userManager->get($params[2]);
|
||||||
if ($initiator !== null) {
|
if ($initiator !== null) {
|
||||||
$dn = $initiator->getDisplayName();
|
$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]])
|
(string) $l->t('%s has mentioned you in a comment on "%s".', [$dn, $params[0]])
|
||||||
);
|
);
|
||||||
$notification->setRichSubject(
|
$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' => [
|
'user' => [
|
||||||
'type' => 'user',
|
'type' => 'user',
|
||||||
'id' => $params[2],
|
'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])
|
(string) $l->t('The board "%s" has been shared with you by %s.', [$params[0], $dn])
|
||||||
);
|
);
|
||||||
$notification->setRichSubject(
|
$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' => [
|
'user' => [
|
||||||
'type' => 'user',
|
'type' => 'user',
|
||||||
'id' => $params[1],
|
'id' => $params[1],
|
||||||
|
|||||||
@@ -63,29 +63,52 @@ class DeckProvider implements IProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public function search(IUser $user, ISearchQuery $query): SearchResult {
|
public function search(IUser $user, ISearchQuery $query): SearchResult {
|
||||||
$cursor = $query->getCursor() !== null ? (int)$query->getCursor() : null;
|
$cursor = $query->getCursor();
|
||||||
$boardResults = $this->searchService->searchBoards($query->getTerm(), $query->getLimit(), $cursor);
|
[$boardCursor, $cardCursor] = $this->parseCursor($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)
|
|
||||||
);
|
|
||||||
|
|
||||||
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(
|
return SearchResult::complete(
|
||||||
'Deck',
|
'Deck',
|
||||||
$results
|
$resultEntries
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$newCursor = $this->getNewCursor($boardObjects, $cardObjects);
|
||||||
return SearchResult::paginated(
|
return SearchResult::paginated(
|
||||||
'Deck',
|
'Deck',
|
||||||
$results,
|
$resultEntries,
|
||||||
$cardResults[count($results) - 1]->getLastModified()
|
$newCursor
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,4 +118,27 @@ class DeckProvider implements IProvider {
|
|||||||
}
|
}
|
||||||
return 10;
|
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 (min($boardTimestamps) ?: '') . '|' . (min($cardTimestamps) ?: '');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ class AQueryParameter {
|
|||||||
|
|
||||||
public function getValue() {
|
public function getValue() {
|
||||||
if (is_string($this->value) && mb_strlen($this->value) > 1) {
|
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 $param;
|
||||||
}
|
}
|
||||||
return $this->value;
|
return $this->value;
|
||||||
|
|||||||
@@ -74,6 +74,8 @@ class AssignmentService {
|
|||||||
* @var IEventDispatcher
|
* @var IEventDispatcher
|
||||||
*/
|
*/
|
||||||
private $eventDispatcher;
|
private $eventDispatcher;
|
||||||
|
/** @var string|null */
|
||||||
|
private $currentUser;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
PermissionService $permissionService,
|
PermissionService $permissionService,
|
||||||
@@ -138,8 +140,7 @@ class AssignmentService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if ($userId !== $this->currentUser) {
|
if ($type === Assignment::TYPE_USER && $userId !== $this->currentUser) {
|
||||||
/* Notifyuser about the card assignment */
|
|
||||||
$this->notificationHelper->sendCardAssigned($card, $userId);
|
$this->notificationHelper->sendCardAssigned($card, $userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -183,8 +184,12 @@ class AssignmentService {
|
|||||||
$assignment = $this->assignedUsersMapper->delete($assignment);
|
$assignment = $this->assignedUsersMapper->delete($assignment);
|
||||||
$card = $this->cardMapper->find($cardId);
|
$card = $this->cardMapper->find($cardId);
|
||||||
$this->activityManager->triggerEvent(ActivityManager::DECK_OBJECT_CARD, $card, ActivityManager::SUBJECT_CARD_USER_UNASSIGN, ['assigneduser' => $userId]);
|
$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->changeHelper->cardChanged($cardId);
|
||||||
|
|
||||||
|
|
||||||
$this->eventDispatcher->dispatchTyped(new CardUpdatedEvent($card));
|
$this->eventDispatcher->dispatchTyped(new CardUpdatedEvent($card));
|
||||||
|
|
||||||
return $assignment;
|
return $assignment;
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ use OCA\Deck\InvalidAttachmentType;
|
|||||||
use OCA\Deck\NoPermissionException;
|
use OCA\Deck\NoPermissionException;
|
||||||
use OCA\Deck\NotFoundException;
|
use OCA\Deck\NotFoundException;
|
||||||
use OCA\Deck\StatusException;
|
use OCA\Deck\StatusException;
|
||||||
|
use OCP\AppFramework\Db\IMapperException;
|
||||||
use OCP\AppFramework\Http\Response;
|
use OCP\AppFramework\Http\Response;
|
||||||
use OCP\ICache;
|
use OCP\ICache;
|
||||||
use OCP\ICacheFactory;
|
use OCP\ICacheFactory;
|
||||||
@@ -320,14 +321,10 @@ class AttachmentService {
|
|||||||
* Either mark an attachment as deleted for later removal or just remove it depending
|
* Either mark an attachment as deleted for later removal or just remove it depending
|
||||||
* on the IAttachmentService implementation
|
* on the IAttachmentService implementation
|
||||||
*
|
*
|
||||||
* @param $attachmentId
|
* @throws NoPermissionException
|
||||||
* @return \OCP\AppFramework\Db\Entity
|
* @throws NotFoundException
|
||||||
* @throws \OCA\Deck\NoPermissionException
|
|
||||||
* @throws \OCP\AppFramework\Db\DoesNotExistException
|
|
||||||
* @throws \OCP\AppFramework\Db\MultipleObjectsReturnedException
|
|
||||||
* @throws BadRequestException
|
|
||||||
*/
|
*/
|
||||||
public function delete($cardId, $attachmentId, $type = 'deck_file') {
|
public function delete(int $cardId, int $attachmentId, string $type = 'deck_file'): Attachment {
|
||||||
try {
|
try {
|
||||||
$service = $this->getService($type);
|
$service = $this->getService($type);
|
||||||
} catch (InvalidAttachmentType $e) {
|
} catch (InvalidAttachmentType $e) {
|
||||||
@@ -340,40 +337,32 @@ class AttachmentService {
|
|||||||
$attachment->setType($type);
|
$attachment->setType($type);
|
||||||
$attachment->setCardId($cardId);
|
$attachment->setCardId($cardId);
|
||||||
$service->extendData($attachment);
|
$service->extendData($attachment);
|
||||||
$service->delete($attachment);
|
} else {
|
||||||
$this->changeHelper->cardChanged($attachment->getCardId());
|
try {
|
||||||
$this->activityManager->triggerEvent(ActivityManager::DECK_OBJECT_CARD, $attachment, ActivityManager::SUBJECT_ATTACHMENT_DELETE);
|
$attachment = $this->attachmentMapper->find($attachmentId);
|
||||||
return $attachment;
|
} catch (IMapperException $e) {
|
||||||
|
throw new NoPermissionException('Permission denied');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
|
||||||
$attachment = $this->attachmentMapper->find($attachmentId);
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
throw new NoPermissionException('Permission denied');
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->permissionService->checkPermission($this->cardMapper, $attachment->getCardId(), Acl::PERMISSION_EDIT);
|
$this->permissionService->checkPermission($this->cardMapper, $attachment->getCardId(), Acl::PERMISSION_EDIT);
|
||||||
$this->cache->clear('card-' . $attachment->getCardId());
|
|
||||||
|
|
||||||
if ($service->allowUndo()) {
|
if ($service->allowUndo()) {
|
||||||
$service->markAsDeleted($attachment);
|
$service->markAsDeleted($attachment);
|
||||||
$this->activityManager->triggerEvent(ActivityManager::DECK_OBJECT_CARD, $attachment, ActivityManager::SUBJECT_ATTACHMENT_DELETE);
|
$attachment = $this->attachmentMapper->update($attachment);
|
||||||
$this->changeHelper->cardChanged($attachment->getCardId());
|
} else {
|
||||||
return $this->attachmentMapper->update($attachment);
|
$service->delete($attachment);
|
||||||
|
if (!$service instanceof ICustomAttachmentService) {
|
||||||
|
$attachment = $this->attachmentMapper->delete($attachment);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
$service->delete($attachment);
|
|
||||||
|
|
||||||
$attachment = $this->attachmentMapper->delete($attachment);
|
$this->cache->clear('card-' . $attachment->getCardId());
|
||||||
$this->changeHelper->cardChanged($attachment->getCardId());
|
$this->changeHelper->cardChanged($attachment->getCardId());
|
||||||
$this->activityManager->triggerEvent(ActivityManager::DECK_OBJECT_CARD, $attachment, ActivityManager::SUBJECT_ATTACHMENT_DELETE);
|
$this->activityManager->triggerEvent(ActivityManager::DECK_OBJECT_CARD, $attachment, ActivityManager::SUBJECT_ATTACHMENT_DELETE);
|
||||||
return $attachment;
|
return $attachment;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function restore($cardId, $attachmentId, $type = 'deck_file') {
|
public function restore(int $cardId, int $attachmentId, string $type = 'deck_file'): Attachment {
|
||||||
if (is_numeric($attachmentId) === false) {
|
|
||||||
throw new BadRequestException('attachment id must be a number');
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$attachment = $this->attachmentMapper->find($attachmentId);
|
$attachment = $this->attachmentMapper->find($attachmentId);
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
|
|||||||
@@ -510,11 +510,10 @@ class BoardService {
|
|||||||
$acl->setPermissionEdit($edit);
|
$acl->setPermissionEdit($edit);
|
||||||
$acl->setPermissionShare($share);
|
$acl->setPermissionShare($share);
|
||||||
$acl->setPermissionManage($manage);
|
$acl->setPermissionManage($manage);
|
||||||
|
|
||||||
$this->notificationHelper->sendBoardShared($boardId, $acl);
|
|
||||||
|
|
||||||
$newAcl = $this->aclMapper->insert($acl);
|
$newAcl = $this->aclMapper->insert($acl);
|
||||||
|
|
||||||
$this->activityManager->triggerEvent(ActivityManager::DECK_OBJECT_BOARD, $newAcl, ActivityManager::SUBJECT_BOARD_SHARE);
|
$this->activityManager->triggerEvent(ActivityManager::DECK_OBJECT_BOARD, $newAcl, ActivityManager::SUBJECT_BOARD_SHARE);
|
||||||
|
$this->notificationHelper->sendBoardShared((int)$boardId, $acl);
|
||||||
$this->boardMapper->mapAcl($newAcl);
|
$this->boardMapper->mapAcl($newAcl);
|
||||||
$this->changeHelper->boardChanged($boardId);
|
$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->activityManager->triggerEvent(ActivityManager::DECK_OBJECT_BOARD, $acl, ActivityManager::SUBJECT_BOARD_UNSHARE);
|
||||||
|
$this->notificationHelper->sendBoardShared($acl->getBoardId(), $acl, true);
|
||||||
$this->changeHelper->boardChanged($acl->getBoardId());
|
$this->changeHelper->boardChanged($acl->getBoardId());
|
||||||
|
|
||||||
$version = \OCP\Util::getVersion()[0];
|
$version = \OCP\Util::getVersion()[0];
|
||||||
|
|||||||
@@ -105,8 +105,10 @@ class CardService {
|
|||||||
$card->setAttachmentCount($this->attachmentService->count($cardId));
|
$card->setAttachmentCount($this->attachmentService->count($cardId));
|
||||||
$user = $this->userManager->get($this->currentUser);
|
$user = $this->userManager->get($this->currentUser);
|
||||||
$lastRead = $this->commentsManager->getReadMark('deckCard', (string)$card->getId(), $user);
|
$lastRead = $this->commentsManager->getReadMark('deckCard', (string)$card->getId(), $user);
|
||||||
$count = $this->commentsManager->getNumberOfCommentsForObject('deckCard', (string)$card->getId(), $lastRead);
|
$countUnreadComments = $this->commentsManager->getNumberOfCommentsForObject('deckCard', (string)$card->getId(), $lastRead);
|
||||||
$card->setCommentsUnread($count);
|
$countComments = $this->commentsManager->getNumberOfCommentsForObject('deckCard', (string)$card->getId());
|
||||||
|
$card->setCommentsUnread($countUnreadComments);
|
||||||
|
$card->setCommentsCount($countComments);
|
||||||
|
|
||||||
$stack = $this->stackMapper->find($card->getStackId());
|
$stack = $this->stackMapper->find($card->getStackId());
|
||||||
$board = $this->boardService->find($stack->getBoardId());
|
$board = $this->boardService->find($stack->getBoardId());
|
||||||
@@ -243,6 +245,7 @@ class CardService {
|
|||||||
$this->cardMapper->update($card);
|
$this->cardMapper->update($card);
|
||||||
|
|
||||||
$this->activityManager->triggerEvent(ActivityManager::DECK_OBJECT_CARD, $card, ActivityManager::SUBJECT_CARD_DELETE);
|
$this->activityManager->triggerEvent(ActivityManager::DECK_OBJECT_CARD, $card, ActivityManager::SUBJECT_CARD_DELETE);
|
||||||
|
$this->notificationHelper->markDuedateAsRead($card);
|
||||||
$this->changeHelper->cardChanged($card->getId(), false);
|
$this->changeHelper->cardChanged($card->getId(), false);
|
||||||
$this->eventDispatcher->dispatchTyped(new CardDeletedEvent($card));
|
$this->eventDispatcher->dispatchTyped(new CardDeletedEvent($card));
|
||||||
|
|
||||||
@@ -322,6 +325,15 @@ class CardService {
|
|||||||
$card->setOrder($order);
|
$card->setOrder($order);
|
||||||
$card->setOwner($owner);
|
$card->setOwner($owner);
|
||||||
$card->setDuedate($duedate);
|
$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) {
|
if ($deletedAt !== null) {
|
||||||
$card->setDeletedAt($deletedAt);
|
$card->setDeletedAt($deletedAt);
|
||||||
}
|
}
|
||||||
@@ -341,6 +353,9 @@ class CardService {
|
|||||||
|
|
||||||
|
|
||||||
$card = $this->cardMapper->update($card);
|
$card = $this->cardMapper->update($card);
|
||||||
|
if ($resetDuedateNotification) {
|
||||||
|
$this->notificationHelper->markDuedateAsRead($card);
|
||||||
|
}
|
||||||
$this->changeHelper->cardChanged($card->getId(), true);
|
$this->changeHelper->cardChanged($card->getId(), true);
|
||||||
|
|
||||||
$this->eventDispatcher->dispatchTyped(new CardUpdatedEvent($card));
|
$this->eventDispatcher->dispatchTyped(new CardUpdatedEvent($card));
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace OCA\Deck\Service;
|
namespace OCA\Deck\Service;
|
||||||
|
|
||||||
|
use OCA\Circles\Api\v1\Circles;
|
||||||
use OCP\App\IAppManager;
|
use OCP\App\IAppManager;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -53,8 +54,8 @@ class CirclesService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
\OCA\Circles\Api\v1\Circles::getMember($circleId, $userId, 1, true);
|
$member = \OCA\Circles\Api\v1\Circles::getMember($circleId, $userId, 1, true);
|
||||||
return true;
|
return $member->getLevel() >= Circles::LEVEL_MEMBER;
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -73,16 +73,20 @@ class ConfigService {
|
|||||||
if (!$this->groupManager->isAdmin($this->userId)) {
|
if (!$this->groupManager->isAdmin($this->userId)) {
|
||||||
throw new NoPermissionException('You must be admin to get the group limit');
|
throw new NoPermissionException('You must be admin to get the group limit');
|
||||||
}
|
}
|
||||||
$result = $this->getGroupLimit();
|
return $this->getGroupLimit();
|
||||||
break;
|
|
||||||
case 'calendar':
|
case 'calendar':
|
||||||
$result = (bool)$this->config->getUserValue($this->userId, Application::APP_ID, 'calendar', true);
|
if ($this->userId === null) {
|
||||||
break;
|
return false;
|
||||||
|
}
|
||||||
|
return (bool)$this->config->getUserValue($this->userId, Application::APP_ID, 'calendar', true);
|
||||||
}
|
}
|
||||||
return $result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function isCalendarEnabled(int $boardId = null): bool {
|
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);
|
$defaultState = (bool)$this->config->getUserValue($this->userId, Application::APP_ID, 'calendar', true);
|
||||||
if ($boardId === null) {
|
if ($boardId === null) {
|
||||||
return $defaultState;
|
return $defaultState;
|
||||||
|
|||||||
@@ -23,7 +23,10 @@
|
|||||||
|
|
||||||
namespace OCA\Deck\Service;
|
namespace OCA\Deck\Service;
|
||||||
|
|
||||||
|
use OCA\Deck\Db\Acl;
|
||||||
use OCA\Deck\Db\Attachment;
|
use OCA\Deck\Db\Attachment;
|
||||||
|
use OCA\Deck\Db\CardMapper;
|
||||||
|
use OCA\Deck\NoPermissionException;
|
||||||
use OCA\Deck\Sharing\DeckShareProvider;
|
use OCA\Deck\Sharing\DeckShareProvider;
|
||||||
use OCA\Deck\StatusException;
|
use OCA\Deck\StatusException;
|
||||||
use OCP\AppFramework\Http\StreamResponse;
|
use OCP\AppFramework\Http\StreamResponse;
|
||||||
@@ -38,6 +41,7 @@ use OCP\IRequest;
|
|||||||
use OCP\Share\Exceptions\ShareNotFound;
|
use OCP\Share\Exceptions\ShareNotFound;
|
||||||
use OCP\Share\IManager;
|
use OCP\Share\IManager;
|
||||||
use OCP\Share\IShare;
|
use OCP\Share\IShare;
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
|
||||||
class FilesAppService implements IAttachmentService, ICustomAttachmentService {
|
class FilesAppService implements IAttachmentService, ICustomAttachmentService {
|
||||||
private $request;
|
private $request;
|
||||||
@@ -48,8 +52,10 @@ class FilesAppService implements IAttachmentService, ICustomAttachmentService {
|
|||||||
private $configService;
|
private $configService;
|
||||||
private $l10n;
|
private $l10n;
|
||||||
private $preview;
|
private $preview;
|
||||||
private $permissionService;
|
|
||||||
private $mimeTypeDetector;
|
private $mimeTypeDetector;
|
||||||
|
private $permissionService;
|
||||||
|
private $cardMapper;
|
||||||
|
private $logger;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
IRequest $request,
|
IRequest $request,
|
||||||
@@ -59,8 +65,10 @@ class FilesAppService implements IAttachmentService, ICustomAttachmentService {
|
|||||||
ConfigService $configService,
|
ConfigService $configService,
|
||||||
DeckShareProvider $shareProvider,
|
DeckShareProvider $shareProvider,
|
||||||
IPreview $preview,
|
IPreview $preview,
|
||||||
PermissionService $permissionService,
|
|
||||||
IMimeTypeDetector $mimeTypeDetector,
|
IMimeTypeDetector $mimeTypeDetector,
|
||||||
|
PermissionService $permissionService,
|
||||||
|
CardMapper $cardMapper,
|
||||||
|
LoggerInterface $logger,
|
||||||
string $userId = null
|
string $userId = null
|
||||||
) {
|
) {
|
||||||
$this->request = $request;
|
$this->request = $request;
|
||||||
@@ -72,15 +80,20 @@ class FilesAppService implements IAttachmentService, ICustomAttachmentService {
|
|||||||
$this->userId = $userId;
|
$this->userId = $userId;
|
||||||
$this->preview = $preview;
|
$this->preview = $preview;
|
||||||
$this->mimeTypeDetector = $mimeTypeDetector;
|
$this->mimeTypeDetector = $mimeTypeDetector;
|
||||||
|
$this->permissionService = $permissionService;
|
||||||
|
$this->cardMapper = $cardMapper;
|
||||||
|
$this->logger = $logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function listAttachments(int $cardId): array {
|
public function listAttachments(int $cardId): array {
|
||||||
$shares = $this->shareProvider->getSharedWithByType($cardId, IShare::TYPE_DECK, -1, 0);
|
$shares = $this->shareProvider->getSharedWithByType($cardId, IShare::TYPE_DECK, -1, 0);
|
||||||
$shares = array_filter($shares, function ($share) {
|
return array_filter(array_map(function (IShare $share) use ($cardId) {
|
||||||
return $share->getPermissions() > 0;
|
try {
|
||||||
});
|
$file = $share->getNode();
|
||||||
return array_map(function (IShare $share) use ($cardId) {
|
} catch (NotFoundException $e) {
|
||||||
$file = $share->getNode();
|
$this->logger->debug('Unable to find node for share with ID ' . $share->getId());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
$attachment = new Attachment();
|
$attachment = new Attachment();
|
||||||
$attachment->setType('file');
|
$attachment->setType('file');
|
||||||
$attachment->setId((int)$share->getId());
|
$attachment->setId((int)$share->getId());
|
||||||
@@ -89,9 +102,9 @@ class FilesAppService implements IAttachmentService, ICustomAttachmentService {
|
|||||||
$attachment->setData($file->getName());
|
$attachment->setData($file->getName());
|
||||||
$attachment->setLastModified($file->getMTime());
|
$attachment->setLastModified($file->getMTime());
|
||||||
$attachment->setCreatedAt($share->getShareTime()->getTimestamp());
|
$attachment->setCreatedAt($share->getShareTime()->getTimestamp());
|
||||||
$attachment->setDeletedAt(0);
|
$attachment->setDeletedAt($share->getPermissions() === 0 ? $share->getShareTime()->getTimestamp() : 0);
|
||||||
return $attachment;
|
return $attachment;
|
||||||
}, $shares);
|
}, $shares));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getAttachmentCount(int $cardId): int {
|
public function getAttachmentCount(int $cardId): int {
|
||||||
@@ -125,7 +138,11 @@ class FilesAppService implements IAttachmentService, ICustomAttachmentService {
|
|||||||
public function extendData(Attachment $attachment) {
|
public function extendData(Attachment $attachment) {
|
||||||
$userFolder = $this->rootFolder->getUserFolder($this->userId);
|
$userFolder = $this->rootFolder->getUserFolder($this->userId);
|
||||||
$share = $this->shareProvider->getShareById($attachment->getId());
|
$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([
|
$attachment->setExtendedData([
|
||||||
'path' => $userFolder->getRelativePath($file->getPath()),
|
'path' => $userFolder->getRelativePath($file->getPath()),
|
||||||
'fileid' => $file->getId(),
|
'fileid' => $file->getId(),
|
||||||
@@ -140,6 +157,7 @@ class FilesAppService implements IAttachmentService, ICustomAttachmentService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public function display(Attachment $attachment) {
|
public function display(Attachment $attachment) {
|
||||||
|
// Problem: Folders
|
||||||
/** @psalm-suppress InvalidCatch */
|
/** @psalm-suppress InvalidCatch */
|
||||||
try {
|
try {
|
||||||
$share = $this->shareProvider->getShareById($attachment->getId());
|
$share = $this->shareProvider->getShareById($attachment->getId());
|
||||||
@@ -161,6 +179,9 @@ class FilesAppService implements IAttachmentService, ICustomAttachmentService {
|
|||||||
$file = $this->getUploadedFile();
|
$file = $this->getUploadedFile();
|
||||||
$fileName = $file['name'];
|
$fileName = $file['name'];
|
||||||
|
|
||||||
|
// get shares for current card
|
||||||
|
// check if similar filename already exists
|
||||||
|
|
||||||
$userFolder = $this->rootFolder->getUserFolder($this->userId);
|
$userFolder = $this->rootFolder->getUserFolder($this->userId);
|
||||||
try {
|
try {
|
||||||
$folder = $userFolder->get($this->configService->getAttachmentFolder());
|
$folder = $userFolder->get($this->configService->getAttachmentFolder());
|
||||||
@@ -241,12 +262,16 @@ class FilesAppService implements IAttachmentService, ICustomAttachmentService {
|
|||||||
$file = $share->getNode();
|
$file = $share->getNode();
|
||||||
$attachment->setData($file->getName());
|
$attachment->setData($file->getName());
|
||||||
|
|
||||||
if ($file->getOwner() !== null && $file->getOwner()->getUID() === $this->userId) {
|
// Deleting a Nextcloud file attachment will remove the share to the card, keeping the source file untouched
|
||||||
$file->delete();
|
// 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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->shareManager->deleteFromSelf($share, $this->userId);
|
throw new NoPermissionException('No permission to remove the attachment from the card');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function allowUndo() {
|
public function allowUndo() {
|
||||||
|
|||||||
@@ -23,6 +23,8 @@
|
|||||||
|
|
||||||
namespace OCA\Deck\Service;
|
namespace OCA\Deck\Service;
|
||||||
|
|
||||||
|
use OC\Cache\CappedMemoryCache;
|
||||||
|
use OCA\Circles\Model\Member;
|
||||||
use OCA\Deck\Db\Acl;
|
use OCA\Deck\Db\Acl;
|
||||||
use OCA\Deck\Db\AclMapper;
|
use OCA\Deck\Db\AclMapper;
|
||||||
use OCA\Deck\Db\Board;
|
use OCA\Deck\Db\Board;
|
||||||
@@ -31,7 +33,6 @@ use OCA\Deck\Db\IPermissionMapper;
|
|||||||
use OCA\Deck\Db\User;
|
use OCA\Deck\Db\User;
|
||||||
use OCA\Deck\NoPermissionException;
|
use OCA\Deck\NoPermissionException;
|
||||||
use OCP\AppFramework\Db\DoesNotExistException;
|
use OCP\AppFramework\Db\DoesNotExistException;
|
||||||
use OCP\AppFramework\Db\Entity;
|
|
||||||
use OCP\AppFramework\Db\MultipleObjectsReturnedException;
|
use OCP\AppFramework\Db\MultipleObjectsReturnedException;
|
||||||
use OCP\IConfig;
|
use OCP\IConfig;
|
||||||
use OCP\IGroupManager;
|
use OCP\IGroupManager;
|
||||||
@@ -61,6 +62,7 @@ class PermissionService {
|
|||||||
private $users = [];
|
private $users = [];
|
||||||
|
|
||||||
private $circlesEnabled = false;
|
private $circlesEnabled = false;
|
||||||
|
private $boardCache;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
ILogger $logger,
|
ILogger $logger,
|
||||||
@@ -81,6 +83,8 @@ class PermissionService {
|
|||||||
$this->config = $config;
|
$this->config = $config;
|
||||||
$this->userId = $userId;
|
$this->userId = $userId;
|
||||||
|
|
||||||
|
$this->boardCache = new CappedMemoryCache();
|
||||||
|
|
||||||
$this->circlesEnabled = \OC::$server->getAppManager()->isEnabledForUser('circles') &&
|
$this->circlesEnabled = \OC::$server->getAppManager()->isEnabledForUser('circles') &&
|
||||||
(version_compare(\OC::$server->getAppManager()->getAppVersion('circles'), '0.17.1') >= 0);
|
(version_compare(\OC::$server->getAppManager()->getAppVersion('circles'), '0.17.1') >= 0);
|
||||||
}
|
}
|
||||||
@@ -149,10 +153,13 @@ class PermissionService {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
$acls = $this->aclMapper->findAll($boardId);
|
try {
|
||||||
$result = $this->userCan($acls, $permission, $userId);
|
$acls = $this->getBoard($boardId)->getAcl();
|
||||||
if ($result) {
|
$result = $this->userCan($acls, $permission, $userId);
|
||||||
return true;
|
if ($result) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} catch (DoesNotExistException | MultipleObjectsReturnedException $e) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Throw NoPermission to not leak information about existing entries
|
// Throw NoPermission to not leak information about existing entries
|
||||||
@@ -168,13 +175,24 @@ class PermissionService {
|
|||||||
$userId = $this->userId;
|
$userId = $this->userId;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
$board = $this->boardMapper->find($boardId);
|
$board = $this->getBoard($boardId);
|
||||||
return $board && $userId === $board->getOwner();
|
return $userId === $board->getOwner();
|
||||||
} catch (DoesNotExistException | MultipleObjectsReturnedException $e) {
|
} catch (DoesNotExistException | MultipleObjectsReturnedException $e) {
|
||||||
}
|
}
|
||||||
return false;
|
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
|
* 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) {
|
if ($this->circlesEnabled && $acl->getType() === Acl::PERMISSION_TYPE_CIRCLE) {
|
||||||
try {
|
try {
|
||||||
\OCA\Circles\Api\v1\Circles::getMember($acl->getParticipant(), $this->userId, 1, true);
|
$member = \OCA\Circles\Api\v1\Circles::getMember($acl->getParticipant(), $this->userId, 1, true);
|
||||||
return $acl->getPermission($permission);
|
return $member->getLevel() >= Member::LEVEL_MEMBER && $acl->getPermission($permission);
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
$this->logger->info('Member not found in circle that was accessed. This should not happen.');
|
$this->logger->info('Member not found in circle that was accessed. This should not happen.');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ class SearchService {
|
|||||||
return $board->getId();
|
return $board->getId();
|
||||||
}, $boards);
|
}, $boards);
|
||||||
$matchedCards = $this->cardMapper->search($boardIds, $this->filterStringParser->parse($term), $limit, $cursor);
|
$matchedCards = $this->cardMapper->search($boardIds, $this->filterStringParser->parse($term), $limit, $cursor);
|
||||||
|
|
||||||
$self = $this;
|
$self = $this;
|
||||||
return array_map(function (Card $card) use ($self) {
|
return array_map(function (Card $card) use ($self) {
|
||||||
$self->cardService->enrich($card);
|
$self->cardService->enrich($card);
|
||||||
@@ -91,9 +91,24 @@ class SearchService {
|
|||||||
|
|
||||||
public function searchBoards(string $term, ?int $limit, ?int $cursor): array {
|
public function searchBoards(string $term, ?int $limit, ?int $cursor): array {
|
||||||
$boards = $this->boardService->getUserBoards();
|
$boards = $this->boardService->getUserBoards();
|
||||||
return array_filter($boards, static function (Board $board) use ($term) {
|
// get boards that have a lastmodified date which is lower than the cursor
|
||||||
return mb_stripos(mb_strtolower($board->getTitle()), mb_strtolower($term)) > -1;
|
// 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 {
|
public function searchComments(string $term, ?int $limit = null, ?int $cursor = null): array {
|
||||||
|
|||||||
@@ -48,7 +48,6 @@ class StackService {
|
|||||||
private $assignedUsersMapper;
|
private $assignedUsersMapper;
|
||||||
private $attachmentService;
|
private $attachmentService;
|
||||||
private $activityManager;
|
private $activityManager;
|
||||||
private $symfonyAdapter;
|
|
||||||
private $changeHelper;
|
private $changeHelper;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
@@ -110,6 +109,7 @@ class StackService {
|
|||||||
throw new BadRequestException('stack id must be a number');
|
throw new BadRequestException('stack id must be a number');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$this->permissionService->checkPermission($this->stackMapper, $stackId, Acl::PERMISSION_READ);
|
||||||
$stack = $this->stackMapper->find($stackId);
|
$stack = $this->stackMapper->find($stackId);
|
||||||
$cards = $this->cardMapper->findAll($stackId);
|
$cards = $this->cardMapper->findAll($stackId);
|
||||||
foreach ($cards as $cardIndex => $card) {
|
foreach ($cards as $cardIndex => $card) {
|
||||||
|
|||||||
@@ -271,9 +271,9 @@ class DeckShareProvider implements \OCP\Share\IShareProvider {
|
|||||||
return $share;
|
return $share;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function applyBoardPermission($share, $permissions) {
|
private function applyBoardPermission($share, $permissions, $userId) {
|
||||||
try {
|
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) {
|
} catch (NoPermissionException $e) {
|
||||||
$permissions &= Constants::PERMISSION_ALL - Constants::PERMISSION_UPDATE;
|
$permissions &= Constants::PERMISSION_ALL - Constants::PERMISSION_UPDATE;
|
||||||
$permissions &= Constants::PERMISSION_ALL - Constants::PERMISSION_CREATE;
|
$permissions &= Constants::PERMISSION_ALL - Constants::PERMISSION_CREATE;
|
||||||
@@ -281,7 +281,7 @@ class DeckShareProvider implements \OCP\Share\IShareProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
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) {
|
} catch (NoPermissionException $e) {
|
||||||
$permissions &= Constants::PERMISSION_ALL - Constants::PERMISSION_SHARE;
|
$permissions &= Constants::PERMISSION_ALL - Constants::PERMISSION_SHARE;
|
||||||
}
|
}
|
||||||
@@ -646,7 +646,7 @@ class DeckShareProvider implements \OCP\Share\IShareProvider {
|
|||||||
$stmt = $query->execute();
|
$stmt = $query->execute();
|
||||||
|
|
||||||
while ($data = $stmt->fetch()) {
|
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']);
|
$shareMap[$data['parent']]->setTarget($data['file_target']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,12 @@
|
|||||||
|
|
||||||
namespace OCA\Deck;
|
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 {
|
class StatusException extends \Exception {
|
||||||
public function __construct($message) {
|
public function __construct($message) {
|
||||||
parent::__construct($message);
|
parent::__construct($message);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "deck",
|
"name": "deck",
|
||||||
"description": "",
|
"description": "",
|
||||||
"version": "1.0.0",
|
"version": "1.4.7",
|
||||||
"authors": [
|
"authors": [
|
||||||
{
|
{
|
||||||
"name": "Julius Härtl",
|
"name": "Julius Härtl",
|
||||||
@@ -129,4 +129,4 @@
|
|||||||
"<rootDir>/node_modules/jest-serializer-vue"
|
"<rootDir>/node_modules/jest-serializer-vue"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -131,6 +131,12 @@ export default {
|
|||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.attachments-drag-zone.drop-upload--sidebar {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex-basis: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.dragover {
|
.dragover {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
background: var(--color-primary-light);
|
background: var(--color-primary-light);
|
||||||
|
|||||||
@@ -32,7 +32,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<EmptyContent v-else-if="isEmpty" key="empty" icon="icon-deck">
|
<EmptyContent v-else-if="isEmpty" key="empty" icon="icon-deck">
|
||||||
{{ t('deck', 'No lists available') }}
|
{{ t('deck', 'No lists available') }}
|
||||||
<template #desc>
|
<template v-if="canManage" #desc>
|
||||||
{{ t('deck', 'Create a new list to add cards to this board') }}
|
{{ t('deck', 'Create a new list to add cards to this board') }}
|
||||||
<form @submit.prevent="addNewStack()">
|
<form @submit.prevent="addNewStack()">
|
||||||
<input id="new-stack-input-main"
|
<input id="new-stack-input-main"
|
||||||
@@ -110,6 +110,7 @@ export default {
|
|||||||
}),
|
}),
|
||||||
...mapGetters([
|
...mapGetters([
|
||||||
'canEdit',
|
'canEdit',
|
||||||
|
'canManage',
|
||||||
]),
|
]),
|
||||||
stacksByBoard() {
|
stacksByBoard() {
|
||||||
return this.$store.getters.stacksByBoard(this.board.id)
|
return this.$store.getters.stacksByBoard(this.board.id)
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ import { CollectionList } from 'nextcloud-vue-collections'
|
|||||||
import { mapGetters, mapState } from 'vuex'
|
import { mapGetters, mapState } from 'vuex'
|
||||||
import { getCurrentUser } from '@nextcloud/auth'
|
import { getCurrentUser } from '@nextcloud/auth'
|
||||||
import { showError } from '@nextcloud/dialogs'
|
import { showError } from '@nextcloud/dialogs'
|
||||||
import { debounce } from 'lodash'
|
import debounce from 'lodash/debounce'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'SharingTabSidebar',
|
name: 'SharingTabSidebar',
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<AttachmentDragAndDrop :card-id="cardId" class="drop-upload--sidebar">
|
<AttachmentDragAndDrop :card-id="cardId" class="drop-upload--sidebar">
|
||||||
<div class="button-group">
|
<div class="button-group" v-if="!isReadOnly">
|
||||||
<button class="icon-upload" @click="uploadNewFile()">
|
<button class="icon-upload" @click="uploadNewFile()">
|
||||||
{{ t('deck', 'Upload new files') }}
|
{{ t('deck', 'Upload new files') }}
|
||||||
</button>
|
</button>
|
||||||
@@ -49,18 +49,25 @@
|
|||||||
</li>
|
</li>
|
||||||
<li v-for="attachment in attachments"
|
<li v-for="attachment in attachments"
|
||||||
:key="attachment.id"
|
:key="attachment.id"
|
||||||
class="attachment">
|
class="attachment"
|
||||||
|
:class="{ 'attachment--deleted': attachment.deletedAt > 0 }">
|
||||||
<a class="fileicon"
|
<a class="fileicon"
|
||||||
|
:href="internalLink(attachment)"
|
||||||
:style="mimetypeForAttachment(attachment)"
|
:style="mimetypeForAttachment(attachment)"
|
||||||
@click.prevent="showViewer(attachment)" />
|
@click.prevent="showViewer(attachment)" />
|
||||||
<div class="details">
|
<div class="details">
|
||||||
<a @click.prevent="showViewer(attachment)">
|
<a :href="internalLink(attachment)" @click.prevent="showViewer(attachment)">
|
||||||
<div class="filename">
|
<div class="filename">
|
||||||
<span class="basename">{{ attachment.data }}</span>
|
<span class="basename">{{ attachment.data }}</span>
|
||||||
</div>
|
</div>
|
||||||
<span class="filesize">{{ formattedFileSize(attachment.extendedData.filesize) }}</span>
|
<div v-if="attachment.deletedAt === 0">
|
||||||
<span class="filedate">{{ relativeDate(attachment.createdAt*1000) }}</span>
|
<span class="filesize">{{ formattedFileSize(attachment.extendedData.filesize) }}</span>
|
||||||
<span class="filedate">{{ attachment.createdBy }}</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>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<Actions v-if="selectable">
|
<Actions v-if="selectable">
|
||||||
@@ -68,12 +75,12 @@
|
|||||||
{{ t('deck', 'Add this attachment') }}
|
{{ t('deck', 'Add this attachment') }}
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
</Actions>
|
</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)">
|
<ActionLink v-if="attachment.extendedData.fileid" icon="icon-folder" :href="internalLink(attachment)">
|
||||||
{{ t('deck', 'Show in Files') }}
|
{{ t('deck', 'Show in Files') }}
|
||||||
</ActionLink>
|
</ActionLink>
|
||||||
<ActionButton v-if="attachment.extendedData.fileid" icon="icon-delete" @click="unshareAttachment(attachment)">
|
<ActionButton v-if="attachment.extendedData.fileid && !isReadOnly" icon="icon-delete" @click="unshareAttachment(attachment)">
|
||||||
{{ t('deck', 'Unshare file') }}
|
{{ t('deck', 'Remove attachment') }}
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
|
|
||||||
<ActionButton v-if="!attachment.extendedData.fileid && attachment.deletedAt === 0" icon="icon-delete" @click="$emit('deleteAttachment', attachment)">
|
<ActionButton v-if="!attachment.extendedData.fileid && attachment.deletedAt === 0" icon="icon-delete" @click="$emit('deleteAttachment', attachment)">
|
||||||
@@ -143,6 +150,7 @@ export default {
|
|||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
attachments() {
|
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)
|
return [...this.$store.getters.attachmentsByCard(this.cardId)].filter(attachment => attachment.deletedAt >= 0).sort((a, b) => b.id - a.id)
|
||||||
},
|
},
|
||||||
mimetypeForAttachment() {
|
mimetypeForAttachment() {
|
||||||
@@ -320,9 +328,10 @@ export default {
|
|||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.attachment--info,
|
||||||
.filesize, .filedate {
|
.filesize, .filedate {
|
||||||
font-size: 90%;
|
font-size: 90%;
|
||||||
color: darkgray;
|
color: var(--color-text-maxcontrast);
|
||||||
}
|
}
|
||||||
.app-popover-menu-utils {
|
.app-popover-menu-utils {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|||||||
@@ -185,6 +185,13 @@ export default {
|
|||||||
|
|
||||||
<style lang="scss" scoped>
|
<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
|
// FIXME: Obivously we should at some point not randomly reuse the sidebar component
|
||||||
// since this is not oficially supported
|
// since this is not oficially supported
|
||||||
.modal__card .app-sidebar {
|
.modal__card .app-sidebar {
|
||||||
@@ -202,7 +209,6 @@ export default {
|
|||||||
.app-sidebar-header {
|
.app-sidebar-header {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 0;
|
top: 0;
|
||||||
padding-top: $modal-padding;
|
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
background-color: var(--color-main-background);
|
background-color: var(--color-main-background);
|
||||||
}
|
}
|
||||||
@@ -214,12 +220,6 @@ export default {
|
|||||||
background-color: var(--color-main-background);
|
background-color: var(--color-main-background);
|
||||||
}
|
}
|
||||||
|
|
||||||
section.app-sidebar__tab--active {
|
|
||||||
min-height: auto;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
#emptycontent, .emptycontent {
|
#emptycontent, .emptycontent {
|
||||||
margin-top: 88px;
|
margin-top: 88px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,11 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</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" />
|
<CommentForm v-model="newComment" @submit="createComment" />
|
||||||
|
|
||||||
<ul v-if="getCommentsForCard(card.id).length > 0" id="commentsFeed">
|
<ul v-if="getCommentsForCard(card.id).length > 0" id="commentsFeed">
|
||||||
@@ -23,8 +27,8 @@
|
|||||||
</ul>
|
</ul>
|
||||||
<div v-else-if="isLoading" class="icon icon-loading" />
|
<div v-else-if="isLoading" class="icon icon-loading" />
|
||||||
<div v-else class="emptycontent">
|
<div v-else class="emptycontent">
|
||||||
<div class="icon-comment" />
|
<div :class="{ 'icon-comment': !error, 'icon-error': error }" />
|
||||||
<p>{{ t('deck', 'No comments yet. Begin the discussion!') }}</p>
|
<p>{{ error || t('deck', 'No comments yet. Begin the discussion!') }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -36,6 +40,7 @@ import CommentItem from './CommentItem'
|
|||||||
import CommentForm from './CommentForm'
|
import CommentForm from './CommentForm'
|
||||||
import InfiniteLoading from 'vue-infinite-loading'
|
import InfiniteLoading from 'vue-infinite-loading'
|
||||||
import { getCurrentUser } from '@nextcloud/auth'
|
import { getCurrentUser } from '@nextcloud/auth'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'CardSidebarTabComments',
|
name: 'CardSidebarTabComments',
|
||||||
components: {
|
components: {
|
||||||
@@ -60,6 +65,7 @@ export default {
|
|||||||
newComment: '',
|
newComment: '',
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
currentUser: getCurrentUser(),
|
currentUser: getCurrentUser(),
|
||||||
|
error: null,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -85,19 +91,34 @@ export default {
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
async infiniteHandler($state) {
|
async infiniteHandler($state) {
|
||||||
await this.loadMore()
|
this.error = null
|
||||||
if (this.hasMoreComments(this.card.id)) {
|
try {
|
||||||
$state.loaded()
|
await this.loadMore()
|
||||||
} else {
|
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()
|
$state.complete()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async loadComments() {
|
async loadComments() {
|
||||||
|
this.$store.dispatch('setReplyTo', null)
|
||||||
|
this.error = null
|
||||||
this.isLoading = true
|
this.isLoading = true
|
||||||
await this.$store.dispatch('fetchComments', { cardId: this.card.id })
|
try {
|
||||||
this.isLoading = false
|
await this.$store.dispatch('fetchComments', { cardId: this.card.id })
|
||||||
if (this.card.commentsUnread > 0) {
|
this.isLoading = false
|
||||||
await this.$store.dispatch('markCommentsAsRead', this.card.id)
|
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) {
|
async createComment(content) {
|
||||||
@@ -115,6 +136,9 @@ export default {
|
|||||||
await this.$store.dispatch('fetchMore', { cardId: this.card.id })
|
await this.$store.dispatch('fetchMore', { cardId: this.card.id })
|
||||||
this.isLoading = false
|
this.isLoading = false
|
||||||
},
|
},
|
||||||
|
cancelReply() {
|
||||||
|
this.$store.dispatch('setReplyTo', null)
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
<At ref="at"
|
<At ref="at"
|
||||||
v-model="commentText"
|
v-model="commentText"
|
||||||
:members="members"
|
:members="members"
|
||||||
name-key="uid"
|
name-key="displayname"
|
||||||
:tab-select="true">
|
:tab-select="true">
|
||||||
<template v-slot:item="s">
|
<template v-slot:item="s">
|
||||||
<Avatar class="atwho-li--avatar" :user="s.item.uid" :size="24" />
|
<Avatar class="atwho-li--avatar" :user="s.item.uid" :size="24" />
|
||||||
@@ -41,6 +41,7 @@
|
|||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
<div ref="contentEditable"
|
<div ref="contentEditable"
|
||||||
|
class="comment-form__contenteditable"
|
||||||
contenteditable
|
contenteditable
|
||||||
@keydown.enter="handleKeydown"
|
@keydown.enter="handleKeydown"
|
||||||
@paste="onPaste"
|
@paste="onPaste"
|
||||||
@@ -175,6 +176,11 @@ export default {
|
|||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
@import '../../css/comments';
|
@import '../../css/comments';
|
||||||
|
|
||||||
|
.comment-form__contenteditable {
|
||||||
|
word-break: break-word;
|
||||||
|
border-radius: var(--border-radius-large)
|
||||||
|
}
|
||||||
|
|
||||||
.atwho-wrap {
|
.atwho-wrap {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
& > div[contenteditable] {
|
& > div[contenteditable] {
|
||||||
|
|||||||
@@ -1,10 +1,22 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="reply" class="reply">
|
<div v-if="reply" class="reply" :class="{ 'reply--preview': preview }">
|
||||||
<span class="reply--hint">{{ t('deck', 'In reply to') }} <UserBubble :user="comment.actorId" :display-name="comment.actorDisplayName" /></span>
|
<div class="reply--wrapper">
|
||||||
<RichText class="comment--content"
|
<div class="reply--header">
|
||||||
:text="richText(comment)"
|
<div class="reply--hint">
|
||||||
:arguments="richArgs(comment)"
|
{{ t('deck', 'In reply to') }}
|
||||||
:autolink="true" />
|
<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>
|
</div>
|
||||||
<li v-else class="comment">
|
<li v-else class="comment">
|
||||||
<template>
|
<template>
|
||||||
@@ -14,13 +26,19 @@
|
|||||||
{{ comment.actorDisplayName }}
|
{{ comment.actorDisplayName }}
|
||||||
</span>
|
</span>
|
||||||
<Actions v-show="!edit" :force-menu="true">
|
<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') }}
|
{{ t('deck', 'Reply') }}
|
||||||
</ActionButton>
|
</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') }}
|
{{ t('deck', 'Update') }}
|
||||||
</ActionButton>
|
</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') }}
|
{{ t('deck', 'Delete') }}
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
</Actions>
|
</Actions>
|
||||||
@@ -86,6 +104,10 @@ export default {
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
preview: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@@ -175,20 +197,41 @@ export default {
|
|||||||
@import '../../css/comments';
|
@import '../../css/comments';
|
||||||
|
|
||||||
.reply {
|
.reply {
|
||||||
border-left: 3px solid var(--color-primary-element);
|
margin: 0 0 0 44px;
|
||||||
padding-left: 5px;
|
|
||||||
margin-left: 2px;
|
&.reply--preview {
|
||||||
margin-bottom: 5px;
|
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 {
|
&::v-deep .rich-text--wrapper {
|
||||||
margin-top: -3px;
|
margin-top: -3px;
|
||||||
color: var(--color-text-light);
|
color: var(--color-text-lighter);
|
||||||
|
}
|
||||||
|
|
||||||
|
.reply--header {
|
||||||
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
.reply--hint {
|
.reply--hint {
|
||||||
font-size: 0.9em;
|
|
||||||
color: var(--color-text-lighter);
|
color: var(--color-text-lighter);
|
||||||
vertical-align: top;
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.comment--content {
|
.comment--content {
|
||||||
|
|||||||
@@ -306,6 +306,7 @@ h5 {
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
background-color: var(--color-main-background);
|
background-color: var(--color-main-background);
|
||||||
color: var(--color-main-text);
|
color: var(--color-main-text);
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.CodeMirror-placeholder {
|
.CodeMirror-placeholder {
|
||||||
|
|||||||
@@ -22,7 +22,13 @@
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div v-if="card" class="badges">
|
<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">
|
<div v-if="card.description && checkListCount > 0" class="card-tasks icon icon-checkmark">
|
||||||
{{ checkListCheckedCount }}/{{ checkListCount }}
|
{{ checkListCheckedCount }}/{{ checkListCount }}
|
||||||
@@ -58,6 +64,21 @@ export default {
|
|||||||
checkListCheckedCount() {
|
checkListCheckedCount() {
|
||||||
return (this.card.description.match(/^\s*([*+-]|(\d\.))\s+\[\s*x\s*\](.*)$/gim) || []).length
|
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>
|
</script>
|
||||||
@@ -70,7 +91,7 @@ export default {
|
|||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
padding: 12px 18px;
|
padding: 10px 20px;
|
||||||
padding-right: 4px;
|
padding-right: 4px;
|
||||||
margin-right: 5px;
|
margin-right: 5px;
|
||||||
background-position: left;
|
background-position: left;
|
||||||
@@ -78,8 +99,8 @@ export default {
|
|||||||
span {
|
span {
|
||||||
margin-left: 18px;
|
margin-left: 18px;
|
||||||
}
|
}
|
||||||
&.icon-edit {
|
&.icon-comment--unread {
|
||||||
opacity: 0.5;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,10 @@
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div v-if="searchQuery!==''" class="global-search">
|
<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>
|
<Actions>
|
||||||
<ActionButton icon="icon-close" @click="$store.commit('setSearchQuery', '')" />
|
<ActionButton icon="icon-close" @click="$store.commit('setSearchQuery', '')" />
|
||||||
</Actions>
|
</Actions>
|
||||||
@@ -107,23 +110,38 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
searchQuery() {
|
async searchQuery() {
|
||||||
this.cursor = null
|
this.cursor = null
|
||||||
this.loading = true
|
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: {
|
methods: {
|
||||||
infiniteHandler($state) {
|
async infiniteHandler($state) {
|
||||||
this.loading = true
|
this.loading = true
|
||||||
this.search().then((data) => {
|
try {
|
||||||
|
const data = await this.search()
|
||||||
if (data.length) {
|
if (data.length) {
|
||||||
$state.loaded()
|
$state.loaded()
|
||||||
} else {
|
} else {
|
||||||
$state.complete()
|
$state.complete()
|
||||||
}
|
}
|
||||||
this.loading = false
|
this.loading = false
|
||||||
})
|
} catch (e) {
|
||||||
|
if (!axios.isCancel(e)) {
|
||||||
|
console.error('Search request failed', e)
|
||||||
|
$state.complete()
|
||||||
|
this.loading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
async search() {
|
async search() {
|
||||||
if (this.cancel) {
|
if (this.cancel) {
|
||||||
@@ -177,6 +195,13 @@ export default {
|
|||||||
padding: 10px;
|
padding: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
h2 > div {
|
||||||
|
display: inline-block;
|
||||||
|
|
||||||
|
&.icon-loading-small {
|
||||||
|
margin-right: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
h2::v-deep span {
|
h2::v-deep span {
|
||||||
background-color: var(--color-background-dark);
|
background-color: var(--color-background-dark);
|
||||||
padding: 3px;
|
padding: 3px;
|
||||||
|
|||||||
@@ -48,4 +48,5 @@
|
|||||||
|
|
||||||
.comment--content {
|
.comment--content {
|
||||||
margin-left: 44px;
|
margin-left: 44px;
|
||||||
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -92,24 +92,39 @@ export default {
|
|||||||
|
|
||||||
const filterOutQuotes = (q) => {
|
const filterOutQuotes = (q) => {
|
||||||
if (q[0] === '"' && q[q.length - 1] === '"') {
|
if (q[0] === '"' && q[q.length - 1] === '"') {
|
||||||
return q.substr(1, -1)
|
return q.substr(1, q.length - 2)
|
||||||
}
|
}
|
||||||
return q
|
return q
|
||||||
}
|
}
|
||||||
for (const match of matches) {
|
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 (filter === 'title') {
|
||||||
|
if (isEmptyQuery) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
hasMatch = hasMatch && card.title.toLowerCase().includes(filterOutQuotes(query).toLowerCase())
|
hasMatch = hasMatch && card.title.toLowerCase().includes(filterOutQuotes(query).toLowerCase())
|
||||||
} else if (filter === 'description') {
|
} else if (filter === 'description') {
|
||||||
|
if (isEmptyQuery) {
|
||||||
|
hasMatch = hasMatch && !!card.description
|
||||||
|
continue
|
||||||
|
}
|
||||||
hasMatch = hasMatch && card.description.toLowerCase().includes(filterOutQuotes(query).toLowerCase())
|
hasMatch = hasMatch && card.description.toLowerCase().includes(filterOutQuotes(query).toLowerCase())
|
||||||
} else if (filter === 'list') {
|
} else if (filter === 'list') {
|
||||||
const stack = this.getters.stackById(card.stackId)
|
if (isEmptyQuery) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const stack = getters.stackById(card.stackId)
|
||||||
if (!stack) {
|
if (!stack) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
hasMatch = hasMatch && stack.title.toLowerCase().includes(filterOutQuotes(query).toLowerCase())
|
hasMatch = hasMatch && stack.title.toLowerCase().includes(filterOutQuotes(query).toLowerCase())
|
||||||
} else if (filter === 'tag') {
|
} 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
|
hasMatch = hasMatch && card.labels.findIndex((label) => label.title.toLowerCase().includes(filterOutQuotes(query).toLowerCase())) !== -1
|
||||||
} else if (filter === 'date') {
|
} else if (filter === 'date') {
|
||||||
const datediffHour = ((new Date(card.duedate) - new Date()) / 3600 / 1000)
|
const datediffHour = ((new Date(card.duedate) - new Date()) / 3600 / 1000)
|
||||||
@@ -158,6 +173,10 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
} else if (filter === 'assigned') {
|
} else if (filter === 'assigned') {
|
||||||
|
if (isEmptyQuery) {
|
||||||
|
hasMatch = hasMatch && card.assignedUsers.length > 0
|
||||||
|
continue
|
||||||
|
}
|
||||||
hasMatch = hasMatch && card.assignedUsers.findIndex((assignment) => {
|
hasMatch = hasMatch && card.assignedUsers.findIndex((assignment) => {
|
||||||
return assignment.participant.primaryKey.toLowerCase() === filterOutQuotes(query).toLowerCase()
|
return assignment.participant.primaryKey.toLowerCase() === filterOutQuotes(query).toLowerCase()
|
||||||
|| assignment.participant.displayname.toLowerCase() === filterOutQuotes(query).toLowerCase()
|
|| assignment.participant.displayname.toLowerCase() === filterOutQuotes(query).toLowerCase()
|
||||||
|
|||||||
@@ -420,6 +420,7 @@ export default new Vuex.Store({
|
|||||||
params.append('format', 'json')
|
params.append('format', 'json')
|
||||||
params.append('perPage', 20)
|
params.append('perPage', 20)
|
||||||
params.append('itemType', [0, 1, 4, 7])
|
params.append('itemType', [0, 1, 4, 7])
|
||||||
|
params.append('lookup', false)
|
||||||
|
|
||||||
const response = await axios.get(generateOcsUrl('apps/files_sharing/api/v1') + 'sharees', { params })
|
const response = await axios.get(generateOcsUrl('apps/files_sharing/api/v1') + 'sharees', { params })
|
||||||
commit('setSharees', response.data.ocs.data)
|
commit('setSharees', response.data.ocs.data)
|
||||||
|
|||||||
@@ -8,4 +8,5 @@ default:
|
|||||||
baseUrl: http://localhost:8080/
|
baseUrl: http://localhost:8080/
|
||||||
- RequestContext
|
- RequestContext
|
||||||
- BoardContext
|
- BoardContext
|
||||||
|
- CommentContext
|
||||||
- SearchContext
|
- SearchContext
|
||||||
|
|||||||
@@ -27,6 +27,10 @@ class BoardContext implements Context {
|
|||||||
$this->serverContext = $environment->getContext('ServerContext');
|
$this->serverContext = $environment->getContext('ServerContext');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getLastUsedCard() {
|
||||||
|
return $this->card;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @Given /^creates a board named "([^"]*)" with color "([^"]*)"$/
|
* @Given /^creates a board named "([^"]*)" with color "([^"]*)"$/
|
||||||
*/
|
*/
|
||||||
|
|||||||
31
tests/integration/features/bootstrap/CommentContext.php
Normal file
31
tests/integration/features/bootstrap/CommentContext.php
Normal 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
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,6 +13,7 @@ class SearchContext implements Context {
|
|||||||
protected $boardContext;
|
protected $boardContext;
|
||||||
|
|
||||||
private $searchResults;
|
private $searchResults;
|
||||||
|
private $unifiedSearchResult;
|
||||||
|
|
||||||
/** @BeforeScenario */
|
/** @BeforeScenario */
|
||||||
public function gatherContexts(BeforeScenarioScope $scope) {
|
public function gatherContexts(BeforeScenarioScope $scope) {
|
||||||
@@ -32,6 +33,18 @@ class SearchContext implements Context {
|
|||||||
$this->searchResults = json_decode($data, true);
|
$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 '([^']*)'$/
|
* @When /^searching for '([^']*)'$/
|
||||||
* @param string $term
|
* @param string $term
|
||||||
@@ -78,4 +91,33 @@ class SearchContext implements Context {
|
|||||||
public function theCardIsNotFound($arg1) {
|
public function theCardIsNotFound($arg1) {
|
||||||
Assert::assertFalse($this->cardIsFound($arg1), 'Card can not be found');
|
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');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -257,3 +257,10 @@ Feature: Searching for cards
|
|||||||
Then the card "Example task 1" is not found
|
Then the card "Example task 1" is not found
|
||||||
And the card "Labeled card" is not found
|
And the card "Labeled card" is not found
|
||||||
And the card "Multi labeled card" is 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
|
||||||
|
|||||||
@@ -105,7 +105,8 @@
|
|||||||
<ParamNameMismatch occurrences="1">
|
<ParamNameMismatch occurrences="1">
|
||||||
<code>$boardId</code>
|
<code>$boardId</code>
|
||||||
</ParamNameMismatch>
|
</ParamNameMismatch>
|
||||||
<UndefinedClass occurrences="1">
|
<UndefinedClass occurrences="2">
|
||||||
|
<code>\OCA\Circles\Api\v1\Circles</code>
|
||||||
<code>\OCA\Circles\Api\v1\Circles</code>
|
<code>\OCA\Circles\Api\v1\Circles</code>
|
||||||
</UndefinedClass>
|
</UndefinedClass>
|
||||||
</file>
|
</file>
|
||||||
@@ -170,11 +171,6 @@
|
|||||||
<code>$stackId</code>
|
<code>$stackId</code>
|
||||||
</ParamNameMismatch>
|
</ParamNameMismatch>
|
||||||
</file>
|
</file>
|
||||||
<file src="lib/Notification/NotificationHelper.php">
|
|
||||||
<InvalidScalarArgument occurrences="1">
|
|
||||||
<code>$board->getId()</code>
|
|
||||||
</InvalidScalarArgument>
|
|
||||||
</file>
|
|
||||||
<file src="lib/Notification/Notifier.php">
|
<file src="lib/Notification/Notifier.php">
|
||||||
<RedundantCast occurrences="7">
|
<RedundantCast occurrences="7">
|
||||||
<code>(string) $l->t('%s has mentioned you in a comment on "%s".', [$dn, $params[0]])</code>
|
<code>(string) $l->t('%s has mentioned you in a comment on "%s".', [$dn, $params[0]])</code>
|
||||||
@@ -196,12 +192,9 @@
|
|||||||
<code>$cardId</code>
|
<code>$cardId</code>
|
||||||
<code>$cardId</code>
|
<code>$cardId</code>
|
||||||
</InvalidScalarArgument>
|
</InvalidScalarArgument>
|
||||||
<UndefinedThisPropertyAssignment occurrences="1">
|
</file>
|
||||||
<code>$this->currentUser</code>
|
<file src="lib/Service/AttachmentService.php">
|
||||||
</UndefinedThisPropertyAssignment>
|
<InvalidCatch occurrences="1"/>
|
||||||
<UndefinedThisPropertyFetch occurrences="1">
|
|
||||||
<code>$this->currentUser</code>
|
|
||||||
</UndefinedThisPropertyFetch>
|
|
||||||
</file>
|
</file>
|
||||||
<file src="lib/Service/BoardService.php">
|
<file src="lib/Service/BoardService.php">
|
||||||
<TooManyArguments occurrences="2">
|
<TooManyArguments occurrences="2">
|
||||||
@@ -265,7 +258,6 @@
|
|||||||
</RedundantCondition>
|
</RedundantCondition>
|
||||||
</file>
|
</file>
|
||||||
<file src="lib/Service/FilesAppService.php">
|
<file src="lib/Service/FilesAppService.php">
|
||||||
<InvalidCatch occurrences="1"/>
|
|
||||||
<MissingDependency occurrences="4">
|
<MissingDependency occurrences="4">
|
||||||
<code>$this->rootFolder</code>
|
<code>$this->rootFolder</code>
|
||||||
<code>$this->rootFolder</code>
|
<code>$this->rootFolder</code>
|
||||||
@@ -274,18 +266,12 @@
|
|||||||
</MissingDependency>
|
</MissingDependency>
|
||||||
</file>
|
</file>
|
||||||
<file src="lib/Service/PermissionService.php">
|
<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>
|
||||||
<code>\OCA\Circles\Api\v1\Circles</code>
|
<code>\OCA\Circles\Api\v1\Circles</code>
|
||||||
</UndefinedClass>
|
</UndefinedClass>
|
||||||
</file>
|
</file>
|
||||||
<file src="lib/Service/SearchService.php">
|
|
||||||
<UndefinedThisPropertyFetch occurrences="3">
|
|
||||||
<code>$this->l10n</code>
|
|
||||||
<code>$this->urlGenerator</code>
|
|
||||||
<code>$this->userManager</code>
|
|
||||||
</UndefinedThisPropertyFetch>
|
|
||||||
</file>
|
|
||||||
<file src="lib/Service/StackService.php">
|
<file src="lib/Service/StackService.php">
|
||||||
<UndefinedClass occurrences="1">
|
<UndefinedClass occurrences="1">
|
||||||
<code>BadRquestException</code>
|
<code>BadRquestException</code>
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ namespace OCA\Deck\Db;
|
|||||||
|
|
||||||
use OCP\IGroupManager;
|
use OCP\IGroupManager;
|
||||||
use OCP\IUserManager;
|
use OCP\IUserManager;
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
use Test\AppFramework\Db\MapperTestUtility;
|
use Test\AppFramework\Db\MapperTestUtility;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -54,7 +55,8 @@ class AclMapperTest extends MapperTestUtility {
|
|||||||
$this->aclMapper,
|
$this->aclMapper,
|
||||||
\OC::$server->query(StackMapper::class),
|
\OC::$server->query(StackMapper::class),
|
||||||
$this->userManager,
|
$this->userManager,
|
||||||
$this->groupManager
|
$this->groupManager,
|
||||||
|
$this->createMock(LoggerInterface::class)
|
||||||
);
|
);
|
||||||
|
|
||||||
$this->boards = [
|
$this->boards = [
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ namespace OCA\Deck\Db;
|
|||||||
use OCP\IDBConnection;
|
use OCP\IDBConnection;
|
||||||
use OCP\IGroupManager;
|
use OCP\IGroupManager;
|
||||||
use OCP\IUserManager;
|
use OCP\IUserManager;
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
use Test\AppFramework\Db\MapperTestUtility;
|
use Test\AppFramework\Db\MapperTestUtility;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -61,7 +62,8 @@ class BoardMapperTest extends MapperTestUtility {
|
|||||||
\OC::$server->query(AclMapper::class),
|
\OC::$server->query(AclMapper::class),
|
||||||
\OC::$server->query(StackMapper::class),
|
\OC::$server->query(StackMapper::class),
|
||||||
$this->userManager,
|
$this->userManager,
|
||||||
$this->groupManager
|
$this->groupManager,
|
||||||
|
$this->createMock(LoggerInterface::class)
|
||||||
);
|
);
|
||||||
$this->aclMapper = \OC::$server->query(AclMapper::class);
|
$this->aclMapper = \OC::$server->query(AclMapper::class);
|
||||||
$this->labelMapper = \OC::$server->query(LabelMapper::class);
|
$this->labelMapper = \OC::$server->query(LabelMapper::class);
|
||||||
|
|||||||
@@ -83,6 +83,7 @@ class CardTest extends TestCase {
|
|||||||
'assignedUsers' => null,
|
'assignedUsers' => null,
|
||||||
'deletedAt' => 0,
|
'deletedAt' => 0,
|
||||||
'commentsUnread' => 0,
|
'commentsUnread' => 0,
|
||||||
|
'commentsCount' => 0,
|
||||||
'lastEditor' => null,
|
'lastEditor' => null,
|
||||||
'ETag' => $card->getETag(),
|
'ETag' => $card->getETag(),
|
||||||
], $card->jsonSerialize());
|
], $card->jsonSerialize());
|
||||||
@@ -109,6 +110,7 @@ class CardTest extends TestCase {
|
|||||||
'assignedUsers' => null,
|
'assignedUsers' => null,
|
||||||
'deletedAt' => 0,
|
'deletedAt' => 0,
|
||||||
'commentsUnread' => 0,
|
'commentsUnread' => 0,
|
||||||
|
'commentsCount' => 0,
|
||||||
'lastEditor' => null,
|
'lastEditor' => null,
|
||||||
'ETag' => $card->getETag(),
|
'ETag' => $card->getETag(),
|
||||||
], $card->jsonSerialize());
|
], $card->jsonSerialize());
|
||||||
@@ -145,6 +147,7 @@ class CardTest extends TestCase {
|
|||||||
'assignedUsers' => ['user1'],
|
'assignedUsers' => ['user1'],
|
||||||
'deletedAt' => 0,
|
'deletedAt' => 0,
|
||||||
'commentsUnread' => 0,
|
'commentsUnread' => 0,
|
||||||
|
'commentsCount' => 0,
|
||||||
'lastEditor' => null,
|
'lastEditor' => null,
|
||||||
'ETag' => $card->getETag(),
|
'ETag' => $card->getETag(),
|
||||||
], $card->jsonSerialize());
|
], $card->jsonSerialize());
|
||||||
|
|||||||
@@ -47,10 +47,12 @@ class ExceptionMiddlewareTest extends \Test\TestCase {
|
|||||||
public function setUp(): void {
|
public function setUp(): void {
|
||||||
$this->logger = $this->createMock(ILogger::class);
|
$this->logger = $this->createMock(ILogger::class);
|
||||||
$this->config = $this->createMock(IConfig::class);
|
$this->config = $this->createMock(IConfig::class);
|
||||||
|
$this->request = $this->createMock(IRequest::class);
|
||||||
$this->controller = $this->createMock(Controller::class);
|
$this->controller = $this->createMock(Controller::class);
|
||||||
$this->exceptionMiddleware = new ExceptionMiddleware(
|
$this->exceptionMiddleware = new ExceptionMiddleware(
|
||||||
$this->logger,
|
$this->logger,
|
||||||
$this->config
|
$this->config,
|
||||||
|
$this->request
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,10 +83,11 @@ class ExceptionMiddlewareTest extends \Test\TestCase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public function testAfterExceptionFail() {
|
public function testAfterExceptionFail() {
|
||||||
|
$this->request->expects($this->any())->method('getId')->willReturn('abc123');
|
||||||
// BoardService $boardService, PermissionService $permissionService, $userId
|
// BoardService $boardService, PermissionService $permissionService, $userId
|
||||||
$boardController = new BoardController('deck', $this->createMock(IRequest::class), $this->createMock(BoardService::class), $this->createMock(PermissionService::class), 'admin');
|
$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'));
|
$result = $this->exceptionMiddleware->afterException($boardController, 'bar', new \Exception('other exception message'));
|
||||||
$this->assertEquals('failed hard', $result->getData()['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']);
|
$this->assertEquals(500, $result->getData()['status']);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -130,7 +130,8 @@ class NotificationHelperTest extends \Test\TestCase {
|
|||||||
$card = Card::fromParams([
|
$card = Card::fromParams([
|
||||||
'notified' => false,
|
'notified' => false,
|
||||||
'id' => 123,
|
'id' => 123,
|
||||||
'title' => 'MyCardTitle'
|
'title' => 'MyCardTitle',
|
||||||
|
'duedate' => '2020-12-24'
|
||||||
]);
|
]);
|
||||||
$this->cardMapper->expects($this->once())
|
$this->cardMapper->expects($this->once())
|
||||||
->method('findBoardId')
|
->method('findBoardId')
|
||||||
@@ -225,7 +226,8 @@ class NotificationHelperTest extends \Test\TestCase {
|
|||||||
$card = Card::fromParams([
|
$card = Card::fromParams([
|
||||||
'notified' => false,
|
'notified' => false,
|
||||||
'id' => 123,
|
'id' => 123,
|
||||||
'title' => 'MyCardTitle'
|
'title' => 'MyCardTitle',
|
||||||
|
'duedate' => '2020-12-24'
|
||||||
]);
|
]);
|
||||||
$card->setAssignedUsers([
|
$card->setAssignedUsers([
|
||||||
new User($users[0])
|
new User($users[0])
|
||||||
@@ -323,7 +325,8 @@ class NotificationHelperTest extends \Test\TestCase {
|
|||||||
$card = Card::fromParams([
|
$card = Card::fromParams([
|
||||||
'notified' => false,
|
'notified' => false,
|
||||||
'id' => 123,
|
'id' => 123,
|
||||||
'title' => 'MyCardTitle'
|
'title' => 'MyCardTitle',
|
||||||
|
'duedate' => '2020-12-24'
|
||||||
]);
|
]);
|
||||||
$card->setAssignedUsers([
|
$card->setAssignedUsers([
|
||||||
new User($users[0])
|
new User($users[0])
|
||||||
@@ -470,7 +473,7 @@ class NotificationHelperTest extends \Test\TestCase {
|
|||||||
->with(123)
|
->with(123)
|
||||||
->willReturn($board);
|
->willReturn($board);
|
||||||
$user = $this->createMock(IUser::class);
|
$user = $this->createMock(IUser::class);
|
||||||
$user->expects($this->once())
|
$user->expects($this->any())
|
||||||
->method('getUID')
|
->method('getUID')
|
||||||
->willReturn('userA');
|
->willReturn('userA');
|
||||||
$group = $this->createMock(IGroup::class);
|
$group = $this->createMock(IGroup::class);
|
||||||
|
|||||||
@@ -25,29 +25,33 @@ namespace OCA\Deck\Notification;
|
|||||||
|
|
||||||
use OCA\Deck\Db\BoardMapper;
|
use OCA\Deck\Db\BoardMapper;
|
||||||
use OCA\Deck\Db\CardMapper;
|
use OCA\Deck\Db\CardMapper;
|
||||||
|
use OCA\Deck\Db\StackMapper;
|
||||||
use OCP\IL10N;
|
use OCP\IL10N;
|
||||||
use OCP\IURLGenerator;
|
use OCP\IURLGenerator;
|
||||||
use OCP\IUser;
|
use OCP\IUser;
|
||||||
use OCP\IUserManager;
|
use OCP\IUserManager;
|
||||||
use OCP\L10N\IFactory;
|
use OCP\L10N\IFactory;
|
||||||
use OCP\Notification\INotification;
|
use OCP\Notification\INotification;
|
||||||
|
use PHPUnit\Framework\MockObject\MockObject;
|
||||||
|
|
||||||
class NotifierTest extends \Test\TestCase {
|
class NotifierTest extends \Test\TestCase {
|
||||||
|
|
||||||
/** @var IFactory */
|
/** @var IFactory|MockObject */
|
||||||
protected $l10nFactory;
|
protected $l10nFactory;
|
||||||
/** @var IURLGenerator */
|
/** @var IURLGenerator|MockObject */
|
||||||
protected $url;
|
protected $url;
|
||||||
/** @var IUserManager */
|
/** @var IUserManager|MockObject */
|
||||||
protected $userManager;
|
protected $userManager;
|
||||||
/** @var CardMapper */
|
/** @var CardMapper|MockObject */
|
||||||
protected $cardMapper;
|
protected $cardMapper;
|
||||||
|
/** @var StackMapper|MockObject */
|
||||||
|
protected $stackMapper;
|
||||||
/** @var BoardMapper */
|
/** @var BoardMapper */
|
||||||
protected $boardMapper;
|
protected $boardMapper;
|
||||||
|
/** @var IL10N|MockObject */
|
||||||
|
protected $l10n;
|
||||||
/** @var Notifier */
|
/** @var Notifier */
|
||||||
protected $notifier;
|
protected $notifier;
|
||||||
/** @var IL10N */
|
|
||||||
protected $l10n;
|
|
||||||
|
|
||||||
public function setUp(): void {
|
public function setUp(): void {
|
||||||
parent::setUp();
|
parent::setUp();
|
||||||
@@ -55,12 +59,14 @@ class NotifierTest extends \Test\TestCase {
|
|||||||
$this->url = $this->createMock(IURLGenerator::class);
|
$this->url = $this->createMock(IURLGenerator::class);
|
||||||
$this->userManager = $this->createMock(IUserManager::class);
|
$this->userManager = $this->createMock(IUserManager::class);
|
||||||
$this->cardMapper = $this->createMock(CardMapper::class);
|
$this->cardMapper = $this->createMock(CardMapper::class);
|
||||||
|
$this->stackMapper = $this->createMock(StackMapper::class);
|
||||||
$this->boardMapper = $this->createMock(BoardMapper::class);
|
$this->boardMapper = $this->createMock(BoardMapper::class);
|
||||||
$this->notifier = new Notifier(
|
$this->notifier = new Notifier(
|
||||||
$this->l10nFactory,
|
$this->l10nFactory,
|
||||||
$this->url,
|
$this->url,
|
||||||
$this->userManager,
|
$this->userManager,
|
||||||
$this->cardMapper,
|
$this->cardMapper,
|
||||||
|
$this->stackMapper,
|
||||||
$this->boardMapper
|
$this->boardMapper
|
||||||
);
|
);
|
||||||
$this->l10n = \OC::$server->getL10N('deck');
|
$this->l10n = \OC::$server->getL10N('deck');
|
||||||
@@ -149,7 +155,7 @@ class NotifierTest extends \Test\TestCase {
|
|||||||
->with($expectedMessage);
|
->with($expectedMessage);
|
||||||
$notification->expects($this->once())
|
$notification->expects($this->once())
|
||||||
->method('setRichSubject')
|
->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())
|
$this->url->expects($this->once())
|
||||||
@@ -218,11 +224,25 @@ class NotifierTest extends \Test\TestCase {
|
|||||||
->with($expectedMessage);
|
->with($expectedMessage);
|
||||||
$notification->expects($this->once())
|
$notification->expects($this->once())
|
||||||
->method('setRichSubject')
|
->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' => [
|
'user' => [
|
||||||
'type' => 'user',
|
'type' => 'user',
|
||||||
'id' => 'otheruser',
|
'id' => 'otheruser',
|
||||||
'name' => $dn,
|
'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);
|
->with($expectedMessage);
|
||||||
$notification->expects($this->once())
|
$notification->expects($this->once())
|
||||||
->method('setRichSubject')
|
->method('setRichSubject')
|
||||||
->with('{user} has shared the board Board title with you.', [
|
->with('{user} has shared {deck-board} with you.', [
|
||||||
'user' => [
|
'user' => [
|
||||||
'type' => 'user',
|
'type' => 'user',
|
||||||
'id' => 'otheruser',
|
'id' => 'otheruser',
|
||||||
'name' => $dn,
|
'name' => $dn,
|
||||||
|
],
|
||||||
|
'deck-board' => [
|
||||||
|
'type' => 'deck-board',
|
||||||
|
'id' => 123,
|
||||||
|
'name' => 'Board title',
|
||||||
|
'link' => '#/board/123',
|
||||||
]
|
]
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
47
tests/unit/Search/Query/AQueryParameterTest.php
Normal file
47
tests/unit/Search/Query/AQueryParameterTest.php
Normal 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -128,7 +128,7 @@ class CardServiceTest extends TestCase {
|
|||||||
$this->userManager->expects($this->once())
|
$this->userManager->expects($this->once())
|
||||||
->method('get')
|
->method('get')
|
||||||
->willReturn($user);
|
->willReturn($user);
|
||||||
$this->commentsManager->expects($this->once())
|
$this->commentsManager->expects($this->any())
|
||||||
->method('getNumberOfCommentsForObject')
|
->method('getNumberOfCommentsForObject')
|
||||||
->willReturn(0);
|
->willReturn(0);
|
||||||
$boardMock = $this->createMock(Board::class);
|
$boardMock = $this->createMock(Board::class);
|
||||||
|
|||||||
@@ -146,7 +146,7 @@ class PermissionServiceTest extends \Test\TestCase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public function testUserIsBoardOwnerNull() {
|
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));
|
$this->assertEquals(false, $this->service->userIsBoardOwner(123));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -225,12 +225,9 @@ class PermissionServiceTest extends \Test\TestCase {
|
|||||||
$board = new Board();
|
$board = new Board();
|
||||||
$board->setId($boardId);
|
$board->setId($boardId);
|
||||||
$board->setOwner($owner);
|
$board->setOwner($owner);
|
||||||
|
$board->setAcl($this->getAcls($boardId));
|
||||||
$this->boardMapper->expects($this->any())->method('find')->willReturn($board);
|
$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())
|
$this->shareManager->expects($this->any())
|
||||||
->method('sharingDisabledForUser')
|
->method('sharingDisabledForUser')
|
||||||
->willReturn(false);
|
->willReturn(false);
|
||||||
@@ -250,14 +247,12 @@ class PermissionServiceTest extends \Test\TestCase {
|
|||||||
$board = new Board();
|
$board = new Board();
|
||||||
$board->setId($boardId);
|
$board->setId($boardId);
|
||||||
$board->setOwner($owner);
|
$board->setOwner($owner);
|
||||||
|
$board->setAcl($this->getAcls($boardId));
|
||||||
if ($boardId === null) {
|
if ($boardId === null) {
|
||||||
$this->boardMapper->expects($this->any())->method('find')->willThrowException(new DoesNotExistException('not found'));
|
$this->boardMapper->expects($this->any())->method('find')->willThrowException(new DoesNotExistException('not found'));
|
||||||
} else {
|
} else {
|
||||||
$this->boardMapper->expects($this->any())->method('find')->willReturn($board);
|
$this->boardMapper->expects($this->any())->method('find')->willReturn($board);
|
||||||
}
|
}
|
||||||
$acls = $this->getAcls($boardId);
|
|
||||||
$this->aclMapper->expects($this->any())->method('findAll')->willReturn($acls);
|
|
||||||
|
|
||||||
|
|
||||||
if ($result) {
|
if ($result) {
|
||||||
$actual = $this->service->checkPermission($mapper, 1234, $permission);
|
$actual = $this->service->checkPermission($mapper, 1234, $permission);
|
||||||
|
|||||||
Reference in New Issue
Block a user