Task Issue 939 Assign Plan Roles Pagination Search - sebiboga/testlink-upgraded GitHub Wiki
Issue: #939
Status: IMPLEMENTED & VERIFIED (2026-09-19) — branch task/issue-939
Legacy gui/templates/dashio/usermanagement/usersAssign.tpl:111-120 builds the
plan-roles user grid as a DataTable (via DataTables.inc.tpl) because the
shared $tlCfg->gui->usersAssign block defaults pagination->enabled to true
(config.inc.php:663-666, items_per_page[values] = '[20, 40, 60, -1], [20, 40, 60, "All"]').
That DataTable gives:
- an entries-per-page menu 20 / 40 / 60 / All;
- a search box labelled "User" (
usersAssign.tpl:116language.search); - sortable user columns with search disabled on the role column (legacy
columnDefs: [{searchable:false, targets:1}],usersAssign.tpl:115-117); -
stateSavepersistence of page/search/entries across reloads (DataTables.inc.tpl).
The modern screen usersAssignPlan.html rendered a static
<table id="assignTable"> with no search box, no pagination, no sorting —
a gap for projects with many users. The sibling project screen closed the same
gap in #930; the plan screen (which shares the legacy template and config) had a
parallel unresolved gap.
Landed on task/issue-939 (gui/templates/usermanagement/usersAssignPlan.html, diff +151/-51):
-
DataTables 1.13.7 bootstrap build in
<head>(same CDN asusersAssignProject.html:10,118-119/cfieldsAssignView.html:9,166-167). -
renderUsersTable()initialises#assignTable(only when users exist and pagination is enabled — empty state and the pagination-disabled fallback stay plain tables):-
lengthMenu: [[20,40,60,-1],[20,40,60,assign.all]],pageLength20,order: [[1,'asc']](login asc — same default asusersAssignProject.html); -
columnDefs: ordinal (0) + override-select (4) not orderable; the inherited-role column (3) and Plan Role Override column (4) not searchable — only Login (1) + Last Name (2) are searched, replicating the legacy "searchable only on the user column" rule; -
language.search= localizedassign.searchUsers("User"); -
view state is preserved explicitly, not via
stateSave: legacyDataTables.inc.tplsetstateSave:truefor full page navigations, but the modern same-page plan switcher must never leak the previous plan's page/search into the next plan.applyBulkRole()captures search/len/order/page before the re-render andrenderUsersTable(keep)re-applies them — an in-plan bulk "Do" keeps the user's view exactly like the legacy pure-DOMset_combo_group();loadUsersnever passeskeep, so every project/plan load starts a clean grid (cross-plan isolation verified with the fixture's second plan PLAN939-R2).
-
-
State-driven data model (the key decision). DataTables pagination keeps
only the current page in the DOM and caches cell HTML at init, so DOM-only
change tracking would drop edits made on page 2 when browsing back to page 1.
The screen keeps
users[]+userById(plusroles[]) as the single source of truth and re-creates every override select from the model:-
rolesOptions(u, chosenVal)— one value-0assign.noOverrideoption, then one option per assignable role; admin role (id 8) offered only as the row's current explicit assignment (issue #928); no<inherited>id-0 duplicate (issue #1545,filterTLRolesInherited). -
buildSelectHtml(u)— deterministic select markup generated from the model, NOT from livetd.innerHTML(browsers do not serialise the current<option selected>into innerHTML). -
onRoleChange(sel, uid)— updatesuserById[uid].roleVal, applies the.changedclass + "Modified" badge viacreatedRow, and re-syncs the cell:dt.cell($(sel).closest('tr'), 4).data(buildSelectHtml(u), false). -
applyBulkRole()(the #938 "Set roles to / Do" control) andsaveAssignments()iterate the model (users[]), so bulk "Do" applies to every non-admin user on ALL pages and Save persists overrides correctly; disabled global-admin rows stay excluded (issue #927).
-
-
BFF: none needed —
GET /roles/meta/tplan-rolesalready returns the full active-user list withroleID/effectiveRoleID/isInherited/isAdminand the assignable role catalog (legacy pre-filtered dataset parity). -
i18n:
assign.all= "All" added to all 10 locale bundles (gui/templates/i18n/{en,de,es,fr,it,ja,pt,ro,ru,zh}.json); existingassign.searchUsers,assign.noUsers,common.modified,common.showEntries/entries/pageInfo/prev/next/filterreused. Every bundle validated withpython3 -m json.tool.
Assign Test Plan Roles — DataTable with 20/40/60/All entries menu, "User" search box, pagination 1/2 (26 users):

"User" search box filtering the grid:

Fixture tmp/fixtures_939.php: project PLANROLES939 + active test plan
PLAN939-R1 (25 users u93901..u93925, global roles 4/6/7/9 cycled, all
active; overrides u93901=7, u93904=7, u93911=6, u93921=9) + second
active plan PLAN939-R2 (cross-plan isolation test); admin = locked
global-admin row. 26 active users ⇒ 2 pages at page size 20.
-
Legacy chrome: search label "User"; entries menu 20/40/60/All;
Showing 1 to 20 of 26 entries; pages 1/2; footer "26 users". -
Search (user columns only):
u93901→ 1 row;test designer(a role shown inside the selects) → 0 rows;u9391→ 10 rowsu93910..u93919; "filtered from 26 total". - Sorting: Login header cycles asc → desc → original; default login asc (admin, u93901…). Sort survives paging/search.
-
Pagination + entries: page 2 = 6 rows
u93920..u93925; All = 26 rows; round-trips stable. -
Edits survive paging: change
u93902→4 andu93920→7 on different pages; return to page 1 — both choices + "Modified" badges + Save enabled persist. -
Bulk "Do" + Save on ALL pages: after bulk → Save →
user_testplan_rolesfor plan 5 = 25 rows (admin absent); pre-edit values co-exist; second bulk+Save → all 25 = 9. Sub-check exposed and fixed a save-button regression inapplyBulkRole(Save stayed disabled after consecutive bulk flows). -
i18n + empty state + Event Viewer:
?locale=roshows Utilizator / "Toate" / "Se afiseaza 1 pana la 20 din 26 inregistrari"; zero active users → "No users in this project." placeholder, no DataTable wrapper, footer "0 users"; events only INFO(16)/AUDIT, zero rows log_level ≥ 32. -
Cross-plan isolation: on
PLAN939-R1set searchu9391(10 rows); switch toPLAN939-R2→ search box empty, 26 rows, "26 users" footer; switch back toPLAN939-R1→ also clean (nostateSavein the modern switcher); an in-plan bulk "Do" instead keeps the current search/page (view preserved byapplyBulkRolecapture +renderUsersTable(keep)).
Full manual pass: tmp/TLU_Test_Cases.md — Suite 939 — 8/8 PASS.
Code review (subagent) before commit: no blockers/majors; XSS-clean (all
user/role strings escaped via esc(), options built as strings).
- #930 — the identical gap closed for the Assign Test Project Roles screen.
- #927 — global-admin rows locked (disabled select, never bulk-applied/saved).
- #928 — admin role (id 8) excluded from assignable options unless current.
- #938 — bulk "Set roles to / Do" (pre-existing, retained; now model-driven).
- #1545 — duplicate value-0
<inherited>option fixed pre-#939.
Refs #939.