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

knowledgepixels / nanodash / 28899187612

07 Jul 2026 09:14PM UTC coverage: 28.105% (-0.4%) from 28.465%
28899187612

Pull #543

github

web-flow
Merge 39c9ede0f into f1a7efda7
Pull Request #543: feat: query-form views (gen:QueryFormView) with view-results page

1813 of 7309 branches covered (24.81%)

Branch coverage included in aggregate %.

3717 of 12367 relevant lines covered (30.06%)

4.46 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/QueryResultList.java
1
package com.knowledgepixels.nanodash.component;
2

3
import com.knowledgepixels.nanodash.*;
4
import com.knowledgepixels.nanodash.component.menu.EntryActionMenu;
5
import com.knowledgepixels.nanodash.domain.IndividualAgent;
6
import com.knowledgepixels.nanodash.domain.User;
7
import com.knowledgepixels.nanodash.page.ExplorePage;
8
import com.knowledgepixels.nanodash.page.NanodashPage;
9
import com.knowledgepixels.nanodash.page.PublishPage;
10
import com.knowledgepixels.nanodash.page.QueryPage;
11
import com.knowledgepixels.nanodash.page.UserPage;
12
import org.apache.wicket.Component;
13
import org.apache.wicket.ajax.AjaxRequestTarget;
14
import org.apache.wicket.ajax.markup.html.navigation.paging.AjaxPagingNavigator;
15
import org.apache.wicket.markup.html.form.TextField;
16
import org.apache.wicket.extensions.markup.html.repeater.data.table.NavigatorLabel;
17
import org.apache.wicket.markup.html.WebMarkupContainer;
18
import org.apache.wicket.markup.html.basic.Label;
19
import org.apache.wicket.markup.html.link.AbstractLink;
20
import org.apache.wicket.markup.html.link.BookmarkablePageLink;
21
import org.apache.wicket.markup.html.list.ListItem;
22
import org.apache.wicket.markup.html.list.ListView;
23
import org.apache.wicket.markup.repeater.Item;
24
import org.apache.wicket.markup.repeater.RepeatingView;
25
import org.apache.wicket.markup.repeater.data.DataView;
26
import org.apache.wicket.model.Model;
27
import org.apache.wicket.model.util.ListModel;
28
import org.apache.wicket.request.cycle.RequestCycle;
29
import org.apache.wicket.request.mapper.parameter.PageParameters;
30
import org.apache.wicket.request.resource.ContextRelativeResourceReference;
31
import org.apache.wicket.util.string.Strings;
32
import org.eclipse.rdf4j.model.IRI;
33
import org.nanopub.extra.services.ApiResponse;
34
import org.nanopub.extra.services.ApiResponseEntry;
35
import org.nanopub.extra.services.QueryRef;
36

37
import java.util.ArrayList;
38
import java.util.Collections;
39
import java.util.HashSet;
40
import java.util.List;
41
import java.util.Set;
42

43
/**
44
 * Component for displaying query results in a list format.
45
 */
46
public class QueryResultList extends QueryResult {
47

48
    private static final String SEPARATOR = " · ";
49

50
    private FilteredQueryResultDataProvider filteredDataProvider;
51
    private Model<String> filterModel = Model.of("");
×
52
    private WebMarkupContainer itemsContainer;
53

54
    /**
55
     * Constructor for QueryResultList.
56
     *
57
     * @param markupId    the markup ID
58
     * @param queryRef    the query reference
59
     * @param response    the API response
60
     * @param viewDisplay the view display
61
     */
62
    QueryResultList(String markupId, QueryRef queryRef, ApiResponse response, ViewDisplay viewDisplay) {
63
        super(markupId, queryRef, response, viewDisplay);
×
64

65
        String label = grlcQuery.getLabel();
×
66
        if (viewDisplay.getTitle() != null) {
×
67
            label = viewDisplay.getTitle();
×
68
        }
69
        add(new Label("label", label).setVisible(label != null && !label.isEmpty()));
×
70
        setOutputMarkupId(true);
×
71

72
        TextField<String> filterField = new TextField<>("filter", filterModel);
×
73
        filterField.setOutputMarkupId(true);
×
74
        filterField.add(new FilterUpdatingBehavior() {
×
75
            @Override
76
            protected void onUpdate(AjaxRequestTarget target) {
77
                if (filteredDataProvider != null && itemsContainer != null) {
×
78
                    filteredDataProvider.setFilterText(filterModel.getObject());
×
79
                    target.add(itemsContainer);
×
80
                }
81
            }
×
82
        });
83
        filterField.setVisible(!fitsOnFirstPage());
×
84
        add(filterField);
×
85

86
        populateComponent();
×
87
    }
×
88

89
    @Override
90
    protected void populateComponent() {
91
        QueryResultDataProvider dataProvider = new QueryResultDataProvider(response.getData());
×
92
        filteredDataProvider = new FilteredQueryResultDataProvider(dataProvider, response);
×
93
        DataView<ApiResponseEntry> dataView = new DataView<>("items", filteredDataProvider) {
×
94

95
            @Override
96
            protected void populateItem(Item<ApiResponseEntry> item) {
97
                ApiResponseEntry entry = item.getModelObject();
×
98
                RepeatingView listItem = new RepeatingView("listItem");
×
99

100
                // Columns that only feed action query mappings carry action data, not
101
                // row content — don't render them as visible row text.
102
                View viewForColumns = viewDisplay.getView();
×
103
                Set<String> hiddenColumns = viewForColumns != null
×
104
                        ? viewForColumns.getActionMappingSourceColumns() : Collections.emptySet();
×
105
                List<Component> components = new ArrayList<>();
×
106
                // The row's "^" source link, if any — folded into the per-row actions
107
                // dropdown appended at the end of the row.
108
                String sourceUri = null;
×
109
                for (String key : response.getHeader()) {
×
110
                    if (key.endsWith("_label") || key.endsWith("_label_multi") || hiddenColumns.contains(key)) {
×
111
                        continue;
×
112
                    }
113
                    String entryValue = entry.get(key);
×
114
                    if (entryValue != null && !entryValue.isBlank()) {
×
115
                        if (key.endsWith("user_iri")) {
×
116
                            IRI userIri = Utils.vf.createIRI(entryValue);
×
117
                            IRI profilePicIri = User.getProfilePicture(userIri);
×
118
                            String imgSrc;
119
                            if (profilePicIri != null) {
×
120
                                imgSrc = Strings.escapeMarkup(profilePicIri.stringValue()).toString();
×
121
                            } else if (IndividualAgent.isSoftware(userIri)) {
×
122
                                imgSrc = RequestCycle.get().urlFor(new ContextRelativeResourceReference("images/bot-icon.svg", false), null).toString();
×
123
                            } else {
124
                                imgSrc = RequestCycle.get().urlFor(new ContextRelativeResourceReference("images/user-icon.svg", false), null).toString();
×
125
                            }
126
                            String userLabel = entry.get(key + "_label");
×
127
                            String displayLabel = userLabel != null && !userLabel.isBlank() ? userLabel : User.getShortDisplayName(userIri);
×
128
                            String userUrl = UserPage.MOUNT_PATH + "?id=" + Utils.urlEncode(entryValue);
×
129
                            String linkHtml = "<a href=\"" + Strings.escapeMarkup(userUrl) + "\">" + Strings.escapeMarkup(displayLabel) + "</a>";
×
130
                            components.add(new ComponentSequence("component", " ", List.of(
×
131
                                    new Label("component", "<img class=\"user-icon\" src=\"" + imgSrc + "\" />").setEscapeModelStrings(false),
×
132
                                    new Label("component", linkHtml).setEscapeModelStrings(false))));
×
133
                        } else if (key.endsWith("template_iri")) {
×
134
                            String templateLabel = entry.get(key + "_label");
×
135
                            String displayLabel = templateLabel != null && !templateLabel.isBlank() ? templateLabel : entryValue;
×
136
                            String templateUrl = PublishPage.MOUNT_PATH + "?template=" + Utils.urlEncode(entryValue) + "&template-version=latest" + templateLinkContextParam();
×
137
                            String linkHtml = "<a href=\"" + Strings.escapeMarkup(templateUrl) + "\">" + Strings.escapeMarkup(displayLabel) + "</a>";
×
138
                            components.add(new ComponentSequence("component", " ", List.of(
×
139
                                    new Label("component", "<span class=\"form-icon\"></span>").setEscapeModelStrings(false),
×
140
                                    new Label("component", linkHtml).setEscapeModelStrings(false))));
×
141
                        } else if (key.endsWith("query_iri")) {
×
142
                            String queryLabel = entry.get(key + "_label");
×
143
                            String displayLabel = queryLabel != null && !queryLabel.isBlank() ? queryLabel : entryValue;
×
144
                            String queryUrl = QueryPage.MOUNT_PATH + "?id=" + Utils.urlEncode(entryValue) + templateLinkContextParam();
×
145
                            String linkHtml = "<a href=\"" + Strings.escapeMarkup(queryUrl) + "\">" + Strings.escapeMarkup(displayLabel) + "</a>";
×
146
                            components.add(new Label("component", linkHtml).setEscapeModelStrings(false));
×
147
                        } else if (key.endsWith("_multi_iri")) {
×
148
                            String[] uris = entryValue.split("\\s+");
×
149
                            String labelKey = key.substring(0, key.length() - "_multi_iri".length()) + "_label_multi";
×
150
                            String labelValue = entry.get(labelKey);
×
151
                            String[] labels = labelValue != null ? labelValue.split("\n", -1) : null;
×
152
                            List<Component> links = new ArrayList<>();
×
153
                            for (int i = 0; i < uris.length; i++) {
×
154
                                String uri = uris[i];
×
155
                                if (uri.isBlank()) continue;
×
156
                                String label = (labels != null && i < labels.length && !labels[i].isBlank()) ? Utils.unescapeMultiValue(labels[i]) : null;
×
157
                                // SPARQL coalesce often falls back to the URI string itself; treat that as no label
158
                                // so NanodashLink can derive a short name from the URI.
159
                                if (label != null && label.equals(uri)) label = null;
×
160
                                links.add(new NanodashLink("component", uri, null, null, label, contextId));
×
161
                            }
162
                            components.add(new ComponentSequence("component", ", ", links));
×
163
                        } else if (key.endsWith("_multi_val")) {
×
164
                            String labelKey = key.substring(0, key.length() - "_multi_val".length()) + "_label_multi";
×
165
                            String labelValue = entry.get(labelKey);
×
166
                            String[] parts = entryValue.split("\n", -1);
×
167
                            String[] labels = labelValue != null ? labelValue.split("\n", -1) : null;
×
168
                            List<Component> multiComponents = new ArrayList<>();
×
169
                            for (int i = 0; i < parts.length; i++) {
×
170
                                String part = parts[i];
×
171
                                String label = (labels != null && i < labels.length && !labels[i].isBlank()) ? Utils.unescapeMultiValue(labels[i]) : null;
×
172
                                if (part.matches("https?://.+")) {
×
173
                                    if (label != null && label.equals(part)) label = null;
×
174
                                    multiComponents.add(new NanodashLink("component", part, null, null, label, contextId));
×
175
                                } else {
176
                                    String unescaped = Utils.unescapeMultiValue(part);
×
177
                                    if (label == null && Utils.isDateTimeLiteral(unescaped)) {
×
178
                                        // Friendly relative time, matching single-value items.
179
                                        multiComponents.add(new Label("component", Utils.friendlyDateHtml(unescaped, unescaped)).setEscapeModelStrings(false));
×
180
                                    } else {
181
                                        String display = label != null ? label : unescaped;
×
182
                                        boolean isHtml = Utils.looksLikeHtml(display);
×
183
                                        if (isHtml) {
×
184
                                            display = withContextInHtmlLinks(Utils.sanitizeHtml(display));
×
185
                                        }
186
                                        multiComponents.add(new Label("component", display).setEscapeModelStrings(!isHtml));
×
187
                                    }
188
                                }
189
                            }
190
                            components.add(new ComponentSequence("component", ", ", multiComponents));
×
191
                        } else if (key.endsWith("_multi")) {
×
192
                            String[] parts = entryValue.split("\n", -1);
×
193
                            String labelKey = key.substring(0, key.length() - "_multi".length()) + "_label_multi";
×
194
                            String labelValue = entry.get(labelKey);
×
195
                            String[] labels = labelValue != null ? labelValue.split("\n", -1) : null;
×
196
                            List<Component> multiComponents = new ArrayList<>();
×
197
                            for (int i = 0; i < parts.length; i++) {
×
198
                                boolean hasLabel = labels != null && i < labels.length && !labels[i].isBlank();
×
199
                                String display = hasLabel ? Utils.unescapeMultiValue(labels[i]) : Utils.unescapeMultiValue(parts[i]);
×
200
                                if (!hasLabel && Utils.isDateTimeLiteral(display)) {
×
201
                                    // Friendly relative time, matching single-value items.
202
                                    multiComponents.add(new Label("component", Utils.friendlyDateHtml(display, display)).setEscapeModelStrings(false));
×
203
                                } else {
204
                                    multiComponents.add(new Label("component", display));
×
205
                                }
206
                            }
207
                            components.add(new ComponentSequence("component", ", ", multiComponents));
×
208
                        } else if (entryValue.matches("https?://.+")) {
×
209
                            String entryLabel = entry.get(key + "_label");
×
210
                            if ("^".equals(entryLabel)) {
×
211
                                // Folded into the per-row actions dropdown appended below.
212
                                sourceUri = entryValue;
×
213
                            } else {
214
                                components.add(new NanodashLink("component", entryValue, null, null, entryLabel, contextId));
×
215
                            }
216
                        } else {
×
217
                            String entryLabel = entry.get(key + "_label");
×
218
                            boolean hasLabel = entryLabel != null && !entryLabel.isBlank() && !entryLabel.equals(entryValue);
×
219
                            if (!hasLabel && Utils.isDateTimeLiteral(entryValue)) {
×
220
                                // Show a friendly relative time (client-side); raw ISO value stays as no-script fallback.
221
                                components.add(new Label("component", Utils.friendlyDateHtml(entryValue, entryValue)).setEscapeModelStrings(false));
×
222
                            } else {
223
                                String display = hasLabel ? entryLabel : entryValue;
×
224
                                if (Utils.looksLikeHtml(display)) {
×
225
                                    display = withContextInHtmlLinks(Utils.sanitizeHtml(display));
×
226
                                } else if (hasLabel) {
×
227
                                    display = Strings.escapeMarkup(display).toString();
×
228
                                }
229
                                if (hasLabel) {
×
230
                                    // Separate display label for a (non-IRI) literal value; the full
231
                                    // literal is shown on hover via the standard styled tooltip.
232
                                    display = "<span class=\"tooltip\"><span class=\"tooltiptext tooltiptext-auto\">" + Strings.escapeMarkup(entryValue) + "</span>" + display + "</span>";
×
233
                                }
234
                                components.add(new Label("component", display).setEscapeModelStrings(false));
×
235
                            }
236
                        }
237
                    }
238
                }
239
                List<AbstractLink> actionLinks = ViewActionMappings.buildEntryActionLinks(viewDisplay.getView(), entry,
×
240
                        queryRef, resourceWithProfile, contextId, partId, refRoot, postPublishTab);
×
241
                // The former "^" source link joins the same dropdown, as a "source" entry.
242
                if (sourceUri != null) {
×
243
                    AbstractLink sourceLink = new BookmarkablePageLink<NanodashPage>("link", ExplorePage.class,
×
244
                            new PageParameters().set("id", sourceUri));
×
245
                    sourceLink.add(NavigationContext.pageContextFallback());
×
246
                    sourceLink.setBody(Model.of("<span class=\"actionmenu-icon\">↗︎</span>source")).setEscapeModelStrings(false);
×
247
                    actionLinks.add(sourceLink);
×
248
                }
249
                // Append the per-row dropdown (entry actions + source) as the trailing item;
250
                // it hugs the preceding content with a plain space, not the list separator.
251
                Set<Integer> spaceBeforeMenu = new HashSet<>();
×
252
                if (!actionLinks.isEmpty()) {
×
253
                    spaceBeforeMenu.add(components.size());
×
254
                    components.add(new EntryActionMenu("component", actionLinks));
×
255
                }
256
                ComponentSequence componentSequence = new ComponentSequence(listItem.newChildId(), SEPARATOR, components, spaceBeforeMenu);
×
257
                listItem.add(componentSequence);
×
258
                item.add(listItem);
×
259
            }
×
260
        };
261
        dataView.setItemsPerPage(viewDisplay.getPageSize());
×
262

263
        WebMarkupContainer navigation = new WebMarkupContainer("navigation");
×
264
        navigation.add(new NavigatorLabel("navigatorLabel", dataView));
×
265
        AjaxPagingNavigator pagingNavigator = new AjaxPagingNavigator("navigator", dataView);
×
266
        navigation.setVisible(dataView.getPageCount() > 1);
×
267
        navigation.add(pagingNavigator);
×
268

269
        // Hidden when the empty-actions line below shows instead, which carries
270
        // its own "Nothing here yet:" text; this note still covers the case of
271
        // the filter text matching no row.
272
        Label noRecordsLabel = new Label("no-records", "(nothing found)") {
×
273
            @Override
274
            protected void onConfigure() {
275
                super.onConfigure();
×
276
                setVisible(filteredDataProvider.size() == 0 && !hasEmptyStateActions());
×
277
            }
×
278
        };
279

280
        // When the result is genuinely empty (not merely filtered down to zero
281
        // rows), the view-level actions are promoted from the dropdown menu to
282
        // visible buttons in the empty state — same as in QueryResultTable.
283
        WebMarkupContainer emptyActions = new WebMarkupContainer("empty-actions") {
×
284
            @Override
285
            protected void onConfigure() {
286
                super.onConfigure();
×
287
                setVisible(hasEmptyStateActions());
×
288
            }
×
289
        };
290
        // menuActions is filled by the builder after construction, so the list
291
        // is wrapped as a live model rather than copied here.
292
        emptyActions.add(new ListView<MenuAction>("actions", new ListModel<>(menuActions)) {
×
293
            @Override
294
            protected void populateItem(ListItem<MenuAction> item) {
295
                MenuAction action = item.getModelObject();
×
296
                AbstractLink link = new BookmarkablePageLink<NanodashPage>("link", action.pageClass(), action.params());
×
297
                link.setBody(Model.of(action.label()));
×
298
                item.add(link);
×
299
            }
×
300
        });
301

302
        itemsContainer = new WebMarkupContainer("items-container");
×
303
        itemsContainer.setOutputMarkupId(true);
×
304
        itemsContainer.add(dataView);
×
305
        itemsContainer.add(noRecordsLabel);
×
306
        itemsContainer.add(emptyActions);
×
307
        itemsContainer.add(navigation);
×
308
        add(itemsContainer);
×
309
    }
×
310

311
}
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