feat(comments): add sort order toggle for task comments
Allow users to switch between oldest-first and newest-first comment ordering via a toggle button in the comments heading. The preference is persisted as a frontend setting (commentSortOrder) and passed to the API as the order_by query parameter so pagination works correctly. When all comments fit on a single page, toggling reverses them client-side to avoid an extra API call. New comments are inserted at the correct position based on the active sort order, and in newest-first mode the view scrolls to the top after adding a comment. Link-share users fall back to a local (non-persisted) sort order since they cannot save user settings.
This commit is contained in:
parent
c12bbbe93e
commit
6555595a5c
|
|
@ -5,8 +5,10 @@ import {
|
|||
faAnglesUp,
|
||||
faArchive,
|
||||
faArrowLeft,
|
||||
faArrowDownShortWide,
|
||||
faArrowUpFromBracket,
|
||||
faArrowUpRightFromSquare,
|
||||
faArrowUpShortWide,
|
||||
faBold,
|
||||
faItalic,
|
||||
faStrikethrough,
|
||||
|
|
@ -183,7 +185,9 @@ library.add(faTimesCircle)
|
|||
library.add(faTrashAlt)
|
||||
library.add(faUser)
|
||||
library.add(faUsers)
|
||||
library.add(faArrowDownShortWide)
|
||||
library.add(faArrowUpFromBracket)
|
||||
library.add(faArrowUpShortWide)
|
||||
library.add(faX)
|
||||
library.add(faAnglesUp)
|
||||
library.add(faBolt)
|
||||
|
|
|
|||
|
|
@ -6,12 +6,23 @@
|
|||
>
|
||||
<h3
|
||||
v-if="canWrite || comments.length > 0"
|
||||
class="comments-heading"
|
||||
:class="{'d-print-none': comments.length === 0}"
|
||||
>
|
||||
<span class="icon is-grey">
|
||||
<Icon :icon="['far', 'comments']" />
|
||||
<span>
|
||||
<span class="icon is-grey">
|
||||
<Icon :icon="['far', 'comments']" />
|
||||
</span>
|
||||
{{ $t('task.comment.title') }}
|
||||
</span>
|
||||
{{ $t('task.comment.title') }}
|
||||
<BaseButton
|
||||
v-if="comments.length > 0"
|
||||
class="comment-sort-button"
|
||||
@click="toggleSortOrder"
|
||||
>
|
||||
<Icon :icon="commentSortOrder === 'asc' ? 'arrow-down-short-wide' : 'arrow-up-short-wide'" />
|
||||
{{ commentSortOrder === 'asc' ? $t('task.comment.sortOldestFirst') : $t('task.comment.sortNewestFirst') }}
|
||||
</BaseButton>
|
||||
</h3>
|
||||
<div class="comments">
|
||||
<span
|
||||
|
|
@ -214,6 +225,7 @@
|
|||
import {ref, reactive, computed, shallowReactive, watch} from 'vue'
|
||||
import {useI18n} from 'vue-i18n'
|
||||
|
||||
import BaseButton from '@/components/base/BaseButton.vue'
|
||||
import CustomTransition from '@/components/misc/CustomTransition.vue'
|
||||
import Editor from '@/components/input/AsyncEditor'
|
||||
import PaginationEmit from '@/components/misc/PaginationEmit.vue'
|
||||
|
|
@ -250,6 +262,9 @@ const {t} = useI18n({useScope: 'global'})
|
|||
const configStore = useConfigStore()
|
||||
const authStore = useAuthStore()
|
||||
|
||||
const localSortOrder = ref<'asc' | 'desc' | null>(null)
|
||||
const commentSortOrder = computed(() => localSortOrder.value ?? authStore.settings.frontendSettings.commentSortOrder ?? 'asc')
|
||||
|
||||
const comments = ref<ITaskComment[]>([])
|
||||
|
||||
const showDeleteModal = ref(false)
|
||||
|
|
@ -338,14 +353,14 @@ async function loadComments(taskId: ITask['id']) {
|
|||
commentEdit.taskId = taskId
|
||||
commentToDelete.taskId = taskId
|
||||
|
||||
if (typeof props.initialComments !== 'undefined' && currentPage.value === 1) {
|
||||
if (commentSortOrder.value === 'asc' && typeof props.initialComments !== 'undefined' && currentPage.value === 1) {
|
||||
if (props.initialComments.length < configStore.maxItemsPerPage) {
|
||||
comments.value = props.initialComments
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
comments.value = await taskCommentService.getAll({taskId}, {}, currentPage.value)
|
||||
comments.value = await taskCommentService.getAll({taskId}, {order_by: commentSortOrder.value}, currentPage.value)
|
||||
}
|
||||
|
||||
async function changePage(page: number) {
|
||||
|
|
@ -354,6 +369,30 @@ async function changePage(page: number) {
|
|||
await loadComments(props.taskId)
|
||||
}
|
||||
|
||||
async function toggleSortOrder() {
|
||||
const newOrder = commentSortOrder.value === 'asc' ? 'desc' : 'asc'
|
||||
if (!authStore.isLinkShareAuth) {
|
||||
await authStore.saveUserSettings({
|
||||
settings: {
|
||||
...authStore.settings,
|
||||
frontendSettings: {
|
||||
...authStore.settings.frontendSettings,
|
||||
commentSortOrder: newOrder,
|
||||
},
|
||||
},
|
||||
showMessage: false,
|
||||
})
|
||||
} else {
|
||||
localSortOrder.value = newOrder
|
||||
}
|
||||
if (taskCommentService.totalPages > 1) {
|
||||
currentPage.value = 1
|
||||
await loadComments(props.taskId)
|
||||
} else {
|
||||
comments.value.reverse()
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => [props.taskId, props.initialComments],
|
||||
() => {
|
||||
|
|
@ -378,12 +417,24 @@ async function addComment() {
|
|||
newComment.taskId = props.taskId
|
||||
newComment.comment = newCommentText.value
|
||||
const comment = await taskCommentService.create(newComment)
|
||||
comments.value.push(comment)
|
||||
|
||||
if (commentSortOrder.value === 'desc' && currentPage.value > 1) {
|
||||
currentPage.value = 1
|
||||
await loadComments(props.taskId)
|
||||
} else if (commentSortOrder.value === 'desc') {
|
||||
comments.value.unshift(comment)
|
||||
} else {
|
||||
comments.value.push(comment)
|
||||
}
|
||||
newCommentText.value = ''
|
||||
|
||||
// Ensure draft is cleared from localStorage
|
||||
clearEditorDraft(commentStorageKey.value)
|
||||
|
||||
if (commentSortOrder.value === 'desc') {
|
||||
commentsRef.value?.scrollIntoView({behavior: 'smooth', block: 'start', inline: 'nearest'})
|
||||
}
|
||||
|
||||
success({message: t('task.comment.addedSuccess')})
|
||||
} finally {
|
||||
creating.value = false
|
||||
|
|
@ -510,6 +561,25 @@ function getCommentUrl(commentId: string) {
|
|||
inline-size: calc(100% - 48px - 2rem);
|
||||
}
|
||||
|
||||
.comments-heading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.comment-sort-button {
|
||||
font-size: .75rem;
|
||||
font-weight: normal;
|
||||
color: var(--grey-500);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: .25rem;
|
||||
|
||||
&:hover {
|
||||
color: var(--grey-700);
|
||||
}
|
||||
}
|
||||
|
||||
.comments-container {
|
||||
scroll-margin-block-start: 4rem;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -928,7 +928,9 @@
|
|||
"deleteText1": "Are you sure you want to delete this comment?",
|
||||
"deleteSuccess": "The comment was deleted successfully.",
|
||||
"addedSuccess": "The comment was added successfully.",
|
||||
"permalink": "Copy permalink to this comment"
|
||||
"permalink": "Copy permalink to this comment",
|
||||
"sortNewestFirst": "Newest first",
|
||||
"sortOldestFirst": "Oldest first"
|
||||
},
|
||||
"mention": {
|
||||
"noUsersFound": "No users found"
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ export interface IFrontendSettings {
|
|||
backgroundBrightness: number | null
|
||||
alwaysShowBucketTaskCount: boolean
|
||||
sidebarWidth: number | null
|
||||
commentSortOrder: 'asc' | 'desc'
|
||||
}
|
||||
|
||||
export interface IExtraSettingsLink {
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ export default class UserSettingsModel extends AbstractModel<IUserSettings> impl
|
|||
defaultTaskRelationType: RELATION_KIND.RELATED,
|
||||
alwaysShowBucketTaskCount: false,
|
||||
sidebarWidth: null,
|
||||
commentSortOrder: 'asc',
|
||||
}
|
||||
extraSettingsLinks = {}
|
||||
|
||||
|
|
|
|||
|
|
@ -139,6 +139,7 @@ export const useAuthStore = defineStore('auth', () => {
|
|||
defaultTaskRelationType: RELATION_KIND.RELATED,
|
||||
backgroundBrightness: 100,
|
||||
sidebarWidth: null,
|
||||
commentSortOrder: 'asc',
|
||||
...newSettings.frontendSettings,
|
||||
},
|
||||
})
|
||||
|
|
|
|||
Loading…
Reference in New Issue