From faaa5cab1621181c0be3b23842d1e814db2ff3ef Mon Sep 17 00:00:00 2001 From: Ibrahim Ismail <162009874+robot-iraq@users.noreply.github.com> Date: Fri, 27 Feb 2026 16:42:06 +0300 Subject: [PATCH] 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 --- packages/actions/src/CreateAction.php | 4 + .../src/Resources/Pages/CreateRecord.php | 4 + tests/src/Actions/CreateActionTest.php | 109 ++++++++++++++++++ .../CreateAnotherPreservingRepeaterPost.php | 36 ++++++ ...otherPreservingRepeaterWithDefaultPost.php | 39 +++++++ ...rtmentsRelationManagerWithPreservation.php | 29 +++++ ...eateAndPreserveRepeaterRelationManager.php | 44 +++++++ ...erveRepeaterWithDefaultRelationManager.php | 47 ++++++++ .../Resources/Pages/CreateRecordTest.php | 102 ++++++++++++++++ 9 files changed, 414 insertions(+) create mode 100644 tests/src/Fixtures/Resources/Posts/Pages/CreateAnotherPreservingRepeaterPost.php create mode 100644 tests/src/Fixtures/Resources/Posts/Pages/CreateAnotherPreservingRepeaterWithDefaultPost.php create mode 100644 tests/src/Fixtures/Resources/Tickets/RelationManagers/DepartmentsRelationManagerWithPreservation.php create mode 100644 tests/src/Fixtures/Resources/Users/RelationManagers/PostsWithCreateAndPreserveRepeaterRelationManager.php create mode 100644 tests/src/Fixtures/Resources/Users/RelationManagers/PostsWithCreateAndPreserveRepeaterWithDefaultRelationManager.php diff --git a/packages/actions/src/CreateAction.php b/packages/actions/src/CreateAction.php index d681f0d988..b1e1e37977 100644 --- a/packages/actions/src/CreateAction.php +++ b/packages/actions/src/CreateAction.php @@ -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; diff --git a/packages/panels/src/Resources/Pages/CreateRecord.php b/packages/panels/src/Resources/Pages/CreateRecord.php index 642127e772..20c1a95609 100644 --- a/packages/panels/src/Resources/Pages/CreateRecord.php +++ b/packages/panels/src/Resources/Pages/CreateRecord.php @@ -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; diff --git a/tests/src/Actions/CreateActionTest.php b/tests/src/Actions/CreateActionTest.php index 6ea9141cb9..29a0ad6c0c 100644 --- a/tests/src/Actions/CreateActionTest.php +++ b/tests/src/Actions/CreateActionTest.php @@ -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(); diff --git a/tests/src/Fixtures/Resources/Posts/Pages/CreateAnotherPreservingRepeaterPost.php b/tests/src/Fixtures/Resources/Posts/Pages/CreateAnotherPreservingRepeaterPost.php new file mode 100644 index 0000000000..7937b25e89 --- /dev/null +++ b/tests/src/Fixtures/Resources/Posts/Pages/CreateAnotherPreservingRepeaterPost.php @@ -0,0 +1,36 @@ +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']); + } +} diff --git a/tests/src/Fixtures/Resources/Posts/Pages/CreateAnotherPreservingRepeaterWithDefaultPost.php b/tests/src/Fixtures/Resources/Posts/Pages/CreateAnotherPreservingRepeaterWithDefaultPost.php new file mode 100644 index 0000000000..e518f1a6f7 --- /dev/null +++ b/tests/src/Fixtures/Resources/Posts/Pages/CreateAnotherPreservingRepeaterWithDefaultPost.php @@ -0,0 +1,39 @@ +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']); + } +} diff --git a/tests/src/Fixtures/Resources/Tickets/RelationManagers/DepartmentsRelationManagerWithPreservation.php b/tests/src/Fixtures/Resources/Tickets/RelationManagers/DepartmentsRelationManagerWithPreservation.php new file mode 100644 index 0000000000..d14e5b5905 --- /dev/null +++ b/tests/src/Fixtures/Resources/Tickets/RelationManagers/DepartmentsRelationManagerWithPreservation.php @@ -0,0 +1,29 @@ +headerActions([ + CreateAction::make() + ->preserveFormDataWhenCreatingAnother(['name']), + ]); + } + + public function form(Schema $schema): Schema + { + return DepartmentForm::configure($schema); + } +} diff --git a/tests/src/Fixtures/Resources/Users/RelationManagers/PostsWithCreateAndPreserveRepeaterRelationManager.php b/tests/src/Fixtures/Resources/Users/RelationManagers/PostsWithCreateAndPreserveRepeaterRelationManager.php new file mode 100644 index 0000000000..e680d6a98c --- /dev/null +++ b/tests/src/Fixtures/Resources/Users/RelationManagers/PostsWithCreateAndPreserveRepeaterRelationManager.php @@ -0,0 +1,44 @@ +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'), + ]), + ]); + } +} diff --git a/tests/src/Fixtures/Resources/Users/RelationManagers/PostsWithCreateAndPreserveRepeaterWithDefaultRelationManager.php b/tests/src/Fixtures/Resources/Users/RelationManagers/PostsWithCreateAndPreserveRepeaterWithDefaultRelationManager.php new file mode 100644 index 0000000000..e261f29aae --- /dev/null +++ b/tests/src/Fixtures/Resources/Users/RelationManagers/PostsWithCreateAndPreserveRepeaterWithDefaultRelationManager.php @@ -0,0 +1,47 @@ +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'], + ]), + ]); + } +} diff --git a/tests/src/Panels/Resources/Pages/CreateRecordTest.php b/tests/src/Panels/Resources/Pages/CreateRecordTest.php index 13d58579c0..ae56927447 100644 --- a/tests/src/Panels/Resources/Pages/CreateRecordTest.php +++ b/tests/src/Panels/Resources/Pages/CreateRecordTest.php @@ -1,11 +1,14 @@ 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();