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

Use ObjectItemListView for Maniphest

Summary:
This isn't quite complete, but everything else is technical cleanup. Broadly:

- Removed checkboxes. Selected state is now indicated with CSS, and toggled with shift-click. When nothing is selected, the text reads "Shift-Click Tasks to Select" to let users discover this feature.
- Updated drag-to-reorder code to work with ObjectItemListView.
- Closed/resolved is now shown with a grey footer icon.
- Assigned is now shown with a user profile image handle icon, with a hover state.

This could probably use some more tweaks, but overall I think it looks pretty reasonable?

Test Plan: {F35897}

Reviewers: chad

Reviewed By: chad

CC: aran

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

+358 -530
+56 -55
src/__celerity_resource_map__.php
··· 1638 1638 ), 1639 1639 'javelin-behavior-maniphest-batch-selector' => 1640 1640 array( 1641 - 'uri' => '/res/398cf8d7/rsrc/js/application/maniphest/behavior-batch-selector.js', 1641 + 'uri' => '/res/f8cf3b84/rsrc/js/application/maniphest/behavior-batch-selector.js', 1642 1642 'type' => 'js', 1643 1643 'requires' => 1644 1644 array( 1645 1645 0 => 'javelin-behavior', 1646 1646 1 => 'javelin-dom', 1647 1647 2 => 'javelin-stratcom', 1648 + 3 => 'javelin-util', 1648 1649 ), 1649 1650 'disk' => '/rsrc/js/application/maniphest/behavior-batch-selector.js', 1650 1651 ), ··· 1663 1664 ), 1664 1665 'javelin-behavior-maniphest-subpriority-editor' => 1665 1666 array( 1666 - 'uri' => '/res/5e02f19a/rsrc/js/application/maniphest/behavior-subpriorityeditor.js', 1667 + 'uri' => '/res/21b73c2a/rsrc/js/application/maniphest/behavior-subpriorityeditor.js', 1667 1668 'type' => 'js', 1668 1669 'requires' => 1669 1670 array( ··· 2632 2633 ), 2633 2634 'maniphest-task-summary-css' => 2634 2635 array( 2635 - 'uri' => '/res/b3930263/rsrc/css/application/maniphest/task-summary.css', 2636 + 'uri' => '/res/7aa9e2eb/rsrc/css/application/maniphest/task-summary.css', 2636 2637 'type' => 'css', 2637 2638 'requires' => 2638 2639 array( ··· 2995 2996 ), 2996 2997 'phabricator-object-item-list-view-css' => 2997 2998 array( 2998 - 'uri' => '/res/aa09c531/rsrc/css/layout/phabricator-object-item-list-view.css', 2999 + 'uri' => '/res/7d35d590/rsrc/css/layout/phabricator-object-item-list-view.css', 2999 3000 'type' => 'css', 3000 3001 'requires' => 3001 3002 array( ··· 3693 3694 ), array( 3694 3695 'packages' => 3695 3696 array( 3696 - '6c294512' => 3697 + 'a56848af' => 3697 3698 array( 3698 3699 'name' => 'core.pkg.css', 3699 3700 'symbols' => ··· 3735 3736 34 => 'phabricator-object-item-list-view-css', 3736 3737 35 => 'global-drag-and-drop-css', 3737 3738 ), 3738 - 'uri' => '/res/pkg/6c294512/core.pkg.css', 3739 + 'uri' => '/res/pkg/a56848af/core.pkg.css', 3739 3740 'type' => 'css', 3740 3741 ), 3741 3742 '95ceba95' => ··· 3895 3896 'uri' => '/res/pkg/fe22443b/javelin.pkg.js', 3896 3897 'type' => 'js', 3897 3898 ), 3898 - 'c41b4907' => 3899 + '6b1fccc6' => 3899 3900 array( 3900 3901 'name' => 'maniphest.pkg.css', 3901 3902 'symbols' => ··· 3905 3906 2 => 'aphront-attached-file-view-css', 3906 3907 3 => 'phabricator-project-tag-css', 3907 3908 ), 3908 - 'uri' => '/res/pkg/c41b4907/maniphest.pkg.css', 3909 + 'uri' => '/res/pkg/6b1fccc6/maniphest.pkg.css', 3909 3910 'type' => 'css', 3910 3911 ), 3911 - '7707de41' => 3912 + 'f85eb6d8' => 3912 3913 array( 3913 3914 'name' => 'maniphest.pkg.js', 3914 3915 'symbols' => ··· 3919 3920 3 => 'javelin-behavior-maniphest-transaction-expand', 3920 3921 4 => 'javelin-behavior-maniphest-subpriority-editor', 3921 3922 ), 3922 - 'uri' => '/res/pkg/7707de41/maniphest.pkg.js', 3923 + 'uri' => '/res/pkg/f85eb6d8/maniphest.pkg.js', 3923 3924 'type' => 'js', 3924 3925 ), 3925 3926 ), 3926 3927 'reverse' => 3927 3928 array( 3928 - 'aphront-attached-file-view-css' => 'c41b4907', 3929 - 'aphront-dialog-view-css' => '6c294512', 3930 - 'aphront-error-view-css' => '6c294512', 3931 - 'aphront-form-view-css' => '6c294512', 3932 - 'aphront-list-filter-view-css' => '6c294512', 3933 - 'aphront-pager-view-css' => '6c294512', 3934 - 'aphront-panel-view-css' => '6c294512', 3935 - 'aphront-table-view-css' => '6c294512', 3936 - 'aphront-tokenizer-control-css' => '6c294512', 3937 - 'aphront-tooltip-css' => '6c294512', 3938 - 'aphront-typeahead-control-css' => '6c294512', 3929 + 'aphront-attached-file-view-css' => '6b1fccc6', 3930 + 'aphront-dialog-view-css' => 'a56848af', 3931 + 'aphront-error-view-css' => 'a56848af', 3932 + 'aphront-form-view-css' => 'a56848af', 3933 + 'aphront-list-filter-view-css' => 'a56848af', 3934 + 'aphront-pager-view-css' => 'a56848af', 3935 + 'aphront-panel-view-css' => 'a56848af', 3936 + 'aphront-table-view-css' => 'a56848af', 3937 + 'aphront-tokenizer-control-css' => 'a56848af', 3938 + 'aphront-tooltip-css' => 'a56848af', 3939 + 'aphront-typeahead-control-css' => 'a56848af', 3939 3940 'differential-changeset-view-css' => '8aaacd1b', 3940 3941 'differential-core-view-css' => '8aaacd1b', 3941 3942 'differential-inline-comment-editor' => '322728f3', ··· 3949 3950 'differential-table-of-contents-css' => '8aaacd1b', 3950 3951 'diffusion-commit-view-css' => 'c8ce2d88', 3951 3952 'diffusion-icons-css' => 'c8ce2d88', 3952 - 'global-drag-and-drop-css' => '6c294512', 3953 + 'global-drag-and-drop-css' => 'a56848af', 3953 3954 'inline-comment-summary-css' => '8aaacd1b', 3954 3955 'javelin-aphlict' => '95ceba95', 3955 3956 'javelin-behavior' => 'fe22443b', ··· 3982 3983 'javelin-behavior-konami' => '95ceba95', 3983 3984 'javelin-behavior-lightbox-attachments' => '95ceba95', 3984 3985 'javelin-behavior-load-blame' => '322728f3', 3985 - 'javelin-behavior-maniphest-batch-selector' => '7707de41', 3986 - 'javelin-behavior-maniphest-subpriority-editor' => '7707de41', 3987 - 'javelin-behavior-maniphest-transaction-controls' => '7707de41', 3988 - 'javelin-behavior-maniphest-transaction-expand' => '7707de41', 3989 - 'javelin-behavior-maniphest-transaction-preview' => '7707de41', 3986 + 'javelin-behavior-maniphest-batch-selector' => 'f85eb6d8', 3987 + 'javelin-behavior-maniphest-subpriority-editor' => 'f85eb6d8', 3988 + 'javelin-behavior-maniphest-transaction-controls' => 'f85eb6d8', 3989 + 'javelin-behavior-maniphest-transaction-expand' => 'f85eb6d8', 3990 + 'javelin-behavior-maniphest-transaction-preview' => 'f85eb6d8', 3990 3991 'javelin-behavior-phabricator-active-nav' => '95ceba95', 3991 3992 'javelin-behavior-phabricator-autofocus' => '95ceba95', 3992 3993 'javelin-behavior-phabricator-gesture' => '95ceba95', ··· 4021 4022 'javelin-util' => 'fe22443b', 4022 4023 'javelin-vector' => 'fe22443b', 4023 4024 'javelin-workflow' => 'fe22443b', 4024 - 'lightbox-attachment-css' => '6c294512', 4025 - 'maniphest-task-summary-css' => 'c41b4907', 4026 - 'maniphest-transaction-detail-css' => 'c41b4907', 4025 + 'lightbox-attachment-css' => 'a56848af', 4026 + 'maniphest-task-summary-css' => '6b1fccc6', 4027 + 'maniphest-transaction-detail-css' => '6b1fccc6', 4027 4028 'phabricator-busy' => '95ceba95', 4028 4029 'phabricator-content-source-view-css' => '8aaacd1b', 4029 - 'phabricator-core-buttons-css' => '6c294512', 4030 - 'phabricator-core-css' => '6c294512', 4031 - 'phabricator-crumbs-view-css' => '6c294512', 4032 - 'phabricator-directory-css' => '6c294512', 4030 + 'phabricator-core-buttons-css' => 'a56848af', 4031 + 'phabricator-core-css' => 'a56848af', 4032 + 'phabricator-crumbs-view-css' => 'a56848af', 4033 + 'phabricator-directory-css' => 'a56848af', 4033 4034 'phabricator-drag-and-drop-file-upload' => '322728f3', 4034 4035 'phabricator-dropdown-menu' => '95ceba95', 4035 4036 'phabricator-file-upload' => '95ceba95', 4036 - 'phabricator-filetree-view-css' => '6c294512', 4037 - 'phabricator-flag-css' => '6c294512', 4038 - 'phabricator-form-view-css' => '6c294512', 4039 - 'phabricator-header-view-css' => '6c294512', 4040 - 'phabricator-jump-nav' => '6c294512', 4037 + 'phabricator-filetree-view-css' => 'a56848af', 4038 + 'phabricator-flag-css' => 'a56848af', 4039 + 'phabricator-form-view-css' => 'a56848af', 4040 + 'phabricator-header-view-css' => 'a56848af', 4041 + 'phabricator-jump-nav' => 'a56848af', 4041 4042 'phabricator-keyboard-shortcut' => '95ceba95', 4042 4043 'phabricator-keyboard-shortcut-manager' => '95ceba95', 4043 - 'phabricator-main-menu-view' => '6c294512', 4044 + 'phabricator-main-menu-view' => 'a56848af', 4044 4045 'phabricator-menu-item' => '95ceba95', 4045 - 'phabricator-nav-view-css' => '6c294512', 4046 + 'phabricator-nav-view-css' => 'a56848af', 4046 4047 'phabricator-notification' => '95ceba95', 4047 - 'phabricator-notification-css' => '6c294512', 4048 - 'phabricator-notification-menu-css' => '6c294512', 4049 - 'phabricator-object-item-list-view-css' => '6c294512', 4048 + 'phabricator-notification-css' => 'a56848af', 4049 + 'phabricator-notification-menu-css' => 'a56848af', 4050 + 'phabricator-object-item-list-view-css' => 'a56848af', 4050 4051 'phabricator-object-selector-css' => '8aaacd1b', 4051 4052 'phabricator-paste-file-upload' => '95ceba95', 4052 4053 'phabricator-prefab' => '95ceba95', 4053 - 'phabricator-project-tag-css' => 'c41b4907', 4054 - 'phabricator-remarkup-css' => '6c294512', 4054 + 'phabricator-project-tag-css' => '6b1fccc6', 4055 + 'phabricator-remarkup-css' => 'a56848af', 4055 4056 'phabricator-shaped-request' => '322728f3', 4056 - 'phabricator-side-menu-view-css' => '6c294512', 4057 - 'phabricator-standard-page-view' => '6c294512', 4057 + 'phabricator-side-menu-view-css' => 'a56848af', 4058 + 'phabricator-standard-page-view' => 'a56848af', 4058 4059 'phabricator-textareautils' => '95ceba95', 4059 4060 'phabricator-tooltip' => '95ceba95', 4060 - 'phabricator-transaction-view-css' => '6c294512', 4061 - 'phabricator-zindex-css' => '6c294512', 4062 - 'sprite-apps-large-css' => '6c294512', 4063 - 'sprite-gradient-css' => '6c294512', 4064 - 'sprite-icon-css' => '6c294512', 4065 - 'sprite-menu-css' => '6c294512', 4066 - 'syntax-highlighting-css' => '6c294512', 4061 + 'phabricator-transaction-view-css' => 'a56848af', 4062 + 'phabricator-zindex-css' => 'a56848af', 4063 + 'sprite-apps-large-css' => 'a56848af', 4064 + 'sprite-gradient-css' => 'a56848af', 4065 + 'sprite-icon-css' => 'a56848af', 4066 + 'sprite-menu-css' => 'a56848af', 4067 + 'syntax-highlighting-css' => 'a56848af', 4067 4068 ), 4068 4069 ));
+19 -7
src/applications/maniphest/controller/ManiphestSubpriorityController.php
··· 7 7 8 8 public function processRequest() { 9 9 $request = $this->getRequest(); 10 + $user = $request->getUser(); 10 11 11 12 if (!$request->validateCSRF()) { 12 13 return new Aphront403Response(); ··· 50 51 $task->setSubpriority($new_sub); 51 52 $task->save(); 52 53 53 - $pri_class = ManiphestTaskSummaryView::getPriorityClass( 54 - $task->getPriority()); 55 - $class = 'maniphest-task-handle maniphest-active-handle '.$pri_class; 54 + $phids = $task->getProjectPHIDs(); 55 + if ($task->getOwnerPHID()) { 56 + $phids[] = $task->getOwnerPHID(); 57 + } 58 + 59 + $handles = id(new PhabricatorObjectHandleData($phids)) 60 + ->setViewer($user) 61 + ->loadHandles(); 56 62 57 - $response = array( 58 - 'className' => $class, 59 - ); 63 + $view = id(new ManiphestTaskListView()) 64 + ->setUser($user) 65 + ->setShowSubpriorityControls(true) 66 + ->setShowBatchControls(true) 67 + ->setHandles($handles) 68 + ->setTasks(array($task)); 60 69 61 - return id(new AphrontAjaxResponse())->setContent($response); 70 + return id(new AphrontAjaxResponse())->setContent( 71 + array( 72 + 'tasks' => $view, 73 + )); 62 74 } 63 75 64 76 }
+28 -16
src/applications/maniphest/controller/ManiphestTaskListController.php
··· 378 378 $selector->appendChild($lists); 379 379 $selector->appendChild($this->renderBatchEditor($query)); 380 380 381 - $form_id = celerity_generate_unique_node_id(); 382 - $selector = phabricator_form( 383 - $user, 384 - array( 385 - 'method' => 'POST', 386 - 'action' => '/maniphest/batch/', 387 - 'id' => $form_id, 388 - ), 389 - $selector->render()); 390 - 391 381 $list_container->appendChild($selector); 392 382 $list_container->appendChild($pager); 393 383 394 384 Javelin::initBehavior( 395 385 'maniphest-subpriority-editor', 396 386 array( 397 - 'root' => $form_id, 398 387 'uri' => '/maniphest/subpriority/', 399 388 )); 400 389 } ··· 644 633 } 645 634 646 635 private function renderBatchEditor(PhabricatorSearchQuery $search_query) { 636 + $user = $this->getRequest()->getUser(); 637 + 647 638 Javelin::initBehavior( 648 639 'maniphest-batch-selector', 649 640 array( ··· 651 642 'selectNone' => 'batch-select-none', 652 643 'submit' => 'batch-select-submit', 653 644 'status' => 'batch-select-status-cell', 645 + 'idContainer' => 'batch-select-id-container', 646 + 'formID' => 'batch-select-form', 654 647 )); 655 648 656 649 $select_all = javelin_tag( ··· 690 683 ), 691 684 pht('Export to Excel')); 692 685 693 - return hsprintf( 686 + $hidden = phutil_tag( 687 + 'div', 688 + array( 689 + 'id' => 'batch-select-id-container', 690 + ), 691 + ''); 692 + 693 + $editor = hsprintf( 694 694 '<div class="maniphest-batch-editor">'. 695 695 '<div class="batch-editor-header">%s</div>'. 696 696 '<table class="maniphest-batch-editor-layout">'. ··· 698 698 '<td>%s%s</td>'. 699 699 '<td>%s</td>'. 700 700 '<td id="batch-select-status-cell">%s</td>'. 701 - '<td class="batch-select-submit-cell">%s</td>'. 701 + '<td class="batch-select-submit-cell">%s%s</td>'. 702 702 '</tr>'. 703 703 '</table>'. 704 - '</table>', 704 + '</div>', 705 705 pht('Batch Task Editor'), 706 706 $select_all, 707 707 $select_none, 708 708 $export, 709 - pht('0 Selected'), 710 - $submit); 709 + '', 710 + $submit, 711 + $hidden); 712 + 713 + $editor = phabricator_form( 714 + $user, 715 + array( 716 + 'method' => 'POST', 717 + 'action' => '/maniphest/batch/', 718 + 'id' => 'batch-select-form', 719 + ), 720 + $editor); 721 + 722 + return $editor; 711 723 } 712 724 713 725 private function buildQueryFromRequest() {
+64 -9
src/applications/maniphest/view/ManiphestTaskListView.php
··· 33 33 } 34 34 35 35 public function render() { 36 + $handles = $this->handles; 36 37 37 - $views = array(); 38 + $list = new PhabricatorObjectItemListView(); 39 + $list->setCards(true); 40 + $list->setFlush(true); 41 + 42 + $status_map = ManiphestTaskStatus::getTaskStatusMap(); 43 + $color_map = array( 44 + ManiphestTaskPriority::PRIORITY_UNBREAK_NOW => 'magenta', 45 + ManiphestTaskPriority::PRIORITY_TRIAGE => 'violet', 46 + ManiphestTaskPriority::PRIORITY_HIGH => 'red', 47 + ManiphestTaskPriority::PRIORITY_NORMAL => 'orange', 48 + ManiphestTaskPriority::PRIORITY_LOW => 'yellow', 49 + ManiphestTaskPriority::PRIORITY_WISH => 'sky', 50 + ); 51 + 38 52 foreach ($this->tasks as $task) { 39 - $view = new ManiphestTaskSummaryView(); 40 - $view->setTask($task); 41 - $view->setShowBatchControls($this->showBatchControls); 42 - $view->setShowSubpriorityControls($this->showSubpriorityControls); 43 - $view->setUser($this->user); 44 - $view->setHandles($this->handles); 45 - $views[] = $view->render(); 53 + $item = new PhabricatorObjectItemView(); 54 + $item->setObjectName('T'.$task->getID()); 55 + $item->setHeader($task->getTitle()); 56 + $item->setHref('/T'.$task->getID()); 57 + 58 + if ($task->getOwnerPHID()) { 59 + $owner = $handles[$task->getOwnerPHID()]; 60 + $item->addHandleIcon( 61 + $owner, 62 + pht('Assigned: %s', $owner->getName())); 63 + } 64 + 65 + $status = $task->getStatus(); 66 + if ($status != ManiphestTaskStatus::STATUS_OPEN) { 67 + $item->addFootIcon( 68 + ($status == ManiphestTaskStatus::STATUS_CLOSED_RESOLVED) 69 + ? 'enable-white' 70 + : 'delete-white', 71 + idx($status_map, $status, 'Unknown')); 72 + } 73 + 74 + $item->setBarColor(idx($color_map, $task->getPriority(), 'grey')); 75 + 76 + $item->addIcon( 77 + 'none', 78 + phabricator_datetime($task->getDateModified(), $this->getUser())); 79 + 80 + if ($this->showSubpriorityControls) { 81 + $item->setGrippable(true); 82 + $item->addSigil('maniphest-task'); 83 + } 84 + 85 + if ($task->getProjectPHIDs()) { 86 + $projects_view = new ManiphestTaskProjectsView(); 87 + $projects_view->setHandles( 88 + array_select_keys( 89 + $handles, 90 + $task->getProjectPHIDs())); 91 + 92 + $item->addAttribute($projects_view); 93 + } 94 + 95 + $item->setMetadata( 96 + array( 97 + 'taskID' => $task->getID(), 98 + )); 99 + 100 + $list->addItem($item); 46 101 } 47 102 48 - return $views; 103 + return $list; 49 104 } 50 105 51 106 }
-196
src/applications/maniphest/view/ManiphestTaskSummaryView.php
··· 1 - <?php 2 - 3 - /** 4 - * @group maniphest 5 - */ 6 - final class ManiphestTaskSummaryView extends ManiphestView { 7 - 8 - private $task; 9 - private $handles; 10 - private $showBatchControls; 11 - private $showSubpriorityControls; 12 - 13 - public function setTask(ManiphestTask $task) { 14 - $this->task = $task; 15 - return $this; 16 - } 17 - 18 - public function setHandles(array $handles) { 19 - assert_instances_of($handles, 'PhabricatorObjectHandle'); 20 - $this->handles = $handles; 21 - return $this; 22 - } 23 - 24 - public function setShowBatchControls($show_batch_controls) { 25 - $this->showBatchControls = $show_batch_controls; 26 - return $this; 27 - } 28 - 29 - public function setShowSubpriorityControls($show_subpriority_controls) { 30 - $this->showSubpriorityControls = $show_subpriority_controls; 31 - return $this; 32 - } 33 - 34 - public static function getPriorityClass($priority) { 35 - $classes = array( 36 - ManiphestTaskPriority::PRIORITY_UNBREAK_NOW => 'pri-unbreak', 37 - ManiphestTaskPriority::PRIORITY_TRIAGE => 'pri-triage', 38 - ManiphestTaskPriority::PRIORITY_HIGH => 'pri-high', 39 - ManiphestTaskPriority::PRIORITY_NORMAL => 'pri-normal', 40 - ManiphestTaskPriority::PRIORITY_LOW => 'pri-low', 41 - ManiphestTaskPriority::PRIORITY_WISH => 'pri-wish', 42 - ); 43 - 44 - return idx($classes, $priority); 45 - } 46 - 47 - public function render() { 48 - 49 - if (!$this->user) { 50 - throw new Exception("Call setUser() before rendering!"); 51 - } 52 - 53 - $task = $this->task; 54 - $handles = $this->handles; 55 - 56 - require_celerity_resource('maniphest-task-summary-css'); 57 - 58 - $pri_class = self::getPriorityClass($task->getPriority()); 59 - $status_map = ManiphestTaskStatus::getTaskStatusMap(); 60 - 61 - $batch = null; 62 - if ($this->showBatchControls) { 63 - $batch = phutil_tag( 64 - 'td', 65 - array( 66 - 'rowspan' => 2, 67 - 'class' => 'maniphest-task-batch', 68 - ), 69 - javelin_tag( 70 - 'input', 71 - array( 72 - 'type' => 'checkbox', 73 - 'name' => 'batch[]', 74 - 'value' => $task->getID(), 75 - 'sigil' => 'maniphest-batch', 76 - ))); 77 - } 78 - 79 - $projects_view = new ManiphestTaskProjectsView(); 80 - $projects_view->setHandles( 81 - array_select_keys( 82 - $this->handles, 83 - $task->getProjectPHIDs())); 84 - 85 - $control_class = null; 86 - $control_sigil = null; 87 - if ($this->showSubpriorityControls) { 88 - $control_class = 'maniphest-active-handle'; 89 - $control_sigil = 'maniphest-task-handle'; 90 - } 91 - 92 - $handle = javelin_tag( 93 - 'td', 94 - array( 95 - 'rowspan' => 2, 96 - 'class' => 'maniphest-task-handle '.$pri_class.' '.$control_class, 97 - 'sigil' => $control_sigil, 98 - ), 99 - ''); 100 - 101 - $task_name = phutil_tag( 102 - 'span', 103 - array( 104 - 'class' => 'maniphest-task-name', 105 - ), 106 - phutil_tag( 107 - 'a', 108 - array( 109 - 'href' => '/T'.$task->getID(), 110 - ), 111 - $task->getTitle())); 112 - 113 - $task_updated = phutil_tag( 114 - 'span', 115 - array( 116 - 'class' => 'maniphest-task-updated', 117 - ), 118 - phabricator_date($task->getDateModified(), $this->user)); 119 - 120 - $task_info = phutil_tag( 121 - 'td', 122 - array( 123 - 'colspan' => 2, 124 - 'class' => 'maniphest-task-number', 125 - ), 126 - array( 127 - 'T'.$task->getID(), 128 - $task_name, 129 - $task_updated, 130 - )); 131 - 132 - $owner = ''; 133 - if ($task->getOwnerPHID()) { 134 - $owner = pht('Assigned to %s', 135 - $handles[$task->getOwnerPHID()]->renderLink()); 136 - } 137 - 138 - $task_owner = phutil_tag( 139 - 'span', 140 - array( 141 - 'class' => 'maniphest-task-owner', 142 - ), 143 - $task->getOwnerPHID() 144 - ? $owner 145 - : phutil_tag('em', array(), pht('None'))); 146 - 147 - $task_status = phutil_tag( 148 - 'td', 149 - array( 150 - 'class' => 'maniphest-task-status', 151 - ), 152 - array( 153 - idx($status_map, $task->getStatus(), pht('Unknown')), 154 - $task_owner, 155 - )); 156 - 157 - $task_projects = phutil_tag( 158 - 'td', 159 - array( 160 - 'class' => 'maniphest-task-projects', 161 - ), 162 - $projects_view->render()); 163 - 164 - $row1 = phutil_tag( 165 - 'tr', 166 - array(), 167 - array( 168 - $handle, 169 - $batch, 170 - $task_info, 171 - )); 172 - 173 - $row2 = phutil_tag( 174 - 'tr', 175 - array(), 176 - array( 177 - $task_status, 178 - $task_projects, 179 - )); 180 - 181 - return javelin_tag( 182 - 'table', 183 - array( 184 - 'class' => 'maniphest-task-summary', 185 - 'sigil' => 'maniphest-task', 186 - 'meta' => array( 187 - 'taskID' => $task->getID(), 188 - ), 189 - ), 190 - array( 191 - $row1, 192 - $row2, 193 - )); 194 - } 195 - 196 - }
+10 -15
src/applications/project/controller/PhabricatorProjectProfileController.php
··· 240 240 PhabricatorProject $project, 241 241 PhabricatorProjectProfile $profile) { 242 242 243 + $user = $this->getRequest()->getUser(); 244 + 243 245 $query = id(new ManiphestTaskQuery()) 244 246 ->withAnyProjects(array($project->getPHID())) 245 247 ->withStatus(ManiphestTaskQuery::STATUS_OPEN) ··· 250 252 $count = $query->getRowCount(); 251 253 252 254 $phids = mpull($tasks, 'getOwnerPHID'); 255 + $phids = array_merge( 256 + $phids, 257 + array_mergev(mpull($tasks, 'getProjectPHIDs'))); 253 258 $phids = array_filter($phids); 254 259 $handles = $this->loadViewerHandles($phids); 255 260 256 - $task_views = array(); 257 - foreach ($tasks as $task) { 258 - $view = id(new ManiphestTaskSummaryView()) 259 - ->setTask($task) 260 - ->setHandles($handles) 261 - ->setUser($this->getRequest()->getUser()); 262 - $task_views[] = $view->render(); 263 - } 264 - 265 - if (empty($tasks)) { 266 - $task_views = phutil_tag('em', array(), pht('No open tasks.')); 267 - } else { 268 - $task_views = phutil_implode_html('', $task_views); 269 - } 261 + $task_list = new ManiphestTaskListView(); 262 + $task_list->setUser($user); 263 + $task_list->setTasks($tasks); 264 + $task_list->setHandles($handles); 270 265 271 266 $open = number_format($count); 272 267 ··· 286 281 '</div> 287 282 </div>', 288 283 pht('Open Tasks (%s)', $open), 289 - $task_views, 284 + $task_list, 290 285 $more_link); 291 286 292 287 return $content;
+9
src/view/layout/PhabricatorObjectItemListView.php
··· 8 8 private $stackable; 9 9 private $cards; 10 10 private $noDataString; 11 + private $flush; 12 + 13 + public function setFlush($flush) { 14 + $this->flush = $flush; 15 + return $this; 16 + } 11 17 12 18 public function setHeader($header) { 13 19 $this->header = $header; ··· 73 79 } 74 80 if ($this->cards) { 75 81 $classes[] = 'phabricator-object-list-cards'; 82 + } 83 + if ($this->flush) { 84 + $classes[] = 'phabricator-object-list-flush'; 76 85 } 77 86 78 87 return phutil_tag(
+68 -41
src/view/layout/PhabricatorObjectItemView.php
··· 1 1 <?php 2 2 3 - final class PhabricatorObjectItemView extends AphrontView { 3 + final class PhabricatorObjectItemView extends AphrontTagView { 4 4 5 5 private $objectName; 6 6 private $header; ··· 105 105 return $this; 106 106 } 107 107 108 - public function render() { 108 + protected function getTagName() { 109 + return 'li'; 110 + } 111 + 112 + protected function getTagAttributes() { 113 + $item_classes = array(); 114 + $item_classes[] = 'phabricator-object-item'; 115 + 116 + if ($this->icons) { 117 + $item_classes[] = 'phabricator-object-item-with-icons'; 118 + } 119 + 120 + if ($this->attributes) { 121 + $item_classes[] = 'phabricator-object-item-with-attrs'; 122 + } 123 + 124 + if ($this->handleIcons) { 125 + $item_classes[] = 'phabricator-object-item-with-handle-icons'; 126 + } 127 + 128 + if ($this->barColor) { 129 + $item_classes[] = 'phabricator-object-item-bar-color-'.$this->barColor; 130 + } 131 + 132 + if ($this->footIcons) { 133 + $item_classes[] = 'phabricator-object-item-with-foot-icons'; 134 + } 135 + 136 + switch ($this->effect) { 137 + case 'highlighted': 138 + $item_classes[] = 'phabricator-object-item-highlighted'; 139 + break; 140 + case 'selected': 141 + $item_classes[] = 'phabricator-object-item-selected'; 142 + break; 143 + case null: 144 + break; 145 + default: 146 + throw new Exception(pht("Invalid effect!")); 147 + } 148 + 149 + if ($this->getGrippable()) { 150 + $item_classes[] = 'phabricator-object-item-grippable'; 151 + } 152 + 153 + return array( 154 + 'class' => $item_classes, 155 + ); 156 + } 157 + 158 + public function getTagContent() { 109 159 $content_classes = array(); 110 - $item_classes = array(); 111 160 $content_classes[] = 'phabricator-object-item-content'; 112 161 113 162 $header_name = null; ··· 131 180 ), 132 181 $this->header); 133 182 134 - $header = phutil_tag( 183 + $header = javelin_tag( 135 184 'div', 136 185 array( 137 186 'class' => 'phabricator-object-item-name', 187 + 'sigil' => 'slippery', 138 188 ), 139 189 array( 140 190 $header_name, ··· 162 212 ), 163 213 $spec['label']); 164 214 165 - 166 215 if ($spec['href']) { 167 216 $icon_href = phutil_tag( 168 217 'a', ··· 172 221 $icon_href = array($label, $icon); 173 222 } 174 223 224 + $classes = array(); 225 + $classes[] = 'phabricator-object-item-icon'; 226 + if ($spec['icon'] == 'none') { 227 + $classes[] = 'phabricator-object-item-icon-none'; 228 + } 229 + 175 230 $icon_list[] = phutil_tag( 176 231 'li', 177 232 array( 178 - 'class' => 'phabricator-object-item-icon', 233 + 'class' => implode(' ', $classes), 179 234 ), 180 235 $icon_href); 181 236 } ··· 186 241 'class' => 'phabricator-object-item-icons', 187 242 ), 188 243 $icon_list); 189 - $item_classes[] = 'phabricator-object-item-with-icons'; 190 244 } 191 245 192 246 if ($this->handleIcons) { ··· 200 254 'class' => 'phabricator-object-item-handle-icons', 201 255 ), 202 256 $handle_bar); 203 - $item_classes[] = 'phabricator-object-item-with-handle-icons'; 204 257 } 205 258 206 259 if ($icons) { ··· 240 293 'class' => 'phabricator-object-item-attributes', 241 294 ), 242 295 $attrs); 243 - $item_classes[] = 'phabricator-object-item-with-attrs'; 244 296 } 245 297 246 298 $foot = null; ··· 255 307 'class' => 'phabricator-object-item-foot-icons', 256 308 ), 257 309 $foot_bar); 258 - $item_classes[] = 'phabricator-object-item-with-foot-icons'; 259 - } 260 - 261 - $item_classes[] = 'phabricator-object-item'; 262 - if ($this->barColor) { 263 - $item_classes[] = 'phabricator-object-item-bar-color-'.$this->barColor; 264 - } 265 - 266 - switch ($this->effect) { 267 - case 'highlighted': 268 - $item_classes[] = 'phabricator-object-item-highlighted'; 269 - break; 270 - case 'selected': 271 - $item_classes[] = 'phabricator-object-item-selected'; 272 - break; 273 - case null: 274 - break; 275 - default: 276 - throw new Exception(pht("Invalid effect!")); 277 310 } 278 311 279 312 $grippable = null; 280 313 if ($this->getGrippable()) { 281 - $item_classes[] = 'phabricator-object-item-grippable'; 282 314 $grippable = phutil_tag( 283 315 'div', 284 316 array( ··· 300 332 )); 301 333 302 334 return phutil_tag( 303 - 'li', 335 + 'div', 304 336 array( 305 - 'class' => implode(' ', $item_classes), 337 + 'class' => 'phabricator-object-item-frame', 306 338 ), 307 - phutil_tag( 308 - 'div', 309 - array( 310 - 'class' => 'phabricator-object-item-frame', 311 - ), 312 - array( 313 - $grippable, 314 - $icons, 315 - $content, 316 - ))); 339 + array( 340 + $grippable, 341 + $icons, 342 + $content, 343 + )); 317 344 } 318 345 319 346 private function renderFootIcon($icon, $label) {
+3 -113
webroot/rsrc/css/application/maniphest/task-summary.css
··· 2 2 * @provides maniphest-task-summary-css 3 3 */ 4 4 5 - .maniphest-task-summary { 6 - width: 100%; 7 - margin: 0 0 -1px 0; 8 - border-collapse: separate; 9 - color: #333; 10 - border: 1px solid #c0c5d1; 11 - } 12 - 13 5 .maniphest-task-group { 14 6 padding-bottom: 30px; 15 7 } 16 8 17 - .maniphest-task-summary td { 18 - padding: 0 10px; 19 - background: #fff; 20 - } 21 - 22 - .maniphest-task-summary td em { 23 - color: #888888; 24 - } 25 - 26 9 .maniphest-batch-selected td { 27 10 background: #fff; 28 - } 29 - 30 - .maniphest-task-summary .maniphest-task-handle { 31 - padding: 0 4px 0 0; 32 - width: 5px; 33 - } 34 - 35 - .maniphest-task-summary td.maniphest-task-batch { 36 - padding: 15px 4px 0 10px; 37 - width: 8px; 38 - text-align: center; 39 - overflow: hidden; 40 11 } 41 12 42 13 .device-phone .maniphest-task-batch, ··· 44 15 display: none; 45 16 } 46 17 47 - .maniphest-task-summary td.maniphest-task-batch, 48 - .maniphest-task-summary td.maniphest-task-batch input { 49 - cursor: pointer; 50 - } 51 - 52 - .maniphest-task-summary td.maniphest-task-batch input { 53 - margin: 0; 54 - } 55 - 56 - .maniphest-task-summary td.maniphest-task-number { 57 - padding: 6px 0 2px 10px; 58 - font-weight: bold; 59 - color: #333; 60 - } 61 - 62 - .maniphest-task-summary td.maniphest-task-status { 63 - padding: 2px 10px 6px 10px; 64 - text-align: left; 65 - color: #777; 66 - font-size: 12px; 67 - } 68 - 69 - .maniphest-task-summary .maniphest-task-owner { 70 - padding-left: 20px; 71 - } 72 - 73 - .maniphest-task-summary .maniphest-task-name { 74 - font-weight: bold; 75 - overflow: hidden; 76 - margin-left: 5px; 77 - } 78 - 79 - .maniphest-task-summary td.maniphest-task-projects { 80 - text-align: right; 81 - padding: 0px 8px; 82 - } 83 - 84 - .maniphest-task-summary .maniphest-task-updated { 85 - float: right; 86 - padding: 0 8px; 87 - color: #777; 88 - font-size: 11px; 89 - font-weight: normal; 90 - } 91 - 92 - .maniphest-task-summary .pri-unbreak { 93 - border-color: #ff0000; 94 - background-color: #ff0000; 95 - } 96 - 97 - .maniphest-task-summary .pri-triage { 98 - border-color: #ee00ee; 99 - background-color: #ee00ee; 100 - } 101 - 102 - .maniphest-task-summary .pri-high { 103 - border-color: #ff6622; 104 - background-color: #ff6622; 105 - } 106 - 107 - .maniphest-task-summary .pri-normal { 108 - border-color: #ffaa66; 109 - background-color: #ffaa66; 110 - } 111 - 112 - .maniphest-task-summary .pri-low { 113 - border-color: #eecc66; 114 - background-color: #eecc66; 115 - } 116 - 117 - .maniphest-task-summary .pri-wish { 118 - border-color: #0099ff; 119 - background-color: #0099ff; 120 - } 121 - 122 18 .maniphest-task-group-header { 123 19 font-size: 16px; 124 20 font-weight: bold; ··· 179 75 width: 100%; 180 76 } 181 77 182 - td.maniphest-active-handle { 183 - cursor: move; 184 - background-image: url('/rsrc/image/grippy_texture.png'); 185 - background-position: 3px 0px; 186 - background-repeat: repeat-y; 187 - } 188 - 189 78 .maniphest-subpriority-target { 190 79 position: relative; 191 80 border: 1px dashed #aaaaaa; 192 81 background: #f9f9f9; 82 + margin: 4px; 193 83 } 194 84 195 85 .maniphest-task-loading { 196 - opacity: 0.5; 86 + opacity: 0.75; 197 87 } 198 88 199 89 .maniphest-task-dragging { 200 90 position: relative; 201 - opacity: 0.5; 91 + opacity: 0.90; 202 92 } 203 93 204 94 .maniphest-list-container {
+2 -2
webroot/rsrc/css/layout/phabricator-object-item-list-view.css
··· 38 38 } 39 39 40 40 .phabricator-object-item-name { 41 - display: block; 41 + display: inline-block; 42 42 font-weight: bold; 43 43 font-size: 14px; 44 44 padding: 0 10px; ··· 49 49 padding: 8px 0; 50 50 } 51 51 52 - 53 52 .phabricator-object-item-objname { 54 53 color: #222222; 54 + cursor: text; 55 55 } 56 56 57 57 .phabricator-object-item-with-attrs .phabricator-object-item-name {
+68 -64
webroot/rsrc/js/application/maniphest/behavior-batch-selector.js
··· 3 3 * @requires javelin-behavior 4 4 * javelin-dom 5 5 * javelin-stratcom 6 + * javelin-util 6 7 */ 7 8 8 9 JX.behavior('maniphest-batch-selector', function(config) { 9 10 10 - // When a task row's selection state is changed, this issues updates to other 11 - // parts of the application. 11 + var selected = {}; 12 12 13 - var onchange = function(task) { 14 - var input = JX.DOM.find(task, 'input', 'maniphest-batch'); 15 - var state = input.checked; 13 + // Test if a task node is selected. 16 14 17 - JX.DOM.alterClass(task, 'maniphest-batch-selected', state); 15 + var get_id = function(task) { 16 + return JX.Stratcom.getData(task).taskID; 17 + } 18 18 19 - JX.Stratcom.invoke( 20 - (state ? 'maniphest-batch-task-add' : 'maniphest-batch-task-rem'), 21 - null, 22 - {id: input.value}) 23 - }; 24 - 19 + var is_selected = function(task) { 20 + return (get_id(task) in selected); 21 + } 25 22 26 23 // Change the selected state of a task. 27 - // If 'to' is undefined, toggle. Otherwise, set to true or false. 28 24 29 25 var change = function(task, to) { 30 - 31 - var input = JX.DOM.find(task, 'input', 'maniphest-batch'); 32 - var state = input.checked; 33 26 if (to === undefined) { 34 - input.checked = !input.checked; 27 + to = !is_selected(task); 28 + } 29 + 30 + if (to) { 31 + selected[get_id(task)] = true; 35 32 } else { 36 - input.checked = to; 33 + delete selected[get_id(task)]; 37 34 } 38 - onchange(task); 35 + 36 + JX.DOM.alterClass( 37 + task, 38 + 'phabricator-object-item-selected', 39 + is_selected(task)); 40 + 41 + update(); 39 42 }; 40 43 41 44 ··· 43 46 // buttons). 44 47 45 48 var changeall = function(to) { 46 - var inputs = JX.DOM.scry(document.body, 'table', 'maniphest-task'); 49 + var inputs = JX.DOM.scry(document.body, 'li', 'maniphest-task'); 47 50 for (var ii = 0; ii < inputs.length; ii++) { 48 51 change(inputs[ii], to); 49 52 } 50 53 } 51 54 55 + // Clear any document text selection after toggling a task via shift click, 56 + // since errant clicks tend to start selecting various ranges otherwise. 57 + 58 + var clear_selection = function() { 59 + if (window.getSelection) { 60 + if (window.getSelection().empty) { 61 + window.getSelection().empty(); 62 + } else if (window.getSelection().removeAllRanges) { 63 + window.getSelection().removeAllRanges(); 64 + } 65 + } else if (document.selection) { 66 + document.selection.empty(); 67 + } 68 + } 52 69 53 70 // Update the status text showing how many tasks are selected, and the button 54 71 // state. 55 72 56 - var selected = {}; 57 - var selected_count = 0; 58 - 59 73 var update = function() { 60 - var status = (selected_count == 1) 61 - ? '1 Selected Task' 62 - : selected_count + ' Selected Tasks'; 74 + var count = JX.keys(selected).length; 75 + var status; 76 + if (count == 0) { 77 + status = 'Shift-Click to Select Tasks'; 78 + } else if (status == 1) { 79 + status = '1 Selected Task'; 80 + } else { 81 + status = count + ' Selected Tasks'; 82 + } 63 83 JX.DOM.setContent(JX.$(config.status), status); 64 84 65 85 var submit = JX.$(config.submit); 66 - var disable = (selected_count == 0); 86 + var disable = (count == 0); 67 87 submit.disabled = disable; 68 88 JX.DOM.alterClass(submit, 'disabled', disable); 69 89 }; 70 90 71 - 72 - // When the user clicks the entire <td /> surrounding the checkbox, count it 73 - // as a checkbox click. 91 + // When he user shift-clicks the task, update the rest of the application 92 + // state. 74 93 75 94 JX.Stratcom.listen( 76 95 'click', 77 96 'maniphest-task', 78 97 function(e) { 79 - if (!JX.DOM.isNode(e.getTarget(), 'td')) { 80 - // Only count clicks in the <td />, not (e.g.) the table border. 98 + var raw = e.getRawEvent(); 99 + if (!raw.shiftKey) { 100 + return; 101 + } 102 + 103 + if (raw.ctrlKey || raw.altKey || raw.metaKey || e.isRightButton()) { 81 104 return; 82 105 } 83 106 84 - // Check if the clicked <td /> contains a checkbox. 85 - var inputs = JX.DOM.scry(e.getTarget(), 'input', 'maniphest-batch'); 86 - if (!inputs.length) { 107 + if (JX.Stratcom.pass(e)) { 87 108 return; 88 109 } 89 110 111 + e.kill(); 90 112 change(e.getNode('maniphest-task')); 91 - }); 92 113 93 - 94 - // When he user clicks the <input />, update the rest of the application 95 - // state. 96 - 97 - JX.Stratcom.listen( 98 - ['click', 'onchange'], 99 - 'maniphest-batch', 100 - function(e) { 101 - onchange(e.getNode('maniphest-task')); 114 + clear_selection(); 102 115 }); 103 116 104 117 ··· 125 138 e.kill(); 126 139 }); 127 140 141 + // When the user submits the form, dump selected state into it. 128 142 129 - JX.Stratcom.listen( 130 - 'maniphest-batch-task-add', 143 + JX.DOM.listen( 144 + JX.$(config.formID), 145 + 'submit', 131 146 null, 132 147 function(e) { 133 - var id = e.getData().id; 134 - if (!(id in selected)) { 135 - selected[id] = true; 136 - selected_count++; 137 - update(); 148 + var inputs = []; 149 + for (var k in selected) { 150 + inputs.push( 151 + JX.$N('input', {type: 'hidden', name: 'batch[]', value: k})); 138 152 } 153 + JX.DOM.setContent(JX.$(config.idContainer), inputs); 139 154 }); 140 155 141 - 142 - JX.Stratcom.listen( 143 - 'maniphest-batch-task-rem', 144 - null, 145 - function(e) { 146 - var id = e.getData().id; 147 - if (id in selected) { 148 - delete selected[id]; 149 - selected_count--; 150 - update(); 151 - } 152 - }); 156 + update(); 153 157 154 158 });
+31 -12
webroot/rsrc/js/application/maniphest/behavior-subpriorityeditor.js
··· 15 15 var origin = null; 16 16 var targets = null; 17 17 var target = null; 18 - var droptarget = JX.$N('div', {className: 'maniphest-subpriority-target'}); 18 + var droptarget = JX.$N('li', {className: 'maniphest-subpriority-target'}); 19 19 20 20 var ondrag = function(e) { 21 21 if (dragging || sending) { 22 22 return; 23 23 } 24 24 25 + if (!e.isNormalMouseEvent()) { 26 + return; 27 + } 28 + 29 + // Can't grab onto slippery nodes. 30 + if (e.getNode('slippery')) { 31 + return; 32 + } 33 + 25 34 dragging = e.getNode('maniphest-task'); 26 35 origin = JX.$V(e); 27 36 28 - var tasks = JX.DOM.scry(JX.$(config.root), 'table', 'maniphest-task'); 29 - var heads = JX.DOM.scry(JX.$(config.root), 'h1', 'task-group'); 37 + var tasks = JX.DOM.scry(document.body, 'li', 'maniphest-task'); 38 + var heads = JX.DOM.scry(document.body, 'h1', 'task-group'); 30 39 31 40 var nodes = tasks.concat(heads); 32 41 ··· 107 116 108 117 if (cur_target) { 109 118 if (cur_target.nextSibling) { 110 - cur_target.parentNode.insertBefore( 111 - droptarget, 112 - cur_target.nextSibling); 119 + if (JX.DOM.isType(cur_target, 'h1')) { 120 + // Dropping at the beginning of a priority list. 121 + cur_target.nextSibling.insertBefore( 122 + droptarget, 123 + cur_target.nextSibling.firstChild); 124 + } else { 125 + // Dropping in the middle of a priority list. 126 + cur_target.parentNode.insertBefore( 127 + droptarget, 128 + cur_target.nextSibling); 129 + } 113 130 } else { 131 + // Dropping at the end of a priority list. 114 132 cur_target.parentNode.appendChild(droptarget); 115 133 } 116 134 } ··· 180 198 JX.DOM.alterClass(sending, 'maniphest-task-loading', true); 181 199 182 200 var onresponse = function(r) { 183 - JX.DOM.alterClass(sending, 'maniphest-task-loading', false); 184 - var handle = JX.DOM.find(sending, 'td', 'maniphest-task-handle'); 185 - handle.className = r.className; 201 + var nodes = JX.$H(r.tasks).getFragment().firstChild; 202 + var task = JX.DOM.find(nodes, 'li', 'maniphest-task'); 203 + JX.DOM.replace(sending, task); 204 + 186 205 sending = null; 187 206 }; 188 207 ··· 196 215 // NOTE: Javelin does not dispatch mousemove by default. 197 216 JX.enableDispatch(document.body, 'mousemove'); 198 217 199 - JX.Stratcom.listen('mousedown', 'maniphest-task-handle', ondrag); 200 - JX.Stratcom.listen('mousemove', null, onmove); 201 - JX.Stratcom.listen('mouseup', null, ondrop); 218 + JX.Stratcom.listen('mousedown', 'maniphest-task', ondrag); 219 + JX.Stratcom.listen('mousemove', null, onmove); 220 + JX.Stratcom.listen('mouseup', null, ondrop); 202 221 203 222 });