diff --git a/app/assets/javascripts/admin/order_cycles/controllers/order_cycle_exchanges_controller.js.coffee b/app/assets/javascripts/admin/order_cycles/controllers/order_cycle_exchanges_controller.js.coffee index ee7c1aa5cf..db2cb94213 100644 --- a/app/assets/javascripts/admin/order_cycles/controllers/order_cycle_exchanges_controller.js.coffee +++ b/app/assets/javascripts/admin/order_cycles/controllers/order_cycle_exchanges_controller.js.coffee @@ -35,7 +35,11 @@ angular.module('admin.orderCycles') OrderCycle.removeExchangeFee(exchange, index) $scope.order_cycle_form.$dirty = true - $scope.setPickupTimeFieldDirty = (index) -> + $scope.setPickupTimeFieldDirty = (index, pickup_time) -> + # if the pickup_time is already set we are in edit mode, so no need to set pickup_time field as dirty + # to show it is required (it has a red border when set to dirty) + return if pickup_time + $timeout -> pickup_time_field_name = "order_cycle_outgoing_exchange_" + index + "_pickup_time" $scope.order_cycle_form[pickup_time_field_name].$setDirty() diff --git a/app/views/admin/order_cycles/_exchange_form.html.haml b/app/views/admin/order_cycles/_exchange_form.html.haml index e5aa662c60..d92099cd8c 100644 --- a/app/views/admin/order_cycles/_exchange_form.html.haml +++ b/app/views/admin/order_cycles/_exchange_form.html.haml @@ -10,7 +10,7 @@ %td.tags.panel-toggle.text-center{ name: "tags", ng: { if: 'enterprises[exchange.enterprise_id].managed || order_cycle.viewing_as_coordinator' } } {{ exchange.tags.length }} %td.collection-details - = text_field_tag 'order_cycle_outgoing_exchange_{{ $index }}_pickup_time', '', 'ng-init' => 'setPickupTimeFieldDirty($index)', 'id' => 'order_cycle_outgoing_exchange_{{ $index }}_pickup_time', 'required' => 'required', 'placeholder' => t('.pickup_time_placeholder'), 'ng-model' => 'exchange.pickup_time', 'ng-disabled' => '!enterprises[exchange.enterprise_id].managed && !order_cycle.viewing_as_coordinator', 'maxlength' => 35 + = text_field_tag 'order_cycle_outgoing_exchange_{{ $index }}_pickup_time', '', 'ng-init' => 'setPickupTimeFieldDirty($index, exchange.pickup_time)', 'id' => 'order_cycle_outgoing_exchange_{{ $index }}_pickup_time', 'required' => 'required', 'placeholder' => t('.pickup_time_placeholder'), 'ng-model' => 'exchange.pickup_time', 'ng-disabled' => '!enterprises[exchange.enterprise_id].managed && !order_cycle.viewing_as_coordinator', 'maxlength' => 35 %span.icon-question-sign{'ofn-with-tip' => t('.pickup_time_tip')} %br/ = text_field_tag 'order_cycle_outgoing_exchange_{{ $index }}_pickup_instructions', '', 'id' => 'order_cycle_outgoing_exchange_{{ $index }}_pickup_instructions', 'placeholder' => t('.pickup_instructions_placeholder'), 'ng-model' => 'exchange.pickup_instructions', 'ng-disabled' => '!enterprises[exchange.enterprise_id].managed && !order_cycle.viewing_as_coordinator' diff --git a/app/views/admin/order_cycles/checkout_options.html.haml b/app/views/admin/order_cycles/checkout_options.html.haml index 42c3213301..80d58be8c6 100644 --- a/app/views/admin/order_cycles/checkout_options.html.haml +++ b/app/views/admin/order_cycles/checkout_options.html.haml @@ -3,8 +3,7 @@ - content_for :page_title do = t :edit_order_cycle -= form_for [main_app, :admin, @order_cycle], html: { class: "order_cycle" } do |f| - += form_for [main_app, :admin, @order_cycle], html: { class: "order_cycle" , data: { controller: 'unsaved-changes', action: 'beforeunload@window->unsaved-changes#leavingPage', 'unsaved-changes-changed': "false" } } do |f| = render 'wizard_progress' %fieldset.no-border-bottom diff --git a/app/webpacker/controllers/unsaved_changes_controller.js b/app/webpacker/controllers/unsaved_changes_controller.js new file mode 100644 index 0000000000..afdb7ea8eb --- /dev/null +++ b/app/webpacker/controllers/unsaved_changes_controller.js @@ -0,0 +1,122 @@ +import { Controller } from "stimulus"; + +// UnsavedChanges allows you to promp the user about unsaved changes when trying to leave the page +// +// Usage : +// - with beforeunload event : +//
+// +// - with turbolinks : +// +// +// You can also combine the two event trigger ie : +// + ` + }) + + describe("#connect", () => { + describe("when disable-submit-button is true", () => { + beforeEach(() => { + document.body.innerHTML = ` + + ` + }) + + it("disables any submit button", () => { + const submit = document.getElementById("test-submit") + + expect(submit.disabled).toBe(true) + }) + }) + + describe("when disable-submit-button is false", () => { + beforeEach(() => { + document.body.innerHTML = ` + + ` + }) + + it("doesn't disable any submit button", () => { + const submit = document.getElementById("test-submit") + + expect(submit.disabled).toBe(false) + }) + }) + + describe("when disable-submit-button is not set", () => { + it("doesn't disable any submit button", () => { + const submit = document.getElementById("test-submit") + + expect(submit.disabled).toBe(false) + }) + }) + }) + + describe("#formIsChanged", () => { + let checkbox + let submit + + beforeEach(() => { + checkbox = document.getElementById("test-checkbox") + submit = document.getElementById("test-submit") + }) + + it("changed is set to true", () => { + const form = document.getElementById("test-form") + + checkbox.click() + + expect(form.dataset.unsavedChangesChanged).toBe("true") + }) + + describe("when disable-submit-button is true", () => { + it("enables any submit button", () => { + checkbox.click() + + expect(submit.disabled).toBe(false) + }) + }) + + describe("when disable-submit-button is false", () => { + it("does nothing", () => { + expect(submit.disabled).toBe(false) + + checkbox.click() + + expect(submit.disabled).toBe(false) + }) + }) + }) + + describe('#leavingPage', () => { + let checkbox + + beforeEach(() => { + // Add a mock I18n object to + const mockedT = jest.fn() + mockedT.mockImplementation((string) => (string)) + + global.I18n = { + t: mockedT + } + + checkbox = document.getElementById("test-checkbox") + }) + + afterEach(() => { + delete global.I18n + }) + + describe('when triggering a beforeunload event', () => { + it("triggers leave page pop up when leaving page and form has been interacted with", () => { + // interact with the form + checkbox.click() + + // trigger beforeunload to simulate leaving the page + const beforeunloadEvent = new Event("beforeunload") + window.dispatchEvent(beforeunloadEvent) + + // Test the event returnValue has been set, we don't really care about the value as + // the brower will ignore it + expect(beforeunloadEvent.returnValue).toBeTruthy() + }) + }) + + describe('when triggering a turbolinks:before-visit event', () => { + let confirmSpy + + beforeEach(() => { + confirmSpy = jest.spyOn(window, 'confirm') + }) + + afterEach(() => { + // cleanup + confirmSpy.mockRestore() + }) + + it("triggers a confirm popup up when leaving page and form has been interacted with", () => { + confirmSpy.mockImplementation((msg) => {}) + + // interact with the form + checkbox.click() + + // trigger turbolinks:before-visit to simulate leaving the page + const turbolinkEv = new Event("turbolinks:before-visit") + window.dispatchEvent(turbolinkEv) + + expect(confirmSpy).toHaveBeenCalled() + }) + + it("stays on the page if user clicks cancel on the confirm popup", () => { + // return false to simulate a user clicking on cancel + confirmSpy.mockImplementation((msg) => (false)) + + // interact with the form + checkbox.click() + + // trigger turbolinks:before-visit to simulate leaving the page + const turbolinkEv = new Event("turbolinks:before-visit") + const preventDefaultSpy = jest.spyOn(turbolinkEv, 'preventDefault') + + window.dispatchEvent(turbolinkEv) + + expect(confirmSpy).toHaveBeenCalled() + expect(preventDefaultSpy).toHaveBeenCalled() + }) + }) + }) + + describe('#handleSubmit', () => { + let checkbox + + beforeEach(() => { + // Add a mock I18n object to + const mockedT = jest.fn() + mockedT.mockImplementation((string) => (string)) + + global.I18n = { + t: mockedT + } + + checkbox = document.getElementById("test-checkbox") + }) + + afterEach(() => { + delete global.I18n + }) + + describe('when submiting the form', () => { + it("changed is set to true", () => { + const form = document.getElementById("test-form") + + // interact with the form + checkbox.click() + + // submit the form + const submitEvent = new Event("submit") + form.dispatchEvent(submitEvent) + + expect(form.dataset.unsavedChangesChanged).toBe("false") + }) + }) + }) +})