FEATURE: add composer form template field type (#31584)

Adds a "composer" form template field type.
This commit is contained in:
Renato Atilio
2026-01-20 15:48:50 -03:00
committed by GitHub
parent b4237309c6
commit c8de2f68b0
11 changed files with 87 additions and 16 deletions
@@ -69,4 +69,14 @@
font-size: var(--font-down-1);
color: var(--primary-medium);
}
&__composer {
// same default effective height (150px) than a text control
height: 189px;
width: 100%;
.d-editor-preview-wrapper {
display: none;
}
}
}
+1
View File
@@ -7930,6 +7930,7 @@ en:
upload: "Upload a file"
multiselect: "Multiple choice"
tagchooser: "Tag Group selector"
composer: "Markdown answer"
validations_modal:
button_title: "Validations"
modal_title: "Validation Options"
@@ -54,6 +54,10 @@ export default class FormTemplateForm extends Component {
type: "tagchooser",
icon: "bullseye",
},
{
type: "composer",
icon: "fab-markdown",
},
];
get disablePreviewButton() {
@@ -85,4 +85,11 @@ export const templateFormFields = [
validations:
# ${i18n("admin.form_templates.field_placeholders.validations")}`,
},
{
type: "composer",
structure: `- type: composer
id: ${i18n("admin.form_templates.field_placeholders.id")}
attributes:
label: "${i18n("admin.form_templates.field_placeholders.label")}"`,
},
];
@@ -158,10 +158,10 @@ export default class ComposerEditor extends Component {
placeholder = `composer.${key}`;
}
return applyValueTransformer(
"composer-editor-reply-placeholder",
placeholder,
{ model: this.composer }
return i18n(
applyValueTransformer("composer-editor-reply-placeholder", placeholder, {
model: this.composer,
})
);
}
@@ -154,14 +154,6 @@ export default class DEditor extends Component {
});
}
@discourseComputed("placeholder")
placeholderTranslated(placeholder) {
if (placeholder) {
return i18n(placeholder);
}
return null;
}
@discourseComputed("siteSettings.rich_editor", "forceEditorMode")
showEditorModeToggle() {
return this.siteSettings.rich_editor && isNone(this.forceEditorMode);
@@ -833,7 +825,7 @@ export default class DEditor extends Component {
@markdownOptions={{this.markdownOptions}}
@keymap={{this.keymap}}
@value={{this.value}}
@placeholder={{this.placeholderTranslated}}
@placeholder={{@placeholder}}
@disabled={{this.disabled}}
@change={{this.onChange}}
@focusIn={{this.handleFocusIn}}
@@ -0,0 +1,45 @@
import Component from "@glimmer/component";
import { tracked } from "@glimmer/tracking";
import { action } from "@ember/object";
import { next } from "@ember/runloop";
import { htmlSafe } from "@ember/template";
import DEditor from "discourse/components/d-editor";
import icon from "discourse/helpers/d-icon";
export default class FormTemplateFieldComposer extends Component {
@tracked composerValue = this.args.value || "";
@action
handleInput(event) {
this.composerValue = event.target.value;
next(this, () => {
this.args.onChange?.(event);
});
}
<template>
<div class="control-group form-template-field" data-field-type="composer">
{{#if @attributes.label}}
<label class="form-template-field__label">
{{@attributes.label}}
{{#if @validations.required}}
{{icon "asterisk" class="form-template-field__required-indicator"}}
{{/if}}
</label>
{{/if}}
{{#if @attributes.description}}
<span class="form-template-field__description">
{{htmlSafe @attributes.description}}
</span>
{{/if}}
<input type="hidden" name={{@id}} value={{this.composerValue}} />
<DEditor
class="form-template-field__composer"
@value={{this.composerValue}}
@change={{this.handleInput}}
@placeholder={{@attributes.placeholder}}
/>
</div>
</template>
}
@@ -7,6 +7,7 @@ import { service } from "@ember/service";
import Yaml from "js-yaml";
import FormTemplate from "discourse/models/form-template";
import CheckboxField from "./checkbox";
import ComposerField from "./composer";
import DropdownField from "./dropdown";
import InputField from "./input";
import MultiSelectField from "./multi-select";
@@ -42,6 +43,7 @@ export default class FormTemplateFieldWrapper extends Component {
textarea: TextareaField,
"tag-chooser": TagChooserField,
upload: UploadField,
composer: ComposerField,
};
constructor() {
@@ -161,7 +161,7 @@ export default class PostTranslationEditor extends Component {
class="translation-editor"
@value={{readonly this.composer.model.reply}}
@change={{this.handleInput}}
@placeholder="composer.translations.placeholder"
@placeholder={{i18n "composer.translations.placeholder"}}
@extraButtons={{@extraButtons}}
@forcePreview={{true}}
@processPreview={{false}}
@@ -2,7 +2,7 @@
class FormTemplateYamlValidator < ActiveModel::Validator
RESERVED_KEYWORDS = %w[title body category category_id tags]
ALLOWED_TYPES = %w[checkbox dropdown input multi-select textarea upload tag-chooser]
ALLOWED_TYPES = %w[checkbox dropdown input multi-select textarea upload tag-chooser composer]
HTML_SANITIZATION_OPTIONS = {
elements: ["a"],
attributes: {
@@ -126,6 +126,11 @@ describe "Composer Form Templates", type: :system do
attributes:
none_label: "Select an item"
label: "multi-select"
- type: composer
id: 7
attributes:
label: "composer"
placeholder: "Enter rich text here"
),
)
end
@@ -538,12 +543,17 @@ describe "Composer Form Templates", type: :system do
expect(page).to have_css(".d-editor-preview", text: "Option 4")
message = "This is a test message!"
find("textarea").fill_in(with: message)
composer.fill_form_template_field("textarea", message)
expect(page).to have_css(".d-editor-preview", text: message)
attach_file("5-uploader", "#{Rails.root}/spec/fixtures/images/logo.png", make_visible: true)
expect(page).to have_css(".d-editor-preview img")
composer_message = "This is **bold** composer content"
composer.fill_form_template_field("composer", composer_message)
expect(page).to have_css(".d-editor-preview", text: "This is bold composer content")
end
context "when using tagchooser" do