@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

Use the new "CurtainObjectRefList" UI element for subscribers

Summary:
Depends on D20966. Ref T13486. Curtains currently render subscribers in a plain text list, but the new ref list element is a good fit for this.

Also, improve the sorting and ordering behavior.

This makes the subscriber list take up a bit more space, but it should make it a lot easier to read at a glance.

Test Plan: Viewed object subscriber lists at varying limits and subscriber counts, saw sensible subscriber lists.

Maniphest Tasks: T13486

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

+286 -25
+2 -2
resources/celerity/map.php
··· 146 146 'rsrc/css/phui/phui-comment-form.css' => '68a2d99a', 147 147 'rsrc/css/phui/phui-comment-panel.css' => 'ec4e31c0', 148 148 'rsrc/css/phui/phui-crumbs-view.css' => '614f43cf', 149 - 'rsrc/css/phui/phui-curtain-object-ref-view.css' => 'e3331b60', 149 + 'rsrc/css/phui/phui-curtain-object-ref-view.css' => '12404744', 150 150 'rsrc/css/phui/phui-curtain-view.css' => '68c5efb6', 151 151 'rsrc/css/phui/phui-document-pro.css' => 'b9613a10', 152 152 'rsrc/css/phui/phui-document-summary.css' => 'b068eed1', ··· 834 834 'phui-comment-form-css' => '68a2d99a', 835 835 'phui-comment-panel-css' => 'ec4e31c0', 836 836 'phui-crumbs-view-css' => '614f43cf', 837 - 'phui-curtain-object-ref-view-css' => 'e3331b60', 837 + 'phui-curtain-object-ref-view-css' => '12404744', 838 838 'phui-curtain-view-css' => '68c5efb6', 839 839 'phui-document-summary-view-css' => 'b068eed1', 840 840 'phui-document-view-css' => '52b748a5',
+2
src/__phutil_library_map__.php
··· 2042 2042 'PHUILauncherView' => 'view/phui/PHUILauncherView.php', 2043 2043 'PHUILeftRightExample' => 'applications/uiexample/examples/PHUILeftRightExample.php', 2044 2044 'PHUILeftRightView' => 'view/phui/PHUILeftRightView.php', 2045 + 'PHUILinkView' => 'view/phui/PHUILinkView.php', 2045 2046 'PHUIListExample' => 'applications/uiexample/examples/PHUIListExample.php', 2046 2047 'PHUIListItemView' => 'view/phui/PHUIListItemView.php', 2047 2048 'PHUIListView' => 'view/phui/PHUIListView.php', ··· 8242 8243 'PHUILauncherView' => 'AphrontTagView', 8243 8244 'PHUILeftRightExample' => 'PhabricatorUIExample', 8244 8245 'PHUILeftRightView' => 'AphrontTagView', 8246 + 'PHUILinkView' => 'AphrontTagView', 8245 8247 'PHUIListExample' => 'PhabricatorUIExample', 8246 8248 'PHUIListItemView' => 'AphrontTagView', 8247 8249 'PHUIListView' => 'AphrontTagView',
+5 -2
src/applications/maniphest/controller/ManiphestTaskDetailController.php
··· 336 336 $curtain->addAction($relationship_submenu); 337 337 } 338 338 339 + $viewer_phid = $viewer->getPHID(); 339 340 $owner_phid = $task->getOwnerPHID(); 340 341 $author_phid = $task->getAuthorPHID(); 341 342 $handles = $viewer->loadHandles(array($owner_phid, $author_phid)); ··· 346 347 347 348 if ($owner_phid) { 348 349 $assigned_ref = $assigned_refs->newObjectRefView() 349 - ->setHandle($handles[$owner_phid]); 350 + ->setHandle($handles[$owner_phid]) 351 + ->setHighlighted($owner_phid === $viewer_phid); 350 352 } 351 353 352 354 $curtain->newPanel() ··· 358 360 359 361 $author_ref = $author_refs->newObjectRefView() 360 362 ->setHandle($handles[$author_phid]) 361 - ->setEpoch($task->getDateCreated()); 363 + ->setEpoch($task->getDateCreated()) 364 + ->setHighlighted($author_phid === $viewer_phid); 362 365 363 366 $curtain->newPanel() 364 367 ->setHeaderText(pht('Authored By'))
+112 -8
src/applications/subscriptions/engineextension/PhabricatorSubscriptionsCurtainExtension.php
··· 15 15 16 16 public function buildCurtainPanel($object) { 17 17 $viewer = $this->getViewer(); 18 + $viewer_phid = $viewer->getPHID(); 18 19 $object_phid = $object->getPHID(); 19 20 21 + $max_handles = 100; 22 + $max_visible = 8; 23 + 24 + // TODO: We should limit the number of subscriber PHIDs we'll load, so 25 + // we degrade gracefully when objects have thousands of subscribers. 26 + 20 27 $subscriber_phids = PhabricatorSubscribersQuery::loadSubscribersForPHID( 21 28 $object_phid); 29 + $subscriber_count = count($subscriber_phids); 22 30 23 - $handles = $viewer->loadHandles($subscriber_phids); 31 + $subscriber_phids = $this->sortSubscriberPHIDs( 32 + $subscriber_phids, 33 + null); 24 34 25 - // TODO: This class can't accept a HandleList yet. 26 - $handles = iterator_to_array($handles); 35 + // If we have fewer subscribers than the maximum number of handles we're 36 + // willing to load, load all the handles and then sort the list based on 37 + // complete handle data. 27 38 28 - $susbscribers_view = id(new SubscriptionListStringBuilder()) 29 - ->setObjectPHID($object_phid) 30 - ->setHandles($handles) 31 - ->buildPropertyString(); 39 + // If we have too many PHIDs, we'll skip this step and accept a less 40 + // useful ordering. 41 + $handles = null; 42 + if ($subscriber_count <= $max_handles) { 43 + $handles = $viewer->loadHandles($subscriber_phids); 44 + 45 + $subscriber_phids = $this->sortSubscriberPHIDs( 46 + $subscriber_phids, 47 + $handles); 48 + } 49 + 50 + // If we have more PHIDs to show than visible slots, slice the list. 51 + if ($subscriber_count > $max_visible) { 52 + $visible_phids = array_slice($subscriber_phids, 0, $max_visible - 1); 53 + $show_all = true; 54 + } else { 55 + $visible_phids = $subscriber_phids; 56 + $show_all = false; 57 + } 58 + 59 + // If we didn't load handles earlier because we had too many PHIDs, 60 + // load them now. 61 + if ($handles === null) { 62 + $handles = $viewer->loadHandles($visible_phids); 63 + } 64 + 65 + $ref_list = id(new PHUICurtainObjectRefListView()) 66 + ->setViewer($viewer) 67 + ->setEmptyMessage(pht('None')); 68 + 69 + foreach ($visible_phids as $phid) { 70 + $ref = $ref_list->newObjectRefView() 71 + ->setHandle($handles[$phid]); 72 + 73 + if ($phid === $viewer_phid) { 74 + $ref->setHighlighted(true); 75 + } 76 + } 77 + 78 + if ($show_all) { 79 + $view_all_uri = urisprintf( 80 + '/subscriptions/list/%s/', 81 + $object_phid); 82 + 83 + $ref_list->newTailLink() 84 + ->setURI($view_all_uri) 85 + ->setText(pht('View All %d Subscriber(s)', $subscriber_count)) 86 + ->setWorkflow(true); 87 + } 32 88 33 89 return $this->newPanel() 34 90 ->setHeaderText(pht('Subscribers')) 35 91 ->setOrder(20000) 36 - ->appendChild($susbscribers_view); 92 + ->appendChild($ref_list); 93 + } 94 + 95 + private function sortSubscriberPHIDs(array $subscriber_phids, $handles) { 96 + 97 + // Sort subscriber PHIDs with or without handle data. If we have handles, 98 + // we can sort results more comprehensively. 99 + 100 + $viewer = $this->getViewer(); 101 + 102 + $user_type = PhabricatorPeopleUserPHIDType::TYPECONST; 103 + $viewer_phid = $viewer->getPHID(); 104 + 105 + $type_order_map = array( 106 + PhabricatorPeopleUserPHIDType::TYPECONST => 0, 107 + PhabricatorProjectProjectPHIDType::TYPECONST => 1, 108 + PhabricatorOwnersPackagePHIDType::TYPECONST => 2, 109 + ); 110 + $default_type_order = count($type_order_map); 111 + 112 + $subscriber_map = array(); 113 + foreach ($subscriber_phids as $subscriber_phid) { 114 + $is_viewer = ($viewer_phid === $subscriber_phid); 115 + 116 + $subscriber_type = phid_get_type($subscriber_phid); 117 + $type_order = idx($type_order_map, $subscriber_type, $default_type_order); 118 + 119 + $sort_name = ''; 120 + $is_complete = false; 121 + if ($handles) { 122 + if (isset($handles[$subscriber_phid])) { 123 + $handle = $handles[$subscriber_phid]; 124 + if ($handle->isComplete()) { 125 + $is_complete = true; 126 + $sort_name = $handle->getLinkName(); 127 + } 128 + } 129 + } 130 + 131 + $subscriber_map[$subscriber_phid] = id(new PhutilSortVector()) 132 + ->addInt($is_viewer ? 0 : 1) 133 + ->addInt($is_complete ? 0 : 1) 134 + ->addInt($type_order) 135 + ->addString($sort_name); 136 + } 137 + 138 + $subscriber_map = msortv($subscriber_map, 'getSelf'); 139 + 140 + return array_keys($subscriber_map); 37 141 } 38 142 39 143 }
+5
src/infrastructure/internationalization/translation/PhabricatorUSEnglishTranslation.php
··· 1718 1718 'then try again.', 1719 1719 ), 1720 1720 1721 + 'View All %d Subscriber(s)' => array( 1722 + 'View Subscriber', 1723 + 'View All %d Subscribers', 1724 + ), 1725 + 1721 1726 ); 1722 1727 } 1723 1728
+32 -10
src/view/phui/PHUICurtainObjectRefListView.php
··· 5 5 6 6 private $refs = array(); 7 7 private $emptyMessage; 8 + private $tail = array(); 8 9 9 10 protected function getTagAttributes() { 10 11 return array( ··· 20 21 protected function getTagContent() { 21 22 $refs = $this->refs; 22 23 23 - if (!$refs) { 24 - if ($this->emptyMessage) { 25 - return phutil_tag( 26 - 'div', 27 - array( 28 - 'class' => 'phui-curtain-object-ref-list-view-empty', 29 - ), 30 - $this->emptyMessage); 31 - } 24 + if (!$refs && ($this->emptyMessage !== null)) { 25 + $view = phutil_tag( 26 + 'div', 27 + array( 28 + 'class' => 'phui-curtain-object-ref-list-view-empty', 29 + ), 30 + $this->emptyMessage); 31 + } else { 32 + $view = $refs; 32 33 } 33 34 34 - return $refs; 35 + $tail = null; 36 + if ($this->tail) { 37 + $tail = phutil_tag( 38 + 'div', 39 + array( 40 + 'class' => 'phui-curtain-object-ref-list-view-tail', 41 + ), 42 + $this->tail); 43 + } 44 + 45 + return array( 46 + $view, 47 + $tail, 48 + ); 35 49 } 36 50 37 51 public function newObjectRefView() { ··· 41 55 $this->refs[] = $ref_view; 42 56 43 57 return $ref_view; 58 + } 59 + 60 + public function newTailLink() { 61 + $link = new PHUILinkView(); 62 + 63 + $this->tail[] = $link; 64 + 65 + return $link; 44 66 } 45 67 46 68 }
+40 -1
src/view/phui/PHUICurtainObjectRefView.php
··· 5 5 6 6 private $handle; 7 7 private $epoch; 8 + private $highlighted; 8 9 9 10 public function setHandle(PhabricatorObjectHandle $handle) { 10 11 $this->handle = $handle; ··· 16 17 return $this; 17 18 } 18 19 20 + public function setHighlighted($highlighted) { 21 + $this->highlighted = $highlighted; 22 + return $this; 23 + } 24 + 19 25 protected function getTagAttributes() { 26 + $classes = array(); 27 + $classes[] = 'phui-curtain-object-ref-view'; 28 + 29 + if ($this->highlighted) { 30 + $classes[] = 'phui-curtain-object-ref-view-highlighted'; 31 + } 32 + $classes = implode(' ', $classes); 33 + 20 34 return array( 21 - 'class' => 'phui-curtain-object-ref-view', 35 + 'class' => $classes, 22 36 ); 23 37 } 24 38 ··· 114 128 $image_uri = $this->getImageURI(); 115 129 $target_uri = $this->getTargetURI(); 116 130 131 + $icon_view = null; 132 + if ($image_uri == null) { 133 + $icon_view = $this->newIconView(); 134 + } 135 + 117 136 if ($image_uri !== null) { 118 137 $image_view = javelin_tag( 119 138 'a', ··· 122 141 'href' => $target_uri, 123 142 'aural' => false, 124 143 )); 144 + } else if ($icon_view !== null) { 145 + $image_view = javelin_tag( 146 + 'a', 147 + array( 148 + 'href' => $target_uri, 149 + 'class' => 'phui-curtain-object-ref-view-icon-image', 150 + 'aural' => false, 151 + ), 152 + $icon_view); 125 153 } else { 126 154 $image_view = null; 127 155 } ··· 149 177 } 150 178 151 179 return $image_uri; 180 + } 181 + 182 + private function newIconView() { 183 + $handle = $this->handle; 184 + 185 + if ($handle) { 186 + $icon_view = id(new PHUIIconView()) 187 + ->setIcon($handle->getIcon()); 188 + } 189 + 190 + return $icon_view; 152 191 } 153 192 154 193
+50
src/view/phui/PHUILinkView.php
··· 1 + <?php 2 + 3 + final class PHUILinkView 4 + extends AphrontTagView { 5 + 6 + private $uri; 7 + private $text; 8 + private $workflow; 9 + 10 + public function setURI($uri) { 11 + $this->uri = $uri; 12 + return $this; 13 + } 14 + 15 + public function getURI() { 16 + return $this->uri; 17 + } 18 + 19 + public function setText($text) { 20 + $this->text = $text; 21 + return $this; 22 + } 23 + 24 + public function setWorkflow($workflow) { 25 + $this->workflow = $workflow; 26 + return $this; 27 + } 28 + 29 + protected function getTagName() { 30 + return 'a'; 31 + } 32 + 33 + protected function getTagAttributes() { 34 + $sigil = array(); 35 + 36 + if ($this->workflow) { 37 + $sigil[] = 'workflow'; 38 + } 39 + 40 + return array( 41 + 'href' => $this->getURI(), 42 + 'sigil' => $sigil, 43 + ); 44 + } 45 + 46 + protected function getTagContent() { 47 + return $this->text; 48 + } 49 + 50 + }
+38 -2
webroot/rsrc/css/phui/phui-curtain-object-ref-view.css
··· 7 7 color: {$greytext}; 8 8 } 9 9 10 + .phui-curtain-object-ref-view { 11 + padding: 4px 6px; 12 + border-radius: 3px; 13 + } 14 + 10 15 .phui-curtain-object-ref-view-image-cell { 11 16 min-width: 32px; 12 - min-height: 32px; 17 + padding-bottom: 24px; 13 18 } 14 19 15 20 .phui-curtain-object-ref-view-image-cell > a { ··· 18 23 background-size: 100%; 19 24 border-radius: 3px; 20 25 display: block; 26 + position: absolute; 27 + } 28 + 29 + .phui-curtain-object-ref-view-image-cell .phui-icon-view { 30 + font-size: 16px; 31 + line-height: 16px; 32 + vertical-align: middle; 33 + text-align: center; 34 + width: 24px; 35 + height: 24px; 36 + top: 3px; 37 + display: block; 38 + position: absolute; 39 + color: #ffffff; 40 + } 41 + 42 + .phui-curtain-object-ref-view-icon-image { 43 + background-color: {$backdrop}; 21 44 } 22 45 23 46 .phui-curtain-object-ref-view-title-cell { ··· 26 49 overflow: hidden; 27 50 28 51 /* This is forcing "text-overflow: ellipsis" to actually work. */ 29 - max-width: 225px; 52 + max-width: 210px; 30 53 } 31 54 32 55 .phui-curtain-object-ref-view-without-content > ··· 46 69 .phui-curtain-object-ref-view-epoch-cell { 47 70 color: {$greytext}; 48 71 } 72 + 73 + .phui-curtain-object-ref-list-view-tail { 74 + text-align: center; 75 + margin-top: 8px; 76 + padding: 4px; 77 + background: {$lightgreybackground}; 78 + border-top: 1px dashed {$thinblueborder}; 79 + box-shadow: inset 0 2px 3px rgba(0, 0, 0, 0.04); 80 + } 81 + 82 + .phui-curtain-object-ref-view-highlighted { 83 + background: {$bluebackground}; 84 + }