23 Commits

Author SHA1 Message Date
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
16 changed files with 677 additions and 17 deletions

View File

@@ -13,7 +13,7 @@
(function () { (function () {
"use strict"; "use strict";
var THRESHOLD = 100; // ms; a press on a card held longer than this is a drag, not a click var THRESHOLD = 120; // ms; a press on a card held longer than this is a drag, not a click
var pressStart = 0; var pressStart = 0;
function onDown(e) { function onDown(e) {

View File

@@ -18,6 +18,8 @@
var group = document.createElement("span"); var group = document.createElement("span");
group.className = "ot-recurrence-status"; group.className = "ot-recurrence-status";
var radios = [];
for (var i = 0; i < select.options.length; i++) { for (var i = 0; i < select.options.length; i++) {
var opt = select.options[i]; var opt = select.options[i];
@@ -30,15 +32,21 @@
var radio = document.createElement("input"); var radio = document.createElement("input");
radio.type = "radio"; radio.type = "radio";
radio.name = "ot-recurrence-status"; // 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.value = opt.value;
radio.checked = opt.value === select.value; radio.checked = opt.value === select.value;
radio.style.marginRight = "4px"; radio.style.marginRight = "4px";
radios.push(radio);
radio.addEventListener("change", function () { radio.addEventListener("change", function () {
if (this.checked) { for (var k = 0; k < radios.length; k++) {
radios[k].checked = (radios[k] === this);
}
select.value = this.value; select.value = this.value;
select.dispatchEvent(new Event("change", { bubbles: true })); select.dispatchEvent(new Event("change", { bubbles: true }));
}
}); });
label.appendChild(radio); label.appendChild(radio);

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

@@ -18,6 +18,12 @@ class ConfigController extends BaseController
'organon_tweaks_keep_scroll' => (int) $this->configModel->get('organon_tweaks_keep_scroll', 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_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_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(),
))); )));
@@ -30,17 +36,53 @@ class ConfigController extends BaseController
$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; $keepScroll = isset($values['organon_tweaks_keep_scroll']) ? 1 : 0;
$quickClickOnly = isset($values['organon_tweaks_quick_click_only']) ? 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( if ($this->configModel->save(array(
'organon_tweaks_always_comment_icon' => $alwaysCommentIcon, 'organon_tweaks_always_comment_icon' => $alwaysCommentIcon,
'organon_tweaks_keep_scroll' => $keepScroll, 'organon_tweaks_keep_scroll' => $keepScroll,
'organon_tweaks_quick_click_only' => $quickClickOnly, '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,6 +20,13 @@ 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.
@@ -50,12 +57,57 @@ class Plugin extends Base
'template' => 'plugins/OrganonTweaks/Asset/js/enhance-recurrence.js', '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'),
); );
} }
@@ -76,7 +128,7 @@ class Plugin extends Base
public function getPluginVersion() public function getPluginVersion()
{ {
return '1.3.1'; return '1.8.2';
} }
public function getPluginHomepage() public function getPluginHomepage()

View File

@@ -93,3 +93,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

@@ -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,10 +6,13 @@
<?= $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"> <p class="form-help"><?= t('A "+" bubble on cards with no comments yet, to add the first one from the board.') ?></p>
<?= 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.') ?>
</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>
<fieldset> <fieldset>
@@ -20,17 +23,36 @@
</fieldset> </fieldset>
<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) ?> <?= $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"> <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.') ?> <?= 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> </p>
</fieldset> </fieldset>
<fieldset> <fieldset>
<?= $this->form->checkbox('organon_tweaks_enhance_recurrence', t('Enhance the recurrence dialog'), 1, isset($values['organon_tweaks_enhance_recurrence']) && $values['organon_tweaks_enhance_recurrence'] == 1) ?> <legend><?= t('Board filters') ?></legend>
<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).') ?> <?= $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> <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> </fieldset>
<div class="form-actions"> <div class="form-actions">

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 v1.3.1 OrganonTweaks v1.8.2