14 Commits

Author SHA1 Message Date
8be9442cb7 title sort via accent-stripped dashed slug (fixes names, keeps numbers) (v1.9.1) 2026-07-13 14:38:31 -03:00
f9a7637e44 OrganonTweaks: per-column persistent sort -- number-aware title sort + sticky toggle (v1.9.0) 2026-07-13 10:32:04 -03:00
d079198103 feature : persistent sort 2026-07-13 10:25:28 -03:00
7ba4348de1 README ASCII 2026-07-13 08:56:18 -03:00
019b08084b README: More-plugins section -> Dr. Beco heading + full sibling list (v1.8.5) 2026-07-13 08:49:00 -03:00
2f78aa2087 separate the Integrations section with a trailing rule (v1.8.4) 2026-07-11 15:42:36 -03:00
8057272b9c document card-interaction tweaks in README (v1.8.3) 2026-07-10 18:35:42 -03:00
dc22739647 v1.8.2 admin global settings groupped 2026-07-10 18:30:34 -03:00
2d3c716852 v1.8.1 stage D button save & close unnecessary bugfix 2026-07-10 18:26:44 -03:00
269964d4cc add Save & Close button on the task edit form v1.8 t2 2026-07-10 18:18:26 -03:00
ac8e3b9544 add Save & Close button on the task edit form v1.8 2026-07-10 18:13:19 -03:00
717095848b card-view title click opens the edit modal 2026-07-10 18:09:06 -03:00
a905079abc board card title click opens the edit modal 2026-07-10 18:06:24 -03:00
e032d5d2b4 show existing keyboard-shortcut hints on the task Actions menu 2026-07-10 17:56:55 -03:00
13 changed files with 601 additions and 15 deletions

View File

@@ -70,9 +70,29 @@
} }
} }
function hideNativeSort() {
// Only when our persistent-sort control is present (the tweak is on); otherwise leave the
// native fa-sort menu alone.
if (!document.querySelector(".organontweaks-sort")) {
return;
}
var sorts = document.querySelectorAll("#board .fa-sort");
for (var i = 0; i < sorts.length; i++) {
var dd = sorts[i].closest(".dropdown");
// Skip our own control (its OFF-state icon is also fa-sort).
if (dd && !dd.classList.contains("organontweaks-sort")) {
dd.style.display = "none";
}
}
}
function apply() { function apply() {
relocateColumnMenuItems(); relocateColumnMenuItems();
repositionCommentIcons(); repositionCommentIcons();
hideNativeSort();
} }
function init() { function init() {

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

@@ -22,6 +22,9 @@ class ConfigController extends BaseController
'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), '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_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_persistent_sort' => (int) $this->configModel->get('organon_tweaks_persistent_sort', 1),
), ),
'errors' => array(), 'errors' => array(),
))); )));
@@ -39,6 +42,9 @@ class ConfigController extends BaseController
$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; $monthFilters = isset($values['organon_tweaks_month_filters']) ? 1 : 0;
$recurringFilters = isset($values['organon_tweaks_recurring_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;
$persistentSort = isset($values['organon_tweaks_persistent_sort']) ? 1 : 0;
if ($this->configModel->save(array( if ($this->configModel->save(array(
'organon_tweaks_always_comment_icon' => $alwaysCommentIcon, 'organon_tweaks_always_comment_icon' => $alwaysCommentIcon,
@@ -49,6 +55,9 @@ class ConfigController extends BaseController
'organon_tweaks_show_all_filter' => $showAllFilter, 'organon_tweaks_show_all_filter' => $showAllFilter,
'organon_tweaks_month_filters' => $monthFilters, 'organon_tweaks_month_filters' => $monthFilters,
'organon_tweaks_recurring_filters' => $recurringFilters, 'organon_tweaks_recurring_filters' => $recurringFilters,
'organon_tweaks_shortcut_labels' => $shortcutLabels,
'organon_tweaks_title_click_edit' => $titleClickEdit,
'organon_tweaks_persistent_sort' => $persistentSort,
))) { ))) {
$this->flash->success(t('Settings saved successfully.')); $this->flash->success(t('Settings saved successfully.'));
} else { } else {

View File

@@ -0,0 +1,61 @@
<?php
namespace Kanboard\Plugin\OrganonTweaks\Controller;
use Kanboard\Controller\BaseController;
use Kanboard\Core\Controller\AccessForbiddenException;
use Kanboard\Plugin\OrganonTweaks\Helper\OrganonSortHelper;
/**
* Per-column persistent sort, driven from the column's sort control on the board. Project-member
* (task-update) gated + CSRF, mirroring core TaskReorderController; returns to the board.
*/
class OrganonSortController extends BaseController
{
private function authorize()
{
$project = $this->getProject();
if (! $this->helper->user->hasProjectAccess('TaskModificationController', 'update', $project['id'])) {
throw new AccessForbiddenException();
}
return $project;
}
private function sorter()
{
return new OrganonSortHelper($this->container);
}
private function backToBoard(array $project)
{
$this->response->redirect($this->helper->url->to('BoardViewController', 'show', array('project_id' => $project['id'])));
}
public function sort()
{
$this->checkCSRFParam();
$project = $this->authorize();
$this->sorter()->applySort(
$project['id'],
$this->request->getIntegerParam('swimlane_id'),
$this->request->getIntegerParam('column_id'),
$this->request->getStringParam('sort'),
$this->request->getStringParam('direction')
);
$this->backToBoard($project);
}
public function toggle()
{
$this->checkCSRFParam();
$project = $this->authorize();
$this->sorter()->toggle($project['id'], $this->request->getIntegerParam('column_id'));
$this->backToBoard($project);
}
}

View File

@@ -0,0 +1,176 @@
<?php
namespace Kanboard\Plugin\OrganonTweaks\Helper;
use Kanboard\Core\Base;
use Kanboard\Model\TaskModel;
/**
* Per-column persistent sort. Kanboard's native column sort is a one-shot physical reorder (writes
* task positions); this stores the chosen sort per column and, when "persistent" is on, re-applies it
* whenever a card is dropped in or created -- so the column stays sorted. Adds a number-aware title
* sort on top of the native criteria.
*
* Everything lives in project metadata, per column, as literal strings:
* organon_sort_by_<col> = id | priority | assignee-priority | assignee | due-date | title
* organon_sort_dir_<col> = asc | desc
* organon_sort_stick_<col> = on | off (default off = native one-shot behavior)
*/
class OrganonSortHelper extends Base
{
const BY_PREFIX = 'organon_sort_by_';
const DIR_PREFIX = 'organon_sort_dir_';
const STICK_PREFIX = 'organon_sort_stick_';
// The sort criteria this control offers. Native ones dispatch to core TaskReorderModel; 'title'
// is our own number-aware sort. Any value not here is ignored (no reorder, nothing stored).
const CRITERIA = array('id', 'priority', 'assignee-priority', 'assignee', 'due-date', 'title');
public function isSticky($project_id, $column_id)
{
return $this->projectMetadataModel->get((int) $project_id, self::STICK_PREFIX.(int) $column_id, 'off') === 'on';
}
public function sortBy($project_id, $column_id)
{
return (string) $this->projectMetadataModel->get((int) $project_id, self::BY_PREFIX.(int) $column_id, '');
}
public function sortDir($project_id, $column_id)
{
return $this->projectMetadataModel->get((int) $project_id, self::DIR_PREFIX.(int) $column_id, 'asc') === 'desc' ? 'desc' : 'asc';
}
/**
* Sort the column now and record the choice (for the icon + the sticky re-apply).
*/
public function applySort($project_id, $swimlane_id, $column_id, $sort, $direction)
{
if (! in_array($sort, self::CRITERIA, true)) {
return;
}
$direction = $direction === 'desc' ? 'desc' : 'asc';
$this->reorder((int) $project_id, (int) $swimlane_id, (int) $column_id, $sort, $direction);
$this->projectMetadataModel->save((int) $project_id, array(
self::BY_PREFIX.(int) $column_id => $sort,
self::DIR_PREFIX.(int) $column_id => $direction,
));
}
/**
* Re-apply the stored sort (called on card drop / creation when persistent is on). No-op if the
* column has no stored sort yet.
*/
public function reapply($project_id, $swimlane_id, $column_id)
{
$sort = $this->sortBy($project_id, $column_id);
if ($sort !== '' && in_array($sort, self::CRITERIA, true)) {
$this->reorder((int) $project_id, (int) $swimlane_id, (int) $column_id, $sort, $this->sortDir($project_id, $column_id));
}
}
/**
* Flip the persistent flag for a column; returns the new state.
*
* @return string 'on' | 'off'
*/
public function toggle($project_id, $column_id)
{
$new = $this->isSticky($project_id, $column_id) ? 'off' : 'on';
$this->projectMetadataModel->save((int) $project_id, array(self::STICK_PREFIX.(int) $column_id => $new));
return $new;
}
/**
* Dispatch the physical reorder: native criteria reuse core TaskReorderModel; 'title' is ours.
*/
private function reorder($project_id, $swimlane_id, $column_id, $sort, $direction)
{
switch ($sort) {
case 'id':
$this->taskReorderModel->reorderByTaskId($project_id, $swimlane_id, $column_id, $direction);
break;
case 'priority':
$this->taskReorderModel->reorderByPriority($project_id, $swimlane_id, $column_id, $direction);
break;
case 'assignee-priority':
$this->taskReorderModel->reorderByAssigneeAndPriority($project_id, $swimlane_id, $column_id, $direction);
break;
case 'assignee':
$this->taskReorderModel->reorderByAssignee($project_id, $swimlane_id, $column_id, $direction);
break;
case 'due-date':
$this->taskReorderModel->reorderByDueDate($project_id, $swimlane_id, $column_id, $direction);
break;
case 'title':
$this->reorderByTitle($project_id, $swimlane_id, $column_id, $direction);
break;
}
}
/**
* Title sort, then write positions. Mirrors the native reorder scope (all tasks in the
* column+swimlane, no status filter). Core's TaskReorderModel::reorderTasks is protected, so the
* 3-line position write is replicated here.
*
* Each title is reduced to a comparison slug -- lowercased, accent-stripped, spaces -> dashes --
* then compared with strnatcasecmp. The dash is deliberate: strnatcasecmp SKIPS whitespace, so a
* plain space would make "Luiz Eduardo" sort after "Luiza..." (it would compare "LuizEduardo" vs
* "Luiza..."); a dash is not whitespace, so the separator is honoured, while strnatcasecmp still
* groups digit runs ("Task 2" < "Task 10"). The result is case-insensitive, accent-insensitive,
* whitespace-correct and number-aware.
*/
private function reorderByTitle($project_id, $swimlane_id, $column_id, $direction)
{
$rows = $this->db->table(TaskModel::TABLE)
->columns('id', 'title')
->eq('project_id', $project_id)
->eq('swimlane_id', $swimlane_id)
->eq('column_id', $column_id)
->findAll();
foreach ($rows as &$row) {
$row['sortkey'] = str_replace(' ', '-', $this->stripAccents(mb_strtolower((string) $row['title'], 'UTF-8')));
}
unset($row);
usort($rows, function ($a, $b) use ($direction) {
$cmp = strnatcasecmp($a['sortkey'], $b['sortkey']);
return $direction === 'desc' ? -$cmp : $cmp;
});
$position = 1;
foreach ($rows as $row) {
$this->db->table(TaskModel::TABLE)->eq('id', $row['id'])->update(array('position' => $position));
$position++;
}
}
/**
* Flatten a string for sorting: strip accents to their base letter so accented names sort by that
* base (Avila near A, not after Z). Uses ext-intl Normalizer when available (deterministic and
* locale-independent), falling back to iconv //TRANSLIT (locale-dependent), then the raw string
* (still sorts, just accent-naive). Case is already lowered by the caller.
*
* @param string $s
* @return string
*/
private function stripAccents($s)
{
if (class_exists('Normalizer')) {
$decomposed = \Normalizer::normalize($s, \Normalizer::FORM_D);
if ($decomposed !== false) {
return preg_replace('/\p{Mn}/u', '', $decomposed);
}
}
$translit = @iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $s);
return $translit !== false ? $translit : $s;
}
}

View File

@@ -3,6 +3,7 @@
namespace Kanboard\Plugin\OrganonTweaks; namespace Kanboard\Plugin\OrganonTweaks;
use Kanboard\Core\Plugin\Base; use Kanboard\Core\Plugin\Base;
use Kanboard\Model\TaskModel;
class Plugin extends Base class Plugin extends Base
{ {
@@ -58,6 +59,58 @@ class Plugin extends Base
)); ));
} }
// 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: per-column persistent sort. A sort control (number-aware title sort + a
// "Persistent sort: ON/OFF" toggle) replaces the native column sort. Sorting always reorders
// physically (writes positions, like native); when persistent is ON, the column is re-sorted
// whenever a card is dropped in or created, so it stays sorted. Persistent OFF = native
// one-shot. Control injected via the column dropdown hook + relocate.js (which also hides the
// native fa-sort menu).
if ((int) $this->configModel->get('organon_tweaks_persistent_sort', 1) === 1) {
$this->template->hook->attach('template:board:column:dropdown', 'organonTweaks:board/sort_control');
$container = $this->container;
$reapply = function ($project_id, $swimlane_id, $column_id) use ($container) {
$helper = new \Kanboard\Plugin\OrganonTweaks\Helper\OrganonSortHelper($container);
if ($helper->isSticky($project_id, $column_id)) {
$helper->reapply($project_id, $swimlane_id, $column_id);
}
};
// Card dropped into a column -> re-apply that column's sort (the plugin on() wrapper drops
// the event, so subscribe on the dispatcher to receive the TaskEvent).
$this->dispatcher->addListener(TaskModel::EVENT_MOVE_COLUMN, function ($event) use ($reapply) {
$task = $event['task'];
$reapply((int) $task['project_id'], (int) $task['swimlane_id'], (int) $event['dst_column_id']);
});
// New card created in a column -> re-apply that column's sort.
$this->hook->on('model:task:creation:aftersave', function ($task_id) use ($container, $reapply) {
$task = $container['taskFinderModel']->getById($task_id);
if (! empty($task)) {
$reapply((int) $task['project_id'], (int) $task['swimlane_id'], (int) $task['column_id']);
}
});
}
// Auto-managed shared custom filters (v1.5 "Show all tasks", v1.6 month filters, v1.7 // 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 // recurring filters). Ensured lazily whenever a project header renders (covers old + new
// boards); each group is gated by its own config toggle inside // boards); each group is gated by its own config toggle inside
@@ -88,7 +141,7 @@ class Plugin extends Base
public function getHelpers() public function getHelpers()
{ {
return array( return array(
'Plugin\OrganonTweaks\Helper' => array('OrganonColumnHelper', 'OrganonProjectHelper'), 'Plugin\OrganonTweaks\Helper' => array('OrganonColumnHelper', 'OrganonProjectHelper', 'OrganonSortHelper'),
); );
} }
@@ -109,7 +162,7 @@ class Plugin extends Base
public function getPluginVersion() public function getPluginVersion()
{ {
return '1.7.2'; return '1.9.1';
} }
public function getPluginHomepage() public function getPluginHomepage()

View File

@@ -52,12 +52,37 @@ board stays where you were.
Dropping a card sometimes leaks a trailing "click" that opens the card -- an unwanted side Dropping a card sometimes leaks a trailing "click" that opens the card -- an unwanted side
effect of the drag. A real click is a quick press-release; a drag is a press held much effect of the drag. A real click is a quick press-release; a drag is a press held much
longer. So `Asset/js/click-guard.js` times the press on a card (`mousedown`/`touchstart` to longer. So `Asset/js/click-guard.js` times the press on a card (`mousedown`/`touchstart` to
the following `click`) and, if it was held longer than **100 ms**, swallows the click in the the following `click`) and, if it was held longer than **120 ms**, swallows the click in the
capture phase before Kanboard's open handler -- so a drag never opens the card. Normal capture phase before Kanboard's open handler -- so a drag never opens the card. Normal
clicks (well under 100 ms) still open cards as usual. The 100 ms threshold is fixed. clicks (well under 120 ms) still open cards as usual. The 120 ms threshold is fixed.
- **On by default.** Toggle it under "Settings -> Organon Tweaks". - **On by default.** Toggle it under "Settings -> Organon Tweaks".
### Show keyboard shortcuts on the task menu
Kanboard already binds handy keys on the task view (`e` = edit, `s` = new sub-task, `c` = new
comment, `l` = new internal link) but never shows them, so they go undiscovered. This tweak adds a
small key hint next to each of those items in the task **Actions** sidebar. It is **display only**:
it adds no new bindings, it just surfaces the ones Kanboard already has.
- Implemented in `Asset/js/shortcut-labels.js`, matching the menu items by their FontAwesome icon
class, so it is independent of the URL format and the interface language.
- **On by default.** Toggle it under "Settings -> Organon Tweaks".
### Click a title to edit
By default, clicking a card title (on the board) or the title on the card view lands you on the
read-only card view. With this tweak on, clicking the title opens the **edit** form directly,
reusing Kanboard's own modal -- so from the board you edit and, on Save, come straight back to the
board. It applies only where you may edit (a read-only viewer keeps the normal title), and
shared/public boards are untouched.
- Implemented in `Asset/js/title-click-edit.js` -- a capture-phase click that reuses `KB.modal.open`.
On the board, a hidden marker (`Template/board/title_edit.php`, rendered by
`template:board:private:task:before-title` only when the user may edit) carries the
server-generated edit URL; on the card view the title reuses `#task-view`'s `data-edit-url`.
- **On by default.** Toggle it under "Settings -> Organon Tweaks".
### Enhance the recurrence dialog ### Enhance the recurrence dialog
Kanboard's native "Edit recurrence" dialog is a plain form. This makes two small usability Kanboard's native "Edit recurrence" dialog is a plain form. This makes two small usability
@@ -71,13 +96,56 @@ edits when it opens (there is no template hook, so it is done in JS,
- **On by default.** Toggle it under "Settings -> Organon Tweaks". - **On by default.** Toggle it under "Settings -> Organon Tweaks".
### Persistent column sort
Kanboard's native column sort is a one-shot physical reorder: it rewrites task positions once, so
the order already survives refreshes, but a newly created or dropped-in card lands out of order
until you sort again -- and there is no sort-by-title. This tweak replaces the per-column sort
control with one that:
- adds a **number-aware Title sort** (A to Z / Z to A), so "Task 2" sorts before "Task 10";
- adds a **Persistent sort: ON / OFF** toggle. When ON, the column is re-sorted (server-side,
physically) whenever a card is dropped into it or created in it, so it **stays** sorted; when
OFF it behaves exactly like the native one-shot sort.
The header icon shows the state: the native double-arrow when persistent is OFF, and a **red
single arrow** (up for ascending, down for descending) when it is ON.
- Reorders reuse core's `TaskReorderModel` for the native criteria (id / priority / assignee /
due date) and add the title sort; the sticky re-apply runs on the move / creation events.
- The control is injected via the `template:board:column:dropdown` hook + `Asset/js/relocate.js`
(which also hides the native sort menu). Per-column state lives in project metadata.
- **On by default.** Toggle it under "Settings -> Organon Tweaks".
## Settings ## Settings
Global (per Kanboard instance) and admin-only, under "Settings -> Organon Tweaks": Global (per Kanboard instance) and admin-only, under "Settings -> Organon Tweaks", grouped as on
the page:
**Board card readability**
- **Always show the comment icon on board cards** -- default on. - **Always show the comment icon on board cards** -- default on.
- **Keep the board scroll position across refreshes** -- default on. - **Emphasize due date by default** -- default off (overridable per board under Settings -> Integrations).
**Board filters** (shared custom filters added to every board)
- **Add a "Show all tasks" filter to every board** -- default on.
- **Add month filters to every board** -- default on.
- **Add recurring filters to every board** -- default on.
**Card interaction**
- **Open a card only on a quick click** -- default on. - **Open a card only on a quick click** -- default on.
- **Show keyboard shortcuts on the task menu** -- default on.
- **Clicking a task title opens the edit form** -- default on.
**Board columns**
- **Per-column persistent sort** -- default on.
Standalone:
- **Keep the board scroll position across refreshes** -- default on.
- **Enhance the recurrence dialog** -- default on. - **Enhance the recurrence dialog** -- default on.
## Requirements ## Requirements
@@ -94,7 +162,7 @@ name). No build step and no database migration.
AGPL-3.0. See LICENSE. AGPL-3.0. See LICENSE.
## More Kanboard plugins by Ruben (drbeco) ## More Kanboard plugins by Dr. Beco
All free and AGPL-3.0, at [code.beco.cc](https://code.beco.cc/beco): All free and AGPL-3.0, at [code.beco.cc](https://code.beco.cc/beco):
@@ -102,6 +170,10 @@ All free and AGPL-3.0, at [code.beco.cc](https://code.beco.cc/beco):
monthly, weekly, daily), driven by the card due date. monthly, weekly, daily), driven by the card due date.
- **[FinanceBuddy](https://code.beco.cc/beco/FinanceBuddy)** -- attach a money value (debit/credit - **[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. and installments) to cards, shown on the card and totalled per column.
- **[QualKard](https://code.beco.cc/beco/QualKard)** -- flashcard spaced repetition: grade a card by
dragging it to a column, and it comes back when due (needs RecoReco).
- **[WorkspaceOrg](https://code.beco.cc/beco/WorkspaceOrg)** -- group your projects into personal
per-user workspaces, shown on a "My workspaces" dashboard page with a badge on each project.
- **[BulkMoveTasks](https://code.beco.cc/beco/BulkMoveTasks)** -- move every task from one board - **[BulkMoveTasks](https://code.beco.cc/beco/BulkMoveTasks)** -- move every task from one board
column to another in a single action. column to another in a single action.
- **[ShrinkVertically](https://code.beco.cc/beco/ShrinkVertically)** -- shrink vertically-collapsed - **[ShrinkVertically](https://code.beco.cc/beco/ShrinkVertically)** -- shrink vertically-collapsed

View File

@@ -0,0 +1,69 @@
<?php
// Per-column persistent-sort control. Rendered by template:board:column:dropdown (so it sits in the
// column header, as its own dropdown); relocate.js hides the native fa-sort menu in its favour.
// Icon: native double-arrow when persistent is off, a red single arrow (up=asc / down=desc) when on.
if ($column['nb_tasks'] <= 0 || ! $this->user->hasProjectAccess('TaskModificationController', 'update', $column['project_id'])) {
return;
}
$pid = $column['project_id'];
$cid = $column['id'];
$sid = $swimlane['id'];
$sticky = $this->OrganonSortHelper->isSticky($pid, $cid);
$curBy = $this->OrganonSortHelper->sortBy($pid, $cid);
$curDir = $this->OrganonSortHelper->sortDir($pid, $cid);
$applied = $curBy !== '';
$desc = $curDir === 'desc';
$criteria = array(
array('title', 'asc', t('Title (A to Z)')),
array('title', 'desc', t('Title (Z to A)')),
array('id', 'asc', t('Id (ASC)')),
array('id', 'desc', t('Id (DESC)')),
array('priority', 'asc', t('Priority (ASC)')),
array('priority', 'desc', t('Priority (DESC)')),
array('due-date', 'asc', t('Due date (ASC)')),
array('due-date', 'desc', t('Due date (DESC)')),
array('assignee', 'asc', t('Assignee (A-Z)')),
array('assignee', 'desc', t('Assignee (Z-A)')),
array('assignee-priority', 'asc', t('Assignee + priority (ASC)')),
array('assignee-priority', 'desc', t('Assignee + priority (DESC)')),
);
?>
<span class="dropdown organontweaks-sort">
<a href="#" class="dropdown-menu" title="<?= t('Sort this column') ?>">
<?php if ($sticky && $applied): ?>
<?php /* Persistent ON: a single red arrow for the direction (8593 up / 8595 down),
written as HTML entities so the source stays ASCII. OFF = the native
double-arrow (fa-sort) below -- the natural, non-sticky state. */ ?>
<span class="organontweaks-sort-arrow" style="color: #c0392b;"><?= $desc ? '&#8595;' : '&#8593;' ?></span>
<?php else: ?>
<i class="fa fa-sort fa-fw" aria-hidden="true"></i>
<?php endif ?>
</a>
<ul>
<li>
<?= $this->url->link(
'<i class="fa fa-thumb-tack fa-fw"></i> '.($sticky ? t('Persistent sort: ON') : t('Persistent sort: OFF')),
'OrganonSortController',
'toggle',
array('plugin' => 'OrganonTweaks', 'project_id' => $pid, 'column_id' => $cid),
true
) ?>
</li>
<li class="no-hover"><strong><?= t('Sort by') ?></strong></li>
<?php foreach ($criteria as $c): ?>
<?php $active = ($curBy === $c[0] && $curDir === $c[1]); ?>
<li>
<?= $this->url->link(
'<i class="fa '.($active ? 'fa-check' : '').' fa-fw"></i> '.$this->text->e($c[2]),
'OrganonSortController',
'sort',
array('plugin' => 'OrganonTweaks', 'project_id' => $pid, 'column_id' => $cid, 'swimlane_id' => $sid, 'sort' => $c[0], 'direction' => $c[1]),
true
) ?>
</li>
<?php endforeach ?>
</ul>
</span>

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

@@ -22,13 +22,6 @@
</p> </p>
</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) ?>
<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>
</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">
@@ -49,6 +42,26 @@
<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> <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>
<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>
<legend><?= t('Board columns') ?></legend>
<?= $this->form->checkbox('organon_tweaks_persistent_sort', t('Per-column persistent sort'), 1, isset($values['organon_tweaks_persistent_sort']) && $values['organon_tweaks_persistent_sort'] == 1) ?>
<p class="form-help"><?= t('Replaces the native column sort with a control that adds a number-aware Title sort and a "Persistent sort: ON/OFF" toggle. When ON, the column is re-sorted whenever a card is dropped in or created, so it stays sorted (a red up/down arrow marks it). OFF is the native one-shot sort.') ?></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

@@ -22,3 +22,4 @@
<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>
<hr>

View File

@@ -1 +1 @@
OrganonTweaks v1.7.2 OrganonTweaks v1.9.1