Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bc266ff969 | |||
| 8a97c5cb94 | |||
| d1178bc463 | |||
| c747d33491 | |||
| 850127e712 | |||
| 70f42352b0 | |||
| d57544ebfd | |||
| c3896e1962 | |||
| 065825269e | |||
| 6726dfa456 | |||
| 17ebfcccd9 | |||
| 95561e5565 | |||
| ff6abf1d49 | |||
| 310c34620f | |||
| a5721890a6 | |||
| 9c27b51eed | |||
| 1bea749fed |
35
Asset/js/click-guard.js
Normal file
35
Asset/js/click-guard.js
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
/*
|
||||||
|
* OrganonTweaks -- only open a card on a quick click.
|
||||||
|
*
|
||||||
|
* A real click is a quick press-release; a drag is a press, some movement, and a release
|
||||||
|
* much later. When you drop a card, jQuery UI can leak a trailing "click" on it, and
|
||||||
|
* Kanboard opens the card -- an unwanted side effect of the drag.
|
||||||
|
*
|
||||||
|
* We simply time the press on a card: record the moment of mousedown / touchstart, and on
|
||||||
|
* the click that follows, if more than THRESHOLD ms elapsed the press was a drag (held),
|
||||||
|
* not a click, so we swallow the click in the capture phase before Kanboard's open handler
|
||||||
|
* runs. A normal click is well under the threshold and opens the card as usual.
|
||||||
|
*/
|
||||||
|
(function () {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
var THRESHOLD = 110; // ms; a press on a card held longer than this is a drag, not a click
|
||||||
|
var pressStart = 0;
|
||||||
|
|
||||||
|
function onDown(e) {
|
||||||
|
pressStart = (e.target && e.target.closest && e.target.closest(".task-board")) ? Date.now() : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener("mousedown", onDown, true);
|
||||||
|
document.addEventListener("touchstart", onDown, true);
|
||||||
|
|
||||||
|
document.addEventListener("click", function (e) {
|
||||||
|
if (pressStart &&
|
||||||
|
e.target && e.target.closest && e.target.closest(".task-board") &&
|
||||||
|
(Date.now() - pressStart) > THRESHOLD) {
|
||||||
|
e.stopImmediatePropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
pressStart = 0;
|
||||||
|
}, true);
|
||||||
|
})();
|
||||||
105
Asset/js/enhance-recurrence.js
Normal file
105
Asset/js/enhance-recurrence.js
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
/*
|
||||||
|
* OrganonTweaks -- enhance Kanboard's native "Edit recurrence" dialog.
|
||||||
|
*
|
||||||
|
* That template (task_recurrence/edit.php) has no hook, so we transform the form in JS when
|
||||||
|
* the modal opens:
|
||||||
|
* 1. Replace the "Generate recurrent task" Yes/No dropdown with two radio buttons (one
|
||||||
|
* click instead of open + select). The original <select> is kept, hidden, and stays in
|
||||||
|
* sync, so the form still submits recurrence_status normally.
|
||||||
|
* 2. Add a small [+7] button next to the "Factor" input that adds 7 (a week of days).
|
||||||
|
*
|
||||||
|
* Detection: the modal is injected into #modal-box dynamically, so we watch the document for
|
||||||
|
* an un-enhanced recurrence form appearing. Loaded only when the setting is enabled.
|
||||||
|
*/
|
||||||
|
(function () {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
function enhanceStatus(select) {
|
||||||
|
var group = document.createElement("span");
|
||||||
|
group.className = "ot-recurrence-status";
|
||||||
|
|
||||||
|
var radios = [];
|
||||||
|
|
||||||
|
for (var i = 0; i < select.options.length; i++) {
|
||||||
|
var opt = select.options[i];
|
||||||
|
|
||||||
|
var label = document.createElement("label");
|
||||||
|
// Kanboard styles form labels as display:block; force inline so Yes/No sit
|
||||||
|
// side by side instead of stacked.
|
||||||
|
label.style.display = "inline-block";
|
||||||
|
label.style.marginRight = "14px";
|
||||||
|
label.style.cursor = "pointer";
|
||||||
|
|
||||||
|
var radio = document.createElement("input");
|
||||||
|
radio.type = "radio";
|
||||||
|
// No name attribute on purpose: a named radio would be SUBMITTED with the form,
|
||||||
|
// and Kanboard rejects the extra key ("invalid identifier"). These are display
|
||||||
|
// only -- the hidden <select> carries the value -- so we enforce the mutual
|
||||||
|
// exclusivity ourselves.
|
||||||
|
radio.value = opt.value;
|
||||||
|
radio.checked = opt.value === select.value;
|
||||||
|
radio.style.marginRight = "4px";
|
||||||
|
radios.push(radio);
|
||||||
|
|
||||||
|
radio.addEventListener("change", function () {
|
||||||
|
for (var k = 0; k < radios.length; k++) {
|
||||||
|
radios[k].checked = (radios[k] === this);
|
||||||
|
}
|
||||||
|
select.value = this.value;
|
||||||
|
select.dispatchEvent(new Event("change", { bubbles: true }));
|
||||||
|
});
|
||||||
|
|
||||||
|
label.appendChild(radio);
|
||||||
|
label.appendChild(document.createTextNode(opt.text));
|
||||||
|
group.appendChild(label);
|
||||||
|
}
|
||||||
|
|
||||||
|
select.style.display = "none";
|
||||||
|
select.parentNode.insertBefore(group, select.nextSibling);
|
||||||
|
}
|
||||||
|
|
||||||
|
function enhanceFactor(input) {
|
||||||
|
var btn = document.createElement("button");
|
||||||
|
btn.type = "button";
|
||||||
|
btn.className = "btn";
|
||||||
|
btn.textContent = "+7";
|
||||||
|
btn.title = "Add 7 (one week of days)";
|
||||||
|
btn.style.marginLeft = "6px";
|
||||||
|
btn.addEventListener("click", function () {
|
||||||
|
input.value = (parseInt(input.value, 10) || 0) + 7;
|
||||||
|
input.dispatchEvent(new Event("change", { bubbles: true }));
|
||||||
|
});
|
||||||
|
|
||||||
|
input.parentNode.insertBefore(btn, input.nextSibling);
|
||||||
|
}
|
||||||
|
|
||||||
|
function enhance() {
|
||||||
|
var select = document.querySelector('select[name="recurrence_status"]:not([data-ot-recurrence])');
|
||||||
|
if (!select) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
select.setAttribute("data-ot-recurrence", "1");
|
||||||
|
enhanceStatus(select);
|
||||||
|
|
||||||
|
var factor = document.querySelector('input[name="recurrence_factor"]:not([data-ot-recurrence])');
|
||||||
|
if (factor) {
|
||||||
|
factor.setAttribute("data-ot-recurrence", "1");
|
||||||
|
enhanceFactor(factor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
enhance();
|
||||||
|
|
||||||
|
if (window.MutationObserver) {
|
||||||
|
new MutationObserver(enhance).observe(document.body, { childList: true, subtree: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (document.readyState === "loading") {
|
||||||
|
document.addEventListener("DOMContentLoaded", init);
|
||||||
|
} else {
|
||||||
|
init();
|
||||||
|
}
|
||||||
|
})();
|
||||||
55
Asset/js/keep-scroll.js
Normal file
55
Asset/js/keep-scroll.js
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
/*
|
||||||
|
* OrganonTweaks -- keep the board's horizontal scroll position across refreshes.
|
||||||
|
*
|
||||||
|
* When you drop a card (and on Kanboard's periodic AJAX polling) the board is rebuilt with
|
||||||
|
* `$("#board-container").replaceWith(data)` (BoardDragAndDrop.refresh). The brand-new
|
||||||
|
* element starts at scrollLeft 0, so the board snaps back to the first column -- a jarring
|
||||||
|
* jump. This remembers the last scroll position and restores it the moment a replacement
|
||||||
|
* container appears, so the view stays put.
|
||||||
|
*
|
||||||
|
* We observe the STABLE parent (the container itself is replaced) and read the scroll from
|
||||||
|
* whichever #board-container is current, so it keeps working after every rebuild.
|
||||||
|
*/
|
||||||
|
(function () {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
var container = document.getElementById("board-container");
|
||||||
|
if (!container) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var parent = container.parentNode;
|
||||||
|
var lastScrollLeft = container.scrollLeft;
|
||||||
|
var lastContainer = container;
|
||||||
|
|
||||||
|
// 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) {
|
||||||
|
var c = document.getElementById("board-container");
|
||||||
|
if (c && e.target === c) {
|
||||||
|
lastScrollLeft = c.scrollLeft;
|
||||||
|
}
|
||||||
|
}, true);
|
||||||
|
|
||||||
|
// When the board is rebuilt, #board-container becomes a new element at scrollLeft 0;
|
||||||
|
// restore the remembered position before the browser paints it.
|
||||||
|
if (window.MutationObserver) {
|
||||||
|
new MutationObserver(function () {
|
||||||
|
var c = document.getElementById("board-container");
|
||||||
|
if (c && c !== lastContainer) {
|
||||||
|
lastContainer = c;
|
||||||
|
if (c.scrollLeft !== lastScrollLeft) {
|
||||||
|
c.scrollLeft = lastScrollLeft;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).observe(parent, { childList: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (document.readyState === "loading") {
|
||||||
|
document.addEventListener("DOMContentLoaded", init);
|
||||||
|
} else {
|
||||||
|
init();
|
||||||
|
}
|
||||||
|
})();
|
||||||
@@ -1,17 +1,23 @@
|
|||||||
/*
|
/*
|
||||||
* OrganonTweaks -- place the "Remove this Column" entry inside the native column menu.
|
* OrganonTweaks -- move plugin-injected DOM bits into their native positions on the board.
|
||||||
*
|
*
|
||||||
* Kanboard only exposes the template:board:column:dropdown hook AFTER the column menu
|
* Both fixes are needed because Kanboard's relevant hooks render at the END of their
|
||||||
* <ul>, and its dropdown JS clones just that <ul> when the menu opens. So the hook item
|
* container, not where the item belongs:
|
||||||
* cannot reach the menu on its own. column_dropdown.php renders the item hidden next to
|
*
|
||||||
* the menu; this script moves each such item into that column's real <ul> so it appears
|
* 1. "Remove this Column": the template:board:column:dropdown hook renders AFTER the column
|
||||||
* alongside the native entries. It re-runs when the board is redrawn (AJAX polling /
|
* menu <ul> (and the dropdown JS clones only that <ul>), so we move the hidden <li> into
|
||||||
* drag-and-drop refresh).
|
* the column's real <ul>.
|
||||||
|
* 2. "Always show comment icon": the template:board:task:icons hook renders at the end of
|
||||||
|
* the card icons row, so the "+" bubble lands after priority instead of in the native
|
||||||
|
* comment slot. We move it just before the description icon (where the native comment
|
||||||
|
* icon sits), so the order matches whether or not the card has comments.
|
||||||
|
*
|
||||||
|
* Both re-run when the board is redrawn (AJAX polling / drag-and-drop refresh).
|
||||||
*/
|
*/
|
||||||
(function () {
|
(function () {
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
function relocate() {
|
function relocateColumnMenuItems() {
|
||||||
var items = document.querySelectorAll(".organontweaks-remove-column-item");
|
var items = document.querySelectorAll(".organontweaks-remove-column-item");
|
||||||
|
|
||||||
for (var i = 0; i < items.length; i++) {
|
for (var i = 0; i < items.length; i++) {
|
||||||
@@ -34,17 +40,57 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function repositionCommentIcons() {
|
||||||
|
var items = document.querySelectorAll(".organontweaks-comment-add");
|
||||||
|
|
||||||
|
for (var i = 0; i < items.length; i++) {
|
||||||
|
var item = items[i];
|
||||||
|
var row = item.parentNode;
|
||||||
|
|
||||||
|
if (!row) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Native comment icon sits just before the description icon; if there is no
|
||||||
|
// description, before the age / closed block. That is the slot we move into.
|
||||||
|
// NB: the description icon is wrapped in <span class="tooltip"> (Kanboard's
|
||||||
|
// tooltipLink), not an <a>, so we walk up to whatever is the row's direct child.
|
||||||
|
var anchor = row.querySelector("i.fa-file-text-o") ||
|
||||||
|
row.querySelector(".task-icon-age") ||
|
||||||
|
row.querySelector(".task-board-closed");
|
||||||
|
|
||||||
|
while (anchor && anchor.parentNode !== row) {
|
||||||
|
anchor = anchor.parentNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move only if needed (keeps the MutationObserver from looping).
|
||||||
|
if (anchor && anchor !== item && item.nextElementSibling !== anchor) {
|
||||||
|
row.insertBefore(item, anchor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function apply() {
|
||||||
|
relocateColumnMenuItems();
|
||||||
|
repositionCommentIcons();
|
||||||
|
}
|
||||||
|
|
||||||
function init() {
|
function init() {
|
||||||
if (!document.getElementById("board")) {
|
var container = document.getElementById("board-container");
|
||||||
|
if (!container) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
relocate();
|
apply();
|
||||||
|
|
||||||
var container = document.getElementById("board-container");
|
// Observe the STABLE parent, not #board-container: Kanboard replaces the container
|
||||||
|
// on every card drop / AJAX poll (BoardDragAndDrop.refresh), which would kill an
|
||||||
|
// observer bound to the container itself and leave the items un-relocated after a
|
||||||
|
// move. The parent survives, so apply() keeps running on every rebuild.
|
||||||
|
var parent = container.parentNode;
|
||||||
|
|
||||||
if (container && window.MutationObserver) {
|
if (parent && window.MutationObserver) {
|
||||||
new MutationObserver(relocate).observe(container, { childList: true, subtree: true });
|
new MutationObserver(apply).observe(parent, { childList: true, subtree: true });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,11 @@ class ConfigController extends BaseController
|
|||||||
'title' => t('Settings').' > '.t('Organon Tweaks'),
|
'title' => t('Settings').' > '.t('Organon Tweaks'),
|
||||||
'values' => array(
|
'values' => array(
|
||||||
'organon_tweaks_always_comment_icon' => (int) $this->configModel->get('organon_tweaks_always_comment_icon', 1),
|
'organon_tweaks_always_comment_icon' => (int) $this->configModel->get('organon_tweaks_always_comment_icon', 1),
|
||||||
|
'organon_tweaks_keep_scroll' => (int) $this->configModel->get('organon_tweaks_keep_scroll', 1),
|
||||||
|
'organon_tweaks_quick_click_only' => (int) $this->configModel->get('organon_tweaks_quick_click_only', 1),
|
||||||
|
'organon_tweaks_enhance_recurrence' => (int) $this->configModel->get('organon_tweaks_enhance_recurrence', 1),
|
||||||
|
'organon_tweaks_emphasize_duedate_default' => (int) $this->configModel->get('organon_tweaks_emphasize_duedate_default', 0),
|
||||||
|
'organon_tweaks_show_all_filter' => (int) $this->configModel->get('organon_tweaks_show_all_filter', 1),
|
||||||
),
|
),
|
||||||
'errors' => array(),
|
'errors' => array(),
|
||||||
)));
|
)));
|
||||||
@@ -25,13 +30,35 @@ class ConfigController extends BaseController
|
|||||||
$values = $this->request->getValues();
|
$values = $this->request->getValues();
|
||||||
|
|
||||||
$alwaysCommentIcon = isset($values['organon_tweaks_always_comment_icon']) ? 1 : 0;
|
$alwaysCommentIcon = isset($values['organon_tweaks_always_comment_icon']) ? 1 : 0;
|
||||||
|
$keepScroll = isset($values['organon_tweaks_keep_scroll']) ? 1 : 0;
|
||||||
|
$quickClickOnly = isset($values['organon_tweaks_quick_click_only']) ? 1 : 0;
|
||||||
|
$enhanceRecurrence = isset($values['organon_tweaks_enhance_recurrence']) ? 1 : 0;
|
||||||
|
$emphasizeDueDateDefault = isset($values['organon_tweaks_emphasize_duedate_default']) ? 1 : 0;
|
||||||
|
$showAllFilter = isset($values['organon_tweaks_show_all_filter']) ? 1 : 0;
|
||||||
|
|
||||||
if ($this->configModel->save(array('organon_tweaks_always_comment_icon' => $alwaysCommentIcon))) {
|
if ($this->configModel->save(array(
|
||||||
|
'organon_tweaks_always_comment_icon' => $alwaysCommentIcon,
|
||||||
|
'organon_tweaks_keep_scroll' => $keepScroll,
|
||||||
|
'organon_tweaks_quick_click_only' => $quickClickOnly,
|
||||||
|
'organon_tweaks_enhance_recurrence' => $enhanceRecurrence,
|
||||||
|
'organon_tweaks_emphasize_duedate_default' => $emphasizeDueDateDefault,
|
||||||
|
'organon_tweaks_show_all_filter' => $showAllFilter,
|
||||||
|
))) {
|
||||||
$this->flash->success(t('Settings saved successfully.'));
|
$this->flash->success(t('Settings saved successfully.'));
|
||||||
} else {
|
} else {
|
||||||
$this->flash->failure(t('Unable to save your settings.'));
|
$this->flash->failure(t('Unable to save your settings.'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Apply the "Show all tasks" toggle across all boards immediately: on -> (re)create the
|
||||||
|
// filter everywhere, off -> remove every one (it is a plugin-managed, reserved-name filter).
|
||||||
|
$projectHelper = new \Kanboard\Plugin\OrganonTweaks\Helper\OrganonProjectHelper($this->container);
|
||||||
|
|
||||||
|
if ($showAllFilter === 1) {
|
||||||
|
$projectHelper->addShowAllFilterToAllProjects();
|
||||||
|
} else {
|
||||||
|
$projectHelper->removeAllShowAllFilters();
|
||||||
|
}
|
||||||
|
|
||||||
$this->response->redirect($this->helper->url->to('ConfigController', 'show', array('plugin' => 'OrganonTweaks')));
|
$this->response->redirect($this->helper->url->to('ConfigController', 'show', array('plugin' => 'OrganonTweaks')));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
129
Helper/OrganonProjectHelper.php
Normal file
129
Helper/OrganonProjectHelper.php
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Kanboard\Plugin\OrganonTweaks\Helper;
|
||||||
|
|
||||||
|
use Kanboard\Core\Base;
|
||||||
|
use Kanboard\Model\CustomFilterModel;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Per-board helper for OrganonTweaks.
|
||||||
|
*
|
||||||
|
* Resolves the 3-state "emphasize due date" setting for the CURRENT board (read from the route
|
||||||
|
* project_id, which the router populates for pretty URLs before the layout renders):
|
||||||
|
* per-board 'on' -> on, 'off' -> off, 'default'/absent -> follow the LIVE global default.
|
||||||
|
*
|
||||||
|
* The stored values are the non-falsy strings 'on'/'off' (not '1'/'0'): Kanboard's
|
||||||
|
* MetadataModel::get() uses `?:`, and PHP treats the string "0" as falsy, so a stored "0" would
|
||||||
|
* come back as the 'default' fallback and "Always off" would never be honored.
|
||||||
|
*
|
||||||
|
* Also maintains a shared "Show all tasks" custom filter (an empty query) on every board, so there
|
||||||
|
* is always a one-click way to clear the board filter and reveal open + closed tasks.
|
||||||
|
*/
|
||||||
|
class OrganonProjectHelper extends Base
|
||||||
|
{
|
||||||
|
const EMPHASIZE_KEY = 'organon_tweaks_emphasize_duedate';
|
||||||
|
const EMPHASIZE_DEFAULT_KEY = 'organon_tweaks_emphasize_duedate_default';
|
||||||
|
|
||||||
|
const SHOW_ALL_FILTER_KEY = 'organon_tweaks_show_all_filter';
|
||||||
|
|
||||||
|
// The plugin RESERVES this custom-filter name: a fixed (non-translated) string so the dedup is
|
||||||
|
// stable across locales. A user's own filter with this name is absorbed into the canonical form.
|
||||||
|
const SHOW_ALL_FILTER_NAME = 'Show all tasks';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Should the current board emphasize the due date? Board pages only (project_id in the route).
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function isEmphasizeDueDate()
|
||||||
|
{
|
||||||
|
$project_id = $this->request->getIntegerParam('project_id');
|
||||||
|
|
||||||
|
if ($project_id <= 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$value = $this->projectMetadataModel->get($project_id, self::EMPHASIZE_KEY, 'default');
|
||||||
|
|
||||||
|
if ($value === 'on') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($value === 'off') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 'default' (or absent) -> follow the live global default.
|
||||||
|
return (int) $this->configModel->get(self::EMPHASIZE_DEFAULT_KEY, 0) === 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensure the board has exactly one canonical "Show all tasks" filter (empty query, shared).
|
||||||
|
* Idempotent: create it when absent, force a same-named one back to the canonical form (reserved
|
||||||
|
* name), and drop any duplicates. Called lazily on every project-header render, so it covers old
|
||||||
|
* and new boards alike. No-op without a real project and a real owner (custom_filters.user_id has
|
||||||
|
* a FK to users, so a system id like 0 would fail the insert).
|
||||||
|
*
|
||||||
|
* @param int $project_id
|
||||||
|
*/
|
||||||
|
public function ensureShowAllFilter($project_id)
|
||||||
|
{
|
||||||
|
$project_id = (int) $project_id;
|
||||||
|
$user_id = (int) $this->userSession->getId();
|
||||||
|
|
||||||
|
if ($project_id <= 0 || $user_id <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$existing = $this->db->table(CustomFilterModel::TABLE)
|
||||||
|
->eq('project_id', $project_id)
|
||||||
|
->eq('name', self::SHOW_ALL_FILTER_NAME)
|
||||||
|
->findAll();
|
||||||
|
|
||||||
|
if (empty($existing)) {
|
||||||
|
$this->db->table(CustomFilterModel::TABLE)->insert(array(
|
||||||
|
'filter' => '',
|
||||||
|
'project_id' => $project_id,
|
||||||
|
'user_id' => $user_id,
|
||||||
|
'name' => self::SHOW_ALL_FILTER_NAME,
|
||||||
|
'is_shared' => 1,
|
||||||
|
'append' => 0,
|
||||||
|
));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reserved name: keep one, force it to the canonical empty + shared form, drop the rest.
|
||||||
|
$keep = array_shift($existing);
|
||||||
|
|
||||||
|
if ($keep['filter'] !== '' || (int) $keep['is_shared'] !== 1) {
|
||||||
|
$this->db->table(CustomFilterModel::TABLE)
|
||||||
|
->eq('id', $keep['id'])
|
||||||
|
->update(array('filter' => '', 'is_shared' => 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($existing as $duplicate) {
|
||||||
|
$this->db->table(CustomFilterModel::TABLE)->eq('id', $duplicate['id'])->remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle turned ON: eagerly (re)create the "Show all tasks" filter on every existing board.
|
||||||
|
* ensureShowAllFilter() keeps covering boards created afterwards.
|
||||||
|
*/
|
||||||
|
public function addShowAllFilterToAllProjects()
|
||||||
|
{
|
||||||
|
foreach ($this->projectModel->getAll() as $project) {
|
||||||
|
$this->ensureShowAllFilter((int) $project['id']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle turned OFF: remove every "Show all tasks" filter across all boards (reserved name).
|
||||||
|
*/
|
||||||
|
public function removeAllShowAllFilters()
|
||||||
|
{
|
||||||
|
$this->db->table(CustomFilterModel::TABLE)
|
||||||
|
->eq('name', self::SHOW_ALL_FILTER_NAME)
|
||||||
|
->remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
42
Plugin.php
42
Plugin.php
@@ -20,18 +20,56 @@ class Plugin extends Base
|
|||||||
// 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');
|
||||||
|
|
||||||
|
// Per-board tweak: emphasize the due date on the board. Configured with 3-state radios on the
|
||||||
|
// board's Settings -> Integrations (Use default / Always on / Always off); the global default
|
||||||
|
// (for "Use default" boards) lives in the plugin settings above. The emphasis <style> is
|
||||||
|
// emitted into the head only on enabled board pages (gated by OrganonProjectHelper).
|
||||||
|
$this->template->hook->attach('template:project:integrations', 'organonTweaks:project/integration');
|
||||||
|
$this->template->hook->attach('template:layout:head', 'organonTweaks:layout/emphasize_duedate');
|
||||||
|
|
||||||
// Tweak: always show a comment icon on board cards. When a card has no comments
|
// Tweak: always show a comment icon on board cards. When a card has no comments
|
||||||
// yet, add a bubble "+" that opens the same comment modal (add the first comment
|
// yet, add a bubble "+" that opens the same comment modal (add the first comment
|
||||||
// straight from the board). Cards with comments already show the native count.
|
// straight from the board). Cards with comments already show the native count.
|
||||||
if ((int) $this->configModel->get('organon_tweaks_always_comment_icon', 1) === 1) {
|
if ((int) $this->configModel->get('organon_tweaks_always_comment_icon', 1) === 1) {
|
||||||
$this->template->hook->attach('template:board:task:icons', 'organonTweaks:board/task_comment_icon');
|
$this->template->hook->attach('template:board:task:icons', 'organonTweaks:board/task_comment_icon');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Tweak: keep the board horizontal scroll position across refreshes (Kanboard
|
||||||
|
// rebuilds #board-container on card drop / polling, resetting scrollLeft to 0).
|
||||||
|
if ((int) $this->configModel->get('organon_tweaks_keep_scroll', 1) === 1) {
|
||||||
|
$this->hook->on('template:layout:js', array(
|
||||||
|
'template' => 'plugins/OrganonTweaks/Asset/js/keep-scroll.js',
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tweak: only open a card on a quick click (a press held > 100ms is a drag, so the
|
||||||
|
// trailing click is swallowed and the card does not open).
|
||||||
|
if ((int) $this->configModel->get('organon_tweaks_quick_click_only', 1) === 1) {
|
||||||
|
$this->hook->on('template:layout:js', array(
|
||||||
|
'template' => 'plugins/OrganonTweaks/Asset/js/click-guard.js',
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tweak: enhance the native "Edit recurrence" dialog (Yes/No radios + a [+7] button
|
||||||
|
// on the factor). No template hook there, so it is done in JS.
|
||||||
|
if ((int) $this->configModel->get('organon_tweaks_enhance_recurrence', 1) === 1) {
|
||||||
|
$this->hook->on('template:layout:js', array(
|
||||||
|
'template' => 'plugins/OrganonTweaks/Asset/js/enhance-recurrence.js',
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tweak: keep a shared "Show all tasks" custom filter (empty query -> open + closed) on every
|
||||||
|
// board as a one-click "clear the filter". Ensured lazily when a project header renders, so
|
||||||
|
// it covers old and new boards. The config toggle bulk-adds/removes them (ConfigController).
|
||||||
|
if ((int) $this->configModel->get('organon_tweaks_show_all_filter', 1) === 1) {
|
||||||
|
$this->template->hook->attach('template:project:header:before', 'organonTweaks:project_header/show_all_filter');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getHelpers()
|
public function getHelpers()
|
||||||
{
|
{
|
||||||
return array(
|
return array(
|
||||||
'Plugin\OrganonTweaks\Helper' => array('OrganonColumnHelper'),
|
'Plugin\OrganonTweaks\Helper' => array('OrganonColumnHelper', 'OrganonProjectHelper'),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,7 +90,7 @@ class Plugin extends Base
|
|||||||
|
|
||||||
public function getPluginVersion()
|
public function getPluginVersion()
|
||||||
{
|
{
|
||||||
return '0.3.0';
|
return '1.5.0';
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getPluginHomepage()
|
public function getPluginHomepage()
|
||||||
|
|||||||
39
README.md
39
README.md
@@ -35,11 +35,50 @@ have comments keep showing the count as before.
|
|||||||
read-only viewers).
|
read-only viewers).
|
||||||
- **On by default.** Toggle it under "Settings -> Organon Tweaks".
|
- **On by default.** Toggle it under "Settings -> Organon Tweaks".
|
||||||
|
|
||||||
|
### Keep the board scroll position across refreshes
|
||||||
|
|
||||||
|
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
|
||||||
|
the far left -- so the view jumps back to the first column. This tweak remembers the
|
||||||
|
horizontal scroll position and restores it as soon as the rebuilt board appears, so the
|
||||||
|
board stays where you were.
|
||||||
|
|
||||||
|
- 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.
|
||||||
|
- **On by default.** Toggle it under "Settings -> Organon Tweaks".
|
||||||
|
|
||||||
|
### Open a card only on a quick click
|
||||||
|
|
||||||
|
Dropping a card sometimes leaks a trailing "click" that opens the card -- an unwanted side
|
||||||
|
effect of the drag. A real click is a quick press-release; a drag is a press held much
|
||||||
|
longer. So `Asset/js/click-guard.js` times the press on a card (`mousedown`/`touchstart` to
|
||||||
|
the following `click`) and, if it was held longer than **100 ms**, swallows the click in the
|
||||||
|
capture phase before Kanboard's open handler -- so a drag never opens the card. Normal
|
||||||
|
clicks (well under 100 ms) still open cards as usual. The 100 ms threshold is fixed.
|
||||||
|
|
||||||
|
- **On by default.** Toggle it under "Settings -> Organon Tweaks".
|
||||||
|
|
||||||
|
### Enhance the recurrence dialog
|
||||||
|
|
||||||
|
Kanboard's native "Edit recurrence" dialog is a plain form. This makes two small usability
|
||||||
|
edits when it opens (there is no template hook, so it is done in JS,
|
||||||
|
`Asset/js/enhance-recurrence.js`):
|
||||||
|
|
||||||
|
- the "Generate recurrent task" **Yes/No dropdown** becomes two **radio buttons** (one click
|
||||||
|
to set, instead of open + select). The original select is kept hidden and stays in sync, so
|
||||||
|
the form submits exactly as before.
|
||||||
|
- a small **[+7]** button next to the "Factor" input adds 7 to it (one week of days).
|
||||||
|
|
||||||
|
- **On by default.** Toggle it under "Settings -> Organon Tweaks".
|
||||||
|
|
||||||
## Settings
|
## Settings
|
||||||
|
|
||||||
Global (per Kanboard instance) and admin-only, under "Settings -> Organon Tweaks":
|
Global (per Kanboard instance) and admin-only, under "Settings -> Organon Tweaks":
|
||||||
|
|
||||||
- **Always show the comment icon on board cards** -- default on.
|
- **Always show the comment icon on board cards** -- default on.
|
||||||
|
- **Keep the board scroll position across refreshes** -- default on.
|
||||||
|
- **Open a card only on a quick click** -- default on.
|
||||||
|
- **Enhance the recurrence dialog** -- default on.
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
|
|||||||
@@ -10,5 +10,9 @@
|
|||||||
* so it does not appear for read-only viewers.
|
* so it does not appear for read-only viewers.
|
||||||
*/ ?>
|
*/ ?>
|
||||||
<?php if (empty($task['nb_comments']) && $this->user->hasProjectAccess('CommentController', 'save', $task['project_id'])): ?>
|
<?php if (empty($task['nb_comments']) && $this->user->hasProjectAccess('CommentController', 'save', $task['project_id'])): ?>
|
||||||
|
<?php /* The hook renders at the END of the icons row; relocate.js moves this span up to
|
||||||
|
the native comment slot (before the description icon) so the order matches. */ ?>
|
||||||
|
<span class="organontweaks-comment-add">
|
||||||
<?= $this->modal->medium('comments-o', '+', 'CommentListController', 'show', array('task_id' => $task['id']), t('Add a comment')) ?>
|
<?= $this->modal->medium('comments-o', '+', 'CommentListController', 'show', array('task_id' => $task['id']), t('Add a comment')) ?>
|
||||||
|
</span>
|
||||||
<?php endif ?>
|
<?php endif ?>
|
||||||
|
|||||||
@@ -12,6 +12,41 @@
|
|||||||
</p>
|
</p>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
|
<fieldset>
|
||||||
|
<?= $this->form->checkbox('organon_tweaks_keep_scroll', t('Keep the board scroll position across refreshes'), 1, isset($values['organon_tweaks_keep_scroll']) && $values['organon_tweaks_keep_scroll'] == 1) ?>
|
||||||
|
<p class="form-help">
|
||||||
|
<?= t('When a card is dropped (or the board auto-refreshes), Kanboard rebuilds the board and jumps back to the first column. This restores the horizontal scroll position so the board stays where you were.') ?>
|
||||||
|
</p>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<fieldset>
|
||||||
|
<?= $this->form->checkbox('organon_tweaks_quick_click_only', t('Open a card only on a quick click'), 1, isset($values['organon_tweaks_quick_click_only']) && $values['organon_tweaks_quick_click_only'] == 1) ?>
|
||||||
|
<p class="form-help">
|
||||||
|
<?= t('A press on a card held longer than 100 ms is treated as a drag, so releasing it does not open the card. Fixes a drag sometimes opening the card on release. Normal clicks are well under 100 ms.') ?>
|
||||||
|
</p>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<fieldset>
|
||||||
|
<?= $this->form->checkbox('organon_tweaks_enhance_recurrence', t('Enhance the native recurrence dialog'), 1, isset($values['organon_tweaks_enhance_recurrence']) && $values['organon_tweaks_enhance_recurrence'] == 1) ?>
|
||||||
|
<p class="form-help">
|
||||||
|
<?= t('In the native "Edit recurrence" dialog, replace the Generate recurrent task dropdown with Yes/No radio buttons (one click), and add a [+7] button next to the factor that adds 7 (one week of days).') ?>
|
||||||
|
</p>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<fieldset>
|
||||||
|
<?= $this->form->checkbox('organon_tweaks_emphasize_duedate_default', t('Emphasize due date by default'), 1, isset($values['organon_tweaks_emphasize_duedate_default']) && $values['organon_tweaks_emphasize_duedate_default'] == 1) ?>
|
||||||
|
<p class="form-help">
|
||||||
|
<?= t('Default for boards set to "Use default": enlarge and bold the due date on the board. Each board can override this under its own Settings -> Integrations.') ?>
|
||||||
|
</p>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<fieldset>
|
||||||
|
<?= $this->form->checkbox('organon_tweaks_show_all_filter', t('Add a "Show all tasks" filter to every board'), 1, isset($values['organon_tweaks_show_all_filter']) && $values['organon_tweaks_show_all_filter'] == 1) ?>
|
||||||
|
<p class="form-help">
|
||||||
|
<?= t('Keep a shared "Show all tasks" custom filter (an empty query, so open and closed tasks are shown) on every board, old and new. It appears in the board filter bookmark menu as a one-click way to clear the filter. Turning this off removes it from all boards; turning it on recreates it everywhere.') ?>
|
||||||
|
</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>
|
||||||
|
|||||||
3
Template/layout/emphasize_duedate.php
Normal file
3
Template/layout/emphasize_duedate.php
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<?php if ($this->OrganonProjectHelper->isEmphasizeDueDate()): ?>
|
||||||
|
<style>.task-date { font-size: 1.375em; font-weight: bold; }</style>
|
||||||
|
<?php endif ?>
|
||||||
24
Template/project/integration.php
Normal file
24
Template/project/integration.php
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<?php $choice = isset($values['organon_tweaks_emphasize_duedate']) && in_array($values['organon_tweaks_emphasize_duedate'], array('on', 'off'), true) ? $values['organon_tweaks_emphasize_duedate'] : 'default' ?>
|
||||||
|
|
||||||
|
<h3><i class="fa fa-umbrella fa-fw" aria-hidden="true"></i> <?= t('OrganonTweaks') ?></h3>
|
||||||
|
<div class="listing">
|
||||||
|
<p><strong><?= t('Emphasize due date on the board') ?></strong></p>
|
||||||
|
|
||||||
|
<label style="display:inline-block; margin-right:16px;">
|
||||||
|
<input type="radio" name="organon_tweaks_emphasize_duedate" value="default" <?= $choice === 'default' ? 'checked="checked"' : '' ?>> <?= t('Use default') ?>
|
||||||
|
</label>
|
||||||
|
<label style="display:inline-block; margin-right:16px;">
|
||||||
|
<input type="radio" name="organon_tweaks_emphasize_duedate" value="on" <?= $choice === 'on' ? 'checked="checked"' : '' ?>> <?= t('Always on') ?>
|
||||||
|
</label>
|
||||||
|
<label style="display:inline-block; margin-right:16px;">
|
||||||
|
<input type="radio" name="organon_tweaks_emphasize_duedate" value="off" <?= $choice === 'off' ? 'checked="checked"' : '' ?>> <?= t('Always off') ?>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<p class="form-help">
|
||||||
|
<?= t('Enlarge and bold the due date on this board. "Use default" follows the global OrganonTweaks setting.') ?>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-actions">
|
||||||
|
<button type="submit" class="btn btn-blue"><?= t('Save') ?></button>
|
||||||
|
</div>
|
||||||
1
Template/project_header/show_all_filter.php
Normal file
1
Template/project_header/show_all_filter.php
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<?php $this->OrganonProjectHelper->ensureShowAllFilter($project['id']) ?>
|
||||||
Reference in New Issue
Block a user