Implement fetching newer comments and infinite loading
Signed-off-by: Julius Härtl <jus@bitgrid.net>
This commit is contained in:
@@ -36,14 +36,16 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ul v-if="comments[card.id] && comments[card.id].length > 0" id="commentsFeed">
|
<ul v-if="getCommentsForCard(card.id).length > 0" id="commentsFeed">
|
||||||
<CommentItem v-for="comment in comments[card.id]"
|
<CommentItem v-for="comment in getCommentsForCard(card.id)"
|
||||||
:key="comment.id"
|
:key="comment.id"
|
||||||
:comment="comment"
|
:comment="comment"
|
||||||
@doReload="loadComments" />
|
@doReload="loadComments" />
|
||||||
<a @click="loadMore">
|
<InfiniteLoading :identifier="card.id" @infinite="infiniteHandler">
|
||||||
{{ t('deck', 'Load More') }}
|
<div slot="spinner" class="icon-loading" />
|
||||||
</a>
|
<div slot="no-more" />
|
||||||
|
<div slot="no-results" />
|
||||||
|
</InfiniteLoading>
|
||||||
</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">
|
||||||
@@ -59,9 +61,10 @@ import tippy from 'tippy.js'
|
|||||||
import { Editor, EditorContent } from 'tiptap'
|
import { Editor, EditorContent } from 'tiptap'
|
||||||
import { Mention } from 'tiptap-extensions'
|
import { Mention } from 'tiptap-extensions'
|
||||||
|
|
||||||
import { mapState } from 'vuex'
|
import { mapState, mapGetters } from 'vuex'
|
||||||
import { Avatar } from '@nextcloud/vue'
|
import { Avatar } from '@nextcloud/vue'
|
||||||
import CommentItem from './CommentItem'
|
import CommentItem from './CommentItem'
|
||||||
|
import InfiniteLoading from 'vue-infinite-loading'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'CardSidebarTabComments',
|
name: 'CardSidebarTabComments',
|
||||||
@@ -69,6 +72,7 @@ export default {
|
|||||||
Avatar,
|
Avatar,
|
||||||
CommentItem,
|
CommentItem,
|
||||||
EditorContent,
|
EditorContent,
|
||||||
|
InfiniteLoading,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
card: {
|
card: {
|
||||||
@@ -80,9 +84,6 @@ export default {
|
|||||||
return {
|
return {
|
||||||
newComment: '',
|
newComment: '',
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
limit: 20,
|
|
||||||
offset: 0,
|
|
||||||
|
|
||||||
editor: new Editor({
|
editor: new Editor({
|
||||||
extensions: [
|
extensions: [
|
||||||
new Mention({
|
new Mention({
|
||||||
@@ -176,7 +177,7 @@ export default {
|
|||||||
comments: state => state.comment.comments,
|
comments: state => state.comment.comments,
|
||||||
currentBoard: state => state.currentBoard,
|
currentBoard: state => state.currentBoard,
|
||||||
}),
|
}),
|
||||||
|
...mapGetters(['getCommentsForCard', 'hasMoreComments']),
|
||||||
hasResults() {
|
hasResults() {
|
||||||
return this.filteredUsers.length
|
return this.filteredUsers.length
|
||||||
},
|
},
|
||||||
@@ -193,17 +194,19 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
created() {
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
loadComments() {
|
async infiniteHandler($state) {
|
||||||
|
await this.loadMore()
|
||||||
|
if (this.hasMoreComments(this.card.id)) {
|
||||||
|
$state.loaded()
|
||||||
|
} else {
|
||||||
|
$state.complete()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async loadComments() {
|
||||||
this.isLoading = true
|
this.isLoading = true
|
||||||
this.card.limit = this.limit
|
await this.$store.dispatch('fetchComments', { cardId: this.card.id })
|
||||||
this.card.offset = this.offset
|
this.isLoading = false
|
||||||
this.$store.dispatch('listComments', this.card).then(response => {
|
|
||||||
this.isLoading = false
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
createComment() {
|
createComment() {
|
||||||
const commentObj = {
|
const commentObj = {
|
||||||
@@ -215,9 +218,10 @@ export default {
|
|||||||
this.newComment = ''
|
this.newComment = ''
|
||||||
this.editor.setContent('')
|
this.editor.setContent('')
|
||||||
},
|
},
|
||||||
loadMore() {
|
async loadMore() {
|
||||||
this.offset = this.offset + this.limit
|
this.isLoading = true
|
||||||
this.loadComments()
|
await this.$store.dispatch('fetchMore', { cardId: this.card.id })
|
||||||
|
this.isLoading = false
|
||||||
},
|
},
|
||||||
|
|
||||||
// navigate to the previous item
|
// navigate to the previous item
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import axios from '@nextcloud/axios'
|
import axios from '@nextcloud/axios'
|
||||||
|
import './../models'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This class handles all the api communication with the Deck backend.
|
* This class handles all the api communication with the Deck backend.
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
|
|
||||||
import axios from '@nextcloud/axios'
|
import axios from '@nextcloud/axios'
|
||||||
import { getCurrentUser } from '@nextcloud/auth'
|
import { getCurrentUser } from '@nextcloud/auth'
|
||||||
|
import xmlToTagList from '../helpers/xml'
|
||||||
|
|
||||||
export class CommentApi {
|
export class CommentApi {
|
||||||
|
|
||||||
@@ -30,97 +31,62 @@ export class CommentApi {
|
|||||||
return OC.linkToRemote(url)
|
return OC.linkToRemote(url)
|
||||||
}
|
}
|
||||||
|
|
||||||
listComments(card) {
|
async loadComments({ cardId, limit, offset }) {
|
||||||
return axios({
|
const response = await axios({
|
||||||
method: 'REPORT',
|
method: 'REPORT',
|
||||||
url: this.url(`${card.id}`),
|
url: this.url(`${cardId}`),
|
||||||
data: `<?xml version="1.0" encoding="utf-8" ?>
|
data: `<?xml version="1.0" encoding="utf-8" ?>
|
||||||
<oc:filter-comments xmlns:D="DAV:" xmlns:oc="http://owncloud.org/ns">
|
<oc:filter-comments xmlns:D="DAV:" xmlns:oc="http://owncloud.org/ns">
|
||||||
<oc:limit>${card.limit}</oc:limit>
|
<oc:limit>${limit}</oc:limit>
|
||||||
<oc:offset>${card.offset}</oc:offset>
|
<oc:offset>${offset}</oc:offset>
|
||||||
</oc:filter-comments>`,
|
</oc:filter-comments>`,
|
||||||
}).then(
|
})
|
||||||
(response) => {
|
return xmlToTagList(response.data)
|
||||||
return Promise.resolve(response.data)
|
|
||||||
},
|
|
||||||
(err) => {
|
|
||||||
return Promise.reject(err)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.catch((err) => {
|
|
||||||
return Promise.reject(err)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
createComment(commentObj) {
|
async createComment({ cardId, comment }) {
|
||||||
return axios({
|
const response = await axios({
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: this.url(`${commentObj.cardId}`),
|
url: this.url(`${cardId}`),
|
||||||
data: { actorType: 'users', message: `${commentObj.comment}`, verb: 'comment' },
|
data: { actorType: 'users', message: `${comment}`, verb: 'comment' },
|
||||||
}).then(
|
})
|
||||||
(response) => {
|
|
||||||
const header = response.headers['content-location']
|
|
||||||
const headerArray = header.split('/')
|
|
||||||
const id = headerArray[headerArray.length - 1]
|
|
||||||
|
|
||||||
const ret = {
|
const header = response.headers['content-location']
|
||||||
cardId: (commentObj.cardId).toString(),
|
const headerArray = header.split('/')
|
||||||
id: id,
|
const id = headerArray[headerArray.length - 1]
|
||||||
uId: getCurrentUser().uid,
|
|
||||||
creationDateTime: (new Date()).toString(),
|
const ret = {
|
||||||
message: commentObj.comment,
|
cardId: (cardId).toString(),
|
||||||
}
|
id: id,
|
||||||
return Promise.resolve(ret)
|
uId: getCurrentUser().uid,
|
||||||
},
|
creationDateTime: (new Date()).toString(),
|
||||||
(err) => {
|
message: comment,
|
||||||
return Promise.reject(err)
|
}
|
||||||
}
|
return ret
|
||||||
)
|
|
||||||
.catch((err) => {
|
|
||||||
return Promise.reject(err)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
updateComment(data) {
|
async updateComment({ cardId, commentId, comment }) {
|
||||||
return axios({
|
const response = await axios({
|
||||||
method: 'PROPPATCH',
|
method: 'PROPPATCH',
|
||||||
url: this.url(`${data.cardId}/${data.commentId}`),
|
url: this.url(`${cardId}/${commentId}`),
|
||||||
data: `<?xml version="1.0"?>
|
data: `<?xml version="1.0"?>
|
||||||
<d:propertyupdate xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns">
|
<d:propertyupdate xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns">
|
||||||
<d:set>
|
<d:set>
|
||||||
<d:prop>
|
<d:prop>
|
||||||
<oc:message>${data.comment}</oc:message>
|
<oc:message>${comment}</oc:message>
|
||||||
</d:prop>
|
</d:prop>
|
||||||
</d:set>
|
</d:set>
|
||||||
</d:propertyupdate>`,
|
</d:propertyupdate>`,
|
||||||
}).then(
|
})
|
||||||
(response) => {
|
return response.data
|
||||||
return Promise.resolve(response.data)
|
|
||||||
},
|
|
||||||
(err) => {
|
|
||||||
return Promise.reject(err)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.catch((err) => {
|
|
||||||
return Promise.reject(err)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteComment(data) {
|
async deleteComment({ cardId, commentId }) {
|
||||||
return axios({
|
const response = await axios({
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
url: this.url(`${data.cardId}/${data.commentId}`),
|
url: this.url(`${cardId}/${commentId}`),
|
||||||
}).then(
|
})
|
||||||
(response) => {
|
return response.data
|
||||||
return Promise.resolve(response.data)
|
|
||||||
},
|
|
||||||
(err) => {
|
|
||||||
return Promise.reject(err)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.catch((err) => {
|
|
||||||
return Promise.reject(err)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import axios from '@nextcloud/axios'
|
import axios from '@nextcloud/axios'
|
||||||
|
import './../models'
|
||||||
|
|
||||||
export class StackApi {
|
export class StackApi {
|
||||||
|
|
||||||
|
|||||||
@@ -21,73 +21,93 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { CommentApi } from '../services/CommentApi'
|
import { CommentApi } from '../services/CommentApi'
|
||||||
import xmlToTagList from '../helpers/xml'
|
|
||||||
import Vue from 'vue'
|
import Vue from 'vue'
|
||||||
|
|
||||||
const apiClient = new CommentApi()
|
const apiClient = new CommentApi()
|
||||||
|
|
||||||
|
const COMMENT_FETCH_LIMIT = 3
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
state: {
|
state: {
|
||||||
comments: {},
|
comments: {},
|
||||||
},
|
},
|
||||||
|
getters: {
|
||||||
|
getCommentsForCard: (state) => (id) => {
|
||||||
|
if (state.comments[id]) {
|
||||||
|
return [...state.comments[id].comments].sort((a, b) => b.id - a.id)
|
||||||
|
}
|
||||||
|
return []
|
||||||
|
},
|
||||||
|
hasMoreComments: (state) => (cardId) => {
|
||||||
|
return state.comments[cardId] && state.comments[cardId].hasMore
|
||||||
|
},
|
||||||
|
},
|
||||||
mutations: {
|
mutations: {
|
||||||
addComments(state, commentObj) {
|
endReached(state, { cardId }) {
|
||||||
if (state.comments[commentObj.cardId] === undefined) {
|
if (state.comments[cardId]) {
|
||||||
Vue.set(state.comments, commentObj.cardId, commentObj.comments)
|
state.comments[cardId].hasMore = false
|
||||||
} else {
|
|
||||||
// FIXME append comments once incremental fetching is implemented
|
|
||||||
// state.comments[commentObj.cardId].push(...commentObj.comments)
|
|
||||||
Vue.set(state.comments, commentObj.cardId, commentObj.comments)
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
createComment(state, newComment) {
|
addComments(state, { comments, cardId }) {
|
||||||
if (state.comments[newComment.cardId] === undefined) {
|
if (state.comments[cardId] === undefined) {
|
||||||
state.comments[newComment.cardId] = []
|
Vue.set(state.comments, cardId, {
|
||||||
|
hasMore: comments.length > 0,
|
||||||
|
comments,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
const newComments = comments.filter((comment) => {
|
||||||
|
return state.comments[cardId].comments.findIndex((item) => item.id === comment.id) === -1
|
||||||
|
})
|
||||||
|
state.comments[cardId].comments.push(...newComments)
|
||||||
}
|
}
|
||||||
state.comments[newComment.cardId].push(newComment)
|
|
||||||
},
|
},
|
||||||
updateComment(state, comment) {
|
updateComment(state, comment) {
|
||||||
const existingIndex = state.comments[comment.cardId].findIndex(_comment => _comment.id === comment.commentId)
|
const existingIndex = state.comments[comment.cardId].comments.findIndex(_comment => _comment.id === comment.commentId)
|
||||||
if (existingIndex !== -1) {
|
if (existingIndex !== -1) {
|
||||||
state.comments[comment.cardId][existingIndex].message = comment.comment
|
state.comments[comment.cardId].comments[existingIndex].message = comment.comment
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
deleteComment(state, comment) {
|
deleteComment(state, comment) {
|
||||||
const existingIndex = state.comments[comment.cardId].findIndex(_comment => _comment.id === comment.commentId)
|
const existingIndex = state.comments[comment.cardId].comments.findIndex(_comment => _comment.id === comment.commentId)
|
||||||
if (existingIndex !== -1) {
|
if (existingIndex !== -1) {
|
||||||
state.comments[comment.cardId].splice(existingIndex, 1)
|
state.comments[comment.cardId].comments.splice(existingIndex, 1)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
listComments({ commit }, card) {
|
async fetchComments({ commit }, { cardId, offset }) {
|
||||||
apiClient.listComments(card)
|
const comments = await apiClient.loadComments({
|
||||||
.then((comments) => {
|
cardId,
|
||||||
const commentsJson = xmlToTagList(comments)
|
limit: COMMENT_FETCH_LIMIT,
|
||||||
const returnObj = {
|
offset: offset || 0,
|
||||||
cardId: card.id,
|
})
|
||||||
comments: commentsJson,
|
|
||||||
}
|
if (comments.length < COMMENT_FETCH_LIMIT) {
|
||||||
commit('addComments', returnObj)
|
commit('endReached', { cardId })
|
||||||
})
|
return
|
||||||
|
}
|
||||||
|
commit('addComments', {
|
||||||
|
cardId,
|
||||||
|
comments,
|
||||||
|
})
|
||||||
},
|
},
|
||||||
createComment({ commit }, newComment) {
|
async fetchMore({ commit, dispatch, getters }, { cardId }) {
|
||||||
apiClient.createComment(newComment)
|
// fetch newer comments first
|
||||||
.then((newComment) => {
|
await dispatch('fetchComments', { cardId })
|
||||||
commit('createComment', newComment)
|
await dispatch('fetchComments', { cardId, offset: getters.getCommentsForCard(cardId).length })
|
||||||
})
|
|
||||||
},
|
},
|
||||||
deleteComment({ commit }, data) {
|
async createComment({ commit, dispatch }, { cardId, comment }) {
|
||||||
apiClient.deleteComment(data)
|
await apiClient.createComment({ cardId, comment })
|
||||||
.then((retVal) => {
|
await dispatch('fetchComments', { cardId })
|
||||||
commit('deleteComment', data)
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
updateComment({ commit }, data) {
|
async deleteComment({ commit }, data) {
|
||||||
apiClient.updateComment(data)
|
await apiClient.deleteComment(data)
|
||||||
.then((retVal) => {
|
commit('deleteComment', data)
|
||||||
commit('updateComment', data)
|
},
|
||||||
})
|
async updateComment({ commit }, data) {
|
||||||
|
await apiClient.updateComment(data)
|
||||||
|
commit('updateComment', data)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -110,12 +110,9 @@ export default new Vuex.Store({
|
|||||||
toggleShowArchived(state) {
|
toggleShowArchived(state) {
|
||||||
state.showArchived = !state.showArchived
|
state.showArchived = !state.showArchived
|
||||||
},
|
},
|
||||||
/**
|
/*
|
||||||
* Adds or replaces a board in the store.
|
* Adds or replaces a board in the store.
|
||||||
* Matches a board by it's id.
|
* Matches a board by it's id.
|
||||||
*
|
|
||||||
* @param state
|
|
||||||
* @param board
|
|
||||||
*/
|
*/
|
||||||
addBoard(state, board) {
|
addBoard(state, board) {
|
||||||
const indexExisting = state.boards.findIndex((b) => {
|
const indexExisting = state.boards.findIndex((b) => {
|
||||||
@@ -141,11 +138,8 @@ export default new Vuex.Store({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/*
|
||||||
* Removes the board from the store.
|
* Removes the board from the store.
|
||||||
*
|
|
||||||
* @param state
|
|
||||||
* @param board
|
|
||||||
*/
|
*/
|
||||||
removeBoard(state, board) {
|
removeBoard(state, board) {
|
||||||
state.boards = state.boards.filter((b) => {
|
state.boards = state.boards.filter((b) => {
|
||||||
@@ -207,7 +201,6 @@ export default new Vuex.Store({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
updateLabelFromCurrentBoard(state, newLabel) {
|
updateLabelFromCurrentBoard(state, newLabel) {
|
||||||
|
|
||||||
const labelToUpdate = state.currentBoard.labels.find((l) => {
|
const labelToUpdate = state.currentBoard.labels.find((l) => {
|
||||||
return newLabel.id === l.id
|
return newLabel.id === l.id
|
||||||
})
|
})
|
||||||
@@ -264,6 +257,7 @@ export default new Vuex.Store({
|
|||||||
toggleShowArchived({ commit }) {
|
toggleShowArchived({ commit }) {
|
||||||
commit('toggleShowArchived')
|
commit('toggleShowArchived')
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param commit
|
* @param commit
|
||||||
* @param state
|
* @param state
|
||||||
|
|||||||
Reference in New Issue
Block a user