Files
WorkspaceOrg/Helper/WorkspaceOrgHelper.php

284 lines
7.6 KiB
PHP
Raw Permalink Normal View History

<?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();
// Full project rows (id, name, owner_name, is_active, ...) for the user's active projects,
// the same data source the native "My projects" dashboard uses.
$ids = $this->projectPermissionModel->getActiveProjectIds($this->uid());
$projects = empty($ids) ? array() : $this->projectModel->getQueryColumnStats($ids)->findAll();
foreach ($projects as $project) {
$groups[$this->workspaceOf($project['id'])][] = $project;
}
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)));
}
}