33 Commits
v0.3 ... v1.8.3

Author SHA1 Message Date
8057272b9c document card-interaction tweaks in README (v1.8.3) 2026-07-10 18:35:42 -03:00
dc22739647 v1.8.2 admin global settings groupped 2026-07-10 18:30:34 -03:00
2d3c716852 v1.8.1 stage D button save & close unnecessary bugfix 2026-07-10 18:26:44 -03:00
269964d4cc add Save & Close button on the task edit form v1.8 t2 2026-07-10 18:18:26 -03:00
ac8e3b9544 add Save & Close button on the task edit form v1.8 2026-07-10 18:13:19 -03:00
717095848b card-view title click opens the edit modal 2026-07-10 18:09:06 -03:00
a905079abc board card title click opens the edit modal 2026-07-10 18:06:24 -03:00
e032d5d2b4 show existing keyboard-shortcut hints on the task Actions menu 2026-07-10 17:56:55 -03:00
b63d366296 add cross-promo plugin section to READMEs; bump patch (v*.*.+1) 2026-07-10 09:15:36 -03:00
89888235d1 custom filter renames for better grouping v1.7.1 2026-07-09 07:54:12 -03:00
4ebebd5ffb recurring:XY help text explains keywords X={evt,cal,any} Y={ori,dup,all} 2026-07-09 07:44:00 -03:00
9b637160b2 recurring:XY search keyword + auto-added recurring filter bookmarks v1.7 2026-07-09 07:40:52 -03:00
e85d5d71f9 settings page optimized 2026-07-09 07:13:11 -03:00
ce3205646f group card-readability + filter settings, trim help texts, 120ms click-guard 2026-07-09 07:08:38 -03:00
4ce18d9e5f cleaning help text in global OrganonTweaks settings page 2026-07-09 07:03:34 -03:00
4d7f88aa94 v1.6 duemonth/aroundmonth search keywords + auto-added month filter bookmarks 2026-07-09 06:49:15 -03:00
bc266ff969 auto-managed shared "Show all tasks" filter on every board + config toggle v1.5 2026-07-08 21:09:30 -03:00
8a97c5cb94 fix Always-off (on/off/default safe values) + bump emphasis to 1.375em (v1.4.2) 2026-07-08 13:03:35 -03:00
d1178bc463 fix stuck enhance-recurrence toggle + clarify label (v1.4.1) 2026-07-08 09:13:07 -03:00
c747d33491 per-board "Emphasize due date" with global default (v1.4) 2026-07-08 09:00:50 -03:00
850127e712 increase click-guard.js to 110ms differentiate drag/click t3 try remove cached js first 2026-07-07 22:05:19 -03:00
70f42352b0 increase click-guard.js to 120ms differentiate drag/click t2 2026-07-07 22:04:14 -03:00
d57544ebfd increase Asset/js/click-guard.js 100ms to 110ms differentiate drag/click v1.3.3 2026-07-07 22:02:34 -03:00
c3896e1962 enhance-recurrence radio button only on display bugfix v1.3.2 2026-07-07 09:51:38 -03:00
065825269e v1.3.1 bump 2026-07-07 09:45:17 -03:00
6726dfa456 enhance recurrence dialog yes/no side-by-side v1.3.1 2026-07-07 09:43:54 -03:00
17ebfcccd9 OrganonTweaks: enhance recurrence dialog -- Yes/No radios + [+7] factor button (JS); added asset , v1.3.0 2026-07-07 09:41:15 -03:00
95561e5565 OrganonTweaks: enhance recurrence dialog -- Yes/No radios + [+7] factor button (JS); v1.3.0 2026-07-07 09:40:51 -03:00
ff6abf1d49 fix comment "+" position: anchor on description span (tooltipLink), not <a>, so it sits left; v1.2.1 2026-07-07 09:19:50 -03:00
310c34620f OrganonTweaks: swallow post-drag click so a held press (>100ms) never opens the card; v1.2 2026-07-06 22:27:33 -03:00
a5721890a6 harden relocate.js: observe stable parent so items re-relocate after #board-container rebuild; v1.1 2026-07-06 21:19:06 -03:00
9c27b51eed keep board horizontal scroll across refresh: restore scrollLeft after #board-container rebuild; v1.0 2026-07-06 21:12:26 -03:00
1bea749fed always show comment icon on cards: bubble+ to add the first comment, placed in native comment slot; v0.3.1 2026-07-06 07:28:06 -03:00
19 changed files with 1053 additions and 20 deletions

35
Asset/js/click-guard.js Normal file
View 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 = 120; // 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);
})();

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

View File

@@ -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 });
} }
} }

View File

@@ -0,0 +1,42 @@
/*
* OrganonTweaks -- show the keyboard shortcuts Kanboard already binds, next to the task Actions
* menu items. Display only: this adds NO key bindings, it just surfaces the existing ones (listed
* in Kanboard's own "Keyboard shortcuts" help). Items are matched by their FontAwesome icon class,
* so it is independent of the URL format (pretty vs query-string) and the interface language.
*/
(function () {
"use strict";
var HINTS = [
{ icon: "fa-edit", key: "e" }, // Edit the task
{ icon: "fa-plus", key: "s" }, // Add a sub-task
{ icon: "fa-comment-o", key: "c" }, // Add a comment
{ icon: "fa-code-fork", key: "l" } // Add internal link
];
function annotate() {
var sidebar = document.querySelector(".sidebar");
if (!sidebar) { return; }
HINTS.forEach(function (hint) {
var icon = sidebar.querySelector("li a i." + hint.icon);
if (!icon) { return; }
var link = icon.closest("a");
if (!link || link.querySelector(".organon-kbd")) { return; }
var kbd = document.createElement("span");
kbd.className = "organon-kbd";
kbd.textContent = hint.key;
kbd.style.cssText = "margin-left:8px;padding:0 5px;border:1px solid #ccc;" +
"border-radius:3px;font-family:monospace;font-size:0.85em;color:#777;background:#f7f7f7;";
link.appendChild(kbd);
});
}
if (document.readyState !== "loading") {
annotate();
} else {
document.addEventListener("DOMContentLoaded", annotate);
}
})();

View File

@@ -0,0 +1,67 @@
/*
* OrganonTweaks -- clicking a task title opens the edit modal instead of the read-only card view,
* reusing Kanboard's own modal opener (KB.modal.open).
* - Board: the title is a link, and a hidden marker rendered next to it (only when the user may
* edit) carries the server-generated edit URL.
* - Card view: the title (#task-summary > h2) opens the same modal via #task-view's data-edit-url,
* but only when the user may edit -- detected by the presence of the sidebar "Edit" action.
*
* A capture-phase delegated click runs before Kanboard's own handlers, survives the board's AJAX
* re-renders, and stops both the link navigation and the card's own click.
*/
(function () {
"use strict";
function openEdit(url) {
if (url && window.KB && KB.modal) {
KB.modal.open(url, "large", false);
}
}
// The sidebar "Edit the task" action (fa-edit) only renders when the current user may edit.
function canEdit() {
return !!document.querySelector(".sidebar a i.fa-edit");
}
document.addEventListener("click", function (e) {
if (!e.target || !e.target.closest) {
return;
}
// Board card title -> open the edit modal via the per-card marker URL.
var link = e.target.closest(".task-board-title a");
if (link) {
var card = link.closest(".task-board");
var marker = card ? card.querySelector(".organon-edit-url") : null;
if (marker) {
e.preventDefault();
e.stopPropagation();
openEdit(marker.getAttribute("data-url"));
}
return;
}
// Card-view title -> open the same edit modal (#task-view carries the edit URL).
var title = e.target.closest("#task-summary > h2");
if (title && canEdit()) {
var taskView = document.querySelector("#task-view");
if (taskView && taskView.getAttribute("data-edit-url")) {
openEdit(taskView.getAttribute("data-edit-url"));
}
}
}, true);
// Make the card-view title look clickable (the board title is already a link).
function markTitle() {
var title = document.querySelector("#task-summary > h2");
if (title && canEdit()) {
title.style.cursor = "pointer";
}
}
if (document.readyState !== "loading") {
markTitle();
} else {
document.addEventListener("DOMContentLoaded", markTitle);
}
})();

View File

@@ -15,6 +15,15 @@ class ConfigController extends BaseController
'title' => t('Settings').' &gt; '.t('Organon Tweaks'), 'title' => t('Settings').' &gt; '.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),
'organon_tweaks_month_filters' => (int) $this->configModel->get('organon_tweaks_month_filters', 1),
'organon_tweaks_recurring_filters' => (int) $this->configModel->get('organon_tweaks_recurring_filters', 1),
'organon_tweaks_shortcut_labels' => (int) $this->configModel->get('organon_tweaks_shortcut_labels', 1),
'organon_tweaks_title_click_edit' => (int) $this->configModel->get('organon_tweaks_title_click_edit', 1),
), ),
'errors' => array(), 'errors' => array(),
))); )));
@@ -25,13 +34,55 @@ 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;
$monthFilters = isset($values['organon_tweaks_month_filters']) ? 1 : 0;
$recurringFilters = isset($values['organon_tweaks_recurring_filters']) ? 1 : 0;
$shortcutLabels = isset($values['organon_tweaks_shortcut_labels']) ? 1 : 0;
$titleClickEdit = isset($values['organon_tweaks_title_click_edit']) ? 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,
'organon_tweaks_month_filters' => $monthFilters,
'organon_tweaks_recurring_filters' => $recurringFilters,
'organon_tweaks_shortcut_labels' => $shortcutLabels,
'organon_tweaks_title_click_edit' => $titleClickEdit,
))) {
$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 each filter-group toggle across all boards immediately: on -> (re)create the group's
// reserved-name filters everywhere, off -> remove them (they are plugin-managed).
$projectHelper = new \Kanboard\Plugin\OrganonTweaks\Helper\OrganonProjectHelper($this->container);
if ($showAllFilter === 1) {
$projectHelper->addGroupToAllProjects($projectHelper::GROUP_SHOW_ALL);
} else {
$projectHelper->removeGroup($projectHelper::GROUP_SHOW_ALL);
}
if ($monthFilters === 1) {
$projectHelper->addGroupToAllProjects($projectHelper::GROUP_MONTH);
} else {
$projectHelper->removeGroup($projectHelper::GROUP_MONTH);
}
if ($recurringFilters === 1) {
$projectHelper->addGroupToAllProjects($projectHelper::GROUP_RECURRING);
} else {
$projectHelper->removeGroup($projectHelper::GROUP_RECURRING);
}
$this->response->redirect($this->helper->url->to('ConfigController', 'show', array('plugin' => 'OrganonTweaks'))); $this->response->redirect($this->helper->url->to('ConfigController', 'show', array('plugin' => 'OrganonTweaks')));
} }
} }

View File

@@ -0,0 +1,92 @@
<?php
namespace Kanboard\Plugin\OrganonTweaks\Filter;
use Kanboard\Core\Filter\FilterInterface;
use Kanboard\Filter\BaseFilter;
use Kanboard\Model\TaskModel;
/**
* Filter tasks by a whole calendar month, optionally padded.
*
* Registered twice by the plugin (via $container->extend('taskLexer', ...)):
* - `duemonth:VALUE` -- padDays 0: due within the target month.
* - `aroundmonth:VALUE` -- padDays 10: the month minus 10 days .. plus 10 days (i.e. the last
* 10 days of the previous month, the whole month, and the first 10 days of the next month).
*
* VALUE is `this` (also `next` / `last`) or an explicit month such as `2026-01` or `january-2026`
* (anything strtotime() can parse). Bounds are computed with mktime (correct for any month length)
* at search time, so the filter auto-rolls.
*
* Both bounds (gte start, lte end) are added inside this single apply() on the single-value path,
* so they AND -- the range Kanboard's saved-filter string syntax cannot express (repeating the
* same attribute ORs via OrCriteria, and there is no AND keyword).
*/
class TaskMonthRangeFilter extends BaseFilter implements FilterInterface
{
private $attribute = 'duemonth';
private $padDays = 0;
public function setAttribute($attribute)
{
$this->attribute = $attribute;
return $this;
}
public function setPadDays($days)
{
$this->padDays = (int) $days;
return $this;
}
public function getAttributes()
{
return array($this->attribute);
}
public function apply()
{
$reference = $this->resolveMonthTimestamp((string) $this->value);
if ($reference === false) {
return $this; // unknown/typo value -> no-op (matches nothing added)
}
$month = (int) date('n', $reference);
$year = (int) date('Y', $reference);
$first = mktime(0, 0, 0, $month, 1, $year);
$last = mktime(23, 59, 59, $month + 1, 0, $year); // day 0 of next month = last day of this
$pad = $this->padDays * 86400;
$this->query->neq(TaskModel::TABLE.'.date_due', 0);
$this->query->gte(TaskModel::TABLE.'.date_due', $first - $pad);
$this->query->lte(TaskModel::TABLE.'.date_due', $last + $pad);
return $this;
}
/**
* Resolve the filter value to any timestamp inside the target month (or false if unparseable).
*
* @param string $value
* @return int|false
*/
private function resolveMonthTimestamp($value)
{
$value = strtolower(trim($value));
if ($value === '' || $value === 'this') {
return strtotime('first day of this month');
}
if ($value === 'next') {
return strtotime('first day of next month');
}
if ($value === 'last' || $value === 'prev' || $value === 'previous') {
return strtotime('first day of last month');
}
return strtotime($value); // 2026-01, january-2026, "last day of january 2026", ...
}
}

View File

@@ -0,0 +1,100 @@
<?php
namespace Kanboard\Plugin\OrganonTweaks\Filter;
use Kanboard\Core\Filter\FilterInterface;
use Kanboard\Filter\BaseFilter;
use Kanboard\Model\TaskModel;
use PicoDb\Database;
/**
* Filter tasks by recurrence: `recurring:XY`, one attribute with one composite value so it can
* INTERSECT the two axes (stacking two values of the same attribute would only OR them).
*
* X = system : evt (event-triggered = Kanboard native) | cal (calendar = RecoReco) | any
* Y = role : ori (original/template) | dup (duplicate/copy) | all
*
* Conditions are authoritative -- taken straight from how core classifies these cards:
* - evtori = recurrence_status PENDING, evtdup = recurrence_status PROCESSED. This is exactly how
* core colors the board refresh icon (app/Template/board/task_footer.php: normal icon = PENDING,
* fa-inverse = PROCESSED), so the filter matches what the board shows.
* - calori = RecoReco metadata recoreco_enabled=1, caldup = recoreco_clone=1.
* - *all / any* = the OR-unions of the above.
*
* Cross-plugin: the calendar side reads RecoReco metadata keys. If RecoReco is not installed the
* metadata sets are empty, so `cal*` matches nothing and `evt*` still works (event-only degrade).
*
* One attribute + one value -> single-value path, so the predicates below are grouped in a scoped
* beginOr/closeOr and the whole group still ANDs with any other search attribute.
*/
class TaskRecurringFilter extends BaseFilter implements FilterInterface
{
private $db;
public function setDatabase(Database $db)
{
$this->db = $db;
return $this;
}
public function getAttributes()
{
return array('recurring');
}
public function apply()
{
$value = strtolower(trim((string) $this->value));
$system = substr($value, 0, 3); // evt | cal | any
$role = substr($value, 3, 3); // ori | dup | all
if (! in_array($system, array('evt', 'cal', 'any'), true) ||
! in_array($role, array('ori', 'dup', 'all'), true)) {
return $this; // unknown/typo value -> no-op
}
$wantEvt = $system === 'evt' || $system === 'any';
$wantCal = $system === 'cal' || $system === 'any';
$wantOri = $role === 'ori' || $role === 'all';
$wantDup = $role === 'dup' || $role === 'all';
$this->query->beginOr();
if ($wantEvt && $wantOri) {
$this->query->eq(TaskModel::TABLE.'.recurrence_status', TaskModel::RECURRING_STATUS_PENDING);
}
if ($wantEvt && $wantDup) {
$this->query->eq(TaskModel::TABLE.'.recurrence_status', TaskModel::RECURRING_STATUS_PROCESSED);
}
if ($wantCal && $wantOri) {
$this->query->in(TaskModel::TABLE.'.id', $this->metaIds('recoreco_enabled'));
}
if ($wantCal && $wantDup) {
$this->query->in(TaskModel::TABLE.'.id', $this->metaIds('recoreco_clone'));
}
$this->query->closeOr();
return $this;
}
/**
* Task ids carrying a RecoReco metadata flag set to 1. Returns a non-matching sentinel when the
* set is empty so the IN clause matches nothing (RecoReco absent -> event-only degrade).
*
* @param string $name
* @return array
*/
private function metaIds($name)
{
$ids = $this->db->table('task_has_metadata')
->eq('name', $name)
->eq('value', '1')
->findAllByColumn('task_id');
return ! empty($ids) ? $ids : array(-1);
}
}

View File

@@ -0,0 +1,189 @@
<?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 shared, reserved-name custom filters on every board (v1.5 "Show all tasks", v1.6
* "Due this month" / "Around this month"), each group gated by its own config toggle.
*/
class OrganonProjectHelper extends Base
{
const EMPHASIZE_KEY = 'organon_tweaks_emphasize_duedate';
const EMPHASIZE_DEFAULT_KEY = 'organon_tweaks_emphasize_duedate_default';
// Config toggles for the auto-managed custom-filter groups.
const SHOW_ALL_FILTER_KEY = 'organon_tweaks_show_all_filter';
const MONTH_FILTERS_KEY = 'organon_tweaks_month_filters';
const RECURRING_FILTERS_KEY = 'organon_tweaks_recurring_filters';
// Reserved custom-filter groups: each row is [name, query, append]. The NAME is reserved by the
// plugin (fixed, non-translated strings so dedup is stable across locales); a user's own filter
// with a reserved name is absorbed into the canonical form. "Show all tasks" is a REPLACE filter
// (append 0) so it clears; the rest are APPEND (1) so clicks stack (distinct attributes AND).
// Names are prefixed by group so the dropdown -- which core sorts alphabetically by name
// (CustomFilterModel::getAll -> asc(name)) -- clusters them: Board < Month < Recurring.
const GROUP_SHOW_ALL = array(
array('Board: show all tasks', '', 0),
);
const GROUP_MONTH = array(
array('Month: due this month', 'duemonth:this', 1),
array('Month: around this month', 'aroundmonth:this', 1),
);
const GROUP_RECURRING = array(
array('Recurring: all', 'recurring:anyall', 1),
array('Recurring: event', 'recurring:evtall', 1),
array('Recurring: calendar', 'recurring:calall', 1),
array('Recurring: templates', 'recurring:anyori', 1),
array('Recurring: duplicates', 'recurring:anydup', 1),
);
/**
* 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 one canonical reserved custom filter on a board (shared). Idempotent: create when
* absent, force a same-named one back to the canonical form (query + shared + append) since the
* name is reserved, and drop duplicates. 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
* @param string $name
* @param string $query
* @param int $append
*/
public function ensureReservedFilter($project_id, $name, $query, $append)
{
$project_id = (int) $project_id;
$user_id = (int) $this->userSession->getId();
if ($project_id <= 0 || $user_id <= 0) {
return;
}
$append = $append ? 1 : 0;
$existing = $this->db->table(CustomFilterModel::TABLE)
->eq('project_id', $project_id)
->eq('name', $name)
->findAll();
if (empty($existing)) {
$this->db->table(CustomFilterModel::TABLE)->insert(array(
'filter' => $query,
'project_id' => $project_id,
'user_id' => $user_id,
'name' => $name,
'is_shared' => 1,
'append' => $append,
));
return;
}
// Reserved name: keep one, force it to the canonical form, drop the rest.
$keep = array_shift($existing);
if ($keep['filter'] !== $query || (int) $keep['is_shared'] !== 1 || (int) $keep['append'] !== $append) {
$this->db->table(CustomFilterModel::TABLE)
->eq('id', $keep['id'])
->update(array('filter' => $query, 'is_shared' => 1, 'append' => $append));
}
foreach ($existing as $duplicate) {
$this->db->table(CustomFilterModel::TABLE)->eq('id', $duplicate['id'])->remove();
}
}
/**
* Ensure every ENABLED group's filters on a board. Called lazily on each project-header render,
* so it covers old and new boards alike.
*
* @param int $project_id
*/
public function ensureBoardFilters($project_id)
{
if ((int) $this->configModel->get(self::SHOW_ALL_FILTER_KEY, 1) === 1) {
$this->ensureGroup($project_id, self::GROUP_SHOW_ALL);
}
if ((int) $this->configModel->get(self::MONTH_FILTERS_KEY, 1) === 1) {
$this->ensureGroup($project_id, self::GROUP_MONTH);
}
if ((int) $this->configModel->get(self::RECURRING_FILTERS_KEY, 1) === 1) {
$this->ensureGroup($project_id, self::GROUP_RECURRING);
}
}
private function ensureGroup($project_id, array $rows)
{
foreach ($rows as $row) {
$this->ensureReservedFilter($project_id, $row[0], $row[1], $row[2]);
}
}
/**
* Toggle turned ON: eagerly (re)create a group's filters on every existing board.
* ensureBoardFilters() keeps covering boards created afterwards.
*
* @param array $rows
*/
public function addGroupToAllProjects(array $rows)
{
foreach ($this->projectModel->getAll() as $project) {
$this->ensureGroup((int) $project['id'], $rows);
}
}
/**
* Toggle turned OFF: remove a group's filters across all boards (by their reserved names).
*
* @param array $rows
*/
public function removeGroup(array $rows)
{
$names = array();
foreach ($rows as $row) {
$names[] = $row[0];
}
$this->db->table(CustomFilterModel::TABLE)->in('name', $names)->remove();
}
}

View File

@@ -20,18 +20,94 @@ 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: show the keyboard shortcuts Kanboard already binds next to the task Actions menu
// items (display only -- no new bindings). Done in JS since those menu <li> have no hook.
if ((int) $this->configModel->get('organon_tweaks_shortcut_labels', 1) === 1) {
$this->hook->on('template:layout:js', array(
'template' => 'plugins/OrganonTweaks/Asset/js/shortcut-labels.js',
));
}
// Tweak: clicking a task title opens the edit modal (instead of the read-only card view).
// JS reuses Kanboard's own modal opener. On the board the title is a link, so a hidden marker
// (rendered by the hook below, only when the user may edit) carries the server-generated edit
// URL for the JS to open; on the card view the JS reads #task-view's data-edit-url.
if ((int) $this->configModel->get('organon_tweaks_title_click_edit', 1) === 1) {
$this->hook->on('template:layout:js', array(
'template' => 'plugins/OrganonTweaks/Asset/js/title-click-edit.js',
));
$this->template->hook->attach('template:board:private:task:before-title', 'organonTweaks:board/title_edit');
}
// Auto-managed shared custom filters (v1.5 "Show all tasks", v1.6 month filters, v1.7
// recurring filters). Ensured lazily whenever a project header renders (covers old + new
// boards); each group is gated by its own config toggle inside
// OrganonProjectHelper::ensureBoardFilters(). The toggles also bulk add/remove the filters.
$showAllFilters = (int) $this->configModel->get('organon_tweaks_show_all_filter', 1) === 1;
$monthFilters = (int) $this->configModel->get('organon_tweaks_month_filters', 1) === 1;
$recurringFilters = (int) $this->configModel->get('organon_tweaks_recurring_filters', 1) === 1;
if ($showAllFilters || $monthFilters || $recurringFilters) {
$this->template->hook->attach('template:project:header:before', 'organonTweaks:project_header/board_filters');
}
// Custom board-search keywords, registered into the task lexer by extending its factory
// service (Pimple preserves factory status on extend, so the lexer stays per-request fresh).
// `duemonth:VALUE` / `aroundmonth:VALUE` = a calendar month (padded by 10 days for around);
// `recurring:XY` = classify recurrence (X system evt/cal/any, Y role ori/dup/all). Always
// available to type, independent of the bookmark toggles above. Guarded so a future Kanboard
// service rename no-ops (keyword absent) instead of breaking.
if (isset($this->container['taskLexer'])) {
$this->container->extend('taskLexer', function ($builder, $c) {
return $builder
->withFilter((new \Kanboard\Plugin\OrganonTweaks\Filter\TaskMonthRangeFilter())->setAttribute('duemonth')->setPadDays(0))
->withFilter((new \Kanboard\Plugin\OrganonTweaks\Filter\TaskMonthRangeFilter())->setAttribute('aroundmonth')->setPadDays(10))
->withFilter((new \Kanboard\Plugin\OrganonTweaks\Filter\TaskRecurringFilter())->setDatabase($c['db']));
});
}
} }
public function getHelpers() public function getHelpers()
{ {
return array( return array(
'Plugin\OrganonTweaks\Helper' => array('OrganonColumnHelper'), 'Plugin\OrganonTweaks\Helper' => array('OrganonColumnHelper', 'OrganonProjectHelper'),
); );
} }
@@ -52,7 +128,7 @@ class Plugin extends Base
public function getPluginVersion() public function getPluginVersion()
{ {
return '0.3.0'; return '1.8.3';
} }
public function getPluginHomepage() public function getPluginHomepage()

View File

@@ -35,11 +35,93 @@ 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 **120 ms**, swallows the click in the
capture phase before Kanboard's open handler -- so a drag never opens the card. Normal
clicks (well under 120 ms) still open cards as usual. The 120 ms threshold is fixed.
- **On by default.** Toggle it under "Settings -> Organon Tweaks".
### Show keyboard shortcuts on the task menu
Kanboard already binds handy keys on the task view (`e` = edit, `s` = new sub-task, `c` = new
comment, `l` = new internal link) but never shows them, so they go undiscovered. This tweak adds a
small key hint next to each of those items in the task **Actions** sidebar. It is **display only**:
it adds no new bindings, it just surfaces the ones Kanboard already has.
- Implemented in `Asset/js/shortcut-labels.js`, matching the menu items by their FontAwesome icon
class, so it is independent of the URL format and the interface language.
- **On by default.** Toggle it under "Settings -> Organon Tweaks".
### Click a title to edit
By default, clicking a card title (on the board) or the title on the card view lands you on the
read-only card view. With this tweak on, clicking the title opens the **edit** form directly,
reusing Kanboard's own modal -- so from the board you edit and, on Save, come straight back to the
board. It applies only where you may edit (a read-only viewer keeps the normal title), and
shared/public boards are untouched.
- Implemented in `Asset/js/title-click-edit.js` -- a capture-phase click that reuses `KB.modal.open`.
On the board, a hidden marker (`Template/board/title_edit.php`, rendered by
`template:board:private:task:before-title` only when the user may edit) carries the
server-generated edit URL; on the card view the title reuses `#task-view`'s `data-edit-url`.
- **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", grouped as on
the page:
**Board card readability**
- **Always show the comment icon on board cards** -- default on. - **Always show the comment icon on board cards** -- default on.
- **Emphasize due date by default** -- default off (overridable per board under Settings -> Integrations).
**Board filters** (shared custom filters added to every board)
- **Add a "Show all tasks" filter to every board** -- default on.
- **Add month filters to every board** -- default on.
- **Add recurring filters to every board** -- default on.
**Card interaction**
- **Open a card only on a quick click** -- default on.
- **Show keyboard shortcuts on the task menu** -- default on.
- **Clicking a task title opens the edit form** -- default on.
Standalone:
- **Keep the board scroll position across refreshes** -- default on.
- **Enhance the recurrence dialog** -- default on.
## Requirements ## Requirements
@@ -54,3 +136,18 @@ name). No build step and no database migration.
## License ## License
AGPL-3.0. See LICENSE. AGPL-3.0. See LICENSE.
## More Kanboard plugins by Ruben (drbeco)
All free and AGPL-3.0, at [code.beco.cc](https://code.beco.cc/beco):
- **[RecoReco](https://code.beco.cc/beco/RecoReco)** -- calendar-scheduled recurring cards (yearly,
monthly, weekly, daily), driven by the card due date.
- **[FinanceBuddy](https://code.beco.cc/beco/FinanceBuddy)** -- attach a money value (debit/credit
and installments) to cards, shown on the card and totalled per column.
- **[BulkMoveTasks](https://code.beco.cc/beco/BulkMoveTasks)** -- move every task from one board
column to another in a single action.
- **[ShrinkVertically](https://code.beco.cc/beco/ShrinkVertically)** -- shrink vertically-collapsed
board columns so the horizontal scrollbar stays within reach.
- **[TweakDrag](https://code.beco.cc/beco/TweakDrag)** -- board drag and touch niceties:
drag-to-scroll, a wider column gap, and smoother card dragging.

View File

@@ -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 ?>

View File

@@ -0,0 +1,3 @@
<?php if ($this->user->hasProjectAccess('TaskModificationController', 'edit', $task['project_id'])): ?>
<span class="organon-edit-url" style="display:none" data-url="<?= $this->url->href('TaskModificationController', 'edit', array('task_id' => $task['id'])) ?>"></span>
<?php endif ?>

View File

@@ -6,12 +6,55 @@
<?= $this->form->csrf() ?> <?= $this->form->csrf() ?>
<fieldset> <fieldset>
<legend><?= t('Board card readability') ?></legend>
<?= $this->form->checkbox('organon_tweaks_always_comment_icon', t('Always show the comment icon on board cards'), 1, isset($values['organon_tweaks_always_comment_icon']) && $values['organon_tweaks_always_comment_icon'] == 1) ?> <?= $this->form->checkbox('organon_tweaks_always_comment_icon', t('Always show the comment icon on board cards'), 1, isset($values['organon_tweaks_always_comment_icon']) && $values['organon_tweaks_always_comment_icon'] == 1) ?>
<p class="form-help"><?= t('A "+" bubble on cards with no comments yet, to add the first one from the board.') ?></p>
<?= $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('Enlarge and bold the due date; the default for boards set to "Use default" (overridable per board under Settings -> Integrations).') ?></p>
</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"> <p class="form-help">
<?= t('Show a comment bubble with a "+" on cards that have no comments yet, so you can add the first comment straight from the board. Cards that already have comments keep showing the count as usual.') ?> <?= 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> </p>
</fieldset> </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 radios, and add a [+7] button on the factor to add a week at once.') ?>
</p>
</fieldset>
<fieldset>
<legend><?= t('Board filters') ?></legend>
<?= $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('One-click clear that shows open and closed tasks.') ?></p>
<?= $this->form->checkbox('organon_tweaks_month_filters', t('Add month filters to every board'), 1, isset($values['organon_tweaks_month_filters']) && $values['organon_tweaks_month_filters'] == 1) ?>
<p class="form-help"><?= t('"Due this month" and "Around this month" (duemonth: / aroundmonth: keywords).') ?></p>
<?= $this->form->checkbox('organon_tweaks_recurring_filters', t('Add recurring filters to every board'), 1, isset($values['organon_tweaks_recurring_filters']) && $values['organon_tweaks_recurring_filters'] == 1) ?>
<p class="form-help"><?= t('Adds recurrence bookmarks. Also a recurring:XY search keyword -- X = evt/cal/any (native/calendar/both), Y = ori/dup/all (template/duplicate/both). Example: recurring:evtori.') ?></p>
</fieldset>
<fieldset>
<legend><?= t('Card interaction') ?></legend>
<?= $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 held longer than 120 ms is treated as a drag, so releasing it does not open the card. Fixes a drag sometimes opening the card on release.') ?></p>
<?= $this->form->checkbox('organon_tweaks_shortcut_labels', t('Show keyboard shortcuts on the task menu'), 1, isset($values['organon_tweaks_shortcut_labels']) && $values['organon_tweaks_shortcut_labels'] == 1) ?>
<p class="form-help"><?= t('Adds the existing key hints (e, s, c, l) next to the task Actions items.') ?></p>
<?= $this->form->checkbox('organon_tweaks_title_click_edit', t('Clicking a task title opens the edit form'), 1, isset($values['organon_tweaks_title_click_edit']) && $values['organon_tweaks_title_click_edit'] == 1) ?>
<p class="form-help"><?= t('On the board and the card view, click the title to open the edit modal (only where you may edit).') ?></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>

View File

@@ -0,0 +1,3 @@
<?php if ($this->OrganonProjectHelper->isEmphasizeDueDate()): ?>
<style>.task-date { font-size: 1.375em; font-weight: bold; }</style>
<?php endif ?>

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

View File

@@ -0,0 +1 @@
<?php $this->OrganonProjectHelper->ensureBoardFilters($project['id']) ?>

View File

@@ -1 +1 @@
OrganonTweaks v0.3 OrganonTweaks v1.8.3