223 lines
12 KiB
PHP
223 lines
12 KiB
PHP
<?php
|
|
|
|
namespace Kanboard\Plugin\OrganonTweaks;
|
|
|
|
use Kanboard\Core\Plugin\Base;
|
|
use Kanboard\Model\SubtaskModel;
|
|
use Kanboard\Model\TaskModel;
|
|
|
|
class Plugin extends Base
|
|
{
|
|
public function initialize()
|
|
{
|
|
// "Remove this Column" entry in the board column header dropdown. The only
|
|
// column hook renders outside the native menu <ul>, so the item is rendered
|
|
// hidden and relocate.js moves it into the menu (like BulkMoveTasks).
|
|
$this->template->hook->attach('template:board:column:dropdown', 'organonTweaks:board/column_dropdown');
|
|
|
|
$this->hook->on('template:layout:js', array(
|
|
'template' => 'plugins/OrganonTweaks/Asset/js/relocate.js',
|
|
));
|
|
|
|
// Fix a core Kanboard touch bug: the subtask / board-column / swimlane reorder sortables
|
|
// use handle:"td:first i", which makes the gear/caret menu icons and the subtask status
|
|
// checkbox (all <i> in the first cell) drag handles too, so on touch devices Touch Punch
|
|
// swallows their taps -- the gear "drags" instead of opening. Re-scope the handle to the
|
|
// real drag icon (.draggable-row-handle) in JS. Always on; harmless on desktop.
|
|
$this->hook->on('template:layout:js', array(
|
|
'template' => 'plugins/OrganonTweaks/Asset/js/sortable-handle-fix.js',
|
|
));
|
|
|
|
// Settings page for the plugin's tweaks.
|
|
$this->template->hook->attach('template:config:sidebar', 'organonTweaks:config/sidebar');
|
|
|
|
// Per-board tweak: emphasize the due date on the board. Configured with 3-state radios on the
|
|
// board's Settings -> Integrations (Use default / Always on / Always off); the global default
|
|
// (for "Use default" boards) lives in the plugin settings above. The emphasis <style> is
|
|
// emitted into the head only on enabled board pages (gated by OrganonProjectHelper).
|
|
$this->template->hook->attach('template:project:integrations', 'organonTweaks:project/integration');
|
|
$this->template->hook->attach('template:layout:head', 'organonTweaks:layout/emphasize_duedate');
|
|
|
|
// Tweak: always show a comment icon on board cards. When a card has no comments
|
|
// yet, add a bubble "+" that opens the same comment modal (add the first comment
|
|
// straight from the board). Cards with comments already show the native count.
|
|
if ((int) $this->configModel->get('organon_tweaks_always_comment_icon', 1) === 1) {
|
|
$this->template->hook->attach('template:board:task:icons', 'organonTweaks:board/task_comment_icon');
|
|
}
|
|
|
|
// Tweak: keep the board horizontal scroll position across refreshes (Kanboard
|
|
// rebuilds #board-container on card drop / polling, resetting scrollLeft to 0).
|
|
if ((int) $this->configModel->get('organon_tweaks_keep_scroll', 1) === 1) {
|
|
$this->hook->on('template:layout:js', array(
|
|
'template' => 'plugins/OrganonTweaks/Asset/js/keep-scroll.js',
|
|
));
|
|
}
|
|
|
|
// Tweak: only open a card on a quick click (a press held > 100ms is a drag, so the
|
|
// trailing click is swallowed and the card does not open).
|
|
if ((int) $this->configModel->get('organon_tweaks_quick_click_only', 1) === 1) {
|
|
$this->hook->on('template:layout:js', array(
|
|
'template' => 'plugins/OrganonTweaks/Asset/js/click-guard.js',
|
|
));
|
|
}
|
|
|
|
// Tweak: enhance the native "Edit recurrence" dialog (Yes/No radios + a [+7] button
|
|
// on the factor). No template hook there, so it is done in JS.
|
|
if ((int) $this->configModel->get('organon_tweaks_enhance_recurrence', 1) === 1) {
|
|
$this->hook->on('template:layout:js', array(
|
|
'template' => 'plugins/OrganonTweaks/Asset/js/enhance-recurrence.js',
|
|
));
|
|
}
|
|
|
|
// 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']);
|
|
}
|
|
});
|
|
}
|
|
|
|
// Tweak: a Done/Todo 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');
|
|
// Bulk "Mark all as Done/Todo" entry in the column header dropdown (moved into the native
|
|
// menu by relocate.js). Opens a two-button modal that confirms + picks the direction.
|
|
$this->template->hook->attach('template:board:column:dropdown', 'organonTweaks:board/mark_all_item');
|
|
|
|
// Sub-tweak (opt-in): auto-mark a card Done when its LAST subtask is completed. On every
|
|
// subtask change, recompute all-done and mark Done only on the up-transition
|
|
// (state-comparison in the helper), so a manual unmark is honored -- nothing re-fires.
|
|
// Dispatcher direct (the plugin on() wrapper drops the event). DELETE fires before the row
|
|
// is removed, so its id is excluded from the recount.
|
|
if ((int) $this->configModel->get('organon_tweaks_done_autosubtasks', 0) === 1) {
|
|
$container = $this->container;
|
|
$sync = function ($task_id, $exclude_id = 0) use ($container) {
|
|
$helper = new \Kanboard\Plugin\OrganonTweaks\Helper\OrganonDoneHelper($container);
|
|
$helper->syncSubtasksDone((int) $task_id, (int) $exclude_id);
|
|
};
|
|
$this->dispatcher->addListener(SubtaskModel::EVENT_UPDATE, function ($event) use ($sync) {
|
|
$sync($event['subtask']['task_id']);
|
|
});
|
|
$this->dispatcher->addListener(SubtaskModel::EVENT_CREATE, function ($event) use ($sync) {
|
|
$sync($event['subtask']['task_id']);
|
|
});
|
|
$this->dispatcher->addListener(SubtaskModel::EVENT_DELETE, function ($event) use ($sync) {
|
|
$sync($event['subtask']['task_id'], $event['subtask']['id']);
|
|
});
|
|
}
|
|
}
|
|
|
|
// Auto-managed shared custom filters (v1.5 "Show all tasks", v1.6 month filters, v1.7
|
|
// recurring filters). Ensured lazily whenever a project header renders (covers old + new
|
|
// boards); each group is gated by its own config toggle inside
|
|
// OrganonProjectHelper::ensureBoardFilters(). The toggles also bulk add/remove the filters.
|
|
$showAllFilters = (int) $this->configModel->get('organon_tweaks_show_all_filter', 1) === 1;
|
|
$monthFilters = (int) $this->configModel->get('organon_tweaks_month_filters', 1) === 1;
|
|
$recurringFilters = (int) $this->configModel->get('organon_tweaks_recurring_filters', 1) === 1;
|
|
if ($showAllFilters || $monthFilters || $recurringFilters) {
|
|
$this->template->hook->attach('template:project:header:before', 'organonTweaks:project_header/board_filters');
|
|
}
|
|
|
|
// Custom board-search keywords, registered into the task lexer by extending its factory
|
|
// service (Pimple preserves factory status on extend, so the lexer stays per-request fresh).
|
|
// `duemonth:VALUE` / `aroundmonth:VALUE` = a calendar month (padded by 10 days for around);
|
|
// `recurring:XY` = classify recurrence (X system evt/cal/any, Y role ori/dup/all). Always
|
|
// available to type, independent of the bookmark toggles above. Guarded so a future Kanboard
|
|
// service rename no-ops (keyword absent) instead of breaking.
|
|
if (isset($this->container['taskLexer'])) {
|
|
$this->container->extend('taskLexer', function ($builder, $c) {
|
|
return $builder
|
|
->withFilter((new \Kanboard\Plugin\OrganonTweaks\Filter\TaskMonthRangeFilter())->setAttribute('duemonth')->setPadDays(0))
|
|
->withFilter((new \Kanboard\Plugin\OrganonTweaks\Filter\TaskMonthRangeFilter())->setAttribute('aroundmonth')->setPadDays(10))
|
|
->withFilter((new \Kanboard\Plugin\OrganonTweaks\Filter\TaskRecurringFilter())->setDatabase($c['db']));
|
|
});
|
|
}
|
|
}
|
|
|
|
public function getHelpers()
|
|
{
|
|
return array(
|
|
'Plugin\OrganonTweaks\Helper' => array('OrganonColumnHelper', 'OrganonProjectHelper', 'OrganonSortHelper', 'OrganonDoneHelper'),
|
|
);
|
|
}
|
|
|
|
public function getPluginName()
|
|
{
|
|
return 'OrganonTweaks';
|
|
}
|
|
|
|
public function getPluginDescription()
|
|
{
|
|
return t('Umbrella plugin for small Kanboard tweaks: remove an empty board column, always show the card comment icon, and more.');
|
|
}
|
|
|
|
public function getPluginAuthor()
|
|
{
|
|
return 'Ruben (drbeco)';
|
|
}
|
|
|
|
public function getPluginVersion()
|
|
{
|
|
return '2.3.0';
|
|
}
|
|
|
|
public function getPluginHomepage()
|
|
{
|
|
return 'https://code.beco.cc/beco/OrganonTweaks';
|
|
}
|
|
|
|
public function getCompatibleVersion()
|
|
{
|
|
return '>=1.2.0';
|
|
}
|
|
}
|