From 1ab794f43ecb6eb41a1d44fee811b9e22bbfee65 Mon Sep 17 00:00:00 2001 From: Jack McDade Date: Thu, 23 Jul 2026 10:56:44 -0400 Subject: [PATCH] Ability to insert configs before/after other fields/sections --- src/Facades/Form.php | 2 +- src/Forms/FormRepository.php | 15 ++- .../Controllers/CP/Forms/FormsController.php | 45 ++++++++- tests/Feature/Forms/EditFormTest.php | 91 +++++++++++++++++++ tests/Forms/FormRepositoryTest.php | 47 ++++++++++ 5 files changed, 193 insertions(+), 7 deletions(-) diff --git a/src/Facades/Form.php b/src/Facades/Form.php index 6e102c2b56d..14f9ff3f77f 100644 --- a/src/Facades/Form.php +++ b/src/Facades/Form.php @@ -13,7 +13,7 @@ * @method static \Illuminate\Support\Collection all() * @method static int count() * @method static \Statamic\Contracts\Forms\Form make(string $handle = null) - * @method static void appendConfigFields(mixed $handles, string $display, array $fields) + * @method static void appendConfigFields(mixed $handles, string $display, array $fields, ?string $before = null, ?string $after = null) * @method static array extraConfigFor(string $handle) * @method static self redirect(string $form, \Closure $callback) * @method static \Closure getSubmissionRedirect(Submission $submission) diff --git a/src/Forms/FormRepository.php b/src/Forms/FormRepository.php index dbef859b5a5..da04ca1f8f2 100644 --- a/src/Forms/FormRepository.php +++ b/src/Forms/FormRepository.php @@ -85,12 +85,18 @@ public function make($handle = null) return $form; } - public function appendConfigFields($handles, string $display, array $fields) + public function appendConfigFields($handles, string $display, array $fields, ?string $before = null, ?string $after = null) { + if ($before && $after) { + throw new \InvalidArgumentException('Pass only before or after, not both.'); + } + $this->configs[] = [ 'display' => $display, 'handles' => Arr::wrap($handles), 'fields' => $fields, + 'before' => $before, + 'after' => $after, ]; } @@ -103,14 +109,15 @@ public function extraConfigFor($handle) return in_array('*', $config['handles']) || in_array($handle, $config['handles']); }) ->flatMap(function ($config) use ($reserved) { - return [ - Str::snake($config['display']) => [ + Str::snake($config['display']) => array_filter([ 'display' => $config['display'], 'fields' => collect($config['fields']) ->filter(fn ($field, $index) => ! in_array($field['handle'] ?? $index, $reserved)) ->all(), - ], + 'before' => $config['before'] ?? null, + 'after' => $config['after'] ?? null, + ], fn ($value) => ! is_null($value)), ]; }) ->all(); diff --git a/src/Http/Controllers/CP/Forms/FormsController.php b/src/Http/Controllers/CP/Forms/FormsController.php index 7a93b3560f6..c79727d071b 100644 --- a/src/Http/Controllers/CP/Forms/FormsController.php +++ b/src/Http/Controllers/CP/Forms/FormsController.php @@ -13,6 +13,7 @@ use Statamic\Http\Controllers\CP\Forms\Concerns\ProvidesFormAbilities; use Statamic\Rules\Handle; use Statamic\Statamic; +use Statamic\Support\Arr; use Statamic\Support\Str; use function Statamic\trans as __; @@ -389,13 +390,23 @@ protected function editFormBlueprint($form) $merged = false; foreach ($fields as $sectionHandle => $section) { if ($section['display'] == __($config['display'])) { - $fields[$sectionHandle]['fields'] += $config['fields']; + $fields[$sectionHandle]['fields'] = $this->insertRelative( + $section['fields'], + $config['fields'], + $config['before'] ?? null, + $config['after'] ?? null, + ); $merged = true; } } if (! $merged) { - $fields[$handle] = $config; + $fields = $this->insertRelative( + $fields, + [$handle => Arr::except($config, ['before', 'after'])], + $config['before'] ?? null, + $config['after'] ?? null, + ); } } @@ -418,4 +429,34 @@ protected function editFormBlueprint($form) ])->all()); } + + protected function insertRelative(array $existing, array $new, ?string $before = null, ?string $after = null): array + { + if (! $before && ! $after) { + return $existing + $new; + } + + $result = []; + $inserted = false; + + foreach ($existing as $handle => $item) { + if ($before === $handle) { + foreach ($new as $newHandle => $newItem) { + $result[$newHandle] = $newItem; + } + $inserted = true; + } + + $result[$handle] = $item; + + if ($after === $handle) { + foreach ($new as $newHandle => $newItem) { + $result[$newHandle] = $newItem; + } + $inserted = true; + } + } + + return $inserted ? $result : $existing + $new; + } } diff --git a/tests/Feature/Forms/EditFormTest.php b/tests/Feature/Forms/EditFormTest.php index dac8ecaf928..c06f84c9ba6 100644 --- a/tests/Feature/Forms/EditFormTest.php +++ b/tests/Feature/Forms/EditFormTest.php @@ -139,4 +139,95 @@ public function fields_can_be_added() 'Second injected into additional section', ]); } + + #[Test] + public function fields_can_be_added_before_an_existing_field() + { + $this->setTestRoles(['test' => ['access cp', 'configure forms']]); + $user = User::make()->assignRole('test')->save(); + $form = tap(Form::make('test'))->save(); + + Form::appendConfigFields('*', 'Fields', [ + 'recaptcha' => ['type' => 'text', 'display' => 'Injected before honeypot'], + ], before: 'honeypot'); + + $this + ->actingAs($user) + ->get(cp_route('forms.edit', $form->handle())) + ->assertSuccessful() + ->assertSeeInOrder([ + 'Title', + 'Injected before honeypot', + 'Honeypot', + 'Store Submissions', + ]); + } + + #[Test] + public function fields_can_be_added_after_an_existing_field() + { + $this->setTestRoles(['test' => ['access cp', 'configure forms']]); + $user = User::make()->assignRole('test')->save(); + $form = tap(Form::make('test'))->save(); + + Form::appendConfigFields('*', 'Submissions', [ + 'webhook' => ['type' => 'text', 'display' => 'Injected after store'], + ], after: 'store'); + + $this + ->actingAs($user) + ->get(cp_route('forms.edit', $form->handle())) + ->assertSuccessful() + ->assertSeeInOrder([ + 'Store Submissions', + 'Injected after store', + 'Enable Fake Submission Generator', + ]); + } + + #[Test] + public function sections_can_be_added_before_an_existing_section() + { + $this->setTestRoles(['test' => ['access cp', 'configure forms']]); + $user = User::make()->assignRole('test')->save(); + $form = tap(Form::make('test'))->save(); + + Form::appendConfigFields('*', 'Automagic Forms', [ + 'automagic_form' => ['type' => 'toggle', 'display' => 'Enable Automagic Form'], + ], before: 'submissions'); + + $this + ->actingAs($user) + ->get(cp_route('forms.edit', $form->handle())) + ->assertSuccessful() + ->assertSeeInOrder([ + 'Honeypot', + 'Automagic Forms', + 'Enable Automagic Form', + 'Store Submissions', + ]); + } + + #[Test] + public function sections_can_be_added_after_an_existing_section() + { + $this->setTestRoles(['test' => ['access cp', 'configure forms']]); + $user = User::make()->assignRole('test')->save(); + $form = tap(Form::make('test'))->save(); + + Form::appendConfigFields('*', 'Automagic Forms', [ + 'automagic_form' => ['type' => 'toggle', 'display' => 'Enable Automagic Form'], + ], after: 'fields'); + + $this + ->actingAs($user) + ->get(cp_route('forms.edit', $form->handle())) + ->assertSuccessful() + ->assertSeeInOrder([ + 'Honeypot', + 'Automagic Forms', + 'Enable Automagic Form', + 'Store Submissions', + ]); + } } diff --git a/tests/Forms/FormRepositoryTest.php b/tests/Forms/FormRepositoryTest.php index 08cca2e1c89..653fabaadd3 100644 --- a/tests/Forms/FormRepositoryTest.php +++ b/tests/Forms/FormRepositoryTest.php @@ -80,4 +80,51 @@ public function it_registers_config() ], ], $this->repo->extraConfigFor('another_form')); } + + #[Test] + public function it_registers_config_with_before_position() + { + $this->repo->appendConfigFields('*', 'Fields', [ + 'recaptcha' => ['type' => 'toggle'], + ], before: 'honeypot'); + + $this->assertEquals([ + 'fields' => [ + 'display' => 'Fields', + 'fields' => [ + 'recaptcha' => ['type' => 'toggle'], + ], + 'before' => 'honeypot', + ], + ], $this->repo->extraConfigFor('test_form')); + } + + #[Test] + public function it_registers_config_with_after_position() + { + $this->repo->appendConfigFields('*', 'Fields', [ + 'recaptcha' => ['type' => 'toggle'], + ], after: 'honeypot'); + + $this->assertEquals([ + 'fields' => [ + 'display' => 'Fields', + 'fields' => [ + 'recaptcha' => ['type' => 'toggle'], + ], + 'after' => 'honeypot', + ], + ], $this->repo->extraConfigFor('test_form')); + } + + #[Test] + public function it_throws_when_both_before_and_after_are_provided() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Pass only before or after, not both.'); + + $this->repo->appendConfigFields('*', 'Fields', [ + 'recaptcha' => ['type' => 'toggle'], + ], before: 'honeypot', after: 'title'); + } }