Repository navigation
Task Issue 974 codetracker used on testproject
Issue: #974
Status: IMPLEMENTED & VERIFIED (2026-09-25) — branch task/issue-974
Screen: gui/templates/codetracker/codetrackerView.html
BFF: api/codetracker/index.php
In TestLink 1.9.20 the code-tracker edit page carried an fa-info-circle anchor
next to the Name label. Clicking it revealed which test projects the tracker is
linked to — or the note "Code Tracker Not Used (Linked)" when it is linked to none.
Loading the page also deleted orphaned link rows left behind by a deleted test
project.
| Step | Legacy code | What it does |
|---|---|---|
| 1 | lib/codetrackers/codeTrackerEdit.php:150-157 |
$dummy = $commandMgr->codeTrackerMgr->getLinks($argsObj->id, ['getDeadLinks' => true]); then unlink($id, $key) for each — "just to fix erroneous test project delete"
|
| 2 | lib/codetrackers/codeTrackerEdit.php:158-159 |
$gui->testProjectSet = $commandMgr->codeTrackerMgr->getLinks($argsObj->id); — map testproject_id => testproject_name
|
| 3 | lib/functions/tlCodeTracker.class.php:471-500 |
getLinks() = testproject_codetracker LEFT OUTER JOIN nodes_hierarchy; with getDeadLinks it keeps only rows whose project node is NULL
|
| 4 | gui/templates/dashio/codetrackers/codeTrackerEdit.tpl:163-165 |
<a href="javascript:displayUsedBy('usedByEnvelope')"><i class="fas fa-info-circle" title="{$labels.show_hide_linked_to_project}"></i></a> |
| 5 | codeTrackerEdit.tpl:174-180 |
hidden #usedByOuter row wrapping #usedByEnvelope
|
| 6 | codeTrackerEdit.tpl:73-116 |
displayUsedBy() toggles the row; renders <b>Used on Test Project</b> + one ` |
| 7 | lib/codetrackers/codeTrackerEdit.php:180-184 |
the whole edit controller is gated on codetracker_management
|
| 8 | locale/en_US/strings.txt:3679,3866,3867 |
Used on Test Project, Show/Hide (Linked to Project), Code Tracker Not Used (Linked)
|
The 2.0.1 rewrite dropped steps 1–6: the modern edit modal showed only
Name / Type / config / Active / Save. The BFF called getLinks($id) but kept only
count($links) and dropped the names (link_count is what the delete gating needs,
#971) — so the data stopped at the API boundary and the used-by block had nothing to
render.
The sibling screens already had this exact feature: api/issuetracker/index.php:266-291
-
gui/templates/issuetracker/issuetrackerView.html:83-88,346-364(issue #964) andreqMgrSystemView.html(rms.usedOnTestProject). The code-tracker screen was the last one without it.
- New helper
attachLinks($mgr, $id, &$item, $purgeDead)(lines 118-156) — a faithful port ofinitializeGui():-
$purgeDead→getLinks($id, ['getDeadLinks' => true])+unlink($id, $tpid)for each orphan, so the used-by list never contains aNULLname andlink_countis never inflated; - then
links[] = testproject_namefor each remaining row, andlink_count = count(links).
-
-
GET /{id}callsattachLinks(..., $canManage)— this is the request the edit modal makes, i.e. exactly where legacy ran the purge. The purge is a write, and legacy reserved the edit page forcodetracker_managementholders (codeTrackerEdit.php:180-184), so it is limited to managers: a view-only caller (right 52) can no longer cause a DB write through a read route. - Dead rows are filtered out of the returned list for everybody (a dead row LEFT JOINs
to a NULL name). A manager never sees one because the purge just removed it, but a
viewer would otherwise be shown a phantom project and an inflated
link_count— which could disagree with the grid's delete gating (#971). -
POST(create),PUT(update) andDELETEcallattachLinks(..., false)so a write response is never misleading (a tracker that has links reports them; the deleted one reports its pre-delete state, which is what the legacy delete form showed to explain a refusal). -
trackerToJSON()always emits alinkskey. The list route keeps onlylink_count(fromgetAll('add_link_count')): the grid needs no names and resolving them per row would be an N+1 query. Documented in the code. - Values are returned raw — escaping stays in the screen (JSON API, issue #1581).
-
#btnUsedBy(fa-info-circle,data-i18n-title="ct.showHideLinkedToProject") next to the Name input + hidden#usedByOuter/#usedByEnvelope, mirroringcodeTrackerEdit.tpl:163-180. -
toggleUsedBy()— port ofdisplayUsedBy(): collapses/expands, renders the bold header plus one escaped name per line, or the italics "not used (linked)" note. Guarded byif (!canManage) return;because legacy gated the whole edit page oncodetracker_management. -
usedByProjects[]is filled fromr.item.linksineditTracker()and reset to[]inshowCreateModal()(legacy only built$gui->testProjectSetwhenid > 0). Both open paths collapse the block, so the envelope never shows a stale list. - Every project name goes through the existing
esc()helper — a project named<img src=x onerror=…>renders as inert text.
3 new keys in all 10 locale bundles (en, de, es, fr, it, ja, pt, ro, ru, zh),
inserted alphabetically inside the existing ct.* group (+3 lines per file, no
reformat):
| Key | en | source |
|---|---|---|
ct.usedOnTestproject |
Used on Test Project | strings.txt:3679 |
ct.notUsedLinked |
Code Tracker Not Used (Linked) | strings.txt:3867 |
ct.showHideLinkedToProject |
Show/Hide (Linked to Project) | strings.txt:3866 |
![]()
![]()
Test suite: tmp/TLU_Test_Cases.md → 974.1–974.15, 15/15 PASS (tooltip, linked
state, toggle on/off/on, unlinked state, create-modal state, Romanian locale, XSS in a
project name, BFF links, dead-link purge, grid + write-route regression, syntax
gates, Event Viewer). events gained 1 row (a normal log_level=16 activity from
the audited edit-session) and 0 rows with log_level IN (1,2).
Seeding a codetrackers row whose type is not in tlCodeTracker::$systems makes
GET /api/codetracker/index.php return HTTP 500 with an empty body, blanking the
whole grid: getImplementationForType()
(lib/functions/tlCodeTracker.class.php:115-126) reads $this->systems[$codeTrackerType]
with no isset guard and returns the literal class name "Interface", so
$impl::checkEnv() fatals. One corrupt row kills the screen for every user. Legacy
could not hit it (its type domain only offers valid types) and it is outside this issue
— filed as a separate bug issue.