DEV: Apply form template to categories (#20337)

This commit is contained in:
Keegan George 2023-02-23 11:18:14 -08:00 committed by GitHub
parent a9f2c6db64
commit 6108eee31d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 324 additions and 11 deletions

View File

@ -1,4 +1,38 @@
<DEditor
@value={{this.category.topic_template}}
@showLink={{this.showInsertLinkButton}}
/>
{{#if this.siteSettings.experimental_form_templates}}
<div class="control-group">
<DToggleSwitch
class="toggle-template-type"
@state={{this.showFormTemplate}}
@label={{this.templateTypeToggleLabel}}
{{on "click" this.toggleTemplateType}}
/>
</div>
{{#if this.showFormTemplate}}
<div class="control-group">
<FormTemplateChooser
@class="select-category-template"
@value={{this.category.form_template_ids}}
@onChange={{action (mut this.category.form_template_ids)}}
/>
<p class="select-category-template__info desc">
{{#if this.currentUser.staff}}
<LinkTo @route="adminCustomizeFormTemplates">
{{i18n "admin.form_templates.edit_category.select_template_help"}}
</LinkTo>
{{/if}}
</p>
</div>
{{else}}
<DEditor
@value={{this.category.topic_template}}
@showLink={{this.showInsertLinkButton}}
/>
{{/if}}
{{else}}
<DEditor
@value={{this.category.topic_template}}
@showLink={{this.showInsertLinkButton}}
/>
{{/if}}

View File

@ -1,15 +1,40 @@
import { buildCategoryPanel } from "discourse/components/edit-category-panel";
import { observes } from "discourse-common/utils/decorators";
import discourseComputed, { observes } from "discourse-common/utils/decorators";
import { schedule } from "@ember/runloop";
import { action, computed } from "@ember/object";
export default buildCategoryPanel("topic-template", {
// Modals are defined using the singleton pattern.
// Opening the insert link modal will destroy the edit category modal.
showInsertLinkButton: false,
showFormTemplate: computed("category.form_template_ids", {
get() {
return Boolean(this.category.form_template_ids.length);
},
set(key, value) {
return value;
},
}),
@observes("activeTab")
@discourseComputed("showFormTemplate")
templateTypeToggleLabel(showFormTemplate) {
if (showFormTemplate) {
return "admin.form_templates.edit_category.toggle_form_template";
}
return "admin.form_templates.edit_category.toggle_freeform";
},
@action
toggleTemplateType() {
this.toggleProperty("showFormTemplate");
if (!this.showFormTemplate) {
// Clear associated form templates if switching to freeform
this.set("category.form_template_ids", []);
}
},
@observes("activeTab", "showFormTemplate")
_activeTabChanged() {
if (this.activeTab) {
if (this.activeTab && !this.showFormTemplate) {
schedule("afterRender", () =>
this.element.querySelector(".d-editor-input").focus()
);

View File

@ -221,6 +221,7 @@ const Category = RestModel.extend({
allow_badges: this.allow_badges,
custom_fields: this.custom_fields,
topic_template: this.topic_template,
form_template_ids: this.form_template_ids,
all_topics_wiki: this.all_topics_wiki,
allow_unlimited_owner_edits_on_first_post:
this.allow_unlimited_owner_edits_on_first_post,

View File

@ -0,0 +1,51 @@
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 pretender, { response } from "discourse/tests/helpers/create-pretender";
import selectKit from "discourse/tests/helpers/select-kit-helper";
module(
"Integration | Component | select-kit/form-template-chooser",
function (hooks) {
setupRenderingTest(hooks);
hooks.beforeEach(function () {
this.set("subject", selectKit());
pretender.get("/admin/customize/form-templates.json", () => {
return response({
form_templates: [
{ id: 1, name: "template 1", template: "test: true" },
{ id: 2, name: "template 2", template: "test: false" },
],
});
});
});
test("displays form templates", async function (assert) {
await render(hbs`<FormTemplateChooser />`);
await this.subject.expand();
assert.strictEqual(this.subject.rowByIndex(0).value(), "1");
assert.strictEqual(this.subject.rowByIndex(1).value(), "2");
});
test("displays selected value", async function (assert) {
this.set("value", [1]);
await render(hbs`<FormTemplateChooser @value={{this.value}} />`);
assert.strictEqual(this.subject.header().name(), "template 1");
});
test("when no templates are available, the select is disabled", async function (assert) {
pretender.get("/admin/customize/form-templates.json", () => {
return response({ form_templates: [] });
});
await render(hbs`<FormTemplateChooser />`);
assert.ok(this.subject.isDisabled());
});
}
);

View File

@ -0,0 +1,44 @@
import MultiSelectComponent from "select-kit/components/multi-select";
import FormTemplate from "admin/models/form-template";
import { computed } from "@ember/object";
export default MultiSelectComponent.extend({
pluginApiIdentifiers: ["form-template-chooser"],
classNames: ["form-template-chooser"],
selectKitOptions: {
none: "admin.form_templates.edit_category.select_template",
},
init() {
this._super(...arguments);
if (!this.templates) {
this._fetchTemplates();
}
},
@computed("templates")
get content() {
if (!this.templates) {
return this._fetchTemplates();
}
return this.templates;
},
_fetchTemplates() {
FormTemplate.findAll().then((result) => {
const sortedTemplates = this._sortTemplatesByName(result);
if (sortedTemplates.length > 0) {
return this.set("templates", sortedTemplates);
} else {
this.set("templates", sortedTemplates);
this.set("selectKit.options.disabled", true);
}
});
},
_sortTemplatesByName(templates) {
return templates.sort((a, b) => a.name.localeCompare(b.name));
},
});

View File

@ -176,6 +176,13 @@ div.edit-category {
margin-bottom: 1em;
}
}
.edit-category-tab-topic-template {
.select-category-template__info {
margin-block: 0.25rem;
font-size: var(--font-down-1);
}
}
}
.category-permissions-table {

View File

@ -410,6 +410,7 @@ class CategoriesController < ApplicationController
allowed_tags: [],
allowed_tag_groups: [],
required_tag_groups: %i[name min_count],
form_template_ids: [],
)
if result[:required_tag_groups] && !result[:required_tag_groups].is_a?(Array)

View File

@ -144,6 +144,9 @@ class Category < ActiveRecord::Base
has_many :sidebar_section_links, as: :linkable, dependent: :delete_all
has_many :embeddable_hosts, dependent: :destroy
has_many :category_form_templates, dependent: :destroy
has_many :form_templates, through: :category_form_templates
belongs_to :reviewable_by_group, class_name: "Group"
scope :latest, -> { order("topic_count DESC") }

View File

@ -0,0 +1,22 @@
# frozen_string_literal: true
class CategoryFormTemplate < ActiveRecord::Base
belongs_to :category
belongs_to :form_template
end
# == Schema Information
#
# Table name: category_form_templates
#
# id :bigint not null, primary key
# category_id :bigint not null
# form_template_id :bigint not null
# created_at :datetime not null
# updated_at :datetime not null
#
# Indexes
#
# index_category_form_templates_on_category_id (category_id)
# index_category_form_templates_on_form_template_id (form_template_id)
#

View File

@ -4,6 +4,9 @@ class FormTemplate < ActiveRecord::Base
validates :name, presence: true, uniqueness: true, length: { maximum: 100 }
validates :template, presence: true, length: { maximum: 2000 }
validates_with FormTemplateYamlValidator
has_many :category_form_templates, dependent: :destroy
has_many :categories, through: :category_form_templates
end
# == Schema Information

View File

@ -19,6 +19,7 @@ class BasicCategorySerializer < ApplicationSerializer
:notification_level,
:can_edit,
:topic_template,
:form_template_ids,
:has_children,
:sort_order,
:sort_ascending,
@ -91,4 +92,8 @@ class BasicCategorySerializer < ApplicationSerializer
def include_custom_fields?
custom_fields.present?
end
def include_form_template_ids?
SiteSetting.experimental_form_templates
end
end

View File

@ -3632,7 +3632,7 @@ en:
back: "Back to category"
general: "General"
settings: "Settings"
topic_template: "Topic Template"
topic_template: "Template"
tags: "Tags"
tags_allowed_tags: "Restrict these tags to this category:"
tags_allowed_tag_groups: "Restrict these tag groups to this category:"
@ -5565,6 +5565,12 @@ en:
dropdown: "Dropdown"
upload: "Upload a file"
multiselect: "Multiple choice"
edit_category:
toggle_freeform: "form template disabled"
toggle_form_template: "form template enabled"
select_template: "Select form templates"
select_template_help: "Add/Edit Form Templates"
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

@ -0,0 +1,12 @@
# frozen_string_literal: true
class CreateCategoryFormTemplates < ActiveRecord::Migration[7.0]
def change
create_table :category_form_templates do |t|
t.references :category, null: false
t.references :form_template, null: false
t.timestamps
end
end
end

View File

@ -674,6 +674,8 @@ RSpec.describe CategoriesController do
readonly = CategoryGroup.permission_types[:readonly]
create_post = CategoryGroup.permission_types[:create_post]
tag_group = Fabricate(:tag_group)
form_template_1 = Fabricate(:form_template)
form_template_2 = Fabricate(:form_template)
put "/categories/#{category.id}.json",
params: {
@ -693,6 +695,7 @@ RSpec.describe CategoriesController do
minimum_required_tags: "",
allow_global_tags: "true",
required_tag_groups: [{ name: tag_group.name, min_count: 2 }],
form_template_ids: [form_template_1.id, form_template_2.id],
}
expect(response.status).to eq(200)
@ -713,6 +716,7 @@ RSpec.describe CategoriesController do
expect(category.category_required_tag_groups.count).to eq(1)
expect(category.category_required_tag_groups.first.tag_group.id).to eq(tag_group.id)
expect(category.category_required_tag_groups.first.min_count).to eq(2)
expect(category.form_template_ids).to eq([form_template_1.id, form_template_2.id])
end
it "logs the changes correctly" do
@ -839,6 +843,7 @@ RSpec.describe CategoriesController do
expect(category.tag_groups).to be_blank
expect(category.category_required_tag_groups).to eq([])
expect(category.custom_fields).to eq({ "field_1" => "hi" })
expect(category.form_template_ids.count).to eq(0)
end
end
end

View File

@ -0,0 +1,65 @@
# frozen_string_literal: true
describe "Edit Category", type: :system, js: true do
fab!(:color_scheme) { Fabricate(:color_scheme) }
fab!(:theme) { Fabricate(:theme) }
fab!(:admin) { Fabricate(:admin) }
fab!(:form_template) { Fabricate(:form_template) }
fab!(:form_template_2) { Fabricate(:form_template) }
fab!(:category) do
Fabricate(:category, name: "Cool Category", slug: "cool-cat", topic_count: 3234)
end
let(:category_page) { PageObjects::Pages::Category.new }
before do
SiteSetting.experimental_form_templates = true
sign_in(admin)
end
describe "when editing a category with no form templates set" do
before { category.update(form_template_ids: []) }
it "should have form templates disabled and topic template enabled" do
category_page.visit_edit_template(category)
expect(category_page).not_to have_form_template_enabled
expect(category_page).to have_d_editor
end
it "should allow you to select and save a form template" do
category_page.visit_edit_template(category)
category_page.toggle_form_templates
expect(category_page).not_to have_d_editor
category_page.select_form_template(form_template.name)
expect(category_page).to have_selected_template(form_template.name)
category_page.save_settings
try_until_success do
expect(Category.find_by_id(category.id).form_template_ids).to eq([form_template.id])
end
end
it "should allow you to select and save multiple form templates" do
category_page.visit_edit_template(category)
category_page.toggle_form_templates
category_page.select_form_template(form_template.name)
category_page.select_form_template(form_template_2.name)
category_page.save_settings
try_until_success do
expect(Category.find_by_id(category.id).form_template_ids).to eq(
[form_template.id, form_template_2.id],
)
end
end
end
describe "when editing a category with form templates set" do
before { category.update(form_template_ids: [form_template.id, form_template_2.id]) }
it "should have form templates enabled and showing the selected templates" do
category_page.visit_edit_template(category)
expect(category_page).to have_form_template_enabled
expect(category_page).not_to have_d_editor
selected_templates = "#{form_template.name},#{form_template_2.name}"
expect(category_page).to have_selected_template(selected_templates)
end
end
end

View File

@ -15,6 +15,11 @@ module PageObjects
self
end
def visit_edit_template(category)
page.visit("/c/#{category.slug}/edit/topic-template")
self
end
def back_to_category
find(".edit-category-title-bar span", text: "Back to category").click
self
@ -29,6 +34,30 @@ module PageObjects
find(".edit-category-tab .#{setting} label.checkbox-label", text: text).click
self
end
# Edit Category Page
def has_form_template_enabled?
find(".d-toggle-switch .toggle-template-type", visible: false)["aria-checked"] == "true"
end
def has_d_editor?
page.has_selector?(".d-editor")
end
def has_selected_template?(template_name)
find(".select-category-template .select-kit-header")["data-name"] == template_name
end
def toggle_form_templates
find(".d-toggle-switch .d-toggle-switch__checkbox-slider").click
self
end
def select_form_template(template_name)
find(".select-category-template").click
find(".select-kit-collection .select-kit-row", text: template_name).click
find(".select-category-template").click
end
end
end
end