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

knowledgepixels / nanodash / 27145358627

08 Jun 2026 02:39PM UTC coverage: 20.682% (-0.3%) from 20.947%
27145358627

push

github

web-flow
Merge pull request #479 from knowledgepixels/feat/about-pages-478

Resource-page tabs, presets, and role-gated view actions (#478, #302)

1052 of 6429 branches covered (16.36%)

Branch coverage included in aggregate %.

2642 of 11432 relevant lines covered (23.11%)

3.31 hits per line

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

26.36
src/main/java/com/knowledgepixels/nanodash/component/TitleBar.java
1
package com.knowledgepixels.nanodash.component;
2

3
import java.io.Serializable;
4
import java.util.ArrayList;
5
import java.util.List;
6

7
import org.apache.wicket.behavior.AttributeAppender;
8
import org.apache.wicket.markup.html.WebMarkupContainer;
9
import org.apache.wicket.markup.html.WebPage;
10
import org.apache.wicket.markup.html.link.BookmarkablePageLink;
11
import org.apache.wicket.markup.html.panel.EmptyPanel;
12
import org.apache.wicket.markup.html.panel.Panel;
13
import org.apache.wicket.markup.repeater.Item;
14
import org.apache.wicket.markup.repeater.data.DataView;
15
import org.apache.wicket.markup.repeater.data.ListDataProvider;
16

17
import com.knowledgepixels.nanodash.NanodashPageRef;
18
import com.knowledgepixels.nanodash.NanodashPreferences;
19
import com.knowledgepixels.nanodash.page.NanodashPage;
20
import com.knowledgepixels.nanodash.page.PublishPage;
21
import com.knowledgepixels.nanodash.page.QueryListPage;
22
import com.knowledgepixels.nanodash.page.SpaceListPage;
23
import com.knowledgepixels.nanodash.page.UserListPage;
24

25
/**
26
 * TitleBar is the top bar of the Nanodash application, which contains
27
 * navigation elements such as profile, my channel, users, connectors,
28
 * publish, query, and breadcrumb navigation.
29
 */
30
public class TitleBar extends Panel {
31

32
    private String highlight;
33

34
    /**
35
     * The breadcrumb/tab strip row (the grey band just below the nav bar). Holds
36
     * the breadcrumb links on the left and, optionally, the resource tab strip on
37
     * the right (see {@link #setTabs(ResourceTabs)}).
38
     */
39
    private WebMarkupContainer breadcrumbPath;
40

41
    /**
42
     * Constructs a TitleBar with the specified id, page, highlight element,
43
     * and an array of path references for breadcrumb navigation.
44
     *
45
     * @param id        the component id
46
     * @param page      the current Nanodash page
47
     * @param highlight the id of the element to highlight
48
     * @param pathRefs  an array of NanodashPageRef for breadcrumb navigation
49
     */
50
    public TitleBar(String id, NanodashPage page, String highlight, NanodashPageRef... pathRefs) {
51
        super(id);
9✔
52
        this.highlight = highlight;
9✔
53
        add(new ProfileItem("profile", page));
39✔
54

55
        createNavLink("users", UserListPage.class);
15✔
56
        createNavLink("connectors", SpaceListPage.class);
15✔
57
        createNavLink("publish", PublishPage.class).setVisible(!NanodashPreferences.get().isReadOnlyMode());
33!
58
        createNavLink("query", QueryListPage.class);
15✔
59

60
        breadcrumbPath = new WebMarkupContainer("breadcrumbpath");
18✔
61
        WebMarkupContainer breadcrumbLinks = new WebMarkupContainer("breadcrumblinks");
15✔
62
        List<CrumbPart> crumbParts = buildCrumbParts(pathRefs);
9✔
63
        if (!crumbParts.isEmpty()) {
9!
64
            CrumbPart first = crumbParts.get(0);
×
65
            breadcrumbLinks.add(first.ref().createComponent("firstpathelement", first.label()));
×
66
            // Getting serialization exception if not using 'new ArrayList<...>(...)' here:
67
            List<CrumbPart> moreParts = new ArrayList<CrumbPart>(crumbParts.subList(1, crumbParts.size()));
×
68
            breadcrumbLinks.add(new DataView<CrumbPart>("morepathelements", new ListDataProvider<CrumbPart>(moreParts)) {
×
69

70
                @Override
71
                protected void populateItem(Item<CrumbPart> item) {
72
                    CrumbPart part = item.getModelObject();
×
73
                    item.add(part.ref().createComponent("furtherpathelement", part.label()));
×
74
                }
×
75

76
            });
77
        } else {
×
78
            breadcrumbLinks.setVisible(false);
12✔
79
        }
80
        breadcrumbPath.add(breadcrumbLinks);
30✔
81
        // Tab strip placeholder (right side of the strip); replaced via setTabs().
82
        breadcrumbPath.add(new EmptyPanel("tabs").setVisible(false));
45✔
83
        // The strip shows when there is a breadcrumb to display; setTabs() also
84
        // forces it visible so a tab strip shows even without a breadcrumb.
85
        breadcrumbPath.setVisible(pathRefs.length > 0);
24!
86
        add(breadcrumbPath);
30✔
87
    }
3✔
88

89
    /**
90
     * One breadcrumb segment: the {@link NanodashPageRef} it links to and the
91
     * (possibly split) label text to show for it.
92
     */
93
    public record CrumbPart(NanodashPageRef ref, String label) implements Serializable {
×
94
    }
95

96
    /**
97
     * Flattens a breadcrumb path into the segments to render. Two transforms are
98
     * applied to each ref's label:
99
     * <ul>
100
     *   <li>For each non-root crumb, the part it shares with its parent label
101
     *       is stripped (e.g. parent "Knowledge Pixels", child "Knowledge
102
     *       Pixels Incubator" renders as "Incubator"). The shared part is the
103
     *       longest common character prefix, but only stripped when it covers
104
     *       (almost) all of the parent and ends on a non-letter/digit boundary
105
     *       in the child — this also catches singular/plural variations like
106
     *       parent "Nano Sessions", child "Nano Session #30" → "#30".</li>
107
     *   <li>The remaining label is split on list/title punctuation ({@code ,},
108
     *       {@code :}, {@code ;}, {@code |}, and spaced {@code -}) into separate
109
     *       breadcrumb segments — e.g. "General Nanopub Ecosystem Ontology,
110
     *       version 0.4 (incomplete)" renders as two crumbs "General Nanopub
111
     *       Ecosystem Ontology" › "version 0.4 (incomplete)". All segments of a
112
     *       label link to the same page.</li>
113
     * </ul>
114
     * The parent-prefix comparison uses the original (un-simplified) labels.
115
     */
116
    static List<CrumbPart> buildCrumbParts(NanodashPageRef[] pathRefs) {
117
        List<CrumbPart> parts = new ArrayList<>();
12✔
118
        for (int i = 0; i < pathRefs.length; i++) {
18!
119
            String label = pathRefs[i].getLabel();
×
120
            if (label == null) {
×
121
                parts.add(new CrumbPart(pathRefs[i], null));
×
122
                continue;
×
123
            }
124
            if (i > 0) {
×
125
                label = stripParentPrefix(pathRefs[i - 1].getLabel(), label);
×
126
            }
127
            for (String segment : splitLabel(label)) {
×
128
                parts.add(new CrumbPart(pathRefs[i], segment));
×
129
            }
×
130
        }
131
        return parts;
6✔
132
    }
133

134
    /**
135
     * Splits a label on list/title separators — comma, colon, semicolon, pipe
136
     * (with or without surrounding spaces), or a space-surrounded hyphen — into
137
     * trimmed, non-empty segments. Returns the trimmed whole label if there is
138
     * no separator (so the result always has at least one element).
139
     */
140
    static List<String> splitLabel(String label) {
141
        List<String> segments = new ArrayList<>();
×
142
        if (label == null) return segments;
×
143
        for (String s : label.split("\\s*[,;:]\\s+|\\s*\\|\\s*|\\s+-\\s+")) {
×
144
            String trimmed = s.trim();
×
145
            if (!trimmed.isEmpty()) segments.add(trimmed);
×
146
        }
147
        if (segments.isEmpty()) {
×
148
            String trimmed = label.trim();
×
149
            if (!trimmed.isEmpty()) segments.add(trimmed);
×
150
        }
151
        return segments;
×
152
    }
153

154
    /**
155
     * If the child label appears to restate the parent's "topic" at the start,
156
     * strips that shared prefix and returns the remainder. Otherwise returns
157
     * the child label unchanged.
158
     */
159
    private static String stripParentPrefix(String parent, String child) {
160
        if (parent == null || parent.isEmpty() || child == null) return child;
×
161
        int lcp = 0;
×
162
        int max = Math.min(parent.length(), child.length());
×
163
        while (lcp < max && parent.charAt(lcp) == child.charAt(lcp)) lcp++;
×
164
        // Require a substantial match: at least 3 chars, and within 2 chars of
165
        // the full parent length (so things like "Sessions" vs "Session" still
166
        // match, but "Nanopublication Sessions" vs "Nano Session #30" doesn't).
167
        if (lcp < 3 || lcp < parent.length() - 2) return child;
×
168
        // The boundary in the child must not fall mid-word, otherwise we'd be
169
        // chopping off a partial word like "Foo Bar" -> "Foo Baz Quux" -> "z Quux".
170
        if (lcp >= child.length() || Character.isLetterOrDigit(child.charAt(lcp))) return child;
×
171
        String remainder = child.substring(lcp).replaceAll("^\\s+", "");
×
172
        if (remainder.isEmpty()) return child;
×
173
        return remainder;
×
174
    }
175

176
    /**
177
     * Places a resource tab strip (Content | About | Raw) on the right side of
178
     * the breadcrumb strip, and forces the strip visible so the tabs show even on
179
     * pages without a breadcrumb (e.g. user pages). Returns {@code this} for
180
     * fluent use at the {@code add(...)} call site.
181
     *
182
     * @param tabs the tab strip (its markup id must be {@code "tabs"})
183
     * @return this TitleBar
184
     */
185
    public TitleBar setTabs(ResourceTabs tabs) {
186
        breadcrumbPath.addOrReplace(tabs);
×
187
        breadcrumbPath.setVisible(true);
×
188
        return this;
×
189
    }
190

191
    private BookmarkablePageLink<Void> createNavLink(String id, Class<? extends WebPage> pageClass) {
192
        BookmarkablePageLink<Void> link = new BookmarkablePageLink<>(id, pageClass);
18✔
193
        if (id.equals(highlight)) {
15!
194
            link.add(new AttributeAppender("class", "selected"));
×
195
        }
196
        add(link);
27✔
197
        return link;
6✔
198
    }
199

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