diff --git a/resources/sql/autopatches/20160113.propanel.1.storage.sql b/resources/sql/autopatches/20160113.propanel.1.storage.sql new file mode 100644 index 0000000000..3a322daaca --- /dev/null +++ b/resources/sql/autopatches/20160113.propanel.1.storage.sql @@ -0,0 +1,13 @@ +CREATE TABLE {$NAMESPACE}_search.search_profilepanelconfiguration ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + phid VARBINARY(64) NOT NULL, + profilePHID VARBINARY(64) NOT NULL, + panelKey VARCHAR(64) NOT NULL COLLATE {$COLLATE_TEXT}, + builtinKey VARCHAR(64) COLLATE {$COLLATE_TEXT}, + panelOrder INT UNSIGNED, + visibility VARCHAR(32) NOT NULL COLLATE {$COLLATE_TEXT}, + panelProperties LONGTEXT NOT NULL COLLATE {$COLLATE_TEXT}, + dateCreated INT UNSIGNED NOT NULL, + dateModified INT UNSIGNED NOT NULL, + KEY `key_profile` (profilePHID, panelOrder) +) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT}; diff --git a/resources/sql/autopatches/20160113.propanel.2.xaction.sql b/resources/sql/autopatches/20160113.propanel.2.xaction.sql new file mode 100644 index 0000000000..cbc78808ef --- /dev/null +++ b/resources/sql/autopatches/20160113.propanel.2.xaction.sql @@ -0,0 +1,19 @@ +CREATE TABLE {$NAMESPACE}_search.search_profilepanelconfigurationtransaction ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + phid VARBINARY(64) NOT NULL, + authorPHID VARBINARY(64) NOT NULL, + objectPHID VARBINARY(64) NOT NULL, + viewPolicy VARBINARY(64) NOT NULL, + editPolicy VARBINARY(64) NOT NULL, + commentPHID VARBINARY(64) DEFAULT NULL, + commentVersion INT UNSIGNED NOT NULL, + transactionType VARCHAR(32) COLLATE {$COLLATE_TEXT} NOT NULL, + oldValue LONGTEXT COLLATE {$COLLATE_TEXT} NOT NULL, + newValue LONGTEXT COLLATE {$COLLATE_TEXT} NOT NULL, + contentSource LONGTEXT COLLATE {$COLLATE_TEXT} NOT NULL, + metadata LONGTEXT COLLATE {$COLLATE_TEXT} NOT NULL, + dateCreated INT UNSIGNED NOT NULL, + dateModified INT UNSIGNED NOT NULL, + UNIQUE KEY `key_phid` (`phid`), + KEY `key_object` (`objectPHID`) +) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT}; diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 9dd1e917c7..beaa00bcd6 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -2831,7 +2831,10 @@ phutil_register_library_map(array( 'PhabricatorProfilePanelConfiguration' => 'applications/search/storage/PhabricatorProfilePanelConfiguration.php', 'PhabricatorProfilePanelConfigurationQuery' => 'applications/search/query/PhabricatorProfilePanelConfigurationQuery.php', 'PhabricatorProfilePanelConfigurationTransaction' => 'applications/search/storage/PhabricatorProfilePanelConfigurationTransaction.php', + 'PhabricatorProfilePanelEditEngine' => 'applications/search/editor/PhabricatorProfilePanelEditEngine.php', + 'PhabricatorProfilePanelEditor' => 'applications/search/editor/PhabricatorProfilePanelEditor.php', 'PhabricatorProfilePanelEngine' => 'applications/search/engine/PhabricatorProfilePanelEngine.php', + 'PhabricatorProfilePanelIconSet' => 'applications/search/profilepanel/PhabricatorProfilePanelIconSet.php', 'PhabricatorProfilePanelInterface' => 'applications/search/interface/PhabricatorProfilePanelInterface.php', 'PhabricatorProfilePanelPHIDType' => 'applications/search/phidtype/PhabricatorProfilePanelPHIDType.php', 'PhabricatorProject' => 'applications/project/storage/PhabricatorProject.php', @@ -2896,6 +2899,7 @@ phutil_register_library_map(array( 'PhabricatorProjectOrUserDatasource' => 'applications/project/typeahead/PhabricatorProjectOrUserDatasource.php', 'PhabricatorProjectOrUserFunctionDatasource' => 'applications/project/typeahead/PhabricatorProjectOrUserFunctionDatasource.php', 'PhabricatorProjectPHIDResolver' => 'applications/phid/resolver/PhabricatorProjectPHIDResolver.php', + 'PhabricatorProjectPanelController' => 'applications/project/controller/PhabricatorProjectPanelController.php', 'PhabricatorProjectProfileController' => 'applications/project/controller/PhabricatorProjectProfileController.php', 'PhabricatorProjectProjectHasMemberEdgeType' => 'applications/project/edge/PhabricatorProjectProjectHasMemberEdgeType.php', 'PhabricatorProjectProjectHasObjectEdgeType' => 'applications/project/edge/PhabricatorProjectProjectHasObjectEdgeType.php', @@ -7188,10 +7192,14 @@ phutil_register_library_map(array( 'PhabricatorSearchDAO', 'PhabricatorPolicyInterface', 'PhabricatorExtendedPolicyInterface', + 'PhabricatorApplicationTransactionInterface', ), 'PhabricatorProfilePanelConfigurationQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorProfilePanelConfigurationTransaction' => 'PhabricatorApplicationTransaction', + 'PhabricatorProfilePanelEditEngine' => 'PhabricatorEditEngine', + 'PhabricatorProfilePanelEditor' => 'PhabricatorApplicationTransactionEditor', 'PhabricatorProfilePanelEngine' => 'Phobject', + 'PhabricatorProfilePanelIconSet' => 'PhabricatorIconSet', 'PhabricatorProfilePanelPHIDType' => 'PhabricatorPHIDType', 'PhabricatorProject' => array( 'PhabricatorProjectDAO', @@ -7277,6 +7285,7 @@ phutil_register_library_map(array( 'PhabricatorProjectOrUserDatasource' => 'PhabricatorTypeaheadCompositeDatasource', 'PhabricatorProjectOrUserFunctionDatasource' => 'PhabricatorTypeaheadCompositeDatasource', 'PhabricatorProjectPHIDResolver' => 'PhabricatorPHIDResolver', + 'PhabricatorProjectPanelController' => 'PhabricatorProjectController', 'PhabricatorProjectProfileController' => 'PhabricatorProjectController', 'PhabricatorProjectProjectHasMemberEdgeType' => 'PhabricatorEdgeType', 'PhabricatorProjectProjectHasObjectEdgeType' => 'PhabricatorEdgeType', diff --git a/src/applications/base/PhabricatorApplication.php b/src/applications/base/PhabricatorApplication.php index 0a6a2b4cb5..2e94c18f7e 100644 --- a/src/applications/base/PhabricatorApplication.php +++ b/src/applications/base/PhabricatorApplication.php @@ -635,4 +635,18 @@ abstract class PhabricatorApplication return $base.'(?:query/(?P[^/]+)/)?'; } + protected function getPanelRouting($controller) { + $edit_route = $this->getEditRoutePattern(); + + return array( + '(?Pview)/(?P[^/]+)/' => $controller, + '(?Phide)/(?P[^/]+)/' => $controller, + '(?Pconfigure)/' => $controller, + '(?Pedit)/'.$edit_route => $controller, + '(?Pnew)/(?[^/]+)/'.$edit_route => $controller, + '(?Pbuiltin)/(?[^/]+)/'.$edit_route + => $controller, + ); + } + } diff --git a/src/applications/project/application/PhabricatorProjectApplication.php b/src/applications/project/application/PhabricatorProjectApplication.php index 954c46c170..e3732f2b31 100644 --- a/src/applications/project/application/PhabricatorProjectApplication.php +++ b/src/applications/project/application/PhabricatorProjectApplication.php @@ -61,6 +61,8 @@ final class PhabricatorProjectApplication extends PhabricatorApplication { => 'PhabricatorProjectEditPictureController', $this->getEditRoutePattern('edit/') => 'PhabricatorProjectEditController', + '(?P[1-9]\d*)/panel/' + => $this->getPanelRouting('PhabricatorProjectPanelController'), 'subprojects/(?P[1-9]\d*)/' => 'PhabricatorProjectSubprojectsController', 'milestones/(?P[1-9]\d*)/' diff --git a/src/applications/project/controller/PhabricatorProjectController.php b/src/applications/project/controller/PhabricatorProjectController.php index 0729010480..9c6fa2f4e2 100644 --- a/src/applications/project/controller/PhabricatorProjectController.php +++ b/src/applications/project/controller/PhabricatorProjectController.php @@ -18,7 +18,10 @@ abstract class PhabricatorProjectController extends PhabricatorController { $viewer = $this->getViewer(); $request = $this->getRequest(); - $id = $request->getURIData('id'); + $id = nonempty( + $request->getURIData('projectID'), + $request->getURIData('id')); + $slug = $request->getURIData('slug'); if ($slug) { diff --git a/src/applications/project/controller/PhabricatorProjectPanelController.php b/src/applications/project/controller/PhabricatorProjectPanelController.php new file mode 100644 index 0000000000..02b16dcfb4 --- /dev/null +++ b/src/applications/project/controller/PhabricatorProjectPanelController.php @@ -0,0 +1,21 @@ +loadProject(); + if ($response) { + return $response; + } + + $viewer = $this->getViewer(); + $project = $this->getProject(); + + return id(new PhabricatorProfilePanelEngine()) + ->setProfileObject($project) + ->setController($this) + ->buildResponse(); + } + +} diff --git a/src/applications/project/profilepanel/PhabricatorProjectDetailsProfilePanel.php b/src/applications/project/profilepanel/PhabricatorProjectDetailsProfilePanel.php index 13f914415c..5a8bacf9dd 100644 --- a/src/applications/project/profilepanel/PhabricatorProjectDetailsProfilePanel.php +++ b/src/applications/project/profilepanel/PhabricatorProjectDetailsProfilePanel.php @@ -5,6 +5,36 @@ final class PhabricatorProjectDetailsProfilePanel const PANELKEY = 'project.details'; + public function getPanelTypeName() { + return pht('Project Details'); + } + + private function getDefaultName() { + return pht('Project Details'); + } + + public function getDisplayName( + PhabricatorProfilePanelConfiguration $config) { + $name = $config->getPanelProperty('name'); + + if (strlen($name)) { + return $name; + } + + return $this->getDefaultName(); + } + + public function buildEditEngineFields( + PhabricatorProfilePanelConfiguration $config) { + return array( + id(new PhabricatorTextEditField()) + ->setKey('name') + ->setLabel(pht('Name')) + ->setPlaceholder($this->getDefaultName()) + ->setValue($config->getPanelProperty('name')), + ); + } + protected function newNavigationMenuItems( PhabricatorProfilePanelConfiguration $config) { diff --git a/src/applications/project/profilepanel/PhabricatorProjectMembersProfilePanel.php b/src/applications/project/profilepanel/PhabricatorProjectMembersProfilePanel.php index a8b674e274..0db2a03773 100644 --- a/src/applications/project/profilepanel/PhabricatorProjectMembersProfilePanel.php +++ b/src/applications/project/profilepanel/PhabricatorProjectMembersProfilePanel.php @@ -5,6 +5,36 @@ final class PhabricatorProjectMembersProfilePanel const PANELKEY = 'project.members'; + public function getPanelTypeName() { + return pht('Project Members'); + } + + private function getDefaultName() { + return pht('Members'); + } + + public function getDisplayName( + PhabricatorProfilePanelConfiguration $config) { + $name = $config->getPanelProperty('name'); + + if (strlen($name)) { + return $name; + } + + return $this->getDefaultName(); + } + + public function buildEditEngineFields( + PhabricatorProfilePanelConfiguration $config) { + return array( + id(new PhabricatorTextEditField()) + ->setKey('name') + ->setLabel(pht('Name')) + ->setPlaceholder($this->getDefaultName()) + ->setValue($config->getPanelProperty('name')), + ); + } + protected function newNavigationMenuItems( PhabricatorProfilePanelConfiguration $config) { @@ -12,7 +42,7 @@ final class PhabricatorProjectMembersProfilePanel $id = $project->getID(); - $name = pht('Members'); + $name = $this->getDisplayName($config); $icon = 'fa-group'; $href = "/project/members/{$id}/"; diff --git a/src/applications/project/profilepanel/PhabricatorProjectWorkboardProfilePanel.php b/src/applications/project/profilepanel/PhabricatorProjectWorkboardProfilePanel.php index a234dfcded..91f2f553f5 100644 --- a/src/applications/project/profilepanel/PhabricatorProjectWorkboardProfilePanel.php +++ b/src/applications/project/profilepanel/PhabricatorProjectWorkboardProfilePanel.php @@ -5,6 +5,36 @@ final class PhabricatorProjectWorkboardProfilePanel const PANELKEY = 'project.workboard'; + public function getPanelTypeName() { + return pht('Project Workboard'); + } + + private function getDefaultName() { + return pht('Workboard'); + } + + public function getDisplayName( + PhabricatorProfilePanelConfiguration $config) { + $name = $config->getPanelProperty('name'); + + if (strlen($name)) { + return $name; + } + + return $this->getDefaultName(); + } + + public function buildEditEngineFields( + PhabricatorProfilePanelConfiguration $config) { + return array( + id(new PhabricatorTextEditField()) + ->setKey('name') + ->setLabel(pht('Name')) + ->setPlaceholder($this->getDefaultName()) + ->setValue($config->getPanelProperty('name')), + ); + } + protected function newNavigationMenuItems( PhabricatorProfilePanelConfiguration $config) { $viewer = $this->getViewer(); @@ -29,7 +59,7 @@ final class PhabricatorProjectWorkboardProfilePanel $id = $project->getID(); $href = "/project/board/{$id}/"; - $name = pht('Workboard'); + $name = $this->getDisplayName($config); $item = id(new PHUIListItemView()) ->setRenderNameAsTooltip(true) diff --git a/src/applications/project/storage/PhabricatorProject.php b/src/applications/project/storage/PhabricatorProject.php index 4cf1c05e23..99fe366583 100644 --- a/src/applications/project/storage/PhabricatorProject.php +++ b/src/applications/project/storage/PhabricatorProject.php @@ -658,36 +658,36 @@ final class PhabricatorProject extends PhabricatorProjectDAO public function getBuiltinProfilePanels() { $panels = array(); - $panels[] = id(new PhabricatorProfilePanelConfiguration()) + $panels[] = PhabricatorProfilePanelConfiguration::initializeNewBuiltin() ->setBuiltinKey(self::PANEL_PROFILE) ->setPanelKey(PhabricatorProjectDetailsProfilePanel::PANELKEY); - $panels[] = id(new PhabricatorProfilePanelConfiguration()) + $panels[] = PhabricatorProfilePanelConfiguration::initializeNewBuiltin() ->setBuiltinKey(self::PANEL_WORKBOARD) ->setPanelKey(PhabricatorProjectWorkboardProfilePanel::PANELKEY); // TODO: This is temporary. - $href = urisprintf( + $uri = urisprintf( '/maniphest/?statuses=open()&projects=%s#R', $this->getPHID()); - $panels[] = id(new PhabricatorProfilePanelConfiguration()) + $panels[] = PhabricatorProfilePanelConfiguration::initializeNewBuiltin() ->setBuiltinKey('tasks') ->setPanelKey(PhabricatorLinkProfilePanel::PANELKEY) - ->setPanelProperty('icon', 'fa-anchor') + ->setPanelProperty('icon', 'maniphest') ->setPanelProperty('name', pht('Open Tasks')) - ->setPanelProperty('href', $href); + ->setPanelProperty('uri', $uri); // TODO: This is temporary. $id = $this->getID(); - $panels[] = id(new PhabricatorProfilePanelConfiguration()) + $panels[] = PhabricatorProfilePanelConfiguration::initializeNewBuiltin() ->setBuiltinKey('feed') ->setPanelKey(PhabricatorLinkProfilePanel::PANELKEY) - ->setPanelProperty('icon', 'fa-newspaper-o') + ->setPanelProperty('icon', 'feed') ->setPanelProperty('name', pht('Feed')) - ->setPanelProperty('href', "/project/feed/{$id}/"); + ->setPanelProperty('uri', "/project/feed/{$id}/"); - $panels[] = id(new PhabricatorProfilePanelConfiguration()) + $panels[] = PhabricatorProfilePanelConfiguration::initializeNewBuiltin() ->setBuiltinKey(self::PANEL_MEMBERS) ->setPanelKey(PhabricatorProjectMembersProfilePanel::PANELKEY); diff --git a/src/applications/search/editor/PhabricatorProfilePanelEditEngine.php b/src/applications/search/editor/PhabricatorProfilePanelEditEngine.php new file mode 100644 index 0000000000..7713a97f7c --- /dev/null +++ b/src/applications/search/editor/PhabricatorProfilePanelEditEngine.php @@ -0,0 +1,136 @@ +panelEngine = $engine; + return $this; + } + + public function getPanelEngine() { + return $this->panelEngine; + } + + public function setProfileObject( + PhabricatorProfilePanelInterface $profile_object) { + $this->profileObject = $profile_object; + return $this; + } + + public function getProfileObject() { + return $this->profileObject; + } + + public function setNewPanelConfiguration( + PhabricatorProfilePanelConfiguration $configuration) { + $this->newPanelConfiguration = $configuration; + return $this; + } + + public function getNewPanelConfiguration() { + return $this->newPanelConfiguration; + } + + public function setIsBuiltin($is_builtin) { + $this->isBuiltin = $is_builtin; + return $this; + } + + public function getIsBuiltin() { + return $this->isBuiltin; + } + + public function getEngineName() { + return pht('Profile Panels'); + } + + public function getSummaryHeader() { + return pht('Edit Profile Panel Configurations'); + } + + public function getSummaryText() { + return pht('This engine is used to modify menu items on profiles.'); + } + + public function getEngineApplicationClass() { + return 'PhabricatorSearchApplication'; + } + + protected function newEditableObject() { + if (!$this->newPanelConfiguration) { + throw new Exception( + pht('Profile panels can not be generated without an object context.')); + } + + return clone $this->newPanelConfiguration; + } + + protected function newObjectQuery() { + return id(new PhabricatorProfilePanelConfigurationQuery()); + } + + protected function getObjectCreateTitleText($object) { + if ($this->getIsBuiltin()) { + return pht('Edit Builtin Item'); + } else { + return pht('Create Menu Item'); + } + } + + protected function getObjectCreateButtonText($object) { + if ($this->getIsBuiltin()) { + return pht('Save Changes'); + } else { + return pht('Create Menu Item'); + } + } + + protected function getObjectEditTitleText($object) { + return pht('Edit Menu Item: %s', $object->getDisplayName()); + } + + protected function getObjectEditShortText($object) { + return pht('Edit Menu Item'); + } + + protected function getObjectCreateShortText() { + return pht('Edit Menu Item'); + } + + protected function getObjectCreateCancelURI($object) { + return $this->getPanelEngine()->getConfigureURI(); + } + + protected function getObjectViewURI($object) { + return $this->getPanelEngine()->getConfigureURI(); + } + + protected function buildCustomEditFields($object) { + $panel = $object->getPanel(); + $fields = $panel->buildEditEngineFields($object); + + $type_property = + PhabricatorProfilePanelConfigurationTransaction::TYPE_PROPERTY; + + foreach ($fields as $field) { + $field + ->setTransactionType($type_property) + ->setMetadataValue('property.key', $field->getKey()); + } + + return $fields; + } + +} diff --git a/src/applications/search/editor/PhabricatorProfilePanelEditor.php b/src/applications/search/editor/PhabricatorProfilePanelEditor.php new file mode 100644 index 0000000000..37169550d5 --- /dev/null +++ b/src/applications/search/editor/PhabricatorProfilePanelEditor.php @@ -0,0 +1,70 @@ +getTransactionType()) { + case PhabricatorProfilePanelConfigurationTransaction::TYPE_PROPERTY: + $key = $xaction->getMetadataValue('property.key'); + return $object->getPanelProperty($key, null); + } + } + + protected function getCustomTransactionNewValue( + PhabricatorLiskDAO $object, + PhabricatorApplicationTransaction $xaction) { + + switch ($xaction->getTransactionType()) { + case PhabricatorProfilePanelConfigurationTransaction::TYPE_PROPERTY: + return $xaction->getNewValue(); + } + } + + protected function applyCustomInternalTransaction( + PhabricatorLiskDAO $object, + PhabricatorApplicationTransaction $xaction) { + + switch ($xaction->getTransactionType()) { + case PhabricatorProfilePanelConfigurationTransaction::TYPE_PROPERTY: + $key = $xaction->getMetadataValue('property.key'); + $value = $xaction->getNewValue(); + $object->setPanelProperty($key, $value); + return; + } + + return parent::applyCustomInternalTransaction($object, $xaction); + } + + protected function applyCustomExternalTransaction( + PhabricatorLiskDAO $object, + PhabricatorApplicationTransaction $xaction) { + + switch ($xaction->getTransactionType()) { + case PhabricatorProfilePanelConfigurationTransaction::TYPE_PROPERTY: + return; + } + + return parent::applyCustomExternalTransaction($object, $xaction); + } + +} diff --git a/src/applications/search/engine/PhabricatorProfilePanelEngine.php b/src/applications/search/engine/PhabricatorProfilePanelEngine.php index 8fdd4f2f90..df50be15e8 100644 --- a/src/applications/search/engine/PhabricatorProfilePanelEngine.php +++ b/src/applications/search/engine/PhabricatorProfilePanelEngine.php @@ -5,6 +5,7 @@ final class PhabricatorProfilePanelEngine extends Phobject { private $viewer; private $profileObject; private $panels; + private $controller; public function setViewer(PhabricatorUser $viewer) { $this->viewer = $viewer; @@ -25,6 +26,103 @@ final class PhabricatorProfilePanelEngine extends Phobject { return $this->profileObject; } + public function setController(PhabricatorController $controller) { + $this->controller = $controller; + return $this; + } + + public function getController() { + return $this->controller; + } + + public function buildResponse() { + $controller = $this->getController(); + + $viewer = $controller->getViewer(); + $this->setViewer($viewer); + + $request = $controller->getRequest(); + + $panel_action = $request->getURIData('panelAction'); + $panel_id = $request->getURIData('panelID'); + + $panel_list = $this->loadPanels(); + + $selected_panel = null; + if (strlen($panel_id)) { + $panel_id_int = (int)$panel_id; + foreach ($panel_list as $panel) { + if ($panel_id_int) { + if ((int)$panel->getID() === $panel_id) { + $selected_panel = $panel; + break; + } + } + + $builtin_key = $panel->getBuiltinKey(); + if ($builtin_key === (string)$panel_id) { + $selected_panel = $panel; + break; + } + } + } + + switch ($panel_action) { + case 'view': + case 'info': + case 'hide': + case 'builtin': + if (!$selected_panel) { + return new Aphront404Response(); + } + break; + } + + $navigation = $this->buildNavigation(); + $navigation->selectFilter('panel.configure'); + + $crumbs = $controller->buildApplicationCrumbsForEditEngine(); + + switch ($panel_action) { + case 'view': + $content = $this->buildPanelViewContent($selected_panel); + break; + case 'configure': + $content = $this->buildPanelConfigureContent($panel_list); + $crumbs->addTextCrumb(pht('Configure Menu')); + break; + case 'new': + $panel_key = $request->getURIData('panelKey'); + $content = $this->buildPanelNewContent($panel_key); + break; + case 'builtin': + $content = $this->buildPanelBuiltinContent($selected_panel); + break; + case 'edit': + $content = $this->buildPanelEditContent(); + break; + default: + throw new Exception( + pht( + 'Unsupported panel action "%s".', + $panel_action)); + } + + if ($content instanceof AphrontResponse) { + return $content; + } + + if ($content instanceof AphrontResponseProducerInterface) { + return $content; + } + + return $controller->newPage() + ->setTitle(pht('Profile Stuff')) + ->setNavigation($navigation) + ->setCrumbs($crumbs) + ->appendChild($content); + } + public function buildNavigation() { $nav = id(new AphrontSideNavFilterView()) ->setIconNav(true) @@ -60,6 +158,11 @@ final class PhabricatorProfilePanelEngine extends Phobject { } } + $configure_item = $this->newConfigureMenuItem(); + if ($configure_item) { + $nav->addMenuItem($configure_item); + } + $nav->selectFilter(null); return $nav; @@ -75,10 +178,25 @@ final class PhabricatorProfilePanelEngine extends Phobject { private function loadPanels() { $viewer = $this->getViewer(); + $object = $this->getProfileObject(); $panels = $this->loadBuiltinProfilePanels(); - // TODO: Load persisted panels. + $stored_panels = id(new PhabricatorProfilePanelConfigurationQuery()) + ->setViewer($viewer) + ->withProfilePHIDs(array($object->getPHID())) + ->execute(); + + // Merge the stored panels into the builtin panels. If a builtin panel has + // a stored version, replace the defaults with the stored changes. + foreach ($stored_panels as $stored_panel) { + $builtin_key = $stored_panel->getBuiltinKey(); + if ($builtin_key !== null) { + $panels[$builtin_key] = $stored_panel; + } else { + $panels[] = $stored_panel; + } + } foreach ($panels as $panel) { $impl = $panel->getPanel(); @@ -86,6 +204,10 @@ final class PhabricatorProfilePanelEngine extends Phobject { $impl->setViewer($viewer); } + // Normalize keys since callers shouldn't rely on this array being + // partially keyed. + $panels = array_values($panels); + return $panels; } @@ -128,6 +250,7 @@ final class PhabricatorProfilePanelEngine extends Phobject { } $builtin + ->setProfilePHID($object->getPHID()) ->attachPanel($panel) ->attachProfileObject($object) ->setPanelOrder($order); @@ -149,4 +272,208 @@ final class PhabricatorProfilePanelEngine extends Phobject { } } + private function newConfigureMenuItem() { + if (!PhabricatorEnv::getEnvConfig('phabricator.show-prototypes')) { + return null; + } + + $viewer = $this->getViewer(); + $object = $this->getProfileObject(); + + $can_edit = PhabricatorPolicyFilter::hasCapability( + $viewer, + $object, + PhabricatorPolicyCapability::CAN_EDIT); + + return id(new PHUIListItemView()) + ->setName('Configure Menu') + ->setKey('panel.configure') + ->setIcon('fa-gear') + ->setHref($this->getPanelURI('configure/')) + ->setDisabled(!$can_edit) + ->setWorkflow(!$can_edit) + ->setRenderNameAsTooltip(true); + } + + public function getConfigureURI() { + return $this->getPanelURI('configure/'); + } + + private function getPanelURI($path) { + $project = $this->getProfileObject(); + $id = $project->getID(); + return "/project/{$id}/panel/{$path}"; + } + + private function buildPanelConfigureContent(array $panels) { + $viewer = $this->getViewer(); + $object = $this->getProfileObject(); + + PhabricatorPolicyFilter::requireCapability( + $viewer, + $object, + PhabricatorPolicyCapability::CAN_EDIT); + + $list = new PHUIObjectItemListView(); + foreach ($panels as $panel) { + $id = $panel->getID(); + $builtin_key = $panel->getBuiltinKey(); + + $can_edit = PhabricatorPolicyFilter::hasCapability( + $viewer, + $panel, + PhabricatorPolicyCapability::CAN_EDIT); + + $item = id(new PHUIObjectItemView()); + + $name = $panel->getDisplayName(); + $type = $panel->getPanelTypeName(); + if (!strlen(trim($name))) { + $name = pht('Untitled "%s" Item', $type); + } + + $item->setHeader($name); + $item->addAttribute($type); + + if ($can_edit) { + if ($id) { + $item->setHref($this->getPanelURI("edit/{$id}/")); + } else { + $item->setHref($this->getPanelURI("builtin/{$builtin_key}/")); + } + } + + $list->addItem($item); + } + + $action_view = id(new PhabricatorActionListView()) + ->setUser($viewer); + + $panel_types = PhabricatorProfilePanel::getAllPanels(); + + $action_view->addAction( + id(new PhabricatorActionView()) + ->setLabel(true) + ->setName(pht('Add New Menu Item...'))); + + foreach ($panel_types as $panel_type) { + if (!$panel_type->canAddToObject($object)) { + continue; + } + + $panel_key = $panel_type->getPanelKey(); + + $action_view->addAction( + id(new PhabricatorActionView()) + ->setIcon($panel_type->getPanelTypeIcon()) + ->setName($panel_type->getPanelTypeName()) + ->setHref($this->getPanelURI("new/{$panel_key}/"))); + } + + $action_view->addAction( + id(new PhabricatorActionView()) + ->setLabel(true) + ->setName(pht('Documentation'))); + + $action_view->addAction( + id(new PhabricatorActionView()) + ->setIcon('fa-book') + ->setName(pht('TODO: Write Documentation'))); + + $action_button = id(new PHUIButtonView()) + ->setTag('a') + ->setText(pht('Configure Menu')) + ->setHref('#') + ->setIconFont('fa-gear') + ->setDropdownMenu($action_view); + + $header = id(new PHUIHeaderView()) + ->setHeader(pht('Profile Menu Items')) + ->addActionLink($action_button); + + $box = id(new PHUIObjectBoxView()) + ->setHeader($header) + ->setObjectList($list); + + return $box; + } + + private function buildPanelNewContent($panel_key) { + $panel_types = PhabricatorProfilePanel::getAllPanels(); + $panel_type = idx($panel_types, $panel_key); + if (!$panel_type) { + return new Aphront404Response(); + } + + $object = $this->getProfileObject(); + if (!$panel_type->canAddToObject($object)) { + return new Aphront404Response(); + } + + $configuration = + PhabricatorProfilePanelConfiguration::initializeNewPanelConfiguration( + $object, + $panel_type); + + $viewer = $this->getViewer(); + + PhabricatorPolicyFilter::requireCapability( + $viewer, + $configuration, + PhabricatorPolicyCapability::CAN_EDIT); + + $controller = $this->getController(); + + return id(new PhabricatorProfilePanelEditEngine()) + ->setPanelEngine($this) + ->setProfileObject($object) + ->setNewPanelConfiguration($configuration) + ->setController($controller) + ->buildResponse(); + } + + private function buildPanelEditContent() { + $viewer = $this->getViewer(); + $object = $this->getProfileObject(); + $controller = $this->getController(); + + return id(new PhabricatorProfilePanelEditEngine()) + ->setPanelEngine($this) + ->setProfileObject($object) + ->setController($controller) + ->buildResponse(); + } + + private function buildPanelBuiltinContent( + PhabricatorProfilePanelConfiguration $configuration) { + + // If this builtin panel has already been persisted, redirect to the + // edit page. + $id = $configuration->getID(); + if ($id) { + return id(new AphrontRedirectResponse()) + ->setURI($this->getPanelURI("edit/{$id}/")); + } + + // Otherwise, act like we're creating a new panel, we're just starting + // with the builtin template. + $viewer = $this->getViewer(); + + PhabricatorPolicyFilter::requireCapability( + $viewer, + $configuration, + PhabricatorPolicyCapability::CAN_EDIT); + + $object = $this->getProfileObject(); + $controller = $this->getController(); + + return id(new PhabricatorProfilePanelEditEngine()) + ->setIsBuiltin(true) + ->setPanelEngine($this) + ->setProfileObject($object) + ->setNewPanelConfiguration($configuration) + ->setController($controller) + ->buildResponse(); + } + } diff --git a/src/applications/search/profilepanel/PhabricatorLinkProfilePanel.php b/src/applications/search/profilepanel/PhabricatorLinkProfilePanel.php index 4a87c5e3b9..d2b19ad368 100644 --- a/src/applications/search/profilepanel/PhabricatorLinkProfilePanel.php +++ b/src/applications/search/profilepanel/PhabricatorLinkProfilePanel.php @@ -5,19 +5,89 @@ final class PhabricatorLinkProfilePanel const PANELKEY = 'link'; + public function getPanelTypeIcon() { + return 'fa-link'; + } + + public function getPanelTypeName() { + return pht('Link'); + } + + public function canAddToObject( + PhabricatorProfilePanelInterface $object) { + return true; + } + + public function getDisplayName( + PhabricatorProfilePanelConfiguration $config) { + return $this->getLinkName($config); + } + + public function buildEditEngineFields( + PhabricatorProfilePanelConfiguration $config) { + return array( + id(new PhabricatorTextEditField()) + ->setKey('name') + ->setLabel(pht('Name')) + ->setIsRequired(true) + ->setValue($this->getLinkName($config)), + id(new PhabricatorTextEditField()) + ->setKey('uri') + ->setLabel(pht('URI')) + ->setIsRequired(true) + ->setValue($this->getLinkURI($config)), + id(new PhabricatorIconSetEditField()) + ->setKey('icon') + ->setLabel(pht('Icon')) + ->setIconSet(new PhabricatorProfilePanelIconSet()) + ->setValue($this->getLinkIcon($config)), + ); + } + + private function getLinkName( + PhabricatorProfilePanelConfiguration $config) { + return $config->getPanelProperty('name'); + } + + private function getLinkIcon( + PhabricatorProfilePanelConfiguration $config) { + return $config->getPanelProperty('icon', 'link'); + } + + private function getLinkURI( + PhabricatorProfilePanelConfiguration $config) { + return $config->getPanelProperty('uri'); + } + + private function isValidLinkURI($uri) { + return PhabricatorEnv::isValidURIForLink($uri); + } + protected function newNavigationMenuItems( PhabricatorProfilePanelConfiguration $config) { - $icon = $config->getPanelProperty('icon'); - $name = $config->getPanelProperty('name'); - $href = $config->getPanelProperty('href'); + $icon = $this->getLinkIcon($config); + $name = $this->getLinkName($config); + $href = $this->getLinkURI($config); + + if (!$this->isValidLinkURI($href)) { + $href = '#'; + } + + $icon_object = id(new PhabricatorProfilePanelIconSet()) + ->getIcon($icon); + if ($icon_object) { + $icon_class = $icon_object->getIcon(); + } else { + $icon_class = 'fa-link'; + } $item = id(new PHUIListItemView()) ->setRenderNameAsTooltip(true) ->setType(PHUIListItemView::TYPE_ICON_NAV) ->setHref($href) ->setName($name) - ->setIcon($icon); + ->setIcon($icon_class); return array( $item, diff --git a/src/applications/search/profilepanel/PhabricatorProfilePanel.php b/src/applications/search/profilepanel/PhabricatorProfilePanel.php index aae8a60fc8..6f029927d8 100644 --- a/src/applications/search/profilepanel/PhabricatorProfilePanel.php +++ b/src/applications/search/profilepanel/PhabricatorProfilePanel.php @@ -12,6 +12,25 @@ abstract class PhabricatorProfilePanel extends Phobject { abstract protected function newNavigationMenuItems( PhabricatorProfilePanelConfiguration $config); + public function getPanelTypeIcon() { + return null; + } + + abstract public function getPanelTypeName(); + + abstract public function getDisplayName( + PhabricatorProfilePanelConfiguration $config); + + public function buildEditEngineFields( + PhabricatorProfilePanelConfiguration $config) { + return array(); + } + + public function canAddToObject( + PhabricatorProfilePanelInterface $object) { + return false; + } + public function setViewer(PhabricatorUser $viewer) { $this->viewer = $viewer; return $this; diff --git a/src/applications/search/profilepanel/PhabricatorProfilePanelIconSet.php b/src/applications/search/profilepanel/PhabricatorProfilePanelIconSet.php new file mode 100644 index 0000000000..19b164e932 --- /dev/null +++ b/src/applications/search/profilepanel/PhabricatorProfilePanelIconSet.php @@ -0,0 +1,42 @@ + 'link', + 'icon' => 'fa-link', + 'name' => pht('Link'), + ), + array( + 'key' => 'maniphest', + 'icon' => 'fa-anchor', + 'name' => pht('Maniphest'), + ), + array( + 'key' => 'feed', + 'icon' => 'fa-newspaper-o', + 'name' => pht('Feed'), + ), + ); + + $icons = array(); + foreach ($list as $spec) { + $icons[] = id(new PhabricatorIconSetIcon()) + ->setKey($spec['key']) + ->setIcon($spec['icon']) + ->setLabel($spec['name']); + } + + return $icons; + } + +} diff --git a/src/applications/search/query/PhabricatorProfilePanelConfigurationQuery.php b/src/applications/search/query/PhabricatorProfilePanelConfigurationQuery.php index 7cca0ca115..52e12581cf 100644 --- a/src/applications/search/query/PhabricatorProfilePanelConfigurationQuery.php +++ b/src/applications/search/query/PhabricatorProfilePanelConfigurationQuery.php @@ -5,7 +5,7 @@ final class PhabricatorProfilePanelConfigurationQuery private $ids; private $phids; - private $profileObjectPHIDs; + private $profilePHIDs; public function withIDs(array $ids) { $this->ids = $ids; @@ -17,8 +17,8 @@ final class PhabricatorProfilePanelConfigurationQuery return $this; } - public function withProfileObjectPHIDs(array $phids) { - $this->profileObjectPHIDs = $phids; + public function withProfilePHIDs(array $phids) { + $this->profilePHIDs = $phids; return $this; } @@ -47,16 +47,54 @@ final class PhabricatorProfilePanelConfigurationQuery $this->phids); } - if ($this->profileObjectPHIDs !== null) { + if ($this->profilePHIDs !== null) { $where[] = qsprintf( $conn, - 'profileObjectPHID IN (%Ls)', - $this->profileObjectPHIDs); + 'profilePHID IN (%Ls)', + $this->profilePHIDs); } return $where; } + protected function willFilterPage(array $page) { + $panels = PhabricatorProfilePanel::getAllPanels(); + foreach ($page as $key => $panel) { + $panel_type = idx($panels, $panel->getPanelKey()); + if (!$panel_type) { + $this->didRejectResult($panel); + unset($page[$key]); + continue; + } + $panel->attachPanel($panel_type); + } + + if (!$page) { + return array(); + } + + $profile_phids = mpull($page, 'getProfilePHID'); + + $profiles = id(new PhabricatorObjectQuery()) + ->setViewer($this->getViewer()) + ->setParentQuery($this) + ->withPHIDs($profile_phids) + ->execute(); + $profiles = mpull($profiles, null, 'getPHID'); + + foreach ($page as $key => $panel) { + $profile = idx($profiles, $panel->getProfilePHID()); + if (!$profile) { + $this->didRejectResult($panel); + unset($page[$key]); + continue; + } + $panel->attachProfileObject($profile); + } + + return $page; + } + public function getQueryApplicationClass() { return 'PhabricatorSearchApplication'; } diff --git a/src/applications/search/storage/PhabricatorProfilePanelConfiguration.php b/src/applications/search/storage/PhabricatorProfilePanelConfiguration.php index 9c38bbfb85..a8981ad729 100644 --- a/src/applications/search/storage/PhabricatorProfilePanelConfiguration.php +++ b/src/applications/search/storage/PhabricatorProfilePanelConfiguration.php @@ -4,26 +4,34 @@ final class PhabricatorProfilePanelConfiguration extends PhabricatorSearchDAO implements PhabricatorPolicyInterface, - PhabricatorExtendedPolicyInterface { + PhabricatorExtendedPolicyInterface, + PhabricatorApplicationTransactionInterface { protected $profilePHID; protected $panelKey; protected $builtinKey; protected $panelOrder; - protected $isDisabled; + protected $visibility; protected $panelProperties = array(); private $profileObject = self::ATTACHABLE; private $panel = self::ATTACHABLE; + const VISIBILITY_VISIBLE = 'visible'; + const VISIBILITY_DISABLED = 'disabled'; + + public static function initializeNewBuiltin() { + return id(new self()) + ->setVisibility(self::VISIBILITY_VISIBLE); + } + public static function initializeNewPanelConfiguration( PhabricatorProfilePanelInterface $profile_object, PhabricatorProfilePanel $panel) { - return id(new self()) + return self::initializeNewBuiltin() ->setProfilePHID($profile_object->getPHID()) ->setPanelKey($panel->getPanelKey()) - ->setIsDisabled(0) ->attachPanel($panel) ->attachProfileObject($profile_object); } @@ -36,9 +44,9 @@ final class PhabricatorProfilePanelConfiguration ), self::CONFIG_COLUMN_SCHEMA => array( 'panelKey' => 'text64', - 'builtinKey' => 'text64', - 'panelOrder' => 'uint32', - 'isDisabled' => 'bool', + 'builtinKey' => 'text64?', + 'panelOrder' => 'uint32?', + 'visibility' => 'text32', ), self::CONFIG_KEY_SCHEMA => array( 'key_profile' => array( @@ -48,6 +56,11 @@ final class PhabricatorProfilePanelConfiguration ) + parent::getConfiguration(); } + public function generatePHID() { + return PhabricatorPHID::generateNewPHID( + PhabricatorProfilePanelPHIDType::TYPECONST); + } + public function attachPanel(PhabricatorProfilePanel $panel) { $this->panel = $panel; return $this; @@ -67,10 +80,6 @@ final class PhabricatorProfilePanelConfiguration return $this->assertAttached($this->profileObject); } - public function buildNavigationMenuItems() { - return $this->getPanel()->buildNavigationMenuItems($this); - } - public function setPanelProperty($key, $value) { $this->panelProperties[$key] = $value; return $this; @@ -80,6 +89,18 @@ final class PhabricatorProfilePanelConfiguration return idx($this->panelProperties, $key, $default); } + public function buildNavigationMenuItems() { + return $this->getPanel()->buildNavigationMenuItems($this); + } + + public function getPanelTypeName() { + return $this->getPanel()->getPanelTypeName(); + } + + public function getDisplayName() { + return $this->getPanel()->getDisplayName($this); + } + /* -( PhabricatorPolicyInterface )----------------------------------------- */ @@ -121,4 +142,27 @@ final class PhabricatorProfilePanelConfiguration ); } + +/* -( PhabricatorApplicationTransactionInterface )------------------------- */ + + + public function getApplicationTransactionEditor() { + return new PhabricatorProfilePanelEditor(); + } + + public function getApplicationTransactionObject() { + return $this; + } + + public function getApplicationTransactionTemplate() { + return new PhabricatorProfilePanelConfigurationTransaction(); + } + + public function willRenderTimeline( + PhabricatorApplicationTransactionView $timeline, + AphrontRequest $request) { + + return $timeline; + } + } diff --git a/src/applications/search/storage/PhabricatorProfilePanelConfigurationTransaction.php b/src/applications/search/storage/PhabricatorProfilePanelConfigurationTransaction.php index 0ab756430a..b4b7111673 100644 --- a/src/applications/search/storage/PhabricatorProfilePanelConfigurationTransaction.php +++ b/src/applications/search/storage/PhabricatorProfilePanelConfigurationTransaction.php @@ -3,6 +3,8 @@ final class PhabricatorProfilePanelConfigurationTransaction extends PhabricatorApplicationTransaction { + const TYPE_PROPERTY = 'profilepanel.property'; + public function getApplicationName() { return 'search'; } diff --git a/src/applications/transactions/controller/PhabricatorEditEngineConfigurationListController.php b/src/applications/transactions/controller/PhabricatorEditEngineConfigurationListController.php index dbf73477da..7f9b02244f 100644 --- a/src/applications/transactions/controller/PhabricatorEditEngineConfigurationListController.php +++ b/src/applications/transactions/controller/PhabricatorEditEngineConfigurationListController.php @@ -16,6 +16,10 @@ final class PhabricatorEditEngineConfigurationListController $engine = PhabricatorEditEngine::getByKey($viewer, $engine_key) ->setViewer($viewer); + if (!$engine->isEngineConfigurable()) { + return new Aphront404Response(); + } + $items = array(); $items[] = id(new PHUIListItemView()) ->setType(PHUIListItemView::TYPE_LABEL) diff --git a/src/applications/transactions/controller/PhabricatorEditEngineController.php b/src/applications/transactions/controller/PhabricatorEditEngineController.php index 9c712f7e3a..b02bf8302f 100644 --- a/src/applications/transactions/controller/PhabricatorEditEngineController.php +++ b/src/applications/transactions/controller/PhabricatorEditEngineController.php @@ -72,6 +72,10 @@ abstract class PhabricatorEditEngineController $engine = $config->getEngine(); } + if (!$engine->isEngineConfigurable()) { + return null; + } + return $config; } diff --git a/src/applications/transactions/editengine/PhabricatorEditEngine.php b/src/applications/transactions/editengine/PhabricatorEditEngine.php index 6f1a2a669a..0fb96e7e19 100644 --- a/src/applications/transactions/editengine/PhabricatorEditEngine.php +++ b/src/applications/transactions/editengine/PhabricatorEditEngine.php @@ -57,6 +57,10 @@ abstract class PhabricatorEditEngine return $this; } + public function isEngineConfigurable() { + return true; + } + /* -( Managing Fields )---------------------------------------------------- */ @@ -1005,8 +1009,11 @@ abstract class PhabricatorEditEngine } $header = id(new PHUIHeaderView()) - ->setHeader($header_text) - ->addActionLink($action_button); + ->setHeader($header_text); + + if ($action_button) { + $header->addActionLink($action_button); + } $crumbs = $this->buildCrumbs($object, $final = true); @@ -1066,6 +1073,10 @@ abstract class PhabricatorEditEngine } private function buildEditFormActionButton($object) { + if (!$this->isEngineConfigurable()) { + return null; + } + $viewer = $this->getViewer(); $action_view = id(new PhabricatorActionListView()) diff --git a/src/applications/transactions/editfield/PhabricatorTextEditField.php b/src/applications/transactions/editfield/PhabricatorTextEditField.php index 342b0c7157..fa51ff6142 100644 --- a/src/applications/transactions/editfield/PhabricatorTextEditField.php +++ b/src/applications/transactions/editfield/PhabricatorTextEditField.php @@ -3,8 +3,26 @@ final class PhabricatorTextEditField extends PhabricatorEditField { + private $placeholder; + + public function setPlaceholder($placeholder) { + $this->placeholder = $placeholder; + return $this; + } + + public function getPlaceholder() { + return $this->placeholder; + } + protected function newControl() { - return new AphrontFormTextControl(); + $control = new AphrontFormTextControl(); + + $placeholder = $this->getPlaceholder(); + if (strlen($placeholder)) { + $control->setPlaceholder($placeholder); + } + + return $control; } protected function newConduitParameterType() { diff --git a/src/applications/transactions/query/PhabricatorEditEngineSearchEngine.php b/src/applications/transactions/query/PhabricatorEditEngineSearchEngine.php index b2959a8596..4ba4ed9b12 100644 --- a/src/applications/transactions/query/PhabricatorEditEngineSearchEngine.php +++ b/src/applications/transactions/query/PhabricatorEditEngineSearchEngine.php @@ -62,6 +62,10 @@ final class PhabricatorEditEngineSearchEngine $list = id(new PHUIObjectItemListView()) ->setUser($viewer); foreach ($engines as $engine) { + if (!$engine->isEngineConfigurable()) { + continue; + } + $engine_key = $engine->getEngineKey(); $query_uri = "/transactions/editengine/{$engine_key}/"; diff --git a/src/docs/user/field/conduit_changes.diviner b/src/docs/user/field/conduit_changes.diviner index e77a582251..a5aa8fef6e 100644 --- a/src/docs/user/field/conduit_changes.diviner +++ b/src/docs/user/field/conduit_changes.diviner @@ -39,7 +39,7 @@ Use {nav My Deprecated Calls} to find calls to deprecated methods you have made, and {nav Deprecated Call Logs} to find deprecated calls by all users. You can also search for calls by specific users. For example, it may be useful -to serach for any bot accounts you run to make sure they aren't calling +to search for any bot accounts you run to make sure they aren't calling outdated APIs. The most common cause of calls to deprecated methods is users running very