3 Commits

8 changed files with 312 additions and 37 deletions

View File

@@ -1,41 +1,108 @@
/* /*
* OrganonTweaks -- keep the board's horizontal scroll position across refreshes AND full reloads. * OrganonTweaks -- keep the board's scroll position (horizontal AND vertical) across board AJAX
* rebuilds and full page reloads.
* *
* Two cases lose the horizontal scroll and snap the board back to the first column: * Two events reset the scroll and jump the board around:
* 1. AJAX rebuild -- dropping a card / periodic polling replaces #board-container * 1. AJAX rebuild -- dropping a card / periodic polling replaces #board-container
* (BoardDragAndDrop.refresh), and the new element starts at scrollLeft 0. * (BoardDragAndDrop.refresh); the new element starts at scroll 0.
* 2. Full page reload -- e.g. clicking the Done/Todo badge, which navigates and redirects back to * 2. Full page reload -- e.g. clicking the Todo/Done badge or the "Mark all" action, which
* the board; a fresh page starts at scrollLeft 0. * navigate and redirect back to the board; a fresh page starts at scroll 0.
* *
* We remember the last position and restore it: in-memory for the AJAX rebuild (observing the STABLE * Axes restored (all persisted per board in sessionStorage, so they survive a full reload):
* parent, since the container itself is replaced), and in sessionStorage (keyed per board) so it * - horizontal: #board-container.scrollLeft (key :hx)
* survives a full reload as well. * - 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 boardKey() { function baseKey() {
var m = location.href.match(/board\/(\d+)/) || location.href.match(/project_id=(\d+)/); var m = location.href.match(/board\/(\d+)/) || location.href.match(/project_id=(\d+)/);
return "organon-board-scroll-" + (m ? m[1] : location.pathname); return "organon-board-scroll-" + (m ? m[1] : location.pathname);
} }
function readStored() { function readNum(suffix) {
try { try {
var v = window.sessionStorage.getItem(boardKey()); var v = window.sessionStorage.getItem(baseKey() + suffix);
return v ? parseInt(v, 10) : 0; return v !== null ? parseInt(v, 10) : null;
} catch (e) { } catch (e) {
return 0; return null;
} }
} }
function writeStored(value) { function writeNum(suffix, value) {
try { try {
window.sessionStorage.setItem(boardKey(), value); window.sessionStorage.setItem(baseKey() + suffix, value);
} catch (e) { } catch (e) {
// sessionStorage unavailable (private mode / disabled) -- degrade to AJAX-only restore. // 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) {
@@ -43,37 +110,61 @@
} }
var parent = container.parentNode; var parent = container.parentNode;
var stored = readStored();
var lastScrollLeft = stored || container.scrollLeft;
var lastContainer = container;
// Restore across a full page reload (badge click, etc.), not only AJAX rebuilds. // --- horizontal ---
if (stored && container.scrollLeft !== stored) { var storedX = readNum(":hx");
container.scrollLeft = stored; var lastScrollLeft = storedX !== null ? storedX : container.scrollLeft;
var lastContainer = container;
if (storedX !== null && container.scrollLeft !== storedX) {
suppressBriefly();
container.scrollLeft = storedX;
} }
// Remember the position whenever the board is scrolled (capture: scroll does not bubble, and // --- vertical (page + per-column), now and again after layout settles ---
// this survives the container being replaced). 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;
writeStored(lastScrollLeft); 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; restore // Page vertical scroll targets the document, so listen on window.
// 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"] });
} }
} }

View 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
});
})();

View File

@@ -27,6 +27,7 @@ class ConfigController extends BaseController
'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_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_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(),
))); )));
@@ -49,6 +50,7 @@ class ConfigController extends BaseController
$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; $doneBadge = isset($values['organon_tweaks_done_badge']) ? 1 : 0;
$doneClosesTask = isset($values['organon_tweaks_done_closes_task']) ? 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,
@@ -64,6 +66,7 @@ class ConfigController extends BaseController
'organon_tweaks_persistent_sort' => $persistentSort, 'organon_tweaks_persistent_sort' => $persistentSort,
'organon_tweaks_done_badge' => $doneBadge, 'organon_tweaks_done_badge' => $doneBadge,
'organon_tweaks_done_closes_task' => $doneClosesTask, '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 {

View File

@@ -3,6 +3,7 @@
namespace Kanboard\Plugin\OrganonTweaks\Helper; namespace Kanboard\Plugin\OrganonTweaks\Helper;
use Kanboard\Core\Base; use Kanboard\Core\Base;
use Kanboard\Model\SubtaskModel;
/** /**
* Done/Todo badge helper. * Done/Todo badge helper.
@@ -22,6 +23,8 @@ class OrganonDoneHelper extends Base
{ {
const DONE_KEY = 'organon_done'; const DONE_KEY = 'organon_done';
const CLOSES_KEY = 'organon_tweaks_done_closes_task'; 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) * 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; 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);
}
}
} }

View File

@@ -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');
@@ -122,6 +132,28 @@ class Plugin extends Base
// Bulk "Mark all as Done/Todo" entry in the column header dropdown (moved into the native // 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. // 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'); $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
@@ -175,7 +207,7 @@ class Plugin extends Base
public function getPluginVersion() public function getPluginVersion()
{ {
return '2.1.2'; return '2.3.0';
} }
public function getPluginHomepage() public function getPluginHomepage()

View File

@@ -40,14 +40,18 @@ 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. A **full page reload** (for 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 example clicking the Todo/Done badge or the "Mark all" action, which navigate and redirect back to
the same. This tweak remembers the horizontal scroll position and restores it in both cases, the board) does the same. This tweak remembers the scroll position -- **horizontal and vertical** --
so the board stays where you were. 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) and restores `scrollLeft` on whichever `#board-container` is current replaced) and restores, on whichever `#board-container` is current after an AJAX rebuild and again
after an AJAX rebuild, and persists the position per board in `sessionStorage` so it also after a full reload (persisted per board in `sessionStorage`):
survives a full page reload. - 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
@@ -138,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). 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 - With that option **off**, the badge is an independent marker stored in task metadata
(`organon_done`) that never touches the open/closed status. (`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 - **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 (`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 that both confirms and picks the direction -- **Mark all Done** or **Mark all Todo** -- then sets
@@ -145,6 +155,23 @@ recolored -- only the badge.
the same close-mode/marker-mode semantics as the badge. the same close-mode/marker-mode semantics as the badge.
- **Off by default.** Toggle it under "Settings -> Organon Tweaks". - **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

View File

@@ -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) ?> <?= $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> <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> </fieldset>
<div class="form-actions"> <div class="form-actions">

View File

@@ -1 +1 @@
OrganonTweaks v2.1.2 OrganonTweaks v2.3.0