diff --git a/CHANGELOG.md b/CHANGELOG.md index f8b7b543..81456f6c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ - feat!: Implement `FormField` model and `DataLoader`, and refactor `FormFieldsConnectionResolver` to extend `AbstractConnectionResolver`. - feat!: Refactor `FormsConnectionResolver` and `EntriesConnectionResolver` for compatibility with WPGraphQL v1.26.0 improvements. - feat!: Narrow `FormField.choices` and `FormField.inputs` field types to their implementations. +- fix: Handle RadioField submission values when using a custom "other" choice. H/t @Gytjarek . - dev: Use `FormFieldsDataLoader` to resolve fields instead of instantiating a new `Model`. - chore: Add iterable type hints. - chore!: Bump minimum WPGraphQL version to v1.26.0. diff --git a/src/Data/EntryObjectMutation.php b/src/Data/EntryObjectMutation.php index 1d447dea..9a3d353a 100644 --- a/src/Data/EntryObjectMutation.php +++ b/src/Data/EntryObjectMutation.php @@ -71,6 +71,9 @@ public static function get_field_value_input( array $args, array $form, bool $is case 'calculation': $field_value_input = FieldValueInput\ProductValueInput::class; break; + case 'radio': + $field_value_input = FieldValueInput\RadioValueInput::class; + break; case 'date': case 'hidden': case 'number': @@ -79,7 +82,6 @@ public static function get_field_value_input( array $args, array $form, bool $is case 'post_excerpt': case 'post_title': case 'price': - case 'radio': case 'select': case 'text': case 'textarea': diff --git a/src/Data/FieldValueInput/RadioValueInput.php b/src/Data/FieldValueInput/RadioValueInput.php new file mode 100644 index 00000000..cf34d2c4 --- /dev/null +++ b/src/Data/FieldValueInput/RadioValueInput.php @@ -0,0 +1,85 @@ + + */ + public $value; + + /** + * {@inheritDoc} + */ + protected function get_field_name(): string { + return 'value'; + } + + /** + * {@inheritDoc} + * + * @return array + */ + protected function prepare_value() { + $value = $this->args; + + // Handle values with price. + if ( ! empty( $this->field->enablePrice ) && false === strpos( $value, '|' ) ) { + $value_key = ! empty( $this->field->enablePrice ) || ! empty( $this->field->enableChoiceValue ) ? 'value' : 'text'; + $choice_key = array_search( $value, array_column( $this->field->choices, $value_key ), true ); + $choice = $this->field->choices[ $choice_key ]; + $price = rgempty( 'price', $choice ) ? 0 : GFCommon::to_number( rgar( $choice, 'price' ) ); + $value = $value . '|' . $price; + } + + if ( empty( $this->field->enableOtherChoice ) ) { + return [ + $this->field->id => $value, + ]; + } + + $allowed_values = wp_list_pluck( $this->field->choices, 'value' ); + + if ( ! in_array( $value, $allowed_values, true ) ) { + $_POST[ $this->field->id . '_other' ] = $value; + $_POST[ $this->field->id ] = 'gf_other_choice'; + return [ + $this->field->id => 'gf_other_choice', + $this->field->id . '_other' => $value, + ]; + } + + return [ + $this->field->id => $value, + ]; + } + + /** + * {@inheritDoc} + */ + public function add_value_to_submission( array &$field_values ): void { + $field_values += $this->value; + } +} diff --git a/src/Model/FormField.php b/src/Model/FormField.php index 2a78cd45..9f6916ac 100644 --- a/src/Model/FormField.php +++ b/src/Model/FormField.php @@ -25,6 +25,7 @@ * @property int $databaseId The database ID of the field. * @property string $id The global Relay ID of the field. * @property array[] $inputs The inputs for the field. + * @property string $inputType The input type of the field. * @property \GF_Field $gfField The Gravity Forms field object. * @property int $layoutGridColumSpan The layout grid column span of the field. */ @@ -224,6 +225,7 @@ static function ( $choice ) use ( $data ) { ); }, 'databaseId' => static fn (): int => (int) $data->id, + 'gfField' => static fn (): GF_Field => $data, 'id' => static fn (): string => Relay::toGlobalId( FormFieldsLoader::$name, $data->formId . ':' . $data->id ), 'inputs' => static function () use ( $data ): ?array { // Emails fields are handled later. @@ -269,7 +271,7 @@ static function ( $input ) use ( $data ) { return $inputs; }, - 'gfField' => static fn (): GF_Field => $data, + 'inputType' => static fn (): string => $data->get_input_type(), 'layoutGridColumSpan' => static fn (): ?int => ! empty( $data->layoutGridColumnSpan ) ? (int) $data->layoutGridColumnSpan : null, ]; } diff --git a/src/Mutation/UpdateEntry.php b/src/Mutation/UpdateEntry.php index 1cc5a1b3..6e594bbc 100644 --- a/src/Mutation/UpdateEntry.php +++ b/src/Mutation/UpdateEntry.php @@ -290,6 +290,11 @@ public static function prepare_field_values_for_save( array $values, array $entr $field_id = strtok( (string) $id, '.' ); $field = GFUtils::get_field_by_id( $form, (int) $field_id ); + // Radio fields use the `_other` field for the other choice. + if ( 'radio' === $field->get_input_type() && 'gf_other_choice' === $value ) { + $value = $values[ $id . '_other' ]; + } + // Post images can sometimes already be prepared. if ( 'post_image' !== $field->type || is_array( $value ) ) { $value = GFFormsModel::prepare_value( $form, $field, $value, $input_name, $entry['id'], $entry ); diff --git a/src/Type/WPObject/FormField/FieldValue/FieldValues.php b/src/Type/WPObject/FormField/FieldValue/FieldValues.php index 2065df0c..736ffa99 100644 --- a/src/Type/WPObject/FormField/FieldValue/FieldValues.php +++ b/src/Type/WPObject/FormField/FieldValue/FieldValues.php @@ -40,6 +40,11 @@ public static function value(): array { return null; } + // If its a radio field with a "other" choice on a draft entry, the value is stored in a different field. + if ( 'radio' === $source->inputType && isset( $context->gfEntry->entry[ $source->databaseId ] ) && 'gf_other_choice' === $context->gfEntry->entry[ $source->databaseId ] && isset( $context->gfEntry->entry[ $source->databaseId . '_other' ] ) ) { + return $context->gfEntry->entry[ $source->databaseId . '_other' ]; + } + return $source->gfField->get_value_export( $context->gfEntry->entry, (string) $source->databaseId ) ?: null; }, ], diff --git a/tests/_support/Helper/Wpunit.php b/tests/_support/Helper/Wpunit.php index 4937a4c2..bd82781b 100644 --- a/tests/_support/Helper/Wpunit.php +++ b/tests/_support/Helper/Wpunit.php @@ -995,7 +995,6 @@ public function getRadioFieldArgs(): array { 'description', 'descriptionPlacement', 'enableChoiceValue', - 'enableOtherChoice', 'errorMessage', 'inputName', 'isRequired', diff --git a/tests/wpunit/RadioFieldOtherChoiceTest.php b/tests/wpunit/RadioFieldOtherChoiceTest.php new file mode 100644 index 00000000..537d540b --- /dev/null +++ b/tests/wpunit/RadioFieldOtherChoiceTest.php @@ -0,0 +1,283 @@ +runTestField(); + } + + /** + * Tests submitting the field values as a draft entry with submitGfForm. + */ + public function testSubmitDraft(): void { + $this->runTestSubmitDraft(); + } + + /** + * Tests submitting the field values as an entry with submitGfForm. + */ + public function testSubmitForm(): void { + $this->runtestSubmitForm(); + } + + /** + * Tests updating the field value with updateGfEntry. + */ + public function testUpdateEntry(): void { + $this->runtestUpdateEntry(); + } + + /** + * Tests updating the draft field value with updateGfEntry. + */ + public function testUpdateDraft(): void { + $this->runTestUpdateDraft(); + } + + /** + * Sets the correct Field Helper. + */ + public function field_helper() { + return $this->tester->getPropertyHelper( 'RadioField' ); + } + + /** + * Generates the form fields from factory. Must be wrappend in an array. + */ + public function generate_fields(): array { + return [ $this->factory->field->create( + array_merge( + $this->property_helper->values, + [ + 'enableOtherChoice' => true, + ] + ) + ) ]; + } + + /** + * The value as expected in GraphQL. + */ + public function field_value() { + return 'Some Value'; + } + + /** + * The value as expected in GraphQL when updating from field_value(). + */ + public function updated_field_value() { + return 'Some Other Value'; + } + + /** + * The value as expected by Gravity Forms. + */ + public function value() { + return [ $this->fields[0]['id'] => $this->field_value ]; + } + + /** + * The GraphQL query string. + */ + public function field_query(): string { + return '... on RadioField { + adminLabel + canPrepopulate + choices { + isOtherChoice + isSelected + text + value + } + conditionalLogic { + actionType + logicType + rules { + fieldId + operator + value + } + } + cssClass + description + descriptionPlacement + errorMessage + hasChoiceValue + hasOtherChoice + inputName + isRequired + label + labelPlacement + personalData { + isIdentificationField + shouldErase + shouldExport + } + shouldAllowDuplicates + value + } + '; + } + + /** + * SubmitForm mutation string. + */ + public function submit_form_mutation(): string { + return ' + mutation ($formId: ID!, $fieldId: Int!, $value: String!, $draft: Boolean) { + submitGfForm( input: { id: $formId, saveAsDraft: $draft, fieldValues: {id: $fieldId, value: $value}}) { + errors { + id + message + } + entry { + formFields { + nodes { + ... on RadioField { + value + } + } + } + ... on GfSubmittedEntry { + databaseId + } + ... on GfDraftEntry { + resumeToken + } + } + } + } + '; + } + + /** + * Returns the UpdateEntry mutation string. + */ + public function update_entry_mutation(): string { + return ' + mutation updateGfEntry( $entryId: ID!, $fieldId: Int!, $value: String! ){ + updateGfEntry( input: { id: $entryId, shouldValidate: true, fieldValues: {id: $fieldId, value: $value} }) { + errors { + id + message + } + entry { + formFields { + nodes { + ... on RadioField { + value + } + } + } + } + } + } + '; + } + + /** + * Returns the UpdateDraftEntry mutation string. + */ + public function update_draft_entry_mutation(): string { + return ' + mutation updateGfDraftEntry( $resumeToken: ID!, $fieldId: Int!, $value: String! ){ + updateGfDraftEntry( input: {id: $resumeToken, idType: RESUME_TOKEN, shouldValidate: true, fieldValues: {id: $fieldId, value: $value} }) { + errors { + id + message + } + entry: draftEntry { + formFields { + nodes { + ... on RadioField { + value + } + } + } + } + } + } + '; + } + + /** + * {@inheritDoc} + */ + public function expected_field_response( array $form ): array { + $expected = $this->getExpectedFormFieldValues( $form['fields'][0] ); + $expected[] = $this->expected_field_value( 'value', $this->field_value ); + + return [ + $this->expectedObject( + 'gfEntry', + [ + $this->expectedObject( + 'formFields', + [ + $this->expectedNode( + 'nodes', + $expected, + 0 + ), + ] + ), + ] + ), + ]; + } + + /** + * The expected WPGraphQL mutation response. + * + * @param string $mutationName . + * @param mixed $value . + */ + public function expected_mutation_response( string $mutationName, $value ): array { + return [ + $this->expectedObject( + $mutationName, + [ + $this->expectedObject( + 'entry', + [ + $this->expectedObject( + 'formFields', + [ + $this->expectedNode( + 'nodes', + [ + $this->expected_field_value( 'value', $value ), + ] + ), + ] + ), + ] + ), + ] + ), + ]; + } + + /** + * Checks if values submitted by GraphQL are the same as whats stored on the server. + * + * @param array $actual_entry . + * @param array $form . + */ + public function check_saved_values( $actual_entry, $form ): void { + $this->assertEquals( $this->field_value, $actual_entry[ $form['fields'][0]->id ], 'Submit mutation entry value not equal' ); + } +} diff --git a/vendor/composer/autoload_classmap.php b/vendor/composer/autoload_classmap.php index 6ccbced0..2ba69199 100644 --- a/vendor/composer/autoload_classmap.php +++ b/vendor/composer/autoload_classmap.php @@ -28,6 +28,7 @@ 'WPGraphQL\\GF\\Data\\FieldValueInput\\ListValuesInput' => $baseDir . '/src/Data/FieldValueInput/ListValuesInput.php', 'WPGraphQL\\GF\\Data\\FieldValueInput\\NameValuesInput' => $baseDir . '/src/Data/FieldValueInput/NameValuesInput.php', 'WPGraphQL\\GF\\Data\\FieldValueInput\\ProductValueInput' => $baseDir . '/src/Data/FieldValueInput/ProductValueInput.php', + 'WPGraphQL\\GF\\Data\\FieldValueInput\\RadioValueInput' => $baseDir . '/src/Data/FieldValueInput/RadioValueInput.php', 'WPGraphQL\\GF\\Data\\FieldValueInput\\ValueInput' => $baseDir . '/src/Data/FieldValueInput/ValueInput.php', 'WPGraphQL\\GF\\Data\\FieldValueInput\\ValuesInput' => $baseDir . '/src/Data/FieldValueInput/ValuesInput.php', 'WPGraphQL\\GF\\Data\\Loader\\DraftEntriesLoader' => $baseDir . '/src/Data/Loader/DraftEntriesLoader.php', diff --git a/vendor/composer/autoload_static.php b/vendor/composer/autoload_static.php index 778e7840..a6f4ae0b 100644 --- a/vendor/composer/autoload_static.php +++ b/vendor/composer/autoload_static.php @@ -47,6 +47,7 @@ class ComposerStaticInit15520cc730459b54805c9b10144520df 'WPGraphQL\\GF\\Data\\FieldValueInput\\ListValuesInput' => __DIR__ . '/../..' . '/src/Data/FieldValueInput/ListValuesInput.php', 'WPGraphQL\\GF\\Data\\FieldValueInput\\NameValuesInput' => __DIR__ . '/../..' . '/src/Data/FieldValueInput/NameValuesInput.php', 'WPGraphQL\\GF\\Data\\FieldValueInput\\ProductValueInput' => __DIR__ . '/../..' . '/src/Data/FieldValueInput/ProductValueInput.php', + 'WPGraphQL\\GF\\Data\\FieldValueInput\\RadioValueInput' => __DIR__ . '/../..' . '/src/Data/FieldValueInput/RadioValueInput.php', 'WPGraphQL\\GF\\Data\\FieldValueInput\\ValueInput' => __DIR__ . '/../..' . '/src/Data/FieldValueInput/ValueInput.php', 'WPGraphQL\\GF\\Data\\FieldValueInput\\ValuesInput' => __DIR__ . '/../..' . '/src/Data/FieldValueInput/ValuesInput.php', 'WPGraphQL\\GF\\Data\\Loader\\DraftEntriesLoader' => __DIR__ . '/../..' . '/src/Data/Loader/DraftEntriesLoader.php', diff --git a/vendor/composer/installed.php b/vendor/composer/installed.php index 300f5de2..cb656a99 100644 --- a/vendor/composer/installed.php +++ b/vendor/composer/installed.php @@ -3,7 +3,7 @@ 'name' => 'harness-software/wp-graphql-gravity-forms', 'pretty_version' => 'dev-develop', 'version' => 'dev-develop', - 'reference' => '5cff081a7cf4dd60ac43574c2167720f0317e0f7', + 'reference' => 'e68b7c0b1d9aaf702384eae00af7d3c94e6c0f29', 'type' => 'wordpress-plugin', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), @@ -13,7 +13,7 @@ 'harness-software/wp-graphql-gravity-forms' => array( 'pretty_version' => 'dev-develop', 'version' => 'dev-develop', - 'reference' => '5cff081a7cf4dd60ac43574c2167720f0317e0f7', + 'reference' => 'e68b7c0b1d9aaf702384eae00af7d3c94e6c0f29', 'type' => 'wordpress-plugin', 'install_path' => __DIR__ . '/../../', 'aliases' => array(),