mirror of
https://github.com/discourse/discourse.git
synced 2025-02-25 18:55:32 -06:00
FEATURE: Multiple Draft Topics (#30790)
Allows users to save multiple topic and personal message drafts, allowing more flexibility around content creation. The "New Topic" button will now always start a fresh topic. Drafts can be resumed from the drafts dropdown menu or using the "My Drafts" link in the sidebar. Since drafts require a unique `draft_key` and `user_id` combination, we have updated the format of the draft key for both topics and personal messages. They will now have a prefix like "new_topic_" or "new_message_" with the timestamp of when the composer was first opened appended.
This commit is contained in:
parent
29e48a6478
commit
c64b5d6d7a
@ -82,32 +82,28 @@ export default class DNavigation extends Component {
|
||||
|
||||
@discourseComputed(
|
||||
"createTopicDisabled",
|
||||
"hasDraft",
|
||||
"categoryReadOnlyBanner",
|
||||
"canCreateTopicOnTag",
|
||||
"tag.id"
|
||||
)
|
||||
createTopicButtonDisabled(
|
||||
createTopicDisabled,
|
||||
hasDraft,
|
||||
categoryReadOnlyBanner,
|
||||
canCreateTopicOnTag,
|
||||
tagId
|
||||
) {
|
||||
if (tagId && !canCreateTopicOnTag) {
|
||||
return true;
|
||||
} else if (categoryReadOnlyBanner && !hasDraft) {
|
||||
} else if (categoryReadOnlyBanner) {
|
||||
return false;
|
||||
}
|
||||
return createTopicDisabled;
|
||||
}
|
||||
|
||||
@discourseComputed("categoryReadOnlyBanner", "hasDraft")
|
||||
createTopicClass(categoryReadOnlyBanner, hasDraft) {
|
||||
@discourseComputed("categoryReadOnlyBanner")
|
||||
createTopicClass(categoryReadOnlyBanner) {
|
||||
let classNames = ["btn-default"];
|
||||
if (hasDraft) {
|
||||
classNames.push("open-draft");
|
||||
} else if (categoryReadOnlyBanner) {
|
||||
if (categoryReadOnlyBanner) {
|
||||
classNames.push("disabled");
|
||||
}
|
||||
return classNames.join(" ");
|
||||
@ -192,7 +188,7 @@ export default class DNavigation extends Component {
|
||||
|
||||
@action
|
||||
clickCreateTopicButton() {
|
||||
if (this.categoryReadOnlyBanner && !this.hasDraft) {
|
||||
if (this.categoryReadOnlyBanner) {
|
||||
this.dialog.alert({ message: htmlSafe(this.categoryReadOnlyBanner) });
|
||||
} else {
|
||||
this.createTopic();
|
||||
|
@ -45,7 +45,6 @@
|
||||
@canCreateTopicOnTag={{@canCreateTopicOnTag}}
|
||||
@createTopic={{@createTopic}}
|
||||
@createTopicDisabled={{@createTopicDisabled}}
|
||||
@hasDraft={{this.currentUser.has_topic_draft}}
|
||||
@draftCount={{this.currentUser.draft_count}}
|
||||
@editCategory={{this.editCategory}}
|
||||
@showCategoryAdmin={{@showCategoryAdmin}}
|
||||
|
@ -190,7 +190,7 @@
|
||||
<DiscourseLinkedText
|
||||
@action={{fn
|
||||
this.composer.openNewTopic
|
||||
(hash category=@category preferDraft=true)
|
||||
(hash category=@category)
|
||||
}}
|
||||
@text="topic.suggest_create_topic"
|
||||
/>
|
||||
|
@ -296,7 +296,6 @@ export default class ReviewableItem extends Component {
|
||||
action: Composer.EDIT,
|
||||
draftKey: post.get("topic.draft_key"),
|
||||
draftSequence: post.get("topic.draft_sequence"),
|
||||
skipDraftCheck: true,
|
||||
skipJumpOnSave: true,
|
||||
};
|
||||
|
||||
|
@ -17,7 +17,6 @@ import dIcon from "discourse/helpers/d-icon";
|
||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||
import ClickTrack from "discourse/lib/click-track";
|
||||
import DiscourseURL from "discourse/lib/url";
|
||||
import { NEW_TOPIC_KEY } from "discourse/models/composer";
|
||||
import Draft from "discourse/models/draft";
|
||||
import Post from "discourse/models/post";
|
||||
import { i18n } from "discourse-i18n";
|
||||
@ -127,10 +126,6 @@ export default class UserStreamComponent extends Component {
|
||||
try {
|
||||
await Draft.clear(draft.draft_key, draft.sequence);
|
||||
this.args.stream.remove(draft);
|
||||
|
||||
if (draft.draft_key === NEW_TOPIC_KEY) {
|
||||
this.currentUser.has_topic_draft = false;
|
||||
}
|
||||
} catch (error) {
|
||||
popupAjaxError(error);
|
||||
}
|
||||
|
@ -26,9 +26,7 @@ export default class CategoriesController extends Controller {
|
||||
|
||||
@action
|
||||
createTopic() {
|
||||
this.composer.openNewTopic({
|
||||
preferDraft: true,
|
||||
});
|
||||
this.composer.openNewTopic();
|
||||
}
|
||||
|
||||
@action
|
||||
|
@ -170,7 +170,6 @@ export default class DiscoveryListController extends Controller {
|
||||
.filter(Boolean)
|
||||
.reject((t) => ["none", "all"].includes(t))
|
||||
.join(","),
|
||||
preferDraft: true,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -1310,7 +1310,7 @@ export default class Composer extends RestModel {
|
||||
return true;
|
||||
}
|
||||
|
||||
saveDraft(user) {
|
||||
saveDraft() {
|
||||
if (!this.canSaveDraft) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
@ -1339,10 +1339,6 @@ export default class Composer extends RestModel {
|
||||
draftConflictUser: result.conflict_user,
|
||||
});
|
||||
} else {
|
||||
if (this.draftKey === NEW_TOPIC_KEY && user) {
|
||||
user.set("has_topic_draft", true);
|
||||
}
|
||||
|
||||
this.setProperties({
|
||||
draftStatus: null,
|
||||
draftConflictUser: null,
|
||||
|
@ -38,13 +38,11 @@ export default class UserDraft extends RestModel {
|
||||
|
||||
@discourseComputed("draft_key")
|
||||
draftType(draftKey) {
|
||||
switch (draftKey) {
|
||||
case NEW_TOPIC_KEY:
|
||||
return i18n("drafts.new_topic");
|
||||
case NEW_PRIVATE_MESSAGE_KEY:
|
||||
return i18n("drafts.new_private_message");
|
||||
default:
|
||||
return false;
|
||||
if (draftKey.startsWith(NEW_TOPIC_KEY)) {
|
||||
return i18n("drafts.new_topic");
|
||||
} else if (draftKey.startsWith(NEW_PRIVATE_MESSAGE_KEY)) {
|
||||
return i18n("drafts.new_private_message");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@ -76,8 +76,8 @@ export default class UserDraftsStream extends RestModel {
|
||||
draft.excerpt = excerpt(cooked.toString(), 300);
|
||||
draft.post_number = draft.data.postId || null;
|
||||
if (
|
||||
draft.draft_key === NEW_PRIVATE_MESSAGE_KEY ||
|
||||
draft.draft_key === NEW_TOPIC_KEY
|
||||
draft.draft_key.startsWith(NEW_PRIVATE_MESSAGE_KEY) ||
|
||||
draft.draft_key.startsWith(NEW_TOPIC_KEY)
|
||||
) {
|
||||
draft.title = draft.data.title;
|
||||
}
|
||||
|
@ -137,7 +137,7 @@ export default class ApplicationRoute extends DiscourseRoute {
|
||||
action: Composer.PRIVATE_MESSAGE,
|
||||
recipients,
|
||||
archetypeId: "private_message",
|
||||
draftKey: Composer.NEW_PRIVATE_MESSAGE_KEY,
|
||||
draftKey: this.composer.privateMessageDraftKey,
|
||||
draftSequence: 0,
|
||||
reply,
|
||||
title,
|
||||
|
@ -38,6 +38,7 @@ import { escapeExpression } from "discourse/lib/utilities";
|
||||
import Category from "discourse/models/category";
|
||||
import Composer, {
|
||||
CREATE_TOPIC,
|
||||
NEW_PRIVATE_MESSAGE_KEY,
|
||||
NEW_TOPIC_KEY,
|
||||
SAVE_ICONS,
|
||||
SAVE_LABELS,
|
||||
@ -138,6 +139,14 @@ export default class ComposerService extends Service {
|
||||
return this.model?.composeState === Composer.OPEN;
|
||||
}
|
||||
|
||||
get topicDraftKey() {
|
||||
return NEW_TOPIC_KEY + "_" + new Date().getTime();
|
||||
}
|
||||
|
||||
get privateMessageDraftKey() {
|
||||
return NEW_PRIVATE_MESSAGE_KEY + "_" + new Date().getTime();
|
||||
}
|
||||
|
||||
@on("init")
|
||||
_setupPreview() {
|
||||
const val = this.site.mobileView
|
||||
@ -577,8 +586,8 @@ export default class ComposerService extends Service {
|
||||
|
||||
if (opts.fallbackToNewTopic) {
|
||||
return await this.open({
|
||||
action: Composer.CREATE_TOPIC,
|
||||
draftKey: Composer.NEW_TOPIC_KEY,
|
||||
action: CREATE_TOPIC,
|
||||
draftKey: this.topicDraftKey,
|
||||
...(opts.openOpts || {}),
|
||||
});
|
||||
}
|
||||
@ -1179,10 +1188,6 @@ export default class ComposerService extends Service {
|
||||
);
|
||||
}
|
||||
|
||||
if (this.get("model.draftKey") === Composer.NEW_TOPIC_KEY) {
|
||||
this.currentUser.set("has_topic_draft", false);
|
||||
}
|
||||
|
||||
if (result.responseJson.route_to) {
|
||||
// TODO: await this:
|
||||
this.destroyDraft();
|
||||
@ -1274,7 +1279,6 @@ export default class ComposerService extends Service {
|
||||
@param {Number} [opts.prioritizedCategoryId]
|
||||
@param {Number} [opts.formTemplateId]
|
||||
@param {String} [opts.draftSequence]
|
||||
@param {Boolean} [opts.skipDraftCheck]
|
||||
@param {Boolean} [opts.skipJumpOnSave] Option to skip navigating to the post when saved in this composer session
|
||||
@param {Boolean} [opts.skipFormTemplate] Option to skip the form template even if configured for the category
|
||||
**/
|
||||
@ -1369,28 +1373,10 @@ export default class ComposerService extends Service {
|
||||
composerModel.setProperties({ unlistTopic: false, whisper: false });
|
||||
}
|
||||
|
||||
// we need a draft sequence for the composer to work
|
||||
if (opts.draftSequence === undefined) {
|
||||
let data = await Draft.get(opts.draftKey);
|
||||
|
||||
if (opts.skipDraftCheck) {
|
||||
data.draft = undefined;
|
||||
} else {
|
||||
data = await this.confirmDraftAbandon(data);
|
||||
}
|
||||
|
||||
opts.draft ||= data.draft;
|
||||
opts.draftSequence = data.draft_sequence;
|
||||
|
||||
await this._setModel(composerModel, opts);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
await this._setModel(composerModel, opts);
|
||||
|
||||
// otherwise, do the draft check async
|
||||
if (!opts.draft && !opts.skipDraftCheck) {
|
||||
if (!opts.draft) {
|
||||
let data = await Draft.get(opts.draftKey);
|
||||
data = await this.confirmDraftAbandon(data);
|
||||
|
||||
@ -1406,49 +1392,19 @@ export default class ComposerService extends Service {
|
||||
}
|
||||
}
|
||||
|
||||
async #openNewTopicDraft() {
|
||||
if (
|
||||
this.model?.action === Composer.CREATE_TOPIC &&
|
||||
this.model?.draftKey === Composer.NEW_TOPIC_KEY
|
||||
) {
|
||||
this.set("model.composeState", Composer.OPEN);
|
||||
} else {
|
||||
const data = await Draft.get(Composer.NEW_TOPIC_KEY);
|
||||
if (data.draft) {
|
||||
return this.open({
|
||||
action: Composer.CREATE_TOPIC,
|
||||
draft: data.draft,
|
||||
draftKey: Composer.NEW_TOPIC_KEY,
|
||||
draftSequence: data.draft_sequence,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
async openNewTopic({
|
||||
title,
|
||||
body,
|
||||
category,
|
||||
tags,
|
||||
formTemplate,
|
||||
preferDraft = false,
|
||||
} = {}) {
|
||||
if (preferDraft && this.currentUser.has_topic_draft) {
|
||||
return this.#openNewTopicDraft();
|
||||
} else {
|
||||
return this.open({
|
||||
prioritizedCategoryId: category?.id,
|
||||
topicCategoryId: category?.id,
|
||||
formTemplateId: formTemplate?.id,
|
||||
topicTitle: title,
|
||||
topicBody: body,
|
||||
topicTags: tags,
|
||||
action: CREATE_TOPIC,
|
||||
draftKey: NEW_TOPIC_KEY,
|
||||
draftSequence: 0,
|
||||
});
|
||||
}
|
||||
async openNewTopic({ title, body, category, tags, formTemplate } = {}) {
|
||||
return this.open({
|
||||
prioritizedCategoryId: category?.id,
|
||||
topicCategoryId: category?.id,
|
||||
formTemplateId: formTemplate?.id,
|
||||
topicTitle: title,
|
||||
topicBody: body,
|
||||
topicTags: tags,
|
||||
action: CREATE_TOPIC,
|
||||
draftKey: this.topicDraftKey,
|
||||
draftSequence: 0,
|
||||
});
|
||||
}
|
||||
|
||||
@action
|
||||
@ -1459,7 +1415,7 @@ export default class ComposerService extends Service {
|
||||
topicTitle: title,
|
||||
topicBody: body,
|
||||
archetypeId: "private_message",
|
||||
draftKey: Composer.NEW_PRIVATE_MESSAGE_KEY,
|
||||
draftKey: this.privateMessageDraftKey,
|
||||
hasGroups,
|
||||
});
|
||||
}
|
||||
@ -1570,10 +1526,6 @@ export default class ComposerService extends Service {
|
||||
return;
|
||||
}
|
||||
|
||||
if (key === Composer.NEW_TOPIC_KEY) {
|
||||
this.currentUser.set("has_topic_draft", false);
|
||||
}
|
||||
|
||||
if (this._saveDraftPromise) {
|
||||
await this._saveDraftPromise;
|
||||
return await this.destroyDraft();
|
||||
@ -1717,12 +1669,10 @@ export default class ComposerService extends Service {
|
||||
}
|
||||
}
|
||||
|
||||
this._saveDraftPromise = this.model
|
||||
.saveDraft(this.currentUser)
|
||||
.finally(() => {
|
||||
this._lastDraftSaved = Date.now();
|
||||
this._saveDraftPromise = null;
|
||||
});
|
||||
this._saveDraftPromise = this.model.saveDraft().finally(() => {
|
||||
this._lastDraftSaved = Date.now();
|
||||
this._saveDraftPromise = null;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -385,12 +385,15 @@ acceptance("Composer Actions With New Topic Draft", function (needs) {
|
||||
needs.hooks.afterEach(() => toggleCheckDraftPopup(false));
|
||||
|
||||
test("shared draft", async function (assert) {
|
||||
updateCurrentUser({ has_topic_draft: true });
|
||||
updateCurrentUser({ draft_count: 1 });
|
||||
stubDraftResponse();
|
||||
toggleCheckDraftPopup(true);
|
||||
|
||||
await visit("/");
|
||||
await click("button.open-draft");
|
||||
await click("button.topic-drafts-menu-trigger");
|
||||
await click(
|
||||
".topic-drafts-menu-content .topic-drafts-item:first-child button"
|
||||
);
|
||||
|
||||
await fillIn(
|
||||
"#reply-title",
|
||||
|
@ -16,10 +16,7 @@ import LinkLookup from "discourse/lib/link-lookup";
|
||||
import { cloneJSON } from "discourse/lib/object";
|
||||
import { withPluginApi } from "discourse/lib/plugin-api";
|
||||
import { translateModKey } from "discourse/lib/utilities";
|
||||
import Composer, {
|
||||
CREATE_TOPIC,
|
||||
NEW_TOPIC_KEY,
|
||||
} from "discourse/models/composer";
|
||||
import Composer, { CREATE_TOPIC } from "discourse/models/composer";
|
||||
import Draft from "discourse/models/draft";
|
||||
import { toggleCheckDraftPopup } from "discourse/services/composer";
|
||||
import TopicFixtures from "discourse/tests/fixtures/topic";
|
||||
@ -27,7 +24,6 @@ import pretender, { response } from "discourse/tests/helpers/create-pretender";
|
||||
import {
|
||||
acceptance,
|
||||
metaModifier,
|
||||
updateCurrentUser,
|
||||
} from "discourse/tests/helpers/qunit-helpers";
|
||||
import selectKit from "discourse/tests/helpers/select-kit-helper";
|
||||
import { i18n } from "discourse-i18n";
|
||||
@ -910,27 +906,6 @@ acceptance("Composer", function (needs) {
|
||||
assert.strictEqual(privateMessageUsers.header().value(), "codinghorror");
|
||||
});
|
||||
|
||||
test("Loads tags and category from draft payload", async function (assert) {
|
||||
updateCurrentUser({ has_topic_draft: true });
|
||||
|
||||
sinon.stub(Draft, "get").resolves({
|
||||
draft:
|
||||
'{"reply":"Hey there","action":"createTopic","title":"Draft topic","categoryId":2,"tags":["fun", "xmark"],"archetypeId":"regular","metaData":null,"composerTime":25269,"typingTime":8100}',
|
||||
draft_sequence: 0,
|
||||
draft_key: NEW_TOPIC_KEY,
|
||||
});
|
||||
|
||||
await visit("/latest");
|
||||
assert.dom("#create-topic").hasText(i18n("topic.create"));
|
||||
|
||||
await click("#create-topic");
|
||||
assert.strictEqual(selectKit(".category-chooser").header().value(), "2");
|
||||
assert.strictEqual(
|
||||
selectKit(".mini-tag-chooser").header().value(),
|
||||
"fun,xmark"
|
||||
);
|
||||
});
|
||||
|
||||
test("Deleting the text content of the first post in a private message", async function (assert) {
|
||||
await visit("/t/34");
|
||||
|
||||
|
@ -296,7 +296,6 @@ export default {
|
||||
day_6_start_time: 480,
|
||||
day_6_end_time: 1020,
|
||||
},
|
||||
has_topic_draft: true,
|
||||
},
|
||||
},
|
||||
"/u/eviltrout/card.json": {
|
||||
|
@ -289,7 +289,6 @@ export default class ComposerActions extends DropdownSelectBoxComponent {
|
||||
options.action = PRIVATE_MESSAGE;
|
||||
options.recipients = recipients.join(",");
|
||||
options.archetypeId = "private_message";
|
||||
options.skipDraftCheck = true;
|
||||
|
||||
this._replyFromExisting(options, _postSnapshot, _topicSnapshot);
|
||||
}
|
||||
@ -297,14 +296,12 @@ export default class ComposerActions extends DropdownSelectBoxComponent {
|
||||
replyToTopicSelected(options) {
|
||||
options.action = REPLY;
|
||||
options.topic = _topicSnapshot;
|
||||
options.skipDraftCheck = true;
|
||||
this._openComposer(options);
|
||||
}
|
||||
|
||||
replyToPostSelected(options) {
|
||||
options.action = REPLY;
|
||||
options.post = _postSnapshot;
|
||||
options.skipDraftCheck = true;
|
||||
this._openComposer(options);
|
||||
}
|
||||
|
||||
@ -326,7 +323,6 @@ export default class ComposerActions extends DropdownSelectBoxComponent {
|
||||
options.action = CREATE_TOPIC;
|
||||
options.categoryId = this.get("composerModel.topic.category.id");
|
||||
options.disableScopedCategory = true;
|
||||
options.skipDraftCheck = true;
|
||||
this._replyFromExisting(options, _postSnapshot, _topicSnapshot);
|
||||
}
|
||||
|
||||
@ -352,7 +348,6 @@ export default class ComposerActions extends DropdownSelectBoxComponent {
|
||||
options.action = PRIVATE_MESSAGE;
|
||||
options.recipients = usernames;
|
||||
options.archetypeId = "private_message";
|
||||
options.skipDraftCheck = true;
|
||||
|
||||
this._replyFromExisting(options, _postSnapshot, _topicSnapshot);
|
||||
}
|
||||
@ -362,7 +357,6 @@ export default class ComposerActions extends DropdownSelectBoxComponent {
|
||||
options.categoryId = this.get("composerModel.categoryId");
|
||||
options.topicTitle = this.get("composerModel.title");
|
||||
options.tags = this.get("composerModel.tags");
|
||||
options.skipDraftCheck = true;
|
||||
this._openComposer(options);
|
||||
}
|
||||
|
||||
|
@ -9,7 +9,7 @@ class Draft < ActiveRecord::Base
|
||||
|
||||
has_many :upload_references, as: :target, dependent: :delete_all
|
||||
|
||||
validates :draft_key, length: { maximum: 25 }
|
||||
validates :draft_key, length: { maximum: 40 }
|
||||
|
||||
after_commit :update_draft_count, on: %i[create destroy]
|
||||
|
||||
@ -131,12 +131,6 @@ class Draft < ActiveRecord::Base
|
||||
data if current_sequence == draft_sequence
|
||||
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)
|
||||
if !user || !user.id || !User.human_user_id?(user.id)
|
||||
raise StandardError.new("user not present")
|
||||
|
@ -450,11 +450,7 @@ class Topic < ActiveRecord::Base
|
||||
end
|
||||
|
||||
def advance_draft_sequence
|
||||
if self.private_message?
|
||||
DraftSequence.next!(user, Draft::NEW_PRIVATE_MESSAGE)
|
||||
else
|
||||
DraftSequence.next!(user, Draft::NEW_TOPIC)
|
||||
end
|
||||
DraftSequence.next!(user, self.draft_key)
|
||||
end
|
||||
|
||||
def ensure_topic_has_a_category
|
||||
|
@ -218,17 +218,16 @@ class UserStat < ActiveRecord::Base
|
||||
|
||||
def self.update_draft_count(user_id = nil)
|
||||
if user_id.present?
|
||||
draft_count, has_topic_draft =
|
||||
DB.query_single <<~SQL, user_id: user_id, new_topic: Draft::NEW_TOPIC
|
||||
draft_count = DB.query_single(<<~SQL, user_id: user_id).first
|
||||
UPDATE user_stats
|
||||
SET draft_count = (SELECT COUNT(*) FROM drafts WHERE user_id = :user_id)
|
||||
WHERE user_id = :user_id
|
||||
RETURNING draft_count, (SELECT 1 FROM drafts WHERE user_id = :user_id AND draft_key = :new_topic)
|
||||
RETURNING draft_count
|
||||
SQL
|
||||
|
||||
MessageBus.publish(
|
||||
"/user-drafts/#{user_id}",
|
||||
{ draft_count: draft_count, has_topic_draft: !!has_topic_draft },
|
||||
{ draft_count: draft_count },
|
||||
user_ids: [user_id],
|
||||
)
|
||||
else
|
||||
|
@ -63,7 +63,6 @@ class CurrentUserSerializer < BasicUserSerializer
|
||||
:ignored_users,
|
||||
:featured_topic,
|
||||
:do_not_disturb_until,
|
||||
:has_topic_draft,
|
||||
:can_review,
|
||||
:draft_count,
|
||||
:pending_posts_count,
|
||||
@ -306,14 +305,6 @@ class CurrentUserSerializer < BasicUserSerializer
|
||||
BasicTopicSerializer.new(object.user_profile.featured_topic, scope: scope, root: false).as_json
|
||||
end
|
||||
|
||||
def has_topic_draft
|
||||
true
|
||||
end
|
||||
|
||||
def include_has_topic_draft?
|
||||
Draft.has_topic_draft(object)
|
||||
end
|
||||
|
||||
def unseen_reviewable_count
|
||||
Reviewable.unseen_reviewable_count(object)
|
||||
end
|
||||
|
@ -372,15 +372,21 @@ RSpec.describe PostCreator do
|
||||
end
|
||||
|
||||
it "clears the draft if advanced_draft is true" do
|
||||
creator = PostCreator.new(user, basic_topic_params.merge(advance_draft: true))
|
||||
Draft.set(user, Draft::NEW_TOPIC, 0, "test")
|
||||
draft_key = Draft::NEW_TOPIC + "_#{Time.now.to_i}"
|
||||
creator = PostCreator.new(user, basic_topic_params.merge(draft_key: draft_key))
|
||||
Draft.set(user, draft_key, 0, "test")
|
||||
expect(Draft.where(user: user).size).to eq(1)
|
||||
expect { creator.create }.to change { Draft.count }.by(-1)
|
||||
end
|
||||
|
||||
it "does not clear the draft if advanced_draft is false" do
|
||||
creator = PostCreator.new(user, basic_topic_params.merge(advance_draft: false))
|
||||
Draft.set(user, Draft::NEW_TOPIC, 0, "test")
|
||||
draft_key = Draft::NEW_TOPIC + "_#{Time.now.to_i}"
|
||||
creator =
|
||||
PostCreator.new(
|
||||
user,
|
||||
basic_topic_params.merge(advance_draft: false, draft_key: draft_key),
|
||||
)
|
||||
Draft.set(user, draft_key, 0, "test")
|
||||
expect(Draft.where(user: user).size).to eq(1)
|
||||
expect { creator.create }.not_to change { Draft.count }
|
||||
end
|
||||
|
@ -3,11 +3,12 @@
|
||||
RSpec.describe DraftSequence do
|
||||
fab!(:user)
|
||||
fab!(:upload)
|
||||
let!(:topic_draft_key) { Draft::NEW_TOPIC + "_0001" }
|
||||
|
||||
describe ".next" do
|
||||
it "should produce next sequence for a key" do
|
||||
expect(DraftSequence.next!(user, "test")).to eq 1
|
||||
expect(DraftSequence.next!(user, "test")).to eq 2
|
||||
expect(DraftSequence.next!(user, topic_draft_key)).to eq 1
|
||||
expect(DraftSequence.next!(user, topic_draft_key)).to eq 2
|
||||
end
|
||||
|
||||
it "should not produce next sequence for non-human user" do
|
||||
@ -18,7 +19,7 @@ RSpec.describe DraftSequence do
|
||||
it "deletes old drafts and associated upload references" do
|
||||
Draft.set(
|
||||
user,
|
||||
Draft::NEW_TOPIC,
|
||||
topic_draft_key,
|
||||
0,
|
||||
{
|
||||
reply: "[#{upload.original_filename}|attachment](#{upload.short_url})",
|
||||
@ -33,7 +34,7 @@ RSpec.describe DraftSequence do
|
||||
}.to_json,
|
||||
)
|
||||
|
||||
expect { DraftSequence.next!(user, Draft::NEW_TOPIC) }.to change { Draft.count }.by(
|
||||
expect { DraftSequence.next!(user, topic_draft_key) }.to change { Draft.count }.by(
|
||||
-1,
|
||||
).and change { UploadReference.count }.by(-1).and change {
|
||||
user.reload.user_stat.draft_count
|
||||
|
@ -2,8 +2,11 @@
|
||||
|
||||
RSpec.describe Draft do
|
||||
fab!(:user)
|
||||
|
||||
fab!(:post)
|
||||
let(:topic_key_1) { Draft::NEW_TOPIC + "_0001" }
|
||||
let(:topic_key_2) { Draft::NEW_TOPIC + "_0002" }
|
||||
let(:pm_key_1) { Draft::NEW_PRIVATE_MESSAGE + "_0001" }
|
||||
let(:pm_key_2) { Draft::NEW_PRIVATE_MESSAGE + "_0002" }
|
||||
|
||||
it { is_expected.to have_many(:upload_references).dependent(:delete_all) }
|
||||
|
||||
@ -62,7 +65,7 @@ RSpec.describe Draft do
|
||||
|
||||
it "uses the user id and key correctly" do
|
||||
Draft.set(user, "test", 0, "data")
|
||||
expect(Draft.get(Fabricate.build(:coding_horror), "test", 0)).to eq nil
|
||||
expect(Draft.get(Fabricate(:user), "test", 0)).to eq nil
|
||||
end
|
||||
|
||||
it "should overwrite draft data correctly" do
|
||||
@ -130,17 +133,15 @@ RSpec.describe Draft do
|
||||
end
|
||||
|
||||
it "can cleanup old drafts" do
|
||||
key = Draft::NEW_TOPIC
|
||||
|
||||
Draft.set(user, key, 0, "draft")
|
||||
Draft.set(user, topic_key_1, 0, "draft")
|
||||
|
||||
Draft.cleanup!
|
||||
expect(Draft.count).to eq 1
|
||||
expect(user.user_stat.draft_count).to eq(1)
|
||||
|
||||
seq = DraftSequence.next!(user, key)
|
||||
seq = DraftSequence.next!(user, topic_key_1)
|
||||
|
||||
Draft.set(user, key, seq, "draft")
|
||||
Draft.set(user, topic_key_1, seq, "updated draft")
|
||||
DraftSequence.update_all("sequence = sequence + 1")
|
||||
|
||||
draft = Draft.first
|
||||
@ -154,7 +155,7 @@ RSpec.describe Draft do
|
||||
expect(user.reload.user_stat.draft_count).to eq(0)
|
||||
expect(UploadReference.count).to eq(0)
|
||||
|
||||
Draft.set(Fabricate(:user), Draft::NEW_TOPIC, 0, "draft")
|
||||
Draft.set(Fabricate(:user), topic_key_1, 0, "draft")
|
||||
|
||||
Draft.cleanup!
|
||||
|
||||
@ -169,22 +170,20 @@ RSpec.describe Draft do
|
||||
end
|
||||
|
||||
it "updates draft count when a draft is created or destroyed" do
|
||||
Draft.set(Fabricate(:user), Draft::NEW_TOPIC, 0, "data")
|
||||
Draft.set(Fabricate(:user), topic_key_1, 0, "data")
|
||||
|
||||
messages =
|
||||
MessageBus.track_publish("/user-drafts/#{user.id}") do
|
||||
Draft.set(user, Draft::NEW_TOPIC, 0, "data")
|
||||
Draft.set(user, topic_key_1, 0, "data")
|
||||
end
|
||||
|
||||
expect(messages.first.data[:draft_count]).to eq(1)
|
||||
expect(messages.first.data[:has_topic_draft]).to eq(true)
|
||||
expect(messages.first.user_ids).to contain_exactly(user.id)
|
||||
|
||||
messages =
|
||||
MessageBus.track_publish("/user-drafts/#{user.id}") { Draft.where(user: user).destroy_all }
|
||||
|
||||
expect(messages.first.data[:draft_count]).to eq(0)
|
||||
expect(messages.first.data[:has_topic_draft]).to eq(false)
|
||||
expect(messages.first.user_ids).to contain_exactly(user.id)
|
||||
end
|
||||
|
||||
@ -213,34 +212,49 @@ RSpec.describe Draft do
|
||||
end
|
||||
end
|
||||
|
||||
describe "key expiry" do
|
||||
it "nukes new topic draft after a topic is created" do
|
||||
Draft.set(user, Draft::NEW_TOPIC, 0, "my draft")
|
||||
_t = Fabricate(:topic, user: user, advance_draft: true)
|
||||
s = DraftSequence.current(user, Draft::NEW_TOPIC)
|
||||
expect(Draft.get(user, Draft::NEW_TOPIC, s)).to eq nil
|
||||
expect(Draft.count).to eq 0
|
||||
describe "multiple drafts" do
|
||||
it "allows new topic draft when a topic draft exists" do
|
||||
Draft.set(user, topic_key_1, 0, "topic draft 1")
|
||||
Draft.set(user, topic_key_2, 0, "topic draft 2")
|
||||
seq_1 = DraftSequence.current(user, topic_key_1)
|
||||
seq_2 = DraftSequence.current(user, topic_key_2)
|
||||
|
||||
expect(Draft.get(user, topic_key_1, seq_1)).to eq "topic draft 1"
|
||||
expect(Draft.get(user, topic_key_2, seq_2)).to eq "topic draft 2"
|
||||
expect(Draft.count).to eq 2
|
||||
end
|
||||
|
||||
it "nukes new pm draft after a pm is created" do
|
||||
Draft.set(user, Draft::NEW_PRIVATE_MESSAGE, 0, "my draft")
|
||||
t =
|
||||
Fabricate(
|
||||
:topic,
|
||||
user: user,
|
||||
archetype: Archetype.private_message,
|
||||
category_id: nil,
|
||||
advance_draft: true,
|
||||
)
|
||||
s = DraftSequence.current(t.user, Draft::NEW_PRIVATE_MESSAGE)
|
||||
expect(Draft.get(user, Draft::NEW_PRIVATE_MESSAGE, s)).to eq nil
|
||||
it "allows new pm draft when a pm draft exists" do
|
||||
Draft.set(user, pm_key_1, 0, "pm draft 1")
|
||||
Draft.set(user, pm_key_2, 0, "pm draft 2")
|
||||
seq_1 = DraftSequence.current(user, pm_key_1)
|
||||
seq_2 = DraftSequence.current(user, pm_key_2)
|
||||
|
||||
expect(Draft.get(user, pm_key_1, seq_1)).to eq "pm draft 1"
|
||||
expect(Draft.get(user, pm_key_2, seq_2)).to eq "pm draft 2"
|
||||
expect(Draft.count).to eq 2
|
||||
end
|
||||
|
||||
it "does not nuke new topic draft after a pm is created" do
|
||||
Draft.set(user, Draft::NEW_TOPIC, 0, "my draft")
|
||||
t = Fabricate(:topic, user: user, archetype: Archetype.private_message, category_id: nil)
|
||||
s = DraftSequence.current(t.user, Draft::NEW_TOPIC)
|
||||
expect(Draft.get(user, Draft::NEW_TOPIC, s)).to eq "my draft"
|
||||
it "allows both topic drafts and PM drafts" do
|
||||
Draft.set(user, topic_key_1, 0, "my topic draft 1")
|
||||
Draft.set(user, topic_key_2, 0, "my topic draft 2")
|
||||
Draft.set(user, pm_key_1, 0, "my pm draft 1")
|
||||
Draft.set(user, pm_key_2, 0, "my pm draft 2")
|
||||
|
||||
new_topic =
|
||||
Fabricate(:topic, user: user, archetype: Archetype.private_message, category_id: nil)
|
||||
new_pm = Fabricate(:private_message_topic, user: user)
|
||||
|
||||
topic_seq_1 = DraftSequence.current(new_topic.user, topic_key_1)
|
||||
topic_seq_2 = DraftSequence.current(new_topic.user, topic_key_2)
|
||||
pm_seq_1 = DraftSequence.current(new_pm.user, pm_key_1)
|
||||
pm_seq_2 = DraftSequence.current(new_pm.user, pm_key_2)
|
||||
|
||||
expect(Draft.get(user, topic_key_1, topic_seq_1)).to eq "my topic draft 1"
|
||||
expect(Draft.get(user, topic_key_2, topic_seq_2)).to eq "my topic draft 2"
|
||||
expect(Draft.get(user, pm_key_1, pm_seq_1)).to eq "my pm draft 1"
|
||||
expect(Draft.get(user, pm_key_2, pm_seq_2)).to eq "my pm draft 2"
|
||||
expect(Draft.count).to eq 4
|
||||
end
|
||||
|
||||
it "nukes the post draft when a post is created" do
|
||||
@ -269,10 +283,11 @@ RSpec.describe Draft do
|
||||
end
|
||||
|
||||
it "increases revision each time you set" do
|
||||
Draft.set(user, "new_topic", 0, "hello")
|
||||
Draft.set(user, "new_topic", 0, "goodbye")
|
||||
Draft.set(user, topic_key_1, 0, "hello")
|
||||
Draft.set(user, topic_key_1, 0, "goodbye")
|
||||
|
||||
expect(Draft.find_by(user_id: user.id, draft_key: "new_topic").revisions).to eq(2)
|
||||
expect(Draft.count).to eq 1
|
||||
expect(Draft.find_by(user_id: user.id, draft_key: topic_key_1).revisions).to eq(2)
|
||||
end
|
||||
|
||||
it "handles owner switching gracefully" do
|
||||
@ -306,5 +321,5 @@ RSpec.describe Draft do
|
||||
end
|
||||
end
|
||||
|
||||
it { is_expected.to validate_length_of(:draft_key).is_at_most(25) }
|
||||
it { is_expected.to validate_length_of(:draft_key).is_at_most(40) }
|
||||
end
|
||||
|
@ -128,28 +128,6 @@ RSpec.describe CurrentUserSerializer do
|
||||
end
|
||||
end
|
||||
|
||||
describe "#has_topic_draft" do
|
||||
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
|
||||
|
||||
describe "#can_ignore_users" do
|
||||
let(:guardian) { Guardian.new(user) }
|
||||
let(:payload) { serializer.as_json }
|
||||
|
@ -81,41 +81,17 @@ describe "Composer - discard draft modal", type: :system do
|
||||
it "destroys draft" do
|
||||
visit "/new-topic"
|
||||
|
||||
composer.fill_title("this is a test topic")
|
||||
composer.fill_content("a b c d e f g")
|
||||
|
||||
wait_for(timeout: 5) { Draft.count == 1 }
|
||||
|
||||
composer.close
|
||||
|
||||
expect(discard_draft_modal).to be_open
|
||||
discard_draft_modal.click_save
|
||||
discard_draft_modal.click_discard
|
||||
|
||||
wait_for(timeout: 5) { Draft.last != nil }
|
||||
draft_key = Draft.last.draft_key
|
||||
|
||||
visit "/new-topic"
|
||||
|
||||
expect(dialog).to be_open
|
||||
expect(page).to have_content(I18n.t("js.drafts.abandon.confirm"))
|
||||
dialog.click_danger
|
||||
|
||||
wait_for(timeout: 5) { Draft.find_by(draft_key: draft_key) == nil }
|
||||
end
|
||||
|
||||
it "resumes draft when using /new-message" do
|
||||
visit "/new-message"
|
||||
|
||||
composer.fill_content("a b c d e f g")
|
||||
composer.close
|
||||
|
||||
expect(discard_draft_modal).to be_open
|
||||
discard_draft_modal.click_save
|
||||
|
||||
visit "/new-message"
|
||||
|
||||
expect(dialog).to be_open
|
||||
expect(page).to have_content(I18n.t("js.drafts.abandon.confirm"))
|
||||
dialog.click_button I18n.t("js.drafts.abandon.no_value")
|
||||
|
||||
expect(composer).to be_opened
|
||||
expect(composer).to have_content("a b c d e f g")
|
||||
wait_for(timeout: 5) { Draft.count == 0 }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
Loading…
Reference in New Issue
Block a user