Skip to content
Open
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
2 changes: 2 additions & 0 deletions docs/05-panel-configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,8 @@ public function panel(Panel $panel): Panel
}
```

Action modals that cannot contain unsaved changes, such as the read-only `ViewAction` modal, do not trigger the alert. You may also [disable the alert for a specific action](actions/modals#disabling-the-unsaved-changes-alert) using the `unsavedChangesAlert(false)` method.

## Enabling database transactions

By default, Filament does not wrap operations in database transactions, and allows the user to enable this themselves when they have tested to ensure that their operations are safe to be wrapped in a transaction. However, you can enable database transactions at once for all operations by using the `databaseTransactions()` method:
Expand Down
21 changes: 21 additions & 0 deletions packages/actions/docs/02-modals.md
Original file line number Diff line number Diff line change
Expand Up @@ -747,6 +747,27 @@ use Filament\Support\View\Components\ModalComponent;
ModalComponent::closedByEscaping(false);
```

### Disabling the unsaved changes alert

When [unsaved changes alerts](../panel-configuration#unsaved-changes-alerts) are enabled for a panel, users are warned before leaving the page while an action modal is open. If a specific action's modal cannot contain unsaved changes, you can disable the warning for it using the `unsavedChangesAlert(false)` method:

```php
use Filament\Actions\Action;

Action::make('updateAuthor')
->schema([
// ...
])
->action(function (array $data): void {
// ...
})
->unsavedChangesAlert(false)
```

<UtilityInjection set="actions" version="5.x">The `unsavedChangesAlert()` method also accepts a function to dynamically calculate the value. You can inject various utilities into the function as parameters.</UtilityInjection>

By default, actions with a [disabled schema](#disabling-all-form-fields), such as `ViewAction`, do not trigger the alert, since their modals do not accept user input.

### Hiding the modal close button

By default, modals have a close button in the top right corner. If you wish to hide the close button, you can use the `modalCloseButton(false)` method:
Expand Down
14 changes: 14 additions & 0 deletions packages/actions/src/Concerns/CanOpenModal.php
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,8 @@ trait CanOpenModal

protected bool | Closure | null $isModalAutofocused = null;

protected bool | Closure | null $hasUnsavedChangesAlert = null;

protected string | BackedEnum | Htmlable | Closure | null $modalIcon = null;

/**
Expand Down Expand Up @@ -121,6 +123,13 @@ public function closeModalByEscaping(bool | Closure | null $condition = true): s
return $this;
}

public function unsavedChangesAlert(bool | Closure | null $condition = true): static
{
$this->hasUnsavedChangesAlert = $condition;

return $this;
}

/**
* @deprecated Use `modalAlignment(Alignment::Center)` instead.
*/
Expand Down Expand Up @@ -730,6 +739,11 @@ public function isModalAutofocused(): bool
return $this->evaluate($this->isModalAutofocused) ?? ModalComponent::$isAutofocused;
}

public function hasUnsavedChangesAlert(): bool
{
return (bool) ($this->evaluate($this->hasUnsavedChangesAlert) ?? (! $this->isSchemaDisabled()));
}

/**
* @deprecated Use `makeModalSubmitAction()` instead.
*
Expand Down
2 changes: 2 additions & 0 deletions packages/actions/src/Concerns/InteractsWithActions.php
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,8 @@ public function mountAction(string $name, array $arguments = [], array $context
throw $exception;
}

$this->mountedActions[array_key_last($this->mountedActions)]['hasUnsavedChangesAlert'] = $action->hasUnsavedChangesAlert();

if (! $this->mountedActionShouldOpenModal(mountedAction: $action)) {
if ($context['mountedFromUrl'] ?? false) {
// A modal-less action mounted from the URL has nothing to show the user, so
Expand Down
4 changes: 3 additions & 1 deletion packages/panels/resources/js/unsaved-changes-alert.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,9 @@ window.setUpUnsavedActionChangesAlert = ({
}

if (
($wire.mountedActions?.length ?? 0) &&
($wire.mountedActions ?? []).some(
(mountedAction) => mountedAction.hasUnsavedChangesAlert ?? true,
) &&
!$wire?.__instance?.effects?.redirect
) {
event.preventDefault()
Expand Down
36 changes: 34 additions & 2 deletions tests/src/Actions/ActionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
use Filament\Schemas\Concerns\InteractsWithSchemas;
use Filament\Schemas\Contracts\HasSchemas;
use Filament\Support\Colors\Color;
use Filament\Support\Enums\IconPosition;
use Filament\Support\Enums\IconSize;
use Filament\Support\Enums\Size;
use Filament\Support\Enums\Width;
use Filament\Support\Facades\FilamentView;
Expand Down Expand Up @@ -232,6 +234,36 @@
});
});

describe('unsaved changes alerts', function (): void {
it('flags a mounted action with an editable schema for the unsaved changes alert', function (): void {
livewire(Actions::class)
->mountAction('data')
->assertSet('mountedActions.0.hasUnsavedChangesAlert', true);
});

it('does not flag a mounted action with a disabled schema for the unsaved changes alert', function (): void {
livewire(Actions::class)
->mountAction('disabledSchema')
->assertSet('mountedActions.0.hasUnsavedChangesAlert', false);
});

it('does not flag a mounted action that opts out of the unsaved changes alert', function (): void {
livewire(Actions::class)
->mountAction('withoutUnsavedChangesAlert')
->assertSet('mountedActions.0.hasUnsavedChangesAlert', false);
});

it('has an unsaved changes alert by default', function (): void {
expect(Action::make('test')->hasUnsavedChangesAlert())
->toBeTrue();
});

it('can opt an action with a disabled schema back in to the unsaved changes alert', function (): void {
expect(Action::make('test')->disabledSchema()->unsavedChangesAlert()->hasUnsavedChangesAlert())
->toBeTrue();
});
});

describe('arguments', function (): void {
it('can mount an action with arguments', function (): void {
livewire(Actions::class)
Expand Down Expand Up @@ -1945,7 +1977,7 @@
it('renders an `iconPosition(After)` icon after the label', function (): void {
$html = Action::make('test')
->icon('heroicon-o-arrow-right')
->iconPosition(\Filament\Support\Enums\IconPosition::After)
->iconPosition(IconPosition::After)
->label('Next')
->toHtml();

Expand Down Expand Up @@ -1979,7 +2011,7 @@
it('renders an `iconSize()` as a `fi-size-*` class on the icon', function (): void {
$html = Action::make('test')
->icon('heroicon-o-trash')
->iconSize(\Filament\Support\Enums\IconSize::Large)
->iconSize(IconSize::Large)
->toHtml();

expect($html)->toContain('fi-size-lg');
Expand Down
9 changes: 9 additions & 0 deletions tests/src/Actions/ViewActionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,15 @@
->assertSchemaStateSet(['name' => 'Department 2']);
});

it('does not flag a mounted `ViewAction` for the unsaved changes alert', function (): void {
$ticket = Ticket::factory()->create();
$department = Department::factory()->hasAttached($ticket)->create();

livewire(DepartmentsRelationManager::class, ['ownerRecord' => $ticket, 'pageClass' => EditTicket::class])
->mountAction(TestAction::make(ViewAction::class)->table($department))
->assertSet('mountedActions.0.hasUnsavedChangesAlert', false);
});

it('returns `view` from `getDefaultName()`', function (): void {
expect(ViewAction::getDefaultName())->toBe('view');
});
16 changes: 16 additions & 0 deletions tests/src/Fixtures/Pages/Actions.php
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,22 @@ protected function getHeaderActions(): array
->action(function (): void {
$this->dispatch('enforcement-authorized-called');
}),
Action::make('disabledSchema')
->schema([
TextInput::make('payload'),
])
->disabledSchema()
->action(function (): void {
$this->dispatch('disabled-schema-called');
}),
Action::make('withoutUnsavedChangesAlert')
->schema([
TextInput::make('payload'),
])
->unsavedChangesAlert(false)
->action(function (): void {
$this->dispatch('without-unsaved-changes-alert-called');
}),
];
}
}