1 Commits
v1.5 ... v1.6

8 changed files with 211 additions and 41 deletions

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.0';
} }
public function getPluginHomepage() public function getPluginHomepage()

View File

@@ -47,6 +47,13 @@
</p> </p>
</fieldset> </fieldset>
<fieldset>
<?= $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('Keep shared "Due this month" and "Around this month" custom filters on every board (using the duemonth: and aroundmonth: search keywords this plugin adds). They are append filters, so clicking them stacks onto the current filter. Turning this off removes them from all boards; turning it on recreates them everywhere.') ?>
</p>
</fieldset>
<div class="form-actions"> <div class="form-actions">
<button type="submit" class="btn btn-blue"><?= t('Save') ?></button> <button type="submit" class="btn btn-blue"><?= t('Save') ?></button>
</div> </div>

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