Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
0a334b9
wip
duncanmcclean Jun 16, 2026
14e4257
rename draft to incomplete
duncanmcclean Jun 17, 2026
dc3a5f5
push filters into the query string like other listings
duncanmcclean Jun 17, 2026
059b185
wip
duncanmcclean Jun 17, 2026
8f2a870
show complete submissions by default
duncanmcclean Jun 17, 2026
cc5fc42
add status indicator to submissions
duncanmcclean Jun 17, 2026
33fd58f
a few leftover bits from the rename
duncanmcclean Jun 17, 2026
9c7fb63
remove `isWithheld` method
duncanmcclean Jun 17, 2026
65fb4bf
formatting
duncanmcclean Jun 17, 2026
e53afa5
Merge branch 'forms-2' into forms-2-submission-statuses
duncanmcclean Jun 17, 2026
49995d4
fix test namespace
duncanmcclean Jun 17, 2026
7cd4b22
skip new tests temporarily to see if it fixes the failing tests
duncanmcclean Jun 17, 2026
d6c8ca8
wip
duncanmcclean Jun 17, 2026
ec60564
wip
duncanmcclean Jun 17, 2026
3305cff
fix failing tests
duncanmcclean Jun 17, 2026
41fc2f6
Extract form submission logic into `SubmitForm` action
duncanmcclean Jun 18, 2026
f644710
Merge branch 'forms-2' into forms-2-submission-statuses
duncanmcclean Jun 19, 2026
7cc2d48
Merge branch 'forms-2' into forms-2-submission-statuses
duncanmcclean Jun 22, 2026
ad5958b
Merge branch 'forms-2-submission-statuses' into forms-2-submit-form-a…
duncanmcclean Jun 22, 2026
ba87c32
Add `saveDraft` method
duncanmcclean Jun 23, 2026
af75c2a
provide `pages` variable in `form:create` tag
duncanmcclean Jun 23, 2026
f68410a
allow js drivers to add renderable data to pages
duncanmcclean Jun 23, 2026
c722435
add missing import
duncanmcclean Jun 24, 2026
aaa19c0
rename `saveDraft` to `saveIncomplete` to avoid confusion
duncanmcclean Jun 24, 2026
c487fb5
Merge branch 'forms-2' into forms-2-submit-form-action
duncanmcclean Jun 25, 2026
f807007
wip
duncanmcclean Jun 25, 2026
aff6db7
delete mark not spam action. not necessary in this pr
duncanmcclean Jun 25, 2026
f039d10
job has been renamed. delete old versions
duncanmcclean Jun 25, 2026
99a1ab8
update tests
duncanmcclean Jun 25, 2026
b29330e
drop `saveIncomplete` in favour of `asPartial`
duncanmcclean Jun 25, 2026
e410291
wip
duncanmcclean Jun 25, 2026
2f3f084
Merge branch 'forms-2-tag-data' into forms-2-submit-form-action
duncanmcclean Jun 25, 2026
8fc800d
wip
duncanmcclean Jun 26, 2026
22c2545
wip
duncanmcclean Jun 26, 2026
333e099
wip
duncanmcclean Jun 26, 2026
a4857cd
wip
duncanmcclean Jun 26, 2026
48c07a9
wip
duncanmcclean Jun 26, 2026
b7101dc
wip
duncanmcclean Jun 26, 2026
930eb7f
Don't dispatch `SubmissionCreated` twice when finalizing a persisted …
duncanmcclean Jun 29, 2026
cf2c232
evaluate page logic
duncanmcclean Jun 29, 2026
157bfe9
avoid open redirects with previous url
duncanmcclean Jun 29, 2026
a5749e6
validate extensions of uploaded files
duncanmcclean Jun 29, 2026
a6c8a28
prevent skipping pages
duncanmcclean Jun 29, 2026
87283d2
tweak the comment
duncanmcclean Jun 29, 2026
cff34f4
strict
duncanmcclean Jun 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions config/forms.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,16 @@
| Partial Submissions
|--------------------------------------------------------------------------
|
| Partial submissions are automatically deleted after a set number
| of days. Set this to null to prevent their automatic deletion.
| Partial submissions are automatically deleted after a set number of days.
| Set this to null to prevent their automatic deletion. You may also enable
| garbage collection to delete related assets at the same time.
|
*/

'delete_partial_submissions_after' => 7,

'garbage_collect_assets' => false,

/*
|--------------------------------------------------------------------------
| Exporters
Expand Down
3 changes: 2 additions & 1 deletion routes/web.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
use Statamic\Http\Middleware\AuthGuard;
use Statamic\Http\Middleware\CP\AuthGuard as CPAuthGuard;
use Statamic\Http\Middleware\CP\HandleInertiaRequests;
use Statamic\Http\Middleware\HandleFormPrecognitiveRequests;
use Statamic\Http\Middleware\RedirectIfTwoFactorSetupIncomplete;
use Statamic\Http\Middleware\RequireElevatedSession;
use Statamic\Statamic;
Expand All @@ -36,7 +37,7 @@

Route::name('statamic.')->group(function () {
Route::group(['prefix' => config('statamic.routes.action')], function () {
Route::post('forms/{form}', [FormController::class, 'submit'])->middleware([HandlePrecognitiveRequests::class, 'throttle:statamic.forms'])->name('forms.submit');
Route::post('forms/{form}', [FormController::class, 'submit'])->middleware([HandleFormPrecognitiveRequests::class, 'throttle:statamic.forms'])->name('forms.submit');

Route::get('protect/password', [PasswordProtectController::class, 'show'])->name('protect.password.show')->middleware([HandleInertiaRequests::class]);
Route::post('protect/password', [PasswordProtectController::class, 'store'])->name('protect.password.store');
Expand Down
12 changes: 11 additions & 1 deletion src/Exceptions/SilentFormFailureException.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,17 @@

namespace Statamic\Exceptions;

use Statamic\Contracts\Forms\Submission;

class SilentFormFailureException extends \Exception
{
//
public function __construct(protected ?Submission $submission = null)
{
parent::__construct();
}

public function submission(): ?Submission
{
return $this->submission;
}
}
17 changes: 8 additions & 9 deletions src/Forms/Fields/FormFields.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,13 @@ public function contents(): array

public function pages(): Collection
{
if (isset($this->contents['pages'])) {
return collect($this->contents['pages']);
}
$pages = isset($this->contents['pages'])
? collect($this->contents['pages'])
: collect([['sections' => $this->contents['sections'] ?? []]]);

return collect([
['sections' => $this->contents['sections'] ?? []],
return $pages->map(fn (array $page, int $index): array => [
...$page,
'id' => $page['id'] ?? ($pages->count() === 1 ? 'main' : 'page_'.($index + 1)),
]);
}

Expand Down Expand Up @@ -78,8 +79,6 @@ public function toBlueprint(): Blueprint
{
$tabs = $this->pages()
->mapWithKeys(function (array $page, int $index): array {
$id = $page['id'] ?? ($this->pages()->count() === 1 ? 'main' : 'page_'.($index + 1));

$sections = collect($page['sections'] ?? [])
->map(function (array $section): array {
return [
Expand All @@ -105,8 +104,8 @@ public function toBlueprint(): Blueprint
->all();

return [
$id => [
...$page,
$page['id'] => [
...Arr::except($page, 'id'),
'display' => $page['display'] ?? __('Page :current of :total', ['current' => $index + 1, 'total' => $this->pages()->count()]),
'sections' => $sections,
],
Expand Down
9 changes: 9 additions & 0 deletions src/Forms/Form.php
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,15 @@ private function convertFieldsFromBlueprint(Blueprint $blueprint): array
return ['sections' => $sections];
}

public function hasMultiplePages(): bool
{
if (! Statamic::formsProInstalled()) {
return false;
}

return $this->formFields()->pages()->count() > 1;
}

/**
* Get the blueprint.
*
Expand Down
12 changes: 12 additions & 0 deletions src/Forms/JsDrivers/AbstractJsDriver.php
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,18 @@ public function addToRenderableFieldAttributes($field)
return [];
}

/**
* Add to renderable page view data.
*
* @param \Statamic\Fields\Tab $page
* @param array $data
* @return array
*/
public function addToRenderablePageData($page, $data)
{
return [];
}

/**
* Render form html.
*
Expand Down
2 changes: 2 additions & 0 deletions src/Forms/JsDrivers/JsDriver.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ public function addToFormAttributes();

public function addToRenderableFieldData($field, $data);

// public function addToRenderablePageData($page, array $data): array;

public function addToRenderableFieldAttributes($field);

public function render($html);
Expand Down
97 changes: 97 additions & 0 deletions src/Forms/Logic/PageLogic.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
<?php

namespace Statamic\Forms\Logic;

use Illuminate\Support\Collection;
use Statamic\Contracts\Forms\Form;
use Statamic\Support\Arr;

class PageLogic
{
private ?Collection $pages = null;

public function __construct(private readonly Form $form)
{
}

public function path(array $data): array
{
return $this->buildPath($data, null);
}

public function pathTo(array $data, string $page): array
{
return $this->buildPath($data, $page);
}

private function buildPath(array $data, ?string $until): array
{
$path = [];
$pageId = Arr::get($this->pages()->first(), 'id');

// The "not in path" guard stops a cyclical rule from looping forever.
while ($pageId !== null && ! in_array($pageId, $path, true)) {
$path[] = $pageId;

if ($pageId === $until) {
break;
}

$pageId = $this->nextPage($pageId, $data);
}

return $path;
}

public function nextPage(string $currentPageId, array $data): ?string
{
$pages = $this->pages();
$currentIndex = $pages->search(fn (array $page): bool => $page['id'] === $currentPageId);

if ($currentIndex === false) {
return null;
}

if ($destination = $this->matchingDestination($pages->get($currentIndex), $data)) {
return $destination;
}

$nextPage = $pages->get($currentIndex + 1);

return $nextPage ? $nextPage['id'] : null;
}

public function isFinalPage(string $currentPageId, array $data): bool
{
return $this->nextPage($currentPageId, $data) === null;
}

private function matchingDestination(array $page, array $data): ?string
{
$evaluator = new RuleEvaluator;

foreach ($page['rules'] ?? [] as $rule) {
$destination = $rule['destination'] ?? null;

if (! $destination || ! $this->pageExists($destination)) {
continue;
}

if ($evaluator->passes($rule['conditions'] ?? [], $data)) {
return $destination;
}
}

return null;
}

private function pageExists(string $id): bool
{
return $this->pages()->contains(fn (array $page): bool => $page['id'] === $id);
}

private function pages(): Collection
{
return $this->pages ??= $this->form->formFields()->pages();
}
}
Loading
Loading