From b8813e9759432f97b354dd64c8b6881212539c5b Mon Sep 17 00:00:00 2001 From: Renato Atilio Date: Tue, 10 Oct 2023 16:21:06 -0300 Subject: [PATCH] UX: keep form template client state when shrinking/reopening the composer (#23858) * UX: keep form template client state when shrinking/reopening the composer --- .../app/lib/form-template-validation.js | 11 ++ .../discourse/app/services/composer.js | 57 +++++--- .../acceptance/composer-form-template-test.js | 128 ++++++++++++++++++ 3 files changed, 178 insertions(+), 18 deletions(-) create mode 100644 app/assets/javascripts/discourse/tests/acceptance/composer-form-template-test.js diff --git a/app/assets/javascripts/discourse/app/lib/form-template-validation.js b/app/assets/javascripts/discourse/app/lib/form-template-validation.js index 33aa93c7b06..8e25e08d757 100644 --- a/app/assets/javascripts/discourse/app/lib/form-template-validation.js +++ b/app/assets/javascripts/discourse/app/lib/form-template-validation.js @@ -1,5 +1,16 @@ import I18n from "I18n"; +export function getFormTemplateObject(form) { + const formData = new FormData(form); + + const formObject = {}; + formData.forEach((value, key) => { + formObject[key] = value; + }); + + return formObject; +} + export default function prepareFormTemplateData(form, formTemplate) { const labelMap = formTemplate.reduce((acc, field) => { acc[field.id] = field.attributes.label; diff --git a/app/assets/javascripts/discourse/app/services/composer.js b/app/assets/javascripts/discourse/app/services/composer.js index 8e405620d5a..ff8bafd2915 100644 --- a/app/assets/javascripts/discourse/app/services/composer.js +++ b/app/assets/javascripts/discourse/app/services/composer.js @@ -36,7 +36,9 @@ import { categoryBadgeHTML } from "discourse/helpers/category-link"; import renderTags from "discourse/lib/render-tags"; import { htmlSafe } from "@ember/template"; import { iconHTML } from "discourse-common/lib/icon-library"; -import prepareFormTemplateData from "discourse/lib/form-template-validation"; +import prepareFormTemplateData, { + getFormTemplateObject, +} from "discourse/lib/form-template-validation"; import DiscardDraftModal from "discourse/components/modal/discard-draft"; import PostEnqueuedModal from "discourse/components/modal/post-enqueued"; import { disableImplicitInjections } from "discourse/lib/implicit-injections"; @@ -172,6 +174,14 @@ export default class ComposerService extends Service { return this.model.category?.get("form_template_ids"); } + get hasFormTemplate() { + return ( + this.formTemplateIds?.length > 0 && + !this.get("model.replyingToTopic") && + !this.get("model.editingPost") + ); + } + get formTemplateInitialValues() { return this._formTemplateInitialValues; } @@ -726,7 +736,11 @@ export default class ComposerService extends Service { const composer = this.model; - if (isEmpty(composer?.reply) && isEmpty(composer?.title)) { + if ( + isEmpty(composer?.reply) && + isEmpty(composer?.title) && + !this.hasFormTemplate + ) { this.close(); } else if (composer?.viewOpenOrFullscreen) { this.shrink(); @@ -923,21 +937,15 @@ export default class ComposerService extends Service { this.set("showPreview", false); } - if (this.siteSettings.experimental_form_templates) { - if ( - this.formTemplateIds?.length > 0 && - !this.get("model.replyingToTopic") && - !this.get("model.editingPost") - ) { - const formTemplateData = prepareFormTemplateData( - document.querySelector("#form-template-form"), - this.selectedFormTemplate - ); - if (formTemplateData) { - this.model.set("reply", formTemplateData); - } else { - return; - } + if (this.hasFormTemplate) { + const formTemplateData = prepareFormTemplateData( + document.querySelector("#form-template-form"), + this.selectedFormTemplate + ); + if (formTemplateData) { + this.model.set("reply", formTemplateData); + } else { + return; } } @@ -1610,7 +1618,8 @@ export default class ComposerService extends Service { shrink() { if ( this.get("model.replyDirty") || - (this.get("model.canEditTitle") && this.get("model.titleDirty")) + (this.get("model.canEditTitle") && this.get("model.titleDirty")) || + this.hasFormTemplate ) { this.collapse(); } else { @@ -1626,6 +1635,15 @@ export default class ComposerService extends Service { if (this.model.draftSaving) { this._saveDraftDebounce = discourseDebounce(this, this._saveDraft, 2000); } else { + // This is a temporary solution to avoid losing the current form template state + // until we have a proper draft system for these forms + if (this.hasFormTemplate) { + const form = document.querySelector("#form-template-form"); + if (form) { + this.set("formTemplateInitialValues", getFormTemplateObject(form)); + } + } + this._saveDraftPromise = this.model .saveDraft(this.currentUser) .finally(() => { @@ -1723,6 +1741,9 @@ export default class ComposerService extends Service { document.activeElement?.blur(); document.documentElement.style.removeProperty("--composer-height"); this.setProperties({ model: null, lastValidatedAt: null }); + + // This is a temporary solution to reset the saved form template state while we don't store drafts + this.set("formTemplateInitialValues", undefined); } closeAutocomplete() { diff --git a/app/assets/javascripts/discourse/tests/acceptance/composer-form-template-test.js b/app/assets/javascripts/discourse/tests/acceptance/composer-form-template-test.js new file mode 100644 index 00000000000..28f295b1dde --- /dev/null +++ b/app/assets/javascripts/discourse/tests/acceptance/composer-form-template-test.js @@ -0,0 +1,128 @@ +import { click, fillIn, visit } from "@ember/test-helpers"; +import { toggleCheckDraftPopup } from "discourse/services/composer"; +import { cloneJSON } from "discourse-common/lib/object"; +import TopicFixtures from "discourse/tests/fixtures/topic"; +import { acceptance } from "discourse/tests/helpers/qunit-helpers"; +import selectKit from "discourse/tests/helpers/select-kit-helper"; +import { test } from "qunit"; + +acceptance("Composer Form Template", function (needs) { + needs.user({ + id: 5, + username: "kris", + whisperer: true, + }); + needs.settings({ + experimental_form_templates: true, + general_category_id: 1, + default_composer_category: 1, + }); + needs.site({ + can_tag_topics: true, + categories: [ + { + id: 1, + name: "General", + slug: "general", + permission: 1, + topic_template: null, + form_template_ids: [1], + }, + { + id: 2, + name: "test too", + slug: "test-too", + permission: 1, + topic_template: "", + }, + ], + }); + needs.pretender((server, helper) => { + server.put("/u/kris.json", () => helper.response({ user: {} })); + + server.get("/form-templates/1.json", () => { + return helper.response({ + form_template: { + name: "Testing", + template: `- type: input + id: full-name + attributes: + label: "Full name" + description: "What is your full name?" +- type: textarea + id: description + attributes: + label: "Description"`, + }, + }); + }); + + server.get("/posts/419", () => { + return helper.response({ id: 419 }); + }); + server.get("/composer/mentions", () => { + return helper.response({ + users: [], + user_reasons: {}, + groups: { staff: { user_count: 30 } }, + group_reasons: {}, + max_users_notified_per_group_mention: 100, + }); + }); + server.get("/t/960.json", () => { + const topicList = cloneJSON(TopicFixtures["/t/9/1.json"]); + topicList.post_stream.posts[2].post_type = 4; + return helper.response(topicList); + }); + }); + + needs.hooks.afterEach(() => toggleCheckDraftPopup(false)); + + test("Composer Form Template is shrank and reopened", async function (assert) { + await visit("/"); + await click("#create-topic"); + + assert.strictEqual(selectKit(".category-chooser").header().value(), "1"); + + assert.ok( + document.querySelector("#reply-control").classList.contains("open"), + "reply control is open" + ); + + await fillIn(".form-template-field__input[name='full-name']", "John Smith"); + + await fillIn( + ".form-template-field__textarea[name='description']", + "Community manager" + ); + + await click(".toggle-minimize"); + + assert.ok( + document.querySelector("#reply-control").classList.contains("draft"), + "reply control is minimized into draft mode" + ); + + await click(".toggle-fullscreen"); + + assert.ok( + document.querySelector("#reply-control").classList.contains("open"), + "reply control is opened from draft mode" + ); + + assert.strictEqual( + document.querySelector(".form-template-field__input[name='full-name']") + .value, + "John Smith", + "keeps the value of the input field when composer is re-opened from draft mode" + ); + + assert.strictEqual( + document.querySelector( + ".form-template-field__textarea[name='description']" + ).value, + "Community manager", + "keeps the value of the textarea field when composer is re-opened from draft mode" + ); + }); +});