2 Commits
v2.0 ... v2.1.2

13 changed files with 218 additions and 56 deletions

View File

@@ -1,18 +1,41 @@
/* /*
* OrganonTweaks -- keep the board's horizontal scroll position across refreshes. * OrganonTweaks -- keep the board's horizontal scroll position across refreshes AND full reloads.
* *
* When you drop a card (and on Kanboard's periodic AJAX polling) the board is rebuilt with * Two cases lose the horizontal scroll and snap the board back to the first column:
* `$("#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), and the new element starts at scrollLeft 0.
* jump. This remembers the last scroll position and restores it the moment a replacement * 2. Full page reload -- e.g. clicking the Done/Todo badge, which navigates and redirects back to
* container appears, so the view stays put. * the board; a fresh page starts at scrollLeft 0.
* *
* We observe the STABLE parent (the container itself is replaced) and read the scroll from * We remember the last position and restore it: in-memory for the AJAX rebuild (observing the STABLE
* whichever #board-container is current, so it keeps working after every rebuild. * parent, since the container itself is replaced), and in sessionStorage (keyed per board) so it
* survives a full reload as well.
*/ */
(function () { (function () {
"use strict"; "use strict";
function boardKey() {
var m = location.href.match(/board\/(\d+)/) || location.href.match(/project_id=(\d+)/);
return "organon-board-scroll-" + (m ? m[1] : location.pathname);
}
function readStored() {
try {
var v = window.sessionStorage.getItem(boardKey());
return v ? parseInt(v, 10) : 0;
} catch (e) {
return 0;
}
}
function writeStored(value) {
try {
window.sessionStorage.setItem(boardKey(), value);
} catch (e) {
// sessionStorage unavailable (private mode / disabled) -- degrade to AJAX-only restore.
}
}
function init() { function init() {
var container = document.getElementById("board-container"); var container = document.getElementById("board-container");
if (!container) { if (!container) {
@@ -20,20 +43,27 @@
} }
var parent = container.parentNode; var parent = container.parentNode;
var lastScrollLeft = container.scrollLeft; var stored = readStored();
var lastScrollLeft = stored || container.scrollLeft;
var lastContainer = container; var lastContainer = container;
// Remember the position whenever the board is scrolled (capture: scroll does not // Restore across a full page reload (badge click, etc.), not only AJAX rebuilds.
// bubble, and this survives the container being replaced). if (stored && container.scrollLeft !== stored) {
container.scrollLeft = stored;
}
// Remember the position whenever the board is scrolled (capture: scroll does not bubble, and
// this survives the container being replaced).
document.addEventListener("scroll", function (e) { document.addEventListener("scroll", function (e) {
var c = document.getElementById("board-container"); var c = document.getElementById("board-container");
if (c && e.target === c) { if (c && e.target === c) {
lastScrollLeft = c.scrollLeft; lastScrollLeft = c.scrollLeft;
writeStored(lastScrollLeft);
} }
}, true); }, true);
// When the board is rebuilt, #board-container becomes a new element at scrollLeft 0; // When the board is rebuilt, #board-container becomes a new element at scrollLeft 0; restore
// restore the remembered position before the browser paints it. // the remembered position before the browser paints it.
if (window.MutationObserver) { if (window.MutationObserver) {
new MutationObserver(function () { new MutationObserver(function () {
var c = document.getElementById("board-container"); var c = document.getElementById("board-container");

View File

@@ -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];

View File

@@ -3,16 +3,21 @@
namespace Kanboard\Plugin\OrganonTweaks\Controller; namespace Kanboard\Plugin\OrganonTweaks\Controller;
use Kanboard\Controller\BaseController; use Kanboard\Controller\BaseController;
use Kanboard\Core\Controller\AccessForbiddenException;
use Kanboard\Model\TaskModel;
use Kanboard\Plugin\OrganonTweaks\Helper\OrganonDoneHelper; use Kanboard\Plugin\OrganonTweaks\Helper\OrganonDoneHelper;
/** /**
* Toggle a task's Done/Due badge (OrganonTweaks). * Toggle a task's Done/Todo badge (OrganonTweaks).
* *
* One-click CSRF link from the board card face and the task view. Single source of truth per mode: * 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-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/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. * - 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). * 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 class DoneController extends BaseController
{ {
@@ -44,4 +49,70 @@ class DoneController extends BaseController
$this->response->redirect($this->helper->url->to('TaskViewController', 'show', array('task_id' => $task['id'])), true); $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();
}
}
} }

View File

@@ -5,7 +5,7 @@ namespace Kanboard\Plugin\OrganonTweaks\Helper;
use Kanboard\Core\Base; use Kanboard\Core\Base;
/** /**
* Done/Due badge helper. * Done/Todo badge helper.
* *
* "Done" has a single source of truth PER MODE (no drift with native Close/Open): * "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). * - close-mode on ("check done also closes tasks"): Done == the task is CLOSED (is_active == 0).

View File

@@ -111,7 +111,7 @@ class Plugin extends Base
}); });
} }
// Tweak: a Done/Due status badge on the card face (top-right, below the header) and in the // Tweak: a Done/Todo status badge on the card face (top-right, below the header) and in the
// task view (4th column, near the due date). Two-state toggle; single source of truth per // 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 // 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. // sync); close-mode OFF => a task-metadata marker (organon_done). Server-rendered, no JS.
@@ -119,6 +119,9 @@ class Plugin extends Base
$this->template->hook->attach('template:board:private:task:before-title', 'organonTweaks:board/done_badge'); $this->template->hook->attach('template: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:task:details:fourth-column', 'organonTweaks:task/done_badge');
$this->template->hook->attach('template:layout:head', 'organonTweaks:layout/done_style'); $this->template->hook->attach('template:layout:head', 'organonTweaks:layout/done_style');
// Bulk "Mark all as Done/Todo" entry in the column header dropdown (moved into the native
// menu by relocate.js). Opens a two-button modal that confirms + picks the direction.
$this->template->hook->attach('template:board:column:dropdown', 'organonTweaks:board/mark_all_item');
} }
// Auto-managed shared custom filters (v1.5 "Show all tasks", v1.6 month filters, v1.7 // Auto-managed shared custom filters (v1.5 "Show all tasks", v1.6 month filters, v1.7
@@ -172,7 +175,7 @@ class Plugin extends Base
public function getPluginVersion() public function getPluginVersion()
{ {
return '2.0.0'; return '2.1.2';
} }
public function getPluginHomepage() public function getPluginHomepage()

View File

@@ -39,12 +39,15 @@ have comments keep showing the count as before.
When you drop a card (or Kanboard auto-refreshes the board via polling), it rebuilds the 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 Done/Todo badge, which navigates and redirects back to the board) does
board stays where you were. the same. This tweak remembers the horizontal scroll position and restores it in both cases,
so the board stays where you were.
- Implemented in `Asset/js/keep-scroll.js`: it observes the stable parent (the container - Implemented in `Asset/js/keep-scroll.js`: it observes the stable parent (the container
itself is replaced), reads/writes `scrollLeft` on whichever `#board-container` is current. itself is replaced) and restores `scrollLeft` on whichever `#board-container` is current
after an AJAX rebuild, and persists the position per board in `sessionStorage` so it also
survives a full page reload.
- **On by default.** Toggle it under "Settings -> Organon Tweaks". - **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,12 +120,12 @@ single arrow** (up for ascending, down for descending) when it is ON.
(which also hides the native sort menu). Per-column state lives in project metadata. (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/Due badge ### Done/Todo badge
A two-state toggle badge for marking a card done, shown on the board card face (top-right, below the A two-state toggle badge for marking a card done, shown on the board card face (above the title) and
header) and on the task view (4th column, near the due date). It reads **`[ ] Due`** (dark red) while on the task view (4th column, near the due date). It reads **Todo** (black on light red) while the
the task is pending; one click flips it to **`[x] Done`** (light green). The card itself is not task is pending; one click flips it to **Done** (black on light green). The card itself is not
recolored -- only the badge. The "Due" colors reuse FinanceBuddy's debit badge for consistency. recolored -- only the badge.
- Rendered server-side (no JavaScript) via `template:board:private:task:before-title` - Rendered server-side (no JavaScript) via `template:board:private:task:before-title`
(`Template/board/done_badge.php`), `template:task:details:fourth-column` (`Template/board/done_badge.php`), `template:task:details:fourth-column`
@@ -135,6 +138,11 @@ recolored -- only the badge. The "Due" colors reuse FinanceBuddy's debit badge f
open-only filter, but reappears if you clear the filter (or use the "Board: show all tasks" filter). 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.
- **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". - **Off by default.** Toggle it under "Settings -> Organon Tweaks".
## Settings ## Settings
@@ -165,7 +173,7 @@ the page:
**Done badge** **Done badge**
- **Show a Done/Due badge on cards** -- default off. - **Show a Done/Todo badge on cards** -- default off.
- **Marking Done also closes the task** -- default off. - **Marking Done also closes the task** -- default off.
Standalone: Standalone:

View File

@@ -1,6 +1,6 @@
<?php <?php
/** /**
* Done/Due badge on the board card face. Rendered by template:board:private:task:before-title; the * 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. * 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 * 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). * (collapsed cards do not fire this hook).
@@ -12,7 +12,7 @@ $done = $this->OrganonDoneHelper->isDone($task);
?> ?>
<span class="organontweaks-done<?= $done ? ' is-done' : '' ?>"> <span class="organontweaks-done<?= $done ? ' is-done' : '' ?>">
<?= $this->url->link( <?= $this->url->link(
$done ? '<i class="fa fa-check-square fa-fw"></i> '.t('Done') : '<i class="fa fa-square-o fa-fw"></i> '.t('Due'), $done ? '<i class="fa fa-check-square"></i> '.t('Done') : '<i class="fa fa-square"></i> '.t('Todo'),
'DoneController', 'DoneController',
'toggle', 'toggle',
array('plugin' => 'OrganonTweaks', 'task_id' => $task['id'], 'project_id' => $task['project_id'], 'from' => 'board'), array('plugin' => 'OrganonTweaks', 'task_id' => $task['id'], 'project_id' => $task['project_id'], 'from' => 'board'),

View 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>

View 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>

View File

@@ -65,11 +65,11 @@
<fieldset> <fieldset>
<legend><?= t('Done badge') ?></legend> <legend><?= t('Done badge') ?></legend>
<?= $this->form->checkbox('organon_tweaks_done_badge', t('Show a Done/Due badge on cards'), 1, isset($values['organon_tweaks_done_badge']) && $values['organon_tweaks_done_badge'] == 1) ?> <?= $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 (top-right) and the task view (near the due date): "Due" (red) until you click it, then "Done" (green). Off by default.') ?></p> <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) ?> <?= $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 Due 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>
</fieldset> </fieldset>
<div class="form-actions"> <div class="form-actions">

View File

@@ -1,33 +1,29 @@
<?php <?php
/** /**
* Done/Due badge styling (emitted into the head when the feature is enabled). Two states via * Done/Todo badge styling (emitted into the head when the feature is enabled). NATURAL document flow --
* .is-done. The Due colors reuse FinanceBuddy's debit badge (#b94a48 / white); Done is a light green * the badge renders above the title (board card) or as a list item (task view); no absolute
* with black font. On the board the badge is pinned top-right, below the header (the card .task-board * positioning, so it adapts to any card / column / avatar / username size. Two states via .is-done:
* is position:relative); the title reserves right padding so the badge does not cover it. In the task * Todo = black filled box + black text on a light red (#ef9a9a); Done = checked box, black on light
* view it is a normal inline list item. Font size is inherited (0.9em on the board, matching the card * green (#a5d6a7). Both states use black text (core's `.task-board a { color:#000 }` forces the icon
* id / assignee). All values are safe to tune. * black anyway, so the backgrounds are kept light for contrast). Colors are tunable.
*/ */
?> ?>
<style> <style>
.organontweaks-done a { span.organontweaks-done a {
display: inline-block;
text-decoration: none; text-decoration: none;
padding: 0 5px; padding: 3px 10px;
border-radius: 3px; border-radius: 3px;
background: #b94a48; background: #ef9a9a;
color: #fff;
}
.organontweaks-done.is-done a {
background: #a5d6a7;
color: #000; color: #000;
font-weight: bold; font-weight: bold;
} }
.task-board .organontweaks-done { span.organontweaks-done.is-done a {
position: absolute; background: #a5d6a7;
top: 24px; color: #000;
right: 5px;
z-index: 5;
} }
.task-board .task-board-title { .organontweaks-done {
padding-right: 4.2em; display: inline-block;
margin: 1px 0;
} }
</style> </style>

View File

@@ -1,6 +1,6 @@
<?php <?php
/** /**
* Done/Due badge in the task view, 4th column (near the due date). Rendered by * 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 * 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. * two-state toggle as the board badge; only shown to users who may edit the task.
*/ */
@@ -12,7 +12,7 @@ $done = $this->OrganonDoneHelper->isDone($task);
<li> <li>
<span class="organontweaks-done<?= $done ? ' is-done' : '' ?>"> <span class="organontweaks-done<?= $done ? ' is-done' : '' ?>">
<?= $this->url->link( <?= $this->url->link(
$done ? '<i class="fa fa-check-square fa-fw"></i> '.t('Done') : '<i class="fa fa-square-o fa-fw"></i> '.t('Due'), $done ? '<i class="fa fa-check-square"></i> '.t('Done') : '<i class="fa fa-square"></i> '.t('Todo'),
'DoneController', 'DoneController',
'toggle', 'toggle',
array('plugin' => 'OrganonTweaks', 'task_id' => $task['id'], 'project_id' => $task['project_id'], 'from' => 'task'), array('plugin' => 'OrganonTweaks', 'task_id' => $task['id'], 'project_id' => $task['project_id'], 'from' => 'task'),

View File

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