Some checks failed
Main Confidence / confidence (push) Failing after 43s
## Summary - consolidate workspace and tenant shell resolution behind a canonical resolved shell context - align workspace switching, tenant selection, and tenant clearing with the new recovery and fallback rules - add focused Pest coverage for shell resolution and update root dev orchestration so platform Vite starts correctly from repo-root commands ## Testing - cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Monitoring/HeaderContextBarTest.php - cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Workspaces/GlobalContextShellContractTest.php - manual integrated-browser smoke for tenant-bound shell actions and context recovery flows - validated corepack pnpm build:platform, corepack pnpm dev:platform, corepack pnpm dev:website, and corepack pnpm dev ## Notes - Livewire v4 / Filament v5 remain unchanged and provider registration stays in bootstrap/providers.php - no new globally searchable resources or destructive Filament actions were introduced Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de> Reviewed-on: #246
226 lines
13 KiB
PHP
226 lines
13 KiB
PHP
@php
|
|
use App\Filament\Pages\ChooseTenant;
|
|
use App\Filament\Pages\ChooseWorkspace;
|
|
use App\Models\Tenant;
|
|
use App\Models\User;
|
|
use App\Support\OperateHub\OperateHubShell;
|
|
use App\Support\Workspaces\WorkspaceContext;
|
|
use Filament\Facades\Filament;
|
|
|
|
/** @var WorkspaceContext $workspaceContext */
|
|
$workspaceContext = app(WorkspaceContext::class);
|
|
$resolvedContext = app(OperateHubShell::class)->resolvedContext(request());
|
|
|
|
$workspace = $resolvedContext->workspace;
|
|
|
|
$user = auth()->user();
|
|
|
|
$tenants = collect();
|
|
if ($user instanceof User && $workspace) {
|
|
$tenants = collect($user->getTenants(Filament::getCurrentOrDefaultPanel()))
|
|
->filter(fn ($tenant): bool => $tenant instanceof Tenant && (int) $tenant->workspace_id === (int) $workspace->getKey())
|
|
->values();
|
|
}
|
|
|
|
$currentTenant = $resolvedContext->tenant;
|
|
$currentTenantId = $currentTenant instanceof Tenant ? (int) $currentTenant->getKey() : null;
|
|
$currentTenantName = $currentTenant instanceof Tenant ? $currentTenant->getFilamentName() : null;
|
|
|
|
$lastTenantId = $workspaceContext->lastTenantId(request());
|
|
$canClearTenantContext = $currentTenant instanceof Tenant || $lastTenantId !== null;
|
|
@endphp
|
|
|
|
@php
|
|
$tenantLabel = $currentTenantName ?? 'No tenant selected';
|
|
$workspaceLabel = $workspace?->name ?? 'Choose workspace';
|
|
$hasActiveTenant = $currentTenantName !== null;
|
|
$managedTenantsUrl = $workspace
|
|
? route('admin.workspace.managed-tenants.index', ['workspace' => $workspace])
|
|
: route('admin.onboarding');
|
|
$workspaceUrl = $workspace
|
|
? route('admin.home')
|
|
: ChooseWorkspace::getUrl(panel: 'admin');
|
|
$tenantTriggerLabel = $workspace ? $tenantLabel : 'Choose workspace';
|
|
@endphp
|
|
|
|
<div class="inline-flex items-center gap-0 rounded-lg border border-gray-200 bg-white text-sm dark:border-white/10 dark:bg-white/5">
|
|
{{-- Workspace label: standalone link --}}
|
|
<a
|
|
href="{{ $workspaceUrl }}"
|
|
wire:navigate
|
|
class="inline-flex items-center gap-1.5 rounded-l-lg px-2.5 py-1.5 font-medium text-gray-700 transition hover:bg-gray-50 hover:text-primary-600 dark:text-gray-200 dark:hover:bg-white/10 dark:hover:text-primary-400"
|
|
>
|
|
<x-filament::icon icon="heroicon-o-squares-2x2" class="h-4 w-4 text-gray-400 dark:text-gray-500" />
|
|
{{ $workspaceLabel }}
|
|
</a>
|
|
|
|
@if ($workspace)
|
|
<x-filament::icon icon="heroicon-m-chevron-right" class="h-3 w-3 shrink-0 text-gray-300 dark:text-gray-600" />
|
|
@endif
|
|
|
|
{{-- Dropdown trigger: tenant label + chevron --}}
|
|
<x-filament::dropdown placement="bottom-start" teleport width="xs">
|
|
<x-slot name="trigger">
|
|
<button
|
|
type="button"
|
|
aria-label="{{ $workspace ? 'Tenant scope' : 'Select tenant' }}"
|
|
class="inline-flex items-center gap-1.5 rounded-r-lg px-2 py-1.5 transition hover:bg-gray-50 dark:hover:bg-white/10"
|
|
>
|
|
<span class="{{ $workspace && $hasActiveTenant ? 'font-medium text-primary-600 dark:text-primary-400' : 'text-gray-500 dark:text-gray-400' }}">
|
|
{{ $tenantTriggerLabel }}
|
|
</span>
|
|
|
|
<x-filament::icon icon="heroicon-m-chevron-down" class="h-3.5 w-3.5 text-gray-400 dark:text-gray-500" />
|
|
</button>
|
|
</x-slot>
|
|
|
|
<x-filament::dropdown.list>
|
|
<div class="space-y-3 px-3 py-2" x-data="{ query: '' }">
|
|
@if ($resolvedContext->showsRecoveryNotice())
|
|
<div class="rounded-lg border border-amber-200 bg-amber-50 px-3 py-2 text-xs text-amber-800 dark:border-amber-500/30 dark:bg-amber-500/10 dark:text-amber-200">
|
|
<div class="font-semibold">Context unavailable</div>
|
|
|
|
@if ($workspace)
|
|
<div>The requested scope could not be restored. The shell is showing a valid workspace state instead.</div>
|
|
@else
|
|
<div>Choose a workspace to continue with a valid admin context.</div>
|
|
@endif
|
|
</div>
|
|
@endif
|
|
|
|
{{-- Workspace section --}}
|
|
<div class="space-y-1">
|
|
<div class="text-xs font-semibold uppercase tracking-wider text-gray-400 dark:text-gray-500">
|
|
Workspace
|
|
</div>
|
|
|
|
<div class="flex items-center justify-between rounded-lg bg-gray-50 px-3 py-2 dark:bg-white/5">
|
|
<div class="flex items-center gap-2">
|
|
<x-filament::icon icon="heroicon-o-squares-2x2" class="h-4 w-4 text-gray-500 dark:text-gray-400" />
|
|
<span class="text-sm font-medium text-gray-950 dark:text-white">{{ $workspaceLabel }}</span>
|
|
</div>
|
|
|
|
<a
|
|
href="{{ ChooseWorkspace::getUrl(panel: 'admin').'?choose=1' }}"
|
|
class="text-xs font-medium text-primary-600 hover:text-primary-500 dark:text-primary-400 dark:hover:text-primary-300"
|
|
>
|
|
Switch workspace
|
|
</a>
|
|
</div>
|
|
|
|
<a
|
|
href="{{ route('admin.home') }}"
|
|
class="flex items-center gap-2 rounded-lg px-3 py-2 text-sm text-gray-700 transition hover:bg-gray-50 dark:text-gray-300 dark:hover:bg-white/5"
|
|
>
|
|
<x-filament::icon icon="heroicon-o-home" class="h-4 w-4 text-gray-400 dark:text-gray-500" />
|
|
Workspace Home
|
|
</a>
|
|
</div>
|
|
|
|
@if ($workspace)
|
|
<div class="border-t border-gray-200 dark:border-white/10"></div>
|
|
|
|
{{-- Tenant section --}}
|
|
<div class="space-y-2">
|
|
<div class="flex items-center justify-between">
|
|
<div class="text-xs font-semibold uppercase tracking-wider text-gray-400 dark:text-gray-500">
|
|
Selected tenant
|
|
</div>
|
|
</div>
|
|
|
|
@if ($resolvedContext->pageCategory->requiresExplicitTenant() && $hasActiveTenant)
|
|
<div class="space-y-2">
|
|
<div class="flex items-center gap-2 rounded-lg bg-primary-50 px-3 py-2 dark:bg-primary-950/30">
|
|
<x-filament::icon icon="heroicon-o-building-office-2" class="h-4 w-4 text-primary-600 dark:text-primary-400" />
|
|
<span class="text-sm font-medium text-primary-700 dark:text-primary-300">{{ $currentTenantName }}</span>
|
|
<a
|
|
href="{{ ChooseTenant::getUrl(panel: 'admin') }}"
|
|
class="ml-auto text-xs font-medium text-primary-600 hover:text-primary-500 dark:text-primary-400 dark:hover:text-primary-300"
|
|
>
|
|
Switch tenant
|
|
</a>
|
|
</div>
|
|
|
|
@if ($canClearTenantContext)
|
|
<form method="POST" action="{{ route('admin.clear-tenant-context') }}">
|
|
@csrf
|
|
|
|
<button type="submit" class="w-full rounded-lg px-3 py-1.5 text-left text-xs font-medium text-gray-500 transition hover:bg-gray-50 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-white/5 dark:hover:text-gray-200">
|
|
Clear tenant scope
|
|
</button>
|
|
</form>
|
|
@endif
|
|
</div>
|
|
@else
|
|
@if ($tenants->isEmpty())
|
|
<div class="space-y-2 rounded-lg border border-dashed border-gray-300 px-3 py-3 text-center text-xs text-gray-500 dark:border-gray-600 dark:text-gray-400">
|
|
<div>No active tenants are available for the standard operating context in this workspace.</div>
|
|
<a href="{{ $managedTenantsUrl }}" class="inline-flex items-center gap-1 text-primary-600 hover:text-primary-500 dark:text-primary-400 dark:hover:text-primary-300">
|
|
<x-filament::icon icon="heroicon-o-arrow-top-right-on-square" class="h-3.5 w-3.5" />
|
|
View managed tenants
|
|
</a>
|
|
</div>
|
|
@else
|
|
@if (! $hasActiveTenant)
|
|
<div class="rounded-lg bg-gray-50 px-3 py-2 text-xs text-gray-600 dark:bg-white/5 dark:text-gray-400">
|
|
No tenant selected. Workspace-wide pages remain available, and choosing a tenant only sets the normal active operating context.
|
|
</div>
|
|
@endif
|
|
|
|
<input
|
|
type="text"
|
|
class="fi-input fi-text-input w-full"
|
|
placeholder="Search tenants…"
|
|
x-model="query"
|
|
/>
|
|
|
|
<div class="max-h-48 overflow-auto rounded-lg border border-gray-200 dark:border-gray-700">
|
|
@foreach ($tenants as $tenant)
|
|
@php
|
|
$isActive = $currentTenantId !== null && (int) $tenant->getKey() === $currentTenantId;
|
|
@endphp
|
|
|
|
<form method="POST" action="{{ route('admin.select-tenant') }}">
|
|
@csrf
|
|
<input type="hidden" name="tenant_id" value="{{ (int) $tenant->getKey() }}" />
|
|
|
|
<button
|
|
type="submit"
|
|
class="flex w-full items-center gap-2 px-3 py-2 text-left text-sm transition {{ $isActive ? 'bg-primary-50 font-medium text-primary-700 dark:bg-primary-950/30 dark:text-primary-300' : 'text-gray-700 hover:bg-gray-50 dark:text-gray-300 dark:hover:bg-white/5' }}"
|
|
data-search="{{ (string) str($tenant->getFilamentName())->lower() }}"
|
|
x-show="query === '' || ($el.dataset.search ?? '').includes(query.toLowerCase())"
|
|
>
|
|
@if ($isActive)
|
|
<x-filament::icon icon="heroicon-s-check-circle" class="h-4 w-4 shrink-0 text-primary-600 dark:text-primary-400" />
|
|
@else
|
|
<x-filament::icon icon="heroicon-o-building-office-2" class="h-4 w-4 shrink-0 text-gray-400 dark:text-gray-500" />
|
|
@endif
|
|
|
|
{{ $tenant->getFilamentName() }}
|
|
</button>
|
|
</form>
|
|
@endforeach
|
|
</div>
|
|
|
|
@if ($canClearTenantContext)
|
|
<form method="POST" action="{{ route('admin.clear-tenant-context') }}">
|
|
@csrf
|
|
|
|
<button type="submit" class="w-full rounded-lg px-3 py-1.5 text-left text-xs font-medium text-gray-500 transition hover:bg-gray-50 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-white/5 dark:hover:text-gray-200">
|
|
Clear tenant scope
|
|
</button>
|
|
</form>
|
|
@endif
|
|
@endif
|
|
@endif
|
|
</div>
|
|
@else
|
|
<div class="rounded-lg border border-dashed border-gray-300 px-3 py-3 text-center text-xs text-gray-500 dark:border-gray-600 dark:text-gray-400">
|
|
Choose a workspace first.
|
|
</div>
|
|
@endif
|
|
</div>
|
|
</x-filament::dropdown.list>
|
|
</x-filament::dropdown>
|
|
</div>
|