diff options
-rw-r--r-- | appinfo/routes.php | 1 | ||||
-rw-r--r-- | lib/Controller/PollController.php | 12 | ||||
-rw-r--r-- | lib/Controller/ShareController.php | 24 | ||||
-rw-r--r-- | lib/Model/Acl.php | 40 | ||||
-rw-r--r-- | lib/Service/ShareService.php | 30 | ||||
-rw-r--r-- | src/js/App.vue | 2 | ||||
-rw-r--r-- | src/js/components/Base/PersonalLink.vue | 92 | ||||
-rw-r--r-- | src/js/components/Base/PublicRegisterModal.vue (renamed from src/js/components/VoteTable/VoteHeaderPublic.vue) | 146 | ||||
-rw-r--r-- | src/js/components/Create/CreateDlg.vue | 4 | ||||
-rw-r--r-- | src/js/components/SideBar/SideBarTabConfiguration.vue | 2 | ||||
-rw-r--r-- | src/js/components/SideBar/SideBarTabShare.vue | 4 | ||||
-rw-r--r-- | src/js/components/Subscription/Subscription.vue | 4 | ||||
-rw-r--r-- | src/js/store/modules/poll.js | 13 | ||||
-rw-r--r-- | src/js/store/modules/subModules/share.js | 84 | ||||
-rw-r--r-- | src/js/store/modules/subModules/shares.js | 4 | ||||
-rw-r--r-- | src/js/views/Vote.vue | 40 |
16 files changed, 348 insertions, 154 deletions
diff --git a/appinfo/routes.php b/appinfo/routes.php index a2ba77ad..b4a23685 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -56,6 +56,7 @@ return [ // ['name' => 'vote#getByToken', 'url' => '/votes/get/s/{token}', 'verb' => 'GET'], ['name' => 'share#add', 'url' => '/share/add', 'verb' => 'POST'], + ['name' => 'share#get', 'url' => '/share/{token}', 'verb' => 'GET'], ['name' => 'share#personal', 'url' => '/share/personal', 'verb' => 'POST'], ['name' => 'share#delete', 'url' => '/share/delete/{token}', 'verb' => 'DELETE'], ['name' => 'share#sendInvitation', 'url' => '/share/send/{token}', 'verb' => 'POST'], diff --git a/lib/Controller/PollController.php b/lib/Controller/PollController.php index a564fe40..494cb59a 100644 --- a/lib/Controller/PollController.php +++ b/lib/Controller/PollController.php @@ -151,8 +151,15 @@ class PollController extends Controller { } try { - $shares = $this->shareService->list($pollId, $token); + if ($token) { + $share = $this->shareService->get($token); + $shares = []; + } else { + $share = null; + $shares = $this->shareService->list($pollId, $token); + } } catch (Exception $e) { + $share = null; $shares = []; } @@ -161,8 +168,9 @@ class PollController extends Controller { 'poll' => $poll, 'comments' => $comments, 'options' => $options, + 'share' => $share, 'shares' => $shares, - 'votes' => $votes + 'votes' => $votes, ], Http::STATUS_OK); } diff --git a/lib/Controller/ShareController.php b/lib/Controller/ShareController.php index 9cd5f554..0c50b4c7 100644 --- a/lib/Controller/ShareController.php +++ b/lib/Controller/ShareController.php @@ -94,6 +94,26 @@ class ShareController extends Controller { * @param string $userEmail * @return DataResponse */ + public function get($token) { + try { + return new DataResponse(['share' => $this->shareService->get($token)], Http::STATUS_CREATED); + } catch (NotAuthorizedException $e) { + return new DataResponse(['error' => $e->getMessage()], $e->getStatus()); + } catch (\Exception $e) { + return new DataResponse($e, Http::STATUS_CONFLICT); + } + } + + /** + * Add share + * @NoAdminRequired + * @param int $pollId + * @param int $pollId + * @param string $type + * @param string $userId + * @param string $userEmail + * @return DataResponse + */ public function setEmailAddress($token, $userEmail) { try { return new DataResponse(['share' => $this->shareService->setEmailAddress($token, $userEmail)], Http::STATUS_OK); @@ -115,10 +135,10 @@ class ShareController extends Controller { * @param string $userName * @return DataResponse */ - public function personal($token, $userName) { + public function personal($token, $userName, $emailAddress = '') { try { - return new DataResponse($this->shareService->personal($token, $userName), Http::STATUS_CREATED); + return new DataResponse($this->shareService->personal($token, $userName, $emailAddress), Http::STATUS_CREATED); } catch (NotAuthorizedException $e) { return new DataResponse(['error' => $e->getMessage()], $e->getStatus()); } catch (InvalidUsername $e) { diff --git a/lib/Model/Acl.php b/lib/Model/Acl.php index ee7b627b..86743c67 100644 --- a/lib/Model/Acl.php +++ b/lib/Model/Acl.php @@ -75,6 +75,9 @@ class Acl implements JsonSerializable { /** @var Poll */ private $poll; + /** @var Share */ + private $share; + /** * Acl constructor. * @param string $appName @@ -84,7 +87,8 @@ class Acl implements JsonSerializable { * @param PollMapper $pollMapper * @param VoteMapper $voteMapper * @param ShareMapper $shareMapper - * @param Poll $pollMapper + * @param Poll $poll + * @param Share $share * */ public function __construct( @@ -94,7 +98,8 @@ class Acl implements JsonSerializable { PollMapper $pollMapper, VoteMapper $voteMapper, ShareMapper $shareMapper, - Poll $poll + Poll $poll, + Share $share ) { $this->userId = $userId; $this->userManager = $userManager; @@ -103,6 +108,7 @@ class Acl implements JsonSerializable { $this->voteMapper = $voteMapper; $this->shareMapper = $shareMapper; $this->poll = $poll; + $this->share = $share; } /** @@ -117,26 +123,27 @@ class Acl implements JsonSerializable { $this->token = $token; $this->pollId = 0; $this->userId = null; - $share = $this->shareMapper->findByToken($token); + $this->share = $this->shareMapper->findByToken($token); if (\OC::$server->getUserSession()->isLoggedIn()) { - if ($share->getType() !== 'group' && $share->getType() !== 'public') { + if ($this->share->getType() !== 'group' && $this->share->getType() !== 'public') { throw new NotAuthorizedException; } $this->userId = \OC::$server->getUserSession()->getUser()->getUID(); } else { - if ($share->getType() === 'group' || $share->getType() === 'user') { + if ($this->share->getType() === 'group' || $this->share->getType() === 'user') { throw new NotAuthorizedException; } - $this->userId = $share->getUserId(); + $this->userId = $this->share->getUserId(); } - $this->pollId = $share->getPollId(); + $this->pollId = $this->share->getPollId(); } elseif ($pollId) { $this->user = \OC::$server->getUserSession()->getUser()->getUID(); $this->pollId = $pollId; + $this->share = null; } $this->poll = $this->pollMapper->find($this->pollId); @@ -310,6 +317,16 @@ class Acl implements JsonSerializable { * @NoAdminRequired * @return bool */ + public function getAllowSubscribe(): bool { + return ($this->hasEmail()) + && !$this->poll->getDeleted() + && $this->getAllowView(); + } + + /** + * @NoAdminRequired + * @return bool + */ public function getAllowComment(): bool { return !$this->poll->getDeleted() && boolval($this->userId); } @@ -348,6 +365,14 @@ class Acl implements JsonSerializable { return $this->token; } + private function hasEmail():bool { + if ($this->share) { + return strlen($this->share->getUserEmail()) > 0; + } else { + return \OC::$server->getUserSession()->isLoggedIn(); + } + } + /** * @return array */ @@ -367,6 +392,7 @@ class Acl implements JsonSerializable { 'allowEdit' => $this->getAllowEdit(), 'allowSeeResults' => $this->getAllowSeeResults(), 'allowSeeUsernames' => $this->getAllowSeeUsernames(), + 'allowSubscribe' => $this->getAllowSubscribe(), 'userHasVoted' => $this->getUserHasVoted(), 'groupShare' => $this->getGroupShare(), 'personalShare' => $this->getPersonalShare(), diff --git a/lib/Service/ShareService.php b/lib/Service/ShareService.php index 70881cf9..83f6233f 100644 --- a/lib/Service/ShareService.php +++ b/lib/Service/ShareService.php @@ -120,6 +120,10 @@ class ShareService { throw new NotAuthorizedException; } + if ($type === 'contact') { + $type = 'external'; + } + $this->share = new Share(); $this->share->setType($type); $this->share->setPollId($pollId); @@ -168,19 +172,20 @@ class ShareService { * @throws NotAuthorizedException * @throws InvalidUsername */ - public function personal($token, $userName) { - $publicShare = $this->shareMapper->findByToken($token); + public function personal($token, $userName, $emailAddress) { + $this->share = $this->shareMapper->findByToken($token); // Return of validatePublicUsername is a DataResponse - $checkUsername = $this->systemController->validatePublicUsername($publicShare->getPollId(), $userName, $token); + $checkUsername = $this->systemController->validatePublicUsername($this->share->getPollId(), $userName, $token); // if status is not 200, return DataResponse from validatePublicUsername if ($checkUsername->getStatus() !== 200) { throw new InvalidUsername; } - if ($publicShare->getType() === 'public') { + if ($this->share->getType() === 'public') { + $pollId = $this->share->getPollId(); $this->share = new Share(); $this->share->setToken(\OC::$server->getSecureRandom()->generate( 16, @@ -189,17 +194,20 @@ class ShareService { ISecureRandom::CHAR_UPPER )); $this->share->setType('external'); - $this->share->setPollId($publicShare->getPollId()); + $this->share->setPollId($pollId); $this->share->setUserId($userName); - $this->share->setUserEmail(''); + $this->share->setUserEmail($emailAddress); $this->share->setInvitationSent(time()); - return $this->shareMapper->insert($this->share); + $this->shareMapper->insert($this->share); + $this->mailService->sendInvitationMail($this->share->getToken()); + return $this->share; - } elseif ($publicShare->getType() === 'email') { + } elseif ($this->share->getType() === 'email') { - $publicShare->setType('external'); - $publicShare->setUserId($userName); - return $this->shareMapper->update($publicShare); + $this->share->setType('external'); + $this->share->setUserId($userName); + $this->share->setUserEmail($emailAddress); + return $this->shareMapper->update($this->share); } else { throw new NotAuthorizedException; diff --git a/src/js/App.vue b/src/js/App.vue index 6ce6cc6e..3ca736e1 100644 --- a/src/js/App.vue +++ b/src/js/App.vue @@ -285,7 +285,7 @@ input { padding: 8px; background-color: var(--color-main-background); border-radius: var(--border-radius); - margin: 12px 0; + margin: 12px 6px; } </style> diff --git a/src/js/components/Base/PersonalLink.vue b/src/js/components/Base/PersonalLink.vue new file mode 100644 index 00000000..198f408c --- /dev/null +++ b/src/js/components/Base/PersonalLink.vue @@ -0,0 +1,92 @@ +<!-- + - @copyright Copyright (c) 2018 René Gieling <github@dartcafe.de> + - + - @author René Gieling <github@dartcafe.de> + - + - @license GNU AGPL version 3 or any later version + - + - This program is free software: you can redistribute it and/or modify + - it under the terms of the GNU Affero General Public License as + - published by the Free Software Foundation, either version 3 of the + - License, or (at your option) any later version. + - + - This program is distributed in the hope that it will be useful, + - but WITHOUT ANY WARRANTY; without even the implied warranty of + - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + - GNU Affero General Public License for more details. + - + - You should have received a copy of the GNU Affero General Public License + - along with this program. If not, see <http://www.gnu.org/licenses/>. + - + --> + +<template> + <div v-show="share.type !== 'public'"> + <h2 class="title"> + {{ t('polls', 'This is a public poll.') }} + </h2> + <p>{{ t('polls', 'The following link is your personal access to this poll. You can reenter this this poll at any time, change your vote and leave comments.') }}</p> + <p>{{ t('polls', 'Your personal link to this poll: {linkURL}', { linkURL: personalLink} ) }}</p> + <ButtonDiv icon="icon-clippy" :title="t('polls','Copy this link to the clipboard')" @click="copyLink()" /> + <ButtonDiv v-if="share.userEmail" + icon="icon-mail" + :title="t('polls','Resend invitation mail to {emailAdress}', { emailAdress: share.userEmail })" + @click="sendInvitation()" /> + </div> +</template> + +<script> +import ButtonDiv from '../Base/ButtonDiv' +import { showSuccess, showError } from '@nextcloud/dialogs' +import { mapState } from 'vuex' + +export default { + name: 'PersonalLink', + + components: { + ButtonDiv, + }, + + computed: { + ...mapState({ + share: state => state.poll.share, + }), + + personalLink() { + return window.location.origin.concat( + this.$router.resolve({ + name: 'publicVote', + params: { token: this.$route.params.token }, + }).href + ) + }, + + }, + + methods: { + sendInvitation() { + this.$store.dispatch('poll/share/sendInvitation') + .then((response) => { + response.data.sentResult.sentMails.forEach((item) => { + showSuccess(t('polls', 'Invitation sent to {emailAddress}', { emailAddress: item.eMailAddress })) + }) + response.data.sentResult.abortedMails.forEach((item) => { + console.error('Mail could not be sent!', { recipient: item }) + showError(t('polls', 'Error sending invitation to {emailAddress}', { emailAddress: item.eMailAddress })) + }) + }) + }, + + copyLink() { + this.$copyText(this.personalLink).then( + function() { + OC.Notification.showTemporary(t('polls', 'Link copied to clipboard'), { type: 'success' }) + }, + function() { + OC.Notification.showTemporary(t('polls', 'Error while copying link to clipboard'), { type: 'error' }) + } + ) + }, + }, +} +</script> diff --git a/src/js/components/VoteTable/VoteHeaderPublic.vue b/src/js/components/Base/PublicRegisterModal.vue index 052a7339..03e83655 100644 --- a/src/js/components/VoteTable/VoteHeaderPublic.vue +++ b/src/js/components/Base/PublicRegisterModal.vue @@ -21,28 +21,9 @@ --> <template> - <div v-if="poll.id" class="vote__header"> - <div v-show="displayLink" class="vote__header__personal-link"> - <ConfigBox :title="t('polls', 'Your personal link to this poll')" - icon-class="icon-share" - :info="t('polls','Copy this link for reentering the poll at any time and edit your votes.')"> - <div class="personal-link"> - {{ personalLink }} - <a class="icon icon-clippy" @click="copyLink()" /> - </div> - </ConfigBox> - <ConfigBox :title="t('polls', 'Your registered email address')" - icon-class="icon-mail" - :info="t('polls','Enter your email address, to be able to subscribe to updates of this poll.')"> - <div class="user-email"> - {{ userEmail }} - <a class="icon icon-rename" /> - </div> - </ConfigBox> - </div> - - <Modal v-show="!isValidUser &!expired & modal" :can-close="false"> - <div class="modal__content"> + <Modal v-show="modal" :can-close="false"> + <div class="modal__content"> + <div class="enter__name"> <h2>{{ t('polls', 'Who are you?') }}</h2> <p>{{ t('polls', 'To participate, tell us how we can call you!') }}</p> @@ -56,46 +37,50 @@ <span v-show="!checkingUserName && userName.length > 2 && !isValidName" class="error">{{ t('polls', 'This name is not valid, i.e. because it is already in use.') }}</span> <span v-show="!checkingUserName && userName.length > 2 && isValidName" class="error">{{ t('polls', 'OK, we will call you {username}.', {username : userName }) }}</span> </div> + </div> + <div class="enter__email"> + <p>{{ t('polls', 'Enter your email address to be able to subscribe to updates and get your personal link via email.') }}</p> - <div class="modal__buttons"> - <a :href="loginLink" class="modal__buttons__link"> {{ t('polls', 'You have an account? Log in here.') }} </a> - <div class="modal__buttons__spacer" /> - <ButtonDiv :title="t('polls', 'Cancel')" - @click="closeModal" /> - <ButtonDiv :primary="true" :disabled="!isValidName || checkingUserName" :title="t('polls', 'OK')" - @click="writeUserName" /> - </div> + <input v-model="emailAddress" :class="{ error: (!isValidName && userName.length > 0), success: isValidName }" + type="text" + :placeholder="t('polls', 'Enter your email address')" @keyup.enter="writeUserName"> </div> - </Modal> - </div> + + <div class="modal__buttons"> + <a :href="loginLink" class="modal__buttons__link"> {{ t('polls', 'You have an account? Log in here.') }} </a> + <div class="modal__buttons__spacer" /> + <ButtonDiv :title="t('polls', 'Cancel')" + @click="closeModal" /> + <ButtonDiv :primary="true" :disabled="!isValidName || checkingUserName" :title="t('polls', 'OK')" + @click="writeUserName" /> + </div> + </div> + </Modal> </template> <script> import debounce from 'lodash/debounce' import axios from '@nextcloud/axios' -import ConfigBox from '../Base/ConfigBox' import ButtonDiv from '../Base/ButtonDiv' import { generateUrl } from '@nextcloud/router' import { Modal } from '@nextcloud/vue' -import { mapState, mapGetters } from 'vuex' +import { mapState } from 'vuex' export default { - name: 'VoteHeaderPublic', + name: 'PublicRegisterModal', components: { Modal, - ConfigBox, ButtonDiv, }, data() { return { userName: '', - token: '', + emailAddress: '', checkingUserName: false, redirecting: false, isValidName: false, - newName: '', modal: true, } }, @@ -103,22 +88,9 @@ export default { computed: { ...mapState({ poll: state => state.poll, - acl: state => state.poll.acl, - }), - - ...mapGetters({ - expired: 'poll/expired', + share: state => state.poll.share, }), - userEmail: { - get() { - return this.poll.shares.list[0].userEmail - }, - set(value) { - // this.writeValueDebounced({ description: value }) - }, - }, - loginLink() { const redirectUrl = this.$router.resolve({ name: 'publicVote', @@ -126,24 +98,6 @@ export default { }).href return generateUrl('login?redirect_url=' + redirectUrl) }, - - personalLink() { - return window.location.origin.concat( - this.$router.resolve({ - name: 'publicVote', - params: { token: this.$route.params.token }, - }).href - ) - }, - - displayLink() { - return (this.acl.userId !== '' && this.acl.userId !== null && this.acl.token) - }, - - isValidUser() { - return (this.acl.userId !== '' && this.acl.userId !== null) - }, - }, watch: { @@ -151,16 +105,20 @@ export default { this.isValidName = false if (this.userName.length > 2) { this.checkingUserName = true - this.isValidName = this.validatePublicUsername() + if (this.userName !== this.share.userid) { + this.isValidName = this.validatePublicUsername() + } } else { this.invalidUserNameMessage = t('polls', 'Please use at least 3 characters for your username!') this.checkingUserName = false } }, + }, - 'poll.id': function(newValue) { - this.setFocus() - }, + mounted() { + this.userName = this.share.userId + this.emailAddress = this.share.userEmail + this.setFocus() }, methods: { @@ -170,22 +128,9 @@ export default { }) }, - showModal() { - this.modal = true - }, closeModal() { this.modal = false }, - copyLink() { - this.$copyText(this.personalLink).then( - function() { - OC.Notification.showTemporary(t('polls', 'Link copied to clipboard'), { type: 'success' }) - }, - function() { - OC.Notification.showTemporary(t('polls', 'Error while copying link to clipboard'), { type: 'error' }) - } - ) - }, validatePublicUsername: debounce(function() { if (this.userName.length > 2) { @@ -213,14 +158,15 @@ export default { writeUserName() { if (this.isValidName) { - this.$store.dispatch('poll/shares/addPersonal', { token: this.$route.params.token, userName: this.userName }) + this.$store.dispatch('poll/shares/addPersonal', { token: this.$route.params.token, userName: this.userName, emailAddress: this.emailAddress }) .then((response) => { if (this.$route.params.token === response.token) { this.$store.dispatch({ type: 'poll/get', pollId: this.$route.params.id, token: this.$route.params.token }) + this.closeModal() } else { - this.token = response.token this.redirecting = true - this.$router.replace({ name: 'publicVote', params: { token: this.token } }) + this.$router.replace({ name: 'publicVote', params: { token: response.token } }) + this.closeModal() } }) .catch(() => { @@ -233,25 +179,11 @@ export default { </script> <style lang="scss"> - .vote__header { - margin: 8px 24px; - } - .vote__header__personal-link { - // display: flex; - padding: 4px 12px; - margin: 0 12px 0 24px; - border: 2px solid var(--color-success); - background-color: var(--color-background-success) !important; - border-radius: var(--border-radius); - font-size: 1.2em; - opacity: 0.8; - .icon { - margin: 0 12px; + .modal__content { + .enter__name, .enter__email { + margin-bottom: 12px; } } - .personal-link, .user-email { - display: flex; - } </style> diff --git a/src/js/components/Create/CreateDlg.vue b/src/js/components/Create/CreateDlg.vue index 5cd03f30..f86131c2 100644 --- a/src/js/components/Create/CreateDlg.vue +++ b/src/js/components/Create/CreateDlg.vue @@ -103,11 +103,11 @@ export default { .then((response) => { emit('update-polls') this.cancel() - OC.Notification.showTemporary(t('polls', 'Poll "%n" added', 1, response.data.id), { type: 'success' }) + OC.Notification.showTemporary(t('polls', 'Poll "{pollTitle}" added', { pollTitle: response.data.id }), { type: 'success' }) this.$router.push({ name: 'vote', params: { id: response.data.id } }) }) .catch(() => { - OC.Notification.showTemporary(t('polls', 'Error while creating Poll "%n"', 1, this.title), { type: 'error' }) + OC.Notification.showTemporary(t('polls', 'Error while creating Poll "{pollTitle}"', { pollTitle: this.title }), { type: 'error' }) }) }, diff --git a/src/js/components/SideBar/SideBarTabConfiguration.vue b/src/js/components/SideBar/SideBarTabConfiguration.vue index 8ca279dc..2e6a3f6f 100644 --- a/src/js/components/SideBar/SideBarTabConfiguration.vue +++ b/src/js/components/SideBar/SideBarTabConfiguration.vue @@ -306,7 +306,7 @@ export default { } else { this.$store.dispatch('poll/update') .then((response) => { - OC.Notification.showTemporary(t('polls', '%n successfully saved', 1, response.data.title), { type: 'success' }) + OC.Notification.showTemporary(t('polls', '"{pollTitle}" successfully saved', 1, { pollTitle: response.data.title }), { type: 'success' }) emit('update-polls') }) .catch(() => { diff --git a/src/js/components/SideBar/SideBarTabShare.vue b/src/js/components/SideBar/SideBarTabShare.vue index b0ad9d8d..ecf8af56 100644 --- a/src/js/components/SideBar/SideBarTabShare.vue +++ b/src/js/components/SideBar/SideBarTabShare.vue @@ -24,7 +24,7 @@ <div> <ConfigBox v-if="!acl.isOwner" :title="t('polls', 'As an admin you may edit this poll')" icon-class="icon-checkmark" /> - <ConfigBox :title="t('polls', 'Invitations')" icon-class="icon-share"> + <ConfigBox :title="t('polls', 'Shares')" icon-class="icon-share"> <TransitionGroup :css="false" tag="div" class="shared-list"> <UserItem v-for="(share) in invitationShares" :key="share.id" v-bind="share" @@ -80,7 +80,7 @@ <Avatar icon-class="icon-public" :is-no-user="true" /> <!-- <div class="avatar icon-public" /> --> <div class="share-item__description"> - {{ t('polls', 'Public link (' + share.token + ')') }} + {{ t('polls', 'Public link ({token})', {token: share.token }) }} </div> </div> <Actions> diff --git a/src/js/components/Subscription/Subscription.vue b/src/js/components/Subscription/Subscription.vue index 78d4a552..d91e8a0a 100644 --- a/src/js/components/Subscription/Subscription.vue +++ b/src/js/components/Subscription/Subscription.vue @@ -24,7 +24,8 @@ <div class="subscription"> <input id="subscribe" v-model="subscribe" type="checkbox" class="checkbox"> - <label for="subscribe">{{ t('polls', 'Receive notification email on activity') }}</label> + <label v-if="share.userEmail" for="subscribe">{{ t('polls', 'Receive notification email on activity to {emailAddress}', {emailAddress: share.userEmail}) }}</label> + <label v-else for="subscribe">{{ t('polls', 'Receive notification email on activity') }}</label> </div> </template> @@ -37,6 +38,7 @@ export default { ...mapState({ subscription: state => state.subscription, poll: state => state.poll, + share: state => state.poll.share, }), subscribe: { diff --git a/src/js/store/modules/poll.js b/src/js/store/modules/poll.js index 8bbdb947..3a8b0f31 100644 --- a/src/js/store/modules/poll.js +++ b/src/js/store/modules/poll.js @@ -28,6 +28,7 @@ import acl from './subModules/acl.js' import comments from './subModules/comments.js' import options from './subModules/options.js' import shares from './subModules/shares.js' +import share from './subModules/share.js' import votes from './subModules/votes.js' const defaultPoll = () => { @@ -58,7 +59,9 @@ const modules = { options: options, shares: shares, votes: votes, + share: share, } + const mutations = { set(state, payload) { Object.assign(state, payload.poll) @@ -148,6 +151,7 @@ const actions = { context.commit('comments/reset') context.commit('options/reset') context.commit('shares/reset') + context.commit('share/reset') context.commit('votes/reset') return } @@ -158,13 +162,12 @@ const actions = { context.commit('comments/set', response.data) context.commit('options/set', response.data) context.commit('shares/set', response.data) + context.commit('share/set', response.data) context.commit('votes/set', response.data) return response - }, (error) => { - if (error.response.status !== '404' && error.response.status !== '401') { - console.debug('Error loading poll', { error: error.response }, { payload: payload }) - return error.response - } + }) + .catch((error) => { + console.debug('Error loading poll', { error: error.response }, { payload: payload }) throw error }) }, diff --git a/src/js/store/modules/subModules/share.js b/src/js/store/modules/subModules/share.js new file mode 100644 index 00000000..1e92b655 --- /dev/null +++ b/src/js/store/modules/subModules/share.js @@ -0,0 +1,84 @@ +/* + * @copyright Copyright (c) 2019 Rene Gieling <github@dartcafe.de> + * + * @author Rene Gieling <github@dartcafe.de> + * @author Julius Härtl <jus@bitgrid.net> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +import axios from '@nextcloud/axios' +import { generateUrl } from '@nextcloud/router' + +const defaultShares = () => { + return { + displayName: '', + externalUser: false, + id: null, + invitationSent: 0, + pollId: null, + token: '', + type: '', + userEmail: '', + userId: '', + } +} + +const state = defaultShares() + +const namespaced = true + +const mutations = { + set(state, payload) { + Object.assign(state, payload.share) + }, + + reset(state) { + Object.assign(state, defaultShares()) + }, + +} + +const actions = { + get(context, payload) { + const endPoint = 'apps/polls/share' + return axios.get(generateUrl(endPoint.concat('/', payload.token))) + .then((response) => { + context.commit('set', { share: response.data.share }) + return response.data + }) + .catch((error) => { + console.error('Error retrieving share', { error: error.response }, { payload: payload }) + throw error + }) + }, + + sendInvitation(context, payload) { + const endPoint = 'apps/polls/share/send' + return axios.post(generateUrl(endPoint.concat('/', context.state.token))) + .then((response) => { + context.commit('set', { share: response.data.share }) + return response + }) + .catch((error) => { + console.error('Error sending invitation', { error: error.response }, { payload: payload }) + throw error + }) + }, +} + +export default { namespaced, state, mutations, actions } diff --git a/src/js/store/modules/subModules/shares.js b/src/js/store/modules/subModules/shares.js index d3f5b5c7..c684e297 100644 --- a/src/js/store/modules/subModules/shares.js +++ b/src/js/store/modules/subModules/shares.js @@ -64,7 +64,7 @@ const getters = { invitation: state => { const invitationTypes = ['user', 'group', 'email', 'external', 'contact'] return state.list.filter(share => { - return invitationTypes.includes(share.type) + return invitationTypes.includes(share.type) && share.invitationSent }) }, @@ -105,7 +105,7 @@ const actions = { addPersonal(context, payload) { const endPoint = 'apps/polls/share/personal' - return axios.post(generateUrl(endPoint), { token: payload.token, userName: payload.userName }) + return axios.post(generateUrl(endPoint), { token: payload.token, userName: payload.userName, emailAddress: payload.emailAddress }) .then((response) => { return { token: response.data.token } }) diff --git a/src/js/views/Vote.vue b/src/js/views/Vote.vue index 585f8176..0f110a42 100644 --- a/src/js/views/Vote.vue +++ b/src/js/views/Vote.vue @@ -62,8 +62,8 @@ </h3> </div> - <div class="area__public"> - <VoteHeaderPublic v-if="!getCurrentUser()" /> + <div v-if="$route.name === 'publicVote' && poll.id" class="area__public"> + <PersonalLink v-if="share.userId" /> </div> <div class="area__main"> @@ -80,10 +80,14 @@ </div> <div class="area__footer"> - <Subscription v-if="getCurrentUser() || (acl.token && poll.shares.list[0].userEmail)" /> + <Subscription v-if="acl.allowSubscribe" /> <ParticipantsList v-if="acl.allowSeeUsernames" /> </div> <LoadingOverlay v-if="isLoading" /> + + <div v-if="$route.name === 'publicVote' && poll.id"> + <PublicRegisterModal v-show="!share.userId && !isExpired" /> + </div> </AppContent> </template> @@ -92,11 +96,13 @@ import { Actions, ActionButton, AppContent } from '@nextcloud/vue' import Subscription from '../components/Subscription/Subscription' import Badge from '../components/Base/Badge' import ParticipantsList from '../components/Base/ParticipantsList' +import PersonalLink from '../components/Base/PersonalLink' import PollInformation from '../components/Base/PollInformation' import LoadingOverlay from '../components/Base/LoadingOverlay' -import VoteHeaderPublic from '../components/VoteTable/VoteHeaderPublic' +import PublicRegisterModal from '../components/Base/PublicRegisterModal' import VoteTable from '../components/VoteTable/VoteTable' import { mapState, mapGetters } from 'vuex' +import { getCurrentUser } from '@nextcloud/auth' import { emit } from '@nextcloud/event-bus' import moment from '@nextcloud/moment' @@ -107,11 +113,12 @@ export default { ActionButton, AppContent, Badge, - Subscription, + LoadingOverlay, ParticipantsList, + PersonalLink, PollInformation, - LoadingOverlay, - VoteHeaderPublic, + PublicRegisterModal, + Subscription, VoteTable, }, @@ -130,6 +137,7 @@ export default { poll: state => state.poll, acl: state => state.poll.acl, options: state => state.poll.options.list, + share: state => state.poll.share, }), ...mapGetters({ @@ -181,8 +189,19 @@ export default { }, created() { - this.loadPoll() - emit('toggle-sidebar', { open: (window.innerWidth > 920) }) + if (getCurrentUser() && this.$route.params.token) { + // reroute to the internal vote page, if the user is logged in + this.$store.dispatch('poll/shares/get', { token: this.$route.params.token }) + .then((response) => { + this.$router.replace({ name: 'vote', params: { id: response.share.pollId } }) + }) + .catch(() => { + this.$router.replace({ name: 'notfound' }) + }) + } else { + this.loadPoll() + emit('toggle-sidebar', { open: (window.innerWidth > 920) }) + } }, beforeDestroy() { @@ -210,8 +229,7 @@ export default { this.isLoading = false window.document.title = this.windowTitle }) - .catch((error) => { - console.error(error) + .catch(() => { this.isLoading = false this.$router.replace({ name: 'notfound' }) }) |