From 200acada1b18ea56934030313dfbe4e9a07d4bb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9rgio=20Saquetim?= Date: Thu, 24 Apr 2025 22:01:00 -0300 Subject: [PATCH 1/5] DEV: Add compatibility with the Glimmer Post Stream --- .../components/solved-accepted-answer.gjs | 96 +++++++++ .../initializers/extend-for-solved-button.gjs | 136 +++++++++++++ .../initializers/extend-for-solved-button.js | 186 ------------------ 3 files changed, 232 insertions(+), 186 deletions(-) create mode 100644 assets/javascripts/discourse/components/solved-accepted-answer.gjs create mode 100644 assets/javascripts/discourse/initializers/extend-for-solved-button.gjs delete mode 100644 assets/javascripts/discourse/initializers/extend-for-solved-button.js diff --git a/assets/javascripts/discourse/components/solved-accepted-answer.gjs b/assets/javascripts/discourse/components/solved-accepted-answer.gjs new file mode 100644 index 00000000..5d53a803 --- /dev/null +++ b/assets/javascripts/discourse/components/solved-accepted-answer.gjs @@ -0,0 +1,96 @@ +import Component from "@glimmer/component"; +import { service } from "@ember/service"; +import { htmlSafe } from "@ember/template"; +import concatClass from "discourse/helpers/concat-class"; +import { iconHTML } from "discourse/lib/icon-library"; +import { formatUsername } from "discourse/lib/utilities"; +import { i18n } from "discourse-i18n"; + +export default class SolvedAcceptedAnswer extends Component { + @service siteSettings; + @service store; + + get topic() { + return this.args.post.topic; + } + + get hasExcerpt() { + return !!this.topic.accepted_answer.excerpt; + } + + get htmlAccepter() { + const username = this.topic.accepted_answer.accepter_username; + const name = this.topic.accepted_answer.accepter_name; + + if (!this.siteSettings.show_who_marked_solved) { + return; + } + + const formattedUsername = + this.siteSettings.display_name_on_posts && name + ? name + : formatUsername(username); + + return htmlSafe( + i18n("solved.marked_solved_by", { + username: formattedUsername, + username_lower: username.toLowerCase(), + }) + ); + } + + get htmlExcerpt() { + return htmlSafe(this.topic.accepted_answer.excerpt); + } + + get htmlSolvedBy() { + const username = this.topic.accepted_answer.username; + const name = this.topic.accepted_answer.name; + const postNumber = this.topic.accepted_answer.post_number; + + if (!username || !postNumber) { + return; + } + + const displayedUser = + this.siteSettings.display_name_on_posts && name + ? name + : formatUsername(username); + + const data = { + icon: iconHTML("square-check", { class: "accepted" }), + username_lower: username.toLowerCase(), + username: displayedUser, + post_path: `${this.topic.url}/${postNumber}`, + post_number: postNumber, + user_path: this.store.createRecord("user", { username }).path, + }; + + return htmlSafe(i18n("solved.accepted_html", data)); + } + + +} diff --git a/assets/javascripts/discourse/initializers/extend-for-solved-button.gjs b/assets/javascripts/discourse/initializers/extend-for-solved-button.gjs new file mode 100644 index 00000000..207d73fb --- /dev/null +++ b/assets/javascripts/discourse/initializers/extend-for-solved-button.gjs @@ -0,0 +1,136 @@ +import Component from "@glimmer/component"; +import { computed } from "@ember/object"; +import { withSilencedDeprecations } from "discourse/lib/deprecated"; +import { iconHTML } from "discourse/lib/icon-library"; +import { withPluginApi } from "discourse/lib/plugin-api"; +import { formatUsername } from "discourse/lib/utilities"; +import Topic from "discourse/models/topic"; +import User from "discourse/models/user"; +import PostCooked from "discourse/widgets/post-cooked"; +import RenderGlimmer from "discourse/widgets/render-glimmer"; +import { i18n } from "discourse-i18n"; +import SolvedAcceptAnswerButton from "../components/solved-accept-answer-button"; +import SolvedAcceptedAnswer from "../components/solved-accepted-answer"; +import SolvedUnacceptAnswerButton from "../components/solved-unaccept-answer-button"; + +function initializeWithApi(api) { + customizePost(api); + customizePostMenu(api); + + if (api.addDiscoveryQueryParam) { + api.addDiscoveryQueryParam("solved", { replace: true, refreshModel: true }); + } +} + +function customizePost(api) { + api.addTrackedPostProperties( + "can_accept_answer", + "can_unaccept_answer", + "accepted_answer", + "topic_accepted_answer" + ); + + api.renderAfterWrapperOutlet( + "post-content-cooked-html", + class extends Component { + static shouldRender(args) { + return args.post.post_number === 1 && args.post.topic.accepted_answer; + } + + + } + ); + + withSilencedDeprecations("discourse.post-stream-widget-overrides", () => + customizeWidgetPost(api) + ); +} + +function customizeWidgetPost(api) { + api.decorateWidget("post-contents:after-cooked", (helper) => { + let post = helper.getModel(); + + if (helper.attrs.post_number === 1 && post?.topic?.accepted_answer) { + return new RenderGlimmer( + helper.widget, + "div", + , + { post } + ); + } + }); +} + +function customizePostMenu(api) { + api.registerValueTransformer( + "post-menu-buttons", + ({ + value: dag, + context: { + post, + firstButtonKey, + secondLastHiddenButtonKey, + lastHiddenButtonKey, + }, + }) => { + let solvedButton; + + if (post.can_accept_answer) { + solvedButton = SolvedAcceptAnswerButton; + } else if (post.accepted_answer) { + solvedButton = SolvedUnacceptAnswerButton; + } + + solvedButton && + dag.add( + "solved", + solvedButton, + post.topic_accepted_answer && !post.accepted_answer + ? { + before: lastHiddenButtonKey, + after: secondLastHiddenButtonKey, + } + : { + before: [ + "assign", // button added by the assign plugin + firstButtonKey, + ], + } + ); + } + ); +} + +export default { + name: "extend-for-solved-button", + initialize() { + withPluginApi("1.34.0", initializeWithApi); + + withPluginApi("0.8.10", (api) => { + api.replaceIcon( + "notification.solved.accepted_notification", + "square-check" + ); + }); + + withPluginApi("0.11.0", (api) => { + api.addAdvancedSearchOptions({ + statusOptions: [ + { + name: i18n("search.advanced.statuses.solved"), + value: "solved", + }, + { + name: i18n("search.advanced.statuses.unsolved"), + value: "unsolved", + }, + ], + }); + }); + + withPluginApi("0.11.7", (api) => { + api.addSearchSuggestion("status:solved"); + api.addSearchSuggestion("status:unsolved"); + }); + }, +}; diff --git a/assets/javascripts/discourse/initializers/extend-for-solved-button.js b/assets/javascripts/discourse/initializers/extend-for-solved-button.js deleted file mode 100644 index fff26d87..00000000 --- a/assets/javascripts/discourse/initializers/extend-for-solved-button.js +++ /dev/null @@ -1,186 +0,0 @@ -import { computed } from "@ember/object"; -import { iconHTML } from "discourse/lib/icon-library"; -import { withPluginApi } from "discourse/lib/plugin-api"; -import { formatUsername } from "discourse/lib/utilities"; -import Topic from "discourse/models/topic"; -import User from "discourse/models/user"; -import PostCooked from "discourse/widgets/post-cooked"; -import { i18n } from "discourse-i18n"; -import SolvedAcceptAnswerButton, { - acceptAnswer, -} from "../components/solved-accept-answer-button"; -import SolvedUnacceptAnswerButton, { - unacceptAnswer, -} from "../components/solved-unaccept-answer-button"; - -function initializeWithApi(api) { - customizePostMenu(api); - - api.includePostAttributes( - "can_accept_answer", - "can_unaccept_answer", - "accepted_answer", - "topic_accepted_answer" - ); - - if (api.addDiscoveryQueryParam) { - api.addDiscoveryQueryParam("solved", { replace: true, refreshModel: true }); - } - - api.decorateWidget("post-contents:after-cooked", (dec) => { - if (dec.attrs.post_number === 1) { - const postModel = dec.getModel(); - if (postModel) { - const topic = postModel.topic; - if (topic.accepted_answer) { - const hasExcerpt = !!topic.accepted_answer.excerpt; - const excerpt = hasExcerpt - ? `
${topic.accepted_answer.excerpt}
` - : ""; - const solvedQuote = ` - `; - - const cooked = new PostCooked({ cooked: solvedQuote }, dec); - return dec.rawHtml(cooked.init()); - } - } - } - }); - - api.attachWidgetAction("post", "acceptAnswer", function () { - acceptAnswer(this.model, this.appEvents, this.currentUser); - }); - - api.attachWidgetAction("post", "unacceptAnswer", function () { - unacceptAnswer(this.model, this.appEvents); - }); -} - -function customizePostMenu(api) { - api.registerValueTransformer( - "post-menu-buttons", - ({ - value: dag, - context: { - post, - firstButtonKey, - secondLastHiddenButtonKey, - lastHiddenButtonKey, - }, - }) => { - let solvedButton; - - if (post.can_accept_answer) { - solvedButton = SolvedAcceptAnswerButton; - } else if (post.accepted_answer) { - solvedButton = SolvedUnacceptAnswerButton; - } - - solvedButton && - dag.add( - "solved", - solvedButton, - post.topic_accepted_answer && !post.accepted_answer - ? { - before: lastHiddenButtonKey, - after: secondLastHiddenButtonKey, - } - : { - before: [ - "assign", // button added by the assign plugin - firstButtonKey, - ], - } - ); - } - ); -} - -export default { - name: "extend-for-solved-button", - initialize() { - Topic.reopen({ - // keeping this here cause there is complex localization - solvedByHtml: computed("accepted_answer", "id", function () { - const username = this.get("accepted_answer.username"); - const name = this.get("accepted_answer.name"); - const postNumber = this.get("accepted_answer.post_number"); - - if (!username || !postNumber) { - return ""; - } - - const displayedUser = - this.siteSettings.display_name_on_posts && name - ? name - : formatUsername(username); - - return i18n("solved.accepted_html", { - icon: iconHTML("square-check", { class: "accepted" }), - username_lower: username.toLowerCase(), - username: displayedUser, - post_path: `${this.url}/${postNumber}`, - post_number: postNumber, - user_path: User.create({ username }).path, - }); - }), - accepterHtml: computed("accepted_answer", function () { - const username = this.get("accepted_answer.accepter_username"); - const name = this.get("accepted_answer.accepter_name"); - if (!this.siteSettings.show_who_marked_solved) { - return ""; - } - const formattedUsername = - this.siteSettings.display_name_on_posts && name - ? name - : formatUsername(username); - return i18n("solved.marked_solved_by", { - username: formattedUsername, - username_lower: username.toLowerCase(), - }); - }), - }); - - withPluginApi("1.34.0", initializeWithApi); - - withPluginApi("0.8.10", (api) => { - api.replaceIcon( - "notification.solved.accepted_notification", - "square-check" - ); - }); - - withPluginApi("0.11.0", (api) => { - api.addAdvancedSearchOptions({ - statusOptions: [ - { - name: i18n("search.advanced.statuses.solved"), - value: "solved", - }, - { - name: i18n("search.advanced.statuses.unsolved"), - value: "unsolved", - }, - ], - }); - }); - - withPluginApi("0.11.7", (api) => { - api.addSearchSuggestion("status:solved"); - api.addSearchSuggestion("status:unsolved"); - }); - }, -}; From 3ebcc092cb7638d54632fd45942d92749c2f686f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9rgio=20Saquetim?= Date: Fri, 25 Apr 2025 00:28:15 -0300 Subject: [PATCH 2/5] Fix failing test --- .../javascripts/discourse/components/solved-accepted-answer.gjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/javascripts/discourse/components/solved-accepted-answer.gjs b/assets/javascripts/discourse/components/solved-accepted-answer.gjs index 5d53a803..84961ce3 100644 --- a/assets/javascripts/discourse/components/solved-accepted-answer.gjs +++ b/assets/javascripts/discourse/components/solved-accepted-answer.gjs @@ -75,7 +75,7 @@ export default class SolvedAcceptedAnswer extends Component { data-post={{this.topic.accepted_answer.post_number}} data-topic={{this.topic.id}} > -
+
{{this.htmlSolvedBy}} From 52aea58a5253f644c700ea886364c4dc6709dafd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9rgio=20Saquetim?= Date: Tue, 29 Apr 2025 19:59:36 -0300 Subject: [PATCH 3/5] Fix the expansion of the accepted answer --- .../components/solved-accepted-answer.gjs | 99 ++++++++++++++++--- assets/stylesheets/solutions.scss | 7 +- 2 files changed, 90 insertions(+), 16 deletions(-) diff --git a/assets/javascripts/discourse/components/solved-accepted-answer.gjs b/assets/javascripts/discourse/components/solved-accepted-answer.gjs index 84961ce3..d34a4df0 100644 --- a/assets/javascripts/discourse/components/solved-accepted-answer.gjs +++ b/assets/javascripts/discourse/components/solved-accepted-answer.gjs @@ -1,7 +1,14 @@ import Component from "@glimmer/component"; +import { tracked } from "@glimmer/tracking"; +import { on } from "@ember/modifier"; +import { action } from "@ember/object"; import { service } from "@ember/service"; import { htmlSafe } from "@ember/template"; +import AsyncContent from "discourse/components/async-content"; +import PostCookedHtml from "discourse/components/post/cooked-html"; import concatClass from "discourse/helpers/concat-class"; +import icon from "discourse/helpers/d-icon"; +import { ajax } from "discourse/lib/ajax"; import { iconHTML } from "discourse/lib/icon-library"; import { formatUsername } from "discourse/lib/utilities"; import { i18n } from "discourse-i18n"; @@ -10,17 +17,27 @@ export default class SolvedAcceptedAnswer extends Component { @service siteSettings; @service store; + @tracked expanded = false; + + get acceptedAnswer() { + return this.topic.accepted_answer; + } + + get quoteId() { + return `accepted-answer-${this.topic.id}-${this.acceptedAnswer.post_number}`; + } + get topic() { return this.args.post.topic; } get hasExcerpt() { - return !!this.topic.accepted_answer.excerpt; + return !!this.acceptedAnswer.excerpt; } get htmlAccepter() { - const username = this.topic.accepted_answer.accepter_username; - const name = this.topic.accepted_answer.accepter_name; + const username = this.acceptedAnswer.accepter_username; + const name = this.acceptedAnswer.accepter_name; if (!this.siteSettings.show_who_marked_solved) { return; @@ -39,14 +56,10 @@ export default class SolvedAcceptedAnswer extends Component { ); } - get htmlExcerpt() { - return htmlSafe(this.topic.accepted_answer.excerpt); - } - get htmlSolvedBy() { - const username = this.topic.accepted_answer.username; - const name = this.topic.accepted_answer.name; - const postNumber = this.topic.accepted_answer.post_number; + const username = this.acceptedAnswer.username; + const name = this.acceptedAnswer.name; + const postNumber = this.acceptedAnswer.post_number; if (!username || !postNumber) { return; @@ -69,13 +82,39 @@ export default class SolvedAcceptedAnswer extends Component { return htmlSafe(i18n("solved.accepted_html", data)); } + @action + toggleExpandedPost() { + if (!this.hasExcerpt) { + return; + } + + this.expanded = !this.expanded; + } + + @action + async loadExpandedAcceptedAnswer(postNumber) { + const acceptedAnswer = await ajax( + `/posts/by_number/${this.topic.id}/${postNumber}` + ); + + return this.store.createRecord("post", acceptedAnswer); + } +