├── crud-ui-demo ├── src │ └── main │ │ ├── resources │ │ ├── META-INF │ │ │ └── resources │ │ │ │ └── icons │ │ │ │ └── icon.png │ │ ├── application.properties │ │ └── banner.txt │ │ ├── java │ │ └── org │ │ │ └── vaadin │ │ │ └── crudui │ │ │ └── demo │ │ │ ├── repository │ │ │ ├── GroupRepository.java │ │ │ ├── TechnologyRepository.java │ │ │ └── UserRepository.java │ │ │ ├── entity │ │ │ ├── MaritalStatus.java │ │ │ ├── Group.java │ │ │ ├── Technology.java │ │ │ └── User.java │ │ │ ├── DemoUtils.java │ │ │ ├── service │ │ │ ├── GroupService.java │ │ │ ├── UserService.java │ │ │ └── TechnologyService.java │ │ │ ├── ui │ │ │ ├── view │ │ │ │ ├── DefaultView.java │ │ │ │ ├── HomeView.java │ │ │ │ ├── TreeView.java │ │ │ │ └── CustomizedView.java │ │ │ └── MainLayout.java │ │ │ └── Application.java │ │ ├── frontend │ │ ├── index.html │ │ └── theme-handler.js │ │ └── bundles │ │ └── README.md ├── .gitignore └── pom.xml ├── add-on ├── src │ └── main │ │ └── java │ │ └── org │ │ └── vaadin │ │ ├── crudui │ │ ├── crud │ │ │ ├── CrudOperation.java │ │ │ ├── CrudOperationException.java │ │ │ ├── AddOperationListener.java │ │ │ ├── UpdateOperationListener.java │ │ │ ├── DeleteOperationListener.java │ │ │ ├── FindAllCrudOperationListener.java │ │ │ ├── CrudListener.java │ │ │ ├── LazyCrudListener.java │ │ │ ├── LazyFindAllCrudOperationListener.java │ │ │ ├── Crud.java │ │ │ ├── impl │ │ │ │ ├── TreeGridCrud.java │ │ │ │ ├── GridCrud.java │ │ │ │ └── AbstractGridCrud.java │ │ │ └── AbstractCrud.java │ │ ├── form │ │ │ ├── FieldCreationListener.java │ │ │ ├── FieldProvider.java │ │ │ ├── impl │ │ │ │ ├── field │ │ │ │ │ └── provider │ │ │ │ │ │ ├── RadioButtonGroupProvider.java │ │ │ │ │ │ ├── CheckBoxGroupProvider.java │ │ │ │ │ │ ├── MultiSelectComboBoxProvider.java │ │ │ │ │ │ ├── ComboBoxProvider.java │ │ │ │ │ │ ├── AbstractListingProvider.java │ │ │ │ │ │ └── DefaultFieldProvider.java │ │ │ │ └── form │ │ │ │ │ └── factory │ │ │ │ │ └── DefaultCrudFormFactory.java │ │ │ ├── CrudFormFactory.java │ │ │ ├── CrudFormConfiguration.java │ │ │ ├── AbstractCrudFormFactory.java │ │ │ └── AbstractAutoGeneratedCrudFormFactory.java │ │ └── layout │ │ │ ├── CrudLayout.java │ │ │ └── impl │ │ │ ├── VerticalSplitCrudLayout.java │ │ │ ├── HorizontalSplitCrudLayout.java │ │ │ ├── WindowBasedCrudLayout.java │ │ │ └── AbstractTwoComponentsCrudLayout.java │ │ └── data │ │ └── converter │ │ ├── StringToCharacterConverter.java │ │ └── StringToByteConverter.java ├── assembly │ ├── MANIFEST.MF │ └── assembly.xml └── pom.xml ├── pom.xml ├── .gitignore ├── README.md └── LICENSE.txt /crud-ui-demo/src/main/resources/META-INF/resources/icons/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alejandro-du/crudui/HEAD/crud-ui-demo/src/main/resources/META-INF/resources/icons/icon.png -------------------------------------------------------------------------------- /add-on/src/main/java/org/vaadin/crudui/crud/CrudOperation.java: -------------------------------------------------------------------------------- 1 | package org.vaadin.crudui.crud; 2 | 3 | /** 4 | * @author Alejandro Duarte. 5 | */ 6 | public enum CrudOperation { 7 | 8 | READ, ADD, UPDATE, DELETE; 9 | 10 | } 11 | -------------------------------------------------------------------------------- /add-on/assembly/MANIFEST.MF: -------------------------------------------------------------------------------- 1 | Manifest-Version: 1.0 2 | Vaadin-Package-Version: 1 3 | Vaadin-Addon: ${project.build.finalName}.${project.packaging} 4 | Implementation-Vendor: ${organization.name} 5 | Implementation-Title: ${project.name} 6 | Implementation-Version: ${project.version} 7 | -------------------------------------------------------------------------------- /crud-ui-demo/src/main/java/org/vaadin/crudui/demo/repository/GroupRepository.java: -------------------------------------------------------------------------------- 1 | package org.vaadin.crudui.demo.repository; 2 | 3 | import org.springframework.data.jpa.repository.JpaRepository; 4 | import org.vaadin.crudui.demo.entity.Group; 5 | 6 | public interface GroupRepository extends JpaRepository { 7 | 8 | } 9 | -------------------------------------------------------------------------------- /add-on/src/main/java/org/vaadin/crudui/crud/CrudOperationException.java: -------------------------------------------------------------------------------- 1 | package org.vaadin.crudui.crud; 2 | 3 | /** 4 | * @author Alejandro Duarte. 5 | */ 6 | public class CrudOperationException extends RuntimeException { 7 | 8 | public CrudOperationException(String message) { 9 | super(message); 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /crud-ui-demo/src/main/java/org/vaadin/crudui/demo/entity/MaritalStatus.java: -------------------------------------------------------------------------------- 1 | package org.vaadin.crudui.demo.entity; 2 | 3 | public enum MaritalStatus { 4 | 5 | SINGLE, MARRIED, OTHER; 6 | 7 | @Override 8 | public String toString() { 9 | return name().charAt(0) + name().substring(1).toLowerCase(); 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /add-on/src/main/java/org/vaadin/crudui/crud/AddOperationListener.java: -------------------------------------------------------------------------------- 1 | package org.vaadin.crudui.crud; 2 | 3 | import java.io.Serializable; 4 | 5 | /** 6 | * @author Alejandro Duarte. 7 | */ 8 | @FunctionalInterface 9 | public interface AddOperationListener extends Serializable { 10 | 11 | T perform(T domainObject); 12 | 13 | } 14 | -------------------------------------------------------------------------------- /add-on/src/main/java/org/vaadin/crudui/crud/UpdateOperationListener.java: -------------------------------------------------------------------------------- 1 | package org.vaadin.crudui.crud; 2 | 3 | import java.io.Serializable; 4 | 5 | /** 6 | * @author Alejandro Duarte. 7 | */ 8 | @FunctionalInterface 9 | public interface UpdateOperationListener extends Serializable { 10 | 11 | T perform(T domainObject); 12 | 13 | } 14 | -------------------------------------------------------------------------------- /add-on/src/main/java/org/vaadin/crudui/crud/DeleteOperationListener.java: -------------------------------------------------------------------------------- 1 | package org.vaadin.crudui.crud; 2 | 3 | import java.io.Serializable; 4 | 5 | /** 6 | * @author Alejandro Duarte. 7 | */ 8 | @FunctionalInterface 9 | public interface DeleteOperationListener extends Serializable { 10 | 11 | void perform(T domainObject); 12 | 13 | } 14 | -------------------------------------------------------------------------------- /crud-ui-demo/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | server.port=8080 2 | 3 | logging.level.org.atmosphere = warn 4 | 5 | #spring.datasource.url=jdbc:mariadb://localhost:3306/crud-ui-demo 6 | spring.datasource.driver-class-name=org.h2.Driver 7 | spring.datasource.username=admin 8 | spring.datasource.password= 9 | spring.jpa.hibernate.ddl-auto=update 10 | -------------------------------------------------------------------------------- /crud-ui-demo/src/main/resources/banner.txt: -------------------------------------------------------------------------------- 1 | _ _ _ 2 | ___ _ __ _ _ __| | _ _(_) __| | ___ _ __ ___ ___ 3 | / __| '__| | | |/ _` |_____| | | | |_____ / _` |/ _ \ '_ ` _ \ / _ \ 4 | | (__| | | |_| | (_| |_____| |_| | |_____| (_| | __/ | | | | | (_) | 5 | \___|_| \__,_|\__,_| \__,_|_| \__,_|\___|_| |_| |_|\___/ 6 | -------------------------------------------------------------------------------- /add-on/src/main/java/org/vaadin/crudui/crud/FindAllCrudOperationListener.java: -------------------------------------------------------------------------------- 1 | package org.vaadin.crudui.crud; 2 | 3 | import java.io.Serializable; 4 | import java.util.Collection; 5 | 6 | /** 7 | * @author Alejandro Duarte. 8 | */ 9 | @FunctionalInterface 10 | public interface FindAllCrudOperationListener extends Serializable { 11 | 12 | Collection findAll(); 13 | 14 | } 15 | -------------------------------------------------------------------------------- /add-on/src/main/java/org/vaadin/crudui/form/FieldCreationListener.java: -------------------------------------------------------------------------------- 1 | package org.vaadin.crudui.form; 2 | 3 | import java.io.Serializable; 4 | 5 | import com.vaadin.flow.component.HasValue; 6 | 7 | /** 8 | * @author Alejandro Duarte. 9 | */ 10 | @FunctionalInterface 11 | public interface FieldCreationListener extends Serializable { 12 | 13 | void fieldCreated(HasValue field); 14 | 15 | } 16 | -------------------------------------------------------------------------------- /add-on/src/main/java/org/vaadin/crudui/form/FieldProvider.java: -------------------------------------------------------------------------------- 1 | package org.vaadin.crudui.form; 2 | 3 | import java.io.Serializable; 4 | 5 | import com.vaadin.flow.component.HasValueAndElement; 6 | 7 | /** 8 | * @author Alejandro Duarte. 9 | */ 10 | @FunctionalInterface 11 | public interface FieldProvider extends Serializable { 12 | 13 | C buildField(T t); 14 | 15 | } 16 | -------------------------------------------------------------------------------- /crud-ui-demo/.gitignore: -------------------------------------------------------------------------------- 1 | /target/ 2 | .idea/ 3 | .settings 4 | .project 5 | .classpath 6 | 7 | *.iml 8 | .DS_Store 9 | 10 | # The following files are generated/updated by vaadin-maven-plugin 11 | node_modules/ 12 | 13 | # Browser drivers for local integration tests 14 | drivers/ 15 | # Error screenshots generated by TestBench for failed integration tests 16 | error-screenshots/ 17 | webpack.generated.js 18 | 19 | generated/ -------------------------------------------------------------------------------- /add-on/src/main/java/org/vaadin/crudui/crud/CrudListener.java: -------------------------------------------------------------------------------- 1 | package org.vaadin.crudui.crud; 2 | 3 | import java.io.Serializable; 4 | import java.util.Collection; 5 | 6 | /** 7 | * @author Alejandro Duarte 8 | */ 9 | public interface CrudListener extends Serializable { 10 | 11 | Collection findAll(); 12 | 13 | T add(T domainObjectToAdd); 14 | 15 | T update(T domainObjectToUpdate); 16 | 17 | void delete(T domainObjectToDelete); 18 | 19 | } 20 | -------------------------------------------------------------------------------- /crud-ui-demo/src/main/java/org/vaadin/crudui/demo/repository/TechnologyRepository.java: -------------------------------------------------------------------------------- 1 | package org.vaadin.crudui.demo.repository; 2 | 3 | import java.util.List; 4 | 5 | import org.springframework.data.jpa.repository.JpaRepository; 6 | import org.vaadin.crudui.demo.entity.Technology; 7 | 8 | public interface TechnologyRepository extends JpaRepository { 9 | 10 | List findAllByParent(Technology parent); 11 | 12 | List findAllByParentIsNull(); 13 | 14 | } 15 | -------------------------------------------------------------------------------- /add-on/src/main/java/org/vaadin/crudui/crud/LazyCrudListener.java: -------------------------------------------------------------------------------- 1 | package org.vaadin.crudui.crud; 2 | 3 | import com.vaadin.flow.data.provider.DataProvider; 4 | 5 | import java.util.Collection; 6 | 7 | /** 8 | * @author Alejandro Duarte 9 | */ 10 | public interface LazyCrudListener extends CrudListener { 11 | 12 | default Collection findAll() { 13 | throw new UnsupportedOperationException("Use fetch and count methods instead."); 14 | } 15 | 16 | DataProvider getDataProvider(); 17 | 18 | } 19 | -------------------------------------------------------------------------------- /add-on/src/main/java/org/vaadin/crudui/crud/LazyFindAllCrudOperationListener.java: -------------------------------------------------------------------------------- 1 | package org.vaadin.crudui.crud; 2 | 3 | import com.vaadin.flow.data.provider.DataProvider; 4 | 5 | import java.util.Collection; 6 | 7 | /** 8 | * @author Alejandro Duarte 9 | */ 10 | public interface LazyFindAllCrudOperationListener extends FindAllCrudOperationListener { 11 | 12 | default Collection findAll() { 13 | throw new UnsupportedOperationException("Use fetch and count methods instead."); 14 | } 15 | 16 | DataProvider getDataProvider(); 17 | } 18 | -------------------------------------------------------------------------------- /crud-ui-demo/src/main/java/org/vaadin/crudui/demo/repository/UserRepository.java: -------------------------------------------------------------------------------- 1 | package org.vaadin.crudui.demo.repository; 2 | 3 | import org.springframework.data.domain.Page; 4 | import org.springframework.data.domain.Pageable; 5 | import org.springframework.data.jpa.repository.JpaRepository; 6 | import org.vaadin.crudui.demo.entity.User; 7 | 8 | public interface UserRepository extends JpaRepository { 9 | 10 | Page findByNameContainingIgnoreCase(String name, Pageable pageable); 11 | 12 | long countByNameContainingIgnoreCase(String name); 13 | 14 | } 15 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | org.vaadin.crudui 7 | crud-ui 8 | pom 9 | 1.0.0 10 | 11 | 12 | add-on 13 | crud-ui-demo 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /crud-ui-demo/src/main/frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | HELP.md 3 | target/ 4 | !.mvn/wrapper/maven-wrapper.jar 5 | !**/src/main/**/target/ 6 | !**/src/test/**/target/ 7 | 8 | /**/package-lock.json 9 | /**/package.json 10 | /**/tsconfig.json 11 | /**/types.d.ts 12 | /**/vite.config.ts 13 | /**/vite.generated.ts 14 | /**/dev.bundle 15 | 16 | ### STS ### 17 | .apt_generated 18 | .classpath 19 | .factorypath 20 | .project 21 | .settings 22 | .springBeans 23 | .sts4-cache 24 | 25 | ### IntelliJ IDEA ### 26 | .idea 27 | *.iws 28 | *.iml 29 | *.ipr 30 | 31 | ### NetBeans ### 32 | /nbproject/private/ 33 | /nbbuild/ 34 | /dist/ 35 | /nbdist/ 36 | /.nb-gradle/ 37 | build/ 38 | !**/src/main/**/build/ 39 | !**/src/test/**/build/ 40 | 41 | ### VS Code ### 42 | .vscode/ 43 | -------------------------------------------------------------------------------- /crud-ui-demo/src/main/java/org/vaadin/crudui/demo/DemoUtils.java: -------------------------------------------------------------------------------- 1 | package org.vaadin.crudui.demo; 2 | 3 | import org.apache.commons.lang3.StringUtils; 4 | 5 | public class DemoUtils { 6 | 7 | public static String getViewName(Class clazz) { 8 | String lowerCase = StringUtils.join( 9 | StringUtils.splitByCharacterTypeCamelCase(clazz.getSimpleName()), 10 | " ").toLowerCase().replace("view", "").replace("crud", "CRUD"); 11 | 12 | return lowerCase.substring(0, 1).toUpperCase() + lowerCase.substring(1); 13 | } 14 | 15 | public static String getGitHubLink(Class clazz) { 16 | return "https://github.com/alejandro-du/crudui/tree/master/crud-ui-demo/src/main/java/" 17 | + clazz.getName().replace(".", "/") 18 | + ".java"; 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /add-on/src/main/java/org/vaadin/crudui/layout/CrudLayout.java: -------------------------------------------------------------------------------- 1 | package org.vaadin.crudui.layout; 2 | 3 | import java.util.stream.Stream; 4 | 5 | import com.vaadin.flow.component.Component; 6 | 7 | import org.vaadin.crudui.crud.CrudOperation; 8 | 9 | /** 10 | * @author Alejandro Duarte 11 | */ 12 | public interface CrudLayout { 13 | 14 | void setMainComponent(Component component); 15 | 16 | void addFilterComponent(Component component); 17 | 18 | default void addFilterComponents(Component... components) { 19 | Stream.of(components).forEach(this::addFilterComponent); 20 | } 21 | 22 | void addToolbarComponent(Component component); 23 | 24 | void showForm(CrudOperation operation, Component form, String caption); 25 | 26 | void hideForm(); 27 | 28 | } 29 | -------------------------------------------------------------------------------- /add-on/src/main/java/org/vaadin/crudui/layout/impl/VerticalSplitCrudLayout.java: -------------------------------------------------------------------------------- 1 | package org.vaadin.crudui.layout.impl; 2 | 3 | import com.vaadin.flow.component.splitlayout.SplitLayout; 4 | import com.vaadin.flow.component.splitlayout.SplitLayoutVariant; 5 | 6 | /** 7 | * @author Alejandro Duarte. 8 | */ 9 | public class VerticalSplitCrudLayout extends AbstractTwoComponentsCrudLayout { 10 | 11 | @Override 12 | protected SplitLayout buildMainLayout() { 13 | SplitLayout mainLayout = new SplitLayout(firstComponent, secondComponent); 14 | mainLayout.setOrientation(SplitLayout.Orientation.VERTICAL); 15 | mainLayout.setSizeFull(); 16 | mainLayout.setSplitterPosition(65); 17 | mainLayout.addThemeVariants(SplitLayoutVariant.LUMO_MINIMAL); 18 | return mainLayout; 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /crud-ui-demo/src/main/java/org/vaadin/crudui/demo/service/GroupService.java: -------------------------------------------------------------------------------- 1 | package org.vaadin.crudui.demo.service; 2 | 3 | import org.springframework.stereotype.Service; 4 | import org.vaadin.crudui.demo.entity.Group; 5 | import org.vaadin.crudui.demo.repository.GroupRepository; 6 | 7 | import java.util.List; 8 | 9 | @Service 10 | public class GroupService { 11 | 12 | private final GroupRepository groupRepository; 13 | 14 | public GroupService(GroupRepository groupRepository) { 15 | this.groupRepository = groupRepository; 16 | } 17 | 18 | public List findAll() { 19 | return groupRepository.findAll(); 20 | } 21 | 22 | public int count() { 23 | return (int) groupRepository.count(); 24 | } 25 | 26 | public Group save(Group group) { 27 | return groupRepository.save(group); 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /add-on/src/main/java/org/vaadin/crudui/layout/impl/HorizontalSplitCrudLayout.java: -------------------------------------------------------------------------------- 1 | package org.vaadin.crudui.layout.impl; 2 | 3 | import com.vaadin.flow.component.splitlayout.SplitLayout; 4 | import com.vaadin.flow.component.splitlayout.SplitLayoutVariant; 5 | 6 | /** 7 | * @author Alejandro Duarte. 8 | */ 9 | public class HorizontalSplitCrudLayout extends AbstractTwoComponentsCrudLayout { 10 | 11 | @Override 12 | protected SplitLayout buildMainLayout() { 13 | SplitLayout mainLayout = new SplitLayout(firstComponent, secondComponent); 14 | mainLayout.setOrientation(SplitLayout.Orientation.HORIZONTAL); 15 | mainLayout.setSizeFull(); 16 | mainLayout.setSplitterPosition(65); 17 | mainLayout.addThemeVariants(SplitLayoutVariant.LUMO_MINIMAL); 18 | formComponentLayout.setPadding(true); 19 | return mainLayout; 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /crud-ui-demo/src/main/java/org/vaadin/crudui/demo/ui/view/DefaultView.java: -------------------------------------------------------------------------------- 1 | package org.vaadin.crudui.demo.ui.view; 2 | 3 | import com.vaadin.flow.component.orderedlayout.VerticalLayout; 4 | import com.vaadin.flow.router.Route; 5 | 6 | import org.vaadin.crudui.crud.impl.GridCrud; 7 | import org.vaadin.crudui.demo.entity.User; 8 | import org.vaadin.crudui.demo.service.UserService; 9 | import org.vaadin.crudui.demo.ui.MainLayout; 10 | 11 | @Route(value = "default", layout = MainLayout.class) 12 | public class DefaultView extends VerticalLayout { 13 | 14 | public DefaultView(UserService userService) { 15 | GridCrud crud = new GridCrud<>(User.class); 16 | crud.setOperations( 17 | userService::findAll, 18 | userService::save, 19 | userService::save, 20 | userService::delete); 21 | 22 | add(crud); 23 | setSizeFull(); 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /add-on/src/main/java/org/vaadin/data/converter/StringToCharacterConverter.java: -------------------------------------------------------------------------------- 1 | package org.vaadin.data.converter; 2 | 3 | import com.vaadin.flow.data.binder.Result; 4 | import com.vaadin.flow.data.binder.ValueContext; 5 | import com.vaadin.flow.data.converter.Converter; 6 | 7 | public class StringToCharacterConverter implements Converter { 8 | 9 | @Override 10 | public Result convertToModel(String value, ValueContext context) { 11 | if (value == null) { 12 | return Result.ok(null); 13 | } 14 | 15 | if (value.length() > 1) { 16 | return Result.error("Could not convert '" + value); 17 | } 18 | 19 | return Result.ok(value.charAt(0)); 20 | } 21 | 22 | @Override 23 | public String convertToPresentation(Character value, ValueContext context) { 24 | if (value == null) { 25 | return null; 26 | } 27 | 28 | return value.toString(); 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /crud-ui-demo/src/main/frontend/theme-handler.js: -------------------------------------------------------------------------------- 1 | window.applyTheme = (theme) => { 2 | document.documentElement.setAttribute("theme", theme); 3 | const logoSrc = theme === "dark" ? "https://i.imgur.com/3xp1nLI.png" : "https://i.imgur.com/GPpnszs.png"; 4 | document.getElementsByClassName("logo")[0].src = logoSrc; 5 | 6 | const iconName = theme === "dark" ? "vaadin:sun-o" : "vaadin:moon"; 7 | const themeSwitcher = document.querySelector('vaadin-button'); 8 | const icon = themeSwitcher.querySelector('vaadin-icon[slot="prefix"]'); 9 | icon.setAttribute('icon', iconName); 10 | }; 11 | 12 | window.applySystemTheme = () => { 13 | const theme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : ""; 14 | window.applyTheme(theme); 15 | }; 16 | 17 | window.switchTheme = () => { 18 | const theme = document.documentElement.getAttribute("theme") === "dark" ? "" : "dark"; 19 | window.applyTheme(theme); 20 | } 21 | 22 | window 23 | .matchMedia("(prefers-color-scheme: dark)") 24 | .addEventListener('change', applySystemTheme); 25 | -------------------------------------------------------------------------------- /add-on/src/main/java/org/vaadin/crudui/form/impl/field/provider/RadioButtonGroupProvider.java: -------------------------------------------------------------------------------- 1 | package org.vaadin.crudui.form.impl.field.provider; 2 | 3 | import com.vaadin.flow.component.Component; 4 | import com.vaadin.flow.component.radiobutton.RadioButtonGroup; 5 | import com.vaadin.flow.data.renderer.ComponentRenderer; 6 | 7 | import java.util.Collection; 8 | 9 | /** 10 | * @author Alejandro Duarte 11 | */ 12 | public class RadioButtonGroupProvider extends AbstractListingProvider, T> { 13 | 14 | public RadioButtonGroupProvider(Collection items) { 15 | super(items); 16 | } 17 | 18 | public RadioButtonGroupProvider(String caption, Collection items) { 19 | super(caption, items); 20 | } 21 | 22 | public RadioButtonGroupProvider(String caption, Collection items, 23 | ComponentRenderer renderer) { 24 | super(caption, items, renderer); 25 | } 26 | 27 | @Override 28 | protected RadioButtonGroup buildAbstractListing() { 29 | RadioButtonGroup field = new RadioButtonGroup<>(); 30 | if (renderer != null) { 31 | field.setRenderer(renderer); 32 | } 33 | field.setItems(items); 34 | return field; 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /add-on/src/main/java/org/vaadin/crudui/form/impl/field/provider/CheckBoxGroupProvider.java: -------------------------------------------------------------------------------- 1 | package org.vaadin.crudui.form.impl.field.provider; 2 | 3 | import com.vaadin.flow.component.ItemLabelGenerator; 4 | import com.vaadin.flow.component.checkbox.CheckboxGroup; 5 | 6 | import java.util.Collection; 7 | 8 | /** 9 | * @author Alejandro Duarte 10 | */ 11 | public class CheckBoxGroupProvider extends AbstractListingProvider, T> { 12 | 13 | private ItemLabelGenerator itemLabelGenerator; 14 | 15 | public CheckBoxGroupProvider(Collection items) { 16 | super(items); 17 | } 18 | 19 | public CheckBoxGroupProvider(String caption, Collection items) { 20 | super(caption, items); 21 | } 22 | 23 | public CheckBoxGroupProvider(String caption, Collection items, ItemLabelGenerator itemLabelGenerator) { 24 | super(caption, items); 25 | this.itemLabelGenerator = itemLabelGenerator; 26 | } 27 | 28 | @Override 29 | protected CheckboxGroup buildAbstractListing() { 30 | CheckboxGroup field = new CheckboxGroup<>(); 31 | if (itemLabelGenerator != null) { 32 | field.setItemLabelGenerator(itemLabelGenerator); 33 | } 34 | return field; 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /add-on/src/main/java/org/vaadin/crudui/form/impl/field/provider/MultiSelectComboBoxProvider.java: -------------------------------------------------------------------------------- 1 | package org.vaadin.crudui.form.impl.field.provider; 2 | 3 | import java.util.Collection; 4 | 5 | import com.vaadin.flow.component.ItemLabelGenerator; 6 | import com.vaadin.flow.component.combobox.MultiSelectComboBox; 7 | 8 | public class MultiSelectComboBoxProvider extends AbstractListingProvider, T> { 9 | 10 | private ItemLabelGenerator itemLabelGenerator; 11 | 12 | public MultiSelectComboBoxProvider(Collection items) { 13 | super(items); 14 | } 15 | 16 | public MultiSelectComboBoxProvider(String caption, Collection items) { 17 | super(caption, items); 18 | } 19 | 20 | public MultiSelectComboBoxProvider(String caption, Collection items, ItemLabelGenerator itemLabelGenerator) { 21 | super(caption, items); 22 | this.itemLabelGenerator = itemLabelGenerator; 23 | } 24 | 25 | @Override 26 | protected MultiSelectComboBox buildAbstractListing() { 27 | MultiSelectComboBox field = new MultiSelectComboBox<>(); 28 | if (itemLabelGenerator != null) { 29 | field.setItemLabelGenerator(itemLabelGenerator); 30 | } 31 | return field; 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /add-on/src/main/java/org/vaadin/crudui/crud/Crud.java: -------------------------------------------------------------------------------- 1 | package org.vaadin.crudui.crud; 2 | 3 | import com.vaadin.flow.data.provider.DataProvider; 4 | 5 | import org.vaadin.crudui.form.CrudFormFactory; 6 | import org.vaadin.crudui.layout.CrudLayout; 7 | 8 | /** 9 | * @author Alejandro Duarte 10 | */ 11 | public interface Crud { 12 | 13 | void setAddOperationVisible(boolean visible); 14 | 15 | void setUpdateOperationVisible(boolean visible); 16 | 17 | void setDeleteOperationVisible(boolean visible); 18 | 19 | void setFindAllOperationVisible(boolean visible); 20 | 21 | CrudFormFactory getCrudFormFactory(); 22 | 23 | void setCrudFormFactory(CrudFormFactory crudFormFactory); 24 | 25 | void setFindAllOperation(FindAllCrudOperationListener findAllOperation); 26 | 27 | void setFindAllOperation(DataProvider dataProvider); 28 | 29 | void setAddOperation(AddOperationListener addOperation); 30 | 31 | void setUpdateOperation(UpdateOperationListener updateOperation); 32 | 33 | void setDeleteOperation(DeleteOperationListener deleteOperation); 34 | 35 | void setOperations(FindAllCrudOperationListener findAllOperation, AddOperationListener addOperation, 36 | UpdateOperationListener updateOperation, DeleteOperationListener deleteOperation); 37 | 38 | void setCrudListener(CrudListener crudListener); 39 | 40 | CrudLayout getCrudLayout(); 41 | 42 | } 43 | -------------------------------------------------------------------------------- /crud-ui-demo/src/main/java/org/vaadin/crudui/demo/entity/Group.java: -------------------------------------------------------------------------------- 1 | package org.vaadin.crudui.demo.entity; 2 | 3 | import jakarta.persistence.Entity; 4 | import jakarta.persistence.GeneratedValue; 5 | import jakarta.persistence.Id; 6 | import jakarta.persistence.Table; 7 | 8 | @Entity 9 | @Table(name = "group_") 10 | public class Group { 11 | 12 | @Id 13 | @GeneratedValue 14 | private Long id; 15 | 16 | private String name; 17 | 18 | private Boolean admin = false; 19 | 20 | public Group() { 21 | } 22 | 23 | public Group(String name) { 24 | this.name = name; 25 | } 26 | 27 | @Override 28 | public boolean equals(Object o) { 29 | if (this == o) 30 | return true; 31 | if (o == null || getClass() != o.getClass()) 32 | return false; 33 | 34 | Group group = (Group) o; 35 | 36 | return id != null ? id.equals(group.id) : group.id == null; 37 | } 38 | 39 | @Override 40 | public int hashCode() { 41 | return id != null ? id.hashCode() : 0; 42 | } 43 | 44 | public Long getId() { 45 | return id; 46 | } 47 | 48 | public void setId(Long id) { 49 | this.id = id; 50 | } 51 | 52 | public String getName() { 53 | return name; 54 | } 55 | 56 | public void setName(String name) { 57 | this.name = name; 58 | } 59 | 60 | public Boolean getAdmin() { 61 | return admin; 62 | } 63 | 64 | public void setAdmin(Boolean admin) { 65 | this.admin = admin; 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /add-on/src/main/java/org/vaadin/crudui/form/impl/field/provider/ComboBoxProvider.java: -------------------------------------------------------------------------------- 1 | package org.vaadin.crudui.form.impl.field.provider; 2 | 3 | import java.util.Collection; 4 | 5 | import com.vaadin.flow.component.Component; 6 | import com.vaadin.flow.component.ItemLabelGenerator; 7 | import com.vaadin.flow.component.combobox.ComboBox; 8 | import com.vaadin.flow.data.renderer.ComponentRenderer; 9 | 10 | /** 11 | * @author Alejandro Duarte 12 | */ 13 | public class ComboBoxProvider extends AbstractListingProvider, T> { 14 | 15 | private ItemLabelGenerator itemLabelGenerator; 16 | 17 | public ComboBoxProvider(Collection items) { 18 | super(items); 19 | } 20 | 21 | public ComboBoxProvider(String caption, Collection items) { 22 | super(caption, items); 23 | } 24 | 25 | public ComboBoxProvider(String caption, Collection items, ComponentRenderer renderer, 26 | ItemLabelGenerator itemLabelGenerator) { 27 | super(caption, items, renderer); 28 | this.itemLabelGenerator = itemLabelGenerator; 29 | } 30 | 31 | @Override 32 | protected ComboBox buildAbstractListing() { 33 | ComboBox field = new ComboBox<>(); 34 | if (renderer != null) { 35 | field.setRenderer(renderer); 36 | } 37 | if (itemLabelGenerator != null) { 38 | field.setItemLabelGenerator(itemLabelGenerator); 39 | } 40 | field.setItems(items); 41 | return field; 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /add-on/assembly/assembly.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | Crud UI add-on 7 | 8 | 10 | 11 | 12 | zip 13 | 14 | 15 | 16 | false 17 | 18 | 19 | 20 | .. 21 | 22 | LICENSE.txt 23 | README.md 24 | 25 | 26 | 27 | target 28 | 29 | 30 | *.jar 31 | *.pdf 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | assembly/MANIFEST.MF 40 | META-INF 41 | true 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /crud-ui-demo/src/main/java/org/vaadin/crudui/demo/ui/view/HomeView.java: -------------------------------------------------------------------------------- 1 | package org.vaadin.crudui.demo.ui.view; 2 | 3 | import java.io.File; 4 | import java.io.IOException; 5 | 6 | import com.flowingcode.vaadin.addons.markdown.MarkdownViewer; 7 | import com.vaadin.flow.component.Html; 8 | import com.vaadin.flow.component.html.H1; 9 | import com.vaadin.flow.component.orderedlayout.VerticalLayout; 10 | import com.vaadin.flow.router.Route; 11 | 12 | import org.apache.commons.io.FileUtils; 13 | import org.vaadin.crudui.demo.ui.MainLayout; 14 | 15 | @Route(value = "", layout = MainLayout.class) 16 | public class HomeView extends VerticalLayout { 17 | 18 | public HomeView() throws IOException { 19 | String readmeContent = FileUtils.readFileToString(new File(System.getProperty("user.dir"), "../README.md"), "UTF-8"); 20 | 21 | add( 22 | new H1("Crud UI Add-on demo"), 23 | new Html(""" 24 |
25 |

26 | This is the demo app for the 27 | Crud UI add-on for Vaadin. 28 | The full source code is 29 | available on GitHub. 30 |

31 |

32 | Use the tabs above to navigate through the different demo views. 33 | There's a link to the implementation at the bottom of each view. 34 |

35 |
36 | """), 37 | new MarkdownViewer(readmeContent) 38 | ); 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /crud-ui-demo/src/main/bundles/README.md: -------------------------------------------------------------------------------- 1 | This directory is automatically generated by Vaadin and contains the pre-compiled 2 | frontend files/resources for your project (frontend development bundle). 3 | 4 | It should be added to Version Control System and committed, so that other developers 5 | do not have to compile it again. 6 | 7 | Frontend development bundle is automatically updated when needed: 8 | - an npm/pnpm package is added with @NpmPackage or directly into package.json 9 | - CSS, JavaScript or TypeScript files are added with @CssImport, @JsModule or @JavaScript 10 | - Vaadin add-on with front-end customizations is added 11 | - Custom theme imports/assets added into 'theme.json' file 12 | - Exported web component is added. 13 | 14 | If your project development needs a hot deployment of the frontend changes, 15 | you can switch Flow to use Vite development server (default in Vaadin 23.3 and earlier versions): 16 | - set `vaadin.frontend.hotdeploy=true` in `application.properties` 17 | - configure `vaadin-maven-plugin`: 18 | ``` 19 | 20 | true 21 | 22 | ``` 23 | - configure `jetty-maven-plugin`: 24 | ``` 25 | 26 | 27 | true 28 | 29 | 30 | ``` 31 | 32 | Read more [about Vaadin development mode](https://vaadin.com/docs/next/flow/configuration/development-mode#precompiled-bundle). -------------------------------------------------------------------------------- /crud-ui-demo/src/main/java/org/vaadin/crudui/demo/service/UserService.java: -------------------------------------------------------------------------------- 1 | package org.vaadin.crudui.demo.service; 2 | 3 | import java.util.List; 4 | 5 | import org.springframework.data.domain.Page; 6 | import org.springframework.data.domain.PageRequest; 7 | import org.springframework.stereotype.Service; 8 | import org.vaadin.crudui.demo.entity.User; 9 | import org.vaadin.crudui.demo.repository.UserRepository; 10 | 11 | @Service 12 | public class UserService { 13 | 14 | public static final int USERS_COUNT_LIMIT = 1000; 15 | 16 | public static class LimitReached extends RuntimeException { 17 | } 18 | 19 | private final UserRepository userRepository; 20 | 21 | public UserService(UserRepository userRepository) { 22 | this.userRepository = userRepository; 23 | } 24 | 25 | public List findAll() { 26 | return userRepository.findAll(); 27 | } 28 | 29 | public int countAll() { 30 | return (int) userRepository.count(); 31 | } 32 | 33 | public User save(User user) { 34 | if (countAll() >= USERS_COUNT_LIMIT) { 35 | throw new LimitReached(); 36 | } 37 | 38 | return userRepository.save(user); 39 | } 40 | 41 | public void delete(User user) { 42 | userRepository.delete(user); 43 | } 44 | 45 | public Page findByNameContainingIgnoreCase(String name, int page, int pageSize) { 46 | return userRepository.findByNameContainingIgnoreCase(name, PageRequest.of(page, pageSize)); 47 | } 48 | 49 | public long countByNameContainingIgnoreCase(String name) { 50 | return userRepository.countByNameContainingIgnoreCase(name); 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /add-on/src/main/java/org/vaadin/crudui/form/impl/field/provider/AbstractListingProvider.java: -------------------------------------------------------------------------------- 1 | package org.vaadin.crudui.form.impl.field.provider; 2 | 3 | import java.util.Collection; 4 | 5 | import org.vaadin.crudui.form.FieldProvider; 6 | 7 | import com.vaadin.flow.component.Component; 8 | import com.vaadin.flow.component.HasValueAndElement; 9 | import com.vaadin.flow.data.provider.HasListDataView; 10 | import com.vaadin.flow.data.renderer.ComponentRenderer; 11 | import com.vaadin.flow.data.renderer.TextRenderer; 12 | 13 | /** 14 | * @author Alejandro Duarte 15 | */ 16 | public abstract class AbstractListingProvider & HasValueAndElement, T> 17 | implements FieldProvider { 18 | 19 | protected String caption; 20 | protected Collection items; 21 | protected ComponentRenderer renderer; 22 | 23 | protected abstract C buildAbstractListing(); 24 | 25 | public AbstractListingProvider(Collection items) { 26 | this(null, items, new TextRenderer<>()); 27 | } 28 | 29 | public AbstractListingProvider(String caption, Collection items) { 30 | this(caption, items, new TextRenderer<>()); 31 | } 32 | 33 | public AbstractListingProvider(String caption, Collection items, 34 | ComponentRenderer renderer) { 35 | this.caption = caption; 36 | this.items = items; 37 | this.renderer = renderer; 38 | } 39 | 40 | @Override 41 | public C buildField(T t) { 42 | C field = buildAbstractListing(); 43 | field.setItems(items); 44 | return field; 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /crud-ui-demo/src/main/java/org/vaadin/crudui/demo/ui/view/TreeView.java: -------------------------------------------------------------------------------- 1 | package org.vaadin.crudui.demo.ui.view; 2 | 3 | import com.vaadin.flow.component.combobox.ComboBox; 4 | import com.vaadin.flow.component.orderedlayout.VerticalLayout; 5 | import com.vaadin.flow.component.textfield.TextArea; 6 | import com.vaadin.flow.router.Route; 7 | 8 | import org.vaadin.crudui.crud.impl.TreeGridCrud; 9 | import org.vaadin.crudui.demo.entity.Technology; 10 | import org.vaadin.crudui.demo.service.TechnologyService; 11 | import org.vaadin.crudui.demo.ui.MainLayout; 12 | 13 | @Route(value = "tree", layout = MainLayout.class) 14 | public class TreeView extends VerticalLayout { 15 | 16 | public TreeView(TechnologyService technologyService) { 17 | TreeGridCrud crud = new TreeGridCrud<>(Technology.class); 18 | crud.setShowNotifications(false); 19 | 20 | crud.getGrid().removeAllColumns(); 21 | crud.getGrid().addHierarchyColumn(Technology::getName).setHeader("Name"); 22 | crud.getGrid().addColumn(Technology::getVersion).setHeader("Version"); 23 | crud.getGrid().addColumn(Technology::getLastPatchedAt).setHeader("Last Patched At"); 24 | 25 | crud.getCrudFormFactory().setVisibleProperties("name", "version", "parent", "lastPatchedAt", "description"); 26 | crud.getCrudFormFactory().setFieldProvider("description", technology -> new TextArea()); 27 | crud.getCrudFormFactory().setFieldProvider("parent", 28 | technology -> new ComboBox("Parent", technologyService.findAll())); 29 | 30 | crud.setFindAllOperation(technologyService::findRoots); 31 | crud.setChildItemProvider(technologyService::findChildren); 32 | crud.setAddOperation(technologyService::save); 33 | crud.setUpdateOperation(technologyService::save); 34 | crud.setDeleteOperation(technologyService::delete); 35 | 36 | addAndExpand(crud); 37 | setSizeFull(); 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /add-on/src/main/java/org/vaadin/crudui/form/impl/field/provider/DefaultFieldProvider.java: -------------------------------------------------------------------------------- 1 | package org.vaadin.crudui.form.impl.field.provider; 2 | 3 | import com.vaadin.flow.component.HasValueAndElement; 4 | import com.vaadin.flow.component.checkbox.Checkbox; 5 | import com.vaadin.flow.component.combobox.ComboBox; 6 | import com.vaadin.flow.component.datepicker.DatePicker; 7 | import com.vaadin.flow.component.datetimepicker.DateTimePicker; 8 | import com.vaadin.flow.component.textfield.BigDecimalField; 9 | import com.vaadin.flow.component.textfield.IntegerField; 10 | import com.vaadin.flow.component.textfield.NumberField; 11 | import com.vaadin.flow.component.textfield.TextField; 12 | import org.vaadin.crudui.form.FieldProvider; 13 | 14 | import java.math.BigDecimal; 15 | import java.time.LocalDate; 16 | import java.time.LocalDateTime; 17 | import java.util.Arrays; 18 | import java.util.Date; 19 | 20 | /** 21 | * @author Alejandro Duarte. 22 | */ 23 | public class DefaultFieldProvider implements FieldProvider { 24 | 25 | private Class type; 26 | 27 | public DefaultFieldProvider(Class type) { 28 | this.type = type; 29 | } 30 | 31 | @Override 32 | public HasValueAndElement buildField(Object t) { 33 | if (Boolean.class.isAssignableFrom(type) || boolean.class == type) { 34 | return new Checkbox(); 35 | } 36 | 37 | if (LocalDate.class.isAssignableFrom(type) || Date.class.isAssignableFrom(type)) { 38 | return new DatePicker(); 39 | } 40 | 41 | if (LocalDateTime.class.isAssignableFrom(type)) { 42 | return new DateTimePicker(); 43 | } 44 | 45 | if (Double.class.isAssignableFrom(type) || type == double.class) { 46 | return new NumberField(); 47 | } 48 | 49 | if (Integer.class.isAssignableFrom(type) || type == int.class) { 50 | return new IntegerField(); 51 | } 52 | 53 | if (BigDecimal.class.isAssignableFrom(type)) { 54 | return new BigDecimalField(); 55 | } 56 | 57 | if (Enum.class.isAssignableFrom(type)) { 58 | Object[] values = type.getEnumConstants(); 59 | ComboBox comboBox = new ComboBox<>(); 60 | comboBox.setItems(Arrays.asList(values)); 61 | return comboBox; 62 | } 63 | 64 | if (String.class.isAssignableFrom(type) || Character.class.isAssignableFrom(type) 65 | || Byte.class.isAssignableFrom(type) 66 | || Number.class.isAssignableFrom(type) || type.isPrimitive()) { 67 | return new TextField(); 68 | } 69 | 70 | return null; 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /add-on/src/main/java/org/vaadin/data/converter/StringToByteConverter.java: -------------------------------------------------------------------------------- 1 | package org.vaadin.data.converter; 2 | 3 | import com.vaadin.flow.data.binder.Result; 4 | import com.vaadin.flow.data.binder.ValueContext; 5 | import com.vaadin.flow.data.converter.AbstractStringToNumberConverter; 6 | 7 | import java.text.NumberFormat; 8 | import java.util.Locale; 9 | 10 | public class StringToByteConverter extends AbstractStringToNumberConverter { 11 | 12 | /** 13 | * Creates a new converter instance with the given error message. Empty 14 | * strings are converted to null. 15 | * 16 | * @param errorMessage the error message to use if conversion fails 17 | */ 18 | public StringToByteConverter(String errorMessage) { 19 | this(null, errorMessage); 20 | } 21 | 22 | /** 23 | * Creates a new converter instance with the given empty string value and 24 | * error message. 25 | * 26 | * @param emptyValue the presentation value to return when converting an empty 27 | * string, may be null 28 | * @param errorMessage the error message to use if conversion fails 29 | */ 30 | public StringToByteConverter(Byte emptyValue, String errorMessage) { 31 | super(emptyValue, errorMessage); 32 | } 33 | 34 | /** 35 | * Returns the format used by 36 | * {@link #convertToPresentation(Object, ValueContext)} and 37 | * {@link #convertToModel(String, ValueContext)}. 38 | * 39 | * @param locale The locale to use 40 | * @return A NumberFormat instance 41 | */ 42 | @Override 43 | protected NumberFormat getFormat(Locale locale) { 44 | if (locale == null) { 45 | locale = Locale.getDefault(); 46 | } 47 | return NumberFormat.getIntegerInstance(locale); 48 | } 49 | 50 | @Override 51 | public Result convertToModel(String value, ValueContext context) { 52 | Result n = convertToNumber(value, context); 53 | return n.flatMap(number -> { 54 | if (number == null) { 55 | return Result.ok(null); 56 | } else { 57 | byte intValue = number.byteValue(); 58 | if (intValue == number.longValue()) { 59 | // If the value of n is outside the range of long, the 60 | // return value of longValue() is either Long.MIN_VALUE or 61 | // Long.MAX_VALUE. The/ above comparison promotes int to 62 | // long and thus does not need to consider wrap-around. 63 | return Result.ok(intValue); 64 | } else { 65 | return Result.error(getErrorMessage(context)); 66 | } 67 | } 68 | }); 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /crud-ui-demo/src/main/java/org/vaadin/crudui/demo/service/TechnologyService.java: -------------------------------------------------------------------------------- 1 | package org.vaadin.crudui.demo.service; 2 | 3 | import java.util.HashSet; 4 | import java.util.List; 5 | import java.util.Set; 6 | import java.util.stream.Collectors; 7 | 8 | import org.springframework.stereotype.Service; 9 | import org.springframework.transaction.annotation.Propagation; 10 | import org.springframework.transaction.annotation.Transactional; 11 | import org.vaadin.crudui.demo.entity.Technology; 12 | import org.vaadin.crudui.demo.repository.TechnologyRepository; 13 | 14 | @Service 15 | @Transactional(propagation = Propagation.SUPPORTS, readOnly = true) 16 | public class TechnologyService { 17 | 18 | private final TechnologyRepository technologyRepository; 19 | 20 | public TechnologyService(TechnologyRepository technologyRepository) { 21 | this.technologyRepository = technologyRepository; 22 | } 23 | 24 | @Transactional 25 | public Technology save(Technology technology) { 26 | validate(technology); 27 | technology = technologyRepository.save(technology); 28 | return technology; 29 | } 30 | 31 | private void validate(Technology technology) { 32 | Technology parent = technology.getParent(); 33 | 34 | // if is its on parent 35 | if (parent != null && parent.equals(technology)) 36 | throw new IllegalArgumentException("Technology can not be its own parent"); 37 | } 38 | 39 | @Transactional 40 | public void delete(Technology technology) { 41 | List children = findChildren(technology); 42 | children.forEach(this::delete); 43 | technologyRepository.delete(technology); 44 | } 45 | 46 | public List findChildren(Technology technology) { 47 | return technologyRepository.findAllByParent(technology); 48 | } 49 | 50 | public List findRoots() { 51 | return technologyRepository.findAllByParentIsNull(); 52 | } 53 | 54 | public List findAll() { 55 | return technologyRepository.findAll(); 56 | } 57 | 58 | public List getLeaves() { 59 | List categories = findAll(); 60 | 61 | Set parents = new HashSet<>(); 62 | categories.forEach(technology -> { 63 | if (technology.hasParent()) 64 | parents.add(technology.getParent()); 65 | }); 66 | 67 | return categories.stream().filter(technology -> { 68 | return !parents.contains(technology); 69 | }).collect(Collectors.toList()); 70 | } 71 | 72 | public Technology getTechnology(Long technologyId) { 73 | return technologyRepository.findById(technologyId) 74 | .orElseThrow(() -> new IllegalArgumentException("There is no technology with id " + technologyId)); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /crud-ui-demo/src/main/java/org/vaadin/crudui/demo/entity/Technology.java: -------------------------------------------------------------------------------- 1 | package org.vaadin.crudui.demo.entity; 2 | 3 | import java.io.Serializable; 4 | import java.time.LocalDateTime; 5 | 6 | import jakarta.persistence.Column; 7 | import jakarta.persistence.Entity; 8 | import jakarta.persistence.GeneratedValue; 9 | import jakarta.persistence.Id; 10 | import jakarta.persistence.Lob; 11 | import jakarta.persistence.ManyToOne; 12 | import jakarta.persistence.Table; 13 | import jakarta.validation.constraints.NotBlank; 14 | import jakarta.validation.constraints.NotNull; 15 | import jakarta.validation.constraints.Past; 16 | import jakarta.validation.constraints.Positive; 17 | 18 | @Entity 19 | @Table(name = "technology") 20 | public class Technology implements Serializable { 21 | 22 | @Id 23 | @GeneratedValue 24 | private Long id; 25 | 26 | @NotBlank 27 | @NotNull 28 | @Column(unique = true) 29 | private String name; 30 | 31 | @Positive 32 | private Double version; 33 | 34 | @Lob 35 | private String description; 36 | 37 | @Past 38 | private LocalDateTime lastPatchedAt; 39 | 40 | @ManyToOne 41 | private Technology parent; 42 | 43 | public Technology() { 44 | } 45 | 46 | public Technology(String name, Double version, String description, LocalDateTime lastPatchedAt, Technology parent) { 47 | this.name = name; 48 | this.version = version; 49 | this.description = description; 50 | this.lastPatchedAt = lastPatchedAt; 51 | this.parent = parent; 52 | } 53 | 54 | public Long getId() { 55 | return id; 56 | } 57 | 58 | public void setId(Long id) { 59 | this.id = id; 60 | } 61 | 62 | public Double getVersion() { 63 | return version; 64 | } 65 | 66 | public void setVersion(Double version) { 67 | this.version = version; 68 | } 69 | 70 | public String getName() { 71 | return name; 72 | } 73 | 74 | public void setName(String name) { 75 | this.name = name; 76 | } 77 | 78 | public String getDescription() { 79 | return description; 80 | } 81 | 82 | public LocalDateTime getLastPatchedAt() { 83 | return lastPatchedAt; 84 | } 85 | 86 | public void setLastPatchedAt(LocalDateTime lastPatchedAt) { 87 | this.lastPatchedAt = lastPatchedAt; 88 | } 89 | 90 | public void setDescription(String description) { 91 | this.description = description; 92 | } 93 | 94 | public Technology getParent() { 95 | return parent; 96 | } 97 | 98 | public void setParent(Technology parent) { 99 | this.parent = parent; 100 | } 101 | 102 | public String toString() { 103 | return name; 104 | } 105 | 106 | public boolean hasParent() { 107 | return parent != null; 108 | } 109 | 110 | public boolean isRoot() { 111 | return !hasParent(); 112 | } 113 | 114 | } 115 | -------------------------------------------------------------------------------- /add-on/src/main/java/org/vaadin/crudui/form/CrudFormFactory.java: -------------------------------------------------------------------------------- 1 | package org.vaadin.crudui.form; 2 | 3 | import com.vaadin.flow.component.ClickEvent; 4 | import com.vaadin.flow.component.Component; 5 | import com.vaadin.flow.component.ComponentEventListener; 6 | import com.vaadin.flow.component.HasValueAndElement; 7 | import com.vaadin.flow.component.button.Button; 8 | import com.vaadin.flow.data.converter.Converter; 9 | import com.vaadin.flow.function.SerializableConsumer; 10 | import com.vaadin.flow.function.SerializableSupplier; 11 | import org.vaadin.crudui.crud.CrudOperation; 12 | 13 | import java.io.Serializable; 14 | 15 | /** 16 | * @author Alejandro Duarte 17 | */ 18 | public interface CrudFormFactory extends Serializable { 19 | 20 | void setNewInstanceSupplier(SerializableSupplier newInstanceSupplier); 21 | 22 | SerializableSupplier getNewInstanceSupplier(); 23 | 24 | Component buildNewForm(CrudOperation operation, T domainObject, boolean readOnly, 25 | ComponentEventListener> cancelButtonClickListener, 26 | ComponentEventListener> operationButtonClickListener); 27 | 28 | String buildCaption(CrudOperation operation, T domainObject); 29 | 30 | void setCaption(CrudOperation operation, String caption); 31 | 32 | void setVisibleProperties(CrudOperation operation, String... properties); 33 | 34 | void setVisibleProperties(String... properties); 35 | 36 | void setDisabledProperties(CrudOperation operation, String... properties); 37 | 38 | void setDisabledProperties(String... properties); 39 | 40 | void setFieldCaptions(CrudOperation operation, String... captions); 41 | 42 | void setFieldCaptions(String... captions); 43 | 44 | void setFieldType(CrudOperation operation, String property, Class> type); 45 | 46 | void setFieldType(String property, Class> type); 47 | 48 | void setFieldCreationListener(CrudOperation operation, String property, FieldCreationListener listener); 49 | 50 | void setFieldCreationListener(String property, FieldCreationListener listener); 51 | 52 | void setFieldProvider(CrudOperation operation, String property, FieldProvider provider); 53 | 54 | void setFieldProvider(String property, FieldProvider provider); 55 | 56 | void setConverter(CrudOperation operation, String property, Converter converter); 57 | 58 | void setConverter(String property, Converter converter); 59 | 60 | void setUseBeanValidation(CrudOperation operation, boolean useBeanValidation); 61 | 62 | void setUseBeanValidation(boolean useBeanValidation); 63 | 64 | void setShowNotifications(boolean showNotifications); 65 | 66 | void setErrorListener(SerializableConsumer errorListener); 67 | 68 | void showError(CrudOperation operation, Exception e); 69 | 70 | } 71 | -------------------------------------------------------------------------------- /add-on/src/main/java/org/vaadin/crudui/crud/impl/TreeGridCrud.java: -------------------------------------------------------------------------------- 1 | package org.vaadin.crudui.crud.impl; 2 | 3 | import java.util.Collection; 4 | 5 | import com.vaadin.flow.component.grid.Grid; 6 | import com.vaadin.flow.component.treegrid.TreeGrid; 7 | import com.vaadin.flow.data.provider.DataProvider; 8 | import com.vaadin.flow.data.provider.hierarchy.HierarchicalDataProvider; 9 | import com.vaadin.flow.function.ValueProvider; 10 | 11 | import org.vaadin.crudui.crud.CrudListener; 12 | import org.vaadin.crudui.crud.LazyFindAllCrudOperationListener; 13 | import org.vaadin.crudui.form.CrudFormFactory; 14 | import org.vaadin.crudui.layout.CrudLayout; 15 | 16 | public class TreeGridCrud extends AbstractGridCrud { 17 | 18 | private ValueProvider> childItemProvider; 19 | 20 | public TreeGridCrud(Class domainType) { 21 | super(domainType); 22 | } 23 | 24 | public TreeGridCrud(Class domainType, CrudLayout crudLayout) { 25 | super(domainType, crudLayout); 26 | } 27 | 28 | public TreeGridCrud(Class domainType, CrudFormFactory crudFormFactory) { 29 | super(domainType, crudFormFactory); 30 | } 31 | 32 | public TreeGridCrud(Class domainType, CrudListener crudListener) { 33 | super(domainType, crudListener); 34 | } 35 | 36 | public TreeGridCrud(Class domainType, CrudLayout crudLayout, CrudFormFactory crudFormFactory) { 37 | super(domainType, crudLayout, crudFormFactory); 38 | } 39 | 40 | public TreeGridCrud(Class domainType, CrudLayout crudLayout, CrudFormFactory crudFormFactory, 41 | CrudListener crudListener) { 42 | super(domainType, crudLayout, crudFormFactory, crudListener); 43 | } 44 | 45 | @Override 46 | protected Grid createGrid() { 47 | return new TreeGrid<>(domainType); 48 | } 49 | 50 | @Override 51 | public TreeGrid getGrid() { 52 | return (TreeGrid) super.getGrid(); 53 | } 54 | 55 | public void refreshGrid() { 56 | if (LazyFindAllCrudOperationListener.class.isAssignableFrom(findAllOperation.getClass())) { 57 | LazyFindAllCrudOperationListener findAll = (LazyFindAllCrudOperationListener) findAllOperation; 58 | DataProvider dataProvider = findAll.getDataProvider(); 59 | 60 | if (!HierarchicalDataProvider.class.isAssignableFrom(dataProvider.getClass())) 61 | throw new UnsupportedOperationException( 62 | "The data provider for TreeGridCrud must implement HierarchicalDataProvider"); 63 | 64 | getGrid().setItems(dataProvider); 65 | } else { 66 | Collection items = findAllOperation.findAll(); 67 | getGrid().setItems(items, childItemProvider); 68 | } 69 | } 70 | 71 | public ValueProvider> getChildItemProvider() { 72 | return childItemProvider; 73 | } 74 | 75 | public void setChildItemProvider(ValueProvider> childItemProvider) { 76 | this.childItemProvider = childItemProvider; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /add-on/src/main/java/org/vaadin/crudui/crud/impl/GridCrud.java: -------------------------------------------------------------------------------- 1 | package org.vaadin.crudui.crud.impl; 2 | 3 | import java.util.Collection; 4 | import java.util.Map; 5 | import java.util.WeakHashMap; 6 | 7 | import org.vaadin.crudui.crud.CrudListener; 8 | import org.vaadin.crudui.crud.LazyFindAllCrudOperationListener; 9 | import org.vaadin.crudui.form.CrudFormFactory; 10 | import org.vaadin.crudui.layout.CrudLayout; 11 | 12 | import com.vaadin.flow.component.button.Button; 13 | import com.vaadin.flow.component.grid.Grid; 14 | import com.vaadin.flow.component.grid.Grid.Column; 15 | import com.vaadin.flow.component.icon.VaadinIcon; 16 | 17 | /** 18 | * @author Alejandro Duarte 19 | */ 20 | public class GridCrud extends AbstractGridCrud { 21 | 22 | private Column componentColumn; 23 | 24 | private final Map map = new WeakHashMap<>(); 25 | 26 | public GridCrud(Class domainType) { 27 | super(domainType); 28 | } 29 | 30 | public GridCrud(Class domainType, CrudLayout crudLayout) { 31 | super(domainType, crudLayout); 32 | } 33 | 34 | public GridCrud(Class domainType, CrudFormFactory crudFormFactory) { 35 | super(domainType, crudFormFactory); 36 | } 37 | 38 | public GridCrud(Class domainType, CrudListener crudListener) { 39 | super(domainType, crudListener); 40 | } 41 | 42 | public GridCrud(Class domainType, CrudLayout crudLayout, CrudFormFactory crudFormFactory) { 43 | super(domainType, crudLayout, crudFormFactory); 44 | } 45 | 46 | public GridCrud(Class domainType, CrudLayout crudLayout, CrudFormFactory crudFormFactory, 47 | CrudListener crudListener) { 48 | super(domainType, crudLayout, crudFormFactory, crudListener); 49 | } 50 | 51 | @Override 52 | protected Grid createGrid() { 53 | return new Grid(domainType); 54 | } 55 | 56 | public void refreshGrid() { 57 | if (LazyFindAllCrudOperationListener.class.isAssignableFrom(findAllOperation.getClass())) { 58 | LazyFindAllCrudOperationListener findAll = (LazyFindAllCrudOperationListener) findAllOperation; 59 | 60 | grid.setDataProvider(findAll.getDataProvider()); 61 | 62 | } else { 63 | Collection items = findAllOperation.findAll(); 64 | grid.setItems(items); 65 | } 66 | } 67 | 68 | public void addUpdateButtonColumn() { 69 | if (componentColumn == null) { 70 | componentColumn = grid.addComponentColumn(item -> { 71 | Button button = new Button(VaadinIcon.PENCIL.create()); 72 | button.addClickListener(e -> { 73 | grid.select(item); 74 | updateButtonClicked(); 75 | }); 76 | 77 | if (map.put(item, button) == null) { 78 | button.setEnabled(false); 79 | } 80 | 81 | return button; 82 | }); 83 | } 84 | } 85 | 86 | public Button getUpdateButton(T item) { 87 | return map.get(item); 88 | } 89 | 90 | public void setUpdateButtonColumnEnabled(boolean enabled) { 91 | map.values().stream().forEach(b -> b.setEnabled(enabled)); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /add-on/src/main/java/org/vaadin/crudui/form/impl/form/factory/DefaultCrudFormFactory.java: -------------------------------------------------------------------------------- 1 | package org.vaadin.crudui.form.impl.form.factory; 2 | 3 | import java.util.List; 4 | 5 | import com.vaadin.flow.component.ClickEvent; 6 | import com.vaadin.flow.component.Component; 7 | import com.vaadin.flow.component.ComponentEventListener; 8 | import com.vaadin.flow.component.HasValueAndElement; 9 | import com.vaadin.flow.component.button.Button; 10 | import com.vaadin.flow.component.formlayout.FormLayout; 11 | import com.vaadin.flow.component.orderedlayout.FlexComponent.Alignment; 12 | import com.vaadin.flow.component.orderedlayout.VerticalLayout; 13 | 14 | import org.vaadin.crudui.crud.CrudOperation; 15 | import org.vaadin.crudui.form.AbstractAutoGeneratedCrudFormFactory; 16 | 17 | /** 18 | * @author Alejandro Duarte 19 | */ 20 | public class DefaultCrudFormFactory extends AbstractAutoGeneratedCrudFormFactory { 21 | 22 | private FormLayout.ResponsiveStep[] responsiveSteps; 23 | 24 | public DefaultCrudFormFactory(Class domainType) { 25 | this(domainType, (FormLayout.ResponsiveStep[]) null); 26 | } 27 | 28 | public DefaultCrudFormFactory(Class domainType, FormLayout.ResponsiveStep... responsiveSteps) { 29 | super(domainType); 30 | if (responsiveSteps != null) { 31 | this.responsiveSteps = responsiveSteps; 32 | } else { 33 | this.responsiveSteps = new FormLayout.ResponsiveStep[] { 34 | new FormLayout.ResponsiveStep("0em", 1), 35 | new FormLayout.ResponsiveStep("25em", 2) 36 | }; 37 | } 38 | } 39 | 40 | @Override 41 | public Component buildNewForm(CrudOperation operation, T domainObject, boolean readOnly, 42 | ComponentEventListener> cancelButtonClickListener, 43 | ComponentEventListener> operationButtonClickListener) { 44 | FormLayout formLayout = new FormLayout(); 45 | formLayout.setSizeFull(); 46 | formLayout.setResponsiveSteps(responsiveSteps); 47 | 48 | List fields = buildFields(operation, domainObject, readOnly); 49 | fields.stream() 50 | .forEach(field -> formLayout.getElement().appendChild(field.getElement())); 51 | 52 | Component footerLayout = buildFooter(operation, domainObject, cancelButtonClickListener, 53 | operationButtonClickListener); 54 | 55 | VerticalLayout mainLayout = new VerticalLayout(formLayout, footerLayout); 56 | mainLayout.setHorizontalComponentAlignment(Alignment.END, footerLayout); 57 | mainLayout.setMargin(false); 58 | mainLayout.setPadding(false); 59 | mainLayout.setSpacing(true); 60 | 61 | configureForm(formLayout, fields); 62 | 63 | return mainLayout; 64 | } 65 | 66 | protected void configureForm(FormLayout formLayout, List fields) { 67 | // No-op 68 | } 69 | 70 | @Override 71 | public String buildCaption(CrudOperation operation, T domainObject) { 72 | // If null, CrudLayout.showForm will build its own, for backward compatibility 73 | return configurations.get(operation).getCaption(); 74 | } 75 | 76 | } 77 | -------------------------------------------------------------------------------- /add-on/src/main/java/org/vaadin/crudui/form/CrudFormConfiguration.java: -------------------------------------------------------------------------------- 1 | package org.vaadin.crudui.form; 2 | 3 | import java.io.Serializable; 4 | import java.util.ArrayList; 5 | import java.util.HashMap; 6 | import java.util.List; 7 | import java.util.Map; 8 | 9 | import com.vaadin.flow.component.HasValueAndElement; 10 | import com.vaadin.flow.data.converter.Converter; 11 | 12 | /** 13 | * @author Alejandro Duarte. 14 | */ 15 | public class CrudFormConfiguration implements Serializable { 16 | 17 | protected String caption; 18 | protected List visibleProperties = new ArrayList<>(); 19 | protected List disabledProperties = new ArrayList<>(); 20 | protected List fieldCaptions = new ArrayList<>(); 21 | protected Map>> fieldTypes = new HashMap<>(); 22 | protected Map fieldCreationListeners = new HashMap<>(); 23 | protected Map> fieldProviders = new HashMap<>(); 24 | protected Map> converters = new HashMap<>(); 25 | protected boolean useBeanValidation; 26 | 27 | public String getCaption() { 28 | return caption; 29 | } 30 | 31 | public void setCaption(String caption) { 32 | this.caption = caption; 33 | } 34 | 35 | public List getVisibleProperties() { 36 | return visibleProperties; 37 | } 38 | 39 | public void setVisibleProperties(List visibleProperties) { 40 | this.visibleProperties = visibleProperties; 41 | } 42 | 43 | public List getDisabledProperties() { 44 | return disabledProperties; 45 | } 46 | 47 | public void setDisabledProperties(List disabledProperties) { 48 | this.disabledProperties = disabledProperties; 49 | } 50 | 51 | public List getFieldCaptions() { 52 | return fieldCaptions; 53 | } 54 | 55 | public void setFieldCaptions(List fieldCaptions) { 56 | this.fieldCaptions = fieldCaptions; 57 | } 58 | 59 | public Map>> getFieldTypes() { 60 | return fieldTypes; 61 | } 62 | 63 | public void setFieldTypes(Map>> fieldTypes) { 64 | this.fieldTypes = fieldTypes; 65 | } 66 | 67 | public Map getFieldCreationListeners() { 68 | return fieldCreationListeners; 69 | } 70 | 71 | public void setFieldCreationListeners(Map fieldCreationListeners) { 72 | this.fieldCreationListeners = fieldCreationListeners; 73 | } 74 | 75 | public Map> getFieldProviders() { 76 | return fieldProviders; 77 | } 78 | 79 | public void setFieldProviders(Map> fieldProviders) { 80 | this.fieldProviders = fieldProviders; 81 | } 82 | 83 | public Map> getConverters() { 84 | return converters; 85 | } 86 | 87 | public void setConverters(Map> converters) { 88 | this.converters = converters; 89 | } 90 | 91 | public boolean isUseBeanValidation() { 92 | return useBeanValidation; 93 | } 94 | 95 | public void setUseBeanValidation(boolean useBeanValidation) { 96 | this.useBeanValidation = useBeanValidation; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /add-on/src/main/java/org/vaadin/crudui/crud/AbstractCrud.java: -------------------------------------------------------------------------------- 1 | package org.vaadin.crudui.crud; 2 | 3 | import java.util.Collections; 4 | 5 | import com.vaadin.flow.component.Component; 6 | import com.vaadin.flow.component.Composite; 7 | import com.vaadin.flow.component.HasSize; 8 | import com.vaadin.flow.component.orderedlayout.VerticalLayout; 9 | import com.vaadin.flow.data.provider.DataProvider; 10 | 11 | import org.vaadin.crudui.form.CrudFormFactory; 12 | import org.vaadin.crudui.layout.CrudLayout; 13 | 14 | /** 15 | * @author Alejandro Duarte 16 | */ 17 | public abstract class AbstractCrud extends Composite implements Crud, HasSize { 18 | 19 | protected Class domainType; 20 | 21 | protected FindAllCrudOperationListener findAllOperation = Collections::emptyList; 22 | protected AddOperationListener addOperation = t -> null; 23 | protected UpdateOperationListener updateOperation = t -> null; 24 | protected DeleteOperationListener deleteOperation = t -> { 25 | }; 26 | 27 | protected CrudLayout crudLayout; 28 | protected CrudFormFactory crudFormFactory; 29 | 30 | public AbstractCrud(Class domainType, CrudLayout crudLayout, CrudFormFactory crudFormFactory, 31 | CrudListener crudListener) { 32 | this.domainType = domainType; 33 | this.crudLayout = crudLayout; 34 | this.crudFormFactory = crudFormFactory; 35 | 36 | if (crudListener != null) { 37 | setCrudListener(crudListener); 38 | } 39 | getContent().add((Component) crudLayout); 40 | 41 | getContent().setPadding(false); 42 | getContent().setMargin(false); 43 | setSizeFull(); 44 | } 45 | 46 | public CrudLayout getCrudLayout() { 47 | return crudLayout; 48 | } 49 | 50 | @Override 51 | public void setCrudFormFactory(CrudFormFactory crudFormFactory) { 52 | this.crudFormFactory = crudFormFactory; 53 | } 54 | 55 | @Override 56 | public void setFindAllOperation(FindAllCrudOperationListener findAllOperation) { 57 | this.findAllOperation = findAllOperation; 58 | } 59 | 60 | @Override 61 | public void setFindAllOperation(DataProvider dataProvider) { 62 | this.findAllOperation = (LazyFindAllCrudOperationListener) () -> dataProvider; 63 | } 64 | 65 | @Override 66 | public void setAddOperation(AddOperationListener addOperation) { 67 | this.addOperation = addOperation; 68 | } 69 | 70 | @Override 71 | public void setUpdateOperation(UpdateOperationListener updateOperation) { 72 | this.updateOperation = updateOperation; 73 | } 74 | 75 | @Override 76 | public void setDeleteOperation(DeleteOperationListener deleteOperation) { 77 | this.deleteOperation = deleteOperation; 78 | } 79 | 80 | @Override 81 | public void setOperations(FindAllCrudOperationListener findAllOperation, AddOperationListener addOperation, 82 | UpdateOperationListener updateOperation, DeleteOperationListener deleteOperation) { 83 | setFindAllOperation(findAllOperation); 84 | setAddOperation(addOperation); 85 | setUpdateOperation(updateOperation); 86 | setDeleteOperation(deleteOperation); 87 | } 88 | 89 | @Override 90 | public CrudFormFactory getCrudFormFactory() { 91 | return crudFormFactory; 92 | } 93 | 94 | @Override 95 | public void setCrudListener(CrudListener crudListener) { 96 | setAddOperation(crudListener::add); 97 | setUpdateOperation(crudListener::update); 98 | setDeleteOperation(crudListener::delete); 99 | 100 | if (LazyCrudListener.class.isAssignableFrom(crudListener.getClass())) { 101 | setFindAllOperation( 102 | (LazyFindAllCrudOperationListener) ((LazyCrudListener) crudListener)::getDataProvider); 103 | } else { 104 | setFindAllOperation(crudListener::findAll); 105 | } 106 | } 107 | 108 | } 109 | -------------------------------------------------------------------------------- /crud-ui-demo/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | org.springframework.boot 8 | spring-boot-starter-parent 9 | 3.3.1 10 | 11 | 12 | 13 | org.vaadin.crudui 14 | crud-ui-demo 15 | 2.0.0 16 | jar 17 | 18 | 19 | 21 20 | 24.4.4 21 | 22 | 23 | 24 | 25 | vaadin-prereleases 26 | https://maven.vaadin.com/vaadin-prereleases/ 27 | 28 | 29 | vaadin-addons 30 | https://maven.vaadin.com/vaadin-addons 31 | 32 | 33 | 34 | 35 | 36 | 37 | com.vaadin 38 | vaadin-bom 39 | ${vaadin.version} 40 | pom 41 | import 42 | 43 | 44 | 45 | 46 | 47 | 48 | org.springframework.boot 49 | spring-boot-starter-data-jpa 50 | 51 | 52 | com.vaadin 53 | vaadin-spring-boot-starter 54 | 55 | 56 | org.vaadin.crudui 57 | crudui 58 | 7.2.0 59 | 60 | 61 | com.flowingcode.vaadin.addons 62 | markdown-editor-addon 63 | 1.1.0 64 | 65 | 66 | 67 | org.springframework.boot 68 | spring-boot-devtools 69 | runtime 70 | true 71 | 72 | 73 | com.h2database 74 | h2 75 | runtime 76 | 77 | 78 | 79 | 80 | 81 | 82 | org.springframework.boot 83 | spring-boot-maven-plugin 84 | 85 | 86 | 87 | org.projectlombok 88 | lombok 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | production 99 | 100 | 101 | 102 | com.vaadin 103 | vaadin-maven-plugin 104 | ${vaadin.version} 105 | 106 | 107 | frontend 108 | compile 109 | 110 | prepare-frontend 111 | build-frontend 112 | 113 | 114 | true 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | -------------------------------------------------------------------------------- /crud-ui-demo/src/main/java/org/vaadin/crudui/demo/Application.java: -------------------------------------------------------------------------------- 1 | package org.vaadin.crudui.demo; 2 | 3 | import java.math.BigDecimal; 4 | import java.time.LocalDate; 5 | import java.time.LocalDateTime; 6 | import java.util.ArrayList; 7 | import java.util.HashSet; 8 | import java.util.List; 9 | import java.util.Random; 10 | import java.util.UUID; 11 | import java.util.stream.Collectors; 12 | import java.util.stream.IntStream; 13 | import java.util.stream.Stream; 14 | 15 | import org.slf4j.Logger; 16 | import org.slf4j.LoggerFactory; 17 | import org.springframework.boot.SpringApplication; 18 | import org.springframework.boot.autoconfigure.SpringBootApplication; 19 | import org.springframework.context.ApplicationListener; 20 | import org.springframework.context.annotation.Bean; 21 | import org.springframework.context.event.ContextRefreshedEvent; 22 | import org.vaadin.crudui.demo.entity.Group; 23 | import org.vaadin.crudui.demo.entity.MaritalStatus; 24 | import org.vaadin.crudui.demo.entity.Technology; 25 | import org.vaadin.crudui.demo.entity.User; 26 | import org.vaadin.crudui.demo.service.GroupService; 27 | import org.vaadin.crudui.demo.service.TechnologyService; 28 | import org.vaadin.crudui.demo.service.UserService; 29 | 30 | /** 31 | * The entry point of the Spring Boot application. 32 | */ 33 | @SpringBootApplication 34 | public class Application { 35 | 36 | public static final int DEMO_USERS_COUNT = UserService.USERS_COUNT_LIMIT / 2; 37 | 38 | private static Logger log = LoggerFactory.getLogger(Application.class); 39 | 40 | public static void main(String[] args) { 41 | SpringApplication.run(Application.class, args); 42 | } 43 | 44 | @Bean 45 | public ApplicationListener initDatabase(GroupService groupService, UserService userService, 46 | TechnologyService technologyService) { 47 | return event -> { 48 | if (groupService.count() == 0) { 49 | createDemoData(groupService, userService, technologyService); 50 | } 51 | }; 52 | } 53 | 54 | private void createDemoData(GroupService groupService, UserService userService, 55 | TechnologyService technologyService) { 56 | log.info("Creating demo data..."); 57 | 58 | Stream.of("Services,IT,HR,Management,Marketing,Sales,Operations,Finance".split(",")) 59 | .map(Group::new) 60 | .forEach(groupService::save); 61 | 62 | List allGroups = groupService.findAll(); 63 | 64 | groupService.findAll(); 65 | 66 | String[] firstNames = "Maria,Edgar,Juan,Angelica,Nicole,Brenda,Clare,Cathy,Elizabeth,Tom,John,Daniel,Edward,Hank,Arthur,Bill,Alejandro" 67 | .split(","); 68 | String[] lastNames = "Smith,Duarte,Avendano,Vento,Johnson,Williams,Jones,Brown,Miller,Wilson,Wright,Thompson,Lee" 69 | .split(","); 70 | 71 | Random rand = new Random(); 72 | 73 | IntStream.rangeClosed(1, DEMO_USERS_COUNT) 74 | .mapToObj(i -> { 75 | String name = firstNames[rand.nextInt(firstNames.length)] + " " 76 | + lastNames[rand.nextInt(lastNames.length)]; 77 | ArrayList groups = IntStream.rangeClosed(1, 1 + rand.nextInt(2)) 78 | .mapToObj(j -> allGroups.get(rand.nextInt(allGroups.size()))) 79 | .collect(Collectors.toCollection(ArrayList::new)); 80 | 81 | return new User( 82 | name, 83 | LocalDate.now().minusDays(rand.nextInt(365 * 99)), 84 | rand.nextInt(9000000) + 1000000, 85 | name.replace(" ", "").toLowerCase() + i + "@test.com", 86 | BigDecimal.valueOf(5000), 87 | UUID.randomUUID().toString(), 88 | rand.nextInt(10) > 0, 89 | groups.get(rand.nextInt(groups.size())), 90 | new HashSet<>(groups), 91 | MaritalStatus.values()[rand.nextInt(MaritalStatus.values().length)]); 92 | }) 93 | .forEach(userService::save); 94 | 95 | String[] parentTechs = new String[] { "Java", "Javascript", "Databases" }; 96 | String[][] childrenTechs = new String[][] { 97 | { "Vaadin", "Spring", "Quarkus" }, 98 | { "Hilla", "React", "Svelte" }, 99 | { "MariaDB", "MySQL", "Postgres" } 100 | }; 101 | 102 | for (int i = 0; i < parentTechs.length; i++) { 103 | Technology parentTech = technologyService 104 | .save(new Technology(parentTechs[i], null, parentTechs[i], null, null)); 105 | for (int j = 0; j < childrenTechs[i].length; j++) { 106 | var technology = new Technology(childrenTechs[i][j], rand.nextDouble() * 10, childrenTechs[i][j], 107 | LocalDateTime.now().minusHours(rand.nextInt(24 * 60)), parentTech); 108 | technologyService.save(technology); 109 | } 110 | } 111 | 112 | log.info("Demo data created."); 113 | } 114 | 115 | } 116 | -------------------------------------------------------------------------------- /crud-ui-demo/src/main/java/org/vaadin/crudui/demo/entity/User.java: -------------------------------------------------------------------------------- 1 | package org.vaadin.crudui.demo.entity; 2 | 3 | import java.math.BigDecimal; 4 | import java.time.LocalDate; 5 | import java.util.HashSet; 6 | import java.util.Set; 7 | 8 | import org.hibernate.annotations.Fetch; 9 | import org.hibernate.annotations.FetchMode; 10 | 11 | import jakarta.persistence.Column; 12 | import jakarta.persistence.Entity; 13 | import jakarta.persistence.GeneratedValue; 14 | import jakarta.persistence.Id; 15 | import jakarta.persistence.ManyToMany; 16 | import jakarta.persistence.ManyToOne; 17 | import jakarta.persistence.Table; 18 | import jakarta.validation.constraints.Email; 19 | import jakarta.validation.constraints.NotNull; 20 | import jakarta.validation.constraints.Past; 21 | import jakarta.validation.constraints.Size; 22 | 23 | @Entity 24 | @Table(name = "user_") 25 | public class User { 26 | 27 | @NotNull 28 | @Id 29 | @GeneratedValue 30 | private Long id; 31 | 32 | @NotNull 33 | private String name; 34 | 35 | @Past 36 | private LocalDate birthDate; 37 | 38 | @NotNull 39 | private int phoneNumber; // as an int for testing purposes 40 | 41 | @NotNull 42 | @Email 43 | @Column(unique = true) 44 | private String email; 45 | 46 | @NotNull 47 | private BigDecimal salary; 48 | 49 | @NotNull 50 | @Size(min = 6, max = 100) 51 | private String password; 52 | 53 | private Boolean active = true; 54 | 55 | @ManyToOne 56 | private Group mainGroup; 57 | 58 | @ManyToMany 59 | @Fetch(FetchMode.JOIN) 60 | @NotNull 61 | private Set groups = new HashSet<>(); 62 | 63 | private MaritalStatus maritalStatus; 64 | 65 | public User() { 66 | } 67 | 68 | public User(@NotNull String name, @Past LocalDate birthDate, @NotNull int phoneNumber, @NotNull @Email String email, 69 | @NotNull BigDecimal salary, 70 | @NotNull @Size(min = 6, max = 100) String password, Boolean active, Group mainGroup, Set groups, 71 | MaritalStatus maritalStatus) { 72 | this.name = name; 73 | this.birthDate = birthDate; 74 | this.phoneNumber = phoneNumber; 75 | this.email = email; 76 | this.salary = salary; 77 | this.password = password; 78 | this.active = active; 79 | this.mainGroup = mainGroup; 80 | this.groups = groups; 81 | this.maritalStatus = maritalStatus; 82 | } 83 | 84 | @Override 85 | public boolean equals(Object o) { 86 | if (this == o) 87 | return true; 88 | if (o == null || getClass() != o.getClass()) 89 | return false; 90 | 91 | User user = (User) o; 92 | 93 | return id != null ? id.equals(user.id) : user.id == null; 94 | } 95 | 96 | @Override 97 | public int hashCode() { 98 | return id != null ? id.hashCode() : 0; 99 | } 100 | 101 | public Long getId() { 102 | return id; 103 | } 104 | 105 | public void setId(Long id) { 106 | this.id = id; 107 | } 108 | 109 | public String getName() { 110 | return name; 111 | } 112 | 113 | public void setName(String name) { 114 | this.name = name; 115 | } 116 | 117 | public LocalDate getBirthDate() { 118 | return birthDate; 119 | } 120 | 121 | public void setBirthDate(LocalDate birthDate) { 122 | this.birthDate = birthDate; 123 | } 124 | 125 | public String getEmail() { 126 | return email; 127 | } 128 | 129 | public void setEmail(String email) { 130 | this.email = email; 131 | } 132 | 133 | public BigDecimal getSalary() { 134 | return salary; 135 | } 136 | 137 | public void setSalary(BigDecimal salary) { 138 | this.salary = salary; 139 | } 140 | 141 | public int getPhoneNumber() { 142 | return phoneNumber; 143 | } 144 | 145 | public void setPhoneNumber(int phoneNumber) { 146 | this.phoneNumber = phoneNumber; 147 | } 148 | 149 | public String getPassword() { 150 | return password; 151 | } 152 | 153 | public void setPassword(String password) { 154 | this.password = password; 155 | } 156 | 157 | public Boolean getActive() { 158 | return active; 159 | } 160 | 161 | public void setActive(Boolean active) { 162 | this.active = active; 163 | } 164 | 165 | public Group getMainGroup() { 166 | return mainGroup; 167 | } 168 | 169 | public void setMainGroup(Group mainGroup) { 170 | this.mainGroup = mainGroup; 171 | } 172 | 173 | public Set getGroups() { 174 | return groups; 175 | } 176 | 177 | public void setGroups(Set groups) { 178 | this.groups = groups; 179 | } 180 | 181 | public MaritalStatus getMaritalStatus() { 182 | return maritalStatus; 183 | } 184 | 185 | public void setMaritalStatus(MaritalStatus maritalStatus) { 186 | this.maritalStatus = maritalStatus; 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /add-on/src/main/java/org/vaadin/crudui/layout/impl/WindowBasedCrudLayout.java: -------------------------------------------------------------------------------- 1 | package org.vaadin.crudui.layout.impl; 2 | 3 | import java.util.HashMap; 4 | import java.util.Map; 5 | 6 | import com.vaadin.flow.component.Component; 7 | import com.vaadin.flow.component.Composite; 8 | import com.vaadin.flow.component.HasSize; 9 | import com.vaadin.flow.component.dialog.Dialog; 10 | import com.vaadin.flow.component.html.H3; 11 | import com.vaadin.flow.component.orderedlayout.FlexComponent.JustifyContentMode; 12 | import com.vaadin.flow.component.orderedlayout.HorizontalLayout; 13 | import com.vaadin.flow.component.orderedlayout.VerticalLayout; 14 | 15 | import org.vaadin.crudui.crud.CrudOperation; 16 | import org.vaadin.crudui.layout.CrudLayout; 17 | 18 | /** 19 | * @author Alejandro Duarte 20 | */ 21 | public class WindowBasedCrudLayout extends Composite implements CrudLayout, HasSize { 22 | 23 | protected VerticalLayout mainLayout = new VerticalLayout(); 24 | protected HorizontalLayout headerLayout = new HorizontalLayout(); 25 | protected HorizontalLayout toolbarLayout = new HorizontalLayout(); 26 | protected HorizontalLayout filterLayout = new HorizontalLayout(); 27 | protected VerticalLayout mainComponentLayout = new VerticalLayout(); 28 | protected Dialog dialog; 29 | protected String formWindowWidth; 30 | 31 | protected Map windowCaptions = new HashMap<>(); 32 | 33 | public WindowBasedCrudLayout() { 34 | getContent().setPadding(false); 35 | getContent().setMargin(false); 36 | getContent().add(mainLayout); 37 | 38 | mainLayout.setSizeFull(); 39 | mainLayout.setMargin(false); 40 | mainLayout.setPadding(false); 41 | mainLayout.setSpacing(true); 42 | setSizeFull(); 43 | 44 | headerLayout.setVisible(false); 45 | headerLayout.setWidthFull(); 46 | headerLayout.setSpacing(true); 47 | headerLayout.setMargin(false); 48 | 49 | toolbarLayout.setVisible(false); 50 | headerLayout.add(toolbarLayout); 51 | 52 | filterLayout.setJustifyContentMode(JustifyContentMode.START); 53 | filterLayout.setVisible(false); 54 | filterLayout.setWidthFull(); 55 | filterLayout.setSpacing(true); 56 | headerLayout.add(filterLayout); 57 | 58 | mainComponentLayout.setWidth("100%"); 59 | mainComponentLayout.setHeight(null); 60 | mainComponentLayout.setMargin(false); 61 | mainComponentLayout.setPadding(false); 62 | mainComponentLayout.setId("mainComponentLayout"); 63 | mainLayout.add(mainComponentLayout); 64 | mainLayout.expand(mainComponentLayout); 65 | 66 | setWindowCaption(CrudOperation.ADD, "Add"); 67 | setWindowCaption(CrudOperation.UPDATE, "Update"); 68 | setWindowCaption(CrudOperation.DELETE, "Are you sure you want to delete this item?"); 69 | } 70 | 71 | @Override 72 | public void setMainComponent(Component component) { 73 | mainComponentLayout.removeAll(); 74 | mainComponentLayout.add(component); 75 | } 76 | 77 | @Override 78 | public void addFilterComponent(Component component) { 79 | if (!headerLayout.isVisible()) { 80 | headerLayout.setVisible(true); 81 | mainLayout.getElement().insertChild(mainLayout.getComponentCount() - 1, headerLayout.getElement()); 82 | } 83 | 84 | filterLayout.setVisible(true); 85 | filterLayout.add(component); 86 | filterLayout.expand(component); 87 | } 88 | 89 | @Override 90 | public void addToolbarComponent(Component component) { 91 | if (!headerLayout.isVisible()) { 92 | headerLayout.setVisible(true); 93 | mainLayout.getElement().insertChild(mainLayout.getComponentCount() - 1, headerLayout.getElement()); 94 | } 95 | 96 | toolbarLayout.setVisible(true); 97 | toolbarLayout.add(component); 98 | } 99 | 100 | public void showDialog(String caption, Component form) { 101 | VerticalLayout dialogLayout = new VerticalLayout(form); 102 | dialogLayout.setWidth("100%"); 103 | dialogLayout.setMargin(false); 104 | dialogLayout.setPadding(false); 105 | 106 | dialog = new Dialog(new H3(caption), dialogLayout); 107 | dialog.setWidth(formWindowWidth); 108 | dialog.setDraggable(true); 109 | dialog.setResizable(true); 110 | dialog.open(); 111 | } 112 | 113 | @Override 114 | public void showForm(CrudOperation operation, Component form, String formCaption) { 115 | if (!operation.equals(CrudOperation.READ)) { 116 | String caption = (formCaption != null ? formCaption : windowCaptions.get(operation)); 117 | showDialog(caption, form); 118 | } 119 | } 120 | 121 | @Override 122 | public void hideForm() { 123 | if (dialog != null) { 124 | dialog.close(); 125 | } 126 | } 127 | 128 | public void setWindowCaption(CrudOperation operation, String caption) { 129 | windowCaptions.put(operation, caption); 130 | } 131 | 132 | public void setFormWindowWidth(String formWindowWidth) { 133 | this.formWindowWidth = formWindowWidth; 134 | } 135 | 136 | } 137 | -------------------------------------------------------------------------------- /add-on/src/main/java/org/vaadin/crudui/form/AbstractCrudFormFactory.java: -------------------------------------------------------------------------------- 1 | package org.vaadin.crudui.form; 2 | 3 | import com.vaadin.flow.component.HasValueAndElement; 4 | import com.vaadin.flow.component.notification.Notification; 5 | import com.vaadin.flow.data.converter.Converter; 6 | import com.vaadin.flow.function.SerializableConsumer; 7 | import org.vaadin.crudui.crud.CrudOperation; 8 | 9 | import java.util.Arrays; 10 | import java.util.HashMap; 11 | import java.util.Map; 12 | 13 | /** 14 | * @author Alejandro Duarte 15 | */ 16 | public abstract class AbstractCrudFormFactory implements CrudFormFactory { 17 | 18 | protected Map configurations = new HashMap<>(); 19 | 20 | protected SerializableConsumer errorListener; 21 | 22 | protected boolean showNotifications; 23 | 24 | @Override 25 | public void setCaption(CrudOperation operation, String caption) { 26 | getConfiguration(operation).setCaption(caption); 27 | } 28 | 29 | @Override 30 | public void setVisibleProperties(CrudOperation operation, String... properties) { 31 | getConfiguration(operation).setVisibleProperties(Arrays.asList(properties)); 32 | } 33 | 34 | @Override 35 | public void setVisibleProperties(String... properties) { 36 | Arrays.stream(CrudOperation.values()).forEach(operation -> setVisibleProperties(operation, properties)); 37 | } 38 | 39 | @Override 40 | public void setDisabledProperties(CrudOperation operation, String... properties) { 41 | getConfiguration(operation).setDisabledProperties(Arrays.asList(properties)); 42 | } 43 | 44 | @Override 45 | public void setDisabledProperties(String... properties) { 46 | Arrays.stream(CrudOperation.values()).forEach(operation -> setDisabledProperties(operation, properties)); 47 | } 48 | 49 | @Override 50 | public void setFieldCaptions(CrudOperation operation, String... captions) { 51 | getConfiguration(operation).setFieldCaptions(Arrays.asList(captions)); 52 | } 53 | 54 | @Override 55 | public void setFieldCaptions(String... captions) { 56 | Arrays.stream(CrudOperation.values()).forEach(operation -> setFieldCaptions(operation, captions)); 57 | } 58 | 59 | @Override 60 | public void setFieldType(CrudOperation operation, String property, Class> type) { 61 | getConfiguration(operation).getFieldTypes().put(property, type); 62 | } 63 | 64 | @Override 65 | public void setFieldType(String property, Class> type) { 66 | Arrays.stream(CrudOperation.values()).forEach(operation -> setFieldType(operation, property, type)); 67 | } 68 | 69 | @Override 70 | public void setFieldCreationListener(CrudOperation operation, String property, FieldCreationListener listener) { 71 | getConfiguration(operation).getFieldCreationListeners().put(property, listener); 72 | } 73 | 74 | @Override 75 | public void setFieldCreationListener(String property, FieldCreationListener listener) { 76 | Arrays.stream(CrudOperation.values()) 77 | .forEach(operation -> setFieldCreationListener(operation, property, listener)); 78 | } 79 | 80 | @Override 81 | public void setFieldProvider(CrudOperation operation, String property, FieldProvider provider) { 82 | getConfiguration(operation).getFieldProviders().put(property, provider); 83 | } 84 | 85 | @Override 86 | public void setFieldProvider(String property, FieldProvider provider) { 87 | Arrays.stream(CrudOperation.values()).forEach(operation -> setFieldProvider(operation, property, provider)); 88 | } 89 | 90 | @Override 91 | public void setConverter(CrudOperation operation, String property, Converter converter) { 92 | getConfiguration(operation).getConverters().put(property, converter); 93 | } 94 | 95 | @Override 96 | public void setConverter(String property, Converter converter) { 97 | Arrays.stream(CrudOperation.values()).forEach(operation -> setConverter(operation, property, converter)); 98 | } 99 | 100 | @Override 101 | public void setUseBeanValidation(CrudOperation operation, boolean useBeanValidation) { 102 | getConfiguration(operation).setUseBeanValidation(useBeanValidation); 103 | } 104 | 105 | @Override 106 | public void setUseBeanValidation(boolean useBeanValidation) { 107 | Arrays.stream(CrudOperation.values()).forEach(operation -> setUseBeanValidation(operation, useBeanValidation)); 108 | } 109 | 110 | @Override 111 | public void setShowNotifications(boolean showNotifications) { 112 | this.showNotifications = showNotifications; 113 | } 114 | 115 | public void showNotification(String text) { 116 | if (showNotifications) { 117 | Notification.show(text); 118 | } 119 | } 120 | 121 | @Override 122 | public void setErrorListener(SerializableConsumer errorListener) { 123 | this.errorListener = errorListener; 124 | } 125 | 126 | protected CrudFormConfiguration getConfiguration(CrudOperation operation) { 127 | configurations.putIfAbsent(operation, new CrudFormConfiguration()); 128 | return configurations.get(operation); 129 | } 130 | 131 | } 132 | -------------------------------------------------------------------------------- /crud-ui-demo/src/main/java/org/vaadin/crudui/demo/ui/view/CustomizedView.java: -------------------------------------------------------------------------------- 1 | package org.vaadin.crudui.demo.ui.view; 2 | 3 | import com.vaadin.flow.component.HasValue; 4 | import com.vaadin.flow.component.button.Button; 5 | import com.vaadin.flow.component.button.ButtonVariant; 6 | import com.vaadin.flow.component.grid.Grid; 7 | import com.vaadin.flow.component.icon.VaadinIcon; 8 | import com.vaadin.flow.component.orderedlayout.VerticalLayout; 9 | import com.vaadin.flow.data.provider.DataProvider; 10 | import com.vaadin.flow.data.renderer.TextRenderer; 11 | import com.vaadin.flow.router.Route; 12 | 13 | import org.vaadin.crudui.crud.CrudOperation; 14 | import org.vaadin.crudui.crud.CrudOperationException; 15 | import org.vaadin.crudui.crud.LazyCrudListener; 16 | import org.vaadin.crudui.crud.impl.GridCrud; 17 | import org.vaadin.crudui.demo.entity.Group; 18 | import org.vaadin.crudui.demo.entity.User; 19 | import org.vaadin.crudui.demo.service.GroupService; 20 | import org.vaadin.crudui.demo.service.UserService; 21 | import org.vaadin.crudui.demo.ui.MainLayout; 22 | import org.vaadin.crudui.form.CrudFormFactory; 23 | import org.vaadin.crudui.form.impl.field.provider.ComboBoxProvider; 24 | import org.vaadin.crudui.form.impl.field.provider.MultiSelectComboBoxProvider; 25 | import org.vaadin.crudui.form.impl.form.factory.DefaultCrudFormFactory; 26 | import org.vaadin.crudui.layout.impl.WindowBasedCrudLayout; 27 | 28 | @Route(value = "customized", layout = MainLayout.class) 29 | public class CustomizedView extends VerticalLayout implements LazyCrudListener { 30 | 31 | private UserService userService; 32 | private HasValue nameFilter; 33 | 34 | public CustomizedView(UserService userService, GroupService groupService) { 35 | this.userService = userService; 36 | 37 | // CRUD layout configuration 38 | WindowBasedCrudLayout crudLayout = new WindowBasedCrudLayout(); 39 | crudLayout.setFormWindowWidth("70%"); 40 | 41 | // CRUD forms configuration 42 | CrudFormFactory formFactory = new DefaultCrudFormFactory<>(User.class); 43 | formFactory.setUseBeanValidation(true); 44 | formFactory.setCaption(CrudOperation.ADD, "Create new User"); 45 | 46 | formFactory.setVisibleProperties( 47 | "name", "birthDate", "email", "salary", "phoneNumber", "maritalStatus", "groups", "active", 48 | "mainGroup"); 49 | formFactory.setFieldCaptions( 50 | "The name", "The birthdate", "The e-mail", "The Salary", "The phone number", "The marital status", 51 | "The groups", "Is it active?", "The main group", "The password"); 52 | formFactory.setVisibleProperties(CrudOperation.ADD, 53 | "name", "birthDate", "email", "salary", "phoneNumber", "maritalStatus", "groups", "active", "mainGroup", 54 | "password"); 55 | 56 | formFactory.setVisibleProperties(CrudOperation.DELETE, "name", "mainGroup"); 57 | formFactory.setFieldCaptions(CrudOperation.DELETE, "The name", "The main group"); 58 | 59 | formFactory.setFieldProvider("groups", 60 | new MultiSelectComboBoxProvider<>("Groups", groupService.findAll(), Group::getName)); 61 | formFactory.setFieldProvider("mainGroup", new ComboBoxProvider<>("Main Group", groupService.findAll(), 62 | new TextRenderer<>(Group::getName), Group::getName)); 63 | 64 | // CRUD component configuration 65 | GridCrud crud = new GridCrud<>(User.class, crudLayout, formFactory, this); 66 | crud.setClickRowToUpdate(true); 67 | crud.setUpdateOperationVisible(false); 68 | crud.setDeleteOperationVisible(false); 69 | 70 | // grid configuration 71 | Grid grid = crud.getGrid(); 72 | grid.setColumns("name", "birthDate", "maritalStatus", "email", "phoneNumber"); 73 | grid.getColumnByKey("name").setAutoWidth(true); 74 | grid.getColumnByKey("email").setAutoWidth(true); 75 | grid.setColumnReorderingAllowed(true); 76 | grid.addComponentColumn(user -> { 77 | var button = new Button(VaadinIcon.TRASH.create(), event -> crud.showDeleteConfirmation(user)); 78 | button.addThemeVariants(ButtonVariant.LUMO_ERROR, ButtonVariant.LUMO_SMALL); 79 | return button; 80 | }).setFlexGrow(0); 81 | 82 | // filters 83 | nameFilter = crud.addFilterProperty("Filter by name"); 84 | 85 | // view configuration 86 | setSizeFull(); 87 | add(crud); 88 | } 89 | 90 | @Override 91 | public DataProvider getDataProvider() { 92 | return DataProvider.fromCallbacks( 93 | query -> userService.findByNameContainingIgnoreCase( 94 | nameFilter.getValue(), query.getPage(), query.getPageSize()).stream(), 95 | query -> (int) userService.countByNameContainingIgnoreCase(nameFilter.getValue())); 96 | } 97 | 98 | @Override 99 | public User add(User user) { 100 | return userService.save(user); 101 | } 102 | 103 | @Override 104 | public User update(User user) { 105 | if (user.getId().equals(1L)) { 106 | throw new CrudOperationException("Simulated exception (only for user ID 1)."); 107 | } 108 | return userService.save(user); 109 | } 110 | 111 | @Override 112 | public void delete(User user) { 113 | userService.delete(user); 114 | } 115 | 116 | } 117 | -------------------------------------------------------------------------------- /crud-ui-demo/src/main/java/org/vaadin/crudui/demo/ui/MainLayout.java: -------------------------------------------------------------------------------- 1 | package org.vaadin.crudui.demo.ui; 2 | 3 | import java.util.HashMap; 4 | import java.util.Map; 5 | 6 | import com.vaadin.flow.component.Component; 7 | import com.vaadin.flow.component.HasComponents; 8 | import com.vaadin.flow.component.HasElement; 9 | import com.vaadin.flow.component.Html; 10 | import com.vaadin.flow.component.UI; 11 | import com.vaadin.flow.component.applayout.AppLayout; 12 | import com.vaadin.flow.component.button.Button; 13 | import com.vaadin.flow.component.button.ButtonVariant; 14 | import com.vaadin.flow.component.dependency.JsModule; 15 | import com.vaadin.flow.component.html.Anchor; 16 | import com.vaadin.flow.component.html.Image; 17 | import com.vaadin.flow.component.icon.VaadinIcon; 18 | import com.vaadin.flow.component.orderedlayout.HorizontalLayout; 19 | import com.vaadin.flow.component.orderedlayout.VerticalLayout; 20 | import com.vaadin.flow.component.tabs.Tab; 21 | import com.vaadin.flow.component.tabs.Tabs; 22 | import com.vaadin.flow.router.AfterNavigationEvent; 23 | import com.vaadin.flow.router.AfterNavigationObserver; 24 | import com.vaadin.flow.router.BeforeEnterEvent; 25 | import com.vaadin.flow.router.BeforeEnterObserver; 26 | 27 | import org.vaadin.crudui.demo.DemoUtils; 28 | import org.vaadin.crudui.demo.ui.view.CustomizedView; 29 | import org.vaadin.crudui.demo.ui.view.DefaultView; 30 | import org.vaadin.crudui.demo.ui.view.HomeView; 31 | import org.vaadin.crudui.demo.ui.view.TreeView; 32 | 33 | @JsModule("theme-handler.js") 34 | public class MainLayout extends AppLayout implements BeforeEnterObserver, AfterNavigationObserver { 35 | 36 | private VerticalLayout viewContainer = new VerticalLayout(); 37 | private HorizontalLayout footer = new HorizontalLayout(); 38 | private Tabs tabs = new Tabs(); 39 | private Image logo = new Image(); 40 | private Button themeSwitcher = new Button(VaadinIcon.MOON_O.create()); 41 | private Map> tabToView = new HashMap<>(); 42 | private Map, Tab> viewToTab = new HashMap<>(); 43 | 44 | public MainLayout() { 45 | logo.addClassName("logo"); 46 | logo.setHeight("44px"); 47 | 48 | tabs.addSelectedChangeListener(this::tabsSelectionChanged); 49 | addTab(HomeView.class); 50 | addTab(CustomizedView.class); 51 | addTab(DefaultView.class); 52 | addTab(TreeView.class); 53 | 54 | themeSwitcher.setId("theme-switch"); 55 | themeSwitcher.addThemeVariants(ButtonVariant.LUMO_TERTIARY_INLINE); 56 | themeSwitcher.addClickListener(e -> UI.getCurrent().getPage().executeJs("window.switchTheme()")); 57 | 58 | var headerLayout = new HorizontalLayout(logo, tabs, themeSwitcher); 59 | headerLayout.setMargin(true); 60 | headerLayout.setWidthFull(); 61 | headerLayout.expand(tabs); 62 | addToNavbar(headerLayout); 63 | 64 | viewContainer.setSizeFull(); 65 | viewContainer.setPadding(false); 66 | 67 | footer.setSpacing(false); 68 | footer.setMargin(false); 69 | footer.setPadding(true); 70 | 71 | var content = new VerticalLayout(); 72 | content.setSizeFull(); 73 | content.setPadding(false); 74 | content.setMargin(false); 75 | content.setSpacing(false); 76 | content.add(viewContainer, footer); 77 | 78 | setContent(content); 79 | UI.getCurrent().getPage().executeJs("window.applySystemTheme()"); 80 | } 81 | 82 | @Override 83 | public void showRouterLayoutContent(HasElement content) { 84 | viewContainer.removeAll(); 85 | viewContainer.add(content.getElement().getComponent().get()); 86 | afterNavigation(); 87 | } 88 | 89 | private void tabsSelectionChanged(Tabs.SelectedChangeEvent event) { 90 | if (event.isFromClient()) { 91 | UI.getCurrent().navigate((Class) tabToView.get(event.getSelectedTab())); 92 | } 93 | } 94 | 95 | private void addTab(Class clazz) { 96 | Tab tab = new Tab(DemoUtils.getViewName(clazz)); 97 | tabs.add(tab); 98 | tabToView.put(tab, clazz); 99 | viewToTab.put(clazz, tab); 100 | } 101 | 102 | @Override 103 | public void beforeEnter(BeforeEnterEvent event) { 104 | selectTabByCurrentView(event); 105 | } 106 | 107 | public void selectTabByCurrentView(BeforeEnterEvent event) { 108 | Class viewClass = event.getNavigationTarget(); 109 | tabs.setSelectedTab(viewToTab.get(viewClass)); 110 | } 111 | 112 | @Override 113 | public void afterNavigation(AfterNavigationEvent event) { 114 | updatePageTitle(); 115 | addSourceCodeAnchorToCurrentView(); 116 | } 117 | 118 | public void updatePageTitle() { 119 | Class viewClass = tabToView.get(tabs.getSelectedTab()); 120 | UI.getCurrent().getPage().setTitle(DemoUtils.getViewName(viewClass) + " - " + "Crud UI add-on demo"); 121 | } 122 | 123 | public void addSourceCodeAnchorToCurrentView() { 124 | footer.removeAll(); 125 | Class viewClass = tabToView.get(tabs.getSelectedTab()); 126 | if (!HomeView.class.equals(viewClass)) { 127 | footer.add( 128 | new Html("Source code 👉 "), 129 | new Anchor(DemoUtils.getGitHubLink(viewClass), viewClass.getSimpleName() + ".java")); 130 | } 131 | } 132 | 133 | } 134 | -------------------------------------------------------------------------------- /add-on/src/main/java/org/vaadin/crudui/layout/impl/AbstractTwoComponentsCrudLayout.java: -------------------------------------------------------------------------------- 1 | package org.vaadin.crudui.layout.impl; 2 | 3 | import java.util.HashMap; 4 | import java.util.Map; 5 | 6 | import com.vaadin.flow.component.Component; 7 | import com.vaadin.flow.component.Composite; 8 | import com.vaadin.flow.component.HasSize; 9 | import com.vaadin.flow.component.html.Div; 10 | import com.vaadin.flow.component.html.H3; 11 | import com.vaadin.flow.component.orderedlayout.FlexComponent.Alignment; 12 | import com.vaadin.flow.component.orderedlayout.FlexComponent.JustifyContentMode; 13 | import com.vaadin.flow.component.orderedlayout.HorizontalLayout; 14 | import com.vaadin.flow.component.orderedlayout.VerticalLayout; 15 | 16 | import org.vaadin.crudui.crud.CrudOperation; 17 | import org.vaadin.crudui.layout.CrudLayout; 18 | 19 | /** 20 | * @author Alejandro Duarte. 21 | */ 22 | public abstract class AbstractTwoComponentsCrudLayout extends Composite
implements CrudLayout, HasSize { 23 | 24 | protected VerticalLayout firstComponent = new VerticalLayout(); 25 | protected VerticalLayout secondComponent = new VerticalLayout(); 26 | protected HorizontalLayout firstComponentHeaderLayout = new HorizontalLayout(); 27 | protected HorizontalLayout secondComponentHeaderLayout = new HorizontalLayout(); 28 | protected VerticalLayout formComponentLayout = new VerticalLayout(); 29 | protected HorizontalLayout formCaptionLayout = new HorizontalLayout(); 30 | protected HorizontalLayout toolbar = new HorizontalLayout(); 31 | protected HorizontalLayout filterLayout = new HorizontalLayout(); 32 | protected VerticalLayout mainComponentLayout = new VerticalLayout(); 33 | 34 | protected Map formCaptions = new HashMap<>(); 35 | 36 | public AbstractTwoComponentsCrudLayout() { 37 | firstComponent.setMargin(false); 38 | firstComponent.setPadding(false); 39 | firstComponent.setSpacing(false); 40 | firstComponent.add(firstComponentHeaderLayout); 41 | 42 | firstComponentHeaderLayout.setVisible(false); 43 | firstComponentHeaderLayout.setWidthFull(); 44 | firstComponentHeaderLayout.setSpacing(true); 45 | firstComponentHeaderLayout.setMargin(false); 46 | 47 | formCaptionLayout.setWidthFull(); 48 | formCaptionLayout.setDefaultVerticalComponentAlignment(Alignment.CENTER); 49 | 50 | secondComponent.setSizeFull(); 51 | secondComponent.setMargin(false); 52 | secondComponent.setPadding(false); 53 | secondComponent.add(secondComponentHeaderLayout); 54 | 55 | secondComponentHeaderLayout.setVisible(false); 56 | secondComponentHeaderLayout.setWidthFull(); 57 | secondComponentHeaderLayout.setSpacing(true); 58 | secondComponentHeaderLayout.setPadding(true); 59 | secondComponentHeaderLayout.getStyle().set("background-color", "var(--lumo-contrast-5pct)"); 60 | secondComponentHeaderLayout.add(formCaptionLayout, toolbar); 61 | 62 | formComponentLayout.setSizeFull(); 63 | formComponentLayout.setMargin(false); 64 | formComponentLayout.setPadding(false); 65 | secondComponent.add(formComponentLayout); 66 | secondComponent.expand(formComponentLayout); 67 | 68 | toolbar.setJustifyContentMode(JustifyContentMode.END); 69 | 70 | filterLayout.setVisible(false); 71 | filterLayout.setWidthFull(); 72 | filterLayout.setSpacing(true); 73 | firstComponentHeaderLayout.add(filterLayout); 74 | 75 | mainComponentLayout.setSizeFull(); 76 | mainComponentLayout.setMargin(false); 77 | mainComponentLayout.setPadding(false); 78 | firstComponent.add(mainComponentLayout); 79 | firstComponent.expand(mainComponentLayout); 80 | 81 | setFormCaption(CrudOperation.ADD, "Add"); 82 | setFormCaption(CrudOperation.UPDATE, "Update"); 83 | setFormCaption(CrudOperation.DELETE, "Are you sure you want to delete this item?"); 84 | 85 | Component mainLayout = buildMainLayout(); 86 | getContent().add(mainLayout); 87 | setSizeFull(); 88 | } 89 | 90 | protected abstract Component buildMainLayout(); 91 | 92 | @Override 93 | public void setMainComponent(Component component) { 94 | mainComponentLayout.removeAll(); 95 | mainComponentLayout.add(component); 96 | } 97 | 98 | @Override 99 | public void addToolbarComponent(Component component) { 100 | secondComponentHeaderLayout.setVisible(true); 101 | toolbar.setVisible(true); 102 | toolbar.add(component); 103 | } 104 | 105 | @Override 106 | public void addFilterComponent(Component component) { 107 | if (!firstComponentHeaderLayout.isVisible()) { 108 | firstComponentHeaderLayout.setVisible(true); 109 | firstComponent.getElement().insertChild(firstComponent.getComponentCount() - 1, 110 | firstComponentHeaderLayout.getElement()); 111 | } 112 | 113 | filterLayout.setVisible(true); 114 | filterLayout.add(component); 115 | filterLayout.expand(component); 116 | } 117 | 118 | @Override 119 | public void showForm(CrudOperation operation, Component form, String formCaption) { 120 | String caption = (formCaption != null ? formCaption : formCaptions.get(operation)); 121 | formCaptionLayout.removeAll(); 122 | 123 | if (caption != null) { 124 | H3 h3 = new H3(caption); 125 | h3.setWidthFull(); 126 | formCaptionLayout.add(h3); 127 | } 128 | 129 | formComponentLayout.removeAll(); 130 | formComponentLayout.add(form); 131 | } 132 | 133 | @Override 134 | public void hideForm() { 135 | formComponentLayout.removeAll(); 136 | formCaptionLayout.removeAll(); 137 | } 138 | 139 | public void setFormCaption(CrudOperation operation, String caption) { 140 | formCaptions.put(operation, caption); 141 | } 142 | 143 | } 144 | -------------------------------------------------------------------------------- /add-on/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | org.vaadin.crudui 8 | crudui 9 | jar 10 | 7.2.0 11 | Crud UI Add-on 12 | 13 | 14 | 21 15 | 21 16 | UTF-8 17 | UTF-8 18 | 19 | 24.4.4 20 | 21 | ${project.basedir}/drivers 22 | true 23 | 24 | 25 | 26 | 27 | Apache 2 28 | http://www.apache.org/licenses/LICENSE-2.0.txt 29 | repo 30 | 31 | 32 | 33 | 34 | Alejandro Duarte 35 | https://github.com/alejandro-du 36 | 37 | 38 | 39 | https://github.com/alejandro-du/crudui.git 40 | scm:git:https://github.com/alejandro-du/crudui.git 41 | scm:git:https://github.com/alejandro-du/crudui.git 42 | HEAD 43 | 44 | 45 | 46 | GitHub 47 | https://github.com/alejandro-du/crudui/issues 48 | 49 | 50 | 51 | 52 | Vaadin Directory 53 | http://maven.vaadin.com/vaadin-addons 54 | 55 | 56 | 57 | 58 | 59 | 60 | com.vaadin 61 | vaadin-bom 62 | pom 63 | import 64 | ${vaadin.version} 65 | 66 | 67 | 68 | 69 | 70 | 71 | jakarta.servlet 72 | jakarta.servlet-api 73 | 6.0.0 74 | provided 75 | 76 | 77 | com.vaadin 78 | vaadin-core 79 | 80 | 81 | org.springframework.data 82 | spring-data-commons 83 | 3.3.1 84 | true 85 | provided 86 | 87 | 88 | 89 | junit 90 | junit 91 | 4.13.2 92 | test 93 | 94 | 95 | 96 | 97 | jetty:run 98 | 99 | 100 | org.apache.maven.plugins 101 | maven-jar-plugin 102 | 3.1.0 103 | 104 | 105 | true 106 | 107 | false 108 | true 109 | 110 | 111 | 1 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | directory 122 | 123 | 124 | 125 | org.apache.maven.plugins 126 | maven-assembly-plugin 127 | 3.1.0 128 | 129 | false 130 | 131 | assembly/assembly.xml 132 | 133 | 134 | 135 | 136 | 137 | single 138 | 139 | install 140 | 141 | 142 | 143 | 144 | org.apache.maven.plugins 145 | maven-source-plugin 146 | 3.0.1 147 | 148 | 149 | attach-sources 150 | verify 151 | 152 | jar-no-fork 153 | 154 | 155 | 156 | 157 | 158 | org.apache.maven.plugins 159 | maven-javadoc-plugin 160 | 3.0.1 161 | 162 | 163 | attach-javadocs 164 | verify 165 | 166 | jar 167 | 168 | 169 | 170 | 171 | true 172 | 173 | 174 | 175 | org.apache.maven.plugins 176 | maven-jar-plugin 177 | 3.1.2 178 | 179 | 180 | 181 | META-INF/VAADIN/config/flow-build-info.json 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Published on Vaadin Directory](https://img.shields.io/badge/Vaadin%20Directory-published-00b4f0.svg)](https://vaadin.com/directory/component/crud-ui-add-on) 2 | [![Stars on Vaadin Directory](https://img.shields.io/vaadin-directory/star/crud-ui-add-on.svg)](https://vaadin.com/directory/component/crud-ui-add-on) 3 | [![Latest version on vaadin.com/directory](https://img.shields.io/vaadin-directory/v/crud-ui-add-on.svg)](https://img.shields.io/vaadin-directory/v/crud-ui-add-on.svg) 4 | 5 | _For questions on usage, please [use the forum](https://vaadin.com/forum/t/crud-ui-add-on/156634/1)._ 6 | 7 | Crud UI Add-on provides an API to automatically generate CRUD-like UIs with grids and forms for any Java Bean at runtime. 8 | 9 | The API is defined through 4 interfaces and their implementations: 10 | 11 | * **`Crud`**: A Vaadin UI component that can be added to any `ComponentContainer`. This is the actual CRUD final users will see in the browser. Implementations: 12 | 13 | * `GridCrud`: A CRUD UI based on Vaadin's standard `Grid` component. 14 | * `TreeGridCrud`: A CRUD UI based on Vaadin's standard `TreeGrid` component. 15 | 16 | * **`CrudLayout`**: Defines CRUD layouts and related behaviors. Implementations: 17 | 18 | * `WindowBasedCrudLayout`: Shows forms in pop-up windows. 19 | * `HorizontalSplitCrudLayout`: Grid on the left, form on the right in a split layout. 20 | * `VerticalSplitCrudLayout`: Grid on the top, form on the bottom in a draggable split layout. 21 | 22 | * **`CrudFormFactory`**: Builds required UI forms for new, update, delete operations. Implementations: 23 | 24 | * `DefaultCrudFormFactory`: Java Reflection-based autogenerated UI forms customizable through `FieldProvider` implementations. 25 | 26 | * **`CrudListener`**: Connects the CRUD to your backend operations. You must implement this interface or call the equivalent methods defined in the `Crud` interface. 27 | 28 | # Basic usage 29 | 30 | Let's say, you have the following domain/entity/Java Bean class: 31 | 32 | ```java 33 | public class User { 34 | 35 | private Long id; 36 | private String name; 37 | private Date birthDate; 38 | private String email; 39 | private String password; 40 | 41 | ... getters & setters ... 42 | } 43 | ``` 44 | 45 | You can create a new CRUD component and add it into any Vaadin layout as follows: 46 | ```java 47 | GridCrud crud = new GridCrud<>(User.class); 48 | someLayout.addComponent(crud); 49 | ``` 50 | 51 | Then use lambda expressions or method references to delegate CRUD operations to your backend implemented for example with JPA/Hibernate, Spring Data, MyBatis, and others: 52 | 53 | ```java 54 | crud.setFindAllOperation(() -> backend.findAll()); 55 | crud.setAddOperation(backend::add); 56 | crud.setUpdateOperation(backend::update); 57 | crud.setDeleteOperation(backend::delete); 58 | ``` 59 | 60 | # Advanced usage 61 | 62 | You can enable _Java Bean Validation_ as follows (don't forget to add the corresponding Java Validation API dependency to your project): 63 | 64 | ```java 65 | crud.getCrudFormFactory().setUseBeanValidation(true); 66 | ``` 67 | 68 | As an alternative to method references and lambda expressions, you can implement the `CrudListener` interface to connect the CRUD UI to your backend: 69 | 70 | ```java 71 | crud.setCrudListener(new CrudListener() { 72 | 73 | @Override 74 | public Collection findAll() { 75 | return backend.findAllUsers(); 76 | } 77 | 78 | @Override 79 | public User add(User user) { 80 | return backend.add(user); 81 | } 82 | 83 | @Override 84 | public User update(User user) { 85 | return backend.update(user); 86 | } 87 | 88 | @Override 89 | public void delete(User user) { 90 | backend.remove(user); 91 | } 92 | }); 93 | ``` 94 | 95 | To enable lazy loading implement `LazyCrudListener` and use the Vaadin's `DataProvider` interface: 96 | 97 | ```java 98 | crud.setCrudListener(new LazyCrudListener<>() { 99 | @Override 100 | public DataProvider getDataProvider() { 101 | return DataProvider.fromCallbacks( 102 | query -> userService.findAll(query.getPage(), query.getPageSize()).stream(), 103 | query -> (int) userService.countAll() 104 | ); 105 | } 106 | 107 | ... other CRUD methods ... 108 | 109 | }); 110 | ``` 111 | 112 | # Customization 113 | 114 | To change the general layout, use an alternative `CrudLayout` implementation (defaults to `WindowBasedCrudLayout`): 115 | 116 | ```java 117 | GridCrud crud = new GridCrud<>(User.class, new HorizontalSplitCrudLayout()); 118 | ```` 119 | 120 | To configure form behavior or override related functionality, define a `CrudFormFactory`: 121 | 122 | ```java 123 | CustomCrudFormFactory formFactory = new CustomCrudFormFactory<>(User.class); 124 | crud.setCrudFormFactory(formFactory); 125 | ``` 126 | 127 | To configure form fields visibility: 128 | 129 | ```java 130 | formFactory.setVisibleProperties(CrudOperation.READ, "name", "birthDate", "email", "groups", "mainGroup", "active"); 131 | formFactory.setVisibleProperties(CrudOperation.ADD, "name", "birthDate", "email", "password", "groups", "mainGroup", "active"); 132 | formFactory.setVisibleProperties(CrudOperation.UPDATE, "name", "birthDate", "email", "groups", "mainGroup", "active"); 133 | formFactory.setVisibleProperties(CrudOperation.DELETE, "name", "email"); 134 | ```` 135 | 136 | To configure field captions in the same order as you defined the set of visible properties: 137 | 138 | ```java 139 | formFactory.setFieldCaptions("The name", "The birthdate", "The e-mail"); 140 | ``` 141 | 142 | To add columns as nested properties in the `Crud` component of `GridCrud` instances: 143 | 144 | ```java 145 | crud.getGrid().addColumn(user -> user.getMainGroup().getName()).setHeader("Main group").setKey("key"); 146 | ``` 147 | 148 | To configure the type of UI component to use for a specific field: 149 | 150 | ```java 151 | formFactory.setFieldType("password", PasswordField.class); 152 | ``` 153 | 154 | To further customize fields after their creation: 155 | 156 | ```java 157 | formFactory.setFieldCreationListener("birthDate", field -> ... your own logic here ...); 158 | ``` 159 | 160 | To manually create input fields, define a `FieldProvider`: 161 | 162 | ```java 163 | formFactory.setFieldProvider("groups", user -> { 164 | CheckboxGroup checkboxes = new CheckboxGroup<>(); 165 | checkboxes.setItems(groups); 166 | checkboxes.setItemLabelGenerator(Group::getName); 167 | return checkboxes; 168 | }); 169 | ``` 170 | 171 | Or use an included or custom `FieldProvider` implementation: 172 | 173 | ```java 174 | formFactory.setFieldProvider("groups", 175 | new CheckBoxGroupProvider<>("Groups", GroupRepository.findAll(), Group::getName)); 176 | ``` 177 | 178 | To set a `Converter`: 179 | 180 | ```java 181 | formFactory.setConverter("salary", new Converter() { 182 | @Override 183 | public Result convertToModel(String value, ValueContext valueContext) { 184 | return Result.ok(new BigDecimal(value)); // error handling omitted 185 | } 186 | 187 | @Override 188 | public String convertToPresentation(BigDecimal value, ValueContext valueContext) { 189 | return value.toPlainString(); 190 | } 191 | }); 192 | ``` 193 | 194 | To customize button captions: 195 | 196 | ```java 197 | formFactory.setButtonCaption(CrudOperation.ADD, "Add new user"); 198 | crud.setRowCountCaption("%d user(s) found"); 199 | ``` 200 | 201 | To customize form titles: 202 | 203 | ```java 204 | crud.getCrudFormFactory().setCaption(CrudOperation.ADD, "Create new User"); 205 | crud.getCrudFormFactory().setCaption(CrudOperation.UPDATE, "Modify User"); 206 | ``` 207 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | -------------------------------------------------------------------------------- /add-on/src/main/java/org/vaadin/crudui/crud/impl/AbstractGridCrud.java: -------------------------------------------------------------------------------- 1 | package org.vaadin.crudui.crud.impl; 2 | 3 | import com.vaadin.flow.component.AttachEvent; 4 | import com.vaadin.flow.component.ClickEvent; 5 | import com.vaadin.flow.component.Component; 6 | import com.vaadin.flow.component.ComponentEventListener; 7 | import com.vaadin.flow.component.HasValue; 8 | import com.vaadin.flow.component.button.Button; 9 | import com.vaadin.flow.component.button.ButtonVariant; 10 | import com.vaadin.flow.component.grid.Grid; 11 | import com.vaadin.flow.component.icon.VaadinIcon; 12 | import com.vaadin.flow.component.notification.Notification; 13 | import com.vaadin.flow.component.textfield.TextField; 14 | import com.vaadin.flow.component.treegrid.TreeGrid; 15 | import com.vaadin.flow.data.provider.Query; 16 | import com.vaadin.flow.data.provider.hierarchy.HierarchicalDataProvider; 17 | import com.vaadin.flow.data.provider.hierarchy.HierarchicalQuery; 18 | 19 | import org.vaadin.crudui.crud.AbstractCrud; 20 | import org.vaadin.crudui.crud.CrudListener; 21 | import org.vaadin.crudui.crud.CrudOperation; 22 | import org.vaadin.crudui.crud.CrudOperationException; 23 | import org.vaadin.crudui.form.CrudFormFactory; 24 | import org.vaadin.crudui.form.impl.form.factory.DefaultCrudFormFactory; 25 | import org.vaadin.crudui.layout.CrudLayout; 26 | import org.vaadin.crudui.layout.impl.HorizontalSplitCrudLayout; 27 | 28 | /** 29 | * @author Alejandro Duarte 30 | */ 31 | public abstract class AbstractGridCrud extends AbstractCrud { 32 | 33 | protected String rowCountCaption = "%d items(s) found"; 34 | protected String savedMessage = "Item saved"; 35 | protected String deletedMessage = "Item deleted"; 36 | protected boolean showNotifications = true; 37 | 38 | protected Button findAllButton; 39 | protected Button addButton; 40 | protected Button updateButton; 41 | protected Button deleteButton; 42 | protected Grid grid; 43 | 44 | private boolean clickRowToUpdate; 45 | 46 | AbstractGridCrud(Class domainType) { 47 | this(domainType, new HorizontalSplitCrudLayout(), new DefaultCrudFormFactory<>(domainType), null); 48 | } 49 | 50 | AbstractGridCrud(Class domainType, CrudLayout crudLayout) { 51 | this(domainType, crudLayout, new DefaultCrudFormFactory<>(domainType), null); 52 | } 53 | 54 | AbstractGridCrud(Class domainType, CrudFormFactory crudFormFactory) { 55 | this(domainType, new HorizontalSplitCrudLayout(), crudFormFactory, null); 56 | } 57 | 58 | AbstractGridCrud(Class domainType, CrudListener crudListener) { 59 | this(domainType, new HorizontalSplitCrudLayout(), new DefaultCrudFormFactory<>(domainType), crudListener); 60 | } 61 | 62 | AbstractGridCrud(Class domainType, CrudLayout crudLayout, CrudListener crudListener) { 63 | this(domainType, crudLayout, new DefaultCrudFormFactory<>(domainType), crudListener); 64 | } 65 | 66 | AbstractGridCrud(Class domainType, CrudLayout crudLayout, CrudFormFactory crudFormFactory) { 67 | this(domainType, crudLayout, crudFormFactory, null); 68 | } 69 | 70 | AbstractGridCrud(Class domainType, CrudLayout crudLayout, CrudFormFactory crudFormFactory, 71 | CrudListener crudListener) { 72 | super(domainType, crudLayout, crudFormFactory, crudListener); 73 | initLayout(); 74 | } 75 | 76 | protected void initLayout() { 77 | findAllButton = new Button(VaadinIcon.REFRESH.create(), e -> findAllButtonClicked()); 78 | findAllButton.getElement().setAttribute("title", "Refresh list"); 79 | findAllButton.setVisible(false); 80 | 81 | crudLayout.addToolbarComponent(findAllButton); 82 | 83 | addButton = new Button(VaadinIcon.PLUS.create(), e -> addButtonClicked()); 84 | addButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); 85 | addButton.getElement().setAttribute("title", "Add"); 86 | crudLayout.addToolbarComponent(addButton); 87 | 88 | updateButton = new Button(VaadinIcon.PENCIL.create(), e -> updateButtonClicked()); 89 | updateButton.getElement().setAttribute("title", "Update"); 90 | crudLayout.addToolbarComponent(updateButton); 91 | 92 | deleteButton = new Button(VaadinIcon.TRASH.create(), e -> deleteButtonClicked()); 93 | deleteButton.addThemeVariants(ButtonVariant.LUMO_ERROR); 94 | deleteButton.getElement().setAttribute("title", "Delete"); 95 | crudLayout.addToolbarComponent(deleteButton); 96 | 97 | grid = createGrid(); 98 | grid.addSelectionListener(e -> gridSelectionChanged()); 99 | crudLayout.setMainComponent(grid); 100 | 101 | updateButtons(); 102 | } 103 | 104 | protected abstract Grid createGrid(); 105 | 106 | public abstract void refreshGrid(); 107 | 108 | @Override 109 | protected void onAttach(AttachEvent attachEvent) { 110 | super.onAttach(attachEvent); 111 | refreshGrid(); 112 | } 113 | 114 | @Override 115 | public void setAddOperationVisible(boolean visible) { 116 | addButton.setVisible(visible); 117 | } 118 | 119 | @Override 120 | public void setUpdateOperationVisible(boolean visible) { 121 | updateButton.setVisible(visible); 122 | } 123 | 124 | @Override 125 | public void setDeleteOperationVisible(boolean visible) { 126 | deleteButton.setVisible(visible); 127 | } 128 | 129 | @Override 130 | public void setFindAllOperationVisible(boolean visible) { 131 | findAllButton.setVisible(visible); 132 | } 133 | 134 | public HasValue addFilterProperty(String caption) { 135 | TextField field = new TextField(); 136 | field.setPlaceholder(caption); 137 | field.setPrefixComponent(VaadinIcon.SEARCH.create()); 138 | field.setClearButtonVisible(true); 139 | field.addValueChangeListener(e -> findAllButtonClicked()); 140 | crudLayout.addFilterComponent(field); 141 | return field; 142 | } 143 | 144 | public void setClickRowToUpdate(boolean clickRowToUpdate) { 145 | this.clickRowToUpdate = clickRowToUpdate; 146 | } 147 | 148 | protected void updateButtons() { 149 | boolean rowSelected = !grid.asSingleSelect().isEmpty(); 150 | updateButton.setEnabled(rowSelected); 151 | deleteButton.setEnabled(rowSelected); 152 | } 153 | 154 | protected void gridSelectionChanged() { 155 | updateButtons(); 156 | T domainObject = grid.asSingleSelect().getValue(); 157 | 158 | if (domainObject != null) { 159 | if (clickRowToUpdate) { 160 | updateButtonClicked(); 161 | } else { 162 | Component form = crudFormFactory.buildNewForm(CrudOperation.READ, domainObject, true, null, 163 | event -> grid.asSingleSelect().clear()); 164 | String caption = crudFormFactory.buildCaption(CrudOperation.READ, domainObject); 165 | crudLayout.showForm(CrudOperation.READ, form, caption); 166 | } 167 | } else { 168 | crudLayout.hideForm(); 169 | } 170 | } 171 | 172 | protected void findAllButtonClicked() { 173 | grid.asSingleSelect().clear(); 174 | refreshGrid(); 175 | 176 | int count = countRows(); 177 | showNotification(String.format(rowCountCaption, count)); 178 | } 179 | 180 | private int countRows() { 181 | var query = new Query(); 182 | var provider = grid.getDataProvider(); 183 | if (HierarchicalDataProvider.class.isAssignableFrom(provider.getClass())) 184 | query = new HierarchicalQuery(null, null); 185 | 186 | return provider.size(query); 187 | } 188 | 189 | protected void addButtonClicked() { 190 | grid.asSingleSelect().clear(); 191 | T domainObject = crudFormFactory.getNewInstanceSupplier().get(); 192 | showForm(CrudOperation.ADD, domainObject, false, savedMessage, event -> { 193 | try { 194 | T addedObject = addOperation.perform(domainObject); 195 | refreshGrid(); 196 | grid.asSingleSelect().setValue(addedObject); 197 | grid.deselect(addedObject); 198 | showNotification(savedMessage); 199 | if (!grid.getClass().isAssignableFrom(TreeGrid.class)) { 200 | grid.scrollToItem(addedObject); 201 | } 202 | } catch (IllegalArgumentException ignore) { 203 | } 204 | }); 205 | } 206 | 207 | protected void updateButtonClicked() { 208 | T domainObject = grid.asSingleSelect().getValue(); 209 | showForm(CrudOperation.UPDATE, domainObject, false, savedMessage, event -> { 210 | try { 211 | T updatedObject = updateOperation.perform(domainObject); 212 | grid.asSingleSelect().clear(); 213 | refreshGrid(); 214 | grid.asSingleSelect().setValue(updatedObject); 215 | grid.deselect(updatedObject); 216 | showNotification(savedMessage); 217 | if (!grid.getClass().isAssignableFrom(TreeGrid.class)) { 218 | grid.scrollToItem(updatedObject); 219 | } 220 | } catch (CrudOperationException e1) { 221 | showNotification(e1.getMessage()); 222 | throw e1; 223 | } 224 | }); 225 | } 226 | 227 | protected void deleteButtonClicked() { 228 | T domainObject = grid.asSingleSelect().getValue(); 229 | showDeleteConfirmation(domainObject); 230 | } 231 | 232 | public void showDeleteConfirmation(T domainObject) { 233 | showForm(CrudOperation.DELETE, domainObject, true, deletedMessage, event -> { 234 | try { 235 | deleteOperation.perform(domainObject); 236 | refreshGrid(); 237 | grid.asSingleSelect().clear(); 238 | showNotification(deletedMessage); 239 | crudLayout.hideForm(); 240 | } catch (CrudOperationException e1) { 241 | showNotification(e1.getMessage()); 242 | refreshGrid(); 243 | } catch (Exception e2) { 244 | refreshGrid(); 245 | throw e2; 246 | } 247 | }); 248 | } 249 | 250 | protected void showForm(CrudOperation operation, T domainObject, boolean readOnly, String successMessage, 251 | ComponentEventListener> buttonClickListener) { 252 | Component form = crudFormFactory.buildNewForm(operation, domainObject, readOnly, cancelClickEvent -> { 253 | if (clickRowToUpdate || operation == CrudOperation.ADD) { 254 | grid.asSingleSelect().clear(); 255 | crudLayout.hideForm(); 256 | } else { 257 | T selected = grid.asSingleSelect().getValue(); 258 | grid.asSingleSelect().clear(); 259 | grid.select(selected); 260 | } 261 | }, operationPerformedClickEvent -> { 262 | buttonClickListener.onComponentEvent(operationPerformedClickEvent); 263 | if (!clickRowToUpdate) { 264 | crudLayout.hideForm(); 265 | } 266 | }); 267 | String caption = crudFormFactory.buildCaption(operation, domainObject); 268 | crudLayout.showForm(operation, form, caption); 269 | } 270 | 271 | public Grid getGrid() { 272 | return grid; 273 | } 274 | 275 | public Button getFindAllButton() { 276 | return findAllButton; 277 | } 278 | 279 | public Button getAddButton() { 280 | return addButton; 281 | } 282 | 283 | public Button getUpdateButton() { 284 | return updateButton; 285 | } 286 | 287 | public Button getDeleteButton() { 288 | return deleteButton; 289 | } 290 | 291 | public void setRowCountCaption(String rowCountCaption) { 292 | this.rowCountCaption = rowCountCaption; 293 | } 294 | 295 | public void setSavedMessage(String savedMessage) { 296 | this.savedMessage = savedMessage; 297 | } 298 | 299 | public void setDeletedMessage(String deletedMessage) { 300 | this.deletedMessage = deletedMessage; 301 | } 302 | 303 | public void setShowNotifications(boolean showNotifications) { 304 | this.showNotifications = showNotifications; 305 | } 306 | 307 | public void showNotification(String text) { 308 | if (showNotifications) { 309 | Notification.show(text); 310 | } 311 | } 312 | 313 | } 314 | -------------------------------------------------------------------------------- /add-on/src/main/java/org/vaadin/crudui/form/AbstractAutoGeneratedCrudFormFactory.java: -------------------------------------------------------------------------------- 1 | package org.vaadin.crudui.form; 2 | 3 | import java.beans.IntrospectionException; 4 | import java.beans.PropertyDescriptor; 5 | import java.lang.reflect.InvocationTargetException; 6 | import java.lang.reflect.Method; 7 | import java.math.BigInteger; 8 | import java.util.ArrayList; 9 | import java.util.Date; 10 | import java.util.HashMap; 11 | import java.util.HashSet; 12 | import java.util.List; 13 | import java.util.Map; 14 | import java.util.Objects; 15 | import java.util.Set; 16 | import java.util.stream.Collectors; 17 | 18 | import com.vaadin.flow.component.ClickEvent; 19 | import com.vaadin.flow.component.Component; 20 | import com.vaadin.flow.component.ComponentEventListener; 21 | import com.vaadin.flow.component.Focusable; 22 | import com.vaadin.flow.component.HasEnabled; 23 | import com.vaadin.flow.component.HasSize; 24 | import com.vaadin.flow.component.HasValue; 25 | import com.vaadin.flow.component.HasValueAndElement; 26 | import com.vaadin.flow.component.Key; 27 | import com.vaadin.flow.component.button.Button; 28 | import com.vaadin.flow.component.icon.Icon; 29 | import com.vaadin.flow.component.icon.VaadinIcon; 30 | import com.vaadin.flow.component.orderedlayout.HorizontalLayout; 31 | import com.vaadin.flow.component.orderedlayout.FlexComponent.JustifyContentMode; 32 | import com.vaadin.flow.component.textfield.PasswordField; 33 | import com.vaadin.flow.component.textfield.TextArea; 34 | import com.vaadin.flow.component.textfield.TextField; 35 | import com.vaadin.flow.data.binder.BeanValidationBinder; 36 | import com.vaadin.flow.data.binder.Binder; 37 | import com.vaadin.flow.data.converter.LocalDateToDateConverter; 38 | import com.vaadin.flow.data.converter.StringToBigIntegerConverter; 39 | import com.vaadin.flow.data.converter.StringToFloatConverter; 40 | import com.vaadin.flow.data.converter.StringToLongConverter; 41 | import com.vaadin.flow.function.SerializableSupplier; 42 | import com.vaadin.flow.internal.BeanUtil; 43 | import com.vaadin.flow.shared.util.SharedUtil; 44 | 45 | import org.vaadin.crudui.crud.CrudOperation; 46 | import org.vaadin.crudui.crud.CrudOperationException; 47 | import org.vaadin.crudui.form.impl.field.provider.DefaultFieldProvider; 48 | import org.vaadin.data.converter.StringToByteConverter; 49 | import org.vaadin.data.converter.StringToCharacterConverter; 50 | 51 | /** 52 | * @author Alejandro Duarte. 53 | */ 54 | public abstract class AbstractAutoGeneratedCrudFormFactory extends AbstractCrudFormFactory { 55 | 56 | protected Map buttonCaptions = new HashMap<>(); 57 | protected Map buttonIcons = new HashMap<>(); 58 | protected Map> buttonStyleNames = new HashMap<>(); 59 | protected Map buttonThemes = new HashMap<>(); 60 | 61 | protected String cancelButtonCaption = "Cancel"; 62 | protected String validationErrorMessage = "Please fix the errors and try again"; 63 | protected Class domainType; 64 | protected SerializableSupplier newInstanceSupplier; 65 | 66 | protected Binder binder; 67 | 68 | public AbstractAutoGeneratedCrudFormFactory(Class domainType) { 69 | this.domainType = domainType; 70 | 71 | setButtonCaption(CrudOperation.READ, "Ok"); 72 | setButtonCaption(CrudOperation.ADD, "Add"); 73 | setButtonCaption(CrudOperation.UPDATE, "Update"); 74 | setButtonCaption(CrudOperation.DELETE, "Yes, delete"); 75 | 76 | setButtonIcon(CrudOperation.READ, null); 77 | setButtonIcon(CrudOperation.ADD, VaadinIcon.CHECK.create()); 78 | setButtonIcon(CrudOperation.UPDATE, VaadinIcon.CHECK.create()); 79 | setButtonIcon(CrudOperation.DELETE, VaadinIcon.TRASH.create()); 80 | 81 | addButtonStyleName(CrudOperation.READ, null); 82 | addButtonTheme(CrudOperation.ADD, "primary"); 83 | addButtonTheme(CrudOperation.UPDATE, "primary"); 84 | addButtonTheme(CrudOperation.DELETE, "error"); 85 | 86 | setVisibleProperties(discoverProperties().toArray(new String[0])); 87 | } 88 | 89 | @Override 90 | public void setNewInstanceSupplier(SerializableSupplier newInstanceSupplier) { 91 | this.newInstanceSupplier = newInstanceSupplier; 92 | } 93 | 94 | @Override 95 | public SerializableSupplier getNewInstanceSupplier() { 96 | if (newInstanceSupplier == null) { 97 | newInstanceSupplier = () -> { 98 | try { 99 | return domainType.newInstance(); 100 | } catch (InstantiationException | IllegalAccessException e) { 101 | e.printStackTrace(); 102 | return null; 103 | } 104 | }; 105 | } 106 | return newInstanceSupplier; 107 | } 108 | 109 | public void setButtonCaption(CrudOperation operation, String caption) { 110 | buttonCaptions.put(operation, caption); 111 | } 112 | 113 | public void setButtonIcon(CrudOperation operation, Icon icon) { 114 | buttonIcons.put(operation, icon); 115 | } 116 | 117 | public void addButtonStyleName(CrudOperation operation, String styleName) { 118 | buttonStyleNames.putIfAbsent(operation, new HashSet<>()); 119 | buttonStyleNames.get(operation).add(styleName); 120 | } 121 | 122 | public void addButtonTheme(CrudOperation operation, String theme) { 123 | buttonThemes.put(operation, theme); 124 | } 125 | 126 | public void setCancelButtonCaption(String cancelButtonCaption) { 127 | this.cancelButtonCaption = cancelButtonCaption; 128 | } 129 | 130 | public void setValidationErrorMessage(String validationErrorMessage) { 131 | this.validationErrorMessage = validationErrorMessage; 132 | } 133 | 134 | protected List discoverProperties() { 135 | try { 136 | List descriptors = BeanUtil.getBeanPropertyDescriptors(domainType); 137 | return descriptors.stream().filter(d -> !d.getName().equals("class")).map(d -> d.getName()) 138 | .collect(Collectors.toList()); 139 | } catch (IntrospectionException e) { 140 | throw new RuntimeException(e); 141 | } 142 | } 143 | 144 | protected List buildFields(CrudOperation operation, T domainObject, boolean readOnly) { 145 | binder = buildBinder(operation, domainObject); 146 | ArrayList fields = new ArrayList<>(); 147 | CrudFormConfiguration configuration = getConfiguration(operation); 148 | 149 | boolean focused = false; 150 | 151 | ArrayList fieldsWithCreationListeners = new ArrayList<>(); 152 | ArrayList creationListeners = new ArrayList<>(); 153 | 154 | for (int i = 0; i < configuration.getVisibleProperties().size(); i++) { 155 | String property = configuration.getVisibleProperties().get(i); 156 | try { 157 | String fieldCaption = null; 158 | if (!configuration.getFieldCaptions().isEmpty()) { 159 | fieldCaption = configuration.getFieldCaptions().get(i); 160 | } 161 | 162 | Class propertyType = BeanUtil.getPropertyType(domainObject.getClass(), property); 163 | 164 | if (propertyType != null) { 165 | 166 | HasValueAndElement field = buildField(configuration, property, propertyType, domainObject); 167 | 168 | if (field != null) { 169 | configureField(field, property, fieldCaption, readOnly, configuration); 170 | bindField(field, property, propertyType, configuration); 171 | fields.add(field); 172 | 173 | if (!focused) { 174 | if (field.isEnabled() && !field.isReadOnly() && field instanceof Focusable) { 175 | ((Focusable) field).focus(); 176 | focused = true; 177 | } 178 | } 179 | 180 | FieldCreationListener creationListener = configuration.getFieldCreationListeners() 181 | .get(property); 182 | if (creationListener != null) { 183 | fieldsWithCreationListeners.add(field); 184 | creationListeners.add(creationListener); 185 | } 186 | } 187 | } 188 | 189 | } catch (Exception e) { 190 | throw new RuntimeException( 191 | "Error creating Field for property " + domainObject.getClass().getName() + "." + property, e); 192 | } 193 | } 194 | 195 | binder.readBean(domainObject); 196 | 197 | for (int i = 0; i < fieldsWithCreationListeners.size(); i++) { 198 | creationListeners.get(i).fieldCreated(fieldsWithCreationListeners.get(i)); 199 | } 200 | 201 | if (!fields.isEmpty() && !readOnly) { 202 | HasValue field = fields.get(0); 203 | if (field instanceof Focusable) { 204 | ((Focusable) field).focus(); 205 | } 206 | } 207 | 208 | return fields; 209 | } 210 | 211 | protected HasValueAndElement buildField(CrudFormConfiguration configuration, String property, Class propertyType, 212 | T domainObject) throws InstantiationException, IllegalAccessException { 213 | HasValueAndElement field; 214 | FieldProvider provider = (FieldProvider) configuration.getFieldProviders().get(property); 215 | 216 | if (provider != null) { 217 | field = provider.buildField(domainObject); 218 | } else { 219 | Class> fieldType = configuration.getFieldTypes().get(property); 220 | if (fieldType != null) { 221 | field = fieldType.newInstance(); 222 | } else { 223 | field = getDefaultFieldProvider(propertyType).buildField(domainObject); 224 | } 225 | } 226 | 227 | return field; 228 | } 229 | 230 | protected FieldProvider getDefaultFieldProvider(Class propertyType) { 231 | return new DefaultFieldProvider(propertyType); 232 | } 233 | 234 | private void configureField(HasValue field, String property, String fieldCaption, boolean readOnly, 235 | CrudFormConfiguration configuration) { 236 | if (fieldCaption == null) { 237 | fieldCaption = propertyIdToHumanFriendly(property); 238 | } 239 | 240 | try { 241 | Method setLabelMethod = field.getClass().getMethod("setLabel", String.class); 242 | setLabelMethod.invoke(field, fieldCaption); 243 | 244 | } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException ignore) { 245 | } 246 | 247 | if (field != null && field instanceof HasSize) { 248 | ((HasSize) field).setWidth("100%"); 249 | } 250 | 251 | field.setReadOnly(readOnly); 252 | 253 | if (!configuration.getDisabledProperties().isEmpty() && field instanceof HasEnabled) { 254 | ((HasEnabled) field).setEnabled(!configuration.getDisabledProperties().contains(property)); 255 | } 256 | } 257 | 258 | protected String propertyIdToHumanFriendly(String property) { 259 | return SharedUtil.propertyIdToHumanFriendly(property); 260 | } 261 | 262 | protected void bindField(HasValue field, String property, Class propertyType, 263 | CrudFormConfiguration configuration) { 264 | Binder.BindingBuilder bindingBuilder = binder.forField(field); 265 | 266 | if (TextField.class.isAssignableFrom(field.getClass()) || PasswordField.class.isAssignableFrom(field.getClass()) 267 | || TextArea.class.isAssignableFrom(field.getClass())) { 268 | bindingBuilder = bindingBuilder.withNullRepresentation(""); 269 | } 270 | 271 | if (configuration.getConverters().containsKey(property)) { 272 | bindingBuilder = bindingBuilder.withConverter(configuration.getConverters().get(property)); 273 | 274 | } else if (Long.class.isAssignableFrom(propertyType) || long.class.isAssignableFrom(propertyType)) { 275 | bindingBuilder = bindingBuilder.withConverter(new StringToLongConverter(null, "Must be a number")); 276 | 277 | } else if (BigInteger.class.isAssignableFrom(propertyType)) { 278 | bindingBuilder = bindingBuilder.withConverter(new StringToBigIntegerConverter(null, "Must be a number")); 279 | 280 | } else if (Byte.class.isAssignableFrom(propertyType) || byte.class.isAssignableFrom(propertyType)) { 281 | bindingBuilder = bindingBuilder.withConverter(new StringToByteConverter(null, "Must be a number")); 282 | 283 | } else if (Character.class.isAssignableFrom(propertyType) || char.class.isAssignableFrom(propertyType)) { 284 | bindingBuilder = bindingBuilder.withConverter(new StringToCharacterConverter()); 285 | 286 | } else if (Float.class.isAssignableFrom(propertyType) || float.class.isAssignableFrom(propertyType)) { 287 | bindingBuilder = bindingBuilder.withConverter(new StringToFloatConverter(null, "Must be a number")); 288 | 289 | } else if (Date.class.isAssignableFrom(propertyType)) { 290 | bindingBuilder = bindingBuilder.withConverter(new LocalDateToDateConverter()); 291 | } 292 | 293 | bindingBuilder.bind(property); 294 | } 295 | 296 | protected Binder buildBinder(CrudOperation operation, T domainObject) { 297 | Binder binder; 298 | 299 | if (getConfiguration(operation).isUseBeanValidation()) { 300 | binder = new BeanValidationBinder(domainObject.getClass()); 301 | } else { 302 | binder = new Binder(domainObject.getClass()); 303 | } 304 | 305 | return binder; 306 | } 307 | 308 | protected Button buildOperationButton(CrudOperation operation, T domainObject, 309 | ComponentEventListener> clickListener) { 310 | if (clickListener == null) { 311 | return null; 312 | } 313 | 314 | String caption = buttonCaptions.get(operation); 315 | Icon icon = buttonIcons.get(operation); 316 | Button button = icon != null ? new Button(caption, icon) : new Button(caption); 317 | if (buttonStyleNames.containsKey(operation)) { 318 | buttonStyleNames.get(operation).stream().filter(Objects::nonNull) 319 | .forEach(button::addClassName); 320 | } 321 | if (buttonThemes.containsKey(operation)) { 322 | button.getElement().setAttribute("theme", buttonThemes.get(operation)); 323 | } 324 | 325 | button.addClickListener(event -> { 326 | if (binder.writeBeanIfValid(domainObject)) { 327 | try { 328 | clickListener.onComponentEvent(event); 329 | } catch (Exception e) { 330 | showError(operation, e); 331 | } 332 | } else { 333 | showNotification(validationErrorMessage); 334 | } 335 | }); 336 | return button; 337 | } 338 | 339 | @Override 340 | public void showError(CrudOperation operation, Exception e) { 341 | if (errorListener != null) { 342 | errorListener.accept(e); 343 | } else { 344 | if (CrudOperationException.class.isAssignableFrom(e.getClass())) { 345 | showNotification(e.getMessage()); 346 | } else { 347 | showNotification("Error"); 348 | throw new RuntimeException("Error executing " + operation.name() + " operation", e); 349 | } 350 | } 351 | } 352 | 353 | protected Button buildCancelButton(ComponentEventListener> clickListener) { 354 | if (clickListener == null) { 355 | return null; 356 | } 357 | 358 | return new Button(cancelButtonCaption, clickListener); 359 | } 360 | 361 | protected Component buildFooter(CrudOperation operation, T domainObject, 362 | ComponentEventListener> cancelButtonClickListener, 363 | ComponentEventListener> operationButtonClickListener) { 364 | Button operationButton = buildOperationButton(operation, domainObject, operationButtonClickListener); 365 | Button cancelButton = buildCancelButton(cancelButtonClickListener); 366 | 367 | HorizontalLayout footerLayout = new HorizontalLayout(); 368 | footerLayout.setWidthFull(); 369 | footerLayout.setSpacing(true); 370 | footerLayout.setPadding(true); 371 | footerLayout.setMargin(false); 372 | footerLayout.getStyle().set("background-color", "var(--lumo-contrast-5pct)"); 373 | footerLayout.setJustifyContentMode(JustifyContentMode.END); 374 | 375 | if (operationButton != null) { 376 | operationButton.addClickShortcut(Key.ENTER); 377 | footerLayout.add(operationButton); 378 | } 379 | 380 | if (cancelButton != null) { 381 | footerLayout.add(cancelButton); 382 | } 383 | 384 | return footerLayout; 385 | } 386 | 387 | } 388 | --------------------------------------------------------------------------------