Merge pull request #3030 from nextcloud/backport/3014
Proper error handling when fetching comments fails
This commit is contained in:
@@ -22,7 +22,9 @@
|
||||
.icon-activity {
|
||||
@include icon-color('activity-dark', 'activity', $color-black);
|
||||
}
|
||||
|
||||
.icon-comment--unread {
|
||||
@include icon-color('comment', 'actions', $color-primary, 1, true);
|
||||
}
|
||||
|
||||
.avatardiv.circles {
|
||||
background: var(--color-primary);
|
||||
|
||||
@@ -49,6 +49,7 @@ class Card extends RelationalEntity {
|
||||
protected $notified = false;
|
||||
protected $deletedAt = 0;
|
||||
protected $commentsUnread = 0;
|
||||
protected $commentsCount = 0;
|
||||
|
||||
protected $relatedStack = null;
|
||||
protected $relatedBoard = null;
|
||||
@@ -75,6 +76,7 @@ class Card extends RelationalEntity {
|
||||
$this->addRelation('attachmentCount');
|
||||
$this->addRelation('participants');
|
||||
$this->addRelation('commentsUnread');
|
||||
$this->addRelation('commentsCount');
|
||||
$this->addResolvable('owner');
|
||||
|
||||
$this->addRelation('relatedStack');
|
||||
|
||||
@@ -105,8 +105,10 @@ class CardService {
|
||||
$card->setAttachmentCount($this->attachmentService->count($cardId));
|
||||
$user = $this->userManager->get($this->currentUser);
|
||||
$lastRead = $this->commentsManager->getReadMark('deckCard', (string)$card->getId(), $user);
|
||||
$count = $this->commentsManager->getNumberOfCommentsForObject('deckCard', (string)$card->getId(), $lastRead);
|
||||
$card->setCommentsUnread($count);
|
||||
$countUnreadComments = $this->commentsManager->getNumberOfCommentsForObject('deckCard', (string)$card->getId(), $lastRead);
|
||||
$countComments = $this->commentsManager->getNumberOfCommentsForObject('deckCard', (string)$card->getId());
|
||||
$card->setCommentsUnread($countUnreadComments);
|
||||
$card->setCommentsCount($countComments);
|
||||
|
||||
$stack = $this->stackMapper->find($card->getStackId());
|
||||
$board = $this->boardService->find($stack->getBoardId());
|
||||
|
||||
@@ -7,7 +7,11 @@
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<CommentItem v-if="replyTo" :comment="replyTo" :reply="true" />
|
||||
<CommentItem v-if="replyTo"
|
||||
:comment="replyTo"
|
||||
:reply="true"
|
||||
:preview="true"
|
||||
@cancel="cancelReply" />
|
||||
<CommentForm v-model="newComment" @submit="createComment" />
|
||||
|
||||
<ul v-if="getCommentsForCard(card.id).length > 0" id="commentsFeed">
|
||||
@@ -23,8 +27,8 @@
|
||||
</ul>
|
||||
<div v-else-if="isLoading" class="icon icon-loading" />
|
||||
<div v-else class="emptycontent">
|
||||
<div class="icon-comment" />
|
||||
<p>{{ t('deck', 'No comments yet. Begin the discussion!') }}</p>
|
||||
<div :class="{ 'icon-comment': !error, 'icon-error': error }" />
|
||||
<p>{{ error || t('deck', 'No comments yet. Begin the discussion!') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -36,6 +40,7 @@ import CommentItem from './CommentItem'
|
||||
import CommentForm from './CommentForm'
|
||||
import InfiniteLoading from 'vue-infinite-loading'
|
||||
import { getCurrentUser } from '@nextcloud/auth'
|
||||
|
||||
export default {
|
||||
name: 'CardSidebarTabComments',
|
||||
components: {
|
||||
@@ -60,6 +65,7 @@ export default {
|
||||
newComment: '',
|
||||
isLoading: false,
|
||||
currentUser: getCurrentUser(),
|
||||
error: null,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -85,19 +91,34 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
async infiniteHandler($state) {
|
||||
await this.loadMore()
|
||||
if (this.hasMoreComments(this.card.id)) {
|
||||
$state.loaded()
|
||||
} else {
|
||||
this.error = null
|
||||
try {
|
||||
await this.loadMore()
|
||||
if (this.hasMoreComments(this.card.id)) {
|
||||
$state.loaded()
|
||||
} else {
|
||||
$state.complete()
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to fetch more comments during infinite loading', e)
|
||||
this.error = t('deck', 'Failed to load comments')
|
||||
$state.complete()
|
||||
}
|
||||
},
|
||||
async loadComments() {
|
||||
this.$store.dispatch('setReplyTo', null)
|
||||
this.error = null
|
||||
this.isLoading = true
|
||||
await this.$store.dispatch('fetchComments', { cardId: this.card.id })
|
||||
this.isLoading = false
|
||||
if (this.card.commentsUnread > 0) {
|
||||
await this.$store.dispatch('markCommentsAsRead', this.card.id)
|
||||
try {
|
||||
await this.$store.dispatch('fetchComments', { cardId: this.card.id })
|
||||
this.isLoading = false
|
||||
if (this.card.commentsUnread > 0) {
|
||||
await this.$store.dispatch('markCommentsAsRead', this.card.id)
|
||||
}
|
||||
} catch (e) {
|
||||
this.isLoading = false
|
||||
console.error('Failed to fetch more comments during infinite loading', e)
|
||||
this.error = t('deck', 'Failed to load comments')
|
||||
}
|
||||
},
|
||||
async createComment(content) {
|
||||
@@ -115,6 +136,9 @@ export default {
|
||||
await this.$store.dispatch('fetchMore', { cardId: this.card.id })
|
||||
this.isLoading = false
|
||||
},
|
||||
cancelReply() {
|
||||
this.$store.dispatch('setReplyTo', null)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -41,6 +41,7 @@
|
||||
</span>
|
||||
</template>
|
||||
<div ref="contentEditable"
|
||||
class="comment-form__contenteditable"
|
||||
contenteditable
|
||||
@keydown.enter="handleKeydown"
|
||||
@paste="onPaste"
|
||||
@@ -175,6 +176,11 @@ export default {
|
||||
<style scoped lang="scss">
|
||||
@import '../../css/comments';
|
||||
|
||||
.comment-form__contenteditable {
|
||||
word-break: break-word;
|
||||
border-radius: var(--border-radius-large)
|
||||
}
|
||||
|
||||
.atwho-wrap {
|
||||
width: 100%;
|
||||
& > div[contenteditable] {
|
||||
|
||||
@@ -1,10 +1,22 @@
|
||||
<template>
|
||||
<div v-if="reply" class="reply">
|
||||
<span class="reply--hint">{{ t('deck', 'In reply to') }} <UserBubble :user="comment.actorId" :display-name="comment.actorDisplayName" /></span>
|
||||
<RichText class="comment--content"
|
||||
:text="richText(comment)"
|
||||
:arguments="richArgs(comment)"
|
||||
:autolink="true" />
|
||||
<div v-if="reply" class="reply" :class="{ 'reply--preview': preview }">
|
||||
<div class="reply--wrapper">
|
||||
<div class="reply--header">
|
||||
<div class="reply--hint">
|
||||
{{ t('deck', 'In reply to') }}
|
||||
<UserBubble :user="comment.actorId" :display-name="comment.actorDisplayName" />
|
||||
</div>
|
||||
<Actions v-if="preview" class="reply--cancel">
|
||||
<ActionButton icon="icon-close" @click="$emit('cancel')">
|
||||
{{ t('deck', 'Cancel reply') }}
|
||||
</ActionButton>
|
||||
</Actions>
|
||||
</div>
|
||||
<RichText class="comment--content"
|
||||
:text="richText(comment)"
|
||||
:arguments="richArgs(comment)"
|
||||
:autolink="true" />
|
||||
</div>
|
||||
</div>
|
||||
<li v-else class="comment">
|
||||
<template>
|
||||
@@ -14,13 +26,19 @@
|
||||
{{ comment.actorDisplayName }}
|
||||
</span>
|
||||
<Actions v-show="!edit" :force-menu="true">
|
||||
<ActionButton icon="icon-reply" @click="replyTo()">
|
||||
<ActionButton icon="icon-reply" :close-after-click="true" @click="replyTo()">
|
||||
{{ t('deck', 'Reply') }}
|
||||
</ActionButton>
|
||||
<ActionButton v-if="canEdit" icon="icon-rename" @click="showUpdateForm()">
|
||||
<ActionButton v-if="canEdit"
|
||||
icon="icon-rename"
|
||||
:close-after-click="true"
|
||||
@click="showUpdateForm()">
|
||||
{{ t('deck', 'Update') }}
|
||||
</ActionButton>
|
||||
<ActionButton v-if="canEdit" icon="icon-delete" @click="deleteComment()">
|
||||
<ActionButton v-if="canEdit"
|
||||
icon="icon-delete"
|
||||
:close-after-click="true"
|
||||
@click="deleteComment()">
|
||||
{{ t('deck', 'Delete') }}
|
||||
</ActionButton>
|
||||
</Actions>
|
||||
@@ -86,6 +104,10 @@ export default {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
preview: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@@ -175,20 +197,41 @@ export default {
|
||||
@import '../../css/comments';
|
||||
|
||||
.reply {
|
||||
border-left: 3px solid var(--color-primary-element);
|
||||
padding-left: 5px;
|
||||
margin-left: 2px;
|
||||
margin-bottom: 5px;
|
||||
margin: 0 0 0 44px;
|
||||
|
||||
&.reply--preview {
|
||||
margin: 4px 0;
|
||||
padding: 8px;
|
||||
background-color: var(--color-background-hover);
|
||||
border-radius: var(--border-radius-large);
|
||||
|
||||
.reply--wrapper {
|
||||
margin: 8px;
|
||||
}
|
||||
|
||||
.reply--cancel {
|
||||
margin-right: -12px;
|
||||
margin-top: -12px;
|
||||
}
|
||||
}
|
||||
|
||||
.reply--wrapper {
|
||||
border-left: 4px solid var(--color-border-dark);
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
&::v-deep .rich-text--wrapper {
|
||||
margin-top: -3px;
|
||||
color: var(--color-text-light);
|
||||
color: var(--color-text-lighter);
|
||||
}
|
||||
|
||||
.reply--header {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.reply--hint {
|
||||
font-size: 0.9em;
|
||||
color: var(--color-text-lighter);
|
||||
vertical-align: top;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.comment--content {
|
||||
|
||||
@@ -22,7 +22,13 @@
|
||||
|
||||
<template>
|
||||
<div v-if="card" class="badges">
|
||||
<div v-if="card.commentsUnread > 0" class="icon icon-comment" />
|
||||
<div v-if="card.commentsCount > 0"
|
||||
v-tooltip="commentsHint"
|
||||
class="icon icon-comment"
|
||||
:class="{ 'icon-comment--unread': card.commentsUnread > 0 }"
|
||||
@click.stop="openComments">
|
||||
{{ card.commentsCount }}
|
||||
</div>
|
||||
|
||||
<div v-if="card.description && checkListCount > 0" class="card-tasks icon icon-checkmark">
|
||||
{{ checkListCheckedCount }}/{{ checkListCount }}
|
||||
@@ -58,6 +64,21 @@ export default {
|
||||
checkListCheckedCount() {
|
||||
return (this.card.description.match(/^\s*([*+-]|(\d\.))\s+\[\s*x\s*\](.*)$/gim) || []).length
|
||||
},
|
||||
commentsHint() {
|
||||
if (this.card.commentsUnread > 0) {
|
||||
return t('deck', '{count} comments, {unread} unread', {
|
||||
count: this.card.commentsCount,
|
||||
unread: this.card.commentsUnread
|
||||
})
|
||||
}
|
||||
return null
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
openComments() {
|
||||
const boardId = this.card && this.card.boardId ? this.card.boardId : this.$route.params.id
|
||||
this.$router.push({ name: 'card', params: { id: boardId, cardId: this.card.id, tabId: 'comments' } })
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -70,7 +91,7 @@ export default {
|
||||
|
||||
.icon {
|
||||
opacity: 0.5;
|
||||
padding: 12px 18px;
|
||||
padding: 10px 20px;
|
||||
padding-right: 4px;
|
||||
margin-right: 5px;
|
||||
background-position: left;
|
||||
@@ -78,8 +99,8 @@ export default {
|
||||
span {
|
||||
margin-left: 18px;
|
||||
}
|
||||
&.icon-edit {
|
||||
opacity: 0.5;
|
||||
&.icon-comment--unread {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,4 +48,5 @@
|
||||
|
||||
.comment--content {
|
||||
margin-left: 44px;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
@@ -83,6 +83,7 @@ class CardTest extends TestCase {
|
||||
'assignedUsers' => null,
|
||||
'deletedAt' => 0,
|
||||
'commentsUnread' => 0,
|
||||
'commentsCount' => 0,
|
||||
'lastEditor' => null,
|
||||
'ETag' => $card->getETag(),
|
||||
], $card->jsonSerialize());
|
||||
@@ -109,6 +110,7 @@ class CardTest extends TestCase {
|
||||
'assignedUsers' => null,
|
||||
'deletedAt' => 0,
|
||||
'commentsUnread' => 0,
|
||||
'commentsCount' => 0,
|
||||
'lastEditor' => null,
|
||||
'ETag' => $card->getETag(),
|
||||
], $card->jsonSerialize());
|
||||
@@ -145,6 +147,7 @@ class CardTest extends TestCase {
|
||||
'assignedUsers' => ['user1'],
|
||||
'deletedAt' => 0,
|
||||
'commentsUnread' => 0,
|
||||
'commentsCount' => 0,
|
||||
'lastEditor' => null,
|
||||
'ETag' => $card->getETag(),
|
||||
], $card->jsonSerialize());
|
||||
|
||||
@@ -128,7 +128,7 @@ class CardServiceTest extends TestCase {
|
||||
$this->userManager->expects($this->once())
|
||||
->method('get')
|
||||
->willReturn($user);
|
||||
$this->commentsManager->expects($this->once())
|
||||
$this->commentsManager->expects($this->any())
|
||||
->method('getNumberOfCommentsForObject')
|
||||
->willReturn(0);
|
||||
$boardMock = $this->createMock(Board::class);
|
||||
|
||||
Reference in New Issue
Block a user