mirror of
https://github.com/discourse/discourse.git
synced 2024-11-26 02:40:53 -06:00
DEV: Create posts from form templates (#21980)
This commit is contained in:
parent
4973f0ccde
commit
39efa4c32a
@ -6,7 +6,7 @@ export default class AdminFormTemplateValidationOptions extends Controller.exten
|
||||
ModalFunctionality
|
||||
) {
|
||||
TABLE_HEADER_KEYS = ["key", "type", "description"];
|
||||
VALIDATION_KEYS = ["required", "minimum", "maximum", "pattern"];
|
||||
VALIDATION_KEYS = ["required", "minimum", "maximum", "pattern", "type"];
|
||||
|
||||
get tableHeaders() {
|
||||
const translatedHeaders = [];
|
||||
|
@ -17,6 +17,7 @@
|
||||
@onPopupMenuAction={{this.onPopupMenuAction}}
|
||||
@popupMenuOptions={{this.popupMenuOptions}}
|
||||
@formTemplateIds={{this.formTemplateIds}}
|
||||
@replyingToTopic={{this.composer.replyingToTopic}}
|
||||
@disabled={{this.disableTextarea}}
|
||||
@outletArgs={{hash composer=this.composer editorType="composer"}}
|
||||
>
|
||||
|
@ -183,6 +183,11 @@ export default class ComposerMessages extends Component {
|
||||
return;
|
||||
}
|
||||
|
||||
// We don't care about similar topics when creating with a form template
|
||||
if (this.composer?.category?.form_template_ids.length > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: pass the 200 in from somewhere
|
||||
const raw = (this.composer.reply || "").slice(0, 200);
|
||||
const title = this.composer.title || "";
|
||||
|
@ -1,7 +1,10 @@
|
||||
<div class="d-editor-container">
|
||||
<div
|
||||
class="d-editor-container
|
||||
{{if this.showFormTemplateForm 'has-form-template'}}"
|
||||
>
|
||||
<div class="d-editor-textarea-column">
|
||||
{{yield}}
|
||||
{{#if @formTemplateIds}}
|
||||
{{#if this.showFormTemplateForm}}
|
||||
{{#if (gt @formTemplateIds.length 1)}}
|
||||
<FormTemplateChooser
|
||||
@class="composer-select-form-template"
|
||||
@ -11,7 +14,9 @@
|
||||
@options={{hash maximum=1}}
|
||||
/>
|
||||
{{/if}}
|
||||
<FormTemplateField::Wrapper @id={{this.selectedFormTemplateId}} />
|
||||
<form id="form-template-form">
|
||||
<FormTemplateField::Wrapper @id={{this.selectedFormTemplateId}} />
|
||||
</form>
|
||||
{{else}}
|
||||
<div
|
||||
class="d-editor-textarea-wrapper
|
||||
|
@ -247,6 +247,15 @@ export default Component.extend(TextareaTextManipulation, {
|
||||
this.selectedFormTemplateId = formTemplateId;
|
||||
},
|
||||
|
||||
@discourseComputed("formTemplateIds", "replyingToTopic")
|
||||
showFormTemplateForm(formTemplateIds, replyingToTopic) {
|
||||
if (formTemplateIds?.length > 0 && !replyingToTopic) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
@discourseComputed("placeholder")
|
||||
placeholderTranslated(placeholder) {
|
||||
if (placeholder) {
|
||||
|
@ -1,6 +1,11 @@
|
||||
<div class="control-group form-template-field" data-field-type="checkbox">
|
||||
<label class="form-template-field__label">
|
||||
<Input class="form-template-field__checkbox" @type="checkbox" />
|
||||
<Input
|
||||
name={{@attributes.label}}
|
||||
class="form-template-field__checkbox"
|
||||
@type="checkbox"
|
||||
required={{if @validations.required "required" ""}}
|
||||
/>
|
||||
{{@attributes.label}}
|
||||
</label>
|
||||
</div>
|
@ -2,14 +2,25 @@
|
||||
{{#if @attributes.label}}
|
||||
<label class="form-template-field__label">{{@attributes.label}}</label>
|
||||
{{/if}}
|
||||
<ComboBox
|
||||
@class="form-template-field__dropdown"
|
||||
@content={{@choices}}
|
||||
@nameProperty={{null}}
|
||||
@valueProperty={{null}}
|
||||
@options={{hash
|
||||
translatedNone=@attributes.none_label
|
||||
filterable=@attributes.filterable
|
||||
}}
|
||||
/>
|
||||
|
||||
{{! TODO(@keegan): Update implementation to use <ComboBox/> instead }}
|
||||
{{! Current using <select> as it integrates easily with FormData (will update in v2) }}
|
||||
<select
|
||||
name={{@attributes.label}}
|
||||
class="form-template-field__dropdown"
|
||||
required={{if @validations.required "required" ""}}
|
||||
>
|
||||
{{#if @attributes.none_label}}
|
||||
<option
|
||||
class="form-template-field__dropdown-placeholder"
|
||||
value=""
|
||||
disabled
|
||||
selected
|
||||
hidden
|
||||
>{{@attributes.none_label}}</option>
|
||||
{{/if}}
|
||||
{{#each @choices as |choice|}}
|
||||
<option value={{choice}}>{{choice}}</option>
|
||||
{{/each}}
|
||||
</select>
|
||||
</div>
|
@ -1,10 +1,17 @@
|
||||
<div class="control-group form-template-field" data-field-type="input">
|
||||
{{! TODO(@keegan): Make label required }}
|
||||
{{#if @attributes.label}}
|
||||
<label class="form-template-field__label">{{@attributes.label}}</label>
|
||||
{{/if}}
|
||||
|
||||
<Input
|
||||
name={{@attributes.label}}
|
||||
class="form-template-field__input"
|
||||
@type="text"
|
||||
@type={{if @validations.type @validations.type "text"}}
|
||||
placeholder={{@attributes.placeholder}}
|
||||
required={{if @validations.required "required" ""}}
|
||||
pattern={{@validations.pattern}}
|
||||
minLength={{@validations.min}}
|
||||
maxLength={{@validations.max}}
|
||||
/>
|
||||
</div>
|
@ -2,14 +2,25 @@
|
||||
{{#if @attributes.label}}
|
||||
<label class="form-template-field__label">{{@attributes.label}}</label>
|
||||
{{/if}}
|
||||
<MultiSelect
|
||||
@class="form-template-field__multi-select"
|
||||
@content={{@choices}}
|
||||
@nameProperty={{null}}
|
||||
@valueProperty={{null}}
|
||||
@options={{hash
|
||||
translatedNone=@attributes.none_label
|
||||
maximum=@validations.maximum
|
||||
}}
|
||||
/>
|
||||
|
||||
{{! TODO(@keegan): Update implementation to use <MultiSelect/> instead }}
|
||||
{{! Current using <select multiple> as it integrates easily with FormData (will update in v2) }}
|
||||
<select
|
||||
name={{@attributes.label}}
|
||||
class="form-template-field__multi-select"
|
||||
required={{if @validations.required "required" ""}}
|
||||
multiple="multiple"
|
||||
>
|
||||
{{#if @attributes.none_label}}
|
||||
<option
|
||||
class="form-template-field__multi-select-placeholder"
|
||||
value=""
|
||||
disabled
|
||||
hidden
|
||||
>{{@attributes.none_label}}</option>
|
||||
{{/if}}
|
||||
{{#each @choices as |choice|}}
|
||||
<option value={{choice}}>{{choice}}</option>
|
||||
{{/each}}
|
||||
</select>
|
||||
</div>
|
@ -3,7 +3,12 @@
|
||||
<label class="form-template-field__label">{{@attributes.label}}</label>
|
||||
{{/if}}
|
||||
<Textarea
|
||||
name={{@attributes.label}}
|
||||
class="form-template-field__textarea"
|
||||
placeholder={{@attributes.placeholder}}
|
||||
pattern={{@validations.pattern}}
|
||||
minLength={{@validations.min}}
|
||||
maxLength={{@validations.max}}
|
||||
required={{if @validations.required "required" ""}}
|
||||
/>
|
||||
</div>
|
@ -0,0 +1,82 @@
|
||||
export default function prepareFormTemplateData(form) {
|
||||
const formData = new FormData(form);
|
||||
|
||||
// Validate the form template
|
||||
_validateFormTemplateData(form);
|
||||
if (!form.checkValidity()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Gather form template data
|
||||
const formDetails = [];
|
||||
for (let [key, value] of formData.entries()) {
|
||||
formDetails.push({
|
||||
[key]: value,
|
||||
});
|
||||
}
|
||||
|
||||
const mergedData = [];
|
||||
const mergedKeys = new Set();
|
||||
|
||||
for (const item of formDetails) {
|
||||
const key = Object.keys(item)[0]; // Get the key of the current item
|
||||
if (mergedKeys.has(key)) {
|
||||
// If the key has already been merged, append the value to the existing key
|
||||
mergedData[mergedData.length - 1][key] += "\n" + item[key];
|
||||
} else {
|
||||
mergedData.push(item);
|
||||
mergedKeys.add(key);
|
||||
}
|
||||
}
|
||||
|
||||
// Construct formatted post output
|
||||
const formattedOutput = mergedData.map((item) => {
|
||||
const key = Object.keys(item)[0];
|
||||
const value = item[key];
|
||||
if (value) {
|
||||
return `### ${key}\n${value}`;
|
||||
}
|
||||
});
|
||||
|
||||
return formattedOutput.join("\n\n");
|
||||
}
|
||||
|
||||
function _validateFormTemplateData(form) {
|
||||
const fields = Array.from(form.elements);
|
||||
|
||||
fields.forEach((field) => {
|
||||
field.setAttribute("aria-invalid", false);
|
||||
|
||||
const errorBox = document.createElement("div");
|
||||
errorBox.classList.add("form-template-field__error", "popup-tip");
|
||||
const errorId = field.id + "-error";
|
||||
|
||||
field.addEventListener("invalid", () => {
|
||||
field.setAttribute("aria-invalid", true);
|
||||
errorBox.classList.add("bad");
|
||||
errorBox.setAttribute("id", errorId);
|
||||
field.setAttribute("aria-describedby", errorId);
|
||||
|
||||
if (!field.nextElementSibling) {
|
||||
field.insertAdjacentElement("afterend", errorBox);
|
||||
}
|
||||
|
||||
errorBox.textContent = field.validationMessage;
|
||||
});
|
||||
|
||||
// Mark the field as valid as changed:
|
||||
field.addEventListener("input", () => {
|
||||
const valid = field.checkValidity();
|
||||
if (valid) {
|
||||
field.setAttribute("aria-invalid", false);
|
||||
errorBox.classList.remove("bad");
|
||||
|
||||
errorBox.textContent = "";
|
||||
}
|
||||
});
|
||||
|
||||
field.addEventListener("blur", () => {
|
||||
field.checkValidity();
|
||||
});
|
||||
});
|
||||
}
|
@ -33,6 +33,7 @@ import { categoryBadgeHTML } from "discourse/helpers/category-link";
|
||||
import renderTags from "discourse/lib/render-tags";
|
||||
import { htmlSafe } from "@ember/template";
|
||||
import { iconHTML } from "discourse-common/lib/icon-library";
|
||||
import prepareFormTemplateData from "discourse/lib/form-template-validation";
|
||||
|
||||
async function loadDraft(store, opts = {}) {
|
||||
let { draft, draftKey, draftSequence } = opts;
|
||||
@ -929,6 +930,22 @@ export default class ComposerController extends Controller {
|
||||
this.set("showPreview", false);
|
||||
}
|
||||
|
||||
if (this.siteSettings.experimental_form_templates) {
|
||||
if (
|
||||
this.formTemplateIds?.length > 0 &&
|
||||
!this.get("model.replyingToTopic")
|
||||
) {
|
||||
const formTemplateData = prepareFormTemplateData(
|
||||
document.querySelector("#form-template-form")
|
||||
);
|
||||
if (formTemplateData) {
|
||||
this.model.set("reply", formTemplateData);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const composer = this.model;
|
||||
|
||||
if (composer?.cantSubmitPost) {
|
||||
@ -1246,6 +1263,7 @@ export default class ComposerController extends Controller {
|
||||
"id",
|
||||
opts.prioritizedCategoryId
|
||||
);
|
||||
|
||||
if (category) {
|
||||
this.set("prioritizedCategoryId", opts.prioritizedCategoryId);
|
||||
}
|
||||
|
@ -3,7 +3,7 @@ import { setupRenderingTest } from "discourse/tests/helpers/component-test";
|
||||
import { render } from "@ember/test-helpers";
|
||||
import { hbs } from "ember-cli-htmlbars";
|
||||
import selectKit from "discourse/tests/helpers/select-kit-helper";
|
||||
import { exists } from "discourse/tests/helpers/qunit-helpers";
|
||||
import { exists, query, queryAll } from "discourse/tests/helpers/qunit-helpers";
|
||||
|
||||
module(
|
||||
"Integration | Component | form-template-field | dropdown",
|
||||
@ -16,7 +16,6 @@ module(
|
||||
|
||||
test("renders a dropdown with choices", async function (assert) {
|
||||
const choices = ["Choice 1", "Choice 2", "Choice 3"];
|
||||
|
||||
this.set("choices", choices);
|
||||
|
||||
await render(
|
||||
@ -27,22 +26,22 @@ module(
|
||||
"A dropdown component exists"
|
||||
);
|
||||
|
||||
await this.subject.expand();
|
||||
|
||||
const dropdown = this.subject.displayedContent();
|
||||
const dropdown = queryAll(
|
||||
".form-template-field__dropdown option:not(.form-template-field__dropdown-placeholder)"
|
||||
);
|
||||
assert.strictEqual(dropdown.length, 3, "it has 3 choices");
|
||||
assert.strictEqual(
|
||||
dropdown[0].name,
|
||||
dropdown[0].value,
|
||||
"Choice 1",
|
||||
"it has the correct name for choice 1"
|
||||
);
|
||||
assert.strictEqual(
|
||||
dropdown[1].name,
|
||||
dropdown[1].value,
|
||||
"Choice 2",
|
||||
"it has the correct name for choice 2"
|
||||
);
|
||||
assert.strictEqual(
|
||||
dropdown[2].name,
|
||||
dropdown[2].value,
|
||||
"Choice 3",
|
||||
"it has the correct name for choice 3"
|
||||
);
|
||||
@ -66,9 +65,8 @@ module(
|
||||
"A dropdown component exists"
|
||||
);
|
||||
|
||||
await this.subject.expand();
|
||||
assert.strictEqual(
|
||||
this.subject.header().label(),
|
||||
query(".form-template-field__dropdown-placeholder").innerText,
|
||||
attributes.none_label,
|
||||
"None label is correct"
|
||||
);
|
||||
|
@ -3,7 +3,7 @@ import { setupRenderingTest } from "discourse/tests/helpers/component-test";
|
||||
import { render } from "@ember/test-helpers";
|
||||
import { hbs } from "ember-cli-htmlbars";
|
||||
import selectKit from "discourse/tests/helpers/select-kit-helper";
|
||||
import { exists } from "discourse/tests/helpers/qunit-helpers";
|
||||
import { exists, query, queryAll } from "discourse/tests/helpers/qunit-helpers";
|
||||
|
||||
module(
|
||||
"Integration | Component | form-template-field | multi-select",
|
||||
@ -27,22 +27,22 @@ module(
|
||||
"A multiselect component exists"
|
||||
);
|
||||
|
||||
await this.subject.expand();
|
||||
|
||||
const dropdown = this.subject.displayedContent();
|
||||
const dropdown = queryAll(
|
||||
".form-template-field__multi-select option:not(.form-template-field__multi-select-placeholder)"
|
||||
);
|
||||
assert.strictEqual(dropdown.length, 3, "it has 3 choices");
|
||||
assert.strictEqual(
|
||||
dropdown[0].name,
|
||||
dropdown[0].value,
|
||||
"Choice 1",
|
||||
"it has the correct name for choice 1"
|
||||
);
|
||||
assert.strictEqual(
|
||||
dropdown[1].name,
|
||||
dropdown[1].value,
|
||||
"Choice 2",
|
||||
"it has the correct name for choice 2"
|
||||
);
|
||||
assert.strictEqual(
|
||||
dropdown[2].name,
|
||||
dropdown[2].value,
|
||||
"Choice 3",
|
||||
"it has the correct name for choice 3"
|
||||
);
|
||||
@ -66,9 +66,8 @@ module(
|
||||
"A multiselect dropdown component exists"
|
||||
);
|
||||
|
||||
await this.subject.expand();
|
||||
assert.strictEqual(
|
||||
this.subject.header().label(),
|
||||
query(".form-template-field__multi-select-placeholder").innerText,
|
||||
attributes.none_label,
|
||||
"None label is correct"
|
||||
);
|
||||
|
@ -16,9 +16,19 @@ html.composer-open {
|
||||
margin-right: auto;
|
||||
max-width: $reply-area-max-width;
|
||||
width: 100%;
|
||||
|
||||
&.hide-preview {
|
||||
max-width: 740px;
|
||||
}
|
||||
|
||||
&:has(.has-form-template) {
|
||||
max-width: 740px;
|
||||
|
||||
.toggle-preview {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1200px) {
|
||||
min-width: 0;
|
||||
}
|
||||
@ -356,6 +366,10 @@ html.composer-open {
|
||||
color: var(--danger);
|
||||
}
|
||||
}
|
||||
|
||||
.preview-template {
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
#draft-status,
|
||||
|
@ -14,6 +14,7 @@
|
||||
@import "date-time-input-range";
|
||||
@import "date-time-input";
|
||||
@import "footer-nav";
|
||||
@import "form-template-field";
|
||||
@import "group-member-dropdown";
|
||||
@import "groups-form-membership-fields";
|
||||
@import "hashtag";
|
||||
|
@ -0,0 +1,16 @@
|
||||
.form-template-field {
|
||||
// TODO(@keegan): Remove after updating dropdown/multi-select to use select-kit
|
||||
&__dropdown {
|
||||
border: 1px solid var(--primary-medium);
|
||||
border-radius: var(--d-input-border-radius);
|
||||
padding: 0.5em 0.65em;
|
||||
font-size: var(--font-0);
|
||||
}
|
||||
|
||||
&__multi-select {
|
||||
border: 1px solid var(--primary-medium);
|
||||
border-radius: var(--d-input-border-radius);
|
||||
padding: 0.5em 0.65em;
|
||||
font-size: var(--font-0);
|
||||
}
|
||||
}
|
@ -2,6 +2,13 @@
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
max-width: 100%;
|
||||
|
||||
&.has-form-template {
|
||||
.d-editor-preview-wrapper {
|
||||
display: none;
|
||||
flex: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.d-editor {
|
||||
@ -353,7 +360,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
.d-editor .form-template-form__wrapper {
|
||||
.d-editor #form-template-form {
|
||||
overflow: auto;
|
||||
background: var(--primary-very-low);
|
||||
padding: 1rem;
|
||||
|
@ -5641,15 +5641,19 @@ en:
|
||||
minimum:
|
||||
key: "minimum"
|
||||
type: "integer"
|
||||
description: "In text fields, specifies the minimum number of characters allowed."
|
||||
description: "For text fields, specifies the minimum number of characters allowed."
|
||||
maximum:
|
||||
key: "maximum"
|
||||
type: "integer"
|
||||
description: "In text fields, specifies the maximum number of characters allowed. In multi select dropdowns, specifies the maximum number of options that can be selected."
|
||||
description: "For text fields, specifies the maximum number of characters allowed."
|
||||
pattern:
|
||||
key: "pattern"
|
||||
type: "regex string"
|
||||
description: "In text fields, a regular expression specifying the allowed input."
|
||||
description: "For text fields, a regular expression specifying the allowed input."
|
||||
type:
|
||||
key: "type"
|
||||
type: "string"
|
||||
description: "For input fields, you can specify the type of input that should be expected (text|email|date|number|url|tel|color)"
|
||||
preview_modal:
|
||||
title: "Preview Template"
|
||||
field_placeholders:
|
||||
|
@ -82,6 +82,7 @@ module SvgSprite
|
||||
exclamation-circle
|
||||
exclamation-triangle
|
||||
external-link-alt
|
||||
eye
|
||||
fab-android
|
||||
fab-apple
|
||||
fab-chrome
|
||||
|
@ -3,10 +3,20 @@
|
||||
describe "Composer Form Templates", type: :system do
|
||||
fab!(:user) { Fabricate(:user) }
|
||||
fab!(:form_template_1) do
|
||||
Fabricate(:form_template, name: "Bug Reports", template: "- type: checkbox")
|
||||
Fabricate(
|
||||
:form_template,
|
||||
name: "Bug Reports",
|
||||
template:
|
||||
"- type: input
|
||||
attributes:
|
||||
label: What is your full name?
|
||||
placeholder: John Doe
|
||||
validations:
|
||||
required: true",
|
||||
)
|
||||
end
|
||||
fab!(:form_template_2) do
|
||||
Fabricate(:form_template, name: "Feature Request", template: "- type: input")
|
||||
Fabricate(:form_template, name: "Feature Request", template: "- type: checkbox")
|
||||
end
|
||||
fab!(:form_template_3) do
|
||||
Fabricate(:form_template, name: "Awesome Possum", template: "- type: dropdown")
|
||||
@ -66,6 +76,7 @@ describe "Composer Form Templates", type: :system do
|
||||
let(:category_page) { PageObjects::Pages::Category.new }
|
||||
let(:composer) { PageObjects::Components::Composer.new }
|
||||
let(:form_template_chooser) { PageObjects::Components::SelectKit.new(".form-template-chooser") }
|
||||
let(:topic_page) { PageObjects::Pages::Topic.new }
|
||||
|
||||
before do
|
||||
SiteSetting.experimental_form_templates = true
|
||||
@ -90,7 +101,21 @@ describe "Composer Form Templates", type: :system do
|
||||
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("checkbox")
|
||||
expect(composer).to have_form_template_field("input")
|
||||
end
|
||||
|
||||
it "shows the preview when a category without a form template is selected" do
|
||||
category_page.visit(category_no_template)
|
||||
category_page.new_topic_button.click
|
||||
expect(composer).to have_composer_preview
|
||||
expect(composer).to have_composer_preview_toggle
|
||||
end
|
||||
|
||||
it "hides the preivew when a category with a form template is selected" do
|
||||
category_page.visit(category_with_template_1)
|
||||
category_page.new_topic_button.click
|
||||
expect(composer).to have_no_composer_preview
|
||||
expect(composer).to have_no_composer_preview_toggle
|
||||
end
|
||||
|
||||
it "shows the correct template when switching categories" do
|
||||
@ -105,11 +130,11 @@ describe "Composer Form Templates", type: :system do
|
||||
# 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")
|
||||
expect(composer).to have_form_template_field("input")
|
||||
# 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")
|
||||
expect(composer).to have_form_template_field("checkbox")
|
||||
end
|
||||
|
||||
it "does not show form template chooser when a category only has form template" do
|
||||
@ -127,9 +152,9 @@ describe "Composer Form Templates", type: :system do
|
||||
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")
|
||||
form_template_chooser.select_row_by_name(form_template_2.name)
|
||||
expect(composer).to have_form_template_field("checkbox")
|
||||
end
|
||||
|
||||
it "shows the correct template options when switching categories" do
|
||||
@ -152,4 +177,22 @@ describe "Composer Form Templates", type: :system do
|
||||
form_template_chooser.select_row_by_name(form_template_2.name)
|
||||
expect(form_template_chooser).to have_selected_name(form_template_2.name)
|
||||
end
|
||||
|
||||
it "forms a post when template fields are filled in" do
|
||||
topic_title = "A topic about Batman"
|
||||
|
||||
category_page.visit(category_with_template_1)
|
||||
category_page.new_topic_button.click
|
||||
composer.fill_title(topic_title)
|
||||
composer.fill_form_template_field("input", "Bruce Wayne")
|
||||
composer.create
|
||||
topic = Topic.where(user: user, title: topic_title)
|
||||
topic_id = Topic.where(user: user, title: topic_title).pluck(:id)
|
||||
post = Post.where(topic_id: topic_id).first
|
||||
|
||||
expect(topic_page).to have_topic_title(topic_title)
|
||||
expect(find("#{topic_page.post_by_number_selector(1)} .cooked p")).to have_content(
|
||||
"Bruce Wayne",
|
||||
)
|
||||
end
|
||||
end
|
||||
|
105
spec/system/composer/template_validation_spec.rb
Normal file
105
spec/system/composer/template_validation_spec.rb
Normal file
@ -0,0 +1,105 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
describe "Composer Form Template Validations", type: :system, js: true do
|
||||
fab!(:user) { Fabricate(:user) }
|
||||
fab!(:form_template) do
|
||||
Fabricate(
|
||||
:form_template,
|
||||
name: "Bug Reports",
|
||||
template:
|
||||
"- type: input
|
||||
attributes:
|
||||
label: What is your full name?
|
||||
placeholder: John Doe
|
||||
validations:
|
||||
required: true
|
||||
type: email
|
||||
min: 10",
|
||||
)
|
||||
end
|
||||
|
||||
fab!(:form_template_2) do
|
||||
Fabricate(
|
||||
:form_template,
|
||||
name: "Websites",
|
||||
template:
|
||||
"- type: input
|
||||
attributes:
|
||||
label: What is your website name?
|
||||
placeholder: https://www.example.com
|
||||
validations:
|
||||
pattern: https?://.+",
|
||||
)
|
||||
end
|
||||
fab!(:category_with_template) do
|
||||
Fabricate(
|
||||
:category,
|
||||
name: "Reports",
|
||||
slug: "reports",
|
||||
topic_count: 2,
|
||||
form_template_ids: [form_template.id],
|
||||
)
|
||||
end
|
||||
fab!(:category_with_template_2) do
|
||||
Fabricate(
|
||||
:category,
|
||||
name: "Websites",
|
||||
slug: "websites",
|
||||
topic_count: 2,
|
||||
form_template_ids: [form_template_2.id],
|
||||
)
|
||||
end
|
||||
let(:category_page) { PageObjects::Pages::Category.new }
|
||||
let(:composer) { PageObjects::Components::Composer.new }
|
||||
|
||||
let(:error_types) do
|
||||
{
|
||||
required_error: "Please fill out this field.",
|
||||
type_error: "Please include an '@' in the email address. 'Bruce Wayne' is missing an '@'.",
|
||||
min_error:
|
||||
"Please lengthen this text to 10 characters or more (you are currently using 7 characters).",
|
||||
pattern_error: "Please match the requested format.",
|
||||
}
|
||||
end
|
||||
let(:topic_title) { "A topic about Batman" }
|
||||
|
||||
before do
|
||||
SiteSetting.experimental_form_templates = true
|
||||
sign_in user
|
||||
end
|
||||
|
||||
it "shows an error when a required input is not filled in" do
|
||||
category_page.visit(category_with_template)
|
||||
category_page.new_topic_button.click
|
||||
composer.fill_title(topic_title)
|
||||
composer.create
|
||||
expect(composer).to have_form_template_field_error(error_types[:required_error])
|
||||
end
|
||||
|
||||
it "shows an error when an input filled doesn't satisfy the type expected" do
|
||||
category_page.visit(category_with_template)
|
||||
category_page.new_topic_button.click
|
||||
composer.fill_title(topic_title)
|
||||
composer.create
|
||||
composer.fill_form_template_field("input", "Bruce Wayne")
|
||||
expect(composer).to have_form_template_field_error(error_types[:type_error])
|
||||
end
|
||||
|
||||
it "shows an error when an input doesn't satisfy the min length expected" do
|
||||
category_page.visit(category_with_template)
|
||||
category_page.new_topic_button.click
|
||||
composer.fill_title(topic_title)
|
||||
composer.create
|
||||
composer.fill_form_template_field("input", "b@b.com")
|
||||
expect(composer).to have_form_template_field_error(error_types[:min_error])
|
||||
end
|
||||
|
||||
it "shows an error when an input doesn't satisfy the requested pattern" do
|
||||
category_page.visit(category_with_template_2)
|
||||
category_page.new_topic_button.click
|
||||
composer.fill_title(topic_title)
|
||||
composer.fill_form_template_field("input", "www.example.com")
|
||||
composer.create
|
||||
expect(composer).to have_form_template_field_error(error_types[:pattern_error])
|
||||
end
|
||||
end
|
@ -30,6 +30,11 @@ module PageObjects
|
||||
self
|
||||
end
|
||||
|
||||
def fill_form_template_field(field, content)
|
||||
form_template_field(field).fill_in(with: content)
|
||||
self
|
||||
end
|
||||
|
||||
def type_content(content)
|
||||
composer_input.send_keys(content)
|
||||
self
|
||||
@ -113,6 +118,22 @@ module PageObjects
|
||||
page.has_css?(COMPOSER_INPUT_SELECTOR)
|
||||
end
|
||||
|
||||
def has_composer_preview?
|
||||
page.has_css?("#{COMPOSER_ID} .d-editor-preview-wrapper")
|
||||
end
|
||||
|
||||
def has_no_composer_preview?
|
||||
page.has_no_css?("#{COMPOSER_ID} .d-editor-preview-wrapper")
|
||||
end
|
||||
|
||||
def has_composer_preview_toggle?
|
||||
page.has_css?("#{COMPOSER_ID} .toggle-preview")
|
||||
end
|
||||
|
||||
def has_no_composer_preview_toggle?
|
||||
page.has_no_css?("#{COMPOSER_ID} .toggle-preview")
|
||||
end
|
||||
|
||||
def has_form_template?
|
||||
page.has_css?(".form-template-form__wrapper")
|
||||
end
|
||||
@ -131,6 +152,10 @@ module PageObjects
|
||||
page.has_css?(FORM_TEMPLATE_CHOOSER_SELECTOR)
|
||||
end
|
||||
|
||||
def has_form_template_field_error?(error)
|
||||
page.has_css?(".form-template-field__error", text: error)
|
||||
end
|
||||
|
||||
def composer_input
|
||||
find("#{COMPOSER_ID} .d-editor .d-editor-input")
|
||||
end
|
||||
@ -139,6 +164,10 @@ module PageObjects
|
||||
find("#{COMPOSER_ID} .composer-popup")
|
||||
end
|
||||
|
||||
def form_template_field(field)
|
||||
find(".form-template-field[data-field-type='#{field}']")
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def emoji_preview_selector(emoji)
|
||||
|
Loading…
Reference in New Issue
Block a user