@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.

Allow columns to have a point limit

Summary:
Fixes T5885. This implements optional soft point limits for workboard columns, per traditional Kanban.

- Allow columns to have a point limit set.
- When a column has a point limit, show it in the header.
- If a column has too many points in it, show the column and point count in red.

@chad, this could probably use some design tweaks. In particular:

- I changed the color of "hidden" columns to avoid confusion with "overfull" columns. We might be able to find a better color.
- UI hints for overfull columns might need adjustment.

(After T4427, we'll let you sum some custom field instead of total number of tasks, which is why this is called "points" rather than "number of tasks".)

Test Plan:
{F190914}

Note that:

- "Pre-planning" has a limit, so it shows "4/12".
- "Planning" has a limit and is overfull, so it shows "5 / 4".
- Other columns do not have limits.
- "Post-planning" is a hidden column. This might be too muted now.

Transactions:

{F190915}

Error messages / edit screen:

{F190916}

Reviewers: btrahan, chad

Reviewed By: btrahan

Subscribers: chad, epriestley

Maniphest Tasks: T5885

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

+178 -43
+12 -12
resources/celerity/map.php
··· 145 145 'rsrc/css/phui/phui-text.css' => '23e9b4b7', 146 146 'rsrc/css/phui/phui-timeline-view.css' => 'bbd990d0', 147 147 'rsrc/css/phui/phui-workboard-view.css' => '2bf82d00', 148 - 'rsrc/css/phui/phui-workpanel-view.css' => 'e26044fa', 148 + 'rsrc/css/phui/phui-workpanel-view.css' => '198c7e6c', 149 149 'rsrc/css/sprite-apps-large.css' => '20ec0cc0', 150 150 'rsrc/css/sprite-apps.css' => 'd5baed0f', 151 151 'rsrc/css/sprite-conpherence.css' => '3b4a0487', ··· 412 412 'rsrc/js/application/policy/behavior-policy-rule-editor.js' => 'fe9a552f', 413 413 'rsrc/js/application/ponder/behavior-votebox.js' => '4e9b766b', 414 414 'rsrc/js/application/projects/behavior-boards-dropdown.js' => '0ec56e1d', 415 - 'rsrc/js/application/projects/behavior-project-boards.js' => 'e4b6c65a', 415 + 'rsrc/js/application/projects/behavior-project-boards.js' => 'a6c6a058', 416 416 'rsrc/js/application/projects/behavior-project-create.js' => '065227cc', 417 417 'rsrc/js/application/projects/behavior-reorder-columns.js' => 'e1d25dfb', 418 418 'rsrc/js/application/releeph/releeph-preview-branch.js' => 'b2b4fbaf', ··· 636 636 'javelin-behavior-policy-control' => 'f3fef818', 637 637 'javelin-behavior-policy-rule-editor' => 'fe9a552f', 638 638 'javelin-behavior-ponder-votebox' => '4e9b766b', 639 - 'javelin-behavior-project-boards' => 'e4b6c65a', 639 + 'javelin-behavior-project-boards' => 'a6c6a058', 640 640 'javelin-behavior-project-create' => '065227cc', 641 641 'javelin-behavior-refresh-csrf' => '7814b593', 642 642 'javelin-behavior-releeph-preview-branch' => 'b2b4fbaf', ··· 792 792 'phui-text-css' => '23e9b4b7', 793 793 'phui-timeline-view-css' => 'bbd990d0', 794 794 'phui-workboard-view-css' => '2bf82d00', 795 - 'phui-workpanel-view-css' => 'e26044fa', 795 + 'phui-workpanel-view-css' => '198c7e6c', 796 796 'phuix-action-list-view' => 'b5c256b8', 797 797 'phuix-action-view' => '6e8cefa4', 798 798 'phuix-dropdown-menu' => 'bd4c8dca', ··· 1471 1471 'a5d7cf86' => array( 1472 1472 'javelin-dom', 1473 1473 ), 1474 + 'a6c6a058' => array( 1475 + 'javelin-behavior', 1476 + 'javelin-dom', 1477 + 'javelin-util', 1478 + 'javelin-stratcom', 1479 + 'javelin-workflow', 1480 + 'phabricator-draggable-list', 1481 + ), 1474 1482 'a80d0378' => array( 1475 1483 'javelin-behavior', 1476 1484 'javelin-stratcom', ··· 1769 1777 'javelin-vector', 1770 1778 'javelin-dom', 1771 1779 'javelin-uri', 1772 - ), 1773 - 'e4b6c65a' => array( 1774 - 'javelin-behavior', 1775 - 'javelin-dom', 1776 - 'javelin-util', 1777 - 'javelin-stratcom', 1778 - 'javelin-workflow', 1779 - 'phabricator-draggable-list', 1780 1780 ), 1781 1781 'e566f52c' => array( 1782 1782 'javelin-behavior',
+2 -2
src/__phutil_library_map__.php
··· 1930 1930 'PhabricatorProjectArchiveController' => 'applications/project/controller/PhabricatorProjectArchiveController.php', 1931 1931 'PhabricatorProjectBoardController' => 'applications/project/controller/PhabricatorProjectBoardController.php', 1932 1932 'PhabricatorProjectBoardDeleteController' => 'applications/project/controller/PhabricatorProjectBoardDeleteController.php', 1933 - 'PhabricatorProjectBoardEditController' => 'applications/project/controller/PhabricatorProjectBoardEditController.php', 1934 1933 'PhabricatorProjectBoardImportController' => 'applications/project/controller/PhabricatorProjectBoardImportController.php', 1935 1934 'PhabricatorProjectBoardReorderController' => 'applications/project/controller/PhabricatorProjectBoardReorderController.php', 1936 1935 'PhabricatorProjectBoardViewController' => 'applications/project/controller/PhabricatorProjectBoardViewController.php', 1937 1936 'PhabricatorProjectColumn' => 'applications/project/storage/PhabricatorProjectColumn.php', 1938 1937 'PhabricatorProjectColumnDetailController' => 'applications/project/controller/PhabricatorProjectColumnDetailController.php', 1938 + 'PhabricatorProjectColumnEditController' => 'applications/project/controller/PhabricatorProjectColumnEditController.php', 1939 1939 'PhabricatorProjectColumnPHIDType' => 'applications/project/phid/PhabricatorProjectColumnPHIDType.php', 1940 1940 'PhabricatorProjectColumnPosition' => 'applications/project/storage/PhabricatorProjectColumnPosition.php', 1941 1941 'PhabricatorProjectColumnPositionQuery' => 'applications/project/query/PhabricatorProjectColumnPositionQuery.php', ··· 4771 4771 'PhabricatorProjectArchiveController' => 'PhabricatorProjectController', 4772 4772 'PhabricatorProjectBoardController' => 'PhabricatorProjectController', 4773 4773 'PhabricatorProjectBoardDeleteController' => 'PhabricatorProjectBoardController', 4774 - 'PhabricatorProjectBoardEditController' => 'PhabricatorProjectBoardController', 4775 4774 'PhabricatorProjectBoardImportController' => 'PhabricatorProjectBoardController', 4776 4775 'PhabricatorProjectBoardReorderController' => 'PhabricatorProjectBoardController', 4777 4776 'PhabricatorProjectBoardViewController' => 'PhabricatorProjectBoardController', ··· 4781 4780 'PhabricatorDestructibleInterface', 4782 4781 ), 4783 4782 'PhabricatorProjectColumnDetailController' => 'PhabricatorProjectBoardController', 4783 + 'PhabricatorProjectColumnEditController' => 'PhabricatorProjectBoardController', 4784 4784 'PhabricatorProjectColumnPHIDType' => 'PhabricatorPHIDType', 4785 4785 'PhabricatorProjectColumnPosition' => array( 4786 4786 'PhabricatorProjectDAO',
+1 -1
src/applications/project/application/PhabricatorProjectApplication.php
··· 66 66 'move/(?P<id>[1-9]\d*)/' => 'PhabricatorProjectMoveController', 67 67 'board/(?P<projectID>[1-9]\d*)/' => array( 68 68 'edit/(?:(?P<id>\d+)/)?' 69 - => 'PhabricatorProjectBoardEditController', 69 + => 'PhabricatorProjectColumnEditController', 70 70 'delete/(?:(?P<id>\d+)/)?' 71 71 => 'PhabricatorProjectBoardDeleteController', 72 72 'column/(?:(?P<id>\d+)/)?'
+31 -7
src/applications/project/controller/PhabricatorProjectBoardEditController.php src/applications/project/controller/PhabricatorProjectColumnEditController.php
··· 1 1 <?php 2 2 3 - final class PhabricatorProjectBoardEditController 3 + final class PhabricatorProjectColumnEditController 4 4 extends PhabricatorProjectBoardController { 5 5 6 6 private $id; ··· 50 50 } 51 51 52 52 $e_name = null; 53 + $e_limit = null; 54 + 55 + $v_limit = $column->getPointLimit(); 56 + $v_name = $column->getName(); 57 + 53 58 $validation_exception = null; 54 59 $base_uri = '/board/'.$this->projectID.'/'; 55 60 if ($is_new) { ··· 60 65 } 61 66 62 67 if ($request->isFormPost()) { 63 - $new_name = $request->getStr('name'); 68 + $v_name = $request->getStr('name'); 69 + $v_limit = $request->getStr('limit'); 64 70 65 71 if ($is_new) { 66 72 $column->setProjectPHID($project->getPHID()); ··· 79 85 $column->setSequence($new_sequence); 80 86 } 81 87 88 + $xactions = array(); 89 + 82 90 $type_name = PhabricatorProjectColumnTransaction::TYPE_NAME; 83 - $xactions = array(id(new PhabricatorProjectColumnTransaction()) 91 + $xactions[] = id(new PhabricatorProjectColumnTransaction()) 84 92 ->setTransactionType($type_name) 85 - ->setNewValue($new_name)); 93 + ->setNewValue($v_name); 94 + 95 + $type_limit = PhabricatorProjectColumnTransaction::TYPE_LIMIT; 96 + $xactions[] = id(new PhabricatorProjectColumnTransaction()) 97 + ->setTransactionType($type_limit) 98 + ->setNewValue($v_limit); 86 99 87 100 try { 88 101 $editor = id(new PhabricatorProjectColumnTransactionEditor()) ··· 93 106 return id(new AphrontRedirectResponse())->setURI($view_uri); 94 107 } catch (PhabricatorApplicationTransactionValidationException $ex) { 95 108 $e_name = $ex->getShortMessage($type_name); 109 + $e_limit = $ex->getShortMessage($type_limit); 96 110 $validation_exception = $ex; 97 111 } 98 112 } 99 113 100 114 $form = new AphrontFormView(); 101 - $form->setUser($request->getUser()) 115 + $form 116 + ->setUser($request->getUser()) 102 117 ->appendChild( 103 118 id(new AphrontFormTextControl()) 104 - ->setValue($column->getName()) 119 + ->setValue($v_name) 105 120 ->setLabel(pht('Name')) 106 121 ->setName('name') 107 122 ->setError($e_name) 108 123 ->setCaption( 109 - pht('This will be displayed as the header of the column.'))); 124 + pht('This will be displayed as the header of the column.'))) 125 + ->appendChild( 126 + id(new AphrontFormTextControl()) 127 + ->setValue($v_limit) 128 + ->setLabel(pht('Point Limit')) 129 + ->setName('limit') 130 + ->setError($e_limit) 131 + ->setCaption( 132 + pht('Maximum number of points of tasks allowed in the column.'))); 133 + 110 134 111 135 if ($is_new) { 112 136 $title = pht('Create Column');
+11 -6
src/applications/project/controller/PhabricatorProjectBoardViewController.php
··· 225 225 226 226 $panel = id(new PHUIWorkpanelView()) 227 227 ->setHeader($column->getDisplayName()) 228 - ->setHeaderColor($column->getHeaderColor()); 228 + ->addSigil('workpanel'); 229 + 230 + $header_icon = $column->getHeaderIcon(); 231 + if ($header_icon) { 232 + $panel->setHeaderIcon($header_icon); 233 + } 234 + 235 + if ($column->isHidden()) { 236 + $panel->addClass('project-panel-hidden'); 237 + } 229 238 230 239 $column_menu = $this->buildColumnMenu($project, $column); 231 240 $panel->addHeaderAction($column_menu); ··· 252 261 'columnPHID' => $column->getPHID(), 253 262 'countTagID' => $tag_id, 254 263 'countTagContentID' => $tag_content_id, 264 + 'pointLimit' => $column->getPointLimit(), 255 265 )); 256 266 257 267 foreach ($column_tasks as $task) { ··· 268 278 ->getItem()); 269 279 } 270 280 $panel->setCards($cards); 271 - 272 - if (!$column_tasks) { 273 - $cards->addClass('project-column-empty'); 274 - } 275 - 276 281 $board->addPanel($panel); 277 282 } 278 283
+6
src/applications/project/controller/PhabricatorProjectColumnDetailController.php
··· 160 160 pht('Editable By'), 161 161 $descriptions[PhabricatorPolicyCapability::CAN_EDIT]); 162 162 163 + 164 + $limit = $column->getPointLimit(); 165 + $properties->addProperty( 166 + pht('Point Limit'), 167 + $limit ? $limit : pht('No Limit')); 168 + 163 169 return $properties; 164 170 } 165 171
+25
src/applications/project/editor/PhabricatorProjectColumnTransactionEditor.php
··· 16 16 17 17 $types[] = PhabricatorProjectColumnTransaction::TYPE_NAME; 18 18 $types[] = PhabricatorProjectColumnTransaction::TYPE_STATUS; 19 + $types[] = PhabricatorProjectColumnTransaction::TYPE_LIMIT; 19 20 20 21 return $types; 21 22 } ··· 29 30 return $object->getName(); 30 31 case PhabricatorProjectColumnTransaction::TYPE_STATUS: 31 32 return $object->getStatus(); 33 + case PhabricatorProjectColumnTransaction::TYPE_LIMIT: 34 + return $object->getPointLimit(); 35 + 32 36 } 33 37 34 38 return parent::getCustomTransactionOldValue($object, $xaction); ··· 42 46 case PhabricatorProjectColumnTransaction::TYPE_NAME: 43 47 case PhabricatorProjectColumnTransaction::TYPE_STATUS: 44 48 return $xaction->getNewValue(); 49 + case PhabricatorProjectColumnTransaction::TYPE_LIMIT: 50 + if ($xaction->getNewValue()) { 51 + return (int)$xaction->getNewValue(); 52 + } 53 + return null; 45 54 } 46 55 47 56 return parent::getCustomTransactionNewValue($object, $xaction); ··· 58 67 case PhabricatorProjectColumnTransaction::TYPE_STATUS: 59 68 $object->setStatus($xaction->getNewValue()); 60 69 return; 70 + case PhabricatorProjectColumnTransaction::TYPE_LIMIT: 71 + $object->setPointLimit($xaction->getNewValue()); 72 + return; 61 73 } 62 74 63 75 return parent::applyCustomInternalTransaction($object, $xaction); ··· 70 82 switch ($xaction->getTransactionType()) { 71 83 case PhabricatorProjectColumnTransaction::TYPE_NAME: 72 84 case PhabricatorProjectColumnTransaction::TYPE_STATUS: 85 + case PhabricatorProjectColumnTransaction::TYPE_LIMIT: 73 86 return; 74 87 } 75 88 ··· 84 97 $errors = parent::validateTransaction($object, $type, $xactions); 85 98 86 99 switch ($type) { 100 + case PhabricatorProjectColumnTransaction::TYPE_LIMIT: 101 + foreach ($xactions as $xaction) { 102 + $value = $xaction->getNewValue(); 103 + if (strlen($value) && !preg_match('/^\d+\z/', $value)) { 104 + $errors[] = new PhabricatorApplicationTransactionValidationError( 105 + $type, 106 + pht('Invalid'), 107 + pht('Column point limit must be empty, or a positive integer.'), 108 + $xaction); 109 + } 110 + } 111 + break; 87 112 case PhabricatorProjectColumnTransaction::TYPE_NAME: 88 113 $missing = $this->validateIsEmptyTextField( 89 114 $object->getName(),
+27 -4
src/applications/project/storage/PhabricatorProjectColumn.php
··· 71 71 return pht('Unnamed Column'); 72 72 } 73 73 74 - public function getHeaderColor() { 74 + public function getHeaderIcon() { 75 + $icon = null; 76 + 75 77 if ($this->isHidden()) { 76 - return PHUIActionHeaderView::HEADER_LIGHTRED; 78 + $icon = 'fa-eye-slash'; 79 + $text = pht('Hidden'); 77 80 } 78 81 79 82 if ($this->isDefaultColumn()) { 80 - return PHUIActionHeaderView::HEADER_DARK_GREY; 83 + $icon = 'fa-archive'; 84 + $text = pht('Default'); 85 + } 86 + 87 + if ($icon) { 88 + return id(new PHUIIconView()) 89 + ->setIconFont($icon) 90 + ->addSigil('has-tooltip') 91 + ->setMetadata( 92 + array( 93 + 'tip' => $text, 94 + ));; 81 95 } 82 96 83 - return PHUIActionHeaderView::HEADER_GREY; 97 + return null; 84 98 } 85 99 86 100 public function getProperty($key, $default = null) { ··· 89 103 90 104 public function setProperty($key, $value) { 91 105 $this->properties[$key] = $value; 106 + return $this; 107 + } 108 + 109 + public function getPointLimit() { 110 + return $this->getProperty('pointLimit'); 111 + } 112 + 113 + public function setPointLimit($limit) { 114 + $this->setProperty('pointLimit', $limit); 92 115 return $this; 93 116 } 94 117
+19
src/applications/project/storage/PhabricatorProjectColumnTransaction.php
··· 5 5 6 6 const TYPE_NAME = 'project:col:name'; 7 7 const TYPE_STATUS = 'project:col:status'; 8 + const TYPE_LIMIT = 'project:col:limit'; 8 9 9 10 public function getApplicationName() { 10 11 return 'project'; ··· 43 44 $author_handle); 44 45 } 45 46 } 47 + case PhabricatorProjectColumnTransaction::TYPE_LIMIT: 48 + if (!$old) { 49 + return pht( 50 + '%s set the point limit for this column to %s.', 51 + $author_handle, 52 + $new); 53 + } else if (!$new) { 54 + return pht( 55 + '%s removed the point limit for this column.', 56 + $author_handle); 57 + } else { 58 + return pht( 59 + '%s changed point limit for this column from %s to %s.', 60 + $author_handle, 61 + $old, 62 + $new); 63 + } 64 + 46 65 case PhabricatorProjectColumnTransaction::TYPE_STATUS: 47 66 switch ($new) { 48 67 case PhabricatorProjectColumn::STATUS_ACTIVE:
+14
src/view/phui/PHUIWorkpanelView.php
··· 8 8 private $headerColor = PHUIActionHeaderView::HEADER_GREY; 9 9 private $headerActions = array(); 10 10 private $headerTag; 11 + private $headerIcon; 12 + 13 + public function setHeaderIcon(PHUIIconView $header_icon) { 14 + $this->headerIcon = $header_icon; 15 + return $this; 16 + } 17 + 18 + public function getHeaderIcon() { 19 + return $this->headerIcon; 20 + } 11 21 12 22 public function setCards(PHUIObjectItemListView $cards) { 13 23 $this->cards[] = $cards; ··· 64 74 $header = id(new PHUIActionHeaderView()) 65 75 ->setHeaderTitle($this->header) 66 76 ->setHeaderColor($this->headerColor); 77 + 78 + if ($this->headerIcon) { 79 + $header->setHeaderIcon($this->headerIcon); 80 + } 67 81 68 82 if ($this->headerTag) { 69 83 $header->setTag($this->headerTag);
+9 -4
webroot/rsrc/css/phui/phui-workpanel-view.css
··· 93 93 width: auto; 94 94 } 95 95 96 - .project-column-empty { 96 + .project-panel-hidden { 97 + opacity: 0.75; 98 + } 99 + 100 + .project-panel-empty .phui-object-item-list-view { 97 101 background: rgba(255,255,255,.4); 98 102 border-radius: 3px; 99 103 margin-bottom: 4px; 100 104 border: 1px dashed #fff; 101 105 } 102 106 103 - .project-column-empty .drag-ghost { 107 + .project-panel-empty .phui-object-item-list-view .drag-ghost { 104 108 display: none; 105 109 } 106 110 107 - .project-column-empty.drag-target-list { 111 + .project-panel-empty .phui-object-item-list-view.drag-target-list { 108 112 background: rgba(255,255,255,.7); 109 113 } 110 114 111 - .phui-workpanel-view .phui-workpanel-lightred .phui-action-header { 115 + .project-panel-over-limit .phui-action-header { 112 116 border-top: 1px solid {$redborder}; 113 117 border-left: 1px solid {$redborder}; 114 118 border-right: 1px solid {$redborder}; 119 + background: {$lightredbackground}; 115 120 } 116 121 117 122 /* - Workpanel Cards -----------------------------------------------------------
+21 -7
webroot/rsrc/js/application/projects/behavior-project-boards.js
··· 18 18 var data = JX.Stratcom.getData(col); 19 19 var cards = finditems(col); 20 20 21 - // Add the "empty" CSS class if the column has nothing in it. 22 - JX.DOM.alterClass(col, 'project-column-empty', !cards.length); 23 - 24 21 // Update the count of tasks in the column header. 25 22 if (!data.countTagNode) { 26 23 data.countTagNode = JX.$(data.countTagID); ··· 33 30 sum += 1; 34 31 } 35 32 36 - JX.DOM.setContent(JX.$(data.countTagContentID), sum); 37 - 38 33 // TODO: This is a little bit hacky, but we don't have a PHUIX version of 39 34 // this element yet. 40 35 36 + var over_limit = (data.pointLimit && (sum > data.pointLimit)); 37 + 38 + var display_value = sum; 39 + if (data.pointLimit) { 40 + display_value = sum + ' / ' + data.pointLimit; 41 + } 42 + JX.DOM.setContent(JX.$(data.countTagContentID), display_value); 43 + 44 + 45 + var panel_map = { 46 + 'project-panel-empty': !cards.length, 47 + 'project-panel-over-limit': over_limit 48 + }; 49 + var panel = JX.DOM.findAbove(col, 'div', 'workpanel'); 50 + for (var k in panel_map) { 51 + JX.DOM.alterClass(panel, k, !!panel_map[k]); 52 + } 53 + 41 54 var color_map = { 42 55 'phui-tag-shade-disabled': (sum === 0), 43 - 'phui-tag-shade-blue': (sum > 0) 56 + 'phui-tag-shade-blue': (sum > 0 && !over_limit), 57 + 'phui-tag-shade-red': (over_limit) 44 58 }; 45 59 for (var k in color_map) { 46 - JX.DOM.alterClass(data.countTagNode, k, color_map[k]); 60 + JX.DOM.alterClass(data.countTagNode, k, !!color_map[k]); 47 61 } 48 62 } 49 63