mirror of
https://github.com/coollabsio/coolify.git
synced 2026-11-06 13:10:37 -04:00
feat(dashboard): show pending team invitations
Login no longer accepts invitations, so the dashboard lists the unexpired invitations for the user's email with a link to the invitation page, where the user accepts them.
This commit is contained in:
@@ -5,6 +5,7 @@ namespace App\Livewire;
|
||||
use App\Models\PrivateKey;
|
||||
use App\Models\Project;
|
||||
use App\Models\Server;
|
||||
use App\Models\TeamInvitation;
|
||||
use Illuminate\Support\Collection;
|
||||
use Livewire\Component;
|
||||
|
||||
@@ -40,6 +41,34 @@ class Dashboard extends Component
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.dashboard');
|
||||
return view('livewire.dashboard', [
|
||||
'pendingInvitations' => $this->pendingInvitations(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unexpired invitations for the user's email to teams the user has not joined yet.
|
||||
*
|
||||
* Uses hasExpired() instead of isValid() so rendering never deletes invitations or users.
|
||||
*
|
||||
* @return Collection<int, TeamInvitation>
|
||||
*/
|
||||
private function pendingInvitations(): Collection
|
||||
{
|
||||
$user = auth()->user();
|
||||
|
||||
if (! $user) {
|
||||
return collect();
|
||||
}
|
||||
|
||||
return TeamInvitation::query()
|
||||
->where('email', strtolower($user->email))
|
||||
->whereHas('team')
|
||||
->whereNotIn('team_id', $user->teams()->select('teams.id'))
|
||||
->with('team:id,name')
|
||||
->oldest()
|
||||
->get()
|
||||
->reject(fn (TeamInvitation $invitation) => $invitation->hasExpired())
|
||||
->values();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,23 @@
|
||||
@endphp
|
||||
|
||||
<div class="flex min-w-0 flex-col gap-8">
|
||||
@if ($pendingInvitations->isNotEmpty())
|
||||
<div class="flex min-w-0 flex-col gap-2">
|
||||
@foreach ($pendingInvitations as $invitation)
|
||||
<x-callout type="info" title="Pending team invitation"
|
||||
wire:key="dashboard-invitation-{{ $invitation->uuid }}">
|
||||
<div class="flex min-w-0 flex-wrap items-center justify-between gap-2">
|
||||
<span class="min-w-0">Team <span class="font-semibold">{{ $invitation->team->name }}</span>
|
||||
invited you as {{ ucfirst($invitation->role) }}.</span>
|
||||
<a href="{{ route('team.invitation.show', $invitation->uuid) }}" class="button shrink-0">
|
||||
Review invitation
|
||||
</a>
|
||||
</div>
|
||||
</x-callout>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<livewire:dashboard.active-deployments />
|
||||
|
||||
@if ($hasTrafficAnalytics)
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
<?php
|
||||
|
||||
use App\Livewire\Dashboard;
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\Team;
|
||||
use App\Models\TeamInvitation;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Queue;
|
||||
use Livewire\Livewire;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
function createDashboardTestInvitation(Team $team, string $email, array $attributes = []): TeamInvitation
|
||||
{
|
||||
$uuid = $attributes['uuid'] ?? (string) str()->uuid();
|
||||
|
||||
return TeamInvitation::create(array_merge([
|
||||
'team_id' => $team->id,
|
||||
'uuid' => $uuid,
|
||||
'email' => $email,
|
||||
'role' => 'admin',
|
||||
'link' => url('/invitations/'.$uuid),
|
||||
'via' => 'link',
|
||||
], $attributes));
|
||||
}
|
||||
|
||||
beforeEach(function () {
|
||||
Queue::fake();
|
||||
|
||||
InstanceSettings::unguarded(fn () => InstanceSettings::query()->create(['id' => 0]));
|
||||
|
||||
$this->user = User::factory()->create(['email' => 'invited@example.com']);
|
||||
$this->ownTeam = Team::factory()->create();
|
||||
$this->user->teams()->attach($this->ownTeam, ['role' => 'owner']);
|
||||
$this->invitingTeam = Team::factory()->create(['name' => 'Acme Platform']);
|
||||
|
||||
Team::query()->update(['show_boarding' => false]);
|
||||
|
||||
$this->actingAs($this->user);
|
||||
session(['currentTeam' => $this->ownTeam]);
|
||||
});
|
||||
|
||||
it('shows a pending invitation for the user email with a review link', function () {
|
||||
$invitation = createDashboardTestInvitation($this->invitingTeam, 'Invited@Example.com');
|
||||
|
||||
Livewire::test(Dashboard::class)
|
||||
->assertSee('Acme Platform')
|
||||
->assertSee('invited you as Admin.')
|
||||
->assertSee('Review invitation')
|
||||
->assertSee(route('team.invitation.show', $invitation->uuid), false);
|
||||
});
|
||||
|
||||
it('does not show invitations for another email', function () {
|
||||
$invitation = createDashboardTestInvitation($this->invitingTeam, 'someone-else@example.com');
|
||||
|
||||
Livewire::test(Dashboard::class)
|
||||
->assertDontSee('Acme Platform')
|
||||
->assertDontSee('Review invitation')
|
||||
->assertDontSee(route('team.invitation.show', $invitation->uuid), false);
|
||||
});
|
||||
|
||||
it('does not show expired invitations and keeps them in place', function () {
|
||||
$invitation = createDashboardTestInvitation($this->invitingTeam, 'invited@example.com');
|
||||
$invitation->forceFill([
|
||||
'created_at' => now()->subDays(config('constants.invitation.link.expiration_days') + 1),
|
||||
])->save();
|
||||
|
||||
Livewire::test(Dashboard::class)
|
||||
->assertDontSee('Acme Platform')
|
||||
->assertDontSee('Review invitation');
|
||||
|
||||
expect(TeamInvitation::query()->whereKey($invitation->id)->exists())->toBeTrue()
|
||||
->and(User::query()->whereKey($this->user->id)->exists())->toBeTrue();
|
||||
});
|
||||
|
||||
it('does not show invitations to a team the user already belongs to', function () {
|
||||
$this->user->teams()->attach($this->invitingTeam, ['role' => 'member']);
|
||||
createDashboardTestInvitation($this->invitingTeam, 'invited@example.com');
|
||||
|
||||
Livewire::test(Dashboard::class)
|
||||
->assertDontSee('Acme Platform')
|
||||
->assertDontSee('Review invitation');
|
||||
});
|
||||
|
||||
it('no longer shows the invitation after it is accepted', function () {
|
||||
$invitation = createDashboardTestInvitation($this->invitingTeam, 'invited@example.com');
|
||||
|
||||
$this->get(route('dashboard'))
|
||||
->assertOk()
|
||||
->assertSee(route('team.invitation.show', $invitation->uuid), false);
|
||||
|
||||
$this->post(route('team.invitation.accept', $invitation->uuid))
|
||||
->assertRedirect(route('team.index'));
|
||||
|
||||
expect($this->user->teams()->whereKey($this->invitingTeam->id)->exists())->toBeTrue();
|
||||
|
||||
$this->actingAs($this->user->fresh());
|
||||
session(['currentTeam' => $this->invitingTeam->fresh()]);
|
||||
|
||||
Livewire::test(Dashboard::class)
|
||||
->assertDontSee('Acme Platform')
|
||||
->assertDontSee('Review invitation')
|
||||
->assertDontSee(route('team.invitation.show', $invitation->uuid), false);
|
||||
});
|
||||
Reference in New Issue
Block a user