Skip to content

Commit 4e53676

Browse files
authored
feat(admin-nav): move config items into a sidebar accordion menu (#1534)
* move admin interface configuration items into accordion menu * open mobile drawer before accordion e2e
1 parent 21b656b commit 4e53676

39 files changed

Lines changed: 782 additions & 851 deletions

AGENTS.md

Lines changed: 72 additions & 101 deletions
Large diffs are not rendered by default.

ARCHITECTURE.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,8 @@ web/
9494
│ ├── Auth/openid.php LightOpenID — third-party, intentionally global ns
9595
│ ├── Security/CSRF.php Sbpp\Security\CSRF — token helpers
9696
│ ├── Security/Crypto.php Sbpp\Security\Crypto — password / token crypto
97-
│ ├── View/AdminTabs.php Sbpp\View\AdminTabs — Pattern A admin sub-section nav
97+
│ ├── View/AdminNavCatalog.php Sbpp\View\AdminNavCatalog — Pattern A section catalogs for the main-sidebar accordion (#1490)
98+
│ ├── View/AdminTabs.php Sbpp\View\AdminTabs — back-link chrome for edit-* admin pages (non-empty tabs are a no-op post-#1490)
9899
│ ├── View/ Sbpp\View\* — typed Smarty view-model DTOs
99100
│ ├── View/Install/ Sbpp\View\Install\* — install-wizard step DTOs (#1332)
100101
│ ├── Markup/ Sbpp\Markup\IntroRenderer — admin Markdown -> safe HTML

web/includes/View/AdminAdminsListView.php

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@ final class AdminAdminsListView extends View
3636
*/
3737
public function __construct(
3838
public readonly bool $can_list_admins,
39-
public readonly bool $can_add_admins,
4039
public readonly bool $can_edit_admins,
4140
public readonly bool $can_delete_admins,
4241
public readonly int $admin_count,
Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace Sbpp\View;
5+
6+
use Sbpp\Auth\UserManager;
7+
use Sbpp\Config;
8+
9+
/**
10+
* Section catalogs for Pattern A admin routes (`?section=<slug>`).
11+
*
12+
* Single source for the nested children the main sidebar accordion
13+
* renders (#1490) and for the `$sections` arrays page handlers use
14+
* for routing defaults. Permission and feature-toggle gates match
15+
* the pre-#1490 AdminTabs filter so a link either appears in the
16+
* accordion and resolves, or is absent from both.
17+
*
18+
* @phpstan-type TabSpec array{
19+
* name: string,
20+
* permission: int|string,
21+
* url: string,
22+
* slug: string,
23+
* icon?: string,
24+
* config?: bool,
25+
* }
26+
*/
27+
final class AdminNavCatalog
28+
{
29+
private function __construct()
30+
{
31+
}
32+
33+
/**
34+
* Unfiltered section catalog for one admin endpoint (`c=` value).
35+
* Empty list means the category has no accordion children (comms,
36+
* export) or is unknown.
37+
*
38+
* @return list<TabSpec>
39+
*/
40+
public static function sectionsFor(string $endpoint): array
41+
{
42+
return match ($endpoint) {
43+
'admins' => [
44+
[
45+
'slug' => 'admins',
46+
'name' => 'Admins',
47+
'permission' => ADMIN_OWNER | ADMIN_LIST_ADMINS,
48+
'url' => 'index.php?p=admin&c=admins&section=admins',
49+
'icon' => 'users',
50+
],
51+
[
52+
'slug' => 'add-admin',
53+
'name' => 'Add admin',
54+
'permission' => ADMIN_OWNER | ADMIN_ADD_ADMINS,
55+
'url' => 'index.php?p=admin&c=admins&section=add-admin',
56+
'icon' => 'user-plus',
57+
],
58+
[
59+
'slug' => 'overrides',
60+
'name' => 'Overrides',
61+
'permission' => ADMIN_OWNER | ADMIN_ADD_ADMINS,
62+
'url' => 'index.php?p=admin&c=admins&section=overrides',
63+
'icon' => 'shield',
64+
],
65+
],
66+
'servers' => [
67+
[
68+
'slug' => 'list',
69+
'name' => 'List servers',
70+
'permission' => ADMIN_OWNER | ADMIN_LIST_SERVERS,
71+
'url' => 'index.php?p=admin&c=servers&section=list',
72+
'icon' => 'server',
73+
],
74+
[
75+
'slug' => 'add',
76+
'name' => 'Add new server',
77+
'permission' => ADMIN_OWNER | ADMIN_ADD_SERVER,
78+
'url' => 'index.php?p=admin&c=servers&section=add',
79+
'icon' => 'plus',
80+
],
81+
],
82+
'bans' => self::bansSections(),
83+
'groups' => [
84+
[
85+
'slug' => 'list',
86+
'name' => 'List groups',
87+
'permission' => ADMIN_OWNER | ADMIN_LIST_GROUPS,
88+
'url' => 'index.php?p=admin&c=groups&section=list',
89+
'icon' => 'users',
90+
],
91+
[
92+
'slug' => 'add',
93+
'name' => 'Add a group',
94+
'permission' => ADMIN_OWNER | ADMIN_ADD_GROUP,
95+
'url' => 'index.php?p=admin&c=groups&section=add',
96+
'icon' => 'plus',
97+
],
98+
],
99+
'settings' => [
100+
[
101+
'slug' => 'settings',
102+
'name' => 'Main',
103+
'permission' => ADMIN_OWNER | ADMIN_WEB_SETTINGS,
104+
'url' => 'index.php?p=admin&c=settings&section=settings',
105+
'icon' => 'settings',
106+
],
107+
[
108+
'slug' => 'features',
109+
'name' => 'Features',
110+
'permission' => ADMIN_OWNER | ADMIN_WEB_SETTINGS,
111+
'url' => 'index.php?p=admin&c=settings&section=features',
112+
'icon' => 'toggle-right',
113+
],
114+
[
115+
'slug' => 'logs',
116+
'name' => 'System Log',
117+
'permission' => ADMIN_OWNER | ADMIN_WEB_SETTINGS,
118+
'url' => 'index.php?p=admin&c=settings&section=logs',
119+
'icon' => 'scroll-text',
120+
],
121+
[
122+
'slug' => 'themes',
123+
'name' => 'Themes',
124+
'permission' => ADMIN_OWNER | ADMIN_WEB_SETTINGS,
125+
'url' => 'index.php?p=admin&c=settings&section=themes',
126+
'icon' => 'palette',
127+
],
128+
],
129+
'mods' => [
130+
[
131+
'slug' => 'list',
132+
'name' => 'List MODs',
133+
'permission' => ADMIN_OWNER | ADMIN_LIST_MODS,
134+
'url' => 'index.php?p=admin&c=mods&section=list',
135+
'icon' => 'puzzle',
136+
],
137+
[
138+
'slug' => 'add',
139+
'name' => 'Add new MOD',
140+
'permission' => ADMIN_OWNER | ADMIN_ADD_MODS,
141+
'url' => 'index.php?p=admin&c=mods&section=add',
142+
'icon' => 'plus',
143+
],
144+
],
145+
default => [],
146+
};
147+
}
148+
149+
/**
150+
* Filter a section list the same way AdminTabs did: drop entries
151+
* the user cannot reach and honour optional `config` toggles.
152+
*
153+
* @param list<TabSpec> $sections
154+
* @return list<TabSpec>
155+
*/
156+
public static function filterForUser(array $sections, UserManager $userbank): array
157+
{
158+
$out = [];
159+
foreach ($sections as $tab) {
160+
if (!$userbank->HasAccess($tab['permission'])) {
161+
continue;
162+
}
163+
if (isset($tab['config']) && !$tab['config']) {
164+
continue;
165+
}
166+
$out[] = $tab;
167+
}
168+
return $out;
169+
}
170+
171+
/**
172+
* @return list<TabSpec>
173+
*/
174+
private static function bansSections(): array
175+
{
176+
$sections = [
177+
[
178+
'slug' => 'add-ban',
179+
'name' => 'Add a ban',
180+
'permission' => ADMIN_OWNER | ADMIN_ADD_BAN,
181+
'url' => 'index.php?p=admin&c=bans&section=add-ban',
182+
'icon' => 'plus',
183+
],
184+
];
185+
186+
if (Config::getBool('config.enableprotest')) {
187+
$sections[] = [
188+
'slug' => 'protests',
189+
'name' => 'Ban protests',
190+
'permission' => ADMIN_OWNER | ADMIN_BAN_PROTESTS,
191+
'url' => 'index.php?p=admin&c=bans&section=protests',
192+
'icon' => 'flag',
193+
];
194+
}
195+
if (Config::getBool('config.enablesubmit')) {
196+
$sections[] = [
197+
'slug' => 'submissions',
198+
'name' => 'Ban submissions',
199+
'permission' => ADMIN_OWNER | ADMIN_BAN_SUBMISSIONS,
200+
'url' => 'index.php?p=admin&c=bans&section=submissions',
201+
'icon' => 'clipboard-list',
202+
];
203+
}
204+
205+
$sections[] = [
206+
'slug' => 'import',
207+
'name' => 'Import bans',
208+
'permission' => ADMIN_OWNER | ADMIN_BAN_IMPORT,
209+
'url' => 'index.php?p=admin&c=bans&section=import',
210+
'icon' => 'upload',
211+
];
212+
213+
if (Config::getBool('config.enablegroupbanning')) {
214+
$sections[] = [
215+
'slug' => 'group-ban',
216+
'name' => 'Group ban',
217+
'permission' => ADMIN_OWNER | ADMIN_ADD_BAN,
218+
'url' => 'index.php?p=admin&c=bans&section=group-ban',
219+
'icon' => 'users',
220+
];
221+
}
222+
223+
return $sections;
224+
}
225+
}

web/includes/View/AdminTabs.php

Lines changed: 18 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -5,77 +5,23 @@
55
/**
66
* Class AdminTabs
77
*
8-
* Drives the intra-page section nav for Pattern A admin routes — the
9-
* routes that subdivide via `?section=<slug>` URLs (servers, mods,
10-
* groups, settings, comms, admins, bans; see AGENTS.md "Sub-paged
11-
* admin routes").
8+
* Two render shapes remain after #1490:
129
*
13-
* #1259 unified the chrome
14-
* ------------------------
15-
* Pre-#1259 there were two visuals:
16-
* - settings rendered a vertical 14rem sidebar inline in every
17-
* `page_admin_settings_*.tpl`,
18-
* - servers / mods / groups rendered a horizontal pill strip via
19-
* `core/admin_tabs.tpl`.
20-
* The horizontal strip read as "tabs into one document" rather than
21-
* "navigation between sibling pages" even after #1239 routed each
22-
* section to its own URL, and stacked badly on dense routes (mods has
23-
* a 3-section strip already; admins family hits 4+). #1259 lifted the
24-
* settings sidebar into the parameterized `core/admin_sidebar.tpl`
25-
* partial and pointed every Pattern A handler at it via this class.
26-
*
27-
* #1239 — anchor links, no JS toggle
28-
* ----------------------------------
29-
* Pre-#1239 the partial emitted `<button onclick="openTab(this, …)">`
30-
* elements that called a JS function from the v1.x sourcebans.js bulk
31-
* file (removed at #1123 D1). Clicks were silent no-ops — every pane
32-
* was visible permanently — until the broken chrome was repaired in
33-
* #1239 by routing each section through its own `?section=<slug>` URL.
34-
*
35-
* Two render shapes
36-
* -----------------
3710
* 1. `$tabs === []` (edit-* pages: admin.edit.ban.php, admin.rcon.php,
38-
* admin.email.php, …):
11+
* admin.email.php, admin.export.php, …):
3912
* Emits `core/admin_tabs.tpl` which renders just the trailing
4013
* "Back" anchor — there's no sub-section nav for these surfaces,
41-
* only the affordance to leave. This shape is unchanged by #1259.
42-
*
43-
* 2. `$tabs !== []` (Pattern A pages: admin.servers, admin.mods,
44-
* admin.groups, admin.settings, admin.comms, admin.admins,
45-
* admin.bans):
46-
* Opens the sidebar shell (`<div class="admin-sidebar-shell">`),
47-
* emits `core/admin_sidebar.tpl` (the <aside> + link list), then
48-
* opens the content column (`<div class="admin-sidebar-content">`).
49-
* The page handler is responsible for closing both wrappers AFTER
50-
* `Renderer::render(...)` runs:
14+
* only the affordance to leave.
5115
*
52-
* ```php
53-
* new AdminTabs($sections, $userbank, $theme, $section, 'Settings sections');
54-
* Renderer::render($theme, new AdminSettingsView(...));
55-
* echo '</div></div><!-- /.admin-sidebar-content + /.admin-sidebar-shell -->';
56-
* ```
16+
* 2. `$tabs !== []` (legacy Pattern A callers):
17+
* No-op. Section links live in the main sidebar accordion
18+
* (`AdminNavCatalog` + `core/navbar.tpl`, #1490). Page handlers
19+
* may still construct AdminTabs with a non-empty list during
20+
* migration; the constructor accepts the call and renders
21+
* nothing so content is not wrapped in a second rail.
5722
*
58-
* The wrapper opens before the View and closes after, so each
59-
* `Renderer::render` call slots into the content column without
60-
* per-View structural changes.
61-
*
62-
* Each tab in the `$tabs` array carries:
63-
* - `name` Display label rendered as the link text.
64-
* - `permission` Bitmask for `CUserManager::HasAccess()`. The tab
65-
* is omitted entirely when the current user lacks
66-
* the flag.
67-
* - `url` (required for non-empty tabs) The link target. The
68-
* page handler is responsible for building it
69-
* (typically `index.php?p=admin&c=<page>&section=<slug>`)
70-
* so the partial doesn't need to know about routing.
71-
* - `slug` (required for non-empty tabs) Short identifier used
72-
* for the `data-testid` and active-tab matching.
73-
* - `icon` (optional) Lucide icon name (e.g. `server`,
74-
* `puzzle`). When omitted the partial falls back to
75-
* a generic `circle-dot` so every row has matching
76-
* visual weight.
77-
* - `config` (optional) Feature-toggle gate; the tab is omitted
78-
* when `config` is set and falsy.
23+
* Prefer dropping non-empty `new AdminTabs(...)` calls from Pattern A
24+
* handlers entirely. Keep the empty-tabs Back-link shape.
7925
*
8026
* @phpstan-type TabSpec array{
8127
* name: string,
@@ -93,17 +39,10 @@ final class AdminTabs
9339

9440
/**
9541
* @param list<TabSpec> $tabs
96-
* @param string|null $activeSlug Slug of the section to mark with
97-
* `aria-current="page"`. When null, the first accessible tab's
98-
* slug wins. When the value doesn't match any visible tab no
99-
* tab is marked active (and the sidebar falls back to its
100-
* all-inactive look — still better than every entry looking
101-
* identical).
102-
* @param string|null $sidebarLabel aria-label for the sidebar
103-
* <aside>. Screen readers announce the navigation by this
104-
* label ("Settings sections" / "Server sections" / …). Only
105-
* consumed when `$tabs` is non-empty (the empty-tabs Back-link
106-
* shape has no sidebar).
42+
* @param string|null $activeSlug Kept for call-site compatibility;
43+
* unused when `$tabs` is non-empty (main sidebar owns active state).
44+
* @param string|null $sidebarLabel Kept for call-site compatibility;
45+
* unused when `$tabs` is non-empty.
10746
*/
10847
public function __construct(
10948
array $tabs,
@@ -128,30 +67,15 @@ public function __construct(
12867
}
12968

13069
if ($this->tabs === []) {
131-
// Edit-* shape: just the trailing Back anchor. The legacy
132-
// partial still owns this surface — there's no sidebar to
133-
// unify, only the "leave this page" affordance.
13470
$theme->assign('tabs', $this->tabs);
13571
$theme->assign('active_tab', $resolvedActive);
13672
$theme->display('core/admin_tabs.tpl');
13773
return;
13874
}
13975

140-
// Sidebar shape (#1259). Open the shell + render the <aside> +
141-
// open the content column. Closing tags live in the calling
142-
// page handler — see the class docblock for the contract.
143-
$sidebarId = 'admin-sidebar';
144-
$sidebarLabel = $sidebarLabel !== null && $sidebarLabel !== ''
145-
? $sidebarLabel
146-
: 'Page sections';
147-
148-
echo '<div class="admin-sidebar-shell" data-testid="admin-sidebar-shell">';
149-
$theme->assign('tabs', $this->tabs);
150-
$theme->assign('active_tab', $resolvedActive);
151-
$theme->assign('sidebar_id', $sidebarId);
152-
$theme->assign('sidebar_label', $sidebarLabel);
153-
$theme->display('core/admin_sidebar.tpl');
154-
echo '<div class="admin-sidebar-content">';
76+
// #1490 — section nav moved into the main sidebar accordion.
77+
// Non-empty AdminTabs no longer opens admin-sidebar-shell.
78+
unset($sidebarLabel);
15579
}
15680
}
15781

0 commit comments

Comments
 (0)