├── .gitignore ├── Css └── milestone.css ├── Doc ├── Gantt-Milestone-and-blocked.png └── milestoneview.png ├── Formatter └── TaskGanttLinkAwareFormatter.php ├── LICENSE ├── Locale ├── cs_CZ │ └── translations.php ├── de_DE │ └── translations.php ├── fr_FR │ └── translations.php ├── it_IT │ └── translations.php └── ru_RU │ └── translations.php ├── Makefile ├── Model └── TaskLinkExtModel.php ├── Plugin.php ├── README.md └── Template ├── milestone ├── dropdown.php ├── show.php └── table.php └── task_internal_link └── show.php /.gitignore: -------------------------------------------------------------------------------- 1 | *.un~ 2 | *.buildpath 3 | *.project 4 | *.zip 5 | -------------------------------------------------------------------------------- /Css/milestone.css: -------------------------------------------------------------------------------- 1 | .total { 2 | padding-right: 10px; 3 | text-align: right; 4 | } 5 | 6 | .progress-bar { 7 | border: 1px solid black; 8 | border-radius: 3px; 9 | width: 99%; 10 | margin: 2px auto; 11 | } 12 | 13 | .progress-bar .progress { 14 | border-right: 1px solid black; 15 | border-radius: 3px; 16 | width: 0%; 17 | text-align: left; 18 | overflow-wrap: normal; 19 | padding-bottom: 0; 20 | margin-bottom: 0; 21 | } 22 | -------------------------------------------------------------------------------- /Doc/Gantt-Milestone-and-blocked.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oliviermaridat/kanboard-milestone-plugin/d16e8f53fdd5eaa01ba745d23b05bce2a15a0ad1/Doc/Gantt-Milestone-and-blocked.png -------------------------------------------------------------------------------- /Doc/milestoneview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oliviermaridat/kanboard-milestone-plugin/d16e8f53fdd5eaa01ba745d23b05bce2a15a0ad1/Doc/milestoneview.png -------------------------------------------------------------------------------- /Formatter/TaskGanttLinkAwareFormatter.php: -------------------------------------------------------------------------------- 1 | query->findAll() as $task) { 37 | $this->formatTaskUsingLinks($task); 38 | } 39 | foreach ($this->query->findAll() as $task) { 40 | $bars[] = $this->formatTaskUsingLinks($task); 41 | } 42 | 43 | return $bars; 44 | } 45 | 46 | /** 47 | * Format a single task 48 | * 49 | * @access private 50 | * @param array $task 51 | * @return array 52 | */ 53 | private function formatTaskUsingLinks(array $task) 54 | { 55 | if (! isset($this->columns[$task['project_id']])) { 56 | $this->columns[$task['project_id']] = $this->columnModel->getList($task['project_id']); 57 | } 58 | 59 | $start = $task['date_started'] ?: time(); 60 | $end = $task['date_completed'] ?: ($task['date_due'] ?: $start); 61 | 62 | if ((empty($task['date_completed']) && empty($task['date_due'])) || empty($task['date_started'])) { 63 | // Follow target milestone start/due dates 64 | list($dates_started, $dates_due) = $this->taskLinkExtModel->getAllDates($task['id'], 8, $this->known_start_dates, $this->known_due_dates); 65 | if (empty($task['date_started']) && !empty($dates_started)) { 66 | $start = max($start, min($dates_started)); 67 | } 68 | if (empty($task['date_completed']) && empty($task['date_due']) && !empty($dates_due)) { 69 | $end = min($dates_due); 70 | } 71 | // Start after any blocking tasks 72 | if (empty($task['date_started'])) { 73 | list($dates_started, $dates_due) = $this->taskLinkExtModel->getAllDates($task['id'], 3, $this->known_start_dates, $this->known_due_dates); 74 | if (!empty($dates_due)) { 75 | $start = max(array_merge(array($start), $dates_due)); 76 | } 77 | } 78 | $this->known_start_dates[$task['id']] = $start; 79 | $this->known_due_dates[$task['id']] = $end; 80 | } 81 | 82 | return array( 83 | 'type' => 'task', 84 | 'id' => $task['id'], 85 | 'title' => $task['title'], 86 | 'start' => array( 87 | (int) date('Y', $start), 88 | (int) date('n', $start), 89 | (int) date('j', $start), 90 | ), 91 | 'end' => array( 92 | (int) date('Y', $end), 93 | (int) date('n', $end), 94 | (int) date('j', $end), 95 | ), 96 | 'column_title' => $task['column_name'], 97 | 'assignee' => $task['assignee_name'] ?: $task['assignee_username'], 98 | 'progress' => $this->taskModel->getProgress($task, $this->columns[$task['project_id']]).'%', 99 | 'link' => $this->helper->url->href('TaskViewController', 'show', array('project_id' => $task['project_id'], 'task_id' => $task['id'])), 100 | 'color' => $this->colorModel->getColorProperties($task['color_id']), 101 | 'not_defined' => empty($task['date_due']) || empty($task['date_started']), 102 | 'date_started_not_defined' => empty($task['date_started']), 103 | 'date_due_not_defined' => empty($task['date_completed']) && empty($task['date_due']), 104 | ); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016-2017 Olivier Maridat 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /Locale/cs_CZ/translations.php: -------------------------------------------------------------------------------- 1 | 'Milník', 4 | 'The Milestone Plugin for Kanboard adds a section for milestones to show their related tasks.' => 'Milestone Plugin pro Kanboard přidává sekci pro milníky, aby ukázala související úkoly.', 5 | 'Total progress' => 'Celkový pokrok', 6 | 'Total time tracking' => 'Sledování celkového času', 7 | 'remaining' => 'zbývající', 8 | 'Edit the internal link' => 'Upravit interní odkaz' 9 | ); 10 | -------------------------------------------------------------------------------- /Locale/de_DE/translations.php: -------------------------------------------------------------------------------- 1 | 'Meilenstein', 4 | 'The Milestone Plugin for Kanboard adds a section for milestones to show their related tasks.' => 'Das Milestone Plugin für Kanboard fügt einen Abschnitt für Meilensteine hinzu, um die zugehörigen Aufgaben anzuzeigen.', 5 | 'Total progress' => 'Gesamter Fortschritt', 6 | 'Total time tracking' => 'Gesamtzeiterfassung', 7 | 'remaining' => 'verblieben', 8 | 'Edit the internal link' => 'Bearbeiten des internen Links' 9 | ); 10 | -------------------------------------------------------------------------------- /Locale/fr_FR/translations.php: -------------------------------------------------------------------------------- 1 | 'Milestone', 5 | 'The Milestone Plugin for Kanboard adds a section for milestones to show their related tasks.' => 'Le plugin Milestone pour Kanboard ajoute une section lorsque l\'on parcours une tâche de type "Milestone" (c-à-d ayant un lien "is a milestone of") permettant de visualiser rapidement l\'avancement des tâches à effectuer pour cloturer cette étape du projet.', 6 | 'Total progress' => 'Suivi du progrès total', 7 | 'Total time tracking' => 'Suivi du temps total', 8 | 'remaining' => 'restant', 9 | 'Edit the internal link' => 'Modifier le lien interne' 10 | ); 11 | -------------------------------------------------------------------------------- /Locale/it_IT/translations.php: -------------------------------------------------------------------------------- 1 | 'Milestone', 5 | 'The Milestone Plugin for Kanboard adds a section for milestones to show their related tasks.' => 'Il plug-in Milestone per Kanboard aggiunge una sezione per le pietre miliari per mostrare le loro attività correlate.', 6 | 'Total progress' => 'Progresso totale', 7 | 'Total time tracking' => 'Monitoraggio del tempo totale', 8 | 'remaining' => 'residuo', 9 | 'Edit the internal link' => 'Modifica il collegamento interno', 10 | 'Add a new task into this milestone' => 'Aggiungi una nuova attività a questo Milestone', 11 | ]; -------------------------------------------------------------------------------- /Locale/ru_RU/translations.php: -------------------------------------------------------------------------------- 1 | 'Версия', 4 | 'The Milestone Plugin for Kanboard adds a section for milestones to show their related tasks.' => 'Плагин, добавляющий механизм версий в Kanboard для отображения с связанными задачами', 5 | 'Total progress' => 'Общее прогресс', 6 | 'Total time tracking' => 'Общее время', 7 | 'remaining' => 'осталось', 8 | 'Edit the internal link' => 'Редактировать внутреннюю ссылку' 9 | ); 10 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: 2 | @ echo "Build archive for plugin ${plugin} version=${version}" 3 | @ git archive HEAD --prefix=${plugin}/ --format=zip -o ${plugin}-${version}.zip 4 | -------------------------------------------------------------------------------- /Model/TaskLinkExtModel.php: -------------------------------------------------------------------------------- 1 | db 32 | ->table(self::TABLE) 33 | ->columns( 34 | self::TABLE.'.id', 35 | self::TABLE.'.opposite_task_id AS task_id', 36 | LinkModel::TABLE.'.label', 37 | TaskModel::TABLE.'.title', 38 | TaskModel::TABLE.'.is_active', 39 | TaskModel::TABLE.'.project_id', 40 | TaskModel::TABLE.'.column_id', 41 | TaskModel::TABLE.'.color_id', 42 | TaskModel::TABLE.'.date_completed', 43 | TaskModel::TABLE.'.date_started', 44 | TaskModel::TABLE.'.date_due', 45 | TaskModel::TABLE.'.time_spent AS task_time_spent', 46 | TaskModel::TABLE.'.time_estimated AS task_time_estimated', 47 | TaskModel::TABLE.'.owner_id AS task_assignee_id', 48 | UserModel::TABLE.'.username AS task_assignee_username', 49 | UserModel::TABLE.'.name AS task_assignee_name', 50 | ColumnModel::TABLE.'.title AS column_title', 51 | ProjectModel::TABLE.'.name AS project_name' 52 | ) 53 | ->eq(self::TABLE.'.task_id', $task_id) 54 | ->join(LinkModel::TABLE, 'id', 'link_id') 55 | ->join(TaskModel::TABLE, 'id', 'opposite_task_id') 56 | ->join(ColumnModel::TABLE, 'id', 'column_id', TaskModel::TABLE) 57 | ->join(UserModel::TABLE, 'id', 'owner_id', TaskModel::TABLE) 58 | ->join(ProjectModel::TABLE, 'id', 'project_id', TaskModel::TABLE) 59 | ->asc(LinkModel::TABLE.'.id') 60 | ->desc(ColumnModel::TABLE.'.position') 61 | ->desc(TaskModel::TABLE.'.is_active') 62 | ->asc(TaskModel::TABLE.'.position') 63 | ->asc(TaskModel::TABLE.'.id'); 64 | if (NULL != $link_ids && is_array($link_ids) && !empty($link_ids)) { 65 | $query->in(self::TABLE.'.link_id', $link_ids); 66 | } 67 | return $query->findAll(); 68 | } 69 | 70 | /** 71 | * Get all links started and due dates attached to the given task 72 | * 73 | * @access public 74 | * @param integer $task_id Task id 75 | * @param integer $link_id Filter on a link id (default: no filter) 76 | * @param array $know_start_dates Already known start dates 77 | * @param array $know_due_dates Already known due dates 78 | * @return array Two arrays containing non empty started dates, and non empty due dates 79 | */ 80 | public function getAllDates($task_id, $link_id, $know_start_dates, $know_due_dates) 81 | { 82 | $links = $this->getAll($task_id, array($link_id)); 83 | $dates_started = array(); 84 | $dates_due = array(); 85 | foreach($links as $link) { 86 | // Existing or known start date 87 | if (!empty($link['date_started'])) { 88 | $dates_started[] = $link['date_started']; 89 | } 90 | else if (isset($know_start_dates[$link['task_id']])) { 91 | $dates_started[] = $know_start_dates[$link['task_id']]; 92 | } 93 | // Existing completed or due date, or known due date 94 | if (!empty($link['date_completed'])) { 95 | $dates_due[] = $link['date_completed']; 96 | } 97 | else if (!empty($link['date_due'])) { 98 | $dates_due[] = $link['date_due']; 99 | } 100 | else if (isset($know_due_dates[$link['task_id']])) { 101 | $dates_due[] = $know_due_dates[$link['task_id']]; 102 | } 103 | } 104 | return array($dates_started, $dates_due); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /Plugin.php: -------------------------------------------------------------------------------- 1 | hook->on('template:layout:css', array('template' => 'plugins/Milestone/Css/milestone.css')); 13 | $this->template->hook->attach('template:task:dropdown', 'milestone:milestone/dropdown'); 14 | $this->template->setTemplateOverride('task_internal_link/show', 'milestone:task_internal_link/show'); 15 | $this->template->setTemplateOverride('milestone/show', 'milestone:milestone/show'); 16 | $this->template->setTemplateOverride('milestone/table', 'milestone:milestone/table'); 17 | 18 | $this->hook->on('controller:tasklink:form:default', function (array $default_values) { 19 | return (0 != $this->request->getIntegerParam('link_id')) ? array('link_id' => $this->request->getIntegerParam('link_id')) : array(); 20 | }); 21 | 22 | $this->container['taskGanttFormatter'] = $this->container->factory(function ($c) { 23 | return new TaskGanttLinkAwareFormatter($c); 24 | }); 25 | } 26 | 27 | public function onStartup() 28 | { 29 | Translator::load($this->languageModel->getCurrentLanguage(), __DIR__.'/Locale'); 30 | } 31 | 32 | public function getClasses() 33 | { 34 | return array( 35 | 'Plugin\Milestone\Model' => array( 36 | 'TaskLinkExtModel' 37 | ) 38 | ); 39 | } 40 | 41 | public function getPluginName() 42 | { 43 | return t('Milestone'); 44 | } 45 | 46 | public function getPluginAuthor() 47 | { 48 | return 'Olivier Maridat'; 49 | } 50 | 51 | public function getPluginVersion() 52 | { 53 | return '1.1.2'; 54 | } 55 | 56 | public function getPluginHomepage() 57 | { 58 | return 'https://github.com/oliviermaridat/kanboard-milestone-plugin'; 59 | } 60 | 61 | public function getPluginDescription() 62 | { 63 | return t('The Milestone Plugin for Kanboard adds a section for milestones to show their related tasks.'); 64 | } 65 | 66 | public function getCompatibleVersion() 67 | { 68 | return '>=1.0.43'; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Kanboard Milestone Plugin 2 | =================================== 3 | 4 | This plugin adds a section for milestones to show their related tasks. 5 | 6 | The new Milestone section is added between the "Sub-tasks" and the "Internal links" sections as depicted below. 7 | 8 |  9 | 10 | It also uses "is a milestone of" and "is blocked by" task links to infer start and due dates if you are using the [Gantt Plugin](https://github.com/kanboard/plugin-gantt). 11 | 12 |  13 | 14 | Author 15 | ------ 16 | 17 | - Olivier Maridat 18 | - License MIT 19 | 20 | Requirements 21 | ------ 22 | 23 | * Kanboard >= 1.0.37 24 | 25 | Installation 26 | ------------ 27 | 28 | You have the choice between 3 methods: 29 | 30 | - Install the plugin from the Kanboard plugin manager in one click 31 | - Download the zip file and decompress everything under the directory plugins/Milestone 32 | - Clone this repository into the folder plugins/Milestone 33 | 34 | Note: Plugin folder is case-sensitive. 35 | 36 | Documentation 37 | ------------- 38 | 39 | Milestone management is based on task links. It provides a proper view to show all the tasks linked with "is a milestone of" to the current task. 40 | 41 | To create a milestone: 42 | 43 | * Pick a task, 44 | * click on "Add internal link" on the sidebar, 45 | * create a new link with another task by selected the label "is a milestone of", 46 | * and the new "Milestone" element should appear on the picked task view. 47 | * You can add the same link to other tasks that should be finished for this milestone. 48 | -------------------------------------------------------------------------------- /Template/milestone/dropdown.php: -------------------------------------------------------------------------------- 1 | 2 |