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

knowledgepixels / nanodash / 28789064412

06 Jul 2026 11:44AM UTC coverage: 28.51% (+0.008%) from 28.502%
28789064412

push

github

web-flow
Merge pull request #535 from knowledgepixels/feat/navigation-context

feat: persistent navigation context with back-link and post-publish forwarding

1811 of 7177 branches covered (25.23%)

Branch coverage included in aggregate %.

3710 of 12188 relevant lines covered (30.44%)

4.52 hits per line

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

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

3
import com.knowledgepixels.nanodash.component.menu.ViewDisplayMenu;
4
import com.knowledgepixels.nanodash.domain.AbstractResourceWithProfile;
5
import com.knowledgepixels.nanodash.page.NanodashPage;
6
import org.apache.wicket.markup.html.basic.Label;
7
import org.apache.wicket.markup.html.panel.Panel;
8
import org.apache.wicket.request.mapper.parameter.PageParameters;
9
import org.nanopub.extra.services.ApiResponse;
10
import org.nanopub.extra.services.QueryRef;
11

12
import java.io.Serializable;
13
import java.util.ArrayList;
14
import java.util.List;
15
import java.util.regex.Matcher;
16
import java.util.regex.Pattern;
17

18
/**
19
 * Abstract base class for displaying query results in different formats.
20
 */
21
public abstract class QueryResult extends Panel {
22

23
    /**
24
     * A view-level action, shown as a top entry of the view's dropdown menu.
25
     */
26
    public record MenuAction(String label, Class<? extends NanodashPage> pageClass, PageParameters params) implements Serializable {
×
27
    }
28

29
    protected final List<MenuAction> menuActions = new ArrayList<>();
×
30
    protected String contextId;
31
    protected String partId;
32
    protected String postPublishTab;
33
    // The ref (root definition) this view is pinned to (?root=), used to scope per-entry
34
    // action visibility to the claimant being viewed. Null = the resource's representative
35
    // ref. See docs/space-ref-identity.md.
36
    protected String refRoot;
37
    protected boolean finalized = false;
×
38
    protected final QueryRef queryRef;
39
    protected final ViewDisplay viewDisplay;
40
    protected final ApiResponse response;
41
    protected AbstractResourceWithProfile resourceWithProfile;
42
    protected AbstractResourceWithProfile pageResource;
43
    protected boolean showViewDisplayMenu = true;
×
44
    protected final GrlcQuery grlcQuery;
45

46
    /**
47
     * Constructor for QueryResult.
48
     *
49
     * @param markupId    the markup ID
50
     * @param queryRef    the query reference
51
     * @param response    the API response
52
     * @param viewDisplay the view display
53
     */
54
    public QueryResult(String markupId, QueryRef queryRef, ApiResponse response, ViewDisplay viewDisplay) {
55
        super(markupId);
×
56
        this.queryRef = queryRef;
×
57
        this.viewDisplay = viewDisplay;
×
58
        this.response = response;
×
59
        this.grlcQuery = GrlcQuery.get(queryRef);
×
60
    }
×
61

62
    @Override
63
    protected void onBeforeRender() {
64
        if (!finalized) {
×
65
            // View-level actions used to render as a button strip in the header here;
66
            // they now live as the top entries of the view's dropdown menu instead.
67
            add(new Label("buttons").setVisible(false));
×
68
            if (showViewDisplayMenu) {
×
69
                if (viewDisplay.getNanopubId() != null || !menuActions.isEmpty()) {
×
70
                    add(new ViewDisplayMenu("np", viewDisplay, queryRef, pageResource, menuActions));
×
71
                } else {
72
                    add(new Label("np").setVisible(false));
×
73
                }
74
            }
75
            finalized = true;
×
76
        }
77
        super.onBeforeRender();
×
78
    }
×
79

80
    /**
81
     * The view-level actions to render as top entries of the view's dropdown menu.
82
     *
83
     * @return the collected view-level menu actions
84
     */
85
    public List<MenuAction> getMenuActions() {
86
        return menuActions;
×
87
    }
88

89
    /**
90
     * Set the resource with profile for this component.
91
     *
92
     * @param resourceWithProfile The resource with profile to set.
93
     */
94
    public void setResourceWithProfile(AbstractResourceWithProfile resourceWithProfile) {
95
        this.resourceWithProfile = resourceWithProfile;
×
96
    }
×
97

98
    public void setPageResource(AbstractResourceWithProfile pageResource) {
99
        this.pageResource = pageResource;
×
100
    }
×
101

102
    /**
103
     * Set the context ID for this component.
104
     *
105
     * @param contextId The context ID to set.
106
     */
107
    public void setContextId(String contextId) {
108
        this.contextId = contextId;
×
109
    }
×
110

111
    /**
112
     * Set the part ID when this view is shown on a part page (e.g. paper collection).
113
     * Used for redirect-after-publish to return to the part page.
114
     *
115
     * @param partId The part ID to set, or null when on the main context page.
116
     */
117
    public void setPartId(String partId) {
118
        this.partId = partId;
×
119
    }
×
120

121
    /**
122
     * Set the tab to return to after publishing one of this view's action
123
     * buttons (e.g. {@code "about"} so a space's About-tab views send the user
124
     * back to About instead of the default Content tab). Null leaves the
125
     * post-publish redirect on its default tab.
126
     *
127
     * @param postPublishTab the tab name, or null for the default
128
     */
129
    public void setPostPublishTab(String postPublishTab) {
130
        this.postPublishTab = postPublishTab;
×
131
    }
×
132

133
    /**
134
     * Set the ref (root definition) this view is pinned to, so per-entry action visibility
135
     * is gated against that claimant's authority rather than the resource's representative
136
     * ref. Null = representative ref. See docs/space-ref-identity.md.
137
     *
138
     * @param refRoot the ref's root nanopub, or null
139
     */
140
    public void setRefRoot(String refRoot) {
141
        this.refRoot = refRoot;
×
142
    }
×
143

144
    /**
145
     * @return the tab to return to after publishing via an action button, or null for the default
146
     */
147
    public String getPostPublishTab() {
148
        return postPublishTab;
×
149
    }
150

151
    // A view-level action button; collected here and rendered as a top entry of the
152
    // view's dropdown menu (see ViewDisplayMenu).
153
    public void addButton(String label, Class<? extends NanodashPage> pageClass, PageParameters parameters) {
154
        if (parameters == null) {
×
155
            parameters = new PageParameters();
×
156
        }
157
        if (contextId != null) {
×
158
            parameters.set("context", contextId);
×
159
        }
160
        menuActions.add(new MenuAction(label, pageClass, parameters));
×
161
    }
×
162

163
    /**
164
     * The navigation context to stamp on links in result cells: this view's context
165
     * resource if bound to one, else the page's navigation context. Only usable at
166
     * render time (needs the page).
167
     *
168
     * @return the context id, or null if neither is set
169
     */
170
    private String renderContextId() {
171
        if (contextId != null) return contextId;
×
172
        if (getPage() instanceof NanodashPage nanodashPage) return nanodashPage.getContextId();
×
173
        return null;
×
174
    }
175

176
    /**
177
     * The {@code &context=...} URL suffix for template/query links in result cells.
178
     * Empty string when no context is set. Only usable at render time (needs the page).
179
     *
180
     * @return the context URL parameter suffix, possibly empty
181
     */
182
    protected String templateLinkContextParam() {
183
        String ctx = renderContextId();
×
184
        return ctx == null ? "" : "&context=" + Utils.urlEncode(ctx);
×
185
    }
186

187
    private static final Pattern INTERNAL_HREF_PATTERN = Pattern.compile("href=\"(/[^\"]*)\"");
×
188

189
    /**
190
     * Appends the navigation context to app-internal links ({@code href="/..."}) inside
191
     * sanitized result-cell HTML, so ready-made links coming from the query data itself
192
     * (e.g. template or query links emitted by the SPARQL) also lead back to the
193
     * current context. Links already carrying a context are left alone.
194
     *
195
     * @param sanitizedHtml the sanitized cell HTML, or null
196
     * @return the HTML with context-enriched internal links
197
     */
198
    protected String withContextInHtmlLinks(String sanitizedHtml) {
199
        String ctx = renderContextId();
×
200
        if (ctx == null || sanitizedHtml == null) return sanitizedHtml;
×
201
        String encodedCtx = Utils.urlEncode(ctx);
×
202
        Matcher m = INTERNAL_HREF_PATTERN.matcher(sanitizedHtml);
×
203
        StringBuilder sb = new StringBuilder();
×
204
        while (m.find()) {
×
205
            String url = m.group(1);
×
206
            String replacement = m.group();
×
207
            // The sanitizer escapes "=" as "&#61;", so check both spellings.
208
            if (!url.contains("context=") && !url.contains("context&#61;")) {
×
209
                String separator = url.contains("?") ? "&amp;" : "?";
×
210
                replacement = "href=\"" + url + separator + "context=" + encodedCtx + "\"";
×
211
            }
212
            m.appendReplacement(sb, Matcher.quoteReplacement(replacement));
×
213
        }
×
214
        m.appendTail(sb);
×
215
        return sb.toString();
×
216
    }
217

218
    /**
219
     * Whether all result rows fit on the first page, so no pagination is needed
220
     * and the filter textfield can be hidden. Also true when the page size is
221
     * unlimited ({@code < 1}).
222
     *
223
     * @return true if all entries fit on the first page
224
     */
225
    protected boolean fitsOnFirstPage() {
226
        int pageSize = viewDisplay.getPageSize();
×
227
        return pageSize < 1 || response.getData().size() <= pageSize;
×
228
    }
229

230
    /**
231
     * Whether the empty state should point the viewer to the view-level actions:
232
     * the underlying response (not just a filtered view of it) has no rows, and
233
     * there is at least one action the viewer is entitled to.
234
     *
235
     * @return true if the empty-state call-to-action buttons should show
236
     */
237
    protected boolean hasEmptyStateActions() {
238
        return response.getData().isEmpty() && !menuActions.isEmpty();
×
239
    }
240

241
    /**
242
     * Populate the component with the query results.
243
     */
244
    protected abstract void populateComponent();
245

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