Skip to content
This repository was archived by the owner on Jul 22, 2025. It is now read-only.

Commit 5a0c875

Browse files
authored
FEATURE: Show 'marked solved by' in OP when topic is solved (#343)
Depends on: #342 This feature adds the "Marked solved as" information to the solved post appended to OP. Originally, I had moved the widget usage to a [component](https://github.com/discourse/discourse-solved/blob/39baa0be4a889fdbff108e887a677d9a298d27d4/assets/javascripts/discourse/components/solved-post.gjs), but due to "cooking quotes", after some internal discussion (t/95318/25) we will stick to widgets for now as the post-stream gets modernized.
1 parent e0a579e commit 5a0c875

File tree

8 files changed

+141
-56
lines changed

8 files changed

+141
-56
lines changed

assets/javascripts/discourse/components/solved-accept-answer-button.gjs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export default class SolvedAcceptAnswerButton extends Component {
1919

2020
@action
2121
acceptAnswer() {
22-
acceptAnswer(this.args.post, this.appEvents);
22+
acceptAnswer(this.args.post, this.appEvents, this.currentUser);
2323
}
2424

2525
<template>
@@ -34,9 +34,9 @@ export default class SolvedAcceptAnswerButton extends Component {
3434
</template>
3535
}
3636

37-
export function acceptAnswer(post, appEvents) {
37+
export function acceptAnswer(post, appEvents, acceptingUser) {
3838
// TODO (glimmer-post-menu): Remove this exported function and move the code into the button action after the widget code is removed
39-
acceptPost(post);
39+
acceptPost(post, acceptingUser);
4040

4141
appEvents.trigger("discourse-solved:solution-toggled", post);
4242

@@ -46,7 +46,7 @@ export function acceptAnswer(post, appEvents) {
4646
});
4747
}
4848

49-
function acceptPost(post) {
49+
function acceptPost(post, acceptingUser) {
5050
const topic = post.topic;
5151

5252
clearAccepted(topic);
@@ -62,6 +62,8 @@ function acceptPost(post) {
6262
name: post.name,
6363
post_number: post.post_number,
6464
excerpt: post.cooked,
65+
accepter_username: acceptingUser.username,
66+
accepter_name: acceptingUser.name,
6567
});
6668

6769
ajax("/solution/accept", {

assets/javascripts/discourse/initializers/extend-for-solved-button.js

Lines changed: 29 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -36,38 +36,32 @@ function initializeWithApi(api) {
3636
const topic = postModel.topic;
3737
if (topic.accepted_answer) {
3838
const hasExcerpt = !!topic.accepted_answer.excerpt;
39-
40-
const withExcerpt = `
41-
<aside class='quote accepted-answer' data-post="${
42-
topic.get("accepted_answer").post_number
43-
}" data-topic="${topic.id}">
44-
<div class='title'>
45-
${topic.acceptedAnswerHtml} <div class="quote-controls"><\/div>
39+
const excerpt = hasExcerpt
40+
? ` <blockquote> ${topic.accepted_answer.excerpt} </blockquote> `
41+
: "";
42+
const solvedQuote = `
43+
<aside class='quote accepted-answer' data-post="${topic.get("accepted_answer").post_number}" data-topic="${topic.id}">
44+
<div class='title ${hasExcerpt ? "" : "title-only"}'>
45+
<div class="accepted-answer--solver">
46+
${topic.solvedByHtml}
47+
<\/div>
48+
<div class="accepted-answer--accepter">
49+
${topic.accepterHtml}
50+
<\/div>
51+
<div class="quote-controls"><\/div>
4652
</div>
47-
<blockquote>
48-
${topic.accepted_answer.excerpt}
49-
</blockquote>
53+
${excerpt}
5054
</aside>`;
5155

52-
const withoutExcerpt = `
53-
<aside class='quote accepted-answer'>
54-
<div class='title title-only'>
55-
${topic.acceptedAnswerHtml}
56-
</div>
57-
</aside>`;
58-
59-
const cooked = new PostCooked(
60-
{ cooked: hasExcerpt ? withExcerpt : withoutExcerpt },
61-
dec
62-
);
56+
const cooked = new PostCooked({ cooked: solvedQuote }, dec);
6357
return dec.rawHtml(cooked.init());
6458
}
6559
}
6660
}
6761
});
6862

6963
api.attachWidgetAction("post", "acceptAnswer", function () {
70-
acceptAnswer(this.model, this.appEvents);
64+
acceptAnswer(this.model, this.appEvents, this.currentUser);
7165
});
7266

7367
api.attachWidgetAction("post", "unacceptAnswer", function () {
@@ -173,7 +167,7 @@ export default {
173167
initialize() {
174168
Topic.reopen({
175169
// keeping this here cause there is complex localization
176-
acceptedAnswerHtml: computed("accepted_answer", "id", function () {
170+
solvedByHtml: computed("accepted_answer", "id", function () {
177171
const username = this.get("accepted_answer.username");
178172
const name = this.get("accepted_answer.name");
179173
const postNumber = this.get("accepted_answer.post_number");
@@ -196,6 +190,18 @@ export default {
196190
user_path: User.create({ username }).path,
197191
});
198192
}),
193+
accepterHtml: computed("accepted_answer", function () {
194+
const username = this.get("accepted_answer.accepter_username");
195+
const name = this.get("accepted_answer.accepter_name");
196+
const formattedUsername =
197+
this.siteSettings.display_name_on_posts && name
198+
? name
199+
: formatUsername(username);
200+
return i18n("solved.marked_solved_by", {
201+
username: formattedUsername,
202+
username_lower: username.toLowerCase(),
203+
});
204+
}),
199205
});
200206

201207
withPluginApi("2.0.0", (api) => {

assets/stylesheets/solutions.scss

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,37 @@ $solved-color: green;
6262
font-size: 13px;
6363
}
6464

65-
aside.quote .title.title-only {
66-
padding: 12px;
65+
aside.quote.accepted-answer {
66+
.title {
67+
display: flex;
68+
flex-wrap: wrap;
69+
70+
&.title-only {
71+
padding: 12px;
72+
}
73+
}
74+
75+
.accepted-answer--solver {
76+
margin-right: auto;
77+
}
78+
79+
.accepted-answer--accepter {
80+
font-size: var(--font-down-1);
81+
margin-left: auto;
82+
margin-top: auto;
83+
}
84+
85+
@media screen and (max-width: 768px) {
86+
.accepted-answer--accepter {
87+
width: 100%;
88+
margin-top: 0.25em;
89+
order: 3;
90+
}
91+
92+
.quote-controls {
93+
order: 2;
94+
}
95+
}
6796
}
6897

6998
.user-card-metadata-outlet.accepted-answers {

config/locales/client.en.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ en:
3333
no_solved_topics_title: "You haven’t solved any topics yet"
3434
no_solved_topics_title_others: "%{username} has not solved any topics yet"
3535
no_solved_topics_body: "When you provide a helpful reply to a topic, your reply might be selected as the solution by the topic owner or staff."
36+
marked_solved_by: "Marked as solved by <a href data-user-card='%{username_lower}'>%{username}</a></span>"
3637

3738
no_answer:
3839
title: Has your question been answered?

lib/discourse_solved/topic_view_serializer_extension.rb

Lines changed: 27 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -6,43 +6,46 @@ module DiscourseSolved::TopicViewSerializerExtension
66
prepended { attributes :accepted_answer }
77

88
def include_accepted_answer?
9-
SiteSetting.solved_enabled? && accepted_answer_post_id
9+
SiteSetting.solved_enabled? && object.topic.solved.present?
1010
end
1111

1212
def accepted_answer
13-
if info = accepted_answer_post_info
14-
{ post_number: info[0], username: info[1], excerpt: info[2], name: info[3] }
15-
end
13+
accepted_answer_post_info
1614
end
1715

1816
private
1917

2018
def accepted_answer_post_info
21-
post_info =
22-
if post = object.posts.find { |p| p.post_number == accepted_answer_post_id }
23-
[post.post_number, post.user.username, post.cooked, post.user.name]
24-
else
25-
Post
26-
.where(id: accepted_answer_post_id, topic_id: object.topic.id)
27-
.joins(:user)
28-
.pluck("post_number", "username", "cooked", "name")
29-
.first
30-
end
31-
32-
if post_info
33-
post_info[2] = if SiteSetting.solved_quote_length > 0
34-
PrettyText.excerpt(post_info[2], SiteSetting.solved_quote_length, keep_emoji_images: true)
19+
solved = object.topic.solved
20+
answer_post = solved.answer_post
21+
answer_post_user = answer_post.user
22+
accepter = solved.accepter
23+
24+
excerpt =
25+
if SiteSetting.solved_quote_length > 0
26+
PrettyText.excerpt(
27+
answer_post.cooked,
28+
SiteSetting.solved_quote_length,
29+
keep_emoji_images: true,
30+
)
3531
else
3632
nil
3733
end
3834

39-
post_info[3] = nil if !SiteSetting.enable_names || !SiteSetting.display_name_on_posts
40-
41-
post_info
35+
accepted_answer = {
36+
post_number: answer_post.post_number,
37+
username: answer_post_user.username,
38+
name: answer_post_user.name,
39+
accepter_username: accepter.username,
40+
accepter_name: accepter.name,
41+
excerpt:,
42+
}
43+
44+
if !SiteSetting.enable_names || !SiteSetting.display_name_on_posts
45+
accepted_answer[:name] = nil
46+
accepted_answer[:accepter_name] = nil
4247
end
43-
end
4448

45-
def accepted_answer_post_id
46-
object.topic.solved&.answer_post_id
49+
accepted_answer
4750
end
4851
end

spec/requests/topics_controller_spec.rb

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,18 +65,21 @@ def schema_json(answerCount)
6565

6666
it "should include user name in output with the corresponding site setting" do
6767
SiteSetting.display_name_on_posts = true
68-
Fabricate(:solved_topic, topic: topic, answer_post: p2)
68+
accepter = Fabricate(:user)
69+
Fabricate(:solved_topic, topic: topic, answer_post: p2, accepter:)
6970

7071
get "/t/#{topic.slug}/#{topic.id}.json"
7172

7273
expect(response.parsed_body["accepted_answer"]["name"]).to eq(p2.user.name)
7374
expect(response.parsed_body["accepted_answer"]["username"]).to eq(p2.user.username)
75+
expect(response.parsed_body["accepted_answer"]["accepter_name"]).to eq(accepter.name)
76+
expect(response.parsed_body["accepted_answer"]["accepter_username"]).to eq(accepter.username)
7477

7578
# enable_names is default ON, this ensures disabling it also disables names here
7679
SiteSetting.enable_names = false
7780
get "/t/#{topic.slug}/#{topic.id}.json"
7881
expect(response.parsed_body["accepted_answer"]["name"]).to eq(nil)
79-
expect(response.parsed_body["accepted_answer"]["username"]).to eq(p2.user.username)
82+
expect(response.parsed_body["accepted_answer"]["accepter_name"]).to eq(nil)
8083
end
8184

8285
it "should not include user name when site setting is disabled" do

spec/system/solved_spec.rb

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# frozen_string_literal: true
2+
3+
describe "About page", type: :system do
4+
fab!(:admin)
5+
fab!(:solver) { Fabricate(:user) }
6+
fab!(:accepter) { Fabricate(:user) }
7+
fab!(:topic) { Fabricate(:post, user: admin).topic }
8+
fab!(:post1) { Fabricate(:post, topic:, user: solver, cooked: "The answer is 42") }
9+
let(:topic_page) { PageObjects::Pages::Topic.new }
10+
11+
before do
12+
SiteSetting.solved_enabled = true
13+
SiteSetting.allow_solved_on_all_topics = true
14+
SiteSetting.accept_all_solutions_allowed_groups = Group::AUTO_GROUPS[:everyone]
15+
end
16+
17+
it "accepts post as solution and shows in OP" do
18+
sign_in(accepter)
19+
20+
topic_page.visit_topic(topic, post_number: 2)
21+
22+
expect(topic_page).to have_css(".post-action-menu__solved-unaccepted")
23+
24+
find(".post-action-menu__solved-unaccepted").click
25+
26+
expect(topic_page).to have_css(".post-action-menu__solved-accepted")
27+
expect(topic_page.find(".title .accepted-answer--solver")).to have_content(
28+
"Solved by #{solver.username}",
29+
)
30+
expect(topic_page.find(".title .accepted-answer--accepter")).to have_content(
31+
"Marked as solved by #{accepter.username}",
32+
)
33+
expect(topic_page.find("blockquote")).to have_content("The answer is 42")
34+
end
35+
end

test/javascripts/helpers/discourse-solved-helpers.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,12 @@ export const postStreamWithAcceptedAnswerExcerpt = (excerpt) => {
199199
featured_link: null,
200200
topic_timer: null,
201201
message_bus_last_id: 0,
202-
accepted_answer: { post_number: 2, username: "kzh", excerpt },
202+
accepted_answer: {
203+
post_number: 2,
204+
username: "kzh",
205+
excerpt,
206+
accepter_username: "tomtom",
207+
accepter_name: "Tomtom",
208+
},
203209
};
204210
};

0 commit comments

Comments
 (0)