From b287cdd9ba59afbe7fa0989acb2a4ac2a2ad1376 Mon Sep 17 00:00:00 2001 From: epriestley Date: Sat, 6 Feb 2016 11:40:30 -0800 Subject: [PATCH 01/65] Move drag ghosts above dialogs Summary: These currently have a z-index which puts them beneath dialogs, which doesn't work well for dialogs like "Reorder Columns" on workboards. Test Plan: - Dragged columns on a workboard in the "Reorder Columns" dialog. - Dragged normal stuff, too. Reviewers: chad Reviewed By: chad Differential Revision: https://secure.phabricator.com/D15197 --- resources/celerity/map.php | 6 +++--- webroot/rsrc/css/core/z-index.css | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 9003476d2d..5f5a9ee47f 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -7,7 +7,7 @@ */ return array( 'names' => array( - 'core.pkg.css' => 'e33b14a4', + 'core.pkg.css' => 'c477bd31', 'core.pkg.js' => 'ef5e33db', 'darkconsole.pkg.js' => 'e7393ebb', 'differential.pkg.css' => '2de124c9', @@ -106,7 +106,7 @@ return array( 'rsrc/css/core/core.css' => '5b3563c8', 'rsrc/css/core/remarkup.css' => 'e1c8b32f', 'rsrc/css/core/syntax.css' => '9fd11da8', - 'rsrc/css/core/z-index.css' => '5c7025bf', + 'rsrc/css/core/z-index.css' => '5b6fcf3f', 'rsrc/css/diviner/diviner-shared.css' => 'aa3656aa', 'rsrc/css/font/font-aleo.css' => '8bdb2835', 'rsrc/css/font/font-awesome.css' => 'c43323c5', @@ -779,7 +779,7 @@ return array( 'phabricator-uiexample-reactor-select' => 'a155550f', 'phabricator-uiexample-reactor-sendclass' => '1def2711', 'phabricator-uiexample-reactor-sendproperties' => 'b1f0ccee', - 'phabricator-zindex-css' => '5c7025bf', + 'phabricator-zindex-css' => '5b6fcf3f', 'phame-css' => '1dbbacf9', 'pholio-css' => '95174bdd', 'pholio-edit-css' => '3ad9d1ee', diff --git a/webroot/rsrc/css/core/z-index.css b/webroot/rsrc/css/core/z-index.css index 2d4c050d0c..23c6c59bf7 100644 --- a/webroot/rsrc/css/core/z-index.css +++ b/webroot/rsrc/css/core/z-index.css @@ -110,10 +110,6 @@ div.phui-calendar-day-event { z-index: 9; } -.drag-frame { - z-index: 10; -} - .jx-mask { z-index: 10; } @@ -142,6 +138,10 @@ div.jx-typeahead-results { z-index: 15; } +.drag-frame { + z-index: 16; +} + .jx-hovercard-container { z-index: 17; } From 5523217fbb2d55f1c3d6ae4cf355178f84a14d5c Mon Sep 17 00:00:00 2001 From: Chad Little Date: Sat, 6 Feb 2016 11:58:26 -0800 Subject: [PATCH 02/65] Add a coverImage field to PHUIObjectItemView Summary: Allows setting of an image to a Workboard card. (Hides from regular view, just in case). Test Plan: Fake setting a Pholio Mock image. Reviewers: epriestley Reviewed By: epriestley Subscribers: Korvin Differential Revision: https://secure.phabricator.com/D15198 --- resources/celerity/map.php | 10 ++--- src/view/phui/PHUIObjectItemView.php | 39 ++++++++++++++++++- .../css/phui/phui-object-item-list-view.css | 4 ++ .../css/phui/workboards/phui-workcard.css | 6 +++ 4 files changed, 52 insertions(+), 7 deletions(-) diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 5f5a9ee47f..077303a539 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -7,7 +7,7 @@ */ return array( 'names' => array( - 'core.pkg.css' => 'c477bd31', + 'core.pkg.css' => 'a7d4cf8f', 'core.pkg.js' => 'ef5e33db', 'darkconsole.pkg.js' => 'e7393ebb', 'differential.pkg.css' => '2de124c9', @@ -143,7 +143,7 @@ return array( 'rsrc/css/phui/phui-info-view.css' => '6d7c3509', 'rsrc/css/phui/phui-list.css' => '9da2aa00', 'rsrc/css/phui/phui-object-box.css' => '407eaf5a', - 'rsrc/css/phui/phui-object-item-list-view.css' => 'fe594a65', + 'rsrc/css/phui/phui-object-item-list-view.css' => '8f443e8b', 'rsrc/css/phui/phui-pager.css' => 'bea33d23', 'rsrc/css/phui/phui-pinboard-view.css' => '2495140e', 'rsrc/css/phui/phui-profile-menu.css' => 'ab4fcf5f', @@ -155,7 +155,7 @@ return array( 'rsrc/css/phui/phui-timeline-view.css' => '2efceff8', 'rsrc/css/phui/phui-two-column-view.css' => 'c75bfc5b', 'rsrc/css/phui/workboards/phui-workboard.css' => 'b07a5524', - 'rsrc/css/phui/workboards/phui-workcard.css' => 'b4322ca7', + 'rsrc/css/phui/workboards/phui-workcard.css' => 'adf34f58', 'rsrc/css/phui/workboards/phui-workpanel.css' => 'e1bd8d04', 'rsrc/css/sprite-login.css' => '60e8560e', 'rsrc/css/sprite-menu.css' => '9dd65b92', @@ -819,7 +819,7 @@ return array( 'phui-inline-comment-view-css' => '0fdb3667', 'phui-list-view-css' => '9da2aa00', 'phui-object-box-css' => '407eaf5a', - 'phui-object-item-list-view-css' => 'fe594a65', + 'phui-object-item-list-view-css' => '8f443e8b', 'phui-pager-css' => 'bea33d23', 'phui-pinboard-view-css' => '2495140e', 'phui-profile-menu-css' => 'ab4fcf5f', @@ -832,7 +832,7 @@ return array( 'phui-timeline-view-css' => '2efceff8', 'phui-two-column-view-css' => 'c75bfc5b', 'phui-workboard-view-css' => 'b07a5524', - 'phui-workcard-view-css' => 'b4322ca7', + 'phui-workcard-view-css' => 'adf34f58', 'phui-workpanel-view-css' => 'e1bd8d04', 'phuix-action-list-view' => 'b5c256b8', 'phuix-action-view' => '8cf6d262', diff --git a/src/view/phui/PHUIObjectItemView.php b/src/view/phui/PHUIObjectItemView.php index c7a3d074b5..4b56d746a7 100644 --- a/src/view/phui/PHUIObjectItemView.php +++ b/src/view/phui/PHUIObjectItemView.php @@ -27,6 +27,7 @@ final class PHUIObjectItemView extends AphrontTagView { private $countdownNum; private $countdownNoun; private $launchButton; + private $coverImage; const AGE_FRESH = 'fresh'; const AGE_STALE = 'stale'; @@ -150,6 +151,11 @@ final class PHUIObjectItemView extends AphrontTagView { return $this->imageIcon; } + public function setCoverImage($image) { + $this->coverImage = $image; + return $this; + } + public function setState($state) { $this->state = $state; switch ($state) { @@ -720,16 +726,45 @@ final class PHUIObjectItemView extends AphrontTagView { $actions); } - return phutil_tag( + $frame_content = phutil_tag( 'div', array( - 'class' => 'phui-object-item-frame', + 'class' => 'phui-object-item-frame-content', ), array( $actions, $image, $box, )); + + $frame_cover = null; + if ($this->coverImage) { + $cover_image = phutil_tag( + 'img', + array( + 'src' => $this->coverImage, + 'class' => 'phui-object-item-cover-image', + )); + + $frame_cover = phutil_tag( + 'div', + array( + 'class' => 'phui-object-item-frame-cover', + ), + $cover_image); + } + + $frame = phutil_tag( + 'div', + array( + 'class' => 'phui-object-item-frame', + ), + array( + $frame_cover, + $frame_content, + )); + + return $frame; } private function renderStatusIcon($icon, $label) { diff --git a/webroot/rsrc/css/phui/phui-object-item-list-view.css b/webroot/rsrc/css/phui/phui-object-item-list-view.css index 551144673b..81509008c9 100644 --- a/webroot/rsrc/css/phui/phui-object-item-list-view.css +++ b/webroot/rsrc/css/phui/phui-object-item-list-view.css @@ -56,6 +56,10 @@ ul.phui-object-item-list-view { overflow: hidden; } +.phui-object-item-cover-image { + display: none; +} + .phui-object-item-no-bar .phui-object-item-frame { border-width: 1px; } diff --git a/webroot/rsrc/css/phui/workboards/phui-workcard.css b/webroot/rsrc/css/phui/workboards/phui-workcard.css index d0c427da57..dbdee0103a 100644 --- a/webroot/rsrc/css/phui/workboards/phui-workcard.css +++ b/webroot/rsrc/css/phui/workboards/phui-workcard.css @@ -100,6 +100,12 @@ margin-bottom: 8px; } +.phui-workcard .phui-object-item-cover-image { + display: block; + padding: 8px 8px 0 8px; + width: 263px; +} + /* - Draggable Colors --------------------------------------------------------*/ From 7550557e44282afa77357807984680461e715394 Mon Sep 17 00:00:00 2001 From: epriestley Date: Sat, 6 Feb 2016 12:40:31 -0800 Subject: [PATCH 03/65] Don't show archived projects by default in policy control Summary: When filling in filler projects, only select active ones. Also use a slightly more modern method signature. Test Plan: Disabled a project, saw it vanish from the control. Reviewers: chad Reviewed By: chad Differential Revision: https://secure.phabricator.com/D15199 --- .../PhabricatorPeopleProfileViewController.php | 5 ++++- .../policy/query/PhabricatorPolicyQuery.php | 4 ++++ .../project/query/PhabricatorProjectQuery.php | 13 +++++++++++++ 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/src/applications/people/controller/PhabricatorPeopleProfileViewController.php b/src/applications/people/controller/PhabricatorPeopleProfileViewController.php index 45125c28b0..ed2a2189a6 100644 --- a/src/applications/people/controller/PhabricatorPeopleProfileViewController.php +++ b/src/applications/people/controller/PhabricatorPeopleProfileViewController.php @@ -130,7 +130,10 @@ final class PhabricatorPeopleProfileViewController ->setViewer($viewer) ->withMemberPHIDs(array($user->getPHID())) ->needImages(true) - ->withStatus(PhabricatorProjectQuery::STATUS_OPEN) + ->withStatuses( + array( + PhabricatorProjectStatus::STATUS_ACTIVE, + )) ->execute(); $header = id(new PHUIHeaderView()) diff --git a/src/applications/policy/query/PhabricatorPolicyQuery.php b/src/applications/policy/query/PhabricatorPolicyQuery.php index df1d6fb0b8..e6e7008827 100644 --- a/src/applications/policy/query/PhabricatorPolicyQuery.php +++ b/src/applications/policy/query/PhabricatorPolicyQuery.php @@ -230,6 +230,10 @@ final class PhabricatorPolicyQuery ->setViewer($viewer) ->withMemberPHIDs(array($viewer->getPHID())) ->withIsMilestone(false) + ->withStatuses( + array( + PhabricatorProjectStatus::STATUS_ACTIVE, + )) ->setLimit($default_limit) ->execute(); $default_projects = mpull($default_projects, null, 'getPHID'); diff --git a/src/applications/project/query/PhabricatorProjectQuery.php b/src/applications/project/query/PhabricatorProjectQuery.php index 304dd956e1..f4711bb4f7 100644 --- a/src/applications/project/query/PhabricatorProjectQuery.php +++ b/src/applications/project/query/PhabricatorProjectQuery.php @@ -27,6 +27,7 @@ final class PhabricatorProjectQuery const STATUS_CLOSED = 'status-closed'; const STATUS_ACTIVE = 'status-active'; const STATUS_ARCHIVED = 'status-archived'; + private $statuses; private $needSlugs; private $needMembers; @@ -49,6 +50,11 @@ final class PhabricatorProjectQuery return $this; } + public function withStatuses(array $statuses) { + $this->statuses = $statuses; + return $this; + } + public function withMemberPHIDs(array $member_phids) { $this->memberPHIDs = $member_phids; return $this; @@ -387,6 +393,13 @@ final class PhabricatorProjectQuery $filter); } + if ($this->statuses !== null) { + $where[] = qsprintf( + $conn, + 'status IN (%Ls)', + $this->statuses); + } + if ($this->ids !== null) { $where[] = qsprintf( $conn, From f097c9c5951a9238d4de10b08df43fa0a27aea7f Mon Sep 17 00:00:00 2001 From: epriestley Date: Sat, 6 Feb 2016 12:48:01 -0800 Subject: [PATCH 04/65] Disable "Subprojects" menu item for milestone projects Summary: Ref T10010. Milestones can't have subprojects, so this item isn't very useful. I think there is also an argument for disabling "Members", but that panel is a little less useless and explains the membership rule, so I'm less certain about removing it. I do generally lean toward removing it at some point, though. Test Plan: - Viewed a milestone, no "Subprojects" menu item. - Viewed a normal project, saw item. - Edited both menus, saw consistent UI. Reviewers: chad Reviewed By: chad Maniphest Tasks: T10010 Differential Revision: https://secure.phabricator.com/D15200 --- .../PhabricatorProjectSubprojectsProfilePanel.php | 8 ++++++++ .../search/engine/PhabricatorProfilePanelEngine.php | 8 ++++++++ .../search/profilepanel/PhabricatorProfilePanel.php | 4 ++++ .../storage/PhabricatorProfilePanelConfiguration.php | 4 ++++ 4 files changed, 24 insertions(+) diff --git a/src/applications/project/profilepanel/PhabricatorProjectSubprojectsProfilePanel.php b/src/applications/project/profilepanel/PhabricatorProjectSubprojectsProfilePanel.php index fad6378d69..342c8025aa 100644 --- a/src/applications/project/profilepanel/PhabricatorProjectSubprojectsProfilePanel.php +++ b/src/applications/project/profilepanel/PhabricatorProjectSubprojectsProfilePanel.php @@ -13,6 +13,14 @@ final class PhabricatorProjectSubprojectsProfilePanel return pht('Subprojects'); } + public function shouldEnableForObject($object) { + if ($object->isMilestone()) { + return false; + } + + return true; + } + public function getDisplayName( PhabricatorProfilePanelConfiguration $config) { $name = $config->getPanelProperty('name'); diff --git a/src/applications/search/engine/PhabricatorProfilePanelEngine.php b/src/applications/search/engine/PhabricatorProfilePanelEngine.php index a3907feac8..e9cea05db7 100644 --- a/src/applications/search/engine/PhabricatorProfilePanelEngine.php +++ b/src/applications/search/engine/PhabricatorProfilePanelEngine.php @@ -239,6 +239,10 @@ abstract class PhabricatorProfilePanelEngine extends Phobject { // Merge the stored panels into the builtin panels. If a builtin panel has // a stored version, replace the defaults with the stored changes. foreach ($stored_panels as $stored_panel) { + if (!$stored_panel->shouldEnableForObject($object)) { + continue; + } + $builtin_key = $stored_panel->getBuiltinKey(); if ($builtin_key !== null) { // If this builtin actually exists, replace the builtin with the @@ -341,6 +345,10 @@ abstract class PhabricatorProfilePanelEngine extends Phobject { ->attachProfileObject($object) ->setPanelOrder($order); + if (!$builtin->shouldEnableForObject($object)) { + continue; + } + $map[$builtin_key] = $builtin; $order++; diff --git a/src/applications/search/profilepanel/PhabricatorProfilePanel.php b/src/applications/search/profilepanel/PhabricatorProfilePanel.php index 8316d13467..8fcbd3f96a 100644 --- a/src/applications/search/profilepanel/PhabricatorProfilePanel.php +++ b/src/applications/search/profilepanel/PhabricatorProfilePanel.php @@ -30,6 +30,10 @@ abstract class PhabricatorProfilePanel extends Phobject { return false; } + public function shouldEnableForObject($object) { + return true; + } + public function canHidePanel( PhabricatorProfilePanelConfiguration $config) { return true; diff --git a/src/applications/search/storage/PhabricatorProfilePanelConfiguration.php b/src/applications/search/storage/PhabricatorProfilePanelConfiguration.php index faeaeb5207..d6f70e802e 100644 --- a/src/applications/search/storage/PhabricatorProfilePanelConfiguration.php +++ b/src/applications/search/storage/PhabricatorProfilePanelConfiguration.php @@ -109,6 +109,10 @@ final class PhabricatorProfilePanelConfiguration return $this->getPanel()->canHidePanel($this); } + public function shouldEnableForObject($object) { + return $this->getPanel()->shouldEnableForObject($object); + } + public function getSortKey() { $order = $this->getPanelOrder(); if ($order === null) { From b6a38b403cbf86c9e8fff1ba6d4d0e1641b378e7 Mon Sep 17 00:00:00 2001 From: epriestley Date: Sat, 6 Feb 2016 13:02:13 -0800 Subject: [PATCH 05/65] Add storage and read logic for workboard card cover photos Summary: No way to set photos yet, but if you magic them in they work. Primarily, this consolidates rendering logic so the move + edit + view controllers all run the same code to do tags / cover photos. Test Plan: {F1095870} Reviewers: chad Reviewed By: chad Differential Revision: https://secure.phabricator.com/D15201 --- .../sql/autopatches/20160206.cover.1.sql | 5 + src/__phutil_library_map__.php | 2 + .../PhabricatorFileThumbnailTransform.php | 5 + .../maniphest/editor/ManiphestEditEngine.php | 40 ++--- .../maniphest/storage/ManiphestTask.php | 15 ++ .../PhabricatorProjectBoardViewController.php | 57 +++---- .../PhabricatorProjectMoveController.php | 41 ++--- .../PhabricatorBoardRenderingEngine.php | 144 ++++++++++++++++++ .../project/view/ProjectBoardTaskCard.php | 15 ++ 9 files changed, 237 insertions(+), 87 deletions(-) create mode 100644 resources/sql/autopatches/20160206.cover.1.sql create mode 100644 src/applications/project/engine/PhabricatorBoardRenderingEngine.php diff --git a/resources/sql/autopatches/20160206.cover.1.sql b/resources/sql/autopatches/20160206.cover.1.sql new file mode 100644 index 0000000000..1e8b473a5e --- /dev/null +++ b/resources/sql/autopatches/20160206.cover.1.sql @@ -0,0 +1,5 @@ +ALTER TABLE {$NAMESPACE}_maniphest.maniphest_task + ADD properties LONGTEXT NOT NULL COLLATE {$COLLATE_TEXT}; + +UPDATE {$NAMESPACE}_maniphest.maniphest_task + SET properties = '{}' WHERE properties = ''; diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 763b41e99b..9e65dda01a 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -1815,6 +1815,7 @@ phutil_register_library_map(array( 'PhabricatorBinariesSetupCheck' => 'applications/config/check/PhabricatorBinariesSetupCheck.php', 'PhabricatorBitbucketAuthProvider' => 'applications/auth/provider/PhabricatorBitbucketAuthProvider.php', 'PhabricatorBoardLayoutEngine' => 'applications/project/engine/PhabricatorBoardLayoutEngine.php', + 'PhabricatorBoardRenderingEngine' => 'applications/project/engine/PhabricatorBoardRenderingEngine.php', 'PhabricatorBot' => 'infrastructure/daemon/bot/PhabricatorBot.php', 'PhabricatorBotChannel' => 'infrastructure/daemon/bot/target/PhabricatorBotChannel.php', 'PhabricatorBotDebugLogHandler' => 'infrastructure/daemon/bot/handler/PhabricatorBotDebugLogHandler.php', @@ -6043,6 +6044,7 @@ phutil_register_library_map(array( 'PhabricatorBinariesSetupCheck' => 'PhabricatorSetupCheck', 'PhabricatorBitbucketAuthProvider' => 'PhabricatorOAuth1AuthProvider', 'PhabricatorBoardLayoutEngine' => 'Phobject', + 'PhabricatorBoardRenderingEngine' => 'Phobject', 'PhabricatorBot' => 'PhabricatorDaemon', 'PhabricatorBotChannel' => 'PhabricatorBotTarget', 'PhabricatorBotDebugLogHandler' => 'PhabricatorBotHandler', diff --git a/src/applications/files/transform/PhabricatorFileThumbnailTransform.php b/src/applications/files/transform/PhabricatorFileThumbnailTransform.php index 8d55109eda..2c1f1149f3 100644 --- a/src/applications/files/transform/PhabricatorFileThumbnailTransform.php +++ b/src/applications/files/transform/PhabricatorFileThumbnailTransform.php @@ -7,6 +7,7 @@ final class PhabricatorFileThumbnailTransform const TRANSFORM_PINBOARD = 'pinboard'; const TRANSFORM_THUMBGRID = 'thumbgrid'; const TRANSFORM_PREVIEW = 'preview'; + const TRANSFORM_WORKCARD = 'workcard'; private $name; private $key; @@ -73,6 +74,10 @@ final class PhabricatorFileThumbnailTransform ->setName(pht('Preview (220px)')) ->setKey(self::TRANSFORM_PREVIEW) ->setDimensions(220, null), + id(new self()) + ->setName(pht('Workcard (526px)')) + ->setKey(self::TRANSFORM_WORKCARD) + ->setDimensions(526, null), ); } diff --git a/src/applications/maniphest/editor/ManiphestEditEngine.php b/src/applications/maniphest/editor/ManiphestEditEngine.php index 3f88a9abe3..efc337d729 100644 --- a/src/applications/maniphest/editor/ManiphestEditEngine.php +++ b/src/applications/maniphest/editor/ManiphestEditEngine.php @@ -334,39 +334,27 @@ final class ManiphestEditEngine 'sortMap' => $sort_map, ); - // TODO: This should just use HandlePool once we get through the EditEngine - // transition. - $owner = null; - if ($task->getOwnerPHID()) { - $owner = id(new PhabricatorHandleQuery()) - ->setViewer($viewer) - ->withPHIDs(array($task->getOwnerPHID())) - ->executeOne(); - } - - $handle_phids = $task->getProjectPHIDs(); - $handle_phids = array_fuse($handle_phids); - $handle_phids = array_diff_key($handle_phids, $board_phids); - - $project_handles = $viewer->loadHandles($handle_phids); - $project_handles = iterator_to_array($project_handles); - - $tasks = id(new ProjectBoardTaskCard()) + $rendering_engine = id(new PhabricatorBoardRenderingEngine()) ->setViewer($viewer) - ->setTask($task) - ->setOwner($owner) - ->setProjectHandles($project_handles) - ->setCanEdit(true) - ->getItem(); + ->setObjects(array($task)) + ->setExcludedProjectPHIDs($board_phids); - $tasks->addClass('phui-workcard'); + $card = $rendering_engine->renderCard($task->getPHID()); + + $item = $card->getItem(); + $item->addClass('phui-workcard'); $payload = array( - 'tasks' => $tasks, + 'tasks' => $item, 'data' => $data, ); - return id(new AphrontAjaxResponse())->setContent($payload); + return id(new AphrontAjaxResponse()) + ->setContent( + array( + 'tasks' => $item, + 'data' => $data, + )); } diff --git a/src/applications/maniphest/storage/ManiphestTask.php b/src/applications/maniphest/storage/ManiphestTask.php index cc9a5bd80a..5b6c4129dd 100644 --- a/src/applications/maniphest/storage/ManiphestTask.php +++ b/src/applications/maniphest/storage/ManiphestTask.php @@ -38,6 +38,7 @@ final class ManiphestTask extends ManiphestDAO protected $ownerOrdering; protected $spacePHID; + protected $properties = array(); private $subscriberPHIDs = self::ATTACHABLE; private $groupByProjectPHID = self::ATTACHABLE; @@ -74,6 +75,7 @@ final class ManiphestTask extends ManiphestDAO 'ccPHIDs' => self::SERIALIZATION_JSON, 'attached' => self::SERIALIZATION_JSON, 'projectPHIDs' => self::SERIALIZATION_JSON, + 'properties' => self::SERIALIZATION_JSON, ), self::CONFIG_COLUMN_SCHEMA => array( 'ownerPHID' => 'phid?', @@ -215,6 +217,19 @@ final class ManiphestTask extends ManiphestDAO ); } + public function setProperty($key, $value) { + $this->properties[$key] = $value; + return $this; + } + + public function getProperty($key, $default = null) { + return idx($this->properties, $key, $default); + } + + public function getCoverImageThumbnailPHID() { + return idx($this->properties, 'cover.thumbnailPHID'); + } + /* -( PhabricatorSubscribableInterface )----------------------------------- */ diff --git a/src/applications/project/controller/PhabricatorProjectBoardViewController.php b/src/applications/project/controller/PhabricatorProjectBoardViewController.php index d0580a95ad..1f00a8a7a6 100644 --- a/src/applications/project/controller/PhabricatorProjectBoardViewController.php +++ b/src/applications/project/controller/PhabricatorProjectBoardViewController.php @@ -7,7 +7,6 @@ final class PhabricatorProjectBoardViewController private $id; private $slug; - private $handles; private $queryKey; private $filter; private $sortKey; @@ -226,22 +225,9 @@ final class PhabricatorProjectBoardViewController 'project-boards', $behavior_config); - $this->handles = ManiphestTaskListView::loadTaskHandles($viewer, $tasks); - - $all_project_phids = array(); - foreach ($tasks as $task) { - foreach ($task->getProjectPHIDs() as $project_phid) { - $all_project_phids[$project_phid] = $project_phid; - } - } - - foreach ($select_phids as $phid) { - unset($all_project_phids[$phid]); - } - - $all_handles = $viewer->loadHandles($all_project_phids); - $all_handles = iterator_to_array($all_handles); - + $visible_columns = array(); + $column_phids = array(); + $visible_phids = array(); foreach ($columns as $column) { if (!$this->showHidden) { if ($column->isHidden()) { @@ -268,6 +254,25 @@ final class PhabricatorProjectBoardViewController $column_tasks = array_select_keys($column_tasks, array_keys($tasks)); } + $column_phid = $column->getPHID(); + + $visible_columns[$column_phid] = $column; + $column_phids[$column_phid] = $column_tasks; + + foreach ($column_tasks as $phid => $task) { + $visible_phids[$phid] = $phid; + } + } + + $rendering_engine = id(new PhabricatorBoardRenderingEngine()) + ->setViewer($viewer) + ->setObjects(array_select_keys($tasks, $visible_phids)) + ->setEditMap($task_can_edit_map) + ->setExcludedProjectPHIDs($select_phids); + + foreach ($visible_columns as $column_phid => $column) { + $column_tasks = $column_phids[$column_phid]; + $panel = id(new PHUIWorkpanelView()) ->setHeader($column->getDisplayName()) ->setSubHeader($column->getDisplayType()) @@ -317,22 +322,10 @@ final class PhabricatorProjectBoardViewController )); foreach ($column_tasks as $task) { - $owner = null; - if ($task->getOwnerPHID()) { - $owner = $this->handles[$task->getOwnerPHID()]; - } - $can_edit = idx($task_can_edit_map, $task->getPHID(), false); - - $handles = array_select_keys($all_handles, $task->getProjectPHIDs()); - - $cards->addItem(id(new ProjectBoardTaskCard()) - ->setViewer($viewer) - ->setProjectHandles($handles) - ->setTask($task) - ->setOwner($owner) - ->setCanEdit($can_edit) - ->getItem()); + $card = $rendering_engine->renderCard($task->getPHID()); + $cards->addItem($card->getItem()); } + $panel->setCards($cards); $board->addPanel($panel); } diff --git a/src/applications/project/controller/PhabricatorProjectMoveController.php b/src/applications/project/controller/PhabricatorProjectMoveController.php index 7cbbf3d0ae..8cc8a355af 100644 --- a/src/applications/project/controller/PhabricatorProjectMoveController.php +++ b/src/applications/project/controller/PhabricatorProjectMoveController.php @@ -175,24 +175,11 @@ final class PhabricatorProjectMoveController $editor->applyTransactions($object, $xactions); - $owner = null; - if ($object->getOwnerPHID()) { - $owner = id(new PhabricatorHandleQuery()) - ->setViewer($viewer) - ->withPHIDs(array($object->getOwnerPHID())) - ->executeOne(); - } - // Reload the object so it reflects edits which have been applied. $object = id(new ManiphestTaskQuery()) ->setViewer($viewer) ->withPHIDs(array($object_phid)) ->needProjectPHIDs(true) - ->requireCapabilities( - array( - PhabricatorPolicyCapability::CAN_VIEW, - PhabricatorPolicyCapability::CAN_EDIT, - )) ->executeOne(); $except_phids = array($board_phid); @@ -206,25 +193,21 @@ final class PhabricatorProjectMoveController } } - $except_phids = array_fuse($except_phids); - $handle_phids = array_fuse($object->getProjectPHIDs()); - $handle_phids = array_diff_key($handle_phids, $except_phids); - - $project_handles = $viewer->loadHandles($handle_phids); - $project_handles = iterator_to_array($project_handles); - - $card = id(new ProjectBoardTaskCard()) + $rendering_engine = id(new PhabricatorBoardRenderingEngine()) ->setViewer($viewer) - ->setTask($object) - ->setOwner($owner) - ->setCanEdit(true) - ->setProjectHandles($project_handles) - ->getItem(); + ->setObjects(array($object)) + ->setExcludedProjectPHIDs($except_phids); - $card->addClass('phui-workcard'); + $card = $rendering_engine->renderCard($object->getPHID()); - return id(new AphrontAjaxResponse())->setContent( - array('task' => $card)); + $item = $card->getItem(); + $item->addClass('phui-workcard'); + + return id(new AphrontAjaxResponse()) + ->setContent( + array( + 'task' => $item, + )); } } diff --git a/src/applications/project/engine/PhabricatorBoardRenderingEngine.php b/src/applications/project/engine/PhabricatorBoardRenderingEngine.php new file mode 100644 index 0000000000..ca8b0633da --- /dev/null +++ b/src/applications/project/engine/PhabricatorBoardRenderingEngine.php @@ -0,0 +1,144 @@ +viewer = $viewer; + return $this; + } + + public function getViewer() { + return $this->viewer; + } + + public function setObjects(array $objects) { + $this->objects = mpull($objects, null, 'getPHID'); + return $this; + } + + public function getObjects() { + return $this->objects; + } + + public function setExcludedProjectPHIDs(array $phids) { + $this->excludedProjectPHIDs = $phids; + return $this; + } + + public function getExcludedProjectPHIDs() { + return $this->excludedProjectPHIDs; + } + + public function setEditMap(array $edit_map) { + $this->editMap = $edit_map; + return $this; + } + + public function getEditMap() { + return $this->editMap; + } + + public function renderCard($phid) { + $this->willRender(); + + $viewer = $this->getViewer(); + $object = idx($this->getObjects(), $phid); + + $card = id(new ProjectBoardTaskCard()) + ->setViewer($viewer) + ->setTask($object) + ->setCanEdit($this->getCanEdit($phid)); + + $owner_phid = $object->getOwnerPHID(); + if ($owner_phid) { + $owner_handle = $this->handles[$owner_phid]; + $card->setOwner($owner_handle); + } + + $project_phids = $object->getProjectPHIDs(); + $project_handles = array_select_keys($this->handles, $project_phids); + if ($project_handles) { + $card->setProjectHandles($project_handles); + } + + $cover_phid = $object->getCoverImageThumbnailPHID(); + if ($cover_phid) { + $cover_file = idx($this->coverFiles, $cover_phid); + if ($cover_file) { + $card->setCoverImageFile($cover_file); + } + } + + return $card; + } + + private function willRender() { + if ($this->loaded) { + return; + } + + $phids = array(); + foreach ($this->objects as $object) { + $owner_phid = $object->getOwnerPHID(); + if ($owner_phid) { + $phids[$owner_phid] = $owner_phid; + } + + foreach ($object->getProjectPHIDs() as $phid) { + $phids[$phid] = $phid; + } + } + + if ($this->excludedProjectPHIDs) { + foreach ($this->excludedProjectPHIDs as $excluded_phid) { + unset($phids[$excluded_phid]); + } + } + + $viewer = $this->getViewer(); + + $handles = $viewer->loadHandles($phids); + $handles = iterator_to_array($handles); + $this->handles = $handles; + + $cover_phids = array(); + foreach ($this->objects as $object) { + $cover_phid = $object->getCoverImageThumbnailPHID(); + if ($cover_phid) { + $cover_phids[$cover_phid] = $cover_phid; + } + } + + if ($cover_phids) { + $cover_files = id(new PhabricatorFileQuery()) + ->setViewer($viewer) + ->withPHIDs($cover_phids) + ->execute(); + $cover_files = mpull($cover_files, null, 'getPHID'); + } else { + $cover_files = array(); + } + + $this->coverFiles = $cover_files; + + $this->loaded = true; + } + + private function getCanEdit($phid) { + if ($this->editMap === null) { + return true; + } + + return idx($this->editMap, $phid); + } + +} diff --git a/src/applications/project/view/ProjectBoardTaskCard.php b/src/applications/project/view/ProjectBoardTaskCard.php index 2c69e0f37b..065acc6c13 100644 --- a/src/applications/project/view/ProjectBoardTaskCard.php +++ b/src/applications/project/view/ProjectBoardTaskCard.php @@ -7,6 +7,7 @@ final class ProjectBoardTaskCard extends Phobject { private $task; private $owner; private $canEdit; + private $coverImageFile; public function setViewer(PhabricatorUser $viewer) { $this->viewer = $viewer; @@ -25,6 +26,15 @@ final class ProjectBoardTaskCard extends Phobject { return $this->projectHandles; } + public function setCoverImageFile(PhabricatorFile $cover_image_file) { + $this->coverImageFile = $cover_image_file; + return $this; + } + + public function getCoverImageFile() { + return $this->coverImageFile; + } + public function setTask(ManiphestTask $task) { $this->task = $task; return $this; @@ -84,6 +94,11 @@ final class ProjectBoardTaskCard extends Phobject { $card->addHandleIcon($owner, $owner->getName()); } + $cover_file = $this->getCoverImageFile(); + if ($cover_file) { + $card->setCoverImage($cover_file->getBestURI()); + } + if ($task->isClosed()) { $icon = ManiphestTaskStatus::getStatusIcon($task->getStatus()); $icon = id(new PHUIIconView()) From 78c248d330245b2f74267590d3d0f6af952c99f3 Mon Sep 17 00:00:00 2001 From: epriestley Date: Sat, 6 Feb 2016 14:05:15 -0800 Subject: [PATCH 06/65] Support drag-and-drop to set cover images on workboard cards Summary: This was slightly more complex than I believed, but not too terrible. Test Plan: {F1096126} - Also used some normal file uploaders to make sure I didn't break that. Reviewers: chad Reviewed By: chad Differential Revision: https://secure.phabricator.com/D15202 --- resources/builtin/image-526x526.png | Bin 0 -> 2435 bytes resources/celerity/map.php | 65 ++++---- src/__phutil_library_map__.php | 2 + .../editor/ManiphestTransactionEditor.php | 79 +++++++++ .../maniphest/storage/ManiphestTask.php | 4 + .../storage/ManiphestTransaction.php | 4 + .../PhabricatorProjectApplication.php | 1 + .../PhabricatorProjectBoardViewController.php | 3 + .../PhabricatorProjectController.php | 49 ++++++ .../PhabricatorProjectCoverController.php | 53 ++++++ .../PhabricatorProjectMoveController.php | 37 +---- .../css/phui/workboards/phui-workcard.css | 4 + .../projects/behavior-project-boards.js | 34 ++++ webroot/rsrc/js/core/DragAndDropFileUpload.js | 155 +++++++++--------- webroot/rsrc/js/core/FileUpload.js | 1 + 15 files changed, 352 insertions(+), 139 deletions(-) create mode 100644 resources/builtin/image-526x526.png create mode 100644 src/applications/project/controller/PhabricatorProjectCoverController.php diff --git a/resources/builtin/image-526x526.png b/resources/builtin/image-526x526.png new file mode 100644 index 0000000000000000000000000000000000000000..853539d21aec0c1af03257e10ceeab52a50bcc0b GIT binary patch literal 2435 zcmeAS@N?(olHy`uVBq!ia0y~yVB!N|4kn;T{7s=eASGGi8c`CQpH@Ur(SQadTTvFZr20(BV`AhCuwx}3MyviWbNkvxzTyTzlqz} z=hoO=Ffg8bCh^y$;$Q@f^4of@~^+|KUZJtfhA87q_C zKX|YpYVEeDwM9E)w5Fabn|t{1;T`Ax{Hd|2`=ui4!7Adt=hX7X2NxH+yI)h9Td+yv z$fHL|RzI`Pv9D}+?_ZfvR`xAHVecJxvr8X?=ikdQ+yBgD0@sgy!4tPXe0zJlwYBv| zYg1EGW8=y7-z_o)CUUvGi|>s-kg-f@OVrwadHZW@Ny^7sAAWpfVc6=y8pL?i-*wL& zccE?Dw`))J;&RB@a{Fzt^+Y8_kV|6zsRcz%KNqfZX@|j^mzS4kXJ^NMQQpr4l&W0S zaa`xLRIl6q`~E9CdT*R<@A>fL_dBcdzWtQQa-=(`s?la_xs-K>FOSR_v+QFhyRs?KE?zC!`4-P>FKA{ z-{0lt=E|B&E_wPcYVEYL_xJb9Z+6|6@$tvU$Ft4zcl828(?(R2S$$8x$no#rzh8cy zk*qv-+PVJcQPTY_w#F_L>?XMmEO2<_wL!V3~h>cfb_-iu;0!5e&uUh z$)OKV9-fy}6g3c1V%Qs(UtRs1$NTcd&Wd%Wxl5S3YZLmU={tWLK7|LJu*zr0?GrPb+8D_0(SdfL=nY-Piv71v*1U-!4->#M8WJEP7B zU+nxdy}lvD!EjmEt0zyE{ANgh@Ld>NNdhd6Nh*=C*8xCL>=M{32kbVEI%Q-;P-`?1 rN$&nEoqcips@}UX6wLY0BEcYgc&4%S6!SU2RuzM%tDnm{r-UW|Uft$t literal 0 HcmV?d00001 diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 077303a539..ed0bf7dd88 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -8,10 +8,10 @@ return array( 'names' => array( 'core.pkg.css' => 'a7d4cf8f', - 'core.pkg.js' => 'ef5e33db', + 'core.pkg.js' => '808ae845', 'darkconsole.pkg.js' => 'e7393ebb', 'differential.pkg.css' => '2de124c9', - 'differential.pkg.js' => '5c2ba922', + 'differential.pkg.js' => '6b42b4bc', 'diffusion.pkg.css' => 'f45955ed', 'diffusion.pkg.js' => '3a9a8bfa', 'maniphest.pkg.css' => '4845691a', @@ -155,7 +155,7 @@ return array( 'rsrc/css/phui/phui-timeline-view.css' => '2efceff8', 'rsrc/css/phui/phui-two-column-view.css' => 'c75bfc5b', 'rsrc/css/phui/workboards/phui-workboard.css' => 'b07a5524', - 'rsrc/css/phui/workboards/phui-workcard.css' => 'adf34f58', + 'rsrc/css/phui/workboards/phui-workcard.css' => 'a869098a', 'rsrc/css/phui/workboards/phui-workpanel.css' => 'e1bd8d04', 'rsrc/css/sprite-login.css' => '60e8560e', 'rsrc/css/sprite-menu.css' => '9dd65b92', @@ -414,7 +414,7 @@ return array( 'rsrc/js/application/phortune/phortune-credit-card-form.js' => '2290aeef', 'rsrc/js/application/policy/behavior-policy-control.js' => 'd0c516d5', 'rsrc/js/application/policy/behavior-policy-rule-editor.js' => '5e9f347c', - 'rsrc/js/application/projects/behavior-project-boards.js' => '48470f95', + 'rsrc/js/application/projects/behavior-project-boards.js' => '5191522f', 'rsrc/js/application/projects/behavior-project-create.js' => '065227cc', 'rsrc/js/application/projects/behavior-reorder-columns.js' => 'e1d25dfb', 'rsrc/js/application/releeph/releeph-preview-branch.js' => 'b2b4fbaf', @@ -446,9 +446,9 @@ return array( 'rsrc/js/application/uiexample/gesture-example.js' => '558829c2', 'rsrc/js/application/uiexample/notification-example.js' => '8ce821c5', 'rsrc/js/core/Busy.js' => '59a7976a', - 'rsrc/js/core/DragAndDropFileUpload.js' => 'ad10aeac', + 'rsrc/js/core/DragAndDropFileUpload.js' => 'da044194', 'rsrc/js/core/DraggableList.js' => '8905523d', - 'rsrc/js/core/FileUpload.js' => '477359c8', + 'rsrc/js/core/FileUpload.js' => '680ea2c8', 'rsrc/js/core/Hovercard.js' => '1bd28176', 'rsrc/js/core/KeyboardShortcut.js' => '1ae869f2', 'rsrc/js/core/KeyboardShortcutManager.js' => 'c1700f6f', @@ -654,7 +654,7 @@ return array( 'javelin-behavior-phui-profile-menu' => '12884df9', 'javelin-behavior-policy-control' => 'd0c516d5', 'javelin-behavior-policy-rule-editor' => '5e9f347c', - 'javelin-behavior-project-boards' => '48470f95', + 'javelin-behavior-project-boards' => '5191522f', 'javelin-behavior-project-create' => '065227cc', 'javelin-behavior-quicksand-blacklist' => '7927a7d3', 'javelin-behavior-recurring-edit' => '5f1c4d5f', @@ -741,11 +741,11 @@ return array( 'phabricator-core-css' => '5b3563c8', 'phabricator-countdown-css' => 'e7544472', 'phabricator-dashboard-css' => 'eb458607', - 'phabricator-drag-and-drop-file-upload' => 'ad10aeac', + 'phabricator-drag-and-drop-file-upload' => 'da044194', 'phabricator-draggable-list' => '8905523d', 'phabricator-fatal-config-template-css' => '8e6c6fcd', 'phabricator-feed-css' => 'ecd4ec57', - 'phabricator-file-upload' => '477359c8', + 'phabricator-file-upload' => '680ea2c8', 'phabricator-filetree-view-css' => 'fccf9f82', 'phabricator-flag-css' => '5337623f', 'phabricator-keyboard-shortcut' => '1ae869f2', @@ -832,7 +832,7 @@ return array( 'phui-timeline-view-css' => '2efceff8', 'phui-two-column-view-css' => 'c75bfc5b', 'phui-workboard-view-css' => 'b07a5524', - 'phui-workcard-view-css' => 'adf34f58', + 'phui-workcard-view-css' => 'a869098a', 'phui-workpanel-view-css' => 'e1bd8d04', 'phuix-action-list-view' => 'b5c256b8', 'phuix-action-view' => '8cf6d262', @@ -1133,11 +1133,6 @@ return array( 'javelin-dom', 'javelin-workflow', ), - '477359c8' => array( - 'javelin-install', - 'javelin-dom', - 'phabricator-notification', - ), 47830651 => array( 'javelin-behavior', 'javelin-dom', @@ -1154,15 +1149,6 @@ return array( 'javelin-dom', 'javelin-workflow', ), - '48470f95' => array( - 'javelin-behavior', - 'javelin-dom', - 'javelin-util', - 'javelin-vector', - 'javelin-stratcom', - 'javelin-workflow', - 'phabricator-draggable-list', - ), '49b73b36' => array( 'javelin-behavior', 'javelin-dom', @@ -1204,6 +1190,16 @@ return array( 'javelin-typeahead-source', 'javelin-util', ), + '5191522f' => array( + 'javelin-behavior', + 'javelin-dom', + 'javelin-util', + 'javelin-vector', + 'javelin-stratcom', + 'javelin-workflow', + 'phabricator-draggable-list', + 'phabricator-drag-and-drop-file-upload', + ), '519705ea' => array( 'javelin-install', 'javelin-dom', @@ -1331,6 +1327,11 @@ return array( 'javelin-request', 'javelin-workflow', ), + '680ea2c8' => array( + 'javelin-install', + 'javelin-dom', + 'phabricator-notification', + ), '6882e80a' => array( 'javelin-dom', ), @@ -1674,14 +1675,6 @@ return array( 'javelin-util', 'phabricator-busy', ), - 'ad10aeac' => array( - 'javelin-install', - 'javelin-util', - 'javelin-request', - 'javelin-dom', - 'javelin-uri', - 'phabricator-file-upload', - ), 'b064af76' => array( 'javelin-behavior', 'javelin-stratcom', @@ -1897,6 +1890,14 @@ return array( 'javelin-util', 'phabricator-shaped-request', ), + 'da044194' => array( + 'javelin-install', + 'javelin-util', + 'javelin-request', + 'javelin-dom', + 'javelin-uri', + 'phabricator-file-upload', + ), 'dbbf48b6' => array( 'javelin-behavior', 'javelin-stratcom', diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 9e65dda01a..76105fdda8 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -2880,6 +2880,7 @@ phutil_register_library_map(array( 'PhabricatorProjectConfiguredCustomField' => 'applications/project/customfield/PhabricatorProjectConfiguredCustomField.php', 'PhabricatorProjectController' => 'applications/project/controller/PhabricatorProjectController.php', 'PhabricatorProjectCoreTestCase' => 'applications/project/__tests__/PhabricatorProjectCoreTestCase.php', + 'PhabricatorProjectCoverController' => 'applications/project/controller/PhabricatorProjectCoverController.php', 'PhabricatorProjectCustomField' => 'applications/project/customfield/PhabricatorProjectCustomField.php', 'PhabricatorProjectCustomFieldNumericIndex' => 'applications/project/storage/PhabricatorProjectCustomFieldNumericIndex.php', 'PhabricatorProjectCustomFieldStorage' => 'applications/project/storage/PhabricatorProjectCustomFieldStorage.php', @@ -7304,6 +7305,7 @@ phutil_register_library_map(array( ), 'PhabricatorProjectController' => 'PhabricatorController', 'PhabricatorProjectCoreTestCase' => 'PhabricatorTestCase', + 'PhabricatorProjectCoverController' => 'PhabricatorProjectController', 'PhabricatorProjectCustomField' => 'PhabricatorCustomField', 'PhabricatorProjectCustomFieldNumericIndex' => 'PhabricatorCustomFieldNumericIndexStorage', 'PhabricatorProjectCustomFieldStorage' => 'PhabricatorCustomFieldStorage', diff --git a/src/applications/maniphest/editor/ManiphestTransactionEditor.php b/src/applications/maniphest/editor/ManiphestTransactionEditor.php index 46e2a617e8..8473b3c933 100644 --- a/src/applications/maniphest/editor/ManiphestTransactionEditor.php +++ b/src/applications/maniphest/editor/ManiphestTransactionEditor.php @@ -28,6 +28,7 @@ final class ManiphestTransactionEditor $types[] = ManiphestTransaction::TYPE_UNBLOCK; $types[] = ManiphestTransaction::TYPE_PARENT; $types[] = ManiphestTransaction::TYPE_COLUMN; + $types[] = ManiphestTransaction::TYPE_COVER_IMAGE; $types[] = PhabricatorTransactions::TYPE_VIEW_POLICY; $types[] = PhabricatorTransactions::TYPE_EDIT_POLICY; @@ -66,6 +67,8 @@ final class ManiphestTransactionEditor return $xaction->getOldValue(); case ManiphestTransaction::TYPE_SUBPRIORITY: return $object->getSubpriority(); + case ManiphestTransaction::TYPE_COVER_IMAGE: + return $object->getCoverImageFilePHID(); case ManiphestTransaction::TYPE_MERGED_INTO: case ManiphestTransaction::TYPE_MERGED_FROM: return null; @@ -92,6 +95,7 @@ final class ManiphestTransactionEditor case ManiphestTransaction::TYPE_MERGED_INTO: case ManiphestTransaction::TYPE_MERGED_FROM: case ManiphestTransaction::TYPE_UNBLOCK: + case ManiphestTransaction::TYPE_COVER_IMAGE: return $xaction->getNewValue(); case ManiphestTransaction::TYPE_PARENT: case ManiphestTransaction::TYPE_COLUMN: @@ -161,6 +165,32 @@ final class ManiphestTransactionEditor case ManiphestTransaction::TYPE_MERGED_INTO: $object->setStatus(ManiphestTaskStatus::getDuplicateStatus()); return; + case ManiphestTransaction::TYPE_COVER_IMAGE: + $file_phid = $xaction->getNewValue(); + + if ($file_phid) { + $file = id(new PhabricatorFileQuery()) + ->setViewer($this->getActor()) + ->withPHIDs(array($file_phid)) + ->executeOne(); + } else { + $file = null; + } + + if (!$file || !$file->isTransformableImage()) { + $object->setProperty('cover.filePHID', null); + $object->setProperty('cover.thumbnailPHID', null); + return; + } + + $xform_key = PhabricatorFileThumbnailTransform::TRANSFORM_WORKCARD; + + $xform = PhabricatorFileTransform::getTransformByKey($xform_key) + ->executeTransform($file); + + $object->setProperty('cover.filePHID', $file->getPHID()); + $object->setProperty('cover.thumbnailPHID', $xform->getPHID()); + return; case ManiphestTransaction::TYPE_MERGED_FROM: case ManiphestTransaction::TYPE_PARENT: case ManiphestTransaction::TYPE_COLUMN: @@ -819,6 +849,41 @@ final class ManiphestTransactionEditor } } break; + case ManiphestTransaction::TYPE_COVER_IMAGE: + foreach ($xactions as $xaction) { + $old = $xaction->getOldValue(); + $new = $xaction->getNewValue(); + if (!$new) { + continue; + } + + if ($new === $old) { + continue; + } + + $file = id(new PhabricatorFileQuery()) + ->setViewer($this->getActor()) + ->withPHIDs(array($new)) + ->executeOne(); + if (!$file) { + $errors[] = new PhabricatorApplicationTransactionValidationError( + $type, + pht('Invalid'), + pht('File "%s" is not valid.', $new), + $xaction); + continue; + } + + if (!$file->isTransformableImage()) { + $errors[] = new PhabricatorApplicationTransactionValidationError( + $type, + pht('Invalid'), + pht('File "%s" is not a valid image file.', $new), + $xaction); + continue; + } + } + break; } return $errors; @@ -941,5 +1006,19 @@ final class ManiphestTransactionEditor ->executeOne(); } + protected function extractFilePHIDsFromCustomTransaction( + PhabricatorLiskDAO $object, + PhabricatorApplicationTransaction $xaction) { + $phids = parent::extractFilePHIDsFromCustomTransaction($object, $xaction); + + switch ($xaction->getTransactionType()) { + case ManiphestTransaction::TYPE_COVER_IMAGE: + $phids[] = $xaction->getNewValue(); + break; + } + + return $phids; + } + } diff --git a/src/applications/maniphest/storage/ManiphestTask.php b/src/applications/maniphest/storage/ManiphestTask.php index 5b6c4129dd..2571b7a516 100644 --- a/src/applications/maniphest/storage/ManiphestTask.php +++ b/src/applications/maniphest/storage/ManiphestTask.php @@ -226,6 +226,10 @@ final class ManiphestTask extends ManiphestDAO return idx($this->properties, $key, $default); } + public function getCoverImageFilePHID() { + return idx($this->properties, 'cover.filePHID'); + } + public function getCoverImageThumbnailPHID() { return idx($this->properties, 'cover.thumbnailPHID'); } diff --git a/src/applications/maniphest/storage/ManiphestTransaction.php b/src/applications/maniphest/storage/ManiphestTransaction.php index 6c2c8c306f..812ac0a7de 100644 --- a/src/applications/maniphest/storage/ManiphestTransaction.php +++ b/src/applications/maniphest/storage/ManiphestTransaction.php @@ -16,6 +16,7 @@ final class ManiphestTransaction const TYPE_UNBLOCK = 'unblock'; const TYPE_PARENT = 'parent'; const TYPE_COLUMN = 'column'; + const TYPE_COVER_IMAGE = 'cover-image'; // NOTE: this type is deprecated. Keep it around for legacy installs // so any transactions render correctly. @@ -162,6 +163,9 @@ final class ManiphestTransaction sort($new_cols); return ($old_cols === $new_cols); + case self::TYPE_COVER_IMAGE: + // At least for now, don't show these. + return true; } return parent::shouldHide(); diff --git a/src/applications/project/application/PhabricatorProjectApplication.php b/src/applications/project/application/PhabricatorProjectApplication.php index 5f3cb9e090..bd321bbaa8 100644 --- a/src/applications/project/application/PhabricatorProjectApplication.php +++ b/src/applications/project/application/PhabricatorProjectApplication.php @@ -72,6 +72,7 @@ final class PhabricatorProjectApplication extends PhabricatorApplication { '(?:query/(?P[^/]+)/)?' => 'PhabricatorProjectBoardViewController', 'move/(?P[1-9]\d*)/' => 'PhabricatorProjectMoveController', + 'cover/' => 'PhabricatorProjectCoverController', 'board/(?P[1-9]\d*)/' => array( 'edit/(?:(?P\d+)/)?' => 'PhabricatorProjectColumnEditController', diff --git a/src/applications/project/controller/PhabricatorProjectBoardViewController.php b/src/applications/project/controller/PhabricatorProjectBoardViewController.php index 1f00a8a7a6..ce8a63062e 100644 --- a/src/applications/project/controller/PhabricatorProjectBoardViewController.php +++ b/src/applications/project/controller/PhabricatorProjectBoardViewController.php @@ -219,6 +219,9 @@ final class PhabricatorProjectBoardViewController 'projectPHID' => $project->getPHID(), 'moveURI' => $this->getApplicationURI('move/'.$project->getID().'/'), 'createURI' => $this->getCreateURI(), + 'uploadURI' => '/file/dropupload/', + 'coverURI' => $this->getApplicationURI('cover/'), + 'chunkThreshold' => PhabricatorFileStorageEngine::getChunkThreshold(), 'order' => $this->sortKey, ); $this->initBehavior( diff --git a/src/applications/project/controller/PhabricatorProjectController.php b/src/applications/project/controller/PhabricatorProjectController.php index 8687e0a3a7..e995461ba6 100644 --- a/src/applications/project/controller/PhabricatorProjectController.php +++ b/src/applications/project/controller/PhabricatorProjectController.php @@ -147,4 +147,53 @@ abstract class PhabricatorProjectController extends PhabricatorController { return $this; } + protected function newCardResponse($board_phid, $object_phid) { + $viewer = $this->getViewer(); + + $project = id(new PhabricatorProjectQuery()) + ->setViewer($viewer) + ->withPHIDs(array($board_phid)) + ->executeOne(); + if (!$project) { + return new Aphront404Response(); + } + + // Reload the object so it reflects edits which have been applied. + $object = id(new ManiphestTaskQuery()) + ->setViewer($viewer) + ->withPHIDs(array($object_phid)) + ->needProjectPHIDs(true) + ->executeOne(); + if (!$object) { + return new Aphront404Response(); + } + + $except_phids = array($board_phid); + if ($project->getHasSubprojects() || $project->getHasMilestones()) { + $descendants = id(new PhabricatorProjectQuery()) + ->setViewer($viewer) + ->withAncestorProjectPHIDs($except_phids) + ->execute(); + foreach ($descendants as $descendant) { + $except_phids[] = $descendant->getPHID(); + } + } + + $rendering_engine = id(new PhabricatorBoardRenderingEngine()) + ->setViewer($viewer) + ->setObjects(array($object)) + ->setExcludedProjectPHIDs($except_phids); + + $card = $rendering_engine->renderCard($object->getPHID()); + + $item = $card->getItem(); + $item->addClass('phui-workcard'); + + return id(new AphrontAjaxResponse()) + ->setContent( + array( + 'task' => $item, + )); + } + } diff --git a/src/applications/project/controller/PhabricatorProjectCoverController.php b/src/applications/project/controller/PhabricatorProjectCoverController.php new file mode 100644 index 0000000000..22f787e56b --- /dev/null +++ b/src/applications/project/controller/PhabricatorProjectCoverController.php @@ -0,0 +1,53 @@ +getViewer(); + + $request->validateCSRF(); + + $board_phid = $request->getStr('boardPHID'); + $object_phid = $request->getStr('objectPHID'); + $file_phid = $request->getStr('filePHID'); + + $object = id(new ManiphestTaskQuery()) + ->setViewer($viewer) + ->withPHIDs(array($object_phid)) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->executeOne(); + if (!$object) { + return new Aphront404Response(); + } + + $file = id(new PhabricatorFileQuery()) + ->setViewer($viewer) + ->withPHIDs(array($file_phid)) + ->executeOne(); + if (!$file) { + return new Aphront404Response(); + } + + $xactions = array(); + + $xactions[] = id(new ManiphestTransaction()) + ->setTransactionType(ManiphestTransaction::TYPE_COVER_IMAGE) + ->setNewValue($file->getPHID()); + + $editor = id(new ManiphestTransactionEditor()) + ->setActor($viewer) + ->setContinueOnMissingFields(true) + ->setContinueOnNoEffect(true) + ->setContentSourceFromRequest($request); + + $editor->applyTransactions($object, $xactions); + + return $this->newCardResponse($board_phid, $object_phid); + } + +} diff --git a/src/applications/project/controller/PhabricatorProjectMoveController.php b/src/applications/project/controller/PhabricatorProjectMoveController.php index 8cc8a355af..4aa9e0eec2 100644 --- a/src/applications/project/controller/PhabricatorProjectMoveController.php +++ b/src/applications/project/controller/PhabricatorProjectMoveController.php @@ -7,13 +7,14 @@ final class PhabricatorProjectMoveController $viewer = $request->getViewer(); $id = $request->getURIData('id'); + $request->validateCSRF(); + $column_phid = $request->getStr('columnPHID'); $object_phid = $request->getStr('objectPHID'); $after_phid = $request->getStr('afterPHID'); $before_phid = $request->getStr('beforePHID'); $order = $request->getStr('order', PhabricatorProjectColumn::DEFAULT_ORDER); - $project = id(new PhabricatorProjectQuery()) ->setViewer($viewer) ->requireCapabilities( @@ -175,39 +176,7 @@ final class PhabricatorProjectMoveController $editor->applyTransactions($object, $xactions); - // Reload the object so it reflects edits which have been applied. - $object = id(new ManiphestTaskQuery()) - ->setViewer($viewer) - ->withPHIDs(array($object_phid)) - ->needProjectPHIDs(true) - ->executeOne(); - - $except_phids = array($board_phid); - if ($project->getHasSubprojects() || $project->getHasMilestones()) { - $descendants = id(new PhabricatorProjectQuery()) - ->setViewer($viewer) - ->withAncestorProjectPHIDs($except_phids) - ->execute(); - foreach ($descendants as $descendant) { - $except_phids[] = $descendant->getPHID(); - } - } - - $rendering_engine = id(new PhabricatorBoardRenderingEngine()) - ->setViewer($viewer) - ->setObjects(array($object)) - ->setExcludedProjectPHIDs($except_phids); - - $card = $rendering_engine->renderCard($object->getPHID()); - - $item = $card->getItem(); - $item->addClass('phui-workcard'); - - return id(new AphrontAjaxResponse()) - ->setContent( - array( - 'task' => $item, - )); + return $this->newCardResponse($board_phid, $object_phid); } } diff --git a/webroot/rsrc/css/phui/workboards/phui-workcard.css b/webroot/rsrc/css/phui/workboards/phui-workcard.css index dbdee0103a..e8a6b44942 100644 --- a/webroot/rsrc/css/phui/workboards/phui-workcard.css +++ b/webroot/rsrc/css/phui/workboards/phui-workcard.css @@ -106,6 +106,10 @@ width: 263px; } +.phui-workcard.phui-object-item.phui-workcard-upload-target { + background-color: {$sh-greenbackground}; +} + /* - Draggable Colors --------------------------------------------------------*/ diff --git a/webroot/rsrc/js/application/projects/behavior-project-boards.js b/webroot/rsrc/js/application/projects/behavior-project-boards.js index 8d9b61cc8c..6ae321df8a 100644 --- a/webroot/rsrc/js/application/projects/behavior-project-boards.js +++ b/webroot/rsrc/js/application/projects/behavior-project-boards.js @@ -7,6 +7,7 @@ * javelin-stratcom * javelin-workflow * phabricator-draggable-list + * phabricator-drag-and-drop-file-upload */ JX.behavior('project-boards', function(config, statics) { @@ -348,6 +349,39 @@ JX.behavior('project-boards', function(config, statics) { init_board(); } }); + + if (JX.PhabricatorDragAndDropFileUpload.isSupported()) { + var drop = new JX.PhabricatorDragAndDropFileUpload('project-card') + .setURI(config.uploadURI) + .setChunkThreshold(config.chunkThreshold); + + drop.listen('didBeginDrag', function(node) { + JX.DOM.alterClass(node, 'phui-workcard-upload-target', true); + }); + + drop.listen('didEndDrag', function(node) { + JX.DOM.alterClass(node, 'phui-workcard-upload-target', false); + }); + + drop.listen('didUpload', function(file) { + var node = file.getTargetNode(); + + var data = { + boardPHID: statics.projectPHID, + objectPHID: JX.Stratcom.getData(node).objectPHID, + filePHID: file.getPHID() + }; + + new JX.Workflow(config.coverURI, data) + .setHandler(function(r) { + JX.DOM.replace(node, JX.$H(r.task)); + }) + .start(); + }); + + drop.start(); + } + return true; } diff --git a/webroot/rsrc/js/core/DragAndDropFileUpload.js b/webroot/rsrc/js/core/DragAndDropFileUpload.js index cee617061c..c3c9801a17 100644 --- a/webroot/rsrc/js/core/DragAndDropFileUpload.js +++ b/webroot/rsrc/js/core/DragAndDropFileUpload.js @@ -11,8 +11,12 @@ JX.install('PhabricatorDragAndDropFileUpload', { - construct : function(node) { - this._node = node; + construct : function(target) { + if (JX.DOM.isNode(target)) { + this._node = target; + } else { + this._sigil = target; + } }, events : [ @@ -39,6 +43,7 @@ JX.install('PhabricatorDragAndDropFileUpload', { members : { _node : null, + _sigil: null, _depth : 0, _isEnabled: false, @@ -53,18 +58,21 @@ JX.install('PhabricatorDragAndDropFileUpload', { _updateDepth : function(delta) { if (this._depth === 0 && delta > 0) { - this.invoke('didBeginDrag'); + this.invoke('didBeginDrag', this._getTarget()); } this._depth += delta; if (this._depth === 0 && delta < 0) { - this.invoke('didEndDrag'); + this.invoke('didEndDrag', this._getTarget()); } }, - start : function() { + _getTarget: function() { + return this._target || this._node; + }, + start : function() { // TODO: move this to JX.DOM.contains()? function contains(container, child) { @@ -80,87 +88,87 @@ JX.install('PhabricatorDragAndDropFileUpload', { // Firefox has some issues sometimes; implement this click handler so // the user can recover. See T5188. - JX.DOM.listen( - this._node, - 'click', - null, - JX.bind(this, function (e) { - if (!this.getIsEnabled()) { - return; - } - if (this._depth) { - e.kill(); - // Force depth to 0. - this._updateDepth(-this._depth); - } - })); + var on_click = JX.bind(this, function (e) { + if (!this.getIsEnabled()) { + return; + } + + if (this._depth) { + e.kill(); + // Force depth to 0. + this._updateDepth(-this._depth); + } + }); // We track depth so that the _node may have children inside of it and // not become unselected when they are dragged over. - JX.DOM.listen( - this._node, - 'dragenter', - null, - JX.bind(this, function(e) { - if (!this.getIsEnabled()) { - return; - } + var on_dragenter = JX.bind(this, function(e) { + if (!this.getIsEnabled()) { + return; + } - if (contains(this._node, e.getTarget())) { - this._updateDepth(1); - } - })); + if (!this._node && !this._depth) { + this._target = e.getNode(this._sigil); + } - JX.DOM.listen( - this._node, - 'dragleave', - null, - JX.bind(this, function(e) { - if (!this.getIsEnabled()) { - return; - } + if (contains(this._getTarget(), e.getTarget())) { + this._updateDepth(1); + } + }); - if (contains(this._node, e.getTarget())) { - this._updateDepth(-1); - } - })); + var on_dragleave = JX.bind(this, function(e) { + if (!this.getIsEnabled()) { + return; + } - JX.DOM.listen( - this._node, - 'dragover', - null, - JX.bind(this, function(e) { - if (!this.getIsEnabled()) { - return; - } + if (contains(this._getTarget(), e.getTarget())) { + this._updateDepth(-1); + } + }); - // NOTE: We must set this, or Chrome refuses to drop files from the - // download shelf. - e.getRawEvent().dataTransfer.dropEffect = 'copy'; - e.kill(); - })); + var on_dragover = JX.bind(this, function(e) { + if (!this.getIsEnabled()) { + return; + } - JX.DOM.listen( - this._node, - 'drop', - null, - JX.bind(this, function(e) { - if (!this.getIsEnabled()) { - return; - } + // NOTE: We must set this, or Chrome refuses to drop files from the + // download shelf. + e.getRawEvent().dataTransfer.dropEffect = 'copy'; + e.kill(); + }); - e.kill(); + var on_drop = JX.bind(this, function(e) { + if (!this.getIsEnabled()) { + return; + } - var files = e.getRawEvent().dataTransfer.files; - for (var ii = 0; ii < files.length; ii++) { - this._sendRequest(files[ii]); - } + e.kill(); - // Force depth to 0. - this._updateDepth(-this._depth); - })); + var files = e.getRawEvent().dataTransfer.files; + for (var ii = 0; ii < files.length; ii++) { + this._sendRequest(files[ii]); + } - if (JX.PhabricatorDragAndDropFileUpload.isPasteSupported()) { + // Force depth to 0. + this._updateDepth(-this._depth); + }); + + if (this._node) { + JX.DOM.listen(this._node, 'click', null, on_click); + JX.DOM.listen(this._node, 'dragenter', null, on_dragenter); + JX.DOM.listen(this._node, 'dragleave', null, on_dragleave); + JX.DOM.listen(this._node, 'dragover', null, on_dragover); + JX.DOM.listen(this._node, 'drop', null, on_drop); + } else { + JX.Stratcom.listen('click', this._sigil, on_click); + JX.Stratcom.listen('dragenter', this._sigil, on_dragenter); + JX.Stratcom.listen('dragleave', this._sigil, on_dragleave); + JX.Stratcom.listen('dragover', this._sigil, on_dragover); + JX.Stratcom.listen('drop', this._sigil, on_drop); + } + + if (JX.PhabricatorDragAndDropFileUpload.isPasteSupported() && + this._node) { JX.DOM.listen( this._node, 'paste', @@ -399,6 +407,7 @@ JX.install('PhabricatorDragAndDropFileUpload', { .setURI(r.uri) .setMarkup(r.html) .setStatus('done') + .setTargetNode(this._getTarget()) .update(); this.invoke('didUpload', file); diff --git a/webroot/rsrc/js/core/FileUpload.js b/webroot/rsrc/js/core/FileUpload.js index eff1121c59..22ba33f97a 100644 --- a/webroot/rsrc/js/core/FileUpload.js +++ b/webroot/rsrc/js/core/FileUpload.js @@ -23,6 +23,7 @@ JX.install('PhabricatorFileUpload', { URI: null, status: null, markup: null, + targetNode: null, error: null }, From 4974e8487b9faec6334db15b350d684771b012c6 Mon Sep 17 00:00:00 2001 From: epriestley Date: Sat, 6 Feb 2016 16:07:12 -0800 Subject: [PATCH 07/65] Scale up small cover images instead of surrounding them with empty space Summary: This makes small cover images full-width instead of teeny tiny dots in the middle of an island of whitespace. Test Plan: Uploaded a small cover image. Reviewers: chad Reviewed By: chad Differential Revision: https://secure.phabricator.com/D15203 --- .../files/transform/PhabricatorFileThumbnailTransform.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/applications/files/transform/PhabricatorFileThumbnailTransform.php b/src/applications/files/transform/PhabricatorFileThumbnailTransform.php index 2c1f1149f3..546021fc95 100644 --- a/src/applications/files/transform/PhabricatorFileThumbnailTransform.php +++ b/src/applications/files/transform/PhabricatorFileThumbnailTransform.php @@ -77,6 +77,7 @@ final class PhabricatorFileThumbnailTransform id(new self()) ->setName(pht('Workcard (526px)')) ->setKey(self::TRANSFORM_WORKCARD) + ->setScaleUp(true) ->setDimensions(526, null), ); } From a45fe337a12f29efdf425086018f1160906f0d0f Mon Sep 17 00:00:00 2001 From: epriestley Date: Sat, 6 Feb 2016 16:25:18 -0800 Subject: [PATCH 08/65] Link proxy column headers on workboards to proxied projects Summary: Ref T10010. Allows you to click "Milestone 99" to jump directly to that project. Test Plan: - Clicked milestone header, went to milestone. - Clicked normal column header, nothing happened. Wow! Reviewers: chad Reviewed By: chad Maniphest Tasks: T10010 Differential Revision: https://secure.phabricator.com/D15204 --- .../PhabricatorProjectBoardViewController.php | 7 ++++++ src/view/phui/PHUIHeaderView.php | 25 ++++++++++++++++++- src/view/phui/PHUIWorkpanelView.php | 15 +++++++++++ 3 files changed, 46 insertions(+), 1 deletion(-) diff --git a/src/applications/project/controller/PhabricatorProjectBoardViewController.php b/src/applications/project/controller/PhabricatorProjectBoardViewController.php index ce8a63062e..a10cdc1696 100644 --- a/src/applications/project/controller/PhabricatorProjectBoardViewController.php +++ b/src/applications/project/controller/PhabricatorProjectBoardViewController.php @@ -281,6 +281,13 @@ final class PhabricatorProjectBoardViewController ->setSubHeader($column->getDisplayType()) ->addSigil('workpanel'); + $proxy = $column->getProxy(); + if ($proxy) { + $proxy_id = $proxy->getID(); + $href = $this->getApplicationURI("view/{$proxy_id}/"); + $panel->setHref($href); + } + $header_icon = $column->getHeaderIcon(); if ($header_icon) { $panel->setHeaderIcon($header_icon); diff --git a/src/view/phui/PHUIHeaderView.php b/src/view/phui/PHUIHeaderView.php index 48fedd11ae..50f0e36317 100644 --- a/src/view/phui/PHUIHeaderView.php +++ b/src/view/phui/PHUIHeaderView.php @@ -22,6 +22,7 @@ final class PHUIHeaderView extends AphrontTagView { private $epoch; private $actionIcons = array(); private $badges = array(); + private $href; public function setHeader($header) { $this->header = $header; @@ -147,6 +148,15 @@ final class PHUIHeaderView extends AphrontTagView { return $this; } + public function setHref($href) { + $this->href = $href; + return $this; + } + + public function getHref() { + return $this->href; + } + protected function getTagName() { return 'div'; } @@ -290,12 +300,25 @@ final class PHUIHeaderView extends AphrontTagView { ->setIcon($this->headerIcon); $left[] = $icon; } + + $header_content = $this->header; + + $href = $this->getHref(); + if ($href !== null) { + $header_content = phutil_tag( + 'a', + array( + 'href' => $href, + ), + $header_content); + } + $left[] = phutil_tag( 'span', array( 'class' => 'phui-header-header', ), - $this->header); + $header_content); if ($this->subheader || $this->badges) { $badges = null; diff --git a/src/view/phui/PHUIWorkpanelView.php b/src/view/phui/PHUIWorkpanelView.php index 50b2e12161..f672dc8e04 100644 --- a/src/view/phui/PHUIWorkpanelView.php +++ b/src/view/phui/PHUIWorkpanelView.php @@ -9,6 +9,7 @@ final class PHUIWorkpanelView extends AphrontTagView { private $headerActions = array(); private $headerTag; private $headerIcon; + private $href; public function setHeaderIcon($icon) { $this->headerIcon = $icon; @@ -49,6 +50,15 @@ final class PHUIWorkpanelView extends AphrontTagView { return $this; } + public function setHref($href) { + $this->href = $href; + return $this; + } + + public function getHref() { + return $this->href; + } + protected function getTagAttributes() { return array( 'class' => 'phui-workpanel-view', @@ -85,6 +95,11 @@ final class PHUIWorkpanelView extends AphrontTagView { $header->addActionIcon($action); } + $href = $this->getHref(); + if ($href !== null) { + $header->setHref($href); + } + $body = phutil_tag( 'div', array( From 73f5ff0659290ddac0f57a0622dd3a3e4efd0013 Mon Sep 17 00:00:00 2001 From: epriestley Date: Sat, 6 Feb 2016 20:30:57 -0800 Subject: [PATCH 09/65] Make printing workboards work OK Summary: Fixes T7844. This isn't going to win any awards, but it has all the information. Mostly, we don't (or shouldn't, at least?) need the global `static` stuff anymore because we dropped the top-level custom scrollbar. Test Plan: Printed a PDF of a workboard, got all the cards in the output. Reviewers: chad Reviewed By: chad Maniphest Tasks: T7844 Differential Revision: https://secure.phabricator.com/D15205 --- resources/celerity/map.php | 14 +++++++------- .../css/application/base/standard-page-view.css | 12 ------------ webroot/rsrc/css/phui/phui-profile-menu.css | 4 ++++ .../rsrc/css/phui/workboards/phui-workboard.css | 10 ++++++++++ 4 files changed, 21 insertions(+), 19 deletions(-) diff --git a/resources/celerity/map.php b/resources/celerity/map.php index ed0bf7dd88..6de64110fb 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -7,7 +7,7 @@ */ return array( 'names' => array( - 'core.pkg.css' => 'a7d4cf8f', + 'core.pkg.css' => 'b4a7e275', 'core.pkg.js' => '808ae845', 'darkconsole.pkg.js' => 'e7393ebb', 'differential.pkg.css' => '2de124c9', @@ -36,7 +36,7 @@ return array( 'rsrc/css/application/base/notification-menu.css' => 'f31c0bde', 'rsrc/css/application/base/phabricator-application-launch-view.css' => '95351601', 'rsrc/css/application/base/phui-theme.css' => 'ab7b848c', - 'rsrc/css/application/base/standard-page-view.css' => 'c4467133', + 'rsrc/css/application/base/standard-page-view.css' => 'e709f6d0', 'rsrc/css/application/chatlog/chatlog.css' => 'd295b020', 'rsrc/css/application/conduit/conduit-api.css' => '7bc725c4', 'rsrc/css/application/config/config-options.css' => '0ede4c9b', @@ -146,7 +146,7 @@ return array( 'rsrc/css/phui/phui-object-item-list-view.css' => '8f443e8b', 'rsrc/css/phui/phui-pager.css' => 'bea33d23', 'rsrc/css/phui/phui-pinboard-view.css' => '2495140e', - 'rsrc/css/phui/phui-profile-menu.css' => 'ab4fcf5f', + 'rsrc/css/phui/phui-profile-menu.css' => '4a243229', 'rsrc/css/phui/phui-property-list-view.css' => '27b2849e', 'rsrc/css/phui/phui-remarkup-preview.css' => '1a8f2591', 'rsrc/css/phui/phui-spacing.css' => '042804d6', @@ -154,7 +154,7 @@ return array( 'rsrc/css/phui/phui-tag-view.css' => '9d5d4400', 'rsrc/css/phui/phui-timeline-view.css' => '2efceff8', 'rsrc/css/phui/phui-two-column-view.css' => 'c75bfc5b', - 'rsrc/css/phui/workboards/phui-workboard.css' => 'b07a5524', + 'rsrc/css/phui/workboards/phui-workboard.css' => '2a04b1a8', 'rsrc/css/phui/workboards/phui-workcard.css' => 'a869098a', 'rsrc/css/phui/workboards/phui-workpanel.css' => 'e1bd8d04', 'rsrc/css/sprite-login.css' => '60e8560e', @@ -764,7 +764,7 @@ return array( 'phabricator-side-menu-view-css' => '3a3d9f41', 'phabricator-slowvote-css' => 'da0afb1b', 'phabricator-source-code-view-css' => 'cbeef983', - 'phabricator-standard-page-view' => 'c4467133', + 'phabricator-standard-page-view' => 'e709f6d0', 'phabricator-textareautils' => '9e54692d', 'phabricator-title' => 'df5e11d2', 'phabricator-tooltip' => '6323f942', @@ -822,7 +822,7 @@ return array( 'phui-object-item-list-view-css' => '8f443e8b', 'phui-pager-css' => 'bea33d23', 'phui-pinboard-view-css' => '2495140e', - 'phui-profile-menu-css' => 'ab4fcf5f', + 'phui-profile-menu-css' => '4a243229', 'phui-property-list-view-css' => '27b2849e', 'phui-remarkup-preview-css' => '1a8f2591', 'phui-spacing-css' => '042804d6', @@ -831,7 +831,7 @@ return array( 'phui-theme-css' => 'ab7b848c', 'phui-timeline-view-css' => '2efceff8', 'phui-two-column-view-css' => 'c75bfc5b', - 'phui-workboard-view-css' => 'b07a5524', + 'phui-workboard-view-css' => '2a04b1a8', 'phui-workcard-view-css' => 'a869098a', 'phui-workpanel-view-css' => 'e1bd8d04', 'phuix-action-list-view' => 'b5c256b8', diff --git a/webroot/rsrc/css/application/base/standard-page-view.css b/webroot/rsrc/css/application/base/standard-page-view.css index ca69443d07..4b53f61a6a 100644 --- a/webroot/rsrc/css/application/base/standard-page-view.css +++ b/webroot/rsrc/css/application/base/standard-page-view.css @@ -136,18 +136,6 @@ a.handle-availability-disabled { right: 0; } -/* Fixes so pages actually print when magic scrollbar is present */ -!print .main-page-frame { - position: static; - overflow: visible; -} - -!print .jx-scrollbar-viewport { - position: static; - width: auto !important; - height: auto !important; -} - .jx-scrollbar-test { position: absolute; left: -300px; diff --git a/webroot/rsrc/css/phui/phui-profile-menu.css b/webroot/rsrc/css/phui/phui-profile-menu.css index 889d98a075..cc98269a53 100644 --- a/webroot/rsrc/css/phui/phui-profile-menu.css +++ b/webroot/rsrc/css/phui/phui-profile-menu.css @@ -300,3 +300,7 @@ max-width: {$menu.profile.width}; } } + +!print .phui-profile-menu .phabricator-side-menu { + display: none; +} diff --git a/webroot/rsrc/css/phui/workboards/phui-workboard.css b/webroot/rsrc/css/phui/workboards/phui-workboard.css index 54b4907861..81139410dd 100644 --- a/webroot/rsrc/css/phui/workboards/phui-workboard.css +++ b/webroot/rsrc/css/phui/workboards/phui-workboard.css @@ -52,6 +52,16 @@ left: {$menu.profile.width.collapsed}; } +!print .project-board-wrapper .phui-workboard-view-shadow { + position: static; +} + +!print .project-board-wrapper .aphront-multi-column-column-outer { + display: block; + margin: 0 0 18px; + page-break-inside: avoid; +} + .device-desktop .phui-workboard-view .aphront-multi-column-fixed .aphront-multi-column-inner { margin-left: 0; From d9bd062bba7227718a0c402157ff335f564090ae Mon Sep 17 00:00:00 2001 From: epriestley Date: Sat, 6 Feb 2016 21:19:10 -0800 Subject: [PATCH 10/65] Fix an issue with viewing an empty board with milestone columns Summary: Ref T10010. - Viewing an empty board with milestone columns did a meaningless edge query. Don't do that. - When creating the first milestone of a parent, force the indexing engine to rematerialize it inline. This sets `hasMilestones` properly. Otherwise, the daemons may take some time to fix this in the indexer. Test Plan: - Viewed an empty board of a project with a milestone. - Viewed a normal board. - Created the first milestone of a project with a big queue of daemons, saw project state immediately fully reflect the project having milestones. Reviewers: chad Reviewed By: chad Maniphest Tasks: T10010 Differential Revision: https://secure.phabricator.com/D15206 --- .../project/editor/PhabricatorProjectTransactionEditor.php | 6 ++++++ .../project/engine/PhabricatorBoardLayoutEngine.php | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/applications/project/editor/PhabricatorProjectTransactionEditor.php b/src/applications/project/editor/PhabricatorProjectTransactionEditor.php index 867bf7e9fe..20eb2c42bf 100644 --- a/src/applications/project/editor/PhabricatorProjectTransactionEditor.php +++ b/src/applications/project/editor/PhabricatorProjectTransactionEditor.php @@ -631,6 +631,7 @@ final class PhabricatorProjectTransactionEditor } break; case PhabricatorProjectTransaction::TYPE_PARENT: + case PhabricatorProjectTransaction::TYPE_MILESTONE: $materialize = true; $new_parent = $object->getParentProject(); break; @@ -669,6 +670,11 @@ final class PhabricatorProjectTransactionEditor ->rematerialize($object); } + if ($new_parent) { + id(new PhabricatorProjectsMembershipIndexEngineExtension()) + ->rematerialize($new_parent); + } + return parent::applyFinalEffects($object, $xactions); } diff --git a/src/applications/project/engine/PhabricatorBoardLayoutEngine.php b/src/applications/project/engine/PhabricatorBoardLayoutEngine.php index 4415e2fdf9..cfd752abb3 100644 --- a/src/applications/project/engine/PhabricatorBoardLayoutEngine.php +++ b/src/applications/project/engine/PhabricatorBoardLayoutEngine.php @@ -435,7 +435,7 @@ final class PhabricatorBoardLayoutEngine extends Phobject { // If we have proxies, we need to force cards into the correct proxy // columns. - if ($proxy_map) { + if ($proxy_map && $object_phids) { $edge_query = id(new PhabricatorEdgeQuery()) ->withSourcePHIDs($object_phids) ->withEdgeTypes( From 604b780953497567d6349ba0ba377670606ebe61 Mon Sep 17 00:00:00 2001 From: Jeremy Cowgar Date: Sun, 7 Feb 2016 10:00:14 -0800 Subject: [PATCH 11/65] Fix an issue where 'Attending' would appear on calendar view unnecessarily Summary: Ref T10295 * Viewing Upcoming Events in the calendar would display 'Attending: ' even if there were not attendees. This caused confusion, such as 'Is it telling me I am "Attending?"' * When a calendar event has no attendees, simply do not display the 'Attending: ' label Test Plan: * Add a new event with no one attending. * Add a new event with one or more attendees. * View the Upcoming Events query of the Calendar app. * Notice how the one with no attendees does not show 'Attending: ' while the other with attendees will show the already existing 'Attending: jdoe, ssmith' label. Reviewers: #blessed_reviewers, epriestley Reviewed By: #blessed_reviewers, epriestley Subscribers: Korvin, epriestley Maniphest Tasks: T10295 Differential Revision: https://secure.phabricator.com/D15207 --- .../PhabricatorCalendarEventSearchEngine.php | 35 ++++++++++--------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/src/applications/calendar/query/PhabricatorCalendarEventSearchEngine.php b/src/applications/calendar/query/PhabricatorCalendarEventSearchEngine.php index 8614e68f56..c23996470d 100644 --- a/src/applications/calendar/query/PhabricatorCalendarEventSearchEngine.php +++ b/src/applications/calendar/query/PhabricatorCalendarEventSearchEngine.php @@ -266,7 +266,6 @@ final class PhabricatorCalendarEventSearchEngine $list = new PHUIObjectItemListView(); foreach ($events as $event) { - $duration = ''; $event_date_info = $this->getEventDateLabel($event); $creator_handle = $handles[$event->getUserPHID()]; $attendees = array(); @@ -275,18 +274,6 @@ final class PhabricatorCalendarEventSearchEngine $attendees[] = $invitee->getInviteePHID(); } - $attendees = pht( - 'Attending: %s', - $viewer->renderHandleList($attendees) - ->setAsInline(1) - ->render()); - - if (strlen($event->getDuration()) > 0) { - $duration = pht( - 'Duration: %s', - $event->getDuration()); - } - if ($event->getIsGhostEvent()) { $title_text = $event->getMonogram() .' (' @@ -302,9 +289,25 @@ final class PhabricatorCalendarEventSearchEngine ->setObject($event) ->setHeader($title_text) ->setHref($event->getURI()) - ->addAttribute($event_date_info) - ->addAttribute($attendees) - ->addIcon('none', $duration); + ->addAttribute($event_date_info); + + if ($attendees) { + $attending = pht( + 'Attending: %s', + $viewer->renderHandleList($attendees) + ->setAsInline(1) + ->render()); + + $item->addAttribute($attending); + } + + if (strlen($event->getDuration()) > 0) { + $duration = pht( + 'Duration: %s', + $event->getDuration()); + + $item->addIcon('none', $duration); + } $list->addItem($item); } From b5518d4bfb0da4101ab3c65b7a29611dbc52badf Mon Sep 17 00:00:00 2001 From: Vlad Albulescu Date: Sun, 7 Feb 2016 16:29:13 -0800 Subject: [PATCH 12/65] fix a typo in troubleshooting perf wiki page Summary: Fix typo. That said, I love the example. Test Plan: no Reviewers: epriestley, #blessed_reviewers Reviewed By: epriestley, #blessed_reviewers Subscribers: Korvin Differential Revision: https://secure.phabricator.com/D15210 --- src/docs/user/field/performance.diviner | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/docs/user/field/performance.diviner b/src/docs/user/field/performance.diviner index 4aa959b228..2272af582c 100644 --- a/src/docs/user/field/performance.diviner +++ b/src/docs/user/field/performance.diviner @@ -52,7 +52,7 @@ inherent complexity, like these: - {icon times, color=red} A 100MB wiki page takes a long time to render. - {icon times, color=red} A turing-complete simulation of Conway's Game of - Life implented in 958,000 Herald rules executes slowly. + Life implemented in 958,000 Herald rules executes slowly. - {icon times, color=red} Uploading an 8GB file takes several minutes. Generally, the path forward will be: From 4dd6a1224d367ceb773af0e93c467dbf3f9544b9 Mon Sep 17 00:00:00 2001 From: epriestley Date: Sun, 7 Feb 2016 15:02:00 -0800 Subject: [PATCH 13/65] Make waving cover files around on boards more reliable Summary: Currently, in Safari, if you drag an image onto a board to make it a cover file and then wave it around wildly a lot over differnent cards, it sometimes glitches out a bit and won't drop on them properly. This appears to be because sequencing and delivery of dragenter/dragleave events isn't always totally ideal. Instead, just cancel any existing drag when we get a new drag that targets a new drop target. Test Plan: - Opened a board with a bunch of cards. - Dragged a file from my desktop onto the board. - Waved it around wildly, hovering over many different cards. - Before patch: sometimes cards under the cursor stopped highlighting properly. - After patch: behavior seems correct and consistent. Reviewers: chad Reviewed By: chad Differential Revision: https://secure.phabricator.com/D15208 --- resources/celerity/map.php | 22 +++++++++---------- webroot/rsrc/js/core/DragAndDropFileUpload.js | 13 +++++++++-- 2 files changed, 22 insertions(+), 13 deletions(-) diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 6de64110fb..eb82ce0670 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -11,7 +11,7 @@ return array( 'core.pkg.js' => '808ae845', 'darkconsole.pkg.js' => 'e7393ebb', 'differential.pkg.css' => '2de124c9', - 'differential.pkg.js' => '6b42b4bc', + 'differential.pkg.js' => 'd0cd0df6', 'diffusion.pkg.css' => 'f45955ed', 'diffusion.pkg.js' => '3a9a8bfa', 'maniphest.pkg.css' => '4845691a', @@ -446,7 +446,7 @@ return array( 'rsrc/js/application/uiexample/gesture-example.js' => '558829c2', 'rsrc/js/application/uiexample/notification-example.js' => '8ce821c5', 'rsrc/js/core/Busy.js' => '59a7976a', - 'rsrc/js/core/DragAndDropFileUpload.js' => 'da044194', + 'rsrc/js/core/DragAndDropFileUpload.js' => '81f182b5', 'rsrc/js/core/DraggableList.js' => '8905523d', 'rsrc/js/core/FileUpload.js' => '680ea2c8', 'rsrc/js/core/Hovercard.js' => '1bd28176', @@ -741,7 +741,7 @@ return array( 'phabricator-core-css' => '5b3563c8', 'phabricator-countdown-css' => 'e7544472', 'phabricator-dashboard-css' => 'eb458607', - 'phabricator-drag-and-drop-file-upload' => 'da044194', + 'phabricator-drag-and-drop-file-upload' => '81f182b5', 'phabricator-draggable-list' => '8905523d', 'phabricator-fatal-config-template-css' => '8e6c6fcd', 'phabricator-feed-css' => 'ecd4ec57', @@ -1451,6 +1451,14 @@ return array( 'javelin-vector', 'javelin-stratcom', ), + '81f182b5' => array( + 'javelin-install', + 'javelin-util', + 'javelin-request', + 'javelin-dom', + 'javelin-uri', + 'phabricator-file-upload', + ), '834a1173' => array( 'javelin-behavior', 'javelin-scrollbar', @@ -1890,14 +1898,6 @@ return array( 'javelin-util', 'phabricator-shaped-request', ), - 'da044194' => array( - 'javelin-install', - 'javelin-util', - 'javelin-request', - 'javelin-dom', - 'javelin-uri', - 'phabricator-file-upload', - ), 'dbbf48b6' => array( 'javelin-behavior', 'javelin-stratcom', diff --git a/webroot/rsrc/js/core/DragAndDropFileUpload.js b/webroot/rsrc/js/core/DragAndDropFileUpload.js index c3c9801a17..08cda15798 100644 --- a/webroot/rsrc/js/core/DragAndDropFileUpload.js +++ b/webroot/rsrc/js/core/DragAndDropFileUpload.js @@ -107,13 +107,18 @@ JX.install('PhabricatorDragAndDropFileUpload', { return; } - if (!this._node && !this._depth) { - this._target = e.getNode(this._sigil); + if (!this._node) { + var target = e.getNode(this._sigil); + if (target !== this._target) { + this._updateDepth(-this._depth); + this._target = target; + } } if (contains(this._getTarget(), e.getTarget())) { this._updateDepth(1); } + }); var on_dragleave = JX.bind(this, function(e) { @@ -121,6 +126,10 @@ JX.install('PhabricatorDragAndDropFileUpload', { return; } + if (!this._getTarget()) { + return; + } + if (contains(this._getTarget(), e.getTarget())) { this._updateDepth(-1); } From ca83eb1ca6013b6bf65fe5fa9d067034e8298fbb Mon Sep 17 00:00:00 2001 From: epriestley Date: Sun, 7 Feb 2016 15:52:26 -0800 Subject: [PATCH 14/65] Make workboard columns fixed-height and internally scrollable Summary: Ref T5240. - Columns are fixed height. - Columns scroll internally. - Drag behaviors generally align with these column behaviors. Test Plan: {F1099061} Reviewers: chad Reviewed By: chad Maniphest Tasks: T5240 Differential Revision: https://secure.phabricator.com/D15209 --- resources/celerity/map.php | 26 +++---- src/view/phui/PHUIWorkboardView.php | 11 +-- .../css/phui/workboards/phui-workboard.css | 7 ++ webroot/rsrc/js/core/DraggableList.js | 71 ++++++++++++++----- 4 files changed, 80 insertions(+), 35 deletions(-) diff --git a/resources/celerity/map.php b/resources/celerity/map.php index eb82ce0670..e5a3d3a5ad 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -8,7 +8,7 @@ return array( 'names' => array( 'core.pkg.css' => 'b4a7e275', - 'core.pkg.js' => '808ae845', + 'core.pkg.js' => '771b0e84', 'darkconsole.pkg.js' => 'e7393ebb', 'differential.pkg.css' => '2de124c9', 'differential.pkg.js' => 'd0cd0df6', @@ -154,7 +154,7 @@ return array( 'rsrc/css/phui/phui-tag-view.css' => '9d5d4400', 'rsrc/css/phui/phui-timeline-view.css' => '2efceff8', 'rsrc/css/phui/phui-two-column-view.css' => 'c75bfc5b', - 'rsrc/css/phui/workboards/phui-workboard.css' => '2a04b1a8', + 'rsrc/css/phui/workboards/phui-workboard.css' => 'f526057c', 'rsrc/css/phui/workboards/phui-workcard.css' => 'a869098a', 'rsrc/css/phui/workboards/phui-workpanel.css' => 'e1bd8d04', 'rsrc/css/sprite-login.css' => '60e8560e', @@ -447,7 +447,7 @@ return array( 'rsrc/js/application/uiexample/notification-example.js' => '8ce821c5', 'rsrc/js/core/Busy.js' => '59a7976a', 'rsrc/js/core/DragAndDropFileUpload.js' => '81f182b5', - 'rsrc/js/core/DraggableList.js' => '8905523d', + 'rsrc/js/core/DraggableList.js' => '8199fb41', 'rsrc/js/core/FileUpload.js' => '680ea2c8', 'rsrc/js/core/Hovercard.js' => '1bd28176', 'rsrc/js/core/KeyboardShortcut.js' => '1ae869f2', @@ -742,7 +742,7 @@ return array( 'phabricator-countdown-css' => 'e7544472', 'phabricator-dashboard-css' => 'eb458607', 'phabricator-drag-and-drop-file-upload' => '81f182b5', - 'phabricator-draggable-list' => '8905523d', + 'phabricator-draggable-list' => '8199fb41', 'phabricator-fatal-config-template-css' => '8e6c6fcd', 'phabricator-feed-css' => 'ecd4ec57', 'phabricator-file-upload' => '680ea2c8', @@ -831,7 +831,7 @@ return array( 'phui-theme-css' => 'ab7b848c', 'phui-timeline-view-css' => '2efceff8', 'phui-two-column-view-css' => 'c75bfc5b', - 'phui-workboard-view-css' => '2a04b1a8', + 'phui-workboard-view-css' => 'f526057c', 'phui-workcard-view-css' => 'a869098a', 'phui-workpanel-view-css' => 'e1bd8d04', 'phuix-action-list-view' => 'b5c256b8', @@ -1451,6 +1451,14 @@ return array( 'javelin-vector', 'javelin-stratcom', ), + '8199fb41' => array( + 'javelin-install', + 'javelin-dom', + 'javelin-stratcom', + 'javelin-util', + 'javelin-vector', + 'javelin-magical-init', + ), '81f182b5' => array( 'javelin-install', 'javelin-util', @@ -1496,14 +1504,6 @@ return array( 'javelin-stratcom', 'javelin-dom', ), - '8905523d' => array( - 'javelin-install', - 'javelin-dom', - 'javelin-stratcom', - 'javelin-util', - 'javelin-vector', - 'javelin-magical-init', - ), '8a41885b' => array( 'javelin-install', 'javelin-dom', diff --git a/src/view/phui/PHUIWorkboardView.php b/src/view/phui/PHUIWorkboardView.php index 45061f25fe..0483f61752 100644 --- a/src/view/phui/PHUIWorkboardView.php +++ b/src/view/phui/PHUIWorkboardView.php @@ -25,12 +25,13 @@ final class PHUIWorkboardView extends AphrontTagView { $view->addColumn($panel); } - $board = phutil_tag( + $board = javelin_tag( 'div', - array( - 'class' => 'phui-workboard-view-shadow', - ), - $view); + array( + 'class' => 'phui-workboard-view-shadow', + 'sigil' => 'lock-scroll-while-dragging', + ), + $view); return $board; } diff --git a/webroot/rsrc/css/phui/workboards/phui-workboard.css b/webroot/rsrc/css/phui/workboards/phui-workboard.css index 81139410dd..4c197f462c 100644 --- a/webroot/rsrc/css/phui/workboards/phui-workboard.css +++ b/webroot/rsrc/css/phui/workboards/phui-workboard.css @@ -12,6 +12,7 @@ .device-desktop .phui-workboard-view-shadow { overflow-x: auto; + overflow-y: hidden; position: absolute; top: 79px; bottom: 0; @@ -70,3 +71,9 @@ .device .project-board-wrapper { margin: 16px; } + +.device-desktop .phui-workpanel-body { + max-height: calc(100vh - 170px); + overflow-y: scroll; + overflow-x: hidden; +} diff --git a/webroot/rsrc/js/core/DraggableList.js b/webroot/rsrc/js/core/DraggableList.js index efe137e7ab..41757e6b38 100644 --- a/webroot/rsrc/js/core/DraggableList.js +++ b/webroot/rsrc/js/core/DraggableList.js @@ -310,6 +310,10 @@ JX.install('DraggableList', { return target_list; }, + _getTarget: function() { + return this._target; + }, + _setTarget : function(cur_target) { var ghost = this.getGhostNode(); var target = this._target; @@ -433,8 +437,6 @@ JX.install('DraggableList', { this._cursorOrigin.y - (this._cursorScroll.y - s.y)); } - this._updateAutoscroll(this._cursorPosition); - var p = JX.$V(this._cursorPosition.x, this._cursorPosition.y); var group = this._group; @@ -459,6 +461,8 @@ JX.install('DraggableList', { } } + this._updateAutoscroll(this._cursorPosition); + var f = JX.$V(this._frame); p.x -= f.x; p.y -= f.y; @@ -475,7 +479,7 @@ JX.install('DraggableList', { }, _updateAutoscroll: function(p) { - var container = this._dragging.parentNode; + var container = this._getScrollAnchor().parentNode; var autoscroll = {}; var outer = this.getOuterContainer(); @@ -595,6 +599,22 @@ JX.install('DraggableList', { this.invoke('didEndDrag', dragging); }, + _getScrollAnchor: function() { + // If you drag an item from column "A" into column "B", then move the + // mouse to the top or bottom of the screen, we need to scroll the target + // column (column "B"), not the original column. + + var group = this._group; + for (var ii = 0; ii < group.length; ii++) { + var target = group[ii]._getTarget(); + if (target) { + return group[ii]._ghostNode; + } + } + + return this._dragging; + }, + _onautoscroll: function() { var u = this._autoscroll.up; var d = this._autoscroll.down; @@ -613,20 +633,22 @@ JX.install('DraggableList', { var amount = 12 * (delta / 10); + var anchor = this._getScrollAnchor(); + if (u && (u != d)) { - this._tryScroll(this._dragging, u, 'scrollTop', amount); + this._tryScroll(anchor, u, 'scrollTop', amount); } if (d && (d != u)) { - this._tryScroll(this._dragging, d, 'scrollTop', -amount); + this._tryScroll(anchor, d, 'scrollTop', -amount); } if (l && (l != r)) { - this._tryScroll(this._dragging, l, 'scrollLeft', amount); + this._tryScroll(anchor, l, 'scrollLeft', amount); } if (r && (r != l)) { - this._tryScroll(this._dragging, r, 'scrollLeft', -amount); + this._tryScroll(anchor, r, 'scrollLeft', -amount); } }, @@ -639,19 +661,34 @@ JX.install('DraggableList', { var container = from.parentNode; while (container) { - // Read the current scroll value. - value = container[property]; - // Try to scroll. - container[property] -= amount; - - // If we scrolled it, we're all done. - if (container[property] != value) { - break; + // In Safari, we'll eventually reach `window.document`, which is not + // sufficently node-like to support sigil tests. + var lock; + if (container === window.document) { + lock = false; + } else { + // Some elements may respond to, e.g., `scrollTop` adjustment, even + // though they are not scrollable. This sigil disables adjustment + // for them. + lock = JX.Stratcom.hasSigil(container, 'lock-scroll-while-dragging'); } - if (container == to) { - break; + if (!lock) { + // Read the current scroll value. + value = container[property]; + + // Try to scroll. + container[property] -= amount; + + // If we scrolled it, we're all done. + if (container[property] != value) { + break; + } + + if (container == to) { + break; + } } container = container.parentNode; From 39dc2c038dd1f3f475ccea7d55ba09752845ed2b Mon Sep 17 00:00:00 2001 From: epriestley Date: Sun, 7 Feb 2016 16:08:09 -0800 Subject: [PATCH 15/65] Allow workboards to be panned horizontally by dragging the background Summary: Ref T5240. For boards with a lot of columns and users without "shift + mousewheel" or a touchpad, allow click-drag on the board background to pan the board horizontally. The `ew-resize` cursor cue might be a little too intense. If it's annoying, we could drop it and just leave this as a secret feature to discover. Test Plan: Panned the board horizontally. Reviewers: chad Reviewed By: chad Maniphest Tasks: T5240 Differential Revision: https://secure.phabricator.com/D15211 --- resources/celerity/map.php | 28 ++++++------- src/view/phui/PHUIWorkboardView.php | 2 +- .../css/phui/workboards/phui-workboard.css | 9 ++++ .../projects/behavior-project-boards.js | 41 +++++++++++++++++++ 4 files changed, 65 insertions(+), 15 deletions(-) diff --git a/resources/celerity/map.php b/resources/celerity/map.php index e5a3d3a5ad..5d684cf0b3 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -154,7 +154,7 @@ return array( 'rsrc/css/phui/phui-tag-view.css' => '9d5d4400', 'rsrc/css/phui/phui-timeline-view.css' => '2efceff8', 'rsrc/css/phui/phui-two-column-view.css' => 'c75bfc5b', - 'rsrc/css/phui/workboards/phui-workboard.css' => 'f526057c', + 'rsrc/css/phui/workboards/phui-workboard.css' => '2b5367d2', 'rsrc/css/phui/workboards/phui-workcard.css' => 'a869098a', 'rsrc/css/phui/workboards/phui-workpanel.css' => 'e1bd8d04', 'rsrc/css/sprite-login.css' => '60e8560e', @@ -414,7 +414,7 @@ return array( 'rsrc/js/application/phortune/phortune-credit-card-form.js' => '2290aeef', 'rsrc/js/application/policy/behavior-policy-control.js' => 'd0c516d5', 'rsrc/js/application/policy/behavior-policy-rule-editor.js' => '5e9f347c', - 'rsrc/js/application/projects/behavior-project-boards.js' => '5191522f', + 'rsrc/js/application/projects/behavior-project-boards.js' => 'a1807fd7', 'rsrc/js/application/projects/behavior-project-create.js' => '065227cc', 'rsrc/js/application/projects/behavior-reorder-columns.js' => 'e1d25dfb', 'rsrc/js/application/releeph/releeph-preview-branch.js' => 'b2b4fbaf', @@ -654,7 +654,7 @@ return array( 'javelin-behavior-phui-profile-menu' => '12884df9', 'javelin-behavior-policy-control' => 'd0c516d5', 'javelin-behavior-policy-rule-editor' => '5e9f347c', - 'javelin-behavior-project-boards' => '5191522f', + 'javelin-behavior-project-boards' => 'a1807fd7', 'javelin-behavior-project-create' => '065227cc', 'javelin-behavior-quicksand-blacklist' => '7927a7d3', 'javelin-behavior-recurring-edit' => '5f1c4d5f', @@ -831,7 +831,7 @@ return array( 'phui-theme-css' => 'ab7b848c', 'phui-timeline-view-css' => '2efceff8', 'phui-two-column-view-css' => 'c75bfc5b', - 'phui-workboard-view-css' => 'f526057c', + 'phui-workboard-view-css' => '2b5367d2', 'phui-workcard-view-css' => 'a869098a', 'phui-workpanel-view-css' => 'e1bd8d04', 'phuix-action-list-view' => 'b5c256b8', @@ -1190,16 +1190,6 @@ return array( 'javelin-typeahead-source', 'javelin-util', ), - '5191522f' => array( - 'javelin-behavior', - 'javelin-dom', - 'javelin-util', - 'javelin-vector', - 'javelin-stratcom', - 'javelin-workflow', - 'phabricator-draggable-list', - 'phabricator-drag-and-drop-file-upload', - ), '519705ea' => array( 'javelin-install', 'javelin-dom', @@ -1616,6 +1606,16 @@ return array( 'javelin-dom', 'javelin-reactor-dom', ), + 'a1807fd7' => array( + 'javelin-behavior', + 'javelin-dom', + 'javelin-util', + 'javelin-vector', + 'javelin-stratcom', + 'javelin-workflow', + 'phabricator-draggable-list', + 'phabricator-drag-and-drop-file-upload', + ), 'a2828756' => array( 'javelin-dom', 'javelin-util', diff --git a/src/view/phui/PHUIWorkboardView.php b/src/view/phui/PHUIWorkboardView.php index 0483f61752..cc2b705c15 100644 --- a/src/view/phui/PHUIWorkboardView.php +++ b/src/view/phui/PHUIWorkboardView.php @@ -29,7 +29,7 @@ final class PHUIWorkboardView extends AphrontTagView { 'div', array( 'class' => 'phui-workboard-view-shadow', - 'sigil' => 'lock-scroll-while-dragging', + 'sigil' => 'workboard-shadow lock-scroll-while-dragging', ), $view); diff --git a/webroot/rsrc/css/phui/workboards/phui-workboard.css b/webroot/rsrc/css/phui/workboards/phui-workboard.css index 4c197f462c..22d5514b43 100644 --- a/webroot/rsrc/css/phui/workboards/phui-workboard.css +++ b/webroot/rsrc/css/phui/workboards/phui-workboard.css @@ -77,3 +77,12 @@ overflow-y: scroll; overflow-x: hidden; } + +.device-desktop .phui-workboard-view .aphront-multi-column-view { + pointer-events: none; +} + +.device-desktop .phui-workpanel-view { + pointer-events: auto; + cursor: auto; +} diff --git a/webroot/rsrc/js/application/projects/behavior-project-boards.js b/webroot/rsrc/js/application/projects/behavior-project-boards.js index 6ae321df8a..86230f3b6a 100644 --- a/webroot/rsrc/js/application/projects/behavior-project-boards.js +++ b/webroot/rsrc/js/application/projects/behavior-project-boards.js @@ -382,6 +382,47 @@ JX.behavior('project-boards', function(config, statics) { drop.start(); } + // When the user drags the workboard background, pan the workboard + // horizontally. This allows you to scroll across cards with only the + // mouse, without shift + scrollwheel or using the scrollbar. + + var pan_origin = null; + var pan_node = null; + var pan_x = null; + + JX.Stratcom.listen('mousedown', 'workboard-shadow', function(e) { + if (!JX.Device.isDesktop()) { + return; + } + + if (e.getNode('workpanel')) { + return; + } + + if (JX.Stratcom.pass()) { + return; + } + + e.kill(); + + pan_origin = JX.$V(e); + pan_node = e.getNode('workboard-shadow'); + pan_x = pan_node.scrollLeft; + }); + + JX.Stratcom.listen('mousemove', null, function(e) { + if (!pan_origin) { + return; + } + + var cursor = JX.$V(e); + pan_node.scrollLeft = pan_x + (pan_origin.x - cursor.x); + }); + + JX.Stratcom.listen('mouseup', null, function() { + pan_origin = null; + }); + return true; } From d78061d820eb22d587686c437136d34815caa8f0 Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 8 Feb 2016 10:03:09 -0800 Subject: [PATCH 16/65] Only prevent drag-scroll in the Y direction on workboards Summary: This scroll lock thing prevented both X and Y scrolling, but should only prevent Y scrolling. Dragging a card to the edge to scroll left/right is fine. Test Plan: Scrolled a workboard left/right by dragging a card to the edge. Reviewers: chad Reviewed By: chad Differential Revision: https://secure.phabricator.com/D15213 --- resources/celerity/map.php | 22 +++++++++++----------- src/view/phui/PHUIWorkboardView.php | 2 +- webroot/rsrc/js/core/DraggableList.js | 11 +++++++++-- 3 files changed, 21 insertions(+), 14 deletions(-) diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 5d684cf0b3..9905d9c3d2 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -8,7 +8,7 @@ return array( 'names' => array( 'core.pkg.css' => 'b4a7e275', - 'core.pkg.js' => '771b0e84', + 'core.pkg.js' => '17380dd3', 'darkconsole.pkg.js' => 'e7393ebb', 'differential.pkg.css' => '2de124c9', 'differential.pkg.js' => 'd0cd0df6', @@ -447,7 +447,7 @@ return array( 'rsrc/js/application/uiexample/notification-example.js' => '8ce821c5', 'rsrc/js/core/Busy.js' => '59a7976a', 'rsrc/js/core/DragAndDropFileUpload.js' => '81f182b5', - 'rsrc/js/core/DraggableList.js' => '8199fb41', + 'rsrc/js/core/DraggableList.js' => '705df8d1', 'rsrc/js/core/FileUpload.js' => '680ea2c8', 'rsrc/js/core/Hovercard.js' => '1bd28176', 'rsrc/js/core/KeyboardShortcut.js' => '1ae869f2', @@ -742,7 +742,7 @@ return array( 'phabricator-countdown-css' => 'e7544472', 'phabricator-dashboard-css' => 'eb458607', 'phabricator-drag-and-drop-file-upload' => '81f182b5', - 'phabricator-draggable-list' => '8199fb41', + 'phabricator-draggable-list' => '705df8d1', 'phabricator-fatal-config-template-css' => '8e6c6fcd', 'phabricator-feed-css' => 'ecd4ec57', 'phabricator-file-upload' => '680ea2c8', @@ -1352,6 +1352,14 @@ return array( 'javelin-typeahead', 'javelin-uri', ), + '705df8d1' => array( + 'javelin-install', + 'javelin-dom', + 'javelin-stratcom', + 'javelin-util', + 'javelin-vector', + 'javelin-magical-init', + ), '70baed2f' => array( 'javelin-install', 'javelin-dom', @@ -1441,14 +1449,6 @@ return array( 'javelin-vector', 'javelin-stratcom', ), - '8199fb41' => array( - 'javelin-install', - 'javelin-dom', - 'javelin-stratcom', - 'javelin-util', - 'javelin-vector', - 'javelin-magical-init', - ), '81f182b5' => array( 'javelin-install', 'javelin-util', diff --git a/src/view/phui/PHUIWorkboardView.php b/src/view/phui/PHUIWorkboardView.php index cc2b705c15..e9ec37452a 100644 --- a/src/view/phui/PHUIWorkboardView.php +++ b/src/view/phui/PHUIWorkboardView.php @@ -29,7 +29,7 @@ final class PHUIWorkboardView extends AphrontTagView { 'div', array( 'class' => 'phui-workboard-view-shadow', - 'sigil' => 'workboard-shadow lock-scroll-while-dragging', + 'sigil' => 'workboard-shadow lock-scroll-y-while-dragging', ), $view); diff --git a/webroot/rsrc/js/core/DraggableList.js b/webroot/rsrc/js/core/DraggableList.js index 41757e6b38..d808784807 100644 --- a/webroot/rsrc/js/core/DraggableList.js +++ b/webroot/rsrc/js/core/DraggableList.js @@ -664,14 +664,21 @@ JX.install('DraggableList', { // In Safari, we'll eventually reach `window.document`, which is not // sufficently node-like to support sigil tests. - var lock; + var lock = false; if (container === window.document) { lock = false; } else { // Some elements may respond to, e.g., `scrollTop` adjustment, even // though they are not scrollable. This sigil disables adjustment // for them. - lock = JX.Stratcom.hasSigil(container, 'lock-scroll-while-dragging'); + var lock_sigil; + if (property == 'scrollTop') { + lock_sigil = 'lock-scroll-y-while-dragging'; + } + + if (lock_sigil) { + lock = JX.Stratcom.hasSigil(container, lock_sigil); + } } if (!lock) { From 07f1a03262d5ae3bac0b0a2bbe7f54d9167bba4c Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 8 Feb 2016 10:27:00 -0800 Subject: [PATCH 17/65] Fix a bad call when prefilling ApplicationSearch from `?projects=some_slug` Summary: Fixes T10299. Test Plan: - Visited `/maniphest/?projects=x` locally, where `x` is some valid project slug. - Before patch: Fatal on `requireViewer()` call. - After patch: Works correctly, filling the correct project into parameters. Reviewers: chad Reviewed By: chad Maniphest Tasks: T10299 Differential Revision: https://secure.phabricator.com/D15214 --- .../project/searchfield/PhabricatorProjectSearchField.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/applications/project/searchfield/PhabricatorProjectSearchField.php b/src/applications/project/searchfield/PhabricatorProjectSearchField.php index f187444e5d..8b81cb9d15 100644 --- a/src/applications/project/searchfield/PhabricatorProjectSearchField.php +++ b/src/applications/project/searchfield/PhabricatorProjectSearchField.php @@ -34,7 +34,7 @@ final class PhabricatorProjectSearchField if ($slugs) { $projects = id(new PhabricatorProjectQuery()) - ->setViewer($this->requireViewer()) + ->setViewer($this->getViewer()) ->withSlugs($slugs) ->execute(); foreach ($projects as $project) { From 9c95b387bd1e2e32d14f638b48d9a44c49c111d8 Mon Sep 17 00:00:00 2001 From: Chad Little Date: Mon, 8 Feb 2016 13:42:40 -0800 Subject: [PATCH 18/65] Tidy up Workboard CSS a little Summary: - Custom scrollbars, colors - New div with some better padding (floor for the column) - More consistent spacing around the board itself. - Slightly darker columns - Smaller horizonal scrollbar Test Plan: Chrome Mac / Desktop. {F1100342} Reviewers: epriestley Reviewed By: epriestley Subscribers: Korvin Differential Revision: https://secure.phabricator.com/D15217 --- resources/celerity/map.php | 18 +++++------ src/view/phui/PHUIWorkpanelView.php | 4 ++- .../css/phui/workboards/phui-workboard.css | 10 ++---- .../css/phui/workboards/phui-workcard.css | 4 +++ .../css/phui/workboards/phui-workpanel.css | 31 +++++++++++++++++-- 5 files changed, 46 insertions(+), 21 deletions(-) diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 9905d9c3d2..5de230fe1c 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -154,9 +154,9 @@ return array( 'rsrc/css/phui/phui-tag-view.css' => '9d5d4400', 'rsrc/css/phui/phui-timeline-view.css' => '2efceff8', 'rsrc/css/phui/phui-two-column-view.css' => 'c75bfc5b', - 'rsrc/css/phui/workboards/phui-workboard.css' => '2b5367d2', - 'rsrc/css/phui/workboards/phui-workcard.css' => 'a869098a', - 'rsrc/css/phui/workboards/phui-workpanel.css' => 'e1bd8d04', + 'rsrc/css/phui/workboards/phui-workboard.css' => '0047084f', + 'rsrc/css/phui/workboards/phui-workcard.css' => '3646fb96', + 'rsrc/css/phui/workboards/phui-workpanel.css' => 'a78c0661', 'rsrc/css/sprite-login.css' => '60e8560e', 'rsrc/css/sprite-menu.css' => '9dd65b92', 'rsrc/css/sprite-tokens.css' => '4f399012', @@ -831,9 +831,9 @@ return array( 'phui-theme-css' => 'ab7b848c', 'phui-timeline-view-css' => '2efceff8', 'phui-two-column-view-css' => 'c75bfc5b', - 'phui-workboard-view-css' => '2b5367d2', - 'phui-workcard-view-css' => 'a869098a', - 'phui-workpanel-view-css' => 'e1bd8d04', + 'phui-workboard-view-css' => '0047084f', + 'phui-workcard-view-css' => '3646fb96', + 'phui-workpanel-view-css' => 'a78c0661', 'phuix-action-list-view' => 'b5c256b8', 'phuix-action-view' => '8cf6d262', 'phuix-autocomplete' => '9196fb06', @@ -1631,6 +1631,9 @@ return array( 'javelin-uri', 'phabricator-notification', ), + 'a78c0661' => array( + 'phui-workcard-view-css', + ), 'a80d0378' => array( 'javelin-behavior', 'javelin-stratcom', @@ -1919,9 +1922,6 @@ return array( 'javelin-dom', 'phabricator-prefab', ), - 'e1bd8d04' => array( - 'phui-workcard-view-css', - ), 'e1d25dfb' => array( 'javelin-behavior', 'javelin-stratcom', diff --git a/src/view/phui/PHUIWorkpanelView.php b/src/view/phui/PHUIWorkpanelView.php index f672dc8e04..b94b423ced 100644 --- a/src/view/phui/PHUIWorkpanelView.php +++ b/src/view/phui/PHUIWorkpanelView.php @@ -103,10 +103,12 @@ final class PHUIWorkpanelView extends AphrontTagView { $body = phutil_tag( 'div', array( - 'class' => 'phui-workpanel-body', + 'class' => 'phui-workpanel-body-content', ), $this->cards); + $body = phutil_tag_div('phui-workpanel-body', $body); + $view = id(new PHUIBoxView()) ->setColor(PHUIBoxView::GREY) ->addClass('phui-workpanel-view-inner') diff --git a/webroot/rsrc/css/phui/workboards/phui-workboard.css b/webroot/rsrc/css/phui/workboards/phui-workboard.css index 22d5514b43..d2652cc45e 100644 --- a/webroot/rsrc/css/phui/workboards/phui-workboard.css +++ b/webroot/rsrc/css/phui/workboards/phui-workboard.css @@ -35,8 +35,8 @@ } .phui-workboard-view-shadow::-webkit-scrollbar { - height: 12px; - width: 12px; + height: 8px; + width: 8px; background: rgba(200,200,200,.6); } @@ -72,12 +72,6 @@ margin: 16px; } -.device-desktop .phui-workpanel-body { - max-height: calc(100vh - 170px); - overflow-y: scroll; - overflow-x: hidden; -} - .device-desktop .phui-workboard-view .aphront-multi-column-view { pointer-events: none; } diff --git a/webroot/rsrc/css/phui/workboards/phui-workcard.css b/webroot/rsrc/css/phui/workboards/phui-workcard.css index e8a6b44942..d35ac11628 100644 --- a/webroot/rsrc/css/phui/workboards/phui-workcard.css +++ b/webroot/rsrc/css/phui/workboards/phui-workcard.css @@ -110,6 +110,10 @@ background-color: {$sh-greenbackground}; } +.phui-object-item-list-view .phui-workcard:last-child { + margin-bottom: 0; +} + /* - Draggable Colors --------------------------------------------------------*/ diff --git a/webroot/rsrc/css/phui/workboards/phui-workpanel.css b/webroot/rsrc/css/phui/workboards/phui-workpanel.css index b600577d33..77bddf9f0f 100644 --- a/webroot/rsrc/css/phui/workboards/phui-workpanel.css +++ b/webroot/rsrc/css/phui/workboards/phui-workpanel.css @@ -35,8 +35,12 @@ user-select: none; } +.phui-workpanel-view .phui-box-grey { + background-color: rgba(71,87,120,0.1); +} + .phui-workpanel-view.phui-workboard-column-milestone .phui-box-grey { - background-color: rgba(234, 230, 247, 0.75); + background-color: rgba(234, 230, 247, 0.85); } .phui-workpanel-view .phui-header-col2 .phui-icon-view { @@ -46,11 +50,14 @@ .phui-workpanel-view .phui-workpanel-header-action { float: right; width: 24px; - border-left: 1px solid #b3b5b6; } .phui-workpanel-view .phui-workpanel-body { - padding: 8px 8px 4px 8px; + padding: 8px 4px 8px 0; +} + +.phui-workpanel-view .phui-workpanel-body-content { + padding: 0 4px 0 8px; } .device .phui-workpanel-view .phui-workpanel-body { @@ -89,6 +96,24 @@ opacity: 0.75; } +.device-desktop .phui-workpanel-body-content { + max-height: calc(100vh - 162px); + overflow-y: auto; + overflow-x: hidden; +} + +.device-desktop .phui-workpanel-body-content::-webkit-scrollbar { + height: 8px; + width: 8px; + background: rgba(71,87,120,0.2); + border-radius: 4px; +} + +.device-desktop .phui-workpanel-body-content::-webkit-scrollbar-thumb { + background: rgba(71,87,120,0.4); + border-radius: 4px; +} + .project-panel-empty .phui-object-item-list-view { background: {$sh-indigobackground}; border-radius: 3px; From 3682cc9bb2e86adebfce50a5c70d10088bdf4c50 Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 8 Feb 2016 11:24:32 -0800 Subject: [PATCH 19/65] Allow workboards to be disabled, hiding "(Backlog)" column annotations Summary: Fixes T7410. - Adds a "Disable Workboard" action to the "Manage Backlog" menu. - We'll probably move this somewhere else if/when that column gets too messy. - Disabling a board hides it, prevents it from being recreated by non-editors, and hides the "Project (Backlog)" annotations. - Resotring a board puts it back in pristine condition. Test Plan: - Disabled a board. - Verified "(Backlog)" annotations vanished. - Enabled a board. Reviewers: chad Reviewed By: chad Subscribers: mbishopim3 Maniphest Tasks: T7410 Differential Revision: https://secure.phabricator.com/D15215 --- src/__phutil_library_map__.php | 2 + .../PhabricatorProjectApplication.php | 2 + ...abricatorProjectBoardDisableController.php | 61 +++++++++++++ .../PhabricatorProjectBoardViewController.php | 89 +++++++++++++++++-- .../PhabricatorProjectTransactionEditor.php | 9 ++ .../engine/PhabricatorBoardLayoutEngine.php | 21 ++++- .../storage/PhabricatorProjectTransaction.php | 26 ++++++ 7 files changed, 201 insertions(+), 9 deletions(-) create mode 100644 src/applications/project/controller/PhabricatorProjectBoardDisableController.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 76105fdda8..accf2c05cb 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -2860,6 +2860,7 @@ phutil_register_library_map(array( 'PhabricatorProjectApplication' => 'applications/project/application/PhabricatorProjectApplication.php', 'PhabricatorProjectArchiveController' => 'applications/project/controller/PhabricatorProjectArchiveController.php', 'PhabricatorProjectBoardController' => 'applications/project/controller/PhabricatorProjectBoardController.php', + 'PhabricatorProjectBoardDisableController' => 'applications/project/controller/PhabricatorProjectBoardDisableController.php', 'PhabricatorProjectBoardImportController' => 'applications/project/controller/PhabricatorProjectBoardImportController.php', 'PhabricatorProjectBoardReorderController' => 'applications/project/controller/PhabricatorProjectBoardReorderController.php', 'PhabricatorProjectBoardViewController' => 'applications/project/controller/PhabricatorProjectBoardViewController.php', @@ -7274,6 +7275,7 @@ phutil_register_library_map(array( 'PhabricatorProjectApplication' => 'PhabricatorApplication', 'PhabricatorProjectArchiveController' => 'PhabricatorProjectController', 'PhabricatorProjectBoardController' => 'PhabricatorProjectController', + 'PhabricatorProjectBoardDisableController' => 'PhabricatorProjectBoardController', 'PhabricatorProjectBoardImportController' => 'PhabricatorProjectBoardController', 'PhabricatorProjectBoardReorderController' => 'PhabricatorProjectBoardController', 'PhabricatorProjectBoardViewController' => 'PhabricatorProjectBoardController', diff --git a/src/applications/project/application/PhabricatorProjectApplication.php b/src/applications/project/application/PhabricatorProjectApplication.php index bd321bbaa8..0f5f90a737 100644 --- a/src/applications/project/application/PhabricatorProjectApplication.php +++ b/src/applications/project/application/PhabricatorProjectApplication.php @@ -84,6 +84,8 @@ final class PhabricatorProjectApplication extends PhabricatorApplication { => 'PhabricatorProjectBoardImportController', 'reorder/' => 'PhabricatorProjectBoardReorderController', + 'disable/' + => 'PhabricatorProjectBoardDisableController', ), 'update/(?P[1-9]\d*)/(?P[^/]+)/' => 'PhabricatorProjectUpdateController', diff --git a/src/applications/project/controller/PhabricatorProjectBoardDisableController.php b/src/applications/project/controller/PhabricatorProjectBoardDisableController.php new file mode 100644 index 0000000000..0440ff9eff --- /dev/null +++ b/src/applications/project/controller/PhabricatorProjectBoardDisableController.php @@ -0,0 +1,61 @@ +getUser(); + $project_id = $request->getURIData('projectID'); + + $project = id(new PhabricatorProjectQuery()) + ->setViewer($viewer) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->withIDs(array($project_id)) + ->executeOne(); + if (!$project) { + return new Aphront404Response(); + } + + if (!$project->getHasWorkboard()) { + return new Aphront404Response(); + } + + $this->setProject($project); + $id = $project->getID(); + + $board_uri = $this->getApplicationURI("board/{$id}/"); + + if ($request->isFormPost()) { + $xactions = array(); + + $xactions[] = id(new PhabricatorProjectTransaction()) + ->setTransactionType(PhabricatorProjectTransaction::TYPE_HASWORKBOARD) + ->setNewValue(0); + + id(new PhabricatorProjectTransactionEditor()) + ->setActor($viewer) + ->setContentSourceFromRequest($request) + ->setContinueOnNoEffect(true) + ->setContinueOnMissingFields(true) + ->applyTransactions($project, $xactions); + + return id(new AphrontRedirectResponse()) + ->setURI($board_uri); + } + + return $this->newDialog() + ->setTitle(pht('Disable Workboard')) + ->appendParagraph( + pht( + 'Disabling a workboard hides the board. Objects on the board '. + 'will no longer be annotated with column names in other '. + 'applications. You can restore the workboard later.')) + ->addCancelButton($board_uri) + ->addSubmitButton(pht('Disable Workboard')); + } + +} diff --git a/src/applications/project/controller/PhabricatorProjectBoardViewController.php b/src/applications/project/controller/PhabricatorProjectBoardViewController.php index a10cdc1696..ffedb6b17f 100644 --- a/src/applications/project/controller/PhabricatorProjectBoardViewController.php +++ b/src/applications/project/controller/PhabricatorProjectBoardViewController.php @@ -121,18 +121,27 @@ final class PhabricatorProjectBoardViewController ->setViewer($viewer) ->setBoardPHIDs(array($board_phid)) ->setObjectPHIDs(array_keys($tasks)) + ->setFetchAllBoards(true) ->executeLayout(); $columns = $layout_engine->getColumns($board_phid); - if (!$columns) { + if (!$columns || !$project->getHasWorkboard()) { $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $project, PhabricatorPolicyCapability::CAN_EDIT); - if (!$can_edit) { - $content = $this->buildNoAccessContent($project); + if (!$columns) { + if (!$can_edit) { + $content = $this->buildNoAccessContent($project); + } else { + $content = $this->buildInitializeContent($project); + } } else { - $content = $this->buildInitializeContent($project); + if (!$can_edit) { + $content = $this->buildDisabledContent($project); + } else { + $content = $this->buildEnableContent($project); + } } if ($content instanceof AphrontResponse) { @@ -544,6 +553,12 @@ final class PhabricatorProjectBoardViewController $request = $this->getRequest(); $viewer = $request->getUser(); + $id = $project->getID(); + + $disable_uri = $this->getApplicationURI("board/{$id}/disable/"); + $add_uri = $this->getApplicationURI("board/{$id}/edit/"); + $reorder_uri = $this->getApplicationURI("board/{$id}/reorder/"); + $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $project, @@ -554,14 +569,14 @@ final class PhabricatorProjectBoardViewController $manage_items[] = id(new PhabricatorActionView()) ->setIcon('fa-plus') ->setName(pht('Add Column')) - ->setHref($this->getApplicationURI('board/'.$this->id.'/edit/')) + ->setHref($add_uri) ->setDisabled(!$can_edit) ->setWorkflow(!$can_edit); $manage_items[] = id(new PhabricatorActionView()) ->setIcon('fa-exchange') ->setName(pht('Reorder Columns')) - ->setHref($this->getApplicationURI('board/'.$this->id.'/reorder/')) + ->setHref($reorder_uri) ->setDisabled(!$can_edit) ->setWorkflow(true); @@ -595,6 +610,13 @@ final class PhabricatorProjectBoardViewController ->setHref($batch_edit_uri) ->setDisabled(!$can_batch_edit); + $manage_items[] = id(new PhabricatorActionView()) + ->setIcon('fa-ban') + ->setName(pht('Disable Workboard')) + ->setHref($disable_uri) + ->setWorkflow(true) + ->setDisabled(!$can_edit); + $manage_menu = id(new PhabricatorActionListView()) ->setUser($viewer); foreach ($manage_items as $item) { @@ -852,4 +874,59 @@ final class PhabricatorProjectBoardViewController ->addCancelButton($profile_uri); } + + private function buildEnableContent(PhabricatorProject $project) { + $request = $this->getRequest(); + $viewer = $this->getViewer(); + + $id = $project->getID(); + $profile_uri = $this->getApplicationURI("profile/{$id}/"); + $board_uri = $this->getApplicationURI("board/{$id}/"); + + if ($request->isFormPost()) { + $xactions = array(); + + $xactions[] = id(new PhabricatorProjectTransaction()) + ->setTransactionType(PhabricatorProjectTransaction::TYPE_HASWORKBOARD) + ->setNewValue(1); + + id(new PhabricatorProjectTransactionEditor()) + ->setActor($viewer) + ->setContentSourceFromRequest($request) + ->setContinueOnNoEffect(true) + ->setContinueOnMissingFields(true) + ->applyTransactions($project, $xactions); + + return id(new AphrontRedirectResponse()) + ->setURI($board_uri); + } + + return $this->newDialog() + ->setTitle(pht('Workboard Disabled')) + ->addHiddenInput('initialize', 1) + ->appendParagraph( + pht( + 'This workboard has been disabled, but can be restored to its '. + 'former glory.')) + ->addCancelButton($profile_uri) + ->addSubmitButton(pht('Enable Workboard')); + } + + private function buildDisabledContent(PhabricatorProject $project) { + $viewer = $this->getViewer(); + + $id = $project->getID(); + + $profile_uri = $this->getApplicationURI("profile/{$id}/"); + + return $this->newDialog() + ->setTitle(pht('Workboard Disabled')) + ->appendParagraph( + pht( + 'This workboard has been disabled, and you do not have permission '. + 'to enable it. Only users who can edit this project can restore '. + 'the workboard.')) + ->addCancelButton($profile_uri); + } + } diff --git a/src/applications/project/editor/PhabricatorProjectTransactionEditor.php b/src/applications/project/editor/PhabricatorProjectTransactionEditor.php index 20eb2c42bf..51a120000a 100644 --- a/src/applications/project/editor/PhabricatorProjectTransactionEditor.php +++ b/src/applications/project/editor/PhabricatorProjectTransactionEditor.php @@ -39,6 +39,7 @@ final class PhabricatorProjectTransactionEditor $types[] = PhabricatorProjectTransaction::TYPE_LOCKED; $types[] = PhabricatorProjectTransaction::TYPE_PARENT; $types[] = PhabricatorProjectTransaction::TYPE_MILESTONE; + $types[] = PhabricatorProjectTransaction::TYPE_HASWORKBOARD; return $types; } @@ -65,6 +66,8 @@ final class PhabricatorProjectTransactionEditor return $object->getColor(); case PhabricatorProjectTransaction::TYPE_LOCKED: return (int)$object->getIsMembershipLocked(); + case PhabricatorProjectTransaction::TYPE_HASWORKBOARD: + return (int)$object->getHasWorkboard(); case PhabricatorProjectTransaction::TYPE_PARENT: case PhabricatorProjectTransaction::TYPE_MILESTONE: return null; @@ -87,6 +90,8 @@ final class PhabricatorProjectTransactionEditor case PhabricatorProjectTransaction::TYPE_PARENT: case PhabricatorProjectTransaction::TYPE_MILESTONE: return $xaction->getNewValue(); + case PhabricatorProjectTransaction::TYPE_HASWORKBOARD: + return (int)$xaction->getNewValue(); case PhabricatorProjectTransaction::TYPE_SLUGS: return $this->normalizeSlugs($xaction->getNewValue()); } @@ -131,6 +136,9 @@ final class PhabricatorProjectTransactionEditor $object->setMilestoneNumber($number); $object->setParentProjectPHID($xaction->getNewValue()); return; + case PhabricatorProjectTransaction::TYPE_HASWORKBOARD: + $object->setHasWorkboard($xaction->getNewValue()); + return; } return parent::applyCustomInternalTransaction($object, $xaction); @@ -172,6 +180,7 @@ final class PhabricatorProjectTransactionEditor case PhabricatorProjectTransaction::TYPE_LOCKED: case PhabricatorProjectTransaction::TYPE_PARENT: case PhabricatorProjectTransaction::TYPE_MILESTONE: + case PhabricatorProjectTransaction::TYPE_HASWORKBOARD: return; } diff --git a/src/applications/project/engine/PhabricatorBoardLayoutEngine.php b/src/applications/project/engine/PhabricatorBoardLayoutEngine.php index cfd752abb3..caedee336c 100644 --- a/src/applications/project/engine/PhabricatorBoardLayoutEngine.php +++ b/src/applications/project/engine/PhabricatorBoardLayoutEngine.php @@ -9,6 +9,7 @@ final class PhabricatorBoardLayoutEngine extends Phobject { private $columnMap = array(); private $objectColumnMap = array(); private $boardLayout = array(); + private $fetchAllBoards; private $remQueue = array(); private $addQueue = array(); @@ -40,6 +41,18 @@ final class PhabricatorBoardLayoutEngine extends Phobject { return $this->objectPHIDs; } + /** + * Fetch all boards, even if the board is disabled. + */ + public function setFetchAllBoards($fetch_all) { + $this->fetchAllBoards = $fetch_all; + return $this; + } + + public function getFetchAllBoards() { + return $this->fetchAllBoards; + } + public function executeLayout() { $viewer = $this->getViewer(); @@ -301,9 +314,11 @@ final class PhabricatorBoardLayoutEngine extends Phobject { ->execute(); $boards = mpull($boards, null, 'getPHID'); - foreach ($boards as $key => $board) { - if (!$board->getHasWorkboard()) { - unset($boards[$key]); + if (!$this->fetchAllBoards) { + foreach ($boards as $key => $board) { + if (!$board->getHasWorkboard()) { + unset($boards[$key]); + } } } diff --git a/src/applications/project/storage/PhabricatorProjectTransaction.php b/src/applications/project/storage/PhabricatorProjectTransaction.php index 1186e9c4f7..f668b0d2ec 100644 --- a/src/applications/project/storage/PhabricatorProjectTransaction.php +++ b/src/applications/project/storage/PhabricatorProjectTransaction.php @@ -12,6 +12,7 @@ final class PhabricatorProjectTransaction const TYPE_LOCKED = 'project:locked'; const TYPE_PARENT = 'project:parent'; const TYPE_MILESTONE = 'project:milestone'; + const TYPE_HASWORKBOARD = 'project:hasworkboard'; // NOTE: This is deprecated, members are just a normal edge now. const TYPE_MEMBERS = 'project:members'; @@ -246,6 +247,17 @@ final class PhabricatorProjectTransaction } } break; + + case self::TYPE_HASWORKBOARD: + if ($new) { + return pht( + '%s enabled the workboard for this project.', + $author_handle); + } else { + return pht( + '%s disabled the workboard for this project.', + $author_handle); + } } return parent::getTitle(); @@ -366,6 +378,20 @@ final class PhabricatorProjectTransaction $object_handle, $this->renderSlugList($rem)); } + + case self::TYPE_HASWORKBOARD: + if ($new) { + return pht( + '%s enabled the workboard for %s.', + $author_handle, + $object_handle); + } else { + return pht( + '%s disabled the workboard for %s.', + $author_handle, + $object_handle); + } + } return parent::getTitleForFeed(); From 32225d1dd0f8274ae9323650cbf20ea0571424de Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 8 Feb 2016 12:17:16 -0800 Subject: [PATCH 20/65] Remove three ancient columns from Maniphest tasks: attached, projectPHIDs, ccPHIDs Summary: Before edges, we stored some of this stuff directly on tasks. - `attached` was migrated to edges in Jan 2013. - `projectPHIDs` was never used, as far as I can tell? - `ccPHIDs` was migrated away and dropped more than a year ago. None of these columns are used in modern code (instead, modern code uses edges). Test Plan: `grep`, browsed around, `bin/storage upgrade`, unit tests. Reviewers: chad Reviewed By: chad Differential Revision: https://secure.phabricator.com/D15216 --- resources/sql/autopatches/20160208.task.1.sql | 2 ++ resources/sql/autopatches/20160208.task.2.sql | 2 ++ .../migrate-maniphest-dependencies.php | 28 +------------------ .../patches/migrate-maniphest-revisions.php | 27 +----------------- .../maniphest/storage/ManiphestTask.php | 17 ----------- 5 files changed, 6 insertions(+), 70 deletions(-) create mode 100644 resources/sql/autopatches/20160208.task.1.sql create mode 100644 resources/sql/autopatches/20160208.task.2.sql diff --git a/resources/sql/autopatches/20160208.task.1.sql b/resources/sql/autopatches/20160208.task.1.sql new file mode 100644 index 0000000000..786107666c --- /dev/null +++ b/resources/sql/autopatches/20160208.task.1.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_maniphest.maniphest_task + DROP projectPHIDs; diff --git a/resources/sql/autopatches/20160208.task.2.sql b/resources/sql/autopatches/20160208.task.2.sql new file mode 100644 index 0000000000..d0889ca92d --- /dev/null +++ b/resources/sql/autopatches/20160208.task.2.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_maniphest.maniphest_task + DROP attached; diff --git a/resources/sql/patches/migrate-maniphest-dependencies.php b/resources/sql/patches/migrate-maniphest-dependencies.php index 074018264c..1c607edd1d 100644 --- a/resources/sql/patches/migrate-maniphest-dependencies.php +++ b/resources/sql/patches/migrate-maniphest-dependencies.php @@ -1,29 +1,3 @@ openTransaction(); - -foreach (new LiskMigrationIterator($table) as $task) { - $id = $task->getID(); - echo pht('Task %d: ', $id); - - $deps = $task->getAttachedPHIDs(ManiphestTaskPHIDType::TYPECONST); - if (!$deps) { - echo "-\n"; - continue; - } - - $editor = new PhabricatorEdgeEditor(); - foreach ($deps as $dep) { - $editor->addEdge( - $task->getPHID(), - ManiphestTaskDependsOnTaskEdgeType::EDGECONST, - $dep); - } - $editor->save(); - echo pht('OKAY')."\n"; -} - -$table->saveTransaction(); -echo pht('Done.')."\n"; +// From 2013-2016, this migration moved dependent tasks to edges. diff --git a/resources/sql/patches/migrate-maniphest-revisions.php b/resources/sql/patches/migrate-maniphest-revisions.php index 2a8f8061b4..26d44299f6 100644 --- a/resources/sql/patches/migrate-maniphest-revisions.php +++ b/resources/sql/patches/migrate-maniphest-revisions.php @@ -1,28 +1,3 @@ establishConnection('w'); - -foreach (new LiskMigrationIterator($table) as $task) { - $id = $task->getID(); - echo pht('Task %d: ', $id); - - $revs = $task->getAttachedPHIDs(DifferentialRevisionPHIDType::TYPECONST); - if (!$revs) { - echo "-\n"; - continue; - } - - $editor = new PhabricatorEdgeEditor(); - foreach ($revs as $rev) { - $editor->addEdge( - $task->getPHID(), - ManiphestTaskHasRevisionEdgeType::EDGECONST, - $rev); - } - $editor->save(); - echo pht('OKAY')."\n"; -} - -echo pht('Done.')."\n"; +// From 2013-2016, this migration moved revisions attached to tasks to edges. diff --git a/src/applications/maniphest/storage/ManiphestTask.php b/src/applications/maniphest/storage/ManiphestTask.php index 2571b7a516..a51920aeee 100644 --- a/src/applications/maniphest/storage/ManiphestTask.php +++ b/src/applications/maniphest/storage/ManiphestTask.php @@ -34,8 +34,6 @@ final class ManiphestTask extends ManiphestDAO protected $viewPolicy = PhabricatorPolicies::POLICY_USER; protected $editPolicy = PhabricatorPolicies::POLICY_USER; - protected $projectPHIDs = array(); - protected $ownerOrdering; protected $spacePHID; protected $properties = array(); @@ -45,9 +43,6 @@ final class ManiphestTask extends ManiphestDAO private $customFields = self::ATTACHABLE; private $edgeProjectPHIDs = self::ATTACHABLE; - // TODO: This field is unused and should eventually be removed. - protected $attached = array(); - public static function initializeNewTask(PhabricatorUser $actor) { $app = id(new PhabricatorApplicationQuery()) ->setViewer($actor) @@ -72,9 +67,6 @@ final class ManiphestTask extends ManiphestDAO return array( self::CONFIG_AUX_PHID => true, self::CONFIG_SERIALIZATION => array( - 'ccPHIDs' => self::SERIALIZATION_JSON, - 'attached' => self::SERIALIZATION_JSON, - 'projectPHIDs' => self::SERIALIZATION_JSON, 'properties' => self::SERIALIZATION_JSON, ), self::CONFIG_COLUMN_SCHEMA => array( @@ -88,11 +80,6 @@ final class ManiphestTask extends ManiphestDAO 'ownerOrdering' => 'text64?', 'originalEmailSource' => 'text255?', 'subpriority' => 'double', - - // T6203/NULLABILITY - // This should not be nullable. It's going away soon anyway. - 'ccPHIDs' => 'text?', - ), self::CONFIG_KEY_SCHEMA => array( 'key_phid' => null, @@ -143,10 +130,6 @@ final class ManiphestTask extends ManiphestDAO ManiphestTaskDependedOnByTaskEdgeType::EDGECONST); } - public function getAttachedPHIDs($type) { - return array_keys(idx($this->attached, $type, array())); - } - public function generatePHID() { return PhabricatorPHID::generateNewPHID(ManiphestTaskPHIDType::TYPECONST); } From e9f3807cf53fe8ebde356cc5aa57563b79fcd81c Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 8 Feb 2016 14:16:51 -0800 Subject: [PATCH 21/65] Add a "points" field to tasks Summary: Currently never read or written. Supports fractions. There's no such thing as an unsigned double so this also supports negative values, technically, although I'll eventually prevent this in the UI. Test Plan: `bin/storage upgrade`, then created and edited a task. Nothing was different. Reviewers: chad Reviewed By: chad Differential Revision: https://secure.phabricator.com/D15218 --- resources/sql/autopatches/20160208.task.3.sql | 2 ++ src/applications/maniphest/storage/ManiphestTask.php | 2 ++ 2 files changed, 4 insertions(+) create mode 100644 resources/sql/autopatches/20160208.task.3.sql diff --git a/resources/sql/autopatches/20160208.task.3.sql b/resources/sql/autopatches/20160208.task.3.sql new file mode 100644 index 0000000000..9fae66d8df --- /dev/null +++ b/resources/sql/autopatches/20160208.task.3.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_maniphest.maniphest_task + ADD points DOUBLE; diff --git a/src/applications/maniphest/storage/ManiphestTask.php b/src/applications/maniphest/storage/ManiphestTask.php index a51920aeee..db72bc526c 100644 --- a/src/applications/maniphest/storage/ManiphestTask.php +++ b/src/applications/maniphest/storage/ManiphestTask.php @@ -37,6 +37,7 @@ final class ManiphestTask extends ManiphestDAO protected $ownerOrdering; protected $spacePHID; protected $properties = array(); + protected $points; private $subscriberPHIDs = self::ATTACHABLE; private $groupByProjectPHID = self::ATTACHABLE; @@ -80,6 +81,7 @@ final class ManiphestTask extends ManiphestDAO 'ownerOrdering' => 'text64?', 'originalEmailSource' => 'text255?', 'subpriority' => 'double', + 'points' => 'double?', ), self::CONFIG_KEY_SCHEMA => array( 'key_phid' => null, From 86c2f9df2ee2f00493c23807a46510f2634a46af Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 8 Feb 2016 14:37:31 -0800 Subject: [PATCH 22/65] First cut of progress bars (PHUISegmentBarView) Summary: Ref T10288. I couldn't figure out how to reasonably get the interior right borders to round like the mock, but I think this is otherwise mostly faithful. Feel free to fix stuff. Test Plan: {F1100415} Reviewers: chad Reviewed By: chad Maniphest Tasks: T10288 Differential Revision: https://secure.phabricator.com/D15219 --- resources/celerity/map.php | 2 + src/__phutil_library_map__.php | 4 + .../PhabricatorAphrontBarUIExample.php | 77 +++++++++---------- src/view/phui/PHUISegmentBarSegmentView.php | 57 ++++++++++++++ src/view/phui/PHUISegmentBarView.php | 65 ++++++++++++++++ .../rsrc/css/phui/phui-segment-bar-view.css | 71 +++++++++++++++++ 6 files changed, 235 insertions(+), 41 deletions(-) create mode 100644 src/view/phui/PHUISegmentBarSegmentView.php create mode 100644 src/view/phui/PHUISegmentBarView.php create mode 100644 webroot/rsrc/css/phui/phui-segment-bar-view.css diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 5de230fe1c..69d2e06c60 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -149,6 +149,7 @@ return array( 'rsrc/css/phui/phui-profile-menu.css' => '4a243229', 'rsrc/css/phui/phui-property-list-view.css' => '27b2849e', 'rsrc/css/phui/phui-remarkup-preview.css' => '1a8f2591', + 'rsrc/css/phui/phui-segment-bar-view.css' => '728e4d19', 'rsrc/css/phui/phui-spacing.css' => '042804d6', 'rsrc/css/phui/phui-status.css' => '888cedb8', 'rsrc/css/phui/phui-tag-view.css' => '9d5d4400', @@ -825,6 +826,7 @@ return array( 'phui-profile-menu-css' => '4a243229', 'phui-property-list-view-css' => '27b2849e', 'phui-remarkup-preview-css' => '1a8f2591', + 'phui-segment-bar-view-css' => '728e4d19', 'phui-spacing-css' => '042804d6', 'phui-status-list-view-css' => '888cedb8', 'phui-tag-view-css' => '9d5d4400', diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index accf2c05cb..9066784dbd 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -1522,6 +1522,8 @@ phutil_register_library_map(array( 'PHUIPropertyListView' => 'view/phui/PHUIPropertyListView.php', 'PHUIRemarkupPreviewPanel' => 'view/phui/PHUIRemarkupPreviewPanel.php', 'PHUIRemarkupView' => 'infrastructure/markup/view/PHUIRemarkupView.php', + 'PHUISegmentBarSegmentView' => 'view/phui/PHUISegmentBarSegmentView.php', + 'PHUISegmentBarView' => 'view/phui/PHUISegmentBarView.php', 'PHUISpacesNamespaceContextView' => 'applications/spaces/view/PHUISpacesNamespaceContextView.php', 'PHUIStatusItemView' => 'view/phui/PHUIStatusItemView.php', 'PHUIStatusListView' => 'view/phui/PHUIStatusListView.php', @@ -5705,6 +5707,8 @@ phutil_register_library_map(array( 'PHUIPropertyListView' => 'AphrontView', 'PHUIRemarkupPreviewPanel' => 'AphrontTagView', 'PHUIRemarkupView' => 'AphrontView', + 'PHUISegmentBarSegmentView' => 'AphrontTagView', + 'PHUISegmentBarView' => 'AphrontTagView', 'PHUISpacesNamespaceContextView' => 'AphrontView', 'PHUIStatusItemView' => 'AphrontTagView', 'PHUIStatusListView' => 'AphrontTagView', diff --git a/src/applications/uiexample/examples/PhabricatorAphrontBarUIExample.php b/src/applications/uiexample/examples/PhabricatorAphrontBarUIExample.php index f66c5c1618..741056dd4a 100644 --- a/src/applications/uiexample/examples/PhabricatorAphrontBarUIExample.php +++ b/src/applications/uiexample/examples/PhabricatorAphrontBarUIExample.php @@ -12,10 +12,7 @@ final class PhabricatorAphrontBarUIExample extends PhabricatorUIExample { public function renderExample() { $out = array(); - $out[] = $this->renderTestThings('AphrontProgressBarView', 13, 10); - $out[] = $this->renderTestThings('AphrontGlyphBarView', 13, 10); - $out[] = $this->renderWeirdOrderGlyphBars(); - $out[] = $this->renderAsciiStarBar(); + $out[] = $this->renderRainbow(); return $out; } @@ -26,48 +23,46 @@ final class PhabricatorAphrontBarUIExample extends PhabricatorUIExample { ->appendChild($thing); } - private function renderTestThings($class, $max, $incr) { + private function renderRainbow() { + $colors = array( + 'red', + 'orange', + 'yellow', + 'green', + 'blue', + 'indigo', + 'violet', + ); + + $labels = array( + pht('Empty'), + pht('Red'), + pht('Orange'), + pht('Yellow'), + pht('Green'), + pht('Blue'), + pht('Indigo'), + pht('Violet'), + ); + $bars = array(); - for ($ii = 0; $ii <= $max; $ii++) { - $bars[] = newv($class, array()) - ->setValue($ii * $incr) - ->setMax($max * $incr) - ->setCaption("{$ii} outta {$max} ain't bad!"); - } - return $this->wrap("Test {$class}", $bars); - } - private function renderWeirdOrderGlyphBars() { - $views = array(); - $indices = array(1, 3, 7, 4, 2, 8, 9, 5, 10, 6); - $max = count($indices); - foreach ($indices as $index) { - $views[] = id(new AphrontGlyphBarView()) - ->setValue($index) - ->setMax($max) - ->setNumGlyphs(5) - ->setCaption("Lol score is {$index}/{$max}") - ->setGlyph(hsprintf('%s', 'LOL!')) - ->setBackgroundGlyph(hsprintf('%s', '____')); - $views[] = hsprintf('
'); + for ($jj = -1; $jj < count($colors); $jj++) { + $bar = id(new PHUISegmentBarView()) + ->setLabel($labels[$jj + 1]); + for ($ii = 0; $ii <= $jj; $ii++) { + $bar->newSegment() + ->setWidth(1 / 7) + ->setColor($colors[$ii]); + } + $bars[] = $bar; } - return $this->wrap( - pht('Glyph bars in weird order'), - $views); - } + $bars = phutil_implode_html( + phutil_tag('br'), + $bars); - private function renderAsciiStarBar() { - $bar = id(new AphrontGlyphBarView()) - ->setValue(50) - ->setMax(100) - ->setCaption(pht('Glyphs!')) - ->setNumGlyphs(10) - ->setGlyph(hsprintf('%s', '*')); - - return $this->wrap( - pht('ASCII star glyph bar'), - $bar); + return $this->wrap(pht('Rainbow Bars'), $bars); } } diff --git a/src/view/phui/PHUISegmentBarSegmentView.php b/src/view/phui/PHUISegmentBarSegmentView.php new file mode 100644 index 0000000000..1b94df0e2a --- /dev/null +++ b/src/view/phui/PHUISegmentBarSegmentView.php @@ -0,0 +1,57 @@ +width = $width; + return $this; + } + + public function getWidth() { + return $this->width; + } + + public function setColor($color) { + $this->color = $color; + return $this; + } + + public function setPosition($position) { + $this->position = $position; + return $this; + } + + protected function canAppendChild() { + return false; + } + + protected function getTagAttributes() { + $classes = array( + 'phui-segment-bar-segment-view', + ); + + if ($this->color) { + $classes[] = $this->color; + } + + // Convert width to a percentage, and round it up slightly so that bars + // are full if they have, e.g., three segments at 1/3 + 1/3 + 1/3. + $width = 100 * $this->width; + $width = ceil(100 * $width) / 100; + $width = sprintf('%.2f%%', $width); + + $left = 100 * $this->position; + $left = floor(100 * $left) / 100; + $left = sprintf('%.2f%%', $left); + + return array( + 'class' => implode(' ', $classes), + 'style' => "left: {$left}; width: {$width};", + ); + } + +} diff --git a/src/view/phui/PHUISegmentBarView.php b/src/view/phui/PHUISegmentBarView.php new file mode 100644 index 0000000000..632c5327eb --- /dev/null +++ b/src/view/phui/PHUISegmentBarView.php @@ -0,0 +1,65 @@ +label = $label; + return $this; + } + + public function newSegment() { + $segment = new PHUISegmentBarSegmentView(); + $this->segments[] = $segment; + return $segment; + } + + protected function canAppendChild() { + return false; + } + + protected function getTagAttributes() { + return array( + 'class' => 'phui-segment-bar-view', + ); + } + + protected function getTagContent() { + require_celerity_resource('phui-segment-bar-view-css'); + + $label = $this->label; + if (strlen($label)) { + $label = phutil_tag( + 'div', + array( + 'class' => 'phui-segment-bar-label', + ), + $label); + } + + $segments = $this->segments; + + $position = 0; + foreach ($segments as $segment) { + $segment->setPosition($position); + $position += $segment->getWidth(); + } + + $segments = array_reverse($segments); + + $segments = phutil_tag( + 'div', + array( + 'class' => 'phui-segment-bar-segments', + ), + $segments); + + return array( + $label, + $segments, + ); + } + +} diff --git a/webroot/rsrc/css/phui/phui-segment-bar-view.css b/webroot/rsrc/css/phui/phui-segment-bar-view.css new file mode 100644 index 0000000000..c965257268 --- /dev/null +++ b/webroot/rsrc/css/phui/phui-segment-bar-view.css @@ -0,0 +1,71 @@ +/** + * @provides phui-segment-bar-view-css + */ + +.phui-segment-bar-label { + font-size: {$smallerfontsize}; + margin-bottom: 4px; +} + +.phui-segment-bar-segments { + background: {$lightgreybackground}; + border-radius: 4px; + position: relative; + overflow: hidden; + height: 8px; + border: 1px solid rgba(0, 0, 0, 0.15); +} + +.phui-segment-bar-segment-view { + position: absolute; + top: 0; + bottom: 0; + margin-left: -4px; + border-right: 5px solid; + border-radius: 0 4px 4px 0; +} + +.phui-segment-bar-segment-view.red { + background: {$red}; + border-color: {$red}; +} + +.phui-segment-bar-segment-view.orange { + background: {$orange}; + border-color: {$orange}; +} + +.phui-segment-bar-segment-view.yellow { + background: {$yellow}; + border-color: {$yellow} +} + +.phui-segment-bar-segment-view.green { + background: {$green}; + border-color: {$green}; +} + +.phui-segment-bar-segment-view.blue { + background: {$blue}; + border-color: {$blue}; +} + +.phui-segment-bar-segment-view.indigo { + background: {$indigo}; + border-color: {$indigo}; +} + +.phui-segment-bar-segment-view.violet { + background: {$violet}; + border-color: {$violet}; +} + +.phui-segment-bar-segment-view.pink { + background: {$pink}; + border-color: {$pink}; +} + +.phui-segment-bar-segment-view.sky { + background: {$sky}; + border-color: {$sky}; +} From f84130f9cd7463178fc27a0cfad1f8be5f5ddb43 Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 8 Feb 2016 15:24:52 -0800 Subject: [PATCH 23/65] Support enabling a formal points field in Maniphest Summary: Ref T4427. - New config option for labels, enabling, etc., but no UI/niceness yet. - When enabled, add a field. - Allow nonnegative values, including fractional values. - EditEngine is nice and Conduit / actions basically just work with a tiny bit of extra support code. Test Plan: - Edited points via "Edit". - Edited points via Conduit. - Edited points via stacked actions. - Tried to set "zebra" points. - Tried to set -1 points. Reviewers: chad Reviewed By: chad Maniphest Tasks: T4427 Differential Revision: https://secure.phabricator.com/D15220 --- resources/celerity/map.php | 12 ++--- src/__phutil_library_map__.php | 10 +++- .../parametertype/ConduitIntParameterType.php | 2 +- .../ConduitPointsParameterType.php | 49 +++++++++++++++++++ .../PhabricatorManiphestConfigOptions.php | 4 +- .../constants/ManiphestTaskPoints.php | 24 +++++++++ .../maniphest/editor/ManiphestEditEngine.php | 43 +++++++++++----- .../editor/ManiphestTransactionEditor.php | 39 +++++++++++++++ .../storage/ManiphestTransaction.php | 33 +++++++++++++ ...abricatorEditEnginePointsCommentAction.php | 16 ++++++ .../editfield/PhabricatorPointsEditField.php | 17 +++++++ webroot/rsrc/js/phuix/PHUIXFormControl.js | 22 +++++++++ 12 files changed, 250 insertions(+), 21 deletions(-) create mode 100644 src/applications/conduit/parametertype/ConduitPointsParameterType.php create mode 100644 src/applications/maniphest/constants/ManiphestTaskPoints.php create mode 100644 src/applications/transactions/commentaction/PhabricatorEditEnginePointsCommentAction.php create mode 100644 src/applications/transactions/editfield/PhabricatorPointsEditField.php diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 69d2e06c60..fe2711dceb 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -507,7 +507,7 @@ return array( 'rsrc/js/phuix/PHUIXActionView.js' => '8cf6d262', 'rsrc/js/phuix/PHUIXAutocomplete.js' => '9196fb06', 'rsrc/js/phuix/PHUIXDropdownMenu.js' => 'bd4c8dca', - 'rsrc/js/phuix/PHUIXFormControl.js' => '8fba1997', + 'rsrc/js/phuix/PHUIXFormControl.js' => 'a7763e11', 'rsrc/js/phuix/PHUIXIconView.js' => 'bff6884b', ), 'symbols' => array( @@ -840,7 +840,7 @@ return array( 'phuix-action-view' => '8cf6d262', 'phuix-autocomplete' => '9196fb06', 'phuix-dropdown-menu' => 'bd4c8dca', - 'phuix-form-control-view' => '8fba1997', + 'phuix-form-control-view' => 'a7763e11', 'phuix-icon-view' => 'bff6884b', 'policy-css' => '957ea14c', 'policy-edit-css' => '815c66f7', @@ -1526,10 +1526,6 @@ return array( 'javelin-stratcom', 'javelin-install', ), - '8fba1997' => array( - 'javelin-install', - 'javelin-dom', - ), '901935ef' => array( 'javelin-behavior', 'javelin-dom', @@ -1633,6 +1629,10 @@ return array( 'javelin-uri', 'phabricator-notification', ), + 'a7763e11' => array( + 'javelin-install', + 'javelin-dom', + ), 'a78c0661' => array( 'phui-workcard-view-css', ), diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 9066784dbd..cdd633de41 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -241,6 +241,7 @@ phutil_register_library_map(array( 'ConduitPHIDParameterType' => 'applications/conduit/parametertype/ConduitPHIDParameterType.php', 'ConduitParameterType' => 'applications/conduit/parametertype/ConduitParameterType.php', 'ConduitPingConduitAPIMethod' => 'applications/conduit/method/ConduitPingConduitAPIMethod.php', + 'ConduitPointsParameterType' => 'applications/conduit/parametertype/ConduitPointsParameterType.php', 'ConduitProjectListParameterType' => 'applications/conduit/parametertype/ConduitProjectListParameterType.php', 'ConduitQueryConduitAPIMethod' => 'applications/conduit/method/ConduitQueryConduitAPIMethod.php', 'ConduitResultSearchEngineExtension' => 'applications/conduit/query/ConduitResultSearchEngineExtension.php', @@ -1339,6 +1340,7 @@ phutil_register_library_map(array( 'ManiphestTaskOpenStatusDatasource' => 'applications/maniphest/typeahead/ManiphestTaskOpenStatusDatasource.php', 'ManiphestTaskPHIDResolver' => 'applications/maniphest/httpparametertype/ManiphestTaskPHIDResolver.php', 'ManiphestTaskPHIDType' => 'applications/maniphest/phid/ManiphestTaskPHIDType.php', + 'ManiphestTaskPoints' => 'applications/maniphest/constants/ManiphestTaskPoints.php', 'ManiphestTaskPriority' => 'applications/maniphest/constants/ManiphestTaskPriority.php', 'ManiphestTaskPriorityDatasource' => 'applications/maniphest/typeahead/ManiphestTaskPriorityDatasource.php', 'ManiphestTaskPriorityHeraldAction' => 'applications/maniphest/herald/ManiphestTaskPriorityHeraldAction.php', @@ -2206,6 +2208,7 @@ phutil_register_library_map(array( 'PhabricatorEditEngineExtension' => 'applications/transactions/engineextension/PhabricatorEditEngineExtension.php', 'PhabricatorEditEngineExtensionModule' => 'applications/transactions/engineextension/PhabricatorEditEngineExtensionModule.php', 'PhabricatorEditEngineListController' => 'applications/transactions/controller/PhabricatorEditEngineListController.php', + 'PhabricatorEditEnginePointsCommentAction' => 'applications/transactions/commentaction/PhabricatorEditEnginePointsCommentAction.php', 'PhabricatorEditEngineQuery' => 'applications/transactions/query/PhabricatorEditEngineQuery.php', 'PhabricatorEditEngineSearchEngine' => 'applications/transactions/query/PhabricatorEditEngineSearchEngine.php', 'PhabricatorEditEngineSelectCommentAction' => 'applications/transactions/commentaction/PhabricatorEditEngineSelectCommentAction.php', @@ -2813,6 +2816,7 @@ phutil_register_library_map(array( 'PhabricatorPhurlURLViewController' => 'applications/phurl/controller/PhabricatorPhurlURLViewController.php', 'PhabricatorPirateEnglishTranslation' => 'infrastructure/internationalization/translation/PhabricatorPirateEnglishTranslation.php', 'PhabricatorPlatformSite' => 'aphront/site/PhabricatorPlatformSite.php', + 'PhabricatorPointsEditField' => 'applications/transactions/editfield/PhabricatorPointsEditField.php', 'PhabricatorPolicies' => 'applications/policy/constants/PhabricatorPolicies.php', 'PhabricatorPolicy' => 'applications/policy/storage/PhabricatorPolicy.php', 'PhabricatorPolicyApplication' => 'applications/policy/application/PhabricatorPolicyApplication.php', @@ -4226,7 +4230,7 @@ phutil_register_library_map(array( 'ConduitGetCapabilitiesConduitAPIMethod' => 'ConduitAPIMethod', 'ConduitGetCertificateConduitAPIMethod' => 'ConduitAPIMethod', 'ConduitIntListParameterType' => 'ConduitListParameterType', - 'ConduitIntParameterType' => 'ConduitListParameterType', + 'ConduitIntParameterType' => 'ConduitParameterType', 'ConduitListParameterType' => 'ConduitParameterType', 'ConduitLogGarbageCollector' => 'PhabricatorGarbageCollector', 'ConduitMethodDoesNotExistException' => 'ConduitMethodNotFoundException', @@ -4235,6 +4239,7 @@ phutil_register_library_map(array( 'ConduitPHIDParameterType' => 'ConduitParameterType', 'ConduitParameterType' => 'Phobject', 'ConduitPingConduitAPIMethod' => 'ConduitAPIMethod', + 'ConduitPointsParameterType' => 'ConduitParameterType', 'ConduitProjectListParameterType' => 'ConduitListParameterType', 'ConduitQueryConduitAPIMethod' => 'ConduitAPIMethod', 'ConduitResultSearchEngineExtension' => 'PhabricatorSearchEngineExtension', @@ -5508,6 +5513,7 @@ phutil_register_library_map(array( 'ManiphestTaskOpenStatusDatasource' => 'PhabricatorTypeaheadDatasource', 'ManiphestTaskPHIDResolver' => 'PhabricatorPHIDResolver', 'ManiphestTaskPHIDType' => 'PhabricatorPHIDType', + 'ManiphestTaskPoints' => 'Phobject', 'ManiphestTaskPriority' => 'ManiphestConstants', 'ManiphestTaskPriorityDatasource' => 'PhabricatorTypeaheadDatasource', 'ManiphestTaskPriorityHeraldAction' => 'HeraldAction', @@ -6508,6 +6514,7 @@ phutil_register_library_map(array( 'PhabricatorEditEngineExtension' => 'Phobject', 'PhabricatorEditEngineExtensionModule' => 'PhabricatorConfigModule', 'PhabricatorEditEngineListController' => 'PhabricatorEditEngineController', + 'PhabricatorEditEnginePointsCommentAction' => 'PhabricatorEditEngineCommentAction', 'PhabricatorEditEngineQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorEditEngineSearchEngine' => 'PhabricatorApplicationSearchEngine', 'PhabricatorEditEngineSelectCommentAction' => 'PhabricatorEditEngineCommentAction', @@ -7206,6 +7213,7 @@ phutil_register_library_map(array( 'PhabricatorPhurlURLViewController' => 'PhabricatorPhurlController', 'PhabricatorPirateEnglishTranslation' => 'PhutilTranslation', 'PhabricatorPlatformSite' => 'PhabricatorSite', + 'PhabricatorPointsEditField' => 'PhabricatorEditField', 'PhabricatorPolicies' => 'PhabricatorPolicyConstants', 'PhabricatorPolicy' => array( 'PhabricatorPolicyDAO', diff --git a/src/applications/conduit/parametertype/ConduitIntParameterType.php b/src/applications/conduit/parametertype/ConduitIntParameterType.php index e9943e53a7..54f66fdf6c 100644 --- a/src/applications/conduit/parametertype/ConduitIntParameterType.php +++ b/src/applications/conduit/parametertype/ConduitIntParameterType.php @@ -1,7 +1,7 @@ raiseValidationException( + $request, + $key, + pht('Expected numeric points value, got something else.')); + } + + if ($value !== null) { + $value = (double)$value; + if ($value < 0) { + $this->raiseValidationException( + $request, + $key, + pht('Point values must be nonnegative.')); + } + } + + return $value; + } + + protected function getParameterTypeName() { + return 'points'; + } + + protected function getParameterFormatDescriptions() { + return array( + pht('A nonnegative number, or null.'), + ); + } + + protected function getParameterExamples() { + return array( + 'null', + '0', + '1', + '15', + '0.5', + ); + } + +} diff --git a/src/applications/maniphest/config/PhabricatorManiphestConfigOptions.php b/src/applications/maniphest/config/PhabricatorManiphestConfigOptions.php index 3d47c59e1c..4fd9535ac7 100644 --- a/src/applications/maniphest/config/PhabricatorManiphestConfigOptions.php +++ b/src/applications/maniphest/config/PhabricatorManiphestConfigOptions.php @@ -336,7 +336,9 @@ EOTEXT '"Needs Triage" panel on the home page. You should adjust this if '. 'you adjust priorities using `%s`.', 'maniphest.priorities')), - + $this->newOption('maniphest.points', 'map', array()) + ->setDescription( + pht('PROTOTYPE! Very hot. Burns user. Do not touch!')), ); } diff --git a/src/applications/maniphest/constants/ManiphestTaskPoints.php b/src/applications/maniphest/constants/ManiphestTaskPoints.php new file mode 100644 index 0000000000..8df71f7936 --- /dev/null +++ b/src/applications/maniphest/constants/ManiphestTaskPoints.php @@ -0,0 +1,24 @@ +getViewer()->getPHID()); } - return array( + $fields = array( id(new PhabricatorHandlesEditField()) ->setKey('parent') ->setLabel(pht('Parent Task')) @@ -149,18 +149,37 @@ final class ManiphestEditEngine ->setValue($object->getPriority()) ->setOptions($priority_map) ->setCommentActionLabel(pht('Change Priority')), - id(new PhabricatorRemarkupEditField()) - ->setKey('description') - ->setLabel(pht('Description')) - ->setDescription(pht('Task description.')) - ->setConduitDescription(pht('Update the task description.')) - ->setConduitTypeDescription(pht('New task description.')) - ->setTransactionType(ManiphestTransaction::TYPE_DESCRIPTION) - ->setValue($object->getDescription()) - ->setPreviewPanel( - id(new PHUIRemarkupPreviewPanel()) - ->setHeader(pht('Description Preview'))), ); + + if (ManiphestTaskPoints::getIsEnabled()) { + $points_label = ManiphestTaskPoints::getPointsLabel(); + $action_label = ManiphestTaskPoints::getPointsActionLabel(); + + $fields[] = id(new PhabricatorPointsEditField()) + ->setKey('points') + ->setLabel($points_label) + ->setDescription(pht('Point value of the task.')) + ->setConduitDescription(pht('Change the task point value.')) + ->setConduitTypeDescription(pht('New task point value.')) + ->setTransactionType(ManiphestTransaction::TYPE_POINTS) + ->setIsCopyable(true) + ->setValue($object->getPoints()) + ->setCommentActionLabel($action_label); + } + + $fields[] = id(new PhabricatorRemarkupEditField()) + ->setKey('description') + ->setLabel(pht('Description')) + ->setDescription(pht('Task description.')) + ->setConduitDescription(pht('Update the task description.')) + ->setConduitTypeDescription(pht('New task description.')) + ->setTransactionType(ManiphestTransaction::TYPE_DESCRIPTION) + ->setValue($object->getDescription()) + ->setPreviewPanel( + id(new PHUIRemarkupPreviewPanel()) + ->setHeader(pht('Description Preview'))); + + return $fields; } private function getTaskStatusMap(ManiphestTask $task) { diff --git a/src/applications/maniphest/editor/ManiphestTransactionEditor.php b/src/applications/maniphest/editor/ManiphestTransactionEditor.php index 8473b3c933..073bf87bea 100644 --- a/src/applications/maniphest/editor/ManiphestTransactionEditor.php +++ b/src/applications/maniphest/editor/ManiphestTransactionEditor.php @@ -29,6 +29,7 @@ final class ManiphestTransactionEditor $types[] = ManiphestTransaction::TYPE_PARENT; $types[] = ManiphestTransaction::TYPE_COLUMN; $types[] = ManiphestTransaction::TYPE_COVER_IMAGE; + $types[] = ManiphestTransaction::TYPE_POINTS; $types[] = PhabricatorTransactions::TYPE_VIEW_POLICY; $types[] = PhabricatorTransactions::TYPE_EDIT_POLICY; @@ -69,6 +70,8 @@ final class ManiphestTransactionEditor return $object->getSubpriority(); case ManiphestTransaction::TYPE_COVER_IMAGE: return $object->getCoverImageFilePHID(); + case ManiphestTransaction::TYPE_POINTS: + return $object->getPoints(); case ManiphestTransaction::TYPE_MERGED_INTO: case ManiphestTransaction::TYPE_MERGED_FROM: return null; @@ -100,6 +103,15 @@ final class ManiphestTransactionEditor case ManiphestTransaction::TYPE_PARENT: case ManiphestTransaction::TYPE_COLUMN: return $xaction->getNewValue(); + case ManiphestTransaction::TYPE_POINTS: + $value = $xaction->getNewValue(); + if (!strlen($value)) { + $value = null; + } + if ($value !== null) { + $value = (double)$value; + } + return $value; } } @@ -191,6 +203,9 @@ final class ManiphestTransactionEditor $object->setProperty('cover.filePHID', $file->getPHID()); $object->setProperty('cover.thumbnailPHID', $xform->getPHID()); return; + case ManiphestTransaction::TYPE_POINTS: + $object->setPoints($xaction->getNewValue()); + return; case ManiphestTransaction::TYPE_MERGED_FROM: case ManiphestTransaction::TYPE_PARENT: case ManiphestTransaction::TYPE_COLUMN: @@ -884,6 +899,30 @@ final class ManiphestTransactionEditor } } break; + + case ManiphestTransaction::TYPE_POINTS: + foreach ($xactions as $xaction) { + $new = $xaction->getNewValue(); + if (strlen($new) && !is_numeric($new)) { + $errors[] = new PhabricatorApplicationTransactionValidationError( + $type, + pht('Invalid'), + pht('Points value must be numeric or empty.'), + $xaction); + continue; + } + + if ((double)$new < 0) { + $errors[] = new PhabricatorApplicationTransactionValidationError( + $type, + pht('Invalid'), + pht('Points value must be nonnegative.'), + $xaction); + continue; + } + } + break; + } return $errors; diff --git a/src/applications/maniphest/storage/ManiphestTransaction.php b/src/applications/maniphest/storage/ManiphestTransaction.php index 812ac0a7de..2a4584c49f 100644 --- a/src/applications/maniphest/storage/ManiphestTransaction.php +++ b/src/applications/maniphest/storage/ManiphestTransaction.php @@ -17,6 +17,7 @@ final class ManiphestTransaction const TYPE_PARENT = 'parent'; const TYPE_COLUMN = 'column'; const TYPE_COVER_IMAGE = 'cover-image'; + const TYPE_POINTS = 'points'; // NOTE: this type is deprecated. Keep it around for legacy installs // so any transactions render correctly. @@ -166,11 +167,24 @@ final class ManiphestTransaction case self::TYPE_COVER_IMAGE: // At least for now, don't show these. return true; + case self::TYPE_POINTS: + if (!ManiphestTaskPoints::getIsEnabled()) { + return true; + } } return parent::shouldHide(); } + public function shouldHideForMail(array $xactions) { + switch ($this->getTransactionType()) { + case self::TYPE_POINTS: + return true; + } + + return parent::shouldHideForMail(); + } + public function shouldHideForFeed() { switch ($this->getTransactionType()) { case self::TYPE_UNBLOCK: @@ -181,6 +195,8 @@ final class ManiphestTransaction return true; } break; + case self::TYPE_POINTS: + return true; } return parent::shouldHideForFeed(); @@ -624,6 +640,23 @@ final class ManiphestTransaction $this->renderHandleList($new)); break; + case self::TYPE_POINTS: + if ($old === null) { + return pht( + '%s set the point value for this task to %s.', + $this->renderHandleLink($author_phid), + $new); + } else if ($new === null) { + return pht( + '%s removed the point value for this task.', + $this->renderHandleLink($author_phid)); + } else { + return pht( + '%s changed the point value for this task from %s to %s.', + $this->renderHandleLink($author_phid), + $old, + $new); + } } diff --git a/src/applications/transactions/commentaction/PhabricatorEditEnginePointsCommentAction.php b/src/applications/transactions/commentaction/PhabricatorEditEnginePointsCommentAction.php new file mode 100644 index 0000000000..c64262af96 --- /dev/null +++ b/src/applications/transactions/commentaction/PhabricatorEditEnginePointsCommentAction.php @@ -0,0 +1,16 @@ + $this->getValue(), + ); + } + +} diff --git a/src/applications/transactions/editfield/PhabricatorPointsEditField.php b/src/applications/transactions/editfield/PhabricatorPointsEditField.php new file mode 100644 index 0000000000..48c1b3b35b --- /dev/null +++ b/src/applications/transactions/editfield/PhabricatorPointsEditField.php @@ -0,0 +1,17 @@ + Date: Mon, 8 Feb 2016 17:31:21 -0800 Subject: [PATCH 24/65] Add a basic progress bar for milestones Summary: Ref T4427. This kind of works. Test Plan: {F1100578} Reviewers: chad Reviewed By: chad Maniphest Tasks: T4427 Differential Revision: https://secure.phabricator.com/D15221 --- resources/celerity/map.php | 10 +- src/__phutil_library_map__.php | 2 + .../maniphest/editor/ManiphestEditEngine.php | 35 +++- .../engine/PhabricatorBoardLayoutEngine.php | 9 +- .../PhabricatorProjectProfilePanelEngine.php | 4 + .../PhabricatorProjectPointsProfilePanel.php | 192 ++++++++++++++++++ ...habricatorProjectWorkboardProfilePanel.php | 20 +- .../project/storage/PhabricatorProject.php | 1 + .../engine/PhabricatorProfilePanelEngine.php | 15 +- src/view/phui/PHUISegmentBarSegmentView.php | 22 ++ webroot/rsrc/css/phui/phui-profile-menu.css | 12 ++ .../rsrc/css/phui/phui-segment-bar-view.css | 2 +- 12 files changed, 293 insertions(+), 31 deletions(-) create mode 100644 src/applications/project/profilepanel/PhabricatorProjectPointsProfilePanel.php diff --git a/resources/celerity/map.php b/resources/celerity/map.php index fe2711dceb..c995903991 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -7,7 +7,7 @@ */ return array( 'names' => array( - 'core.pkg.css' => 'b4a7e275', + 'core.pkg.css' => 'bef9c7cb', 'core.pkg.js' => '17380dd3', 'darkconsole.pkg.js' => 'e7393ebb', 'differential.pkg.css' => '2de124c9', @@ -146,10 +146,10 @@ return array( 'rsrc/css/phui/phui-object-item-list-view.css' => '8f443e8b', 'rsrc/css/phui/phui-pager.css' => 'bea33d23', 'rsrc/css/phui/phui-pinboard-view.css' => '2495140e', - 'rsrc/css/phui/phui-profile-menu.css' => '4a243229', + 'rsrc/css/phui/phui-profile-menu.css' => '2d5f0c75', 'rsrc/css/phui/phui-property-list-view.css' => '27b2849e', 'rsrc/css/phui/phui-remarkup-preview.css' => '1a8f2591', - 'rsrc/css/phui/phui-segment-bar-view.css' => '728e4d19', + 'rsrc/css/phui/phui-segment-bar-view.css' => '52e7e529', 'rsrc/css/phui/phui-spacing.css' => '042804d6', 'rsrc/css/phui/phui-status.css' => '888cedb8', 'rsrc/css/phui/phui-tag-view.css' => '9d5d4400', @@ -823,10 +823,10 @@ return array( 'phui-object-item-list-view-css' => '8f443e8b', 'phui-pager-css' => 'bea33d23', 'phui-pinboard-view-css' => '2495140e', - 'phui-profile-menu-css' => '4a243229', + 'phui-profile-menu-css' => '2d5f0c75', 'phui-property-list-view-css' => '27b2849e', 'phui-remarkup-preview-css' => '1a8f2591', - 'phui-segment-bar-view-css' => '728e4d19', + 'phui-segment-bar-view-css' => '52e7e529', 'phui-spacing-css' => '042804d6', 'phui-status-list-view-css' => '888cedb8', 'phui-tag-view-css' => '9d5d4400', diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index cdd633de41..3c972e329d 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -2933,6 +2933,7 @@ phutil_register_library_map(array( 'PhabricatorProjectOrUserFunctionDatasource' => 'applications/project/typeahead/PhabricatorProjectOrUserFunctionDatasource.php', 'PhabricatorProjectPHIDResolver' => 'applications/phid/resolver/PhabricatorProjectPHIDResolver.php', 'PhabricatorProjectPanelController' => 'applications/project/controller/PhabricatorProjectPanelController.php', + 'PhabricatorProjectPointsProfilePanel' => 'applications/project/profilepanel/PhabricatorProjectPointsProfilePanel.php', 'PhabricatorProjectProfileController' => 'applications/project/controller/PhabricatorProjectProfileController.php', 'PhabricatorProjectProfilePanelEngine' => 'applications/project/engine/PhabricatorProjectProfilePanelEngine.php', 'PhabricatorProjectProjectHasMemberEdgeType' => 'applications/project/edge/PhabricatorProjectProjectHasMemberEdgeType.php', @@ -7364,6 +7365,7 @@ phutil_register_library_map(array( 'PhabricatorProjectOrUserFunctionDatasource' => 'PhabricatorTypeaheadCompositeDatasource', 'PhabricatorProjectPHIDResolver' => 'PhabricatorPHIDResolver', 'PhabricatorProjectPanelController' => 'PhabricatorProjectController', + 'PhabricatorProjectPointsProfilePanel' => 'PhabricatorProfilePanel', 'PhabricatorProjectProfileController' => 'PhabricatorProjectController', 'PhabricatorProjectProfilePanelEngine' => 'PhabricatorProfilePanelEngine', 'PhabricatorProjectProjectHasMemberEdgeType' => 'PhabricatorEdgeType', diff --git a/src/applications/maniphest/editor/ManiphestEditEngine.php b/src/applications/maniphest/editor/ManiphestEditEngine.php index 644870b6d0..24208bf866 100644 --- a/src/applications/maniphest/editor/ManiphestEditEngine.php +++ b/src/applications/maniphest/editor/ManiphestEditEngine.php @@ -307,28 +307,45 @@ final class ManiphestEditEngine // currently leave the card where it was but should really move it to the // proper new column. + $board_phid = $column->getProjectPHID(); + $descendant_projects = id(new PhabricatorProjectQuery()) ->setViewer($viewer) ->withAncestorProjectPHIDs(array($column->getProjectPHID())) ->execute(); $board_phids = mpull($descendant_projects, 'getPHID', 'getPHID'); - $board_phids[$column->getProjectPHID()] = $column->getProjectPHID(); + $board_phids[$board_phid] = $board_phid; $project_map = array_fuse($task->getProjectPHIDs()); $remove_card = !array_intersect_key($board_phids, $project_map); - $positions = id(new PhabricatorProjectColumnPositionQuery()) + // TODO: Maybe the caller should pass a list of visible task PHIDs so we + // know which ones we need to reorder? This is a HUGE overfetch. + $objects = id(new ManiphestTaskQuery()) + ->setViewer($viewer) + ->withEdgeLogicPHIDs( + PhabricatorProjectObjectHasProjectEdgeType::EDGECONST, + PhabricatorQueryConstraint::OPERATOR_ANCESTOR, + array($board_phids)) ->setViewer($viewer) - ->withBoardPHIDs(array($column->getProjectPHID())) - ->withColumnPHIDs(array($column->getPHID())) ->execute(); - $task_phids = mpull($positions, 'getObjectPHID'); + $objects = mpull($objects, null, 'getPHID'); - $column_tasks = id(new ManiphestTaskQuery()) + $layout_engine = id(new PhabricatorBoardLayoutEngine()) ->setViewer($viewer) - ->withPHIDs($task_phids) - ->needProjectPHIDs(true) - ->execute(); + ->setBoardPHIDs(array($board_phid)) + ->setObjectPHIDs(array_keys($objects)) + ->executeLayout(); + + $positions = $layout_engine->getColumnObjectPositions( + $board_phid, + $column_phid); + + $column_phids = $layout_engine->getColumnObjectPHIDs( + $board_phid, + $column_phid); + + $column_tasks = array_select_keys($objects, $column_phids); if ($order == PhabricatorProjectColumn::ORDER_NATURAL) { // TODO: This is a little bit awkward, because PHP and JS use diff --git a/src/applications/project/engine/PhabricatorBoardLayoutEngine.php b/src/applications/project/engine/PhabricatorBoardLayoutEngine.php index caedee336c..2415a99579 100644 --- a/src/applications/project/engine/PhabricatorBoardLayoutEngine.php +++ b/src/applications/project/engine/PhabricatorBoardLayoutEngine.php @@ -86,9 +86,14 @@ final class PhabricatorBoardLayoutEngine extends Phobject { return array_select_keys($this->columnMap, array_keys($columns)); } - public function getColumnObjectPHIDs($board_phid, $column_phid) { + public function getColumnObjectPositions($board_phid, $column_phid) { $columns = idx($this->boardLayout, $board_phid, array()); - $positions = idx($columns, $column_phid, array()); + return idx($columns, $column_phid, array()); + } + + + public function getColumnObjectPHIDs($board_phid, $column_phid) { + $positions = $this->getColumnObjectPositions($board_phid, $column_phid); return mpull($positions, 'getObjectPHID'); } diff --git a/src/applications/project/engine/PhabricatorProjectProfilePanelEngine.php b/src/applications/project/engine/PhabricatorProjectProfilePanelEngine.php index 58b9b0b1ab..3a34b6c80c 100644 --- a/src/applications/project/engine/PhabricatorProjectProfilePanelEngine.php +++ b/src/applications/project/engine/PhabricatorProjectProfilePanelEngine.php @@ -20,6 +20,10 @@ final class PhabricatorProjectProfilePanelEngine ->setBuiltinKey(PhabricatorProject::PANEL_PROFILE) ->setPanelKey(PhabricatorProjectDetailsProfilePanel::PANELKEY); + $panels[] = $this->newPanel() + ->setBuiltinKey(PhabricatorProject::PANEL_POINTS) + ->setPanelKey(PhabricatorProjectPointsProfilePanel::PANELKEY); + $panels[] = $this->newPanel() ->setBuiltinKey(PhabricatorProject::PANEL_WORKBOARD) ->setPanelKey(PhabricatorProjectWorkboardProfilePanel::PANELKEY); diff --git a/src/applications/project/profilepanel/PhabricatorProjectPointsProfilePanel.php b/src/applications/project/profilepanel/PhabricatorProjectPointsProfilePanel.php new file mode 100644 index 0000000000..02a2f3984f --- /dev/null +++ b/src/applications/project/profilepanel/PhabricatorProjectPointsProfilePanel.php @@ -0,0 +1,192 @@ +getViewer(); + + // Only render this element for milestones. + if (!$object->isMilestone()) { + return false; + } + + // Don't show if points aren't configured. + if (!ManiphestTaskPoints::getIsEnabled()) { + return false; + } + + // Points are only available if Maniphest is installed. + $class = 'PhabricatorManiphestApplication'; + if (!PhabricatorApplication::isClassInstalledForViewer($class, $viewer)) { + return false; + } + + return true; + } + + public function getDisplayName( + PhabricatorProfilePanelConfiguration $config) { + return $this->getDefaultName(); + } + + public function buildEditEngineFields( + PhabricatorProfilePanelConfiguration $config) { + return array( + id(new PhabricatorInstructionsEditField()) + ->setValue( + pht( + 'This is a progress bar which shows how many points of work '. + 'are complete within the milestone. It has no configurable '. + 'settings.')), + ); + } + + protected function newNavigationMenuItems( + PhabricatorProfilePanelConfiguration $config) { + $viewer = $this->getViewer(); + $project = $config->getProfileObject(); + + $limit = 250; + + $tasks = id(new ManiphestTaskQuery()) + ->setViewer($viewer) + ->withEdgeLogicPHIDs( + PhabricatorProjectObjectHasProjectEdgeType::EDGECONST, + PhabricatorQueryConstraint::OPERATOR_AND, + array($project->getPHID())) + ->setLimit($limit + 1) + ->execute(); + + if (count($tasks) > $limit) { + return $this->renderError( + pht( + 'Too many tasks to compute statistics for (more than %s).', + new PhutilNumber($limit))); + } + + if (!$tasks) { + return $this->renderError( + pht( + 'This milestone has no tasks yet.')); + } + + $statuses = array(); + $points_done = 0; + $points_total = 0; + $no_points = 0; + foreach ($tasks as $task) { + $points = $task->getPoints(); + + if ($points === null) { + $no_points++; + continue; + } + + if (!$points) { + continue; + } + + $status = $task->getStatus(); + if (empty($statuses[$status])) { + $statuses[$status] = 0; + } + $statuses[$status] += $points; + + if (ManiphestTaskStatus::isClosedStatus($status)) { + $points_done += $points; + } + + $points_total += $points; + } + + if ($no_points == count($tasks)) { + return $this->renderError( + pht('No tasks have assigned point values.')); + } + + + if (!$points_total) { + return $this->renderError( + pht('All tasks with assigned point values are worth zero points.')); + } + + $label = pht( + '%s of %s %s', + new PhutilNumber($points_done), + new PhutilNumber($points_total), + ManiphestTaskPoints::getPointsLabel()); + + $bar = id(new PHUISegmentBarView()) + ->setLabel($label); + + $map = ManiphestTaskStatus::getTaskStatusMap(); + $statuses = array_select_keys($statuses, array_keys($map)); + + foreach ($statuses as $status => $points) { + if (!$points) { + continue; + } + + if (!ManiphestTaskStatus::isClosedStatus($status)) { + continue; + } + + $color = ManiphestTaskStatus::getStatusColor($status); + if (!$color) { + $color = 'sky'; + } + + $tooltip = pht( + '%s %s', + new PhutilNumber($points), + ManiphestTaskStatus::getTaskStatusName($status)); + + $bar->newSegment() + ->setWidth($points / $points_total) + ->setColor($color) + ->setTooltip($tooltip); + } + + $bar = phutil_tag( + 'div', + array( + 'class' => 'phui-profile-segment-bar', + ), + $bar); + + $item = $this->newItem() + ->appendChild($bar); + + return array( + $item, + ); + } + + private function renderError($message) { + $message = phutil_tag( + 'div', + array( + 'class' => 'phui-profile-menu-error', + ), + $message); + + $item = $this->newItem() + ->appendChild($message); + + return array( + $item, + ); + } + +} diff --git a/src/applications/project/profilepanel/PhabricatorProjectWorkboardProfilePanel.php b/src/applications/project/profilepanel/PhabricatorProjectWorkboardProfilePanel.php index 9bf28cdb31..8bfeeb4cfe 100644 --- a/src/applications/project/profilepanel/PhabricatorProjectWorkboardProfilePanel.php +++ b/src/applications/project/profilepanel/PhabricatorProjectWorkboardProfilePanel.php @@ -18,6 +18,18 @@ final class PhabricatorProjectWorkboardProfilePanel return true; } + public function shouldEnableForObject($object) { + $viewer = $this->getViewer(); + + // Workboards are only available if Maniphest is installed. + $class = 'PhabricatorManiphestApplication'; + if (!PhabricatorApplication::isClassInstalledForViewer($class, $viewer)) { + return false; + } + + return true; + } + public function getDisplayName( PhabricatorProfilePanelConfiguration $config) { $name = $config->getPanelProperty('name'); @@ -42,14 +54,6 @@ final class PhabricatorProjectWorkboardProfilePanel protected function newNavigationMenuItems( PhabricatorProfilePanelConfiguration $config) { - $viewer = $this->getViewer(); - - // Workboards are only available if Maniphest is installed. - $class = 'PhabricatorManiphestApplication'; - if (!PhabricatorApplication::isClassInstalledForViewer($class, $viewer)) { - return array(); - } - $project = $config->getProfileObject(); $has_workboard = $project->getHasWorkboard(); diff --git a/src/applications/project/storage/PhabricatorProject.php b/src/applications/project/storage/PhabricatorProject.php index 093b368ef7..2d8b3f5218 100644 --- a/src/applications/project/storage/PhabricatorProject.php +++ b/src/applications/project/storage/PhabricatorProject.php @@ -48,6 +48,7 @@ final class PhabricatorProject extends PhabricatorProjectDAO const TABLE_DATASOURCE_TOKEN = 'project_datasourcetoken'; const PANEL_PROFILE = 'project.profile'; + const PANEL_POINTS = 'project.points'; const PANEL_WORKBOARD = 'project.workboard'; const PANEL_MEMBERS = 'project.members'; const PANEL_MANAGE = 'project.manage'; diff --git a/src/applications/search/engine/PhabricatorProfilePanelEngine.php b/src/applications/search/engine/PhabricatorProfilePanelEngine.php index e9cea05db7..b92ba451d0 100644 --- a/src/applications/search/engine/PhabricatorProfilePanelEngine.php +++ b/src/applications/search/engine/PhabricatorProfilePanelEngine.php @@ -236,6 +236,11 @@ abstract class PhabricatorProfilePanelEngine extends Phobject { ->withProfilePHIDs(array($object->getPHID())) ->execute(); + foreach ($stored_panels as $stored_panel) { + $impl = $stored_panel->getPanel(); + $impl->setViewer($viewer); + } + // Merge the stored panels into the builtin panels. If a builtin panel has // a stored version, replace the defaults with the stored changes. foreach ($stored_panels as $stored_panel) { @@ -259,12 +264,6 @@ abstract class PhabricatorProfilePanelEngine extends Phobject { } } - foreach ($panels as $panel) { - $impl = $panel->getPanel(); - - $impl->setViewer($viewer); - } - $panels = msort($panels, 'getSortKey'); // Normalize keys since callers shouldn't rely on this array being @@ -306,6 +305,7 @@ abstract class PhabricatorProfilePanelEngine extends Phobject { $builtins = $this->getBuiltinProfilePanels($object); $panels = PhabricatorProfilePanel::getAllPanels(); + $viewer = $this->getViewer(); $order = 1; $map = array(); @@ -339,6 +339,9 @@ abstract class PhabricatorProfilePanelEngine extends Phobject { $panel_key)); } + $panel = clone $panel; + $panel->setViewer($viewer); + $builtin ->setProfilePHID($object->getPHID()) ->attachPanel($panel) diff --git a/src/view/phui/PHUISegmentBarSegmentView.php b/src/view/phui/PHUISegmentBarSegmentView.php index 1b94df0e2a..6bd3c530ef 100644 --- a/src/view/phui/PHUISegmentBarSegmentView.php +++ b/src/view/phui/PHUISegmentBarSegmentView.php @@ -5,6 +5,7 @@ final class PHUISegmentBarSegmentView extends AphrontTagView { private $width; private $color; private $position; + private $tooltip; public function setWidth($width) { $this->width = $width; @@ -25,6 +26,11 @@ final class PHUISegmentBarSegmentView extends AphrontTagView { return $this; } + public function setTooltip($tooltip) { + $this->tooltip = $tooltip; + return $this; + } + protected function canAppendChild() { return false; } @@ -48,9 +54,25 @@ final class PHUISegmentBarSegmentView extends AphrontTagView { $left = floor(100 * $left) / 100; $left = sprintf('%.2f%%', $left); + $tooltip = $this->tooltip; + if (strlen($tooltip)) { + Javelin::initBehavior('phabricator-tooltips'); + + $sigil = 'has-tooltip'; + $meta = array( + 'tip' => $tooltip, + 'align' => 'E', + ); + } else { + $sigil = null; + $meta = null; + } + return array( 'class' => implode(' ', $classes), 'style' => "left: {$left}; width: {$width};", + 'sigil' => $sigil, + 'meta' => $meta, ); } diff --git a/webroot/rsrc/css/phui/phui-profile-menu.css b/webroot/rsrc/css/phui/phui-profile-menu.css index cc98269a53..d1704feeee 100644 --- a/webroot/rsrc/css/phui/phui-profile-menu.css +++ b/webroot/rsrc/css/phui/phui-profile-menu.css @@ -149,6 +149,18 @@ color: {$menu.profile.text}; } +.phui-profile-menu .phabricator-side-menu .phui-profile-menu-error { + color: {$greytext}; + font-size: {$smallerfontsize}; + padding: 18px 15px; +} + +.phui-profile-menu .phabricator-side-menu .phui-profile-segment-bar { + color: {$menu.profile.text}; + padding: 12px 15px 18px; +} + + .phui-profile-menu .phabricator-side-menu .phui-profile-menu-spacer { box-sizing: border-box; height: {$menu.profile.item.height}; diff --git a/webroot/rsrc/css/phui/phui-segment-bar-view.css b/webroot/rsrc/css/phui/phui-segment-bar-view.css index c965257268..c0ee6a27a8 100644 --- a/webroot/rsrc/css/phui/phui-segment-bar-view.css +++ b/webroot/rsrc/css/phui/phui-segment-bar-view.css @@ -20,7 +20,7 @@ position: absolute; top: 0; bottom: 0; - margin-left: -4px; + margin-left: -5px; border-right: 5px solid; border-radius: 0 4px 4px 0; } From 735b722cb25690c53c42b0cac340850898fd3efa Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 9 Feb 2016 04:17:35 -0800 Subject: [PATCH 25/65] Fix a bad parameter in a parent::shouldHideForMail() call in Maniphest Fixes T10303. Auditors: chad --- src/applications/maniphest/storage/ManiphestTransaction.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/applications/maniphest/storage/ManiphestTransaction.php b/src/applications/maniphest/storage/ManiphestTransaction.php index 2a4584c49f..55d14083e6 100644 --- a/src/applications/maniphest/storage/ManiphestTransaction.php +++ b/src/applications/maniphest/storage/ManiphestTransaction.php @@ -182,7 +182,7 @@ final class ManiphestTransaction return true; } - return parent::shouldHideForMail(); + return parent::shouldHideForMail($xactions); } public function shouldHideForFeed() { From 55f607e397e0b240de5518ad0374b591628e2296 Mon Sep 17 00:00:00 2001 From: Chad Little Date: Tue, 9 Feb 2016 07:23:53 -0800 Subject: [PATCH 26/65] Minor touchups to dialogs Summary: Better margins, top and bottom. Fixed width on really wide screens. Test Plan: Test mobile, tablet, desktop editing a workboard card. Reviewers: epriestley Reviewed By: epriestley Subscribers: Korvin Differential Revision: https://secure.phabricator.com/D15222 --- resources/celerity/map.php | 6 +++--- webroot/rsrc/css/aphront/dialog-view.css | 4 +++- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/resources/celerity/map.php b/resources/celerity/map.php index c995903991..9a5263fc8c 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -7,7 +7,7 @@ */ return array( 'names' => array( - 'core.pkg.css' => 'bef9c7cb', + 'core.pkg.css' => 'e55b8873', 'core.pkg.js' => '17380dd3', 'darkconsole.pkg.js' => 'e7393ebb', 'differential.pkg.css' => '2de124c9', @@ -18,7 +18,7 @@ return array( 'maniphest.pkg.js' => '949a7498', 'rsrc/css/aphront/aphront-bars.css' => '231ac33c', 'rsrc/css/aphront/dark-console.css' => '6378ef3d', - 'rsrc/css/aphront/dialog-view.css' => 'be0e3a46', + 'rsrc/css/aphront/dialog-view.css' => 'e2f4919b', 'rsrc/css/aphront/lightbox-attachment.css' => '7acac05d', 'rsrc/css/aphront/list-filter-view.css' => '5d6f0526', 'rsrc/css/aphront/multi-column.css' => 'fd18389d', @@ -514,7 +514,7 @@ return array( 'almanac-css' => 'dbb9b3af', 'aphront-bars' => '231ac33c', 'aphront-dark-console-css' => '6378ef3d', - 'aphront-dialog-view-css' => 'be0e3a46', + 'aphront-dialog-view-css' => 'e2f4919b', 'aphront-list-filter-view-css' => '5d6f0526', 'aphront-multi-column-view-css' => 'fd18389d', 'aphront-panel-view-css' => '8427b78d', diff --git a/webroot/rsrc/css/aphront/dialog-view.css b/webroot/rsrc/css/aphront/dialog-view.css index 3043400357..c83ab931e6 100644 --- a/webroot/rsrc/css/aphront/dialog-view.css +++ b/webroot/rsrc/css/aphront/dialog-view.css @@ -8,6 +8,7 @@ border: 1px solid {$lightblueborder}; border-radius: 3px; background-color: #fff; + box-shadow: {$dropshadow}; } .device-phone .aphront-dialog-view { @@ -16,7 +17,7 @@ } .aphront-dialog-view-standalone { - margin: auto; + margin: 32px auto; } .aphront-dialog-head { @@ -33,6 +34,7 @@ .aphront-dialog-view-width-full { width: 90%; + max-width: 1040px; } .aphront-dialog-body { From 4c1140a3a78944813beaf249b49aea961d1fa1c8 Mon Sep 17 00:00:00 2001 From: Aviv Eyal Date: Tue, 9 Feb 2016 18:20:22 +0000 Subject: [PATCH 27/65] unbreak exception handling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary: fix T10306 Test Plan: ¯\_(ツ)_/¯ Reviewers: chad, #blessed_reviewers, epriestley Reviewed By: #blessed_reviewers, epriestley Subscribers: Korvin Maniphest Tasks: T10306 Differential Revision: https://secure.phabricator.com/D15229 --- .../diffusion/controller/DiffusionServeController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/applications/diffusion/controller/DiffusionServeController.php b/src/applications/diffusion/controller/DiffusionServeController.php index 8f3eb364f6..8d131eca6d 100644 --- a/src/applications/diffusion/controller/DiffusionServeController.php +++ b/src/applications/diffusion/controller/DiffusionServeController.php @@ -100,7 +100,7 @@ final class DiffusionServeController extends DiffusionController { ->setResultCode(500) ->setProperties( array( - 'exception.class' => $ex->getClass(), + 'exception.class' => get_class($ex), 'exception.message' => $ex->getMessage(), )); } From aa6c993848797d792a886b4a570f20ca897ade7e Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 9 Feb 2016 04:28:22 -0800 Subject: [PATCH 28/65] Fix two minor points UI issues Summary: Ref T4427. - When points are configured, show them on the task detail page (just a simple property, at least for now). - Typecast points better to avoid "joe changed points from 12 to 12." beacuse we compare the stored value (as a string) to the new value (as a double). Test Plan: - Saw points on detail view. - Created task with points, then edited it without touching points. No more spurious "changed from 12 to 12" transaction. Reviewers: chad Reviewed By: chad Maniphest Tasks: T4427 Differential Revision: https://secure.phabricator.com/D15223 --- .../controller/ManiphestTaskDetailController.php | 9 +++++++++ .../maniphest/editor/ManiphestTransactionEditor.php | 6 +++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/applications/maniphest/controller/ManiphestTaskDetailController.php b/src/applications/maniphest/controller/ManiphestTaskDetailController.php index 6ca5ab2946..a15b8f4594 100644 --- a/src/applications/maniphest/controller/ManiphestTaskDetailController.php +++ b/src/applications/maniphest/controller/ManiphestTaskDetailController.php @@ -229,6 +229,15 @@ final class ManiphestTaskDetailController extends ManiphestController { $view->addProperty(pht('Author'), $author); + if (ManiphestTaskPoints::getIsEnabled()) { + $points = $task->getPoints(); + if ($points !== null) { + $view->addProperty( + ManiphestTaskPoints::getPointsLabel(), + $task->getPoints()); + } + } + $source = $task->getOriginalEmailSource(); if ($source) { $subject = '[T'.$task->getID().'] '.$task->getTitle(); diff --git a/src/applications/maniphest/editor/ManiphestTransactionEditor.php b/src/applications/maniphest/editor/ManiphestTransactionEditor.php index 073bf87bea..922317b206 100644 --- a/src/applications/maniphest/editor/ManiphestTransactionEditor.php +++ b/src/applications/maniphest/editor/ManiphestTransactionEditor.php @@ -71,7 +71,11 @@ final class ManiphestTransactionEditor case ManiphestTransaction::TYPE_COVER_IMAGE: return $object->getCoverImageFilePHID(); case ManiphestTransaction::TYPE_POINTS: - return $object->getPoints(); + $points = $object->getPoints(); + if ($points !== null) { + $points = (double)$points; + } + return $points; case ManiphestTransaction::TYPE_MERGED_INTO: case ManiphestTransaction::TYPE_MERGED_FROM: return null; From c2502f1bebbf65cf7ea64ca7e1849ffb402e0319 Mon Sep 17 00:00:00 2001 From: Chad Little Date: Wed, 10 Feb 2016 15:31:02 +0000 Subject: [PATCH 29/65] Add fullscreen mode to Workboards Summary: Cleans up Crumb Actions on Workboards. Adds fullscreen option. Fixes T6066 Test Plan: Clicky Clicky. Test icon does not display on tablet, mobile. {F1102458} Reviewers: epriestley Reviewed By: epriestley Subscribers: Korvin Maniphest Tasks: T6066 Differential Revision: https://secure.phabricator.com/D15230 --- resources/celerity/map.php | 10 +++--- .../PhabricatorProjectBoardViewController.php | 31 ++++++++++++++++-- src/view/phui/PHUICrumbsView.php | 32 ++++++++++++++----- webroot/rsrc/css/phui/phui-crumbs-view.css | 15 +++++++-- .../css/phui/workboards/phui-workboard.css | 25 +++++++++++++++ 5 files changed, 95 insertions(+), 18 deletions(-) diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 9a5263fc8c..664d5b8e2e 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -7,7 +7,7 @@ */ return array( 'names' => array( - 'core.pkg.css' => 'e55b8873', + 'core.pkg.css' => 'e8763436', 'core.pkg.js' => '17380dd3', 'darkconsole.pkg.js' => 'e7393ebb', 'differential.pkg.css' => '2de124c9', @@ -126,7 +126,7 @@ return array( 'rsrc/css/phui/phui-box.css' => '6e8ac7fd', 'rsrc/css/phui/phui-button.css' => 'd6ac72db', 'rsrc/css/phui/phui-chart.css' => '6bf6f78e', - 'rsrc/css/phui/phui-crumbs-view.css' => '414406b5', + 'rsrc/css/phui/phui-crumbs-view.css' => '79d536e5', 'rsrc/css/phui/phui-document-pro.css' => '8799acf7', 'rsrc/css/phui/phui-document-summary.css' => '9ca48bdf', 'rsrc/css/phui/phui-document.css' => '9c71d2bf', @@ -155,7 +155,7 @@ return array( 'rsrc/css/phui/phui-tag-view.css' => '9d5d4400', 'rsrc/css/phui/phui-timeline-view.css' => '2efceff8', 'rsrc/css/phui/phui-two-column-view.css' => 'c75bfc5b', - 'rsrc/css/phui/workboards/phui-workboard.css' => '0047084f', + 'rsrc/css/phui/workboards/phui-workboard.css' => 'e9e56029', 'rsrc/css/phui/workboards/phui-workcard.css' => '3646fb96', 'rsrc/css/phui/workboards/phui-workpanel.css' => 'a78c0661', 'rsrc/css/sprite-login.css' => '60e8560e', @@ -800,7 +800,7 @@ return array( 'phui-calendar-list-css' => 'c1c7f338', 'phui-calendar-month-css' => '476be7e0', 'phui-chart-css' => '6bf6f78e', - 'phui-crumbs-view-css' => '414406b5', + 'phui-crumbs-view-css' => '79d536e5', 'phui-document-summary-view-css' => '9ca48bdf', 'phui-document-view-css' => '9c71d2bf', 'phui-document-view-pro-css' => '8799acf7', @@ -833,7 +833,7 @@ return array( 'phui-theme-css' => 'ab7b848c', 'phui-timeline-view-css' => '2efceff8', 'phui-two-column-view-css' => 'c75bfc5b', - 'phui-workboard-view-css' => '0047084f', + 'phui-workboard-view-css' => 'e9e56029', 'phui-workcard-view-css' => '3646fb96', 'phui-workpanel-view-css' => 'a78c0661', 'phuix-action-list-view' => 'b5c256b8', diff --git a/src/applications/project/controller/PhabricatorProjectBoardViewController.php b/src/applications/project/controller/PhabricatorProjectBoardViewController.php index ffedb6b17f..ca5132388d 100644 --- a/src/applications/project/controller/PhabricatorProjectBoardViewController.php +++ b/src/applications/project/controller/PhabricatorProjectBoardViewController.php @@ -373,6 +373,9 @@ final class PhabricatorProjectBoardViewController ->addClass('project-board-wrapper'); $nav = $this->getProfileMenu(); + $divider = id(new PHUIListItemView()) + ->setType(PHUIListItemView::TYPE_DIVIDER); + $fullscreen = $this->buildFullscreenMenu(); $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb(pht('Workboard')); @@ -380,7 +383,9 @@ final class PhabricatorProjectBoardViewController $crumbs->addAction($sort_menu); $crumbs->addAction($filter_menu); + $crumbs->addAction($divider); $crumbs->addAction($manage_menu); + $crumbs->addAction($fullscreen); return $this->newPage() ->setTitle( @@ -460,7 +465,7 @@ final class PhabricatorProjectBoardViewController } $sort_button = id(new PHUIListItemView()) - ->setName(pht('Sort: %s', $active_order)) + ->setName($active_order) ->setIcon('fa-sort-amount-asc') ->setHref('#') ->addSigil('boards-dropdown-menu') @@ -534,7 +539,7 @@ final class PhabricatorProjectBoardViewController } $filter_button = id(new PHUIListItemView()) - ->setName(pht('Filter: %s', $active_filter)) + ->setName($active_filter) ->setIcon('fa-search') ->setHref('#') ->addSigil('boards-dropdown-menu') @@ -624,18 +629,38 @@ final class PhabricatorProjectBoardViewController } $manage_button = id(new PHUIListItemView()) - ->setName(pht('Manage Board')) ->setIcon('fa-cog') ->setHref('#') ->addSigil('boards-dropdown-menu') + ->addSigil('has-tooltip') ->setMetadata( array( + 'tip' => pht('Manage'), + 'align' => 'S', 'items' => hsprintf('%s', $manage_menu), )); return $manage_button; } + private function buildFullscreenMenu() { + + $up = id(new PHUIListItemView()) + ->setIcon('fa-arrows-alt') + ->setHref('#') + ->addClass('phui-workboard-expand-icon') + ->addSigil('jx-toggle-class') + ->addSigil('has-tooltip') + ->setMetaData(array( + 'tip' => pht('Fullscreen'), + 'map' => array( + 'phabricator-standard-page' => 'phui-workboard-fullscreen', + ), + )); + + return $up; + } + private function buildColumnMenu( PhabricatorProject $project, PhabricatorProjectColumn $column) { diff --git a/src/view/phui/PHUICrumbsView.php b/src/view/phui/PHUICrumbsView.php index c85c0055d1..00a1f1911e 100644 --- a/src/view/phui/PHUICrumbsView.php +++ b/src/view/phui/PHUICrumbsView.php @@ -52,6 +52,15 @@ final class PHUICrumbsView extends AphrontView { if ($this->actions) { $actions = array(); foreach ($this->actions as $action) { + if ($action->getType() == PHUIListItemView::TYPE_DIVIDER) { + $actions[] = phutil_tag( + 'span', + array( + 'class' => 'phui-crumb-action-divider', + )); + continue; + } + $icon = null; if ($action->getIcon()) { $icon_name = $action->getIcon(); @@ -63,19 +72,26 @@ final class PHUICrumbsView extends AphrontView { ->setIcon($icon_name); } - $name = phutil_tag( - 'span', - array( - 'class' => 'phui-crumbs-action-name', - ), - $action->getName()); + + $action_classes = $action->getClasses(); + $action_classes[] = 'phui-crumbs-action'; + + $name = null; + if ($action->getName()) { + $name = phutil_tag( + 'span', + array( + 'class' => 'phui-crumbs-action-name', + ), + $action->getName()); + } else { + $action_classes[] = 'phui-crumbs-action-icon'; + } $action_sigils = $action->getSigils(); if ($action->getWorkflow()) { $action_sigils[] = 'workflow'; } - $action_classes = $action->getClasses(); - $action_classes[] = 'phui-crumbs-action'; if ($action->getDisabled()) { $action_classes[] = 'phui-crumbs-action-disabled'; diff --git a/webroot/rsrc/css/phui/phui-crumbs-view.css b/webroot/rsrc/css/phui/phui-crumbs-view.css index 71113dadc9..81c8471c4f 100644 --- a/webroot/rsrc/css/phui/phui-crumbs-view.css +++ b/webroot/rsrc/css/phui/phui-crumbs-view.css @@ -89,12 +89,15 @@ } a.phui-crumbs-action .phui-icon-view { - margin-right: 5px; color: {$darkbluetext}; } +a.phui-crumbs-action .phui-crumbs-action-name { + margin-left: 6px; +} + .device-phone a.phui-crumbs-action .phui-icon-view { - margin-left: 5px; + margin-left: 4px; } .phui-crumb-divider { @@ -112,3 +115,11 @@ body .phui-crumbs-view + .phui-object-box { body .phui-crumbs-view + .phui-object-item-list-view { padding-top: 0; } + +.phui-crumb-action-divider { + border-left: 1px solid {$lightgreyborder}; +} + +.phui-crumbs-action-icon + .phui-crumbs-action-icon { + padding-left: 4px; +} diff --git a/webroot/rsrc/css/phui/workboards/phui-workboard.css b/webroot/rsrc/css/phui/workboards/phui-workboard.css index d2652cc45e..36913d1663 100644 --- a/webroot/rsrc/css/phui/workboards/phui-workboard.css +++ b/webroot/rsrc/css/phui/workboards/phui-workboard.css @@ -80,3 +80,28 @@ pointer-events: auto; cursor: auto; } + +/* Fullscreen */ + +.device-desktop .phui-workboard-fullscreen .phabricator-main-menu { + display: none; +} + +.device-desktop .phui-workboard-fullscreen .phui-profile-menu + .phui-workboard-view-shadow { + top: 35px; + left: 0; +} + +.device-desktop .phui-workboard-fullscreen .phui-workpanel-body-content { + max-height: calc(100vh - 120px); +} + +.device-desktop .phui-workboard-fullscreen .phui-profile-menu + .phabricator-nav-local { + display: none; +} + +.device .phui-workboard-expand-icon { + display: none; +} From 7bca452fad1b27533037f9695b538e0572df6ee3 Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 10 Feb 2016 05:19:02 -0800 Subject: [PATCH 30/65] Fix two issues with unusual milestone workboard initialization orders Summary: Fixes T10316. Fixes T10311. Test Plan: - Create a project, create a milestone, go back to the parent, go to the workboard. Previously, fatal. Now, prompts you to create workboard. - Create a project, create a milestone, create the parent workboard, put a task in the milestone's column, go to the milestone workboard. Previously, fatal. Now, prompts you to create workboard. Reviewers: chad Reviewed By: chad Maniphest Tasks: T10311, T10316 Differential Revision: https://secure.phabricator.com/D15232 --- .../PhabricatorProjectBoardViewController.php | 12 +++++++++++- .../project/engine/PhabricatorBoardLayoutEngine.php | 13 ++++++++++--- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/src/applications/project/controller/PhabricatorProjectBoardViewController.php b/src/applications/project/controller/PhabricatorProjectBoardViewController.php index ca5132388d..3a46a262f6 100644 --- a/src/applications/project/controller/PhabricatorProjectBoardViewController.php +++ b/src/applications/project/controller/PhabricatorProjectBoardViewController.php @@ -126,11 +126,21 @@ final class PhabricatorProjectBoardViewController $columns = $layout_engine->getColumns($board_phid); if (!$columns || !$project->getHasWorkboard()) { + $has_normal_columns = false; + + foreach ($columns as $column) { + if (!$column->getProxyPHID()) { + $has_normal_columns = true; + break; + } + } + $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $project, PhabricatorPolicyCapability::CAN_EDIT); - if (!$columns) { + + if (!$has_normal_columns) { if (!$can_edit) { $content = $this->buildNoAccessContent($project); } else { diff --git a/src/applications/project/engine/PhabricatorBoardLayoutEngine.php b/src/applications/project/engine/PhabricatorBoardLayoutEngine.php index 2415a99579..3525af5ff0 100644 --- a/src/applications/project/engine/PhabricatorBoardLayoutEngine.php +++ b/src/applications/project/engine/PhabricatorBoardLayoutEngine.php @@ -366,7 +366,12 @@ final class PhabricatorBoardLayoutEngine extends Phobject { if ($board->getHasMilestones() || $board->getHasSubprojects()) { $child_projects = idx($children, $board_phid, array()); - $next_sequence = last($board_columns)->getSequence() + 1; + if ($board_columns) { + $next_sequence = last($board_columns)->getSequence() + 1; + } else { + $next_sequence = 1; + } + $proxy_columns = mpull($board_columns, null, 'getProxyPHID'); foreach ($child_projects as $child_phid => $child) { if (isset($proxy_columns[$child_phid])) { @@ -433,6 +438,7 @@ final class PhabricatorBoardLayoutEngine extends Phobject { $position_groups = mgroup($positions, 'getObjectPHID'); $layout = array(); + $default_phid = null; foreach ($columns as $column) { $column_phid = $column->getPHID(); $layout[$column_phid] = array(); @@ -565,8 +571,9 @@ final class PhabricatorBoardLayoutEngine extends Phobject { } } - // If the object has no position, put it on the default column. - if (!$positions) { + // If the object has no position, put it on the default column if + // one exists. + if (!$positions && $default_phid) { $new_position = id(new PhabricatorProjectColumnPosition()) ->setBoardPHID($board_phid) ->setColumnPHID($default_phid) From 3f920d8904745e12e7771d98ebc9d0d7c5a4a8d5 Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 10 Feb 2016 12:34:43 -0800 Subject: [PATCH 31/65] Fix method call on possible null Summary: Fixes T10317. If we failed to load `$device`, it will be null, so we won't be able to call `getName()` on it. Test Plan: `SSH_CLIENT='127.0.0.1' ./bin/ssh-exec --phabricator-ssh-device xyz` no longer fatals Reviewers: chad Reviewed By: chad Maniphest Tasks: T10317 Differential Revision: https://secure.phabricator.com/D15235 --- scripts/ssh/ssh-exec.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/ssh/ssh-exec.php b/scripts/ssh/ssh-exec.php index 45dacddebd..2238bab478 100755 --- a/scripts/ssh/ssh-exec.php +++ b/scripts/ssh/ssh-exec.php @@ -129,7 +129,7 @@ try { throw new Exception( pht( 'Invalid device name ("%s"). There is no device with this name.', - $device->getName())); + $device_name)); } // We're authenticated as a device, but we're going to read the user out of From 9bca1a56da65581182e041fbd91db91dcdc3828e Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 9 Feb 2016 04:47:25 -0800 Subject: [PATCH 32/65] Begin generalizing Javascript for Workboard state handling Summary: Ref T4900. Broadly, workboard state management is fairly ad-hoc now, which makes things like this (where some kind of edit affects global state) difficult: - Updating points header to reflect a sum change after dragging a task. - Changing progress bars after editing a task to change resolution or points value. - Moving a card to the correct column after editing it and changing subprojects/iterations. - Responding to real-time notifications about other users moving cards. This begins rewriting the code in a way that can better accommodate these kinds of far-reaching state update. This change just moves cover image stuff. I'll continue moving features one at a time until boards work better. Test Plan: Updated some cover images. Reviewers: chad Reviewed By: chad Maniphest Tasks: T4900 Differential Revision: https://secure.phabricator.com/D15224 --- resources/celerity/map.php | 19 ++++- .../PhabricatorProjectBoardViewController.php | 7 +- .../rsrc/js/application/projects/Workboard.js | 79 +++++++++++++++++++ .../projects/behavior-project-boards.js | 39 ++------- 4 files changed, 108 insertions(+), 36 deletions(-) create mode 100644 webroot/rsrc/js/application/projects/Workboard.js diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 664d5b8e2e..dfb3cd864f 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -415,7 +415,8 @@ return array( 'rsrc/js/application/phortune/phortune-credit-card-form.js' => '2290aeef', 'rsrc/js/application/policy/behavior-policy-control.js' => 'd0c516d5', 'rsrc/js/application/policy/behavior-policy-rule-editor.js' => '5e9f347c', - 'rsrc/js/application/projects/behavior-project-boards.js' => 'a1807fd7', + 'rsrc/js/application/projects/Workboard.js' => '761753ee', + 'rsrc/js/application/projects/behavior-project-boards.js' => 'a1b41573', 'rsrc/js/application/projects/behavior-project-create.js' => '065227cc', 'rsrc/js/application/projects/behavior-reorder-columns.js' => 'e1d25dfb', 'rsrc/js/application/releeph/releeph-preview-branch.js' => 'b2b4fbaf', @@ -655,7 +656,7 @@ return array( 'javelin-behavior-phui-profile-menu' => '12884df9', 'javelin-behavior-policy-control' => 'd0c516d5', 'javelin-behavior-policy-rule-editor' => '5e9f347c', - 'javelin-behavior-project-boards' => 'a1807fd7', + 'javelin-behavior-project-boards' => 'a1b41573', 'javelin-behavior-project-create' => '065227cc', 'javelin-behavior-quicksand-blacklist' => '7927a7d3', 'javelin-behavior-recurring-edit' => '5f1c4d5f', @@ -722,6 +723,7 @@ return array( 'javelin-view-renderer' => '6c2b09a2', 'javelin-view-visitor' => 'efe49472', 'javelin-websocket' => 'e292eaf4', + 'javelin-workboard' => '761753ee', 'javelin-workflow' => '5b2e3e2b', 'lightbox-attachment-css' => '7acac05d', 'maniphest-batch-editor' => 'b0f0b6d5', @@ -1393,6 +1395,16 @@ return array( 'javelin-json', 'phabricator-prefab', ), + '761753ee' => array( + 'javelin-install', + 'javelin-dom', + 'javelin-util', + 'javelin-vector', + 'javelin-stratcom', + 'javelin-workflow', + 'phabricator-draggable-list', + 'phabricator-drag-and-drop-file-upload', + ), '76b9fc3e' => array( 'javelin-behavior', 'javelin-stratcom', @@ -1604,7 +1616,7 @@ return array( 'javelin-dom', 'javelin-reactor-dom', ), - 'a1807fd7' => array( + 'a1b41573' => array( 'javelin-behavior', 'javelin-dom', 'javelin-util', @@ -1613,6 +1625,7 @@ return array( 'javelin-workflow', 'phabricator-draggable-list', 'phabricator-drag-and-drop-file-upload', + 'javelin-workboard', ), 'a2828756' => array( 'javelin-dom', diff --git a/src/applications/project/controller/PhabricatorProjectBoardViewController.php b/src/applications/project/controller/PhabricatorProjectBoardViewController.php index 3a46a262f6..f2eba7d9e8 100644 --- a/src/applications/project/controller/PhabricatorProjectBoardViewController.php +++ b/src/applications/project/controller/PhabricatorProjectBoardViewController.php @@ -231,7 +231,12 @@ final class PhabricatorProjectBoardViewController $board = id(new PHUIWorkboardView()) ->setUser($viewer) - ->setID($board_id); + ->setID($board_id) + ->addSigil('jx-workboard') + ->setMetadata( + array( + 'boardPHID' => $project->getPHID(), + )); $behavior_config = array( 'boardID' => $board_id, diff --git a/webroot/rsrc/js/application/projects/Workboard.js b/webroot/rsrc/js/application/projects/Workboard.js new file mode 100644 index 0000000000..a05cd22122 --- /dev/null +++ b/webroot/rsrc/js/application/projects/Workboard.js @@ -0,0 +1,79 @@ +/** + * @provides javelin-workboard + * @requires javelin-install + * javelin-dom + * javelin-util + * javelin-vector + * javelin-stratcom + * javelin-workflow + * phabricator-draggable-list + * phabricator-drag-and-drop-file-upload + * @javelin + */ + +JX.install('Workboard', { + + construct: function(config) { + this._config = config; + + this._boardNodes = {}; + + this._setupCoverImageHandlers(); + }, + + members: { + _config: null, + _boardNodes: null, + _currentBoard: null, + + addBoard: function(board_phid, board_node) { + this._currentBoard = board_phid; + this._boardNodes[board_phid] = board_node; + }, + + _getConfig: function() { + return this._config; + }, + + _setupCoverImageHandlers: function() { + if (!JX.PhabricatorDragAndDropFileUpload.isSupported()) { + return; + } + + var config = this._getConfig(); + + var drop = new JX.PhabricatorDragAndDropFileUpload('project-card') + .setURI(config.uploadURI) + .setChunkThreshold(config.chunkThreshold); + + drop.listen('didBeginDrag', function(node) { + JX.DOM.alterClass(node, 'phui-workcard-upload-target', true); + }); + + drop.listen('didEndDrag', function(node) { + JX.DOM.alterClass(node, 'phui-workcard-upload-target', false); + }); + + drop.listen('didUpload', function(file) { + var node = file.getTargetNode(); + + var board = JX.DOM.findAbove(node, 'div', 'jx-workboard'); + + var data = { + boardPHID: JX.Stratcom.getData(board).boardPHID, + objectPHID: JX.Stratcom.getData(node).objectPHID, + filePHID: file.getPHID() + }; + + new JX.Workflow(config.coverURI, data) + .setHandler(function(r) { + JX.DOM.replace(node, JX.$H(r.task)); + }) + .start(); + }); + + drop.start(); + } + } + +}); diff --git a/webroot/rsrc/js/application/projects/behavior-project-boards.js b/webroot/rsrc/js/application/projects/behavior-project-boards.js index 86230f3b6a..8f2ca9f0d5 100644 --- a/webroot/rsrc/js/application/projects/behavior-project-boards.js +++ b/webroot/rsrc/js/application/projects/behavior-project-boards.js @@ -8,6 +8,7 @@ * javelin-workflow * phabricator-draggable-list * phabricator-drag-and-drop-file-upload + * javelin-workboard */ JX.behavior('project-boards', function(config, statics) { @@ -350,38 +351,6 @@ JX.behavior('project-boards', function(config, statics) { } }); - if (JX.PhabricatorDragAndDropFileUpload.isSupported()) { - var drop = new JX.PhabricatorDragAndDropFileUpload('project-card') - .setURI(config.uploadURI) - .setChunkThreshold(config.chunkThreshold); - - drop.listen('didBeginDrag', function(node) { - JX.DOM.alterClass(node, 'phui-workcard-upload-target', true); - }); - - drop.listen('didEndDrag', function(node) { - JX.DOM.alterClass(node, 'phui-workcard-upload-target', false); - }); - - drop.listen('didUpload', function(file) { - var node = file.getTargetNode(); - - var data = { - boardPHID: statics.projectPHID, - objectPHID: JX.Stratcom.getData(node).objectPHID, - filePHID: file.getPHID() - }; - - new JX.Workflow(config.coverURI, data) - .setHandler(function(r) { - JX.DOM.replace(node, JX.$H(r.task)); - }) - .start(); - }); - - drop.start(); - } - // When the user drags the workboard background, pan the workboard // horizontally. This allows you to scroll across cards with only the // mouse, without shift + scrollwheel or using the scrollbar. @@ -435,4 +404,10 @@ JX.behavior('project-boards', function(config, statics) { statics.setup = setup(); } + if (!statics.workboard) { + statics.workboard = new JX.Workboard(config); + } + + statics.workboard.addBoard(config.projectPHID, JX.$(config.boardID)); + }); From 3309ae1a304f915feaa194ea55efe354c9c72d8e Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 9 Feb 2016 05:03:54 -0800 Subject: [PATCH 33/65] Move workboard panning to new Workboard code Summary: Ref T4900. Continuing to move this over into a more structured approach. Test Plan: Panned a workboard. Reviewers: chad Reviewed By: chad Maniphest Tasks: T4900 Differential Revision: https://secure.phabricator.com/D15225 --- resources/celerity/map.php | 50 +++++++++---------- .../rsrc/js/application/projects/Workboard.js | 49 ++++++++++++++++++ .../projects/behavior-project-boards.js | 41 --------------- 3 files changed, 74 insertions(+), 66 deletions(-) diff --git a/resources/celerity/map.php b/resources/celerity/map.php index dfb3cd864f..464d3e1b74 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -415,8 +415,8 @@ return array( 'rsrc/js/application/phortune/phortune-credit-card-form.js' => '2290aeef', 'rsrc/js/application/policy/behavior-policy-control.js' => 'd0c516d5', 'rsrc/js/application/policy/behavior-policy-rule-editor.js' => '5e9f347c', - 'rsrc/js/application/projects/Workboard.js' => '761753ee', - 'rsrc/js/application/projects/behavior-project-boards.js' => 'a1b41573', + 'rsrc/js/application/projects/Workboard.js' => '555f521a', + 'rsrc/js/application/projects/behavior-project-boards.js' => '9ed304e5', 'rsrc/js/application/projects/behavior-project-create.js' => '065227cc', 'rsrc/js/application/projects/behavior-reorder-columns.js' => 'e1d25dfb', 'rsrc/js/application/releeph/releeph-preview-branch.js' => 'b2b4fbaf', @@ -656,7 +656,7 @@ return array( 'javelin-behavior-phui-profile-menu' => '12884df9', 'javelin-behavior-policy-control' => 'd0c516d5', 'javelin-behavior-policy-rule-editor' => '5e9f347c', - 'javelin-behavior-project-boards' => 'a1b41573', + 'javelin-behavior-project-boards' => '9ed304e5', 'javelin-behavior-project-create' => '065227cc', 'javelin-behavior-quicksand-blacklist' => '7927a7d3', 'javelin-behavior-recurring-edit' => '5f1c4d5f', @@ -723,7 +723,7 @@ return array( 'javelin-view-renderer' => '6c2b09a2', 'javelin-view-visitor' => 'efe49472', 'javelin-websocket' => 'e292eaf4', - 'javelin-workboard' => '761753ee', + 'javelin-workboard' => '555f521a', 'javelin-workflow' => '5b2e3e2b', 'lightbox-attachment-css' => '7acac05d', 'maniphest-batch-editor' => 'b0f0b6d5', @@ -1223,6 +1223,16 @@ return array( 'javelin-request', 'javelin-typeahead-source', ), + '555f521a' => array( + 'javelin-install', + 'javelin-dom', + 'javelin-util', + 'javelin-vector', + 'javelin-stratcom', + 'javelin-workflow', + 'phabricator-draggable-list', + 'phabricator-drag-and-drop-file-upload', + ), '558829c2' => array( 'javelin-stratcom', 'javelin-behavior', @@ -1395,16 +1405,6 @@ return array( 'javelin-json', 'phabricator-prefab', ), - '761753ee' => array( - 'javelin-install', - 'javelin-dom', - 'javelin-util', - 'javelin-vector', - 'javelin-stratcom', - 'javelin-workflow', - 'phabricator-draggable-list', - 'phabricator-drag-and-drop-file-upload', - ), '76b9fc3e' => array( 'javelin-behavior', 'javelin-stratcom', @@ -1597,6 +1597,17 @@ return array( 'javelin-dom', 'javelin-vector', ), + '9ed304e5' => array( + 'javelin-behavior', + 'javelin-dom', + 'javelin-util', + 'javelin-vector', + 'javelin-stratcom', + 'javelin-workflow', + 'phabricator-draggable-list', + 'phabricator-drag-and-drop-file-upload', + 'javelin-workboard', + ), '9f36c42d' => array( 'javelin-behavior', 'javelin-stratcom', @@ -1616,17 +1627,6 @@ return array( 'javelin-dom', 'javelin-reactor-dom', ), - 'a1b41573' => array( - 'javelin-behavior', - 'javelin-dom', - 'javelin-util', - 'javelin-vector', - 'javelin-stratcom', - 'javelin-workflow', - 'phabricator-draggable-list', - 'phabricator-drag-and-drop-file-upload', - 'javelin-workboard', - ), 'a2828756' => array( 'javelin-dom', 'javelin-util', diff --git a/webroot/rsrc/js/application/projects/Workboard.js b/webroot/rsrc/js/application/projects/Workboard.js index a05cd22122..7fbeaaf85a 100644 --- a/webroot/rsrc/js/application/projects/Workboard.js +++ b/webroot/rsrc/js/application/projects/Workboard.js @@ -19,6 +19,7 @@ JX.install('Workboard', { this._boardNodes = {}; this._setupCoverImageHandlers(); + this._setupPanHandlers(); }, members: { @@ -26,6 +27,10 @@ JX.install('Workboard', { _boardNodes: null, _currentBoard: null, + _panOrigin: null, + _panNode: null, + _panX: null, + addBoard: function(board_phid, board_node) { this._currentBoard = board_phid; this._boardNodes[board_phid] = board_node; @@ -73,7 +78,51 @@ JX.install('Workboard', { }); drop.start(); + }, + + _setupPanHandlers: function() { + var mousedown = JX.bind(this, this._onpanmousedown); + var mousemove = JX.bind(this, this._onpanmousemove); + var mouseup = JX.bind(this, this._onpanmouseup); + + JX.Stratcom.listen('mousedown', 'workboard-shadow', mousedown); + JX.Stratcom.listen('mousemove', null, mousemove); + JX.Stratcom.listen('mouseup', null, mouseup); + }, + + _onpanmousedown: function(e) { + if (!JX.Device.isDesktop()) { + return; + } + + if (e.getNode('workpanel')) { + return; + } + + if (JX.Stratcom.pass()) { + return; + } + + e.kill(); + + this._panOrigin = JX.$V(e); + this._panNode = e.getNode('workboard-shadow'); + this._panX = this._panNode.scrollLeft; + }, + + _onpanmousemove: function(e) { + if (!this._panOrigin) { + return; + } + + var cursor = JX.$V(e); + this._panNode.scrollLeft = this._panX + (this._panOrigin.x - cursor.x); + }, + + _onpanmouseup: function() { + this._panOrigin = null; } + } }); diff --git a/webroot/rsrc/js/application/projects/behavior-project-boards.js b/webroot/rsrc/js/application/projects/behavior-project-boards.js index 8f2ca9f0d5..6adf6d059a 100644 --- a/webroot/rsrc/js/application/projects/behavior-project-boards.js +++ b/webroot/rsrc/js/application/projects/behavior-project-boards.js @@ -351,47 +351,6 @@ JX.behavior('project-boards', function(config, statics) { } }); - // When the user drags the workboard background, pan the workboard - // horizontally. This allows you to scroll across cards with only the - // mouse, without shift + scrollwheel or using the scrollbar. - - var pan_origin = null; - var pan_node = null; - var pan_x = null; - - JX.Stratcom.listen('mousedown', 'workboard-shadow', function(e) { - if (!JX.Device.isDesktop()) { - return; - } - - if (e.getNode('workpanel')) { - return; - } - - if (JX.Stratcom.pass()) { - return; - } - - e.kill(); - - pan_origin = JX.$V(e); - pan_node = e.getNode('workboard-shadow'); - pan_x = pan_node.scrollLeft; - }); - - JX.Stratcom.listen('mousemove', null, function(e) { - if (!pan_origin) { - return; - } - - var cursor = JX.$V(e); - pan_node.scrollLeft = pan_x + (pan_origin.x - cursor.x); - }); - - JX.Stratcom.listen('mouseup', null, function() { - pan_origin = null; - }); - return true; } From bc591b1b5f7dc4052ecad775bf854a56e2095f9c Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 9 Feb 2016 05:30:08 -0800 Subject: [PATCH 34/65] Mostly move workboard card moves to new Workboard code Summary: Ref T4900. This gets moves on the new stuff and cleans them up a little. Two behaviors haven't been ported yet: height adjustment during drags (which is broken anyway with inner scroll) and updating point counts (coming soon). Test Plan: Dragged cards around on a board, including top/bottom positions and normal/priority sort. Reviewers: chad Reviewed By: chad Maniphest Tasks: T4900 Differential Revision: https://secure.phabricator.com/D15226 --- resources/celerity/map.php | 50 +++++----- .../rsrc/js/application/projects/Workboard.js | 85 ++++++++++++++++- .../projects/behavior-project-boards.js | 95 ------------------- 3 files changed, 109 insertions(+), 121 deletions(-) diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 464d3e1b74..39cc673b5d 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -415,8 +415,8 @@ return array( 'rsrc/js/application/phortune/phortune-credit-card-form.js' => '2290aeef', 'rsrc/js/application/policy/behavior-policy-control.js' => 'd0c516d5', 'rsrc/js/application/policy/behavior-policy-rule-editor.js' => '5e9f347c', - 'rsrc/js/application/projects/Workboard.js' => '555f521a', - 'rsrc/js/application/projects/behavior-project-boards.js' => '9ed304e5', + 'rsrc/js/application/projects/Workboard.js' => 'b38d2c73', + 'rsrc/js/application/projects/behavior-project-boards.js' => '7f4359dd', 'rsrc/js/application/projects/behavior-project-create.js' => '065227cc', 'rsrc/js/application/projects/behavior-reorder-columns.js' => 'e1d25dfb', 'rsrc/js/application/releeph/releeph-preview-branch.js' => 'b2b4fbaf', @@ -656,7 +656,7 @@ return array( 'javelin-behavior-phui-profile-menu' => '12884df9', 'javelin-behavior-policy-control' => 'd0c516d5', 'javelin-behavior-policy-rule-editor' => '5e9f347c', - 'javelin-behavior-project-boards' => '9ed304e5', + 'javelin-behavior-project-boards' => '7f4359dd', 'javelin-behavior-project-create' => '065227cc', 'javelin-behavior-quicksand-blacklist' => '7927a7d3', 'javelin-behavior-recurring-edit' => '5f1c4d5f', @@ -723,7 +723,7 @@ return array( 'javelin-view-renderer' => '6c2b09a2', 'javelin-view-visitor' => 'efe49472', 'javelin-websocket' => 'e292eaf4', - 'javelin-workboard' => '555f521a', + 'javelin-workboard' => 'b38d2c73', 'javelin-workflow' => '5b2e3e2b', 'lightbox-attachment-css' => '7acac05d', 'maniphest-batch-editor' => 'b0f0b6d5', @@ -1223,16 +1223,6 @@ return array( 'javelin-request', 'javelin-typeahead-source', ), - '555f521a' => array( - 'javelin-install', - 'javelin-dom', - 'javelin-util', - 'javelin-vector', - 'javelin-stratcom', - 'javelin-workflow', - 'phabricator-draggable-list', - 'phabricator-drag-and-drop-file-upload', - ), '558829c2' => array( 'javelin-stratcom', 'javelin-behavior', @@ -1456,6 +1446,17 @@ return array( 'javelin-behavior', 'javelin-history', ), + '7f4359dd' => array( + 'javelin-behavior', + 'javelin-dom', + 'javelin-util', + 'javelin-vector', + 'javelin-stratcom', + 'javelin-workflow', + 'phabricator-draggable-list', + 'phabricator-drag-and-drop-file-upload', + 'javelin-workboard', + ), '805b806a' => array( 'javelin-magical-init', 'javelin-install', @@ -1597,17 +1598,6 @@ return array( 'javelin-dom', 'javelin-vector', ), - '9ed304e5' => array( - 'javelin-behavior', - 'javelin-dom', - 'javelin-util', - 'javelin-vector', - 'javelin-stratcom', - 'javelin-workflow', - 'phabricator-draggable-list', - 'phabricator-drag-and-drop-file-upload', - 'javelin-workboard', - ), '9f36c42d' => array( 'javelin-behavior', 'javelin-stratcom', @@ -1727,6 +1717,16 @@ return array( 'javelin-uri', 'javelin-request', ), + 'b38d2c73' => array( + 'javelin-install', + 'javelin-dom', + 'javelin-util', + 'javelin-vector', + 'javelin-stratcom', + 'javelin-workflow', + 'phabricator-draggable-list', + 'phabricator-drag-and-drop-file-upload', + ), 'b3a4b884' => array( 'javelin-behavior', 'phabricator-prefab', diff --git a/webroot/rsrc/js/application/projects/Workboard.js b/webroot/rsrc/js/application/projects/Workboard.js index 7fbeaaf85a..b4277d8e1b 100644 --- a/webroot/rsrc/js/application/projects/Workboard.js +++ b/webroot/rsrc/js/application/projects/Workboard.js @@ -33,7 +33,11 @@ JX.install('Workboard', { addBoard: function(board_phid, board_node) { this._currentBoard = board_phid; - this._boardNodes[board_phid] = board_node; + + if (!this._boardNodes[board_phid]) { + this._boardNodes[board_phid] = board_node; + this._setupDragHandlers(board_node); + } }, _getConfig: function() { @@ -121,6 +125,85 @@ JX.install('Workboard', { _onpanmouseup: function() { this._panOrigin = null; + }, + + + _setupDragHandlers: function(board_node) { + var columns = this._findBoardColumns(board_node); + var column; + var ii; + var lists = []; + + for (ii = 0; ii < columns.length; ii++) { + column = columns[ii]; + + var list = new JX.DraggableList('project-card', column) + .setOuterContainer(board_node) + .setFindItemsHandler(JX.bind(this, this._findCardsInColumn, column)) + .setCanDragX(true); + + // TODO: Restore these behaviors. + // list.listen('didSend', JX.bind(list, onupdate, cols[ii])); + // list.listen('didReceive', JX.bind(list, onupdate, cols[ii])); + // onupdate(cols[ii]); + + list.listen('didDrop', JX.bind(this, this._onmovecard, list)); + + // TODO: Restore these behaviors. + // list.listen('didBeginDrag', JX.bind(null, onbegindrag)); + // list.listen('didEndDrag', JX.bind(null, onenddrag)); + + lists.push(list); + } + + for (ii = 0; ii < lists.length; ii++) { + lists[ii].setGroup(lists); + } + }, + + _findBoardColumns: function(board_node) { + return JX.DOM.scry(board_node, 'ul', 'project-column'); + }, + + _findCardsInColumn: function(column_node) { + return JX.DOM.scry(column_node, 'li', 'project-card'); + }, + + _onmovecard: function(list, item, after_node) { + list.lock(); + JX.DOM.alterClass(item, 'drag-sending', true); + + var item_phid = JX.Stratcom.getData(item).objectPHID; + var data = { + objectPHID: item_phid, + columnPHID: JX.Stratcom.getData(list.getRootNode()).columnPHID + }; + + if (after_node) { + data.afterPHID = JX.Stratcom.getData(after_node).objectPHID; + } + + var before_node = item.nextSibling; + if (before_node) { + var before_phid = JX.Stratcom.getData(before_node).objectPHID; + if (before_phid) { + data.beforePHID = before_phid; + } + } + + // TODO: This should be managed per-board. + var config = this._getConfig(); + data.order = config.order; + + new JX.Workflow(config.moveURI, data) + .setHandler(JX.bind(this, this._oncardupdate, item, list)) + .start(); + }, + + _oncardupdate: function(item, list, response) { + list.unlock(); + JX.DOM.alterClass(item, 'drag-sending', false); + JX.DOM.replace(item, JX.$H(response.task)); } } diff --git a/webroot/rsrc/js/application/projects/behavior-project-boards.js b/webroot/rsrc/js/application/projects/behavior-project-boards.js index 6adf6d059a..6d035d7251 100644 --- a/webroot/rsrc/js/application/projects/behavior-project-boards.js +++ b/webroot/rsrc/js/application/projects/behavior-project-boards.js @@ -13,9 +13,6 @@ JX.behavior('project-boards', function(config, statics) { - function finditems(col) { - return JX.DOM.scry(col, 'li', 'project-card'); - } function onupdate(col) { var data = JX.Stratcom.getData(col); @@ -64,15 +61,6 @@ JX.behavior('project-boards', function(config, statics) { } } - function onresponse(response, item, list) { - list.unlock(); - JX.DOM.alterClass(item, 'drag-sending', false); - JX.DOM.replace(item, JX.$H(response.task)); - } - - function getcolumns() { - return JX.DOM.scry(JX.$(statics.boardID), 'ul', 'project-column'); - } function colsort(u, v) { var ud = JX.Stratcom.getData(u).sort || []; @@ -126,56 +114,6 @@ JX.behavior('project-boards', function(config, statics) { getcontainer().style.minHeight = ''; } - function ondrop(list, item, after) { - list.lock(); - JX.DOM.alterClass(item, 'drag-sending', true); - - var item_phid = JX.Stratcom.getData(item).objectPHID; - var data = { - objectPHID: item_phid, - columnPHID: JX.Stratcom.getData(list.getRootNode()).columnPHID - }; - - var after_phid = null; - var items = finditems(list.getRootNode()); - if (after) { - after_phid = JX.Stratcom.getData(after).objectPHID; - data.afterPHID = after_phid; - } - var ii; - var ii_item; - var ii_item_phid; - var ii_prev_item_phid = null; - var before_phid = null; - for (ii = 0; ii < items.length; ii++) { - ii_item = items[ii]; - ii_item_phid = JX.Stratcom.getData(ii_item).objectPHID; - if (ii_item_phid == item_phid) { - // skip the item we just dropped - continue; - } - // note this handles when there is no after phid - we are at the top of - // the list - quite nicely - if (ii_prev_item_phid == after_phid) { - before_phid = ii_item_phid; - break; - } - ii_prev_item_phid = ii_item_phid; - } - if (before_phid) { - data.beforePHID = before_phid; - } - - data.order = statics.order; - - var workflow = new JX.Workflow(statics.moveURI, data) - .setHandler(function(response) { - onresponse(response, item, list); - }); - - workflow.start(); - } - function onedit(column, r) { var new_card = JX.$H(r.tasks).getNode(); var new_data = JX.Stratcom.getData(new_card); @@ -226,35 +164,6 @@ JX.behavior('project-boards', function(config, statics) { statics.createURI = update_config.createURI; } - function init_board() { - var lists = []; - var ii; - var cols = getcolumns(); - - for (ii = 0; ii < cols.length; ii++) { - var list = new JX.DraggableList('project-card', cols[ii]) - .setFindItemsHandler(JX.bind(null, finditems, cols[ii])) - .setOuterContainer(JX.$(config.boardID)) - .setCanDragX(true); - - list.listen('didSend', JX.bind(list, onupdate, cols[ii])); - list.listen('didReceive', JX.bind(list, onupdate, cols[ii])); - - list.listen('didDrop', JX.bind(null, ondrop, list)); - - list.listen('didBeginDrag', JX.bind(null, onbegindrag)); - list.listen('didEndDrag', JX.bind(null, onenddrag)); - - lists.push(list); - - onupdate(cols[ii]); - } - - for (ii = 0; ii < lists.length; ii++) { - lists[ii].setGroup(lists); - } - } - function setup() { JX.Stratcom.listen( @@ -346,9 +255,6 @@ JX.behavior('project-boards', function(config, statics) { statics.boardID = new_config.boardID; } update_statics(new_config); - if (data.fromServer) { - init_board(); - } }); return true; @@ -359,7 +265,6 @@ JX.behavior('project-boards', function(config, statics) { var current_page_id = JX.Quicksand.getCurrentPageID(); statics.boardConfigCache = {}; statics.boardConfigCache[current_page_id] = config; - init_board(); statics.setup = setup(); } From 9ed9764784be9fd5eedcf375fe55e834c2f0c79f Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 9 Feb 2016 05:40:56 -0800 Subject: [PATCH 35/65] Replace height buffer behavior while dragging on workboards with infinite column height Summary: Ref T4900. The root problem is that dragging stuff near the bottom of the board can cause jittery, jumpy behaviors. Internal scrolling has changed the nature of this problem. Previously, the height of the board itself would jump around, but it's now fixed so the height of columns jumps around instead. We could take the same approach and add a chunk to the bottom of each column when a drag starts, but this is really distracting visually since it's obvious to the user. Instead, treat columns as infinitely tall (so dragging beneath them still counts as dragging to the bottom position). Test Plan: - View a board with a column taller than the screen (has a scrollbar). - Drag a card to near the bottom position. - Move the mouse down a little bit at a time, continuing toward the bottom of the page. - Before patch: at some point, UI flips out and starts rapidly adding, scrolling, and removing the ghost. - After patch: sensible behavior, ghost is in bottom position for all cursor locations. Also works for dragging to the top. (This leaves us with a little less dead space for cancelling drags, but you've still got the left menu, anything offscreen, and the escape key, which seems fine.) Reviewers: chad Reviewed By: chad Maniphest Tasks: T4900 Differential Revision: https://secure.phabricator.com/D15227 --- resources/celerity/map.php | 72 +++++++++---------- .../rsrc/js/application/projects/Workboard.js | 7 +- .../projects/behavior-project-boards.js | 35 --------- webroot/rsrc/js/core/DraggableList.js | 13 +++- 4 files changed, 50 insertions(+), 77 deletions(-) diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 39cc673b5d..55a4bec3b3 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -8,7 +8,7 @@ return array( 'names' => array( 'core.pkg.css' => 'e8763436', - 'core.pkg.js' => '17380dd3', + 'core.pkg.js' => 'd7daa6d8', 'darkconsole.pkg.js' => 'e7393ebb', 'differential.pkg.css' => '2de124c9', 'differential.pkg.js' => 'd0cd0df6', @@ -415,8 +415,8 @@ return array( 'rsrc/js/application/phortune/phortune-credit-card-form.js' => '2290aeef', 'rsrc/js/application/policy/behavior-policy-control.js' => 'd0c516d5', 'rsrc/js/application/policy/behavior-policy-rule-editor.js' => '5e9f347c', - 'rsrc/js/application/projects/Workboard.js' => 'b38d2c73', - 'rsrc/js/application/projects/behavior-project-boards.js' => '7f4359dd', + 'rsrc/js/application/projects/Workboard.js' => 'fa8ab410', + 'rsrc/js/application/projects/behavior-project-boards.js' => '7784bfc6', 'rsrc/js/application/projects/behavior-project-create.js' => '065227cc', 'rsrc/js/application/projects/behavior-reorder-columns.js' => 'e1d25dfb', 'rsrc/js/application/releeph/releeph-preview-branch.js' => 'b2b4fbaf', @@ -449,7 +449,7 @@ return array( 'rsrc/js/application/uiexample/notification-example.js' => '8ce821c5', 'rsrc/js/core/Busy.js' => '59a7976a', 'rsrc/js/core/DragAndDropFileUpload.js' => '81f182b5', - 'rsrc/js/core/DraggableList.js' => '705df8d1', + 'rsrc/js/core/DraggableList.js' => '5a13c79f', 'rsrc/js/core/FileUpload.js' => '680ea2c8', 'rsrc/js/core/Hovercard.js' => '1bd28176', 'rsrc/js/core/KeyboardShortcut.js' => '1ae869f2', @@ -656,7 +656,7 @@ return array( 'javelin-behavior-phui-profile-menu' => '12884df9', 'javelin-behavior-policy-control' => 'd0c516d5', 'javelin-behavior-policy-rule-editor' => '5e9f347c', - 'javelin-behavior-project-boards' => '7f4359dd', + 'javelin-behavior-project-boards' => '7784bfc6', 'javelin-behavior-project-create' => '065227cc', 'javelin-behavior-quicksand-blacklist' => '7927a7d3', 'javelin-behavior-recurring-edit' => '5f1c4d5f', @@ -723,7 +723,7 @@ return array( 'javelin-view-renderer' => '6c2b09a2', 'javelin-view-visitor' => 'efe49472', 'javelin-websocket' => 'e292eaf4', - 'javelin-workboard' => 'b38d2c73', + 'javelin-workboard' => 'fa8ab410', 'javelin-workflow' => '5b2e3e2b', 'lightbox-attachment-css' => '7acac05d', 'maniphest-batch-editor' => 'b0f0b6d5', @@ -745,7 +745,7 @@ return array( 'phabricator-countdown-css' => 'e7544472', 'phabricator-dashboard-css' => 'eb458607', 'phabricator-drag-and-drop-file-upload' => '81f182b5', - 'phabricator-draggable-list' => '705df8d1', + 'phabricator-draggable-list' => '5a13c79f', 'phabricator-fatal-config-template-css' => '8e6c6fcd', 'phabricator-feed-css' => 'ecd4ec57', 'phabricator-file-upload' => '680ea2c8', @@ -1255,6 +1255,14 @@ return array( 'javelin-dom', 'javelin-stratcom', ), + '5a13c79f' => array( + 'javelin-install', + 'javelin-dom', + 'javelin-stratcom', + 'javelin-util', + 'javelin-vector', + 'javelin-magical-init', + ), '5b2e3e2b' => array( 'javelin-stratcom', 'javelin-request', @@ -1356,14 +1364,6 @@ return array( 'javelin-typeahead', 'javelin-uri', ), - '705df8d1' => array( - 'javelin-install', - 'javelin-dom', - 'javelin-stratcom', - 'javelin-util', - 'javelin-vector', - 'javelin-magical-init', - ), '70baed2f' => array( 'javelin-install', 'javelin-dom', @@ -1407,6 +1407,17 @@ return array( 'javelin-reactor', 'javelin-util', ), + '7784bfc6' => array( + 'javelin-behavior', + 'javelin-dom', + 'javelin-util', + 'javelin-vector', + 'javelin-stratcom', + 'javelin-workflow', + 'phabricator-draggable-list', + 'phabricator-drag-and-drop-file-upload', + 'javelin-workboard', + ), '782ab6e7' => array( 'javelin-behavior', 'javelin-dom', @@ -1446,17 +1457,6 @@ return array( 'javelin-behavior', 'javelin-history', ), - '7f4359dd' => array( - 'javelin-behavior', - 'javelin-dom', - 'javelin-util', - 'javelin-vector', - 'javelin-stratcom', - 'javelin-workflow', - 'phabricator-draggable-list', - 'phabricator-drag-and-drop-file-upload', - 'javelin-workboard', - ), '805b806a' => array( 'javelin-magical-init', 'javelin-install', @@ -1717,16 +1717,6 @@ return array( 'javelin-uri', 'javelin-request', ), - 'b38d2c73' => array( - 'javelin-install', - 'javelin-dom', - 'javelin-util', - 'javelin-vector', - 'javelin-stratcom', - 'javelin-workflow', - 'phabricator-draggable-list', - 'phabricator-drag-and-drop-file-upload', - ), 'b3a4b884' => array( 'javelin-behavior', 'phabricator-prefab', @@ -2088,6 +2078,16 @@ return array( 'javelin-vector', 'javelin-magical-init', ), + 'fa8ab410' => array( + 'javelin-install', + 'javelin-dom', + 'javelin-util', + 'javelin-vector', + 'javelin-stratcom', + 'javelin-workflow', + 'phabricator-draggable-list', + 'phabricator-drag-and-drop-file-upload', + ), 'fb20ac8d' => array( 'javelin-behavior', 'javelin-aphlict', diff --git a/webroot/rsrc/js/application/projects/Workboard.js b/webroot/rsrc/js/application/projects/Workboard.js index b4277d8e1b..459f7f5722 100644 --- a/webroot/rsrc/js/application/projects/Workboard.js +++ b/webroot/rsrc/js/application/projects/Workboard.js @@ -140,7 +140,8 @@ JX.install('Workboard', { var list = new JX.DraggableList('project-card', column) .setOuterContainer(board_node) .setFindItemsHandler(JX.bind(this, this._findCardsInColumn, column)) - .setCanDragX(true); + .setCanDragX(true) + .setHasInfiniteHeight(true); // TODO: Restore these behaviors. // list.listen('didSend', JX.bind(list, onupdate, cols[ii])); @@ -149,10 +150,6 @@ JX.install('Workboard', { list.listen('didDrop', JX.bind(this, this._onmovecard, list)); - // TODO: Restore these behaviors. - // list.listen('didBeginDrag', JX.bind(null, onbegindrag)); - // list.listen('didEndDrag', JX.bind(null, onenddrag)); - lists.push(list); } diff --git a/webroot/rsrc/js/application/projects/behavior-project-boards.js b/webroot/rsrc/js/application/projects/behavior-project-boards.js index 6d035d7251..8943e5b045 100644 --- a/webroot/rsrc/js/application/projects/behavior-project-boards.js +++ b/webroot/rsrc/js/application/projects/behavior-project-boards.js @@ -79,41 +79,6 @@ JX.behavior('project-boards', function(config, statics) { return 0; } - function getcontainer() { - return JX.DOM.find( - JX.$(statics.boardID), - 'div', - 'aphront-multi-column-view'); - } - - function onbegindrag(item) { - // If the longest column on the board is taller than the window, the board - // will scroll vertically. Dragging an item to the longest column may - // make it longer, by the total height of the board, plus the height of - // the drop target. - - // If this happens, the scrollbar will jump around and the scroll position - // can be adjusted in a disorienting way. To reproduce this, drag a task - // to the bottom of the longest column on a scrolling board and wave the - // task in and out of the column. The scroll bar will jump around and - // it will be hard to lock onto a target. - - // To fix this, set the minimum board height to the current board height - // plus the size of the drop target (which is the size of the item plus - // a bit of margin). This makes sure the scroll bar never needs to - // recalculate. - - var item_size = JX.Vector.getDim(item); - var container = getcontainer(); - var container_size = JX.Vector.getDim(container); - - container.style.minHeight = (item_size.y + container_size.y + 12) + 'px'; - } - - function onenddrag() { - getcontainer().style.minHeight = ''; - } - function onedit(column, r) { var new_card = JX.$H(r.tasks).getNode(); var new_data = JX.Stratcom.getData(new_card); diff --git a/webroot/rsrc/js/core/DraggableList.js b/webroot/rsrc/js/core/DraggableList.js index d808784807..99dd6e8fba 100644 --- a/webroot/rsrc/js/core/DraggableList.js +++ b/webroot/rsrc/js/core/DraggableList.js @@ -40,7 +40,8 @@ JX.install('DraggableList', { properties : { findItemsHandler: null, canDragX: false, - outerContainer: null + outerContainer: null, + hasInfiniteHeight: false }, members : { @@ -286,6 +287,7 @@ JX.install('DraggableList', { _getTargetList : function(p) { var target_list; + var infinity; if (this._hasGroup()) { var group = this._group; for (var ii = 0; ii < group.length; ii++) { @@ -293,6 +295,15 @@ JX.install('DraggableList', { var rp = JX.$V(root); var rd = JX.Vector.getDim(root); + if (group[ii].getHasInfiniteHeight()) { + // The math doesn't work out quite right if we actually use + // Math.Infinity, so approximate infinity as the document height. + infinity = infinity || JX.Vector.getDocument().y; + + rp.y = 0; + rd.y = infinity; + } + var is_target = false; if (p.x >= rp.x && p.y >= rp.y) { if (p.x <= (rp.x + rd.x) && p.y <= (rp.y + rd.y)) { From 01084bfe22b4d4e4b994e2989c451d41f57f507e Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 9 Feb 2016 06:14:42 -0800 Subject: [PATCH 36/65] Begin making card updates on boards independent of context Summary: Ref T4900. To eventually support realtime board updates, we need to be able to perform a board state update without the context of the action which caused it. For example, if the server says "update card Y", we need to know what to do without being told "card Y was moved from column A to column B" explicitly. Currently, all the update code relies on knowing what happened and which nodes were affected. This is only a small step forward, but starts making things a bit more independent and consistent. Test Plan: - Moved cards around. - Changed card cover images. Reviewers: chad Reviewed By: chad Maniphest Tasks: T4900 Differential Revision: https://secure.phabricator.com/D15228 --- resources/celerity/map.php | 50 +++++----- .../PhabricatorProjectController.php | 3 +- .../rsrc/js/application/projects/Workboard.js | 94 +++++++++++++------ .../projects/behavior-project-boards.js | 7 +- 4 files changed, 97 insertions(+), 57 deletions(-) diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 55a4bec3b3..06017dae72 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -415,8 +415,8 @@ return array( 'rsrc/js/application/phortune/phortune-credit-card-form.js' => '2290aeef', 'rsrc/js/application/policy/behavior-policy-control.js' => 'd0c516d5', 'rsrc/js/application/policy/behavior-policy-rule-editor.js' => '5e9f347c', - 'rsrc/js/application/projects/Workboard.js' => 'fa8ab410', - 'rsrc/js/application/projects/behavior-project-boards.js' => '7784bfc6', + 'rsrc/js/application/projects/Workboard.js' => '088b2495', + 'rsrc/js/application/projects/behavior-project-boards.js' => '37eb99e4', 'rsrc/js/application/projects/behavior-project-create.js' => '065227cc', 'rsrc/js/application/projects/behavior-reorder-columns.js' => 'e1d25dfb', 'rsrc/js/application/releeph/releeph-preview-branch.js' => 'b2b4fbaf', @@ -656,7 +656,7 @@ return array( 'javelin-behavior-phui-profile-menu' => '12884df9', 'javelin-behavior-policy-control' => 'd0c516d5', 'javelin-behavior-policy-rule-editor' => '5e9f347c', - 'javelin-behavior-project-boards' => '7784bfc6', + 'javelin-behavior-project-boards' => '37eb99e4', 'javelin-behavior-project-create' => '065227cc', 'javelin-behavior-quicksand-blacklist' => '7927a7d3', 'javelin-behavior-recurring-edit' => '5f1c4d5f', @@ -723,7 +723,7 @@ return array( 'javelin-view-renderer' => '6c2b09a2', 'javelin-view-visitor' => 'efe49472', 'javelin-websocket' => 'e292eaf4', - 'javelin-workboard' => 'fa8ab410', + 'javelin-workboard' => '088b2495', 'javelin-workflow' => '5b2e3e2b', 'lightbox-attachment-css' => '7acac05d', 'maniphest-batch-editor' => 'b0f0b6d5', @@ -930,6 +930,16 @@ return array( 'javelin-stratcom', 'javelin-vector', ), + '088b2495' => array( + 'javelin-install', + 'javelin-dom', + 'javelin-util', + 'javelin-vector', + 'javelin-stratcom', + 'javelin-workflow', + 'phabricator-draggable-list', + 'phabricator-drag-and-drop-file-upload', + ), '0a3f3021' => array( 'javelin-behavior', 'javelin-stratcom', @@ -1086,6 +1096,17 @@ return array( 'javelin-vector', 'phuix-autocomplete', ), + '37eb99e4' => array( + 'javelin-behavior', + 'javelin-dom', + 'javelin-util', + 'javelin-vector', + 'javelin-stratcom', + 'javelin-workflow', + 'phabricator-draggable-list', + 'phabricator-drag-and-drop-file-upload', + 'javelin-workboard', + ), '3ab51e2c' => array( 'javelin-behavior', 'javelin-behavior-device', @@ -1407,17 +1428,6 @@ return array( 'javelin-reactor', 'javelin-util', ), - '7784bfc6' => array( - 'javelin-behavior', - 'javelin-dom', - 'javelin-util', - 'javelin-vector', - 'javelin-stratcom', - 'javelin-workflow', - 'phabricator-draggable-list', - 'phabricator-drag-and-drop-file-upload', - 'javelin-workboard', - ), '782ab6e7' => array( 'javelin-behavior', 'javelin-dom', @@ -2078,16 +2088,6 @@ return array( 'javelin-vector', 'javelin-magical-init', ), - 'fa8ab410' => array( - 'javelin-install', - 'javelin-dom', - 'javelin-util', - 'javelin-vector', - 'javelin-stratcom', - 'javelin-workflow', - 'phabricator-draggable-list', - 'phabricator-drag-and-drop-file-upload', - ), 'fb20ac8d' => array( 'javelin-behavior', 'javelin-aphlict', diff --git a/src/applications/project/controller/PhabricatorProjectController.php b/src/applications/project/controller/PhabricatorProjectController.php index e995461ba6..755b7ca75e 100644 --- a/src/applications/project/controller/PhabricatorProjectController.php +++ b/src/applications/project/controller/PhabricatorProjectController.php @@ -192,7 +192,8 @@ abstract class PhabricatorProjectController extends PhabricatorController { return id(new AphrontAjaxResponse()) ->setContent( array( - 'task' => $item, + 'objectPHID' => $object->getPHID(), + 'cardHTML' => $item, )); } diff --git a/webroot/rsrc/js/application/projects/Workboard.js b/webroot/rsrc/js/application/projects/Workboard.js index 459f7f5722..96214a4c29 100644 --- a/webroot/rsrc/js/application/projects/Workboard.js +++ b/webroot/rsrc/js/application/projects/Workboard.js @@ -17,9 +17,14 @@ JX.install('Workboard', { this._config = config; this._boardNodes = {}; + this._columnMap = {}; + }, - this._setupCoverImageHandlers(); - this._setupPanHandlers(); + properties: { + uploadURI: null, + coverURI: null, + moveURI: null, + chunkThreshold: null }, members: { @@ -31,13 +36,19 @@ JX.install('Workboard', { _panNode: null, _panX: null, + _columnMap: null, + + start: function() { + this._setupCoverImageHandlers(); + this._setupPanHandlers(); + + return this; + }, + addBoard: function(board_phid, board_node) { this._currentBoard = board_phid; - - if (!this._boardNodes[board_phid]) { - this._boardNodes[board_phid] = board_node; - this._setupDragHandlers(board_node); - } + this._boardNodes[board_phid] = board_node; + this._setupDragHandlers(board_node); }, _getConfig: function() { @@ -49,11 +60,9 @@ JX.install('Workboard', { return; } - var config = this._getConfig(); - var drop = new JX.PhabricatorDragAndDropFileUpload('project-card') - .setURI(config.uploadURI) - .setChunkThreshold(config.chunkThreshold); + .setURI(this.getUploadURI()) + .setChunkThreshold(this.getChunkThreshold()); drop.listen('didBeginDrag', function(node) { JX.DOM.alterClass(node, 'phui-workcard-upload-target', true); @@ -63,27 +72,26 @@ JX.install('Workboard', { JX.DOM.alterClass(node, 'phui-workcard-upload-target', false); }); - drop.listen('didUpload', function(file) { - var node = file.getTargetNode(); - - var board = JX.DOM.findAbove(node, 'div', 'jx-workboard'); - - var data = { - boardPHID: JX.Stratcom.getData(board).boardPHID, - objectPHID: JX.Stratcom.getData(node).objectPHID, - filePHID: file.getPHID() - }; - - new JX.Workflow(config.coverURI, data) - .setHandler(function(r) { - JX.DOM.replace(node, JX.$H(r.task)); - }) - .start(); - }); + drop.listen('didUpload', JX.bind(this, this._oncoverupload)); drop.start(); }, + _oncoverupload: function(file) { + var node = file.getTargetNode(); + var board = JX.DOM.findAbove(node, 'div', 'jx-workboard'); + + var data = { + boardPHID: JX.Stratcom.getData(board).boardPHID, + objectPHID: JX.Stratcom.getData(node).objectPHID, + filePHID: file.getPHID() + }; + + new JX.Workflow(this.getCoverURI(), data) + .setHandler(JX.bind(this, this._queueCardUpdate)) + .start(); + }, + _setupPanHandlers: function() { var mousedown = JX.bind(this, this._onpanmousedown); var mousemove = JX.bind(this, this._onpanmousemove); @@ -192,7 +200,7 @@ JX.install('Workboard', { var config = this._getConfig(); data.order = config.order; - new JX.Workflow(config.moveURI, data) + new JX.Workflow(this.getMoveURI(), data) .setHandler(JX.bind(this, this._oncardupdate, item, list)) .start(); }, @@ -200,7 +208,33 @@ JX.install('Workboard', { _oncardupdate: function(item, list, response) { list.unlock(); JX.DOM.alterClass(item, 'drag-sending', false); - JX.DOM.replace(item, JX.$H(response.task)); + + this._queueCardUpdate(response); + }, + + _queueCardUpdate: function(response) { + var board_node = this._boardNodes[this._currentBoard]; + + var columns = this._findBoardColumns(board_node); + var cards; + var ii; + var jj; + var data; + + for (ii = 0; ii < columns.length; ii++) { + cards = this._findCardsInColumn(columns[ii]); + for (jj = 0; jj < cards.length; jj++) { + data = JX.Stratcom.getData(cards[jj]); + if (data.objectPHID == response.objectPHID) { + this._replaceCard(cards[jj], JX.$H(response.cardHTML)); + } + } + } + + }, + + _replaceCard: function(old_node, new_node) { + JX.DOM.replace(old_node, new_node); } } diff --git a/webroot/rsrc/js/application/projects/behavior-project-boards.js b/webroot/rsrc/js/application/projects/behavior-project-boards.js index 8943e5b045..b084a2d00e 100644 --- a/webroot/rsrc/js/application/projects/behavior-project-boards.js +++ b/webroot/rsrc/js/application/projects/behavior-project-boards.js @@ -234,7 +234,12 @@ JX.behavior('project-boards', function(config, statics) { } if (!statics.workboard) { - statics.workboard = new JX.Workboard(config); + statics.workboard = new JX.Workboard(config) + .setUploadURI(config.uploadURI) + .setCoverURI(config.coverURI) + .setMoveURI(config.moveURI) + .setChunkThreshold(config.chunkThreshold) + .start(); } statics.workboard.addBoard(config.projectPHID, JX.$(config.boardID)); From 0bf3519045b2d6a9653bf97fe5386de9bb623854 Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 10 Feb 2016 05:59:46 -0800 Subject: [PATCH 37/65] Rewrite workboards to have way more bugs Summary: Ref T4900. Briefly: - Much more layout and rendering is now done in Javascript. - This should otherwise be identical to the behavior at HEAD, except that: - editing a task and removing the current board from it no longer removes the task; and - points still don't work. However, this can now plausibly support realtime workboard updates and other complex state-based behaviors like points calculations in a future change. Test Plan: - Changed card covers. - Moved cards. - Sorted board by priority and natural. - Added new cards. - Edited cards in place. Reviewers: chad Reviewed By: chad Maniphest Tasks: T4900 Differential Revision: https://secure.phabricator.com/D15234 --- resources/celerity/map.php | 70 +++-- src/__phutil_library_map__.php | 2 + .../ManiphestTaskEditController.php | 1 + .../maniphest/editor/ManiphestEditEngine.php | 101 +------- .../maniphest/storage/ManiphestTask.php | 18 +- .../PhabricatorProjectBoardViewController.php | 42 +-- .../PhabricatorProjectController.php | 51 +--- .../engine/PhabricatorBoardResponseEngine.php | 146 +++++++++++ .../project/view/ProjectBoardTaskCard.php | 6 +- .../rsrc/js/application/projects/Workboard.js | 242 ------------------ .../js/application/projects/WorkboardBoard.js | 221 ++++++++++++++++ .../js/application/projects/WorkboardCard.js | 56 ++++ .../application/projects/WorkboardColumn.js | 177 +++++++++++++ .../projects/WorkboardController.js | 201 +++++++++++++++ .../projects/behavior-project-boards.js | 144 +++-------- 15 files changed, 936 insertions(+), 542 deletions(-) create mode 100644 src/applications/project/engine/PhabricatorBoardResponseEngine.php delete mode 100644 webroot/rsrc/js/application/projects/Workboard.js create mode 100644 webroot/rsrc/js/application/projects/WorkboardBoard.js create mode 100644 webroot/rsrc/js/application/projects/WorkboardCard.js create mode 100644 webroot/rsrc/js/application/projects/WorkboardColumn.js create mode 100644 webroot/rsrc/js/application/projects/WorkboardController.js diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 06017dae72..870d8fa6e8 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -415,8 +415,11 @@ return array( 'rsrc/js/application/phortune/phortune-credit-card-form.js' => '2290aeef', 'rsrc/js/application/policy/behavior-policy-control.js' => 'd0c516d5', 'rsrc/js/application/policy/behavior-policy-rule-editor.js' => '5e9f347c', - 'rsrc/js/application/projects/Workboard.js' => '088b2495', - 'rsrc/js/application/projects/behavior-project-boards.js' => '37eb99e4', + 'rsrc/js/application/projects/WorkboardBoard.js' => '069d6dd3', + 'rsrc/js/application/projects/WorkboardCard.js' => '2fcefa17', + 'rsrc/js/application/projects/WorkboardColumn.js' => 'e8f303bb', + 'rsrc/js/application/projects/WorkboardController.js' => 'fa1378c3', + 'rsrc/js/application/projects/behavior-project-boards.js' => 'e1b56d72', 'rsrc/js/application/projects/behavior-project-create.js' => '065227cc', 'rsrc/js/application/projects/behavior-reorder-columns.js' => 'e1d25dfb', 'rsrc/js/application/releeph/releeph-preview-branch.js' => 'b2b4fbaf', @@ -656,7 +659,7 @@ return array( 'javelin-behavior-phui-profile-menu' => '12884df9', 'javelin-behavior-policy-control' => 'd0c516d5', 'javelin-behavior-policy-rule-editor' => '5e9f347c', - 'javelin-behavior-project-boards' => '37eb99e4', + 'javelin-behavior-project-boards' => 'e1b56d72', 'javelin-behavior-project-create' => '065227cc', 'javelin-behavior-quicksand-blacklist' => '7927a7d3', 'javelin-behavior-recurring-edit' => '5f1c4d5f', @@ -723,7 +726,10 @@ return array( 'javelin-view-renderer' => '6c2b09a2', 'javelin-view-visitor' => 'efe49472', 'javelin-websocket' => 'e292eaf4', - 'javelin-workboard' => '088b2495', + 'javelin-workboard-board' => '069d6dd3', + 'javelin-workboard-card' => '2fcefa17', + 'javelin-workboard-column' => 'e8f303bb', + 'javelin-workboard-controller' => 'fa1378c3', 'javelin-workflow' => '5b2e3e2b', 'lightbox-attachment-css' => '7acac05d', 'maniphest-batch-editor' => 'b0f0b6d5', @@ -913,6 +919,15 @@ return array( 'javelin-stratcom', 'javelin-workflow', ), + '069d6dd3' => array( + 'javelin-install', + 'javelin-dom', + 'javelin-util', + 'javelin-stratcom', + 'javelin-workflow', + 'phabricator-draggable-list', + 'javelin-workboard-column', + ), '06c32383' => array( 'javelin-behavior', 'javelin-typeahead-ondemand-source', @@ -930,16 +945,6 @@ return array( 'javelin-stratcom', 'javelin-vector', ), - '088b2495' => array( - 'javelin-install', - 'javelin-dom', - 'javelin-util', - 'javelin-vector', - 'javelin-stratcom', - 'javelin-workflow', - 'phabricator-draggable-list', - 'phabricator-drag-and-drop-file-upload', - ), '0a3f3021' => array( 'javelin-behavior', 'javelin-stratcom', @@ -1077,6 +1082,9 @@ return array( '2ee659ce' => array( 'javelin-install', ), + '2fcefa17' => array( + 'javelin-install', + ), '327a00d1' => array( 'javelin-behavior', 'javelin-stratcom', @@ -1096,17 +1104,6 @@ return array( 'javelin-vector', 'phuix-autocomplete', ), - '37eb99e4' => array( - 'javelin-behavior', - 'javelin-dom', - 'javelin-util', - 'javelin-vector', - 'javelin-stratcom', - 'javelin-workflow', - 'phabricator-draggable-list', - 'phabricator-drag-and-drop-file-upload', - 'javelin-workboard', - ), '3ab51e2c' => array( 'javelin-behavior', 'javelin-behavior-device', @@ -1937,6 +1934,15 @@ return array( 'javelin-dom', 'phabricator-prefab', ), + 'e1b56d72' => array( + 'javelin-behavior', + 'javelin-dom', + 'javelin-util', + 'javelin-vector', + 'javelin-stratcom', + 'javelin-workflow', + 'javelin-workboard-controller', + ), 'e1d25dfb' => array( 'javelin-behavior', 'javelin-stratcom', @@ -2004,6 +2010,10 @@ return array( 'e6e25838' => array( 'javelin-install', ), + 'e8f303bb' => array( + 'javelin-install', + 'javelin-workboard-card', + ), 'e9581f08' => array( 'javelin-behavior', 'javelin-stratcom', @@ -2088,6 +2098,16 @@ return array( 'javelin-vector', 'javelin-magical-init', ), + 'fa1378c3' => array( + 'javelin-install', + 'javelin-dom', + 'javelin-util', + 'javelin-vector', + 'javelin-stratcom', + 'javelin-workflow', + 'phabricator-drag-and-drop-file-upload', + 'javelin-workboard-board', + ), 'fb20ac8d' => array( 'javelin-behavior', 'javelin-aphlict', diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 3c972e329d..134e36b2b9 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -1820,6 +1820,7 @@ phutil_register_library_map(array( 'PhabricatorBitbucketAuthProvider' => 'applications/auth/provider/PhabricatorBitbucketAuthProvider.php', 'PhabricatorBoardLayoutEngine' => 'applications/project/engine/PhabricatorBoardLayoutEngine.php', 'PhabricatorBoardRenderingEngine' => 'applications/project/engine/PhabricatorBoardRenderingEngine.php', + 'PhabricatorBoardResponseEngine' => 'applications/project/engine/PhabricatorBoardResponseEngine.php', 'PhabricatorBot' => 'infrastructure/daemon/bot/PhabricatorBot.php', 'PhabricatorBotChannel' => 'infrastructure/daemon/bot/target/PhabricatorBotChannel.php', 'PhabricatorBotDebugLogHandler' => 'infrastructure/daemon/bot/handler/PhabricatorBotDebugLogHandler.php', @@ -6058,6 +6059,7 @@ phutil_register_library_map(array( 'PhabricatorBitbucketAuthProvider' => 'PhabricatorOAuth1AuthProvider', 'PhabricatorBoardLayoutEngine' => 'Phobject', 'PhabricatorBoardRenderingEngine' => 'Phobject', + 'PhabricatorBoardResponseEngine' => 'Phobject', 'PhabricatorBot' => 'PhabricatorDaemon', 'PhabricatorBotChannel' => 'PhabricatorBotTarget', 'PhabricatorBotDebugLogHandler' => 'PhabricatorBotHandler', diff --git a/src/applications/maniphest/controller/ManiphestTaskEditController.php b/src/applications/maniphest/controller/ManiphestTaskEditController.php index 5cf07566a4..9483529138 100644 --- a/src/applications/maniphest/controller/ManiphestTaskEditController.php +++ b/src/applications/maniphest/controller/ManiphestTaskEditController.php @@ -9,6 +9,7 @@ final class ManiphestTaskEditController extends ManiphestController { ->addContextParameter('responseType') ->addContextParameter('columnPHID') ->addContextParameter('order') + ->addContextParameter('visiblePHIDs') ->buildResponse(); } diff --git a/src/applications/maniphest/editor/ManiphestEditEngine.php b/src/applications/maniphest/editor/ManiphestEditEngine.php index 24208bf866..fe9b96f25b 100644 --- a/src/applications/maniphest/editor/ManiphestEditEngine.php +++ b/src/applications/maniphest/editor/ManiphestEditEngine.php @@ -289,7 +289,11 @@ final class ManiphestEditEngine $viewer = $request->getViewer(); $column_phid = $request->getStr('columnPHID'); - $order = $request->getStr('order'); + + $visible_phids = $request->getStrList('visiblePHIDs'); + if (!$visible_phids) { + $visible_phids = array(); + } $column = id(new PhabricatorProjectColumnQuery()) ->setViewer($viewer) @@ -299,98 +303,15 @@ final class ManiphestEditEngine return new Aphront404Response(); } - // If the workboard's project and all descendant projects have been removed - // from the card's project list, we are going to remove it from the board - // completely. - - // TODO: If the user did something sneaky and changed a subproject, we'll - // currently leave the card where it was but should really move it to the - // proper new column. - $board_phid = $column->getProjectPHID(); + $object_phid = $task->getPHID(); - $descendant_projects = id(new PhabricatorProjectQuery()) + return id(new PhabricatorBoardResponseEngine()) ->setViewer($viewer) - ->withAncestorProjectPHIDs(array($column->getProjectPHID())) - ->execute(); - $board_phids = mpull($descendant_projects, 'getPHID', 'getPHID'); - $board_phids[$board_phid] = $board_phid; - - $project_map = array_fuse($task->getProjectPHIDs()); - $remove_card = !array_intersect_key($board_phids, $project_map); - - // TODO: Maybe the caller should pass a list of visible task PHIDs so we - // know which ones we need to reorder? This is a HUGE overfetch. - $objects = id(new ManiphestTaskQuery()) - ->setViewer($viewer) - ->withEdgeLogicPHIDs( - PhabricatorProjectObjectHasProjectEdgeType::EDGECONST, - PhabricatorQueryConstraint::OPERATOR_ANCESTOR, - array($board_phids)) - ->setViewer($viewer) - ->execute(); - $objects = mpull($objects, null, 'getPHID'); - - $layout_engine = id(new PhabricatorBoardLayoutEngine()) - ->setViewer($viewer) - ->setBoardPHIDs(array($board_phid)) - ->setObjectPHIDs(array_keys($objects)) - ->executeLayout(); - - $positions = $layout_engine->getColumnObjectPositions( - $board_phid, - $column_phid); - - $column_phids = $layout_engine->getColumnObjectPHIDs( - $board_phid, - $column_phid); - - $column_tasks = array_select_keys($objects, $column_phids); - - if ($order == PhabricatorProjectColumn::ORDER_NATURAL) { - // TODO: This is a little bit awkward, because PHP and JS use - // slightly different sort order parameters to achieve the same - // effect. It would be good to unify this a bit at some point. - $sort_map = array(); - foreach ($positions as $position) { - $sort_map[$position->getObjectPHID()] = array( - -$position->getSequence(), - $position->getID(), - ); - } - } else { - $sort_map = mpull( - $column_tasks, - 'getPrioritySortVector', - 'getPHID'); - } - - $data = array( - 'removeFromBoard' => $remove_card, - 'sortMap' => $sort_map, - ); - - $rendering_engine = id(new PhabricatorBoardRenderingEngine()) - ->setViewer($viewer) - ->setObjects(array($task)) - ->setExcludedProjectPHIDs($board_phids); - - $card = $rendering_engine->renderCard($task->getPHID()); - - $item = $card->getItem(); - $item->addClass('phui-workcard'); - - $payload = array( - 'tasks' => $item, - 'data' => $data, - ); - - return id(new AphrontAjaxResponse()) - ->setContent( - array( - 'tasks' => $item, - 'data' => $data, - )); + ->setBoardPHID($board_phid) + ->setObjectPHID($object_phid) + ->setVisiblePHIDs($visible_phids) + ->buildResponse(); } diff --git a/src/applications/maniphest/storage/ManiphestTask.php b/src/applications/maniphest/storage/ManiphestTask.php index db72bc526c..5ac290078b 100644 --- a/src/applications/maniphest/storage/ManiphestTask.php +++ b/src/applications/maniphest/storage/ManiphestTask.php @@ -194,14 +194,6 @@ final class ManiphestTask extends ManiphestDAO return ManiphestTaskStatus::isClosedStatus($this->getStatus()); } - public function getPrioritySortVector() { - return array( - $this->getPriority(), - -$this->getSubpriority(), - $this->getID(), - ); - } - public function setProperty($key, $value) { $this->properties[$key] = $value; return $this; @@ -219,6 +211,16 @@ final class ManiphestTask extends ManiphestDAO return idx($this->properties, 'cover.thumbnailPHID'); } + public function getWorkboardOrderVectors() { + return array( + PhabricatorProjectColumn::ORDER_PRIORITY => array( + (int)-$this->getPriority(), + (double)-$this->getSubpriority(), + (int)-$this->getID(), + ), + ); + } + /* -( PhabricatorSubscribableInterface )----------------------------------- */ diff --git a/src/applications/project/controller/PhabricatorProjectBoardViewController.php b/src/applications/project/controller/PhabricatorProjectBoardViewController.php index f2eba7d9e8..2138367ca9 100644 --- a/src/applications/project/controller/PhabricatorProjectBoardViewController.php +++ b/src/applications/project/controller/PhabricatorProjectBoardViewController.php @@ -238,20 +238,6 @@ final class PhabricatorProjectBoardViewController 'boardPHID' => $project->getPHID(), )); - $behavior_config = array( - 'boardID' => $board_id, - 'projectPHID' => $project->getPHID(), - 'moveURI' => $this->getApplicationURI('move/'.$project->getID().'/'), - 'createURI' => $this->getCreateURI(), - 'uploadURI' => '/file/dropupload/', - 'coverURI' => $this->getApplicationURI('cover/'), - 'chunkThreshold' => PhabricatorFileStorageEngine::getChunkThreshold(), - 'order' => $this->sortKey, - ); - $this->initBehavior( - 'project-boards', - $behavior_config); - $visible_columns = array(); $column_phids = array(); $visible_phids = array(); @@ -297,6 +283,9 @@ final class PhabricatorProjectBoardViewController ->setEditMap($task_can_edit_map) ->setExcludedProjectPHIDs($select_phids); + $templates = array(); + $column_maps = array(); + $all_tasks = array(); foreach ($visible_columns as $column_phid => $column) { $column_tasks = $column_phids[$column_phid]; @@ -356,14 +345,35 @@ final class PhabricatorProjectBoardViewController )); foreach ($column_tasks as $task) { - $card = $rendering_engine->renderCard($task->getPHID()); - $cards->addItem($card->getItem()); + $object_phid = $task->getPHID(); + + $card = $rendering_engine->renderCard($object_phid); + $templates[$object_phid] = hsprintf('%s', $card->getItem()); + $column_maps[$column_phid][] = $object_phid; + + $all_tasks[$object_phid] = $task; } $panel->setCards($cards); $board->addPanel($panel); } + $behavior_config = array( + 'boardID' => $board_id, + 'projectPHID' => $project->getPHID(), + 'moveURI' => $this->getApplicationURI('move/'.$project->getID().'/'), + 'createURI' => $this->getCreateURI(), + 'uploadURI' => '/file/dropupload/', + 'coverURI' => $this->getApplicationURI('cover/'), + 'chunkThreshold' => PhabricatorFileStorageEngine::getChunkThreshold(), + 'order' => $this->sortKey, + 'templateMap' => $templates, + 'columnMaps' => $column_maps, + 'orderMaps' => mpull($all_tasks, 'getWorkboardOrderVectors'), + ); + $this->initBehavior('project-boards', $behavior_config); + + $sort_menu = $this->buildSortMenu( $viewer, $this->sortKey); diff --git a/src/applications/project/controller/PhabricatorProjectController.php b/src/applications/project/controller/PhabricatorProjectController.php index 755b7ca75e..57d531c951 100644 --- a/src/applications/project/controller/PhabricatorProjectController.php +++ b/src/applications/project/controller/PhabricatorProjectController.php @@ -150,51 +150,18 @@ abstract class PhabricatorProjectController extends PhabricatorController { protected function newCardResponse($board_phid, $object_phid) { $viewer = $this->getViewer(); - $project = id(new PhabricatorProjectQuery()) - ->setViewer($viewer) - ->withPHIDs(array($board_phid)) - ->executeOne(); - if (!$project) { - return new Aphront404Response(); + $request = $this->getRequest(); + $visible_phids = $request->getStrList('visiblePHIDs'); + if (!$visible_phids) { + $visible_phids = array(); } - // Reload the object so it reflects edits which have been applied. - $object = id(new ManiphestTaskQuery()) + return id(new PhabricatorBoardResponseEngine()) ->setViewer($viewer) - ->withPHIDs(array($object_phid)) - ->needProjectPHIDs(true) - ->executeOne(); - if (!$object) { - return new Aphront404Response(); - } - - $except_phids = array($board_phid); - if ($project->getHasSubprojects() || $project->getHasMilestones()) { - $descendants = id(new PhabricatorProjectQuery()) - ->setViewer($viewer) - ->withAncestorProjectPHIDs($except_phids) - ->execute(); - foreach ($descendants as $descendant) { - $except_phids[] = $descendant->getPHID(); - } - } - - $rendering_engine = id(new PhabricatorBoardRenderingEngine()) - ->setViewer($viewer) - ->setObjects(array($object)) - ->setExcludedProjectPHIDs($except_phids); - - $card = $rendering_engine->renderCard($object->getPHID()); - - $item = $card->getItem(); - $item->addClass('phui-workcard'); - - return id(new AphrontAjaxResponse()) - ->setContent( - array( - 'objectPHID' => $object->getPHID(), - 'cardHTML' => $item, - )); + ->setBoardPHID($board_phid) + ->setObjectPHID($object_phid) + ->setVisiblePHIDs($visible_phids) + ->buildResponse(); } } diff --git a/src/applications/project/engine/PhabricatorBoardResponseEngine.php b/src/applications/project/engine/PhabricatorBoardResponseEngine.php new file mode 100644 index 0000000000..d1fd3f33a8 --- /dev/null +++ b/src/applications/project/engine/PhabricatorBoardResponseEngine.php @@ -0,0 +1,146 @@ +viewer = $viewer; + return $this; + } + + public function getViewer() { + return $this->viewer; + } + + public function setBoardPHID($board_phid) { + $this->boardPHID = $board_phid; + return $this; + } + + public function getBoardPHID() { + return $this->boardPHID; + } + + public function setObjectPHID($object_phid) { + $this->objectPHID = $object_phid; + return $this; + } + + public function getObjectPHID() { + return $this->objectPHID; + } + + public function setVisiblePHIDs(array $visible_phids) { + $this->visiblePHIDs = $visible_phids; + return $this; + } + + public function getVisiblePHIDs() { + return $this->visiblePHIDs; + } + + public function buildResponse() { + $viewer = $this->getViewer(); + $object_phid = $this->getObjectPHID(); + $board_phid = $this->getBoardPHID(); + + // Load all the other tasks that are visible in the affected columns and + // perform layout for them. + $visible_phids = $this->getAllVisiblePHIDs(); + + $layout_engine = id(new PhabricatorBoardLayoutEngine()) + ->setViewer($viewer) + ->setBoardPHIDs(array($board_phid)) + ->setObjectPHIDs($visible_phids) + ->executeLayout(); + + $object_columns = $layout_engine->getObjectColumns( + $board_phid, + $object_phid); + + $natural = array(); + foreach ($object_columns as $column_phid => $column) { + $column_object_phids = $layout_engine->getColumnObjectPHIDs( + $board_phid, + $column_phid); + $natural[$column_phid] = array_values($column_object_phids); + } + + $all_visible = id(new ManiphestTaskQuery()) + ->setViewer($viewer) + ->withPHIDs($visible_phids) + ->execute(); + + $order_maps = array(); + foreach ($all_visible as $visible) { + $order_maps[$visible->getPHID()] = $visible->getWorkboardOrderVectors(); + } + + $template = $this->buildTemplate(); + + $payload = array( + 'objectPHID' => $object_phid, + 'cardHTML' => $template, + 'columnMaps' => $natural, + 'orderMaps' => $order_maps, + ); + + return id(new AphrontAjaxResponse()) + ->setContent($payload); + } + + private function buildTemplate() { + $viewer = $this->getViewer(); + $object_phid = $this->getObjectPHID(); + + $excluded_phids = $this->loadExcludedProjectPHIDs(); + + $object = id(new ManiphestTaskQuery()) + ->setViewer($viewer) + ->withPHIDs(array($object_phid)) + ->needProjectPHIDs(true) + ->executeOne(); + if (!$object) { + return new Aphront404Response(); + } + + $rendering_engine = id(new PhabricatorBoardRenderingEngine()) + ->setViewer($viewer) + ->setObjects(array($object)) + ->setExcludedProjectPHIDs($excluded_phids); + + $card = $rendering_engine->renderCard($object_phid); + + return hsprintf('%s', $card->getItem()); + } + + private function loadExcludedProjectPHIDs() { + $viewer = $this->getViewer(); + $board_phid = $this->getBoardPHID(); + + $exclude_phids = array($board_phid); + + $descendants = id(new PhabricatorProjectQuery()) + ->setViewer($viewer) + ->withAncestorProjectPHIDs($exclude_phids) + ->execute(); + + foreach ($descendants as $descendant) { + $exclude_phids[] = $descendant->getPHID(); + } + + return array_fuse($exclude_phids); + } + + private function getAllVisiblePHIDs() { + $visible_phids = $this->getVisiblePHIDs(); + $visible_phids[] = $this->getObjectPHID(); + $visible_phids = array_fuse($visible_phids); + return $visible_phids; + } + +} diff --git a/src/applications/project/view/ProjectBoardTaskCard.php b/src/applications/project/view/ProjectBoardTaskCard.php index 065acc6c13..7e927bdd72 100644 --- a/src/applications/project/view/ProjectBoardTaskCard.php +++ b/src/applications/project/view/ProjectBoardTaskCard.php @@ -78,10 +78,6 @@ final class ProjectBoardTaskCard extends Phobject { ->setHref('/T'.$task->getID()) ->addSigil('project-card') ->setDisabled($task->isClosed()) - ->setMetadata( - array( - 'objectPHID' => $task->getPHID(), - )) ->addAction( id(new PHUIListItemView()) ->setName(pht('Edit')) @@ -115,6 +111,8 @@ final class ProjectBoardTaskCard extends Phobject { $card->addAttribute($tag_list); } + $card->addClass('phui-workcard'); + return $card; } diff --git a/webroot/rsrc/js/application/projects/Workboard.js b/webroot/rsrc/js/application/projects/Workboard.js deleted file mode 100644 index 96214a4c29..0000000000 --- a/webroot/rsrc/js/application/projects/Workboard.js +++ /dev/null @@ -1,242 +0,0 @@ -/** - * @provides javelin-workboard - * @requires javelin-install - * javelin-dom - * javelin-util - * javelin-vector - * javelin-stratcom - * javelin-workflow - * phabricator-draggable-list - * phabricator-drag-and-drop-file-upload - * @javelin - */ - -JX.install('Workboard', { - - construct: function(config) { - this._config = config; - - this._boardNodes = {}; - this._columnMap = {}; - }, - - properties: { - uploadURI: null, - coverURI: null, - moveURI: null, - chunkThreshold: null - }, - - members: { - _config: null, - _boardNodes: null, - _currentBoard: null, - - _panOrigin: null, - _panNode: null, - _panX: null, - - _columnMap: null, - - start: function() { - this._setupCoverImageHandlers(); - this._setupPanHandlers(); - - return this; - }, - - addBoard: function(board_phid, board_node) { - this._currentBoard = board_phid; - this._boardNodes[board_phid] = board_node; - this._setupDragHandlers(board_node); - }, - - _getConfig: function() { - return this._config; - }, - - _setupCoverImageHandlers: function() { - if (!JX.PhabricatorDragAndDropFileUpload.isSupported()) { - return; - } - - var drop = new JX.PhabricatorDragAndDropFileUpload('project-card') - .setURI(this.getUploadURI()) - .setChunkThreshold(this.getChunkThreshold()); - - drop.listen('didBeginDrag', function(node) { - JX.DOM.alterClass(node, 'phui-workcard-upload-target', true); - }); - - drop.listen('didEndDrag', function(node) { - JX.DOM.alterClass(node, 'phui-workcard-upload-target', false); - }); - - drop.listen('didUpload', JX.bind(this, this._oncoverupload)); - - drop.start(); - }, - - _oncoverupload: function(file) { - var node = file.getTargetNode(); - var board = JX.DOM.findAbove(node, 'div', 'jx-workboard'); - - var data = { - boardPHID: JX.Stratcom.getData(board).boardPHID, - objectPHID: JX.Stratcom.getData(node).objectPHID, - filePHID: file.getPHID() - }; - - new JX.Workflow(this.getCoverURI(), data) - .setHandler(JX.bind(this, this._queueCardUpdate)) - .start(); - }, - - _setupPanHandlers: function() { - var mousedown = JX.bind(this, this._onpanmousedown); - var mousemove = JX.bind(this, this._onpanmousemove); - var mouseup = JX.bind(this, this._onpanmouseup); - - JX.Stratcom.listen('mousedown', 'workboard-shadow', mousedown); - JX.Stratcom.listen('mousemove', null, mousemove); - JX.Stratcom.listen('mouseup', null, mouseup); - }, - - _onpanmousedown: function(e) { - if (!JX.Device.isDesktop()) { - return; - } - - if (e.getNode('workpanel')) { - return; - } - - if (JX.Stratcom.pass()) { - return; - } - - e.kill(); - - this._panOrigin = JX.$V(e); - this._panNode = e.getNode('workboard-shadow'); - this._panX = this._panNode.scrollLeft; - }, - - _onpanmousemove: function(e) { - if (!this._panOrigin) { - return; - } - - var cursor = JX.$V(e); - this._panNode.scrollLeft = this._panX + (this._panOrigin.x - cursor.x); - }, - - _onpanmouseup: function() { - this._panOrigin = null; - }, - - - _setupDragHandlers: function(board_node) { - var columns = this._findBoardColumns(board_node); - var column; - var ii; - var lists = []; - - for (ii = 0; ii < columns.length; ii++) { - column = columns[ii]; - - var list = new JX.DraggableList('project-card', column) - .setOuterContainer(board_node) - .setFindItemsHandler(JX.bind(this, this._findCardsInColumn, column)) - .setCanDragX(true) - .setHasInfiniteHeight(true); - - // TODO: Restore these behaviors. - // list.listen('didSend', JX.bind(list, onupdate, cols[ii])); - // list.listen('didReceive', JX.bind(list, onupdate, cols[ii])); - // onupdate(cols[ii]); - - list.listen('didDrop', JX.bind(this, this._onmovecard, list)); - - lists.push(list); - } - - for (ii = 0; ii < lists.length; ii++) { - lists[ii].setGroup(lists); - } - }, - - _findBoardColumns: function(board_node) { - return JX.DOM.scry(board_node, 'ul', 'project-column'); - }, - - _findCardsInColumn: function(column_node) { - return JX.DOM.scry(column_node, 'li', 'project-card'); - }, - - _onmovecard: function(list, item, after_node) { - list.lock(); - JX.DOM.alterClass(item, 'drag-sending', true); - - var item_phid = JX.Stratcom.getData(item).objectPHID; - var data = { - objectPHID: item_phid, - columnPHID: JX.Stratcom.getData(list.getRootNode()).columnPHID - }; - - if (after_node) { - data.afterPHID = JX.Stratcom.getData(after_node).objectPHID; - } - - var before_node = item.nextSibling; - if (before_node) { - var before_phid = JX.Stratcom.getData(before_node).objectPHID; - if (before_phid) { - data.beforePHID = before_phid; - } - } - - // TODO: This should be managed per-board. - var config = this._getConfig(); - data.order = config.order; - - new JX.Workflow(this.getMoveURI(), data) - .setHandler(JX.bind(this, this._oncardupdate, item, list)) - .start(); - }, - - _oncardupdate: function(item, list, response) { - list.unlock(); - JX.DOM.alterClass(item, 'drag-sending', false); - - this._queueCardUpdate(response); - }, - - _queueCardUpdate: function(response) { - var board_node = this._boardNodes[this._currentBoard]; - - var columns = this._findBoardColumns(board_node); - var cards; - var ii; - var jj; - var data; - - for (ii = 0; ii < columns.length; ii++) { - cards = this._findCardsInColumn(columns[ii]); - for (jj = 0; jj < cards.length; jj++) { - data = JX.Stratcom.getData(cards[jj]); - if (data.objectPHID == response.objectPHID) { - this._replaceCard(cards[jj], JX.$H(response.cardHTML)); - } - } - } - - }, - - _replaceCard: function(old_node, new_node) { - JX.DOM.replace(old_node, new_node); - } - - } - -}); diff --git a/webroot/rsrc/js/application/projects/WorkboardBoard.js b/webroot/rsrc/js/application/projects/WorkboardBoard.js new file mode 100644 index 0000000000..343086d569 --- /dev/null +++ b/webroot/rsrc/js/application/projects/WorkboardBoard.js @@ -0,0 +1,221 @@ +/** + * @provides javelin-workboard-board + * @requires javelin-install + * javelin-dom + * javelin-util + * javelin-stratcom + * javelin-workflow + * phabricator-draggable-list + * javelin-workboard-column + * @javelin + */ + +JX.install('WorkboardBoard', { + + construct: function(controller, phid, root) { + this._controller = controller; + this._phid = phid; + this._root = root; + + this._templates = {}; + this._orderMaps = {}; + this._buildColumns(); + }, + + properties: { + order: null, + }, + + members: { + _controller: null, + _phid: null, + _root: null, + _columns: null, + _templates: null, + _orderMaps: null, + + getRoot: function() { + return this._root; + }, + + getColumns: function() { + return this._columns; + }, + + getColumn: function(k) { + return this._columns[k]; + }, + + getPHID: function() { + return this._phid; + }, + + setCardTemplate: function(phid, template) { + this._templates[phid] = template; + return this; + }, + + getCardTemplate: function(phid) { + return this._templates[phid]; + }, + + getController: function() { + return this._controller; + }, + + setOrderMap: function(phid, map) { + this._orderMaps[phid] = map; + return this; + }, + + getOrderVector: function(phid, key) { + return this._orderMaps[phid][key]; + }, + + start: function() { + this._setupDragHandlers(); + + for (var k in this._columns) { + this._columns[k].redraw(); + } + }, + + _buildColumns: function() { + var nodes = JX.DOM.scry(this.getRoot(), 'ul', 'project-column'); + + this._columns = {}; + for (var ii = 0; ii < nodes.length; ii++) { + var node = nodes[ii]; + var data = JX.Stratcom.getData(node); + var phid = data.columnPHID; + + this._columns[phid] = new JX.WorkboardColumn(this, phid, node); + } + }, + + _setupDragHandlers: function() { + var columns = this.getColumns(); + + var lists = []; + for (var k in columns) { + var column = columns[k]; + + var list = new JX.DraggableList('project-card', column.getRoot()) + .setOuterContainer(this.getRoot()) + .setFindItemsHandler(JX.bind(column, column.getCardNodes)) + .setCanDragX(true) + .setHasInfiniteHeight(true); + + list.listen('didDrop', JX.bind(this, this._onmovecard, list)); + + lists.push(list); + } + + for (var ii = 0; ii < lists.length; ii++) { + lists[ii].setGroup(lists); + } + }, + + _findCardsInColumn: function(column_node) { + return JX.DOM.scry(column_node, 'li', 'project-card'); + }, + + _onmovecard: function(list, item, after_node, src_list) { + list.lock(); + JX.DOM.alterClass(item, 'drag-sending', true); + + var src_phid = JX.Stratcom.getData(src_list.getRootNode()).columnPHID; + var dst_phid = JX.Stratcom.getData(list.getRootNode()).columnPHID; + + var item_phid = JX.Stratcom.getData(item).objectPHID; + var data = { + objectPHID: item_phid, + columnPHID: dst_phid, + order: this.getOrder() + }; + + if (after_node) { + data.afterPHID = JX.Stratcom.getData(after_node).objectPHID; + } + + var before_node = item.nextSibling; + if (before_node) { + var before_phid = JX.Stratcom.getData(before_node).objectPHID; + if (before_phid) { + data.beforePHID = before_phid; + } + } + + var visible_phids = []; + var column = this.getColumn(dst_phid); + for (var object_phid in column.getCards()) { + visible_phids.push(object_phid); + } + + data.visiblePHIDs = visible_phids.join(','); + + var onupdate = JX.bind( + this, + this._oncardupdate, + list, + src_phid, + dst_phid, + data.afterPHID); + + new JX.Workflow(this.getController().getMoveURI(), data) + .setHandler(onupdate) + .start(); + }, + + _oncardupdate: function(list, src_phid, dst_phid, after_phid, response) { + var src_column = this.getColumn(src_phid); + var dst_column = this.getColumn(dst_phid); + + var card = src_column.removeCard(response.objectPHID); + dst_column.addCard(card, after_phid); + + this.updateCard(response); + + list.unlock(); + }, + + updateCard: function(response) { + var columns = this.getColumns(); + + var phid = response.objectPHID; + + if (!this._templates[phid]) { + for (var add_phid in response.columnMaps) { + this.getColumn(add_phid).newCard(phid); + } + } + + this.setCardTemplate(phid, response.cardHTML); + + var order_maps = response.orderMaps; + for (var order_phid in order_maps) { + this.setOrderMap(order_phid, order_maps[order_phid]); + } + + var column_maps = response.columnMaps; + for (var natural_phid in column_maps) { + this.getColumn(natural_phid).setNaturalOrder(column_maps[natural_phid]); + } + + for (var column_phid in columns) { + var cards = columns[column_phid].getCards(); + for (var object_phid in cards) { + if (object_phid !== phid) { + continue; + } + + var card = cards[object_phid]; + card.redraw(); + } + columns[column_phid].redraw(); + } + } + + } + +}); diff --git a/webroot/rsrc/js/application/projects/WorkboardCard.js b/webroot/rsrc/js/application/projects/WorkboardCard.js new file mode 100644 index 0000000000..69294219c0 --- /dev/null +++ b/webroot/rsrc/js/application/projects/WorkboardCard.js @@ -0,0 +1,56 @@ +/** + * @provides javelin-workboard-card + * @requires javelin-install + * @javelin + */ + +JX.install('WorkboardCard', { + + construct: function(column, phid) { + this._column = column; + this._phid = phid; + }, + + members: { + _column: null, + _phid: null, + _root: null, + + getPHID: function() { + return this._phid; + }, + + getColumn: function() { + return this._column; + }, + + setColumn: function(column) { + this._column = column; + }, + + getNode: function() { + if (!this._root) { + var phid = this.getPHID(); + var template = this.getColumn().getBoard().getCardTemplate(phid); + this._root = JX.$H(template).getFragment().firstChild; + + JX.Stratcom.getData(this._root).objectPHID = this.getPHID(); + } + return this._root; + }, + + redraw: function() { + var old_node = this._root; + this._root = null; + var new_node = this.getNode(); + + if (old_node && old_node.parentNode) { + JX.DOM.replace(old_node, new_node); + } + + return this; + } + + } + +}); diff --git a/webroot/rsrc/js/application/projects/WorkboardColumn.js b/webroot/rsrc/js/application/projects/WorkboardColumn.js new file mode 100644 index 0000000000..77f9ca3146 --- /dev/null +++ b/webroot/rsrc/js/application/projects/WorkboardColumn.js @@ -0,0 +1,177 @@ +/** + * @provides javelin-workboard-column + * @requires javelin-install + * javelin-workboard-card + * @javelin + */ + +JX.install('WorkboardColumn', { + + construct: function(board, phid, root) { + this._board = board; + this._phid = phid; + this._root = root; + + this._cards = {}; + this._naturalOrder = []; + }, + + members: { + _phid: null, + _root: null, + _board: null, + _cards: null, + _naturalOrder: null, + + getPHID: function() { + return this._phid; + }, + + getRoot: function() { + return this._root; + }, + + getCards: function() { + return this._cards; + }, + + getCard: function(phid) { + return this._cards[phid]; + }, + + getBoard: function() { + return this._board; + }, + + setNaturalOrder: function(order) { + this._naturalOrder = order; + return this; + }, + + newCard: function(phid) { + var card = new JX.WorkboardCard(this, phid); + + this._cards[phid] = card; + this._naturalOrder.push(phid); + + return card; + }, + + removeCard: function(phid) { + var card = this._cards[phid]; + delete this._cards[phid]; + + for (var ii = 0; ii < this._naturalOrder.length; ii++) { + if (this._naturalOrder[ii] == phid) { + this._naturalOrder.splice(ii, 1); + break; + } + } + + return card; + }, + + addCard: function(card, after) { + var phid = card.getPHID(); + + card.setColumn(this); + this._cards[phid] = card; + + var index = 0; + + if (after) { + for (var ii = 0; ii < this._naturalOrder.length; ii++) { + if (this._naturalOrder[ii] == after) { + index = ii + 1; + break; + } + } + } + + if (index > this._naturalOrder.length) { + this._naturalOrder.push(phid); + } else { + this._naturalOrder.splice(index, 0, phid); + } + + return this; + }, + + getCardNodes: function() { + var cards = this.getCards(); + + var nodes = []; + for (var k in cards) { + nodes.push(cards[k].getNode()); + } + + return nodes; + }, + + getCardPHIDs: function() { + return JX.keys(this.getCards()); + }, + + redraw: function() { + var order = this.getBoard().getOrder(); + + var list; + if (order == 'natural') { + list = this._getCardsSortedNaturally(); + } else { + list = this._getCardsSortedByKey(order); + } + + var content = []; + for (var ii = 0; ii < list.length; ii++) { + var node = list[ii].getNode(); + content.push(node); + } + + JX.DOM.setContent(this.getRoot(), content); + }, + + _getCardsSortedNaturally: function() { + var list = []; + + for (var ii = 0; ii < this._naturalOrder.length; ii++) { + var phid = this._naturalOrder[ii]; + list.push(this.getCard(phid)); + } + + return list; + }, + + _getCardsSortedByKey: function(order) { + var cards = this.getCards(); + + var list = []; + for (var k in cards) { + list.push(cards[k]); + } + + list.sort(JX.bind(this, this._sortCards, order)); + + return list; + }, + + _sortCards: function(order, u, v) { + var ud = this.getBoard().getOrderVector(u.getPHID(), order); + var vd = this.getBoard().getOrderVector(v.getPHID(), order); + + for (var ii = 0; ii < ud.length; ii++) { + if (ud[ii] > vd[ii]) { + return 1; + } + + if (ud[ii] < vd[ii]) { + return -1; + } + } + + return 0; + } + + } + +}); diff --git a/webroot/rsrc/js/application/projects/WorkboardController.js b/webroot/rsrc/js/application/projects/WorkboardController.js new file mode 100644 index 0000000000..5b5f5de181 --- /dev/null +++ b/webroot/rsrc/js/application/projects/WorkboardController.js @@ -0,0 +1,201 @@ +/** + * @provides javelin-workboard-controller + * @requires javelin-install + * javelin-dom + * javelin-util + * javelin-vector + * javelin-stratcom + * javelin-workflow + * phabricator-drag-and-drop-file-upload + * javelin-workboard-board + * @javelin + */ + +JX.install('WorkboardController', { + + construct: function() { + this._boards = {}; + }, + + properties: { + uploadURI: null, + coverURI: null, + moveURI: null, + createURI: null, + chunkThreshold: null + }, + + members: { + _boards: null, + + _panOrigin: null, + _panNode: null, + _panX: null, + + start: function() { + this._setupCoverImageHandlers(); + this._setupPanHandlers(); + this._setupEditHandlers(); + + return this; + }, + + newBoard: function(phid, node) { + var board = new JX.WorkboardBoard(this, phid, node); + this._boards[phid] = board; + return board; + }, + + _getBoard: function(board_phid) { + return this._boards[board_phid]; + }, + + _setupCoverImageHandlers: function() { + if (!JX.PhabricatorDragAndDropFileUpload.isSupported()) { + return; + } + + var drop = new JX.PhabricatorDragAndDropFileUpload('project-card') + .setURI(this.getUploadURI()) + .setChunkThreshold(this.getChunkThreshold()); + + drop.listen('didBeginDrag', function(node) { + JX.DOM.alterClass(node, 'phui-workcard-upload-target', true); + }); + + drop.listen('didEndDrag', function(node) { + JX.DOM.alterClass(node, 'phui-workcard-upload-target', false); + }); + + drop.listen('didUpload', JX.bind(this, this._oncoverupload)); + + drop.start(); + }, + + _oncoverupload: function(file) { + var node = file.getTargetNode(); + + var board = this._getBoardFromNode(node); + + var column_node = JX.DOM.findAbove(node, 'ul', 'project-column'); + var column_phid = JX.Stratcom.getData(column_node).columnPHID; + var column = board.getColumn(column_phid); + + var data = { + boardPHID: board.getPHID(), + objectPHID: JX.Stratcom.getData(node).objectPHID, + filePHID: file.getPHID(), + visiblePHIDs: column.getCardPHIDs() + }; + + new JX.Workflow(this.getCoverURI(), data) + .setHandler(JX.bind(board, board.updateCard)) + .start(); + }, + + _getBoardFromNode: function(node) { + var board_node = JX.DOM.findAbove(node, 'div', 'jx-workboard'); + var board_phid = JX.Stratcom.getData(board_node).boardPHID; + return this._getBoard(board_phid); + }, + + _setupPanHandlers: function() { + var mousedown = JX.bind(this, this._onpanmousedown); + var mousemove = JX.bind(this, this._onpanmousemove); + var mouseup = JX.bind(this, this._onpanmouseup); + + JX.Stratcom.listen('mousedown', 'workboard-shadow', mousedown); + JX.Stratcom.listen('mousemove', null, mousemove); + JX.Stratcom.listen('mouseup', null, mouseup); + }, + + _onpanmousedown: function(e) { + if (!JX.Device.isDesktop()) { + return; + } + + if (e.getNode('workpanel')) { + return; + } + + if (JX.Stratcom.pass()) { + return; + } + + e.kill(); + + this._panOrigin = JX.$V(e); + this._panNode = e.getNode('workboard-shadow'); + this._panX = this._panNode.scrollLeft; + }, + + _onpanmousemove: function(e) { + if (!this._panOrigin) { + return; + } + + var cursor = JX.$V(e); + this._panNode.scrollLeft = this._panX + (this._panOrigin.x - cursor.x); + }, + + _onpanmouseup: function() { + this._panOrigin = null; + }, + + _setupEditHandlers: function() { + var onadd = JX.bind(this, this._onaddcard); + var onedit = JX.bind(this, this._oneditcard); + + JX.Stratcom.listen('click', 'column-add-task', onadd); + JX.Stratcom.listen('click', 'edit-project-card', onedit); + }, + + _onaddcard: function(e) { + // We want the 'boards-dropdown-menu' behavior to see this event and + // close the dropdown, but don't want to follow the link. + e.prevent(); + + var column_data = e.getNodeData('column-add-task'); + var column_phid = column_data.columnPHID; + + var board_phid = column_data.projectPHID; + var board = this._getBoard(board_phid); + var column = board.getColumn(column_phid); + + var request_data = { + responseType: 'card', + columnPHID: column.getPHID(), + projects: board.getPHID(), + visiblePHIDs: column.getCardPHIDs(), + order: board.getOrder() + }; + + new JX.Workflow(this.getCreateURI(), request_data) + .setHandler(JX.bind(board, board.updateCard)) + .start(); + }, + + _oneditcard: function(e) { + e.kill(); + + var column_node = e.getNode('project-column'); + var column_phid = JX.Stratcom.getData(column_node).columnPHID; + + var board = this._getBoardFromNode(column_node); + var column = board.getColumn(column_phid); + + var request_data = { + responseType: 'card', + columnPHID: column.getPHID(), + visiblePHIDs: column.getCardPHIDs(), + order: board.getOrder() + }; + + new JX.Workflow(e.getNode('tag:a').href, request_data) + .setHandler(JX.bind(board, board.updateCard)) + .start(); + } + + } + +}); diff --git a/webroot/rsrc/js/application/projects/behavior-project-boards.js b/webroot/rsrc/js/application/projects/behavior-project-boards.js index b084a2d00e..5267a09fc2 100644 --- a/webroot/rsrc/js/application/projects/behavior-project-boards.js +++ b/webroot/rsrc/js/application/projects/behavior-project-boards.js @@ -6,9 +6,7 @@ * javelin-vector * javelin-stratcom * javelin-workflow - * phabricator-draggable-list - * phabricator-drag-and-drop-file-upload - * javelin-workboard + * javelin-workboard-controller */ JX.behavior('project-boards', function(config, statics) { @@ -61,66 +59,6 @@ JX.behavior('project-boards', function(config, statics) { } } - - function colsort(u, v) { - var ud = JX.Stratcom.getData(u).sort || []; - var vd = JX.Stratcom.getData(v).sort || []; - - for (var ii = 0; ii < ud.length; ii++) { - - if (parseInt(ud[ii]) < parseInt(vd[ii])) { - return 1; - } - if (parseInt(ud[ii]) > parseInt(vd[ii])) { - return -1; - } - } - - return 0; - } - - function onedit(column, r) { - var new_card = JX.$H(r.tasks).getNode(); - var new_data = JX.Stratcom.getData(new_card); - var items = finditems(column); - var edited = false; - var remove_index = null; - - for (var ii = 0; ii < items.length; ii++) { - var item = items[ii]; - - var data = JX.Stratcom.getData(item); - var phid = data.objectPHID; - - if (phid == new_data.objectPHID) { - if (r.data.removeFromBoard) { - remove_index = ii; - } - items[ii] = new_card; - data = new_data; - edited = true; - } - - data.sort = r.data.sortMap[data.objectPHID] || data.sort; - } - - // this is an add then...! - if (!edited) { - items[items.length + 1] = new_card; - new_data.sort = r.data.sortMap[new_data.objectPHID] || new_data.sort; - } - - if (remove_index !== null) { - items.splice(remove_index, 1); - } - - items.sort(colsort); - - JX.DOM.setContent(column, items); - - onupdate(column); - }; - function update_statics(update_config) { statics.boardID = update_config.boardID; statics.projectPHID = update_config.projectPHID; @@ -130,56 +68,6 @@ JX.behavior('project-boards', function(config, statics) { } function setup() { - - JX.Stratcom.listen( - 'click', - ['edit-project-card'], - function(e) { - e.kill(); - var column = e.getNode('project-column'); - var request_data = { - responseType: 'card', - columnPHID: JX.Stratcom.getData(column).columnPHID, - order: statics.order - }; - new JX.Workflow(e.getNode('tag:a').href, request_data) - .setHandler(JX.bind(null, onedit, column)) - .start(); - }); - - JX.Stratcom.listen( - 'click', - ['column-add-task'], - function (e) { - - // We want the 'boards-dropdown-menu' behavior to see this event and - // close the dropdown, but don't want to follow the link. - e.prevent(); - - var column_data = e.getNodeData('column-add-task'); - var column_phid = column_data.columnPHID; - - var request_data = { - responseType: 'card', - columnPHID: column_phid, - projects: column_data.projectPHID, - order: statics.order - }; - - var cols = getcolumns(); - var ii; - var column; - for (ii = 0; ii < cols.length; ii++) { - if (JX.Stratcom.getData(cols[ii]).columnPHID == column_phid) { - column = cols[ii]; - break; - } - } - new JX.Workflow(statics.createURI, request_data) - .setHandler(JX.bind(null, onedit, column)) - .start(); - }); - JX.Stratcom.listen('click', 'boards-dropdown-menu', function(e) { var data = e.getNodeData('boards-dropdown-menu'); if (data.menu) { @@ -234,14 +122,40 @@ JX.behavior('project-boards', function(config, statics) { } if (!statics.workboard) { - statics.workboard = new JX.Workboard(config) + statics.workboard = new JX.WorkboardController() .setUploadURI(config.uploadURI) .setCoverURI(config.coverURI) .setMoveURI(config.moveURI) + .setCreateURI(config.createURI) .setChunkThreshold(config.chunkThreshold) .start(); } - statics.workboard.addBoard(config.projectPHID, JX.$(config.boardID)); + var board_phid = config.projectPHID; + var board_node = JX.$(config.boardID); + + var board = statics.workboard.newBoard(board_phid, board_node) + .setOrder(config.order); + + var templates = config.templateMap; + for (var k in templates) { + board.setCardTemplate(k, templates[k]); + } + + var column_maps = config.columnMaps; + for (var column_phid in column_maps) { + var column = board.getColumn(column_phid); + var column_map = column_maps[column_phid]; + for (var ii = 0; ii < column_map.length; ii++) { + column.newCard(column_map[ii]); + } + } + + var order_maps = config.orderMaps; + for (var object_phid in order_maps) { + board.setOrderMap(object_phid, order_maps[object_phid]); + } + + board.start(); }); From 888ea4e4fe7424ef46bb9111fa28abb005a8a07f Mon Sep 17 00:00:00 2001 From: Chad Little Date: Wed, 10 Feb 2016 13:56:12 -0800 Subject: [PATCH 38/65] Minor Segment UI tweaks Summary: Nicer spacing, no border. Test Plan: Test collapsed and wide layouts Reviewers: epriestley Reviewed By: epriestley Subscribers: Korvin Differential Revision: https://secure.phabricator.com/D15238 --- resources/celerity/map.php | 10 +++++----- webroot/rsrc/css/phui/phui-profile-menu.css | 11 +++++++++-- webroot/rsrc/css/phui/phui-segment-bar-view.css | 1 - 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 870d8fa6e8..6aeac0d365 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -7,7 +7,7 @@ */ return array( 'names' => array( - 'core.pkg.css' => 'e8763436', + 'core.pkg.css' => '69616aad', 'core.pkg.js' => 'd7daa6d8', 'darkconsole.pkg.js' => 'e7393ebb', 'differential.pkg.css' => '2de124c9', @@ -146,10 +146,10 @@ return array( 'rsrc/css/phui/phui-object-item-list-view.css' => '8f443e8b', 'rsrc/css/phui/phui-pager.css' => 'bea33d23', 'rsrc/css/phui/phui-pinboard-view.css' => '2495140e', - 'rsrc/css/phui/phui-profile-menu.css' => '2d5f0c75', + 'rsrc/css/phui/phui-profile-menu.css' => 'f709256c', 'rsrc/css/phui/phui-property-list-view.css' => '27b2849e', 'rsrc/css/phui/phui-remarkup-preview.css' => '1a8f2591', - 'rsrc/css/phui/phui-segment-bar-view.css' => '52e7e529', + 'rsrc/css/phui/phui-segment-bar-view.css' => '6622f0a1', 'rsrc/css/phui/phui-spacing.css' => '042804d6', 'rsrc/css/phui/phui-status.css' => '888cedb8', 'rsrc/css/phui/phui-tag-view.css' => '9d5d4400', @@ -831,10 +831,10 @@ return array( 'phui-object-item-list-view-css' => '8f443e8b', 'phui-pager-css' => 'bea33d23', 'phui-pinboard-view-css' => '2495140e', - 'phui-profile-menu-css' => '2d5f0c75', + 'phui-profile-menu-css' => 'f709256c', 'phui-property-list-view-css' => '27b2849e', 'phui-remarkup-preview-css' => '1a8f2591', - 'phui-segment-bar-view-css' => '52e7e529', + 'phui-segment-bar-view-css' => '6622f0a1', 'phui-spacing-css' => '042804d6', 'phui-status-list-view-css' => '888cedb8', 'phui-tag-view-css' => '9d5d4400', diff --git a/webroot/rsrc/css/phui/phui-profile-menu.css b/webroot/rsrc/css/phui/phui-profile-menu.css index d1704feeee..76a51d9130 100644 --- a/webroot/rsrc/css/phui/phui-profile-menu.css +++ b/webroot/rsrc/css/phui/phui-profile-menu.css @@ -155,9 +155,16 @@ padding: 18px 15px; } -.phui-profile-menu .phabricator-side-menu .phui-profile-segment-bar { +.phui-profile-menu .phabricator-side-menu .phui-profile-segment-bar { color: {$menu.profile.text}; - padding: 12px 15px 18px; + font-size: {$smallerfontsize}; + -webkit-font-smoothing: antialiased; + padding: 8px 12px 16px; +} + +.phui-profile-menu .phui-profile-menu-collapsed .phabricator-side-menu + .phui-profile-segment-bar { + padding: 8px 8px 16px; } diff --git a/webroot/rsrc/css/phui/phui-segment-bar-view.css b/webroot/rsrc/css/phui/phui-segment-bar-view.css index c0ee6a27a8..5d07039c31 100644 --- a/webroot/rsrc/css/phui/phui-segment-bar-view.css +++ b/webroot/rsrc/css/phui/phui-segment-bar-view.css @@ -13,7 +13,6 @@ position: relative; overflow: hidden; height: 8px; - border: 1px solid rgba(0, 0, 0, 0.15); } .phui-segment-bar-segment-view { From 1fb76655dfb4613cbeafde8179d9283aa322040b Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 10 Feb 2016 13:53:36 -0800 Subject: [PATCH 39/65] Restore column point counts to workboards Summary: Ref T4427. Test Plan: - Dragged a 17 XP task from "Hunting" to "Slain". - Saw 17 XP move. - Level up! Reviewers: chad Reviewed By: chad Maniphest Tasks: T4427 Differential Revision: https://secure.phabricator.com/D15237 --- .../maniphest/storage/ManiphestTask.php | 7 ++ .../PhabricatorProjectBoardViewController.php | 24 ++-- .../engine/PhabricatorBoardResponseEngine.php | 41 ++++--- .../js/application/projects/WorkboardBoard.js | 43 ++++++- .../js/application/projects/WorkboardCard.js | 12 ++ .../application/projects/WorkboardColumn.js | 111 +++++++++++++++++- .../projects/behavior-project-boards.js | 56 ++------- 7 files changed, 212 insertions(+), 82 deletions(-) diff --git a/src/applications/maniphest/storage/ManiphestTask.php b/src/applications/maniphest/storage/ManiphestTask.php index 5ac290078b..93a5f5562c 100644 --- a/src/applications/maniphest/storage/ManiphestTask.php +++ b/src/applications/maniphest/storage/ManiphestTask.php @@ -221,6 +221,13 @@ final class ManiphestTask extends ManiphestDAO ); } + public function getWorkboardProperties() { + return array( + 'status' => $this->getStatus(), + 'points' => (double)$this->getPoints(), + ); + } + /* -( PhabricatorSubscribableInterface )----------------------------------- */ diff --git a/src/applications/project/controller/PhabricatorProjectBoardViewController.php b/src/applications/project/controller/PhabricatorProjectBoardViewController.php index 2138367ca9..c075072d9f 100644 --- a/src/applications/project/controller/PhabricatorProjectBoardViewController.php +++ b/src/applications/project/controller/PhabricatorProjectBoardViewController.php @@ -318,14 +318,17 @@ final class PhabricatorProjectBoardViewController $column_menu = $this->buildColumnMenu($project, $column); $panel->addHeaderAction($column_menu); - $tag_id = celerity_generate_unique_node_id(); - $tag_content_id = celerity_generate_unique_node_id(); - $count_tag = id(new PHUITagView()) ->setType(PHUITagView::TYPE_SHADE) ->setShade(PHUITagView::COLOR_BLUE) - ->setID($tag_id) - ->setName(phutil_tag('span', array('id' => $tag_content_id), '-')) + ->addSigil('column-points') + ->setName( + javelin_tag( + 'span', + array( + 'sigil' => 'column-points-content', + ), + pht('-'))) ->setStyle('display: none'); $panel->setHeaderTag($count_tag); @@ -339,8 +342,6 @@ final class PhabricatorProjectBoardViewController ->setMetadata( array( 'columnPHID' => $column->getPHID(), - 'countTagID' => $tag_id, - 'countTagContentID' => $tag_content_id, 'pointLimit' => $column->getPointLimit(), )); @@ -359,17 +360,22 @@ final class PhabricatorProjectBoardViewController } $behavior_config = array( - 'boardID' => $board_id, - 'projectPHID' => $project->getPHID(), 'moveURI' => $this->getApplicationURI('move/'.$project->getID().'/'), 'createURI' => $this->getCreateURI(), 'uploadURI' => '/file/dropupload/', 'coverURI' => $this->getApplicationURI('cover/'), 'chunkThreshold' => PhabricatorFileStorageEngine::getChunkThreshold(), + 'pointsEnabled' => ManiphestTaskPoints::getIsEnabled(), + + 'boardPHID' => $project->getPHID(), 'order' => $this->sortKey, 'templateMap' => $templates, 'columnMaps' => $column_maps, 'orderMaps' => mpull($all_tasks, 'getWorkboardOrderVectors'), + 'propertyMaps' => mpull($all_tasks, 'getWorkboardProperties'), + + 'boardID' => $board_id, + 'projectPHID' => $project->getPHID(), ); $this->initBehavior('project-boards', $behavior_config); diff --git a/src/applications/project/engine/PhabricatorBoardResponseEngine.php b/src/applications/project/engine/PhabricatorBoardResponseEngine.php index d1fd3f33a8..969dfa3bc8 100644 --- a/src/applications/project/engine/PhabricatorBoardResponseEngine.php +++ b/src/applications/project/engine/PhabricatorBoardResponseEngine.php @@ -80,25 +80,6 @@ final class PhabricatorBoardResponseEngine extends Phobject { $order_maps[$visible->getPHID()] = $visible->getWorkboardOrderVectors(); } - $template = $this->buildTemplate(); - - $payload = array( - 'objectPHID' => $object_phid, - 'cardHTML' => $template, - 'columnMaps' => $natural, - 'orderMaps' => $order_maps, - ); - - return id(new AphrontAjaxResponse()) - ->setContent($payload); - } - - private function buildTemplate() { - $viewer = $this->getViewer(); - $object_phid = $this->getObjectPHID(); - - $excluded_phids = $this->loadExcludedProjectPHIDs(); - $object = id(new ManiphestTaskQuery()) ->setViewer($viewer) ->withPHIDs(array($object_phid)) @@ -108,6 +89,28 @@ final class PhabricatorBoardResponseEngine extends Phobject { return new Aphront404Response(); } + $template = $this->buildTemplate($object); + + $payload = array( + 'objectPHID' => $object_phid, + 'cardHTML' => $template, + 'columnMaps' => $natural, + 'orderMaps' => $order_maps, + 'propertyMaps' => array( + $object_phid => $object->getWorkboardProperties(), + ), + ); + + return id(new AphrontAjaxResponse()) + ->setContent($payload); + } + + private function buildTemplate($object) { + $viewer = $this->getViewer(); + $object_phid = $this->getObjectPHID(); + + $excluded_phids = $this->loadExcludedProjectPHIDs(); + $rendering_engine = id(new PhabricatorBoardRenderingEngine()) ->setViewer($viewer) ->setObjects(array($object)) diff --git a/webroot/rsrc/js/application/projects/WorkboardBoard.js b/webroot/rsrc/js/application/projects/WorkboardBoard.js index 343086d569..4506041cca 100644 --- a/webroot/rsrc/js/application/projects/WorkboardBoard.js +++ b/webroot/rsrc/js/application/projects/WorkboardBoard.js @@ -19,11 +19,13 @@ JX.install('WorkboardBoard', { this._templates = {}; this._orderMaps = {}; + this._propertiesMap = {}; this._buildColumns(); }, properties: { order: null, + pointsEnabled: false }, members: { @@ -33,6 +35,7 @@ JX.install('WorkboardBoard', { _columns: null, _templates: null, _orderMaps: null, + _propertiesMap: null, getRoot: function() { return this._root; @@ -55,6 +58,15 @@ JX.install('WorkboardBoard', { return this; }, + setObjectProperties: function(phid, properties) { + this._propertiesMap[phid] = properties; + return this; + }, + + getObjectProperties: function(phid) { + return this._propertiesMap[phid]; + }, + getCardTemplate: function(phid) { return this._templates[phid]; }, @@ -174,12 +186,18 @@ JX.install('WorkboardBoard', { var card = src_column.removeCard(response.objectPHID); dst_column.addCard(card, after_phid); + src_column.markForRedraw(); + dst_column.markForRedraw(); + this.updateCard(response); list.unlock(); }, - updateCard: function(response) { + updateCard: function(response, options) { + options = options || {}; + options.dirtyColumns = options.dirtyColumns || {}; + var columns = this.getColumns(); var phid = response.objectPHID; @@ -202,8 +220,15 @@ JX.install('WorkboardBoard', { this.getColumn(natural_phid).setNaturalOrder(column_maps[natural_phid]); } + var property_maps = response.propertyMaps; + for (var property_phid in property_maps) { + this.setObjectProperties(property_phid, property_maps[property_phid]); + } + for (var column_phid in columns) { - var cards = columns[column_phid].getCards(); + var column = columns[column_phid]; + + var cards = column.getCards(); for (var object_phid in cards) { if (object_phid !== phid) { continue; @@ -211,8 +236,20 @@ JX.install('WorkboardBoard', { var card = cards[object_phid]; card.redraw(); + + column.markForRedraw(); + } + } + + this._redrawColumns(); + }, + + _redrawColumns: function() { + var columns = this.getColumns(); + for (var k in columns) { + if (columns[k].isMarkedForRedraw()) { + columns[k].redraw(); } - columns[column_phid].redraw(); } } diff --git a/webroot/rsrc/js/application/projects/WorkboardCard.js b/webroot/rsrc/js/application/projects/WorkboardCard.js index 69294219c0..b506e655c1 100644 --- a/webroot/rsrc/js/application/projects/WorkboardCard.js +++ b/webroot/rsrc/js/application/projects/WorkboardCard.js @@ -28,6 +28,18 @@ JX.install('WorkboardCard', { this._column = column; }, + getProperties: function() { + return this.getColumn().getBoard().getObjectProperties(this.getPHID()); + }, + + getPoints: function() { + return this.getProperties().points; + }, + + getStatus: function() { + return this.getProperties().status; + }, + getNode: function() { if (!this._root) { var phid = this.getPHID(); diff --git a/webroot/rsrc/js/application/projects/WorkboardColumn.js b/webroot/rsrc/js/application/projects/WorkboardColumn.js index 77f9ca3146..738cf151c2 100644 --- a/webroot/rsrc/js/application/projects/WorkboardColumn.js +++ b/webroot/rsrc/js/application/projects/WorkboardColumn.js @@ -12,6 +12,14 @@ JX.install('WorkboardColumn', { this._phid = phid; this._root = root; + this._panel = JX.DOM.findAbove(root, 'div', 'workpanel'); + this._pointsNode = JX.DOM.find(this._panel, 'span', 'column-points'); + + this._pointsContentNode = JX.DOM.find( + this._panel, + 'span', + 'column-points-content'); + this._cards = {}; this._naturalOrder = []; }, @@ -22,6 +30,10 @@ JX.install('WorkboardColumn', { _board: null, _cards: null, _naturalOrder: null, + _panel: null, + _pointsNode: null, + _pointsContentNode: null, + _dirty: true, getPHID: function() { return this._phid; @@ -48,6 +60,18 @@ JX.install('WorkboardColumn', { return this; }, + getPointsNode: function() { + return this._pointsNode; + }, + + getPointsContentNode: function() { + return this._pointsContentNode; + }, + + getWorkpanelNode: function() { + return this._panel; + }, + newCard: function(phid) { var card = new JX.WorkboardCard(this, phid); @@ -112,8 +136,21 @@ JX.install('WorkboardColumn', { return JX.keys(this.getCards()); }, + getPointLimit: function() { + return JX.Stratcom.getData(this.getRoot()).pointLimit; + }, + + markForRedraw: function() { + this._dirty = true; + }, + + isMarkedForRedraw: function() { + return this._dirty; + }, + redraw: function() { - var order = this.getBoard().getOrder(); + var board = this.getBoard(); + var order = board.getOrder(); var list; if (order == 'natural') { @@ -124,11 +161,18 @@ JX.install('WorkboardColumn', { var content = []; for (var ii = 0; ii < list.length; ii++) { - var node = list[ii].getNode(); + var card = list[ii]; + + var node = card.getNode(); content.push(node); + } JX.DOM.setContent(this.getRoot(), content); + + this._redrawFrame(); + + this._dirty = false; }, _getCardsSortedNaturally: function() { @@ -170,6 +214,69 @@ JX.install('WorkboardColumn', { } return 0; + }, + + _redrawFrame: function() { + var cards = this.getCards(); + var board = this.getBoard(); + + var points = {}; + for (var phid in cards) { + var card = cards[phid]; + + var card_points; + if (board.getPointsEnabled()) { + card_points = card.getPoints(); + } else { + card_points = 1; + } + + if (card_points !== null) { + var status = card.getStatus(); + if (!points[status]) { + points[status] = 0; + } + points[status] += card_points; + } + } + + var total_points = 0; + for (var k in points) { + total_points += points[k]; + } + + var limit = this.getPointLimit(); + + var display_value; + if (limit !== null && limit !== 0) { + display_value = total_points + ' / ' + limit; + } else { + display_value = total_points; + } + + var over_limit = ((limit !== null) && (total_points > limit)); + + var content_node = this.getPointsContentNode(); + var points_node = this.getPointsNode(); + + JX.DOM.setContent(content_node, display_value); + + var is_empty = !this.getCardPHIDs().length; + var panel = JX.DOM.findAbove(this.getRoot(), 'div', 'workpanel'); + JX.DOM.alterClass(panel, 'project-panel-empty', is_empty); + JX.DOM.alterClass(panel, 'project-panel-over-limit', over_limit); + + var color_map = { + 'phui-tag-shade-disabled': (total_points === 0), + 'phui-tag-shade-blue': (total_points > 0 && !over_limit), + 'phui-tag-shade-red': (over_limit) + }; + + for (var c in color_map) { + JX.DOM.alterClass(points_node, c, !!color_map[c]); + } + + JX.DOM.show(points_node); } } diff --git a/webroot/rsrc/js/application/projects/behavior-project-boards.js b/webroot/rsrc/js/application/projects/behavior-project-boards.js index 5267a09fc2..8fccb1dc91 100644 --- a/webroot/rsrc/js/application/projects/behavior-project-boards.js +++ b/webroot/rsrc/js/application/projects/behavior-project-boards.js @@ -11,54 +11,6 @@ JX.behavior('project-boards', function(config, statics) { - - function onupdate(col) { - var data = JX.Stratcom.getData(col); - var cards = finditems(col); - - // Update the count of tasks in the column header. - if (!data.countTagNode) { - data.countTagNode = JX.$(data.countTagID); - JX.DOM.show(data.countTagNode); - } - - var sum = 0; - for (var ii = 0; ii < cards.length; ii++) { - // TODO: Allow this to be computed in some more clever way. - sum += 1; - } - - // TODO: This is a little bit hacky, but we don't have a PHUIX version of - // this element yet. - - var over_limit = (data.pointLimit && (sum > data.pointLimit)); - - var display_value = sum; - if (data.pointLimit) { - display_value = sum + ' / ' + data.pointLimit; - } - JX.DOM.setContent(JX.$(data.countTagContentID), display_value); - - - var panel_map = { - 'project-panel-empty': !cards.length, - 'project-panel-over-limit': over_limit - }; - var panel = JX.DOM.findAbove(col, 'div', 'workpanel'); - for (var p in panel_map) { - JX.DOM.alterClass(panel, p, !!panel_map[p]); - } - - var color_map = { - 'phui-tag-shade-disabled': (sum === 0), - 'phui-tag-shade-blue': (sum > 0 && !over_limit), - 'phui-tag-shade-red': (over_limit) - }; - for (var c in color_map) { - JX.DOM.alterClass(data.countTagNode, c, !!color_map[c]); - } - } - function update_statics(update_config) { statics.boardID = update_config.boardID; statics.projectPHID = update_config.projectPHID; @@ -135,7 +87,8 @@ JX.behavior('project-boards', function(config, statics) { var board_node = JX.$(config.boardID); var board = statics.workboard.newBoard(board_phid, board_node) - .setOrder(config.order); + .setOrder(config.order) + .setPointsEnabled(config.pointsEnabled); var templates = config.templateMap; for (var k in templates) { @@ -156,6 +109,11 @@ JX.behavior('project-boards', function(config, statics) { board.setOrderMap(object_phid, order_maps[object_phid]); } + var property_maps = config.propertyMaps; + for (var property_phid in property_maps) { + board.setObjectProperties(property_phid, property_maps[property_phid]); + } + board.start(); }); From 0b5abf7bb51af5c3da6a760e5a7c10dedd313340 Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 10 Feb 2016 15:06:20 -0800 Subject: [PATCH 40/65] Allow "0" to be a valid workboard column point limit Summary: Fixes T6580. Now: - Empty field means "unlimited". - Zero means 0. - Nonzero means that number. (Although you can now have fractional points, I didn't change columns to allow fractional limits, because too bad.) Test Plan: {F1103688} Reviewers: chad Reviewed By: chad Maniphest Tasks: T6580 Differential Revision: https://secure.phabricator.com/D15239 --- resources/celerity/map.php | 66 +++++++++---------- ...abricatorProjectColumnDetailController.php | 9 ++- ...bricatorProjectColumnTransactionEditor.php | 10 ++- 3 files changed, 46 insertions(+), 39 deletions(-) diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 6aeac0d365..f734f7c465 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -415,11 +415,11 @@ return array( 'rsrc/js/application/phortune/phortune-credit-card-form.js' => '2290aeef', 'rsrc/js/application/policy/behavior-policy-control.js' => 'd0c516d5', 'rsrc/js/application/policy/behavior-policy-rule-editor.js' => '5e9f347c', - 'rsrc/js/application/projects/WorkboardBoard.js' => '069d6dd3', - 'rsrc/js/application/projects/WorkboardCard.js' => '2fcefa17', - 'rsrc/js/application/projects/WorkboardColumn.js' => 'e8f303bb', + 'rsrc/js/application/projects/WorkboardBoard.js' => '52291776', + 'rsrc/js/application/projects/WorkboardCard.js' => 'c587b80f', + 'rsrc/js/application/projects/WorkboardColumn.js' => 'f05d6e5d', 'rsrc/js/application/projects/WorkboardController.js' => 'fa1378c3', - 'rsrc/js/application/projects/behavior-project-boards.js' => 'e1b56d72', + 'rsrc/js/application/projects/behavior-project-boards.js' => '14a1faae', 'rsrc/js/application/projects/behavior-project-create.js' => '065227cc', 'rsrc/js/application/projects/behavior-reorder-columns.js' => 'e1d25dfb', 'rsrc/js/application/releeph/releeph-preview-branch.js' => 'b2b4fbaf', @@ -659,7 +659,7 @@ return array( 'javelin-behavior-phui-profile-menu' => '12884df9', 'javelin-behavior-policy-control' => 'd0c516d5', 'javelin-behavior-policy-rule-editor' => '5e9f347c', - 'javelin-behavior-project-boards' => 'e1b56d72', + 'javelin-behavior-project-boards' => '14a1faae', 'javelin-behavior-project-create' => '065227cc', 'javelin-behavior-quicksand-blacklist' => '7927a7d3', 'javelin-behavior-recurring-edit' => '5f1c4d5f', @@ -726,9 +726,9 @@ return array( 'javelin-view-renderer' => '6c2b09a2', 'javelin-view-visitor' => 'efe49472', 'javelin-websocket' => 'e292eaf4', - 'javelin-workboard-board' => '069d6dd3', - 'javelin-workboard-card' => '2fcefa17', - 'javelin-workboard-column' => 'e8f303bb', + 'javelin-workboard-board' => '52291776', + 'javelin-workboard-card' => 'c587b80f', + 'javelin-workboard-column' => 'f05d6e5d', 'javelin-workboard-controller' => 'fa1378c3', 'javelin-workflow' => '5b2e3e2b', 'lightbox-attachment-css' => '7acac05d', @@ -919,15 +919,6 @@ return array( 'javelin-stratcom', 'javelin-workflow', ), - '069d6dd3' => array( - 'javelin-install', - 'javelin-dom', - 'javelin-util', - 'javelin-stratcom', - 'javelin-workflow', - 'phabricator-draggable-list', - 'javelin-workboard-column', - ), '06c32383' => array( 'javelin-behavior', 'javelin-typeahead-ondemand-source', @@ -975,6 +966,15 @@ return array( 'javelin-dom', 'javelin-history', ), + '14a1faae' => array( + 'javelin-behavior', + 'javelin-dom', + 'javelin-util', + 'javelin-vector', + 'javelin-stratcom', + 'javelin-workflow', + 'javelin-workboard-controller', + ), '1ad0a787' => array( 'javelin-install', 'javelin-reactor', @@ -1082,9 +1082,6 @@ return array( '2ee659ce' => array( 'javelin-install', ), - '2fcefa17' => array( - 'javelin-install', - ), '327a00d1' => array( 'javelin-behavior', 'javelin-stratcom', @@ -1217,6 +1214,15 @@ return array( 'javelin-dom', 'javelin-reactor-dom', ), + 52291776 => array( + 'javelin-install', + 'javelin-dom', + 'javelin-util', + 'javelin-stratcom', + 'javelin-workflow', + 'phabricator-draggable-list', + 'javelin-workboard-column', + ), '5359e785' => array( 'javelin-install', 'javelin-util', @@ -1802,6 +1808,9 @@ return array( 'javelin-dom', 'javelin-vector', ), + 'c587b80f' => array( + 'javelin-install', + ), 'c72aa091' => array( 'javelin-behavior', 'javelin-dom', @@ -1934,15 +1943,6 @@ return array( 'javelin-dom', 'phabricator-prefab', ), - 'e1b56d72' => array( - 'javelin-behavior', - 'javelin-dom', - 'javelin-util', - 'javelin-vector', - 'javelin-stratcom', - 'javelin-workflow', - 'javelin-workboard-controller', - ), 'e1d25dfb' => array( 'javelin-behavior', 'javelin-stratcom', @@ -2010,10 +2010,6 @@ return array( 'e6e25838' => array( 'javelin-install', ), - 'e8f303bb' => array( - 'javelin-install', - 'javelin-workboard-card', - ), 'e9581f08' => array( 'javelin-behavior', 'javelin-stratcom', @@ -2049,6 +2045,10 @@ return array( 'javelin-workflow', 'javelin-json', ), + 'f05d6e5d' => array( + 'javelin-install', + 'javelin-workboard-card', + ), 'f411b6ae' => array( 'javelin-behavior', 'javelin-stratcom', diff --git a/src/applications/project/controller/PhabricatorProjectColumnDetailController.php b/src/applications/project/controller/PhabricatorProjectColumnDetailController.php index e008c832d9..b0cd4ac0b4 100644 --- a/src/applications/project/controller/PhabricatorProjectColumnDetailController.php +++ b/src/applications/project/controller/PhabricatorProjectColumnDetailController.php @@ -120,9 +120,12 @@ final class PhabricatorProjectColumnDetailController ->setActionList($actions); $limit = $column->getPointLimit(); - $properties->addProperty( - pht('Point Limit'), - $limit ? $limit : pht('No Limit')); + if ($limit === null) { + $limit_text = pht('No Limit'); + } else { + $limit_text = $limit; + } + $properties->addProperty(pht('Point Limit'), $limit_text); return $properties; } diff --git a/src/applications/project/editor/PhabricatorProjectColumnTransactionEditor.php b/src/applications/project/editor/PhabricatorProjectColumnTransactionEditor.php index 08b0355ce5..19f059b8b5 100644 --- a/src/applications/project/editor/PhabricatorProjectColumnTransactionEditor.php +++ b/src/applications/project/editor/PhabricatorProjectColumnTransactionEditor.php @@ -47,10 +47,12 @@ final class PhabricatorProjectColumnTransactionEditor case PhabricatorProjectColumnTransaction::TYPE_STATUS: return $xaction->getNewValue(); case PhabricatorProjectColumnTransaction::TYPE_LIMIT: - if ($xaction->getNewValue()) { + $value = $xaction->getNewValue(); + if (strlen($value)) { return (int)$xaction->getNewValue(); + } else { + return null; } - return null; } return parent::getCustomTransactionNewValue($object, $xaction); @@ -104,7 +106,9 @@ final class PhabricatorProjectColumnTransactionEditor $errors[] = new PhabricatorApplicationTransactionValidationError( $type, pht('Invalid'), - pht('Column point limit must be empty, or a positive integer.'), + pht( + 'Column point limit must either be empty or a nonnegative '. + 'integer.'), $xaction); } } From 968ac764532d2a863d46c655b5476153fa60b64e Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 10 Feb 2016 15:39:08 -0800 Subject: [PATCH 41/65] Don't adjust task priority after a workboard drag unless we need to Summary: Fixes T8197. Currently, if you priority-sort a workboard and drag a card to the top or bottom, we change the priority even if we do not need to. For example, if the lowest priority in a column is "Low", and you drag a "Wishlist" task underneath it, we incorrectly increase the priority of the task to "Low", when we do not actually need to touch it. This is bad/confusing. A similar thing happens when dragging a "High" priority task to the top of a column where the highest priority is currently "Normal". Test Plan: - Create a column with a "Normal" task. - Sort workboard by Priority. - Drag a "High" task above it. After patch: task still "High". - Drag a "Wishlist" task below it. After patch: task still "Wishlist". Also dragged a ton of tasks into the middle of other tasks. Reviewers: chad Reviewed By: chad Maniphest Tasks: T8197 Differential Revision: https://secure.phabricator.com/D15240 --- .../maniphest/storage/ManiphestTask.php | 33 ++++ .../PhabricatorProjectMoveController.php | 149 ++++++++++++------ 2 files changed, 133 insertions(+), 49 deletions(-) diff --git a/src/applications/maniphest/storage/ManiphestTask.php b/src/applications/maniphest/storage/ManiphestTask.php index 93a5f5562c..e90f0e806c 100644 --- a/src/applications/maniphest/storage/ManiphestTask.php +++ b/src/applications/maniphest/storage/ManiphestTask.php @@ -221,6 +221,39 @@ final class ManiphestTask extends ManiphestDAO ); } + private function comparePriorityTo(ManiphestTask $other) { + $upri = $this->getPriority(); + $vpri = $other->getPriority(); + + if ($upri != $vpri) { + return ($upri - $vpri); + } + + $usub = $this->getSubpriority(); + $vsub = $other->getSubpriority(); + + if ($usub != $vsub) { + return ($usub - $vsub); + } + + $uid = $this->getID(); + $vid = $other->getID(); + + if ($uid != $vid) { + return ($uid - $vid); + } + + return 0; + } + + public function isLowerPriorityThan(ManiphestTask $other) { + return ($this->comparePriorityTo($other) < 0); + } + + public function isHigherPriorityThan(ManiphestTask $other) { + return ($this->comparePriorityTo($other) > 0); + } + public function getWorkboardProperties() { return array( 'status' => $this->getStatus(), diff --git a/src/applications/project/controller/PhabricatorProjectMoveController.php b/src/applications/project/controller/PhabricatorProjectMoveController.php index 4aa9e0eec2..d3540a1781 100644 --- a/src/applications/project/controller/PhabricatorProjectMoveController.php +++ b/src/applications/project/controller/PhabricatorProjectMoveController.php @@ -90,55 +90,13 @@ final class PhabricatorProjectMoveController 'projectPHID' => $column->getProjectPHID(), )); - $task_phids = array(); - if ($after_phid) { - $task_phids[] = $after_phid; - } - if ($before_phid) { - $task_phids[] = $before_phid; - } - - if ($task_phids && ($order == PhabricatorProjectColumn::ORDER_PRIORITY)) { - $tasks = id(new ManiphestTaskQuery()) - ->setViewer($viewer) - ->withPHIDs($task_phids) - ->needProjectPHIDs(true) - ->requireCapabilities( - array( - PhabricatorPolicyCapability::CAN_VIEW, - PhabricatorPolicyCapability::CAN_EDIT, - )) - ->execute(); - if (count($tasks) != count($task_phids)) { - return new Aphront404Response(); - } - $tasks = mpull($tasks, null, 'getPHID'); - - $try = array( - array($after_phid, true), - array($before_phid, false), - ); - - $pri = null; - $sub = null; - foreach ($try as $spec) { - list($task_phid, $is_after) = $spec; - $task = idx($tasks, $task_phid); - if ($task) { - list($pri, $sub) = ManiphestTransactionEditor::getAdjacentSubpriority( - $task, - $is_after); - break; - } - } - - if ($pri !== null) { - $xactions[] = id(new ManiphestTransaction()) - ->setTransactionType(ManiphestTransaction::TYPE_PRIORITY) - ->setNewValue($pri); - $xactions[] = id(new ManiphestTransaction()) - ->setTransactionType(ManiphestTransaction::TYPE_SUBPRIORITY) - ->setNewValue($sub); + if ($order == PhabricatorProjectColumn::ORDER_PRIORITY) { + $priority_xactions = $this->getPriorityTransactions( + $object, + $after_phid, + $before_phid); + foreach ($priority_xactions as $xaction) { + $xactions[] = $xaction; } } @@ -179,4 +137,97 @@ final class PhabricatorProjectMoveController return $this->newCardResponse($board_phid, $object_phid); } + private function getPriorityTransactions( + ManiphestTask $task, + $after_phid, + $before_phid) { + + list($after_task, $before_task) = $this->loadPriorityTasks( + $after_phid, + $before_phid); + + $must_move = false; + if ($after_task && !$task->isLowerPriorityThan($after_task)) { + $must_move = true; + } + + if ($before_task && !$task->isHigherPriorityThan($before_task)) { + $must_move = true; + } + + // The move doesn't require a priority change to be valid, so don't + // change the priority since we are not being forced to. + if (!$must_move) { + return array(); + } + + $try = array( + array($after_task, true), + array($before_task, false), + ); + + $pri = null; + $sub = null; + foreach ($try as $spec) { + list($task, $is_after) = $spec; + + if (!$task) { + continue; + } + + list($pri, $sub) = ManiphestTransactionEditor::getAdjacentSubpriority( + $task, + $is_after); + } + + $xactions = array(); + if ($pri !== null) { + $xactions[] = id(new ManiphestTransaction()) + ->setTransactionType(ManiphestTransaction::TYPE_PRIORITY) + ->setNewValue($pri); + $xactions[] = id(new ManiphestTransaction()) + ->setTransactionType(ManiphestTransaction::TYPE_SUBPRIORITY) + ->setNewValue($sub); + } + + return $xactions; + } + + private function loadPriorityTasks($after_phid, $before_phid) { + $viewer = $this->getViewer(); + + $task_phids = array(); + + if ($after_phid) { + $task_phids[] = $after_phid; + } + if ($before_phid) { + $task_phids[] = $before_phid; + } + + if (!$task_phids) { + return array(null, null); + } + + $tasks = id(new ManiphestTaskQuery()) + ->setViewer($viewer) + ->withPHIDs($task_phids) + ->execute(); + $tasks = mpull($tasks, null, 'getPHID'); + + if ($after_phid) { + $after_task = idx($tasks, $after_phid); + } else { + $after_task = null; + } + + if ($before_phid) { + $before_task = idx($tasks, $before_phid); + } else { + $before_task = null; + } + + return array($after_task, $before_task); + } + } From 705c8c956af55bd2e0aff1e7abe68c05e7f170ac Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 11 Feb 2016 09:43:30 -0800 Subject: [PATCH 42/65] Fix one straggler milestone URI Summary: I missed this during cleanup. Test Plan: Go to a project, then: Subprojects, Create Milestone, Cancel. No longer 404/fatals. Reviewers: chad Reviewed By: chad Differential Revision: https://secure.phabricator.com/D15241 --- .../application/PhabricatorProjectApplication.php | 2 -- .../project/engine/PhabricatorProjectEditEngine.php | 12 ++++-------- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/src/applications/project/application/PhabricatorProjectApplication.php b/src/applications/project/application/PhabricatorProjectApplication.php index 0f5f90a737..2d00c4c90c 100644 --- a/src/applications/project/application/PhabricatorProjectApplication.php +++ b/src/applications/project/application/PhabricatorProjectApplication.php @@ -65,8 +65,6 @@ final class PhabricatorProjectApplication extends PhabricatorApplication { => $this->getPanelRouting('PhabricatorProjectPanelController'), 'subprojects/(?P[1-9]\d*)/' => 'PhabricatorProjectSubprojectsController', - 'milestones/(?P[1-9]\d*)/' - => 'PhabricatorProjectMilestonesController', 'board/(?P[1-9]\d*)/'. '(?Pfilter/)?'. '(?:query/(?P[^/]+)/)?' diff --git a/src/applications/project/engine/PhabricatorProjectEditEngine.php b/src/applications/project/engine/PhabricatorProjectEditEngine.php index 54144cbbb5..71f91b7bc1 100644 --- a/src/applications/project/engine/PhabricatorProjectEditEngine.php +++ b/src/applications/project/engine/PhabricatorProjectEditEngine.php @@ -88,15 +88,11 @@ final class PhabricatorProjectEditEngine protected function getObjectCreateCancelURI($object) { $parent = $this->getParentProject(); - if ($parent) { - $id = $parent->getID(); - return "/project/subprojects/{$id}/"; - } - $milestone = $this->getMilestoneProject(); - if ($milestone) { - $id = $milestone->getID(); - return "/project/milestones/{$id}/"; + + if ($parent || $milestone) { + $id = nonempty($parent, $milestone)->getID(); + return "/project/subprojects/{$id}/"; } return parent::getObjectCreateCancelURI($object); From ad77b014f10edbbbba70966f58b79ad906077df1 Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 11 Feb 2016 09:51:32 -0800 Subject: [PATCH 43/65] Fix an issue with creating tasks directly into milestone columns Summary: These columns were conflating `projectPHID` (the defualt project to add to the task) with `boardPHID` (the board the column appears on). Separate them to fix the beahvior. Test Plan: Used "Add Task" from dropdown menu of a milestone column on a parent project's workboard. Reviewers: chad Reviewed By: chad Differential Revision: https://secure.phabricator.com/D15242 --- resources/celerity/map.php | 24 +++++++++---------- .../PhabricatorProjectBoardViewController.php | 1 + .../projects/WorkboardController.js | 4 ++-- 3 files changed, 15 insertions(+), 14 deletions(-) diff --git a/resources/celerity/map.php b/resources/celerity/map.php index f734f7c465..be34573f67 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -418,7 +418,7 @@ return array( 'rsrc/js/application/projects/WorkboardBoard.js' => '52291776', 'rsrc/js/application/projects/WorkboardCard.js' => 'c587b80f', 'rsrc/js/application/projects/WorkboardColumn.js' => 'f05d6e5d', - 'rsrc/js/application/projects/WorkboardController.js' => 'fa1378c3', + 'rsrc/js/application/projects/WorkboardController.js' => '55baf5ed', 'rsrc/js/application/projects/behavior-project-boards.js' => '14a1faae', 'rsrc/js/application/projects/behavior-project-create.js' => '065227cc', 'rsrc/js/application/projects/behavior-reorder-columns.js' => 'e1d25dfb', @@ -729,7 +729,7 @@ return array( 'javelin-workboard-board' => '52291776', 'javelin-workboard-card' => 'c587b80f', 'javelin-workboard-column' => 'f05d6e5d', - 'javelin-workboard-controller' => 'fa1378c3', + 'javelin-workboard-controller' => '55baf5ed', 'javelin-workflow' => '5b2e3e2b', 'lightbox-attachment-css' => '7acac05d', 'maniphest-batch-editor' => 'b0f0b6d5', @@ -1253,6 +1253,16 @@ return array( 'javelin-vector', 'javelin-dom', ), + '55baf5ed' => array( + 'javelin-install', + 'javelin-dom', + 'javelin-util', + 'javelin-vector', + 'javelin-stratcom', + 'javelin-workflow', + 'phabricator-drag-and-drop-file-upload', + 'javelin-workboard-board', + ), '56a1ca03' => array( 'javelin-behavior', 'javelin-behavior-device', @@ -2098,16 +2108,6 @@ return array( 'javelin-vector', 'javelin-magical-init', ), - 'fa1378c3' => array( - 'javelin-install', - 'javelin-dom', - 'javelin-util', - 'javelin-vector', - 'javelin-stratcom', - 'javelin-workflow', - 'phabricator-drag-and-drop-file-upload', - 'javelin-workboard-board', - ), 'fb20ac8d' => array( 'javelin-behavior', 'javelin-aphlict', diff --git a/src/applications/project/controller/PhabricatorProjectBoardViewController.php b/src/applications/project/controller/PhabricatorProjectBoardViewController.php index c075072d9f..0d977c2aca 100644 --- a/src/applications/project/controller/PhabricatorProjectBoardViewController.php +++ b/src/applications/project/controller/PhabricatorProjectBoardViewController.php @@ -720,6 +720,7 @@ final class PhabricatorProjectBoardViewController ->setMetadata( array( 'columnPHID' => $column->getPHID(), + 'boardPHID' => $project->getPHID(), 'projectPHID' => $default_phid, )); diff --git a/webroot/rsrc/js/application/projects/WorkboardController.js b/webroot/rsrc/js/application/projects/WorkboardController.js index 5b5f5de181..b284a418dc 100644 --- a/webroot/rsrc/js/application/projects/WorkboardController.js +++ b/webroot/rsrc/js/application/projects/WorkboardController.js @@ -158,14 +158,14 @@ JX.install('WorkboardController', { var column_data = e.getNodeData('column-add-task'); var column_phid = column_data.columnPHID; - var board_phid = column_data.projectPHID; + var board_phid = column_data.boardPHID; var board = this._getBoard(board_phid); var column = board.getColumn(column_phid); var request_data = { responseType: 'card', columnPHID: column.getPHID(), - projects: board.getPHID(), + projects: column_data.projectPHID, visiblePHIDs: column.getCardPHIDs(), order: board.getOrder() }; From 5383ea9d5666870987c2c93b5db6c6f01ff9954e Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 11 Feb 2016 09:59:59 -0800 Subject: [PATCH 44/65] Add points to workboard cards Summary: Fixes T10328. Test Plan: {F1104609} Reviewers: chad Reviewed By: chad Maniphest Tasks: T10328 Differential Revision: https://secure.phabricator.com/D15243 --- .../project/view/ProjectBoardTaskCard.php | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/applications/project/view/ProjectBoardTaskCard.php b/src/applications/project/view/ProjectBoardTaskCard.php index 7e927bdd72..c7eda01189 100644 --- a/src/applications/project/view/ProjectBoardTaskCard.php +++ b/src/applications/project/view/ProjectBoardTaskCard.php @@ -95,6 +95,17 @@ final class ProjectBoardTaskCard extends Phobject { $card->setCoverImage($cover_file->getBestURI()); } + if (ManiphestTaskPoints::getIsEnabled()) { + $points = $task->getPoints(); + if ($points !== null) { + $points_tag = id(new PHUITagView()) + ->setType(PHUITagView::TYPE_SHADE) + ->setShade(PHUITagView::COLOR_BLUE) + ->setName($points); + $card->addAttribute($points_tag); + } + } + if ($task->isClosed()) { $icon = ManiphestTaskStatus::getStatusIcon($task->getStatus()); $icon = id(new PHUIIconView()) From f163d83935d9916e6ff8a73068d4f5e5ff797f17 Mon Sep 17 00:00:00 2001 From: Chad Little Date: Thu, 11 Feb 2016 11:21:34 -0800 Subject: [PATCH 45/65] Don't link commit uri in Crumbs Summary: These are not needed I think? and handy for cut and paste. Fixes T7628 Test Plan: cut and paste easier from commit hash. Reviewers: epriestley Reviewed By: epriestley Subscribers: Korvin Maniphest Tasks: T7628 Differential Revision: https://secure.phabricator.com/D15245 --- .../diffusion/controller/DiffusionController.php | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/applications/diffusion/controller/DiffusionController.php b/src/applications/diffusion/controller/DiffusionController.php index a3c661a26d..00bb0ce4d4 100644 --- a/src/applications/diffusion/controller/DiffusionController.php +++ b/src/applications/diffusion/controller/DiffusionController.php @@ -147,7 +147,7 @@ abstract class DiffusionController extends PhabricatorController { $crumb_list[] = $crumb; $stable_commit = $drequest->getStableCommit(); - $commit_name = $repository->formatCommitName($stable_commit); + $commit_name = $repository->formatCommitName($stable_commit, $local = true); $commit_uri = $repository->getCommitURI($stable_commit); if ($spec['tags']) { @@ -171,8 +171,7 @@ abstract class DiffusionController extends PhabricatorController { if ($spec['commit']) { $crumb = id(new PHUICrumbView()) - ->setName($commit_name) - ->setHref($commit_uri); + ->setName($commit_name); $crumb_list[] = $crumb; return $crumb_list; } From 12f131c06418bfebfb820cea68dc5e35f18b895f Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 11 Feb 2016 10:10:50 -0800 Subject: [PATCH 46/65] Expose task point counts in `maniphest.search` Summary: Ref T4427. Test Plan: {F1104631} Reviewers: chad Reviewed By: chad Maniphest Tasks: T4427 Differential Revision: https://secure.phabricator.com/D15244 --- src/applications/maniphest/storage/ManiphestTask.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/applications/maniphest/storage/ManiphestTask.php b/src/applications/maniphest/storage/ManiphestTask.php index e90f0e806c..0c1a1bc787 100644 --- a/src/applications/maniphest/storage/ManiphestTask.php +++ b/src/applications/maniphest/storage/ManiphestTask.php @@ -462,6 +462,10 @@ final class ManiphestTask extends ManiphestDAO ->setKey('priority') ->setType('map') ->setDescription(pht('Information about task priority.')), + id(new PhabricatorConduitSearchFieldSpecification()) + ->setKey('points') + ->setType('points') + ->setDescription(pht('Point value of the task.')), ); } @@ -488,6 +492,7 @@ final class ManiphestTask extends ManiphestDAO 'ownerPHID' => $this->getOwnerPHID(), 'status' => $status_info, 'priority' => $priority_info, + 'points' => $this->getPoints(), ); } From a39d0344c643a4312ea3e6a9c75c6278ef9c6d14 Mon Sep 17 00:00:00 2001 From: Chad Little Date: Thu, 11 Feb 2016 11:29:07 -0800 Subject: [PATCH 47/65] Add commit id to header in Diffusion Summary: Also adds the commit to the header underneath the title. Ref T7628 Test Plan: Review a few Diffusion pages. Reviewers: epriestley Reviewed By: epriestley Subscribers: Korvin Maniphest Tasks: T7628 Differential Revision: https://secure.phabricator.com/D15246 --- .../diffusion/controller/DiffusionCommitController.php | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/applications/diffusion/controller/DiffusionCommitController.php b/src/applications/diffusion/controller/DiffusionCommitController.php index efa3104a92..9d6b845c91 100644 --- a/src/applications/diffusion/controller/DiffusionCommitController.php +++ b/src/applications/diffusion/controller/DiffusionCommitController.php @@ -92,10 +92,9 @@ final class DiffusionCommitController extends DiffusionController { $engine = PhabricatorMarkupEngine::newDifferentialMarkupEngine(); $engine->setConfig('viewer', $user); - require_celerity_resource('phabricator-remarkup-css'); - $headsup_view = id(new PHUIHeaderView()) - ->setHeader(nonempty($commit->getSummary(), pht('Commit Detail'))); + ->setHeader(nonempty($commit->getSummary(), pht('Commit Detail'))) + ->setSubheader(pht('Commit: %s', $commit->getCommitIdentifier())); $headsup_actions = $this->renderHeadsupActionList($commit, $repository); From 50b8815e442a9ed1f3d02242173c2948da24426b Mon Sep 17 00:00:00 2001 From: Chad Little Date: Thu, 11 Feb 2016 20:54:48 +0000 Subject: [PATCH 48/65] Remove dropshadow from dialog on single pages Summary: Restricts the dropshadow to just the js dialog. Test Plan: Test login page, logout dialog. Reviewers: epriestley Reviewed By: epriestley Subscribers: Korvin Differential Revision: https://secure.phabricator.com/D15248 --- resources/celerity/map.php | 6 +++--- webroot/rsrc/css/aphront/dialog-view.css | 3 +++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/resources/celerity/map.php b/resources/celerity/map.php index be34573f67..78351f0774 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -7,7 +7,7 @@ */ return array( 'names' => array( - 'core.pkg.css' => '69616aad', + 'core.pkg.css' => 'bd041864', 'core.pkg.js' => 'd7daa6d8', 'darkconsole.pkg.js' => 'e7393ebb', 'differential.pkg.css' => '2de124c9', @@ -18,7 +18,7 @@ return array( 'maniphest.pkg.js' => '949a7498', 'rsrc/css/aphront/aphront-bars.css' => '231ac33c', 'rsrc/css/aphront/dark-console.css' => '6378ef3d', - 'rsrc/css/aphront/dialog-view.css' => 'e2f4919b', + 'rsrc/css/aphront/dialog-view.css' => 'b4334e08', 'rsrc/css/aphront/lightbox-attachment.css' => '7acac05d', 'rsrc/css/aphront/list-filter-view.css' => '5d6f0526', 'rsrc/css/aphront/multi-column.css' => 'fd18389d', @@ -518,7 +518,7 @@ return array( 'almanac-css' => 'dbb9b3af', 'aphront-bars' => '231ac33c', 'aphront-dark-console-css' => '6378ef3d', - 'aphront-dialog-view-css' => 'e2f4919b', + 'aphront-dialog-view-css' => 'b4334e08', 'aphront-list-filter-view-css' => '5d6f0526', 'aphront-multi-column-view-css' => 'fd18389d', 'aphront-panel-view-css' => '8427b78d', diff --git a/webroot/rsrc/css/aphront/dialog-view.css b/webroot/rsrc/css/aphront/dialog-view.css index c83ab931e6..8909368beb 100644 --- a/webroot/rsrc/css/aphront/dialog-view.css +++ b/webroot/rsrc/css/aphront/dialog-view.css @@ -8,6 +8,9 @@ border: 1px solid {$lightblueborder}; border-radius: 3px; background-color: #fff; +} + +.jx-client-dialog .aphront-dialog-view { box-shadow: {$dropshadow}; } From f5c8a2fb18ec2956a22003c28c2d7db23b80898c Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 11 Feb 2016 12:54:43 -0800 Subject: [PATCH 49/65] Write "Why does Phabricator need so many databases?" Summary: We will sell you as many new databases as you want, cheap! Just $1 per database! Test Plan: (O).(O) Reviewers: chad Reviewed By: chad Differential Revision: https://secure.phabricator.com/D15249 --- src/docs/contributor/database.diviner | 7 +- src/docs/flavor/so_many_databases.diviner | 131 ++++++++++++++++++++++ 2 files changed, 134 insertions(+), 4 deletions(-) create mode 100644 src/docs/flavor/so_many_databases.diviner diff --git a/src/docs/contributor/database.diviner b/src/docs/contributor/database.diviner index 8ac6bf7798..59a7dc2b2c 100644 --- a/src/docs/contributor/database.diviner +++ b/src/docs/contributor/database.diviner @@ -28,11 +28,10 @@ Databases ========= Each Phabricator application has its own database. The names are prefixed by -`phabricator_` (this is configurable). This design has two advantages: +`phabricator_` (this is configurable). - - Each database is easier to comprehend and to maintain. - - We don't do cross-database joins so each database can live on its own - machine. This gives us flexibility in sharding data later. +Phabricator uses a separate database for each application. To understand why, +see @{article:Why does Phabricator need so many databases?}. Connections =========== diff --git a/src/docs/flavor/so_many_databases.diviner b/src/docs/flavor/so_many_databases.diviner new file mode 100644 index 0000000000..c170661050 --- /dev/null +++ b/src/docs/flavor/so_many_databases.diviner @@ -0,0 +1,131 @@ +@title Why does Phabricator need so many databases? +@group lore + +Phabricator uses about 60 databases (and we may have added more by the time you +read this document). This sometimes comes as a surprise, since you might assume +it would only use one database. + +The approach we use is designed to work at scale for huge installs with many +thousands of users. We care a lot about working well for large installs, and +about scaling up gracefully to meet the needs of growing organizations. We want +small startups to be able to install Phabricator and have it grow with them as +they expand to many thousands of employees. + +A cost of this approach is that it makes Phabricator more difficult to install +on shared hosts which require a lot of work to create or authorize access to +each database. However, Phabricator does a lot of advanced or complex things +which are difficult to configure or manage on shared hosts, and we don't +recommend installing it on a shared host. The install documentation explicitly +discouarges installing on shared hosts. + +Broadly, in cases where we must choose between operating well at scale for +growing organizations and installing easily on shared hosts, we prioritize +operating at scale. + + +Listing Databases +================= + +You can get a full list of the databases Phabricator needs with `bin/storage +databases`. It will look something like this: + +``` +$ /core/lib/phabricator/bin/storage databases +secure_audit +secure_calendar +secure_chatlog +secure_conduit +secure_countdown +secure_daemon +secure_differential +secure_draft +secure_drydock +secure_feed +...... +``` + +Roughly, each application has its own database, and then there are some +databases which support internal systems or shared infrastructure. + + +Operating at Scale +================== + +This storage design is aimed at large installs that may need more than one +physical database server to handle the load the install generates. + +The primary reason we a database per application is to allow large installs to +scale up by spreading database load across more hardware. A large organization +with many thousands of active users may find themselves limited by the capacity +of a single database backend. + +If so, they can launch a second backend, move some applications over to it, and +continue piling on more users. + +This can't continue forever, but provides a substantial amount of headroom for +large installs to spread the workload across more hardware and continue scaling +up. + +To make this possible, we put each application in its own database and use +database boundaries to enforce the logical constraints that the application +must have in order for this to work. For example, we can not perform joins +between separable tables, because they may not be on the same hardware. + +Establishing boundaries with application databases is a simple, straightforward +way to partition storage and make administrative operations like spreading load +realistic. + + +Ease of Development +=================== + +This design is also easier for us to work with, and easier for users who +want to work with the raw database data to understand and interact with. + +We have a large number of tables (more than 400) and we can not reasonably +reduce the number of tables very much (each table generally represents some +meaningful type of object in some application0. It's easier to develop with +tables which are organized into separate application databases, just like it's +easier to work with a large project if you organize source files into +directories. + +If you aren't developing Phabricator and never look at the data in the +database, you probably don't benefit from this organization. However, if you +are a developer or want to extend Phabricator or look under the hood, it's +easier to find what you're looking for and work with the tables and data when +they're organized by application. + + +Databases Have No Cost +====================== + +In almost all cases, creating databases has zero cost, just like organizing +source code into directories has zero cost. + +Even if we didn't derive enormous benefits from this approach at scale, there +is little reason //not// to organize storage like this. + +There are a handful of administrative tasks which are very slightly more +complex to perform on multiple databases, but these are all either automated +with `bin/storage` or easy to build on top of the list of databases emitted by +`bin/storage databases`. + +For example, you can dump all the databases with `bin/storage dump`, and you +can destroy all the databases with `bin/storage destroy`. + +As mentioned above, an exception to this is that if you're installing on a +shared host and need to jump through hoops to individually authorize access to +each database, databases do cost something. + +However, this cost is an artificial cost imposed by the selected environment, +and this is only the first of many issues you'll run into trying to install and +run Phabricator on a shared host. These issues are why we strongly discourage +using shared hosts, and recommend against them in the install guide. + + +Next Steps +========== + +Continue by: + + - learning more about databases in @{article:Database Schema}. From 86611708191c47ee26ceae0817697b2d14c548ec Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 11 Feb 2016 13:47:53 -0800 Subject: [PATCH 50/65] Fix a couple typos in "why so many databases?" document Summary: One missing word, one `0` that should be a `)`, simplify a couple of mega-clauses to improve readability? Test Plan: ((O)) . ((O)) Reviewers: michaeljs1990, chad Reviewed By: chad Differential Revision: https://secure.phabricator.com/D15252 --- src/docs/flavor/so_many_databases.diviner | 31 +++++++++++------------ 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/src/docs/flavor/so_many_databases.diviner b/src/docs/flavor/so_many_databases.diviner index c170661050..8fa41e6c97 100644 --- a/src/docs/flavor/so_many_databases.diviner +++ b/src/docs/flavor/so_many_databases.diviner @@ -54,10 +54,10 @@ Operating at Scale This storage design is aimed at large installs that may need more than one physical database server to handle the load the install generates. -The primary reason we a database per application is to allow large installs to -scale up by spreading database load across more hardware. A large organization -with many thousands of active users may find themselves limited by the capacity -of a single database backend. +The primary reason we use a separate database for each application is to allow +large installs to scale up by spreading database load across more hardware. A +large organization with many thousands of active users may find themselves +limited by the capacity of a single database backend. If so, they can launch a second backend, move some applications over to it, and continue piling on more users. @@ -80,30 +80,29 @@ Ease of Development =================== This design is also easier for us to work with, and easier for users who -want to work with the raw database data to understand and interact with. +want to work with the raw data in the database. We have a large number of tables (more than 400) and we can not reasonably reduce the number of tables very much (each table generally represents some -meaningful type of object in some application0. It's easier to develop with +meaningful type of object in some application). It's easier to develop with tables which are organized into separate application databases, just like it's easier to work with a large project if you organize source files into directories. If you aren't developing Phabricator and never look at the data in the -database, you probably don't benefit from this organization. However, if you +database, you probably won't benefit from this organization. However, if you are a developer or want to extend Phabricator or look under the hood, it's -easier to find what you're looking for and work with the tables and data when -they're organized by application. +easier to find what you're looking for and work with the tables when they're +organized by application. -Databases Have No Cost -====================== +More Databases Cost Nothing +=========================== -In almost all cases, creating databases has zero cost, just like organizing -source code into directories has zero cost. - -Even if we didn't derive enormous benefits from this approach at scale, there -is little reason //not// to organize storage like this. +In almost all cases, creating more databases has zero cost, just like +organizing source code into directories has zero cost. Even if we didn't derive +enormous benefits from this approach at scale, there is little reason //not// +to organize storage like this. There are a handful of administrative tasks which are very slightly more complex to perform on multiple databases, but these are all either automated From 8934dee5437b95e340d12b72993a60153f0e9152 Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 11 Feb 2016 15:17:36 -0800 Subject: [PATCH 51/65] Add "does not match regexp" to Herald Summary: Fixes T10330. - Anywhere we support "matches regexp", also allow "does not match regexp". Although you can sometimes write a clever negative regexp, these rules are better expressed with "does not match " anyway, and sometimes no regexp will work. - Always allow "does not contain" when we support "contains". - Fix some JS issues with certain rules affecting custom fields. Test Plan: - Wrote an "Affected files do not match regexp" rule that required every diff to touch "MANUALCHANGELOG.md". - Tried to diff without the file; rejected. - Tried to diff with the file; accepted. - Wrote a bunch of "contains" and "does not contain" rules against text fields and custom fields, then edited tasks to trigger/observe them. - Swapped the editor into custom text, user, remarkup, etc fields, no more JS errors. {F1105172} Reviewers: chad Reviewed By: chad Maniphest Tasks: T10330 Differential Revision: https://secure.phabricator.com/D15254 --- .../herald/adapter/HeraldAdapter.php | 24 ++++++++++++------- src/applications/herald/field/HeraldField.php | 5 ++++ .../field/PhabricatorCustomField.php | 14 +++++++++++ .../PhabricatorCustomFieldHeraldField.php | 12 ++++++++++ .../PhabricatorStandardCustomFieldBool.php | 4 ++++ .../PhabricatorStandardCustomFieldLink.php | 5 ++++ .../PhabricatorStandardCustomFieldPHIDs.php | 4 ++++ ...PhabricatorStandardCustomFieldRemarkup.php | 5 ++++ .../PhabricatorStandardCustomFieldText.php | 5 ++++ ...habricatorStandardCustomFieldTokenizer.php | 8 +++++++ 10 files changed, 78 insertions(+), 8 deletions(-) diff --git a/src/applications/herald/adapter/HeraldAdapter.php b/src/applications/herald/adapter/HeraldAdapter.php index bd6d4953da..e08118a369 100644 --- a/src/applications/herald/adapter/HeraldAdapter.php +++ b/src/applications/herald/adapter/HeraldAdapter.php @@ -14,6 +14,7 @@ abstract class HeraldAdapter extends Phobject { const CONDITION_IS_ME = 'me'; const CONDITION_IS_NOT_ME = '!me'; const CONDITION_REGEXP = 'regexp'; + const CONDITION_NOT_REGEXP = '!regexp'; const CONDITION_RULE = 'conditions'; const CONDITION_NOT_RULE = '!conditions'; const CONDITION_EXISTS = 'exists'; @@ -322,6 +323,7 @@ abstract class HeraldAdapter extends Phobject { self::CONDITION_IS_ME => pht('is myself'), self::CONDITION_IS_NOT_ME => pht('is not myself'), self::CONDITION_REGEXP => pht('matches regexp'), + self::CONDITION_NOT_REGEXP => pht('does not match regexp'), self::CONDITION_RULE => pht('matches:'), self::CONDITION_NOT_RULE => pht('does not match:'), self::CONDITION_EXISTS => pht('exists'), @@ -364,16 +366,18 @@ abstract class HeraldAdapter extends Phobject { switch ($condition_type) { case self::CONDITION_CONTAINS: - // "Contains" can take an array of strings, as in "Any changed - // filename" for diffs. + case self::CONDITION_NOT_CONTAINS: + // "Contains and "does not contain" can take an array of strings, as in + // "Any changed filename" for diffs. + + $result_if_match = ($condition_type == self::CONDITION_CONTAINS); + foreach ((array)$field_value as $value) { if (stripos($value, $condition_value) !== false) { - return true; + return $result_if_match; } } - return false; - case self::CONDITION_NOT_CONTAINS: - return (stripos($field_value, $condition_value) === false); + return !$result_if_match; case self::CONDITION_IS: return ($field_value == $condition_value); case self::CONDITION_IS_NOT: @@ -427,6 +431,9 @@ abstract class HeraldAdapter extends Phobject { case self::CONDITION_NEVER: return false; case self::CONDITION_REGEXP: + case self::CONDITION_NOT_REGEXP: + $result_if_match = ($condition_type == self::CONDITION_REGEXP); + foreach ((array)$field_value as $value) { // We add the 'S' flag because we use the regexp multiple times. // It shouldn't cause any troubles if the flag is already there @@ -437,10 +444,10 @@ abstract class HeraldAdapter extends Phobject { pht('Regular expression is not valid!')); } if ($result) { - return true; + return $result_if_match; } } - return false; + return !$result_if_match; case self::CONDITION_REGEXP_PAIR: // Match a JSON-encoded pair of regular expressions against a // dictionary. The first regexp must match the dictionary key, and the @@ -509,6 +516,7 @@ abstract class HeraldAdapter extends Phobject { switch ($condition_type) { case self::CONDITION_REGEXP: + case self::CONDITION_NOT_REGEXP: $ok = @preg_match($condition_value, ''); if ($ok === false) { throw new HeraldInvalidConditionException( diff --git a/src/applications/herald/field/HeraldField.php b/src/applications/herald/field/HeraldField.php index 2aba443077..98d6d8ffc4 100644 --- a/src/applications/herald/field/HeraldField.php +++ b/src/applications/herald/field/HeraldField.php @@ -47,6 +47,7 @@ abstract class HeraldField extends Phobject { HeraldAdapter::CONDITION_IS, HeraldAdapter::CONDITION_IS_NOT, HeraldAdapter::CONDITION_REGEXP, + HeraldAdapter::CONDITION_NOT_REGEXP, ); case self::STANDARD_PHID: return array( @@ -76,12 +77,16 @@ abstract class HeraldField extends Phobject { case self::STANDARD_TEXT_LIST: return array( HeraldAdapter::CONDITION_CONTAINS, + HeraldAdapter::CONDITION_NOT_CONTAINS, HeraldAdapter::CONDITION_REGEXP, + HeraldAdapter::CONDITION_NOT_REGEXP, ); case self::STANDARD_TEXT_MAP: return array( HeraldAdapter::CONDITION_CONTAINS, + HeraldAdapter::CONDITION_NOT_CONTAINS, HeraldAdapter::CONDITION_REGEXP, + HeraldAdapter::CONDITION_NOT_REGEXP, HeraldAdapter::CONDITION_REGEXP_PAIR, ); } diff --git a/src/infrastructure/customfield/field/PhabricatorCustomField.php b/src/infrastructure/customfield/field/PhabricatorCustomField.php index a89d8005f0..cb561fca58 100644 --- a/src/infrastructure/customfield/field/PhabricatorCustomField.php +++ b/src/infrastructure/customfield/field/PhabricatorCustomField.php @@ -1446,5 +1446,19 @@ abstract class PhabricatorCustomField extends Phobject { return null; } + public function getHeraldFieldStandardType() { + if ($this->proxy) { + return $this->proxy->getHeraldFieldStandardType(); + } + return null; + } + + public function getHeraldDatasource() { + if ($this->proxy) { + return $this->proxy->getHeraldDatasource(); + } + return null; + } + } diff --git a/src/infrastructure/customfield/herald/PhabricatorCustomFieldHeraldField.php b/src/infrastructure/customfield/herald/PhabricatorCustomFieldHeraldField.php index 30e9fc4c12..b9e5724ef6 100644 --- a/src/infrastructure/customfield/herald/PhabricatorCustomFieldHeraldField.php +++ b/src/infrastructure/customfield/herald/PhabricatorCustomFieldHeraldField.php @@ -65,8 +65,20 @@ final class PhabricatorCustomFieldHeraldField extends HeraldField { return $this->getCustomField()->getHeraldFieldConditions(); } + protected function getHeraldFieldStandardType() { + return $this->getCustomField()->getHeraldFieldStandardType(); + } + public function getHeraldFieldValueType($condition) { + if ($this->getHeraldFieldStandardType()) { + return parent::getHeraldFieldValueType($condition); + } + return $this->getCustomField()->getHeraldFieldValueType($condition); } + protected function getDatasource() { + return $this->getCustomField()->getHeraldDatasource(); + } + } diff --git a/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldBool.php b/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldBool.php index b1fc7f7a9b..f1d1371a7d 100644 --- a/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldBool.php +++ b/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldBool.php @@ -129,6 +129,10 @@ final class PhabricatorStandardCustomFieldBool ); } + public function getHeraldFieldStandardType() { + return HeraldField::STANDARD_BOOL; + } + protected function getHTTPParameterType() { return new AphrontBoolHTTPParameterType(); } diff --git a/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldLink.php b/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldLink.php index 904a457616..66f3605b9c 100644 --- a/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldLink.php +++ b/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldLink.php @@ -77,9 +77,14 @@ final class PhabricatorStandardCustomFieldLink HeraldAdapter::CONDITION_IS, HeraldAdapter::CONDITION_IS_NOT, HeraldAdapter::CONDITION_REGEXP, + HeraldAdapter::CONDITION_NOT_REGEXP, ); } + public function getHeraldFieldStandardType() { + return HeraldField::STANDARD_TEXT; + } + protected function getHTTPParameterType() { return new AphrontStringHTTPParameterType(); } diff --git a/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldPHIDs.php b/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldPHIDs.php index b9b1bf6505..78c8caa5a9 100644 --- a/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldPHIDs.php +++ b/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldPHIDs.php @@ -241,6 +241,10 @@ abstract class PhabricatorStandardCustomFieldPHIDs ); } + public function getHeraldFieldStandardType() { + return HeraldField::STANDARD_PHID_NULLABLE; + } + public function getHeraldFieldValue() { // If the field has a `null` value, make sure we hand an `array()` to // Herald. diff --git a/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldRemarkup.php b/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldRemarkup.php index 14e4eede5a..be7db42004 100644 --- a/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldRemarkup.php +++ b/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldRemarkup.php @@ -92,9 +92,14 @@ final class PhabricatorStandardCustomFieldRemarkup HeraldAdapter::CONDITION_IS, HeraldAdapter::CONDITION_IS_NOT, HeraldAdapter::CONDITION_REGEXP, + HeraldAdapter::CONDITION_NOT_REGEXP, ); } + public function getHeraldFieldStandardType() { + return HeraldField::STANDARD_TEXT; + } + protected function getHTTPParameterType() { return new AphrontStringHTTPParameterType(); } diff --git a/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldText.php b/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldText.php index 716e1dfc99..8a4ae97bcf 100644 --- a/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldText.php +++ b/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldText.php @@ -60,9 +60,14 @@ final class PhabricatorStandardCustomFieldText HeraldAdapter::CONDITION_IS, HeraldAdapter::CONDITION_IS_NOT, HeraldAdapter::CONDITION_REGEXP, + HeraldAdapter::CONDITION_NOT_REGEXP, ); } + public function getHeraldFieldStandardType() { + return HeraldField::STANDARD_TEXT; + } + protected function getHTTPParameterType() { return new AphrontStringHTTPParameterType(); } diff --git a/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldTokenizer.php b/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldTokenizer.php index f6a542ec7f..f9a69c2838 100644 --- a/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldTokenizer.php +++ b/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldTokenizer.php @@ -44,4 +44,12 @@ abstract class PhabricatorStandardCustomFieldTokenizer ->setDatasource($this->getDatasource()); } + public function getHeraldFieldStandardType() { + return HeraldField::STANDARD_PHID_LIST; + } + + public function getHeraldDatasource() { + return $this->getDatasource(); + } + } From 6ae0a62f9f4822164f0f5684c7431411bf4c09b3 Mon Sep 17 00:00:00 2001 From: Chad Little Date: Thu, 11 Feb 2016 14:55:44 -0800 Subject: [PATCH 52/65] New People Hovercards Summary: Mimics the Project Hovercards, more custom UI. Test Plan: Hover over person with and without badges, hover over project. Reviewers: epriestley Reviewed By: epriestley Subscribers: Korvin Differential Revision: https://secure.phabricator.com/D15253 --- resources/celerity/map.php | 4 +- src/__phutil_library_map__.php | 6 +- .../PeopleHovercardEngineExtension.php | 66 +------- .../people/view/PhabricatorUserCardView.php | 151 ++++++++++++++++++ ...icatorProjectHovercardEngineExtension.php} | 2 +- .../application/project/project-card-view.css | 7 +- 6 files changed, 169 insertions(+), 67 deletions(-) create mode 100644 src/applications/people/view/PhabricatorUserCardView.php rename src/applications/project/{events/ProjectHovercardEngineExtension.php => engineextension/PhabricatorProjectHovercardEngineExtension.php} (95%) diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 78351f0774..bcdc04d534 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -93,7 +93,7 @@ return array( 'rsrc/css/application/policy/policy-transaction-detail.css' => '82100a43', 'rsrc/css/application/policy/policy.css' => '957ea14c', 'rsrc/css/application/ponder/ponder-view.css' => '7b0df4da', - 'rsrc/css/application/project/project-card-view.css' => '9c3631e5', + 'rsrc/css/application/project/project-card-view.css' => '9418c97d', 'rsrc/css/application/project/project-view.css' => '4693497c', 'rsrc/css/application/releeph/releeph-core.css' => '9b3c5733', 'rsrc/css/application/releeph/releeph-preview-branch.css' => 'b7a6f4a5', @@ -854,7 +854,7 @@ return array( 'policy-edit-css' => '815c66f7', 'policy-transaction-detail-css' => '82100a43', 'ponder-view-css' => '7b0df4da', - 'project-card-view-css' => '9c3631e5', + 'project-card-view-css' => '9418c97d', 'project-view-css' => '4693497c', 'releeph-core' => '9b3c5733', 'releeph-preview-branch' => 'b7a6f4a5', diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 134e36b2b9..0534da7c3c 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -2904,6 +2904,7 @@ phutil_register_library_map(array( 'PhabricatorProjectHeraldAction' => 'applications/project/herald/PhabricatorProjectHeraldAction.php', 'PhabricatorProjectHeraldAdapter' => 'applications/project/herald/PhabricatorProjectHeraldAdapter.php', 'PhabricatorProjectHeraldFieldGroup' => 'applications/project/herald/PhabricatorProjectHeraldFieldGroup.php', + 'PhabricatorProjectHovercardEngineExtension' => 'applications/project/engineextension/PhabricatorProjectHovercardEngineExtension.php', 'PhabricatorProjectIconSet' => 'applications/project/icon/PhabricatorProjectIconSet.php', 'PhabricatorProjectIconsConfigOptionType' => 'applications/project/config/PhabricatorProjectIconsConfigOptionType.php', 'PhabricatorProjectInterface' => 'applications/project/interface/PhabricatorProjectInterface.php', @@ -3376,6 +3377,7 @@ phutil_register_library_map(array( 'PhabricatorUnsubscribedFromObjectEdgeType' => 'applications/transactions/edges/PhabricatorUnsubscribedFromObjectEdgeType.php', 'PhabricatorUser' => 'applications/people/storage/PhabricatorUser.php', 'PhabricatorUserBlurbField' => 'applications/people/customfield/PhabricatorUserBlurbField.php', + 'PhabricatorUserCardView' => 'applications/people/view/PhabricatorUserCardView.php', 'PhabricatorUserConfigOptions' => 'applications/people/config/PhabricatorUserConfigOptions.php', 'PhabricatorUserConfiguredCustomField' => 'applications/people/customfield/PhabricatorUserConfiguredCustomField.php', 'PhabricatorUserConfiguredCustomFieldStorage' => 'applications/people/storage/PhabricatorUserConfiguredCustomFieldStorage.php', @@ -3828,7 +3830,6 @@ phutil_register_library_map(array( 'ProjectDefaultJoinCapability' => 'applications/project/capability/ProjectDefaultJoinCapability.php', 'ProjectDefaultViewCapability' => 'applications/project/capability/ProjectDefaultViewCapability.php', 'ProjectEditConduitAPIMethod' => 'applications/project/conduit/ProjectEditConduitAPIMethod.php', - 'ProjectHovercardEngineExtension' => 'applications/project/events/ProjectHovercardEngineExtension.php', 'ProjectQueryConduitAPIMethod' => 'applications/project/conduit/ProjectQueryConduitAPIMethod.php', 'ProjectRemarkupRule' => 'applications/project/remarkup/ProjectRemarkupRule.php', 'ProjectRemarkupRuleTestCase' => 'applications/project/remarkup/__tests__/ProjectRemarkupRuleTestCase.php', @@ -7338,6 +7339,7 @@ phutil_register_library_map(array( 'PhabricatorProjectHeraldAction' => 'HeraldAction', 'PhabricatorProjectHeraldAdapter' => 'HeraldAdapter', 'PhabricatorProjectHeraldFieldGroup' => 'HeraldFieldGroup', + 'PhabricatorProjectHovercardEngineExtension' => 'PhabricatorHovercardEngineExtension', 'PhabricatorProjectIconSet' => 'PhabricatorIconSet', 'PhabricatorProjectIconsConfigOptionType' => 'PhabricatorConfigJSONOptionType', 'PhabricatorProjectListController' => 'PhabricatorProjectController', @@ -7887,6 +7889,7 @@ phutil_register_library_map(array( 'PhabricatorFulltextInterface', ), 'PhabricatorUserBlurbField' => 'PhabricatorUserCustomField', + 'PhabricatorUserCardView' => 'AphrontTagView', 'PhabricatorUserConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorUserConfiguredCustomField' => array( 'PhabricatorUserCustomField', @@ -8474,7 +8477,6 @@ phutil_register_library_map(array( 'ProjectDefaultJoinCapability' => 'PhabricatorPolicyCapability', 'ProjectDefaultViewCapability' => 'PhabricatorPolicyCapability', 'ProjectEditConduitAPIMethod' => 'PhabricatorEditEngineAPIMethod', - 'ProjectHovercardEngineExtension' => 'PhabricatorHovercardEngineExtension', 'ProjectQueryConduitAPIMethod' => 'ProjectConduitAPIMethod', 'ProjectRemarkupRule' => 'PhabricatorObjectRemarkupRule', 'ProjectRemarkupRuleTestCase' => 'PhabricatorTestCase', diff --git a/src/applications/people/engineextension/PeopleHovercardEngineExtension.php b/src/applications/people/engineextension/PeopleHovercardEngineExtension.php index e986ec87f7..22d63eda8a 100644 --- a/src/applications/people/engineextension/PeopleHovercardEngineExtension.php +++ b/src/applications/people/engineextension/PeopleHovercardEngineExtension.php @@ -25,6 +25,7 @@ final class PeopleHovercardEngineExtension ->setViewer($viewer) ->withPHIDs($phids) ->needAvailability(true) + ->needProfileImage(true) ->needProfile(true) ->needBadges(true) ->execute(); @@ -47,69 +48,12 @@ final class PeopleHovercardEngineExtension return; } - $hovercard->setTitle($user->getUsername()); + $user_card = id(new PhabricatorUserCardView()) + ->setProfile($user) + ->setViewer($viewer); - $profile = $user->getUserProfile(); - $detail = $user->getRealName(); - if ($profile->getTitle()) { - $detail .= ' - '.$profile->getTitle(); - } - $hovercard->setDetail($detail); + $hovercard->appendChild($user_card); - if ($user->getIsDisabled()) { - $hovercard->addField(pht('Account'), pht('Disabled')); - } else if (!$user->isUserActivated()) { - $hovercard->addField(pht('Account'), pht('Not Activated')); - } else if (PhabricatorApplication::isClassInstalledForViewer( - 'PhabricatorCalendarApplication', - $viewer)) { - $hovercard->addField( - pht('Status'), - $user->getAvailabilityDescription($viewer)); - } - - $hovercard->addField( - pht('User Since'), - phabricator_date($user->getDateCreated(), $viewer)); - - if ($profile->getBlurb()) { - $hovercard->addField(pht('Blurb'), - id(new PhutilUTF8StringTruncator()) - ->setMaximumGlyphs(120) - ->truncateString($profile->getBlurb())); - } - - $badges = $this->buildBadges($user, $viewer); - foreach ($badges as $badge) { - $hovercard->addBadge($badge); - } - } - - private function buildBadges( - PhabricatorUser $user, - $viewer) { - - $class = 'PhabricatorBadgesApplication'; - $items = array(); - - if (PhabricatorApplication::isClassInstalledForViewer($class, $viewer)) { - $badge_phids = $user->getBadgePHIDs(); - if ($badge_phids) { - $badges = id(new PhabricatorBadgesQuery()) - ->setViewer($viewer) - ->withPHIDs($badge_phids) - ->withStatuses(array(PhabricatorBadgesBadge::STATUS_ACTIVE)) - ->execute(); - - foreach ($badges as $badge) { - $items[] = id(new PHUIBadgeMiniView()) - ->setIcon($badge->getIcon()) - ->setHeader($badge->getName()) - ->setQuality($badge->getQuality()); - } - } - } - return $items; } } diff --git a/src/applications/people/view/PhabricatorUserCardView.php b/src/applications/people/view/PhabricatorUserCardView.php new file mode 100644 index 0000000000..60ba08c93b --- /dev/null +++ b/src/applications/people/view/PhabricatorUserCardView.php @@ -0,0 +1,151 @@ +profile = $profile; + return $this; + } + + public function setViewer(PhabricatorUser $viewer) { + $this->viewer = $viewer; + return $this; + } + + public function setTag($tag) { + $this->tag = $tag; + return $this; + } + + protected function getTagName() { + if ($this->tag) { + return $this->tag; + } + return 'div'; + } + + protected function getTagAttributes() { + $classes = array(); + $classes[] = 'project-card-view'; + + if ($this->profile->getIsDisabled()) { + $classes[] = 'project-card-grey'; + } else { + $classes[] = 'project-card-blue'; + } + + return array( + 'class' => implode($classes, ' '), + ); + } + + protected function getTagContent() { + + $user = $this->profile; + $profile = $user->loadUserProfile(); + $picture = $user->getProfileImageURI(); + $viewer = $this->viewer; + + require_celerity_resource('project-card-view-css'); + + $profile_icon = PhabricatorPeopleIconSet::getIconIcon($profile->getIcon()); + $profile_title = $profile->getDisplayTitle(); + + $tag = id(new PHUITagView()) + ->setIcon($profile_icon) + ->setName($profile_title) + ->addClass('project-view-header-tag') + ->setType(PHUITagView::TYPE_SHADE); + + $header = id(new PHUIHeaderView()) + ->setHeader(array($user->getFullName(), $tag)) + ->setUser($viewer) + ->setImage($picture); + + $body = array(); + + $body[] = $this->addItem( + pht('User Since'), + phabricator_date($profile->getDateCreated(), $viewer)); + + if (PhabricatorApplication::isClassInstalledForViewer( + 'PhabricatorCalendarApplication', + $viewer)) { + $availability = $user->getAvailabilityDescription($viewer); + $body[] = $this->addItem(pht('Status'), $availability); + } + + $badges = $this->buildBadges($user, $viewer); + if ($badges) { + $badges = id(new PHUIBadgeBoxView()) + ->addItems($badges) + ->setCollapsed(true); + $body[] = phutil_tag( + 'div', + array( + 'class' => 'phui-hovercard-body-item hovercard-badges', + ), + $badges); + } + + $body = phutil_tag( + 'div', + array( + 'class' => 'project-card-body', + ), + $body); + + $card = phutil_tag( + 'div', + array( + 'class' => 'project-card-inner', + ), + array( + $header, + $body, + )); + + return $card; + } + + private function addItem($label, $value) { + $item = array( + phutil_tag('strong', array(), $label), + ': ', + phutil_tag('span', array(), $value), + ); + return phutil_tag_div('project-card-item', $item); + } + + private function buildBadges( + PhabricatorUser $user, + $viewer) { + + $class = 'PhabricatorBadgesApplication'; + $items = array(); + + if (PhabricatorApplication::isClassInstalledForViewer($class, $viewer)) { + $badge_phids = $user->getBadgePHIDs(); + if ($badge_phids) { + $badges = id(new PhabricatorBadgesQuery()) + ->setViewer($viewer) + ->withPHIDs($badge_phids) + ->withStatuses(array(PhabricatorBadgesBadge::STATUS_ACTIVE)) + ->execute(); + + foreach ($badges as $badge) { + $items[] = id(new PHUIBadgeMiniView()) + ->setIcon($badge->getIcon()) + ->setHeader($badge->getName()) + ->setQuality($badge->getQuality()); + } + } + } + return $items; + } + +} diff --git a/src/applications/project/events/ProjectHovercardEngineExtension.php b/src/applications/project/engineextension/PhabricatorProjectHovercardEngineExtension.php similarity index 95% rename from src/applications/project/events/ProjectHovercardEngineExtension.php rename to src/applications/project/engineextension/PhabricatorProjectHovercardEngineExtension.php index deef9a30f8..d2d1f9ab82 100644 --- a/src/applications/project/events/ProjectHovercardEngineExtension.php +++ b/src/applications/project/engineextension/PhabricatorProjectHovercardEngineExtension.php @@ -1,6 +1,6 @@ Date: Thu, 11 Feb 2016 17:04:14 -0800 Subject: [PATCH 53/65] Minor CSS touches to workboard quest experience Summary: minor spacing updates, but i need to likely take a more details pass, specifically points look janky with project tags since they are not in the same `li`. Test Plan: Zoom into tags, see they all are same height and align. Reviewers: epriestley Reviewed By: epriestley Subscribers: Korvin Differential Revision: https://secure.phabricator.com/D15255 --- resources/celerity/map.php | 10 +++++----- src/applications/project/view/ProjectBoardTaskCard.php | 1 + webroot/rsrc/css/phui/phui-object-item-list-view.css | 2 +- webroot/rsrc/css/phui/phui-segment-bar-view.css | 6 ++++++ 4 files changed, 13 insertions(+), 6 deletions(-) diff --git a/resources/celerity/map.php b/resources/celerity/map.php index bcdc04d534..644ec2fb2f 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -7,7 +7,7 @@ */ return array( 'names' => array( - 'core.pkg.css' => 'bd041864', + 'core.pkg.css' => 'b59766ad', 'core.pkg.js' => 'd7daa6d8', 'darkconsole.pkg.js' => 'e7393ebb', 'differential.pkg.css' => '2de124c9', @@ -143,13 +143,13 @@ return array( 'rsrc/css/phui/phui-info-view.css' => '6d7c3509', 'rsrc/css/phui/phui-list.css' => '9da2aa00', 'rsrc/css/phui/phui-object-box.css' => '407eaf5a', - 'rsrc/css/phui/phui-object-item-list-view.css' => '8f443e8b', + 'rsrc/css/phui/phui-object-item-list-view.css' => 'be31c3a7', 'rsrc/css/phui/phui-pager.css' => 'bea33d23', 'rsrc/css/phui/phui-pinboard-view.css' => '2495140e', 'rsrc/css/phui/phui-profile-menu.css' => 'f709256c', 'rsrc/css/phui/phui-property-list-view.css' => '27b2849e', 'rsrc/css/phui/phui-remarkup-preview.css' => '1a8f2591', - 'rsrc/css/phui/phui-segment-bar-view.css' => '6622f0a1', + 'rsrc/css/phui/phui-segment-bar-view.css' => '46342871', 'rsrc/css/phui/phui-spacing.css' => '042804d6', 'rsrc/css/phui/phui-status.css' => '888cedb8', 'rsrc/css/phui/phui-tag-view.css' => '9d5d4400', @@ -828,13 +828,13 @@ return array( 'phui-inline-comment-view-css' => '0fdb3667', 'phui-list-view-css' => '9da2aa00', 'phui-object-box-css' => '407eaf5a', - 'phui-object-item-list-view-css' => '8f443e8b', + 'phui-object-item-list-view-css' => 'be31c3a7', 'phui-pager-css' => 'bea33d23', 'phui-pinboard-view-css' => '2495140e', 'phui-profile-menu-css' => 'f709256c', 'phui-property-list-view-css' => '27b2849e', 'phui-remarkup-preview-css' => '1a8f2591', - 'phui-segment-bar-view-css' => '6622f0a1', + 'phui-segment-bar-view-css' => '46342871', 'phui-spacing-css' => '042804d6', 'phui-status-list-view-css' => '888cedb8', 'phui-tag-view-css' => '9d5d4400', diff --git a/src/applications/project/view/ProjectBoardTaskCard.php b/src/applications/project/view/ProjectBoardTaskCard.php index c7eda01189..087e9c1789 100644 --- a/src/applications/project/view/ProjectBoardTaskCard.php +++ b/src/applications/project/view/ProjectBoardTaskCard.php @@ -101,6 +101,7 @@ final class ProjectBoardTaskCard extends Phobject { $points_tag = id(new PHUITagView()) ->setType(PHUITagView::TYPE_SHADE) ->setShade(PHUITagView::COLOR_BLUE) + ->setSlimShady(true) ->setName($points); $card->addAttribute($points_tag); } diff --git a/webroot/rsrc/css/phui/phui-object-item-list-view.css b/webroot/rsrc/css/phui/phui-object-item-list-view.css index 81509008c9..913975c6eb 100644 --- a/webroot/rsrc/css/phui/phui-object-item-list-view.css +++ b/webroot/rsrc/css/phui/phui-object-item-list-view.css @@ -307,7 +307,7 @@ ul.phui-object-item-list-view { .phui-object-item-attribute { display: inline-block; color: {$greytext}; - vertical-align: middle; + vertical-align: top; } .phui-object-item-attribute-spacer { diff --git a/webroot/rsrc/css/phui/phui-segment-bar-view.css b/webroot/rsrc/css/phui/phui-segment-bar-view.css index 5d07039c31..92665165ba 100644 --- a/webroot/rsrc/css/phui/phui-segment-bar-view.css +++ b/webroot/rsrc/css/phui/phui-segment-bar-view.css @@ -7,6 +7,12 @@ margin-bottom: 4px; } +.phui-profile-menu-collapsed .phui-segment-bar-label { + width: 74px; + overflow: hidden; + text-overflow: ellipsis; +} + .phui-segment-bar-segments { background: {$lightgreybackground}; border-radius: 4px; From 7e94d2f80819fcc8c6fe7785274774b8fd6237ca Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 11 Feb 2016 17:32:44 -0800 Subject: [PATCH 54/65] Permit users to touch `maniphest.points` Summary: Ref T4427. Seems fine / not egregiously broken. Test Plan: Edited points configuration. Tried to set a bad value. Set a good value. Persued examples and help text. Reviewers: chad Reviewed By: chad Maniphest Tasks: T4427 Differential Revision: https://secure.phabricator.com/D15256 --- src/__phutil_library_map__.php | 2 + .../ManiphestPointsConfigOptionType.php | 10 +++++ .../PhabricatorManiphestConfigOptions.php | 44 +++++++++++++++++-- .../constants/ManiphestTaskPoints.php | 17 +++++++ 4 files changed, 70 insertions(+), 3 deletions(-) create mode 100644 src/applications/maniphest/config/ManiphestPointsConfigOptionType.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 0534da7c3c..b66d31b3c0 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -1300,6 +1300,7 @@ phutil_register_library_map(array( 'ManiphestHovercardEngineExtension' => 'applications/maniphest/engineextension/ManiphestHovercardEngineExtension.php', 'ManiphestInfoConduitAPIMethod' => 'applications/maniphest/conduit/ManiphestInfoConduitAPIMethod.php', 'ManiphestNameIndex' => 'applications/maniphest/storage/ManiphestNameIndex.php', + 'ManiphestPointsConfigOptionType' => 'applications/maniphest/config/ManiphestPointsConfigOptionType.php', 'ManiphestPriorityConfigOptionType' => 'applications/maniphest/config/ManiphestPriorityConfigOptionType.php', 'ManiphestPriorityEmailCommand' => 'applications/maniphest/command/ManiphestPriorityEmailCommand.php', 'ManiphestProjectNameFulltextEngineExtension' => 'applications/maniphest/engineextension/ManiphestProjectNameFulltextEngineExtension.php', @@ -5460,6 +5461,7 @@ phutil_register_library_map(array( 'ManiphestHovercardEngineExtension' => 'PhabricatorHovercardEngineExtension', 'ManiphestInfoConduitAPIMethod' => 'ManiphestConduitAPIMethod', 'ManiphestNameIndex' => 'ManiphestDAO', + 'ManiphestPointsConfigOptionType' => 'PhabricatorConfigJSONOptionType', 'ManiphestPriorityConfigOptionType' => 'PhabricatorConfigJSONOptionType', 'ManiphestPriorityEmailCommand' => 'ManiphestEmailCommand', 'ManiphestProjectNameFulltextEngineExtension' => 'PhabricatorFulltextEngineExtension', diff --git a/src/applications/maniphest/config/ManiphestPointsConfigOptionType.php b/src/applications/maniphest/config/ManiphestPointsConfigOptionType.php new file mode 100644 index 0000000000..f79050b4a1 --- /dev/null +++ b/src/applications/maniphest/config/ManiphestPointsConfigOptionType.php @@ -0,0 +1,10 @@ +encodeFormatted($fields_example); + $points_type = 'custom:ManiphestPointsConfigOptionType'; + + $points_example_1 = array( + 'enabled' => true, + 'label' => pht('Story Points'), + 'action' => pht('Change Story Points'), + ); + $points_json_1 = id(new PhutilJSON())->encodeFormatted($points_example_1); + + $points_example_2 = array( + 'enabled' => true, + 'label' => pht('Estimated Hours'), + 'action' => pht('Change Estimate'), + ); + $points_json_2 = id(new PhutilJSON())->encodeFormatted($points_example_2); + + $points_description = $this->deformat(pht(<<newOption('maniphest.custom-field-definitions', 'wild', array()) ->setSummary(pht('Custom Maniphest fields.')) @@ -336,9 +372,11 @@ EOTEXT '"Needs Triage" panel on the home page. You should adjust this if '. 'you adjust priorities using `%s`.', 'maniphest.priorities')), - $this->newOption('maniphest.points', 'map', array()) - ->setDescription( - pht('PROTOTYPE! Very hot. Burns user. Do not touch!')), + $this->newOption('maniphest.points', $points_type, array()) + ->setSummary(pht('Configure point values for tasks.')) + ->setDescription($points_description) + ->addExample($points_json_1, pht('Points Config')) + ->addExample($points_json_2, pht('Hours Config')), ); } diff --git a/src/applications/maniphest/constants/ManiphestTaskPoints.php b/src/applications/maniphest/constants/ManiphestTaskPoints.php index 8df71f7936..55af72956b 100644 --- a/src/applications/maniphest/constants/ManiphestTaskPoints.php +++ b/src/applications/maniphest/constants/ManiphestTaskPoints.php @@ -21,4 +21,21 @@ final class ManiphestTaskPoints extends Phobject { return PhabricatorEnv::getEnvConfig('maniphest.points'); } + public static function validateConfiguration($config) { + if (!is_array($config)) { + throw new Exception( + pht( + 'Configuration is not valid. Maniphest points configuration must '. + 'be a dictionary.')); + } + + PhutilTypeSpec::checkMap( + $config, + array( + 'enabled' => 'optional bool', + 'label' => 'optional string', + 'action' => 'optional string', + )); + } + } From 99af097ff613512d0065abb9e93c1ce7744e46ac Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 11 Feb 2016 20:30:36 -0800 Subject: [PATCH 55/65] Allow task statuses to have claiming disabled Summary: Fixes T10343. All solutions here seem basically fine. I think adding this small bit of complexity is OK, and sorrrrt of like this behavior sometimes. - Allow disabling this behavior per-status. - Disable it by default for "Invalid" and "Duplicate" (I left "wontfix", since that's a resolution?). Beyond being more flexible, I think this is slightly better? Test Plan: - Closed a task as invalid: no claim. - Closed a task as resolved: claim. Reviewers: chad Reviewed By: chad Maniphest Tasks: T10343 Differential Revision: https://secure.phabricator.com/D15257 --- .../maniphest/config/PhabricatorManiphestConfigOptions.php | 7 +++++-- .../maniphest/constants/ManiphestTaskStatus.php | 5 +++++ .../maniphest/editor/ManiphestTransactionEditor.php | 5 ++++- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/applications/maniphest/config/PhabricatorManiphestConfigOptions.php b/src/applications/maniphest/config/PhabricatorManiphestConfigOptions.php index cb011984bf..724309175d 100644 --- a/src/applications/maniphest/config/PhabricatorManiphestConfigOptions.php +++ b/src/applications/maniphest/config/PhabricatorManiphestConfigOptions.php @@ -111,6 +111,7 @@ final class PhabricatorManiphestConfigOptions 'name' => pht('Invalid'), 'name.full' => pht('Closed, Invalid'), 'closed' => true, + 'claim' => false, 'prefixes' => array( 'invalidate', 'invalidates', @@ -126,6 +127,7 @@ final class PhabricatorManiphestConfigOptions 'transaction.icon' => 'fa-files-o', 'special' => ManiphestTaskStatus::SPECIAL_DUPLICATE, 'closed' => true, + 'claim' => false, ), 'spite' => array( 'name' => pht('Spite'), @@ -202,6 +204,9 @@ The keys you can provide in a specification are: tasks can not be created or edited to have this status. Existing tasks with this status will not be affected, but you can batch edit them or let them die out on their own. + - `claim` //Optional bool.// By default, closing an unassigned task claims + it. You can set this to `false` to disable this behavior for a particular + status. Statuses will appear in the UI in the order specified. Note the status marked `special` as `duplicate` is not settable directly and will not appear in UI @@ -289,8 +294,6 @@ See the example below for a starting point. EOTEXT )); - - return array( $this->newOption('maniphest.custom-field-definitions', 'wild', array()) ->setSummary(pht('Custom Maniphest fields.')) diff --git a/src/applications/maniphest/constants/ManiphestTaskStatus.php b/src/applications/maniphest/constants/ManiphestTaskStatus.php index ab99f212e5..3a839d8fac 100644 --- a/src/applications/maniphest/constants/ManiphestTaskStatus.php +++ b/src/applications/maniphest/constants/ManiphestTaskStatus.php @@ -155,6 +155,10 @@ final class ManiphestTaskStatus extends ManiphestConstants { return false; } + public static function isClaimStatus($status) { + return self::getStatusAttribute($status, 'claim', true); + } + public static function isClosedStatus($status) { return !self::isOpenStatus($status); } @@ -279,6 +283,7 @@ final class ManiphestTaskStatus extends ManiphestConstants { 'suffixes' => 'optional list', 'keywords' => 'optional list', 'disabled' => 'optional bool', + 'claim' => 'optional bool', )); } diff --git a/src/applications/maniphest/editor/ManiphestTransactionEditor.php b/src/applications/maniphest/editor/ManiphestTransactionEditor.php index 922317b206..08cfc66632 100644 --- a/src/applications/maniphest/editor/ManiphestTransactionEditor.php +++ b/src/applications/maniphest/editor/ManiphestTransactionEditor.php @@ -971,8 +971,11 @@ final class ManiphestTransactionEditor // If the task is not assigned, not being assigned, currently open, and // being closed, try to assign the actor as the owner. if ($is_unassigned && !$any_assign && $is_open && $is_closing) { + $is_claim = ManiphestTaskStatus::isClaimStatus($new_status); + // Don't assign the actor if they aren't a real user. - if ($actor_phid) { + // Don't claim the task if the status is configured to not claim. + if ($actor_phid && $is_claim) { $results[] = id(new ManiphestTransaction()) ->setTransactionType(ManiphestTransaction::TYPE_OWNER) ->setNewValue($actor_phid); From de379c8b61b8a6035aa9cc020b1abdd51e4ad414 Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 12 Feb 2016 06:19:26 -0800 Subject: [PATCH 56/65] Allow workboard sorting and filtering to be saved as defaults Summary: Fixes T6641. This allows users who have permission to edit a project to use "Save as Default" to save the current order and filter as defaults for the project. These are per-board defaults, and apply to all users. The rationale is that I think the best default ordering/filtering depends mostly on the board, not the viewer. This seems to align with most requests in the task, although rationale is a bit light. But, for example, it seems reasonable you might want to change the default filter to "All Tasks" on a sprint board, so you can see what's in the "Done" column. This also fixes some minor issues I ran into: - Herald could hit an issue while checking permissions if the project was a subproject and a non-member had a triggering rule. - "Advanced filter..." did not prefill with the current filter. Test Plan: - Set default order and filter on a workboard. - Reloaded board, saw settings stick. - Tried to edit a board as an unprivileged user (disabled menu items, error). - Reviewed transaction log. Reviewers: chad Reviewed By: chad Maniphest Tasks: T6641 Differential Revision: https://secure.phabricator.com/D15260 --- resources/sql/autopatches/20160212.proj.1.sql | 2 + resources/sql/autopatches/20160212.proj.2.sql | 2 + src/__phutil_library_map__.php | 2 + .../PhabricatorProjectApplication.php | 2 + .../PhabricatorProjectBoardViewController.php | 123 +++++++++++++++--- .../PhabricatorProjectDefaultController.php | 90 +++++++++++++ .../PhabricatorProjectTransactionEditor.php | 26 +++- .../project/storage/PhabricatorProject.php | 30 +++++ .../storage/PhabricatorProjectTransaction.php | 48 +++++-- 9 files changed, 293 insertions(+), 32 deletions(-) create mode 100644 resources/sql/autopatches/20160212.proj.1.sql create mode 100644 resources/sql/autopatches/20160212.proj.2.sql create mode 100644 src/applications/project/controller/PhabricatorProjectDefaultController.php diff --git a/resources/sql/autopatches/20160212.proj.1.sql b/resources/sql/autopatches/20160212.proj.1.sql new file mode 100644 index 0000000000..7d8c19b0b1 --- /dev/null +++ b/resources/sql/autopatches/20160212.proj.1.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_project.project + ADD properties LONGTEXT NOT NULL COLLATE {$COLLATE_TEXT}; diff --git a/resources/sql/autopatches/20160212.proj.2.sql b/resources/sql/autopatches/20160212.proj.2.sql new file mode 100644 index 0000000000..f6f793aec4 --- /dev/null +++ b/resources/sql/autopatches/20160212.proj.2.sql @@ -0,0 +1,2 @@ +UPDATE {$NAMESPACE}_project.project + SET properties = '{}' WHERE properties = ''; diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index b66d31b3c0..aacf85ac45 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -2896,6 +2896,7 @@ phutil_register_library_map(array( 'PhabricatorProjectCustomFieldStringIndex' => 'applications/project/storage/PhabricatorProjectCustomFieldStringIndex.php', 'PhabricatorProjectDAO' => 'applications/project/storage/PhabricatorProjectDAO.php', 'PhabricatorProjectDatasource' => 'applications/project/typeahead/PhabricatorProjectDatasource.php', + 'PhabricatorProjectDefaultController' => 'applications/project/controller/PhabricatorProjectDefaultController.php', 'PhabricatorProjectDescriptionField' => 'applications/project/customfield/PhabricatorProjectDescriptionField.php', 'PhabricatorProjectDetailsProfilePanel' => 'applications/project/profilepanel/PhabricatorProjectDetailsProfilePanel.php', 'PhabricatorProjectEditController' => 'applications/project/controller/PhabricatorProjectEditController.php', @@ -7332,6 +7333,7 @@ phutil_register_library_map(array( 'PhabricatorProjectCustomFieldStringIndex' => 'PhabricatorCustomFieldStringIndexStorage', 'PhabricatorProjectDAO' => 'PhabricatorLiskDAO', 'PhabricatorProjectDatasource' => 'PhabricatorTypeaheadDatasource', + 'PhabricatorProjectDefaultController' => 'PhabricatorProjectBoardController', 'PhabricatorProjectDescriptionField' => 'PhabricatorProjectStandardCustomField', 'PhabricatorProjectDetailsProfilePanel' => 'PhabricatorProfilePanel', 'PhabricatorProjectEditController' => 'PhabricatorProjectController', diff --git a/src/applications/project/application/PhabricatorProjectApplication.php b/src/applications/project/application/PhabricatorProjectApplication.php index 2d00c4c90c..3a39aefa22 100644 --- a/src/applications/project/application/PhabricatorProjectApplication.php +++ b/src/applications/project/application/PhabricatorProjectApplication.php @@ -94,6 +94,8 @@ final class PhabricatorProjectApplication extends PhabricatorApplication { => 'PhabricatorProjectSilenceController', 'warning/(?P[1-9]\d*)/' => 'PhabricatorProjectSubprojectWarningController', + 'default/(?P[1-9]\d*)/(?P[^/]+)/' + => 'PhabricatorProjectDefaultController', ), '/tag/' => array( '(?P[^/]+)/' => 'PhabricatorProjectViewController', diff --git a/src/applications/project/controller/PhabricatorProjectBoardViewController.php b/src/applications/project/controller/PhabricatorProjectBoardViewController.php index 0d977c2aca..e9a5159295 100644 --- a/src/applications/project/controller/PhabricatorProjectBoardViewController.php +++ b/src/applications/project/controller/PhabricatorProjectBoardViewController.php @@ -8,7 +8,6 @@ final class PhabricatorProjectBoardViewController private $id; private $slug; private $queryKey; - private $filter; private $sortKey; private $showHidden; @@ -56,10 +55,18 @@ final class PhabricatorProjectBoardViewController $search_engine->getQueryResultsPageURI($saved->getQueryKey()))); } - $query_key = $request->getURIData('queryKey'); - if (!$query_key) { - $query_key = 'open'; + $query_key = $this->getDefaultFilter($project); + + $request_query = $request->getStr('filter'); + if (strlen($request_query)) { + $query_key = $request_query; } + + $uri_query = $request->getURIData('queryKey'); + if (strlen($uri_query)) { + $query_key = $uri_query; + } + $this->queryKey = $query_key; $custom_query = null; @@ -382,10 +389,12 @@ final class PhabricatorProjectBoardViewController $sort_menu = $this->buildSortMenu( $viewer, + $project, $this->sortKey); $filter_menu = $this->buildFilterMenu( $viewer, + $project, $custom_query, $search_engine, $query_key); @@ -445,20 +454,49 @@ final class PhabricatorProjectBoardViewController $this->showHidden = $request->getBool('hidden'); $this->id = $project->getID(); - $sort_key = $request->getStr('order'); - switch ($sort_key) { + $sort_key = $this->getDefaultSort($project); + + $request_sort = $request->getStr('order'); + if ($this->isValidSort($request_sort)) { + $sort_key = $request_sort; + } + + $this->sortKey = $sort_key; + } + + private function getDefaultSort(PhabricatorProject $project) { + $default_sort = $project->getDefaultWorkboardSort(); + + if ($this->isValidSort($default_sort)) { + return $default_sort; + } + + return PhabricatorProjectColumn::DEFAULT_ORDER; + } + + private function getDefaultFilter(PhabricatorProject $project) { + $default_filter = $project->getDefaultWorkboardFilter(); + + if (strlen($default_filter)) { + return $default_filter; + } + + return 'open'; + } + + private function isValidSort($sort) { + switch ($sort) { case PhabricatorProjectColumn::ORDER_NATURAL: case PhabricatorProjectColumn::ORDER_PRIORITY: - break; - default: - $sort_key = PhabricatorProjectColumn::DEFAULT_ORDER; - break; + return true; } - $this->sortKey = $sort_key; + + return false; } private function buildSortMenu( PhabricatorUser $viewer, + PhabricatorProject $project, $sort_key) { $sort_icon = id(new PHUIIconView()) @@ -489,6 +527,24 @@ final class PhabricatorProjectBoardViewController $items[] = $item; } + $id = $project->getID(); + + $save_uri = "default/{$id}/sort/"; + $save_uri = $this->getApplicationURI($save_uri); + $save_uri = $this->getURIWithState($save_uri, $force = true); + + $can_edit = PhabricatorPolicyFilter::hasCapability( + $viewer, + $project, + PhabricatorPolicyCapability::CAN_EDIT); + + $items[] = id(new PhabricatorActionView()) + ->setIcon('fa-floppy-o') + ->setName(pht('Save as Default')) + ->setHref($save_uri) + ->setWorkflow(true) + ->setDisabled(!$can_edit); + $sort_menu = id(new PhabricatorActionListView()) ->setUser($viewer); foreach ($items as $item) { @@ -507,8 +563,10 @@ final class PhabricatorProjectBoardViewController return $sort_button; } + private function buildFilterMenu( PhabricatorUser $viewer, + PhabricatorProject $project, $custom_query, PhabricatorApplicationSearchEngine $engine, $query_key) { @@ -551,18 +609,40 @@ final class PhabricatorProjectBoardViewController $uri = $engine->getQueryResultsPageURI($key); } - $uri = $this->getURIWithState($uri); + $uri = $this->getURIWithState($uri) + ->setQueryParam('filter', null); $item->setHref($uri); $items[] = $item; } + $id = $project->getID(); + + $filter_uri = $this->getApplicationURI("board/{$id}/filter/"); + $filter_uri = $this->getURIWithState($filter_uri, $force = true); + $items[] = id(new PhabricatorActionView()) ->setIcon('fa-cog') - ->setHref($this->getApplicationURI('board/'.$this->id.'/filter/')) + ->setHref($filter_uri) ->setWorkflow(true) ->setName(pht('Advanced Filter...')); + $save_uri = "default/{$id}/filter/"; + $save_uri = $this->getApplicationURI($save_uri); + $save_uri = $this->getURIWithState($save_uri, $force = true); + + $can_edit = PhabricatorPolicyFilter::hasCapability( + $viewer, + $project, + PhabricatorPolicyCapability::CAN_EDIT); + + $items[] = id(new PhabricatorActionView()) + ->setIcon('fa-floppy-o') + ->setName(pht('Save as Default')) + ->setHref($save_uri) + ->setWorkflow(true) + ->setDisabled(!$can_edit); + $filter_menu = id(new PhabricatorActionListView()) ->setUser($viewer); foreach ($items as $item) { @@ -793,22 +873,31 @@ final class PhabricatorProjectBoardViewController * the rest of the board state persistent. If no URI is provided, this method * starts with the request URI. * - * @param string|null URI to add state parameters to. - * @return PhutilURI URI with state parameters. + * @param string|null URI to add state parameters to. + * @param bool True to explicitly include all state. + * @return PhutilURI URI with state parameters. */ - private function getURIWithState($base = null) { + private function getURIWithState($base = null, $force = false) { + $project = $this->getProject(); + if ($base === null) { $base = $this->getRequest()->getRequestURI(); } $base = new PhutilURI($base); - if ($this->sortKey != PhabricatorProjectColumn::DEFAULT_ORDER) { + if ($force || ($this->sortKey != $this->getDefaultSort($project))) { $base->setQueryParam('order', $this->sortKey); } else { $base->setQueryParam('order', null); } + if ($force || ($this->queryKey != $this->getDefaultFilter($project))) { + $base->setQueryParam('filter', $this->queryKey); + } else { + $base->setQueryParam('filter', null); + } + $base->setQueryParam('hidden', $this->showHidden ? 'true' : null); return $base; diff --git a/src/applications/project/controller/PhabricatorProjectDefaultController.php b/src/applications/project/controller/PhabricatorProjectDefaultController.php new file mode 100644 index 0000000000..0246f33f43 --- /dev/null +++ b/src/applications/project/controller/PhabricatorProjectDefaultController.php @@ -0,0 +1,90 @@ +getViewer(); + $project_id = $request->getURIData('projectID'); + + $project = id(new PhabricatorProjectQuery()) + ->setViewer($viewer) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->withIDs(array($project_id)) + ->executeOne(); + if (!$project) { + return new Aphront404Response(); + } + $this->setProject($project); + + $target = $request->getURIData('target'); + switch ($target) { + case 'filter': + $title = pht('Set Board Default Filter'); + $body = pht( + 'Make the current filter the new default filter for this board? '. + 'All users will see the new filter as the default when they view '. + 'the board.'); + $button = pht('Save Default Filter'); + + $xaction_value = $request->getStr('filter'); + $xaction_type = PhabricatorProjectTransaction::TYPE_DEFAULT_FILTER; + break; + case 'sort': + $title = pht('Set Board Default Order'); + $body = pht( + 'Make the current sort order the new default order for this board? '. + 'All users will see the new order as the default when they view '. + 'the board.'); + $button = pht('Save Default Order'); + + $xaction_value = $request->getStr('order'); + $xaction_type = PhabricatorProjectTransaction::TYPE_DEFAULT_SORT; + break; + default: + return new Aphront404Response(); + } + + $id = $project->getID(); + + $view_uri = $this->getApplicationURI("board/{$id}/"); + $view_uri = new PhutilURI($view_uri); + foreach ($request->getPassthroughRequestData() as $key => $value) { + $view_uri->setQueryParam($key, $value); + } + + if ($request->isFormPost()) { + $xactions = array(); + + $xactions[] = id(new PhabricatorProjectTransaction()) + ->setTransactionType($xaction_type) + ->setNewValue($xaction_value); + + id(new PhabricatorProjectTransactionEditor()) + ->setActor($viewer) + ->setContentSourceFromRequest($request) + ->setContinueOnNoEffect(true) + ->setContinueOnMissingFields(true) + ->applyTransactions($project, $xactions); + + return id(new AphrontRedirectResponse())->setURI($view_uri); + } + + $dialog = $this->newDialog() + ->setTitle($title) + ->appendChild($body) + ->setDisableWorkflowOnCancel(true) + ->addCancelButton($view_uri) + ->addSubmitButton($title); + + foreach ($request->getPassthroughRequestData() as $key => $value) { + $dialog->addHiddenInput($key, $value); + } + + return $dialog; + } +} diff --git a/src/applications/project/editor/PhabricatorProjectTransactionEditor.php b/src/applications/project/editor/PhabricatorProjectTransactionEditor.php index 51a120000a..c9594122d8 100644 --- a/src/applications/project/editor/PhabricatorProjectTransactionEditor.php +++ b/src/applications/project/editor/PhabricatorProjectTransactionEditor.php @@ -40,6 +40,8 @@ final class PhabricatorProjectTransactionEditor $types[] = PhabricatorProjectTransaction::TYPE_PARENT; $types[] = PhabricatorProjectTransaction::TYPE_MILESTONE; $types[] = PhabricatorProjectTransaction::TYPE_HASWORKBOARD; + $types[] = PhabricatorProjectTransaction::TYPE_DEFAULT_SORT; + $types[] = PhabricatorProjectTransaction::TYPE_DEFAULT_FILTER; return $types; } @@ -71,6 +73,10 @@ final class PhabricatorProjectTransactionEditor case PhabricatorProjectTransaction::TYPE_PARENT: case PhabricatorProjectTransaction::TYPE_MILESTONE: return null; + case PhabricatorProjectTransaction::TYPE_DEFAULT_SORT: + return $object->getDefaultWorkboardSort(); + case PhabricatorProjectTransaction::TYPE_DEFAULT_FILTER: + return $object->getDefaultWorkboardFilter(); } return parent::getCustomTransactionOldValue($object, $xaction); @@ -89,6 +95,8 @@ final class PhabricatorProjectTransactionEditor case PhabricatorProjectTransaction::TYPE_LOCKED: case PhabricatorProjectTransaction::TYPE_PARENT: case PhabricatorProjectTransaction::TYPE_MILESTONE: + case PhabricatorProjectTransaction::TYPE_DEFAULT_SORT: + case PhabricatorProjectTransaction::TYPE_DEFAULT_FILTER: return $xaction->getNewValue(); case PhabricatorProjectTransaction::TYPE_HASWORKBOARD: return (int)$xaction->getNewValue(); @@ -139,6 +147,12 @@ final class PhabricatorProjectTransactionEditor case PhabricatorProjectTransaction::TYPE_HASWORKBOARD: $object->setHasWorkboard($xaction->getNewValue()); return; + case PhabricatorProjectTransaction::TYPE_DEFAULT_SORT: + $object->setDefaultWorkboardSort($xaction->getNewValue()); + return; + case PhabricatorProjectTransaction::TYPE_DEFAULT_FILTER: + $object->setDefaultWorkboardFilter($xaction->getNewValue()); + return; } return parent::applyCustomInternalTransaction($object, $xaction); @@ -181,6 +195,8 @@ final class PhabricatorProjectTransactionEditor case PhabricatorProjectTransaction::TYPE_PARENT: case PhabricatorProjectTransaction::TYPE_MILESTONE: case PhabricatorProjectTransaction::TYPE_HASWORKBOARD: + case PhabricatorProjectTransaction::TYPE_DEFAULT_SORT: + case PhabricatorProjectTransaction::TYPE_DEFAULT_FILTER: return; } @@ -866,8 +882,16 @@ final class PhabricatorProjectTransactionEditor PhabricatorLiskDAO $object, array $xactions) { + // Herald rules may run on behalf of other users and need to execute + // membership checks against ancestors. + $project = id(new PhabricatorProjectQuery()) + ->setViewer(PhabricatorUser::getOmnipotentUser()) + ->withPHIDs(array($object->getPHID())) + ->needAncestorMembers(true) + ->executeOne(); + return id(new PhabricatorProjectHeraldAdapter()) - ->setProject($object); + ->setProject($project); } } diff --git a/src/applications/project/storage/PhabricatorProject.php b/src/applications/project/storage/PhabricatorProject.php index 2d8b3f5218..861fac646b 100644 --- a/src/applications/project/storage/PhabricatorProject.php +++ b/src/applications/project/storage/PhabricatorProject.php @@ -36,6 +36,8 @@ final class PhabricatorProject extends PhabricatorProjectDAO protected $projectDepth; protected $projectPathKey; + protected $properties = array(); + private $memberPHIDs = self::ATTACHABLE; private $watcherPHIDs = self::ATTACHABLE; private $sparseWatchers = self::ATTACHABLE; @@ -198,6 +200,9 @@ final class PhabricatorProject extends PhabricatorProjectDAO protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, + self::CONFIG_SERIALIZATION => array( + 'properties' => self::SERIALIZATION_JSON, + ), self::CONFIG_COLUMN_SCHEMA => array( 'name' => 'sort128', 'status' => 'text32', @@ -549,6 +554,31 @@ final class PhabricatorProject extends PhabricatorProjectDAO return idx($map, $color, $color); } + public function getProperty($key, $default = null) { + return idx($this->properties, $key, $default); + } + + public function setProperty($key, $value) { + $this->properties[$key] = $value; + return $this; + } + + public function getDefaultWorkboardSort() { + return $this->getProperty('workboard.sort.default'); + } + + public function setDefaultWorkboardSort($sort) { + return $this->setProperty('workboard.sort.default', $sort); + } + + public function getDefaultWorkboardFilter() { + return $this->getProperty('workboard.filter.default'); + } + + public function setDefaultWorkboardFilter($filter) { + return $this->setProperty('workboard.filter.default', $filter); + } + /* -( PhabricatorCustomFieldInterface )------------------------------------ */ diff --git a/src/applications/project/storage/PhabricatorProjectTransaction.php b/src/applications/project/storage/PhabricatorProjectTransaction.php index f668b0d2ec..e812952630 100644 --- a/src/applications/project/storage/PhabricatorProjectTransaction.php +++ b/src/applications/project/storage/PhabricatorProjectTransaction.php @@ -13,6 +13,8 @@ final class PhabricatorProjectTransaction const TYPE_PARENT = 'project:parent'; const TYPE_MILESTONE = 'project:milestone'; const TYPE_HASWORKBOARD = 'project:hasworkboard'; + const TYPE_DEFAULT_SORT = 'project:sort'; + const TYPE_DEFAULT_FILTER = 'project:filter'; // NOTE: This is deprecated, members are just a normal edge now. const TYPE_MEMBERS = 'project:members'; @@ -66,8 +68,29 @@ final class PhabricatorProjectTransaction return parent::getColor(); } - public function getIcon() { + public function shouldHideForFeed() { + switch ($this->getTransactionType()) { + case self::TYPE_HASWORKBOARD: + case self::TYPE_DEFAULT_SORT: + case self::TYPE_DEFAULT_FILTER: + return true; + } + return parent::shouldHideForFeed(); + } + + public function shouldHideForMail(array $xactions) { + switch ($this->getTransactionType()) { + case self::TYPE_HASWORKBOARD: + case self::TYPE_DEFAULT_SORT: + case self::TYPE_DEFAULT_FILTER: + return true; + } + + return parent::shouldHideForMail($xactions); + } + + public function getIcon() { $old = $this->getOldValue(); $new = $this->getNewValue(); @@ -258,6 +281,16 @@ final class PhabricatorProjectTransaction '%s disabled the workboard for this project.', $author_handle); } + + case self::TYPE_DEFAULT_SORT: + return pht( + '%s changed the default sort order for the project workboard.', + $author_handle); + + case self::TYPE_DEFAULT_FILTER: + return pht( + '%s changed the default filter for the project workboard.', + $author_handle); } return parent::getTitle(); @@ -379,19 +412,6 @@ final class PhabricatorProjectTransaction $this->renderSlugList($rem)); } - case self::TYPE_HASWORKBOARD: - if ($new) { - return pht( - '%s enabled the workboard for %s.', - $author_handle, - $object_handle); - } else { - return pht( - '%s disabled the workboard for %s.', - $author_handle, - $object_handle); - } - } return parent::getTitleForFeed(); From 8af3abc40a6631348d1f2d4ab06d24c7734e9458 Mon Sep 17 00:00:00 2001 From: Jay Shirley Date: Fri, 12 Feb 2016 07:48:57 -0800 Subject: [PATCH 57/65] Add `transactionID` to maniphest.gettransactions output Summary: This commit adds the `transactionID` field to manphest.gettransactions, to satisfy the request in T10327 Test Plan: Call the `maniphest.gettransactions` endpoint, verify `transactionID` is present Reviewers: epriestley, #blessed_reviewers Reviewed By: epriestley, #blessed_reviewers Subscribers: chad, Korvin Differential Revision: https://secure.phabricator.com/D15250 --- .../conduit/ManiphestGetTaskTransactionsConduitAPIMethod.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/applications/maniphest/conduit/ManiphestGetTaskTransactionsConduitAPIMethod.php b/src/applications/maniphest/conduit/ManiphestGetTaskTransactionsConduitAPIMethod.php index 4c64a56b33..8b0d0496cf 100644 --- a/src/applications/maniphest/conduit/ManiphestGetTaskTransactionsConduitAPIMethod.php +++ b/src/applications/maniphest/conduit/ManiphestGetTaskTransactionsConduitAPIMethod.php @@ -59,6 +59,7 @@ final class ManiphestGetTaskTransactionsConduitAPIMethod $results[$task_id][] = array( 'taskID' => $task_id, + 'transactionID' => $transaction->getID(), 'transactionPHID' => $transaction->getPHID(), 'transactionType' => $transaction->getTransactionType(), 'oldValue' => $transaction->getOldValue(), From 5e3754828f7bf83e7fd45335155d4fe95d95397a Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 12 Feb 2016 04:14:41 -0800 Subject: [PATCH 58/65] Fix handling of gzip in VCS responses Summary: Fixes T10264. I'm reasonably confident that this is the chain of events here: First, prior to 8269fd6e, we would ignore "Content-Encoding" when reading inbound bodies. So if a request was gzipped, we would read a gzipped body, then give `git-http-backend` a gzipped body with "Content-Encoding: gzip". Everything matched normally, so that was fine, except in the cluster. In the cluster, we'd accept "gzip + compressed body" and proxy it, but not tell cURL that it was already compressed. cURL would think it was raw data, so it would arrive on the repository host with a compressed body but no "Content-Encoding: gzip". Then we'd hand it to git in the same form. This caused the issue in 8269fd6e: handing it compressed data, but no "this is compressed" header. To fix this, I made us decompress the encoding when we read the body, so the cluster now proxies raw data instead of proxying gzipped data. This fixed the issue in the cluster, but created a new issue on non-cluster hosts. The new issue is that we accept "gzip + compressed body" and decompress the body, but then pass the //original// header to `git-http-backend`. So now we have the opposite problem from what we originally had: a "gzip" header, but a raw body. To fix //this//, we could do two things: - Revert 8269fd6e, then change the proxy request to preserve "Content-Encoding" instead. - Stop telling `git-http-backend` that we're handing it compressed data when we're handing it raw data. I did the latter here because it's an easier change to make and test, we'll need to interact with the raw data later anyway, to implement repository virtualization in connection with T8238. Test Plan: See T10264 for users confirming this fix. Reviewers: chad Reviewed By: chad Maniphest Tasks: T10264 Differential Revision: https://secure.phabricator.com/D15258 --- .../diffusion/controller/DiffusionServeController.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/applications/diffusion/controller/DiffusionServeController.php b/src/applications/diffusion/controller/DiffusionServeController.php index 8d131eca6d..9de692d620 100644 --- a/src/applications/diffusion/controller/DiffusionServeController.php +++ b/src/applications/diffusion/controller/DiffusionServeController.php @@ -427,11 +427,14 @@ final class DiffusionServeController extends DiffusionController { '$PATH')); } + // NOTE: We do not set HTTP_CONTENT_ENCODING here, because we already + // decompressed the request when we read the request body, so the body is + // just plain data with no encoding. + $env = array( 'REQUEST_METHOD' => $_SERVER['REQUEST_METHOD'], 'QUERY_STRING' => $query_string, 'CONTENT_TYPE' => $request->getHTTPHeader('Content-Type'), - 'HTTP_CONTENT_ENCODING' => $request->getHTTPHeader('Content-Encoding'), 'REMOTE_ADDR' => $_SERVER['REMOTE_ADDR'], 'GIT_PROJECT_ROOT' => $repository_root, 'GIT_HTTP_EXPORT_ALL' => '1', From a5bbe256c8eab7f3088bae98d9ee6adde6bb8fa5 Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 12 Feb 2016 04:50:03 -0800 Subject: [PATCH 59/65] Fix a couple of missing translation strings Summary: Clean the UI up a little. Test Plan: {F1106533} Reviewers: chad Reviewed By: chad Differential Revision: https://secure.phabricator.com/D15259 --- .../PhabricatorUSEnglishTranslation.php | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/infrastructure/internationalization/translation/PhabricatorUSEnglishTranslation.php b/src/infrastructure/internationalization/translation/PhabricatorUSEnglishTranslation.php index ff4174522c..93027ce5d6 100644 --- a/src/infrastructure/internationalization/translation/PhabricatorUSEnglishTranslation.php +++ b/src/infrastructure/internationalization/translation/PhabricatorUSEnglishTranslation.php @@ -1514,6 +1514,20 @@ final class PhabricatorUSEnglishTranslation 'Permanently destroyed %s object.', 'Permanently destroyed %s objects.', ), + + '%s added %s watcher(s) for %s: %s.' => array( + array( + '%s added a watcher for %3$s: %4$s.', + '%s added watchers for %3$s: %4$s.', + ), + ), + + '%s removed %s watcher(s) for %s: %s.' => array( + array( + '%s removed a watcher for %3$s: %4$s.', + '%s removed watchers for %3$s: %4$s.', + ), + ), ); } From 2f054edfa22ea40ce3179688fce25d0c618d6c98 Mon Sep 17 00:00:00 2001 From: June Rhodes Date: Fri, 12 Feb 2016 05:03:43 -0800 Subject: [PATCH 60/65] Hide milestone columns when milestone is archived Summary: Fixes T10310. This replaces the "Hide Column" / "Show Column" option for milestone columns with one that archives/unarchives, and hides milestone columns when the milestone project is archived. Test Plan: - Hid and unhid a normal column (got normal dialogs). - Hid and unhid a milestone column (got "archive project" dialogs, underlying project archived/unarchived, column vanished). Reviewers: hach-que, #blessed_reviewers, chad Reviewed By: #blessed_reviewers, chad Subscribers: jcowgar, Korvin Maniphest Tasks: T10310 Differential Revision: https://secure.phabricator.com/D15231 --- ...PhabricatorProjectColumnHideController.php | 96 +++++++++++++------ .../storage/PhabricatorProjectColumn.php | 5 + 2 files changed, 72 insertions(+), 29 deletions(-) diff --git a/src/applications/project/controller/PhabricatorProjectColumnHideController.php b/src/applications/project/controller/PhabricatorProjectColumnHideController.php index ae39e783bb..41665ea6f8 100644 --- a/src/applications/project/controller/PhabricatorProjectColumnHideController.php +++ b/src/applications/project/controller/PhabricatorProjectColumnHideController.php @@ -52,42 +52,80 @@ final class PhabricatorProjectColumnHideController ->addCancelButton($view_uri, pht('Okay')); } + $proxy = $column->getProxy(); + if ($request->isFormPost()) { - if ($column->isHidden()) { - $new_status = PhabricatorProjectColumn::STATUS_ACTIVE; + if ($proxy) { + if ($proxy->isArchived()) { + $new_status = PhabricatorProjectStatus::STATUS_ACTIVE; + } else { + $new_status = PhabricatorProjectStatus::STATUS_ARCHIVED; + } + + $xactions = array(); + + $xactions[] = id(new PhabricatorProjectTransaction()) + ->setTransactionType(PhabricatorProjectTransaction::TYPE_STATUS) + ->setNewValue($new_status); + + id(new PhabricatorProjectTransactionEditor()) + ->setActor($viewer) + ->setContentSourceFromRequest($request) + ->setContinueOnNoEffect(true) + ->setContinueOnMissingFields(true) + ->applyTransactions($proxy, $xactions); } else { - $new_status = PhabricatorProjectColumn::STATUS_HIDDEN; + if ($column->isHidden()) { + $new_status = PhabricatorProjectColumn::STATUS_ACTIVE; + } else { + $new_status = PhabricatorProjectColumn::STATUS_HIDDEN; + } + + $type_status = PhabricatorProjectColumnTransaction::TYPE_STATUS; + $xactions = array( + id(new PhabricatorProjectColumnTransaction()) + ->setTransactionType($type_status) + ->setNewValue($new_status), + ); + + $editor = id(new PhabricatorProjectColumnTransactionEditor()) + ->setActor($viewer) + ->setContinueOnNoEffect(true) + ->setContentSourceFromRequest($request) + ->applyTransactions($column, $xactions); } - $type_status = PhabricatorProjectColumnTransaction::TYPE_STATUS; - $xactions = array( - id(new PhabricatorProjectColumnTransaction()) - ->setTransactionType($type_status) - ->setNewValue($new_status), - ); - - $editor = id(new PhabricatorProjectColumnTransactionEditor()) - ->setActor($viewer) - ->setContinueOnNoEffect(true) - ->setContentSourceFromRequest($request) - ->applyTransactions($column, $xactions); - return id(new AphrontRedirectResponse())->setURI($view_uri); } - if ($column->isHidden()) { - $title = pht('Show Column'); + if ($proxy) { + if ($column->isHidden()) { + $title = pht('Activate and Show Column'); + $body = pht( + 'This column is hidden because it represents an archived '. + 'subproject. Do you want to activate the subproject so the '. + 'column is visible again?'); + $button = pht('Activate Subproject'); + } else { + $title = pht('Archive and Hide Column'); + $body = pht( + 'This column is visible because it represents an active '. + 'subproject. Do you want to hide the column by archiving the '. + 'subproject?'); + $button = pht('Archive Subproject'); + } } else { - $title = pht('Hide Column'); - } - - if ($column->isHidden()) { - $body = pht( - 'Are you sure you want to show this column?'); - } else { - $body = pht( - 'Are you sure you want to hide this column? It will no longer '. - 'appear on the workboard.'); + if ($column->isHidden()) { + $title = pht('Show Column'); + $body = pht('Are you sure you want to show this column?'); + $button = pht('Show Column'); + } else { + $title = pht('Hide Column'); + $body = pht( + 'Are you sure you want to hide this column? It will no longer '. + 'appear on the workboard.'); + $button = pht('Hide Column'); + } } $dialog = $this->newDialog() @@ -96,7 +134,7 @@ final class PhabricatorProjectColumnHideController ->appendChild($body) ->setDisableWorkflowOnCancel(true) ->addCancelButton($view_uri) - ->addSubmitButton($title); + ->addSubmitButton($button); foreach ($request->getPassthroughRequestData() as $key => $value) { $dialog->addHiddenInput($key, $value); diff --git a/src/applications/project/storage/PhabricatorProjectColumn.php b/src/applications/project/storage/PhabricatorProjectColumn.php index 0ab6ec89c0..4092ca7fa8 100644 --- a/src/applications/project/storage/PhabricatorProjectColumn.php +++ b/src/applications/project/storage/PhabricatorProjectColumn.php @@ -86,6 +86,11 @@ final class PhabricatorProjectColumn } public function isHidden() { + $proxy = $this->getProxy(); + if ($proxy) { + return $proxy->isArchived(); + } + return ($this->getStatus() == self::STATUS_HIDDEN); } From 68d05934a796c4add09339d350cc35976de58eb3 Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 12 Feb 2016 09:05:05 -0800 Subject: [PATCH 61/65] Don't show un-completeable results in people/project autocomplete Summary: Fixes T10285. - If a result (like a milestone) has no primary hashtag, try to fill in a secondary hashtag. - If we can't find any hashtag, don't return the result. This produces these behaviors: - By default, you can't autocomplete milestones. - If you give one a hashtag, you can. We might want to give milestones "special" hashtags eventually (like `#xyz/33`) but this fixes the confusing/broken behavior in the UI and we can wait for a better use case for letting you autocomplete milestones, I think. Also, don't try to cycle hashtags when renaming milestones. This was a little inconsistent before. Test Plan: - Autocompleted normal projects. - Autocompleted milestones with explicit hashtags. - No autocomplete entry for milestones with no special hashtags. - Used normal typeahead to get tag-less milestones to make sure I didn't break anything. Reviewers: chad Reviewed By: chad Maniphest Tasks: T10285 Differential Revision: https://secure.phabricator.com/D15261 --- .../PhabricatorProjectTransactionEditor.php | 9 +++++---- .../PhabricatorProjectDatasource.php | 20 ++++++++++++++++++- .../control/PhabricatorRemarkupControl.php | 6 +++++- 3 files changed, 29 insertions(+), 6 deletions(-) diff --git a/src/applications/project/editor/PhabricatorProjectTransactionEditor.php b/src/applications/project/editor/PhabricatorProjectTransactionEditor.php index c9594122d8..b1aab1e417 100644 --- a/src/applications/project/editor/PhabricatorProjectTransactionEditor.php +++ b/src/applications/project/editor/PhabricatorProjectTransactionEditor.php @@ -169,11 +169,12 @@ final class PhabricatorProjectTransactionEditor case PhabricatorProjectTransaction::TYPE_NAME: // First, add the old name as a secondary slug; this is helpful // for renames and generally a good thing to do. - if ($old !== null) { - $this->addSlug($object, $old, false); + if (!$this->getIsMilestone()) { + if ($old !== null) { + $this->addSlug($object, $old, false); + } + $this->addSlug($object, $new, false); } - $this->addSlug($object, $new, false); - return; case PhabricatorProjectTransaction::TYPE_SLUGS: $old = $xaction->getOldValue(); diff --git a/src/applications/project/typeahead/PhabricatorProjectDatasource.php b/src/applications/project/typeahead/PhabricatorProjectDatasource.php index 33fb8df0e9..40d8dbb0e6 100644 --- a/src/applications/project/typeahead/PhabricatorProjectDatasource.php +++ b/src/applications/project/typeahead/PhabricatorProjectDatasource.php @@ -38,6 +38,8 @@ final class PhabricatorProjectDatasource $query->withIsMilestone(false); } + $for_autocomplete = $this->getParameter('autocomplete'); + $projs = $this->executeQuery($query); $projs = mpull($projs, null, 'getPHID'); @@ -58,6 +60,23 @@ final class PhabricatorProjectDatasource if (!isset($has_cols[$proj->getPHID()])) { continue; } + + $slug = $proj->getPrimarySlug(); + if (!strlen($slug)) { + foreach ($proj->getSlugs() as $slug_object) { + $slug = $slug_object->getSlug(); + if (strlen($slug)) { + break; + } + } + } + + // If we're building results for the autocompleter and this project + // doesn't have any usable slugs, don't return it as a result. + if ($for_autocomplete && !strlen($slug)) { + continue; + } + $closed = null; if ($proj->isArchived()) { $closed = pht('Archived'); @@ -78,7 +97,6 @@ final class PhabricatorProjectDatasource ->setPriorityType('proj') ->setClosed($closed); - $slug = $proj->getPrimarySlug(); if (strlen($slug)) { $proj_result->setAutocomplete('#'.$slug); } diff --git a/src/view/form/control/PhabricatorRemarkupControl.php b/src/view/form/control/PhabricatorRemarkupControl.php index 11388d257e..eeec4971da 100644 --- a/src/view/form/control/PhabricatorRemarkupControl.php +++ b/src/view/form/control/PhabricatorRemarkupControl.php @@ -45,7 +45,11 @@ final class PhabricatorRemarkupControl extends AphrontFormTextAreaControl { $root_id = celerity_generate_unique_node_id(); $user_datasource = new PhabricatorPeopleDatasource(); - $proj_datasource = new PhabricatorProjectDatasource(); + $proj_datasource = id(new PhabricatorProjectDatasource()) + ->setParameters( + array( + 'autocomplete' => 1, + )); Javelin::initBehavior( 'phabricator-remarkup-assist', From ca908d7cc4a8836b8ea0b291601b8aa8450a05e0 Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 12 Feb 2016 10:57:13 -0800 Subject: [PATCH 62/65] Don't autoname milestones, but do show the previous milestone name as a hint Summary: Fixes T10347. In the long run maybe we'll try to guess this better, but for now get rid of the "Milestone X" hardcode and just show what the last one was called. Test Plan: - Created the first milestone for a project. - Created the nth milestone for a project. Reviewers: chad Reviewed By: chad Maniphest Tasks: T10347 Differential Revision: https://secure.phabricator.com/D15262 --- .../engine/PhabricatorProjectEditEngine.php | 39 +++++++++++++------ .../project/query/PhabricatorProjectQuery.php | 23 +++++++++++ .../PhabricatorEditEngineConfiguration.php | 14 ++++++- 3 files changed, 64 insertions(+), 12 deletions(-) diff --git a/src/applications/project/engine/PhabricatorProjectEditEngine.php b/src/applications/project/engine/PhabricatorProjectEditEngine.php index 71f91b7bc1..ee101cb2e4 100644 --- a/src/applications/project/engine/PhabricatorProjectEditEngine.php +++ b/src/applications/project/engine/PhabricatorProjectEditEngine.php @@ -43,17 +43,7 @@ final class PhabricatorProjectEditEngine } protected function newEditableObject() { - $project = PhabricatorProject::initializeNewProject($this->getViewer()); - - $milestone = $this->getMilestoneProject(); - if ($milestone) { - $default_name = pht( - 'Milestone %s', - new PhutilNumber($milestone->loadNextMilestoneNumber())); - $project->setName($default_name); - } - - return $project; + return PhabricatorProject::initializeNewProject($this->getViewer()); } protected function newObjectQuery() { @@ -139,6 +129,7 @@ final class PhabricatorProjectEditEngine array( 'parent', 'milestone', + 'milestone.previous', 'name', 'std:project:internal:description', 'icon', @@ -166,8 +157,26 @@ final class PhabricatorProjectEditEngine $parent_phid = null; } + $previous_milestone_phid = null; if ($milestone) { $milestone_phid = $milestone->getPHID(); + + // Load the current milestone so we can show the user a hint about what + // it was called, so they don't have to remember if the next one should + // be "Sprint 287" or "Sprint 278". + + $number = ($milestone->loadNextMilestoneNumber() - 1); + if ($number > 0) { + $previous_milestone = id(new PhabricatorProjectQuery()) + ->setViewer($this->getViewer()) + ->withParentProjectPHIDs(array($milestone->getPHID())) + ->withIsMilestone(true) + ->withMilestoneNumberBetween($number, $number) + ->executeOne(); + if ($previous_milestone) { + $previous_milestone_phid = $previous_milestone->getPHID(); + } + } } else { $milestone_phid = null; } @@ -203,6 +212,14 @@ final class PhabricatorProjectEditEngine ->setIsDefaultable(false) ->setIsLockable(false) ->setIsLocked(true), + id(new PhabricatorHandlesEditField()) + ->setKey('milestone.previous') + ->setLabel(pht('Previous Milestone')) + ->setSingleValue($previous_milestone_phid) + ->setIsReorderable(false) + ->setIsDefaultable(false) + ->setIsLockable(false) + ->setIsLocked(true), id(new PhabricatorTextEditField()) ->setKey('name') ->setLabel(pht('Name')) diff --git a/src/applications/project/query/PhabricatorProjectQuery.php b/src/applications/project/query/PhabricatorProjectQuery.php index f4711bb4f7..d12e66e392 100644 --- a/src/applications/project/query/PhabricatorProjectQuery.php +++ b/src/applications/project/query/PhabricatorProjectQuery.php @@ -20,6 +20,8 @@ final class PhabricatorProjectQuery private $hasSubprojects; private $minDepth; private $maxDepth; + private $minMilestoneNumber; + private $maxMilestoneNumber; private $status = 'status-any'; const STATUS_ANY = 'status-any'; @@ -111,6 +113,12 @@ final class PhabricatorProjectQuery return $this; } + public function withMilestoneNumberBetween($min, $max) { + $this->minMilestoneNumber = $min; + $this->maxMilestoneNumber = $max; + return $this; + } + public function needMembers($need_members) { $this->needMembers = $need_members; return $this; @@ -494,6 +502,7 @@ final class PhabricatorProjectQuery } } + if ($this->hasSubprojects !== null) { $where[] = qsprintf( $conn, @@ -515,6 +524,20 @@ final class PhabricatorProjectQuery $this->maxDepth); } + if ($this->minMilestoneNumber !== null) { + $where[] = qsprintf( + $conn, + 'milestoneNumber >= %d', + $this->minMilestoneNumber); + } + + if ($this->maxMilestoneNumber !== null) { + $where[] = qsprintf( + $conn, + 'milestoneNumber <= %d', + $this->maxMilestoneNumber); + } + return $where; } diff --git a/src/applications/transactions/storage/PhabricatorEditEngineConfiguration.php b/src/applications/transactions/storage/PhabricatorEditEngineConfiguration.php index a2793d626f..38a4fda0a6 100644 --- a/src/applications/transactions/storage/PhabricatorEditEngineConfiguration.php +++ b/src/applications/transactions/storage/PhabricatorEditEngineConfiguration.php @@ -175,8 +175,20 @@ final class PhabricatorEditEngineConfiguration } private function reorderFields(array $fields) { + // Fields which can not be reordered are fixed in order at the top of the + // form. These are used to show instructions or contextual information. + + $fixed = array(); + foreach ($fields as $key => $field) { + if (!$field->getIsReorderable()) { + $fixed[$key] = $field; + } + } + $keys = $this->getFieldOrder(); - $fields = array_select_keys($fields, $keys) + $fields; + + $fields = $fixed + array_select_keys($fields, $keys) + $fields; + return $fields; } From de4167d6832d52479e7bb813b287719736b4fdd6 Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 12 Feb 2016 11:10:04 -0800 Subject: [PATCH 63/65] Put boundary spaces around crumb names so double-clicking doesn't flip out Summary: Occasionally, double clicking crumbs to select them is useful. Test Plan: {F1107226} Reviewers: chad Reviewed By: chad Differential Revision: https://secure.phabricator.com/D15263 --- src/view/phui/PHUICrumbView.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/view/phui/PHUICrumbView.php b/src/view/phui/PHUICrumbView.php index 5039e17606..392a354123 100644 --- a/src/view/phui/PHUICrumbView.php +++ b/src/view/phui/PHUICrumbView.php @@ -73,12 +73,16 @@ final class PHUICrumbView extends AphrontView { ->setIcon($this->icon); } + // Surround the crumb name with spaces so that double clicking it only + // selects the crumb itself. + $name = array(' ', $this->name, ' '); + $name = phutil_tag( 'span', array( 'class' => 'phui-crumb-name', ), - $this->name); + $name); $divider = null; if (!$this->isLastCrumb) { From 1e3a5bd2c0aede5f10cc4c13f168c7966c59da9b Mon Sep 17 00:00:00 2001 From: Chad Little Date: Fri, 12 Feb 2016 12:07:05 -0800 Subject: [PATCH 64/65] Filter out archived projects from ProjectProfileView Summary: This just hides them, should still show on "View All". Test Plan: Hide a Milestone, no longer see it on home. Click "View All", see all Milestones. Reviewers: epriestley Reviewed By: epriestley Subscribers: Korvin Differential Revision: https://secure.phabricator.com/D15264 --- .../controller/PhabricatorProjectProfileController.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/applications/project/controller/PhabricatorProjectProfileController.php b/src/applications/project/controller/PhabricatorProjectProfileController.php index 4e670475ce..27cf1bf344 100644 --- a/src/applications/project/controller/PhabricatorProjectProfileController.php +++ b/src/applications/project/controller/PhabricatorProjectProfileController.php @@ -201,6 +201,10 @@ final class PhabricatorProjectProfileController ->withParentProjectPHIDs(array($project->getPHID())) ->needImages(true) ->withIsMilestone(true) + ->withStatuses( + array( + PhabricatorProjectStatus::STATUS_ACTIVE, + )) ->setOrder('newest') ->execute(); if (!$milestones) { @@ -244,6 +248,10 @@ final class PhabricatorProjectProfileController ->setViewer($viewer) ->withParentProjectPHIDs(array($project->getPHID())) ->needImages(true) + ->withStatuses( + array( + PhabricatorProjectStatus::STATUS_ACTIVE, + )) ->withIsMilestone(false) ->setLimit($limit) ->execute(); From bec21d80a58403a388d175cd14f994ba7adefd3f Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 12 Feb 2016 13:55:32 -0800 Subject: [PATCH 65/65] Don't try to import proxy columns Summary: Fixes T10346. You finally wrung a clue out of the reporter and I think I figured this out. Here's the bug: - Create a project with a workboard and subprojects/milestones. - Create a new project, import columns from the first project. - We incorrectly import empty columns for the subprojects/milestons. Instead, skip proxy columns during import. Also, allow "hide column" to continue on missing fields, so columns with no name can be hidden. Test Plan: - Did the stuff above. - Workboard no longer populated with a bunch of "Unnamed Column" columns. - Hid several "Unnamed Column" columns. Reviewers: chad Reviewed By: chad Maniphest Tasks: T10346 Differential Revision: https://secure.phabricator.com/D15265 --- .../controller/PhabricatorProjectBoardImportController.php | 4 ++++ .../controller/PhabricatorProjectColumnHideController.php | 1 + 2 files changed, 5 insertions(+) diff --git a/src/applications/project/controller/PhabricatorProjectBoardImportController.php b/src/applications/project/controller/PhabricatorProjectBoardImportController.php index 8df4e8b810..988084d3ee 100644 --- a/src/applications/project/controller/PhabricatorProjectBoardImportController.php +++ b/src/applications/project/controller/PhabricatorProjectBoardImportController.php @@ -50,6 +50,10 @@ final class PhabricatorProjectBoardImportController if ($import_column->isHidden()) { continue; } + if ($import_column->getProxy()) { + continue; + } + $new_column = PhabricatorProjectColumn::initializeNewColumn($viewer) ->setSequence($import_column->getSequence()) ->setProjectPHID($project->getPHID()) diff --git a/src/applications/project/controller/PhabricatorProjectColumnHideController.php b/src/applications/project/controller/PhabricatorProjectColumnHideController.php index 41665ea6f8..27dbb17c47 100644 --- a/src/applications/project/controller/PhabricatorProjectColumnHideController.php +++ b/src/applications/project/controller/PhabricatorProjectColumnHideController.php @@ -91,6 +91,7 @@ final class PhabricatorProjectColumnHideController $editor = id(new PhabricatorProjectColumnTransactionEditor()) ->setActor($viewer) ->setContinueOnNoEffect(true) + ->setContinueOnMissingFields(true) ->setContentSourceFromRequest($request) ->applyTransactions($column, $xactions); }