From 61f82bb97b8a2fe45cdf7628cc1646f07a065671 Mon Sep 17 00:00:00 2001 From: epriestley Date: Sat, 5 Mar 2016 14:45:56 -0800 Subject: [PATCH] Introduce "Curtain" views, panels, and extensions Summary: This opens up the new action column to have specialized rendering and behavior. Briefly: - Converted applications (right now, only Paste) render a `CurtainView` to build the column content. - This view uses new extensions to build panels (projects, subscribers, tokens). - The panel extension code and rendering can be changed without breaking old stuff. Minor changes: - Token awards now load their tokens, for consistency/simplicity. - Removed the rest of the "fork of" / "forked from" UI in Paste -- I essentially removed these features a while ago, and no one has complained. Test Plan: UI is a bit rough, but works, and it's going to get changed now anyway: {F1160550} {F1160551} Reviewers: chad Reviewed By: chad Differential Revision: https://secure.phabricator.com/D15414 --- resources/celerity/map.php | 2 + src/__phutil_library_map__.php | 12 ++ .../base/controller/PhabricatorController.php | 21 ++- .../PhabricatorPasteViewController.php | 102 +++++--------- .../PhabricatorProjectsCurtainExtension.php | 91 +++++++++++++ ...abricatorSubscriptionsCurtainExtension.php | 39 ++++++ .../PhabricatorTokensCurtainExtension.php | 67 ++++++++++ .../query/PhabricatorTokenGivenQuery.php | 29 +++- .../tokens/storage/PhabricatorTokenGiven.php | 10 ++ src/view/extension/PHUICurtainExtension.php | 124 ++++++++++++++++++ src/view/layout/PHUICurtainPanelView.php | 63 +++++++++ src/view/layout/PHUICurtainView.php | 52 ++++++++ src/view/phui/PHUITwoColumnView.php | 25 +++- webroot/rsrc/css/phui/phui-curtain-view.css | 22 ++++ 14 files changed, 586 insertions(+), 73 deletions(-) create mode 100644 src/applications/project/engineextension/PhabricatorProjectsCurtainExtension.php create mode 100644 src/applications/subscriptions/engineextension/PhabricatorSubscriptionsCurtainExtension.php create mode 100644 src/applications/tokens/engineextension/PhabricatorTokensCurtainExtension.php create mode 100644 src/view/extension/PHUICurtainExtension.php create mode 100644 src/view/layout/PHUICurtainPanelView.php create mode 100644 src/view/layout/PHUICurtainView.php create mode 100644 webroot/rsrc/css/phui/phui-curtain-view.css diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 4ab5e3cd3a..dce8137749 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -127,6 +127,7 @@ return array( 'rsrc/css/phui/phui-button.css' => 'a64a8de6', 'rsrc/css/phui/phui-chart.css' => '6bf6f78e', 'rsrc/css/phui/phui-crumbs-view.css' => '79d536e5', + 'rsrc/css/phui/phui-curtain-view.css' => '8bb7ee8f', 'rsrc/css/phui/phui-document-pro.css' => '92d5b648', 'rsrc/css/phui/phui-document-summary.css' => '9ca48bdf', 'rsrc/css/phui/phui-document.css' => '9c71d2bf', @@ -811,6 +812,7 @@ return array( 'phui-calendar-month-css' => '476be7e0', 'phui-chart-css' => '6bf6f78e', 'phui-crumbs-view-css' => '79d536e5', + 'phui-curtain-view-css' => '8bb7ee8f', 'phui-document-summary-view-css' => '9ca48bdf', 'phui-document-view-css' => '9c71d2bf', 'phui-document-view-pro-css' => '92d5b648', diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 0a5b5e7cd9..45adf16a3a 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -1505,6 +1505,9 @@ phutil_register_library_map(array( 'PHUIColorPalletteExample' => 'applications/uiexample/examples/PHUIColorPalletteExample.php', 'PHUICrumbView' => 'view/phui/PHUICrumbView.php', 'PHUICrumbsView' => 'view/phui/PHUICrumbsView.php', + 'PHUICurtainExtension' => 'view/extension/PHUICurtainExtension.php', + 'PHUICurtainPanelView' => 'view/layout/PHUICurtainPanelView.php', + 'PHUICurtainView' => 'view/layout/PHUICurtainView.php', 'PHUIDiffInlineCommentDetailView' => 'infrastructure/diff/view/PHUIDiffInlineCommentDetailView.php', 'PHUIDiffInlineCommentEditView' => 'infrastructure/diff/view/PHUIDiffInlineCommentEditView.php', 'PHUIDiffInlineCommentRowScaffold' => 'infrastructure/diff/view/PHUIDiffInlineCommentRowScaffold.php', @@ -3011,6 +3014,7 @@ phutil_register_library_map(array( 'PhabricatorProjectWatcherListView' => 'applications/project/view/PhabricatorProjectWatcherListView.php', 'PhabricatorProjectWorkboardBackgroundColor' => 'applications/project/constants/PhabricatorProjectWorkboardBackgroundColor.php', 'PhabricatorProjectWorkboardProfilePanel' => 'applications/project/profilepanel/PhabricatorProjectWorkboardProfilePanel.php', + 'PhabricatorProjectsCurtainExtension' => 'applications/project/engineextension/PhabricatorProjectsCurtainExtension.php', 'PhabricatorProjectsEditEngineExtension' => 'applications/project/engineextension/PhabricatorProjectsEditEngineExtension.php', 'PhabricatorProjectsEditField' => 'applications/transactions/editfield/PhabricatorProjectsEditField.php', 'PhabricatorProjectsFulltextEngineExtension' => 'applications/project/engineextension/PhabricatorProjectsFulltextEngineExtension.php', @@ -3320,6 +3324,7 @@ phutil_register_library_map(array( 'PhabricatorSubscriptionsAddSelfHeraldAction' => 'applications/subscriptions/herald/PhabricatorSubscriptionsAddSelfHeraldAction.php', 'PhabricatorSubscriptionsAddSubscribersHeraldAction' => 'applications/subscriptions/herald/PhabricatorSubscriptionsAddSubscribersHeraldAction.php', 'PhabricatorSubscriptionsApplication' => 'applications/subscriptions/application/PhabricatorSubscriptionsApplication.php', + 'PhabricatorSubscriptionsCurtainExtension' => 'applications/subscriptions/engineextension/PhabricatorSubscriptionsCurtainExtension.php', 'PhabricatorSubscriptionsEditController' => 'applications/subscriptions/controller/PhabricatorSubscriptionsEditController.php', 'PhabricatorSubscriptionsEditEngineExtension' => 'applications/subscriptions/engineextension/PhabricatorSubscriptionsEditEngineExtension.php', 'PhabricatorSubscriptionsEditor' => 'applications/subscriptions/editor/PhabricatorSubscriptionsEditor.php', @@ -3388,6 +3393,7 @@ phutil_register_library_map(array( 'PhabricatorTokenUIEventListener' => 'applications/tokens/event/PhabricatorTokenUIEventListener.php', 'PhabricatorTokenizerEditField' => 'applications/transactions/editfield/PhabricatorTokenizerEditField.php', 'PhabricatorTokensApplication' => 'applications/tokens/application/PhabricatorTokensApplication.php', + 'PhabricatorTokensCurtainExtension' => 'applications/tokens/engineextension/PhabricatorTokensCurtainExtension.php', 'PhabricatorTokensSettingsPanel' => 'applications/settings/panel/PhabricatorTokensSettingsPanel.php', 'PhabricatorTooltipUIExample' => 'applications/uiexample/examples/PhabricatorTooltipUIExample.php', 'PhabricatorTransactions' => 'applications/transactions/constants/PhabricatorTransactions.php', @@ -5751,6 +5757,9 @@ phutil_register_library_map(array( 'PHUIColorPalletteExample' => 'PhabricatorUIExample', 'PHUICrumbView' => 'AphrontView', 'PHUICrumbsView' => 'AphrontView', + 'PHUICurtainExtension' => 'Phobject', + 'PHUICurtainPanelView' => 'AphrontTagView', + 'PHUICurtainView' => 'AphrontTagView', 'PHUIDiffInlineCommentDetailView' => 'PHUIDiffInlineCommentView', 'PHUIDiffInlineCommentEditView' => 'PHUIDiffInlineCommentView', 'PHUIDiffInlineCommentRowScaffold' => 'AphrontView', @@ -7503,6 +7512,7 @@ phutil_register_library_map(array( 'PhabricatorProjectWatcherListView' => 'PhabricatorProjectUserListView', 'PhabricatorProjectWorkboardBackgroundColor' => 'Phobject', 'PhabricatorProjectWorkboardProfilePanel' => 'PhabricatorProfilePanel', + 'PhabricatorProjectsCurtainExtension' => 'PHUICurtainExtension', 'PhabricatorProjectsEditEngineExtension' => 'PhabricatorEditEngineExtension', 'PhabricatorProjectsEditField' => 'PhabricatorTokenizerEditField', 'PhabricatorProjectsFulltextEngineExtension' => 'PhabricatorFulltextEngineExtension', @@ -7872,6 +7882,7 @@ phutil_register_library_map(array( 'PhabricatorSubscriptionsAddSelfHeraldAction' => 'PhabricatorSubscriptionsHeraldAction', 'PhabricatorSubscriptionsAddSubscribersHeraldAction' => 'PhabricatorSubscriptionsHeraldAction', 'PhabricatorSubscriptionsApplication' => 'PhabricatorApplication', + 'PhabricatorSubscriptionsCurtainExtension' => 'PHUICurtainExtension', 'PhabricatorSubscriptionsEditController' => 'PhabricatorController', 'PhabricatorSubscriptionsEditEngineExtension' => 'PhabricatorEditEngineExtension', 'PhabricatorSubscriptionsEditor' => 'PhabricatorEditor', @@ -7945,6 +7956,7 @@ phutil_register_library_map(array( 'PhabricatorTokenUIEventListener' => 'PhabricatorEventListener', 'PhabricatorTokenizerEditField' => 'PhabricatorPHIDListEditField', 'PhabricatorTokensApplication' => 'PhabricatorApplication', + 'PhabricatorTokensCurtainExtension' => 'PHUICurtainExtension', 'PhabricatorTokensSettingsPanel' => 'PhabricatorSettingsPanel', 'PhabricatorTooltipUIExample' => 'PhabricatorUIExample', 'PhabricatorTransactions' => 'Phobject', diff --git a/src/applications/base/controller/PhabricatorController.php b/src/applications/base/controller/PhabricatorController.php index e6997d97ab..fffe299b3f 100644 --- a/src/applications/base/controller/PhabricatorController.php +++ b/src/applications/base/controller/PhabricatorController.php @@ -468,7 +468,26 @@ abstract class PhabricatorController extends AphrontController { public function newApplicationMenu() { return id(new PHUIApplicationMenuView()) - ->setViewer($this->getRequest()->getUser()); + ->setViewer($this->getViewer()); + } + + public function newCurtainView($object) { + $viewer = $this->getViewer(); + + $action_list = id(new PhabricatorActionListView()) + ->setViewer($viewer) + ->setObject($object); + + $curtain = id(new PHUICurtainView()) + ->setViewer($viewer) + ->setActionList($action_list); + + $panels = PHUICurtainExtension::buildExtensionPanels($viewer, $object); + foreach ($panels as $panel) { + $curtain->addPanel($panel); + } + + return $curtain; } protected function buildTransactionTimeline( diff --git a/src/applications/paste/controller/PhabricatorPasteViewController.php b/src/applications/paste/controller/PhabricatorPasteViewController.php index 18d3ad1fb6..f29c93a162 100644 --- a/src/applications/paste/controller/PhabricatorPasteViewController.php +++ b/src/applications/paste/controller/PhabricatorPasteViewController.php @@ -40,15 +40,9 @@ final class PhabricatorPasteViewController extends PhabricatorPasteController { return new Aphront404Response(); } - $forks = id(new PhabricatorPasteQuery()) - ->setViewer($viewer) - ->withParentPHIDs(array($paste->getPHID())) - ->execute(); - $fork_phids = mpull($forks, 'getPHID'); - $header = $this->buildHeaderView($paste); - $actions = $this->buildActionView($viewer, $paste); - $properties = $this->buildPropertyView($paste, $fork_phids); + $curtain = $this->buildCurtain($paste); + $subheader = $this->buildSubheaderView($paste); $source_code = $this->buildSourceCodeView($paste, $this->highlightMap); @@ -78,8 +72,7 @@ final class PhabricatorPasteViewController extends PhabricatorPasteController { $timeline, $comment_view, )) - ->setPropertyList($properties) - ->setActionList($actions) + ->setCurtain($curtain) ->addClass('ponder-question-view'); return $this->newPage() @@ -116,9 +109,9 @@ final class PhabricatorPasteViewController extends PhabricatorPasteController { return $header; } - private function buildActionView( - PhabricatorUser $viewer, - PhabricatorPaste $paste) { + private function buildCurtain(PhabricatorPaste $paste) { + $viewer = $this->getViewer(); + $curtain = $this->newCurtainView($paste); $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, @@ -126,43 +119,42 @@ final class PhabricatorPasteViewController extends PhabricatorPasteController { PhabricatorPolicyCapability::CAN_EDIT); $id = $paste->getID(); + $edit_uri = $this->getApplicationURI("edit/{$id}/"); + $archive_uri = $this->getApplicationURI("archive/{$id}/"); + $raw_uri = $this->getApplicationURI("raw/{$id}/"); - $action_list = id(new PhabricatorActionListView()) - ->setUser($viewer) - ->setObject($paste); - - $action_list->addAction( - id(new PhabricatorActionView()) - ->setName(pht('Edit Paste')) - ->setIcon('fa-pencil') - ->setDisabled(!$can_edit) - ->setHref($this->getApplicationURI("edit/{$id}/"))); + $curtain->addAction( + id(new PhabricatorActionView()) + ->setName(pht('Edit Paste')) + ->setIcon('fa-pencil') + ->setDisabled(!$can_edit) + ->setHref($edit_uri)); if ($paste->isArchived()) { - $action_list->addAction( + $curtain->addAction( id(new PhabricatorActionView()) - ->setName(pht('Activate Paste')) - ->setIcon('fa-check') - ->setDisabled(!$can_edit) - ->setWorkflow($can_edit) - ->setHref($this->getApplicationURI("archive/{$id}/"))); + ->setName(pht('Activate Paste')) + ->setIcon('fa-check') + ->setDisabled(!$can_edit) + ->setWorkflow($can_edit) + ->setHref($archive_uri)); } else { - $action_list->addAction( + $curtain->addAction( id(new PhabricatorActionView()) - ->setName(pht('Archive Paste')) - ->setIcon('fa-ban') - ->setDisabled(!$can_edit) - ->setWorkflow($can_edit) - ->setHref($this->getApplicationURI("archive/{$id}/"))); + ->setName(pht('Archive Paste')) + ->setIcon('fa-ban') + ->setDisabled(!$can_edit) + ->setWorkflow($can_edit) + ->setHref($archive_uri)); } - $action_list->addAction( - id(new PhabricatorActionView()) - ->setName(pht('View Raw File')) - ->setIcon('fa-file-text-o') - ->setHref($this->getApplicationURI("raw/{$id}/"))); + $curtain->addAction( + id(new PhabricatorActionView()) + ->setName(pht('View Raw File')) + ->setIcon('fa-file-text-o') + ->setHref($raw_uri)); - return $action_list; + return $curtain; } @@ -191,32 +183,4 @@ final class PhabricatorPasteViewController extends PhabricatorPasteController { ->setContent($content); } - private function buildPropertyView( - PhabricatorPaste $paste, - array $child_phids) { - $viewer = $this->getViewer(); - - $properties = id(new PHUIPropertyListView()) - ->setUser($viewer) - ->setObject($paste); - - if ($paste->getParentPHID()) { - $properties->addProperty( - pht('Forked From'), - $viewer->renderHandle($paste->getParentPHID())); - } - - if ($child_phids) { - $properties->addProperty( - pht('Forks'), - $viewer->renderHandleList($child_phids)); - } - - $descriptions = PhabricatorPolicyQuery::renderPolicyDescriptions( - $viewer, - $paste); - - return $properties; - } - } diff --git a/src/applications/project/engineextension/PhabricatorProjectsCurtainExtension.php b/src/applications/project/engineextension/PhabricatorProjectsCurtainExtension.php new file mode 100644 index 0000000000..cc394c2283 --- /dev/null +++ b/src/applications/project/engineextension/PhabricatorProjectsCurtainExtension.php @@ -0,0 +1,91 @@ +getViewer(); + + $project_phids = PhabricatorEdgeQuery::loadDestinationPHIDs( + $object->getPHID(), + PhabricatorProjectObjectHasProjectEdgeType::EDGECONST); + + $has_projects = (bool)$project_phids; + $project_phids = array_reverse($project_phids); + $handles = $viewer->loadHandles($project_phids); + + // If this object can appear on boards, build the workboard annotations. + // Some day, this might be a generic interface. For now, only tasks can + // appear on boards. + $can_appear_on_boards = ($object instanceof ManiphestTask); + + $annotations = array(); + if ($has_projects && $can_appear_on_boards) { + $engine = id(new PhabricatorBoardLayoutEngine()) + ->setViewer($viewer) + ->setBoardPHIDs($project_phids) + ->setObjectPHIDs(array($object->getPHID())) + ->executeLayout(); + + // TDOO: Generalize this UI and move it out of Maniphest. + require_celerity_resource('maniphest-task-summary-css'); + + foreach ($project_phids as $project_phid) { + $handle = $handles[$project_phid]; + + $columns = $engine->getObjectColumns( + $project_phid, + $object->getPHID()); + + $annotation = array(); + foreach ($columns as $column) { + $project_id = $column->getProject()->getID(); + + $column_name = pht('(%s)', $column->getDisplayName()); + $column_link = phutil_tag( + 'a', + array( + 'href' => "/project/board/{$project_id}/", + 'class' => 'maniphest-board-link', + ), + $column_name); + + $annotation[] = $column_link; + } + + if ($annotation) { + $annotations[$project_phid] = array( + ' ', + phutil_implode_html(', ', $annotation), + ); + } + } + + } + + if ($has_projects) { + $list = id(new PHUIHandleTagListView()) + ->setHandles($handles) + ->setAnnotations($annotations) + ->setShowHovercards(true); + } else { + $list = phutil_tag('em', array(), pht('None')); + } + + return $this->newPanel() + ->setHeaderText(pht('Projects')) + ->setOrder(10000) + ->appendChild($list); + } + +} diff --git a/src/applications/subscriptions/engineextension/PhabricatorSubscriptionsCurtainExtension.php b/src/applications/subscriptions/engineextension/PhabricatorSubscriptionsCurtainExtension.php new file mode 100644 index 0000000000..6467141de2 --- /dev/null +++ b/src/applications/subscriptions/engineextension/PhabricatorSubscriptionsCurtainExtension.php @@ -0,0 +1,39 @@ +getViewer(); + $object_phid = $object->getPHID(); + + $subscriber_phids = PhabricatorSubscribersQuery::loadSubscribersForPHID( + $object_phid); + + $handles = $viewer->loadHandles($subscriber_phids); + + // TODO: This class can't accept a HandleList yet. + $handles = iterator_to_array($handles); + + $susbscribers_view = id(new SubscriptionListStringBuilder()) + ->setObjectPHID($object_phid) + ->setHandles($handles) + ->buildPropertyString(); + + return $this->newPanel() + ->setHeaderText(pht('Subscribers')) + ->setOrder(20000) + ->appendChild($susbscribers_view); + } + +} diff --git a/src/applications/tokens/engineextension/PhabricatorTokensCurtainExtension.php b/src/applications/tokens/engineextension/PhabricatorTokensCurtainExtension.php new file mode 100644 index 0000000000..1d1ca2551c --- /dev/null +++ b/src/applications/tokens/engineextension/PhabricatorTokensCurtainExtension.php @@ -0,0 +1,67 @@ +getViewer(); + + $tokens_given = id(new PhabricatorTokenGivenQuery()) + ->setViewer($viewer) + ->withObjectPHIDs(array($object->getPHID())) + ->execute(); + if (!$tokens_given) { + return null; + } + + $author_phids = mpull($tokens_given, 'getAuthorPHID'); + $handles = $viewer->loadHandles($author_phids); + + Javelin::initBehavior('phabricator-tooltips'); + + $list = array(); + foreach ($tokens_given as $token_given) { + $token = $token_given->getToken(); + + $aural = javelin_tag( + 'span', + array( + 'aural' => true, + ), + pht( + '"%s" token, awarded by %s.', + $token->getName(), + $handles[$token_given->getAuthorPHID()]->getName())); + + $list[] = javelin_tag( + 'span', + array( + 'sigil' => 'has-tooltip', + 'class' => 'token-icon', + 'meta' => array( + 'tip' => $handles[$token_given->getAuthorPHID()]->getName(), + ), + ), + array( + $aural, + $token->renderIcon(), + )); + } + + return $this->newPanel() + ->setHeaderText(pht('Tokens')) + ->setOrder(30000) + ->appendChild($list); + } + +} diff --git a/src/applications/tokens/query/PhabricatorTokenGivenQuery.php b/src/applications/tokens/query/PhabricatorTokenGivenQuery.php index 224efff198..b4a86428c1 100644 --- a/src/applications/tokens/query/PhabricatorTokenGivenQuery.php +++ b/src/applications/tokens/query/PhabricatorTokenGivenQuery.php @@ -58,10 +58,12 @@ final class PhabricatorTokenGivenQuery } protected function willFilterPage(array $results) { + $viewer = $this->getViewer(); + $object_phids = mpull($results, 'getObjectPHID'); $objects = id(new PhabricatorObjectQuery()) - ->setViewer($this->getViewer()) + ->setViewer($viewer) ->withPHIDs($object_phids) ->execute(); $objects = mpull($objects, null, 'getPHID'); @@ -80,6 +82,31 @@ final class PhabricatorTokenGivenQuery unset($results[$key]); } + if (!$results) { + return $results; + } + + $token_phids = mpull($results, 'getTokenPHID'); + + $tokens = id(new PhabricatorTokenQuery()) + ->setViewer($viewer) + ->withPHIDs($token_phids) + ->execute(); + $tokens = mpull($tokens, null, 'getPHID'); + + foreach ($results as $key => $result) { + $token_phid = $result->getTokenPHID(); + + $token = idx($tokens, $token_phid); + if (!$token) { + $this->didRejectResult($result); + unset($results[$key]); + continue; + } + + $result->attachToken($token); + } + return $results; } diff --git a/src/applications/tokens/storage/PhabricatorTokenGiven.php b/src/applications/tokens/storage/PhabricatorTokenGiven.php index 59ffc819d1..44db1baaf5 100644 --- a/src/applications/tokens/storage/PhabricatorTokenGiven.php +++ b/src/applications/tokens/storage/PhabricatorTokenGiven.php @@ -8,6 +8,7 @@ final class PhabricatorTokenGiven extends PhabricatorTokenDAO protected $tokenPHID; private $object = self::ATTACHABLE; + private $token = self::ATTACHABLE; protected function getConfiguration() { return array( @@ -35,6 +36,15 @@ final class PhabricatorTokenGiven extends PhabricatorTokenDAO return $this->assertAttached($this->object); } + public function attachToken(PhabricatorToken $token) { + $this->token = $token; + return $this; + } + + public function getToken() { + return $this->assertAttached($this->token); + } + public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, diff --git a/src/view/extension/PHUICurtainExtension.php b/src/view/extension/PHUICurtainExtension.php new file mode 100644 index 0000000000..7f7693cbac --- /dev/null +++ b/src/view/extension/PHUICurtainExtension.php @@ -0,0 +1,124 @@ +viewer = $viewer; + return $this; + } + + public function getViewer() { + return $this->viewer; + } + + abstract public function shouldEnableForObject($object); + abstract public function getExtensionApplication(); + + public function buildCurtainPanels($object) { + $panel = $this->buildCurtainPanel($object); + + if ($panel !== null) { + return array($panel); + } + + return array(); + } + + public function buildCurtainPanel($object) { + throw new PhutilMethodNotImplementedException(); + } + + final public function getExtensionKey() { + return $this->getPhobjectClassConstant('EXTENSIONKEY'); + } + + final public static function getAllExtensions() { + return id(new PhutilClassMapQuery()) + ->setAncestorClass(__CLASS__) + ->setUniqueMethod('getExtensionKey') + ->execute(); + } + + protected function newPanel() { + return new PHUICurtainPanelView(); + } + + final public static function buildExtensionPanels( + PhabricatorUser $viewer, + $object) { + + $extensions = self::getAllExtensions(); + foreach ($extensions as $extension) { + $extension->setViewer($viewer); + } + + foreach ($extensions as $key => $extension) { + $application = $extension->getExtensionApplication(); + if (!($application instanceof PhabricatorApplication)) { + throw new Exception( + pht( + 'Curtain extension ("%s", of class "%s") did not return an '. + 'application from method "%s". This method must return an '. + 'object of class "%s".', + $key, + get_class($extension), + 'getExtensionApplication()', + 'PhabricatorApplication')); + } + + $has_application = PhabricatorApplication::isClassInstalledForViewer( + get_class($application), + $viewer); + + if (!$has_application) { + unset($extensions[$key]); + } + } + + foreach ($extensions as $key => $extension) { + if (!$extension->shouldEnableForObject($object)) { + unset($extensions[$key]); + } + } + + $result = array(); + + foreach ($extensions as $key => $extension) { + $panels = $extension->buildCurtainPanels($object); + if (!is_array($panels)) { + throw new Exception( + pht( + 'Curtain extension ("%s", of class "%s") did not return a list of '. + 'curtain panels from method "%s". This method must return an '. + 'array, and each value in the array must be a "%s" object.', + $key, + get_class($extension), + 'buildCurtainPanels()', + 'PHUICurtainPanelView')); + } + + foreach ($panels as $panel_key => $panel) { + if (!($panel instanceof PHUICurtainPanelView)) { + throw new Exception( + pht( + 'Curtain extension ("%s", of class "%s") returned a list of '. + 'curtain panels from "%s" that contains an invalid value: '. + 'a value (with key "%s") is not an object of class "%s". '. + 'Each item in the returned array must be a panel.', + $key, + get_class($extension), + 'buildCurtainPanels()', + $panel_key, + 'PHUICurtainPanelView')); + } + + $result[] = $panel; + } + } + + return $result; + } + +} diff --git a/src/view/layout/PHUICurtainPanelView.php b/src/view/layout/PHUICurtainPanelView.php new file mode 100644 index 0000000000..238e56a374 --- /dev/null +++ b/src/view/layout/PHUICurtainPanelView.php @@ -0,0 +1,63 @@ +headerText = $header_text; + return $this; + } + + public function getHeaderText() { + return $this->headerText; + } + + public function setOrder($order) { + $this->order = $order; + return $this; + } + + public function getOrder() { + return $this->order; + } + + public function getOrderVector() { + return id(new PhutilSortVector()) + ->addInt($this->getOrder()); + } + + protected function getTagAttributes() { + return array( + 'class' => 'phui-curtain-panel', + ); + } + + protected function getTagContent() { + $header = null; + + $header_text = $this->getHeaderText(); + if (strlen($header_text)) { + $header = phutil_tag( + 'div', + array( + 'class' => 'phui-curtain-panel-header', + ), + $header_text); + } + + $body = phutil_tag( + 'div', + array( + 'class' => 'phui-curtain-panel-body', + ), + $this->renderChildren()); + + return array( + $header, + $body, + ); + } + +} diff --git a/src/view/layout/PHUICurtainView.php b/src/view/layout/PHUICurtainView.php new file mode 100644 index 0000000000..6d2388802b --- /dev/null +++ b/src/view/layout/PHUICurtainView.php @@ -0,0 +1,52 @@ +getActionList()->addAction($action); + return $this; + } + + public function addPanel(PHUICurtainPanelView $curtain_panel) { + $this->panels[] = $curtain_panel; + return $this; + } + + public function setActionList(PhabricatorActionListView $action_list) { + $this->actionList = $action_list; + return $this; + } + + public function getActionList() { + return $this->actionList; + } + + protected function canAppendChild() { + return false; + } + + protected function getTagContent() { + $action_list = $this->actionList; + + require_celerity_resource('phui-curtain-view-css'); + + $panels = $this->renderPanels(); + + return id(new PHUIObjectBoxView()) + ->appendChild($action_list) + ->appendChild($panels) + ->addClass('phui-two-column-properties'); + } + + private function renderPanels() { + $panels = $this->panels; + $panels = msortv($panels, 'getOrderVector'); + + return $panels; + } + + +} diff --git a/src/view/phui/PHUITwoColumnView.php b/src/view/phui/PHUITwoColumnView.php index fb27a1a50d..1977ddce5e 100644 --- a/src/view/phui/PHUITwoColumnView.php +++ b/src/view/phui/PHUITwoColumnView.php @@ -11,6 +11,7 @@ final class PHUITwoColumnView extends AphrontTagView { private $propertySection = array(); private $actionList; private $propertyList; + private $curtain; const DISPLAY_LEFT = 'phui-side-column-left'; const DISPLAY_RIGHT = 'phui-side-column-right'; @@ -50,6 +51,15 @@ final class PHUITwoColumnView extends AphrontTagView { return $this; } + public function setCurtain(PHUICurtainView $curtain) { + $this->curtain = $curtain; + return $this; + } + + public function getCurtain() { + return $this->curtain; + } + public function setFluid($fluid) { $this->fluid = $fluid; return $this; @@ -98,9 +108,17 @@ final class PHUITwoColumnView extends AphrontTagView { $header = null; if ($this->header) { - if ($this->actionList) { - $this->header->setActionList($this->actionList); + $curtain = $this->getCurtain(); + if ($curtain) { + $action_list = $curtain->getActionList(); + } else { + $action_list = $this->actionList; } + + if ($action_list) { + $this->header->setActionList($action_list); + } + $header = phutil_tag_div( 'phui-two-column-header', $this->header); } @@ -166,6 +184,8 @@ final class PHUITwoColumnView extends AphrontTagView { ->addClass('phui-two-column-properties'); } + $curtain = $this->getCurtain(); + return phutil_tag( 'div', array( @@ -173,6 +193,7 @@ final class PHUITwoColumnView extends AphrontTagView { ), array( $properties, + $curtain, $this->sideColumn, )); } diff --git a/webroot/rsrc/css/phui/phui-curtain-view.css b/webroot/rsrc/css/phui/phui-curtain-view.css new file mode 100644 index 0000000000..0c507c8e84 --- /dev/null +++ b/webroot/rsrc/css/phui/phui-curtain-view.css @@ -0,0 +1,22 @@ +/** + * @provides phui-curtain-view-css + */ + +.phui-curtain-panel { + margin: 4px; + padding: 4px 0; +} + +.device-desktop .phui-curtain-panel { + border-top: 1px solid {$lightblueborder}; +} + +.phui-curtain-panel-header { + padding: 4px 0 0; + color: {$bluetext}; + font-weight: bold; +} + +.phui-curtain-panel-body { + padding: 4px 0 0; +}