@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

Preview the effects of a drag-and-drop operation on workboards

Summary:
Ref T10335. Ref T5474. When you drag-and-drop a card on a workboard, show a UI hint which lists all the things that the operation will do.

This shows: column moves; changes because of dragging a card to a different header; and changes which will be caused by triggers.

Not implemented here:

- Actions are currently shown even if they have no effect. For example, if you drag a "Normal" task to a different column, it says "Change priority to Normal.". I plan to hide actions which have no effect, but figuring this out is a little bit tricky.
- I'd like to make "trigger effects" vs "non-trigger effects" a little more clear in the future, probably.

Test Plan:
Dragged stuff between columns and headers, and into columns with triggers. Got appropriate preview text hints previewing what the action would do in the UI.

(This is tricky to take a screenshot of since it only shows up while the mouse cursor is down.)

Reviewers: amckinley

Reviewed By: amckinley

Maniphest Tasks: T10335, T5474

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

+522 -101
+60 -53
resources/celerity/map.php
··· 10 10 'conpherence.pkg.css' => '3c8a0668', 11 11 'conpherence.pkg.js' => '020aebcf', 12 12 'core.pkg.css' => 'b797945d', 13 - 'core.pkg.js' => 'f9c2509b', 13 + 'core.pkg.js' => 'eaca003c', 14 14 'differential.pkg.css' => '8d8360fb', 15 15 'differential.pkg.js' => '67e02996', 16 16 'diffusion.pkg.css' => '42c75c37', ··· 178 178 'rsrc/css/phui/workboards/phui-workboard-color.css' => 'e86de308', 179 179 'rsrc/css/phui/workboards/phui-workboard.css' => '74fc9d98', 180 180 'rsrc/css/phui/workboards/phui-workcard.css' => '9e9eb0df', 181 - 'rsrc/css/phui/workboards/phui-workpanel.css' => 'c5b408ad', 181 + 'rsrc/css/phui/workboards/phui-workpanel.css' => 'e5461a51', 182 182 'rsrc/css/sprite-login.css' => '18b368a6', 183 183 'rsrc/css/sprite-tokens.css' => 'f1896dc5', 184 184 'rsrc/css/syntax/syntax-default.css' => '055fc231', ··· 408 408 'rsrc/js/application/phortune/phortune-credit-card-form.js' => 'd12d214f', 409 409 'rsrc/js/application/policy/behavior-policy-control.js' => '0eaa33a9', 410 410 'rsrc/js/application/policy/behavior-policy-rule-editor.js' => '9347f172', 411 - 'rsrc/js/application/projects/WorkboardBoard.js' => '9d59f098', 411 + 'rsrc/js/application/projects/WorkboardBoard.js' => 'ba6e36b0', 412 412 'rsrc/js/application/projects/WorkboardCard.js' => '0392a5d8', 413 413 'rsrc/js/application/projects/WorkboardCardTemplate.js' => '2a61f8d4', 414 - 'rsrc/js/application/projects/WorkboardColumn.js' => 'ec5c5ce0', 414 + 'rsrc/js/application/projects/WorkboardColumn.js' => 'c344eb3c', 415 415 'rsrc/js/application/projects/WorkboardController.js' => '42c7a5a7', 416 + 'rsrc/js/application/projects/WorkboardDropEffect.js' => '101121be', 416 417 'rsrc/js/application/projects/WorkboardHeader.js' => '111bfd2d', 417 - 'rsrc/js/application/projects/WorkboardHeaderTemplate.js' => 'b65351bd', 418 + 'rsrc/js/application/projects/WorkboardHeaderTemplate.js' => 'ebe83a6b', 418 419 'rsrc/js/application/projects/WorkboardOrderTemplate.js' => '03e8891f', 419 - 'rsrc/js/application/projects/behavior-project-boards.js' => '412af9d4', 420 + 'rsrc/js/application/projects/behavior-project-boards.js' => 'cd7c9d4f', 420 421 'rsrc/js/application/projects/behavior-project-create.js' => '34c53422', 421 422 'rsrc/js/application/projects/behavior-reorder-columns.js' => '8ac32fd9', 422 423 'rsrc/js/application/releeph/releeph-preview-branch.js' => '75184d68', ··· 437 438 'rsrc/js/application/uiexample/notification-example.js' => '29819b75', 438 439 'rsrc/js/core/Busy.js' => '5202e831', 439 440 'rsrc/js/core/DragAndDropFileUpload.js' => '4370900d', 440 - 'rsrc/js/core/DraggableList.js' => '8bc7d797', 441 + 'rsrc/js/core/DraggableList.js' => 'c9ad6f70', 441 442 'rsrc/js/core/Favicon.js' => '7930776a', 442 443 'rsrc/js/core/FileUpload.js' => 'ab85e184', 443 444 'rsrc/js/core/Hovercard.js' => '074f0783', ··· 657 658 'javelin-behavior-phuix-example' => 'c2c500a7', 658 659 'javelin-behavior-policy-control' => '0eaa33a9', 659 660 'javelin-behavior-policy-rule-editor' => '9347f172', 660 - 'javelin-behavior-project-boards' => '412af9d4', 661 + 'javelin-behavior-project-boards' => 'cd7c9d4f', 661 662 'javelin-behavior-project-create' => '34c53422', 662 663 'javelin-behavior-quicksand-blacklist' => '5a6f6a06', 663 664 'javelin-behavior-read-only-warning' => 'b9109f8f', ··· 729 730 'javelin-view-renderer' => '9aae2b66', 730 731 'javelin-view-visitor' => '308f9fe4', 731 732 'javelin-websocket' => 'fdc13e4e', 732 - 'javelin-workboard-board' => '9d59f098', 733 + 'javelin-workboard-board' => 'ba6e36b0', 733 734 'javelin-workboard-card' => '0392a5d8', 734 735 'javelin-workboard-card-template' => '2a61f8d4', 735 - 'javelin-workboard-column' => 'ec5c5ce0', 736 + 'javelin-workboard-column' => 'c344eb3c', 736 737 'javelin-workboard-controller' => '42c7a5a7', 738 + 'javelin-workboard-drop-effect' => '101121be', 737 739 'javelin-workboard-header' => '111bfd2d', 738 - 'javelin-workboard-header-template' => 'b65351bd', 740 + 'javelin-workboard-header-template' => 'ebe83a6b', 739 741 'javelin-workboard-order-template' => '03e8891f', 740 742 'javelin-workflow' => '958e9045', 741 743 'maniphest-report-css' => '3d53188b', ··· 761 763 'phabricator-diff-changeset-list' => '04023d82', 762 764 'phabricator-diff-inline' => 'a4a14a94', 763 765 'phabricator-drag-and-drop-file-upload' => '4370900d', 764 - 'phabricator-draggable-list' => '8bc7d797', 766 + 'phabricator-draggable-list' => 'c9ad6f70', 765 767 'phabricator-fatal-config-template-css' => '20babf50', 766 768 'phabricator-favicon' => '7930776a', 767 769 'phabricator-feed-css' => 'd8b6e3f8', ··· 860 862 'phui-workboard-color-css' => 'e86de308', 861 863 'phui-workboard-view-css' => '74fc9d98', 862 864 'phui-workcard-view-css' => '9e9eb0df', 863 - 'phui-workpanel-view-css' => 'c5b408ad', 865 + 'phui-workpanel-view-css' => 'e5461a51', 864 866 'phuix-action-list-view' => 'c68f183f', 865 867 'phuix-action-view' => 'aaa08f3b', 866 868 'phuix-autocomplete' => '8f139ef0', ··· 1001 1003 'javelin-workflow', 1002 1004 'phuix-icon-view', 1003 1005 ), 1006 + '101121be' => array( 1007 + 'javelin-install', 1008 + 'javelin-dom', 1009 + ), 1004 1010 '111bfd2d' => array( 1005 1011 'javelin-install', 1006 1012 ), ··· 1226 1232 '407ee861' => array( 1227 1233 'javelin-behavior', 1228 1234 'javelin-uri', 1229 - ), 1230 - '412af9d4' => array( 1231 - 'javelin-behavior', 1232 - 'javelin-dom', 1233 - 'javelin-util', 1234 - 'javelin-vector', 1235 - 'javelin-stratcom', 1236 - 'javelin-workflow', 1237 - 'javelin-workboard-controller', 1238 1235 ), 1239 1236 '4234f572' => array( 1240 1237 'syntax-default-css', ··· 1593 1590 'javelin-dom', 1594 1591 'javelin-typeahead-normalizer', 1595 1592 ), 1596 - '8bc7d797' => array( 1597 - 'javelin-install', 1598 - 'javelin-dom', 1599 - 'javelin-stratcom', 1600 - 'javelin-util', 1601 - 'javelin-vector', 1602 - 'javelin-magical-init', 1603 - ), 1604 1593 '8c2ed2bf' => array( 1605 1594 'javelin-behavior', 1606 1595 'javelin-dom', ··· 1725 1714 'javelin-uri', 1726 1715 'phabricator-textareautils', 1727 1716 ), 1728 - '9d59f098' => array( 1729 - 'javelin-install', 1730 - 'javelin-dom', 1731 - 'javelin-util', 1732 - 'javelin-stratcom', 1733 - 'javelin-workflow', 1734 - 'phabricator-draggable-list', 1735 - 'javelin-workboard-column', 1736 - 'javelin-workboard-header-template', 1737 - 'javelin-workboard-card-template', 1738 - 'javelin-workboard-order-template', 1739 - ), 1740 1717 '9f081f05' => array( 1741 1718 'javelin-behavior', 1742 1719 'javelin-dom', ··· 1885 1862 'javelin-stratcom', 1886 1863 'javelin-dom', 1887 1864 ), 1888 - 'b65351bd' => array( 1889 - 'javelin-install', 1890 - ), 1891 1865 'b7b73831' => array( 1892 1866 'javelin-behavior', 1893 1867 'javelin-dom', ··· 1906 1880 'javelin-uri', 1907 1881 'phabricator-notification', 1908 1882 ), 1883 + 'ba6e36b0' => array( 1884 + 'javelin-install', 1885 + 'javelin-dom', 1886 + 'javelin-util', 1887 + 'javelin-stratcom', 1888 + 'javelin-workflow', 1889 + 'phabricator-draggable-list', 1890 + 'javelin-workboard-column', 1891 + 'javelin-workboard-header-template', 1892 + 'javelin-workboard-card-template', 1893 + 'javelin-workboard-order-template', 1894 + ), 1909 1895 'bdce4d78' => array( 1910 1896 'javelin-install', 1911 1897 'javelin-util', ··· 1930 1916 'javelin-dom', 1931 1917 'phuix-button-view', 1932 1918 ), 1919 + 'c344eb3c' => array( 1920 + 'javelin-install', 1921 + 'javelin-workboard-card', 1922 + 'javelin-workboard-header', 1923 + ), 1933 1924 'c3703a16' => array( 1934 1925 'javelin-behavior', 1935 1926 'javelin-aphlict', 1936 1927 'phabricator-phtize', 1937 1928 'javelin-dom', 1938 - ), 1939 - 'c5b408ad' => array( 1940 - 'phui-workcard-view-css', 1941 1929 ), 1942 1930 'c687e867' => array( 1943 1931 'javelin-behavior', ··· 1978 1966 'javelin-util', 1979 1967 'phabricator-keyboard-shortcut-manager', 1980 1968 ), 1969 + 'c9ad6f70' => array( 1970 + 'javelin-install', 1971 + 'javelin-dom', 1972 + 'javelin-stratcom', 1973 + 'javelin-util', 1974 + 'javelin-vector', 1975 + 'javelin-magical-init', 1976 + ), 1977 + 'cd7c9d4f' => array( 1978 + 'javelin-behavior', 1979 + 'javelin-dom', 1980 + 'javelin-util', 1981 + 'javelin-vector', 1982 + 'javelin-stratcom', 1983 + 'javelin-workflow', 1984 + 'javelin-workboard-controller', 1985 + 'javelin-workboard-drop-effect', 1986 + ), 1981 1987 'cf32921f' => array( 1982 1988 'javelin-behavior', 1983 1989 'javelin-dom', ··· 2038 2044 'javelin-dom', 2039 2045 'javelin-history', 2040 2046 ), 2047 + 'e5461a51' => array( 2048 + 'phui-workcard-view-css', 2049 + ), 2041 2050 'e562708c' => array( 2042 2051 'javelin-install', 2043 2052 ), ··· 2068 2077 'javelin-install', 2069 2078 'javelin-event', 2070 2079 ), 2080 + 'ebe83a6b' => array( 2081 + 'javelin-install', 2082 + ), 2071 2083 'ec4e31c0' => array( 2072 2084 'phui-timeline-view-css', 2073 - ), 2074 - 'ec5c5ce0' => array( 2075 - 'javelin-install', 2076 - 'javelin-workboard-card', 2077 - 'javelin-workboard-header', 2078 2085 ), 2079 2086 'ee77366f' => array( 2080 2087 'aphront-dialog-view-css',
+2
src/__phutil_library_map__.php
··· 4094 4094 'PhabricatorProjectDefaultController' => 'applications/project/controller/PhabricatorProjectDefaultController.php', 4095 4095 'PhabricatorProjectDescriptionField' => 'applications/project/customfield/PhabricatorProjectDescriptionField.php', 4096 4096 'PhabricatorProjectDetailsProfileMenuItem' => 'applications/project/menuitem/PhabricatorProjectDetailsProfileMenuItem.php', 4097 + 'PhabricatorProjectDropEffect' => 'applications/project/icon/PhabricatorProjectDropEffect.php', 4097 4098 'PhabricatorProjectEditController' => 'applications/project/controller/PhabricatorProjectEditController.php', 4098 4099 'PhabricatorProjectEditEngine' => 'applications/project/engine/PhabricatorProjectEditEngine.php', 4099 4100 'PhabricatorProjectEditPictureController' => 'applications/project/controller/PhabricatorProjectEditPictureController.php', ··· 10219 10220 'PhabricatorProjectDefaultController' => 'PhabricatorProjectBoardController', 10220 10221 'PhabricatorProjectDescriptionField' => 'PhabricatorProjectStandardCustomField', 10221 10222 'PhabricatorProjectDetailsProfileMenuItem' => 'PhabricatorProfileMenuItem', 10223 + 'PhabricatorProjectDropEffect' => 'Phobject', 10222 10224 'PhabricatorProjectEditController' => 'PhabricatorProjectController', 10223 10225 'PhabricatorProjectEditEngine' => 'PhabricatorEditEngine', 10224 10226 'PhabricatorProjectEditPictureController' => 'PhabricatorProjectController',
+13 -3
src/applications/project/controller/PhabricatorProjectBoardViewController.php
··· 540 540 ->setExcludedProjectPHIDs($select_phids); 541 541 542 542 $templates = array(); 543 - $column_maps = array(); 544 543 $all_tasks = array(); 544 + $column_templates = array(); 545 545 foreach ($visible_columns as $column_phid => $column) { 546 546 $column_tasks = $column_phids[$column_phid]; 547 547 ··· 606 606 'pointLimit' => $column->getPointLimit(), 607 607 )); 608 608 609 + $card_phids = array(); 609 610 foreach ($column_tasks as $task) { 610 611 $object_phid = $task->getPHID(); 611 612 612 613 $card = $rendering_engine->renderCard($object_phid); 613 614 $templates[$object_phid] = hsprintf('%s', $card->getItem()); 614 - $column_maps[$column_phid][] = $object_phid; 615 + $card_phids[] = $object_phid; 615 616 616 617 $all_tasks[$object_phid] = $task; 617 618 } 618 619 619 620 $panel->setCards($cards); 620 621 $board->addPanel($panel); 622 + 623 + $drop_effects = $column->getDropEffects(); 624 + $drop_effects = mpull($drop_effects, 'toDictionary'); 625 + 626 + $column_templates[] = array( 627 + 'columnPHID' => $column_phid, 628 + 'effects' => $drop_effects, 629 + 'cardPHIDs' => $card_phids, 630 + ); 621 631 } 622 632 623 633 $order_key = $this->sortKey; ··· 661 671 'headers' => $headers, 662 672 'headerKeys' => $header_keys, 663 673 'templateMap' => $templates, 664 - 'columnMaps' => $column_maps, 665 674 'orderMaps' => $vector_map, 666 675 'propertyMaps' => $properties, 676 + 'columnTemplates' => $column_templates, 667 677 668 678 'boardID' => $board_id, 669 679 'projectPHID' => $project->getPHID(),
+45
src/applications/project/icon/PhabricatorProjectDropEffect.php
··· 1 + <?php 2 + 3 + final class PhabricatorProjectDropEffect 4 + extends Phobject { 5 + 6 + private $icon; 7 + private $color; 8 + private $content; 9 + 10 + public function setIcon($icon) { 11 + $this->icon = $icon; 12 + return $this; 13 + } 14 + 15 + public function getIcon() { 16 + return $this->icon; 17 + } 18 + 19 + public function setColor($color) { 20 + $this->color = $color; 21 + return $this; 22 + } 23 + 24 + public function getColor() { 25 + return $this->color; 26 + } 27 + 28 + public function setContent($content) { 29 + $this->content = $content; 30 + return $this; 31 + } 32 + 33 + public function getContent() { 34 + return $this->content; 35 + } 36 + 37 + public function toDictionary() { 38 + return array( 39 + 'icon' => $this->getIcon(), 40 + 'color' => $this->getColor(), 41 + 'content' => hsprintf('%s', $this->getContent()), 42 + ); 43 + } 44 + 45 + }
+11
src/applications/project/order/PhabricatorProjectColumnHeader.php
··· 9 9 private $name; 10 10 private $icon; 11 11 private $editProperties; 12 + private $dropEffects = array(); 12 13 13 14 public function setOrderKey($order_key) { 14 15 $this->orderKey = $order_key; ··· 64 65 return $this->editProperties; 65 66 } 66 67 68 + public function addDropEffect(PhabricatorProjectDropEffect $effect) { 69 + $this->dropEffects[] = $effect; 70 + return $this; 71 + } 72 + 73 + public function getDropEffects() { 74 + return $this->dropEffects; 75 + } 76 + 67 77 public function toDictionary() { 68 78 return array( 69 79 'order' => $this->getOrderKey(), ··· 71 81 'template' => hsprintf('%s', $this->newView()), 72 82 'vector' => $this->getSortVector(), 73 83 'editProperties' => $this->getEditProperties(), 84 + 'effects' => mpull($this->getDropEffects(), 'toDictionary'), 74 85 ); 75 86 } 76 87
+4
src/applications/project/order/PhabricatorProjectColumnOrder.php
··· 196 196 ->setOrderKey($this->getColumnOrderKey()); 197 197 } 198 198 199 + final protected function newEffect() { 200 + return new PhabricatorProjectDropEffect(); 201 + } 202 + 199 203 final public function toDictionary() { 200 204 return array( 201 205 'orderKey' => $this->getColumnOrderKey(),
+15
src/applications/project/order/PhabricatorProjectColumnOwnerOrder.php
··· 122 122 $header_key = $this->newHeaderKeyForOwnerPHID($owner_phid); 123 123 124 124 $owner_image = null; 125 + $effect_content = null; 125 126 if ($owner_phid === null) { 126 127 $owner = null; 127 128 $sort_vector = $this->newSortVectorForUnowned(); 128 129 $owner_name = pht('Not Assigned'); 130 + 131 + $effect_content = pht('Remove task assignee.'); 129 132 } else { 130 133 $owner = idx($owner_users, $owner_phid); 131 134 if ($owner) { 132 135 $sort_vector = $this->newSortVectorForOwner($owner); 133 136 $owner_name = $owner->getUsername(); 134 137 $owner_image = $owner->getProfileImageURI(); 138 + 139 + $effect_content = pht( 140 + 'Assign task to %s.', 141 + phutil_tag('strong', array(), $owner_name)); 135 142 } else { 136 143 $sort_vector = $this->newSortVectorForOwnerPHID($owner_phid); 137 144 $owner_name = pht('Unknown User ("%s")', $owner_phid); ··· 158 165 array( 159 166 'value' => $owner_phid, 160 167 )); 168 + 169 + if ($effect_content !== null) { 170 + $header->addDropEffect( 171 + $this->newEffect() 172 + ->setIcon($owner_icon) 173 + ->setColor($owner_color) 174 + ->setContent($effect_content)); 175 + } 161 176 162 177 $headers[] = $header; 163 178 }
+10 -1
src/applications/project/order/PhabricatorProjectColumnPriorityOrder.php
··· 65 65 $icon_view = id(new PHUIIconView()) 66 66 ->setIcon($priority_icon, $priority_color); 67 67 68 + $drop_effect = $this->newEffect() 69 + ->setIcon($priority_icon) 70 + ->setColor($priority_color) 71 + ->setContent( 72 + pht( 73 + 'Change priority to %s.', 74 + phutil_tag('strong', array(), $priority_name))); 75 + 68 76 $header = $this->newHeader() 69 77 ->setHeaderKey($header_key) 70 78 ->setSortVector($sort_vector) ··· 73 81 ->setEditProperties( 74 82 array( 75 83 'value' => (int)$priority, 76 - )); 84 + )) 85 + ->addDropEffect($drop_effect); 77 86 78 87 $headers[] = $header; 79 88 }
+10 -1
src/applications/project/order/PhabricatorProjectColumnStatusOrder.php
··· 72 72 $icon_view = id(new PHUIIconView()) 73 73 ->setIcon($status_icon, $status_color); 74 74 75 + $drop_effect = $this->newEffect() 76 + ->setIcon($status_icon) 77 + ->setColor($status_color) 78 + ->setContent( 79 + pht( 80 + 'Change status to %s.', 81 + phutil_tag('strong', array(), $status_name))); 82 + 75 83 $header = $this->newHeader() 76 84 ->setHeaderKey($header_key) 77 85 ->setSortVector($sort_vector) ··· 80 88 ->setEditProperties( 81 89 array( 82 90 'value' => $status_key, 83 - )); 91 + )) 92 + ->addDropEffect($drop_effect); 84 93 85 94 $headers[] = $header; 86 95 }
+35
src/applications/project/storage/PhabricatorProjectColumn.php
··· 218 218 $this->getProject()->getID()); 219 219 } 220 220 221 + public function getDropEffects() { 222 + $effects = array(); 223 + 224 + $proxy = $this->getProxy(); 225 + if ($proxy && $proxy->isMilestone()) { 226 + $effects[] = id(new PhabricatorProjectDropEffect()) 227 + ->setIcon($proxy->getProxyColumnIcon()) 228 + ->setColor('violet') 229 + ->setContent( 230 + pht( 231 + 'Move to milestone %s.', 232 + phutil_tag('strong', array(), $this->getDisplayName()))); 233 + } else { 234 + $effects[] = id(new PhabricatorProjectDropEffect()) 235 + ->setIcon('fa-columns') 236 + ->setColor('blue') 237 + ->setContent( 238 + pht( 239 + 'Move to column %s.', 240 + phutil_tag('strong', array(), $this->getDisplayName()))); 241 + } 242 + 243 + 244 + if ($this->canHaveTrigger()) { 245 + $trigger = $this->getTrigger(); 246 + if ($trigger) { 247 + foreach ($trigger->getDropEffects() as $trigger_effect) { 248 + $effects[] = $trigger_effect; 249 + } 250 + } 251 + } 252 + 253 + return $effects; 254 + } 255 + 221 256 222 257 /* -( PhabricatorConduitResultInterface )---------------------------------- */ 223 258
+13
src/applications/project/storage/PhabricatorProjectTrigger.php
··· 170 170 return $this->triggerRules; 171 171 } 172 172 173 + public function getDropEffects() { 174 + $effects = array(); 175 + 176 + $rules = $this->getTriggerRules(); 177 + foreach ($rules as $rule) { 178 + foreach ($rule->getDropEffects() as $effect) { 179 + $effects[] = $effect; 180 + } 181 + } 182 + 183 + return $effects; 184 + } 185 + 173 186 public function getRulesDescription() { 174 187 $rules = $this->getTriggerRules(); 175 188 if (!$rules) {
+4
src/applications/project/trigger/PhabricatorProjectTriggerInvalidRule.php
··· 19 19 return array(); 20 20 } 21 21 22 + protected function newDropEffects($value) { 23 + return array(); 24 + } 25 + 22 26 }
+17
src/applications/project/trigger/PhabricatorProjectTriggerManiphestStatusRule.php
··· 38 38 ); 39 39 } 40 40 41 + protected function newDropEffects($value) { 42 + $status_name = ManiphestTaskStatus::getTaskStatusName($value); 43 + $status_icon = ManiphestTaskStatus::getStatusIcon($value); 44 + $status_color = ManiphestTaskStatus::getStatusColor($value); 45 + 46 + $content = pht( 47 + 'Change status to %s.', 48 + phutil_tag('strong', array(), $status_name)); 49 + 50 + return array( 51 + $this->newEffect() 52 + ->setIcon($status_icon) 53 + ->setColor($status_color) 54 + ->setContent($content), 55 + ); 56 + } 57 + 41 58 }
+9
src/applications/project/trigger/PhabricatorProjectTriggerRule.php
··· 40 40 abstract public function getDescription(); 41 41 abstract protected function assertValidRuleValue($value); 42 42 abstract protected function newDropTransactions($object, $value); 43 + abstract protected function newDropEffects($value); 43 44 44 45 final public function getDropTransactions($object, $value) { 45 46 return $this->newDropTransactions($object, $value); ··· 84 85 85 86 final protected function newTransaction() { 86 87 return $this->getObject()->getApplicationTransactionTemplate(); 88 + } 89 + 90 + final public function getDropEffects() { 91 + return $this->newDropEffects($this->getValue()); 92 + } 93 + 94 + final protected function newEffect() { 95 + return new PhabricatorProjectDropEffect(); 87 96 } 88 97 89 98 }
+4
src/applications/project/trigger/PhabricatorProjectTriggerUnknownRule.php
··· 19 19 return array(); 20 20 } 21 21 22 + protected function newDropEffects($value) { 23 + return array(); 24 + } 25 + 22 26 }
+36
webroot/rsrc/css/phui/workboards/phui-workpanel.css
··· 178 178 margin-left: 36px; 179 179 overflow: hidden; 180 180 } 181 + 182 + .workboard-drop-preview { 183 + pointer-events: none; 184 + position: absolute; 185 + bottom: 12px; 186 + right: 12px; 187 + width: 300px; 188 + border-radius: 3px; 189 + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.15); 190 + border: 1px solid {$lightblueborder}; 191 + padding: 4px 0; 192 + } 193 + 194 + .workboard-drop-preview:hover { 195 + opacity: 0.25; 196 + } 197 + 198 + .workboard-drop-preview li { 199 + white-space: nowrap; 200 + overflow: hidden; 201 + text-overflow: ellipsis; 202 + margin: 4px 8px; 203 + color: {$greytext}; 204 + } 205 + 206 + .workboard-drop-preview li .phui-icon-view { 207 + position: relative; 208 + display: inline-block; 209 + text-align: center; 210 + width: 24px; 211 + height: 18px; 212 + padding-top: 6px; 213 + border-radius: 3px; 214 + background: {$bluebackground}; 215 + margin-right: 6px; 216 + }
+138 -34
webroot/rsrc/js/application/projects/WorkboardBoard.js
··· 39 39 _columns: null, 40 40 _headers: null, 41 41 _cards: null, 42 + _dropPreviewNode: null, 43 + _dropPreviewListNode: null, 42 44 43 45 getRoot: function() { 44 46 return this._root; ··· 180 182 list.setCompareOnReorder(true); 181 183 } 182 184 185 + list.setTargetChangeHandler(JX.bind(this, this._didChangeDropTarget)); 186 + 183 187 list.listen('didDrop', JX.bind(this, this._onmovecard, list)); 184 188 185 189 lists.push(list); ··· 190 194 } 191 195 }, 192 196 193 - _findCardsInColumn: function(column_node) { 194 - return JX.DOM.scry(column_node, 'li', 'project-card'); 195 - }, 197 + _didChangeDropTarget: function(src_list, src_node, dst_list, dst_node) { 198 + var node = this._getDropPreviewNode(); 196 199 197 - _onmovecard: function(list, item, after_node, src_list) { 198 - list.lock(); 199 - JX.DOM.alterClass(item, 'drag-sending', true); 200 + if (!dst_list) { 201 + // The card is being dragged into a dead area, like the left menu. 202 + JX.DOM.remove(node); 203 + return; 204 + } 205 + 206 + if (dst_node === false) { 207 + // The card is being dragged over itself, so dropping it won't 208 + // affect anything. 209 + JX.DOM.remove(node); 210 + return; 211 + } 200 212 201 213 var src_phid = JX.Stratcom.getData(src_list.getRootNode()).columnPHID; 202 - var dst_phid = JX.Stratcom.getData(list.getRootNode()).columnPHID; 214 + var dst_phid = JX.Stratcom.getData(dst_list.getRootNode()).columnPHID; 215 + 216 + var src_column = this.getColumn(src_phid); 217 + var dst_column = this.getColumn(dst_phid); 218 + 219 + var effects = []; 203 220 204 - var item_phid = JX.Stratcom.getData(item).objectPHID; 205 - var data = { 206 - objectPHID: item_phid, 207 - columnPHID: dst_phid, 208 - order: this.getOrder() 209 - }; 221 + if (src_column !== dst_column) { 222 + effects = effects.concat(dst_column.getDropEffects()); 223 + } 224 + 225 + var context = this._getDropContext(dst_node); 226 + if (context.headerKey) { 227 + var header = this.getHeaderTemplate(context.headerKey); 228 + effects = effects.concat(header.getDropEffects()); 229 + } 230 + 231 + if (!effects.length) { 232 + JX.DOM.remove(node); 233 + return; 234 + } 235 + 236 + var items = []; 237 + for (var ii = 0; ii < effects.length; ii++) { 238 + var effect = effects[ii]; 239 + items.push(effect.newNode()); 240 + } 241 + 242 + JX.DOM.setContent(this._getDropPreviewListNode(), items); 243 + 244 + document.body.appendChild(node); 245 + }, 246 + 247 + _getDropPreviewNode: function() { 248 + if (!this._dropPreviewNode) { 249 + var attributes = { 250 + className: 'workboard-drop-preview' 251 + }; 252 + 253 + var content = [ 254 + this._getDropPreviewListNode() 255 + ]; 256 + 257 + this._dropPreviewNode = JX.$N('div', attributes, content); 258 + } 259 + 260 + return this._dropPreviewNode; 261 + }, 262 + 263 + _getDropPreviewListNode: function() { 264 + if (!this._dropPreviewListNode) { 265 + var attributes = {}; 266 + this._dropPreviewListNode = JX.$N('ul', attributes); 267 + } 268 + 269 + return this._dropPreviewListNode; 270 + }, 271 + 272 + _findCardsInColumn: function(column_node) { 273 + return JX.DOM.scry(column_node, 'li', 'project-card'); 274 + }, 275 + 276 + _getDropContext: function(after_node, item) { 277 + var header_key; 278 + var before_phid; 279 + var after_phid; 210 280 211 281 // We're going to send an "afterPHID" and a "beforePHID" if the card 212 282 // was dropped immediately adjacent to another card. If a card was ··· 231 301 232 302 if (after_data) { 233 303 if (after_data.objectPHID) { 234 - data.afterPHID = after_data.objectPHID; 304 + after_phid = after_data.objectPHID; 235 305 } 236 306 } 237 307 238 - var before_data; 239 - var before_card = item.nextSibling; 240 - while (before_card) { 241 - before_data = JX.Stratcom.getData(before_card); 242 - if (before_data.objectPHID) { 243 - break; 244 - } 245 - if (before_data.headerKey) { 246 - break; 308 + if (item) { 309 + var before_data; 310 + var before_card = item.nextSibling; 311 + while (before_card) { 312 + before_data = JX.Stratcom.getData(before_card); 313 + if (before_data.objectPHID) { 314 + break; 315 + } 316 + if (before_data.headerKey) { 317 + break; 318 + } 319 + before_card = before_card.nextSibling; 247 320 } 248 - before_card = before_card.nextSibling; 249 - } 250 321 251 - if (before_data) { 252 - if (before_data.objectPHID) { 253 - data.beforePHID = before_data.objectPHID; 322 + if (before_data) { 323 + if (before_data.objectPHID) { 324 + before_phid = before_data.objectPHID; 325 + } 254 326 } 255 327 } 256 328 ··· 265 337 } 266 338 267 339 if (header_data) { 268 - var header_key = header_data.headerKey; 269 - if (header_key) { 270 - var properties = this.getHeaderTemplate(header_key) 271 - .getEditProperties(); 272 - data.header = JX.JSON.stringify(properties); 273 - } 340 + header_key = header_data.headerKey; 341 + } 342 + 343 + return { 344 + headerKey: header_key, 345 + afterPHID: after_phid, 346 + beforePHID: before_phid 347 + }; 348 + }, 349 + 350 + _onmovecard: function(list, item, after_node, src_list) { 351 + list.lock(); 352 + JX.DOM.alterClass(item, 'drag-sending', true); 353 + 354 + var src_phid = JX.Stratcom.getData(src_list.getRootNode()).columnPHID; 355 + var dst_phid = JX.Stratcom.getData(list.getRootNode()).columnPHID; 356 + 357 + var item_phid = JX.Stratcom.getData(item).objectPHID; 358 + var data = { 359 + objectPHID: item_phid, 360 + columnPHID: dst_phid, 361 + order: this.getOrder() 362 + }; 363 + 364 + var context = this._getDropContext(after_node); 365 + 366 + if (context.afterPHID) { 367 + data.afterPHID = context.afterPHID; 368 + } 369 + 370 + if (context.beforePHID) { 371 + data.beforePHID = context.beforePHID; 372 + } 373 + 374 + if (context.headerKey) { 375 + var properties = this.getHeaderTemplate(context.headerKey) 376 + .getEditProperties(); 377 + data.header = JX.JSON.stringify(properties); 274 378 } 275 379 276 380 var visible_phids = [];
+11
webroot/rsrc/js/application/projects/WorkboardColumn.js
··· 25 25 this._headers = {}; 26 26 this._objects = []; 27 27 this._naturalOrder = []; 28 + this._dropEffects = []; 28 29 }, 29 30 30 31 members: { ··· 40 41 _pointsContentNode: null, 41 42 _dirty: true, 42 43 _objects: null, 44 + _dropEffects: null, 43 45 44 46 getPHID: function() { 45 47 return this._phid; ··· 69 71 this._naturalOrder = order; 70 72 this._orderVectors = null; 71 73 return this; 74 + }, 75 + 76 + setDropEffects: function(effects) { 77 + this._dropEffects = effects; 78 + return this; 79 + }, 80 + 81 + getDropEffects: function() { 82 + return this._dropEffects; 72 83 }, 73 84 74 85 getPointsNode: function() {
+35
webroot/rsrc/js/application/projects/WorkboardDropEffect.js
··· 1 + /** 2 + * @provides javelin-workboard-drop-effect 3 + * @requires javelin-install 4 + * javelin-dom 5 + * @javelin 6 + */ 7 + 8 + JX.install('WorkboardDropEffect', { 9 + 10 + properties: { 11 + icon: null, 12 + color: null, 13 + content: null 14 + }, 15 + 16 + statics: { 17 + newFromDictionary: function(map) { 18 + return new JX.WorkboardDropEffect() 19 + .setIcon(map.icon) 20 + .setColor(map.color) 21 + .setContent(JX.$H(map.content)); 22 + } 23 + }, 24 + 25 + members: { 26 + newNode: function() { 27 + var icon = new JX.PHUIXIconView() 28 + .setIcon(this.getIcon()) 29 + .setColor(this.getColor()) 30 + .getNode(); 31 + 32 + return JX.$N('li', {}, [icon, this.getContent()]); 33 + } 34 + } 35 + });
+2 -1
webroot/rsrc/js/application/projects/WorkboardHeaderTemplate.js
··· 14 14 template: null, 15 15 order: null, 16 16 vector: null, 17 - editProperties: null 17 + editProperties: null, 18 + dropEffects: [] 18 19 }, 19 20 20 21 members: {
+28 -7
webroot/rsrc/js/application/projects/behavior-project-boards.js
··· 7 7 * javelin-stratcom 8 8 * javelin-workflow 9 9 * javelin-workboard-controller 10 + * javelin-workboard-drop-effect 10 11 */ 11 12 12 13 JX.behavior('project-boards', function(config, statics) { ··· 88 89 } 89 90 90 91 var ii; 91 - var column_maps = config.columnMaps; 92 - for (var column_phid in column_maps) { 93 - var column = board.getColumn(column_phid); 94 - var column_map = column_maps[column_phid]; 95 - for (ii = 0; ii < column_map.length; ii++) { 96 - column.newCard(column_map[ii]); 92 + var jj; 93 + var effects; 94 + 95 + for (ii = 0; ii < config.columnTemplates.length; ii++) { 96 + var spec = config.columnTemplates[ii]; 97 + 98 + var column = board.getColumn(spec.columnPHID); 99 + 100 + effects = []; 101 + for (jj = 0; jj < spec.effects.length; jj++) { 102 + effects.push( 103 + JX.WorkboardDropEffect.newFromDictionary( 104 + spec.effects[jj])); 105 + } 106 + column.setDropEffects(effects); 107 + 108 + for (jj = 0; jj < spec.cardPHIDs.length; jj++) { 109 + column.newCard(spec.cardPHIDs[jj]); 97 110 } 98 111 } 99 112 ··· 115 128 for (ii = 0; ii < headers.length; ii++) { 116 129 var header = headers[ii]; 117 130 131 + effects = []; 132 + for (jj = 0; jj < header.effects.length; jj++) { 133 + effects.push( 134 + JX.WorkboardDropEffect.newFromDictionary( 135 + header.effects[jj])); 136 + } 137 + 118 138 board.getHeaderTemplate(header.key) 119 139 .setOrder(header.order) 120 140 .setNodeHTMLTemplate(header.template) 121 141 .setVector(header.vector) 122 - .setEditProperties(header.editProperties); 142 + .setEditProperties(header.editProperties) 143 + .setDropEffects(effects); 123 144 } 124 145 125 146 var orders = config.orders;
+20 -1
webroot/rsrc/js/core/DraggableList.js
··· 45 45 outerContainer: null, 46 46 hasInfiniteHeight: false, 47 47 compareOnMove: false, 48 - compareOnReorder: false 48 + compareOnReorder: false, 49 + targetChangeHandler: null 49 50 }, 50 51 51 52 members : { ··· 53 54 _dragging : null, 54 55 _locked : 0, 55 56 _target : null, 57 + _lastTarget: null, 56 58 _targets : null, 57 59 _ghostHandler : null, 58 60 _ghostNode : null, ··· 372 374 return this; 373 375 }, 374 376 377 + _didChangeTarget: function(dst_list, dst_node) { 378 + if (dst_node === this._lastTarget) { 379 + return; 380 + } 381 + 382 + this._lastTarget = dst_node; 383 + 384 + var handler = this.getTargetChangeHandler(); 385 + if (handler) { 386 + handler(this, this._dragging, dst_list, dst_node); 387 + } 388 + }, 389 + 375 390 _setIsDropTarget: function(is_target) { 376 391 var root = this.getRootNode(); 377 392 JX.DOM.alterClass(root, 'drag-target-list', is_target); ··· 540 555 } 541 556 } 542 557 558 + this._didChangeTarget(target_list, cur_target); 559 + 543 560 this._updateAutoscroll(this._cursorPosition); 544 561 545 562 var f = JX.$V(this._frame); ··· 672 689 group[ii]._setIsDropTarget(false); 673 690 group[ii]._clearTarget(); 674 691 } 692 + 693 + this._didChangeTarget(null, null); 675 694 676 695 JX.DOM.alterClass(dragging, 'drag-dragging', false); 677 696 JX.Tooltip.unlock();