Add screenshot support for tasks (copy/paste images directly)

This commit is contained in:
Frederic Guillot 2015-04-12 18:44:42 -04:00
parent 2a150dd3be
commit 3b403a1a4b
33 changed files with 419 additions and 29 deletions

View File

@ -80,6 +80,7 @@ Documentation
#### Working with tasks
- [Creating tasks](docs/creating-tasks.markdown)
- [Adding screenshots](docs/screenshots.markdown)
- [Task links](docs/task-links.markdown)
- [Transitions](docs/transitions.markdown)
- [Time tracking](docs/time-tracking.markdown)

View File

@ -335,4 +335,19 @@ class Board extends Base
$this->response->redirect($this->helper->url('board', 'show', array('project_id' => $values['project_id'])));
}
/**
* Screenshot popover
*
* @access public
*/
public function screenshot()
{
$task = $this->getTask();
$this->response->html($this->template->render('file/screenshot', array(
'task' => $task,
'redirect' => 'board',
)));
}
}

View File

@ -10,6 +10,32 @@ namespace Controller;
*/
class File extends Base
{
/**
* Screenshot
*
* @access public
*/
public function screenshot()
{
$task = $this->getTask();
if ($this->request->isPost() && $this->file->uploadScreenshot($task['project_id'], $task['id'], $this->request->getValue('screenshot'))) {
$this->session->flash(t('Screenshot uploaded successfully.'));
if ($this->request->getStringParam('redirect') === 'board') {
$this->response->redirect($this->helper->url('board', 'show', array('project_id' => $task['project_id'])));
}
$this->response->redirect($this->helper->url('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])));
}
$this->response->html($this->taskLayout('file/screenshot', array(
'task' => $task,
'redirect' => 'task',
)));
}
/**
* File upload form
*
@ -34,13 +60,11 @@ class File extends Base
{
$task = $this->getTask();
if ($this->file->upload($task['project_id'], $task['id'], 'files') === true) {
$this->response->redirect('?controller=task&action=show&task_id='.$task['id'].'&project_id='.$task['project_id'].'#attachments');
}
else {
if (! $this->file->upload($task['project_id'], $task['id'], 'files')) {
$this->session->flashError(t('Unable to upload the file.'));
$this->response->redirect('?controller=file&action=create&task_id='.$task['id'].'&project_id='.$task['project_id']);
}
$this->response->redirect($this->helper->url('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])));
}
/**
@ -59,7 +83,7 @@ class File extends Base
$this->response->binary(file_get_contents($filename));
}
$this->response->redirect('?controller=task&action=show&task_id='.$task['id'].'&project_id='.$task['project_id']);
$this->response->redirect($this->helper->url('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])));
}
/**
@ -140,7 +164,7 @@ class File extends Base
$this->session->flashError(t('Unable to remove this file.'));
}
$this->response->redirect('?controller=task&action=show&task_id='.$task['id'].'&project_id='.$task['project_id']);
$this->response->redirect($this->helper->url('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])));
}
/**

View File

@ -853,4 +853,10 @@ return array(
// 'Burndown chart for "%s"' => '',
// 'Burndown chart' => '',
// 'This chart show the task complexity over the time (Work Remaining).' => '',
// 'Screenshot taken %s' => '',
// 'Add a screenshot' => '',
// 'Copy and paste images are only supported with Mozilla Firefox and Google Chrome.' => '',
// 'Take a screenshot and press CTRL+V or ⌘+V to paste here.' => '',
// 'Screenshot uploaded successfully.' => '',
// 'SEK - Swedish Krona' => '',
);

View File

@ -853,4 +853,10 @@ return array(
// 'Burndown chart for "%s"' => '',
// 'Burndown chart' => '',
// 'This chart show the task complexity over the time (Work Remaining).' => '',
// 'Screenshot taken %s' => '',
// 'Add a screenshot' => '',
// 'Copy and paste images are only supported with Mozilla Firefox and Google Chrome.' => '',
// 'Take a screenshot and press CTRL+V or ⌘+V to paste here.' => '',
// 'Screenshot uploaded successfully.' => '',
// 'SEK - Swedish Krona' => '',
);

View File

@ -853,4 +853,10 @@ return array(
// 'Burndown chart for "%s"' => '',
// 'Burndown chart' => '',
// 'This chart show the task complexity over the time (Work Remaining).' => '',
// 'Screenshot taken %s' => '',
// 'Add a screenshot' => '',
// 'Copy and paste images are only supported with Mozilla Firefox and Google Chrome.' => '',
// 'Take a screenshot and press CTRL+V or ⌘+V to paste here.' => '',
// 'Screenshot uploaded successfully.' => '',
// 'SEK - Swedish Krona' => '',
);

View File

@ -853,4 +853,10 @@ return array(
// 'Burndown chart for "%s"' => '',
// 'Burndown chart' => '',
// 'This chart show the task complexity over the time (Work Remaining).' => '',
// 'Screenshot taken %s' => '',
// 'Add a screenshot' => '',
// 'Copy and paste images are only supported with Mozilla Firefox and Google Chrome.' => '',
// 'Take a screenshot and press CTRL+V or ⌘+V to paste here.' => '',
// 'Screenshot uploaded successfully.' => '',
// 'SEK - Swedish Krona' => '',
);

View File

@ -849,10 +849,16 @@ return array(
'Test your device' => 'Testez votre appareil',
'Assign a color when the task is moved to a specific column' => 'Assigner une couleur lorsque la tâche est déplacée dans une colonne spécifique',
'%s via Kanboard' => '%s via Kanboard',
'uploaded by: %s' => 'Télécharger par : %s',
'uploaded on: %s' => 'Télécharger le : %s',
'uploaded by: %s' => 'Téléchargé par %s',
'uploaded on: %s' => 'Téléchargé le %s',
'size: %s' => 'Taille : %s',
'Burndown chart for "%s"' => 'Graphique d\'avancement pour « %s »',
'Burndown chart' => 'Graphique d\'avancement',
'This chart show the task complexity over the time (Work Remaining).' => 'Ce graphique représente la complexité des tâches en fonction du temps (travail restant).',
'Screenshot taken %s' => 'Capture d\'écran prise le %s',
'Add a screenshot' => 'Ajouter une capture d\'écran',
'Copy and paste images are only supported with Mozilla Firefox and Google Chrome.' => 'Copier/coller des images est uniquement supporté par Mozilla Firefox et Google Chrome.',
'Take a screenshot and press CTRL+V or ⌘+V to paste here.' => 'Prenez une capture d\'écran et appuyez sur CTRL+V ou ⌘+V pour coller ici.',
'Screenshot uploaded successfully.' => 'Capture d\'écran téléchargée avec succès.',
'SEK - Swedish Krona' => 'SEK - Couronne suédoise',
);

View File

@ -853,4 +853,10 @@ return array(
// 'Burndown chart for "%s"' => '',
// 'Burndown chart' => '',
// 'This chart show the task complexity over the time (Work Remaining).' => '',
// 'Screenshot taken %s' => '',
// 'Add a screenshot' => '',
// 'Copy and paste images are only supported with Mozilla Firefox and Google Chrome.' => '',
// 'Take a screenshot and press CTRL+V or ⌘+V to paste here.' => '',
// 'Screenshot uploaded successfully.' => '',
// 'SEK - Swedish Krona' => '',
);

View File

@ -853,4 +853,10 @@ return array(
// 'Burndown chart for "%s"' => '',
// 'Burndown chart' => '',
// 'This chart show the task complexity over the time (Work Remaining).' => '',
// 'Screenshot taken %s' => '',
// 'Add a screenshot' => '',
// 'Copy and paste images are only supported with Mozilla Firefox and Google Chrome.' => '',
// 'Take a screenshot and press CTRL+V or ⌘+V to paste here.' => '',
// 'Screenshot uploaded successfully.' => '',
// 'SEK - Swedish Krona' => '',
);

View File

@ -853,4 +853,10 @@ return array(
// 'Burndown chart for "%s"' => '',
// 'Burndown chart' => '',
// 'This chart show the task complexity over the time (Work Remaining).' => '',
// 'Screenshot taken %s' => '',
// 'Add a screenshot' => '',
// 'Copy and paste images are only supported with Mozilla Firefox and Google Chrome.' => '',
// 'Take a screenshot and press CTRL+V or ⌘+V to paste here.' => '',
// 'Screenshot uploaded successfully.' => '',
// 'SEK - Swedish Krona' => '',
);

View File

@ -853,4 +853,10 @@ return array(
// 'Burndown chart for "%s"' => '',
// 'Burndown chart' => '',
// 'This chart show the task complexity over the time (Work Remaining).' => '',
// 'Screenshot taken %s' => '',
// 'Add a screenshot' => '',
// 'Copy and paste images are only supported with Mozilla Firefox and Google Chrome.' => '',
// 'Take a screenshot and press CTRL+V or ⌘+V to paste here.' => '',
// 'Screenshot uploaded successfully.' => '',
// 'SEK - Swedish Krona' => '',
);

View File

@ -853,4 +853,10 @@ return array(
// 'Burndown chart for "%s"' => '',
// 'Burndown chart' => '',
// 'This chart show the task complexity over the time (Work Remaining).' => '',
// 'Screenshot taken %s' => '',
// 'Add a screenshot' => '',
// 'Copy and paste images are only supported with Mozilla Firefox and Google Chrome.' => '',
// 'Take a screenshot and press CTRL+V or ⌘+V to paste here.' => '',
// 'Screenshot uploaded successfully.' => '',
// 'SEK - Swedish Krona' => '',
);

View File

@ -853,4 +853,10 @@ return array(
// 'Burndown chart for "%s"' => '',
// 'Burndown chart' => '',
// 'This chart show the task complexity over the time (Work Remaining).' => '',
// 'Screenshot taken %s' => '',
// 'Add a screenshot' => '',
// 'Copy and paste images are only supported with Mozilla Firefox and Google Chrome.' => '',
// 'Take a screenshot and press CTRL+V or ⌘+V to paste here.' => '',
// 'Screenshot uploaded successfully.' => '',
// 'SEK - Swedish Krona' => '',
);

View File

@ -853,4 +853,10 @@ return array(
// 'Burndown chart for "%s"' => '',
// 'Burndown chart' => '',
// 'This chart show the task complexity over the time (Work Remaining).' => '',
// 'Screenshot taken %s' => '',
// 'Add a screenshot' => '',
// 'Copy and paste images are only supported with Mozilla Firefox and Google Chrome.' => '',
// 'Take a screenshot and press CTRL+V or ⌘+V to paste here.' => '',
// 'Screenshot uploaded successfully.' => '',
// 'SEK - Swedish Krona' => '',
);

View File

@ -853,4 +853,10 @@ return array(
// 'Burndown chart for "%s"' => '',
// 'Burndown chart' => '',
// 'This chart show the task complexity over the time (Work Remaining).' => '',
// 'Screenshot taken %s' => '',
// 'Add a screenshot' => '',
// 'Copy and paste images are only supported with Mozilla Firefox and Google Chrome.' => '',
// 'Take a screenshot and press CTRL+V or ⌘+V to paste here.' => '',
// 'Screenshot uploaded successfully.' => '',
// 'SEK - Swedish Krona' => '',
);

View File

@ -802,7 +802,6 @@ return array(
'The budget line have been created successfully.' => 'Budgetlinjen har skapats.',
'Unable to create the budget line.' => 'Kunde inte skapa budgetlinjen.',
'Unable to remove this budget line.' => 'Kunde inte ta bort budgetlinjen.',
'SEK - Swedish Krona' => 'SEK - Svensk Krona',
'USD - US Dollar' => 'USD - Amerikanska Dollar',
'Remaining' => 'Återstående',
'Destination column' => 'Målkolumn',
@ -854,4 +853,10 @@ return array(
'Burndown chart for "%s"' => 'Burndown diagram för "%s"',
'Burndown chart' => 'Burndown diagram',
'This chart show the task complexity over the time (Work Remaining).' => 'Diagrammet visar uppgiftens svårighet över tid (återstående arbete).',
// 'Screenshot taken %s' => '',
// 'Add a screenshot' => '',
// 'Copy and paste images are only supported with Mozilla Firefox and Google Chrome.' => '',
// 'Take a screenshot and press CTRL+V or ⌘+V to paste here.' => '',
// 'Screenshot uploaded successfully.' => '',
'SEK - Swedish Krona' => 'SEK - Svensk Krona',
);

View File

@ -853,4 +853,10 @@ return array(
// 'Burndown chart for "%s"' => '',
// 'Burndown chart' => '',
// 'This chart show the task complexity over the time (Work Remaining).' => '',
// 'Screenshot taken %s' => '',
// 'Add a screenshot' => '',
// 'Copy and paste images are only supported with Mozilla Firefox and Google Chrome.' => '',
// 'Take a screenshot and press CTRL+V or ⌘+V to paste here.' => '',
// 'Screenshot uploaded successfully.' => '',
// 'SEK - Swedish Krona' => '',
);

View File

@ -853,4 +853,10 @@ return array(
// 'Burndown chart for "%s"' => '',
// 'Burndown chart' => '',
// 'This chart show the task complexity over the time (Work Remaining).' => '',
// 'Screenshot taken %s' => '',
// 'Add a screenshot' => '',
// 'Copy and paste images are only supported with Mozilla Firefox and Google Chrome.' => '',
// 'Take a screenshot and press CTRL+V or ⌘+V to paste here.' => '',
// 'Screenshot uploaded successfully.' => '',
// 'SEK - Swedish Krona' => '',
);

View File

@ -853,4 +853,10 @@ return array(
// 'Burndown chart for "%s"' => '',
// 'Burndown chart' => '',
// 'This chart show the task complexity over the time (Work Remaining).' => '',
// 'Screenshot taken %s' => '',
// 'Add a screenshot' => '',
// 'Copy and paste images are only supported with Mozilla Firefox and Google Chrome.' => '',
// 'Take a screenshot and press CTRL+V or ⌘+V to paste here.' => '',
// 'Screenshot uploaded successfully.' => '',
// 'SEK - Swedish Krona' => '',
);

View File

@ -40,6 +40,7 @@ class Config extends Base
'INR' => t('INR - Indian Rupee'),
'JPY' => t('JPY - Japanese Yen'),
'RSD' => t('RSD - Serbian dinar'),
'SEK' => t('SEK - Swedish Krona'),
);
}

View File

@ -248,9 +248,9 @@ class File extends Base
* Handle file upload
*
* @access public
* @param integer $project_id Project id
* @param integer $task_id Task id
* @param string $form_name File form name
* @param integer $project_id Project id
* @param integer $task_id Task id
* @param string $form_name File form name
* @return bool
*/
public function upload($project_id, $task_id, $form_name)
@ -287,6 +287,38 @@ class File extends Base
return count(array_unique($result)) === 1;
}
/**
* Handle screenshot upload
*
* @access public
* @param integer $project_id Project id
* @param integer $task_id Task id
* @param string $blob Base64 encoded image
* @return bool
*/
public function uploadScreenshot($project_id, $task_id, $blob)
{
$data = base64_decode($blob);
if (empty($data)) {
return false;
}
$original_filename = e('Screenshot taken %s', dt('%B %e, %Y at %k:%M %p', time()));
$destination_filename = $this->generatePath($project_id, $task_id, $original_filename);
@mkdir(FILES_DIR.dirname($destination_filename), 0755, true);
@file_put_contents(FILES_DIR.$destination_filename, $data);
return $this->create(
$task_id,
$original_filename,
$destination_filename,
true,
strlen($data)
);
}
/**
* Generate a jpeg thumbnail from an image (output directly the image)
*

View File

@ -8,6 +8,7 @@
<li><i class="fa fa-comment-o"></i> <?= $this->a(t('Add a comment'), 'comment', 'create', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'task-board-popover') ?></li>
<li><i class="fa fa-code-fork"></i> <?= $this->a(t('Add a link'), 'tasklink', 'create', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'task-board-popover') ?></li>
<li><i class="fa fa-pencil-square-o"></i> <?= $this->a(t('Edit this task'), 'task', 'edit', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'task-board-popover') ?></li>
<li><i class="fa fa-camera"></i> <?= $this->a(t('Add a screenshot'), 'board', 'screenshot', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'task-board-popover') ?></li>
<li><i class="fa fa-close"></i> <?= $this->a(t('Close this task'), 'task', 'close', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'redirect' => 'board'), false, 'task-board-popover') ?></li>
</ul>
</span>

View File

@ -0,0 +1,19 @@
<div class="page-header">
<h2><?= t('Add a screenshot') ?></h2>
</div>
<div id="screenshot-zone">
<p id="screenshot-inner"><?= t('Take a screenshot and press CTRL+V or ⌘+V to paste here.') ?></p>
</div>
<form action="<?= $this->u('file', 'screenshot', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'redirect' => $redirect)) ?>" method="post">
<input type="hidden" name="screenshot"/>
<?= $this->formCsrf() ?>
<div class="form-actions">
<input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/>
<?= t('or') ?>
<?= $this->a(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'close-popover') ?>
</div>
</form>
<p class="alert alert-info"><?= t('Copy and paste images are only supported with Mozilla Firefox and Google Chrome.') ?></p>

View File

@ -36,6 +36,9 @@
<li>
<?= $this->a(t('Attach a document'), 'file', 'create', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>
</li>
<li>
<?= $this->a(t('Add a screenshot'), 'file', 'screenshot', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>
</li>
<li>
<?= $this->a(t('Duplicate'), 'task', 'duplicate', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>
</li>

View File

@ -769,7 +769,7 @@ nav .active a {
/* board table */
#board-container {
padding-bottom: 180px; /* Space to avoid dropdown menu truncated */
padding-bottom: 200px; /* Space to avoid dropdown menu truncated */
overflow-x: scroll;
}
@ -1083,17 +1083,21 @@ span.task-board-date-overdue {
.task-link-closed {
text-decoration: line-through;
}
.task-show-images {
list-style-type: none;
}
.task-show-images li img {
width: 100%;
}
.task-show-images li .img_container {
width: 250px;
height: 100px;
overflow: hidden;
}
.task-show-images li {
padding: 10px;
overflow: auto;
@ -1102,20 +1106,44 @@ span.task-board-date-overdue {
display: inline-block;
vertical-align: top;
}
.task-show-images li p{
padding: 5px;
font-weight: bold;
}
.task-show-images li:hover {
background: #eee;
}
.task-show-image-actions {
margin-left: 5px;
}
.task-show-file-table {
width: auto;
}
/* comments */
/* screenshots */
#screenshot-zone {
position: relative;
border: 2px dashed #ccc;
width: 90%;
height: 250px;
overflow: auto;
}
#screenshot-inner {
position: absolute;
left: 0;
bottom: 48%;
width: 100%;
text-align: center;
}
#screenshot-zone.screenshot-pasted {
border: 2px solid #333;
}/* comments */
.comment {
margin-bottom: 20px;
}

View File

@ -20,7 +20,7 @@
/* board table */
#board-container {
padding-bottom: 180px; /* Space to avoid dropdown menu truncated */
padding-bottom: 200px; /* Space to avoid dropdown menu truncated */
overflow-x: scroll;
}

View File

@ -243,17 +243,21 @@ span.task-board-date-overdue {
.task-link-closed {
text-decoration: line-through;
}
.task-show-images {
list-style-type: none;
}
.task-show-images li img {
width: 100%;
}
.task-show-images li .img_container {
width: 250px;
height: 100px;
overflow: hidden;
}
.task-show-images li {
padding: 10px;
overflow: auto;
@ -262,16 +266,41 @@ span.task-board-date-overdue {
display: inline-block;
vertical-align: top;
}
.task-show-images li p{
padding: 5px;
font-weight: bold;
}
.task-show-images li:hover {
background: #eee;
}
.task-show-image-actions {
margin-left: 5px;
}
.task-show-file-table {
width: auto;
}
/* screenshots */
#screenshot-zone {
position: relative;
border: 2px dashed #ccc;
width: 90%;
height: 250px;
overflow: auto;
}
#screenshot-inner {
position: absolute;
left: 0;
bottom: 48%;
width: 100%;
text-align: center;
}
#screenshot-zone.screenshot-pasted {
border: 2px solid #333;
}

File diff suppressed because one or more lines are too long

View File

@ -263,6 +263,11 @@ var Kanboard = (function() {
}
}
});
// Screenshot
if (Kanboard.Exists("screenshot-zone")) {
Kanboard.Screenshot.Init();
}
}
};

102
assets/js/src/screenshot.js Normal file
View File

@ -0,0 +1,102 @@
Kanboard.Screenshot = (function() {
var pasteCatcher = null;
// Setup event listener and workarounds
function init()
{
if (! window.Clipboard) {
// Create a contenteditable element
pasteCatcher = document.createElement("div");
pasteCatcher.setAttribute("contenteditable", "");
pasteCatcher.style.opacity = 0;
document.body.appendChild(pasteCatcher);
// Make sure it is always in focus
pasteCatcher.focus();
document.addEventListener("click", function() { pasteCatcher.focus(); });
}
window.addEventListener("paste", pasteHandler);
}
// Paste event callback
function pasteHandler(e)
{
// Firefox doesn't have the property e.clipboardData.items (only Chrome)
if (e.clipboardData && e.clipboardData.items) {
var items = e.clipboardData.items;
if (items) {
for (var i = 0; i < items.length; i++) {
// Find an image in pasted elements
if (items[i].type.indexOf("image") !== -1) {
var blob = items[i].getAsFile();
// Get the image as base64 data
var reader = new FileReader();
reader.onload = function(event) {
createImage(event.target.result);
};
reader.readAsDataURL(blob);
}
}
}
}
else {
// Handle Firefox
setTimeout(checkInput, 100);
}
}
// Parse the input in the paste catcher element
function checkInput()
{
var child = pasteCatcher.childNodes[0];
pasteCatcher.innerHTML = "";
if (child) {
// If the user pastes an image, the src attribute
// will represent the image as a base64 encoded string.
if (child.tagName === "IMG") {
createImage(child.src);
}
}
}
// Creates a new image from a given source
function createImage(blob)
{
var pastedImage = new Image();
pastedImage.src = blob;
// Send the image content to the form variable
pastedImage.onload = function() {
var sourceSplit = blob.split("base64,");
var sourceString = sourceSplit[1];
$("input[name=screenshot]").val(sourceString);
};
document.getElementById("screenshot-inner").style.display = "none";
document.getElementById("screenshot-zone").className = "screenshot-pasted";
document.getElementById("screenshot-zone").appendChild(pastedImage);
}
jQuery(document).ready(function() {
if (Kanboard.Exists("screenshot-zone")) {
init();
}
});
return {
Init: init
};
})();

26
docs/screenshots.markdown Normal file
View File

@ -0,0 +1,26 @@
Adding screenshots
==================
You can copy and paste images directly in Kanboard to save time.
These images are uploaded as attachments to the task.
This is especially useful for taking screenshots to describe an issue by example.
You can add screenshots directly from the board by clicking on the dropdown menu or in the task view page.
![Dropdown screenshot menu](http://kanboard.net/screenshots/documentation/dropdown-screenshot.png)
To add a new image, take your screenshot and paste with CTRL+V or Command+V:
![Screenshot page](http://kanboard.net/screenshots/documentation/task-screenshot.png)
On Mac OS X, you can use those shortcuts to take screenshots:
- Command-Control-Shift-3: Take a screenshot of the screen, and save it to the clipboard
- Command-Control-Shift-4, then select an area: Take a screenshot of an area and save it to the clipboard
- Command-Control-Shift-4, then space, then click a window: Take a screenshot of a window and save it to the clipboard
There are also several third-party applications that can be used to take screenshots with annotations and shapes.
Note: this feature **works only with Mozilla Firefox and Google Chrome**.
If you use another browser, you have to upload an attachment manually.

View File

@ -3,9 +3,9 @@
app_css="base links title table form button alert tooltip header board task comment subtask markdown listing activity dashboard pagination popover confirm sidebar responsive dropdown"
vendor_css="jquery-ui.min chosen.min fullcalendar.min font-awesome.min"
app_js="base board calendar analytic swimlane dashboard budget"
app_js="base board calendar analytic swimlane dashboard budget screenshot"
vendor_js="jquery-1.11.1.min jquery-ui.min jquery.ui.touch-punch.min chosen.jquery.min dropit.min moment.min fullcalendar.min mousetrap.min mousetrap-global-bind.min app.min"
lang_js="da de es fi fr hu it ja pl pt-br ru sv th zh-cn tr"
lang_js="da de es fi fr hu it ja nl pl pt-br ru sv th tr zh-cn"
function merge_css {