Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[BUU] Change product columns to be shown #12546

Merged
merged 25 commits into from
Jun 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()" }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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]
49 changes: 36 additions & 13 deletions app/controllers/admin/column_preferences_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -28,11 +36,26 @@ def permitted_params
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
Expand Down
23 changes: 23 additions & 0 deletions app/views/admin/column_preferences/_form.html.haml
Original file line number Diff line number Diff line change
@@ -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 <details> 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"
Original file line number Diff line number Diff line change
@@ -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: }
22 changes: 11 additions & 11 deletions app/views/admin/products_v3/_product_row.html.haml
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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
Expand Down
5 changes: 4 additions & 1 deletion app/views/admin/products_v3/_sort.html.haml
Original file line number Diff line number Diff line change
@@ -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)

Expand All @@ -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" }
47 changes: 24 additions & 23 deletions app/views/admin/products_v3/_table.html.haml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand All @@ -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|
Expand Down
22 changes: 11 additions & 11 deletions app/views/admin/products_v3/_variant_row.html.haml
Original file line number Diff line number Diff line change
@@ -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" }
Expand All @@ -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" }
Expand All @@ -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,
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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}")

Expand Down
43 changes: 43 additions & 0 deletions app/webpacker/controllers/column_preferences_controller.js
Original file line number Diff line number Diff line change
@@ -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);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now the next step would be to have the column selector be labels for hidden checkboxes in the columns and couple the visibility of the columns with the value of the hidden input in CSS. Then you won't need JS at all. 😉

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤯

That's so crazy it might just work

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know why, but I tried it, and it worked with pretty minimal changes!
dacook@a46eded

But pretty minimal benefit too, so I won't pursue it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, I didn't know the has selector. That's clever. But as long as we need the column counter, we can't get rid of the JS. Hm, I would probably scrap the counter but I'm not the designer. 😜

Copy link
Member Author

@dacook dacook Jun 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Haha, unfortunately the counter is needed in two places, where i've employed colspan to make rows span. One of them I think I could probably work around, but the other requires to span all visible rows. Unfortunately there isn't a builtin solution for that. (you can use an arbitrary high number like colspan="999", which automatically spans all columns, but this doesn't take into account hidden columns).
Hmm, maybe it could be hacked from within the first column...

Edit: nope, it cant.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I saw one example that used visibility: collapse which can hide entire columns. I haven't tested the effect on colspan though.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did use visibility: collapse initially on this PR, and it worked well to hide the columns. I think it handled the colspan too, which was a big bonus. But I couldn't get the column widths to behave properly (70fab2b)

It might be worth trying it again after the latest tweaks, but I'm not keen to add more work unless it's needed..


// 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";
}
}
1 change: 1 addition & 0 deletions app/webpacker/controllers/dropdown_controller.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Controller } from "stimulus";

// Close a <details> element when click outside
export default class extends Controller {

connect() {
Expand Down
Loading
Loading