Skip to content

Commit 2f10baa

Browse files
fix: hide actions with authorization feedback when denial has no message (#19792)
* Add "or hidden" mode for action auth tooltips and notifications * Update 01-overview.md * Make new functionality the default * test: relocate authorization tests into ActionTest.php Moves the authorization feedback test cases from a standalone AuthorizationFeedbackTest.php into a `describe('authorization', ...)` block in ActionTest.php, alongside related Action behaviour tests. Adds four baseline tests for the wider authorization concern (default visibility, `authorize(false)` without feedback methods, `Response` acceptance, and `authorizationMessage()` chaining), since no prior tests covered any of `CanBeAuthorized`. --------- Co-authored-by: Dan Harrin <git@danharrin.com>
1 parent b360824 commit 2f10baa

3 files changed

Lines changed: 182 additions & 4 deletions

File tree

packages/actions/docs/01-overview.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,8 @@ Action::make('edit')
262262
->authorizationTooltip()
263263
```
264264

265+
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.
266+
265267
<AutoScreenshot name="actions/trigger-button/authorization-tooltip" alt="Disabled action button with an authorization tooltip" version="4.x" />
266268

267269
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:
@@ -275,6 +277,8 @@ Action::make('edit')
275277
->authorizationNotification()
276278
```
277279

280+
As with `authorizationTooltip()`, the action is hidden if the denial does not provide a message, unless you supply a fallback with `authorizationMessage()`.
281+
278282
### Disabling a button
279283

280284
If you want to disable a button instead of hiding it, you can use the `disabled()` method:

packages/actions/src/Concerns/CanBeAuthorized.php

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -205,15 +205,17 @@ public function hasAuthorizationNotification(): bool
205205

206206
public function isAuthorizedOrNotHiddenWhenUnauthorized(): bool
207207
{
208-
if ($this->hasAuthorizationTooltip()) {
209-
return true;
208+
if (! $this->hasAuthorizationTooltip() && ! $this->hasAuthorizationNotification()) {
209+
return $this->isAuthorized();
210210
}
211211

212-
if ($this->hasAuthorizationNotification()) {
212+
$response = $this->getAuthorizationResponse();
213+
214+
if ($response->allowed()) {
213215
return true;
214216
}
215217

216-
return $this->isAuthorized();
218+
return filled($response->message()) || filled($this->getAuthorizationMessage());
217219
}
218220

219221
public function authorizeIndividualRecords(bool | string | Closure | null $callback = true): static

tests/src/Actions/ActionTest.php

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
use Filament\Tests\Actions\TestCase;
1212
use Filament\Tests\Fixtures\Models\Post;
1313
use Filament\Tests\Fixtures\Pages\Actions;
14+
use Illuminate\Auth\Access\Response;
1415
use Illuminate\Support\Str;
1516

1617
use function Filament\Tests\livewire;
@@ -1548,3 +1549,174 @@
15481549
expect($action)->toBeInstanceOf(Action::class);
15491550
});
15501551
});
1552+
1553+
describe('authorization', function (): void {
1554+
it('is visible by default when no `authorize()` is configured', function (): void {
1555+
$action = Action::make('test');
1556+
1557+
expect($action->isAuthorizedOrNotHiddenWhenUnauthorized())->toBeTrue();
1558+
expect($action->isVisible())->toBeTrue();
1559+
});
1560+
1561+
it('is hidden when `authorize()` returns `false` and no auth feedback method is set', function (): void {
1562+
$action = Action::make('test')
1563+
->authorize(fn (): bool => false);
1564+
1565+
expect($action->isAuthorizedOrNotHiddenWhenUnauthorized())->toBeFalse();
1566+
expect($action->isVisible())->toBeFalse();
1567+
});
1568+
1569+
it('accepts an `authorize()` closure returning a `Response`', function (): void {
1570+
$action = Action::make('test')
1571+
->authorize(fn (): Response => Response::deny('Nope.'));
1572+
1573+
expect($action->getAuthorizationResponse()->message())->toBe('Nope.');
1574+
});
1575+
1576+
it('can chain `authorizationMessage()` to set a fallback message', function (): void {
1577+
$action = Action::make('test')
1578+
->authorizationMessage('Custom fallback.');
1579+
1580+
expect($action->getAuthorizationMessage())->toBe('Custom fallback.');
1581+
});
1582+
1583+
describe('with `authorizationNotification()`', function (): void {
1584+
it('shows the action when the user is allowed', function (): void {
1585+
$action = Action::make('test')
1586+
->authorize(fn (): Response => Response::allow())
1587+
->authorizationNotification();
1588+
1589+
expect($action->isAuthorizedOrNotHiddenWhenUnauthorized())->toBeTrue();
1590+
expect($action->isVisible())->toBeTrue();
1591+
});
1592+
1593+
it('shows the action when denied with a message', function (): void {
1594+
$action = Action::make('test')
1595+
->authorize(fn (): Response => Response::deny('You cannot do that.'))
1596+
->authorizationNotification();
1597+
1598+
expect($action->isAuthorizedOrNotHiddenWhenUnauthorized())->toBeTrue();
1599+
expect($action->isVisible())->toBeTrue();
1600+
expect($action->hasAuthorizationNotification())->toBeTrue();
1601+
expect($action->getAuthorizationResponseWithMessage()->message())->toBe('You cannot do that.');
1602+
});
1603+
1604+
it('hides the action when denied with `Response::deny()` and no message', function (): void {
1605+
$action = Action::make('test')
1606+
->authorize(fn (): Response => Response::deny())
1607+
->authorizationNotification();
1608+
1609+
expect($action->isAuthorizedOrNotHiddenWhenUnauthorized())->toBeFalse();
1610+
expect($action->isVisible())->toBeFalse();
1611+
});
1612+
1613+
it('hides the action when the policy returns bare `false`', function (): void {
1614+
$action = Action::make('test')
1615+
->authorize(fn (): bool => false)
1616+
->authorizationNotification();
1617+
1618+
expect($action->isAuthorizedOrNotHiddenWhenUnauthorized())->toBeFalse();
1619+
expect($action->isVisible())->toBeFalse();
1620+
});
1621+
1622+
it('shows the action when `authorizationMessage()` is set even if the policy returns bare `false`', function (): void {
1623+
$action = Action::make('test')
1624+
->authorize(fn (): bool => false)
1625+
->authorizationMessage('Explicit message.')
1626+
->authorizationNotification();
1627+
1628+
expect($action->isAuthorizedOrNotHiddenWhenUnauthorized())->toBeTrue();
1629+
expect($action->isVisible())->toBeTrue();
1630+
expect($action->getAuthorizationResponseWithMessage()->message())->toBe('Explicit message.');
1631+
});
1632+
1633+
it('is a no-op when `condition: false` is passed', function (): void {
1634+
$action = Action::make('test')
1635+
->authorize(fn (): bool => false)
1636+
->authorizationNotification(false);
1637+
1638+
expect($action->hasAuthorizationNotification())->toBeFalse();
1639+
expect($action->isAuthorizedOrNotHiddenWhenUnauthorized())->toBeFalse();
1640+
expect($action->isVisible())->toBeFalse();
1641+
});
1642+
1643+
it('stays hidden when `visible(false)` is also set', function (): void {
1644+
$action = Action::make('test')
1645+
->authorize(fn (): Response => Response::deny('Has a message.'))
1646+
->authorizationNotification()
1647+
->visible(false);
1648+
1649+
expect($action->isVisible())->toBeFalse();
1650+
});
1651+
});
1652+
1653+
describe('with `authorizationTooltip()`', function (): void {
1654+
it('shows the action when the user is allowed', function (): void {
1655+
$action = Action::make('test')
1656+
->authorize(fn (): Response => Response::allow())
1657+
->authorizationTooltip();
1658+
1659+
expect($action->isAuthorizedOrNotHiddenWhenUnauthorized())->toBeTrue();
1660+
expect($action->isVisible())->toBeTrue();
1661+
});
1662+
1663+
it('shows the action with the deny message as a tooltip when denied with a message', function (): void {
1664+
$action = Action::make('test')
1665+
->authorize(fn (): Response => Response::deny('You cannot do that.'))
1666+
->authorizationTooltip();
1667+
1668+
expect($action->isAuthorizedOrNotHiddenWhenUnauthorized())->toBeTrue();
1669+
expect($action->isVisible())->toBeTrue();
1670+
expect($action->hasAuthorizationTooltip())->toBeTrue();
1671+
expect($action->getTooltip())->toBe('You cannot do that.');
1672+
});
1673+
1674+
it('hides the action when denied with `Response::deny()` and no message', function (): void {
1675+
$action = Action::make('test')
1676+
->authorize(fn (): Response => Response::deny())
1677+
->authorizationTooltip();
1678+
1679+
expect($action->isAuthorizedOrNotHiddenWhenUnauthorized())->toBeFalse();
1680+
expect($action->isVisible())->toBeFalse();
1681+
});
1682+
1683+
it('hides the action when the policy returns bare `false`', function (): void {
1684+
$action = Action::make('test')
1685+
->authorize(fn (): bool => false)
1686+
->authorizationTooltip();
1687+
1688+
expect($action->isAuthorizedOrNotHiddenWhenUnauthorized())->toBeFalse();
1689+
expect($action->isVisible())->toBeFalse();
1690+
});
1691+
1692+
it('shows the action when `authorizationMessage()` is set even if the policy returns bare `false`', function (): void {
1693+
$action = Action::make('test')
1694+
->authorize(fn (): bool => false)
1695+
->authorizationMessage('Explicit message.')
1696+
->authorizationTooltip();
1697+
1698+
expect($action->isAuthorizedOrNotHiddenWhenUnauthorized())->toBeTrue();
1699+
expect($action->isVisible())->toBeTrue();
1700+
expect($action->getTooltip())->toBe('Explicit message.');
1701+
});
1702+
1703+
it('is a no-op when `condition: false` is passed', function (): void {
1704+
$action = Action::make('test')
1705+
->authorize(fn (): bool => false)
1706+
->authorizationTooltip(false);
1707+
1708+
expect($action->hasAuthorizationTooltip())->toBeFalse();
1709+
expect($action->isAuthorizedOrNotHiddenWhenUnauthorized())->toBeFalse();
1710+
expect($action->isVisible())->toBeFalse();
1711+
});
1712+
1713+
it('stays hidden when `visible(false)` is also set', function (): void {
1714+
$action = Action::make('test')
1715+
->authorize(fn (): Response => Response::deny('Has a message.'))
1716+
->authorizationTooltip()
1717+
->visible(false);
1718+
1719+
expect($action->isVisible())->toBeFalse();
1720+
});
1721+
});
1722+
});

0 commit comments

Comments
 (0)