From 066010db7dc45a74d9c81daa8c4b661b66ca03e8 Mon Sep 17 00:00:00 2001 From: Joffrey JAFFEUX Date: Fri, 3 Aug 2018 16:41:37 -0400 Subject: [PATCH] FEATURE: introduces list/compact_list components --- .../admin/components/value-list.js.es6 | 164 ++++++++---------- .../admin/mixins/setting-component.js.es6 | 14 +- .../components/site-settings/compact-list.hbs | 3 + .../components/site-settings/list.hbs | 2 +- .../admin/templates/components/value-list.hbs | 17 +- .../select-kit/components/select-kit.js.es6 | 11 +- .../select-kit/mixins/dom-helpers.js.es6 | 9 +- .../stylesheets/common/admin/admin_base.scss | 46 +++-- .../stylesheets/common/admin/customize.scss | 8 + app/serializers/theme_settings_serializer.rb | 11 +- config/locales/client.en.yml | 3 + config/site_settings.yml | 2 + lib/site_settings/type_supervisor.rb | 5 +- lib/theme_settings_manager.rb | 9 +- lib/theme_settings_parser.rb | 5 + .../site_settings/type_supervisor_spec.rb | 6 +- .../components/theme_settings_manager_spec.rb | 7 + spec/components/theme_settings_parser_spec.rb | 7 + .../theme_settings/valid_settings.yaml | 6 + .../components/value-list-test.js.es6 | 122 +++++++++---- 20 files changed, 297 insertions(+), 160 deletions(-) create mode 100644 app/assets/javascripts/admin/templates/components/site-settings/compact-list.hbs diff --git a/app/assets/javascripts/admin/components/value-list.js.es6 b/app/assets/javascripts/admin/components/value-list.js.es6 index 60e4a3cda73..1f66a00bc74 100644 --- a/app/assets/javascripts/admin/components/value-list.js.es6 +++ b/app/assets/javascripts/admin/components/value-list.js.es6 @@ -1,113 +1,101 @@ +import { on } from "ember-addons/ember-computed-decorators"; +import computed from "ember-addons/ember-computed-decorators"; + export default Ember.Component.extend({ classNameBindings: [":value-list"], - _enableSorting: function() { - const self = this; - const placeholder = document.createElement("div"); - placeholder.className = "placeholder"; + inputInvalid: Ember.computed.empty("newValue"), - let dragging = null; - let over = null; - let nodePlacement; + inputDelimiter: null, + inputType: null, + newValue: "", + collection: null, + values: null, - this.$().on("dragstart.discourse", ".values .value", function(e) { - dragging = e.currentTarget; - e.dataTransfer.effectAllowed = "move"; - e.dataTransfer.setData("text/html", e.currentTarget); - }); + @computed("addKey", "filteredChoices.length") + noneKey(addKey, filteredChoicesLength) { + return addKey || filteredChoicesLength === 0 + ? "admin.site_settings.value_list.no_choices_none" + : "admin.site_settings.value_list.default_none"; + }, - this.$().on("dragend.discourse", ".values .value", function() { - Ember.run(function() { - dragging.parentNode.removeChild(placeholder); - dragging.style.display = "block"; - - // Update data - const from = Number(dragging.dataset.index); - let to = Number(over.dataset.index); - if (from < to) to--; - if (nodePlacement === "after") to++; - - const collection = self.get("collection"); - const fromObj = collection.objectAt(from); - collection.replace(from, 1); - collection.replace(to, 0, [fromObj]); - self._saveValues(); - }); - return false; - }); - - this.$().on("dragover.discourse", ".values", function(e) { - e.preventDefault(); - dragging.style.display = "none"; - if (e.target.className === "placeholder") { - return; - } - over = e.target; - - const relY = e.originalEvent.clientY - over.offsetTop; - const height = over.offsetHeight / 2; - const parent = e.target.parentNode; - - if (relY > height) { - nodePlacement = "after"; - parent.insertBefore(placeholder, e.target.nextElementSibling); - } else if (relY < height) { - nodePlacement = "before"; - parent.insertBefore(placeholder, e.target); - } - }); - }.on("didInsertElement"), - - _removeSorting: function() { - this.$() - .off("dragover.discourse") - .off("dragend.discourse") - .off("dragstart.discourse"); - }.on("willDestroyElement"), - - _setupCollection: function() { + @on("didReceiveAttrs") + _setupCollection() { const values = this.get("values"); if (this.get("inputType") === "array") { this.set("collection", values || []); - } else { - this.set("collection", values && values.length ? values.split("\n") : []); + return; } - } - .on("init") - .observes("values"), - _saveValues: function() { - if (this.get("inputType") === "array") { - this.set("values", this.get("collection")); - } else { - this.set("values", this.get("collection").join("\n")); - } + this.set( + "collection", + this._splitValues(values, this.get("inputDelimiter") || "\n") + ); }, - inputInvalid: Ember.computed.empty("newValue"), + @computed("choices.[]", "collection.[]") + filteredChoices(choices, collection) { + return Ember.makeArray(choices).filter(i => collection.indexOf(i) < 0); + }, - keyDown(e) { - if (e.keyCode === 13) { - this.send("addValue"); - } + keyDown(event) { + if (event.keyCode === 13) this.send("addValue", this.get("newValue")); }, actions: { - addValue() { - if (this.get("inputInvalid")) { - return; - } + changeValue(index, newValue) { + this._replaceValue(index, newValue); + }, + + addValue(newValue) { + if (this.get("inputInvalid")) return; - this.get("collection").addObject(this.get("newValue")); this.set("newValue", ""); - - this._saveValues(); + this._addValue(newValue); }, removeValue(value) { - const collection = this.get("collection"); - collection.removeObject(value); - this._saveValues(); + this._removeValue(value); + }, + + selectChoice(choice) { + this._addValue(choice); + } + }, + + _addValue(value) { + this.get("collection").addObject(value); + this._saveValues(); + }, + + _removeValue(value) { + const collection = this.get("collection"); + collection.removeObject(value); + this._saveValues(); + }, + + _replaceValue(index, newValue) { + this.get("collection").replace(index, 1, [newValue]); + this._saveValues(); + }, + + _saveValues() { + if (this.get("inputType") === "array") { + this.set("values", this.get("collection")); + return; + } + + this.set( + "values", + this.get("collection").join(this.get("inputDelimiter") || "\n") + ); + }, + + _splitValues(values, delimiter) { + if (values && values.length) { + return values.split(delimiter).filter(x => x); + } else { + return []; } } }); diff --git a/app/assets/javascripts/admin/mixins/setting-component.js.es6 b/app/assets/javascripts/admin/mixins/setting-component.js.es6 index e00f14f4b3a..9636c47732d 100644 --- a/app/assets/javascripts/admin/mixins/setting-component.js.es6 +++ b/app/assets/javascripts/admin/mixins/setting-component.js.es6 @@ -10,7 +10,8 @@ const CUSTOM_TYPES = [ "category_list", "value_list", "category", - "uploaded_image_list" + "uploaded_image_list", + "compact_list" ]; export default Ember.Mixin.create({ @@ -59,11 +60,20 @@ export default Ember.Mixin.create({ return setting.replace(/\_/g, " "); }, - @computed("setting.type") + @computed("type") componentType(type) { return CUSTOM_TYPES.indexOf(type) !== -1 ? type : "string"; }, + @computed("setting") + type(setting) { + if (setting.type === "list" && setting.list_type) { + return `${setting.list_type}_list`; + } + + return setting.type; + }, + @computed("typeClass") componentName(typeClass) { return "site-settings/" + typeClass; diff --git a/app/assets/javascripts/admin/templates/components/site-settings/compact-list.hbs b/app/assets/javascripts/admin/templates/components/site-settings/compact-list.hbs new file mode 100644 index 00000000000..e741bea5ed1 --- /dev/null +++ b/app/assets/javascripts/admin/templates/components/site-settings/compact-list.hbs @@ -0,0 +1,3 @@ +{{list-setting settingValue=value choices=setting.choices settingName=setting.setting}} +{{setting-validation-message message=validationMessage}} +
{{{unbound setting.description}}}
diff --git a/app/assets/javascripts/admin/templates/components/site-settings/list.hbs b/app/assets/javascripts/admin/templates/components/site-settings/list.hbs index e741bea5ed1..0abde37586f 100644 --- a/app/assets/javascripts/admin/templates/components/site-settings/list.hbs +++ b/app/assets/javascripts/admin/templates/components/site-settings/list.hbs @@ -1,3 +1,3 @@ -{{list-setting settingValue=value choices=setting.choices settingName=setting.setting}} +{{value-list values=value inputDelimiter="|" choices=setting.choices}} {{setting-validation-message message=validationMessage}}
{{{unbound setting.description}}}
diff --git a/app/assets/javascripts/admin/templates/components/value-list.hbs b/app/assets/javascripts/admin/templates/components/value-list.hbs index 6f6e1aa1941..261cb42f467 100644 --- a/app/assets/javascripts/admin/templates/components/value-list.hbs +++ b/app/assets/javascripts/admin/templates/components/value-list.hbs @@ -1,18 +1,21 @@ {{#if collection}}
{{#each collection as |value index|}} -
+
{{d-button action="removeValue" actionParam=value icon="times" - class="btn-small"}} - {{value}} + class="remove-value-btn btn-small"}} + + {{input value=value class="value-input" focus-out=(action "changeValue" index)}}
{{/each}}
{{/if}} -
- {{text-field value=newValue placeholderKey=addKey}} - {{d-button action="addValue" icon="plus" class="btn-primary btn-small" disabled=inputInvalid}} -
+{{combo-box + allowAny=true + allowContentReplacement=true + none=noneKey + content=filteredChoices + onSelect=(action "selectChoice")}} diff --git a/app/assets/javascripts/select-kit/components/select-kit.js.es6 b/app/assets/javascripts/select-kit/components/select-kit.js.es6 index 3155e0ce141..972143a40b0 100644 --- a/app/assets/javascripts/select-kit/components/select-kit.js.es6 +++ b/app/assets/javascripts/select-kit/components/select-kit.js.es6 @@ -151,9 +151,14 @@ export default Ember.Component.extend( this ); - const existingCreatedComputedContent = this.get( - "computedContent" - ).filterBy("created", true); + let existingCreatedComputedContent = []; + if (!this.get("allowContentReplacement")) { + existingCreatedComputedContent = this.get("computedContent").filterBy( + "created", + true + ); + } + this.setProperties({ computedContent: content .map(c => this.computeContentItem(c)) diff --git a/app/assets/javascripts/select-kit/mixins/dom-helpers.js.es6 b/app/assets/javascripts/select-kit/mixins/dom-helpers.js.es6 index 8eae8b38ac1..73a3c2c6e2b 100644 --- a/app/assets/javascripts/select-kit/mixins/dom-helpers.js.es6 +++ b/app/assets/javascripts/select-kit/mixins/dom-helpers.js.es6 @@ -77,8 +77,8 @@ export default Ember.Mixin.create({ // use to collapse and remove focus close(event) { - this.collapse(event); this.setProperties({ isFocused: false }); + this.collapse(event); }, focus() { @@ -118,8 +118,11 @@ export default Ember.Mixin.create({ collapse() { this.set("isExpanded", false); - Ember.run.schedule("afterRender", () => this._removeFixedPosition()); - this._boundaryActionHandler("onCollapse", this); + + Ember.run.next(() => { + Ember.run.schedule("afterRender", () => this._removeFixedPosition()); + this._boundaryActionHandler("onCollapse", this); + }); }, // lose focus of the component in two steps diff --git a/app/assets/stylesheets/common/admin/admin_base.scss b/app/assets/stylesheets/common/admin/admin_base.scss index b424d4dac53..e8b33cb0822 100644 --- a/app/assets/stylesheets/common/admin/admin_base.scss +++ b/app/assets/stylesheets/common/admin/admin_base.scss @@ -873,25 +873,43 @@ table#user-badges { .value-list { .value { - border-bottom: 1px solid #ddd; - padding: 3px; - margin-right: 10px; + padding: 0.125em 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; - cursor: move; + display: flex; + + &:last-child { + border-bottom: none; + } + + .value-input { + box-sizing: border-box; + flex: 1; + border-color: $primary-low; + cursor: pointer; + margin: 0; + + &:focus { + border-color: $tertiary; + box-shadow: none; + } + } + + .remove-value-btn { + margin-right: 0.25em; + width: 29px; + border: 1px solid $primary-low; + outline: none; + padding: 0; + + &:focus { + border-color: $tertiary; + } + } } .values { - margin-bottom: 10px; - } - .placeholder { - border-bottom: 1px solid #ddd; - padding: 3px; - margin-right: 10px; - height: 30px; - } - input[type="text"] { - width: 90%; + margin-bottom: 0.5em; } } diff --git a/app/assets/stylesheets/common/admin/customize.scss b/app/assets/stylesheets/common/admin/customize.scss index b4fc2158aa4..bc08db8fcc3 100644 --- a/app/assets/stylesheets/common/admin/customize.scss +++ b/app/assets/stylesheets/common/admin/customize.scss @@ -444,9 +444,17 @@ padding: 0.25em 0; &.input-area { width: 75%; + .value-list, + .select-kit, input[type="text"] { width: 50%; } + + .value-list { + .select-kit { + width: 100%; + } + } } &.label-area { width: 25%; diff --git a/app/serializers/theme_settings_serializer.rb b/app/serializers/theme_settings_serializer.rb index cb3aa20794b..d7fe44c1400 100644 --- a/app/serializers/theme_settings_serializer.rb +++ b/app/serializers/theme_settings_serializer.rb @@ -1,5 +1,6 @@ class ThemeSettingsSerializer < ApplicationSerializer - attributes :setting, :type, :default, :value, :description, :valid_values + attributes :setting, :type, :default, :value, :description, :valid_values, + :list_type def setting object.name @@ -32,4 +33,12 @@ class ThemeSettingsSerializer < ApplicationSerializer def include_description? object.description.present? end + + def list_type + object.list_type + end + + def include_list_type? + object.type == ThemeSetting.types[:list] + end end diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index ccc85631817..927fff07cac 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -3828,6 +3828,9 @@ en: clear_filter: "Clear" add_url: "add URL" add_host: "add host" + value_list: + default_none: "Type to filter or create..." + no_choices_none: "Type to create..." uploaded_image_list: label: "Edit list" empty: "There are no pictures yet. Please upload one." diff --git a/config/site_settings.yml b/config/site_settings.yml index b1266b5b9c4..f9aef37efd6 100644 --- a/config/site_settings.yml +++ b/config/site_settings.yml @@ -116,6 +116,7 @@ basic: client: true refresh: true type: list + list_type: compact default: "latest|new|unread|top|categories" regex: "latest" regex_error: "site_settings.errors.must_include_latest" @@ -176,6 +177,7 @@ basic: category_colors: client: true type: list + list_type: compact default: 'BF1E2E|F1592A|F7941D|9EB83B|3AB54A|12A89D|25AAE2|0E76BD|652D90|92278F|ED207B|8C6238|231F20|808281|B3B5B4|283890' category_style: client: true diff --git a/lib/site_settings/type_supervisor.rb b/lib/site_settings/type_supervisor.rb index 25860d1da9a..32fd7733751 100644 --- a/lib/site_settings/type_supervisor.rb +++ b/lib/site_settings/type_supervisor.rb @@ -6,7 +6,7 @@ module SiteSettings; end class SiteSettings::TypeSupervisor include SiteSettings::Validations - CONSUMED_OPTS = %i[enum choices type validator min max regex hidden regex_error allow_any].freeze + CONSUMED_OPTS = %i[enum choices type validator min max regex hidden regex_error allow_any list_type].freeze VALIDATOR_OPTS = %i[min max regex hidden regex_error].freeze # For plugins, so they can tell if a feature is supported @@ -63,6 +63,7 @@ class SiteSettings::TypeSupervisor @validators = {} @types = {} @allow_any = {} + @list_type = {} end def load_setting(name_arg, opts = {}) @@ -88,6 +89,7 @@ class SiteSettings::TypeSupervisor if type.to_sym == :list @allow_any[name] = opts[:allow_any] == false ? false : true + @list_type[name] = opts[:list_type] if opts[:list_type] end end @types[name] = get_data_type(name, @defaults_provider[name]) @@ -144,6 +146,7 @@ class SiteSettings::TypeSupervisor end result[:choices] = @choices[name] if @choices.has_key? name + result[:list_type] = @list_type[name] if @list_type.has_key? name result end diff --git a/lib/theme_settings_manager.rb b/lib/theme_settings_manager.rb index 537b2c7de11..80802719440 100644 --- a/lib/theme_settings_manager.rb +++ b/lib/theme_settings_manager.rb @@ -93,8 +93,13 @@ class ThemeSettingsManager (max.is_a?(::Integer) || max.is_a?(::Float)) && max != ::Float::INFINITY end - class List < self; end - class String < self + class List < self + def list_type + @opts[:list_type] + end + end + + class String < self def is_valid_value?(new_value) (@opts[:min]..@opts[:max]).include? new_value.to_s.length end diff --git a/lib/theme_settings_parser.rb b/lib/theme_settings_parser.rb index 475d24a948e..064dd7ef143 100644 --- a/lib/theme_settings_parser.rb +++ b/lib/theme_settings_parser.rb @@ -33,6 +33,11 @@ class ThemeSettingsParser opts[:max] = raw_opts[:max].is_a?(Numeric) ? raw_opts[:max] : Float::INFINITY opts[:min] = raw_opts[:min].is_a?(Numeric) ? raw_opts[:min] : -Float::INFINITY end + + if raw_opts[:list_type] + opts[:list_type] = raw_opts[:list_type] + end + opts end diff --git a/spec/components/site_settings/type_supervisor_spec.rb b/spec/components/site_settings/type_supervisor_spec.rb index fb27122c867..1683ebf9e89 100644 --- a/spec/components/site_settings/type_supervisor_spec.rb +++ b/spec/components/site_settings/type_supervisor_spec.rb @@ -306,7 +306,7 @@ describe SiteSettings::TypeSupervisor do settings.setting(:type_url_list, 'string', type: 'url_list') settings.setting(:type_enum_choices, '2', type: 'enum', choices: ['1', '2']) settings.setting(:type_enum_class, 'a', enum: 'TestEnumClass2') - settings.setting(:type_list, 'a', type: 'list', choices: ['a', 'b']) + settings.setting(:type_list, 'a', type: 'list', choices: ['a', 'b'], list_type: 'compact') settings.refresh! end @@ -336,6 +336,10 @@ describe SiteSettings::TypeSupervisor do expect(settings.type_supervisor.type_hash(:type_list)[:choices]).to eq ['a', 'b'] end + it 'returns list list_type' do + expect(settings.type_supervisor.type_hash(:type_list)[:list_type]).to eq 'compact' + end + it 'returns enum choices' do hash = settings.type_supervisor.type_hash(:type_enum_choices) expect(hash[:valid_values]).to eq [{ name: '1', value: '1' }, { name: '2', value: '2' }] diff --git a/spec/components/theme_settings_manager_spec.rb b/spec/components/theme_settings_manager_spec.rb index f5b90619205..eb29ed2024c 100644 --- a/spec/components/theme_settings_manager_spec.rb +++ b/spec/components/theme_settings_manager_spec.rb @@ -114,4 +114,11 @@ describe ThemeSettingsManager do expect { string_setting.value = ("a" * 21) }.to raise_error(Discourse::InvalidParameters) end end + + context "List" do + it "can have a list type" do + list_setting = find_by_name(:compact_list_setting) + expect(list_setting.list_type).to eq("compact") + end + end end diff --git a/spec/components/theme_settings_parser_spec.rb b/spec/components/theme_settings_parser_spec.rb index cfdea790e63..4f8e9c9a42e 100644 --- a/spec/components/theme_settings_parser_spec.rb +++ b/spec/components/theme_settings_parser_spec.rb @@ -82,4 +82,11 @@ describe ThemeSettingsParser do expect(choices.length).to eq(1) end end + + context "list setting" do + it "supports list type" do + list_type = loader.find_by_name(:compact_list_setting)[:opts][:list_type] + expect(list_type).to eq("compact") + end + end end diff --git a/spec/fixtures/theme_settings/valid_settings.yaml b/spec/fixtures/theme_settings/valid_settings.yaml index 33c9c4ed85e..8250f34f38a 100644 --- a/spec/fixtures/theme_settings/valid_settings.yaml +++ b/spec/fixtures/theme_settings/valid_settings.yaml @@ -37,6 +37,12 @@ list_setting: description: "help text" default: "name|age|last name" +compact_list_setting: + type: list + list_type: compact + description: "help text" + default: "name|age|last name" + enum_setting: default: "trust level 4" type: enum diff --git a/test/javascripts/components/value-list-test.js.es6 b/test/javascripts/components/value-list-test.js.es6 index 7af861a9b17..8a5d08ea3d5 100644 --- a/test/javascripts/components/value-list-test.js.es6 +++ b/test/javascripts/components/value-list-test.js.es6 @@ -1,63 +1,111 @@ import componentTest from "helpers/component-test"; moduleForComponent("value-list", { integration: true }); -componentTest("functionality", { - template: '{{value-list values=values inputType="array"}}', +componentTest("adding a value", { + template: "{{value-list values=values}}", + async test(assert) { - assert.ok(this.$(".values .value").length === 0, "it has no values"); - assert.ok(this.$("input").length, "it renders the input"); + this.set("values", "vinkas\nosama"); + + await selectKit().expand(); + await selectKit().fillInFilter("eviltrout"); + await selectKit().keyboard("enter"); + assert.ok( - this.$(".btn-primary[disabled]").length, - "it is disabled with no value" + find(".values .value").length === 3, + "it adds the value to the list of values" ); - await fillIn("input", "eviltrout"); + assert.deepEqual( + this.get("values"), + "vinkas\nosama\neviltrout", + "it adds the value to the list of values" + ); + } +}); + +componentTest("removing a value", { + template: "{{value-list values=values}}", + + async test(assert) { + this.set("values", "vinkas\nosama"); + + await click(".values .value[data-index='0'] .remove-value-btn"); + assert.ok( - !this.$(".btn-primary[disabled]").length, - "it isn't disabled anymore" + find(".values .value").length === 1, + "it removes the value from the list of values" ); - await click(".btn-primary"); - assert.equal(this.$(".values .value").length, 1, "it adds the value"); - assert.equal(this.$("input").val(), "", "it clears the input"); - assert.ok(this.$(".btn-primary[disabled]").length, "it is disabled again"); - assert.equal(this.get("values"), "eviltrout", "it appends the value"); - - await click(".value .btn-small"); - assert.ok(this.$(".values .value").length === 0, "it removes the value"); + assert.equal(this.get("values"), "osama", "it removes the expected value"); } }); -componentTest("with string delimited values", { - template: "{{value-list values=valueString}}", - beforeEach() { - this.set("valueString", "hello\nworld"); - }, +componentTest("selecting a value", { + template: "{{value-list values=values choices=choices}}", async test(assert) { - assert.equal(this.$(".values .value").length, 2); + this.set("values", "vinkas\nosama"); + this.set("choices", ["maja", "michael"]); - await fillIn("input", "eviltrout"); - await click(".btn-primary"); + await selectKit().expand(); + await selectKit().selectRowByValue("maja"); - assert.equal(this.$(".values .value").length, 3); - assert.equal(this.get("valueString"), "hello\nworld\neviltrout"); + assert.ok( + find(".values .value").length === 3, + "it adds the value to the list of values" + ); + + assert.deepEqual( + this.get("values"), + "vinkas\nosama\nmaja", + "it adds the value to the list of values" + ); } }); -componentTest("with array values", { - template: '{{value-list values=valueArray inputType="array"}}', - beforeEach() { - this.set("valueArray", ["abc", "def"]); - }, +componentTest("array support", { + template: "{{value-list values=values inputType='array'}}", async test(assert) { - assert.equal(this.$(".values .value").length, 2); + this.set("values", ["vinkas", "osama"]); - await fillIn("input", "eviltrout"); - await click(".btn-primary"); + await selectKit().expand(); + await selectKit().fillInFilter("eviltrout"); + await selectKit().keyboard("enter"); - assert.equal(this.$(".values .value").length, 3); - assert.deepEqual(this.get("valueArray"), ["abc", "def", "eviltrout"]); + assert.ok( + find(".values .value").length === 3, + "it adds the value to the list of values" + ); + + assert.deepEqual( + this.get("values"), + ["vinkas", "osama", "eviltrout"], + "it adds the value to the list of values" + ); + } +}); + +componentTest("delimiter support", { + template: "{{value-list values=values inputDelimiter='|'}}", + + async test(assert) { + this.set("values", "vinkas|osama"); + + await selectKit().expand(); + await selectKit().fillInFilter("eviltrout"); + await selectKit().keyboard("enter"); + + assert.ok( + find(".values .value").length === 3, + "it adds the value to the list of values" + ); + + assert.deepEqual( + this.get("values"), + "vinkas|osama|eviltrout", + "it adds the value to the list of values" + ); } });