feat(spec-077): topbar workspace+tenant dropdowns

This commit is contained in:
Ahmed Darrazi 2026-02-06 21:44:16 +01:00
parent b07313cfe1
commit ffbf342d52
2 changed files with 101 additions and 22 deletions

View File

@ -2,6 +2,9 @@
use App\Filament\Pages\ChooseWorkspace; use App\Filament\Pages\ChooseWorkspace;
use App\Models\Tenant; use App\Models\Tenant;
use App\Models\User; use App\Models\User;
use App\Models\WorkspaceMembership;
use App\Services\Auth\WorkspaceRoleCapabilityMap;
use App\Support\Auth\Capabilities;
use App\Support\Workspaces\WorkspaceContext; use App\Support\Workspaces\WorkspaceContext;
use Filament\Facades\Filament; use Filament\Facades\Filament;
@ -12,9 +15,29 @@
$user = auth()->user(); $user = auth()->user();
$canSeeAllWorkspaceTenants = false;
if ($user instanceof User && $workspace) {
$roles = WorkspaceRoleCapabilityMap::rolesWithCapability(Capabilities::WORKSPACE_MEMBERSHIP_MANAGE);
$canSeeAllWorkspaceTenants = WorkspaceMembership::query()
->where('workspace_id', (int) $workspace->getKey())
->where('user_id', (int) $user->getKey())
->whereIn('role', $roles)
->exists();
}
$tenants = collect(); $tenants = collect();
if ($user instanceof User) { if ($user instanceof User && $workspace) {
$tenants = collect($user->getTenants(Filament::getCurrentOrDefaultPanel())); if ($canSeeAllWorkspaceTenants) {
$tenants = Tenant::query()
->where('workspace_id', (int) $workspace->getKey())
->orderBy('name')
->get();
} else {
$tenants = collect($user->getTenants(Filament::getCurrentOrDefaultPanel()))
->filter(fn ($tenant): bool => $tenant instanceof Tenant && (int) $tenant->workspace_id === (int) $workspace->getKey())
->values();
}
} }
$currentTenant = Filament::getTenant(); $currentTenant = Filament::getTenant();
@ -25,41 +48,72 @@
@endphp @endphp
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<x-filament::dropdown placement="bottom-start" teleport>
<x-slot name="trigger">
<x-filament::button
color="gray"
outlined
size="sm"
icon="heroicon-o-squares-2x2"
>
{{ $workspace?->name ?? 'Select workspace' }}
</x-filament::button>
</x-slot>
<x-filament::dropdown.list>
<a <a
href="{{ ChooseWorkspace::getUrl() }}" href="{{ ChooseWorkspace::getUrl() }}"
class="text-sm text-gray-600 hover:text-gray-900 dark:text-gray-300 dark:hover:text-gray-100" class="block px-3 py-2 text-sm hover:bg-gray-50 dark:hover:bg-gray-800"
> >
<span class="font-medium">Workspace:</span> Switch workspace
<span>{{ $workspace?->name ?? '—' }}</span>
</a> </a>
@if ($canSeeAllWorkspaceTenants)
<a
href="{{ route('filament.admin.resources.workspaces.index') }}"
class="block px-3 py-2 text-sm hover:bg-gray-50 dark:hover:bg-gray-800"
>
Manage workspaces
</a>
@endif
</x-filament::dropdown.list>
</x-filament::dropdown>
<div class="h-4 w-px bg-gray-200 dark:bg-gray-700"></div> <div class="h-4 w-px bg-gray-200 dark:bg-gray-700"></div>
<x-filament::dropdown placement="bottom-start" teleport> <x-filament::dropdown placement="bottom-start" teleport>
<x-slot name="trigger"> <x-slot name="trigger">
<button <x-filament::button
type="button" color="gray"
class="text-sm text-gray-600 hover:text-gray-900 dark:text-gray-300 dark:hover:text-gray-100" outlined
size="sm"
icon="heroicon-o-building-office-2"
> >
<span class="font-medium">Tenant:</span> {{ $currentTenantName ?? 'Select tenant' }}
<span>{{ $currentTenantName ?? '—' }}</span> </x-filament::button>
</button>
</x-slot> </x-slot>
<x-filament::dropdown.list> <x-filament::dropdown.list>
<div class="px-3 py-2 space-y-2" x-data="{ query: '' }"> <div class="px-3 py-2 space-y-2" x-data="{ query: '' }">
<div class="text-xs font-medium text-gray-500 dark:text-gray-400">Tenant context</div> <div class="text-xs font-medium text-gray-500 dark:text-gray-400">
Tenant context
@if ($canSeeAllWorkspaceTenants)
<span class="text-gray-400">· all workspace tenants</span>
@endif
</div>
@if (! $workspace) @if (! $workspace)
<div class="text-xs text-gray-500 dark:text-gray-400">Choose a workspace first.</div> <div class="text-xs text-gray-500 dark:text-gray-400">Choose a workspace first.</div>
@elseif ($tenants->isEmpty()) @elseif ($tenants->isEmpty())
<div class="text-xs text-gray-500 dark:text-gray-400">No tenants you can access in this workspace.</div> <div class="text-xs text-gray-500 dark:text-gray-400">
{{ $canSeeAllWorkspaceTenants ? 'No tenants exist in this workspace.' : 'No tenants you can access in this workspace.' }}
</div>
@else @else
<div class="space-y-2"> <div class="space-y-2">
<input <input
type="text" type="text"
class="fi-input fi-text-input w-full" class="fi-input fi-text-input w-full"
placeholder="Select tenant" placeholder="Search tenants"
x-model="query" x-model="query"
/> />

View File

@ -11,6 +11,8 @@
$tenant = Tenant::factory()->create(['status' => 'active']); $tenant = Tenant::factory()->create(['status' => 'active']);
[$user, $tenant] = createUserWithTenant($tenant, role: 'owner'); [$user, $tenant] = createUserWithTenant($tenant, role: 'owner');
$workspaceName = $tenant->workspace?->name;
Filament::setTenant(null, true); Filament::setTenant(null, true);
$this->actingAs($user) $this->actingAs($user)
@ -22,9 +24,10 @@
]) ])
->get('/admin/operations') ->get('/admin/operations')
->assertOk() ->assertOk()
->assertSee('Workspace:') ->assertSee($workspaceName ?? 'Select workspace')
->assertSee('Tenant:') ->assertSee('Select tenant')
->assertSee('Select tenant…') ->assertSee('Search tenants…')
->assertSee('Switch workspace')
->assertSee('admin/select-tenant') ->assertSee('admin/select-tenant')
->assertSee('Clear tenant context') ->assertSee('Clear tenant context')
->assertSee($tenant->getFilamentName()); ->assertSee($tenant->getFilamentName());
@ -39,7 +42,7 @@
it('filters the header tenant picker to tenants the user can access', function (): void { it('filters the header tenant picker to tenants the user can access', function (): void {
$tenantA = Tenant::factory()->create(['status' => 'active']); $tenantA = Tenant::factory()->create(['status' => 'active']);
[$user, $tenantA] = createUserWithTenant($tenantA, role: 'owner'); [$user, $tenantA] = createUserWithTenant($tenantA, role: 'owner', workspaceRole: 'readonly');
$tenantB = Tenant::factory()->create([ $tenantB = Tenant::factory()->create([
'status' => 'active', 'status' => 'active',
@ -59,6 +62,28 @@
->assertDontSee($tenantB->getFilamentName()); ->assertDontSee($tenantB->getFilamentName());
}); });
it('shows all workspace tenants in the header tenant picker for workspace owners', function (): void {
$tenantA = Tenant::factory()->create(['status' => 'active']);
[$user, $tenantA] = createUserWithTenant($tenantA, role: 'owner', workspaceRole: 'owner');
$tenantB = Tenant::factory()->create([
'status' => 'active',
'workspace_id' => (int) $tenantA->workspace_id,
'name' => 'ZZZ-UNASSIGNED-TENANT-NAME-12345',
]);
Filament::setTenant(null, true);
$this->actingAs($user)
->withSession([
WorkspaceContext::SESSION_KEY => (int) $tenantA->workspace_id,
])
->get('/admin/operations')
->assertOk()
->assertSee($tenantA->getFilamentName())
->assertSee($tenantB->getFilamentName());
});
it('does not implicitly switch tenant when opening canonical operation deep links', function (): void { it('does not implicitly switch tenant when opening canonical operation deep links', function (): void {
$tenantA = Tenant::factory()->create(['status' => 'active']); $tenantA = Tenant::factory()->create(['status' => 'active']);
[$user, $tenantA] = createUserWithTenant($tenantA, role: 'owner'); [$user, $tenantA] = createUserWithTenant($tenantA, role: 'owner');