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:
@@ -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',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user