Skip to content

Commit 87d263b

Browse files
ugur-vaadinvursen
andauthored
feat: add getItemIndex and getParent to hierarchical data providers (#22356)
This PR adds getItemIndex and getParent to HierarchicalDataProviders. This allows the users to define ways to provide item indexes and parents of items efficiently. This allows the TreeGrid component to implement scrollToItem functionality for any data provider without breaking existing implementations. Part of vaadin/flow-components#8076 --------- Co-authored-by: Sergey Vinogradov <mr.vursen@gmail.com>
1 parent 6e9e0bc commit 87d263b

File tree

4 files changed

+284
-0
lines changed

4 files changed

+284
-0
lines changed

flow-data/src/main/java/com/vaadin/flow/data/provider/hierarchy/HierarchicalDataProvider.java

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,12 @@
1515
*/
1616
package com.vaadin.flow.data.provider.hierarchy;
1717

18+
import java.util.Objects;
1819
import java.util.stream.Stream;
1920

2021
import com.vaadin.flow.data.provider.DataProvider;
2122
import com.vaadin.flow.data.provider.FilterUtils;
23+
import com.vaadin.flow.data.provider.InMemoryDataProvider;
2224
import com.vaadin.flow.data.provider.Query;
2325
import com.vaadin.flow.data.provider.hierarchy.HierarchicalFilterUtils.HierarchialConfigurableFilterDataProviderWrapper;
2426
import com.vaadin.flow.data.provider.hierarchy.HierarchicalFilterUtils.HierarchicalFilterDataProviderWrapper;
@@ -316,6 +318,54 @@ public default Stream<T> fetch(Query<T, F> query) {
316318
*/
317319
public boolean hasChildren(T item);
318320

321+
/**
322+
* Gets the parent item for the given item.
323+
*
324+
* @param item
325+
* the item for which to retrieve the parent item for
326+
* @return parent item for the given item or {@code null} if the item is a
327+
* root item
328+
* @throws UnsupportedOperationException
329+
* if not implemented
330+
*/
331+
default T getParent(T item) {
332+
throw new UnsupportedOperationException(
333+
"The getParent method is not implemented for this data provider");
334+
}
335+
336+
/**
337+
* Gets the index of a given item based on the given hierarchical query.
338+
* <p>
339+
* This method must be implemented in accordance with the selected hierarchy
340+
* type, see {@link #getHierarchyFormat()} and {@link HierarchyFormat}.
341+
* <ul>
342+
* <li>If {@link HierarchyFormat#FLATTENED} is used, it should be
343+
* implemented to return the index in the entire flattened tree.
344+
* <li>If {@link HierarchyFormat#NESTED} is used, it should be implemented
345+
* to return the index within the given parent item.
346+
* </ul>
347+
* <p>
348+
* This method has a default implementation for in-memory data providers.
349+
*
350+
* @param item
351+
* the item to get the index for
352+
* @param query
353+
* given query to request data with
354+
* @return the index of the provided item or -1 if not found
355+
* @throws UnsupportedOperationException
356+
* if not implemented
357+
*/
358+
default int getItemIndex(T item, HierarchicalQuery<T, F> query) {
359+
if (isInMemory()) {
360+
Objects.requireNonNull(item, "Item cannot be null");
361+
Objects.requireNonNull(query, "Query cannot be null");
362+
return fetchChildren(query).map(this::getId).toList()
363+
.indexOf(getId(item));
364+
}
365+
throw new UnsupportedOperationException(
366+
"The getItemIndex method is not implemented for this data provider");
367+
}
368+
319369
/**
320370
* Gets the depth of a given item in the hierarchy, starting from zero
321371
* (root).

flow-data/src/main/java/com/vaadin/flow/data/provider/hierarchy/TreeDataProvider.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,16 @@ public boolean hasChildren(T item) {
109109
return !treeData.getChildren(item).isEmpty();
110110
}
111111

112+
@Override
113+
public T getParent(T item) {
114+
Objects.requireNonNull(item, "Item cannot be null.");
115+
try {
116+
return getTreeData().getParent(item);
117+
} catch (IllegalArgumentException e) {
118+
return null;
119+
}
120+
}
121+
112122
@Override
113123
public int getDepth(T item) {
114124
int depth = 0;
Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
/*
2+
* Copyright 2000-2025 Vaadin Ltd.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
5+
* use this file except in compliance with the License. You may obtain a copy of
6+
* the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
* License for the specific language governing permissions and limitations under
14+
* the License.
15+
*/
16+
package com.vaadin.flow.data.provider.hierarchy;
17+
18+
import com.vaadin.flow.data.provider.*;
19+
import com.vaadin.flow.shared.Registration;
20+
import org.junit.Assert;
21+
import org.junit.Test;
22+
23+
import java.util.stream.IntStream;
24+
import java.util.stream.Stream;
25+
26+
public class HierarchicalDataProviderTest {
27+
28+
@Test
29+
public void getParent_throwsUnsupportedOperationException() {
30+
var dataProvider = new TestDataProvider();
31+
var rootItem = new TestBean(null, 0, 3);
32+
Assert.assertThrows(UnsupportedOperationException.class,
33+
() -> dataProvider.getParent(rootItem));
34+
}
35+
36+
@Test
37+
public void inMemoryDataProvider_getNullItemIndex_throwsNullPointerException() {
38+
var dataProvider = new TestDataProvider();
39+
dataProvider.setInMemory(true);
40+
var query = new HierarchicalQuery<TestBean, Object>(null, null);
41+
Assert.assertThrows(NullPointerException.class,
42+
() -> dataProvider.getItemIndex(null, query));
43+
}
44+
45+
@Test
46+
public void inMemoryDataProvider_getItemIndexWithNullQuery_throwsNullPointerException() {
47+
var dataProvider = new TestDataProvider();
48+
dataProvider.setInMemory(true);
49+
var rootItem = new TestBean(null, 0, 3);
50+
Assert.assertThrows(NullPointerException.class,
51+
() -> dataProvider.getItemIndex(rootItem, null));
52+
}
53+
54+
@Test
55+
public void defaultDataProvider_getItemIndex_throwsUnsupportedOperationException() {
56+
var dataProvider = new TestDataProvider();
57+
var rootItem = new TestBean(null, 0, 3);
58+
Assert.assertThrows(UnsupportedOperationException.class,
59+
() -> dataProvider.getItemIndex(rootItem, null));
60+
}
61+
62+
@Test
63+
public void inMemoryDataProvider_getItemIndex_returnsCorrectIndex() {
64+
var dataProvider = new TestDataProvider();
65+
dataProvider.setInMemory(true);
66+
var rootItem = new TestBean(null, 0, 3);
67+
var query = new HierarchicalQuery<TestBean, Object>(null, null);
68+
var itemIndex = dataProvider.getItemIndex(rootItem, query);
69+
Assert.assertEquals(3, itemIndex);
70+
}
71+
72+
@Test
73+
public void inMemoryDataProvider_getChildItemIndex_returnsCorrectIndex() {
74+
var dataProvider = new TestDataProvider();
75+
dataProvider.setInMemory(true);
76+
var rootItem = new TestBean(null, 0, 3);
77+
var childItem = new TestBean(rootItem.getId(), 1, 3);
78+
var query = new HierarchicalQuery<>(null, rootItem);
79+
var itemIndex = dataProvider.getItemIndex(childItem, query);
80+
Assert.assertEquals(3, itemIndex);
81+
}
82+
83+
@Test
84+
public void inMemoryDataProvider_getItemIndexInIncorrectParent_returnsMinusOne() {
85+
var dataProvider = new TestDataProvider();
86+
dataProvider.setInMemory(true);
87+
var rootItem = new TestBean(null, 0, 3);
88+
var anotherRootItem = new TestBean(null, 0, 4);
89+
var query = new HierarchicalQuery<>(null, anotherRootItem);
90+
var itemIndex = dataProvider.getItemIndex(rootItem, query);
91+
Assert.assertEquals(-1, itemIndex);
92+
}
93+
94+
@Test
95+
public void inMemoryDataProvider_getItemIndexForAnIncorrectItem_returnsMinusOne() {
96+
var dataProvider = new TestDataProvider();
97+
dataProvider.setInMemory(true);
98+
var notPresentItem = new TestBean(null, 0, 20000);
99+
var query = new HierarchicalQuery<TestBean, Object>(null, null);
100+
var itemIndex = dataProvider.getItemIndex(notPresentItem, query);
101+
Assert.assertEquals(-1, itemIndex);
102+
}
103+
104+
private static class TestDataProvider
105+
implements HierarchicalDataProvider<TestBean, Object> {
106+
107+
private static final int DEPTH = 3;
108+
private static final int ROOT_ITEM_COUNT = 50;
109+
private static final int CHILD_PER_LEVEL = 10;
110+
111+
private boolean inMemory;
112+
113+
@Override
114+
public int getChildCount(HierarchicalQuery<TestBean, Object> query) {
115+
return hasChildren(query.getParent()) ? ROOT_ITEM_COUNT : 0;
116+
}
117+
118+
@Override
119+
public Stream<TestBean> fetchChildren(
120+
HierarchicalQuery<TestBean, Object> query) {
121+
if (query.getParent() == null) {
122+
return IntStream.range(0, ROOT_ITEM_COUNT)
123+
.mapToObj(i -> new TestBean(null, 0, i));
124+
}
125+
return IntStream.range(0, CHILD_PER_LEVEL)
126+
.mapToObj(i -> new TestBean(query.getParent().getId(),
127+
query.getParent().getDepth() + 1, i));
128+
}
129+
130+
@Override
131+
public boolean hasChildren(TestBean o) {
132+
if (o == null) {
133+
return true;
134+
}
135+
return o.getDepth() < DEPTH - 1;
136+
}
137+
138+
public void setInMemory(boolean inMemory) {
139+
this.inMemory = inMemory;
140+
}
141+
142+
@Override
143+
public boolean isInMemory() {
144+
return inMemory;
145+
}
146+
147+
@Override
148+
public void refreshItem(TestBean o) {
149+
// NO-OP
150+
}
151+
152+
@Override
153+
public void refreshAll() {
154+
// NO-OP
155+
}
156+
157+
@Override
158+
public Registration addDataProviderListener(
159+
DataProviderListener<TestBean> listener) {
160+
return null;
161+
}
162+
}
163+
164+
private static class TestBean {
165+
private final String id;
166+
private final int depth;
167+
private final int index;
168+
169+
public TestBean(String parentId, int depth, int index) {
170+
id = (parentId == null ? "" : parentId) + "/" + depth + "/" + index;
171+
this.depth = depth;
172+
this.index = index;
173+
}
174+
175+
public int getDepth() {
176+
return depth;
177+
}
178+
179+
public int getIndex() {
180+
return index;
181+
}
182+
183+
public String getId() {
184+
return id;
185+
}
186+
187+
@Override
188+
public String toString() {
189+
return depth + " | " + index;
190+
}
191+
192+
@Override
193+
public int hashCode() {
194+
return id.hashCode();
195+
}
196+
197+
@Override
198+
public boolean equals(Object obj) {
199+
if (obj instanceof TestBean other) {
200+
return id.equals(other.id);
201+
}
202+
return false;
203+
}
204+
}
205+
}

flow-data/src/test/java/com/vaadin/flow/data/provider/hierarchy/TreeDataProviderTest.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,25 @@ public void addFilter() {
288288
assertEquals(8, sizeWithUnfilteredQuery());
289289
}
290290

291+
@Test
292+
public void rootItem_getParent_returnsNull() {
293+
var rootItem = rootData.get(0);
294+
assertNull(getDataProvider().getParent(rootItem));
295+
}
296+
297+
@Test
298+
public void childItem_getParent_returnsParent() {
299+
var rootItem = rootData.get(0);
300+
var childItem = data.getChildren(rootItem).get(0);
301+
assertEquals(rootItem, data.getParent(childItem));
302+
}
303+
304+
@Test
305+
public void notPresentItem_getParent_returnsNull() {
306+
var itemNotPresentInProvider = new StrBean("Not present", -1, 0);
307+
assertNull(getDataProvider().getParent(itemNotPresentInProvider));
308+
}
309+
291310
@Override
292311
public void filteringListDataProvider_convertFilter() {
293312
HierarchicalDataProvider<StrBean, String> strFilterDataProvider = getDataProvider()

0 commit comments

Comments
 (0)