Bug 42188: (follow-up) Pretty-render biblio info column in log viewer

The CATALOGUING info column for MODIFY/DELETE logs stores "biblio "
followed by a pretty-printed JSON dump of the biblio hashref, which
includes a MARC-in-JSON _marc structure. Raw, this is noisy and hard
for librarians to read in the log viewer.

Extract the nested-object formatter already used by the struct-diff
renderer into a shared fmtJsonValue() helper, and use it from a new
renderCatalogInfo() that runs over each div.loginfo on page load. If
the cell text matches "biblio {JSON}" it is replaced with the pretty
rendering; otherwise the cell is left untouched so non-catalog info
payloads (and the bare "biblio" logged on ADD) are unaffected.

Falling back silently on a parse failure means the old non-JSON
Dumper-format item payloads continue to display as-is.

Test plan:
1. Enable CataloguingLog.
2. Create, edit, and delete a biblio record via the staff UI.
3. Go to Tools -> Log viewer, filter by module=Cataloguing.
4. Verify:
   - ADD row's info cell shows just "biblio".
   - MODIFY and DELETE rows' info cells render as a nested list
     (leader, fields array of tag/indicator/subfield objects, plus
     frameworkcode/opac_suppressed/serial).
   - Diff column still renders the before/after struct-diff table.
5. Check another module's log entries (e.g. MEMBERS) are untouched.

Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Lisette Scheer <lisette@bywatersolutions.com>
Signed-off-by: Lucas Gass <lucas@bywatersolutions.com>
This commit is contained in:
Martin Renvoize 2026-04-21 17:17:51 +01:00 committed by Lucas Gass
parent 4ec43ac10d
commit 4bd9e7836b
No known key found for this signature in database
GPG Key ID: 60B447398B0D36CA
2 changed files with 72 additions and 27 deletions

View File

@ -95,6 +95,16 @@
table.struct-diff ul.diff-obj li {
white-space: nowrap;
}
/* Catalog log info column (biblio JSON payload) */
div.loginfo-struct {
font-family: monospace;
font-size: 0.85em;
}
div.loginfo-struct ul.diff-obj {
list-style: none;
margin: 0;
padding: 0 0 0 0.8em;
}
</style>
[% END %]
</head>

View File

@ -21,6 +21,56 @@ function collectDiffEntries(node, prefix) {
return entries;
}
/**
* Pretty-print a parsed JSON value as nested <ul> markup. Arrays become
* ordered lists preserving MARC-in-JSON field order; objects render their
* keys. Used by both the struct-diff renderer and the catalog info renderer.
*/
function fmtJsonValue(v) {
if (v === undefined || v === null) return "";
if (typeof v !== "object") return $("<span>").text(String(v)).html();
if (Array.isArray(v)) {
var items = v
.map(function (item) {
return "<li>" + fmtJsonValue(item) + "</li>";
})
.join("");
return '<ul class="diff-obj">' + items + "</ul>";
}
var items = Object.keys(v)
.map(function (k) {
return (
"<li>" +
$("<span>").text(k).html() +
": " +
fmtJsonValue(v[k]) +
"</li>"
);
})
.join("");
return '<ul class="diff-obj">' + items + "</ul>";
}
/**
* Render the info column for a CATALOGUING log entry. The payload is
* "biblio <JSON>" where the JSON is the pre-change (MODIFY) or final
* (DELETE) state of the record, including a MARC-in-JSON `_marc` key.
* Returns rendered HTML, or null if the text doesn't look like a
* biblio JSON payload (e.g. the bare "biblio" prefix logged on ADD).
*/
function renderCatalogInfo(raw) {
if (!raw) return null;
var match = raw.match(/^biblio\s+(\{[\s\S]*\})\s*$/);
if (!match) return null;
var parsed;
try {
parsed = JSON.parse(match[1]);
} catch (e) {
return null;
}
return '<div class="loginfo-struct">' + fmtJsonValue(parsed) + "</div>";
}
/**
* Render Struct::Diff JSON as a human-readable before/after table.
* Falls back to a <pre> block for non-Struct::Diff JSON or plain text.
@ -40,39 +90,16 @@ function renderStructDiff(raw) {
var entries = collectDiffEntries(diff.D, "");
if (!entries.length) return "";
function fmt(v) {
if (v === undefined || v === null) return "";
if (typeof v !== "object") return $("<span>").text(String(v)).html();
if (Array.isArray(v)) {
var items = v
.map(function (item) {
return "<li>" + fmt(item) + "</li>";
})
.join("");
return '<ul class="diff-obj">' + items + "</ul>";
}
var items = Object.keys(v)
.map(function (k) {
return (
"<li>" +
$("<span>").text(k).html() +
": " +
fmt(v[k]) +
"</li>"
);
})
.join("");
return '<ul class="diff-obj">' + items + "</ul>";
}
var rows = entries
.map(function (e) {
var before =
e.before !== undefined
? "<del>" + fmt(e.before) + "</del>"
? "<del>" + fmtJsonValue(e.before) + "</del>"
: "";
var after =
e.after !== undefined ? "<ins>" + fmt(e.after) + "</ins>" : "";
e.after !== undefined
? "<ins>" + fmtJsonValue(e.after) + "</ins>"
: "";
return (
"<tr>" +
'<td class="diff-key">' +
@ -269,6 +296,14 @@ $(document).ready(function () {
$(this).html(renderStructDiff(raw));
}
});
$("div.loginfo").each(function () {
var raw = $(this).text().trim();
var rendered = renderCatalogInfo(raw);
if (rendered) {
$(this).html(rendered);
}
});
patron_autocomplete($("#user"), {
"on-select-callback": function (event, ui) {
$("#user").val(ui.item.patron_id);