Task Issue 1631 Assign Plan Roles Disabled Toolbar Suppressed - sebiboga/testlink-upgraded GitHub Wiki
Issue: #1631
Status: IMPLEMENTED & VERIFIED (2026-09-26) — branch task/issue-1631, commits 73f35d580 + the post-code-review fixup (see Code review below)
Screen: gui/templates/usermanagement/usersAssignPlan.html (Assign Test Plan Roles)
BFF: unchanged — api/roles/index.php:870-912 already reported the legacy-equivalent plans: [] / projects: []
i18n: unchanged — no new user-facing string (both notices reuse existing localized keys)
The gap is the plan-screen twin of #1621, and the legacy guard is literally the same
one — the shared usersAssign.tpl served both the project and the plan context:
{* gui/templates/dashio/usermanagement/usersAssign.tpl:142 *}
{if $gui->features neq ''}
<form method="post" action="{$umgmt}/usersAssign.php" ...>
… Test Project combo + Test Plan combo (or the read-only project name),
… "Set roles to" combo, Do button, user grid, Save / warn_demo note …
</form>
{/if} {* :295, if $gui->features *}Controller side (lib/usermanagement/usersAssign.php, case 'testplan'):
-
:109-111—getTestPlanEffectiveRoles()returned NULL ⇒user_feedback = no_test_plans_available. It returns NULL only whenget_all_testplans()is null, i.e. the project in the session has no active plan at all. -
:123-127—count($gui->features) == 0⇒user_feedback = $gui->not_for_you(=testplan_roles_assign_disabled): active plans exist, but the caller cannot assign roles on any of them.
In both cases $gui->features is null/empty, so the guard is false and the legacy page
rendered title + menu + that single message and no controls whatsoever.
showPlanDisabled() (:257-267) |
hid the grid and showed the notice — left .toolbar on screen: the populated Test Project combo, the fully populated "Set roles to" combo and an enabled "Do" button, which applyBulkRole() could only run over an empty row set. The dead affordance #1621 had just removed on the project screen. |
loadProjects() (:226-240) |
no projects.length === 0 branch at all. A role_management-only user got projects: [] and kept the entire form live (enabled combo holding only the placeholder, 11-role bulk combo, enabled Do) over a blank work area — with neither the notice nor the empty hint rendered. |
| Element |
u1631a (no assignable plan) before |
after |
u1631b (no project at all) before |
after |
|---|---|---|---|---|
.toolbar |
ON-SCREEN | NOT-RENDERED | ON-SCREEN | NOT-RENDERED |
#bulkDoBtn |
ON-SCREEN, disabled === false
|
NOT-RENDERED | ON-SCREEN, disabled === false
|
NOT-RENDERED |
#bulkRoleSelect |
11 role options | — (hidden) | 11 role options | — (hidden) |
#projectSelect |
ON-SCREEN, T1631P selected |
hidden + disabled | ON-SCREEN, 1 placeholder option | hidden + disabled |
#disabledMsg |
ON-SCREEN, …do not allow you Assign Roles for Test Plans
|
ON-SCREEN (same) | NOT-RENDERED | ON-SCREEN, …do not allow you Assign Roles for Test Plans (see the code-review table: not_for_you, the same notice the sibling project screen uses) |
#tabsBar |
ON-SCREEN | ON-SCREEN (legacy menu outside the guard) | ON-SCREEN | ON-SCREEN |
BFF answers that drive it (no BFF change needed):
u1631a GET /api/roles/index.php/meta/tplan-roles?tproject_id=8&tplan_id=0
-> {"status":"ok","plans":[],"projects":[{"id":8,"name":"T1631P"}],"totalPlans":1}
u1631b GET /api/roles/index.php/meta/tplan-roles?tproject_id=0&tplan_id=0
-> {"status":"ok","plans":[],"projects":[],"totalPlans":0}
All in gui/templates/usermanagement/usersAssignPlan.html (+50 lines, 0 deletions).
-
showPlanDisabled()suppresses the whole form —$('#demoBanner').hide(); $('.toolbar').hide();plus#projectSelect.prop('disabled', true). The demo banner goes with the toolbar because the legacywarn_demonote sat inside the same guard (usersAssign.tpl:286-292) — the pairing #1621 established. The tab bar stays: legacy kept its menu outside the guard. -
New
projects.length === 0branch inloadProjects()— enters the same state. It passes theassign.rolesForPlansDisablednotice explicitly, because this state has no legacy rendering at all (legacy threw, see the review table) and the closest reachable legacy message isnot_for_you— the very one the sibling project screen shows for the identical condition (usersAssignProject.html:235→assign.rolesDisabled). The plan-count-driven messageassign.noUsablePlans(= the verbatim legacy$TLS_no_test_plans_available,locale/en_US/strings.txt:2151) is kept for the genuine "no active plan" case, where legacy did render it. -
New
showAssignForm()recovery helper —$('.toolbar').show(); $('#projectSelect').prop('disabled', false); $('#denyBox').hide(); applyDemoMode();wired into every transition out of the suppressed state:loadPlans()success,loadUsers()success callback (not on entry, so a 403 can still end in the deny box), the projectchangehandler's empty-value branch and the planchangehandler's empty-value branch. -
loadProjects()validates the requested project (see the review section below) — the ASIDE/tab entry point passes the stringtproject_id=0, which is truthy, so without validation it selected no project, landed in the suppressed state and, with the toolbar hidden, left the user with no way out. It now falls back to the first accessible project, exactly likeusersAssignProject.html:238-243and legacyusersAssign.php:380-397. -
The DataTables handle is dropped in
showPlanDisabled()before the<tbody>is emptied, so a later render can never destroy a DataTable over a hand-emptied body. -
A non-403 failure of the meta read now shows
#emptyMsginstead of leaving a live toolbar over a blank area — the sameelsebranch the sibling screen has (usersAssignProject.html:247).
The issue correctly warned that #1621's one-liner could not simply be copied here, because this
screen keeps its own in-page project/plan switchers while the legacy page reloaded on every
context change. showAssignForm() therefore restores the form on every in-page transition
that leaves the suppressed state, and routes through applyDemoMode() instead of a bare
.show() — the demo banner, the Save button and the demoNote are mutually exclusive in legacy
and must be restored together.
Honest scope statement (corrected by the code review of this run): with the toolbar suppressed, the switchers it contains are hidden too, so in practice the suppressed state is left by a page load — the ASIDE entry, a tab, a reload — which is exactly what legacy did. The helper's real value is that no in-page transition can leave a half-suppressed DOM behind, and that the demo triple is restored atomically. It is not a claim that the hidden switchers themselves can revive the form; the code review of the first commit pointed this out and the commit message, CHANGELOG and this page were corrected accordingly.
An independent subagent review of 73f35d580 returned one BLOCKER, two MAJOR and several MINOR
findings; the following were fixed in the follow-up commit:
| Severity | Finding | Resolution |
|---|---|---|
| BLOCKER |
?tproject_id=0 — the ASIDE entry point (aside.tpl:73 → getActions()) and every tab bar (tp || '0') pass the string "0", which is truthy, so loadProjects() selected no project → showPlanDisabled() → toolbar hidden → permanently dead page (a regression versus the pre-fix toolbar) |
requested project is now validated against the accessible list and falls back to the first one (loadProjects()), like the sibling screen |
| MAJOR | the "recovery" guarantee was overstated (see above) | wording corrected in the code, commit message, CHANGELOG and this page |
| MAJOR | the projects: [] notice was claimed as legacy parity, but init_args() (usersAssign.php:173-175) throws "INVALID Test Project ID" when the session has no test project, so legacy never rendered that state |
the branch now shows the not_for_you notice — the same one the sibling project screen shows for the identical condition — and the divergence is documented as intentional |
| MINOR | stale DataTables instance in the suppressed state |
assignDt.destroy() added to showPlanDisabled()
|
| MINOR |
showAssignForm() did not clear #denyBox
|
added |
| MINOR | non-403 failures of the meta read left a live toolbar over a blank area |
else { $('#emptyMsg').show(); } added, like the sibling screen |
| MINOR |
showAssignForm() ran on loadUsers() entry, flashing the toolbar before a 403 could still deny |
moved into the success callback |
| MINOR | comments referenced a non-existent showNoProjects() and a "below" declaration that is above |
corrected |
| NIT |
.show() on a display:flex element |
verified not a regression: jQuery restores the stylesheet display (.toolbar measured back as flex, inline "") |
The two notices are the existing localized keys:
assign.rolesForPlansDisabled (= $TLS_testplan_roles_assign_disabled) and
assign.noUsablePlans (= $TLS_no_test_plans_available, identical English text). No new string, so
nothing to add to the ten bundles.
| # | Case | Measured |
|---|---|---|
| 1 |
primary u1631a, project visible, no assignable plan |
toolbarVisible:false, doBtnVisible:false, notice ON-SCREEN with …do not allow you Assign Roles for Test Plans, tabs kept |
| 2 |
primary u1631b, no accessible project at all |
toolbarVisible:false, projectSelDisabled:true, notice ON-SCREEN There are no usable test plans on this test project
|
| 3 | regression, admin normal state (project 12 / plan 13) |
toolbar ON-SCREEN, projectVal:"12", planVal:"13", 3 grid rows, 11 bulk options, no notice, footer 3 users
|
| 4 | recovery suppressed → re-select the project | suppressed toolbar:false/msg:true/rows:0 → after $('#projectSelect').val('12').trigger('change') toolbar:true/msg:false/rows:3
|
| 5 | recovery suppressed → clear the project combo |
toolbar:true, #emptyMsg ON-SCREEN, project re-enabled, plan combo disabled |
| 6 | regression, bulk "Do" + Save | model [admin:0 skipped, u1631a:7, u1631b:7], Save enabled, toast "User Roles updated", user_testplan_roles = (7,13,7) (8,13,7)
|
| 7 | regression, demoMode pairing (demoMode=true; applyDemoMode()) |
demo on {banner:true, save:false, note:true} → suppressed {banner:false, toolbar:false, note:false} → recovered {banner:true, toolbar:true, note:true, save:false}
|
| 8 | regression, sibling usersAssignProject.html (untouched) |
toolbarVisible:true, 3 rows, no notice |
| 9 | syntax gates |
node --check on the extracted inline script OK; php -l tmp/fixtures_1631.php OK |
| 10 | Event Viewer | whole browser phase: 27 rows, all log_level=16 (INFO) — no new Error/Warning |
| 11 | browser console | 0 JS errors after the fix |
php tmp/fixtures_1631.php
# private test project T1631P (prefix T16) + ACTIVE plan T1631P-P1
# role rm1631 = role_management only -> user u1631b (no project at all)
# role pm1631 = role_management + user_role_assignment -> user u1631a (project, no assignable plan)
# both users: password "testlink"Entry points (hit http://localhost:8082/logout.php first — TestLink ignores a login POST
over a live session):
- suppressed state A →
u1631a/testlink→…/usersAssignPlan.html?tproject_id=<T1631P id> - suppressed state B →
u1631b/testlink→…/usersAssignPlan.html - normal state →
admin/admin→ same URL
-
$db->exec_query("SELECT id …")returns a record set, not an int.intval()on it yields 1, so a reset block written asDELETE FROM users WHERE id = intval(exec_query(...))silently deletes user id 1 (admin). Always resolve ids through a helper that returns 0 when there is no row. (Cost me a broken admin login on this run — see below.) - This run's freshly imported DB shipped an
adminrow whose bcrypt hash does not verifyadmin(password_verify()→ false), soadmin/adminreturned "Invalid login or password". Restored locally withUPDATE users SET password=password_hash('admin') WHERE login='admin'. Verify the admin login before trusting any 401/403 you see in a fresh run.
Before — u1631a (project visible, no assignable plan): the
testplan_roles_assign_disabled notice and a live toolbar with the 11-role combo and an
enabled "Do" button.

Before — u1631b (no accessible project at all): a live toolbar over a completely blank
work area, no notice at all.

After — u1631a: the whole form is suppressed, only the tab bar and the notice remain
(legacy parity: title + menu + one message).

After — u1631b: the same suppressed state, re-taken after the code-review fixup so it
shows the corrected not_for_you notice.

Test suite: tmp/TLU_Test_Cases.md, section
Task — Issue #1631: suppress the whole assignment toolbar in the
no-assignable-plans state (Assign Test Plan Roles).