diff --git a/app/assets/javascripts/admin/bulk_product_update.js.coffee b/app/assets/javascripts/admin/bulk_product_update.js.coffee index 9f0c5a7a81..910fb39f8b 100644 --- a/app/assets/javascripts/admin/bulk_product_update.js.coffee +++ b/app/assets/javascripts/admin/bulk_product_update.js.coffee @@ -248,7 +248,6 @@ angular.module("ofn.admin").controller "AdminProductEditCtrl", ($scope, $timeout else product.variant_unit = product.variant_unit_scale = null - $scope.packVariant product, product.master if product.master if product.variants for id, variant of product.variants @@ -299,7 +298,6 @@ filterSubmitProducts = (productsToFilter) -> if product.hasOwnProperty("id") filteredProduct = {id: product.id} filteredVariants = [] - filteredMaster = null hasUpdatableProperty = false if product.hasOwnProperty("variants") @@ -309,16 +307,6 @@ filterSubmitProducts = (productsToFilter) -> variantHasUpdatableProperty = result.hasUpdatableProperty filteredVariants.push filteredVariant if variantHasUpdatableProperty - if product.master?.hasOwnProperty("unit_value") - filteredMaster ?= { id: product.master.id } - filteredMaster.unit_value = product.master.unit_value - if product.master?.hasOwnProperty("unit_description") - filteredMaster ?= { id: product.master.id } - filteredMaster.unit_description = product.master.unit_description - if product.master?.hasOwnProperty("display_as") - filteredMaster ?= { id: product.master.id } - filteredMaster.display_as = product.master.display_as - if product.hasOwnProperty("sku") filteredProduct.sku = product.sku hasUpdatableProperty = true @@ -350,9 +338,6 @@ filterSubmitProducts = (productsToFilter) -> if product.hasOwnProperty("inherits_properties") filteredProduct.inherits_properties = product.inherits_properties hasUpdatableProperty = true - if filteredMaster? - filteredProduct.master_attributes = filteredMaster - hasUpdatableProperty = true if filteredVariants.length > 0 # Note that the name of the property changes to enable mass assignment of variants attributes with rails filteredProduct.variants_attributes = filteredVariants hasUpdatableProperty = true diff --git a/app/helpers/bulk_form_builder.rb b/app/helpers/bulk_form_builder.rb new file mode 100644 index 0000000000..dcb45ae3bc --- /dev/null +++ b/app/helpers/bulk_form_builder.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +class BulkFormBuilder < ActionView::Helpers::FormBuilder + def text_field(field, **opts) + # Mark field if it is changed (unsaved) + changed_method = "#{field}_changed?" + if object.respond_to?(changed_method) && object.public_send(changed_method) + opts[:class] = "#{opts[:class]} changed".strip + end + + super(field, **opts) + end +end diff --git a/app/models/spree/price.rb b/app/models/spree/price.rb index 68d5caf306..710e4ef5c3 100644 --- a/app/models/spree/price.rb +++ b/app/models/spree/price.rb @@ -24,6 +24,10 @@ module Spree amount end + def price_changed? + amount_changed? + end + def price=(price) self[:amount] = parse_price(price) end diff --git a/app/models/spree/variant.rb b/app/models/spree/variant.rb index 528790313b..ea05767a63 100644 --- a/app/models/spree/variant.rb +++ b/app/models/spree/variant.rb @@ -50,7 +50,8 @@ module Spree has_many :prices, class_name: 'Spree::Price', dependent: :destroy - delegate :display_price, :display_amount, :price, :price=, :currency, :currency=, + delegate :display_price, :display_amount, :price, :price_changed?, :price=, + :currency, :currency=, to: :find_or_build_default_price has_many :exchange_variants diff --git a/app/services/sets/model_set.rb b/app/services/sets/model_set.rb index 7347e2d9c9..1db02abf92 100644 --- a/app/services/sets/model_set.rb +++ b/app/services/sets/model_set.rb @@ -79,5 +79,11 @@ module Sets def persisted? false end + + def find_model(collection, model_id) + collection.find do |model| + model.id.to_s == model_id.to_s && model.persisted? + end + end end end diff --git a/app/services/sets/product_set.rb b/app/services/sets/product_set.rb index 7eb73e3bee..23be044239 100644 --- a/app/services/sets/product_set.rb +++ b/app/services/sets/product_set.rb @@ -59,12 +59,11 @@ module Sets ExchangeVariantDeleter.new.delete(product) if product.saved_change_to_supplier_id? - update_product_variants(product, attributes) && - update_product_master(product, attributes) + update_product_variants(product, attributes) end def update_product_only_attributes(product, attributes) - variant_related_attrs = [:id, :variants_attributes, :master_attributes] + variant_related_attrs = [:id, :variants_attributes] product_related_attrs = attributes.except(*variant_related_attrs) return true if product_related_attrs.blank? @@ -94,12 +93,6 @@ module Sets update_variants_attributes(product, attributes[:variants_attributes]) end - def update_product_master(product, attributes) - return true unless attributes[:master_attributes] - - create_or_update_variant(product, attributes[:master_attributes]) - end - def update_variants_attributes(product, variants_attributes) variants_attributes.each do |attributes| create_or_update_variant(product, attributes) @@ -119,6 +112,7 @@ module Sets def create_variant(product, variant_attributes) return if variant_attributes.blank? + # 'You need to save the variant to create a stock item before you can set stock levels.' on_hand = variant_attributes.delete(:on_hand) on_demand = variant_attributes.delete(:on_demand) @@ -147,11 +141,5 @@ module Sets report.add_metadata(:variant_error, variant.errors.first) unless variant.valid? end end - - def find_model(collection, model_id) - collection.find do |model| - model.id.to_s == model_id.to_s && model.persisted? - end - end end end diff --git a/app/views/admin/products_v3/_table.html.haml b/app/views/admin/products_v3/_table.html.haml index 7417f676cd..33806fc195 100644 --- a/app/views/admin/products_v3/_table.html.haml +++ b/app/views/admin/products_v3/_table.html.haml @@ -1,10 +1,11 @@ = form_with url: bulk_update_admin_products_path, method: :patch, id: "products-form", + builder: BulkFormBuilder, html: {'data-reflex-serialize-form': true, 'data-reflex': 'submit->products#bulk_update', 'data-controller': "bulk-form", 'data-bulk-form-disable-selector-value': "#sort,#filters"} do |form| %fieldset.form-actions.hidden{ 'data-bulk-form-target': "actions" } .container .status.ten.columns - .modified_summary{ 'data-bulk-form-target': "modifiedSummary", 'data-translation-key': 'admin.products_v3.table.modified_summary'} + .changed_summary{ 'data-bulk-form-target': "changedSummary", 'data-translation-key': 'admin.products_v3.table.changed_summary'} - if defined?(error_msg) && error_msg.present? .error = error_msg diff --git a/app/webpacker/controllers/bulk_form_controller.js b/app/webpacker/controllers/bulk_form_controller.js index 526aa1d5b5..842a1aaa89 100644 --- a/app/webpacker/controllers/bulk_form_controller.js +++ b/app/webpacker/controllers/bulk_form_controller.js @@ -1,8 +1,8 @@ import { Controller } from "stimulus"; -// Manages "modified" state for a form with multiple records +// Manages "changed" state for a form with multiple records export default class BulkFormController extends Controller { - static targets = ["actions", "modifiedSummary"]; + static targets = ["actions", "changedSummary"]; static values = { disableSelector: String, }; @@ -12,10 +12,10 @@ export default class BulkFormController extends Controller { this.form = this.element; // Start listening for any changes within the form - // this.element.addEventListener('change', this.toggleModified.bind(this)); // dunno why this doesn't work + // this.element.addEventListener('change', this.toggleChanged.bind(this)); // dunno why this doesn't work for (const element of this.form.elements) { - element.addEventListener("keyup", this.toggleModified.bind(this)); // instant response - element.addEventListener("change", this.toggleModified.bind(this)); // just in case (eg right-click paste) + element.addEventListener("keyup", this.toggleChanged.bind(this)); // instant response + element.addEventListener("change", this.toggleChanged.bind(this)); // just in case (eg right-click paste) // Set up a tree of fields according to their associated record const recordContainer = element.closest("[data-record-id]"); // The JS could be more efficient if this data was added to each element. But I didn't want to pollute the HTML too much. @@ -33,32 +33,32 @@ export default class BulkFormController extends Controller { window.removeEventListener("beforeunload", this.preventLeavingBulkForm); } - toggleModified(e) { + toggleChanged(e) { const element = e.target; - element.classList.toggle("modified", this.#isModified(element)); + element.classList.toggle("changed", this.#isChanged(element)); - this.toggleFormModified(); + this.toggleFormChanged(); } - toggleFormModified() { - // For each record, check if any fields are modified - const modifiedRecordCount = Object.values(this.recordElements).filter((elements) => - elements.some(this.#isModified) + toggleFormChanged() { + // For each record, check if any fields are changed + const changedRecordCount = Object.values(this.recordElements).filter((elements) => + elements.some(this.#isChanged) ).length; - const formModified = modifiedRecordCount > 0; + const formChanged = changedRecordCount > 0; // Show actions - this.actionsTarget.classList.toggle("hidden", !formModified); - this.#disableOtherElements(formModified); // like filters and sorting + this.actionsTarget.classList.toggle("hidden", !formChanged); + this.#disableOtherElements(formChanged); // like filters and sorting - // Display number of records modified - const key = this.modifiedSummaryTarget && this.modifiedSummaryTarget.dataset.translationKey; + // Display number of records changed + const key = this.changedSummaryTarget && this.changedSummaryTarget.dataset.translationKey; if (key) { - this.modifiedSummaryTarget.textContent = I18n.t(key, { count: modifiedRecordCount }); + this.changedSummaryTarget.textContent = I18n.t(key, { count: changedRecordCount }); } // Prevent accidental data loss - if (formModified) { + if (formChanged) { window.addEventListener("beforeunload", this.preventLeavingBulkForm); } else { window.removeEventListener("beforeunload", this.preventLeavingBulkForm); @@ -85,7 +85,7 @@ export default class BulkFormController extends Controller { } } - #isModified(element) { + #isChanged(element) { return element.value != element.defaultValue; } } diff --git a/app/webpacker/css/admin/products_v3.scss b/app/webpacker/css/admin/products_v3.scss index 4ea63f3a28..2764f4b954 100644 --- a/app/webpacker/css/admin/products_v3.scss +++ b/app/webpacker/css/admin/products_v3.scss @@ -68,7 +68,7 @@ .content { // Plain content fields need help to align with text in inputs (due to vertical-align) - margin: $vpadding-txt+1px 0; + margin: $vpadding-txt + 1px 0; @extend .line-clamp-1; } @@ -126,8 +126,8 @@ border-color: $color-txt-hover-brd; } - &.modified { - border-color: $color-txt-modified-brd; + &.changed { + border-color: $color-txt-changed-brd; } } diff --git a/app/webpacker/css/admin_v3/globals/variables.scss b/app/webpacker/css/admin_v3/globals/variables.scss index aa45159e59..73a32c877b 100644 --- a/app/webpacker/css/admin_v3/globals/variables.scss +++ b/app/webpacker/css/admin_v3/globals/variables.scss @@ -74,7 +74,7 @@ $color-sel-hover-bg: $lighter-grey !default; $color-txt-brd: $color-border !default; $color-txt-text: $near-black !default; $color-txt-hover-brd: $teal !default; -$color-txt-modified-brd: $bright-orange !default; +$color-txt-changed-brd: $bright-orange !default; $vpadding-txt: 5px; $hpadding-txt: 8px; diff --git a/app/webpacker/css/admin_v3/shared/forms.scss b/app/webpacker/css/admin_v3/shared/forms.scss index 6b9cb9cf8e..96cef47615 100644 --- a/app/webpacker/css/admin_v3/shared/forms.scss +++ b/app/webpacker/css/admin_v3/shared/forms.scss @@ -31,8 +31,8 @@ fieldset { opacity: 0.7; } - &.modified { - border-color: $color-txt-modified-brd; + &.changed { + border-color: $color-txt-changed-brd; } } diff --git a/config/locales/en.yml b/config/locales/en.yml index 2e9842a73f..68cd6ef091 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -833,7 +833,7 @@ en: import_products: Import multiple products no_products_found_for_search: No products found for your search criteria table: - modified_summary: + changed_summary: zero: "" one: "%{count} product modified." other: "%{count} products modified." diff --git a/spec/helpers/bulk_form_builder_spec.rb b/spec/helpers/bulk_form_builder_spec.rb new file mode 100644 index 0000000000..00ab150c2c --- /dev/null +++ b/spec/helpers/bulk_form_builder_spec.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +require 'spec_helper' + +class TestHelper < ActionView::Base; end + +describe BulkFormBuilder do + describe '#text_field' do + let(:product) { create(:product) } + let(:form) { BulkFormBuilder.new(:product, product, self, {}) } + + it { expect(form.text_field(:name)).to_not include "changed" } + + context "attribute has been changed" do + before { product.assign_attributes name: "updated name" } + + it { expect(form.text_field(:name)).to include "changed" } + + context "and saved" do + before { product.save } + + it { expect(form.text_field(:name)).to_not include "changed" } + end + end + end +end diff --git a/spec/javascripts/stimulus/bulk_form_controller_test.js b/spec/javascripts/stimulus/bulk_form_controller_test.js index cc31531950..db0f990eef 100644 --- a/spec/javascripts/stimulus/bulk_form_controller_test.js +++ b/spec/javascripts/stimulus/bulk_form_controller_test.js @@ -32,7 +32,7 @@ describe("BulkFormController", () => {