2 Commits

7 changed files with 182 additions and 2 deletions

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_done_badge' => (int) $this->configModel->get('organon_tweaks_done_badge', 0),
'organon_tweaks_done_closes_task' => (int) $this->configModel->get('organon_tweaks_done_closes_task', 0),
'organon_tweaks_done_autosubtasks' => (int) $this->configModel->get('organon_tweaks_done_autosubtasks', 0),
),
'errors' => array(),
)));
@@ -49,6 +50,7 @@ class ConfigController extends BaseController
$persistentSort = isset($values['organon_tweaks_persistent_sort']) ? 1 : 0;
$doneBadge = isset($values['organon_tweaks_done_badge']) ? 1 : 0;
$doneClosesTask = isset($values['organon_tweaks_done_closes_task']) ? 1 : 0;
$doneAutoSubtasks = isset($values['organon_tweaks_done_autosubtasks']) ? 1 : 0;
if ($this->configModel->save(array(
'organon_tweaks_always_comment_icon' => $alwaysCommentIcon,
@@ -64,6 +66,7 @@ class ConfigController extends BaseController
'organon_tweaks_persistent_sort' => $persistentSort,
'organon_tweaks_done_badge' => $doneBadge,
'organon_tweaks_done_closes_task' => $doneClosesTask,
'organon_tweaks_done_autosubtasks' => $doneAutoSubtasks,
))) {
$this->flash->success(t('Settings saved successfully.'));
} else {

View File

@@ -3,6 +3,7 @@
namespace Kanboard\Plugin\OrganonTweaks\Helper;
use Kanboard\Core\Base;
use Kanboard\Model\SubtaskModel;
/**
* Done/Todo badge helper.
@@ -22,6 +23,8 @@ class OrganonDoneHelper extends Base
{
const DONE_KEY = 'organon_done';
const CLOSES_KEY = 'organon_tweaks_done_closes_task';
const AUTOSUB_KEY = 'organon_tweaks_done_autosubtasks';
const ALLDONE_KEY = 'organon_subtasks_alldone';
/**
* Is this task Done? (mode-aware: closed status in close-mode, else the metadata marker)
@@ -47,4 +50,63 @@ class OrganonDoneHelper extends Base
{
return (int) $this->configModel->get(self::CLOSES_KEY, 0) === 1;
}
/**
* Mark a task Done in the current mode. Idempotent -- a no-op if it is already Done. Close-mode
* closes the task; marker-mode sets the DONE_KEY metadata.
*
* @param int $task_id
*/
public function markDone($task_id)
{
$task = $this->taskFinderModel->getById((int) $task_id);
if (empty($task) || $this->isDone($task)) {
return;
}
if ($this->closesTask()) {
$this->taskStatusModel->close((int) $task_id);
} else {
$this->taskMetadataModel->save((int) $task_id, array(self::DONE_KEY => 'on'));
}
}
/**
* Recompute whether the task's subtasks are ALL done and, only on the up-transition
* (was-not-all-done -> now-all-done), auto-mark the task Done. The last-seen all-done state is kept
* in ALLDONE_KEY ('on'/'off') to detect that edge -- so a manual unmark is honored (nothing
* re-fires) and title edits / refreshes never re-mark. Never auto-unmarks.
*
* @param int $task_id
* @param int $exclude_id a subtask id to exclude from the counts (the row being deleted --
* EVENT_DELETE fires BEFORE the row is removed, so it still counts)
*/
public function syncSubtasksDone($task_id, $exclude_id = 0)
{
$task_id = (int) $task_id;
$exclude_id = (int) $exclude_id;
$totalQuery = $this->db->table(SubtaskModel::TABLE)->eq('task_id', $task_id);
$doneQuery = $this->db->table(SubtaskModel::TABLE)->eq('task_id', $task_id)->eq('status', SubtaskModel::STATUS_DONE);
if ($exclude_id > 0) {
$totalQuery->neq('id', $exclude_id);
$doneQuery->neq('id', $exclude_id);
}
$total = $totalQuery->count();
$done = $doneQuery->count();
$allDone = $total > 0 && $done === $total;
$prev = $this->taskMetadataModel->get($task_id, self::ALLDONE_KEY, 'off') === 'on';
// Save the new state BEFORE marking: in close-mode markDone() -> close() -> closeAll() re-fires
// subtask events into this method; with the marker already 'on', that re-entry sees no
// transition and is a no-op (no loop).
$this->taskMetadataModel->save($task_id, array(self::ALLDONE_KEY => $allDone ? 'on' : 'off'));
if ($allDone && ! $prev) {
$this->markDone($task_id);
}
}
}

View File

@@ -3,6 +3,7 @@
namespace Kanboard\Plugin\OrganonTweaks;
use Kanboard\Core\Plugin\Base;
use Kanboard\Model\SubtaskModel;
use Kanboard\Model\TaskModel;
class Plugin extends Base
@@ -18,6 +19,15 @@ class Plugin extends Base
'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.
$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
// menu by relocate.js). Opens a two-button modal that confirms + picks the direction.
$this->template->hook->attach('template:board:column:dropdown', 'organonTweaks:board/mark_all_item');
// Sub-tweak (opt-in): auto-mark a card Done when its LAST subtask is completed. On every
// subtask change, recompute all-done and mark Done only on the up-transition
// (state-comparison in the helper), so a manual unmark is honored -- nothing re-fires.
// Dispatcher direct (the plugin on() wrapper drops the event). DELETE fires before the row
// is removed, so its id is excluded from the recount.
if ((int) $this->configModel->get('organon_tweaks_done_autosubtasks', 0) === 1) {
$container = $this->container;
$sync = function ($task_id, $exclude_id = 0) use ($container) {
$helper = new \Kanboard\Plugin\OrganonTweaks\Helper\OrganonDoneHelper($container);
$helper->syncSubtasksDone((int) $task_id, (int) $exclude_id);
};
$this->dispatcher->addListener(SubtaskModel::EVENT_UPDATE, function ($event) use ($sync) {
$sync($event['subtask']['task_id']);
});
$this->dispatcher->addListener(SubtaskModel::EVENT_CREATE, function ($event) use ($sync) {
$sync($event['subtask']['task_id']);
});
$this->dispatcher->addListener(SubtaskModel::EVENT_DELETE, function ($event) use ($sync) {
$sync($event['subtask']['task_id'], $event['subtask']['id']);
});
}
}
// Auto-managed shared custom filters (v1.5 "Show all tasks", v1.6 month filters, v1.7
@@ -175,7 +207,7 @@ class Plugin extends Base
public function getPluginVersion()
{
return '2.1.3';
return '2.3.0';
}
public function getPluginHomepage()

View File

@@ -142,6 +142,12 @@ recolored -- only the badge.
open-only filter, but reappears if you clear the filter (or use the "Board: show all tasks" filter).
- With that option **off**, the badge is an independent marker stored in task metadata
(`organon_done`) that never touches the open/closed status.
- **Auto-mark on subtasks (opt-in).** When every subtask on a card is completed, the badge flips to
Done automatically (honoring the mode above -- marker or close). It only nudges at the moment the
last subtask completes: recomputing all-done on each subtask change and acting only on the
not-all-done -> all-done transition, so if you then set the badge back to Todo it is respected (the
subtasks stay done -- the description may hold unfinished business). It never auto-reverts to Todo.
Off by default; enable it in the Done badge settings.
- **Bulk per-column action.** The board column header dropdown gains a **Mark all as Done/Todo** entry
(`Template/board/mark_all_item.php`, moved into the menu by `relocate.js`). It opens a single modal
that both confirms and picks the direction -- **Mark all Done** or **Mark all Todo** -- then sets
@@ -149,6 +155,23 @@ recolored -- only the badge.
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
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) ?>
<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">

View File

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