#24 todo app
This commit is contained in:
@@ -65,7 +65,7 @@ public interface FilesService {
|
||||
}
|
||||
}
|
||||
|
||||
void createAppDirectory(App app);
|
||||
void createAppDirectoryIfNotExists(App app);
|
||||
|
||||
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.
|
||||
*
|
||||
* @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
|
||||
*/
|
||||
List<ContentContainer> list(App app, Optional<Path> path);
|
||||
|
||||
@@ -28,7 +28,7 @@ public class FilesServiceImpl implements FilesService {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void createAppDirectory(App app) {
|
||||
public void createAppDirectoryIfNotExists(App app) {
|
||||
try {
|
||||
this.fileSystemService.createDirectory(resolve(app, Path.of("")));
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import de.nbscloud.files.FileSystemService;
|
||||
import de.nbscloud.files.LocationTracker;
|
||||
import de.nbscloud.files.SessionInfo;
|
||||
import de.nbscloud.files.Share;
|
||||
import de.nbscloud.files.api.FilesService.ContentContainer;
|
||||
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.ShareForm;
|
||||
import de.nbscloud.webcontainer.MessageHelper;
|
||||
import de.nbscloud.files.SessionInfo;
|
||||
import de.nbscloud.webcontainer.registry.AppRegistry;
|
||||
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.lang3.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
@@ -43,7 +44,6 @@ import java.time.Instant;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
@@ -248,11 +248,6 @@ public class FilesController implements InitializingBean {
|
||||
final Path filePath = this.locationTracker.getRelativeToBaseDir(this.locationTracker.resolve(filename));
|
||||
final String shareUuid = UUID.randomUUID().toString();
|
||||
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)) {
|
||||
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.setPath(filePath.toString());
|
||||
share.setExpiryDate(expiryDate);
|
||||
share.setExpiryDate(ControllerUtils.parseDate(expiryDateString));
|
||||
share.setOneTime(oneTime);
|
||||
share.setPassword(password);
|
||||
|
||||
@@ -281,6 +276,10 @@ public class FilesController implements InitializingBean {
|
||||
|
||||
@GetMapping("/files/shares/files/browse/**")
|
||||
public String sharesStart(Model model, HttpServletRequest httpServletRequest, String sortOrder) {
|
||||
if(!this.sessionInfo.isRestrictedSession()) {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
|
||||
final String start = start(model, httpServletRequest, sortOrder);
|
||||
|
||||
model.addAttribute("prefix", "/files/shares");
|
||||
@@ -301,8 +300,6 @@ public class FilesController implements InitializingBean {
|
||||
final Optional<LocalDate> optExpiryDate = Optional.ofNullable(share.getExpiryDate());
|
||||
final boolean expired = optExpiryDate.map(expiryDate -> LocalDate.now().isAfter(expiryDate)).orElse(false);
|
||||
|
||||
this.sessionInfo.setRestrictedSession(true);
|
||||
|
||||
if(share.getPassword() != null) {
|
||||
model.addAttribute("form", new PasswordForm(shareUuid, null));
|
||||
this.webContainerSharedConfig.addDefaults(model);
|
||||
@@ -320,6 +317,8 @@ public class FilesController implements InitializingBean {
|
||||
this.locationTracker.setBaseDirPath(share.getPath());
|
||||
this.locationTracker.resetCurrentLocation();
|
||||
|
||||
this.sessionInfo.setRestrictedSession(true);
|
||||
|
||||
return "redirect:/files/browse/";
|
||||
}
|
||||
else {
|
||||
@@ -354,6 +353,8 @@ public class FilesController implements InitializingBean {
|
||||
this.locationTracker.setBaseDirPath(share.getPath());
|
||||
this.locationTracker.resetCurrentLocation();
|
||||
|
||||
this.sessionInfo.setRestrictedSession(true);
|
||||
|
||||
return "redirect:/files/browse/";
|
||||
}
|
||||
else {
|
||||
@@ -408,6 +409,10 @@ public class FilesController implements InitializingBean {
|
||||
|
||||
@GetMapping("/files/shares/files/preview")
|
||||
public void sharesPreview(HttpServletResponse response, String filename) {
|
||||
if(!this.sessionInfo.isRestrictedSession()) {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
|
||||
preview(response, filename);
|
||||
}
|
||||
|
||||
@@ -426,6 +431,10 @@ public class FilesController implements InitializingBean {
|
||||
|
||||
@GetMapping("/files/shares/files/gallery")
|
||||
public String sharesGallery(Model model) {
|
||||
if(!this.sessionInfo.isRestrictedSession()) {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
|
||||
final String gallery = gallery(model);
|
||||
|
||||
model.addAttribute("prefix", "/files/shares");
|
||||
|
||||
@@ -20,7 +20,7 @@ import java.nio.file.Path;
|
||||
import java.util.Optional;
|
||||
|
||||
@Controller
|
||||
public class NotesController implements InitializingBean {
|
||||
public class NotesController {
|
||||
private static final Logger logger = LoggerFactory.getLogger(NotesController.class);
|
||||
|
||||
@Autowired
|
||||
@@ -42,6 +42,8 @@ public class NotesController implements InitializingBean {
|
||||
|
||||
@GetMapping("/notes")
|
||||
public String start(Model model, @RequestParam(required = false) String notePath) {
|
||||
this.filesService.createAppDirectoryIfNotExists(this.app);
|
||||
|
||||
final Optional<Path> optNotePath = notePathToPath(notePath);
|
||||
|
||||
if(optNotePath.isPresent()) {
|
||||
@@ -121,9 +123,4 @@ public class NotesController implements InitializingBean {
|
||||
|
||||
return "redirect:?notePath=" + currentPath;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterPropertiesSet() throws Exception {
|
||||
//this.filesService.createAppDirectory(this.app);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,5 +31,9 @@
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>de.77zzcx7.nbs-cloud</groupId>
|
||||
<artifactId>files-api</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
5
todo/src/main/java/de/nbscloud/todo/Category.java
Normal file
5
todo/src/main/java/de/nbscloud/todo/Category.java
Normal file
@@ -0,0 +1,5 @@
|
||||
package de.nbscloud.todo;
|
||||
|
||||
public enum Category {
|
||||
A, B, C;
|
||||
}
|
||||
@@ -1,19 +1,26 @@
|
||||
package de.nbscloud.todo;
|
||||
|
||||
import de.nbscloud.todo.widget.TodoWidget;
|
||||
import de.nbscloud.webcontainer.registry.App;
|
||||
import de.nbscloud.webcontainer.registry.AppRegistry;
|
||||
import de.nbscloud.webcontainer.registry.Widget;
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
@Component
|
||||
public class TodoApp implements App, InitializingBean {
|
||||
|
||||
public static final String ID = "todo";
|
||||
@Autowired
|
||||
private AppRegistry appRegistry;
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return "todo";
|
||||
return ID;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -31,6 +38,11 @@ public class TodoApp implements App, InitializingBean {
|
||||
return 40;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Widget> getWidgets() {
|
||||
return List.of(new TodoWidget());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterPropertiesSet() throws Exception {
|
||||
this.appRegistry.registerApp(this);
|
||||
|
||||
74
todo/src/main/java/de/nbscloud/todo/TodoItem.java
Normal file
74
todo/src/main/java/de/nbscloud/todo/TodoItem.java
Normal 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;
|
||||
}
|
||||
}
|
||||
16
todo/src/main/java/de/nbscloud/todo/TodoList.java
Normal file
16
todo/src/main/java/de/nbscloud/todo/TodoList.java
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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:";
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
11
todo/src/main/java/de/nbscloud/todo/widget/TodoWidget.java
Normal file
11
todo/src/main/java/de/nbscloud/todo/widget/TodoWidget.java
Normal 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";
|
||||
}
|
||||
}
|
||||
7
todo/src/main/resources/i18n/todo_messages.properties
Normal file
7
todo/src/main/resources/i18n/todo_messages.properties
Normal 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
|
||||
@@ -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
|
||||
24
todo/src/main/resources/static/css/todo_main.css
Normal file
24
todo/src/main/resources/static/css/todo_main.css
Normal 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;
|
||||
}
|
||||
24
todo/src/main/resources/templates/todo/includes/menu.html
Normal file
24
todo/src/main/resources/templates/todo/includes/menu.html
Normal 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"></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>
|
||||
39
todo/src/main/resources/templates/todo/todoIndex.html
Normal file
39
todo/src/main/resources/templates/todo/todoIndex.html
Normal 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(' ', 10)), ' ')}"
|
||||
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>
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,7 @@ info.build.group=@project.groupId@
|
||||
info.build.artifact=@project.artifactId@
|
||||
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-request-size=-1
|
||||
|
||||
Reference in New Issue
Block a user