@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator

Render user hovercards with context information about their ability to see the context object

Summary:
Ref T13602. When rendering a user hovercard, pass the object on which the reference appears. If the user can't see the object, make it clear on the hovecard.

Restyle the "nopermission" markup in mentions to make it more obvious what the style means: instead of grey text, use red with an explicit icon.

Test Plan: {F8430398}

Maniphest Tasks: T13602

Differential Revision: https://secure.phabricator.com/D21554

+212 -69
+37 -37
resources/celerity/map.php
··· 9 9 'names' => array( 10 10 'conpherence.pkg.css' => '0e3cf785', 11 11 'conpherence.pkg.js' => '020aebcf', 12 - 'core.pkg.css' => '970b3ceb', 13 - 'core.pkg.js' => '2fe70e3d', 12 + 'core.pkg.css' => '7cb6808c', 13 + 'core.pkg.js' => '079198f6', 14 14 'dark-console.pkg.js' => '187792c2', 15 15 'differential.pkg.css' => '5c459f92', 16 16 'differential.pkg.js' => '5080baf4', ··· 101 101 'rsrc/css/application/policy/policy-transaction-detail.css' => 'c02b8384', 102 102 'rsrc/css/application/policy/policy.css' => 'ceb56a08', 103 103 'rsrc/css/application/ponder/ponder-view.css' => '05a09d0a', 104 - 'rsrc/css/application/project/project-card-view.css' => '4e7371cd', 104 + 'rsrc/css/application/project/project-card-view.css' => 'a9f2c2dd', 105 105 'rsrc/css/application/project/project-triggers.css' => 'cd9c8bb9', 106 106 'rsrc/css/application/project/project-view.css' => '567858b3', 107 107 'rsrc/css/application/releeph/releeph-core.css' => 'f81ff2db', ··· 114 114 'rsrc/css/application/tokens/tokens.css' => 'ce5a50bd', 115 115 'rsrc/css/application/uiexample/example.css' => 'b4795059', 116 116 'rsrc/css/core/core.css' => 'b3ebd90d', 117 - 'rsrc/css/core/remarkup.css' => '24d48a73', 117 + 'rsrc/css/core/remarkup.css' => '5baa3bd9', 118 118 'rsrc/css/core/syntax.css' => '548567f6', 119 119 'rsrc/css/core/z-index.css' => 'ac3bfcd4', 120 120 'rsrc/css/diviner/diviner-shared.css' => '4bd263b0', ··· 181 181 'rsrc/css/phui/phui-segment-bar-view.css' => '5166b370', 182 182 'rsrc/css/phui/phui-spacing.css' => 'b05cadc3', 183 183 'rsrc/css/phui/phui-status.css' => 'e5ff8be0', 184 - 'rsrc/css/phui/phui-tag-view.css' => '8519160a', 184 + 'rsrc/css/phui/phui-tag-view.css' => 'fb811341', 185 185 'rsrc/css/phui/phui-timeline-view.css' => '2d32d7a9', 186 186 'rsrc/css/phui/phui-two-column-view.css' => 'f96d319f', 187 187 'rsrc/css/phui/workboards/phui-workboard-color.css' => 'e86de308', ··· 460 460 'rsrc/js/core/DraggableList.js' => '0169e425', 461 461 'rsrc/js/core/Favicon.js' => '7930776a', 462 462 'rsrc/js/core/FileUpload.js' => 'ab85e184', 463 - 'rsrc/js/core/Hovercard.js' => 'd9d29a5f', 464 - 'rsrc/js/core/HovercardList.js' => '10a5f4bf', 463 + 'rsrc/js/core/Hovercard.js' => '6199f752', 464 + 'rsrc/js/core/HovercardList.js' => 'de4b4919', 465 465 'rsrc/js/core/KeyboardShortcut.js' => '1a844c06', 466 466 'rsrc/js/core/KeyboardShortcutManager.js' => '81debc48', 467 467 'rsrc/js/core/MultirowRowManager.js' => '5b54c823', ··· 486 486 'rsrc/js/core/behavior-global-drag-and-drop.js' => '1cab0e9a', 487 487 'rsrc/js/core/behavior-high-security-warning.js' => 'dae2d55b', 488 488 'rsrc/js/core/behavior-history-install.js' => '6a1583a8', 489 - 'rsrc/js/core/behavior-hovercard.js' => '3f446c72', 489 + 'rsrc/js/core/behavior-hovercard.js' => '183738e6', 490 490 'rsrc/js/core/behavior-keyboard-pager.js' => '1325b731', 491 491 'rsrc/js/core/behavior-keyboard-shortcuts.js' => '42c44e8b', 492 492 'rsrc/js/core/behavior-lightbox-attachments.js' => 'c7e748bf', ··· 671 671 'javelin-behavior-pholio-mock-view' => '5aa1544e', 672 672 'javelin-behavior-phui-dropdown-menu' => '5cf0501a', 673 673 'javelin-behavior-phui-file-upload' => 'e150bd50', 674 - 'javelin-behavior-phui-hovercards' => '3f446c72', 674 + 'javelin-behavior-phui-hovercards' => '183738e6', 675 675 'javelin-behavior-phui-selectable-list' => 'b26a41e4', 676 676 'javelin-behavior-phui-submenu' => 'b5e9bff9', 677 677 'javelin-behavior-phui-tab-group' => '242aa08b', ··· 807 807 'phabricator-object-selector-css' => 'ee77366f', 808 808 'phabricator-phtize' => '2f1db1ed', 809 809 'phabricator-prefab' => '5793d835', 810 - 'phabricator-remarkup-css' => '24d48a73', 810 + 'phabricator-remarkup-css' => '5baa3bd9', 811 811 'phabricator-search-results-css' => '9ea70ace', 812 812 'phabricator-shaped-request' => '995f5102', 813 813 'phabricator-slowvote-css' => '1694baed', ··· 859 859 'phui-formation-view-css' => 'd2dec8ed', 860 860 'phui-head-thing-view-css' => 'd7f293df', 861 861 'phui-header-view-css' => '36c86a58', 862 - 'phui-hovercard' => 'd9d29a5f', 863 - 'phui-hovercard-list' => '10a5f4bf', 862 + 'phui-hovercard' => '6199f752', 863 + 'phui-hovercard-list' => 'de4b4919', 864 864 'phui-hovercard-view-css' => '6ca90fa0', 865 865 'phui-icon-set-selector-css' => '7aa5f3ec', 866 866 'phui-icon-view-css' => '4cbc684a', ··· 886 886 'phui-segment-bar-view-css' => '5166b370', 887 887 'phui-spacing-css' => 'b05cadc3', 888 888 'phui-status-list-view-css' => 'e5ff8be0', 889 - 'phui-tag-view-css' => '8519160a', 889 + 'phui-tag-view-css' => 'fb811341', 890 890 'phui-theme-css' => '35883b37', 891 891 'phui-timeline-view-css' => '2d32d7a9', 892 892 'phui-two-column-view-css' => 'f96d319f', ··· 908 908 'policy-edit-css' => '8794e2ed', 909 909 'policy-transaction-detail-css' => 'c02b8384', 910 910 'ponder-view-css' => '05a09d0a', 911 - 'project-card-view-css' => '4e7371cd', 911 + 'project-card-view-css' => 'a9f2c2dd', 912 912 'project-triggers-css' => 'cd9c8bb9', 913 913 'project-view-css' => '567858b3', 914 914 'releeph-core' => 'f81ff2db', ··· 1025 1025 'javelin-workflow', 1026 1026 'phuix-icon-view', 1027 1027 ), 1028 - '10a5f4bf' => array( 1029 - 'javelin-install', 1030 - 'javelin-dom', 1031 - 'javelin-vector', 1032 - 'javelin-request', 1033 - 'javelin-uri', 1034 - 'phui-hovercard', 1035 - ), 1036 1028 '111bfd2d' => array( 1037 1029 'javelin-install', 1038 1030 ), ··· 1047 1039 'javelin-stratcom', 1048 1040 'javelin-util', 1049 1041 ), 1042 + '183738e6' => array( 1043 + 'javelin-behavior', 1044 + 'javelin-behavior-device', 1045 + 'javelin-stratcom', 1046 + 'javelin-vector', 1047 + 'phui-hovercard', 1048 + 'phui-hovercard-list', 1049 + ), 1050 1050 '1a844c06' => array( 1051 1051 'javelin-install', 1052 1052 'javelin-util', ··· 1268 1268 'phabricator-phtize', 1269 1269 'phabricator-drag-and-drop-file-upload', 1270 1270 'phabricator-draggable-list', 1271 - ), 1272 - '3f446c72' => array( 1273 - 'javelin-behavior', 1274 - 'javelin-behavior-device', 1275 - 'javelin-stratcom', 1276 - 'javelin-vector', 1277 - 'phui-hovercard', 1278 - 'phui-hovercard-list', 1279 1271 ), 1280 1272 '407ee861' => array( 1281 1273 'javelin-behavior', ··· 1527 1519 ), 1528 1520 '60cd9241' => array( 1529 1521 'javelin-behavior', 1522 + ), 1523 + '6199f752' => array( 1524 + 'javelin-install', 1525 + 'javelin-dom', 1526 + 'javelin-vector', 1527 + 'javelin-request', 1528 + 'javelin-uri', 1530 1529 ), 1531 1530 '6337cf26' => array( 1532 1531 'javelin-behavior', ··· 2136 2135 'javelin-util', 2137 2136 'phabricator-shaped-request', 2138 2137 ), 2139 - 'd9d29a5f' => array( 2140 - 'javelin-install', 2141 - 'javelin-dom', 2142 - 'javelin-vector', 2143 - 'javelin-request', 2144 - 'javelin-uri', 2145 - ), 2146 2138 'da15d3dc' => array( 2147 2139 'phui-oi-list-view-css', 2148 2140 ), ··· 2154 2146 'javelin-behavior', 2155 2147 'javelin-uri', 2156 2148 'phabricator-notification', 2149 + ), 2150 + 'de4b4919' => array( 2151 + 'javelin-install', 2152 + 'javelin-dom', 2153 + 'javelin-vector', 2154 + 'javelin-request', 2155 + 'javelin-uri', 2156 + 'phui-hovercard', 2157 2157 ), 2158 2158 'e150bd50' => array( 2159 2159 'javelin-behavior',
+6 -2
src/applications/maniphest/controller/ManiphestTaskDetailController.php
··· 638 638 'href' => $commit->getURI(), 639 639 'sigil' => 'hovercard', 640 640 'meta' => array( 641 - 'hoverPHID' => $commit->getPHID(), 641 + 'hovercardSpec' => array( 642 + 'objectPHID' => $commit->getPHID(), 643 + ), 642 644 ), 643 645 ), 644 646 $commit->getSummary()); ··· 705 707 'href' => $revision->getURI(), 706 708 'sigil' => 'hovercard', 707 709 'meta' => array( 708 - 'hoverPHID' => $revision->getPHID(), 710 + 'hovercardSpec' => array( 711 + 'objectPHID' => $revision->getPHID(), 712 + ), 709 713 ), 710 714 ), 711 715 $revision->getTitle());
+4 -2
src/applications/people/engineextension/PeopleHovercardEngineExtension.php
··· 47 47 return; 48 48 } 49 49 50 + $is_exiled = $hovercard->getIsExiled(); 51 + 50 52 $user_card = id(new PhabricatorUserCardView()) 51 53 ->setProfile($user) 52 - ->setViewer($viewer); 54 + ->setViewer($viewer) 55 + ->setIsExiled($is_exiled); 53 56 54 57 $hovercard->appendChild($user_card); 55 - 56 58 } 57 59 58 60 }
+6 -2
src/applications/people/markup/PhabricatorMentionRemarkupRule.php
··· 110 110 111 111 if ($exists) { 112 112 $user = $actual_users[$username]; 113 - Javelin::initBehavior('phui-hovercards'); 114 113 115 114 // Check if the user has view access to the object she was mentioned in 116 115 if ($policy_object) { ··· 159 158 ->setName('@'.$user->getUserName()) 160 159 ->setHref($user_href); 161 160 161 + if ($context_object) { 162 + $tag->setContextObject($context_object); 163 + } 164 + 162 165 if ($user_can_not_view) { 163 - $tag->addClass('phabricator-remarkup-mention-nopermission'); 166 + $tag->setIcon('fa-eye-slash red'); 167 + $tag->setIsExiled(true); 164 168 } 165 169 166 170 if ($user->getIsDisabled()) {
+30 -2
src/applications/people/view/PhabricatorUserCardView.php
··· 5 5 private $profile; 6 6 private $viewer; 7 7 private $tag; 8 + private $isExiled; 8 9 9 10 public function setProfile(PhabricatorUser $profile) { 10 11 $this->profile = $profile; ··· 40 41 return array( 41 42 'class' => implode(' ', $classes), 42 43 ); 44 + } 45 + 46 + public function setIsExiled($is_exiled) { 47 + $this->isExiled = $is_exiled; 48 + return $this; 49 + } 50 + 51 + public function getIsExiled() { 52 + return $this->isExiled; 43 53 } 44 54 45 55 protected function getTagContent() { ··· 108 118 } 109 119 } 110 120 121 + if ($this->getIsExiled()) { 122 + $body[] = $this->addItem( 123 + 'fa-eye-slash red', 124 + pht('This user can not see this object.'), 125 + array( 126 + 'project-card-item-exiled', 127 + )); 128 + } 129 + 111 130 $classes[] = 'project-card-image'; 112 131 $image = phutil_tag( 113 132 'img', ··· 160 179 return $card; 161 180 } 162 181 163 - private function addItem($icon, $value) { 182 + private function addItem($icon, $value, $classes = array()) { 183 + $classes[] = 'project-card-item'; 184 + 164 185 $icon = id(new PHUIIconView()) 165 186 ->addClass('project-card-item-icon') 166 187 ->setIcon($icon); 188 + 167 189 $text = phutil_tag( 168 190 'span', 169 191 array( 170 192 'class' => 'project-card-item-text', 171 193 ), 172 194 $value); 173 - return phutil_tag_div('project-card-item', array($icon, $text)); 195 + 196 + return phutil_tag( 197 + 'div', 198 + array( 199 + 'class' => implode(' ', $classes), 200 + ), 201 + array($icon, $text)); 174 202 } 175 203 176 204 }
+3 -1
src/applications/phid/PhabricatorObjectHandle.php
··· 306 306 $attributes = array( 307 307 'sigil' => 'hovercard', 308 308 'meta' => array( 309 - 'hoverPHID' => $this->getPHID(), 309 + 'hovercardSpec' => array( 310 + 'objectPHID' => $this->getPHID(), 311 + ), 310 312 ), 311 313 ); 312 314
+44 -12
src/applications/search/controller/PhabricatorSearchHovercardController.php
··· 32 32 33 33 $object_phids = array(); 34 34 $handle_phids = array(); 35 + $context_phids = array(); 35 36 foreach ($cards as $card) { 36 37 $object_phid = idx($card, 'objectPHID'); 37 38 38 39 $handle_phids[] = $object_phid; 39 40 $object_phids[] = $object_phid; 41 + 42 + $context_phid = idx($card, 'contextPHID'); 43 + 44 + if ($context_phid) { 45 + $object_phids[] = $context_phid; 46 + $context_phids[] = $context_phid; 47 + } 40 48 } 41 49 42 50 $handles = id(new PhabricatorHandleQuery()) ··· 50 58 ->execute(); 51 59 $objects = mpull($objects, null, 'getPHID'); 52 60 61 + $context_objects = array_select_keys($objects, $context_phids); 62 + 63 + if ($context_objects) { 64 + PhabricatorPolicyFilterSet::loadHandleViewCapabilities( 65 + $viewer, 66 + $handles, 67 + $context_objects); 68 + } 69 + 53 70 $extensions = 54 71 PhabricatorHovercardEngineExtension::getAllEnabledExtensions(); 55 72 56 73 $extension_maps = array(); 57 - foreach ($extensions as $key => $extension) { 74 + foreach ($extensions as $extension_key => $extension) { 58 75 $extension->setViewer($viewer); 59 76 60 77 $extension_phids = array(); ··· 64 81 } 65 82 } 66 83 67 - $extension_maps[$key] = $extension_phids; 84 + $extension_maps[$extension_key] = $extension_phids; 68 85 } 69 86 70 87 $extension_data = array(); 71 - foreach ($extensions as $key => $extension) { 72 - $extension_phids = $extension_maps[$key]; 88 + foreach ($extensions as $extension_key => $extension) { 89 + $extension_phids = $extension_maps[$extension_key]; 73 90 if (!$extension_phids) { 74 - unset($extensions[$key]); 91 + unset($extensions[$extension_key]); 75 92 continue; 76 93 } 77 94 78 - $extension_data[$key] = $extension->willRenderHovercards( 95 + $extension_data[$extension_key] = $extension->willRenderHovercards( 79 96 array_select_keys($objects, $extension_phids)); 80 97 } 81 98 ··· 86 103 $handle = $handles[$object_phid]; 87 104 $object = idx($objects, $object_phid); 88 105 106 + $context_phid = idx($card, 'contextPHID'); 107 + if ($context_phid) { 108 + $context_object = idx($context_objects, $context_phid); 109 + } else { 110 + $context_object = null; 111 + } 112 + 89 113 $hovercard = id(new PHUIHovercardView()) 90 114 ->setUser($viewer) 91 115 ->setObjectHandle($handle); 92 116 117 + if ($context_object) { 118 + if ($handle->hasCapabilities()) { 119 + if (!$handle->hasViewCapability($context_object)) { 120 + $hovercard->setIsExiled(true); 121 + } 122 + } 123 + } 124 + 93 125 if ($object) { 94 126 $hovercard->setObject($object); 95 127 96 - foreach ($extension_maps as $key => $extension_phids) { 97 - if (isset($extension_phids[$phid])) { 98 - $extensions[$key]->renderHovercard( 128 + foreach ($extension_maps as $extension_key => $extension_phids) { 129 + if (isset($extension_phids[$object_phid])) { 130 + $extensions[$extension_key]->renderHovercard( 99 131 $hovercard, 100 132 $handle, 101 133 $object, 102 - $extension_data[$key]); 134 + $extension_data[$extension_key]); 103 135 } 104 136 } 105 137 } ··· 114 146 )); 115 147 } 116 148 117 - foreach ($results as $key => $hovercard) { 118 - $results[$key] = phutil_tag('div', 149 + foreach ($results as $result_key => $hovercard) { 150 + $results[$result_key] = phutil_tag('div', 119 151 array( 120 152 'class' => 'ml', 121 153 ),
+3 -1
src/infrastructure/graph/ManiphestTaskGraph.php
··· 69 69 'href' => $object->getURI(), 70 70 'sigil' => 'hovercard', 71 71 'meta' => array( 72 - 'hoverPHID' => $object->getPHID(), 72 + 'hovercardSpec' => array( 73 + 'objectPHID' => $object->getPHID(), 74 + ), 73 75 ), 74 76 ), 75 77 $object->getTitle());
+10
src/view/phui/PHUIHovercardView.php
··· 18 18 private $fields = array(); 19 19 private $actions = array(); 20 20 private $badges = array(); 21 + private $isExiled; 21 22 22 23 public function setObjectHandle(PhabricatorObjectHandle $handle) { 23 24 $this->handle = $handle; ··· 41 42 public function setDetail($detail) { 42 43 $this->detail = $detail; 43 44 return $this; 45 + } 46 + 47 + public function setIsExiled($is_exiled) { 48 + $this->isExiled = $is_exiled; 49 + return $this; 50 + } 51 + 52 + public function getIsExiled() { 53 + return $this->isExiled; 44 54 } 45 55 46 56 public function addField($label, $value) {
+34 -1
src/view/phui/PHUITagView.php
··· 44 44 private $shade; 45 45 private $slimShady; 46 46 private $border; 47 + private $contextObject; 48 + private $isExiled; 47 49 48 50 public function setType($type) { 49 51 $this->type = $type; ··· 127 129 return strlen($this->href) ? 'a' : 'span'; 128 130 } 129 131 132 + public function setContextObject($context_object) { 133 + $this->contextObject = $context_object; 134 + return $this; 135 + } 136 + 137 + public function getContextObject() { 138 + return $this->contextObject; 139 + } 140 + 141 + public function setIsExiled($is_exiled) { 142 + $this->isExiled = $is_exiled; 143 + return $this; 144 + } 145 + 146 + public function getIsExiled() { 147 + return $this->isExiled; 148 + } 149 + 130 150 protected function getTagAttributes() { 131 151 require_celerity_resource('phui-tag-view-css'); 132 152 ··· 155 175 $classes[] = 'phui-tag-'.$this->border; 156 176 } 157 177 178 + if ($this->getIsExiled()) { 179 + $classes[] = 'phui-tag-exiled'; 180 + } 181 + 158 182 $attributes = array( 159 183 'href' => $this->href, 160 184 'class' => $classes, ··· 170 194 if ($this->phid) { 171 195 Javelin::initBehavior('phui-hovercards'); 172 196 197 + $hovercard_spec = array( 198 + 'objectPHID' => $this->phid, 199 + ); 200 + 201 + $context_object = $this->getContextObject(); 202 + if ($context_object) { 203 + $hovercard_spec['contextPHID'] = $context_object->getPHID(); 204 + } 205 + 173 206 $attributes += array( 174 207 'sigil' => 'hovercard', 175 208 'meta' => array( 176 - 'hoverPHID' => $this->phid, 209 + 'hovercardSpec' => $hovercard_spec, 177 210 ), 178 211 ); 179 212 }
+11
webroot/rsrc/css/application/project/project-card-view.css
··· 72 72 color: {$greytext}; 73 73 } 74 74 75 + .project-card-view .project-card-item-exiled { 76 + background-color: {$lightredbackground}; 77 + border-radius: 4px; 78 + padding: 2px 8px; 79 + margin: 2px 0; 80 + } 81 + 82 + .project-card-view .project-card-item-exiled .project-card-item-text { 83 + color: {$red}; 84 + } 85 + 75 86 .project-card-view .project-card-item-icon { 76 87 width: 20px; 77 88 }
-5
webroot/rsrc/css/core/remarkup.css
··· 291 291 color: {$greytext}; 292 292 } 293 293 294 - .phabricator-remarkup-mention-nopermission .phui-tag-core { 295 - background: {$lightgreybackground}; 296 - color: {$lightgreytext}; 297 - } 298 - 299 294 .phabricator-remarkup .remarkup-note { 300 295 margin: 16px 0; 301 296 padding: 12px;
+12
webroot/rsrc/css/phui/phui-tag-view.css
··· 531 531 color: {$blacktext}; 532 532 border-color: {$blacktext}; 533 533 } 534 + 535 + .phui-tag-exiled .phui-tag-core { 536 + border-color: {$lightredborder}; 537 + color: {$red}; 538 + background: {$lightredbackground}; 539 + } 540 + 541 + 542 + a.phui-tag-view.phui-tag-exiled:hover 543 + .phui-tag-core.phui-tag-color-person { 544 + border-color: {$red}; 545 + }
+1
webroot/rsrc/js/core/Hovercard.js
··· 13 13 properties: { 14 14 hovercardKey: null, 15 15 objectPHID: null, 16 + contextPHID: null, 16 17 isLoading: false, 17 18 isLoaded: false, 18 19 content: null
+10 -3
webroot/rsrc/js/core/HovercardList.js
··· 31 31 if (!(hovercard_key in this._cards)) { 32 32 var card = new JX.Hovercard() 33 33 .setHovercardKey(hovercard_key) 34 - .setObjectPHID(spec.hoverPHID); 34 + .setObjectPHID(spec.objectPHID) 35 + .setContextPHID(spec.contextPHID || null); 35 36 36 37 this._cards[hovercard_key] = card; 37 38 } ··· 76 77 }, 77 78 78 79 _newHovercardKey: function(spec) { 79 - return 'phid=' + spec.hoverPHID; 80 + var parts = [ 81 + spec.objectPHID, 82 + spec.contextPHID 83 + ]; 84 + 85 + return parts.join('/'); 80 86 }, 81 87 82 88 _newCardRequest: function(card) { 83 89 return { 84 - objectPHID: card.getObjectPHID() 90 + objectPHID: card.getObjectPHID(), 91 + contextPHID: card.getContextPHID() 85 92 }; 86 93 }, 87 94
+1 -1
webroot/rsrc/js/core/behavior-hovercard.js
··· 32 32 } 33 33 34 34 var node = e.getNode('hovercard'); 35 - var data = e.getNodeData('hovercard'); 35 + var data = e.getNodeData('hovercard').hovercardSpec; 36 36 37 37 var card = cards.getCard(data); 38 38