Commit new state

This commit is contained in:
Julius Haertl
2016-07-02 22:13:32 +02:00
parent dae1a9b3d4
commit 7a9489adf0
31 changed files with 884 additions and 97 deletions

View File

@@ -203,7 +203,7 @@
<field>
<name>title</name>
<type>text</type>
<notnull>true</notnull>
<notnull>false</notnull>
<length>64</length>
</field>
<field>

View File

@@ -5,7 +5,7 @@
<description>My first ownCloud app</description>
<licence>AGPL</licence>
<author>Julius Härtl</author>
<version>0.0.1.9</version>
<version>0.0.1.10</version>
<namespace>Deck</namespace>
<category>other</category>
<dependencies>

View File

@@ -40,6 +40,14 @@ return [
['name' => 'card#rename', 'url' => '/cards/rename/', 'verb' => 'PUT'],
['name' => 'card#reorder', 'url' => '/cards/reorder/', 'verb' => 'PUT'],
['name' => 'card#delete', 'url' => '/cards/{cardId}/', 'verb' => 'DELETE'],
// card - assign labels
['name' => 'card#assignLabel', 'url' => '/cards/{cardId}/label/{labelId}', 'verb' => 'POST'],
['name' => 'card#removeLabel', 'url' => '/cards/{cardId}/label/{labelId}', 'verb' => 'DELETE'],
// labels
['name' => 'label#create', 'url' => '/labels/', 'verb' => 'POST'],
['name' => 'label#update', 'url' => '/labels/', 'verb' => 'PUT'],
['name' => 'label#delete', 'url' => '/labels/{labelId}/', 'verb' => 'DELETE'],
// TODO: Implement public board sharing
['name' => 'public#index', 'url' => '/public/board/:hash', 'verb' => 'GET'],

View File

@@ -51,8 +51,8 @@ class CardController extends Controller {
/**
* @NoAdminRequired
*/
public function update($id, $title, $stackId, $type, $order) {
return $this->cardService->update($id, $title, $stackId, $type, $order, $this->userId);
public function update($id, $title, $stackId, $type, $order, $description) {
return $this->cardService->update($id, $title, $stackId, $type, $order, $description, $this->userId);
}
/**
* @NoAdminRequired
@@ -60,6 +60,17 @@ class CardController extends Controller {
public function delete($cardId) {
return $this->cardService->delete($this->userId, $cardId);
}
/**
* @NoAdminRequired
*/
public function assignLabel($cardId, $labelId) {
return $this->cardService->assignLabel($this->userId, $cardId, $labelId);
}
/**
* @NoAdminRequired
*/
public function removeLabel($cardId, $labelId) {
return $this->cardService->removeLabel($this->userId, $cardId, $labelId);
}
}

View File

@@ -0,0 +1,43 @@
<?php
namespace OCA\Deck\Controller;
use OCA\Deck\Service\LabelService;
use OCP\IRequest;
use OCP\AppFramework\Http\DataResponse;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Db\DoesNotExistException;
class LabelController extends Controller {
private $userId;
private $labelService;
public function __construct($appName,
IRequest $request,
LabelService $labelService,
$userId){
parent::__construct($appName, $request);
$this->userId = $userId;
$this->labelService = $labelService;
}
/**
* @NoAdminRequired
*/
public function create($title, $color, $boardId) {
return $this->labelService->create($title, $this->userId, $color, $boardId);
}
/**
* @NoAdminRequired
*/
public function update($id, $title, $color) {
return $this->labelService->update($id, $title, $this->userId, $color);
}
/**
* @NoAdminRequired
*/
public function delete($labelId) {
return $this->labelService->delete($this->userId, $labelId);
}
}

View File

@@ -27,54 +27,62 @@
height:100%;
white-space: nowrap; /* important */
overflow: auto;
background-color:#ffffff;
padding:0;
top:-40px;
padding-top:40px;
z-index:100;
}
#board #innerBoard {
padding:10px;
margin-top:40px;
}
#board h1 {
font-size:14pt;
margin-bottom:10px;
padding:10px;
position: fixed;
width:100%;
#board-header {
width: inherit;
color: #333333;
padding-right:250px;
z-index:100;
position:relative;
z-index:200;
background-color:#f7f7f7;
}
#board-header h1 {
font-size:14pt;
margin: 0;
padding:10px;
}
#board-actions {
position:absolute;
font-size:10pt;
/*position:absolute;
right:5px;
top:5px;
z-index:999;
z-index:999;*/
float:right;
position:relative;
margin-top:-5px;
color: #888;
}
#board-actions .filter {
#board-actions .filter .filter-button {
margin-left:10px;
margin-right:10px;
position:relative;
}
#board-actions .filter:hover {
color:#333333;
cursor: pointer;
}
.filter .filter-select {
position: absolute;
top: 42px;
right: -15px;
width: 100px;
.filter {
}
.filter .filter-select li {
.filter-select {
position: absolute;
right: auto;
top:42px;
left:-21%;
}
.filter-select li {
padding: 3px;
overflow: hidden;
width:auto;
}
.filter .filter-select li span {
.filter-select li span {
display: block;
float: left;
width: 20px;height:20px;
@@ -89,6 +97,12 @@
background-color: transparent;
color: #fff;
}
.board-action-button {
font-size:12pt;
font-weight:100;
border:none;
margin-left:10px;
}
.stack {
width:320px;
margin-right:10px;
@@ -284,7 +298,7 @@
margin-bottom:0px;
background-color:#f0f0f0;
}
#card-header .icon-close {
.icon-close {
position: absolute;
top:5px;
right:5px;
@@ -300,13 +314,36 @@
#card-dates span {
}
#card-description h3 {
border-bottom:1px solid #333333;
font-weight: 600;
font-size:10pt;
padding:5px;
}
#card-description textarea {
width:100%;
height:200px;
border: none;
margin: 10px;
margin: 0px;
padding: 0px;
}
#card-description .container {
background-color: white;
}
#card-description .container.ng-hide-remove {
animation: fade 1s forwards;
background-color:rgba(255, 255, 100, 1);
}
@keyframes fade {
from {background-color:rgba(255, 255, 100, 1);}
to {background-color:rgba(255, 255, 255, 0);}
}
#card-attachments,
#sidebar-header,
.card-block {
padding:10px;
}
@@ -320,15 +357,20 @@
max-width: 100%;
border-left: none;
width:500px;
box-shadow: 0px 0px 5px 0px #aaa;
/*box-shadow: 0px 0px 5px 0px #aaa;*/
border-left:1px solid #eeeeee;
}
#app-sidebar.details-visible {
right: 0px;
}
#app-content {
overflow:hidden;
}
#app-content.details-visible {
margin-right: 500px;
}
.labels {
display:block;
overflow:hidden;
@@ -341,6 +383,12 @@
color: #ffffff;
font-size:80%;
font-weight:900;
min-width:20px;
display: inline-block;
text-align: center;
}
.labels li span {
}
#assigned-users {
padding:10px;
@@ -355,16 +403,18 @@
.colorselect .color {
opacity:0.7;
width:27px;
height:27px;
width:26px;
height:26px;
float:left;
margin-right:1px;
border:1px solid #ffffff;
margin-right:2px;
border:none;
}
.colorselect .selected {
opacity:1.0;
border:1px solid #333333;
width: 26px;
height:26px;
}
@@ -425,3 +475,100 @@ button:hover {
border:0;
background-color: transparent;
}
/* board detail */
#board-detail-labels {
padding: 10px;
}
#board-detail-labels ul li {
display: block;
font-size:10pt;
float: none;
margin-bottom:1px;
overflow: hidden;
}
#board-detail-labels ul li input {
float:left;
font-size:10pt;
padding:5px;
}
#board-detail-labels ul li .label-title {
float:left;
width:90%;
font-size:10pt;
padding:5px;
border:none;
margin-right:2px;
}
#board-detail-labels ul li .fa {
float:right;
padding:5px;
}
#board-detail-labels .color {
width:28px;
height:31px;
}
.tabHeaders {
clear: both;
overflow:hidden;
}
#shareWithList .avatar {
float: left;
margin-top: -5px;
margin-right:10px;
}
.ui-select-container.dropdown {
background: #ffffff;
border-radius: 0px;
box-shadow: none;
display: block;
margin-right: 0;
position: static;
width: 100%;
z-index: auto;
padding: 16px;
}
.ui-select-match-close {
float:right;
left: -20px;
margin-top:3px;
z-index: 100;
position: relative;
}
.ui-select-match-item {
padding:2px;
float:left;
display: block;
margin-right:-17px !important;
}
.ui-select-match-item .select-label {
padding:4px;
color:#fff;
padding-right:23px;
}
.ui-select-container {
background-color:#eeeeee !important;
}
.ui-select-container.open {
border: 1px solid #aaaaaa;
}
.ui-select-search {
padding:0px !important;
margin:2px !important;
}
.ui-select-choices-row-inner {
margin-bottom:2px; width:100%;
padding:0;
}
.ui-select-choices-row-inner span {
padding:3px;
padding-left: 10px;
padding-right:10px;
width:100%;
}

View File

@@ -3,9 +3,8 @@
namespace OCA\Deck\Db;
use JsonSerializable;
use OCP\AppFramework\Db\Entity;
class Board extends Entity implements JsonSerializable {
class Board extends \OCA\Deck\Db\Entity implements JsonSerializable {
public $id;
protected $title;
@@ -13,9 +12,12 @@ class Board extends Entity implements JsonSerializable {
protected $color;
protected $archived;
protected $labels;
public function __construct() {
$this->addType('id','integer');
$this->addRelation('labels');
}
public function jsonSerialize() {
return [
'id' => $this->id,

View File

@@ -2,7 +2,6 @@
namespace OCA\Deck\Db;
use OCP\AppFramework\Db\Entity;
use OCP\IDb;
use OCP\AppFramework\Db\Mapper;
@@ -10,6 +9,11 @@ use OCP\AppFramework\Db\Mapper;
class BoardMapper extends Mapper {
private $labelMapper;
private $_relationMappers = array();
public function addRelationMapper($name, $mapper) {
$this->_relationMappers[$name] = $mapper;
}
public function __construct(IDb $db, LabelMapper $labelMapper) {
parent::__construct($db, 'deck_boards', '\OCA\Deck\Db\Board');
@@ -36,8 +40,9 @@ class BoardMapper extends Mapper {
return $this->findEntities($sql, [$userId], $limit, $offset);
}
public function delete(Entity $entity) {
// FIXME: delete linked elements, because owncloud doesn't support foreign keys for apps
public function delete(\OCP\AppFramework\Db\Entity $entity) {
//$this->deleteRelationalEntities('label', $entity);
return parent::delete($entity);
}
}

View File

@@ -3,7 +3,6 @@
namespace OCA\Deck\Db;
use JsonSerializable;
use OCP\AppFramework\Db\Entity;
class Card extends Entity implements JsonSerializable {

View File

@@ -10,8 +10,11 @@ use OCP\AppFramework\Db\Mapper;
class CardMapper extends Mapper {
public function __construct(IDb $db) {
private $labelMapper;
public function __construct(IDb $db, LabelMapper $labelMapper) {
parent::__construct($db, 'deck_cards', '\OCA\Deck\Db\Card');
$this->labelMapper = $labelMapper;
}
public function insert(Entity $entity) {
@@ -37,7 +40,10 @@ class CardMapper extends Mapper {
public function find($id) {
$sql = 'SELECT * FROM `*PREFIX*deck_cards` ' .
'WHERE `id` = ?';
return $this->findEntity($sql, [$id]);
$card = $this->findEntity($sql, [$id]);
$labels = $this->labelMapper->findAssignedLabelsForCard($card->id);
$card->setLabels($labels);
return $card;
}
public function findAllByBoard($boardId, $limit=null, $offset=null) {
@@ -55,4 +61,20 @@ class CardMapper extends Mapper {
return parent::delete($entity);
}
public function assignLabel($card, $label) {
$sql = 'INSERT INTO `*PREFIX*deck_assigned_labels` (`label_id`,`card_id`) VALUES (?,?)';
$stmt = $this->db->prepare($sql);
$stmt->bindParam(1, $label, \PDO::PARAM_INT);
$stmt->bindParam(2, $card, \PDO::PARAM_INT);
$stmt->execute();
}
public function removeLabel($card, $label) {
$sql = 'DELETE FROM `*PREFIX*deck_assigned_labels` WHERE card_id = ? AND label_id = ?';
$stmt = $this->db->prepare($sql);
$stmt->bindParam(1, $card, \PDO::PARAM_INT);
$stmt->bindParam(2, $label, \PDO::PARAM_INT);
$stmt->execute();
}
}

48
db/entity.php Normal file
View File

@@ -0,0 +1,48 @@
<?php
/**
* Created by PhpStorm.
* User: jus
* Date: 22.06.16
* Time: 13:32
*/
namespace OCA\Deck\Db;
class Entity extends \OCP\AppFramework\Db\Entity {
private $_relations = array();
private $_updatedFields = array();
/**
* Mark a property as relation so it will not get updated using Mapper::update
* @param string $property Name of the property
*/
public function addRelation(string $property) {
if (!in_array($property, $this->_relations)) {
$this->_relations[] = $property;
}
}
/**
* Mark am attribute as updated
* overwritten from \OCP\AppFramework\Db\Entity to avoid writing relational attributes
* @param string $attribute the name of the attribute
* @since 7.0.0
*/
protected function markFieldUpdated($attribute){
if(!in_array($attribute, $this->_relations)) {
$this->_updatedFields[$attribute] = true;
}
}
/**
* overwritten from \OCP\AppFramework\Db\Entity to avoid writing relational attributes
* @return array Array of field's update status
*/
public function getUpdatedFields(){
return $this->_updatedFields;
}
}

View File

@@ -3,7 +3,6 @@
namespace OCA\Deck\Db;
use JsonSerializable;
use OCP\AppFramework\Db\Entity;
class Label extends Entity implements JsonSerializable {

View File

@@ -24,11 +24,11 @@ class LabelMapper extends DeckMapper {
}
public function findAssignedLabelsForCard($cardId) {
$sql = 'SELECT * FROM `*PREFIX*deck_assigned_labels` as al INNER JOIN *PREFIX*deck_labels as l ON l.id = al.label_id WHERE `card_id` = ?';
$sql = 'SELECT l.* FROM `*PREFIX*deck_assigned_labels` as al INNER JOIN *PREFIX*deck_labels as l ON l.id = al.label_id WHERE `card_id` = ?';
return $this->findEntities($sql, [$cardId], $limit, $offset);
}
public function findAssignedLabelsForBoard($boardId, $limit=null, $offset=null) {
$sql = "SELECT c.id as card_id, l.id as id, l.title as title, color FROM oc_deck_cards as c " .
$sql = "SELECT c.id as card_id, l.id as id, l.title as title, l.color as color FROM oc_deck_cards as c " .
" INNER JOIN oc_deck_assigned_labels as al, oc_deck_labels as l ON al.card_id = c.id AND al.label_id = l.id WHERE board_id=?";
$entities = $this->findEntities($sql, [$boardId], $limit, $offset);
return $entities;

View File

@@ -1,2 +1,28 @@
var app = angular.module('Deck', ['ngRoute', 'ngSanitize', 'ui.router', 'as.sortable']);
angular.module('markdown', [])
.provider('markdown', [function () {
var opts = {};
return {
config: function (newOpts) {
opts = newOpts;
},
$get: function () {
return new window.showdown.Converter(opts);
}
};
}])
.filter('markdown', ['markdown', function (markdown) {
return function (text) {
return markdown.makeHtml(text || '');
};
}]);
var app = angular.module('Deck', [
'ngRoute',
'ngSanitize',
'ui.router',
'ui.select',
'as.sortable',
'markdown',
'ngAnimate'
]);

View File

@@ -17,6 +17,14 @@ app.config(function ($provide, $routeProvider, $interpolateProvider, $httpProvid
templateUrl: "/board.html",
controller: 'BoardController'
})
.state('board.detail', {
url: "/detail/",
views: {
"sidebarView": {
templateUrl: "/board.sidebarView.html",
}
}
})
.state('board.card', {
url: "/card/:cardId",
views: {

View File

@@ -6,10 +6,16 @@ app.run(function ($document, $rootScope, $transitions) {
$transitions.onEnter({to: 'board.card'}, function ($state, $transition$) {
$rootScope.sidebar.show = true;
});
$transitions.onEnter({to: 'board.detail'}, function ($state, $transition$) {
$rootScope.sidebar.show = true;
});
$transitions.onEnter({to: 'board'}, function ($state) {
$rootScope.sidebar.show = false;
});
$transitions.onExit({from: 'board.card'}, function ($state) {
$rootScope.sidebar.show = false;
});
$transitions.onExit({from: 'board.detail'}, function ($state) {
$rootScope.sidebar.show = false;
});
});

View File

@@ -13,7 +13,9 @@
"momentjs": "~2.11.*",
"es6-shim": "~0.*",
"js-url": "~2.*",
"masonry": "~4.0.0"
"masonry": "~4.0.0",
"showdown": "~1.4.2",
"angular-ui-select": "~0.18.0"
},
"license": "AGPL-3.0",
"private": true,

View File

@@ -1,13 +1,18 @@
app.controller('BoardController', function ($rootScope, $scope, $stateParams, StatusService, BoardService, StackService, CardService) {
app.controller('BoardController', function ($rootScope, $scope, $stateParams, StatusService, BoardService, StackService, CardService, LabelService) {
$scope.sidebar = $rootScope.sidebar;
$scope.id = $stateParams.boardId;
$scope.status={},
$scope.newLabel={};
$scope.status.boardtab = $stateParams.detailTab;
$scope.stackservice = StackService;
$scope.boardservice = BoardService;
$scope.statusservice = StatusService.getInstance();
$scope.labelservice = LabelService;
$scope.defaultColors = ['31CC7C', '317CCC', 'FF7A66', 'F1DB50', '7C31CC', 'CC317C', '3A3B3D', 'CACBCD'];
// fetch data
@@ -15,7 +20,6 @@ app.controller('BoardController', function ($rootScope, $scope, $stateParams, St
$scope.statusservice.retainWaiting();
$scope.statusservice.retainWaiting();
console.log("foo");
StackService.fetchAll($scope.id).then(function(data) {
console.log(data);
@@ -61,6 +65,26 @@ app.controller('BoardController', function ($rootScope, $scope, $stateParams, St
}
$scope.labelDelete = function(label) {
LabelService.delete(label.id);
// remove from board data
var i = BoardService.getCurrent().labels.indexOf(label);
BoardService.getCurrent().labels.splice(i, 1);
// TODO: remove from cards
}
$scope.labelCreate = function(label) {
label.boardId = $scope.id;
LabelService.create(label);
BoardService.getCurrent().labels.push(label);
$scope.status.createLabel = false;
$scope.newLabel = {};
}
$scope.labelUpdate = function(label) {
label.edit = false;
LabelService.update(label);
}
// TODO: move to filter?
// Lighten Color of the board for background usage
$scope.rgblight = function (hex) {
var result = /^([A-Fa-f\d]{2})([A-Fa-f\d]{2})([A-Fa-f\d]{2})$/i.exec(hex);
@@ -77,6 +101,49 @@ app.controller('BoardController', function ($rootScope, $scope, $stateParams, St
}
};
// TODO: move to filter?
// RGB2HLS by Garry Tan
// http://axonflux.com/handy-rgb-to-hsl-and-rgb-to-hsv-color-model-c
$scope.textColor = function (hex) {
var result = /^([A-Fa-f\d]{2})([A-Fa-f\d]{2})([A-Fa-f\d]{2})$/i.exec(hex);
var color = result ? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16)
} : null;
if(result !== null) {
r = color.r/255;
g = color.g/255;
b = color.b/255;
var max = Math.max(r, g, b), min = Math.min(r, g, b);
var h, s, l = (max + min) / 2;
if(max == min){
h = s = 0; // achromatic
}else{
var d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
switch(max){
case r: h = (g - b) / d + (g < b ? 6 : 0); break;
case g: h = (b - r) / d + 2; break;
case b: h = (r - g) / d + 4; break;
}
h /= 6;
}
// TODO: Maybe just darken/lighten the color
if(l<0.5) {
return "#ffffff";
} else {
return "#000000";
}
//var rgba = "rgba(" + color.r + "," + color.g + "," + color.b + ",0.7)";
//return rgba;
} else {
return "#aa0000";
}
};
// settings for card sorting
$scope.sortOptions = {

View File

@@ -2,6 +2,7 @@
app.controller('CardController', function ($scope, $rootScope, $routeParams, $location, $stateParams, BoardService, CardService, StackService, StatusService) {
$scope.sidebar = $rootScope.sidebar;
$scope.status = {};
$scope.cardservice = CardService;
$scope.cardId = $stateParams.cardId;
@@ -27,6 +28,22 @@ app.controller('CardController', function ($scope, $rootScope, $routeParams, $lo
});
};
$scope.updateCard = function(card) {
CardService.update(CardService.getCurrent());
$scope.status.description = false;
}
$scope.editDescription = function() {
$scope.status.description = true;
}
$scope.labelAssign = function(element, model) {
CardService.assignLabel($scope.cardId, element.id)
}
$scope.labelRemove = function(element, model) {
CardService.removeLabel($scope.cardId, element.id)
}
/*var menu = $('#app-content');
menu.click(function(event){

View File

@@ -1,5 +1,31 @@
var app = angular.module('Deck', ['ngRoute', 'ngSanitize', 'ui.router', 'as.sortable']);
angular.module('markdown', [])
.provider('markdown', [function () {
var opts = {};
return {
config: function (newOpts) {
opts = newOpts;
},
$get: function () {
return new window.showdown.Converter(opts);
}
};
}])
.filter('markdown', ['markdown', function (markdown) {
return function (text) {
return markdown.makeHtml(text || '');
};
}]);
var app = angular.module('Deck', [
'ngRoute',
'ngSanitize',
'ui.router',
'ui.select',
'as.sortable',
'markdown',
'ngAnimate'
]);
app.config(["$provide", "$routeProvider", "$interpolateProvider", "$httpProvider", "$urlRouterProvider", "$stateProvider", "$compileProvider", function ($provide, $routeProvider, $interpolateProvider, $httpProvider, $urlRouterProvider, $stateProvider, $compileProvider) {
@@ -21,6 +47,14 @@ app.config(["$provide", "$routeProvider", "$interpolateProvider", "$httpProvider
templateUrl: "/board.html",
controller: 'BoardController'
})
.state('board.detail', {
url: "/detail/",
views: {
"sidebarView": {
templateUrl: "/board.sidebarView.html",
}
}
})
.state('board.card', {
url: "/card/:cardId",
views: {
@@ -45,12 +79,18 @@ app.run(["$document", "$rootScope", "$transitions", function ($document, $rootSc
$transitions.onEnter({to: 'board.card'}, function ($state, $transition$) {
$rootScope.sidebar.show = true;
});
$transitions.onEnter({to: 'board.detail'}, function ($state, $transition$) {
$rootScope.sidebar.show = true;
});
$transitions.onEnter({to: 'board'}, function ($state) {
$rootScope.sidebar.show = false;
});
$transitions.onExit({from: 'board.card'}, function ($state) {
$rootScope.sidebar.show = false;
});
$transitions.onExit({from: 'board.detail'}, function ($state) {
$rootScope.sidebar.show = false;
});
}]);
@@ -61,15 +101,20 @@ app.controller('AppController', ["$scope", "$location", "$http", "$route", "$log
$scope.sidebar = $rootScope.sidebar;
}]);
app.controller('BoardController', ["$rootScope", "$scope", "$stateParams", "StatusService", "BoardService", "StackService", "CardService", function ($rootScope, $scope, $stateParams, StatusService, BoardService, StackService, CardService) {
app.controller('BoardController', ["$rootScope", "$scope", "$stateParams", "StatusService", "BoardService", "StackService", "CardService", "LabelService", function ($rootScope, $scope, $stateParams, StatusService, BoardService, StackService, CardService, LabelService) {
$scope.sidebar = $rootScope.sidebar;
$scope.id = $stateParams.boardId;
$scope.status={},
$scope.newLabel={};
$scope.status.boardtab = $stateParams.detailTab;
$scope.stackservice = StackService;
$scope.boardservice = BoardService;
$scope.statusservice = StatusService.getInstance();
$scope.labelservice = LabelService;
$scope.defaultColors = ['31CC7C', '317CCC', 'FF7A66', 'F1DB50', '7C31CC', 'CC317C', '3A3B3D', 'CACBCD'];
// fetch data
@@ -77,7 +122,6 @@ app.controller('BoardController', ["$rootScope", "$scope", "$stateParams", "Stat
$scope.statusservice.retainWaiting();
$scope.statusservice.retainWaiting();
console.log("foo");
StackService.fetchAll($scope.id).then(function(data) {
console.log(data);
@@ -123,6 +167,26 @@ app.controller('BoardController', ["$rootScope", "$scope", "$stateParams", "Stat
}
$scope.labelDelete = function(label) {
LabelService.delete(label.id);
// remove from board data
var i = BoardService.getCurrent().labels.indexOf(label);
BoardService.getCurrent().labels.splice(i, 1);
// TODO: remove from cards
}
$scope.labelCreate = function(label) {
label.boardId = $scope.id;
LabelService.create(label);
BoardService.getCurrent().labels.push(label);
$scope.status.createLabel = false;
$scope.newLabel = {};
}
$scope.labelUpdate = function(label) {
label.edit = false;
LabelService.update(label);
}
// TODO: move to filter?
// Lighten Color of the board for background usage
$scope.rgblight = function (hex) {
var result = /^([A-Fa-f\d]{2})([A-Fa-f\d]{2})([A-Fa-f\d]{2})$/i.exec(hex);
@@ -139,6 +203,49 @@ app.controller('BoardController', ["$rootScope", "$scope", "$stateParams", "Stat
}
};
// TODO: move to filter?
// RGB2HLS by Garry Tan
// http://axonflux.com/handy-rgb-to-hsl-and-rgb-to-hsv-color-model-c
$scope.textColor = function (hex) {
var result = /^([A-Fa-f\d]{2})([A-Fa-f\d]{2})([A-Fa-f\d]{2})$/i.exec(hex);
var color = result ? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16)
} : null;
if(result !== null) {
r = color.r/255;
g = color.g/255;
b = color.b/255;
var max = Math.max(r, g, b), min = Math.min(r, g, b);
var h, s, l = (max + min) / 2;
if(max == min){
h = s = 0; // achromatic
}else{
var d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
switch(max){
case r: h = (g - b) / d + (g < b ? 6 : 0); break;
case g: h = (b - r) / d + 2; break;
case b: h = (r - g) / d + 4; break;
}
h /= 6;
}
// TODO: Maybe just darken/lighten the color
if(l<0.5) {
return "#ffffff";
} else {
return "#000000";
}
//var rgba = "rgba(" + color.r + "," + color.g + "," + color.b + ",0.7)";
//return rgba;
} else {
return "#aa0000";
}
};
// settings for card sorting
$scope.sortOptions = {
@@ -191,6 +298,7 @@ app.controller('BoardController', ["$rootScope", "$scope", "$stateParams", "Stat
app.controller('CardController', ["$scope", "$rootScope", "$routeParams", "$location", "$stateParams", "BoardService", "CardService", "StackService", "StatusService", function ($scope, $rootScope, $routeParams, $location, $stateParams, BoardService, CardService, StackService, StatusService) {
$scope.sidebar = $rootScope.sidebar;
$scope.status = {};
$scope.cardservice = CardService;
$scope.cardId = $stateParams.cardId;
@@ -216,6 +324,22 @@ app.controller('CardController', ["$scope", "$rootScope", "$routeParams", "$loca
});
};
$scope.updateCard = function(card) {
CardService.update(CardService.getCurrent());
$scope.status.description = false;
}
$scope.editDescription = function() {
$scope.status.description = true;
}
$scope.labelAssign = function(element, model) {
CardService.assignLabel($scope.cardId, element.id)
}
$scope.labelRemove = function(element, model) {
CardService.removeLabel($scope.cardId, element.id)
}
/*var menu = $('#app-content');
menu.click(function(event){
@@ -433,6 +557,7 @@ app.factory('ApiService', ["$http", "$q", function($http, $q){
};
// methods for managing data
ApiService.prototype.clear = function() {
this.data = {};
@@ -444,6 +569,7 @@ app.factory('ApiService', ["$http", "$q", function($http, $q){
} else {
Object.keys(entity).forEach(function (key) {
element[key] = entity[key];
if(element[key]!==null)
element[key].status = {};
});
}
@@ -516,9 +642,42 @@ app.factory('CardService', ["ApiService", "$http", "$q", function(ApiService, $h
return deferred.promise;
}
CardService.prototype.assignLabel = function(card, label) {
//['name' => 'card#assignLabel', 'url' => '/cards/{cardId}/label/{labelId}', 'verb' => 'POST'],
var url = this.baseUrl + '/' + card + '/label/' + label;
var deferred = $q.defer();
var self = this;
$http.post(url).then(function (response) {
deferred.resolve(response.data);
}, function (error) {
deferred.reject('Error while update ' + self.endpoint);
});
return deferred.promise;
}
CardService.prototype.removeLabel = function(card, label) {
// ['name' => 'card#removeLabel', 'url' => '/cards/{cardId}/label/{labelId}', 'verb' => 'DELETE'],
var url = this.baseUrl + '/' + card + '/label/' + label;
var deferred = $q.defer();
var self = this;
$http.delete(url).then(function (response) {
deferred.resolve(response.data);
}, function (error) {
deferred.reject('Error while update ' + self.endpoint);
});
return deferred.promise;
}
service = new CardService($http, 'cards', $q)
return service;
}]);
app.factory('LabelService', ["ApiService", "$http", "$q", function(ApiService, $http, $q){
var LabelService = function($http, ep, $q) {
ApiService.call(this, $http, ep, $q);
};
LabelService.prototype = angular.copy(ApiService.prototype);
service = new LabelService($http, 'labels', $q)
return service;
}]);
app.factory('StackService', ["ApiService", "$http", "$q", function(ApiService, $http, $q){
var StackService = function($http, ep, $q) {
ApiService.call(this, $http, ep, $q);

View File

@@ -86,6 +86,7 @@ app.factory('ApiService', function($http, $q){
};
// methods for managing data
ApiService.prototype.clear = function() {
this.data = {};
@@ -97,6 +98,7 @@ app.factory('ApiService', function($http, $q){
} else {
Object.keys(entity).forEach(function (key) {
element[key] = entity[key];
if(element[key]!==null)
element[key].status = {};
});
}

View File

@@ -28,6 +28,31 @@ app.factory('CardService', function(ApiService, $http, $q){
return deferred.promise;
}
CardService.prototype.assignLabel = function(card, label) {
//['name' => 'card#assignLabel', 'url' => '/cards/{cardId}/label/{labelId}', 'verb' => 'POST'],
var url = this.baseUrl + '/' + card + '/label/' + label;
var deferred = $q.defer();
var self = this;
$http.post(url).then(function (response) {
deferred.resolve(response.data);
}, function (error) {
deferred.reject('Error while update ' + self.endpoint);
});
return deferred.promise;
}
CardService.prototype.removeLabel = function(card, label) {
// ['name' => 'card#removeLabel', 'url' => '/cards/{cardId}/label/{labelId}', 'verb' => 'DELETE'],
var url = this.baseUrl + '/' + card + '/label/' + label;
var deferred = $q.defer();
var self = this;
$http.delete(url).then(function (response) {
deferred.resolve(response.data);
}, function (error) {
deferred.reject('Error while update ' + self.endpoint);
});
return deferred.promise;
}
service = new CardService($http, 'cards', $q)
return service;
});

View File

@@ -0,0 +1,8 @@
app.factory('LabelService', function(ApiService, $http, $q){
var LabelService = function($http, ep, $q) {
ApiService.call(this, $http, ep, $q);
};
LabelService.prototype = angular.copy(ApiService.prototype);
service = new LabelService($http, 'labels', $q)
return service;
});

View File

@@ -2,6 +2,7 @@
namespace OCA\Deck\Service;
use OCA\Deck\Db\Label;
use OCP\ILogger;
use OCP\IL10N;
use OCP\AppFramework\Db\DoesNotExistException;
@@ -9,19 +10,23 @@ use OCP\AppFramework\Utility\ITimeFactory;
use \OCA\Deck\Db\Board;
use \OCA\Deck\Db\BoardMapper;
use \OCA\Deck\Db\LabelMapper;
class BoardService {
private $boardMapper;
private $labelMapper;
private $logger;
private $l10n;
private $timeFactory;
public function __construct(BoardMapper $boardMapper, ILogger $logger,
IL10N $l10n,
ITimeFactory $timeFactory) {
ITimeFactory $timeFactory,
LabelMapper $labelMapper) {
$this->boardMapper = $boardMapper;
$this->labelMapper = $labelMapper;
$this->logger = $logger;
}
@@ -45,7 +50,19 @@ class BoardService {
$board->setTitle($title);
$board->setOwner($userId);
$board->setColor($color);
return $this->boardMapper->insert($board);
$new_board = $this->boardMapper->insert($board);
// create new labels
$default_labels = ['31CC7C', '317CCC', 'FF7A66', 'F1DB50', '7C31CC', 'CC317C', '3A3B3D', 'CACBCD'];
$labels = [];
foreach ($default_labels as $color) {
$label = new Label();
$label->setColor($color);
$label->setBoardId($new_board->getId());
$labels[] = $this->labelMapper->insert($label);
}
$new_board->setLabels($labels);
return $new_board;
}

View File

@@ -39,13 +39,14 @@ class CardService {
return $this->cardMapper->delete($this->cardMapper->find($id));
}
public function update($id, $title, $stackId, $type, $order, $owner) {
public function update($id, $title, $stackId, $type, $order, $description, $owner) {
$card = $this->cardMapper->find($id);
$card->setTitle($title);
$card->setStackId($stackId);
$card->setType($type);
$card->setOrder($order);
$card->setOwner($owner);
$card->setDescription($description);
return $this->cardMapper->update($card);
}
@@ -75,4 +76,13 @@ class CardService {
$cards = $this->cardMapper->findAll($stackId);
return $cards;
}
public function assignLabel($userId, $cardId, $labelId) {
$this->cardMapper->assignLabel($cardId, $labelId);
}
public function removeLabel($userId, $cardId, $labelId) {
$this->cardMapper->removeLabel($cardId, $labelId);
}
}

56
service/labelservice.php Normal file
View File

@@ -0,0 +1,56 @@
<?php
namespace OCA\Deck\Service;
use OCA\Deck\Db\Label;
use OCP\ILogger;
use OCP\IL10N;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Utility\ITimeFactory;
use \OCA\Deck\Db\Board;
use \OCA\Deck\Db\BoardMapper;
use \OCA\Deck\Db\LabelMapper;
class LabelService {
private $labelMapper;
private $logger;
private $l10n;
private $timeFactory;
public function __construct(ILogger $logger,
IL10N $l10n,
ITimeFactory $timeFactory,
LabelMapper $labelMapper) {
$this->labelMapper = $labelMapper;
$this->logger = $logger;
}
public function find($userId, $labelId) {
$label = $this->labelMapper->find($labelId);
// FIXME: [share] Check for user permissions
return $label;
}
public function create($title, $userId, $color, $boardId) {
$label = new Label();
$label->setTitle($title);
$label->setColor($color);
$label->setBoardId($boardId);
return $this->labelMapper->insert($label);
}
public function delete($userId, $id) {
return $this->labelMapper->delete($this->find($userId, $id));
}
public function update($id, $title, $userId, $color) {
$label = $this->find($userId, $id);
$label->setTitle($title);
$label->setColor($color);
return $this->labelMapper->update($label);
}
}

View File

@@ -5,6 +5,7 @@ use OCP\Util;
Util::addStyle('deck', 'font-awesome');
Util::addStyle('deck', 'style');
Util::addStyle('deck', '../js/vendor/ng-sortable/dist/ng-sortable.min');
Util::addStyle('deck', '../js/vendor/angular-ui-select/dist/select.min');
//Util::addStyle('deck', '../js/vendor/ng-sortable/dist/ng-sortable.style.min');
Util::addScript('deck', 'vendor/angular/angular.min');
Util::addScript('deck', 'vendor/angular-route/angular-route.min');
@@ -12,6 +13,8 @@ Util::addScript('deck', 'vendor/angular-sanitize/angular-sanitize.min');
Util::addScript('deck', 'vendor/angular-animate/angular-animate.min');
Util::addScript('deck', 'vendor/angular-ui-router/release/angular-ui-router.min');
Util::addScript('deck', 'vendor/ng-sortable/dist/ng-sortable.min');
Util::addScript('deck', 'vendor/angular-ui-select/dist/select.min');
Util::addScript('deck', 'vendor/showdown/dist/showdown.min');
Util::addScript('deck', 'public/app');
?>
@@ -33,15 +36,15 @@ Util::addScript('deck', 'public/app');
<script type="text/ng-template" id="/boardlist.sidebarView.html">
<?php print_unescaped($this->inc('part.empty')); ?>
</script>
<script type="text/ng-template" id="/board.sidebarView.html">
<?php print_unescaped($this->inc('part.board.sidebarView')); ?>
</script>
<script type="text/ng-template" id="/board.mainView.html">
<?php print_unescaped($this->inc('part.board.mainView')); ?>
</script>
<script type="text/ng-template" id="/board.html">
<?php print_unescaped($this->inc('part.board')); ?>
</script>
<script type="text/ng-template" id="/board.sidebarView.html">
<?php print_unescaped($this->inc('part.empty')); ?>
</script>
<script type="text/ng-template" id="/card.sidebarView.html">
<?php print_unescaped($this->inc('part.card')); ?>
</script>

View File

@@ -4,26 +4,30 @@
<h2>{{ statusservice.title }}</h2>
<p>{{ statusservice.text }}</p></div>
</div>
<div id="board" class="scroll-container" >
<h1>
<div id="board-header">
<h1>
{{ boardservice.data[id].title }}
</h1>
<div id="board-actions">
<div><i class="fa fa-filter"> </i> Filter</div>
<div class="filter">by label <i class="fa fa-caret-down"> </i>
<ul class="filter-select bubble">
<div class="filter"><span class="filter-button" ng-click="status.filter.label=!status.filter.label">by label <i class="fa fa-caret-down"> </i></span></div>
<ul class="filter-select bubble" ng-if="status.filter.label">
<li ng-repeat="label in boardservice.data[id].labels"><span style="background-color:#{{ label.color }};"> </span> {{ label.title }}</li>
</ul>
<div class="filter"><span class="filter-button" ng-click="status.filter.assignee=!status.filter.assignee">by assignee<i class="fa fa-caret-down"> </i></span></div>
<ul class="filter-select bubble" ng-if="status.filter.assignee">
<li ng-repeat="label in boardservice.data[id].labels"><span style="background-color:#{{ label.color }};"> </span> {{ label.title }}</li>
</ul>
</div>
<div class="filter">by creator <i class="fa fa-caret-down"> </i></div>
<div class="filter">by members <i class="fa fa-caret-down"> </i></div>
<div><i class="fa fa-share-alt"> </i></div>
<div><i class="fa fa-users"> </i></div>
<div><i class="fa fa-ellipsis-h"> </i></div>
<div class="board-action-button"><a class="fa fa-share-alt" ui-sref="board.detail({ id: id })"> </a></div>
<div class="board-action-button"><a class="fa fa-users" ui-sref="board.detail({ id: id })"> </a></div>
<div class="board-action-button"><a class="fa fa-ellipsis-h" ui-sref="board.detail({ id: id })"> </a></div>
</div>
</h1>
</div>
<div id="board" class="scroll-container" >
<div id="innerBoard" data-ng-model="stacks">
@@ -44,7 +48,8 @@
<div class="card-upper">
<h3>{{ c.title }}</h3>
<ul class="labels">
<li ng-repeat="label in c.labels" style="background-color: #{{ label.color }};"><span>{{ label.title }}</span></li>
<li ng-repeat="label in c.labels" style="background-color: #{{ label.color }};"><span>{{ label.title }}</span>
</li>
</ul>
</div>

View File

@@ -1,6 +1,4 @@
<?php print_unescaped($this->inc('part.board.mainView')); ?>
<route-loading-indicator></route-loading-indicator>
<div id="app-sidebar" class="details-view scroll-container" ng-class="{ 'details-visible': sidebar.show }" ui-view="sidebarView" ng-controller="CardController">
<div id="app-sidebar" class="details-view scroll-container" ng-class="{ 'details-visible': sidebar.show }" ui-view="sidebarView">
</div>

View File

@@ -0,0 +1,79 @@
<div id="board-status" ng-if="statusservice.active">
<div id="emptycontent">
<div class="icon-{{ statusservice.icon }}"></div>
<h2>{{ statusservice.title }}</h2>
<p>{{ statusservice.text }}</p></div>
</div>
<div id="sidebar-header">
<a class="icon-close" ui-sref="board" ng-click="sidebar.show=!sidebar.show"> &nbsp;</a>
<h2>{{ boardservice.getCurrent().title }}</h2>
</div>
<ul class="tabHeaders">
<li class="tabHeader" ng-class="{'selected': (status.boardtab==0 || !status.boardtab)}" ng-click="status.boardtab=0"><a>Sharing</a></li>
<li class="tabHeader" ng-class="{'selected': (status.boardtab==1)}" ng-click="status.boardtab=1"><a>Labels</a></li>
<li class="tabHeader" ng-class="{'selected': (status.boardtab==2)}" ng-click="status.boardtab=2"><a>Settings</a></li>
</ul>
<div class="tabsContainer">
<div id="commentsTabView" class="tab commentsTabView" ng-if="status.boardtab==0 || !status.boardtab">
<input class="shareWithField ui-autocomplete-input" type="text" placeholder="Mit Benutzern, Gruppen oder entfernten Benutzern teilen…" autocomplete="off">
<ul id="shareWithList" class="shareWithList">
<li data-share-id="57" data-share-type="0" data-share-with="directmenu">
<a href="#" class="unshare"><span class="icon-loading-small"></span><span class="icon icon-delete"><br /></span><span class="hidden-visually">Freigabe aufheben</span></a>
<div class="avatar " data-username="directmenu" style="height: 32px; width: 32px; color: rgb(255, 255, 255); font-weight: normal; text-align: center; line-height: 32px; font-size: 17.6px; background-color: rgb(195, 222, 124);">D</div>
<span class="has-tooltip username" title="" data-original-title="directmenu" aria-describedby="tooltip777914">directmenu</span>
<span class="shareOption">
<input id="canShare-view17-directmenu" type="checkbox" name="share" class="permissions checkbox" checked="checked" data-permissions="16">
<label for="canShare-view17-directmenu">kann teilen</label>
</span>
<span class="shareOption"><input id="canEdit-view17-directmenu" type="checkbox" name="edit" class="permissions checkbox" checked="checked">
<label for="canEdit-view17-directmenu">kann bearbeiten</label>
</span>
</li>
</ul>
</div>
<div id="board-detail-labels" class="tab commentsTabView" ng-if="status.boardtab==1">
<ul class="labels">
<li ng-repeat="label in boardservice.getCurrent().labels">
<span class="label-title" style="background-color:#{{label.color}}; color:{{ textColor(label.color) }};" ng-if="!label.edit">
{{ label.title }}
</span>
<span class="label-title" style="background-color:#{{label.color}}; color:{{ textColor(label.color) }}; width:188px;" ng-if="label.edit">
<input type="text" placeholder="" ng-model="label.title" class="input-inline" style="background-color:#{{label.color}}; color:{{ textColor(label.color) }};" />
</span>
<div class="colorselect" ng-if="label.edit">
<div class="color" ng-repeat="c in defaultColors" style="background-color:#{{ c }};" ng-click="label.color=c" ng-class="{'selected': (c == label.color) }"><br /></div>
</div>
<a class="fa fa-save" ng-click="labelUpdate(label)" ng-if="label.edit"> </a>
<a class="fa fa-edit" ng-click="label.edit=true" ng-if="!label.edit"> </a>
<a class="fa fa-remove" ng-click="labelDelete(label)"> </a>
</li>
<li ng-if="status.createLabel">
<form ng-submit="labelCreate(newLabel)">
<span class="label-title" style="background-color:#{{newLabel.color}}; color:{{ textColor(newLabel.color) }}; width:188px;">
<input type="text" class="input-inline" ng-model="newLabel.title" style="color:{{ textColor(newLabel.color) }};" autofocus-on-insert />
</span>
<div class="colorselect">
<div class="color" ng-repeat="c in defaultColors" style="background-color:#{{ c }};" ng-click="newLabel.color=c" ng-class="{'selected': (c == newLabel.color) }"><br /></div>
</div>
<a class="fa fa-save" ng-click="labelCreate(newLabel)"> </a>
</form>
</li>
<li ng-if="!status.createLabel">
<a ng-click="status.createLabel=true"><span class="fa fa-plus"> </span> Create a new Label</a>
</li>
</ul>
</div>
<div id="commentsTabView" class="tab commentsTabView" ng-if="status.boardtab==2">
<p><input type="checkbox" class="checkbox" id="allowInvite" /> <label for="allowInvite">Allow members to invite other users</label></p>
<p><input type="checkbox" class="checkbox" id="allowInvite" /> <label for="allowInvite">Allow members to make board public</label></p>
<p><input type="checkbox" class="checkbox" id="allowInvite" /> <label for="allowInvite">Allow members to change labels</label></p>
<p><input type="checkbox" class="checkbox" id="allowInvite" /> <label for="allowInvite">Allow members to create new stacks</label></p>
</div>
</div>

View File

@@ -4,6 +4,7 @@
<h2>{{ statusservice.title }}</h2>
<p>{{ statusservice.text }}</p></div>
</div>
{{card=cardservice.getCurrent();""}}
<div id="card-header">
<a class="icon-close" ui-sref="board" ng-click="sidebar.show=!sidebar.show"> &nbsp;</a>
<h2>
@@ -18,26 +19,40 @@
<div id="card-dates">
Modified: <span>{{ cardservice.getCurrent().lastModified*1000|date:'medium' }}</span>
Created: <span>{{ cardservice.getCurrent().createdAt*1000|date:'medium' }}</span>
by <span>{{ cardservice.getCurrent().owner }}</span>
</div>
<ul class="labels">
<li style="background-color:#aa0000;">important</li>
<li style="background-color:#00aa00;">action-needed</li>
<li style="background-color:#00a;">action-needed</li>
</ul>
<ui-select multiple tagging tagging-label="(custom 'new' label)" ng-model="card.labels" theme="bootstrap" style="width:100%;" title="Choose a label" placeholder="Add a label"
on-select="labelAssign($item, $model)" on-remove="labelRemove($item, $model)">
<ui-select-match placeholder="Select labels..."><span class="select-label" style="background-color:#{{$item.color}}">{{$item.title}}</span></ui-select-match>
<ui-select-choices repeat="label in boardservice.getCurrent().labels | filter:$select.search">
<span style="background-color:#{{label.color}}">{{label.title}}</span>
</ui-select-choices>
</ui-select>
<br style="clear:both;"/>
<br style="clear:both;"/>
<div id="assigned-users">
<ui-select multiple tagging tagging-label="(custom 'new' label)" ng-model="card.assignees" theme="bootstrap" style="width:100%;" title="Choose a label">
<ui-select-match placeholder="Select labels..."><span style="background-color:#{{$item.color}}">{{$item.title}}</span></ui-select-match>
<ui-select-choices repeat="label in boardservice.getCurrent().labels | filter:$select.search">
<div class="avatardiv" style="height: 30px; width: 30px; color: rgb(255, 255, 255); font-weight: normal; text-align: center; line-height: 30px; font-size: 17px; background-color: rgb(213, 231, 116);">D</div>
<div class="avatardiv" style="height: 30px; width: 30px; color: rgb(255, 255, 255); font-weight: normal; text-align: center; line-height: 30px; font-size: 17px; background-color: rgb(213, 120, 220);">E</div>
<div class="avatardiv" style="height: 30px; width: 30px; color: rgb(255, 255, 255); font-weight: normal; text-align: center; line-height: 30px; font-size: 17px; background-color: rgb(120, 120, 220);">C</div>
<div class="avatardiv" style="height: 30px; width: 30px; color: rgb(255, 255, 255); font-weight: normal; text-align: center; line-height: 30px; font-size: 17px; background-color: rgb(120, 220, 220);">K</div>
<div class="avatardiv" style="height: 30px; width: 30px; color: rgb(255, 255, 255); font-weight: normal; text-align: center; line-height: 30px; font-size: 17px; background-color: rgb(220, 220, 220);">+</div>
</ui-select-choices>
</ui-select>
</div>
<div id="card-description">
<textarea ng-model="cardservice.getCurrent().description">{{ cardservice.getCurrent().description }}</textarea>
<div class="saved">Saved</div>
</div>
<h3>Description</h3>
<textarea ng-if="status.description" placeholder="Enter your description here ..." ng-blur="updateCard(cardservice.getCurrent())" ng-model="cardservice.getCurrent().description" autofocus-on-insert> </textarea>
<div class="container" ng-click="editDescription()" ng-show="!status.description" ng-animate><div ng-bind-html="cardservice.getCurrent().description | markdown"></div><div class="placeholder" ng-if="!cardservice.getCurrent().description">Add a card description ...</div></div>
</div>
</div>
<ul class="tabHeaders"> <li class="tabHeader selected" data-tabid="commentsTabView" data-tabindex="0"> <a href="#">Kommentare</a> </li> <li class="tabHeader" data-tabid="shareTabView" data-tabindex="1"> <a href="#">Anhänge</a> </li> <li class="tabHeader" data-tabid="versionsTabView" data-tabindex="2"> <a href="#">Beschreibung</a> </li> </ul>
<!--
<div id="card-attachments">
<h3>Attachments</h3>