DEV: Create form templates (#20189)

This commit is contained in:
Keegan George 2023-02-08 11:21:39 -08:00 committed by GitHub
parent 7622dbcebf
commit 871607a420
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
34 changed files with 1148 additions and 0 deletions

View File

@ -0,0 +1,57 @@
<div class="form-templates--form">
<div class="control-group">
<label for="template-name">
{{i18n "admin.form_templates.new_template_form.name.label"}}
</label>
<TextField
@value={{this.templateName}}
@name="template-name"
@class="form-templates--form-name-input"
@placeholderKey="admin.form_templates.new_template_form.name.placeholder"
/>
</div>
<div class="control-group form-templates--quick-insert-field-buttons">
<span>
{{I18n "admin.form_templates.quick_insert_fields.add_new_field"}}
</span>
{{#each this.quickInsertFields as |field|}}
<DButton
@class="btn-flat btn-icon-text quick-insert-{{field.type}}"
@icon={{field.icon}}
@label="admin.form_templates.quick_insert_fields.{{field.type}}"
@action={{this.onInsertField}}
@actionParam={{field.type}}
/>
{{/each}}
</div>
<div class="control-group">
<AceEditor @content={{this.templateContent}} @mode="yaml" />
</div>
<div class="footer-buttons">
<DButton
@class="btn-primary"
@label="admin.form_templates.new_template_form.submit"
@icon="check"
@action={{this.onSubmit}}
@disabled={{this.formSubmitted}}
/>
<DButton
@label="admin.form_templates.new_template_form.cancel"
@icon="times"
@action={{this.onCancel}}
/>
{{#if this.isEditing}}
<DButton
@class="btn-danger"
@label="admin.form_templates.view_template.delete"
@icon="trash-alt"
@action={{this.onDelete}}
/>
{{/if}}
</div>
</div>

View File

@ -0,0 +1,109 @@
import Component from "@glimmer/component";
import { action } from "@ember/object";
import { inject as service } from "@ember/service";
import { tracked } from "@glimmer/tracking";
import I18n from "I18n";
import { popupAjaxError } from "discourse/lib/ajax-error";
import { templateFormFields } from "admin/lib/template-form-fields";
import FormTemplate from "admin/models/form-template";
export default class FormTemplateForm extends Component {
@service router;
@service dialog;
@tracked formSubmitted = false;
@tracked templateContent = this.args.model?.template || "";
isEditing = this.args.model?.id ? true : false;
templateName = this.args.model?.name;
quickInsertFields = [
{
type: "checkbox",
icon: "check-square",
},
{
type: "input",
icon: "grip-lines",
},
{
type: "textarea",
icon: "align-left",
},
{
type: "dropdown",
icon: "chevron-circle-down",
},
{
type: "upload",
icon: "cloud-upload-alt",
},
{
type: "multiselect",
icon: "bullseye",
},
];
@action
onSubmit() {
if (!this.formSubmitted) {
this.formSubmitted = true;
}
const postData = {
name: this.templateName,
template: this.templateContent,
};
if (this.isEditing) {
postData["id"] = this.args.model.id;
FormTemplate.updateTemplate(this.args.model.id, postData)
.then(() => {
this.formSubmitted = false;
this.router.transitionTo("adminCustomizeFormTemplates.index");
})
.catch((e) => {
popupAjaxError(e);
this.formSubmitted = false;
});
} else {
FormTemplate.createTemplate(postData)
.then(() => {
this.formSubmitted = false;
this.router.transitionTo("adminCustomizeFormTemplates.index");
})
.catch((e) => {
popupAjaxError(e);
this.formSubmitted = false;
});
}
}
@action
onCancel() {
this.router.transitionTo("adminCustomizeFormTemplates.index");
}
@action
onDelete() {
return this.dialog.yesNoConfirm({
message: I18n.t("admin.form_templates.delete_confirm"),
didConfirm: () => {
FormTemplate.deleteTemplate(this.args.model.id)
.then(() => {
this.router.transitionTo("adminCustomizeFormTemplates.index");
})
.catch(popupAjaxError);
},
});
}
@action
onInsertField(type) {
const structure = templateFormFields.findBy("type", type).structure;
if (this.templateContent.length === 0) {
this.templateContent += structure;
} else {
this.templateContent += `\n${structure}`;
}
}
}

View File

@ -0,0 +1,4 @@
<div class="form-templates--info">
<h2>{{i18n "admin.form_templates.title"}}</h2>
<p class="desc">{{i18n "admin.form_templates.help"}}</p>
</div>

View File

@ -0,0 +1,23 @@
<tr class="admin-list-item">
<td class="col first">{{@template.name}}</td>
<td class="col action">
<DButton
@title="admin.form_templates.list_table.actions.view"
@icon="far-eye"
@class="btn-view-template"
@action={{this.viewTemplate}}
/>
<DButton
@title="admin.form_templates.list_table.actions.edit"
@icon="pencil-alt"
@class="btn-edit-template"
@action={{this.editTemplate}}
/>
<DButton
@title="admin.form_templates.list_table.actions.delete"
@icon="far-trash-alt"
@class="btn-danger btn-delete-template"
@action={{this.deleteTemplate}}
/>
</td>
</tr>

View File

@ -0,0 +1,47 @@
import Component from "@glimmer/component";
import { action } from "@ember/object";
import showModal from "discourse/lib/show-modal";
import { inject as service } from "@ember/service";
import { ajax } from "discourse/lib/ajax";
import { popupAjaxError } from "discourse/lib/ajax-error";
import I18n from "I18n";
export default class FormTemplateRowItem extends Component {
@service router;
@service dialog;
@action
viewTemplate() {
showModal("admin-customize-form-template-view", {
admin: true,
model: this.args.template,
refreshModel: this.args.refreshModel,
});
}
@action
editTemplate() {
this.router.transitionTo(
"adminCustomizeFormTemplates.edit",
this.args.template
);
}
@action
deleteTemplate() {
return this.dialog.yesNoConfirm({
message: I18n.t("admin.form_templates.delete_confirm", {
template_name: this.args.template.name,
}),
didConfirm: () => {
ajax(`/admin/customize/form-templates/${this.args.template.id}.json`, {
type: "DELETE",
})
.then(() => {
this.args.refreshModel();
})
.catch(popupAjaxError);
},
});
}
}

View File

@ -0,0 +1,14 @@
import Controller from "@ember/controller";
import { action } from "@ember/object";
export default class AdminCustomizeFormTemplatesIndex extends Controller {
@action
newTemplate() {
this.transitionToRoute("adminCustomizeFormTemplates.new");
}
@action
reload() {
this.send("reloadModel");
}
}

View File

@ -0,0 +1,37 @@
import Controller from "@ember/controller";
import ModalFunctionality from "discourse/mixins/modal-functionality";
import { action } from "@ember/object";
import { inject as service } from "@ember/service";
import I18n from "I18n";
import { popupAjaxError } from "discourse/lib/ajax-error";
import { ajax } from "discourse/lib/ajax";
export default class AdminCustomizeFormTemplateView extends Controller.extend(
ModalFunctionality
) {
@service router;
@service dialog;
@action
editTemplate() {
this.router.transitionTo("adminCustomizeFormTemplates.edit", this.model);
}
@action
deleteTemplate() {
return this.dialog.yesNoConfirm({
message: I18n.t("admin.form_templates.delete_confirm", {
template_name: this.model.name,
}),
didConfirm: () => {
ajax(`/admin/customize/form-templates/${this.model.id}.json`, {
type: "DELETE",
})
.then(() => {
this.refreshModel();
})
.catch(popupAjaxError);
},
});
}
}

View File

@ -0,0 +1,70 @@
// TODO(@keegan): Add translations for template strings
export const templateFormFields = [
{
type: "checkbox",
structure: `- type: checkbox
choices:
- "Option 1"
- "Option 2"
- "Option 3"
attributes:
label: "Enter question here"
description: "Enter description here"
validations:
required: true`,
},
{
type: "input",
structure: `- type: input
attributes:
label: "Enter input label here"
description: "Enter input description here"
placeholder: "Enter input placeholder here"
validations:
required: true`,
},
{
type: "textarea",
structure: `- type: textarea
attributes:
label: "Enter textarea label here"
description: "Enter textarea description here"
placeholder: "Enter textarea placeholder here"
validations:
required: true`,
},
{
type: "dropdown",
structure: `- type: dropdown
choices:
- "Option 1"
- "Option 2"
- "Option 3"
attributes:
label: "Enter dropdown label here"
description: "Enter dropdown description here"
validations:
required: true`,
},
{
type: "upload",
structure: `- type: upload
attributes:
file_types: "jpg, png, gif"
label: "Enter upload label here"
description: "Enter upload description here"`,
},
{
type: "multiselect",
structure: `- type: multiple_choice
choices:
- "Option 1"
- "Option 2"
- "Option 3"
attributes:
label: "Enter multiple choice label here"
description: "Enter multiple choice description here"
validations:
required: true`,
},
];

View File

@ -0,0 +1,38 @@
import RestModel from "discourse/models/rest";
import { ajax } from "discourse/lib/ajax";
export default class FormTemplate extends RestModel {}
FormTemplate.reopenClass({
createTemplate(data) {
return ajax("/admin/customize/form-templates.json", {
type: "POST",
data,
});
},
updateTemplate(id, data) {
return ajax(`/admin/customize/form-templates/${id}.json`, {
type: "PUT",
data,
});
},
deleteTemplate(id) {
return ajax(`/admin/customize/form-templates/${id}.json`, {
type: "DELETE",
});
},
findAll() {
return ajax(`/admin/customize/form-templates.json`).then((model) => {
return model.form_templates.sort((a, b) => a.id - b.id);
});
},
findById(id) {
return ajax(`/admin/customize/form-templates/${id}.json`).then((model) => {
return model.form_template;
});
},
});

View File

@ -0,0 +1,12 @@
import DiscourseRoute from "discourse/routes/discourse";
import FormTemplate from "admin/models/form-template";
export default class AdminCustomizeFormTemplatesEdit extends DiscourseRoute {
model(params) {
return FormTemplate.findById(params.id);
}
setupController(controller, model) {
controller.set("model", model);
}
}

View File

@ -0,0 +1,17 @@
import DiscourseRoute from "discourse/routes/discourse";
import { action } from "@ember/object";
import FormTemplate from "admin/models/form-template";
export default class AdminCustomizeFormTemplatesIndex extends DiscourseRoute {
model() {
return FormTemplate.findAll();
}
setupController(controller, model) {
controller.set("model", model);
}
@action
reloadModel() {
this.refresh();
}
}

View File

@ -97,6 +97,14 @@ export default function () {
this.route("edit", { path: "/:field_name" });
}
);
this.route(
"adminCustomizeFormTemplates",
{ path: "/form-templates", resetNamespace: true },
function () {
this.route("new", { path: "/new" });
this.route("edit", { path: "/:id" });
}
);
this.route(
"adminWatchedWords",
{ path: "/watched_words", resetNamespace: true },

View File

@ -0,0 +1,4 @@
<div class="edit-form-template">
<FormTemplate::InfoHeader />
<FormTemplate::Form @model={{this.model}} />
</div>

View File

@ -0,0 +1,32 @@
<div class="form-templates">
<FormTemplate::InfoHeader />
{{#if this.model}}
<table class="form-templates--table grid">
<thead>
<th class="col heading">
{{i18n "admin.form_templates.list_table.headings.name"}}
</th>
<th class="col heading sr-only">
{{i18n "admin.form_templates.list_table.headings.actions"}}
</th>
</thead>
<tbody>
{{#each this.model as |template|}}
<FormTemplate::RowItem
@template={{template}}
@refreshModel={{this.reload}}
/>
{{/each}}
</tbody>
</table>
{{/if}}
<DButton
@class="btn-primary"
@label="admin.form_templates.new_template"
@title="admin.form_templates.new_template"
@icon="plus"
@action={{this.newTemplate}}
/>
</div>

View File

@ -0,0 +1,4 @@
<div class="new-form-template">
<FormTemplate::InfoHeader />
<FormTemplate::Form />
</div>

View File

@ -45,6 +45,13 @@
@label="admin.embedding.title"
@class="admin-customize-embedding"
/>
{{#if this.siteSettings.experimental_form_templates}}
<NavItem
@route="adminCustomizeFormTemplates"
@label="admin.form_templates.nav_title"
@class="admin-customize-form-templates"
/>
{{/if}}
{{/if}}
<NavItem
@route="adminWatchedWords"

View File

@ -0,0 +1,22 @@
<DModalBody @rawTitle={{this.model.name}}>
<HighlightedCode @lang="yaml" @code={{this.model.template}} />
{{! ? TODO(@keegan): Perhaps add what places (ex. categories) the templates are active in }}
</DModalBody>
<div class="modal-footer">
<DButton
class="btn-primary"
@action={{this.editTemplate}}
@icon="pencil-alt"
@label="admin.form_templates.view_template.edit"
/>
<DButton
@action={{route-action "closeModal"}}
@label="admin.form_templates.view_template.close"
/>
<DButton
class="btn-danger"
@action={{this.deleteTemplate}}
@icon="trash-alt"
@label="admin.form_templates.view_template.delete"
/>
</div>

View File

@ -923,3 +923,80 @@ table.permalinks {
}
}
}
.form-templates {
&--info {
margin-top: 1rem;
}
&--table {
margin-bottom: 1rem;
.admin-list-item .action {
text-align: right;
}
}
&--form {
input {
width: 300px;
}
.ace-wrapper {
position: relative;
height: calc(100vh - 450px);
min-height: 200px;
width: 100%;
box-shadow: shadow("footer-nav");
border-radius: 4px;
}
.ace_editor {
border-radius: 4px;
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
}
.ace_placeholder {
font-family: inherit;
font-size: var(--font-up-1);
color: var(--primary-high);
}
.footer-buttons {
display: flex;
gap: 0.5rem;
.btn-danger {
margin-left: auto;
}
}
}
&--quick-insert-field-buttons {
display: flex;
align-items: center;
flex-wrap: wrap;
margin-left: 1rem;
span {
margin-right: 0.25rem;
}
.btn {
&:not(:last-child) {
border-right: 1px solid var(--primary-low);
}
}
}
}
.admin-customize-form-template-view-modal {
.modal-footer {
.btn:last-child {
margin-left: auto;
}
}
}

View File

@ -0,0 +1,58 @@
# frozen_string_literal: true
class Admin::FormTemplatesController < Admin::StaffController
before_action :ensure_form_templates_enabled
def index
form_templates = FormTemplate.all
render_serialized(form_templates, AdminFormTemplateSerializer, root: "form_templates")
end
def new
end
def create
params.require(:name)
params.require(:template)
begin
template = FormTemplate.create!(name: params[:name], template: params[:template])
render_serialized(template, AdminFormTemplateSerializer, root: "form_template")
rescue FormTemplate::NotAllowed => err
render_json_error(err.message)
end
end
def show
template = FormTemplate.find(params[:id])
render_serialized(template, AdminFormTemplateSerializer, root: "form_template")
end
def edit
FormTemplate.find(params[:id])
end
def update
template = FormTemplate.find(params[:id])
begin
template.update!(name: params[:name], template: params[:template])
render_serialized(template, AdminFormTemplateSerializer, root: "form_template")
rescue FormTemplate::NotAllowed => err
render_json_error(err.message)
end
end
def destroy
template = FormTemplate.find(params[:id])
template.destroy!
render json: success_json
end
private
def ensure_form_templates_enabled
raise Discourse::InvalidAccess.new unless SiteSetting.experimental_form_templates
end
end

View File

@ -0,0 +1,22 @@
# frozen_string_literal: true
class FormTemplate < ActiveRecord::Base
validates :name, presence: true, uniqueness: true, length: { maximum: 100 }
validates :template, presence: true, length: { maximum: 2000 }
validates_with FormTemplateYamlValidator
end
# == Schema Information
#
# Table name: form_templates
#
# id :bigint not null, primary key
# name :string(100) not null
# template :text not null
# created_at :datetime not null
# updated_at :datetime not null
#
# Indexes
#
# index_form_templates_on_name (name) UNIQUE
#

View File

@ -0,0 +1,5 @@
# frozen_string_literal: true
class AdminFormTemplateSerializer < ApplicationSerializer
attributes :id, :name, :template
end

View File

@ -5508,6 +5508,41 @@ en:
found_matches: "Found matches:"
no_matches: "No matches found"
form_templates:
nav_title: "Templates"
title: "Form Templates"
help: "Create a template structure that can be used to create new topics, posts, and messages."
new_template: "New Template"
list_table:
headings:
name: "Name"
actions: "Actions"
actions:
view: "View Template"
edit: "Edit Template"
delete: "Delete Template"
view_template:
close: "Close"
edit: "Edit"
delete: "Delete"
new_template_form:
submit: "Save"
cancel: "Cancel"
name:
label: "Template Name"
placeholder: "Enter a name for this template..."
template:
label: "Template"
placeholder: "Create a YAML template here..."
delete_confirm: "Are you sure you would like to delete this template?"
quick_insert_fields:
add_new_field: "Add"
checkbox: "Checkbox"
input: "Short answer"
textarea: "Long answer"
dropdown: "Dropdown"
upload: "Upload a file"
multiselect: "Multiple choice"
impersonate:
title: "Impersonate"
help: "Use this tool to impersonate a user account for debugging purposes. You will have to log out once finished."

View File

@ -5250,3 +5250,7 @@ en:
payload_url:
blocked_or_internal: "Payload URL cannot be used because it resolves to a blocked or internal IP"
unsafe: "Payload URL cannot be used because it's unsafe"
form_templates:
errors:
invalid_yaml: "is not a valid YAML string"

View File

@ -233,6 +233,7 @@ Discourse::Application.routes.draw do
scope "/customize", constraints: AdminConstraint.new do
resources :user_fields, constraints: AdminConstraint.new
resources :emojis, constraints: AdminConstraint.new
resources :form_templates, constraints: AdminConstraint.new, path: "/form-templates"
get "themes/:id/:target/:field_name/edit" => "themes#index"
get "themes/:id" => "themes#index"

View File

@ -2090,6 +2090,10 @@ developer:
client: true
default: false
hidden: true
experimental_form_templates:
client: true
default: false
hidden: true
navigation:
navigation_menu:

View File

@ -0,0 +1,14 @@
# frozen_string_literal: true
class CreateFormTemplates < ActiveRecord::Migration[7.0]
def change
create_table :form_templates do |t|
t.string :name, null: false, limit: 100
t.text :template, null: false, limit: 2000
t.timestamps null: false
end
add_index :form_templates, :name, unique: true
end
end

View File

@ -6,6 +6,7 @@ module SvgSprite
%w[
adjust
address-book
align-left
ambulance
anchor
angle-double-down
@ -34,6 +35,7 @@ module SvgSprite
book-reader
bookmark
briefcase
bullseye
calendar-alt
caret-down
caret-left
@ -45,11 +47,13 @@ module SvgSprite
check
check-circle
check-square
chevron-circle-down
chevron-down
chevron-left
chevron-right
chevron-up
circle
cloud-upload-alt
code
cog
columns
@ -134,6 +138,7 @@ module SvgSprite
gift
globe
globe-americas
grip-lines
hand-point-right
hands-helping
heart

View File

@ -0,0 +1,11 @@
# frozen_string_literal: true
class FormTemplateYamlValidator < ActiveModel::Validator
def validate(record)
begin
yaml = Psych.safe_load(record.template)
rescue Psych::SyntaxError
record.errors.add(:template, I18n.t("form_templates.errors.invalid_yaml"))
end
end
end

View File

@ -0,0 +1,6 @@
# frozen_string_literal: true
Fabricator(:form_template) do
name { sequence(:name) { |i| "template_#{i}" } }
template "some yaml template: value"
end

View File

@ -0,0 +1,20 @@
# frozen_string_literal: true
require "rails_helper"
RSpec.describe FormTemplate, type: :model do
it "can't have duplicate names" do
Fabricate(:form_template, name: "Bug Report", template: "some yaml: true")
t = Fabricate.build(:form_template, name: "Bug Report", template: "some yaml: true")
expect(t.save).to eq(false)
t = Fabricate.build(:form_template, name: "Bug Report", template: "some yaml: true")
expect(t.save).to eq(false)
expect(described_class.count).to eq(1)
end
it "can't have an invalid yaml template" do
template = "first: good\nsecond; bad"
t = Fabricate.build(:form_template, name: "Feature Request", template: template)
expect(t.save).to eq(false)
end
end

View File

@ -0,0 +1,173 @@
# frozen_string_literal: true
RSpec.describe Admin::FormTemplatesController do
fab!(:admin) { Fabricate(:admin) }
fab!(:user) { Fabricate(:user) }
before { SiteSetting.experimental_form_templates = true }
describe "#index" do
fab!(:form_template) { Fabricate(:form_template) }
context "when logged in as an admin" do
before { sign_in(admin) }
it "should work if you are an admin" do
get "/admin/customize/form-templates.json"
expect(response.status).to eq(200)
json = response.parsed_body
expect(json["form_templates"]).to be_present
end
end
context "when logged in as a non-admin user" do
before { sign_in(user) }
it "should not work if you are not an admin" do
get "/admin/customize/form-templates.json"
expect(response.status).to eq(404)
end
end
context "when experiemental form templates is disabled" do
before do
sign_in(admin)
SiteSetting.experimental_form_templates = false
end
it "should not work if you are an admin" do
get "/admin/customize/form-templates.json"
expect(response.status).to eq(403)
end
end
end
describe "#show" do
fab!(:form_template) { Fabricate(:form_template) }
context "when logged in as an admin" do
before { sign_in(admin) }
it "should work if you are an admin" do
get "/admin/customize/form-templates/#{form_template.id}.json"
expect(response.status).to eq(200)
json = response.parsed_body
current_template = json["form_template"]
expect(current_template["id"]).to eq(form_template.id)
expect(current_template["name"]).to eq(form_template.name)
expect(current_template["template"]).to eq(form_template.template)
end
end
end
describe "#create" do
context "when logged in as an admin" do
before { sign_in(admin) }
it "creates a form template" do
expect {
post "/admin/customize/form-templates.json",
params: {
name: "Bug Reports",
template:
"body:\n- 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",
}
expect(response.status).to eq(200)
}.to change(FormTemplate, :count).by(1)
end
end
context "when logged in as a non-admin user" do
before { sign_in(user) }
it "prevents creation with a 404 response" do
expect do
post "/admin/customize/form-templates.json",
params: {
name: "Feature Requests",
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\n- type: input\n attributes:\n label: \"Enter input label here\"\n description: \"Enter input description here\"\n placeholder: \"Enter input placeholder here\"\n validations:\n required: true",
}
end.not_to change { FormTemplate.count }
expect(response.status).to eq(404)
expect(response.parsed_body["errors"]).to include(I18n.t("not_found"))
end
end
end
describe "#update" do
fab!(:form_template) { Fabricate(:form_template) }
context "when logged in as an admin" do
before { sign_in(admin) }
it "updates a form template" do
put "/admin/customize/form-templates/#{form_template.id}.json",
params: {
id: form_template.id,
name: "Updated Template",
template: "New yaml: true",
}
expect(response.status).to eq(200)
form_template.reload
expect(form_template.name).to eq("Updated Template")
expect(form_template.template).to eq("New yaml: true")
end
end
context "when logged in as a non-admin user" do
before { sign_in(user) }
it "prevents update with a 404 response" do
form_template.reload
original_name = form_template.name
put "/admin/customize/form-templates/#{form_template.id}.json",
params: {
name: "Updated Template",
template: "New yaml: true",
}
expect(response.status).to eq(404)
expect(response.parsed_body["errors"]).to include(I18n.t("not_found"))
form_template.reload
expect(form_template.name).to eq(original_name)
end
end
end
describe "#destroy" do
fab!(:form_template) { Fabricate(:form_template) }
context "when logged in as an admin" do
before { sign_in(admin) }
it "deletes a form template" do
expect {
delete "/admin/customize/form-templates/#{form_template.id}.json"
expect(response.status).to eq(200)
}.to change(FormTemplate, :count).by(-1)
end
end
context "when logged in as a non-admin user" do
before { sign_in(user) }
it "prevents deletion with a 404 response" do
expect do
delete "/admin/customize/form-templates/#{form_template.id}.json"
end.not_to change { FormTemplate.count }
expect(response.status).to eq(404)
expect(response.parsed_body["errors"]).to include(I18n.t("not_found"))
end
end
end
end

View File

@ -0,0 +1,141 @@
# frozen_string_literal: true
describe "Admin Customize Form Templates", type: :system, js: true do
let(:form_template_page) { PageObjects::Pages::FormTemplate.new }
let(:ace_editor) { PageObjects::Components::AceEditor.new }
fab!(:admin) { Fabricate(:admin) }
fab!(:form_template) { Fabricate(:form_template) }
before do
SiteSetting.experimental_form_templates = true
sign_in(admin)
end
describe "when visiting the page to customize form templates" do
it "should show the existing form templates in a table" do
visit("/admin/customize/form-templates")
expect(form_template_page).to have_form_template_table
expect(form_template_page).to have_form_template(form_template.name)
end
it "should show the form template structure in a modal" do
visit("/admin/customize/form-templates")
form_template_page.click_view_form_template
expect(form_template_page).to have_template_structure("some yaml template: value")
end
end
describe "when visiting the page to edit a form template" do
it "should prefill form data" do
visit("/admin/customize/form-templates/#{form_template.id}")
expect(form_template_page).to have_name_value(form_template.name)
# difficult to test the ace editor content (todo later)
end
end
def quick_insertion_test(field_type, content)
visit("/admin/customize/form-templates/new")
form_template_page.type_in_template_name("New Template")
form_template_page.click_quick_insert(field_type)
expect(ace_editor).to have_text(content)
end
describe "when visiting the page to create a new form template" do
it "should allow admin to create a new form template" do
visit("/admin/customize/form-templates/new")
sample_name = "My First Template"
sample_template = "test: true"
form_template_page.type_in_template_name(sample_name)
ace_editor.type_input(sample_template)
form_template_page.click_save_button
expect(form_template_page).to have_form_template(sample_name)
end
it "should allow quick insertion of checkbox field" do
quick_insertion_test(
"checkbox",
'- type: checkbox
choices:
- "Option 1"
- "Option 2"
- "Option 3"
attributes:
label: "Enter question here"
description: "Enter description here"
validations:
required: true',
)
end
it "should allow quick insertion of short answer field" do
quick_insertion_test(
"input",
'- type: input
attributes:
label: "Enter input label here"
description: "Enter input description here"
placeholder: "Enter input placeholder here"
validations:
required: true',
)
end
it "should allow quick insertion of long answer field" do
quick_insertion_test(
"textarea",
'- type: textarea
attributes:
label: "Enter textarea label here"
description: "Enter textarea description here"
placeholder: "Enter textarea placeholder here"
validations:
required: true',
)
end
it "should allow quick insertion of dropdown field" do
quick_insertion_test(
"dropdown",
'- type: dropdown
choices:
- "Option 1"
- "Option 2"
- "Option 3"
attributes:
label: "Enter dropdown label here"
description: "Enter dropdown description here"
validations:
required: true',
)
end
it "should allow quick insertion of upload field" do
quick_insertion_test(
"upload",
'- type: upload
attributes:
file_types: "jpg, png, gif"
label: "Enter upload label here"
description: "Enter upload description here"',
)
end
it "should allow quick insertion of multiple choice field" do
quick_insertion_test(
"multiselect",
'- type: multiple_choice
choices:
- "Option 1"
- "Option 2"
- "Option 3"
attributes:
label: "Enter multiple choice label here"
description: "Enter multiple choice description here"
validations:
required: true',
)
end
end
end

View File

@ -0,0 +1,25 @@
# frozen_string_literal: true
module PageObjects
module Components
class AceEditor < PageObjects::Components::Base
def type_input(content)
editor_input.send_keys(content)
self
end
def fill_input(content)
editor_input.fill_in(with: content)
self
end
def clear_input
fill_input("")
end
def editor_input
find(".ace-wrapper .ace_text-input", visible: false)
end
end
end
end

View File

@ -0,0 +1,42 @@
# frozen_string_literal: true
module PageObjects
module Pages
class FormTemplate < PageObjects::Pages::Base
# Form Template Index
def has_form_template_table?
page.has_selector?("table.form-templates--table")
end
def click_view_form_template
find(".form-templates--table tr:first-child .btn-view-template").click
end
def has_form_template?(name)
find(".form-templates--table tbody tr td", text: name).present?
end
def has_template_structure?(structure)
find("code", text: structure).present?
end
# Form Template new/edit form related
def type_in_template_name(input)
find(".form-templates--form-name-input").send_keys(input)
self
end
def click_save_button
find(".form-templates--form .footer-buttons .btn-primary").click
end
def click_quick_insert(field_type)
find(".form-templates--form .quick-insert-#{field_type}").click
end
def has_name_value?(name)
find(".form-templates--form-name-input").value == name
end
end
end
end