Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 27 additions & 8 deletions services/src/main/java/io/meeds/task/mcp/TaskMcpTool.java
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
import java.util.function.BiFunction;
import java.util.function.Supplier;
import java.util.regex.Pattern;
import java.util.stream.Stream;

import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
Expand Down Expand Up @@ -74,6 +75,7 @@
import org.exoplatform.task.domain.Priority;
import org.exoplatform.task.dto.ChangeLogEntry;
import org.exoplatform.task.dto.CommentDto;
import org.exoplatform.task.dto.CommentsSummaryDto;
import org.exoplatform.task.dto.LabelDto;
import org.exoplatform.task.dto.ProjectDto;
import org.exoplatform.task.dto.StatusDto;
Expand Down Expand Up @@ -1744,7 +1746,9 @@ private long countTasks(TaskQuery taskQuery) {
* @param includeDescription whether each item carries its description as
* cut plain text
* @return their list items, each overdue one with its days overdue, each
* with its changes since that date when one is given
* with its changes since that date when one is given, each
* commented one with its comments count and latest comment, all read
* in two queries, whatever the number of tasks
*/
private List<TaskListItemModel> toTaskListItems(List<TaskDto> tasks,
Date since,
Expand All @@ -1755,8 +1759,17 @@ private List<TaskListItemModel> toTaskListItems(List<TaskDto> tasks,
}
LocalDate today = getToday(zone);
Identity aclIdentity = getCurrentUserAclIdentity();
Map<Long, CommentsSummaryDto> commentsSummaries = commentService.getCommentsSummaries(tasks.stream()
.map(TaskDto::getId)
.toList());
return tasks.stream().map(task -> {
TaskListItemModel item = toTaskListItem(task, since, zone, today, aclIdentity);
CommentsSummaryDto commentsSummary = commentsSummaries.get(task.getId());
if (commentsSummary != null) {
item.setCommentsCount((int) commentsSummary.getCount());
item.setLastComment(formatDate(commentsSummary.getLastCommentDate()),
getTaskAssignee(commentsSummary.getLastCommentAuthor()));
}
if (includeDescription) {
// the stored HTML read as text, without the rendering that resolves links and mentions
item.setPlainTextDescription(TaskDescriptionText.toPlainText(task.getDescription()));
Expand All @@ -1767,9 +1780,10 @@ private List<TaskListItemModel> toTaskListItems(List<TaskDto> tasks,

/**
* Maps a task to its list item: what tells it apart on its board, as a
* search result, with its assignee, due date, creation date, priority and
* comments count. It reads neither the description, nor the coworkers, nor the last update
* date that the full model holds.
* search result, with its assignee, due date, creation date and priority,
* without its comments, which {@link #toTaskListItems} reads for the whole
* page. It reads neither the description, nor the coworkers, nor the last
* update date that the full model holds.
*
* @param task a task
* @param since the date the changes are listed from, null for none
Expand Down Expand Up @@ -1802,7 +1816,7 @@ private TaskListItemModel toTaskListItem(TaskDto task, Date since, ZoneId zone,
formatDate(task.getCreatedTime()),
getDaysOverdue(task, zone, today),
task.getPriority(),
commentService.countComments(task.getId()),
0,
changes);
}

Expand Down Expand Up @@ -1953,7 +1967,8 @@ private TaskWithChangeLogModel toTaskWithChangeLogModel(TaskDto task, Instant si
* @param logs its change log entries, as the task service returns them
* @param since only the entries and comments after this instant, null for
* all
* @return its change log and comments, oldest first when it has comments
* @return its change log and comments, replies included, oldest first when
* it has comments
*/
private List<TaskChangeLog> getChangeLogs(TaskDto task, List<ChangeLogEntry> logs, Instant since) {
List<TaskChangeLog> taskLogs = logs.stream()
Expand All @@ -1964,8 +1979,12 @@ private List<TaskChangeLog> getChangeLogs(TaskDto task, List<ChangeLogEntry> log
.toList();
int commentsCount = commentService.countComments(task.getId());
if (commentsCount > 0) {
List<TaskChangeLog> commentTaskLogs = commentService.getComments(task.getId(), 0, commentsCount)
// the count bounds the top-level comments, each read with its replies
List<TaskChangeLog> commentTaskLogs = commentService.getCommentsWithSubs(task.getId(), 0, commentsCount)
.stream()
.flatMap(c -> Stream.concat(Stream.of(c),
CollectionUtils.emptyIfNull(c.getSubComments())
.stream()))
.filter(c -> since == null
|| Instant.ofEpochMilli(c.getCreatedTime().getTime())
.isAfter(since))
Expand Down Expand Up @@ -2105,7 +2124,7 @@ private TaskModel toTaskModel(TaskDto task, ZoneId zone, LocalDate today) {
Locale locale = getCurrentUserLocale();
StatusDto status = task.getStatus();
ProjectDto project = status != null && status.getProject() != null ? status.getProject() : null;
int commentsCount = commentService.countComments(task.getId());
int commentsCount = commentService.countCommentsWithSubs(task.getId());
String assignee = task.getAssignee();
Space space = project == null ? null : getProjectSpace(project);
Long projectId = project == null ? null : project.getId();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@
import lombok.NoArgsConstructor;

/**
* The assignee of a task in a list: who it is, without the profile
* get_user_by_username returns.
* A user of a task in a list, its assignee or the author of its latest
* comment: who it is, without the profile get_user_by_username returns.
*/
@Data
@NoArgsConstructor
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,9 @@
* A task in a list: what tells the card apart on its board, as a search
* result does, with its assignee, due date, creation date, priority and
* comments count, without its description, coworkers or other dates, which
* get_task_by_id returns. The creation date tells the tasks created since a
* date apart. On request, the description as cut plain text.
* get_task_by_id returns. The creation date tells apart the tasks created
* since a date; the date of the latest comment tells apart those nobody has
* commented on since. On request, the description as cut plain text.
*/
@Data
@NoArgsConstructor
Expand Down Expand Up @@ -84,9 +85,25 @@ public class TaskListItemModel {

private Priority priority;

/**
* The number of its comments, replies included.
*/
@JsonProperty("comments_count")
private int commentsCount;

/**
* When its latest comment, a reply or not, was written, formatted as
* created_date; none for a task without comments.
*/
@JsonProperty("last_comment_date")
private String lastCommentDate;

/**
* Who wrote its latest comment; none for a task without comments.
*/
@JsonProperty("last_comment_author")
private TaskAssigneeModel lastCommentAuthor;

/**
* The changes since the date of a listing with {@code changed_since} or
* {@code completed_since}, oldest first; none otherwise.
Expand All @@ -109,8 +126,10 @@ public class TaskListItemModel {
private Boolean descriptionTruncated;

/**
* A list item without its description, which
* {@link #setPlainTextDescription(String)} adds on request.
* A list item without its latest comment, which
* {@link #setLastComment(String, TaskAssigneeModel)} adds, nor its
* description, which {@link #setPlainTextDescription(String)} adds on
* request.
*
* @param id the task
* @param title its title
Expand All @@ -125,7 +144,7 @@ public class TaskListItemModel {
* @param createdDate its creation date
* @param daysOverdue its days overdue, null when not overdue
* @param priority its priority
* @param commentsCount its comments count
* @param commentsCount its comments count, replies included
* @param changes its changes since a date, null for none
*/
public TaskListItemModel(long id, // NOSONAR the item's fields, each one its own
Expand Down Expand Up @@ -157,6 +176,8 @@ public TaskListItemModel(long id, // NOSONAR the item's fields, each one its own
daysOverdue,
priority,
commentsCount,
null,
null,
changes,
null,
null);
Expand All @@ -175,4 +196,15 @@ public void setPlainTextDescription(String plainText) {
this.descriptionTruncated = cut != null && cut.length() < plainText.length() ? Boolean.TRUE : null;
}

/**
* Sets when and by whom its latest comment was written.
*
* @param date the date of its latest comment, formatted as created_date
* @param author its author
*/
public void setLastComment(String date, TaskAssigneeModel author) {
this.lastCommentDate = date;
this.lastCommentAuthor = author;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,15 @@
*/
package org.exoplatform.task.dao;

import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.exoplatform.commons.api.persistence.GenericDAO;
import org.exoplatform.commons.utils.ListAccess;
import org.exoplatform.task.domain.Comment;
import org.exoplatform.task.dto.CommentsSummaryDto;

public interface CommentHandler extends GenericDAO<Comment, Long> {
ListAccess<Comment> findComments(long taskId);
Expand All @@ -40,4 +43,15 @@ public interface CommentHandler extends GenericDAO<Comment, Long> {
*/
Comment getLastComment(long taskId);

/**
* Summarizes the comments of several tasks in two queries, whatever their
* number.
*
* @param taskIds the tasks
* @return for each of them that has comments, the number of its comments,
* replies included, and its latest comment's date and author; the
* tasks without comments are absent
*/
Map<Long, CommentsSummaryDto> getCommentsSummaries(Collection<Long> taskIds);

}
Original file line number Diff line number Diff line change
Expand Up @@ -24,17 +24,38 @@
import org.exoplatform.commons.utils.ListAccess;
import org.exoplatform.task.dao.CommentHandler;
import org.exoplatform.task.domain.Comment;
import org.exoplatform.task.dto.CommentsSummaryDto;

import jakarta.persistence.NoResultException;
import jakarta.persistence.TypedQuery;

import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

public class CommentDAOImpl extends CommonJPADAO<Comment, Long> implements CommentHandler {

/**
* For each of the given tasks that has comments, the number of its
* comments, replies included, and the creation time of its latest one: one
* grouped scan, with no subquery run per row.
*/
private static final String COMMENTS_COUNTS_QUERY = "SELECT c.task.id, count(c), max(c.createdTime) FROM TaskComment c"
+ " WHERE c.task.id IN (:taskIds) GROUP BY c.task.id";

/**
* The comments of the given tasks created at one of the given times: the
* latest comment of each task, with the comments of another task created
* at the same time as one of them, which the caller leaves out.
*/
private static final String LATEST_COMMENTS_QUERY = "SELECT c.task.id, c.createdTime, c.author, c.id FROM TaskComment c"
+ " WHERE c.task.id IN (:taskIds) AND c.createdTime IN (:createdTimes)";

@Override
public void delete(Comment entity) {
List<Comment> comments = getSubComments(Collections.singletonList(entity));
Expand Down Expand Up @@ -109,4 +130,43 @@ public int countCommentsWithSubs(long taskId) {
}
}

/**
* Runs {@link #COMMENTS_COUNTS_QUERY}, then {@link #LATEST_COMMENTS_QUERY}
* for the authors of the latest comments, keeping for each task the comment
* created at its latest time, the one created last when several were.
*
* @param taskIds the tasks
* @return the summaries of those that have comments, by task id
*/
@Override
public Map<Long, CommentsSummaryDto> getCommentsSummaries(Collection<Long> taskIds) {
if (CollectionUtils.isEmpty(taskIds)) {
return Collections.emptyMap();
}
TypedQuery<Object[]> countsQuery = getEntityManager().createQuery(COMMENTS_COUNTS_QUERY, Object[].class);
countsQuery.setParameter("taskIds", taskIds);
Map<Long, CommentsSummaryDto> summaries = new LinkedHashMap<>();
for (Object[] row : countsQuery.getResultList()) {
long taskId = ((Number) row[0]).longValue();
summaries.put(taskId, new CommentsSummaryDto(taskId, ((Number) row[1]).longValue(), (Date) row[2], null, 0));
}
if (summaries.isEmpty()) {
return summaries;
}
TypedQuery<Object[]> latestQuery = getEntityManager().createQuery(LATEST_COMMENTS_QUERY, Object[].class);
latestQuery.setParameter("taskIds", summaries.keySet());
latestQuery.setParameter("createdTimes",
summaries.values().stream().map(CommentsSummaryDto::getLastCommentDate).distinct().toList());
for (Object[] row : latestQuery.getResultList()) {
CommentsSummaryDto summary = summaries.get(((Number) row[0]).longValue());
long commentId = ((Number) row[3]).longValue();
if (((Date) row[1]).getTime() == summary.getLastCommentDate().getTime()
&& (summary.getLastCommentAuthor() == null || commentId > summary.getLastCommentId())) {
summary.setLastCommentAuthor((String) row[2]);
summary.setLastCommentId(commentId);
}
}
return summaries;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/**
* This file is part of the Meeds project (https://meeds.io/).
*
* Copyright (C) 2026 Meeds Association contact@meeds.io
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.exoplatform.task.dto;

import java.util.Date;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
* The comments of a task at a glance: how many it has, replies included, and
* when and by whom the latest one, a reply or not, was written.
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class CommentsSummaryDto {

/** The task. */
private long taskId;

/** The number of its comments, replies included. */
private long count;

/** The creation date of its latest comment, a reply or not. */
private Date lastCommentDate;

/** The username of the author of its latest comment. */
private String lastCommentAuthor;

/**
* The id of its latest comment, which tells apart two comments created at
* the same time.
*/
private long lastCommentId;

}
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,12 @@
*/
package org.exoplatform.task.service;

import java.util.Collection;
import java.util.List;
import java.util.Map;

import org.exoplatform.task.dto.CommentDto;
import org.exoplatform.task.dto.CommentsSummaryDto;
import org.exoplatform.task.dto.TaskDto;
import org.exoplatform.task.exception.EntityNotFoundException;

Expand Down Expand Up @@ -61,4 +64,16 @@ public interface CommentService {
* @return null if no comments else last created comment on task
*/
CommentDto getLastComment(long taskId);

/**
* Summarizes the comments of several tasks in two queries, whatever their
* number.
*
* @param taskIds the tasks
* @return for each of them that has comments, the number of its comments,
* replies included, and its latest comment's date and author; the
* tasks without comments are absent
*/
Map<Long, CommentsSummaryDto> getCommentsSummaries(Collection<Long> taskIds);

}
Loading
Loading