fix: replace grid action column with link-column on primary data cell

Remove the dedicated "Edit"/"Open" action button column from all list
pages. Instead, wrap the primary data column (name, description, subject,
etc.) in a clickable <a> link via the new rowInteraction.linkColumn option.

Double-click, Enter-key, and click-to-focus row navigation preserved.
Pages with explicit actions config are unaffected. Address-book opts out
of auto-wrapping (linkColumn: false) and renders the link in its own
formatter to avoid nested <a> with avatar lightbox.

Also fixes pre-existing XSS in insertActionColumn (unescaped href/label)
and updates typography test for --text-page-title token change.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-24 19:43:03 +01:00
parent af36583862
commit 1c5648c727
16 changed files with 87 additions and 85 deletions

View File

@@ -3,7 +3,7 @@ import { initMultiSelectCascade } from '../../../../js/components/app-multiselec
import { initStandardListPage } from '../../../../js/core/app-grid-factory.js';
import { readPageConfig } from '../../../../js/core/app-page-config.js';
import { warnOnce } from '../../../../js/core/app-dom.js';
import { getAppBase, escapeHtml, buildBadgeList, gridFilterMultiCsv, gridFilterText } from '../../../../js/pages/app-list-utils.js';
import { getAppBase, escapeHtml, buildBadgeList, gridFilterMultiCsv, gridFilterText, withCurrentListReturn } from '../../../../js/pages/app-list-utils.js';
const config = readPageConfig('address-book-index');
if (config) {
@@ -50,7 +50,7 @@ if (config) {
{
name: labels.name || 'Name',
sort: true,
formatter: (cell) => {
formatter: (cell, row) => {
const nameValue = typeof cell === 'object' && cell !== null
? (cell.name ?? '')
: cell;
@@ -58,13 +58,18 @@ if (config) {
? (cell.avatar_uuid ?? '')
: '';
const name = escapeHtml(nameValue || '-');
const uuid = row?.cells?.[uuidIndex]?.data;
const editUrl = uuid ? withCurrentListReturn(new URL(`address-book/view/${uuid}`, appBase).toString()) : '';
const nameHtml = editUrl
? `<a class="app-grid-link-cell" href="${escapeHtml(editUrl)}">${name}</a>`
: `<span>${name}</span>`;
if (!avatarUuid) {
const initials = escapeHtml(initialsForName(nameValue));
return gridjs.html(`<span class="grid-name-cell"><span class="grid-avatar grid-avatar-placeholder">${initials}</span><span>${name}</span></span>`);
return gridjs.html(`<span class="grid-name-cell"><span class="grid-avatar grid-avatar-placeholder">${initials}</span>${nameHtml}</span>`);
}
const src = new URL(`admin/users/avatar-file?uuid=${avatarUuid}&size=64`, appBase).toString();
const full = new URL(`admin/users/avatar-file?uuid=${avatarUuid}&size=256`, appBase).toString();
return gridjs.html(`<span class="grid-name-cell"><a data-fslightbox="address-book-avatars" href="${full}"><img class="grid-avatar" src="${src}" alt="" loading="lazy"></a><span>${name}</span></span>`);
return gridjs.html(`<span class="grid-name-cell"><a data-fslightbox="address-book-avatars" href="${full}"><img class="grid-avatar" src="${src}" alt="" loading="lazy"></a>${nameHtml}</span>`);
},
},
{ name: labels.email || 'Email', sort: true },
@@ -99,6 +104,7 @@ if (config) {
rowDataset: (row) => ({
uuid: row?.cells?.[uuidIndex]?.data,
}),
rowInteraction: { linkColumn: false },
rowDblClick: {
getUrl: (rowData) => {
const uuid = rowData?.cells?.[uuidIndex]?.data;