From 29829c062cd531c29bc5cf74cd5d2211341f24d1 Mon Sep 17 00:00:00 2001 From: Martin Renvoize Date: Fri, 6 Mar 2026 15:54:46 +0000 Subject: [PATCH] Bug 36698: Render diff column as a human-readable table in the log viewer Previously the diff column in the log viewer displayed raw Struct::Diff JSON (e.g. {"D":{"superlibrarian":{"N":1,"O":0}}}), which was not useful to end users. This follow-up adds a JavaScript renderer (renderStructDiff) to viewlog.js that parses the Struct::Diff JSON and presents it as a compact Field / Before / After table, reusing the existing / colour styling already present for the jsdiff compare feature. Nested diffs (e.g. granular subpermissions) are flattened using dot-separated key names. Non-Struct::Diff content falls back gracefully to a
block so that entries from other modules are not affected.

The change benefits all modules that currently populate the diff column:
HOLDS (MODIFY/SUSPEND/RESUME/CANCEL/FILL), CATALOGUING (MODIFY),
APIKEYS (MODIFY/REVOKE/ACTIVATE), and MEMBERS (MODIFY for permissions).

Signed-off-by: David Nind 
Signed-off-by: Lisette Scheer 
---
 .../prog/en/modules/tools/viewlog.tt          | 29 +++++-
 koha-tmpl/intranet-tmpl/prog/js/viewlog.js    | 99 +++++++++++++++++++
 2 files changed, 127 insertions(+), 1 deletion(-)

diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/tools/viewlog.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/tools/viewlog.tt
index 42634a034e8..c399f5bc31b 100644
--- a/koha-tmpl/intranet-tmpl/prog/en/modules/tools/viewlog.tt
+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/tools/viewlog.tt
@@ -60,6 +60,33 @@
             overflow: scroll;
             padding: 10px;
         }
+        /* Struct::Diff renderer */
+        table.struct-diff {
+            border-collapse: collapse;
+            font-size: 0.85em;
+            width: 100%;
+        }
+        table.struct-diff th,
+        table.struct-diff td {
+            border: 1px solid #ccc;
+            padding: 2px 6px;
+            vertical-align: top;
+        }
+        table.struct-diff th {
+            background-color: #f5f5f5;
+            white-space: nowrap;
+        }
+        table.struct-diff td.diff-key {
+            font-family: monospace;
+            white-space: nowrap;
+        }
+        table.struct-diff del,
+        table.struct-diff ins {
+            font-family: monospace;
+            text-decoration: none;
+            padding: 1px 3px;
+            display: inline-block;
+        }
     
 [% END %]
 
@@ -410,7 +437,7 @@
                                     [% END %]
                                 
                                 [% PROCESS translate_log_interface log_interface=loopro.interface %]
-                                [% loopro.diff | html %]
+                                [% loopro.diff | html %]
                             
                         [% END %]
                     
diff --git a/koha-tmpl/intranet-tmpl/prog/js/viewlog.js b/koha-tmpl/intranet-tmpl/prog/js/viewlog.js
index 417dcaf18b7..6d9e84319b8 100644
--- a/koha-tmpl/intranet-tmpl/prog/js/viewlog.js
+++ b/koha-tmpl/intranet-tmpl/prog/js/viewlog.js
@@ -1,3 +1,95 @@
+/**
+ * Recursively collect changed entries from a Struct::Diff node.
+ * Returns an array of { key, before, after } objects.
+ * Nested diffs are flattened with dot-separated keys.
+ */
+function collectDiffEntries(node, prefix) {
+    var entries = [];
+    Object.keys(node).forEach(function (key) {
+        var change = node[key];
+        var fullKey = prefix ? prefix + "." + key : key;
+        if ("O" in change || "N" in change) {
+            entries.push({ key: fullKey, before: change.O, after: change.N });
+        } else if ("A" in change) {
+            entries.push({ key: fullKey, before: undefined, after: change.A });
+        } else if ("R" in change) {
+            entries.push({ key: fullKey, before: change.R, after: undefined });
+        } else if ("D" in change) {
+            entries = entries.concat(collectDiffEntries(change.D, fullKey));
+        }
+    });
+    return entries;
+}
+
+/**
+ * Render Struct::Diff JSON as a human-readable before/after table.
+ * Falls back to a 
 block for non-Struct::Diff JSON or plain text.
+ */
+function renderStructDiff(raw) {
+    if (!raw) return "";
+    var diff;
+    try {
+        diff = JSON.parse(raw);
+    } catch (e) {
+        return $("
").text(raw)[0].outerHTML;
+    }
+    if (!diff || !diff.D || typeof diff.D !== "object") {
+        return $("
").text(raw)[0].outerHTML;
+    }
+
+    var entries = collectDiffEntries(diff.D, "");
+    if (!entries.length) return "";
+
+    function fmt(v) {
+        if (v === undefined) return "";
+        var s = typeof v === "object" ? JSON.stringify(v) : String(v);
+        return $("").text(s).html();
+    }
+
+    var rows = entries
+        .map(function (e) {
+            var before =
+                e.before !== undefined
+                    ? "" + fmt(e.before) + ""
+                    : "";
+            var after =
+                e.after !== undefined ? "" + fmt(e.after) + "" : "";
+            return (
+                "" +
+                '' +
+                $("").text(e.key).html() +
+                "" +
+                '' +
+                before +
+                "" +
+                '' +
+                after +
+                "" +
+                ""
+            );
+        })
+        .join("");
+
+    return (
+        '' +
+        "" +
+        "" +
+        "" +
+        "" +
+        "" +
+        "" +
+        rows +
+        "" +
+        "
" + + __("Field") + + "" + + __("Before") + + "" + + __("After") + + "
" + ); +} + function tickAll(section) { $("input[type='checkbox'][name='" + section + "']").prop("checked", true); $("#" + section.slice(0, -1) + "ALL").prop("checked", true); @@ -151,6 +243,13 @@ $(document).ready(function () { e.preventDefault(); $(".compare:checked").prop("checked", false).change(); }); + + $("td.diff-col").each(function () { + var raw = $(this).text().trim(); + if (raw) { + $(this).html(renderStructDiff(raw)); + } + }); patron_autocomplete($("#user"), { "on-select-callback": function (event, ui) { $("#user").val(ui.item.patron_id); -- 2.39.5