Merge pull request #12133 from dacook/buu/new-variant-11066

[BUU] Create a new variant inline
This commit is contained in:
Rachel Arnould
2024-02-16 14:32:17 +01:00
committed by GitHub
17 changed files with 588 additions and 138 deletions

View File

@@ -69,7 +69,7 @@ module Spree
%w(weight volume).include?(variant.product&.variant_unit)
}
validates :unit_value, numericality: { greater_than: 0 }
validates :unit_value, numericality: { greater_than: 0 }, allow_blank: true
validates :price, numericality: { greater_than_or_equal_to: 0 }
validates :unit_description, presence: true, if: ->(variant) {

View File

@@ -0,0 +1,35 @@
%td.with-image
%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
= f.hidden_field :id
= f.text_field :name, 'aria-label': t('admin.products_page.columns.name')
= error_message_on product, :name
%td.field
= f.text_field :sku, 'aria-label': t('admin.products_page.columns.sku')
= error_message_on product, :sku
%td.align-right
.content
= product.variant_unit.upcase_first
/ TODO: properly handle custom unit names
= WeightsAndMeasures::UNITS[product.variant_unit] && "(" + WeightsAndMeasures::UNITS[product.variant_unit][product.variant_unit_scale]["name"] + ")"
%td.align-right
-# empty
%td.align-right
-# empty
%td.align-left
.content= product.supplier&.name
%td.align-left
.content= product.primary_taxon&.name
%td.align-left
%td.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
= link_to t('admin.products_page.actions.edit'), edit_admin_product_path(product)
= link_to t('admin.products_page.actions.clone'), clone_admin_product_path(product)
%a{ "data-controller": "modal-link", "data-action": "click->modal-link#setModalDataSetOnConfirm click->modal-link#open",
"data-modal-link-target-value": "product-delete-modal", "class": "delete",
"data-modal-link-modal-dataset-value": {'data-current-id': product.id}.to_json }
= t('admin.products_page.actions.delete')

View File

@@ -1,9 +1,9 @@
= 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",
'data-bulk-form-error-value': defined?(error_counts),
} do |form|
html: { data: { reflex: 'submit->products#bulk_update', 'reflex-serialize-form': true,
controller: "bulk-form", 'bulk-form-disable-selector-value': "#sort,#filters",
'bulk-form-error-value': defined?(error_counts),
} } do |form|
= render(partial: "admin/shared/flashes", locals: { flashes: }) if defined? flashes
%table.products
%col{ width:"4%" }
@@ -50,85 +50,25 @@
%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{ 'data-record-id': product_form.object.id }
%tbody.relaxed{ data: { 'record-id': product_form.object.id, controller: "nested-form",
action: 'nested-form:add->bulk-form#registerElements' } }
%tr
%td.with-image
%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
= product_form.hidden_field :id
= product_form.text_field :name, 'aria-label': t('admin.products_page.columns.name')
= error_message_on product, :name
%td.field
= product_form.text_field :sku, 'aria-label': t('admin.products_page.columns.sku')
= error_message_on product, :sku
%td.align-right
.content
= product.variant_unit.upcase_first
/ TODO: properly handle custom unit names
= WeightsAndMeasures::UNITS[product.variant_unit] && "(" + WeightsAndMeasures::UNITS[product.variant_unit][product.variant_unit_scale]["name"] + ")"
%td.align-right
-# empty
%td.align-right
-# empty
%td.align-left
.content= product.supplier&.name
%td.align-left
.content= product.primary_taxon&.name
%td.align-left
%td.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
= link_to t('admin.products_page.actions.edit'), edit_admin_product_path(product)
= link_to t('admin.products_page.actions.clone'), clone_admin_product_path(product)
%a{ "data-controller": "modal-link", "data-action": "click->modal-link#setModalDataSetOnConfirm click->modal-link#open",
"data-modal-link-target-value": "product-delete-modal", "class": "delete",
"data-modal-link-modal-dataset-value": {'data-current-id': product.id}.to_json }
= t('admin.products_page.actions.delete')
= 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, variant_index:) do |variant_form|
= form.fields_for("products][#{product_index}][variants_attributes][", variant, index: variant_index) do |variant_form|
%tr.condensed
%td
-# empty
%td.field
= variant_form.hidden_field :id
= variant_form.text_field :display_name, 'aria-label': t('admin.products_page.columns.name'), placeholder: product.name
= error_message_on variant, :display_name
%td.field
= variant_form.text_field :sku, 'aria-label': t('admin.products_page.columns.sku')
= error_message_on variant, :sku
%td.align-right
.content= variant.unit_to_display
%td.field
= variant_form.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')}
= variant.on_demand ? t(:on_demand) : variant.on_hand
%div.on-hand__popout{ style: 'display: none;', 'data-controller': 'toggle-control', 'data-popout-target': "dialog" }
.field
= variant_form.number_field :on_hand, min: 0, 'aria-label': t('admin.products_page.columns.on_hand'), 'data-toggle-control-target': 'control', disabled: variant_form.object.on_demand
= error_message_on variant, :on_hand
.field.checkbox
= variant_form.label :on_demand do
= variant_form.check_box :on_demand, 'data-action': 'change->toggle-control#disableIfPresent change->popout#closeIfChecked'
= t(:on_demand)
%td.align-left
.content= variant.product.supplier&.name # same as product
%td.align-left
-# empty
%td.align-left
.content= variant.tax_category&.name || "None" # TODO: convert to dropdown, else translate hardcoded string.
%td.align-left
-# empty
%td.align-right
= render(VerticalEllipsisMenu::Component.new) do
= link_to t('admin.products_page.actions.edit'), edit_admin_product_variant_path(product, variant)
- if product.variants.size > 1
%a{ "data-controller": "modal-link", "data-action": "click->modal-link#setModalDataSetOnConfirm click->modal-link#open",
"data-modal-link-target-value": "variant-delete-modal", "class": "delete",
"data-modal-link-modal-dataset-value": {'data-current-id': variant.id}.to_json }
= t('admin.products_page.actions.delete')
= 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
= render partial: 'variant_row', locals: { variant: new_variant_form.object, f: new_variant_form }
%tr{ 'data-nested-form-target': "target" }
%tr.condensed
%td
%td{ colspan: 10 }
%button.secondary.condensed.naked.icon-plus{ 'data-action': "nested-form#add",
'aria-label': t('.new_variant') }
=t('.new_variant')

View File

@@ -0,0 +1,47 @@
%td
-# empty
%td.field
= 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
= f.text_field :sku, 'aria-label': t('admin.products_page.columns.sku')
= error_message_on variant, :sku
- 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
= 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')}
= variant.on_demand ? t(:on_demand) : variant.on_hand
%div.on-hand__popout{ 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
.field.checkbox
= 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
.content= variant.product.supplier&.name # same as product
%td.align-left
-# empty
%td.align-left
.content= variant.tax_category&.name || "None" # TODO: convert to dropdown, else translate hardcoded string.
%td.align-left
-# empty
%td.align-right
- if variant.persisted?
= render(VerticalEllipsisMenu::Component.new) do
= link_to t('admin.products_page.actions.edit'), edit_admin_product_variant_path(variant.product, variant)
- if variant.product.variants.size > 1
%a{ "data-controller": "modal-link", "data-action": "click->modal-link#setModalDataSetOnConfirm click->modal-link#open",
"data-modal-link-target-value": "variant-delete-modal", "class": "delete",
"data-modal-link-modal-dataset-value": {'data-current-id': variant.id}.to_json }
= t('admin.products_page.actions.delete')

View File

@@ -13,18 +13,7 @@ export default class BulkFormController extends Controller {
this.form = this.element;
// Start listening for any changes within the form
// this.element.addEventListener('change', this.toggleChanged.bind(this)); // dunno why this doesn't work
for (const element of this.form.elements) {
element.addEventListener("input", this.toggleChanged.bind(this)); // immediately respond to any change
// 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.
const recordId = recordContainer && recordContainer.dataset.recordId;
if (recordId) {
this.recordElements[recordId] ||= [];
this.recordElements[recordId].push(element);
}
}
this.#registerElements(this.form.elements);
this.toggleFormChanged();
}
@@ -35,6 +24,15 @@ export default class BulkFormController extends Controller {
window.removeEventListener("beforeunload", this.preventLeavingBulkForm);
}
// Register any new elements (may be called by another controller after dynamically adding fields)
registerElements() {
const registeredElements = Object.values(this.recordElements).flat();
// Select only elements that haven't been registered yet
const newElements = Array.from(this.form.elements).filter(n => !registeredElements.includes(n));
this.#registerElements(newElements);
}
toggleChanged(e) {
const element = e.target;
element.classList.toggle("changed", this.#isChanged(element));
@@ -50,7 +48,7 @@ export default class BulkFormController extends Controller {
const formChanged = changedRecordCount > 0 || this.errorValue;
// Show actions
this.actionsTarget.classList.toggle("hidden", !formChanged);
this.hasActionsTarget && this.actionsTarget.classList.toggle("hidden", !formChanged);
this.#disableOtherElements(formChanged); // like filters and sorting
// Display number of records changed
@@ -78,6 +76,20 @@ export default class BulkFormController extends Controller {
// private
#registerElements(elements) {
for (const element of elements) {
element.addEventListener("input", this.toggleChanged.bind(this)); // immediately respond to any change
// 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.
const recordId = recordContainer && recordContainer.dataset.recordId;
if (recordId) {
this.recordElements[recordId] ||= [];
this.recordElements[recordId].push(element);
}
}
}
#disableOtherElements(disable) {
if (!this.hasDisableSelectorValue) return;

View File

@@ -6,12 +6,15 @@ import StimulusReflex from "stimulus_reflex";
import consumer from "../channels/consumer";
import controller from "../controllers/application_controller";
import CableReady from "cable_ready";
import NestedForm from 'stimulus-rails-nested-form/dist/stimulus-rails-nested-form.umd.js' // the default module entry point is broken
const application = Application.start();
const context = require.context("controllers", true, /_controller\.js$/);
const contextComponents = require.context("../../components", true, /_controller\.js$/);
application.load(definitionsFromContext(context).concat(definitionsFromContext(contextComponents)));
application.register('nested-form', NestedForm);
application.consumer = consumer;
StimulusReflex.initialize(application, { controller, isolate: true });
StimulusReflex.debug = process.env.RAILS_ENV === "development";

View File

@@ -88,6 +88,11 @@
height: 100%;
}
}
// Reveal naked button text when any part of row is hovered
button.naked {
color: $color-link;
}
}
th,
@@ -160,20 +165,18 @@
}
// "Naked" inputs. Row hover helps reveal them.
tbody input:not([type="checkbox"]) {
background-color: $color-tbl-cell-bg;
height: auto;
font-size: inherit;
font-weight: inherit;
&:not(:focus):not(.changed):not([disabled]) {
border-color: transparent;
tbody {
input:not([type="checkbox"]) {
background-color: $color-tbl-cell-bg;
height: auto;
font-size: inherit;
font-weight: inherit;
}
}
.field_with_errors {
input {
border-color: $color-error;
:not(.field_with_errors) > {
input:not([type="checkbox"]):not(:focus):not(.changed):not([disabled]) {
border-color: transparent;
}
}
}
}

View File

@@ -1,3 +1,6 @@
// Button styles
// Design reference: https://github.com/openfoodfoundation/openfoodnetwork/wiki/Design-styleguide%3A-links-and-buttons
@mixin disabled-button() {
&:disabled,
&:disabled:hover {
@@ -47,11 +50,7 @@ button:not(.plain):not(.trix-button),
color: $color-btn-hover-text;
}
&.fullwidth {
width: 100%;
text-align: center;
}
// --- Colours ---
&.secondary {
background-color: $color-btn-secondary-bg;
color: $color-link;
@@ -65,6 +64,25 @@ button:not(.plain):not(.trix-button),
border-color: $color-link-focus;
color: $color-link-focus;
}
// "Naked" variation: text appears on hover
&.naked {
border: none;
color: transparent;
&:before {
color: $color-link;
}
&:hover {
background-color: $color-btn-secondary-bg;
color: $color-link;
}
&:focus {
background-color: $color-btn-secondary-hover-bg;
color: $color-link-focus;
}
}
}
&.disruptive {
@@ -86,6 +104,7 @@ button:not(.plain):not(.trix-button),
}
}
// --- Sizes ---
&.mini {
line-height: 18px;
height: auto; // DC: I don't like fixed heights.
@@ -110,15 +129,9 @@ button:not(.plain):not(.trix-button),
text-transform: uppercase;
}
&.red {
background-color: $color-btn-red-bg;
border-color: $color-btn-red-bg;
@include disabled-button(); // required for specifity
&:hover {
background-color: $color-btn-red-hover-bg;
border-color: $color-btn-red-hover-bg;
}
&.fullwidth {
width: 100%;
text-align: center;
}
.badge {
@@ -144,6 +157,7 @@ button:not(.plain):not(.trix-button),
}
}
// --- Reset buttons ---
input[type="reset"] {
// Reset button looks like a link, but has a border the same as buttons when active.
background: none;

View File

@@ -45,6 +45,7 @@ b {
}
// links
// Design reference: https://github.com/openfoodfoundation/openfoodnetwork/wiki/Design-styleguide%3A-links-and-buttons
//--------------------------------------------------------------
a:not(.button) {
color: $color-link;

View File

@@ -872,8 +872,9 @@ en:
invalid:
one: "%{count} product could not be saved. Please review the errors and try again."
other: "%{count} products could not be saved. Please review the errors and try again."
save: Save changes
reset: Discard changes
save: Save changes
new_variant: New variant
bulk_update:
success: Changes saved
edit_image:

View File

@@ -34,6 +34,7 @@
"shortcut-buttons-flatpickr": "^0.4.0",
"stimulus": "^3.2.2",
"stimulus-flatpickr": "^1.4.0",
"stimulus-rails-nested-form": "https://github.com/openfoodfoundation/stimulus-rails-nested-form.git#dist",
"stimulus_reflex": "3.5.0-rc3",
"tom-select": "^2.3.1",
"trix": "^2.0.10",

View File

@@ -181,6 +181,48 @@ describe("BulkFormController", () => {
});
});
describe("Adding new fields", () => {
beforeEach(() => {
document.body.innerHTML = `
<form id="form" data-controller="bulk-form" data-action="custom-event->bulk-form#registerElements",
<div data-record-id="1">
<input id="input1a" type="text" value="initial1a">
<template id="template">
<input id="input1b" type="text" value="initial1b">
</template>
</div>
<div data-record-id="2">
<input id="input2" type="text" value="initial2">
</div>
<input type="submit">
</form>
`;
});
describe("registerElements", () => {
beforeEach(() => {
// Add new field after controller has initialised
input1a.insertAdjacentHTML("afterend", template.innerHTML);
// Trigger bulk-form#registerElements
form.dispatchEvent(new Event("custom-event"));
});
it("onInput", () => {
input1b.value = 'updated1b';
input1b.dispatchEvent(new Event("input"));
// Expect only updated field to show changed
expect(input1b.classList).toContain('changed');
expect(input2.classList).not.toContain('changed');
// Change back to original value
input1b.value = 'initial1b';
input1b.dispatchEvent(new Event("input"));
expect(input1b.classList).not.toContain('changed');
});
})
});
// unable to test disconnect at this stage
// describe("disconnect()", () => {
// it("resets other elements", () => {

View File

@@ -36,8 +36,10 @@ describe ProductsReflex, type: :reflex, feature: :admin_style_v3 do
describe '#bulk_update' do
let!(:variant_a1) {
create(:variant, product: product_a, display_name: "Medium box", sku: "APL-01", price: 5.25,
on_hand: 5, on_demand: false)
product_a.variants.first.tap{ |v|
v.update! display_name: "Medium box", sku: "APL-01", price: 5.25, on_hand: 5,
on_demand: false
}
}
let!(:product_c) { create(:simple_product, name: "Carrots", sku: "CAR-00") }
let!(:product_b) { create(:simple_product, name: "Bananas", sku: "BAN-00") }
@@ -101,6 +103,63 @@ describe ProductsReflex, type: :reflex, feature: :admin_style_v3 do
expect(flash).to include success: "Changes saved"
end
it "creates new variants" do
# Form field names:
# '[products][0][id]' (hidden field)
# '[products][0][name]'
# '[products][0][variants_attributes][0][id]' (hidden field)
# '[products][0][variants_attributes][0][display_name]'
# '[products][0][variants_attributes][1][display_name]' (id is omitted for new record)
# '[products][0][variants_attributes][2][display_name]' (more than 1 new record is allowed)
params = {
"products" => {
"0" => {
"id" => product_a.id.to_s,
"name" => "Pommes",
"variants_attributes" => {
"0" => {
"id" => variant_a1.id.to_s,
"display_name" => "Large box",
},
"1" => {
"display_name" => "Small box",
"sku" => "POM-02",
"price" => "5.25",
"unit_value" => "0.5",
},
"2" => {
"sku" => "POM-03",
"price" => "15.25",
"unit_value" => "2",
},
},
},
},
}
expect{
run_reflex(:bulk_update, params:)
product_a.reload
variant_a1.reload
}.to change{ product_a.name }.to("Pommes")
.and change{ variant_a1.display_name }.to("Large box")
.and change{ product_a.variants.count }.by(2)
variant_a2 = product_a.variants[1]
expect(variant_a2.display_name).to eq "Small box"
expect(variant_a2.sku).to eq "POM-02"
expect(variant_a2.price).to eq 5.25
expect(variant_a2.unit_value).to eq 0.5
variant_a3 = product_a.variants[2]
expect(variant_a3.display_name).to be_nil
expect(variant_a3.sku).to eq "POM-03"
expect(variant_a3.price).to eq 15.25
expect(variant_a3.unit_value).to eq 2
expect(flash).to include success: "Changes saved"
end
describe "sorting" do
let(:params) {
{

View File

@@ -243,6 +243,46 @@ describe Sets::ProductSet do
end
end
end
context "new variant" do
let(:variants_attributes) {
[
{ id: product.variants.first.id.to_s }, # default variant unchanged
{ sku: "new sku", price: "5.00", unit_value: "5" }, # omit ID for new variant
]
}
it "creates new variant" do
expect {
product_set.save
expect(product_set.errors).to be_empty
}.to change { product.variants.count }.by(1)
expect(product.variants.last.sku).to eq "new sku"
expect(product.variants.last.price).to eq 5.00
expect(product.variants.last.unit_value).to eq 5
end
context "variant has error" do
let(:variants_attributes) {
[
{ id: product.variants.first.id.to_s }, # default variant unchanged
{ sku: "new sku", unit_value: "blah" }, # price missing, unit_value should be number
]
}
include_examples "nothing saved"
it "logs variant errors" do
product_set.save
expect(product_set.errors.full_messages).to include(
"Variant price is not a number",
"Variant price can't be blank",
"Variant unit value is not a number"
)
end
end
end
end
context 'when there are multiple products' do

View File

@@ -307,7 +307,7 @@ describe '
click_button 'Save Changes', match: :first
expect(page.find("#status-message"))
.to have_content "Variant unit value can't be blank Variant unit value is not a number"
.to have_content "Variant unit value can't be blank"
end
it "creating a variant with unit value is: '120g' and 'on_demand' checked" do
@@ -323,7 +323,7 @@ describe '
click_button 'Save Changes', match: :first
expect(page.find("#status-message"))
.to have_content "Variant unit value can't be blank Variant unit value is not a number"
.to have_content "Variant unit value can't be blank"
end
end
end

View File

@@ -164,8 +164,10 @@ describe 'As an admin, I can manage products', feature: :admin_style_v3 do
describe "updating" do
let!(:variant_a1) {
create(:variant, product: product_a, display_name: "Medium box", sku: "APL-01", price: 5.25,
on_hand: 5, on_demand: false)
product_a.variants.first.tap{ |v|
v.update! display_name: "Medium box", sku: "APL-01", price: 5.25, on_hand: 5,
on_demand: false
}
}
let!(:product_a) { create(:simple_product, name: "Apples", sku: "APL-00") }
before do
@@ -193,6 +195,8 @@ describe 'As an admin, I can manage products', feature: :admin_style_v3 do
expect {
click_button "Save changes"
expect(page).to have_content "Changes saved"
product_a.reload
variant_a1.reload
}.to change { product_a.name }.to("Pommes")
@@ -213,7 +217,6 @@ describe 'As an admin, I can manage products', feature: :admin_style_v3 do
expect(page).to have_css "button[aria-label='On Hand']", text: "6"
end
expect(page).to have_content "Changes saved"
end
it "switches stock to on-demand" do
@@ -226,6 +229,8 @@ describe 'As an admin, I can manage products', feature: :admin_style_v3 do
expect {
click_button "Save changes"
expect(page).to have_content "Changes saved"
product_a.reload
variant_a1.reload
}.to change{ variant_a1.on_demand }.to(true)
@@ -233,8 +238,6 @@ describe 'As an admin, I can manage products', feature: :admin_style_v3 do
within row_containing_name("Medium box") do
expect(page).to have_css "button[aria-label='On Hand']", text: "On demand"
end
expect(page).to have_content "Changes saved"
end
it "discards changes and reloads latest data" do
@@ -288,13 +291,13 @@ describe 'As an admin, I can manage products', feature: :admin_style_v3 do
expect {
click_button "Save changes"
expect(page).to have_content "1 product was saved correctly"
expect(page).to have_content "1 product could not be saved"
expect(page).to have_content "Please review the errors and try again"
product_a.reload
}.to_not change { product_a.name }
expect(page).to have_content("1 product was saved correctly")
expect(page).to have_content("1 product could not be saved")
expect(page).to have_content "Please review the errors and try again"
# (there's no identifier displayed, so the user must remember which product it is..)
within row_containing_name("") do
expect(page).to have_field "Name", with: ""
@@ -313,19 +316,264 @@ describe 'As an admin, I can manage products', feature: :admin_style_v3 do
end
it "saves changes after fixing errors" do
within row_containing_name("Apples") do
expect {
click_button "Save changes"
expect(page).to have_content("1 product could not be saved")
product_a.reload
}.to_not change { product_a.name }
within row_containing_name("") do
fill_in "Name", with: "Pommes"
fill_in "SKU", with: "POM-00"
end
expect {
click_button "Save changes"
expect(page).to have_content "Changes saved"
product_a.reload
variant_a1.reload
}.to change { product_a.name }.to("Pommes")
.and change{ product_a.sku }.to("POM-00")
end
end
expect(page).to have_content "Changes saved"
describe "adding variants" do
it "creates a new variant" do
click_on "New variant"
# find empty row for Apples
new_variant_row = find_field("Name", placeholder: "Apples", with: "").ancestor("tr")
expect(new_variant_row).to be_present
within new_variant_row do
fill_in "Name", with: "Large box"
fill_in "SKU", with: "APL-02"
fill_in "Unit", with: 1000
fill_in "Price", with: 10.25
click_on "On Hand" # activate popout
end
fill_in "On Hand", with: "3"
expect {
click_button "Save changes"
expect(page).to have_content "Changes saved"
product_a.reload
}.to change { product_a.variants.count }.by(1)
new_variant = product_a.variants.last
expect(new_variant.display_name).to eq "Large box"
expect(new_variant.sku).to eq "APL-02"
expect(new_variant.price).to eq 10.25
expect(new_variant.unit_value).to eq 1000
expect(new_variant.on_hand).to eq 3
within row_containing_name("Large box") do
expect(page).to have_field "Name", with: "Large box"
expect(page).to have_field "SKU", with: "APL-02"
expect(page).to have_field "Price", with: "10.25"
expect(page).to have_content "1kg"
expect(page).to have_css "button[aria-label='On Hand']", text: "3"
end
end
context "with invalid data" do
before do
click_on "New variant"
# find empty row for Apples
new_variant_row = find_field("Name", placeholder: "Apples", with: "").ancestor("tr")
expect(new_variant_row).to be_present
within new_variant_row do
fill_in "Name", with: "N" * 256 # too long
fill_in "SKU", with: "n" * 256
fill_in "Unit", with: "" # can't be blank
fill_in "Price", with: "10.25" # valid
end
end
it "shows errors for both existing and new variant fields" do
# Update existing variant with invalid data too
within row_containing_name("Medium box") do
fill_in "Name", with: "M" * 256
fill_in "SKU", with: "m" * 256
fill_in "Price", with: "10.25"
end
expect {
click_button "Save changes"
expect(page).to have_content "1 product could not be saved"
expect(page).to have_content "Please review the errors and try again"
variant_a1.reload
}.to_not change { variant_a1.display_name }
# New variant
within row_containing_name("N" * 256) do
expect(page).to have_field "Name", with: "N" * 256
expect(page).to have_field "SKU", with: "n" * 256
expect(page).to have_content "is too long"
expect(page).to have_field "Unit", with: ""
expect(page).to have_content "can't be blank"
expect(page).to have_field "Price", with: "10.25" # other updated value is retained
end
# Existing variant
within row_containing_name("M" * 256) do
expect(page).to have_field "Name", with: "M" * 256
expect(page).to have_field "SKU", with: "m" * 256
expect(page).to have_content "is too long"
end
end
it "saves changes after fixing errors" do
expect {
click_button "Save changes"
variant_a1.reload
}.to_not change { variant_a1.display_name }
within row_containing_name("N" * 256) do
fill_in "Name", with: "Nice box"
fill_in "SKU", with: "APL-02"
fill_in "Unit", with: 200
end
expect {
click_button "Save changes"
expect(page).to have_content "Changes saved"
product_a.reload
}.to change { product_a.variants.count }.by(1)
new_variant = product_a.variants.last
expect(new_variant.display_name).to eq "Nice box"
expect(new_variant.sku).to eq "APL-02"
expect(new_variant.price).to eq 10.25
expect(new_variant.unit_value).to eq 200
end
end
end
describe "adding variants" do
it "creates a new variant" do
click_on "New variant"
# find empty row for Apples
new_variant_row = find_field("Name", placeholder: "Apples", with: "").ancestor("tr")
expect(new_variant_row).to be_present
within new_variant_row do
fill_in "Name", with: "Large box"
fill_in "SKU", with: "APL-02"
fill_in "Unit", with: 1000
fill_in "Price", with: 10.25
click_on "On Hand" # activate popout
end
fill_in "On Hand", with: "3"
expect {
click_button "Save changes"
expect(page).to have_content "Changes saved"
product_a.reload
}.to change { product_a.variants.count }.by(1)
new_variant = product_a.variants.last
expect(new_variant.display_name).to eq "Large box"
expect(new_variant.sku).to eq "APL-02"
expect(new_variant.price).to eq 10.25
expect(new_variant.unit_value).to eq 1000
expect(new_variant.on_hand).to eq 3
within row_containing_name("Large box") do
expect(page).to have_field "Name", with: "Large box"
expect(page).to have_field "SKU", with: "APL-02"
expect(page).to have_field "Price", with: "10.25"
expect(page).to have_content "1kg"
expect(page).to have_css "button[aria-label='On Hand']", text: "3"
end
end
context "with invalid data" do
before do
click_on "New variant"
# find empty row for Apples
new_variant_row = find_field("Name", placeholder: "Apples", with: "").ancestor("tr")
expect(new_variant_row).to be_present
within new_variant_row do
fill_in "Name", with: "N" * 256 # too long
fill_in "SKU", with: "n" * 256
fill_in "Unit", with: "" # can't be blank
fill_in "Price", with: "10.25" # valid
end
end
it "shows errors for both existing and new variant fields" do
# Update existing variant with invalid data too
within row_containing_name("Medium box") do
fill_in "Name", with: "M" * 256
fill_in "SKU", with: "m" * 256
fill_in "Price", with: "10.25"
end
expect {
click_button "Save changes"
expect(page).to have_content "1 product could not be saved"
expect(page).to have_content "Please review the errors and try again"
variant_a1.reload
}.to_not change { variant_a1.display_name }
# New variant
within row_containing_name("N" * 256) do
expect(page).to have_field "Name", with: "N" * 256
expect(page).to have_field "SKU", with: "n" * 256
expect(page).to have_content "is too long"
expect(page).to have_field "Unit", with: ""
expect(page).to have_content "can't be blank"
expect(page).to have_field "Price", with: "10.25" # other updated value is retained
end
# Existing variant
within row_containing_name("M" * 256) do
expect(page).to have_field "Name", with: "M" * 256
expect(page).to have_field "SKU", with: "m" * 256
expect(page).to have_content "is too long"
end
end
it "saves changes after fixing errors" do
expect {
click_button "Save changes"
variant_a1.reload
}.to_not change { variant_a1.display_name }
within row_containing_name("N" * 256) do
fill_in "Name", with: "Nice box"
fill_in "SKU", with: "APL-02"
fill_in "Unit", with: "200"
end
expect {
click_button "Save changes"
expect(page).to have_content "Changes saved"
product_a.reload
}.to change { product_a.variants.count }.by(1)
new_variant = product_a.variants.last
expect(new_variant.display_name).to eq "Nice box"
expect(new_variant.sku).to eq "APL-02"
expect(new_variant.price).to eq 10.25
expect(new_variant.unit_value).to eq 200
end
end
end

View File

@@ -8350,6 +8350,10 @@ stimulus-flatpickr@^1.4.0:
resolved "https://registry.yarnpkg.com/stimulus-flatpickr/-/stimulus-flatpickr-1.4.0.tgz#a41071a3e69cfc50b7eaaacf356fc0ab1ab0543c"
integrity sha512-rcC/c9+E+f5W2kOjaaLShtf3i+p95ACqt+oGzSAgeuZh2YeIN8gW4EWO7h0STBLzSVPl6BjIfPWP7upMPavIVQ==
"stimulus-rails-nested-form@https://github.com/openfoodfoundation/stimulus-rails-nested-form.git#dist":
version "4.1.0"
resolved "https://github.com/openfoodfoundation/stimulus-rails-nested-form.git#d3b82ea638a7156f1122736cf739ab1821a1817e"
stimulus@^3.2.2:
version "3.2.2"
resolved "https://registry.yarnpkg.com/stimulus/-/stimulus-3.2.2.tgz#a2e955f43e12e2e5784b175d4df5517ef678aa68"