feat: add comment count to tasks (#1771)

This commit is contained in:
Mithilesh Gupta
2025-11-12 03:30:05 +05:30
committed by GitHub
parent e371ee6f12
commit 01a84dd2d5
13 changed files with 172 additions and 6 deletions

View File

@@ -149,8 +149,8 @@ const {
() => props.viewId,
{position: 'asc'},
() => projectId.value === -1
? null
: 'subtasks',
? 'comment_count'
: ['subtasks', 'comment_count'],
)
const taskPositionService = ref(new TaskPositionService())

View File

@@ -41,6 +41,9 @@
<FancyCheckbox v-model="activeColumns.assignees">
{{ $t('task.attributes.assignees') }}
</FancyCheckbox>
<FancyCheckbox v-model="activeColumns.commentCount">
{{ $t('task.attributes.commentCount') }}
</FancyCheckbox>
<FancyCheckbox v-model="activeColumns.dueDate">
{{ $t('task.attributes.dueDate') }}
</FancyCheckbox>
@@ -132,6 +135,9 @@
@click="sort('due_date', $event)"
/>
</th>
<th v-if="activeColumns.commentCount">
{{ $t('task.attributes.commentCount') }}
</th>
<th v-if="activeColumns.startDate">
{{ $t('task.attributes.startDate') }}
<Sort
@@ -228,6 +234,15 @@
v-if="activeColumns.dueDate"
:date="t.dueDate"
/>
<td v-if="activeColumns.commentCount">
<span
v-if="t.commentCount && t.commentCount > 0"
class="comment-badge"
>
<Icon icon="comment" />
{{ t.commentCount }}
</span>
</td>
<DateTableCell
v-if="activeColumns.startDate"
:date="t.startDate"
@@ -320,6 +335,7 @@ const ACTIVE_COLUMNS_DEFAULT = {
updated: false,
createdBy: false,
doneAt: false,
commentCount: false,
}
const SORT_BY_DEFAULT: SortBy = {
@@ -329,7 +345,12 @@ const SORT_BY_DEFAULT: SortBy = {
const activeColumns = useStorage('tableViewColumns', {...ACTIVE_COLUMNS_DEFAULT})
const sortBy = useStorage<SortBy>('tableViewSortBy', {...SORT_BY_DEFAULT})
const taskList = useTaskList(() => props.projectId, () => props.viewId, sortBy.value)
const taskList = useTaskList(
() => props.projectId,
() => props.viewId,
sortBy.value,
() => 'comment_count',
)
const {
loading,

View File

@@ -86,6 +86,14 @@
>
<Icon icon="paperclip" />
</span>
<span
v-if="task.commentCount && task.commentCount > 0"
v-tooltip="$t('task.attributes.comment', task.commentCount)"
class="project-task-icon comment-count-icon"
>
<Icon :icon="['far', 'comments']" />
<span class="comment-count-badge">{{ task.commentCount }}</span>
</span>
<span
v-if="!isEditorContentEmpty(task.description)"
class="icon"
@@ -396,4 +404,22 @@ $task-background: var(--white);
inline-size: 100%;
block-size: 0.5rem;
}
.comment-count-icon {
display: inline-flex;
align-items: center;
gap: 0.25rem;
font-size: 0.875rem;
color: var(--grey-500);
background: var(--grey-100);
border-radius: $radius;
padding: 0.25rem;
margin-inline-end: .25rem;
.comment-count-badge {
font-weight: 600;
font-size: 0.75rem;
line-height: 1;
}
}
</style>

View File

@@ -120,6 +120,14 @@
>
<Icon icon="history" />
</span>
<span
v-if="task.commentCount && task.commentCount > 0"
class="project-task-icon comment-count-icon"
:title="`${task.commentCount} ${task.commentCount === 1 ? 'comment' : 'comments'}`"
>
<Icon :icon="['far', 'comments']" />
<span class="comment-count-badge">{{ task.commentCount }}</span>
</span>
</span>
<ChecklistSummary :task="task" />
@@ -565,4 +573,22 @@ defineExpose({
border: 1px solid var(--grey-200);
}
}
.comment-count-icon {
display: inline-flex;
align-items: center;
gap: 0.25rem;
font-size: 0.875rem;
color: var(--grey-500);
.comment-count-badge {
font-weight: 600;
font-size: 0.75rem;
line-height: 1;
}
&:hover {
color: var(--primary);
}
}
</style>

View File

@@ -862,6 +862,8 @@
"relatedTasks": "Related Tasks",
"reminders": "Reminders",
"repeat": "Repeat",
"comment": "{count} comment | {count} comments",
"commentCount": "Number of comments",
"startDate": "Start Date",
"title": "Title",
"updated": "Updated",

View File

@@ -49,6 +49,7 @@ export interface ITask extends IAbstract {
reactions: IReactionPerEntity
comments: ITaskComment[]
commentCount?: number
createdBy: IUser
created: Date

View File

@@ -4,7 +4,7 @@ import TaskModel from '@/models/task'
import type {ITask} from '@/modelTypes/ITask'
import BucketModel from '@/models/bucket'
export type ExpandTaskFilterParam = 'subtasks' | 'buckets' | 'reactions' | null
export type ExpandTaskFilterParam = 'subtasks' | 'buckets' | 'reactions' | 'comment_count' | null
export interface TaskFilterParams {
sort_by: ('start_date' | 'end_date' | 'due_date' | 'done' | 'id' | 'position' | 'title')[],

View File

@@ -262,6 +262,7 @@ export const useKanbanStore = defineStore('kanban', () => {
try {
const newBuckets = await taskCollectionService.getAll({projectId, viewId}, {
...params,
expand: 'comment_count',
per_page: TASKS_PER_BUCKET,
})
setBuckets(newBuckets)
@@ -300,6 +301,7 @@ export const useKanbanStore = defineStore('kanban', () => {
params.filter = `${params.filter === '' ? '' : params.filter + ' && '}bucket_id = ${bucketId}`
params.filter_timezone = authStore.settings.timezone
params.per_page = TASKS_PER_BUCKET
params.expand = 'comment_count'
const taskService = new TaskCollectionService()
try {

View File

@@ -192,6 +192,7 @@ async function loadPendingTasks(from: Date|string, to: Date|string) {
filter: 'done = false',
filter_include_nulls: props.showNulls,
s: '',
expand: 'comment_count',
}
if (!showAll.value) {

View File

@@ -0,0 +1,36 @@
// Vikunja is a to-do list application to facilitate your life.
// Copyright 2018-present Vikunja and contributors. All rights reserved.
//
// 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 <https://www.gnu.org/licenses/>.
package migration
import (
"src.techknowlogick.com/xormigrate"
"xorm.io/xorm"
)
func init() {
migrations = append(migrations, &xormigrate.Migration{
ID: "20251108154913",
Description: "Add index on task_comments.task_id for better query performance",
Migrate: func(tx *xorm.Engine) error {
_, err := tx.Exec("CREATE INDEX IF NOT EXISTS IDX_task_comments_task_id ON task_comments (task_id)")
return err
},
Rollback: func(tx *xorm.Engine) error {
return nil
},
})
}

View File

@@ -70,6 +70,7 @@ const TaskCollectionExpandSubtasks TaskCollectionExpandable = `subtasks`
const TaskCollectionExpandBuckets TaskCollectionExpandable = `buckets`
const TaskCollectionExpandReactions TaskCollectionExpandable = `reactions`
const TaskCollectionExpandComments TaskCollectionExpandable = `comments`
const TaskCollectionExpandCommentCount TaskCollectionExpandable = `comment_count`
// Validate validates if the TaskCollectionExpandable value is valid.
func (t TaskCollectionExpandable) Validate() error {
@@ -82,9 +83,11 @@ func (t TaskCollectionExpandable) Validate() error {
return nil
case TaskCollectionExpandComments:
return nil
case TaskCollectionExpandCommentCount:
return nil
}
return InvalidFieldErrorWithMessage([]string{"expand"}, "Expand must be one of the following values: subtasks, buckets, reactions")
return InvalidFieldErrorWithMessage([]string{"expand"}, "Expand must be one of the following values: subtasks, buckets, reactions, comments, comment_count")
}
func validateTaskField(fieldName string) error {
@@ -286,6 +289,7 @@ func (tf *TaskCollection) ReadAll(s *xorm.Session, a web.Auth, search string, pa
tc.ProjectViewID = tf.ProjectViewID
tc.ProjectID = tf.ProjectID
tc.isSavedFilter = true
tc.Expand = tf.Expand
if tf.Filter != "" {
if tc.Filter != "" {

View File

@@ -34,7 +34,7 @@ type TaskComment struct {
Comment string `xorm:"text not null" json:"comment" valid:"dbtext,required"`
AuthorID int64 `xorm:"not null" json:"-"`
Author *user.User `xorm:"-" json:"author"`
TaskID int64 `xorm:"not null" json:"-" param:"task"`
TaskID int64 `xorm:"index not null" json:"-" param:"task"`
Reactions ReactionMap `xorm:"-" json:"reactions"`
@@ -274,6 +274,43 @@ func addCommentsToTasks(s *xorm.Session, taskIDs []int64, taskMap map[int64]*Tas
return nil
}
func addCommentCountToTasks(s *xorm.Session, taskIDs []int64, taskMap map[int64]*Task) error {
if len(taskIDs) == 0 {
return nil
}
zero := int64(0)
for _, taskID := range taskIDs {
if task, ok := taskMap[taskID]; ok {
task.CommentCount = &zero
}
}
type CommentCount struct {
TaskID int64 `xorm:"task_id"`
Count int64 `xorm:"count"`
}
counts := []CommentCount{}
if err := s.
Select("task_id, COUNT(*) as count").
Where(builder.In("task_id", taskIDs)).
GroupBy("task_id").
Table("task_comments").
Find(&counts); err != nil {
return err
}
for _, c := range counts {
if task, ok := taskMap[c.TaskID]; ok {
task.CommentCount = &c.Count
}
}
return nil
}
func getAllCommentsForTasksWithoutPermissionCheck(s *xorm.Session, taskIDs []int64, search string, page int, perPage int) (result []*TaskComment, resultCount int, numberOfTotalItems int64, err error) {
// Because we can't extend the type in general, we need to do this here.
// Not a good solution, but saves performance.

View File

@@ -125,6 +125,9 @@ type Task struct {
// All comments of this task. Only present when fetching tasks with the `expand` parameter set to `comments`.
Comments []*TaskComment `xorm:"-" json:"comments,omitempty"`
// Comment count of this task. Only present when fetching tasks with the `expand` parameter set to `comment_count`.
CommentCount *int64 `xorm:"-" json:"comment_count,omitempty"`
// Behaves exactly the same as with the TaskCollection.Expand parameter
Expand []TaskCollectionExpandable `xorm:"-" json:"-" query:"expand"`
@@ -587,6 +590,8 @@ func addBucketsToTasks(s *xorm.Session, a web.Auth, taskIDs []int64, taskMap map
// This function takes a map with pointers and returns a slice with pointers to tasks
// It adds more stuff like assignees/labels/etc to a bunch of tasks
//
//nolint:gocyclo
func addMoreInfoToTasks(s *xorm.Session, taskMap map[int64]*Task, a web.Auth, view *ProjectView, expand []TaskCollectionExpandable) (err error) {
// No need to iterate over users and stuff if the project doesn't have tasks
@@ -679,6 +684,11 @@ func addMoreInfoToTasks(s *xorm.Session, taskMap map[int64]*Task, a web.Auth, vi
if err != nil {
return err
}
case TaskCollectionExpandCommentCount:
err = addCommentCountToTasks(s, taskIDs, taskMap)
if err != nil {
return err
}
}
expanded[expandable] = true
}