Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f9a7637e44 | |||
| d079198103 | |||
| 7ba4348de1 | |||
| 019b08084b | |||
| 2f78aa2087 | |||
| 8057272b9c | |||
| dc22739647 | |||
| 2d3c716852 | |||
| 269964d4cc | |||
| ac8e3b9544 | |||
| 717095848b | |||
| a905079abc | |||
| e032d5d2b4 |
@@ -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() {
|
||||||
|
|||||||
42
Asset/js/shortcut-labels.js
Normal file
42
Asset/js/shortcut-labels.js
Normal 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);
|
||||||
|
}
|
||||||
|
})();
|
||||||
67
Asset/js/title-click-edit.js
Normal file
67
Asset/js/title-click-edit.js
Normal 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);
|
||||||
|
}
|
||||||
|
})();
|
||||||
@@ -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 {
|
||||||
|
|||||||
61
Controller/OrganonSortController.php
Normal file
61
Controller/OrganonSortController.php
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
140
Helper/OrganonSortHelper.php
Normal file
140
Helper/OrganonSortHelper.php
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
<?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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Number-aware title sort (strnatcasecmp, so "Task 2" precedes "Task 10"), 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.
|
||||||
|
*/
|
||||||
|
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();
|
||||||
|
|
||||||
|
usort($rows, function ($a, $b) use ($direction) {
|
||||||
|
$cmp = strnatcasecmp((string) $a['title'], (string) $b['title']);
|
||||||
|
|
||||||
|
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++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
57
Plugin.php
57
Plugin.php
@@ -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.0';
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getPluginHomepage()
|
public function getPluginHomepage()
|
||||||
|
|||||||
82
README.md
82
README.md
@@ -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
|
||||||
|
|||||||
69
Template/board/sort_control.php
Normal file
69
Template/board/sort_control.php
Normal 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 ? '↓' : '↑' ?></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>
|
||||||
3
Template/board/title_edit.php
Normal file
3
Template/board/title_edit.php
Normal 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 ?>
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user