DEV: Asyncify most of Composer controller (#17974)

…and fix cases where we were breaking the promise/async chain (by not awaiting or not returning promises)
This commit is contained in:
Jarek Radosz 2022-08-18 13:58:08 +02:00 committed by GitHub
parent a252bbf3e8
commit 7b51ac418b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 211 additions and 241 deletions

View File

@ -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 });
},

View File

@ -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");
},
},