Skip to content

Commit faaa5ca

Browse files
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>
1 parent 41f0963 commit faaa5ca

File tree

9 files changed

+414
-0
lines changed

9 files changed

+414
-0
lines changed

packages/actions/src/CreateAction.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,10 @@ protected function setUp(): void
132132
...$preserveRawState ?? [],
133133
]);
134134

135+
// Rebuild child schemas without double-firing `afterStateHydrated()` hooks.
136+
$hydratedDefaultState = null;
137+
$schema->hydrateState($hydratedDefaultState, shouldCallHydrationHooks: false);
138+
135139
$this->halt();
136140

137141
return;

packages/panels/src/Resources/Pages/CreateRecord.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,10 @@ public function create(bool $another = false): void
143143
...$preserveRawState,
144144
]);
145145

146+
// Rebuild child schemas without double-firing `afterStateHydrated()` hooks.
147+
$hydratedDefaultState = null;
148+
$this->form->hydrateState($hydratedDefaultState, shouldCallHydrationHooks: false);
149+
146150
$this->isCreating = false;
147151

148152
return;

tests/src/Actions/CreateActionTest.php

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,17 @@
22

33
use Filament\Actions\CreateAction;
44
use Filament\Actions\Testing\TestAction;
5+
use Filament\Forms\Components\Repeater;
56
use Filament\Tests\Fixtures\Models\Department;
7+
use Filament\Tests\Fixtures\Models\Post;
68
use Filament\Tests\Fixtures\Models\Ticket;
9+
use Filament\Tests\Fixtures\Models\User;
710
use Filament\Tests\Fixtures\Resources\Tickets\Pages\EditTicket;
811
use Filament\Tests\Fixtures\Resources\Tickets\RelationManagers\DepartmentsRelationManager;
12+
use Filament\Tests\Fixtures\Resources\Tickets\RelationManagers\DepartmentsRelationManagerWithPreservation;
13+
use Filament\Tests\Fixtures\Resources\Users\Pages\EditUser;
14+
use Filament\Tests\Fixtures\Resources\Users\RelationManagers\PostsWithCreateAndPreserveRepeaterRelationManager;
15+
use Filament\Tests\Fixtures\Resources\Users\RelationManagers\PostsWithCreateAndPreserveRepeaterWithDefaultRelationManager;
916
use Filament\Tests\Panels\Resources\TestCase;
1017
use Illuminate\Support\Str;
1118

@@ -102,6 +109,108 @@
102109
assertDatabaseHas(Department::class, ['name' => $firstName]);
103110
});
104111

112+
it('can create another record and preserve data using `CreateAction`', function (): void {
113+
$ticket = Ticket::factory()->create();
114+
115+
livewire(DepartmentsRelationManagerWithPreservation::class, ['ownerRecord' => $ticket, 'pageClass' => EditTicket::class])
116+
->mountAction(TestAction::make(CreateAction::class)->table(), ['another' => true])
117+
->fillForm([
118+
'name' => $firstName = Str::random(),
119+
])
120+
->callMountedAction()
121+
->assertHasNoFormErrors()
122+
->assertSchemaStateSet([
123+
'name' => $firstName,
124+
])
125+
->fillForm([
126+
'name' => $secondName = Str::random(),
127+
])
128+
->callMountedAction()
129+
->assertHasNoFormErrors();
130+
131+
assertDatabaseHas(Department::class, ['name' => $firstName]);
132+
assertDatabaseHas(Department::class, ['name' => $secondName]);
133+
});
134+
135+
it('can create another record and preserve repeater data using `CreateAction`', function (): void {
136+
$undoRepeaterFake = Repeater::fake();
137+
138+
$user = User::factory()->create();
139+
140+
$repeaterItems = [
141+
['name' => 'First Item', 'email' => 'first@example.com'],
142+
['name' => 'Second Item', 'email' => 'second@example.com'],
143+
];
144+
145+
livewire(PostsWithCreateAndPreserveRepeaterRelationManager::class, ['ownerRecord' => $user, 'pageClass' => EditUser::class])
146+
->mountAction(TestAction::make(CreateAction::class)->table(), ['another' => true])
147+
->fillForm([
148+
'title' => $firstTitle = Str::random(),
149+
'rating' => 5,
150+
'json_array_of_objects' => $repeaterItems,
151+
])
152+
->callMountedAction()
153+
->assertHasNoFormErrors()
154+
->fillForm([
155+
'title' => $secondTitle = Str::random(),
156+
'rating' => 3,
157+
])
158+
->callMountedAction()
159+
->assertHasNoFormErrors();
160+
161+
$record = Post::query()->where('title', $firstTitle)->first();
162+
163+
expect($record)->not->toBeNull();
164+
expect($record->json_array_of_objects)->toBe($repeaterItems);
165+
166+
$record2 = Post::query()->where('title', $secondTitle)->first();
167+
168+
expect($record2)->not->toBeNull();
169+
expect($record2->json_array_of_objects)->toBe($repeaterItems);
170+
171+
$undoRepeaterFake();
172+
});
173+
174+
it('can create another record and preserve repeater data with `default()` values using `CreateAction`', function (): void {
175+
$undoRepeaterFake = Repeater::fake();
176+
177+
$user = User::factory()->create();
178+
179+
$repeaterItems = [
180+
['name' => 'Custom A', 'email' => 'a@example.com'],
181+
['name' => 'Custom B', 'email' => 'b@example.com'],
182+
['name' => 'Custom C', 'email' => 'c@example.com'],
183+
];
184+
185+
livewire(PostsWithCreateAndPreserveRepeaterWithDefaultRelationManager::class, ['ownerRecord' => $user, 'pageClass' => EditUser::class])
186+
->mountAction(TestAction::make(CreateAction::class)->table(), ['another' => true])
187+
->fillForm([
188+
'title' => $firstTitle = Str::random(),
189+
'rating' => 5,
190+
'json_array_of_objects' => $repeaterItems,
191+
])
192+
->callMountedAction()
193+
->assertHasNoFormErrors()
194+
->fillForm([
195+
'title' => $secondTitle = Str::random(),
196+
'rating' => 3,
197+
])
198+
->callMountedAction()
199+
->assertHasNoFormErrors();
200+
201+
$record = Post::query()->where('title', $firstTitle)->first();
202+
203+
expect($record)->not->toBeNull();
204+
expect($record->json_array_of_objects)->toBe($repeaterItems);
205+
206+
$record2 = Post::query()->where('title', $secondTitle)->first();
207+
208+
expect($record2)->not->toBeNull();
209+
expect($record2->json_array_of_objects)->toBe($repeaterItems);
210+
211+
$undoRepeaterFake();
212+
});
213+
105214
it('can cancel `CreateAction` without creating record', function (): void {
106215
$ticket = Ticket::factory()->create();
107216
$initialCount = Department::count();
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<?php
2+
3+
namespace Filament\Tests\Fixtures\Resources\Posts\Pages;
4+
5+
use Filament\Forms;
6+
use Filament\Resources\Pages\CreateRecord;
7+
use Filament\Schemas\Schema;
8+
use Filament\Tests\Fixtures\Resources\Posts\PostResource;
9+
use Illuminate\Support\Arr;
10+
11+
class CreateAnotherPreservingRepeaterPost extends CreateRecord
12+
{
13+
protected static string $resource = PostResource::class;
14+
15+
public function form(Schema $form): Schema
16+
{
17+
return $form
18+
->components([
19+
Forms\Components\TextInput::make('title')->required(),
20+
Forms\Components\TextInput::make('rating')->numeric()->required(),
21+
Forms\Components\Select::make('author_id')
22+
->relationship('author', 'name')
23+
->required(),
24+
Forms\Components\Repeater::make('json_array_of_objects')
25+
->schema([
26+
Forms\Components\TextInput::make('name'),
27+
Forms\Components\TextInput::make('email'),
28+
]),
29+
]);
30+
}
31+
32+
protected function preserveFormDataWhenCreatingAnother(array $data): array
33+
{
34+
return Arr::only($data, ['json_array_of_objects']);
35+
}
36+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php
2+
3+
namespace Filament\Tests\Fixtures\Resources\Posts\Pages;
4+
5+
use Filament\Forms;
6+
use Filament\Resources\Pages\CreateRecord;
7+
use Filament\Schemas\Schema;
8+
use Filament\Tests\Fixtures\Resources\Posts\PostResource;
9+
use Illuminate\Support\Arr;
10+
11+
class CreateAnotherPreservingRepeaterWithDefaultPost extends CreateRecord
12+
{
13+
protected static string $resource = PostResource::class;
14+
15+
public function form(Schema $form): Schema
16+
{
17+
return $form
18+
->components([
19+
Forms\Components\TextInput::make('title')->required(),
20+
Forms\Components\TextInput::make('rating')->numeric()->required(),
21+
Forms\Components\Select::make('author_id')
22+
->relationship('author', 'name')
23+
->required(),
24+
Forms\Components\Repeater::make('json_array_of_objects')
25+
->schema([
26+
Forms\Components\TextInput::make('name'),
27+
Forms\Components\TextInput::make('email'),
28+
])
29+
->default([
30+
['name' => 'Default Name', 'email' => 'default@example.com'],
31+
]),
32+
]);
33+
}
34+
35+
protected function preserveFormDataWhenCreatingAnother(array $data): array
36+
{
37+
return Arr::only($data, ['json_array_of_objects']);
38+
}
39+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
3+
namespace Filament\Tests\Fixtures\Resources\Tickets\RelationManagers;
4+
5+
use Filament\Actions\CreateAction;
6+
use Filament\Resources\RelationManagers\RelationManager;
7+
use Filament\Schemas\Schema;
8+
use Filament\Tables\Table;
9+
use Filament\Tests\Fixtures\Resources\Departments\Schemas\DepartmentForm;
10+
use Filament\Tests\Fixtures\Resources\Departments\Tables\DepartmentsTable;
11+
12+
class DepartmentsRelationManagerWithPreservation extends RelationManager
13+
{
14+
protected static string $relationship = 'departments';
15+
16+
public function table(Table $table): Table
17+
{
18+
return DepartmentsTable::configure($table)
19+
->headerActions([
20+
CreateAction::make()
21+
->preserveFormDataWhenCreatingAnother(['name']),
22+
]);
23+
}
24+
25+
public function form(Schema $schema): Schema
26+
{
27+
return DepartmentForm::configure($schema);
28+
}
29+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<?php
2+
3+
namespace Filament\Tests\Fixtures\Resources\Users\RelationManagers;
4+
5+
use Filament\Actions\CreateAction;
6+
use Filament\Forms\Components\Repeater;
7+
use Filament\Forms\Components\TextInput;
8+
use Filament\Resources\RelationManagers\RelationManager;
9+
use Filament\Schemas\Schema;
10+
use Filament\Tables\Columns\TextColumn;
11+
use Filament\Tables\Table;
12+
13+
class PostsWithCreateAndPreserveRepeaterRelationManager extends RelationManager
14+
{
15+
protected static string $relationship = 'posts';
16+
17+
public function table(Table $table): Table
18+
{
19+
return $table
20+
->recordTitleAttribute('title')
21+
->inverseRelationship('author')
22+
->columns([
23+
TextColumn::make('title'),
24+
])
25+
->headerActions([
26+
CreateAction::make()
27+
->preserveFormDataWhenCreatingAnother(['json_array_of_objects']),
28+
]);
29+
}
30+
31+
public function form(Schema $schema): Schema
32+
{
33+
return $schema
34+
->components([
35+
TextInput::make('title')->required(),
36+
TextInput::make('rating')->numeric()->required(),
37+
Repeater::make('json_array_of_objects')
38+
->schema([
39+
TextInput::make('name'),
40+
TextInput::make('email'),
41+
]),
42+
]);
43+
}
44+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<?php
2+
3+
namespace Filament\Tests\Fixtures\Resources\Users\RelationManagers;
4+
5+
use Filament\Actions\CreateAction;
6+
use Filament\Forms\Components\Repeater;
7+
use Filament\Forms\Components\TextInput;
8+
use Filament\Resources\RelationManagers\RelationManager;
9+
use Filament\Schemas\Schema;
10+
use Filament\Tables\Columns\TextColumn;
11+
use Filament\Tables\Table;
12+
13+
class PostsWithCreateAndPreserveRepeaterWithDefaultRelationManager extends RelationManager
14+
{
15+
protected static string $relationship = 'posts';
16+
17+
public function table(Table $table): Table
18+
{
19+
return $table
20+
->recordTitleAttribute('title')
21+
->inverseRelationship('author')
22+
->columns([
23+
TextColumn::make('title'),
24+
])
25+
->headerActions([
26+
CreateAction::make()
27+
->preserveFormDataWhenCreatingAnother(['json_array_of_objects']),
28+
]);
29+
}
30+
31+
public function form(Schema $schema): Schema
32+
{
33+
return $schema
34+
->components([
35+
TextInput::make('title')->required(),
36+
TextInput::make('rating')->numeric()->required(),
37+
Repeater::make('json_array_of_objects')
38+
->schema([
39+
TextInput::make('name'),
40+
TextInput::make('email'),
41+
])
42+
->default([
43+
['name' => 'Default Name', 'email' => 'default@example.com'],
44+
]),
45+
]);
46+
}
47+
}

0 commit comments

Comments
 (0)