Skip to content

Enhanced selection throws IndexOutOfBoundsException in grid with filterable column #125

@ngonzalezpazFC

Description

@ngonzalezpazFC

Describe the bug

An IndexOutOfBoundsException is thrown when a grid column is configured as filterable and the grid with GridHelper.setEnhancedSelectionEnabled(grid, true);
The issue occurs when the applied filter results in no matching items, leaving the grid empty after filtering.

Example stacktrace:

java.lang.IndexOutOfBoundsException: Requested index 0 on empty data.
	at com.vaadin.flow.data.provider.AbstractListDataView.validateItemIndex(AbstractListDataView.java:297) ~[flow-data-24.5.9.jar:24.5.9]
	at com.vaadin.flow.data.provider.AbstractListDataView.getItem(AbstractListDataView.java:82) ~[flow-data-24.5.9.jar:24.5.9]
	at com.flowingcode.vaadin.addons.gridhelpers.EnhancedSelectionGridHelper.lambda$enableEnhancedSelection$b9260111$1(EnhancedSelectionGridHelper.java:113) ~[grid-helpers-1.3.2.jar:1.3.2]
	at com.vaadin.flow.internal.nodefeature.ElementListenerMap.lambda$fireEvent$2(ElementListenerMap.java:475) ~[flow-server-24.5.9.jar:24.5.9]
	at java.base/java.util.ArrayList.forEach(ArrayList.java:1511) ~[na:na]
	at com.vaadin.flow.internal.nodefeature.ElementListenerMap.fireEvent(ElementListenerMap.java:475) ~[flow-server-24.5.9.jar:24.5.9]
	at com.vaadin.flow.server.communication.rpc.EventRpcHandler.handleNode(EventRpcHandler.java:62) ~[flow-server-24.5.9.jar:24.5.9]
	at com.vaadin.flow.server.communication.rpc.AbstractRpcInvocationHandler.handle(AbstractRpcInvocationHandler.java:73) ~[flow-server-24.5.9.jar:24.5.9]
	at com.vaadin.flow.server.communication.ServerRpcHandler.handleInvocationData(ServerRpcHandler.java:550) ~[flow-server-24.5.9.jar:24.5.9]
	at com.vaadin.flow.server.communication.ServerRpcHandler.lambda$handleInvocations$6(ServerRpcHandler.java:531) ~[flow-server-24.5.9.jar:24.5.9]
	at java.base/java.util.ArrayList.forEach(ArrayList.java:1511) ~[na:na]
	...

Expected behavior

No exceptions should be thrown after filtering and obtaining no results.

Minimal reproducible example

import com.flowingcode.vaadin.addons.gridhelpers.GridHelper;
import com.vaadin.flow.component.Component;
import com.vaadin.flow.component.grid.Grid;
import com.vaadin.flow.component.grid.Grid.SelectionMode;
import com.vaadin.flow.component.grid.HeaderRow;
import com.vaadin.flow.component.grid.dataview.GridListDataView;
import com.vaadin.flow.component.html.NativeLabel;
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.component.textfield.TextFieldVariant;
import com.vaadin.flow.data.value.ValueChangeMode;
import com.vaadin.flow.router.Menu;
import com.vaadin.flow.router.PageTitle;
import com.vaadin.flow.router.Route;
import java.util.List;
import java.util.function.Consumer;
import org.vaadin.lineawesome.LineAwesomeIconUrl;

@PageTitle("Hello World")
@Route("")
@Menu(order = 0, icon = LineAwesomeIconUrl.GLOBE_SOLID)
public class HelloWorldView extends HorizontalLayout {

  TextField name;
  Grid<Person> grid = new Grid<>(Person.class, false);

  public HelloWorldView() {
    Grid.Column<Person> nameColumn = grid.addColumn(Person::getFullName);
    Grid.Column<Person> emailColumn = grid.addColumn(Person::getEmail);
    Grid.Column<Person> professionColumn = grid.addColumn(Person::getProfession);
    Grid.Column<Person> docColumn = grid.addColumn(Person::getDocumentNumber);

    List<Person> people =
        List.of(new Person("Alice Johnson", "alice.johnson@example.com", "Software Engineer", 100000001L),
            new Person("Bob Smith", "bob.smith@example.com", "Doctor", 100000002L),
            new Person("Charlie Brown", "charlie.brown@example.com", "Teacher", 100000003L),
            new Person("David Wilson", "david.wilson@example.com", "Lawyer", 100000004L),
            new Person("Emma Davis", "emma.davis@example.com", "Architect", 100000005L),
            new Person("Frank Miller", "frank.miller@example.com", "Graphic Designer", 100000006L),
            new Person("Grace Lee", "grace.lee@example.com", "Nurse", 100000007L),
            new Person("Henry Adams", "henry.adams@example.com", "Mechanical Engineer", 100000008L),
            new Person("Isabel White", "isabel.white@example.com", "Journalist", 100000009L),
            new Person("Jack Turner", "jack.turner@example.com", "Accountant", 100000010L));
    GridListDataView<Person> dataView = grid.setItems(people);
    PersonFilter personFilter = new PersonFilter(dataView);

    grid.setSelectionMode(SelectionMode.MULTI);
    GridHelper.setEnhancedSelectionEnabled(grid, true);

    grid.getHeaderRows().clear();
    HeaderRow headerRow = grid.appendHeaderRow();

    headerRow.getCell(nameColumn).setComponent(createFilterHeader("Name", personFilter::setFullName));
    headerRow.getCell(emailColumn).setComponent(createFilterHeader("Email", personFilter::setEmail));
    headerRow.getCell(professionColumn).setComponent(createFilterHeader("Profession", personFilter::setProfession));
    headerRow.getCell(docColumn).setComponent(createFilterHeader("Profession", personFilter::setDocumentNumber));

    add(grid);
  }

  private static Component createFilterHeader(String labelText, Consumer<String> filterChangeConsumer) {
    NativeLabel label = new NativeLabel(labelText);
    label.getStyle().set("padding-top", "var(--lumo-space-m)").set("font-size", "var(--lumo-font-size-xs)");
    TextField textField = new TextField();
    textField.setValueChangeMode(ValueChangeMode.EAGER);
    textField.setClearButtonVisible(true);
    textField.addThemeVariants(TextFieldVariant.LUMO_SMALL);
    textField.setWidthFull();
    textField.getStyle().set("max-width", "100%");
    textField.addValueChangeListener(e -> {
      filterChangeConsumer.accept(e.getValue());
    });

    VerticalLayout layout = new VerticalLayout(label, textField);
    layout.getThemeList().clear();
    layout.getThemeList().add("spacing-xs");

    return layout;
  }


  public class Person {
    String fullName;
    String email;
    String profession;
    Long documentNumber;

    public Person(String fullName, String email, String profession, Long documentNumber) {
      this.fullName = fullName;
      this.email = email;
      this.profession = profession;
      this.documentNumber = documentNumber;
    }

    public String getFullName() {
      return fullName;
    }

    public void setFullName(String fullName) {
      this.fullName = fullName;
    }

    public String getEmail() {
      return email;
    }

    public void setEmail(String email) {
      this.email = email;
    }

    public String getProfession() {
      return profession;
    }

    public void setProfession(String profession) {
      this.profession = profession;
    }

    public Long getDocumentNumber() {
      return documentNumber;
    }

    public void setDocumentNumber(Long documentNumber) {
      this.documentNumber = documentNumber;
    }
  }


  private static class PersonFilter {
    private final GridListDataView<Person> dataView;

    private String fullName;
    private String email;
    private String profession;
    private Long documentNumber;

    public PersonFilter(GridListDataView<Person> dataView) {
      this.dataView = dataView;
      this.dataView.addFilter(this::test);
    }

    public void setFullName(String fullName) {
      this.fullName = fullName;
      this.dataView.refreshAll();
    }

    public void setDocumentNumber(String documentNumber) {
      this.dataView.refreshAll();
      if (!documentNumber.isBlank())
        this.documentNumber = Long.valueOf(documentNumber);
      else
        this.documentNumber = null;
    }

    public void setEmail(String email) {
      this.email = email;
      this.dataView.refreshAll();
    }

    public void setProfession(String profession) {
      this.profession = profession;
      this.dataView.refreshAll();
    }

    public boolean test(Person person) {
      boolean matchesFullName = matches(person.getFullName(), fullName);
      boolean matchesEmail = matches(person.getEmail(), email);
      boolean matchesProfession = matches(person.getProfession(), profession);
      boolean matchesDocumentNumber = matches(person.getDocumentNumber(), documentNumber);


      return matchesFullName && matchesEmail && matchesProfession && matchesDocumentNumber;
    }

    private boolean matches(String value, String searchTerm) {
      return searchTerm == null || searchTerm.isEmpty() || value.toLowerCase().contains(searchTerm.toLowerCase());
    }

    private boolean matches(Long value, Long searchTerm) {
      return searchTerm == null || value.toString().contains(searchTerm.toString());
    }
  }
}

Add-on Version

1.4.1

Vaadin Version

24.6.4

Additional information

No response

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

No type

Projects

Status

Done

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions