diff --git a/CHANGELOG.md b/CHANGELOG.md index f0b3cb50..2a0f3c57 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ Lines marked **Upgrade step** need you to act after updating. - The form list loads faster when a team has many forms. It now needs the same few database queries for one form or a hundred. Before, every form added 3 queries. (#256) - On screens narrower than 1280 px, a form's editor bar now sits in two rows under the app header: the form with Dashboard and Preview, then Editor, Settings, Integrations and Submissions as tabs. Nothing in it scrolls sideways any more, and the header keeps the logo and your team name. On every screen, keyboard focus in the editor bar and the account menu now shows a blue ring. (#257) - A form's pages have one top bar instead of two: the form's name with its menu, a Draft or Live label, the pages, Preview, Publish and your account. Publish first checks for questions without text, rules that point to a deleted question and a missing privacy link. Once the form is live, Share has its link, Open form and Unpublish. On a phone the pages sit in a tab bar at the bottom. (#265) +- A logic rule with a missing field now tells you in plain words what to fill in, like "Pick a question for condition 1." (#278) ### Fixed diff --git a/app/Http/Controllers/Api/FormSubmitController.php b/app/Http/Controllers/Api/FormSubmitController.php index b403e3a2..584364e5 100644 --- a/app/Http/Controllers/Api/FormSubmitController.php +++ b/app/Http/Controllers/Api/FormSubmitController.php @@ -24,11 +24,11 @@ class FormSubmitController extends Controller For form blocks that have only one input field, you submit one payload-object with the keys "payload" and "actionId". The payload is the user input and the actionId is the ID of the form block interaction field. - For multiple choice questions, the payload is an array of payload-objects. + For checkbox and consent questions, the payload is an array of payload-objects, one for each ticked choice or policy. A single choice question sends one payload-object for the picked choice. DESC, example: [ '7j' => ['payload' => 'Secret', 'actionId' => '59R'], '4x1' => ['payload' => 'mail@example.com', 'actionId' => 'pQ6'], - // For multiple choice questions, the payload is an array of the selected options + // For checkbox questions, the payload is an array of the ticked choices '5yB' => [ ['payload' => 'Pineapple', 'actionId' => 'qYr'], ['payload' => 'Cherry', 'actionId' => 'vor'], diff --git a/app/Http/Requests/FormBlockLogicRequest.php b/app/Http/Requests/FormBlockLogicRequest.php index 59077a18..3f13a23c 100644 --- a/app/Http/Requests/FormBlockLogicRequest.php +++ b/app/Http/Requests/FormBlockLogicRequest.php @@ -45,7 +45,7 @@ public function rules(): array return [ 'name' => 'required|string|max:255', 'conditions' => 'required|array', - 'conditions.*.source' => ['required', 'string', $this->inThisForm('The question of your #:position condition is not in this form.')], + 'conditions.*.source' => ['required', 'string', $this->inThisForm('Pick a question from this form for condition :position.')], 'conditions.*.operator' => 'required|string|in:equals,equalsNot,contains,containsNot,isLowerThan,isGreaterThan', 'conditions.*.value' => 'required|string', 'conditions.*.chainOperator' => 'required|string|in:and,or', @@ -79,14 +79,14 @@ public function after(): array public function messages() { return [ - 'name.required' => 'The name is required.', - 'conditions.required' => 'At least one condition is required.', - 'conditions.*.source.required' => 'The source block for your #:position condition is required.', - 'conditions.*.value.required' => 'The value for your #:position condition is required.', - 'conditions.*.operator.required' => 'The operator for the #:position condition is required.', - 'conditions.*.chainOperator.required' => 'The chain operator for the #:position condition is required.', - 'action.required' => 'The action is required.', - 'evaluate.required' => 'The evaluate is required.', + 'name.required' => 'Give the rule a name.', + 'conditions.required' => 'Add at least one condition.', + 'conditions.*.source.required' => 'Pick a question for condition :position.', + 'conditions.*.value.required' => 'Add an answer for condition :position.', + 'conditions.*.operator.required' => 'Pick how to compare in condition :position.', + 'conditions.*.chainOperator.required' => 'Pick "and" or "or" for condition :position.', + 'action.required' => 'Pick what the rule does: hide, show or go to.', + 'evaluate.required' => 'Pick when the rule runs: before or after the question.', ]; } } diff --git a/tests/Feature/FormBlockLogicTest.php b/tests/Feature/FormBlockLogicTest.php index e436a2d5..606d0590 100644 --- a/tests/Feature/FormBlockLogicTest.php +++ b/tests/Feature/FormBlockLogicTest.php @@ -111,6 +111,26 @@ expect(FormBlockLogic::pluck('name', 'id')->all())->toBe([$logic->id => 'Old']); })->with(['xlf', '.0', 'e0']); +test('a rule with empty fields gets errors in the editor\'s words', function () { + $block = FormBlock::factory()->create(); + + $this->actingAs($block->form->user) + ->json('post', route('api.logics.create', $block->id), ['conditions' => [[]]]) + ->assertStatus(422) + ->assertJsonValidationErrors([ + 'name' => 'Give the rule a name.', + 'conditions.0.source' => 'Pick a question for condition 1.', + 'conditions.0.value' => 'Add an answer for condition 1.', + 'conditions.0.operator' => 'Pick how to compare in condition 1.', + 'conditions.0.chainOperator' => 'Pick "and" or "or" for condition 1.', + 'action' => 'Pick what the rule does: hide, show or go to.', + 'evaluate' => 'Pick when the rule runs: before or after the question.', + ]); + + $this->json('post', route('api.logics.create', $block->id), []) + ->assertJsonValidationErrors(['conditions' => 'Add at least one condition.']); +}); + test('a rule that points to a deleted question or one of another form is refused and changes nothing', function (string $method, string $field) { $block = FormBlock::factory()->create(); $question = FormBlock::factory()->for($block->form)->create(); @@ -135,7 +155,7 @@ $this->json('post', $route, $payload) ->assertStatus(422) - ->assertJsonValidationErrors([$field => 'is not in this form']) + ->assertJsonValidationErrors([$field => $field === 'action_payload' ? 'The question to go to is not in this form.' : 'Pick a question from this form for condition 1.']) ->assertJsonMissingValidationErrors($field === 'action_payload' ? 'conditions.0.source' : 'action_payload'); }