Implement due dates for cards
Signed-off-by: Julius Härtl <jus@bitgrid.net>
This commit is contained in:
committed by
Julius Härtl
parent
58bf51accd
commit
9a77bd7c7c
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
54
img/calendar-white.svg
Normal 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
1
img/calendar.svg
Normal 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 |
@@ -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,
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
|
||||
|
||||
/*
|
||||
* @copyright Copyright (c) 2016 Julius Härtl <jus@bitgrid.net>
|
||||
*
|
||||
@@ -82,6 +80,38 @@ app.controller('CardController', function ($scope, $rootScope, $routeParams, $lo
|
||||
|
||||
$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);
|
||||
};
|
||||
});
|
||||
|
||||
50
js/directive/datepicker.js
Normal file
50
js/directive/datepicker.js
Normal 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
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
});
|
||||
41
js/directive/timepicker.js
Normal file
41
js/directive/timepicker.js
Normal 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
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
@@ -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 "";
|
||||
}
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user