Skip to content

Commit d9af83f

Browse files
DEV: Reimplement widget UI as glimmer components (#243)
This aims to be a 1:1 port of functionality and HTML structure. In future, this plugin may benefit from a refactor to use float-kit. Also unskips the system spec and makes improvements to prevent flakiness.
1 parent 983027d commit d9af83f

File tree

14 files changed

+395
-388
lines changed

14 files changed

+395
-388
lines changed
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import Component from "@glimmer/component";
2+
import { tracked } from "@glimmer/tracking";
3+
import { hash } from "@ember/helper";
4+
import { action } from "@ember/object";
5+
import { service } from "@ember/service";
6+
import { htmlSafe } from "@ember/template";
7+
import concatClass from "discourse/helpers/concat-class";
8+
import routeAction from "discourse/helpers/route-action";
9+
import { ajax } from "discourse/lib/ajax";
10+
import { popupAjaxError } from "discourse/lib/ajax-error";
11+
import closeOnClickOutside from "discourse/modifiers/close-on-click-outside";
12+
import { i18n } from "discourse-i18n";
13+
import VoteButton from "./vote-button";
14+
import VoteCount from "./vote-count";
15+
import VoteOptions from "./vote-options";
16+
17+
export default class VoteBox extends Component {
18+
@service siteSettings;
19+
@service currentUser;
20+
21+
@tracked votesAlert;
22+
@tracked allowClick = true;
23+
@tracked initialVote = false;
24+
@tracked showOptions = false;
25+
26+
@action
27+
addVote() {
28+
let topic = this.args.topic;
29+
return ajax("/voting/vote", {
30+
type: "POST",
31+
data: {
32+
topic_id: topic.id,
33+
},
34+
})
35+
.then((result) => {
36+
topic.vote_count = result.vote_count;
37+
topic.user_voted = true;
38+
this.currentUser.votes_exceeded = !result.can_vote;
39+
this.currentUser.votes_left = result.votes_left;
40+
if (result.alert) {
41+
this.votesAlert = result.votes_left;
42+
}
43+
this.allowClick = true;
44+
this.showOptions = false;
45+
})
46+
.catch(popupAjaxError);
47+
}
48+
49+
@action
50+
removeVote() {
51+
const topic = this.args.topic;
52+
53+
return ajax("/voting/unvote", {
54+
type: "POST",
55+
data: {
56+
topic_id: topic.id,
57+
},
58+
})
59+
.then((result) => {
60+
topic.vote_count = result.vote_count;
61+
topic.user_voted = false;
62+
this.currentUser.votes_exceeded = !result.can_vote;
63+
this.currentUser.votes_left = result.votes_left;
64+
this.allowClick = true;
65+
this.showOptions = false;
66+
})
67+
.catch(popupAjaxError);
68+
}
69+
70+
@action
71+
showVoteOptions() {
72+
this.showOptions = true;
73+
}
74+
75+
@action
76+
closeVoteOptions() {
77+
this.showOptions = false;
78+
}
79+
80+
@action
81+
closeVotesAlert() {
82+
this.votesAlert = null;
83+
}
84+
85+
<template>
86+
<div
87+
class={{concatClass
88+
"voting-wrapper"
89+
(if this.siteSettings.topic_voting_show_who_voted "show-pointer")
90+
}}
91+
>
92+
<VoteCount @topic={{@topic}} @showLogin={{routeAction "showLogin"}} />
93+
<VoteButton
94+
@topic={{@topic}}
95+
@allowClick={{this.allowClick}}
96+
@showVoteOptions={{this.showVoteOptions}}
97+
@addVote={{this.addVote}}
98+
@showLogin={{routeAction "showLogin"}}
99+
/>
100+
101+
{{#if this.showOptions}}
102+
<VoteOptions
103+
@topic={{@topic}}
104+
@removeVote={{this.removeVote}}
105+
{{closeOnClickOutside this.closeVoteOptions (hash)}}
106+
/>
107+
{{/if}}
108+
109+
{{#if this.votesAlert}}
110+
<div
111+
class="voting-popup-menu vote-options popup-menu"
112+
{{closeOnClickOutside this.closeVotesAlert (hash)}}
113+
>
114+
{{htmlSafe
115+
(i18n
116+
"topic_voting.votes_left"
117+
count=this.votesAlert
118+
path="/my/activity/votes"
119+
)
120+
}}
121+
</div>
122+
{{/if}}
123+
</div>
124+
</template>
125+
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import Component from "@glimmer/component";
2+
import { action } from "@ember/object";
3+
import { service } from "@ember/service";
4+
import DButton from "discourse/components/d-button";
5+
import cookie from "discourse/lib/cookie";
6+
import { applyBehaviorTransformer } from "discourse/lib/transformer";
7+
import { i18n } from "discourse-i18n";
8+
9+
export default class VoteBox extends Component {
10+
@service siteSettings;
11+
@service currentUser;
12+
13+
get wrapperClasses() {
14+
const classes = [];
15+
const { topic } = this.args;
16+
if (topic.closed) {
17+
classes.push("voting-closed");
18+
} else {
19+
if (!topic.user_voted) {
20+
classes.push("nonvote");
21+
} else {
22+
if (this.currentUser && this.currentUser.votes_exceeded) {
23+
classes.push("vote-limited nonvote");
24+
} else {
25+
classes.push("vote");
26+
}
27+
}
28+
}
29+
if (this.siteSettings.topic_voting_show_who_voted) {
30+
classes.push("show-pointer");
31+
}
32+
return classes.join(" ");
33+
}
34+
35+
get buttonContent() {
36+
const { topic } = this.args;
37+
if (this.currentUser) {
38+
if (topic.closed) {
39+
return i18n("topic_voting.voting_closed_title");
40+
}
41+
42+
if (topic.user_voted) {
43+
return i18n("topic_voting.voted_title");
44+
}
45+
46+
if (this.currentUser.votes_exceeded) {
47+
return i18n("topic_voting.voting_limit");
48+
}
49+
50+
return i18n("topic_voting.vote_title");
51+
}
52+
53+
if (topic.vote_count) {
54+
return i18n("topic_voting.anonymous_button", {
55+
count: topic.vote_count,
56+
});
57+
}
58+
59+
return i18n("topic_voting.anonymous_button", { count: 1 });
60+
}
61+
62+
@action
63+
click() {
64+
applyBehaviorTransformer("topic-vote-button-click", () => {
65+
if (!this.currentUser) {
66+
cookie("destination_url", window.location.href, { path: "/" });
67+
this.args.showLogin();
68+
return;
69+
}
70+
71+
const { topic } = this.args;
72+
73+
if (
74+
!topic.closed &&
75+
!topic.user_voted &&
76+
!this.currentUser.votes_exceeded
77+
) {
78+
this.args.addVote();
79+
}
80+
81+
if (topic.user_voted || this.currentUser.votes_exceeded) {
82+
this.args.showVoteOptions();
83+
}
84+
});
85+
}
86+
87+
<template>
88+
<div class={{this.wrapperClasses}}>
89+
<DButton
90+
@translatedTitle={{if
91+
this.currentUser
92+
(i18n
93+
"topic_voting.votes_left_button_title"
94+
count=this.currentUser.votes_left
95+
)
96+
""
97+
}}
98+
@translatedLabel={{this.buttonContent}}
99+
class="btn-primary vote-button"
100+
@action={{this.click}}
101+
/>
102+
</div>
103+
</template>
104+
}
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import Component from "@glimmer/component";
2+
import { tracked } from "@glimmer/tracking";
3+
import { hash } from "@ember/helper";
4+
import { on } from "@ember/modifier";
5+
import { action } from "@ember/object";
6+
import { service } from "@ember/service";
7+
import { eq } from "truth-helpers";
8+
import AsyncContent from "discourse/components/async-content";
9+
import SmallUserList from "discourse/components/small-user-list";
10+
import concatClass from "discourse/helpers/concat-class";
11+
import { ajax } from "discourse/lib/ajax";
12+
import cookie from "discourse/lib/cookie";
13+
import { bind } from "discourse/lib/decorators";
14+
import getURL from "discourse/lib/get-url";
15+
import closeOnClickOutside from "discourse/modifiers/close-on-click-outside";
16+
17+
export default class VoteBox extends Component {
18+
@service siteSettings;
19+
@service currentUser;
20+
21+
@tracked showWhoVoted = false;
22+
23+
@bind
24+
async loadWhoVoted() {
25+
return ajax("/voting/who", {
26+
type: "GET",
27+
data: {
28+
topic_id: this.args.topic.id,
29+
},
30+
}).then((users) =>
31+
users.map((user) => {
32+
return {
33+
template: user.avatar_template,
34+
username: user.username,
35+
post_url: user.post_url,
36+
url: getURL("/u/") + user.username.toLowerCase(),
37+
};
38+
})
39+
);
40+
}
41+
42+
@action
43+
click(event) {
44+
event.preventDefault();
45+
event.stopPropagation();
46+
47+
if (!this.currentUser) {
48+
cookie("destination_url", window.location.href, { path: "/" });
49+
this.args.showLogin();
50+
return;
51+
}
52+
53+
if (this.showWhoVoted) {
54+
this.showWhoVoted = false;
55+
} else if (this.siteSettings.topic_voting_show_who_voted) {
56+
this.showWhoVoted = true;
57+
}
58+
}
59+
60+
@action
61+
clickOutside() {
62+
this.showWhoVoted = false;
63+
}
64+
65+
<template>
66+
<div
67+
class={{concatClass
68+
"vote-count-wrapper"
69+
(if (eq @topic.vote_count 0) "no-votes")
70+
}}
71+
{{on "click" this.click}}
72+
role="button"
73+
>
74+
<div class="vote-count">
75+
{{@topic.vote_count}}
76+
</div>
77+
</div>
78+
79+
{{#if this.showWhoVoted}}
80+
<div
81+
class="who-voted popup-menu voting-popup-menu"
82+
{{closeOnClickOutside
83+
this.clickOutside
84+
(hash secondaryTargetSelector=".vote-count-wrapper")
85+
}}
86+
>
87+
<AsyncContent @asyncData={{this.loadWhoVoted}}>
88+
<:content as |voters|>
89+
<SmallUserList @users={{voters}} class="regular-votes" />
90+
</:content>
91+
</AsyncContent>
92+
</div>
93+
{{/if}}
94+
</template>
95+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import Component from "@glimmer/component";
2+
import { on } from "@ember/modifier";
3+
import { service } from "@ember/service";
4+
import icon from "discourse/helpers/d-icon";
5+
import { i18n } from "discourse-i18n";
6+
7+
export default class VoteBox extends Component {
8+
@service currentUser;
9+
10+
<template>
11+
<div class="vote-options voting-popup-menu popup-menu" ...attributes>
12+
{{#if @topic.user_voted}}
13+
<div
14+
role="button"
15+
class="remove-vote vote-option"
16+
{{on "click" @removeVote}}
17+
>
18+
{{icon "xmark"}}
19+
{{i18n "topic_voting.remove_vote"}}
20+
</div>
21+
{{else if this.currentUser.votes_exceeded}}
22+
<div>{{i18n "topic_voting.reached_limit"}}</div>
23+
<p>
24+
<a href="/my/activity/votes">{{i18n "topic_voting.list_votes"}}</a>
25+
</p>
26+
{{/if}}
27+
</div>
28+
</template>
29+
}

assets/javascripts/discourse/connectors/topic-above-post-stream/topic-title-voting.gjs

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import Component from "@ember/component";
22
import { classNames, tagName } from "@ember-decorators/component";
3-
import MountWidget from "discourse/components/mount-widget";
43
import routeAction from "discourse/helpers/route-action";
4+
import VoteBox from "../../components/vote-box";
55

66
@tagName("div")
77
@classNames("topic-above-post-stream-outlet", "topic-title-voting")
@@ -11,10 +11,8 @@ export default class TopicTitleVoting extends Component {
1111
{{#if this.model.postStream.loaded}}
1212
{{#if this.model.postStream.firstPostPresent}}
1313
<div class="voting title-voting">
14-
{{! template-lint-disable no-capital-arguments }}
15-
<MountWidget
16-
@widget="vote-box"
17-
@args={{this.model}}
14+
<VoteBox
15+
@topic={{this.model}}
1816
@showLogin={{routeAction "showLogin"}}
1917
/>
2018
</div>

0 commit comments

Comments
 (0)