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:
David Battersby
2025-01-29 10:23:26 +04:00
committed by GitHub
parent 29e48a6478
commit c64b5d6d7a
25 changed files with 132 additions and 275 deletions

View File

@ -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

View File

@ -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