diff options
-rw-r--r-- | .gitignore | 1 | ||||
-rw-r--r-- | app/Controller/Base.php | 1 | ||||
-rw-r--r-- | app/Controller/Task.php | 178 | ||||
-rw-r--r-- | app/Core/Response.php | 5 | ||||
-rw-r--r-- | app/Locales/es_ES/translations.php | 7 | ||||
-rw-r--r-- | app/Locales/fr_FR/translations.php | 7 | ||||
-rw-r--r-- | app/Locales/pl_PL/translations.php | 7 | ||||
-rw-r--r-- | app/Locales/pt_BR/translations.php | 7 | ||||
-rw-r--r-- | app/Model/Acl.php | 23 | ||||
-rw-r--r-- | app/Model/File.php | 176 | ||||
-rw-r--r-- | app/Model/Task.php | 1 | ||||
-rw-r--r-- | app/Schema/Mysql.php | 17 | ||||
-rw-r--r-- | app/Schema/Sqlite.php | 16 | ||||
-rw-r--r-- | app/Templates/board_show.php | 6 | ||||
-rw-r--r-- | app/Templates/task_close.php | 4 | ||||
-rw-r--r-- | app/Templates/task_layout.php | 3 | ||||
-rw-r--r-- | app/Templates/task_open.php | 24 | ||||
-rw-r--r-- | app/Templates/task_open_file.php | 6 | ||||
-rw-r--r-- | app/Templates/task_remove.php | 4 | ||||
-rw-r--r-- | app/Templates/task_remove_file.php | 14 | ||||
-rw-r--r-- | app/Templates/task_show.php | 17 | ||||
-rw-r--r-- | app/Templates/task_sidebar.php | 4 | ||||
-rw-r--r-- | app/Templates/task_upload.php | 10 | ||||
-rw-r--r-- | assets/css/app.css | 37 | ||||
-rw-r--r-- | assets/js/task.js | 27 |
25 files changed, 549 insertions, 53 deletions
@@ -52,3 +52,4 @@ Thumbs.db # App specific # ################ config.php +data/files
\ No newline at end of file diff --git a/app/Controller/Base.php b/app/Controller/Base.php index bb9add4f..183b9395 100644 --- a/app/Controller/Base.php +++ b/app/Controller/Base.php @@ -216,6 +216,7 @@ abstract class Base 'task' => $task, 'columns_list' => $this->board->getColumnsList($task['project_id']), 'colors_list' => $this->task->getColors(), + 'files' => $this->file->getAll($task['id']), 'menu' => 'tasks', 'title' => $task['title'], ))); diff --git a/app/Controller/Task.php b/app/Controller/Task.php index 2291ad43..1b67b6a0 100644 --- a/app/Controller/Task.php +++ b/app/Controller/Task.php @@ -3,6 +3,7 @@ namespace Controller; use Model\Project; +use Model\File; /** * Task controller @@ -12,6 +13,19 @@ use Model\Project; */ class Task extends Base { + private function getTask() + { + $task = $this->task->getById($this->request->getIntegerParam('task_id'), true); + + if (! $task) { + $this->notfound(); + } + + $this->checkProjectPermissions($task['project_id']); + + return $task; + } + /** * Webhook to create a task (useful for external software) * @@ -57,12 +71,7 @@ class Task extends Base */ public function show() { - $task = $this->task->getById($this->request->getIntegerParam('task_id'), true); - - if (! $task) $this->notfound(); - $this->checkProjectPermissions($task['project_id']); - - $this->showTask($task); + $this->showTask($this->getTask()); } /** @@ -247,10 +256,7 @@ class Task extends Base */ public function close() { - $task = $this->task->getById($this->request->getIntegerParam('task_id')); - - if (! $task) $this->notfound(); - $this->checkProjectPermissions($task['project_id']); + $task = $this->getTask(); if ($this->task->close($task['id'])) { $this->session->flash(t('Task closed successfully.')); @@ -268,10 +274,7 @@ class Task extends Base */ public function confirmClose() { - $task = $this->task->getById($this->request->getIntegerParam('task_id'), true); - - if (! $task) $this->notfound(); - $this->checkProjectPermissions($task['project_id']); + $task = $this->getTask(); $this->response->html($this->taskLayout('task_close', array( 'task' => $task, @@ -287,10 +290,7 @@ class Task extends Base */ public function open() { - $task = $this->task->getById($this->request->getIntegerParam('task_id')); - - if (! $task) $this->notfound(); - $this->checkProjectPermissions($task['project_id']); + $task = $this->getTask(); if ($this->task->open($task['id'])) { $this->session->flash(t('Task opened successfully.')); @@ -308,10 +308,7 @@ class Task extends Base */ public function confirmOpen() { - $task = $this->task->getById($this->request->getIntegerParam('task_id'), true); - - if (! $task) $this->notfound(); - $this->checkProjectPermissions($task['project_id']); + $task = $this->getTask(); $this->response->html($this->taskLayout('task_open', array( 'task' => $task, @@ -327,10 +324,7 @@ class Task extends Base */ public function remove() { - $task = $this->task->getById($this->request->getIntegerParam('task_id')); - - if (! $task) $this->notfound(); - $this->checkProjectPermissions($task['project_id']); + $task = $this->getTask(); if ($this->task->remove($task['id'])) { $this->session->flash(t('Task removed successfully.')); @@ -348,10 +342,7 @@ class Task extends Base */ public function confirmRemove() { - $task = $this->task->getById($this->request->getIntegerParam('task_id'), true); - - if (! $task) $this->notfound(); - $this->checkProjectPermissions($task['project_id']); + $task = $this->getTask(); $this->response->html($this->taskLayout('task_remove', array( 'task' => $task, @@ -367,10 +358,7 @@ class Task extends Base */ public function duplicate() { - $task = $this->task->getById($this->request->getIntegerParam('task_id')); - - if (! $task) $this->notfound(); - $this->checkProjectPermissions($task['project_id']); + $task = $this->getTask(); if (! empty($task['date_due'])) { $task['date_due'] = date(t('m/d/Y'), $task['date_due']); @@ -394,4 +382,126 @@ class Task extends Base 'title' => t('New task') ))); } + + /** + * File upload form + * + * @access public + */ + public function file() + { + $task = $this->getTask(); + + $this->response->html($this->taskLayout('task_upload', array( + 'task' => $task, + 'menu' => 'tasks', + 'title' => t('Attach a document') + ))); + } + + /** + * File upload (save files) + * + * @access public + */ + public function upload() + { + $task = $this->getTask(); + $this->file->upload($task['project_id'], $task['id'], 'files'); + $this->response->redirect('?controller=task&action=show&task_id='.$task['id'].'#attachments'); + } + + /** + * File download + * + * @access public + */ + public function download() + { + $task = $this->getTask(); + $file = $this->file->getById($this->request->getIntegerParam('file_id')); + $filename = File::BASE_PATH.$file['path']; + + if ($file['task_id'] == $task['id'] && file_exists($filename)) { + $this->response->forceDownload($file['name']); + $this->response->binary(file_get_contents($filename)); + } + + $this->response->redirect('?controller=task&action=show&task_id='.$task['id']); + } + + /** + * Open a file (show the content in a popover) + * + * @access public + */ + public function openFile() + { + $task = $this->getTask(); + $file = $this->file->getById($this->request->getIntegerParam('file_id')); + + if ($file['task_id'] == $task['id']) { + $this->response->html($this->template->load('task_open_file', array( + 'file' => $file + ))); + } + } + + /** + * Return the file content (work only for images) + * + * @access public + */ + public function image() + { + $task = $this->getTask(); + $file = $this->file->getById($this->request->getIntegerParam('file_id')); + $filename = File::BASE_PATH.$file['path']; + + if ($file['task_id'] == $task['id'] && file_exists($filename)) { + $metadata = getimagesize($filename); + + if (isset($metadata['mime'])) { + $this->response->contentType($metadata['mime']); + readfile($filename); + } + } + } + + /** + * Remove a file + * + * @access public + */ + public function removeFile() + { + $task = $this->getTask(); + $file = $this->file->getById($this->request->getIntegerParam('file_id')); + + if ($file['task_id'] == $task['id'] && $this->file->remove($file['id'])) { + $this->session->flash(t('File removed successfully.')); + } else { + $this->session->flashError(t('Unable to remove this file.')); + } + + $this->response->redirect('?controller=task&action=show&task_id='.$task['id']); + } + + /** + * Confirmation dialog before removing a file + * + * @access public + */ + public function confirmRemoveFile() + { + $task = $this->getTask(); + $file = $this->file->getById($this->request->getIntegerParam('file_id')); + + $this->response->html($this->taskLayout('task_remove_file', array( + 'task' => $task, + 'file' => $file, + 'menu' => 'tasks', + 'title' => t('Remove a file') + ))); + } } diff --git a/app/Core/Response.php b/app/Core/Response.php index a5f0e4dc..ee98c9ed 100644 --- a/app/Core/Response.php +++ b/app/Core/Response.php @@ -4,6 +4,11 @@ namespace Core; class Response { + public function contentType($mimetype) + { + header('Content-Type: '.$mimetype); + } + public function forceDownload($filename) { header('Content-Disposition: attachment; filename="'.$filename.'"'); diff --git a/app/Locales/es_ES/translations.php b/app/Locales/es_ES/translations.php index ce797972..7374f6b6 100644 --- a/app/Locales/es_ES/translations.php +++ b/app/Locales/es_ES/translations.php @@ -331,4 +331,11 @@ return array( // 'All categories' => '', // 'No category' => '', // 'The name is required' => '', + // 'Remove a file' => '', + // 'Unable to remove this file.' => '', + // 'File removed successfully.' => '', + // 'Attach a document' => '', + // 'Do you really want to remove this file: "%s"?' => '', + // 'open' => '', + // 'Attachments' => '', ); diff --git a/app/Locales/fr_FR/translations.php b/app/Locales/fr_FR/translations.php index c93a83ae..26fee468 100644 --- a/app/Locales/fr_FR/translations.php +++ b/app/Locales/fr_FR/translations.php @@ -331,4 +331,11 @@ return array( 'All categories' => 'Toutes les catégories', 'No category' => 'Aucune catégorie', 'The name is required' => 'Le nom est requis', + 'Remove a file' => 'Supprimer un fichier', + 'Unable to remove this file.' => 'Impossible de supprimer ce fichier.', + 'File removed successfully.' => 'Fichier supprimé avec succès.', + 'Attach a document' => 'Joindre un document', + 'Do you really want to remove this file: "%s"?' => 'Voulez-vous vraiment supprimer ce fichier « %s » ?', + 'open' => 'ouvrir', + 'Attachments' => 'Pièces-jointes', ); diff --git a/app/Locales/pl_PL/translations.php b/app/Locales/pl_PL/translations.php index 81ecaf01..43adb330 100644 --- a/app/Locales/pl_PL/translations.php +++ b/app/Locales/pl_PL/translations.php @@ -336,4 +336,11 @@ return array( // 'All categories' => '', // 'No category' => '', // 'The name is required' => '', + // 'Remove a file' => '', + // 'Unable to remove this file.' => '', + // 'File removed successfully.' => '', + // 'Attach a document' => '', + // 'Do you really want to remove this file: "%s"?' => '', + // 'open' => '', + // 'Attachments' => '', ); diff --git a/app/Locales/pt_BR/translations.php b/app/Locales/pt_BR/translations.php index 7c9a6c17..0b4765d1 100644 --- a/app/Locales/pt_BR/translations.php +++ b/app/Locales/pt_BR/translations.php @@ -332,4 +332,11 @@ return array( // 'All categories' => '', // 'No category' => '', // 'The name is required' => '', + // 'Remove a file' => '', + // 'Unable to remove this file.' => '', + // 'File removed successfully.' => '', + // 'Attach a document' => '', + // 'Do you really want to remove this file: "%s"?' => '', + // 'open' => '', + // 'Attachments' => '', ); diff --git a/app/Model/Acl.php b/app/Model/Acl.php index ad2118f4..be32196a 100644 --- a/app/Model/Acl.php +++ b/app/Model/Acl.php @@ -32,10 +32,31 @@ class Acl extends Base 'app' => array('index'), 'board' => array('index', 'show', 'assign', 'assigntask', 'save', 'check'), 'project' => array('tasks', 'index', 'forbidden', 'search'), - 'task' => array('show', 'create', 'save', 'edit', 'update', 'close', 'confirmclose', 'open', 'confirmopen', 'description', 'duplicate', 'remove', 'confirmremove'), 'comment' => array('save', 'confirm', 'remove', 'update', 'edit'), 'user' => array('index', 'edit', 'update', 'forbidden', 'logout', 'index', 'unlinkgoogle'), 'config' => array('index', 'removeremembermetoken'), + 'task' => array( + 'show', + 'create', + 'save', + 'edit', + 'update', + 'close', + 'confirmclose', + 'open', + 'confirmopen', + 'description', + 'duplicate', + 'remove', + 'confirmremove', + 'file', + 'upload', + 'download', + 'openfile', + 'image', + 'removefile', + 'confirmremovefile', + ), ); /** diff --git a/app/Model/File.php b/app/Model/File.php new file mode 100644 index 00000000..1e2e1432 --- /dev/null +++ b/app/Model/File.php @@ -0,0 +1,176 @@ +<?php + +namespace Model; + +use SimpleValidator\Validator; +use SimpleValidator\Validators; + +/** + * File model + * + * @package model + * @author Frederic Guillot + */ +class File extends Base +{ + /** + * SQL table name + * + * @var string + */ + const TABLE = 'task_has_files'; + + /** + * Directory where are stored files + * + * @var string + */ + const BASE_PATH = 'data/files/'; + + /** + * Get a file by the id + * + * @access public + * @param integer $file_id File id + * @return array + */ + public function getById($file_id) + { + return $this->db->table(self::TABLE)->eq('id', $file_id)->findOne(); + } + + /** + * Remove a file + * + * @access public + * @param integer $file_id File id + * @return bool + */ + public function remove($file_id) + { + $file = $this->getbyId($file_id); + + if (! empty($file) && @unlink(self::BASE_PATH.$file['path'])) { + return $this->db->table(self::TABLE)->eq('id', $file_id)->remove(); + } + + return false; + } + + /** + * Create a file entry in the database + * + * @access public + * @param integer $task_id Task id + * @param string $name Filename + * @param string $path Path on the disk + * @param bool $is_image Image or not + * @return bool + */ + public function create($task_id, $name, $path, $is_image) + { + return $this->db->table(self::TABLE)->save(array( + 'task_id' => $task_id, + 'name' => $name, + 'path' => $path, + 'is_image' => $is_image ? '1' : '0', + )); + } + + /** + * Get all files for a given task + * + * @access public + * @param integer $task_id Task id + * @return array + */ + public function getAll($task_id) + { + return $listing = $this->db->table(self::TABLE) + ->eq('task_id', $task_id) + ->asc('name') + ->findAll(); + } + + /** + * Check if a filename is an image + * + * @access public + * @param string $filename Filename + * @return bool + */ + public function isImage($filename) + { + return getimagesize($filename) !== false; + } + + /** + * Generate the path for a new filename + * + * @access public + * @param integer $project_id Project id + * @param integer $task_id Task id + * @param string $filename Filename + * @return bool + */ + public function generatePath($project_id, $task_id, $filename) + { + return $project_id.DIRECTORY_SEPARATOR.$task_id.DIRECTORY_SEPARATOR.hash('sha1', $filename.time()); + } + + /** + * Check if the base directory is created correctly + * + * @access public + */ + public function setup() + { + if (! is_dir(self::BASE_PATH)) { + if (! mkdir(self::BASE_PATH, 0755, true)) { + die('Unable to create the upload directory: "'.self::BASE_PATH.'"'); + } + } + + if (! is_writable(self::BASE_PATH)) { + die('The directory "'.self::BASE_PATH.'" must be writeable by your webserver user'); + } + } + + /** + * Handle file upload + * + * @access public + * @param integer $project_id Project id + * @param integer $task_id Task id + * @param string $form_name File form name + */ + public function upload($project_id, $task_id, $form_name) + { + $this->setup(); + + if (! empty($_FILES[$form_name])) { + + foreach ($_FILES[$form_name]['error'] as $key => $error) { + + if ($error == UPLOAD_ERR_OK && $_FILES[$form_name]['size'][$key] > 0) { + + $original_filename = basename($_FILES[$form_name]['name'][$key]); + $uploaded_filename = $_FILES[$form_name]['tmp_name'][$key]; + $destination_filename = $this->generatePath($project_id, $task_id, $original_filename); + + @mkdir(self::BASE_PATH.dirname($destination_filename), 0755, true); + + if (@move_uploaded_file($uploaded_filename, self::BASE_PATH.$destination_filename)) { + + $this->create( + $task_id, + $original_filename, + $destination_filename, + $this->isImage(self::BASE_PATH.$destination_filename) + ); + } + } + } + } + } +} diff --git a/app/Model/Task.php b/app/Model/Task.php index bd67d272..f31594c9 100644 --- a/app/Model/Task.php +++ b/app/Model/Task.php @@ -139,6 +139,7 @@ class Task extends Base ->table(self::TABLE) ->columns( '(SELECT count(*) FROM comments WHERE task_id=tasks.id) AS nb_comments', + '(SELECT count(*) FROM task_has_files WHERE task_id=tasks.id) AS nb_files', 'tasks.id', 'tasks.title', 'tasks.description', diff --git a/app/Schema/Mysql.php b/app/Schema/Mysql.php index 6764ad5d..d3b111f9 100644 --- a/app/Schema/Mysql.php +++ b/app/Schema/Mysql.php @@ -2,7 +2,22 @@ namespace Schema; -const VERSION = 16; +const VERSION = 17; + +function version_17($pdo) +{ + $pdo->exec(" + CREATE TABLE task_has_files ( + id INT NOT NULL AUTO_INCREMENT, + name VARCHAR(50), + path VARCHAR(255), + is_image TINYINT(1) DEFAULT 0, + task_id INT, + PRIMARY KEY (id), + FOREIGN KEY(task_id) REFERENCES tasks(id) ON DELETE CASCADE + ) ENGINE=InnoDB CHARSET=utf8" + ); +} function version_16($pdo) { diff --git a/app/Schema/Sqlite.php b/app/Schema/Sqlite.php index 0bb4de8d..94ef0316 100644 --- a/app/Schema/Sqlite.php +++ b/app/Schema/Sqlite.php @@ -2,7 +2,21 @@ namespace Schema; -const VERSION = 16; +const VERSION = 17; + +function version_17($pdo) +{ + $pdo->exec(" + CREATE TABLE task_has_files ( + id INTEGER PRIMARY KEY, + name TEXT COLLATE NOCASE, + path TEXT, + is_image INTEGER DEFAULT 0, + task_id INTEGER, + FOREIGN KEY(task_id) REFERENCES tasks(id) ON DELETE CASCADE + )" + ); +} function version_16($pdo) { diff --git a/app/Templates/board_show.php b/app/Templates/board_show.php index 719e3bdd..78f9dd50 100644 --- a/app/Templates/board_show.php +++ b/app/Templates/board_show.php @@ -59,7 +59,7 @@ </div> <?php endif ?> - <?php if (! empty($task['date_due']) || ! empty($task['nb_comments']) || ! empty($task['description'])): ?> + <?php if (! empty($task['date_due']) || ! empty($task['nb_files']) || ! empty($task['nb_comments']) || ! empty($task['description'])): ?> <div class="task-footer"> <?php if (! empty($task['date_due'])): ?> @@ -69,6 +69,10 @@ <?php endif ?> <div class="task-icons"> + <?php if (! empty($task['nb_files'])): ?> + <?= $task['nb_files'] ?> <i class="fa fa-paperclip"></i> + <?php endif ?> + <?php if (! empty($task['nb_comments'])): ?> <?= $task['nb_comments'] ?> <i class="fa fa-comment-o" title="<?= p($task['nb_comments'], t('%d comment', $task['nb_comments']), t('%d comments', $task['nb_comments'])) ?>"></i> <?php endif ?> diff --git a/app/Templates/task_close.php b/app/Templates/task_close.php index 3531b37d..6843c2f6 100644 --- a/app/Templates/task_close.php +++ b/app/Templates/task_close.php @@ -1,3 +1,7 @@ +<div class="page-header"> + <h2><?= t('Close a task') ?></h2> +</div> + <div class="confirm"> <p class="alert alert-info"> <?= t('Do you really want to close this task: "%s"?', Helper\escape($task['title'])) ?> diff --git a/app/Templates/task_layout.php b/app/Templates/task_layout.php index 9a6bbd00..ce5f36c5 100644 --- a/app/Templates/task_layout.php +++ b/app/Templates/task_layout.php @@ -13,4 +13,5 @@ <?= $task_content_for_layout ?> </div> </section> -</section>
\ No newline at end of file +</section> +<script type="text/javascript" src="assets/js/task.js"></script>
\ No newline at end of file diff --git a/app/Templates/task_open.php b/app/Templates/task_open.php index 54cc11f0..59ea0b54 100644 --- a/app/Templates/task_open.php +++ b/app/Templates/task_open.php @@ -1,16 +1,14 @@ -<section id="main"> - <div class="page-header"> - <h2><?= t('Open a task') ?></h2> - </div> +<div class="page-header"> + <h2><?= t('Open a task') ?></h2> +</div> - <div class="confirm"> - <p class="alert alert-info"> - <?= t('Do you really want to open this task: "%s"?', Helper\escape($task['title'])) ?> - </p> +<div class="confirm"> + <p class="alert alert-info"> + <?= t('Do you really want to open this task: "%s"?', Helper\escape($task['title'])) ?> + </p> - <div class="form-actions"> - <a href="?controller=task&action=open&task_id=<?= $task['id'] ?>" class="btn btn-red"><?= t('Yes') ?></a> - <?= t('or') ?> <a href="?controller=task&action=show&task_id=<?= $task['id'] ?>"><?= t('cancel') ?></a> - </div> + <div class="form-actions"> + <a href="?controller=task&action=open&task_id=<?= $task['id'] ?>" class="btn btn-red"><?= t('Yes') ?></a> + <?= t('or') ?> <a href="?controller=task&action=show&task_id=<?= $task['id'] ?>"><?= t('cancel') ?></a> </div> -</section>
\ No newline at end of file +</div>
\ No newline at end of file diff --git a/app/Templates/task_open_file.php b/app/Templates/task_open_file.php new file mode 100644 index 00000000..e0817f01 --- /dev/null +++ b/app/Templates/task_open_file.php @@ -0,0 +1,6 @@ +<div class="page-header"> + <h2><?= Helper\escape($file['name']) ?></h2> + <div class="task-file-viewer"> + <img src="?controller=task&action=image&file_id=<?= $file['id'] ?>&task_id=<?= $file['task_id'] ?>" alt="<?= Helper\escape($file['name']) ?>"/> + </div> +</div>
\ No newline at end of file diff --git a/app/Templates/task_remove.php b/app/Templates/task_remove.php index 1aa9503b..60e4e8e7 100644 --- a/app/Templates/task_remove.php +++ b/app/Templates/task_remove.php @@ -1,3 +1,7 @@ +<div class="page-header"> + <h2><?= t('Remove a task') ?></h2> +</div> + <div class="confirm"> <p class="alert alert-info"> <?= t('Do you really want to remove this task: "%s"?', Helper\escape($task['title'])) ?> diff --git a/app/Templates/task_remove_file.php b/app/Templates/task_remove_file.php new file mode 100644 index 00000000..9687b602 --- /dev/null +++ b/app/Templates/task_remove_file.php @@ -0,0 +1,14 @@ +<div class="page-header"> + <h2><?= t('Remove a file') ?></h2> +</div> + +<div class="confirm"> + <p class="alert alert-info"> + <?= t('Do you really want to remove this file: "%s"?', Helper\escape($file['name'])) ?> + </p> + + <div class="form-actions"> + <a href="?controller=task&action=removeFile&task_id=<?= $task['id'] ?>&file_id=<?= $file['id'] ?>" class="btn btn-red"><?= t('Yes') ?></a> + <?= t('or') ?> <a href="?controller=task&action=show&task_id=<?= $task['id'] ?>"><?= t('cancel') ?></a> + </div> +</div>
\ No newline at end of file diff --git a/app/Templates/task_show.php b/app/Templates/task_show.php index a5b79359..56f6cba5 100644 --- a/app/Templates/task_show.php +++ b/app/Templates/task_show.php @@ -64,6 +64,23 @@ </form> <?php endif ?> +<?php if (! empty($files)): ?> + <h2 id="attachments"><?= t('Attachments') ?></h2> + <ul class="task-show-files"> + <?php foreach ($files as $file): ?> + <li> + <a href="?controller=task&action=download&file_id=<?= $file['id'] ?>&task_id=<?= $task['id'] ?>"><?= Helper\escape($file['name']) ?></a> + <span class="task-show-file-actions"> + <?php if ($file['is_image']): ?> + <a href="?controller=task&action=openFile&file_id=<?= $file['id'] ?>&task_id=<?= $task['id'] ?>" class="popover"><?= t('open') ?></a>, + <?php endif ?> + <a href="?controller=task&action=confirmRemoveFile&file_id=<?= $file['id'] ?>&task_id=<?= $task['id'] ?>"><?= t('remove') ?></a> + </span> + </li> + <?php endforeach ?> + </ul> +<?php endif ?> + <h2><?= t('Comments') ?></h2> <?php if ($comments): ?> <ul id="comments"> diff --git a/app/Templates/task_sidebar.php b/app/Templates/task_sidebar.php index 314d5214..9dbc1a8c 100644 --- a/app/Templates/task_sidebar.php +++ b/app/Templates/task_sidebar.php @@ -2,8 +2,10 @@ <h2><?= t('Actions') ?></h2> <div class="task-show-actions"> <ul> - <li><a href="?controller=task&action=duplicate&project_id=<?= $task['project_id'] ?>&task_id=<?= $task['id'] ?>"><?= t('Duplicate') ?></a></li> + <li><a href="?controller=task&action=show&task_id=<?= $task['id'] ?>"><?= t('Description') ?></a></li> <li><a href="?controller=task&action=edit&task_id=<?= $task['id'] ?>"><?= t('Edit') ?></a></li> + <li><a href="?controller=task&action=file&task_id=<?= $task['id'] ?>"><?= t('Attach a document') ?></a></li> + <li><a href="?controller=task&action=duplicate&project_id=<?= $task['project_id'] ?>&task_id=<?= $task['id'] ?>"><?= t('Duplicate') ?></a></li> <li> <?php if ($task['is_active'] == 1): ?> <a href="?controller=task&action=confirmClose&task_id=<?= $task['id'] ?>"><?= t('Close this task') ?></a> diff --git a/app/Templates/task_upload.php b/app/Templates/task_upload.php new file mode 100644 index 00000000..7100ab31 --- /dev/null +++ b/app/Templates/task_upload.php @@ -0,0 +1,10 @@ +<div class="page-header"> + <h2><?= t('Attach a document') ?></h2> +</div> + +<form action="?controller=task&action=upload&task_id=<?= $task['id'] ?>" method="post" enctype="multipart/form-data"> + <input type="file" name="files[]" multiple /> + <div class="form-actions"> + <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> + </div> +</form>
\ No newline at end of file diff --git a/assets/css/app.css b/assets/css/app.css index 45ec7444..51bdb878 100644 --- a/assets/css/app.css +++ b/assets/css/app.css @@ -702,6 +702,43 @@ div.task .task-score { padding: 10px; } +.task-show-files a { + font-weight: bold; + text-decoration: none; +} + +.task-show-files li { + margin-left: 25px; + list-style-type: square; + line-height: 25px; +} + +.task-show-file-actions { + font-size: 0.75em; +} + +.task-show-file-actions:before { + content: " ["; +} + +.task-show-file-actions:after { + content: "]"; +} + +.task-show-file-actions a { + color: #333; +} + +.task-file-viewer { + position: relative; +} + +.task-file-viewer img { + max-width: 95%; + max-height: 85%; + margin-top: 10px; +} + /* markdown content */ .markdown { line-height: 1.4em; diff --git a/assets/js/task.js b/assets/js/task.js new file mode 100644 index 00000000..f95792c3 --- /dev/null +++ b/assets/js/task.js @@ -0,0 +1,27 @@ +(function () { + + // Show popup + function popover_show(content) + { + $("body").append('<div id="popover-container"><div id="popover-content">' + content + '</div></div>'); + + $("#popover-container").click(function() { + $(this).remove(); + }); + + $("#popover-content").click(function(e) { + e.stopPropagation(); + }); + } + + $(".popover").click(function(e) { + + e.preventDefault(); + e.stopPropagation(); + + $.get($(this).attr("href"), function(data) { + popover_show(data); + }); + }); + +}()); |