diff --git a/app/components/product_component/product_component.html.haml b/app/components/product_component/product_component.html.haml index e0d9d760a8..55c3e47e67 100644 --- a/app/components/product_component/product_component.html.haml +++ b/app/components/product_component/product_component.html.haml @@ -1,6 +1,6 @@ %tr - @columns.each do |column| %td.products_column{class: column[:id]} - - if column[:id] == "name" && @image + - if column[:id] == "name" && @image&.attachment.present? = image_tag @image.url(:mini) = column[:value] diff --git a/app/controllers/spree/admin/orders_controller.rb b/app/controllers/spree/admin/orders_controller.rb index 51a21a8b60..43cbdf2d9c 100644 --- a/app/controllers/spree/admin/orders_controller.rb +++ b/app/controllers/spree/admin/orders_controller.rb @@ -5,6 +5,7 @@ require 'open_food_network/spree_api_key_loader' module Spree module Admin class OrdersController < Spree::Admin::BaseController + include CablecarResponses include OpenFoodNetwork::SpreeApiKeyLoader helper CheckoutHelper @@ -16,6 +17,13 @@ module Spree respond_to :html, :json + def index + orders = SearchOrders.new(search_params, spree_current_user).orders + @pagy, @orders = pagy(orders, items: params[:per_page] || 15) + + update_search_results if searching? + end + def new @order = Order.create @order.created_by = spree_current_user @@ -110,6 +118,25 @@ module Spree private + def update_search_results + render cable_ready: cable_car.inner_html( + "#orders-index", + partial("spree/admin/orders/table", locals: { pagy: @pagy, orders: @orders }) + ) + end + + def searching? + params[:q].present? && request.format.symbol == :cable_ready + end + + def search_params + search_defaults.deep_merge(params.permit!).to_h.with_indifferent_access + end + + def search_defaults + { q: { completed_at_not_null: 1, s: "completed_at desc" } } + end + def on_update @order.recreate_all_fees! diff --git a/app/services/search_orders.rb b/app/services/search_orders.rb index 6b08d78cfb..60cf824e63 100644 --- a/app/services/search_orders.rb +++ b/app/services/search_orders.rb @@ -26,7 +26,7 @@ class SearchOrders base_query = ::Permissions::Order.new(current_user).editable_orders.not_empty .or(::Permissions::Order.new(current_user).editable_orders.finalized) - return base_query unless params[:shipping_method_id] + return base_query if params[:shipping_method_id].blank? base_query .joins(shipments: :shipping_rates) diff --git a/app/views/admin/shared/_stimulus_page_controls.html.haml b/app/views/admin/shared/_stimulus_page_controls.html.haml new file mode 100644 index 0000000000..9e69c82203 --- /dev/null +++ b/app/views/admin/shared/_stimulus_page_controls.html.haml @@ -0,0 +1,8 @@ += select_tag :per_page, + options_for_select([15, 50, 100].collect{|num| [t('js.admin.orders.index.per_page', results: num), num] }, params[:per_page]), + { class: "no-search primary per-page-dropdown", data: { controller: "tom-select search", action: "change->search#changePerPage" } } + +- if pagy + %span.per-page-feedback + = t("spree.admin.orders.index.results_found", number: pagy.count) + = t("spree.admin.orders.index.viewing", start: pagy.from, end: pagy.to ) diff --git a/app/views/admin/shared/_stimulus_pagination.html.haml b/app/views/admin/shared/_stimulus_pagination.html.haml new file mode 100644 index 0000000000..e7e97fc643 --- /dev/null +++ b/app/views/admin/shared/_stimulus_pagination.html.haml @@ -0,0 +1,22 @@ +- link = pagy_link_proc(pagy) + +.pagination{ "data-controller": "search" } + - if pagy.prev + %button{ data: { action: 'click->search#changePage', page: pagy.prev } }!= pagy_t('pagy.nav.prev') + - else + %button.disabled{disabled: "disabled"}!= pagy_t('pagy.nav.prev') + + - pagy.series.each do |item| # series example: [1, :gap, 7, 8, "9", 10, 11, :gap, 36] + - if item.is_a?(Integer) # page link + %button{ data: { action: 'click->search#changePage', page: item } }= item + + - elsif item.is_a?(String) # current page + %button.active= item + + - elsif item == :gap # page gap + %span.pagination-ellipsis!= pagy_t('pagy.nav.gap') + + - if pagy.next + %button{ data: { action: 'click->search#changePage', page: pagy.next } }!= pagy_t('pagy.nav.next') + - else + %button.disabled.pagination-next{disabled: "disabled"}!= pagy_t('pagy.nav.next') diff --git a/app/views/spree/admin/orders/_bulk_actions.html.haml b/app/views/spree/admin/orders/_bulk_actions.html.haml new file mode 100644 index 0000000000..bdbfdd466a --- /dev/null +++ b/app/views/spree/admin/orders/_bulk_actions.html.haml @@ -0,0 +1,20 @@ +%button.plain.ofn-drop-down.disabled{ "data-checked-target": "disable" } + %span{ class: 'icon-reorder' } + ="#{t('admin.actions')}".html_safe + %span.toggle-off.icon-caret-up + %span.toggle-on.icon-caret-down + + %div.menu.dropdown-content + %div.menu_item + %span.name{ "data-controller": "modal-link", "data-action": "click->modal-link#open", "data-modal-link-target-value": "resend_confirmation" } + = t('spree.admin.orders.index.resend_confirmation') + - if Spree::Config[:enable_invoices?] + %div.menu_item + %span.name{ "data-controller": "modal-link", "data-action": "click->modal-link#open", "data-modal-link-target-value": "send_invoice" } + = t('spree.admin.orders.index.send_invoice') + %div.menu_item + %span.name.invoices-modal{'ng-controller' => 'bulkInvoiceCtrl', 'ng-click' => 'createBulkInvoice()' } + = t('spree.admin.orders.index.print_invoices') + %div.menu_item + %span.name{ "data-controller": "modal-link", "data-action": "click->modal-link#open", "data-modal-link-target-value": "cancel_orders" } + = t('spree.admin.orders.index.cancel_orders') diff --git a/app/views/spree/admin/orders/_filters.html.haml b/app/views/spree/admin/orders/_filters.html.haml index f328c149ea..9f924fffa1 100644 --- a/app/views/spree/admin/orders/_filters.html.haml +++ b/app/views/spree/admin/orders/_filters.html.haml @@ -1,59 +1,60 @@ -%div.admin-orders-index-search - = form_tag spree.admin_orders_url, {name: "orders_form", "ng-submit" => "fetchResults()"} do +%div.admin-orders-index-search{ "data-controller": "search" } + = form_with url: spree.admin_orders_url, id: "orders_form", method: :get, data: { remote: true, "search-target": "form" } do + = hidden_field_tag :page, 1, class: "page" + = hidden_field_tag :per_page, 15, class: "per-page" + = hidden_field_tag "[q][s]", params.dig(:q, :s) || "completed_at desc", class: "sort", "data-default": "completed_at desc" + .field-block.alpha.four.columns .date-range-filter.field = label_tag nil, t(:date_range) - .date-range-fields{ data: { controller: "flatpickr", "flatpickr-mode-value": "range", "flatpickr-default-date": "{{ [q.completed_at_gteq, q.completed_at_lteq] }}" } } + .date-range-fields{ data: { controller: "flatpickr", "flatpickr-mode-value": "range" } } = text_field_tag nil, nil, class: "datepicker", data: { "flatpickr-target": "instance", action: "flatpickr_clear@window->flatpickr#clear" } = text_field_tag "q[completed_at_gteq]", nil, "ng-model": "q.completed_at_gteq", data: { "flatpickr-target": "start" }, style: "display: none" = text_field_tag "q[completed_at_lteq]", nil, "ng-model": "q.completed_at_lteq", data: { "flatpickr-target": "end" }, style: "display: none" .field = label_tag nil, t(:status) - %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'}) + = 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: "primary", "data-controller": "tom-select" }) .four.columns .field = 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()" + = text_field_tag "q[number_cont]", nil .field = 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()" + = email_field_tag "q[email_cont]", nil .four.columns .field = 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()" + = text_field_tag "q[bill_address_firstname_start]", nil, size: 25 .field = 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()" + = text_field_tag "q[bill_address_lastname_start]", nil, size: 25 .omega.four.columns .field.checkbox %label - = check_box_tag "q[completed_at_not_null]", 1, true, {'ng-model' => 'q.completed_at_not_null'} + = check_box_tag "q[completed_at_not_null]", 1, true = t(:show_only_complete_orders) .field = label_tag nil, t(:shipping_method) - %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': 'q.shipping_method_id'}) + = select_tag(: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: "primary", "data-controller": "tom-select" }) .field-block.alpha.eight.columns = label_tag nil, t(:distributors) - %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'}) + = 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: "fullwidth", multiple: true, data: { controller: "tom-select", "tom-select-options-value": { plugins: ['remove_button'], maxItems: nil } }}) .field-block.omega.eight.columns = label_tag nil, t(:order_cycles) - %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'}) + = 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: "fullwidth", multiple: true, data: { controller: "tom-select", "tom-select-options-value": { plugins: ['remove_button'], maxItems: nil } }}) .clearfix .actions.filter-actions - %a.button.icon-search{'ng-click' => 'fetchResults()'} + %button{type: "submit", class: "button"} + %i.icon-search = t(:filter_results) - %a.button{'ng-click' => 'clearFilters()', "id": "clear_filters_button"} + %button{"id": "clear_filters_button", type: "button", "data-controller": "search", "data-action": "click->search#reset" } = t(:clear_filters) diff --git a/app/views/spree/admin/orders/_table.html.haml b/app/views/spree/admin/orders/_table.html.haml new file mode 100644 index 0000000000..f15cc261d1 --- /dev/null +++ b/app/views/spree/admin/orders/_table.html.haml @@ -0,0 +1,30 @@ +.row.index-controls + %div{ style: "display: flex; justify-content: space-between;" } + = render partial: "bulk_actions" + .per-page.right + = render partial: 'admin/shared/stimulus_page_controls', locals: { pagy: pagy } + +%table#listing_orders.index.responsive{width: "100%" } + %colgroup + %col{style: "width: 3%"} + %thead + %tr + %th + %input#selectAll{ type: 'checkbox', data: { "checked-target": "all", action: "change->checked#toggleAll" } } + %th + = t(:products_distributor) + + - columns = ['completed_at', 'number', 'state', 'payment_state', 'shipment_state', 'email', 'bill_address_lastname', 'total'] + + = render partial: "spree/admin/shared/stimulus_sortable_header", collection: columns, as: :column, + locals: { sorted: params.dig(:q, :s), default: "completed_at desc" } + + %th.actions + %tbody + = render partial: "table_row", collection: orders, as: :order + + +- if pagy&.count&.positive? + = render partial: "admin/shared/stimulus_pagination", locals: { pagy: pagy } +- else + .no-objects-found= t('spree.admin.orders.index.no_orders_found') diff --git a/app/views/spree/admin/orders/_table_row.html.haml b/app/views/spree/admin/orders/_table_row.html.haml new file mode 100644 index 0000000000..58c09d4bed --- /dev/null +++ b/app/views/spree/admin/orders/_table_row.html.haml @@ -0,0 +1,48 @@ +%tr{ id: dom_id(order), class: "state-#{order.state}" } + %td.align-center + %input{type: 'checkbox', value: order.id, name: 'order_ids[]', "data-checked-target": "checkbox", "data-action": "change->checked#toggleCheckbox" } + %td.align-center + = order.distributor.name + %td.align-center + = I18n.l(order.completed_at, format: '%B %d, %Y') if order.completed_at + %td + %a{ href: edit_admin_order_path(order) } + = order.number + - if order.special_instructions + %div + %br + %span.icon-warning-sign{'ofn-with-tip' => order.special_instructions.to_s } + = t('spree.admin.orders.index.note') + %td.align-center + %span.state{ class: order.state.to_s } + = t('js.admin.orders.order_state.' + order.state.to_s) + %td.align-center + - if order.payment_state + %span.state{class: 'order.payment_state'} + %a{href: spree.admin_order_payments_path(order) } + = t('js.admin.orders.payment_states.' + order.payment_state.to_s) + - if order.display_outstanding_balance + %span + = "(#{order.display_outstanding_balance})" + %td.align-center + - if order.shipment_state + %span.state{class: order.shipment_state.to_s} + = t('js.admin.orders.shipment_states.' + order.shipment_state.to_s) + %td + %a{ href: "mailto:#{order.email}", target: "_blank" } + = order.email + %td + = order.bill_address.full_name + %td.align-center + %span + = order.display_total + %td.actions + .flex + %div.row-loading-icons + - if local_assigns[:success] + %i.success.icon-ok-sign{"data-controller": "ephemeral"} + %a.icon_link.with-tip.icon-edit.no-text{href: edit_admin_order_path(order), 'ofn-with-tip' => t('spree.admin.orders.index.edit')} + - if order.ready_to_ship? + %button.icon-road.icon_link.with-tip.no-text{rel: 'nofollow', 'ofn-with-tip' => t('spree.admin.orders.index.ship')} + - if order.payment_required? && order.pending_payments.reject(&:requires_authorization?).any? + %button.icon-capture.icon_link.no-text{rel: 'nofollow', 'ofn-with-tip' => t('spree.admin.orders.index.capture')} diff --git a/app/views/spree/admin/orders/index.html.haml b/app/views/spree/admin/orders/index.html.haml index 5c55106a03..d46af52b9a 100644 --- a/app/views/spree/admin/orders/index.html.haml +++ b/app/views/spree/admin/orders/index.html.haml @@ -9,120 +9,14 @@ = render partial: 'spree/admin/shared/order_sub_menu' -- content_for :main_ng_app_name do - = "ofn.admin" - -- content_for :main_ng_ctrl_name do - = "ordersCtrl" - - content_for :table_filter_title do = t(:search) - content_for :table_filter do = render partial: 'filters' -.row.index-controls{'ng-show' => '!RequestMonitor.loading && orders.length > 0'} - %div{style: "display: flex; justify-content: space-between;"} - .ofn-drop-down-with-prepend - .ofn-drop-down-prepend{"ng-class": "selected_orders.length == 0 ? 'disabled' : ''"} - {{ "spree.admin.orders.index.selected" | t:{count: selected_orders.length} }} - .ofn-drop-down{"ng-class": "selected_orders.length == 0 ? 'disabled' : ''"} - %span{ :class => 'icon-reorder' } - ="#{t('admin.actions')}".html_safe - %span{ 'ng-class' => "expanded && 'icon-caret-up' || !expanded && 'icon-caret-down'" } - %div.menu{ 'ng-show' => "expanded" } - %div.menu_item - %span.name{ "data-controller": "modal-link", "data-action": "click->modal-link#open", "data-modal-link-target-value": "resend_confirmation" } - = t('.resend_confirmation') - - if Spree::Config[:enable_invoices?] - %div.menu_item - %span.name{ "data-controller": "modal-link", "data-action": "click->modal-link#open", "data-modal-link-target-value": "send_invoice" } - = t('.send_invoice') - %div.menu_item - %span.name.invoices-modal{'ng-controller' => 'bulkInvoiceCtrl', 'ng-click' => 'createBulkInvoice()' } - = t('.print_invoices') - %div.menu_item - %span.name{ "data-controller": "modal-link", "data-action": "click->modal-link#open", "data-modal-link-target-value": "cancel_orders" } - = t('.cancel_orders') - - = render partial: 'admin/shared/angular_per_page_controls', locals: { position: "right", model: "orders" } - -%table#listing_orders.index.responsive{width: "100%", 'ng-init' => 'initialise()', 'ng-show' => "!RequestMonitor.loading && orders.length > 0" } - %colgroup - %col{style: "width: 3%"} - %thead - %tr - %th - %input#selectAll{type: 'checkbox', 'ng-change' => 'toggleAll()', 'ng-model' => 'select_all'} - %th - = t(:products_distributor) - %th - %a{'ng-click' => "sortOptions.toggle('completed_at')"} - = t(:completed_at, scope: 'activerecord.attributes.spree/order') - %span{'ng-show' => "sorting == 'completed_at asc'"}= "▲".html_safe - %span{'ng-show' => "sorting == 'completed_at desc' || sorting === undefined"}= "▼".html_safe - - - ['number', 'state', 'payment_state', 'shipment_state', 'email', 'bill_address_lastname', 'total'].each do |column_name| - %th - = render partial: 'spree/admin/shared/sortable_header', locals: {column_name: column_name} - - %th.actions - %tbody - %tr{ng: {repeat: 'order in orders track by order.id', class: {even: "'even'", odd: "'odd'"}}, 'ng-class' => "{'state-{{order.state}}': true, 'row-loading': rowStatus[order.id] == 'loading'}"} - %td.align-center - %input{type: 'checkbox', 'ng-model' => 'checkboxes[order.id]', 'ng-change' => 'toggleSelection(order.id)', value: '{{order.id}}', name: 'order_ids[]'} - %td.align-center - {{order.distributor_name}} - %td.align-center - {{order.completed_at}} - %td - %a{'ng-href' => '{{order.edit_path}}'} - {{order.number}} - %div{'ng-if' => 'order.special_instructions'} - %br - %span.icon-warning-sign{'ofn-with-tip' => "{{order.special_instructions}}"} - = t('.note') - %td.align-center - %span.state{'ng-class' => 'order.state'} - {{'js.admin.orders.order_state.' + order.state | t}} - %td.align-center - %span.state{'ng-class' => 'order.payment_state', 'ng-if' => 'order.payment_state'} - %a{'ng-href' => '{{order.payments_path}}' } - {{'js.admin.orders.payment_states.' + order.payment_state | t}} - %span{'ng-if' => 'order.display_outstanding_balance'} - ({{order.display_outstanding_balance}}) - %td.align-center - %span.state{'ng-class' => 'order.shipment_state', 'ng-if' => 'order.shipment_state'} - {{'js.admin.orders.shipment_states.' + order.shipment_state | t}} - %td - %a{ ng: { href: "mailto:{{order.email}}" } } - {{order.email}} - %td - {{order.full_name}} - %td.align-center - %span{'ng-bind-html' => 'order.display_total'} - %td.actions - %div.row-loading-icons - %div{ng: {show: 'rowStatus[order.id] == "loading"', cloak: true}, style: "width: 30px; height: 30px;"} - = render partial: "components/spinner" - %i.success.icon-ok-sign{ng: {show: 'rowStatus[order.id] == "success"'} } - %i.error.icon-remove-sign.with-tip{ng: {show: 'rowStatus[order.id] == "error"'}, 'ofn-with-tip' => t('.order_not_updated')} - %a.icon_link.with-tip.icon-edit.no-text{'ng-href' => '{{order.edit_path}}', 'data-action' => 'edit', 'ofn-with-tip' => t('.edit')} - %div{'ng-if' => 'order.ready_to_ship'} - %button.icon-road.icon_link.with-tip.no-text{'ng-click' => 'shipOrder(order)', rel: 'nofollow', 'ofn-with-tip' => t('.ship')} - %div{'ng-if' => 'order.ready_to_capture'} - %button.icon-capture.icon_link.no-text{'ng-click' => 'capturePayment(order)', rel: 'nofollow', 'ofn-with-tip' => t('.capture')} - -.sixteen.columns.alpha#loading{ 'ng-show' => 'RequestMonitor.loading' } - = render partial: "components/admin_spinner" - %h1 - = t('.loading') - -%div{'ng-show' => "!RequestMonitor.loading && orders.length > 0" } - = render partial: 'admin/shared/angular_pagination' - -.no-objects-found{'ng-show' => "!RequestMonitor.loading && orders.length == 0"} - = t('.no_orders_found') +#orders-index{"data-controller": "search checked"} + = render partial: "table", locals: { pagy: @pagy, orders: @orders } = render 'spree/admin/shared/custom-confirm' diff --git a/app/views/spree/admin/shared/_stimulus_sortable_header.html.haml b/app/views/spree/admin/shared/_stimulus_sortable_header.html.haml new file mode 100644 index 0000000000..46443016ce --- /dev/null +++ b/app/views/spree/admin/shared/_stimulus_sortable_header.html.haml @@ -0,0 +1,8 @@ +%th + %a{ "data-action": "click->search#changeSorting", "data-column": "#{column}", "data-current": sorted.to_s } + = t("spree.admin.shared.sortable_header.#{column.to_s}") + + - if sorted == "#{column} asc" || sorted.blank? && local_assigns[:default] == "#{column} asc" + = "▲".html_safe + - if sorted == "#{column} desc" || sorted.blank? && local_assigns[:default] == "#{column} desc" + = "▼".html_safe diff --git a/app/views/spree/admin/shared/_tabs.html.haml b/app/views/spree/admin/shared/_tabs.html.haml index 91efb77da3..d84e381c57 100644 --- a/app/views/spree/admin/shared/_tabs.html.haml +++ b/app/views/spree/admin/shared/_tabs.html.haml @@ -1,7 +1,7 @@ = tab :overview, label: 'dashboard', url: spree.admin_dashboard_path, icon: 'icon-dashboard' = tab :products, :properties, :inventory, :product_import, :images, :variants, :product_properties, :group_buy_options, :seo, url: admin_products_path, icon: 'icon-th-large' = tab :order_cycles, url: main_app.admin_order_cycles_path, icon: 'icon-refresh' -= tab :orders, :subscriptions, :customer_details, :adjustments, :payments, :return_authorizations, url: admin_orders_path('q[s]' => 'completed_at desc'), icon: 'icon-shopping-cart' += tab :orders, :subscriptions, :customer_details, :adjustments, :payments, :return_authorizations, url: admin_orders_path, icon: 'icon-shopping-cart' = tab :reports, url: main_app.admin_reports_path, icon: 'icon-file' = tab :general_settings, :mail_methods, :tax_categories, :tax_rates, :tax_settings, :zones, :countries, :states, :payment_methods, :taxonomies, :shipping_methods, :shipping_categories, :enterprise_fees, :contents, :invoice_settings, :matomo_settings, :stripe_connect_settings, label: 'configuration', icon: 'icon-wrench', url: edit_admin_general_settings_path = tab :enterprises, :enterprise_relationships, :vouchers, :oidc_settings, url: main_app.admin_enterprises_path diff --git a/app/webpacker/controllers/cancel_orders_controller.js b/app/webpacker/controllers/cancel_orders_controller.js index d1872feb07..a017edda2b 100644 --- a/app/webpacker/controllers/cancel_orders_controller.js +++ b/app/webpacker/controllers/cancel_orders_controller.js @@ -6,9 +6,7 @@ export default class extends ApplicationController { } confirm() { - const send_cancellation_email = document.querySelector( - "#send_cancellation_email" - ).checked; + const send_cancellation_email = document.querySelector("#send_cancellation_email").checked; const restock_items = document.querySelector("#restock_items").checked; const order_ids = []; @@ -23,8 +21,7 @@ export default class extends ApplicationController { send_cancellation_email: send_cancellation_email, restock_items: restock_items, }; - this.stimulate("CancelOrdersReflex#confirm", params).then(() => - window.location.reload() - ); + + this.stimulate("CancelOrdersReflex#confirm", params); } } diff --git a/app/webpacker/controllers/checked_controller.js b/app/webpacker/controllers/checked_controller.js index 30a00aee79..66ab32ae0c 100644 --- a/app/webpacker/controllers/checked_controller.js +++ b/app/webpacker/controllers/checked_controller.js @@ -1,19 +1,39 @@ import { Controller } from "stimulus"; export default class extends Controller { - static targets = ["all", "checkbox"]; + static targets = ["all", "checkbox", "disable"]; connect() { - this.toggleCheckbox() + this.toggleCheckbox(); } toggleAll() { - this.checkboxTargets.forEach(checkbox => { + this.checkboxTargets.forEach((checkbox) => { checkbox.checked = this.allTarget.checked; }); + this.#toggleDisabled(); } toggleCheckbox() { - this.allTarget.checked = this.checkboxTargets.every(checkbox => checkbox.checked); + this.allTarget.checked = this.checkboxTargets.every((checkbox) => checkbox.checked); + this.#toggleDisabled(); + } + + // private + + #toggleDisabled() { + if (!this.hasDisableTarget) { + return; + } + + if (this.#noneChecked()) { + this.disableTargets.forEach((element) => element.classList.add("disabled")); + } else { + this.disableTargets.forEach((element) => element.classList.remove("disabled")); + } + } + + #noneChecked() { + return this.checkboxTargets.every((checkbox) => !checkbox.checked); } } diff --git a/app/webpacker/controllers/ephemeral_controller.js b/app/webpacker/controllers/ephemeral_controller.js new file mode 100644 index 0000000000..bba181a733 --- /dev/null +++ b/app/webpacker/controllers/ephemeral_controller.js @@ -0,0 +1,16 @@ +import { Controller } from "stimulus"; + +export default class extends Controller { + connect() { + setTimeout(this.fadeout, 1500); + } + + fadeout = () => { + this.element.classList.add("animate-hide-500"); + setTimeout(this.remove, 500); + }; + + remove = () => { + this.element.remove(); + }; +} diff --git a/app/webpacker/controllers/search_controller.js b/app/webpacker/controllers/search_controller.js new file mode 100644 index 0000000000..fdcc13439b --- /dev/null +++ b/app/webpacker/controllers/search_controller.js @@ -0,0 +1,68 @@ +import { Controller } from "stimulus"; + +export default class extends Controller { + static targets = ["form"]; + + connect() { + this.#setup(); + } + + changePage(event) { + this.page.value = event.target.dataset.page; + this.submitSearch(); + this.page.value = 1; + } + + changePerPage(event) { + this.per_page.value = parseInt(event.target.value); + this.submitSearch(); + } + + changeSorting(event) { + let current = event.target.dataset.current; + let column = event.target.dataset.column; + + this.sort.value = current === `${column} asc` ? `${column} desc` : `${column} asc`; + + this.submitSearch(); + } + + submitSearch() { + this.form.requestSubmit(); + } + + reset() { + this.clearForm(); + this.submitSearch(); + } + + clearForm() { + this.form.reset(); + this.#clearCustomElements(); + if (this.page) this.page.value = 1; + if (this.sort) this.sort.value = this.sort.dataset.default; + } + + // private + + #setup() { + if (this.hasFormTarget) { + this.form = this.formTarget; + this.form.controller = this; + } else { + this.form = document.querySelector("form[data-search-target=form]"); + } + + this.page = this.form.querySelector(".page"); + this.per_page = this.form.querySelector(".per-page"); + this.sort = this.form.querySelector(".sort"); + } + + #clearCustomElements() { + window.dispatchEvent(new CustomEvent("flatpickr:clear")); + + this.form.querySelectorAll(".tomselected").forEach((select) => { + select.tomselect?.clear(); + }); + } +} diff --git a/app/webpacker/css/admin/animations.scss b/app/webpacker/css/admin/animations.scss index bbc766ecfd..7b6e451078 100644 --- a/app/webpacker/css/admin/animations.scss +++ b/app/webpacker/css/admin/animations.scss @@ -10,6 +10,16 @@ } } +@keyframes fade-out-hide { + 0% {opacity: 1; visibility: visible;} + 99% {opacity: 0; visibility: visible;} + 100% {opacity: 0; visibility: hidden;} +} + +.animate-hide-500 { + animation: fade-out-hide 0.5s; +} + // @-webkit-keyframes slideOutDown // 0% // -webkit-transform: translateY(0) diff --git a/app/webpacker/css/admin/components/buttons.scss b/app/webpacker/css/admin/components/buttons.scss index 1a59a4fff2..5de746ea48 100644 --- a/app/webpacker/css/admin/components/buttons.scss +++ b/app/webpacker/css/admin/components/buttons.scss @@ -1,6 +1,6 @@ input[type="submit"], input[type="button"], -button, +button:not(.plain), .button { position: relative; cursor: pointer; diff --git a/app/webpacker/css/admin/components/per_page_controls.scss b/app/webpacker/css/admin/components/per_page_controls.scss index 1b4e2dc3f9..f843bb1674 100644 --- a/app/webpacker/css/admin/components/per_page_controls.scss +++ b/app/webpacker/css/admin/components/per_page_controls.scss @@ -10,6 +10,10 @@ margin-right: 1em; margin-left: 0; } + + .ts-control > * { + padding-right: 2.75em; + } } .per-page-feedback { diff --git a/app/webpacker/css/admin/components/tom_select.scss b/app/webpacker/css/admin/components/tom_select.scss index 2fb878f4bd..75684615fd 100644 --- a/app/webpacker/css/admin/components/tom_select.scss +++ b/app/webpacker/css/admin/components/tom_select.scss @@ -1,5 +1,14 @@ +.ts-wrapper { + min-height: initial; +} + .ts-dropdown { margin-top: 0; + + .option { + min-height: 2.25em; + display: block; + } } .ts-wrapper.single .ts-control, @@ -45,6 +54,10 @@ } } +.ts-wrapper .select-multiple { + cursor: pointer; +} + .ts-wrapper.dropdown-active.primary .ts-control { background-color: $spree-green; border-color: $spree-green; diff --git a/app/webpacker/css/admin/dropdown.scss b/app/webpacker/css/admin/dropdown.scss index 85fd5c7416..d3eab51beb 100644 --- a/app/webpacker/css/admin/dropdown.scss +++ b/app/webpacker/css/admin/dropdown.scss @@ -6,130 +6,156 @@ margin-left: 3px; } -.ofn-drop-down:hover, .ofn-drop-down.expanded { - border: 1px solid #adadad; - color: #575757; +.ofn-drop-down { + .dropdown-content { + display: none; + } + + .toggle-off { + display: none; + } + + &:active:not(.disabled), + &:focus:not(.disabled) { + .dropdown-content { + display: inline-block; + } + + .toggle-off { + display: inline-block; + } + + .toggle-on { + display: none; + } + } +} + +.ofn-drop-down:hover, +.ofn-drop-down.expanded { + border: 1px solid #adadad; + color: #575757; } @mixin ofn-drop-down-style { - padding: 7px 15px; - border-radius: 3px; - border: 1px solid #d4d4d4; - background-color: #f5f5f5; - display: block; - color: #828282; - cursor: pointer; - -moz-user-select: none; - -khtml-user-select: none; - -webkit-user-select: none; - -ms-user-select: none; - user-select: none; - text-align: center; + padding: 7px 15px; + border-radius: 3px; + border: 1px solid #d4d4d4; + background-color: #f5f5f5; + display: block; + color: #828282; + cursor: pointer; + -moz-user-select: none; + -khtml-user-select: none; + -webkit-user-select: none; + -ms-user-select: none; + user-select: none; + text-align: center; margin-right: 10px; - &.disabled { - opacity: 0.5; - - &:hover { - cursor: default; - border-color: #d4d4d4; - color: #828282; - } - } + &.disabled { + opacity: 0.5; + + &:hover { + cursor: default; + border-color: #d4d4d4; + color: #828282; + } + } } .ofn-drop-down-with-prepend { - display: flex; + display: flex; - &.right { - float: right; + &.right { + float: right; + } - } + .ofn-drop-down { + border-top-left-radius: 0; + border-bottom-left-radius: 0; + } - .ofn-drop-down { - border-top-left-radius: 0; - border-bottom-left-radius: 0; - } + .ofn-drop-down-prepend { + @include ofn-drop-down-style; - .ofn-drop-down-prepend { - @include ofn-drop-down-style; - - border-right: none; - margin-left: 0; - margin-right: 0; - border-top-right-radius: 0; - border-bottom-right-radius: 0; - cursor: default; - } + border-right: none; + margin-left: 0; + margin-right: 0; + border-top-right-radius: 0; + border-bottom-right-radius: 0; + cursor: default; + } } .ofn-drop-down { - @include ofn-drop-down-style; + @include ofn-drop-down-style; - position: relative; - float: left; + position: relative; + float: left; - &.right { - float: right; + &.right { + float: right; margin-right: 0px; margin-left: 10px; - } + } - &:hover, &.expanded { + &:hover, + &.expanded { border: 1px solid #adadad; color: #575757; } - > span { - width: auto; - text-transform: uppercase; - font-size: 85%; - font-weight: 600; - } + > span { + width: auto; + text-transform: uppercase; + font-size: 85%; + font-weight: 600; + } - .menu { - margin-top: 1px; - position: absolute; - float: none; - top:100%; - left: 0px; - padding: 5px 0px; - border: 1px solid #adadad; - background-color: #ffffff; - box-shadow: 1px 3px 10px #888888; - z-index: 100; + .menu { + margin-top: 1px; + position: absolute; + float: none; + top: 100%; + left: 0px; + padding: 5px 0px; + border: 1px solid #adadad; + background-color: #ffffff; + box-shadow: 1px 3px 10px #888888; + z-index: 100; white-space: nowrap; - .filter { - padding-left: 5px; - padding-right: 5px; - position: relative; - - > input[type="text"] { - border: 1px solid rgba(18, 18, 18, 0.1); - width: 100%; - padding-left: 30px; - padding-top: 10px; - padding-bottom: 10px; - font-size: 13px; - color:#454545; - } + .filter { + padding-left: 5px; + padding-right: 5px; + position: relative; - &:after { - content: "\f002"; - font-family: FontAwesome; - position: absolute; - left: 15px; - top: 13px; - color:#454545; - } - } + > input[type="text"] { + border: 1px solid rgba(18, 18, 18, 0.1); + width: 100%; + padding-left: 30px; + padding-top: 10px; + padding-bottom: 10px; + font-size: 13px; + color: #454545; + } - .menu_item { - margin: 0px; - padding: 2px 10px; - color: #454545; - text-align: left; + &:after { + content: "\f002"; + font-family: FontAwesome; + position: absolute; + left: 15px; + top: 13px; + color: #454545; + } + } + + .menu_item { + margin: 0px; + padding: 2px 10px; + color: #454545; + text-align: left; display: block; .check { @@ -146,78 +172,79 @@ padding: 0px 15px 0px 0px; } - &.selected{ + &.selected { .check:before { content: "\2713"; } } - &.hidden { - display: none; - } - } + &.hidden { + display: none; + } + } - .menu_item:hover { - background-color: #ededed; - } - } + .menu_item:hover { + background-color: #ededed; + } + } } .ofn-drop-down-v2 { - border: 1px solid $pale-blue; - background-color: white; - padding: 0px; - - &:hover { - border-color: $spree-blue; - } + border: 1px solid $pale-blue; + background-color: white; + padding: 0px; - .ofn-drop-down-label { - color: $color-3; - padding: 10px; - width: 235px; - display: flex; - justify-content: space-between; + &:hover { + border-color: $spree-blue; + } - &:hover { - color: $color-3; - } + .ofn-drop-down-label { + color: $color-3; + padding: 10px; + width: 235px; + display: flex; + justify-content: space-between; - .label { - padding-right: 10px; - } + &:hover { + color: $color-3; + } - .icon-caret-down, .icon-caret-up { - padding-right: 0px; - } - } + .label { + padding-right: 10px; + } - .menu { - width: 100%; - } + .icon-caret-down, + .icon-caret-up { + padding-right: 0px; + } + } - .menu_items { - max-height: 200px; - overflow-y: scroll; + .menu { + width: 100%; + } - .menu_item { - margin-bottom: 5px; - color: #454545; - font-weight: 400; - cursor: pointer; - padding-top: 4px; - padding-bottom: 5px; - text-transform: uppercase; - font-size: 85%; - } - } + .menu_items { + max-height: 200px; + overflow-y: scroll; + + .menu_item { + margin-bottom: 5px; + color: #454545; + font-weight: 400; + cursor: pointer; + padding-top: 4px; + padding-bottom: 5px; + text-transform: uppercase; + font-size: 85%; + } + } } .ofn-drop-down.ofn-drop-down-v2 { - // Add very specific styling here for components that are in transition: - // ie. the ones using the two classes above - .ofn-drop-down-label { - padding-top: 7px; - padding-bottom: 7px; - } + // Add very specific styling here for components that are in transition: + // ie. the ones using the two classes above + .ofn-drop-down-label { + padding-top: 7px; + padding-bottom: 7px; + } } diff --git a/app/webpacker/css/admin/shared/forms.scss b/app/webpacker/css/admin/shared/forms.scss index f2ef36e111..bd5274bced 100644 --- a/app/webpacker/css/admin/shared/forms.scss +++ b/app/webpacker/css/admin/shared/forms.scss @@ -1,12 +1,12 @@ -$text-inputs: - "input[type=text], input[type=password], input[type=email], input[type=url], input[type=tel]"; +$text-inputs: "input[type=text], input[type=password], input[type=email], input[type=url], input[type=tel]"; #{$text-inputs}, input[type="date"], input[type="datetime"], input[type="time"], input[type="number"], -textarea, fieldset { +textarea, +fieldset { @include border-radius($border-radius); padding: 7px 10px; border: 1px solid $color-txt-brd; @@ -48,7 +48,9 @@ label { } } -.label-block label { display: block } +.label-block label { + display: block; +} span.info { font-style: italic; @@ -63,7 +65,7 @@ span.info { padding: 10px 0; &.checkbox { - min-height: 73px; + min-height: 70px; input[type="checkbox"] { display: inline-block; @@ -85,7 +87,6 @@ span.info { display: inline-block; padding-right: 10px; - label { font-weight: normal; text-transform: none; @@ -171,14 +172,18 @@ fieldset { display: inline-block; } - button, .button, input[type="submit"], input[type="button"], span.or { + button, + .button, + input[type="submit"], + input[type="button"], + span.or { @include border-radius($border-radius); -webkit-box-shadow: 0 0 0 15px $color-1; - -moz-box-shadow: 0 0 0 15px $color-1; - -ms-box-shadow: 0 0 0 15px $color-1; - -o-box-shadow: 0 0 0 15px $color-1; - box-shadow: 0 0 0 15px $color-1; + -moz-box-shadow: 0 0 0 15px $color-1; + -ms-box-shadow: 0 0 0 15px $color-1; + -o-box-shadow: 0 0 0 15px $color-1; + box-shadow: 0 0 0 15px $color-1; &:hover { border-color: $color-1; @@ -197,10 +202,10 @@ fieldset { position: relative; -webkit-box-shadow: 0 0 0 5px $color-1; - -moz-box-shadow: 0 0 0 5px $color-1; - -ms-box-shadow: 0 0 0 5px $color-1; - -o-box-shadow: 0 0 0 5px $color-1; - box-shadow: 0 0 0 5px $color-1; + -moz-box-shadow: 0 0 0 5px $color-1; + -ms-box-shadow: 0 0 0 5px $color-1; + -o-box-shadow: 0 0 0 5px $color-1; + box-shadow: 0 0 0 5px $color-1; } } @@ -210,7 +215,8 @@ fieldset { display: table; width: 100%; - label, input { + label, + input { display: table-cell !important; } input { @@ -219,7 +225,7 @@ fieldset { &.checkbox { input { - width: auto !important + width: auto !important; } } } @@ -247,11 +253,12 @@ select { align-items: center; margin-top: 3px; - input, label { + input, + label { cursor: pointer; } label { margin: 0; - padding-left: .4rem; + padding-left: 0.4rem; } } diff --git a/config/locales/en.yml b/config/locales/en.yml index 8e20e22529..b825c04849 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -4285,6 +4285,7 @@ See the %{link} to find out more about %{sitename}'s features and to start using sortable_header: name: "Name" number: "Number" + completed_at: "Completed At" state: "State" payment_state: "Payment State" shipment_state: "Shipment State"