mirror of
https://github.com/openfoodfoundation/openfoodnetwork
synced 2026-01-24 20:36:49 +00:00
Merge pull request #13252 from rioug/13159-add-tag-variant
[Variant tags] Add variant tag on Bulk Edit Product screen
This commit is contained in:
@@ -1 +1,3 @@
|
||||
%h1 #{@title}
|
||||
- # the stimulus contoller "example-component--example" lives in app/component/example_component/example_controller.js
|
||||
%div{ "data-controller": "example-component--example"}
|
||||
%h1 #{@title}
|
||||
|
||||
4
app/components/example_component/example_controller.js
Normal file
4
app/components/example_component/example_controller.js
Normal file
@@ -0,0 +1,4 @@
|
||||
// This controller will be called "example-component--example", ie "component-subdirectory--js-file-name"
|
||||
import { Controller } from "stimulus";
|
||||
|
||||
export default class extends Controller {}
|
||||
14
app/components/tag_list_input_component.rb
Normal file
14
app/components/tag_list_input_component.rb
Normal file
@@ -0,0 +1,14 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class TagListInputComponent < ViewComponent::Base
|
||||
# method in a "hidden_field" form helper and is the method used to get a list of tag on the model
|
||||
def initialize(form:, method:, tags:,
|
||||
placeholder: I18n.t("components.tag_list_input.default_placeholder"))
|
||||
@f = form
|
||||
@method = method
|
||||
@tags = tags
|
||||
@placeholder = placeholder
|
||||
end
|
||||
|
||||
attr_reader :f, :method, :tags, :placeholder
|
||||
end
|
||||
@@ -0,0 +1,19 @@
|
||||
%div{ "data-controller": "tag-list-input-component--tag-list-input" }
|
||||
.tags-input
|
||||
.tags
|
||||
- # We use display:none instead of hidden field, so changes to the value can be picked up by the bulkFormController
|
||||
= f.text_field method.to_sym, value: tags.join(","), "data-tag-list-input-component--tag-list-input-target": "tagList", "style": "display: none"
|
||||
%ul.tag-list{"data-tag-list-input-component--tag-list-input-target": "list"}
|
||||
%template{"data-tag-list-input-component--tag-list-input-target": "template"}
|
||||
%li.tag-item
|
||||
.tag-template
|
||||
%span
|
||||
%a.remove-button{ "data-action": "click->tag-list-input-component--tag-list-input#removeTag" }
|
||||
✖
|
||||
- tags.each do |tag|
|
||||
%li.tag-item
|
||||
.tag-template
|
||||
%span=tag
|
||||
%a.remove-button{ "data-action": "click->tag-list-input-component--tag-list-input#removeTag" }
|
||||
✖
|
||||
= text_field_tag "variant_add_tag_#{f.object.id}".to_sym, nil, class: "input", placeholder: placeholder, "data-action": "keydown.enter->tag-list-input-component--tag-list-input#addTag keyup->tag-list-input-component--tag-list-input#filterInput", "data-tag-list-input-component--tag-list-input-target": "newTag"
|
||||
@@ -0,0 +1,68 @@
|
||||
// Tags input
|
||||
.tags-input {
|
||||
display: block;
|
||||
|
||||
.tags {
|
||||
-moz-appearance: textfield;
|
||||
-webkit-appearance: textfield;
|
||||
overflow: hidden;
|
||||
word-wrap: break-word;
|
||||
cursor: text;
|
||||
background-color: #fff;
|
||||
height: 100%;
|
||||
box-shadow: none;
|
||||
|
||||
&:has(.changed) {
|
||||
border: 1px solid $color-txt-changed-brd;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.tag-error {
|
||||
color: $color-error;
|
||||
}
|
||||
|
||||
.tag-list {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
li.tag-item {
|
||||
border-radius: 3px;
|
||||
margin: 2px 0 2px 3px;
|
||||
padding: 0 5px;
|
||||
display: inline-block;
|
||||
float: left;
|
||||
font-size: 14px;
|
||||
line-height: 25px;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
color: white !important;
|
||||
background-image: none;
|
||||
background-color: $teal;
|
||||
|
||||
.remove-button {
|
||||
margin: 0 0 0 5px;
|
||||
padding: 0;
|
||||
border: none;
|
||||
background: 0 0;
|
||||
cursor: pointer;
|
||||
vertical-align: middle;
|
||||
font:
|
||||
700 16px Arial,
|
||||
sans-serif;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
.input {
|
||||
border: 0;
|
||||
outline: 0;
|
||||
margin: 2px;
|
||||
padding: 0 0 0 5px;
|
||||
float: left;
|
||||
height: 26px;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import { Controller } from "stimulus";
|
||||
|
||||
export default class extends Controller {
|
||||
static targets = ["tagList", "newTag", "template", "list"];
|
||||
|
||||
addTag(event) {
|
||||
// prevent hotkey form submitting the form (default action for "enter" key)
|
||||
event.preventDefault();
|
||||
|
||||
// Check if tag already exist
|
||||
const newTagName = this.newTagTarget.value.trim();
|
||||
if (newTagName.length == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const tags = this.tagListTarget.value.split(",");
|
||||
const index = tags.indexOf(newTagName);
|
||||
if (index != -1) {
|
||||
// highlight the value in red
|
||||
this.newTagTarget.classList.add("tag-error");
|
||||
return;
|
||||
}
|
||||
|
||||
// add to tagList
|
||||
this.tagListTarget.value = this.tagListTarget.value.concat(`,${newTagName}`);
|
||||
|
||||
// Create new li component with value
|
||||
const newTagElement = this.templateTarget.content.cloneNode(true);
|
||||
const spanElement = newTagElement.querySelector("span");
|
||||
spanElement.innerText = newTagName;
|
||||
this.listTarget.appendChild(newTagElement);
|
||||
|
||||
// Clear new tag value
|
||||
this.newTagTarget.value = "";
|
||||
}
|
||||
|
||||
removeTag(event) {
|
||||
// Text to remove
|
||||
const tagName = event.srcElement.previousElementSibling.textContent;
|
||||
|
||||
// Remove tag from list
|
||||
const tags = this.tagListTarget.value.split(",");
|
||||
this.tagListTarget.value = tags.filter(tag => tag != tagName).join(",");
|
||||
|
||||
// manualy dispatch an Input event so the change gets picked up by the bulk form controller
|
||||
this.tagListTarget.dispatchEvent(new InputEvent("input"));
|
||||
|
||||
// Remove HTML element from the list
|
||||
event.srcElement.parentElement.parentElement.remove();
|
||||
}
|
||||
|
||||
filterInput(event) {
|
||||
// clear error class if key is not enter
|
||||
if (event.key !== "Enter") {
|
||||
this.newTagTarget.classList.remove("tag-error");
|
||||
}
|
||||
|
||||
// Strip comma from tag name
|
||||
if (event.key === ",") {
|
||||
event.srcElement.value = event.srcElement.value.replace(",", "");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,8 @@ module Spree
|
||||
|
||||
acts_as_paranoid
|
||||
|
||||
acts_as_taggable
|
||||
|
||||
searchable_attributes :sku, :display_as, :display_name, :primary_taxon_id, :supplier_id
|
||||
searchable_associations :product, :default_price, :primary_taxon, :supplier
|
||||
searchable_scopes :active, :deleted
|
||||
|
||||
@@ -8,7 +8,7 @@ module PermittedAttributes
|
||||
:shipping_category_id, :price, :unit_value,
|
||||
:unit_description, :variant_unit, :variant_unit_name, :variant_unit_scale, :display_name,
|
||||
:display_as, :tax_category_id, :weight, :height, :width, :depth, :taxon_ids,
|
||||
:primary_taxon_id, :supplier_id
|
||||
:primary_taxon_id, :supplier_id, :tag_list
|
||||
]
|
||||
end
|
||||
end
|
||||
|
||||
@@ -18,6 +18,9 @@
|
||||
%td.col-category.align-left
|
||||
-# empty
|
||||
%td.col-tax_category.align-left
|
||||
- if feature?(:variant_tag, spree_current_user)
|
||||
%td.col-tags.align-left
|
||||
-# empty
|
||||
%td.col-inherits_properties.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
|
||||
|
||||
@@ -19,7 +19,8 @@
|
||||
%tr{ 'data-nested-form-target': "target" }
|
||||
%tr.condensed
|
||||
%td
|
||||
%td{ colspan: 11 }
|
||||
- colspan = feature?(:variant_tag, spree_current_user) ? 12 : 11
|
||||
%td{ colspan: "#{colspan}" }
|
||||
%button.secondary.condensed.naked.icon-plus{ 'data-action': "nested-form#add",
|
||||
'aria-label': t('.new_variant') }
|
||||
=t('.new_variant')
|
||||
|
||||
@@ -26,11 +26,14 @@
|
||||
%col.col-producer{ style:"min-width: 6em" }= # (grow to fill)
|
||||
%col.col-category{ width:"8%" }
|
||||
%col.col-tax_category{ width:"8%" }
|
||||
- if feature?(:variant_tag, spree_current_user)
|
||||
%col.col-tags{ width:"8%" }
|
||||
%col.col-inherits_properties{ width:"5%" }
|
||||
%col{ width:"5%", style:"min-width: 3em"}= # Actions
|
||||
%thead
|
||||
%tr
|
||||
%td.form-actions-wrapper{ colspan: 12 }
|
||||
- colspan = feature?(:variant_tag, spree_current_user) ? 13 : 12
|
||||
%td.form-actions-wrapper{ colspan: "#{colspan}" }
|
||||
.form-actions-wrapper2
|
||||
%fieldset.form-actions{ class: ("hidden" unless defined?(@error_counts)), 'data-bulk-form-target': "actions" }
|
||||
.container
|
||||
@@ -60,6 +63,8 @@
|
||||
%th.align-left.col-producer= t('admin.products_page.columns.producer')
|
||||
%th.align-left.col-category= t('admin.products_page.columns.category')
|
||||
%th.align-left.col-tax_category= t('admin.products_page.columns.tax_category')
|
||||
- if feature?(:variant_tag, spree_current_user)
|
||||
%th.align-left.col-tags= t('admin.products_page.columns.tags')
|
||||
%th.align-left.col-inherits_properties= t('admin.products_page.columns.inherits_properties')
|
||||
%th.align-right= t('admin.products_page.columns.actions')
|
||||
- products.each_with_index do |product, product_index|
|
||||
|
||||
@@ -74,6 +74,9 @@
|
||||
aria_label: t('.tax_category_field_name'),
|
||||
placeholder_value: t('.search_for_tax_categories')))
|
||||
= error_message_on variant, :tax_category
|
||||
- if feature?(:variant_tag, spree_current_user)
|
||||
%td.col-tags.field.naked_inputs
|
||||
= render TagListInputComponent.new(form: f, method: "tag_list", tags: variant.tag_list, placeholder: t('.add_a_tag'))
|
||||
%td.col-inherits_properties.align-left
|
||||
-# empty
|
||||
%td.align-right
|
||||
|
||||
@@ -168,6 +168,8 @@ export default class BulkFormController extends Controller {
|
||||
|
||||
return !areBothBlank && selectedOption !== defaultSelected;
|
||||
} else {
|
||||
// This doesn't work with hidden field
|
||||
// Workaround: use a text field with "display:none;"
|
||||
return element.defaultValue !== undefined && element.value != element.defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ export default class ColumnPreferencesController extends Controller {
|
||||
const element = e.target || e;
|
||||
const name = element.dataset.columnName;
|
||||
|
||||
// Css defined in app/webpacker/css/admin/products_v3.scss
|
||||
this.table.classList.toggle(`hide-${name}`, !element.checked);
|
||||
|
||||
// Reset cell colspans
|
||||
|
||||
@@ -184,9 +184,8 @@
|
||||
}
|
||||
|
||||
// Hide columns
|
||||
$columns:
|
||||
"image", "name", "sku", "unit_scale", "unit", "price", "on_hand", "producer", "category",
|
||||
"tax_category", "inherits_properties";
|
||||
$columns: "image", "name", "sku", "unit_scale", "unit", "price", "on_hand", "producer",
|
||||
"category", "tax_category", "tags", "inherits_properties";
|
||||
@each $col in $columns {
|
||||
&.hide-#{$col} {
|
||||
.col-#{$col} {
|
||||
|
||||
@@ -128,6 +128,7 @@
|
||||
|
||||
@import "app/components/modal_component/modal_component";
|
||||
@import "app/components/vertical_ellipsis_menu/component"; // admin_v3 and only V3
|
||||
@import "app/components/tag_list_input_component/tag_list_input_component";
|
||||
@import "app/webpacker/css/admin/trix.scss";
|
||||
|
||||
@import "terms_of_service_banner"; // admin_v3
|
||||
|
||||
@@ -638,6 +638,7 @@ en:
|
||||
inherits_properties: "Inherits Properties?"
|
||||
import_date: "Import Date"
|
||||
actions: Actions
|
||||
tags: Tags
|
||||
columns_selector:
|
||||
unit: Unit
|
||||
price: Price
|
||||
@@ -1018,6 +1019,7 @@ en:
|
||||
tax_category_field_name: "Tax Category"
|
||||
producer_field_name: "Producer"
|
||||
select_unit_scale: Select unit scale
|
||||
add_a_tag: Add a tag
|
||||
clone:
|
||||
success: Successfully cloned the product
|
||||
error: Unable to clone the product
|
||||
@@ -5058,6 +5060,8 @@ See the %{link} to find out more about %{sitename}'s features and to start using
|
||||
pagination:
|
||||
next: Next
|
||||
previous: Previous
|
||||
tag_list_input:
|
||||
default_placeholder: Add a tag
|
||||
|
||||
|
||||
# Gem to prevent bot form submissions
|
||||
|
||||
@@ -81,7 +81,7 @@ module OpenFoodNetwork
|
||||
producer_visibility = display_producer_column?(user)
|
||||
|
||||
I18n.with_options scope: 'admin.products_page.columns' do
|
||||
{
|
||||
columns = {
|
||||
image: { name: t(:image), visible: true },
|
||||
name: { name: t(:name), visible: true },
|
||||
sku: { name: t(:sku), visible: true },
|
||||
@@ -92,8 +92,14 @@ module OpenFoodNetwork
|
||||
producer: { name: t(:producer), visible: producer_visibility },
|
||||
category: { name: t(:category), visible: true },
|
||||
tax_category: { name: t(:tax_category), visible: true },
|
||||
inherits_properties: { name: t(:inherits_properties), visible: true },
|
||||
}
|
||||
if OpenFoodNetwork::FeatureToggle.enabled?(:variant_tag, user)
|
||||
columns[:tags] = { name: t(:tags), visible: true }
|
||||
end
|
||||
|
||||
columns[:inherits_properties] = { name: t(:inherits_properties), visible: true }
|
||||
|
||||
columns
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -61,6 +61,9 @@ module OpenFoodNetwork
|
||||
"open_in_same_tab" => <<~DESC,
|
||||
Open the admin dashboard in the same tab instead of a new tab.
|
||||
DESC
|
||||
"variant_tag" => <<~DESC,
|
||||
Variant Tag are available on the Bulk Edit Products page.
|
||||
DESC
|
||||
}.merge(conditional_features).freeze;
|
||||
|
||||
# Features you would like to be enabled to start with.
|
||||
|
||||
@@ -15,16 +15,16 @@ describe("BulkFormController", () => {
|
||||
// Mock I18n. TODO: moved to a shared helper
|
||||
beforeAll(() => {
|
||||
const mockedT = jest.fn();
|
||||
mockedT.mockImplementation((string, opts) => (string + ', ' + JSON.stringify(opts)));
|
||||
mockedT.mockImplementation((string, opts) => string + ", " + JSON.stringify(opts));
|
||||
|
||||
global.I18n = {
|
||||
t: mockedT
|
||||
global.I18n = {
|
||||
t: mockedT,
|
||||
};
|
||||
})
|
||||
});
|
||||
// (jest still doesn't have aroundEach https://github.com/jestjs/jest/issues/4543 )
|
||||
afterAll(() => {
|
||||
delete global.I18n;
|
||||
})
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
document.body.innerHTML = `
|
||||
@@ -33,36 +33,38 @@ describe("BulkFormController", () => {
|
||||
<form data-controller="bulk-form" data-bulk-form-disable-selector-value="#disable1,#disable2">
|
||||
<div id="actions" data-bulk-form-target="actions" class="hidden"></div>
|
||||
<div id="changed_summary" data-bulk-form-target="changedSummary" data-translation-key="changed_summary"></div>
|
||||
<div data-record-id="1">
|
||||
<input id="input1a" type="text" value="initial1a">
|
||||
<input id="input1b" type="text" value="initial1b">
|
||||
<select id="select1">
|
||||
<option>one</option>
|
||||
<option selected>two</option>
|
||||
</select>
|
||||
<button>a button is counted as a form element, but value is undefined</button>
|
||||
</div>
|
||||
<div data-record-id="2">
|
||||
<input id="input2" type="text" value="initial2">
|
||||
</div>
|
||||
<input type="submit">
|
||||
<table class="products">
|
||||
<div data-record-id="1">
|
||||
<input id="input1a" type="text" value="initial1a">
|
||||
<input id="input1b" type="text" value="initial1b">
|
||||
<select id="select1">
|
||||
<option>one</option>
|
||||
<option selected>two</option>
|
||||
</select>
|
||||
<button>a button is counted as a form element, but value is undefined</button>
|
||||
</div>
|
||||
<div data-record-id="2">
|
||||
<input id="input2" type="text" value="initial2">
|
||||
</div>
|
||||
<input type="submit">
|
||||
</table>
|
||||
</form>
|
||||
`;
|
||||
});
|
||||
|
||||
describe("marking changed fields", () => {
|
||||
it("input: onInput", () => {
|
||||
input1a.value = 'updated1a';
|
||||
input1a.value = "updated1a";
|
||||
input1a.dispatchEvent(new Event("input"));
|
||||
// Expect only first field to show changed
|
||||
expect(input1a.classList).toContain('changed');
|
||||
expect(input1b.classList).not.toContain('changed');
|
||||
expect(input2.classList).not.toContain('changed');
|
||||
expect(input1a.classList).toContain("changed");
|
||||
expect(input1b.classList).not.toContain("changed");
|
||||
expect(input2.classList).not.toContain("changed");
|
||||
|
||||
// Change back to original value
|
||||
input1a.value = 'initial1a';
|
||||
input1a.value = "initial1a";
|
||||
input1a.dispatchEvent(new Event("input"));
|
||||
expect(input1a.classList).not.toContain('changed');
|
||||
expect(input1a.classList).not.toContain("changed");
|
||||
});
|
||||
|
||||
it("select: onInput", () => {
|
||||
@@ -71,59 +73,61 @@ describe("BulkFormController", () => {
|
||||
select1.options[1].selected = false;
|
||||
select1.dispatchEvent(new Event("input"));
|
||||
// Expect select to show changed
|
||||
expect(input1a.classList).not.toContain('changed');
|
||||
expect(input1b.classList).not.toContain('changed');
|
||||
expect(select1.classList).toContain('changed');
|
||||
expect(input1a.classList).not.toContain("changed");
|
||||
expect(input1b.classList).not.toContain("changed");
|
||||
expect(select1.classList).toContain("changed");
|
||||
|
||||
// Change back to original value
|
||||
select1.options[0].selected = false;
|
||||
select1.options[1].selected = true;
|
||||
select1.dispatchEvent(new Event("input"));
|
||||
expect(select1.classList).not.toContain('changed');
|
||||
expect(select1.classList).not.toContain("changed");
|
||||
});
|
||||
|
||||
it("multiple fields", () => {
|
||||
input1a.value = 'updated1a';
|
||||
input1a.value = "updated1a";
|
||||
input1a.dispatchEvent(new Event("input"));
|
||||
input2.value = 'updated2';
|
||||
input2.value = "updated2";
|
||||
input2.dispatchEvent(new Event("input"));
|
||||
// Expect only first field to show changed
|
||||
expect(input1a.classList).toContain('changed');
|
||||
expect(input1b.classList).not.toContain('changed');
|
||||
expect(input2.classList).toContain('changed');
|
||||
expect(input1a.classList).toContain("changed");
|
||||
expect(input1b.classList).not.toContain("changed");
|
||||
expect(input2.classList).toContain("changed");
|
||||
|
||||
// Change only one back to original value
|
||||
input1a.value = 'initial1a';
|
||||
input1a.value = "initial1a";
|
||||
input1a.dispatchEvent(new Event("input"));
|
||||
expect(input1a.classList).not.toContain('changed');
|
||||
expect(input1b.classList).not.toContain('changed');
|
||||
expect(input2.classList).toContain('changed');
|
||||
expect(input1a.classList).not.toContain("changed");
|
||||
expect(input1b.classList).not.toContain("changed");
|
||||
expect(input2.classList).toContain("changed");
|
||||
});
|
||||
|
||||
describe("select not include_blank", () => {
|
||||
beforeEach(() => {
|
||||
document.body.innerHTML = `
|
||||
<form data-controller="bulk-form" data-bulk-form-disable-selector-value="#disable1,#disable2">
|
||||
<div data-record-id="1">
|
||||
<select id="select1">
|
||||
<option value="1">one</option>
|
||||
<option value="2">two</option>
|
||||
</select>
|
||||
</div>
|
||||
<input type="submit">
|
||||
<table class="products">
|
||||
<div data-record-id="1">
|
||||
<select id="select1">
|
||||
<option value="1">one</option>
|
||||
<option value="2">two</option>
|
||||
</select>
|
||||
</div>
|
||||
<input type="submit">
|
||||
</table>
|
||||
</form>
|
||||
`;
|
||||
});
|
||||
|
||||
it("shows as changed", () => {
|
||||
// Expect select to show changed (select-one always has something selected)
|
||||
expect(select1.classList).toContain('changed');
|
||||
expect(select1.classList).toContain("changed");
|
||||
|
||||
// Change selection
|
||||
select1.options[0].selected = false;
|
||||
select1.options[1].selected = true;
|
||||
select1.dispatchEvent(new Event("input"));
|
||||
expect(select1.classList).toContain('changed');
|
||||
expect(select1.classList).toContain("changed");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -131,87 +135,89 @@ describe("BulkFormController", () => {
|
||||
beforeEach(() => {
|
||||
document.body.innerHTML = `
|
||||
<form data-controller="bulk-form" data-bulk-form-disable-selector-value="#disable1,#disable2">
|
||||
<div data-record-id="1">
|
||||
<select id="select1">
|
||||
<option value="">blank</option>
|
||||
<option value="1">one</option>
|
||||
</select>
|
||||
</div>
|
||||
<input type="submit">
|
||||
<table class="products">
|
||||
<div data-record-id="1">
|
||||
<select id="select1">
|
||||
<option value="">blank</option>
|
||||
<option value="1">one</option>
|
||||
</select>
|
||||
</div>
|
||||
<input type="submit">
|
||||
</table>
|
||||
</form>
|
||||
`;
|
||||
});
|
||||
|
||||
it("does not show as changed", () => {
|
||||
expect(select1.classList).not.toContain('changed');
|
||||
expect(select1.classList).not.toContain("changed");
|
||||
|
||||
// Change selection
|
||||
select1.options[0].selected = false;
|
||||
select1.options[1].selected = true;
|
||||
select1.dispatchEvent(new Event("input"));
|
||||
expect(select1.classList).toContain('changed');
|
||||
expect(select1.classList).toContain("changed");
|
||||
});
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
describe("activating sections, and showing a summary", () => {
|
||||
// This scenario should probably be broken up into smaller units.
|
||||
it("counts changed records ", () => {
|
||||
// Record 1: First field changed
|
||||
input1a.value = 'updated1a';
|
||||
input1a.value = "updated1a";
|
||||
input1a.dispatchEvent(new Event("input"));
|
||||
// Actions and changed summary are shown, with other sections disabled
|
||||
expect(actions.classList).not.toContain('hidden');
|
||||
expect(actions.classList).not.toContain("hidden");
|
||||
expect(changed_summary.textContent).toBe('changed_summary, {"count":1}');
|
||||
expect(disable1.classList).toContain('disabled-section');
|
||||
expect(disable1.classList).toContain("disabled-section");
|
||||
expect(disable1_element.disabled).toBe(true);
|
||||
expect(disable2.classList).toContain('disabled-section');
|
||||
expect(disable2.classList).toContain("disabled-section");
|
||||
expect(disable2_element.disabled).toBe(true);
|
||||
|
||||
// Record 1: Second field changed
|
||||
input1b.value = 'updated1b';
|
||||
input1b.value = "updated1b";
|
||||
input1b.dispatchEvent(new Event("input"));
|
||||
// Expect to show same summary translation
|
||||
expect(actions.classList).not.toContain('hidden');
|
||||
expect(actions.classList).not.toContain("hidden");
|
||||
expect(changed_summary.textContent).toBe('changed_summary, {"count":1}');
|
||||
|
||||
// Record 2: has been changed
|
||||
input2.value = 'updated2';
|
||||
input2.value = "updated2";
|
||||
input2.dispatchEvent(new Event("input"));
|
||||
// Expect summary to count both records
|
||||
expect(actions.classList).not.toContain('hidden');
|
||||
expect(actions.classList).not.toContain("hidden");
|
||||
expect(changed_summary.textContent).toBe('changed_summary, {"count":2}');
|
||||
|
||||
// Record 1: Change first field back to original value
|
||||
input1a.value = 'initial1a';
|
||||
input1a.value = "initial1a";
|
||||
input1a.dispatchEvent(new Event("input"));
|
||||
// Both records are still changed.
|
||||
expect(input1a.classList).not.toContain('changed');
|
||||
expect(input1b.classList).toContain('changed');
|
||||
expect(input2.classList).toContain('changed');
|
||||
expect(actions.classList).not.toContain('hidden');
|
||||
expect(input1a.classList).not.toContain("changed");
|
||||
expect(input1b.classList).toContain("changed");
|
||||
expect(input2.classList).toContain("changed");
|
||||
expect(actions.classList).not.toContain("hidden");
|
||||
expect(changed_summary.textContent).toBe('changed_summary, {"count":2}');
|
||||
|
||||
// Record 1: Change second field back to original value
|
||||
input1b.value = 'initial1b';
|
||||
input1b.value = "initial1b";
|
||||
input1b.dispatchEvent(new Event("input"));
|
||||
// Both fields for record 1 show unchanged, but second record is still changed
|
||||
expect(actions.classList).not.toContain('hidden');
|
||||
expect(actions.classList).not.toContain("hidden");
|
||||
expect(changed_summary.textContent).toBe('changed_summary, {"count":1}');
|
||||
expect(disable1.classList).toContain('disabled-section');
|
||||
expect(disable1.classList).toContain("disabled-section");
|
||||
expect(disable1_element.disabled).toBe(true);
|
||||
expect(disable2.classList).toContain('disabled-section');
|
||||
expect(disable2.classList).toContain("disabled-section");
|
||||
expect(disable2_element.disabled).toBe(true);
|
||||
|
||||
// Record 2: Change back to original value
|
||||
input2.value = 'initial2';
|
||||
input2.value = "initial2";
|
||||
input2.dispatchEvent(new Event("input"));
|
||||
// Actions are hidden and other sections are now re-enabled
|
||||
expect(actions.classList).toContain('hidden');
|
||||
expect(actions.classList).toContain("hidden");
|
||||
expect(changed_summary.textContent).toBe('changed_summary, {"count":0}');
|
||||
expect(disable1.classList).not.toContain('disabled-section');
|
||||
expect(disable1.classList).not.toContain("disabled-section");
|
||||
expect(disable1_element.disabled).toBe(false);
|
||||
expect(disable2.classList).not.toContain('disabled-section');
|
||||
expect(disable2.classList).not.toContain("disabled-section");
|
||||
expect(disable2_element.disabled).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -221,13 +227,15 @@ describe("BulkFormController", () => {
|
||||
beforeEach(() => {
|
||||
document.body.innerHTML = `
|
||||
<form data-controller="bulk-form" data-bulk-form-error-value="true">
|
||||
<div id="actions" data-bulk-form-target="actions">
|
||||
An error occurred.
|
||||
<input type="submit">
|
||||
</div>
|
||||
<div data-record-id="1">
|
||||
<input id="input1a" type="text" value="initial1a">
|
||||
</div>
|
||||
<table class="products">
|
||||
<div id="actions" data-bulk-form-target="actions">
|
||||
An error occurred.
|
||||
<input type="submit">
|
||||
</div>
|
||||
<div data-record-id="1">
|
||||
<input id="input1a" type="text" value="initial1a">
|
||||
</div>
|
||||
</table>
|
||||
</form>
|
||||
`;
|
||||
|
||||
@@ -238,19 +246,19 @@ describe("BulkFormController", () => {
|
||||
|
||||
it("form actions section remains visible", () => {
|
||||
// Expect actions to remain visible
|
||||
expect(actions.classList).not.toContain('hidden');
|
||||
expect(actions.classList).not.toContain("hidden");
|
||||
|
||||
// Record 1: First field changed
|
||||
input1a.value = 'updated1a';
|
||||
input1a.value = "updated1a";
|
||||
input1a.dispatchEvent(new Event("input"));
|
||||
// Expect actions to remain visible
|
||||
expect(actions.classList).not.toContain('hidden');
|
||||
expect(actions.classList).not.toContain("hidden");
|
||||
|
||||
// Change back to original value
|
||||
input1a.value = 'initial1a';
|
||||
input1a.value = "initial1a";
|
||||
input1a.dispatchEvent(new Event("input"));
|
||||
// Expect actions to remain visible
|
||||
expect(actions.classList).not.toContain('hidden');
|
||||
expect(actions.classList).not.toContain("hidden");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -258,16 +266,18 @@ describe("BulkFormController", () => {
|
||||
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">
|
||||
<table class="products">
|
||||
<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">
|
||||
</table>
|
||||
</form>
|
||||
`;
|
||||
});
|
||||
@@ -282,18 +292,18 @@ describe("BulkFormController", () => {
|
||||
});
|
||||
|
||||
it("onInput", () => {
|
||||
input1b.value = 'updated1b';
|
||||
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');
|
||||
expect(input1b.classList).toContain("changed");
|
||||
expect(input2.classList).not.toContain("changed");
|
||||
|
||||
// Change back to original value
|
||||
input1b.value = 'initial1b';
|
||||
input1b.value = "initial1b";
|
||||
input1b.dispatchEvent(new Event("input"));
|
||||
expect(input1b.classList).not.toContain('changed');
|
||||
expect(input1b.classList).not.toContain("changed");
|
||||
});
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
// unable to test disconnect at this stage
|
||||
|
||||
166
spec/javascripts/stimulus/tag_list_input_controller_test.js
Normal file
166
spec/javascripts/stimulus/tag_list_input_controller_test.js
Normal file
@@ -0,0 +1,166 @@
|
||||
/**
|
||||
* @jest-environment jsdom
|
||||
*/
|
||||
|
||||
import { Application } from "stimulus";
|
||||
import tag_list_input_controller from "../../../app/components/tag_list_input_component/tag_list_input_controller";
|
||||
|
||||
describe("TagListInputController", () => {
|
||||
beforeAll(() => {
|
||||
const application = Application.start();
|
||||
application.register("tag-list-input-component--tag-list-input", tag_list_input_controller);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
document.body.innerHTML = `
|
||||
<div data-controller="tag-list-input-component--tag-list-input">
|
||||
<input
|
||||
value="tag 1,tag 2,tag 3"
|
||||
data-tag-list-input-component--tag-list-input-target="tagList"
|
||||
type="hidden"
|
||||
name="variant_tag_list" id="variant_tag_list"
|
||||
>
|
||||
<div class="tags-input">
|
||||
<div class="tags">
|
||||
<ul class="tag-list" data-tag-list-input-component--tag-list-input-target="list">
|
||||
<template data-tag-list-input-component--tag-list-input-target="template">
|
||||
<li class="tag-item">
|
||||
<div class="tag-template">
|
||||
<span></span>
|
||||
<a
|
||||
class="remove-button"
|
||||
data-action="click->tag-list-input-component--tag-list-input#removeTag"
|
||||
>✖</a>
|
||||
</div>
|
||||
</li>
|
||||
</template>
|
||||
<li class="tag-item">
|
||||
<div class="tag-template">
|
||||
<span>tag 1</span>
|
||||
<a
|
||||
class="remove-button"
|
||||
data-action="click->tag-list-input-component--tag-list-input#removeTag"
|
||||
>✖</a>
|
||||
</div>
|
||||
</li>
|
||||
<li class="tag-item">
|
||||
<div class="tag-template">
|
||||
<span>tag 2</span>
|
||||
<a
|
||||
class="remove-button"
|
||||
data-action="click->tag-list-input-component--tag-list-input#removeTag"
|
||||
>✖</a>
|
||||
</div>
|
||||
</li>
|
||||
<li class="tag-item">
|
||||
<div class="tag-template">
|
||||
<span>tag 3</span>
|
||||
<a
|
||||
class="remove-button"
|
||||
data-action="click->tag-list-input-component--tag-list-input#removeTag"
|
||||
>✖</a>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<input
|
||||
type="text"
|
||||
name="variant_add_tag"
|
||||
id="variant_add_tag"
|
||||
placeholder="Add a tag"
|
||||
data-action="keydown.enter->tag-list-input-component--tag-list-input#addTag keyup->tag-list-input-component--tag-list-input#filterInput" data-tag-list-input-component--tag-list-input-target="newTag"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
});
|
||||
|
||||
describe("addTag", () => {
|
||||
beforeEach(() => {
|
||||
variant_add_tag.value = "new_tag";
|
||||
variant_add_tag.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter" }));
|
||||
});
|
||||
|
||||
it("updates the hidden input tag list", () => {
|
||||
expect(variant_tag_list.value).toBe("tag 1,tag 2,tag 3,new_tag");
|
||||
});
|
||||
|
||||
it("adds the new tag to the HTML tag list", () => {
|
||||
const tagList = document.getElementsByClassName("tag-list")[0];
|
||||
|
||||
// 1 template + 3 tags + 1 new tag
|
||||
expect(tagList.childElementCount).toBe(5);
|
||||
});
|
||||
|
||||
it("clears the tag input", () => {
|
||||
expect(variant_add_tag.value).toBe("");
|
||||
});
|
||||
|
||||
describe("with an empty new tag", () => {
|
||||
it("doesn't add the tag", () => {
|
||||
variant_add_tag.value = " ";
|
||||
variant_add_tag.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter" }));
|
||||
|
||||
const tagList = document.getElementsByClassName("tag-list")[0];
|
||||
|
||||
// 1 template + 3 tags + new tag (added in the beforeEach)
|
||||
expect(tagList.childElementCount).toBe(5);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when tag already exist", () => {
|
||||
beforeEach(() => {
|
||||
// Trying to add an existing tag
|
||||
variant_add_tag.value = "tag 2";
|
||||
variant_add_tag.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter" }));
|
||||
});
|
||||
|
||||
it("doesn't add the tag", () => {
|
||||
const tagList = document.getElementsByClassName("tag-list")[0];
|
||||
|
||||
// 1 template + 4 tags
|
||||
expect(tagList.childElementCount).toBe(5);
|
||||
expect(variant_add_tag.value).toBe("tag 2");
|
||||
});
|
||||
|
||||
it("highlights the new tag name in red", () => {
|
||||
expect(variant_add_tag.classList).toContain("tag-error");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("removeTag", () => {
|
||||
beforeEach(() => {
|
||||
const removeButtons = document.getElementsByClassName("remove-button");
|
||||
// Click on tag 2
|
||||
removeButtons[1].click();
|
||||
});
|
||||
|
||||
it("updates the hidden input tag list", () => {
|
||||
expect(variant_tag_list.value).toBe("tag 1,tag 3");
|
||||
});
|
||||
|
||||
it("removes the tag from the HTML tag list", () => {
|
||||
const tagList = document.getElementsByClassName("tag-list")[0];
|
||||
// 1 template + 2 tags
|
||||
expect(tagList.childElementCount).toBe(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe("filterInput", () => {
|
||||
it("removes comma from the tag input", () => {
|
||||
variant_add_tag.value = "text";
|
||||
variant_add_tag.dispatchEvent(new KeyboardEvent("keyup", { key: "," }));
|
||||
|
||||
expect(variant_add_tag.value).toBe("text");
|
||||
});
|
||||
|
||||
it("removes error highlight", () => {
|
||||
variant_add_tag.value = "text";
|
||||
variant_add_tag.classList.add("tag-error");
|
||||
|
||||
variant_add_tag.dispatchEvent(new KeyboardEvent("keyup", { key: "a" }));
|
||||
|
||||
expect(variant_add_tag.classList).not.toContain("tag-error");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -71,7 +71,7 @@ RSpec.describe OpenOrderCycleJob do
|
||||
.and change { variant.on_demand }.to(true)
|
||||
.and change { variant.on_hand }.by(0)
|
||||
.and change { variant_discontinued.on_hand }.to(0)
|
||||
.and query_database 58
|
||||
.and query_database 59
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
Reference in New Issue
Block a user