DEV: refactor topic-summary widget to topic-map-summary component (#25447)

* shift topic-summary widget to topic-map-summary component
* remove relativeDate memoization which was causing bug where displayed date never updated
This commit is contained in:
Kelv 2024-01-31 22:09:39 +08:00 committed by GitHub
parent ec26dc51cd
commit bfa3e056f1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 212 additions and 225 deletions

View File

@ -3,12 +3,7 @@ import { longDate, relativeAge } from "discourse/lib/formatter";
export default class RelativeDate extends Component {
get datetime() {
if (this.memoizedDatetime) {
return this.memoizedDatetime;
}
this.memoizedDatetime = new Date(this.args.date);
return this.memoizedDatetime;
return new Date(this.args.date);
}
get title() {

View File

@ -0,0 +1,154 @@
import Component from "@glimmer/component";
import { htmlSafe } from "@ember/template";
import DButton from "discourse/components/d-button";
import RelativeDate from "discourse/components/relative-date";
import TopicParticipants from "discourse/components/topic-map/topic-participants";
import number from "discourse/helpers/number";
import slice from "discourse/helpers/slice";
import i18n from "discourse-common/helpers/i18n";
import { avatarImg } from "discourse-common/lib/avatar-utils";
import gt from "truth-helpers/helpers/gt";
export default class TopicMapSummary extends Component {
get toggleMapButton() {
return {
title: this.args.collapsed
? "topic.expand_details"
: "topic.collapse_details",
icon: this.args.collapsed ? "chevron-down" : "chevron-up",
ariaExpanded: this.args.collapsed ? "false" : "true",
ariaControls: "topic-map-expanded",
action: this.args.toggleMap,
};
}
get shouldShowParticipants() {
return (
this.args.collapsed &&
this.args.postAttrs.topicPostsCount > 2 &&
this.args.postAttrs.participants &&
this.args.postAttrs.participants.length > 0
);
}
get createdByAvatar() {
return htmlSafe(
avatarImg({
avatarTemplate: this.args.postAttrs.createdByAvatarTemplate,
size: "tiny",
title:
this.args.postAttrs.createdByName ||
this.args.postAttrs.createdByUsername,
})
);
}
get lastPostAvatar() {
return htmlSafe(
avatarImg({
avatarTemplate: this.args.postAttrs.lastPostAvatarTemplate,
size: "tiny",
title:
this.args.postAttrs.lastPostName ||
this.args.postAttrs.lastPostUsername,
})
);
}
<template>
<nav class="buttons">
<DButton
@icon={{this.toggleMapButton.icon}}
@title={{this.toggleMapButton.title}}
@ariaExpanded={{this.toggleMapButton.ariaExpanded}}
@ariaControls={{this.toggleMapButton.ariaControls}}
@action={{this.toggleMapButton.action}}
class="btn"
/>
</nav>
<ul>
<li class="created-at">
<h4 role="presentation">{{i18n "created_lowercase"}}</h4>
<div class="topic-map-post created-at">
<a
class="trigger-user-card"
data-user-card={{@postAttrs.createdByUsername}}
title={{@postAttrs.createdByUsername}}
aria-hidden="true"
/>
{{this.createdByAvatar}}
<RelativeDate @date={{@postAttrs.topicCreatedAt}} />
</div>
</li>
<li class="last-reply">
<a href={{@postAttrs.lastPostUrl}}>
<h4 role="presentation">{{i18n "last_reply_lowercase"}}</h4>
<div class="topic-map-post last-reply">
<a
class="trigger-user-card"
data-user-card={{@postAttrs.lastPostUsername}}
title={{@postAttrs.lastPostUsername}}
aria-hidden="true"
/>
{{this.lastPostAvatar}}
<RelativeDate @date={{@postAttrs.lastPostAt}} />
</div>
</a>
</li>
<li class="replies">
{{number @postAttrs.topicReplyCount noTitle="true"}}
<h4 role="presentation">{{i18n
"replies_lowercase"
count=@postAttrs.topicReplyCount
}}</h4>
</li>
<li class="secondary views">
{{number
@postAttrs.topicViews
noTitle="true"
class=@postAttrs.topicViewsHeat
}}
<h4 role="presentation">{{i18n
"views_lowercase"
count=@postAttrs.topicViews
}}</h4>
</li>
{{#if (gt @postAttrs.participantCount 0)}}
<li class="secondary users">
{{number @postAttrs.participantCount noTitle="true"}}
<h4 role="presentation">{{i18n
"users_lowercase"
count=@postAttrs.participantCount
}}</h4>
</li>
{{/if}}
{{#if (gt @postAttrs.topicLikeCount 0)}}
<li class="secondary likes">
{{number @postAttrs.topicLikeCount noTitle="true"}}
<h4 role="presentation">{{i18n
"likes_lowercase"
count=@postAttrs.topicLikeCount
}}</h4>
</li>
{{/if}}
{{#if (gt @postAttrs.topicLinkCount 0)}}
<li class="secondary links">
{{number @postAttrs.topicLinkCount noTitle="true"}}
<h4 role="presentation">{{i18n
"links_lowercase"
count=@postAttrs.topicLinkCount
}}</h4>
</li>
{{/if}}
{{#if this.shouldShowParticipants}}
<li class="avatars">
<TopicParticipants
@participants={{slice 0 3 @postAttrs.participants}}
@userFilters={{@postAttrs.userFilters}}
/>
</li>
{{/if}}
</ul>
</template>
}

View File

@ -1,9 +1,7 @@
import { htmlSafe } from "@ember/template";
import { hbs } from "ember-cli-htmlbars";
import { h } from "virtual-dom";
import { dateNode, numberNode } from "discourse/helpers/node";
import { replaceEmoji } from "discourse/widgets/emoji";
import { avatarFor } from "discourse/widgets/post";
import RenderGlimmer from "discourse/widgets/render-glimmer";
import { createWidget } from "discourse/widgets/widget";
import I18n from "discourse-i18n";
@ -46,170 +44,6 @@ createWidget("topic-map-show-links", {
},
});
createWidget("topic-map-summary", {
tagName: "section.map",
buildClasses(attrs, state) {
if (state.collapsed) {
return "map-collapsed";
}
},
html(attrs, state) {
const contents = [];
contents.push(
h("li.created-at", [
h(
"h4",
{
attributes: { role: "presentation" },
},
I18n.t("created_lowercase")
),
h("div.topic-map-post.created-at", [
avatarFor("tiny", {
username: attrs.createdByUsername,
template: attrs.createdByAvatarTemplate,
name: attrs.createdByName,
}),
dateNode(attrs.topicCreatedAt),
]),
])
);
contents.push(
h(
"li.last-reply",
h("a", { attributes: { href: attrs.lastPostUrl } }, [
h(
"h4",
{
attributes: { role: "presentation" },
},
I18n.t("last_reply_lowercase")
),
h("div.topic-map-post.last-reply", [
avatarFor("tiny", {
username: attrs.lastPostUsername,
template: attrs.lastPostAvatarTemplate,
name: attrs.lastPostName,
}),
dateNode(attrs.lastPostAt),
]),
])
)
);
contents.push(
h("li.replies", [
numberNode(attrs.topicReplyCount),
h(
"h4",
{
attributes: { role: "presentation" },
},
I18n.t("replies_lowercase", {
count: attrs.topicReplyCount,
}).toString()
),
])
);
contents.push(
h("li.secondary.views", [
numberNode(attrs.topicViews, { className: attrs.topicViewsHeat }),
h(
"h4",
{
attributes: { role: "presentation" },
},
I18n.t("views_lowercase", { count: attrs.topicViews }).toString()
),
])
);
if (attrs.participantCount > 0) {
contents.push(
h("li.secondary.users", [
numberNode(attrs.participantCount),
h(
"h4",
{
attributes: { role: "presentation" },
},
I18n.t("users_lowercase", {
count: attrs.participantCount,
}).toString()
),
])
);
}
if (attrs.topicLikeCount) {
contents.push(
h("li.secondary.likes", [
numberNode(attrs.topicLikeCount),
h(
"h4",
{
attributes: { role: "presentation" },
},
I18n.t("likes_lowercase", {
count: attrs.topicLikeCount,
}).toString()
),
])
);
}
if (attrs.topicLinkLength > 0) {
contents.push(
h("li.secondary.links", [
numberNode(attrs.topicLinkLength),
h(
"h4",
{
attributes: { role: "presentation" },
},
I18n.t("links_lowercase", {
count: attrs.topicLinkLength,
}).toString()
),
])
);
}
if (
state.collapsed &&
attrs.topicPostsCount > 2 &&
attrs.participants &&
attrs.participants.length > 0
) {
const participants = renderParticipants.call(
this,
"li.avatars",
"",
attrs.userFilters,
attrs.participants.slice(0, 3)
);
contents.push(participants);
}
const nav = h(
"nav.buttons",
this.attach("button", {
title: state.collapsed
? "topic.expand_details"
: "topic.collapse_details",
icon: state.collapsed ? "chevron-down" : "chevron-up",
ariaExpanded: state.collapsed ? "false" : "true",
ariaControls: "topic-map-expanded",
action: "toggleMap",
className: "btn",
})
);
return [nav, h("ul", contents)];
},
});
createWidget("topic-map-link", {
tagName: "a.topic-link.track-link",
@ -326,7 +160,7 @@ export default createWidget("topic-map", {
},
html(attrs, state) {
const contents = [this.attach("topic-map-summary", attrs, { state })];
const contents = [this.buildTopicMapSummary(attrs, state)];
if (!state.collapsed) {
contents.push(this.attach("topic-map-expanded", attrs));
@ -344,6 +178,29 @@ export default createWidget("topic-map", {
toggleMap() {
this.state.collapsed = !this.state.collapsed;
this.scheduleRerender();
},
buildTopicMapSummary(attrs, state) {
const { collapsed } = state;
const wrapperClass = collapsed
? "section.map.map-collapsed"
: "section.map";
return new RenderGlimmer(
this,
wrapperClass,
hbs`<TopicMap::TopicMapSummary
@postAttrs={{@data.postAttrs}}
@toggleMap={{@data.toggleMap}}
@collapsed={{@data.collapsed}}
/>`,
{
toggleMap: this.toggleMap.bind(this),
postAttrs: attrs,
collapsed,
}
);
},
buildSummaryBox(attrs) {

View File

@ -661,29 +661,23 @@ acceptance("Topic stats update automatically", function () {
test("Likes count updates automatically", async function (assert) {
await visit("/t/internationalization-localization/280");
const likesDisplay = query("#post_1 .topic-map .likes .number");
const oldLikes = likesDisplay.textContent;
const likesCountSelectors = "#post_1 .topic-map .likes .number";
const oldLikesCount = query(likesCountSelectors).textContent;
const likesChangedFixture = {
id: 280,
type: "stats",
like_count: 999,
like_count: parseInt(oldLikesCount, 10) + 42,
};
const expectedLikesCount = likesChangedFixture.like_count.toString();
// simulate the topic like_count being changed
await publishToMessageBus("/topic/280", likesChangedFixture);
const newLikes = likesDisplay.textContent;
assert.dom(likesCountSelectors).hasText(expectedLikesCount);
assert.notEqual(
oldLikes,
newLikes,
"it updates the like count on the topic stats"
);
assert.equal(
newLikes,
likesChangedFixture.like_count,
"it updates the like count with the expected value"
oldLikesCount,
expectedLikesCount,
"it updates the likes count on the topic stats"
);
});
@ -703,83 +697,70 @@ acceptance("Topic stats update automatically", function () {
test("Replies count updates automatically", async function (assert) {
await visit("/t/internationalization-localization/280");
const repliesDisplay = query("#post_1 .topic-map .replies .number");
const oldReplies = repliesDisplay.textContent;
const repliesCountSelectors = "#post_1 .topic-map .replies .number";
const oldRepliesCount = query(repliesCountSelectors).textContent;
const expectedRepliesCount = (
postsChangedFixture.posts_count - 1
).toString();
// simulate the topic posts_count being changed
await publishToMessageBus("/topic/280", postsChangedFixture);
const newLikes = repliesDisplay.textContent;
assert.dom(repliesCountSelectors).hasText(expectedRepliesCount);
assert.notEqual(
oldReplies,
newLikes,
oldRepliesCount,
expectedRepliesCount,
"it updates the replies count on the topic stats"
);
assert.equal(
newLikes,
postsChangedFixture.posts_count - 1, // replies = posts_count - 1
"it updates the replies count with the expected value"
);
});
test("Last replier avatar updates automatically", async function (assert) {
await visit("/t/internationalization-localization/280");
const avatarSelectors = "#post_1 .topic-map .last-reply .avatar";
const avatarImg = query(avatarSelectors);
const avatarImg = query("#post_1 .topic-map .last-reply .avatar");
const oldAvatarTitle = avatarImg.title;
const oldAvatarSrc = avatarImg.src;
const expectedAvatarTitle = postsChangedFixture.last_poster.name;
const expectedAvatarSrc = postsChangedFixture.last_poster.avatar_template;
// simulate the topic posts_count being changed
await publishToMessageBus("/topic/280", postsChangedFixture);
const newAvatarTitle = avatarImg.title;
const newAvatarSrc = avatarImg.src;
assert.dom(avatarSelectors).hasAttribute("title", expectedAvatarTitle);
assert.notEqual(
oldAvatarTitle,
newAvatarTitle,
expectedAvatarTitle,
"it updates the last poster avatar title on the topic stats"
);
assert.equal(
newAvatarTitle,
postsChangedFixture.last_poster.name,
"it updates the last poster avatar title with the expected value"
);
assert.dom(avatarSelectors).hasAttribute("src", expectedAvatarSrc);
assert.notEqual(
oldAvatarSrc,
newAvatarSrc,
expectedAvatarSrc,
"it updates the last poster avatar src on the topic stats"
);
assert.equal(
newAvatarSrc,
`${document.location.origin}${postsChangedFixture.last_poster.avatar_template}`,
"it updates the last poster avatar src with the expected value"
);
});
test("Last replied at updates automatically", async function (assert) {
await visit("/t/internationalization-localization/280");
const lastRepliedAtDisplay = query(
"#post_1 .topic-map .last-reply .relative-date"
);
const lastRepliedAtSelectors =
"#post_1 .topic-map .last-reply .relative-date";
const lastRepliedAtDisplay = query(lastRepliedAtSelectors);
const oldTime = lastRepliedAtDisplay.dataset.time;
const expectedTime = Date.parse(
postsChangedFixture.last_posted_at
).toString();
// simulate the topic posts_count being changed
await publishToMessageBus("/topic/280", postsChangedFixture);
const newTime = lastRepliedAtDisplay.dataset.time;
assert.dom(lastRepliedAtSelectors).hasAttribute("data-time", expectedTime);
assert.notEqual(
oldTime,
newTime,
expectedTime,
"it updates the last posted time on the topic stats"
);
assert.equal(
newTime,
new Date(postsChangedFixture.last_posted_at).getTime(),
"it updates the last posted time with the expected value"
);
});
});