• 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

17.24
src/main/java/com/knowledgepixels/nanodash/component/ViewActionMappings.java
1
package com.knowledgepixels.nanodash.component;
2

3
import com.knowledgepixels.nanodash.QueryResult;
4
import com.knowledgepixels.nanodash.SpaceMemberRole;
5
import com.knowledgepixels.nanodash.Utils;
6
import com.knowledgepixels.nanodash.View;
7
import com.knowledgepixels.nanodash.ViewDisplay;
8
import com.knowledgepixels.nanodash.domain.AbstractResourceWithProfile;
9
import com.knowledgepixels.nanodash.domain.MaintainedResource;
10
import com.knowledgepixels.nanodash.domain.Space;
11
import com.knowledgepixels.nanodash.page.NanodashPage;
12
import com.knowledgepixels.nanodash.page.PublishPage;
13
import com.knowledgepixels.nanodash.repository.MaintainedResourceRepository;
14
import com.knowledgepixels.nanodash.template.Template;
15
import org.apache.wicket.markup.html.link.AbstractLink;
16
import org.apache.wicket.markup.html.link.BookmarkablePageLink;
17
import org.apache.wicket.model.Model;
18
import org.apache.wicket.request.mapper.parameter.PageParameters;
19
import org.eclipse.rdf4j.model.IRI;
20
import org.nanopub.extra.services.ApiResponseEntry;
21
import org.nanopub.extra.services.QueryRef;
22

23
import java.util.ArrayList;
24
import java.util.List;
25

26
/**
27
 * Applies a view entry action's per-row query mappings and decides whether the
28
 * action's button should render for that row. See docs/magic-query-params.md
29
 * (phase 2: empty-into-required visibility + multiple mappings / non-{@code param_}
30
 * targets).
31
 */
32
class ViewActionMappings {
33

34
    private ViewActionMappings() {
35
    }
36

37
    /**
38
     * Writes the action's mapped values into the link parameters and returns whether
39
     * the button should be shown for this row.
40
     *
41
     * <p>Each mapping is {@code "col:target"}: the row's value for result column
42
     * {@code col} is written to URL parameter {@code param_target}, or — when
43
     * {@code target} starts with {@code @} — to the raw URL key {@code target}
44
     * (fill-mode keys such as {@code @derive-a} / {@code @supersede}). The button is
45
     * <b>hidden</b> (returns false) if any <i>required</i> mapped value is empty: a
46
     * raw key is always required; a {@code param_} target is required unless its
47
     * template placeholder is optional. Empty values for optional placeholders are
48
     * simply skipped (button kept).</p>
49
     *
50
     * @param view      the view declaring the action
51
     * @param actionIri the action node IRI
52
     * @param row       the result row
53
     * @param params    the link parameters to populate
54
     * @return true if the action button should be rendered for this row
55
     */
56
    /**
57
     * Adds a button to the result component for each result action declared by the view,
58
     * linking to the action's template on the publish page. Resource-context parameters
59
     * (the target field, the context, the part, and the part field) are only set when the
60
     * corresponding id/contextId is available, so this also works on resource-less pages
61
     * such as the general Spaces page or the standalone view-results page.
62
     *
63
     * @param result              the result component to add the action buttons to
64
     * @param viewDisplay         the view display whose view declares the actions
65
     * @param queryRef            the query reference backing the component (used for refresh and query mapping)
66
     * @param id                  the resource id, or null if there is no specific resource in context
67
     * @param contextId           the context id, or null if there is no context
68
     * @param resourceWithProfile the resource whose page the component is on, or null
69
     * @param refRoot             the pinned ref's root nanopub, or null
70
     */
71
    static void addResultActions(QueryResult result, ViewDisplay viewDisplay, QueryRef queryRef, String id, String contextId, AbstractResourceWithProfile resourceWithProfile, String refRoot) {
72
        View view = viewDisplay.getView();
×
73
        if (view == null) return;
×
74
        for (IRI actionIri : view.getViewResultActionList()) {
×
75
            // Per-action role gating (docs/role-specific-views.md): skip an action
76
            // whose gen:isVisibleTo the current viewer does not satisfy. Additive —
77
            // actions without gen:isVisibleTo are unaffected. Gated against the pinned
78
            // ref's authority on a ?root=-pinned page. See docs/space-ref-identity.md.
79
            if (!SpaceMemberRole.isViewerEntitled(view.getActionVisibleTo(actionIri), resourceWithProfile, refRoot)) continue;
×
80
            Template t = view.getTemplateForAction(actionIri);
×
81
            if (t == null) continue;
×
82
            String targetField = view.getTemplateTargetFieldForAction(actionIri);
×
83
            if (targetField == null) targetField = "resource";
×
84
            String label = view.getLabelForAction(actionIri);
×
85
            if (label == null) label = "action...";
×
86
            if (!label.endsWith("...")) label += "...";
×
87
            PageParameters params = new PageParameters().set("template", t.getId())
×
88
                    .set("template-version", "latest");
×
89
            if (id != null) params.set("param_" + targetField, id);
×
90
            if (contextId != null) params.set("context", contextId);
×
91
            if (id != null && contextId != null && !id.equals(contextId)) {
×
92
                params.set("part", id);
×
93
            }
94
            String partField = view.getTemplatePartFieldForAction(actionIri);
×
95
            if (partField != null && contextId != null) {
×
96
                // The part field pre-fills a namespaced child IRI (the user fills the suffix).
97
                // TODO Find a better way to pass the MaintainedResource object to this method:
98
                MaintainedResource r = MaintainedResourceRepository.get().findById(contextId);
×
99
                String namespace = null;
×
100
                if (r != null) {
×
101
                    namespace = r.getNamespace();
×
102
                } else if (resourceWithProfile instanceof Space) {
×
103
                    // The Space-creation templates' `space` placeholder has a fixed
104
                    // `https://w3id.org/spaces/` prefix, so the pre-fill is relative to it.
105
                    // Nesting the new space's IRI under this space's path makes it a
106
                    // sub-space via the prefix match.
107
                    namespace = contextId.replaceFirst("https://w3id.org/spaces/", "") + "/";
×
108
                }
109
                if (namespace != null) {
×
110
                    params.set("param_" + partField, namespace + "<SET-SUFFIX>");
×
111
                }
112
            }
113
            String queryMapping = view.getTemplateQueryMapping(actionIri);
×
114
            if (queryMapping != null && queryMapping.contains(":")) {
×
115
                params.set("values-from-query", queryRef.getAsUrlString());
×
116
                params.set("values-from-query-mapping", queryMapping);
×
117
            }
118
            params.set("refresh-upon-publish", queryRef.getAsUrlString());
×
119
            if (result.getPostPublishTab() != null) params.set("postpub-tab", result.getPostPublishTab());
×
120
            result.addButton(label, PublishPage.class, params);
×
121
        }
×
122
    }
×
123

124
    /**
125
     * Builds the entry-action links of a view for one result row: one publish-page link
126
     * per {@code gen:ViewEntryAction} the viewer is entitled to and whose query mappings
127
     * are satisfied by the row (see {@link #applyEntryMappings}). Each link uses the
128
     * markup id {@code "link"}, ready for an {@link com.knowledgepixels.nanodash.component.menu.EntryActionMenu}.
129
     *
130
     * @param view                the view declaring the actions, or null for none
131
     * @param row                 the result row the actions apply to
132
     * @param queryRef            the query reference backing the component (used for refresh and query mapping)
133
     * @param entitlementResource the resource the viewer's entitlement is checked against, or null
134
     * @param contextId           the context id, or null if there is no context
135
     * @param partId              the part id when shown on a part page, or null
136
     * @param refRoot             the pinned ref's root nanopub, or null
137
     * @param postPublishTab      the tab to return to after publishing, or null for the default
138
     * @return the entry-action links for this row (never null)
139
     */
140
    static List<AbstractLink> buildEntryActionLinks(View view, ApiResponseEntry row, QueryRef queryRef,
141
            AbstractResourceWithProfile entitlementResource, String contextId, String partId, String refRoot, String postPublishTab) {
142
        List<AbstractLink> links = new ArrayList<>();
×
143
        if (view == null) return links;
×
144
        for (IRI actionIri : view.getViewEntryActionList()) {
×
145
            // Per-action role gating (docs/role-specific-views.md): skip an action
146
            // whose gen:isVisibleTo the viewer does not satisfy.
147
            if (!SpaceMemberRole.isViewerEntitled(view.getActionVisibleTo(actionIri), entitlementResource, refRoot)) continue;
×
148
            Template t = view.getTemplateForAction(actionIri);
×
149
            if (t == null) continue;
×
150
            String targetField = view.getTemplateTargetFieldForAction(actionIri);
×
151
            if (targetField == null) targetField = "resource";
×
152
            String label = view.getLabelForAction(actionIri);
×
153
            if (label == null) label = "action...";
×
154
            if (!label.endsWith("...")) label += "...";
×
155
            PageParameters params = new PageParameters().set("template", t.getId())
×
156
                    .set("param_" + targetField, contextId)
×
157
                    .set("context", contextId)
×
158
                    .set("template-version", "latest");
×
159
            if (partId != null && contextId != null && !partId.equals(contextId)) {
×
160
                params.set("part", partId);
×
161
            }
162
            String partField = view.getTemplatePartFieldForAction(actionIri);
×
163
            if (partField != null) {
×
164
                // The part field pre-fills a namespaced child IRI (the user fills the suffix).
165
                // TODO Find a better way to pass the MaintainedResource object to this method:
166
                MaintainedResource r = MaintainedResourceRepository.get().findById(contextId);
×
167
                if (r != null && r.getNamespace() != null) {
×
168
                    params.set("param_" + partField, r.getNamespace() + "<SET-SUFFIX>");
×
169
                }
170
            }
171
            // Apply the action's query mappings; hide the button for this row
172
            // if any required mapped value is empty (docs/magic-query-params.md).
173
            if (!applyEntryMappings(view, actionIri, row, params)) {
×
174
                continue;
×
175
            }
176
            params.set("refresh-upon-publish", queryRef.getAsUrlString());
×
177
            if (postPublishTab != null) params.set("postpub-tab", postPublishTab);
×
178
            AbstractLink button = new BookmarkablePageLink<NanodashPage>("link", PublishPage.class, params);
×
179
            // A label that starts with a leading symbol/emoji renders that as the entry icon.
180
            String iconBody = Utils.menuEntryIconBodyHtml(label);
×
181
            if (iconBody != null) {
×
182
                button.setBody(Model.of(iconBody)).setEscapeModelStrings(false);
×
183
            } else {
184
                button.setBody(Model.of(label));
×
185
            }
186
            links.add(button);
×
187
        }
×
188
        return links;
×
189
    }
190

191
    static boolean applyEntryMappings(View view, IRI actionIri, ApiResponseEntry row, PageParameters params) {
192
        Template template = view.getTemplateForAction(actionIri);
12✔
193
        for (String mapping : view.getTemplateQueryMappings(actionIri)) {
36✔
194
            int sep = mapping.indexOf(':');
12✔
195
            if (sep < 0) continue;
6!
196
            String col = mapping.substring(0, sep);
15✔
197
            String target = mapping.substring(sep + 1);
18✔
198
            boolean rawKey = target.startsWith("@");
12✔
199
            String key = rawKey ? target.substring(1) : target;
24✔
200
            String value = row.get(col);
12✔
201
            if (value == null || value.isBlank()) {
15✔
202
                // Empty: hide the action only if the target is required.
203
                if (rawKey || template == null || template.isRequiredField(key)) return false;
30!
204
                continue;
205
            }
206
            params.set(rawKey ? key : "param_" + key, value);
30✔
207
        }
3✔
208
        return true;
6✔
209
    }
210

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