Skip to content
Merged
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
89 changes: 80 additions & 9 deletions src/main/java/com/crowdin/client/tasks/TasksApi.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,7 @@
import com.crowdin.client.core.model.PatchRequest;
import com.crowdin.client.core.model.ResponseList;
import com.crowdin.client.core.model.ResponseObject;
import com.crowdin.client.tasks.model.AddTaskRequest;
import com.crowdin.client.tasks.model.AddTaskSettingsTemplateRequest;
import com.crowdin.client.tasks.model.Status;
import com.crowdin.client.tasks.model.Task;
import com.crowdin.client.tasks.model.TaskResponseList;
import com.crowdin.client.tasks.model.TaskResponseObject;
import com.crowdin.client.tasks.model.TaskSettingsTemplate;
import com.crowdin.client.tasks.model.TaskSettingsTemplateResponseList;
import com.crowdin.client.tasks.model.TaskSettingsTemplateResponseObject;
import com.crowdin.client.tasks.model.*;

import java.util.List;
import java.util.Map;
Expand Down Expand Up @@ -248,6 +240,85 @@ public ResponseObject<TaskSettingsTemplate> editTaskSettingsTemplate(Long projec
return ResponseObject.of(responseObject.getData());
}

/**
* @param projectId project identifier
* @param taskId task identifier
* @param limit maximum number of items to retrieve (default 25)
* @param offset starting offset in the collection (default 0)
* @return list of task comments
* @see <ul>
* <li><a href="https://support.crowdin.com/developer/api/v2/#tag/Tasks/operation/api.projects.tasks.comments.getMany" target="_blank"><b>API Documentation</b></a></li>
* <li><a href="https://support.crowdin.com/developer/enterprise/api/v2/#tag/Tasks/operation/api.projects.tasks.comments.getMany" target="_blank"><b>Enterprise API Documentation</b></a></li>
* </ul>
*/
public ResponseList<TaskComment> listTasksComments(Long projectId, Long taskId, Integer limit, Integer offset) throws HttpException, HttpBadRequestException {
Map<String, Optional<Object>> queryParams = HttpRequestConfig.buildUrlParams(
"limit", Optional.ofNullable(limit),
"offset", Optional.ofNullable(offset)
);
TaskCommentResponseList taskCommentResponseList = this.httpClient.get(this.url + "/projects/" + projectId + "/tasks/" + taskId + "/comments", new HttpRequestConfig(queryParams), TaskCommentResponseList.class);
return TaskCommentResponseList.to(taskCommentResponseList);
}

/**
* @param projectId project identifier
* @param taskId task identifier
* @param request request object
* @return newly created task comment
* @see <ul>
* <li><a href="https://support.crowdin.com/developer/api/v2/#tag/Tasks/operation/api.projects.tasks.comments.post" target="_blank"><b>API Documentation</b></a></li>
* <li><a href="https://support.crowdin.com/developer/enterprise/api/v2/#tag/Tasks/operation/api.projects.tasks.comments.post" target="_blank"><b>Enterprise API Documentation</b></a></li>
* </ul>
*/
public ResponseObject<TaskComment> addTaskComment(Long projectId, Long taskId, CreateTaskCommentRequest request) throws HttpException, HttpBadRequestException {
TaskCommentResponseObject taskCommentResponseObject = this.httpClient.post(this.url + "/projects/" + projectId + "/tasks/" + taskId + "/comments", request, new HttpRequestConfig(), TaskCommentResponseObject.class);
return ResponseObject.of(taskCommentResponseObject.getData());
}

/**
* @param projectId project identifier
* @param taskId task identifier
* @param commentId comment identifier
* @return task comment
* @see <ul>
* <li><a href="https://support.crowdin.com/developer/api/v2/#tag/Tasks/operation/api.projects.tasks.comments.get" target="_blank"><b>API Documentation</b></a></li>
* <li><a href="https://support.crowdin.com/developer/enterprise/api/v2/#tag/Tasks/operation/api.projects.tasks.comments.get" target="_blank"><b>Enterprise API Documentation</b></a></li>
* </ul>
*/
public ResponseObject<TaskComment> getTaskComment(Long projectId, Long taskId, Long commentId) throws HttpException, HttpBadRequestException {
TaskCommentResponseObject taskCommentResponseObject = this.httpClient.get(this.url + "/projects/" + projectId + "/tasks/" + taskId + "/comments/" + commentId, new HttpRequestConfig(), TaskCommentResponseObject.class);
return ResponseObject.of(taskCommentResponseObject.getData());
}

/**
* @param projectId project identifier
* @param taskId task identifier
* @param commentId task identifier
* @see <ul>
* <li><a href="https://support.crowdin.com/developer/api/v2/#tag/Tasks/operation/api.projects.tasks.comments.delete" target="_blank"><b>API Documentation</b></a></li>
* <li><a href="https://support.crowdin.com/developer/enterprise/api/v2/#tag/Tasks/operation/api.projects.tasks.comments.delete" target="_blank"><b>Enterprise API Documentation</b></a></li>
* </ul>
*/
public void deleteTaskComment(Long projectId, Long taskId, Long commentId) throws HttpException, HttpBadRequestException {
this.httpClient.delete(this.url + "/projects/" + projectId + "/tasks/" + taskId + "/comments/" + commentId, new HttpRequestConfig(), Void.class);
}

/**
* @param projectId project identifier
* @param taskId task identifier
* @param commentId task identifier
* @param request request object
* @return updated task comment
* @see <ul>
* <li><a href="https://support.crowdin.com/developer/api/v2/#tag/Tasks/operation/api.projects.tasks.comments.patch" target="_blank"><b>API Documentation</b></a></li>
* <li><a href="https://support.crowdin.com/developer/enterprise/api/v2/#tag/Tasks/operation/api.projects.tasks.comments.patch" target="_blank"><b>Enterprise API Documentation</b></a></li>
* </ul>
*/
public ResponseObject<TaskComment> editTaskComment(Long projectId, Long taskId, Long commentId, List<PatchRequest> request) throws HttpException, HttpBadRequestException {
TaskCommentResponseObject taskCommentResponseObject = this.httpClient.patch(this.url + "/projects/" + projectId + "/tasks/" + taskId + "/comments/" + commentId, request, new HttpRequestConfig(), TaskCommentResponseObject.class);
return ResponseObject.of(taskCommentResponseObject.getData());
}

//<editor-fold desc="Helper methods">

private String formUrl_taskSettingsTemplates(Long projectId) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.crowdin.client.tasks.model;

import lombok.Data;

@Data
public class CreateTaskCommentRequest {
private String text;
private Long timeSpent;
}
16 changes: 16 additions & 0 deletions src/main/java/com/crowdin/client/tasks/model/TaskComment.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.crowdin.client.tasks.model;

import lombok.Data;

import java.util.Date;

@Data
public class TaskComment {
private Long id;
private Long userId;
private Long taskId;
private String text;
private Long timeSpent;
private Date createdAt;
private Date updatedAt;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.crowdin.client.tasks.model;

import com.crowdin.client.core.model.Pagination;
import com.crowdin.client.core.model.ResponseList;
import com.crowdin.client.core.model.ResponseObject;
import lombok.Data;

import java.util.List;
import java.util.stream.Collectors;

@Data
public class TaskCommentResponseList {

private List<TaskCommentResponseObject> data;
private Pagination pagination;

public static ResponseList<TaskComment> to(TaskCommentResponseList taskCommentResponseList) {
return ResponseList.of(
taskCommentResponseList.getData().stream()
.map(TaskCommentResponseObject::getData)
.map(ResponseObject::of)
.collect(Collectors.toList()),
taskCommentResponseList.getPagination()
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.crowdin.client.tasks.model;

import lombok.Data;

@Data
public class TaskCommentResponseObject {

private TaskComment data;
}
183 changes: 183 additions & 0 deletions src/test/java/com/crowdin/client/tasks/TaskCommentsApiTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
package com.crowdin.client.tasks;

import com.crowdin.client.core.model.PatchOperation;
import com.crowdin.client.core.model.PatchRequest;
import com.crowdin.client.core.model.ResponseList;
import com.crowdin.client.core.model.ResponseObject;
import com.crowdin.client.framework.RequestMock;
import com.crowdin.client.framework.TestClient;
import com.crowdin.client.tasks.model.*;
import lombok.SneakyThrows;
import org.apache.http.client.methods.HttpDelete;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPatch;
import org.apache.http.client.methods.HttpPost;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import java.time.Instant;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.TimeZone;

import static java.util.Collections.singletonList;
import static org.junit.jupiter.api.Assertions.*;

public class TaskCommentsApiTest extends TestClient {

private final Long projectId = 12L;
private final Long taskId = 203L;
private final Long taskCommentId = 1233L;
private final Long userId = 5L;

private TimeZone originalTimeZone;

private static final Instant EXPECTED_DATE = Instant.parse("2025-08-23T09:04:29Z");

@BeforeEach
void setUp() {
originalTimeZone = TimeZone.getDefault();
TimeZone.setDefault(TimeZone.getTimeZone("GMT"));
}

@AfterEach
void tearDown() {
TimeZone.setDefault(originalTimeZone);
}

@Override
public List<RequestMock> getMocks() {
return Arrays.asList(
// LIST
RequestMock.build(
formUrl_taskComments(projectId, taskId),
HttpGet.METHOD_NAME,
"api/tasks/comments/listTaskCommentsResponse.json",
new HashMap<String, Integer>() {{
put("limit", 20);
put("offset", 10);
}}
),

// ADD
RequestMock.build(
formUrl_taskComments(projectId, taskId),
HttpPost.METHOD_NAME,
"api/tasks/comments/addTaskCommentRequest.json",
"api/tasks/comments/taskCommentsResponse_single.json"
),

// GET
RequestMock.build(
formUrl_taskCommentId(projectId, taskId, taskCommentId),
HttpGet.METHOD_NAME,
"api/tasks/comments/taskCommentsResponse_single.json"
),

// DELETE
RequestMock.build(
formUrl_taskCommentId(projectId, taskId, taskCommentId),
HttpDelete.METHOD_NAME
),

// PATCH
RequestMock.build(
formUrl_taskCommentId(projectId, taskId, taskCommentId),
HttpPatch.METHOD_NAME,
"api/tasks/comments/editTaskCommentTextRequest.json",
"api/tasks/comments/taskCommentsResponse_single.json"
)
);
}

@Test
@SneakyThrows
public void listTaskCommentsTest() {
ResponseList<TaskComment> response = this.getTasksApi().listTasksComments(projectId, taskId, 20, 10);

assertNotNull(response.getData().get(0).getData());
assertEquals(1, response.getData().size());

TaskComment comment = response.getData().get(0).getData();

assertTaskComment(comment);
}

@Test
@SneakyThrows
public void addTaskCommentTest() {
CreateTaskCommentRequest request = new CreateTaskCommentRequest();
request.setText("translate task");
request.setTimeSpent(3600L);

ResponseObject<TaskComment> response = this.getTasksApi().addTaskComment(projectId, taskId, request);

assertNotNull(response.getData());

TaskComment comment = response.getData();

assertTaskComment(comment);
}

@Test
@SneakyThrows
public void getTaskCommentTest() {
ResponseObject<TaskComment> response = this.getTasksApi().getTaskComment(projectId, taskId, taskCommentId);

assertNotNull(response.getData());

TaskComment comment = response.getData();

assertTaskComment(comment);
}

@Test
@SneakyThrows
public void editTaskCommentTest() {
PatchRequest request = new PatchRequest();
request.setOp(PatchOperation.REPLACE);
request.setValue("translate task");
request.setPath("/text");
ResponseObject<TaskComment> response = this.getTasksApi().editTaskComment(projectId, taskId, taskCommentId, singletonList(request));

assertNotNull(response.getData());

TaskComment comment = response.getData();

assertTaskComment(comment);
}

@Test
@SneakyThrows
public void deleteTaskCommentTest() {
assertDoesNotThrow(() ->
this.getTasksApi().deleteTaskComment(projectId, taskId, taskCommentId)
);
}

@SneakyThrows
private void assertTaskComment(TaskComment comment) {
assertEquals(taskCommentId, comment.getId());
assertEquals(taskId, comment.getTaskId());
assertEquals(userId, comment.getUserId());
assertEquals("translate task", comment.getText());
assertEquals(3600L, comment.getTimeSpent());

assertNotNull(comment.getCreatedAt());
assertNotNull(comment.getUpdatedAt());
assertEquals(EXPECTED_DATE, comment.getCreatedAt().toInstant());
assertEquals(EXPECTED_DATE, comment.getUpdatedAt().toInstant());
}

//<editor-fold desc="Form Url methods for mocks">
private String formUrl_taskComments(Long projectId, Long taskId) {
return this.url + "/projects/" + projectId + "/tasks/" + taskId + "/comments";
}

private String formUrl_taskCommentId(Long projectId, Long taskId, Long taskCommentId) {
return this.url + "/projects/" + projectId + "/tasks/" + taskId + "/comments/" + taskCommentId;
}
//</editor-fold>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"text": "translate task",
"timeSpent": 3600
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[
{
"op": "replace",
"path": "/text",
"value": "translate task"
}
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"data": [
{
"data": {
"id": 1233,
"userId": 5,
"taskId": 203,
"text": "translate task",
"timeSpent": 3600,
"createdAt": "2025-08-23T09:04:29+00:00",
"updatedAt": "2025-08-23T09:04:29+00:00"
}
}
],
"pagination": {
"offset": 10,
"limit": 20
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"data": {
"id": 1233,
"userId": 5,
"taskId": 203,
"text": "translate task",
"timeSpent": 3600,
"createdAt": "2025-08-23T09:04:29+00:00",
"updatedAt": "2025-08-23T09:04:29+00:00"
}
}
Loading