Skip to content

Commit

Permalink
FEATURE: support to initial values for form templates through /new-to…
Browse files Browse the repository at this point in the history
…pic (#23313)

* FEATURE: adds support for initial values through /new-topic to form templates
  • Loading branch information
renato committed Aug 29, 2023
1 parent 8dddc9e commit 58b49bc
Show file tree
Hide file tree
Showing 27 changed files with 178 additions and 45 deletions.
Expand Up @@ -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:
Expand All @@ -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(
Expand All @@ -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(
Expand All @@ -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")}"
Expand All @@ -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
Expand All @@ -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")}"
Expand Down
Expand Up @@ -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
Expand Down
Expand Up @@ -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}}
Expand Down
5 changes: 4 additions & 1 deletion app/assets/javascripts/discourse/app/components/d-editor.hbs
Expand Up @@ -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
Expand Down
@@ -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" ""}}
/>
Expand Down
Expand Up @@ -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" ""}}
>
Expand All @@ -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>
Expand Up @@ -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" ""}}
Expand Down
Expand Up @@ -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"
Expand All @@ -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>
@@ -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);
}
}
Expand Up @@ -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}}
Expand Down
Expand Up @@ -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"
Expand Down
Expand Up @@ -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() {
Expand Down
Expand Up @@ -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>
Expand Down
2 changes: 2 additions & 0 deletions app/assets/javascripts/discourse/app/routes/new-topic.js
Expand Up @@ -54,6 +54,8 @@ export default class extends DiscourseRoute {
category,
tags: transition.to.queryParams.tags,
});

this.composer.set("formTemplateInitialValues", transition.to.queryParams);
});
}

Expand Down
9 changes: 8 additions & 1 deletion app/assets/javascripts/discourse/app/services/composer.js
Expand Up @@ -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
Expand Down Expand Up @@ -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 &&
Expand Down
Expand Up @@ -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(
Expand All @@ -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",
Expand All @@ -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({
Expand Down
3 changes: 3 additions & 0 deletions app/models/form_template.rb
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions config/locales/client.en.yml
Expand Up @@ -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"
Expand Down
3 changes: 3 additions & 0 deletions config/locales/server.en.yml
Expand Up @@ -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}"
2 changes: 1 addition & 1 deletion config/site_settings.yml
Expand Up @@ -1123,7 +1123,7 @@ posting:
min: 5
max: 255
max_form_template_content_length:
default: 2000
default: 5000
max: 150000

email:
Expand Down
37 changes: 31 additions & 6 deletions lib/validators/form_template_yaml_validator.rb
@@ -1,39 +1,64 @@
# 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(", "),
),
)
)
end
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
3 changes: 2 additions & 1 deletion spec/fabricators/form_template_fabricator.rb
Expand Up @@ -2,5 +2,6 @@

Fabricator(:form_template) do
name { sequence(:name) { |i| "template_#{i}" } }
template "- type: input"
template "- type: input
id: name"
end

0 comments on commit 58b49bc

Please sign in to comment.