4 Commits
v1.5 ... v1.6.1

9 changed files with 221 additions and 56 deletions

View File

@@ -13,7 +13,7 @@
(function () { (function () {
"use strict"; "use strict";
var THRESHOLD = 110; // 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

@@ -20,6 +20,7 @@ class ConfigController extends BaseController
'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_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_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),
), ),
'errors' => array(), 'errors' => array(),
))); )));
@@ -35,6 +36,7 @@ class ConfigController extends BaseController
$enhanceRecurrence = isset($values['organon_tweaks_enhance_recurrence']) ? 1 : 0; $enhanceRecurrence = isset($values['organon_tweaks_enhance_recurrence']) ? 1 : 0;
$emphasizeDueDateDefault = isset($values['organon_tweaks_emphasize_duedate_default']) ? 1 : 0; $emphasizeDueDateDefault = isset($values['organon_tweaks_emphasize_duedate_default']) ? 1 : 0;
$showAllFilter = isset($values['organon_tweaks_show_all_filter']) ? 1 : 0; $showAllFilter = isset($values['organon_tweaks_show_all_filter']) ? 1 : 0;
$monthFilters = isset($values['organon_tweaks_month_filters']) ? 1 : 0;
if ($this->configModel->save(array( if ($this->configModel->save(array(
'organon_tweaks_always_comment_icon' => $alwaysCommentIcon, 'organon_tweaks_always_comment_icon' => $alwaysCommentIcon,
@@ -43,20 +45,27 @@ class ConfigController extends BaseController
'organon_tweaks_enhance_recurrence' => $enhanceRecurrence, 'organon_tweaks_enhance_recurrence' => $enhanceRecurrence,
'organon_tweaks_emphasize_duedate_default' => $emphasizeDueDateDefault, 'organon_tweaks_emphasize_duedate_default' => $emphasizeDueDateDefault,
'organon_tweaks_show_all_filter' => $showAllFilter, 'organon_tweaks_show_all_filter' => $showAllFilter,
'organon_tweaks_month_filters' => $monthFilters,
))) { ))) {
$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 // Apply each filter-group toggle across all boards immediately: on -> (re)create the group's
// filter everywhere, off -> remove every one (it is a plugin-managed, reserved-name filter). // reserved-name filters everywhere, off -> remove them (they are plugin-managed).
$projectHelper = new \Kanboard\Plugin\OrganonTweaks\Helper\OrganonProjectHelper($this->container); $projectHelper = new \Kanboard\Plugin\OrganonTweaks\Helper\OrganonProjectHelper($this->container);
if ($showAllFilter === 1) { if ($showAllFilter === 1) {
$projectHelper->addShowAllFilterToAllProjects(); $projectHelper->addGroupToAllProjects($projectHelper::GROUP_SHOW_ALL);
} else { } else {
$projectHelper->removeAllShowAllFilters(); $projectHelper->removeGroup($projectHelper::GROUP_SHOW_ALL);
}
if ($monthFilters === 1) {
$projectHelper->addGroupToAllProjects($projectHelper::GROUP_MONTH);
} else {
$projectHelper->removeGroup($projectHelper::GROUP_MONTH);
} }
$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

@@ -16,19 +16,29 @@ use Kanboard\Model\CustomFilterModel;
* MetadataModel::get() uses `?:`, and PHP treats the string "0" as falsy, so a stored "0" would * 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. * 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 * Also maintains shared, reserved-name custom filters on every board (v1.5 "Show all tasks", v1.6
* is always a one-click way to clear the board filter and reveal open + closed tasks. * "Due this month" / "Around this month"), each group gated by its own config toggle.
*/ */
class OrganonProjectHelper extends Base class OrganonProjectHelper extends Base
{ {
const EMPHASIZE_KEY = 'organon_tweaks_emphasize_duedate'; const EMPHASIZE_KEY = 'organon_tweaks_emphasize_duedate';
const EMPHASIZE_DEFAULT_KEY = 'organon_tweaks_emphasize_duedate_default'; 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 SHOW_ALL_FILTER_KEY = 'organon_tweaks_show_all_filter';
const MONTH_FILTERS_KEY = 'organon_tweaks_month_filters';
// The plugin RESERVES this custom-filter name: a fixed (non-translated) string so the dedup is // Reserved custom-filter groups: each row is [name, query, append]. The NAME is reserved by the
// stable across locales. A user's own filter with this name is absorbed into the canonical form. // plugin (fixed, non-translated strings so dedup is stable across locales); a user's own filter
const SHOW_ALL_FILTER_NAME = 'Show all tasks'; // 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).
const GROUP_SHOW_ALL = array(
array('Show all tasks', '', 0),
);
const GROUP_MONTH = array(
array('Due this month', 'duemonth:this', 1),
array('Around this month', 'aroundmonth:this', 1),
);
/** /**
* Should the current board emphasize the due date? Board pages only (project_id in the route). * Should the current board emphasize the due date? Board pages only (project_id in the route).
@@ -58,15 +68,17 @@ class OrganonProjectHelper extends Base
} }
/** /**
* Ensure the board has exactly one canonical "Show all tasks" filter (empty query, shared). * Ensure one canonical reserved custom filter on a board (shared). Idempotent: create when
* Idempotent: create it when absent, force a same-named one back to the canonical form (reserved * absent, force a same-named one back to the canonical form (query + shared + append) since the
* name), and drop any duplicates. Called lazily on every project-header render, so it covers old * name is reserved, and drop duplicates. No-op without a real project and a real owner
* and new boards alike. No-op without a real project and a real owner (custom_filters.user_id has * (custom_filters.user_id has a FK to users, so a system id like 0 would fail the insert).
* a FK to users, so a system id like 0 would fail the insert).
* *
* @param int $project_id * @param int $project_id
* @param string $name
* @param string $query
* @param int $append
*/ */
public function ensureShowAllFilter($project_id) public function ensureReservedFilter($project_id, $name, $query, $append)
{ {
$project_id = (int) $project_id; $project_id = (int) $project_id;
$user_id = (int) $this->userSession->getId(); $user_id = (int) $this->userSession->getId();
@@ -75,30 +87,32 @@ class OrganonProjectHelper extends Base
return; return;
} }
$append = $append ? 1 : 0;
$existing = $this->db->table(CustomFilterModel::TABLE) $existing = $this->db->table(CustomFilterModel::TABLE)
->eq('project_id', $project_id) ->eq('project_id', $project_id)
->eq('name', self::SHOW_ALL_FILTER_NAME) ->eq('name', $name)
->findAll(); ->findAll();
if (empty($existing)) { if (empty($existing)) {
$this->db->table(CustomFilterModel::TABLE)->insert(array( $this->db->table(CustomFilterModel::TABLE)->insert(array(
'filter' => '', 'filter' => $query,
'project_id' => $project_id, 'project_id' => $project_id,
'user_id' => $user_id, 'user_id' => $user_id,
'name' => self::SHOW_ALL_FILTER_NAME, 'name' => $name,
'is_shared' => 1, 'is_shared' => 1,
'append' => 0, 'append' => $append,
)); ));
return; return;
} }
// Reserved name: keep one, force it to the canonical empty + shared form, drop the rest. // Reserved name: keep one, force it to the canonical form, drop the rest.
$keep = array_shift($existing); $keep = array_shift($existing);
if ($keep['filter'] !== '' || (int) $keep['is_shared'] !== 1) { if ($keep['filter'] !== $query || (int) $keep['is_shared'] !== 1 || (int) $keep['append'] !== $append) {
$this->db->table(CustomFilterModel::TABLE) $this->db->table(CustomFilterModel::TABLE)
->eq('id', $keep['id']) ->eq('id', $keep['id'])
->update(array('filter' => '', 'is_shared' => 1)); ->update(array('filter' => $query, 'is_shared' => 1, 'append' => $append));
} }
foreach ($existing as $duplicate) { foreach ($existing as $duplicate) {
@@ -107,23 +121,55 @@ class OrganonProjectHelper extends Base
} }
/** /**
* Toggle turned ON: eagerly (re)create the "Show all tasks" filter on every existing board. * Ensure every ENABLED group's filters on a board. Called lazily on each project-header render,
* ensureShowAllFilter() keeps covering boards created afterwards. * so it covers old and new boards alike.
*
* @param int $project_id
*/ */
public function addShowAllFilterToAllProjects() public function ensureBoardFilters($project_id)
{ {
foreach ($this->projectModel->getAll() as $project) { if ((int) $this->configModel->get(self::SHOW_ALL_FILTER_KEY, 1) === 1) {
$this->ensureShowAllFilter((int) $project['id']); $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);
}
}
private function ensureGroup($project_id, array $rows)
{
foreach ($rows as $row) {
$this->ensureReservedFilter($project_id, $row[0], $row[1], $row[2]);
} }
} }
/** /**
* Toggle turned OFF: remove every "Show all tasks" filter across all boards (reserved name). * 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 removeAllShowAllFilters() public function addGroupToAllProjects(array $rows)
{ {
$this->db->table(CustomFilterModel::TABLE) foreach ($this->projectModel->getAll() as $project) {
->eq('name', self::SHOW_ALL_FILTER_NAME) $this->ensureGroup((int) $project['id'], $rows);
->remove(); }
}
/**
* 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

@@ -58,11 +58,27 @@ class Plugin extends Base
)); ));
} }
// Tweak: keep a shared "Show all tasks" custom filter (empty query -> open + closed) on every // Auto-managed shared custom filters (v1.5 "Show all tasks", v1.6 month filters). Ensured
// board as a one-click "clear the filter". Ensured lazily when a project header renders, so // lazily whenever a project header renders (covers old + new boards); each group is gated by
// it covers old and new boards. The config toggle bulk-adds/removes them (ConfigController). // its own config toggle inside OrganonProjectHelper::ensureBoardFilters(). The toggles also
if ((int) $this->configModel->get('organon_tweaks_show_all_filter', 1) === 1) { // bulk add/remove the filters (ConfigController).
$this->template->hook->attach('template:project:header:before', 'organonTweaks:project_header/show_all_filter'); $showAllFilters = (int) $this->configModel->get('organon_tweaks_show_all_filter', 1) === 1;
$monthFilters = (int) $this->configModel->get('organon_tweaks_month_filters', 1) === 1;
if ($showAllFilters || $monthFilters) {
$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` = a whole calendar month; `aroundmonth:VALUE` = that month padded by 10
// days on each side. 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));
});
} }
} }
@@ -90,7 +106,7 @@ class Plugin extends Base
public function getPluginVersion() public function getPluginVersion()
{ {
return '1.5.0'; return '1.6.1';
} }
public function getPluginHomepage() public function getPluginHomepage()

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>
@@ -22,29 +25,28 @@
<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_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"> <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('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> </p>
</fieldset> </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) ?> <?= $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('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).') ?> <?= 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_emphasize_duedate_default', t('Emphasize due date by default'), 1, isset($values['organon_tweaks_emphasize_duedate_default']) && $values['organon_tweaks_emphasize_duedate_default'] == 1) ?> <legend><?= t('Board filters') ?></legend>
<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) ?> <?= $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"> <p class="form-help"><?= t('One-click clear that shows open and closed tasks.') ?></p>
<?= 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> <?= $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('Filter by recurrence (recurring: keyword) -- not active yet.') ?></p>
</fieldset> </fieldset>
<div class="form-actions"> <div class="form-actions">

View File

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

View File

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

View File

@@ -1 +1 @@
OrganonTweaks v1.5 OrganonTweaks v1.6.1