• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

knowledgepixels / nanodash / 26622127697

29 May 2026 06:33AM UTC coverage: 20.725% (-0.007%) from 20.732%
26622127697

push

github

tkuhn
feat: apply view filters live as the user types

Filter text fields used AjaxFormComponentUpdatingBehavior("change"),
which only fires on blur/Enter, so the filter required pressing Enter.

Add a reusable FilterUpdatingBehavior that listens on the "input" event
(fires per keystroke) with a 300ms debounce so rapid typing coalesces
into a single request. Swap it into all filter fields.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

1003 of 6148 branches covered (16.31%)

Branch coverage included in aggregate %.

2584 of 11160 relevant lines covered (23.15%)

3.31 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
src/main/java/com/knowledgepixels/nanodash/component/ItemListPanel.java
1
package com.knowledgepixels.nanodash.component;
2

3
import java.io.Serializable;
4
import java.util.ArrayList;
5
import java.util.List;
6
import java.util.function.Function;
7
import java.util.function.Supplier;
8

9
import com.knowledgepixels.nanodash.FilteredListDataProvider;
10

11
import org.apache.wicket.Component;
12
import org.apache.wicket.ajax.AjaxRequestTarget;
13
import org.apache.wicket.behavior.AttributeAppender;
14
import org.apache.wicket.extensions.ajax.markup.html.AjaxLazyLoadPanel;
15
import org.apache.wicket.markup.html.WebMarkupContainer;
16
import org.apache.wicket.markup.html.basic.Label;
17
import org.apache.wicket.markup.html.form.TextField;
18
import org.apache.wicket.markup.html.link.AbstractLink;
19
import org.apache.wicket.markup.html.link.BookmarkablePageLink;
20
import org.apache.wicket.markup.html.panel.Panel;
21
import org.apache.wicket.model.IModel;
22
import org.apache.wicket.model.Model;
23
import org.apache.wicket.request.mapper.parameter.PageParameters;
24
import org.nanopub.extra.services.ApiResponse;
25
import org.nanopub.extra.services.QueryRef;
26

27
import com.knowledgepixels.nanodash.ApiCache;
28
import com.knowledgepixels.nanodash.domain.AbstractResourceWithProfile;
29
import com.knowledgepixels.nanodash.page.NanodashPage;
30

31
public class ItemListPanel<T extends Serializable> extends Panel {
32

33
    private String description;
34
    private List<AbstractLink> buttons = new ArrayList<>();
×
35
    private List<AbstractLink> memberButtons = new ArrayList<>();
×
36
    private List<AbstractLink> adminButtons = new ArrayList<>();
×
37
    private AbstractResourceWithProfile resourceWithProfile;
38
    private boolean finalized = false;
×
39
    private boolean lazyLoading = false;
×
40
    private ReadyFunction readyFunction;
41

42
    private ItemListPanel(String markupId, String title) {
43
        super(markupId);
×
44
        setOutputMarkupId(true);
×
45

46
        add(new Label("title", title));
×
47
        WebMarkupContainer filterContainer = new WebMarkupContainer("filterContainer");
×
48
        filterContainer.add(new TextField<>("filter", Model.of("")).setVisible(false));
×
49
        filterContainer.setVisible(false);
×
50
        add(filterContainer);
×
51
    }
×
52

53
    private void addFilterAndItemList(String itemListId, List<T> items, ComponentProvider<T> compProvider, FilteredListDataProvider.SerializableFunction<T, String> filterTextGetter) {
54
        if (filterTextGetter != null) {
×
55
            IModel<String> filterModel = Model.of("");
×
56
            TextField<String> filterField = new TextField<>("filter", filterModel);
×
57
            filterField.setOutputMarkupId(true);
×
58
            filterField.add(new FilterUpdatingBehavior() {
×
59
                @Override
60
                protected void onUpdate(AjaxRequestTarget target) {
61
                    target.add(ItemListPanel.this);
×
62
                }
×
63
            });
64
            WebMarkupContainer filterContainer = new WebMarkupContainer("filterContainer");
×
65
            filterContainer.add(filterField);
×
66
            get("filterContainer").replaceWith(filterContainer);
×
67
            add(new ItemList<T>(itemListId, items, compProvider, filterTextGetter, filterModel));
×
68
        } else {
×
69
            add(new ItemList<T>(itemListId, items, compProvider, null));
×
70
        }
71
    }
×
72

73
    public ItemListPanel(String markupId, String title, List<T> items, ComponentProvider<T> compProvider) {
74
        this(markupId, title, items, compProvider, null);
×
75
    }
×
76

77
    public ItemListPanel(String markupId, String title, List<T> items, ComponentProvider<T> compProvider, FilteredListDataProvider.SerializableFunction<T, String> filterTextGetter) {
78
        this(markupId, title);
×
79
        addFilterAndItemList("itemlist", items, compProvider, filterTextGetter);
×
80
    }
×
81

82
    public ItemListPanel(String markupId, String title, QueryRef queryRef, ApiResultListProvider<T> resultListProvider, ComponentProvider<T> compProvider) {
83
        this(markupId, title, queryRef, resultListProvider, compProvider, null);
×
84
    }
×
85

86
    public ItemListPanel(String markupId, String title, QueryRef queryRef, ApiResultListProvider<T> resultListProvider, ComponentProvider<T> compProvider, FilteredListDataProvider.SerializableFunction<T, String> filterTextGetter) {
87
        this(markupId, title);
×
88

89
        ApiResponse qResponse = ApiCache.retrieveResponseAsync(queryRef);
×
90
        if (qResponse != null) {
×
91
            addFilterAndItemList("itemlist", resultListProvider.apply(qResponse), compProvider, filterTextGetter);
×
92
        } else {
93
            lazyLoading = true;
×
94
            final FilteredListDataProvider.SerializableFunction<T, String> getter = filterTextGetter;
×
95
            add(new ApiResultComponent("itemlist", queryRef) {
×
96
                @Override
97
                public Component getApiResultComponent(String markupId, ApiResponse response) {
98
                    if (getter != null) {
×
99
                        IModel<String> filterModel = Model.of("");
×
100
                        ItemListPanel.this.get("filterContainer").replaceWith(createFilterContainer(filterModel, ItemListPanel.this));
×
101
                        return new ItemList<T>(markupId, resultListProvider.apply(response), compProvider, getter, filterModel);
×
102
                    }
103
                    return new ItemList<T>(markupId, resultListProvider.apply(response), compProvider, null);
×
104
                }
105
            });
106
        }
107
    }
×
108

109
    private WebMarkupContainer createFilterContainer(IModel<String> filterModel, ItemListPanel<?> panel) {
110
        WebMarkupContainer filterContainer = new WebMarkupContainer("filterContainer");
×
111
        TextField<String> filterField = new TextField<>("filter", filterModel);
×
112
        filterField.setOutputMarkupId(true);
×
113
        filterField.add(new FilterUpdatingBehavior() {
×
114
            @Override
115
            protected void onUpdate(AjaxRequestTarget target) {
116
                target.add(panel);
×
117
            }
×
118
        });
119
        filterContainer.add(filterField);
×
120
        return filterContainer;
×
121
    }
122

123
    public ItemListPanel(String markupId, String title, ReadyFunction readyFunction, ResultFunction<List<T>> resultFunction, ComponentProvider<T> compProvider) {
124
        this(markupId, title, readyFunction, resultFunction, compProvider, null);
×
125
    }
×
126

127
    public ItemListPanel(String markupId, String title, ReadyFunction readyFunction, ResultFunction<List<T>> resultFunction, ComponentProvider<T> compProvider, FilteredListDataProvider.SerializableFunction<T, String> filterTextGetter) {
128
        this(markupId, title);
×
129
        this.readyFunction = readyFunction;
×
130

131
        if (readyFunction.get()) {
×
132
            addFilterAndItemList("itemlist", resultFunction.get(), compProvider, filterTextGetter);
×
133
        } else {
134
            lazyLoading = true;
×
135
            final FilteredListDataProvider.SerializableFunction<T, String> getter = filterTextGetter;
×
136
            add(new MethodResultComponent<List<T>>("itemlist", readyFunction, resultFunction) {
×
137
                @Override
138
                public Component getResultComponent(String markupId, List<T> result) {
139
                    if (getter != null) {
×
140
                        IModel<String> filterModel = Model.of("");
×
141
                        ItemListPanel.this.get("filterContainer").replaceWith(createFilterContainer(filterModel, ItemListPanel.this));
×
142
                        return new ItemList<T>(markupId, resultFunction.get(), compProvider, getter, filterModel);
×
143
                    }
144
                    return new ItemList<T>(markupId, resultFunction.get(), compProvider, null);
×
145
                }
146
            });
147
        }
148
    }
×
149

150
    public ItemListPanel<T> setDescription(String description) {
151
        this.description = description;
×
152
        return this;
×
153
    }
154

155
    public ItemListPanel<T> makeInline() {
156
        add(new AttributeAppender("class", " inline"));
×
157
        return this;
×
158
    }
159

160
    // TODO Improve this (member/admin) button handling:
161
    public ItemListPanel<T> addButton(String label, Class<? extends NanodashPage> pageClass, PageParameters parameters) {
162
        if (parameters == null) parameters = new PageParameters();
×
163
        if (resourceWithProfile != null) parameters.set("context", resourceWithProfile.getId());
×
164
        AbstractLink button = new BookmarkablePageLink<NanodashPage>("button", pageClass, parameters);
×
165
        button.setBody(Model.of(label));
×
166
        buttons.add(button);
×
167
        return this;
×
168
    }
169

170
    public ItemListPanel<T> addMemberButton(String label, Class<? extends NanodashPage> pageClass, PageParameters parameters) {
171
        if (parameters == null) parameters = new PageParameters();
×
172
        if (resourceWithProfile != null) parameters.set("context", resourceWithProfile.getId());
×
173
        AbstractLink button = new BookmarkablePageLink<NanodashPage>("button", pageClass, parameters);
×
174
        button.setBody(Model.of(label));
×
175
        memberButtons.add(button);
×
176
        return this;
×
177
    }
178

179
    public ItemListPanel<T> addAdminButton(String label, Class<? extends NanodashPage> pageClass, PageParameters parameters) {
180
        if (parameters == null) parameters = new PageParameters();
×
181
        if (resourceWithProfile != null) parameters.set("context", resourceWithProfile.getId());
×
182
        AbstractLink button = new BookmarkablePageLink<NanodashPage>("button", pageClass, parameters);
×
183
        button.setBody(Model.of(label));
×
184
        adminButtons.add(button);
×
185
        return this;
×
186
    }
187

188
    public ItemListPanel<T> setResourceWithProfile(AbstractResourceWithProfile resourceWithProfile) {
189
        this.resourceWithProfile = resourceWithProfile;
×
190
        return this;
×
191
    }
192

193
    public ItemListPanel<T> setReadyFunction(ReadyFunction readyFunction) {
194
        this.readyFunction = readyFunction;
×
195
        return this;
×
196
    }
197

198
    @Override
199
    protected void onBeforeRender() {
200
        if (!finalized) {
×
201
            add(new Label("description", description).setVisible(description != null));
×
202
            if (resourceWithProfile != null && readyFunction != null && !readyFunction.get()) {
×
203
                add(new AjaxLazyLoadPanel<Component>("buttons") {
×
204

205
                    @Override
206
                    public Component getLazyLoadComponent(String markupId) {
207
                        return new ButtonList(markupId, resourceWithProfile, buttons, memberButtons, adminButtons);
×
208
                    }
209

210
                    @Override
211
                    protected boolean isContentReady() {
212
                        return readyFunction.get();
×
213
                    }
214

215
                    @Override
216
                    public Component getLoadingComponent(String id) {
217
                        if (lazyLoading) {
×
218
                            return new Label(id).setVisible(false);
×
219
                        } else {
220
                            return new Label(id, ResultComponent.getWaitComponentHtml(null)).setEscapeModelStrings(false);
×
221
                        }
222
                    }
223

224
                });
225
            } else {
226
                add(new ButtonList("buttons", resourceWithProfile, buttons, memberButtons, adminButtons));
×
227
            }
228
            finalized = true;
×
229
        }
230
        super.onBeforeRender();
×
231
    }
×
232

233

234
    public abstract class MethodResultComponent<R> extends ResultComponent {
235

236
        private final Supplier<Boolean> readyFunction;
237
        private final Supplier<R> resultFunction;
238

239
        public MethodResultComponent(String id, ReadyFunction readyFunction, ResultFunction<R> resultFunction) {
×
240
            super(id);
×
241
            setOutputMarkupId(true);
×
242
            this.readyFunction = readyFunction;
×
243
            this.resultFunction = resultFunction;
×
244
        }
×
245

246
        /**
247
         * {@inheritDoc}
248
         */
249
        @Override
250
        protected boolean isContentReady() {
251
            return readyFunction.get();
×
252
        }
253

254
        /**
255
         * {@inheritDoc}
256
         */
257
        @Override
258
        public Component getLazyLoadComponent(String markupId) {
259
            R result = resultFunction.get();
×
260
            return getResultComponent(markupId, result);
×
261
        }
262

263
        public abstract Component getResultComponent(String markupId, R result);
264

265
    }
266

267
    public static interface ComponentProvider<T> extends Function<T, Component>, Serializable {
268
    }
269

270
    public static interface ApiResultListProvider<T> extends Function<ApiResponse, List<T>>, Serializable {
271
    }
272

273
    public static interface ReadyFunction extends Supplier<Boolean>, Serializable {
274
    }
275

276
    public static interface ResultFunction<X> extends Supplier<X>, Serializable {
277
    }
278

279
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc