Generated project

This commit is contained in:
start.vaadin.com
2026-08-08 10:23:47 +00:00
commit 3257c0443d
24 changed files with 1220 additions and 0 deletions
@@ -0,0 +1,21 @@
package com.example;
import com.vaadin.flow.theme.aura.Aura;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import com.vaadin.flow.component.dependency.StyleSheet;
import com.vaadin.flow.component.page.AppShellConfigurator;
import com.vaadin.flow.component.page.Push;
@SpringBootApplication
@StyleSheet(Aura.STYLESHEET)
@StyleSheet("styles.css") // Your custom styles
@Push
public class Application implements AppShellConfigurator {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
@@ -0,0 +1,74 @@
package com.example.base.ui;
import com.vaadin.flow.component.Component;
import com.vaadin.flow.component.Unit;
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.Span;
import com.vaadin.flow.component.icon.Icon;
import com.vaadin.flow.component.icon.SvgIcon;
import com.vaadin.flow.component.orderedlayout.*;
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;
@Layout
public final class MainLayout extends AppLayout {
MainLayout() {
setPrimarySection(Section.DRAWER);
addToDrawer(createApplicationHeader(), createApplicationDrawer(), createApplicationFooter());
}
private Component createApplicationHeader() {
// TODO Replace with real application logo and name
var appLogo = new Avatar("My Application");
appLogo.addClassName("app-logo");
appLogo.addThemeVariants(AvatarVariant.AURA_FILLED, AvatarVariant.XSMALL);
var appName = new Span("My Application");
appName.addClassName("app-name");
var header = new HorizontalLayout(appLogo, appName);
header.setAlignItems(FlexComponent.Alignment.CENTER);
header.setPadding(true);
return header;
}
private Component createApplicationDrawer() {
var scroller = new Scroller(createSideNav());
scroller.addThemeVariants(ScrollerVariant.OVERFLOW_INDICATORS);
return scroller;
}
private Component createApplicationFooter() {
var footer = new VerticalLayout(new Span("Made with ❤️ with Vaadin"));
footer.setAlignItems(FlexComponent.Alignment.CENTER);
footer.addClassName("app-footer");
return footer;
}
private SideNav createSideNav() {
var nav = new SideNav();
nav.setMinWidth(200, Unit.PIXELS);
MenuConfiguration.getMenuEntries().forEach(entry -> nav.addItem(createSideNavItem(entry)));
return nav;
}
private SideNavItem createSideNavItem(MenuEntry menuEntry) {
if (menuEntry.icon() != null) {
Component icon = null;
if (menuEntry.icon().contains(".svg")) {
icon = new SvgIcon(menuEntry.icon());
} else {
icon = new Icon(menuEntry.icon());
}
return new SideNavItem(menuEntry.title(), menuEntry.menuClass(), icon);
} else {
return new SideNavItem(menuEntry.title(), menuEntry.menuClass());
}
}
}
@@ -0,0 +1,19 @@
package com.example.base.ui;
import com.vaadin.flow.component.Composite;
import com.vaadin.flow.component.applayout.DrawerToggle;
import com.vaadin.flow.component.dependency.StyleSheet;
import com.vaadin.flow.component.html.H1;
import com.vaadin.flow.component.orderedlayout.FlexComponent;
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
@StyleSheet("view-title.css")
public class ViewTitle extends Composite<HorizontalLayout> {
public ViewTitle(String title) {
addClassName("view-title");
var h = new H1(title);
getContent().add(new DrawerToggle(), h);
getContent().setDefaultVerticalComponentAlignment(FlexComponent.Alignment.CENTER);
}
}
@@ -0,0 +1,4 @@
@NullMarked
package com.example.base.ui;
import org.jspecify.annotations.NullMarked;
@@ -0,0 +1,86 @@
package com.example.examplefeature;
import jakarta.persistence.*;
import org.jspecify.annotations.Nullable;
import java.time.Instant;
import java.time.LocalDate;
@Entity
@Table(name = "task")
public class Task {
public static final int DESCRIPTION_MAX_LENGTH = 300;
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
@Column(name = "task_id")
private Long id;
@Column(name = "description", nullable = false, length = DESCRIPTION_MAX_LENGTH)
private String description = "";
@Column(name = "creation_date", nullable = false)
private Instant creationDate;
@Column(name = "due_date")
@Nullable
private LocalDate dueDate;
protected Task() { // To keep Hibernate happy
}
public Task(String description, Instant creationDate) {
setDescription(description);
this.creationDate = creationDate;
}
public @Nullable Long getId() {
return id;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
if (description.length() > DESCRIPTION_MAX_LENGTH) {
throw new IllegalArgumentException("Description length exceeds " + DESCRIPTION_MAX_LENGTH);
}
this.description = description;
}
public Instant getCreationDate() {
return creationDate;
}
public @Nullable LocalDate getDueDate() {
return dueDate;
}
public void setDueDate(@Nullable LocalDate dueDate) {
this.dueDate = dueDate;
}
@Override
public boolean equals(Object obj) {
if (obj == null || !getClass().isAssignableFrom(obj.getClass())) {
return false;
}
if (obj == this) {
return true;
}
Task other = (Task) obj;
return getId() != null && getId().equals(other.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 getClass().hashCode();
}
}
@@ -0,0 +1,13 @@
package com.example.examplefeature;
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;
interface TaskRepository extends JpaRepository<Task, Long>, JpaSpecificationExecutor<Task> {
// If you don't need a total row count, Slice is better than Page as it only performs a select query.
// Page performs both a select and a count query.
Slice<Task> findAllBy(Pageable pageable);
}
@@ -0,0 +1,33 @@
package com.example.examplefeature;
import org.jspecify.annotations.Nullable;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.Instant;
import java.time.LocalDate;
import java.util.List;
@Service
public class TaskService {
private final TaskRepository taskRepository;
TaskService(TaskRepository taskRepository) {
this.taskRepository = taskRepository;
}
@Transactional
public void createTask(String description, @Nullable LocalDate dueDate) {
var task = new Task(description, Instant.now());
task.setDueDate(dueDate);
taskRepository.saveAndFlush(task);
}
@Transactional(readOnly = true)
public List<Task> list(Pageable pageable) {
return taskRepository.findAllBy(pageable).toList();
}
}
@@ -0,0 +1,5 @@
@NullMarked
package com.example.examplefeature;
// TODO Remove this package once you have added real features
import org.jspecify.annotations.NullMarked;
@@ -0,0 +1,90 @@
package com.example.examplefeature.ui;
import com.example.base.ui.ViewTitle;
import com.example.examplefeature.Task;
import com.example.examplefeature.TaskService;
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.notification.Notification;
import com.vaadin.flow.component.notification.NotificationVariant;
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
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 java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
import java.util.Optional;
import static com.vaadin.flow.spring.data.VaadinSpringDataHelpers.toSpringPageRequest;
@Route(value = "")
@PageTitle("Task List")
@Menu(order = 0, icon = "icons/clipboard-check.svg", title = "Task List")
class TaskListView extends VerticalLayout {
private final TaskService taskService;
final TextField description;
final DatePicker dueDate;
final Button createBtn;
final Grid<Task> taskGrid;
TaskListView(TaskService taskService) {
this.taskService = taskService;
description = new TextField();
description.setPlaceholder("What do you want to do?");
description.setAriaLabel("Task description");
description.setMaxLength(Task.DESCRIPTION_MAX_LENGTH);
description.setMinWidth("15em");
dueDate = new DatePicker();
dueDate.setPlaceholder("Due date");
dueDate.setAriaLabel("Due date");
createBtn = new Button("Create", event -> createTask());
createBtn.addThemeVariants(ButtonVariant.PRIMARY);
var toolbar = new HorizontalLayout();
toolbar.add(new ViewTitle("Task List"), description, dueDate, createBtn);
toolbar.setFlexGrow(1, description, dueDate);
toolbar.setWrap(true);
toolbar.setWidthFull();
var dateTimeFormatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM).withLocale(getLocale())
.withZone(ZoneId.systemDefault());
var dateFormatter = DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM).withLocale(getLocale());
taskGrid = new Grid<>();
taskGrid.setItems(query -> taskService.list(toSpringPageRequest(query)).stream());
taskGrid.addColumn(Task::getDescription).setHeader("Description");
taskGrid.addColumn(task -> Optional.ofNullable(task.getDueDate()).map(dateFormatter::format).orElse("Never"))
.setHeader("Due Date");
taskGrid.addColumn(task -> dateTimeFormatter.format(task.getCreationDate())).setHeader("Creation Date");
taskGrid.setEmptyStateText("You have no tasks to complete");
taskGrid.setSizeFull();
setSizeFull();
add(toolbar, taskGrid);
}
private void createTask() {
if (description.getValue().isBlank()) {
description.setInvalid(true);
description.setErrorMessage("Description is required");
return;
}
taskService.createTask(description.getValue(), dueDate.getValue());
taskGrid.getDataProvider().refreshAll();
description.clear();
dueDate.clear();
Notification.show("Task added", 3000, Notification.Position.BOTTOM_END)
.addThemeVariants(NotificationVariant.SUCCESS);
}
}
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="8" height="4" x="8" y="2" rx="1" ry="1"/><path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"/><path d="m9 14 2 2 4-4"/></svg>

After

Width:  |  Height:  |  Size: 327 B

@@ -0,0 +1,29 @@
/* Add your styles here */
.app-logo {
border-radius: min(40%, var(--vaadin-radius-m));
margin-inline-start: var(--vaadin-padding-inline-container);
}
.app-name {
font-weight: var(--aura-font-weight-semibold);
font-size: var(--aura-font-size-l);
}
.app-footer {
color: var(--vaadin-text-color-secondary);
font-size: var(--aura-font-size-s);
}
/* TODO: This is only for demo purposes. You can delete it. */
vaadin-side-nav:has(vaadin-side-nav-item:only-child)::after {
display: block;
content: "More menu items will appear here when you add more views.";
font-size: var(--aura-font-size-s);
color: var(--vaadin-text-color-secondary);
box-sizing: border-box;
padding: 1em;
max-width: 14em;
min-width: 100%;
white-space: normal;
}
@@ -0,0 +1,8 @@
.view-title h1 {
font-size: var(--aura-font-size-l);
font-weight: var(--aura-font-weight-semibold);
}
.view-title vaadin-drawer-toggle {
margin: 0;
}
+17
View File
@@ -0,0 +1,17 @@
server.port=${PORT:8080}
logging.level.org.atmosphere=warn
# 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,com.flowingcode,com.example
# Let Hibernate update the schema automatically or create it if it does not exist.
#
# DO NOT DO THIS IN PRODUCTION!
#
# Instead, use Flyway or another controlled way of managing your database schema.
# See https://vaadin.com/docs/latest/building-apps/forms-data/add-flyway for instructions.
spring.jpa.hibernate.ddl-auto=update
@@ -0,0 +1,40 @@
package com.example.examplefeature;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.domain.PageRequest;
import org.springframework.transaction.annotation.Transactional;
import java.time.Instant;
import java.time.LocalDate;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
@Transactional
class TaskServiceTest {
@Autowired
TaskService taskService;
@Test
public void tasks_are_stored_in_the_database_with_the_current_timestamp() {
var now = Instant.now();
var desc = "Do this";
var due = LocalDate.of(2025, 2, 7);
taskService.createTask(desc, due);
var task = taskService.list(PageRequest.ofSize(1)).get(0);
assertThat(task.getDescription().equals(desc));
assertThat(task.getDueDate().equals(due));
assertThat(task.getCreationDate().isAfter(now));
}
@Test
public void tasks_are_validated_before_they_are_stored() {
assertThatThrownBy(() -> taskService.createTask("X".repeat(Task.DESCRIPTION_MAX_LENGTH + 1), null))
.isInstanceOf(IllegalArgumentException.class);
}
}
@@ -0,0 +1,81 @@
package com.example.examplefeature.ui;
import com.vaadin.browserless.SpringBrowserlessTest;
import com.vaadin.flow.component.notification.Notification;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDate;
import static org.assertj.core.api.Assertions.assertThat;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
@Transactional
class TaskListViewTest extends SpringBrowserlessTest {
@Test
void empty_grid_shows_no_tasks() {
var view = navigate(TaskListView.class);
assertThat(test(view.taskGrid).size()).isZero();
assertThat(view.taskGrid.getEmptyStateText()).isEqualTo("You have no tasks to complete");
}
@Test
void create_task_with_empty_description_does_nothing() {
var view = navigate(TaskListView.class);
test(view.createBtn).click();
assertThat(test(view.taskGrid).size()).isZero();
assertThat(view.description.isInvalid()).isTrue();
assertThat($(Notification.class).exists()).isFalse();
}
@Test
void create_task_without_due_date() {
var view = navigate(TaskListView.class);
test(view.description).setValue("Buy groceries");
test(view.createBtn).click();
assertThat(test(view.taskGrid).size()).isEqualTo(1);
assertThat(test(view.taskGrid).getCellText(0, 0)).isEqualTo("Buy groceries");
assertThat(test(view.taskGrid).getCellText(0, 1)).isEqualTo("Never");
assertThat(test(view.taskGrid).getCellText(0, 2)).isNotEmpty();
assertThat(view.description.getValue()).isEmpty();
assertThat(view.dueDate.getValue()).isNull();
var notification = $(Notification.class).single();
assertThat(test(notification).getText()).contains("Task added");
}
@Test
void create_task_with_due_date() {
var view = navigate(TaskListView.class);
test(view.description).setValue("File taxes");
test(view.dueDate).setValue(LocalDate.of(2026, 3, 15));
test(view.createBtn).click();
assertThat(test(view.taskGrid).size()).isEqualTo(1);
assertThat(test(view.taskGrid).getCellText(0, 0)).isEqualTo("File taxes");
assertThat(test(view.taskGrid).getCellText(0, 1)).isNotEqualTo("Never");
}
@Test
void create_multiple_tasks() {
var view = navigate(TaskListView.class);
test(view.description).setValue("First task");
test(view.createBtn).click();
test(view.description).setValue("Second task");
test(view.dueDate).setValue(LocalDate.of(2026, 6, 1));
test(view.createBtn).click();
assertThat(test(view.taskGrid).size()).isEqualTo(2);
}
}