Compare commits

..

15 Commits

Author SHA1 Message Date
Julius Härtl
bd8fd6a66b Bump version to 1.4.2
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-05-03 14:10:54 +02:00
Julius Härtl
0eba8d0840 Merge pull request #3040 from nextcloud/backport/3038/stable1.4
[stable1.4] Get attachment from the user node instead of the share source
2021-05-03 09:02:33 -01:00
Julius Härtl
8fc95dc40d Merge pull request #3039 from nextcloud/backport/3037/stable1.4
[stable1.4] Catch any error during circle detail fetching
2021-05-03 09:02:26 -01:00
Julius Härtl
ecd3e25588 Get attachment from the user node instead of the share source
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-05-03 09:44:20 +00:00
Julius Härtl
914f912612 Catch any error during circle detail fetching
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-05-03 11:43:43 +02:00
Christoph Wurst
e68f723095 Merge pull request #3031 from nextcloud/backport/3016/stable1.4
[stable1.4] Allow searching for filters without a query to match all that have a given filter set
2021-04-30 15:30:21 +02:00
Julius Härtl
5f71be2e7f Add test case for special characters
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-04-30 13:13:39 +00:00
Julius Härtl
bc2a72f035 Catch canceled requests and show better loading indication
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-04-30 13:13:39 +00:00
Julius Härtl
cf4be82827 Fix handling of quotes in queries
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-04-30 13:13:39 +00:00
Julius Härtl
23580705aa Allow searching for filters without a query to match all that have a given filter set
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-04-30 13:13:39 +00:00
Christoph Wurst
65c8c394a8 Merge pull request #3030 from nextcloud/backport/3014
Proper error handling when fetching comments fails
2021-04-30 15:12:10 +02:00
Julius Härtl
422788a6a3 Show comment counter and highlight if unread comments are available
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-04-30 10:39:22 +02:00
Julius Härtl
2d5e29de5d Allow to cancel repies and adapt comment ui to talk
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-04-30 10:39:22 +02:00
Julius Härtl
2a307b92a7 Wrap lines properly in comment text
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-04-30 10:38:09 +02:00
Julius Härtl
2d8dbc70ad Proper error handling when fetching comments fails
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-04-30 10:37:54 +02:00
22 changed files with 281 additions and 75 deletions

View File

@@ -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

View File

@@ -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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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(),

View File

@@ -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>

View File

@@ -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] {

View File

@@ -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 {

View File

@@ -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;
} }
} }
} }

View File

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

View File

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

View File

@@ -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()

View File

@@ -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-&gt;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-&gt;t('%s has mentioned you in a comment on "%s".', [$dn, $params[0]])</code> <code>(string) $l-&gt;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-&gt;currentUser</code>
</UndefinedThisPropertyAssignment>
<UndefinedThisPropertyFetch occurrences="1">
<code>$this-&gt;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-&gt;rootFolder</code> <code>$this-&gt;rootFolder</code>
<code>$this-&gt;rootFolder</code> <code>$this-&gt;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-&gt;l10n</code>
<code>$this-&gt;urlGenerator</code>
<code>$this-&gt;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>

View File

@@ -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 = [

View File

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

View File

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

View File

@@ -0,0 +1,47 @@
<?php
/*
* @copyright Copyright (c) 2021 Julius Härtl <jus@bitgrid.net>
*
* @author Julius Härtl <jus@bitgrid.net>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
declare(strict_types=1);
namespace OCA\Deck\Search\Query;
use PHPUnit\Framework\TestCase;
class AQueryParameterTest extends TestCase {
public function dataValue() {
return [
['foo', 'foo'],
['spätial character', 'spätial character'],
['"spätial character"', 'spätial character'],
['"spätial "character"', 'spätial "character'],
['"spätial 🐘"', 'spätial 🐘'],
['\'spätial character\'', '\'spätial character\''],
];
}
/** @dataProvider dataValue */
public function testValue($input, $expectedValue) {
$parameter = new StringQueryParameter('test', 0, $input);
$this->assertEquals($expectedValue, $parameter->getValue());
}
}

View File

@@ -128,7 +128,7 @@ class CardServiceTest extends TestCase {
$this->userManager->expects($this->once()) $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);