Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,23 @@ Add invite action to header actions in your resource view or edit page (e.g. `Ap

```

Add invite bulk action to table toolbar actions

```php
use Filament\Actions\BulkActionGroup;
use Tapp\FilamentInvite\Actions\InviteBulkAction;

public static function table(Table $table): Table
{
return $table
->toolbarActions([
BulkActionGroup::make([
InviteBulkAction::make(),
]),
]);
}
```

## Customization

### Reset URL
Expand Down
13 changes: 1 addition & 12 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
@@ -1,13 +1,2 @@
parameters:
ignoreErrors:
-
message: '#^Call to an undefined method Illuminate\\Contracts\\Auth\\PasswordBroker\:\:createToken\(\)\.$#'
identifier: method.notFound
count: 1
path: src/Actions/InviteAction.php

-
message: '#^PHPDoc tag @var with type Illuminate\\Contracts\\Auth\\MustVerifyEmail is not subtype of native type Illuminate\\Database\\Eloquent\\Model\.$#'
identifier: varTag.nativeType
count: 1
path: src/Actions/InviteAction.php
ignoreErrors: []
19 changes: 17 additions & 2 deletions resources/lang/en/invite.php
Original file line number Diff line number Diff line change
@@ -1,6 +1,21 @@
<?php

// translations for Tapp/FilamentInvite
return [
//
'bulk' => [
'label' => 'Invite',
'modal_heading' => 'Send Invite Emails',
'notifications' => [
'none' => [
'title' => 'No invites sent',
'body' => 'Selected users are already verified or cannot be invited.',
],
'sent' => [
'title' => 'Invites sent',
'body' => '{1} :count invite sent.|[2,*] :count invites sent.',
],
'skipped' => [
'body' => '{1} :count user skipped.|[2,*] :count users skipped.',
],
],
],
];
22 changes: 8 additions & 14 deletions src/Actions/InviteAction.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,14 @@

use Filament\Actions\Action;
use Filament\Actions\Concerns\CanCustomizeProcess;
use Filament\Facades\Filament;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Notification;
use Illuminate\Support\Facades\Password;
use Tapp\FilamentInvite\Notifications\SetPassword;
use Tapp\FilamentInvite\Concerns\InvitesUsers;

class InviteAction extends Action
{
use CanCustomizeProcess;
use InvitesUsers;

public static function getDefaultName(): ?string
{
Expand All @@ -33,20 +31,16 @@ protected function setUp(): void
$this->icon('heroicon-m-envelope');

$this->hidden(function (Model $user) {
/** @var MustVerifyEmail $user */
if (! $user instanceof MustVerifyEmail) {
return true;
}

return $user->hasVerifiedEmail() || auth()->user()->can('update', $user) === false;
});

$this->action(function (): void {
$result = $this->process(static function (Model $user) {
$token = Password::broker(Filament::getAuthPasswordBroker())->createToken($user);

// Use the method if the developer has specified one
if (method_exists($user, 'sendPasswordSetNotification')) {
$user->sendPasswordSetNotification($token);
} else {
Notification::send($user, new SetPassword($token));
}
$this->process(function (Model $user): void {
$this->sendInviteToUser($user);
});

$this->success();
Expand Down
79 changes: 79 additions & 0 deletions src/Actions/InviteBulkAction.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
<?php

namespace Tapp\FilamentInvite\Actions;

use Filament\Actions\BulkAction;
use Filament\Notifications\Notification;
use Illuminate\Database\Eloquent\Collection;
use Tapp\FilamentInvite\Concerns\InvitesUsers;

class InviteBulkAction extends BulkAction
{
use InvitesUsers;

public static function getDefaultName(): ?string
{
return 'invite_users';
}

protected function setUp(): void
{
parent::setUp();

$this->label(__('filament-invite::invite.bulk.label'));

$this->icon('heroicon-m-envelope');

$this->requiresConfirmation();

$this->modalHeading(__('filament-invite::invite.bulk.modal_heading'));

$this->action(function (Collection $records): void {
$invitedCount = 0;
$skippedCount = 0;

foreach ($records as $user) {
if (! $this->canInviteUser($user)) {
$skippedCount++;

continue;
}

$this->sendInviteToUser($user);
$invitedCount++;
}

if ($invitedCount === 0) {
Notification::make()
->title(__('filament-invite::invite.bulk.notifications.none.title'))
->body(__('filament-invite::invite.bulk.notifications.none.body'))
->warning()
->send();

return;
}

$body = trans_choice(
'filament-invite::invite.bulk.notifications.sent.body',
$invitedCount,
['count' => $invitedCount],
);

if ($skippedCount > 0) {
$body .= ' ' . trans_choice(
'filament-invite::invite.bulk.notifications.skipped.body',
$skippedCount,
['count' => $skippedCount],
);
}

Notification::make()
->title(__('filament-invite::invite.bulk.notifications.sent.title'))
->body($body)
->success()
->send();
});

$this->deselectRecordsAfterCompletion();
}
}
49 changes: 49 additions & 0 deletions src/Concerns/InvitesUsers.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?php

namespace Tapp\FilamentInvite\Concerns;

use Filament\Facades\Filament;
use Illuminate\Auth\Passwords\PasswordBroker;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Notification;
use Illuminate\Support\Facades\Password;
use RuntimeException;
use Tapp\FilamentInvite\Notifications\SetPassword;

trait InvitesUsers
{
protected function sendInviteToUser(Model $user): void
{
$broker = Password::broker(Filament::getAuthPasswordBroker());

if (! $broker instanceof PasswordBroker) {
throw new RuntimeException('Unexpected password broker implementation.');
}

$token = $broker->createToken($user);

if (method_exists($user, 'sendPasswordSetNotification')) {
$user->sendPasswordSetNotification($token);

return;
}

Notification::send($user, new SetPassword($token));
}

protected function canInviteUser(Model $user): bool
{
if ($user instanceof MustVerifyEmail && $user->hasVerifiedEmail()) {
return false;
}

$authenticatedUser = auth()->user();

if ($authenticatedUser === null) {
return false;
}

return $authenticatedUser->can('update', $user);
}
}
6 changes: 3 additions & 3 deletions src/FilamentInviteServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,9 @@ public function configurePackage(Package $package): void
->hasInstallCommand(function (InstallCommand $command) {
$command
->publishConfigFile();
});

$package->hasConfigFile();
})
->hasConfigFile()
->hasTranslations();
}

public function packageBooted(): void
Expand Down
63 changes: 63 additions & 0 deletions tests/InviteBulkActionTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<?php

use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Tapp\FilamentInvite\Actions\InviteBulkAction;
use Tapp\FilamentInvite\Concerns\InvitesUsers;

it('registers invite bulk action with the expected default name', function (): void {
expect(InviteBulkAction::getDefaultName())->toBe('invite_users');
});

it('can determine which users are eligible for invites', function (): void {
$inviter = new class extends Authenticatable
{
use Notifiable;

public function can($abilities, $arguments = []): bool
{
return $abilities === 'update';
}
};

$unverifiedUser = new class extends Authenticatable implements MustVerifyEmail
{
use Notifiable;

protected $table = 'users';

public function hasVerifiedEmail(): bool
{
return false;
}
};

$verifiedUser = new class extends Authenticatable implements MustVerifyEmail
{
use Notifiable;

protected $table = 'users';

public function hasVerifiedEmail(): bool
{
return true;
}
};

$action = new class extends Model
{
use InvitesUsers;

public function exposeCanInviteUser(Model $user): bool
{
return $this->canInviteUser($user);
}
};

$this->actingAs($inviter);

expect($action->exposeCanInviteUser($unverifiedUser))->toBeTrue()
->and($action->exposeCanInviteUser($verifiedUser))->toBeFalse();
});