Merge pull request #1281 from nextcloud/admin-section
Adding admin section
This commit is contained in:
Коммит
5a76364c53
|
@ -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')
|
||||
|
|
Загрузка…
Ссылка в новой задаче