FEATURE: support to initial values for form templates through /new-topic (#23313)

* FEATURE: adds support for initial values through /new-topic to form templates
This commit is contained in:
Renato Atilio 2023-08-29 18:41:33 -03:00 committed by GitHub
parent 8dddc9eb39
commit 58b49bce41
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 178 additions and 45 deletions

View File

@ -4,6 +4,7 @@ export const templateFormFields = [
{
type: "checkbox",
structure: `- type: checkbox
id: ${I18n.t("admin.form_templates.field_placeholders.id")}
attributes:
label: "${I18n.t("admin.form_templates.field_placeholders.label")}"
validations:
@ -12,6 +13,7 @@ export const templateFormFields = [
{
type: "input",
structure: `- type: input
id: ${I18n.t("admin.form_templates.field_placeholders.id")}
attributes:
label: "${I18n.t("admin.form_templates.field_placeholders.label")}"
placeholder: "${I18n.t(
@ -23,6 +25,7 @@ export const templateFormFields = [
{
type: "textarea",
structure: `- type: textarea
id: ${I18n.t("admin.form_templates.field_placeholders.id")}
attributes:
label: "${I18n.t("admin.form_templates.field_placeholders.label")}"
placeholder: "${I18n.t(
@ -34,6 +37,7 @@ export const templateFormFields = [
{
type: "dropdown",
structure: `- type: dropdown
id: ${I18n.t("admin.form_templates.field_placeholders.id")}
choices:
- "${I18n.t("admin.form_templates.field_placeholders.choices.first")}"
- "${I18n.t("admin.form_templates.field_placeholders.choices.second")}"
@ -50,6 +54,7 @@ export const templateFormFields = [
{
type: "upload",
structure: `- type: upload
id: ${I18n.t("admin.form_templates.field_placeholders.id")}
attributes:
file_types: ".jpg, .png, .gif"
allow_multiple: false
@ -60,6 +65,7 @@ export const templateFormFields = [
{
type: "multiselect",
structure: `- type: multi-select
id: ${I18n.t("admin.form_templates.field_placeholders.id")}
choices:
- "${I18n.t("admin.form_templates.field_placeholders.choices.first")}"
- "${I18n.t("admin.form_templates.field_placeholders.choices.second")}"

View File

@ -125,6 +125,7 @@
@focusTarget={{this.composer.focusTarget}}
@disableTextarea={{this.composer.disableTextarea}}
@formTemplateIds={{this.composer.formTemplateIds}}
@formTemplateInitialValues={{this.composer.formTemplateInitialValues}}
>
<div class="composer-fields">
<PluginOutlet

View File

@ -17,6 +17,7 @@
@onPopupMenuAction={{this.onPopupMenuAction}}
@popupMenuOptions={{this.popupMenuOptions}}
@formTemplateIds={{this.formTemplateIds}}
@formTemplateInitialValues={{@formTemplateInitialValues}}
@replyingToTopic={{this.composer.replyingToTopic}}
@editingPost={{this.composer.editingPost}}
@disabled={{this.disableTextarea}}

View File

@ -15,7 +15,10 @@
/>
{{/if}}
<form id="form-template-form">
<FormTemplateField::Wrapper @id={{this.selectedFormTemplateId}} />
<FormTemplateField::Wrapper
@id={{this.selectedFormTemplateId}}
@initialValues={{@formTemplateInitialValues}}
/>
</form>
{{else}}
<div

View File

@ -1,8 +1,9 @@
<div class="control-group form-template-field" data-field-type="checkbox">
<label class="form-template-field__label">
<Input
name={{@attributes.label}}
name={{@id}}
class="form-template-field__checkbox"
@checked={{@value}}
@type="checkbox"
required={{if @validations.required "required" ""}}
/>

View File

@ -11,7 +11,7 @@
{{! 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}}
name={{@id}}
class="form-template-field__dropdown"
required={{if @validations.required "required" ""}}
>
@ -25,7 +25,7 @@
>{{@attributes.none_label}}</option>
{{/if}}
{{#each @choices as |choice|}}
<option value={{choice}}>{{choice}}</option>
<option value={{choice}} selected={{eq @value choice}}>{{choice}}</option>
{{/each}}
</select>
</div>

View File

@ -9,8 +9,9 @@
{{/if}}
<Input
name={{@attributes.label}}
name={{@id}}
class="form-template-field__input"
@value={{@value}}
@type={{if @validations.type @validations.type "text"}}
placeholder={{@attributes.placeholder}}
required={{if @validations.required "required" ""}}

View File

@ -11,7 +11,7 @@
{{! 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}}
name={{@id}}
class="form-template-field__multi-select"
required={{if @validations.required "required" ""}}
multiple="multiple"
@ -25,7 +25,10 @@
>{{@attributes.none_label}}</option>
{{/if}}
{{#each @choices as |choice|}}
<option value={{choice}}>{{choice}}</option>
<option
value={{choice}}
selected={{this.isSelected choice}}
>{{choice}}</option>
{{/each}}
</select>
</div>

View File

@ -0,0 +1,9 @@
import Component from "@glimmer/component";
import { action } from "@ember/object";
export default class FormTemplateFieldMultiSelect extends Component {
@action
isSelected(option) {
return this.args.value?.includes(option);
}
}

View File

@ -8,7 +8,8 @@
</label>
{{/if}}
<Textarea
name={{@attributes.label}}
name={{@id}}
@value={{@value}}
class="form-template-field__textarea"
placeholder={{@attributes.placeholder}}
pattern={{@validations.pattern}}

View File

@ -8,7 +8,7 @@
</label>
{{/if}}
<input type="hidden" name={{@attributes.label}} value={{this.uploadValue}} />
<input type="hidden" name={{@id}} value={{this.uploadValue}} />
<PickFilesButton
@fileInputClass="form-template-field__upload"

View File

@ -12,12 +12,8 @@ export default class FormTemplateFieldUpload extends Component.extend(
@tracked uploadComplete = false;
@tracked uploadedFiles = [];
@tracked disabled = this.uploading;
@tracked
fileUploadElementId = this.attributes?.label
? `${dasherize(this.attributes.label)}-uploader`
: `${this.elementId}-uploader`;
@tracked fileUploadElementId = `${dasherize(this.id)}-uploader`;
@tracked fileInputSelector = `#${this.fileUploadElementId}`;
@tracked id = this.fileUploadElementId;
@computed("uploading", "uploadValue")
get uploadStatus() {

View File

@ -6,9 +6,11 @@
{{#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>

View File

@ -54,6 +54,8 @@ export default class extends DiscourseRoute {
category,
tags: transition.to.queryParams.tags,
});
this.composer.set("formTemplateInitialValues", transition.to.queryParams);
});
}

View File

@ -174,6 +174,14 @@ export default class ComposerService extends Service {
return this.model.category?.get("form_template_ids");
}
get formTemplateInitialValues() {
return this._formTemplateInitialValues;
}
set formTemplateInitialValues(values) {
return this.set("_formTemplateInitialValues", values);
}
@discourseComputed("showPreview")
toggleText(showPreview) {
return showPreview
@ -246,7 +254,6 @@ export default class ComposerService extends Service {
canEditTags(canEditTitle, creatingPrivateMessage) {
const isPrivateMessage =
creatingPrivateMessage || this.get("model.topic.isPrivateMessage");
return (
canEditTitle &&
this.site.can_tag_topics &&

View File

@ -2,7 +2,7 @@ import { module, test } from "qunit";
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 { exists, query } from "discourse/tests/helpers/qunit-helpers";
import pretender, { response } from "discourse/tests/helpers/create-pretender";
module(
@ -24,7 +24,12 @@ module(
});
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 content = `- type: checkbox\n id: checkbox\n
- type: input\n id: name
- type: textarea\n id: notes
- type: dropdown\n id: dropdown
- type: upload\n id: upload
- type: multi-select\n id: multi`;
const componentTypes = [
"checkbox",
"input",
@ -47,6 +52,36 @@ module(
});
});
test("renders a component based on the component type found in the content YAML, with initial values", async function (assert) {
const content = `- type: checkbox\n id: checkbox\n
- type: input\n id: name
- type: textarea\n id: notes
- type: dropdown\n id: dropdown\n choices:\n - "Option 1"\n - "Option 2"\n - "Option 3"
- type: multi-select\n id: multi\n choices:\n - "Option 1"\n - "Option 2"\n - "Option 3"`;
this.set("content", content);
const initialValues = {
checkbox: "on",
name: "Test Name",
notes: "Test Notes",
dropdown: "Option 1",
multi: ["Option 1"],
};
this.set("initialValues", initialValues);
await render(
hbs`<FormTemplateField::Wrapper @content={{this.content}} @initialValues={{this.initialValues}} />`
);
Object.keys(initialValues).forEach((componentId) => {
assert.equal(
query(`[name='${componentId}']`).value,
initialValues[componentId],
`${componentId} component has initial value`
);
});
});
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({

View File

@ -16,6 +16,9 @@ class FormTemplate < ActiveRecord::Base
has_many :category_form_templates, dependent: :destroy
has_many :categories, through: :category_form_templates
class NotAllowed < StandardError
end
end
# == Schema Information

View File

@ -5750,6 +5750,7 @@ en:
title: "Preview Template"
field_placeholders:
validations: "enter validations here"
id: "enter-id-here"
label: "Enter label here"
placeholder: "Enter placeholder here"
none_label: "Select an item"

View File

@ -5291,3 +5291,6 @@ en:
invalid_yaml: "is not a valid YAML string"
invalid_type: "contains an invalid template type: %{type} (valid types are: %{valid_types})"
missing_type: "is missing a field type"
missing_id: "is missing a field id"
duplicate_ids: "has duplicate ids"
reserved_id: "has a reserved keyword as id: %{id}"

View File

@ -1123,7 +1123,7 @@ posting:
min: 5
max: 255
max_form_template_content_length:
default: 2000
default: 5000
max: 150000
email:

View File

@ -1,27 +1,30 @@
# frozen_string_literal: true
class FormTemplateYamlValidator < ActiveModel::Validator
RESERVED_KEYWORDS = %w[title body category category_id tags]
ALLOWED_TYPES = %w[checkbox dropdown input multi-select textarea upload]
def validate(record)
begin
yaml = Psych.safe_load(record.template)
check_missing_type(record, yaml)
check_missing_fields(record, yaml)
check_allowed_types(record, yaml)
check_ids(record, yaml)
rescue Psych::SyntaxError
record.errors.add(:template, I18n.t("form_templates.errors.invalid_yaml"))
end
end
def check_allowed_types(record, yaml)
allowed_types = %w[checkbox dropdown input multi-select textarea upload]
yaml.each do |field|
if !allowed_types.include?(field["type"])
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(", "),
valid_types: ALLOWED_TYPES.join(", "),
),
)
)
@ -29,11 +32,33 @@ class FormTemplateYamlValidator < ActiveModel::Validator
end
end
def check_missing_type(record, yaml)
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"))
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
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"]
end
end
end

View File

@ -2,5 +2,6 @@
Fabricator(:form_template) do
name { sequence(:name) { |i| "template_#{i}" } }
template "- type: input"
template "- type: input
id: name"
end

View File

@ -4,11 +4,12 @@ require "rails_helper"
RSpec.describe FormTemplate, type: :model do
it "can't have duplicate names" do
Fabricate(:form_template, name: "Bug Report", template: "- type: input")
t = Fabricate.build(:form_template, name: "Bug Report", template: "- type: input")
Fabricate(:form_template, name: "Bug Report", template: "- type: input\n id: name")
t = Fabricate.build(:form_template, name: "Bug Report", template: "- type: input\n id: name")
expect(t.save).to eq(false)
t = Fabricate.build(:form_template, name: "Bug Report", template: "- type: input")
t = Fabricate.build(:form_template, name: "Bug Report", template: "- type: input\n id: name")
expect(t.save).to eq(false)
expect(t.errors.full_messages.first).to include(I18n.t("errors.messages.taken"))
expect(described_class.count).to eq(1)
end
@ -16,17 +17,33 @@ RSpec.describe FormTemplate, type: :model do
template = "- type: checkbox\nattributes; bad"
t = Fabricate.build(:form_template, name: "Feature Request", template: template)
expect(t.save).to eq(false)
expect(t.errors.full_messages.first).to include(I18n.t("form_templates.errors.invalid_yaml"))
end
it "must have a supported type" do
template = "- type: fancy"
template = "- type: fancy\n id: something"
t = Fabricate.build(:form_template, name: "Fancy Template", template: template)
expect(t.save).to eq(false)
expect(t.errors.full_messages.first).to include(
I18n.t(
"form_templates.errors.invalid_type",
type: "fancy",
valid_types: FormTemplateYamlValidator::ALLOWED_TYPES.join(", "),
),
)
end
it "must have a type property" do
template = "- hello: world"
template = "- hello: world\n id: something"
t = Fabricate.build(:form_template, name: "Basic Template", template: template)
expect(t.save).to eq(false)
expect(t.errors.full_messages.first).to include(I18n.t("form_templates.errors.missing_type"))
end
it "must have a id property" do
template = "- type: checkbox"
t = Fabricate.build(:form_template, name: "Basic Template", template: template)
expect(t.save).to eq(false)
expect(t.errors.full_messages.first).to include(I18n.t("form_templates.errors.missing_id"))
end
end

View File

@ -74,7 +74,7 @@ RSpec.describe Admin::FormTemplatesController do
params: {
name: "Bug Reports",
template:
"- type: input\n attributes:\n label: Website or apps\n description: |\n Which website or app were you using when the bug happened?\n placeholder: |\n e.g. website URL, name of the app\n validations:\n required: true",
"- type: input\n id: website\n attributes:\n label: Website or apps\n description: |\n Which website or app were you using when the bug happened?\n placeholder: |\n e.g. website URL, name of the app\n validations:\n required: true",
}
expect(response.status).to eq(200)
@ -112,13 +112,13 @@ RSpec.describe Admin::FormTemplatesController do
params: {
id: form_template.id,
name: "Updated Template",
template: "- type: checkbox",
template: "- type: checkbox\n id: checkbox",
}
expect(response.status).to eq(200)
form_template.reload
expect(form_template.name).to eq("Updated Template")
expect(form_template.template).to eq("- type: checkbox")
expect(form_template.template).to eq("- type: checkbox\n id: checkbox")
end
end

View File

@ -66,7 +66,7 @@ describe "Admin Customize Form Templates", type: :system do
form_template_page.visit_new
sample_name = "My First Template"
sample_template = "- type: input"
sample_template = "- type: input\n id: name"
form_template_page.type_in_template_name(sample_name)
ace_editor.type_input(sample_template)
@ -101,7 +101,7 @@ describe "Admin Customize Form Templates", type: :system do
it "should show a preview of the template in a modal when clicking the preview button" do
form_template_page.visit_new
form_template_page.type_in_template_name("New Template")
ace_editor.type_input("- type: input")
ace_editor.type_input("- type: input\n id: name")
form_template_page.click_preview_button
expect(form_template_page).to have_preview_modal
@ -112,7 +112,12 @@ describe "Admin Customize Form Templates", type: :system do
form_template_page.visit_new
form_template_page.type_in_template_name("New Template")
ace_editor.type_input(
"- type: input\n- type: textarea\n- type: checkbox\n- type: dropdown\n- type: upload\n- type: multi-select",
"- type: input\n id: name
\b\b- type: textarea\n id: description
\b\b- type: checkbox\n id: checkbox
\b\b- type: dropdown\n id: dropdown
\b\b- type: upload\n id: upload
\b\b- type: multi-select\n id: multi-select",
)
form_template_page.click_preview_button
expect(form_template_page).to have_input_field("input")
@ -127,6 +132,7 @@ describe "Admin Customize Form Templates", type: :system do
quick_insertion_test(
"checkbox",
'- type: checkbox
id: enter-id-here
attributes:
label: "Enter label here"
validations:
@ -138,6 +144,7 @@ describe "Admin Customize Form Templates", type: :system do
quick_insertion_test(
"input",
'- type: input
id: enter-id-here
attributes:
label: "Enter label here"
placeholder: "Enter placeholder here"
@ -150,6 +157,7 @@ describe "Admin Customize Form Templates", type: :system do
quick_insertion_test(
"textarea",
'- type: textarea
id: enter-id-here
attributes:
label: "Enter label here"
placeholder: "Enter placeholder here"
@ -162,6 +170,7 @@ describe "Admin Customize Form Templates", type: :system do
quick_insertion_test(
"dropdown",
'- type: dropdown
id: enter-id-here
choices:
- "Option 1"
- "Option 2"
@ -179,6 +188,7 @@ describe "Admin Customize Form Templates", type: :system do
quick_insertion_test(
"upload",
'- type: upload
id: enter-id-here
attributes:
file_types: ".jpg, .png, .gif"
allow_multiple: false
@ -192,6 +202,7 @@ describe "Admin Customize Form Templates", type: :system do
quick_insertion_test(
"multiselect",
'- type: multi-select
id: enter-id-here
choices:
- "Option 1"
- "Option 2"

View File

@ -8,6 +8,7 @@ describe "Composer Form Templates", type: :system do
name: "Bug Reports",
template:
"- type: input
id: full-name
attributes:
label: What is your full name?
placeholder: John Doe
@ -16,13 +17,13 @@ describe "Composer Form Templates", type: :system do
)
end
fab!(:form_template_2) do
Fabricate(:form_template, name: "Feature Request", template: "- type: checkbox")
Fabricate(:form_template, name: "Feature Request", template: "- type: checkbox\n id: check")
end
fab!(:form_template_3) do
Fabricate(:form_template, name: "Awesome Possum", template: "- type: dropdown")
Fabricate(:form_template, name: "Awesome Possum", template: "- type: dropdown\n id: dropdown")
end
fab!(:form_template_4) do
Fabricate(:form_template, name: "Biography", template: "- type: textarea")
Fabricate(:form_template, name: "Biography", template: "- type: textarea\n id: bio")
end
fab!(:form_template_5) do
Fabricate(
@ -31,12 +32,14 @@ describe "Composer Form Templates", type: :system do
template:
%Q(
- type: input
id: full-name
attributes:
label: "What is your name?"
placeholder: "John Smith"
validations:
required: false
- type: upload
id: prescription
attributes:
file_types: ".jpg, .png"
allow_multiple: false
@ -44,6 +47,7 @@ describe "Composer Form Templates", type: :system do
validations:
required: true
- type: upload
id: additional-docs
attributes:
file_types: ".jpg, .png, .pdf, .mp3, .mp4"
allow_multiple: true
@ -240,7 +244,7 @@ describe "Composer Form Templates", type: :system do
category_page.visit(category_with_upload_template)
category_page.new_topic_button.click
attach_file "upload-your-prescription-uploader",
attach_file "prescription-uploader",
"#{Rails.root}/spec/fixtures/images/logo.png",
make_visible: true
composer.fill_title(topic_title)
@ -253,11 +257,9 @@ describe "Composer Form Templates", type: :system do
end
it "doesn't allow uploading an invalid file type" do
topic_title = "Bruce Wayne's Medication"
category_page.visit(category_with_upload_template)
category_page.new_topic_button.click
attach_file "upload-your-prescription-uploader",
attach_file "prescription-uploader",
"#{Rails.root}/spec/fixtures/images/animated.gif",
make_visible: true
expect(find("#dialog-holder .dialog-body p", visible: :all)).to have_content(
@ -270,10 +272,10 @@ describe "Composer Form Templates", type: :system do
category_page.visit(category_with_upload_template)
category_page.new_topic_button.click
attach_file "upload-your-prescription-uploader",
attach_file "prescription-uploader",
"#{Rails.root}/spec/fixtures/images/logo.png",
make_visible: true
attach_file "any-additional-docs-uploader",
attach_file "additional-docs-uploader",
[
"#{Rails.root}/spec/fixtures/media/small.mp3",
"#{Rails.root}/spec/fixtures/media/small.mp4",

View File

@ -8,6 +8,7 @@ describe "Composer Form Template Validations", type: :system, js: true do
name: "Bug Reports",
template:
"- type: input
id: full-name
attributes:
label: What is your full name?
placeholder: John Doe
@ -24,6 +25,7 @@ describe "Composer Form Template Validations", type: :system, js: true do
name: "Websites",
template:
"- type: input
id: website-name
attributes:
label: What is your website name?
placeholder: https://www.example.com