Skip to content

Commit

Permalink
3090 v2 distribution confirmation (#4367)
Browse files Browse the repository at this point in the history
* [#3090] Use a Stimulus controller to intercept distribution form submit
and render a bootstrap modal displaying partner name, storage location,
and list of items and quantities. The No button simply closes the modal
so user can return to the new distribution form to make further edits.
The Yes button submits the form and the existing workflow continues.

* [#3090] Maintain distribution system tests wrt confirmation modal

* [#3090] Verify items and quantities in distribution confirmation modal

* [#3090] Fix request system test wrt distribution confirmation modal

* [#3090] Fix distribution system tests wrt instance var and new path

* [#3090] PR feedback: update wording on confirmation modal

* [#3090] Integrate validation into confirmation:
1. Add validation only endpoint for distribution
2. Update confirmation JS controller to check if form is valid
(using the new validation endpoint) before displaying modal.

* [#3090] PR feedback:
Combine same named items & quantities for display in confirmation modal

* [#3090] PR feedback:
* Move confirm style into custom
* Remove call to debug form data
* Remove console logs for valid and invalid

* [#3090] PR feedback:
* Move modal content generation to server side rendering
* Remove unneeded dom targets from stimulus controller

* [#3090] PR feedback: use specific partner and storage names in system test

* [#3090] PR feedback: rename to confirmation controller

* [#3090] PR feedback: use meta level authenticity token

* [#3090] PR feedback: Quotes for consistency in fetch request headers
  • Loading branch information
danielabar authored Jun 23, 2024
1 parent d18ecc4 commit b172f33
Show file tree
Hide file tree
Showing 10 changed files with 404 additions and 27 deletions.
1 change: 0 additions & 1 deletion app/assets/stylesheets/application.scss
Original file line number Diff line number Diff line change
Expand Up @@ -193,4 +193,3 @@ div.warning {
margin: 5px;
text-align: center;
}

6 changes: 6 additions & 0 deletions app/assets/stylesheets/custom.scss
Original file line number Diff line number Diff line change
Expand Up @@ -92,3 +92,9 @@
.percent {
text-align: right;
}

.confirm {
.message {
margin-top: 40px;
}
}
16 changes: 16 additions & 0 deletions app/controllers/distributions_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,22 @@ def index
end
end

# This endpoint is in support of displaying a confirmation modal before a distribution is created.
# Since the modal should only be shown for a valid distribution, client side JS will invoke this
# endpoint, and if the distribution is valid, this endpoint also returns the HTML for the modal content.
# Important: The distribution model is intentionally NOT saved to the database at this point because
# the user has not yet confirmed that they want to create it.
def validate
@dist = Distribution.new(distribution_params.merge(organization: current_organization))
@dist.line_items.combine!
if @dist.valid?
body = render_to_string(template: 'distributions/validate', formats: [:html], layout: false)
render json: {valid: true, body: body}
else
render json: {valid: false}
end
end

def create
dist = Distribution.new(distribution_params.merge(organization: current_organization))
result = DistributionCreateService.new(dist, request_id).call
Expand Down
108 changes: 108 additions & 0 deletions app/javascript/controllers/confirmation_controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { Controller } from "@hotwired/stimulus"

/**
* Connects to data-controller="confirmation"
* Displays a confirmation modal with the details of the form that user just submitted.
* Launched when the user clicks Save from the form.
* First runs a "pre-check" on the form data to a validation endpoint,
* which is specified in the controller's `preCheckPathValue` property.
* If the pre-check passes, it shows the modal. Because the confirmation modal should only be shown
* when the form data can pass initial validation.
* If the pre-check fails, it submits the form to the server for full validation and render with the errors.
*
* The pre-check validation endpoint also returns the html body to display in the modal if validation passes.
* If the user clicks the "Yes..." button from the modal, it submits the form.
* If the user clicks the "No..." button from the modal, it closes and user remains on the same url.
*/
export default class extends Controller {
static targets = [
"modal",
"form"
]

static values = {
preCheckPath: String
}

openModal(event) {
event.preventDefault();

const formData = new FormData(this.formTarget);
const formObject = this.buildNestedObject(formData);

fetch(this.preCheckPathValue, {
method: "POST",
headers: {
"X-CSRF-Token": this.getMetaToken(),
"X-Requested-With": "XMLHttpRequest",
"Content-Type": "application/json",
"Accept": "application/json"
},
body: JSON.stringify(formObject),
credentials: "same-origin"
})
.then((response) => response.json())
.then((data) => {
if (data.valid) {
this.modalTarget.innerHTML = data.body;
$(this.modalTarget).modal("show");
} else {
this.formTarget.requestSubmit();
}
})
.catch((error) => {
// Something went wrong in communication to server validation endpoint
// In this case, just submit the form as if the user had clicked Save.
// NICE TO HAVE: Send to bugsnag but need to install/configure https://www.npmjs.com/package/@bugsnag/js
console.log(`=== ConfirmationController ERROR ${error}`);
this.formTarget.requestSubmit();
});
}

getMetaToken() {
const metaTokenElement = document.querySelector("meta[name='csrf-token']");
return metaTokenElement
? metaTokenElement.content
: "default_test_csrf_token";
}

// Prepare the form data for submission as expected by Rails, excluding
// the form level authenticity token because that is specific to creation.
// This controller needs to submit a validation only request.
buildNestedObject(formData) {
let formObject = {};
for (let [key, value] of formData.entries()) {
if (key === "authenticity_token") {
continue;
}

const keys = key.split(/[\[\]]+/).filter((k) => k);
keys.reduce((obj, k, i) => {
if (i === keys.length - 1) {
obj[k] = value;
} else {
obj[k] = obj[k] || {};
}
return obj[k];
}, formObject);
}

return formObject;
}

debugFormData() {
const formData = new FormData(this.formTarget);
let formDataString = "=== ConfirmationController FormData:\n";
for (const [key, value] of formData.entries()) {
formDataString += `${key}: ${value}\n`;
}
console.log(formDataString);
}

submitForm() {
$(this.modalTarget).modal("hide");
this.formTarget.requestSubmit();
}
}
20 changes: 17 additions & 3 deletions app/views/distributions/_form.html.erb
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
<%= simple_form_for distribution, data: { controller: "form-input" }, html: {class: "storage-location-required"}, wrapper_mappings: { datetime: :custom_multi_select } do |f| %>
<%= simple_form_for distribution,
data: { controller: "form-input", confirmation_target: "form" },
html: { class: "storage-location-required" },
wrapper_mappings: { datetime: :custom_multi_select } do |f| %>

<div class="box-body">
<%= f.simple_fields_for :request do |r| %>
Expand All @@ -9,6 +12,7 @@
collection: current_organization.partners.alphabetized,
label: "Partner",
error: "Which partner is this distribution going to?" %>

<div class='w-72'>
<%= f.input :issued_at, as: :datetime, ampm: true, minute_step: 15, label: "Distribution date and time", html5: true, :input_html => { :value => date_place_holder&.strftime("%Y-%m-%dT%0k:%M")} %>
</div>
Expand Down Expand Up @@ -42,7 +46,17 @@
</fieldset>
</div>
<div class="card-footer">
<%= submit_button %>
</div>
<%= submit_button({}, { action: "click->confirmation#openModal" }) %>
</div>
<% end %>
<%# Confirmation modal: See confirmation_controller.js for how this gets displayed %>
<%# and app/controllers/distributions_controller.rb#validate for how it gets populated. %>
<div id="distributionConfirmationModal"
class="modal confirm"
aria-labelledby="distributionConfirmationModal"
aria-hidden="true"
tabindex="-1"
data-bs-backdrop="static"
data-confirmation-target="modal">
</div>
4 changes: 3 additions & 1 deletion app/views/distributions/new.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,9 @@
<!-- form start -->
<div class="card-body">
<!-- Default box -->
<div class="box">
<div class="box"
data-controller="confirmation"
data-confirmation-pre-check-path-value="<%= validate_distributions_path(format: :json) %>">
<%= render 'form', distribution: @distribution, date_place_holder: Time.zone.now.end_of_day %>
</div><!-- /.box -->
</div>
Expand Down
42 changes: 42 additions & 0 deletions app/views/distributions/validate.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Distribution Confirmation</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<p class="lead">You are about to create a distribution for
<span class="fw-bolder fst-italic"
data-testid="distribution-confirmation-partner"><%= @dist.partner_name %></span>
from
<span class="fw-bolder fst-italic"
data-testid="distribution-confirmation-storage"><%= @dist.storage_location.name %></span>
</p>

<table class="table">
<thead>
<tr>
<th>Item Name</th>
<th>Quantity</th>
</tr>
</thead>
<tbody>
<% @dist.line_items.each do |line_item| %>
<tr>
<td><%= line_item.name %></td>
<td><%= line_item.quantity %></td>
</tr>
<% end %>
</tbody>
</table>

<div class="message fs-5">
<p>Please confirm that the above list is what you want to distribute.</p>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal" aria-label="No I need to make changes">No, I need to make changes</button>
<button type="button" class="btn btn-primary" data-action="confirmation#submitForm">Yes, it's correct</button>
</div>
</div>
</div>
1 change: 1 addition & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ def set_up_flipper
resources :distributions do
get :print, on: :member
collection do
post :validate
get :calendar
get :schedule
get :pickup_day
Expand Down
Loading

0 comments on commit b172f33

Please sign in to comment.