1
0
This commit is contained in:
2023-02-21 18:31:01 +01:00
parent 455cb63652
commit b6e6a94a1e
20 changed files with 495 additions and 21 deletions

View File

@@ -65,7 +65,7 @@ public interface FilesService {
} }
} }
void createAppDirectory(App app); void createAppDirectoryIfNotExists(App app);
void createDirectory(App app, Path path); void createDirectory(App app, Path path);
@@ -81,7 +81,7 @@ public interface FilesService {
* Paths in return list are always relative to the appDir. Non-recursive list. * Paths in return list are always relative to the appDir. Non-recursive list.
* *
* @param app to list the files for * @param app to list the files for
* @param path in case of {@link Optional#EMPTY} the appDir is used as start dir. If not empty, path has to be relative * @param path in case of an empty optional the appDir is used as start dir. If not empty, path has to be relative
* to the appDir * to the appDir
*/ */
List<ContentContainer> list(App app, Optional<Path> path); List<ContentContainer> list(App app, Optional<Path> path);

View File

@@ -28,7 +28,7 @@ public class FilesServiceImpl implements FilesService {
} }
@Override @Override
public void createAppDirectory(App app) { public void createAppDirectoryIfNotExists(App app) {
try { try {
this.fileSystemService.createDirectory(resolve(app, Path.of(""))); this.fileSystemService.createDirectory(resolve(app, Path.of("")));
} }

View File

@@ -4,6 +4,7 @@ import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import de.nbscloud.files.FileSystemService; import de.nbscloud.files.FileSystemService;
import de.nbscloud.files.LocationTracker; import de.nbscloud.files.LocationTracker;
import de.nbscloud.files.SessionInfo;
import de.nbscloud.files.Share; import de.nbscloud.files.Share;
import de.nbscloud.files.api.FilesService.ContentContainer; import de.nbscloud.files.api.FilesService.ContentContainer;
import de.nbscloud.files.config.FilesConfig; import de.nbscloud.files.config.FilesConfig;
@@ -12,9 +13,9 @@ import de.nbscloud.files.form.PasswordForm;
import de.nbscloud.files.form.RenameForm; import de.nbscloud.files.form.RenameForm;
import de.nbscloud.files.form.ShareForm; import de.nbscloud.files.form.ShareForm;
import de.nbscloud.webcontainer.MessageHelper; import de.nbscloud.webcontainer.MessageHelper;
import de.nbscloud.files.SessionInfo;
import de.nbscloud.webcontainer.registry.AppRegistry; import de.nbscloud.webcontainer.registry.AppRegistry;
import de.nbscloud.webcontainer.shared.config.WebContainerSharedConfig; import de.nbscloud.webcontainer.shared.config.WebContainerSharedConfig;
import de.nbscloud.webcontainer.shared.util.ControllerUtils;
import org.apache.commons.io.input.ObservableInputStream; import org.apache.commons.io.input.ObservableInputStream;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger; import org.slf4j.Logger;
@@ -43,7 +44,6 @@ import java.time.Instant;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.ZoneId; import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.UUID; import java.util.UUID;
@@ -248,11 +248,6 @@ public class FilesController implements InitializingBean {
final Path filePath = this.locationTracker.getRelativeToBaseDir(this.locationTracker.resolve(filename)); final Path filePath = this.locationTracker.getRelativeToBaseDir(this.locationTracker.resolve(filename));
final String shareUuid = UUID.randomUUID().toString(); final String shareUuid = UUID.randomUUID().toString();
final Share share = new Share(); final Share share = new Share();
// The format is always "yyyy-MM-dd", see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/date
final LocalDate expiryDate = Optional.ofNullable(expiryDateString)
.map(ed -> ed.isBlank() ? null : ed)
.map(ed -> LocalDate.parse(ed, DateTimeFormatter.ofPattern("yyyy-MM-dd")))
.orElse(null);
if(StringUtils.isEmpty(password)) { if(StringUtils.isEmpty(password)) {
password = null; // if no real password has been provided assume no password password = null; // if no real password has been provided assume no password
@@ -260,7 +255,7 @@ public class FilesController implements InitializingBean {
share.setUuid(shareUuid); share.setUuid(shareUuid);
share.setPath(filePath.toString()); share.setPath(filePath.toString());
share.setExpiryDate(expiryDate); share.setExpiryDate(ControllerUtils.parseDate(expiryDateString));
share.setOneTime(oneTime); share.setOneTime(oneTime);
share.setPassword(password); share.setPassword(password);
@@ -281,6 +276,10 @@ public class FilesController implements InitializingBean {
@GetMapping("/files/shares/files/browse/**") @GetMapping("/files/shares/files/browse/**")
public String sharesStart(Model model, HttpServletRequest httpServletRequest, String sortOrder) { public String sharesStart(Model model, HttpServletRequest httpServletRequest, String sortOrder) {
if(!this.sessionInfo.isRestrictedSession()) {
throw new IllegalStateException();
}
final String start = start(model, httpServletRequest, sortOrder); final String start = start(model, httpServletRequest, sortOrder);
model.addAttribute("prefix", "/files/shares"); model.addAttribute("prefix", "/files/shares");
@@ -301,8 +300,6 @@ public class FilesController implements InitializingBean {
final Optional<LocalDate> optExpiryDate = Optional.ofNullable(share.getExpiryDate()); final Optional<LocalDate> optExpiryDate = Optional.ofNullable(share.getExpiryDate());
final boolean expired = optExpiryDate.map(expiryDate -> LocalDate.now().isAfter(expiryDate)).orElse(false); final boolean expired = optExpiryDate.map(expiryDate -> LocalDate.now().isAfter(expiryDate)).orElse(false);
this.sessionInfo.setRestrictedSession(true);
if(share.getPassword() != null) { if(share.getPassword() != null) {
model.addAttribute("form", new PasswordForm(shareUuid, null)); model.addAttribute("form", new PasswordForm(shareUuid, null));
this.webContainerSharedConfig.addDefaults(model); this.webContainerSharedConfig.addDefaults(model);
@@ -320,6 +317,8 @@ public class FilesController implements InitializingBean {
this.locationTracker.setBaseDirPath(share.getPath()); this.locationTracker.setBaseDirPath(share.getPath());
this.locationTracker.resetCurrentLocation(); this.locationTracker.resetCurrentLocation();
this.sessionInfo.setRestrictedSession(true);
return "redirect:/files/browse/"; return "redirect:/files/browse/";
} }
else { else {
@@ -354,6 +353,8 @@ public class FilesController implements InitializingBean {
this.locationTracker.setBaseDirPath(share.getPath()); this.locationTracker.setBaseDirPath(share.getPath());
this.locationTracker.resetCurrentLocation(); this.locationTracker.resetCurrentLocation();
this.sessionInfo.setRestrictedSession(true);
return "redirect:/files/browse/"; return "redirect:/files/browse/";
} }
else { else {
@@ -408,6 +409,10 @@ public class FilesController implements InitializingBean {
@GetMapping("/files/shares/files/preview") @GetMapping("/files/shares/files/preview")
public void sharesPreview(HttpServletResponse response, String filename) { public void sharesPreview(HttpServletResponse response, String filename) {
if(!this.sessionInfo.isRestrictedSession()) {
throw new IllegalStateException();
}
preview(response, filename); preview(response, filename);
} }
@@ -426,6 +431,10 @@ public class FilesController implements InitializingBean {
@GetMapping("/files/shares/files/gallery") @GetMapping("/files/shares/files/gallery")
public String sharesGallery(Model model) { public String sharesGallery(Model model) {
if(!this.sessionInfo.isRestrictedSession()) {
throw new IllegalStateException();
}
final String gallery = gallery(model); final String gallery = gallery(model);
model.addAttribute("prefix", "/files/shares"); model.addAttribute("prefix", "/files/shares");

View File

@@ -20,7 +20,7 @@ import java.nio.file.Path;
import java.util.Optional; import java.util.Optional;
@Controller @Controller
public class NotesController implements InitializingBean { public class NotesController {
private static final Logger logger = LoggerFactory.getLogger(NotesController.class); private static final Logger logger = LoggerFactory.getLogger(NotesController.class);
@Autowired @Autowired
@@ -42,6 +42,8 @@ public class NotesController implements InitializingBean {
@GetMapping("/notes") @GetMapping("/notes")
public String start(Model model, @RequestParam(required = false) String notePath) { public String start(Model model, @RequestParam(required = false) String notePath) {
this.filesService.createAppDirectoryIfNotExists(this.app);
final Optional<Path> optNotePath = notePathToPath(notePath); final Optional<Path> optNotePath = notePathToPath(notePath);
if(optNotePath.isPresent()) { if(optNotePath.isPresent()) {
@@ -121,9 +123,4 @@ public class NotesController implements InitializingBean {
return "redirect:?notePath=" + currentPath; return "redirect:?notePath=" + currentPath;
} }
@Override
public void afterPropertiesSet() throws Exception {
//this.filesService.createAppDirectory(this.app);
}
} }

View File

@@ -31,5 +31,9 @@
<artifactId>spring-boot-starter-web</artifactId> <artifactId>spring-boot-starter-web</artifactId>
<scope>provided</scope> <scope>provided</scope>
</dependency> </dependency>
<dependency>
<groupId>de.77zzcx7.nbs-cloud</groupId>
<artifactId>files-api</artifactId>
</dependency>
</dependencies> </dependencies>
</project> </project>

View File

@@ -0,0 +1,5 @@
package de.nbscloud.todo;
public enum Category {
A, B, C;
}

View File

@@ -1,19 +1,26 @@
package de.nbscloud.todo; package de.nbscloud.todo;
import de.nbscloud.todo.widget.TodoWidget;
import de.nbscloud.webcontainer.registry.App; import de.nbscloud.webcontainer.registry.App;
import de.nbscloud.webcontainer.registry.AppRegistry; import de.nbscloud.webcontainer.registry.AppRegistry;
import de.nbscloud.webcontainer.registry.Widget;
import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import java.util.Collection;
import java.util.List;
@Component @Component
public class TodoApp implements App, InitializingBean { public class TodoApp implements App, InitializingBean {
public static final String ID = "todo";
@Autowired @Autowired
private AppRegistry appRegistry; private AppRegistry appRegistry;
@Override @Override
public String getId() { public String getId() {
return "todo"; return ID;
} }
@Override @Override
@@ -31,6 +38,11 @@ public class TodoApp implements App, InitializingBean {
return 40; return 40;
} }
@Override
public Collection<Widget> getWidgets() {
return List.of(new TodoWidget());
}
@Override @Override
public void afterPropertiesSet() throws Exception { public void afterPropertiesSet() throws Exception {
this.appRegistry.registerApp(this); this.appRegistry.registerApp(this);

View File

@@ -0,0 +1,74 @@
package de.nbscloud.todo;
import java.time.LocalDate;
import java.util.UUID;
public class TodoItem {
private String id;
private boolean done;
private String text;
private Category category;
private LocalDate added;
private LocalDate due;
public static final TodoItem create(boolean done, String text, Category category, LocalDate added, LocalDate due) {
final TodoItem item = new TodoItem();
item.setDone(done);
item.setText(text);
item.setCategory(category);
item.setAdded(added);
item.setDue(due);
item.setId(UUID.randomUUID().toString());
return item;
}
public boolean isDone() {
return done;
}
public void setDone(boolean done) {
this.done = done;
}
public String getText() {
return text;
}
public void setText(String text) {
this.text = text;
}
public Category getCategory() {
return category;
}
public void setCategory(Category category) {
this.category = category;
}
public LocalDate getAdded() {
return added;
}
public void setAdded(LocalDate added) {
this.added = added;
}
public LocalDate getDue() {
return due;
}
public void setDue(LocalDate due) {
this.due = due;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
}

View File

@@ -0,0 +1,16 @@
package de.nbscloud.todo;
import java.util.ArrayList;
import java.util.List;
public class TodoList {
private List<TodoItem> todos = new ArrayList<>();
public List<TodoItem> getTodos() {
return todos;
}
public void setTodos(List<TodoItem> todos) {
this.todos = todos;
}
}

View File

@@ -0,0 +1,159 @@
package de.nbscloud.todo.controller;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import de.nbscloud.files.api.FilesService;
import de.nbscloud.todo.Category;
import de.nbscloud.todo.TodoApp;
import de.nbscloud.todo.TodoItem;
import de.nbscloud.todo.TodoList;
import de.nbscloud.webcontainer.MessageHelper;
import de.nbscloud.webcontainer.registry.AppRegistry;
import de.nbscloud.webcontainer.shared.config.WebContainerSharedConfig;
import de.nbscloud.webcontainer.shared.util.ControllerUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import java.nio.charset.StandardCharsets;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.time.LocalDate;
import java.util.*;
import java.util.stream.Collectors;
@Controller
public class TodoController {
private static final Logger logger = LoggerFactory.getLogger(TodoController.class);
public static final Comparator<TodoItem> TODO_ITEM_COMPARATOR = Comparator.comparing(TodoItem::isDone)
.thenComparing(TodoItem::getDue,
Comparator.nullsLast(Comparator.naturalOrder()))
.thenComparing(TodoItem::getAdded);
static final Path FILENAME = Path.of("todo");
@Autowired
private AppRegistry appRegistry;
@Autowired
private FilesService filesService;
@Autowired
private WebContainerSharedConfig webContainerSharedConfig;
@Autowired
private TodoApp app;
@Autowired
private MessageHelper messageHelper;
@Autowired
private ObjectMapper mapper;
@GetMapping("/todo")
public String start(Model model) {
try {
this.filesService.createAppDirectoryIfNotExists(this.app);
final String todoListJson = new String(this.filesService.get(app, FILENAME));
final TodoList todos = mapper.readValue(todoListJson, TodoList.class);
final Map<Category, List<TodoItem>> categoryMap = new TreeMap<>(Comparator.comparing(Category::name));
todos.getTodos().stream().collect(Collectors.groupingBy(TodoItem::getCategory, () -> categoryMap, Collectors.toList()));
if(categoryMap.containsKey(Category.A)) {
Collections.sort(categoryMap.get(Category.A), TODO_ITEM_COMPARATOR);
}
if(categoryMap.containsKey(Category.B)) {
Collections.sort(categoryMap.get(Category.B), TODO_ITEM_COMPARATOR);
}
if(categoryMap.containsKey(Category.C)) {
Collections.sort(categoryMap.get(Category.C), TODO_ITEM_COMPARATOR);
}
model.addAttribute("todosByCategory", categoryMap);
}
catch(JsonProcessingException e) {
logger.error("Error while parsing todo file", e);
this.messageHelper.addError(e.getMessage());
}
catch(RuntimeException re) {
if(re.getCause() instanceof NoSuchFileException) {
logger.debug("Todo file does not exist, create it");
try {
this.filesService.createFile(app, FILENAME, mapper.writeValueAsString(new TodoList()).getBytes(StandardCharsets.UTF_8));
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}
else {
logger.error("Error while reading todo file", re);
this.messageHelper.addError(re.getMessage());
}
}
model.addAttribute("categories", Category.values());
this.messageHelper.addAndClearAll(model);
model.addAttribute("apps", this.appRegistry.getAll());
this.webContainerSharedConfig.addDefaults(model);
return "todo/todoIndex";
}
@PostMapping("/todo/addItem")
public String addItem(Model model, String text, Category category, String due) {
try {
final String todoListJson = new String(this.filesService.get(app, FILENAME));
final TodoList todos = mapper.readValue(todoListJson, TodoList.class);
todos.getTodos().add(TodoItem.create(false, text, category, LocalDate.now(), ControllerUtils.parseDate(due)));
this.filesService.overwriteFile(app, FILENAME, mapper.writeValueAsBytes(todos));
this.messageHelper.addInfo("nbscloud.todo.item.add");
} catch (JsonProcessingException e) {
logger.error("Error", e);
this.messageHelper.addError(e.getMessage());
}
model.addAttribute("categories", Category.values());
model.addAttribute("apps", this.appRegistry.getAll());
this.webContainerSharedConfig.addDefaults(model);
return "redirect:";
}
@PostMapping("/todo/toggle")
public String toggle(Model model, String id, boolean done) {
try {
final String todoListJson = new String(this.filesService.get(app, FILENAME));
final TodoList todos = mapper.readValue(todoListJson, TodoList.class);
todos.getTodos().stream().filter(item -> item.getId().equals(id)).findFirst().ifPresent(item -> item.setDone(done));
this.filesService.overwriteFile(app, FILENAME, mapper.writeValueAsBytes(todos));
if(done) {
this.messageHelper.addInfo("nbscloud.todo.item.toggle.done");
}
else {
this.messageHelper.addInfo("nbscloud.todo.item.toggle.inprogress");
}
} catch (JsonProcessingException e) {
logger.error("Error", e);
this.messageHelper.addError(e.getMessage());
}
model.addAttribute("categories", Category.values());
model.addAttribute("apps", this.appRegistry.getAll());
this.webContainerSharedConfig.addDefaults(model);
return "redirect:";
}
}

View File

@@ -0,0 +1,58 @@
package de.nbscloud.todo.controller;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import de.nbscloud.files.api.FilesService;
import de.nbscloud.todo.Category;
import de.nbscloud.todo.TodoApp;
import de.nbscloud.todo.TodoItem;
import de.nbscloud.todo.TodoList;
import de.nbscloud.webcontainer.shared.config.WebContainerSharedConfig;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.*;
import java.util.stream.Collectors;
@Controller
public class TodoWidgetController {
private static final Logger logger = LoggerFactory.getLogger(TodoWidgetController.class);
@Autowired
private WebContainerSharedConfig webContainerSharedConfig;
@Autowired
private FilesService filesService;
@Autowired
private TodoApp app;
@Autowired
private ObjectMapper mapper;
@GetMapping("todo/widgets/todoOverview")
public String getTodoOverview(HttpServletRequest request, HttpServletResponse response, Model model) {
try {
this.filesService.createAppDirectoryIfNotExists(this.app);
final String todoListJson = new String(this.filesService.get(app, TodoController.FILENAME));
final TodoList todos = mapper.readValue(todoListJson, TodoList.class);
final List<TodoItem> topFiveTodos = todos.getTodos().stream().filter(item -> !item.isDone()).sorted(
Comparator.comparing(TodoItem::getDue,
Comparator.nullsLast(Comparator.naturalOrder()))
.thenComparing(TodoItem::getCategory)
.thenComparing(TodoItem::getAdded)).limit(5).collect(Collectors.toList());
model.addAttribute("todos", topFiveTodos);
}
catch(JsonProcessingException e) {
logger.error("Error while parsing todo file", e);
}
return "todo/widgets/todoOverview :: todo-overview";
}
}

View File

@@ -0,0 +1,11 @@
package de.nbscloud.todo.widget;
import de.nbscloud.todo.TodoApp;
import de.nbscloud.webcontainer.registry.Widget;
public class TodoWidget implements Widget {
@Override
public String getPath() {
return TodoApp.ID + "/widgets/todoOverview";
}
}

View File

@@ -0,0 +1,7 @@
nbscloud.todo.index.title=nbscloud - todo
nbscloud.todo.widget.heading=todo overview
nbscloud.todo.item.add=Todo added
nbscloud.todo.item.toggle.done=Done
nbscloud.todo.item.toggle.inprogress=In progress

View File

@@ -0,0 +1,7 @@
nbscloud.todo.index.title=nbscloud - todo
nbscloud.todo.widget.heading=todo \u00FCbersicht
nbscloud.todo.item.add=Todo hinzugef\u00FCgt
nbscloud.todo.item.toggle.done=Fertig
nbscloud.todo.item.toggle.inprogress=Unfertig

View File

@@ -0,0 +1,24 @@
#content-container {
display: flex;
flex-direction: row;
flex-wrap: wrap;
row-gap: 2em;
column-gap: 2em;
}
.todo-category {
flex-grow: 1;
background-color:var(--background-color-highlight);
padding: 1em;
align-self: stretch;
word-break: break-all;
flex-basis: 100%;
}
.todo-category-header {
text-decoration-line: underline;
}
.todo-item > td:nth-child(2) {
padding-inline: 3em;
}

View File

@@ -0,0 +1,24 @@
<div id="menu-container" th:fragment="menu">
<div class="menu-spacer"></div>
<div id="add-item-container" class="menu-container">
<details>
<summary>
<span id="add-item-container-span-input" class="icon menu-icon">&#xf23a;</span>
</summary>
<div class="menu-modal">
<div class="menu-modal-content">
<form method="POST" action="#" th:action="@{/todo/addItem}" enctype="multipart/form-data">
<input id="add-item-container-text" type="text" name="text" />
<select size="1" id="add-item-category" name="category">
<option th:each="category : ${categories}"
th:value="${category}"
th:text="${category}" />
</select>
<input type="date" id="add-item-due" name="due"/>
<input type="submit" value="Add"/>
</form>
</div>
</div>
</details>
</div>
</div>

View File

@@ -0,0 +1,39 @@
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title th:text="#{nbscloud.todo.index.title}"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link th:if="${darkMode}" rel="stylesheet" th:href="@{/css/darkModeColors.css}"/>
<link th:if="${!darkMode}" rel="stylesheet" th:href="@{/css/lightModeColors.css}"/>
<link rel="stylesheet" th:href="@{/css/main.css}"/>
<link rel="stylesheet" th:href="@{/css/todo_main.css}"/>
<link rel="shortcut icon" th:href="@{/favicon.ico}"/>
</head>
<body>
<div id="main-container">
<div th:replace="includes/header :: header"/>
<div th:replace="includes/messages :: messages"/>
<div th:replace="todo/includes/menu :: menu"/>
<div id="content-container">
<div class="todo-category" th:each="entry : ${todosByCategory}">
<p th:text="${#strings.prepend(#strings.append(entry.key, #strings.repeat('&nbsp;', 10)), '&nbsp;')}"
class="todo-category-header"/>
<table id="todo-item-table">
<tr class="todo-item" th:each="todo : ${entry.value}">
<td>
<form method="POST" th:action="@{/todo/toggle}" enctype="multipart/form-data">
<input type="checkbox" th:checked="${todo.done}" name="done" onClick="this.form.submit()" /> <!-- fucking javascript -->
<input type="hidden" name="id" th:value="${todo.id}" class="display-none"/>
</form>
</td>
<td th:text="${todo.text}"/>
<td th:text="${todo.due}"/>
</tr>
</table>
</div>
</div>
<div th:replace="includes/footer :: footer"/>
</div>
</body>
</html>

View File

@@ -0,0 +1,12 @@
<div id="todo-widget" class="widget" th:fragment="todo-overview">
<p class="widget-heading" th:text="#{nbscloud.todo.widget.heading}"/>
<table id="todo-widget-table">
<tbody>
<tr th:each="todo : ${todos}">
<td th:text="${todo.category}"/>
<td th:text="${todo.text}"/>
<td th:text="${todo.due}"/>
</tr>
</tbody>
</table>
</div>

View File

@@ -0,0 +1,16 @@
package de.nbscloud.webcontainer.shared.util;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.Optional;
public class ControllerUtils {
public static LocalDate parseDate(String date) {
// The format is always "yyyy-MM-dd", see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/date
return Optional.ofNullable(date)
.map(ed -> ed.isBlank() ? null : ed)
.map(ed -> LocalDate.parse(ed, DateTimeFormatter.ofPattern("yyyy-MM-dd")))
.orElse(null);
}
}

View File

@@ -9,7 +9,7 @@ info.build.group=@project.groupId@
info.build.artifact=@project.artifactId@ info.build.artifact=@project.artifactId@
info.build.version=@project.version@ info.build.version=@project.version@
spring.messages.basename=i18n/container_messages,i18n/files_messages,i18n/dashboard_messages,i18n/notes_messages spring.messages.basename=i18n/container_messages,i18n/files_messages,i18n/dashboard_messages,i18n/notes_messages,i18n/todo_messages
spring.servlet.multipart.max-file-size=-1 spring.servlet.multipart.max-file-size=-1
spring.servlet.multipart.max-request-size=-1 spring.servlet.multipart.max-request-size=-1