Repository navigation
Task Issue 1644 Assign Plan Roles Dynamic Role Column Heading
Task 1644 — dynamic localized role-column header Test Plan Role (<plan>) in Assign Test Plan Roles (gap vs legacy)
Issue: #1644
Status: IMPLEMENTED & VERIFIED — branch task/issue-1644 (the issue is closed
by the same run, after the branch was pushed)
Related: #1612 (the identical defect on the project twin,
usersAssignProject.html — a separate issue, deliberately not touched here),
#1676, #1664, #1707, #1645, #1646
Legacy usersAssign.tpl — the template shared by the test-project and the
test-plan contexts — built the role-override column caption out of TWO parts:
{* gui/templates/dashio/usermanagement/usersAssign.tpl:216-218 (deleted in ab387af72) *}
<th>{$labels.User}</th>
{$featureVerbose=$gui->featureType}
<th>{lang_get s="th_roles_$featureVerbose"} ({$my_feature_name|escape})</th>-
$featureVerboseis$gui->featureType, so on this screenlang_getresolvesth_roles_testplan→$TLS_th_roles_testplan=Test Plan Role(locale/en_US/strings.txt:2144). The label was localized in every shipped locale, not English-only. -
$my_feature_nameis assigned inside the combo loop, only for the selected option (usersAssign.tpl:177-179):{foreach from=$gui->features item=f} <option value="{$f.id}" {if $featureID == f.id} selected="selected" {/if}>{$f.name|escape}</option> {if $featureID == f.id}{$my_feature_name=$f.name}{/if} {/foreach}
i.e. the name of the currently selected test plan, HTML-escaped.
So the header read e.g. Test Plan Role (A-PUBLIC-1644) and changed with the
plan — including via a deep link, because $featureID comes from tplan_id.
The 2.0.1 port replaced it with a static, context-free label:
{* gui/templates/usermanagement/usersAssignPlan.html:116, before *}
<th style="width:220px;" data-i18n="assign.planRoleOverride">Plan Role Override</th>Nothing anywhere rendered th_roles_testplan or the selected plan name
(grep -rn "th_roles" gui/templates/i18n/*.json → 0 hits). The consequence is
practical, not cosmetic: the same user list is reused for every plan, so the
column that assigns plan-specific overrides carried no indication of which
plan it applies to.
Each value is seeded from that locale's own legacy $TLS_th_roles_testplan,
so restoring the string cannot regress it to English-everywhere:
| bundle | value | seeded from |
|---|---|---|
en.json |
Test Plan Role |
locale/en_US/strings.txt:2144 |
de.json |
Testplan Rolle |
locale/de_DE/strings.txt:2145 |
es.json |
Rol para el Plan de Pruebas |
locale/es_ES/strings.txt:2213 |
fr.json |
Rôle de campagne de test |
locale/fr_FR/strings.txt:2170 |
it.json |
Ruolo in Test Plan |
locale/it_IT/strings.txt:1464 |
ja.json |
テスト計画での役割 |
locale/ja_JP/strings.txt:2459 |
pt.json |
Papéis do Plano de Teste |
locale/pt_BR/strings.txt |
ru.json |
Роль в тест-плане |
locale/ru_RU/strings.txt |
zh.json |
测试计划角色 |
locale/zh_CN/strings.txt |
ro.json |
Rol în Planul de Test |
no $TLS_th_roles_testplan in locale/ro_RO/strings.txt → faithful translation |
<th style="width:220px;" id="planRoleHeading">Test Plan Role</th>No data-i18n on purpose: TLi18n.apply() (i18n.js:190-209) would overwrite
the composed caption with the bare label on every load — the very defect the
issue is about.
var planNameById = {}; // plan id (string) -> plan name, rebuilt per project load
function updatePlanRoleHeading() {
var label = TLi18n.t('header.planRoleHeading');
var planName = (currentPlan == null) ? '' : (planNameById[String(currentPlan)] || '');
$('#planRoleHeading').text(planName ? label + ' (' + planName + ')' : label);
}The plan name is interpolated raw and .text() does the escaping — that is
exactly what Smarty's {$my_feature_name|escape} does (escape, then write
markup, so the browser shows the characters). Calling esc() on top of .text()
double-escapes; that was the one bug this implementation found in itself, caught
by the escaping test case and fixed before landing.
Call sites — every path that changes or clears the plan context:
| where | why |
|---|---|
TLi18n.load callback |
bare label, so the <th> is never empty while the combo loads |
loadPlans() (synchronous reset + map rebuild + no-plan branch) |
the caption is reset synchronously, before the request leaves, so it can never name the previous project's plan during the in-flight window (measured); the map is rebuilt before anything reads it |
loadUsers() |
the caption follows the plan as the context changes, not when its users arrive, so it cannot lag one redraw behind the combo |
#planSelect change, empty branch |
combo cleared → bare label, never Test Plan Role ()
|
#projectSelect change, empty branch |
no project → no plan → bare label |
api/roles/index.php:895 already returns planOpts = getAssignablePlans() (defined at :153) →
[{id, name}, …], and the screen already consumes it to fill the combo
(the combo fill sits a few lines below the map build in the same loadPlans()). This is a pure front-end port.
Fixture (the DB is freshly imported every run — testplans was empty):
php tmp/fixtures_1644.php
# project ALPHA1644 (1) with plans A-PUBLIC-1644 (2), A-PRIVATE-1644 (3),
# 'A&B <draft>-1644' (5, HTML metacharacters); project BETA1644-EMPTY (4), no plans17-case suite Suite 1644 in tmp/TLU_Test_Cases.md — 17 PASS, plus the
regression matrix from the issue's investigation comment, all measured in headless
Chrome as admin:
- auto-selected first plan →
Test Plan Role (A-PRIVATE-1644) - manual combo switch public ↔ private → caption follows both ways
- combo cleared →
Test Plan Role(no empty()) - project with no plans → legacy disabled state, caption without a plan
- project switch → caption follows the new project's own plans
- plan named
A&B <draft>-1644→ rendered as text,children.length === 0 -
?locale=de→Testplan Rolle (A-PUBLIC-1644);?locale=ro→Rol în Planul de Test (…) - deep link
?tplan_id=2→ caption names plan 2 - DataTables sort/search redraw, bulk "Do", row
onRoleChange→ caption survives every re-init -
node --checkOK, 10/10 bundlesjson.tool-valid, 0 console errors/warnings,eventstable: 3 rows, alllog_level=16(audit) — 0 new ERROR/WARNING rows
