diff options
author | dartcafe <github@dartcafe.de> | 2019-12-09 01:17:11 +0300 |
---|---|---|
committer | dartcafe <github@dartcafe.de> | 2019-12-09 01:17:11 +0300 |
commit | 1d4a9334c7298416032b2f453e174c35d70217ce (patch) | |
tree | 44feef8a7f7439b8789ed9e5b7260fd4fb816663 | |
parent | b26f96c7e932ef9d178cdd6e041cdee11218e354 (diff) |
* some enhancements
44 files changed, 999 insertions, 1005 deletions
diff --git a/appinfo/info.xml b/appinfo/info.xml index d66c0b08..9dc6f0da 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -23,6 +23,6 @@ <screenshot>https://raw.githubusercontent.com/nextcloud/polls/master/screenshots/vote.png</screenshot> <screenshot>https://raw.githubusercontent.com/nextcloud/polls/master/screenshots/edit-poll.png</screenshot> <dependencies> - <nextcloud min-version="14" max-version="17" /> + <nextcloud min-version="14" max-version="18" /> </dependencies> </info> diff --git a/appinfo/routes.php b/appinfo/routes.php index b3065269..f86a10b1 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -24,16 +24,13 @@ return [ 'routes' => [ ['name' => 'page#index', 'url' => '/', 'verb' => 'GET'], - ['name' => 'page#vote_poll', 'url' => '/vote/{pollId}', 'verb' => 'GET'], ['name' => 'page#vote_public', 'url' => '/s/{pollId}', 'verb' => 'GET'], - ['name' => 'PublicAPI#get', 'url' => '/api/{token}', 'verb' => 'GET' ], ['name' => 'notification#get', 'url' => '/get/notification/{pollId}', 'verb' => 'GET'], ['name' => 'notification#set', 'url' => '/set/notification/', 'verb' => 'POST'], ['name' => 'comment#get', 'url' => '/get/comments/{pollId}', 'verb' => 'GET'], - ['name' => 'comment#test', 'url' => '/get/test/{pollId}', 'verb' => 'GET'], ['name' => 'comment#write', 'url' => '/write/comment', 'verb' => 'POST'], ['name' => 'comment#getByToken', 'url' => '/get/comments/s/{token}', 'verb' => 'GET'], @@ -51,8 +48,7 @@ return [ ['name' => 'event#list', 'url' => '/get/events', 'verb' => 'GET'], ['name' => 'event#get', 'url' => '/get/event/{pollId}', 'verb' => 'GET'], - ['name' => 'event#write', 'url' => '/write/event/', 'verb' => 'POST'], - ['name' => 'event#add', 'url' => '/add/event/', 'verb' => 'POST'], + ['name' => 'event#write', 'url' => '/write/event', 'verb' => 'POST'], ['name' => 'event#getByToken', 'url' => '/get/event/s/{token}', 'verb' => 'GET'], ['name' => 'share#getShares', 'url' => '/get/shares/{pollId}', 'verb' => 'GET'], @@ -61,7 +57,7 @@ return [ ['name' => 'share#remove', 'url' => '/remove/share', 'verb' => 'POST'], ['name' => 'share#get', 'url' => '/get/share/{token}', 'verb' => 'GET'], - ['name' => 'acl#getByToken', 'url' => '/get/aclbytoken/{token}', 'verb' => 'GET'], + ['name' => 'acl#getByToken', 'url' => '/get/acl/s/{token}', 'verb' => 'GET'], ['name' => 'acl#get', 'url' => '/get/acl/{id}', 'verb' => 'GET'], ['name' => 'system#get_site_users_and_groups', 'url' => '/get/siteusers', 'verb' => 'POST'], diff --git a/img/expired-unvoted-vote.svg b/img/expired-unvoted-vote.svg deleted file mode 100644 index ca46c722..00000000 --- a/img/expired-unvoted-vote.svg +++ /dev/null @@ -1,14 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16"> - <ellipse - style="opacity:1;fill:none;fill-opacity:1;stroke:#f45573;stroke-width:1.49987304;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" - cx="8" - cy="8" - rx="6.2500634" - ry="6.2500639" /> - <rect - style="fill:none;fill-opacity:1;stroke:#ffc107;stroke-width:0.86666656;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" - width="5.6333332" - height="5.6333332" - x="5.1833334" - y="5.1833334" /> -</svg> diff --git a/img/expired-voted-vote.svg b/img/expired-voted-vote.svg deleted file mode 100644 index a58f41b0..00000000 --- a/img/expired-voted-vote.svg +++ /dev/null @@ -1,11 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16"> - <ellipse - style="opacity:1;fill:none;fill-opacity:1;stroke:#f45573;stroke-width:1.49987304;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" - cx="8" - cy="8" - rx="6.2500634" - ry="6.2500639" /> - <path - d="M 6.9766048,11.334813 3.39273,7.7509392 4.4157633,6.7271819 6.9766048,9.285851 11.569757,4.6651869 12.60727,5.7034244 Z" - style="fill:#49bc49;fill-opacity:1" /> -</svg> diff --git a/img/no-comment.svg b/img/no-comment.svg deleted file mode 100644 index 1ac7b557..00000000 --- a/img/no-comment.svg +++ /dev/null @@ -1,5 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16"> - <path - style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#f45573;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.39999998;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" - d="M 2.7395492,2.1143382 C 2.406783,2.1147393 2.1371235,2.3821537 2.1367191,2.7121496 l 0,6.7829882 c 4.043e-4,0.329996 0.2700638,0.5974112 0.6028301,0.5978122 l 0.5846175,0 -1.1049559,3.792712 3.2785391,-3.792712 7.7627011,0 c 0.332766,-4.01e-4 0.602426,-0.2678152 0.60283,-0.5978122 l 0,-6.7829882 c -4.04e-4,-0.329996 -0.270064,-0.5974105 -0.60283,-0.5978114 z" /> -</svg> diff --git a/img/open-unvoted-vote.svg b/img/open-unvoted-vote.svg deleted file mode 100644 index 4376c065..00000000 --- a/img/open-unvoted-vote.svg +++ /dev/null @@ -1,14 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16"> - <path - style="opacity:1;fill:none;fill-opacity:1;stroke:#49bc49;stroke-width:1.5;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" - d="m 14.244065,8.0059972 c 0,3.4518138 -2.798249,6.2500608 -6.2500623,6.2500608 -3.4518138,0 -6.250062,-2.798247 -6.2500622,-6.2500608 0,-3.4518134 2.7982482,-6.2500612 6.2500622,-6.2500612" /> - <path - style="fill:#49bc49;fill-opacity:1;stroke:none;stroke-width:0.11827402;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" - d="M 7.9774453,0.41685427 11.667719,1.8149286 7.9774453,3.2780296 v 0 z" /> - <rect - style="fill:none;fill-opacity:1;stroke:#ffc107;stroke-width:0.86666656;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" - width="5.6333332" - height="5.6333332" - x="5.1833334" - y="5.1833334" /> -</svg> diff --git a/img/open-voted-vote.svg b/img/open-voted-vote.svg deleted file mode 100644 index 58e5764f..00000000 --- a/img/open-voted-vote.svg +++ /dev/null @@ -1,11 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16"> - <path - d="M 6.9766048,11.334813 3.39273,7.750939 4.4157633,6.7271817 6.9766048,9.2858508 11.569757,4.6651867 12.60727,5.7034243 Z" - style="fill:#49bc49;fill-opacity:1" /> - <path - style="opacity:1;fill:none;fill-opacity:1;stroke:#49bc49;stroke-width:1.5;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" - d="m 14.250063,7.9999999 c 0,3.4518141 -2.798249,6.2500611 -6.2500635,6.2500611 -3.4518138,0 -6.250062,-2.798247 -6.2500622,-6.2500611 0,-3.4518134 2.7982482,-6.2500609 6.2500622,-6.2500609" /> - <path - style="fill:#49bc49;fill-opacity:1;stroke:none;stroke-width:0.11827402;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" - d="M 7.977,0.4168247 11.667274,1.8148987 7.977,3.278 v 0 z" /> -</svg> diff --git a/img/save.svg b/img/save.svg deleted file mode 100644 index f8fbaae9..00000000 --- a/img/save.svg +++ /dev/null @@ -1,35 +0,0 @@ -<?xml version="1.0" encoding="UTF-8" standalone="no"?> -<svg - xmlns:dc="http://purl.org/dc/elements/1.1/" - xmlns:cc="http://creativecommons.org/ns#" - xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" - xmlns:svg="http://www.w3.org/2000/svg" - xmlns="http://www.w3.org/2000/svg" - id="svg4" - height="16" - width="16" - viewbox="0 0 16 16" - version="1.1"> - <metadata - id="metadata10"> - <rdf:RDF> - <cc:Work - rdf:about=""> - <dc:format>image/svg+xml</dc:format> - <dc:type - rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> - <dc:title></dc:title> - </cc:Work> - </rdf:RDF> - </metadata> - <defs - id="defs8" /> - <g - id="XMLID_2_" - transform="matrix(0.22613747,0,0,0.23288366,-2.8432917,-3.1784157)"> - <path - d="M 19.4,18 V 78 H 76.5 V 31.4 L 65.3,18 H 58.4 V 40.9 H 28 V 18 Z m 22.9,2.9 V 35.2 H 53.7 V 20.9 Z M 28,50.4 H 68 V 72.3 H 28 Z m 5.7,4.7 V 58 h 28.6 v -2.9 z m 0,8.6 v 2.9 h 28.6 v -2.9 z" - class="st0" - id="XMLID_9_" /> - </g> -</svg> diff --git a/img/share.svg b/img/share.svg deleted file mode 100644 index d67d35c6..00000000 --- a/img/share.svg +++ /dev/null @@ -1,6 +0,0 @@ -<?xml version="1.0" encoding="UTF-8" standalone="no"?> -<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="16" width="16" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/"> - <g transform="translate(0 -1036.4)"> - <path d="m12.228 1037.4c-1.3565 0-2.4592 1.0977-2.4592 2.4542 0 0.075 0.0084 0.1504 0.0149 0.2236l-4.7346 2.4145c-0.4291-0.3667-0.98611-0.5863-1.5947-0.5863-1.3565 0-2.4542 1.0977-2.4542 2.4543 0 1.3565 1.0977 2.4542 2.4542 2.4542 0.54607 0 1.0528-0.1755 1.4606-0.477l4.8637 2.4741c-0.0024 0.044-0.0099 0.089-0.0099 0.1342 0 1.3565 1.1027 2.4542 2.4592 2.4542s2.4542-1.0977 2.4542-2.4542-1.0977-2.4592-2.4542-2.4592c-0.63653 0-1.218 0.2437-1.6544 0.6409l-4.6953-2.4c0.01892-0.1228 0.03478-0.2494 0.03478-0.3775 0-0.072-0.0089-0.1437-0.0149-0.2137l4.7395-2.4145c0.42802 0.3627 0.98488 0.5813 1.5898 0.5813 1.3565 0 2.4542-1.1027 2.4542-2.4592s-1.0977-2.4542-2.4542-2.4542z"/> - </g> -</svg> diff --git a/img/toggle.svg b/img/toggle.svg deleted file mode 100644 index 71d260f7..00000000 --- a/img/toggle.svg +++ /dev/null @@ -1,86 +0,0 @@ -<?xml version="1.0" encoding="UTF-8" standalone="no"?> -<svg - xmlns:dc="http://purl.org/dc/elements/1.1/" - xmlns:cc="http://creativecommons.org/ns#" - xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" - xmlns:svg="http://www.w3.org/2000/svg" - xmlns="http://www.w3.org/2000/svg" - xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" - xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" - id="svg4" - height="20" - width="4" - viewbox="0 0 16 16" - version="1.1" - sodipodi:docname="toggle.svg" - inkscape:version="0.92.2 (5c3e80d, 2017-08-06)"> - <sodipodi:namedview - pagecolor="#ffffff" - bordercolor="#666666" - borderopacity="1" - objecttolerance="10" - gridtolerance="10" - guidetolerance="10" - inkscape:pageopacity="0" - inkscape:pageshadow="2" - inkscape:window-width="1920" - inkscape:window-height="1017" - id="namedview6" - showgrid="false" - inkscape:zoom="32" - inkscape:cx="-2.3727798" - inkscape:cy="9.3760237" - inkscape:window-x="-8" - inkscape:window-y="-8" - inkscape:window-maximized="1" - inkscape:current-layer="svg4" /> - <metadata - id="metadata10"> - <rdf:RDF> - <cc:Work - rdf:about=""> - <dc:format>image/svg+xml</dc:format> - <dc:type - rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> - <dc:title></dc:title> - </cc:Work> - </rdf:RDF> - </metadata> - <defs - id="defs8" /> - <rect - style="fill:#00000d;fill-opacity:1;stroke:#000000;stroke-width:0.5714286;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:19.89999962;stroke-dasharray:none;stroke-opacity:1;paint-order:markers fill stroke" - id="rect4513" - width="1.4285715" - height="1.4285713" - x="1.2857143" - y="13.285715" /> - <rect - style="fill:#00000d;fill-opacity:1;stroke:#000000;stroke-width:0.5714286;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:19.89999962;stroke-dasharray:none;stroke-opacity:1;paint-order:markers fill stroke" - id="rect4513-2" - width="1.4285715" - height="1.4285715" - x="1.2857143" - y="17.285715" /> - <rect - style="fill:#00000d;fill-opacity:1;stroke:#000000;stroke-width:0.5714286;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:19.89999962;stroke-dasharray:none;stroke-opacity:1;paint-order:markers fill stroke" - id="rect4513-6" - width="1.4285715" - height="1.4285715" - x="1.2857143" - y="9.2857151" /> - <rect - style="fill:#00000d;fill-opacity:1;stroke:#000000;stroke-width:0.5714286;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:19.89999962;stroke-dasharray:none;stroke-opacity:1;paint-order:markers fill stroke" - id="rect4513-26" - width="1.4285716" - height="1.4285715" - x="1.2857141" - y="5.2857151" /> - <rect - style="fill:#00000d;fill-opacity:1;stroke:#000000;stroke-width:0.5714286;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:19.89999962;stroke-dasharray:none;stroke-opacity:1;paint-order:markers fill stroke" - id="rect4513-9" - width="1.4285715" - height="1.4285715" - x="1.2857143" - y="1.2857143" /> -</svg> diff --git a/img/yes-comment.svg b/img/yes-comment.svg deleted file mode 100644 index 57d21f20..00000000 --- a/img/yes-comment.svg +++ /dev/null @@ -1,5 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16"> - <path - style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#49bc49;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.39999998;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" - d="M 2.7395491,2.114338 C 2.4067829,2.1147391 2.1371234,2.3821535 2.136719,2.7121494 l 0,6.7829882 c 4.043e-4,0.329996 0.2700638,0.5974114 0.6028301,0.5978124 l 0.5846175,0 -1.1049559,3.792712 3.2785391,-3.792712 7.7627012,0 c 0.332766,-4.01e-4 0.602426,-0.2678154 0.60283,-0.5978124 l 0,-6.7829882 C 13.862877,2.3821534 13.593217,2.1147389 13.260451,2.114338 Z" /> -</svg> diff --git a/lib/Controller/AclController.php b/lib/Controller/AclController.php index 0986084d..4d3467d7 100644 --- a/lib/Controller/AclController.php +++ b/lib/Controller/AclController.php @@ -66,6 +66,7 @@ class AclController extends Controller { /** * Read acl with share token * @NoAdminRequired + * @PublicPage * @NoCSRFRequired * @param integer $pollId * @return array diff --git a/lib/Controller/EventController.php b/lib/Controller/EventController.php index 794594c9..61a0f053 100644 --- a/lib/Controller/EventController.php +++ b/lib/Controller/EventController.php @@ -42,17 +42,16 @@ use OCA\Polls\Db\EventMapper; use OCA\Polls\Service\EventService; use OCA\Polls\Model\Acl; - - class EventController extends Controller { - private $userId; - private $mapper; - private $logger; - private $groupManager; - private $userManager; - private $eventService; - private $acl; + // private $userId; + // private $mapper; + // private $logger; + // private $groupManager; + // private $userManager; + // private $eventService; + // private $acl; + // private $event; /** * CommentController constructor. @@ -73,6 +72,7 @@ class EventController extends Controller { IRequest $request, ILogger $logger, EventMapper $mapper, + Event $event, IGroupManager $groupManager, IUserManager $userManager, EventService $eventService, @@ -85,6 +85,7 @@ class EventController extends Controller { $this->groupManager = $groupManager; $this->userManager = $userManager; $this->eventService = $eventService; + $this->event = $event; $this->acl = $acl; } @@ -99,7 +100,14 @@ class EventController extends Controller { $events = []; if (\OC::$server->getUserSession()->isLoggedIn()) { try { - $events = $this->mapper->findAll(); + + $events = array_filter($this->mapper->findAll(), function($item) { + if ($this->acl->setPollId($item->getId())->getAllowView()) { + return true; + } else { + return false; + } + }); } catch (DoesNotExistException $e) { $events = []; // return new DataResponse($e, Http::STATUS_NOT_FOUND); @@ -117,56 +125,56 @@ class EventController extends Controller { * @return array */ public function get($pollId) { - $data = array(); try { if (!$this->acl->getFoundByToken()) { $this->acl->setPollId($pollId); } - $event = $this->mapper->find($pollId); + $this->event = $this->mapper->find($pollId); } catch (DoesNotExistException $e) { $this->logger->info('Poll ' . $pollId . ' not found!', ['app' => 'polls']); return new DataResponse($e, Http::STATUS_NOT_FOUND); } - if ($event->getType() == 0) { + if ($this->event->getType() == 0) { $pollType = 'datePoll'; } else { $pollType = 'textPoll'; } - $accessType = $event->getAccess(); - if (!strpos('|public|hidden|registered', $accessType)) { - $accessType = 'select'; - } - if ($event->getExpire() == null) { - $expired = false; - $expiration = false; + // TODO: add migration for this + if ($this->event->getAccess() === 'public') { + $accessType = 'public'; + } else if ($this->event->getAccess() === 'hidden') { + $accessType = 'hidden'; + } else if ($this->event->getAccess() === 'registered') { + $accessType = 'public'; + $this->event->setAccess('public'); } else { - $expired = time() > strtotime($event->getExpire()); - $expiration = true; + $accessType = 'hidden'; + $this->event->setAccess('hidden'); } return new DataResponse((object) [ - 'id' => $event->getId(), - 'type' => $pollType, - 'title' => $event->getTitle(), - 'description' => $event->getDescription(), - 'owner' => $event->getOwner(), - 'ownerDisplayName' => $this->userManager->get($event->getOwner())->getDisplayName(), - 'created' => $event->getCreated(), - 'access' => $accessType, - 'expiration' => $expiration, - 'expired' => $expired, - 'expirationDate' => $event->getExpire(), - 'isAnonymous' => boolval($event->getIsAnonymous()), - 'fullAnonymous' => boolval($event->getFullAnonymous()), - 'allowMaybe' => boolval($event->getAllowMaybe()), - 'acl' => $this->acl - ], - Http::STATUS_OK); + 'id' => $this->event->getId(), + 'type' => $pollType, + 'title' => $this->event->getTitle(), + 'description' => $this->event->getDescription(), + 'owner' => $this->event->getOwner(), + 'created' => $this->event->getCreated(), + 'access' => $this->event->getAccess(), + 'expire' => $this->event->getExpire(), + 'isAnonymous' => boolval($this->event->getIsAnonymous()), + 'fullAnonymous' => boolval($this->event->getFullAnonymous()), + 'allowMaybe' => boolval($this->event->getAllowMaybe()), + 'voteLimit' => $this->event->getVoteLimit(), + 'showResults' =>$this->event->getShowResults(), + 'deleted' => boolval($this->event->getDeleted()), + 'deleteDate' => $this->event->getDeleteDate() + ], + Http::STATUS_OK); } @@ -190,126 +198,63 @@ class EventController extends Controller { } - - /** * Write poll (create/update) * @NoAdminRequired * @param Array $event - * @param Array $options - * @param Array $shares - * @param string $mode * @return DataResponse */ - public function add($event) { - if (!\OC::$server->getUserSession()->isLoggedIn()) { - return new DataResponse(null, Http::STATUS_UNAUTHORIZED); - } - - $NewEvent = new Event(); - - // Set the configuration options entered by the user - $NewEvent->setTitle($event['title']); - $NewEvent->setDescription($event['description']); - - if ($event['type'] === 'datePoll') { - $NewEvent->setType(0); - } elseif ($event['type'] === 'textPoll') { - $NewEvent->setType(1); - } - - $NewEvent->setAccess('hidden'); - $NewEvent->setIsAnonymous(0); - $NewEvent->setFullAnonymous(0); - $NewEvent->setAllowMaybe(0); - - $NewEvent->setOwner($this->userId); - $NewEvent->setCreated(date('Y-m-d H:i:s')); - $NewEvent = $this->mapper->insert($NewEvent); - - return new DataResponse($this->get($NewEvent->getId()), Http::STATUS_OK); - - } - - /** - * Write poll (create/update) - * @NoAdminRequired - * @param Array $event - * @param Array $options - * @param Array $shares - * @param string $mode - * @return DataResponse - */ - - public function write($event, $mode) { - if (!\OC::$server->getUserSession()->isLoggedIn()) { - return new DataResponse(null, Http::STATUS_UNAUTHORIZED); - } else { - $adminAccess = $this->groupManager->isAdmin($this->userId); - } - - $NewEvent = new Event(); - - // Set the configuration options entered by the user - $NewEvent->setTitle($event['title']); - $NewEvent->setDescription($event['description']); - - $NewEvent->setIsAnonymous(intval($event['isAnonymous'])); - $NewEvent->setFullAnonymous(intval($event['fullAnonymous'])); - $NewEvent->setAllowMaybe(intval($event['allowMaybe'])); - - if ($event['access'] === 'select') { - } else { - $NewEvent->setAccess($event['access']); - } - - if ($event['expiration']) { - $NewEvent->setExpire(date('Y-m-d H:i:s', strtotime($event['expirationDate']))); - } else { - $NewEvent->setExpire(null); - } - - if ($event['type'] === 'datePoll') { - $NewEvent->setType(0); - } elseif ($event['type'] === 'textPoll') { - $NewEvent->setType(1); - } + public function write($event) { try { // Find existing poll - $oldEvent = $this->mapper->find($event['id']); + $this->event = $this->mapper->find($event['id']); + $this->acl->setPollId($this->event->getId()); - // Check if current user is allowed to edit existing poll - if ($oldEvent->getOwner() !== $this->userId && !$adminAccess) { - // If current user is not owner of existing poll deny access - return new DataResponse(null, Http::STATUS_UNAUTHORIZED); + if (!$this->acl->getAllowEdit()) { + $this->logger->alert('Unauthorized delete attempt from user ' . $this->userId); + return new DataResponse('Unauthorized delete attempt.', Http::STATUS_UNAUTHORIZED); } - // else take owner, and id of existing poll - $NewEvent->setOwner($oldEvent->getOwner()); - $NewEvent->setId($oldEvent->getId()); - try { - $this->mapper->update($NewEvent); - - } catch (Exception $e) { - $this->logger->alert('Poll ' . $oldEvent['id'] . ' not found!', ['app' => 'polls']); - return new DataResponse(null, Http::STATUS_NOT_FOUND); + } catch (Exception $e) { + $this->event = new Event(); + + // $this->event->setType($event['type']); + if ($event['type'] === 'datePoll') { + $this->event->setType(0); + } elseif ($event['type'] === 'textPoll') { + $this->event->setType(1); + } else { + $this->event->setType($event['type']); } - } catch (Exception $e) { + $this->event->setOwner($this->userId); + $this->event->setCreated(date('Y-m-d H:i:s',time())); + $this->logger->error(date('Y-m-d H:i:s',time())); + } finally { - // Create new poll - // Define current user as owner, set new creation date - $NewEvent->setOwner($this->userId); - $NewEvent->setCreated(date('Y-m-d H:i:s')); - $NewEvent = $this->mapper->insert($NewEvent); + $this->event->setTitle($event['title']); + $this->event->setDescription($event['description']); - } finally { + $this->event->setAccess($event['access']); + // $this->event->setExpire($event['expire']); + if ($event['expire']) { + $this->event->setExpire(date('Y-m-d H:i:s', strtotime($event['expire']))); + } else { + $this->event->setExpire(null); + } + $this->event->setIsAnonymous(intval($event['isAnonymous'])); + $this->event->setFullAnonymous(intval($event['fullAnonymous'])); + $this->event->setAllowMaybe(intval($event['allowMaybe'])); + $this->event->setDeleted($event['deleted']); + // $this->event->setDeleteDate(time()); + $this->event->setVoteLimit(intval($event['voteLimit'])); + $this->event->setShowResults($event['showResults']); - return new DataResponse($this->get($NewEvent->getId()), Http::STATUS_OK); + $this->mapper->insertOrUpdate($this->event); + return new DataResponse($this->event, Http::STATUS_OK); } - } } diff --git a/lib/Controller/PageController.php b/lib/Controller/PageController.php index 06b88078..5d7e2602 100644 --- a/lib/Controller/PageController.php +++ b/lib/Controller/PageController.php @@ -58,7 +58,16 @@ class PageController extends Controller { * @NoAdminRequired * @NoCSRFRequired */ - public function index() { + public function index($path = 'all') { + return new TemplateResponse('polls', 'polls.tmpl', + ['urlGenerator' => $this->urlGenerator]); + } + + /** + * @NoAdminRequired + * @NoCSRFRequired + */ + public function all($path = 'all') { return new TemplateResponse('polls', 'polls.tmpl', ['urlGenerator' => $this->urlGenerator]); } diff --git a/lib/Controller/ShareController.php b/lib/Controller/ShareController.php index ab00c1ce..d1c4aae8 100644 --- a/lib/Controller/ShareController.php +++ b/lib/Controller/ShareController.php @@ -110,7 +110,7 @@ class ShareController extends Controller { * @return DataResponse */ public function getShares($pollId) { - // $this->acl->setPollId($pollId) + if ($this->acl->setPollId($pollId)->getAllowEdit()) { try { $event = $this->eventMapper->find($pollId); diff --git a/lib/Db/Event.php b/lib/Db/Event.php index a6aa7080..289c1c5d 100644 --- a/lib/Db/Event.php +++ b/lib/Db/Event.php @@ -50,6 +50,8 @@ use OCP\AppFramework\Db\Entity; * @method void setFullAnonymous(integer $value) * @method integer getAllowMaybe() * @method void setAllowMaybe(integer $value) + * @method integer getData() + * @method void setData(integer $value) */ class Event extends Entity implements JsonSerializable { protected $type; @@ -63,6 +65,10 @@ class Event extends Entity implements JsonSerializable { protected $isAnonymous; protected $fullAnonymous; protected $allowMaybe; + protected $showResults; + protected $voteLimit; + protected $deleted; + protected $deleteDate; public function jsonSerialize() { return [ @@ -74,9 +80,13 @@ class Event extends Entity implements JsonSerializable { 'created' => $this->created, 'access' => $this->access, 'expire' => $this->expire, - 'isAnonymous' => $this->isAnonymous, - 'fullAnonymous' => $this->fullAnonymous, - 'allowMaybe' => $this->allowMaybe + 'isAnonymous' => boolval($this->isAnonymous), + 'fullAnonymous' => boolval($this->fullAnonymous), + 'allowMaybe' => boolval($this->allowMaybe), + 'voteLimit' => $this->showResults, + 'showResults' => $this->showResults, + 'deleted' => boolval($this->deleted), + 'deleteDate' => $this->deleteDate ]; } } diff --git a/lib/Db/EventMapper.php b/lib/Db/EventMapper.php index f8fcd3c3..b9788cd2 100644 --- a/lib/Db/EventMapper.php +++ b/lib/Db/EventMapper.php @@ -65,7 +65,8 @@ class EventMapper extends QBMapper { $qb = $this->db->getQueryBuilder(); $qb->select('*') - ->from($this->getTableName()); + ->from($this->getTableName()) +; return $this->findEntities($qb); } diff --git a/lib/Migration/Version0010Date20190801063812.php b/lib/Migration/Version0010Date20190801063812.php index a334e056..c6c2fecd 100644 --- a/lib/Migration/Version0010Date20190801063812.php +++ b/lib/Migration/Version0010Date20190801063812.php @@ -32,6 +32,7 @@ use OCP\IConfig; use OCP\IDBConnection; use OCP\Migration\SimpleMigrationStep; use OCP\Migration\IOutput; +use OCP\Security\ISecureRandom; /** * Installation class for the polls app. @@ -65,6 +66,33 @@ class Version0010Date20190801063812 extends SimpleMigrationStep { /** @var ISchemaWrapper $schema */ $schema = $schemaClosure(); + if ($schema->hasTable('polls_events')) { + $table = $schema->getTable('polls_events'); + if (!$table->hasColumn('deleted')) { + $table->addColumn('deleted', Type::BOOLEAN, [ + 'notnull' => false, + 'default' => 0 + ]); + } + if (!$table->hasColumn('delete_date')) { + $table->addColumn('delete_date', Type::DATETIME, [ + 'notnull' => true + ]); + } + if (!$table->hasColumn('vote_limit')) { + $table->addColumn('vote_limit', Type::INTEGER, [ + 'notnull' => false, + 'default' => 0 + ]); + } + if (!$table->hasColumn('show_results')) { + $table->addColumn('show_results', Type::STRING, [ + 'notnull' => true, + 'lenght' => 64 + ]); + } + + } if (!$schema->hasTable('polls_share')) { $table = $schema->createTable('polls_share'); $table->addColumn('id', Type::INTEGER, [ @@ -79,9 +107,8 @@ class Version0010Date20190801063812 extends SimpleMigrationStep { 'notnull' => true, 'length' => 128, ]); - $table->addColumn('poll_id', Type::STRING, [ - 'notnull' => true, - 'length' => 128, + $table->addColumn('poll_id', Type::INTEGER, [ + 'notnull' => true ]); $table->addColumn('user_id', Type::STRING, [ 'notnull' => false, @@ -109,11 +136,12 @@ class Version0010Date20190801063812 extends SimpleMigrationStep { if ($schema->hasTable('polls_share')) { $this->copyTokens(); + // $this->copyInvitationTokens(); } } /** - * Copy date options + * Copy public tokens */ protected function copyTokens() { $insert = $this->connection->getQueryBuilder(); @@ -123,23 +151,67 @@ class Version0010Date20190801063812 extends SimpleMigrationStep { 'type' => $insert->createParameter('type'), 'poll_id' => $insert->createParameter('poll_id'), 'user_id' => $insert->createParameter('user_id'), - 'user_email' => $insert->createParameter('user_email'), + 'user_email' => $insert->createParameter('user_email') ]); $query = $this->connection->getQueryBuilder(); $query->select('*') ->from('polls_events'); $result = $query->execute(); + while ($row = $result->fetch()) { if ($row['access'] == 'public') { + // copy the hash to a public share + $insert + ->setParameter('token', $row['hash']) + ->setParameter('type', 'public') + ->setParameter('poll_id', $row['id']) + ->setParameter('user_id', null) + ->setParameter('user_email', null); + $insert->execute(); + } elseif ($row['access'] == 'hidden') { + // copy the hash to a public share + // poll stays hidden for registered users $insert ->setParameter('token', $row['hash']) - ->setParameter('type', $row['access']) + ->setParameter('type', 'public') ->setParameter('poll_id', $row['id']) ->setParameter('user_id', null) ->setParameter('user_email', null); $insert->execute(); + } elseif ($row['access'] == 'registered') { + // copy the hash to a public share + // to keep the hash + $insert + ->setParameter('token', $row['hash']) + ->setParameter('type', 'public') + ->setParameter('poll_id', $row['id']) + ->setParameter('user_id', null) + ->setParameter('user_email', null); + } else { + // create a personal share for invitated users + + // explode the access entry to single access strings + $users = explode(';', $row['access']); + foreach ($users as $value) { + // separate 'user' and 'group' from user names and create + // a share for every entry + $parts = explode('_', $value); + $insert + ->setParameter('token', \OC::$server->getSecureRandom()->generate( + 16, + ISecureRandom::CHAR_DIGITS . + ISecureRandom::CHAR_LOWER . + ISecureRandom::CHAR_UPPER + )) + ->setParameter('type', $parts[0]) + ->setParameter('poll_id', $row['id']) + ->setParameter('user_id', $parts[1]) + ->setParameter('user_email', null); + $insert->execute(); + } } } $result->closeCursor(); } + } diff --git a/lib/Model/Acl.php b/lib/Model/Acl.php index 035ca50d..245b0d08 100644 --- a/lib/Model/Acl.php +++ b/lib/Model/Acl.php @@ -29,7 +29,9 @@ use Exception; use OCP\AppFramework\Db\DoesNotExistException; use OCP\IGroupManager; +use OCP\ILogger; use OCA\Polls\Db\Event; +use OCA\Polls\Db\Share; use OCA\Polls\Db\EventMapper; use OCA\Polls\Db\ShareMapper; @@ -42,6 +44,10 @@ class Acl implements JsonSerializable { /** @var int */ private $pollId = 0; + private $logger; + + /** @var array */ + private $shares = []; /** @var string */ private $token = ''; @@ -66,6 +72,7 @@ class Acl implements JsonSerializable { public function __construct( string $appName, $userId, + ILogger $logger, IGroupManager $groupManager, EventMapper $eventMapper, ShareMapper $shareMapper, @@ -76,11 +83,12 @@ class Acl implements JsonSerializable { $this->eventMapper = $eventMapper; $this->shareMapper = $shareMapper; $this->event = $event; + $this->logger = $logger; } /** - * @NoAdminRequired + * @NoAdminRequired * @return string */ public function getUserId() { @@ -88,7 +96,7 @@ class Acl implements JsonSerializable { } /** - * @NoAdminRequired + * @NoAdminRequired * @return string */ public function setUserId($userId): Acl { @@ -97,7 +105,7 @@ class Acl implements JsonSerializable { } /** - * @NoAdminRequired + * @NoAdminRequired * @return int */ public function getPollId(): int { @@ -105,18 +113,19 @@ class Acl implements JsonSerializable { } /** - * @NoAdminRequired + * @NoAdminRequired * @return int */ public function setPollId(int $pollId): Acl { $this->pollId = $pollId; $this->event = $this->eventMapper->find($this->pollId); + $this->shares = $this->shareMapper->findByPoll($this->pollId); return $this; } /** - * @NoAdminRequired + * @NoAdminRequired * @return bool */ public function getIsOwner(): bool { @@ -128,7 +137,7 @@ class Acl implements JsonSerializable { } /** - * @NoAdminRequired + * @NoAdminRequired * @return bool */ public function getIsAdmin(): bool { @@ -140,23 +149,58 @@ class Acl implements JsonSerializable { } /** - * @NoAdminRequired + * @NoAdminRequired * @return bool */ public function getAllowView(): bool { - if ($this->pollId) { - return true; - } else { - return false; - } + return ( + $this->getIsOwner() + || $this->getIsAdmin() + || ($this->getGroupShare() && !$this->event->getDeleted()) + || ($this->getPersonalShare() && !$this->event->getDeleted()) + || $this->event->getAccess() !== 'hidden' + ); + } + + /** + * @NoAdminRequired + * @return bool + */ + public function getGroupShare(): bool { + return count( + array_filter($this->shareMapper->findByPoll($this->getPollId()), function($item) { + if ($item->getType() === 'group' && $this->groupManager->isInGroup($this->getUserId(),$item->getUserId())) { + return true; + } + }) + ); } /** - * @NoAdminRequired + * @NoAdminRequired + * @return bool + */ + public function getPersonalShare(): bool { + + return count( + array_filter($this->shareMapper->findByPoll($this->getPollId()), function($item) { + if ($item->getType() === 'user' && $item->getUserId() === $this->getUserId()) { + return true; + } + }) + ); + } + + /** + * @NoAdminRequired * @return bool */ public function getAllowVote(): bool { - if ($this->pollId) { + // $this->logger->error('Expiration: ' . $this->event->getExpire()); + // $this->logger->error('Expiration time: ' . strtotime($this->event->getExpire())); + // $this->logger->error('low: ' . time()); + // $this->logger->error('compare: ' . (strtotime($this->event->getExpire()) > time())); + if ($this->getAllowView() && strtotime($this->event->getExpire()) > time()) { return true; } else { return false; @@ -164,7 +208,7 @@ class Acl implements JsonSerializable { } /** - * @NoAdminRequired + * @NoAdminRequired * @return bool */ public function getAllowComment(): bool { @@ -172,7 +216,7 @@ class Acl implements JsonSerializable { } /** - * @NoAdminRequired + * @NoAdminRequired * @return bool */ public function getAllowEdit(): bool { @@ -180,7 +224,7 @@ class Acl implements JsonSerializable { } /** - * @NoAdminRequired + * @NoAdminRequired * @return bool */ public function getAllowSeeUsernames(): bool { @@ -188,7 +232,7 @@ class Acl implements JsonSerializable { } /** - * @NoAdminRequired + * @NoAdminRequired * @return bool */ public function getAllowSeeAllVotes(): bool { @@ -201,7 +245,7 @@ class Acl implements JsonSerializable { } /** - * @NoAdminRequired + * @NoAdminRequired * @return bool */ public function getFoundByToken(): bool { @@ -209,7 +253,7 @@ class Acl implements JsonSerializable { } /** - * @NoAdminRequired + * @NoAdminRequired * @return string */ public function getToken(): string { @@ -217,6 +261,7 @@ class Acl implements JsonSerializable { } /** + * @NoAdminRequired * @return string */ public function setToken(string $token): Acl { @@ -248,8 +293,8 @@ class Acl implements JsonSerializable { } /** - * @NoAdminRequired - * @return string + * @NoAdminRequired + * @return string */ public function getAccessLevel(): string { if ($this->getIsOwner()) { @@ -283,6 +328,8 @@ class Acl implements JsonSerializable { 'allowEdit' => $this->getAllowEdit(), 'allowSeeUsernames' => $this->getAllowSeeUsernames(), 'allowSeeAllVotes' => $this->getAllowSeeAllVotes(), + 'groupShare' => $this->getGroupShare(), + 'personalShare' => $this->getPersonalShare(), 'foundByToken' => $this->getFoundByToken(), 'accessLevel' => $this->getAccessLevel() ]; diff --git a/package.json b/package.json index 41d92896..fc081bea 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,7 @@ "test:coverage": "jest --coverage" }, "dependencies": { - "@babel/runtime": "^7.7.4", + "@babel/runtime": "^7.7.6", "@nextcloud/vue": "^1.2.2", "acorn": "^7.1.0", "acorn-jsx": "^5.1.0", @@ -66,16 +66,16 @@ "node": ">=10.0.0" }, "devDependencies": { - "@babel/core": "^7.7.4", + "@babel/core": "^7.7.5", "@babel/plugin-syntax-dynamic-import": "^7.7.4", - "@babel/preset-env": "^7.7.4", + "@babel/preset-env": "^7.7.6", "babel-eslint": "^10.0.2", "babel-loader": "^8.0.6", - "css-loader": "^3.2.0", + "css-loader": "^3.2.1", "eslint": "^5.16.0", "eslint-config-standard": "^12.0.0", "eslint-friendly-formatter": "^4.0.1", - "eslint-loader": "^3.0.2", + "eslint-loader": "^3.0.3", "eslint-plugin-import": "^2.18.2", "eslint-plugin-node": "^10.0.0", "eslint-plugin-promise": "^4.2.1", diff --git a/src/js/assets/app.png b/src/js/assets/app.png Binary files differnew file mode 100644 index 00000000..84a879bb --- /dev/null +++ b/src/js/assets/app.png diff --git a/src/js/assets/app.svg b/src/js/assets/app.svg new file mode 100644 index 00000000..cbf61396 --- /dev/null +++ b/src/js/assets/app.svg @@ -0,0 +1,38 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + id="svg8" + viewBox="0 0 32 32" + x="0px" + y="0px" + enable-background="new 0 0 595.275 311.111" + width="32" + height="32" + xml:space="preserve" + version="1.1"><metadata + id="metadata14"><rdf:RDF><cc:Work + rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs + id="defs12" /><rect + id="rect2" + y="2" + x="3" + height="26" + width="7" + style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.93541431" /><rect + id="rect4" + y="12" + x="12" + height="16" + width="7" + style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.93541431" /><rect + id="rect6" + y="8" + x="21" + height="20" + width="7" + style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.8918826" /></svg>
\ No newline at end of file diff --git a/src/js/components/PollList/PollListItem.vue b/src/js/components/PollList/PollListItem.vue index 151a18b4..95c0468a 100644 --- a/src/js/components/PollList/PollListItem.vue +++ b/src/js/components/PollList/PollListItem.vue @@ -45,7 +45,7 @@ </div> <div v-else class="pollListItem poll"> - <div v-tooltip.auto="pollType" class="thumbnail" :class="[pType, {expired : poll.event.expired}]"> + <div v-tooltip.auto="pollType" class="thumbnail" :class="[pType, {expired : poll.expired}]"> {{ pollType }} </div> @@ -53,10 +53,10 @@ <router-link :to="{name: 'vote', params: {id: poll.id}}" class="title"> <div class="name"> - {{ poll.event.title }} + {{ poll.title }} </div> <div class="description"> - {{ poll.event.description }} + {{ poll.description }} </div> </router-link> @@ -78,14 +78,14 @@ </div> <div class="owner"> - <user-div :user-id="poll.event.owner" :display-name="poll.event.ownerDisplayName" /> + <user-div :user-id="poll.owner" :display-name="poll.ownerDisplayName" /> </div> <div class="dates"> <div class="created "> {{ timeSpanCreated }} </div> - <div class="expiry" :class="{ expired : poll.event.expired }"> + <div class="expiry" :class="{ expired : poll.expired }"> {{ timeSpanExpiration }} </div> </div> @@ -120,17 +120,27 @@ export default { // TODO: dity hack aType() { - if (this.poll.event.access === 'public') { - return this.poll.event.access - } else if (this.poll.event.access === 'registered') { - return this.poll.event.access - } else if (this.poll.event.access === 'hidden') { - return this.poll.event.access + if (this.poll.access === 'public') { + return this.poll.access + } else if (this.poll.access === 'registered') { + return this.poll.access + } else if (this.poll.access === 'hidden') { + return this.poll.access } else { return 'select' } }, + expired() { + if (this.poll.expire === null) { + return false + } else if (Date.parse(this.poll.expire) < Date.now()) { + return false + } else { + return true + } + }, + accessType() { if (this.aType === 'public') { return t('polls', 'Public access') @@ -147,7 +157,7 @@ export default { // TODO: dity hack pType() { - if (this.poll.event.type === '1') { + if (this.poll.type === '1') { // TRANSLATORS This means that this is the type of the poll. Another type is a 'date poll'. return t('polls', 'textPoll') } else { @@ -168,41 +178,19 @@ export default { }, timeSpanCreated() { - return moment(this.poll.event.created).fromNow() + return moment(this.poll.created).fromNow() }, timeSpanExpiration() { - if (this.poll.event.expiration) { - return moment(this.poll.event.expirationDate).fromNow() + if (this.poll.expire) { + return moment(this.poll.expire).fromNow() } else { return t('polls', 'never') } }, - participants() { - return this.poll.votes - .map(item => item.userId) - .filter((value, index, self) => self.indexOf(value) === index) - }, - countvotes() { - return this.participants.length - }, - countComments() { - if (this.poll.comments.length > 999) { - return '999+' - } - return this.poll.comments.length - }, - countCommentsHint() { - return n('polls', 'There is %n comment', 'There are %n comments', this.poll.comments.length) - }, - countShares() { - return this.poll.shares.length - }, - votedBycurrentUser() { - return this.participants.indexOf(OC.getCurrentUser().uid) > -1 - }, + voteUrl() { - return OC.generateUrl('apps/polls/poll/') + this.poll.event.id + return OC.generateUrl('apps/polls/poll/') + this.poll.id }, menuItems() { @@ -221,7 +209,7 @@ export default { } ] - if (this.poll.event.owner === OC.getCurrentUser().uid) { + if (this.poll.owner === OC.getCurrentUser().uid) { // items.push({ // key: 'editPoll', // icon: 'icon-rename', diff --git a/src/js/components/base/sideBarClose.vue b/src/js/components/VoteTable/VoteHeader.vue index 6ff1c14f..d8eb7fc7 100644 --- a/src/js/components/base/sideBarClose.vue +++ b/src/js/components/VoteTable/VoteHeader.vue @@ -21,27 +21,57 @@ --> <template> - <div class="close flex-row"> - <a id="closeDetails" :title="closeDetailLabel" :alt="closeDetailLabelAlt" - class="close icon-close has-tooltip-bottom" href="#" @:click="hideSidebar" /> + <div class="voteHeader"> + <h2> + {{ event.title }} + <span v-if="expired" class="label error">{{ t('polls', 'Expired since %n', 1, timeSpanExpiration) }}</span> + <span v-if="!expired && isExpirationSet" class="label success">{{ t('polls', 'Place your votes until %n', 1, event.expire) }}</span> + <span v-if="!isExpirationSet" class="label success">{{ t('polls', 'No expiration date set') }}</span> + </h2> + <h3> + {{ event.description }} + </h3> </div> </template> <script> +import { mapState, mapGetters } from 'vuex' + export default { - name: 'SideBarClose', + name: 'VoteHeader', data() { return { - closeDetailLabel: t('Close details'), - closeDetailLabelAlt: t('Close') + voteSaved: false, + delay: 50, + newName: '' } }, + computed: { + ...mapState({ + event: state => state.event + }), + + ...mapGetters([ + 'isExpirationSet', + 'expired', + 'timeSpanExpiration' + ]) + + }, + methods: { - hideSidebar() { - OC.Apps.hideAppSidebar() + indicateVoteSaved() { + this.voteSaved = true + window.setTimeout(this.timer, this.delay) } } } </script> + +<style lang="scss" scoped> + .voteHeader { + margin: 8px 24px; + } +</style> diff --git a/src/js/components/VoteTable/VoteHeaderPublic.vue b/src/js/components/VoteTable/VoteHeaderPublic.vue new file mode 100644 index 00000000..4c679729 --- /dev/null +++ b/src/js/components/VoteTable/VoteHeaderPublic.vue @@ -0,0 +1,221 @@ +<!-- + - @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 class="voteHeader"> + <div v-if="!isValidUser" class="getUsername"> + <!-- <label> + {{ t('polls', 'Enter a valid username, to participate in this poll.') }} + </label> --> + + <form v-if="!redirecting"> + <input v-model="userName" :class="{ error: (!isValidName && userName.length > 0), success: isValidName }" type="text" + :placeholder="t('polls', 'Enter a valid username with at least 3 Characters')"> + <input v-show="isValidName && !checkingUserName" class="icon-confirm" :class="{ error: !isValidName, success: isValidName }" + @click="writeUserName"> + <span v-show="checkingUserName" class="icon-loading-small" /> + <!-- <span v-if="!isValidName" class="error"> {{ invalidUserNameMessage }} </span> --> + </form> + <div v-else> + <span>{{ t('polls', 'You will be redirected to your personal share.') }}</span> + <span> + {{ t('polls', 'If you are not redirected to your poll click this link:') }} + <router-link :to="{ name: 'publicVote', params: { token: token }}"> + Link + </router-link> + </span> + </div> + </div> + <div v-if="displayLink" class="personal-link"> + {{ t('polls', 'Your personal link to this poll: %n', 1, personalLink) }} + <a class="icon icon-clippy" @click="copyLink( { url: OC.generateUrl($route.path) } )" /> + </div> + </div> +</template> + +<script> +import debounce from 'lodash/debounce' +import axios from 'nextcloud-axios' +import { mapState } from 'vuex' + +export default { + name: 'VoteHeaderPublic', + + data() { + return { + userName: '', + token: '', + checkingUserName: false, + redirecting: false, + isValidName: false, + newName: '' + } + }, + + computed: { + ...mapState({ + event: state => state.event, + acl: state => state.acl + }), + + personalLink() { + return location.protocol.concat('//', window.location.hostname, OC.generateUrl(this.$route.path)) + }, + + displayLink() { + return (this.acl.userId !== '' && this.acl.userId !== null && this.acl.foundByToken) + }, + + isValidUser() { + return (this.acl.userId !== '' && this.acl.userId !== null) + } + + }, + + watch: { + userName: function() { + if (this.userName.length > 2) { + this.isValidName = this.validatePublicUsername() + } else { + this.invalidUserNameMessage = t('polls', 'Please use at least 3 characters for your user name!') + this.isValidName = false + } + } + }, + + methods: { + copyLink(payload) { + this.$copyText(window.location.origin + payload.url).then( + function(e) { + OC.Notification.showTemporary(t('polls', 'Link copied to clipboard'), { type: 'success' }) + }, + function(e) { + OC.Notification.showTemporary(t('polls', 'Error while copying link to clipboard'), { type: 'error' }) + } + ) + }, + validatePublicUsername: debounce(function() { + if (this.userName.length > 2) { + this.checkingUserName = true + return axios.post(OC.generateUrl('apps/polls/check/username'), { pollId: this.event.id, userName: this.userName, token: this.$route.params.token }) + .then((response) => { + this.checkingUserName = false + this.isValidName = true + this.invalidUserNameMessage = 'User name is OK.' + return true + }) + .catch(() => { + this.checkingUserName = false + this.isValidName = false + this.invalidUserNameMessage = t('polls', 'This user name can not be choosed.') + return false + }) + } else { + this.checkingUserName = false + this.isValidName = false + this.invalidUserNameMessage = t('polls', 'Please use at least 3 characters for your user name!') + return false + } + }, 500), + + writeUserName() { + if (this.validatePublicUsername()) { + this.$store.dispatch('addShareFromUser', { token: this.$route.params.token, userName: this.userName }) + .then((response) => { + this.token = response.token + this.redirecting = true + this.$router.replace({ name: 'publicVote', params: { 'token': response.token } }) + }) + .catch(() => { + OC.Notification.showTemporary(t('polls', 'Error saving user name"', 1, event.title), { type: 'error' }) + }) + } + } + } +} +</script> + +<style lang="scss" scoped> + .voteHeader { + margin: 8px 24px; + } + + .personal-link { + display: flex; + padding: 4px 12px; + margin: 0 12px 0 24px; + border: 2px solid var(--color-success); + background-color: #d6fdda !important; + border-radius: var(--border-radius); + font-size: 1.2em; + opacity: 0.8; + .icon { + margin: 0 12px; + } + } + + .getUsername { + & > label { + margin-right: 12px; + } + + margin: 0 12px 12px 24px; + border:2px solid var(--color-border-dark); + font-size: 1.2em; + padding: 0 12px 0 12px; + display: flex; + align-items: center; + border-radius: var(--border-radius); + background-color: var(--color-background-dark); + flex-wrap: wrap; + + form, div { + flex: 1; + display: flex; + + } + input { + flex: 1; + } + + .icon-loading-small { + position: relative; + right: 24px; + top: 0px; + } + + input[type="text"] + .icon-confirm, input[type="text"] + .icon-loading-small { + flex: 0; + margin-left: -8px !important; + border-left-color: transparent !important; + border-radius: 0 var(--border-radius) var(--border-radius) 0 !important; + background-clip: padding-box; + opacity: 1; + height: 34px; + width: 34px; + padding: 7px 20px; + cursor: pointer; + margin-right: 0; + } + } + +</style> diff --git a/src/js/components/VoteTable/VoteTable.vue b/src/js/components/VoteTable/VoteTable.vue index a5b534e1..c5c8a48b 100644 --- a/src/js/components/VoteTable/VoteTable.vue +++ b/src/js/components/VoteTable/VoteTable.vue @@ -32,8 +32,7 @@ <VoteTableHeader v-for="(option) in sortedOptions" :key="option.id" :option="option" - :poll-type="event.type" - :mode="poll.mode" /> + :poll-type="event.type" /> </div> <div v-for="(participant) in participants" :key="participant" :class="{currentuser: (participant === currentUser) }"> @@ -64,8 +63,8 @@ export default { computed: { ...mapState({ - poll: state => state.poll, - event: state => state.event + event: state => state.event, + acl: state => state.acl }), ...mapGetters([ @@ -74,7 +73,7 @@ export default { ]), currentUser() { - return this.event.acl.userId + return this.acl.userId }, noOptions() { @@ -95,7 +94,7 @@ export default { setTo: nextAnswer }) .then(() => { - this.$emit('voteSaved') + // this.$emit('voteSaved') }) } } diff --git a/src/js/components/VoteTable/VoteTableHeader.vue b/src/js/components/VoteTable/VoteTableHeader.vue index 33db5e6c..28634cb8 100644 --- a/src/js/components/VoteTable/VoteTableHeader.vue +++ b/src/js/components/VoteTable/VoteTableHeader.vue @@ -24,7 +24,6 @@ <div class="vote-header" :class=" { winner: isWinner }"> <div v-if="textPoll" class="text-box"> {{ option.pollOptionText }} - <a v-if="mode === 'edit'" class="icon-delete" @click="$emit('remove')" /> </div> <div v-if="datePoll" v-tooltip.auto="localFullDate" class="date-box"> @@ -68,10 +67,6 @@ export default { pollType: { type: String, default: undefined - }, - mode: { - type: String, - default: 'vote' } }, diff --git a/src/js/components/VoteTable/VoteTableItem.vue b/src/js/components/VoteTable/VoteTableItem.vue index 0b83311f..1e3526c0 100644 --- a/src/js/components/VoteTable/VoteTableItem.vue +++ b/src/js/components/VoteTable/VoteTableItem.vue @@ -45,7 +45,8 @@ export default { computed: { ...mapState({ - event: state => state.event + event: state => state.event, + acl: state => state.acl }), answer() { @@ -64,7 +65,7 @@ export default { }, isActive() { - return (this.isValidUser && this.event.acl.userId === this.userId && !this.event.expired) + return (this.isValidUser && this.acl.userId === this.userId && !this.event.expired) } }, diff --git a/src/js/components/base/sideBar.vue b/src/js/components/base/sideBar.vue deleted file mode 100644 index 0b9eb190..00000000 --- a/src/js/components/base/sideBar.vue +++ /dev/null @@ -1,45 +0,0 @@ -<!-- - - @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 class="polls-sidebar"> - <slot /> - </div> -</template> - -<script> -export default { - name: 'SideBar' -} -</script> - -<style lang="scss"> -.polls-sidebar { - min-width: 300px; - border-left: 1px solid var(--color-border); - z-index: 500; - > ul, - > div { - padding: 8px; - } -} -</style> diff --git a/src/js/components/create/createDlg.vue b/src/js/components/create/createDlg.vue index c7136373..4330abb9 100644 --- a/src/js/components/create/createDlg.vue +++ b/src/js/components/create/createDlg.vue @@ -23,19 +23,19 @@ <template lang="html"> <div class="create-dialog"> <h2>{{ t('polls', 'Create new poll') }}</h2> - <input id="pollTitle" v-model="event.title" type="text" + <input id="pollTitle" v-model="title" type="text" :placeholder="t('polls', 'Enter Title')"> <div class="configBox"> <label class="title icon-checkmark"> {{ t('polls', 'Poll type') }} </label> - <input id="datePoll" v-model="event.type" value="datePoll" + <input id="datePoll" v-model="type" value="datePoll" :disabled="protect" type="radio" class="radio"> <label for="datePoll"> {{ t('polls', 'Event schedule') }} </label> - <input id="textPoll" v-model="event.type" value="textPoll" + <input id="textPoll" v-model="type" value="textPoll" :disabled="protect" type="radio" class="radio"> <label for="textPoll"> {{ t('polls', 'Text based') }} @@ -54,47 +54,48 @@ </template> <script> +import { mapState, mapMutations } from 'vuex' export default { name: 'CreateDlg', data() { return { - event: { - title: '', - description: '', - type: 'datePoll', - allowMaybe: false, - isAnonymous: false, - trueAnonymous: false, - expiration: false, - expirationDate: '', - access: 'hidden' - - } + id: 0, + type: 'datePoll', + title: '' } }, computed: { + ...mapState({ + event: state => state.event + }), + titleEmpty() { - return this.event.title === '' + return this.title === '' } }, methods: { + ...mapMutations([ 'setEventProperty', 'resetEvent', 'reset' ]), + cancel() { - this.event.title = '' - this.event.type = 'datePoll' + this.title = '' + this.type = 'datePoll' this.$emit('closeCreate') }, confirm() { - this.$store - .dispatch('addEventPromise', { event: this.event }) + this.resetEvent() + this.reset() + this.setEventProperty({ 'id': 0 }) + this.setEventProperty({ 'title': this.title }) + this.setEventProperty({ 'type': this.type }) + this.$store.dispatch('writeEventPromise') .then((response) => { - OC.Notification.showTemporary(t('polls', 'Poll "%n" added', 1, this.event.title), { type: 'success' }) - this.$store.dispatch('loadPolls') - this.$router.push({ name: 'edit', params: { id: response.data.id } }) this.cancel() + OC.Notification.showTemporary(t('polls', 'Poll "%n" added', 1, this.event.title), { type: 'success' }) + this.$router.push({ name: 'vote', params: { id: this.event.id } }) }) .catch(() => { OC.Notification.showTemporary(t('polls', 'Error while creating Poll "%n"', 1, this.event.title), { type: 'error' }) diff --git a/src/js/components/navigation/navigation.vue b/src/js/components/navigation/navigation.vue index 3733c5f0..5509369e 100644 --- a/src/js/components/navigation/navigation.vue +++ b/src/js/components/navigation/navigation.vue @@ -29,11 +29,11 @@ :title="t('polls', 'All polls')" :allow-collapse="true" icon="icon-folder" - :to="{ name: 'list'}" + :to="{ name: 'list', params: {type: 'all'}}" :open="true"> <ul> <AppNavigationItem - v-for="(poll) in pollList" + v-for="(poll) in allPolls" :key="poll.id" :title="poll.title" :icon="eventIcon(poll.type)" @@ -43,7 +43,8 @@ <AppNavigationItem :title="t('polls', 'My polls')" :allow-collapse="true" - icon="icon-folder" + icon="icon-user" + :to="{ name: 'list', params: {type: 'my'}}" :open="false"> <ul> <AppNavigationItem @@ -57,7 +58,8 @@ <AppNavigationItem :title="t('polls', 'Public polls')" :allow-collapse="true" - icon="icon-folder" + icon="icon-link" + :to="{ name: 'list', params: {type: 'public'}}" :open="false"> <ul> <AppNavigationItem @@ -71,7 +73,8 @@ <AppNavigationItem :title="t('polls', 'Hidden polls')" :allow-collapse="true" - icon="icon-folder" + icon="icon-password" + :to="{ name: 'list', params: {type: 'hidden'}}" :open="false"> <ul> <AppNavigationItem @@ -82,6 +85,21 @@ :to="{name: 'vote', params: {id: poll.id}}" /> </ul> </AppNavigationItem> + <AppNavigationItem + :title="t('polls', 'Deleted polls')" + :allow-collapse="true" + icon="icon-delete" + :to="{ name: 'list', params: {type: 'deleted'}}" + :open="false"> + <ul> + <AppNavigationItem + v-for="(poll) in deletedPolls" + :key="poll.id" + :title="poll.title" + :icon="eventIcon(poll.type)" + :to="{name: 'vote', params: {id: poll.id}}" /> + </ul> + </AppNavigationItem> </ul> <AppNavigationSettings> @@ -118,9 +136,11 @@ export default { computed: { ...mapGetters([ + 'allPolls', 'myPolls', 'publicPolls', - 'hiddenPolls' + 'hiddenPolls', + 'deletedPolls' ]), pollList() { diff --git a/src/js/components/navigation/store/polls.js b/src/js/components/navigation/store/polls.js index 6adf6556..f43ba652 100644 --- a/src/js/components/navigation/store/polls.js +++ b/src/js/components/navigation/store/polls.js @@ -37,6 +37,9 @@ const getters = { countPolls: (state) => { return state.list.length }, + allPolls: (state) => { + return state.list.filter(poll => (!poll.deleted)) + }, myPolls: (state) => { return state.list.filter(poll => (poll.owner === OC.getCurrentUser().uid)) }, @@ -45,6 +48,9 @@ const getters = { }, hiddenPolls: (state) => { return state.list.filter(poll => (poll.access === 'hidden')) + }, + deletedPolls: (state) => { + return state.list.filter(poll => (poll.deleted)) } } diff --git a/src/js/components/sidebar/SideBar.vue b/src/js/components/sidebar/SideBar.vue new file mode 100644 index 00000000..30931f01 --- /dev/null +++ b/src/js/components/sidebar/SideBar.vue @@ -0,0 +1,109 @@ +<!-- + - @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> + <AppSidebar :active="initialTab" :title="t('polls', 'Details')" @close="$emit('closeSideBar')"> + <UserDiv slot="primary-actions" :user-id="event.owner" :description="t('polls', 'Owner')" /> + + <AppSidebarTab :name="t('polls', 'Comments')" icon="icon-comment"> + <SideBarTabComments /> + </AppSidebarTab> + + <AppSidebarTab v-if="acl.allowEdit && event.type === 'datePoll'" :name="t('polls', 'Date options')" icon="icon-calendar"> + <SideBarTabDateOptions /> + </AppSidebarTab> + + <AppSidebarTab v-if="acl.allowEdit && event.type === 'textPoll'" :name="t('polls', 'Text options')" icon="icon-toggle-filelist"> + <SideBarTabTextOptions /> + </AppSidebarTab> + + <AppSidebarTab v-if="acl.allowEdit" :name="t('polls', 'Configuration')" icon="icon-settings"> + <SideBarTabConfiguration @deletePoll="$emit('deletePoll')" /> + </AppSidebarTab> + + <AppSidebarTab v-if="acl.allowEdit" :name="t('polls', 'Shares')" icon="icon-share"> + <SideBarTabShare /> + </AppSidebarTab> + </AppSidebar> +</template> + +<script> +import { AppSidebar, AppSidebarTab } from '@nextcloud/vue' + +import SideBarTabConfiguration from './SideBarTabConfiguration' +import SideBarTabDateOptions from './SideBarTabDateOptions' +import SideBarTabTextOptions from './SideBarTabTextOptions' +import SideBarTabComments from './SideBarTabComments' +import SideBarTabShare from './SideBarTabShare' +import { mapState } from 'vuex' + +export default { + name: 'SideBar', + components: { + SideBarTabConfiguration, + SideBarTabComments, + SideBarTabDateOptions, + SideBarTabTextOptions, + SideBarTabShare, + AppSidebar, + AppSidebarTab + }, + + data() { + return { + initialTab: 'comments' + } + }, + + computed: { + ...mapState({ + event: state => state.event, + acl: state => state.acl + }) + } + +} + +</script> + +<style scoped lang="scss"> + + ul { + & > li { + margin-bottom: 30px; + & > .comment-item { + display: flex; + align-items: center; + + & > .date { + right: 0; + top: 5px; + opacity: 0.5; + } + } + & > .message { + margin-left: 44px; + flex: 1 1; + } + } + } +</style> diff --git a/src/js/components/sidebar/SideBarTabConfiguration.vue b/src/js/components/sidebar/SideBarTabConfiguration.vue index 561b1f6f..1e135af1 100644 --- a/src/js/components/sidebar/SideBarTabConfiguration.vue +++ b/src/js/components/sidebar/SideBarTabConfiguration.vue @@ -31,19 +31,19 @@ </label> </div> - <div v-if="allowEdit" class="configBox"> + <div v-if="acl.allowEdit" class="configBox"> <label class="icon-sound title"> {{ t('polls', 'Title') }} </label> <input v-model="eventTitle" :class="{ error: titleEmpty }" type="text"> </div> - <div v-if="allowEdit" class="configBox"> + <div v-if="acl.allowEdit" class="configBox"> <label class="icon-edit title"> {{ t('polls', 'Description') }} </label> <textarea v-model="eventDescription" /> - <!-- <textarea v-if="allowEdit" :value="event.description" @input="updateDescription" /> --> + <!-- <textarea v-if="acl.allowEdit" :value="event.description" @input="updateDescription" /> --> </div> <div class="configBox"> @@ -53,7 +53,7 @@ <input id="allowMaybe" v-model="eventAllowMaybe" - :disabled="!allowEdit" + :disabled="!acl.allowEdit" type="checkbox" class="checkbox"> <label for="allowMaybe" class="title"> @@ -61,7 +61,7 @@ </label> <input id="anonymous" v-model="eventIsAnonymous" - :disabled="!allowEdit" + :disabled="!acl.allowEdit" type="checkbox" class="checkbox"> <label for="anonymous" class="title"> @@ -71,7 +71,7 @@ <input v-show="event.isAnonymous" id="trueAnonymous" v-model="eventFullAnonymous" - :disabled="!allowEdit" + :disabled="!acl.allowEdit" type="checkbox" class="checkbox"> <label v-show="event.isAnonymous" class="title" for="trueAnonymous"> @@ -80,17 +80,17 @@ <input id="expiration" v-model="eventExpiration" - :disabled="!allowEdit" + :disabled="!acl.allowEdit" type="checkbox" class="checkbox"> - <label class="title" for="expiration"> + <label class="title" for="expirtion"> {{ t('polls', 'Expires') }} </label> - <date-picker v-show="event.expiration" - v-model="eventExpirationDate" + <date-picker v-show="event.expire" + v-model="eventExpiration" v-bind="expirationDatePicker" - :disabled="!allowEdit" + :disabled="!acl.allowEdit" :time-picker-options="{ start: '00:00', step: '00:05', end: '23:55' }" style="width:170px" /> </div> @@ -99,47 +99,33 @@ <label class="title icon-category-auth"> {{ t('polls', 'Access') }} </label> - <input id="private" - v-model="eventAccess" - :disabled="!allowEdit" - type="radio" - value="registered" - class="radio"> - <label for="private" class="title"> - <div class="title icon-group" /> - <span>{{ t('polls', 'Registered users only') }}</span> - </label> + <input id="hidden" v-model="eventAccess" - :disabled="!allowEdit" + :disabled="!acl.allowEdit" type="radio" value="hidden" class="radio"> <label for="hidden" class="title"> <div class="title icon-category-security" /> - <span>{{ t('polls', 'hidden') }}</span> + <span>{{ t('polls', 'Hidden to other users') }}</span> </label> + <input id="public" v-model="eventAccess" - :disabled="!allowEdit" + :disabled="!acl.allowEdit" type="radio" value="public" class="radio"> <label for="public" class="title"> <div class="title icon-link" /> - <span>{{ t('polls', 'Public access') }}</span> - </label> - <input id="select" - v-model="eventAccess" - :disabled="!allowEdit" - type="radio" - value="select" - class="radio"> - <label for="select" class="title"> - <div class="title icon-shared" /> - <span>{{ t('polls', 'Only shared') }}</span> + <span>{{ t('polls', 'Visible to other users') }}</span> </label> </div> + + <button class="button btn primary" @click="$emit('deletePoll')"> + <span>{{ t('polls', 'Delete this poll') }}</span> + </button> </div> </template> @@ -160,14 +146,12 @@ export default { computed: { ...mapState({ - poll: state => state.poll, - event: state => state.event + event: state => state.event, + acl: state => state.acl }), ...mapGetters([ - 'languageCodeShort', - 'adminMode', - 'allowEdit' + 'languageCodeShort' ]), // Add bindings @@ -200,10 +184,10 @@ export default { eventExpiration: { get() { - return this.event.expiration + return this.event.expire }, set(value) { - this.writeValue({ 'expiration': value }) + this.writeValue({ 'expire': value }) } }, @@ -234,14 +218,14 @@ export default { } }, - eventExpirationDate: { - get() { - return this.$store.state.event.expirationDate - }, - set(value) { - this.writeValue({ 'expirationDate': value }) - } - }, + // eventExpiration: { + // get() { + // return this.$store.state.event.expiration + // }, + // set(value) { + // this.writeValue({ 'expiration': value }) + // } + // }, expirationDatePicker() { return { @@ -282,7 +266,7 @@ export default { saveButtonTitle: function() { if (this.writingPoll) { return t('polls', 'Writing poll') - } else if (this.allowEdit) { + } else if (this.acl.allowEdit) { return t('polls', 'Update poll') } else { return t('polls', 'Create new poll') @@ -291,10 +275,8 @@ export default { }, methods: { - ...mapMutations([ 'setEventProperty', 'pollSetProperty' ]), - ...mapActions([ - 'writeEventPromise' - ]), + ...mapMutations([ 'setEventProperty' ]), + ...mapActions([ 'writeEventPromise' ]), writeValueDebounced: debounce(function(e) { this.writeValue(e) @@ -317,7 +299,7 @@ export default { }, write() { - if (this.allowEdit) { + if (this.acl.allowEdit) { this.writePoll() } diff --git a/src/js/router.js b/src/js/router.js index 5c9690a4..1e6ccebc 100644 --- a/src/js/router.js +++ b/src/js/router.js @@ -38,11 +38,11 @@ export default new Router({ linkActiveClass: 'active', routes: [ { - path: '/:index(index.php/)?apps/polls/', + path: '/:index(index.php/)?apps/polls/:type?', components: { default: List }, - props: false, + props: true, name: 'list' }, { @@ -54,14 +54,6 @@ export default new Router({ name: 'vote' }, { - path: '/:index(index.php/)?apps/polls/edit/:id', - components: { - default: Vote - }, - props: true, - name: 'edit' - }, - { path: '/:index(index.php/)?apps/polls/vote/:id', components: { default: Vote diff --git a/src/js/store/index.js b/src/js/store/index.js index fcda4cb6..cdf31566 100644 --- a/src/js/store/index.js +++ b/src/js/store/index.js @@ -24,7 +24,7 @@ import Vue from 'vue' import Vuex from 'vuex' -import poll from './modules/currentPoll' +import acl from './modules/acl' import comments from './modules/comments' import event from './modules/event' import notification from './modules/notification' @@ -41,7 +41,7 @@ const debug = process.env.NODE_ENV !== 'production' export default new Vuex.Store({ modules: { - poll, + acl, comments, event, notification, diff --git a/src/js/store/modules/acl.js b/src/js/store/modules/acl.js new file mode 100644 index 00000000..0fcc47a0 --- /dev/null +++ b/src/js/store/modules/acl.js @@ -0,0 +1,82 @@ +/* + * @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' + +const defaultAcl = () => { + return { + userId: null, + pollId: null, + token: null, + isOwner: false, + isAdmin: false, + allowView: false, + allowVote: false, + allowComment: false, + allowEdit: false, + allowSeeUsernames: false, + allowSeeAllVotes: false, + foundByToken: false, + accessLevel: '' + } +} + +const state = defaultAcl() + +const mutations = { + + setAcl(state, payload) { + Object.assign(state, payload.acl) + }, + + reset(state) { + Object.assign(state, defaultAcl()) + } + +} + +const actions = { + + loadPoll({ commit, rootState }, payload) { + commit('reset') + let endPoint = 'apps/polls/get/acl/' + + if (payload.token !== undefined) { + endPoint = endPoint.concat('s/', payload.token) + } else if (payload.pollId !== undefined) { + endPoint = endPoint.concat(payload.pollId) + } else { + return + } + + return axios.get(OC.generateUrl(endPoint)) + .then((response) => { + commit('setAcl', { 'acl': response.data }) + }, (error) => { + console.error('Error loading comments', { 'error': error.response }, { 'payload': payload }) + throw error + }) + } +} + +export default { state, mutations, actions } diff --git a/src/js/store/modules/currentPoll.js b/src/js/store/modules/currentPoll.js deleted file mode 100644 index 2835a423..00000000 --- a/src/js/store/modules/currentPoll.js +++ /dev/null @@ -1,59 +0,0 @@ -/* - * @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/>. - * - */ - -const defaultPoll = () => { - return { - id: 0, - mode: 'create' - } -} - -const state = defaultPoll() - -const mutations = { - pollSet(state, payload) { - Object.assign(state, payload.poll) - }, - - pollReset(state) { - Object.assign(state, defaultPoll()) - }, - - pollSetProperty(state, payload) { - Object.assign(state, payload) - } -} - -const actions = { - loadPoll({ commit, rootState }, payload) { - commit('pollSetProperty', { 'mode': payload.mode }) - commit('pollSetProperty', { 'id': payload.pollId }) - - } -} - -export default { - state, - mutations, - actions -} diff --git a/src/js/store/modules/event.js b/src/js/store/modules/event.js index 71f43b16..6fefbdbd 100644 --- a/src/js/store/modules/event.js +++ b/src/js/store/modules/event.js @@ -30,16 +30,17 @@ const defaultEvent = () => { type: 'datePoll', title: '', description: '', + owner: undefined, created: '', access: 'public', - expiration: false, - expirationDate: '', - expired: false, + expire: null, isAnonymous: false, fullAnonymous: false, allowMaybe: false, - owner: undefined, - acl: {} + voteLimit: null, + showResults: true, + deleted: false, + deleteDate: null } } @@ -50,7 +51,7 @@ const mutations = { Object.assign(state, payload.event) }, - eventReset(state) { + resetEvent(state) { Object.assign(state, defaultEvent()) }, @@ -66,34 +67,38 @@ const getters = { return moment(state.created).fromNow() }, - timeSpanExpiration: state => { - if (state.expiration) { - return moment(state.expirationDate).fromNow() + isExpirationSet: state => { + return Boolean(moment(state.expire).unix()) + }, + + expired: (state, getters) => { + return (getters.isExpirationSet && moment(state.expire).diff() < 0) + }, + + timeSpanExpiration: (state, getters) => { + if (getters.expired) { + return moment(state.expire).fromNow() } else { return t('polls', 'never') } }, - accessType: state => { - if (state.acl.accessLevel === 'public') { + accessType: (state, getters, rootState) => { + if (rootState.acl.accessLevel === 'public') { return t('polls', 'Public access') - } else if (state.acl.accessLevel === 'select') { + } else if (rootState.acl.accessLevel === 'select') { return t('polls', 'Only shared') - } else if (state.acl.accessLevel === 'registered') { + } else if (rootState.acl.accessLevel === 'registered') { return t('polls', 'Registered users only') - } else if (state.acl.accessLevel === 'hidden') { + } else if (rootState.acl.accessLevel === 'hidden') { return t('polls', 'Hidden poll') } else { - return state.acl.accessLevel + return rootState.acl.accessLevel } }, - adminMode: state => { - return (!state.acl.isOwner && state.acl.isAdmin) - }, - - allowEdit: (state, getters) => { - return (state.acl.allowEdit) + allowEdit: (state, getters, rootState) => { + return (rootState.acl.allowEdit) } } @@ -101,7 +106,7 @@ const getters = { const actions = { loadEvent({ commit }, payload) { - commit('eventReset') + commit('resetEvent') let endPoint = 'apps/polls/get/event/' if (payload.token !== undefined) { @@ -115,7 +120,6 @@ const actions = { return axios.get(OC.generateUrl(endPoint)) .then((response) => { commit('setEvent', { 'event': response.data }) - // return response }, (error) => { if (error.response.status !== '404') { console.error('Error loading event', { 'error': error.response }, { 'payload': payload }) @@ -124,17 +128,6 @@ const actions = { }) }, - addEventPromise({ commit }, payload) { - return axios.post(OC.generateUrl('apps/polls/add/event'), { event: payload.event }) - .then((response) => { - return response - }, (error) => { - console.error('Error adding event', { 'error': error.response }, { 'payload': payload }) - throw error - }) - - }, - deleteEventPromise({ commit }, payload) { return axios.post(OC.generateUrl('apps/polls/delete/event'), { event: payload.id }) .then((response) => { @@ -147,9 +140,10 @@ const actions = { }, writeEventPromise({ commit, rootState }) { - return axios.post(OC.generateUrl('apps/polls/write/event'), { event: state, mode: rootState.poll.mode }) + return axios.post(OC.generateUrl('apps/polls/write/event'), { event: state }) .then((response) => { commit('setEvent', { 'event': response.data }) + return response.event }, (error) => { console.error('Error writing event:', { 'error': error.response }, { 'state': state }) throw error @@ -158,4 +152,4 @@ const actions = { } } -export default { state, mutations, getters, actions } +export default { state, mutations, getters, actions, defaultEvent } diff --git a/src/js/store/modules/shares.js b/src/js/store/modules/shares.js index d3e70c51..11d4767c 100644 --- a/src/js/store/modules/shares.js +++ b/src/js/store/modules/shares.js @@ -23,30 +23,30 @@ import axios from 'nextcloud-axios' -const defaultComments = () => { +const defaultShares = () => { return { list: [] } } -const state = defaultComments() +const state = defaultShares() const mutations = { - sharesSet(state, payload) { + setShares(state, payload) { Object.assign(state, payload) }, - shareRemove(state, payload) { + removeShare(state, payload) { state.list = state.list.filter(share => { return share.id !== payload.share.id }) }, reset(state) { - Object.assign(state, defaultComments()) + Object.assign(state, defaultShares()) }, - shareAdd(state, payload) { + addShare(state, payload) { state.list.push(payload) } @@ -79,6 +79,12 @@ const getters = { const actions = { loadPoll({ commit, rootState }, payload) { commit('reset') + // console.log(rootState.acl) + // if (!rootState.acl.allowEdit) { + // console.log('rootState.acl.allowEdit', rootState.acl.allowEdit) + // return + // } + // console.log('number 1') let endPoint = 'apps/polls/get/shares/' if (payload.token !== undefined) { @@ -91,7 +97,7 @@ const actions = { return axios.get(OC.generateUrl(endPoint)) .then((response) => { - commit('sharesSet', { + commit('setShares', { 'list': response.data }) }, (error) => { @@ -125,7 +131,7 @@ const actions = { payload.share.pollId = rootState.event.id return axios.post(OC.generateUrl('apps/polls/write/share'), { pollId: rootState.event.id, share: payload.share }) .then((response) => { - commit('shareAdd', response.data) + commit('addShare', response.data) }, (error) => { console.error('Error writing share', { 'error': error.response }, { 'payload': payload }) throw error @@ -135,7 +141,7 @@ const actions = { removeShareAsync({ commit, getters, dispatch, rootState }, payload) { return axios.post(OC.generateUrl('apps/polls/remove/share'), { share: payload.share }) .then((response) => { - commit('shareRemove', { 'share': payload.share }) + commit('removeShare', { 'share': payload.share }) }, (error) => { console.error('Error removing share', { 'error': error.response }, { 'payload': payload }) throw error diff --git a/src/js/store/modules/votes.js b/src/js/store/modules/votes.js index ac25162a..36ac8a1d 100644 --- a/src/js/store/modules/votes.js +++ b/src/js/store/modules/votes.js @@ -71,8 +71,8 @@ const getters = { } }) - if (!list.includes(rootState.event.acl.userId) && rootState.event.acl.userId !== null) { - list.push(rootState.event.acl.userId) + if (!list.includes(rootState.acl.userId) && rootState.acl.userId !== null) { + list.push(rootState.acl.userId) } return list @@ -143,13 +143,13 @@ const actions = { let endPoint = 'apps/polls/set/vote/' - if (rootState.event.acl.foundByToken) { + if (rootState.acl.foundByToken) { endPoint = endPoint.concat('s/') } return axios.post(OC.generateUrl(endPoint), { pollId: rootState.event.id, - token: rootState.event.acl.token, + token: rootState.acl.token, option: payload.option, userId: payload.userId, setTo: payload.setTo diff --git a/src/js/views/PollList.vue b/src/js/views/PollList.vue index da2420de..fddb5326 100644 --- a/src/js/views/PollList.vue +++ b/src/js/views/PollList.vue @@ -38,9 +38,9 @@ v-for="(poll, index) in pollList" :key="poll.id" :poll="poll" - @deletePoll="removePoll(index, poll.event)" - @editPoll="callPoll(index, poll.event, 'edit')" - @clonePoll="callPoll(index, poll.event, 'clone')" /> + @deletePoll="removePoll(index, poll)" + @editPoll="callPoll(index, poll, 'edit')" + @clonePoll="callPoll(index, poll, 'clone')" /> </transition-group> </div> <loading-overlay v-if="loading" /> @@ -50,7 +50,7 @@ <script> import PollListItem from '../components/PollList/PollListItem' - +import { mapGetters } from 'vuex' export default { name: 'PollList', @@ -61,13 +61,37 @@ export default { data() { return { noPolls: false, - loading: true + loading: true, + pollList: this.$store.state.polls.list } }, computed: { - pollList() { - return this.$store.state.polls.list + ...mapGetters([ + 'myPolls', + 'publicPolls', + 'hiddenPolls', + 'deletedPolls' + ]) + + // pollList() { + // return this.$store.state.polls.list + // } + }, + + watch: { + $route(to, from) { + if (this.$route.params.type === 'all') { + this.pollList = this.$store.state.polls.list + } else if (this.$route.params.type === 'my') { + this.pollList = this.myPolls + } else if (this.$route.params.type === 'public') { + this.pollList = this.publicPolls + } else if (this.$route.params.type === 'hidden') { + this.pollList = this.hiddenPolls + } else if (this.$route.params.type === 'deleted') { + this.pollList = this.deletedPolls + } } }, diff --git a/src/js/views/PublicVote.vue b/src/js/views/PublicVote.vue index 85c19e2a..ed5f1a1e 100644 --- a/src/js/views/PublicVote.vue +++ b/src/js/views/PublicVote.vue @@ -26,84 +26,33 @@ <a v-if="!sideBarOpen" href="#" class="icon icon-settings active" :title="t('polls', 'Open Sidebar')" @click="toggleSideBar()" /> - <div> - <h2> - {{ event.title }} - <span v-if="event.expired" class="label error">{{ t('polls', 'Expired') }}</span> - <span v-if="!event.expired && event.expiration" class="label success">{{ t('polls', 'Votes are possible until %n', 1, event.expirationDate) }}</span> - <span v-if="!event.expiration" class="label success">{{ t('polls', 'No expiration date set') }}</span> - <transition name="fade"> - <span v-if="voteSaved" class="label success">Vote saved</span> - </transition> - </h2> - <h3> - {{ event.description }} - </h3> - </div> - - <div v-if="!isValidUser" class="get-username"> - <!-- <label> - {{ t('polls', 'Enter a valid username, to participate in this poll.') }} - </label> --> - - <form v-if="!redirecting"> - <input v-model="userName" :class="{ error: (!isValidName && userName.length > 0), success: isValidName }" type="text" - :placeholder="t('polls', 'Enter a valid username with at least 3 Characters')"> - <input v-show="isValidName && !checkingUserName" class="icon-confirm" :class="{ error: !isValidName, success: isValidName }" - @click="writeUserName"> - <span v-show="checkingUserName" class="icon-loading-small" /> - <!-- <span v-if="!isValidName" class="error"> {{ invalidUserNameMessage }} </span> --> - </form> - <div v-else> - <span>{{ t('polls', 'You will be redirected to your personal share.') }}</span> - <span> - {{ t('polls', 'If you are not redirected to your poll click this link:') }} - <router-link :to="{ name: 'publicVote', params: { token: token }}"> - Link - </router-link> - </span> - </div> - </div> - <div v-if="displayLink" class="personal-link"> - {{ t('polls', 'Your personal link to this poll: %n', 1, personalLink) }} - <a class="icon icon-clippy" @click="copyLink( { url: OC.generateUrl($route.path) } )" /> - </div> - - <VoteTable v-show="!loading" @voteSaved="indicateVoteSaved()" /> + <VoteHeader /> + <VoteHeaderPublic /> + <VoteTable /> <Notification /> </div> - <AppSidebar v-if="sideBarOpen" :active="initialTab" :title="t('polls', 'Details')" - @close="toggleSideBar"> - <template slot="primary-actions"> - <UserDiv :user-id="event.owner" :description="t('polls', 'Owner')" /> - </template> - - <AppSidebarTab :name="t('polls', 'Comments')" icon="icon-comment"> - <SideBarTabComments /> - </AppSidebarTab> - </AppSidebar> + <SideBar v-if="sideBarOpen" @closeSideBar="toggleSideBar" /> <LoadingOverlay v-if="loading" /> </AppContent> </template> <script> -import Notification from '../components/notification/notification' +import VoteHeader from '../components/VoteTable/VoteHeader' +import VoteHeaderPublic from '../components/VoteTable/VoteHeaderPublic' import VoteTable from '../components/VoteTable/VoteTable' -import SideBarTabComments from '../components/SideBar/SideBarTabComments' -import debounce from 'lodash/debounce' -import axios from 'nextcloud-axios' -import { mapState, mapGetters } from 'vuex' -import { AppSidebar, AppSidebarTab } from '@nextcloud/vue' +import Notification from '../components/notification/notification' +import SideBar from '../components/SideBar/SideBar' +import { mapState } from 'vuex' export default { name: 'Vote', components: { Notification, - SideBarTabComments, + VoteHeader, + VoteHeaderPublic, VoteTable, - AppSidebar, - AppSidebarTab + SideBar }, data() { @@ -111,42 +60,18 @@ export default { voteSaved: false, delay: 50, sideBarOpen: false, - loading: false, - checkingUserName: false, - token: '', - userName: '', - isValidName: false, - invalidUserNameMessage: '', - redirecting: false, initialTab: 'comments' } }, computed: { ...mapState({ - poll: state => state.poll, event: state => state.event, - shares: state => state.poll.shares + acl: state => state.acl }), - ...mapGetters([ - 'allowEdit' - ]), - - personalLink() { - return location.protocol.concat('//', window.location.hostname, OC.generateUrl(this.$route.path)) - }, - - displayLink() { - return (this.event.acl.userId !== '' && this.event.acl.userId !== null && this.event.acl.foundByToken) - }, - windowTitle: function() { return t('polls', 'Polls') + ' - ' + this.event.title - }, - - isValidUser() { - return (this.event.acl.userId !== '' && this.event.acl.userId !== null) } }, @@ -154,14 +79,6 @@ export default { watch: { '$route'(to, from) { this.loadPoll() - }, - userName: function() { - if (this.userName.length > 2) { - this.isValidName = this.validatePublicUsername() - } else { - this.invalidUserNameMessage = t('polls', 'Please use at least 3 characters for your user name!') - this.isValidName = false - } } }, @@ -172,8 +89,6 @@ export default { methods: { loadPoll() { this.loading = false - // this.$store.dispatch('getShareAsync', { token: this.$route.params.token }) - // .then((response) => { this.$store.dispatch('loadEvent', { token: this.$route.params.token }) .then((response) => { this.$store.dispatch('loadPoll', { token: this.$route.params.token }) @@ -187,91 +102,10 @@ export default { }) }, - copyLink(payload) { - this.$copyText(window.location.origin + payload.url).then( - function(e) { - OC.Notification.showTemporary(t('polls', 'Link copied to clipboard'), { type: 'success' }) - }, - function(e) { - OC.Notification.showTemporary(t('polls', 'Error while copying link to clipboard'), { type: 'error' }) - } - ) - }, - - validatePublicUsername: debounce(function() { - if (this.userName.length > 2) { - this.checkingUserName = true - return axios.post(OC.generateUrl('apps/polls/check/username'), { pollId: this.event.id, userName: this.userName, token: this.$route.params.token }) - .then((response) => { - this.checkingUserName = false - this.isValidName = true - this.invalidUserNameMessage = 'User name is OK.' - return true - }) - .catch(() => { - this.checkingUserName = false - this.isValidName = false - this.invalidUserNameMessage = t('polls', 'This user name can not be choosed.') - return false - }) - } else { - this.checkingUserName = false - this.isValidName = false - this.invalidUserNameMessage = t('polls', 'Please use at least 3 characters for your user name!') - return false - } - }, 500), - - writeUserName() { - if (this.validatePublicUsername()) { - this.$store.dispatch('addShareFromUser', { token: this.$route.params.token, userName: this.userName }) - .then((response) => { - this.token = response.token - this.redirecting = true - this.$router.replace({ name: 'publicVote', params: { 'token': response.token } }) - }) - .catch(() => { - OC.Notification.showTemporary(t('polls', 'Error saving user name"', 1, event.title), { type: 'error' }) - }) - } - }, - toggleSideBar() { this.sideBarOpen = !this.sideBarOpen - }, - - openConfigurationTab() { - this.initialTab = 'configuration' - this.sideBarOpen = true - this.$store.commit('pollSetProperty', { 'mode': 'edit' }) - }, - - openOptionsTab() { - if (this.event.type === 'datePoll') { - this.initialTab = 'date-options' - } else if (this.event.type === 'textPoll') { - this.initialTab = 'text-options' - } - this.sideBarOpen = true - this.$store.commit('pollSetProperty', { 'mode': 'edit' }) - }, - - toggleEdit() { - if (this.poll.mode === 'vote') { - this.$store.commit('pollSetProperty', { 'mode': 'edit' }) - } else if (this.poll.mode === 'edit') { - this.$store.commit('pollSetProperty', { 'mode': 'vote' }) - } - }, - - timer() { - this.voteSaved = false - }, - - indicateVoteSaved() { - this.voteSaved = true - window.setTimeout(this.timer, this.delay) } + } } </script> @@ -289,64 +123,6 @@ export default { } } - .personal-link { - display: flex; - padding: 4px 12px; - margin: 0 12px 0 24px; - border: 2px solid var(--color-success); - background-color: #d6fdda !important; - border-radius: var(--border-radius); - font-size: 1.2em; - opacity: 0.8; - .icon { - margin: 0 12px; - } - } - .get-username { - & > label { - margin-right: 12px; - } - - margin: 0 12px 12px 24px; - border:2px solid var(--color-border-dark); - font-size: 1.2em; - padding: 0 12px 0 12px; - display: flex; - align-items: center; - border-radius: var(--border-radius); - background-color: var(--color-background-dark); - flex-wrap: wrap; - - form, div { - flex: 1; - display: flex; - - } - input { - flex: 1; - } - - .icon-loading-small { - position: relative; - right: 24px; - top: 0px; - } - - input[type="text"] + .icon-confirm, input[type="text"] + .icon-loading-small { - flex: 0; - margin-left: -8px !important; - border-left-color: transparent !important; - border-radius: 0 var(--border-radius) var(--border-radius) 0 !important; - background-clip: padding-box; - opacity: 1; - height: 34px; - width: 34px; - padding: 7px 20px; - cursor: pointer; - margin-right: 0; - } - } - .icon.icon-settings.active { display: block; width: 44px; diff --git a/src/js/views/Vote.vue b/src/js/views/Vote.vue index aeb03008..d3cfd666 100644 --- a/src/js/views/Vote.vue +++ b/src/js/views/Vote.vue @@ -25,79 +25,30 @@ <div v-if="event.id > 0" class="main-container"> <a v-if="!sideBarOpen" href="#" class="icon icon-settings active" :title="t('polls', 'Open Sidebar')" @click="toggleSideBar()" /> - - <div> - <h2> - {{ event.title }} - <span v-if="event.expired" class="label error">{{ t('polls', 'Expired') }}</span> - <span v-if="!event.expired && event.expiration" class="label success">{{ t('polls', 'Votes are possible until %n', 1, event.expirationDate) }}</span> - <span v-if="!event.expiration" class="label success">{{ t('polls', 'No expiration date set') }}</span> - <transition name="fade"> - <span v-if="voteSaved" class="label success">Vote saved</span> - </transition> - </h2> - <h3> - {{ event.description }} - </h3> - </div> - - <VoteTable v-show="!loading" @voteSaved="indicateVoteSaved()" /> + <VoteHeader /> + <VoteTable /> <Notification /> </div> - <AppSidebar v-if="sideBarOpen" :active="initialTab" :title="t('polls', 'Details')" - @close="toggleSideBar"> - <template slot="primary-actions"> - <UserDiv :user-id="event.owner" :description="t('polls', 'Owner')" /> - </template> - - <AppSidebarTab :name="t('polls', 'Comments')" icon="icon-comment"> - <SideBarTabComments /> - </AppSidebarTab> - - <AppSidebarTab v-if="allowEdit && event.type === 'datePoll'" :name="t('polls', 'Date options')" icon="icon-calendar"> - <SideBarTabDateOptions /> - </AppSidebarTab> - - <AppSidebarTab v-if="allowEdit && event.type === 'textPoll'" :name="t('polls', 'Text options')" icon="icon-toggle-filelist"> - <SideBarTabTextOptions /> - </AppSidebarTab> - - <AppSidebarTab v-if="allowEdit" :name="t('polls', 'Configuration')" icon="icon-settings"> - <SideBarTabConfiguration /> - </AppSidebarTab> - - <AppSidebarTab v-if="allowEdit" :name="t('polls', 'Shares')" icon="icon-share"> - <SideBarTabShare /> - </AppSidebarTab> - </AppSidebar> + <SideBar v-if="sideBarOpen" @closeSideBar="toggleSideBar" /> <LoadingOverlay v-if="loading" /> </AppContent> </template> <script> import Notification from '../components/notification/notification' +import VoteHeader from '../components/VoteTable/VoteHeader' import VoteTable from '../components/VoteTable/VoteTable' -import SideBarTabConfiguration from '../components/SideBar/SideBarTabConfiguration' -import SideBarTabDateOptions from '../components/SideBar/SideBarTabDateOptions' -import SideBarTabTextOptions from '../components/SideBar/SideBarTabTextOptions' -import SideBarTabComments from '../components/SideBar/SideBarTabComments' -import SideBarTabShare from '../components/SideBar/SideBarTabShare' +import SideBar from '../components/SideBar/SideBar' import { mapState, mapGetters } from 'vuex' -import { AppSidebar, AppSidebarTab } from '@nextcloud/vue' export default { name: 'Vote', components: { Notification, - SideBarTabConfiguration, - SideBarTabComments, - SideBarTabDateOptions, - SideBarTabTextOptions, - SideBarTabShare, + VoteHeader, VoteTable, - AppSidebar, - AppSidebarTab + SideBar }, data() { @@ -113,17 +64,23 @@ export default { computed: { ...mapState({ - poll: state => state.poll, event: state => state.event, - shares: state => state.poll.shares + shares: state => state.shares, + acl: state => state.acl }), ...mapGetters([ - 'allowEdit' + 'isExpirationSet', + 'expired', + 'timeSpanExpiration' ]), windowTitle: function() { return t('polls', 'Polls') + ' - ' + this.event.title + }, + + votePossible() { + return this.acl.allowVote && !this.expired } }, @@ -178,23 +135,6 @@ export default { } this.sideBarOpen = true this.$store.commit('pollSetProperty', { 'mode': 'edit' }) - }, - - toggleEdit() { - if (this.poll.mode === 'vote') { - this.$store.commit('pollSetProperty', { 'mode': 'edit' }) - } else if (this.poll.mode === 'edit') { - this.$store.commit('pollSetProperty', { 'mode': 'vote' }) - } - }, - - timer() { - this.voteSaved = false - }, - - indicateVoteSaved() { - this.voteSaved = true - window.setTimeout(this.timer, this.delay) } } } |