DEV: Add compatibility with the Glimmer Post Stream (#363)
This commit is contained in:
parent
ae01ad30c3
commit
8e72136f54
|
@ -0,0 +1,166 @@
|
|||
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";
|
||||
|
||||
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.acceptedAnswer.excerpt;
|
||||
}
|
||||
|
||||
get htmlAccepter() {
|
||||
const username = this.acceptedAnswer.accepter_username;
|
||||
const name = this.acceptedAnswer.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 htmlSolvedBy() {
|
||||
const username = this.acceptedAnswer.username;
|
||||
const name = this.acceptedAnswer.name;
|
||||
const postNumber = this.acceptedAnswer.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));
|
||||
}
|
||||
|
||||
@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);
|
||||
}
|
||||
|
||||
<template>
|
||||
<aside
|
||||
class="quote accepted-answer"
|
||||
data-post={{this.acceptedAnswer.post_number}}
|
||||
data-topic={{this.topic.id}}
|
||||
data-expanded={{this.expanded}}
|
||||
>
|
||||
{{! template-lint-disable no-invalid-interactive }}
|
||||
<div
|
||||
class={{concatClass
|
||||
"title"
|
||||
(unless this.hasExcerpt "title-only")
|
||||
(if this.hasExcerpt "quote__title--can-toggle-content")
|
||||
}}
|
||||
{{on "click" this.toggleExpandedPost}}
|
||||
>
|
||||
<div class="accepted-answer--solver-accepter">
|
||||
<div class="accepted-answer--solver">
|
||||
{{this.htmlSolvedBy}}
|
||||
</div>
|
||||
<div class="accepted-answer--accepter">
|
||||
{{this.htmlAccepter}}
|
||||
</div>
|
||||
</div>
|
||||
{{#if this.hasExcerpt}}
|
||||
<div class="quote-controls">
|
||||
<button
|
||||
aria-controls={{this.quoteId}}
|
||||
aria-expanded={{this.expanded}}
|
||||
class="quote-toggle btn-flat"
|
||||
type="button"
|
||||
>
|
||||
{{icon
|
||||
(if this.expanded "chevron-up" "chevron-down")
|
||||
title="post.expand_collapse"
|
||||
}}
|
||||
</button>
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{#if this.hasExcerpt}}
|
||||
<blockquote id={{this.quoteId}}>
|
||||
{{#if this.expanded}}
|
||||
<AsyncContent
|
||||
@asyncData={{this.loadExpandedAcceptedAnswer}}
|
||||
@context={{this.acceptedAnswer.post_number}}
|
||||
>
|
||||
<:content as |expandedAnswer|>
|
||||
<div class="expanded-quote" data-post-id={{expandedAnswer.id}}>
|
||||
<PostCookedHtml
|
||||
@post={{expandedAnswer}}
|
||||
@streamElement={{false}}
|
||||
/>
|
||||
</div>
|
||||
</:content>
|
||||
</AsyncContent>
|
||||
{{else}}
|
||||
{{htmlSafe this.acceptedAnswer.excerpt}}
|
||||
{{/if}}
|
||||
</blockquote>
|
||||
{{/if}}
|
||||
</aside>
|
||||
</template>
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
<template><SolvedAcceptedAnswer @post={{@outletArgs.post}} /></template>
|
||||
}
|
||||
);
|
||||
|
||||
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",
|
||||
<template><SolvedAcceptedAnswer @post={{@data.post}} /></template>,
|
||||
{ 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");
|
||||
});
|
||||
},
|
||||
};
|
|
@ -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
|
||||
? ` <blockquote> ${topic.accepted_answer.excerpt} </blockquote> `
|
||||
: "";
|
||||
const solvedQuote = `
|
||||
<aside class='quote accepted-answer' data-post="${topic.get("accepted_answer").post_number}" data-topic="${topic.id}">
|
||||
<div class='title ${hasExcerpt ? "" : "title-only"}'>
|
||||
<div class="accepted-answer--solver-accepter">
|
||||
<div class="accepted-answer--solver">
|
||||
${topic.solvedByHtml}
|
||||
<\/div>
|
||||
<div class="accepted-answer--accepter">
|
||||
${topic.accepterHtml}
|
||||
<\/div>
|
||||
</div>
|
||||
<div class="quote-controls"><\/div>
|
||||
</div>
|
||||
${excerpt}
|
||||
</aside>`;
|
||||
|
||||
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");
|
||||
});
|
||||
},
|
||||
};
|
|
@ -58,10 +58,14 @@ $solved-color: var(--success);
|
|||
}
|
||||
|
||||
aside.quote.accepted-answer {
|
||||
.title {
|
||||
> .title {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
|
||||
&.quote__title--can-toggle-content {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.accepted-answer--solver-accepter {
|
||||
|
|
|
@ -15,22 +15,28 @@ describe "About page", type: :system do
|
|||
SiteSetting.show_who_marked_solved = true
|
||||
end
|
||||
|
||||
it "accepts post as solution and shows in OP" do
|
||||
sign_in(accepter)
|
||||
%w[enabled disabled].each do |value|
|
||||
before { SiteSetting.glimmer_post_stream_mode = value }
|
||||
|
||||
topic_page.visit_topic(topic, post_number: 2)
|
||||
context "when glimmer_post_stream_mode=#{value}" do
|
||||
it "accepts post as solution and shows in OP" do
|
||||
sign_in(accepter)
|
||||
|
||||
expect(topic_page).to have_css(".post-action-menu__solved-unaccepted")
|
||||
topic_page.visit_topic(topic, post_number: 2)
|
||||
|
||||
find(".post-action-menu__solved-unaccepted").click
|
||||
expect(topic_page).to have_css(".post-action-menu__solved-unaccepted")
|
||||
|
||||
expect(topic_page).to have_css(".post-action-menu__solved-accepted")
|
||||
expect(topic_page.find(".title .accepted-answer--solver")).to have_content(
|
||||
"Solved by #{solver.username}",
|
||||
)
|
||||
expect(topic_page.find(".title .accepted-answer--accepter")).to have_content(
|
||||
"Marked as solved by #{accepter.username}",
|
||||
)
|
||||
expect(topic_page.find("blockquote")).to have_content("The answer is 42")
|
||||
find(".post-action-menu__solved-unaccepted").click
|
||||
|
||||
expect(topic_page).to have_css(".post-action-menu__solved-accepted")
|
||||
expect(topic_page.find(".title .accepted-answer--solver")).to have_content(
|
||||
"Solved by #{solver.username}",
|
||||
)
|
||||
expect(topic_page.find(".title .accepted-answer--accepter")).to have_content(
|
||||
"Marked as solved by #{accepter.username}",
|
||||
)
|
||||
expect(topic_page.find("blockquote")).to have_content("The answer is 42")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -8,38 +8,46 @@ import pretender, {
|
|||
import { acceptance } from "discourse/tests/helpers/qunit-helpers";
|
||||
import { postStreamWithAcceptedAnswerExcerpt } from "../helpers/discourse-solved-helpers";
|
||||
|
||||
acceptance("Discourse Solved Plugin", function (needs) {
|
||||
needs.user();
|
||||
["enabled", "disabled"].forEach((postStreamMode) => {
|
||||
acceptance(
|
||||
`Discourse Solved Plugin (glimmer_post_stream_mode = ${postStreamMode})`,
|
||||
function (needs) {
|
||||
needs.settings({
|
||||
glimmer_post_stream_mode: postStreamMode,
|
||||
});
|
||||
needs.user();
|
||||
|
||||
test("A topic with an accepted answer shows an excerpt of the answer, if provided", async function (assert) {
|
||||
pretender.get("/t/11.json", () =>
|
||||
response(postStreamWithAcceptedAnswerExcerpt("this is an excerpt"))
|
||||
);
|
||||
test("A topic with an accepted answer shows an excerpt of the answer, if provided", async function (assert) {
|
||||
pretender.get("/t/11.json", () =>
|
||||
response(postStreamWithAcceptedAnswerExcerpt("this is an excerpt"))
|
||||
);
|
||||
|
||||
pretender.get("/t/12.json", () =>
|
||||
response(postStreamWithAcceptedAnswerExcerpt(null))
|
||||
);
|
||||
pretender.get("/t/12.json", () =>
|
||||
response(postStreamWithAcceptedAnswerExcerpt(null))
|
||||
);
|
||||
|
||||
await visit("/t/with-excerpt/11");
|
||||
assert.dom(".quote blockquote").hasText("this is an excerpt");
|
||||
await visit("/t/with-excerpt/11");
|
||||
assert.dom(".quote blockquote").hasText("this is an excerpt");
|
||||
|
||||
await visit("/t/without-excerpt/12");
|
||||
assert.dom(".quote blockquote").doesNotExist();
|
||||
assert.dom(".quote .title.title-only").exists();
|
||||
});
|
||||
await visit("/t/without-excerpt/12");
|
||||
assert.dom(".quote blockquote").doesNotExist();
|
||||
assert.dom(".quote .title.title-only").exists();
|
||||
});
|
||||
|
||||
test("Full page search displays solved status", async function (assert) {
|
||||
pretender.get("/search", () => {
|
||||
const fixtures = cloneJSON(fixturesByUrl["/search.json"]);
|
||||
fixtures.topics[0].has_accepted_answer = true;
|
||||
return response(fixtures);
|
||||
});
|
||||
test("Full page search displays solved status", async function (assert) {
|
||||
pretender.get("/search", () => {
|
||||
const fixtures = cloneJSON(fixturesByUrl["/search.json"]);
|
||||
fixtures.topics[0].has_accepted_answer = true;
|
||||
return response(fixtures);
|
||||
});
|
||||
|
||||
await visit("/search");
|
||||
await fillIn(".search-query", "discourse");
|
||||
await click(".search-cta");
|
||||
await visit("/search");
|
||||
await fillIn(".search-query", "discourse");
|
||||
await click(".search-cta");
|
||||
|
||||
assert.dom(".fps-topic").exists({ count: 1 }, "has one post");
|
||||
assert.dom(".topic-statuses .solved").exists("shows the right icon");
|
||||
});
|
||||
assert.dom(".fps-topic").exists({ count: 1 }, "has one post");
|
||||
assert.dom(".topic-statuses .solved").exists("shows the right icon");
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue