My WorkspaceOrg main page, personal per-user project workspaces -- dashboard page, badges, CRUD + assign
This commit is contained in:
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)));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user