Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bd8fd6a66b | ||
|
|
0eba8d0840 | ||
|
|
8fc95dc40d | ||
|
|
ecd3e25588 | ||
|
|
914f912612 | ||
|
|
e68f723095 | ||
|
|
5f71be2e7f | ||
|
|
bc2a72f035 | ||
|
|
cf4be82827 | ||
|
|
23580705aa | ||
|
|
65c8c394a8 | ||
|
|
422788a6a3 | ||
|
|
2d5e29de5d | ||
|
|
2a307b92a7 | ||
|
|
2d8dbc70ad |
@@ -1,6 +1,15 @@
|
|||||||
# 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.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
|
## 1.4.1 - 2021-04-20
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
- 🚀 Get your project organized
|
- 🚀 Get your project organized
|
||||||
|
|
||||||
</description>
|
</description>
|
||||||
<version>1.4.1</version>
|
<version>1.4.2</version>
|
||||||
<licence>agpl</licence>
|
<licence>agpl</licence>
|
||||||
<author>Julius Härtl</author>
|
<author>Julius Härtl</author>
|
||||||
<namespace>Deck</namespace>
|
<namespace>Deck</namespace>
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -25,9 +25,9 @@ namespace OCA\Deck\Db;
|
|||||||
|
|
||||||
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,6 +35,7 @@ class BoardMapper extends DeckMapper implements IPermissionMapper {
|
|||||||
private $stackMapper;
|
private $stackMapper;
|
||||||
private $userManager;
|
private $userManager;
|
||||||
private $groupManager;
|
private $groupManager;
|
||||||
|
private $logger;
|
||||||
|
|
||||||
private $circlesEnabled;
|
private $circlesEnabled;
|
||||||
|
|
||||||
@@ -44,7 +45,8 @@ class BoardMapper extends DeckMapper implements IPermissionMapper {
|
|||||||
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 +54,7 @@ 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->circlesEnabled = \OC::$server->getAppManager()->isEnabledForUser('circles');
|
$this->circlesEnabled = \OC::$server->getAppManager()->isEnabledForUser('circles');
|
||||||
}
|
}
|
||||||
@@ -248,7 +251,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 +259,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 +271,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');
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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());
|
||||||
|
|||||||
@@ -125,7 +125,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(),
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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,6 @@
|
|||||||
<code>$cardId</code>
|
<code>$cardId</code>
|
||||||
<code>$cardId</code>
|
<code>$cardId</code>
|
||||||
</InvalidScalarArgument>
|
</InvalidScalarArgument>
|
||||||
<UndefinedThisPropertyAssignment occurrences="1">
|
|
||||||
<code>$this->currentUser</code>
|
|
||||||
</UndefinedThisPropertyAssignment>
|
|
||||||
<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 +255,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>
|
||||||
@@ -279,13 +268,6 @@
|
|||||||
<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
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);
|
||||||
|
|||||||
Reference in New Issue
Block a user