Generated project

This commit is contained in:
start.vaadin.com
2025-04-03 10:08:22 +00:00
commit e0e60be428
30 changed files with 1565 additions and 0 deletions

View File

@@ -0,0 +1,23 @@
<!DOCTYPE html>
<!--
This file is auto-generated by Vaadin.
-->
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style>
body, #outlet {
height: 100vh;
width: 100%;
margin: 0;
}
</style>
<!-- index.ts is included here automatically (either by the dev server or during the build) -->
</head>
<body>
<!-- This outlet div is where the views are rendered -->
<div id="outlet"></div>
</body>
</html>

View File

@@ -0,0 +1,9 @@
{
"lumoImports": [
"typography",
"color",
"spacing",
"badge",
"utility"
]
}

View File

@@ -0,0 +1,24 @@
package de.assecutor.votianlt;
import com.vaadin.flow.component.page.AppShellConfigurator;
import com.vaadin.flow.theme.Theme;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import java.time.Clock;
@SpringBootApplication
@Theme("default")
public class Application implements AppShellConfigurator {
@Bean
public Clock clock() {
return Clock.systemDefaultZone(); // You can also use Clock.systemUTC()
}
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}

View File

@@ -0,0 +1,44 @@
package de.assecutor.votianlt.base.domain;
import jakarta.persistence.MappedSuperclass;
import org.jspecify.annotations.Nullable;
import org.springframework.data.util.ProxyUtils;
@MappedSuperclass
public abstract class AbstractEntity<ID> {
public abstract @Nullable ID getId();
@Override
public String toString() {
return "%s{id=%s}".formatted(getClass().getSimpleName(), getId());
}
@Override
public int hashCode() {
// Hashcode should never change during the lifetime of an object. Because of
// this we can't use getId() to calculate the hashcode. Unless you have sets
// with lots of entities in them, returning the same hashcode should not be a
// problem.
return ProxyUtils.getUserClass(getClass()).hashCode();
}
@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
} else if (obj == this) {
return true;
}
var thisUserClass = ProxyUtils.getUserClass(getClass());
var otherUserClass = ProxyUtils.getUserClass(obj);
if (thisUserClass != otherUserClass) {
return false;
}
var id = getId();
return id != null && id.equals(((AbstractEntity<?>) obj).getId());
}
}

View File

@@ -0,0 +1,4 @@
@NullMarked
package de.assecutor.votianlt.base.domain;
import org.jspecify.annotations.NullMarked;

View File

@@ -0,0 +1,41 @@
package de.assecutor.votianlt.base.ui.component;
import com.vaadin.flow.component.Component;
import com.vaadin.flow.component.Composite;
import com.vaadin.flow.component.applayout.DrawerToggle;
import com.vaadin.flow.component.html.Div;
import com.vaadin.flow.component.html.H1;
import com.vaadin.flow.component.html.Header;
import com.vaadin.flow.theme.lumo.LumoUtility.*;
public final class ViewToolbar extends Composite<Header> {
public ViewToolbar(String viewTitle, Component... components) {
addClassNames(Display.FLEX, FlexDirection.COLUMN, JustifyContent.BETWEEN, AlignItems.STRETCH, Gap.MEDIUM,
FlexDirection.Breakpoint.Medium.ROW, AlignItems.Breakpoint.Medium.CENTER);
var drawerToggle = new DrawerToggle();
drawerToggle.addClassNames(Margin.NONE);
var title = new H1(viewTitle);
title.addClassNames(FontSize.XLARGE, Margin.NONE, FontWeight.LIGHT);
var toggleAndTitle = new Div(drawerToggle, title);
toggleAndTitle.addClassNames(Display.FLEX, AlignItems.CENTER);
getContent().add(toggleAndTitle);
if (components.length > 0) {
var actions = new Div(components);
actions.addClassNames(Display.FLEX, FlexDirection.COLUMN, JustifyContent.BETWEEN, Flex.GROW, Gap.SMALL,
FlexDirection.Breakpoint.Medium.ROW);
getContent().add(actions);
}
}
public static Component group(Component... components) {
var group = new Div(components);
group.addClassNames(Display.FLEX, FlexDirection.COLUMN, AlignItems.STRETCH, Gap.SMALL,
FlexDirection.Breakpoint.Medium.ROW, AlignItems.Breakpoint.Medium.CENTER);
return group;
}
}

View File

@@ -0,0 +1,4 @@
@NullMarked
package de.assecutor.votianlt.base.ui.component;
import org.jspecify.annotations.NullMarked;

View File

@@ -0,0 +1,32 @@
package de.assecutor.votianlt.base.ui.view;
import com.vaadin.flow.component.Component;
import com.vaadin.flow.component.notification.Notification;
import com.vaadin.flow.component.notification.NotificationVariant;
import com.vaadin.flow.server.VaadinServiceInitListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
class MainErrorHandler {
private static final Logger log = LoggerFactory.getLogger(MainErrorHandler.class);
@Bean
public VaadinServiceInitListener errorHandlerInitializer() {
return (event) -> event.getSource().addSessionInitListener(
sessionInitEvent -> sessionInitEvent.getSession().setErrorHandler(errorEvent -> {
log.error("An unexpected error occurred", errorEvent.getThrowable());
errorEvent.getComponent().flatMap(Component::getUI).ifPresent(ui -> {
var notification = new Notification(
"An unexpected error has occurred. Please try again later.");
notification.addThemeVariants(NotificationVariant.LUMO_ERROR);
notification.setPosition(Notification.Position.TOP_CENTER);
notification.setDuration(3000);
ui.access(notification::open);
});
}));
}
}

View File

@@ -0,0 +1,78 @@
package de.assecutor.votianlt.base.ui.view;
import com.vaadin.flow.component.Component;
import com.vaadin.flow.component.applayout.AppLayout;
import com.vaadin.flow.component.avatar.Avatar;
import com.vaadin.flow.component.avatar.AvatarVariant;
import com.vaadin.flow.component.html.Div;
import com.vaadin.flow.component.html.Span;
import com.vaadin.flow.component.icon.Icon;
import com.vaadin.flow.component.icon.VaadinIcon;
import com.vaadin.flow.component.menubar.MenuBar;
import com.vaadin.flow.component.menubar.MenuBarVariant;
import com.vaadin.flow.component.orderedlayout.Scroller;
import com.vaadin.flow.component.sidenav.SideNav;
import com.vaadin.flow.component.sidenav.SideNavItem;
import com.vaadin.flow.router.Layout;
import com.vaadin.flow.server.menu.MenuConfiguration;
import com.vaadin.flow.server.menu.MenuEntry;
import static com.vaadin.flow.theme.lumo.LumoUtility.*;
@Layout
public final class MainLayout extends AppLayout {
public MainLayout() {
setPrimarySection(Section.DRAWER);
addToDrawer(createHeader(), new Scroller(createSideNav()), createUserMenu());
}
private Div createHeader() {
// TODO Replace with real application logo and name
var appLogo = VaadinIcon.CUBES.create();
appLogo.addClassNames(TextColor.PRIMARY, IconSize.LARGE);
var appName = new Span("Votianlt");
appName.addClassNames(FontWeight.SEMIBOLD, FontSize.LARGE);
var header = new Div(appLogo, appName);
header.addClassNames(Display.FLEX, Padding.MEDIUM, Gap.MEDIUM, AlignItems.CENTER);
return header;
}
private SideNav createSideNav() {
var nav = new SideNav();
nav.addClassNames(Margin.Horizontal.MEDIUM);
MenuConfiguration.getMenuEntries().forEach(entry -> nav.addItem(createSideNavItem(entry)));
return nav;
}
private SideNavItem createSideNavItem(MenuEntry menuEntry) {
if (menuEntry.icon() != null) {
return new SideNavItem(menuEntry.title(), menuEntry.path(), new Icon(menuEntry.icon()));
} else {
return new SideNavItem(menuEntry.title(), menuEntry.path());
}
}
private Component createUserMenu() {
// TODO Replace with real user information and actions
var avatar = new Avatar("John Smith");
avatar.addThemeVariants(AvatarVariant.LUMO_XSMALL);
avatar.addClassNames(Margin.Right.SMALL);
avatar.setColorIndex(5);
var userMenu = new MenuBar();
userMenu.addThemeVariants(MenuBarVariant.LUMO_TERTIARY_INLINE);
userMenu.addClassNames(Margin.MEDIUM);
var userMenuItem = userMenu.addItem(avatar);
userMenuItem.add("John Smith");
userMenuItem.getSubMenu().addItem("View Profile");
userMenuItem.getSubMenu().addItem("Manage Settings");
userMenuItem.getSubMenu().addItem("Logout");
return userMenu;
}
}

View File

@@ -0,0 +1,4 @@
@NullMarked
package de.assecutor.votianlt.base.ui.view;
import org.jspecify.annotations.NullMarked;

View File

@@ -0,0 +1,61 @@
package de.assecutor.votianlt.todo.domain;
import de.assecutor.votianlt.base.domain.AbstractEntity;
import jakarta.persistence.*;
import jakarta.validation.constraints.Size;
import org.jspecify.annotations.Nullable;
import java.time.Instant;
import java.time.LocalDate;
@Entity
@Table(name = "todo")
public class Todo extends AbstractEntity<Long> {
public static final int DESCRIPTION_MAX_LENGTH = 255;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "todo_id")
private Long id;
@Column(name = "description", nullable = false, length = DESCRIPTION_MAX_LENGTH)
@Size(max = DESCRIPTION_MAX_LENGTH)
private String description;
@Column(name = "creation_date", nullable = false)
private Instant creationDate;
@Column(name = "due_date")
@Nullable
private LocalDate dueDate;
@Override
public @Nullable Long getId() {
return id;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public Instant getCreationDate() {
return creationDate;
}
public void setCreationDate(Instant creationDate) {
this.creationDate = creationDate;
}
public @Nullable LocalDate getDueDate() {
return dueDate;
}
public void setDueDate(@Nullable LocalDate dueDate) {
this.dueDate = dueDate;
}
}

View File

@@ -0,0 +1,12 @@
package de.assecutor.votianlt.todo.domain;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Slice;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
public interface TodoRepository extends JpaRepository<Todo, Long>, JpaSpecificationExecutor<Todo> {
// If you don't need a total row count, Slice is better than Page.
Slice<Todo> findAllBy(Pageable pageable);
}

View File

@@ -0,0 +1,4 @@
@NullMarked
package de.assecutor.votianlt.todo.domain;
import org.jspecify.annotations.NullMarked;

View File

@@ -0,0 +1,43 @@
package de.assecutor.votianlt.todo.service;
import de.assecutor.votianlt.todo.domain.Todo;
import de.assecutor.votianlt.todo.domain.TodoRepository;
import org.jspecify.annotations.Nullable;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import java.time.Clock;
import java.time.LocalDate;
import java.util.List;
@Service
@Transactional(propagation = Propagation.REQUIRES_NEW)
public class TodoService {
private final TodoRepository todoRepository;
private final Clock clock;
TodoService(TodoRepository todoRepository, Clock clock) {
this.todoRepository = todoRepository;
this.clock = clock;
}
public void createTodo(String description, @Nullable LocalDate dueDate) {
if ("fail".equals(description)) {
throw new RuntimeException("This is for testing the error handler");
}
var todo = new Todo();
todo.setDescription(description);
todo.setCreationDate(clock.instant());
todo.setDueDate(dueDate);
todoRepository.saveAndFlush(todo);
}
public List<Todo> list(Pageable pageable) {
return todoRepository.findAllBy(pageable).toList();
}
}

View File

@@ -0,0 +1,4 @@
@NullMarked
package de.assecutor.votianlt.todo.service;
import org.jspecify.annotations.NullMarked;

View File

@@ -0,0 +1,83 @@
package de.assecutor.votianlt.todo.ui.view;
import de.assecutor.votianlt.base.ui.component.ViewToolbar;
import de.assecutor.votianlt.todo.domain.Todo;
import de.assecutor.votianlt.todo.service.TodoService;
import com.vaadin.flow.component.button.Button;
import com.vaadin.flow.component.button.ButtonVariant;
import com.vaadin.flow.component.datepicker.DatePicker;
import com.vaadin.flow.component.grid.Grid;
import com.vaadin.flow.component.html.Main;
import com.vaadin.flow.component.notification.Notification;
import com.vaadin.flow.component.notification.NotificationVariant;
import com.vaadin.flow.component.textfield.TextField;
import com.vaadin.flow.router.Menu;
import com.vaadin.flow.router.PageTitle;
import com.vaadin.flow.router.Route;
import com.vaadin.flow.theme.lumo.LumoUtility;
import java.time.Clock;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
import java.util.Optional;
import static com.vaadin.flow.spring.data.VaadinSpringDataHelpers.toSpringPageRequest;
@Route("")
@PageTitle("Task List")
@Menu(order = 0, icon = "vaadin:clipboard-check", title = "Task List")
public class TodoView extends Main {
private final TodoService todoService;
final TextField description;
final DatePicker dueDate;
final Button createBtn;
final Grid<Todo> todoGrid;
public TodoView(TodoService todoService, Clock clock) {
this.todoService = todoService;
description = new TextField();
description.setPlaceholder("What do you want to do?");
description.setAriaLabel("Task description");
description.setMaxLength(Todo.DESCRIPTION_MAX_LENGTH);
description.setMinWidth("20em");
dueDate = new DatePicker();
dueDate.setPlaceholder("Due date");
dueDate.setAriaLabel("Due date");
createBtn = new Button("Create", event -> createTodo());
createBtn.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
var dateTimeFormatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM).withZone(clock.getZone())
.withLocale(getLocale());
var dateFormatter = DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM).withLocale(getLocale());
todoGrid = new Grid<>();
todoGrid.setItems(query -> todoService.list(toSpringPageRequest(query)).stream());
todoGrid.addColumn(Todo::getDescription).setHeader("Description");
todoGrid.addColumn(todo -> Optional.ofNullable(todo.getDueDate()).map(dateFormatter::format).orElse("Never"))
.setHeader("Due Date");
todoGrid.addColumn(todo -> dateTimeFormatter.format(todo.getCreationDate())).setHeader("Creation Date");
todoGrid.setSizeFull();
setSizeFull();
addClassNames(LumoUtility.BoxSizing.BORDER, LumoUtility.Display.FLEX, LumoUtility.FlexDirection.COLUMN,
LumoUtility.Padding.MEDIUM, LumoUtility.Gap.SMALL);
add(new ViewToolbar("Task List", ViewToolbar.group(description, dueDate, createBtn)));
add(todoGrid);
}
private void createTodo() {
todoService.createTodo(description.getValue(), dueDate.getValue());
todoGrid.getDataProvider().refreshAll();
description.clear();
dueDate.clear();
Notification.show("Task added", 3000, Notification.Position.BOTTOM_END)
.addThemeVariants(NotificationVariant.LUMO_SUCCESS);
}
}

View File

@@ -0,0 +1,4 @@
@NullMarked
package de.assecutor.votianlt.todo.ui.view;
import org.jspecify.annotations.NullMarked;

View File

@@ -0,0 +1,12 @@
server.port=${PORT:8080}
logging.level.org.atmosphere=warn
spring.mustache.check-template-location=false
# Launch the default browser when starting the application in development mode
vaadin.launch-browser=true
# To improve the performance during development.
# For more information https://vaadin.com/docs/latest/flow/integrations/spring/configuration#special-configuration-parameters
vaadin.allowed-packages=com.vaadin,org.vaadin,de.assecutor.votianlt
# Open-in-view is only needed if you use lazy-loaded entities in your Flow views.
spring.jpa.open-in-view=false