Implement api endpoints for comment reply handling

Signed-off-by: Julius Härtl <jus@bitgrid.net>
This commit is contained in:
Julius Härtl
2020-02-11 18:21:43 +01:00
parent e6de5fe3a9
commit 841fa0d4dd
10 changed files with 307 additions and 153 deletions

View File

@@ -123,7 +123,9 @@ return [
['name' => 'attachment_api#restore', 'url' => '/api/v1.0/boards/{boardId}/stacks/{stackId}/cards/{cardId}/attachments/{attachmentId}/restore', 'verb' => 'PUT'], ['name' => 'attachment_api#restore', 'url' => '/api/v1.0/boards/{boardId}/stacks/{stackId}/cards/{cardId}/attachments/{attachmentId}/restore', 'verb' => 'PUT'],
['name' => 'comments_api#list', 'url' => '/api/v1.0/boards/{boardId}/stacks/{stackId}/cards/{cardId}/comments', 'verb' => 'GET'], ['name' => 'comments_api#list', 'url' => '/api/v1.0/boards/{boardId}/stacks/{stackId}/cards/{cardId}/comments', 'verb' => 'GET'],
['name' => 'comments_api#create', 'url' => '/api/v1.0/boards/{boardId}/stacks/{stackId}/cards/{cardId}/comments', 'verb' => 'POST'],
['name' => 'comments_api#update', 'url' => '/api/v1.0/boards/{boardId}/stacks/{stackId}/cards/{cardId}/comments/{commentId}', 'verb' => 'PUT'],
['name' => 'comments_api#delete', 'url' => '/api/v1.0/boards/{boardId}/stacks/{stackId}/cards/{cardId}/comments/{commentId}', 'verb' => 'DELETE'],
['name' => 'board_api#preflighted_cors', 'url' => '/api/v1.0/{path}','verb' => 'OPTIONS', 'requirements' => ['path' => '.+']], ['name' => 'board_api#preflighted_cors', 'url' => '/api/v1.0/{path}','verb' => 'OPTIONS', 'requirements' => ['path' => '.+']],
] ]

View File

@@ -61,6 +61,7 @@
@include icon-black-white('clone', 'deck', 1); @include icon-black-white('clone', 'deck', 1);
@include icon-black-white('filter', 'deck', 1); @include icon-black-white('filter', 'deck', 1);
@include icon-black-white('attach', 'deck', 1); @include icon-black-white('attach', 'deck', 1);
@include icon-black-white('reply', 'deck', 1);
.icon-toggle-compact-collapsed { .icon-toggle-compact-collapsed {
@include icon-color('toggle-view-expand', 'deck', $color-black); @include icon-color('toggle-view-expand', 'deck', $color-black);

View File

@@ -56,6 +56,8 @@ class Application extends App {
public const APP_ID = 'deck'; public const APP_ID = 'deck';
public const COMMENT_ENTITY_TYPE = 'deckCard';
/** @var IServerContainer */ /** @var IServerContainer */
private $server; private $server;
@@ -149,7 +151,7 @@ class Application extends App {
public function registerCommentsEntity(): void { public function registerCommentsEntity(): void {
$this->server->getEventDispatcher()->addListener(CommentsEntityEvent::EVENT_ENTITY, function(CommentsEntityEvent $event) { $this->server->getEventDispatcher()->addListener(CommentsEntityEvent::EVENT_ENTITY, function(CommentsEntityEvent $event) {
$event->addEntityCollection('deckCard', function($name) { $event->addEntityCollection(self::COMMENT_ENTITY_TYPE, function($name) {
/** @var CardMapper */ /** @var CardMapper */
$cardMapper = $this->getContainer()->query(CardMapper::class); $cardMapper = $this->getContainer()->query(CardMapper::class);
$permissionService = $this->getContainer()->query(PermissionService::class); $permissionService = $this->getContainer()->query(PermissionService::class);

View File

@@ -23,87 +23,59 @@
namespace OCA\Deck\Controller; namespace OCA\Deck\Controller;
use OCA\Deck\Service\CommentService;
use OCA\Deck\StatusException;
use OCP\AppFramework\ApiController; use OCP\AppFramework\ApiController;
use OCP\AppFramework\Http\DataResponse; use OCP\AppFramework\Http\DataResponse;
use OCP\AppFramework\Http\JSONResponse;
use OCP\Comments\IComment;
use OCP\Comments\ICommentsManager;
use OCP\Comments\NotFoundException;
use OCP\ILogger;
use OCP\IRequest; use OCP\IRequest;
use OCP\IUserManager;
class CommentsApiController extends ApiController { class CommentsApiController extends ApiController {
/** @var ICommentsManager */ /** @var CommentService */
private $commentsManager; private $commentService;
/** @var IUserManager */
private $userManager;
/** @var ILogger */
private $logger;
public function __construct( public function __construct(
$appName, IRequest $request, $corsMethods = 'PUT, POST, GET, DELETE, PATCH', $corsAllowedHeaders = 'Authorization, Content-Type, Accept', $corsMaxAge = 1728000, $appName, IRequest $request, $corsMethods = 'PUT, POST, GET, DELETE, PATCH', $corsAllowedHeaders = 'Authorization, Content-Type, Accept', $corsMaxAge = 1728000,
ICommentsManager $commentsManager, CommentService $commentService
IUserManager $userManager,
ILogger $logger
) { ) {
parent::__construct($appName, $request, $corsMethods, $corsAllowedHeaders, $corsMaxAge); parent::__construct($appName, $request, $corsMethods, $corsAllowedHeaders, $corsMaxAge);
$this->commentService = $commentService;
$this->commentsManager = $commentsManager;
$this->userManager = $userManager;
$this->logger = $logger;
} }
/** /**
* @NoAdminRequired * @NoAdminRequired
* @CORS
* @NoCSRFRequired * @NoCSRFRequired
* @throws StatusException
*/ */
public function list(int $cardId, $limit = 20, $offset = 0): JSONResponse { public function list(string $cardId, int $limit = 20, int $offset = 0): DataResponse {
$comments = $this->commentsManager->getForObject('deckCard', $cardId, $limit, $offset); return $this->commentService->list($cardId, $limit, $offset);
$result = [];
foreach ($comments as $comment) {
$formattedComment = $this->formatComment($comment);
try {
if ($comment->getParentId() !== '0' && $replyTo = $this->commentsManager->get($comment->getParentId())) {
$formattedComment['replyTo'] = $this->formatComment($replyTo);
}
} catch (NotFoundException $e) {
}
$result[] = $formattedComment;
}
return new JSONResponse($result);
} }
private function formatComment(IComment $comment): array { /**
$user = $this->userManager->get($comment->getActorId()); * @NoAdminRequired
$actorDisplayName = $user !== null ? $user->getDisplayName() : $comment->getActorId(); * @NoCSRFRequired
* @throws StatusException
return [ */
'id' => $comment->getId(), public function create(string $cardId, string $message, string $parentId = '0'): DataResponse {
'message' => $comment->getMessage(), return $this->commentService->create($cardId, $message, $parentId);
'actorId' => $comment->getActorId(),
'actorType' => $comment->getActorType(),
'actorDisplayName' => $actorDisplayName,
'mentions' => array_map(function($mention) {
try {
$displayName = $this->commentsManager->resolveDisplayName($mention['type'], $mention['id']);
} catch (\OutOfBoundsException $e) {
$this->logger->logException($e);
// No displayname, upon client's discretion what to display.
$displayName = '';
}
return [
'mentionId' => $mention['id'],
'mentionType' => $mention['type'],
'mentionDisplayName' => $displayName
];
}, $comment->getMentions()),
];
} }
/**
* @NoAdminRequired
* @NoCSRFRequired
* @throws StatusException
*/
public function update(string $cardId, string $commentId, string $message): DataResponse {
return $this->commentService->update($cardId, $commentId, $message);
}
/**
* @NoAdminRequired
* @NoCSRFRequired
* @throws StatusException
*/
public function delete(string $cardId, string $commentId): DataResponse {
return $this->commentService->delete($cardId, $commentId);
}
} }

View File

@@ -0,0 +1,172 @@
<?php
/**
* @copyright Copyright (c) 2020 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/>.
*
*/
namespace OCA\Deck\Service;
use OCA\Deck\AppInfo\Application;
use OCA\Deck\BadRequestException;
use OCA\Deck\NoPermissionException;
use OCA\Deck\NotFoundException;
use OCA\Deck\StatusException;
use OCP\AppFramework\Http\DataResponse;
use OCP\Comments\IComment;
use OCP\Comments\ICommentsManager;
use OCP\Comments\MessageTooLongException;
use OCP\Comments\NotFoundException as CommentNotFoundException;
use OCP\ILogger;
use OCP\IUserManager;
use OutOfBoundsException;
use Sabre\DAV\Exception\Forbidden;
use function is_numeric;
class CommentService {
/**
* @var ICommentsManager
*/
private $commentsManager;
/**
* @var IUserManager
*/
private $userManager;
/** @var ILogger */
private $logger;
private $userId;
public function __construct(ICommentsManager $commentsManager, IUserManager $userManager, ILogger $logger, $userId) {
$this->commentsManager = $commentsManager;
$this->userManager = $userManager;
$this->logger = $logger;
$this->userId = $userId;
}
public function list(string $cardId, int $limit = 20, int $offset = 0): DataResponse {
if (!is_numeric($cardId)) {
throw new BadRequestException('A valid card id must be provided');
}
$comments = $this->commentsManager->getForObject(Application::COMMENT_ENTITY_TYPE, $cardId, $limit, $offset);
$result = [];
foreach ($comments as $comment) {
$formattedComment = $this->formatComment($comment);
try {
if ($comment->getParentId() !== '0' && $replyTo = $this->commentsManager->get($comment->getParentId())) {
$formattedComment['replyTo'] = $this->formatComment($replyTo);
}
} catch (CommentNotFoundException $e) {
}
$result[] = $formattedComment;
}
return new DataResponse($result);
}
/**
* @param string $cardId
* @param string $message
* @param string $replyTo
* @return DataResponse
* @throws BadRequestException
* @throws NotFoundException
*/
public function create(string $cardId, string $message, string $replyTo = '0'): DataResponse {
if (!is_numeric($cardId)) {
throw new BadRequestException('A valid card id must be provided');
}
try {
$comment = $this->commentsManager->create('users', $this->userId, Application::COMMENT_ENTITY_TYPE, $cardId);
$comment->setMessage($message);
$comment->setVerb('comment');
$comment->setParentId($replyTo);
$this->commentsManager->save($comment);
return new DataResponse($this->formatComment($comment));
} catch (\InvalidArgumentException $e) {
throw new BadRequestException('Invalid input values');
} catch (MessageTooLongException $e) {
$msg = 'Message exceeds allowed character limit of ';
throw new BadRequestException($msg . IComment::MAX_MESSAGE_LENGTH);
} catch (CommentNotFoundException $e) {
throw new NotFoundException('Could not create comment.');
}
}
public function update(string $cardId, string $commentId, string $message): DataResponse {
if (!is_numeric($cardId)) {
throw new BadRequestException('A valid card id must be provided');
}
if (!is_numeric($commentId)) {
throw new BadRequestException('A valid comment id must be provided');
}
try {
$comment = $this->commentsManager->get($commentId);
} catch (CommentNotFoundException $e) {
throw new NotFoundException('No comment found.');
}
if ($comment->getActorType() !== 'users' || $comment->getActorId() !== $this->userId) {
throw new NoPermissionException('Only authors are allowed to edit their comment.');
}
$comment->setMessage($message);
$this->commentsManager->save($comment);
return new DataResponse($this->formatComment($comment));
}
public function delete(string $cardId, string $commentId): DataResponse {
if (!is_numeric($cardId)) {
throw new BadRequestException('A valid card id must be provided');
}
if (!is_numeric($commentId)) {
throw new BadRequestException('A valid comment id must be provided');
}
$this->commentsManager->delete($commentId);
return new DataResponse([]);
}
private function formatComment(IComment $comment): array {
$user = $this->userManager->get($comment->getActorId());
$actorDisplayName = $user !== null ? $user->getDisplayName() : $comment->getActorId();
return [
'id' => $comment->getId(),
'objectId' => $comment->getObjectId(),
'message' => $comment->getMessage(),
'actorId' => $comment->getActorId(),
'actorType' => $comment->getActorType(),
'actorDisplayName' => $actorDisplayName,
'mentions' => array_map(function($mention) {
try {
$displayName = $this->commentsManager->resolveDisplayName($mention['type'], $mention['id']);
} catch (OutOfBoundsException $e) {
$this->logger->logException($e);
// No displayname, upon client's discretion what to display.
$displayName = '';
}
return [
'mentionId' => $mention['id'],
'mentionType' => $mention['type'],
'mentionDisplayName' => $displayName
];
}, $comment->getMentions()),
];
}
}

View File

@@ -7,6 +7,7 @@
</span> </span>
</div> </div>
<CommentItem v-if="replyTo" :comment="replyTo" :reply="true" />
<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">
@@ -58,6 +59,7 @@ export default {
computed: { computed: {
...mapState({ ...mapState({
currentBoard: state => state.currentBoard, currentBoard: state => state.currentBoard,
replyTo: state => state.comment.replyTo,
}), }),
...mapGetters([ ...mapGetters([
'getCommentsForCard', 'getCommentsForCard',
@@ -98,6 +100,7 @@ export default {
comment: content, comment: content,
} }
await this.$store.dispatch('createComment', commentObj) await this.$store.dispatch('createComment', commentObj)
this.$store.dispatch('setReplyTo', null)
this.newComment = '' this.newComment = ''
await this.loadComments() await this.loadComments()
}, },

View File

@@ -45,7 +45,7 @@
@keydown.enter="handleKeydown" @keydown.enter="handleKeydown"
@paste="onPaste" @paste="onPaste"
@blur="error = null" @blur="error = null"
@input="validate" /> @input="validate()" />
</At> </At>
<input v-tooltip="t('deck', 'Save')" <input v-tooltip="t('deck', 'Save')"
class="icon-confirm" class="icon-confirm"
@@ -99,10 +99,10 @@ export default {
}, },
}, },
methods: { methods: {
validate() { validate(submit) {
this.error = null this.error = null
const content = this.contentEditableToParsed() const content = this.contentEditableToParsed()
if (content.length === 0) { if (submit && content.length === 0) {
this.error = t('deck', 'The comment cannot be empty.') this.error = t('deck', 'The comment cannot be empty.')
} }
if (content.length > 1000) { if (content.length > 1000) {
@@ -111,14 +111,13 @@ export default {
return this.error === null ? content : null return this.error === null ? content : null
}, },
submit() { submit() {
const content = this.validate() const content = this.validate(true)
if (content) { if (content) {
this.$emit('input', content) this.$emit('input', content)
this.$emit('submit', content) this.$emit('submit', content)
} }
}, },
/** /* All credits for this go to the talk app
* All credits for this go to the talk app
* https://github.com/nextcloud/spreed/blob/e69740b372e17eec4541337b47baa262a5766510/src/components/NewMessageForm/NewMessageForm.vue#L100-L143 * https://github.com/nextcloud/spreed/blob/e69740b372e17eec4541337b47baa262a5766510/src/components/NewMessageForm/NewMessageForm.vue#L100-L143
*/ */
contentEditableToParsed() { contentEditableToParsed() {

View File

@@ -1,5 +1,12 @@
<template> <template>
<li class="comment"> <div v-if="reply" class="reply">
{{ t('deck', 'In reply to') }} <UserBubble :user="comment.actorId" :display-name="comment.actorDisplayName" />
<RichText class="comment--content"
:text="richText(comment)"
:arguments="richArgs(comment)"
:autolink="true" />
</div>
<li v-else class="comment">
<template> <template>
<div class="comment--header"> <div class="comment--header">
<Avatar :user="comment.actorId" /> <Avatar :user="comment.actorId" />
@@ -7,10 +14,13 @@
{{ comment.actorDisplayName }} {{ comment.actorDisplayName }}
</span> </span>
<Actions v-show="canEdit && !edit"> <Actions v-show="canEdit && !edit">
<ActionButton icon="icon-reply" @click="replyTo()">
{{ t('deck', 'Reply') }}
</ActionButton>
<ActionButton icon="icon-rename" @click="showUpdateForm()"> <ActionButton icon="icon-rename" @click="showUpdateForm()">
{{ t('deck', 'Update') }} {{ t('deck', 'Update') }}
</ActionButton> </ActionButton>
<ActionButton icon="icon-delete" @click="deleteComment(comment.id)"> <ActionButton icon="icon-delete" @click="deleteComment()">
{{ t('deck', 'Delete') }} {{ t('deck', 'Delete') }}
</ActionButton> </ActionButton>
</Actions> </Actions>
@@ -18,11 +28,12 @@
<ActionButton icon="icon-close" @click="hideUpdateForm" /> <ActionButton icon="icon-close" @click="hideUpdateForm" />
</Actions> </Actions>
</div> </div>
<CommentItem v-if="comment.replyTo" :reply="true" :comment="comment.replyTo" />
<RichText v-show="!edit" <RichText v-show="!edit"
ref="richTextElement" ref="richTextElement"
class="comment--content" class="comment--content"
:text="richText" :text="richText(comment)"
:arguments="richArgs" :arguments="richArgs(comment)"
:autolink="true" /> :autolink="true" />
<CommentForm v-if="edit" v-model="commentMsg" @submit="updateComment" /> <CommentForm v-if="edit" v-model="commentMsg" @submit="updateComment" />
</template> </template>
@@ -53,6 +64,7 @@ export default {
name: 'CommentItem', name: 'CommentItem',
components: { components: {
Avatar, Avatar,
UserBubble,
Actions, Actions,
ActionButton, ActionButton,
CommentForm, CommentForm,
@@ -63,6 +75,10 @@ export default {
type: Object, type: Object,
default: undefined, default: undefined,
}, },
reply: {
type: Boolean,
default: false,
},
}, },
data() { data() {
return { return {
@@ -76,40 +92,48 @@ export default {
return this.comment.actorId === getCurrentUser().uid return this.comment.actorId === getCurrentUser().uid
}, },
richText() { richText() {
let message = this.parsedMessage return (comment) => {
this.comment.mentions.forEach((mention, index) => { let message = this.parsedMessage(comment.message)
// Currently only [a-z\-_0-9] are allowed inside of placeholders so we use a hash of the mention id as a unique identifier comment.mentions.forEach((mention, index) => {
const hash = md5(mention.mentionId) // Currently only [a-z\-_0-9] are allowed inside of placeholders so we use a hash of the mention id as a unique identifier
message = message.split('@' + mention.mentionId + '').join(`{user-${hash}}`) const hash = md5(mention.mentionId)
message = message.split('@"' + mention.mentionId + '"').join(`{user-${hash}}`) message = message.split('@' + mention.mentionId + '').join(`{user-${hash}}`)
message = message.split('@"' + mention.mentionId + '"').join(`{user-${hash}}`)
}) })
return message return message
}
}, },
richArgs() { richArgs() {
const mentions = [...this.comment.mentions] return (comment) => {
const result = mentions.reduce(function(result, item, index) { const mentions = [...comment.mentions]
const itemKey = 'user-' + md5(item.mentionId) const result = mentions.reduce((result, item, index) => {
result[itemKey] = { const itemKey = 'user-' + md5(item.mentionId)
component: AtMention, result[itemKey] = {
props: { component: AtMention,
user: item.mentionId, props: {
displayName: item.mentionDisplayName, user: item.mentionId,
}, displayName: item.mentionDisplayName,
} },
}
return result
}, {})
return result return result
}, {}) }
return result
}, },
parsedMessage() { parsedMessage() {
const div = document.createElement('div') return (message) => {
div.innerHTML = this.comment.message const div = document.createElement('div')
return (div.textContent || div.innerText || '') div.innerHTML = message
return (div.textContent || div.innerText || '')
}
}, },
}, },
methods: { methods: {
replyTo() {
this.$store.dispatch('setReplyTo', this.comment)
},
showUpdateForm() { showUpdateForm() {
this.commentMsg = this.$refs.richTextElement.$el.innerHTML this.commentMsg = this.$refs.richTextElement.$el.innerHTML
this.edit = true this.edit = true
@@ -121,16 +145,16 @@ export default {
async updateComment() { async updateComment() {
const data = { const data = {
comment: this.commentMsg, comment: this.commentMsg,
cardId: this.comment.cardId, cardId: this.comment.objectId,
commentId: this.comment.id, commentId: this.comment.id,
} }
await this.$store.dispatch('updateComment', data) await this.$store.dispatch('updateComment', data)
this.hideUpdateForm() this.hideUpdateForm()
}, },
deleteComment(commentId) { deleteComment() {
const data = { const data = {
commentId: commentId, commentId: this.comment.id,
cardId: this.comment.cardId, cardId: this.comment.objectId,
} }
this.$store.dispatch('deleteComment', data) this.$store.dispatch('deleteComment', data)
}, },
@@ -141,6 +165,15 @@ export default {
<style scoped lang="scss"> <style scoped lang="scss">
@import "../../css/comments"; @import "../../css/comments";
.reply {
border-left: 3px solid var(--color-primary-element);
padding-left: 10px;
margin-left: 44px;
.comment--content {
margin: 0;
}
}
.comment--content::v-deep a { .comment--content::v-deep a {
text-decoration: underline; text-decoration: underline;
} }

View File

@@ -21,7 +21,6 @@
*/ */
import axios from '@nextcloud/axios' import axios from '@nextcloud/axios'
import { getCurrentUser } from '@nextcloud/auth'
import xmlToTagList from '../helpers/xml' import xmlToTagList from '../helpers/xml'
export class CommentApi { export class CommentApi {
@@ -33,19 +32,9 @@ export class CommentApi {
async loadComments({ cardId, limit, offset }) { async loadComments({ cardId, limit, offset }) {
const api = await axios.get(OC.generateUrl(`/apps/deck/api/v1.0/boards/0/stacks/0/cards/${cardId}/comments`), { const api = await axios.get(OC.generateUrl(`/apps/deck/api/v1.0/boards/0/stacks/0/cards/${cardId}/comments`), {
headers: {'OCS-APIRequest': 'true'} headers: { 'OCS-APIRequest': 'true' },
}) })
return api.data return api.data
const response = await axios({
method: 'REPORT',
url: this.url(`${cardId}`),
data: `<?xml version="1.0" encoding="utf-8" ?>
<oc:filter-comments xmlns:D="DAV:" xmlns:oc="http://owncloud.org/ns">
<oc:limit>${limit}</oc:limit>
<oc:offset>${offset}</oc:offset>
</oc:filter-comments>`,
})
return xmlToTagList(response.data)
} }
async fetchComment({ cardId, commentId }) { async fetchComment({ cardId, commentId }) {
@@ -71,49 +60,24 @@ export class CommentApi {
return xmlToTagList(response.data) return xmlToTagList(response.data)
} }
async createComment({ cardId, comment }) { async createComment({ cardId, comment, replyTo }) {
const response = await axios({ const api = await axios.post(OC.generateUrl(`/apps/deck/api/v1.0/boards/0/stacks/0/cards/${cardId}/comments`), {
method: 'POST', message: `${comment}`,
url: this.url(`${cardId}`), parentId: replyTo ? replyTo.id : null,
data: { actorType: 'users', message: `${comment}`, verb: 'comment' },
}) })
return api.data
const header = response.headers['content-location']
const headerArray = header.split('/')
const id = headerArray[headerArray.length - 1]
const ret = {
cardId: (cardId).toString(),
id: id,
uId: getCurrentUser().uid,
creationDateTime: (new Date()).toString(),
message: comment,
}
return ret
} }
async updateComment({ cardId, commentId, comment }) { async updateComment({ cardId, commentId, comment }) {
const response = await axios({ const api = await axios.put(OC.generateUrl(`/apps/deck/api/v1.0/boards/0/stacks/0/cards/${cardId}/comments/${commentId}`), {
method: 'PROPPATCH', message: `${comment}`,
url: this.url(`${cardId}/${commentId}`),
data: `<?xml version="1.0"?>
<d:propertyupdate xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns">
<d:set>
<d:prop>
<oc:message>${comment}</oc:message>
</d:prop>
</d:set>
</d:propertyupdate>`,
}) })
return response.data return api.data
} }
async deleteComment({ cardId, commentId }) { async deleteComment({ cardId, commentId }) {
const response = await axios({ const api = await axios.delete(OC.generateUrl(`/apps/deck/api/v1.0/boards/0/stacks/0/cards/${cardId}/comments/${commentId}`))
method: 'DELETE', return api.data
url: this.url(`${cardId}/${commentId}`),
})
return response.data
} }
async markCommentsAsRead(cardId) { async markCommentsAsRead(cardId) {

View File

@@ -30,6 +30,7 @@ const COMMENT_FETCH_LIMIT = 10
export default { export default {
state: { state: {
comments: {}, comments: {},
replyTo: null,
}, },
getters: { getters: {
getCommentsForCard: (state) => (id) => { getCommentsForCard: (state) => (id) => {
@@ -78,6 +79,9 @@ export default {
Vue.set(_comment, 'isUnread', false) Vue.set(_comment, 'isUnread', false)
}) })
}, },
setReplyTo(state, comment) {
Vue.set(state, 'replyTo', comment)
},
}, },
actions: { actions: {
async fetchComments({ commit }, { cardId, offset }) { async fetchComments({ commit }, { cardId, offset }) {
@@ -103,8 +107,8 @@ export default {
await dispatch('fetchComments', { cardId, offset: getters.getCommentsForCard(cardId).length }) await dispatch('fetchComments', { cardId, offset: getters.getCommentsForCard(cardId).length })
}, },
async createComment({ commit, dispatch }, { cardId, comment }) { async createComment({ commit, dispatch, state }, { cardId, comment }) {
await apiClient.createComment({ cardId, comment }) await apiClient.createComment({ cardId, comment, replyTo: state.replyTo })
await dispatch('fetchComments', { cardId }) await dispatch('fetchComments', { cardId })
}, },
async deleteComment({ commit }, data) { async deleteComment({ commit }, data) {
@@ -112,13 +116,15 @@ export default {
commit('deleteComment', data) commit('deleteComment', data)
}, },
async updateComment({ commit }, data) { async updateComment({ commit }, data) {
await apiClient.updateComment(data) const comment = await apiClient.updateComment(data)
const commentData = await apiClient.fetchComment(data) await commit('updateComment', { cardId: data.cardId, comment: comment })
await commit('updateComment', { cardId: data.cardId, comment: commentData[0] })
}, },
async markCommentsAsRead({ commit }, cardId) { async markCommentsAsRead({ commit }, cardId) {
await apiClient.markCommentsAsRead(cardId) await apiClient.markCommentsAsRead(cardId)
await commit('markCommentsAsRead', cardId) await commit('markCommentsAsRead', cardId)
}, },
setReplyTo({ commit }, comment) {
commit('setReplyTo', comment)
},
}, },
} }