diff --git a/app/assets/javascripts/discourse/app/controllers/composer.js b/app/assets/javascripts/discourse/app/controllers/composer.js index b3c0340c736..0a31c59f67a 100644 --- a/app/assets/javascripts/discourse/app/controllers/composer.js +++ b/app/assets/javascripts/discourse/app/controllers/composer.js @@ -6,7 +6,7 @@ import { authorizesOneOrMoreExtensions, uploadIcon, } from "discourse/lib/uploads"; -import { cancel, run, scheduleOnce } from "@ember/runloop"; +import { cancel, scheduleOnce } from "@ember/runloop"; import { cannotPostAgain, durationTextFromSeconds, @@ -424,53 +424,43 @@ export default Controller.extend({ // - openOpts: this object will be passed to this.open if fallbackToNewTopic is // true or topic is provided @action - focusComposer(opts = {}) { - return this._openComposerForFocus(opts).then(() => { - this._focusAndInsertText(opts.insertText); - }); + async focusComposer(opts = {}) { + await this._openComposerForFocus(opts); + this._focusAndInsertText(opts.insertText); }, - _openComposerForFocus(opts) { + async _openComposerForFocus(opts) { if (this.get("model.viewOpen")) { - return Promise.resolve(); - } else { - const opened = this.openIfDraft(); - if (opened) { - return Promise.resolve(); - } + return; + } - if (opts.topic) { - return this.open( - Object.assign( - { - action: Composer.REPLY, - draftKey: opts.topic.get("draft_key"), - draftSequence: opts.topic.get("draft_sequence"), - topic: opts.topic, - }, - opts.openOpts || {} - ) - ); - } + const opened = this.openIfDraft(); + if (opened) { + return; + } - if (opts.fallbackToNewTopic) { - return this.open( - Object.assign( - { - action: Composer.CREATE_TOPIC, - draftKey: Composer.NEW_TOPIC_KEY, - }, - opts.openOpts || {} - ) - ); - } + if (opts.topic) { + return await this.open({ + action: Composer.REPLY, + draftKey: opts.topic.get("draft_key"), + draftSequence: opts.topic.get("draft_sequence"), + topic: opts.topic, + ...(opts.openOpts || {}), + }); + } + + if (opts.fallbackToNewTopic) { + return await this.open({ + action: Composer.CREATE_TOPIC, + draftKey: Composer.NEW_TOPIC_KEY, + ...(opts.openOpts || {}), + }); } }, _focusAndInsertText(insertText) { scheduleOnce("afterRender", () => { - const input = document.querySelector("textarea.d-editor-input"); - input && input.focus(); + document.querySelector("textarea.d-editor-input")?.focus(); if (insertText) { this.model.appendText(insertText, null, { new_line: true }); @@ -480,23 +470,25 @@ export default Controller.extend({ @action openIfDraft(event) { - if (this.get("model.viewDraft")) { - // when called from shortcut, ensure we don't propagate the key to - // the composer input title - if (event) { - event.preventDefault(); - event.stopPropagation(); - } - - this.set("model.composeState", Composer.OPEN); - document.documentElement.style.setProperty( - "--composer-height", - this.get("model.composerHeight") - ); - return true; + if (!this.get("model.viewDraft")) { + return false; } - return false; + // when called from shortcut, ensure we don't propagate the key to + // the composer input title + if (event) { + event.preventDefault(); + event.stopPropagation(); + } + + this.set("model.composeState", Composer.OPEN); + + document.documentElement.style.setProperty( + "--composer-height", + this.get("model.composerHeight") + ); + + return true; }, @action @@ -513,37 +505,29 @@ export default Controller.extend({ this.close(); }, - openComposer(options, post, topic) { - this.open(options).then(() => { - let url; - if (post) { - url = post.url; - } - if (!post && topic) { - url = topic.url; - } + async openComposer(options, post, topic) { + await this.open(options); - let topicTitle; - if (topic) { - topicTitle = topic.title; - } + let url = post?.url || topic?.url; + const topicTitle = topic?.title; - if (!url || !topicTitle) { - return; - } + if (!url || !topicTitle) { + return; + } - url = `${location.protocol}//${location.host}${url}`; - const link = `[${escapeExpression(topicTitle)}](${url})`; - const continueDiscussion = I18n.t("post.continue_discussion", { - postLink: link, - }); + url = `${location.protocol}//${location.host}${url}`; + const link = `[${escapeExpression(topicTitle)}](${url})`; + const continueDiscussion = I18n.t("post.continue_discussion", { + postLink: link, + }); - const reply = this.get("model.reply"); - if (!reply || !reply.includes(continueDiscussion)) { - this.model.prependText(continueDiscussion, { - new_line: true, - }); - } + const reply = this.get("model.reply"); + if (reply?.includes(continueDiscussion)) { + return; + } + + this.model.prependText(continueDiscussion, { + new_line: true, }); }, @@ -654,19 +638,17 @@ export default Controller.extend({ }, // Toggle the reply view - toggle() { + async toggle() { this.closeAutocomplete(); const composer = this.model; if (isEmpty(composer?.reply) && isEmpty(composer?.title)) { this.close(); + } else if (composer?.viewOpenOrFullscreen) { + this.shrink(); } else { - if (composer?.viewOpenOrFullscreen) { - this.shrink(); - } else { - this.cancelComposer(); - } + await this.cancelComposer(); } return false; @@ -678,7 +660,7 @@ export default Controller.extend({ }, // Import a quote from the post - importQuote(toolbarEvent) { + async importQuote(toolbarEvent) { const postStream = this.get("topic.postStream"); let postId = this.get("model.post.id"); @@ -702,22 +684,21 @@ export default Controller.extend({ } } - if (postId) { - this.set("model.loading", true); - - return this.store.find("post", postId).then((post) => { - const quote = buildQuote(post, post.raw, { - full: true, - }); - - toolbarEvent.addText(quote); - this.set("model.loading", false); - }); + if (!postId) { + return; } + + this.set("model.loading", true); + + const post = await this.store.find("post", postId); + const quote = buildQuote(post, post.raw, { full: true }); + + toolbarEvent.addText(quote); + this.set("model.loading", false); }, - cancel() { - this.cancelComposer(); + async cancel() { + await this.cancelComposer(); }, save(ignore, event) { @@ -1002,6 +983,7 @@ export default Controller.extend({ } if (result.responseJson.route_to) { + // TODO: await this: this.destroyDraft(); if (result.responseJson.message) { return bootbox.alert(result.responseJson.message, () => { @@ -1072,9 +1054,7 @@ export default Controller.extend({ @param {Boolean} [opts.skipDraftCheck] @param {Boolean} [opts.skipJumpOnSave] Option to skip navigating to the post when saved in this composer session **/ - open(opts) { - opts = opts || {}; - + open(opts = {}) { if (!opts.draftKey) { throw new Error("composer opened without a proper draft key"); } @@ -1192,7 +1172,7 @@ export default Controller.extend({ }); } - this._setModel(composerModel, opts).then(resolve, reject); + return this._setModel(composerModel, opts).then(resolve, reject); }); promise = promise.finally(() => { @@ -1202,83 +1182,77 @@ export default Controller.extend({ }, // Given a potential instance and options, set the model for this composer. - _setModel(optionalComposerModel, opts) { - let promise = Promise.resolve(); - + async _setModel(optionalComposerModel, opts) { this.set("linkLookup", null); - promise = promise.then(() => { - if (opts.draft) { - return loadDraft(this.store, opts).then((model) => { - if (!model) { - throw new Error("draft was not found"); - } - return model; - }); - } else { - let model = - optionalComposerModel || this.store.createRecord("composer"); - return model.open(opts).then(() => model); + let composerModel; + if (opts.draft) { + composerModel = await loadDraft(this.store, opts); + + if (!composerModel) { + throw new Error("draft was not found"); } + } else { + const model = + optionalComposerModel || this.store.createRecord("composer"); + + await model.open(opts); + composerModel = model; + } + + this.set("model", composerModel); + + composerModel.setProperties({ + composeState: Composer.OPEN, + isWarning: false, + hasTargetGroups: opts.hasGroups, }); - promise.then((composerModel) => { - this.set("model", composerModel); - - composerModel.setProperties({ - composeState: Composer.OPEN, - isWarning: false, - hasTargetGroups: opts.hasGroups, - }); - - if (!this.model.targetRecipients) { - if (opts.usernames) { - deprecated("`usernames` is deprecated, use `recipients` instead."); - this.model.set("targetRecipients", opts.usernames); - } else if (opts.recipients) { - this.model.set("targetRecipients", opts.recipients); - } + if (!this.model.targetRecipients) { + if (opts.usernames) { + deprecated("`usernames` is deprecated, use `recipients` instead."); + this.model.set("targetRecipients", opts.usernames); + } else if (opts.recipients) { + this.model.set("targetRecipients", opts.recipients); } + } - if ( - opts.topicTitle && - opts.topicTitle.length <= this.siteSettings.max_topic_title_length - ) { - this.model.set("title", opts.topicTitle); - } + if ( + opts.topicTitle && + opts.topicTitle.length <= this.siteSettings.max_topic_title_length + ) { + this.model.set("title", opts.topicTitle); + } - if (opts.topicCategoryId) { - this.model.set("categoryId", opts.topicCategoryId); - } + if (opts.topicCategoryId) { + this.model.set("categoryId", opts.topicCategoryId); + } - if (opts.topicTags && this.site.can_tag_topics) { - let tags = escapeExpression(opts.topicTags) - .split(",") - .slice(0, this.siteSettings.max_tags_per_topic); + if (opts.topicTags && this.site.can_tag_topics) { + let tags = escapeExpression(opts.topicTags) + .split(",") + .slice(0, this.siteSettings.max_tags_per_topic); - tags.forEach( - (tag, index, array) => - (array[index] = tag.substring(0, this.siteSettings.max_tag_length)) - ); - - this.model.set("tags", tags); - } - - if (opts.topicBody) { - this.model.set("reply", opts.topicBody); - } - - const defaultComposerHeight = - this.model.action === "reply" ? "300px" : "400px"; - - this.set("model.composerHeight", defaultComposerHeight); - document.documentElement.style.setProperty( - "--composer-height", - defaultComposerHeight + tags.forEach( + (tag, index, array) => + (array[index] = tag.substring(0, this.siteSettings.max_tag_length)) ); - }); - return promise; + this.model.set("tags", tags); + } + + if (opts.topicBody) { + this.model.set("reply", opts.topicBody); + } + + const defaultComposerHeight = + this.model.action === "reply" ? "300px" : "400px"; + + this.set("model.composerHeight", defaultComposerHeight); + document.documentElement.style.setProperty( + "--composer-height", + defaultComposerHeight + ); }, viewNewReply() { @@ -1287,24 +1261,24 @@ export default Controller.extend({ return false; }, - destroyDraft(draftSequence = null) { + async destroyDraft(draftSequence = null) { const key = this.get("model.draftKey"); - if (key) { - if (key === Composer.NEW_TOPIC_KEY) { - this.currentUser.set("has_topic_draft", false); - } - - if (this._saveDraftPromise) { - return this._saveDraftPromise.then(() => this.destroyDraft()); - } - - const sequence = draftSequence || this.get("model.draftSequence"); - return Draft.clear(key, sequence).then(() => - this.appEvents.trigger("draft:destroyed", key) - ); - } else { - return Promise.resolve(); + if (!key) { + return; } + + if (key === Composer.NEW_TOPIC_KEY) { + this.currentUser.set("has_topic_draft", false); + } + + if (this._saveDraftPromise) { + await this._saveDraftPromise; + return await this.destroyDraft(); + } + + const sequence = draftSequence || this.get("model.draftSequence"); + await Draft.clear(key, sequence); + this.appEvents.trigger("draft:destroyed", key); }, confirmDraftAbandon(data) { @@ -1319,30 +1293,30 @@ export default Controller.extend({ return data; } - if (_checkDraftPopup) { - return new Promise((resolve) => { - bootbox.dialog(I18n.t("drafts.abandon.confirm"), [ - { - label: I18n.t("drafts.abandon.no_value"), - callback: () => resolve(data), - }, - { - label: I18n.t("drafts.abandon.yes_value"), - class: "btn-danger", - icon: iconHTML("far-trash-alt"), - callback: () => { - this.destroyDraft(data.draft_sequence).finally(() => { - data.draft = null; - resolve(data); - }); - }, - }, - ]); - }); - } else { + if (!_checkDraftPopup) { data.draft = null; return data; } + + return new Promise((resolve) => { + bootbox.dialog(I18n.t("drafts.abandon.confirm"), [ + { + label: I18n.t("drafts.abandon.no_value"), + callback: () => resolve(data), + }, + { + label: I18n.t("drafts.abandon.yes_value"), + class: "btn-danger", + icon: iconHTML("far-trash-alt"), + callback: () => { + this.destroyDraft(data.draft_sequence).finally(() => { + data.draft = null; + resolve(data); + }); + }, + }, + ]); + }); }, cancelComposer() { @@ -1352,7 +1326,7 @@ export default Controller.extend({ cancel(this._saveDraftDebounce); } - let promise = new Promise((resolve, reject) => { + return new Promise((resolve, reject) => { if (this.get("model.hasMetaData") || this.get("model.replyDirty")) { const modal = showModal("discard-draft", { model: this.model, @@ -1360,7 +1334,7 @@ export default Controller.extend({ }); modal.setProperties({ onDestroyDraft: () => { - this.destroyDraft() + return this.destroyDraft() .then(() => { this.model.clearState(); this.close(); @@ -1375,7 +1349,7 @@ export default Controller.extend({ this.model.clearState(); this.close(); this.appEvents.trigger("composer:cancelled"); - resolve(); + return resolve(); }, // needed to resume saving drafts if composer stays open onDismissModal: () => reject(), @@ -1392,9 +1366,7 @@ export default Controller.extend({ resolve(); }); } - }); - - return promise.finally(() => { + }).finally(() => { this.skipAutoSave = false; }); }, @@ -1411,26 +1383,19 @@ export default Controller.extend({ }, _saveDraft() { - const model = this.model; - if (model) { - if (model.draftSaving) { - // in test debounce is Ember.run, this will cause - // an infinite loop - if (!isTesting()) { - this._saveDraftDebounce = discourseDebounce( - this, - this._saveDraft, - 2000 - ); - } - } else { - this._saveDraftPromise = model - .saveDraft(this.currentUser) - .finally(() => { - this._lastDraftSaved = Date.now(); - this._saveDraftPromise = null; - }); - } + if (!this.model) { + return; + } + + if (this.model.draftSaving) { + this._saveDraftDebounce = discourseDebounce(this, this._saveDraft, 2000); + } else { + this._saveDraftPromise = this.model + .saveDraft(this.currentUser) + .finally(() => { + this._lastDraftSaved = Date.now(); + this._saveDraftPromise = null; + }); } }, @@ -1449,8 +1414,11 @@ export default Controller.extend({ if (Date.now() - this._lastDraftSaved > 15000) { this._saveDraft(); } else { - let method = isTesting() ? run : discourseDebounce; - this._saveDraftDebounce = method(this, this._saveDraft, 2000); + this._saveDraftDebounce = discourseDebounce( + this, + this._saveDraft, + 2000 + ); } } }, @@ -1516,7 +1484,7 @@ export default Controller.extend({ elem.classList.remove("fullscreen-composer"); elem.classList.remove("composer-open"); - document.activeElement && document.activeElement.blur(); + document.activeElement?.blur(); this.setProperties({ model: null, lastValidatedAt: null }); }, diff --git a/app/assets/javascripts/discourse/app/controllers/discard-draft.js b/app/assets/javascripts/discourse/app/controllers/discard-draft.js index eecd880f546..75bf5a53129 100644 --- a/app/assets/javascripts/discourse/app/controllers/discard-draft.js +++ b/app/assets/javascripts/discourse/app/controllers/discard-draft.js @@ -3,16 +3,18 @@ import ModalFunctionality from "discourse/mixins/modal-functionality"; export default Controller.extend(ModalFunctionality, { actions: { - destroyDraft() { - this.onDestroyDraft(); + async destroyDraft() { + await this.onDestroyDraft(); this.send("closeModal"); }, - saveDraftAndClose() { - this.onSaveDraft(); + + async saveDraftAndClose() { + await this.onSaveDraft(); this.send("closeModal"); }, - dismissModal() { - this.onDismissModal(); + + async dismissModal() { + await this.onDismissModal(); this.send("closeModal"); }, },