6 Commits
v0.1 ... v1.0.1

15 changed files with 244 additions and 70 deletions

View File

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

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

@@ -0,0 +1,57 @@
/*
* BulkMoveTasks -- place the "Move tasks in bulk" 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. The column_dropdown.php template renders the item
* hidden next to the menu; this script moves each such item into that column's real <ul>
* so it appears alongside "Hide this column", "Create tasks in bulk", etc.
*
* It re-runs when the board is redrawn (AJAX polling / drag-and-drop refresh).
*/
(function () {
"use strict";
function relocate() {
var items = document.querySelectorAll(".bulkmovetasks-menu-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,103 @@
<?php
namespace Kanboard\Plugin\BulkMoveTasks\Controller;
use Kanboard\Controller\BaseController;
use Kanboard\Core\Controller\AccessForbiddenException;
use Kanboard\Model\TaskModel;
/**
* Move every task of a board column (within one swimlane) to another column.
*/
class BulkMoveController extends BaseController
{
/**
* Render the modal: pick the destination column.
*/
public function show()
{
$project = $this->getProject();
$this->checkAccess($project['id']);
$column_id = $this->request->getIntegerParam('column_id');
$swimlane_id = $this->request->getIntegerParam('swimlane_id');
// Offer every column of the project except the source one as a destination.
$columns = $this->columnModel->getList($project['id']);
unset($columns[$column_id]);
$this->response->html($this->template->render('bulkMoveTasks:board/move_all_tasks', array(
'project' => $project,
'nb_tasks' => $this->taskFinderModel->countByColumnAndSwimlaneId($project['id'], $column_id, $swimlane_id),
'column' => $this->columnModel->getColumnTitleById($column_id),
'swimlane' => $this->swimlaneModel->getNameById($swimlane_id),
'columns_list' => $columns,
'values' => array(
'column_id' => $column_id,
'swimlane_id' => $swimlane_id,
),
)));
}
/**
* Perform the move and return to the board.
*/
public function move()
{
$project = $this->getProject();
$this->checkAccess($project['id']);
$values = $this->request->getValues();
$src_column_id = isset($values['column_id']) ? (int) $values['column_id'] : 0;
$dst_column_id = isset($values['dst_column_id']) ? (int) $values['dst_column_id'] : 0;
$swimlane_id = isset($values['swimlane_id']) ? (int) $values['swimlane_id'] : 0;
$columns = $this->columnModel->getList($project['id']);
if ($src_column_id === 0 || $dst_column_id === 0 || $src_column_id === $dst_column_id || ! isset($columns[$dst_column_id])) {
$this->flash->failure(t('Please choose a different destination column.'));
} else {
$moved = $this->moveAllTasks($project['id'], $src_column_id, $dst_column_id, $swimlane_id);
$this->flash->success(t('%d task(s) moved from "%s" to "%s".', $moved, $this->columnModel->getColumnTitleById($src_column_id), $this->columnModel->getColumnTitleById($dst_column_id)));
}
$this->response->redirect($this->helper->url->to('BoardViewController', 'show', array('project_id' => $project['id'])));
}
/**
* Move every open task of (source column, swimlane) to the end of the
* destination column, keeping their relative order. Returns the count moved.
*/
private function moveAllTasks($project_id, $src_column_id, $dst_column_id, $swimlane_id)
{
$task_ids = $this->db->table(TaskModel::TABLE)
->eq('project_id', $project_id)
->eq('column_id', $src_column_id)
->eq('swimlane_id', $swimlane_id)
->eq('is_active', TaskModel::STATUS_OPEN)
->asc('position')
->findAllByColumn('id');
$position = $this->taskFinderModel->countByColumnAndSwimlaneId($project_id, $dst_column_id, $swimlane_id) + 1;
$moved = 0;
foreach ($task_ids as $task_id) {
if ($this->taskPositionModel->movePosition($project_id, $task_id, $dst_column_id, $position, $swimlane_id)) {
$position++;
$moved++;
}
}
return $moved;
}
/**
* Allow only users who can modify tasks in this project.
*/
private function checkAccess($project_id)
{
if (! $this->helper->user->hasProjectAccess('TaskModificationController', 'update', $project_id)) {
throw new AccessForbiddenException();
}
}
}

View File

View File

View File

@@ -1,6 +1,6 @@
<?php <?php
namespace Kanboard\Plugin\Skeleton; namespace Kanboard\Plugin\BulkMoveTasks;
use Kanboard\Core\Plugin\Base; use Kanboard\Core\Plugin\Base;
@@ -8,28 +8,24 @@ class Plugin extends Base
{ {
public function initialize() public function initialize()
{ {
// 1. Render a visible word at the top of every page (the demo output). // Render the "Move tasks in bulk" entry. The only column hook sits outside the
$this->template->hook->attach('template:layout:top', 'skeleton:layout/header'); // native menu <ul>, so column_dropdown.php renders it hidden and relocate.js
// moves it into the menu (passes the current $column and $swimlane).
$this->template->hook->attach('template:board:column:dropdown', 'bulkMoveTasks: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/BulkMoveTasks/Asset/js/relocate.js',
)); ));
} }
public function getPluginName() public function getPluginName()
{ {
return 'Skeleton'; return 'BulkMoveTasks';
} }
public function getPluginDescription() public function getPluginDescription()
{ {
return t('Reusable skeleton/template for building Kanboard plugins.'); return t('Move all tasks from one board column to another in a single action.');
} }
public function getPluginAuthor() public function getPluginAuthor()
@@ -39,12 +35,12 @@ class Plugin extends Base
public function getPluginVersion() public function getPluginVersion()
{ {
return '0.1.0'; return '1.0.1';
} }
public function getPluginHomepage() public function getPluginHomepage()
{ {
return 'https://code.beco.cc/beco/kanboard-plugin-skeleton'; return 'https://code.beco.cc/beco/BulkMoveTasks';
} }
public function getCompatibleVersion() public function getCompatibleVersion()

View File

@@ -1,19 +1,27 @@
# Skeleton -- a Kanboard plugin template # BulkMoveTasks -- move a whole column of tasks at once
A minimal, working Kanboard plugin that you copy and rename as the starting point for a A Kanboard plugin that adds a "Move all tasks to another column" action to the board
real plugin. By itself it does only one trivial thing: it renders the word "Skeleton" at column header menu. It moves every task of that column (within the swimlane you clicked)
the top of every page. It changes no data and runs no database migration. to a destination column you pick, in one step, instead of dragging cards one by one.
## What it demonstrates ## What it does
- A complete `Plugin.php` registration class with all the metadata Kanboard shows in In the board, open a column's title dropdown. When the column has tasks you will see
Settings -> Plugins (name, description, author, version, homepage, compatible version). **Move all tasks to another column**. It opens a small dialog listing the other columns of
- A template hook (`template:layout:top`) that injects a template into the page. the project; choose one and confirm. All open tasks of the source column and swimlane are
- Asset hooks (`template:layout:css` and `template:layout:js`) that load a stylesheet and appended, in their existing order, to the end of the destination column.
a script. They are empty for now but prove the injection path works -- handy when a real
plugin needs custom CSS or JS. Each task is moved with Kanboard's normal move logic, so positions are recalculated and
- The standard plugin directory layout, with stub folders (`Controller/`, `Model/`, the usual move events fire (activity stream, automatic actions, etc.) just as if you had
`Schema/`, `Locale/`, `Test/`) ready to grow into. dragged the cards yourself.
## Scope of a move
- Acts on one swimlane at a time -- the swimlane of the column header you used. On a board
with several swimlanes, repeat per swimlane.
- Moves the open (active) tasks shown on the board. Closed tasks are not affected.
- Only users who can modify tasks in the project see the action, and the server re-checks
that permission before moving anything.
## Requirements ## Requirements
@@ -21,46 +29,26 @@ 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/BulkMoveTasks/`. The
directory name must be exactly `BulkMoveTasks` (Kanboard derives the plugin namespace from
1. Copy this folder into your Kanboard installation as `plugins/Skeleton/`. the folder 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
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.
- **[OrganonTweaks](https://code.beco.cc/beco/OrganonTweaks)** -- an umbrella of small
quality-of-life board tweaks: remove an empty column, always show the comment icon, emphasize due
dates, extra search keywords and shared board filters, and more.
- **[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.

View File

View File

@@ -0,0 +1,13 @@
<?php if ($column['nb_tasks'] > 0 && $this->user->hasProjectAccess('TaskModificationController', 'update', $column['project_id'])): ?>
<?php /* Rendered here (outside the menu <ul>) by the only available hook, then moved
into the column's real dropdown <ul> by Asset/js/relocate.js so it sits next
to the native entries. Hidden until relocated to avoid a flash in the header. */ ?>
<li class="bulkmovetasks-menu-item" style="display: none;">
<?= $this->modal->medium('arrows-h', t('Move tasks in bulk'), 'BulkMoveController', 'show', array(
'plugin' => 'BulkMoveTasks',
'project_id' => $column['project_id'],
'column_id' => $column['id'],
'swimlane_id' => $swimlane['id'],
)) ?>
</li>
<?php endif ?>

View File

@@ -0,0 +1,20 @@
<div class="page-header">
<h2><?= t('Move all tasks to another column') ?></h2>
</div>
<form method="post" action="<?= $this->url->href('BulkMoveController', 'move', array('plugin' => 'BulkMoveTasks', 'project_id' => $project['id'])) ?>">
<?= $this->form->csrf() ?>
<?= $this->form->hidden('column_id', $values) ?>
<?= $this->form->hidden('swimlane_id', $values) ?>
<p class="alert">
<?= t('%d task(s) from the column "%s" (swimlane "%s") will be moved.', $nb_tasks, $column, $swimlane) ?>
</p>
<?= $this->form->label(t('Destination column'), 'dst_column_id') ?>
<?= $this->form->select('dst_column_id', $columns_list) ?>
<?= $this->modal->submitButtons(array(
'submitLabel' => t('Move'),
)) ?>
</form>

View File

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

View File

View File

@@ -1 +1 @@
BulkMoveTasks v0.1 BulkMoveTasks v1.0.1