141 lines
5.5 KiB
PHP
141 lines
5.5 KiB
PHP
|
|
<?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++;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|