Skip to content

Commit

Permalink
Merge pull request #12268 from dacook/buu/change-variant-unit-11061
Browse files Browse the repository at this point in the history
[BUU] Change variant unit values
  • Loading branch information
mkllnk committed Mar 28, 2024
2 parents 4498e91 + 266e94e commit d8641bf
Show file tree
Hide file tree
Showing 25 changed files with 997 additions and 224 deletions.
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# Controller for "New Products" form (spree/admin/products/new)
angular.module("admin.products")
.controller "unitsCtrl", ($scope, VariantUnitManager, OptionValueNamer, UnitPrices, PriceParser) ->
$scope.product = { master: {} }
Expand All @@ -12,13 +13,15 @@ angular.module("admin.products")

$scope.variant_unit_options = VariantUnitManager.variantUnitOptions()

# Extract variant_unit and variant_unit_scale from dropdown variant_unit_with_scale,
# and update hidden product fields
$scope.processVariantUnitWithScale = ->
if $scope.product.variant_unit_with_scale
match = $scope.product.variant_unit_with_scale.match(/^([^_]+)_([\d\.]+)$/)
match = $scope.product.variant_unit_with_scale.match(/^([^_]+)_([\d\.]+)$/) # matches string like "weight_1000"
if match
$scope.product.variant_unit = match[1]
$scope.product.variant_unit_scale = parseFloat(match[2])
else
else # "items"
$scope.product.variant_unit = $scope.product.variant_unit_with_scale
$scope.product.variant_unit_scale = null
else if $scope.product.variant_unit
Expand All @@ -32,6 +35,8 @@ angular.module("admin.products")
else
$scope.product.variant_unit = $scope.product.variant_unit_scale = null

# Extract unit_value and unit_description from text field unit_value_with_description,
# and update hidden variant fields
$scope.processUnitValueWithDescription = ->
if $scope.product.master.hasOwnProperty("unit_value_with_description")
match = $scope.product.master.unit_value_with_description.match(/^([\d\.,]+(?= *|$)|)( *)(.*)$/)
Expand All @@ -45,6 +50,7 @@ angular.module("admin.products")
value = window.bigDecimal.divide(value, $scope.product.variant_unit_scale, 2) if $scope.product.master.unit_value && $scope.product.variant_unit_scale
$scope.product.master.unit_value_with_description = value + " " + $scope.product.master.unit_description

# Calculate unit price based on product price and variant_unit_scale
$scope.processUnitPrice = ->
price = $scope.product.price
scale = $scope.product.variant_unit_scale
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
angular.module("admin.products").factory "OptionValueNamer", (VariantUnitManager) ->
# Javascript clone of VariantUnits::OptionValueNamer, for bulk product editing.
class OptionValueNamer
constructor: (@variant) ->

Expand Down
8 changes: 5 additions & 3 deletions app/views/admin/products_v3/_product_row.html.haml
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,16 @@
%a.image-field{ href: admin_product_images_path(product), data: { controller: "modal", reflex: "click->products#edit_image", "current-id": product.id} }
= image_tag product.image&.url(:mini) || Spree::Image.default_image_url(:mini), width: 40, height: 40
.button.secondary.mini= t('admin.products_page.image.edit')
%td.field.align-left.header
%td.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
%td.field.naked_inputs
= f.text_field :sku, 'aria-label': t('admin.products_page.columns.sku')
= error_message_on product, :sku
%td.multi-field{ 'data-controller': 'toggle-control', 'data-toggle-control-match-value': 'items' }
%td.multi-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,
options_for_select(WeightsAndMeasures.variant_unit_options, product.variant_unit_with_scale),
{},
Expand Down
12 changes: 6 additions & 6 deletions app/views/admin/products_v3/_table.html.haml
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@
%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:"5%"}
%col{ width:"10%"}
%col{ width:"15%"}= # Producer
%col{ width:"8%"}
Expand Down Expand Up @@ -43,7 +43,7 @@
%th.align-left.with-input= t('admin.products_page.columns.name')
%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-right= t('admin.products_page.columns.unit')
%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')
Expand All @@ -53,20 +53,20 @@
%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|
%tbody.relaxed.naked_inputs{ data: { 'record-id': product_form.object.id,
controller: "nested-form",
%tbody.relaxed{ data: { 'record-id': product_form.object.id,
controller: "nested-form product",
action: 'rails-nested-form:add->bulk-form#registerElements' } }
%tr
= render partial: 'product_row', locals: { product:, f: product_form }
- product.variants.each_with_index do |variant, variant_index|
= form.fields_for("products][#{product_index}][variants_attributes][", variant, index: variant_index) do |variant_form|
%tr.condensed
%tr.condensed{ 'data-controller': "variant" }
= render partial: 'variant_row', locals: { variant:, f: variant_form }

= form.fields_for("products][#{product_index}][variants_attributes][NEW_RECORD", product.variants.build) do |new_variant_form|
%template{ 'data-nested-form-target': "template" }
%tr.condensed
%tr.condensed{ 'data-controller': "variant" }
= render partial: 'variant_row', locals: { variant: new_variant_form.object, f: new_variant_form }

%tr{ 'data-nested-form-target': "target" }
Expand Down
35 changes: 22 additions & 13 deletions app/views/admin/products_v3/_variant_row.html.haml
Original file line number Diff line number Diff line change
@@ -1,28 +1,37 @@
%td
-# empty
%td.field
%td.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
%td.field.naked_inputs
= f.text_field :sku, 'aria-label': t('admin.products_page.columns.sku')
= error_message_on variant, :sku
%td
-# empty
- if variant.persisted?
%td.align-right
.content= variant.unit_to_display
- else # until unit component is developed, use a basic input just so we can create new records
%td.field
= f.number_field :unit_value, 'aria-label': t('admin.products_page.columns.unit')
= error_message_on variant, :unit_value
%td.field
%td.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" }
.field
-# Show a composite field for unit_value and unit_description
= f.hidden_field :unit_value
= f.hidden_field :unit_description
-# todo: create a method for value_with_description
= f.text_field :unit_value_with_description,
value: [number_with_precision((variant.unit_value || 1) / (variant.product.variant_unit_scale || 1), precision: nil, strip_insignificant_zeros: true), variant.unit_description].compact_blank.join(" "),
'aria-label': t('admin.products_page.columns.unit_value'), required: true
.field
= 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
= 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.on-hand__wrapper{'data-controller': "popout"}
%button.on-hand__button{'data-popout-target': "button", 'aria-label': t('admin.products_page.columns.on_hand')}
%td.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.on-hand__popout{ style: 'display: none;', 'data-controller': 'toggle-control', 'data-popout-target': "dialog" }
%div.popout__container{ style: 'display: none;', 'data-controller': 'toggle-control', 'data-popout-target': "dialog" }
.field
= f.number_field :on_hand, min: 0, 'aria-label': t('admin.products_page.columns.on_hand'), 'data-toggle-control-target': 'control', disabled: f.object.on_demand
= error_message_on variant, :on_hand
Expand Down
3 changes: 3 additions & 0 deletions app/views/admin/products_v3/index.html.haml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@
%li#new_product_link
= button_link_to t(:new_product), "/admin/products/new", { :icon => 'icon-plus', :id => 'admin_new_product' }

%script= render partial: "admin/shared/global_var_ofn", formats: :js,
locals: { name: :available_units_sorted, value: WeightsAndMeasures.available_units_sorted }

= render partial: 'spree/admin/shared/product_sub_menu'

#products_v3_page{ "data-controller": "products" }
Expand Down
1 change: 1 addition & 0 deletions app/views/admin/shared/_global_var_ofn.js.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
var ofn_<%= name %> = <%= value.to_json.html_safe %>;
3 changes: 2 additions & 1 deletion app/webpacker/controllers/bulk_form_controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ export default class BulkFormController extends Controller {

toggleFormChanged() {
// For each record, check if any fields are changed
// TODO: optimise basd on current state. if field is changed, but form already changed, no need to update (and vice versa)
const changedRecordCount = Object.values(this.recordElements).filter((elements) =>
elements.some(this.#isChanged)
).length;
Expand All @@ -73,7 +74,7 @@ export default class BulkFormController extends Controller {

// Prevent accidental data loss
if (formChanged) {
window.addEventListener("beforeunload", this.preventLeavingBulkForm);
window.addEventListener("beforeunload", this.preventLeavingBulkForm); // TOFIX: what if it has laredy been added? we can optimise above to avoid this
} else {
window.removeEventListener("beforeunload", this.preventLeavingBulkForm);
}
Expand Down
13 changes: 9 additions & 4 deletions app/webpacker/controllers/popout_controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,13 @@ import { Controller } from "stimulus";
// Allows a form section to "pop out" and show additional options
export default class PopoutController extends Controller {
static targets = ["button", "dialog"];
static values = {
updateDisplay: { Boolean, default: true }
}

connect() {
this.first_input = this.dialogTarget.querySelector("input");
this.displayElements = Array.from(this.element.querySelectorAll('input:not([type="hidden"]'));
this.first_input = this.displayElements[0];

// Show when click or down-arrow on button
this.buttonTarget.addEventListener("click", this.show.bind(this));
Expand Down Expand Up @@ -58,8 +61,10 @@ export default class PopoutController extends Controller {
}

// Update button to represent any changes
this.buttonTarget.innerText = this.#displayValue();
this.buttonTarget.innerHTML ||= "&nbsp;"; // (with default space to help with styling)
if (this.updateDisplayValue) {
this.buttonTarget.textContent = this.#displayValue();
this.buttonTarget.innerHTML ||= "&nbsp;"; // (with default space to help with styling)
}
this.buttonTarget.classList.toggle("changed", this.#isChanged());

this.dialogTarget.style.display = "none";
Expand Down Expand Up @@ -87,7 +92,7 @@ export default class PopoutController extends Controller {
let values = this.#enabledDisplayElements().map((element) => {
if (element.type == "checkbox") {
if (element.checked && element.labels[0]) {
return element.labels[0].innerText;
return element.labels[0].textContent.trim();
}
} else {
return element.value;
Expand Down
38 changes: 38 additions & 0 deletions app/webpacker/controllers/product_controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { Controller } from "stimulus";

// Dynamically update related Product unit fields (expected to move to Variant due to Product Refactor)
//
export default class ProductController extends Controller {
connect() {
// idea: create a helper that includes a nice getter/setter for Rails model attr values, just pass it the attribute name.
// It could automatically find (and cache a ref to) each dom element and get/set the values.
this.variantUnit = this.element.querySelector('[name$="[variant_unit]"]');
this.variantUnitScale = this.element.querySelector('[name$="[variant_unit_scale]"]');
this.variantUnitWithScale = this.element.querySelector('[name$="[variant_unit_with_scale]"]');

// on variant_unit_with_scale changed; update variant_unit and variant_unit_scale
this.variantUnitWithScale.addEventListener("change", this.#updateUnitAndScale.bind(this), {
passive: true,
});
}

// private

// Extract variant_unit and variant_unit_scale from dropdown variant_unit_with_scale,
// and update hidden product fields
#updateUnitAndScale(event) {
const variant_unit_with_scale = this.variantUnitWithScale.value;
const match = variant_unit_with_scale.match(/^([^_]+)_([\d\.]+)$/); // eg "weight_1000"

if (match) {
this.variantUnit.value = match[1];
this.variantUnitScale.value = parseFloat(match[2]);
} else {
// "items"
this.variantUnit.value = variant_unit_with_scale;
this.variantUnitScale.value = "";
}
this.variantUnit.dispatchEvent(new Event("change"));
this.variantUnitScale.dispatchEvent(new Event("change"));
}
}
86 changes: 86 additions & 0 deletions app/webpacker/controllers/variant_controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { Controller } from "stimulus";
import OptionValueNamer from "js/services/option_value_namer";

// Dynamically update related variant fields
//
export default class VariantController extends Controller {
connect() {
// Assuming these will be available on the variant soon, just a quick hack to find the product fields:
const product = this.element.closest("[data-record-id]");
this.variantUnit = product.querySelector('[name$="[variant_unit]"]');
this.variantUnitScale = product.querySelector('[name$="[variant_unit_scale]"]');
this.variantUnitName = product.querySelector('[name$="[variant_unit_name]"]');

this.unitValue = this.element.querySelector('[name$="[unit_value]"]');
this.unitDescription = this.element.querySelector('[name$="[unit_description]"]');
this.unitValueWithDescription = this.element.querySelector(
'[name$="[unit_value_with_description]"]',
);
this.displayAs = this.element.querySelector('[name$="[display_as]"]');
this.unitToDisplay = this.element.querySelector('[name$="[unit_to_display]"]');

// on unit changed; update display_as:placeholder and unit_to_display
[this.variantUnit, this.variantUnitScale, this.variantUnitName].forEach((element) => {
element.addEventListener("change", this.#unitChanged.bind(this), { passive: true });
});
this.variantUnitName.addEventListener("input", this.#unitChanged.bind(this), { passive: true });

// on unit_value_with_description changed; update unit_value and unit_description
// on unit_value and/or unit_description changed; update display_as:placeholder and unit_to_display
this.unitValueWithDescription.addEventListener("input", this.#unitChanged.bind(this), {
passive: true,
});

// on display_as changed; update unit_to_display
// TODO: optimise to avoid unnecessary OptionValueNamer calc
this.displayAs.addEventListener("input", this.#updateUnitDisplay.bind(this), { passive: true });
}

disconnect() {
// Make sure to clean up anything that happened outside
}

// private

// Extract variant_unit and variant_unit_scale from dropdown variant_unit_with_scale,
// and update hidden product fields
#unitChanged(event) {
//Hmm in hindsight the logic in product_controller should be inn this controller already. then we can do everything in one event, and store the generated name in an instance variable.
this.#extractUnitValues();
this.#updateUnitDisplay();
}

// Extract unit_value and unit_description
#extractUnitValues() {
// Extract a number (optional) and text value, separated by a space.
const match = this.unitValueWithDescription.value.match(/^([\d\.\,]+(?= |$)|)( |)(.*)$/);
if (match) {
let unit_value = parseFloat(match[1].replace(",", "."));
unit_value = isNaN(unit_value) ? null : unit_value;
unit_value *= this.variantUnitScale.value ? this.variantUnitScale.value : 1; // Normalise to default scale

this.unitValue.value = unit_value;
this.unitDescription.value = match[3];
}
}

// Update display_as placeholder and unit_to_display
#updateUnitDisplay() {
const unitDisplay = new OptionValueNamer(this.#variant()).name();
this.displayAs.placeholder = unitDisplay;
this.unitToDisplay.textContent = this.displayAs.value || unitDisplay;
}

// A representation of the variant model to satisfy OptionValueNamer.
#variant() {
return {
unit_value: parseFloat(this.unitValue.value),
unit_description: this.unitDescription.value,
product: {
variant_unit: this.variantUnit.value,
variant_unit_scale: parseFloat(this.variantUnitScale.value),
variant_unit_name: this.variantUnitName.value,
},
};
}
}

0 comments on commit d8641bf

Please sign in to comment.