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:
Filipe
2025-05-03 17:34:51 +01:00
committed by GitHub
22 changed files with 491 additions and 115 deletions

View File

@@ -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}

View 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 {}

View 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

View File

@@ -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"

View File

@@ -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;
}
}
}

View File

@@ -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(",", "");
}
}
}

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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')

View File

@@ -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|

View File

@@ -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

View File

@@ -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;
}
}

View File

@@ -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

View File

@@ -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} {

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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.

View File

@@ -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

View 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");
});
});
});

View File

@@ -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