9 Commits
v0.1 ... master

15 changed files with 253 additions and 70 deletions

View File

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

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

@@ -0,0 +1,62 @@
/*
* 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() {
var container = document.getElementById("board-container");
if (!container) {
return;
}
relocate();
// BoardDragAndDrop.refresh() does $("#board-container").replaceWith(data) on every
// drag-and-drop and poll refresh, so an observer bound to #board-container would be
// left watching a detached node and never fire again (the entry vanishes until F5).
// Observe the stable parent instead: it survives the swap, and its childList mutation
// fires exactly when the container is replaced, so relocate() re-runs on the new DOM.
if (window.MutationObserver && container.parentNode) {
new MutationObserver(relocate).observe(container.parentNode, { 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
namespace Kanboard\Plugin\Skeleton;
namespace Kanboard\Plugin\BulkMoveTasks;
use Kanboard\Core\Plugin\Base;
@@ -8,28 +8,24 @@ class Plugin extends Base
{
public function initialize()
{
// 1. Render a visible word at the top of every page (the demo output).
$this->template->hook->attach('template:layout:top', 'skeleton:layout/header');
// Render the "Move tasks in bulk" entry. The only column hook sits outside the
// 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(
'template' => 'plugins/Skeleton/Asset/js/skeleton.js',
'template' => 'plugins/BulkMoveTasks/Asset/js/relocate.js',
));
}
public function getPluginName()
{
return 'Skeleton';
return 'BulkMoveTasks';
}
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()
@@ -39,12 +35,12 @@ class Plugin extends Base
public function getPluginVersion()
{
return '0.1.0';
return '1.0.4';
}
public function getPluginHomepage()
{
return 'https://code.beco.cc/beco/kanboard-plugin-skeleton';
return 'https://code.beco.cc/beco/BulkMoveTasks';
}
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
real plugin. By itself it does only one trivial thing: it renders the word "Skeleton" at
the top of every page. It changes no data and runs no database migration.
A Kanboard plugin that adds a "Move all tasks to another column" action to the board
column header menu. It moves every task of that column (within the swimlane you clicked)
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
Settings -> Plugins (name, description, author, version, homepage, compatible version).
- A template hook (`template:layout:top`) that injects a template into the page.
- Asset hooks (`template:layout:css` and `template:layout:js`) that load a stylesheet and
a script. They are empty for now but prove the injection path works -- handy when a real
plugin needs custom CSS or JS.
- The standard plugin directory layout, with stub folders (`Controller/`, `Model/`,
`Schema/`, `Locale/`, `Test/`) ready to grow into.
In the board, open a column's title dropdown. When the column has tasks you will see
**Move all tasks to another column**. It opens a small dialog listing the other columns of
the project; choose one and confirm. All open tasks of the source column and swimlane are
appended, in their existing order, to the end of the destination column.
Each task is moved with Kanboard's normal move logic, so positions are recalculated and
the usual move events fire (activity stream, automatic actions, etc.) just as if you had
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
@@ -21,46 +29,30 @@ the top of every page. It changes no data and runs no database migration.
## Installation
No build step and no dependencies.
1. Copy this folder into your Kanboard installation as `plugins/Skeleton/`.
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.
Copy this folder into your Kanboard installation as `plugins/BulkMoveTasks/`. The
directory name must be exactly `BulkMoveTasks` (Kanboard derives the plugin namespace from
the folder name). No build step and no database migration.
## License
AGPL-3.0. See LICENSE.
## More Kanboard plugins by Dr. Beco
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.
- **[QualKard](https://code.beco.cc/beco/QualKard)** -- flashcard spaced repetition: grade a card by
dragging it to a column, and it comes back when due (needs RecoReco).
- **[WorkspaceOrg](https://code.beco.cc/beco/WorkspaceOrg)** -- group your projects into personal
per-user workspaces, shown on a "My workspaces" dashboard page with a badge on each project.
- **[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.4