diff --git a/resources/celerity/map.php b/resources/celerity/map.php --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -7,7 +7,7 @@ */ return array( 'names' => array( - 'core.pkg.css' => 'e2460e8f', + 'core.pkg.css' => 'd7ecac6d', 'core.pkg.js' => '3bbe23c6', 'darkconsole.pkg.js' => 'e7393ebb', 'differential.pkg.css' => '02273347', @@ -136,7 +136,7 @@ 'rsrc/css/phui/phui-fontkit.css' => 'dd8ddf27', 'rsrc/css/phui/phui-form-view.css' => '808329f2', 'rsrc/css/phui/phui-form.css' => '25876baf', - 'rsrc/css/phui/phui-header-view.css' => '75aaf372', + 'rsrc/css/phui/phui-header-view.css' => '2dd74fe0', 'rsrc/css/phui/phui-icon.css' => 'bc766998', 'rsrc/css/phui/phui-image-mask.css' => '5a8b09c8', 'rsrc/css/phui/phui-info-panel.css' => '27ea50a1', @@ -779,7 +779,7 @@ 'phui-fontkit-css' => 'dd8ddf27', 'phui-form-css' => '25876baf', 'phui-form-view-css' => '808329f2', - 'phui-header-view-css' => '75aaf372', + 'phui-header-view-css' => '2dd74fe0', 'phui-icon-view-css' => 'bc766998', 'phui-image-mask-css' => '5a8b09c8', 'phui-info-panel-css' => '27ea50a1', diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -2560,6 +2560,7 @@ 'PhabricatorSpacesCapabilityCreateSpaces' => 'applications/spaces/capability/PhabricatorSpacesCapabilityCreateSpaces.php', 'PhabricatorSpacesCapabilityDefaultEdit' => 'applications/spaces/capability/PhabricatorSpacesCapabilityDefaultEdit.php', 'PhabricatorSpacesCapabilityDefaultView' => 'applications/spaces/capability/PhabricatorSpacesCapabilityDefaultView.php', + 'PhabricatorSpacesControl' => 'applications/spaces/view/PhabricatorSpacesControl.php', 'PhabricatorSpacesController' => 'applications/spaces/controller/PhabricatorSpacesController.php', 'PhabricatorSpacesDAO' => 'applications/spaces/storage/PhabricatorSpacesDAO.php', 'PhabricatorSpacesEditController' => 'applications/spaces/controller/PhabricatorSpacesEditController.php', @@ -6041,6 +6042,7 @@ 'PhabricatorSpacesCapabilityCreateSpaces' => 'PhabricatorPolicyCapability', 'PhabricatorSpacesCapabilityDefaultEdit' => 'PhabricatorPolicyCapability', 'PhabricatorSpacesCapabilityDefaultView' => 'PhabricatorPolicyCapability', + 'PhabricatorSpacesControl' => 'AphrontFormControl', 'PhabricatorSpacesController' => 'PhabricatorController', 'PhabricatorSpacesDAO' => 'PhabricatorLiskDAO', 'PhabricatorSpacesEditController' => 'PhabricatorSpacesController', diff --git a/src/applications/paste/controller/PhabricatorPasteEditController.php b/src/applications/paste/controller/PhabricatorPasteEditController.php --- a/src/applications/paste/controller/PhabricatorPasteEditController.php +++ b/src/applications/paste/controller/PhabricatorPasteEditController.php @@ -57,13 +57,12 @@ } } - $text = null; - $e_text = true; - $errors = array(); + $v_space = $paste->getSpacePHID(); if ($is_create && $parent) { $v_title = pht('Fork of %s', $parent->getFullName()); $v_language = $parent->getLanguage(); $v_text = $parent->getRawContent(); + $v_space = $parent->getSpacePHID(); } else { $v_title = $paste->getTitle(); $v_language = $paste->getLanguage(); @@ -81,68 +80,64 @@ $v_projects = array_reverse($v_projects); } + $validation_exception = null; if ($request->isFormPost()) { $xactions = array(); $v_text = $request->getStr('text'); - if (!strlen($v_text)) { - $e_text = pht('Required'); - $errors[] = pht('The paste may not be blank.'); - } else { - $e_text = null; - } - $v_title = $request->getStr('title'); $v_language = $request->getStr('language'); $v_view_policy = $request->getStr('can_view'); $v_edit_policy = $request->getStr('can_edit'); $v_projects = $request->getArr('projects'); + $v_space = $request->getStr('spacePHID'); // NOTE: The author is the only editor and can always view the paste, // so it's impossible for them to choose an invalid policy. - if (!$errors) { - if ($is_create || ($v_text !== $paste->getRawContent())) { - $file = PhabricatorPasteEditor::initializeFileForPaste( - $user, - $v_title, - $v_text); - - $xactions[] = id(new PhabricatorPasteTransaction()) - ->setTransactionType(PhabricatorPasteTransaction::TYPE_CONTENT) - ->setNewValue($file->getPHID()); - } + if ($is_create || ($v_text !== $paste->getRawContent())) { + $file = PhabricatorPasteEditor::initializeFileForPaste( + $user, + $v_title, + $v_text); $xactions[] = id(new PhabricatorPasteTransaction()) - ->setTransactionType(PhabricatorPasteTransaction::TYPE_TITLE) - ->setNewValue($v_title); - $xactions[] = id(new PhabricatorPasteTransaction()) - ->setTransactionType(PhabricatorPasteTransaction::TYPE_LANGUAGE) - ->setNewValue($v_language); - $xactions[] = id(new PhabricatorPasteTransaction()) - ->setTransactionType(PhabricatorTransactions::TYPE_VIEW_POLICY) - ->setNewValue($v_view_policy); - $xactions[] = id(new PhabricatorPasteTransaction()) - ->setTransactionType(PhabricatorTransactions::TYPE_EDIT_POLICY) - ->setNewValue($v_edit_policy); + ->setTransactionType(PhabricatorPasteTransaction::TYPE_CONTENT) + ->setNewValue($file->getPHID()); + } - $proj_edge_type = PhabricatorProjectObjectHasProjectEdgeType::EDGECONST; - $xactions[] = id(new PhabricatorPasteTransaction()) - ->setTransactionType(PhabricatorTransactions::TYPE_EDGE) - ->setMetadataValue('edge:type', $proj_edge_type) - ->setNewValue(array('=' => array_fuse($v_projects))); - - $editor = id(new PhabricatorPasteEditor()) - ->setActor($user) - ->setContentSourceFromRequest($request) - ->setContinueOnNoEffect(true); + $xactions[] = id(new PhabricatorPasteTransaction()) + ->setTransactionType(PhabricatorPasteTransaction::TYPE_TITLE) + ->setNewValue($v_title); + $xactions[] = id(new PhabricatorPasteTransaction()) + ->setTransactionType(PhabricatorPasteTransaction::TYPE_LANGUAGE) + ->setNewValue($v_language); + $xactions[] = id(new PhabricatorPasteTransaction()) + ->setTransactionType(PhabricatorTransactions::TYPE_VIEW_POLICY) + ->setNewValue($v_view_policy); + $xactions[] = id(new PhabricatorPasteTransaction()) + ->setTransactionType(PhabricatorTransactions::TYPE_EDIT_POLICY) + ->setNewValue($v_edit_policy); + $xactions[] = id(new PhabricatorPasteTransaction()) + ->setTransactionType(PhabricatorTransactions::TYPE_SPACE) + ->setNewValue($v_space); + + $proj_edge_type = PhabricatorProjectObjectHasProjectEdgeType::EDGECONST; + $xactions[] = id(new PhabricatorPasteTransaction()) + ->setTransactionType(PhabricatorTransactions::TYPE_EDGE) + ->setMetadataValue('edge:type', $proj_edge_type) + ->setNewValue(array('=' => array_fuse($v_projects))); + + $editor = id(new PhabricatorPasteEditor()) + ->setActor($user) + ->setContentSourceFromRequest($request) + ->setContinueOnNoEffect(true); + + try { $xactions = $editor->applyTransactions($paste, $xactions); return id(new AphrontRedirectResponse())->setURI($paste->getURI()); - } else { - // make sure we update policy so its correctly populated to what - // the user chose - $paste->setViewPolicy($v_view_policy); - $paste->setEditPolicy($v_edit_policy); + } catch (PhabricatorApplicationTransactionValidationException $ex) { + $validation_exception = $ex; } } @@ -172,12 +167,19 @@ ->setObject($paste) ->execute(); + $form->appendControl( + id(new PhabricatorSpacesControl()) + ->setObject($paste) + ->setValue($v_space) + ->setName('spacePHID')); + $form->appendChild( id(new AphrontFormPolicyControl()) ->setUser($user) ->setCapability(PhabricatorPolicyCapability::CAN_VIEW) ->setPolicyObject($paste) ->setPolicies($policies) + ->setValue($v_view_policy) ->setName('can_view')); $form->appendChild( @@ -186,6 +188,7 @@ ->setCapability(PhabricatorPolicyCapability::CAN_EDIT) ->setPolicyObject($paste) ->setPolicies($policies) + ->setValue($v_edit_policy) ->setName('can_edit')); $form->appendControl( @@ -199,7 +202,6 @@ ->appendChild( id(new AphrontFormTextAreaControl()) ->setLabel(pht('Text')) - ->setError($e_text) ->setValue($v_text) ->setHeight(AphrontFormTextAreaControl::HEIGHT_VERY_TALL) ->setCustomClass('PhabricatorMonospaced') @@ -222,9 +224,12 @@ $form_box = id(new PHUIObjectBoxView()) ->setHeaderText($title) - ->setFormErrors($errors) ->setForm($form); + if ($validation_exception) { + $form_box->setValidationException($validation_exception); + } + $crumbs = $this->buildApplicationCrumbs($this->buildSideNavView()); if (!$is_create) { $crumbs->addTextCrumb('P'.$paste->getID(), '/P'.$paste->getID()); diff --git a/src/applications/paste/storage/PhabricatorPaste.php b/src/applications/paste/storage/PhabricatorPaste.php --- a/src/applications/paste/storage/PhabricatorPaste.php +++ b/src/applications/paste/storage/PhabricatorPaste.php @@ -42,7 +42,11 @@ } public function getURI() { - return '/P'.$this->getID(); + return '/'.$this->getMonogram(); + } + + public function getMonogram() { + return 'P'.$this->getID(); } protected function getConfiguration() { diff --git a/src/applications/people/storage/PhabricatorUser.php b/src/applications/people/storage/PhabricatorUser.php --- a/src/applications/people/storage/PhabricatorUser.php +++ b/src/applications/people/storage/PhabricatorUser.php @@ -751,6 +751,20 @@ $email->getUserPHID()); } + public function getDefaultSpacePHID() { + // TODO: We might let the user switch which space they're "in" later on; + // for now just use the global space if one exists. + + $spaces = PhabricatorSpacesNamespaceQuery::getViewerSpaces($this); + foreach ($spaces as $space) { + if ($space->getIsDefaultNamespace()) { + return $space->getPHID(); + } + } + + return null; + } + /** * Grant a user a source of authority, to let them bypass policy checks they diff --git a/src/applications/spaces/application/PhabricatorSpacesApplication.php b/src/applications/spaces/application/PhabricatorSpacesApplication.php --- a/src/applications/spaces/application/PhabricatorSpacesApplication.php +++ b/src/applications/spaces/application/PhabricatorSpacesApplication.php @@ -15,7 +15,7 @@ } public function getFontIcon() { - return 'fa-compass'; + return 'fa-th-large'; } public function getTitleGlyph() { diff --git a/src/applications/spaces/phid/PhabricatorSpacesNamespacePHIDType.php b/src/applications/spaces/phid/PhabricatorSpacesNamespacePHIDType.php --- a/src/applications/spaces/phid/PhabricatorSpacesNamespacePHIDType.php +++ b/src/applications/spaces/phid/PhabricatorSpacesNamespacePHIDType.php @@ -28,7 +28,10 @@ foreach ($handles as $phid => $handle) { $namespace = $objects[$phid]; + $monogram = $namespace->getMonogram(); + $handle->setName($namespace->getNamespaceName()); + $handle->setURI('/'.$monogram); } } diff --git a/src/applications/spaces/view/PhabricatorSpacesControl.php b/src/applications/spaces/view/PhabricatorSpacesControl.php new file mode 100644 --- /dev/null +++ b/src/applications/spaces/view/PhabricatorSpacesControl.php @@ -0,0 +1,49 @@ +object = $object; + return $this; + } + + protected function getCustomControlClass() { + return ''; + } + + protected function getOptions() { + $viewer = $this->getUser(); + $viewer_spaces = PhabricatorSpacesNamespaceQuery::getViewerSpaces($viewer); + + $map = mpull($viewer_spaces, 'getNamespaceName', 'getPHID'); + asort($map); + + return $map; + } + + public function renderInput() { + $viewer = $this->getUser(); + + $this->setLabel(pht('Space')); + + $value = $this->getValue(); + if ($value === null) { + $value = $viewer->getDefaultSpacePHID(); + } + + return AphrontFormSelectControl::renderSelectTag( + $value, + $this->getOptions(), + array( + 'name' => $this->getName(), + )); + } + +} diff --git a/src/applications/transactions/constants/PhabricatorTransactions.php b/src/applications/transactions/constants/PhabricatorTransactions.php --- a/src/applications/transactions/constants/PhabricatorTransactions.php +++ b/src/applications/transactions/constants/PhabricatorTransactions.php @@ -12,6 +12,7 @@ const TYPE_BUILDABLE = 'harbormaster:buildable'; const TYPE_TOKEN = 'token:give'; const TYPE_INLINESTATE = 'core:inlinestate'; + const TYPE_SPACE = 'core:space'; const COLOR_RED = 'red'; const COLOR_ORANGE = 'orange'; diff --git a/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php b/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php --- a/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php +++ b/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php @@ -264,6 +264,10 @@ $types[] = PhabricatorTransactions::TYPE_EDGE; } + if ($this->object instanceof PhabricatorSpacesInterface) { + $types[] = PhabricatorTransactions::TYPE_SPACE; + } + return $types; } @@ -292,6 +296,21 @@ return $object->getEditPolicy(); case PhabricatorTransactions::TYPE_JOIN_POLICY: return $object->getJoinPolicy(); + case PhabricatorTransactions::TYPE_SPACE: + $space_phid = $object->getSpacePHID(); + if ($space_phid === null) { + if ($this->getIsNewObject()) { + // In this case, just return `null` so we know this is the initial + // transaction and it should be hidden. + return null; + } + + $default_space = PhabricatorSpacesNamespaceQuery::getDefaultSpace(); + if ($default_space) { + $space_phid = $default_space->getPHID(); + } + } + return $space_phid; case PhabricatorTransactions::TYPE_EDGE: $edge_type = $xaction->getMetadataValue('edge:type'); if (!$edge_type) { @@ -337,7 +356,16 @@ case PhabricatorTransactions::TYPE_BUILDABLE: case PhabricatorTransactions::TYPE_TOKEN: case PhabricatorTransactions::TYPE_INLINESTATE: - return $xaction->getNewValue(); + case PhabricatorTransactions::TYPE_SPACE: + $space_phid = $xaction->getNewValue(); + if (!strlen($space_phid)) { + // If an install has no Spaces, we might end up with the empty string + // here instead of a strict `null`. Just make this work like callers + // might reasonably expect. + return null; + } else { + return $space_phid; + } case PhabricatorTransactions::TYPE_EDGE: return $this->getEdgeTransactionNewValue($xaction); case PhabricatorTransactions::TYPE_CUSTOMFIELD: @@ -437,6 +465,7 @@ case PhabricatorTransactions::TYPE_SUBSCRIBERS: case PhabricatorTransactions::TYPE_INLINESTATE: case PhabricatorTransactions::TYPE_EDGE: + case PhabricatorTransactions::TYPE_SPACE: case PhabricatorTransactions::TYPE_COMMENT: return $this->applyBuiltinInternalTransaction($object, $xaction); } @@ -485,6 +514,7 @@ case PhabricatorTransactions::TYPE_EDIT_POLICY: case PhabricatorTransactions::TYPE_JOIN_POLICY: case PhabricatorTransactions::TYPE_INLINESTATE: + case PhabricatorTransactions::TYPE_SPACE: case PhabricatorTransactions::TYPE_COMMENT: return $this->applyBuiltinExternalTransaction($object, $xaction); } @@ -537,6 +567,9 @@ case PhabricatorTransactions::TYPE_JOIN_POLICY: $object->setJoinPolicy($xaction->getNewValue()); break; + case PhabricatorTransactions::TYPE_SPACE: + $object->setSpacePHID($xaction->getNewValue()); + break; } } @@ -1190,18 +1223,9 @@ PhabricatorPolicyCapability::CAN_VIEW); break; case PhabricatorTransactions::TYPE_VIEW_POLICY: - PhabricatorPolicyFilter::requireCapability( - $actor, - $object, - PhabricatorPolicyCapability::CAN_EDIT); - break; case PhabricatorTransactions::TYPE_EDIT_POLICY: - PhabricatorPolicyFilter::requireCapability( - $actor, - $object, - PhabricatorPolicyCapability::CAN_EDIT); - break; case PhabricatorTransactions::TYPE_JOIN_POLICY: + case PhabricatorTransactions::TYPE_SPACE: PhabricatorPolicyFilter::requireCapability( $actor, $object, @@ -1882,6 +1906,12 @@ $type, PhabricatorPolicyCapability::CAN_EDIT); break; + case PhabricatorTransactions::TYPE_SPACE: + $errors[] = $this->validateSpaceTransactions( + $object, + $xactions, + $type); + break; case PhabricatorTransactions::TYPE_CUSTOMFIELD: $groups = array(); foreach ($xactions as $xaction) { @@ -1968,6 +1998,52 @@ return $errors; } + + private function validateSpaceTransactions( + PhabricatorLiskDAO $object, + array $xactions, + $transaction_type) { + $errors = array(); + + $all_spaces = PhabricatorSpacesNamespaceQuery::getAllSpaces(); + $viewer_spaces = PhabricatorSpacesNamespaceQuery::getViewerSpaces( + $this->getActor()); + foreach ($xactions as $xaction) { + $space_phid = $xaction->getNewValue(); + + if ($space_phid === null) { + if (!$all_spaces) { + // The install doesn't have any spaces, so this is fine. + continue; + } + + // The install has some spaces, so every object needs to be put + // in a valid space. + $errors[] = new PhabricatorApplicationTransactionValidationError( + $transaction_type, + pht('Invalid'), + pht('You must choose a space for this object.'), + $xaction); + continue; + } + + // If the PHID isn't `null`, it needs to be a valid space that the + // viewer can see. + if (empty($viewer_spaces[$space_phid])) { + $errors[] = new PhabricatorApplicationTransactionValidationError( + $transaction_type, + pht('Invalid'), + pht( + 'You can not shift this object in the selected space, because '. + 'the space does not exist or you do not have access to it.'), + $xaction); + } + } + + return $errors; + } + + protected function adjustObjectForPolicyChecks( PhabricatorLiskDAO $object, array $xactions) { diff --git a/src/applications/transactions/storage/PhabricatorApplicationTransaction.php b/src/applications/transactions/storage/PhabricatorApplicationTransaction.php --- a/src/applications/transactions/storage/PhabricatorApplicationTransaction.php +++ b/src/applications/transactions/storage/PhabricatorApplicationTransaction.php @@ -250,6 +250,14 @@ $phids[] = array($new); } break; + case PhabricatorTransactions::TYPE_SPACE: + if ($old) { + $phids[] = array($old); + } + if ($new) { + $phids[] = array($new); + } + break; case PhabricatorTransactions::TYPE_TOKEN: break; case PhabricatorTransactions::TYPE_BUILDABLE: @@ -369,6 +377,8 @@ return 'fa-wrench'; case PhabricatorTransactions::TYPE_TOKEN: return 'fa-trophy'; + case PhabricatorTransactions::TYPE_SPACE: + return 'fa-th-large'; } return 'fa-pencil'; @@ -438,6 +448,7 @@ case PhabricatorTransactions::TYPE_VIEW_POLICY: case PhabricatorTransactions::TYPE_EDIT_POLICY: case PhabricatorTransactions::TYPE_JOIN_POLICY: + case PhabricatorTransactions::TYPE_SPACE: if ($this->getOldValue() === null) { return true; } else { @@ -597,6 +608,8 @@ return pht( 'All users are already subscribed to this %s.', $this->getApplicationObjectTypeName()); + case PhabricatorTransactions::TYPE_SPACE: + return pht('This object is already in that space.'); case PhabricatorTransactions::TYPE_EDGE: return pht('Edges already exist; transaction has no effect.'); } @@ -636,6 +649,12 @@ $this->getApplicationObjectTypeName(), $this->renderPolicyName($old, 'old'), $this->renderPolicyName($new, 'new')); + case PhabricatorTransactions::TYPE_SPACE: + return pht( + '%s shifted this object from the %s space to the %s space.', + $this->renderHandleLink($author_phid), + $this->renderHandleLink($old), + $this->renderHandleLink($new)); case PhabricatorTransactions::TYPE_SUBSCRIBERS: $add = array_diff($new, $old); $rem = array_diff($old, $new); @@ -821,6 +840,13 @@ '%s updated subscribers of %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); + case PhabricatorTransactions::TYPE_SPACE: + return pht( + '%s shifted %s from the %s space to the %s space.', + $this->renderHandleLink($author_phid), + $this->renderHandleLink($object_phid), + $this->renderHandleLink($old), + $this->renderHandleLink($new)); case PhabricatorTransactions::TYPE_EDGE: $new = ipull($new, 'dst'); $old = ipull($old, 'dst'); diff --git a/src/view/phui/PHUIHeaderView.php b/src/view/phui/PHUIHeaderView.php --- a/src/view/phui/PHUIHeaderView.php +++ b/src/view/phui/PHUIHeaderView.php @@ -174,6 +174,20 @@ $header = array(); + $header[] = $this->renderObjectSpaceInformation(); + + if ($this->objectName) { + $header[] = array( + phutil_tag( + 'a', + array( + 'href' => '/'.$this->objectName, + ), + $this->objectName), + ' ', + ); + } + if ($this->actionLinks) { $actions = array(); foreach ($this->actionLinks as $button) { @@ -200,18 +214,6 @@ } $header[] = $this->header; - if ($this->objectName) { - array_unshift( - $header, - phutil_tag( - 'a', - array( - 'href' => '/'.$this->objectName, - ), - $this->objectName), - ' '); - } - if ($this->tags) { $header[] = ' '; $header[] = phutil_tag( @@ -268,9 +270,9 @@ } private function renderPolicyProperty(PhabricatorPolicyInterface $object) { - $policies = PhabricatorPolicyQuery::loadPolicies( - $this->getUser(), - $object); + $viewer = $this->getUser(); + + $policies = PhabricatorPolicyQuery::loadPolicies($viewer, $object); $view_capability = PhabricatorPolicyCapability::CAN_VIEW; $policy = idx($policies, $view_capability); @@ -294,4 +296,41 @@ return array($icon, $link); } + + private function renderObjectSpaceInformation() { + $viewer = $this->getUser(); + + $object = $this->policyObject; + if (!$object) { + return; + } + + if (!($object instanceof PhabricatorSpacesInterface)) { + return; + } + + $space_phid = $object->getSpacePHID(); + if ($space_phid === null) { + $default_space = PhabricatorSpacesNamespaceQuery::getDefaultSpace(); + if ($default_space) { + $space_phid = $default_space->getPHID(); + } + } + + if ($space_phid === null) { + return; + } + + return phutil_tag( + 'span', + array( + 'class' => 'spaces-name', + ), + array( + $viewer->renderHandle($space_phid), + ' | ', + )); + } + + } diff --git a/webroot/rsrc/css/phui/phui-header-view.css b/webroot/rsrc/css/phui/phui-header-view.css --- a/webroot/rsrc/css/phui/phui-header-view.css +++ b/webroot/rsrc/css/phui/phui-header-view.css @@ -140,3 +140,11 @@ .device .phui-header-action-links .phui-mobile-menu { display: inline-block; } + +.spaces-name { + color: {$lightbluetext}; +} + +.spaces-name .phui-handle { + color: #000; +}