From 68cb8d9965b87b3d6b23cdcfa4f4c2dc82d61684 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Bellet Date: Fri, 29 Apr 2022 10:36:53 +0200 Subject: [PATCH 1/8] Formatting file --- .../controllers/tom_select_controller.js | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/app/webpacker/controllers/tom_select_controller.js b/app/webpacker/controllers/tom_select_controller.js index e1b7a65dfb..9eae0879fe 100644 --- a/app/webpacker/controllers/tom_select_controller.js +++ b/app/webpacker/controllers/tom_select_controller.js @@ -1,22 +1,23 @@ -import { Controller } from "stimulus" -import TomSelect from "tom-select" +import { Controller } from "stimulus"; +import TomSelect from "tom-select"; export default class extends Controller { - static values = { options: Object } + static values = { options: Object }; static defaults = { maxItems: 1, maxOptions: null, plugins: ["dropdown_input"], - allowEmptyOption: true - } + allowEmptyOption: true, + }; connect() { - this.control = new TomSelect( - this.element, { ...this.constructor.defaults, ...this.optionsValue } - ) + this.control = new TomSelect(this.element, { + ...this.constructor.defaults, + ...this.optionsValue, + }); } disconnect() { - if (this.control) this.control.destroy() + if (this.control) this.control.destroy(); } } From 5018f824f573d4d879616237144680732c211a8c Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Bellet Date: Mon, 2 May 2022 13:33:57 +0200 Subject: [PATCH 2/8] Prepare tom-select controller to inherit others controllers --- app/webpacker/controllers/tom_select_controller.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/webpacker/controllers/tom_select_controller.js b/app/webpacker/controllers/tom_select_controller.js index 9eae0879fe..c39e8abaeb 100644 --- a/app/webpacker/controllers/tom_select_controller.js +++ b/app/webpacker/controllers/tom_select_controller.js @@ -10,10 +10,11 @@ export default class extends Controller { allowEmptyOption: true, }; - connect() { + connect(options = {}) { this.control = new TomSelect(this.element, { ...this.constructor.defaults, ...this.optionsValue, + ...options, }); } From ce058c6e364ad08caa981366c20ccf2900c922f1 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Bellet Date: Mon, 2 May 2022 13:36:49 +0200 Subject: [PATCH 3/8] Create a SelectCustomer controller To be used in `/admin/orders/ORDER_ID/customer` page - Fetch `/admin/search/customers.json?` with query as params - Render each items response thanks to `renderOption()` method - `onItemSelect()` is here to fill the associated form in the page --- .../controllers/select_customer_controller.js | 113 ++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 app/webpacker/controllers/select_customer_controller.js diff --git a/app/webpacker/controllers/select_customer_controller.js b/app/webpacker/controllers/select_customer_controller.js new file mode 100644 index 0000000000..7379ac3862 --- /dev/null +++ b/app/webpacker/controllers/select_customer_controller.js @@ -0,0 +1,113 @@ +import TomSelectController from "./tom_select_controller"; + +export default class extends TomSelectController { + static values = { options: Object, distributor: Number }; + + connect() { + const options = { + valueField: "id", + labelField: "email", + searchField: ["email", "full_name", "last_name"], + load: this.load.bind(this), + shouldLoad: (query) => query.length > 2, + render: { + option: this.renderOption.bind(this), + }, + }; + super.connect(options); + this.control.on("item_add", this.onItemSelect.bind(this)); + this.items = []; + } + + load(query, callback) { + var params = { + q: query, + distributor_id: this.distributorValue, + }; + + fetch("/admin/search/customers.json?" + new URLSearchParams(params)) + .then((response) => response.json()) + .then((json) => { + this.items = json; + callback(json); + }) + .catch((error) => { + this.items = []; + console.log(error); + callback(); + }); + } + + renderOption(item, escape) { + return `
+
+
${escape(item.email)}
+ ${ + item.bill_address.firstname + ? `${I18n.t("bill_address")} + ${item.bill_address.firstname} ${ + item.bill_address.lastname + }
+ ${item.bill_address.address1}, ${ + item.bill_address.address2 + }
+ ${item.bill_address.city} +
+ ${ + item.bill_address.state_id && + item.bill_address.state && + item.bill_address.state.name + ? item.bill_address.state.name + : item.bill_address.state_name + } + + ${ + item.bill_address.country && item.bill_address.country.name + ? item.bill_address.country.name + : item.bill_address.country_name + } + ` + : "" + } +
+
`; + } + + onItemSelect(id, item) { + const customer = this.items.find((item) => item.id == id); + ["bill_address", "ship_address"].forEach((address) => { + const data = customer[address]; + const address_parts = [ + "firstname", + "lastname", + "address1", + "address2", + "city", + "zipcode", + "phone", + ]; + const attribute_wrapper = "#order_" + address + "_attributes_"; + address_parts.forEach((part) => { + document.querySelector(attribute_wrapper + part).value = data + ? data[part] + : ""; + }); + this.setValueOnTomSelectController( + document.querySelector(attribute_wrapper + "state_id"), + data ? data.state_id : "" + ); + this.setValueOnTomSelectController( + document.querySelector(attribute_wrapper + "country_id"), + data ? data.country_id : "" + ); + }); + $("#order_email").val(customer.email); + $("#user_id").val(customer.user_id); + } + + setValueOnTomSelectController = (element, value) => { + this.application + .getControllerForElementAndIdentifier(element, "tom-select") + .control.setValue(value, true); + }; +} From 2c29b1f60fde9889da5a699e52c0a69d15746efc Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Bellet Date: Mon, 2 May 2022 13:37:12 +0200 Subject: [PATCH 4/8] Replace Angular directive and use stimulus controller + Update specs as well --- .../customer_search_override.js.coffee | 62 ------------------- .../customer_details/_autocomplete.js.erb | 19 ------ .../orders/customer_details/edit.html.haml | 4 +- spec/support/request/web_helper.rb | 6 ++ spec/system/admin/order_spec.rb | 6 +- 5 files changed, 13 insertions(+), 84 deletions(-) delete mode 100644 app/assets/javascripts/admin/orders/directives/customer_search_override.js.coffee delete mode 100644 app/views/spree/admin/orders/customer_details/_autocomplete.js.erb diff --git a/app/assets/javascripts/admin/orders/directives/customer_search_override.js.coffee b/app/assets/javascripts/admin/orders/directives/customer_search_override.js.coffee deleted file mode 100644 index 79d4ac4a4b..0000000000 --- a/app/assets/javascripts/admin/orders/directives/customer_search_override.js.coffee +++ /dev/null @@ -1,62 +0,0 @@ -angular.module("admin.orders").directive 'customerSearchOverride', -> - restrict: 'C' - scope: - distributorId: '@' - link: (scope, element, attr) -> - if $('#customer_autocomplete_template').length > 0 - customerTemplate = Handlebars.compile($('#customer_autocomplete_template').text()) - - formatCustomerResult = (customer) -> - customerTemplate - customer: customer - bill_address: customer.bill_address - ship_address: customer.ship_address - - element.select2 - placeholder: Spree.translations.choose_a_customer - minimumInputLength: 3 - ajax: - url: '/admin/search/customers.json' - datatype: 'json' - data: (term, page) -> - { - q: term - distributor_id: scope.distributorId # modified - } - results: (data, page) -> - { results: data } - dropdownCssClass: 'customer_search' - formatResult: formatCustomerResult - formatSelection: (customer) -> - _.each [ - 'bill_address' - 'ship_address' - ], (address) -> - data = customer[address] - address_parts = [ - 'firstname' - 'lastname' - 'company' - 'address1' - 'address2' - 'city' - 'zipcode' - 'phone' - ] - attribute_wrapper = '#order_' + address + '_attributes_' - if data # modified - _.each address_parts, (part) -> - $(attribute_wrapper + part).val data[part] - return - $(attribute_wrapper + 'state_id').select2 'val', data['state_id'] - $(attribute_wrapper + 'country_id').select2 'val', data['country_id'] - else - _.each address_parts, (part) -> - $(attribute_wrapper + part).val '' - return - $(attribute_wrapper + 'state_id').select2 'val', '' - $(attribute_wrapper + 'country_id').select2 'val', '' - return - $('#order_email').val customer.email - $('#user_id').val customer.user_id # modified - customer.email diff --git a/app/views/spree/admin/orders/customer_details/_autocomplete.js.erb b/app/views/spree/admin/orders/customer_details/_autocomplete.js.erb deleted file mode 100644 index 9484fa0dbb..0000000000 --- a/app/views/spree/admin/orders/customer_details/_autocomplete.js.erb +++ /dev/null @@ -1,19 +0,0 @@ - diff --git a/app/views/spree/admin/orders/customer_details/edit.html.haml b/app/views/spree/admin/orders/customer_details/edit.html.haml index 765224d247..8735e3f5ee 100644 --- a/app/views/spree/admin/orders/customer_details/edit.html.haml +++ b/app/views/spree/admin/orders/customer_details/edit.html.haml @@ -17,8 +17,8 @@ %legend{:align => "center"}= Spree.t(:customer_search) - content_for :main_ng_app_name do = "admin.orders" - = hidden_field_tag :customer_search_override, nil, distributor_id: @order.distributor_id, :class => 'fullwidth title customer-search-override' - = render :partial => "spree/admin/orders/customer_details/autocomplete", :formats => :js + %label{for: "customer_search_override"}= Spree.t(:choose_a_customer) + %select{name: "customer_search_override", "data-controller": "select-customer", "data-select-customer-distributor-value": @order.distributor_id, class: "primary", placeholder: Spree.t(:choose_a_customer) } = render :partial => 'spree/shared/error_messages', :locals => { :target => @order } diff --git a/spec/support/request/web_helper.rb b/spec/support/request/web_helper.rb index 7daced0791..67d88115b5 100644 --- a/spec/support/request/web_helper.rb +++ b/spec/support/request/web_helper.rb @@ -127,6 +127,12 @@ module WebHelper page.find(:css, 'body').click end + def tomselect_search_and_select(value, options) + page.find("[name='#{options[:from]}']").sibling(".ts-wrapper").click + page.find(:css, '.ts-dropdown input.dropdown-input').set(value) + page.find(:css, '.ts-dropdown .ts-dropdown-content .option', text: value).click + end + def accept_js_alert page.driver.browser.switch_to.alert.accept end diff --git a/spec/system/admin/order_spec.rb b/spec/system/admin/order_spec.rb index c4cbfc125e..98d187c37e 100644 --- a/spec/system/admin/order_spec.rb +++ b/spec/system/admin/order_spec.rb @@ -383,7 +383,7 @@ describe ' expect(page).to have_selector '#select-customer' # And I select that customer's email address and save the order - select2_select customer.email, from: 'customer_search_override', search: true + tomselect_search_and_select customer.email, from: 'customer_search_override' click_button 'Update' expect(page).to have_selector "h1.js-admin-page-title", text: "Customer Details" @@ -391,6 +391,10 @@ describe ' order = Spree::Order.last expect(order.ship_address.lastname).to eq customer.ship_address.lastname expect(order.bill_address.lastname).to eq customer.bill_address.lastname + expect(order.ship_address.zipcode).to eq customer.ship_address.zipcode + expect(order.bill_address.zipcode).to eq customer.bill_address.zipcode + expect(order.ship_address.city).to eq customer.ship_address.city + expect(order.bill_address.city).to eq customer.bill_address.city end context "as an enterprise manager" do From 5e27795a15b13208bb2d5cc22d99e5b608389aac Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Bellet Date: Mon, 23 May 2022 17:55:53 +0200 Subject: [PATCH 5/8] Special method to handle customer with no bill_address --- app/webpacker/controllers/select_customer_controller.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/app/webpacker/controllers/select_customer_controller.js b/app/webpacker/controllers/select_customer_controller.js index 7379ac3862..36f8002c50 100644 --- a/app/webpacker/controllers/select_customer_controller.js +++ b/app/webpacker/controllers/select_customer_controller.js @@ -39,6 +39,9 @@ export default class extends TomSelectController { } renderOption(item, escape) { + if (!item.bill_address) { + return this.renderWithNoBillAddress(item, escape); + } return `
${escape(item.email)}
@@ -73,6 +76,12 @@ export default class extends TomSelectController {
`; } + renderWithNoBillAddress(item, escape) { + return `
+
${escape(item.email)}
+
`; + } + onItemSelect(id, item) { const customer = this.items.find((item) => item.id == id); ["bill_address", "ship_address"].forEach((address) => { From 760de3b5075a50ac7ff5aefe67ad093b41e5b403 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Bellet Date: Tue, 24 May 2022 12:35:23 +0200 Subject: [PATCH 6/8] Do not set `null` value to select controller --- app/webpacker/controllers/select_customer_controller.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/webpacker/controllers/select_customer_controller.js b/app/webpacker/controllers/select_customer_controller.js index 36f8002c50..f67d3f6625 100644 --- a/app/webpacker/controllers/select_customer_controller.js +++ b/app/webpacker/controllers/select_customer_controller.js @@ -115,6 +115,9 @@ export default class extends TomSelectController { } setValueOnTomSelectController = (element, value) => { + if (!value) { + return; + } this.application .getControllerForElementAndIdentifier(element, "tom-select") .control.setValue(value, true); From 39b7f56e6d245293ea66883d0353b5a25e78318b Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Bellet Date: Tue, 31 May 2022 08:57:11 +0200 Subject: [PATCH 7/8] Use mixins `useSearchCustomer` Prefer composition vs. inheritance --- .../controllers/mixins/useSearchCustomer.js | 22 +++++++++++++++++++ .../controllers/select_customer_controller.js | 21 ++---------------- 2 files changed, 24 insertions(+), 19 deletions(-) create mode 100644 app/webpacker/controllers/mixins/useSearchCustomer.js diff --git a/app/webpacker/controllers/mixins/useSearchCustomer.js b/app/webpacker/controllers/mixins/useSearchCustomer.js new file mode 100644 index 0000000000..a7776e14fc --- /dev/null +++ b/app/webpacker/controllers/mixins/useSearchCustomer.js @@ -0,0 +1,22 @@ +export const useSearchCustomer = (controller) => { + Object.assign(controller, { + load: function (query, callback) { + var params = { + q: query, + distributor_id: this.distributorValue, + }; + + fetch("/admin/search/customers.json?" + new URLSearchParams(params)) + .then((response) => response.json()) + .then((json) => { + this.items = json; + callback(json); + }) + .catch((error) => { + this.items = []; + console.log(error); + callback(); + }); + }, + }); +}; diff --git a/app/webpacker/controllers/select_customer_controller.js b/app/webpacker/controllers/select_customer_controller.js index f67d3f6625..c02ff5bcc8 100644 --- a/app/webpacker/controllers/select_customer_controller.js +++ b/app/webpacker/controllers/select_customer_controller.js @@ -1,9 +1,11 @@ import TomSelectController from "./tom_select_controller"; +import { useSearchCustomer } from "./mixins/useSearchCustomer"; export default class extends TomSelectController { static values = { options: Object, distributor: Number }; connect() { + useSearchCustomer(this); const options = { valueField: "id", labelField: "email", @@ -19,25 +21,6 @@ export default class extends TomSelectController { this.items = []; } - load(query, callback) { - var params = { - q: query, - distributor_id: this.distributorValue, - }; - - fetch("/admin/search/customers.json?" + new URLSearchParams(params)) - .then((response) => response.json()) - .then((json) => { - this.items = json; - callback(json); - }) - .catch((error) => { - this.items = []; - console.log(error); - callback(); - }); - } - renderOption(item, escape) { if (!item.bill_address) { return this.renderWithNoBillAddress(item, escape); From 8fec5f1464b43ff72275db3880bbee9b880c8345 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Bellet Date: Tue, 31 May 2022 09:00:24 +0200 Subject: [PATCH 8/8] Use mixins `useRenderCustomer` --- .../controllers/mixins/useRenderCustomer.js | 50 +++++++++++++++++++ .../controllers/select_customer_controller.js | 46 +---------------- 2 files changed, 52 insertions(+), 44 deletions(-) create mode 100644 app/webpacker/controllers/mixins/useRenderCustomer.js diff --git a/app/webpacker/controllers/mixins/useRenderCustomer.js b/app/webpacker/controllers/mixins/useRenderCustomer.js new file mode 100644 index 0000000000..2f23da24c0 --- /dev/null +++ b/app/webpacker/controllers/mixins/useRenderCustomer.js @@ -0,0 +1,50 @@ +export const useRenderCustomer = (controller) => { + Object.assign(controller, { + renderOption(item, escape) { + if (!item.bill_address) { + return this.renderWithNoBillAddress(item, escape); + } + return `
+
+
${escape(item.email)}
+ ${ + item.bill_address.firstname + ? `${I18n.t("bill_address")} + ${item.bill_address.firstname} ${ + item.bill_address.lastname + }
+ ${item.bill_address.address1}, ${ + item.bill_address.address2 + }
+ ${item.bill_address.city} +
+ ${ + item.bill_address.state_id && + item.bill_address.state && + item.bill_address.state.name + ? item.bill_address.state.name + : item.bill_address.state_name + } + + ${ + item.bill_address.country && + item.bill_address.country.name + ? item.bill_address.country.name + : item.bill_address.country_name + } + ` + : "" + } +
+
`; + }, + + renderWithNoBillAddress(item, escape) { + return `
+
${escape( + item.email + )}
+
`; + }, + }); +}; diff --git a/app/webpacker/controllers/select_customer_controller.js b/app/webpacker/controllers/select_customer_controller.js index c02ff5bcc8..7034dbd4d0 100644 --- a/app/webpacker/controllers/select_customer_controller.js +++ b/app/webpacker/controllers/select_customer_controller.js @@ -1,11 +1,13 @@ import TomSelectController from "./tom_select_controller"; import { useSearchCustomer } from "./mixins/useSearchCustomer"; +import { useRenderCustomer } from "./mixins/useRenderCustomer"; export default class extends TomSelectController { static values = { options: Object, distributor: Number }; connect() { useSearchCustomer(this); + useRenderCustomer(this); const options = { valueField: "id", labelField: "email", @@ -21,50 +23,6 @@ export default class extends TomSelectController { this.items = []; } - renderOption(item, escape) { - if (!item.bill_address) { - return this.renderWithNoBillAddress(item, escape); - } - return `
-
-
${escape(item.email)}
- ${ - item.bill_address.firstname - ? `${I18n.t("bill_address")} - ${item.bill_address.firstname} ${ - item.bill_address.lastname - }
- ${item.bill_address.address1}, ${ - item.bill_address.address2 - }
- ${item.bill_address.city} -
- ${ - item.bill_address.state_id && - item.bill_address.state && - item.bill_address.state.name - ? item.bill_address.state.name - : item.bill_address.state_name - } - - ${ - item.bill_address.country && item.bill_address.country.name - ? item.bill_address.country.name - : item.bill_address.country_name - } - ` - : "" - } -
-
`; - } - - renderWithNoBillAddress(item, escape) { - return `
-
${escape(item.email)}
-
`; - } - onItemSelect(id, item) { const customer = this.items.find((item) => item.id == id); ["bill_address", "ship_address"].forEach((address) => {