diff --git a/app/assets/javascripts/admin/index_utils/services/columns.js.coffee b/app/assets/javascripts/admin/index_utils/services/columns.js.coffee index 2d7abfcab7..aeb1a3e3b6 100644 --- a/app/assets/javascripts/admin/index_utils/services/columns.js.coffee +++ b/app/assets/javascripts/admin/index_utils/services/columns.js.coffee @@ -31,7 +31,7 @@ angular.module("admin.indexUtils").factory 'Columns', ($rootScope, $http, $injec savePreferences: (action_name) => $http method: "PUT" - url: "/admin/column_preferences/bulk_update" + url: "/admin/column_preferences/bulk_update.json" data: action_name: action_name column_preferences: (preference for column_name, preference of @columns) diff --git a/app/assets/javascripts/templates/admin/columns_dropdown.html.haml b/app/assets/javascripts/templates/admin/columns_dropdown.html.haml index 61f42974db..7895d7070e 100644 --- a/app/assets/javascripts/templates/admin/columns_dropdown.html.haml +++ b/app/assets/javascripts/templates/admin/columns_dropdown.html.haml @@ -5,8 +5,9 @@ %div.menu{ 'ng-show' => "expanded" } .menu_items .menu_item{ "ng-repeat": "column in columns", "ng-click": "toggle(column);" } - %input.redesigned-input{ type: "checkbox", "ng-checked": "column.visible" } - {{ column.name }} + %input{ type: "checkbox", "ng-checked": "column.visible" } + %span + {{ column.name }} %hr %div.menu_item.text-center %input.fullwidth.orange{ type: "button", "ng-value": "saved() ? 'Saved': 'Saving'", "ng-show": "saved() || saving", "ng-disabled": "saved()" } diff --git a/app/components/multiple_checked_select_component/multiple_checked_select_component.html.haml b/app/components/multiple_checked_select_component/multiple_checked_select_component.html.haml index 8dedce27ca..c9c0abebec 100644 --- a/app/components/multiple_checked_select_component/multiple_checked_select_component.html.haml +++ b/app/components/multiple_checked_select_component/multiple_checked_select_component.html.haml @@ -9,5 +9,5 @@ %div.menu_items - @options.each do |option| %label.menu_item{ "data-multiple-checked-select-target": "option", "data-value": option[1], "data-label": option[0] } - %input.redesigned-input{ type: "checkbox", checked: @selected.include?(option[1]), name: "#{@name}[]", value: option[1] } - = option[0] + %input{ type: "checkbox", checked: @selected.include?(option[1]), name: "#{@name}[]", value: option[1] } + %span= option[0] diff --git a/app/controllers/admin/column_preferences_controller.rb b/app/controllers/admin/column_preferences_controller.rb index fcf627f1b7..5403a9a5ee 100644 --- a/app/controllers/admin/column_preferences_controller.rb +++ b/app/controllers/admin/column_preferences_controller.rb @@ -4,17 +4,25 @@ module Admin class ColumnPreferencesController < Admin::ResourceController before_action :load_collection, only: [:bulk_update] - respond_to :json - def bulk_update @cp_set.collection.each { |cp| authorize! :bulk_update, cp } - if @cp_set.save - render json: @cp_set.collection, each_serializer: Api::Admin::ColumnPreferenceSerializer - elsif @cp_set.errors.present? - render json: { errors: @cp_set.errors }, status: :bad_request - else - render body: nil, status: :internal_server_error + respond_to do |format| + if @cp_set.save + format.json { + render json: @cp_set.collection, each_serializer: Api::Admin::ColumnPreferenceSerializer + } + format.turbo_stream { + flash.now[:success] = t('.success') + render :bulk_update, locals: { action: permitted_params[:action_name] } + } + else + format.json { render json: { errors: @cp_set.errors }, status: :bad_request } + format.turbo_stream { + flash.now[:error] = @cp_set.errors.full_messages.to_sentence + render :bulk_update, locals: { action: permitted_params[:action_name] } + } + end end end @@ -28,11 +36,26 @@ module Admin end def load_collection - collection_hash = Hash[permitted_params[:column_preferences]. - each_with_index.map { |cp, i| [i, cp] }] - collection_hash.select!{ |_i, cp| cp[:action_name] == permitted_params[:action_name] } - @cp_set = Sets::ColumnPreferenceSet.new(@column_preferences, - collection_attributes: collection_hash) + collection_attributes = nil + + respond_to do |format| + format.json do + collection_attributes = Hash[permitted_params[:column_preferences]. + each_with_index.map { |cp, i| [i, cp] }] + collection_attributes.select!{ |_i, cp| + cp[:action_name] == permitted_params[:action_name] + } + end + format.all do + # Inject action name and user ID for each column_preference + collection_attributes = permitted_params[:column_preferences].to_h.each_value { |cp| + cp[:action_name] = permitted_params[:action_name] + cp[:user_id] = spree_current_user.id + } + end + end + + @cp_set = Sets::ColumnPreferenceSet.new(@column_preferences, collection_attributes:) end def collection diff --git a/app/views/admin/column_preferences/_form.html.haml b/app/views/admin/column_preferences/_form.html.haml new file mode 100644 index 0000000000..6296444b02 --- /dev/null +++ b/app/views/admin/column_preferences/_form.html.haml @@ -0,0 +1,23 @@ += form_with url: bulk_update_admin_column_preferences_path, method: :put, + id: :bulk_admin_column_preferences_form, class: "column-preferences", + html: { 'data-controller': "column-preferences" } do |f| + = hidden_field_tag :action_name, action + + / DC: this makes my Chrome DevTools crash when inspecting the
element. If problem continues, we need to use a different method. + %details.ofn-drop-down.ofn-drop-down-v2.right{ 'data-controller': "dropdown" } + %summary.ofn-drop-down-label + = t('admin.columns') + %span.icon-caret + + .menu + .menu_items + - ColumnPreference.for(spree_current_user, action).each_with_index do |column_preference, index| + = f.fields_for("column_preferences", column_preference, index:) do |cp_form| + = cp_form.hidden_field :id + = cp_form.hidden_field :column_name + %label.menu_item + = cp_form.check_box :visible, 'data-column-name': column_preference.column_name + %span= t("admin.products_page.columns." + column_preference.column_name) + + .actions + = f.submit t('admin.column_save_as_default'), class: "secondary fullwidth" diff --git a/app/views/admin/column_preferences/bulk_update.turbo_stream.haml b/app/views/admin/column_preferences/bulk_update.turbo_stream.haml new file mode 100644 index 0000000000..70836e4657 --- /dev/null +++ b/app/views/admin/column_preferences/bulk_update.turbo_stream.haml @@ -0,0 +1,3 @@ += turbo_stream.replace "bulk_admin_column_preferences_form" do + = render partial: "admin/shared/flashes", locals: { flashes: flash } if defined? flash + = render partial: 'form', locals: { action: } diff --git a/app/views/admin/products_v3/_product_row.html.haml b/app/views/admin/products_v3/_product_row.html.haml index d009b544ed..48636dc92e 100644 --- a/app/views/admin/products_v3/_product_row.html.haml +++ b/app/views/admin/products_v3/_product_row.html.haml @@ -1,13 +1,13 @@ -%td.with-image{ id: "image-#{product.id}" } +%td.col-image.with-image{ id: "image-#{product.id}" } = render partial: "product_image", locals: { product: } -%td.field.align-left.header.naked_inputs +%td.col-name.field.align-left.header.naked_inputs = f.hidden_field :id = f.text_field :name, 'aria-label': t('admin.products_page.columns.name') = error_message_on product, :name -%td.field.naked_inputs +%td.col-sku.field.naked_inputs = f.text_field :sku, 'aria-label': t('admin.products_page.columns.sku') = error_message_on product, :sku -%td.field.naked_inputs{ 'data-controller': 'toggle-control', 'data-toggle-control-match-value': 'items' } +%td.col-unit_scale.field.naked_inputs{ 'data-controller': 'toggle-control', 'data-toggle-control-match-value': 'items' } = f.hidden_field :variant_unit = f.hidden_field :variant_unit_scale = f.select :variant_unit_with_scale, @@ -19,23 +19,23 @@ .field = f.text_field :variant_unit_name, 'aria-label': t('items'), 'data-toggle-control-target': 'control', style: (product.variant_unit == "items" ? "" : "display: none") = error_message_on product, :variant_unit_name, 'data-toggle-control-target': 'control' -%td.align-right +%td.col-unit.align-right -# empty -%td.align-right +%td.col-price.align-right -# empty -%td.align-right +%td.col-on_hand.align-right -# empty -%td.naked_inputs +%td.col-producer.naked_inputs = render(SearchableDropdownComponent.new(form: f, name: :supplier_id, aria_label: t('.producer_field_name'), options: producer_options, selected_option: product.supplier_id, placeholder_value: t('admin.products_v3.filters.search_for_producers'))) -%td.align-left +%td.col-category.align-left -# empty -%td.align-left -%td.align-left +%td.col-tax_category.align-left +%td.col-inherits_properties.align-left .content= product.inherits_properties ? 'YES' : 'NO' #TODO: consider using https://github.com/RST-J/human_attribute_values, else use I18n.t (also below) %td.align-right = render(VerticalEllipsisMenu::Component.new) do diff --git a/app/views/admin/products_v3/_sort.html.haml b/app/views/admin/products_v3/_sort.html.haml index f53eec4e76..6749b3b07a 100644 --- a/app/views/admin/products_v3/_sort.html.haml +++ b/app/views/admin/products_v3/_sort.html.haml @@ -1,5 +1,5 @@ #sort - %div + %div.pagination-description - if pagy.present? = t(".pagination.total_html", total: pagy.count, from: pagy.from, to: pagy.to) @@ -13,3 +13,6 @@ options_for_select([15, 25, 50, 100].collect{|i| [t('.pagination.per_page.per_page', num: i), i]}, pagy&.items), class: "no-input per-page", data: { controller: "tom-select search", action: "change->search#changePerPage", "tom-select-options-value": '{ "plugins": [] }'} + + / Columns dropdown + = render partial: "admin/column_preferences/form", locals: { action: "products_v3_index" } diff --git a/app/views/admin/products_v3/_table.html.haml b/app/views/admin/products_v3/_table.html.haml index 464130d181..0b8cc98b79 100644 --- a/app/views/admin/products_v3/_table.html.haml +++ b/app/views/admin/products_v3/_table.html.haml @@ -13,20 +13,21 @@ = hidden_field_tag :producer_id, @producer_id = hidden_field_tag :category_id, @category_id - %table.products + %table.products{ 'data-column-preferences-target': "table" } %colgroup - %col{ width:"56" }= # Img (size + padding) - %col= # (grow to fill) Name - %col{ width:"5%"} - %col{ width:"8%"} - %col{ width:"8%"} - %col{ width:"5%"} - %col{ width:"10%"} - %col{ width:"15%"}= # Producer - %col{ width:"8%"} - %col{ width:"8%"} - %col{ width:"8%"} - %col{ width:"8%"}= # Actions + -# The `min-width` property works in Chrome but not Firefox so is considered progressive enhancement. + %col.col-image{ width:"56px" }= # (image size + padding) + %col.col-name{ style:"min-width: 6em" }= # (grow to fill) + %col.col-sku{ width:"8%", style:"min-width: 6em" } + %col.col-unit_scale{ width:"8%" } + %col.col-unit{ width:"8%" } + %col.col-price{ width:"5%", style:"min-width: 5em" } + %col.col-on_hand{ width:"10%"} + %col.col-producer{ style:"min-width: 6em" }= # (grow to fill) + %col.col-category{ width:"8%" } + %col.col-tax_category{ width:"8%" } + %col.col-inherits_properties{ width:"5%" } + %col{ width:"5%", style:"min-width: 3em"}= # Actions %thead %tr %td.form-actions-wrapper{ colspan: 12 } @@ -48,18 +49,18 @@ = t('.reset') = form.submit t('.save'), class: "medium" %tr - %th.align-left= # image + %th.col-image.align-left= # image = render partial: 'spree/admin/shared/stimulus_sortable_header', locals: { column: :name, sorted: params.dig(:q, :s), default: 'name asc' } - %th.align-left.with-input= t('admin.products_page.columns.sku') - %th.align-left.with-input= t('admin.products_page.columns.unit_scale') - %th.align-left.with-input= t('admin.products_page.columns.unit') - %th.align-left.with-input= t('admin.products_page.columns.price') - %th.align-left.with-input= t('admin.products_page.columns.on_hand') - %th.align-left= t('admin.products_page.columns.producer') - %th.align-left= t('admin.products_page.columns.category') - %th.align-left= t('admin.products_page.columns.tax_category') - %th.align-left= t('admin.products_page.columns.inherits_properties') + %th.align-left.col-sku.with-input= t('admin.products_page.columns.sku') + %th.align-left.col-unit_scale.with-input= t('admin.products_page.columns.unit_scale') + %th.align-left.col-unit.with-input= t('admin.products_page.columns.unit') + %th.align-left.col-price.with-input= t('admin.products_page.columns.price') + %th.align-left.col-on_hand.with-input= t('admin.products_page.columns.on_hand') + %th.align-left.col-producer= t('admin.products_page.columns.producer') + %th.align-left.col-category= t('admin.products_page.columns.category') + %th.align-left.col-tax_category= t('admin.products_page.columns.tax_category') + %th.align-left.col-inherits_properties= t('admin.products_page.columns.inherits_properties') %th.align-right= t('admin.products_page.columns.actions') - products.each_with_index do |product, product_index| = form.fields_for("products", product, index: product_index) do |product_form| diff --git a/app/views/admin/products_v3/_variant_row.html.haml b/app/views/admin/products_v3/_variant_row.html.haml index 3b3b38ac54..311b833e04 100644 --- a/app/views/admin/products_v3/_variant_row.html.haml +++ b/app/views/admin/products_v3/_variant_row.html.haml @@ -1,15 +1,15 @@ -%td +%td.col-image -# empty -%td.field.naked_inputs +%td.col-name.field.naked_inputs = f.hidden_field :id = f.text_field :display_name, 'aria-label': t('admin.products_page.columns.name'), placeholder: variant.product.name = error_message_on variant, :display_name -%td.field.naked_inputs +%td.col-sku.field.naked_inputs = f.text_field :sku, 'aria-label': t('admin.products_page.columns.sku') = error_message_on variant, :sku -%td +%td.col-unit_scale -# empty -%td.field.popout{'data-controller': "popout", 'data-popout-update-display-value': "false"} +%td.col-unit.field.popout{'data-controller': "popout", 'data-popout-update-display-value': "false"} = f.button :unit_to_display, class: "popout__button", 'aria-label': t('admin.products_page.columns.unit'), 'data-popout-target': "button" do = variant.unit_to_display # Show the generated summary of unit values %div.popout__container{ style: 'display: none;', 'data-controller': 'toggle-control', 'data-popout-target': "dialog" } @@ -25,10 +25,10 @@ = f.label :display_as, t('admin.products_page.columns.display_as') = f.text_field :display_as, placeholder: VariantUnits::OptionValueNamer.new(variant).name = error_message_on variant, :unit_value -%td.field.naked_inputs +%td.col-price.field.naked_inputs = f.text_field :price, 'aria-label': t('admin.products_page.columns.price'), value: number_to_currency(variant.price, unit: '')&.strip # TODO: add a spec to prove that this formatting is necessary. If so, it should be in a shared form helper for currency inputs = error_message_on variant, :price -%td.field.popout{'data-controller': "popout"} +%td.col-on_hand.field.popout{'data-controller': "popout"} %button.popout__button{'data-popout-target': "button", 'aria-label': t('admin.products_page.columns.on_hand')} = variant.on_demand ? t(:on_demand) : variant.on_hand %div.popout__container{ style: 'display: none;', 'data-controller': 'toggle-control', 'data-popout-target': "dialog" } @@ -39,16 +39,16 @@ = f.label :on_demand do = f.check_box :on_demand, 'data-action': 'change->toggle-control#disableIfPresent change->popout#closeIfChecked' = t(:on_demand) -%td.align-left +%td.col-producer.align-left -# empty producer name -%td.field.naked_inputs +%td.col-category.field.naked_inputs = render(SearchableDropdownComponent.new(form: f, name: :primary_taxon_id, options: category_options, selected_option: variant.primary_taxon_id, aria_label: t('.category_field_name'), placeholder_value: t('admin.products_v3.filters.search_for_categories'))) -%td.field.naked_inputs +%td.col-tax_category.field.naked_inputs = render(SearchableDropdownComponent.new(form: f, name: :tax_category_id, options: tax_category_options, @@ -57,7 +57,7 @@ aria_label: t('.tax_category_field_name'), placeholder_value: t('.search_for_tax_categories'))) = error_message_on variant, :tax_category -%td.align-left +%td.col-inherits_properties.align-left -# empty %td.align-right = render(VerticalEllipsisMenu::Component.new) do diff --git a/app/views/spree/admin/shared/_stimulus_sortable_header.html.haml b/app/views/spree/admin/shared/_stimulus_sortable_header.html.haml index 08db1b6ce9..051ffa1f4f 100644 --- a/app/views/spree/admin/shared/_stimulus_sortable_header.html.haml +++ b/app/views/spree/admin/shared/_stimulus_sortable_header.html.haml @@ -1,4 +1,4 @@ -%th +%th{ class: "col-#{column}" } %a{ "data-controller": "search", "data-action": "click->search#changeSorting", "data-column": "#{column}", "data-current": (sorted || default).to_s } = t("spree.admin.shared.sortable_header.#{column.to_s}") diff --git a/app/webpacker/controllers/column_preferences_controller.js b/app/webpacker/controllers/column_preferences_controller.js new file mode 100644 index 0000000000..9b55a961dc --- /dev/null +++ b/app/webpacker/controllers/column_preferences_controller.js @@ -0,0 +1,43 @@ +import { Controller } from "stimulus"; + +// Manage column visibility according to checkbox selection +// +export default class ColumnPreferencesController extends Controller { + connect() { + this.table = document.querySelector('table[data-column-preferences-target="table"]'); + this.cols = Array.from(this.table.querySelectorAll('col')); + this.colSpanCells = this.table.querySelectorAll('th[colspan],td[colspan]'); + // Initialise data-default-col-span + this.colSpanCells.forEach((cell)=> { + cell.dataset.defaultColSpan ||= cell.colSpan; + }); + + this.checkboxes = Array.from(this.element.querySelectorAll("input[type=checkbox]")); + for (const element of this.checkboxes) { + // On initial load + this.#showHideColumn(element); + // On checkbox changed + element.addEventListener("change", this.#showHideColumn.bind(this)); + } + } + + // private + + #showHideColumn(e) { + const element = e.target || e; + const name = element.dataset.columnName; + + this.table.classList.toggle(`hide-${name}`, !element.checked); + + // Reset cell colspans + const hiddenColCount = this.checkboxes.filter((checkbox)=> !checkbox.checked).length; + for(const cell of this.colSpanCells) { + const span = parseInt(cell.dataset.defaultColSpan, 10) - hiddenColCount; + cell.colSpan = span; + }; + } + + #showHideElement(element, show) { + element.style.display = show ? "" : "none"; + } +} diff --git a/app/webpacker/controllers/dropdown_controller.js b/app/webpacker/controllers/dropdown_controller.js index 844289e12c..701fdf27a7 100644 --- a/app/webpacker/controllers/dropdown_controller.js +++ b/app/webpacker/controllers/dropdown_controller.js @@ -1,5 +1,6 @@ import { Controller } from "stimulus"; +// Close a
element when click outside export default class extends Controller { connect() { diff --git a/app/webpacker/css/admin/components/input.scss b/app/webpacker/css/admin/components/input.scss index 1c1a847e56..2b390bce6d 100644 --- a/app/webpacker/css/admin/components/input.scss +++ b/app/webpacker/css/admin/components/input.scss @@ -6,32 +6,3 @@ } } } - -input[type="checkbox"].redesigned-input { - position: relative; - top: 1px; - -moz-appearance: none; - -webkit-appearance: none; - -o-appearance: none; - appearance: none; - outline: none; - content: none; - cursor: pointer; - - &:before { - font-family: "FontAwesome"; - content: "\f00c"; - font-size: 15px; - color: transparent !important; - background: transparent !important; - display: block; - width: 15px; - height: 15px; - border: 1px solid #809cb1; - margin-right: 7px; - } - - &:checked:before { - color: $color-txt-text !important; - } -} diff --git a/app/webpacker/css/admin/products_v3.scss b/app/webpacker/css/admin/products_v3.scss index 2b277f0e33..576e6d8af1 100644 --- a/app/webpacker/css/admin/products_v3.scss +++ b/app/webpacker/css/admin/products_v3.scss @@ -96,7 +96,8 @@ // "Naked" inputs. Row hover helps reveal them. .naked_inputs { - input:not([type="checkbox"]), .ts-control { + input:not([type="checkbox"]), + .ts-control { background-color: $color-tbl-cell-bg; } } @@ -179,6 +180,18 @@ .ts-control { z-index: 0; // Avoid hovering over thead } + + // Hide columns + $columns: + "image", "name", "sku", "unit_scale", "unit", "price", "on_hand", "producer", "category", + "tax_category", "inherits_properties"; + @each $col in $columns { + &.hide-#{$col} { + .col-#{$col} { + display: none; + } + } + } } #no-products { @@ -214,6 +227,10 @@ display: none; } + .pagination-description { + flex-grow: 1; // Grow to fill space + } + .with-dropdown { display: flex; justify-content: space-between; @@ -221,7 +238,11 @@ gap: 10px; } .per-page { - width: 10em; + width: 9em; + } + + .column-preferences .ofn-drop-down-label { + width: 13em; } } @@ -370,7 +391,7 @@ border-radius: $border-radius; box-shadow: 0px 0px 8px 0px rgba($near-black, 0.25); - .field{ + .field { margin-bottom: 0.75em; &:last-child { @@ -424,7 +445,7 @@ opacity: 0; } } - + .slide-out { animation: slideOutLeft 0.5s forwards; } diff --git a/app/webpacker/css/admin_v3/components/dropdown.scss b/app/webpacker/css/admin_v3/components/dropdown.scss index 8a101d1c8d..6f8bd99c8a 100644 --- a/app/webpacker/css/admin_v3/components/dropdown.scss +++ b/app/webpacker/css/admin_v3/components/dropdown.scss @@ -101,8 +101,6 @@ > span { width: auto; - text-transform: uppercase; - font-size: 85%; font-weight: 600; } @@ -113,7 +111,7 @@ top: 100%; left: 0px; padding: 5px 0px; - border: 1px solid #adadad; + border-radius: $border-radius; background-color: #ffffff; box-shadow: 1px 3px 10px #888888; z-index: 100; @@ -181,6 +179,25 @@ } } + summary:after { + content: "\f078"; + font-family: FontAwesome; + position: relative; + top: 3px; + font-size: 13px; + } + + + &[open] >, + details[open] > { + summary:after { + content: "\f077"; + font-family: FontAwesome; + } + } +} + +.ofn-drop-down:not(.ofn-dropdown-v2) { > details { margin: -7px -15px; padding: 7px 15px; @@ -190,21 +207,14 @@ display: inline-block; list-style: none; width: auto; - text-transform: uppercase; - font-size: 85%; - font-weight: 600; margin: -8px -15px; padding: 8px 15px; - } - > details > summary:after { - content: "\f0d7"; - font-family: FontAwesome; - } - - > details[open] > summary:after { - content: "\f0d8"; - font-family: FontAwesome; + &:after { + position: relative; + top: -1px; + font-size: 12px; + } } } @@ -212,8 +222,11 @@ border: 1px solid $lighter-grey; background-color: $lighter-grey; padding: 0px; + line-height: normal; + @include border-radius($border-radius); - &:hover { + &:hover, + &.expanded { border-color: $lighter-grey; } @@ -263,17 +276,22 @@ cursor: pointer; padding-top: 4px; padding-bottom: 5px; - text-transform: uppercase; - font-size: 85%; + font-size: inherit; + + // Align checkbox and text + & > * { + vertical-align: middle; + } + } + } + + .actions { + margin-top: 5px; + margin-right: 15px; // Compensate for scrollbar on menu_items + padding: 2px 10px; + + &:hover { + background-color: inherit; } } } - -.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; - } -} diff --git a/config/locales/en.yml b/config/locales/en.yml index 61277a47eb..13b2cd44fa 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -571,6 +571,7 @@ en: per_page: "%{count} items per page" colums: Columns columns: + image: Image name: Name unit_scale: Unit scale unit: Unit @@ -661,7 +662,7 @@ en: choose: "Choose..." please_select: Please select... - column_save_as_default: Save As Default + column_save_as_default: Save as default columns: Columns actions: Actions viewing: "Viewing: %{current_view_name}" @@ -760,6 +761,9 @@ en: balance_due: "Balance Due" destroy: has_associated_subscriptions: "Delete failed: This customer has active subscriptions. Cancel them first." + column_preferences: + bulk_update: + success: "Column preferences saved" contents: edit: title: Content diff --git a/config/routes/admin.rb b/config/routes/admin.rb index 21caa2b677..4016f8a695 100644 --- a/config/routes/admin.rb +++ b/config/routes/admin.rb @@ -95,7 +95,7 @@ Openfoodnetwork::Application.routes.draw do resource :contents - resources :column_preferences, only: [], format: :json do + resources :column_preferences, only: [] do put :bulk_update, on: :collection end diff --git a/lib/open_food_network/column_preference_defaults.rb b/lib/open_food_network/column_preference_defaults.rb index 9bb09b2061..f851efc794 100644 --- a/lib/open_food_network/column_preference_defaults.rb +++ b/lib/open_food_network/column_preference_defaults.rb @@ -77,6 +77,24 @@ module OpenFoodNetwork } end + def products_v3_index_columns + I18n.with_options scope: 'admin.products_page.columns' do + { + image: { name: t(:image), visible: true }, + name: { name: t(:name), visible: true }, + sku: { name: t(:sku), visible: true }, + unit: { name: t(:unit), visible: true }, + unit_scale: { name: t(:unit_scale), visible: true }, + price: { name: t(:price), visible: true }, + on_hand: { name: t(:on_hand), visible: true }, + producer: { name: t(:producer), visible: true }, + category: { name: t(:category), visible: true }, + tax_category: { name: t(:tax_category), visible: true }, + inherits_properties: { name: t(:inherits_properties), visible: true }, + } + end + end + def enterprises_index_columns node = "admin.enterprises.index" { diff --git a/spec/controllers/admin/column_preferences_controller_spec.rb b/spec/controllers/admin/column_preferences_controller_spec.rb index 41f34b163e..c29153534d 100644 --- a/spec/controllers/admin/column_preferences_controller_spec.rb +++ b/spec/controllers/admin/column_preferences_controller_spec.rb @@ -9,13 +9,26 @@ RSpec.describe Admin::ColumnPreferencesController, type: :controller do let!(:user1) { create(:user) } let!(:user2) { create(:user) } let!(:enterprise) { create(:enterprise, owner: user1, users: [user1, user2]) } + let!(:column_preference) { + ColumnPreference.create(user_id: user1.id, action_name: 'enterprises_index', + column_name: "name", visible: true) + } + + shared_examples "where I own the preferences submitted" do + before do + allow(controller).to receive(:spree_current_user) { user1 } + end + + it "allows me to update the column preferences" do + spree_put :bulk_update, format: request_format, action_name: "enterprises_index", + column_preferences: column_preference_params + expect(ColumnPreference.where(user_id: user1.id, + action_name: 'enterprises_index').count).to be 3 + end + end context "json" do - let!(:column_preference) { - ColumnPreference.create(user_id: user1.id, action_name: 'enterprises_index', - column_name: "name", visible: true) - } - + let(:request_format) { :json } let(:column_preference_params) { [ { id: column_preference.id, user_id: user1.id, action_name: "enterprises_index", @@ -27,28 +40,47 @@ RSpec.describe Admin::ColumnPreferencesController, type: :controller do ] } + it_behaves_like "where I own the preferences submitted" + context "where I don't own the preferences submitted" do before do allow(controller).to receive(:spree_current_user) { user2 } end it "prevents me from updating the column preferences" do - spree_put :bulk_update, format: :json, action_name: "enterprises_index", + spree_put :bulk_update, format: request_format, action_name: "enterprises_index", column_preferences: column_preference_params expect(ColumnPreference.count).to be 1 end end + end - context "where I own the preferences submitted" do + context "turbo_stream" do + let(:request_format) { :turbo_stream } + let(:column_preference_params) { + { + '0': { id: column_preference.id, column_name: "name", visible: "0" }, + '1': { id: nil, column_name: "producer", visible: "1" }, + '2': { id: nil, column_name: "status", visible: "1" }, + } + } + + it_behaves_like "where I own the preferences submitted" + + context "where I don't own the preferences submitted" do before do - allow(controller).to receive(:spree_current_user) { user1 } + allow(controller).to receive(:spree_current_user) { user2 } end - it "allows me to update the column preferences" do - spree_put :bulk_update, format: :json, action_name: "enterprises_index", - column_preferences: column_preference_params - expect(ColumnPreference.where(user_id: user1.id, - action_name: 'enterprises_index').count).to be 3 + # This has the same effect as the JSON action, but due to differing implementation, + # it has different expections. + it "prevents me from updating the column preferences" do + expect { + spree_put :bulk_update, format: request_format, action_name: "enterprises_index", + column_preferences: column_preference_params + }.to raise_error(ActiveRecord::RecordNotUnique) + + expect(column_preference.reload.visible).to eq true end end end diff --git a/spec/support/request/admin_helper.rb b/spec/support/request/admin_helper.rb index 60e0fcdbd4..eb06f34b12 100644 --- a/spec/support/request/admin_helper.rb +++ b/spec/support/request/admin_helper.rb @@ -3,10 +3,10 @@ module AdminHelper def toggle_columns(*labels) # open dropdown - # case insensitive search for "Columns" text - find("div#columns-dropdown", text: /columns/i).click + columns_dropdown = ofn_drop_down("Columns") + columns_dropdown.click - within "div#columns-dropdown" do + within columns_dropdown do labels.each do |label| # Convert label to case-insensitive regexp if not one already label = /#{label}/i unless label.is_a?(Regexp) @@ -16,6 +16,10 @@ module AdminHelper end # close dropdown - find("div#columns-dropdown", text: /columns/i).click + columns_dropdown.click + end + + def ofn_drop_down(label) + find(".ofn-drop-down", text: /#{label}/i) end end diff --git a/spec/system/admin/products_v3/products_spec.rb b/spec/system/admin/products_v3/products_spec.rb index 3d081bf82d..ea4a45c7af 100644 --- a/spec/system/admin/products_v3/products_spec.rb +++ b/spec/system/admin/products_v3/products_spec.rb @@ -3,6 +3,7 @@ require "system_helper" RSpec.describe 'As an enterprise user, I can manage my products', feature: :admin_style_v3 do + include AdminHelper include WebHelper include AuthenticationHelper include FileHelper @@ -30,29 +31,45 @@ RSpec.describe 'As an enterprise user, I can manage my products', feature: :admi end end - describe "using the page" do - describe "using column display dropdown" do - let(:product) { create(:simple_product) } + describe "column selector" do + let!(:product) { create(:simple_product) } - before do - pending "Pending implementation, issue #11055" - login_as_admin - visit spree.admin_products_path + before do + visit admin_products_url + end + + it "hides column and remembers saved preference" do + # Name shows by default + expect(page).to have_checked_field "Name" + expect(page).to have_selector "th", text: "Name" + expect_other_columns_visible + + # Name is hidden + ofn_drop_down("Columns").click + within ofn_drop_down("Columns") do + uncheck "Name" end + expect(page).not_to have_selector "th", text: "Name" + expect_other_columns_visible - it "shows a column display dropdown, which shows a list of columns when clicked" do - expect(page).to have_selector "th", text: "NAME" - expect(page).to have_selector "th", text: "PRODUCER" - expect(page).to have_selector "th", text: "PRICE" - expect(page).to have_selector "th", text: "ON HAND" + # Preference saved + click_on "Save as default" + expect(page).to have_content "Column preferences saved" + refresh - toggle_columns /^.{0,1}Producer$/i - - expect(page).not_to have_selector "th", text: "PRODUCER" - expect(page).to have_selector "th", text: "NAME" - expect(page).to have_selector "th", text: "PRICE" - expect(page).to have_selector "th", text: "ON HAND" + # Preference remembered + ofn_drop_down("Columns").click + within ofn_drop_down("Columns") do + expect(page).to have_unchecked_field "Name" end + expect(page).not_to have_selector "th", text: "Name" + expect_other_columns_visible + end + + def expect_other_columns_visible + expect(page).to have_selector "th", text: "Producer" + expect(page).to have_selector "th", text: "Price" + expect(page).to have_selector "th", text: "On Hand" end end @@ -321,6 +338,8 @@ RSpec.describe 'As an enterprise user, I can manage my products', feature: :admi end end + describe "columns" + describe "updating" do let!(:variant_a1) { product_a.variants.first.tap{ |v|