From 9a77bd7c7cc659989f779beea9ec9e487815d2ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julius=20H=C3=A4rtl?= Date: Thu, 8 Jun 2017 23:03:01 +0200 Subject: [PATCH] Implement due dates for cards MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Julius Härtl --- appinfo/database.xml | 7 +- appinfo/info.xml | 2 +- css/style.css | 40 ++++- img/calendar-white.svg | 54 +++++++ img/calendar.svg | 1 + js/bower.json | 3 +- js/controller/CardController.js | 142 +++++++++++------- js/directive/datepicker.js | 50 ++++++ js/directive/timepicker.js | 41 +++++ .../{relativeDateFilter.js => dateFilters.js} | 30 ++++ lib/Controller/CardController.php | 4 +- lib/Db/BoardMapper.php | 3 +- lib/Db/Card.php | 36 +++++ lib/Service/CardService.php | 6 +- templates/main.php | 7 +- templates/part.board.mainView.php | 3 + templates/part.card.php | 7 + 17 files changed, 361 insertions(+), 75 deletions(-) create mode 100644 img/calendar-white.svg create mode 100644 img/calendar.svg create mode 100644 js/directive/datepicker.js create mode 100644 js/directive/timepicker.js rename js/filters/{relativeDateFilter.js => dateFilters.js} (63%) diff --git a/appinfo/database.xml b/appinfo/database.xml index a49526952..92ca3fe4a 100644 --- a/appinfo/database.xml +++ b/appinfo/database.xml @@ -130,7 +130,6 @@ last_modified integer - 8 false true @@ -138,7 +137,6 @@ created_at integer - 8 false true @@ -159,6 +157,11 @@ boolean false + + duedate + timestamp + 0 + deck_cards_stack_id_index diff --git a/appinfo/info.xml b/appinfo/info.xml index 6db4b30dd..aeb615390 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -16,7 +16,7 @@ 💥 This is still alpha software: it may not be stable enough for production! - 0.1.4.2 + 0.1.4.3-1 agpl Julius Härtl Deck diff --git a/css/style.css b/css/style.css index 4463228b9..190317bfe 100644 --- a/css/style.css +++ b/css/style.css @@ -459,13 +459,45 @@ button.button-inline:hover { } .due { - background-color: #eee; - color: #aaa; padding: 1px 3px; border-radius: 4px; margin-right: 2px; + font-size: 90%; + margin-left: 5px; + opacity: .7; +} +.due .icon { + background-size: contain; + float:left; + opacity: 0.7; + margin-top:2px; +} +.overdue { + background-color: #e12419; + color: #fff; +} +.due.now { + background-color: #fbd850; + color: #000; +} +.due.next { + background-color: #22ac2a; + color: #fff; } +.due .icon-calendar { + background-image: url(../img/calendar.svg); + margin-right: 2px; +} +.overdue .icon-calendar { + background-image: url(../img/calendar-white.svg); +} +.now .icon-calendar { + background-image: url(../img/calendar.svg); +} +.next .icon-calendar { + background-image: url(../img/calendar-white.svg); +} /** * Card view right sidebar */ @@ -503,9 +535,9 @@ button.button-inline:hover { margin-bottom: 10px; } -#card-dates span { +#card-meta #duedate { + display: flex; } - #card-description { height: 100%; display: flex; diff --git a/img/calendar-white.svg b/img/calendar-white.svg new file mode 100644 index 000000000..1afec3223 --- /dev/null +++ b/img/calendar-white.svg @@ -0,0 +1,54 @@ + + + + + + image/svg+xml + + + + + + + + diff --git a/img/calendar.svg b/img/calendar.svg new file mode 100644 index 000000000..9290ef60f --- /dev/null +++ b/img/calendar.svg @@ -0,0 +1 @@ + diff --git a/js/bower.json b/js/bower.json index d4a723df9..d728e90e8 100644 --- a/js/bower.json +++ b/js/bower.json @@ -14,7 +14,8 @@ "angular-ui-select": "~0.19.6", "angular-markdown-it": "~0.6.1", "angular-ui-router": "~1.0.0", - "markdown-it-link-target": "~1.0.1" + "markdown-it-link-target": "~1.0.1", + "jquery-timepicker": "883bb2cd94" }, "license": "AGPL-3.0", "private": true, diff --git a/js/controller/CardController.js b/js/controller/CardController.js index 424659160..6146e4d6c 100644 --- a/js/controller/CardController.js +++ b/js/controller/CardController.js @@ -1,5 +1,3 @@ - - /* * @copyright Copyright (c) 2016 Julius Härtl * @@ -23,65 +21,97 @@ */ app.controller('CardController', function ($scope, $rootScope, $routeParams, $location, $stateParams, BoardService, CardService, StackService, StatusService) { - $scope.sidebar = $rootScope.sidebar; - $scope.status = {}; + $scope.sidebar = $rootScope.sidebar; + $scope.status = {}; - $scope.cardservice = CardService; - $scope.cardId = $stateParams.cardId; + $scope.cardservice = CardService; + $scope.cardId = $stateParams.cardId; - $scope.statusservice = StatusService.getInstance(); - $scope.boardservice = BoardService; + $scope.statusservice = StatusService.getInstance(); + $scope.boardservice = BoardService; - $scope.statusservice.retainWaiting(); + $scope.statusservice.retainWaiting(); - CardService.fetchOne($scope.cardId).then(function(data) { - $scope.statusservice.releaseWaiting(); - $scope.archived = CardService.getCurrent().archived; - }, function(error) { - }); + CardService.fetchOne($scope.cardId).then(function (data) { + $scope.statusservice.releaseWaiting(); + $scope.archived = CardService.getCurrent().archived; + }, function (error) { + }); - $scope.cardRenameShow = function() { - if($scope.archived || !BoardService.canEdit()) - return false; - else { - $scope.status.cardRename=true; - } - }; - $scope.cardEditDescriptionShow = function($event) { - if(BoardService.isArchived() || CardService.getCurrent().archived) { - return false; - } - var node = $event.target.nodeName; - if($scope.card.archived || !$scope.boardservice.canEdit()) { - console.log(node); - } else { - console.log("edit"); - $scope.status.cardEditDescription=true; - } - console.log($scope.status.canEditDescription); - }; - // handle rename to update information on the board as well - $scope.cardRename = function(card) { - CardService.rename(card).then(function(data) { - StackService.updateCard(card); - $scope.status.renameCard = false; - }); - }; - $scope.cardUpdate = function(card) { - CardService.update(CardService.getCurrent()).then(function(data) { - $scope.status.cardEditDescription = false; - $('#card-description').find('.save-indicator').fadeIn(500).fadeOut(1000); - }); - }; + $scope.cardRenameShow = function () { + if ($scope.archived || !BoardService.canEdit()) + return false; + else { + $scope.status.cardRename = true; + } + }; + $scope.cardEditDescriptionShow = function ($event) { + if (BoardService.isArchived() || CardService.getCurrent().archived) { + return false; + } + var node = $event.target.nodeName; + if ($scope.card.archived || !$scope.boardservice.canEdit()) { + console.log(node); + } else { + console.log("edit"); + $scope.status.cardEditDescription = true; + } + console.log($scope.status.canEditDescription); + }; + // handle rename to update information on the board as well + $scope.cardRename = function (card) { + CardService.rename(card).then(function (data) { + StackService.updateCard(card); + $scope.status.renameCard = false; + }); + }; + $scope.cardUpdate = function (card) { + CardService.update(CardService.getCurrent()).then(function (data) { + $scope.status.cardEditDescription = false; + $('#card-description').find('.save-indicator').fadeIn(500).fadeOut(1000); + }); + }; - $scope.labelAssign = function(element, model) { - CardService.assignLabel($scope.cardId, element.id); - var card = CardService.getCurrent(); - StackService.updateCard(card); - }; - - $scope.labelRemove = function(element, model) { - CardService.removeLabel($scope.cardId, element.id) - } + $scope.labelAssign = function (element, model) { + CardService.assignLabel($scope.cardId, element.id); + var card = CardService.getCurrent(); + StackService.updateCard(card); + }; + $scope.labelRemove = function (element, model) { + CardService.removeLabel($scope.cardId, element.id) + }; + + $scope.setDuedate = function (duedate) { + var element = CardService.getCurrent(); + var newDate = moment(element.duedate); + if(!newDate.isValid()) { + newDate = moment(); + } + newDate.date(duedate.date()); + newDate.month(duedate.month()); + newDate.year(duedate.year()); + element.duedate = newDate.format('YYYY-MM-DD HH:mm:ss'); + CardService.update(element); + StackService.updateCard(element); + }; + $scope.setDuedateTime = function (time) { + var element = CardService.getCurrent(); + var newDate = moment(element.duedate); + if(!newDate.isValid()) { + newDate = moment(); + } + newDate.hour(time.hour()); + newDate.minute(time.minute()); + element.duedate = newDate.format('YYYY-MM-DD HH:mm:ss'); + CardService.update(element); + StackService.updateCard(element); + }; + + $scope.resetDuedate = function () { + var element = CardService.getCurrent(); + element.duedate = null; + CardService.update(element); + StackService.updateCard(element); + }; }); diff --git a/js/directive/datepicker.js b/js/directive/datepicker.js new file mode 100644 index 000000000..5fde0da7c --- /dev/null +++ b/js/directive/datepicker.js @@ -0,0 +1,50 @@ +/* + * @copyright Copyright (c) 2017 Julius Härtl + * + * @author Julius Härtl + * + * @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 . + * + */ + +app.directive('datepicker', function () { + 'use strict'; + return { + link: function (scope, elm, attr) { + return elm.datepicker({ + dateFormat: "yy-mm-dd", + onSelect: function(date, inst) { + scope.setDuedate(moment(date)); + scope.$apply(); + }, + beforeShow: function(input, inst) { + var dp, marginLeft; + dp = $(inst).datepicker('widget'); + marginLeft = -Math.abs($(input).outerWidth() - dp.outerWidth()) / 2 + 'px'; + dp.css({ + 'margin-left': marginLeft + }); + $("div.ui-datepicker:before").css({ + 'left': 100 + 'px' + }); + return $('.hasDatepicker').datepicker(); + }, + minDate: null + }); + } + + } +}); \ No newline at end of file diff --git a/js/directive/timepicker.js b/js/directive/timepicker.js new file mode 100644 index 000000000..cdbe9662a --- /dev/null +++ b/js/directive/timepicker.js @@ -0,0 +1,41 @@ +/* + * @copyright Copyright (c) 2017 Julius Härtl + * + * @author Julius Härtl + * + * @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 . + * + */ + +app.directive('timepicker', function() { + 'use strict'; + return { + restrict: 'A', + link: function(scope, elm, attr) { + return elm.timepicker({ + onSelect: function(date, inst) { + scope.setDuedateTime(moment("2000-01-01 " + date)); + scope.$apply(); + }, + myPosition: 'center top', + atPosition: 'center bottom', + hourText: t('deck', 'Hours'), + minuteText: t('deck', 'Minutes'), + showPeriodLabels: false + }); + } + }; +}); diff --git a/js/filters/relativeDateFilter.js b/js/filters/dateFilters.js similarity index 63% rename from js/filters/relativeDateFilter.js rename to js/filters/dateFilters.js index 95e780558..0c8d3e3ce 100644 --- a/js/filters/relativeDateFilter.js +++ b/js/filters/dateFilters.js @@ -25,3 +25,33 @@ app.filter('relativeDateFilter', function() { return OC.Util.relativeModifiedDate(timestamp*1000); } }); + +app.filter('relativeDateFilterString', function() { + return function (date) { + return OC.Util.relativeModifiedDate(Date.parse(date)); + } +}); + +app.filter('dateToTimestamp', function() { + return function (date) { + return Date.parse(date); + } +}); + +app.filter('parseDate', function() { + return function (date) { + if(moment(date).isValid()) { + return moment(date).format('YYYY-MM-DD'); + } + return ""; + } +}); + +app.filter('parseTime', function() { + return function (date) { + if(moment(date).isValid()) { + return moment(date).format('HH:mm'); + } + return ""; + } +}); \ No newline at end of file diff --git a/lib/Controller/CardController.php b/lib/Controller/CardController.php index b18ceec73..4e9b52739 100644 --- a/lib/Controller/CardController.php +++ b/lib/Controller/CardController.php @@ -90,8 +90,8 @@ class CardController extends Controller { * @param $description * @return \OCP\AppFramework\Db\Entity */ - public function update($id, $title, $stackId, $type, $order, $description) { - return $this->cardService->update($id, $title, $stackId, $type, $order, $description, $this->userId); + public function update($id, $title, $stackId, $type, $order, $description, $duedate) { + return $this->cardService->update($id, $title, $stackId, $type, $order, $description, $this->userId, $duedate); } /** diff --git a/lib/Db/BoardMapper.php b/lib/Db/BoardMapper.php index 0c16d9d6c..a1f2b2a92 100644 --- a/lib/Db/BoardMapper.php +++ b/lib/Db/BoardMapper.php @@ -140,7 +140,6 @@ class BoardMapper extends DeckMapper implements IPermissionMapper { $timeLimit = time()-(60*5); $sql = 'SELECT id, title, owner, color, archived, deleted_at FROM `*PREFIX*deck_boards` ' . 'WHERE `deleted_at` > 0 AND `deleted_at` < ?'; - \OC::$server->getLogger()->error($sql); return $this->findEntities($sql, [$timeLimit]); } @@ -216,4 +215,4 @@ class BoardMapper extends DeckMapper implements IPermissionMapper { } -} \ No newline at end of file +} diff --git a/lib/Db/Card.php b/lib/Db/Card.php index 53f73771d..e1f581acf 100644 --- a/lib/Db/Card.php +++ b/lib/Db/Card.php @@ -24,6 +24,7 @@ // db/author.php namespace OCA\Deck\Db; +use DateTime; use JsonSerializable; class Card extends RelationalEntity implements JsonSerializable { @@ -39,6 +40,12 @@ class Card extends RelationalEntity implements JsonSerializable { protected $owner; protected $order; protected $archived = false; + protected $duedate = null; + + const DUEDATE_FUTURE = 0; + const DUEDATE_NEXT = 1; + const DUEDATE_NOW = 2; + const DUEDATE_OVERDUE = 3; public function __construct() { $this->addType('id', 'integer'); @@ -51,4 +58,33 @@ class Card extends RelationalEntity implements JsonSerializable { $this->addResolvable('owner'); } + public function jsonSerialize() { + $json = parent::jsonSerialize(); + $json['overdue'] = self::DUEDATE_FUTURE; + $due = strtotime($this->duedate); + + $today = new DateTime(); + $today->setTime( 0, 0, 0 ); + + $match_date = new DateTime($this->duedate); + + $match_date->setTime( 0, 0, 0 ); + + $diff = $today->diff( $match_date ); + $diffDays = (integer)$diff->format( "%R%a" ); // Extract days count in interval + + if($due !== false) { + if ($diffDays === 1) { + $json['overdue'] = self::DUEDATE_NEXT; + } + if ($diffDays === 0) { + $json['overdue'] = self::DUEDATE_NOW; + } + if ($diffDays < 0) { + $json['overdue'] = self::DUEDATE_OVERDUE; + } + } + return $json; + } + } diff --git a/lib/Service/CardService.php b/lib/Service/CardService.php index d841d02bc..5cfaa4097 100644 --- a/lib/Service/CardService.php +++ b/lib/Service/CardService.php @@ -50,9 +50,6 @@ class CardService { return $card; } - /** - * @param integer $order - */ public function create($title, $stackId, $type, $order, $owner) { $this->permissionService->checkPermission($this->stackMapper, $stackId, Acl::PERMISSION_EDIT); if($this->boardService->isArchived($this->stackMapper, $stackId)) { @@ -76,7 +73,7 @@ class CardService { return $this->cardMapper->delete($this->cardMapper->find($id)); } - public function update($id, $title, $stackId, $type, $order, $description, $owner) { + public function update($id, $title, $stackId, $type, $order, $description, $owner, $duedate) { $this->permissionService->checkPermission($this->cardMapper, $id, Acl::PERMISSION_EDIT); if($this->boardService->isArchived($this->cardMapper, $id)) { throw new StatusException('Operation not allowed. This board is archived.'); @@ -91,6 +88,7 @@ class CardService { $card->setOrder($order); $card->setOwner($owner); $card->setDescription($description); + $card->setDuedate($duedate); return $this->cardMapper->update($card); } diff --git a/templates/main.php b/templates/main.php index f2e675ed5..4728dbf19 100644 --- a/templates/main.php +++ b/templates/main.php @@ -36,16 +36,17 @@ Util::addScript('deck', 'vendor/angular-ui-select/dist/select.min'); Util::addScript('deck', 'vendor/markdown-it/dist/markdown-it.min'); Util::addScript('deck', 'vendor/angular-markdown-it/dist/ng-markdownit.min'); Util::addScript('deck', 'vendor/markdown-it-link-target/dist/markdown-it-link-target.min'); +Util::addScript('deck', 'vendor/jquery-timepicker/jquery.ui.timepicker'); -if(!\OC::$server->getConfig()->getSystemValue('debug', false)) { +if(true && !\OC::$server->getConfig()->getSystemValue('debug', false)) { Util::addScript('deck', 'public/app'); } else { // Load seperate JS files when debug mode is enabled $js = [ 'app' => ['App', 'Config', 'Run'], 'controller' => ['AppController', 'BoardController', 'CardController', 'ListController'], - 'directive' => ['appnavigationentryutils', 'appPopoverMenuUtils', 'autofocusoninsert', 'avatar', 'elastic', 'search'], - 'filters' => ['boardFilterAcl', 'cardFilter', 'cardSearchFilter', 'iconWhiteFilter', 'lightenColorFilter', 'orderObjectBy', 'relativeDateFilter', 'textColorFilter'], + 'directive' => ['appnavigationentryutils', 'appPopoverMenuUtils', 'autofocusoninsert', 'avatar', 'elastic', 'search', 'datepicker', 'timepicker'], + 'filters' => ['boardFilterAcl', 'cardFilter', 'cardSearchFilter', 'iconWhiteFilter', 'lightenColorFilter', 'orderObjectBy', 'dateFilters', 'textColorFilter'], 'service' => ['ApiService', 'BoardService', 'CardService', 'LabelService', 'StackService', 'StatusService'], ]; foreach($js as $folder=>$files) { diff --git a/templates/part.board.mainView.php b/templates/part.board.mainView.php index 51c5b1bec..bc32f1b43 100644 --- a/templates/part.board.mainView.php +++ b/templates/part.board.mainView.php @@ -68,6 +68,9 @@
+ {{ c.duedate | relativeDateFilterString }}