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
4 changes: 4 additions & 0 deletions packages/actions/docs/01-overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,8 @@ Action::make('edit')
->authorizationTooltip()
```

If the denial does not provide a message (for example, your policy returns plain `false`, or a `Gate::before()` hook short-circuits the check), the action is hidden instead. You can supply a fallback message with `authorizationMessage()` to keep the action visible in that case.

<AutoScreenshot name="actions/trigger-button/authorization-tooltip" alt="Disabled action button with an authorization tooltip" version="4.x" />

You may instead allow the action to still be clickable even if the user is not authorized, but send a notification containing the response message, using the `authorizationNotification()` method:
Expand All @@ -275,6 +277,8 @@ Action::make('edit')
->authorizationNotification()
```

As with `authorizationTooltip()`, the action is hidden if the denial does not provide a message, unless you supply a fallback with `authorizationMessage()`.

### Disabling a button

If you want to disable a button instead of hiding it, you can use the `disabled()` method:
Expand Down
10 changes: 6 additions & 4 deletions packages/actions/src/Concerns/CanBeAuthorized.php
Original file line number Diff line number Diff line change
Expand Up @@ -205,15 +205,17 @@ public function hasAuthorizationNotification(): bool

public function isAuthorizedOrNotHiddenWhenUnauthorized(): bool
{
if ($this->hasAuthorizationTooltip()) {
return true;
if (! $this->hasAuthorizationTooltip() && ! $this->hasAuthorizationNotification()) {
return $this->isAuthorized();
}

if ($this->hasAuthorizationNotification()) {
$response = $this->getAuthorizationResponse();

if ($response->allowed()) {
return true;
}

return $this->isAuthorized();
return filled($response->message()) || filled($this->getAuthorizationMessage());
}

public function authorizeIndividualRecords(bool | string | Closure | null $callback = true): static
Expand Down
172 changes: 172 additions & 0 deletions tests/src/Actions/ActionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
use Filament\Tests\Actions\TestCase;
use Filament\Tests\Fixtures\Models\Post;
use Filament\Tests\Fixtures\Pages\Actions;
use Illuminate\Auth\Access\Response;
use Illuminate\Support\Str;

use function Filament\Tests\livewire;
Expand Down Expand Up @@ -1548,3 +1549,174 @@
expect($action)->toBeInstanceOf(Action::class);
});
});

describe('authorization', function (): void {
it('is visible by default when no `authorize()` is configured', function (): void {
$action = Action::make('test');

expect($action->isAuthorizedOrNotHiddenWhenUnauthorized())->toBeTrue();
expect($action->isVisible())->toBeTrue();
});

it('is hidden when `authorize()` returns `false` and no auth feedback method is set', function (): void {
$action = Action::make('test')
->authorize(fn (): bool => false);

expect($action->isAuthorizedOrNotHiddenWhenUnauthorized())->toBeFalse();
expect($action->isVisible())->toBeFalse();
});

it('accepts an `authorize()` closure returning a `Response`', function (): void {
$action = Action::make('test')
->authorize(fn (): Response => Response::deny('Nope.'));

expect($action->getAuthorizationResponse()->message())->toBe('Nope.');
});

it('can chain `authorizationMessage()` to set a fallback message', function (): void {
$action = Action::make('test')
->authorizationMessage('Custom fallback.');

expect($action->getAuthorizationMessage())->toBe('Custom fallback.');
});

describe('with `authorizationNotification()`', function (): void {
it('shows the action when the user is allowed', function (): void {
$action = Action::make('test')
->authorize(fn (): Response => Response::allow())
->authorizationNotification();

expect($action->isAuthorizedOrNotHiddenWhenUnauthorized())->toBeTrue();
expect($action->isVisible())->toBeTrue();
});

it('shows the action when denied with a message', function (): void {
$action = Action::make('test')
->authorize(fn (): Response => Response::deny('You cannot do that.'))
->authorizationNotification();

expect($action->isAuthorizedOrNotHiddenWhenUnauthorized())->toBeTrue();
expect($action->isVisible())->toBeTrue();
expect($action->hasAuthorizationNotification())->toBeTrue();
expect($action->getAuthorizationResponseWithMessage()->message())->toBe('You cannot do that.');
});

it('hides the action when denied with `Response::deny()` and no message', function (): void {
$action = Action::make('test')
->authorize(fn (): Response => Response::deny())
->authorizationNotification();

expect($action->isAuthorizedOrNotHiddenWhenUnauthorized())->toBeFalse();
expect($action->isVisible())->toBeFalse();
});

it('hides the action when the policy returns bare `false`', function (): void {
$action = Action::make('test')
->authorize(fn (): bool => false)
->authorizationNotification();

expect($action->isAuthorizedOrNotHiddenWhenUnauthorized())->toBeFalse();
expect($action->isVisible())->toBeFalse();
});

it('shows the action when `authorizationMessage()` is set even if the policy returns bare `false`', function (): void {
$action = Action::make('test')
->authorize(fn (): bool => false)
->authorizationMessage('Explicit message.')
->authorizationNotification();

expect($action->isAuthorizedOrNotHiddenWhenUnauthorized())->toBeTrue();
expect($action->isVisible())->toBeTrue();
expect($action->getAuthorizationResponseWithMessage()->message())->toBe('Explicit message.');
});

it('is a no-op when `condition: false` is passed', function (): void {
$action = Action::make('test')
->authorize(fn (): bool => false)
->authorizationNotification(false);

expect($action->hasAuthorizationNotification())->toBeFalse();
expect($action->isAuthorizedOrNotHiddenWhenUnauthorized())->toBeFalse();
expect($action->isVisible())->toBeFalse();
});

it('stays hidden when `visible(false)` is also set', function (): void {
$action = Action::make('test')
->authorize(fn (): Response => Response::deny('Has a message.'))
->authorizationNotification()
->visible(false);

expect($action->isVisible())->toBeFalse();
});
});

describe('with `authorizationTooltip()`', function (): void {
it('shows the action when the user is allowed', function (): void {
$action = Action::make('test')
->authorize(fn (): Response => Response::allow())
->authorizationTooltip();

expect($action->isAuthorizedOrNotHiddenWhenUnauthorized())->toBeTrue();
expect($action->isVisible())->toBeTrue();
});

it('shows the action with the deny message as a tooltip when denied with a message', function (): void {
$action = Action::make('test')
->authorize(fn (): Response => Response::deny('You cannot do that.'))
->authorizationTooltip();

expect($action->isAuthorizedOrNotHiddenWhenUnauthorized())->toBeTrue();
expect($action->isVisible())->toBeTrue();
expect($action->hasAuthorizationTooltip())->toBeTrue();
expect($action->getTooltip())->toBe('You cannot do that.');
});

it('hides the action when denied with `Response::deny()` and no message', function (): void {
$action = Action::make('test')
->authorize(fn (): Response => Response::deny())
->authorizationTooltip();

expect($action->isAuthorizedOrNotHiddenWhenUnauthorized())->toBeFalse();
expect($action->isVisible())->toBeFalse();
});

it('hides the action when the policy returns bare `false`', function (): void {
$action = Action::make('test')
->authorize(fn (): bool => false)
->authorizationTooltip();

expect($action->isAuthorizedOrNotHiddenWhenUnauthorized())->toBeFalse();
expect($action->isVisible())->toBeFalse();
});

it('shows the action when `authorizationMessage()` is set even if the policy returns bare `false`', function (): void {
$action = Action::make('test')
->authorize(fn (): bool => false)
->authorizationMessage('Explicit message.')
->authorizationTooltip();

expect($action->isAuthorizedOrNotHiddenWhenUnauthorized())->toBeTrue();
expect($action->isVisible())->toBeTrue();
expect($action->getTooltip())->toBe('Explicit message.');
});

it('is a no-op when `condition: false` is passed', function (): void {
$action = Action::make('test')
->authorize(fn (): bool => false)
->authorizationTooltip(false);

expect($action->hasAuthorizationTooltip())->toBeFalse();
expect($action->isAuthorizedOrNotHiddenWhenUnauthorized())->toBeFalse();
expect($action->isVisible())->toBeFalse();
});

it('stays hidden when `visible(false)` is also set', function (): void {
$action = Action::make('test')
->authorize(fn (): Response => Response::deny('Has a message.'))
->authorizationTooltip()
->visible(false);

expect($action->isVisible())->toBeFalse();
});
});
});
Loading