From b326cff7d63f195be57d19abdaaf3908ee7d147a Mon Sep 17 00:00:00 2001 From: David Sevilla Martin <6401250+dsevillamartin@users.noreply.github.com> Date: Fri, 21 Jul 2023 17:06:00 -0400 Subject: [PATCH] Allow voting for multiple options without change perm (#78) * Make help text sticky * Add 'vote' button for multiple votes at once w/o vote change perm * Add poll option to prevent users from changing their vote --- js/src/forum/components/CreatePollModal.js | 15 +++ js/src/forum/components/EditPollModal.js | 2 + js/src/forum/components/PostPoll.js | 144 ++++++++++++++++----- js/src/forum/models/Poll.js | 1 + resources/less/forum.less | 28 +++- resources/locale/en.yml | 5 + src/Access/PollPolicy.php | 2 +- src/Api/Serializers/PollSerializer.php | 11 +- src/Commands/CreatePollHandler.php | 4 +- src/Commands/EditPollHandler.php | 2 +- src/Poll.php | 10 +- 11 files changed, 185 insertions(+), 39 deletions(-) diff --git a/js/src/forum/components/CreatePollModal.js b/js/src/forum/components/CreatePollModal.js index 57fb258c..6eace313 100755 --- a/js/src/forum/components/CreatePollModal.js +++ b/js/src/forum/components/CreatePollModal.js @@ -20,6 +20,7 @@ export default class CreatePollModal extends Modal { this.publicPoll = Stream(false); this.hideVotes = Stream(false); + this.allowChangeVote = Stream(true); this.allowMultipleVotes = Stream(false); this.maxVotes = Stream(0); @@ -39,6 +40,7 @@ export default class CreatePollModal extends Modal { this.question(poll.question); this.publicPoll(poll.publicPoll); this.hideVotes(poll.hideVotes); + this.allowChangeVote(poll.allowChangeVote); this.allowMultipleVotes(poll.allowMultipleVotes); this.maxVotes(poll.maxVotes || 0); @@ -156,6 +158,16 @@ export default class CreatePollModal extends Modal { 20 ); + items.add( + 'allow-change-vote', +
+ + {app.translator.trans('fof-polls.forum.modal.allow_change_vote_label')} + +
, + 20 + ); + items.add( 'allow-multiple-votes',
@@ -256,6 +268,8 @@ export default class CreatePollModal extends Modal { question: this.question(), endDate: this.dateToTimestamp(this.endDate()), publicPoll: this.publicPoll(), + hideVotes: this.hideVotes(), + allowChangeVote: this.allowChangeVote(), allowMultipleVotes: this.allowMultipleVotes(), maxVotes: this.maxVotes(), options: [], @@ -301,6 +315,7 @@ export default class CreatePollModal extends Modal { promise.then(this.hide.bind(this), (err) => { console.error(err); + this.onerror(err); this.loaded(); }); } else { diff --git a/js/src/forum/components/EditPollModal.js b/js/src/forum/components/EditPollModal.js index 0fc47669..23203a35 100755 --- a/js/src/forum/components/EditPollModal.js +++ b/js/src/forum/components/EditPollModal.js @@ -19,6 +19,7 @@ export default class EditPollModal extends CreatePollModal { this.publicPoll = Stream(this.poll.publicPoll()); this.allowMultipleVotes = Stream(this.poll.allowMultipleVotes()); this.hideVotes = Stream(this.poll.hideVotes()); + this.allowChangeVote = Stream(this.poll.allowChangeVote()); this.maxVotes = Stream(this.poll.maxVotes() || 0); if (this.endDate() && dayjs(this.poll.endDate()).isAfter(dayjs())) { @@ -97,6 +98,7 @@ export default class EditPollModal extends CreatePollModal { endDate: this.dateToTimestamp(this.endDate()), publicPoll: this.publicPoll(), hideVotes: this.hideVotes(), + allowChangeVote: this.allowChangeVote(), allowMultipleVotes: this.allowMultipleVotes(), maxVotes: this.maxVotes(), options, diff --git a/js/src/forum/components/PostPoll.js b/js/src/forum/components/PostPoll.js index 83dc2d1f..1ed159ce 100644 --- a/js/src/forum/components/PostPoll.js +++ b/js/src/forum/components/PostPoll.js @@ -5,6 +5,7 @@ import Button from 'flarum/common/components/Button'; import LogInModal from 'flarum/forum/components/LogInModal'; import ListVotersModal from './ListVotersModal'; import classList from 'flarum/common/utils/classList'; +import ItemList from 'flarum/common/utils/ItemList'; import Tooltip from 'flarum/common/components/Tooltip'; import EditPollModal from './EditPollModal'; @@ -13,6 +14,23 @@ export default class PostPoll extends Component { super.oninit(vnode); this.loadingOptions = false; + + this.useSubmitUI = !this.attrs.poll?.canChangeVote() && this.attrs.poll?.allowMultipleVotes(); + this.pendingSubmit = false; + this.pendingOptions = null; + } + + oncreate(vnode) { + super.oncreate(vnode); + + this.preventClose = this.preventClose.bind(this); + window.addEventListener('beforeunload', this.preventClose); + } + + onremove(vnode) { + super.onremove(vnode); + + window.removeEventListener('beforeunload', this.preventClose); } view() { @@ -22,6 +40,8 @@ export default class PostPoll extends Component { if (maxVotes === 0) maxVotes = options.length; + const infoItems = this.infoItems(maxVotes); + return (
@@ -45,41 +65,79 @@ export default class PostPoll extends Component { )}
-
{options.map(this.viewOption.bind(this))}
+
+
{options.map(this.viewOption.bind(this))}
-
- {app.session.user && !poll.canVote() && !poll.hasEnded() && ( - - - {app.translator.trans('fof-polls.forum.no_permission')} - - )} - {poll.endDate() && ( - - - {poll.hasEnded() - ? app.translator.trans('fof-polls.forum.poll_ended') - : app.translator.trans('fof-polls.forum.days_remaining', { time: dayjs(poll.endDate()).fromNow() })} - - )} +
+ {!infoItems.isEmpty() &&
{infoItems.toArray()}
} - {poll.canVote() && ( - - - {app.translator.trans('fof-polls.forum.max_votes_allowed', { max: maxVotes })} - - )} + {this.useSubmitUI && this.pendingSubmit && ( + + )} +
); } + infoItems(maxVotes) { + const items = new ItemList(); + const poll = this.attrs.poll; + const hasVoted = poll.myVotes()?.length > 0; + + if (app.session.user && !poll.canVote() && !poll.hasEnded()) { + items.add( + 'no-permission', + + + {app.translator.trans('fof-polls.forum.no_permission')} + + ); + } + + if (poll.endDate()) { + items.add( + 'end-date', + + + {poll.hasEnded() + ? app.translator.trans('fof-polls.forum.poll_ended') + : app.translator.trans('fof-polls.forum.days_remaining', { time: dayjs(poll.endDate()).fromNow() })} + + ); + } + + if (poll.canVote()) { + items.add( + 'max-votes', + + + {app.translator.trans('fof-polls.forum.max_votes_allowed', { max: maxVotes })} + + ); + + if (!poll.canChangeVote()) { + items.add( + 'cannot-change-vote', + + + {app.translator.trans('fof-polls.forum.poll.cannot_change_vote')} + + ); + } + } + + return items; + } + viewOption(opt) { const poll = this.attrs.poll; const hasVoted = poll.myVotes()?.length > 0; const totalVotes = poll.voteCount(); - const voted = poll.myVotes()?.some?.((vote) => vote.option() === opt); + const voted = this.pendingOptions ? this.pendingOptions.has(opt.id()) : poll.myVotes()?.some?.((vote) => vote.option() === opt); const votes = opt.voteCount(); const percent = totalVotes > 0 ? Math.round((votes / totalVotes) * 100) : 0; @@ -88,9 +146,11 @@ export default class PostPoll extends Component { const isDisabled = this.loadingOptions || (hasVoted && !poll.canChangeVote()); const width = canSeeVoteCount ? percent : (Number(voted) / (poll.myVotes()?.length || 1)) * 100; + const showCheckmark = !app.session.user || (!poll.hasEnded() && poll.canVote() && (!hasVoted || poll.canChangeVote())); + const bar = (
- {((!poll.hasEnded() && poll.canVote()) || !app.session.user) && ( + {showCheckmark && (