Fix: Use fill() instead of rawState() when creating another inside CreateRecord (#19343)

* Refactor form state handling to use `fill` instead of `rawState`.

* fix

---------

Co-authored-by: Dan Harrin <git@danharrin.com>
This commit is contained in:
Ibrahim Ismail
2026-02-27 13:42:06 +00:00
committed by GitHub
co-authored by Dan Harrin
parent 41f0963057
commit faaa5cab16
9 changed files with 414 additions and 0 deletions
+4
View File
@@ -132,6 +132,10 @@ class CreateAction extends Action
...$preserveRawState ?? [],
]);
// Rebuild child schemas without double-firing `afterStateHydrated()` hooks.
$hydratedDefaultState = null;
$schema->hydrateState($hydratedDefaultState, shouldCallHydrationHooks: false);
$this->halt();
return;
@@ -143,6 +143,10 @@ class CreateRecord extends Page
...$preserveRawState,
]);
// Rebuild child schemas without double-firing `afterStateHydrated()` hooks.
$hydratedDefaultState = null;
$this->form->hydrateState($hydratedDefaultState, shouldCallHydrationHooks: false);
$this->isCreating = false;
return;
+109
View File
@@ -2,10 +2,17 @@
use Filament\Actions\CreateAction;
use Filament\Actions\Testing\TestAction;
use Filament\Forms\Components\Repeater;
use Filament\Tests\Fixtures\Models\Department;
use Filament\Tests\Fixtures\Models\Post;
use Filament\Tests\Fixtures\Models\Ticket;
use Filament\Tests\Fixtures\Models\User;
use Filament\Tests\Fixtures\Resources\Tickets\Pages\EditTicket;
use Filament\Tests\Fixtures\Resources\Tickets\RelationManagers\DepartmentsRelationManager;
use Filament\Tests\Fixtures\Resources\Tickets\RelationManagers\DepartmentsRelationManagerWithPreservation;
use Filament\Tests\Fixtures\Resources\Users\Pages\EditUser;
use Filament\Tests\Fixtures\Resources\Users\RelationManagers\PostsWithCreateAndPreserveRepeaterRelationManager;
use Filament\Tests\Fixtures\Resources\Users\RelationManagers\PostsWithCreateAndPreserveRepeaterWithDefaultRelationManager;
use Filament\Tests\Panels\Resources\TestCase;
use Illuminate\Support\Str;
@@ -102,6 +109,108 @@ it('can create another record using `CreateAction`', function (): void {
assertDatabaseHas(Department::class, ['name' => $firstName]);
});
it('can create another record and preserve data using `CreateAction`', function (): void {
$ticket = Ticket::factory()->create();
livewire(DepartmentsRelationManagerWithPreservation::class, ['ownerRecord' => $ticket, 'pageClass' => EditTicket::class])
->mountAction(TestAction::make(CreateAction::class)->table(), ['another' => true])
->fillForm([
'name' => $firstName = Str::random(),
])
->callMountedAction()
->assertHasNoFormErrors()
->assertSchemaStateSet([
'name' => $firstName,
])
->fillForm([
'name' => $secondName = Str::random(),
])
->callMountedAction()
->assertHasNoFormErrors();
assertDatabaseHas(Department::class, ['name' => $firstName]);
assertDatabaseHas(Department::class, ['name' => $secondName]);
});
it('can create another record and preserve repeater data using `CreateAction`', function (): void {
$undoRepeaterFake = Repeater::fake();
$user = User::factory()->create();
$repeaterItems = [
['name' => 'First Item', 'email' => 'first@example.com'],
['name' => 'Second Item', 'email' => 'second@example.com'],
];
livewire(PostsWithCreateAndPreserveRepeaterRelationManager::class, ['ownerRecord' => $user, 'pageClass' => EditUser::class])
->mountAction(TestAction::make(CreateAction::class)->table(), ['another' => true])
->fillForm([
'title' => $firstTitle = Str::random(),
'rating' => 5,
'json_array_of_objects' => $repeaterItems,
])
->callMountedAction()
->assertHasNoFormErrors()
->fillForm([
'title' => $secondTitle = Str::random(),
'rating' => 3,
])
->callMountedAction()
->assertHasNoFormErrors();
$record = Post::query()->where('title', $firstTitle)->first();
expect($record)->not->toBeNull();
expect($record->json_array_of_objects)->toBe($repeaterItems);
$record2 = Post::query()->where('title', $secondTitle)->first();
expect($record2)->not->toBeNull();
expect($record2->json_array_of_objects)->toBe($repeaterItems);
$undoRepeaterFake();
});
it('can create another record and preserve repeater data with `default()` values using `CreateAction`', function (): void {
$undoRepeaterFake = Repeater::fake();
$user = User::factory()->create();
$repeaterItems = [
['name' => 'Custom A', 'email' => 'a@example.com'],
['name' => 'Custom B', 'email' => 'b@example.com'],
['name' => 'Custom C', 'email' => 'c@example.com'],
];
livewire(PostsWithCreateAndPreserveRepeaterWithDefaultRelationManager::class, ['ownerRecord' => $user, 'pageClass' => EditUser::class])
->mountAction(TestAction::make(CreateAction::class)->table(), ['another' => true])
->fillForm([
'title' => $firstTitle = Str::random(),
'rating' => 5,
'json_array_of_objects' => $repeaterItems,
])
->callMountedAction()
->assertHasNoFormErrors()
->fillForm([
'title' => $secondTitle = Str::random(),
'rating' => 3,
])
->callMountedAction()
->assertHasNoFormErrors();
$record = Post::query()->where('title', $firstTitle)->first();
expect($record)->not->toBeNull();
expect($record->json_array_of_objects)->toBe($repeaterItems);
$record2 = Post::query()->where('title', $secondTitle)->first();
expect($record2)->not->toBeNull();
expect($record2->json_array_of_objects)->toBe($repeaterItems);
$undoRepeaterFake();
});
it('can cancel `CreateAction` without creating record', function (): void {
$ticket = Ticket::factory()->create();
$initialCount = Department::count();
@@ -0,0 +1,36 @@
<?php
namespace Filament\Tests\Fixtures\Resources\Posts\Pages;
use Filament\Forms;
use Filament\Resources\Pages\CreateRecord;
use Filament\Schemas\Schema;
use Filament\Tests\Fixtures\Resources\Posts\PostResource;
use Illuminate\Support\Arr;
class CreateAnotherPreservingRepeaterPost extends CreateRecord
{
protected static string $resource = PostResource::class;
public function form(Schema $form): Schema
{
return $form
->components([
Forms\Components\TextInput::make('title')->required(),
Forms\Components\TextInput::make('rating')->numeric()->required(),
Forms\Components\Select::make('author_id')
->relationship('author', 'name')
->required(),
Forms\Components\Repeater::make('json_array_of_objects')
->schema([
Forms\Components\TextInput::make('name'),
Forms\Components\TextInput::make('email'),
]),
]);
}
protected function preserveFormDataWhenCreatingAnother(array $data): array
{
return Arr::only($data, ['json_array_of_objects']);
}
}
@@ -0,0 +1,39 @@
<?php
namespace Filament\Tests\Fixtures\Resources\Posts\Pages;
use Filament\Forms;
use Filament\Resources\Pages\CreateRecord;
use Filament\Schemas\Schema;
use Filament\Tests\Fixtures\Resources\Posts\PostResource;
use Illuminate\Support\Arr;
class CreateAnotherPreservingRepeaterWithDefaultPost extends CreateRecord
{
protected static string $resource = PostResource::class;
public function form(Schema $form): Schema
{
return $form
->components([
Forms\Components\TextInput::make('title')->required(),
Forms\Components\TextInput::make('rating')->numeric()->required(),
Forms\Components\Select::make('author_id')
->relationship('author', 'name')
->required(),
Forms\Components\Repeater::make('json_array_of_objects')
->schema([
Forms\Components\TextInput::make('name'),
Forms\Components\TextInput::make('email'),
])
->default([
['name' => 'Default Name', 'email' => 'default@example.com'],
]),
]);
}
protected function preserveFormDataWhenCreatingAnother(array $data): array
{
return Arr::only($data, ['json_array_of_objects']);
}
}
@@ -0,0 +1,29 @@
<?php
namespace Filament\Tests\Fixtures\Resources\Tickets\RelationManagers;
use Filament\Actions\CreateAction;
use Filament\Resources\RelationManagers\RelationManager;
use Filament\Schemas\Schema;
use Filament\Tables\Table;
use Filament\Tests\Fixtures\Resources\Departments\Schemas\DepartmentForm;
use Filament\Tests\Fixtures\Resources\Departments\Tables\DepartmentsTable;
class DepartmentsRelationManagerWithPreservation extends RelationManager
{
protected static string $relationship = 'departments';
public function table(Table $table): Table
{
return DepartmentsTable::configure($table)
->headerActions([
CreateAction::make()
->preserveFormDataWhenCreatingAnother(['name']),
]);
}
public function form(Schema $schema): Schema
{
return DepartmentForm::configure($schema);
}
}
@@ -0,0 +1,44 @@
<?php
namespace Filament\Tests\Fixtures\Resources\Users\RelationManagers;
use Filament\Actions\CreateAction;
use Filament\Forms\Components\Repeater;
use Filament\Forms\Components\TextInput;
use Filament\Resources\RelationManagers\RelationManager;
use Filament\Schemas\Schema;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
class PostsWithCreateAndPreserveRepeaterRelationManager extends RelationManager
{
protected static string $relationship = 'posts';
public function table(Table $table): Table
{
return $table
->recordTitleAttribute('title')
->inverseRelationship('author')
->columns([
TextColumn::make('title'),
])
->headerActions([
CreateAction::make()
->preserveFormDataWhenCreatingAnother(['json_array_of_objects']),
]);
}
public function form(Schema $schema): Schema
{
return $schema
->components([
TextInput::make('title')->required(),
TextInput::make('rating')->numeric()->required(),
Repeater::make('json_array_of_objects')
->schema([
TextInput::make('name'),
TextInput::make('email'),
]),
]);
}
}
@@ -0,0 +1,47 @@
<?php
namespace Filament\Tests\Fixtures\Resources\Users\RelationManagers;
use Filament\Actions\CreateAction;
use Filament\Forms\Components\Repeater;
use Filament\Forms\Components\TextInput;
use Filament\Resources\RelationManagers\RelationManager;
use Filament\Schemas\Schema;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
class PostsWithCreateAndPreserveRepeaterWithDefaultRelationManager extends RelationManager
{
protected static string $relationship = 'posts';
public function table(Table $table): Table
{
return $table
->recordTitleAttribute('title')
->inverseRelationship('author')
->columns([
TextColumn::make('title'),
])
->headerActions([
CreateAction::make()
->preserveFormDataWhenCreatingAnother(['json_array_of_objects']),
]);
}
public function form(Schema $schema): Schema
{
return $schema
->components([
TextInput::make('title')->required(),
TextInput::make('rating')->numeric()->required(),
Repeater::make('json_array_of_objects')
->schema([
TextInput::make('name'),
TextInput::make('email'),
])
->default([
['name' => 'Default Name', 'email' => 'default@example.com'],
]),
]);
}
}
@@ -1,11 +1,14 @@
<?php
use Filament\Facades\Filament;
use Filament\Forms\Components\Repeater;
use Filament\Resources\Events\RecordCreated;
use Filament\Resources\Events\RecordSaved;
use Filament\Tests\Fixtures\Models\Post;
use Filament\Tests\Fixtures\Policies\TicketPolicy;
use Filament\Tests\Fixtures\Resources\Posts\Pages\CreateAnotherPreservingDataPost;
use Filament\Tests\Fixtures\Resources\Posts\Pages\CreateAnotherPreservingRepeaterPost;
use Filament\Tests\Fixtures\Resources\Posts\Pages\CreateAnotherPreservingRepeaterWithDefaultPost;
use Filament\Tests\Fixtures\Resources\Posts\Pages\CreatePost;
use Filament\Tests\Fixtures\Resources\Posts\PostResource;
use Filament\Tests\Fixtures\Resources\TicketMessages\TicketMessageResource;
@@ -181,6 +184,105 @@ it('can create another and preserve data', function (): void {
expect($record2->tags)->toBe($newData->tags);
});
it('can create another and preserve repeater data', function (): void {
$undoRepeaterFake = Repeater::fake();
$newData = Post::factory()->make();
$newData2 = Post::factory()->make();
$repeaterItems = [
['name' => 'First Item', 'email' => 'first@example.com'],
['name' => 'Second Item', 'email' => 'second@example.com'],
];
livewire(CreateAnotherPreservingRepeaterPost::class)
->fillForm([
'author_id' => $newData->author->getKey(),
'title' => $newData->title,
'rating' => $newData->rating,
'json_array_of_objects' => $repeaterItems,
])
->call('create', true)
->assertHasNoFormErrors()
->assertNoRedirect()
->fillForm([
'author_id' => $newData2->author->getKey(),
'title' => $newData2->title,
'rating' => $newData2->rating,
])
->call('create')
->assertHasNoFormErrors()
->assertRedirect();
$record = Post::query()
->where('title', $newData->title)
->where('rating', $newData->rating)
->first();
expect($record)->not->toBeNull();
expect($record->json_array_of_objects)->toBe($repeaterItems);
$record2 = Post::query()
->where('title', $newData2->title)
->where('rating', $newData2->rating)
->first();
expect($record2)->not->toBeNull();
expect($record2->json_array_of_objects)->toBe($repeaterItems);
$undoRepeaterFake();
});
it('can create another and preserve repeater data with `default()` values', function (): void {
$undoRepeaterFake = Repeater::fake();
$newData = Post::factory()->make();
$newData2 = Post::factory()->make();
$repeaterItems = [
['name' => 'Custom Item A', 'email' => 'a@example.com'],
['name' => 'Custom Item B', 'email' => 'b@example.com'],
['name' => 'Custom Item C', 'email' => 'c@example.com'],
];
livewire(CreateAnotherPreservingRepeaterWithDefaultPost::class)
->fillForm([
'author_id' => $newData->author->getKey(),
'title' => $newData->title,
'rating' => $newData->rating,
'json_array_of_objects' => $repeaterItems,
])
->call('create', true)
->assertHasNoFormErrors()
->assertNoRedirect()
->fillForm([
'author_id' => $newData2->author->getKey(),
'title' => $newData2->title,
'rating' => $newData2->rating,
])
->call('create')
->assertHasNoFormErrors()
->assertRedirect();
$record = Post::query()
->where('title', $newData->title)
->where('rating', $newData->rating)
->first();
expect($record)->not->toBeNull();
expect($record->json_array_of_objects)->toBe($repeaterItems);
$record2 = Post::query()
->where('title', $newData2->title)
->where('rating', $newData2->rating)
->first();
expect($record2)->not->toBeNull();
expect($record2->json_array_of_objects)->toBe($repeaterItems);
$undoRepeaterFake();
});
it('can validate input', function (): void {
Post::factory()->make();