Repository navigation
Requirement Version Compare Modernized
Modernization of Requirement Version Compare
(lib/requirements/reqCompareVersions.php) — issue #1017.
The legacy Smarty diff screen is replaced by a standalone Dashio page
(gui/templates/requirements/reqCompare.html) backed by a dedicated BFF
(api/reqcompare/index.php, versions + compare actions). It is a
non-ASIDE screen reached from the modernized Requirement Viewer
(reqView.html) via the new "Compare versions" toolbar button (shown whenever
the requirement has more than one version/revision in its history).
URL: gui/templates/requirements/reqCompare.html?requirement_id=<id>&tproject_id=<id>
BFF API: api/reqcompare/index.php?action=versions|compare
Rights: mgt_view_req on the owning test project (resolved from
requirements JOIN req_specs) enforced server-side on every route.




| Feature | Legacy behavior | Modern implementation |
|---|---|---|
| Version picker | table of history rows (version/revision, left/right radios, log message, timestamp) | DataTable of history rows (ordered DESC) with two radio columns (left / right), log message, "last change" timestamp + editor; newest two pre-selected (left = newest, right = 2nd newest) |
| Diff method | radio htmlCompare (DaisyDiff) vs htmlCodeCompare (text diff/inline + context + show-all) |
same two radio options; context input + "Show all" shown only in code-compare mode |
| Attributes diff |
getAttrDiff() table (status, type, expected coverage) with changed highlighting |
same — attribute table with changed rows highlighted |
| Scope diff |
diff/HTMLDiffer on the scope of each version/revision |
same — HTML-ins/del or text diff; "No changes" when equal |
| Custom fields diff |
getCFDiff() table of linked design CF values on each item |
same — CF table, show_custom_fields_without_value honored |
| Notification | none | "No changes" badge per unchanged section; diff summary line "Comparing vX rY ↔ vA rB" |
All routes are session-authenticated and JSON; CSRF Origin header required.
| Method | Route | Query | Returns |
|---|---|---|---|
| GET | ?action=versions |
requirement_id, tproject_id (optional) |
{status, tproject_id, tproject_name, req_id, req_doc_id, req_type, context, items:[{item_id, version_id, revision_id, version, revision, scope, status, status_label, type, type_label, expected_coverage, log_message, timestamp, last_editor}]} |
| GET | ?action=compare |
requirement_id, left, right, `method=html |
text, context, context_show_all` |
- Missing/invalid session → HTTP 401.
- Unknown requirement → 404
Requirement not found. - Missing
requirement_id/ missingleftorright→ 400. - Conflicting
tproject_id→ 400. - Unknown action → 404
Unknown action. - Non-GET method → 405.
- No
mgt_view_reqright on the owning test project → 403.
- The version/revision list comes from
requirement_mgr->get_history()(same as legacy), returningitem_id/version/revision/revision_id(-1for plain version rows),scope,status,type,expected_coverage,log_message,timestamp,last_editor. Ordering is the legacy hardcoded DESC (newest version/revision first). - Two diff engines are ported 1:1:
HTMLDiffer::htmlDiff(HTML comparison;nl2brapplied on the scope only when the req type is plain text, matching the legacygetWebEditorCfg('requirement')branch) anddiff::doDiff+inline(HTML code comparison with a context window). - Attribute labels come from
init_labels(['status','type','expected_coverage'])(e.g. "Number of test cases needed"); thetype/statussingle-char columns are compared using the legacy constants. - Custom fields are read via
get_linked_cfieldson each item's node id; date and datetime values are decoded withtlStrftime;show_custom_fields_without_valuedecides whether empty values appear. -
Known quirk (pre-existing):
last_editorrenders as "undefined" for items created viacreate_new_version()/create_new_revision()— that'sget_history()legacy behavior, byte-identical in the already-modernizedreqSpecComparespec_revision_comparepath; intentionally not fixed here (documented, not a regression). -
create_new_version()bumps the previous version's ownrevisioncounter (so a v2 row may showr3next to a separater1revision row) — legacy behavior, reproduced by the fixture.
All labels are client-side via TLi18n; keys under the rcmp. namespace
(rcmp.title, rcmp.selectVersions, rcmp.selectTwo, rcmp.diffMethod,
rcmp.htmlCompare, rcmp.codeCompare, rcmp.context, rcmp.showAll,
rcmp.version, rcmp.revision, rcmp.compare, rcmp.logMessage, rcmp.timestamp,
rcmp.lastChange, rcmp.left, rcmp.right, rcmp.compareSelected, rcmp.invalidContext,
rcmp.computing, rcmp.diffDetails, rcmp.diffBetween, rcmp.noChanges,
rcmp.attributes, rcmp.attribute, rcmp.scope, rcmp.customFields,
rcmp.customField, rcmp.noVersions, rcmp.errLoad, rcmp.errNotFound,
rcmp.errNoRights) — 31 keys, plus footers.reqCompare (footer) and
reqv.compareVersions (the reqView.html toolbar button label). Present in all
10 bundles (en ro de es fr it ja pt ru zh).
- Server-side rights check on every route:
mgt_view_reqrequired even to list versions; the requirement's owning test project is resolved from therequirements JOIN req_specsrows (guest user verified 403). Deep links withouttproject_idstill resolve the owner. - CSRF guarded via Origin header check (all BFF routes).
- All ids are int-cast/prepared; diff HTML is re-escaped client-side
(
sanitizeDiffHtml()stripsscripttags and event attributes) before injection into the panel.
See Suite 1017 — Requirement Version Compare in tmp/TLU_Test_Cases.md
(17/17 PASS): versions list, HTML diff, code-compare diff, identical-pairs
"No changes", unknown-action 404, missing requirement 404, missing-param 400,
tproject-mismatch 400, unauthenticated 401, no-rights 403, screen rendering +
preselection, compare flow via UI, locale switch, reqView.html link-switch,
i18n key/JSON validation in all 10 bundles, and Event Viewer cleanliness
(events table left empty by fixture + screen).
The modern toolbar carries a leading ← Cancel ([#backBtn],
btn-ghost + fa-arrow-left, label common.cancel) placed before the
Refresh and Compare buttons. It restores the legacy cancel_top /
cancel_bottom affordances of reqCompareVersions.tpl:199-201,257-259
(btn_cancel → javascript:history.back()). goBack() in
reqCompare.html calls window.history.back() when the browser has usable
history (window.history.length > 1); on a fresh tab it falls back same-origin
to the modern Requirement Viewer (reqView.html?id=<requirement_id> &tproject_id=<tproject_id>, ids preserved from the compare URL) instead of
leaving the user on a dead about:blank — mirroring the accepted
reqSpecCompare.html toolbar pattern (Refs #1307).
The Last change column cell is now a CLICKABLE link (legacy parity:
reqCompareVersions.tpl:228-230 <td ... onclick="openReqRevisionWindow(item_id)">)
styled exactly like the legacy link (cursor:pointer; color:rgb(0,85,153)) with a
hover tooltip rcmp.openRevision. Clicking it calls openReqRevision(item_id),
the modern mirror of openReqRevisionWindow() (testlink_library.js:1618) which
opens gui/templates/requirements/reqRevisionView.html?showReqSpecTitle=1&item_id=<id>
in a popup named Requirement Revision (same as legacy) sized from the
ReqPopupWidth / ReqPopupHeight cookies (defaults 800x600) via a shared
getCookie() helper — the same pattern already used by reqSpecCompare.html
(openReqSpecRevision, Refs #1358). item_id is the row's real node id
(revision_id when the row is a revision, else version_id) as returned by the BFF
versions action; the already-modernized revision viewer BFF (api/reqrevision)
resolves both node kinds. No BFF change was required — the data was already in
the versions payload.


Source: gui/templates/requirements/reqCompare.html (row cell in verRow(),
helpers openReqRevision() + getCookie()). i18n: rcmp.openRevision
("Open revision in new window", localized in all 10 locales).
The Log message cell in the version/revision list now offers the legacy
tip4log() hover affordance (reqCompareVersions.tpl:32-48 +
lib/ajax/getreqlog.php): hovering a cell shows a mouse-tracked tooltip with the
FULL untruncated log message, while the cell itself shows the text truncated
to req_cfg->log_message_len (config.inc.php:1618 = 200) + ... (legacy
reqCompareVersions.php:280-289).
-
BFF (
api/reqcompare/index.php,versionsaction):log_messageis kept RAW (no server-side truncation/whitespace-collapse anymore) and the payload now carrieslog_message_len— mirrors the already-modernized siblingapi/reqspecspec_revision_compare(Refs #1357). -
Client (
reqCompare.html): reuses the siblingreqSpecCompare.htmltooltip pattern —td.log-cell[data-item-id](cursor:help, max-width 340px), a fixed#logTooltipdiv (dark, teal border, max-width 500px,white-space:pre-wrap),LOG_LEN+fullLogs(item_id → full text) captured from the BFF payload, andbindLogTooltips()delegatingmouseenter/mousemove/mouseleaveon#verTable(namespaced, survives the DataTables tbody rebuild;tbodymouseleave + explicit hide on re-render are the safety net for pointer-on-rebuild). -
normalizeLogTip()mirrorslib/ajax/getreqlog.phpexactly: strips<p>, turns</p>into a line break, trims, and falls back to the legacyempty_log_messagelabel (common.emptyLogMessagei18n key, present in all 10 bundles) — so an empty log shows "Log message is empty" instead of the old-, both in the cell and in the tooltip. -
Search parity: the same
data-searchfull-log indexing used by the sibling (Refs #1357 code review, commitd70e500c6) keeps the full untruncated log findable via the DataTables search box even when the visible cell is truncated. - Truncation is code-point-aware (
Array.from/slice), so multi-byte text and emoji at the 200-char boundary are never split mid-surrogate. - Tooltip content is rendered with
.text()(no HTML injection); the cell keepsescapeHtml().


