diff --git a/app/assets/javascripts/admin/admin_ofn.js.coffee b/app/assets/javascripts/admin/admin_ofn.js.coffee index bca7364525..92aa108402 100644 --- a/app/assets/javascripts/admin/admin_ofn.js.coffee +++ b/app/assets/javascripts/admin/admin_ofn.js.coffee @@ -8,6 +8,7 @@ angular.module("ofn.admin", [ "admin.dropdown", "admin.products", "admin.taxons", - "infinite-scroll" + "infinite-scroll", + "admin.orders" ]).config ($httpProvider) -> $httpProvider.defaults.headers.common["Accept"] = "application/json, text/javascript, */*" diff --git a/app/assets/javascripts/admin/all.js b/app/assets/javascripts/admin/all.js index f28f55565a..8b462886ad 100644 --- a/app/assets/javascripts/admin/all.js +++ b/app/assets/javascripts/admin/all.js @@ -110,5 +110,8 @@ // foundation //= require ../shared/mm-foundation-tpls-0.9.0-20180826174721.min.js +// LocalStorage +//= require ../shared/angular-local-storage.js + // requires the rest of the JS code in this folder //= require_tree . diff --git a/app/assets/javascripts/admin/directives/select2_watch_model.js.coffee b/app/assets/javascripts/admin/directives/select2_watch_model.js.coffee new file mode 100644 index 0000000000..510e0905f4 --- /dev/null +++ b/app/assets/javascripts/admin/directives/select2_watch_model.js.coffee @@ -0,0 +1,8 @@ +angular.module("ofn.admin").directive "select2WatchNgModel", () -> + restrict: 'E' + scope: true + require: "ngModel" + link: (scope, element, attrs, ngModel) -> + ngModel.$render = () -> + newValue = ngModel.$viewValue; + element.children(".select2").select2("val", newValue) diff --git a/app/assets/javascripts/admin/orders/controllers/orders_controller.js.coffee b/app/assets/javascripts/admin/orders/controllers/orders_controller.js.coffee index 4afb7c1c22..3756a66266 100644 --- a/app/assets/javascripts/admin/orders/controllers/orders_controller.js.coffee +++ b/app/assets/javascripts/admin/orders/controllers/orders_controller.js.coffee @@ -1,4 +1,4 @@ -angular.module("admin.orders").controller "ordersCtrl", ($scope, $timeout, RequestMonitor, Orders, SortOptions, $window, $filter) -> +angular.module("admin.orders").controller "ordersCtrl", ($scope, $timeout, RequestMonitor, Orders, SortOptions, $window, $filter, $location, KeyValueMapStore) -> $scope.RequestMonitor = RequestMonitor $scope.pagination = Orders.pagination $scope.orders = Orders.all @@ -15,39 +15,54 @@ angular.module("admin.orders").controller "ordersCtrl", ($scope, $timeout, Reque $scope.poll = 0 $scope.rowStatus = {} + KeyValueMapStore.localStorageKey = 'ordersFilters' + KeyValueMapStore.storableKeys = ["q", "sorting", "page", "per_page"] + $scope.initialise = -> + unless KeyValueMapStore.restoreValues($scope) + $scope.setDefaults() + + $scope.fetchResults() + + $scope.setDefaults = -> $scope.per_page = 15 $scope.q = { completed_at_not_null: true } + + $scope.clearFilters = () -> + KeyValueMapStore.clearKeyValueMap() + $scope.setDefaults() $scope.fetchResults() $scope.fetchResults = (page=1) -> - startDateWithTime = $scope.appendStringIfNotEmpty($scope['q']['completed_at_gteq'], ' 00:00:00') - endDateWithTime = $scope.appendStringIfNotEmpty($scope['q']['completed_at_lteq'], ' 23:59:59') + startDateWithTime = $scope.appendStringIfNotEmpty($scope.q?.completed_at_gteq, ' 00:00:00') + endDateWithTime = $scope.appendStringIfNotEmpty($scope.q?.completed_at_lteq, ' 23:59:59') $scope.resetSelected() params = { 'q[completed_at_gteq]': startDateWithTime, 'q[completed_at_lteq]': endDateWithTime, - 'q[state_eq]': $scope['q']['state_eq'], - 'q[number_cont]': $scope['q']['number_cont'], - 'q[email_cont]': $scope['q']['email_cont'], - 'q[bill_address_firstname_start]': $scope['q']['bill_address_firstname_start'], - 'q[bill_address_lastname_start]': $scope['q']['bill_address_lastname_start'], + 'q[state_eq]': $scope.q?.state_eq, + 'q[number_cont]': $scope.q?.number_cont, + 'q[email_cont]': $scope.q?.email_cont, + 'q[bill_address_firstname_start]': $scope.q?.bill_address_firstname_start, + 'q[bill_address_lastname_start]': $scope.q?.bill_address_lastname_start, # Set default checkbox values to null. See: https://github.com/openfoodfoundation/openfoodnetwork/pull/3076#issuecomment-440010498 - 'q[completed_at_not_null]': $scope['q']['completed_at_not_null'] || null, - 'q[distributor_id_in][]': $scope['q']['distributor_id_in'], - 'q[order_cycle_id_in][]': $scope['q']['order_cycle_id_in'], + 'q[completed_at_not_null]': $scope.q?.completed_at_not_null || null, + 'q[distributor_id_in][]': $scope.q?.distributor_id_in, + 'q[order_cycle_id_in][]': $scope.q?.order_cycle_id_in, 'q[s]': $scope.sorting || 'completed_at desc', - shipping_method_id: $scope.shipping_method_id, + shipping_method_id: $scope.q?.shipping_method_id, per_page: $scope.per_page, page: page } + KeyValueMapStore.setStoredValues($scope) RequestMonitor.load(Orders.index(params).$promise) $scope.appendStringIfNotEmpty = (baseString, stringToAppend) -> return baseString unless baseString + return baseString if baseString.endsWith(stringToAppend) baseString + stringToAppend diff --git a/app/assets/javascripts/admin/orders/orders.js.coffee b/app/assets/javascripts/admin/orders/orders.js.coffee index 1a8b267d30..ee7cd29837 100644 --- a/app/assets/javascripts/admin/orders/orders.js.coffee +++ b/app/assets/javascripts/admin/orders/orders.js.coffee @@ -1 +1 @@ -angular.module("admin.orders", ['admin.indexUtils', 'ngResource', 'mm.foundation']) +angular.module("admin.orders", ['admin.indexUtils', 'ngResource', 'mm.foundation', "OFNShared"]) diff --git a/app/assets/javascripts/admin/services/key_value_map_store.js.coffee b/app/assets/javascripts/admin/services/key_value_map_store.js.coffee new file mode 100644 index 0000000000..1547aaab43 --- /dev/null +++ b/app/assets/javascripts/admin/services/key_value_map_store.js.coffee @@ -0,0 +1,29 @@ +angular.module("admin.indexUtils").factory 'KeyValueMapStore', (localStorageService)-> + new class KeyValueMapStore + localStorageKey: '' + storableKeys: [] + + constructor: -> + localStorageService.setStorageType("sessionStorage") + + getStoredKeyValueMap: -> + localStorageService.get(@localStorageKey) || {} + + setStoredValues: (source) -> + keyValueMap = {} + for key in @storableKeys + keyValueMap[key] = source[key] + localStorageService.set(@localStorageKey, keyValueMap) + + restoreValues: (target) -> + storedKeyValueMap = @getStoredKeyValueMap() + + return false if _.isEmpty(storedKeyValueMap) + + for k,v of storedKeyValueMap + target[k] = v + + return true + + clearKeyValueMap: () -> + localStorageService.remove(@localStorageKey) diff --git a/app/assets/javascripts/admin/utils/directives/date_picker.js.coffee b/app/assets/javascripts/admin/utils/directives/date_picker.js.coffee index 455215f99e..6645c8c620 100644 --- a/app/assets/javascripts/admin/utils/directives/date_picker.js.coffee +++ b/app/assets/javascripts/admin/utils/directives/date_picker.js.coffee @@ -2,10 +2,13 @@ angular.module("admin.utils").directive "datepicker", ($window, $timeout) -> require: "ngModel" link: (scope, element, attrs, ngModel) -> $timeout -> - flatpickr(element, Object.assign( + flapickrInstance = flatpickr(element, Object.assign( {}, $window.FLATPICKR_DATE_DEFAULT, { onOpen: (selectedDates, dateStr, instance) -> instance.setDate(ngModel.$modelValue) } )); + ngModel.$render = () -> + newValue = ngModel.$viewValue; + flapickrInstance?.setDate(newValue) diff --git a/app/assets/javascripts/shared/shared.js.coffee b/app/assets/javascripts/shared/shared.js.coffee index fffd667d1e..089612728e 100644 --- a/app/assets/javascripts/shared/shared.js.coffee +++ b/app/assets/javascripts/shared/shared.js.coffee @@ -1,4 +1,5 @@ window.OFNShared = angular.module("OFNShared", [ - "mm.foundation" + "mm.foundation", + "LocalStorageModule" ]).config ($httpProvider) -> $httpProvider.defaults.headers.common["Accept"] = "application/json, text/javascript, */*" diff --git a/app/assets/stylesheets/admin/components/table-filter.scss b/app/assets/stylesheets/admin/components/table-filter.scss index 51504e4589..5d7f2e7429 100644 --- a/app/assets/stylesheets/admin/components/table-filter.scss +++ b/app/assets/stylesheets/admin/components/table-filter.scss @@ -1,5 +1,4 @@ #table-filter { - .field { input[type="text"], input[type="phone"], input[type="email"], input[type="number"], @@ -9,6 +8,9 @@ } .actions { - text-align: center; + display: flex; + align-items: center; + justify-content: center; + column-gap: 20px; } -} \ No newline at end of file +} diff --git a/app/views/spree/admin/orders/_filters.html.haml b/app/views/spree/admin/orders/_filters.html.haml index dead6051d9..8a789b8c64 100644 --- a/app/views/spree/admin/orders/_filters.html.haml +++ b/app/views/spree/admin/orders/_filters.html.haml @@ -10,22 +10,23 @@ = text_field_tag "q[completed_at_lteq]", nil, class: 'datepicker', datepicker: 'q.completed_at_lteq', 'ng-model' => 'q.completed_at_lteq', :placeholder => t(:stop) .field = label_tag nil, t(:status) - = select_tag("q[state_eq]", - options_for_select(Spree::Order.state_machines[:state].states.collect {|s| [t("spree.order_state.#{s.name}"), s.value]}), - {include_blank: true, class: 'select2', 'ng-model' => 'q.state_eq'}) + %select2-watch-ng-model{'ng-model': 'q.state_eq'} + = select_tag("q[state_eq]", + options_for_select(Spree::Order.state_machines[:state].states.collect {|s| [t("spree.order_state.#{s.name}"), s.value]}), + {include_blank: true, class: 'select2', 'ng-model' => 'q.state_eq'}) .four.columns .field - = label_tag nil, t(:order_number) + = label_tag "q_number_cont", t(:order_number) = text_field_tag "q[number_cont]", nil, "ng-model" => "q.number_cont", "ng-keypress" => "$event.keyCode === 13 && fetchResults()" .field - = label_tag nil, t(:email) - = email_field_tag "q[email_cont", nil, "ng-model" => "q.email_cont", "ng-keypress" => "$event.keyCode === 13 && fetchResults()" + = label_tag "q_email_cont", t(:email) + = email_field_tag "q[email_cont]", nil, "ng-model" => "q.email_cont", "ng-keypress" => "$event.keyCode === 13 && fetchResults()" .four.columns .field - = label_tag nil, t(:first_name_begins_with) + = label_tag "q_bill_address_firstname_start", t(:first_name_begins_with) = text_field_tag "q[bill_address_firstname_start]", nil, size: 25, "ng-model" => "q.bill_address_firstname_start", "ng-keypress" => "$event.keyCode === 13 && fetchResults()" .field - = label_tag nil, t(:last_name_begins_with) + = label_tag "q_bill_address_lastname_start", t(:last_name_begins_with) = text_field_tag "q[bill_address_lastname_start]", nil, size: 25, "ng-model" => "q.bill_address_lastname_start", "ng-keypress" => "$event.keyCode === 13 && fetchResults()" .omega.four.columns .field.checkbox @@ -34,21 +35,26 @@ = t(:show_only_complete_orders) .field = label_tag nil, t(:shipping_method) - = select_tag("shipping_method_id", + %select2-watch-ng-model{'ng-model': 'q.shipping_method_id'} + = select_tag("q[shipping_method_id]", options_for_select(Spree::ShippingMethod.managed_by(spree_current_user).collect {|s| [t("spree.shipping_method_names.#{s.name}"), s.id]}), - {include_blank: true, class: 'select2', 'ng-model' => 'shipping_method_id'}) + {include_blank: true, class: 'select2', 'ng-model': 'q.shipping_method_id'}) .field-block.alpha.eight.columns = label_tag nil, t(:distributors) - = select_tag("q[distributor_id_in]", - options_for_select(Enterprise.is_distributor.managed_by(spree_current_user).map {|e| [e.name, e.id]}, params[:distributor_ids]), - {class: "select2 fullwidth", multiple: true, 'ng-model' => 'q.distributor_id_in'}) + %select2-watch-ng-model{'ng-model': 'q.distributor_id_in'} + = select_tag("q[distributor_id_in]", + options_for_select(Enterprise.is_distributor.managed_by(spree_current_user).map {|e| [e.name, e.id]}, params[:distributor_ids]), + {class: "select2 fullwidth", multiple: true, 'ng-model' => 'q.distributor_id_in'}) .field-block.omega.eight.columns = label_tag nil, t(:order_cycles) - = select_tag("q[order_cycle_id_in]", - options_for_select(OrderCycle.managed_by(spree_current_user).where('order_cycles.orders_close_at is not null').order('order_cycles.orders_close_at DESC').map {|oc| [oc.name, oc.id]}, params[:order_cycle_ids]), - {class: "select2 fullwidth", multiple: true, 'ng-model' => 'q.order_cycle_id_in'}) + %select2-watch-ng-model{'ng-model': 'q.order_cycle_id_in'} + = select_tag("q[order_cycle_id_in]", + options_for_select(OrderCycle.managed_by(spree_current_user).where('order_cycles.orders_close_at is not null').order('order_cycles.orders_close_at DESC').map {|oc| [oc.name, oc.id]}, params[:order_cycle_ids]), + {class: "select2 fullwidth", multiple: true, 'ng-model' => 'q.order_cycle_id_in'}) .clearfix .actions.filter-actions - %div - %a.button.icon-search{'ng-click' => 'fetchResults()'} - = t(:filter_results) + %a.button.icon-search{'ng-click' => 'fetchResults()'} + = t(:filter_results) + %a.button{'ng-click' => 'clearFilters()', "id": "clear_filters_button"} + = t(:clear_filters) + diff --git a/app/views/spree/admin/orders/edit.html.haml b/app/views/spree/admin/orders/edit.html.haml index f86d2f6c1f..d5be74c809 100644 --- a/app/views/spree/admin/orders/edit.html.haml +++ b/app/views/spree/admin/orders/edit.html.haml @@ -1,10 +1,16 @@ = csrf_meta_tags + +- content_for :main_ng_app_name do + = "ofn.admin" + - content_for :page_actions do - if can?(:fire, @order) %li= event_links = render partial: 'spree/admin/shared/order_links' - if can?(:admin, Spree::Order) - %li= button_link_to t(:back_to_orders_list), admin_orders_path, :icon => 'icon-arrow-left' + %li{"ng-controller" => "ordersCtrl"} + %a.button.icon-arrow-left{icon: 'icon-arrow-left', ng: { href: admin_orders_path }} + = t(:back_to_orders_list) = render partial: "spree/admin/shared/order_page_title" = render partial: "spree/admin/shared/order_tabs", locals: { current: 'Order Details' } @@ -17,7 +23,7 @@ = admin_inject_shops(module: 'admin.orders') = admin_inject_order_cycles - %div{"ng-app" => "admin.orders", "ng-controller" => "orderCtrl", "ofn-distributor-id" => @order.distributor_id, "ofn-order-cycle-id" => @order.order_cycle_id} + %div{"ng-controller" => "orderCtrl", "ofn-distributor-id" => @order.distributor_id, "ofn-order-cycle-id" => @order.order_cycle_id} = render :partial => 'add_product' if can?(:update, @order) diff --git a/app/views/spree/admin/orders/index.html.haml b/app/views/spree/admin/orders/index.html.haml index 758fc4dd07..08899c50d0 100644 --- a/app/views/spree/admin/orders/index.html.haml +++ b/app/views/spree/admin/orders/index.html.haml @@ -8,7 +8,7 @@ = render partial: 'spree/admin/shared/order_sub_menu' - content_for :main_ng_app_name do - = "admin.orders" + = "ofn.admin" - content_for :main_ng_ctrl_name do = "ordersCtrl" diff --git a/config/locales/en.yml b/config/locales/en.yml index 85ef5a75a5..576fd259d4 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -333,6 +333,7 @@ en: no_pending_payments: "No pending payments" invalid_payment_state: "Invalid payment state: %{state}" filter_results: Filter Results + clear_filters: Clear filters quantity: Quantity pick_up: Pick up ok: Ok diff --git a/spec/features/admin/orders_spec.rb b/spec/features/admin/orders_spec.rb index df9bc5725c..c777d6e548 100644 --- a/spec/features/admin/orders_spec.rb +++ b/spec/features/admin/orders_spec.rb @@ -111,4 +111,97 @@ feature ' expect(page).to have_current_path spree.edit_admin_order_path(incomplete_order) end end + + context "test the 'Only show the complete orders' checkbox" do + scenario "display or not incomplete order" do + incomplete_order = create(:order, distributor: distributor, order_cycle: order_cycle) + complete_order = create( + :order, + distributor: distributor, + order_cycle: order_cycle, + user: user, + state: 'complete', + payment_state: 'balance_due', + completed_at: 1.day.ago + ) + login_as_admin_and_visit spree.admin_orders_path + expect(page).to have_content complete_order.number + expect(page).to have_no_content incomplete_order.number + + uncheck 'Only show complete orders' + page.find('a.icon-search').click + + expect(page).to have_content complete_order.number + expect(page).to have_content incomplete_order.number + end + end + + context "save the filter params" do + let!(:shipping_method) { create(:shipping_method, name: "UPS Ground") } + let!(:user) { create(:user, email: 'an@email.com') } + let!(:order) do + create( + :order, + distributor: distributor, + order_cycle: order_cycle, + user: user, + number: "R123456", + state: 'complete', + payment_state: 'balance_due', + completed_at: 1.day.ago + ) + end + before :each do + login_as_admin_and_visit spree.admin_orders_path + + # Specify each filters + uncheck 'Only show complete orders' + fill_in "Invoice number", with: "R123456" + select2_select order_cycle.name, from: 'q_order_cycle_id_in' + select2_select distributor.name, from: 'q_distributor_id_in' + select2_select shipping_method.name, from: 'q_shipping_method_id' + select2_select "complete", from: 'q_state_eq' + fill_in "Email", with: user.email + fill_in "First name begins with", with: "J" + fill_in "Last name begins with", with: "D" + find('#q_completed_at_gteq').click + select_date_from_datepicker Time.zone.at(1.week.ago) + find('#q_completed_at_lteq').click + select_date_from_datepicker Time.zone.now + + page.find('a.icon-search').click + end + + scenario "when reloading the page" do + page.driver.refresh + + # Check every filters to be equal + expect(find_field("Only show complete orders")).not_to be_checked + expect(find_field("Invoice number").value).to eq "R123456" + expect(find("#s2id_q_shipping_method_id").text).to eq shipping_method.name + expect(find("#s2id_q_state_eq").text).to eq "complete" + expect(find("#s2id_q_distributor_id_in").text).to eq distributor.name + expect(find("#s2id_q_order_cycle_id_in").text).to eq order_cycle.name + expect(find_field("Email").value).to eq user.email + expect(find_field("First name begins with").value).to eq "J" + expect(find_field("Last name begins with").value).to eq "D" + expect(find("#q_completed_at_gteq").value).to eq 1.week.ago.strftime("%Y-%m-%d") + expect(find("#q_completed_at_lteq").value).to eq Time.zone.now.strftime("%Y-%m-%d") + end + + scenario "and clear filters" do + find("a#clear_filters_button").click + expect(find_field("Only show complete orders")).to be_checked + expect(find_field("Invoice number").value).to eq "" + expect(find("#s2id_q_shipping_method_id").text).to be_empty + expect(find("#s2id_q_state_eq").text).to be_empty + expect(find("#s2id_q_distributor_id_in").text).to be_empty + expect(find("#s2id_q_order_cycle_id_in").text).to be_empty + expect(find_field("Email").value).to be_empty + expect(find_field("First name begins with").value).to be_empty + expect(find_field("Last name begins with").value).to be_empty + expect(find("#q_completed_at_gteq").value).to be_empty + expect(find("#q_completed_at_lteq").value).to be_empty + end + end end diff --git a/spec/javascripts/unit/admin/services/key_value_map_store_spec.js.coffee b/spec/javascripts/unit/admin/services/key_value_map_store_spec.js.coffee new file mode 100644 index 0000000000..9d853f7f7a --- /dev/null +++ b/spec/javascripts/unit/admin/services/key_value_map_store_spec.js.coffee @@ -0,0 +1,38 @@ +describe "Test KeyValueMapStore service", -> + + KeyValueMapStore = null + + beforeEach -> + module "ofn.admin" + + beforeEach inject (_KeyValueMapStore_) -> + KeyValueMapStore = _KeyValueMapStore_ + + it "set and restore filters", -> + KeyValueMapStore.localStorageKey = 'localStorageKey' + KeyValueMapStore.storableKeys = ["a", "b", "c"] + source = + a: "1", + b: "2", + d: "4" + KeyValueMapStore.setStoredValues(source) + source = {} + restored = KeyValueMapStore.restoreValues(source) + expect(restored).toEqual true + expect(source).toEqual {a: '1', b: '2'} + + it "clear filters", -> + KeyValueMapStore.storageKey = 'localStorageKey' + KeyValueMapStore.storableFilters = ["a", "b", "c"] + source = + a: "1", + b: "2", + d: "4" + KeyValueMapStore.setStoredValues(source) + KeyValueMapStore.clearKeyValueMap() + source = {} + restored = KeyValueMapStore.restoreValues(source) + expect(restored).toEqual false + expect(source).toEqual {} + +