Skip to content

Commit d5b1ea8

Browse files
authored
feat: multi-page form submission improvements (#425)
* fix: Flush state between multiple calls to `GFUtils::submit_form()` * feat: improve multi-page submission handling
1 parent c770c4e commit d5b1ea8

File tree

8 files changed

+471
-25
lines changed

8 files changed

+471
-25
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@
55
- feat!: Implement `FormField` model and `DataLoader`, and refactor `FormFieldsConnectionResolver` to extend `AbstractConnectionResolver`.
66
- feat!: Refactor `FormsConnectionResolver` and `EntriesConnectionResolver` for compatibility with WPGraphQL v1.26.0 improvements.
77
- feat!: Narrow `FormField.choices` and `FormField.inputs` field types to their implementations.
8+
- feat: Add `targetPageNumber` and `targetPageFormFields` to `SubmitGfFormPayload` for better multi-page form support.
89
- fix!: Keep `PageField` with previous page data when filtering `formFields` by `pageNumber`. H/t @SamuelHadsall .
910
- fix: Handle RadioField submission values when using a custom "other" choice. H/t @Gytjarek .
1011
- fix: Check for Submission Confirmation url before attempting to get the associated post ID.
12+
- fix: Flush static Gravity Forms state between multiple calls to `GFUtils::submit_form()`.
1113
- feat: Add `FieldError.connectedFormField` connection to `FieldError` type.
1214
- dev: Use `FormFieldsDataLoader` to resolve fields instead of instantiating a new `Model`.
1315
- chore: Add iterable type hints.
@@ -16,6 +18,7 @@
1618
- chore!: Bump minimum Gravity Forms version to v2.7.0.
1719
- chore: Declare `strict_types` in all PHP files.
1820
- chore: Update Composer dev-dependencies and fix test compatibility with `wp-graphql-test-case` v3.0.x.
21+
- docs: Add docs on using Multi-page forms.
1922

2023
## v0.12.6.1
2124

docs/submitting-forms.md

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -317,3 +317,52 @@ mutation submit {
317317
}
318318
}
319319
```
320+
321+
## Submitting Multi-page Forms
322+
323+
When submitting a multi-page form, you can use the `sourcePage` and `targetPage` inputs to validate a specific page of the form before proceeding to the next page. This can then be combined with the Mutation payload's `targetPageNumber` and `targetPageFormFields` to serve the correct fields for the next _valid_ page.
324+
325+
When using a `sourcePage`, only the fields on that page will be validated. If that page fails validation, the `targetPageNumber` and `targetPageFormFields` will return the current page number and fields, instead of the provided `targetPage` input. Similarly, if the page passes validation, but the `targetPage` is not available (e.g. due to conditional page logic), the `targetPageNumber` and `targetPageFormFields` will return the next available page number and fields.
326+
327+
Only once the `targetPage` input is greater than the number of pages in the form, will the submission be processed, _all_ the values validated, and the entry created. As such, when using this pattern, it is recommended to submit all the user-provided `fieldValues` inputs to the mutation, and not just the fields on the current page.
328+
329+
### Example Mutation
330+
331+
```graphql
332+
mutation submit {
333+
submitGfForm(
334+
input: {
335+
id: 50
336+
fieldValues: [
337+
# other form fields would go here.
338+
{
339+
# Text field value
340+
id: 1
341+
value: "This is a text field value."
342+
}
343+
]
344+
saveAsDraft: false
345+
sourcePage: 1 # The page we are validating
346+
targetPage: 2 # The page we want to navigate to.
347+
}
348+
) {
349+
errors {
350+
id
351+
message
352+
}
353+
confirmation {
354+
message
355+
}
356+
entry { # Will only be returned if the `targetPage` is greater than the number of pages in the form.
357+
databaseId
358+
}
359+
targetPageNumber # The page number to navigate to. Will be the same as the `sourcePage` if validation fails, and different than the `targetPage` if the `targetPage` is not available.
360+
targetPageFormFields { # The form fields for the next page.
361+
nodes {
362+
databaseId
363+
# Other field data
364+
}
365+
}
366+
}
367+
}
368+
```

src/Connection/FormFieldsConnection.php

Lines changed: 41 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,24 +12,57 @@
1212

1313
namespace WPGraphQL\GF\Connection;
1414

15+
use GraphQL\Type\Definition\ResolveInfo;
16+
use WPGraphQL\AppContext;
17+
use WPGraphQL\GF\Data\Factory;
18+
use WPGraphQL\GF\Data\Loader\FormsLoader;
19+
use WPGraphQL\GF\Mutation\SubmitForm;
1520
use WPGraphQL\GF\Type\Enum\FormFieldTypeEnum;
21+
use WPGraphQL\GF\Type\WPInterface\FormField;
1622

1723
/**
1824
* Class - FormFieldsConnection
1925
*/
2026
class FormFieldsConnection extends AbstractConnection {
21-
/**
22-
* {@inheritDoc}
23-
*/
24-
public static function register_hooks(): void {
25-
// @todo register to rootQuery.
26-
}
27-
2827
/**
2928
* {@inheritDoc}
3029
*/
3130
public static function register(): void {
32-
// @todo register to rootQuery.
31+
// SubmitGfFormPayload to FormFields.
32+
register_graphql_connection(
33+
[
34+
'fromType' => SubmitForm::$name . 'Payload',
35+
'toType' => FormField::$type,
36+
'fromFieldName' => 'targetPageFormFields',
37+
'resolve' => static function ( $source, array $args, AppContext $context, ResolveInfo $info ) {
38+
// If the source doesn't have a targetPageNumber, we can't resolve the connection.
39+
if ( empty( $source['targetPageNumber'] ) ) {
40+
return null;
41+
}
42+
43+
// If the form isn't stored in the context, we need to fetch it.
44+
if ( empty( $context->gfForm ) && ! empty( $source['form_id'] ) ) {
45+
$form = $context->get_loader( FormsLoader::$name )->load( (int) $source['form_id'] );
46+
47+
if ( null === $form ) {
48+
return null;
49+
}
50+
51+
// Store it in the context for easy access.
52+
$context->gfForm = $form;
53+
}
54+
55+
if ( empty( $context->gfForm->formFields ) ) {
56+
return null;
57+
}
58+
59+
// Set the Args for the connection resolver.
60+
$args['where']['pageNumber'] = $source['targetPageNumber'];
61+
62+
return Factory::resolve_form_fields_connection( $context->gfForm, $args, $context, $info );
63+
},
64+
]
65+
);
3366
}
3467

3568
/**

src/Mutation/SubmitForm.php

Lines changed: 34 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ class SubmitForm extends AbstractMutation {
3535
*
3636
* @var string
3737
*/
38-
public static $name = 'submitGfForm';
38+
public static $name = 'SubmitGfForm';
3939

4040
/**
4141
* {@inheritDoc}
@@ -74,11 +74,11 @@ public static function get_input_fields(): array {
7474
*/
7575
public static function get_output_fields(): array {
7676
return [
77-
'confirmation' => [
77+
'confirmation' => [
7878
'type' => SubmissionConfirmation::$type,
7979
'description' => __( 'The form confirmation data. Null if the submission has `errors`', 'wp-graphql-gravity-forms' ),
8080
],
81-
'entry' => [
81+
'entry' => [
8282
'type' => Entry::$type,
8383
'description' => __( 'The entry that was created.', 'wp-graphql-gravity-forms' ),
8484
'resolve' => static function ( array $payload, array $args, AppContext $context ) {
@@ -117,14 +117,18 @@ public static function get_output_fields(): array {
117117
}
118118
},
119119
],
120-
'errors' => [
120+
'errors' => [
121121
'type' => [ 'list_of' => FieldError::$type ],
122122
'description' => __( 'Field errors.', 'wp-graphql-gravity-forms' ),
123123
],
124-
'resumeUrl' => [
124+
'resumeUrl' => [
125125
'type' => 'String',
126126
'description' => __( 'Draft resume URL. Null if submitting an entry. If the "Referer" header is not included in the request, this will be an empty string.', 'wp-graphql-gravity-forms' ),
127127
],
128+
'targetPageNumber' => [
129+
'type' => 'Int',
130+
'description' => __( 'The page number of the form that should be displayed after submission. This will be different than the `targetPage` provided to the mutation if a field on a previous field failed validation.', 'wp-graphql-gravity-forms' ),
131+
],
128132
];
129133
}
130134

@@ -161,14 +165,15 @@ public static function mutate_and_get_payload(): callable {
161165
if ( $submission['is_valid'] ) {
162166
self::update_entry_properties( $form, $submission, $entry_data );
163167
}
164-
165168
return [
166-
'confirmation' => isset( $submission['confirmation_type'] ) ? EntryObjectMutation::get_submission_confirmation( $submission ) : null,
167-
'entryId' => ! empty( $submission['entry_id'] ) ? absint( $submission['entry_id'] ) : null,
168-
'errors' => isset( $submission['validation_messages'] ) ? EntryObjectMutation::get_submission_errors( $submission['validation_messages'], $form_id ) : null,
169-
'resumeToken' => $submission['resume_token'] ?? null,
170-
'resumeUrl' => isset( $submission['resume_token'] ) ? GFUtils::get_resume_url( $submission['resume_token'], $entry_data['source_url'] ?? '', $form ) : null,
171-
'submission' => $submission,
169+
'confirmation' => isset( $submission['confirmation_type'] ) ? EntryObjectMutation::get_submission_confirmation( $submission ) : null,
170+
'entryId' => ! empty( $submission['entry_id'] ) ? absint( $submission['entry_id'] ) : null,
171+
'errors' => isset( $submission['validation_messages'] ) ? EntryObjectMutation::get_submission_errors( $submission['validation_messages'], $form_id ) : null,
172+
'form_id' => $form_id,
173+
'resumeToken' => $submission['resume_token'] ?? null,
174+
'resumeUrl' => isset( $submission['resume_token'] ) ? GFUtils::get_resume_url( $submission['resume_token'], $entry_data['source_url'] ?? '', $form ) : null,
175+
'submission' => $submission,
176+
'targetPageNumber' => self::get_target_page_number( $target_page, $submission ),
172177
];
173178
};
174179
}
@@ -300,4 +305,21 @@ private static function get_input_values( bool $is_draft, array $field_values, a
300305

301306
return $input_values + $field_values;
302307
}
308+
309+
/**
310+
* Get the target page number use to resolve the mutation.
311+
*
312+
* @param int $original_target_page The original target page number.
313+
* @param array<int|string,mixed> $submission The Gravity Forms submission result array.
314+
*/
315+
private static function get_target_page_number( int $original_target_page, array $submission ): ?int {
316+
// Valid Draft submissions should pass through to the original target page.
317+
if ( ! empty( $submission['resume_token'] ) && ! empty( $submission['is_valid'] ) ) {
318+
return ! empty( $original_target_page ) ? $original_target_page : null;
319+
}
320+
321+
// Regular submissions should return the target page.
322+
// In draft submissions, the target page is the source page, so this will work with invalid submissions.
323+
return ! empty( $submission['page_number'] ) ? (int) $submission['page_number'] : null;
324+
}
303325
}

src/Utils/GFUtils.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -362,6 +362,11 @@ public static function submit_form( int $form_id, array $input_values, array $fi
362362
$source_page,
363363
);
364364

365+
// Cleanup GF state.
366+
unset( $_POST );
367+
\GFFormsModel::flush_current_lead();
368+
\GFFormDisplay::$submission = [];
369+
365370
if ( $submission instanceof \WP_Error ) {
366371
throw new UserError( esc_html( $submission->get_error_message() ) );
367372
}

tests/_support/Helper/GFHelpers/PropertyHelper.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,10 @@ public function gquizWeightedScoreEnabled( ?bool $value = null ): ?bool {
232232
}
233233

234234
public function errorMessage( $value = null ) {
235+
if ( null === $value ) {
236+
return null;
237+
}
238+
235239
return ! empty( $value ) ? $value : 'Some error message';
236240
}
237241

@@ -267,7 +271,7 @@ public function hasInputMask( ?bool $value = null ): ?bool {
267271
}
268272

269273
public function isRequired( $value = null ) {
270-
return null !== $value ? $value : false;
274+
return null !== $value ? ! empty( $value ) : false;
271275
}
272276

273277
public function isSelected( $value = null ) {

tests/wpunit/FormFieldConnectionPageFilterTest.php

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,12 @@ class FormFieldConnectionPageFilterTest extends GFGraphQLTestCase {
1717
private $fields;
1818

1919
/**
20-
* run before each test.
20+
* {@inheritDoc}
2121
*/
2222
public function setUp(): void {
2323
// Before...
2424
parent::setUp();
2525

26-
wp_set_current_user( $this->admin->ID );
27-
2826
$this->fields = $this->generate_form_pages( 3 );
2927
$this->form_id = $this->factory->form->create(
3028
array_merge(
@@ -37,7 +35,7 @@ public function setUp(): void {
3735
}
3836

3937
/**
40-
* Run after each test.
38+
* {@inheritDoc}
4139
*/
4240
public function tearDown(): void {
4341
// Your tear down methods here.

0 commit comments

Comments
 (0)