Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ddbd4c42a6 | |||
| ca57a4044e |
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
|
||||||
|
});
|
||||||
|
})();
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
34
Plugin.php
34
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');
|
||||||
|
|
||||||
@@ -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.3';
|
return '2.3.0';
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getPluginHomepage()
|
public function getPluginHomepage()
|
||||||
|
|||||||
23
README.md
23
README.md
@@ -142,6 +142,12 @@ recolored -- only the badge.
|
|||||||
open-only filter, but reappears if you clear the filter (or use the "Board: show all tasks" filter).
|
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
|
||||||
@@ -149,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
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
Reference in New Issue
Block a user