diff --git a/frontend/themes/test-entities-auth/components/common-input-field.css b/frontend/themes/test-entities-auth/components/common-input-field.css new file mode 100644 index 0000000..172faef --- /dev/null +++ b/frontend/themes/test-entities-auth/components/common-input-field.css @@ -0,0 +1,44 @@ +[part="input-field"] { + box-shadow: inset 0 0 0 1px var(--lumo-contrast-30pct); + background-color: var(--lumo-base-color); +} + +[part="input-field"]::after { + background-color: transparent; + box-shadow: inset 0 0 0 1px var(--lumo-contrast-50pct); +} + +:host(:hover:not([readonly]):not([focused])) [part="input-field"]::after { + opacity: 0.5; +} + +:host([focused]:not([invalid])) [part="input-field"] { + box-shadow: inset 0 0 0 1px var(--lumo-primary-color); +} + +:host([focus-ring]:not([invalid])) [part="input-field"] { + box-shadow: 0 0 0 1px var(--lumo-primary-color), inset 0 0 0 1px var(--lumo-primary-color); +} + +:host([invalid]) [part="input-field"] { + background-color: var(--lumo-error-color-5pct); + box-shadow: inset 0 0 0 1px var(--lumo-error-color); +} + +:host([invalid][focus-ring]) [part="input-field"] { + box-shadow: 0 0 0 1px var(--lumo-error-color), inset 0 0 0 1px var(--lumo-error-color); +} + +:host([readonly]) [part="input-field"] { + box-shadow: none; +} + +:host([readonly]) [part="input-field"]::after { + box-shadow: none; +} + +:host([disabled]) [part="input-field"] { + box-shadow: inset 0 0 0 1px var(--lumo-contrast-10pct); + background-color: var(--lumo-contrast-5pct); +} + diff --git a/frontend/themes/test-entities-auth/components/vaadin-checkbox.css b/frontend/themes/test-entities-auth/components/vaadin-checkbox.css new file mode 100644 index 0000000..1dafc5b --- /dev/null +++ b/frontend/themes/test-entities-auth/components/vaadin-checkbox.css @@ -0,0 +1,13 @@ +[part="checkbox"] { + background-color: var(--lumo-base-color); + box-shadow: inset 0 0 0 1px var(--lumo-contrast-30pct); +} + +:host(:not([checked]):not([indeterminate]):not([disabled]):hover) [part="checkbox"] { + background-color: var(--lumo-base-color); + transition: 0.2s; +} + +:host(:not([focus-ring]):hover) [part="checkbox"] { + box-shadow: inset 0 0 0 1px var(--lumo-contrast-50pct); +} \ No newline at end of file diff --git a/frontend/themes/test-entities-auth/components/vaadin-combo-box.css b/frontend/themes/test-entities-auth/components/vaadin-combo-box.css new file mode 100644 index 0000000..f68e6e7 --- /dev/null +++ b/frontend/themes/test-entities-auth/components/vaadin-combo-box.css @@ -0,0 +1 @@ +@import "common-input-field.css"; \ No newline at end of file diff --git a/frontend/themes/test-entities-auth/components/vaadin-date-picker.css b/frontend/themes/test-entities-auth/components/vaadin-date-picker.css new file mode 100644 index 0000000..f68e6e7 --- /dev/null +++ b/frontend/themes/test-entities-auth/components/vaadin-date-picker.css @@ -0,0 +1 @@ +@import "common-input-field.css"; \ No newline at end of file diff --git a/frontend/themes/test-entities-auth/components/vaadin-multi-select-combo-box.css b/frontend/themes/test-entities-auth/components/vaadin-multi-select-combo-box.css new file mode 100644 index 0000000..f68e6e7 --- /dev/null +++ b/frontend/themes/test-entities-auth/components/vaadin-multi-select-combo-box.css @@ -0,0 +1 @@ +@import "common-input-field.css"; \ No newline at end of file diff --git a/frontend/themes/test-entities-auth/components/vaadin-number-field.css b/frontend/themes/test-entities-auth/components/vaadin-number-field.css new file mode 100644 index 0000000..f68e6e7 --- /dev/null +++ b/frontend/themes/test-entities-auth/components/vaadin-number-field.css @@ -0,0 +1 @@ +@import "common-input-field.css"; \ No newline at end of file diff --git a/frontend/themes/test-entities-auth/components/vaadin-password-field.css b/frontend/themes/test-entities-auth/components/vaadin-password-field.css new file mode 100644 index 0000000..f68e6e7 --- /dev/null +++ b/frontend/themes/test-entities-auth/components/vaadin-password-field.css @@ -0,0 +1 @@ +@import "common-input-field.css"; \ No newline at end of file diff --git a/frontend/themes/test-entities-auth/components/vaadin-radio-button.css b/frontend/themes/test-entities-auth/components/vaadin-radio-button.css new file mode 100644 index 0000000..41b56d0 --- /dev/null +++ b/frontend/themes/test-entities-auth/components/vaadin-radio-button.css @@ -0,0 +1,13 @@ +[part="radio"] { + background-color: var(--lumo-base-color); + box-shadow: inset 0 0 0 1px var(--lumo-contrast-30pct); +} + +:host(:not([checked]):not([indeterminate]):not([disabled]):hover) [part="radio"] { + background-color: var(--lumo-base-color); + transition: 0.2s; +} + +:host(:not([focus-ring]):hover) [part="radio"] { + box-shadow: inset 0 0 0 1px var(--lumo-contrast-50pct); +} \ No newline at end of file diff --git a/frontend/themes/test-entities-auth/components/vaadin-select.css b/frontend/themes/test-entities-auth/components/vaadin-select.css new file mode 100644 index 0000000..f68e6e7 --- /dev/null +++ b/frontend/themes/test-entities-auth/components/vaadin-select.css @@ -0,0 +1 @@ +@import "common-input-field.css"; \ No newline at end of file diff --git a/frontend/themes/test-entities-auth/components/vaadin-text-area.css b/frontend/themes/test-entities-auth/components/vaadin-text-area.css new file mode 100644 index 0000000..4a82b13 --- /dev/null +++ b/frontend/themes/test-entities-auth/components/vaadin-text-area.css @@ -0,0 +1,6 @@ +@import "common-input-field.css"; + +:host(:hover:not([readonly]):not([focused]):not([invalid])) [part='input-field'] { + background-color: var(--lumo-base-color); + box-shadow: inset 0 0 0 1px var(--lumo-contrast-50pct); +} \ No newline at end of file diff --git a/frontend/themes/test-entities-auth/components/vaadin-text-field.css b/frontend/themes/test-entities-auth/components/vaadin-text-field.css new file mode 100644 index 0000000..f68e6e7 --- /dev/null +++ b/frontend/themes/test-entities-auth/components/vaadin-text-field.css @@ -0,0 +1 @@ +@import "common-input-field.css"; \ No newline at end of file diff --git a/frontend/themes/test-entities-auth/components/vaadin-time-picker.css b/frontend/themes/test-entities-auth/components/vaadin-time-picker.css new file mode 100644 index 0000000..f68e6e7 --- /dev/null +++ b/frontend/themes/test-entities-auth/components/vaadin-time-picker.css @@ -0,0 +1 @@ +@import "common-input-field.css"; \ No newline at end of file diff --git a/frontend/themes/test-entities-auth/styles.css b/frontend/themes/test-entities-auth/styles.css index d3ce3ad..ae872e7 100644 --- a/frontend/themes/test-entities-auth/styles.css +++ b/frontend/themes/test-entities-auth/styles.css @@ -1,4 +1,5 @@ @import url('./main-layout.css'); @import url('./views/master-detail-person-view.css'); @import url('./views/master-detail-book-view.css'); +@import url('./views/filter-person-view.css'); @import url('line-awesome/dist/line-awesome/css/line-awesome.min.css'); \ No newline at end of file diff --git a/frontend/themes/test-entities-auth/views/filter-person-view.css b/frontend/themes/test-entities-auth/views/filter-person-view.css new file mode 100644 index 0000000..4e67ac8 --- /dev/null +++ b/frontend/themes/test-entities-auth/views/filter-person-view.css @@ -0,0 +1,67 @@ +.filter-person-view .filter-layout { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); + align-items: end; + gap: 0 var(--lumo-space-l); +} + +.filter-person-view .filter-layout .double-width { + grid-column-end: span 2; +} + +.filter-person-view .filter-layout .actions { + display: flex; + justify-content: flex-end; + align-items: flex-end; + grid-column-end: -1; +} + +.filter-person-view .filter-layout vaadin-combo-box { + --vaadin-field-default-width: auto; +} + +/* Mobile filters */ +.filter-person-view .mobile-filters { + display: none; + position: relative; +} + +.filter-person-view .mobile-filters span { + font-size: var(--lumo-font-size-m); + font-weight: 500; +} + +@media screen and (max-width: 800px) { + .filter-person-view .filter-layout { + display: none; + gap: var(--lumo-space-m); + padding-left: var(--lumo-space-m); + padding-right: var(--lumo-space-m); + } + + .filter-person-view .filter-layout.visible { + display: grid; + padding-top: 0; + } + + .filter-person-view .mobile-filters { + display: flex; + cursor: pointer; + color: var(--lumo-secondary-text-color); + } + + .filter-person-view .mobile-filters:hover::before { + content: ""; + left: 0; + right: 0; + top: 0; + bottom: 0; + position: absolute; + opacity: 0.02; + background-color: currentcolor; + } + + .filter-person-view .mobile-filters:hover { + color: var(--lumo-body-text-color); + } +} \ No newline at end of file diff --git a/src/main/java/my/app/views/filterpersonview/FilterPersonView.java b/src/main/java/my/app/views/filterpersonview/FilterPersonView.java new file mode 100644 index 0000000..1ec2445 --- /dev/null +++ b/src/main/java/my/app/views/filterpersonview/FilterPersonView.java @@ -0,0 +1,174 @@ +package my.app.views.filterpersonview; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.Text; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.checkbox.CheckboxGroup; +import com.vaadin.flow.component.combobox.ComboBox; +import com.vaadin.flow.component.combobox.MultiSelectComboBox; +import com.vaadin.flow.component.datepicker.DatePicker; +import com.vaadin.flow.component.dependency.Uses; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.grid.GridVariant; +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.component.html.H5; +import com.vaadin.flow.component.html.Span; +import com.vaadin.flow.component.icon.Icon; +import com.vaadin.flow.component.icon.VaadinIcon; +import com.vaadin.flow.component.orderedlayout.FlexComponent; +import com.vaadin.flow.component.orderedlayout.FlexLayout; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.renderer.LitRenderer; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.server.auth.AnonymousAllowed; +import com.vaadin.flow.spring.data.VaadinSpringDataHelpers; +import com.vaadin.flow.theme.lumo.LumoIcon; +import com.vaadin.flow.theme.lumo.LumoUtility; +import my.app.data.entity.SamplePerson; +import my.app.data.service.SamplePersonService; +import my.app.views.MainLayout; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.PageRequest; + +@PageTitle("Filter Person") +@Route(value = "filter-person", layout = MainLayout.class) +@AnonymousAllowed +@Uses(Icon.class) +public class FilterPersonView extends Div { + + private final Grid grid = new Grid<>(SamplePerson.class, false); + + private SamplePerson samplePerson; + private DatePicker start; + private DatePicker end; + private Div filterLayout; + + private final SamplePersonService samplePersonService; + + @Autowired + public FilterPersonView(SamplePersonService samplePersonService) { + this.samplePersonService = samplePersonService; + setSizeFull(); + addClassNames("filter-person-view"); + + // Mobile version + HorizontalLayout mobileFilters = new HorizontalLayout(); + mobileFilters.setWidthFull(); + mobileFilters.addClassNames(LumoUtility.Padding.MEDIUM, LumoUtility.BoxSizing.BORDER, LumoUtility.AlignItems.CENTER); + mobileFilters.addClassName("mobile-filters"); + + Icon mobileIcon = new Icon("lumo", "plus"); + Span filtersHeading = new Span("Filters"); + mobileFilters.add(mobileIcon, filtersHeading); + mobileFilters.setFlexGrow(1, filtersHeading); + mobileFilters.addClickListener(e -> { + if (filterLayout.getClassNames().contains("visible")) { + filterLayout.removeClassName("visible"); + mobileIcon.getElement().setAttribute("icon", "lumo:plus"); + } else { + filterLayout.addClassName("visible"); + mobileIcon.getElement().setAttribute("icon", "lumo:minus"); + } + }); + + VerticalLayout layout = new VerticalLayout(mobileFilters, createFilters(), createGrid()); + layout.setSizeFull(); + layout.setPadding(false); + layout.setSpacing(false); + add(layout); + } + + private Component createFilters() { + filterLayout = new Div(); + filterLayout.setWidthFull(); + filterLayout.addClassName("filter-layout"); + filterLayout.addClassNames(LumoUtility.Padding.Horizontal.LARGE, LumoUtility.Padding.Vertical.MEDIUM, LumoUtility.BoxSizing.BORDER); + + TextField nameFilter = new TextField("Name"); + nameFilter.setPlaceholder("First or last name"); + + TextField phoneFilter = new TextField("Phone"); + + MultiSelectComboBox occupationFilter = new MultiSelectComboBox<>("Occupation"); + occupationFilter.setItems("Item 1", "Item 2", "Item 3"); + + CheckboxGroup roleFilter = new CheckboxGroup<>("Role"); + roleFilter.setItems("Worker", "Supervisor", "Management", "External"); + roleFilter.addClassName("double-width"); + + // Action buttons + Button resetBtn = new Button("Reset"); + resetBtn.addThemeVariants(ButtonVariant.LUMO_TERTIARY); + Button searchBtn = new Button("Search"); + searchBtn.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + Div actions = new Div(resetBtn, searchBtn); + actions.addClassName(LumoUtility.Gap.SMALL); + actions.addClassName("actions"); + + filterLayout.add(nameFilter, phoneFilter, createDateRangeFilter(), occupationFilter, roleFilter, actions); + return filterLayout; + } + + + + private Component createDateRangeFilter() { + ComboBox dateType = new ComboBox<>("Date"); + dateType.setItems("Date of birth", "Start date"); + dateType.setWidthFull(); + + start = new DatePicker(); + start.setPlaceholder("From"); + start.setWidth("9rem"); + + end = new DatePicker(); + end.setPlaceholder("To"); + end.setWidth("9rem"); + + // aria-label for screen readers + start.getElement() + .executeJs("const start = this.inputElement;" + + "start.setAttribute('aria-label', 'From date');" + + "start.removeAttribute('aria-labelledby');"); + end.getElement() + .executeJs("const end = this.inputElement;" + + "end.setAttribute('aria-label', 'To date');" + + "end.removeAttribute('aria-labelledby');"); + + FlexLayout dateRangeComponent = new FlexLayout(dateType, start, new Text(" – "), end); + dateRangeComponent.setAlignItems(FlexComponent.Alignment.BASELINE); + dateRangeComponent.addClassName(LumoUtility.Gap.XSMALL); + dateRangeComponent.addClassName("double-width"); + + return dateRangeComponent; + } + + private Component createGrid() { + grid.addColumn("firstName").setAutoWidth(true); + grid.addColumn("lastName").setAutoWidth(true); + grid.addColumn("email").setAutoWidth(true); + grid.addColumn("phone").setAutoWidth(true); + grid.addColumn("dateOfBirth").setAutoWidth(true); + grid.addColumn("occupation").setAutoWidth(true); + LitRenderer importantRenderer = LitRenderer.of( + "") + .withProperty("icon", important -> important.isImportant() ? "check" : "minus").withProperty("color", + important -> important.isImportant() + ? "var(--lumo-primary-text-color)" + : "var(--lumo-disabled-text-color)"); + + grid.addColumn(importantRenderer).setHeader("Important").setAutoWidth(true); + + grid.setItems(query -> samplePersonService.list( + PageRequest.of(query.getPage(), query.getPageSize(), VaadinSpringDataHelpers.toSpringDataSort(query))) + .stream()); + grid.addThemeVariants(GridVariant.LUMO_NO_BORDER); + grid.addClassNames(LumoUtility.Border.TOP, LumoUtility.BorderColor.CONTRAST_10); + + return grid; + } + +}