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

View File

@ -3,16 +3,18 @@ import ModalFunctionality from "discourse/mixins/modal-functionality";
export default Controller.extend(ModalFunctionality, { export default Controller.extend(ModalFunctionality, {
actions: { actions: {
destroyDraft() { async destroyDraft() {
this.onDestroyDraft(); await this.onDestroyDraft();
this.send("closeModal"); this.send("closeModal");
}, },
saveDraftAndClose() {
this.onSaveDraft(); async saveDraftAndClose() {
await this.onSaveDraft();
this.send("closeModal"); this.send("closeModal");
}, },
dismissModal() {
this.onDismissModal(); async dismissModal() {
await this.onDismissModal();
this.send("closeModal"); this.send("closeModal");
}, },
}, },