From 0a334b9b8780f97def066e59c28e5cab211a9d14 Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Tue, 16 Jun 2026 17:04:12 +0100 Subject: [PATCH 01/14] wip --- config/forms.php | 14 ++ src/Actions/MarkNotSpam.php | 48 +++++++ src/Forms/Submission.php | 55 +++++++- src/Http/Controllers/FormController.php | 13 +- src/Jobs/DeleteDraftFormSubmissions.php | 30 +++++ src/Providers/AppServiceProvider.php | 2 + tests/Actions/MarkNotSpamTest.php | 50 +++++++ .../Forms/DeleteDraftFormSubmissionsTest.php | 82 +++++++++++ tests/Forms/SubmissionTest.php | 127 ++++++++++++++++++ 9 files changed, 407 insertions(+), 14 deletions(-) create mode 100644 src/Actions/MarkNotSpam.php create mode 100644 src/Jobs/DeleteDraftFormSubmissions.php create mode 100644 tests/Actions/MarkNotSpamTest.php create mode 100644 tests/Forms/DeleteDraftFormSubmissionsTest.php diff --git a/config/forms.php b/config/forms.php index c2d434f0289..e4cbb3e9ecc 100644 --- a/config/forms.php +++ b/config/forms.php @@ -35,6 +35,20 @@ 'send_email_job' => \Statamic\Forms\SendEmail::class, + /* + |-------------------------------------------------------------------------- + | Draft Submissions + |-------------------------------------------------------------------------- + | + | Partial (or draft) submissions are automatically deleted after a set number + | of days. Set this to null to prevent the automatic deletion of drafts. + | + */ + + 'drafts' => [ + 'delete_after' => 7, + ], + /* |-------------------------------------------------------------------------- | Exporters diff --git a/src/Actions/MarkNotSpam.php b/src/Actions/MarkNotSpam.php new file mode 100644 index 00000000000..0250a4fb3ae --- /dev/null +++ b/src/Actions/MarkNotSpam.php @@ -0,0 +1,48 @@ +isSpam(); + } + + public function authorize($user, $submission) + { + return $user->can('delete', $submission); + } + + public function confirmationText() + { + /** @translation */ + return 'Are you sure you want to mark this submission as not spam?|Are you sure you want to mark these :count submissions as not spam?'; + } + + public function buttonText() + { + /** @translation */ + return 'Mark As Not Spam|Mark :count as Not Spam'; + } + + public function run($submissions, $values) + { + $submissions->each->complete(); + + return trans_choice('Submission marked as not spam|Submissions marked as not spam', $submissions->count()); + } +} diff --git a/src/Forms/Submission.php b/src/Forms/Submission.php index 1ce97715656..b8f02484324 100644 --- a/src/Forms/Submission.php +++ b/src/Forms/Submission.php @@ -19,6 +19,7 @@ use Statamic\Facades\Asset; use Statamic\Facades\File; use Statamic\Facades\FormSubmission; +use Statamic\Facades\Site; use Statamic\Facades\Stache; use Statamic\Forms\Uploaders\AssetsUploader; use Statamic\Forms\Uploaders\FilesUploader; @@ -114,6 +115,30 @@ public function date() return Carbon::createFromTimestamp($this->id()); } + public function isDraft(): bool + { + return (bool) $this->get('draft'); + } + + public function isSpam(): bool + { + return (bool) $this->get('spam'); + } + + public function isWithheld(): bool + { + return $this->isDraft() || $this->isSpam(); + } + + public function status(): string + { + return match (true) { + $this->isSpam() => 'spam', + $this->isDraft() => 'draft', + default => 'submitted', + }; + } + /** * Upload files and return asset IDs. * @@ -152,6 +177,10 @@ public function save() { $isNew = is_null($this->form()->submission($this->id())); + // Withheld submissions (drafts & spam) are stored but skip the + // Creating/Created events so listeners never see an incomplete submission. + $withheld = $this->isWithheld(); + $withEvents = $this->withEvents; $this->withEvents = true; @@ -159,7 +188,7 @@ public function save() $this->afterSaveCallbacks = []; if ($withEvents) { - if ($isNew && SubmissionCreating::dispatch($this) === false) { + if ($isNew && ! $withheld && SubmissionCreating::dispatch($this) === false) { return false; } @@ -175,7 +204,7 @@ public function save() } if ($withEvents) { - if ($isNew) { + if ($isNew && ! $withheld) { SubmissionCreated::dispatch($this); } @@ -183,6 +212,28 @@ public function save() } } + public function complete() + { + $existed = ! is_null($this->form()->submission($this->id())); + + $this->remove('draft')->remove('spam'); + + if ($this->form()->store()) { + $this->save(); + + // A promoted draft already existed, so save() won't fire the created + // event. We dispatch it here so completion always emits it once. + if ($existed) { + SubmissionCreated::dispatch($this); + } + } else { + SubmissionCreated::dispatch($this); + } + + // TODO: Use $this->site() here when we add the "site" key to submissions. + SendEmails::dispatch($this, Site::default()); + } + public function deleteQuietly() { $this->withEvents = false; diff --git a/src/Http/Controllers/FormController.php b/src/Http/Controllers/FormController.php index 1b151be7c80..477d9483c71 100644 --- a/src/Http/Controllers/FormController.php +++ b/src/Http/Controllers/FormController.php @@ -8,13 +8,11 @@ use Illuminate\Validation\ValidationException; use Statamic\Contracts\Forms\Submission; use Statamic\Events\FormSubmitted; -use Statamic\Events\SubmissionCreated; use Statamic\Exceptions\SilentFormFailureException; use Statamic\Facades\Asset; use Statamic\Facades\Form; use Statamic\Facades\Site; use Statamic\Forms\Exceptions\FileContentTypeRequiredException; -use Statamic\Forms\SendEmails; use Statamic\Http\Requests\FrontendFormRequest; use Statamic\Support\Arr; use Statamic\Support\Str; @@ -70,16 +68,7 @@ public function submit(FrontendFormRequest $request, $form) return $this->formSuccess($params, $submission, true); } - if ($form->store()) { - $submission->save(); - } else { - // When the submission is saved, this same created event will be dispatched. - // We'll also fire it here if submissions are not configured to be stored - // so that developers may continue to listen and modify it as needed. - SubmissionCreated::dispatch($submission); - } - - SendEmails::dispatch($submission, $site); + $submission->complete($site); return $this->formSuccess($params, $submission); } diff --git a/src/Jobs/DeleteDraftFormSubmissions.php b/src/Jobs/DeleteDraftFormSubmissions.php new file mode 100644 index 00000000000..927409fe15d --- /dev/null +++ b/src/Jobs/DeleteDraftFormSubmissions.php @@ -0,0 +1,30 @@ +subDays($days); + + FormSubmission::query() + ->where('draft', true) + ->where('date', '<', $threshold) + ->get() + ->each + ->delete(); + } +} diff --git a/src/Providers/AppServiceProvider.php b/src/Providers/AppServiceProvider.php index ea221a1bea6..c9b8023e194 100644 --- a/src/Providers/AppServiceProvider.php +++ b/src/Providers/AppServiceProvider.php @@ -20,6 +20,7 @@ use Statamic\Facades\Token; use Statamic\Facades\User; use Statamic\Fields\FieldsetRecursionStack; +use Statamic\Jobs\DeleteDraftFormSubmissions; use Statamic\Jobs\HandleEntrySchedule; use Statamic\Notifications\ElevatedSessionVerificationCode; use Statamic\Sites\Sites; @@ -141,6 +142,7 @@ public function boot() $this->registerElevatedSessionMacros(); $this->app->make(Schedule::class)->job(HandleEntrySchedule::class)->everyMinute(); + $this->app->make(Schedule::class)->job(DeleteDraftFormSubmissions::class)->daily(); } public function register() diff --git a/tests/Actions/MarkNotSpamTest.php b/tests/Actions/MarkNotSpamTest.php new file mode 100644 index 00000000000..ea60f302876 --- /dev/null +++ b/tests/Actions/MarkNotSpamTest.php @@ -0,0 +1,50 @@ +save(); + + $action = new MarkNotSpam; + + $this->assertTrue($action->visibleTo($form->makeSubmission()->set('spam', true))); + $this->assertFalse($action->visibleTo($form->makeSubmission())); + $this->assertFalse($action->visibleTo($form->makeSubmission()->set('draft', true))); + $this->assertFalse($action->visibleTo($form)); + } + + #[Test] + public function it_removes_the_spam_key_and_dispatches_relevant_events() + { + Bus::fake(); + Event::fake([SubmissionCreated::class]); + + $form = tap(Form::make('contact'))->save(); + $submission = $form->makeSubmission()->set('spam', true); + $submission->save(); + + (new MarkNotSpam)->run(collect([$submission]), []); + + $this->assertFalse($submission->isSpam()); + + Event::assertDispatched(SubmissionCreated::class); + Bus::assertDispatched(SendEmails::class); + } +} diff --git a/tests/Forms/DeleteDraftFormSubmissionsTest.php b/tests/Forms/DeleteDraftFormSubmissionsTest.php new file mode 100644 index 00000000000..01ab6ddee17 --- /dev/null +++ b/tests/Forms/DeleteDraftFormSubmissionsTest.php @@ -0,0 +1,82 @@ + 7]); + + $form = tap(Form::make('contact'))->save(); + + Carbon::setTestNow('2025-06-01 12:00:00'); + $oldDraft = tap($form->makeSubmission()->set('draft', true))->save(); + + Carbon::setTestNow('2025-06-02 12:00:00'); + $oldSubmission = tap($form->makeSubmission())->save(); + + Carbon::setTestNow('2025-06-14 12:00:00'); + $recentDraft = tap($form->makeSubmission()->set('draft', true))->save(); + + Carbon::setTestNow('2025-06-15 12:00:00'); + + (new DeleteDraftFormSubmissions)->handle(); + + $this->assertNull($form->submission($oldDraft->id())); + $this->assertNotNull($form->submission($recentDraft->id())); + $this->assertNotNull($form->submission($oldSubmission->id())); + } + + #[Test] + public function it_only_deletes_spam_submissions() + { + config(['statamic.forms.drafts.delete_after' => 7]); + + $form = tap(Form::make('contact'))->save(); + + Carbon::setTestNow('2025-06-01 12:00:00'); + $draft = tap($form->makeSubmission()->set('draft', true))->save(); + + Carbon::setTestNow('2025-06-02 12:00:00'); + $submitted = tap($form->makeSubmission())->save(); + + Carbon::setTestNow('2025-06-03 12:00:00'); + $spam = tap($form->makeSubmission()->set('spam', true))->save(); + + Carbon::setTestNow('2025-06-30 12:00:00'); + + (new DeleteDraftFormSubmissions)->handle(); + + $this->assertNull($form->submission($draft->id())); + $this->assertNotNull($form->submission($submitted->id())); + $this->assertNotNull($form->submission($spam->id())); + } + + #[Test] + public function it_does_not_delete_anything_when_disabled() + { + config(['statamic.forms.drafts.delete_after' => null]); + + $form = tap(Form::make('contact'))->save(); + + Carbon::setTestNow('2025-06-01 12:00:00'); + $draft = tap($form->makeSubmission()->set('draft', true))->save(); + + Carbon::setTestNow('2025-06-30 12:00:00'); + + (new DeleteDraftFormSubmissions)->handle(); + + $this->assertNotNull($form->submission($draft->id())); + } +} diff --git a/tests/Forms/SubmissionTest.php b/tests/Forms/SubmissionTest.php index d19fde2c5d3..819ae475303 100644 --- a/tests/Forms/SubmissionTest.php +++ b/tests/Forms/SubmissionTest.php @@ -4,6 +4,7 @@ use Carbon\Carbon; use Illuminate\Support\Collection; +use Illuminate\Support\Facades\Bus; use Illuminate\Support\Facades\Event; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Test; @@ -13,6 +14,8 @@ use Statamic\Events\SubmissionSaved; use Statamic\Events\SubmissionSaving; use Statamic\Facades\Form; +use Statamic\Facades\Site; +use Statamic\Forms\SendEmails; use Tests\PreventSavingStacheItemsToDisk; use Tests\TestCase; @@ -240,6 +243,130 @@ public function it_deletes_quietly() $this->assertTrue($return); } + #[Test] + public function it_determines_its_status() + { + $form = tap(Form::make('contact_us'))->save(); + + $submitted = $form->makeSubmission(); + $this->assertFalse($submitted->isDraft()); + $this->assertFalse($submitted->isSpam()); + $this->assertFalse($submitted->isWithheld()); + $this->assertEquals('submitted', $submitted->status()); + + $draft = $form->makeSubmission()->set('draft', true); + $this->assertTrue($draft->isDraft()); + $this->assertFalse($draft->isSpam()); + $this->assertTrue($draft->isWithheld()); + $this->assertEquals('draft', $draft->status()); + + $spam = $form->makeSubmission()->set('spam', true); + $this->assertTrue($spam->isSpam()); + $this->assertFalse($spam->isDraft()); + $this->assertTrue($spam->isWithheld()); + $this->assertEquals('spam', $spam->status()); + } + + #[Test] + #[DataProvider('withheldStatusProvider')] + public function it_does_not_dispatch_creation_events_when_saving_a_withheld_submission(string $status) + { + Event::fake(); + + $form = tap(Form::make('contact_us'))->save(); + + $submission = $form->makeSubmission()->set($status, true); + $submission->save(); + + // Creation events shouldn't be dispatched. + Event::assertNotDispatched(SubmissionCreating::class); + Event::assertNotDispatched(SubmissionCreated::class); + + // But, saving events should. + Event::assertDispatched(SubmissionSaving::class); + Event::assertDispatched(SubmissionSaved::class); + } + + public static function withheldStatusProvider(): array + { + return [ + 'draft' => ['draft'], + 'spam' => ['spam'], + ]; + } + + #[Test] + public function created_event_is_not_automatically_dispatched_when_removing_the_draft_key() + { + $form = tap(Form::make('contact_us'))->save(); + + $submission = $form->makeSubmission()->set('draft', true); + $submission->save(); + + Event::fake(); + + // Removing the draft key turns it into a "real" submission, but because the + // record already exists, save() alone won't dispatch Created. This is why + // complete() dispatches it explicitly (covered by the test below). + $submission->remove('draft'); + $submission->save(); + + Event::assertNotDispatched(SubmissionCreating::class); + Event::assertNotDispatched(SubmissionCreated::class); + Event::assertDispatched(SubmissionSaved::class); + } + + #[Test] + public function completing_a_new_submission_dispatches_created_event_once() + { + Bus::fake(); + Event::fake([SubmissionCreated::class]); + + $form = tap(Form::make('contact_us'))->save(); + $submission = $form->makeSubmission(); + + $submission->complete(Site::default()); + + Event::assertDispatched(SubmissionCreated::class, 1); + Bus::assertDispatched(SendEmails::class, 1); + $this->assertNotNull($form->submission($submission->id())); + } + + #[Test] + public function completing_a_withheld_submission_removes_the_status_key_and_dispatches_events() + { + $form = tap(Form::make('contact_us'))->save(); + $submission = tap($form->makeSubmission()->set('draft', true)->set('spam', true))->save(); + + Bus::fake(); + Event::fake([SubmissionCreated::class, SubmissionCreating::class]); + + $submission->complete(Site::default()); + + $this->assertFalse($submission->isWithheld()); + + // Submission already exists, so save() won't dispatch the Created event, complete() will. + Event::assertDispatched(SubmissionCreated::class, 1); + Event::assertNotDispatched(SubmissionCreating::class); + Bus::assertDispatched(SendEmails::class, 1); + } + + #[Test] + public function completing_a_submission_for_a_non_storing_form_still_dispatches_the_created_event() + { + Bus::fake(); + Event::fake([SubmissionCreated::class]); + + $form = tap(Form::make('contact_us')->store(false))->save(); + $submission = $form->makeSubmission(); + + $submission->complete(Site::default()); + + Event::assertDispatched(SubmissionCreated::class, 1); + Bus::assertDispatched(SendEmails::class, 1); + $this->assertNull($form->submission($submission->id())); + } + #[Test] public function it_clones_internal_collections() { From 14e42574d96d4ab60ac6776a630e428e9f280144 Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Wed, 17 Jun 2026 09:38:20 +0100 Subject: [PATCH 02/14] rename draft to incomplete --- config/forms.php | 4 +-- src/Forms/Submission.php | 19 ++++++----- ...hp => DeleteIncompleteFormSubmissions.php} | 6 ++-- src/Providers/AppServiceProvider.php | 4 +-- tests/Actions/MarkNotSpamTest.php | 5 ++- ...> DeleteIncompleteFormSubmissionsTest.php} | 16 ++++----- tests/Forms/SubmissionTest.php | 33 ++++++++++--------- 7 files changed, 43 insertions(+), 44 deletions(-) rename src/Jobs/{DeleteDraftFormSubmissions.php => DeleteIncompleteFormSubmissions.php} (74%) rename tests/Forms/{DeleteDraftFormSubmissionsTest.php => DeleteIncompleteFormSubmissionsTest.php} (80%) diff --git a/config/forms.php b/config/forms.php index e4cbb3e9ecc..c75154fe4ac 100644 --- a/config/forms.php +++ b/config/forms.php @@ -45,9 +45,7 @@ | */ - 'drafts' => [ - 'delete_after' => 7, - ], + 'delete_incomplete_submissions_after' => 7, /* |-------------------------------------------------------------------------- diff --git a/src/Forms/Submission.php b/src/Forms/Submission.php index b8f02484324..eb11269741f 100644 --- a/src/Forms/Submission.php +++ b/src/Forms/Submission.php @@ -115,9 +115,9 @@ public function date() return Carbon::createFromTimestamp($this->id()); } - public function isDraft(): bool + public function isIncomplete(): bool { - return (bool) $this->get('draft'); + return (bool) $this->get('incomplete'); } public function isSpam(): bool @@ -125,17 +125,18 @@ public function isSpam(): bool return (bool) $this->get('spam'); } + // todo: refactor or rename public function isWithheld(): bool { - return $this->isDraft() || $this->isSpam(); + return $this->isIncomplete() || $this->isSpam(); } public function status(): string { return match (true) { $this->isSpam() => 'spam', - $this->isDraft() => 'draft', - default => 'submitted', + $this->isIncomplete() => 'incomplete', + default => 'complete', }; } @@ -177,8 +178,8 @@ public function save() { $isNew = is_null($this->form()->submission($this->id())); - // Withheld submissions (drafts & spam) are stored but skip the - // Creating/Created events so listeners never see an incomplete submission. + // Incomplete and spam submissions are stored but skip the Creating/Created + // events so listeners never receive an incomplete submission. $withheld = $this->isWithheld(); $withEvents = $this->withEvents; @@ -216,12 +217,12 @@ public function complete() { $existed = ! is_null($this->form()->submission($this->id())); - $this->remove('draft')->remove('spam'); + $this->remove('incomplete')->remove('spam'); if ($this->form()->store()) { $this->save(); - // A promoted draft already existed, so save() won't fire the created + // A promoted incomplete already existed, so save() won't fire the created // event. We dispatch it here so completion always emits it once. if ($existed) { SubmissionCreated::dispatch($this); diff --git a/src/Jobs/DeleteDraftFormSubmissions.php b/src/Jobs/DeleteIncompleteFormSubmissions.php similarity index 74% rename from src/Jobs/DeleteDraftFormSubmissions.php rename to src/Jobs/DeleteIncompleteFormSubmissions.php index 927409fe15d..75813ad0be5 100644 --- a/src/Jobs/DeleteDraftFormSubmissions.php +++ b/src/Jobs/DeleteIncompleteFormSubmissions.php @@ -8,20 +8,20 @@ use Illuminate\Queue\InteractsWithQueue; use Statamic\Facades\FormSubmission; -class DeleteDraftFormSubmissions implements ShouldQueue +class DeleteIncompleteFormSubmissions implements ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable; public function handle(): void { - if (! ($days = config('statamic.forms.drafts.delete_after'))) { + if (! ($days = config('statamic.forms.delete_incomplete_submissions_after'))) { return; } $threshold = now()->subDays($days); FormSubmission::query() - ->where('draft', true) + ->where('incomplete', true) ->where('date', '<', $threshold) ->get() ->each diff --git a/src/Providers/AppServiceProvider.php b/src/Providers/AppServiceProvider.php index c9b8023e194..a16471f21d6 100644 --- a/src/Providers/AppServiceProvider.php +++ b/src/Providers/AppServiceProvider.php @@ -20,7 +20,7 @@ use Statamic\Facades\Token; use Statamic\Facades\User; use Statamic\Fields\FieldsetRecursionStack; -use Statamic\Jobs\DeleteDraftFormSubmissions; +use Statamic\Jobs\DeleteIncompleteFormSubmissions; use Statamic\Jobs\HandleEntrySchedule; use Statamic\Notifications\ElevatedSessionVerificationCode; use Statamic\Sites\Sites; @@ -142,7 +142,7 @@ public function boot() $this->registerElevatedSessionMacros(); $this->app->make(Schedule::class)->job(HandleEntrySchedule::class)->everyMinute(); - $this->app->make(Schedule::class)->job(DeleteDraftFormSubmissions::class)->daily(); + $this->app->make(Schedule::class)->job(DeleteIncompleteFormSubmissions::class)->daily(); } public function register() diff --git a/tests/Actions/MarkNotSpamTest.php b/tests/Actions/MarkNotSpamTest.php index ea60f302876..ed3f13af9cf 100644 --- a/tests/Actions/MarkNotSpamTest.php +++ b/tests/Actions/MarkNotSpamTest.php @@ -24,10 +24,9 @@ public function it_is_only_visible_to_spam_submissions() $action = new MarkNotSpam; - $this->assertTrue($action->visibleTo($form->makeSubmission()->set('spam', true))); $this->assertFalse($action->visibleTo($form->makeSubmission())); - $this->assertFalse($action->visibleTo($form->makeSubmission()->set('draft', true))); - $this->assertFalse($action->visibleTo($form)); + $this->assertFalse($action->visibleTo($form->makeSubmission()->set('incomplete', true))); + $this->assertTrue($action->visibleTo($form->makeSubmission()->set('spam', true))); } #[Test] diff --git a/tests/Forms/DeleteDraftFormSubmissionsTest.php b/tests/Forms/DeleteIncompleteFormSubmissionsTest.php similarity index 80% rename from tests/Forms/DeleteDraftFormSubmissionsTest.php rename to tests/Forms/DeleteIncompleteFormSubmissionsTest.php index 01ab6ddee17..8aea94d8ceb 100644 --- a/tests/Forms/DeleteDraftFormSubmissionsTest.php +++ b/tests/Forms/DeleteIncompleteFormSubmissionsTest.php @@ -5,18 +5,18 @@ use Carbon\Carbon; use PHPUnit\Framework\Attributes\Test; use Statamic\Facades\Form; -use Statamic\Jobs\DeleteDraftFormSubmissions; +use Statamic\Jobs\DeleteIncompleteFormSubmissions; use Tests\PreventSavingStacheItemsToDisk; use Tests\TestCase; -class DeleteDraftFormSubmissionsTest extends TestCase +class DeleteIncompleteFormSubmissionsTest extends TestCase { use PreventSavingStacheItemsToDisk; #[Test] public function it_deletes_drafts_older_than_the_configured_threshold() { - config(['statamic.forms.drafts.delete_after' => 7]); + config(['statamic.forms.delete_incomplete_submissions_after' => 7]); $form = tap(Form::make('contact'))->save(); @@ -31,7 +31,7 @@ public function it_deletes_drafts_older_than_the_configured_threshold() Carbon::setTestNow('2025-06-15 12:00:00'); - (new DeleteDraftFormSubmissions)->handle(); + (new DeleteIncompleteFormSubmissions)->handle(); $this->assertNull($form->submission($oldDraft->id())); $this->assertNotNull($form->submission($recentDraft->id())); @@ -41,7 +41,7 @@ public function it_deletes_drafts_older_than_the_configured_threshold() #[Test] public function it_only_deletes_spam_submissions() { - config(['statamic.forms.drafts.delete_after' => 7]); + config(['statamic.forms.delete_incomplete_submissions_after' => 7]); $form = tap(Form::make('contact'))->save(); @@ -56,7 +56,7 @@ public function it_only_deletes_spam_submissions() Carbon::setTestNow('2025-06-30 12:00:00'); - (new DeleteDraftFormSubmissions)->handle(); + (new DeleteIncompleteFormSubmissions)->handle(); $this->assertNull($form->submission($draft->id())); $this->assertNotNull($form->submission($submitted->id())); @@ -66,7 +66,7 @@ public function it_only_deletes_spam_submissions() #[Test] public function it_does_not_delete_anything_when_disabled() { - config(['statamic.forms.drafts.delete_after' => null]); + config(['statamic.forms.delete_incomplete_submissions_after' => null]); $form = tap(Form::make('contact'))->save(); @@ -75,7 +75,7 @@ public function it_does_not_delete_anything_when_disabled() Carbon::setTestNow('2025-06-30 12:00:00'); - (new DeleteDraftFormSubmissions)->handle(); + (new DeleteIncompleteFormSubmissions)->handle(); $this->assertNotNull($form->submission($draft->id())); } diff --git a/tests/Forms/SubmissionTest.php b/tests/Forms/SubmissionTest.php index 819ae475303..b0a39472a01 100644 --- a/tests/Forms/SubmissionTest.php +++ b/tests/Forms/SubmissionTest.php @@ -249,20 +249,20 @@ public function it_determines_its_status() $form = tap(Form::make('contact_us'))->save(); $submitted = $form->makeSubmission(); - $this->assertFalse($submitted->isDraft()); + $this->assertFalse($submitted->isIncomplete()); $this->assertFalse($submitted->isSpam()); $this->assertFalse($submitted->isWithheld()); - $this->assertEquals('submitted', $submitted->status()); + $this->assertEquals('complete', $submitted->status()); - $draft = $form->makeSubmission()->set('draft', true); - $this->assertTrue($draft->isDraft()); - $this->assertFalse($draft->isSpam()); - $this->assertTrue($draft->isWithheld()); - $this->assertEquals('draft', $draft->status()); + $incomplete = $form->makeSubmission()->set('incomplete', true); + $this->assertTrue($incomplete->isIncomplete()); + $this->assertFalse($incomplete->isSpam()); + $this->assertTrue($incomplete->isWithheld()); + $this->assertEquals('incomplete', $incomplete->status()); $spam = $form->makeSubmission()->set('spam', true); $this->assertTrue($spam->isSpam()); - $this->assertFalse($spam->isDraft()); + $this->assertFalse($spam->isIncomplete()); $this->assertTrue($spam->isWithheld()); $this->assertEquals('spam', $spam->status()); } @@ -290,25 +290,25 @@ public function it_does_not_dispatch_creation_events_when_saving_a_withheld_subm public static function withheldStatusProvider(): array { return [ - 'draft' => ['draft'], + 'incomplete' => ['incomplete'], 'spam' => ['spam'], ]; } #[Test] - public function created_event_is_not_automatically_dispatched_when_removing_the_draft_key() + public function created_event_is_not_automatically_dispatched_when_removing_the_incomplete_key() { $form = tap(Form::make('contact_us'))->save(); - $submission = $form->makeSubmission()->set('draft', true); + $submission = $form->makeSubmission()->set('incomplete', true); $submission->save(); Event::fake(); - // Removing the draft key turns it into a "real" submission, but because the - // record already exists, save() alone won't dispatch Created. This is why + // Removing the incomplete key turns it into a "real" submission, but because + // the record already exists, save() alone won't dispatch Created. This is why // complete() dispatches it explicitly (covered by the test below). - $submission->remove('draft'); + $submission->remove('incomplete'); $submission->save(); Event::assertNotDispatched(SubmissionCreating::class); @@ -329,14 +329,15 @@ public function completing_a_new_submission_dispatches_created_event_once() Event::assertDispatched(SubmissionCreated::class, 1); Bus::assertDispatched(SendEmails::class, 1); + $this->assertNotNull($form->submission($submission->id())); } #[Test] - public function completing_a_withheld_submission_removes_the_status_key_and_dispatches_events() + public function completing_an_incomplete_or_spam_submission_removes_the_status_key_and_dispatches_events() { $form = tap(Form::make('contact_us'))->save(); - $submission = tap($form->makeSubmission()->set('draft', true)->set('spam', true))->save(); + $submission = tap($form->makeSubmission()->set('incomplete', true)->set('spam', true))->save(); Bus::fake(); Event::fake([SubmissionCreated::class, SubmissionCreating::class]); From dc3a5f5d67c1cf69457321dc0f64cc51258d7fbf Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Wed, 17 Jun 2026 09:50:01 +0100 Subject: [PATCH 03/14] push filters into the query string like other listings --- resources/js/components/forms/SubmissionListing.vue | 1 + 1 file changed, 1 insertion(+) diff --git a/resources/js/components/forms/SubmissionListing.vue b/resources/js/components/forms/SubmissionListing.vue index 3ca80b6df83..030e61f6f08 100644 --- a/resources/js/components/forms/SubmissionListing.vue +++ b/resources/js/components/forms/SubmissionListing.vue @@ -9,6 +9,7 @@ :sort-direction="sortDirection" :preferences-prefix="preferencesPrefix" :filters="filters" + push-query >