5 Commits
v0.1 ... v0.3

20 changed files with 329 additions and 70 deletions

View File

@@ -1 +0,0 @@
/* Skeleton plugin styles -- add CSS here. Loaded via template:layout:css. */

56
Asset/js/relocate.js Normal file
View File

@@ -0,0 +1,56 @@
/*
* OrganonTweaks -- place the "Remove this Column" entry inside the native column menu.
*
* Kanboard only exposes the template:board:column:dropdown hook AFTER the column menu
* <ul>, and its dropdown JS clones just that <ul> when the menu opens. So the hook item
* cannot reach the menu on its own. column_dropdown.php renders the item hidden next to
* the menu; this script moves each such item into that column's real <ul> so it appears
* alongside the native entries. It re-runs when the board is redrawn (AJAX polling /
* drag-and-drop refresh).
*/
(function () {
"use strict";
function relocate() {
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 init() {
if (!document.getElementById("board")) {
return;
}
relocate();
var container = document.getElementById("board-container");
if (container && window.MutationObserver) {
new MutationObserver(relocate).observe(container, { childList: true, subtree: true });
}
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", init);
} else {
init();
}
})();

View File

@@ -1 +0,0 @@
// Skeleton plugin script -- add JS here. Loaded via template:layout:js.

View File

View 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();
}
}
}

View File

@@ -0,0 +1,37 @@
<?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').' &gt; '.t('Organon Tweaks'),
'values' => array(
'organon_tweaks_always_comment_icon' => (int) $this->configModel->get('organon_tweaks_always_comment_icon', 1),
),
'errors' => array(),
)));
}
public function save()
{
$values = $this->request->getValues();
$alwaysCommentIcon = isset($values['organon_tweaks_always_comment_icon']) ? 1 : 0;
if ($this->configModel->save(array('organon_tweaks_always_comment_icon' => $alwaysCommentIcon))) {
$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')));
}
}

View 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];
}
}

View File

View File

View File

@@ -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,41 @@ 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');
}
}
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 +52,12 @@ class Plugin extends Base
public function getPluginVersion() public function getPluginVersion()
{ {
return '0.1.0'; return '0.3.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()

View File

@@ -1,19 +1,45 @@
# 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".
## Settings
Global (per Kanboard instance) and admin-only, under "Settings -> Organon Tweaks":
- **Always show the comment icon on board cards** -- default on.
## Requirements ## Requirements
@@ -21,45 +47,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

View File

View 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 ?>

View 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>

View File

@@ -0,0 +1,14 @@
<?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'])): ?>
<?= $this->modal->medium('comments-o', '+', 'CommentListController', 'show', array('task_id' => $task['id']), t('Add a comment')) ?>
<?php endif ?>

18
Template/config/show.php Normal file
View File

@@ -0,0 +1,18 @@
<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>
<div class="form-actions">
<button type="submit" class="btn btn-blue"><?= t('Save') ?></button>
</div>
</form>

View File

@@ -0,0 +1,3 @@
<li <?= $this->app->checkMenuSelection('ConfigController', 'show', 'OrganonTweaks') ?>>
<?= $this->url->link(t('Organon Tweaks'), 'ConfigController', 'show', array('plugin' => 'OrganonTweaks')) ?>
</li>

View File

@@ -1 +0,0 @@
<div class="skeleton-plugin-marker">Skeleton</div>

View File

View File

@@ -1 +1 @@
OrganonTweaks v0.1 OrganonTweaks v0.3