My WorkspaceOrg main page, personal per-user project workspaces -- dashboard page, badges, CRUD + assign
This commit is contained in:
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. */
|
||||
53
Asset/css/workspaceorg.css
Normal file
53
Asset/css/workspaceorg.css
Normal file
@@ -0,0 +1,53 @@
|
||||
/* WorkspaceOrg -- the [Workspace] badge shown before a project title. */
|
||||
.wso-badge {
|
||||
display: inline-block;
|
||||
margin-right: 4px;
|
||||
padding: 0 6px;
|
||||
border-radius: 3px;
|
||||
background: #34495e;
|
||||
color: #fff;
|
||||
font-size: 0.85em;
|
||||
font-weight: bold;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
/* "My workspaces" dashboard page. */
|
||||
.wso-group {
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
.wso-group-title {
|
||||
border-bottom: 1px solid #ddd;
|
||||
padding-bottom: 4px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.wso-group-actions {
|
||||
margin-left: 12px;
|
||||
font-size: 0.7em;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.wso-group-actions a {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.wso-empty {
|
||||
margin: 4px 0 0 4px;
|
||||
color: #999;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.wso-table td {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.wso-move {
|
||||
width: 220px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.wso-move-form {
|
||||
display: inline;
|
||||
margin: 0;
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
// Skeleton plugin script -- add JS here. Loaded via template:layout:js.
|
||||
120
Controller/WorkspaceOrgController.php
Normal file
120
Controller/WorkspaceOrgController.php
Normal file
@@ -0,0 +1,120 @@
|
||||
<?php
|
||||
|
||||
namespace Kanboard\Plugin\WorkspaceOrg\Controller;
|
||||
|
||||
use Kanboard\Controller\BaseController;
|
||||
use Kanboard\Plugin\WorkspaceOrg\Helper\WorkspaceOrgHelper;
|
||||
|
||||
/**
|
||||
* The "My workspaces" dashboard page and its workspace CRUD + project assignment. Every action works
|
||||
* on the current user's own workspaces (user metadata), so no admin/manager gate is needed -- any
|
||||
* logged-in user organizes their own projects. State changes go through getValues() (CSRF-checked)
|
||||
* or checkCSRFParam() for the confirm link, mirroring core's TagController.
|
||||
*/
|
||||
class WorkspaceOrgController extends BaseController
|
||||
{
|
||||
private function workspaces()
|
||||
{
|
||||
return new WorkspaceOrgHelper($this->container);
|
||||
}
|
||||
|
||||
private function backToIndex()
|
||||
{
|
||||
$this->response->redirect($this->helper->url->to('WorkspaceOrgController', 'index', array('plugin' => 'WorkspaceOrg')));
|
||||
}
|
||||
|
||||
public function index()
|
||||
{
|
||||
$helper = $this->workspaces();
|
||||
|
||||
$this->response->html($this->helper->layout->dashboard('workspaceOrg:workspace/index', array(
|
||||
'title' => t('My workspaces'),
|
||||
'user' => $this->getUser(),
|
||||
'groups' => $helper->grouped(),
|
||||
'options' => $helper->options(),
|
||||
'default' => WorkspaceOrgHelper::DEFAULT_WORKSPACE,
|
||||
)));
|
||||
}
|
||||
|
||||
public function create(array $values = array(), array $errors = array())
|
||||
{
|
||||
$this->response->html($this->template->render('workspaceOrg:workspace/create', array(
|
||||
'values' => $values,
|
||||
'errors' => $errors,
|
||||
)));
|
||||
}
|
||||
|
||||
public function save()
|
||||
{
|
||||
$values = $this->request->getValues();
|
||||
$name = isset($values['name']) ? $values['name'] : '';
|
||||
|
||||
if ($name !== '' && $this->workspaces()->addWorkspace($name)) {
|
||||
$this->flash->success(t('Workspace created successfully.'));
|
||||
$this->backToIndex();
|
||||
} else {
|
||||
$this->create($values, array('name' => array(t('Invalid name, or a workspace with this name already exists.'))));
|
||||
}
|
||||
}
|
||||
|
||||
public function edit(array $values = array(), array $errors = array())
|
||||
{
|
||||
if (empty($values)) {
|
||||
$name = $this->request->getStringParam('name');
|
||||
$values = array('old' => $name, 'name' => $name);
|
||||
}
|
||||
|
||||
$this->response->html($this->template->render('workspaceOrg:workspace/edit', array(
|
||||
'values' => $values,
|
||||
'errors' => $errors,
|
||||
)));
|
||||
}
|
||||
|
||||
public function update()
|
||||
{
|
||||
$values = $this->request->getValues();
|
||||
$old = isset($values['old']) ? $values['old'] : '';
|
||||
$name = isset($values['name']) ? $values['name'] : '';
|
||||
|
||||
if ($name !== '' && $this->workspaces()->renameWorkspace($old, $name)) {
|
||||
$this->flash->success(t('Workspace renamed successfully.'));
|
||||
$this->backToIndex();
|
||||
} else {
|
||||
$this->edit($values, array('name' => array(t('Invalid name, or a workspace with this name already exists.'))));
|
||||
}
|
||||
}
|
||||
|
||||
public function confirm()
|
||||
{
|
||||
$this->response->html($this->template->render('workspaceOrg:workspace/remove', array(
|
||||
'name' => $this->request->getStringParam('name'),
|
||||
)));
|
||||
}
|
||||
|
||||
public function remove()
|
||||
{
|
||||
$this->checkCSRFParam();
|
||||
|
||||
if ($this->workspaces()->removeWorkspace($this->request->getStringParam('name'))) {
|
||||
$this->flash->success(t('Workspace removed. Its projects moved to Default.'));
|
||||
} else {
|
||||
$this->flash->failure(t('Unable to remove this workspace.'));
|
||||
}
|
||||
|
||||
$this->backToIndex();
|
||||
}
|
||||
|
||||
public function assign()
|
||||
{
|
||||
$values = $this->request->getValues();
|
||||
$project_id = isset($values['project_id']) ? (int) $values['project_id'] : 0;
|
||||
$workspace = isset($values['workspace']) ? $values['workspace'] : WorkspaceOrgHelper::DEFAULT_WORKSPACE;
|
||||
|
||||
if ($project_id > 0) {
|
||||
$this->workspaces()->assign($project_id, $workspace);
|
||||
$this->flash->success(t('Project moved.'));
|
||||
}
|
||||
|
||||
$this->backToIndex();
|
||||
}
|
||||
}
|
||||
278
Helper/WorkspaceOrgHelper.php
Normal file
278
Helper/WorkspaceOrgHelper.php
Normal file
@@ -0,0 +1,278 @@
|
||||
<?php
|
||||
|
||||
namespace Kanboard\Plugin\WorkspaceOrg\Helper;
|
||||
|
||||
use Kanboard\Core\Base;
|
||||
|
||||
/**
|
||||
* WorkspaceOrg helper: the per-user workspace list and project->workspace map, both stored as JSON
|
||||
* in the current user's metadata (no global state, no schema migration).
|
||||
*
|
||||
* "Default" is not a stored workspace -- it is simply the bucket for any project that is not filed,
|
||||
* so it can never be renamed or removed. The names list holds only the user's custom workspaces.
|
||||
*/
|
||||
class WorkspaceOrgHelper extends Base
|
||||
{
|
||||
const NAMES_KEY = 'workspaceorg_names'; // JSON array of the user's custom workspace names
|
||||
const MAP_KEY = 'workspaceorg_map'; // JSON object { project_id: workspace_name }
|
||||
const DEFAULT_WORKSPACE = 'Default';
|
||||
|
||||
const MAX_NAME_LENGTH = 50;
|
||||
|
||||
/**
|
||||
* Current user id (workspaces are always personal to whoever is logged in).
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
private function uid()
|
||||
{
|
||||
return (int) $this->userSession->getId();
|
||||
}
|
||||
|
||||
/**
|
||||
* The user's custom workspace names, de-duplicated and sorted A-Z (Default not included).
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
public function names()
|
||||
{
|
||||
$raw = $this->userMetadataModel->get($this->uid(), self::NAMES_KEY, '');
|
||||
$names = $raw === '' ? array() : json_decode($raw, true);
|
||||
|
||||
if (! is_array($names)) {
|
||||
$names = array();
|
||||
}
|
||||
|
||||
$names = array_values(array_unique(array_filter($names, 'is_string')));
|
||||
natcasesort($names);
|
||||
|
||||
return array_values($names);
|
||||
}
|
||||
|
||||
/**
|
||||
* The project_id -> workspace_name map.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function map()
|
||||
{
|
||||
$raw = $this->userMetadataModel->get($this->uid(), self::MAP_KEY, '');
|
||||
$map = $raw === '' ? array() : json_decode($raw, true);
|
||||
|
||||
return is_array($map) ? $map : array();
|
||||
}
|
||||
|
||||
/**
|
||||
* The workspace a project is filed under, validated against the current names; falls back to
|
||||
* Default when unassigned or pointing at a deleted workspace.
|
||||
*
|
||||
* @param int $project_id
|
||||
* @return string
|
||||
*/
|
||||
public function workspaceOf($project_id)
|
||||
{
|
||||
$project_id = (int) $project_id;
|
||||
$map = $this->map();
|
||||
|
||||
if (isset($map[$project_id]) && in_array($map[$project_id], $this->names(), true)) {
|
||||
return $map[$project_id];
|
||||
}
|
||||
|
||||
return self::DEFAULT_WORKSPACE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Badge text before a project title: the workspace name, or '' for Default (badge hidden).
|
||||
*
|
||||
* @param int $project_id
|
||||
* @return string
|
||||
*/
|
||||
public function badge($project_id)
|
||||
{
|
||||
$workspace = $this->workspaceOf($project_id);
|
||||
|
||||
return $workspace === self::DEFAULT_WORKSPACE ? '' : $workspace;
|
||||
}
|
||||
|
||||
/**
|
||||
* The options for a "move to" dropdown: custom workspaces A-Z, then Default last.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
public function options()
|
||||
{
|
||||
$options = $this->names();
|
||||
$options[] = self::DEFAULT_WORKSPACE;
|
||||
|
||||
return $options;
|
||||
}
|
||||
|
||||
/**
|
||||
* The current user's active projects grouped by workspace, ready for the page: an ordered map
|
||||
* of workspace_name => list of array('id','name'), custom workspaces A-Z then Default last, and
|
||||
* projects sorted A-Z within each.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function grouped()
|
||||
{
|
||||
$groups = array();
|
||||
|
||||
foreach ($this->names() as $name) {
|
||||
$groups[$name] = array();
|
||||
}
|
||||
|
||||
$groups[self::DEFAULT_WORKSPACE] = array();
|
||||
|
||||
foreach ($this->projectUserRoleModel->getActiveProjectsByUser($this->uid()) as $project_id => $project_name) {
|
||||
$groups[$this->workspaceOf($project_id)][] = array('id' => (int) $project_id, 'name' => $project_name);
|
||||
}
|
||||
|
||||
foreach ($groups as &$list) {
|
||||
usort($list, function ($a, $b) {
|
||||
return strnatcasecmp($a['name'], $b['name']);
|
||||
});
|
||||
}
|
||||
unset($list);
|
||||
|
||||
return $groups;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new (empty) workspace. Rejects blanks, duplicates, and the reserved "Default".
|
||||
*
|
||||
* @param string $name
|
||||
* @return bool
|
||||
*/
|
||||
public function addWorkspace($name)
|
||||
{
|
||||
$name = $this->clean($name);
|
||||
|
||||
if ($name === '' || $this->isDefault($name) || $this->exists($name)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$names = $this->names();
|
||||
$names[] = $name;
|
||||
$this->saveNames($names);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rename a workspace and rewrite it across the assignment map.
|
||||
*
|
||||
* @param string $old
|
||||
* @param string $new
|
||||
* @return bool
|
||||
*/
|
||||
public function renameWorkspace($old, $new)
|
||||
{
|
||||
$new = $this->clean($new);
|
||||
|
||||
if ($new === '' || $this->isDefault($old) || $this->isDefault($new) || ! $this->exists($old)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (strcasecmp($old, $new) !== 0 && $this->exists($new)) {
|
||||
return false; // would collide with a different existing workspace
|
||||
}
|
||||
|
||||
$names = array();
|
||||
foreach ($this->names() as $name) {
|
||||
$names[] = $name === $old ? $new : $name;
|
||||
}
|
||||
$this->saveNames($names);
|
||||
|
||||
$map = $this->map();
|
||||
foreach ($map as $project_id => $workspace) {
|
||||
if ($workspace === $old) {
|
||||
$map[$project_id] = $new;
|
||||
}
|
||||
}
|
||||
$this->saveMap($map);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a workspace; its projects fall back to Default (dropped from the map).
|
||||
*
|
||||
* @param string $name
|
||||
* @return bool
|
||||
*/
|
||||
public function removeWorkspace($name)
|
||||
{
|
||||
if ($this->isDefault($name) || ! $this->exists($name)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$names = array_values(array_filter($this->names(), function ($n) use ($name) {
|
||||
return $n !== $name;
|
||||
}));
|
||||
$this->saveNames($names);
|
||||
|
||||
$map = $this->map();
|
||||
foreach ($map as $project_id => $workspace) {
|
||||
if ($workspace === $name) {
|
||||
unset($map[$project_id]);
|
||||
}
|
||||
}
|
||||
$this->saveMap($map);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* File a project into a workspace. Default (or any unknown name) means "unassigned" -> removed
|
||||
* from the map.
|
||||
*
|
||||
* @param int $project_id
|
||||
* @param string $workspace
|
||||
*/
|
||||
public function assign($project_id, $workspace)
|
||||
{
|
||||
$project_id = (int) $project_id;
|
||||
$map = $this->map();
|
||||
|
||||
if ($this->isDefault($workspace) || ! $this->exists($workspace)) {
|
||||
unset($map[$project_id]);
|
||||
} else {
|
||||
$map[$project_id] = $workspace;
|
||||
}
|
||||
|
||||
$this->saveMap($map);
|
||||
}
|
||||
|
||||
private function clean($name)
|
||||
{
|
||||
return trim(mb_substr((string) $name, 0, self::MAX_NAME_LENGTH));
|
||||
}
|
||||
|
||||
private function isDefault($name)
|
||||
{
|
||||
return strcasecmp((string) $name, self::DEFAULT_WORKSPACE) === 0;
|
||||
}
|
||||
|
||||
private function exists($name)
|
||||
{
|
||||
foreach ($this->names() as $existing) {
|
||||
if (strcasecmp($existing, $name) === 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private function saveNames(array $names)
|
||||
{
|
||||
natcasesort($names);
|
||||
$this->userMetadataModel->save($this->uid(), array(self::NAMES_KEY => json_encode(array_values($names))));
|
||||
}
|
||||
|
||||
private function saveMap(array $map)
|
||||
{
|
||||
$this->userMetadataModel->save($this->uid(), array(self::MAP_KEY => json_encode($map, JSON_FORCE_OBJECT)));
|
||||
}
|
||||
}
|
||||
38
Plugin.php
38
Plugin.php
@@ -1,35 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace Kanboard\Plugin\Skeleton;
|
||||
namespace Kanboard\Plugin\WorkspaceOrg;
|
||||
|
||||
use Kanboard\Core\Plugin\Base;
|
||||
|
||||
/**
|
||||
* WorkspaceOrg -- group your projects into personal workspaces.
|
||||
*
|
||||
* Each user files their own projects into their own named workspaces (a per-user folder). Everything
|
||||
* is stored in user metadata -- nothing global, no database migration. A "My workspaces" page on the
|
||||
* dashboard lists projects grouped by workspace and lets you add/rename/remove workspaces and move
|
||||
* projects between them; a [Workspace] badge appears before the project title on the dashboard and
|
||||
* the project list.
|
||||
*/
|
||||
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');
|
||||
// "My workspaces" entry in the dashboard sidebar (visible to every logged-in user).
|
||||
$this->template->hook->attach('template:dashboard:sidebar', 'workspaceOrg:dashboard/sidebar');
|
||||
|
||||
// [Workspace] badge before the project title -- this hook fires on the dashboard overview,
|
||||
// the dashboard "My projects" tab, and the Projects management list (all via project_title).
|
||||
$this->template->hook->attach('template:dashboard:project:before-title', 'workspaceOrg:project/badge');
|
||||
|
||||
// 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',
|
||||
'template' => 'plugins/WorkspaceOrg/Asset/css/workspaceorg.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 getHelpers()
|
||||
{
|
||||
return array(
|
||||
'Plugin\WorkspaceOrg\Helper' => array('WorkspaceOrgHelper'),
|
||||
);
|
||||
}
|
||||
|
||||
public function getPluginName()
|
||||
{
|
||||
return 'Skeleton';
|
||||
return 'WorkspaceOrg';
|
||||
}
|
||||
|
||||
public function getPluginDescription()
|
||||
{
|
||||
return t('Reusable skeleton/template for building Kanboard plugins.');
|
||||
return t('Group your projects into personal per-user workspaces, shown on a "My workspaces" dashboard page with a badge on each project.');
|
||||
}
|
||||
|
||||
public function getPluginAuthor()
|
||||
@@ -44,7 +58,7 @@ class Plugin extends Base
|
||||
|
||||
public function getPluginHomepage()
|
||||
{
|
||||
return 'https://code.beco.cc/beco/kanboard-plugin-skeleton';
|
||||
return 'https://code.beco.cc/beco/WorkspaceOrg';
|
||||
}
|
||||
|
||||
public function getCompatibleVersion()
|
||||
|
||||
112
README.md
112
README.md
@@ -1,19 +1,43 @@
|
||||
# Skeleton -- a Kanboard plugin template
|
||||
# WorkspaceOrg -- group your projects into personal workspaces
|
||||
|
||||
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 small Kanboard plugin that lets each user file their projects into their own named
|
||||
**workspaces** (think folders: "UNIBRA", "UPE", "Home"), and see them grouped on a
|
||||
**My workspaces** dashboard page. Kanboard's project list is otherwise flat -- this gives you
|
||||
structure without touching how projects themselves work.
|
||||
|
||||
## What it demonstrates
|
||||
Everything is **per user**: your workspaces and how you file your projects are yours alone.
|
||||
Two people can file the same shared project differently, and neither sees the other's grouping.
|
||||
|
||||
- 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.
|
||||
## What it does
|
||||
|
||||
- Adds a **My workspaces** entry to the dashboard sidebar (next to Overview / My projects /
|
||||
My tasks / My subtasks). Visible to every logged-in user -- no admin rights needed.
|
||||
- That page lists your projects **grouped by workspace**, and lets you:
|
||||
- **add / rename / remove** workspaces (each via a small modal, like Kanboard's Tags page);
|
||||
- **move a project** to another workspace with an inline dropdown on its row.
|
||||
- Shows a **`[Workspace]`** badge before the project title on the dashboard overview, the
|
||||
dashboard "My projects" tab, and the Projects management list -- so you can see at a glance
|
||||
where each project is filed.
|
||||
|
||||
## The "Default" workspace
|
||||
|
||||
Every project starts in **Default**, which is simply "not filed yet". Default is not a real
|
||||
stored workspace -- it is the leftover bucket -- so it can never be renamed or removed, and it
|
||||
always sorts last. Projects in Default show **no badge** (to keep unfiled projects clean).
|
||||
|
||||
Removing a workspace moves its projects back to Default; it never deletes anything.
|
||||
|
||||
## Ordering
|
||||
|
||||
On the My workspaces page, workspaces are listed alphabetically (A-Z) with Default last, and the
|
||||
projects within each workspace are alphabetical too.
|
||||
|
||||
## How it stores data
|
||||
|
||||
No database migration. Everything lives in **user metadata**: your workspace names
|
||||
(`workspaceorg_names`) and your project-to-workspace map (`workspaceorg_map`), both as JSON.
|
||||
Nothing is global and nothing touches the projects themselves -- uninstalling the plugin leaves
|
||||
that metadata harmlessly in place and changes nothing about your projects.
|
||||
|
||||
## Requirements
|
||||
|
||||
@@ -21,46 +45,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/WorkspaceOrg/`. The directory name
|
||||
must be exactly `WorkspaceOrg` (Kanboard derives the plugin namespace from the folder name). No
|
||||
build step and no database migration. Reload, then open **My workspaces** from the dashboard.
|
||||
|
||||
## 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.
|
||||
- **[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).
|
||||
- **[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.
|
||||
- **[BulkMoveTasks](https://code.beco.cc/beco/BulkMoveTasks)** -- move every task from one board
|
||||
column to another in a single action.
|
||||
- **[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.
|
||||
|
||||
3
Template/dashboard/sidebar.php
Normal file
3
Template/dashboard/sidebar.php
Normal file
@@ -0,0 +1,3 @@
|
||||
<li <?= $this->app->checkMenuSelection('WorkspaceOrgController', 'index') ?>>
|
||||
<?= $this->url->link(t('My workspaces'), 'WorkspaceOrgController', 'index', array('plugin' => 'WorkspaceOrg')) ?>
|
||||
</li>
|
||||
@@ -1 +0,0 @@
|
||||
<div class="skeleton-plugin-marker">Skeleton</div>
|
||||
14
Template/project/badge.php
Normal file
14
Template/project/badge.php
Normal file
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
// [Workspace] badge before a project title (dashboard overview, My projects tab, Projects list).
|
||||
// Hidden for projects in Default. Reflects the CURRENT user's personal workspace assignment.
|
||||
if (empty($project['id'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$workspace = $this->WorkspaceOrgHelper->badge($project['id']);
|
||||
|
||||
if ($workspace === '') {
|
||||
return;
|
||||
}
|
||||
?>
|
||||
<span class="wso-badge" title="<?= $this->text->e(t('Workspace')) ?>">[<?= $this->text->e($workspace) ?>]</span>
|
||||
12
Template/workspace/create.php
Normal file
12
Template/workspace/create.php
Normal file
@@ -0,0 +1,12 @@
|
||||
<div class="page-header">
|
||||
<h2><?= t('Add workspace') ?></h2>
|
||||
</div>
|
||||
|
||||
<form method="post" action="<?= $this->url->href('WorkspaceOrgController', 'save', array('plugin' => 'WorkspaceOrg')) ?>" autocomplete="off">
|
||||
<?= $this->form->csrf() ?>
|
||||
|
||||
<?= $this->form->label(t('Name'), 'name') ?>
|
||||
<?= $this->form->text('name', $values, $errors, array('autofocus', 'required', 'maxlength="50"')) ?>
|
||||
|
||||
<?= $this->modal->submitButtons() ?>
|
||||
</form>
|
||||
13
Template/workspace/edit.php
Normal file
13
Template/workspace/edit.php
Normal file
@@ -0,0 +1,13 @@
|
||||
<div class="page-header">
|
||||
<h2><?= t('Rename workspace') ?></h2>
|
||||
</div>
|
||||
|
||||
<form method="post" action="<?= $this->url->href('WorkspaceOrgController', 'update', array('plugin' => 'WorkspaceOrg')) ?>" autocomplete="off">
|
||||
<?= $this->form->csrf() ?>
|
||||
<?= $this->form->hidden('old', $values) ?>
|
||||
|
||||
<?= $this->form->label(t('Name'), 'name') ?>
|
||||
<?= $this->form->text('name', $values, $errors, array('autofocus', 'required', 'maxlength="50"')) ?>
|
||||
|
||||
<?= $this->modal->submitButtons() ?>
|
||||
</form>
|
||||
48
Template/workspace/index.php
Normal file
48
Template/workspace/index.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<div class="page-header">
|
||||
<h2><?= t('My workspaces') ?></h2>
|
||||
<ul>
|
||||
<li>
|
||||
<?= $this->modal->medium('plus', t('Add workspace'), 'WorkspaceOrgController', 'create', array('plugin' => 'WorkspaceOrg')) ?>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<?php foreach ($groups as $workspace => $projects): ?>
|
||||
<div class="wso-group">
|
||||
<h3 class="wso-group-title">
|
||||
<?= $this->text->e($workspace) ?>
|
||||
<?php if ($workspace !== $default): ?>
|
||||
<span class="wso-group-actions">
|
||||
<?= $this->modal->medium('edit', t('Rename'), 'WorkspaceOrgController', 'edit', array('plugin' => 'WorkspaceOrg', 'name' => $workspace)) ?>
|
||||
<?= $this->modal->confirm('trash-o', t('Remove'), 'WorkspaceOrgController', 'confirm', array('plugin' => 'WorkspaceOrg', 'name' => $workspace)) ?>
|
||||
</span>
|
||||
<?php endif ?>
|
||||
</h3>
|
||||
|
||||
<?php if (empty($projects)): ?>
|
||||
<p class="wso-empty"><?= t('(no projects yet)') ?></p>
|
||||
<?php else: ?>
|
||||
<table class="table-striped wso-table">
|
||||
<?php foreach ($projects as $project): ?>
|
||||
<tr>
|
||||
<td class="wso-project">
|
||||
<?= $this->url->link($this->text->e($project['name']), 'BoardViewController', 'show', array('project_id' => $project['id'])) ?>
|
||||
</td>
|
||||
<td class="wso-move">
|
||||
<form method="post" class="wso-move-form" action="<?= $this->url->href('WorkspaceOrgController', 'assign', array('plugin' => 'WorkspaceOrg')) ?>">
|
||||
<?= $this->form->csrf() ?>
|
||||
<input type="hidden" name="project_id" value="<?= (int) $project['id'] ?>">
|
||||
<select name="workspace" onchange="this.form.submit()">
|
||||
<?php foreach ($options as $option): ?>
|
||||
<option value="<?= $this->text->e($option) ?>"<?= $option === $workspace ? ' selected="selected"' : '' ?>><?= $this->text->e($option) ?></option>
|
||||
<?php endforeach ?>
|
||||
</select>
|
||||
<noscript><button type="submit" class="btn btn-blue"><?= t('Move') ?></button></noscript>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach ?>
|
||||
</table>
|
||||
<?php endif ?>
|
||||
</div>
|
||||
<?php endforeach ?>
|
||||
11
Template/workspace/remove.php
Normal file
11
Template/workspace/remove.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<div class="page-header">
|
||||
<h2><?= t('Remove workspace') ?></h2>
|
||||
</div>
|
||||
|
||||
<div class="confirm">
|
||||
<p class="alert alert-info">
|
||||
<?= t('Do you really want to remove the workspace "%s"? Its projects will move to Default.', $name) ?>
|
||||
</p>
|
||||
|
||||
<?= $this->modal->confirmButtons('WorkspaceOrgController', 'remove', array('plugin' => 'WorkspaceOrg', 'name' => $name)) ?>
|
||||
</div>
|
||||
Reference in New Issue
Block a user