Merge pull request #1281 from nextcloud/admin-section

Adding admin section
This commit is contained in:
René Gieling 2021-01-02 23:03:12 +01:00 коммит произвёл GitHub
Родитель 3a4c47523a db72dae1d3
Коммит 5a76364c53
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
26 изменённых файлов: 1297 добавлений и 343 удалений

Просмотреть файл

@ -36,6 +36,10 @@ return [
['name' => 'public#validate_public_username', 'url' => '/check/username', 'verb' => 'POST'],
['name' => 'public#validate_email_address', 'url' => '/check/emailaddress/{emailAddress}', 'verb' => 'GET'],
['name' => 'admin#index', 'url' => '/administration', 'verb' => 'GET'],
['name' => 'admin#list', 'url' => '/administration/polls', 'verb' => 'GET'],
['name' => 'admin#takeover', 'url' => '/administration/poll/{pollId}/takeover', 'verb' => 'GET'],
['name' => 'page#index', 'url' => '/', 'verb' => 'GET'],
['name' => 'page#index', 'url' => '/not-found', 'verb' => 'GET', 'postfix' => 'notfound'],
['name' => 'page#index', 'url' => '/list/{id}', 'verb' => 'GET', 'postfix' => 'list'],

Просмотреть файл

@ -0,0 +1,104 @@
<?php
/**
* @copyright Copyright (c) 2017 Vinzenz Rosenkranz <vinzenz.rosenkranz@gmail.com>
*
* @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/>.
*
*/
namespace OCA\Polls\Controller;
use OCP\IRequest;
use OCP\IURLGenerator;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http\DataResponse;
use OCP\AppFramework\Http\TemplateResponse;
use OCA\Polls\Db\Poll;
use OCA\Polls\Service\PollService;
class AdminController extends Controller {
/** @var IURLGenerator */
private $urlGenerator;
/** @var PollService */
private $pollService;
/** @var Poll */
private $poll;
use ResponseHandle;
public function __construct(
string $appName,
IRequest $request,
IURLGenerator $urlGenerator,
PollService $pollService,
Poll $poll
) {
parent::__construct($appName, $request);
$this->urlGenerator = $urlGenerator;
$this->pollService = $pollService;
$this->poll = $poll;
}
/**
* @NoCSRFRequired
*/
public function index(): TemplateResponse {
return new TemplateResponse('polls', 'polls.tmpl',
['urlGenerator' => $this->urlGenerator]);
}
/**
* Get list of polls for administrative purposes
*/
public function list(): DataResponse {
return $this->response(function () {
return $this->pollService->listForAdmin();
});
}
/**
* Get list of polls for administrative purposes
*/
public function takeover(int $pollId): DataResponse {
return $this->response(function () use ($pollId) {
return $this->pollService->takeover($pollId);
});
}
/**
* Switch deleted status (move to deleted polls)
*/
public function switchDeleted($pollId): DataResponse {
return $this->response(function () use ($pollId) {
return $this->pollService->switchDeleted($pollId);
});
}
/**
* Delete poll
*/
public function delete($pollId): DataResponse {
return $this->responseDeleteTolerant(function () use ($pollId) {
return $this->pollService->delete($pollId);
});
}
}

Просмотреть файл

@ -25,10 +25,10 @@
namespace OCA\Polls\Controller;
use OCP\IRequest;
use OCP\IURLGenerator;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\IURLGenerator;
use OCA\Polls\Service\NotificationService;
class PageController extends Controller {
@ -39,7 +39,7 @@ class PageController extends Controller {
private $notificationService;
public function __construct(
$appName,
string $appName,
IRequest $request,
IURLGenerator $urlGenerator,
NotificationService $notificationService

Просмотреть файл

@ -100,7 +100,6 @@ class PollController extends Controller {
});
}
/**
* get complete poll
* @NoAdminRequired
@ -109,7 +108,7 @@ class PollController extends Controller {
return $this->response(function () use ($pollId) {
$this->share = null;
$this->poll = $this->pollService->get($pollId);
$this->acl->setPoll($this->poll);
$this->acl->setPoll($this->poll)->requestView();
return $this->build();
});
}

Просмотреть файл

@ -53,6 +53,8 @@ class Log extends Entity implements JsonSerializable {
public const MSG_ID_ADDOPTION = 'addOption';
public const MSG_ID_DELETEOPTION = 'deleteOption';
public const MSG_ID_SETVOTE = 'setVote';
public const MSG_ID_OWNERCHANGE = 'updateOwner';
public const MSG_ID_INDIVIDUAL = 'message';
/** @var int $pollId */
protected $pollId;

Просмотреть файл

@ -74,4 +74,19 @@ class PollMapper extends QBMapper {
->orWhere($qb->expr()->eq('owner', $qb->createNamedParameter($userId, IQueryBuilder::PARAM_STR)));
return $this->findEntities($qb);
}
/**
* @throws \OCP\AppFramework\Db\DoesNotExistException if not found
* @return Poll[]
*/
public function findForAdmin(string $userId): array {
$qb = $this->db->getQueryBuilder();
$qb->select('*')
->from($this->getTableName())
->where(
$qb->expr()->neq('owner', $qb->createNamedParameter($userId, IQueryBuilder::PARAM_STR))
);
return $this->findEntities($qb);
}
}

Просмотреть файл

@ -82,7 +82,7 @@ class Acl implements JsonSerializable {
/**
* setToken - load share via token and than call setShare
* load share via token and than call setShare
*/
public function setToken(string $token = ''): Acl {
try {
@ -99,17 +99,11 @@ class Acl implements JsonSerializable {
public function setShare(Share $share): Acl {
$this->share = $share;
$this->validateShareAccess();
// load poll, if pollId does not match
if ($this->share->getPollId() !== $this->poll->getId()) {
$this->setPollId($share->getPollId());
}
$this->setPollId($share->getPollId());
$this->requestView();
return $this;
}
/**
* setPollId
*/
public function setPollId(int $pollId = 0): Acl {
try {
return $this->setPoll($this->pollMapper->find($pollId));
@ -118,20 +112,11 @@ class Acl implements JsonSerializable {
}
}
/**
* * setPoll
*
* @return static
*/
public function setPoll(Poll $poll): self {
public function setPoll(Poll $poll): Acl {
$this->poll = $poll;
$this->requestView();
return $this;
}
/**
* getUserId
*/
public function getUserId() {
if ($this->getLoggedIn()) {
return \OC::$server->getUserSession()->getUser()->getUID();
@ -140,11 +125,6 @@ class Acl implements JsonSerializable {
}
}
/**
* * getDisplayName
*
* @return string
*/
private function getDisplayName(): string {
if ($this->getLoggedIn()) {
return $this->userManager->get($this->getUserId())->getDisplayName();
@ -153,16 +133,10 @@ class Acl implements JsonSerializable {
}
}
/**
* getPollId
*/
public function getPollId(): int {
return $this->poll->getId();
}
/**
* getAllowView
*/
public function getAllowView(): bool {
return (
$this->getAllowEdit()
@ -174,9 +148,6 @@ class Acl implements JsonSerializable {
);
}
/**
* getAllowVote
*/
public function getAllowVote(): bool {
return ($this->getAllowView() || $this->getToken())
&& !$this->poll->getExpired()
@ -184,102 +155,72 @@ class Acl implements JsonSerializable {
&& $this->getUserId();
}
/**
* getAllowSubscribe
*/
public function getAllowSubscribe(): bool {
return ($this->hasEmail())
&& !$this->poll->getDeleted()
&& $this->getAllowView();
}
/**
* getAllowComment
*/
public function getAllowComment(): bool {
return !$this->poll->getDeleted() && $this->getUserId();
}
/**
* getAllowEdit
*/
public function getAllowEdit(): bool {
return ($this->getIsOwner() || $this->getIsAdmin());
return ($this->getIsOwner() || $this->hasAdminAccess());
}
/**
* requestView
*/
public function requestView(): void {
if (!$this->getAllowView()) {
throw new NotAuthorizedException;
}
}
/**
* requestVote
*/
public function requestVote(): void {
if (!$this->getAllowVote()) {
throw new NotAuthorizedException;
}
}
/**
* requestComment
*/
public function requestComment(): void {
if (!$this->getAllowComment()) {
throw new NotAuthorizedException;
}
}
/**
* requestEdit
*/
public function requestEdit(): void {
if (!$this->getAllowEdit()) {
throw new NotAuthorizedException;
}
}
/**
* requestDelete
*/
public function requestDelete(): void {
if (!$this->getAllowEdit() || !$this->poll->getDeleted()) {
if (!$this->getAllowEdit() && !$this->getIsAdmin()) {
throw new NotAuthorizedException;
}
}
public function requestTakeOver(): void {
if (!$this->getIsAdmin()) {
throw new NotAuthorizedException;
}
}
/**
* validateUserId
*/
public function validateUserId(string $userId): void {
if ($this->getUserId() !== $userId) {
throw new NotAuthorizedException;
}
}
/**
* getAllowSeeResults
*/
public function getAllowSeeResults(): bool {
return $this->poll->getShowResults() === Poll::SHOW_RESULTS_ALWAYS
|| ($this->poll->getShowResults() === 'expired' && $this->poll->getExpired())
|| $this->getIsOwner();
}
/**
* getAllowSeeUsernames
*/
public function getAllowSeeUsernames(): bool {
return !$this->poll->getAnonymous() || $this->getIsOwner();
}
/**
* getToken
*/
public function getToken(): string {
return strval($this->share->getToken());
}
@ -319,11 +260,19 @@ class Acl implements JsonSerializable {
}
/**
* getIsAdmin - Has user administrative rights?
* Returns true, if user is in admin group and poll has allowed admins to manage the poll
* getIsAdmin - Is the user admin
* Returns true, if user is in admin group
*/
private function getIsAdmin(): bool {
return ($this->getLoggedIn() && $this->groupManager->isAdmin($this->getUserId()) && $this->poll->getAdminAccess());
return ($this->getLoggedIn() && $this->groupManager->isAdmin($this->getUserId()));
}
/**
* hasAdminAccess - Has user administrative rights?
* Returns true, if user is in admin group and poll has allowed admins to manage the poll
*/
private function hasAdminAccess(): bool {
return ($this->getIsAdmin() && $this->poll->getAdminAccess());
}
/**

Просмотреть файл

@ -73,6 +73,8 @@ class Notifier implements INotifier {
throw new \InvalidArgumentException();
}
$notification->setIcon($this->url->getAbsoluteURL($this->url->imagePath('polls', 'polls-black.svg')));
$parameters = $notification->getSubjectParameters();
switch ($notification->getSubject()) {
case 'invitation':
$poll = $this->pollMapper->find(intval($notification->getObjectId()));
@ -97,6 +99,77 @@ class Notifier implements INotifier {
['id' => $poll->getId()]
));
break;
case 'takeOverPoll':
$poll = $this->pollMapper->find(intval($notification->getObjectId()));
$newOwner = $this->userManager->get($parameters['actor']);
$notification->setParsedSubject(
$l->t('%s took over your poll', [$newOwner->getDisplayName()])
);
$notification->setRichSubject(
$l->t('{user} took over your poll "%s" and is the new owner.', [$poll->getTitle()]),
[
'user' => [
'type' => 'user',
'id' => $newOwner->getUID(),
'name' => $newOwner->getDisplayName(),
]
]
);
$notification->setLink($this->url->linkToRouteAbsolute(
'polls.page.vote',
['id' => $poll->getId()]
));
break;
case 'deletePollByOther':
$poll = $this->pollMapper->find(intval($notification->getObjectId()));
$actor = $this->userManager->get($parameters['actor']);
$notification->setParsedSubject(
$l->t('%s permanently deleted your poll', [$actor->getDisplayName()])
);
$notification->setRichSubject(
$l->t('{user} permanently deleted your poll "%s".', $parameters['pollTitle']),
[
'user' => [
'type' => 'user',
'id' => $actor->getUID(),
'name' => $actor->getDisplayName(),
]
]
);
$notification->setLink($this->url->linkToRouteAbsolute(
'polls.page.vote',
['id' => $poll->getId()]
));
break;
case 'softDeletePollByOther':
$poll = $this->pollMapper->find(intval($notification->getObjectId()));
$actor = $this->userManager->get($parameters['actor']);
$notification->setParsedSubject(
$l->t('%s changed the deleted status of your poll', [$actor->getDisplayName()])
);
$notification->setRichSubject(
$l->t('{user} changed the deleted status of your poll "%s".', $parameters['pollTitle']),
[
'user' => [
'type' => 'user',
'id' => $actor->getUID(),
'name' => $actor->getDisplayName(),
]
]
);
$notification->setLink($this->url->linkToRouteAbsolute(
'polls.page.vote',
['id' => $poll->getId()]
));
break;
default:
// Unknown subject => Unknown notification => throw
throw new \InvalidArgumentException();

Просмотреть файл

@ -58,7 +58,7 @@ class CommentService {
* Read all comments of a poll based on the poll id and return list as array
*/
public function list(int $pollId = 0): array {
$this->acl->setPollId($pollId);
$this->acl->setPollId($pollId)->requestView();
if ($this->acl->getAllowSeeUsernames()) {
return $this->commentMapper->findByPoll($this->acl->getPollId());

Просмотреть файл

@ -226,10 +226,6 @@ class MailService {
}
private function getLogString(Log $logItem, string $displayName): string {
if ($logItem->getMessage()) {
return $logItem->getMessage();
}
switch ($logItem->getMessageId()) {
case Log::MSG_ID_SETVOTE:
return $this->trans->t('- %s voted.', [$displayName]);
@ -245,6 +241,10 @@ class MailService {
return $this->trans->t('- A vote option was added.');
case Log::MSG_ID_DELETEOPTION:
return $this->trans->t('- A vote option was removed.');
case Log::MSG_ID_OWNERCHANGE:
return $this->trans->t('- The poll owner changed.');
case Log::MSG_ID_INDIVIDUAL:
return $logItem->getMessage();
default:
return $logItem->getMessageId() . " (" . $displayName . ")";
}

Просмотреть файл

@ -46,7 +46,7 @@ class NotificationService {
public function removeNotification(int $pollId): void {
$notification = $this->notificationManager->createNotification();
$notification->setApp('polls')
$notification->setApp(self::APP_ID)
->setObject('poll', strval($pollId))
->setUser($this->userId);
$this->notificationManager->markProcessed($notification);
@ -54,11 +54,35 @@ class NotificationService {
public function sendInvitation(int $pollId, $recipient): bool {
$notification = $this->notificationManager->createNotification();
$notification->setApp('polls')
$notification->setApp(self::APP_ID)
->setUser($recipient)
->setDateTime(new DateTime())
->setObject('poll', strval($pollId))
->setSubject('invitation', ['pollId' => $pollId, 'recipient' => $recipient]);
->setSubject(self::EVENT_INVITTION, ['pollId' => $pollId, 'recipient' => $recipient]);
$this->notificationManager->notify($notification);
return true;
}
/**
* create a notification
*
* @param array $params
* List of parameters sent to the notification
* following types MUST be defined in the §params array:
* msgId => Type for setSubject
* objectType => Type for setObject
* objectValue => Value for setObject
* recipient => the recipient of the notification
* $params will be set as Subject value
*/
public function createNotification(array $params = []): bool {
$notification = $this->notificationManager->createNotification();
$notification->setApp(self::APP_ID)
->setUser($params['recipient'])
->setDateTime(new DateTime())
->setObject($params['objectType'], strval($params['objectValue']))
->setSubject($params['msgId'], $params);
$this->notificationManager->notify($notification);
return true;
}

Просмотреть файл

@ -73,7 +73,7 @@ class OptionService {
if ($token) {
$this->acl->setToken($token);
} else {
$this->acl->setPollId($pollId);
$this->acl->setPollId($pollId)->requestView();
}
if (!$this->acl->getAllowView()) {
@ -93,7 +93,7 @@ class OptionService {
* @return Option
*/
public function get(int $optionId): Option {
$this->acl->setPollId($this->optionMapper->find($optionId)->getPollId());
$this->acl->setPollId($this->optionMapper->find($optionId)->getPollId())->requestView();
if (!$this->acl->getAllowView()) {
throw new NotAuthorizedException;

Просмотреть файл

@ -40,6 +40,9 @@ use OCA\Polls\Model\Acl;
class PollService {
/** @var string */
private $userId;
/** @var PollMapper */
private $pollMapper;
@ -55,6 +58,9 @@ class PollService {
/** @var LogService */
private $logService;
/** @var NotificationService */
private $notificationService;
/** @var MailService */
private $mailService;
@ -62,19 +68,23 @@ class PollService {
private $acl;
public function __construct(
string $UserId,
PollMapper $pollMapper,
Poll $poll,
VoteMapper $voteMapper,
Vote $vote,
LogService $logService,
NotificationService $notificationService,
MailService $mailService,
Acl $acl
) {
$this->pollMapper = $pollMapper;
$this->userId = $UserId;
$this->poll = $poll;
$this->voteMapper = $voteMapper;
$this->vote = $vote;
$this->logService = $logService;
$this->notificationService = $notificationService;
$this->mailService = $mailService;
$this->acl = $acl;
}
@ -89,7 +99,7 @@ class PollService {
foreach ($polls as $poll) {
try {
$this->acl->setPoll($poll);
$this->acl->setPoll($poll)->requestView();
// TODO: Not the elegant way. Improvement neccessary
$pollList[] = (object) array_merge(
(array) json_decode(json_encode($poll)),
@ -105,6 +115,49 @@ class PollService {
return $pollList;
}
/**
* * Get list of polls
*
* @return Poll[]
*/
public function listForAdmin(): array {
$pollList = [];
$userId = \OC::$server->getUserSession()->getUser()->getUID();
if (\OC::$server->getGroupManager()->isAdmin($userId)) {
try {
$pollList = $this->pollMapper->findForAdmin($userId);
} catch (DoesNotExistException $e) {
// silent catch
}
}
return $pollList;
}
/**
* * Update poll configuration
*
* @return Poll
*/
public function takeover(int $pollId): Poll {
$this->poll = $this->pollMapper->find($pollId);
$originalOwner = $this->poll->getOwner();
$this->poll->setOwner(\OC::$server->getUserSession()->getUser()->getUID());
$this->pollMapper->update($this->poll);
$this->logService->setLog($this->poll->getId(), Log::MSG_ID_OWNERCHANGE);
// send notification to the original owner
$this->notificationService->createNotification([
'msgId' => 'takeOverPoll',
'objectType' => 'poll',
'objectValue' => $this->poll->getId(),
'recipient' => $originalOwner,
'actor' => $this->userId
]);
return $this->poll;
}
/**
* * get poll configuration
*
@ -112,11 +165,7 @@ class PollService {
*/
public function get(int $pollId): Poll {
$this->poll = $this->pollMapper->find($pollId);
$this->acl->setPoll($this->poll);
if (!$this->acl->getAllowView()) {
throw new NotAuthorizedException;
}
$this->acl->setPoll($this->poll)->requestView();
return $this->poll;
}
@ -198,7 +247,7 @@ class PollService {
*/
public function switchDeleted(int $pollId): Poll {
$this->poll = $this->pollMapper->find($pollId);
$this->acl->setPoll($this->poll)->requestEdit();
$this->acl->setPoll($this->poll)->requestDelete();
if ($this->poll->getDeleted()) {
$this->poll->setDeleted(0);
@ -209,6 +258,18 @@ class PollService {
$this->poll = $this->pollMapper->update($this->poll);
$this->logService->setLog($this->poll->getId(), Log::MSG_ID_DELETEPOLL);
if ($this->userId !== $this->poll->getOwner()) {
// send notification to the original owner
$this->notificationService->createNotification([
'msgId' => 'softDeletePollByOther',
'objectType' => 'poll',
'objectValue' => $this->poll->getId(),
'recipient' => $this->poll->getOwner(),
'actor' => $this->userId,
'pollTitle' => $this->poll->getTitle()
]);
}
return $this->poll;
}
@ -221,7 +282,20 @@ class PollService {
$this->poll = $this->pollMapper->find($pollId);
$this->acl->setPoll($this->poll)->requestDelete();
return $this->pollMapper->delete($this->poll);
$this->pollMapper->delete($this->poll);
if ($this->userId !== $this->poll->getOwner()) {
// send notification to the original owner
$this->notificationService->createNotification([
'msgId' => 'deletePollByOther',
'objectType' => 'poll',
'objectValue' => $this->poll->getId(),
'recipient' => $this->poll->getOwner(),
'actor' => $this->userId,
'pollTitle' => $this->poll->getTitle()
]);
}
return $this->poll;
}
/**
@ -231,7 +305,7 @@ class PollService {
*/
public function clone(int $pollId): Poll {
$origin = $this->pollMapper->find($pollId);
$this->acl->setPoll($origin);
$this->acl->setPoll($origin)->requestView();
$this->poll = new Poll();
$this->poll->setCreated(time());

Просмотреть файл

@ -101,7 +101,7 @@ class ShareService {
if ($this->share->getType() === Share::TYPE_PUBLIC && \OC::$server->getUserSession()->isLoggedIn()) {
try {
// Test if the user has already access.
$this->acl->setPollId($this->share->getPollId());
$this->acl->setPollId($this->share->getPollId())->requestView();
} catch (NotAuthorizedException $e) {
// If he is not authorized until now, create a new personal share for this user.
// Return the created share

Просмотреть файл

@ -50,7 +50,7 @@ class SubscriptionService {
if ($token) {
$this->acl->setToken($token);
} else {
$this->acl->setPollId($pollId);
$this->acl->setPollId($pollId)->requestView();
}
try {
@ -77,7 +77,7 @@ class SubscriptionService {
if ($token) {
$this->acl->setToken($token);
} else {
$this->acl->setPollId($pollId);
$this->acl->setPollId($pollId)->requestView();
}
if (!$subscribed) {

Просмотреть файл

@ -75,7 +75,7 @@ class VoteService {
if ($token) {
$this->acl->setToken($token);
} else {
$this->acl->setPollId($pollId);
$this->acl->setPollId($pollId)->requestView();
}
try {

Просмотреть файл

@ -136,6 +136,13 @@ export default {
showError(t('polls', 'Error loading poll list'))
})
}
if (getCurrentUser().isAdmin) {
this.$store.dispatch('pollsAdmin/load')
.catch(() => {
showError(t('polls', 'Error loading poll list'))
})
}
},
},
}

Просмотреть файл

@ -53,16 +53,23 @@ export default {
</script>
<style lang="scss" scoped>
.badge {
border: solid 1px;
border-radius: var(--border-radius);
padding: 1px 4px;
margin: 0 4px;
h2 .badge {
font-size: 60%;
}
.badge {
// border: solid 1px;
border-radius: var(--border-radius);
padding: 5px;
margin: 8px 4px;
text-align: center;
line-height: 1.1em;
overflow: hidden;
text-overflow: ellipsis;
&.withIcon {
padding-left: 26px;
padding-left: 25px !important;
text-align: left;
background-position: 4px center;
}

Просмотреть файл

@ -39,6 +39,10 @@
</AppNavigationItem>
</template>
<template #footer>
<AppNavigationItem v-if="showAdminSection"
:title="t('core', 'Administration')"
icon="icon-settings"
:to="{ name: 'administration' }" />
<AppNavigationItem :title="t('core', 'Settings')" icon="icon-settings" @click="showSettings()" />
</template>
</AppNavigation>
@ -47,8 +51,9 @@
<script>
import { AppNavigation, AppNavigationNew, AppNavigationItem } from '@nextcloud/vue'
import { showError } from '@nextcloud/dialogs'
import { mapGetters } from 'vuex'
import { showError } from '@nextcloud/dialogs'
import { getCurrentUser } from '@nextcloud/auth'
import CreateDlg from '../Create/CreateDlg'
import PollNavigationItems from './PollNavigationItems'
import { emit } from '@nextcloud/event-bus'
@ -114,6 +119,9 @@ export default {
...mapGetters({
filteredPolls: 'polls/filtered',
}),
showAdminSection() {
return getCurrentUser().isAdmin
},
},
created() {

Просмотреть файл

@ -54,73 +54,43 @@
<div v-else class="poll-item__item" :class="{ closed: closed, active: (poll.id === $store.state.poll.id) }">
<div v-tooltip.auto="pollType" :class="'item__type--' + poll.type" />
<router-link :to="{name: 'vote', params: {id: poll.id}}" class="item__title">
<div class="item__title__title">
{{ poll.title }}
<div class="item__title" @click="$emit('goto-poll')">
<div class="item-title-wrapper">
<div class="item__title__title">
{{ poll.title }}
</div>
</div>
<div class="item__title__description">
{{ poll.description ? poll.description : t('polls', 'No description provided') }}
</div>
</router-link>
<Actions :force-menu="true">
<ActionButton icon="icon-add"
:close-after-click="true"
@click="clonePoll()">
{{ t('polls', 'Clone poll') }}
</ActionButton>
<ActionButton v-if="poll.allowEdit && !poll.deleted"
icon="icon-delete"
:close-after-click="true"
@click="switchDeleted()">
{{ t('polls', 'Delete poll') }}
</ActionButton>
<ActionButton v-if="poll.allowEdit && poll.deleted"
icon="icon-history"
:close-after-click="true"
@click="switchDeleted()">
{{ t('polls', 'Restore poll') }}
</ActionButton>
<ActionButton v-if="poll.allowEdit && poll.deleted"
icon="icon-delete"
class="danger"
:close-after-click="true"
@click="deletePermanently()">
{{ t('polls', 'Delete poll permanently') }}
</ActionButton>
</Actions>
<div v-tooltip.auto="accessType" :class="'item__access--' + poll.access" @click="loadPoll()" />
<div class="item__owner" @click="loadPoll()">
</div>
<slot name="actions" />
<div v-tooltip.auto="accessType" :class="accessIcon" @click="$emit('load-poll')" />
<div class="item__owner" @click="$emit('load-poll')">
<UserItem :user-id="poll.owner" :display-name="poll.ownerDisplayName" />
</div>
<div class="wrapper" @click="loadPoll()">
<div class="wrapper" @click="$emit('load-poll')">
<div class="item__created">
{{ timeCreatedRelative }}
</div>
<div class="item__expiry">
{{ timeExpirationRelative }}
<Badge
:title="timeExpirationRelative"
:icon="expiryIcon"
:class="expiryClass" />
</div>
</div>
</div>
</template>
<script>
import { showError } from '@nextcloud/dialogs'
import { emit } from '@nextcloud/event-bus'
import { Actions, ActionButton } from '@nextcloud/vue'
import moment from '@nextcloud/moment'
import Badge from '../Base/Badge'
export default {
name: 'PollItem',
components: {
Actions,
ActionButton,
Badge,
},
props: {
@ -149,19 +119,34 @@ export default {
},
computed: {
closed() {
return (this.poll.expire > 0 && moment.unix(this.poll.expire).diff() < 0)
return (this.poll.expire && moment.unix(this.poll.expire).diff() < 0)
},
closeToClosing() {
return (!this.closed && this.poll.expire && moment.unix(this.poll.expire).diff() < 86400000)
},
accessType() {
if (this.poll.access === 'public') {
if (this.poll.deleted) {
return t('polls', 'Deleted')
} else if (this.poll.access === 'public') {
return t('polls', 'All users')
} else {
return t('polls', 'Only invited users')
}
},
accessIcon() {
if (this.poll.deleted) {
return 'item__access--deleted'
} else if (this.poll.access === 'public') {
return 'item__access--public'
} else {
return 'item__access--hidden'
}
},
pollType() {
if (this.poll.type === 'textPoll') {
return t('polls', 'Text poll')
@ -177,192 +162,171 @@ export default {
return t('polls', 'never')
}
},
expiryClass() {
if (this.closed) {
return 'error'
} else if (this.poll.expire && this.closeToClosing) {
return 'warning'
} else if (this.poll.expire && !this.closed) {
return 'success'
} else {
return 'success'
}
},
expiryIcon() {
if (this.poll.expire) {
return 'icon-calendar'
} else {
return 'icon-calendar'
}
},
timeCreatedRelative() {
return moment.unix(this.poll.created).fromNow()
},
},
methods: {
loadPoll() {
this.$store
.dispatch({ type: 'poll/get', pollId: this.poll.id })
.then(() => {
emit('toggle-sidebar', { open: true })
})
.catch((error) => {
console.error(error)
showError(t('polls', 'Error loading poll'))
})
},
toggleMenu() {
this.openedMenu = !this.openedMenu
},
hideMenu() {
this.openedMenu = false
},
switchDeleted() {
this.$store
.dispatch('poll/switchDeleted', { pollId: this.poll.id })
.then(() => {
emit('update-polls')
})
.catch(() => {
showError(t('polls', 'Error deleting poll.'))
})
this.hideMenu()
},
deletePermanently() {
this.$store
.dispatch('poll/delete', { pollId: this.poll.id })
.then(() => {
emit('update-polls')
})
.catch(() => {
showError(t('polls', 'Error deleting poll.'))
})
this.hideMenu()
},
clonePoll() {
this.$store
.dispatch('poll/clone', { pollId: this.poll.id })
.then(() => {
emit('update-polls')
})
.catch(() => {
showError(t('polls', 'Error cloning poll.'))
})
this.hideMenu()
},
},
}
</script>
<style lang="scss" scoped>
[class^='item__'] {
padding-right: 8px;
display: flex;
align-items: center;
flex: 0 0 auto;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.item__icon-spacer {
width: 44px;
min-width: 44px;
}
.item__title {
display: flex;
flex-direction: column;
flex: 1 0 auto;
align-items: stretch;
width: 210px;
}
.item__title__description {
opacity: 0.5;
}
.item__access {
width: 80px;
}
.item__owner {
width: 230px;
}
.wrapper {
width: 240px;
display: flex;
flex: 0 1 auto;
flex-wrap: wrap;
}
.item__created, .item__expiry {
width: 110px;
}
.closed {
.item__expiry {
color: var(--color-error);
<style lang="scss">
[class^='item__'], .action-item {
display: flex;
align-items: center;
flex: 0 0 auto;
padding-right: 8px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
}
[class^='poll-item__'] {
display: flex;
flex: 1;
padding: 4px 8px;
border-bottom: 1px solid var(--color-border-dark);
background-color: var(--color-main-background)
}
.poll-item__item > .action-item {
display:flex;
}
.poll-item__header {
opacity: 0.7;
flex: auto;
height: 4em;
align-items: center;
padding-left: 52px;
}
.item__icon-spacer {
width: 44px;
min-width: 44px;
}
.sortable {
cursor: pointer;
&:hover {
.sort-indicator.hidden {
visibility: visible;
display: block;
.item-title-wrapper {
display: flex;
}
.item__title {
display: flex;
flex-direction: column;
flex: 1 0 auto;
align-items: stretch;
width: 210px;
// display: inherit;
justify-content: center;
}
.item__title__description {
opacity: 0.5;
display: inherit;
}
.item__access {
width: 80px;
}
.item__owner {
width: 230px;
}
.wrapper {
width: 325px;
display: flex;
flex: 0 1 auto;
flex-wrap: wrap;
}
.item__created, {
width: 110px;
}
.item__expiry {
width: 185px;
.badge {
width: 100%;
}
}
}
[class^='item__type'] {
width: 44px;
background-repeat: no-repeat;
background-position: center;
min-width: 16px;
min-height: 16px;
}
.item__type--textPoll {
background-image: var(--icon-toggle-filelist-000);
}
.item__type--datePoll {
background-image: var(--icon-calendar-000);
}
[class^='item__access'] {
width: 44px;
background-repeat: no-repeat;
background-position: center;
min-width: 16px;
min-height: 16px;
}
.item__access--public {
background-image: var(--icon-polls-public-poll);
}
.item__access--hidden {
background-image: var(--icon-polls-hidden-poll);
}
.poll-item__item {
&.active {
background-color: var(--color-primary-light);
.closed {
.item__expiry {
color: var(--color-error);
}
}
&:hover {
background-color: var(--color-background-hover);
}
}
[class^='poll-item__'] {
display: flex;
flex: 1;
padding: 4px 8px;
border-bottom: 1px solid var(--color-border-dark);
background-color: var(--color-main-background);
}
.poll-item__item {
&.active {
background-color: var(--color-primary-light);
}
&:hover {
background-color: var(--color-background-hover);
}
}
.poll-item__header {
opacity: 0.7;
flex: auto;
height: 4em;
align-items: center;
padding-left: 52px;
}
.sortable {
cursor: pointer;
&:hover {
.sort-indicator.hidden {
visibility: visible;
display: block;
}
}
}
[class^='item__type'] {
width: 44px;
background-repeat: no-repeat;
background-position: center;
min-width: 16px;
min-height: 16px;
}
.item__type--textPoll {
background-image: var(--icon-toggle-filelist-000);
}
.item__type--datePoll {
background-image: var(--icon-calendar-000);
}
[class^='item__access'] {
width: 44px;
background-repeat: no-repeat;
background-position: center;
min-width: 16px;
min-height: 16px;
}
.item__access--public {
background-image: var(--icon-polls-public-poll);
}
.item__access--hidden {
background-image: var(--icon-polls-hidden-poll);
}
.item__access--deleted {
background-image: var(--icon-delete-000);
}
</style>

Просмотреть файл

@ -0,0 +1,269 @@
<!--
- @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-if="header" class="poll-item__header">
<div class="item__title sortable" @click="$emit('sort-list', {sort: 'title'})">
{{ t('polls', 'Title') }}
<span :class="['sort-indicator', { 'hidden': sort !== 'title'}, reverse ? 'icon-triangle-s' : 'icon-triangle-n']" />
</div>
<div class="item__icon-spacer" />
<div class="item__access sortable" @click="$emit('sort-list', {sort: 'access'})">
{{ t('polls', 'Access') }}
<span :class="['sort-indicator', { 'hidden': sort !== 'access'}, reverse ? 'icon-triangle-s' : 'icon-triangle-n']" />
</div>
<div class="item__owner sortable" @click="$emit('sort-list', {sort: 'owner'})">
{{ t('polls', 'Owner') }}
<span :class="['sort-indicator', { 'hidden': sort !== 'owner'}, reverse ? 'icon-triangle-s' : 'icon-triangle-n']" />
</div>
<div class="wrapper">
<div class="item__created sortable" @click="$emit('sort-list', {sort: 'created'})">
{{ t('polls', 'Created') }}
<span :class="['sort-indicator', { 'hidden': sort !== 'created'}, reverse ? 'icon-triangle-s' : 'icon-triangle-n']" />
</div>
<div class="item__expiry sortable" @click="$emit('sort-list', {sort: 'expire'})">
{{ t('polls', 'Closing Date') }}
<span :class="['sort-indicator', { 'hidden': sort !== 'expire'}, reverse ? 'icon-triangle-s' : 'icon-triangle-n']" />
</div>
</div>
</div>
<div v-else class="poll-item__item" :class="{ closed: closed, active: (poll.id === $store.state.poll.id) }">
<div v-tooltip.auto="pollType" :class="'item__type--' + poll.type" />
<div class="item__title">
<div class="item__title__title">
{{ poll.title }}
</div>
<div class="item__title__description">
{{ poll.description ? poll.description : t('polls', 'No description provided') }}
</div>
</div>
<slot name="actions" />
<div v-tooltip.auto="accessType" :class="'item__access--' + poll.access" />
<div class="item__owner">
<UserItem :user-id="poll.owner" :display-name="poll.ownerDisplayName" />
</div>
<div class="wrapper">
<div class="item__created">
{{ timeCreatedRelative }}
</div>
<div class="item__expiry">
{{ timeExpirationRelative }}
</div>
</div>
</div>
</template>
<script>
import moment from '@nextcloud/moment'
export default {
name: 'PollItemAdmin',
props: {
header: {
type: Boolean,
default: false,
},
poll: {
type: Object,
default: undefined,
},
sort: {
type: String,
default: 'created',
},
reverse: {
type: Boolean,
default: true,
},
},
data() {
return {
openedMenu: false,
}
},
computed: {
closed() {
return (this.poll.expire > 0 && moment.unix(this.poll.expire).diff() < 0)
},
accessType() {
if (this.poll.access === 'public') {
return t('polls', 'All users')
} else {
return t('polls', 'Only invited users')
}
},
pollType() {
if (this.poll.type === 'textPoll') {
return t('polls', 'Text poll')
} else {
return t('polls', 'Date poll')
}
},
timeExpirationRelative() {
if (this.poll.expire) {
return moment.unix(this.poll.expire).fromNow()
} else {
return t('polls', 'never')
}
},
timeCreatedRelative() {
return moment.unix(this.poll.created).fromNow()
},
},
}
</script>
<style lang="scss" scoped>
[class^='item__'] {
padding-right: 8px;
display: flex;
align-items: center;
flex: 0 0 auto;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.item__icon-spacer {
width: 44px;
min-width: 44px;
}
.item__title {
display: flex;
flex-direction: column;
flex: 1 0 auto;
align-items: stretch;
width: 210px;
}
.item__title__description {
opacity: 0.5;
}
.item__access {
width: 80px;
}
.item__owner {
width: 230px;
}
.wrapper {
width: 240px;
display: flex;
flex: 0 1 auto;
flex-wrap: wrap;
}
.item__created, .item__expiry {
width: 110px;
}
.closed {
.item__expiry {
color: var(--color-error);
}
}
[class^='poll-item__'] {
display: flex;
flex: 1;
padding: 4px 8px;
border-bottom: 1px solid var(--color-border-dark);
background-color: var(--color-main-background)
}
.poll-item__header {
opacity: 0.7;
flex: auto;
height: 4em;
align-items: center;
padding-left: 52px;
}
.sortable {
cursor: pointer;
&:hover {
.sort-indicator.hidden {
visibility: visible;
display: block;
}
}
}
[class^='item__type'] {
width: 44px;
background-repeat: no-repeat;
background-position: center;
min-width: 16px;
min-height: 16px;
}
.item__type--textPoll {
background-image: var(--icon-toggle-filelist-000);
}
.item__type--datePoll {
background-image: var(--icon-calendar-000);
}
[class^='item__access'] {
width: 44px;
background-repeat: no-repeat;
background-position: center;
min-width: 16px;
min-height: 16px;
}
.item__access--public {
background-image: var(--icon-polls-public-poll);
}
.item__access--hidden {
background-image: var(--icon-polls-hidden-poll);
}
.poll-item__item {
&.active {
background-color: var(--color-primary-light);
}
&:hover {
background-color: var(--color-background-hover);
}
}
</style>

Просмотреть файл

@ -28,6 +28,7 @@ import { generateUrl } from '@nextcloud/router'
// Dynamic loading
const List = () => import('./views/PollList')
const Administration = () => import('./views/Administration')
const Vote = () => import('./views/Vote')
const NotFound = () => import('./views/NotFound')
@ -55,6 +56,13 @@ export default new Router({
props: true,
name: 'list',
},
{
path: '/administration',
components: {
default: Administration,
},
name: 'administration',
},
{
path: '/not-found',
components: {

Просмотреть файл

@ -0,0 +1,74 @@
/*
* @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 { getCurrentUser } from '@nextcloud/auth'
import { generateUrl } from '@nextcloud/router'
const state = {
list: [],
}
const namespaced = true
const mutations = {
set(state, payload) {
Object.assign(state, payload)
},
}
const getters = {
filtered: (state) => (filterId) => {
return state.list
},
}
const actions = {
load(context) {
const endPoint = 'apps/polls/administration'
if (getCurrentUser().isAdmin) {
return axios.get(generateUrl(endPoint + '/polls'))
.then((response) => {
context.commit('set', { list: response.data })
})
.catch((error) => {
console.error('Error loading polls', { error: error.response })
})
}
},
takeOver(context, payload) {
const endPoint = 'apps/polls/administration'
if (getCurrentUser().isAdmin) {
return axios.get(generateUrl(endPoint + '/poll/' + payload.pollId + '/takeover'))
.then((response) => {
return response
})
.catch((error) => {
throw error
})
}
},
}
export default { namespaced, state, mutations, getters, actions }

Просмотреть файл

@ -0,0 +1,263 @@
<!--
- @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>
<AppContent class="poll-list">
<div class="area__header">
<h2 class="title">
{{ title }}
</h2>
</div>
<div class="area__main">
<div>
<h2 class="title">
{{ t('polls', 'Manage polls') }}
</h2>
<h3 class="description">
{{ t('polls', 'Manage polls of other users. You can take over the ownership or delete polls.') }}
</h3>
</div>
<EmptyContent v-if="noPolls" icon="icon-polls">
{{ t('polls', 'No polls found for this category') }}
<template #desc>
{{ t('polls', 'Add one or change category!') }}
</template>
</EmptyContent>
<transition-group v-else
name="list"
tag="div"
class="poll-list__list">
<PollItem key="0" :header="true"
:sort="sort" :reverse="reverse" @sort-list="setSort($event)" />
<PollItem v-for="(poll) in sortedList" :key="poll.id" :poll="poll">
<template #actions>
<Actions :force-menu="true">
<ActionButton icon="icon-add" :close-after-click="true"
@click="confirmTakeOver(poll.id, poll.owner)">
{{ t('polls', 'Take over') }}
</ActionButton>
<ActionButton :icon="poll.deleted ? 'icon-history' : 'icon-delete'" :close-after-click="true"
@click="switchDeleted(poll.id)">
{{ poll.deleted ? t('polls', 'Restore poll') : t('polls', 'Set "deleted" status') }}
</ActionButton>
<ActionButton icon="icon-delete" class="danger" :close-after-click="true"
@click="confirmDelete(poll.id)">
{{ t('polls', 'Delete poll permanently') }}
</ActionButton>
</Actions>
</template>
</PollItem>
</transition-group>
</div>
<LoadingOverlay v-if="isLoading" />
<Modal v-if="takeOverModal" @close="takeOverModal = false">
<div class="modal__content">
<h2>{{ t('polls', 'Do you want to take over this poll from {username} and change the ownership?', {username: takeOverOwner}) }}</h2>
<div>{{ t('polls', 'The original owner will be notified.') }}</div>
<div class="modal__buttons">
<ButtonDiv :title="t('polls', 'No')"
@click="takeOverModal = false" />
<ButtonDiv :primary="true" :title="t('polls', 'Yes')"
@click="takeOver()" />
</div>
</div>
</Modal>
<Modal v-if="deleteModal" @close="deleteModal = false">
<div class="modal__content">
<h2>{{ t('polls', 'Do you want to delete this poll?') }}</h2>
<div>{{ t('polls', 'This action cannot be reverted.') }}</div>
<div>{{ t('polls', 'The original owner will be notified.') }}</div>
<div class="modal__buttons">
<ButtonDiv :title="t('polls', 'No')"
@click="deleteModal = false" />
<ButtonDiv :primary="true" :title="t('polls', 'Yes')"
@click="deletePermanently()" />
</div>
</div>
</Modal>
</AppContent>
</template>
<script>
import { mapGetters } from 'vuex'
import { showError } from '@nextcloud/dialogs'
import { emit } from '@nextcloud/event-bus'
import { Actions, ActionButton, AppContent, EmptyContent, Modal } from '@nextcloud/vue'
import sortBy from 'lodash/sortBy'
import LoadingOverlay from '../components/Base/LoadingOverlay'
import PollItem from '../components/PollList/PollItem'
export default {
name: 'Administration',
components: {
AppContent,
Actions,
ActionButton,
LoadingOverlay,
PollItem,
EmptyContent,
Modal,
},
data() {
return {
isLoading: false,
sort: 'created',
reverse: true,
takeOverModal: false,
takeOverOwner: '',
takeOverPollId: 0,
deleteModal: false,
deletePollId: 0,
}
},
computed: {
...mapGetters({
filteredPolls: 'pollsAdmin/filtered',
}),
title() {
return t('polls', 'Administration')
},
windowTitle() {
return t('polls', 'Polls') + ' - ' + this.title
},
sortedList() {
if (this.reverse) {
return sortBy(this.filteredPolls(this.$route.params.type), this.sort).reverse()
} else {
return sortBy(this.filteredPolls(this.$route.params.type), this.sort)
}
},
noPolls() {
return this.sortedList.length < 1
},
},
watch: {
$route() {
this.refreshView()
},
},
mounted() {
this.refreshView()
},
methods: {
confirmTakeOver(pollId, owner) {
this.takeOverPollId = pollId
this.takeOverOwner = owner
this.takeOverModal = true
},
confirmDelete(pollId) {
this.deletePollId = pollId
this.deleteModal = true
},
switchDeleted(pollId) {
this.$store
.dispatch('poll/switchDeleted', { pollId: pollId })
.then(() => {
emit('update-polls')
})
.catch(() => {
showError(t('polls', 'Error switching deleted status.'))
})
},
deletePermanently() {
this.$store
.dispatch('poll/delete', { pollId: this.deletePollId })
.then(() => {
emit('update-polls')
this.deleteModal = false
})
.catch(() => {
showError(t('polls', 'Error deleting poll.'))
this.deleteModal = false
})
},
takeOver() {
this.$store
.dispatch('pollsAdmin/takeOver', { pollId: this.takeOverPollId })
.then(() => {
emit('update-polls')
this.takeOverModal = false
})
.catch(() => {
showError(t('polls', 'Error overtaking poll.'))
this.takeOverModal = false
})
},
refreshView() {
window.document.title = t('polls', 'Polls') + ' - ' + this.title
if (!this.filteredPolls(this.$route.params.type).find(poll => {
return poll.id === this.$store.state.poll.id
})) {
emit('toggle-sidebar', { open: false })
}
},
setSort(payload) {
if (this.sort === payload.sort) {
this.reverse = !this.reverse
} else {
this.sort = payload.sort
this.reverse = true
}
},
},
}
</script>
<style lang="scss" scoped>
.area__header {
margin-left: 33px !important;
}
.poll-list__list {
width: 100%;
display: flex;
flex-direction: column;
flex-wrap: nowrap;
overflow: scroll;
padding-bottom: 14px;
}
</style>

Просмотреть файл

@ -45,11 +45,42 @@
class="poll-list__list">
<PollItem key="0" :header="true"
:sort="sort" :reverse="reverse" @sort-list="setSort($event)" />
<li is="PollItem"
v-for="(poll, index) in sortedList"
:key="poll.id"
:poll="poll"
@clone-poll="callPoll(index, poll, 'clone')" />
<PollItem v-for="(poll) in sortedList" :key="poll.id" :poll="poll"
@goto-poll="gotoPoll(poll.id)"
@load-poll="loadPoll(poll.id)">
<template #actions>
<Actions :force-menu="true">
<ActionButton icon="icon-add"
:close-after-click="true"
@click="clonePoll(poll.id)">
{{ t('polls', 'Clone poll') }}
</ActionButton>
<ActionButton v-if="poll.allowEdit && !poll.deleted"
icon="icon-delete"
:close-after-click="true"
@click="switchDeleted(poll.id)">
{{ t('polls', 'Delete poll') }}
</ActionButton>
<ActionButton v-if="poll.allowEdit && poll.deleted"
icon="icon-history"
:close-after-click="true"
@click="switchDeleted(poll.id)">
{{ t('polls', 'Restore poll') }}
</ActionButton>
<ActionButton v-if="poll.allowEdit && poll.deleted"
icon="icon-delete"
class="danger"
:close-after-click="true"
@click="deletePermanently(poll.id)">
{{ t('polls', 'Delete poll permanently') }}
</ActionButton>
</Actions>
</template>
</PollItem>
</transition-group>
</div>
<LoadingOverlay v-if="isLoading" />
@ -57,18 +88,21 @@
</template>
<script>
import { AppContent, EmptyContent } from '@nextcloud/vue'
import PollItem from '../components/PollList/PollItem'
import { mapGetters } from 'vuex'
import sortBy from 'lodash/sortBy'
import LoadingOverlay from '../components/Base/LoadingOverlay'
import { showError } from '@nextcloud/dialogs'
import { emit } from '@nextcloud/event-bus'
import { Actions, ActionButton, AppContent, EmptyContent } from '@nextcloud/vue'
import PollItem from '../components/PollList/PollItem'
import LoadingOverlay from '../components/Base/LoadingOverlay'
export default {
name: 'PollList',
components: {
AppContent,
Actions,
ActionButton,
LoadingOverlay,
PollItem,
EmptyContent,
@ -156,6 +190,24 @@ export default {
},
methods: {
gotoPoll(pollId) {
this.$router
.push({ name: 'vote', params: { id: pollId } })
},
loadPoll(pollId) {
this.$store
.dispatch({ type: 'poll/get', pollId: pollId })
.then(() => {
emit('toggle-sidebar', { open: true })
})
.catch((error) => {
console.error(error)
showError(t('polls', 'Error loading poll'))
})
},
refreshView() {
window.document.title = t('polls', 'Polls') + ' - ' + this.title
if (!this.filteredPolls(this.$route.params.type).find(poll => {
@ -183,6 +235,39 @@ export default {
},
})
},
switchDeleted(pollId) {
this.$store
.dispatch('poll/switchDeleted', { pollId: pollId })
.then(() => {
emit('update-polls')
})
.catch(() => {
showError(t('polls', 'Error deleting poll.'))
})
},
deletePermanently(pollId) {
this.$store
.dispatch('poll/delete', { pollId: pollId })
.then(() => {
emit('update-polls')
})
.catch(() => {
showError(t('polls', 'Error deleting poll.'))
})
},
clonePoll(pollId) {
this.$store
.dispatch('poll/clone', { pollId: pollId })
.then(() => {
emit('update-polls')
})
.catch(() => {
showError(t('polls', 'Error cloning poll.'))
})
},
},
}
</script>

Просмотреть файл

@ -43,13 +43,13 @@
<h2 class="title">
{{ poll.title }}
<Badge v-if="closed"
:title="dateExpiryString"
:title="t('polls', 'Closed {relativeTimeAgo}', {relativeTimeAgo: timeExpirationRelative})"
icon="icon-polls-closed"
class="error" />
:class="expiryClass" />
<Badge v-if="!closed && poll.expire"
:title="dateExpiryString"
:title="t('polls', 'Closing {relativeExpirationTime}', {relativeExpirationTime: timeExpirationRelative})"
icon="icon-calendar"
class="success" />
:class="expiryClass" />
<Badge v-if="poll.deleted"
:title="t('polls', 'Deleted')"
icon="icon-delete"
@ -194,9 +194,34 @@ export default {
return t('polls', 'Polls') + ' - ' + this.poll.title
},
dateExpiryString() {
return moment.unix(this.poll.expire).format('LLLL')
// dateExpiryString() {
// return moment.unix(this.poll.expire).format('LLLL')
// },
//
timeExpirationRelative() {
if (this.poll.expire) {
return moment.unix(this.poll.expire).fromNow()
} else {
return t('polls', 'never')
}
},
closeToClosing() {
return (!this.poll.closed && this.poll.expire && moment.unix(this.poll.expire).diff() < 86400000)
},
expiryClass() {
if (this.closed) {
return 'error'
} else if (this.poll.expire && this.closeToClosing) {
return 'warning'
} else if (this.poll.expire && !this.closed) {
return 'success'
} else {
return 'success'
}
},
viewCaption() {
if (this.viewMode === 'desktop') {
return t('polls', 'Switch to mobile view')