feat: sort by undone first
feat: add time ago format for the last updated task time issue #54
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
type TaskUrlResponse = {
|
||||
id: number,
|
||||
id: number | null,
|
||||
url: string
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ type TaskResponse = {
|
||||
id: number,
|
||||
description: string,
|
||||
done: boolean,
|
||||
lastUpdate: string,
|
||||
urls: TaskUrlResponse[]
|
||||
}
|
||||
|
||||
|
||||
+102
-15
@@ -3,11 +3,13 @@ import {
|
||||
Alert, Button, Card, Col, Container, Form, Row, Table
|
||||
} from 'react-bootstrap';
|
||||
import TaskNoteRequest from '../../types/TaskNoteRequest';
|
||||
import { TaskResponse } from '../../types/TaskResponse';
|
||||
import { TaskResponse, TaskUrlResponse } from '../../types/TaskResponse';
|
||||
import api from '../../api-service/api';
|
||||
import ApiConfig from '../../api-service/apiConfig';
|
||||
import './style.css';
|
||||
|
||||
type TaskAction = 'add' | 'edit';
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
@@ -16,6 +18,12 @@ function Task(): JSX.Element {
|
||||
const [formInvalid, setFormInvalid] = useState<boolean>(false);
|
||||
const [errorMessage, setErrorMessage] = useState<string>('');
|
||||
const [tasks, setTasks] = useState<TaskResponse[]>([]);
|
||||
const [taskId, setTaskId] = useState<number>(0);
|
||||
const [taskDescription, setTaskDescription] = useState<string>('');
|
||||
const [taskUrl, setTaskUrl] = useState<string>('');
|
||||
const [taskUrlId, setTaskUrlId] = useState<number>(0);
|
||||
const [taskDone, setTaskDone] = useState<boolean>(false);
|
||||
const [action, setAction] = useState<TaskAction>('add');
|
||||
|
||||
const handleError = (e: unknown): void => {
|
||||
if (typeof e === 'string') {
|
||||
@@ -30,6 +38,7 @@ function Task(): JSX.Element {
|
||||
const loadTasks = async () => {
|
||||
try {
|
||||
const tasksFetched: TaskResponse[] = await api.getJSON(ApiConfig.tasksUrl);
|
||||
tasksFetched.sort((t1, t2) => t1.done === t2.done? 0 : t1.done? 1 : -1);
|
||||
setTasks(tasksFetched);
|
||||
} catch (e) {
|
||||
handleError(e);
|
||||
@@ -53,6 +62,26 @@ function Task(): JSX.Element {
|
||||
return false;
|
||||
};
|
||||
|
||||
const submitEditTask = async (payload: TaskResponse): Promise<boolean> => {
|
||||
try {
|
||||
await api.patchJSON(`${ApiConfig.tasksUrl}/${payload.id}`, payload);
|
||||
loadTasks();
|
||||
return true;
|
||||
} catch (e) {
|
||||
handleError(e);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const resetInputs = () => {
|
||||
setTaskId(0);
|
||||
setTaskDescription('');
|
||||
setTaskDone(false);
|
||||
setTaskUrl('');
|
||||
setTaskUrlId(0);
|
||||
setAction('add');
|
||||
};
|
||||
|
||||
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>): Promise<void> => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
@@ -66,9 +95,35 @@ function Task(): JSX.Element {
|
||||
}
|
||||
|
||||
setFormInvalid(false);
|
||||
const added: boolean = await addTask(form.description.value, form.url.value);
|
||||
if (added) {
|
||||
form.reset();
|
||||
|
||||
if (action === 'add') {
|
||||
const added: boolean = await addTask(form.description.value, form.url.value);
|
||||
if (added) {
|
||||
form.reset();
|
||||
resetInputs();
|
||||
}
|
||||
} else if (action === 'edit') {
|
||||
const urls: TaskUrlResponse[] = [];
|
||||
if (taskUrlId) {
|
||||
urls.push({url: '', id: taskUrlId});
|
||||
}
|
||||
if (taskUrl) {
|
||||
urls.push({url: taskUrl, id: null});
|
||||
}
|
||||
|
||||
const payload: TaskResponse = {
|
||||
id: taskId,
|
||||
description: taskDescription,
|
||||
done: taskDone,
|
||||
lastUpdate: '',
|
||||
urls: urls
|
||||
};
|
||||
|
||||
const edited: boolean = await submitEditTask(payload);
|
||||
if (edited) {
|
||||
form.reset();
|
||||
resetInputs();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -85,6 +140,15 @@ function Task(): JSX.Element {
|
||||
}
|
||||
};
|
||||
|
||||
const editTask = async (task: TaskResponse) => {
|
||||
setTaskId(task.id)
|
||||
setTaskDescription(task.description);
|
||||
setTaskUrl(task.urls.length? task.urls[0].url : '');
|
||||
setTaskUrlId(task.urls.length? task.urls[0].id || 0 : 0);
|
||||
setTaskDone(task.done);
|
||||
setAction('edit');
|
||||
};
|
||||
|
||||
const deleteTask = async (taskId: number) => {
|
||||
try {
|
||||
await api.deleteNoContent(`${ApiConfig.tasksUrl}/${taskId}`);
|
||||
@@ -96,7 +160,6 @@ function Task(): JSX.Element {
|
||||
|
||||
useEffect(() => {
|
||||
loadTasks();
|
||||
// getCsrfToken();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
@@ -121,6 +184,10 @@ function Task(): JSX.Element {
|
||||
type="test"
|
||||
name="description"
|
||||
placeholder="Enter description"
|
||||
value={taskDescription}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setTaskDescription(e.target.value);
|
||||
}}
|
||||
/>
|
||||
</Form.Group>
|
||||
|
||||
@@ -131,6 +198,10 @@ function Task(): JSX.Element {
|
||||
type="text"
|
||||
name="url"
|
||||
placeholder="Additional URL (Optional)"
|
||||
value={taskUrl}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setTaskUrl(e.target.value);
|
||||
}}
|
||||
/>
|
||||
</Form.Group>
|
||||
|
||||
@@ -149,21 +220,25 @@ function Task(): JSX.Element {
|
||||
</Row>
|
||||
<Row className="mt-3">
|
||||
<Col xs={12}>
|
||||
<Table striped bordered hover>
|
||||
<Table striped bordered hover responsive>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Description</th>
|
||||
<th>Done</th>
|
||||
<th>URL</th>
|
||||
<th>Actions</th>
|
||||
<th scope="col" style={{ width: '5%' }}>#</th>
|
||||
<th scope="col" style={{ width: '50%' }}>Description</th>
|
||||
<th scope="col" style={{ width: '10%' }}>Done</th>
|
||||
<th scope="col" style={{ width: '10%' }}>URL</th>
|
||||
<th scope="col" style={{ width: '25%' }}>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{tasks.map((task: TaskResponse) => (
|
||||
<tr key={`task-${task.id}`}>
|
||||
<td className={task.done ? 'text-done' : ''}>{task.id}</td>
|
||||
<td className={task.done ? 'text-done' : ''}>{task.description}</td>
|
||||
<th scope="row" className={task.done ? 'text-done' : ''}>{task.id}</th>
|
||||
<td className={task.done ? 'text-done' : ''}>
|
||||
{task.description}
|
||||
<br />
|
||||
<small className={task.done ? '' : 'time-ago'}>{task.lastUpdate}</small>
|
||||
</td>
|
||||
<td className={task.done ? 'text-done' : ''}>{task.done ? 'Yes' : 'No'}</td>
|
||||
<td className={task.done ? 'text-done' : ''}>
|
||||
{task.urls.length > 0 ? (
|
||||
@@ -173,15 +248,27 @@ function Task(): JSX.Element {
|
||||
<td className={task.done ? 'text-done' : ''}>
|
||||
<Button
|
||||
type="button"
|
||||
variant="link"
|
||||
variant="primary"
|
||||
onClick={() => markAsDone(task)}
|
||||
className="btn-action"
|
||||
>
|
||||
{task.done ? 'Undone' : 'Done'}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
variant="link"
|
||||
variant="outline-primary"
|
||||
onClick={() => editTask(task)}
|
||||
className="btn-action"
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
variant="danger"
|
||||
onClick={() => deleteTask(task.id)}
|
||||
className="btn-action"
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
|
||||
@@ -4,4 +4,13 @@
|
||||
|
||||
.text-done .btn.btn-link {
|
||||
color: #ccc!important;
|
||||
}
|
||||
|
||||
.time-ago {
|
||||
font-size: 0.775em;
|
||||
color: #868181;
|
||||
}
|
||||
|
||||
.btn-action {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
@@ -182,6 +182,13 @@
|
||||
<artifactId>jjwt-jackson</artifactId>
|
||||
<version>0.12.6</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Pretty time - Time ago format -->
|
||||
<dependency>
|
||||
<groupId>org.ocpsoft.prettytime</groupId>
|
||||
<artifactId>prettytime</artifactId>
|
||||
<version>5.0.9.Final</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
|
||||
@@ -48,6 +48,7 @@ import org.springframework.context.annotation.ImportRuntimeHints;
|
||||
io.jsonwebtoken.impl.io.StandardCompressionAlgorithms.class,
|
||||
io.jsonwebtoken.impl.DefaultClaimsBuilder.class,
|
||||
io.jsonwebtoken.impl.DefaultJwtParserBuilder.class,
|
||||
org.ocpsoft.prettytime.PrettyTime.class,
|
||||
})
|
||||
@ImportRuntimeHints(value = {HttpServletRequestRuntimeHint.class})
|
||||
public class CloudNativeConfig {}
|
||||
|
||||
@@ -145,7 +145,8 @@ public class TaskController {
|
||||
@Valid
|
||||
TaskRequest taskRequest) {
|
||||
TaskEntity createdTask = taskService.createTask(taskRequest);
|
||||
return ResponseEntity.status(HttpStatus.CREATED).body(TaskResponse.fromEntity(createdTask));
|
||||
return ResponseEntity.status(HttpStatus.CREATED)
|
||||
.body(TaskResponse.fromEntity(createdTask, null));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package br.com.tasknoteapp.server.entity;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.FetchType;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
@@ -9,6 +10,8 @@ import jakarta.persistence.JoinColumn;
|
||||
import jakarta.persistence.ManyToOne;
|
||||
import jakarta.persistence.OneToMany;
|
||||
import jakarta.persistence.Table;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
@@ -35,4 +38,7 @@ public class TaskEntity {
|
||||
|
||||
@OneToMany(mappedBy = "task", fetch = FetchType.LAZY)
|
||||
private List<TaskUrlEntity> urls;
|
||||
|
||||
@Column(name = "last_update")
|
||||
private LocalDateTime lastUpdate;
|
||||
}
|
||||
|
||||
@@ -7,12 +7,15 @@ import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import org.ocpsoft.prettytime.PrettyTime;
|
||||
|
||||
/** This record represents a task and its urls object to be returned. */
|
||||
@Schema(description = "This record represents a task and its urls object to be returned.")
|
||||
public record TaskResponse(
|
||||
@Schema(description = "The id of the task", example = "1") Long id,
|
||||
@Schema(description = "The description of the task", example = "Task 1") String description,
|
||||
@Schema(description = "The done status of the task", example = "false") Boolean done,
|
||||
@Schema(description = "When was the last update time of the task") String lastUpdate,
|
||||
@Schema(description = "The urls of the task, zero, one or more.", example = "[]")
|
||||
List<TaskUrlResponse> urls) {
|
||||
|
||||
@@ -22,7 +25,7 @@ public record TaskResponse(
|
||||
* @param entity The TaskEntity source data.
|
||||
* @return TaskResponse instance with all task data and urls, if any.
|
||||
*/
|
||||
public static TaskResponse fromEntity(TaskEntity entity) {
|
||||
public static TaskResponse fromEntity(TaskEntity entity, PrettyTime prettyTime) {
|
||||
List<TaskUrlEntity> urls = entity.getUrls();
|
||||
List<TaskUrlResponse> urlsResponse = new ArrayList<>();
|
||||
if (Objects.isNull(urls)) {
|
||||
@@ -34,7 +37,17 @@ public record TaskResponse(
|
||||
}
|
||||
}
|
||||
|
||||
if (Objects.isNull(prettyTime)) {
|
||||
prettyTime = new PrettyTime();
|
||||
}
|
||||
|
||||
String timeAgoFmt = prettyTime.format(entity.getLastUpdate());
|
||||
|
||||
return new TaskResponse(
|
||||
entity.getId(), entity.getDescription(), entity.getDone(), urlsResponse);
|
||||
entity.getId(),
|
||||
entity.getDescription(),
|
||||
entity.getDone(),
|
||||
timeAgoFmt,
|
||||
urlsResponse);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,12 +14,15 @@ import br.com.tasknoteapp.server.service.AuthService;
|
||||
import br.com.tasknoteapp.server.service.TaskService;
|
||||
import br.com.tasknoteapp.server.util.AuthUtil;
|
||||
import jakarta.transaction.Transactional;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.ocpsoft.prettytime.PrettyTime;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/** This class contains the implementation for the Task Service class. */
|
||||
@@ -45,7 +48,8 @@ class TaskServiceImpl implements TaskService {
|
||||
List<TaskEntity> tasks = taskRepository.findAllByUser_id(user.getId());
|
||||
log.info("{} tasks found!", tasks.size());
|
||||
|
||||
return tasks.stream().map(TaskResponse::fromEntity).toList();
|
||||
PrettyTime time = new PrettyTime();
|
||||
return tasks.stream().map((TaskEntity tr) -> TaskResponse.fromEntity(tr, time)).toList();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -58,6 +62,7 @@ class TaskServiceImpl implements TaskService {
|
||||
task.setDescription(taskRequest.description());
|
||||
task.setDone(false);
|
||||
task.setUser(user);
|
||||
task.setLastUpdate(LocalDateTime.now());
|
||||
TaskEntity created = taskRepository.save(task);
|
||||
|
||||
if (!Objects.isNull(taskRequest.urls()) && !taskRequest.urls().isEmpty()) {
|
||||
@@ -89,6 +94,8 @@ class TaskServiceImpl implements TaskService {
|
||||
taskEntity.setDone(patch.done());
|
||||
}
|
||||
|
||||
taskEntity.setLastUpdate(LocalDateTime.now());
|
||||
|
||||
if (!Objects.isNull(patch.urls())) {
|
||||
List<Long> urlIds =
|
||||
patch.urls().stream().filter(p -> p.id() != null).map(TaskUrlPatchRequest::id).toList();
|
||||
@@ -110,7 +117,7 @@ class TaskServiceImpl implements TaskService {
|
||||
|
||||
log.info("Task patched! Id {}", patchedTask.getId());
|
||||
|
||||
return TaskResponse.fromEntity(patchedTask);
|
||||
return TaskResponse.fromEntity(patchedTask, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -148,7 +155,8 @@ class TaskServiceImpl implements TaskService {
|
||||
taskRepository.findAllBySearchTerm(searchTerm.toUpperCase(), user.getId());
|
||||
log.info("{} tasks found!", tasks.size());
|
||||
|
||||
return tasks.stream().map(TaskResponse::fromEntity).toList();
|
||||
PrettyTime time = new PrettyTime();
|
||||
return tasks.stream().map((TaskEntity tr) -> TaskResponse.fromEntity(tr, time)).toList();
|
||||
}
|
||||
|
||||
private UserEntity getCurrentUser() {
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
alter table tasknote.tasks
|
||||
add last_update timestamp;
|
||||
@@ -73,7 +73,7 @@ class HomeControllerTest {
|
||||
@DisplayName("Search happy path should succeed")
|
||||
@WithMockUser(username = "user@domain.com", password = "abcde123456A@")
|
||||
void search_happyPath_shouldSucceed() throws Exception {
|
||||
TaskResponse task = new TaskResponse(1L, "Task 1", false, List.of());
|
||||
TaskResponse task = new TaskResponse(1L, "Task 1", false, "now", List.of());
|
||||
NoteResponse note = new NoteResponse(1L, "Note 1", "Note desc", List.of());
|
||||
SearchResponse response = new SearchResponse(List.of(task), List.of(note));
|
||||
|
||||
|
||||
Reference in New Issue
Block a user