Skip to content

Commit 027181e

Browse files
authored
feat: Support for the new Task Comments API (#333)
1 parent e4ea3a3 commit 027181e

File tree

10 files changed

+364
-9
lines changed

10 files changed

+364
-9
lines changed

src/main/java/com/crowdin/client/tasks/TasksApi.java

Lines changed: 80 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,7 @@
1212
import com.crowdin.client.core.model.PatchRequest;
1313
import com.crowdin.client.core.model.ResponseList;
1414
import com.crowdin.client.core.model.ResponseObject;
15-
import com.crowdin.client.tasks.model.AddTaskRequest;
16-
import com.crowdin.client.tasks.model.AddTaskSettingsTemplateRequest;
17-
import com.crowdin.client.tasks.model.Status;
18-
import com.crowdin.client.tasks.model.Task;
19-
import com.crowdin.client.tasks.model.TaskResponseList;
20-
import com.crowdin.client.tasks.model.TaskResponseObject;
21-
import com.crowdin.client.tasks.model.TaskSettingsTemplate;
22-
import com.crowdin.client.tasks.model.TaskSettingsTemplateResponseList;
23-
import com.crowdin.client.tasks.model.TaskSettingsTemplateResponseObject;
15+
import com.crowdin.client.tasks.model.*;
2416

2517
import java.util.List;
2618
import java.util.Map;
@@ -248,6 +240,85 @@ public ResponseObject<TaskSettingsTemplate> editTaskSettingsTemplate(Long projec
248240
return ResponseObject.of(responseObject.getData());
249241
}
250242

243+
/**
244+
* @param projectId project identifier
245+
* @param taskId task identifier
246+
* @param limit maximum number of items to retrieve (default 25)
247+
* @param offset starting offset in the collection (default 0)
248+
* @return list of task comments
249+
* @see <ul>
250+
* <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>
251+
* <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>
252+
* </ul>
253+
*/
254+
public ResponseList<TaskComment> listTasksComments(Long projectId, Long taskId, Integer limit, Integer offset) throws HttpException, HttpBadRequestException {
255+
Map<String, Optional<Object>> queryParams = HttpRequestConfig.buildUrlParams(
256+
"limit", Optional.ofNullable(limit),
257+
"offset", Optional.ofNullable(offset)
258+
);
259+
TaskCommentResponseList taskCommentResponseList = this.httpClient.get(this.url + "/projects/" + projectId + "/tasks/" + taskId + "/comments", new HttpRequestConfig(queryParams), TaskCommentResponseList.class);
260+
return TaskCommentResponseList.to(taskCommentResponseList);
261+
}
262+
263+
/**
264+
* @param projectId project identifier
265+
* @param taskId task identifier
266+
* @param request request object
267+
* @return newly created task comment
268+
* @see <ul>
269+
* <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>
270+
* <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>
271+
* </ul>
272+
*/
273+
public ResponseObject<TaskComment> addTaskComment(Long projectId, Long taskId, CreateTaskCommentRequest request) throws HttpException, HttpBadRequestException {
274+
TaskCommentResponseObject taskCommentResponseObject = this.httpClient.post(this.url + "/projects/" + projectId + "/tasks/" + taskId + "/comments", request, new HttpRequestConfig(), TaskCommentResponseObject.class);
275+
return ResponseObject.of(taskCommentResponseObject.getData());
276+
}
277+
278+
/**
279+
* @param projectId project identifier
280+
* @param taskId task identifier
281+
* @param commentId comment identifier
282+
* @return task comment
283+
* @see <ul>
284+
* <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>
285+
* <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>
286+
* </ul>
287+
*/
288+
public ResponseObject<TaskComment> getTaskComment(Long projectId, Long taskId, Long commentId) throws HttpException, HttpBadRequestException {
289+
TaskCommentResponseObject taskCommentResponseObject = this.httpClient.get(this.url + "/projects/" + projectId + "/tasks/" + taskId + "/comments/" + commentId, new HttpRequestConfig(), TaskCommentResponseObject.class);
290+
return ResponseObject.of(taskCommentResponseObject.getData());
291+
}
292+
293+
/**
294+
* @param projectId project identifier
295+
* @param taskId task identifier
296+
* @param commentId task identifier
297+
* @see <ul>
298+
* <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>
299+
* <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>
300+
* </ul>
301+
*/
302+
public void deleteTaskComment(Long projectId, Long taskId, Long commentId) throws HttpException, HttpBadRequestException {
303+
this.httpClient.delete(this.url + "/projects/" + projectId + "/tasks/" + taskId + "/comments/" + commentId, new HttpRequestConfig(), Void.class);
304+
}
305+
306+
/**
307+
* @param projectId project identifier
308+
* @param taskId task identifier
309+
* @param commentId task identifier
310+
* @param request request object
311+
* @return updated task comment
312+
* @see <ul>
313+
* <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>
314+
* <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>
315+
* </ul>
316+
*/
317+
public ResponseObject<TaskComment> editTaskComment(Long projectId, Long taskId, Long commentId, List<PatchRequest> request) throws HttpException, HttpBadRequestException {
318+
TaskCommentResponseObject taskCommentResponseObject = this.httpClient.patch(this.url + "/projects/" + projectId + "/tasks/" + taskId + "/comments/" + commentId, request, new HttpRequestConfig(), TaskCommentResponseObject.class);
319+
return ResponseObject.of(taskCommentResponseObject.getData());
320+
}
321+
251322
//<editor-fold desc="Helper methods">
252323

253324
private String formUrl_taskSettingsTemplates(Long projectId) {
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package com.crowdin.client.tasks.model;
2+
3+
import lombok.Data;
4+
5+
@Data
6+
public class CreateTaskCommentRequest {
7+
private String text;
8+
private Long timeSpent;
9+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package com.crowdin.client.tasks.model;
2+
3+
import lombok.Data;
4+
5+
import java.util.Date;
6+
7+
@Data
8+
public class TaskComment {
9+
private Long id;
10+
private Long userId;
11+
private Long taskId;
12+
private String text;
13+
private Long timeSpent;
14+
private Date createdAt;
15+
private Date updatedAt;
16+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package com.crowdin.client.tasks.model;
2+
3+
import com.crowdin.client.core.model.Pagination;
4+
import com.crowdin.client.core.model.ResponseList;
5+
import com.crowdin.client.core.model.ResponseObject;
6+
import lombok.Data;
7+
8+
import java.util.List;
9+
import java.util.stream.Collectors;
10+
11+
@Data
12+
public class TaskCommentResponseList {
13+
14+
private List<TaskCommentResponseObject> data;
15+
private Pagination pagination;
16+
17+
public static ResponseList<TaskComment> to(TaskCommentResponseList taskCommentResponseList) {
18+
return ResponseList.of(
19+
taskCommentResponseList.getData().stream()
20+
.map(TaskCommentResponseObject::getData)
21+
.map(ResponseObject::of)
22+
.collect(Collectors.toList()),
23+
taskCommentResponseList.getPagination()
24+
);
25+
}
26+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package com.crowdin.client.tasks.model;
2+
3+
import lombok.Data;
4+
5+
@Data
6+
public class TaskCommentResponseObject {
7+
8+
private TaskComment data;
9+
}
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
package com.crowdin.client.tasks;
2+
3+
import com.crowdin.client.core.model.PatchOperation;
4+
import com.crowdin.client.core.model.PatchRequest;
5+
import com.crowdin.client.core.model.ResponseList;
6+
import com.crowdin.client.core.model.ResponseObject;
7+
import com.crowdin.client.framework.RequestMock;
8+
import com.crowdin.client.framework.TestClient;
9+
import com.crowdin.client.tasks.model.*;
10+
import lombok.SneakyThrows;
11+
import org.apache.http.client.methods.HttpDelete;
12+
import org.apache.http.client.methods.HttpGet;
13+
import org.apache.http.client.methods.HttpPatch;
14+
import org.apache.http.client.methods.HttpPost;
15+
import org.junit.jupiter.api.AfterEach;
16+
import org.junit.jupiter.api.BeforeEach;
17+
import org.junit.jupiter.api.Test;
18+
19+
import java.time.Instant;
20+
import java.util.Arrays;
21+
import java.util.HashMap;
22+
import java.util.List;
23+
import java.util.TimeZone;
24+
25+
import static java.util.Collections.singletonList;
26+
import static org.junit.jupiter.api.Assertions.*;
27+
28+
public class TaskCommentsApiTest extends TestClient {
29+
30+
private final Long projectId = 12L;
31+
private final Long taskId = 203L;
32+
private final Long taskCommentId = 1233L;
33+
private final Long userId = 5L;
34+
35+
private TimeZone originalTimeZone;
36+
37+
private static final Instant EXPECTED_DATE = Instant.parse("2025-08-23T09:04:29Z");
38+
39+
@BeforeEach
40+
void setUp() {
41+
originalTimeZone = TimeZone.getDefault();
42+
TimeZone.setDefault(TimeZone.getTimeZone("GMT"));
43+
}
44+
45+
@AfterEach
46+
void tearDown() {
47+
TimeZone.setDefault(originalTimeZone);
48+
}
49+
50+
@Override
51+
public List<RequestMock> getMocks() {
52+
return Arrays.asList(
53+
// LIST
54+
RequestMock.build(
55+
formUrl_taskComments(projectId, taskId),
56+
HttpGet.METHOD_NAME,
57+
"api/tasks/comments/listTaskCommentsResponse.json",
58+
new HashMap<String, Integer>() {{
59+
put("limit", 20);
60+
put("offset", 10);
61+
}}
62+
),
63+
64+
// ADD
65+
RequestMock.build(
66+
formUrl_taskComments(projectId, taskId),
67+
HttpPost.METHOD_NAME,
68+
"api/tasks/comments/addTaskCommentRequest.json",
69+
"api/tasks/comments/taskCommentsResponse_single.json"
70+
),
71+
72+
// GET
73+
RequestMock.build(
74+
formUrl_taskCommentId(projectId, taskId, taskCommentId),
75+
HttpGet.METHOD_NAME,
76+
"api/tasks/comments/taskCommentsResponse_single.json"
77+
),
78+
79+
// DELETE
80+
RequestMock.build(
81+
formUrl_taskCommentId(projectId, taskId, taskCommentId),
82+
HttpDelete.METHOD_NAME
83+
),
84+
85+
// PATCH
86+
RequestMock.build(
87+
formUrl_taskCommentId(projectId, taskId, taskCommentId),
88+
HttpPatch.METHOD_NAME,
89+
"api/tasks/comments/editTaskCommentTextRequest.json",
90+
"api/tasks/comments/taskCommentsResponse_single.json"
91+
)
92+
);
93+
}
94+
95+
@Test
96+
@SneakyThrows
97+
public void listTaskCommentsTest() {
98+
ResponseList<TaskComment> response = this.getTasksApi().listTasksComments(projectId, taskId, 20, 10);
99+
100+
assertNotNull(response.getData().get(0).getData());
101+
assertEquals(1, response.getData().size());
102+
103+
TaskComment comment = response.getData().get(0).getData();
104+
105+
assertTaskComment(comment);
106+
}
107+
108+
@Test
109+
@SneakyThrows
110+
public void addTaskCommentTest() {
111+
CreateTaskCommentRequest request = new CreateTaskCommentRequest();
112+
request.setText("translate task");
113+
request.setTimeSpent(3600L);
114+
115+
ResponseObject<TaskComment> response = this.getTasksApi().addTaskComment(projectId, taskId, request);
116+
117+
assertNotNull(response.getData());
118+
119+
TaskComment comment = response.getData();
120+
121+
assertTaskComment(comment);
122+
}
123+
124+
@Test
125+
@SneakyThrows
126+
public void getTaskCommentTest() {
127+
ResponseObject<TaskComment> response = this.getTasksApi().getTaskComment(projectId, taskId, taskCommentId);
128+
129+
assertNotNull(response.getData());
130+
131+
TaskComment comment = response.getData();
132+
133+
assertTaskComment(comment);
134+
}
135+
136+
@Test
137+
@SneakyThrows
138+
public void editTaskCommentTest() {
139+
PatchRequest request = new PatchRequest();
140+
request.setOp(PatchOperation.REPLACE);
141+
request.setValue("translate task");
142+
request.setPath("/text");
143+
ResponseObject<TaskComment> response = this.getTasksApi().editTaskComment(projectId, taskId, taskCommentId, singletonList(request));
144+
145+
assertNotNull(response.getData());
146+
147+
TaskComment comment = response.getData();
148+
149+
assertTaskComment(comment);
150+
}
151+
152+
@Test
153+
@SneakyThrows
154+
public void deleteTaskCommentTest() {
155+
assertDoesNotThrow(() ->
156+
this.getTasksApi().deleteTaskComment(projectId, taskId, taskCommentId)
157+
);
158+
}
159+
160+
@SneakyThrows
161+
private void assertTaskComment(TaskComment comment) {
162+
assertEquals(taskCommentId, comment.getId());
163+
assertEquals(taskId, comment.getTaskId());
164+
assertEquals(userId, comment.getUserId());
165+
assertEquals("translate task", comment.getText());
166+
assertEquals(3600L, comment.getTimeSpent());
167+
168+
assertNotNull(comment.getCreatedAt());
169+
assertNotNull(comment.getUpdatedAt());
170+
assertEquals(EXPECTED_DATE, comment.getCreatedAt().toInstant());
171+
assertEquals(EXPECTED_DATE, comment.getUpdatedAt().toInstant());
172+
}
173+
174+
//<editor-fold desc="Form Url methods for mocks">
175+
private String formUrl_taskComments(Long projectId, Long taskId) {
176+
return this.url + "/projects/" + projectId + "/tasks/" + taskId + "/comments";
177+
}
178+
179+
private String formUrl_taskCommentId(Long projectId, Long taskId, Long taskCommentId) {
180+
return this.url + "/projects/" + projectId + "/tasks/" + taskId + "/comments/" + taskCommentId;
181+
}
182+
//</editor-fold>
183+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"text": "translate task",
3+
"timeSpent": 3600
4+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
[
2+
{
3+
"op": "replace",
4+
"path": "/text",
5+
"value": "translate task"
6+
}
7+
]
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"data": [
3+
{
4+
"data": {
5+
"id": 1233,
6+
"userId": 5,
7+
"taskId": 203,
8+
"text": "translate task",
9+
"timeSpent": 3600,
10+
"createdAt": "2025-08-23T09:04:29+00:00",
11+
"updatedAt": "2025-08-23T09:04:29+00:00"
12+
}
13+
}
14+
],
15+
"pagination": {
16+
"offset": 10,
17+
"limit": 20
18+
}
19+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"data": {
3+
"id": 1233,
4+
"userId": 5,
5+
"taskId": 203,
6+
"text": "translate task",
7+
"timeSpent": 3600,
8+
"createdAt": "2025-08-23T09:04:29+00:00",
9+
"updatedAt": "2025-08-23T09:04:29+00:00"
10+
}
11+
}

0 commit comments

Comments
 (0)