mirror of
https://github.com/discourse/discourse.git
synced 2025-06-06 13:06:56 +08:00
FEATURE: support a description attribute on form template fields (#23744)
* FEATURE: support a description attribute on form template fields
This commit is contained in:
@ -12,4 +12,10 @@
|
|||||||
{{d-icon "asterisk" class="form-template-field__required-indicator"}}
|
{{d-icon "asterisk" class="form-template-field__required-indicator"}}
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
|
{{#if @attributes.description}}
|
||||||
|
<span class="form-template-field__description">
|
||||||
|
{{html-safe @attributes.description}}
|
||||||
|
</span>
|
||||||
|
{{/if}}
|
||||||
</div>
|
</div>
|
@ -8,6 +8,12 @@
|
|||||||
</label>
|
</label>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
|
{{#if @attributes.description}}
|
||||||
|
<span class="form-template-field__description">
|
||||||
|
{{html-safe @attributes.description}}
|
||||||
|
</span>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
{{! TODO(@keegan): Update implementation to use <ComboBox/> instead }}
|
{{! TODO(@keegan): Update implementation to use <ComboBox/> instead }}
|
||||||
{{! Current using <select> as it integrates easily with FormData (will update in v2) }}
|
{{! Current using <select> as it integrates easily with FormData (will update in v2) }}
|
||||||
<select
|
<select
|
||||||
|
@ -8,6 +8,12 @@
|
|||||||
</label>
|
</label>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
|
{{#if @attributes.description}}
|
||||||
|
<span class="form-template-field__description">
|
||||||
|
{{html-safe @attributes.description}}
|
||||||
|
</span>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
<Input
|
<Input
|
||||||
name={{@id}}
|
name={{@id}}
|
||||||
class="form-template-field__input"
|
class="form-template-field__input"
|
||||||
|
@ -8,6 +8,12 @@
|
|||||||
</label>
|
</label>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
|
{{#if @attributes.description}}
|
||||||
|
<span class="form-template-field__description">
|
||||||
|
{{html-safe @attributes.description}}
|
||||||
|
</span>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
{{! TODO(@keegan): Update implementation to use <MultiSelect/> instead }}
|
{{! TODO(@keegan): Update implementation to use <MultiSelect/> instead }}
|
||||||
{{! Current using <select multiple> as it integrates easily with FormData (will update in v2) }}
|
{{! Current using <select multiple> as it integrates easily with FormData (will update in v2) }}
|
||||||
<select
|
<select
|
||||||
|
@ -7,6 +7,13 @@
|
|||||||
{{/if}}
|
{{/if}}
|
||||||
</label>
|
</label>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
|
{{#if @attributes.description}}
|
||||||
|
<span class="form-template-field__description">
|
||||||
|
{{html-safe @attributes.description}}
|
||||||
|
</span>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
<Textarea
|
<Textarea
|
||||||
name={{@id}}
|
name={{@id}}
|
||||||
@value={{@value}}
|
@value={{@value}}
|
||||||
|
@ -8,6 +8,12 @@
|
|||||||
</label>
|
</label>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
|
{{#if @attributes.description}}
|
||||||
|
<span class="form-template-field__description">
|
||||||
|
{{html-safe @attributes.description}}
|
||||||
|
</span>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
<input type="hidden" name={{@id}} value={{this.uploadValue}} />
|
<input type="hidden" name={{@id}} value={{this.uploadValue}} />
|
||||||
|
|
||||||
<PickFilesButton
|
<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();
|
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();
|
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,
|
id: 1,
|
||||||
name: "Bug Reports",
|
name: "Bug Reports",
|
||||||
template:
|
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 {
|
&__textarea {
|
||||||
min-height: 100px;
|
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"
|
missing_id: "is missing a field id"
|
||||||
duplicate_ids: "has duplicate ids"
|
duplicate_ids: "has duplicate ids"
|
||||||
reserved_id: "has a reserved keyword as id: %{id}"
|
reserved_id: "has a reserved keyword as id: %{id}"
|
||||||
|
unsafe_description: "has an unsafe HTML description"
|
||||||
|
@ -3,62 +3,72 @@
|
|||||||
class FormTemplateYamlValidator < ActiveModel::Validator
|
class FormTemplateYamlValidator < ActiveModel::Validator
|
||||||
RESERVED_KEYWORDS = %w[title body category category_id tags]
|
RESERVED_KEYWORDS = %w[title body category category_id tags]
|
||||||
ALLOWED_TYPES = %w[checkbox dropdown input multi-select textarea upload]
|
ALLOWED_TYPES = %w[checkbox dropdown input multi-select textarea upload]
|
||||||
|
HTML_SANITIZATION_OPTIONS = { elements: ["a"], attributes: { "a" => %w[href target] } }
|
||||||
|
|
||||||
def validate(record)
|
def validate(record)
|
||||||
begin
|
begin
|
||||||
yaml = Psych.safe_load(record.template)
|
yaml = Psych.safe_load(record.template)
|
||||||
check_missing_fields(record, yaml)
|
|
||||||
check_allowed_types(record, yaml)
|
unless yaml.is_a?(Array)
|
||||||
check_ids(record, yaml)
|
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
|
rescue Psych::SyntaxError
|
||||||
record.errors.add(:template, I18n.t("form_templates.errors.invalid_yaml"))
|
record.errors.add(:template, I18n.t("form_templates.errors.invalid_yaml"))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def check_allowed_types(record, yaml)
|
def check_allowed_types(record, field)
|
||||||
yaml.each do |field|
|
if !ALLOWED_TYPES.include?(field["type"])
|
||||||
if !ALLOWED_TYPES.include?(field["type"])
|
record.errors.add(
|
||||||
return(
|
:template,
|
||||||
record.errors.add(
|
I18n.t(
|
||||||
:template,
|
"form_templates.errors.invalid_type",
|
||||||
I18n.t(
|
type: field["type"],
|
||||||
"form_templates.errors.invalid_type",
|
valid_types: ALLOWED_TYPES.join(", "),
|
||||||
type: field["type"],
|
),
|
||||||
valid_types: ALLOWED_TYPES.join(", "),
|
)
|
||||||
),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def check_missing_fields(record, yaml)
|
def check_missing_fields(record, field)
|
||||||
yaml.each do |field|
|
if field["type"].blank?
|
||||||
if field["type"].blank?
|
record.errors.add(:template, I18n.t("form_templates.errors.missing_type"))
|
||||||
return(record.errors.add(:template, I18n.t("form_templates.errors.missing_type")))
|
end
|
||||||
end
|
record.errors.add(:template, I18n.t("form_templates.errors.missing_id")) if field["id"].blank?
|
||||||
if field["id"].blank?
|
end
|
||||||
return(record.errors.add(:template, I18n.t("form_templates.errors.missing_id")))
|
|
||||||
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
|
||||||
end
|
end
|
||||||
|
|
||||||
def check_ids(record, yaml)
|
def check_ids(record, field, existing_ids)
|
||||||
ids = []
|
if RESERVED_KEYWORDS.include?(field["id"])
|
||||||
yaml.each do |field|
|
record.errors.add(:template, I18n.t("form_templates.errors.reserved_id", id: field["id"]))
|
||||||
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"]
|
|
||||||
end
|
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
|
||||||
end
|
end
|
||||||
|
174
spec/lib/validators/form_template_yaml_validator_spec.rb
Normal file
174
spec/lib/validators/form_template_yaml_validator_spec.rb
Normal file
@ -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"),
|
required: false"),
|
||||||
)
|
)
|
||||||
end
|
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
|
fab!(:category_with_template_1) do
|
||||||
Fabricate(
|
Fabricate(
|
||||||
:category,
|
:category,
|
||||||
@ -114,6 +139,15 @@ describe "Composer Form Templates", type: :system do
|
|||||||
topic_template: "Testing",
|
topic_template: "Testing",
|
||||||
)
|
)
|
||||||
end
|
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(:category_page) { PageObjects::Pages::Category.new }
|
||||||
let(:composer) { PageObjects::Components::Composer.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("audio")
|
||||||
expect(find("#{topic_page.post_by_number_selector(1)} .cooked")).to have_css("video")
|
expect(find("#{topic_page.post_by_number_selector(1)} .cooked")).to have_css("video")
|
||||||
end
|
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
|
end
|
||||||
|
@ -170,6 +170,14 @@ module PageObjects
|
|||||||
page.has_css?(".form-template-field__error", text: error)
|
page.has_css?(".form-template-field__error", text: error)
|
||||||
end
|
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
|
def composer_input
|
||||||
find("#{COMPOSER_ID} .d-editor .d-editor-input")
|
find("#{COMPOSER_ID} .d-editor .d-editor-input")
|
||||||
end
|
end
|
||||||
|
Reference in New Issue
Block a user