Task Issue 939 Assign Plan Roles Pagination Search - sebiboga/testlink-upgraded GitHub Wiki

Task 939 — DataTables pagination + "User" search on Assign Test Plan Roles (gap vs legacy)

Issue: #939 Status: IMPLEMENTED & VERIFIED (2026-09-19) — branch task/issue-939

The gap

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:116 language.search);
  • sortable user columns with search disabled on the role column (legacy columnDefs: [{searchable:false, targets:1}], usersAssign.tpl:115-117);
  • stateSave persistence 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.

Modern implementation

Landed on task/issue-939 (gui/templates/usermanagement/usersAssignPlan.html, diff +151/-51):

  • DataTables 1.13.7 bootstrap build in <head> (same CDN as usersAssignProject.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]], pageLength 20, order: [[1,'asc']] (login asc — same default as usersAssignProject.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 = localized assign.searchUsers ("User");
    • view state is preserved explicitly, not via stateSave: legacy DataTables.inc.tpl set stateSave:true for 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 and renderUsersTable(keep) re-applies them — an in-plan bulk "Do" keeps the user's view exactly like the legacy pure-DOM set_combo_group(); loadUsers never passes keep, 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 (plus roles[]) as the single source of truth and re-creates every override select from the model:
    • rolesOptions(u, chosenVal) — one value-0 assign.noOverride option, 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 live td.innerHTML (browsers do not serialise the current <option selected> into innerHTML).
    • onRoleChange(sel, uid) — updates userById[uid].roleVal, applies the .changed class + "Modified" badge via createdRow, and re-syncs the cell: dt.cell($(sel).closest('tr'), 4).data(buildSelectHtml(u), false).
    • applyBulkRole() (the #938 "Set roles to / Do" control) and saveAssignments() 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-roles already returns the full active-user list with roleID/effectiveRoleID/isInherited/isAdmin and 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); existing assign.searchUsers, assign.noUsers, common.modified, common.showEntries/entries/pageInfo/prev/next/filter reused. Every bundle validated with python3 -m json.tool.

Screenshots

Assign Test Plan Roles — DataTable with 20/40/60/All entries menu, "User" search box, pagination 1/2 (26 users):

issue-939-usersAssignPlan-datatables

"User" search box filtering the grid:

issue-939-usersAssignPlan-search

Verification evidence

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.

  1. Legacy chrome: search label "User"; entries menu 20/40/60/All; Showing 1 to 20 of 26 entries; pages 1/2; footer "26 users".
  2. Search (user columns only): u93901 → 1 row; test designer (a role shown inside the selects) → 0 rows; u9391 → 10 rows u93910..u93919; "filtered from 26 total".
  3. Sorting: Login header cycles asc → desc → original; default login asc (admin, u93901…). Sort survives paging/search.
  4. Pagination + entries: page 2 = 6 rows u93920..u93925; All = 26 rows; round-trips stable.
  5. Edits survive paging: change u93902→4 and u93920→7 on different pages; return to page 1 — both choices + "Modified" badges + Save enabled persist.
  6. Bulk "Do" + Save on ALL pages: after bulk → Save → user_testplan_roles for 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 in applyBulkRole (Save stayed disabled after consecutive bulk flows).
  7. i18n + empty state + Event Viewer: ?locale=ro shows 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.
  8. Cross-plan isolation: on PLAN939-R1 set search u9391 (10 rows); switch to PLAN939-R2 → search box empty, 26 rows, "26 users" footer; switch back to PLAN939-R1 → also clean (no stateSave in the modern switcher); an in-plan bulk "Do" instead keeps the current search/page (view preserved by applyBulkRole capture + 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).

Related issues

  • #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.

⚠️ **GitHub.com Fallback** ⚠️