Generated project
This commit is contained in:
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user