Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9c27b51eed | |||
| 1bea749fed | |||
| 230982dd4c | |||
| d47809c713 | |||
| 59955097da | |||
| 6cca6ce562 | |||
| 8b7b6478fd |
1
Asset/css/skeleton.css
vendored
1
Asset/css/skeleton.css
vendored
@@ -1 +0,0 @@
|
|||||||
/* Skeleton plugin styles -- add CSS here. Loaded via template:layout:css. */
|
|
||||||
55
Asset/js/keep-scroll.js
Normal file
55
Asset/js/keep-scroll.js
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
/*
|
||||||
|
* OrganonTweaks -- keep the board's horizontal scroll position across refreshes.
|
||||||
|
*
|
||||||
|
* When you drop a card (and on Kanboard's periodic AJAX polling) the board is rebuilt with
|
||||||
|
* `$("#board-container").replaceWith(data)` (BoardDragAndDrop.refresh). The brand-new
|
||||||
|
* element starts at scrollLeft 0, so the board snaps back to the first column -- a jarring
|
||||||
|
* jump. This remembers the last scroll position and restores it the moment a replacement
|
||||||
|
* container appears, so the view stays put.
|
||||||
|
*
|
||||||
|
* We observe the STABLE parent (the container itself is replaced) and read the scroll from
|
||||||
|
* whichever #board-container is current, so it keeps working after every rebuild.
|
||||||
|
*/
|
||||||
|
(function () {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
var container = document.getElementById("board-container");
|
||||||
|
if (!container) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var parent = container.parentNode;
|
||||||
|
var lastScrollLeft = container.scrollLeft;
|
||||||
|
var lastContainer = container;
|
||||||
|
|
||||||
|
// Remember the position whenever the board is scrolled (capture: scroll does not
|
||||||
|
// bubble, and this survives the container being replaced).
|
||||||
|
document.addEventListener("scroll", function (e) {
|
||||||
|
var c = document.getElementById("board-container");
|
||||||
|
if (c && e.target === c) {
|
||||||
|
lastScrollLeft = c.scrollLeft;
|
||||||
|
}
|
||||||
|
}, true);
|
||||||
|
|
||||||
|
// When the board is rebuilt, #board-container becomes a new element at scrollLeft 0;
|
||||||
|
// restore the remembered position before the browser paints it.
|
||||||
|
if (window.MutationObserver) {
|
||||||
|
new MutationObserver(function () {
|
||||||
|
var c = document.getElementById("board-container");
|
||||||
|
if (c && c !== lastContainer) {
|
||||||
|
lastContainer = c;
|
||||||
|
if (c.scrollLeft !== lastScrollLeft) {
|
||||||
|
c.scrollLeft = lastScrollLeft;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).observe(parent, { childList: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (document.readyState === "loading") {
|
||||||
|
document.addEventListener("DOMContentLoaded", init);
|
||||||
|
} else {
|
||||||
|
init();
|
||||||
|
}
|
||||||
|
})();
|
||||||
98
Asset/js/relocate.js
Normal file
98
Asset/js/relocate.js
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
/*
|
||||||
|
* OrganonTweaks -- move plugin-injected DOM bits into their native positions on the board.
|
||||||
|
*
|
||||||
|
* Both fixes are needed because Kanboard's relevant hooks render at the END of their
|
||||||
|
* container, not where the item belongs:
|
||||||
|
*
|
||||||
|
* 1. "Remove this Column": the template:board:column:dropdown hook renders AFTER the column
|
||||||
|
* menu <ul> (and the dropdown JS clones only that <ul>), so we move the hidden <li> into
|
||||||
|
* the column's real <ul>.
|
||||||
|
* 2. "Always show comment icon": the template:board:task:icons hook renders at the end of
|
||||||
|
* the card icons row, so the "+" bubble lands after priority instead of in the native
|
||||||
|
* comment slot. We move it just before the description icon (where the native comment
|
||||||
|
* icon sits), so the order matches whether or not the card has comments.
|
||||||
|
*
|
||||||
|
* Both re-run when the board is redrawn (AJAX polling / drag-and-drop refresh).
|
||||||
|
*/
|
||||||
|
(function () {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
function relocateColumnMenuItems() {
|
||||||
|
var items = document.querySelectorAll(".organontweaks-remove-column-item");
|
||||||
|
|
||||||
|
for (var i = 0; i < items.length; i++) {
|
||||||
|
var li = items[i];
|
||||||
|
|
||||||
|
// Already moved into a menu list: just make sure it is visible.
|
||||||
|
if (li.closest("ul")) {
|
||||||
|
li.style.display = "";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The hook renders the item as a child of the column's dropdown wrapper;
|
||||||
|
// its first <ul> is the native menu.
|
||||||
|
var menu = li.parentNode ? li.parentNode.querySelector("ul") : null;
|
||||||
|
|
||||||
|
if (menu) {
|
||||||
|
menu.appendChild(li);
|
||||||
|
li.style.display = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function repositionCommentIcons() {
|
||||||
|
var items = document.querySelectorAll(".organontweaks-comment-add");
|
||||||
|
|
||||||
|
for (var i = 0; i < items.length; i++) {
|
||||||
|
var item = items[i];
|
||||||
|
var row = item.parentNode;
|
||||||
|
|
||||||
|
if (!row) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Native comment icon sits just before the description icon; if there is no
|
||||||
|
// description, before the age / closed block. That is the slot we move into.
|
||||||
|
var anchor = null;
|
||||||
|
var descIcon = row.querySelector("i.fa-file-text-o");
|
||||||
|
|
||||||
|
if (descIcon) {
|
||||||
|
anchor = descIcon.closest("a");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!anchor) {
|
||||||
|
anchor = row.querySelector(".task-icon-age") || row.querySelector(".task-board-closed");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move only if needed (keeps the MutationObserver from looping).
|
||||||
|
if (anchor && anchor.parentNode === row && item.nextElementSibling !== anchor) {
|
||||||
|
row.insertBefore(item, anchor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function apply() {
|
||||||
|
relocateColumnMenuItems();
|
||||||
|
repositionCommentIcons();
|
||||||
|
}
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
if (!document.getElementById("board")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
apply();
|
||||||
|
|
||||||
|
var container = document.getElementById("board-container");
|
||||||
|
|
||||||
|
if (container && window.MutationObserver) {
|
||||||
|
new MutationObserver(apply).observe(container, { childList: true, subtree: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (document.readyState === "loading") {
|
||||||
|
document.addEventListener("DOMContentLoaded", init);
|
||||||
|
} else {
|
||||||
|
init();
|
||||||
|
}
|
||||||
|
})();
|
||||||
@@ -1 +0,0 @@
|
|||||||
// Skeleton plugin script -- add JS here. Loaded via template:layout:js.
|
|
||||||
71
Controller/ColumnController.php
Normal file
71
Controller/ColumnController.php
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Kanboard\Plugin\OrganonTweaks\Controller;
|
||||||
|
|
||||||
|
use Kanboard\Controller\BaseController;
|
||||||
|
use Kanboard\Core\Controller\AccessForbiddenException;
|
||||||
|
use Kanboard\Model\TaskModel;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove an empty board column straight from its header menu.
|
||||||
|
*/
|
||||||
|
class ColumnController extends BaseController
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Confirmation dialog.
|
||||||
|
*/
|
||||||
|
public function confirm()
|
||||||
|
{
|
||||||
|
$project = $this->getProject();
|
||||||
|
$column_id = $this->request->getIntegerParam('column_id');
|
||||||
|
$this->checkAccess($project['id'], $column_id);
|
||||||
|
|
||||||
|
$this->response->html($this->template->render('organonTweaks:board/remove_column', array(
|
||||||
|
'project' => $project,
|
||||||
|
'column' => $this->columnModel->getColumnTitleById($column_id),
|
||||||
|
'column_id' => $column_id,
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the column and return to the board.
|
||||||
|
*/
|
||||||
|
public function remove()
|
||||||
|
{
|
||||||
|
$project = $this->getProject();
|
||||||
|
$values = $this->request->getValues();
|
||||||
|
$column_id = isset($values['column_id']) ? (int) $values['column_id'] : 0;
|
||||||
|
|
||||||
|
$this->checkAccess($project['id'], $column_id);
|
||||||
|
|
||||||
|
if ($this->columnModel->remove($column_id)) {
|
||||||
|
$this->flash->success(t('Column removed successfully.'));
|
||||||
|
} else {
|
||||||
|
$this->flash->failure(t('Unable to remove this column.'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->response->redirect($this->helper->url->to('BoardViewController', 'show', array('project_id' => $project['id'])));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allow only users who can manage columns, and only for a column with no tasks at all
|
||||||
|
* (open or closed) -- ColumnModel::remove() is a bare delete, so a non-empty column
|
||||||
|
* would orphan its tasks.
|
||||||
|
*/
|
||||||
|
private function checkAccess($project_id, $column_id)
|
||||||
|
{
|
||||||
|
if (! $this->helper->user->hasProjectAccess('ColumnController', 'remove', $project_id)) {
|
||||||
|
throw new AccessForbiddenException();
|
||||||
|
}
|
||||||
|
|
||||||
|
$count = $this->taskFinderModel->countByColumnId(
|
||||||
|
$project_id,
|
||||||
|
$column_id,
|
||||||
|
array(TaskModel::STATUS_OPEN, TaskModel::STATUS_CLOSED)
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($count > 0) {
|
||||||
|
throw new AccessForbiddenException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
42
Controller/ConfigController.php
Normal file
42
Controller/ConfigController.php
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Kanboard\Plugin\OrganonTweaks\Controller;
|
||||||
|
|
||||||
|
use Kanboard\Controller\BaseController;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Settings page for the OrganonTweaks plugin: one toggle per tweak.
|
||||||
|
*/
|
||||||
|
class ConfigController extends BaseController
|
||||||
|
{
|
||||||
|
public function show()
|
||||||
|
{
|
||||||
|
$this->response->html($this->helper->layout->config('organonTweaks:config/show', array(
|
||||||
|
'title' => t('Settings').' > '.t('Organon Tweaks'),
|
||||||
|
'values' => array(
|
||||||
|
'organon_tweaks_always_comment_icon' => (int) $this->configModel->get('organon_tweaks_always_comment_icon', 1),
|
||||||
|
'organon_tweaks_keep_scroll' => (int) $this->configModel->get('organon_tweaks_keep_scroll', 1),
|
||||||
|
),
|
||||||
|
'errors' => array(),
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function save()
|
||||||
|
{
|
||||||
|
$values = $this->request->getValues();
|
||||||
|
|
||||||
|
$alwaysCommentIcon = isset($values['organon_tweaks_always_comment_icon']) ? 1 : 0;
|
||||||
|
$keepScroll = isset($values['organon_tweaks_keep_scroll']) ? 1 : 0;
|
||||||
|
|
||||||
|
if ($this->configModel->save(array(
|
||||||
|
'organon_tweaks_always_comment_icon' => $alwaysCommentIcon,
|
||||||
|
'organon_tweaks_keep_scroll' => $keepScroll,
|
||||||
|
))) {
|
||||||
|
$this->flash->success(t('Settings saved successfully.'));
|
||||||
|
} else {
|
||||||
|
$this->flash->failure(t('Unable to save your settings.'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->response->redirect($this->helper->url->to('ConfigController', 'show', array('plugin' => 'OrganonTweaks')));
|
||||||
|
}
|
||||||
|
}
|
||||||
31
Helper/OrganonColumnHelper.php
Normal file
31
Helper/OrganonColumnHelper.php
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Kanboard\Plugin\OrganonTweaks\Helper;
|
||||||
|
|
||||||
|
use Kanboard\Core\Base;
|
||||||
|
use Kanboard\Model\TaskModel;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Template helper for column tweaks. Exposes an emptiness check that counts BOTH open and
|
||||||
|
* closed tasks (the board's own $column['nb_tasks'] is open-only), so a column that only
|
||||||
|
* holds closed tasks is still treated as non-empty.
|
||||||
|
*/
|
||||||
|
class OrganonColumnHelper extends Base
|
||||||
|
{
|
||||||
|
private static $emptyCache = array();
|
||||||
|
|
||||||
|
public function hasNoTasks($project_id, $column_id)
|
||||||
|
{
|
||||||
|
if (! isset(self::$emptyCache[$column_id])) {
|
||||||
|
$count = $this->taskFinderModel->countByColumnId(
|
||||||
|
$project_id,
|
||||||
|
$column_id,
|
||||||
|
array(TaskModel::STATUS_OPEN, TaskModel::STATUS_CLOSED)
|
||||||
|
);
|
||||||
|
|
||||||
|
self::$emptyCache[$column_id] = ($count === 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::$emptyCache[$column_id];
|
||||||
|
}
|
||||||
|
}
|
||||||
49
Plugin.php
49
Plugin.php
@@ -1,6 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Kanboard\Plugin\Skeleton;
|
namespace Kanboard\Plugin\OrganonTweaks;
|
||||||
|
|
||||||
use Kanboard\Core\Plugin\Base;
|
use Kanboard\Core\Plugin\Base;
|
||||||
|
|
||||||
@@ -8,28 +8,49 @@ class Plugin extends Base
|
|||||||
{
|
{
|
||||||
public function initialize()
|
public function initialize()
|
||||||
{
|
{
|
||||||
// 1. Render a visible word at the top of every page (the demo output).
|
// "Remove this Column" entry in the board column header dropdown. The only
|
||||||
$this->template->hook->attach('template:layout:top', 'skeleton:layout/header');
|
// 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');
|
||||||
|
|
||||||
// 2. Load the plugin stylesheet (currently empty -- proves the CSS hook fires).
|
|
||||||
$this->hook->on('template:layout:css', array(
|
|
||||||
'template' => 'plugins/Skeleton/Asset/css/skeleton.css',
|
|
||||||
));
|
|
||||||
|
|
||||||
// 3. Load the plugin script (currently empty -- proves the JS hook fires).
|
|
||||||
$this->hook->on('template:layout:js', array(
|
$this->hook->on('template:layout:js', array(
|
||||||
'template' => 'plugins/Skeleton/Asset/js/skeleton.js',
|
'template' => 'plugins/OrganonTweaks/Asset/js/relocate.js',
|
||||||
));
|
));
|
||||||
|
|
||||||
|
// Settings page for the plugin's tweaks.
|
||||||
|
$this->template->hook->attach('template:config:sidebar', 'organonTweaks:config/sidebar');
|
||||||
|
|
||||||
|
// 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',
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getHelpers()
|
||||||
|
{
|
||||||
|
return array(
|
||||||
|
'Plugin\OrganonTweaks\Helper' => array('OrganonColumnHelper'),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getPluginName()
|
public function getPluginName()
|
||||||
{
|
{
|
||||||
return 'Skeleton';
|
return 'OrganonTweaks';
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getPluginDescription()
|
public function getPluginDescription()
|
||||||
{
|
{
|
||||||
return t('Reusable skeleton/template for building Kanboard plugins.');
|
return t('Umbrella plugin for small Kanboard tweaks: remove an empty board column, always show the card comment icon, and more.');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getPluginAuthor()
|
public function getPluginAuthor()
|
||||||
@@ -39,12 +60,12 @@ class Plugin extends Base
|
|||||||
|
|
||||||
public function getPluginVersion()
|
public function getPluginVersion()
|
||||||
{
|
{
|
||||||
return '0.1.0';
|
return '1.0.0';
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getPluginHomepage()
|
public function getPluginHomepage()
|
||||||
{
|
{
|
||||||
return 'https://code.beco.cc/beco/kanboard-plugin-skeleton';
|
return 'https://code.beco.cc/beco/OrganonTweaks';
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getCompatibleVersion()
|
public function getCompatibleVersion()
|
||||||
|
|||||||
107
README.md
107
README.md
@@ -1,19 +1,58 @@
|
|||||||
# Skeleton -- a Kanboard plugin template
|
# OrganonTweaks -- umbrella plugin for small Kanboard tweaks
|
||||||
|
|
||||||
A minimal, working Kanboard plugin that you copy and rename as the starting point for a
|
A home for small, unrelated Kanboard adjustments that are not big enough to deserve their
|
||||||
real plugin. By itself it does only one trivial thing: it renders the word "Skeleton" at
|
own plugin. Each tweak is self-contained; more will be added over time. If a tweak grows
|
||||||
the top of every page. It changes no data and runs no database migration.
|
into a real subsystem it graduates into its own plugin (see PLAN-organon-tweaks-plugin.md).
|
||||||
|
|
||||||
## What it demonstrates
|
## Tweaks
|
||||||
|
|
||||||
- A complete `Plugin.php` registration class with all the metadata Kanboard shows in
|
### Remove an empty column
|
||||||
Settings -> Plugins (name, description, author, version, homepage, compatible version).
|
|
||||||
- A template hook (`template:layout:top`) that injects a template into the page.
|
Adds a **Remove this Column** entry (trash icon) to a board column's header dropdown, next
|
||||||
- Asset hooks (`template:layout:css` and `template:layout:js`) that load a stylesheet and
|
to the other column actions. It appears only when the column is completely empty -- no
|
||||||
a script. They are empty for now but prove the injection path works -- handy when a real
|
tasks, open or closed -- because Kanboard's column removal is a plain delete and a non-empty
|
||||||
plugin needs custom CSS or JS.
|
column would orphan its tasks. Clicking it asks for confirmation, then removes the column
|
||||||
- The standard plugin directory layout, with stub folders (`Controller/`, `Model/`,
|
and returns to the board.
|
||||||
`Schema/`, `Locale/`, `Test/`) ready to grow into.
|
|
||||||
|
- The menu entry is rendered by the `template:board:column:dropdown` hook (which sits
|
||||||
|
outside the native menu list), then moved into the menu by `Asset/js/relocate.js`.
|
||||||
|
- The empty check counts open and closed tasks via a helper
|
||||||
|
(`Helper/OrganonColumnHelper.php`, `taskFinderModel->countByColumnId(...)`), since the
|
||||||
|
board's own count is open-only.
|
||||||
|
- Only users allowed to manage columns see it, and the controller re-checks both the
|
||||||
|
permission and the emptiness before deleting.
|
||||||
|
|
||||||
|
### Always show the comment icon
|
||||||
|
|
||||||
|
Kanboard shows the comment icon on a board card only once it has at least one comment, so
|
||||||
|
adding the *first* comment means opening the card and finding the sidebar link. With this
|
||||||
|
tweak on, a card with no comments shows a comment bubble with a **"+"** that opens the same
|
||||||
|
comment modal, letting you add the first comment straight from the board. Cards that already
|
||||||
|
have comments keep showing the count as before.
|
||||||
|
|
||||||
|
- Rendered by the `template:board:task:icons` hook (`Template/board/task_comment_icon.php`),
|
||||||
|
only when the card has no comments and the user may create one (so it does not show for
|
||||||
|
read-only viewers).
|
||||||
|
- **On by default.** Toggle it under "Settings -> Organon Tweaks".
|
||||||
|
|
||||||
|
### Keep the board scroll position across refreshes
|
||||||
|
|
||||||
|
When you drop a card (or Kanboard auto-refreshes the board via polling), it rebuilds the
|
||||||
|
board with `$("#board-container").replaceWith(...)`, and the new element starts scrolled to
|
||||||
|
the far left -- so the view jumps back to the first column. This tweak remembers the
|
||||||
|
horizontal scroll position and restores it as soon as the rebuilt board appears, so the
|
||||||
|
board stays where you were.
|
||||||
|
|
||||||
|
- Implemented in `Asset/js/keep-scroll.js`: it observes the stable parent (the container
|
||||||
|
itself is replaced), reads/writes `scrollLeft` on whichever `#board-container` is current.
|
||||||
|
- **On by default.** Toggle it under "Settings -> Organon Tweaks".
|
||||||
|
|
||||||
|
## Settings
|
||||||
|
|
||||||
|
Global (per Kanboard instance) and admin-only, under "Settings -> Organon Tweaks":
|
||||||
|
|
||||||
|
- **Always show the comment icon on board cards** -- default on.
|
||||||
|
- **Keep the board scroll position across refreshes** -- default on.
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
@@ -21,45 +60,9 @@ the top of every page. It changes no data and runs no database migration.
|
|||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
No build step and no dependencies.
|
Copy this folder into your Kanboard installation as `plugins/OrganonTweaks/`. The directory
|
||||||
|
name must be exactly `OrganonTweaks` (Kanboard derives the plugin namespace from the folder
|
||||||
1. Copy this folder into your Kanboard installation as `plugins/Skeleton/`.
|
name). No build step and no database migration.
|
||||||
2. Reload any page. The word "Skeleton" appears at the top.
|
|
||||||
3. Confirm it under Settings -> Plugins.
|
|
||||||
|
|
||||||
To uninstall, delete `plugins/Skeleton/`. Nothing else is left behind.
|
|
||||||
|
|
||||||
## Directory layout
|
|
||||||
|
|
||||||
```
|
|
||||||
Skeleton/
|
|
||||||
Plugin.php Registration and hook wiring (the only required file).
|
|
||||||
README.md
|
|
||||||
LICENSE AGPL-3.0.
|
|
||||||
Template/
|
|
||||||
layout/header.php The visible "Skeleton" word.
|
|
||||||
Asset/
|
|
||||||
css/skeleton.css Loaded via template:layout:css.
|
|
||||||
js/skeleton.js Loaded via template:layout:js.
|
|
||||||
Controller/ Stub for future request handlers.
|
|
||||||
Model/ Stub for future business logic / DB access.
|
|
||||||
Schema/ Stub for future database migrations.
|
|
||||||
Locale/ Stub for future translations (e.g. pt_BR/, fr_FR/).
|
|
||||||
Test/ Stub for future unit tests.
|
|
||||||
```
|
|
||||||
|
|
||||||
## How to fork this into a new plugin
|
|
||||||
|
|
||||||
1. Copy the folder and rename it, e.g. `plugins/MyPlugin/`. The folder name must match the
|
|
||||||
namespace and start with a capital letter.
|
|
||||||
2. In `Plugin.php`, change the namespace from `Kanboard\Plugin\Skeleton` to
|
|
||||||
`Kanboard\Plugin\MyPlugin`.
|
|
||||||
3. Update the metadata methods (`getPluginName`, `getPluginDescription`, `getPluginAuthor`,
|
|
||||||
`getPluginVersion`, `getPluginHomepage`, `getCompatibleVersion`).
|
|
||||||
4. Update the hook target paths: the lowercase prefix in `'skeleton:layout/header'` and the
|
|
||||||
`plugins/Skeleton/Asset/...` asset paths must match the new plugin name.
|
|
||||||
5. Replace `Template/layout/header.php` with your real template, or attach to a different
|
|
||||||
hook. See the Kanboard plugin hooks documentation for the full list of hook points.
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|||||||
12
Template/board/column_dropdown.php
Normal file
12
Template/board/column_dropdown.php
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<?php if ($this->user->hasProjectAccess('ColumnController', 'remove', $column['project_id']) && $this->OrganonColumnHelper->hasNoTasks($column['project_id'], $column['id'])): ?>
|
||||||
|
<?php /* Rendered outside the menu <ul> by the only column hook, then moved into it by
|
||||||
|
Asset/js/relocate.js. Hidden until relocated so it never flashes in the header.
|
||||||
|
Shown only for empty columns (no open or closed tasks). */ ?>
|
||||||
|
<li class="organontweaks-remove-column-item" style="display: none;">
|
||||||
|
<?= $this->modal->confirm('trash-o', t('Remove this Column'), 'ColumnController', 'confirm', array(
|
||||||
|
'plugin' => 'OrganonTweaks',
|
||||||
|
'project_id' => $column['project_id'],
|
||||||
|
'column_id' => $column['id'],
|
||||||
|
)) ?>
|
||||||
|
</li>
|
||||||
|
<?php endif ?>
|
||||||
17
Template/board/remove_column.php
Normal file
17
Template/board/remove_column.php
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<div class="page-header">
|
||||||
|
<h2><?= t('Remove this column') ?></h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form method="post" action="<?= $this->url->href('ColumnController', 'remove', array('plugin' => 'OrganonTweaks', 'project_id' => $project['id'])) ?>">
|
||||||
|
<?= $this->form->csrf() ?>
|
||||||
|
<?= $this->form->hidden('column_id', array('column_id' => $column_id)) ?>
|
||||||
|
|
||||||
|
<p class="alert alert-info">
|
||||||
|
<?= t('Do you really want to remove this column: "%s"?', $column) ?>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<?= $this->modal->submitButtons(array(
|
||||||
|
'submitLabel' => t('Remove'),
|
||||||
|
'color' => 'red',
|
||||||
|
)) ?>
|
||||||
|
</form>
|
||||||
18
Template/board/task_comment_icon.php
Normal file
18
Template/board/task_comment_icon.php
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<?php /*
|
||||||
|
* Rendered by the template:board:task:icons hook, inside the card icons row. When a card
|
||||||
|
* has no comments yet, Kanboard shows no comment icon at all -- so adding the first comment
|
||||||
|
* means opening the card and finding the sidebar link. This adds a comment bubble with a
|
||||||
|
* "+" that opens the same comment modal (CommentListController::show) the native count icon
|
||||||
|
* uses, so the first comment can be added straight from the board. Cards that already have
|
||||||
|
* comments render nothing here (the native count icon already shows).
|
||||||
|
*
|
||||||
|
* Only shown to users who may create a comment (CommentController::save is PROJECT_MEMBER),
|
||||||
|
* so it does not appear for read-only viewers.
|
||||||
|
*/ ?>
|
||||||
|
<?php if (empty($task['nb_comments']) && $this->user->hasProjectAccess('CommentController', 'save', $task['project_id'])): ?>
|
||||||
|
<?php /* The hook renders at the END of the icons row; relocate.js moves this span up to
|
||||||
|
the native comment slot (before the description icon) so the order matches. */ ?>
|
||||||
|
<span class="organontweaks-comment-add">
|
||||||
|
<?= $this->modal->medium('comments-o', '+', 'CommentListController', 'show', array('task_id' => $task['id']), t('Add a comment')) ?>
|
||||||
|
</span>
|
||||||
|
<?php endif ?>
|
||||||
25
Template/config/show.php
Normal file
25
Template/config/show.php
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
<div class="page-header">
|
||||||
|
<h2><?= t('Organon Tweaks') ?></h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form method="post" action="<?= $this->url->href('ConfigController', 'save', array('plugin' => 'OrganonTweaks')) ?>" autocomplete="off">
|
||||||
|
<?= $this->form->csrf() ?>
|
||||||
|
|
||||||
|
<fieldset>
|
||||||
|
<?= $this->form->checkbox('organon_tweaks_always_comment_icon', t('Always show the comment icon on board cards'), 1, isset($values['organon_tweaks_always_comment_icon']) && $values['organon_tweaks_always_comment_icon'] == 1) ?>
|
||||||
|
<p class="form-help">
|
||||||
|
<?= t('Show a comment bubble with a "+" on cards that have no comments yet, so you can add the first comment straight from the board. Cards that already have comments keep showing the count as usual.') ?>
|
||||||
|
</p>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<fieldset>
|
||||||
|
<?= $this->form->checkbox('organon_tweaks_keep_scroll', t('Keep the board scroll position across refreshes'), 1, isset($values['organon_tweaks_keep_scroll']) && $values['organon_tweaks_keep_scroll'] == 1) ?>
|
||||||
|
<p class="form-help">
|
||||||
|
<?= t('When a card is dropped (or the board auto-refreshes), Kanboard rebuilds the board and jumps back to the first column. This restores the horizontal scroll position so the board stays where you were.') ?>
|
||||||
|
</p>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<div class="form-actions">
|
||||||
|
<button type="submit" class="btn btn-blue"><?= t('Save') ?></button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
3
Template/config/sidebar.php
Normal file
3
Template/config/sidebar.php
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<li <?= $this->app->checkMenuSelection('ConfigController', 'show', 'OrganonTweaks') ?>>
|
||||||
|
<?= $this->url->link(t('Organon Tweaks'), 'ConfigController', 'show', array('plugin' => 'OrganonTweaks')) ?>
|
||||||
|
</li>
|
||||||
@@ -1 +0,0 @@
|
|||||||
<div class="skeleton-plugin-marker">Skeleton</div>
|
|
||||||
Reference in New Issue
Block a user