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

knowledgepixels / nanodash / 35330653258

18 Sep 2026 09:39AM UTC coverage: 48.287% (-0.01%) from 48.297%
35330653258

push

github

web-flow
Merge pull request #715 from knowledgepixels/fix/deactivate-preset-view-display

fix(views): offer "deactivate view display" for preset-supplied views

4807 of 10711 branches covered (44.88%)

Branch coverage included in aggregate %.

8581 of 17015 relevant lines covered (50.43%)

8.09 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/menu/ViewDisplayMenu.java
1
package com.knowledgepixels.nanodash.component.menu;
2

3
import com.knowledgepixels.nanodash.ApiCache;
4
import com.knowledgepixels.nanodash.MagicQueryParams;
5
import com.knowledgepixels.nanodash.NanodashSession;
6
import com.knowledgepixels.nanodash.NanopubElement;
7
import com.knowledgepixels.nanodash.NavigationContext;
8
import com.knowledgepixels.nanodash.QueryResult;
9
import com.knowledgepixels.nanodash.Utils;
10
import com.knowledgepixels.nanodash.View;
11
import com.knowledgepixels.nanodash.ViewDisplay;
12
import com.knowledgepixels.nanodash.component.GuidedChoiceItem;
13
import com.knowledgepixels.nanodash.component.RefreshingResultPanel;
14
import com.knowledgepixels.nanodash.domain.AbstractResourceWithProfile;
15
import com.knowledgepixels.nanodash.domain.IndividualAgent;
16
import com.knowledgepixels.nanodash.domain.Space;
17
import com.knowledgepixels.nanodash.page.ExplorePage;
18
import com.knowledgepixels.nanodash.page.PublishPage;
19
import com.knowledgepixels.nanodash.page.QueryPage;
20
import com.knowledgepixels.nanodash.page.ViewResultsPage;
21
import com.knowledgepixels.nanodash.template.TemplateData;
22
import org.apache.wicket.Component;
23
import org.apache.wicket.ajax.AjaxRequestTarget;
24
import org.apache.wicket.ajax.attributes.AjaxCallListener;
25
import org.apache.wicket.ajax.attributes.AjaxRequestAttributes;
26
import org.apache.wicket.ajax.markup.html.AjaxFallbackLink;
27
import org.apache.wicket.extensions.ajax.markup.html.AjaxLazyLoadPanel;
28
import org.apache.wicket.markup.html.WebMarkupContainer;
29
import org.apache.wicket.markup.html.link.BookmarkablePageLink;
30
import org.apache.wicket.markup.html.link.ExternalLink;
31
import org.apache.wicket.markup.html.link.Link;
32
import org.apache.wicket.markup.repeater.Item;
33
import org.apache.wicket.markup.repeater.data.DataView;
34
import org.apache.wicket.markup.repeater.data.ListDataProvider;
35
import org.apache.wicket.model.Model;
36
import org.apache.wicket.request.mapper.parameter.PageParameters;
37
import org.eclipse.rdf4j.model.IRI;
38
import org.nanopub.extra.services.QueryRef;
39

40
import java.util.List;
41
import java.util.Optional;
42

43
/**
44
 * A dropdown menu panel for view displays, replacing the "^" source link.
45
 * Provides options to show the query, adjust the view display, and see its declaration.
46
 */
47
public class ViewDisplayMenu extends BaseDisplayMenu {
48

49
    /**
50
     * Constructs a ViewDisplayMenu.
51
     *
52
     * @param id           the Wicket component ID
53
     * @param viewDisplay  the view display this menu acts on (must have a non-null nanopub)
54
     * @param queryRef     the query reference used by this view display, or null for a
55
     *                     query-less view (a header view), which hides the
56
     *                     query-dependent entries (show query, full screen). "refresh now"
57
     *                     stays: since issue #654 it brings the view definition up to date
58
     *                     too, which is the whole of what a header view has to refresh.
59
     * @param pageResource the page-level resource used to determine whether "adjust" is visible
60
     * @param viewActions  the view-level actions to show as top entries (may be empty)
61
     */
62
    public ViewDisplayMenu(String id, ViewDisplay viewDisplay, QueryRef queryRef, AbstractResourceWithProfile pageResource, List<QueryResult.MenuAction> viewActions) {
63
        super(id);
×
64

65
        // View-level actions become the top entries of the menu, followed by a
66
        // separator and then the standard view-display options below.
67
        DataView<QueryResult.MenuAction> viewActionView = new DataView<>("viewActions", new ListDataProvider<>(viewActions)) {
×
68
            @Override
69
            protected void populateItem(Item<QueryResult.MenuAction> item) {
70
                QueryResult.MenuAction action = item.getModelObject();
×
71
                BookmarkablePageLink<Void> link = new BookmarkablePageLink<>("viewAction", action.pageClass(), action.params());
×
72
                // A label that starts with a leading symbol/emoji renders that as the entry icon.
73
                String iconBody = Utils.menuEntryIconBodyHtml(action.label());
×
74
                if (iconBody != null) {
×
75
                    link.setBody(Model.of(iconBody)).setEscapeModelStrings(false);
×
76
                } else {
77
                    link.setBody(Model.of(action.label()));
×
78
                }
79
                item.add(link);
×
80
            }
×
81
        };
82
        addEntry("viewActions", viewActionView);
×
83
        WebMarkupContainer separator = new WebMarkupContainer("separator");
×
84
        separator.setVisible(!viewActions.isEmpty());
×
85
        addEntry("separator", separator);
×
86

87
        if (queryRef != null) {
×
88
            PageParameters showQueryParams = new PageParameters().set("id", queryRef.getQueryId());
×
89
            for (var entry : queryRef.getParams().entries()) {
×
90
                showQueryParams.add("queryparam_" + entry.getKey(), entry.getValue());
×
91
            }
×
92
            addEntry("showQuery", new BookmarkablePageLink<Void>("showQuery", QueryPage.class, showQueryParams)
×
93
                    .add(NavigationContext.pageContextFallback()));
×
94
        } else {
×
95
            addEntry("showQuery", new WebMarkupContainer("showQuery").setVisible(false));
×
96
        }
97

98
        addEntry("showView", new BookmarkablePageLink<Void>("showView", ExplorePage.class,
×
99
                new PageParameters().set("id", viewDisplay.getView().getNanopub().getUri()))
×
100
                .add(NavigationContext.pageContextFallback()));
×
101

102
        // Full-screen version of this view on the standalone view-results page, with the
103
        // current query parameters carried along (magic ones excluded — the results page
104
        // re-binds them from the session, so carrying them would duplicate the bindings).
105
        if (queryRef != null) {
×
106
            PageParameters fullScreenParams = new PageParameters().set("view", viewDisplay.getView().getId());
×
107
            for (var entry : queryRef.getParams().entries()) {
×
108
                if (MagicQueryParams.isMagic(entry.getKey())) continue;
×
109
                fullScreenParams.add("queryparam_" + entry.getKey(), entry.getValue());
×
110
            }
×
111
            BookmarkablePageLink<Void> fullScreenLink = new BookmarkablePageLink<>("fullScreen", ViewResultsPage.class, fullScreenParams) {
×
112
                @Override
113
                protected void onConfigure() {
114
                    super.onConfigure();
×
115
                    // Self-referential on the full-screen page itself.
116
                    setVisible(!(getPage() instanceof ViewResultsPage));
×
117
                }
×
118
            };
119
            fullScreenLink.add(NavigationContext.pageContextFallback());
×
120
            addEntry("fullScreen", fullScreenLink);
×
121
        } else {
×
122
            addEntry("fullScreen", new WebMarkupContainer("fullScreen").setVisible(false));
×
123
        }
124

125
        IRI nanopubId = viewDisplay.getNanopubId();
×
126

127
        // Determine whether "adjust" should be visible for this user on this page
128
        boolean showAdjust = false;
×
129
        NanodashSession session = NanodashSession.get();
×
130
        if (pageResource instanceof IndividualAgent ia) {
×
131
            showAdjust = ia.isCurrentUser();
×
132
        } else if (pageResource instanceof Space s) {
×
133
            String pubkeyhash = session.getPubkeyhash();
×
134
            showAdjust = pubkeyhash != null && s.isAdminPubkey(pubkeyhash);
×
135
        } else if (pageResource != null) {
×
136
            Space space = pageResource.getSpace();
×
137
            if (space != null) {
×
138
                String pubkeyhash = session.getPubkeyhash();
×
139
                showAdjust = pubkeyhash != null && space.isAdminPubkey(pubkeyhash);
×
140
            }
141
        }
142

143
        // Determine supersede vs derive based on whether this user's pubkey matches the nanopub's.
144
        // These are only needed when showAdjust is true (i.e. pageResource is non-null).
145
        String adjustUrl = "";
×
146
        String pageResourceId = pageResource != null ? pageResource.getId() : "";
×
147
        if (showAdjust) {
×
148
            String nanopubPubkey = NanopubElement.get(viewDisplay.getNanopub()).getPubkey();
×
149
            String sessionPubkey = session.getPubkeyString();
×
150
            String adjustParam = (nanopubPubkey != null && nanopubPubkey.equals(sessionPubkey))
×
151
                    ? "supersede" : "derive";
×
152
            IRI templateId = TemplateData.get().getTemplateId(viewDisplay.getNanopub());
×
153
            String templateUri = templateId != null ? templateId.stringValue()
×
154
                    : "http://purl.org/np/RACyK2NjqFgezYLiE8FQu7JI0xY1M1aNQbykeCW8oqXkA";
×
155
            adjustUrl = PublishPage.MOUNT_PATH + "?template=" + Utils.urlEncode(templateUri)
×
156
                    + "&" + adjustParam + "=" + Utils.urlEncode(nanopubId.stringValue())
×
157
                    + "&template-version=latest"
158
                    + "&context=" + Utils.urlEncode(pageResourceId);
×
159
        }
160
        // "edit view display" only makes sense for an actual view-display nanopub (one with
161
        // a resolved view IRI). Built-in views rendered directly — e.g. a space's About-tab
162
        // meta-views (roles/members/presets/view-displays) — have no view-display nanopub,
163
        // and neither do views supplied by a preset assignment, so it is hidden for those.
164
        boolean isViewDisplay = viewDisplay.getViewIri() != null;
×
165
        // "deactivate view display" additionally covers preset-supplied views: a deactivation
166
        // nanopub for the view's kind takes precedence over the preset (latest-wins per kind
167
        // in AbstractResourceWithProfile.filterViewDisplays), so it works without a display
168
        // nanopub of its own.
169
        boolean isDeactivatable = isViewDisplay || viewDisplay.isPresetDerived();
×
170
        // Label (with its leading icon) comes from the markup body, so no label arg here.
171
        ExternalLink adjustLink = new ExternalLink("adjust", adjustUrl);
×
172
        adjustLink.setVisible(showAdjust && isViewDisplay);
×
173
        addEntry("adjust", adjustLink);
×
174

175
        BookmarkablePageLink<Void> deactivateLink = new BookmarkablePageLink<>("deactivate", PublishPage.class,
×
176
                new PageParameters()
177
                        .set("template", "https://w3id.org/np/RAZ47_4JquvEXk30HYnVeSgFRcQqHtpdibcfBOeqHI2j4")
×
178
                        .set("template-version", "latest")
×
179
                        .set("param_resource", pageResourceId)
×
180
                        .set("param_view", viewDisplay.getViewIri() != null ? viewDisplay.getViewIri().stringValue() : viewDisplay.getView().getId())
×
181
                        .set("context", pageResourceId)
×
182
                        .set("refresh-upon-publish", pageResourceId));
×
183
        deactivateLink.setVisible(showAdjust && isDeactivatable);
×
184
        addEntry("deactivate", deactivateLink);
×
185

186
        boolean showAddToOwn = session.getUserIri() != null
×
187
                && viewDisplay.getViewIri() != null
×
188
                && pageResource instanceof IndividualAgent ia && !ia.isCurrentUser();
×
189
        String addToOwnUrl = "";
×
190
        if (showAddToOwn) {
×
191
            String userIri = session.getUserIri().stringValue();
×
192
            String viewIri = viewDisplay.getViewIri().stringValue();
×
193
            if (viewDisplay.getView() != null && viewDisplay.getView().getLabel() != null) {
×
194
                GuidedChoiceItem.setLabel(viewIri, viewDisplay.getView().getLabel());
×
195
            }
196
            addToOwnUrl = PublishPage.MOUNT_PATH + "?template=" + Utils.urlEncode("https://w3id.org/np/RAQhTCHtfzGCj1YiE1LualWcZjg3thlRiquFWUE14UF-g")
×
197
                    + "&template-version=latest"
198
                    + "&param_resource=" + Utils.urlEncode(userIri)
×
199
                    + "&param_view=" + Utils.urlEncode(viewIri)
×
200
                    + "&context=" + Utils.urlEncode(userIri)
×
201
                    + "&refresh-upon-publish=" + Utils.urlEncode(userIri)
×
202
                    + "&param_appliesToResource=" + Utils.urlEncode(userIri);
×
203
        }
204
        // Label (with its leading icon) comes from the markup body, so no label arg here.
205
        ExternalLink addToOwnLink = new ExternalLink("addToOwn", addToOwnUrl);
×
206
        addToOwnLink.setVisible(showAddToOwn);
×
207
        addEntry("addToOwn", addToOwnLink);
×
208

209
        // The version of the view definition this display is showing. A newer one can have
210
        // been published since the page was built, which "refresh now" checks for below.
211
        final String shownViewId = viewDisplay.getView() == null ? null : viewDisplay.getView().getId();
×
212

213
        // Refreshes this one view where it stands. Re-rendering the whole page would work too,
214
        // but it takes the reader back to the top of it, away from the view they were looking
215
        // at — and re-runs everything else on the page for a refresh they asked of one view.
216
        AjaxFallbackLink<Void> refreshLink = new AjaxFallbackLink<>("refreshNow") {
×
217
            @Override
218
            public void onClick(Optional<AjaxRequestTarget> target) {
219
                // A header view has no query, and so nothing to mark outdated here — its
220
                // whole content comes from the view definition re-checked just below.
221
                if (queryRef != null) ApiCache.clearCache(queryRef, 0);
×
222
                // Bringing a view up to date is not only a matter of re-running its query:
223
                // the view definition itself can have been superseded since this page was
224
                // built, and neither the memoized resolution nor the version the page's
225
                // structure resolved to would notice on their own (issue #654).
226
                View latestView = shownViewId == null ? null : View.refreshLatestVersion(shownViewId);
×
227
                if (latestView != null && !shownViewId.equals(latestView.getId())) {
×
228
                    // A new version can change everything the display is made of — its query,
229
                    // its columns, its actions, its width — which is more than the piece on
230
                    // screen can be patched into. The version in use comes from the page's
231
                    // structure (the get-view-displays query resolves it server-side), so the
232
                    // structure is what has to be asked again: the same route the page-level
233
                    // "refresh now" takes, with the current structure kept on screen under a
234
                    // spinner until the refreshed one lands.
235
                    AbstractResourceWithProfile r = pageResourceId.isEmpty()
×
236
                            ? null : AbstractResourceWithProfile.get(pageResourceId);
×
237
                    if (r != null) r.forceRefresh(0);
×
238
                    setResponsePage(getPage().getClass(), getPage().getPageParameters());
×
239
                    return;
×
240
                }
241
                QueryResult view = findParent(QueryResult.class);
×
242
                if (view == null && target.isPresent()) {
×
243
                    // Not every view display puts results in the page. A query-form view
244
                    // shows a form, and the results it leads to live on the page it submits
245
                    // to; a header view has no query at all. Either way there is nothing
246
                    // here to bring up to date once the view definition has been re-checked
247
                    // above (and, for the form, its query marked outdated for the next
248
                    // submit). Re-rendering the page on top of that would repaint everything
249
                    // for no visible change — which is what made query-form views, alone
250
                    // among the display types then offering a refresh, flicker on every one.
251
                    return;
×
252
                }
253
                // A view is not always what stands in the page: while it waits for its first
254
                // results it is inside Wicket's lazy-loading panel, and while it is being
255
                // brought up to date inside a RefreshingResultPanel. Either way the wrapper
256
                // is the piece that has to be replaced — replacing what is inside it would
257
                // leave the wrapper around it and address an element the browser does not
258
                // have, so nothing would change on screen and the markup left behind would
259
                // keep linking to components that are no longer there.
260
                Component replaceable = view;
×
261
                while (replaceable != null && "content".equals(replaceable.getId())
×
262
                        && (replaceable.getParent() instanceof RefreshingResultPanel
×
263
                            || replaceable.getParent() instanceof AjaxLazyLoadPanel)) {
×
264
                    replaceable = replaceable.getParent();
×
265
                }
266
                Component rebuilt = (view == null ? null : view.rebuild(replaceable.getId()));
×
267
                if (target.isEmpty() || rebuilt == null || !replaceable.getOutputMarkupId()) {
×
268
                    // No Ajax, nothing to rebuild, or nothing in the page to replace: fall
269
                    // back to re-rendering the page.
270
                    setResponsePage(getPage().getClass(), getPage().getPageParameters());
×
271
                    return;
×
272
                }
273
                // The replacement has to answer to the id the browser already has, or the
274
                // Ajax response would update an element that is not there and leave the old
275
                // markup — and its links, by then removed from the page — on screen.
276
                rebuilt.setMarkupId(replaceable.getMarkupId());
×
277
                replaceable.replaceWith(rebuilt);
×
278
                target.get().add(rebuilt);
×
279
            }
×
280

281
        };
282
        // Offered wherever there is something to bring up to date. That used to mean a query,
283
        // but since issue #654 the refresh re-resolves the view definition as well, which is
284
        // all a query-less header view consists of — so it is offered there too.
285
        refreshLink.setVisible(session.getUserIri() != null && (queryRef != null || shownViewId != null));
×
286
        addEntry("refreshNow", refreshLink);
×
287

288
        BookmarkablePageLink<Void> viewDeclarationLink = new BookmarkablePageLink<>("viewDeclaration", ExplorePage.class,
×
289
                new PageParameters().set("id", nanopubId));
×
290
        viewDeclarationLink.add(NavigationContext.pageContextFallback());
×
291
        viewDeclarationLink.setVisible(viewDisplay.getId() != null);
×
292
        addEntry("viewDeclaration", viewDeclarationLink);
×
293
    }
×
294

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

© 2026 Coveralls, Inc