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:
parent
a252bbf3e8
commit
7b51ac418b
|
@ -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 });
|
||||
},
|
||||
|
||||
|
|
|
@ -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");
|
||||
},
|
||||
},
|
||||
|
|
Loading…
Reference in New Issue