FEATURE: support a description attribute on form template fields (#23744)
* FEATURE: support a description attribute on form template fields
This commit is contained in:
parent
a1aedc9ce1
commit
1d70cf455e
|
@ -12,4 +12,10 @@
|
|||
{{d-icon "asterisk" class="form-template-field__required-indicator"}}
|
||||
{{/if}}
|
||||
</label>
|
||||
|
||||
{{#if @attributes.description}}
|
||||
<span class="form-template-field__description">
|
||||
{{html-safe @attributes.description}}
|
||||
</span>
|
||||
{{/if}}
|
||||
</div>
|
|
@ -8,6 +8,12 @@
|
|||
</label>
|
||||
{{/if}}
|
||||
|
||||
{{#if @attributes.description}}
|
||||
<span class="form-template-field__description">
|
||||
{{html-safe @attributes.description}}
|
||||
</span>
|
||||
{{/if}}
|
||||
|
||||
{{! TODO(@keegan): Update implementation to use <ComboBox/> instead }}
|
||||
{{! Current using <select> as it integrates easily with FormData (will update in v2) }}
|
||||
<select
|
||||
|
|
|
@ -8,6 +8,12 @@
|
|||
</label>
|
||||
{{/if}}
|
||||
|
||||
{{#if @attributes.description}}
|
||||
<span class="form-template-field__description">
|
||||
{{html-safe @attributes.description}}
|
||||
</span>
|
||||
{{/if}}
|
||||
|
||||
<Input
|
||||
name={{@id}}
|
||||
class="form-template-field__input"
|
||||
|
|
|
@ -8,6 +8,12 @@
|
|||
</label>
|
||||
{{/if}}
|
||||
|
||||
{{#if @attributes.description}}
|
||||
<span class="form-template-field__description">
|
||||
{{html-safe @attributes.description}}
|
||||
</span>
|
||||
{{/if}}
|
||||
|
||||
{{! TODO(@keegan): Update implementation to use <MultiSelect/> instead }}
|
||||
{{! Current using <select multiple> as it integrates easily with FormData (will update in v2) }}
|
||||
<select
|
||||
|
|
|
@ -7,6 +7,13 @@
|
|||
{{/if}}
|
||||
</label>
|
||||
{{/if}}
|
||||
|
||||
{{#if @attributes.description}}
|
||||
<span class="form-template-field__description">
|
||||
{{html-safe @attributes.description}}
|
||||
</span>
|
||||
{{/if}}
|
||||
|
||||
<Textarea
|
||||
name={{@id}}
|
||||
@value={{@value}}
|
||||
|
|
|
@ -8,6 +8,12 @@
|
|||
</label>
|
||||
{{/if}}
|
||||
|
||||
{{#if @attributes.description}}
|
||||
<span class="form-template-field__description">
|
||||
{{html-safe @attributes.description}}
|
||||
</span>
|
||||
{{/if}}
|
||||
|
||||
<input type="hidden" name={{@id}} value={{this.uploadValue}} />
|
||||
|
||||
<PickFilesButton
|
||||
|
|
|
@ -0,0 +1,97 @@
|
|||
import Component from "@glimmer/component";
|
||||
import Yaml from "js-yaml";
|
||||
import { tracked } from "@glimmer/tracking";
|
||||
import FormTemplate from "discourse/models/form-template";
|
||||
import { action, get } from "@ember/object";
|
||||
import didUpdate from "@ember/render-modifiers/modifiers/did-update";
|
||||
import CheckboxField from "./checkbox";
|
||||
import InputField from "./input";
|
||||
import DropdownField from "./dropdown";
|
||||
import MultiSelectField from "./multi-select";
|
||||
import TextareaField from "./textarea";
|
||||
import UploadField from "./upload";
|
||||
|
||||
const FormTemplateField = <template>
|
||||
<@component
|
||||
@id={{@content.id}}
|
||||
@attributes={{@content.attributes}}
|
||||
@choices={{@content.choices}}
|
||||
@validations={{@content.validations}}
|
||||
@value={{@initialValue}}
|
||||
/>
|
||||
</template>;
|
||||
|
||||
export default class FormTemplateFieldWrapper extends Component {
|
||||
<template>
|
||||
{{#if this.parsedTemplate}}
|
||||
<div
|
||||
class="form-template-form__wrapper"
|
||||
{{! template-lint-disable modifier-name-case }}
|
||||
{{didUpdate this.refreshTemplate @id}}
|
||||
>
|
||||
{{#each this.parsedTemplate as |content|}}
|
||||
<FormTemplateField
|
||||
@component={{get this.fieldTypes content.type}}
|
||||
@content={{content}}
|
||||
@initialValue={{get this.initialValues content.id}}
|
||||
/>
|
||||
{{/each}}
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="alert alert-error">
|
||||
{{this.error}}
|
||||
</div>
|
||||
{{/if}}
|
||||
</template>
|
||||
|
||||
@tracked error = null;
|
||||
@tracked parsedTemplate = null;
|
||||
|
||||
initialValues = this.args.initialValues ?? {};
|
||||
|
||||
fieldTypes = {
|
||||
checkbox: CheckboxField,
|
||||
input: InputField,
|
||||
dropdown: DropdownField,
|
||||
"multi-select": MultiSelectField,
|
||||
textarea: TextareaField,
|
||||
upload: UploadField,
|
||||
};
|
||||
|
||||
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 {
|
||||
this.parsedTemplate = Yaml.load(templateContent);
|
||||
|
||||
this.args.onSelectFormTemplate?.(this.parsedTemplate);
|
||||
} 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);
|
||||
}
|
||||
}
|
|
@ -1,21 +0,0 @@
|
|||
{{#if this.parsedTemplate}}
|
||||
<div
|
||||
class="form-template-form__wrapper"
|
||||
{{did-update this.refreshTemplate @id}}
|
||||
>
|
||||
{{#each this.parsedTemplate as |content|}}
|
||||
{{component
|
||||
(concat "form-template-field/" content.type)
|
||||
id=content.id
|
||||
attributes=content.attributes
|
||||
choices=content.choices
|
||||
validations=content.validations
|
||||
value=(get @initialValues content.id)
|
||||
}}
|
||||
{{/each}}
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="alert alert-error">
|
||||
{{this.error}}
|
||||
</div>
|
||||
{{/if}}
|
|
@ -1,47 +0,0 @@
|
|||
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;
|
||||
|
||||
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 {
|
||||
this.parsedTemplate = Yaml.load(templateContent);
|
||||
|
||||
this.args.onSelectFormTemplate?.(this.parsedTemplate);
|
||||
} 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);
|
||||
}
|
||||
}
|
|
@ -57,5 +57,18 @@ module(
|
|||
|
||||
assert.dom(".form-template-field__label").doesNotExist();
|
||||
});
|
||||
|
||||
test("renders a description if present", async function (assert) {
|
||||
const attributes = {
|
||||
description: "Your full name",
|
||||
};
|
||||
this.set("attributes", attributes);
|
||||
|
||||
await render(
|
||||
hbs`<FormTemplateField::Input @attributes={{this.attributes}} />`
|
||||
);
|
||||
|
||||
assert.dom(".form-template-field__description").hasText("Your full name");
|
||||
});
|
||||
}
|
||||
);
|
||||
|
|
|
@ -53,5 +53,20 @@ module(
|
|||
|
||||
assert.dom(".form-template-field__label").doesNotExist();
|
||||
});
|
||||
|
||||
test("renders a description if present", async function (assert) {
|
||||
const attributes = {
|
||||
description: "Write your bio here",
|
||||
};
|
||||
this.set("attributes", attributes);
|
||||
|
||||
await render(
|
||||
hbs`<FormTemplateField::Input @attributes={{this.attributes}} />`
|
||||
);
|
||||
|
||||
assert
|
||||
.dom(".form-template-field__description")
|
||||
.hasText("Write your bio here");
|
||||
});
|
||||
}
|
||||
);
|
||||
|
|
|
@ -89,7 +89,7 @@ module(
|
|||
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',
|
||||
'- type: checkbox\n id: options\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',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
|
|
@ -63,4 +63,11 @@
|
|||
&__textarea {
|
||||
min-height: 100px;
|
||||
}
|
||||
|
||||
&__description {
|
||||
display: inline-block;
|
||||
margin-bottom: 0.25rem;
|
||||
font-size: var(--font-down-1);
|
||||
color: var(--primary-medium);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5313,3 +5313,4 @@ en:
|
|||
missing_id: "is missing a field id"
|
||||
duplicate_ids: "has duplicate ids"
|
||||
reserved_id: "has a reserved keyword as id: %{id}"
|
||||
unsafe_description: "has an unsafe HTML description"
|
||||
|
|
|
@ -3,62 +3,72 @@
|
|||
class FormTemplateYamlValidator < ActiveModel::Validator
|
||||
RESERVED_KEYWORDS = %w[title body category category_id tags]
|
||||
ALLOWED_TYPES = %w[checkbox dropdown input multi-select textarea upload]
|
||||
HTML_SANITIZATION_OPTIONS = { elements: ["a"], attributes: { "a" => %w[href target] } }
|
||||
|
||||
def validate(record)
|
||||
begin
|
||||
yaml = Psych.safe_load(record.template)
|
||||
check_missing_fields(record, yaml)
|
||||
check_allowed_types(record, yaml)
|
||||
check_ids(record, yaml)
|
||||
|
||||
unless yaml.is_a?(Array)
|
||||
record.errors.add(:template, I18n.t("form_templates.errors.invalid_yaml"))
|
||||
return
|
||||
end
|
||||
|
||||
existing_ids = []
|
||||
yaml.each do |field|
|
||||
check_missing_fields(record, field)
|
||||
check_allowed_types(record, field)
|
||||
check_ids(record, field, existing_ids)
|
||||
check_descriptions_html(record, field)
|
||||
end
|
||||
rescue Psych::SyntaxError
|
||||
record.errors.add(:template, I18n.t("form_templates.errors.invalid_yaml"))
|
||||
end
|
||||
end
|
||||
|
||||
def check_allowed_types(record, yaml)
|
||||
yaml.each do |field|
|
||||
if !ALLOWED_TYPES.include?(field["type"])
|
||||
return(
|
||||
record.errors.add(
|
||||
:template,
|
||||
I18n.t(
|
||||
"form_templates.errors.invalid_type",
|
||||
type: field["type"],
|
||||
valid_types: ALLOWED_TYPES.join(", "),
|
||||
),
|
||||
)
|
||||
)
|
||||
end
|
||||
def check_allowed_types(record, field)
|
||||
if !ALLOWED_TYPES.include?(field["type"])
|
||||
record.errors.add(
|
||||
:template,
|
||||
I18n.t(
|
||||
"form_templates.errors.invalid_type",
|
||||
type: field["type"],
|
||||
valid_types: ALLOWED_TYPES.join(", "),
|
||||
),
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
def check_missing_fields(record, yaml)
|
||||
yaml.each do |field|
|
||||
if field["type"].blank?
|
||||
return(record.errors.add(:template, I18n.t("form_templates.errors.missing_type")))
|
||||
end
|
||||
if field["id"].blank?
|
||||
return(record.errors.add(:template, I18n.t("form_templates.errors.missing_id")))
|
||||
end
|
||||
def check_missing_fields(record, field)
|
||||
if field["type"].blank?
|
||||
record.errors.add(:template, I18n.t("form_templates.errors.missing_type"))
|
||||
end
|
||||
record.errors.add(:template, I18n.t("form_templates.errors.missing_id")) if field["id"].blank?
|
||||
end
|
||||
|
||||
def check_descriptions_html(record, field)
|
||||
description = field.dig("attributes", "description")
|
||||
|
||||
return if description.blank?
|
||||
|
||||
sanitized_html = Sanitize.fragment(description, HTML_SANITIZATION_OPTIONS)
|
||||
|
||||
is_safe_html = sanitized_html == Loofah.html5_fragment(description).to_s
|
||||
|
||||
unless is_safe_html
|
||||
record.errors.add(:template, I18n.t("form_templates.errors.unsafe_description"))
|
||||
end
|
||||
end
|
||||
|
||||
def check_ids(record, yaml)
|
||||
ids = []
|
||||
yaml.each do |field|
|
||||
next if field["id"].blank?
|
||||
|
||||
if RESERVED_KEYWORDS.include?(field["id"])
|
||||
return(
|
||||
record.errors.add(:template, I18n.t("form_templates.errors.reserved_id", id: field["id"]))
|
||||
)
|
||||
end
|
||||
|
||||
if ids.include?(field["id"])
|
||||
return(record.errors.add(:template, I18n.t("form_templates.errors.duplicate_ids")))
|
||||
end
|
||||
|
||||
ids << field["id"]
|
||||
def check_ids(record, field, existing_ids)
|
||||
if RESERVED_KEYWORDS.include?(field["id"])
|
||||
record.errors.add(:template, I18n.t("form_templates.errors.reserved_id", id: field["id"]))
|
||||
end
|
||||
|
||||
if existing_ids.include?(field["id"])
|
||||
record.errors.add(:template, I18n.t("form_templates.errors.duplicate_ids"))
|
||||
end
|
||||
|
||||
existing_ids << field["id"] unless field["id"].blank?
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,174 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require "rails_helper"
|
||||
|
||||
RSpec.describe FormTemplateYamlValidator, type: :validator do
|
||||
subject(:validator) { described_class.new }
|
||||
|
||||
let(:form_template) { FormTemplate.new(template: yaml_content) }
|
||||
|
||||
describe "#validate" do
|
||||
context "with valid YAML" do
|
||||
let(:yaml_content) { <<~YAML }
|
||||
- type: input
|
||||
id: name
|
||||
attributes:
|
||||
label: "Full name"
|
||||
placeholder: "eg. John Smith"
|
||||
description: "What is your full name?"
|
||||
validations:
|
||||
required: true
|
||||
minimum: 2
|
||||
maximum: 100
|
||||
YAML
|
||||
|
||||
it "does not add any errors" do
|
||||
validator.validate(form_template)
|
||||
expect(form_template.errors).to be_empty
|
||||
end
|
||||
end
|
||||
|
||||
context "with invalid YAML" do
|
||||
let(:yaml_content) { "invalid_yaml_string" }
|
||||
|
||||
it "adds an error message for invalid YAML" do
|
||||
validator.validate(form_template)
|
||||
expect(form_template.errors[:template]).to include(
|
||||
I18n.t("form_templates.errors.invalid_yaml"),
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#check_missing_fields" do
|
||||
context "when type field is missing" do
|
||||
let(:yaml_content) { <<~YAML }
|
||||
- id: name
|
||||
attributes:
|
||||
label: "Full name"
|
||||
YAML
|
||||
|
||||
it "adds an error for missing type field" do
|
||||
validator.validate(form_template)
|
||||
expect(form_template.errors[:template]).to include(
|
||||
I18n.t("form_templates.errors.missing_type"),
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context "when id field is missing" do
|
||||
let(:yaml_content) { <<~YAML }
|
||||
- type: input
|
||||
attributes:
|
||||
label: "Full name"
|
||||
YAML
|
||||
|
||||
it "adds an error for missing id field" do
|
||||
validator.validate(form_template)
|
||||
expect(form_template.errors[:template]).to include(
|
||||
I18n.t("form_templates.errors.missing_id"),
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#check_allowed_types" do
|
||||
context "when YAML has invalid field types" do
|
||||
let(:yaml_content) { <<~YAML }
|
||||
- type: invalid_type
|
||||
id: name
|
||||
attributes:
|
||||
label: "Full name"
|
||||
YAML
|
||||
|
||||
it "adds an error for invalid field types" do
|
||||
validator.validate(form_template)
|
||||
expect(form_template.errors[:template]).to include(
|
||||
I18n.t(
|
||||
"form_templates.errors.invalid_type",
|
||||
type: "invalid_type",
|
||||
valid_types: FormTemplateYamlValidator::ALLOWED_TYPES.join(", "),
|
||||
),
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context "when field type is allowed" do
|
||||
let(:yaml_content) { <<~YAML }
|
||||
- type: input
|
||||
id: name
|
||||
YAML
|
||||
|
||||
it "does not add an error for valid field type" do
|
||||
validator.validate(form_template)
|
||||
expect(form_template.errors[:template]).to be_empty
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#check_descriptions_html" do
|
||||
context "when description field has safe HTML" do
|
||||
let(:yaml_content) { <<~YAML }
|
||||
- type: input
|
||||
id: name
|
||||
attributes:
|
||||
label: "Full name"
|
||||
description: "What is your full name? Details <a href='https://test.com'>here</a>."
|
||||
YAML
|
||||
|
||||
it "does not add an error" do
|
||||
validator.validate(form_template)
|
||||
expect(form_template.errors[:template]).to be_empty
|
||||
end
|
||||
end
|
||||
|
||||
context "when description field has unsafe HTML" do
|
||||
let(:yaml_content) { <<~YAML }
|
||||
- type: input
|
||||
id: name
|
||||
attributes:
|
||||
label: "Full name"
|
||||
description: "What is your full name? Details <script>window.alert('hey');</script>."
|
||||
YAML
|
||||
|
||||
it "adds a validation error" do
|
||||
validator.validate(form_template)
|
||||
expect(form_template.errors[:template]).to include(
|
||||
I18n.t("form_templates.errors.unsafe_description"),
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#check_ids" do
|
||||
context "when YAML has duplicate ids" do
|
||||
let(:yaml_content) { <<~YAML }
|
||||
- type: input
|
||||
id: name
|
||||
- type: input
|
||||
id: name
|
||||
YAML
|
||||
|
||||
it "adds an error for duplicate ids" do
|
||||
validator.validate(form_template)
|
||||
expect(form_template.errors[:template]).to include(
|
||||
I18n.t("form_templates.errors.duplicate_ids"),
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context "when YAML has reserved ids" do
|
||||
let(:yaml_content) { <<~YAML }
|
||||
- type: input
|
||||
id: title
|
||||
YAML
|
||||
|
||||
it "adds an error for reserved ids" do
|
||||
validator.validate(form_template)
|
||||
expect(form_template.errors[:template]).to include(
|
||||
I18n.t("form_templates.errors.reserved_id", id: "title"),
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -56,6 +56,31 @@ describe "Composer Form Templates", type: :system do
|
|||
required: false"),
|
||||
)
|
||||
end
|
||||
fab!(:form_template_6) do
|
||||
Fabricate(
|
||||
:form_template,
|
||||
name: "Descriptions",
|
||||
template:
|
||||
%Q(
|
||||
- type: input
|
||||
id: full-name
|
||||
attributes:
|
||||
label: "Full name"
|
||||
description: "What is your full name?"
|
||||
placeholder: "John Smith"
|
||||
validations:
|
||||
required: false
|
||||
- type: upload
|
||||
id: prescription
|
||||
attributes:
|
||||
file_types: ".jpg, .png"
|
||||
allow_multiple: false
|
||||
label: "Prescription"
|
||||
description: "Upload your prescription"
|
||||
validations:
|
||||
required: true"),
|
||||
)
|
||||
end
|
||||
fab!(:category_with_template_1) do
|
||||
Fabricate(
|
||||
:category,
|
||||
|
@ -114,6 +139,15 @@ describe "Composer Form Templates", type: :system do
|
|||
topic_template: "Testing",
|
||||
)
|
||||
end
|
||||
fab!(:category_with_template_6) do
|
||||
Fabricate(
|
||||
:category,
|
||||
name: "Descriptions",
|
||||
slug: "descriptions",
|
||||
topic_count: 2,
|
||||
form_template_ids: [form_template_6.id],
|
||||
)
|
||||
end
|
||||
|
||||
let(:category_page) { PageObjects::Pages::Category.new }
|
||||
let(:composer) { PageObjects::Components::Composer.new }
|
||||
|
@ -293,4 +327,19 @@ describe "Composer Form Templates", type: :system do
|
|||
expect(find("#{topic_page.post_by_number_selector(1)} .cooked")).to have_css("audio")
|
||||
expect(find("#{topic_page.post_by_number_selector(1)} .cooked")).to have_css("video")
|
||||
end
|
||||
|
||||
it "shows labels and descriptions when a form template is assigned to the category" do
|
||||
category_page.visit(category_with_template_6)
|
||||
category_page.new_topic_button.click
|
||||
expect(composer).to have_no_composer_input
|
||||
expect(composer).to have_form_template
|
||||
|
||||
expect(composer).to have_form_template_field("input")
|
||||
expect(composer).to have_form_template_field_label("Full name")
|
||||
expect(composer).to have_form_template_field_description("What is your full name?")
|
||||
|
||||
expect(composer).to have_form_template_field("upload")
|
||||
expect(composer).to have_form_template_field_label("Prescription")
|
||||
expect(composer).to have_form_template_field_description("Upload your prescription")
|
||||
end
|
||||
end
|
||||
|
|
|
@ -170,6 +170,14 @@ module PageObjects
|
|||
page.has_css?(".form-template-field__error", text: error)
|
||||
end
|
||||
|
||||
def has_form_template_field_label?(label)
|
||||
page.has_css?(".form-template-field__label", text: label)
|
||||
end
|
||||
|
||||
def has_form_template_field_description?(description)
|
||||
page.has_css?(".form-template-field__description", text: description)
|
||||
end
|
||||
|
||||
def composer_input
|
||||
find("#{COMPOSER_ID} .d-editor .d-editor-input")
|
||||
end
|
||||
|
|
Loading…
Reference in New Issue