Repository navigation
Feature Issue 987 projectsView Notes Column
Status: implemented, verified, pushed
Issue: #987 — Implement Notes (description) column in projectsView list (gap vs legacy)
Branch: task/issue-987
Test suite: tmp/TLU_Test_Cases.md → Suite 987 (16/16 PASS)
Follow-up to: #986 — whose §5 Out of scope explicitly deferred this work here.
Issue #986 rebuilt the grid and listed what it deliberately left out. Two legacy
list decorations were still missing from gui/templates/projectsView.html:
| Legacy source | Capability | Modern state before this change |
|---|---|---|
dashio/project/projectView.tpl:88 |
<th {#NOT_SORTABLE#}>{$labels.th_notes}</th> — a Notes column header |
header had 7 <th>, no Notes |
projectView.tpl:113-115 |
the Notes cell: {if $gui->editorType == 'none'}{$testproject.notes|nl2br}{else}{$testproject.notes}{/if}
|
dropped entirely — the description was only visible inside the edit modal |
projectView.tpl:104 |
<i class="fas fa-cubes" style="cursor: help" title="API {$tlCfg->api->id_format|replace:"%s":$testproject.id}"></i> on the name cell |
dropped — the name cell was a bare <strong>name</strong>
|
Measured before the change (Chrome, evaluate_script on
/gui/templates/projectsView.html):
{"ths":["ID","Project Name","Prefix","Issue Tracker","Code Tracker","Status","Actions"],
"rowCount":4,
"firstRow":["#6","Fixture Alpha","AL","none","none","Active","Info\n Edit\n Deactivate\n Delete"]}No BFF change was needed. api/projects/index.php:88 already selects
tp.notes in projectSelect() and :115 already serialises it as
'description' => $row['notes'] ?? '' in formatProject(). The data reached the
browser and was thrown away by the row builder, so this is a front-end-only
change with no API contract impact.
-
New header
<th data-i18n="proj.notes">Notes</th>, placed directly afterProject Name, exactly where legacy puts it (projectView.tpl:88,89). Legacy marks that header{#NOT_SORTABLE#}, so the column is deliberately neither sortable nor per-column filterable — nodata-col-filter="1", andcolumnDefsgives itorderable: falsealongside ID and Actions. -
notesCell(p)(:425-431) renders the value:function notesCell(p) { const notes = (p.description || '').trim(); if (!notes) { return `<span class="col-notes" aria-label="${TLi18n.t('proj.notes')}"></span>`; } return `<span class="col-notes" title="${esc(notes)}">${esc(notes)}</span>`; }
-
esc()on both the text and thetitle— legacy trusted the DB ({$testproject.notes}raw inside a<td>), the modern grid escapes everywhere. Verified: a note containing<b>markup</b>renders as literal text, no<b>element is created. -
.trim()becauseapi/projectscan hand back''/null; the empty branch gets the-placeholder from CSS.
-
-
Multi-line parity. Legacy runs the notes through
nl2brwhen the project editor isnone. The modern equivalent iswhite-space: pre-line, which preserves the author's newlines without injecting<br>into the DOM. -
Truncation. The cell is clamped to 2 lines (
-webkit-line-clamp: 2,max-width: 260px,overflow: hidden) and the full text goes in thetitle, so one 2 KB note cannot blow up a row height while the legacy content stays fully readable. The clamp must live on the inner<span>, not on the<td>:display: -webkit-boxon a<td>cancels the table-cell layout and silently drops thevertical-align: middlethat#projectsTable.dataTable tbody tdsets, which measurably misaligned the whole column by 13px against every other cell. The<td>only carriesmax-width/min-width; the span carries the clamp and thespan.col-notes:empty::before { content:'-' }placeholder (the<td>is never:empty, so the placeholder has to target the span). -
columnDefsgained{ targets: 2, className: 'col-notes', orderable: false }so the class lands on the<td>of exactly that column.
-
Legacy
projectView.tpl:104renderstitle="API {$tlCfg->api->id_format|replace:'%s':$id}", andconfig.inc.php:637sets$tlCfg->api->id_format = "[ID: %s ]"— so legacy showsAPI [ID: 6 ]. -
projectRow()follows the established Dashio pattern already used by the two modernized siblings that carry the same icon (gui/templates/plans/buildsView.html:308-309,gui/templates/platforms/platformsView.html:254-255):const apiIcon = '<i class="fas fa-cubes api-id" title="' + esc(TLi18n.t('proj.apiId').replace('{id}', p.id)) + '"></i>';
with the shared
.api-id { cursor: help; color: #999; font-size: 12px; margin-right: 6px; }rule (same declaration asbuildsView.html:31/platformsView.html:31) andproj.apiId = "[ID: {id}]", mirroring the existingpl.apiId = "[ID: {id}]". Thecursor: helpcomes from legacy's inlinestyle="cursor: help".
Inserting a column shifted every index to its right, so two hardcoded numbers had to move (this is the class of bug a new column always introduces):
- the loading placeholder
colspan="7"→colspan="8"(:161) -
columnDefs:{ targets: 6, orderable: false }(actions) →targets: 7, and the new{ targets: 2, className: 'col-notes', orderable: false }
Regression-checked after the shift: the Actions column is still the
non-sortable one, the Edit modal still opens with the description pre-filled,
and the Info popup (projectInfoView.html?tproject_id=6) still opens.
New keys in all 10 bundles, each inserted in alphabetical position:
| key | value | note |
|---|---|---|
proj.notes |
en/de/es/fr/it/ja/pt/ro/ru/zh: Notes / Notizen / Notas / Notes / Note / 備考 / Notas / Notițe / Заметки / 备注 | real translation, legacy $labels.th_notes
|
proj.apiId |
[ID: {id}] in all 10 |
not translated: it is the format string of config.inc.php:637 $tlCfg->api->id_format = "[ID: %s ]", exactly like the existing pl.apiId = "[ID: {id}]"
|
A separate key from the existing proj.description ("Description") was
required: that one labels the edit modal field, whereas legacy's list header is
$labels.th_notes = "Notes". Reusing proj.description would have relabelled
the modal as well.
Each bundle was inserted line-wise (not re-serialised) so the diff is one line
per key per locale — these files are shared with concurrent CI agents. All 10
files re-validated with python3 -m json.tool; each has exactly one proj.notes
and one proj.apiId.
-
testprojects.api_keyisUNIQUEwith a shared default value (0d8ab81dfa2c77e8235bc829a2ded3edfa2c78235bc829a27eded3ed0d8ab81d). A fixture INSERT that omits the column succeeds once and then dies withDuplicate entry '0d8ab81d…'on the second row. Every seeded project needs its ownapi_key. -
nodes_hierarchyin 2.0.1 hasnode_type_id, not legacynodetype— the same 1.9.20-era fixture SQL does not run here. -
esc()must cover thetitleattribute too, otherwise a note containing a double quote breaks out of the attribute.esc()is applied to both. -
:emptydoes not match the<td>when the cell content is wrapped in a<span>(needed for the clamp). First attempt rendered no placeholder for empty projects; measured::beforecontent was"none"until the selector was moved to> .col-notes:empty::before. -
The global search listens on
keyup, notinput, so a scriptedinputevent produces a false "search does not work" FAIL — it must be dispatched asKeyboardEvent('keyup')(same trap as #986 gotcha 4). -
Code review caught four real defects in the first implementation (all
fixed before landing):
a.
display: -webkit-boxon the<td>brokevertical-align: middleand measurably misaligned the column by 13px — the clamp belongs on the span. b. The tooltip was hardcoded English"API "(i18n rule 3) and used a one-offproject-api-iconclass instead of the shared.api-id. c. The first tooltip valueAPI testproject/6was wrong: legacyconfig.inc.php:637id_formatis"[ID: %s ]", so legacy showsAPI [ID: 6 ]. The code comment also cited aformatStringId()inlib/functions.php— neither the function nor the file exists in this repo; the comment was fabricated and has been removed. d. The column was made sortable + per-column filterable, but legacy marks the Notes header{#NOT_SORTABLE#}— a false parity claim in the first CHANGELOG entry and docs, corrected to match legacy.
Suite tmp/TLU_Test_Cases.md → Suite 987: 16/16 PASS, over 4 fixtures
(2-line note with markup, short note, empty note, 320-byte note).
| Check | Measured |
|---|---|
| header | ["ID","Project Name","Notes","Prefix","Issue Tracker","Code Tracker","Status","Actions"] |
| multi-line note | "Main regression project.\nSecond line of notes <b>with markup</b> & an ampersand." |
| newline preservation | computed white-space: pre-line
|
| escaping | hasRealBold: false |
full text in title
|
identical to the stored note |
| truncation | span clientHeight 30 vs scrollHeight 225, -webkit-line-clamp: 2
|
| empty note | span ::before content "-"
|
| escaping (adversarial) | note "><img src=x onerror=alert(1)> produced 0 <img> elements, no attribute breakout |
| API tooltip |
[ID: 6] … [ID: 9] (legacy id_format) |
Notes not sortable / not filtered (legacy NOT_SORTABLE) |
no aria-sort on the th, cursor: auto, filter inputs stay at 5 (the Notes header has no data-col-filter) |
| vertical alignment | every <td> of the row measured at top 307 / height 57 — Notes no longer offset |
| other columns still filterable | per-column filter inputs 5, prefix/name/status filters unaffected |
| Notes sort asc / desc (regression: the other columns still sort) | name sort re-verified, aria-sort present on the sortable headers |
global search Gamma / regression
|
["#8"] / ["#6"] (notes text is searchable) |
| Actions sortability | no aria-sort (still orderable: false) |
| Edit modal | opens "Edit Test Project", description pre-filled |
| Info popup | opens projectInfoView.html?tproject_id=6
|
| console | no error / warn
|
| Event Viewer |
events unchanged — only the login audit row (log_level 16) |
Screenshots: docs/screenshots/issue-987-projectsview-before.png (7-column
list) and docs/screenshots/issue-987-projectsview-after.png (Notes column +
cube tooltips).
-
#988 — requirement-feature quick toggle in the projectsView list
(legacy
projectView.tpl:118-124). - #990 — API key display in the projectsView edit modal.