Skip to content

Requirement Version Compare Modernized

opencode agent edited this page Sep 24, 2026 · 4 revisions

Requirement Version Compare — Modernized Screen

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.

Selection state

Selection state with the newer screenshot

Diff state

Identical pair — No changes


Table of Contents

  1. What the screen does
  2. REST API Reference
  3. Legacy parity notes
  4. i18n Keys
  5. Security
  6. Testing

1. What the screen does

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"

2. REST API Reference

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`

Error conditions

  • Missing/invalid session → HTTP 401.
  • Unknown requirement → 404 Requirement not found.
  • Missing requirement_id / missing left or right → 400.
  • Conflicting tproject_id → 400.
  • Unknown action → 404 Unknown action.
  • Non-GET method → 405.
  • No mgt_view_req right on the owning test project → 403.

3. Legacy parity notes

  • The version/revision list comes from requirement_mgr->get_history() (same as legacy), returning item_id/version/revision/revision_id (-1 for 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; nl2br applied on the scope only when the req type is plain text, matching the legacy getWebEditorCfg('requirement') branch) and diff::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"); the type/status single-char columns are compared using the legacy constants.
  • Custom fields are read via get_linked_cfields on each item's node id; date and datetime values are decoded with tlStrftime; show_custom_fields_without_value decides whether empty values appear.
  • Known quirk (pre-existing): last_editor renders as "undefined" for items created via create_new_version()/create_new_revision() — that's get_history() legacy behavior, byte-identical in the already-modernized reqSpecCompare spec_revision_compare path; intentionally not fixed here (documented, not a regression).
  • create_new_version() bumps the previous version's own revision counter (so a v2 row may show r3 next to a separate r1 revision row) — legacy behavior, reproduced by the fixture.

4. i18n Keys

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).

5. Security

  • Server-side rights check on every route: mgt_view_req required even to list versions; the requirement's owning test project is resolved from the requirements JOIN req_specs rows (guest user verified 403). Deep links without tproject_id still 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() strips script tags and event attributes) before injection into the panel.

6. Testing

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).

Toolbar — Cancel/Back (Refs #1310)

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).

Version revision popup — Last change cell link (Refs #1309)

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.

the compare screen: Last change cells are now blue clickable links

clicking a cell opens the Requirement Revision popup for that item_id

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).

Log-message hover tooltip (Refs #1308)

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, versions action): log_message is kept RAW (no server-side truncation/whitespace-collapse anymore) and the payload now carries log_message_len — mirrors the already-modernized sibling api/reqspec spec_revision_compare (Refs #1357).
  • Client (reqCompare.html): reuses the sibling reqSpecCompare.html tooltip pattern — td.log-cell[data-item-id] (cursor:help, max-width 340px), a fixed #logTooltip div (dark, teal border, max-width 500px, white-space:pre-wrap), LOG_LEN + fullLogs (item_id → full text) captured from the BFF payload, and bindLogTooltips() delegating mouseenter/mousemove/mouseleave on #verTable (namespaced, survives the DataTables tbody rebuild; tbody mouseleave + explicit hide on re-render are the safety net for pointer-on-rebuild).
  • normalizeLogTip() mirrors lib/ajax/getreqlog.php exactly: strips <p>, turns </p> into a line break, trims, and falls back to the legacy empty_log_message label (common.emptyLogMessage i18n 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-search full-log indexing used by the sibling (Refs #1357 code review, commit d70e500c6) 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 keeps escapeHtml().

before: Log message cells truncated, no hover affordance (item 5 empty shows '-')

hover over a long revision log: tooltip shows the FULL untruncated text

hover over the empty-log cell: tooltip shows the empty_log_message placeholder

Clone this wiki locally