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

knowledgepixels / nanodash / 27622310436

16 Jun 2026 01:49PM UTC coverage: 26.963% (+6.3%) from 20.697%
27622310436

Pull #483

github

web-flow
Merge dbba567c9 into 663f14f46
Pull Request #483: Space/resource About pages, ref-aware spaces, and magic query params

1542 of 6717 branches covered (22.96%)

Branch coverage included in aggregate %.

3407 of 11638 relevant lines covered (29.27%)

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

40
import java.util.ArrayList;
41
import java.util.Collections;
42
import java.util.HashSet;
43
import java.util.List;
44
import java.util.Set;
45

46
/**
47
 * Component for displaying query results in a list format.
48
 */
49
public class QueryResultList extends QueryResult {
50

51
    private static final String SEPARATOR = " · ";
52

53
    private FilteredQueryResultDataProvider filteredDataProvider;
54
    private Model<String> filterModel = Model.of("");
×
55
    private WebMarkupContainer itemsContainer;
56

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

68
        String label = grlcQuery.getLabel();
×
69
        if (viewDisplay.getTitle() != null) {
×
70
            label = viewDisplay.getTitle();
×
71
        }
72
        add(new Label("label", label));
×
73
        setOutputMarkupId(true);
×
74

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

89
        populateComponent();
×
90
    }
×
91

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

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

103
                // Columns that only feed action query mappings carry action data, not
104
                // row content — don't render them as visible row text.
105
                View viewForColumns = viewDisplay.getView();
×
106
                Set<String> hiddenColumns = viewForColumns != null
×
107
                        ? viewForColumns.getActionMappingSourceColumns() : Collections.emptySet();
×
108
                List<Component> components = new ArrayList<>();
×
109
                // The row's "^" source link, if any — folded into the per-row actions
110
                // dropdown appended at the end of the row.
111
                String sourceUri = null;
×
112
                for (String key : response.getHeader()) {
×
113
                    if (key.endsWith("_label") || key.endsWith("_label_multi") || hiddenColumns.contains(key)) {
×
114
                        continue;
×
115
                    }
116
                    String entryValue = entry.get(key);
×
117
                    if (entryValue != null && !entryValue.isBlank()) {
×
118
                        if (key.endsWith("user_iri")) {
×
119
                            IRI userIri = Utils.vf.createIRI(entryValue);
×
120
                            IRI profilePicIri = User.getProfilePicture(userIri);
×
121
                            String imgSrc;
122
                            if (profilePicIri != null) {
×
123
                                imgSrc = Strings.escapeMarkup(profilePicIri.stringValue()).toString();
×
124
                            } else if (IndividualAgent.isSoftware(userIri)) {
×
125
                                imgSrc = RequestCycle.get().urlFor(new ContextRelativeResourceReference("images/bot-icon.svg", false), null).toString();
×
126
                            } else {
127
                                imgSrc = RequestCycle.get().urlFor(new ContextRelativeResourceReference("images/user-icon.svg", false), null).toString();
×
128
                            }
129
                            String userLabel = entry.get(key + "_label");
×
130
                            String displayLabel = userLabel != null && !userLabel.isBlank() ? userLabel : User.getShortDisplayName(userIri);
×
131
                            String userUrl = UserPage.MOUNT_PATH + "?id=" + Utils.urlEncode(entryValue);
×
132
                            String linkHtml = "<a href=\"" + Strings.escapeMarkup(userUrl) + "\">" + Strings.escapeMarkup(displayLabel) + "</a>";
×
133
                            components.add(new ComponentSequence("component", " ", List.of(
×
134
                                    new Label("component", "<img class=\"user-icon\" src=\"" + imgSrc + "\" />").setEscapeModelStrings(false),
×
135
                                    new Label("component", linkHtml).setEscapeModelStrings(false))));
×
136
                        } else if (key.endsWith("template_iri")) {
×
137
                            String templateLabel = entry.get(key + "_label");
×
138
                            String displayLabel = templateLabel != null && !templateLabel.isBlank() ? templateLabel : entryValue;
×
139
                            String templateUrl = PublishPage.MOUNT_PATH + "?template=" + Utils.urlEncode(entryValue) + "&template-version=latest";
×
140
                            String linkHtml = "<a href=\"" + Strings.escapeMarkup(templateUrl) + "\">" + Strings.escapeMarkup(displayLabel) + "</a>";
×
141
                            components.add(new ComponentSequence("component", " ", List.of(
×
142
                                    new Label("component", "<span class=\"form-icon\"></span>").setEscapeModelStrings(false),
×
143
                                    new Label("component", linkHtml).setEscapeModelStrings(false))));
×
144
                        } else if (key.endsWith("query_iri")) {
×
145
                            String queryLabel = entry.get(key + "_label");
×
146
                            String displayLabel = queryLabel != null && !queryLabel.isBlank() ? queryLabel : entryValue;
×
147
                            String queryUrl = QueryPage.MOUNT_PATH + "?id=" + Utils.urlEncode(entryValue);
×
148
                            String linkHtml = "<a href=\"" + Strings.escapeMarkup(queryUrl) + "\">" + Strings.escapeMarkup(displayLabel) + "</a>";
×
149
                            components.add(new Label("component", linkHtml).setEscapeModelStrings(false));
×
150
                        } else if (key.endsWith("_multi_iri")) {
×
151
                            String[] uris = entryValue.split("\\s+");
×
152
                            String labelKey = key.substring(0, key.length() - "_multi_iri".length()) + "_label_multi";
×
153
                            String labelValue = entry.get(labelKey);
×
154
                            String[] labels = labelValue != null ? labelValue.split("\n", -1) : null;
×
155
                            List<Component> links = new ArrayList<>();
×
156
                            for (int i = 0; i < uris.length; i++) {
×
157
                                String uri = uris[i];
×
158
                                if (uri.isBlank()) continue;
×
159
                                String label = (labels != null && i < labels.length && !labels[i].isBlank()) ? Utils.unescapeMultiValue(labels[i]) : null;
×
160
                                // SPARQL coalesce often falls back to the URI string itself; treat that as no label
161
                                // so NanodashLink can derive a short name from the URI.
162
                                if (label != null && label.equals(uri)) label = null;
×
163
                                links.add(new NanodashLink("component", uri, null, null, label, contextId));
×
164
                            }
165
                            components.add(new ComponentSequence("component", ", ", links));
×
166
                        } else if (key.endsWith("_multi_val")) {
×
167
                            String labelKey = key.substring(0, key.length() - "_multi_val".length()) + "_label_multi";
×
168
                            String labelValue = entry.get(labelKey);
×
169
                            String[] parts = entryValue.split("\n", -1);
×
170
                            String[] labels = labelValue != null ? labelValue.split("\n", -1) : null;
×
171
                            List<Component> multiComponents = new ArrayList<>();
×
172
                            for (int i = 0; i < parts.length; i++) {
×
173
                                String part = parts[i];
×
174
                                String label = (labels != null && i < labels.length && !labels[i].isBlank()) ? Utils.unescapeMultiValue(labels[i]) : null;
×
175
                                if (part.matches("https?://.+")) {
×
176
                                    if (label != null && label.equals(part)) label = null;
×
177
                                    multiComponents.add(new NanodashLink("component", part, null, null, label, contextId));
×
178
                                } else {
179
                                    String display = label != null ? label : Utils.unescapeMultiValue(part);
×
180
                                    boolean isHtml = Utils.looksLikeHtml(display);
×
181
                                    if (isHtml) {
×
182
                                        display = Utils.sanitizeHtml(display);
×
183
                                    }
184
                                    multiComponents.add(new Label("component", display).setEscapeModelStrings(!isHtml));
×
185
                                }
186
                            }
187
                            components.add(new ComponentSequence("component", ", ", multiComponents));
×
188
                        } else if (key.endsWith("_multi")) {
×
189
                            String[] parts = entryValue.split("\n", -1);
×
190
                            String labelKey = key.substring(0, key.length() - "_multi".length()) + "_label_multi";
×
191
                            String labelValue = entry.get(labelKey);
×
192
                            String[] labels = labelValue != null ? labelValue.split("\n", -1) : null;
×
193
                            List<Component> multiComponents = new ArrayList<>();
×
194
                            for (int i = 0; i < parts.length; i++) {
×
195
                                String display;
196
                                if (labels != null && i < labels.length && !labels[i].isBlank()) {
×
197
                                    display = Utils.unescapeMultiValue(labels[i]);
×
198
                                } else {
199
                                    display = Utils.unescapeMultiValue(parts[i]);
×
200
                                }
201
                                multiComponents.add(new Label("component", display));
×
202
                            }
203
                            components.add(new ComponentSequence("component", ", ", multiComponents));
×
204
                        } else if (entryValue.matches("https?://.+")) {
×
205
                            String entryLabel = entry.get(key + "_label");
×
206
                            if ("^".equals(entryLabel)) {
×
207
                                // Folded into the per-row actions dropdown appended below.
208
                                sourceUri = entryValue;
×
209
                            } else {
210
                                components.add(new NanodashLink("component", entryValue, null, null, entryLabel, contextId));
×
211
                            }
212
                        } else {
×
213
                            if (Utils.looksLikeHtml(entryValue)) {
×
214
                                entryValue = Utils.sanitizeHtml(entryValue);
×
215
                            }
216
                            components.add(new Label("component", entryValue).setEscapeModelStrings(false));
×
217
                        }
218
                    }
219
                }
220
                View view = viewDisplay.getView();
×
221
                List<AbstractLink> actionLinks = new ArrayList<>();
×
222
                if (view != null) {
×
223
                    for (IRI actionIri : view.getViewEntryActionList()) {
×
224
                        // Per-action role gating (docs/role-specific-views.md): skip an
225
                        // action whose gen:isVisibleTo the viewer does not satisfy.
226
                        if (!SpaceMemberRole.isViewerEntitled(view.getActionVisibleTo(actionIri), resourceWithProfile)) continue;
×
227
                        // TODO Copied code and adjusted from QueryResultTableBuilder:
228
                        Template t = view.getTemplateForAction(actionIri);
×
229
                        if (t == null) continue;
×
230
                        String targetField = view.getTemplateTargetFieldForAction(actionIri);
×
231
                        if (targetField == null) targetField = "resource";
×
232
                        String labelForAction = view.getLabelForAction(actionIri);
×
233
                        if (labelForAction == null) labelForAction = "action...";
×
234
                        if (!labelForAction.endsWith("...")) labelForAction += "...";
×
235
                        PageParameters params = new PageParameters().set("template", t.getId())
×
236
                                .set("param_" + targetField, contextId)
×
237
                                .set("context", contextId)
×
238
                                .set("template-version", "latest");
×
239
                        String partField = view.getTemplatePartFieldForAction(actionIri);
×
240
                        if (partField != null) {
×
241
                            // TODO Find a better way to pass the MaintainedResource object to this method:
242
                            MaintainedResource r = MaintainedResourceRepository.get().findById(contextId);
×
243
                            if (r != null && r.getNamespace() != null) {
×
244
                                params.set("param_" + partField, r.getNamespace() + "<SET-SUFFIX>");
×
245
                            }
246
                        }
247
                        // Apply the action's query mappings; hide the button for this row
248
                        // if any required mapped value is empty (docs/magic-query-params.md).
249
                        if (!ViewActionMappings.applyEntryMappings(view, actionIri, entry, params)) {
×
250
                            continue;
×
251
                        }
252
                        params.set("refresh-upon-publish", queryRef.getAsUrlString());
×
253
                        if (postPublishTab != null) params.set("postpub-tab", postPublishTab);
×
254
                        AbstractLink button = new BookmarkablePageLink<NanodashPage>("link", PublishPage.class, params);
×
255
                        // A label that starts with a leading symbol/emoji renders that as the entry icon.
256
                        String iconBody = Utils.menuEntryIconBodyHtml(labelForAction);
×
257
                        if (iconBody != null) {
×
258
                            button.setBody(Model.of(iconBody)).setEscapeModelStrings(false);
×
259
                        } else {
260
                            button.setBody(Model.of(labelForAction));
×
261
                        }
262
                        actionLinks.add(button);
×
263
                    }
×
264
                }
265
                // The former "^" source link joins the same dropdown, as a "source" entry.
266
                if (sourceUri != null) {
×
267
                    AbstractLink sourceLink = new BookmarkablePageLink<NanodashPage>("link", ExplorePage.class,
×
268
                            new PageParameters().set("id", sourceUri));
×
269
                    sourceLink.setBody(Model.of("<span class=\"actionmenu-icon\">↗︎</span>source")).setEscapeModelStrings(false);
×
270
                    actionLinks.add(sourceLink);
×
271
                }
272
                // Append the per-row dropdown (entry actions + source) as the trailing item;
273
                // it hugs the preceding content with a plain space, not the list separator.
274
                Set<Integer> spaceBeforeMenu = new HashSet<>();
×
275
                if (!actionLinks.isEmpty()) {
×
276
                    spaceBeforeMenu.add(components.size());
×
277
                    components.add(new EntryActionMenu("component", actionLinks));
×
278
                }
279
                ComponentSequence componentSequence = new ComponentSequence(listItem.newChildId(), SEPARATOR, components, spaceBeforeMenu);
×
280
                listItem.add(componentSequence);
×
281
                item.add(listItem);
×
282
            }
×
283
        };
284
        dataView.setItemsPerPage(viewDisplay.getPageSize());
×
285

286
        WebMarkupContainer navigation = new WebMarkupContainer("navigation");
×
287
        navigation.add(new NavigatorLabel("navigatorLabel", dataView));
×
288
        AjaxPagingNavigator pagingNavigator = new AjaxPagingNavigator("navigator", dataView);
×
289
        navigation.setVisible(dataView.getPageCount() > 1);
×
290
        navigation.add(pagingNavigator);
×
291

292
        // Hidden when the empty-actions line below shows instead, which carries
293
        // its own "Nothing here yet:" text; this note still covers the case of
294
        // the filter text matching no row.
295
        Label noRecordsLabel = new Label("no-records", "(nothing found)") {
×
296
            @Override
297
            protected void onConfigure() {
298
                super.onConfigure();
×
299
                setVisible(filteredDataProvider.size() == 0 && !hasEmptyStateActions());
×
300
            }
×
301
        };
302

303
        // When the result is genuinely empty (not merely filtered down to zero
304
        // rows), the view-level actions are promoted from the dropdown menu to
305
        // visible buttons in the empty state — same as in QueryResultTable.
306
        WebMarkupContainer emptyActions = new WebMarkupContainer("empty-actions") {
×
307
            @Override
308
            protected void onConfigure() {
309
                super.onConfigure();
×
310
                setVisible(hasEmptyStateActions());
×
311
            }
×
312
        };
313
        // menuActions is filled by the builder after construction, so the list
314
        // is wrapped as a live model rather than copied here.
315
        emptyActions.add(new ListView<MenuAction>("actions", new ListModel<>(menuActions)) {
×
316
            @Override
317
            protected void populateItem(ListItem<MenuAction> item) {
318
                MenuAction action = item.getModelObject();
×
319
                AbstractLink link = new BookmarkablePageLink<NanodashPage>("link", action.pageClass(), action.params());
×
320
                link.setBody(Model.of(action.label()));
×
321
                item.add(link);
×
322
            }
×
323
        });
324

325
        itemsContainer = new WebMarkupContainer("items-container");
×
326
        itemsContainer.setOutputMarkupId(true);
×
327
        itemsContainer.add(dataView);
×
328
        itemsContainer.add(noRecordsLabel);
×
329
        itemsContainer.add(emptyActions);
×
330
        itemsContainer.add(navigation);
×
331
        add(itemsContainer);
×
332
    }
×
333

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