diff --git a/src/__celerity_resource_map__.php b/src/__celerity_resource_map__.php index ee31b216d4..e986594e78 100644 --- a/src/__celerity_resource_map__.php +++ b/src/__celerity_resource_map__.php @@ -2973,6 +2973,15 @@ celerity_register_resource_map(array( ), 'disk' => '/rsrc/css/layout/phabricator-header-view.css', ), + 'phabricator-hovercard-view-css' => + array( + 'uri' => '/res/061e66df/rsrc/css/layout/phabricator-hovercard-view.css', + 'type' => 'css', + 'requires' => + array( + ), + 'disk' => '/rsrc/css/layout/phabricator-hovercard-view.css', + ), 'phabricator-jump-nav' => array( 'uri' => '/res/745c0e89/rsrc/css/application/directory/phabricator-jump-nav.css', diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index d7542b179c..f5d37cb1f5 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -1001,6 +1001,8 @@ phutil_register_library_map(array( 'PhabricatorHeaderView' => 'view/layout/PhabricatorHeaderView.php', 'PhabricatorHelpController' => 'applications/help/controller/PhabricatorHelpController.php', 'PhabricatorHelpKeyboardShortcutController' => 'applications/help/controller/PhabricatorHelpKeyboardShortcutController.php', + 'PhabricatorHovercardExample' => 'applications/uiexample/examples/PhabricatorHovercardExample.php', + 'PhabricatorHovercardView' => 'view/widget/hovercard/PhabricatorHovercardView.php', 'PhabricatorIRCBot' => 'infrastructure/daemon/bot/PhabricatorIRCBot.php', 'PhabricatorIRCProtocolAdapter' => 'infrastructure/daemon/bot/adapter/PhabricatorIRCProtocolAdapter.php', 'PhabricatorIRCProtocolHandler' => 'infrastructure/daemon/bot/handler/PhabricatorIRCProtocolHandler.php', @@ -2677,6 +2679,8 @@ phutil_register_library_map(array( 'PhabricatorHeaderView' => 'AphrontView', 'PhabricatorHelpController' => 'PhabricatorController', 'PhabricatorHelpKeyboardShortcutController' => 'PhabricatorHelpController', + 'PhabricatorHovercardExample' => 'PhabricatorUIExample', + 'PhabricatorHovercardView' => 'AphrontView', 'PhabricatorIRCBot' => 'PhabricatorDaemon', 'PhabricatorIRCProtocolAdapter' => 'PhabricatorBaseProtocolAdapter', 'PhabricatorIRCProtocolHandler' => 'PhabricatorBotHandler', diff --git a/src/applications/uiexample/examples/PhabricatorHovercardExample.php b/src/applications/uiexample/examples/PhabricatorHovercardExample.php new file mode 100644 index 0000000000..6ceb8083f7 --- /dev/null +++ b/src/applications/uiexample/examples/PhabricatorHovercardExample.php @@ -0,0 +1,77 @@ +PhabricatorHovercardView to render '. + 'hovercards. Aren\'t I genius?'); + } + + public function renderExample() { + $request = $this->getRequest(); + $user = $request->getUser(); + + $elements = array(); + + $diff_handle = $this->createBasicDummyHandle( + "Introduce cooler Differential Revisions", + PhabricatorPHIDConstants::PHID_TYPE_DREV); + + $panel = $this->createPanel("Differential Hovercard"); + $panel->appendChild(id(new PhabricatorHovercardView()) + ->setObjectHandle($diff_handle) + ->addField(pht('Author'), $user->getUsername()) + ->addField(pht('Updated'), phabricator_datetime(time(), $user)) + ->addAction(pht('Subscribe'), '/dev/random') + ->setUser($user)); + $elements[] = $panel; + + $task_handle = $this->createBasicDummyHandle( + "Improve Mobile Experience for Phabricator", + PhabricatorPHIDConstants::PHID_TYPE_TASK); + + $tag = id(new PhabricatorTagView()) + ->setType(PhabricatorTagView::TYPE_STATE) + ->setBackgroundColor(PhabricatorTagView::COLOR_BLACK) + ->setName('Abandoned (Really)'); + $panel = $this->createPanel("Maniphest Hovercard"); + $panel->appendChild(id(new PhabricatorHovercardView()) + ->setObjectHandle($task_handle) + ->setUser($user) + ->addField(pht('Assigned to'), $user->getUsername()) + ->addField(pht('Dependent Tasks'), 'T123, T124, T125') + ->addAction(pht('Subscribe'), '/dev/random') + ->addAction(pht('Create Subtask'), '/dev/urandom') + ->addTag($tag)); + $elements[] = $panel; + + $user_handle = $this->createBasicDummyHandle( + 'gwashington', + PhabricatorPHIDConstants::PHID_TYPE_USER, + 'George Washington'); + $user_handle->setImageURI( + celerity_get_resource_uri('/rsrc/image/people/washington.png')); + $panel = $this->createPanel("Whatevery Hovercard"); + $panel->appendChild(id(new PhabricatorHovercardView()) + ->setObjectHandle($user_handle) + ->addField(pht('Status'), 'Available') + ->addField(pht('Member since'), '30. February 1750') + ->addAction(pht('Start a Conpherence'), '/dev/null') + ->setUser($user)); + $elements[] = $panel; + + return phutil_implode_html("", $elements); + } + + private function createPanel($header) { + $panel = new AphrontPanelView(); + $panel->setNoBackground(); + $panel->setHeader($header); + return $panel; + } + +} diff --git a/src/applications/uiexample/examples/PhabricatorUIExample.php b/src/applications/uiexample/examples/PhabricatorUIExample.php index 6688e489b3..7889b1ed86 100644 --- a/src/applications/uiexample/examples/PhabricatorUIExample.php +++ b/src/applications/uiexample/examples/PhabricatorUIExample.php @@ -17,4 +17,30 @@ abstract class PhabricatorUIExample { abstract public function getDescription(); abstract public function renderExample(); + protected function createBasicDummyHandle($name, $type, $fullname = null, + $uri = null) { + + $id = mt_rand(15, 9999); + $handle = new PhabricatorObjectHandle(); + $handle->setAlternateID(mt_rand(15, 9999)); + $handle->setName($name); + $handle->setType($type); + $handle->setPHID(PhabricatorPHID::generateNewPHID($type)); + + if ($fullname) { + $handle->setFullName($fullname); + } else { + $handle->setFullName(sprintf('%s%d: %s', + substr($type, 0, 1), + $id, + $name)); + } + + if ($uri) { + $handle->setURI($uri); + } + + return $handle; + } + } diff --git a/src/view/widget/hovercard/PhabricatorHovercardView.php b/src/view/widget/hovercard/PhabricatorHovercardView.php new file mode 100644 index 0000000000..86b482fd43 --- /dev/null +++ b/src/view/widget/hovercard/PhabricatorHovercardView.php @@ -0,0 +1,170 @@ +handle = $handle; + return $this; + } + + public function setTitle($title) { + $this->title = $title; + return $this; + } + + public function setDetail($detail) { + $this->detail = $detail; + return $this; + } + + public function addField($label, $value) { + $this->fields[] = array( + 'label' => $label, + 'value' => $value, + ); + return $this; + } + + public function addAction($label, $uri, $workflow = false) { + $this->actions[] = array( + 'label' => $label, + 'uri' => $uri, + 'workflow' => $workflow, + ); + return $this; + } + + public function addTag(PhabricatorTagView $tag) { + $this->tags[] = $tag; + return $this; + } + + public function setColor($color) { + $this->color = $color; + return $this; + } + + public function render() { + $handle = $this->handle; + $user = $this->getUser(); + + $id = $handle->getAlternateID(); + $type = $handle->getType(); + + require_celerity_resource("phabricator-hovercard-view-css"); + + $title = array(); + if ($this->tags) { + $title[] = ' '; + $title[] = phutil_tag( + 'span', + array( + 'class' => 'phabricator-hovercard-tags', + ), + array_interleave(' ', $this->tags)); + } + $title[] = pht("%s: %s", $handle->getTypeName(), substr($type, 0, 1) . $id); + + $body = array(); + if ($this->detail) { + $body[] = hsprintf('%s', $this->detail); + } else { + // Fallback for object handles + $body[] = hsprintf('%s', $handle->getFullName()); + } + + foreach ($this->fields as $field) { + $body[] = hsprintf('%s: %s', + $field['label'], $field['value']); + } + + $body = phutil_implode_html(phutil_tag('br'), $body); + + if ($handle->getImageURI()) { + // Probably a user, we don't need to assume something else + // "Prepend" the image by appending $body + $body = phutil_tag( + 'div', + array( + 'class' => 'profile-header-picture-frame', + 'style' => 'background-image: url('.$handle->getImageURI().');', + ), + '') + ->appendHTML($body); + } + + $buttons = array(); + + foreach ($this->actions as $action) { + $options = array( + 'class' => 'button grey', + 'href' => $action['uri'], + ); + + if ($action['workflow']) { + $options['sigil'] = 'workflow'; + $buttons[] = javelin_tag( + 'a', + $options, + $action['label']); + } else { + $buttons[] = phutil_tag( + 'a', + $options, + $action['label']); + } + } + + $tail = null; + if ($buttons) { + $tail = phutil_tag('div', + array('class' => 'phabricator-hovercard-tail'), + $buttons); + } + + // Assemble container + // TODO: Add color support + $content = hsprintf( + '%s%s%s', + phutil_tag('div', array('class' => 'phabricator-hovercard-head'), $title), + phutil_tag('div', array('class' => 'phabricator-hovercard-body'), $body), + $tail); + + $hovercard = phutil_tag("div", + array( + "class" => "phabricator-hovercard-container", + ), + $content); + + // Wrap for thick border + // and later the tip at the bottom + return phutil_tag('div', + array( + 'class' => 'phabricator-hovercard-wrapper', + ), + $hovercard); + } + +} diff --git a/webroot/rsrc/css/layout/phabricator-hovercard-view.css b/webroot/rsrc/css/layout/phabricator-hovercard-view.css new file mode 100644 index 0000000000..2aeb6499a3 --- /dev/null +++ b/webroot/rsrc/css/layout/phabricator-hovercard-view.css @@ -0,0 +1,69 @@ +/** + * @provides phabricator-hovercard-view-css + */ + +.phabricator-hovercard-wrapper { + border-radius: 4px; + width: 400px; + margin: auto; + padding: 4px; + background-color: #cccccc; +} + +.device-phone .phabricator-hovercard-wrapper { + width: 300px; +} + +.phabricator-hovercard-container { + border-radius: 3px; + border: 1px solid #666666; +} + +.phabricator-hovercard-head { + padding: 10px 15px; + font-weight: bold; + font-size: 15px; + white-space: nowrap; + color: white; + text-shadow: 0 1px 0 #333333; + font-weight: bold; + border-top-left-radius: 2px; + border-top-right-radius: 2px; + overflow: hidden; + + background-color: #559911; +} + +.phabricator-hovercard-tags { + font-size: 13px; + float: right; + white-space: normal; +} + +.phabricator-hovercard-body { + padding: 15px; + background-color: white; + border-bottom-left-radius: 2px; + border-bottom-right-radius: 2px; +} + +.phabricator-hovercard-body .profile-header-picture-frame { + float: left; + margin: 0; + margin-right: 10px; + margin-bottom: 5px; + width: 50px; + height: 50px; +} + +.phabricator-hovercard-tail { + padding: 3px 2px; + background-color: #eeeeee; + border-bottom-left-radius: 2px; + border-bottom-right-radius: 2px; +} + +.phabricator-hovercard-tail button, +.phabricator-hovercard-tail a.button { + margin: 3px; +}