279 lines
7.4 KiB
PHP
279 lines
7.4 KiB
PHP
|
|
<?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)));
|
||
|
|
}
|
||
|
|
}
|