Allow Spaces to be archived

Summary:
Ref T8377. This adds a standard disable/enable feature to Spaces, with a couple of twists:

  - You can't create new stuff in an archived space, and you can't move stuff into an archived space.
  - We don't show results from an archived space by default in ApplicationSearch queries. You can still find these objects if you explicitly search for "Spaces: <the archived space>".

So this is a "put it in a box in the attic" sort of operation, but that seems fairly nice/reasonable.

Test Plan:
  - Archived and activated spaces.
  - Used ApplicationSearch, which omitted archived objects by default but allowed searches for them, specifically, to succeed.
  - Tried to create objects into an archived space (this is not allowed).
  - Edited objects in an archived space (this is OK).

Reviewers: btrahan

Reviewed By: btrahan

Subscribers: epriestley

Maniphest Tasks: T8377

Differential Revision: https://secure.phabricator.com/D13238
This commit is contained in:
epriestley
2015-06-11 10:13:47 -07:00
parent a06618f879
commit 88e7cd158f
18 changed files with 309 additions and 38 deletions

View File

@@ -38,6 +38,15 @@ final class PhabricatorSpacesApplication extends PhabricatorApplication {
return true;
}
public function getHelpDocumentationArticles(PhabricatorUser $viewer) {
return array(
array(
'name' => pht('Spaces User Guide'),
'href' => PhabricatorEnv::getDoclink('Spaces User Guide'),
),
);
}
public function getRemarkupRules() {
return array(
new PhabricatorSpacesRemarkupRule(),
@@ -51,6 +60,8 @@ final class PhabricatorSpacesApplication extends PhabricatorApplication {
'(?:query/(?P<queryKey>[^/]+)/)?' => 'PhabricatorSpacesListController',
'create/' => 'PhabricatorSpacesEditController',
'edit/(?:(?P<id>\d+)/)?' => 'PhabricatorSpacesEditController',
'(?P<action>activate|archive)/(?P<id>\d+)/'
=> 'PhabricatorSpacesArchiveController',
),
);
}

View File

@@ -0,0 +1,76 @@
<?php
final class PhabricatorSpacesArchiveController
extends PhabricatorSpacesController {
public function handleRequest(AphrontRequest $request) {
$viewer = $request->getUser();
$space = id(new PhabricatorSpacesNamespaceQuery())
->setViewer($viewer)
->withIDs(array($request->getURIData('id')))
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->executeOne();
if (!$space) {
return new Aphront404Response();
}
$is_archive = ($request->getURIData('action') == 'archive');
$cancel_uri = '/'.$space->getMonogram();
if ($request->isFormPost()) {
$type_archive = PhabricatorSpacesNamespaceTransaction::TYPE_ARCHIVE;
$xactions = array();
$xactions[] = id(new PhabricatorSpacesNamespaceTransaction())
->setTransactionType($type_archive)
->setNewValue($is_archive ? 1 : 0);
$editor = id(new PhabricatorSpacesNamespaceEditor())
->setActor($viewer)
->setContinueOnNoEffect(true)
->setContinueOnMissingFields(true)
->setContentSourceFromRequest($request);
$editor->applyTransactions($space, $xactions);
return id(new AphrontRedirectResponse())->setURI($cancel_uri);
}
$body = array();
if ($is_archive) {
$title = pht('Archive Space: %s', $space->getNamespaceName());
$body[] = pht(
'If you archive this Space, you will no longer be able to create '.
'new objects inside it.');
$body[] = pht(
'Existing objects in this Space will be hidden from query results '.
'by default.');
$button = pht('Archive Space');
} else {
$title = pht('Activate Space: %s', $space->getNamespaceName());
$body[] = pht(
'If you activate this space, you will be able to create objects '.
'inside it again.');
$body[] = pht(
'Existing objects will no longer be hidden from query results.');
$button = pht('Activate Space');
}
$dialog = $this->newDialog()
->setTitle($title)
->addCancelButton($cancel_uri)
->addSubmitButton($button);
foreach ($body as $paragraph) {
$dialog->appendParagraph($paragraph);
}
return $dialog;
}
}

View File

@@ -37,6 +37,12 @@ final class PhabricatorSpacesViewController
->setHeader($space->getNamespaceName())
->setPolicyObject($space);
if ($space->getIsArchived()) {
$header->setStatus('fa-ban', 'red', pht('Archived'));
} else {
$header->setStatus('fa-check', 'bluegrey', pht('Active'));
}
$box = id(new PHUIObjectBoxView())
->setHeader($header)
->addPropertyList($property_list);
@@ -112,6 +118,26 @@ final class PhabricatorSpacesViewController
->setWorkflow(!$can_edit)
->setDisabled(!$can_edit));
$id = $space->getID();
if ($space->getIsArchived()) {
$list->addAction(
id(new PhabricatorActionView())
->setName(pht('Activate Space'))
->setIcon('fa-check')
->setHref($this->getApplicationURI("activate/{$id}/"))
->setDisabled(!$can_edit)
->setWorkflow(true));
} else {
$list->addAction(
id(new PhabricatorActionView())
->setName(pht('Archive Space'))
->setIcon('fa-ban')
->setHref($this->getApplicationURI("archive/{$id}/"))
->setDisabled(!$can_edit)
->setWorkflow(true));
}
return $list;
}

View File

@@ -17,6 +17,7 @@ final class PhabricatorSpacesNamespaceEditor
$types[] = PhabricatorSpacesNamespaceTransaction::TYPE_NAME;
$types[] = PhabricatorSpacesNamespaceTransaction::TYPE_DESCRIPTION;
$types[] = PhabricatorSpacesNamespaceTransaction::TYPE_DEFAULT;
$types[] = PhabricatorSpacesNamespaceTransaction::TYPE_ARCHIVE;
$types[] = PhabricatorTransactions::TYPE_VIEW_POLICY;
$types[] = PhabricatorTransactions::TYPE_EDIT_POLICY;
@@ -40,6 +41,8 @@ final class PhabricatorSpacesNamespaceEditor
return null;
}
return $object->getDescription();
case PhabricatorSpacesNamespaceTransaction::TYPE_ARCHIVE:
return $object->getIsArchived();
case PhabricatorSpacesNamespaceTransaction::TYPE_DEFAULT:
return $object->getIsDefaultNamespace() ? 1 : null;
case PhabricatorTransactions::TYPE_VIEW_POLICY:
@@ -61,6 +64,8 @@ final class PhabricatorSpacesNamespaceEditor
case PhabricatorTransactions::TYPE_VIEW_POLICY:
case PhabricatorTransactions::TYPE_EDIT_POLICY:
return $xaction->getNewValue();
case PhabricatorSpacesNamespaceTransaction::TYPE_ARCHIVE:
return $xaction->getNewValue() ? 1 : 0;
case PhabricatorSpacesNamespaceTransaction::TYPE_DEFAULT:
return $xaction->getNewValue() ? 1 : null;
}
@@ -84,6 +89,9 @@ final class PhabricatorSpacesNamespaceEditor
case PhabricatorSpacesNamespaceTransaction::TYPE_DEFAULT:
$object->setIsDefaultNamespace($new ? 1 : null);
return;
case PhabricatorSpacesNamespaceTransaction::TYPE_ARCHIVE:
$object->setIsArchived($new ? 1 : 0);
return;
case PhabricatorTransactions::TYPE_VIEW_POLICY:
$object->setViewPolicy($new);
return;
@@ -103,6 +111,7 @@ final class PhabricatorSpacesNamespaceEditor
case PhabricatorSpacesNamespaceTransaction::TYPE_NAME:
case PhabricatorSpacesNamespaceTransaction::TYPE_DESCRIPTION:
case PhabricatorSpacesNamespaceTransaction::TYPE_DEFAULT:
case PhabricatorSpacesNamespaceTransaction::TYPE_ARCHIVE:
case PhabricatorTransactions::TYPE_VIEW_POLICY:
case PhabricatorTransactions::TYPE_EDIT_POLICY:
return;
@@ -128,13 +137,27 @@ final class PhabricatorSpacesNamespaceEditor
$error = new PhabricatorApplicationTransactionValidationError(
$type,
pht('Required'),
pht('Spaces must have a name.'),
pht('Spaces must have a name.'),
nonempty(last($xactions), null));
$error->setIsMissingFieldError(true);
$errors[] = $error;
}
break;
case PhabricatorSpacesNamespaceTransaction::TYPE_DEFAULT:
if (!$this->getIsNewObject()) {
foreach ($xactions as $xaction) {
$errors[] = new PhabricatorApplicationTransactionValidationError(
$type,
pht('Invalid'),
pht(
'Only the first space created can be the default space, and '.
'it must remain the default space evermore.'),
$xaction);
}
}
break;
}
return $errors;

View File

@@ -39,6 +39,10 @@ final class PhabricatorSpacesNamespacePHIDType
$handle->setName($name);
$handle->setFullName(pht('%s %s', $monogram, $name));
$handle->setURI('/'.$monogram);
if ($namespace->getIsArchived()) {
$handle->setStatus(PhabricatorObjectHandle::STATUS_CLOSED);
}
}
}

View File

@@ -9,6 +9,7 @@ final class PhabricatorSpacesNamespaceQuery
private $ids;
private $phids;
private $isDefaultNamespace;
private $isArchived;
public function withIDs(array $ids) {
$this->ids = $ids;
@@ -25,38 +26,32 @@ final class PhabricatorSpacesNamespaceQuery
return $this;
}
public function withIsArchived($archived) {
$this->isArchived = $archived;
return $this;
}
public function getQueryApplicationClass() {
return 'PhabricatorSpacesApplication';
}
protected function loadPage() {
$table = new PhabricatorSpacesNamespace();
$conn_r = $table->establishConnection('r');
$rows = queryfx_all(
$conn_r,
'SELECT * FROM %T %Q %Q %Q',
$table->getTableName(),
$this->buildWhereClause($conn_r),
$this->buildOrderClause($conn_r),
$this->buildLimitClause($conn_r));
return $table->loadAllFromArray($rows);
return $this->loadStandardPage(new PhabricatorSpacesNamespace());
}
protected function buildWhereClause(AphrontDatabaseConnection $conn_r) {
$where = array();
protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
$where = parent::buildWhereClauseParts($conn);
if ($this->ids !== null) {
$where[] = qsprintf(
$conn_r,
$conn,
'id IN (%Ld)',
$this->ids);
}
if ($this->phids !== null) {
$where[] = qsprintf(
$conn_r,
$conn,
'phid IN (%Ls)',
$this->phids);
}
@@ -64,17 +59,23 @@ final class PhabricatorSpacesNamespaceQuery
if ($this->isDefaultNamespace !== null) {
if ($this->isDefaultNamespace) {
$where[] = qsprintf(
$conn_r,
$conn,
'isDefaultNamespace = 1');
} else {
$where[] = qsprintf(
$conn_r,
$conn,
'isDefaultNamespace IS NULL');
}
}
$where[] = $this->buildPagingClause($conn_r);
return $this->formatWhereClause($where);
if ($this->isArchived !== null) {
$where[] = qsprintf(
$conn,
'isArchived = %d',
(int)$this->isArchived);
}
return $where;
}
public static function destroySpacesCache() {
@@ -156,6 +157,21 @@ final class PhabricatorSpacesNamespaceQuery
return $result;
}
public static function getViewerActiveSpaces(PhabricatorUser $viewer) {
$spaces = self::getViewerSpaces($viewer);
foreach ($spaces as $key => $space) {
if ($space->getIsArchived()) {
unset($spaces[$key]);
}
}
return $spaces;
}
/**
* Get the Space PHID for an object, if one exists.
*

View File

@@ -11,28 +11,39 @@ final class PhabricatorSpacesNamespaceSearchEngine
return pht('Spaces');
}
public function buildSavedQueryFromRequest(AphrontRequest $request) {
$saved = new PhabricatorSavedQuery();
return $saved;
public function newQuery() {
return new PhabricatorSpacesNamespaceQuery();
}
public function buildQueryFromSavedQuery(PhabricatorSavedQuery $saved) {
$query = id(new PhabricatorSpacesNamespaceQuery());
public function buildCustomSearchFields() {
return array(
id(new PhabricatorSearchThreeStateField())
->setLabel(pht('Active'))
->setKey('active')
->setOptions(
pht('(Show All)'),
pht('Show Only Active Spaces'),
pht('Hide Active Spaces')),
);
}
public function buildQueryFromParameters(array $map) {
$query = $this->newQuery();
if ($map['active']) {
$query->withIsArchived(!$map['active']);
}
return $query;
}
public function buildSearchForm(
AphrontFormView $form,
PhabricatorSavedQuery $saved_query) {}
protected function getURI($path) {
return '/spaces/'.$path;
}
protected function getBuiltinQueryNames() {
$names = array(
'active' => pht('Active Spaces'),
'all' => pht('All Spaces'),
);
@@ -40,11 +51,12 @@ final class PhabricatorSpacesNamespaceSearchEngine
}
public function buildSavedQueryFromBuiltin($query_key) {
$query = $this->newSavedQuery();
$query->setQueryKey($query_key);
switch ($query_key) {
case 'active':
return $query->setParameter('active', true);
case 'all':
return $query;
}
@@ -72,6 +84,10 @@ final class PhabricatorSpacesNamespaceSearchEngine
$item->addIcon('fa-certificate', pht('Default Space'));
}
if ($space->getIsArchived()) {
$item->setDisabled(true);
}
$list->addItem($item);
}

View File

@@ -12,6 +12,7 @@ final class PhabricatorSpacesNamespace
protected $editPolicy;
protected $isDefaultNamespace;
protected $description;
protected $isArchived;
public static function initializeNewNamespace(PhabricatorUser $actor) {
$app = id(new PhabricatorApplicationQuery())
@@ -28,7 +29,8 @@ final class PhabricatorSpacesNamespace
->setIsDefaultNamespace(null)
->setViewPolicy($view_policy)
->setEditPolicy($edit_policy)
->setDescription('');
->setDescription('')
->setIsArchived(0);
}
protected function getConfiguration() {
@@ -38,6 +40,7 @@ final class PhabricatorSpacesNamespace
'namespaceName' => 'text255',
'isDefaultNamespace' => 'bool?',
'description' => 'text',
'isArchived' => 'bool',
),
self::CONFIG_KEY_SCHEMA => array(
'key_default' => array(

View File

@@ -6,6 +6,7 @@ final class PhabricatorSpacesNamespaceTransaction
const TYPE_NAME = 'spaces:name';
const TYPE_DEFAULT = 'spaces:default';
const TYPE_DESCRIPTION = 'spaces:description';
const TYPE_ARCHIVE = 'spaces:archive';
public function getApplicationName() {
return 'spaces';
@@ -78,6 +79,16 @@ final class PhabricatorSpacesNamespaceTransaction
return pht(
'%s made this the default space.',
$this->renderHandleLink($author_phid));
case self::TYPE_ARCHIVE:
if ($new) {
return pht(
'%s archived this space.',
$this->renderHandleLink($author_phid));
} else {
return pht(
'%s activated this space.',
$this->renderHandleLink($author_phid));
}
}
return parent::getTitle();

View File

@@ -21,9 +21,20 @@ final class PhabricatorSpacesNamespaceDatasource
$spaces = $this->executeQuery($query);
$results = array();
foreach ($spaces as $space) {
$results[] = id(new PhabricatorTypeaheadResult())
->setName($space->getNamespaceName())
$full_name = pht(
'%s %s',
$space->getMonogram(),
$space->getNamespaceName());
$result = id(new PhabricatorTypeaheadResult())
->setName($full_name)
->setPHID($space->getPHID());
if ($space->getIsArchived()) {
$result->setClosed(pht('Archived'));
}
$results[] = $result;
}
return $this->filterResultsAgainstTokens($results);