@@ -40,6 +40,8 @@ const ApiConfig = {
|
||||
|
||||
refreshTokenUrl: `${server}/rest/user-sessions/refresh`,
|
||||
|
||||
tasksUrl: `${server}/rest/tasks`,
|
||||
|
||||
login: async (email: string, password: string): Promise<SigninResponse | Error> => {
|
||||
try {
|
||||
const response = await fetch(ApiConfig.signInUrl, {
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
import { API_TOKEN } from '../app-constants/app-constants';
|
||||
import TaskRequest from '../types/TaskRequest';
|
||||
import { TaskResponse } from '../types/TaskResponse';
|
||||
import ApiConfig from './apiConfig';
|
||||
|
||||
async function addTaskRequest(task: TaskRequest): Promise<TaskResponse | Error> {
|
||||
try {
|
||||
const tokenState = localStorage.getItem(API_TOKEN);
|
||||
const response = await fetch(ApiConfig.tasksUrl, {
|
||||
method: 'POST',
|
||||
mode: 'cors',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${tokenState}`
|
||||
},
|
||||
body: JSON.stringify(task)
|
||||
});
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
return data;
|
||||
}
|
||||
if (response.status === 400) {
|
||||
return new Error('Wrong or missing information!');
|
||||
}
|
||||
if (response.status === 403) {
|
||||
return new Error('Forbidden! Access denied');
|
||||
}
|
||||
if (response.status === 500) {
|
||||
return new Error('Internal Server Error!');
|
||||
}
|
||||
} catch (e) {
|
||||
if (typeof e === 'string') {
|
||||
return new Error(e as string);
|
||||
}
|
||||
}
|
||||
return new Error('Unknown error');
|
||||
}
|
||||
|
||||
async function getTasksRequest(): Promise<TaskResponse[] | Error> {
|
||||
try {
|
||||
const tokenState = localStorage.getItem(API_TOKEN);
|
||||
const response = await fetch(ApiConfig.tasksUrl, {
|
||||
method: 'GET',
|
||||
mode: 'cors',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${tokenState}`
|
||||
}
|
||||
});
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
return data;
|
||||
}
|
||||
if (response.status === 403) {
|
||||
return new Error('Forbidden! Access denied');
|
||||
}
|
||||
if (response.status === 500) {
|
||||
return new Error('Internal Server Error!');
|
||||
}
|
||||
} catch (e) {
|
||||
if (typeof e === 'string') {
|
||||
return new Error(e as string);
|
||||
}
|
||||
}
|
||||
return new Error('Unknown error');
|
||||
}
|
||||
|
||||
async function updateTaskDoneRequest(id: number, done: boolean): Promise<Error | undefined> {
|
||||
try {
|
||||
const tokenState = localStorage.getItem(API_TOKEN);
|
||||
const response = await fetch(`${ApiConfig.tasksUrl}/${id}`, {
|
||||
method: 'PATCH',
|
||||
mode: 'cors',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${tokenState}`
|
||||
},
|
||||
body: JSON.stringify({ done })
|
||||
});
|
||||
if (response.ok) {
|
||||
await response.json();
|
||||
return;
|
||||
}
|
||||
if (response.status === 403) {
|
||||
return new Error('Forbidden! Access denied');
|
||||
}
|
||||
if (response.status === 500) {
|
||||
return new Error('Internal Server Error!');
|
||||
}
|
||||
} catch (e) {
|
||||
if (typeof e === 'string') {
|
||||
return new Error(e as string);
|
||||
}
|
||||
}
|
||||
return new Error('Unknown error');
|
||||
}
|
||||
|
||||
async function deleteTaskRequest(id: number): Promise<Error | undefined> {
|
||||
try {
|
||||
const tokenState = localStorage.getItem(API_TOKEN);
|
||||
const response = await fetch(`${ApiConfig.tasksUrl}/${id}`, {
|
||||
method: 'DELETE',
|
||||
mode: 'cors',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${tokenState}`
|
||||
}
|
||||
});
|
||||
if (response.status === 204) {
|
||||
return;
|
||||
}
|
||||
if (response.status === 403) {
|
||||
return new Error('Forbidden! Access denied');
|
||||
}
|
||||
if (response.status === 500) {
|
||||
return new Error('Internal Server Error!');
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('aha!');
|
||||
if (typeof e === 'string') {
|
||||
return new Error(e as string);
|
||||
}
|
||||
}
|
||||
return new Error('Unknown error');
|
||||
}
|
||||
|
||||
export { addTaskRequest, getTasksRequest, updateTaskDoneRequest, deleteTaskRequest };
|
||||
@@ -1,13 +0,0 @@
|
||||
.footer {
|
||||
height: 200px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: #282c34;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.footer span {
|
||||
font-weight: bold;
|
||||
color: #61dafb;
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
import React, { useContext } from 'react';
|
||||
import { Button } from 'react-bootstrap';
|
||||
import style from './Footer.module.css';
|
||||
import { env } from '../env';
|
||||
import AuthContext from '../context/AuthContext';
|
||||
|
||||
const Footer = () => {
|
||||
const { signOut } = useContext(AuthContext);
|
||||
const build = env.VITE_BUILD;
|
||||
|
||||
return (
|
||||
<footer className={style.footer}>
|
||||
|
||||
<span>React + TS Todoo</span>
|
||||
{' '}
|
||||
@ 2024
|
||||
{` Build: ${build}`}
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => signOut()}
|
||||
>
|
||||
Sair
|
||||
</Button>
|
||||
</footer>
|
||||
);
|
||||
};
|
||||
|
||||
export default Footer;
|
||||
@@ -0,0 +1,42 @@
|
||||
import React, { useContext } from 'react';
|
||||
import { Button, Col, Container, Row } from 'react-bootstrap';
|
||||
import { env } from '../../env';
|
||||
import AuthContext from '../../context/AuthContext';
|
||||
import './style.css';
|
||||
|
||||
/**
|
||||
* Footer component.
|
||||
*
|
||||
* This component provides the footer section of the application,
|
||||
* providing navigation to logout.
|
||||
* It also includes the build version.
|
||||
*
|
||||
* @returns The Footer component.
|
||||
*/
|
||||
function Footer(): JSX.Element {
|
||||
const { signOut, user } = useContext(AuthContext);
|
||||
const build = env.VITE_BUILD;
|
||||
const currentYear = new Date().getFullYear();
|
||||
|
||||
return (
|
||||
<footer className="footer">
|
||||
<Container>
|
||||
<Row className="align-items-center">
|
||||
<Col xs={12} sm={4} className="text-center text-sm-start">
|
||||
<span>TaskNote App © {currentYear} ({build})</span>
|
||||
</Col>
|
||||
<Col xs={12} sm={4} className="text-center text-sm-end">
|
||||
{user?.email}
|
||||
</Col>
|
||||
<Col xs={12} sm={4} className="text-center text-sm-end">
|
||||
<Button type="button" variant="link" onClick={signOut} className="logout-button">
|
||||
Logout
|
||||
</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
</footer>
|
||||
);
|
||||
};
|
||||
|
||||
export default Footer;
|
||||
@@ -0,0 +1,19 @@
|
||||
.footer {
|
||||
background-color: #f8f9fa;
|
||||
padding: 20px 0;
|
||||
margin-top: 50px;
|
||||
}
|
||||
|
||||
.link {
|
||||
color: #007bff;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.logout-button {
|
||||
color: #dc3545!important;
|
||||
text-decoration: none!important;
|
||||
}
|
||||
@@ -24,6 +24,12 @@ function Header() {
|
||||
<LinkContainer to="/home">
|
||||
<Nav.Link>Home</Nav.Link>
|
||||
</LinkContainer>
|
||||
<LinkContainer to="/tasks">
|
||||
<Nav.Link>Tasks</Nav.Link>
|
||||
</LinkContainer>
|
||||
<LinkContainer to="/notes">
|
||||
<Nav.Link>Notes</Nav.Link>
|
||||
</LinkContainer>
|
||||
<LinkContainer to="/about">
|
||||
<Nav.Link>About</Nav.Link>
|
||||
</LinkContainer>
|
||||
|
||||
@@ -39,17 +39,29 @@ const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }: Pro
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const updateUserSession = (userPriv: User | null, bearerToken: string) => {
|
||||
const updateUserSession = (userPriv: User | null, bearerToken: string): User => {
|
||||
if (userPriv) {
|
||||
localStorage.setItem(USER_DATA, JSON.stringify(userPriv));
|
||||
}
|
||||
localStorage.setItem(API_TOKEN, bearerToken);
|
||||
|
||||
if (userPriv) {
|
||||
return userPriv;
|
||||
}
|
||||
|
||||
const savedUser = localStorage.getItem(USER_DATA);
|
||||
if (savedUser) {
|
||||
return JSON.parse(savedUser);
|
||||
}
|
||||
|
||||
return { email: 'undefined' };
|
||||
};
|
||||
|
||||
const checkCurrentAuthUser = async (pathname: string): Promise<void> => {
|
||||
const bearerToken: SigninResponse | undefined = await fetchCurrentSession(pathname);
|
||||
if (bearerToken && bearerToken.token) {
|
||||
updateUserSession(null, bearerToken.token);
|
||||
const userLocal = updateUserSession(null, bearerToken.token);
|
||||
setUser(userLocal);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -61,7 +73,7 @@ const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }: Pro
|
||||
}
|
||||
|
||||
const currentUser: User = {
|
||||
email
|
||||
email: email
|
||||
};
|
||||
|
||||
setSigned(true);
|
||||
@@ -78,7 +90,7 @@ const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }: Pro
|
||||
}
|
||||
|
||||
const currentUser: User = {
|
||||
email
|
||||
email: email
|
||||
};
|
||||
|
||||
setSigned(true);
|
||||
@@ -102,7 +114,8 @@ const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }: Pro
|
||||
const refreshTokenPvt = async (): Promise<void> => {
|
||||
const bearerToken: SigninResponse | undefined = await fetchCurrentSession('/');
|
||||
if (bearerToken) {
|
||||
updateUserSession(null, bearerToken.token);
|
||||
const userLocal = updateUserSession(null, bearerToken.token);
|
||||
setUser(userLocal);
|
||||
}
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
@@ -3,21 +3,28 @@ import { Outlet } from 'react-router-dom';
|
||||
import { Container } from 'react-bootstrap';
|
||||
import Header from '../../components/Header';
|
||||
import Footer from '../../components/Footer';
|
||||
import './style.css';
|
||||
|
||||
/**
|
||||
* Layout component.
|
||||
*
|
||||
* This component provides the layout of the application,
|
||||
* including the outlet responsible for the main content.
|
||||
* It also includes the header and the footer.
|
||||
*
|
||||
* @returns The Layout component.
|
||||
*/
|
||||
function Layout() {
|
||||
function Layout(): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
<div className="page-container">
|
||||
<Header />
|
||||
|
||||
<Container>
|
||||
<Container className="content-container">
|
||||
<Outlet />
|
||||
</Container>
|
||||
|
||||
<Footer />
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
.page-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh; /* Ensure the container takes up full viewport height */
|
||||
}
|
||||
|
||||
.content-container {
|
||||
flex-grow: 1; /* Allow the content area to grow and fill the remaining space */
|
||||
}
|
||||
@@ -2,6 +2,8 @@ import { Navigate, RouteObject } from 'react-router-dom';
|
||||
import getStoredPath from '../utils/PathUtils';
|
||||
import Home from '../views/Home';
|
||||
import About from '../views/About';
|
||||
import Task from '../views/Task';
|
||||
import Note from '../views/Note';
|
||||
|
||||
const BrowserRoutes: RouteObject[] = [
|
||||
{
|
||||
@@ -24,15 +26,19 @@ const BrowserRoutes: RouteObject[] = [
|
||||
},
|
||||
{
|
||||
path: '/home',
|
||||
element: (
|
||||
<Home />
|
||||
)
|
||||
element: <Home />
|
||||
},
|
||||
{
|
||||
path: '/about',
|
||||
element: (
|
||||
<About />
|
||||
)
|
||||
element: <About />
|
||||
},
|
||||
{
|
||||
path: '/tasks',
|
||||
element: <Task />
|
||||
},
|
||||
{
|
||||
path: '/notes',
|
||||
element: <Note />
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
type TaskRequest = {
|
||||
description: string;
|
||||
urls?: string[];
|
||||
}
|
||||
|
||||
export default TaskRequest;
|
||||
@@ -0,0 +1,13 @@
|
||||
type TaskUrlResponse = {
|
||||
id: number,
|
||||
url: string
|
||||
}
|
||||
|
||||
type TaskResponse = {
|
||||
id: number,
|
||||
description: string,
|
||||
done: boolean,
|
||||
urls: TaskUrlResponse[]
|
||||
}
|
||||
|
||||
export type { TaskUrlResponse, TaskResponse };
|
||||
@@ -1,11 +1,55 @@
|
||||
import React from 'react';
|
||||
import { Button, Card, Col, Container, Row } from 'react-bootstrap';
|
||||
import './style.css';
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
function Home() {
|
||||
function Home(): JSX.Element {
|
||||
return (
|
||||
<h1>This is home!</h1>
|
||||
<Container>
|
||||
<Row className="mt-3">
|
||||
<Col xs={12}>
|
||||
<Card>
|
||||
<Card.Body>
|
||||
<Card.Title>Search</Card.Title>
|
||||
<Card.Text>
|
||||
10
|
||||
</Card.Text>
|
||||
</Card.Body>
|
||||
<Card.Footer>3 record(s) found!</Card.Footer>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row className="mt-3">
|
||||
<Col xs={12} sm={6}>
|
||||
<Card className="text-center">
|
||||
<Card.Header>Tasks</Card.Header>
|
||||
<Card.Body>
|
||||
<Card.Title>Pending tasks</Card.Title>
|
||||
<Card.Text>
|
||||
10
|
||||
</Card.Text>
|
||||
<Button variant="primary" type="button">Go to Tasks</Button>
|
||||
</Card.Body>
|
||||
<Card.Footer>Test?</Card.Footer>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={12} sm={6}>
|
||||
<Card className="text-center">
|
||||
<Card.Header>Notes</Card.Header>
|
||||
<Card.Body>
|
||||
<Card.Title>Notes summary</Card.Title>
|
||||
<Card.Text>
|
||||
10
|
||||
</Card.Text>
|
||||
<Button variant="primary" type="button">Go to Notes</Button>
|
||||
</Card.Body>
|
||||
<Card.Footer>Test?</Card.Footer>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
.home-card {
|
||||
padding-top: 100px;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import React from 'react';
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
function Note() {
|
||||
return (
|
||||
<h1>This is task!</h1>
|
||||
);
|
||||
}
|
||||
|
||||
export default Note;
|
||||
@@ -0,0 +1,201 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { Alert, Button, Card, Col, Container, Form, InputGroup, Row, Table } from 'react-bootstrap';
|
||||
import {
|
||||
addTaskRequest,
|
||||
deleteTaskRequest,
|
||||
getTasksRequest,
|
||||
updateTaskDoneRequest
|
||||
} from '../../api-service/taskService';
|
||||
import TaskRequest from '../../types/TaskRequest';
|
||||
import { TaskResponse } from '../../types/TaskResponse';
|
||||
import './style.css';
|
||||
|
||||
function Task(): JSX.Element {
|
||||
const [validated, setValidated] = useState<boolean>(true);
|
||||
const [formInvalid, setFormInvalid] = useState<boolean>(false);
|
||||
const [errorMessage, setErrorMessage] = useState<string>('');
|
||||
const [tasks, setTasks] = useState<TaskResponse[]>([]);
|
||||
|
||||
const handleError = (e: unknown): void => {
|
||||
if (typeof e === 'string') {
|
||||
setErrorMessage(e);
|
||||
setFormInvalid(true);
|
||||
} else if (e instanceof Error) {
|
||||
setErrorMessage(e.message);
|
||||
setFormInvalid(true);
|
||||
}
|
||||
}
|
||||
|
||||
const addTask = async (desc: string, url?: string): Promise<boolean> => {
|
||||
const payload: TaskRequest = {
|
||||
description: desc,
|
||||
urls: url? [url] : []
|
||||
};
|
||||
const response = await addTaskRequest(payload);
|
||||
|
||||
if ('id' in response) {
|
||||
const task: TaskResponse = response;
|
||||
loadTasks();
|
||||
return true;
|
||||
} else {
|
||||
handleError(response);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>): Promise<void> => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
setValidated(true);
|
||||
|
||||
const form = event.currentTarget;
|
||||
if (form.checkValidity() === false) {
|
||||
setFormInvalid(true);
|
||||
return;
|
||||
}
|
||||
|
||||
setFormInvalid(false);
|
||||
const added: boolean = await addTask(form.description.value, form.url.value);
|
||||
if (added) {
|
||||
form.reset();
|
||||
}
|
||||
};
|
||||
|
||||
const loadTasks = async() => {
|
||||
const tasks: TaskResponse[] | Error = await getTasksRequest();
|
||||
if (tasks instanceof Error) {
|
||||
handleError(tasks);
|
||||
} else {
|
||||
setTasks(tasks);
|
||||
}
|
||||
};
|
||||
|
||||
const markAsDone = async (task: TaskResponse) => {
|
||||
const tasks: Error | undefined = await updateTaskDoneRequest(task.id, !task.done);
|
||||
if (tasks instanceof Error) {
|
||||
handleError(tasks);
|
||||
} else {
|
||||
loadTasks();
|
||||
}
|
||||
};
|
||||
|
||||
const deleteTask = async (taskId: number) => {
|
||||
const response: Error | undefined = await deleteTaskRequest(taskId);
|
||||
if (response instanceof Error) {
|
||||
handleError(tasks);
|
||||
} else {
|
||||
loadTasks();
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadTasks();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Row className="mt-3">
|
||||
<Col xs={12}>
|
||||
<Card>
|
||||
<Card.Body>
|
||||
<Card.Title>Add task</Card.Title>
|
||||
|
||||
{formInvalid ? (
|
||||
<Alert variant={"danger"}>
|
||||
{ errorMessage }
|
||||
</Alert>
|
||||
) : null}
|
||||
|
||||
<Form noValidate validated={validated} onSubmit={handleSubmit}>
|
||||
<Form.Group className="mb-3" controlId="formBasicEmail">
|
||||
<Form.Label>Description</Form.Label>
|
||||
<Form.Control
|
||||
required
|
||||
type="test"
|
||||
name="description"
|
||||
placeholder="Enter description"
|
||||
/>
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group className="mb-3" controlId="input-url">
|
||||
<Form.Label>Additional URL</Form.Label>
|
||||
<Form.Control
|
||||
required={false}
|
||||
type="text"
|
||||
name="url"
|
||||
placeholder="Additional URL (Optional)"
|
||||
/>
|
||||
</Form.Group>
|
||||
|
||||
<Button
|
||||
variant="primary"
|
||||
type="submit"
|
||||
className="w-100"
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</Form>
|
||||
|
||||
</Card.Body>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row className="mt-3">
|
||||
<Col xs={12}>
|
||||
<Card>
|
||||
<Card.Body>
|
||||
<Card.Title>Task list</Card.Title>
|
||||
|
||||
<Table striped bordered hover>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Description</th>
|
||||
<th>Done</th>
|
||||
<th>URL</th>
|
||||
<th>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>
|
||||
<td className={task.done ? 'text-done' : ''}>{task.done? 'Yes' : 'No'}</td>
|
||||
<td className={task.done ? 'text-done' : ''}>
|
||||
{task.urls.length > 0? (
|
||||
<a href={`${task.urls[0].url}`}>Link</a>
|
||||
) : '-'}
|
||||
</td>
|
||||
<td className={task.done ? 'text-done' : ''}>
|
||||
<Button
|
||||
type="button"
|
||||
variant="link"
|
||||
onClick={() => markAsDone(task)}
|
||||
>
|
||||
{task.done ? 'Undone' : 'Done'}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="link"
|
||||
onClick={() => deleteTask(task.id)}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</Table>
|
||||
</Card.Body>
|
||||
<Card.Footer>
|
||||
{tasks.length === 0? 'No tasks' : `${tasks.length} pending task(s)`}
|
||||
</Card.Footer>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
export default Task;
|
||||
@@ -0,0 +1,7 @@
|
||||
.text-done {
|
||||
color: #ccc!important;
|
||||
}
|
||||
|
||||
.text-done .btn.btn-link {
|
||||
color: #ccc!important;
|
||||
}
|
||||
+1
-1
@@ -60,7 +60,7 @@ class TaskServiceImpl implements TaskService {
|
||||
task.setUser(user);
|
||||
TaskEntity created = taskRepository.save(task);
|
||||
|
||||
if (Objects.isNull(taskRequest.urls())) {
|
||||
if (!Objects.isNull(taskRequest.urls()) && !taskRequest.urls().isEmpty()) {
|
||||
List<TaskUrlEntity> urls = saveUrls(task, taskRequest.urls());
|
||||
task.setUrls(urls);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user