Task Issue 1613 Assign Project Roles Session Project Preselect - sebiboga/testlink-upgraded GitHub Wiki
Task 1613 — Assign Test Project Roles must preselect the SESSION test project when the URL carries no tproject_id
Issue: #1613
Status: IMPLEMENTED & VERIFIED (2026-10-01) — branch task/issue-1613
Screen: Assign Test Project Roles — gui/templates/usermanagement/usersAssignProject.html
BFF: api/roles/index.php — GET /meta/tproject-roles gained sessionTprojectID
i18n: unchanged — the fix changes which project is preselected, not any label
Legacy lib/usermanagement/usersAssign.php:307-316 (read from git ab387af72^; the
legacy controller and template were deleted in ab387af72, Refs #947), inside
getTestProjectEffectiveRoles():
// If have no a test project ID, try to figure out which test project to show
// Try with session info, if failed go to first test project available.
if (!$argsObj->featureID) {
if ($argsObj->testprojectID) {
$argsObj->featureID = $argsObj->testprojectID; // SESSION PROJECT WINS
} else if (sizeof($features)) {
$xx = current($features);
$argsObj->featureID = $xx['id']; // only then the first combo entry
}
}$argsObj->testprojectID is $_SESSION['testprojectID'], written by the navBar project
combo; gui/templates/dashio/usermanagement/usersAssign.tpl:174-179 then rendered that
combo entry selected. The grid therefore always opened on the user's current context.
The 2.0.1 port kept only two of the three sources:
// usersAssignProject.html:445 (pre-fix)
var pid = (selectedId && $.inArray(String(selectedId), ids) !== -1) ? selectedId : r.projects[0].id;selectedId comes exclusively from the tproject_id URL param
(usersAssignProject.html:400 → loadProjects(tp)), so a direct/bookmarked URL with no
param silently landed on r.projects[0].id — and getAssignableProjects()
(api/roles/index.php:123) orders the combo ORDER BY name ASC, i.e. alphabetically
first, not the session project.
The session source was dropped on both halves of the round trip: the BFF computed
$sessionTprojectID at api/roles/index.php:377 but consumed it only internally for the
rights check (:381) and never serialized it, so the screen could not have honoured it.
Impact beyond cosmetics. currentProject is what saveAssignments() posts
(usersAssignProject.html:911), so role assignments land on a project the user never
picked. The ASIDE/dashio links always append tproject_id
(lib/functions/common.php:1887) — which is why this only bites on a direct/bookmarked
URL, and that is precisely the case legacy handled.
Fixtures: project 1 "Analyzer public project" (APUB, is_public=1),
project 2 "Analyzer private project" (APRIV, is_public=0).
Session project pinned to 1 (navBar combo showed APUB:Analyzer public project,
Dashboard read "Test Project: Analyzer public project").
Screen opened at http://localhost:8082/gui/templates/usermanagement/usersAssignProject.html
(no query string) on the unmodified tree (git diff -- api gui empty, ecb826d16):
{"comboValue":"2","comboText":"Analyzer private project",
"options":[":-- select project --","2:Analyzer private project","1:Analyzer public project"]}// GET /api/roles/index.php/meta/tproject-roles?tproject_id=0
{"keys":["status","items","roles","projects","isPublic","demoMode","roleColouring","pagination"],
"sessionTprojectID": undefined,
"projects":[{"id":2,"name":"Analyzer private project"},{"id":1,"name":"Analyzer public project"}]}
GET /meta/tproject-roles now serializes the session project next to the filtered combo:
'pagination' => getUsersAssignPaginationConfig(),
'sessionTprojectID' => $sessionTprojectID]);$sessionTprojectID is the value already computed at api/roles/index.php:377
(intval($_SESSION['testprojectID']), 0 when the session has no project). Nothing new
is queried and nothing sensitive is exposed — only the id of the project the navBar combo
already put the user in. The screen treats 0/absent as "no session project".
loadProjects() resolves the initial selection with legacy's exact three-step precedence,
each candidate guarded by membership in the filtered combo (a stale/unknown id can
never be selected):
var ids = $.map(r.projects, function(p) { return String(p.id); });
var pid = r.projects[0].id;
var sessionId = (r.sessionTprojectID != null && String(r.sessionTprojectID) !== '')
? String(r.sessionTprojectID) : '';
if (sessionId && $.inArray(sessionId, ids) !== -1) { pid = sessionId; }
if (selectedId && $.inArray(String(selectedId), ids) !== -1) { pid = String(selectedId); }
sel.val(pid);
loadUsers(pid);ids is string-normalized (String(p.id)) so the comparison never suffers jQuery's
strict inArray type mismatch between a URL string and a JSON number; pid is likewise
a string, which sel.val() and loadUsers() both accept unchanged. An explicit URL param
still beats the session — mirroring legacy's if (!$argsObj->featureID) guard, i.e.
the session only fills the gap when the request carries no usable featureID.
No new i18n keys: nothing user-facing was added, so no locale bundle was touched.
| case | observed |
|---|---|
| no param, session = 1 (the repro) | combo 1 "Analyzer public project", grid renders project 1 — was 2 pre-fix |
| BFF payload |
sessionTprojectID: 1 present in the key list |
| no param, session = 2 | combo 2
|
?tproject_id=2, session = 1 |
combo 2 — the URL still wins |
?tproject_id=999, session = 1 |
combo 1 — an id outside the assignable combo falls back to the session project, never to a phantom 999
|
resolution matrix (the exact loadProjects() expression replayed in-page against the real payload shape, 7 rows: session 1 / session 2 / session 0 / key absent / session 99 not assignable / param 2 + session 1 / param 999 + session 1) |
1, 2, 2, 2, 2, 2, 1 — every row matches legacy precedence |
| syntax gates |
php -l api/roles/index.php clean; extracted inline <script> → node --check clean |
regression — usersAssignPlan.html, rolesView.html
|
0 console errors/warnings on both |
Event Viewer / events
|
log_level in (1,2,3) → 0 rows |

Not covered / remaining. The "no assignable project at all" branch would need a second,
non-privileged user holding a project role on only one of the two fixtures; that branch
short-circuits at if (!r.projects || !r.projects.length) before the resolution code, is
untouched by this change, and is already covered by #1621's suite.
- Refs #924 / #935 / #936 / #937 (rights filtering of the combo) — the list this resolution picks from is unchanged.
- Refs #1643 / #1644 / #1707 — the sibling parities on the same screen, all reading the same
GET /meta/tproject-rolespayload. - #1609/#1610/#1611/#1612 — the plan-screen twins of the same modernization chapter.