13 Commits
v1.5 ... v1.8

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

View File

@@ -13,7 +13,7 @@
(function () {
"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;
function onDown(e) {

39
Asset/js/save-close.js Normal file
View File

@@ -0,0 +1,39 @@
/*
* OrganonTweaks -- the "Save & Close" button on the task edit form. It is a plain type=button (so it
* never becomes the Enter default -- Enter still triggers the core Save), and on click it repoints
* the form to the plugin's TaskSaveCloseController and lets Kanboard's own modal submit it. Kanboard
* keeps ownership of the POST, CSRF, error re-render, and the X-Ajax-Redirect navigation; we only
* change the destination, so the controller redirects to the board instead of the card view.
*/
(function () {
"use strict";
document.addEventListener("click", function (e) {
if (!e.target || !e.target.closest) {
return;
}
var btn = e.target.closest("[data-organon-save-close]");
if (!btn) {
return;
}
e.preventDefault();
var form = btn.closest("form");
var url = btn.getAttribute("data-url");
if (!form || !url) {
return;
}
form.setAttribute("action", url);
if (window.KB && KB.modal && KB.modal.isOpen && KB.modal.isOpen() && KB.modal.submitForm) {
KB.modal.submitForm();
} else if (form.requestSubmit) {
form.requestSubmit();
} else {
form.submit();
}
}, false);
})();

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

@@ -20,6 +20,11 @@ class ConfigController extends BaseController
'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),
'organon_tweaks_save_close_button' => (int) $this->configModel->get('organon_tweaks_save_close_button', 1),
),
'errors' => array(),
)));
@@ -35,6 +40,11 @@ class ConfigController extends BaseController
$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;
$saveCloseButton = isset($values['organon_tweaks_save_close_button']) ? 1 : 0;
if ($this->configModel->save(array(
'organon_tweaks_always_comment_icon' => $alwaysCommentIcon,
@@ -43,20 +53,37 @@ class ConfigController extends BaseController
'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,
'organon_tweaks_save_close_button' => $saveCloseButton,
))) {
$this->flash->success(t('Settings saved successfully.'));
} else {
$this->flash->failure(t('Unable to save your settings.'));
}
// Apply the "Show all tasks" toggle across all boards immediately: on -> (re)create the
// filter everywhere, off -> remove every one (it is a plugin-managed, reserved-name filter).
// 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->addShowAllFilterToAllProjects();
$projectHelper->addGroupToAllProjects($projectHelper::GROUP_SHOW_ALL);
} else {
$projectHelper->removeAllShowAllFilters();
$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')));

View File

@@ -0,0 +1,33 @@
<?php
namespace Kanboard\Plugin\OrganonTweaks\Controller;
use Kanboard\Controller\TaskModificationController;
/**
* "Save & Close": the exact core task update, but on success it redirects to the BOARD instead of
* reopening the card view. The plugin's "Save & Close" button repoints the edit form here (via JS),
* so everything else -- validation, permission checks, CSRF, and the modal error re-render -- is
* inherited unchanged from core TaskModificationController. This mirrors core update() line for line
* except the one redirect target (kept in sync on Kanboard upgrades).
*/
class TaskSaveCloseController extends TaskModificationController
{
public function update()
{
$task = $this->getTask();
$values = $this->request->getValues();
$values['id'] = $task['id'];
$values['project_id'] = $task['project_id'];
list($valid, $errors) = $this->taskValidator->validateModification($values);
if ($valid && $this->updateTask($task, $values, $errors)) {
$this->flash->success(t('Task updated successfully.'));
$this->response->redirect($this->helper->url->to('BoardViewController', 'show', array('project_id' => $task['project_id'])), true);
} else {
$this->flash->failure(t('Unable to update your task.'));
$this->edit($values, $errors);
}
}
}

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

@@ -16,19 +16,39 @@ use Kanboard\Model\CustomFilterModel;
* MetadataModel::get() uses `?:`, and PHP treats the string "0" as falsy, so a stored "0" would
* come back as the 'default' fallback and "Always off" would never be honored.
*
* Also maintains a shared "Show all tasks" custom filter (an empty query) on every board, so there
* is always a one-click way to clear the board filter and reveal open + closed tasks.
* 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';
// The plugin RESERVES this custom-filter name: a fixed (non-translated) string so the dedup is
// stable across locales. A user's own filter with this name is absorbed into the canonical form.
const SHOW_ALL_FILTER_NAME = 'Show all tasks';
// 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).
@@ -58,15 +78,17 @@ class OrganonProjectHelper extends Base
}
/**
* Ensure the board has exactly one canonical "Show all tasks" filter (empty query, shared).
* Idempotent: create it when absent, force a same-named one back to the canonical form (reserved
* name), and drop any duplicates. Called lazily on every project-header render, so it covers old
* and new boards alike. No-op without a real project and a real owner (custom_filters.user_id has
* a FK to users, so a system id like 0 would fail the insert).
* 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 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;
$user_id = (int) $this->userSession->getId();
@@ -75,30 +97,32 @@ class OrganonProjectHelper extends Base
return;
}
$append = $append ? 1 : 0;
$existing = $this->db->table(CustomFilterModel::TABLE)
->eq('project_id', $project_id)
->eq('name', self::SHOW_ALL_FILTER_NAME)
->eq('name', $name)
->findAll();
if (empty($existing)) {
$this->db->table(CustomFilterModel::TABLE)->insert(array(
'filter' => '',
'filter' => $query,
'project_id' => $project_id,
'user_id' => $user_id,
'name' => self::SHOW_ALL_FILTER_NAME,
'name' => $name,
'is_shared' => 1,
'append' => 0,
'append' => $append,
));
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);
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)
->eq('id', $keep['id'])
->update(array('filter' => '', 'is_shared' => 1));
->update(array('filter' => $query, 'is_shared' => 1, 'append' => $append));
}
foreach ($existing as $duplicate) {
@@ -107,23 +131,59 @@ class OrganonProjectHelper extends Base
}
/**
* Toggle turned ON: eagerly (re)create the "Show all tasks" filter on every existing board.
* ensureShowAllFilter() keeps covering boards created afterwards.
* 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 addShowAllFilterToAllProjects()
public function ensureBoardFilters($project_id)
{
foreach ($this->projectModel->getAll() as $project) {
$this->ensureShowAllFilter((int) $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 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)
->eq('name', self::SHOW_ALL_FILTER_NAME)
->remove();
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

@@ -58,11 +58,60 @@ class Plugin extends Base
));
}
// Tweak: keep a shared "Show all tasks" custom filter (empty query -> open + closed) on every
// board as a one-click "clear the filter". Ensured lazily when a project header renders, so
// it covers old and new boards. The config toggle bulk-adds/removes them (ConfigController).
if ((int) $this->configModel->get('organon_tweaks_show_all_filter', 1) === 1) {
$this->template->hook->attach('template:project:header:before', 'organonTweaks:project_header/show_all_filter');
// 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');
}
// Tweak: a "Save & Close" button on the task edit form that saves and returns to the board
// (core only reopens the card view). The button posts to a small plugin controller that
// reuses core's update and changes only the redirect; the JS repoints the form action, so
// Kanboard's modal still owns the submit, CSRF, and error re-render.
if ((int) $this->configModel->get('organon_tweaks_save_close_button', 1) === 1) {
$this->template->hook->attach('template:task:form:bottom-before-buttons', 'organonTweaks:task_form/save_close_button');
$this->hook->on('template:layout:js', array(
'template' => 'plugins/OrganonTweaks/Asset/js/save-close.js',
));
}
// 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']));
});
}
}
@@ -90,7 +139,7 @@ class Plugin extends Base
public function getPluginVersion()
{
return '1.5.0';
return '1.8.0';
}
public function getPluginHomepage()

View File

@@ -93,3 +93,18 @@ name). No build step and no database migration.
## 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() ?>
<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) ?>
<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.') ?>
</p>
<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>
@@ -22,29 +25,41 @@
<fieldset>
<?= $this->form->checkbox('organon_tweaks_quick_click_only', t('Open a card only on a quick click'), 1, isset($values['organon_tweaks_quick_click_only']) && $values['organon_tweaks_quick_click_only'] == 1) ?>
<p class="form-help">
<?= t('A press on a card held longer than 100 ms is treated as a drag, so releasing it does not open the card. Fixes a drag sometimes opening the card on release. Normal clicks are well under 100 ms.') ?>
<?= 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>
</fieldset>
<fieldset>
<?= $this->form->checkbox('organon_tweaks_enhance_recurrence', t('Enhance the native recurrence dialog'), 1, isset($values['organon_tweaks_enhance_recurrence']) && $values['organon_tweaks_enhance_recurrence'] == 1) ?>
<p class="form-help">
<?= t('In the native "Edit recurrence" dialog, replace the Generate recurrent task dropdown with Yes/No radio buttons (one click), and add a [+7] button next to the factor that adds 7 (one week of days).') ?>
<?= 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>
<?= $this->form->checkbox('organon_tweaks_emphasize_duedate_default', t('Emphasize due date by default'), 1, isset($values['organon_tweaks_emphasize_duedate_default']) && $values['organon_tweaks_emphasize_duedate_default'] == 1) ?>
<p class="form-help">
<?= t('Default for boards set to "Use default": enlarge and bold the due date on the board. Each board can override this under its own Settings -> Integrations.') ?>
</p>
</fieldset>
<legend><?= t('Board filters') ?></legend>
<fieldset>
<?= $this->form->checkbox('organon_tweaks_show_all_filter', t('Add a "Show all tasks" filter to every board'), 1, isset($values['organon_tweaks_show_all_filter']) && $values['organon_tweaks_show_all_filter'] == 1) ?>
<p class="form-help">
<?= t('Keep a shared "Show all tasks" custom filter (an empty query, so open and closed tasks are shown) on every board, old and new. It appears in the board filter bookmark menu as a one-click way to clear the filter. Turning this off removes it from all boards; turning it on recreates it everywhere.') ?>
</p>
<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_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>
<?= $this->form->checkbox('organon_tweaks_save_close_button', t('Add a "Save & Close" button to the task edit form'), 1, isset($values['organon_tweaks_save_close_button']) && $values['organon_tweaks_save_close_button'] == 1) ?>
<p class="form-help"><?= t('Saves the task and returns to the board, instead of reopening the card view.') ?></p>
</fieldset>
<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

@@ -0,0 +1,8 @@
<?php $task_id = isset($values['id']) ? (int) $values['id'] : 0 ?>
<?php if ($task_id > 0): ?>
<button type="button" class="btn"
data-organon-save-close="1"
data-url="<?= $this->url->href('TaskSaveCloseController', 'update', array('plugin' => 'OrganonTweaks', 'task_id' => $task_id)) ?>">
<?= t('Save & Close') ?>
</button>
<?php endif ?>

View File

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