fix(task): paginate task comments
Resolves https://github.com/go-vikunja/vikunja/issues/329 (cherry picked from commit 54994a1671e0093f71476dcff84144e0ac37d941)
This commit is contained in:
parent
7a839925ea
commit
7a1519da43
97
frontend/src/components/base/BasePagination.vue
Normal file
97
frontend/src/components/base/BasePagination.vue
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
<template>
|
||||||
|
<nav
|
||||||
|
v-if="totalPages > 1"
|
||||||
|
aria-label="pagination"
|
||||||
|
class="pagination is-centered p-4"
|
||||||
|
role="navigation"
|
||||||
|
>
|
||||||
|
<slot
|
||||||
|
name="previous"
|
||||||
|
:disabled="currentPage === 1"
|
||||||
|
>
|
||||||
|
{{ $t('misc.previous') }}
|
||||||
|
</slot>
|
||||||
|
<slot
|
||||||
|
name="next"
|
||||||
|
:disabled="currentPage === totalPages"
|
||||||
|
>
|
||||||
|
{{ $t('misc.next') }}
|
||||||
|
</slot>
|
||||||
|
<ul class="pagination-list">
|
||||||
|
<li
|
||||||
|
v-for="(p, i) in pages"
|
||||||
|
:key="`page-${i}`"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
v-if="p.isEllipsis"
|
||||||
|
class="pagination-ellipsis"
|
||||||
|
>…</span>
|
||||||
|
<slot
|
||||||
|
v-else
|
||||||
|
name="page-link"
|
||||||
|
:page="p"
|
||||||
|
:is-current="p.number === currentPage"
|
||||||
|
>
|
||||||
|
{{ p.number }}
|
||||||
|
</slot>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import {computed} from 'vue'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
totalPages: number,
|
||||||
|
currentPage: number
|
||||||
|
}>()
|
||||||
|
|
||||||
|
function createPagination(totalPages: number, currentPage: number) {
|
||||||
|
const pages = []
|
||||||
|
for (let i = 0; i < totalPages; i++) {
|
||||||
|
if (
|
||||||
|
i > 0 &&
|
||||||
|
(i + 1) < totalPages &&
|
||||||
|
((i + 1) > currentPage + 1 || (i + 1) < currentPage - 1)
|
||||||
|
) {
|
||||||
|
if (pages[i - 1] && !pages[i - 1].isEllipsis) {
|
||||||
|
pages.push({
|
||||||
|
number: 0,
|
||||||
|
isEllipsis: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
pages.push({
|
||||||
|
number: i + 1,
|
||||||
|
isEllipsis: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return pages
|
||||||
|
}
|
||||||
|
|
||||||
|
const pages = computed(() => createPagination(props.totalPages, props.currentPage))
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.pagination {
|
||||||
|
padding-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-previous,
|
||||||
|
.pagination-next {
|
||||||
|
&:not(:disabled):hover {
|
||||||
|
background: $scheme-main;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-list {
|
||||||
|
&, & li {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -1,89 +1,49 @@
|
|||||||
<template>
|
<template>
|
||||||
<nav
|
<BasePagination
|
||||||
v-if="totalPages > 1"
|
:total-pages="totalPages"
|
||||||
aria-label="pagination"
|
:current-page="currentPage"
|
||||||
class="pagination is-centered p-4"
|
|
||||||
role="navigation"
|
|
||||||
>
|
>
|
||||||
<RouterLink
|
<template #previous="{ disabled }">
|
||||||
:disabled="currentPage === 1 || undefined"
|
<RouterLink
|
||||||
:to="getRouteForPagination(currentPage - 1)"
|
:disabled="disabled || undefined"
|
||||||
class="pagination-previous"
|
:to="getRouteForPagination(currentPage - 1)"
|
||||||
>
|
class="pagination-previous"
|
||||||
{{ $t('misc.previous') }}
|
|
||||||
</RouterLink>
|
|
||||||
<RouterLink
|
|
||||||
:disabled="currentPage === totalPages || undefined"
|
|
||||||
:to="getRouteForPagination(currentPage + 1)"
|
|
||||||
class="pagination-next"
|
|
||||||
>
|
|
||||||
{{ $t('misc.next') }}
|
|
||||||
</RouterLink>
|
|
||||||
<ul class="pagination-list">
|
|
||||||
<li
|
|
||||||
v-for="(p, i) in pages"
|
|
||||||
:key="`page-${i}`"
|
|
||||||
>
|
>
|
||||||
<span
|
{{ $t('misc.previous') }}
|
||||||
v-if="p.isEllipsis"
|
</RouterLink>
|
||||||
class="pagination-ellipsis"
|
</template>
|
||||||
>…</span>
|
<template #next="{ disabled }">
|
||||||
<RouterLink
|
<RouterLink
|
||||||
v-else
|
:disabled="disabled || undefined"
|
||||||
class="pagination-link"
|
:to="getRouteForPagination(currentPage + 1)"
|
||||||
:aria-label="'Goto page ' + p.number"
|
class="pagination-next"
|
||||||
:class="{ 'is-current': p.number === currentPage }"
|
>
|
||||||
:to="getRouteForPagination(p.number)"
|
{{ $t('misc.next') }}
|
||||||
>
|
</RouterLink>
|
||||||
{{ p.number }}
|
</template>
|
||||||
</RouterLink>
|
<template #page-link="{ page, isCurrent }">
|
||||||
</li>
|
<RouterLink
|
||||||
</ul>
|
class="pagination-link"
|
||||||
</nav>
|
:aria-label="'Goto page ' + page.number"
|
||||||
|
:class="{ 'is-current': isCurrent }"
|
||||||
|
:to="getRouteForPagination(page.number)"
|
||||||
|
>
|
||||||
|
{{ page.number }}
|
||||||
|
</RouterLink>
|
||||||
|
</template>
|
||||||
|
</BasePagination>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import {computed} from 'vue'
|
import BasePagination from '@/components/base/BasePagination.vue'
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
withDefaults(defineProps<{
|
||||||
totalPages: number,
|
totalPages: number,
|
||||||
currentPage?: number
|
currentPage?: number
|
||||||
}>(), {
|
}>(), {
|
||||||
currentPage: 0,
|
currentPage: 0,
|
||||||
})
|
})
|
||||||
|
|
||||||
function createPagination(totalPages: number, currentPage: number) {
|
|
||||||
const pages = []
|
|
||||||
for (let i = 0; i < totalPages; i++) {
|
|
||||||
|
|
||||||
// Show ellipsis instead of all pages
|
|
||||||
if (
|
|
||||||
i > 0 && // Always at least the first page
|
|
||||||
(i + 1) < totalPages && // And the last page
|
|
||||||
(
|
|
||||||
// And the current with current + 1 and current - 1
|
|
||||||
(i + 1) > currentPage + 1 ||
|
|
||||||
(i + 1) < currentPage - 1
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
// Only add an ellipsis if the last page isn't already one
|
|
||||||
if (pages[i - 1] && !pages[i - 1].isEllipsis) {
|
|
||||||
pages.push({
|
|
||||||
number: 0,
|
|
||||||
isEllipsis: true,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
pages.push({
|
|
||||||
number: i + 1,
|
|
||||||
isEllipsis: false,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return pages
|
|
||||||
}
|
|
||||||
|
|
||||||
function getRouteForPagination(page = 1, type = null) {
|
function getRouteForPagination(page = 1, type = null) {
|
||||||
return {
|
return {
|
||||||
name: type,
|
name: type,
|
||||||
@ -95,20 +55,4 @@ function getRouteForPagination(page = 1, type = null) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
</script>
|
||||||
const pages = computed(() => createPagination(props.totalPages, props.currentPage))
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.pagination {
|
|
||||||
padding-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pagination-previous,
|
|
||||||
.pagination-next {
|
|
||||||
&:not(:disabled):hover {
|
|
||||||
background: $scheme-main;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
55
frontend/src/components/misc/PaginationEmit.vue
Normal file
55
frontend/src/components/misc/PaginationEmit.vue
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
<template>
|
||||||
|
<BasePagination
|
||||||
|
:total-pages="totalPages"
|
||||||
|
:current-page="currentPage"
|
||||||
|
>
|
||||||
|
<template #previous="{ disabled }">
|
||||||
|
<BaseButton
|
||||||
|
:disabled="disabled"
|
||||||
|
class="pagination-previous"
|
||||||
|
@click="changePage(currentPage - 1)"
|
||||||
|
>
|
||||||
|
{{ $t('misc.previous') }}
|
||||||
|
</BaseButton>
|
||||||
|
</template>
|
||||||
|
<template #next="{ disabled }">
|
||||||
|
<BaseButton
|
||||||
|
:disabled="disabled"
|
||||||
|
class="pagination-next"
|
||||||
|
@click="changePage(currentPage + 1)"
|
||||||
|
>
|
||||||
|
{{ $t('misc.next') }}
|
||||||
|
</BaseButton>
|
||||||
|
</template>
|
||||||
|
<template #page-link="{ page, isCurrent }">
|
||||||
|
<BaseButton
|
||||||
|
class="pagination-link"
|
||||||
|
:aria-label="'Goto page ' + page.number"
|
||||||
|
:class="{ 'is-current': isCurrent }"
|
||||||
|
@click="changePage(page.number)"
|
||||||
|
>
|
||||||
|
{{ page.number }}
|
||||||
|
</BaseButton>
|
||||||
|
</template>
|
||||||
|
</BasePagination>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import BasePagination from '@/components/base/BasePagination.vue'
|
||||||
|
import BaseButton from '@/components/base/BaseButton.vue'
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<{
|
||||||
|
totalPages: number,
|
||||||
|
currentPage: number
|
||||||
|
}>(), {
|
||||||
|
currentPage: 1,
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['pageChanged'])
|
||||||
|
|
||||||
|
function changePage(page: number) {
|
||||||
|
if (page >= 1 && page <= props.totalPages) {
|
||||||
|
emit('pageChanged', page)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
@ -1,7 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
v-if="enabled"
|
v-if="enabled"
|
||||||
class="content details"
|
ref="commentsRef"
|
||||||
|
class="content details comments-container"
|
||||||
>
|
>
|
||||||
<h3
|
<h3
|
||||||
v-if="canWrite || comments.length > 0"
|
v-if="canWrite || comments.length > 0"
|
||||||
@ -15,7 +16,7 @@
|
|||||||
<div class="comments">
|
<div class="comments">
|
||||||
<span
|
<span
|
||||||
v-if="taskCommentService.loading && saving === null && !creating"
|
v-if="taskCommentService.loading && saving === null && !creating"
|
||||||
class="is-inline-flex is-align-items-center"
|
class="is-flex is-align-items-center my-4 ml-2"
|
||||||
>
|
>
|
||||||
<span class="loader is-inline-block mr-2" />
|
<span class="loader is-inline-block mr-2" />
|
||||||
{{ $t('task.comment.loading') }}
|
{{ $t('task.comment.loading') }}
|
||||||
@ -107,6 +108,14 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<PaginationEmit
|
||||||
|
v-if="taskCommentService.totalPages > 1"
|
||||||
|
:total-pages="taskCommentService.totalPages"
|
||||||
|
:current-page="currentPage"
|
||||||
|
@pageChanged="changePage"
|
||||||
|
/>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-if="canWrite"
|
v-if="canWrite"
|
||||||
class="media comment d-print-none"
|
class="media comment d-print-none"
|
||||||
@ -160,6 +169,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
:enabled="showDeleteModal"
|
:enabled="showDeleteModal"
|
||||||
@close="showDeleteModal = false"
|
@close="showDeleteModal = false"
|
||||||
@ -185,6 +195,7 @@ import {useI18n} from 'vue-i18n'
|
|||||||
|
|
||||||
import CustomTransition from '@/components/misc/CustomTransition.vue'
|
import CustomTransition from '@/components/misc/CustomTransition.vue'
|
||||||
import Editor from '@/components/input/AsyncEditor'
|
import Editor from '@/components/input/AsyncEditor'
|
||||||
|
import PaginationEmit from '@/components/misc/PaginationEmit.vue'
|
||||||
|
|
||||||
import TaskCommentService from '@/services/taskComment'
|
import TaskCommentService from '@/services/taskComment'
|
||||||
import TaskCommentModel from '@/models/taskComment'
|
import TaskCommentModel from '@/models/taskComment'
|
||||||
@ -242,6 +253,12 @@ const actions = computed(() => {
|
|||||||
])))
|
])))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const frontendUrl = computed(() => configStore.frontendUrl)
|
||||||
|
|
||||||
|
const currentPage = ref(1)
|
||||||
|
|
||||||
|
const commentsRef = ref<HTMLElement | null>(null)
|
||||||
|
|
||||||
async function attachmentUpload(files: File[] | FileList): (Promise<string[]>) {
|
async function attachmentUpload(files: File[] | FileList): (Promise<string[]>) {
|
||||||
|
|
||||||
const uploadPromises: Promise<string>[] = []
|
const uploadPromises: Promise<string>[] = []
|
||||||
@ -267,12 +284,21 @@ async function loadComments(taskId: ITask['id']) {
|
|||||||
newComment.taskId = taskId
|
newComment.taskId = taskId
|
||||||
commentEdit.taskId = taskId
|
commentEdit.taskId = taskId
|
||||||
commentToDelete.taskId = taskId
|
commentToDelete.taskId = taskId
|
||||||
comments.value = await taskCommentService.getAll({taskId})
|
comments.value = await taskCommentService.getAll({taskId}, {}, currentPage.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function changePage(page: number) {
|
||||||
|
commentsRef.value?.scrollIntoView({ behavior: 'smooth', block: 'start', inline: 'nearest' })
|
||||||
|
currentPage.value = page
|
||||||
|
await loadComments(props.taskId)
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.taskId,
|
() => props.taskId,
|
||||||
loadComments,
|
() => {
|
||||||
|
currentPage.value = 1 // Reset to first page when task changes
|
||||||
|
loadComments(props.taskId)
|
||||||
|
},
|
||||||
{immediate: true},
|
{immediate: true},
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -404,4 +430,8 @@ async function deleteComment(commentToDelete: ITaskComment) {
|
|||||||
.media-content {
|
.media-content {
|
||||||
width: calc(100% - 48px - 2rem);
|
width: calc(100% - 48px - 2rem);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.comments-container {
|
||||||
|
scroll-margin-top: 4rem;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
Loading…
x
Reference in New Issue
Block a user