Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions web/pages/admin.edit.mod.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,6 @@

global $userbank, $theme;

new \Sbpp\View\AdminTabs([], $userbank, $theme);

require_once __DIR__ . '/_admin_edit_helpers.php';

$modId = isset($_GET['id']) ? (int) $_GET['id'] : 0;
Expand Down
2 changes: 1 addition & 1 deletion web/tests/e2e/specs/flows/mod-delete-confirm.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@ test.describe('flow: admin mod delete confirm modal (#1397 — RemoveMod zombie)
// Count badge decrements by exactly one (defensive — the
// chrome's `decrementCount` reads the span's text and writes
// back `n - 1`).
await expect(countBadge).toHaveText(String(startingCount - 1));
await expect(countBadge).toHaveText(`(${startingCount - 1})`);

// Success toast surfaces. Anchor on `data-kind="success"` plus
// a hasText filter on the title our handler emits ("Mod
Expand Down
9 changes: 4 additions & 5 deletions web/tests/integration/ModsDeleteDialogTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -218,18 +218,17 @@ public function testPageTailScriptUsesActionsConstant(): void
* The mod count badge must carry `data-testid="mod-count"` so the
* page-tail script can decrement it after a delete and the E2E
* spec can read it as the pre / post-delete oracle. Without the
* testid the badge is unreachable in a theme-agnostic way (the
* surrounding `<p>… configured</p>` shape is too brittle to
* regex against).
* testid the badge is unreachable in a theme-agnostic way. The
* rendered text is `(N)` (same shape as the Admins list badge).
*/
public function testCountBadgeCarriesTestid(): void
{
$html = $this->renderModsPage();

$this->assertMatchesRegularExpression(
'/<span[^>]*data-testid="mod-count"[^>]*>\s*\d+\s*<\/span>/',
'/<span[^>]*data-testid="mod-count"[^>]*>\s*\(?\d+\)?\s*<\/span>/',
$html,
'The mod count number must be wrapped in <span data-testid="mod-count">.'
'The mod count number must be wrapped in <span data-testid="mod-count"> (optionally parenthesised, matching the Admins list badge).'
);
}

Expand Down
7 changes: 7 additions & 0 deletions web/themes/default/css/theme.css
Original file line number Diff line number Diff line change
Expand Up @@ -2130,6 +2130,11 @@ details.queue-row > summary > .row-actions {
.admins-list-card:last-child { border-bottom: none; }
.admins-list-card__body { padding: 0.75rem 1rem 0.25rem; }

.mods-list-cards { display: none; }
.mods-list-card { border-bottom: 1px solid var(--border); }
.mods-list-card:last-child { border-bottom: none; }
.mods-list-card__body { padding: 0.75rem 1rem 0.25rem; }

/* ---- Responsive ---- */
[data-mobile-menu] { display: none; }
@media (max-width: 1024px) {
Expand All @@ -2148,6 +2153,7 @@ details.queue-row > summary > .row-actions {
dance as `.ban-cards` — hidden at desktop, block at mobile. */
.log-cards { display: block; }
.admins-list-cards { display: block; }
.mods-list-cards { display: block; }
/* #1181: filter chip rows wrap onto multiple lines on mobile
instead of horizontal-scrolling, so every chip is reachable
without a swipe. The .scroll-x desktop affordance is the
Expand All @@ -2159,6 +2165,7 @@ details.queue-row > summary > .row-actions {
.ban-cards { display: none; }
.log-cards { display: none; }
.admins-list-cards { display: none; }
.mods-list-cards { display: none; }
}

/* ---- Utility classes used by templates ---- */
Expand Down
21 changes: 13 additions & 8 deletions web/themes/default/page_admin_edit_mod.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -27,21 +27,26 @@
template variable — no MooTools-era `$('enabled').checked = …`
re-paint script.
*}
<div class="page-section">
<div class="page-section" data-testid="editmod-section" style="max-width:48rem">
<div class="mb-6">
{* nofilter: name is htmlspecialchars'd on store in admin.edit.mod.php;
auto-escaping here would double-encode (#1108 / #1113). *}
<h1 style="font-size:var(--fs-xl);font-weight:600;margin:0" data-testid="editmod-title">
Edit mod · {$name nofilter}
</h1>
<p class="text-sm text-muted m-0 mt-2">
Update the configuration for this game mod.
</p>
</div>

<form method="post"
action=""
enctype="multipart/form-data"
autocomplete="off"
data-testid="editmod-form">
{csrf_field}
<div class="card">
<div class="card__header">
<div>
<h3>Edit Mod</h3>
<p>Update the configuration for this game mod.</p>
</div>
</div>
<div class="card__body space-y-4" style="max-width:42rem">
<div class="card__body space-y-4">
<input type="hidden" name="insert_type" value="add">

{* nofilter: mod metadata is htmlspecialchars(strip_tags($_POST[…]))'d in admin.edit.mod.php before INSERT/UPDATE, so values pulled back out of `:prefix_mods` are already entity-encoded; auto-escaping the value attribute would double-encode (#1113 audit). The id="icon_hid" element is the channel the popup uploader writes into via window.opener.icon(). *}
Expand Down
19 changes: 11 additions & 8 deletions web/themes/default/page_admin_mods_add.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -42,14 +42,23 @@
without depending on element ids. The icon-callback hidden input is
`#icon_hid` to mirror the edit-mod template.
*}
<div class="page-section">
<div class="page-section" data-testid="addmod-section" style="max-width:48rem">
{if NOT $permission_add}
<div class="card">
<div class="card__body">
<p class="text-muted">Access denied.</p>
</div>
</div>
{else}
<div class="mb-6">
<h1 style="font-size:var(--fs-xl);font-weight:600;margin:0" data-testid="addmod-title">
Add mod
</h1>
<p class="text-sm text-muted m-0 mt-2">
Configure a new game mod that can be assigned to bans and servers.
</p>
</div>

<form method="post"
action=""
enctype="multipart/form-data"
Expand All @@ -58,13 +67,7 @@
data-testid="addmod-form">
{csrf_field}
<div class="card">
<div class="card__header">
<div>
<h3>Add Mod</h3>
<p>Configure a new game mod that can be assigned to bans and servers.</p>
</div>
</div>
<div class="card__body space-y-4" style="max-width:42rem">
<div class="card__body space-y-4">
{* #1402: the legacy `<input id="fromsub">` hidden was a vestigial
reference to the v1.x ProcessMod() flow; the new submit handler
has no equivalent. Replaced with `#icon_hid` so the upload-icon
Expand Down
127 changes: 104 additions & 23 deletions web/themes/default/page_admin_mods_list.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -43,15 +43,20 @@
</div>
</div>
{else}
<div class="card">
<div class="card__header">
<div>
<h3>Server Mods</h3>
<p><span data-testid="mod-count">{$mod_count}</span> configured</p>
</div>
</div>
<div class="mb-4">
<h1 style="font-size:var(--fs-xl);font-weight:600;margin:0" data-testid="mods-list-title">
Mods
<span class="text-faint" style="font-weight:400;margin-left:0.375rem" data-testid="mod-count">({$mod_count})</span>
</h1>
<p class="text-sm text-muted m-0 mt-2">
Game mods that can be assigned to bans and servers.
</p>
</div>

<div class="card" style="overflow:hidden">
{if $mod_count > 0}
<table class="table" data-testid="mods-table">
<div class="table-scroll">
<table class="table table--compact" data-testid="mods-table">
<thead>
<tr>
<th style="width:40%">Name</th>
Expand All @@ -67,11 +72,11 @@
{foreach from=$mod_list item=mod}
<tr id="mid_{$mod.mid}" data-testid="mod-row" data-id="{$mod.mid}">
<td>
<div class="flex items-center gap-3">
<div class="flex items-center gap-2">
<img src="images/games/{$mod.icon}"
alt=""
width="20"
height="20"
width="18"
height="18"
loading="lazy"
onerror="this.style.visibility='hidden'">
<span class="font-medium">{$mod.name}</span>
Expand All @@ -87,12 +92,20 @@
{/if}
</td>
{if $permission_editmods || $permission_deletemods}
<td style="text-align:right">
<div class="flex justify-end gap-2">
<td class="col-actions" style="text-align:right">
{* Icon-only row actions matching banlist / admins list:
Lucide icon + `data-tooltip` + `aria-label` inside
`.row-actions--icons`. Keep `data-testid` / `data-action`
/ `data-fallback-href` wiring unchanged. *}
<div class="row-actions row-actions--icons">
{if $permission_editmods}
<a class="btn btn--ghost btn--sm"
<a class="btn btn--ghost btn--icon btn--sm"
href="index.php?p=admin&c=mods&o=edit&id={$mod.mid|escape:'url'}"
data-testid="editmod-link">Edit</a>
data-testid="editmod-link"
data-tooltip="Edit"
aria-label="Edit mod {$mod.name|escape}">
<i data-lucide="pencil" style="width:14px;height:14px"></i>
</a>
{/if}
{if $permission_deletemods}
{* #1397: data-action wires the delete button to the inline
Expand All @@ -110,14 +123,17 @@
beyond the bug; the fallback is a graceful degradation
for the rare case where the JSON dispatcher itself is
missing (e.g. third-party theme that stripped api.js). *}
<button class="btn btn--ghost btn--sm"
<button class="btn btn--ghost btn--icon btn--sm"
type="button"
data-action="mod-delete"
data-mid="{$mod.mid}"
data-name="{$mod.name|escape}"
data-fallback-href="index.php?p=admin&amp;c=mods"
data-testid="deletemod-btn"
aria-label="Delete mod {$mod.name|escape}">Delete</button>
data-tooltip="Delete"
aria-label="Delete mod {$mod.name|escape}">
<i data-lucide="trash-2" style="width:14px;height:14px;color:var(--danger)"></i>
</button>
{/if}
</div>
</td>
Expand All @@ -126,6 +142,65 @@
{/foreach}
</tbody>
</table>
</div>

{* Mobile cards — paired surface for the global
`@media (max-width: 768px) { .table { display: none } }`
rule. Same display dance as `.admins-list-cards`. *}
<div class="mods-list-cards" data-testid="mods-list-cards">
{foreach from=$mod_list item=mod}
<div class="mods-list-card" data-testid="mods-list-card" data-id="{$mod.mid}">
<div class="mods-list-card__body flex items-center gap-3">
<img src="images/games/{$mod.icon}"
alt=""
width="28"
height="28"
loading="lazy"
onerror="this.style.visibility='hidden'">
<div style="flex:1;min-width:0">
<div class="font-medium text-sm truncate">{$mod.name}</div>
<div class="text-xs text-muted truncate" style="margin-top:0.125rem">
<span class="font-mono">{$mod.modfolder}</span>
· SU {$mod.steam_universe}
</div>
<div style="margin-top:0.35rem">
{if $mod.enabled}
<span class="pill pill--online">Enabled</span>
{else}
<span class="pill pill--offline">Disabled</span>
{/if}
</div>
</div>
</div>
{if $permission_editmods || $permission_deletemods}
<div class="row-actions row-actions--icons ban-card__actions">
{if $permission_editmods}
<a class="btn btn--ghost btn--icon btn--sm"
href="index.php?p=admin&amp;c=mods&amp;o=edit&amp;id={$mod.mid|escape:'url'}"
data-testid="editmod-link-mobile"
data-tooltip="Edit"
aria-label="Edit mod {$mod.name|escape}">
<i data-lucide="pencil" style="width:14px;height:14px"></i>
</a>
{/if}
{if $permission_deletemods}
<button class="btn btn--ghost btn--icon btn--sm"
type="button"
data-action="mod-delete"
data-mid="{$mod.mid}"
data-name="{$mod.name|escape}"
data-fallback-href="index.php?p=admin&amp;c=mods"
data-testid="deletemod-btn-mobile"
data-tooltip="Delete"
aria-label="Delete mod {$mod.name|escape}">
<i data-lucide="trash-2" style="width:14px;height:14px;color:var(--danger)"></i>
</button>
{/if}
</div>
{/if}
</div>
{/foreach}
</div>
{else}
<div class="card__body">
<p class="text-muted">No mods configured yet.</p>
Expand Down Expand Up @@ -258,10 +333,13 @@

/**
* @param {string} mid
* @returns {Element|null}
* @returns {NodeListOf<Element>}
*/
function rowForMid(mid) {
return document.querySelector('[data-testid="mod-row"][data-id="' + mid + '"]');
function rowsForMid(mid) {
return document.querySelectorAll(
'[data-testid="mod-row"][data-id="' + mid + '"],'
+ '[data-testid="mods-list-card"][data-id="' + mid + '"]'
);
}

/**
Expand All @@ -276,7 +354,7 @@
if (!el) return;
var n = Number((el.textContent || '').replace(/[^0-9]/g, ''));
if (!Number.isFinite(n) || n <= 0) return;
el.textContent = String(n - 1);
el.textContent = '(' + (n - 1).toLocaleString() + ')';
}

/** @returns {HTMLDialogElement|null} */
Expand Down Expand Up @@ -397,8 +475,11 @@
toast('error', 'Delete failed', msg);
return;
}
var row = rowForMid(ctx.mid);
if (row && row.parentNode) row.parentNode.removeChild(row);
var rows = rowsForMid(ctx.mid);
for (var i = 0; i < rows.length; i++) {
var row = rows[i];
if (row && row.parentNode) row.parentNode.removeChild(row);
}
decrementCount();
closeDeleteDialog();
toast('success', 'Mod deleted', ctx.name + ' has been removed.');
Expand Down
Loading