mirror of
https://github.com/openfoodfoundation/openfoodnetwork
synced 2026-01-24 20:36:49 +00:00
Update variant form and rip out angular
This commit is contained in:
@@ -1,32 +0,0 @@
|
||||
angular.module("admin.products").controller "variantUnitsCtrl", ($scope, VariantUnitManager, $timeout, UnitPrices, PriceParser) ->
|
||||
|
||||
$scope.unitName = (scale, type) ->
|
||||
VariantUnitManager.getUnitName(scale, type)
|
||||
|
||||
$scope.$watchCollection "[unit_value_human, variant.price]", ->
|
||||
$scope.processUnitPrice()
|
||||
|
||||
$scope.processUnitPrice = ->
|
||||
if ($scope.variant)
|
||||
price = $scope.variant.price
|
||||
scale = $scope.scale
|
||||
unit_type = angular.element("#product_variant_unit").val()
|
||||
if (unit_type != "items")
|
||||
$scope.updateValue()
|
||||
unit_value = $scope.unit_value
|
||||
else
|
||||
unit_value = 1
|
||||
variant_unit_name = angular.element("#product_variant_unit_name").val()
|
||||
$scope.unit_price = UnitPrices.displayableUnitPrice(price, scale, unit_type, unit_value, variant_unit_name)
|
||||
|
||||
$scope.scale = angular.element('#product_variant_unit_scale').val()
|
||||
|
||||
$scope.updateValue = ->
|
||||
unit_value_human = angular.element('#unit_value_human').val()
|
||||
$scope.unit_value = bigDecimal.multiply(PriceParser.parse(unit_value_human), $scope.scale, 2)
|
||||
|
||||
variant_unit_value = angular.element('#variant_unit_value').val()
|
||||
$scope.unit_value_human = parseFloat(bigDecimal.divide(variant_unit_value, $scope.scale, 2))
|
||||
|
||||
$timeout -> $scope.processUnitPrice()
|
||||
$timeout -> $scope.updateValue()
|
||||
@@ -1,83 +1,108 @@
|
||||
.label-block.left.six.columns.alpha{'ng-app' => 'admin.products', 'ng-controller' => 'variantUnitsCtrl'}
|
||||
.field
|
||||
= f.label :display_name, t('.display_name')
|
||||
= f.text_field :display_name, class: "fullwidth", placeholder: t('.display_name_placeholder')
|
||||
.field
|
||||
= f.label :display_as, t('.display_as')
|
||||
= f.text_field :display_as, class: "fullwidth", placeholder: t('.display_as_placeholder')
|
||||
%div{'data-controller': "edit-variant"}
|
||||
.label-block.left.six.columns.alpha
|
||||
%script= render partial: "admin/shared/global_var_ofn", formats: :js,
|
||||
locals: { name: :available_units_sorted, value: WeightsAndMeasures.available_units_sorted }
|
||||
|
||||
- if @product.variant_unit != 'items'
|
||||
.field
|
||||
= label_tag :unit_value_human, "#{t('admin.'+@product.variant_unit)} ({{unitName(#{@product.variant_unit_scale}, '#{@product.variant_unit}')}})"
|
||||
= hidden_field_tag 'product_variant_unit_scale', @product.variant_unit_scale
|
||||
= number_field_tag :unit_value_human, nil, {class: "fullwidth", step: 0.01, 'ng-model' => 'unit_value_human', 'ng-change' => 'updateValue()'}
|
||||
= f.number_field :unit_value, {hidden: true, 'ng-value' => 'unit_value'}
|
||||
%script= render partial: "admin/shared/global_var_ofn", formats: :js,
|
||||
locals: { name: :currency_config, value: Api::CurrencyConfigSerializer.new({}) }
|
||||
|
||||
.field
|
||||
= f.label :unit_description, t(:spree_admin_unit_description)
|
||||
= f.text_field :unit_description, class: "fullwidth", placeholder: t('admin.products.unit_name_placeholder')
|
||||
.field
|
||||
= f.label :display_name, t('.display_name')
|
||||
= f.text_field :display_name, class: "fullwidth", placeholder: t('.display_name_placeholder')
|
||||
|
||||
%div
|
||||
.field
|
||||
= f.label :sku, t('.sku')
|
||||
= f.text_field :sku, class: 'fullwidth'
|
||||
.field
|
||||
= f.label :price, t('.price')
|
||||
= f.text_field :price, class: 'fullwidth', "ng-model" => "variant.price", "ng-init" => "variant.price = '#{number_to_currency(@variant.price, unit: '')&.strip}'"
|
||||
.field
|
||||
= hidden_field_tag 'product_variant_unit', @product.variant_unit
|
||||
= hidden_field_tag 'product_variant_unit_name', @product.variant_unit_name
|
||||
= f.field_container :unit_price do
|
||||
%div{style: "display: flex"}
|
||||
= f.label :unit_price, t(".unit_price"), {style: "display: inline-block"}
|
||||
%question-mark-with-tooltip{"question-mark-with-tooltip" => "_",
|
||||
"question-mark-with-tooltip-append-to-body" => "true",
|
||||
"question-mark-with-tooltip-placement" => "top",
|
||||
"question-mark-with-tooltip-animation" => true,
|
||||
key: "'js.admin.unit_price_tooltip'"}
|
||||
%input{ "type" => "text", "id" => "variant_unit_price", "name" => "variant[unit_price]",
|
||||
"class" => 'fullwidth', "disabled" => true, "ng-model" => "unit_price"}
|
||||
%div{style: "color: black"}
|
||||
= t("spree.admin.products.new.unit_price_legend")
|
||||
%div{ 'set-on-demand' => '' }
|
||||
.field.checkbox
|
||||
%label
|
||||
= f.check_box :on_demand
|
||||
= t(:on_demand)
|
||||
%div{'ofn-with-tip' => t('admin.products.variants.to_order_tip')}
|
||||
%a= t('admin.whats_this')
|
||||
.field{ 'data-controller': 'toggle-control', 'data-toggle-control-match-value': 'items' }
|
||||
-#TODO translation
|
||||
= f.label :unit_scale, raw(t(:unit_scale) + content_tag(:span, ' *', :class => 'required'))
|
||||
= f.hidden_field :variant_unit
|
||||
= f.hidden_field :variant_unit_scale
|
||||
= f.select :variant_unit_with_scale,
|
||||
options_for_select(WeightsAndMeasures.variant_unit_options, @variant.variant_unit_with_scale),
|
||||
{ include_blank: true },
|
||||
{ class: "fullwidth no-input", 'aria-label': t('admin.products_page.columns.unit_scale'), data: { "controller": "tom-select", "tom-select-options-value": '{ "plugins": [] }', action: "change->toggle-control#displayIfMatch" } }
|
||||
= error_message_on @variant, :variant_unit, 'data-toggle-control-target': 'control'
|
||||
.field
|
||||
= f.label :on_hand, t(:on_hand)
|
||||
.fullwidth
|
||||
= f.text_field :on_hand
|
||||
= f.text_field :variant_unit_name, 'aria-label': t('items'), 'data-toggle-control-target': 'control', style: (@variant.variant_unit == "items" ? "" : "display: none")
|
||||
= error_message_on @variant, :variant_unit_name, 'data-toggle-control-target': 'control'
|
||||
|
||||
.field.popout{'data-controller': "popout", 'data-popout-update-display-value': "false"}
|
||||
-#TODO translation
|
||||
= f.label :unit, raw(t(:unit) + content_tag(:span, ' *', :class => 'required'))
|
||||
= 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.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
|
||||
|
||||
.right.six.columns.omega.label-block
|
||||
- if @product.variant_unit != 'weight'
|
||||
%div
|
||||
.field
|
||||
= f.label :sku, t('.sku')
|
||||
= f.text_field :sku, class: 'fullwidth'
|
||||
.field
|
||||
= f.label :price, raw(t(:price) + content_tag(:span, ' *', :class => 'required'))
|
||||
= f.text_field :price, class: 'fullwidth', value: number_to_currency(@variant.price, unit: '')&.strip
|
||||
.field
|
||||
= hidden_field_tag 'variant_variant_unit', @variant.variant_unit
|
||||
= hidden_field_tag 'variant_variant_unit_name', @variant.variant_unit_name
|
||||
= f.field_container :unit_price do
|
||||
%div{style: "display: flex"}
|
||||
= f.label :unit_price, t(".unit_price"), {style: "display: inline-block"}
|
||||
%question-mark-with-tooltip{"question-mark-with-tooltip" => "_",
|
||||
"question-mark-with-tooltip-append-to-body" => "true",
|
||||
"question-mark-with-tooltip-placement" => "top",
|
||||
"question-mark-with-tooltip-animation" => true,
|
||||
key: "'js.admin.unit_price_tooltip'"}
|
||||
%input{ "type" => "text", "id" => "variant_unit_price", "name" => "variant[unit_price]", "class" => 'fullwidth', "disabled" => true}
|
||||
%div{style: "color: black"}
|
||||
= t("spree.admin.products.new.unit_price_legend")
|
||||
%div{ 'set-on-demand' => '' }
|
||||
.field.checkbox
|
||||
%label
|
||||
= f.check_box :on_demand
|
||||
= t(:on_demand)
|
||||
- #TODO tooltip is broken
|
||||
%div{'ofn-with-tip' => t('admin.products.variants.to_order_tip')}
|
||||
%a= t('admin.whats_this')
|
||||
.field
|
||||
= f.label :on_hand, t(:on_hand)
|
||||
.fullwidth
|
||||
= f.text_field :on_hand
|
||||
|
||||
.right.six.columns.omega.label-block
|
||||
.field
|
||||
= f.label 'weight', t(:weight)+' (kg)'
|
||||
- value = number_with_precision(@variant.weight, precision: 3)
|
||||
= f.number_field 'weight', value: value, class: 'fullwidth', step: 0.001
|
||||
|
||||
- [:height, :width, :depth].each do |field|
|
||||
- [:height, :width, :depth].each do |field|
|
||||
.field
|
||||
= f.label field, t(field)
|
||||
- value = number_with_precision(@variant.send(field), precision: 2)
|
||||
= f.number_field field, value: value, class: 'fullwidth', step: 0.01
|
||||
|
||||
.field
|
||||
= f.label field, t(field)
|
||||
- value = number_with_precision(@variant.send(field), precision: 2)
|
||||
= f.number_field field, value: value, class: 'fullwidth', step: 0.01
|
||||
= f.label :tax_category, t(:tax_category), for: :tax_category_id
|
||||
= f.collection_select(:tax_category_id, @tax_categories, :id, :name, { include_blank: t(:none) }, { class: 'select2 fullwidth' })
|
||||
|
||||
.field
|
||||
= f.label :tax_category, t(:tax_category), for: :tax_category_id
|
||||
= f.collection_select(:tax_category_id, @tax_categories, :id, :name, { include_blank: t(:none) }, { class: 'select2 fullwidth' })
|
||||
.field
|
||||
= f.label :shipping_category_id, t(:shipping_categories)
|
||||
= f.collection_select(:shipping_category_id, @shipping_categories, :id, :name, {}, { class: 'select2 fullwidth' })
|
||||
|
||||
.field
|
||||
= f.label :shipping_category_id, t(:shipping_categories)
|
||||
= f.collection_select(:shipping_category_id, @shipping_categories, :id, :name, {}, { class: 'select2 fullwidth' })
|
||||
.field
|
||||
= f.label :primary_taxon, t('spree.admin.products.primary_taxon_form.product_category')
|
||||
= f.collection_select(:primary_taxon_id, Spree::Taxon.order(:name), :id, :name, { include_blank: true }, { class: "select2 fullwidth" })
|
||||
|
||||
.field
|
||||
= f.label :primary_taxon, t('spree.admin.products.primary_taxon_form.product_category')
|
||||
= f.collection_select(:primary_taxon_id, Spree::Taxon.order(:name), :id, :name, { include_blank: true }, { class: "select2 fullwidth" })
|
||||
.field
|
||||
= f.label :supplier, t(:spree_admin_supplier)
|
||||
= f.collection_select(:supplier_id, @producers, :id, :name, {:include_blank => true}, {:class => "select2 fullwidth"})
|
||||
|
||||
.field
|
||||
= f.label :supplier, t(:spree_admin_supplier)
|
||||
= f.collection_select(:supplier_id, @producers, :id, :name, {:include_blank => true}, {:class => "select2 fullwidth"})
|
||||
|
||||
.clear
|
||||
.clear
|
||||
|
||||
169
app/webpacker/controllers/edit_variant_controller.js
Normal file
169
app/webpacker/controllers/edit_variant_controller.js
Normal file
@@ -0,0 +1,169 @@
|
||||
import { Controller } from "stimulus";
|
||||
import OptionValueNamer from "js/services/option_value_namer";
|
||||
import UnitPrices from "js/services/unit_prices";
|
||||
|
||||
// Dynamically update related variant fields
|
||||
//
|
||||
// TODO refactor so we can extract what's common with Bulk product page
|
||||
export default class EditVariantController extends Controller {
|
||||
connect() {
|
||||
this.unitPrices = new UnitPrices();
|
||||
// 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('[id="variant_variant_unit"]');
|
||||
this.variantUnitScale = this.element.querySelector('[id="variant_variant_unit_scale"]');
|
||||
this.variantUnitName = this.element.querySelector('[id="variant_variant_unit_name"]');
|
||||
this.variantUnitWithScale = this.element.querySelector(
|
||||
'[id="variant_variant_unit_with_scale"]',
|
||||
);
|
||||
this.variantPrice = this.element.querySelector('[id="variant_price"]');
|
||||
|
||||
// on variant_unit_with_scale changed; update variant_unit and variant_unit_scale
|
||||
this.variantUnitWithScale.addEventListener("change", this.#updateUnitAndScale.bind(this), {
|
||||
passive: true,
|
||||
});
|
||||
|
||||
this.unitValue = this.element.querySelector('[id="variant_unit_value"]');
|
||||
this.unitDescription = this.element.querySelector('[id="variant_unit_description"]');
|
||||
this.unitValueWithDescription = this.element.querySelector(
|
||||
'[id="variant_unit_value_with_description"]',
|
||||
);
|
||||
this.displayAs = this.element.querySelector('[id="variant_display_as"]');
|
||||
this.unitToDisplay = this.element.querySelector('[id="variant_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 });
|
||||
|
||||
// update Unit price when variant_unit_with_scale or price changes
|
||||
[this.variantUnitWithScale, this.variantPrice].forEach((element) => {
|
||||
element.addEventListener("change", this.#processUnitPrice.bind(this), { passive: true });
|
||||
});
|
||||
this.unitValueWithDescription.addEventListener("input", this.#processUnitPrice.bind(this), {
|
||||
passive: true,
|
||||
});
|
||||
|
||||
// on variantUnit change we need to check if weight needs to be toggled
|
||||
this.variantUnit.addEventListener("change", this.#toggleWeight.bind(this), { passive: true });
|
||||
|
||||
// update unit price on page load
|
||||
this.#processUnitPrice();
|
||||
this.#toggleWeight();
|
||||
}
|
||||
|
||||
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,
|
||||
variant_unit: this.variantUnit.value,
|
||||
variant_unit_scale: parseFloat(this.variantUnitScale.value),
|
||||
variant_unit_name: this.variantUnitName.value,
|
||||
};
|
||||
}
|
||||
|
||||
// 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"));
|
||||
}
|
||||
|
||||
#processUnitPrice() {
|
||||
const unit_type = this.variantUnit.value;
|
||||
|
||||
// TODO double check this
|
||||
let unit_value = 1;
|
||||
if (unit_type != "items") {
|
||||
unit_value = this.unitValue.value;
|
||||
}
|
||||
|
||||
const unit_price = this.unitPrices.displayableUnitPrice(
|
||||
this.variantPrice.value,
|
||||
parseFloat(this.variantUnitScale.value),
|
||||
unit_type,
|
||||
unit_value,
|
||||
this.variantUnitName.value,
|
||||
);
|
||||
|
||||
this.element.querySelector('[id="variant_unit_price"]').value = unit_price;
|
||||
}
|
||||
|
||||
#toggleWeight() {
|
||||
let display = "block";
|
||||
if (this.variantUnit.value === "weight") {
|
||||
display = "none";
|
||||
}
|
||||
|
||||
this.weight = this.element.querySelector('[id="variant_weight"]');
|
||||
this.weight.parentElement.style.display = display;
|
||||
}
|
||||
|
||||
//#showWeight() {
|
||||
// this.weight = this.element.querySelector('[id="variant_weight"]');
|
||||
// this.weight.parentElement.style.display= "block"
|
||||
//}
|
||||
|
||||
//#hideWeight() {
|
||||
// this.weight = this.element.querySelector('[id="variant_weight"]');
|
||||
// this.weight.parentElement.style.display= "none"
|
||||
//}
|
||||
}
|
||||
Reference in New Issue
Block a user