Repository navigation
Requirement Editor Modernized
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()).

| 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 |
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) |
- Missing/invalid session → HTTP 401.
-
tproject_idmissing → 400. - Missing required field (doc_id/title) →
{status:'error', message}. - No
req_viewright → 403 on form; noreq_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 mode toolbar with the checked stay_here checkbox (bulk-entry default):

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:

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:

- The editor uses the
req_specstable (notrequirement_specs) and reads the spec title fromnodes_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
tllocalization keys as legacy. - On create, the requirement node +
requirementsrow + firstrequirements_revisionsrow are created in one transaction, mirroring the legacydoCreateflow. -
Per-version editing (#1382): mirrors the legacy
reqEdit.phpcontract —get_by_id(id, version_id)loads the exact version when given (lib/requirements/reqCommands.class.php:181) andupdate(id, version_id, ...)targets it (reqCommands.class.php:343). The modern BFF implements the same contract:formpicks the requestedreq_version_id(validated) or falls back to the latest;savewrites throughrequirement_mgr::update(reqId, versionId, ...)which directly updates thatreq_versionsrow. The version selector follows the existingreqView.htmlpattern (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 resolvergetItemTemplateContents('requirement_template','scope','')(lib/functions/common.php:1104) — samestring/string_id(lang_get) /file(getFileContents, already in scope viarequirement_mgr.class.php:18→attachments.inc.php→files.inc.php) /noneresolution and the same missing-file fallback message.template_bodyis exposed so the SPA can re-apply the scaffold after a stay_here bulk-entry reset; the value is also written intorequirement.scopeso 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
formresponses resolverequirement_mgr::get_last_doc_id_for_testproject()(lib/functions/requirement_mgr.class.php:2436,max()over the project's requirement IDs → that requirement'sreq_doc_id) exactly likereqEdit.php:249-251, gated byreq_cfg->allow_insertion_of_last_doc_id(config.inc.php:1804, repo defaultDISABLED— same as legacy). Exposure is always present in the payload (flag + value); the screen renders the teal insert icon next to#reqDocIdONLY whenMODE==create && flag && value, mirroring the three-condition TPL gate; the tooltip reuses the legacyinsert_last_req_doc_idlabel and the click handler is the modern port ofinsert_last_doc_id()(reqEdit.tpl:216-225). -
Event-history icon (#1378): mirrors the legacy two-condition TPL gate
(
mgt_view_eventsright ANDreq_idset, reqEdit.tpl:287-292). BFFformresponses exposecanViewEventsviahasRight('mgt_view_events')— the exact check done inlib/requirements/reqEdit.php:299($gui->grants->mgt_view_events). The screen renders the fa-history icon in the Document ID input group ONLY whenMODE==='edit' && rights.canViewEvents, and the click handler is the modern port ofshowEventHistoryFor(req_id, 'requirements')(testlink_library.js:1381): it submits the hidden#eventhistoryform (target=_blank) to the moderngui/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.
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).

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_idbody arg, fallback latest) — parity with legacyreqViewVersionsViewer.tpl:113-114passing the displayedreq_version_id; -
log_msg= the value of the newlog_messagebody 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.

- Server-side rights check on every route:
req_viewrequired even to read the form;req_mgmtrequired 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.
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.

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.

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 theshow_on_design+enable_on_designCFs for the selected project viaget_linked_cfields(localized labels,verbose_type,possible_values,default_value, storedvalue,requiredfromcfield_testprojects), converting stored date/datetime unix timestamps to ISO for the HTML5 inputs (mirror ofapi/testcases); create mode sends the definitions without values. Save computesoldCF/newCF, runsreqCfValueSet()(ISO→timestamp, multiselect|join, pre-seeds empties for legacy parity) and persists viacfield_mgr::design_values_to_db.simpleReqCompare()now forces a revision when a CF value changed (legacysimpleCompare()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 namedcustom_field_<type>_<id>), prefilled with stored values; readonly mode disables them; save collects + validates (required fields abort withreqe.cfRequired). -
i18n:
reqe.customFields,reqe.yes,reqe.cfSelectEmpty,reqe.cfRequiredadded to all 10 locale bundles.

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_warningflags +markModified()/resetModified()helpers (same names ascheckmodified.js). -
doBeforeUnload()installed onwindow.onbeforeunload; returnsreqe.unsavedWarning("You have unsaved changes. Are you sure you want to leave?") and setsevent.returnValueonly when dirty + warning armed. - Delegated
input changetracking 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()) setsshow_modified_warning=falsebefore 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.
