Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 269964d4cc | |||
| ac8e3b9544 | |||
| 717095848b | |||
| a905079abc | |||
| e032d5d2b4 | |||
| b63d366296 | |||
| 89888235d1 |
39
Asset/js/save-close.js
Normal file
39
Asset/js/save-close.js
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
/*
|
||||||
|
* OrganonTweaks -- the "Save & Close" button on the task edit form. It is a plain type=button (so it
|
||||||
|
* never becomes the Enter default -- Enter still triggers the core Save), and on click it repoints
|
||||||
|
* the form to the plugin's TaskSaveCloseController and lets Kanboard's own modal submit it. Kanboard
|
||||||
|
* keeps ownership of the POST, CSRF, error re-render, and the X-Ajax-Redirect navigation; we only
|
||||||
|
* change the destination, so the controller redirects to the board instead of the card view.
|
||||||
|
*/
|
||||||
|
(function () {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
document.addEventListener("click", function (e) {
|
||||||
|
if (!e.target || !e.target.closest) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var btn = e.target.closest("[data-organon-save-close]");
|
||||||
|
if (!btn) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
var form = btn.closest("form");
|
||||||
|
var url = btn.getAttribute("data-url");
|
||||||
|
if (!form || !url) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
form.setAttribute("action", url);
|
||||||
|
|
||||||
|
if (window.KB && KB.modal && KB.modal.isOpen && KB.modal.isOpen() && KB.modal.submitForm) {
|
||||||
|
KB.modal.submitForm();
|
||||||
|
} else if (form.requestSubmit) {
|
||||||
|
form.requestSubmit();
|
||||||
|
} else {
|
||||||
|
form.submit();
|
||||||
|
}
|
||||||
|
}, false);
|
||||||
|
})();
|
||||||
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_save_close_button' => (int) $this->configModel->get('organon_tweaks_save_close_button', 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;
|
||||||
|
$saveCloseButton = isset($values['organon_tweaks_save_close_button']) ? 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_save_close_button' => $saveCloseButton,
|
||||||
))) {
|
))) {
|
||||||
$this->flash->success(t('Settings saved successfully.'));
|
$this->flash->success(t('Settings saved successfully.'));
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
33
Controller/TaskSaveCloseController.php
Normal file
33
Controller/TaskSaveCloseController.php
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Kanboard\Plugin\OrganonTweaks\Controller;
|
||||||
|
|
||||||
|
use Kanboard\Controller\TaskModificationController;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* "Save & Close": the exact core task update, but on success it redirects to the BOARD instead of
|
||||||
|
* reopening the card view. The plugin's "Save & Close" button repoints the edit form here (via JS),
|
||||||
|
* so everything else -- validation, permission checks, CSRF, and the modal error re-render -- is
|
||||||
|
* inherited unchanged from core TaskModificationController. This mirrors core update() line for line
|
||||||
|
* except the one redirect target (kept in sync on Kanboard upgrades).
|
||||||
|
*/
|
||||||
|
class TaskSaveCloseController extends TaskModificationController
|
||||||
|
{
|
||||||
|
public function update()
|
||||||
|
{
|
||||||
|
$task = $this->getTask();
|
||||||
|
$values = $this->request->getValues();
|
||||||
|
$values['id'] = $task['id'];
|
||||||
|
$values['project_id'] = $task['project_id'];
|
||||||
|
|
||||||
|
list($valid, $errors) = $this->taskValidator->validateModification($values);
|
||||||
|
|
||||||
|
if ($valid && $this->updateTask($task, $values, $errors)) {
|
||||||
|
$this->flash->success(t('Task updated successfully.'));
|
||||||
|
$this->response->redirect($this->helper->url->to('BoardViewController', 'show', array('project_id' => $task['project_id'])), true);
|
||||||
|
} else {
|
||||||
|
$this->flash->failure(t('Unable to update your task.'));
|
||||||
|
$this->edit($values, $errors);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -33,19 +33,21 @@ class OrganonProjectHelper extends Base
|
|||||||
// plugin (fixed, non-translated strings so dedup is stable across locales); a user's own filter
|
// plugin (fixed, non-translated strings so dedup is stable across locales); a user's own filter
|
||||||
// with a reserved name is absorbed into the canonical form. "Show all tasks" is a REPLACE filter
|
// with a reserved name is absorbed into the canonical form. "Show all tasks" is a REPLACE filter
|
||||||
// (append 0) so it clears; the rest are APPEND (1) so clicks stack (distinct attributes AND).
|
// (append 0) so it clears; the rest are APPEND (1) so clicks stack (distinct attributes AND).
|
||||||
|
// Names are prefixed by group so the dropdown -- which core sorts alphabetically by name
|
||||||
|
// (CustomFilterModel::getAll -> asc(name)) -- clusters them: Board < Month < Recurring.
|
||||||
const GROUP_SHOW_ALL = array(
|
const GROUP_SHOW_ALL = array(
|
||||||
array('Show all tasks', '', 0),
|
array('Board: show all tasks', '', 0),
|
||||||
);
|
);
|
||||||
const GROUP_MONTH = array(
|
const GROUP_MONTH = array(
|
||||||
array('Due this month', 'duemonth:this', 1),
|
array('Month: due this month', 'duemonth:this', 1),
|
||||||
array('Around this month', 'aroundmonth:this', 1),
|
array('Month: around this month', 'aroundmonth:this', 1),
|
||||||
);
|
);
|
||||||
const GROUP_RECURRING = array(
|
const GROUP_RECURRING = array(
|
||||||
array('All recurring', 'recurring:anyall', 1),
|
array('Recurring: all', 'recurring:anyall', 1),
|
||||||
array('Event recurring', 'recurring:evtall', 1),
|
array('Recurring: event', 'recurring:evtall', 1),
|
||||||
array('Calendar recurring', 'recurring:calall', 1),
|
array('Recurring: calendar', 'recurring:calall', 1),
|
||||||
array('Recurring templates', 'recurring:anyori', 1),
|
array('Recurring: templates', 'recurring:anyori', 1),
|
||||||
array('Recurring duplicates', 'recurring:anydup', 1),
|
array('Recurring: duplicates', 'recurring:anydup', 1),
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
32
Plugin.php
32
Plugin.php
@@ -58,6 +58,36 @@ 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: a "Save & Close" button on the task edit form that saves and returns to the board
|
||||||
|
// (core only reopens the card view). The button posts to a small plugin controller that
|
||||||
|
// reuses core's update and changes only the redirect; the JS repoints the form action, so
|
||||||
|
// Kanboard's modal still owns the submit, CSRF, and error re-render.
|
||||||
|
if ((int) $this->configModel->get('organon_tweaks_save_close_button', 1) === 1) {
|
||||||
|
$this->template->hook->attach('template:task:form:bottom-before-buttons', 'organonTweaks:task_form/save_close_button');
|
||||||
|
$this->hook->on('template:layout:js', array(
|
||||||
|
'template' => 'plugins/OrganonTweaks/Asset/js/save-close.js',
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
// 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
|
||||||
@@ -109,7 +139,7 @@ class Plugin extends Base
|
|||||||
|
|
||||||
public function getPluginVersion()
|
public function getPluginVersion()
|
||||||
{
|
{
|
||||||
return '1.7.0';
|
return '1.8.0';
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getPluginHomepage()
|
public function getPluginHomepage()
|
||||||
|
|||||||
15
README.md
15
README.md
@@ -93,3 +93,18 @@ name). No build step and no database migration.
|
|||||||
## License
|
## License
|
||||||
|
|
||||||
AGPL-3.0. See LICENSE.
|
AGPL-3.0. See LICENSE.
|
||||||
|
|
||||||
|
## More Kanboard plugins by Ruben (drbeco)
|
||||||
|
|
||||||
|
All free and AGPL-3.0, at [code.beco.cc](https://code.beco.cc/beco):
|
||||||
|
|
||||||
|
- **[RecoReco](https://code.beco.cc/beco/RecoReco)** -- calendar-scheduled recurring cards (yearly,
|
||||||
|
monthly, weekly, daily), driven by the card due date.
|
||||||
|
- **[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.
|
||||||
|
- **[BulkMoveTasks](https://code.beco.cc/beco/BulkMoveTasks)** -- move every task from one board
|
||||||
|
column to another in a single action.
|
||||||
|
- **[ShrinkVertically](https://code.beco.cc/beco/ShrinkVertically)** -- shrink vertically-collapsed
|
||||||
|
board columns so the horizontal scrollbar stays within reach.
|
||||||
|
- **[TweakDrag](https://code.beco.cc/beco/TweakDrag)** -- board drag and touch niceties:
|
||||||
|
drag-to-scroll, a wider column gap, and smoother card dragging.
|
||||||
|
|||||||
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 ?>
|
||||||
@@ -49,6 +49,19 @@
|
|||||||
<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_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>
|
||||||
|
|
||||||
|
<?= $this->form->checkbox('organon_tweaks_save_close_button', t('Add a "Save & Close" button to the task edit form'), 1, isset($values['organon_tweaks_save_close_button']) && $values['organon_tweaks_save_close_button'] == 1) ?>
|
||||||
|
<p class="form-help"><?= t('Saves the task and returns to the board, instead of reopening the card view.') ?></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>
|
||||||
|
|||||||
8
Template/task_form/save_close_button.php
Normal file
8
Template/task_form/save_close_button.php
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<?php $task_id = isset($values['id']) ? (int) $values['id'] : 0 ?>
|
||||||
|
<?php if ($task_id > 0): ?>
|
||||||
|
<button type="button" class="btn"
|
||||||
|
data-organon-save-close="1"
|
||||||
|
data-url="<?= $this->url->href('TaskSaveCloseController', 'update', array('plugin' => 'OrganonTweaks', 'task_id' => $task_id)) ?>">
|
||||||
|
<?= t('Save & Close') ?>
|
||||||
|
</button>
|
||||||
|
<?php endif ?>
|
||||||
Reference in New Issue
Block a user