diff --git a/frontend/src/components/misc/Icon.ts b/frontend/src/components/misc/Icon.ts index db333c0dd..8a5cab738 100644 --- a/frontend/src/components/misc/Icon.ts +++ b/frontend/src/components/misc/Icon.ts @@ -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) diff --git a/frontend/src/components/tasks/partials/Comments.vue b/frontend/src/components/tasks/partials/Comments.vue index 2d25e0df4..336092f34 100644 --- a/frontend/src/components/tasks/partials/Comments.vue +++ b/frontend/src/components/tasks/partials/Comments.vue @@ -6,12 +6,23 @@ >

- - + + + + + {{ $t('task.comment.title') }} - {{ $t('task.comment.title') }} + + + {{ commentSortOrder === 'asc' ? $t('task.comment.sortOldestFirst') : $t('task.comment.sortNewestFirst') }} +

(null) +const commentSortOrder = computed(() => localSortOrder.value ?? authStore.settings.frontendSettings.commentSortOrder ?? 'asc') + const comments = ref([]) 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; } diff --git a/frontend/src/i18n/lang/en.json b/frontend/src/i18n/lang/en.json index b8390c31f..93d63561c 100644 --- a/frontend/src/i18n/lang/en.json +++ b/frontend/src/i18n/lang/en.json @@ -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" diff --git a/frontend/src/modelTypes/IUserSettings.ts b/frontend/src/modelTypes/IUserSettings.ts index d66412a57..8edbd438f 100644 --- a/frontend/src/modelTypes/IUserSettings.ts +++ b/frontend/src/modelTypes/IUserSettings.ts @@ -24,6 +24,7 @@ export interface IFrontendSettings { backgroundBrightness: number | null alwaysShowBucketTaskCount: boolean sidebarWidth: number | null + commentSortOrder: 'asc' | 'desc' } export interface IExtraSettingsLink { diff --git a/frontend/src/models/userSettings.ts b/frontend/src/models/userSettings.ts index d79d0b852..7b61a50ed 100644 --- a/frontend/src/models/userSettings.ts +++ b/frontend/src/models/userSettings.ts @@ -32,6 +32,7 @@ export default class UserSettingsModel extends AbstractModel impl defaultTaskRelationType: RELATION_KIND.RELATED, alwaysShowBucketTaskCount: false, sidebarWidth: null, + commentSortOrder: 'asc', } extraSettingsLinks = {} diff --git a/frontend/src/stores/auth.ts b/frontend/src/stores/auth.ts index 21c3b9f63..a947b2ab1 100644 --- a/frontend/src/stores/auth.ts +++ b/frontend/src/stores/auth.ts @@ -139,6 +139,7 @@ export const useAuthStore = defineStore('auth', () => { defaultTaskRelationType: RELATION_KIND.RELATED, backgroundBrightness: 100, sidebarWidth: null, + commentSortOrder: 'asc', ...newSettings.frontendSettings, }, })