@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
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Update Herald rule creation workflow to use more modern UI elements

Summary: Ref T13480. Creating a rule in Herald currently uses the older radio-button flow. Update it to the "clickable menu" flow to simplify it a little bit.

Test Plan: Created new personal, object, and global rules. Hit the object rule error conditions.

Maniphest Tasks: T13480

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

+315 -246
+3 -3
resources/celerity/map.php
··· 9 9 'names' => array( 10 10 'conpherence.pkg.css' => '3c8a0668', 11 11 'conpherence.pkg.js' => '020aebcf', 12 - 'core.pkg.css' => '6d9a0ba6', 12 + 'core.pkg.css' => '5edb4679', 13 13 'core.pkg.js' => '705aec2c', 14 14 'differential.pkg.css' => '607c84be', 15 15 'differential.pkg.js' => '1b97518d', ··· 165 165 'rsrc/css/phui/phui-left-right.css' => '68513c34', 166 166 'rsrc/css/phui/phui-lightbox.css' => '4ebf22da', 167 167 'rsrc/css/phui/phui-list.css' => 'b05144dd', 168 - 'rsrc/css/phui/phui-object-box.css' => 'f434b6be', 168 + 'rsrc/css/phui/phui-object-box.css' => 'b8d7eea0', 169 169 'rsrc/css/phui/phui-pager.css' => 'd022c7ad', 170 170 'rsrc/css/phui/phui-pinboard-view.css' => '1f08f5d8', 171 171 'rsrc/css/phui/phui-policy-section-view.css' => '139fdc64', ··· 855 855 'phui-left-right-css' => '68513c34', 856 856 'phui-lightbox-css' => '4ebf22da', 857 857 'phui-list-view-css' => 'b05144dd', 858 - 'phui-object-box-css' => 'f434b6be', 858 + 'phui-object-box-css' => 'b8d7eea0', 859 859 'phui-oi-big-ui-css' => 'fa74cc35', 860 860 'phui-oi-color-css' => 'b517bfa0', 861 861 'phui-oi-drag-ui-css' => 'da15d3dc',
+6
src/applications/herald/adapter/HeraldAdapter.php
··· 243 243 abstract public function getAdapterApplicationClass(); 244 244 abstract public function getObject(); 245 245 246 + public function getAdapterContentIcon() { 247 + $application_class = $this->getAdapterApplicationClass(); 248 + $application = newv($application_class, array()); 249 + return $application->getIcon(); 250 + } 251 + 246 252 /** 247 253 * Return a new characteristic object for this adapter. 248 254 *
+280 -243
src/applications/herald/controller/HeraldNewController.php
··· 3 3 final class HeraldNewController extends HeraldController { 4 4 5 5 public function handleRequest(AphrontRequest $request) { 6 - $viewer = $request->getViewer(); 6 + $viewer = $this->getViewer(); 7 7 8 - $content_type_map = HeraldAdapter::getEnabledAdapterMap($viewer); 9 - $rule_type_map = HeraldRuleTypeConfig::getRuleTypeMap(); 8 + $adapter_type_map = HeraldAdapter::getEnabledAdapterMap($viewer); 9 + $adapter_type = $request->getStr('adapter'); 10 10 11 - $errors = array(); 11 + if (!isset($adapter_type_map[$adapter_type])) { 12 + $title = pht('Create Herald Rule'); 13 + $content = $this->newAdapterMenu($title); 14 + } else { 15 + $adapter = HeraldAdapter::getAdapterForContentType($adapter_type); 12 16 13 - $e_type = null; 14 - $e_rule = null; 15 - $e_object = null; 17 + $rule_type_map = HeraldRuleTypeConfig::getRuleTypeMap(); 18 + $rule_type = $request->getStr('type'); 16 19 17 - $step = $request->getInt('step'); 18 - if ($request->isFormPost()) { 19 - $content_type = $request->getStr('content_type'); 20 - if (empty($content_type_map[$content_type])) { 21 - $errors[] = pht('You must choose a content type for this rule.'); 22 - $e_type = pht('Required'); 23 - $step = 0; 24 - } 20 + if (!isset($rule_type_map[$rule_type])) { 21 + $title = pht( 22 + 'Create Herald Rule: %s', 23 + $adapter->getAdapterContentName()); 25 24 26 - if (!$errors && $step > 1) { 27 - $rule_type = $request->getStr('rule_type'); 28 - if (empty($rule_type_map[$rule_type])) { 29 - $errors[] = pht('You must choose a rule type for this rule.'); 30 - $e_rule = pht('Required'); 31 - $step = 1; 32 - } 33 - } 25 + $content = $this->newTypeMenu($adapter, $title); 26 + } else { 27 + if ($rule_type !== HeraldRuleTypeConfig::RULE_TYPE_OBJECT) { 28 + $target_phid = null; 29 + $target_okay = true; 30 + } else { 31 + $object_name = $request->getStr('objectName'); 32 + $target_okay = false; 33 + 34 + $errors = array(); 35 + $e_object = null; 34 36 35 - if (!$errors && $step >= 2) { 36 - $target_phid = null; 37 - $object_name = $request->getStr('objectName'); 38 - $done = false; 39 - if ($rule_type != HeraldRuleTypeConfig::RULE_TYPE_OBJECT) { 40 - $done = true; 41 - } else if (strlen($object_name)) { 42 - $target_object = id(new PhabricatorObjectQuery()) 43 - ->setViewer($viewer) 44 - ->withNames(array($object_name)) 45 - ->executeOne(); 46 - if ($target_object) { 47 - $can_edit = PhabricatorPolicyFilter::hasCapability( 48 - $viewer, 49 - $target_object, 50 - PhabricatorPolicyCapability::CAN_EDIT); 51 - if (!$can_edit) { 52 - $errors[] = pht( 53 - 'You can not create a rule for that object, because you do '. 54 - 'not have permission to edit it. You can only create rules '. 55 - 'for objects you can edit.'); 56 - $e_object = pht('Not Editable'); 57 - $step = 2; 58 - } else { 59 - $adapter = HeraldAdapter::getAdapterForContentType($content_type); 60 - if (!$adapter->canTriggerOnObject($target_object)) { 61 - $errors[] = pht( 62 - 'This object is not of an allowed type for the rule. '. 63 - 'Rules can only trigger on certain objects.'); 37 + if ($request->isFormPost()) { 38 + if (strlen($object_name)) { 39 + $target_object = id(new PhabricatorObjectQuery()) 40 + ->setViewer($viewer) 41 + ->withNames(array($object_name)) 42 + ->executeOne(); 43 + if ($target_object) { 44 + $can_edit = PhabricatorPolicyFilter::hasCapability( 45 + $viewer, 46 + $target_object, 47 + PhabricatorPolicyCapability::CAN_EDIT); 48 + if (!$can_edit) { 49 + $errors[] = pht( 50 + 'You can not create a rule for that object, because you '. 51 + 'do not have permission to edit it. You can only create '. 52 + 'rules for objects you can edit.'); 53 + $e_object = pht('Not Editable'); 54 + } else { 55 + if (!$adapter->canTriggerOnObject($target_object)) { 56 + $errors[] = pht( 57 + 'This object is not of an allowed type for the rule. '. 58 + 'Rules can only trigger on certain objects.'); 59 + $e_object = pht('Invalid'); 60 + } else { 61 + $target_phid = $target_object->getPHID(); 62 + } 63 + } 64 + } else { 65 + $errors[] = pht('No object exists by that name.'); 64 66 $e_object = pht('Invalid'); 65 - $step = 2; 66 - } else { 67 - $target_phid = $target_object->getPHID(); 68 - $done = true; 69 67 } 68 + } else { 69 + $errors[] = pht( 70 + 'You must choose an object to associate this rule with.'); 71 + $e_object = pht('Required'); 70 72 } 71 - } else { 72 - $errors[] = pht('No object exists by that name.'); 73 - $e_object = pht('Invalid'); 74 - $step = 2; 73 + 74 + $target_okay = !$errors; 75 75 } 76 - } else if ($step > 2) { 77 - $errors[] = pht( 78 - 'You must choose an object to associate this rule with.'); 79 - $e_object = pht('Required'); 80 - $step = 2; 81 76 } 82 77 83 - if (!$errors && $done) { 78 + if (!$target_okay) { 79 + $title = pht('Choose Object'); 80 + $content = $this->newTargetForm( 81 + $adapter, 82 + $rule_type, 83 + $object_name, 84 + $errors, 85 + $e_object, 86 + $title); 87 + } else { 84 88 $params = array( 85 - 'content_type' => $content_type, 89 + 'content_type' => $adapter_type, 86 90 'rule_type' => $rule_type, 87 91 'targetPHID' => $target_phid, 88 92 ); 89 93 90 - $uri = new PhutilURI('edit/', $params); 91 - $uri = $this->getApplicationURI($uri); 92 - return id(new AphrontRedirectResponse())->setURI($uri); 94 + $edit_uri = $this->getApplicationURI('edit/'); 95 + $edit_uri = new PhutilURI($edit_uri, $params); 96 + 97 + return id(new AphrontRedirectResponse()) 98 + ->setURI($edit_uri); 93 99 } 94 100 } 95 101 } 96 102 97 - $content_type = $request->getStr('content_type'); 98 - $rule_type = $request->getStr('rule_type'); 99 - 100 - $form = id(new AphrontFormView()) 101 - ->setUser($viewer) 102 - ->setAction($this->getApplicationURI('new/')); 103 - 104 - switch ($step) { 105 - case 0: 106 - default: 107 - $content_types = $this->renderContentTypeControl( 108 - $content_type_map, 109 - $e_type); 110 - 111 - $form 112 - ->addHiddenInput('step', 1) 113 - ->appendChild($content_types); 114 - 115 - $cancel_text = null; 116 - $cancel_uri = $this->getApplicationURI(); 117 - $title = pht('Create Herald Rule'); 118 - break; 119 - case 1: 120 - $rule_types = $this->renderRuleTypeControl( 121 - $rule_type_map, 122 - $e_rule); 123 - 124 - $form 125 - ->addHiddenInput('content_type', $content_type) 126 - ->addHiddenInput('step', 2) 127 - ->appendChild($rule_types); 128 - 129 - $params = array( 130 - 'content_type' => $content_type, 131 - 'step' => '0', 132 - ); 133 - 134 - $cancel_text = pht('Back'); 135 - $cancel_uri = new PhutilURI('new/', $params); 136 - $cancel_uri = $this->getApplicationURI($cancel_uri); 137 - $title = pht('Create Herald Rule: %s', 138 - idx($content_type_map, $content_type)); 139 - break; 140 - case 2: 141 - $adapter = HeraldAdapter::getAdapterForContentType($content_type); 142 - $form 143 - ->addHiddenInput('content_type', $content_type) 144 - ->addHiddenInput('rule_type', $rule_type) 145 - ->addHiddenInput('step', 3) 146 - ->appendChild( 147 - id(new AphrontFormStaticControl()) 148 - ->setLabel(pht('Rule for')) 149 - ->setValue( 150 - phutil_tag( 151 - 'strong', 152 - array(), 153 - idx($content_type_map, $content_type)))) 154 - ->appendChild( 155 - id(new AphrontFormStaticControl()) 156 - ->setLabel(pht('Rule Type')) 157 - ->setValue( 158 - phutil_tag( 159 - 'strong', 160 - array(), 161 - idx($rule_type_map, $rule_type)))) 162 - ->appendRemarkupInstructions( 163 - pht( 164 - 'Choose the object this rule will act on (for example, enter '. 165 - '`rX` to act on the `rX` repository, or `#project` to act on '. 166 - 'a project).')) 167 - ->appendRemarkupInstructions( 168 - $adapter->explainValidTriggerObjects()) 169 - ->appendChild( 170 - id(new AphrontFormTextControl()) 171 - ->setName('objectName') 172 - ->setError($e_object) 173 - ->setValue($request->getStr('objectName')) 174 - ->setLabel(pht('Object'))); 175 - 176 - $params = array( 177 - 'content_type' => $content_type, 178 - 'rule_type' => $rule_type, 179 - 'step' => 1, 180 - ); 181 - 182 - $cancel_text = pht('Back'); 183 - $cancel_uri = new PhutilURI('new/', $params); 184 - $cancel_uri = $this->getApplicationURI($cancel_uri); 185 - $title = pht('Create Herald Rule: %s', 186 - idx($content_type_map, $content_type)); 187 - break; 188 - } 189 - 190 - $form 191 - ->appendChild( 192 - id(new AphrontFormSubmitControl()) 193 - ->setValue(pht('Continue')) 194 - ->addCancelButton($cancel_uri, $cancel_text)); 195 - 196 - $form_box = id(new PHUIObjectBoxView()) 197 - ->setHeaderText($title) 198 - ->setFormErrors($errors) 199 - ->setBackground(PHUIObjectBoxView::WHITE_CONFIG) 200 - ->setForm($form); 201 - 202 103 $crumbs = $this 203 104 ->buildApplicationCrumbs() 204 105 ->addTextCrumb(pht('Create Rule')) 205 106 ->setBorder(true); 206 107 207 108 $view = id(new PHUITwoColumnView()) 208 - ->setFooter($form_box); 109 + ->setFooter($content); 209 110 210 111 return $this->newPage() 211 112 ->setTitle($title) 212 113 ->setCrumbs($crumbs) 213 - ->appendChild( 214 - array( 215 - $view, 216 - )); 114 + ->appendChild($view); 217 115 } 218 116 219 - private function renderContentTypeControl(array $content_type_map, $e_type) { 220 - $request = $this->getRequest(); 117 + private function newAdapterMenu($title) { 118 + $viewer = $this->getViewer(); 119 + 120 + $types = HeraldAdapter::getEnabledAdapterMap($viewer); 121 + 122 + foreach ($types as $key => $type) { 123 + $types[$key] = HeraldAdapter::getAdapterForContentType($key); 124 + } 125 + 126 + $types = msort($types, 'getAdapterContentName'); 127 + 128 + $base_uri = $this->getApplicationURI('create/'); 129 + 130 + $menu = id(new PHUIObjectItemListView()) 131 + ->setViewer($viewer) 132 + ->setBig(true); 133 + 134 + foreach ($types as $key => $adapter) { 135 + $adapter_uri = id(new PhutilURI($base_uri)) 136 + ->replaceQueryParam('adapter', $key); 221 137 222 - $radio = id(new AphrontFormRadioButtonControl()) 223 - ->setLabel(pht('New Rule for')) 224 - ->setName('content_type') 225 - ->setValue($request->getStr('content_type')) 226 - ->setError($e_type); 138 + $description = $adapter->getAdapterContentDescription(); 139 + $description = phutil_escape_html_newlines($description); 227 140 228 - foreach ($content_type_map as $value => $name) { 229 - $adapter = HeraldAdapter::getAdapterForContentType($value); 230 - $radio->addButton( 231 - $value, 232 - $name, 233 - phutil_escape_html_newlines($adapter->getAdapterContentDescription())); 141 + $item = id(new PHUIObjectItemView()) 142 + ->setHeader($adapter->getAdapterContentName()) 143 + ->setImageIcon($adapter->getAdapterContentIcon()) 144 + ->addAttribute($description) 145 + ->setHref($adapter_uri) 146 + ->setClickable(true); 147 + 148 + $menu->addItem($item); 234 149 } 235 150 236 - return $radio; 151 + $box = id(new PHUIObjectBoxView()) 152 + ->setHeaderText($title) 153 + ->setBackground(PHUIObjectBoxView::WHITE_CONFIG) 154 + ->setObjectList($menu); 155 + 156 + return id(new PHUILauncherView()) 157 + ->appendChild($box); 237 158 } 238 159 160 + private function newTypeMenu(HeraldAdapter $adapter, $title) { 161 + $viewer = $this->getViewer(); 239 162 240 - private function renderRuleTypeControl(array $rule_type_map, $e_rule) { 241 - $request = $this->getRequest(); 163 + $global_capability = HeraldManageGlobalRulesCapability::CAPABILITY; 164 + $can_global = $this->hasApplicationCapability($global_capability); 242 165 243 - // Reorder array to put less powerful rules first. 244 - $rule_type_map = array_select_keys( 245 - $rule_type_map, 246 - array( 247 - HeraldRuleTypeConfig::RULE_TYPE_PERSONAL, 248 - HeraldRuleTypeConfig::RULE_TYPE_OBJECT, 249 - HeraldRuleTypeConfig::RULE_TYPE_GLOBAL, 250 - )) + $rule_type_map; 166 + if ($can_global) { 167 + $global_note = pht( 168 + 'You have permission to create and manage global rules.'); 169 + } else { 170 + $global_note = pht( 171 + 'You do not have permission to create or manage global rules.'); 172 + } 173 + $global_note = phutil_tag('em', array(), $global_note); 251 174 252 - list($can_global, $global_link) = $this->explainApplicationCapability( 253 - HeraldManageGlobalRulesCapability::CAPABILITY, 254 - pht('You have permission to create and manage global rules.'), 255 - pht('You do not have permission to create or manage global rules.')); 256 - 257 - $captions = array( 258 - HeraldRuleTypeConfig::RULE_TYPE_PERSONAL => 259 - pht( 175 + $specs = array( 176 + HeraldRuleTypeConfig::RULE_TYPE_PERSONAL => array( 177 + 'name' => pht('Personal Rule'), 178 + 'icon' => 'fa-user', 179 + 'help' => pht( 260 180 'Personal rules notify you about events. You own them, but they can '. 261 181 'only affect you. Personal rules only trigger for objects you have '. 262 182 'permission to see.'), 263 - HeraldRuleTypeConfig::RULE_TYPE_OBJECT => 264 - pht( 183 + 'enabled' => true, 184 + ), 185 + HeraldRuleTypeConfig::RULE_TYPE_OBJECT => array( 186 + 'name' => pht('Object Rule'), 187 + 'icon' => 'fa-cube', 188 + 'help' => pht( 265 189 'Object rules notify anyone about events. They are bound to an '. 266 190 'object (like a repository) and can only act on that object. You '. 267 191 'must be able to edit an object to create object rules for it. '. 268 192 'Other users who can edit the object can edit its rules.'), 269 - HeraldRuleTypeConfig::RULE_TYPE_GLOBAL => 270 - array( 193 + 'enabled' => true, 194 + ), 195 + HeraldRuleTypeConfig::RULE_TYPE_GLOBAL => array( 196 + 'name' => pht('Global Rule'), 197 + 'icon' => 'fa-globe', 198 + 'help' => array( 271 199 pht( 272 200 'Global rules notify anyone about events. Global rules can '. 273 201 'bypass access control policies and act on any object.'), 274 - $global_link, 202 + $global_note, 275 203 ), 204 + 'enabled' => $can_global, 205 + ), 276 206 ); 277 207 278 - $radio = id(new AphrontFormRadioButtonControl()) 279 - ->setLabel(pht('Rule Type')) 280 - ->setName('rule_type') 281 - ->setValue($request->getStr('rule_type')) 282 - ->setError($e_rule); 208 + $adapter_type = $adapter->getAdapterContentType(); 283 209 284 - $adapter = HeraldAdapter::getAdapterForContentType( 285 - $request->getStr('content_type')); 210 + $base_uri = new PhutilURI($this->getApplicationURI('create/')); 286 211 287 - foreach ($rule_type_map as $value => $name) { 288 - $caption = idx($captions, $value); 289 - $disabled = ($value == HeraldRuleTypeConfig::RULE_TYPE_GLOBAL) && 290 - (!$can_global); 212 + $adapter_uri = id(clone $base_uri) 213 + ->replaceQueryParam('adapter', $adapter_type); 214 + 215 + $menu = id(new PHUIObjectItemListView()) 216 + ->setUser($viewer) 217 + ->setBig(true); 218 + 219 + foreach ($specs as $rule_type => $spec) { 220 + $type_uri = id(clone $adapter_uri) 221 + ->replaceQueryParam('type', $rule_type); 222 + 223 + $name = $spec['name']; 224 + $icon = $spec['icon']; 225 + 226 + $description = $spec['help']; 227 + $description = (array)$description; 291 228 292 - if (!$adapter->supportsRuleType($value)) { 293 - $disabled = true; 294 - $caption = array( 295 - $caption, 296 - "\n\n", 297 - phutil_tag( 229 + $enabled = $spec['enabled']; 230 + if ($enabled) { 231 + $enabled = $adapter->supportsRuleType($rule_type); 232 + if (!$enabled) { 233 + $description[] = phutil_tag( 298 234 'em', 299 235 array(), 300 236 pht( 301 - 'This rule type is not supported by the selected content type.')), 302 - ); 237 + 'This rule type is not supported by the selected '. 238 + 'content type.')); 239 + } 240 + } 241 + 242 + $description = phutil_implode_html( 243 + array( 244 + phutil_tag('br'), 245 + phutil_tag('br'), 246 + ), 247 + $description); 248 + 249 + $item = id(new PHUIObjectItemView()) 250 + ->setHeader($name) 251 + ->setImageIcon($icon) 252 + ->addAttribute($description); 253 + 254 + if ($enabled) { 255 + $item 256 + ->setHref($type_uri) 257 + ->setClickable(true); 258 + } else { 259 + $item->setDisabled(true); 303 260 } 304 261 305 - $radio->addButton( 306 - $value, 307 - $name, 308 - phutil_escape_html_newlines($caption), 309 - $disabled ? 'disabled' : null, 310 - $disabled); 262 + $menu->addItem($item); 311 263 } 312 264 313 - return $radio; 265 + $box = id(new PHUIObjectBoxView()) 266 + ->setHeaderText($title) 267 + ->setBackground(PHUIObjectBoxView::WHITE_CONFIG) 268 + ->setObjectList($menu); 269 + 270 + $box->newTailButton() 271 + ->setText(pht('Back to Content Types')) 272 + ->setIcon('fa-chevron-left') 273 + ->setHref($base_uri); 274 + 275 + return id(new PHUILauncherView()) 276 + ->appendChild($box); 277 + } 278 + 279 + 280 + private function newTargetForm( 281 + HeraldAdapter $adapter, 282 + $rule_type, 283 + $object_name, 284 + $errors, 285 + $e_object, 286 + $title) { 287 + 288 + $viewer = $this->getViewer(); 289 + $content_type = $adapter->getAdapterContentType(); 290 + $rule_type_map = HeraldRuleTypeConfig::getRuleTypeMap(); 291 + 292 + $params = array( 293 + 'adapter' => $content_type, 294 + 'type' => $rule_type, 295 + ); 296 + 297 + $form = id(new AphrontFormView()) 298 + ->setViewer($viewer) 299 + ->appendChild( 300 + id(new AphrontFormStaticControl()) 301 + ->setLabel(pht('Rule for')) 302 + ->setValue( 303 + phutil_tag( 304 + 'strong', 305 + array(), 306 + $adapter->getAdapterContentName()))) 307 + ->appendChild( 308 + id(new AphrontFormStaticControl()) 309 + ->setLabel(pht('Rule Type')) 310 + ->setValue( 311 + phutil_tag( 312 + 'strong', 313 + array(), 314 + idx($rule_type_map, $rule_type)))) 315 + ->appendRemarkupInstructions( 316 + pht( 317 + 'Choose the object this rule will act on (for example, enter '. 318 + '`rX` to act on the `rX` repository, or `#project` to act on '. 319 + 'a project).')) 320 + ->appendRemarkupInstructions( 321 + $adapter->explainValidTriggerObjects()) 322 + ->appendChild( 323 + id(new AphrontFormTextControl()) 324 + ->setName('objectName') 325 + ->setError($e_object) 326 + ->setValue($object_name) 327 + ->setLabel(pht('Object'))); 328 + 329 + foreach ($params as $key => $value) { 330 + $form->addHiddenInput($key, $value); 331 + } 332 + 333 + $cancel_params = $params; 334 + unset($cancel_params['type']); 335 + 336 + $cancel_uri = $this->getApplicationURI('new/'); 337 + $cancel_uri = new PhutilURI($cancel_uri, $params); 338 + 339 + $form->appendChild( 340 + id(new AphrontFormSubmitControl()) 341 + ->setValue(pht('Continue')) 342 + ->addCancelButton($cancel_uri, pht('Back'))); 343 + 344 + $form_box = id(new PHUIObjectBoxView()) 345 + ->setHeaderText($title) 346 + ->setFormErrors($errors) 347 + ->setBackground(PHUIObjectBoxView::WHITE_CONFIG) 348 + ->setForm($form); 349 + 350 + return $form_box; 314 351 } 315 352 316 353 }
+20
src/view/phui/PHUIObjectBoxView.php
··· 27 27 private $showHideOpen; 28 28 29 29 private $propertyLists = array(); 30 + private $tailButtons = array(); 30 31 31 32 const COLOR_RED = 'red'; 32 33 const COLOR_BLUE = 'blue'; ··· 151 152 PhabricatorApplicationTransactionValidationException $ex = null) { 152 153 $this->validationException = $ex; 153 154 return $this; 155 + } 156 + 157 + public function newTailButton() { 158 + $button = id(new PHUIButtonView()) 159 + ->setTag('a') 160 + ->setColor(PHUIButtonView::GREY); 161 + 162 + $this->tailButtons[] = $button; 163 + 164 + return $button; 154 165 } 155 166 156 167 protected function getTagAttributes() { ··· 327 338 328 339 if ($this->objectList) { 329 340 $content[] = $this->objectList; 341 + } 342 + 343 + if ($this->tailButtons) { 344 + $content[] = phutil_tag( 345 + 'div', 346 + array( 347 + 'class' => 'phui-object-box-tail-buttons', 348 + ), 349 + $this->tailButtons); 330 350 } 331 351 332 352 return $content;
+6
webroot/rsrc/css/phui/phui-object-box.css
··· 62 62 font-size: {$normalfontsize}; 63 63 } 64 64 65 + .phui-object-box-tail-buttons { 66 + padding: 8px; 67 + background: {$lightgreybackground}; 68 + border-top: 1px solid {$lightgreyborder}; 69 + } 70 + 65 71 /* - Object Box Colors ------------------------------------------------------ */ 66 72 67 73 .phui-box-border.phui-object-box-green {