diff --git a/app/assets/javascripts/admin/addon/components/form-template/form.js b/app/assets/javascripts/admin/addon/components/form-template/form.js index f6655a7de4a..09a2cfc700b 100644 --- a/app/assets/javascripts/admin/addon/components/form-template/form.js +++ b/app/assets/javascripts/admin/addon/components/form-template/form.js @@ -32,10 +32,11 @@ export default class FormTemplateForm extends Component { type: "dropdown", icon: "chevron-circle-down", }, - { - type: "upload", - icon: "cloud-upload-alt", - }, + // TODO(@keegan): add support for uploads + // { + // type: "upload", + // icon: "cloud-upload-alt", + // }, { type: "multiselect", icon: "bullseye", diff --git a/app/assets/javascripts/admin/addon/models/form-template.js b/app/assets/javascripts/admin/addon/models/form-template.js index 4999210c931..9981d9ebbb5 100644 --- a/app/assets/javascripts/admin/addon/models/form-template.js +++ b/app/assets/javascripts/admin/addon/models/form-template.js @@ -30,16 +30,14 @@ export default class FormTemplate extends RestModel { }); } - static findAll() { - return ajax(`/admin/customize/form-templates.json`).then((model) => { - return model.form_templates.sort((a, b) => a.id - b.id); - }); + static async findAll() { + const result = await ajax("/admin/customize/form-templates.json"); + return result.form_templates; } - static findById(id) { - return ajax(`/admin/customize/form-templates/${id}.json`).then((model) => { - return model.form_template; - }); + static async findById(id) { + const result = await ajax(`/admin/customize/form-templates/${id}.json`); + return result.form_template; } static validateTemplate(data) { diff --git a/app/assets/javascripts/discourse/app/components/composer-container.hbs b/app/assets/javascripts/discourse/app/components/composer-container.hbs index 036ef436f6e..20dc3fc5995 100644 --- a/app/assets/javascripts/discourse/app/components/composer-container.hbs +++ b/app/assets/javascripts/discourse/app/components/composer-container.hbs @@ -121,6 +121,7 @@ @afterRefresh={{this.composer.afterRefresh}} @focusTarget={{this.composer.focusTarget}} @disableTextarea={{this.composer.disableTextarea}} + @formTemplateIds={{this.composer.formTemplateIds}} >
diff --git a/app/assets/javascripts/discourse/app/components/d-editor.hbs b/app/assets/javascripts/discourse/app/components/d-editor.hbs index ab33cd874be..a688cd3ef7c 100644 --- a/app/assets/javascripts/discourse/app/components/d-editor.hbs +++ b/app/assets/javascripts/discourse/app/components/d-editor.hbs @@ -1,63 +1,75 @@
{{yield}} - -
- + + + + +
+ {{/if}}
+ {{#each this.parsedTemplate as |content|}} + {{component + (concat "form-template-field/" content.type) + attributes=content.attributes + choices=content.choices + validations=content.validations + }} + {{/each}} +
{{else}}
{{this.error}} diff --git a/app/assets/javascripts/discourse/app/components/form-template-field/wrapper.js b/app/assets/javascripts/discourse/app/components/form-template-field/wrapper.js index f181dd88c47..cafcbda848e 100644 --- a/app/assets/javascripts/discourse/app/components/form-template-field/wrapper.js +++ b/app/assets/javascripts/discourse/app/components/form-template-field/wrapper.js @@ -1,17 +1,45 @@ import Component from "@glimmer/component"; import Yaml from "js-yaml"; import { tracked } from "@glimmer/tracking"; +import FormTemplate from "discourse/models/form-template"; +import { action } from "@ember/object"; export default class FormTemplateFieldWrapper extends Component { @tracked error = null; + @tracked parsedTemplate = null; - get canShowContent() { + constructor() { + super(...arguments); + + if (this.args.content) { + // Content used when no id exists yet + // (i.e. previewing while creating a new template) + this._loadTemplate(this.args.content); + } else if (this.args.id) { + this._fetchTemplate(this.args.id); + } + } + + _loadTemplate(templateContent) { try { - const parsedContent = Yaml.load(this.args.content); - this.parsedContent = parsedContent; - return true; + this.parsedTemplate = Yaml.load(templateContent); } catch (e) { this.error = e; } } + + @action + refreshTemplate() { + if (Array.isArray(this.args?.id) && this.args?.id.length === 0) { + return; + } + + return this._fetchTemplate(this.args.id); + } + + async _fetchTemplate(id) { + const response = await FormTemplate.findById(id); + const templateContent = await response.form_template.template; + return this._loadTemplate(templateContent); + } } diff --git a/app/assets/javascripts/discourse/app/models/form-template.js b/app/assets/javascripts/discourse/app/models/form-template.js new file mode 100644 index 00000000000..b46c8de45e9 --- /dev/null +++ b/app/assets/javascripts/discourse/app/models/form-template.js @@ -0,0 +1,13 @@ +import { ajax } from "discourse/lib/ajax"; +import RestModel from "discourse/models/rest"; + +export default class FormTemplate extends RestModel { + static async findAll() { + const result = await ajax("/form-templates.json"); + return result.form_templates; + } + + static async findById(id) { + return await ajax(`/form-templates/${id}.json`); + } +} diff --git a/app/assets/javascripts/discourse/app/services/composer.js b/app/assets/javascripts/discourse/app/services/composer.js index 66f256fe882..a4511d21333 100644 --- a/app/assets/javascripts/discourse/app/services/composer.js +++ b/app/assets/javascripts/discourse/app/services/composer.js @@ -158,6 +158,14 @@ export default class ComposerController extends Controller { return this.set("_disableSubmit", value); } + get formTemplateIds() { + if (!this.siteSettings.experimental_form_templates) { + return null; + } + + return this.model.category?.get("form_template_ids"); + } + @discourseComputed("showPreview") toggleText(showPreview) { return showPreview @@ -498,6 +506,11 @@ export default class ComposerController extends Controller { }); } + @action + updateCategory(categoryId) { + this.model.categoryId = categoryId; + } + @action openIfDraft(event) { if (!this.get("model.viewDraft")) { diff --git a/app/assets/javascripts/discourse/app/templates/modal/customize-form-template-view.hbs b/app/assets/javascripts/discourse/app/templates/modal/customize-form-template-view.hbs index b27d7740b46..dac821bbd70 100644 --- a/app/assets/javascripts/discourse/app/templates/modal/customize-form-template-view.hbs +++ b/app/assets/javascripts/discourse/app/templates/modal/customize-form-template-view.hbs @@ -8,7 +8,7 @@ />
{{#if this.showPreview}} - + {{else}} {{/if}} diff --git a/app/assets/javascripts/discourse/tests/integration/components/form-template-field/wrapper-test.js b/app/assets/javascripts/discourse/tests/integration/components/form-template-field/wrapper-test.js index 9597db2c4e2..908ee7ca53e 100644 --- a/app/assets/javascripts/discourse/tests/integration/components/form-template-field/wrapper-test.js +++ b/app/assets/javascripts/discourse/tests/integration/components/form-template-field/wrapper-test.js @@ -3,6 +3,7 @@ import { setupRenderingTest } from "discourse/tests/helpers/component-test"; import { render } from "@ember/test-helpers"; import { hbs } from "ember-cli-htmlbars"; import { exists } from "discourse/tests/helpers/qunit-helpers"; +import pretender, { response } from "discourse/tests/helpers/create-pretender"; module( "Integration | Component | form-template-field | wrapper", @@ -22,7 +23,7 @@ module( assert.ok(exists(".alert"), "An alert message should exist"); }); - test("renders a component based on the component type in the template content", async function (assert) { + test("renders a component based on the component type found in the content YAML", async function (assert) { const content = `- type: checkbox\n- type: input\n- type: textarea\n- type: dropdown\n- type: upload\n- type: multi-select`; const componentTypes = [ "checkbox", @@ -45,5 +46,28 @@ module( ); }); }); + + test("renders a component based on the component type found in the content YAML when passed ids", async function (assert) { + pretender.get("/form-templates/1.json", () => { + return response({ + form_template: { + id: 1, + name: "Bug Reports", + template: + '- type: checkbox\n choices:\n - "Option 1"\n - "Option 2"\n - "Option 3"\n attributes:\n label: "Enter question here"\n description: "Enter description here"\n validations:\n required: true', + }, + }); + }); + + this.set("formTemplateId", [1]); + await render( + hbs`` + ); + + assert.ok( + exists(`.form-template-field[data-field-type='checkbox']`), + `Checkbox component renders` + ); + }); } ); diff --git a/app/assets/javascripts/discourse/tests/integration/components/select-kit/form-template-chooser-test.js b/app/assets/javascripts/discourse/tests/integration/components/select-kit/form-template-chooser-test.js index 015ec4ad281..09d248f862d 100644 --- a/app/assets/javascripts/discourse/tests/integration/components/select-kit/form-template-chooser-test.js +++ b/app/assets/javascripts/discourse/tests/integration/components/select-kit/form-template-chooser-test.js @@ -12,7 +12,7 @@ module( hooks.beforeEach(function () { this.set("subject", selectKit()); - pretender.get("/admin/customize/form-templates.json", () => { + pretender.get("/form-templates.json", () => { return response({ form_templates: [ { id: 1, name: "template 1", template: "test: true" }, @@ -40,7 +40,7 @@ module( }); test("when no templates are available, the select is disabled", async function (assert) { - pretender.get("/admin/customize/form-templates.json", () => { + pretender.get("/form-templates.json", () => { return response({ form_templates: [] }); }); diff --git a/app/assets/javascripts/select-kit/addon/components/form-template-chooser.js b/app/assets/javascripts/select-kit/addon/components/form-template-chooser.js index 45bbc849aee..9120819d008 100644 --- a/app/assets/javascripts/select-kit/addon/components/form-template-chooser.js +++ b/app/assets/javascripts/select-kit/addon/components/form-template-chooser.js @@ -1,12 +1,12 @@ import MultiSelectComponent from "select-kit/components/multi-select"; -import FormTemplate from "admin/models/form-template"; +import FormTemplate from "discourse/models/form-template"; import { computed } from "@ember/object"; export default MultiSelectComponent.extend({ pluginApiIdentifiers: ["form-template-chooser"], classNames: ["form-template-chooser"], selectKitOptions: { - none: "admin.form_templates.edit_category.select_template", + none: "form_template_chooser.select_template", }, init() { @@ -17,6 +17,11 @@ export default MultiSelectComponent.extend({ } }, + didUpdateAttrs() { + this._super(...arguments); + this._fetchTemplates(); + }, + @computed("templates") get content() { if (!this.templates) { @@ -28,7 +33,14 @@ export default MultiSelectComponent.extend({ _fetchTemplates() { FormTemplate.findAll().then((result) => { - const sortedTemplates = this._sortTemplatesByName(result); + let sortedTemplates = this._sortTemplatesByName(result); + + if (this.filteredIds) { + sortedTemplates = sortedTemplates.filter((t) => + this.filteredIds.includes(t.id) + ); + } + if (sortedTemplates.length > 0) { return this.set("templates", sortedTemplates); } else { diff --git a/app/assets/stylesheets/common/base/compose.scss b/app/assets/stylesheets/common/base/compose.scss index e216b95a44f..f556c5b26d1 100644 --- a/app/assets/stylesheets/common/base/compose.scss +++ b/app/assets/stylesheets/common/base/compose.scss @@ -394,6 +394,17 @@ html.composer-open { #file-uploader { display: none; } + + .composer-select-form-template { + margin-bottom: 8px; + width: 100%; + + .name, + .formatted-selection, + .d-icon { + color: var(--primary-high); + } + } } .autocomplete { diff --git a/app/assets/stylesheets/common/d-editor.scss b/app/assets/stylesheets/common/d-editor.scss index 48fa3c633fb..55cb7be7234 100644 --- a/app/assets/stylesheets/common/d-editor.scss +++ b/app/assets/stylesheets/common/d-editor.scss @@ -344,3 +344,10 @@ justify-content: center; } } + +.d-editor .form-template-form__wrapper { + overflow: auto; + background: var(--primary-very-low); + padding: 1rem; + border: 1px solid var(--primary-medium); +} diff --git a/app/controllers/admin/form_templates_controller.rb b/app/controllers/admin/form_templates_controller.rb index 9cf39f5861e..968bc521ba2 100644 --- a/app/controllers/admin/form_templates_controller.rb +++ b/app/controllers/admin/form_templates_controller.rb @@ -4,7 +4,7 @@ class Admin::FormTemplatesController < Admin::StaffController before_action :ensure_form_templates_enabled def index - form_templates = FormTemplate.all + form_templates = FormTemplate.all.order(:id) render_serialized(form_templates, AdminFormTemplateSerializer, root: "form_templates") end diff --git a/app/controllers/form_templates_controller.rb b/app/controllers/form_templates_controller.rb new file mode 100644 index 00000000000..8a8d3865c7a --- /dev/null +++ b/app/controllers/form_templates_controller.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +class FormTemplatesController < ApplicationController + requires_login + before_action :ensure_form_templates_enabled + + def index + form_templates = FormTemplate.all.order(:id) + render_serialized(form_templates, FormTemplateSerializer, root: "form_templates") + end + + def show + params.require(:id) + + template = FormTemplate.find_by(id: params[:id]) + raise Discourse::NotFound if template.nil? + + render_serialized(template, FormTemplateSerializer, root: "form_template") + end + + private + + def ensure_form_templates_enabled + raise Discourse::InvalidAccess.new unless SiteSetting.experimental_form_templates + end +end diff --git a/app/serializers/form_template_serializer.rb b/app/serializers/form_template_serializer.rb new file mode 100644 index 00000000000..6b7418c1752 --- /dev/null +++ b/app/serializers/form_template_serializer.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class FormTemplateSerializer < ApplicationSerializer + attributes :id, :name, :template +end diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index d9b52114b52..d1ef7242b9c 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -4525,6 +4525,9 @@ en: char_counter: exceeded: "The maximum number of characters allowed has been exceeded." + form_template_chooser: + select_template: "Select form templates" + # This section is exported to the javascript for i18n in the admin section admin_js: type_to_filter: "type to filter..." diff --git a/config/routes.rb b/config/routes.rb index 0e114877734..b81de7b22dd 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1597,5 +1597,8 @@ Discourse::Application.routes.draw do put "/sidebar_sections/reset/:id" => "sidebar_sections#reset" get "*url", to: "permalinks#show", constraints: PermalinkConstraint.new + + get "/form-templates/:id" => "form_templates#show" + get "/form-templates" => "form_templates#index" end end diff --git a/spec/requests/form_templates_controller_spec.rb b/spec/requests/form_templates_controller_spec.rb new file mode 100644 index 00000000000..25803c3c789 --- /dev/null +++ b/spec/requests/form_templates_controller_spec.rb @@ -0,0 +1,86 @@ +# frozen_string_literal: true + +RSpec.describe FormTemplatesController do + fab!(:user) { Fabricate(:user) } + + before { SiteSetting.experimental_form_templates = true } + + describe "#index" do + fab!(:form_template) { Fabricate(:form_template, id: 2) } + fab!(:form_template_2) { Fabricate(:form_template, id: 1) } + fab!(:form_template_3) { Fabricate(:form_template, id: 3) } + + context "when logged in as a user" do + before { sign_in(user) } + + it "should return all form templates ordered by its ids" do + get "/form-templates.json" + expect(response.status).to eq(200) + json = response.parsed_body + expect(json["form_templates"]).to be_present + expect(json["form_templates"].length).to eq(3) + + templates = json["form_templates"] + expect(templates[0]["id"]).to eq(form_template_2.id) + expect(templates[1]["id"]).to eq(form_template.id) + expect(templates[2]["id"]).to eq(form_template_3.id) + end + end + + context "when you are not logged in" do + it "should deny access" do + get "/form-templates.json" + expect(response.status).to eq(403) + end + end + + context "when experimental form templates is disabled" do + before do + sign_in(user) + SiteSetting.experimental_form_templates = false + end + + it "should not work if you are a logged in user" do + get "/form-templates.json" + expect(response.status).to eq(403) + end + end + end + + describe "#show" do + fab!(:form_template) { Fabricate(:form_template) } + + context "when logged in as a user" do + before { sign_in(user) } + + it "should return a single template" do + get "/form-templates/#{form_template.id}.json" + expect(response.status).to eq(200) + json = response.parsed_body + current_template = json["form_template"] + expect(current_template["id"]).to eq(form_template.id) + expect(current_template["name"]).to eq(form_template.name) + expect(current_template["template"]).to eq(form_template.template) + end + end + + context "when you are not logged in" do + it "should deny access" do + get "/form-templates/#{form_template.id}.json" + expect(response.status).to eq(403) + end + end + + context "when experimental form templates is disabled" do + before do + sign_in(user) + SiteSetting.experimental_form_templates = false + end + + it "should not work if you are a logged in user" do + get "/form-templates/#{form_template.id}.json" + expect(response.status).to eq(403) + end + end + end +end diff --git a/spec/system/admin_customize_form_templates_spec.rb b/spec/system/admin_customize_form_templates_spec.rb index f57d4092a39..525e5a43952 100644 --- a/spec/system/admin_customize_form_templates_spec.rb +++ b/spec/system/admin_customize_form_templates_spec.rb @@ -119,7 +119,8 @@ describe "Admin Customize Form Templates", type: :system, js: true do expect(form_template_page).to have_input_field("textarea") expect(form_template_page).to have_input_field("checkbox") expect(form_template_page).to have_input_field("dropdown") - expect(form_template_page).to have_input_field("upload") + # TODO(@keegan): Add this back when upload functionality is added + # expect(form_template_page).to have_input_field("upload") expect(form_template_page).to have_input_field("multi-select") end @@ -175,7 +176,8 @@ describe "Admin Customize Form Templates", type: :system, js: true do ) end - it "should allow quick insertion of upload field" do + # TODO(@keegan): Unskip this test when Upload functionality is added + xit "should allow quick insertion of upload field" do quick_insertion_test( "upload", '- type: upload diff --git a/spec/system/composer/category_templates_spec.rb b/spec/system/composer/category_templates_spec.rb new file mode 100644 index 00000000000..e09266535eb --- /dev/null +++ b/spec/system/composer/category_templates_spec.rb @@ -0,0 +1,155 @@ +# frozen_string_literal: true + +describe "Composer Form Templates", type: :system, js: true do + fab!(:user) { Fabricate(:user) } + fab!(:form_template_1) do + Fabricate(:form_template, name: "Bug Reports", template: "- type: checkbox") + end + fab!(:form_template_2) do + Fabricate(:form_template, name: "Feature Request", template: "- type: input") + end + fab!(:form_template_3) do + Fabricate(:form_template, name: "Awesome Possum", template: "- type: dropdown") + end + fab!(:form_template_4) do + Fabricate(:form_template, name: "Biography", template: "- type: textarea") + end + fab!(:category_with_template_1) do + Fabricate( + :category, + name: "Reports", + slug: "reports", + topic_count: 2, + form_template_ids: [form_template_1.id], + ) + end + fab!(:category_with_template_2) do + Fabricate( + :category, + name: "Features", + slug: "features", + topic_count: 3, + form_template_ids: [form_template_2.id], + ) + end + fab!(:category_with_multiple_templates_1) do + Fabricate( + :category, + name: "Multiple", + slug: "mulitple", + topic_count: 10, + form_template_ids: [form_template_1.id, form_template_2.id], + ) + end + fab!(:category_with_multiple_templates_2) do + Fabricate( + :category, + name: "More Stuff", + slug: "more-stuff", + topic_count: 10, + form_template_ids: [form_template_3.id, form_template_4.id], + ) + end + fab!(:category_no_template) do + Fabricate(:category, name: "Staff", slug: "staff", topic_count: 2, form_template_ids: []) + end + fab!(:category_topic_template) do + Fabricate( + :category, + name: "Random", + slug: "random", + topic_count: 5, + form_template_ids: [], + topic_template: "Testing", + ) + end + let(:category_page) { PageObjects::Pages::Category.new } + let(:composer) { PageObjects::Components::Composer.new } + let(:form_template_chooser) { PageObjects::Components::SelectKit.new(".form-template-chooser") } + + before do + SiteSetting.experimental_form_templates = true + sign_in user + end + + it "shows a textarea when no form template is assigned to the category" do + category_page.visit(category_no_template) + category_page.new_topic_button.click + expect(composer).to have_composer_input + end + + it "shows a textarea filled in with topic template when a topic template is assigned to the category" do + category_page.visit(category_topic_template) + category_page.new_topic_button.click + expect(composer).to have_composer_input + expect(composer).to have_content(category_topic_template.topic_template) + end + + it "shows a form when a form template is assigned to the category" do + category_page.visit(category_with_template_1) + category_page.new_topic_button.click + expect(composer).not_to have_composer_input + expect(composer).to have_form_template + expect(composer).to have_form_template_field("checkbox") + end + + it "shows the correct template when switching categories" do + category_page.visit(category_no_template) + category_page.new_topic_button.click + # first category has no template + expect(composer).to have_composer_input + # switch to category with topic template + composer.switch_category(category_topic_template.name) + expect(composer).to have_composer_input + expect(composer).to have_content(category_topic_template.topic_template) + # switch to category with form template + composer.switch_category(category_with_template_1.name) + expect(composer).to have_form_template + expect(composer).to have_form_template_field("checkbox") + # switch to category with a different form template + composer.switch_category(category_with_template_2.name) + expect(composer).to have_form_template + expect(composer).to have_form_template_field("input") + end + + it "does not show form template chooser when a category only has form template" do + category_page.visit(category_with_template_1) + category_page.new_topic_button.click + expect(composer).not_to have_form_template_chooser + end + + it "shows form template chooser when a category has multiple form templates" do + category_page.visit(category_with_multiple_templates_1) + category_page.new_topic_button.click + expect(composer).to have_form_template_chooser + end + + it "updates the form template when a different template is selected" do + category_page.visit(category_with_multiple_templates_1) + category_page.new_topic_button.click + expect(composer).to have_form_template_field("checkbox") + form_template_chooser.select_row_by_name(form_template_2.name) + expect(composer).to have_form_template_field("input") + end + + it "shows the correct template options when switching categories" do + category_page.visit(category_with_multiple_templates_1) + category_page.new_topic_button.click + expect(composer).to have_form_template_chooser + form_template_chooser.expand + expect(form_template_chooser).to have_selected_choice_name(form_template_1.name) + expect(form_template_chooser).to have_option_name(form_template_2.name) + composer.switch_category(category_with_multiple_templates_2.name) + form_template_chooser.expand + expect(form_template_chooser).to have_selected_choice_name(form_template_3.name) + expect(form_template_chooser).to have_option_name(form_template_4.name) + end + + it "shows the correct template name in the dropdown header after switching templates" do + category_page.visit(category_with_multiple_templates_1) + category_page.new_topic_button.click + expect(form_template_chooser).to have_selected_name(form_template_1.name) + form_template_chooser.select_row_by_name(form_template_2.name) + expect(form_template_chooser).to have_selected_name(form_template_2.name) + end +end diff --git a/spec/system/page_objects/components/composer.rb b/spec/system/page_objects/components/composer.rb index f81fbe92e97..af667bb8461 100644 --- a/spec/system/page_objects/components/composer.rb +++ b/spec/system/page_objects/components/composer.rb @@ -72,6 +72,11 @@ module PageObjects find(AUTOCOMPLETE_MENU) end + def switch_category(category_name) + find(".category-chooser").click + find(".category-row[data-name='#{category_name}']").click + end + def has_emoji_autocomplete? has_css?(AUTOCOMPLETE_MENU) end @@ -98,6 +103,22 @@ module PageObjects page.has_no_css?(emoji_preview_selector(emoji)) end + def has_composer_input? + page.has_css?("#{COMPOSER_ID} .d-editor .d-editor-input") + end + + def has_form_template? + page.has_css?(".form-template-form__wrapper") + end + + def has_form_template_field?(field) + page.has_css?(".form-template-field[data-field-type='#{field}']") + end + + def has_form_template_chooser? + page.has_css?(".composer-select-form-template") + end + def composer_input find("#{COMPOSER_ID} .d-editor .d-editor-input") end diff --git a/spec/system/page_objects/components/select_kit.rb b/spec/system/page_objects/components/select_kit.rb index 19f6842b919..8228d65b9be 100644 --- a/spec/system/page_objects/components/select_kit.rb +++ b/spec/system/page_objects/components/select_kit.rb @@ -34,8 +34,16 @@ module PageObjects component.find(".select-kit-header[data-value='#{value}']") end - def has_selected_name?(value) - component.find(".select-kit-header[data-name='#{value}']") + def has_selected_name?(name) + component.find(".select-kit-header[data-name='#{name}']") + end + + def has_selected_choice_name?(name) + component.find(".selected-choice[data-name='#{name}']") + end + + def has_option_name?(name) + component.find(".select-kit-collection li[data-name='#{name}']") end def expand