FEATURE: Allow users to save draft and close composer (#12439)
We previously included this option conditionally when users were replying or creating a new topic while they had content already in the composer. This makes the dialog always include three buttons: - Close and discard - Close and save draft for later - Keed editing This also changes how the backend notifies the frontend when there is a current draft topic. This is now sent via the `has_topic_draft` property in the current user serializer.
This commit is contained in:
parent
6eab1e83c3
commit
d470e4fade
|
@ -56,7 +56,8 @@ export default Component.extend({
|
||||||
"fixed",
|
"fixed",
|
||||||
"subtitle",
|
"subtitle",
|
||||||
"rawSubtitle",
|
"rawSubtitle",
|
||||||
"dismissable"
|
"dismissable",
|
||||||
|
"headerClass"
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
|
@ -23,6 +23,7 @@ export default Component.extend({
|
||||||
title: null,
|
title: null,
|
||||||
subtitle: null,
|
subtitle: null,
|
||||||
role: "dialog",
|
role: "dialog",
|
||||||
|
headerClass: null,
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
this._super(...arguments);
|
this._super(...arguments);
|
||||||
|
@ -129,6 +130,10 @@ export default Component.extend({
|
||||||
this.set("dismissable", true);
|
this.set("dismissable", true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (data.headerClass) {
|
||||||
|
this.set("headerClass", data.headerClass);
|
||||||
|
}
|
||||||
|
|
||||||
if (this.element) {
|
if (this.element) {
|
||||||
const autofocusInputs = this.element.querySelectorAll(
|
const autofocusInputs = this.element.querySelectorAll(
|
||||||
".modal-body input[autofocus]"
|
".modal-body input[autofocus]"
|
||||||
|
|
|
@ -18,6 +18,7 @@ import discourseComputed, {
|
||||||
import DiscourseURL from "discourse/lib/url";
|
import DiscourseURL from "discourse/lib/url";
|
||||||
import Draft from "discourse/models/draft";
|
import Draft from "discourse/models/draft";
|
||||||
import I18n from "I18n";
|
import I18n from "I18n";
|
||||||
|
import { iconHTML } from "discourse-common/lib/icon-library";
|
||||||
import { Promise } from "rsvp";
|
import { Promise } from "rsvp";
|
||||||
import bootbox from "bootbox";
|
import bootbox from "bootbox";
|
||||||
import { buildQuote } from "discourse/lib/quote";
|
import { buildQuote } from "discourse/lib/quote";
|
||||||
|
@ -545,9 +546,7 @@ export default Controller.extend({
|
||||||
},
|
},
|
||||||
|
|
||||||
cancel() {
|
cancel() {
|
||||||
const differentDraftContext =
|
this.cancelComposer();
|
||||||
this.get("topic.id") !== this.get("model.topic.id");
|
|
||||||
this.cancelComposer(differentDraftContext);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
save(ignore, event) {
|
save(ignore, event) {
|
||||||
|
@ -903,13 +902,7 @@ export default Controller.extend({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If it's a different draft, cancel it and try opening again.
|
return this.cancelComposer()
|
||||||
const differentDraftContext =
|
|
||||||
opts.post && composerModel.topic
|
|
||||||
? composerModel.topic.id !== opts.post.topic_id
|
|
||||||
: true;
|
|
||||||
|
|
||||||
return this.cancelComposer(differentDraftContext)
|
|
||||||
.then(() => this.open(opts))
|
.then(() => this.open(opts))
|
||||||
.then(resolve, reject);
|
.then(resolve, reject);
|
||||||
}
|
}
|
||||||
|
@ -1037,18 +1030,19 @@ export default Controller.extend({
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
|
|
||||||
destroyDraft() {
|
destroyDraft(draftSequence = null) {
|
||||||
const key = this.get("model.draftKey");
|
const key = this.get("model.draftKey");
|
||||||
if (key) {
|
if (key) {
|
||||||
if (key === "new_topic") {
|
if (key === Composer.NEW_TOPIC_KEY) {
|
||||||
this.send("clearTopicDraft");
|
this.currentUser.set("has_topic_draft", false);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this._saveDraftPromise) {
|
if (this._saveDraftPromise) {
|
||||||
return this._saveDraftPromise.then(() => this.destroyDraft());
|
return this._saveDraftPromise.then(() => this.destroyDraft());
|
||||||
}
|
}
|
||||||
|
|
||||||
return Draft.clear(key, this.get("model.draftSequence")).then(() =>
|
const sequence = draftSequence || this.get("model.draftSequence");
|
||||||
|
return Draft.clear(key, sequence).then(() =>
|
||||||
this.appEvents.trigger("draft:destroyed", key)
|
this.appEvents.trigger("draft:destroyed", key)
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
@ -1078,9 +1072,12 @@ export default Controller.extend({
|
||||||
{
|
{
|
||||||
label: I18n.t("drafts.abandon.yes_value"),
|
label: I18n.t("drafts.abandon.yes_value"),
|
||||||
class: "btn-danger",
|
class: "btn-danger",
|
||||||
|
icon: iconHTML("far-trash-alt"),
|
||||||
callback: () => {
|
callback: () => {
|
||||||
|
this.destroyDraft(data.draft_sequence).finally(() => {
|
||||||
data.draft = null;
|
data.draft = null;
|
||||||
resolve(data);
|
resolve(data);
|
||||||
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
@ -1091,22 +1088,20 @@ export default Controller.extend({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
cancelComposer(differentDraft = false) {
|
cancelComposer() {
|
||||||
this.skipAutoSave = true;
|
this.skipAutoSave = true;
|
||||||
|
|
||||||
if (this._saveDraftDebounce) {
|
if (this._saveDraftDebounce) {
|
||||||
cancel(this._saveDraftDebounce);
|
cancel(this._saveDraftDebounce);
|
||||||
}
|
}
|
||||||
|
|
||||||
let promise = new Promise((resolve, reject) => {
|
let promise = new Promise((resolve) => {
|
||||||
if (this.get("model.hasMetaData") || this.get("model.replyDirty")) {
|
if (this.get("model.hasMetaData") || this.get("model.replyDirty")) {
|
||||||
const controller = showModal("discard-draft", {
|
const controller = showModal("discard-draft", {
|
||||||
model: this.model,
|
model: this.model,
|
||||||
modalClass: "discard-draft-modal",
|
modalClass: "discard-draft-modal",
|
||||||
title: "post.abandon.title",
|
|
||||||
});
|
});
|
||||||
controller.setProperties({
|
controller.setProperties({
|
||||||
differentDraft,
|
|
||||||
onDestroyDraft: () => {
|
onDestroyDraft: () => {
|
||||||
this.destroyDraft()
|
this.destroyDraft()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
|
@ -1118,15 +1113,16 @@ export default Controller.extend({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
onSaveDraft: () => {
|
onSaveDraft: () => {
|
||||||
// cancel composer without destroying draft on new draft context
|
this._saveDraft();
|
||||||
if (differentDraft) {
|
if (this.model.draftKey === Composer.NEW_TOPIC_KEY) {
|
||||||
|
this.currentUser.set("has_topic_draft", true);
|
||||||
|
}
|
||||||
this.model.clearState();
|
this.model.clearState();
|
||||||
this.close();
|
this.close();
|
||||||
resolve();
|
resolve();
|
||||||
}
|
|
||||||
|
|
||||||
reject();
|
|
||||||
},
|
},
|
||||||
|
// needed to resume saving drafts if composer stays open
|
||||||
|
onDismissModal: () => resolve(),
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// it is possible there is some sort of crazy draft with no body ... just give up on it
|
// it is possible there is some sort of crazy draft with no body ... just give up on it
|
||||||
|
|
|
@ -1,40 +1,19 @@
|
||||||
import Controller from "@ember/controller";
|
import Controller from "@ember/controller";
|
||||||
import ModalFunctionality from "discourse/mixins/modal-functionality";
|
import ModalFunctionality from "discourse/mixins/modal-functionality";
|
||||||
import discourseComputed from "discourse-common/utils/decorators";
|
|
||||||
|
|
||||||
export default Controller.extend(ModalFunctionality, {
|
export default Controller.extend(ModalFunctionality, {
|
||||||
differentDraft: null,
|
|
||||||
|
|
||||||
@discourseComputed()
|
|
||||||
keyPrefix() {
|
|
||||||
return this.model.action === "edit" ? "post.abandon_edit" : "post.abandon";
|
|
||||||
},
|
|
||||||
|
|
||||||
@discourseComputed("keyPrefix")
|
|
||||||
descriptionKey(keyPrefix) {
|
|
||||||
return `${keyPrefix}.confirm`;
|
|
||||||
},
|
|
||||||
|
|
||||||
@discourseComputed("keyPrefix")
|
|
||||||
discardKey(keyPrefix) {
|
|
||||||
return `${keyPrefix}.yes_value`;
|
|
||||||
},
|
|
||||||
|
|
||||||
@discourseComputed("keyPrefix", "differentDraft")
|
|
||||||
saveKey(keyPrefix, differentDraft) {
|
|
||||||
return differentDraft
|
|
||||||
? `${keyPrefix}.no_save_draft`
|
|
||||||
: `${keyPrefix}.no_value`;
|
|
||||||
},
|
|
||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
_destroyDraft() {
|
destroyDraft() {
|
||||||
this.onDestroyDraft();
|
this.onDestroyDraft();
|
||||||
this.send("closeModal");
|
this.send("closeModal");
|
||||||
},
|
},
|
||||||
_saveDraft() {
|
saveDraftAndClose() {
|
||||||
this.onSaveDraft();
|
this.onSaveDraft();
|
||||||
this.send("closeModal");
|
this.send("closeModal");
|
||||||
},
|
},
|
||||||
|
dismissModal() {
|
||||||
|
this.onDismissModal();
|
||||||
|
this.send("closeModal");
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,15 +1,6 @@
|
||||||
import NavigationDefaultController from "discourse/controllers/navigation/default";
|
import NavigationDefaultController from "discourse/controllers/navigation/default";
|
||||||
import discourseComputed from "discourse-common/utils/decorators";
|
|
||||||
import { inject } from "@ember/controller";
|
import { inject } from "@ember/controller";
|
||||||
|
|
||||||
export default NavigationDefaultController.extend({
|
export default NavigationDefaultController.extend({
|
||||||
discoveryCategories: inject("discovery/categories"),
|
discoveryCategories: inject("discovery/categories"),
|
||||||
|
|
||||||
@discourseComputed(
|
|
||||||
"discoveryCategories.model",
|
|
||||||
"discoveryCategories.model.draft"
|
|
||||||
)
|
|
||||||
draft() {
|
|
||||||
return this.get("discoveryCategories.model.draft");
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,13 +1,6 @@
|
||||||
import Controller, { inject as controller } from "@ember/controller";
|
import Controller, { inject as controller } from "@ember/controller";
|
||||||
import FilterModeMixin from "discourse/mixins/filter-mode";
|
import FilterModeMixin from "discourse/mixins/filter-mode";
|
||||||
import discourseComputed from "discourse-common/utils/decorators";
|
|
||||||
|
|
||||||
export default Controller.extend(FilterModeMixin, {
|
export default Controller.extend(FilterModeMixin, {
|
||||||
discovery: controller(),
|
discovery: controller(),
|
||||||
discoveryTopics: controller("discovery/topics"),
|
|
||||||
|
|
||||||
@discourseComputed("discoveryTopics.model", "discoveryTopics.model.draft")
|
|
||||||
draft: function () {
|
|
||||||
return this.get("discoveryTopics.model.draft");
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -29,11 +29,6 @@ export default Controller.extend(BulkTopicSelection, FilterModeMixin, {
|
||||||
q: null,
|
q: null,
|
||||||
showInfo: false,
|
showInfo: false,
|
||||||
|
|
||||||
@discourseComputed("list", "list.draft")
|
|
||||||
createTopicLabel(list, listDraft) {
|
|
||||||
return listDraft ? "topic.open_draft" : "topic.create";
|
|
||||||
},
|
|
||||||
|
|
||||||
@discourseComputed(
|
@discourseComputed(
|
||||||
"canCreateTopic",
|
"canCreateTopic",
|
||||||
"category",
|
"category",
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import Composer from "discourse/models/composer";
|
import Composer from "discourse/models/composer";
|
||||||
|
import Draft from "discourse/models/draft";
|
||||||
import Route from "@ember/routing/route";
|
import Route from "@ember/routing/route";
|
||||||
import { once } from "@ember/runloop";
|
import { once } from "@ember/runloop";
|
||||||
import { seenUser } from "discourse/lib/user-presence";
|
import { seenUser } from "discourse/lib/user-presence";
|
||||||
|
@ -42,23 +43,6 @@ const DiscourseRoute = Route.extend({
|
||||||
refreshTitle() {
|
refreshTitle() {
|
||||||
once(this, this._refreshTitleOnce);
|
once(this, this._refreshTitleOnce);
|
||||||
},
|
},
|
||||||
|
|
||||||
clearTopicDraft() {
|
|
||||||
// perhaps re-delegate this to root controller in all cases?
|
|
||||||
// TODO also poison the store so it does not come back from the
|
|
||||||
// dead
|
|
||||||
if (this.get("controller.list.draft")) {
|
|
||||||
this.set("controller.list.draft", null);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.controllerFor("discovery/categories").get("model.draft")) {
|
|
||||||
this.controllerFor("discovery/categories").set("model.draft", null);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.controllerFor("discovery/topics").get("model.draft")) {
|
|
||||||
this.controllerFor("discovery/topics").set("model.draft", null);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
|
||||||
redirectIfLoginRequired() {
|
redirectIfLoginRequired() {
|
||||||
|
@ -68,20 +52,24 @@ const DiscourseRoute = Route.extend({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
openTopicDraft(model) {
|
openTopicDraft() {
|
||||||
const composer = this.controllerFor("composer");
|
const composer = this.controllerFor("composer");
|
||||||
|
|
||||||
if (
|
if (
|
||||||
composer.get("model.action") === Composer.CREATE_TOPIC &&
|
composer.get("model.action") === Composer.CREATE_TOPIC &&
|
||||||
composer.get("model.draftKey") === model.draft_key
|
composer.get("model.draftKey") === Composer.NEW_TOPIC_KEY
|
||||||
) {
|
) {
|
||||||
composer.set("model.composeState", Composer.OPEN);
|
composer.set("model.composeState", Composer.OPEN);
|
||||||
} else {
|
} else {
|
||||||
|
Draft.get(Composer.NEW_TOPIC_KEY).then((data) => {
|
||||||
|
if (data.draft) {
|
||||||
composer.open({
|
composer.open({
|
||||||
action: Composer.CREATE_TOPIC,
|
action: Composer.CREATE_TOPIC,
|
||||||
draft: model.draft,
|
draft: data.draft,
|
||||||
draftKey: model.draft_key,
|
draftKey: Composer.NEW_TOPIC_KEY,
|
||||||
draftSequence: model.draft_sequence,
|
draftSequence: data.draft_sequence,
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -118,9 +118,8 @@ const DiscoveryCategoriesRoute = DiscourseRoute.extend(OpenComposer, {
|
||||||
},
|
},
|
||||||
|
|
||||||
createTopic() {
|
createTopic() {
|
||||||
const model = this.controllerFor("discovery/categories").get("model");
|
if (this.get("currentUser.has_topic_draft")) {
|
||||||
if (model.draft) {
|
this.openTopicDraft();
|
||||||
this.openTopicDraft(model);
|
|
||||||
} else {
|
} else {
|
||||||
this.openComposer(this.controllerFor("discovery/categories"));
|
this.openComposer(this.controllerFor("discovery/categories"));
|
||||||
}
|
}
|
||||||
|
|
|
@ -59,9 +59,8 @@ export default DiscourseRoute.extend(OpenComposer, {
|
||||||
},
|
},
|
||||||
|
|
||||||
createTopic() {
|
createTopic() {
|
||||||
const model = this.controllerFor("discovery/topics").get("model");
|
if (this.get("currentUser.has_topic_draft")) {
|
||||||
if (model.draft) {
|
this.openTopicDraft();
|
||||||
this.openTopicDraft(model);
|
|
||||||
} else {
|
} else {
|
||||||
this.openComposer(this.controllerFor("discovery/topics"));
|
this.openComposer(this.controllerFor("discovery/topics"));
|
||||||
}
|
}
|
||||||
|
|
|
@ -180,11 +180,10 @@ export default DiscourseRoute.extend(FilterModeMixin, {
|
||||||
},
|
},
|
||||||
|
|
||||||
createTopic() {
|
createTopic() {
|
||||||
const controller = this.controllerFor("tag.show");
|
if (this.get("currentUser.has_topic_draft")) {
|
||||||
|
this.openTopicDraft();
|
||||||
if (controller.get("list.draft")) {
|
|
||||||
this.openTopicDraft(controller.get("list"));
|
|
||||||
} else {
|
} else {
|
||||||
|
const controller = this.controllerFor("tag.show");
|
||||||
const composerController = this.controllerFor("composer");
|
const composerController = this.controllerFor("composer");
|
||||||
composerController
|
composerController
|
||||||
.open({
|
.open({
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<div class="modal-outer-container">
|
<div class="modal-outer-container">
|
||||||
<div class="modal-middle-container">
|
<div class="modal-middle-container">
|
||||||
<div class="modal-inner-container">
|
<div class="modal-inner-container">
|
||||||
<div class="modal-header">
|
<div class="modal-header {{headerClass}}">
|
||||||
{{#if dismissable}}
|
{{#if dismissable}}
|
||||||
{{d-button icon="times" action=(route-action "closeModal" "initiatedByCloseButton") class="btn-flat modal-close close" title="modal.close"}}
|
{{d-button icon="times" action=(route-action "closeModal" "initiatedByCloseButton") class="btn-flat modal-close close" title="modal.close"}}
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
@ -23,7 +23,8 @@
|
||||||
panel=panel
|
panel=panel
|
||||||
panelsLength=panels.length
|
panelsLength=panels.length
|
||||||
selectedPanel=selectedPanel
|
selectedPanel=selectedPanel
|
||||||
onSelectPanel=onSelectPanel}}
|
onSelectPanel=onSelectPanel
|
||||||
|
}}
|
||||||
{{/each}}
|
{{/each}}
|
||||||
</ul>
|
</ul>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
{{#d-modal-body}}
|
{{#d-modal-body dismissable=false headerClass="empty"}}
|
||||||
<div class="instructions">
|
<div class="instructions">
|
||||||
{{i18n descriptionKey}}
|
{{i18n "post.cancel_composer.confirm"}}
|
||||||
</div>
|
</div>
|
||||||
{{/d-modal-body}}
|
{{/d-modal-body}}
|
||||||
|
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
{{d-button label=discardKey class="btn-danger" action=(action "_destroyDraft")}}
|
{{d-button icon="far-trash-alt" label="post.cancel_composer.discard" class="btn-danger" action=(action "destroyDraft")}}
|
||||||
{{d-button label=saveKey action=(action "_saveDraft")}}
|
{{d-button label="post.cancel_composer.save_draft" class="save-draft" action=(action "saveDraftAndClose")}}
|
||||||
|
{{d-button label="post.cancel_composer.keep_editing" class="keep-editing" action=(action "dismissModal")}}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -5,6 +5,6 @@
|
||||||
createCategory=(route-action "createCategory")
|
createCategory=(route-action "createCategory")
|
||||||
reorderCategories=(route-action "reorderCategories")
|
reorderCategories=(route-action "reorderCategories")
|
||||||
canCreateTopic=canCreateTopic
|
canCreateTopic=canCreateTopic
|
||||||
hasDraft=draft
|
hasDraft=currentUser.has_topic_draft
|
||||||
createTopic=(route-action "createTopic")}}
|
createTopic=(route-action "createTopic")}}
|
||||||
{{/d-section}}
|
{{/d-section}}
|
||||||
|
|
|
@ -24,7 +24,7 @@
|
||||||
canCreateTopic=canCreateTopic
|
canCreateTopic=canCreateTopic
|
||||||
createTopic=(route-action "createTopic")
|
createTopic=(route-action "createTopic")
|
||||||
createTopicDisabled=cannotCreateTopicOnCategory
|
createTopicDisabled=cannotCreateTopicOnCategory
|
||||||
hasDraft=draft
|
hasDraft=currentUser.has_topic_draft
|
||||||
editCategory=(route-action "editCategory" category)}}
|
editCategory=(route-action "editCategory" category)}}
|
||||||
|
|
||||||
{{plugin-outlet name="category-navigation" args=(hash category=category)}}
|
{{plugin-outlet name="category-navigation" args=(hash category=category)}}
|
||||||
|
|
|
@ -2,6 +2,6 @@
|
||||||
{{d-navigation
|
{{d-navigation
|
||||||
filterMode=filterMode
|
filterMode=filterMode
|
||||||
canCreateTopic=canCreateTopic
|
canCreateTopic=canCreateTopic
|
||||||
hasDraft=draft
|
hasDraft=currentUser.has_topic_draft
|
||||||
createTopic=(route-action "createTopic")}}
|
createTopic=(route-action "createTopic")}}
|
||||||
{{/d-section}}
|
{{/d-section}}
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
{{d-navigation
|
{{d-navigation
|
||||||
filterMode=filterMode
|
filterMode=filterMode
|
||||||
canCreateTopic=canCreateTopic
|
canCreateTopic=canCreateTopic
|
||||||
hasDraft=draft
|
hasDraft=currentUser.has_topic_draft
|
||||||
createTopic=(route-action "createTopic")
|
createTopic=(route-action "createTopic")
|
||||||
category=category
|
category=category
|
||||||
tag=tag
|
tag=tag
|
||||||
|
|
|
@ -291,12 +291,22 @@ acceptance("Composer", function (needs) {
|
||||||
await click(".topic-post:nth-of-type(1) button.show-more-actions");
|
await click(".topic-post:nth-of-type(1) button.show-more-actions");
|
||||||
await click(".topic-post:nth-of-type(1) button.edit");
|
await click(".topic-post:nth-of-type(1) button.edit");
|
||||||
|
|
||||||
await click(".modal-footer button:nth-of-type(2)");
|
await click(".modal-footer button.keep-editing");
|
||||||
|
assert.ok(invisible(".discard-draft-modal.modal"));
|
||||||
assert.ok(!visible(".discard-draft-modal.modal"));
|
|
||||||
assert.equal(
|
assert.equal(
|
||||||
queryAll(".d-editor-input").val(),
|
queryAll(".d-editor-input").val(),
|
||||||
"this is the content of my reply"
|
"this is the content of my reply",
|
||||||
|
"composer does not switch when using Keep Editing button"
|
||||||
|
);
|
||||||
|
|
||||||
|
await click(".topic-post:nth-of-type(1) button.edit");
|
||||||
|
await click(".modal-footer button.save-draft");
|
||||||
|
assert.ok(invisible(".discard-draft-modal.modal"));
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
queryAll(".d-editor-input").val(),
|
||||||
|
queryAll(".topic-post:nth-of-type(1) .cooked > p").text(),
|
||||||
|
"composer has contents of post to be edited"
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -590,10 +600,16 @@ acceptance("Composer", function (needs) {
|
||||||
"it pops up a confirmation dialog"
|
"it pops up a confirmation dialog"
|
||||||
);
|
);
|
||||||
assert.equal(
|
assert.equal(
|
||||||
queryAll(".modal-footer button:nth-of-type(2)").text().trim(),
|
queryAll(".modal-footer button.save-draft").text().trim(),
|
||||||
I18n.t("post.abandon.no_value")
|
I18n.t("post.cancel_composer.save_draft"),
|
||||||
|
"has save draft button"
|
||||||
);
|
);
|
||||||
await click(".modal-footer button:nth-of-type(1)");
|
assert.equal(
|
||||||
|
queryAll(".modal-footer button.keep-editing").text().trim(),
|
||||||
|
I18n.t("post.cancel_composer.keep_editing"),
|
||||||
|
"has keep editing button"
|
||||||
|
);
|
||||||
|
await click(".modal-footer button.save-draft");
|
||||||
assert.equal(
|
assert.equal(
|
||||||
queryAll(".d-editor-input").val().indexOf("This is the second post."),
|
queryAll(".d-editor-input").val().indexOf("This is the second post."),
|
||||||
0,
|
0,
|
||||||
|
@ -615,14 +631,20 @@ acceptance("Composer", function (needs) {
|
||||||
"it pops up a confirmation dialog"
|
"it pops up a confirmation dialog"
|
||||||
);
|
);
|
||||||
assert.equal(
|
assert.equal(
|
||||||
queryAll(".modal-footer button:nth-of-type(2)").text().trim(),
|
queryAll(".modal-footer button.save-draft").text().trim(),
|
||||||
I18n.t("post.abandon.no_save_draft")
|
I18n.t("post.cancel_composer.save_draft"),
|
||||||
|
"has save draft button"
|
||||||
);
|
);
|
||||||
await click(".modal-footer button:nth-of-type(2)");
|
assert.equal(
|
||||||
|
queryAll(".modal-footer button.keep-editing").text().trim(),
|
||||||
|
I18n.t("post.cancel_composer.keep_editing"),
|
||||||
|
"has keep editing button"
|
||||||
|
);
|
||||||
|
await click(".modal-footer button.save-draft");
|
||||||
assert.equal(
|
assert.equal(
|
||||||
queryAll(".d-editor-input").val(),
|
queryAll(".d-editor-input").val(),
|
||||||
"",
|
"",
|
||||||
"it populates the input with the post text"
|
"it clears the composer input"
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -54,8 +54,10 @@
|
||||||
|
|
||||||
.modal-header {
|
.modal-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
&:not(.empty) {
|
||||||
padding: 10px 15px;
|
padding: 10px 15px;
|
||||||
border-bottom: 1px solid var(--primary-low);
|
border-bottom: 1px solid var(--primary-low);
|
||||||
|
}
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
|
|
|
@ -132,6 +132,12 @@ class Draft < ActiveRecord::Base
|
||||||
data if current_sequence == draft_sequence
|
data if current_sequence == draft_sequence
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def self.has_topic_draft(user)
|
||||||
|
return if !user || !user.id || !User.human_user_id?(user.id)
|
||||||
|
|
||||||
|
Draft.where(user_id: user.id, draft_key: NEW_TOPIC).present?
|
||||||
|
end
|
||||||
|
|
||||||
def self.clear(user, key, sequence)
|
def self.clear(user, key, sequence)
|
||||||
return if !user || !user.id || !User.human_user_id?(user.id)
|
return if !user || !user.id || !User.human_user_id?(user.id)
|
||||||
|
|
||||||
|
|
|
@ -51,6 +51,7 @@ class CurrentUserSerializer < BasicUserSerializer
|
||||||
:featured_topic,
|
:featured_topic,
|
||||||
:skip_new_user_tips,
|
:skip_new_user_tips,
|
||||||
:do_not_disturb_until,
|
:do_not_disturb_until,
|
||||||
|
:has_topic_draft,
|
||||||
|
|
||||||
def groups
|
def groups
|
||||||
object.visible_groups.pluck(:id, :name).map { |id, name| { id: id, name: name } }
|
object.visible_groups.pluck(:id, :name).map { |id, name| { id: id, name: name } }
|
||||||
|
@ -238,4 +239,12 @@ class CurrentUserSerializer < BasicUserSerializer
|
||||||
def featured_topic
|
def featured_topic
|
||||||
object.user_profile.featured_topic
|
object.user_profile.featured_topic
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def has_topic_draft
|
||||||
|
true
|
||||||
|
end
|
||||||
|
|
||||||
|
def include_has_topic_draft?
|
||||||
|
Draft.has_topic_draft(object)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -346,9 +346,9 @@ en:
|
||||||
new_private_message: "New private message draft"
|
new_private_message: "New private message draft"
|
||||||
topic_reply: "Draft reply"
|
topic_reply: "Draft reply"
|
||||||
abandon:
|
abandon:
|
||||||
confirm: "You already opened another draft in this topic. Are you sure you want to abandon it?"
|
confirm: "You have a draft in progress for this topic. What would you like to do with it?"
|
||||||
yes_value: "Yes, abandon"
|
yes_value: "Discard"
|
||||||
no_value: "No, keep"
|
no_value: "Resume editing"
|
||||||
|
|
||||||
topic_count_latest:
|
topic_count_latest:
|
||||||
one: "See %{count} new or updated topic"
|
one: "See %{count} new or updated topic"
|
||||||
|
@ -2917,18 +2917,11 @@ en:
|
||||||
attachment_upload_not_allowed_for_new_user: "Sorry, new users can not upload attachments."
|
attachment_upload_not_allowed_for_new_user: "Sorry, new users can not upload attachments."
|
||||||
attachment_download_requires_login: "Sorry, you need to be logged in to download attachments."
|
attachment_download_requires_login: "Sorry, you need to be logged in to download attachments."
|
||||||
|
|
||||||
abandon_edit:
|
cancel_composer:
|
||||||
confirm: "Are you sure you want to discard your changes?"
|
confirm: "What would you like to do with your post?"
|
||||||
no_value: "No, keep"
|
discard: "Discard"
|
||||||
no_save_draft: "No, save draft"
|
save_draft: "Save draft for later"
|
||||||
yes_value: "Yes, discard edit"
|
keep_editing: "Keep editing"
|
||||||
|
|
||||||
abandon:
|
|
||||||
title: "Abandon Draft"
|
|
||||||
confirm: "Are you sure you want to abandon your post?"
|
|
||||||
no_value: "No, keep"
|
|
||||||
no_save_draft: "No, save draft"
|
|
||||||
yes_value: "Yes, abandon"
|
|
||||||
|
|
||||||
via_email: "this post arrived via email"
|
via_email: "this post arrived via email"
|
||||||
via_auto_generated_email: "this post arrived via an auto generated email"
|
via_auto_generated_email: "this post arrived via an auto generated email"
|
||||||
|
|
|
@ -138,4 +138,32 @@ RSpec.describe CurrentUserSerializer do
|
||||||
expect(payload[:groups]).to eq([{ id: public_group.id, name: public_group.name }])
|
expect(payload[:groups]).to eq([{ id: public_group.id, name: public_group.name }])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context "#has_topic_draft" do
|
||||||
|
fab!(:user) { Fabricate(:user) }
|
||||||
|
let :serializer do
|
||||||
|
CurrentUserSerializer.new(user, scope: Guardian.new, root: false)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "is not included by default" do
|
||||||
|
payload = serializer.as_json
|
||||||
|
expect(payload).not_to have_key(:has_topic_draft)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns true when user has a draft" do
|
||||||
|
Draft.set(user, Draft::NEW_TOPIC, 0, "test1")
|
||||||
|
|
||||||
|
payload = serializer.as_json
|
||||||
|
expect(payload[:has_topic_draft]).to eq(true)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "clearing a draft removes has_topic_draft from payload" do
|
||||||
|
sequence = Draft.set(user, Draft::NEW_TOPIC, 0, "test1")
|
||||||
|
Draft.clear(user, Draft::NEW_TOPIC, sequence)
|
||||||
|
|
||||||
|
payload = serializer.as_json
|
||||||
|
expect(payload).not_to have_key(:has_topic_draft)
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in New Issue