Skip to content
Open
14 changes: 14 additions & 0 deletions app/controllers/kits_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,20 @@ def new
@kit.line_items.build
end

def validate
@kit = current_organization.kits.new(kit_params)
@kit.line_items.combine!
@kit.valid?
@kit.errors.add(:base, "At least one item is required") if @kit.line_items.empty?

if @kit.errors.none?
body = render_to_string(template: "kits/validate", formats: [:html], layout: false)
render json: {valid: true, body: body}
else
render json: {valid: false}
end
end

def create
kit_creation = KitCreateService.new(organization_id: current_organization.id, kit_params: kit_params)
kit_creation.call
Expand Down
12 changes: 9 additions & 3 deletions app/javascript/controllers/confirmation_controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ import { Controller } from "@hotwired/stimulus"

* 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.
*
* The button that opened the modal is remembered and passed back into requestSubmit, so that any other
* Stimulus controller composed onto the same form (e.g. duplicate-items) still sees it as `event.submitter`
* when the form is eventually (re)submitted.
*/
export default class extends Controller {
static targets = [
Expand All @@ -29,6 +33,8 @@ export default class extends Controller {
openModal(event) {
event.preventDefault();

this.submitter = event.currentTarget;

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

Expand All @@ -49,15 +55,15 @@ export default class extends Controller {
this.modalTarget.innerHTML = data.body;
$(this.modalTarget).modal("show");
} else {
this.formTarget.requestSubmit();
this.formTarget.requestSubmit(this.submitter);
}
})
.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();
this.formTarget.requestSubmit(this.submitter);
});
}

Expand Down Expand Up @@ -106,6 +112,6 @@ export default class extends Controller {
$(this.modalTarget).find('#modalYes').prop('disabled', true);
$(this.modalTarget).find('#modalNo').prop('disabled', true);
$(this.modalTarget).modal("hide");
this.formTarget.requestSubmit();
this.formTarget.requestSubmit(this.submitter);
}
}
22 changes: 11 additions & 11 deletions app/javascript/controllers/duplicate_items_controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ export default class extends Controller {
connect() {
this.boundHandleSubmit = this.handleSubmit.bind(this)
this.element.addEventListener("submit", this.boundHandleSubmit)

// Disable Rails UJS for this form to prevent "Saving" state
this.element.removeAttribute('data-remote')

// Remove data-disable-with from all submit buttons
const buttons = this.element.querySelectorAll('input[type="submit"], button[type="submit"]')
buttons.forEach(button => {
Expand All @@ -23,9 +23,9 @@ export default class extends Controller {
if (!this.itemSubmitButtonTargets.includes(submitter)) return

event.preventDefault()

const duplicates = this.findDuplicates()

if (duplicates.length > 0) {
this.showModal(duplicates, submitter.name)
} else {
Expand Down Expand Up @@ -100,10 +100,10 @@ export default class extends Controller {

document.getElementById('duplicateItemsModal')?.remove()
document.body.insertAdjacentHTML('beforeend', modalHtml)

const modal = new bootstrap.Modal(document.getElementById('duplicateItemsModal'))
modal.show()

document.getElementById('confirmMerge').addEventListener('click', () => {
this.mergeAndSubmit(duplicates, buttonName)
})
Expand All @@ -115,29 +115,29 @@ export default class extends Controller {

// Separate the first entry from remaining entries
const [firstEntry, ...remainingEntries] = item.entries

// Update the first entry with the merged total
firstEntry.section.querySelector('input[name*="[quantity]"]').value = total

// Remove all duplicate entries from the form submission
remainingEntries.forEach(entry => entry.section.remove())
})

const modal = new bootstrap.Modal(document.getElementById('duplicateItemsModal'))
modal.hide()

this.submitForm(buttonName)
}

submitForm(buttonName) {
this.element.removeEventListener('submit', this.boundHandleSubmit)

const input = document.createElement('input')
input.type = 'hidden'
input.name = buttonName
input.value = '1'
this.element.appendChild(input)

this.element.submit()
}
}
18 changes: 16 additions & 2 deletions app/views/kits/_form.html.erb
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<%= simple_form_for @kit, remote: request.xhr?, data: { controller: "form-input duplicate-items" }, html: { class: 'form-horizontal' } do |f| %>
<%= simple_form_for @kit, remote: request.xhr?, data: { controller: "form-input duplicate-items", confirmation_target: "form" }, html: { class: 'form-horizontal' } do |f| %>
<section class="content">
<div class="container-fluid">
<div class="row">
Expand Down Expand Up @@ -34,11 +34,25 @@
</div>
<div class="card-footer">
<p><b>NB: You will not be able to change the composition of the kit once saved. Partner visibility and name can be changed via the kit's item.</b></p>
<%= submit_button({}, {"duplicate-items-target": "itemSubmitButton"}) %>
<%= submit_button({}, {
"duplicate-items-target": "itemSubmitButton",
action: "click->confirmation#openModal"
}) %>
</div>
</div>
</div>
</div>
</div>
</section>
<% end %>

<%# Confirmation modal: See confirmation_controller.js for how this gets displayed %>
<%# and app/controllers/kits_controller.rb#validate for how it gets populated. %>
<div id="kitConfirmationModal"
class="modal confirm"
aria-labelledby="kitConfirmationModal"
aria-hidden="true"
tabindex="-1"
data-bs-backdrop="static"
data-confirmation-target="modal">
</div>
4 changes: 3 additions & 1 deletion app/views/kits/new.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,7 @@
</div>
</section>

<%= render 'form' %>
<div data-controller="confirmation" data-confirmation-pre-check-path-value="<%= validate_kits_path(format: :json) %>">
<%= render 'form' %>
</div>
<%= render partial: "barcode_items/barcode_modal" %>
37 changes: 37 additions & 0 deletions app/views/kits/validate.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Kit Creation Confirmation</h5>
<button id="modalClose" 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 kit named
<span class="fw-bolder fst-italic" data-testid="kit-confirmation-name"><%= @kit.name %></span>
with value
<span class="fw-bolder fst-italic" data-testid="kit-confirmation-value">$<%= format("%.2f", @kit.value_in_dollars) %></span>
</p>
<table class="table">
<thead>
<tr>
<th>Item Name</th>
<th>Quantity</th>
</tr>
</thead>
<tbody>
<% @kit.line_items.each do |line_item| %>
<tr>
<td><%= line_item.name %></td>
<td><%= line_item.quantity %></td>
</tr>
<% end %>
</tbody>
</table>
<p>Please confirm that this is the correct composition of the kit. Note: You will
<span class="text-danger fw-bold">not</span> be able to edit the items contained in the kit.</p>
</div>
<div class="modal-footer">
<button id="modalNo" 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 id="modalYes" type="button" class="btn btn-success" data-action="confirmation#submitForm">Yes, it's correct</button>
</div>
</div>
</div>
3 changes: 3 additions & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,9 @@ def set_up_flipper
end

resources :kits do
collection do
post :validate
end
member do
get :allocations
post :allocate
Expand Down
58 changes: 58 additions & 0 deletions spec/requests/kit_requests_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,64 @@
end
end

describe "POST #validate" do
let(:item) { create(:item, organization: organization) }

it "returns valid and the confirmation modal body for a valid kit" do
post validate_kits_url(format: :json), params: {
kit: {
name: "A new kit",
value_in_dollars: "10.10",
line_items_attributes: {"0": {item_id: item.id, quantity: 5}}
}
}

expect(response).to be_successful
json = JSON.parse(response.body)
expect(json["valid"]).to eq(true)
expect(json["body"]).to include("A new kit")
expect(json["body"]).to include("$10.10")
expect(json["body"]).to include(item.name)
end

it "returns invalid when the name is blank" do
post validate_kits_url(format: :json), params: {
kit: {
name: "",
value_in_dollars: "10.10",
line_items_attributes: {"0": {item_id: item.id, quantity: 5}}
}
}

expect(response).to be_successful
json = JSON.parse(response.body)
expect(json["valid"]).to eq(false)
expect(json["body"]).to be_nil
end

it "returns invalid when there are no line items" do
post validate_kits_url(format: :json), params: {
kit: {name: "A new kit", value_in_dollars: "10.10"}
}

expect(response).to be_successful
json = JSON.parse(response.body)
expect(json["valid"]).to eq(false)
end

it "does not persist the kit" do
expect {
post validate_kits_url(format: :json), params: {
kit: {
name: "A new kit",
value_in_dollars: "10.10",
line_items_attributes: {"0": {item_id: item.id, quantity: 5}}
}
}
}.not_to change(Kit, :count)
end
end

describe "GET #index" do
before do
# this shouldn't be shown
Expand Down
50 changes: 46 additions & 4 deletions spec/system/kit_system_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,48 @@

click_button "Save"

expect(page).to have_css("#kitConfirmationModal", visible: true)
within "#kitConfirmationModal" do
expect(page).to have_content("Kit Creation Confirmation")
expect(find(:element, "data-testid": "kit-confirmation-name")).to have_text(kit_traits[:name])
expect(find(:element, "data-testid": "kit-confirmation-value")).to have_text("$10.10")
expect(page).to have_content(item.name)
expect(page).to have_content(quantity_per_kit.to_s)
expect(page).to have_css(".text-danger", text: "not")
click_button "Yes, it's correct"
end

expect(page.find(".alert")).to have_content "Kit created successfully"
expect(page).to have_content(kit_traits[:name])
expect(page).to have_content("#{quantity_per_kit} #{item.name}")
end

it "returns to the form with values intact when declining the confirmation" do
visit new_kit_path
kit_traits = attributes_for(:kit)

fill_in "Name", with: kit_traits[:name]
find(:css, '#kit_value_in_dollars').set('10.10')

item = Item.last
quantity_per_kit = 5
select item.name, from: "kit_line_items_attributes_0_item_id"
find(:css, '#kit_line_items_attributes_0_quantity').set(quantity_per_kit)

click_button "Save"

expect(page).to have_css("#kitConfirmationModal", visible: true)
within "#kitConfirmationModal" do
click_button "No, I need to make changes"
end

expect(page).to have_no_css("#kitConfirmationModal", visible: true)
expect(page).to have_current_path(new_kit_path)
expect(page).to have_no_content("Kit created successfully")
expect(page).to have_field("Name", with: kit_traits[:name])
expect(page).to have_field("kit_line_items_attributes_0_quantity", with: quantity_per_kit.to_s)
end

it "can add items correctly" do
visit new_kit_path
new_barcode = "1234567890"
Expand Down Expand Up @@ -210,7 +247,6 @@
describe "when missing required fields" do
it "displays error indicating missing field and preserves filled out fields" do
visit new_kit_path
kit_traits = attributes_for(:kit)

find(:css, '#kit_value_in_dollars').set('10.10')

Expand All @@ -221,8 +257,9 @@

click_button "Save"

expect(page).to have_no_css("#kitConfirmationModal", visible: true)
expect(page.find(".alert")).to have_content "Name can't be blank"
expect(page).to have_content(kit_traits[:quantity])
expect(page).to have_field("kit_line_items_attributes_0_quantity", with: quantity_per_kit.to_s)
expect(page).to have_content(item.name)
end
end
Expand Down Expand Up @@ -253,10 +290,15 @@
fill_in quantity_input[:id], with: "15"
end

# Try to save - should trigger duplicate detection modal
# Try to save - the confirmation preview appears first
click_button "Save"

# JavaScript modal should appear
expect(page).to have_css("#kitConfirmationModal", visible: true)
within "#kitConfirmationModal" do
click_button "Yes, it's correct"
end

# Confirming then triggers duplicate detection
expect(page).to have_css("#duplicateItemsModal", visible: true)
expect(page).to have_content("Multiple Item Entries Detected")
expect(page).to have_content("Merge Items")
Expand Down
Loading