Implement due dates for cards

Signed-off-by: Julius Härtl <jus@bitgrid.net>
This commit is contained in:
Julius Härtl
2017-06-08 23:03:01 +02:00
committed by Julius Härtl
parent 58bf51accd
commit 9a77bd7c7c
17 changed files with 361 additions and 75 deletions

View File

@@ -130,7 +130,6 @@
<name>last_modified</name>
<type>integer</type>
<default></default>
<length>8</length>
<notnull>false</notnull>
<unsigned>true</unsigned>
</field>
@@ -138,7 +137,6 @@
<name>created_at</name>
<type>integer</type>
<default></default>
<length>8</length>
<notnull>false</notnull>
<unsigned>true</unsigned>
</field>
@@ -159,6 +157,11 @@
<type>boolean</type>
<default>false</default>
</field>
<field>
<name>duedate</name>
<type>timestamp</type>
<default>0</default>
</field>
<index>
<name>deck_cards_stack_id_index</name>
<field>

View File

@@ -16,7 +16,7 @@
💥 This is still alpha software: it may not be stable enough for production!
</description>
<version>0.1.4.2</version>
<version>0.1.4.3-1</version>
<licence>agpl</licence>
<author>Julius Härtl</author>
<namespace>Deck</namespace>

View File

@@ -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;

54
img/calendar-white.svg Normal file
View File

@@ -0,0 +1,54 @@
<?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"
height="32"
width="32"
viewbox="0 0 32 32"
version="1.1"
id="svg4"
sodipodi:docname="calendar-white.svg"
inkscape:version="0.92.1 r">
<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" />
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs8" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="958"
inkscape:window-height="1054"
id="namedview6"
showgrid="false"
inkscape:zoom="7.375"
inkscape:cx="-8.0677966"
inkscape:cy="16"
inkscape:window-x="2880"
inkscape:window-y="0"
inkscape:window-maximized="0"
inkscape:current-layer="svg4" />
<path
d="M8 2c-1.108 0-2 .892-2 2v4c0 1.108.892 2 2 2s2-.892 2-2V4c0-1.108-.892-2-2-2zm16 0c-1.108 0-2 .892-2 2v4c0 1.108.892 2 2 2s2-.892 2-2V4c0-1.108-.892-2-2-2zM11 6v2c0 1.662-1.338 3-3 3S5 9.662 5 8V6.125A3.993 3.993 0 0 0 2 10v16c0 2.216 1.784 4 4 4h20c2.216 0 4-1.784 4-4V10a3.993 3.993 0 0 0-3-3.875V8c0 1.662-1.338 3-3 3s-3-1.338-3-3V6zM6.094 16h19.812a.09.09 0 0 1 .094.094v9.812a.09.09 0 0 1-.094.094H6.094A.09.09 0 0 1 6 25.906v-9.812A.09.09 0 0 1 6.094 16z"
id="path2"
style="fill:#ffffff" />
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

1
img/calendar.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="32" width="32" viewbox="0 0 32 32"><path d="M8 2c-1.108 0-2 .892-2 2v4c0 1.108.892 2 2 2s2-.892 2-2V4c0-1.108-.892-2-2-2zm16 0c-1.108 0-2 .892-2 2v4c0 1.108.892 2 2 2s2-.892 2-2V4c0-1.108-.892-2-2-2zM11 6v2c0 1.662-1.338 3-3 3S5 9.662 5 8V6.125A3.993 3.993 0 0 0 2 10v16c0 2.216 1.784 4 4 4h20c2.216 0 4-1.784 4-4V10a3.993 3.993 0 0 0-3-3.875V8c0 1.662-1.338 3-3 3s-3-1.338-3-3V6zM6.094 16h19.812a.09.09 0 0 1 .094.094v9.812a.09.09 0 0 1-.094.094H6.094A.09.09 0 0 1 6 25.906v-9.812A.09.09 0 0 1 6.094 16z"/></svg>

After

Width:  |  Height:  |  Size: 562 B

View File

@@ -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,

View File

@@ -1,5 +1,3 @@
/*
* @copyright Copyright (c) 2016 Julius Härtl <jus@bitgrid.net>
*
@@ -34,54 +32,86 @@ app.controller('CardController', function ($scope, $rootScope, $routeParams, $lo
$scope.statusservice.retainWaiting();
CardService.fetchOne($scope.cardId).then(function(data) {
CardService.fetchOne($scope.cardId).then(function (data) {
$scope.statusservice.releaseWaiting();
$scope.archived = CardService.getCurrent().archived;
}, function(error) {
}, function (error) {
});
$scope.cardRenameShow = function() {
if($scope.archived || !BoardService.canEdit())
$scope.cardRenameShow = function () {
if ($scope.archived || !BoardService.canEdit())
return false;
else {
$scope.status.cardRename=true;
$scope.status.cardRename = true;
}
};
$scope.cardEditDescriptionShow = function($event) {
if(BoardService.isArchived() || CardService.getCurrent().archived) {
$scope.cardEditDescriptionShow = function ($event) {
if (BoardService.isArchived() || CardService.getCurrent().archived) {
return false;
}
var node = $event.target.nodeName;
if($scope.card.archived || !$scope.boardservice.canEdit()) {
if ($scope.card.archived || !$scope.boardservice.canEdit()) {
console.log(node);
} else {
console.log("edit");
$scope.status.cardEditDescription=true;
$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) {
$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.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) {
$scope.labelAssign = function (element, model) {
CardService.assignLabel($scope.cardId, element.id);
var card = CardService.getCurrent();
StackService.updateCard(card);
};
$scope.labelRemove = function(element, model) {
$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);
};
});

View File

@@ -0,0 +1,50 @@
/*
* @copyright Copyright (c) 2017 Julius Härtl <jus@bitgrid.net>
*
* @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/>.
*
*/
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
});
}
}
});

View File

@@ -0,0 +1,41 @@
/*
* @copyright Copyright (c) 2017 Julius Härtl <jus@bitgrid.net>
*
* @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/>.
*
*/
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
});
}
};
});

View File

@@ -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 "";
}
});

View File

@@ -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);
}
/**

View File

@@ -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]);
}

View File

@@ -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;
}
}

View File

@@ -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);
}

View File

@@ -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) {

View File

@@ -68,6 +68,9 @@
<div class="card-controls">
<i class="icon icon-filetype-text" ng-if="c.description" title="{{ c.description }}"></i>
<span class="live-relative-timestamp due" ng-if="c.duedate"
ng-class="{'overdue': c.overdue == 3, 'now': c.overdue == 2, 'next': c.overdue == 1 }"
data-timestamp="{{ c.duedate | dateToTimestamp }}"><i class="icon icon-calendar"></i>{{ c.duedate | relativeDateFilterString }}</span>
<div class="app-popover-menu-utils" ng-if="!boardservice.isArchived()">
<button class="button-inline card-options icon-more" ng-model="card"></button>
<div class="popovermenu hidden">

View File

@@ -45,6 +45,13 @@
style="background-color:#{{label.color}}; color:{{ label.color|textColorFilter }};">{{label.title}}</span>
</ui-select-choices>
</ui-select>
<div id="duedate">
<input class="datepicker-input medium focus" type="text" placeholder="Set a due date" value="{{ cardservice.getCurrent().duedate | parseDate }}" datepicker="due" />
<input class="timepicker-input medium focus" type="text" placeholder="00:00:00" ng-if="cardservice.getCurrent().duedate" value="{{ cardservice.getCurrent().duedate | parseTime }}" timepicker="due" />
<button class="icon icon-delete button-inline" title="Remove duedate" ng-if="cardservice.getCurrent().duedate" ng-click="resetDuedate()"></button>
</div>
</div>
<!--<div id="assigned-users">