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

knowledgepixels / nanodash / 37889468619

09 Oct 2026 05:37AM UTC coverage: 51.491% (+0.08%) from 51.411%
37889468619

Pull #771

github

web-flow
Merge 62f065e74 into 108b57d49
Pull Request #771: Group result-table rows under header rows via a "_group" column

5554 of 11589 branches covered (47.92%)

Branch coverage included in aggregate %.

9829 of 18286 relevant lines covered (53.75%)

8.72 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/QueryResultTable.java
1
package com.knowledgepixels.nanodash.component;
2

3
import com.knowledgepixels.nanodash.*;
4
import com.knowledgepixels.nanodash.component.menu.EntryActionMenu;
5
import com.knowledgepixels.nanodash.page.ExplorePage;
6
import com.knowledgepixels.nanodash.page.NanodashPage;
7
import com.knowledgepixels.nanodash.page.PublishPage;
8
import org.apache.wicket.Component;
9
import org.apache.wicket.ajax.AjaxRequestTarget;
10
import org.apache.wicket.behavior.AttributeAppender;
11
import org.apache.wicket.extensions.ajax.markup.html.repeater.data.table.AjaxFallbackHeadersToolbar;
12
import org.apache.wicket.extensions.ajax.markup.html.repeater.data.table.AjaxNavigationToolbar;
13
import org.apache.wicket.extensions.markup.html.repeater.data.grid.ICellPopulator;
14
import org.apache.wicket.extensions.markup.html.repeater.data.table.AbstractColumn;
15
import org.apache.wicket.extensions.markup.html.repeater.data.table.DataTable;
16
import org.apache.wicket.extensions.markup.html.repeater.data.table.IColumn;
17
import org.apache.wicket.extensions.markup.html.repeater.data.table.IStyledColumn;
18
import org.apache.wicket.markup.html.WebMarkupContainer;
19
import org.apache.wicket.markup.html.basic.Label;
20
import org.apache.wicket.markup.html.form.TextField;
21
import org.apache.wicket.markup.html.link.AbstractLink;
22
import org.apache.wicket.markup.html.link.BookmarkablePageLink;
23
import org.apache.wicket.markup.html.list.ListItem;
24
import org.apache.wicket.markup.html.list.ListView;
25
import org.apache.wicket.markup.repeater.Item;
26
import org.apache.wicket.model.IModel;
27
import org.apache.wicket.model.Model;
28
import org.apache.wicket.model.util.ListModel;
29
import org.apache.wicket.request.mapper.parameter.PageParameters;
30
import org.apache.wicket.util.string.Strings;
31
import org.nanopub.extra.services.ApiResponse;
32
import org.nanopub.extra.services.ApiResponseEntry;
33
import org.nanopub.extra.services.QueryRef;
34
import org.slf4j.Logger;
35
import org.slf4j.LoggerFactory;
36

37
import java.util.ArrayList;
38
import java.util.Collections;
39
import java.util.List;
40
import java.util.Set;
41

42
/**
43
 * Component for displaying query results in a table format.
44
 */
45
public class QueryResultTable extends QueryResult {
46

47
    private static final Logger logger = LoggerFactory.getLogger(QueryResultTable.class);
×
48

49
    private Model<String> errorMessages = Model.of("");
×
50
    private DataTable<ApiResponseEntry, String> table;
51
    private String groupColumnKey = null;
×
52
    private Label noRecordsLabel;
53
    private Label errorLabel;
54
    private FilteredQueryResultDataProvider filteredDataProvider;
55
    private Model<String> filterModel = Model.of("");
×
56
    // The source-nanopub column ("np"/"nps"), folded into the per-row actions dropdown.
57
    private String sourceColumnKey;
58

59
    QueryResultTable(String id, QueryRef queryRef, ApiResponse response, ViewDisplay viewDisplay, boolean plain) {
60
        super(id, queryRef, response, viewDisplay);
×
61

62
        if (plain) {
×
63
            add(new Label("label").setVisible(false));
×
64
            add(new Label("np").setVisible(false));
×
65
            showViewDisplayMenu = false;
×
66
            hideDescription();
×
67
        } else {
68
            String label = grlcQuery.getLabel();
×
69
            if (viewDisplay.getTitle() != null) {
×
70
                label = viewDisplay.getTitle();
×
71
            }
72
            add(new Label("label", label).setVisible(label != null && !label.isEmpty()));
×
73
        }
74

75
        errorLabel = new Label("error-messages", errorMessages);
×
76
        errorLabel.setVisible(false);
×
77
        add(errorLabel);
×
78

79
        TextField<String> filterField = new TextField<>("filter", filterModel);
×
80
        filterField.setOutputMarkupId(true);
×
81
        filterField.add(new FilterUpdatingBehavior() {
×
82
            @Override
83
            protected void onUpdate(AjaxRequestTarget target) {
84
                if (filteredDataProvider != null && table != null) {
×
85
                    filteredDataProvider.setFilterText(filterModel.getObject());
×
86
                    target.add(table);
×
87
                    if (noRecordsLabel != null) {
×
88
                        target.add(noRecordsLabel);
×
89
                    }
90
                }
91
            }
×
92
        });
93
        filterField.setVisible(!fitsOnFirstPage());
×
94
        add(filterField);
×
95

96
        populateComponent();
×
97
    }
×
98

99
    private void addErrorMessage(String errorMessage) {
100
        String s = errorMessages.getObject();
×
101
        if (s.isEmpty()) {
×
102
            s = "Error: " + errorMessage;
×
103
        } else {
104
            s += ", " + errorMessage;
×
105
        }
106
        errorMessages.setObject(s);
×
107
        errorLabel.setVisible(true);
×
108
        if (table != null) {
×
109
            table.setVisible(false);
×
110
        }
111
    }
×
112

113
    @Override
114
    protected void populateComponent() {
115
        List<IColumn<ApiResponseEntry, String>> columns = new ArrayList<>();
×
116
        QueryResultDataProvider dataProvider;
117
        try {
118
            // Columns that only feed action query mappings (conditional targets, the
119
            // local-key bundle) carry action data, not row content — don't render them.
120
            Set<String> hiddenColumns = viewDisplay.getView() != null
×
121
                    ? viewDisplay.getView().getActionMappingSourceColumns() : Collections.emptySet();
×
122
            // The source-nanopub column ("np"/"nps") is no longer rendered as its own
123
            // column; it becomes the "source" entry of this row's actions dropdown.
124
            sourceColumnKey = null;
×
125
            for (String h : response.getHeader()) {
×
126
                if (h.equals("np") || h.equals("nps")) {
×
127
                    sourceColumnKey = h;
×
128
                }
129
            }
130
            // Whether any rendered column carries a visible header label. If none do
131
            // (every column is "_noheader"), the entire header row is dropped — see the
132
            // gated addTopToolbar below.
133
            boolean anyHeaderShown = false;
×
134
            // A column whose name ends in "_group" is not rendered as a column: its value
135
            // labels a group of rows, shown as a full-width header row wherever the value
136
            // changes from one row to the next (in display order), see newRowItem below.
137
            // The groups keep the order the query returned them in; sorting the table by
138
            // a column reorders the rows inside each group only (see
139
            // FilteredQueryResultDataProvider), so there is always one header per group.
140
            groupColumnKey = null;
×
141
            for (String h : response.getHeader()) {
×
142
                if (h.endsWith("_group")) {
×
143
                    groupColumnKey = h;
×
144
                }
145
            }
146
            for (String h : response.getHeader()) {
×
147
                if (h.endsWith("_label") || h.endsWith("_label_multi")
×
148
                    || hiddenColumns.contains(h) || h.equals(sourceColumnKey) || h.equals(groupColumnKey)) {
×
149
                    continue;
×
150
                }
151
                // A trailing "_noheader" hides this column's header label while still
152
                // rendering the column. It is stripped first to recover the logical
153
                // column key, so every other rule (type suffix, _label companion,
154
                // action mappings) operates unchanged on the unmarked name.
155
                boolean noHeader = h.endsWith("_noheader");
×
156
                String key = noHeader ? h.substring(0, h.length() - "_noheader".length()) : h;
×
157
                String displayLabel = key;
×
158
                if (displayLabel.endsWith("_multi_iri")) {
×
159
                    displayLabel = displayLabel.substring(0, displayLabel.length() - "_multi_iri".length());
×
160
                } else if (displayLabel.endsWith("_multi_val")) {
×
161
                    displayLabel = displayLabel.substring(0, displayLabel.length() - "_multi_val".length());
×
162
                } else if (displayLabel.endsWith("_multi")) {
×
163
                    displayLabel = displayLabel.substring(0, displayLabel.length() - "_multi".length());
×
164
                } else if (displayLabel.endsWith("_iri")) {
×
165
                    displayLabel = displayLabel.substring(0, displayLabel.length() - "_iri".length());
×
166
                }
167
                String columnHeader = displayLabel.replaceAll("_", " ");
×
168
                if (noHeader) {
×
169
                    columns.add(new Column("", h, key, null));
×
170
                } else {
171
                    anyHeaderShown = true;
×
172
                    columns.add(new Column(columnHeader, h, key, null));
×
173
                }
174
            }
175
            // A single trailing dropdown column bundling this row's entry-level actions
176
            // and its "source" link, shown whenever either is present.
177
            boolean hasEntryActions = viewDisplay.getView() != null
×
178
                                      && !viewDisplay.getView().getViewEntryActionList().isEmpty();
×
179
            if (hasEntryActions || sourceColumnKey != null) {
×
180
                columns.add(new Column("", Column.ACTIONS, "cell-right"));
×
181
            }
182
            dataProvider = new QueryResultDataProvider(response.getData());
×
183
            filteredDataProvider = new FilteredQueryResultDataProvider(dataProvider, response);
×
184
            // The whole table (header included) is hidden when there is nothing to show;
185
            // a "(nothing found)" note is shown instead. No NoRecordsToolbar, since that
186
            // would leave the header row visible.
187
            final int renderedColumnCount = columns.size();
×
188
            table = new DataTable<>("table", columns, filteredDataProvider, viewDisplay.getPageSize() < 1 ? Integer.MAX_VALUE : viewDisplay.getPageSize()) {
×
189
                @Override
190
                protected void onConfigure() {
191
                    super.onConfigure();
×
192
                    setVisible(errorMessages.getObject().isEmpty() && filteredDataProvider.size() > 0);
×
193
                }
×
194

195
                // The value of the "_group" column of the row rendered before the current
196
                // one on this page; rows are created in display order, so comparing against
197
                // it tells where a new group starts. Reset on the first row of each page.
198
                private String previousGroup = null;
×
199

200
                @Override
201
                protected Item<ApiResponseEntry> newRowItem(String id, int index, IModel<ApiResponseEntry> model) {
202
                    if (groupColumnKey == null) {
×
203
                        return super.newRowItem(id, index, model);
×
204
                    }
205
                    if (index == 0) previousGroup = null;
×
206
                    String group = model.getObject().get(groupColumnKey);
×
207
                    if (group == null) group = "";
×
208
                    final String header = group.equals(previousGroup) || group.isEmpty() ? null : group;
×
209
                    previousGroup = group;
×
210
                    return new Item<ApiResponseEntry>(id, index, model) {
×
211
                        @Override
212
                        protected void onRender() {
213
                            if (header != null) {
×
214
                                // A full-width header row written straight before this row's
215
                                // own <tr>, outside Wicket's component tree (there is no
216
                                // component for it to attach to).
217
                                getResponse().write("<tr class=\"group-header\"><td colspan=\"" + renderedColumnCount + "\">"
×
218
                                        + Strings.escapeMarkup(header) + "</td></tr>");
×
219
                            }
220
                            super.onRender();
×
221
                        }
×
222
                    };
223
                }
224
            };
225
            table.setOutputMarkupPlaceholderTag(true);
×
226
            // Marker class so nanodash.js can wrap emoji in body cells with the same
227
            // monochrome .emoji styling used for headings (e.g. the ✅/⚠️ key-approval
228
            // annotations in the "keys" column).
229
            table.add(new AttributeAppender("class", "result-table"));
×
230
            table.addBottomToolbar(new AjaxNavigationToolbar(table));
×
231
            // Drop the header row entirely when no column has a visible header label.
232
            if (anyHeaderShown) {
×
233
                table.addTopToolbar(new AjaxFallbackHeadersToolbar<String>(table, dataProvider));
×
234
            }
235
            add(table);
×
236
            // Hidden when the empty-actions line below shows instead, which carries
237
            // its own "Nothing here yet:" text; this note still covers the case of
238
            // the filter text matching no row.
239
            noRecordsLabel = new Label("no-records", "(nothing found)") {
×
240
                @Override
241
                protected void onConfigure() {
242
                    super.onConfigure();
×
243
                    setVisible(errorMessages.getObject().isEmpty() && filteredDataProvider.size() == 0 && !hasEmptyStateActions());
×
244
                }
×
245
            };
246
            noRecordsLabel.setOutputMarkupPlaceholderTag(true);
×
247
            add(noRecordsLabel);
×
248
            // When the result is genuinely empty (not merely filtered down to zero
249
            // rows), the view-level actions are promoted from the dropdown menu to
250
            // visible buttons in the empty state, pointing e.g. a space admin to
251
            // "add preset..." as the next step. menuActions only ever contains
252
            // actions the viewer is entitled to (see QueryResultTableBuilder), so
253
            // everyone else just gets the plain note. The same actions stay in the
254
            // dropdown menu, which remains their place once the table has content.
255
            WebMarkupContainer emptyActions = new WebMarkupContainer("empty-actions") {
×
256
                @Override
257
                protected void onConfigure() {
258
                    super.onConfigure();
×
259
                    setVisible(errorMessages.getObject().isEmpty() && hasEmptyStateActions());
×
260
                }
×
261
            };
262
            // menuActions is filled by the builder after construction, so the list
263
            // is wrapped as a live model rather than copied here.
264
            emptyActions.add(new ListView<MenuAction>("actions", new ListModel<>(menuActions)) {
×
265
                @Override
266
                protected void populateItem(ListItem<MenuAction> item) {
267
                    MenuAction action = item.getModelObject();
×
268
                    AbstractLink link = new BookmarkablePageLink<NanodashPage>("link", action.pageClass(), action.params());
×
269
                    link.setBody(Model.of(action.label()));
×
270
                    item.add(link);
×
271
                }
×
272
            });
273
            add(emptyActions);
×
274
        } catch (Exception ex) {
×
275
            logger.error("Error creating table for query {}", grlcQuery.getQueryId(), ex);
×
276
            add(new Label("table", "").setVisible(false));
×
277
            add(new Label("no-records", "").setVisible(false));
×
278
            add(new Label("empty-actions", "").setVisible(false));
×
279
            addErrorMessage(ex.getMessage());
×
280
        }
×
281
    }
×
282

283
    private class Column extends AbstractColumn<ApiResponseEntry, String> implements IStyledColumn<ApiResponseEntry, String> {
284

285
        private String key;
286
        // The actual response-column name to read row data from. Differs from the
287
        // logical key only for "_noheader" columns, whose marker is kept here but
288
        // stripped from key so all name-matching uses the unmarked name.
289
        private String dataKey;
290
        private String cssClass;
291
        public static final String ACTIONS = "*actions*";
292

293
        public Column(String title, String key) {
294
            this(title, key, key, null);
×
295
        }
×
296

297
        public Column(String title, String key, String cssClass) {
298
            this(title, key, key, cssClass);
×
299
        }
×
300

301
        public Column(String title, String dataKey, String key, String cssClass) {
×
302
            // Only a column with a header label is sortable: an empty header is nothing to
303
            // click, and the actions column would otherwise offer to order the table by a
304
            // property no row has, giving a round trip that changes nothing (issue #673).
305
            super(new Model<String>(title), Strings.isEmpty(title) ? null : dataKey);
×
306
            this.key = key;
×
307
            this.dataKey = dataKey;
×
308
            this.cssClass = cssClass;
×
309
        }
×
310

311
        @Override
312
        public String getCssClass() {
313
            return cssClass;
×
314
        }
315

316
        @Override
317
        public void populateItem(Item<ICellPopulator<ApiResponseEntry>> cellItem, String componentId, IModel<ApiResponseEntry> rowModel) {
318
            try {
319
                View view = viewDisplay.getView();
×
320
                if (key.equals(ACTIONS)) {
×
321
                    List<AbstractLink> links = ViewActionMappings.buildEntryActionLinks(view, rowModel.getObject(),
×
322
                            queryRef, resourceWithProfile, contextId, partId, refRoot, postPublishTab);
×
323
                    // The former "^" source link joins the same dropdown, as a "source" entry.
324
                    if (sourceColumnKey != null) {
×
325
                        String sourceUri = rowModel.getObject().get(sourceColumnKey);
×
326
                        if (sourceUri != null && !sourceUri.isBlank()) {
×
327
                            AbstractLink sourceLink = new BookmarkablePageLink<NanodashPage>("link", ExplorePage.class,
×
328
                                    new PageParameters().set("id", sourceUri));
×
329
                            sourceLink.add(NavigationContext.pageContextFallback());
×
330
                            sourceLink.setBody(Model.of("<span class=\"actionmenu-icon\">↗︎</span>source")).setEscapeModelStrings(false);
×
331
                            links.add(sourceLink);
×
332
                        }
333
                    }
334
                    if (links.isEmpty()) {
×
335
                        cellItem.add(new Label(componentId).setVisible(false));
×
336
                    } else {
337
                        cellItem.add(new EntryActionMenu(componentId, links));
×
338
                    }
339
                } else {
×
340
                    String value = rowModel.getObject().get(dataKey);
×
341
                    if (key.endsWith("_multi_iri")) {
×
342
                        String labelKey = key.substring(0, key.length() - "_multi_iri".length()) + "_label_multi";
×
343
                        String labelValue = rowModel.getObject().get(labelKey);
×
344
                        String[] uris = (value == null || value.isBlank()) ? new String[0] : value.split("\\s+");
×
345
                        String[] labels = labelValue != null ? labelValue.split("\n", -1) : null;
×
346
                        List<Component> links = new ArrayList<>();
×
347
                        for (int i = 0; i < uris.length; i++) {
×
348
                            String uri = uris[i];
×
349
                            if (uri.isBlank()) {
×
350
                                continue;
×
351
                            }
352
                            String rawLabel = (labels != null && i < labels.length && !labels[i].isBlank()) ? Utils.unescapeMultiValue(labels[i]) : null;
×
353
                            // SPARQL coalesce often falls back to the URI string itself; treat that as no label
354
                            // so NanodashLink can derive a short name from the URI.
355
                            if (rawLabel != null && rawLabel.equals(uri)) {
×
356
                                rawLabel = null;
×
357
                            }
358
                            links.add(new NanodashLink("component", uri, null, null, rawLabel, contextId));
×
359
                        }
360
                        cellItem.add(new ComponentSequence(componentId, ", ", links));
×
361
                    } else if (key.endsWith("_multi_val")) {
×
362
                        String labelKey = key.substring(0, key.length() - "_multi_val".length()) + "_label_multi";
×
363
                        String labelValue = rowModel.getObject().get(labelKey);
×
364
                        String[] parts = (value == null) ? new String[0] : value.split("\n", -1);
×
365
                        String[] labels = labelValue != null ? labelValue.split("\n", -1) : null;
×
366
                        List<Component> components = new ArrayList<>();
×
367
                        for (int i = 0; i < parts.length; i++) {
×
368
                            String part = parts[i];
×
369
                            String rawLabel = (labels != null && i < labels.length && !labels[i].isBlank()) ? Utils.unescapeMultiValue(labels[i]) : null;
×
370
                            if (isPublishLink(part)) {
×
371
                                components.add(new Label("component", publishButtonHtml(part, rawLabel)).setEscapeModelStrings(false));
×
372
                            } else if (Utils.isUriValue(part)) {
×
373
                                if (rawLabel != null && rawLabel.equals(part)) {
×
374
                                    rawLabel = null;
×
375
                                }
376
                                components.add(new NanodashLink("component", part, null, null, rawLabel, contextId));
×
377
                            } else {
378
                                String label = rawLabel;
×
379
                                String unescaped = Utils.unescapeMultiValue(part);
×
380
                                if (label == null && Utils.isDate(unescaped)) {
×
381
                                    // Friendly relative time, matching single-value cells.
382
                                    components.add(new Label("component", Utils.friendlyDateHtml(unescaped, unescaped)).setEscapeModelStrings(false));
×
383
                                } else {
384
                                    String display = label != null ? label : unescaped;
×
385
                                    if (Utils.looksLikeHtml(display)) {
×
386
                                        components.add(new Label("component", cellHtml(display))
×
387
                                                .setEscapeModelStrings(false)
×
388
                                                .add(new AttributeAppender("class", "cell-data-html")));
×
389
                                    } else {
390
                                        components.add(new Label("component", display));
×
391
                                    }
392
                                }
393
                            }
394
                        }
395
                        cellItem.add(new ComponentSequence(componentId, ", ", components));
×
396
                    } else if (key.endsWith("_multi")) {
×
397
                        String labelKey = key.substring(0, key.length() - "_multi".length()) + "_label_multi";
×
398
                        String labelValue = rowModel.getObject().get(labelKey);
×
399
                        String[] parts = (value == null) ? new String[0] : value.split("\n", -1);
×
400
                        String[] labels = labelValue != null ? labelValue.split("\n", -1) : null;
×
401
                        List<Component> components = new ArrayList<>();
×
402
                        for (int i = 0; i < parts.length; i++) {
×
403
                            boolean hasLabel = labels != null && i < labels.length && !labels[i].isBlank();
×
404
                            String display = hasLabel ? Utils.unescapeMultiValue(labels[i]) : Utils.unescapeMultiValue(parts[i]);
×
405
                            if (!hasLabel && Utils.isDate(display)) {
×
406
                                // Friendly relative time, matching single-value cells.
407
                                components.add(new Label("component", Utils.friendlyDateHtml(display, display)).setEscapeModelStrings(false));
×
408
                            } else if (Utils.looksLikeHtml(display)) {
×
409
                                components.add(new Label("component", cellHtml(display))
×
410
                                        .setEscapeModelStrings(false)
×
411
                                        .add(new AttributeAppender("class", "cell-data-html")));
×
412
                            } else {
413
                                components.add(new Label("component", display));
×
414
                            }
415
                        }
416
                        cellItem.add(new ComponentSequence(componentId, ", ", components));
×
417
                    } else if (key.endsWith("template_iri")) {
×
418
                        String label = rowModel.getObject().get(key + "_label");
×
419
                        if (label == null || label.isBlank()) {
×
420
                            label = truncateLabel(value);
×
421
                        }
422
                        String templateUrl = PublishPage.MOUNT_PATH + "?template=" + Utils.urlEncode(value) + "&template-version=latest";
×
423
                        templateUrl += linkNavParams(templateUrl);
×
424
                        String html = "<a href=\"" + Strings.escapeMarkup(templateUrl) + "\">" + Strings.escapeMarkup(label) + "</a>";
×
425
                        cellItem.add(new Label(componentId, html).setEscapeModelStrings(false));
×
426
                    } else if (isPublishLink(value)) {
×
427
                        // A ready-made link to the publish form: an action the view offers,
428
                        // shown as a button rather than as a link to content.
429
                        String label = rowModel.getObject().get(key + "_label");
×
430
                        cellItem.add(new Label(componentId, publishButtonHtml(value, label)).setEscapeModelStrings(false));
×
431
                    } else if (Utils.isUriValue(value)) {
×
432
                        String label = rowModel.getObject().get(key + "_label");
×
433
                        cellItem.add(new NanodashLink(componentId, value, null, null, label, contextId));
×
434
                    } else {
×
435
                        String litLabel = rowModel.getObject().get(key + "_label");
×
436
                        if (litLabel != null && !litLabel.isBlank() && !litLabel.equals(value)) {
×
437
                            // Separate display label for a (non-IRI) literal value; the full
438
                            // literal is shown on hover via the standard styled tooltip.
439
                            String labelHtml = Utils.looksLikeHtml(litLabel) ? cellHtml(litLabel) : Strings.escapeMarkup(litLabel).toString();
×
440
                            String html = "<span class=\"tooltip\"><span class=\"tooltiptext tooltiptext-auto\">" + Strings.escapeMarkup(value) + "</span>" + labelHtml + "</span>";
×
441
                            cellItem.add(new Label(componentId, html).setEscapeModelStrings(false));
×
442
                        } else if (key.startsWith("pubkey")) {
×
443
                            cellItem.add(new Label(componentId, value).add(new AttributeAppender("style", "overflow-wrap: anywhere;")));
×
444
                        } else if (Utils.isDate(value)) {
×
445
                            // Show a friendly relative time (client-side); raw ISO value stays as no-script fallback.
446
                            cellItem.add(new Label(componentId, Utils.friendlyDateHtml(value, value)).setEscapeModelStrings(false));
×
447
                        } else {
448
                            Label cellLabel;
449
                            if (Utils.looksLikeHtml(value)) {
×
450
                                cellLabel = (Label) new Label(componentId, cellHtml(value))
×
451
                                        .setEscapeModelStrings(false)
×
452
                                        .add(new AttributeAppender("class", "cell-data-html"));
×
453
                            } else {
454
                                cellLabel = new Label(componentId, value);
×
455
                            }
456
                            cellItem.add(cellLabel);
×
457
                        }
458
                    }
459
                }
460
            } catch (Exception ex) {
×
461
                logger.error("Failed to populate table column: ", ex);
×
462
                cellItem.add(new Label(componentId).setVisible(false));
×
463
                addErrorMessage(ex.getMessage());
×
464
            }
×
465
        }
×
466

467
    }
468

469
    private static String truncateLabel(String label) {
470
        return Utils.truncateLabel(label);
×
471
    }
472

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