Compare commits
5 Commits
7ba4348de1
...
v2.0.1
| Author | SHA1 | Date | |
|---|---|---|---|
| c88e52ab01 | |||
| f2f690ef98 | |||
| 8be9442cb7 | |||
| f9a7637e44 | |||
| d079198103 |
@@ -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() {
|
||||
relocateColumnMenuItems();
|
||||
repositionCommentIcons();
|
||||
hideNativeSort();
|
||||
}
|
||||
|
||||
function init() {
|
||||
|
||||
@@ -24,6 +24,9 @@ class ConfigController extends BaseController
|
||||
'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),
|
||||
'organon_tweaks_done_badge' => (int) $this->configModel->get('organon_tweaks_done_badge', 0),
|
||||
'organon_tweaks_done_closes_task' => (int) $this->configModel->get('organon_tweaks_done_closes_task', 0),
|
||||
),
|
||||
'errors' => array(),
|
||||
)));
|
||||
@@ -43,6 +46,9 @@ class ConfigController extends BaseController
|
||||
$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;
|
||||
$doneBadge = isset($values['organon_tweaks_done_badge']) ? 1 : 0;
|
||||
$doneClosesTask = isset($values['organon_tweaks_done_closes_task']) ? 1 : 0;
|
||||
|
||||
if ($this->configModel->save(array(
|
||||
'organon_tweaks_always_comment_icon' => $alwaysCommentIcon,
|
||||
@@ -55,6 +61,9 @@ class ConfigController extends BaseController
|
||||
'organon_tweaks_recurring_filters' => $recurringFilters,
|
||||
'organon_tweaks_shortcut_labels' => $shortcutLabels,
|
||||
'organon_tweaks_title_click_edit' => $titleClickEdit,
|
||||
'organon_tweaks_persistent_sort' => $persistentSort,
|
||||
'organon_tweaks_done_badge' => $doneBadge,
|
||||
'organon_tweaks_done_closes_task' => $doneClosesTask,
|
||||
))) {
|
||||
$this->flash->success(t('Settings saved successfully.'));
|
||||
} else {
|
||||
|
||||
47
Controller/DoneController.php
Normal file
47
Controller/DoneController.php
Normal file
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace Kanboard\Plugin\OrganonTweaks\Controller;
|
||||
|
||||
use Kanboard\Controller\BaseController;
|
||||
use Kanboard\Plugin\OrganonTweaks\Helper\OrganonDoneHelper;
|
||||
|
||||
/**
|
||||
* Toggle a task's Done/Due badge (OrganonTweaks).
|
||||
*
|
||||
* One-click CSRF link from the board card face and the task view. Single source of truth per mode:
|
||||
* - close-mode on: Done == closed, so this just closes/opens the task (no metadata); native
|
||||
* Close/Open stay in sync automatically because the badge reads is_active.
|
||||
* - close-mode off: this flips the 'organon_done' metadata marker and never touches open/closed.
|
||||
* Redirects back to wherever it was clicked (board or task view).
|
||||
*/
|
||||
class DoneController extends BaseController
|
||||
{
|
||||
public function toggle()
|
||||
{
|
||||
$task = $this->getTask();
|
||||
$this->checkCSRFParam();
|
||||
|
||||
$helper = new OrganonDoneHelper($this->container);
|
||||
$done = $helper->isDone($task);
|
||||
|
||||
if ($helper->closesTask()) {
|
||||
if ($done) {
|
||||
$this->taskStatusModel->open($task['id']);
|
||||
} else {
|
||||
$this->taskStatusModel->close($task['id']);
|
||||
}
|
||||
} else {
|
||||
if ($done) {
|
||||
$this->taskMetadataModel->remove($task['id'], OrganonDoneHelper::DONE_KEY);
|
||||
} else {
|
||||
$this->taskMetadataModel->save($task['id'], array(OrganonDoneHelper::DONE_KEY => 'on'));
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->request->getStringParam('from') === 'board') {
|
||||
$this->response->redirect($this->helper->url->to('BoardViewController', 'show', array('project_id' => $task['project_id'])), true);
|
||||
} else {
|
||||
$this->response->redirect($this->helper->url->to('TaskViewController', 'show', array('task_id' => $task['id'])), true);
|
||||
}
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
50
Helper/OrganonDoneHelper.php
Normal file
50
Helper/OrganonDoneHelper.php
Normal file
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace Kanboard\Plugin\OrganonTweaks\Helper;
|
||||
|
||||
use Kanboard\Core\Base;
|
||||
|
||||
/**
|
||||
* Done/Due badge helper.
|
||||
*
|
||||
* "Done" has a single source of truth PER MODE (no drift with native Close/Open):
|
||||
* - close-mode on ("check done also closes tasks"): Done == the task is CLOSED (is_active == 0).
|
||||
* The badge reads the task's own is_active, so however a task is closed/reopened (badge, native
|
||||
* sidebar, bulk, API) the badge always matches. No metadata is stored in this mode.
|
||||
* - close-mode off (marker only): Done == the task metadata DONE_KEY == 'on', independent of the
|
||||
* open/closed status. Stored as the non-falsy string 'on' (never '0'/'1'): MetadataModel::get()
|
||||
* uses `?:` and PHP treats "0" as falsy, so a stored "0" would read back as the default.
|
||||
*
|
||||
* Templates cannot read models directly, so the board/task-view badge templates call isDone() here,
|
||||
* passing the task array (which carries both id and is_active) so the close-mode read needs no query.
|
||||
*/
|
||||
class OrganonDoneHelper extends Base
|
||||
{
|
||||
const DONE_KEY = 'organon_done';
|
||||
const CLOSES_KEY = 'organon_tweaks_done_closes_task';
|
||||
|
||||
/**
|
||||
* Is this task Done? (mode-aware: closed status in close-mode, else the metadata marker)
|
||||
*
|
||||
* @param array $task a task array carrying 'id' and 'is_active'
|
||||
* @return bool
|
||||
*/
|
||||
public function isDone(array $task)
|
||||
{
|
||||
if ($this->closesTask()) {
|
||||
return isset($task['is_active']) && (int) $task['is_active'] === 0;
|
||||
}
|
||||
|
||||
return $this->taskMetadataModel->get((int) $task['id'], self::DONE_KEY, '') === 'on';
|
||||
}
|
||||
|
||||
/**
|
||||
* Should toggling Done also close (and reopen) the task? Global setting, default off.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function closesTask()
|
||||
{
|
||||
return (int) $this->configModel->get(self::CLOSES_KEY, 0) === 1;
|
||||
}
|
||||
}
|
||||
176
Helper/OrganonSortHelper.php
Normal file
176
Helper/OrganonSortHelper.php
Normal 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;
|
||||
}
|
||||
}
|
||||
48
Plugin.php
48
Plugin.php
@@ -3,6 +3,7 @@
|
||||
namespace Kanboard\Plugin\OrganonTweaks;
|
||||
|
||||
use Kanboard\Core\Plugin\Base;
|
||||
use Kanboard\Model\TaskModel;
|
||||
|
||||
class Plugin extends Base
|
||||
{
|
||||
@@ -77,6 +78,49 @@ class Plugin extends Base
|
||||
$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']);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Tweak: a Done/Due status badge on the card face (top-right, below the header) and in the
|
||||
// task view (4th column, near the due date). Two-state toggle; single source of truth per
|
||||
// mode -- close-mode ON => Done == closed (reads is_active, so native Close/Open stay in
|
||||
// sync); close-mode OFF => a task-metadata marker (organon_done). Server-rendered, no JS.
|
||||
if ((int) $this->configModel->get('organon_tweaks_done_badge', 0) === 1) {
|
||||
$this->template->hook->attach('template:board:private:task:before-title', 'organonTweaks:board/done_badge');
|
||||
$this->template->hook->attach('template:task:details:fourth-column', 'organonTweaks:task/done_badge');
|
||||
$this->template->hook->attach('template:layout:head', 'organonTweaks:layout/done_style');
|
||||
}
|
||||
|
||||
// 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
|
||||
@@ -107,7 +151,7 @@ class Plugin extends Base
|
||||
public function getHelpers()
|
||||
{
|
||||
return array(
|
||||
'Plugin\OrganonTweaks\Helper' => array('OrganonColumnHelper', 'OrganonProjectHelper'),
|
||||
'Plugin\OrganonTweaks\Helper' => array('OrganonColumnHelper', 'OrganonProjectHelper', 'OrganonSortHelper', 'OrganonDoneHelper'),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -128,7 +172,7 @@ class Plugin extends Base
|
||||
|
||||
public function getPluginVersion()
|
||||
{
|
||||
return '1.8.6';
|
||||
return '2.0.1';
|
||||
}
|
||||
|
||||
public function getPluginHomepage()
|
||||
|
||||
50
README.md
50
README.md
@@ -96,6 +96,47 @@ edits when it opens (there is no template hook, so it is done in JS,
|
||||
|
||||
- **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".
|
||||
|
||||
### Done/Due badge
|
||||
|
||||
A two-state toggle badge for marking a card done, shown on the board card face (top-right, below the
|
||||
header) and on the task view (4th column, near the due date). It reads **`[ ] Due`** (dark red) while
|
||||
the task is pending; one click flips it to **`[x] Done`** (light green). The card itself is not
|
||||
recolored -- only the badge. The "Due" colors reuse FinanceBuddy's debit badge for consistency.
|
||||
|
||||
- Rendered server-side (no JavaScript) via `template:board:private:task:before-title`
|
||||
(`Template/board/done_badge.php`), `template:task:details:fourth-column`
|
||||
(`Template/task/done_badge.php`), and a small head `<style>` (`Template/layout/done_style.php`).
|
||||
Shown only to users who may edit the task; on the board it appears on expanded (non-collapsed) cards.
|
||||
- **Single source of truth, no drift with native Close/Open.** With *"Marking Done also closes the
|
||||
task"* **on**, "Done" *means* the task is closed: the badge reads the task's own open/closed status,
|
||||
so closing or reopening a task by any route (the badge, the native sidebar, bulk actions, the API)
|
||||
always keeps the badge correct. A closed "Done" card is hidden from the board by Kanboard's default
|
||||
open-only filter, but reappears if you clear the filter (or use the "Board: show all tasks" filter).
|
||||
- With that option **off**, the badge is an independent marker stored in task metadata
|
||||
(`organon_done`) that never touches the open/closed status.
|
||||
- **Off by default.** Toggle it under "Settings -> Organon Tweaks".
|
||||
|
||||
## Settings
|
||||
|
||||
Global (per Kanboard instance) and admin-only, under "Settings -> Organon Tweaks", grouped as on
|
||||
@@ -118,6 +159,15 @@ the page:
|
||||
- **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.
|
||||
|
||||
**Done badge**
|
||||
|
||||
- **Show a Done/Due badge on cards** -- default off.
|
||||
- **Marking Done also closes the task** -- default off.
|
||||
|
||||
Standalone:
|
||||
|
||||
- **Keep the board scroll position across refreshes** -- default on.
|
||||
|
||||
23
Template/board/done_badge.php
Normal file
23
Template/board/done_badge.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
/**
|
||||
* Done/Due badge on the board card face. Rendered by template:board:private:task:before-title; the
|
||||
* CSS in layout/done_style.php pins it to the top-right (below the header) and colors the two states.
|
||||
* Only shown to users who may edit the task (a read-only viewer sees no badge). Expanded cards only
|
||||
* (collapsed cards do not fire this hook).
|
||||
*/
|
||||
if (! $this->user->hasProjectAccess('TaskModificationController', 'edit', $task['project_id'])) {
|
||||
return;
|
||||
}
|
||||
$done = $this->OrganonDoneHelper->isDone($task);
|
||||
?>
|
||||
<span class="organontweaks-done<?= $done ? ' is-done' : '' ?>">
|
||||
<?= $this->url->link(
|
||||
$done ? '<i class="fa fa-check-square"></i> '.t('Done') : '<i class="fa fa-square"></i> '.t('Due'),
|
||||
'DoneController',
|
||||
'toggle',
|
||||
array('plugin' => 'OrganonTweaks', 'task_id' => $task['id'], 'project_id' => $task['project_id'], 'from' => 'board'),
|
||||
true,
|
||||
'',
|
||||
$done ? t('Mark as not done') : t('Mark as done')
|
||||
) ?>
|
||||
</span>
|
||||
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>
|
||||
@@ -55,6 +55,23 @@
|
||||
<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>
|
||||
|
||||
<fieldset>
|
||||
<legend><?= t('Done badge') ?></legend>
|
||||
|
||||
<?= $this->form->checkbox('organon_tweaks_done_badge', t('Show a Done/Due badge on cards'), 1, isset($values['organon_tweaks_done_badge']) && $values['organon_tweaks_done_badge'] == 1) ?>
|
||||
<p class="form-help"><?= t('A two-state toggle badge on the board card (top-right) and the task view (near the due date): "Due" (red) until you click it, then "Done" (green). Off by default.') ?></p>
|
||||
|
||||
<?= $this->form->checkbox('organon_tweaks_done_closes_task', t('Marking Done also closes the task'), 1, isset($values['organon_tweaks_done_closes_task']) && $values['organon_tweaks_done_closes_task'] == 1) ?>
|
||||
<p class="form-help"><?= t('When on, "Done" means the task is closed (so it leaves the board unless you clear the status filter), and clicking Due reopens it -- native Close/Open stay in sync. When off, the badge is an independent marker that never changes the open/closed status.') ?></p>
|
||||
</fieldset>
|
||||
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn btn-blue"><?= t('Save') ?></button>
|
||||
</div>
|
||||
|
||||
29
Template/layout/done_style.php
Normal file
29
Template/layout/done_style.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
/**
|
||||
* Done/Due badge styling (emitted into the head when the feature is enabled). NATURAL document flow --
|
||||
* the badge renders above the title (board card) or as a list item (task view); no absolute
|
||||
* positioning, so it adapts to any card / column / avatar / username size. Two states via .is-done:
|
||||
* Due = black filled box + black text on a light red (#ef9a9a); Done = checked box, black on light
|
||||
* green (#a5d6a7). Both states use black text (core's `.task-board a { color:#000 }` forces the icon
|
||||
* black anyway, so the backgrounds are kept light for contrast). Colors are tunable.
|
||||
*/
|
||||
?>
|
||||
<style>
|
||||
span.organontweaks-done a {
|
||||
display: inline-block;
|
||||
text-decoration: none;
|
||||
padding: 3px 10px;
|
||||
border-radius: 3px;
|
||||
background: #ef9a9a;
|
||||
color: #000;
|
||||
font-weight: bold;
|
||||
}
|
||||
span.organontweaks-done.is-done a {
|
||||
background: #a5d6a7;
|
||||
color: #000;
|
||||
}
|
||||
.organontweaks-done {
|
||||
display: inline-block;
|
||||
margin: 1px 0;
|
||||
}
|
||||
</style>
|
||||
24
Template/task/done_badge.php
Normal file
24
Template/task/done_badge.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
/**
|
||||
* Done/Due badge in the task view, 4th column (near the due date). Rendered by
|
||||
* template:task:details:fourth-column as a list item, matching the surrounding <li> fields. Same
|
||||
* two-state toggle as the board badge; only shown to users who may edit the task.
|
||||
*/
|
||||
if (! $this->user->hasProjectAccess('TaskModificationController', 'edit', $task['project_id'])) {
|
||||
return;
|
||||
}
|
||||
$done = $this->OrganonDoneHelper->isDone($task);
|
||||
?>
|
||||
<li>
|
||||
<span class="organontweaks-done<?= $done ? ' is-done' : '' ?>">
|
||||
<?= $this->url->link(
|
||||
$done ? '<i class="fa fa-check-square"></i> '.t('Done') : '<i class="fa fa-square"></i> '.t('Due'),
|
||||
'DoneController',
|
||||
'toggle',
|
||||
array('plugin' => 'OrganonTweaks', 'task_id' => $task['id'], 'project_id' => $task['project_id'], 'from' => 'task'),
|
||||
true,
|
||||
'',
|
||||
$done ? t('Mark as not done') : t('Mark as done')
|
||||
) ?>
|
||||
</span>
|
||||
</li>
|
||||
Reference in New Issue
Block a user