3 Commits
v0.1 ... v0.2

Author SHA1 Message Date
c0464ddd95 Bulk Move Tasks done v0.2 2026-06-28 17:31:42 -03:00
4c4f4157d3 Template added 2026-06-28 17:30:19 -03:00
63d5918c34 BulkMoveController.php control 2026-06-28 17:30:04 -03:00
14 changed files with 166 additions and 73 deletions

View File

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

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,19 @@ class Plugin extends Base
{ {
public function initialize() public function initialize()
{ {
// 1. Render a visible word at the top of every page (the demo output). // Add a "Move all tasks to another column" entry to the board column
$this->template->hook->attach('template:layout:top', 'skeleton:layout/header'); // header dropdown. The hook 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(
'template' => 'plugins/Skeleton/Asset/js/skeleton.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 +30,12 @@ class Plugin extends Base
public function getPluginVersion() public function getPluginVersion()
{ {
return '0.1.0'; return '0.2.0';
} }
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,45 +29,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/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

View File

View File

@@ -0,0 +1,10 @@
<?php if ($column['nb_tasks'] > 0 && $this->user->hasProjectAccess('TaskModificationController', 'update', $column['project_id'])): ?>
<li>
<?= $this->modal->medium('arrows-h', t('Move all tasks to another column'), '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 v0.2