Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ddbd4c42a6 | |||
| ca57a4044e | |||
| cc397e9982 | |||
| 8136c01403 | |||
| c88e52ab01 | |||
| f2f690ef98 |
@@ -1,18 +1,108 @@
|
|||||||
/*
|
/*
|
||||||
* OrganonTweaks -- keep the board's horizontal scroll position across refreshes.
|
* OrganonTweaks -- keep the board's scroll position (horizontal AND vertical) across board AJAX
|
||||||
|
* rebuilds and full page reloads.
|
||||||
*
|
*
|
||||||
* When you drop a card (and on Kanboard's periodic AJAX polling) the board is rebuilt with
|
* Two events reset the scroll and jump the board around:
|
||||||
* `$("#board-container").replaceWith(data)` (BoardDragAndDrop.refresh). The brand-new
|
* 1. AJAX rebuild -- dropping a card / periodic polling replaces #board-container
|
||||||
* element starts at scrollLeft 0, so the board snaps back to the first column -- a jarring
|
* (BoardDragAndDrop.refresh); the new element starts at scroll 0.
|
||||||
* jump. This remembers the last scroll position and restores it the moment a replacement
|
* 2. Full page reload -- e.g. clicking the Todo/Done badge or the "Mark all" action, which
|
||||||
* container appears, so the view stays put.
|
* navigate and redirect back to the board; a fresh page starts at scroll 0.
|
||||||
*
|
*
|
||||||
* We observe the STABLE parent (the container itself is replaced) and read the scroll from
|
* Axes restored (all persisted per board in sessionStorage, so they survive a full reload):
|
||||||
* whichever #board-container is current, so it keeps working after every rebuild.
|
* - horizontal: #board-container.scrollLeft (key :hx)
|
||||||
|
* - page vertical (expanded mode): window.scrollY (key :vy)
|
||||||
|
* - per-column vertical (compact mode): each native .board-task-list-compact scrollTop, keyed by
|
||||||
|
* its data-swimlane-id + data-column-id (key :vcol:<sw>:<col>)
|
||||||
|
*
|
||||||
|
* The per-column container is native (core board.css: .board-task-list-compact { overflow-y:auto });
|
||||||
|
* ShrinkVertically only re-tunes its max-height, so this works with or without that plugin. Doing all
|
||||||
|
* axes every time is safe: in expanded mode there are no compact lists (that loop is a no-op) and the
|
||||||
|
* page owns the vertical scroll; in compact mode the columns own it and the page barely moves.
|
||||||
*/
|
*/
|
||||||
(function () {
|
(function () {
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
|
function baseKey() {
|
||||||
|
var m = location.href.match(/board\/(\d+)/) || location.href.match(/project_id=(\d+)/);
|
||||||
|
return "organon-board-scroll-" + (m ? m[1] : location.pathname);
|
||||||
|
}
|
||||||
|
|
||||||
|
function readNum(suffix) {
|
||||||
|
try {
|
||||||
|
var v = window.sessionStorage.getItem(baseKey() + suffix);
|
||||||
|
return v !== null ? parseInt(v, 10) : null;
|
||||||
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeNum(suffix, value) {
|
||||||
|
try {
|
||||||
|
window.sessionStorage.setItem(baseKey() + suffix, value);
|
||||||
|
} catch (e) {
|
||||||
|
// sessionStorage unavailable (private mode / disabled) -- degrade to AJAX-only restore.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// While we restore programmatically, the browser fires scroll events; suppress the save briefly so
|
||||||
|
// a not-yet-scrollable column (whose scrollTop stays 0) does not overwrite the stored position.
|
||||||
|
var suppress = 0;
|
||||||
|
function suppressBriefly() {
|
||||||
|
suppress++;
|
||||||
|
window.setTimeout(function () {
|
||||||
|
if (suppress > 0) {
|
||||||
|
suppress--;
|
||||||
|
}
|
||||||
|
}, 150);
|
||||||
|
}
|
||||||
|
|
||||||
|
function colSuffix(el) {
|
||||||
|
return ":vcol:" + (el.getAttribute("data-swimlane-id") || "0") + ":" + (el.getAttribute("data-column-id") || "0");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore the vertical axes. Called on load, again on the next frame (after ShrinkVertically has
|
||||||
|
// settled the column heights), and on every board rebuild.
|
||||||
|
function restoreVertical() {
|
||||||
|
var y = readNum(":vy");
|
||||||
|
if (y !== null && Math.round(window.scrollY) !== y) {
|
||||||
|
suppressBriefly();
|
||||||
|
window.scrollTo(window.scrollX, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
var lists = document.querySelectorAll("#board .board-task-list-compact");
|
||||||
|
for (var i = 0; i < lists.length; i++) {
|
||||||
|
var el = lists[i];
|
||||||
|
var s = readNum(colSuffix(el));
|
||||||
|
if (s !== null && el.scrollTop !== s) {
|
||||||
|
suppressBriefly();
|
||||||
|
el.scrollTop = s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Core adds the .board-task-list-compact class via JS AFTER load (BoardVerticalScrolling.render),
|
||||||
|
// and re-adds it on every board render -- so the per-column scroll containers often do not exist
|
||||||
|
// yet when we first restore (this is why compact restore worked on F5, where the browser restores
|
||||||
|
// element scroll natively, but not after a badge/redirect navigation). Re-run the vertical restore
|
||||||
|
// whenever the board mutates or a class changes, rAF-coalesced. Idempotent (only sets scrollTop on
|
||||||
|
// a mismatch), so it lands once the compact lists appear and never fights the user afterwards.
|
||||||
|
var restoreScheduled = false;
|
||||||
|
function scheduleRestore() {
|
||||||
|
if (restoreScheduled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
restoreScheduled = true;
|
||||||
|
var run = function () {
|
||||||
|
restoreScheduled = false;
|
||||||
|
restoreVertical();
|
||||||
|
};
|
||||||
|
if (window.requestAnimationFrame) {
|
||||||
|
window.requestAnimationFrame(run);
|
||||||
|
} else {
|
||||||
|
window.setTimeout(run, 16);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function init() {
|
function init() {
|
||||||
var container = document.getElementById("board-container");
|
var container = document.getElementById("board-container");
|
||||||
if (!container) {
|
if (!container) {
|
||||||
@@ -20,30 +110,61 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
var parent = container.parentNode;
|
var parent = container.parentNode;
|
||||||
var lastScrollLeft = container.scrollLeft;
|
|
||||||
var lastContainer = container;
|
|
||||||
|
|
||||||
// Remember the position whenever the board is scrolled (capture: scroll does not
|
// --- horizontal ---
|
||||||
// bubble, and this survives the container being replaced).
|
var storedX = readNum(":hx");
|
||||||
|
var lastScrollLeft = storedX !== null ? storedX : container.scrollLeft;
|
||||||
|
var lastContainer = container;
|
||||||
|
if (storedX !== null && container.scrollLeft !== storedX) {
|
||||||
|
suppressBriefly();
|
||||||
|
container.scrollLeft = storedX;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- vertical (page + per-column), now and again after layout settles ---
|
||||||
|
restoreVertical();
|
||||||
|
if (window.requestAnimationFrame) {
|
||||||
|
window.requestAnimationFrame(restoreVertical);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save on scroll. Capture phase: scroll does not bubble, but a capture listener on document
|
||||||
|
// still receives it from #board-container and from any per-column list.
|
||||||
document.addEventListener("scroll", function (e) {
|
document.addEventListener("scroll", function (e) {
|
||||||
|
if (suppress > 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var t = e.target;
|
||||||
var c = document.getElementById("board-container");
|
var c = document.getElementById("board-container");
|
||||||
if (c && e.target === c) {
|
if (t === c) {
|
||||||
lastScrollLeft = c.scrollLeft;
|
lastScrollLeft = c.scrollLeft;
|
||||||
|
writeNum(":hx", c.scrollLeft);
|
||||||
|
} else if (t && t.nodeType === 1 && t.classList && t.classList.contains("board-task-list-compact")) {
|
||||||
|
writeNum(colSuffix(t), t.scrollTop);
|
||||||
}
|
}
|
||||||
}, true);
|
}, true);
|
||||||
|
|
||||||
// When the board is rebuilt, #board-container becomes a new element at scrollLeft 0;
|
// Page vertical scroll targets the document, so listen on window.
|
||||||
// restore the remembered position before the browser paints it.
|
window.addEventListener("scroll", function () {
|
||||||
|
if (suppress === 0) {
|
||||||
|
writeNum(":vy", Math.round(window.scrollY));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Observe the STABLE parent for: (a) #board-container being replaced on an AJAX rebuild
|
||||||
|
// (childList) -> restore horizontal; and (b) class changes anywhere below (attributes) -> the
|
||||||
|
// moment core adds .board-task-list-compact, restore the per-column verticals. subtree covers
|
||||||
|
// the deep .board-task-list elements. Both funnel into the rAF-coalesced scheduleRestore.
|
||||||
if (window.MutationObserver) {
|
if (window.MutationObserver) {
|
||||||
new MutationObserver(function () {
|
new MutationObserver(function () {
|
||||||
var c = document.getElementById("board-container");
|
var c = document.getElementById("board-container");
|
||||||
if (c && c !== lastContainer) {
|
if (c && c !== lastContainer) {
|
||||||
lastContainer = c;
|
lastContainer = c;
|
||||||
if (c.scrollLeft !== lastScrollLeft) {
|
if (c.scrollLeft !== lastScrollLeft) {
|
||||||
|
suppressBriefly();
|
||||||
c.scrollLeft = lastScrollLeft;
|
c.scrollLeft = lastScrollLeft;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}).observe(parent, { childList: true });
|
scheduleRestore();
|
||||||
|
}).observe(parent, { childList: true, subtree: true, attributes: true, attributeFilter: ["class"] });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
function relocateColumnMenuItems() {
|
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++) {
|
for (var i = 0; i < items.length; i++) {
|
||||||
var li = items[i];
|
var li = items[i];
|
||||||
|
|||||||
57
Asset/js/sortable-handle-fix.js
Normal file
57
Asset/js/sortable-handle-fix.js
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
/*
|
||||||
|
* OrganonTweaks -- fix a core Kanboard touch bug on reorder tables.
|
||||||
|
*
|
||||||
|
* Kanboard inits the subtask / board-column / swimlane reorder sortables with
|
||||||
|
* handle:"td:first i", which matches EVERY <i> in a row's first cell -- so the gear/caret menu
|
||||||
|
* icons and the subtask status checkbox become drag handles too. On touch devices the bundled
|
||||||
|
* jQuery UI Touch Punch then captures the press on those icons, preventDefault()s the native
|
||||||
|
* tap-click, and only re-fires a click if the finger did not move at all -- so almost every real
|
||||||
|
* tap is swallowed and the menu never opens (the gear "drags" instead of opening).
|
||||||
|
*
|
||||||
|
* Fix: re-scope the handle option to the real drag icon (.draggable-row-handle) on every such
|
||||||
|
* sortable, so only the arrows drag and the other first-cell icons are plain clicks again.
|
||||||
|
* jQuery UI reads options.handle at press time, so this takes effect immediately, no re-init.
|
||||||
|
* Kanboard re-inits the sortable whenever it re-renders a table (subtask add/edit, column
|
||||||
|
* reorder, and so on), which re-applies the bad handle -- so we re-apply the fix after every
|
||||||
|
* render via a debounced MutationObserver. Always on (no setting). Harmless on desktop, where
|
||||||
|
* the mouse never takes the Touch Punch path.
|
||||||
|
*/
|
||||||
|
(function () {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
var $j = window.jQuery;
|
||||||
|
if (! $j) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
function fixHandles() {
|
||||||
|
$j(".ui-sortable").each(function () {
|
||||||
|
try {
|
||||||
|
var $s = $j(this);
|
||||||
|
if ($s.sortable("option", "handle") === "td:first i") {
|
||||||
|
$s.sortable("option", "handle", ".draggable-row-handle");
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// element is not an initialized sortable -- skip it
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var pending = false;
|
||||||
|
function schedule() {
|
||||||
|
if (pending) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
pending = true;
|
||||||
|
window.setTimeout(function () {
|
||||||
|
pending = false;
|
||||||
|
fixHandles();
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
$j(fixHandles); // initial pass on DOM ready
|
||||||
|
new MutationObserver(schedule).observe(document.body, {
|
||||||
|
childList: true,
|
||||||
|
subtree: true
|
||||||
|
});
|
||||||
|
})();
|
||||||
@@ -25,6 +25,9 @@ class ConfigController extends BaseController
|
|||||||
'organon_tweaks_shortcut_labels' => (int) $this->configModel->get('organon_tweaks_shortcut_labels', 1),
|
'organon_tweaks_shortcut_labels' => (int) $this->configModel->get('organon_tweaks_shortcut_labels', 1),
|
||||||
'organon_tweaks_title_click_edit' => (int) $this->configModel->get('organon_tweaks_title_click_edit', 1),
|
'organon_tweaks_title_click_edit' => (int) $this->configModel->get('organon_tweaks_title_click_edit', 1),
|
||||||
'organon_tweaks_persistent_sort' => (int) $this->configModel->get('organon_tweaks_persistent_sort', 1),
|
'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(),
|
'errors' => array(),
|
||||||
)));
|
)));
|
||||||
@@ -45,6 +48,9 @@ class ConfigController extends BaseController
|
|||||||
$shortcutLabels = isset($values['organon_tweaks_shortcut_labels']) ? 1 : 0;
|
$shortcutLabels = isset($values['organon_tweaks_shortcut_labels']) ? 1 : 0;
|
||||||
$titleClickEdit = isset($values['organon_tweaks_title_click_edit']) ? 1 : 0;
|
$titleClickEdit = isset($values['organon_tweaks_title_click_edit']) ? 1 : 0;
|
||||||
$persistentSort = isset($values['organon_tweaks_persistent_sort']) ? 1 : 0;
|
$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(
|
if ($this->configModel->save(array(
|
||||||
'organon_tweaks_always_comment_icon' => $alwaysCommentIcon,
|
'organon_tweaks_always_comment_icon' => $alwaysCommentIcon,
|
||||||
@@ -58,6 +64,9 @@ class ConfigController extends BaseController
|
|||||||
'organon_tweaks_shortcut_labels' => $shortcutLabels,
|
'organon_tweaks_shortcut_labels' => $shortcutLabels,
|
||||||
'organon_tweaks_title_click_edit' => $titleClickEdit,
|
'organon_tweaks_title_click_edit' => $titleClickEdit,
|
||||||
'organon_tweaks_persistent_sort' => $persistentSort,
|
'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.'));
|
$this->flash->success(t('Settings saved successfully.'));
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
118
Controller/DoneController.php
Normal file
118
Controller/DoneController.php
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
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/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
|
||||||
|
{
|
||||||
|
public function toggle()
|
||||||
|
{
|
||||||
|
$task = $this->getTask();
|
||||||
|
$this->checkCSRFParam();
|
||||||
|
|
||||||
|
$helper = new OrganonDoneHelper($this->container);
|
||||||
|
$done = $helper->isDone($task);
|
||||||
|
|
||||||
|
if ($helper->closesTask()) {
|
||||||
|
if ($done) {
|
||||||
|
$this->taskStatusModel->open($task['id']);
|
||||||
|
} else {
|
||||||
|
$this->taskStatusModel->close($task['id']);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if ($done) {
|
||||||
|
$this->taskMetadataModel->remove($task['id'], OrganonDoneHelper::DONE_KEY);
|
||||||
|
} else {
|
||||||
|
$this->taskMetadataModel->save($task['id'], array(OrganonDoneHelper::DONE_KEY => 'on'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->request->getStringParam('from') === 'board') {
|
||||||
|
$this->response->redirect($this->helper->url->to('BoardViewController', 'show', array('project_id' => $task['project_id'])), true);
|
||||||
|
} else {
|
||||||
|
$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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
112
Helper/OrganonDoneHelper.php
Normal file
112
Helper/OrganonDoneHelper.php
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Kanboard\Plugin\OrganonTweaks\Helper;
|
||||||
|
|
||||||
|
use Kanboard\Core\Base;
|
||||||
|
use Kanboard\Model\SubtaskModel;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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).
|
||||||
|
* The badge reads the task's own is_active, so however a task is closed/reopened (badge, native
|
||||||
|
* sidebar, bulk, API) the badge always matches. No metadata is stored in this mode.
|
||||||
|
* - close-mode off (marker only): Done == the task metadata DONE_KEY == 'on', independent of the
|
||||||
|
* open/closed status. Stored as the non-falsy string 'on' (never '0'/'1'): MetadataModel::get()
|
||||||
|
* uses `?:` and PHP treats "0" as falsy, so a stored "0" would read back as the default.
|
||||||
|
*
|
||||||
|
* Templates cannot read models directly, so the board/task-view badge templates call isDone() here,
|
||||||
|
* passing the task array (which carries both id and is_active) so the close-mode read needs no query.
|
||||||
|
*/
|
||||||
|
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)
|
||||||
|
*
|
||||||
|
* @param array $task a task array carrying 'id' and 'is_active'
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function isDone(array $task)
|
||||||
|
{
|
||||||
|
if ($this->closesTask()) {
|
||||||
|
return isset($task['is_active']) && (int) $task['is_active'] === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->taskMetadataModel->get((int) $task['id'], self::DONE_KEY, '') === 'on';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Should toggling Done also close (and reopen) the task? Global setting, default off.
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function closesTask()
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
49
Plugin.php
49
Plugin.php
@@ -3,6 +3,7 @@
|
|||||||
namespace Kanboard\Plugin\OrganonTweaks;
|
namespace Kanboard\Plugin\OrganonTweaks;
|
||||||
|
|
||||||
use Kanboard\Core\Plugin\Base;
|
use Kanboard\Core\Plugin\Base;
|
||||||
|
use Kanboard\Model\SubtaskModel;
|
||||||
use Kanboard\Model\TaskModel;
|
use Kanboard\Model\TaskModel;
|
||||||
|
|
||||||
class Plugin extends Base
|
class Plugin extends Base
|
||||||
@@ -18,6 +19,15 @@ class Plugin extends Base
|
|||||||
'template' => 'plugins/OrganonTweaks/Asset/js/relocate.js',
|
'template' => 'plugins/OrganonTweaks/Asset/js/relocate.js',
|
||||||
));
|
));
|
||||||
|
|
||||||
|
// Fix a core Kanboard touch bug: the subtask / board-column / swimlane reorder sortables
|
||||||
|
// use handle:"td:first i", which makes the gear/caret menu icons and the subtask status
|
||||||
|
// checkbox (all <i> in the first cell) drag handles too, so on touch devices Touch Punch
|
||||||
|
// swallows their taps -- the gear "drags" instead of opening. Re-scope the handle to the
|
||||||
|
// real drag icon (.draggable-row-handle) in JS. Always on; harmless on desktop.
|
||||||
|
$this->hook->on('template:layout:js', array(
|
||||||
|
'template' => 'plugins/OrganonTweaks/Asset/js/sortable-handle-fix.js',
|
||||||
|
));
|
||||||
|
|
||||||
// Settings page for the plugin's tweaks.
|
// Settings page for the plugin's tweaks.
|
||||||
$this->template->hook->attach('template:config:sidebar', 'organonTweaks:config/sidebar');
|
$this->template->hook->attach('template:config:sidebar', 'organonTweaks:config/sidebar');
|
||||||
|
|
||||||
@@ -111,6 +121,41 @@ class Plugin extends Base
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
if ((int) $this->configModel->get('organon_tweaks_done_badge', 0) === 1) {
|
||||||
|
$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');
|
||||||
|
|
||||||
|
// 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
|
// Auto-managed shared custom filters (v1.5 "Show all tasks", v1.6 month filters, v1.7
|
||||||
// recurring filters). Ensured lazily whenever a project header renders (covers old + new
|
// recurring filters). Ensured lazily whenever a project header renders (covers old + new
|
||||||
// boards); each group is gated by its own config toggle inside
|
// boards); each group is gated by its own config toggle inside
|
||||||
@@ -141,7 +186,7 @@ class Plugin extends Base
|
|||||||
public function getHelpers()
|
public function getHelpers()
|
||||||
{
|
{
|
||||||
return array(
|
return array(
|
||||||
'Plugin\OrganonTweaks\Helper' => array('OrganonColumnHelper', 'OrganonProjectHelper', 'OrganonSortHelper'),
|
'Plugin\OrganonTweaks\Helper' => array('OrganonColumnHelper', 'OrganonProjectHelper', 'OrganonSortHelper', 'OrganonDoneHelper'),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -162,7 +207,7 @@ class Plugin extends Base
|
|||||||
|
|
||||||
public function getPluginVersion()
|
public function getPluginVersion()
|
||||||
{
|
{
|
||||||
return '1.9.1';
|
return '2.3.0';
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getPluginHomepage()
|
public function getPluginHomepage()
|
||||||
|
|||||||
70
README.md
70
README.md
@@ -39,12 +39,19 @@ have comments keep showing the count as before.
|
|||||||
|
|
||||||
When you drop a card (or Kanboard auto-refreshes the board via polling), it rebuilds the
|
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
|
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
|
the far left -- so the view jumps back to the first column. A **full page reload** (for
|
||||||
horizontal scroll position and restores it as soon as the rebuilt board appears, so the
|
example clicking the Todo/Done badge or the "Mark all" action, which navigate and redirect back to
|
||||||
board stays where you were.
|
the board) does the same. This tweak remembers the scroll position -- **horizontal and vertical** --
|
||||||
|
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
|
- Implemented in `Asset/js/keep-scroll.js`: it observes the stable parent (the container itself is
|
||||||
itself is replaced), reads/writes `scrollLeft` on whichever `#board-container` is current.
|
replaced) and restores, on whichever `#board-container` is current after an AJAX rebuild and again
|
||||||
|
after a full reload (persisted per board in `sessionStorage`):
|
||||||
|
- the **horizontal** position (`#board-container.scrollLeft`);
|
||||||
|
- the **page vertical** position in expanded mode (`window.scrollY`);
|
||||||
|
- the **per-column vertical** position in compact/collapsed mode -- each native
|
||||||
|
`.board-task-list-compact` list, keyed by its `data-swimlane-id` + `data-column-id`, so every
|
||||||
|
column keeps its own place independently (works with or without ShrinkVertically).
|
||||||
- **On by default.** Toggle it under "Settings -> Organon Tweaks".
|
- **On by default.** Toggle it under "Settings -> Organon Tweaks".
|
||||||
|
|
||||||
### Open a card only on a quick click
|
### Open a card only on a quick click
|
||||||
@@ -117,6 +124,54 @@ 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.
|
(which also hides the native sort menu). Per-column state lives in project metadata.
|
||||||
- **On by default.** Toggle it under "Settings -> Organon Tweaks".
|
- **On by default.** Toggle it under "Settings -> Organon Tweaks".
|
||||||
|
|
||||||
|
### Done/Todo badge
|
||||||
|
|
||||||
|
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`
|
||||||
|
(`Template/task/done_badge.php`), and a small head `<style>` (`Template/layout/done_style.php`).
|
||||||
|
Shown only to users who may edit the task; on the board it appears on expanded (non-collapsed) cards.
|
||||||
|
- **Single source of truth, no drift with native Close/Open.** With *"Marking Done also closes the
|
||||||
|
task"* **on**, "Done" *means* the task is closed: the badge reads the task's own open/closed status,
|
||||||
|
so closing or reopening a task by any route (the badge, the native sidebar, bulk actions, the API)
|
||||||
|
always keeps the badge correct. A closed "Done" card is hidden from the board by Kanboard's default
|
||||||
|
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
|
||||||
|
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".
|
||||||
|
|
||||||
|
### Fix the subtask / column / swimlane menus on touch devices
|
||||||
|
|
||||||
|
On phones and tablets the little **gear menu** (edit / remove / convert) on a subtask row -- and the
|
||||||
|
same gear on the board **Columns** and **Swimlanes** config tables -- was almost impossible to tap:
|
||||||
|
roughly one tap in fifty opened it, while dragging to reorder worked fine. The cause is an upstream
|
||||||
|
Kanboard bug: those reorder tables set the drag handle to *every* icon in the row's first cell
|
||||||
|
(`handle: "td:first i"`), so the gear, its caret and the subtask status checkbox all count as drag
|
||||||
|
handles. On touch, Kanboard's bundled jQuery UI Touch Punch then treats a tap on them as a drag and
|
||||||
|
swallows the click. Desktop (mouse) is unaffected.
|
||||||
|
|
||||||
|
- `Asset/js/sortable-handle-fix.js` re-scopes those sortables' `handle` to the real drag icon
|
||||||
|
(`.draggable-row-handle`), so only the four-arrows drag and the gear/caret/checkbox are plain taps
|
||||||
|
again -- reordering still works. It re-applies after Kanboard re-renders a table (a debounced
|
||||||
|
MutationObserver), and only touches sortables whose handle is the buggy `td:first i`, so it is inert
|
||||||
|
everywhere else.
|
||||||
|
- **Always on** (no setting) -- it only corrects a broken interaction and does nothing on desktop.
|
||||||
|
|
||||||
## Settings
|
## Settings
|
||||||
|
|
||||||
Global (per Kanboard instance) and admin-only, under "Settings -> Organon Tweaks", grouped as on
|
Global (per Kanboard instance) and admin-only, under "Settings -> Organon Tweaks", grouped as on
|
||||||
@@ -143,6 +198,11 @@ the page:
|
|||||||
|
|
||||||
- **Per-column persistent sort** -- default on.
|
- **Per-column persistent sort** -- default on.
|
||||||
|
|
||||||
|
**Done badge**
|
||||||
|
|
||||||
|
- **Show a Done/Todo badge on cards** -- default off.
|
||||||
|
- **Marking Done also closes the task** -- default off.
|
||||||
|
|
||||||
Standalone:
|
Standalone:
|
||||||
|
|
||||||
- **Keep the board scroll position across refreshes** -- default on.
|
- **Keep the board scroll position across refreshes** -- default on.
|
||||||
|
|||||||
23
Template/board/done_badge.php
Normal file
23
Template/board/done_badge.php
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Done/Todo badge on the board card face. Rendered by template:board:private:task:before-title; the
|
||||||
|
* CSS in layout/done_style.php pins it to the top-right (below the header) and colors the two states.
|
||||||
|
* Only shown to users who may edit the task (a read-only viewer sees no badge). Expanded cards only
|
||||||
|
* (collapsed cards do not fire this hook).
|
||||||
|
*/
|
||||||
|
if (! $this->user->hasProjectAccess('TaskModificationController', 'edit', $task['project_id'])) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$done = $this->OrganonDoneHelper->isDone($task);
|
||||||
|
?>
|
||||||
|
<span class="organontweaks-done<?= $done ? ' is-done' : '' ?>">
|
||||||
|
<?= $this->url->link(
|
||||||
|
$done ? '<i class="fa fa-check-square"></i> '.t('Done') : '<i class="fa fa-square"></i> '.t('Todo'),
|
||||||
|
'DoneController',
|
||||||
|
'toggle',
|
||||||
|
array('plugin' => 'OrganonTweaks', 'task_id' => $task['id'], 'project_id' => $task['project_id'], 'from' => 'board'),
|
||||||
|
true,
|
||||||
|
'',
|
||||||
|
$done ? t('Mark as not done') : t('Mark as done')
|
||||||
|
) ?>
|
||||||
|
</span>
|
||||||
35
Template/board/mark_all_confirm.php
Normal file
35
Template/board/mark_all_confirm.php
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* The "Mark all as Done/Todo" modal: one modal serving both purposes -- confirm and pick direction.
|
||||||
|
* Two buttons set every task in the column to the same state; cancel closes the modal. Each button is
|
||||||
|
* a CSRF link to DoneController::markColumn.
|
||||||
|
*/
|
||||||
|
?>
|
||||||
|
<div class="page-header">
|
||||||
|
<h2><?= t('Mark all tasks in this column') ?></h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="confirm">
|
||||||
|
<?= t('Set every task in this column to the same state. Already-done tasks are overwritten (no harm).') ?>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="form-actions">
|
||||||
|
<?= $this->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') ?>
|
||||||
|
|
||||||
|
<?= $this->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') ?>
|
||||||
|
|
||||||
|
<?= t('or') ?>
|
||||||
|
<?= $this->url->link(t('cancel'), 'BoardViewController', 'show', array('project_id' => $project_id), false, 'close-popover') ?>
|
||||||
|
</div>
|
||||||
19
Template/board/mark_all_item.php
Normal file
19
Template/board/mark_all_item.php
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* "Mark all as Done/Todo" entry in the board column header dropdown. Like the "Remove this Column"
|
||||||
|
* item, the column hook renders outside the native menu <ul>, 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;
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<li class="organontweaks-markall-item" style="display: none;">
|
||||||
|
<?= $this->modal->medium('check-square-o', t('Mark all as Done/Todo'), 'DoneController', 'confirmColumn', array(
|
||||||
|
'plugin' => 'OrganonTweaks',
|
||||||
|
'project_id' => $column['project_id'],
|
||||||
|
'column_id' => $column['id'],
|
||||||
|
'swimlane_id' => $swimlane['id'],
|
||||||
|
)) ?>
|
||||||
|
</li>
|
||||||
@@ -62,6 +62,19 @@
|
|||||||
<p class="form-help"><?= t('Replaces the native column sort with a control that adds a number-aware Title sort and a "Persistent sort: ON/OFF" toggle. When ON, the column is re-sorted whenever a card is dropped in or created, so it stays sorted (a red up/down arrow marks it). OFF is the native one-shot sort.') ?></p>
|
<p class="form-help"><?= t('Replaces the native column sort with a control that adds a number-aware Title sort and a "Persistent sort: ON/OFF" toggle. When ON, the column is re-sorted whenever a card is dropped in or created, so it stays sorted (a red up/down arrow marks it). OFF is the native one-shot sort.') ?></p>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
|
<fieldset>
|
||||||
|
<legend><?= t('Done badge') ?></legend>
|
||||||
|
|
||||||
|
<?= $this->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) ?>
|
||||||
|
<p class="form-help"><?= t('A two-state toggle badge on the board card (above the title) and the task view (near the due date): "Todo" (red) until you click it, then "Done" (green). Off by default.') ?></p>
|
||||||
|
|
||||||
|
<?= $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) ?>
|
||||||
|
<p class="form-help"><?= 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.') ?></p>
|
||||||
|
|
||||||
|
<?= $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) ?>
|
||||||
|
<p class="form-help"><?= 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).') ?></p>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
<div class="form-actions">
|
<div class="form-actions">
|
||||||
<button type="submit" class="btn btn-blue"><?= t('Save') ?></button>
|
<button type="submit" class="btn btn-blue"><?= t('Save') ?></button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
29
Template/layout/done_style.php
Normal file
29
Template/layout/done_style.php
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Done/Todo badge styling (emitted into the head when the feature is enabled). NATURAL document flow --
|
||||||
|
* the badge renders above the title (board card) or as a list item (task view); no absolute
|
||||||
|
* positioning, so it adapts to any card / column / avatar / username size. Two states via .is-done:
|
||||||
|
* Todo = black filled box + black text on a light red (#ef9a9a); Done = checked box, black on light
|
||||||
|
* green (#a5d6a7). Both states use black text (core's `.task-board a { color:#000 }` forces the icon
|
||||||
|
* black anyway, so the backgrounds are kept light for contrast). Colors are tunable.
|
||||||
|
*/
|
||||||
|
?>
|
||||||
|
<style>
|
||||||
|
span.organontweaks-done a {
|
||||||
|
display: inline-block;
|
||||||
|
text-decoration: none;
|
||||||
|
padding: 3px 10px;
|
||||||
|
border-radius: 3px;
|
||||||
|
background: #ef9a9a;
|
||||||
|
color: #000;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
span.organontweaks-done.is-done a {
|
||||||
|
background: #a5d6a7;
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
.organontweaks-done {
|
||||||
|
display: inline-block;
|
||||||
|
margin: 1px 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
24
Template/task/done_badge.php
Normal file
24
Template/task/done_badge.php
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Done/Todo badge in the task view, 4th column (near the due date). Rendered by
|
||||||
|
* template:task:details:fourth-column as a list item, matching the surrounding <li> fields. Same
|
||||||
|
* two-state toggle as the board badge; only shown to users who may edit the task.
|
||||||
|
*/
|
||||||
|
if (! $this->user->hasProjectAccess('TaskModificationController', 'edit', $task['project_id'])) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$done = $this->OrganonDoneHelper->isDone($task);
|
||||||
|
?>
|
||||||
|
<li>
|
||||||
|
<span class="organontweaks-done<?= $done ? ' is-done' : '' ?>">
|
||||||
|
<?= $this->url->link(
|
||||||
|
$done ? '<i class="fa fa-check-square"></i> '.t('Done') : '<i class="fa fa-square"></i> '.t('Todo'),
|
||||||
|
'DoneController',
|
||||||
|
'toggle',
|
||||||
|
array('plugin' => 'OrganonTweaks', 'task_id' => $task['id'], 'project_id' => $task['project_id'], 'from' => 'task'),
|
||||||
|
true,
|
||||||
|
'',
|
||||||
|
$done ? t('Mark as not done') : t('Mark as done')
|
||||||
|
) ?>
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
Reference in New Issue
Block a user