Skip to content

Requirement Editor Modernized

opencode-ci edited this page Sep 16, 2026 · 9 revisions

Requirement Editor (reqEdit) — Modernized Screen

Modernization of Requirement Editor (lib/requirements/reqEdit.php) — GitHub issue #798.

The legacy Smarty screen is replaced by a standalone Dashio page (gui/templates/requirements/reqEdit.html) backed by a plain-PHP REST BFF (api/reqedit/index.php). This is a non-ASIDE screen reached as a popup from the modernized Search → Advanced (full text) results — the openReqEdit link was switched from the legacy reqEdit.php to the new HTML screen.

URL: gui/templates/requirements/reqEdit.html?id=<requirement_id>&tproject_id=<id> (edit) or gui/templates/requirements/reqEdit.html?spec_id=<spec_node_id>&tproject_id=<id> (create) BFF API: api/reqedit/index.php Rights: req_view (view) / req_mgmt (manage) enforced server-side on every route (same as legacy checkRights()).

Requirement Editor


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
Mode (edit / create) requirement_id vs req_spec_id URL args id= opens edit, spec_id= opens create; header shows spec title + latest version chip
Specific version editing (edit) reqEdit.php?doAction=edit&requirement_id=N&req_version_id=VID loads and edits THAT exact version (used by the legacy reqViewVersionsViewer 'Edit' submit); without req_version_id → latest version same (added in #1382) — req_version_id / version_id URL arg deep-links into the exact version; the BFF form returns the selected version deterministically plus the full versions list; the editor shows a Select version dropdown when the requirement has multiple versions (frozen marked *); saving targets the selected version
Document ID required req_doc_id (created as child revision node) same; validation blocks save when empty
Title required text same; validation
Status char(1) status code (D/R/F/I/V/O/N/...) same; mapped to human label on load/save
Type char(1) type code (V/R/...) same; dropdown of 7 types
Expected coverage 1/2/3/5/10 same dropdown
Scope description textarea same
Scope template on create renderGui() default branch pre-fills the Scope editor with the configured requirement_template content (getItemTemplateContents('requirement_template','scope',…) in lib/functions/common.php:1104 — types string / string_id / file, none → empty) same (added in #1381) — the BFF create form response resolves the same legacy function and returns the body as requirement.scope AND template_body; the screen pre-fills the textarea on create and re-applies it after a stay_here bulk-entry reset; edit mode never applies the template (legacy parity)
Save doAction=save → update/create revision POST ?action=save; on create, builds nodes_hierarchy + requirements + first requirements_revisions row and switches to edit mode
Create another after saving (create) stay_here checkbox ("check to create another requirement after saving", BUGID 3953 — create mode only) same (added in #1384) — stay_here checkbox in the toolbar, create mode only; when checked the save resets the form (doc id/title/scope empty, status/type/coverage at defaults) and keeps the caller in create mode on the same spec for bulk entry; when unchecked the form transitions to edit mode for the created requirement
Create New Version (edit) doAction=doCreateVersion → create_new_version() (reqCommands.class.php:609): copy the ENTIRE source version (copy_version: scope, status, type, expected_coverage, custom fields, attachments, TC-link handling), set log_message from the ask4log() prompt (commit_title = "Log message" / please_add_revision_log), freeze the source version when req_cfg->freezeREQVersionOnNewREQVersion (default TRUE), notify requirement monitors (notify=true) same (added in #1377) — POST ?action=version now calls requirement_mgr::create_new_version() with the same opts; the screen prompts for a log message (reqe.newVersionPrompt, revision-log pattern) and passes the editor's selected version_id as the copy source
Insert last doc id (create) icon next to Document ID fills it with the project's last req_doc_id; only when req_cfg->allow_insertion_of_last_doc_id enabled, last_doc_id != null, and create mode (reqEdit.tpl:295-308) same (added in #1379) — BFF form responses expose allow_insert_last_doc_id + last_doc_id; a teal insert icon is rendered next to Document ID in create mode only (config on + project has a prior req) and one-click fills the field; hidden in edit mode and after the first create-save
Event history (edit) question.gif icon next to Document ID, shown only when $gui->grants->mgt_view_events eq "yes" and $gui->req_id, calls showEventHistoryFor(req_id,'requirements') → eventviewer.php in a new tab (reqEdit.tpl:287-292) same (added in #1378) — BFF form responses expose rights.canViewEvents (hasRight('mgt_view_events'), reqEdit.php:299 parity); a fa-history icon in the Document ID input group is rendered in edit mode only (+ right) and submits the hidden #eventhistory GET form to eventviewer.html?object_id=<req_id>&object_type=requirements in a new tab
Cancel return to caller returns without any DB write

2. REST API Reference

All routes are session-authenticated and JSON; CSRF Origin header required.

Method Route Body / Query Returns
GET ?action=form&id=N tproject_id, optional version_id/req_version_id {mode:'edit', requirement, versions[], show_version_selector, options, tproject_id, tproject_name, allow_insert_last_doc_id, last_doc_id, rights} — requirement includes version_id and is_latest; versions[] = {version_id, version, revision, status, is_open}
GET ?action=form&spec_id=N tproject_id {mode:'create', spec_title, versions:[], show_version_selector:false, tproject_id, tproject_name, allow_insert_last_doc_id, last_doc_id, options, rights} — create mode also returns template_body (the configured requirement_template scaffold, '' when type is none) and pre-fills requirement.scope with it (see #1381)
POST ?action=save {id?, spec_id?, tproject_id, doc_id, title, status, type, scope, expected_coverage, version_id?, stay_here?} {status:'ok', id, version_id, stay_here} (update or create) — version_id targets THAT exact requirement version, absent → latest
POST ?action=version {id, tproject_id, version_id?, log_message?} {status:'ok', version, version_id, new_version} — full copy of the source version (type/coverage/CFs/attachments/scope/status), stores log_message, freezes the source per config, notifies monitors (Refs #1377)

Error conditions

  • Missing/invalid session → HTTP 401.
  • tproject_id missing → 400.
  • Missing required field (doc_id/title) → {status:'error', message}.
  • No req_view right → 403 on form; no req_mgmt → 403 on save/version.

On success the screen hides the error bar and (after create/version) refreshes the form with the new id/version.

Create-another-after-saving (stay_here) — screenshot tour

Create mode toolbar with the checked stay_here checkbox (bulk-entry default):

stay_here checkbox in create mode

Immediately after saving with stay_here checked — the form resets to a blank create form on the same spec (doc id/title/scope empty, status/type/coverage at defaults, version chip 0) so the next requirement can be entered right away:

blank create form after save

Per-version editing (req_version_id) — screenshot

Deep link into RE-1 revision 1 (reqEdit.html?id=4&tproject_id=1&req_version_id=5): the Select version dropdown shows v1r1 and the scope is the v1 text (Original scope paragraph one.), NOT the latest version:

per-version deep link into v1

3. Legacy parity notes

  • The editor uses the req_specs table (not requirement_specs) and reads the spec title from nodes_hierarchy/req_specs_revisions (spec node name).
  • Status/type are stored as single-char codes; the BFF maps between the char code and the human label using the same tl localization keys as legacy.
  • On create, the requirement node + requirements row + first requirements_revisions row are created in one transaction, mirroring the legacy doCreate flow.
  • Per-version editing (#1382): mirrors the legacy reqEdit.php contract — get_by_id(id, version_id) loads the exact version when given (lib/requirements/reqCommands.class.php:181) and update(id, version_id, ...) targets it (reqCommands.class.php:343). The modern BFF implements the same contract: form picks the requested req_version_id (validated) or falls back to the latest; save writes through requirement_mgr::update(reqId, versionId, ...) which directly updates that req_versions row. The version selector follows the existing reqView.html pattern (v<version>r<revision>, * for frozen).
  • Requirement template on create (#1381): mirrors renderGui()'s default branch (lib/requirements/reqEdit.php:166). The BFF reuses the exact legacy resolver getItemTemplateContents('requirement_template','scope','') (lib/functions/common.php:1104) — same string / string_id (lang_get) / file (getFileContents, already in scope via requirement_mgr.class.php:18 → attachments.inc.php → files.inc.php) / none resolution and the same missing-file fallback message. template_body is exposed so the SPA can re-apply the scaffold after a stay_here bulk-entry reset; the value is also written into requirement.scope so the editor opens pre-filled exactly like legacy (saving without touching the field persists the template as the scope).
  • Insert last doc id (#1379): mirrors the legacy compute+gate — BFF form responses resolve requirement_mgr::get_last_doc_id_for_testproject() (lib/functions/requirement_mgr.class.php:2436, max() over the project's requirement IDs → that requirement's req_doc_id) exactly like reqEdit.php:249-251, gated by req_cfg->allow_insertion_of_last_doc_id (config.inc.php:1804, repo default DISABLED — same as legacy). Exposure is always present in the payload (flag + value); the screen renders the teal insert icon next to #reqDocId ONLY when MODE==create && flag && value, mirroring the three-condition TPL gate; the tooltip reuses the legacy insert_last_req_doc_id label and the click handler is the modern port of insert_last_doc_id() (reqEdit.tpl:216-225).
  • Event-history icon (#1378): mirrors the legacy two-condition TPL gate (mgt_view_events right AND req_id set, reqEdit.tpl:287-292). BFF form responses expose canViewEvents via hasRight('mgt_view_events') — the exact check done in lib/requirements/reqEdit.php:299 ($gui->grants->mgt_view_events). The screen renders the fa-history icon in the Document ID input group ONLY when MODE==='edit' && rights.canViewEvents, and the click handler is the modern port of showEventHistoryFor(req_id, 'requirements') (testlink_library.js:1381): it submits the hidden #eventhistory form (target=_blank) to the modern gui/templates/eventviewer/eventviewer.html?object_id=<req_id>& object_type=requirements, i.e. the same object-scoped event viewer in a new tab as legacy.

4. i18n Keys

All labels are client-side via TLi18n; keys under the reqe. namespace (reqe.pageTitle, reqe.title, reqe.docId, reqe.status, reqe.type, reqe.expectedCoverage, reqe.scope, reqe.save, reqe.cancel, reqe.newVersion, reqe.stayHere, reqe.spec, reqe.version, reqe.versionSelect, reqe.detailHeader, reqe.insertLastDocId (insert-last-doc-id tooltip), reqe.showEventHistory (event-history icon tooltip), reqe.newVersionPrompt (create-new-version log prompt, added in #1377), and validation/toast messages). Present in all 10 bundles (en ro de es fr it ja pt ru zh).

Event-history icon in Document ID input group Event viewer filtered to the requirement

4b. Create New Version — full legacy parity (Task #1377)

Gap found: the modern ?action=version used requirement_mgr::create_version() which did a bare INSERT with hard-coded defaults type=TL_REQ_TYPE_INFO (1) and expected_coverage=1 — no copy of the source version, no log message, no freeze, no monitor notification. Legacy doCreateVersion() (reqCommands.class.php:609) → create_new_version() (requirement_mgr.class.php:2328) does a full copy.

Fix (BFF api/reqedit/index.php): the version action now calls requirement_mgr::create_new_version($reqId, $userId, {reqVersionID, log_msg, notify:true, freezeSourceVersion}), where:

  • reqVersionID = the editor's selected version (version_id body arg, fallback latest) — parity with legacy reqViewVersionsViewer.tpl:113-114 passing the displayed req_version_id;
  • log_msg = the value of the new log_message body arg (screen prompt);
  • freezeSourceVersion = req_cfg->freezeREQVersionOnNewREQVersion (config.inc.php:1428, repo default TRUE).

create_new_version() internally: computes next version number (last child + 1), notifies monitors (notifyMonitors, mail cfg mail_subject_req_new_version at requirement_mgr.class.php:4435), copies the whole source version via copy_version() (scope/status/type/expected_coverage + copy_cfields() + copy_attachments() + TC-link freeze/close), persists log_message and freezes the source (updateOpen(source, 0)) when configured. Response now returns version + version_id.

Fix (screen reqEdit.html): newVersion() prompts with window.prompt(TLi18n.t('reqe.newVersionPrompt')) before posting — mirroring the modern revision-log pattern (reqSpecView.html:348) and the legacy ask4log()/Ext.Msg.prompt (reqViewVersions.tpl:155). Cancel on the prompt aborts without any request. The POST body carries version_id (source) and log_message.

Related bug fixed (issue #1513): the legacy copy_version() chain emitted two E_WARNINGs under PHP 8 on every new version — $reqTCLinksCfg->freezeLinkOnNewReqVersion (casing typo; real key freezeLinkOnNewREQVersion) and $this->debugMsg in closeOpenTCVersionOnOpenLinks(). Both resolved in lib/functions/requirement_mgr.class.php; the link-freeze-on-new-req-version feature (config default TRUE) now actually takes effect. Event Viewer clean after all verification runs.

Requirement Editor ready to create a new version New version created — type/coverage preserved, log message stored

5. Security

  • Server-side rights check on every route: req_view required even to read the form; req_mgmt required for save/version.
  • CSRF guarded via Origin header check (all BFF routes).
  • Output is HTML-escaped in the client before insertion; DB writes use prepared statements.

6. Testing

See Suite 66 — Requirement Editor (reqEdit) in tmp/TLU_Test_Cases.md (12/12 PASS): edit mode, create mode, validation, create, save persist, Create New Version, BFF rights, no-permission, cancel, i18n integrity, legacy link switch, and Event Viewer cleanliness.

Create New Version full parity (Task #1377) — Suite 185 in tmp/TLU_Test_Cases.md (PASS): log-message prompt shown with reqe.newVersionPrompt, accept creates a full copy (type Feature + coverage 5 + scope/status preserved), log_message persisted verbatim, source version frozen (is_open=0), new version open, prompt Cancel creates nothing, Event Viewer / events table zero Error/Warning rows, browser console clean.

See also Task #1384 — stay_here create-another suite (10/10 PASS): checkbox visible/checked in create mode, bulk entry resets the form on same spec, unchecked save transitions to edit mode, edit mode hides the checkbox, BFF echoes stay_here, i18n reqe.stayHere in all 10 bundles, Event Viewer + console clean.

See also Task #1382 — per-version editing suite (11/11 PASS): deep-link into a specific version via req_version_id, latest-version default, version selector shown for multi-version requirements and hidden for single-version ones, selector switch reloads the chosen version, save targets the selected version only (other versions untouched), save without version_id targets latest, create regression, invalid version_id → 404, BFF payload shape (version_id, is_latest, versions[], show_version_selector), Event Viewer + console clean.

See also Task #1381 — requirement-template scope prefill suite (10/10 PASS): BFF create form resolves requirement_template for types string / string_id (lang_get) / file (file contents + missing-file fallback) / none (empty), exposes template_body, the create screen pre-fills the Scope textarea on load, edit mode stays template-free, stay_here reset re-applies the scaffold, saved scope persists the template body, Event Viewer + console clean. Create mode pre-filled from requirement_template

See also Task #1379 — Insert last doc id suite (10/10 PASS): config-enabled create BFF payload exposes allow_insert_last_doc_id=true + last_doc_id (max req id → its req_doc_id), create screen renders the teal insert icon with the tooltip Insert Document ID of last created Requirement: "REQ-002", one-click fill of #reqDocId, create-save persists and flips to edit mode hiding the icon, edit mode hides it, config-DISABLED + empty-project hide it, duplicate doc id still rejected server-side, EN→Français tooltip localization, Event Viewer + console clean.

Insert last doc id helper in create mode

Event-history icon (Task #1378) — 9/9 PASS: BFF rights.canViewEvents present in both edit and create payloads (admin true, restricted user without mgt_view_events false), fa-history icon visible only in edit mode with the right, click opens the object-scoped modern event viewer in a new tab (Filtered by requirements #<id>), icon hidden in create mode and without the right, i18n reqe.showEventHistory in all 10 bundles, Event Viewer clean.

Design-time Custom Fields (Task #1374) — the modern editor previously dropped the linked custom-field inputs that legacy reqEdit.php renders below the requirement form. Now fully ported, 12/12 PASS:

  • BFF (api/reqedit/index.php): linkedReqCustomFields() serves the show_on_design + enable_on_design CFs for the selected project via get_linked_cfields (localized labels, verbose_type, possible_values, default_value, stored value, required from cfield_testprojects), converting stored date/datetime unix timestamps to ISO for the HTML5 inputs (mirror of api/testcases); create mode sends the definitions without values. Save computes oldCF/newCF, runs reqCfValueSet() (ISO→timestamp, multiselect | join, pre-seeds empties for legacy parity) and persists via cfield_mgr::design_values_to_db. simpleReqCompare() now forces a revision when a CF value changed (legacy simpleCompare() parity).
  • UI (reqEdit.html): a "Custom Fields" card renders each linked CF using the per-type renderer (string/numeric/float/email/checkbox/list/radio/ multiselection/date/datetime/text area, inputs named custom_field_<type>_<id>), prefilled with stored values; readonly mode disables them; save collects + validates (required fields abort with reqe.cfRequired).
  • i18n: reqe.customFields, reqe.yes, reqe.cfSelectEmpty, reqe.cfRequired added to all 10 locale bundles.

Requirement editor with design-time Custom Field (edit mode) Create mode Custom Field input

7. Unsaved-changes warning (Task #1380)

Legacy reqEdit.tpl (lines 228-235) installs checkmodified.js, which sets window.onbeforeunload = doBeforeUnload and warns with warning_unsaved when a dirty editor is left (BUGID 4153). The modern screen had no such guard — leaving the page after editing silently discarded changes.

Modern reqEdit.html now mirrors the same guard:

  • content_modified / show_modified_warning flags + markModified() / resetModified() helpers (same names as checkmodified.js).
  • doBeforeUnload() installed on window.onbeforeunload; returns reqe.unsavedWarning ("You have unsaved changes. Are you sure you want to leave?") and sets event.returnValue only when dirty + warning armed.
  • Delegated input change tracking on .form-control (scope, title, doc id, status, type, expected coverage).
  • loadForm() resets the dirty flag after render (also covers the version-switch reload path).
  • Save success resets the dirty flag (legacy Save sets show_modified_warning=false).
  • Cancel (goBack()) sets show_modified_warning=false before navigating (legacy Cancel onclick parity).

i18n: reqe.unsavedWarning added to all 10 locale bundles (en,de,es,fr,it,ja,pt,ro,ru,zh), reusing the exact native translations shipped for tcedit.unsavedWarning.

Dirty editor after editing the scope — leaving fires the unsaved-changes prompt

Clone this wiki locally