diff --git a/Controller/ConfigController.php b/Controller/ConfigController.php index e118975..5a9d429 100644 --- a/Controller/ConfigController.php +++ b/Controller/ConfigController.php @@ -27,6 +27,7 @@ class ConfigController extends BaseController 'organon_tweaks_persistent_sort' => (int) $this->configModel->get('organon_tweaks_persistent_sort', 1), 'organon_tweaks_done_badge' => (int) $this->configModel->get('organon_tweaks_done_badge', 0), 'organon_tweaks_done_closes_task' => (int) $this->configModel->get('organon_tweaks_done_closes_task', 0), + 'organon_tweaks_done_autosubtasks' => (int) $this->configModel->get('organon_tweaks_done_autosubtasks', 0), ), 'errors' => array(), ))); @@ -49,6 +50,7 @@ class ConfigController extends BaseController $persistentSort = isset($values['organon_tweaks_persistent_sort']) ? 1 : 0; $doneBadge = isset($values['organon_tweaks_done_badge']) ? 1 : 0; $doneClosesTask = isset($values['organon_tweaks_done_closes_task']) ? 1 : 0; + $doneAutoSubtasks = isset($values['organon_tweaks_done_autosubtasks']) ? 1 : 0; if ($this->configModel->save(array( 'organon_tweaks_always_comment_icon' => $alwaysCommentIcon, @@ -64,6 +66,7 @@ class ConfigController extends BaseController 'organon_tweaks_persistent_sort' => $persistentSort, 'organon_tweaks_done_badge' => $doneBadge, 'organon_tweaks_done_closes_task' => $doneClosesTask, + 'organon_tweaks_done_autosubtasks' => $doneAutoSubtasks, ))) { $this->flash->success(t('Settings saved successfully.')); } else { diff --git a/Helper/OrganonDoneHelper.php b/Helper/OrganonDoneHelper.php index 8cd78c3..8967304 100644 --- a/Helper/OrganonDoneHelper.php +++ b/Helper/OrganonDoneHelper.php @@ -3,6 +3,7 @@ namespace Kanboard\Plugin\OrganonTweaks\Helper; use Kanboard\Core\Base; +use Kanboard\Model\SubtaskModel; /** * Done/Todo badge helper. @@ -22,6 +23,8 @@ class OrganonDoneHelper extends Base { const DONE_KEY = 'organon_done'; const CLOSES_KEY = 'organon_tweaks_done_closes_task'; + const AUTOSUB_KEY = 'organon_tweaks_done_autosubtasks'; + const ALLDONE_KEY = 'organon_subtasks_alldone'; /** * Is this task Done? (mode-aware: closed status in close-mode, else the metadata marker) @@ -47,4 +50,63 @@ class OrganonDoneHelper extends Base { return (int) $this->configModel->get(self::CLOSES_KEY, 0) === 1; } + + /** + * Mark a task Done in the current mode. Idempotent -- a no-op if it is already Done. Close-mode + * closes the task; marker-mode sets the DONE_KEY metadata. + * + * @param int $task_id + */ + public function markDone($task_id) + { + $task = $this->taskFinderModel->getById((int) $task_id); + + if (empty($task) || $this->isDone($task)) { + return; + } + + if ($this->closesTask()) { + $this->taskStatusModel->close((int) $task_id); + } else { + $this->taskMetadataModel->save((int) $task_id, array(self::DONE_KEY => 'on')); + } + } + + /** + * Recompute whether the task's subtasks are ALL done and, only on the up-transition + * (was-not-all-done -> now-all-done), auto-mark the task Done. The last-seen all-done state is kept + * in ALLDONE_KEY ('on'/'off') to detect that edge -- so a manual unmark is honored (nothing + * re-fires) and title edits / refreshes never re-mark. Never auto-unmarks. + * + * @param int $task_id + * @param int $exclude_id a subtask id to exclude from the counts (the row being deleted -- + * EVENT_DELETE fires BEFORE the row is removed, so it still counts) + */ + public function syncSubtasksDone($task_id, $exclude_id = 0) + { + $task_id = (int) $task_id; + $exclude_id = (int) $exclude_id; + + $totalQuery = $this->db->table(SubtaskModel::TABLE)->eq('task_id', $task_id); + $doneQuery = $this->db->table(SubtaskModel::TABLE)->eq('task_id', $task_id)->eq('status', SubtaskModel::STATUS_DONE); + + if ($exclude_id > 0) { + $totalQuery->neq('id', $exclude_id); + $doneQuery->neq('id', $exclude_id); + } + + $total = $totalQuery->count(); + $done = $doneQuery->count(); + $allDone = $total > 0 && $done === $total; + $prev = $this->taskMetadataModel->get($task_id, self::ALLDONE_KEY, 'off') === 'on'; + + // Save the new state BEFORE marking: in close-mode markDone() -> close() -> closeAll() re-fires + // subtask events into this method; with the marker already 'on', that re-entry sees no + // transition and is a no-op (no loop). + $this->taskMetadataModel->save($task_id, array(self::ALLDONE_KEY => $allDone ? 'on' : 'off')); + + if ($allDone && ! $prev) { + $this->markDone($task_id); + } + } } diff --git a/Plugin.php b/Plugin.php index be70e14..bc2da83 100644 --- a/Plugin.php +++ b/Plugin.php @@ -3,6 +3,7 @@ namespace Kanboard\Plugin\OrganonTweaks; use Kanboard\Core\Plugin\Base; +use Kanboard\Model\SubtaskModel; use Kanboard\Model\TaskModel; class Plugin extends Base @@ -122,6 +123,28 @@ class Plugin extends Base // Bulk "Mark all as Done/Todo" entry in the column header dropdown (moved into the native // menu by relocate.js). Opens a two-button modal that confirms + picks the direction. $this->template->hook->attach('template:board:column:dropdown', 'organonTweaks:board/mark_all_item'); + + // Sub-tweak (opt-in): auto-mark a card Done when its LAST subtask is completed. On every + // subtask change, recompute all-done and mark Done only on the up-transition + // (state-comparison in the helper), so a manual unmark is honored -- nothing re-fires. + // Dispatcher direct (the plugin on() wrapper drops the event). DELETE fires before the row + // is removed, so its id is excluded from the recount. + if ((int) $this->configModel->get('organon_tweaks_done_autosubtasks', 0) === 1) { + $container = $this->container; + $sync = function ($task_id, $exclude_id = 0) use ($container) { + $helper = new \Kanboard\Plugin\OrganonTweaks\Helper\OrganonDoneHelper($container); + $helper->syncSubtasksDone((int) $task_id, (int) $exclude_id); + }; + $this->dispatcher->addListener(SubtaskModel::EVENT_UPDATE, function ($event) use ($sync) { + $sync($event['subtask']['task_id']); + }); + $this->dispatcher->addListener(SubtaskModel::EVENT_CREATE, function ($event) use ($sync) { + $sync($event['subtask']['task_id']); + }); + $this->dispatcher->addListener(SubtaskModel::EVENT_DELETE, function ($event) use ($sync) { + $sync($event['subtask']['task_id'], $event['subtask']['id']); + }); + } } // Auto-managed shared custom filters (v1.5 "Show all tasks", v1.6 month filters, v1.7 @@ -175,7 +198,7 @@ class Plugin extends Base public function getPluginVersion() { - return '2.1.3'; + return '2.2.0'; } public function getPluginHomepage() diff --git a/README.md b/README.md index 164abc7..8829d11 100644 --- a/README.md +++ b/README.md @@ -142,6 +142,12 @@ recolored -- only the badge. open-only filter, but reappears if you clear the filter (or use the "Board: show all tasks" filter). - With that option **off**, the badge is an independent marker stored in task metadata (`organon_done`) that never touches the open/closed status. +- **Auto-mark on subtasks (opt-in).** When every subtask on a card is completed, the badge flips to + Done automatically (honoring the mode above -- marker or close). It only nudges at the moment the + last subtask completes: recomputing all-done on each subtask change and acting only on the + not-all-done -> all-done transition, so if you then set the badge back to Todo it is respected (the + subtasks stay done -- the description may hold unfinished business). It never auto-reverts to Todo. + Off by default; enable it in the Done badge settings. - **Bulk per-column action.** The board column header dropdown gains a **Mark all as Done/Todo** entry (`Template/board/mark_all_item.php`, moved into the menu by `relocate.js`). It opens a single modal that both confirms and picks the direction -- **Mark all Done** or **Mark all Todo** -- then sets diff --git a/Template/config/show.php b/Template/config/show.php index ef8cc44..859ef59 100644 --- a/Template/config/show.php +++ b/Template/config/show.php @@ -70,6 +70,9 @@ = $this->form->checkbox('organon_tweaks_done_closes_task', t('Marking Done also closes the task'), 1, isset($values['organon_tweaks_done_closes_task']) && $values['organon_tweaks_done_closes_task'] == 1) ?>
= t('When on, "Done" means the task is closed (so it leaves the board unless you clear the status filter), and clicking Todo reopens it -- native Close/Open stay in sync. When off, the badge is an independent marker that never changes the open/closed status.') ?>
+ + = $this->form->checkbox('organon_tweaks_done_autosubtasks', t('Auto-mark Done when the last subtask is completed'), 1, isset($values['organon_tweaks_done_autosubtasks']) && $values['organon_tweaks_done_autosubtasks'] == 1) ?> += t('When every subtask on a card is done, flip the badge to Done automatically (honoring the mode above). It only nudges at that moment -- if you then set it back to Todo, that is respected (the subtasks stay done).') ?>