From 8136c01403c9191fe0a53496481714f5a788d570 Mon Sep 17 00:00:00 2001 From: Ruben Carlo Benante Date: Sat, 18 Jul 2026 22:43:41 -0300 Subject: [PATCH] Todo/Done menu mark all in this column --- Asset/js/keep-scroll.js | 56 +++++++++++++++++----- Asset/js/relocate.js | 2 +- Controller/DoneController.php | 73 ++++++++++++++++++++++++++++- Helper/OrganonDoneHelper.php | 2 +- Plugin.php | 7 ++- README.md | 28 +++++++---- Template/board/done_badge.php | 4 +- Template/board/mark_all_confirm.php | 35 ++++++++++++++ Template/board/mark_all_item.php | 19 ++++++++ Template/config/show.php | 6 +-- Template/layout/done_style.php | 4 +- Template/task/done_badge.php | 4 +- VERSION | 2 +- 13 files changed, 204 insertions(+), 38 deletions(-) create mode 100644 Template/board/mark_all_confirm.php create mode 100644 Template/board/mark_all_item.php diff --git a/Asset/js/keep-scroll.js b/Asset/js/keep-scroll.js index 1cb4eac..f389348 100644 --- a/Asset/js/keep-scroll.js +++ b/Asset/js/keep-scroll.js @@ -1,18 +1,41 @@ /* - * OrganonTweaks -- keep the board's horizontal scroll position across refreshes. + * OrganonTweaks -- keep the board's horizontal scroll position across refreshes AND full reloads. * - * When you drop a card (and on Kanboard's periodic AJAX polling) the board is rebuilt with - * `$("#board-container").replaceWith(data)` (BoardDragAndDrop.refresh). The brand-new - * element starts at scrollLeft 0, so the board snaps back to the first column -- a jarring - * jump. This remembers the last scroll position and restores it the moment a replacement - * container appears, so the view stays put. + * Two cases lose the horizontal scroll and snap the board back to the first column: + * 1. AJAX rebuild -- dropping a card / periodic polling replaces #board-container + * (BoardDragAndDrop.refresh), and the new element starts at scrollLeft 0. + * 2. Full page reload -- e.g. clicking the Done/Todo badge, which navigates and redirects back to + * the board; a fresh page starts at scrollLeft 0. * - * We observe the STABLE parent (the container itself is replaced) and read the scroll from - * whichever #board-container is current, so it keeps working after every rebuild. + * We remember the last position and restore it: in-memory for the AJAX rebuild (observing the STABLE + * parent, since the container itself is replaced), and in sessionStorage (keyed per board) so it + * survives a full reload as well. */ (function () { "use strict"; + function boardKey() { + var m = location.href.match(/board\/(\d+)/) || location.href.match(/project_id=(\d+)/); + return "organon-board-scroll-" + (m ? m[1] : location.pathname); + } + + function readStored() { + try { + var v = window.sessionStorage.getItem(boardKey()); + return v ? parseInt(v, 10) : 0; + } catch (e) { + return 0; + } + } + + function writeStored(value) { + try { + window.sessionStorage.setItem(boardKey(), value); + } catch (e) { + // sessionStorage unavailable (private mode / disabled) -- degrade to AJAX-only restore. + } + } + function init() { var container = document.getElementById("board-container"); if (!container) { @@ -20,20 +43,27 @@ } var parent = container.parentNode; - var lastScrollLeft = container.scrollLeft; + var stored = readStored(); + var lastScrollLeft = stored || container.scrollLeft; var lastContainer = container; - // Remember the position whenever the board is scrolled (capture: scroll does not - // bubble, and this survives the container being replaced). + // Restore across a full page reload (badge click, etc.), not only AJAX rebuilds. + if (stored && container.scrollLeft !== stored) { + container.scrollLeft = stored; + } + + // Remember the position whenever the board is scrolled (capture: scroll does not bubble, and + // this survives the container being replaced). document.addEventListener("scroll", function (e) { var c = document.getElementById("board-container"); if (c && e.target === c) { lastScrollLeft = c.scrollLeft; + writeStored(lastScrollLeft); } }, true); - // When the board is rebuilt, #board-container becomes a new element at scrollLeft 0; - // restore the remembered position before the browser paints it. + // When the board is rebuilt, #board-container becomes a new element at scrollLeft 0; restore + // the remembered position before the browser paints it. if (window.MutationObserver) { new MutationObserver(function () { var c = document.getElementById("board-container"); diff --git a/Asset/js/relocate.js b/Asset/js/relocate.js index 376a472..3aa3b24 100644 --- a/Asset/js/relocate.js +++ b/Asset/js/relocate.js @@ -18,7 +18,7 @@ "use strict"; function relocateColumnMenuItems() { - var items = document.querySelectorAll(".organontweaks-remove-column-item"); + var items = document.querySelectorAll(".organontweaks-remove-column-item, .organontweaks-markall-item"); for (var i = 0; i < items.length; i++) { var li = items[i]; diff --git a/Controller/DoneController.php b/Controller/DoneController.php index 0f063a5..882866a 100644 --- a/Controller/DoneController.php +++ b/Controller/DoneController.php @@ -3,16 +3,21 @@ namespace Kanboard\Plugin\OrganonTweaks\Controller; use Kanboard\Controller\BaseController; +use Kanboard\Core\Controller\AccessForbiddenException; +use Kanboard\Model\TaskModel; use Kanboard\Plugin\OrganonTweaks\Helper\OrganonDoneHelper; /** - * Toggle a task's Done/Due badge (OrganonTweaks). + * Toggle a task's Done/Todo badge (OrganonTweaks). * * One-click CSRF link from the board card face and the task view. Single source of truth per mode: * - close-mode on: Done == closed, so this just closes/opens the task (no metadata); native * Close/Open stay in sync automatically because the badge reads is_active. * - close-mode off: this flips the 'organon_done' metadata marker and never touches open/closed. * Redirects back to wherever it was clicked (board or task view). + * + * Also provides the per-column bulk action (confirmColumn opens a two-button modal; markColumn sets + * EVERY task in the column+swimlane to the chosen state -- overwrite, not a per-task toggle). */ class DoneController extends BaseController { @@ -44,4 +49,70 @@ class DoneController extends BaseController $this->response->redirect($this->helper->url->to('TaskViewController', 'show', array('task_id' => $task['id'])), true); } } + + /** + * Open the "Mark all as Done/Todo" modal for a column: one modal that both confirms and lets the + * user pick the direction (two buttons). No state is tracked -- the direction is chosen here. + */ + public function confirmColumn() + { + $project = $this->getProject(); + $this->checkColumnWriteAccess($project['id']); + + $this->response->html($this->template->render('organonTweaks:board/mark_all_confirm', array( + 'project_id' => $project['id'], + 'column_id' => $this->request->getIntegerParam('column_id'), + 'swimlane_id' => $this->request->getIntegerParam('swimlane_id'), + ))); + } + + /** + * Set EVERY task in the column+swimlane to Done or Todo (direction param). Overwrites uniformly -- + * already-done tasks are harmlessly re-set. Mode-aware, like the badge: close-mode closes/opens + * all; marker-mode sets/clears the metadata on all. + */ + public function markColumn() + { + $project = $this->getProject(); + $this->checkColumnWriteAccess($project['id']); + $this->checkCSRFParam(); + + $column_id = $this->request->getIntegerParam('column_id'); + $swimlane_id = $this->request->getIntegerParam('swimlane_id'); + $done = $this->request->getStringParam('direction') === 'done'; + + $helper = new OrganonDoneHelper($this->container); + $closes = $helper->closesTask(); + + $tasks = $this->db->table(TaskModel::TABLE) + ->columns('id') + ->eq('project_id', $project['id']) + ->eq('column_id', $column_id) + ->eq('swimlane_id', $swimlane_id) + ->findAll(); + + foreach ($tasks as $task) { + if ($closes) { + $done ? $this->taskStatusModel->close($task['id']) : $this->taskStatusModel->open($task['id']); + } elseif ($done) { + $this->taskMetadataModel->save($task['id'], array(OrganonDoneHelper::DONE_KEY => 'on')); + } else { + $this->taskMetadataModel->remove($task['id'], OrganonDoneHelper::DONE_KEY); + } + } + + $this->flash->success(t('All tasks updated.')); + $this->response->redirect($this->helper->url->to('BoardViewController', 'show', array('project_id' => $project['id'])), true); + } + + /** + * Guard the bulk action to users who may modify tasks on this board (the menu item is already + * gated the same way; this backstops a direct request). + */ + private function checkColumnWriteAccess($project_id) + { + if (! $this->helper->user->hasProjectAccess('TaskModificationController', 'update', $project_id)) { + throw new AccessForbiddenException(); + } + } } diff --git a/Helper/OrganonDoneHelper.php b/Helper/OrganonDoneHelper.php index b94c6b4..8cd78c3 100644 --- a/Helper/OrganonDoneHelper.php +++ b/Helper/OrganonDoneHelper.php @@ -5,7 +5,7 @@ namespace Kanboard\Plugin\OrganonTweaks\Helper; use Kanboard\Core\Base; /** - * Done/Due badge helper. + * Done/Todo badge helper. * * "Done" has a single source of truth PER MODE (no drift with native Close/Open): * - close-mode on ("check done also closes tasks"): Done == the task is CLOSED (is_active == 0). diff --git a/Plugin.php b/Plugin.php index 1efbc9d..17816df 100644 --- a/Plugin.php +++ b/Plugin.php @@ -111,7 +111,7 @@ class Plugin extends Base }); } - // Tweak: a Done/Due status badge on the card face (top-right, below the header) and in the + // Tweak: a Done/Todo status badge on the card face (top-right, below the header) and in the // task view (4th column, near the due date). Two-state toggle; single source of truth per // mode -- close-mode ON => Done == closed (reads is_active, so native Close/Open stay in // sync); close-mode OFF => a task-metadata marker (organon_done). Server-rendered, no JS. @@ -119,6 +119,9 @@ class Plugin extends Base $this->template->hook->attach('template:board:private:task:before-title', 'organonTweaks:board/done_badge'); $this->template->hook->attach('template:task:details:fourth-column', 'organonTweaks:task/done_badge'); $this->template->hook->attach('template:layout:head', 'organonTweaks:layout/done_style'); + // 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'); } // Auto-managed shared custom filters (v1.5 "Show all tasks", v1.6 month filters, v1.7 @@ -172,7 +175,7 @@ class Plugin extends Base public function getPluginVersion() { - return '2.0.1'; + return '2.1.2'; } public function getPluginHomepage() diff --git a/README.md b/README.md index 31cb9f6..2949d35 100644 --- a/README.md +++ b/README.md @@ -39,12 +39,15 @@ have comments keep showing the count as before. When you drop a card (or Kanboard auto-refreshes the board via polling), it rebuilds the board with `$("#board-container").replaceWith(...)`, and the new element starts scrolled to -the far left -- so the view jumps back to the first column. This tweak remembers the -horizontal scroll position and restores it as soon as the rebuilt board appears, so the -board stays where you were. +the far left -- so the view jumps back to the first column. A **full page reload** (for +example clicking the Done/Todo badge, which navigates and redirects back to the board) does +the same. This tweak remembers the horizontal scroll position and restores it in both cases, +so the board stays where you were. - Implemented in `Asset/js/keep-scroll.js`: it observes the stable parent (the container - itself is replaced), reads/writes `scrollLeft` on whichever `#board-container` is current. + itself is replaced) and restores `scrollLeft` on whichever `#board-container` is current + after an AJAX rebuild, and persists the position per board in `sessionStorage` so it also + survives a full page reload. - **On by default.** Toggle it under "Settings -> Organon Tweaks". ### Open a card only on a quick click @@ -117,12 +120,12 @@ single arrow** (up for ascending, down for descending) when it is ON. (which also hides the native sort menu). Per-column state lives in project metadata. - **On by default.** Toggle it under "Settings -> Organon Tweaks". -### Done/Due badge +### Done/Todo badge -A two-state toggle badge for marking a card done, shown on the board card face (top-right, below the -header) and on the task view (4th column, near the due date). It reads **`[ ] Due`** (dark red) while -the task is pending; one click flips it to **`[x] Done`** (light green). The card itself is not -recolored -- only the badge. The "Due" colors reuse FinanceBuddy's debit badge for consistency. +A two-state toggle badge for marking a card done, shown on the board card face (above the title) and +on the task view (4th column, near the due date). It reads **Todo** (black on light red) while the +task is pending; one click flips it to **Done** (black on light green). The card itself is not +recolored -- only the badge. - Rendered server-side (no JavaScript) via `template:board:private:task:before-title` (`Template/board/done_badge.php`), `template:task:details:fourth-column` @@ -135,6 +138,11 @@ recolored -- only the badge. The "Due" colors reuse FinanceBuddy's debit badge f 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. +- **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 + every task in that column/swimlane to the chosen state (an overwrite, not a per-task toggle), honoring + the same close-mode/marker-mode semantics as the badge. - **Off by default.** Toggle it under "Settings -> Organon Tweaks". ## Settings @@ -165,7 +173,7 @@ the page: **Done badge** -- **Show a Done/Due badge on cards** -- default off. +- **Show a Done/Todo badge on cards** -- default off. - **Marking Done also closes the task** -- default off. Standalone: diff --git a/Template/board/done_badge.php b/Template/board/done_badge.php index 652015c..d41a513 100644 --- a/Template/board/done_badge.php +++ b/Template/board/done_badge.php @@ -1,6 +1,6 @@ OrganonDoneHelper->isDone($task); ?> url->link( - $done ? ' '.t('Done') : ' '.t('Due'), + $done ? ' '.t('Done') : ' '.t('Todo'), 'DoneController', 'toggle', array('plugin' => 'OrganonTweaks', 'task_id' => $task['id'], 'project_id' => $task['project_id'], 'from' => 'board'), diff --git a/Template/board/mark_all_confirm.php b/Template/board/mark_all_confirm.php new file mode 100644 index 0000000..6f7f358 --- /dev/null +++ b/Template/board/mark_all_confirm.php @@ -0,0 +1,35 @@ + + + +

+ +

+ +
+ url->link(t('Mark all Done'), 'DoneController', 'markColumn', array( + 'plugin' => 'OrganonTweaks', + 'project_id' => $project_id, + 'column_id' => $column_id, + 'swimlane_id' => $swimlane_id, + 'direction' => 'done', + ), true, 'btn btn-blue') ?> + + url->link(t('Mark all Todo'), 'DoneController', 'markColumn', array( + 'plugin' => 'OrganonTweaks', + 'project_id' => $project_id, + 'column_id' => $column_id, + 'swimlane_id' => $swimlane_id, + 'direction' => 'todo', + ), true, 'btn btn-red') ?> + + + url->link(t('cancel'), 'BoardViewController', 'show', array('project_id' => $project_id), false, 'close-popover') ?> +
diff --git a/Template/board/mark_all_item.php b/Template/board/mark_all_item.php new file mode 100644 index 0000000..890eeee --- /dev/null +++ b/Template/board/mark_all_item.php @@ -0,0 +1,19 @@ +, so this is hidden and moved into the + * menu by Asset/js/relocate.js. Shown only on non-empty columns to users who may modify tasks. + * Clicking opens a modal (DoneController::confirmColumn) that both confirms and picks the direction. + */ +if ($column['nb_tasks'] <= 0 || ! $this->user->hasProjectAccess('TaskModificationController', 'update', $column['project_id'])) { + return; +} +?> + diff --git a/Template/config/show.php b/Template/config/show.php index 9a8aa33..ef8cc44 100644 --- a/Template/config/show.php +++ b/Template/config/show.php @@ -65,11 +65,11 @@
- form->checkbox('organon_tweaks_done_badge', t('Show a Done/Due badge on cards'), 1, isset($values['organon_tweaks_done_badge']) && $values['organon_tweaks_done_badge'] == 1) ?> -

+ form->checkbox('organon_tweaks_done_badge', t('Show a Done/Todo badge on cards'), 1, isset($values['organon_tweaks_done_badge']) && $values['organon_tweaks_done_badge'] == 1) ?> +

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) ?> -

+

diff --git a/Template/layout/done_style.php b/Template/layout/done_style.php index 294e1d3..aee9886 100644 --- a/Template/layout/done_style.php +++ b/Template/layout/done_style.php @@ -1,9 +1,9 @@ fields. Same * two-state toggle as the board badge; only shown to users who may edit the task. */ @@ -12,7 +12,7 @@ $done = $this->OrganonDoneHelper->isDone($task);
  • url->link( - $done ? ' '.t('Done') : ' '.t('Due'), + $done ? ' '.t('Done') : ' '.t('Todo'), 'DoneController', 'toggle', array('plugin' => 'OrganonTweaks', 'task_id' => $task['id'], 'project_id' => $task['project_id'], 'from' => 'task'), diff --git a/VERSION b/VERSION index bfaccca..8ee9858 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -OrganonTweaks v2.0.1 +OrganonTweaks v2.1.2