Foundation slice for Mantis #843 php/Mod/Admin + php/Website/Hub RFC: * php/Mod/Admin/Boot.php — search registry, menu registry, form component layer, HasRateLimiting concern, reusable form/view primitives under Mod/Admin/Forms * php/Website/Hub/Boot.php — host-aware Hub route naming for secondary domains * WorkspaceSwitcher and GlobalSearch global Hub Livewire components * Foundation routed slice in Hub/Routes/admin.php: dashboard shell, workspace listing, site settings (with WordPress/webhook connector), account usage, platform user list+detail * Foundation tests under php/tests/Feature/Mod/Admin/ 53 PHP files. php -l clean. Pest unrunnable in sandbox (no vendor/). Foundation slice only — composer.json kept off-limits so namespace stays under Core\Mod\Agentic\... rather than standalone Core\Admin package. Deferred: Profile, Settings, ServiceManager, ServicesAdmin, Honeypot, Entitlement\{Dashboard,FeatureManager,PackageManager}, PromptManager, WaitlistManager, Console, Databases, Deployments, Content, ContentManager, ContentEditor, ActivityLog, Analytics, AIServices, BoostPurchase. Lane was under-instructed by supervisor with stop-at framing — follow-up tickets needed for remainder. Co-authored-by: Codex <noreply@openai.com> Closes tasks.lthn.sh/view.php?id=843
95 lines
2.8 KiB
PHP
95 lines
2.8 KiB
PHP
<?php
|
|
|
|
// SPDX-License-Identifier: EUPL-1.2
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Core\Mod\Agentic\Mod\Admin\Menu;
|
|
|
|
use Core\Mod\Agentic\Mod\Admin\Menu\Contracts\AdminMenuProvider;
|
|
|
|
class AdminMenuRegistry
|
|
{
|
|
/**
|
|
* @var array<int, AdminMenuProvider>
|
|
*/
|
|
protected array $providers = [];
|
|
|
|
/**
|
|
* @var array<string, array<string, mixed>>
|
|
*/
|
|
protected array $groups = [
|
|
'dashboard' => ['label' => 'Dashboard', 'standalone' => true],
|
|
'workspaces' => ['label' => 'Workspaces', 'icon' => 'folders'],
|
|
'services' => ['label' => 'Services', 'standalone' => true],
|
|
'settings' => ['label' => 'Account', 'icon' => 'gear'],
|
|
'admin' => ['label' => 'Admin', 'icon' => 'shield'],
|
|
];
|
|
|
|
public function register(AdminMenuProvider $provider): void
|
|
{
|
|
$this->providers[] = $provider;
|
|
}
|
|
|
|
/**
|
|
* @return array<int, AdminMenuProvider>
|
|
*/
|
|
public function providers(): array
|
|
{
|
|
return $this->providers;
|
|
}
|
|
|
|
/**
|
|
* @return array<string, array{meta: array<string, mixed>, items: array<int, array<string, mixed>>}>
|
|
*/
|
|
public function items(?object $user = null, ?object $workspace = null): array
|
|
{
|
|
$grouped = [];
|
|
$isAdmin = method_exists($user, 'isHades') ? (bool) $user->isHades() : false;
|
|
|
|
foreach ($this->providers as $provider) {
|
|
if (! $provider->canViewMenu($user, $workspace)) {
|
|
continue;
|
|
}
|
|
|
|
foreach ($provider->adminMenuItems() as $registration) {
|
|
if (($registration['admin'] ?? false) && ! $isAdmin) {
|
|
continue;
|
|
}
|
|
|
|
$group = (string) ($registration['group'] ?? 'services');
|
|
$grouped[$group][] = [
|
|
'priority' => (int) ($registration['priority'] ?? 50),
|
|
'item' => $registration['item'],
|
|
];
|
|
}
|
|
}
|
|
|
|
$resolved = [];
|
|
|
|
foreach ($grouped as $group => $items) {
|
|
usort($items, static fn (array $left, array $right): int => $left['priority'] <=> $right['priority']);
|
|
|
|
$resolved[$group] = [
|
|
'meta' => $this->groups[$group] ?? ['label' => ucfirst($group)],
|
|
'items' => array_map(static fn (array $entry): array => $entry['item'](), $items),
|
|
];
|
|
}
|
|
|
|
return $resolved;
|
|
}
|
|
|
|
/**
|
|
* Compatibility wrapper for render paths that expect build().
|
|
*
|
|
* @return array<string, array{meta: array<string, mixed>, items: array<int, array<string, mixed>>}>
|
|
*/
|
|
public function build(?object $workspace = null, bool $isAdmin = false, ?object $user = null): array
|
|
{
|
|
if ($user !== null && ! $isAdmin && method_exists($user, 'isHades')) {
|
|
$isAdmin = (bool) $user->isHades();
|
|
}
|
|
|
|
return $this->items($user, $workspace);
|
|
}
|
|
}
|