commit
d93c5f7924
12
VERSION
12
VERSION
@ -1 +1,11 @@
|
||||
v0.2.0
|
||||
v0.2.1
|
||||
|
||||
Changelog
|
||||
|
||||
* Supports nginx (@alex3305)
|
||||
* Supports use in Docker (Ubuntu Trusty & nginx - @alex3305)
|
||||
* Build scripts executable by default (@niedzielski)
|
||||
* Automatic Actions now allow "Unassigned" and "Uncategorized" for changing assignee or category (Issue #5)
|
||||
* Links in item descriptions open in new tab (Issue #18)
|
||||
* Lanes are now Columns to match common Kanban terminology (Issue #4)
|
||||
* Any exception in API is handled and returns a 503 (inspired by @amalfra)
|
||||
|
@ -15,6 +15,14 @@ $app->response->headers->set('Content-Type', 'application/json');
|
||||
$jsonResponse = new JsonResponse();
|
||||
require_once('helpers.php'); // Must come after $jsonResponse exists.
|
||||
|
||||
// Catch Exception if connection to DB failed
|
||||
function exceptionHandler($exception) {
|
||||
header('Content-Type: application/json');
|
||||
http_response_code(503);
|
||||
echo '{"message": "API Error."}';
|
||||
};
|
||||
set_exception_handler('exceptionHandler');
|
||||
|
||||
R::setup('sqlite:taskboard.db');
|
||||
createInitialUser();
|
||||
|
||||
|
@ -1,19 +1,20 @@
|
||||
<?php
|
||||
|
||||
// Patch for when using nginx instead of apache, source: http://php.net/manual/en/function.getallheaders.php#84262
|
||||
if (!function_exists('getallheaders')) {
|
||||
function getallheaders() {
|
||||
$headers = '';
|
||||
|
||||
foreach ($_SERVER as $name => $value) {
|
||||
if (substr($name, 0, 5) == 'HTTP_') {
|
||||
$headers[str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($name, 5)))))] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
return $headers;
|
||||
}
|
||||
}
|
||||
if (!function_exists('getallheaders')) {
|
||||
function getallheaders() {
|
||||
$headers = '';
|
||||
|
||||
foreach ($_SERVER as $name => $value) {
|
||||
if (substr($name, 0, 5) == 'HTTP_') {
|
||||
$headers[str_replace(' ', '-', ucwords(strtolower(
|
||||
str_replace('_', ' ', substr($name, 5))
|
||||
)))] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
return $headers;
|
||||
}
|
||||
}
|
||||
|
||||
// Log an action. If $itemId is set, it is an item action.
|
||||
function logAction($comment, $oldValue, $newValue, $itemId=null) {
|
||||
@ -344,12 +345,14 @@ function runAutoActions(&$item) {
|
||||
}
|
||||
break;
|
||||
case 1: // Item assigned to user
|
||||
if ($item->assignee == $action->secondaryId) {
|
||||
if ($item->assignee == $action->secondaryId ||
|
||||
($action->secondaryId == 0 && $item->assignee == null)) {
|
||||
updateItemFromAction($item, $action);
|
||||
}
|
||||
break;
|
||||
case 2: // Item assigned to category
|
||||
if ($item->category == $action->secondaryId) {
|
||||
if ($item->category == $action->secondaryId ||
|
||||
($action->secondaryId == 0 && $item->category == null)) {
|
||||
updateItemFromAction($item, $action);
|
||||
}
|
||||
break;
|
||||
|
@ -6,34 +6,29 @@ $app->post('/login', function() use ($app, $jsonResponse) {
|
||||
$expires = ($data->rememberme)
|
||||
? (2 * 7 * 24 * 60 * 60) /* Two weeks */
|
||||
: (1.5 * 60 * 60) /* One and a half hours */;
|
||||
try {
|
||||
$lookup = R::findOne('user', ' username = ? ', [$data->username]);
|
||||
|
||||
$jsonResponse->message = 'Invalid username or password.';
|
||||
$app->response->setStatus(401);
|
||||
$lookup = R::findOne('user', ' username = ? ', [$data->username]);
|
||||
|
||||
if (null != $lookup) {
|
||||
$hash = password_hash($data->password, PASSWORD_BCRYPT, array('salt' => $lookup->salt));
|
||||
if ($lookup->password == $hash) {
|
||||
if ($lookup->logins == 0 && $lookup->username == 'admin') {
|
||||
$jsonResponse->addAlert('warning', "This is your first login, don't forget to change your password.");
|
||||
$jsonResponse->addAlert('success', 'Go to Settings to add your first board.');
|
||||
}
|
||||
setUserToken($lookup, $expires);
|
||||
$lookup->logins = $lookup->logins + 1;
|
||||
$lookup->lastLogin = time();
|
||||
R::store($lookup);
|
||||
$jsonResponse->message = 'Invalid username or password.';
|
||||
$app->response->setStatus(401);
|
||||
|
||||
logAction($lookup->username . ' logged in.', null, null);
|
||||
$jsonResponse->message = 'Login successful.';
|
||||
$jsonResponse->data = $lookup->token;
|
||||
$app->response->setStatus(200);
|
||||
if (null != $lookup) {
|
||||
$hash = password_hash($data->password, PASSWORD_BCRYPT, array('salt' => $lookup->salt));
|
||||
if ($lookup->password == $hash) {
|
||||
if ($lookup->logins == 0 && $lookup->username == 'admin') {
|
||||
$jsonResponse->addAlert('warning', "This is your first login, don't forget to change your password.");
|
||||
$jsonResponse->addAlert('success', 'Go to Settings to add your first board.');
|
||||
}
|
||||
setUserToken($lookup, $expires);
|
||||
$lookup->logins = $lookup->logins + 1;
|
||||
$lookup->lastLogin = time();
|
||||
R::store($lookup);
|
||||
|
||||
logAction($lookup->username . ' logged in.', null, null);
|
||||
$jsonResponse->message = 'Login successful.';
|
||||
$jsonResponse->data = $lookup->token;
|
||||
$app->response->setStatus(200);
|
||||
}
|
||||
} catch (Exception $ex) {
|
||||
}
|
||||
if (!is_writable('taskboard.db')) {
|
||||
$jsonResponse->message = 'The api directory is not writable.';
|
||||
}
|
||||
$app->response->setBody($jsonResponse->asJson());
|
||||
});
|
||||
|
@ -28,10 +28,9 @@ function ($rootScope, $scope, $location, $window, UserService, AuthenticationSer
|
||||
$location.path('/boards');
|
||||
}).error(function(data, status) {
|
||||
$scope.isSaving = false;
|
||||
if (status === 401) {
|
||||
$scope.errors.push(data.message);
|
||||
} else {
|
||||
$scope.errors.push('Something went wrong. Please try again.');
|
||||
$scope.errors.push(data.message);
|
||||
if (status === 503) {
|
||||
$scope.errors[0] = $scope.errors[0] + ' Ensure api directory is writable.';
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@ -4,6 +4,10 @@ function ($scope, $interval, BoardService) {
|
||||
$scope.loadingActions = true;
|
||||
$scope.actions = [];
|
||||
|
||||
$scope.secondarySelection = [];
|
||||
$scope.boardCategories = [{ id: 0, name: 'Uncategorized' }];
|
||||
$scope.userList = [{ id: 0, name: 'Unassigned', username: 'Unassigned' }];
|
||||
|
||||
$scope.actionData = {
|
||||
isSaving: false,
|
||||
board: null,
|
||||
@ -17,58 +21,72 @@ function ($scope, $interval, BoardService) {
|
||||
};
|
||||
$scope.actionDeleting = [];
|
||||
|
||||
$scope.actionTypes = [
|
||||
{ id: 0, action: 'Set item color' },
|
||||
{ id: 1, action: 'Set item category'},
|
||||
{ id: 2, action: 'Set item assignee' },
|
||||
{ id: 3, action: 'Clear item due date' }
|
||||
];
|
||||
$scope.actionOptions = {
|
||||
triggers: [
|
||||
{
|
||||
id: 0,
|
||||
trigger: 'Item moves to lane',
|
||||
actions: [
|
||||
{ id: 0, action: 'Set item color' },
|
||||
{ id: 1, action: 'Set item category'},
|
||||
{ id: 2, action: 'Set item assignee' },
|
||||
{ id: 3, action: 'Clear item due date' }
|
||||
]
|
||||
trigger: 'Item moves to column'
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
trigger: 'Item assigned to user',
|
||||
actions: [
|
||||
{ id: 0, action: 'Set item color' },
|
||||
{ id: 1, action: 'Set item category'},
|
||||
{ id: 3, action: 'Clear item due date' }
|
||||
]
|
||||
trigger: 'Item assigned to user'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
trigger: 'Item set to category',
|
||||
actions: [
|
||||
{ id: 0, action: 'Set item color' },
|
||||
{ id: 2, action: 'Set item assignee' },
|
||||
{ id: 3, action: 'Clear item due date' }
|
||||
]
|
||||
trigger: 'Item set to category'
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
var getBoardData = function(boardId) {
|
||||
if (null === boardId || undefined === boardId)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var boardData;
|
||||
$scope.boards.forEach(function(board) {
|
||||
if (board.id === boardId) {
|
||||
boardData = board;
|
||||
if (null === boardId || undefined === boardId)
|
||||
{
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
return boardData;
|
||||
},
|
||||
var boardData;
|
||||
$scope.boards.forEach(function(board) {
|
||||
if (board.id === boardId) {
|
||||
boardData = board;
|
||||
}
|
||||
});
|
||||
|
||||
return boardData;
|
||||
},
|
||||
|
||||
getCategories = function(boardData) {
|
||||
var categories = [{ id: '0', name: 'Uncategorized' }];
|
||||
|
||||
if (boardData) {
|
||||
boardData.ownCategory.forEach(function(category) {
|
||||
categories.push(category);
|
||||
});
|
||||
}
|
||||
return categories;
|
||||
},
|
||||
|
||||
getUsers = function(boardData) {
|
||||
var userList = [{ id: '0', name: 'Unassigned', username: 'Unassigned' }];
|
||||
|
||||
if (boardData) {
|
||||
boardData.sharedUser.forEach(function(user) {
|
||||
userList.push({ id: user.id, name: user.username });
|
||||
});
|
||||
}
|
||||
return userList;
|
||||
},
|
||||
|
||||
getSecondaryText = function(action) {
|
||||
var text = ': ',
|
||||
actionBoard = getBoardData(action.board_id);
|
||||
actionBoard = getBoardData(action.board_id),
|
||||
boardCategories = getBoardData(actionBoard),
|
||||
userList = getUsers(actionBoard);
|
||||
|
||||
switch(parseInt(action.trigger_id)) {
|
||||
case 0: // Lane
|
||||
@ -79,14 +97,14 @@ function ($scope, $interval, BoardService) {
|
||||
});
|
||||
break;
|
||||
case 1: // User
|
||||
actionBoard.sharedUser.forEach(function(user) {
|
||||
userList.forEach(function(user) {
|
||||
if (user.id === action.secondary_id) {
|
||||
text += user.username;
|
||||
text += user.name;
|
||||
}
|
||||
});
|
||||
break;
|
||||
case 2: // Category
|
||||
actionBoard.ownCategory.forEach(function(category) {
|
||||
boardCategories.forEach(function(category) {
|
||||
if (category.id === action.secondary_id) {
|
||||
text += category.name;
|
||||
}
|
||||
@ -98,41 +116,44 @@ function ($scope, $interval, BoardService) {
|
||||
|
||||
getActionText = function(action) {
|
||||
var text = '',
|
||||
actionBoard = getBoardData(action.board_id);
|
||||
actionBoard = getBoardData(action.board_id),
|
||||
boardCategories = getCategories(actionBoard),
|
||||
userList = getUsers(actionBoard);
|
||||
|
||||
switch(parseInt(action.action_id)) {
|
||||
case 0: // Color
|
||||
text = ': ' + action.color;
|
||||
break;
|
||||
case 1: // Category
|
||||
actionBoard.ownCategory.forEach(function(category) {
|
||||
boardCategories.forEach(function(category) {
|
||||
if (category.id === action.category_id) {
|
||||
text = ': ' + category.name;
|
||||
}
|
||||
});
|
||||
break;
|
||||
case 2: // Assignee
|
||||
actionBoard.sharedUser.forEach(function(user) {
|
||||
userList.forEach(function(user) {
|
||||
if (user.id === action.assignee_id) {
|
||||
text = ': ' + user.username;
|
||||
text = ': ' + user.name;
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
return text;
|
||||
};
|
||||
},
|
||||
|
||||
$scope.updateAutoActions = function(actions) {
|
||||
updateAutoActions = function(actions) {
|
||||
if (!actions) {
|
||||
return;
|
||||
}
|
||||
var mappedActions = [];
|
||||
|
||||
var mappedActions = [];
|
||||
actions.forEach(function(action) {
|
||||
mappedActions.push({
|
||||
id: action.id,
|
||||
board: $scope.boardLookup[action.board_id],
|
||||
trigger: $scope.actionOptions.triggers[action.trigger_id].trigger + getSecondaryText(action),
|
||||
action: $scope.actionOptions.triggers[0].actions[action.action_id].action + getActionText(action)
|
||||
action: $scope.actionTypes[action.action_id].action + getActionText(action)
|
||||
});
|
||||
});
|
||||
|
||||
@ -142,8 +163,9 @@ function ($scope, $interval, BoardService) {
|
||||
$scope.loadActions = function() {
|
||||
BoardService.getAutoActions()
|
||||
.success(function(data) {
|
||||
$scope.updateAutoActions(data.data);
|
||||
updateAutoActions(data.data);
|
||||
$scope.loadingActions = false;
|
||||
$scope.loadActions();
|
||||
});
|
||||
};
|
||||
|
||||
@ -157,8 +179,12 @@ function ($scope, $interval, BoardService) {
|
||||
$scope.addAction = function() {
|
||||
if ($scope.actionData.secondary === null ||
|
||||
($scope.actionData.action !== 3 &&
|
||||
($scope.actionData.color === null && $scope.actionData.category === null && $scope.actionData.assignee === null))) {
|
||||
$scope.alerts.showAlert({ type: 'error', text: 'One or more required fields are not entered. Automatic Action not added.' });
|
||||
($scope.actionData.color === null && $scope.actionData.category === null &&
|
||||
$scope.actionData.assignee === null))) {
|
||||
$scope.alerts.showAlert({
|
||||
type: 'error',
|
||||
text: 'One or more required fields are not entered. Automatic Action not added.'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
@ -183,17 +209,14 @@ function ($scope, $interval, BoardService) {
|
||||
});
|
||||
};
|
||||
|
||||
$scope.secondarySelection = [];
|
||||
$scope.updateSecondary = function() {
|
||||
$scope.secondarySelection = [];
|
||||
$scope.actionData.secondary = null;
|
||||
$scope.actionData.action = 0;
|
||||
|
||||
var boardData = getBoardData($scope.actionData.board);
|
||||
if (boardData) {
|
||||
$scope.boardCategories = boardData.ownCategory;
|
||||
$scope.userList = boardData.sharedUser;
|
||||
}
|
||||
$scope.boardCategories = getCategories(boardData);
|
||||
$scope.userList = getUsers(boardData);
|
||||
|
||||
if (boardData) {
|
||||
switch($scope.actionData.trigger) {
|
||||
@ -201,13 +224,10 @@ function ($scope, $interval, BoardService) {
|
||||
$scope.secondarySelection = boardData.ownLane;
|
||||
break;
|
||||
case 1:
|
||||
$scope.secondarySelection = boardData.sharedUser;
|
||||
$scope.secondarySelection.forEach(function(user) {
|
||||
user.name = user.username;
|
||||
});
|
||||
$scope.secondarySelection = $scope.userList;
|
||||
break;
|
||||
case 2:
|
||||
$scope.secondarySelection = boardData.ownCategory;
|
||||
$scope.secondarySelection = $scope.boardCategories;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
2
lib/marked.min.js
vendored
2
lib/marked.min.js
vendored
File diff suppressed because one or more lines are too long
@ -39,16 +39,16 @@
|
||||
data-ng-class="{'collapsed': lane.collapsed}" data-lane-id="{{ lane.id }}"
|
||||
data-context-menu="onContextMenu(lane.id)" data-target="laneMenu">
|
||||
<h3>{{ lane.name }}
|
||||
<span class="badge" title="Lane Items" data-ng-if="lane.collapsed">
|
||||
<span class="badge" title="Column Items" data-ng-if="lane.collapsed">
|
||||
{{ lane.ownItem.length || 0 }}
|
||||
</span>
|
||||
<span class="fa fa-angle-double-down small shrink" title="Collapse Lane"
|
||||
<span class="fa fa-angle-double-down small shrink" title="Collapse Column"
|
||||
data-ng-click="toggleLane(lane)"></span>
|
||||
<span class="fa fa-angle-double-up small expand" title="Expand Lane"
|
||||
<span class="fa fa-angle-double-up small expand" title="Expand Column"
|
||||
data-ng-click="toggleLane(lane)"></span>
|
||||
</h3>
|
||||
<div class="itemContainer">
|
||||
<div><!-- Needed to fix sortable behavior when there are no items in a lane. --></div>
|
||||
<div><!-- Needed to fix sortable behavior when there are no items in a column. --></div>
|
||||
<div class="boardItem clearfix" data-ng-class="{'filtered': item.filtered}"
|
||||
data-ng-repeat="item in lane.ownItem | orderBy:'position':false"
|
||||
data-ng-dblclick="openItem(item)"
|
||||
|
@ -43,7 +43,7 @@
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group col-md-4">
|
||||
<h5>Lane</h5>
|
||||
<h5>Column</h5>
|
||||
<select class="form-control" data-ng-model="itemFormData.lane"
|
||||
data-ng-disabled="itemFormData.isSaving"
|
||||
data-ng-options="lane.id as lane.name for lane in currentBoard.ownLane | orderBy:'position':false">
|
||||
|
@ -25,7 +25,7 @@
|
||||
<p data-ng-if="viewItem.category">Category: <strong>{{ categories[viewItem.category] }}</strong></p>
|
||||
</div>
|
||||
<div class="col-md-4 text-center">
|
||||
<p>Current Lane: <strong>{{ viewItem.laneName }}</strong></p>
|
||||
<p>Current Column: <strong>{{ viewItem.laneName }}</strong></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="view-item-actions">
|
||||
|
@ -60,7 +60,7 @@
|
||||
<div class="form-group col-md-4">
|
||||
<label for="triggerSelect">Select Action:</label>
|
||||
<select class="form-control" id="triggerSelect" data-ng-model="actionData.action"
|
||||
data-ng-options="action.id as action.action for action in actionOptions.triggers[actionData.trigger].actions"
|
||||
data-ng-options="action.id as action.action for action in actionTypes"
|
||||
data-ng-disabled="null === actionData.board"
|
||||
data-ng-change="resetActionSecondary()">
|
||||
</select>
|
||||
@ -83,7 +83,7 @@
|
||||
<option value="">Select Category</option>
|
||||
</select>
|
||||
<select class="form-control" data-ng-model="actionData.assignee"
|
||||
data-ng-options="user.id as user.username for user in userList"
|
||||
data-ng-options="user.id as user.name for user in userList"
|
||||
data-ng-disabled="null === actionData.board"
|
||||
data-ng-if="actionData.action === 2">
|
||||
<option value="">Select Assignee</option>
|
||||
|
@ -17,7 +17,7 @@
|
||||
</div>
|
||||
<div class="form-group half-width" data-ng-class="{ 'has-error': boardFormData.lanesError }">
|
||||
<form role="form" class="form form-inline" data-ng-submit="boardFormData.addLane()">
|
||||
<h5>Lanes</h5>
|
||||
<h5>Columns</h5>
|
||||
<ul class="list-group lanes">
|
||||
<li class="list-group-item small" data-ng-class="{ disabled: boardFormData.isSaving }"
|
||||
data-ng-repeat="lane in boardFormData.lanes | orderBy:'position':false">
|
||||
@ -32,7 +32,7 @@
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
<input class="form-control" style="width: 85%;" type="text" placeholder="Lane Name"
|
||||
<input class="form-control" style="width: 85%;" type="text" placeholder="Column Name"
|
||||
data-ng-model="boardFormData.laneName" data-ng-disabled="boardFormData.isSaving">
|
||||
<button type="submit" id="modalAddLane" class="btn btn-default fa fa-plus"
|
||||
data-ng-disabled="boardFormData.isSaving"></button>
|
||||
|
@ -4,7 +4,7 @@
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th><th>Lanes</th><th>Categories</th><th>Users</th><th>Actions</th>
|
||||
<th>Name</th><th>Columns</th><th>Categories</th><th>Users</th><th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
@ -97,11 +97,11 @@ Count was done from parent directory of TaskBoard as `./cloc-1.62.pl TaskBoard -
|
||||
|
||||
Language | Files | Blank Lines | Comments | Code
|
||||
-------------------|-------:|-------------:|---------:|---------:
|
||||
Javascript | 22 | 181 | 34 | 1840
|
||||
Javascript | 22 | 187 | 34 | 1853
|
||||
HTML | 17 | 7 | 8 | 936
|
||||
PHP | 6 | 143 | 55 | 834
|
||||
PHP | 6 | 150 | 57 | 849
|
||||
CSS | 1 | 12 | 33 | 609
|
||||
Bourne Again Shell | 4 | 10 | 0 | 53
|
||||
__SUM:__ | __50__ | __353__ | __130__ | __4272__
|
||||
__SUM:__ | __50__ | __366__ | __132__ | __4300__
|
||||
|
||||
Counts Last Updated: Oct. 18, 2014
|
||||
|
Reference in New Issue
Block a user