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

knowledgepixels / nanodash / 27741419211

18 Jun 2026 06:35AM UTC coverage: 26.602% (-0.4%) from 26.963%
27741419211

Pull #484

github

web-flow
Merge eb7ba5ef8 into 0f6281554
Pull Request #484: Space-ref disambiguation: conflict notice, claimants overview, ref-pinned pages

1552 of 6853 branches covered (22.65%)

Branch coverage included in aggregate %.

3418 of 11830 relevant lines covered (28.89%)

4.25 hits per line

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

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

3
import com.knowledgepixels.nanodash.NanodashPageRef;
4
import com.knowledgepixels.nanodash.component.*;
5
import com.knowledgepixels.nanodash.domain.AbstractResourceWithProfile;
6
import com.knowledgepixels.nanodash.domain.MaintainedResource;
7
import com.knowledgepixels.nanodash.domain.Space;
8
import com.knowledgepixels.nanodash.repository.MaintainedResourceRepository;
9
import com.knowledgepixels.nanodash.repository.SpaceRepository;
10
import org.apache.wicket.Component;
11
import org.apache.wicket.RestartResponseException;
12
import org.apache.wicket.extensions.ajax.markup.html.AjaxLazyLoadPanel;
13
import org.apache.wicket.markup.html.WebMarkupContainer;
14
import org.apache.wicket.markup.html.basic.Label;
15
import org.apache.wicket.markup.html.panel.EmptyPanel;
16
import org.apache.wicket.model.IModel;
17
import org.apache.wicket.model.LoadableDetachableModel;
18
import org.apache.wicket.model.Model;
19
import org.apache.wicket.request.mapper.parameter.PageParameters;
20

21
import java.util.List;
22

23
/**
24
 * The SpacePage class represents a space page in the Nanodash application.
25
 */
26
public class SpacePage extends NanodashPage {
27

28
    /**
29
     * The mount path for this page.
30
     */
31
    public static final String MOUNT_PATH = "/space";
32

33
    /**
34
     * {@inheritDoc}
35
     */
36
    @Override
37
    public String getMountPath() {
38
        return MOUNT_PATH;
×
39
    }
40

41
    /**
42
     * Id of the space shown on this page. Only the id is held in the page
43
     * state; the {@link Space} itself is re-fetched from the repository on
44
     * every render via {@link #spaceModel}, so the page tree never carries
45
     * a serialized snapshot of singleton data.
46
     */
47
    private final String spaceId;
48

49
    /**
50
     * LDM that resolves {@link #spaceId} to the live {@link Space} singleton.
51
     */
52
    private final IModel<Space> spaceModel;
53

54
    /**
55
     * Constructor for the SpacePage.
56
     *
57
     * @param parameters the page parameters
58
     */
59
    public SpacePage(final PageParameters parameters) {
60
        super(parameters);
×
61

62
        Space space = resolveSpace(parameters);
×
63
        spaceId = space.getId();
×
64
        spaceModel = new LoadableDetachableModel<Space>() {
×
65
            @Override
66
            protected Space load() {
67
                return SpaceRepository.get().findById(spaceId);
×
68
            }
69
        };
70
        space.triggerDataUpdate();
×
71

72
        ResourceTabs.Tab activeTab = ResourceTabs.activeFromParam(parameters);
×
73

74
        // Optional ?root=<NPID> pins the page to a specific ref (one of the IRI's claimants);
75
        // validated against the known refs. null = the representative (default) ref. Carried
76
        // across tab switches so the pinned ref survives navigation.
77
        String rootParam = parameters.get("root").toString(null);
×
78
        final String effectiveRoot = (rootParam != null && !rootParam.isEmpty()
×
79
                && space.getRefRoots().contains(rootParam)) ? rootParam : null;
×
80

81
        List<AbstractResourceWithProfile> superSpaces = space.getAllSuperSpacesUntilRoot();
×
82
        if (superSpaces.isEmpty()) {
×
83
            // Top-level space (no superspace): show only the tab strip, no breadcrumb.
84
            add(new TitleBar("titlebar", this, null)
×
85
                    .setTabs(new ResourceTabs("tabs", "space", space.getId(), null, activeTab, effectiveRoot)));
×
86
        } else {
87
            superSpaces.add(space);
×
88
            add(new TitleBar("titlebar", this, null,
×
89
                    superSpaces.stream().map(ss -> new NanodashPageRef(SpacePage.class, new PageParameters().add("id", ss.getId()), ss.getLabel())).toArray(NanodashPageRef[]::new)
×
90
            ).setTabs(new ResourceTabs("tabs", "space", space.getId(), null, activeTab, effectiveRoot)));
×
91
        }
92

93
        add(new Label("pagetitle", space.getLabel() + " (space) | nanodash"));
×
94
        add(new Label("spacename", space.getLabel()));
×
95
        add(new Label("titlesuffix", ResourceTabs.titleSuffix(activeTab)));
×
96
        add(new ExternalLinkWithActionsPanel("id", Model.of(space.getId()), Model.of(space.getLabel())));
×
97

98
        // Disambiguation notice: shown when viewing a specific claimant (ref pinned) or the
99
        // default view of an identifier with conflicting claimants. It's a collapsible
100
        // <details>: the summary carries the text, expanding it reveals the full claimants
101
        // table inline (no separate page). See docs/space-ref-identity.md.
102
        String bannerText = "";
×
103
        if (effectiveRoot != null) {
×
104
            bannerText = "Viewing 1 of " + space.getRefCount()
×
105
                    + " definitions (" + shortNp(effectiveRoot) + "). ";
×
106
        } else if (space.hasConflictingRefs()) {
×
107
            bannerText = "⚠ " + space.getRefCount()
×
108
                    + " definitions claim this identifier, with different admins; showing the default. ";
109
        }
110
        boolean showNotice = !bannerText.isEmpty();
×
111
        WebMarkupContainer refConflictNotice = new WebMarkupContainer("ref-conflict");
×
112
        refConflictNotice.setVisible(showNotice);
×
113
        refConflictNotice.add(new Label("ref-conflict-text", bannerText));
×
114
        refConflictNotice.add(showNotice
×
115
                ? new SpaceClaimantsPanel("claimants-panel", space, effectiveRoot)
×
116
                : new EmptyPanel("claimants-panel"));
×
117
        add(refConflictNotice);
×
118

119
        WebMarkupContainer contentContainer = new WebMarkupContainer("contentContainer");
×
120
        add(contentContainer);
×
121
        if (activeTab != ResourceTabs.Tab.CONTENT) {
×
122
            contentContainer.setVisible(false);
×
123
            if (activeTab == ResourceTabs.Tab.ABOUT) {
×
124
                // The panel constructor resolves view nanopubs over the network when
125
                // they aren't freshly cached, which would block the initial page
126
                // render; the view-id list must mirror the panel's View.get calls.
127
                add(LazyContentPanel.of("otherTab", markupId -> new AboutSpacePanel(markupId, spaceModel.getObject(), effectiveRoot),
×
128
                        AboutSpacePanel.SPACE_INFO_VIEW, AboutSpacePanel.PRESET_ASSIGNMENTS_VIEW, AboutSpacePanel.SPACE_ROLES_VIEW, AboutSpacePanel.VIEW_DISPLAYS_VIEW,
129
                        AboutSpacePanel.MEMBERS_VIEW, AboutSpacePanel.OBSERVERS_VIEW));
130
            } else if (activeTab == ResourceTabs.Tab.EXPLORE) {
×
131
                add(LazyContentPanel.of("otherTab", markupId -> new ExplorePanel(markupId, spaceId),
×
132
                        ReferencesPage.REFERENCES_VIEW));
133
            } else {
134
                add(new DownloadRdfLinks("otherTab", "space", space.getId()));
×
135
            }
136
            return;
×
137
        }
138
        add(new EmptyPanel("otherTab").setVisible(false));
×
139

140
        if (effectiveRoot != null) {
×
141
            // Pinned to a specific claimant: render that ref's view displays, not the
142
            // representative ref's (the IRI-keyed singleton). Fetched on demand. See
143
            // docs/space-ref-identity.md.
144
            contentContainer.add(new ViewList("views", space, space.getTopLevelViewDisplays(effectiveRoot), effectiveRoot));
×
145
        } else if (space.isDataInitialized()) {
×
146
            contentContainer.add(new ViewList("views", space));
×
147
        } else {
148
            contentContainer.add(new AjaxLazyLoadPanel<Component>("views") {
×
149

150
                @Override
151
                public Component getLazyLoadComponent(String markupId) {
152
                    return new ViewList(markupId, spaceModel.getObject());
×
153
                }
154

155
                @Override
156
                protected boolean isContentReady() {
157
                    return spaceModel.getObject().isDataInitialized();
×
158
                }
159

160
                @Override
161
                public Component getLoadingComponent(String id) {
162
                    return new Label(id, "<div class=\"row-section\"><div class=\"col-12\">" + ResultComponent.getWaitIconHtml() + "</div></div>").setEscapeModelStrings(false);
×
163
                }
164

165
            });
166
        }
167

168
    }
×
169

170
    /**
171
     * Checks if auto-refresh is enabled for this page.
172
     *
173
     * @return true if auto-refresh is enabled, false otherwise
174
     */
175
    protected boolean hasAutoRefreshEnabled() {
176
        return true;
×
177
    }
178

179
    /** A short, human-scannable form of a nanopub IRI (its artifact-code prefix). */
180
    private static String shortNp(String npIri) {
181
        int i = Math.max(npIri.lastIndexOf('/'), npIri.lastIndexOf('#'));
×
182
        String code = i < 0 ? npIri : npIri.substring(i + 1);
×
183
        return code.length() > 16 ? code.substring(0, 16) + "…" : code;
×
184
    }
185

186
    /**
187
     * {@inheritDoc}
188
     */
189
    @Override
190
    protected void onDetach() {
191
        spaceModel.detach();
×
192
        super.onDetach();
×
193
    }
×
194

195
    /**
196
     * Resolves the {@link Space} from the repository, or redirects as needed.
197
     *
198
     * @param parameters page parameters containing the space {@code id}
199
     * @return the resolved {@link Space}; never {@code null}
200
     * @throws RestartResponseException if the id belongs to a {@link MaintainedResource} or to a part within one
201
     * @throws IllegalArgumentException if the id cannot be resolved to any known resource
202
     */
203
    private Space resolveSpace(PageParameters parameters) {
204
        String id = parameters.get("id").toString();
×
205
        Space resolved = SpaceRepository.get().findById(id);
×
206
        if (resolved == null) {
×
207
            if (MaintainedResourceRepository.get().findById(id) != null) {
×
208
                throw new RestartResponseException(MaintainedResourcePage.class, parameters);
×
209
            }
210
            MaintainedResource containingResource = MaintainedResourceRepository.get().findByNamespace(MaintainedResource.getNamespace(id));
×
211
            if (containingResource != null) {
×
212
                PageParameters partParameters = new PageParameters(parameters);
×
213
                partParameters.set("context", containingResource.getId());
×
214
                throw new RestartResponseException(ResourcePartPage.class, partParameters);
×
215
            }
216
            throw new IllegalArgumentException("No space or resource found for id: " + id);
×
217
        }
218

219
        return resolved;
×
220
    }
221

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