Skip to content
Draft
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
36 changes: 36 additions & 0 deletions packages/actions/docs/02-modals.md
Original file line number Diff line number Diff line change
Expand Up @@ -524,6 +524,42 @@ Action::make('edit')

Now, the edit modal will have a "Delete" button in the footer, which will open a confirmation modal when clicked. This action is completely independent of the `edit` action, and will not run the `edit` action when it is clicked.

#### Grouping extra footer actions

You can use `ActionGroup` to group related actions together in the modal footer:

```php
use Filament\Actions\Action;
use Filament\Actions\ActionGroup;

Action::make('create')
->schema([
// ...
])
->extraModalFooterActions([
Action::make('createAnother')
->action(function () {
// ...
}),
ActionGroup::make([
Action::make('createAndEmail')
->action(function () {
// ...
}),
Action::make('createAndNotify')
->action(function () {
// ...
}),
Action::make('createDraft')
->action(function () {
// ...
}),
])
->button()
->label('More Options'),
])
```

In this example though, you probably want to cancel the `edit` action if the `delete` action is run. You can do this using the `cancelParentActions()` method:

```php
Expand Down
57 changes: 46 additions & 11 deletions packages/actions/src/Concerns/CanOpenModal.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use BackedEnum;
use Closure;
use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Actions\View\ActionsIconAlias;
use Filament\Support\Enums\Alignment;
use Filament\Support\Enums\Width;
Expand All @@ -18,12 +19,12 @@
trait CanOpenModal
{
/**
* @var array<string, Action>
* @var array<string, Action | ActionGroup>
*/
protected array $cachedExtraModalFooterActions;

/**
* @var array<Action> | Closure
* @var array<Action | ActionGroup> | Closure
*/
protected array | Closure $extraModalFooterActions = [];

Expand All @@ -46,7 +47,7 @@ trait CanOpenModal
protected Alignment | string | Closure | null $modalAlignment = null;

/**
* @var array<string, Action>
* @var array<string, Action | ActionGroup>
*/
protected array $cachedModalFooterActions;

Expand Down Expand Up @@ -195,7 +196,7 @@ public function modalFooterActionsAlignment(Alignment | string | Closure | null
}

/**
* @param array<Action> | Closure $actions
* @param array<Action | ActionGroup> | Closure $actions
*
*@deprecated Use `extraModalFooterActions()` instead.
*/
Expand All @@ -207,7 +208,7 @@ public function extraModalActions(array | Closure $actions): static
}

/**
* @param array<Action> | Closure $actions
* @param array<Action | ActionGroup> | Closure $actions
*/
public function extraModalFooterActions(array | Closure $actions): static
{
Expand Down Expand Up @@ -340,7 +341,7 @@ public function modalHidden(bool | Closure | null $condition = true): static
}

/**
* @return array<string, Action>
* @return array<string, Action | ActionGroup>
*/
public function getModalFooterActions(): array
{
Expand Down Expand Up @@ -406,7 +407,17 @@ public function getModalActions(): array
return $this->cachedModalActions;
}

$actions = $this->getModalFooterActions();
$actions = [];

foreach ($this->getModalFooterActions() as $key => $action) {
if ($action instanceof ActionGroup) {
foreach ($action->getFlatActions() as $flatAction) {
$actions[$flatAction->getName()] = $flatAction;
}
} else {
$actions[$key] = $action;
}
}

foreach ($this->modalActions as $action) {
foreach (Arr::wrap($this->evaluate($action)) as $modalAction) {
Expand Down Expand Up @@ -435,14 +446,33 @@ public function prepareModalAction(Action $action): Action
->table($this->getTable());
}

protected function prepareActionGroup(ActionGroup $group): ActionGroup
{
$group
->schemaContainer($this->getSchemaContainer())
->schemaComponent($this->getSchemaComponent())
->livewire($this->getLivewire())
->when(
! $group->hasRecord(),
fn (ActionGroup $group) => $group->record($this->getRecord()),
)
->table($this->getTable());

foreach ($group->getFlatActions() as $nestedAction) {
$this->prepareModalAction($nestedAction);
}

return $group;
}

/**
* @return array<Action>
* @return array<Action | ActionGroup>
*/
public function getVisibleModalFooterActions(): array
{
return array_filter(
$this->getModalFooterActions(),
fn (Action $action): bool => $action->isVisible(),
fn (Action | ActionGroup $action): bool => $action->isVisible(),
);
}

Expand Down Expand Up @@ -489,7 +519,7 @@ public function getModalCancelAction(): ?Action
}

/**
* @return array<Action>
* @return array<Action | ActionGroup>
*/
public function getExtraModalFooterActions(): array
{
Expand All @@ -500,7 +530,12 @@ public function getExtraModalFooterActions(): array
$actions = [];

foreach ($this->evaluate($this->extraModalFooterActions) as $action) {
$actions[$action->getName()] = $this->prepareModalAction($action);
if ($action instanceof ActionGroup) {
$key = 'group_' . spl_object_id($action);
$actions[$key] = $this->prepareActionGroup($action);
} else {
$actions[$action->getName()] = $this->prepareModalAction($action);
}
}

return $this->cachedExtraModalFooterActions = $actions;
Expand Down
58 changes: 58 additions & 0 deletions tests/src/Actions/ActionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -445,3 +445,61 @@
->callAction('shows-notification-with-id')
->assertNotNotified('A notification');
});

it('can call an action registered alongside an ActionGroup in extraModalFooterActions', function (): void {
livewire(Actions::class)
->callAction([
'withGroupedExtraActions',
TestAction::make('simpleExtra'),
])
->assertDispatched('simple-extra-called');
});

it('can call an action with data registered in an ActionGroup in extraModalFooterActions', function (): void {
livewire(Actions::class)
->callAction([
'withGroupedExtraActions',
TestAction::make('option3'),
], [
'value' => $value = Str::random(),
])
->assertHasNoFormErrors()
->assertDispatched('option3-called', value: $value);
});

it('can mount an action that has an ActionGroup in extraModalFooterActions', function (): void {
livewire(Actions::class)
->mountAction('withGroupedExtraActions')
->assertActionMounted('withGroupedExtraActions');
});

it('can call multiple actions registered in an ActionGroup in extraModalFooterActions', function (): void {
livewire(Actions::class)
->callAction([
'withGroupedExtraActions',
TestAction::make('option1'),
])
->assertDispatched('option1-called');

livewire(Actions::class)
->callAction([
'withGroupedExtraActions',
TestAction::make('option2'),
])
->assertDispatched('option2-called');
});

it('can submit parent action after calling an action registered in an ActionGroup in extraModalFooterActions', function (): void {
livewire(Actions::class)
->callAction([
'withGroupedExtraActions',
TestAction::make('option1'),
])
->assertDispatched('option1-called')
->fillForm([
'content' => $content = Str::random(),
])
->callMountedAction()
->assertHasNoActionErrors()
->assertDispatched('grouped-extra-actions-called', content: $content);
});
24 changes: 24 additions & 0 deletions tests/src/Fixtures/Livewire/PostsTable.php
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,30 @@ public function table(Table $table): Table
->action(fn (array $data, Post $record) => $this->dispatch('nested-called', bar: $data['bar'], recordKey: $record->getKey())),
]),
]),
Action::make('withGroupedExtraActions')
->schema([
TextInput::make('content')
->required(),
])
->action(function (array $data, Post $record): void {
$this->dispatch('grouped-extra-actions-called', content: $data['content'], recordKey: $record->getKey());
})
->extraModalFooterActions([
Action::make('simpleExtra')
->action(fn (Post $record) => $this->dispatch('simple-extra-called', recordKey: $record->getKey())),
ActionGroup::make([
Action::make('option1')
->action(fn (Post $record) => $this->dispatch('option1-called', recordKey: $record->getKey())),
Action::make('option2')
->action(fn (Post $record) => $this->dispatch('option2-called', recordKey: $record->getKey())),
Action::make('option3')
->schema([
TextInput::make('value')
->required(),
])
->action(fn (array $data, Post $record) => $this->dispatch('option3-called', value: $data['value'], recordKey: $record->getKey())),
])->button()->label('More Options'),
]),
])
->toolbarActions([
DeleteBulkAction::make(),
Expand Down
25 changes: 25 additions & 0 deletions tests/src/Fixtures/Pages/Actions.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace Filament\Tests\Fixtures\Pages;

use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Forms\Components\TextInput;
use Filament\Notifications\Notification;
use Filament\Pages\Page;
Expand Down Expand Up @@ -121,6 +122,30 @@ protected function getHeaderActions(): array

$action->halt();
}),
Action::make('withGroupedExtraActions')
->schema([
TextInput::make('content')
->required(),
])
->action(function (array $data): void {
$this->dispatch('grouped-extra-actions-called', content: $data['content']);
})
->extraModalFooterActions([
Action::make('simpleExtra')
->action(fn () => $this->dispatch('simple-extra-called')),
ActionGroup::make([
Action::make('option1')
->action(fn () => $this->dispatch('option1-called')),
Action::make('option2')
->action(fn () => $this->dispatch('option2-called')),
Action::make('option3')
->schema([
TextInput::make('value')
->required(),
])
->action(fn (array $data) => $this->dispatch('option3-called', value: $data['value'])),
])->button()->label('More Options'),
]),
Action::make('visible'),
Action::make('hidden')
->hidden(),
Expand Down
68 changes: 68 additions & 0 deletions tests/src/Tables/Actions/ActionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -409,3 +409,71 @@
'title' => $post->title . ' (Copy)',
]);
});

it('can call an action registered alongside an ActionGroup in extraModalFooterActions', function (): void {
$post = Post::factory()->create();

livewire(PostsTable::class)
->callAction([
TestAction::make('withGroupedExtraActions')->table($post),
TestAction::make('simpleExtra'),
])
->assertDispatched('simple-extra-called', recordKey: $post->getKey());
});

it('can call an action with data registered in an ActionGroup in extraModalFooterActions', function (): void {
$post = Post::factory()->create();

livewire(PostsTable::class)
->callAction([
TestAction::make('withGroupedExtraActions')->table($post),
TestAction::make('option3'),
], [
'value' => $value = Str::random(),
])
->assertHasNoFormErrors()
->assertDispatched('option3-called', value: $value, recordKey: $post->getKey());
});

it('can mount an action that has an ActionGroup in extraModalFooterActions', function (): void {
$post = Post::factory()->create();

livewire(PostsTable::class)
->mountTableAction('withGroupedExtraActions', $post)
->assertTableActionMounted('withGroupedExtraActions');
});

it('can call multiple actions registered in an ActionGroup in extraModalFooterActions', function (): void {
$post = Post::factory()->create();

livewire(PostsTable::class)
->callAction([
TestAction::make('withGroupedExtraActions')->table($post),
TestAction::make('option1'),
])
->assertDispatched('option1-called', recordKey: $post->getKey());

livewire(PostsTable::class)
->callAction([
TestAction::make('withGroupedExtraActions')->table($post),
TestAction::make('option2'),
])
->assertDispatched('option2-called', recordKey: $post->getKey());
});

it('can submit parent action after calling an action registered in an ActionGroup in extraModalFooterActions', function (): void {
$post = Post::factory()->create();

livewire(PostsTable::class)
->callAction([
TestAction::make('withGroupedExtraActions')->table($post),
TestAction::make('option1'),
])
->assertDispatched('option1-called', recordKey: $post->getKey())
->fillForm([
'content' => $content = Str::random(),
])
->callMountedTableAction()
->assertHasNoTableActionErrors()
->assertDispatched('grouped-extra-actions-called', content: $content, recordKey: $post->getKey());
});
Loading