Generate unit display with OptionValueNamer

This commit is contained in:
David Cook
2024-03-21 21:38:44 +11:00
parent cf31d09ad8
commit 4ddb2ff1e9
4 changed files with 240 additions and 19 deletions

View File

@@ -1,4 +1,5 @@
import { Controller } from "stimulus";
import OptionValueNamer from "js/services/option_value_namer";
// Dynamically update related variant fields
//
@@ -22,6 +23,7 @@ export default class VariantController extends Controller {
[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
@@ -29,7 +31,8 @@ export default class VariantController extends Controller {
passive: true,
});
// on display_as changed; update unit_to_display (how does this relate to unit_presentation?)
// 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 });
}
@@ -42,7 +45,6 @@ export default class VariantController extends Controller {
// Extract variant_unit and variant_unit_scale from dropdown variant_unit_with_scale,
// and update hidden product fields
#unitChanged(event) {
//todo: deduplicate events.
//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();
@@ -64,10 +66,21 @@ export default class VariantController extends Controller {
// Update display_as placeholder and unit_to_display
#updateUnitDisplay() {
// const unitDisplay = OptionValueNamer...
const unitDisplay =
this.unitValueWithDescription.value + " " + (this.variantUnitName.value || "~g"); //To Remove: DEMO only
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,
},
};
}
}

View File

@@ -0,0 +1,94 @@
import VariantUnitManager from "js/services/variant_unit_manager";
// Javascript clone of VariantUnits::OptionValueNamer, for bulk product editing.
export default class OptionValueNamer {
constructor(variant) {
this.variant = variant;
}
name() {
const [value, unit] = this.option_value_value_unit();
const separator = this.value_scaled() ? '' : ' ';
const name_fields = [];
if (value && unit) {
name_fields.push(`${value}${separator}${unit}`);
}
if (this.variant.unit_description) {
name_fields.push(this.variant.unit_description);
}
return name_fields.join(' ');
}
value_scaled() {
return !!this.variant.product.variant_unit_scale;
}
option_value_value_unit() {
let value, unit_name;
if (this.variant.unit_value) {
if (this.variant.product.variant_unit === "weight" || this.variant.product.variant_unit === "volume") {
[value, unit_name] = this.option_value_value_unit_scaled();
} else {
value = this.variant.unit_value;
unit_name = this.pluralize(this.variant.product.variant_unit_name, value);
}
if (value == parseInt(value, 10)) {
value = parseInt(value, 10);
}
} else {
value = unit_name = null;
}
return [value, unit_name];
}
pluralize(unit_name, count) {
if (count == null) {
return unit_name;
}
const unit_key = this.unit_key(unit_name);
if (!unit_key) {
return unit_name;
}
return I18n.t(["inflections", unit_key], {
count: count,
defaultValue: unit_name
});
}
unit_key(unit_name) {
if (!I18n.unit_keys) {
I18n.unit_keys = {};
for (const [key, translations] of Object.entries(I18n.t("inflections"))) {
for (const [quantifier, translation] of Object.entries(translations)) {
I18n.unit_keys[translation.toLowerCase()] = key;
}
}
}
return I18n.unit_keys[unit_name.toLowerCase()];
}
option_value_value_unit_scaled() {
const [unit_scale, unit_name] = this.scale_for_unit_value();
const value = Math.round((this.variant.unit_value / unit_scale) * 100) / 100;
return [value, unit_name];
}
scale_for_unit_value() {
// Find the largest available and compatible unit where unit_value comes
// to >= 1 when expressed in it.
// If there is none available where this is true, use the smallest
// available unit.
const product = this.variant.product;
const scales = VariantUnitManager.compatibleUnitScales(product.variant_unit_scale, product.variant_unit);
const variantUnitValue = this.variant.unit_value;
// sets largestScale = last element in filtered scales array
const largestScale = scales.filter(s => variantUnitValue / s >= 1).slice(-1)[0];
if (largestScale) {
return [largestScale, VariantUnitManager.getUnitName(largestScale, product.variant_unit)];
} else {
return [scales[0], VariantUnitManager.getUnitName(scales[0], product.variant_unit)];
}
}
}

View File

@@ -0,0 +1,93 @@
// todo load availableUnits. Hmm why not just load from the dropdown?
export default class VariantUnitManager {
static availableUnits = 'g,kg,T,mL,L,gal,kL';
// todo: load units from Ruby also?
static units = {
weight: {
0.001: {
name: 'mg',
system: 'metric'
},
1.0: {
name: 'g',
system: 'metric'
},
1000.0: {
name: 'kg',
system: 'metric'
},
1000000.0: {
name: 'T',
system: 'metric'
},
453.6: {
name: 'lb',
system: 'imperial'
},
28.35: {
name: 'oz',
system: 'imperial'
}
},
volume: {
0.001: {
name: 'mL',
system: 'metric'
},
0.01: {
name: 'cL',
system: 'metric'
},
0.1: {
name: 'dL',
system: 'metric'
},
1.0: {
name: 'L',
system: 'metric'
},
1000.0: {
name: 'kL',
system: 'metric'
},
4.54609: {
name: 'gal',
system: 'metric'
}
},
items: {
1: {
name: 'items'
}
}
};
static getUnitName = (scale, unitType) => {
if (this.units[unitType][scale]) {
return this.units[unitType][scale]['name'];
} else {
return '';
}
};
// filter by system and format
static compatibleUnitScales = (scale, unitType) => {
const scaleSystem = this.units[unitType][scale]['system'];
if (this.availableUnits) {
const available = this.availableUnits.split(",");
return Object.entries(this.units[unitType])
.filter(([scale, scaleInfo]) => {
return scaleInfo['system'] == scaleSystem && available.includes(scaleInfo['name']);
})
.map(([scale, _]) => parseFloat(scale))
.sort((a, b) => a - b);
} else {
return Object.entries(this.units[unitType])
.filter(([scale, scaleInfo]) => {
return scaleInfo['system'] == scaleSystem;
})
.map(([scale, _]) => parseFloat(scale))
.sort((a, b) => a - b);
}
};
}

View File

@@ -258,23 +258,38 @@ describe 'As an admin, I can manage products', feature: :admin_style_v3 do
end
end
it "saves a custom item unit name" do
within row_containing_name("Apples") do
tomselect_select "Items", from: "Unit scale"
fill_in "Items", with: "box"
describe "Changing unit scale" do
it "saves unit values using the new scale" do
within row_containing_name("Medium box") do
expect(page).to have_button "Unit", text: "1g"
end
within row_containing_name("Apples") do
tomselect_select "Weight (kg)", from: "Unit scale"
end
within row_containing_name("Medium box") do
# New scale is visible immediately
expect(page).to have_button "Unit", text: "1kg"
end
end
expect {
click_button "Save changes"
it "saves a custom item unit name" do
within row_containing_name("Apples") do
tomselect_select "Items", from: "Unit scale"
fill_in "Items", with: "box"
end
expect(page).to have_content "Changes saved"
product_a.reload
}.to change{ product_a.variant_unit }.to("items")
.and change{ product_a.variant_unit_name }.to("box")
expect {
click_button "Save changes"
within row_containing_name("Apples") do
pending
expect(page).to have_content "Items (box)"
expect(page).to have_content "Changes saved"
product_a.reload
}.to change{ product_a.variant_unit }.to("items")
.and change{ product_a.variant_unit_name }.to("box")
within row_containing_name("Apples") do
pending "#12005"
expect(page).to have_content "Items (box)"
end
end
end
@@ -284,6 +299,10 @@ describe 'As an admin, I can manage products', feature: :admin_style_v3 do
within row_containing_name("Medium box") do
click_on "Unit" # activate popout
fill_in "Unit value", with: "1000 boxed" # 1000 grams
find_field("Price").click # de-activate popout
# unit value has been parsed and displayed with unit
expect(page).to have_button "Unit", text: "1kg boxed"
end
expect {
@@ -302,6 +321,7 @@ describe 'As an admin, I can manage products', feature: :admin_style_v3 do
it "saves a custom variant unit display name" do
within row_containing_name("Medium box") do
click_on "Unit" # activate popout
fill_in "Display unit as", with: "250g box"
end
@@ -313,8 +333,9 @@ describe 'As an admin, I can manage products', feature: :admin_style_v3 do
}.to change{ variant_a1.unit_to_display }.to("250g box")
within row_containing_name("Medium box") do
expect(page).to have_field "Display unit as", with: "250g box"
expect(page).to have_button "Unit", text: "250g box"
click_on "Unit"
expect(page).to have_field "Display unit as", with: "250g box"
end
end
end