From 24d5d7ee1a92b228f98132e706d25c0d56c763c1 Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Tue, 18 Aug 2026 15:38:16 +0100 Subject: [PATCH 01/21] add `unique_instances` toggle --- lang/en/messages.php | 1 + src/Forms/Form.php | 9 +++++++++ src/Http/Controllers/CP/Forms/FormsController.php | 7 +++++++ 3 files changed, 17 insertions(+) diff --git a/lang/en/messages.php b/lang/en/messages.php index aefafc6ced2..f212d076cf4 100644 --- a/lang/en/messages.php +++ b/lang/en/messages.php @@ -138,6 +138,7 @@ 'form_configure_mailer_instructions' => 'Choose the mailer for sending this email. Leave blank to fall back to the default mailer.', 'form_configure_generate_fake_submissions_instructions' => 'Allow generating fake submissions and workflow testing from the submissions screen.', 'form_configure_store_instructions' => 'Disable to stop storing submissions. Events and email notifications will still be sent.', + 'form_configure_unique_instances_instructions' => 'Treat each entry using this form as its own instance. Submissions will be attached to the entry they were submitted from.', 'form_configure_title_instructions' => 'Use a call to action, such as \'Contact Us\'.', 'form_configure_close_date_instructions' => 'The form will stop accepting submissions after this date. Leave blank to never close.', 'form_configure_submission_limit_instructions' => 'The maximum number of submissions to accept. Leave blank for no limit.', diff --git a/src/Forms/Form.php b/src/Forms/Form.php index 30ec825563d..7222c2596bb 100644 --- a/src/Forms/Form.php +++ b/src/Forms/Form.php @@ -235,6 +235,15 @@ public function hasMultiplePages(): bool return $this->formFields()->pages()->count() > 1; } + public function hasUniqueInstances(): bool + { + if (! Statamic::formsProInstalled()) { + return false; + } + + return (bool) $this->get('unique_instances'); + } + /** * Get the blueprint. * diff --git a/src/Http/Controllers/CP/Forms/FormsController.php b/src/Http/Controllers/CP/Forms/FormsController.php index 2880654919d..152176e228e 100644 --- a/src/Http/Controllers/CP/Forms/FormsController.php +++ b/src/Http/Controllers/CP/Forms/FormsController.php @@ -212,6 +212,13 @@ protected function editFormBlueprint($form) 'type' => 'toggle', 'instructions' => __('statamic::messages.form_configure_store_instructions'), ], + ...(Statamic::formsProInstalled() ? [ + 'unique_instances' => [ + 'display' => __('Unique Instances'), + 'type' => 'toggle', + 'instructions' => __('statamic::messages.form_configure_unique_instances_instructions'), + ], + ] : []), 'generate_fake_submissions' => [ 'display' => __('Enable Fake Submission Generator'), 'type' => 'toggle', From d4b66cfcab0fbe3ce3eaddfdc60b58e0db4ae405 Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Tue, 18 Aug 2026 15:39:49 +0100 Subject: [PATCH 02/21] support `entry` key on submissions --- src/Forms/Submission.php | 30 ++++++++++--- src/Stache/Stores/FormSubmissionsStore.php | 1 + tests/Forms/SubmissionTest.php | 49 ++++++++++++++++++++++ 3 files changed, 75 insertions(+), 5 deletions(-) diff --git a/src/Forms/Submission.php b/src/Forms/Submission.php index e916d4a7a18..0f475978b33 100644 --- a/src/Forms/Submission.php +++ b/src/Forms/Submission.php @@ -17,6 +17,7 @@ use Statamic\Events\SubmissionFinalized; use Statamic\Events\SubmissionSaved; use Statamic\Events\SubmissionSaving; +use Statamic\Facades\Entry; use Statamic\Facades\File; use Statamic\Facades\FormSubmission; use Statamic\Facades\Site as Sites; @@ -70,9 +71,9 @@ public function data($data = null) $data = collect($data); // A full data replacement would otherwise drop the internal lifecycle - // keys, so carry over the existing partial and site values unless the - // incoming payload provides its own. - foreach (['partial', 'site'] as $key) { + // keys, so carry over the existing partial, site and entry values + // unless the incoming payload provides its own. + foreach (['partial', 'site', 'entry'] as $key) { if ($this->has($key) && ! $data->has($key)) { $data[$key] = $this->get($key); } @@ -125,6 +126,16 @@ public function site(Site|string|null $site = null): Site|static return $this; } + /** + * Get the entry this submission is attached to. + * + * @return \Statamic\Contracts\Entries\Entry|null + */ + public function entry() + { + return Entry::find($this->get('entry')); + } + /** * Get the form fields. * @@ -335,7 +346,7 @@ public function toArray() return $this->form()->fields()->keys()->flip() ->reject(function ($field, $key) { - return in_array($key, ['id', 'date', 'form']); + return in_array($key, ['id', 'date', 'form', 'entry']); }) ->map(function ($field, $key) use ($data) { return $data[$key] ?? null; @@ -344,14 +355,23 @@ public function toArray() 'id' => $this->id(), 'date' => $this->date(), ]) + ->when($this->has('entry'), fn ($values) => $values->merge([ + 'entry' => $this->get('entry'), + ])) ->all(); } public function augmentedArrayData() { - return array_merge($this->toArray(), [ + $data = array_merge($this->toArray(), [ 'form' => $this->form, ]); + + if ($this->has('entry')) { + $data['entry'] = $this->entry(); + } + + return $data; } public function blueprint() diff --git a/src/Stache/Stores/FormSubmissionsStore.php b/src/Stache/Stores/FormSubmissionsStore.php index 4660f869168..88c910f764d 100644 --- a/src/Stache/Stores/FormSubmissionsStore.php +++ b/src/Stache/Stores/FormSubmissionsStore.php @@ -18,6 +18,7 @@ class FormSubmissionsStore extends ChildStore protected $storeIndexes = [ 'form', 'date', + 'entry', ]; public function getItemKey($item) diff --git a/tests/Forms/SubmissionTest.php b/tests/Forms/SubmissionTest.php index f70edf270b0..75f1eb4101b 100644 --- a/tests/Forms/SubmissionTest.php +++ b/tests/Forms/SubmissionTest.php @@ -19,6 +19,7 @@ use Statamic\Forms\CreateAssetsFromFileUploads; use Statamic\Forms\DeleteTemporaryFiles; use Statamic\Forms\SendEmails; +use Tests\Factories\EntryFactory; use Tests\PreventSavingStacheItemsToDisk; use Tests\TestCase; @@ -155,6 +156,54 @@ public function setting_data_with_partial_or_site_in_the_payload_overrides_them( $this->assertEquals('de', $submission->get('site')); } + #[Test] + public function setting_data_preserves_the_entry_key() + { + $form = tap(Form::make('contact_us'))->save(); + + $submission = $form->makeSubmission()->set('entry', 'event-1'); + + $submission->data(['foo' => 'bar']); + + $this->assertEquals('bar', $submission->get('foo')); + $this->assertEquals('event-1', $submission->get('entry')); + } + + #[Test] + public function the_entry_is_included_in_to_array() + { + $form = tap(Form::make('contact_us')->formFields([ + 'sections' => [['fields' => [ + ['handle' => 'name', 'field' => ['type' => 'text']], + ]]], + ]))->save(); + + $submission = $form->makeSubmission()->data(['name' => 'San Holo']); + + $this->assertArrayNotHasKey('entry', $submission->toArray()); + + $submission->set('entry', 'event-1'); + + $this->assertEquals('event-1', $submission->toArray()['entry']); + } + + #[Test] + public function the_entry_is_augmented_to_the_entry_object() + { + $entry = (new EntryFactory)->collection('events')->id('event-1')->slug('event-one')->create(); + + $form = tap(Form::make('contact_us')->formFields([ + 'sections' => [['fields' => [ + ['handle' => 'name', 'field' => ['type' => 'text']], + ]]], + ]))->save(); + + $submission = $form->makeSubmission()->data(['name' => 'San Holo'])->set('entry', 'event-1'); + + $this->assertEquals($entry->id(), $submission->entry()->id()); + $this->assertEquals($entry->id(), $submission->augmentedArrayData()['entry']->id()); + } + #[Test] public function it_saves_a_submission() { From 1894815ad8ef0e115bebdb1502370c67728ed638 Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Tue, 18 Aug 2026 16:43:40 +0100 Subject: [PATCH 03/21] output hidden `_entry` field and refactor access feature to allow overriding options --- src/Exceptions/FormRestrictedException.php | 7 +- src/Forms/Form.php | 66 ++--------- src/Forms/Instance.php | 126 +++++++++++++++++++++ src/Forms/SubmitForm.php | 35 +++++- src/Forms/Tags.php | 12 +- src/Http/Controllers/FormController.php | 4 + tests/Forms/AccessTest.php | 71 +++++++++++- tests/Forms/InstanceTest.php | 112 ++++++++++++++++++ tests/Forms/SubmitFormTest.php | 74 ++++++++++++ 9 files changed, 441 insertions(+), 66 deletions(-) create mode 100644 src/Forms/Instance.php create mode 100644 tests/Forms/InstanceTest.php diff --git a/src/Exceptions/FormRestrictedException.php b/src/Exceptions/FormRestrictedException.php index f7d273bb008..6d4812d9e98 100644 --- a/src/Exceptions/FormRestrictedException.php +++ b/src/Exceptions/FormRestrictedException.php @@ -3,16 +3,17 @@ namespace Statamic\Exceptions; use Statamic\Contracts\Forms\Form; +use Statamic\Forms\Instance; class FormRestrictedException extends \Exception { - public function __construct(protected Form $form) + public function __construct(protected Instance $instance) { - parent::__construct($form->restrictionMessage()); + parent::__construct($instance->restrictionMessage()); } public function form(): Form { - return $this->form; + return $this->instance->form(); } } diff --git a/src/Forms/Form.php b/src/Forms/Form.php index 7222c2596bb..fe9c0354c94 100644 --- a/src/Forms/Form.php +++ b/src/Forms/Form.php @@ -2,7 +2,6 @@ namespace Statamic\Forms; -use Carbon\Carbon; use Illuminate\Contracts\Support\Arrayable; use Statamic\Contracts\Data\Augmentable; use Statamic\Contracts\Data\Augmented; @@ -24,7 +23,6 @@ use Statamic\Facades\File; use Statamic\Facades\Form as FormFacade; use Statamic\Facades\FormSubmission; -use Statamic\Facades\User; use Statamic\Facades\YAML; use Statamic\Fields\Blueprint; use Statamic\Forms\Exporters\Exporter; @@ -34,8 +32,6 @@ use Statamic\Support\Str; use Statamic\Support\Traits\FluentlyGetsAndSets; -use function Statamic\trans as __; - class Form implements Arrayable, Augmentable, ContainsQueryableValues, FormContract { use ContainsData, FluentlyGetsAndSets, HasAugmentedInstance; @@ -485,70 +481,24 @@ public function querySubmissions(): SubmissionQueryBuilder return FormSubmission::query()->where('form', $this->handle()); } - public function status(): string - { - return Blink::once('form-status-'.$this->handle(), fn () => match (true) { - $this->closingDateHasPassed() => 'closed', - $this->submissionLimitReached() => 'limit_reached', - default => 'open', - }); - } - - public function restricted(): bool + public function instance(?string $entry = null): Instance { - return $this->restrictionMessage() !== null; + return new Instance($this, $entry); } - public function restrictionMessage(): ?string - { - if ($this->closingDateHasPassed() || $this->submissionLimitReached()) { - return ($msg = $this->get('closed_message')) ? __($msg) : __('statamic::messages.form_closed_message'); - } - - if ($this->get('require_login') && ! User::current()) { - return ($msg = $this->get('require_login_message')) ? __($msg) : __('statamic::messages.form_require_login_message'); - } - - return null; - } - - private function closingDateHasPassed(): bool - { - if (! $date = $this->get('close_date')) { - return false; - } - - return Carbon::parse($date, config('app.timezone'))->isPast(); - } - - private function submissionLimitReached(): bool + public function status(): string { - if (! $limit = (int) $this->get('submission_limit')) { - return false; - } - - return $this->submissionCount() >= $limit; + return $this->instance()->status(); } - private function submissionCount(): int + public function restricted(): bool { - $query = $this->querySubmissions()->whereNull('partial'); - - if ($start = $this->submissionLimitPeriodStart()) { - $query->where('date', '>=', $start); - } - - return $query->count(); + return $this->instance()->restricted(); } - private function submissionLimitPeriodStart(): ?Carbon + public function restrictionMessage(): ?string { - return match ($this->get('submission_limit_period', 'total')) { - 'day' => now()->startOfDay(), - 'week' => now()->startOfWeek(), - 'month' => now()->startOfMonth(), - default => null, - }; + return $this->instance()->restrictionMessage(); } /** diff --git a/src/Forms/Instance.php b/src/Forms/Instance.php new file mode 100644 index 00000000000..46fd2633eaf --- /dev/null +++ b/src/Forms/Instance.php @@ -0,0 +1,126 @@ +form; + } + + public function entry(): ?string + { + return $this->entry; + } + + public function status(): string + { + return Blink::once('form-status-'.$this->form->handle().'-'.$this->entry, fn () => match (true) { + $this->closingDateHasPassed() => 'closed', + $this->submissionLimitReached() => 'limit_reached', + default => 'open', + }); + } + + public function restricted(): bool + { + return $this->restrictionMessage() !== null; + } + + public function restrictionMessage(): ?string + { + if ($this->closingDateHasPassed() || $this->submissionLimitReached()) { + return ($msg = $this->config('closed_message')) ? __($msg) : __('statamic::messages.form_closed_message'); + } + + if ($this->config('require_login') && ! User::current()) { + return ($msg = $this->config('require_login_message')) ? __($msg) : __('statamic::messages.form_require_login_message'); + } + + return null; + } + + public function config(string $key): mixed + { + return $this->overrides()[$key] ?? $this->form->get($key); + } + + private function overrides(): array + { + if (! $this->entry) { + return []; + } + + return Blink::once('form-instance-overrides-'.$this->form->handle().'-'.$this->entry, function () { + if (! $entry = Entry::find($this->entry)) { + return []; + } + + $value = $entry->blueprint()->fields()->all() + ->filter(fn ($field) => $field->type() === 'form') + ->map(fn ($field) => $entry->get($field->handle())) + ->first(fn ($value) => is_array($value) && Arr::get($value, 'form') === $this->form->handle()); + + return Arr::get($value, 'config', []); + }); + } + + private function closingDateHasPassed(): bool + { + if (! $date = $this->config('close_date')) { + return false; + } + + return Carbon::parse($date, config('app.timezone'))->isPast(); + } + + private function submissionLimitReached(): bool + { + if (! $limit = (int) $this->config('submission_limit')) { + return false; + } + + return $this->submissionCount() >= $limit; + } + + private function submissionCount(): int + { + $query = $this->form->querySubmissions()->whereNull('partial'); + + if ($this->entry) { + $query->where('entry', $this->entry); + } + + if ($start = $this->submissionLimitPeriodStart()) { + $query->where('date', '>=', $start); + } + + return $query->count(); + } + + private function submissionLimitPeriodStart(): ?Carbon + { + return match ($this->config('submission_limit_period') ?? 'total') { + 'day' => now()->startOfDay(), + 'week' => now()->startOfWeek(), + 'month' => now()->startOfMonth(), + default => null, + }; + } +} diff --git a/src/Forms/SubmitForm.php b/src/Forms/SubmitForm.php index 27fc1f443ca..75718f0923b 100644 --- a/src/Forms/SubmitForm.php +++ b/src/Forms/SubmitForm.php @@ -5,13 +5,16 @@ use Facades\Statamic\Fields\Validator as FieldValidator; use Illuminate\Support\Traits\Localizable; use Illuminate\Validation\ValidationException; +use Statamic\Contracts\Entries\Entry as EntryContract; use Statamic\Contracts\Forms\Form; use Statamic\Contracts\Forms\Submission; use Statamic\Events\FormSubmitted; +use Statamic\Exceptions\EntryNotFoundException; use Statamic\Exceptions\FormRestrictedException; use Statamic\Exceptions\SilentFormFailureException; use Statamic\Facades\Asset; use Statamic\Facades\AssetContainer; +use Statamic\Facades\Entry; use Statamic\Facades\Site; use Statamic\Forms\Logic\PageLogic; use Statamic\Rules\AllowedFile; @@ -23,6 +26,7 @@ class SubmitForm protected Form $form; protected ?string $page = null; + protected ?string $entry = null; protected ?Submission $submission = null; public function form(Form $form): static @@ -39,6 +43,13 @@ public function page(string $page): static return $this; } + public function entry(?string $entry): static + { + $this->entry = $entry; + + return $this; + } + public function resume(Submission $submission): static { $this->submission = $submission; @@ -53,7 +64,12 @@ public function submission(): ?Submission public function submit(array $data, array $files = []): SubmissionResult { - throw_if($this->form->restricted(), new FormRestrictedException($this->form)); + $entry = $this->getEntry(); + $instance = $this->form->instance($entry?->id()); + + if ($instance->restricted()) { + throw new FormRestrictedException($instance); + } $nextPage = null; $uploadedAssets = []; @@ -64,6 +80,10 @@ public function submit(array $data, array $files = []): SubmissionResult $this->submission = $this->submission ?? $this->form->makeSubmission()->asPartial()->site($this->site()); + if ($entry) { + $this->submission->set('entry', $entry->id()); + } + try { $uploadedAssets = $this->submission->uploadFiles($files); @@ -99,6 +119,19 @@ public function submit(array $data, array $files = []): SubmissionResult return new SubmissionResult($this->submission, $nextPage); } + private function getEntry(): ?EntryContract + { + if (! $this->form->hasUniqueInstances()) { + return null; + } + + try { + return Entry::findOrFail($this->entry); + } catch (EntryNotFoundException) { + throw ValidationException::withMessages(['*' => ['This form must be submitted from an entry.']]); + } + } + /** * Normalize uploaded files to arrays. * diff --git a/src/Forms/Tags.php b/src/Forms/Tags.php index f35a8f4d994..be33ce87d6d 100644 --- a/src/Forms/Tags.php +++ b/src/Forms/Tags.php @@ -99,9 +99,11 @@ public function create() $data['previous_page_url'] = $this->previousPageUrl(); } - $data['restricted'] = $form->restricted(); - $data['restriction_message'] = $form->restrictionMessage(); - $data['status'] = $form->status(); + $instance = $form->instance($form->hasUniqueInstances() ? $this->context->value('id') : null); + + $data['restricted'] = $instance->restricted(); + $data['restriction_message'] = $instance->restrictionMessage(); + $data['status'] = $instance->status(); if ($jsDriver) { $data['js_driver'] = $jsDriver->handle(); @@ -144,6 +146,10 @@ public function create() $params['page'] = Arr::get($this->currentPage(), 'id'); } + if ($entry = $instance->entry()) { + $params['entry'] = $entry; + } + if (! $this->canParseContents()) { return array_merge([ 'attrs' => $this->formAttrs($action, $method, $knownParams, $attrs), diff --git a/src/Http/Controllers/FormController.php b/src/Http/Controllers/FormController.php index 975290875f6..34dd7be6d52 100644 --- a/src/Http/Controllers/FormController.php +++ b/src/Http/Controllers/FormController.php @@ -31,6 +31,10 @@ public function submit(Request $request, $form, SubmitForm $action) $action->form($form); + if (is_string($entry = $request->input('_entry'))) { + $action->entry($entry); + } + if ($form->hasMultiplePages()) { $action->page($form->formFields()->pages()->first()['id']); diff --git a/tests/Forms/AccessTest.php b/tests/Forms/AccessTest.php index 2a9e283d76e..5ec4855fd3c 100644 --- a/tests/Forms/AccessTest.php +++ b/tests/Forms/AccessTest.php @@ -6,9 +6,11 @@ use Facades\Statamic\Console\Processes\Composer; use PHPUnit\Framework\Attributes\Test; use Statamic\Facades\Blink; +use Statamic\Facades\Blueprint; use Statamic\Facades\Form; use Statamic\Facades\Parse; use Statamic\Facades\User; +use Tests\Factories\EntryFactory; use Tests\PreventSavingStacheItemsToDisk; use Tests\TestCase; @@ -50,7 +52,7 @@ protected function makeSubmittableForm(array $data = []) )->save(); } - protected function submit($form, $count = 1, $partial = false) + protected function submit($form, $count = 1, $partial = false, $entry = null) { for ($i = 0; $i < $count; $i++) { $submission = $form->makeSubmission()->id(Carbon::now()->timestamp - $this->submissionId++); @@ -59,10 +61,24 @@ protected function submit($form, $count = 1, $partial = false) $submission->set('partial', true); } + if ($entry) { + $submission->set('entry', $entry); + } + $submission->save(); } } + private function makeEntry(string $id, array $formValue): void + { + Blueprint::make('event')->setNamespace('collections.events')->setContents(['fields' => [ + ['handle' => 'title', 'field' => ['type' => 'text']], + ['handle' => 'rsvp_form', 'field' => ['type' => 'form', 'max_items' => 1]], + ]])->save(); + + (new EntryFactory)->collection('events')->id($id)->slug($id)->data(['rsvp_form' => $formValue])->create(); + } + #[Test] public function a_form_with_no_restrictions_is_not_restricted() { @@ -102,6 +118,59 @@ public function it_is_restricted_when_the_submission_limit_is_reached() $this->assertEquals('This form is no longer accepting submissions.', $form->restrictionMessage()); } + #[Test] + public function the_submission_limit_is_scoped_per_entry_when_unique_instances_is_enabled() + { + $form = $this->makeForm(['submission_limit' => 2, 'unique_instances' => true]); + + $this->submit($form, 2, entry: 'event-1'); + $this->submit($form, 1, entry: 'event-2'); + + $this->assertTrue($form->instance('event-1')->restricted()); + $this->assertEquals('limit_reached', $form->instance('event-1')->status()); + + $this->assertFalse($form->instance('event-2')->restricted()); + $this->assertEquals('open', $form->instance('event-2')->status()); + } + + #[Test] + public function an_entry_can_override_the_submission_limit() + { + $form = $this->makeForm(['submission_limit' => 5, 'unique_instances' => true]); + + $this->makeEntry('event-1', ['form' => 'contact', 'config' => ['submission_limit' => 1]]); + + $this->submit($form, 1, entry: 'event-1'); + + $this->assertTrue($form->instance('event-1')->restricted()); + $this->assertFalse($form->restricted()); + } + + #[Test] + public function an_entry_can_override_the_close_date_and_message() + { + $form = $this->makeForm(['unique_instances' => true]); + + $this->makeEntry('event-1', ['form' => 'contact', 'config' => [ + 'close_date' => '2026-07-01 09:00', + 'closed_message' => 'This event is full.', + ]]); + + $this->assertTrue($form->instance('event-1')->restricted()); + $this->assertEquals('This event is full.', $form->instance('event-1')->restrictionMessage()); + $this->assertFalse($form->restricted()); + } + + #[Test] + public function overrides_from_an_entry_using_a_different_form_are_ignored() + { + $form = $this->makeForm(['unique_instances' => true]); + + $this->makeEntry('event-1', ['form' => 'another_form', 'config' => ['close_date' => '2026-07-01 09:00']]); + + $this->assertFalse($form->instance('event-1')->restricted()); + } + #[Test] public function partial_submissions_are_excluded_from_the_limit() { diff --git a/tests/Forms/InstanceTest.php b/tests/Forms/InstanceTest.php new file mode 100644 index 00000000000..10de6aef358 --- /dev/null +++ b/tests/Forms/InstanceTest.php @@ -0,0 +1,112 @@ +fakeStacheDirectory.'/forms'; + } + + protected function setUp(): void + { + parent::setUp(); + + Composer::shouldReceive('isInstalled')->with('statamic/forms-pro')->andReturn(true); + } + + private function makeForm(array $data = []) + { + return tap(Form::make('contact')->data($data))->save(); + } + + private function makeEntry(string $id, array $formValue): void + { + Blueprint::make('event')->setNamespace('collections.events')->setContents(['fields' => [ + ['handle' => 'title', 'field' => ['type' => 'text']], + ['handle' => 'rsvp_form', 'field' => ['type' => 'form', 'max_items' => 1]], + ]])->save(); + + (new EntryFactory)->collection('events')->id($id)->slug($id)->data(['rsvp_form' => $formValue])->create(); + } + + #[Test] + public function a_form_makes_instances() + { + $form = $this->makeForm(); + + $instance = $form->instance('event-1'); + + $this->assertInstanceOf(Instance::class, $instance); + $this->assertEquals($form->handle(), $instance->form()->handle()); + $this->assertEquals('event-1', $instance->entry()); + + $this->assertNull($form->instance()->entry()); + } + + #[Test] + public function the_default_instance_reads_the_forms_config() + { + $form = $this->makeForm(['submission_limit' => 5]); + + $this->assertEquals(5, $form->instance()->config('submission_limit')); + $this->assertNull($form->instance()->config('close_date')); + } + + #[Test] + public function an_entry_instance_prefers_the_entrys_overrides() + { + $form = $this->makeForm(['submission_limit' => 5, 'closed_message' => 'Closed.']); + + $this->makeEntry('event-1', ['form' => 'contact', 'config' => ['submission_limit' => 1]]); + + $instance = $form->instance('event-1'); + + $this->assertEquals(1, $instance->config('submission_limit')); + $this->assertEquals('Closed.', $instance->config('closed_message')); + } + + #[Test] + public function overrides_from_an_entry_using_a_different_form_are_ignored() + { + $form = $this->makeForm(['submission_limit' => 5]); + + $this->makeEntry('event-1', ['form' => 'another_form', 'config' => ['submission_limit' => 1]]); + + $this->assertEquals(5, $form->instance('event-1')->config('submission_limit')); + } + + #[Test] + public function an_unconfigured_entry_falls_back_to_the_forms_config() + { + $form = $this->makeForm(['submission_limit' => 5]); + + $this->makeEntry('event-1', ['form' => 'contact', 'config' => []]); + + $this->assertEquals(5, $form->instance('event-1')->config('submission_limit')); + } + + #[Test] + public function the_form_delegates_to_its_default_instance() + { + $form = $this->makeForm(['close_date' => '2020-01-01 09:00']); + + $this->assertEquals('closed', $form->status()); + $this->assertTrue($form->restricted()); + $this->assertEquals('This form is no longer accepting submissions.', $form->restrictionMessage()); + } +} diff --git a/tests/Forms/SubmitFormTest.php b/tests/Forms/SubmitFormTest.php index 033f7745d8e..123c9c4bbd5 100644 --- a/tests/Forms/SubmitFormTest.php +++ b/tests/Forms/SubmitFormTest.php @@ -16,12 +16,14 @@ use Statamic\Exceptions\SilentFormFailureException; use Statamic\Facades\Asset; use Statamic\Facades\AssetContainer; +use Statamic\Facades\Blueprint; use Statamic\Facades\Fieldset; use Statamic\Facades\Form; use Statamic\Forms\CreateAssetsFromFileUploads; use Statamic\Forms\SendEmails; use Statamic\Forms\SubmissionResult; use Statamic\Forms\SubmitForm; +use Tests\Factories\EntryFactory; use Tests\PreventSavingStacheItemsToDisk; use Tests\TestCase; @@ -1166,6 +1168,78 @@ public function it_finalizes_when_page_logic_legitimately_skips_a_page() $form->submissions()->each->delete(); } + #[Test] + public function it_attaches_the_entry_when_unique_instances_is_enabled() + { + (new EntryFactory)->collection('events')->id('event-1')->slug('event-one')->create(); + + $this->form->set('unique_instances', true)->save(); + + $result = $this->action()->entry('event-1')->submit(['email' => 'san@holo.com']); + + $this->assertEquals('event-1', $result->submission->get('entry')); + } + + #[Test] + public function it_rejects_the_submission_when_unique_instances_is_enabled_and_no_entry_is_provided() + { + $this->form->set('unique_instances', true)->save(); + + $this->expectException(ValidationException::class); + + $this->action()->submit(['email' => 'san@holo.com']); + } + + #[Test] + public function it_rejects_the_submission_when_the_entry_does_not_exist() + { + $this->form->set('unique_instances', true)->save(); + + $this->expectException(ValidationException::class); + + $this->action()->entry('missing')->submit(['email' => 'san@holo.com']); + } + + #[Test] + public function it_ignores_the_entry_when_unique_instances_is_disabled() + { + (new EntryFactory)->collection('events')->id('event-1')->slug('event-one')->create(); + + $result = $this->action()->entry('event-1')->submit(['email' => 'san@holo.com']); + + $this->assertFalse($result->submission->has('entry')); + } + + #[Test] + public function it_rejects_the_submission_when_an_entry_override_restricts_the_form() + { + Blueprint::make('event')->setNamespace('collections.events')->setContents(['fields' => [ + ['handle' => 'rsvp_form', 'field' => ['type' => 'form', 'max_items' => 1]], + ]])->save(); + + (new EntryFactory)->collection('events')->id('event-1')->slug('event-one')->data([ + 'rsvp_form' => ['form' => 'contact', 'config' => ['close_date' => '2020-01-01 09:00']], + ])->create(); + + $this->form->set('unique_instances', true)->save(); + + $this->expectException(FormRestrictedException::class); + + $this->action()->entry('event-1')->submit(['email' => 'san@holo.com']); + } + + #[Test] + public function it_ignores_unique_instances_when_forms_pro_is_not_installed() + { + Composer::shouldReceive('isInstalled')->with('statamic/forms-pro')->andReturnFalse(); + + $this->form->set('unique_instances', true)->save(); + + $result = $this->action()->submit(['email' => 'san@holo.com']); + + $this->assertFalse($result->submission->has('entry')); + } + private function uploadForm(bool $honeypot = false) { $form = Form::make('uploads'); From 112ab596ebb08213ac769876043a9802e23ee589 Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Tue, 18 Aug 2026 17:07:27 +0100 Subject: [PATCH 04/21] add entry column to submissions listing --- .../CP/Forms/FormSubmissionsController.php | 7 +- .../CP/Submissions/ListedSubmission.php | 11 +++ .../Resources/CP/Submissions/Submissions.php | 14 ++-- .../Forms/ViewSubmissionsListingTest.php | 70 +++++++++++++++++++ 4 files changed, 96 insertions(+), 6 deletions(-) diff --git a/src/Http/Controllers/CP/Forms/FormSubmissionsController.php b/src/Http/Controllers/CP/Forms/FormSubmissionsController.php index 321ed4730b5..ddcb9c60395 100644 --- a/src/Http/Controllers/CP/Forms/FormSubmissionsController.php +++ b/src/Http/Controllers/CP/Forms/FormSubmissionsController.php @@ -5,6 +5,7 @@ use Illuminate\Http\Request; use Inertia\Inertia; use Statamic\CP\Column; +use Statamic\CP\Columns; use Statamic\Events\FormSubmitted; use Statamic\Facades\Scope; use Statamic\Facades\Site; @@ -37,6 +38,10 @@ public function index(FilteredRequest $request, $form) ->blueprint() ->columns() ->prepend(Column::make('status'), 'status') + ->when( + $form->hasUniqueInstances(), + fn (Columns $columns) => $columns->prepend(Column::make('entry')->fieldtype('relationship')->sortable(false), 'entry') + ) ->prepend(Column::make('datestamp'), 'datestamp') ->setPreferred("forms.{$form->handle()}.columns") ->rejectUnlisted() @@ -91,7 +96,7 @@ protected function json(FilteredRequest $request, $form) $submissions = $query->paginate(Statamic::cpPerPage(request('perPage'))); return (new Submissions($submissions)) - ->blueprint($form->blueprint()) + ->form($form) ->columnPreferenceKey("forms.{$form->handle()}.columns") ->additional(['meta' => [ 'activeFilterBadges' => $activeFilterBadges, diff --git a/src/Http/Resources/CP/Submissions/ListedSubmission.php b/src/Http/Resources/CP/Submissions/ListedSubmission.php index 323b6995e2c..ad95661fd16 100644 --- a/src/Http/Resources/CP/Submissions/ListedSubmission.php +++ b/src/Http/Resources/CP/Submissions/ListedSubmission.php @@ -4,6 +4,7 @@ use Illuminate\Http\Resources\Json\JsonResource; use Statamic\Facades\User; +use Statamic\Fields\Field; class ListedSubmission extends JsonResource { @@ -48,6 +49,16 @@ protected function values($extra = []) return ['status' => $this->resource->status()]; } + if ($key === 'entry') { + $entry = (new Field('entry', ['type' => 'entries'])) + ->setValue($this->resource->get('entry')) + ->setParent($this->resource) + ->preProcessIndex() + ->value(); + + return ['entry' => $entry]; + } + $value = $extra[$key] ?? $this->resource->get($key); if (! $field = $this->blueprint->field($key)) { diff --git a/src/Http/Resources/CP/Submissions/Submissions.php b/src/Http/Resources/CP/Submissions/Submissions.php index fd9e89386f6..01d26366a25 100644 --- a/src/Http/Resources/CP/Submissions/Submissions.php +++ b/src/Http/Resources/CP/Submissions/Submissions.php @@ -13,13 +13,13 @@ class Submissions extends ResourceCollection use HasRequestedColumns; public $collects = ListedSubmission::class; - protected $blueprint; + protected $form; protected $columnPreferenceKey; protected $columns; - public function blueprint($blueprint) + public function form($form) { - $this->blueprint = $blueprint; + $this->form = $form; return $this; } @@ -33,8 +33,12 @@ public function columnPreferenceKey($key) private function setColumns() { - $columns = $this->blueprint + $columns = $this->form + ->blueprint() ->columns() + ->when($this->form->hasUniqueInstances(), fn ($columns) => $columns->ensurePrepended( + Column::make('entry')->label(__('Entry'))->fieldtype('relationship')->sortable(false) + )) ->ensurePrepended(Column::make('datestamp')->label('Date')); $status = Column::make('status') @@ -60,7 +64,7 @@ public function toArray($request) return $this->collection->each(function ($collection) { $collection - ->blueprint($this->blueprint) + ->blueprint($this->form->blueprint()) ->columns($this->requestedColumns()); }); } diff --git a/tests/Feature/Forms/ViewSubmissionsListingTest.php b/tests/Feature/Forms/ViewSubmissionsListingTest.php index f6f486bd897..e17ae5a4e30 100644 --- a/tests/Feature/Forms/ViewSubmissionsListingTest.php +++ b/tests/Feature/Forms/ViewSubmissionsListingTest.php @@ -2,10 +2,12 @@ namespace Tests\Feature\Forms; +use Facades\Statamic\Console\Processes\Composer; use PHPUnit\Framework\Attributes\Test; use Statamic\Facades\Form; use Statamic\Facades\FormSubmission; use Statamic\Facades\User; +use Tests\Factories\EntryFactory; use Tests\FakesRoles; use Tests\PreventSavingStacheItemsToDisk; use Tests\TestCase; @@ -22,6 +24,13 @@ protected function resolveApplicationConfiguration($app) $app['config']['statamic.forms.forms'] = $this->fakeStacheDirectory.'/forms'; } + protected function setUp(): void + { + parent::setUp(); + + Composer::shouldReceive('isInstalled')->with('statamic/forms-pro')->andReturn(false)->byDefault(); + } + #[Test] public function it_shows_the_listing_with_the_view_form_submissions_permission() { @@ -79,4 +88,65 @@ public function it_does_not_eager_load_actions_in_submissions_listing() ->assertJsonCount(1, 'data') ->assertJsonMissingPath('data.0.actions'); } + + #[Test] + public function it_includes_the_entry_column_when_unique_instances_is_enabled() + { + Composer::shouldReceive('isInstalled')->with('statamic/forms-pro')->andReturn(true); + + $entry = (new EntryFactory)->collection('events')->id('event-1')->slug('event-one')->data(['title' => 'Event One'])->create(); + + $user = tap(User::make()->makeSuper())->save(); + $form = tap(Form::make('test')->set('unique_instances', true))->save(); + FormSubmission::make()->form($form)->data(['entry' => 'event-1'])->save(); + + $response = $this + ->actingAs($user) + ->getJson(cp_route('forms.submissions.index', $form->handle())) + ->assertSuccessful() + ->assertJsonPath('data.0.entry.0.id', 'event-1') + ->assertJsonPath('data.0.entry.0.title', 'Event One') + ->assertJsonPath('data.0.entry.0.status', 'published') + ->assertJsonPath('data.0.entry.0.edit_url', $entry->editUrl()); + + $this->assertContains('entry', collect($response->json('meta.columns'))->pluck('field')->all()); + } + + #[Test] + public function it_doesnt_include_the_entry_column_when_unique_instances_is_disabled() + { + $user = tap(User::make()->makeSuper())->save(); + $form = tap(Form::make('test'))->save(); + FormSubmission::make()->form($form)->data(['foo' => 'bar'])->save(); + + $response = $this + ->actingAs($user) + ->getJson(cp_route('forms.submissions.index', $form->handle())) + ->assertSuccessful(); + + $this->assertNotContains('entry', collect($response->json('meta.columns'))->pluck('field')->all()); + } + + #[Test] + public function it_filters_submissions_by_entry() + { + Composer::shouldReceive('isInstalled')->with('statamic/forms-pro')->andReturn(true); + + (new EntryFactory)->collection('events')->id('event-1')->slug('event-one')->create(); + (new EntryFactory)->collection('events')->id('event-2')->slug('event-two')->create(); + + $user = tap(User::make()->makeSuper())->save(); + $form = tap(Form::make('test')->set('unique_instances', true))->save(); + FormSubmission::make()->form($form)->data(['entry' => 'event-1'])->save(); + FormSubmission::make()->form($form)->data(['entry' => 'event-2'])->save(); + + $filters = base64_encode(json_encode(['submission_entry' => ['entry' => 'event-2']])); + + $this + ->actingAs($user) + ->getJson(cp_route('forms.submissions.index', $form->handle()).'?filters='.$filters) + ->assertSuccessful() + ->assertJsonCount(1, 'data') + ->assertJsonPath('data.0.entry.0.id', 'event-2'); + } } From 8114902f0e995ddef6dc1e2da8990aa94567f508 Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Tue, 18 Aug 2026 17:14:51 +0100 Subject: [PATCH 05/21] add entry filter to submissions listing --- src/Providers/ExtensionServiceProvider.php | 1 + src/Query/Scopes/Filters/SubmissionEntry.php | 74 ++++++++++++++++++++ tests/Query/SubmissionEntryFilterTest.php | 54 ++++++++++++++ 3 files changed, 129 insertions(+) create mode 100644 src/Query/Scopes/Filters/SubmissionEntry.php create mode 100644 tests/Query/SubmissionEntryFilterTest.php diff --git a/src/Providers/ExtensionServiceProvider.php b/src/Providers/ExtensionServiceProvider.php index d0f524d1433..e6bef989509 100644 --- a/src/Providers/ExtensionServiceProvider.php +++ b/src/Providers/ExtensionServiceProvider.php @@ -201,6 +201,7 @@ class ExtensionServiceProvider extends ServiceProvider Scopes\Filters\Fields::class, Scopes\Filters\Blueprint::class, Scopes\Filters\Status::class, + Scopes\Filters\SubmissionEntry::class, Scopes\Filters\SubmissionSite::class, Scopes\Filters\SubmissionStatus::class, Scopes\Filters\Site::class, diff --git a/src/Query/Scopes/Filters/SubmissionEntry.php b/src/Query/Scopes/Filters/SubmissionEntry.php new file mode 100644 index 00000000000..39ae15b19f1 --- /dev/null +++ b/src/Query/Scopes/Filters/SubmissionEntry.php @@ -0,0 +1,74 @@ + [ + 'display' => __('Entry'), + 'type' => 'select', + 'options' => $this->options()->all(), + ], + ]; + } + + public function autoApply() + { + if ($entry = $this->context['entry'] ?? null) { + return ['entry' => $entry]; + } + + return []; + } + + public function apply($query, $values) + { + $query->where('entry', $values['entry']); + } + + public function badge($values) + { + return __('Entry').': '.($this->options()->get($values['entry']) ?? $values['entry']); + } + + public function visibleTo($key) + { + return $key === 'form-submissions' && $this->form()?->hasUniqueInstances(); + } + + private function form() + { + return Form::find($this->context['form'] ?? null); + } + + private function options() + { + $ids = $this->form() + ->querySubmissions() + ->whereNotNull('entry') + ->get(['entry']) + ->map + ->get('entry') + ->unique() + ->values(); + + return Entry::query() + ->whereIn('id', $ids->all()) + ->get() + ->mapWithKeys(fn ($entry) => [$entry->id() => $entry->value('title')]); + } +} diff --git a/tests/Query/SubmissionEntryFilterTest.php b/tests/Query/SubmissionEntryFilterTest.php new file mode 100644 index 00000000000..90b463d0ae3 --- /dev/null +++ b/tests/Query/SubmissionEntryFilterTest.php @@ -0,0 +1,54 @@ +fakeStacheDirectory.'/forms'; + } + + protected function setUp(): void + { + parent::setUp(); + + Composer::shouldReceive('isInstalled')->with('statamic/forms-pro')->andReturn(true); + + Form::make('test')->set('unique_instances', true)->save(); + } + + private function filter(array $context = []) + { + return Scope::find('submission_entry', array_merge(['form' => 'test'], $context)); + } + + #[Test] + public function it_is_only_visible_when_the_form_has_unique_instances() + { + $this->assertTrue($this->filter()->visibleTo('form-submissions')); + $this->assertFalse($this->filter()->visibleTo('entries')); + + Form::find('test')->set('unique_instances', false)->save(); + + $this->assertFalse($this->filter()->visibleTo('form-submissions')); + } + + #[Test] + public function it_auto_applies_when_the_context_provides_an_entry() + { + $this->assertEquals(['entry' => 'event-1'], $this->filter(['entry' => 'event-1'])->autoApply()); + $this->assertEquals([], $this->filter()->autoApply()); + } +} From f953e1391f8388e5144c87e244d74de6c5dd9c8a Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Thu, 20 Aug 2026 00:09:43 +0200 Subject: [PATCH 06/21] mock `statamic/forms-pro` check in tests --- tests/Feature/Forms/EditFormTest.php | 8 ++++++++ tests/Feature/Forms/UpdateFormTest.php | 8 ++++++++ 2 files changed, 16 insertions(+) diff --git a/tests/Feature/Forms/EditFormTest.php b/tests/Feature/Forms/EditFormTest.php index dac8ecaf928..c51ed565321 100644 --- a/tests/Feature/Forms/EditFormTest.php +++ b/tests/Feature/Forms/EditFormTest.php @@ -2,6 +2,7 @@ namespace Tests\Feature\Forms; +use Facades\Statamic\Console\Processes\Composer; use PHPUnit\Framework\Attributes\Test; use Statamic\Facades\Form; use Statamic\Facades\User; @@ -21,6 +22,13 @@ protected function resolveApplicationConfiguration($app) $app['config']['statamic.forms.forms'] = $this->fakeStacheDirectory.'/forms'; } + protected function setUp(): void + { + parent::setUp(); + + Composer::shouldReceive('isInstalled')->with('statamic/forms-pro')->andReturn(false)->byDefault(); + } + #[Test] public function it_shows_the_edit_page_if_you_have_permission() { diff --git a/tests/Feature/Forms/UpdateFormTest.php b/tests/Feature/Forms/UpdateFormTest.php index 00b7063662c..fe40170a682 100644 --- a/tests/Feature/Forms/UpdateFormTest.php +++ b/tests/Feature/Forms/UpdateFormTest.php @@ -2,6 +2,7 @@ namespace Tests\Feature\Forms; +use Facades\Statamic\Console\Processes\Composer; use PHPUnit\Framework\Attributes\Test; use Statamic\Facades\Form; use Statamic\Facades\User; @@ -21,6 +22,13 @@ protected function resolveApplicationConfiguration($app) $app['config']['statamic.forms.forms'] = $this->fakeStacheDirectory.'/forms'; } + protected function setUp(): void + { + parent::setUp(); + + Composer::shouldReceive('isInstalled')->with('statamic/forms-pro')->andReturn(false)->byDefault(); + } + #[Test] public function it_denies_access_if_you_dont_have_permission() { From bade644e305aaca4d4ade81a581ba2872038ab2d Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Sat, 22 Aug 2026 21:15:32 +0200 Subject: [PATCH 07/21] show entry on submission show page --- resources/js/pages/forms/Submission.vue | 7 +- .../CP/Forms/FormSubmissionsController.php | 31 +++++-- tests/Feature/Forms/ViewSubmissionTest.php | 86 +++++++++++++++++++ 3 files changed, 117 insertions(+), 7 deletions(-) create mode 100644 tests/Feature/Forms/ViewSubmissionTest.php diff --git a/resources/js/pages/forms/Submission.vue b/resources/js/pages/forms/Submission.vue index 702e1359738..4a0ba5d7cbb 100644 --- a/resources/js/pages/forms/Submission.vue +++ b/resources/js/pages/forms/Submission.vue @@ -1,7 +1,7 @@ + + diff --git a/resources/js/pages/forms/Submission.vue b/resources/js/pages/forms/Submission.vue index 4a0ba5d7cbb..a356b904164 100644 --- a/resources/js/pages/forms/Submission.vue +++ b/resources/js/pages/forms/Submission.vue @@ -1,14 +1,11 @@ From 1a11b18ee9ddf2362b9badb961c9563e4a090074 Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Mon, 24 Aug 2026 16:22:41 +0100 Subject: [PATCH 11/21] add "view submissions" and "configure" options to the form fieldtype Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01HWzPmQoku8u1dA6VMp51RS --- lang/en/messages.php | 1 + resources/js/bootstrap/fieldtypes.js | 6 + .../components/fieldtypes/FormFieldtype.vue | 170 ++++++++++++++ .../fieldtypes/FormIndexFieldtype.vue | 22 ++ .../components/fieldtypes/FormRelatedItem.vue | 81 +++++++ .../components/forms/InlineSubmissionForm.vue | 43 ++++ .../js/components/forms/SubmissionListing.vue | 31 ++- src/Forms/Fieldtype.php | 216 +++++++++++++++++- tests/Fieldtypes/FormTest.php | 122 ++++++++++ 9 files changed, 684 insertions(+), 8 deletions(-) create mode 100644 resources/js/components/fieldtypes/FormFieldtype.vue create mode 100644 resources/js/components/fieldtypes/FormIndexFieldtype.vue create mode 100644 resources/js/components/fieldtypes/FormRelatedItem.vue create mode 100644 resources/js/components/forms/InlineSubmissionForm.vue create mode 100644 tests/Fieldtypes/FormTest.php diff --git a/lang/en/messages.php b/lang/en/messages.php index f212d076cf4..447c62a53f6 100644 --- a/lang/en/messages.php +++ b/lang/en/messages.php @@ -147,6 +147,7 @@ 'form_configure_require_login_instructions' => 'Only allow logged in users to submit this form.', 'form_configure_require_login_message_instructions' => 'Shown when a logged out visitor tries to submit the form. Leave blank to use the default message.', 'form_closed_message' => 'This form is no longer accepting submissions.', + 'form_fieldtype_configure_instructions' => 'These settings override the form\'s own settings for this entry. Leave a field empty to use the form\'s setting.', 'form_require_login_message' => 'You must be logged in to submit this form.', 'form_create_description' => 'Get started by creating your first form.', 'form_builder' => 'Form Builder', diff --git a/resources/js/bootstrap/fieldtypes.js b/resources/js/bootstrap/fieldtypes.js index 6f1c83b7936..9de57d56a24 100644 --- a/resources/js/bootstrap/fieldtypes.js +++ b/resources/js/bootstrap/fieldtypes.js @@ -32,6 +32,9 @@ import Grid from '../components/fieldtypes/grid/Grid.vue'; import GridIndex from '../components/fieldtypes/grid/GridIndex.vue'; import GroupFieldtype from '../components/fieldtypes/GroupFieldtype.vue'; import FormBannerFieldtype from '../components/fieldtypes/FormBannerFieldtype.vue'; +import FormFieldtype from '../components/fieldtypes/FormFieldtype.vue'; +import FormIndexFieldtype from '../components/fieldtypes/FormIndexFieldtype.vue'; +import FormRelatedItem from '../components/fieldtypes/FormRelatedItem.vue'; import FormHeadingFieldtype from '../components/fieldtypes/FormHeadingFieldtype.vue'; import FormParagraphFieldtype from '@/components/fieldtypes/FormParagraphFieldtype.vue'; import FormUploadFieldtype from '@/components/fieldtypes/FormUploadFieldtype.vue'; @@ -114,6 +117,9 @@ export default function registerFieldtypes(app) { app.component('grid-fieldtype', Grid); app.component('grid-fieldtype-index', GridIndex); app.component('group-fieldtype', GroupFieldtype); + app.component('form-fieldtype', FormFieldtype); + app.component('form-fieldtype-index', FormIndexFieldtype); + app.component('form-related-item', FormRelatedItem); app.component('form_banner-fieldtype', FormBannerFieldtype); app.component('form_heading-fieldtype', FormHeadingFieldtype); app.component('form_paragraph-fieldtype', FormParagraphFieldtype); diff --git a/resources/js/components/fieldtypes/FormFieldtype.vue b/resources/js/components/fieldtypes/FormFieldtype.vue new file mode 100644 index 00000000000..297fb8acbb6 --- /dev/null +++ b/resources/js/components/fieldtypes/FormFieldtype.vue @@ -0,0 +1,170 @@ + + + diff --git a/resources/js/components/fieldtypes/FormIndexFieldtype.vue b/resources/js/components/fieldtypes/FormIndexFieldtype.vue new file mode 100644 index 00000000000..d2a27930234 --- /dev/null +++ b/resources/js/components/fieldtypes/FormIndexFieldtype.vue @@ -0,0 +1,22 @@ + + + diff --git a/resources/js/components/fieldtypes/FormRelatedItem.vue b/resources/js/components/fieldtypes/FormRelatedItem.vue new file mode 100644 index 00000000000..a7767e643e6 --- /dev/null +++ b/resources/js/components/fieldtypes/FormRelatedItem.vue @@ -0,0 +1,81 @@ + + + diff --git a/resources/js/components/forms/InlineSubmissionForm.vue b/resources/js/components/forms/InlineSubmissionForm.vue new file mode 100644 index 00000000000..a2c32ead806 --- /dev/null +++ b/resources/js/components/forms/InlineSubmissionForm.vue @@ -0,0 +1,43 @@ + + + diff --git a/resources/js/components/forms/SubmissionListing.vue b/resources/js/components/forms/SubmissionListing.vue index db3004806d5..f1423c98665 100644 --- a/resources/js/components/forms/SubmissionListing.vue +++ b/resources/js/components/forms/SubmissionListing.vue @@ -9,30 +9,41 @@ :sort-direction="sortDirection" :preferences-prefix="preferencesPrefix" :filters="filters" - push-query + :allow-presets="false" + :push-query="!viewInStack" >