From e5947e08d389acd41ee2046fef707cc02b04fe6a Mon Sep 17 00:00:00 2001 From: epriestley Date: Sat, 30 Jan 2016 16:03:13 -0800 Subject: [PATCH 01/54] Apply phutil_utf8ize() to stderr output from VCS commands prior to logging Summary: Ref T10228. Commands like `git-http-backend` can emit errors with raw bytes in the output. Sanitize these if present so we can log them in JSON format. Test Plan: Edited this into production. >_> sneaky sneaky <_< Reviewers: chad Reviewed By: chad Maniphest Tasks: T10228 Differential Revision: https://secure.phabricator.com/D15144 --- .../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 1d41d62efa..13290b9f41 100644 --- a/src/applications/diffusion/controller/DiffusionServeController.php +++ b/src/applications/diffusion/controller/DiffusionServeController.php @@ -466,7 +466,10 @@ final class DiffusionServeController extends DiffusionController { if ($err) { return new PhabricatorVCSResponse( 500, - pht('Error %d: %s', $err, $stderr)); + pht( + 'Error %d: %s', + $err, + phutil_utf8ize($stderr))); } return id(new DiffusionGitResponse())->setGitData($stdout); From 8269fd6e6c1f8f03f1645215c5748e794cc8486c Mon Sep 17 00:00:00 2001 From: epriestley Date: Sat, 30 Jan 2016 16:44:33 -0800 Subject: [PATCH 02/54] Decode "Content-Encoding: gzip" content Summary: Fixes T10228. When we receive a gzipped request (rare, but `git` may send them), decode it before providing it to the application. This fixes the issue with proxying certain requests described in T10228. Test Plan: - Applied this fix in production. - Cloned a problem repository cleanly. Reviewers: chad Reviewed By: chad Maniphest Tasks: T10228 Differential Revision: https://secure.phabricator.com/D15145 --- support/PhabricatorStartup.php | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/support/PhabricatorStartup.php b/support/PhabricatorStartup.php index 198cc1b244..bc7daf4ea6 100644 --- a/support/PhabricatorStartup.php +++ b/support/PhabricatorStartup.php @@ -129,7 +129,19 @@ final class PhabricatorStartup { self::beginOutputCapture(); - self::$rawInput = (string)file_get_contents('php://input'); + if (isset($_SERVER['HTTP_CONTENT_ENCODING'])) { + $encoding = trim($_SERVER['HTTP_CONTENT_ENCODING']); + } else { + $encoding = null; + } + + if ($encoding === 'gzip') { + $source_stream = 'compress.zlib://php://input'; + } else { + $source_stream = 'php://input'; + } + + self::$rawInput = (string)file_get_contents($source_stream); } From ce30f8c88bf6541ebbdfa1dd6920658a4cd0255f Mon Sep 17 00:00:00 2001 From: Chad Little Date: Sun, 31 Jan 2016 20:08:13 +0000 Subject: [PATCH 03/54] Make PHUITags not break/wrap Summary: Currently these break at the icon and any whitespace, instead force them to notwrap and stack the display if there are too many large tags. I think we're much more resilient CSS wise now I can't find any hairy edge cases. Test Plan: Try to break tags in 50/50 dashboard layouts like Wikimedia {F1082103} Reviewers: epriestley Reviewed By: epriestley Subscribers: Korvin Maniphest Tasks: T10245 Differential Revision: https://secure.phabricator.com/D15143 --- resources/celerity/map.php | 6 +++--- webroot/rsrc/css/phui/phui-tag-view.css | 3 ++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 56aee8abdc..02e5d3e5d8 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -7,7 +7,7 @@ */ return array( 'names' => array( - 'core.pkg.css' => '5e4df064', + 'core.pkg.css' => '28355cef', 'core.pkg.js' => 'a79eed25', 'darkconsole.pkg.js' => 'e7393ebb', 'differential.pkg.css' => '2de124c9', @@ -149,7 +149,7 @@ return array( 'rsrc/css/phui/phui-remarkup-preview.css' => '1a8f2591', 'rsrc/css/phui/phui-spacing.css' => '042804d6', 'rsrc/css/phui/phui-status.css' => '888cedb8', - 'rsrc/css/phui/phui-tag-view.css' => 'e60e227b', + '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', @@ -825,7 +825,7 @@ return array( 'phui-remarkup-preview-css' => '1a8f2591', 'phui-spacing-css' => '042804d6', 'phui-status-list-view-css' => '888cedb8', - 'phui-tag-view-css' => 'e60e227b', + 'phui-tag-view-css' => '9d5d4400', 'phui-theme-css' => 'ab7b848c', 'phui-timeline-view-css' => '2efceff8', 'phui-two-column-view-css' => 'c75bfc5b', diff --git a/webroot/rsrc/css/phui/phui-tag-view.css b/webroot/rsrc/css/phui/phui-tag-view.css index 4396fe8fe3..c6bb18ed0a 100644 --- a/webroot/rsrc/css/phui/phui-tag-view.css +++ b/webroot/rsrc/css/phui/phui-tag-view.css @@ -7,6 +7,7 @@ text-decoration: none; position: relative; -webkit-font-smoothing: antialiased; + white-space: nowrap; } a.phui-tag-view:hover { @@ -149,7 +150,7 @@ a.phui-tag-view:hover } .phui-object-item .phabricator-handle-tag-list-item { - display: inline; + display: inline-block; margin: 0 4px 2px 0; } From e2da57173405a384c2c32629959f9ba31f1943e8 Mon Sep 17 00:00:00 2001 From: Chad Little Date: Sun, 31 Jan 2016 20:09:06 +0000 Subject: [PATCH 04/54] Add additional icons for User Profiles Summary: Designer, Musician, Spy, Robot Test Plan: Click Choose Icon, see that I am a designer. Reviewers: epriestley Reviewed By: epriestley Subscribers: Korvin Differential Revision: https://secure.phabricator.com/D15147 --- .../people/icon/PhabricatorPeopleIconSet.php | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/applications/people/icon/PhabricatorPeopleIconSet.php b/src/applications/people/icon/PhabricatorPeopleIconSet.php index 06b97829ed..cb34dc4065 100644 --- a/src/applications/people/icon/PhabricatorPeopleIconSet.php +++ b/src/applications/people/icon/PhabricatorPeopleIconSet.php @@ -74,6 +74,26 @@ final class PhabricatorPeopleIconSet 'icon' => 'fa-heart', 'name' => pht('Resources'), ), + array( + 'key' => 'camera', + 'icon' => 'fa-camera-retro', + 'name' => pht('Design'), + ), + array( + 'key' => 'music', + 'icon' => 'fa-headphones', + 'name' => pht('Musician'), + ), + array( + 'key' => 'spy', + 'icon' => 'fa-user-secret', + 'name' => pht('Spy'), + ), + array( + 'key' => 'android', + 'icon' => 'fa-android', + 'name' => pht('Bot'), + ), array( 'key' => 'relationships', 'icon' => 'fa-glass', From 9a9bac0a02ba67924e740760a9874ea54aa6049b Mon Sep 17 00:00:00 2001 From: Chad Little Date: Sun, 31 Jan 2016 12:20:11 -0800 Subject: [PATCH 05/54] Tighten up spacing on feed stories Summary: Normalizes spacing a bit for better display on feed and on profiles/projects. Test Plan: Test layout on project, feed, profiles. Tablet, Mobile, Desktop Reviewers: epriestley Reviewed By: epriestley Subscribers: Korvin Differential Revision: https://secure.phabricator.com/D15149 --- resources/celerity/map.php | 10 +++++----- webroot/rsrc/css/application/project/project-view.css | 7 ++++++- webroot/rsrc/css/phui/phui-feed-story.css | 4 ++-- 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 02e5d3e5d8..002492c3ac 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -7,7 +7,7 @@ */ return array( 'names' => array( - 'core.pkg.css' => '28355cef', + 'core.pkg.css' => '0f87bfe0', 'core.pkg.js' => 'a79eed25', 'darkconsole.pkg.js' => 'e7393ebb', 'differential.pkg.css' => '2de124c9', @@ -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-view.css' => '22f7ed0e', + 'rsrc/css/application/project/project-view.css' => 'c6387c87', 'rsrc/css/application/releeph/releeph-core.css' => '9b3c5733', 'rsrc/css/application/releeph/releeph-preview-branch.css' => 'b7a6f4a5', 'rsrc/css/application/releeph/releeph-request-differential-create-dialog.css' => '8d8b92cd', @@ -129,7 +129,7 @@ return array( 'rsrc/css/phui/phui-document-pro.css' => '8799acf7', 'rsrc/css/phui/phui-document-summary.css' => '9ca48bdf', 'rsrc/css/phui/phui-document.css' => '9c71d2bf', - 'rsrc/css/phui/phui-feed-story.css' => 'b7b26d23', + 'rsrc/css/phui/phui-feed-story.css' => '04aec08f', 'rsrc/css/phui/phui-fontkit.css' => '9cda225e', 'rsrc/css/phui/phui-form-view.css' => '4a1a0f5e', 'rsrc/css/phui/phui-form.css' => '0b98e572', @@ -803,7 +803,7 @@ return array( 'phui-document-summary-view-css' => '9ca48bdf', 'phui-document-view-css' => '9c71d2bf', 'phui-document-view-pro-css' => '8799acf7', - 'phui-feed-story-css' => 'b7b26d23', + 'phui-feed-story-css' => '04aec08f', 'phui-font-icon-base-css' => 'ecbbb4c2', 'phui-fontkit-css' => '9cda225e', 'phui-form-css' => '0b98e572', @@ -842,7 +842,7 @@ return array( 'policy-edit-css' => '815c66f7', 'policy-transaction-detail-css' => '82100a43', 'ponder-view-css' => '7b0df4da', - 'project-view-css' => '22f7ed0e', + 'project-view-css' => 'c6387c87', 'raphael-core' => '51ee6b43', 'raphael-g' => '40dde778', 'raphael-g-line' => '40da039e', diff --git a/webroot/rsrc/css/application/project/project-view.css b/webroot/rsrc/css/application/project/project-view.css index 453942ecc4..ae1f7ec26f 100644 --- a/webroot/rsrc/css/application/project/project-view.css +++ b/webroot/rsrc/css/application/project/project-view.css @@ -31,7 +31,7 @@ padding: 8px; } -.project-view-feed .phui-header-shell { +.project-view-feed .phui-object-box .phui-header-shell { padding: 8px 4px; } @@ -40,6 +40,11 @@ margin-left: 4px; } +.device-desktop .project-view-feed .phui-feed-story, +.device-tablet .project-view-feed .phui-feed-story { + padding-left: 22px; +} + .project-view-home .phui-box-grey { padding: 0; } diff --git a/webroot/rsrc/css/phui/phui-feed-story.css b/webroot/rsrc/css/phui/phui-feed-story.css index 9162d68328..cc0bf279fd 100644 --- a/webroot/rsrc/css/phui/phui-feed-story.css +++ b/webroot/rsrc/css/phui/phui-feed-story.css @@ -24,7 +24,6 @@ padding: 12px 4px; overflow: hidden; color: {$greytext}; - line-height: 16px; word-break: break-word; } @@ -35,6 +34,7 @@ .phui-feed-story-body { margin: 4px 4px 8px; + padding-bottom: 8px; color: {$darkgreytext}; word-break: break-word; max-height: 300px; @@ -43,7 +43,7 @@ .phui-feed-story-foot { font-size: {$smallerfontsize}; - padding: 8px 4px 12px; + padding: 0 4px 12px; } .phui-feed-story-foot, From fcf03c2dbe91fb98b430e0639e55ce115ec263f9 Mon Sep 17 00:00:00 2001 From: Chad Little Date: Sun, 31 Jan 2016 12:38:11 -0800 Subject: [PATCH 06/54] Clean up CSS classes on phui-workcard Summary: Just a bit more consistent here. Test Plan: Pull up a workboard, see no changes Reviewers: epriestley Reviewed By: epriestley Subscribers: Korvin Differential Revision: https://secure.phabricator.com/D15148 --- resources/celerity/map.php | 4 +- .../css/phui/workboards/phui-workcard.css | 40 ++++++++++--------- 2 files changed, 24 insertions(+), 20 deletions(-) diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 002492c3ac..51c31e4b91 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -153,7 +153,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' => 'ffb55371', + 'rsrc/css/phui/workboards/phui-workcard.css' => '0dfd1880', 'rsrc/css/phui/workboards/phui-workpanel.css' => 'e9339dc3', 'rsrc/css/sprite-login.css' => '60e8560e', 'rsrc/css/sprite-menu.css' => '9dd65b92', @@ -830,7 +830,7 @@ return array( 'phui-timeline-view-css' => '2efceff8', 'phui-two-column-view-css' => 'c75bfc5b', 'phui-workboard-view-css' => 'b07a5524', - 'phui-workcard-view-css' => 'ffb55371', + 'phui-workcard-view-css' => '0dfd1880', 'phui-workpanel-view-css' => 'e9339dc3', 'phuix-action-list-view' => 'b5c256b8', 'phuix-action-view' => '8cf6d262', diff --git a/webroot/rsrc/css/phui/workboards/phui-workcard.css b/webroot/rsrc/css/phui/workboards/phui-workcard.css index 95930b0da1..c88d3df802 100644 --- a/webroot/rsrc/css/phui/workboards/phui-workcard.css +++ b/webroot/rsrc/css/phui/workboards/phui-workcard.css @@ -2,26 +2,26 @@ * @provides phui-workcard-view-css */ -.phui-workboard-view .phui-object-item { +.phui-workpanel-view .phui-object-item { background-color: #fff; border-radius: 3px; margin-bottom: 6px; } -.phui-workboard-view .phui-object-item-name { +.phui-workpanel-view .phui-object-item-name { padding-bottom: 4px; } -.phui-workboard-view .phui-object-item-content { +.phui-workpanel-view .phui-object-item-content { margin-top: 0; } -.phui-workboard-view .phui-object-item-frame { +.phui-workpanel-view .phui-object-item-frame { border-top-right-radius: 3px; border-bottom-right-radius: 3px; } -.phui-workboard-view .phui-object-item .phui-object-item-objname { +.phui-workpanel-view .phui-object-item .phui-object-item-objname { -webkit-touch-callout: text; -webkit-user-select: text; -khtml-user-select: text; @@ -30,20 +30,20 @@ user-select: text; } -.phui-workboard-view .phui-object-item-link { +.phui-workpanel-view .phui-object-item-link { white-space: normal; font-weight: normal; color: #000; margin-left: 2px; } -.device-desktop .phui-workboard-view .phui-object-item-with-1-actions +.device-desktop .phui-workpanel-view .phui-object-item-with-1-actions .phui-object-item-content-box { margin-right: 0; overflow: hidden; } -.phui-workboard-view .phui-object-item-objname { +.phui-workpanel-view .phui-object-item-objname { vertical-align: top; } @@ -84,43 +84,47 @@ /* - Draggable Colors --------------------------------------------------------*/ -.phui-workboard-view .phui-object-item.drag-dragging { +.phui-workpanel-view .phui-object-item.drag-dragging { box-shadow: {$dropshadow}; background-color: {$sh-greybackground}; } -.phui-workboard-view .drag-dragging.phui-object-item-bar-color-red { +.phui-workpanel-view .phui-object-item.drag-dragging .phui-list-item-href { + display: none; +} + +.phui-workpanel-view .drag-dragging.phui-object-item-bar-color-red { background-color: {$sh-redbackground}; } -.phui-workboard-view .drag-dragging.phui-object-item-bar-color-orange { +.phui-workpanel-view .drag-dragging.phui-object-item-bar-color-orange { background-color: {$sh-orangebackground}; } -.phui-workboard-view .drag-dragging.phui-object-item-bar-color-yellow { +.phui-workpanel-view .drag-dragging.phui-object-item-bar-color-yellow { background-color: {$sh-yellowbackground}; } -.phui-workboard-view .drag-dragging.phui-object-item-bar-color-green { +.phui-workpanel-view .drag-dragging.phui-object-item-bar-color-green { background-color: {$sh-greenbackground}; } -.phui-workboard-view .drag-dragging.phui-object-item-bar-color-blue { +.phui-workpanel-view .drag-dragging.phui-object-item-bar-color-blue { background-color: {$sh-bluebackground}; } -.phui-workboard-view .drag-dragging.phui-object-item-bar-color-indigo { +.phui-workpanel-view .drag-dragging.phui-object-item-bar-color-indigo { background-color: {$sh-indigobackground}; } -.phui-workboard-view .drag-dragging.phui-object-item-bar-color-violet { +.phui-workpanel-view .drag-dragging.phui-object-item-bar-color-violet { background-color: {$sh-violetbackground}; } -.phui-workboard-view .drag-dragging.phui-object-item-bar-color-pink { +.phui-workpanel-view .drag-dragging.phui-object-item-bar-color-pink { background-color: {$sh-pinkbackground}; } -.phui-workboard-view .drag-dragging.phui-object-item-bar-color-sky { +.phui-workpanel-view .drag-dragging.phui-object-item-bar-color-sky { background-color: {$sh-bluebackground}; } From 080d838c693b493844148325b6516f3f6982ad3c Mon Sep 17 00:00:00 2001 From: Chad Little Date: Fri, 29 Jan 2016 18:44:47 -0800 Subject: [PATCH 07/54] Add project tags to workboard cards Summary: Ref T4863. Add project tags to workboard cards. Test Plan: {F1053509} Reviewers: joshuaspence, #blessed_reviewers, epriestley Reviewed By: #blessed_reviewers, epriestley Subscribers: Luke081515.2, Korvin Maniphest Tasks: T4863 Differential Revision: https://secure.phabricator.com/D14935 --- .../maniphest/editor/ManiphestEditEngine.php | 2 ++ .../phid/view/PHUIHandleTagListView.php | 2 +- .../PhabricatorProjectBoardViewController.php | 1 + .../PhabricatorProjectMoveController.php | 6 ++++- .../project/view/ProjectBoardTaskCard.php | 24 ++++++++++++++++++- src/view/AphrontTagView.php | 4 ++++ 6 files changed, 36 insertions(+), 3 deletions(-) diff --git a/src/applications/maniphest/editor/ManiphestEditEngine.php b/src/applications/maniphest/editor/ManiphestEditEngine.php index b7e3cee965..058a8a98b6 100644 --- a/src/applications/maniphest/editor/ManiphestEditEngine.php +++ b/src/applications/maniphest/editor/ManiphestEditEngine.php @@ -294,6 +294,7 @@ final class ManiphestEditEngine $column_tasks = id(new ManiphestTaskQuery()) ->setViewer($viewer) ->withPHIDs($task_phids) + ->needProjectPHIDs(true) ->execute(); if ($order == PhabricatorProjectColumn::ORDER_NATURAL) { @@ -333,6 +334,7 @@ final class ManiphestEditEngine ->setViewer($viewer) ->setTask($task) ->setOwner($owner) + ->setProject($column->getProject()) ->setCanEdit(true) ->getItem(); diff --git a/src/applications/phid/view/PHUIHandleTagListView.php b/src/applications/phid/view/PHUIHandleTagListView.php index 73cbef2350..0bbfc4d249 100644 --- a/src/applications/phid/view/PHUIHandleTagListView.php +++ b/src/applications/phid/view/PHUIHandleTagListView.php @@ -9,7 +9,7 @@ final class PHUIHandleTagListView extends AphrontTagView { private $slim; private $showHovercards; - public function setHandles(array $handles) { + public function setHandles($handles) { $this->handles = $handles; return $this; } diff --git a/src/applications/project/controller/PhabricatorProjectBoardViewController.php b/src/applications/project/controller/PhabricatorProjectBoardViewController.php index 4d83ca6fa3..2c064eaf71 100644 --- a/src/applications/project/controller/PhabricatorProjectBoardViewController.php +++ b/src/applications/project/controller/PhabricatorProjectBoardViewController.php @@ -304,6 +304,7 @@ final class PhabricatorProjectBoardViewController $can_edit = idx($task_can_edit_map, $task->getPHID(), false); $cards->addItem(id(new ProjectBoardTaskCard()) ->setViewer($viewer) + ->setProject($project) ->setTask($task) ->setOwner($owner) ->setCanEdit($can_edit) diff --git a/src/applications/project/controller/PhabricatorProjectMoveController.php b/src/applications/project/controller/PhabricatorProjectMoveController.php index 1b2429917c..3fc72e7341 100644 --- a/src/applications/project/controller/PhabricatorProjectMoveController.php +++ b/src/applications/project/controller/PhabricatorProjectMoveController.php @@ -26,9 +26,10 @@ final class PhabricatorProjectMoveController return new Aphront404Response(); } - $object = id(new PhabricatorObjectQuery()) + $object = id(new ManiphestTaskQuery()) ->setViewer($viewer) ->withPHIDs(array($object_phid)) + ->needProjectPHIDs(true) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, @@ -95,6 +96,7 @@ final class PhabricatorProjectMoveController $tasks = id(new ManiphestTaskQuery()) ->setViewer($viewer) ->withPHIDs($task_phids) + ->needProjectPHIDs(true) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, @@ -149,11 +151,13 @@ final class PhabricatorProjectMoveController ->withPHIDs(array($object->getOwnerPHID())) ->executeOne(); } + $card = id(new ProjectBoardTaskCard()) ->setViewer($viewer) ->setTask($object) ->setOwner($owner) ->setCanEdit(true) + ->setProject($project) ->getItem(); return id(new AphrontAjaxResponse())->setContent( diff --git a/src/applications/project/view/ProjectBoardTaskCard.php b/src/applications/project/view/ProjectBoardTaskCard.php index 4fc26988d7..9ae3ef83c8 100644 --- a/src/applications/project/view/ProjectBoardTaskCard.php +++ b/src/applications/project/view/ProjectBoardTaskCard.php @@ -3,6 +3,7 @@ final class ProjectBoardTaskCard extends Phobject { private $viewer; + private $project; private $task; private $owner; private $canEdit; @@ -15,6 +16,14 @@ final class ProjectBoardTaskCard extends Phobject { return $this->viewer; } + public function setProject(PhabricatorProject $project) { + $this->project = $project; + return $this; + } + public function getProject() { + return $this->project; + } + public function setTask(ManiphestTask $task) { $this->task = $task; return $this; @@ -44,13 +53,14 @@ final class ProjectBoardTaskCard extends Phobject { $task = $this->getTask(); $owner = $this->getOwner(); $can_edit = $this->getCanEdit(); + $viewer = $this->getViewer(); $color_map = ManiphestTaskPriority::getColorMap(); $bar_color = idx($color_map, $task->getPriority(), 'grey'); $card = id(new PHUIObjectItemView()) ->setObject($task) - ->setUser($this->getViewer()) + ->setUser($viewer) ->setObjectName('T'.$task->getID()) ->setHeader($task->getTitle()) ->setGrippable($can_edit) @@ -73,6 +83,18 @@ final class ProjectBoardTaskCard extends Phobject { $card->addAttribute($owner->renderLink()); } + $project_phids = array_fuse($task->getProjectPHIDs()); + unset($project_phids[$this->project->getPHID()]); + + $handle_list = $viewer->loadHandles($project_phids); + $tag_list = id(new PHUIHandleTagListView()) + ->setSlim(true) + ->setHandles($handle_list); + + if (!$tag_list->isEmpty()) { + $card->addAttribute($tag_list); + } + return $card; } diff --git a/src/view/AphrontTagView.php b/src/view/AphrontTagView.php index a6eb722383..b82ebef9c3 100644 --- a/src/view/AphrontTagView.php +++ b/src/view/AphrontTagView.php @@ -77,6 +77,10 @@ abstract class AphrontTagView extends AphrontView { return $this->id; } + public function isEmpty() { + return empty($this->getTagContent()); + } + protected function getTagName() { return 'div'; } From 354858e434067efb5d8d1dd1cc01f4a2a4b756c4 Mon Sep 17 00:00:00 2001 From: epriestley Date: Sun, 31 Jan 2016 15:16:51 -0800 Subject: [PATCH 08/54] Disambiguate isEmpty() Summary: Fixes T10250. Rename the one I added to `hasAnyProperties()` for clarity. Test Plan: - Viewed a project profile with content. - Viewed a project profile with no properties. - Viewed a workboard with tasks that had a mixture of additional projects and no additional projects. Reviewers: chad Reviewed By: chad Maniphest Tasks: T10250 Differential Revision: https://secure.phabricator.com/D15151 --- .../PhabricatorPeopleProfileViewController.php | 2 +- .../PhabricatorProjectProfileController.php | 2 +- .../project/view/ProjectBoardTaskCard.php | 11 +++++------ src/view/AphrontTagView.php | 4 ---- src/view/phui/PHUIPropertyListView.php | 6 +++--- 5 files changed, 10 insertions(+), 15 deletions(-) diff --git a/src/applications/people/controller/PhabricatorPeopleProfileViewController.php b/src/applications/people/controller/PhabricatorPeopleProfileViewController.php index 1453ab7d71..cdbbddd8d8 100644 --- a/src/applications/people/controller/PhabricatorPeopleProfileViewController.php +++ b/src/applications/people/controller/PhabricatorPeopleProfileViewController.php @@ -112,7 +112,7 @@ final class PhabricatorPeopleProfileViewController PhabricatorCustomField::ROLE_VIEW); $field_list->appendFieldsToPropertyList($user, $viewer, $view); - if ($view->isEmpty()) { + if (!$view->hasAnyProperties()) { return null; } diff --git a/src/applications/project/controller/PhabricatorProjectProfileController.php b/src/applications/project/controller/PhabricatorProjectProfileController.php index c03b82b05d..74acd1edbf 100644 --- a/src/applications/project/controller/PhabricatorProjectProfileController.php +++ b/src/applications/project/controller/PhabricatorProjectProfileController.php @@ -125,7 +125,7 @@ final class PhabricatorProjectProfileController PhabricatorCustomField::ROLE_VIEW); $field_list->appendFieldsToPropertyList($project, $viewer, $view); - if ($view->isEmpty()) { + if (!$view->hasAnyProperties()) { return null; } diff --git a/src/applications/project/view/ProjectBoardTaskCard.php b/src/applications/project/view/ProjectBoardTaskCard.php index 9ae3ef83c8..86f4359c08 100644 --- a/src/applications/project/view/ProjectBoardTaskCard.php +++ b/src/applications/project/view/ProjectBoardTaskCard.php @@ -86,12 +86,11 @@ final class ProjectBoardTaskCard extends Phobject { $project_phids = array_fuse($task->getProjectPHIDs()); unset($project_phids[$this->project->getPHID()]); - $handle_list = $viewer->loadHandles($project_phids); - $tag_list = id(new PHUIHandleTagListView()) - ->setSlim(true) - ->setHandles($handle_list); - - if (!$tag_list->isEmpty()) { + if ($project_phids) { + $handle_list = $viewer->loadHandles($project_phids); + $tag_list = id(new PHUIHandleTagListView()) + ->setSlim(true) + ->setHandles($handle_list); $card->addAttribute($tag_list); } diff --git a/src/view/AphrontTagView.php b/src/view/AphrontTagView.php index b82ebef9c3..a6eb722383 100644 --- a/src/view/AphrontTagView.php +++ b/src/view/AphrontTagView.php @@ -77,10 +77,6 @@ abstract class AphrontTagView extends AphrontView { return $this->id; } - public function isEmpty() { - return empty($this->getTagContent()); - } - protected function getTagName() { return 'div'; } diff --git a/src/view/phui/PHUIPropertyListView.php b/src/view/phui/PHUIPropertyListView.php index 0d60ed25b0..336c494a3c 100644 --- a/src/view/phui/PHUIPropertyListView.php +++ b/src/view/phui/PHUIPropertyListView.php @@ -115,14 +115,14 @@ final class PHUIPropertyListView extends AphrontView { $this->invokedWillRenderEvent = true; } - public function isEmpty() { + public function hasAnyProperties() { $this->invokeWillRenderEvent(); if ($this->parts) { - return false; + return true; } - return true; + return false; } public function render() { From fc9db6e2a2ee929f56eb40530bb6f1fc1b75f563 Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 1 Feb 2016 07:04:19 -0800 Subject: [PATCH 09/54] Put subprojects and milestones back into the Project UI Summary: Ref T10010. Restores subprojects and milestones to the UI with a more modern style and more warnings. Test Plan: {F1085207} {F1085208} Reviewers: chad Reviewed By: chad Maniphest Tasks: T10010 Differential Revision: https://secure.phabricator.com/D15152 --- src/__phutil_library_map__.php | 6 +- .../PhabricatorProjectApplication.php | 2 + .../PhabricatorProjectEditController.php | 16 +- ...PhabricatorProjectMilestonesController.php | 92 ------- .../PhabricatorProjectProfileController.php | 90 +++++++ ...atorProjectSubprojectWarningController.php | 51 ++++ ...habricatorProjectSubprojectsController.php | 230 +++++++++++++++--- .../PhabricatorProjectProfilePanelEngine.php | 4 + ...ProjectsMembershipIndexEngineExtension.php | 17 ++ ...bricatorProjectSubprojectsProfilePanel.php | 63 +++++ src/docs/user/userguide/projects.diviner | 112 +++++++++ 11 files changed, 542 insertions(+), 141 deletions(-) delete mode 100644 src/applications/project/controller/PhabricatorProjectMilestonesController.php create mode 100644 src/applications/project/controller/PhabricatorProjectSubprojectWarningController.php create mode 100644 src/applications/project/profilepanel/PhabricatorProjectSubprojectsProfilePanel.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 6339e6c564..b986242b04 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -2913,7 +2913,6 @@ phutil_register_library_map(array( 'PhabricatorProjectMembersProfilePanel' => 'applications/project/profilepanel/PhabricatorProjectMembersProfilePanel.php', 'PhabricatorProjectMembersRemoveController' => 'applications/project/controller/PhabricatorProjectMembersRemoveController.php', 'PhabricatorProjectMembersViewController' => 'applications/project/controller/PhabricatorProjectMembersViewController.php', - 'PhabricatorProjectMilestonesController' => 'applications/project/controller/PhabricatorProjectMilestonesController.php', 'PhabricatorProjectMoveController' => 'applications/project/controller/PhabricatorProjectMoveController.php', 'PhabricatorProjectNameContextFreeGrammar' => 'applications/project/lipsum/PhabricatorProjectNameContextFreeGrammar.php', 'PhabricatorProjectNoProjectsDatasource' => 'applications/project/typeahead/PhabricatorProjectNoProjectsDatasource.php', @@ -2937,7 +2936,9 @@ phutil_register_library_map(array( 'PhabricatorProjectSlug' => 'applications/project/storage/PhabricatorProjectSlug.php', 'PhabricatorProjectStandardCustomField' => 'applications/project/customfield/PhabricatorProjectStandardCustomField.php', 'PhabricatorProjectStatus' => 'applications/project/constants/PhabricatorProjectStatus.php', + 'PhabricatorProjectSubprojectWarningController' => 'applications/project/controller/PhabricatorProjectSubprojectWarningController.php', 'PhabricatorProjectSubprojectsController' => 'applications/project/controller/PhabricatorProjectSubprojectsController.php', + 'PhabricatorProjectSubprojectsProfilePanel' => 'applications/project/profilepanel/PhabricatorProjectSubprojectsProfilePanel.php', 'PhabricatorProjectTestDataGenerator' => 'applications/project/lipsum/PhabricatorProjectTestDataGenerator.php', 'PhabricatorProjectTransaction' => 'applications/project/storage/PhabricatorProjectTransaction.php', 'PhabricatorProjectTransactionEditor' => 'applications/project/editor/PhabricatorProjectTransactionEditor.php', @@ -7330,7 +7331,6 @@ phutil_register_library_map(array( 'PhabricatorProjectMembersProfilePanel' => 'PhabricatorProfilePanel', 'PhabricatorProjectMembersRemoveController' => 'PhabricatorProjectController', 'PhabricatorProjectMembersViewController' => 'PhabricatorProjectController', - 'PhabricatorProjectMilestonesController' => 'PhabricatorProjectController', 'PhabricatorProjectMoveController' => 'PhabricatorProjectController', 'PhabricatorProjectNameContextFreeGrammar' => 'PhutilContextFreeGrammar', 'PhabricatorProjectNoProjectsDatasource' => 'PhabricatorTypeaheadDatasource', @@ -7357,7 +7357,9 @@ phutil_register_library_map(array( 'PhabricatorStandardCustomFieldInterface', ), 'PhabricatorProjectStatus' => 'Phobject', + 'PhabricatorProjectSubprojectWarningController' => 'PhabricatorProjectController', 'PhabricatorProjectSubprojectsController' => 'PhabricatorProjectController', + 'PhabricatorProjectSubprojectsProfilePanel' => 'PhabricatorProfilePanel', 'PhabricatorProjectTestDataGenerator' => 'PhabricatorTestDataGenerator', 'PhabricatorProjectTransaction' => 'PhabricatorApplicationTransaction', 'PhabricatorProjectTransactionEditor' => 'PhabricatorApplicationTransactionEditor', diff --git a/src/applications/project/application/PhabricatorProjectApplication.php b/src/applications/project/application/PhabricatorProjectApplication.php index 59904cb06b..5f3cb9e090 100644 --- a/src/applications/project/application/PhabricatorProjectApplication.php +++ b/src/applications/project/application/PhabricatorProjectApplication.php @@ -91,6 +91,8 @@ final class PhabricatorProjectApplication extends PhabricatorApplication { => 'PhabricatorProjectWatchController', 'silence/(?P[1-9]\d*)/' => 'PhabricatorProjectSilenceController', + 'warning/(?P[1-9]\d*)/' + => 'PhabricatorProjectSubprojectWarningController', ), '/tag/' => array( '(?P[^/]+)/' => 'PhabricatorProjectViewController', diff --git a/src/applications/project/controller/PhabricatorProjectEditController.php b/src/applications/project/controller/PhabricatorProjectEditController.php index d87910462c..5091135bec 100644 --- a/src/applications/project/controller/PhabricatorProjectEditController.php +++ b/src/applications/project/controller/PhabricatorProjectEditController.php @@ -42,6 +42,7 @@ final class PhabricatorProjectEditController if ($parent_id) { $query = id(new PhabricatorProjectQuery()) ->setViewer($viewer) + ->needImages(true) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, @@ -58,7 +59,7 @@ final class PhabricatorProjectEditController if ($is_milestone) { if (!$parent->supportsMilestones()) { - $cancel_uri = "/project/milestones/{$parent_id}/"; + $cancel_uri = "/project/subprojects/{$parent_id}/"; return $this->newDialog() ->setTitle(pht('No Milestones')) ->appendParagraph( @@ -91,20 +92,13 @@ final class PhabricatorProjectEditController $engine = $this->getEngine(); if ($engine) { $parent = $engine->getParentProject(); - if ($parent) { - $id = $parent->getID(); + $milestone = $engine->getMilestoneProject(); + if ($parent || $milestone) { + $id = nonempty($parent, $milestone)->getID(); $crumbs->addTextCrumb( pht('Subprojects'), $this->getApplicationURI("subprojects/{$id}/")); } - - $milestone = $engine->getMilestoneProject(); - if ($milestone) { - $id = $milestone->getID(); - $crumbs->addTextCrumb( - pht('Milestones'), - $this->getApplicationURI("milestones/{$id}/")); - } } return $crumbs; diff --git a/src/applications/project/controller/PhabricatorProjectMilestonesController.php b/src/applications/project/controller/PhabricatorProjectMilestonesController.php deleted file mode 100644 index ebbe5bc2e3..0000000000 --- a/src/applications/project/controller/PhabricatorProjectMilestonesController.php +++ /dev/null @@ -1,92 +0,0 @@ -getViewer(); - - $response = $this->loadProject(); - if ($response) { - return $response; - } - - $project = $this->getProject(); - $id = $project->getID(); - - $can_edit = PhabricatorPolicyFilter::hasCapability( - $viewer, - $project, - PhabricatorPolicyCapability::CAN_EDIT); - - $has_support = $project->supportsMilestones(); - if ($has_support) { - $milestones = id(new PhabricatorProjectQuery()) - ->setViewer($viewer) - ->withParentProjectPHIDs(array($project->getPHID())) - ->needImages(true) - ->withIsMilestone(true) - ->setOrder('newest') - ->execute(); - } else { - $milestones = array(); - } - - $can_create = $can_edit && $has_support; - - if ($project->getHasMilestones()) { - $button_text = pht('Create Next Milestone'); - } else { - $button_text = pht('Add Milestones'); - } - - $header = id(new PHUIHeaderView()) - ->setHeader(pht('Milestones')) - ->addActionLink( - id(new PHUIButtonView()) - ->setTag('a') - ->setHref("/project/edit/?milestone={$id}") - ->setIcon('fa-plus') - ->setDisabled(!$can_create) - ->setWorkflow(!$can_create) - ->setText($button_text)); - - $box = id(new PHUIObjectBoxView()) - ->setHeader($header); - - if (!$has_support) { - $no_support = pht( - 'This project is a milestone. Milestones can not have their own '. - 'milestones.'); - - $info_view = id(new PHUIInfoView()) - ->setErrors(array($no_support)) - ->setSeverity(PHUIInfoView::SEVERITY_WARNING); - - $box->setInfoView($info_view); - } - - $box->setObjectList( - id(new PhabricatorProjectListView()) - ->setUser($viewer) - ->setProjects($milestones) - ->renderList()); - - $nav = $this->getProfileMenu(); - $nav->selectFilter(PhabricatorProject::PANEL_MILESTONES); - - $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addTextCrumb(pht('Milestones')); - - return $this->newPage() - ->setNavigation($nav) - ->setCrumbs($crumbs) - ->setTitle(array($project->getName(), pht('Milestones'))) - ->appendChild($box); - } - -} diff --git a/src/applications/project/controller/PhabricatorProjectProfileController.php b/src/applications/project/controller/PhabricatorProjectProfileController.php index 74acd1edbf..146c1da96f 100644 --- a/src/applications/project/controller/PhabricatorProjectProfileController.php +++ b/src/applications/project/controller/PhabricatorProjectProfileController.php @@ -45,6 +45,9 @@ final class PhabricatorProjectProfileController $watch_action = $this->renderWatchAction($project); $header->addActionLink($watch_action); + $milestone_list = $this->buildMilestoneList($project); + $subproject_list = $this->buildSubprojectList($project); + $member_list = id(new PhabricatorProjectMemberListView()) ->setUser($viewer) ->setProject($project) @@ -82,6 +85,8 @@ final class PhabricatorProjectProfileController )) ->setSideColumn( array( + $milestone_list, + $subproject_list, $member_list, $watcher_list, )); @@ -176,5 +181,90 @@ final class PhabricatorProjectProfileController ->setHref($watch_href); } + private function buildMilestoneList(PhabricatorProject $project) { + if (!$project->getHasMilestones()) { + return null; + } + + $viewer = $this->getViewer(); + $id = $project->getID(); + + $milestones = id(new PhabricatorProjectQuery()) + ->setViewer($viewer) + ->withParentProjectPHIDs(array($project->getPHID())) + ->needImages(true) + ->withIsMilestone(true) + ->setOrder('newest') + ->execute(); + if (!$milestones) { + return null; + } + + $milestone_list = id(new PhabricatorProjectListView()) + ->setUser($viewer) + ->setProjects($milestones) + ->renderList(); + + $view_all = id(new PHUIButtonView()) + ->setTag('a') + ->setIcon( + id(new PHUIIconView()) + ->setIcon('fa-list-ul')) + ->setText(pht('View All')) + ->setHref("/project/subprojects/{$id}/"); + + $header = id(new PHUIHeaderView()) + ->setHeader(pht('Milestones')) + ->addActionLink($view_all); + + return id(new PHUIObjectBoxView()) + ->setHeader($header) + ->setBackground(PHUIBoxView::GREY) + ->setObjectList($milestone_list); + } + + private function buildSubprojectList(PhabricatorProject $project) { + if (!$project->getHasSubprojects()) { + return null; + } + + $viewer = $this->getViewer(); + $id = $project->getID(); + + $limit = 25; + + $subprojects = id(new PhabricatorProjectQuery()) + ->setViewer($viewer) + ->withParentProjectPHIDs(array($project->getPHID())) + ->needImages(true) + ->withIsMilestone(false) + ->setLimit($limit) + ->execute(); + if (!$subprojects) { + return null; + } + + $subproject_list = id(new PhabricatorProjectListView()) + ->setUser($viewer) + ->setProjects($subprojects) + ->renderList(); + + $view_all = id(new PHUIButtonView()) + ->setTag('a') + ->setIcon( + id(new PHUIIconView()) + ->setIcon('fa-list-ul')) + ->setText(pht('View All')) + ->setHref("/project/subprojects/{$id}/"); + + $header = id(new PHUIHeaderView()) + ->setHeader(pht('Subprojects')) + ->addActionLink($view_all); + + return id(new PHUIObjectBoxView()) + ->setHeader($header) + ->setBackground(PHUIBoxView::GREY) + ->setObjectList($subproject_list); + } } diff --git a/src/applications/project/controller/PhabricatorProjectSubprojectWarningController.php b/src/applications/project/controller/PhabricatorProjectSubprojectWarningController.php new file mode 100644 index 0000000000..9e089f138c --- /dev/null +++ b/src/applications/project/controller/PhabricatorProjectSubprojectWarningController.php @@ -0,0 +1,51 @@ +getViewer(); + + $response = $this->loadProject(); + if ($response) { + return $response; + } + + $project = $this->getProject(); + + $can_edit = PhabricatorPolicyFilter::hasCapability( + $viewer, + $project, + PhabricatorPolicyCapability::CAN_EDIT); + + if (!$can_edit) { + return new Aphront404Response(); + } + + $id = $project->getID(); + $cancel_uri = "/project/subprojects/{$id}/"; + $done_uri = "/project/edit/?parent={$id}"; + + if ($request->isFormPost()) { + return id(new AphrontRedirectResponse()) + ->setURI($done_uri); + } + + $doc_href = PhabricatorEnv::getDoclink('Projects User Guide'); + + $conversion_help = pht( + "Creating a project's first subproject **moves all ". + "members** and **destroys all workboard columns**.". + "\n\n". + "See [[ %s | Projects User Guide ]] in the documentation for details. ". + "This process can not be undone.", + $doc_href); + + return $this->newDialog() + ->setTitle(pht('Convert to Parent Project')) + ->appendChild(new PHUIRemarkupView($viewer, $conversion_help)) + ->addCancelButton($cancel_uri) + ->addSubmitButton(pht('Convert Project')); + } + +} diff --git a/src/applications/project/controller/PhabricatorProjectSubprojectsController.php b/src/applications/project/controller/PhabricatorProjectSubprojectsController.php index a88bf7d07c..afafed77b1 100644 --- a/src/applications/project/controller/PhabricatorProjectSubprojectsController.php +++ b/src/applications/project/controller/PhabricatorProjectSubprojectsController.php @@ -23,9 +23,10 @@ final class PhabricatorProjectSubprojectsController $project, PhabricatorPolicyCapability::CAN_EDIT); - $has_support = $project->supportsSubprojects(); + $allows_subprojects = $project->supportsSubprojects(); + $allows_milestones = $project->supportsMilestones(); - if ($has_support) { + if ($allows_subprojects) { $subprojects = id(new PhabricatorProjectQuery()) ->setViewer($viewer) ->withParentProjectPHIDs(array($project->getPHID())) @@ -36,44 +37,57 @@ final class PhabricatorProjectSubprojectsController $subprojects = array(); } - $can_create = $can_edit && $has_support; - - if ($project->getHasSubprojects()) { - $button_text = pht('Create Subproject'); + if ($allows_milestones) { + $milestones = id(new PhabricatorProjectQuery()) + ->setViewer($viewer) + ->withParentProjectPHIDs(array($project->getPHID())) + ->needImages(true) + ->withIsMilestone(true) + ->setOrder('newest') + ->execute(); } else { - $button_text = pht('Add Subprojects'); + $milestones = array(); } - $header = id(new PHUIHeaderView()) - ->setHeader(pht('Subprojects')) - ->addActionLink( - id(new PHUIButtonView()) - ->setTag('a') - ->setHref("/project/edit/?parent={$id}") - ->setIcon('fa-plus') - ->setDisabled(!$can_create) - ->setWorkflow(!$can_create) - ->setText($button_text)); - - $box = id(new PHUIObjectBoxView()) - ->setHeader($header); - - if (!$has_support) { - $no_support = pht( - 'This project is a milestone. Milestones can not have subprojects.'); - - $info_view = id(new PHUIInfoView()) - ->setErrors(array($no_support)) - ->setSeverity(PHUIInfoView::SEVERITY_WARNING); - - $box->setInfoView($info_view); + if ($milestones) { + $milestone_list = id(new PHUIObjectBoxView()) + ->setHeaderText(pht('Milestones')) + ->setObjectList( + id(new PhabricatorProjectListView()) + ->setUser($viewer) + ->setProjects($milestones) + ->renderList()); + } else { + $milestone_list = null; } - $box->setObjectList( - id(new PhabricatorProjectListView()) - ->setUser($viewer) - ->setProjects($subprojects) - ->renderList()); + if ($subprojects) { + $subproject_list = id(new PHUIObjectBoxView()) + ->setHeaderText(pht('Subprojects')) + ->setObjectList( + id(new PhabricatorProjectListView()) + ->setUser($viewer) + ->setProjects($subprojects) + ->renderList()); + } else { + $subproject_list = null; + } + + $property_list = $this->buildPropertyList( + $project, + $milestones, + $subprojects); + + $action_list = $this->buildActionList( + $project, + $milestones, + $subprojects); + + $property_list->setActionList($action_list); + + $header_box = id(new PHUIObjectBoxView()) + ->setHeaderText(pht('Subprojects and Milestones')) + ->addPropertyList($property_list); $nav = $this->getProfileMenu(); $nav->selectFilter(PhabricatorProject::PANEL_SUBPROJECTS); @@ -85,7 +99,151 @@ final class PhabricatorProjectSubprojectsController ->setNavigation($nav) ->setCrumbs($crumbs) ->setTitle(array($project->getName(), pht('Subprojects'))) - ->appendChild($box); + ->appendChild( + array( + $header_box, + $milestone_list, + $subproject_list, + )); } + private function buildPropertyList( + PhabricatorProject $project, + array $milestones, + array $subprojects) { + $viewer = $this->getViewer(); + + $view = id(new PHUIPropertyListView()) + ->setUser($viewer); + + $view->addProperty( + pht('Prototype'), + $this->renderStatus( + 'fa-exclamation-triangle red', + pht('Warning'), + pht('Subprojects and milestones are only partially implemented.'))); + + if (!$project->supportsMilestones()) { + $milestone_status = $this->renderStatus( + 'fa-times grey', + pht('Already Milestone'), + pht( + 'This project is already a milestone, and milestones may not '. + 'have their own milestones.')); + } else { + if (!$milestones) { + $milestone_status = $this->renderStatus( + 'fa-check grey', + pht('None Created'), + pht( + 'You can create milestones for this project.')); + } else { + $milestone_status = $this->renderStatus( + 'fa-check green', + pht('Has Milestones'), + pht('This project has milestones.')); + } + } + + $view->addProperty(pht('Milestones'), $milestone_status); + + if (!$project->supportsSubprojects()) { + $subproject_status = $this->renderStatus( + 'fa-times grey', + pht('Milestone'), + pht( + 'This project is a milestone, and milestones may not have '. + 'subprojects.')); + } else { + if (!$subprojects) { + $subproject_status = $this->renderStatus( + 'fa-check grey', + pht('None Created'), + pht('You can create subprojects for this project.')); + } else { + $subproject_status = $this->renderStatus( + 'fa-check green', + pht('Has Subprojects'), + pht( + 'This project has subprojects.')); + } + } + + $view->addProperty(pht('Subprojects'), $subproject_status); + + return $view; + } + + private function buildActionList( + PhabricatorProject $project, + array $milestones, + array $subprojects) { + $viewer = $this->getViewer(); + $id = $project->getID(); + + $can_edit = PhabricatorPolicyFilter::hasCapability( + $viewer, + $project, + PhabricatorPolicyCapability::CAN_EDIT); + + $allows_milestones = $project->supportsMilestones(); + $allows_subprojects = $project->supportsSubprojects(); + + $view = id(new PhabricatorActionListView()) + ->setUser($viewer); + + if ($allows_milestones && $milestones) { + $milestone_text = pht('Create Next Milestone'); + } else { + $milestone_text = pht('Create Milestone'); + } + + $can_milestone = ($can_edit && $allows_milestones); + $milestone_href = "/project/edit/?milestone={$id}"; + + $view->addAction( + id(new PhabricatorActionView()) + ->setName($milestone_text) + ->setIcon('fa-plus') + ->setHref($milestone_href) + ->setDisabled(!$can_milestone) + ->setWorkflow(!$can_milestone)); + + $can_subproject = ($can_edit && $allows_subprojects); + + // If we're offering to create the first subproject, we're going to warn + // the user about the effects before moving forward. + if ($can_subproject && !$subprojects) { + $subproject_href = "/project/warning/{$id}/"; + $subproject_disabled = false; + $subproject_workflow = true; + } else { + $subproject_href = "/project/edit/?parent={$id}"; + $subproject_disabled = !$can_subproject; + $subproject_workflow = !$can_subproject; + } + + $view->addAction( + id(new PhabricatorActionView()) + ->setName(pht('Create Subproject')) + ->setIcon('fa-plus') + ->setHref($subproject_href) + ->setDisabled($subproject_disabled) + ->setWorkflow($subproject_workflow)); + + return $view; + } + + private function renderStatus($icon, $target, $note) { + $item = id(new PHUIStatusItemView()) + ->setIcon($icon) + ->setTarget(phutil_tag('strong', array(), $target)) + ->setNote($note); + + return id(new PHUIStatusListView()) + ->addItem($item); + } + + + } diff --git a/src/applications/project/engine/PhabricatorProjectProfilePanelEngine.php b/src/applications/project/engine/PhabricatorProjectProfilePanelEngine.php index 612022d380..58b9b0b1ab 100644 --- a/src/applications/project/engine/PhabricatorProjectProfilePanelEngine.php +++ b/src/applications/project/engine/PhabricatorProjectProfilePanelEngine.php @@ -28,6 +28,10 @@ final class PhabricatorProjectProfilePanelEngine ->setBuiltinKey(PhabricatorProject::PANEL_MEMBERS) ->setPanelKey(PhabricatorProjectMembersProfilePanel::PANELKEY); + $panels[] = $this->newPanel() + ->setBuiltinKey(PhabricatorProject::PANEL_SUBPROJECTS) + ->setPanelKey(PhabricatorProjectSubprojectsProfilePanel::PANELKEY); + $panels[] = $this->newPanel() ->setBuiltinKey(PhabricatorProject::PANEL_MANAGE) ->setPanelKey(PhabricatorProjectManageProfilePanel::PANELKEY); diff --git a/src/applications/project/engineextension/PhabricatorProjectsMembershipIndexEngineExtension.php b/src/applications/project/engineextension/PhabricatorProjectsMembershipIndexEngineExtension.php index e1460381ed..567f5b749e 100644 --- a/src/applications/project/engineextension/PhabricatorProjectsMembershipIndexEngineExtension.php +++ b/src/applications/project/engineextension/PhabricatorProjectsMembershipIndexEngineExtension.php @@ -61,6 +61,15 @@ final class PhabricatorProjectsMembershipIndexEngineExtension $conn_w = $project->establishConnection('w'); + $any_milestone = queryfx_one( + $conn_w, + 'SELECT id FROM %T + WHERE parentProjectPHID = %s AND milestoneNumber IS NOT NULL + LIMIT 1', + $project->getTableName(), + $project_phid); + $has_milestones = (bool)$any_milestone; + $project->openTransaction(); // Delete any existing materialized member edges. @@ -92,6 +101,14 @@ final class PhabricatorProjectsMembershipIndexEngineExtension (int)$has_subprojects, $project->getID()); + // Update the hasMilestones flag. + queryfx( + $conn_w, + 'UPDATE %T SET hasMilestones = %d WHERE id = %d', + $project->getTableName(), + (int)$has_milestones, + $project->getID()); + $project->saveTransaction(); } diff --git a/src/applications/project/profilepanel/PhabricatorProjectSubprojectsProfilePanel.php b/src/applications/project/profilepanel/PhabricatorProjectSubprojectsProfilePanel.php new file mode 100644 index 0000000000..fad6378d69 --- /dev/null +++ b/src/applications/project/profilepanel/PhabricatorProjectSubprojectsProfilePanel.php @@ -0,0 +1,63 @@ +getPanelProperty('name'); + + if (strlen($name)) { + return $name; + } + + return $this->getDefaultName(); + } + + public function buildEditEngineFields( + PhabricatorProfilePanelConfiguration $config) { + return array( + id(new PhabricatorTextEditField()) + ->setKey('name') + ->setLabel(pht('Name')) + ->setPlaceholder($this->getDefaultName()) + ->setValue($config->getPanelProperty('name')), + ); + } + + protected function newNavigationMenuItems( + PhabricatorProfilePanelConfiguration $config) { + + $project = $config->getProfileObject(); + + $has_children = ($project->getHasSubprojects()) || + ($project->getHasMilestones()); + + $id = $project->getID(); + + $name = $this->getDisplayName($config); + $icon = 'fa-sitemap'; + $href = "/project/subprojects/{$id}/"; + + $item = $this->newItem() + ->setHref($href) + ->setName($name) + ->setDisabled(!$has_children) + ->setIcon($icon); + + return array( + $item, + ); + } + +} diff --git a/src/docs/user/userguide/projects.diviner b/src/docs/user/userguide/projects.diviner index f563d527f2..b1456a9e77 100644 --- a/src/docs/user/userguide/projects.diviner +++ b/src/docs/user/userguide/projects.diviner @@ -135,6 +135,118 @@ members or won't have a workboard, you can hide these items to streamline the menu. +Subprojects and Milestones +========================== + +IMPORTANT: This feature is only partially implemented. + +After creating a project, you can use the +{nav icon="sitemap", name="Subprojects"} menu item to add subprojects or +milestones. + +**Subprojects** are projects that are contained inside the main project. You +can use them to break large or complex groups, tags, lists, or undertakings +apart into smaller pieces. + +**Milestones** are a special kind of subproject for organizing tasks into +blocks of work. You can use them to implement sprints, iterations, milestones, +versions, etc. + +Subprojects and milestones have some additional special behaviors and rules, +particularly around policies and membership. See below for details. + +This is a brief summary of the major differences between normal projects, +subprojects, parent projects, and milestones. + +| | Normal | Parent | Subproject | Milestone | +|---|---|---|---|---| +| //Members// | Yes | Union of Subprojects | Yes | Same as Parent | +| //Policies// | Yes | Yes | Affected by Parent | Same as Parent | +| //Workboard// | Yes | No Custom Columns | Yes | Yes | +| //Hashtags// | Yes | Yes | Yes | Special | + + +Subprojects +=========== + +Subprojects are full-power projects that are contained inside some parent +project. You can use them to divide a large or complex project into smaller +parts. + +Subprojects have normal members and normal policies, but note that the policies +of the parent project affect the policies of the subproject (see "Parent +Projects", below). + +Subprojects can have their own subprojects, milestones, or both. If a +subproject has its own subprojects, it is both a subproject and a parent +project. Thus, the parent project rules apply to it, and are stronger than the +subproject rules. + +Subprojects can have normal workboards. + + +Milestones +========== + +Milestones are simple subprojects for tracking sprints, iterations, versions, +or other similar blocks of work. Milestones make it easier to create and manage +a large number of similar subprojects (for example: {nav Sprint 1}, +{nav Sprint 2}, {nav Sprint 3}, etc). + +Milestones can not have direct members or policies. Instead, the membership +and policies of a milestones are always the same as the milestone's parent +project. This makes large numbers of milestones more manageable when changes +occur. + +Milestones can not have subprojects, and can not have their own milestones. + +By default, Milestones do not have their own hashtags. + +Milestones can have normal workboards. + + +Parent Projects +=============== + +When you add the first subproject to an existing project, it is converted into +a **parent project**. Parent projects have some special rules. + +**No Direct Members**: Parent projects can not have members of their own. +Instead, all of the users who are members of any subproject count as members +of the parent project. By joining (or leaving) a subproject, a user is +implicitly added to (or removed from) all ancestors of that project. + +Consequently, when you add the first subproject to an existing project, all of +the project's current members are moved to become members of the subproject +instead. Implicitly, they will remain members of the parent project because the +parent project is an ancestor of the new subproject. + +You can edit the project afterward to change or remove members if you want to +split membership apart in a more granular way across multiple new subprojects. + +**No Workboard Columns**: Parent projects can not have their own workboard +columns: instead, the workboard of a parent project shows columns representing +the child projects. + +Thus, a project's workboard columns are destroyed when you add the first +subproject. All objects on the workboard will be returned to the project's +backlog. The new board will show columns for subprojects instead. + +**Searching**: When you search for a parent project, results for any subproject +are returned. For example, if you search for {nav Engineering}, your query will +match results in {nav Engineering} itself, but also subprojects like +{nav Engineering > Warp Drive} and {nav Engineering > Shield Batteries}. + +**Policy Effects**: To view a subproject or milestone, you must be able to +view the parent project. As a result, the parent project's view policy now +affects child projects. If you restrict the visibility of the parent, you also +restrict the visibility of the children. + +In contrast, permission to edit a parent project grants permission to edit +any subproject. If a user can {nav Root Project}, they can also edit +{nav Root Project > Child} and {nav Root Project > Child > Sprint 3}. + + Policies In Depth ================= From 08e7b6f79f6f71a6de09726e7d29aa31edd6639c Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 1 Feb 2016 09:37:40 -0800 Subject: [PATCH 10/54] Fix object extraction from user profile blurbs Summary: Fixes T10242. Currently, we don't extract files, mentions, etc., properly from user profile blurbs. Test Plan: Uploaded a file to my profile blurb, saw it attach properly. Reviewers: chad Reviewed By: chad Maniphest Tasks: T10242 Differential Revision: https://secure.phabricator.com/D15153 --- .../customfield/PhabricatorUserBlurbField.php | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/applications/people/customfield/PhabricatorUserBlurbField.php b/src/applications/people/customfield/PhabricatorUserBlurbField.php index 3221a33202..04fb419918 100644 --- a/src/applications/people/customfield/PhabricatorUserBlurbField.php +++ b/src/applications/people/customfield/PhabricatorUserBlurbField.php @@ -58,6 +58,13 @@ final class PhabricatorUserBlurbField ->setLabel($this->getFieldName()); } + public function getApplicationTransactionRemarkupBlocks( + PhabricatorApplicationTransaction $xaction) { + return array( + $xaction->getNewValue(), + ); + } + public function renderPropertyViewLabel() { return null; } @@ -67,10 +74,11 @@ final class PhabricatorUserBlurbField if (!strlen($blurb)) { return null; } - return PhabricatorMarkupEngine::renderOneObject( - id(new PhabricatorMarkupOneOff())->setContent($blurb), - 'default', - $this->getViewer()); + + $viewer = $this->getViewer(); + $view = new PHUIRemarkupView($viewer, $blurb); + + return $view; } public function getStyleForPropertyView() { From 18f34fab73972bbc6811e72d52f55b926c9531f4 Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 1 Feb 2016 09:44:55 -0800 Subject: [PATCH 11/54] Always give users "fa-user" icons in tokenizers Summary: Fixes T10247. The flavor icons are unhelpful/confusing in these contexts; show a boringer icon instead. Test Plan: Used tokenizer to select user with custom profile icon. Reloaded page. Saw boringer icon in both cases. Reviewers: chad Reviewed By: chad Maniphest Tasks: T10247 Differential Revision: https://secure.phabricator.com/D15154 --- .../people/phid/PhabricatorPeopleUserPHIDType.php | 6 ++++-- src/applications/phid/PhabricatorObjectHandle.php | 14 ++++++++++++++ .../view/PhabricatorTypeaheadTokenView.php | 2 +- 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/src/applications/people/phid/PhabricatorPeopleUserPHIDType.php b/src/applications/people/phid/PhabricatorPeopleUserPHIDType.php index ad452d5f02..40159d1cc2 100644 --- a/src/applications/people/phid/PhabricatorPeopleUserPHIDType.php +++ b/src/applications/people/phid/PhabricatorPeopleUserPHIDType.php @@ -54,8 +54,10 @@ final class PhabricatorPeopleUserPHIDType extends PhabricatorPHIDType { $icon_icon = PhabricatorPeopleIconSet::getIconIcon($icon_key); $subtitle = $profile->getDisplayTitle(); - $handle->setIcon($icon_icon); - $handle->setSubtitle($subtitle); + $handle + ->setIcon($icon_icon) + ->setSubtitle($subtitle) + ->setTokenIcon('fa-user'); } $availability = null; diff --git a/src/applications/phid/PhabricatorObjectHandle.php b/src/applications/phid/PhabricatorObjectHandle.php index e49094e311..c831005733 100644 --- a/src/applications/phid/PhabricatorObjectHandle.php +++ b/src/applications/phid/PhabricatorObjectHandle.php @@ -28,6 +28,7 @@ final class PhabricatorObjectHandle private $objectName; private $policyFiltered; private $subtitle; + private $tokenIcon; public function setIcon($icon) { $this->icon = $icon; @@ -86,6 +87,19 @@ final class PhabricatorObjectHandle return null; } + public function setTokenIcon($icon) { + $this->tokenIcon = $icon; + return $this; + } + + public function getTokenIcon() { + if ($this->tokenIcon !== null) { + return $this->tokenIcon; + } + + return $this->getIcon(); + } + public function getTypeIcon() { if ($this->getPHIDType()) { return $this->getPHIDType()->getTypeIcon(); diff --git a/src/applications/typeahead/view/PhabricatorTypeaheadTokenView.php b/src/applications/typeahead/view/PhabricatorTypeaheadTokenView.php index 2dacd84022..56867d8278 100644 --- a/src/applications/typeahead/view/PhabricatorTypeaheadTokenView.php +++ b/src/applications/typeahead/view/PhabricatorTypeaheadTokenView.php @@ -32,7 +32,7 @@ final class PhabricatorTypeaheadTokenView $token = id(new PhabricatorTypeaheadTokenView()) ->setKey($handle->getPHID()) ->setValue($handle->getFullName()) - ->setIcon($handle->getIcon()); + ->setIcon($handle->getTokenIcon()); if ($handle->isDisabled() || $handle->getStatus() == PhabricatorObjectHandle::STATUS_CLOSED) { From f5c686d6a4c5fa5b6b5e6de943058f7f1d1074b1 Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 28 Jan 2016 13:29:27 -0800 Subject: [PATCH 12/54] Swap charts from gRaphael to D3 Summary: Mostly, this has just been sitting in my sandbox for a long time. I may also touch some charting stuff with subprojects/milestones, but don't have particular plans to do that. D3 seems a bit more flexible, and it's easier to push more of the style logic into CSS so you can fix my design atrocities. gRaphael also hasn't been updated in ~3+ years. Test Plan: {F1085433} {F1085434} Reviewers: chad Reviewed By: chad Subscribers: cburroughs, yelirekim Differential Revision: https://secure.phabricator.com/D15155 --- resources/celerity/map.php | 25 ++- .../celerity/CelerityResourceTransformer.php | 2 +- .../PhabricatorFactChartController.php | 21 +- .../controller/ManiphestReportController.php | 11 +- support/lint/browser.jshintrc | 3 +- webroot/rsrc/css/phui/phui-chart.css | 54 ++++++ webroot/rsrc/externals/d3/LICENSE | 26 +++ webroot/rsrc/externals/d3/README.md | 9 + webroot/rsrc/externals/d3/d3.min.js | 9 + webroot/rsrc/externals/raphael/g.raphael.js | 12 -- .../rsrc/externals/raphael/g.raphael.line.js | 12 -- webroot/rsrc/externals/raphael/raphael.js | 13 -- .../maniphest/behavior-line-chart.js | 179 +++++++++++------- 13 files changed, 235 insertions(+), 141 deletions(-) create mode 100644 webroot/rsrc/css/phui/phui-chart.css create mode 100644 webroot/rsrc/externals/d3/LICENSE create mode 100644 webroot/rsrc/externals/d3/README.md create mode 100644 webroot/rsrc/externals/d3/d3.min.js delete mode 100644 webroot/rsrc/externals/raphael/g.raphael.js delete mode 100644 webroot/rsrc/externals/raphael/g.raphael.line.js delete mode 100644 webroot/rsrc/externals/raphael/raphael.js diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 51c31e4b91..ac56ed3d1b 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -125,6 +125,7 @@ return array( 'rsrc/css/phui/phui-big-info-view.css' => 'bd903741', '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-document-pro.css' => '8799acf7', 'rsrc/css/phui/phui-document-summary.css' => '9ca48bdf', @@ -158,6 +159,7 @@ return array( 'rsrc/css/sprite-login.css' => '60e8560e', 'rsrc/css/sprite-menu.css' => '9dd65b92', 'rsrc/css/sprite-tokens.css' => '4f399012', + 'rsrc/externals/d3/d3.min.js' => 'a11a5ff2', 'rsrc/externals/font/aleo/aleo-bold.eot' => 'd3d3bed7', 'rsrc/externals/font/aleo/aleo-bold.svg' => '45899c8e', 'rsrc/externals/font/aleo/aleo-bold.ttf' => '4b08bef0', @@ -252,9 +254,6 @@ return array( 'rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadPreloadedSource.js' => '54f314a0', 'rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadSource.js' => '1bc11c4a', 'rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadStaticSource.js' => '6c0e62fa', - 'rsrc/externals/raphael/g.raphael.js' => '40dde778', - 'rsrc/externals/raphael/g.raphael.line.js' => '40da039e', - 'rsrc/externals/raphael/raphael.js' => '51ee6b43', 'rsrc/favicons/apple-touch-icon-120x120.png' => '43742962', 'rsrc/favicons/apple-touch-icon-152x152.png' => '669eaec3', 'rsrc/favicons/apple-touch-icon-76x76.png' => 'ecdef672', @@ -401,7 +400,7 @@ return array( 'rsrc/js/application/herald/herald-rule-editor.js' => '7ebaeed3', 'rsrc/js/application/maniphest/behavior-batch-editor.js' => '782ab6e7', 'rsrc/js/application/maniphest/behavior-batch-selector.js' => '7b98d7c5', - 'rsrc/js/application/maniphest/behavior-line-chart.js' => '88f0c5b3', + 'rsrc/js/application/maniphest/behavior-line-chart.js' => 'e4232876', 'rsrc/js/application/maniphest/behavior-list-edit.js' => 'a9f88de2', 'rsrc/js/application/maniphest/behavior-subpriorityeditor.js' => '71237763', 'rsrc/js/application/owners/OwnersPathEditor.js' => 'aa1733d0', @@ -535,6 +534,7 @@ return array( 'conpherence-transaction-css' => '85d0974c', 'conpherence-update-css' => 'faf6be09', 'conpherence-widget-pane-css' => '775eaaba', + 'd3' => 'a11a5ff2', 'differential-changeset-view-css' => 'b6b0d1bb', 'differential-core-view-css' => '7ac3cabc', 'differential-inline-comment-editor' => '64a5550f', @@ -615,7 +615,7 @@ return array( 'javelin-behavior-icon-composer' => '8499b6ab', 'javelin-behavior-launch-icon-composer' => '48086888', 'javelin-behavior-lightbox-attachments' => 'f8ba29d7', - 'javelin-behavior-line-chart' => '88f0c5b3', + 'javelin-behavior-line-chart' => 'e4232876', 'javelin-behavior-load-blame' => '42126667', 'javelin-behavior-maniphest-batch-editor' => '782ab6e7', 'javelin-behavior-maniphest-batch-selector' => '7b98d7c5', @@ -799,6 +799,7 @@ return array( 'phui-calendar-day-css' => 'd1cf6f93', 'phui-calendar-list-css' => 'c1c7f338', 'phui-calendar-month-css' => '476be7e0', + 'phui-chart-css' => '6bf6f78e', 'phui-crumbs-view-css' => '414406b5', 'phui-document-summary-view-css' => '9ca48bdf', 'phui-document-view-css' => '9c71d2bf', @@ -843,9 +844,6 @@ return array( 'policy-transaction-detail-css' => '82100a43', 'ponder-view-css' => '7b0df4da', 'project-view-css' => 'c6387c87', - 'raphael-core' => '51ee6b43', - 'raphael-g' => '40dde778', - 'raphael-g-line' => '40da039e', 'releeph-core' => '9b3c5733', 'releeph-preview-branch' => 'b7a6f4a5', 'releeph-request-differential-create-dialog' => '8d8b92cd', @@ -1477,11 +1475,6 @@ return array( 'javelin-stratcom', 'javelin-dom', ), - '88f0c5b3' => array( - 'javelin-behavior', - 'javelin-dom', - 'javelin-vector', - ), '8a41885b' => array( 'javelin-install', 'javelin-dom', @@ -1950,6 +1943,12 @@ return array( 'javelin-dom', 'javelin-uri', ), + 'e4232876' => array( + 'javelin-behavior', + 'javelin-dom', + 'javelin-vector', + 'phui-chart-css', + ), 'e4cc26b3' => array( 'javelin-behavior', 'javelin-dom', diff --git a/src/applications/celerity/CelerityResourceTransformer.php b/src/applications/celerity/CelerityResourceTransformer.php index ee4685f4f7..6d86a8806a 100644 --- a/src/applications/celerity/CelerityResourceTransformer.php +++ b/src/applications/celerity/CelerityResourceTransformer.php @@ -66,7 +66,7 @@ final class CelerityResourceTransformer extends Phobject { return $data; } - // Some resources won't survive minification (like Raphael.js), and are + // Some resources won't survive minification (like d3.min.js), and are // marked so as not to be minified. if (strpos($data, '@'.'do-not-minify') !== false) { return $data; diff --git a/src/applications/fact/controller/PhabricatorFactChartController.php b/src/applications/fact/controller/PhabricatorFactChartController.php index d6f3bc0c43..18f769cc47 100644 --- a/src/applications/fact/controller/PhabricatorFactChartController.php +++ b/src/applications/fact/controller/PhabricatorFactChartController.php @@ -30,8 +30,7 @@ final class PhabricatorFactChartController extends PhabricatorFactController { } if (!$points) { - // NOTE: Raphael crashes Safari if you hand it series with no points. - throw new Exception(pht('No data to show!')); + throw new Exception('No data to show!'); } // Limit amount of data passed to browser. @@ -56,16 +55,12 @@ final class PhabricatorFactChartController extends PhabricatorFactController { 'div', array( 'id' => $id, - 'style' => 'border: 1px solid #6f6f6f; '. - 'margin: 1em 2em; '. - 'background: #ffffff; '. - 'height: 400px; ', + 'style' => 'background: #ffffff; '. + 'height: 480px; ', ), ''); - require_celerity_resource('raphael-core'); - require_celerity_resource('raphael-g'); - require_celerity_resource('raphael-g-line'); + require_celerity_resource('d3'); Javelin::initBehavior('line-chart', array( 'hardpoint' => $id, @@ -75,9 +70,9 @@ final class PhabricatorFactChartController extends PhabricatorFactController { 'colors' => array('#0000ff'), )); - $panel = new PHUIObjectBoxView(); - $panel->setHeaderText(pht('Count of %s', $spec->getName())); - $panel->appendChild($chart); + $box = id(new PHUIObjectBoxView()) + ->setHeaderText(pht('Count of %s', $spec->getName())) + ->appendChild($chart); $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb(pht('Chart')); @@ -85,7 +80,7 @@ final class PhabricatorFactChartController extends PhabricatorFactController { return $this->buildApplicationPage( array( $crumbs, - $panel, + $box, ), array( 'title' => pht('Chart'), diff --git a/src/applications/maniphest/controller/ManiphestReportController.php b/src/applications/maniphest/controller/ManiphestReportController.php index 6c2ce9fec3..c7f0cf2186 100644 --- a/src/applications/maniphest/controller/ManiphestReportController.php +++ b/src/applications/maniphest/controller/ManiphestReportController.php @@ -290,9 +290,8 @@ final class ManiphestReportController extends ManiphestController { list($burn_x, $burn_y) = $this->buildSeries($data); - require_celerity_resource('raphael-core'); - require_celerity_resource('raphael-g'); - require_celerity_resource('raphael-g-line'); + require_celerity_resource('d3'); + require_celerity_resource('phui-chart-css'); Javelin::initBehavior('line-chart', array( 'hardpoint' => $id, @@ -306,7 +305,11 @@ final class ManiphestReportController extends ManiphestController { 'yformat' => 'int', )); - return array($filter, $chart, $panel); + $box = id(new PHUIObjectBoxView()) + ->setHeaderText(pht('Burnup Rate')) + ->appendChild($chart); + + return array($filter, $box, $panel); } private function renderReportFilters(array $tokens, $has_window) { diff --git a/support/lint/browser.jshintrc b/support/lint/browser.jshintrc index 9bf5d3fc6a..b88c931eee 100644 --- a/support/lint/browser.jshintrc +++ b/support/lint/browser.jshintrc @@ -17,8 +17,9 @@ "globals": { "JX": false, - "Raphael": false, + "d3": false, "__DEV__": false }, + "browser": true } diff --git a/webroot/rsrc/css/phui/phui-chart.css b/webroot/rsrc/css/phui/phui-chart.css new file mode 100644 index 0000000000..be401b1fe3 --- /dev/null +++ b/webroot/rsrc/css/phui/phui-chart.css @@ -0,0 +1,54 @@ +/** + * @provides phui-chart-css + */ + +.chart .axis line, +.chart .axis path { + fill: none; + stroke: {$blueborder}; + shape-rendering: crispEdges; +} + +.chart .axis text { + fill: {$darkgreytext}; +} + +.chart .outer, +.chart .inner { + shape-rendering: crispEdges; +} + +.chart .outer { + fill: none; + stroke: none; +} + +.chart .inner { + fill: {$lightbluebackground}; + stroke: {$lightblueborder}; +} + +.chart .line { + fill: none; + stroke: {$blue}; + stroke-width: 2px; +} + +.chart .point { + fill: {$lightblue}; + stroke: {$blue}; + stroke-width: 1px; +} + +.chart-tooltip { + position: absolute; + text-align: center; + width: 120px; + height: 16px; + overflow: hidden; + padding: 2px; + background: {$lightbluebackground}; + border: 1px solid {$blueborder}; + border-radius: 8px; + pointer-events: none; +} diff --git a/webroot/rsrc/externals/d3/LICENSE b/webroot/rsrc/externals/d3/LICENSE new file mode 100644 index 0000000000..83013469b9 --- /dev/null +++ b/webroot/rsrc/externals/d3/LICENSE @@ -0,0 +1,26 @@ +Copyright (c) 2010-2014, Michael Bostock +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* The name Michael Bostock may not be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL MICHAEL BOSTOCK BE LIABLE FOR ANY DIRECT, +INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY +OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/webroot/rsrc/externals/d3/README.md b/webroot/rsrc/externals/d3/README.md new file mode 100644 index 0000000000..eb334e2701 --- /dev/null +++ b/webroot/rsrc/externals/d3/README.md @@ -0,0 +1,9 @@ +# Data-Driven Documents + + + +**D3.js** is a JavaScript library for manipulating documents based on data. **D3** helps you bring data to life using HTML, SVG and CSS. D3’s emphasis on web standards gives you the full capabilities of modern browsers without tying yourself to a proprietary framework, combining powerful visualization components and a data-driven approach to DOM manipulation. + +Want to learn more? [See the wiki.](https://github.com/mbostock/d3/wiki) + +For examples, [see the gallery](https://github.com/mbostock/d3/wiki/Gallery) and [mbostock’s bl.ocks](http://bl.ocks.org/mbostock). diff --git a/webroot/rsrc/externals/d3/d3.min.js b/webroot/rsrc/externals/d3/d3.min.js new file mode 100644 index 0000000000..f878a89d69 --- /dev/null +++ b/webroot/rsrc/externals/d3/d3.min.js @@ -0,0 +1,9 @@ +/** + * @provides d3 + * @do-not-minify + */ +!function(){function n(n){return n&&(n.ownerDocument||n.document||n).documentElement}function t(n){return n&&(n.ownerDocument&&n.ownerDocument.defaultView||n.document&&n||n.defaultView)}function e(n,t){return t>n?-1:n>t?1:n>=t?0:NaN}function r(n){return null===n?NaN:+n}function u(n){return!isNaN(n)}function i(n){return{left:function(t,e,r,u){for(arguments.length<3&&(r=0),arguments.length<4&&(u=t.length);u>r;){var i=r+u>>>1;n(t[i],e)<0?r=i+1:u=i}return r},right:function(t,e,r,u){for(arguments.length<3&&(r=0),arguments.length<4&&(u=t.length);u>r;){var i=r+u>>>1;n(t[i],e)>0?u=i:r=i+1}return r}}}function a(n){return n.length}function o(n){for(var t=1;n*t%1;)t*=10;return t}function l(n,t){for(var e in t)Object.defineProperty(n.prototype,e,{value:t[e],enumerable:!1})}function c(){this._=Object.create(null)}function s(n){return(n+="")===xa||n[0]===ba?ba+n:n}function f(n){return(n+="")[0]===ba?n.slice(1):n}function h(n){return s(n)in this._}function g(n){return(n=s(n))in this._&&delete this._[n]}function p(){var n=[];for(var t in this._)n.push(f(t));return n}function v(){var n=0;for(var t in this._)++n;return n}function d(){for(var n in this._)return!1;return!0}function m(){this._=Object.create(null)}function y(n){return n}function M(n,t,e){return function(){var r=e.apply(t,arguments);return r===t?n:r}}function x(n,t){if(t in n)return t;t=t.charAt(0).toUpperCase()+t.slice(1);for(var e=0,r=_a.length;r>e;++e){var u=_a[e]+t;if(u in n)return u}}function b(){}function _(){}function w(n){function t(){for(var t,r=e,u=-1,i=r.length;++ue;e++)for(var u,i=n[e],a=0,o=i.length;o>a;a++)(u=i[a])&&t(u,a,e);return n}function Z(n){return Sa(n,za),n}function V(n){var t,e;return function(r,u,i){var a,o=n[i].update,l=o.length;for(i!=e&&(e=i,t=0),u>=t&&(t=u+1);!(a=o[t])&&++t0&&(n=n.slice(0,o));var c=La.get(n);return c&&(n=c,l=B),o?t?u:r:t?b:i}function $(n,t){return function(e){var r=oa.event;oa.event=e,t[0]=this.__data__;try{n.apply(this,t)}finally{oa.event=r}}}function B(n,t){var e=$(n,t);return function(n){var t=this,r=n.relatedTarget;r&&(r===t||8&r.compareDocumentPosition(t))||e.call(t,n)}}function W(e){var r=".dragsuppress-"+ ++Ta,u="click"+r,i=oa.select(t(e)).on("touchmove"+r,S).on("dragstart"+r,S).on("selectstart"+r,S);if(null==qa&&(qa="onselectstart"in e?!1:x(e.style,"userSelect")),qa){var a=n(e).style,o=a[qa];a[qa]="none"}return function(n){if(i.on(r,null),qa&&(a[qa]=o),n){var t=function(){i.on(u,null)};i.on(u,function(){S(),t()},!0),setTimeout(t,0)}}}function J(n,e){e.changedTouches&&(e=e.changedTouches[0]);var r=n.ownerSVGElement||n;if(r.createSVGPoint){var u=r.createSVGPoint();if(0>Ra){var i=t(n);if(i.scrollX||i.scrollY){r=oa.select("body").append("svg").style({position:"absolute",top:0,left:0,margin:0,padding:0,border:"none"},"important");var a=r[0][0].getScreenCTM();Ra=!(a.f||a.e),r.remove()}}return Ra?(u.x=e.pageX,u.y=e.pageY):(u.x=e.clientX,u.y=e.clientY),u=u.matrixTransform(n.getScreenCTM().inverse()),[u.x,u.y]}var o=n.getBoundingClientRect();return[e.clientX-o.left-n.clientLeft,e.clientY-o.top-n.clientTop]}function G(){return oa.event.changedTouches[0].identifier}function K(n){return n>0?1:0>n?-1:0}function Q(n,t,e){return(t[0]-n[0])*(e[1]-n[1])-(t[1]-n[1])*(e[0]-n[0])}function nn(n){return n>1?0:-1>n?Ua:Math.acos(n)}function tn(n){return n>1?Ha:-1>n?-Ha:Math.asin(n)}function en(n){return((n=Math.exp(n))-1/n)/2}function rn(n){return((n=Math.exp(n))+1/n)/2}function un(n){return((n=Math.exp(2*n))-1)/(n+1)}function an(n){return(n=Math.sin(n/2))*n}function on(){}function ln(n,t,e){return this instanceof ln?(this.h=+n,this.s=+t,void(this.l=+e)):arguments.length<2?n instanceof ln?new ln(n.h,n.s,n.l):_n(""+n,wn,ln):new ln(n,t,e)}function cn(n,t,e){function r(n){return n>360?n-=360:0>n&&(n+=360),60>n?i+(a-i)*n/60:180>n?a:240>n?i+(a-i)*(240-n)/60:i}function u(n){return Math.round(255*r(n))}var i,a;return n=isNaN(n)?0:(n%=360)<0?n+360:n,t=isNaN(t)?0:0>t?0:t>1?1:t,e=0>e?0:e>1?1:e,a=.5>=e?e*(1+t):e+t-e*t,i=2*e-a,new yn(u(n+120),u(n),u(n-120))}function sn(n,t,e){return this instanceof sn?(this.h=+n,this.c=+t,void(this.l=+e)):arguments.length<2?n instanceof sn?new sn(n.h,n.c,n.l):n instanceof hn?pn(n.l,n.a,n.b):pn((n=Sn((n=oa.rgb(n)).r,n.g,n.b)).l,n.a,n.b):new sn(n,t,e)}function fn(n,t,e){return isNaN(n)&&(n=0),isNaN(t)&&(t=0),new hn(e,Math.cos(n*=Oa)*t,Math.sin(n)*t)}function hn(n,t,e){return this instanceof hn?(this.l=+n,this.a=+t,void(this.b=+e)):arguments.length<2?n instanceof hn?new hn(n.l,n.a,n.b):n instanceof sn?fn(n.h,n.c,n.l):Sn((n=yn(n)).r,n.g,n.b):new hn(n,t,e)}function gn(n,t,e){var r=(n+16)/116,u=r+t/500,i=r-e/200;return u=vn(u)*Ka,r=vn(r)*Qa,i=vn(i)*no,new yn(mn(3.2404542*u-1.5371385*r-.4985314*i),mn(-.969266*u+1.8760108*r+.041556*i),mn(.0556434*u-.2040259*r+1.0572252*i))}function pn(n,t,e){return n>0?new sn(Math.atan2(e,t)*Ia,Math.sqrt(t*t+e*e),n):new sn(NaN,NaN,n)}function vn(n){return n>.206893034?n*n*n:(n-4/29)/7.787037}function dn(n){return n>.008856?Math.pow(n,1/3):7.787037*n+4/29}function mn(n){return Math.round(255*(.00304>=n?12.92*n:1.055*Math.pow(n,1/2.4)-.055))}function yn(n,t,e){return this instanceof yn?(this.r=~~n,this.g=~~t,void(this.b=~~e)):arguments.length<2?n instanceof yn?new yn(n.r,n.g,n.b):_n(""+n,yn,cn):new yn(n,t,e)}function Mn(n){return new yn(n>>16,n>>8&255,255&n)}function xn(n){return Mn(n)+""}function bn(n){return 16>n?"0"+Math.max(0,n).toString(16):Math.min(255,n).toString(16)}function _n(n,t,e){var r,u,i,a=0,o=0,l=0;if(r=/([a-z]+)\((.*)\)/.exec(n=n.toLowerCase()))switch(u=r[2].split(","),r[1]){case"hsl":return e(parseFloat(u[0]),parseFloat(u[1])/100,parseFloat(u[2])/100);case"rgb":return t(Nn(u[0]),Nn(u[1]),Nn(u[2]))}return(i=ro.get(n))?t(i.r,i.g,i.b):(null==n||"#"!==n.charAt(0)||isNaN(i=parseInt(n.slice(1),16))||(4===n.length?(a=(3840&i)>>4,a=a>>4|a,o=240&i,o=o>>4|o,l=15&i,l=l<<4|l):7===n.length&&(a=(16711680&i)>>16,o=(65280&i)>>8,l=255&i)),t(a,o,l))}function wn(n,t,e){var r,u,i=Math.min(n/=255,t/=255,e/=255),a=Math.max(n,t,e),o=a-i,l=(a+i)/2;return o?(u=.5>l?o/(a+i):o/(2-a-i),r=n==a?(t-e)/o+(e>t?6:0):t==a?(e-n)/o+2:(n-t)/o+4,r*=60):(r=NaN,u=l>0&&1>l?0:r),new ln(r,u,l)}function Sn(n,t,e){n=kn(n),t=kn(t),e=kn(e);var r=dn((.4124564*n+.3575761*t+.1804375*e)/Ka),u=dn((.2126729*n+.7151522*t+.072175*e)/Qa),i=dn((.0193339*n+.119192*t+.9503041*e)/no);return hn(116*u-16,500*(r-u),200*(u-i))}function kn(n){return(n/=255)<=.04045?n/12.92:Math.pow((n+.055)/1.055,2.4)}function Nn(n){var t=parseFloat(n);return"%"===n.charAt(n.length-1)?Math.round(2.55*t):t}function En(n){return"function"==typeof n?n:function(){return n}}function An(n){return function(t,e,r){return 2===arguments.length&&"function"==typeof e&&(r=e,e=null),Cn(t,e,n,r)}}function Cn(n,t,e,r){function u(){var n,t=l.status;if(!t&&Ln(l)||t>=200&&300>t||304===t){try{n=e.call(i,l)}catch(r){return void a.error.call(i,r)}a.load.call(i,n)}else a.error.call(i,l)}var i={},a=oa.dispatch("beforesend","progress","load","error"),o={},l=new XMLHttpRequest,c=null;return!this.XDomainRequest||"withCredentials"in l||!/^(http(s)?:)?\/\//.test(n)||(l=new XDomainRequest),"onload"in l?l.onload=l.onerror=u:l.onreadystatechange=function(){l.readyState>3&&u()},l.onprogress=function(n){var t=oa.event;oa.event=n;try{a.progress.call(i,l)}finally{oa.event=t}},i.header=function(n,t){return n=(n+"").toLowerCase(),arguments.length<2?o[n]:(null==t?delete o[n]:o[n]=t+"",i)},i.mimeType=function(n){return arguments.length?(t=null==n?null:n+"",i):t},i.responseType=function(n){return arguments.length?(c=n,i):c},i.response=function(n){return e=n,i},["get","post"].forEach(function(n){i[n]=function(){return i.send.apply(i,[n].concat(ca(arguments)))}}),i.send=function(e,r,u){if(2===arguments.length&&"function"==typeof r&&(u=r,r=null),l.open(e,n,!0),null==t||"accept"in o||(o.accept=t+",*/*"),l.setRequestHeader)for(var s in o)l.setRequestHeader(s,o[s]);return null!=t&&l.overrideMimeType&&l.overrideMimeType(t),null!=c&&(l.responseType=c),null!=u&&i.on("error",u).on("load",function(n){u(null,n)}),a.beforesend.call(i,l),l.send(null==r?null:r),i},i.abort=function(){return l.abort(),i},oa.rebind(i,a,"on"),null==r?i:i.get(zn(r))}function zn(n){return 1===n.length?function(t,e){n(null==t?e:null)}:n}function Ln(n){var t=n.responseType;return t&&"text"!==t?n.response:n.responseText}function qn(n,t,e){var r=arguments.length;2>r&&(t=0),3>r&&(e=Date.now());var u=e+t,i={c:n,t:u,n:null};return io?io.n=i:uo=i,io=i,ao||(oo=clearTimeout(oo),ao=1,lo(Tn)),i}function Tn(){var n=Rn(),t=Dn()-n;t>24?(isFinite(t)&&(clearTimeout(oo),oo=setTimeout(Tn,t)),ao=0):(ao=1,lo(Tn))}function Rn(){for(var n=Date.now(),t=uo;t;)n>=t.t&&t.c(n-t.t)&&(t.c=null),t=t.n;return n}function Dn(){for(var n,t=uo,e=1/0;t;)t.c?(t.t8?function(n){return n/e}:function(n){return n*e},symbol:n}}function jn(n){var t=n.decimal,e=n.thousands,r=n.grouping,u=n.currency,i=r&&e?function(n,t){for(var u=n.length,i=[],a=0,o=r[0],l=0;u>0&&o>0&&(l+o+1>t&&(o=Math.max(1,t-l)),i.push(n.substring(u-=o,u+o)),!((l+=o+1)>t));)o=r[a=(a+1)%r.length];return i.reverse().join(e)}:y;return function(n){var e=so.exec(n),r=e[1]||" ",a=e[2]||">",o=e[3]||"-",l=e[4]||"",c=e[5],s=+e[6],f=e[7],h=e[8],g=e[9],p=1,v="",d="",m=!1,y=!0;switch(h&&(h=+h.substring(1)),(c||"0"===r&&"="===a)&&(c=r="0",a="="),g){case"n":f=!0,g="g";break;case"%":p=100,d="%",g="f";break;case"p":p=100,d="%",g="r";break;case"b":case"o":case"x":case"X":"#"===l&&(v="0"+g.toLowerCase());case"c":y=!1;case"d":m=!0,h=0;break;case"s":p=-1,g="r"}"$"===l&&(v=u[0],d=u[1]),"r"!=g||h||(g="g"),null!=h&&("g"==g?h=Math.max(1,Math.min(21,h)):("e"==g||"f"==g)&&(h=Math.max(0,Math.min(20,h)))),g=fo.get(g)||Fn;var M=c&&f;return function(n){var e=d;if(m&&n%1)return"";var u=0>n||0===n&&0>1/n?(n=-n,"-"):"-"===o?"":o;if(0>p){var l=oa.formatPrefix(n,h);n=l.scale(n),e=l.symbol+d}else n*=p;n=g(n,h);var x,b,_=n.lastIndexOf(".");if(0>_){var w=y?n.lastIndexOf("e"):-1;0>w?(x=n,b=""):(x=n.substring(0,w),b=n.substring(w))}else x=n.substring(0,_),b=t+n.substring(_+1);!c&&f&&(x=i(x,1/0));var S=v.length+x.length+b.length+(M?0:u.length),k=s>S?new Array(S=s-S+1).join(r):"";return M&&(x=i(k+x,k.length?s-b.length:1/0)),u+=v,n=x+b,("<"===a?u+n+k:">"===a?k+u+n:"^"===a?k.substring(0,S>>=1)+u+n+k.substring(S):u+(M?n:k+n))+e}}}function Fn(n){return n+""}function Hn(){this._=new Date(arguments.length>1?Date.UTC.apply(this,arguments):arguments[0])}function On(n,t,e){function r(t){var e=n(t),r=i(e,1);return r-t>t-e?e:r}function u(e){return t(e=n(new go(e-1)),1),e}function i(n,e){return t(n=new go(+n),e),n}function a(n,r,i){var a=u(n),o=[];if(i>1)for(;r>a;)e(a)%i||o.push(new Date(+a)),t(a,1);else for(;r>a;)o.push(new Date(+a)),t(a,1);return o}function o(n,t,e){try{go=Hn;var r=new Hn;return r._=n,a(r,t,e)}finally{go=Date}}n.floor=n,n.round=r,n.ceil=u,n.offset=i,n.range=a;var l=n.utc=In(n);return l.floor=l,l.round=In(r),l.ceil=In(u),l.offset=In(i),l.range=o,n}function In(n){return function(t,e){try{go=Hn;var r=new Hn;return r._=t,n(r,e)._}finally{go=Date}}}function Yn(n){function t(n){function t(t){for(var e,u,i,a=[],o=-1,l=0;++oo;){if(r>=c)return-1;if(u=t.charCodeAt(o++),37===u){if(a=t.charAt(o++),i=C[a in vo?t.charAt(o++):a],!i||(r=i(n,e,r))<0)return-1}else if(u!=e.charCodeAt(r++))return-1}return r}function r(n,t,e){_.lastIndex=0;var r=_.exec(t.slice(e));return r?(n.w=w.get(r[0].toLowerCase()),e+r[0].length):-1}function u(n,t,e){x.lastIndex=0;var r=x.exec(t.slice(e));return r?(n.w=b.get(r[0].toLowerCase()),e+r[0].length):-1}function i(n,t,e){N.lastIndex=0;var r=N.exec(t.slice(e));return r?(n.m=E.get(r[0].toLowerCase()),e+r[0].length):-1}function a(n,t,e){S.lastIndex=0;var r=S.exec(t.slice(e));return r?(n.m=k.get(r[0].toLowerCase()),e+r[0].length):-1}function o(n,t,r){return e(n,A.c.toString(),t,r)}function l(n,t,r){return e(n,A.x.toString(),t,r)}function c(n,t,r){return e(n,A.X.toString(),t,r)}function s(n,t,e){var r=M.get(t.slice(e,e+=2).toLowerCase());return null==r?-1:(n.p=r,e)}var f=n.dateTime,h=n.date,g=n.time,p=n.periods,v=n.days,d=n.shortDays,m=n.months,y=n.shortMonths;t.utc=function(n){function e(n){try{go=Hn;var t=new go;return t._=n,r(t)}finally{go=Date}}var r=t(n);return e.parse=function(n){try{go=Hn;var t=r.parse(n);return t&&t._}finally{go=Date}},e.toString=r.toString,e},t.multi=t.utc.multi=ct;var M=oa.map(),x=Vn(v),b=Xn(v),_=Vn(d),w=Xn(d),S=Vn(m),k=Xn(m),N=Vn(y),E=Xn(y);p.forEach(function(n,t){M.set(n.toLowerCase(),t)});var A={a:function(n){return d[n.getDay()]},A:function(n){return v[n.getDay()]},b:function(n){return y[n.getMonth()]},B:function(n){return m[n.getMonth()]},c:t(f),d:function(n,t){return Zn(n.getDate(),t,2)},e:function(n,t){return Zn(n.getDate(),t,2)},H:function(n,t){return Zn(n.getHours(),t,2)},I:function(n,t){return Zn(n.getHours()%12||12,t,2)},j:function(n,t){return Zn(1+ho.dayOfYear(n),t,3)},L:function(n,t){return Zn(n.getMilliseconds(),t,3)},m:function(n,t){return Zn(n.getMonth()+1,t,2)},M:function(n,t){return Zn(n.getMinutes(),t,2)},p:function(n){return p[+(n.getHours()>=12)]},S:function(n,t){return Zn(n.getSeconds(),t,2)},U:function(n,t){return Zn(ho.sundayOfYear(n),t,2)},w:function(n){return n.getDay()},W:function(n,t){return Zn(ho.mondayOfYear(n),t,2)},x:t(h),X:t(g),y:function(n,t){return Zn(n.getFullYear()%100,t,2)},Y:function(n,t){return Zn(n.getFullYear()%1e4,t,4)},Z:ot,"%":function(){return"%"}},C={a:r,A:u,b:i,B:a,c:o,d:tt,e:tt,H:rt,I:rt,j:et,L:at,m:nt,M:ut,p:s,S:it,U:Bn,w:$n,W:Wn,x:l,X:c,y:Gn,Y:Jn,Z:Kn,"%":lt};return t}function Zn(n,t,e){var r=0>n?"-":"",u=(r?-n:n)+"",i=u.length;return r+(e>i?new Array(e-i+1).join(t)+u:u)}function Vn(n){return new RegExp("^(?:"+n.map(oa.requote).join("|")+")","i")}function Xn(n){for(var t=new c,e=-1,r=n.length;++e68?1900:2e3)}function nt(n,t,e){mo.lastIndex=0;var r=mo.exec(t.slice(e,e+2));return r?(n.m=r[0]-1,e+r[0].length):-1}function tt(n,t,e){mo.lastIndex=0;var r=mo.exec(t.slice(e,e+2));return r?(n.d=+r[0],e+r[0].length):-1}function et(n,t,e){mo.lastIndex=0;var r=mo.exec(t.slice(e,e+3));return r?(n.j=+r[0],e+r[0].length):-1}function rt(n,t,e){mo.lastIndex=0;var r=mo.exec(t.slice(e,e+2));return r?(n.H=+r[0],e+r[0].length):-1}function ut(n,t,e){mo.lastIndex=0;var r=mo.exec(t.slice(e,e+2));return r?(n.M=+r[0],e+r[0].length):-1}function it(n,t,e){mo.lastIndex=0;var r=mo.exec(t.slice(e,e+2));return r?(n.S=+r[0],e+r[0].length):-1}function at(n,t,e){mo.lastIndex=0;var r=mo.exec(t.slice(e,e+3));return r?(n.L=+r[0],e+r[0].length):-1}function ot(n){var t=n.getTimezoneOffset(),e=t>0?"-":"+",r=Ma(t)/60|0,u=Ma(t)%60;return e+Zn(r,"0",2)+Zn(u,"0",2)}function lt(n,t,e){yo.lastIndex=0;var r=yo.exec(t.slice(e,e+1));return r?e+r[0].length:-1}function ct(n){for(var t=n.length,e=-1;++e=0?1:-1,o=a*e,l=Math.cos(t),c=Math.sin(t),s=i*c,f=u*l+s*Math.cos(o),h=s*a*Math.sin(o);So.add(Math.atan2(h,f)),r=n,u=l,i=c}var t,e,r,u,i;ko.point=function(a,o){ko.point=n,r=(t=a)*Oa,u=Math.cos(o=(e=o)*Oa/2+Ua/4),i=Math.sin(o)},ko.lineEnd=function(){n(t,e)}}function dt(n){var t=n[0],e=n[1],r=Math.cos(e);return[r*Math.cos(t),r*Math.sin(t),Math.sin(e)]}function mt(n,t){return n[0]*t[0]+n[1]*t[1]+n[2]*t[2]}function yt(n,t){return[n[1]*t[2]-n[2]*t[1],n[2]*t[0]-n[0]*t[2],n[0]*t[1]-n[1]*t[0]]}function Mt(n,t){n[0]+=t[0],n[1]+=t[1],n[2]+=t[2]}function xt(n,t){return[n[0]*t,n[1]*t,n[2]*t]}function bt(n){var t=Math.sqrt(n[0]*n[0]+n[1]*n[1]+n[2]*n[2]);n[0]/=t,n[1]/=t,n[2]/=t}function _t(n){return[Math.atan2(n[1],n[0]),tn(n[2])]}function wt(n,t){return Ma(n[0]-t[0])o;++o)u.point((e=n[o])[0],e[1]);return void u.lineEnd()}var l=new Tt(e,n,null,!0),c=new Tt(e,null,l,!1);l.o=c,i.push(l),a.push(c),l=new Tt(r,n,null,!1),c=new Tt(r,null,l,!0),l.o=c,i.push(l),a.push(c)}}),a.sort(t),qt(i),qt(a),i.length){for(var o=0,l=e,c=a.length;c>o;++o)a[o].e=l=!l;for(var s,f,h=i[0];;){for(var g=h,p=!0;g.v;)if((g=g.n)===h)return;s=g.z,u.lineStart();do{if(g.v=g.o.v=!0,g.e){if(p)for(var o=0,c=s.length;c>o;++o)u.point((f=s[o])[0],f[1]);else r(g.x,g.n.x,1,u);g=g.n}else{if(p){s=g.p.z;for(var o=s.length-1;o>=0;--o)u.point((f=s[o])[0],f[1])}else r(g.x,g.p.x,-1,u);g=g.p}g=g.o,s=g.z,p=!p}while(!g.v);u.lineEnd()}}}function qt(n){if(t=n.length){for(var t,e,r=0,u=n[0];++r0){for(b||(i.polygonStart(),b=!0),i.lineStart();++a1&&2&t&&e.push(e.pop().concat(e.shift())),g.push(e.filter(Dt))}var g,p,v,d=t(i),m=u.invert(r[0],r[1]),y={point:a,lineStart:l,lineEnd:c,polygonStart:function(){y.point=s,y.lineStart=f,y.lineEnd=h,g=[],p=[]},polygonEnd:function(){y.point=a,y.lineStart=l,y.lineEnd=c,g=oa.merge(g);var n=Ot(m,p);g.length?(b||(i.polygonStart(),b=!0),Lt(g,Ut,n,e,i)):n&&(b||(i.polygonStart(),b=!0),i.lineStart(),e(null,null,1,i),i.lineEnd()),b&&(i.polygonEnd(),b=!1),g=p=null},sphere:function(){i.polygonStart(),i.lineStart(),e(null,null,1,i),i.lineEnd(),i.polygonEnd()}},M=Pt(),x=t(M),b=!1;return y}}function Dt(n){return n.length>1}function Pt(){var n,t=[];return{lineStart:function(){t.push(n=[])},point:function(t,e){n.push([t,e])},lineEnd:b,buffer:function(){var e=t;return t=[],n=null,e},rejoin:function(){t.length>1&&t.push(t.pop().concat(t.shift()))}}}function Ut(n,t){return((n=n.x)[0]<0?n[1]-Ha-Da:Ha-n[1])-((t=t.x)[0]<0?t[1]-Ha-Da:Ha-t[1])}function jt(n){var t,e=NaN,r=NaN,u=NaN;return{lineStart:function(){n.lineStart(),t=1},point:function(i,a){var o=i>0?Ua:-Ua,l=Ma(i-e);Ma(l-Ua)0?Ha:-Ha),n.point(u,r),n.lineEnd(),n.lineStart(),n.point(o,r),n.point(i,r),t=0):u!==o&&l>=Ua&&(Ma(e-u)Da?Math.atan((Math.sin(t)*(i=Math.cos(r))*Math.sin(e)-Math.sin(r)*(u=Math.cos(t))*Math.sin(n))/(u*i*a)):(t+r)/2}function Ht(n,t,e,r){var u;if(null==n)u=e*Ha,r.point(-Ua,u),r.point(0,u),r.point(Ua,u),r.point(Ua,0),r.point(Ua,-u),r.point(0,-u),r.point(-Ua,-u),r.point(-Ua,0),r.point(-Ua,u);else if(Ma(n[0]-t[0])>Da){var i=n[0]o;++o){var c=t[o],s=c.length;if(s)for(var f=c[0],h=f[0],g=f[1]/2+Ua/4,p=Math.sin(g),v=Math.cos(g),d=1;;){d===s&&(d=0),n=c[d];var m=n[0],y=n[1]/2+Ua/4,M=Math.sin(y),x=Math.cos(y),b=m-h,_=b>=0?1:-1,w=_*b,S=w>Ua,k=p*M;if(So.add(Math.atan2(k*_*Math.sin(w),v*x+k*Math.cos(w))),i+=S?b+_*ja:b,S^h>=e^m>=e){var N=yt(dt(f),dt(n));bt(N);var E=yt(u,N);bt(E);var A=(S^b>=0?-1:1)*tn(E[2]);(r>A||r===A&&(N[0]||N[1]))&&(a+=S^b>=0?1:-1)}if(!d++)break;h=m,p=M,v=x,f=n}}return(-Da>i||Da>i&&0>So)^1&a}function It(n){function t(n,t){return Math.cos(n)*Math.cos(t)>i}function e(n){var e,i,l,c,s;return{lineStart:function(){c=l=!1,s=1},point:function(f,h){var g,p=[f,h],v=t(f,h),d=a?v?0:u(f,h):v?u(f+(0>f?Ua:-Ua),h):0;if(!e&&(c=l=v)&&n.lineStart(),v!==l&&(g=r(e,p),(wt(e,g)||wt(p,g))&&(p[0]+=Da,p[1]+=Da,v=t(p[0],p[1]))),v!==l)s=0,v?(n.lineStart(),g=r(p,e),n.point(g[0],g[1])):(g=r(e,p),n.point(g[0],g[1]),n.lineEnd()),e=g;else if(o&&e&&a^v){var m;d&i||!(m=r(p,e,!0))||(s=0,a?(n.lineStart(),n.point(m[0][0],m[0][1]),n.point(m[1][0],m[1][1]),n.lineEnd()):(n.point(m[1][0],m[1][1]),n.lineEnd(),n.lineStart(),n.point(m[0][0],m[0][1])))}!v||e&&wt(e,p)||n.point(p[0],p[1]),e=p,l=v,i=d},lineEnd:function(){l&&n.lineEnd(),e=null},clean:function(){return s|(c&&l)<<1}}}function r(n,t,e){var r=dt(n),u=dt(t),a=[1,0,0],o=yt(r,u),l=mt(o,o),c=o[0],s=l-c*c;if(!s)return!e&&n;var f=i*l/s,h=-i*c/s,g=yt(a,o),p=xt(a,f),v=xt(o,h);Mt(p,v);var d=g,m=mt(p,d),y=mt(d,d),M=m*m-y*(mt(p,p)-1);if(!(0>M)){var x=Math.sqrt(M),b=xt(d,(-m-x)/y);if(Mt(b,p),b=_t(b),!e)return b;var _,w=n[0],S=t[0],k=n[1],N=t[1];w>S&&(_=w,w=S,S=_);var E=S-w,A=Ma(E-Ua)E;if(!A&&k>N&&(_=k,k=N,N=_),C?A?k+N>0^b[1]<(Ma(b[0]-w)Ua^(w<=b[0]&&b[0]<=S)){var z=xt(d,(-m+x)/y);return Mt(z,p),[b,_t(z)]}}}function u(t,e){var r=a?n:Ua-n,u=0;return-r>t?u|=1:t>r&&(u|=2),-r>e?u|=4:e>r&&(u|=8),u}var i=Math.cos(n),a=i>0,o=Ma(i)>Da,l=ve(n,6*Oa);return Rt(t,e,l,a?[0,-n]:[-Ua,n-Ua])}function Yt(n,t,e,r){return function(u){var i,a=u.a,o=u.b,l=a.x,c=a.y,s=o.x,f=o.y,h=0,g=1,p=s-l,v=f-c;if(i=n-l,p||!(i>0)){if(i/=p,0>p){if(h>i)return;g>i&&(g=i)}else if(p>0){if(i>g)return;i>h&&(h=i)}if(i=e-l,p||!(0>i)){if(i/=p,0>p){if(i>g)return;i>h&&(h=i)}else if(p>0){if(h>i)return;g>i&&(g=i)}if(i=t-c,v||!(i>0)){if(i/=v,0>v){if(h>i)return;g>i&&(g=i)}else if(v>0){if(i>g)return;i>h&&(h=i)}if(i=r-c,v||!(0>i)){if(i/=v,0>v){if(i>g)return;i>h&&(h=i)}else if(v>0){if(h>i)return;g>i&&(g=i)}return h>0&&(u.a={x:l+h*p,y:c+h*v}),1>g&&(u.b={x:l+g*p,y:c+g*v}),u}}}}}}function Zt(n,t,e,r){function u(r,u){return Ma(r[0]-n)0?0:3:Ma(r[0]-e)0?2:1:Ma(r[1]-t)0?1:0:u>0?3:2}function i(n,t){return a(n.x,t.x)}function a(n,t){var e=u(n,1),r=u(t,1);return e!==r?e-r:0===e?t[1]-n[1]:1===e?n[0]-t[0]:2===e?n[1]-t[1]:t[0]-n[0]}return function(o){function l(n){for(var t=0,e=d.length,r=n[1],u=0;e>u;++u)for(var i,a=1,o=d[u],l=o.length,c=o[0];l>a;++a)i=o[a],c[1]<=r?i[1]>r&&Q(c,i,n)>0&&++t:i[1]<=r&&Q(c,i,n)<0&&--t,c=i;return 0!==t}function c(i,o,l,c){var s=0,f=0;if(null==i||(s=u(i,l))!==(f=u(o,l))||a(i,o)<0^l>0){do c.point(0===s||3===s?n:e,s>1?r:t);while((s=(s+l+4)%4)!==f)}else c.point(o[0],o[1])}function s(u,i){return u>=n&&e>=u&&i>=t&&r>=i}function f(n,t){s(n,t)&&o.point(n,t)}function h(){C.point=p,d&&d.push(m=[]),S=!0,w=!1,b=_=NaN}function g(){v&&(p(y,M),x&&w&&E.rejoin(),v.push(E.buffer())),C.point=f,w&&o.lineEnd()}function p(n,t){n=Math.max(-Fo,Math.min(Fo,n)),t=Math.max(-Fo,Math.min(Fo,t));var e=s(n,t);if(d&&m.push([n,t]),S)y=n,M=t,x=e,S=!1,e&&(o.lineStart(),o.point(n,t));else if(e&&w)o.point(n,t);else{var r={a:{x:b,y:_},b:{x:n,y:t}};A(r)?(w||(o.lineStart(),o.point(r.a.x,r.a.y)),o.point(r.b.x,r.b.y),e||o.lineEnd(),k=!1):e&&(o.lineStart(),o.point(n,t),k=!1)}b=n,_=t,w=e}var v,d,m,y,M,x,b,_,w,S,k,N=o,E=Pt(),A=Yt(n,t,e,r),C={point:f,lineStart:h,lineEnd:g,polygonStart:function(){o=E,v=[],d=[],k=!0},polygonEnd:function(){o=N,v=oa.merge(v);var t=l([n,r]),e=k&&t,u=v.length;(e||u)&&(o.polygonStart(),e&&(o.lineStart(),c(null,null,1,o),o.lineEnd()),u&&Lt(v,i,t,c,o),o.polygonEnd()),v=d=m=null}};return C}}function Vt(n){var t=0,e=Ua/3,r=oe(n),u=r(t,e);return u.parallels=function(n){return arguments.length?r(t=n[0]*Ua/180,e=n[1]*Ua/180):[t/Ua*180,e/Ua*180]},u}function Xt(n,t){function e(n,t){var e=Math.sqrt(i-2*u*Math.sin(t))/u;return[e*Math.sin(n*=u),a-e*Math.cos(n)]}var r=Math.sin(n),u=(r+Math.sin(t))/2,i=1+r*(2*u-r),a=Math.sqrt(i)/u;return e.invert=function(n,t){var e=a-t;return[Math.atan2(n,e)/u,tn((i-(n*n+e*e)*u*u)/(2*u))]},e}function $t(){function n(n,t){Oo+=u*n-r*t,r=n,u=t}var t,e,r,u;Xo.point=function(i,a){Xo.point=n,t=r=i,e=u=a},Xo.lineEnd=function(){n(t,e)}}function Bt(n,t){Io>n&&(Io=n),n>Zo&&(Zo=n),Yo>t&&(Yo=t),t>Vo&&(Vo=t)}function Wt(){function n(n,t){a.push("M",n,",",t,i)}function t(n,t){a.push("M",n,",",t),o.point=e}function e(n,t){a.push("L",n,",",t)}function r(){o.point=n}function u(){a.push("Z")}var i=Jt(4.5),a=[],o={point:n,lineStart:function(){o.point=t},lineEnd:r,polygonStart:function(){o.lineEnd=u},polygonEnd:function(){o.lineEnd=r,o.point=n},pointRadius:function(n){return i=Jt(n),o},result:function(){if(a.length){var n=a.join("");return a=[],n}}};return o}function Jt(n){return"m0,"+n+"a"+n+","+n+" 0 1,1 0,"+-2*n+"a"+n+","+n+" 0 1,1 0,"+2*n+"z"}function Gt(n,t){Ao+=n,Co+=t,++zo}function Kt(){function n(n,r){var u=n-t,i=r-e,a=Math.sqrt(u*u+i*i);Lo+=a*(t+n)/2,qo+=a*(e+r)/2,To+=a,Gt(t=n,e=r)}var t,e;Bo.point=function(r,u){Bo.point=n,Gt(t=r,e=u)}}function Qt(){Bo.point=Gt}function ne(){function n(n,t){var e=n-r,i=t-u,a=Math.sqrt(e*e+i*i);Lo+=a*(r+n)/2,qo+=a*(u+t)/2,To+=a,a=u*n-r*t,Ro+=a*(r+n),Do+=a*(u+t),Po+=3*a,Gt(r=n,u=t)}var t,e,r,u;Bo.point=function(i,a){Bo.point=n,Gt(t=r=i,e=u=a)},Bo.lineEnd=function(){n(t,e)}}function te(n){function t(t,e){n.moveTo(t+a,e),n.arc(t,e,a,0,ja)}function e(t,e){n.moveTo(t,e),o.point=r}function r(t,e){n.lineTo(t,e)}function u(){o.point=t}function i(){n.closePath()}var a=4.5,o={point:t,lineStart:function(){o.point=e},lineEnd:u,polygonStart:function(){o.lineEnd=i},polygonEnd:function(){o.lineEnd=u,o.point=t},pointRadius:function(n){return a=n,o},result:b};return o}function ee(n){function t(n){return(o?r:e)(n)}function e(t){return ie(t,function(e,r){e=n(e,r),t.point(e[0],e[1])})}function r(t){function e(e,r){e=n(e,r),t.point(e[0],e[1])}function r(){M=NaN,S.point=i,t.lineStart()}function i(e,r){var i=dt([e,r]),a=n(e,r);u(M,x,y,b,_,w,M=a[0],x=a[1],y=e,b=i[0],_=i[1],w=i[2],o,t),t.point(M,x)}function a(){S.point=e,t.lineEnd()}function l(){ +r(),S.point=c,S.lineEnd=s}function c(n,t){i(f=n,h=t),g=M,p=x,v=b,d=_,m=w,S.point=i}function s(){u(M,x,y,b,_,w,g,p,f,v,d,m,o,t),S.lineEnd=a,a()}var f,h,g,p,v,d,m,y,M,x,b,_,w,S={point:e,lineStart:r,lineEnd:a,polygonStart:function(){t.polygonStart(),S.lineStart=l},polygonEnd:function(){t.polygonEnd(),S.lineStart=r}};return S}function u(t,e,r,o,l,c,s,f,h,g,p,v,d,m){var y=s-t,M=f-e,x=y*y+M*M;if(x>4*i&&d--){var b=o+g,_=l+p,w=c+v,S=Math.sqrt(b*b+_*_+w*w),k=Math.asin(w/=S),N=Ma(Ma(w)-1)i||Ma((y*z+M*L)/x-.5)>.3||a>o*g+l*p+c*v)&&(u(t,e,r,o,l,c,A,C,N,b/=S,_/=S,w,d,m),m.point(A,C),u(A,C,N,b,_,w,s,f,h,g,p,v,d,m))}}var i=.5,a=Math.cos(30*Oa),o=16;return t.precision=function(n){return arguments.length?(o=(i=n*n)>0&&16,t):Math.sqrt(i)},t}function re(n){var t=ee(function(t,e){return n([t*Ia,e*Ia])});return function(n){return le(t(n))}}function ue(n){this.stream=n}function ie(n,t){return{point:t,sphere:function(){n.sphere()},lineStart:function(){n.lineStart()},lineEnd:function(){n.lineEnd()},polygonStart:function(){n.polygonStart()},polygonEnd:function(){n.polygonEnd()}}}function ae(n){return oe(function(){return n})()}function oe(n){function t(n){return n=o(n[0]*Oa,n[1]*Oa),[n[0]*h+l,c-n[1]*h]}function e(n){return n=o.invert((n[0]-l)/h,(c-n[1])/h),n&&[n[0]*Ia,n[1]*Ia]}function r(){o=Ct(a=fe(m,M,x),i);var n=i(v,d);return l=g-n[0]*h,c=p+n[1]*h,u()}function u(){return s&&(s.valid=!1,s=null),t}var i,a,o,l,c,s,f=ee(function(n,t){return n=i(n,t),[n[0]*h+l,c-n[1]*h]}),h=150,g=480,p=250,v=0,d=0,m=0,M=0,x=0,b=jo,_=y,w=null,S=null;return t.stream=function(n){return s&&(s.valid=!1),s=le(b(a,f(_(n)))),s.valid=!0,s},t.clipAngle=function(n){return arguments.length?(b=null==n?(w=n,jo):It((w=+n)*Oa),u()):w},t.clipExtent=function(n){return arguments.length?(S=n,_=n?Zt(n[0][0],n[0][1],n[1][0],n[1][1]):y,u()):S},t.scale=function(n){return arguments.length?(h=+n,r()):h},t.translate=function(n){return arguments.length?(g=+n[0],p=+n[1],r()):[g,p]},t.center=function(n){return arguments.length?(v=n[0]%360*Oa,d=n[1]%360*Oa,r()):[v*Ia,d*Ia]},t.rotate=function(n){return arguments.length?(m=n[0]%360*Oa,M=n[1]%360*Oa,x=n.length>2?n[2]%360*Oa:0,r()):[m*Ia,M*Ia,x*Ia]},oa.rebind(t,f,"precision"),function(){return i=n.apply(this,arguments),t.invert=i.invert&&e,r()}}function le(n){return ie(n,function(t,e){n.point(t*Oa,e*Oa)})}function ce(n,t){return[n,t]}function se(n,t){return[n>Ua?n-ja:-Ua>n?n+ja:n,t]}function fe(n,t,e){return n?t||e?Ct(ge(n),pe(t,e)):ge(n):t||e?pe(t,e):se}function he(n){return function(t,e){return t+=n,[t>Ua?t-ja:-Ua>t?t+ja:t,e]}}function ge(n){var t=he(n);return t.invert=he(-n),t}function pe(n,t){function e(n,t){var e=Math.cos(t),o=Math.cos(n)*e,l=Math.sin(n)*e,c=Math.sin(t),s=c*r+o*u;return[Math.atan2(l*i-s*a,o*r-c*u),tn(s*i+l*a)]}var r=Math.cos(n),u=Math.sin(n),i=Math.cos(t),a=Math.sin(t);return e.invert=function(n,t){var e=Math.cos(t),o=Math.cos(n)*e,l=Math.sin(n)*e,c=Math.sin(t),s=c*i-l*a;return[Math.atan2(l*i+c*a,o*r+s*u),tn(s*r-o*u)]},e}function ve(n,t){var e=Math.cos(n),r=Math.sin(n);return function(u,i,a,o){var l=a*t;null!=u?(u=de(e,u),i=de(e,i),(a>0?i>u:u>i)&&(u+=a*ja)):(u=n+a*ja,i=n-.5*l);for(var c,s=u;a>0?s>i:i>s;s-=l)o.point((c=_t([e,-r*Math.cos(s),-r*Math.sin(s)]))[0],c[1])}}function de(n,t){var e=dt(t);e[0]-=n,bt(e);var r=nn(-e[1]);return((-e[2]<0?-r:r)+2*Math.PI-Da)%(2*Math.PI)}function me(n,t,e){var r=oa.range(n,t-Da,e).concat(t);return function(n){return r.map(function(t){return[n,t]})}}function ye(n,t,e){var r=oa.range(n,t-Da,e).concat(t);return function(n){return r.map(function(t){return[t,n]})}}function Me(n){return n.source}function xe(n){return n.target}function be(n,t,e,r){var u=Math.cos(t),i=Math.sin(t),a=Math.cos(r),o=Math.sin(r),l=u*Math.cos(n),c=u*Math.sin(n),s=a*Math.cos(e),f=a*Math.sin(e),h=2*Math.asin(Math.sqrt(an(r-t)+u*a*an(e-n))),g=1/Math.sin(h),p=h?function(n){var t=Math.sin(n*=h)*g,e=Math.sin(h-n)*g,r=e*l+t*s,u=e*c+t*f,a=e*i+t*o;return[Math.atan2(u,r)*Ia,Math.atan2(a,Math.sqrt(r*r+u*u))*Ia]}:function(){return[n*Ia,t*Ia]};return p.distance=h,p}function _e(){function n(n,u){var i=Math.sin(u*=Oa),a=Math.cos(u),o=Ma((n*=Oa)-t),l=Math.cos(o);Wo+=Math.atan2(Math.sqrt((o=a*Math.sin(o))*o+(o=r*i-e*a*l)*o),e*i+r*a*l),t=n,e=i,r=a}var t,e,r;Jo.point=function(u,i){t=u*Oa,e=Math.sin(i*=Oa),r=Math.cos(i),Jo.point=n},Jo.lineEnd=function(){Jo.point=Jo.lineEnd=b}}function we(n,t){function e(t,e){var r=Math.cos(t),u=Math.cos(e),i=n(r*u);return[i*u*Math.sin(t),i*Math.sin(e)]}return e.invert=function(n,e){var r=Math.sqrt(n*n+e*e),u=t(r),i=Math.sin(u),a=Math.cos(u);return[Math.atan2(n*i,r*a),Math.asin(r&&e*i/r)]},e}function Se(n,t){function e(n,t){a>0?-Ha+Da>t&&(t=-Ha+Da):t>Ha-Da&&(t=Ha-Da);var e=a/Math.pow(u(t),i);return[e*Math.sin(i*n),a-e*Math.cos(i*n)]}var r=Math.cos(n),u=function(n){return Math.tan(Ua/4+n/2)},i=n===t?Math.sin(n):Math.log(r/Math.cos(t))/Math.log(u(t)/u(n)),a=r*Math.pow(u(n),i)/i;return i?(e.invert=function(n,t){var e=a-t,r=K(i)*Math.sqrt(n*n+e*e);return[Math.atan2(n,e)/i,2*Math.atan(Math.pow(a/r,1/i))-Ha]},e):Ne}function ke(n,t){function e(n,t){var e=i-t;return[e*Math.sin(u*n),i-e*Math.cos(u*n)]}var r=Math.cos(n),u=n===t?Math.sin(n):(r-Math.cos(t))/(t-n),i=r/u+n;return Ma(u)u;u++){for(;r>1&&Q(n[e[r-2]],n[e[r-1]],n[u])<=0;)--r;e[r++]=u}return e.slice(0,r)}function qe(n,t){return n[0]-t[0]||n[1]-t[1]}function Te(n,t,e){return(e[0]-t[0])*(n[1]-t[1])<(e[1]-t[1])*(n[0]-t[0])}function Re(n,t,e,r){var u=n[0],i=e[0],a=t[0]-u,o=r[0]-i,l=n[1],c=e[1],s=t[1]-l,f=r[1]-c,h=(o*(l-c)-f*(u-i))/(f*a-o*s);return[u+h*a,l+h*s]}function De(n){var t=n[0],e=n[n.length-1];return!(t[0]-e[0]||t[1]-e[1])}function Pe(){rr(this),this.edge=this.site=this.circle=null}function Ue(n){var t=ll.pop()||new Pe;return t.site=n,t}function je(n){Be(n),il.remove(n),ll.push(n),rr(n)}function Fe(n){var t=n.circle,e=t.x,r=t.cy,u={x:e,y:r},i=n.P,a=n.N,o=[n];je(n);for(var l=i;l.circle&&Ma(e-l.circle.x)s;++s)c=o[s],l=o[s-1],nr(c.edge,l.site,c.site,u);l=o[0],c=o[f-1],c.edge=Ke(l.site,c.site,null,u),$e(l),$e(c)}function He(n){for(var t,e,r,u,i=n.x,a=n.y,o=il._;o;)if(r=Oe(o,a)-i,r>Da)o=o.L;else{if(u=i-Ie(o,a),!(u>Da)){r>-Da?(t=o.P,e=o):u>-Da?(t=o,e=o.N):t=e=o;break}if(!o.R){t=o;break}o=o.R}var l=Ue(n);if(il.insert(t,l),t||e){if(t===e)return Be(t),e=Ue(t.site),il.insert(l,e),l.edge=e.edge=Ke(t.site,l.site),$e(t),void $e(e);if(!e)return void(l.edge=Ke(t.site,l.site));Be(t),Be(e);var c=t.site,s=c.x,f=c.y,h=n.x-s,g=n.y-f,p=e.site,v=p.x-s,d=p.y-f,m=2*(h*d-g*v),y=h*h+g*g,M=v*v+d*d,x={x:(d*y-g*M)/m+s,y:(h*M-v*y)/m+f};nr(e.edge,c,p,x),l.edge=Ke(c,n,null,x),e.edge=Ke(n,p,null,x),$e(t),$e(e)}}function Oe(n,t){var e=n.site,r=e.x,u=e.y,i=u-t;if(!i)return r;var a=n.P;if(!a)return-(1/0);e=a.site;var o=e.x,l=e.y,c=l-t;if(!c)return o;var s=o-r,f=1/i-1/c,h=s/c;return f?(-h+Math.sqrt(h*h-2*f*(s*s/(-2*c)-l+c/2+u-i/2)))/f+r:(r+o)/2}function Ie(n,t){var e=n.N;if(e)return Oe(e,t);var r=n.site;return r.y===t?r.x:1/0}function Ye(n){this.site=n,this.edges=[]}function Ze(n){for(var t,e,r,u,i,a,o,l,c,s,f=n[0][0],h=n[1][0],g=n[0][1],p=n[1][1],v=ul,d=v.length;d--;)if(i=v[d],i&&i.prepare())for(o=i.edges,l=o.length,a=0;l>a;)s=o[a].end(),r=s.x,u=s.y,c=o[++a%l].start(),t=c.x,e=c.y,(Ma(r-t)>Da||Ma(u-e)>Da)&&(o.splice(a,0,new tr(Qe(i.site,s,Ma(r-f)Da?{x:f,y:Ma(t-f)Da?{x:Ma(e-p)Da?{x:h,y:Ma(t-h)Da?{x:Ma(e-g)=-Pa)){var g=l*l+c*c,p=s*s+f*f,v=(f*g-c*p)/h,d=(l*p-s*g)/h,f=d+o,m=cl.pop()||new Xe;m.arc=n,m.site=u,m.x=v+a,m.y=f+Math.sqrt(v*v+d*d),m.cy=f,n.circle=m;for(var y=null,M=ol._;M;)if(m.yd||d>=o)return;if(h>p){if(i){if(i.y>=c)return}else i={x:d,y:l};e={x:d,y:c}}else{if(i){if(i.yr||r>1)if(h>p){if(i){if(i.y>=c)return}else i={x:(l-u)/r,y:l};e={x:(c-u)/r,y:c}}else{if(i){if(i.yg){if(i){if(i.x>=o)return}else i={x:a,y:r*a+u};e={x:o,y:r*o+u}}else{if(i){if(i.xi||f>a||r>h||u>g)){if(p=n.point){var p,v=t-n.x,d=e-n.y,m=v*v+d*d;if(l>m){var y=Math.sqrt(l=m);r=t-y,u=e-y,i=t+y,a=e+y,o=p}}for(var M=n.nodes,x=.5*(s+h),b=.5*(f+g),_=t>=x,w=e>=b,S=w<<1|_,k=S+4;k>S;++S)if(n=M[3&S])switch(3&S){case 0:c(n,s,f,x,b);break;case 1:c(n,x,f,h,b);break;case 2:c(n,s,b,x,g);break;case 3:c(n,x,b,h,g)}}}(n,r,u,i,a),o}function vr(n,t){n=oa.rgb(n),t=oa.rgb(t);var e=n.r,r=n.g,u=n.b,i=t.r-e,a=t.g-r,o=t.b-u;return function(n){return"#"+bn(Math.round(e+i*n))+bn(Math.round(r+a*n))+bn(Math.round(u+o*n))}}function dr(n,t){var e,r={},u={};for(e in n)e in t?r[e]=Mr(n[e],t[e]):u[e]=n[e];for(e in t)e in n||(u[e]=t[e]);return function(n){for(e in r)u[e]=r[e](n);return u}}function mr(n,t){return n=+n,t=+t,function(e){return n*(1-e)+t*e}}function yr(n,t){var e,r,u,i=fl.lastIndex=hl.lastIndex=0,a=-1,o=[],l=[];for(n+="",t+="";(e=fl.exec(n))&&(r=hl.exec(t));)(u=r.index)>i&&(u=t.slice(i,u),o[a]?o[a]+=u:o[++a]=u),(e=e[0])===(r=r[0])?o[a]?o[a]+=r:o[++a]=r:(o[++a]=null,l.push({i:a,x:mr(e,r)})),i=hl.lastIndex;return ir;++r)o[(e=l[r]).i]=e.x(n);return o.join("")})}function Mr(n,t){for(var e,r=oa.interpolators.length;--r>=0&&!(e=oa.interpolators[r](n,t)););return e}function xr(n,t){var e,r=[],u=[],i=n.length,a=t.length,o=Math.min(n.length,t.length);for(e=0;o>e;++e)r.push(Mr(n[e],t[e]));for(;i>e;++e)u[e]=n[e];for(;a>e;++e)u[e]=t[e];return function(n){for(e=0;o>e;++e)u[e]=r[e](n);return u}}function br(n){return function(t){return 0>=t?0:t>=1?1:n(t)}}function _r(n){return function(t){return 1-n(1-t)}}function wr(n){return function(t){return.5*(.5>t?n(2*t):2-n(2-2*t))}}function Sr(n){return n*n}function kr(n){return n*n*n}function Nr(n){if(0>=n)return 0;if(n>=1)return 1;var t=n*n,e=t*n;return 4*(.5>n?e:3*(n-t)+e-.75)}function Er(n){return function(t){return Math.pow(t,n)}}function Ar(n){return 1-Math.cos(n*Ha)}function Cr(n){return Math.pow(2,10*(n-1))}function zr(n){return 1-Math.sqrt(1-n*n)}function Lr(n,t){var e;return arguments.length<2&&(t=.45),arguments.length?e=t/ja*Math.asin(1/n):(n=1,e=t/4),function(r){return 1+n*Math.pow(2,-10*r)*Math.sin((r-e)*ja/t)}}function qr(n){return n||(n=1.70158),function(t){return t*t*((n+1)*t-n)}}function Tr(n){return 1/2.75>n?7.5625*n*n:2/2.75>n?7.5625*(n-=1.5/2.75)*n+.75:2.5/2.75>n?7.5625*(n-=2.25/2.75)*n+.9375:7.5625*(n-=2.625/2.75)*n+.984375}function Rr(n,t){n=oa.hcl(n),t=oa.hcl(t);var e=n.h,r=n.c,u=n.l,i=t.h-e,a=t.c-r,o=t.l-u;return isNaN(a)&&(a=0,r=isNaN(r)?t.c:r),isNaN(i)?(i=0,e=isNaN(e)?t.h:e):i>180?i-=360:-180>i&&(i+=360),function(n){return fn(e+i*n,r+a*n,u+o*n)+""}}function Dr(n,t){n=oa.hsl(n),t=oa.hsl(t);var e=n.h,r=n.s,u=n.l,i=t.h-e,a=t.s-r,o=t.l-u;return isNaN(a)&&(a=0,r=isNaN(r)?t.s:r),isNaN(i)?(i=0,e=isNaN(e)?t.h:e):i>180?i-=360:-180>i&&(i+=360),function(n){return cn(e+i*n,r+a*n,u+o*n)+""}}function Pr(n,t){n=oa.lab(n),t=oa.lab(t);var e=n.l,r=n.a,u=n.b,i=t.l-e,a=t.a-r,o=t.b-u;return function(n){return gn(e+i*n,r+a*n,u+o*n)+""}}function Ur(n,t){return t-=n,function(e){return Math.round(n+t*e)}}function jr(n){var t=[n.a,n.b],e=[n.c,n.d],r=Hr(t),u=Fr(t,e),i=Hr(Or(e,t,-u))||0;t[0]*e[1]180?t+=360:t-n>180&&(n+=360),r.push({i:e.push(Ir(e)+"rotate(",null,")")-2,x:mr(n,t)})):t&&e.push(Ir(e)+"rotate("+t+")")}function Vr(n,t,e,r){n!==t?r.push({i:e.push(Ir(e)+"skewX(",null,")")-2,x:mr(n,t)}):t&&e.push(Ir(e)+"skewX("+t+")")}function Xr(n,t,e,r){if(n[0]!==t[0]||n[1]!==t[1]){var u=e.push(Ir(e)+"scale(",null,",",null,")");r.push({i:u-4,x:mr(n[0],t[0])},{i:u-2,x:mr(n[1],t[1])})}else(1!==t[0]||1!==t[1])&&e.push(Ir(e)+"scale("+t+")")}function $r(n,t){var e=[],r=[];return n=oa.transform(n),t=oa.transform(t),Yr(n.translate,t.translate,e,r),Zr(n.rotate,t.rotate,e,r),Vr(n.skew,t.skew,e,r),Xr(n.scale,t.scale,e,r),n=t=null,function(n){for(var t,u=-1,i=r.length;++u=0;)e.push(u[r])}function au(n,t){for(var e=[n],r=[];null!=(n=e.pop());)if(r.push(n),(i=n.children)&&(u=i.length))for(var u,i,a=-1;++ae;++e)(t=n[e][1])>u&&(r=e,u=t);return r}function mu(n){return n.reduce(yu,0)}function yu(n,t){return n+t[1]}function Mu(n,t){return xu(n,Math.ceil(Math.log(t.length)/Math.LN2+1))}function xu(n,t){for(var e=-1,r=+n[0],u=(n[1]-r)/t,i=[];++e<=t;)i[e]=u*e+r;return i}function bu(n){return[oa.min(n),oa.max(n)]}function _u(n,t){return n.value-t.value}function wu(n,t){var e=n._pack_next;n._pack_next=t,t._pack_prev=n,t._pack_next=e,e._pack_prev=t}function Su(n,t){n._pack_next=t,t._pack_prev=n}function ku(n,t){var e=t.x-n.x,r=t.y-n.y,u=n.r+t.r;return.999*u*u>e*e+r*r}function Nu(n){function t(n){s=Math.min(n.x-n.r,s),f=Math.max(n.x+n.r,f),h=Math.min(n.y-n.r,h),g=Math.max(n.y+n.r,g)}if((e=n.children)&&(c=e.length)){var e,r,u,i,a,o,l,c,s=1/0,f=-(1/0),h=1/0,g=-(1/0);if(e.forEach(Eu),r=e[0],r.x=-r.r,r.y=0,t(r),c>1&&(u=e[1],u.x=u.r,u.y=0,t(u),c>2))for(i=e[2],zu(r,u,i),t(i),wu(r,i),r._pack_prev=i,wu(i,u),u=r._pack_next,a=3;c>a;a++){zu(r,u,i=e[a]);var p=0,v=1,d=1;for(o=u._pack_next;o!==u;o=o._pack_next,v++)if(ku(o,i)){p=1;break}if(1==p)for(l=r._pack_prev;l!==o._pack_prev&&!ku(l,i);l=l._pack_prev,d++);p?(d>v||v==d&&u.ra;a++)i=e[a],i.x-=m,i.y-=y,M=Math.max(M,i.r+Math.sqrt(i.x*i.x+i.y*i.y));n.r=M,e.forEach(Au)}}function Eu(n){n._pack_next=n._pack_prev=n}function Au(n){delete n._pack_next,delete n._pack_prev}function Cu(n,t,e,r){var u=n.children;if(n.x=t+=r*n.x,n.y=e+=r*n.y,n.r*=r,u)for(var i=-1,a=u.length;++i=0;)t=u[i],t.z+=e,t.m+=e,e+=t.s+(r+=t.c)}function Pu(n,t,e){return n.a.parent===t.parent?n.a:e}function Uu(n){return 1+oa.max(n,function(n){return n.y})}function ju(n){return n.reduce(function(n,t){return n+t.x},0)/n.length}function Fu(n){var t=n.children;return t&&t.length?Fu(t[0]):n}function Hu(n){var t,e=n.children;return e&&(t=e.length)?Hu(e[t-1]):n}function Ou(n){return{x:n.x,y:n.y,dx:n.dx,dy:n.dy}}function Iu(n,t){var e=n.x+t[3],r=n.y+t[0],u=n.dx-t[1]-t[3],i=n.dy-t[0]-t[2];return 0>u&&(e+=u/2,u=0),0>i&&(r+=i/2,i=0),{x:e,y:r,dx:u,dy:i}}function Yu(n){var t=n[0],e=n[n.length-1];return e>t?[t,e]:[e,t]}function Zu(n){return n.rangeExtent?n.rangeExtent():Yu(n.range())}function Vu(n,t,e,r){var u=e(n[0],n[1]),i=r(t[0],t[1]);return function(n){return i(u(n))}}function Xu(n,t){var e,r=0,u=n.length-1,i=n[r],a=n[u];return i>a&&(e=r,r=u,u=e,e=i,i=a,a=e),n[r]=t.floor(i),n[u]=t.ceil(a),n}function $u(n){return n?{floor:function(t){return Math.floor(t/n)*n},ceil:function(t){return Math.ceil(t/n)*n}}:wl}function Bu(n,t,e,r){var u=[],i=[],a=0,o=Math.min(n.length,t.length)-1;for(n[o]2?Bu:Vu,l=r?Wr:Br;return a=u(n,t,l,e),o=u(t,n,l,Mr),i}function i(n){return a(n)}var a,o;return i.invert=function(n){return o(n)},i.domain=function(t){return arguments.length?(n=t.map(Number),u()):n},i.range=function(n){return arguments.length?(t=n,u()):t},i.rangeRound=function(n){return i.range(n).interpolate(Ur)},i.clamp=function(n){return arguments.length?(r=n,u()):r},i.interpolate=function(n){return arguments.length?(e=n,u()):e},i.ticks=function(t){return Qu(n,t)},i.tickFormat=function(t,e){return ni(n,t,e)},i.nice=function(t){return Gu(n,t),u()},i.copy=function(){return Wu(n,t,e,r)},u()}function Ju(n,t){return oa.rebind(n,t,"range","rangeRound","interpolate","clamp")}function Gu(n,t){return Xu(n,$u(Ku(n,t)[2])),Xu(n,$u(Ku(n,t)[2])),n}function Ku(n,t){null==t&&(t=10);var e=Yu(n),r=e[1]-e[0],u=Math.pow(10,Math.floor(Math.log(r/t)/Math.LN10)),i=t/r*u;return.15>=i?u*=10:.35>=i?u*=5:.75>=i&&(u*=2),e[0]=Math.ceil(e[0]/u)*u,e[1]=Math.floor(e[1]/u)*u+.5*u,e[2]=u,e}function Qu(n,t){return oa.range.apply(oa,Ku(n,t))}function ni(n,t,e){var r=Ku(n,t);if(e){var u=so.exec(e);if(u.shift(),"s"===u[8]){var i=oa.formatPrefix(Math.max(Ma(r[0]),Ma(r[1])));return u[7]||(u[7]="."+ti(i.scale(r[2]))),u[8]="f",e=oa.format(u.join("")),function(n){return e(i.scale(n))+i.symbol}}u[7]||(u[7]="."+ei(u[8],r)),e=u.join("")}else e=",."+ti(r[2])+"f";return oa.format(e)}function ti(n){return-Math.floor(Math.log(n)/Math.LN10+.01)}function ei(n,t){var e=ti(t[2]);return n in Sl?Math.abs(e-ti(Math.max(Ma(t[0]),Ma(t[1]))))+ +("e"!==n):e-2*("%"===n)}function ri(n,t,e,r){function u(n){return(e?Math.log(0>n?0:n):-Math.log(n>0?0:-n))/Math.log(t)}function i(n){return e?Math.pow(t,n):-Math.pow(t,-n)}function a(t){return n(u(t))}return a.invert=function(t){return i(n.invert(t))},a.domain=function(t){return arguments.length?(e=t[0]>=0,n.domain((r=t.map(Number)).map(u)),a):r},a.base=function(e){return arguments.length?(t=+e,n.domain(r.map(u)),a):t},a.nice=function(){var t=Xu(r.map(u),e?Math:Nl);return n.domain(t),r=t.map(i),a},a.ticks=function(){var n=Yu(r),a=[],o=n[0],l=n[1],c=Math.floor(u(o)),s=Math.ceil(u(l)),f=t%1?2:t;if(isFinite(s-c)){if(e){for(;s>c;c++)for(var h=1;f>h;h++)a.push(i(c)*h);a.push(i(c))}else for(a.push(i(c));c++0;h--)a.push(i(c)*h);for(c=0;a[c]l;s--);a=a.slice(c,s)}return a},a.tickFormat=function(n,e){if(!arguments.length)return kl;arguments.length<2?e=kl:"function"!=typeof e&&(e=oa.format(e));var r=Math.max(1,t*n/a.ticks().length);return function(n){var a=n/i(Math.round(u(n)));return t-.5>a*t&&(a*=t),r>=a?e(n):""}},a.copy=function(){return ri(n.copy(),t,e,r)},Ju(a,n)}function ui(n,t,e){function r(t){return n(u(t))}var u=ii(t),i=ii(1/t);return r.invert=function(t){return i(n.invert(t))},r.domain=function(t){return arguments.length?(n.domain((e=t.map(Number)).map(u)),r):e},r.ticks=function(n){return Qu(e,n)},r.tickFormat=function(n,t){return ni(e,n,t)},r.nice=function(n){return r.domain(Gu(e,n))},r.exponent=function(a){return arguments.length?(u=ii(t=a),i=ii(1/t),n.domain(e.map(u)),r):t},r.copy=function(){return ui(n.copy(),t,e)},Ju(r,n)}function ii(n){return function(t){return 0>t?-Math.pow(-t,n):Math.pow(t,n)}}function ai(n,t){function e(e){return i[((u.get(e)||("range"===t.t?u.set(e,n.push(e)):NaN))-1)%i.length]}function r(t,e){return oa.range(n.length).map(function(n){return t+e*n})}var u,i,a;return e.domain=function(r){if(!arguments.length)return n;n=[],u=new c;for(var i,a=-1,o=r.length;++ae?[NaN,NaN]:[e>0?o[e-1]:n[0],et?NaN:t/i+n,[t,t+1/i]},r.copy=function(){return li(n,t,e)},u()}function ci(n,t){function e(e){return e>=e?t[oa.bisect(n,e)]:void 0}return e.domain=function(t){return arguments.length?(n=t,e):n},e.range=function(n){return arguments.length?(t=n,e):t},e.invertExtent=function(e){return e=t.indexOf(e),[n[e-1],n[e]]},e.copy=function(){return ci(n,t)},e}function si(n){function t(n){return+n}return t.invert=t,t.domain=t.range=function(e){return arguments.length?(n=e.map(t),t):n},t.ticks=function(t){return Qu(n,t)},t.tickFormat=function(t,e){return ni(n,t,e)},t.copy=function(){return si(n)},t}function fi(){return 0}function hi(n){return n.innerRadius}function gi(n){return n.outerRadius}function pi(n){return n.startAngle}function vi(n){return n.endAngle}function di(n){return n&&n.padAngle}function mi(n,t,e,r){return(n-e)*t-(t-r)*n>0?0:1}function yi(n,t,e,r,u){var i=n[0]-t[0],a=n[1]-t[1],o=(u?r:-r)/Math.sqrt(i*i+a*a),l=o*a,c=-o*i,s=n[0]+l,f=n[1]+c,h=t[0]+l,g=t[1]+c,p=(s+h)/2,v=(f+g)/2,d=h-s,m=g-f,y=d*d+m*m,M=e-r,x=s*g-h*f,b=(0>m?-1:1)*Math.sqrt(Math.max(0,M*M*y-x*x)),_=(x*m-d*b)/y,w=(-x*d-m*b)/y,S=(x*m+d*b)/y,k=(-x*d+m*b)/y,N=_-p,E=w-v,A=S-p,C=k-v;return N*N+E*E>A*A+C*C&&(_=S,w=k),[[_-l,w-c],[_*e/M,w*e/M]]}function Mi(n){function t(t){function a(){c.push("M",i(n(s),o))}for(var l,c=[],s=[],f=-1,h=t.length,g=En(e),p=En(r);++f1?n.join("L"):n+"Z"}function bi(n){return n.join("L")+"Z"}function _i(n){for(var t=0,e=n.length,r=n[0],u=[r[0],",",r[1]];++t1&&u.push("H",r[0]),u.join("")}function wi(n){for(var t=0,e=n.length,r=n[0],u=[r[0],",",r[1]];++t1){o=t[1],i=n[l],l++,r+="C"+(u[0]+a[0])+","+(u[1]+a[1])+","+(i[0]-o[0])+","+(i[1]-o[1])+","+i[0]+","+i[1];for(var c=2;c9&&(u=3*t/Math.sqrt(u),a[o]=u*e,a[o+1]=u*r));for(o=-1;++o<=l;)u=(n[Math.min(l,o+1)][0]-n[Math.max(0,o-1)][0])/(6*(1+a[o]*a[o])),i.push([u||0,a[o]*u||0]);return i}function Fi(n){return n.length<3?xi(n):n[0]+Ai(n,ji(n))}function Hi(n){for(var t,e,r,u=-1,i=n.length;++u=t?a(n-t):void(s.c=a)}function a(e){var u=p.active,i=p[u];i&&(i.timer.c=null,i.timer.t=NaN,--p.count,delete p[u],i.event&&i.event.interrupt.call(n,n.__data__,i.index));for(var a in p)if(r>+a){var c=p[a];c.timer.c=null,c.timer.t=NaN,--p.count,delete p[a]}s.c=o,qn(function(){return s.c&&o(e||1)&&(s.c=null,s.t=NaN),1},0,l),p.active=r,v.event&&v.event.start.call(n,n.__data__,t),g=[],v.tween.forEach(function(e,r){(r=r.call(n,n.__data__,t))&&g.push(r)}),h=v.ease,f=v.duration}function o(u){for(var i=u/f,a=h(i),o=g.length;o>0;)g[--o].call(n,a);return i>=1?(v.event&&v.event.end.call(n,n.__data__,t),--p.count?delete p[r]:delete n[e],1):void 0}var l,s,f,h,g,p=n[e]||(n[e]={active:0,count:0}),v=p[r];v||(l=u.time,s=qn(i,0,l),v=p[r]={tween:new c,time:l,timer:s,delay:u.delay,duration:u.duration,ease:u.ease,index:t},u=null,++p.count)}function na(n,t,e){n.attr("transform",function(n){var r=t(n);return"translate("+(isFinite(r)?r:e(n))+",0)"})}function ta(n,t,e){n.attr("transform",function(n){var r=t(n);return"translate(0,"+(isFinite(r)?r:e(n))+")"})}function ea(n){return n.toISOString()}function ra(n,t,e){function r(t){return n(t)}function u(n,e){var r=n[1]-n[0],u=r/e,i=oa.bisect(Gl,u);return i==Gl.length?[t.year,Ku(n.map(function(n){return n/31536e6}),e)[2]]:i?t[u/Gl[i-1]1?{floor:function(t){for(;e(t=n.floor(t));)t=ua(t-1);return t},ceil:function(t){for(;e(t=n.ceil(t));)t=ua(+t+1);return t}}:n))},r.ticks=function(n,t){var e=Yu(r.domain()),i=null==n?u(e,10):"number"==typeof n?u(e,n):!n.range&&[{range:n},t];return i&&(n=i[0],t=i[1]),n.range(e[0],ua(+e[1]+1),1>t?1:t)},r.tickFormat=function(){return e},r.copy=function(){return ra(n.copy(),t,e)},Ju(r,n)}function ua(n){return new Date(n)}function ia(n){return JSON.parse(n.responseText)}function aa(n){var t=sa.createRange();return t.selectNode(sa.body),t.createContextualFragment(n.responseText)}var oa={version:"3.5.14"},la=[].slice,ca=function(n){return la.call(n)},sa=this.document;if(sa)try{ca(sa.documentElement.childNodes)[0].nodeType}catch(fa){ca=function(n){for(var t=n.length,e=new Array(t);t--;)e[t]=n[t];return e}}if(Date.now||(Date.now=function(){return+new Date}),sa)try{sa.createElement("DIV").style.setProperty("opacity",0,"")}catch(ha){var ga=this.Element.prototype,pa=ga.setAttribute,va=ga.setAttributeNS,da=this.CSSStyleDeclaration.prototype,ma=da.setProperty;ga.setAttribute=function(n,t){pa.call(this,n,t+"")},ga.setAttributeNS=function(n,t,e){va.call(this,n,t,e+"")},da.setProperty=function(n,t,e){ma.call(this,n,t+"",e)}}oa.ascending=e,oa.descending=function(n,t){return n>t?-1:t>n?1:t>=n?0:NaN},oa.min=function(n,t){var e,r,u=-1,i=n.length;if(1===arguments.length){for(;++u=r){e=r;break}for(;++ur&&(e=r)}else{for(;++u=r){e=r;break}for(;++ur&&(e=r)}return e},oa.max=function(n,t){var e,r,u=-1,i=n.length;if(1===arguments.length){for(;++u=r){e=r;break}for(;++ue&&(e=r)}else{for(;++u=r){e=r;break}for(;++ue&&(e=r)}return e},oa.extent=function(n,t){var e,r,u,i=-1,a=n.length;if(1===arguments.length){for(;++i=r){e=u=r;break}for(;++ir&&(e=r),r>u&&(u=r))}else{for(;++i=r){e=u=r;break}for(;++ir&&(e=r),r>u&&(u=r))}return[e,u]},oa.sum=function(n,t){var e,r=0,i=n.length,a=-1;if(1===arguments.length)for(;++a1?l/(s-1):void 0},oa.deviation=function(){var n=oa.variance.apply(this,arguments);return n?Math.sqrt(n):n};var ya=i(e);oa.bisectLeft=ya.left,oa.bisect=oa.bisectRight=ya.right,oa.bisector=function(n){return i(1===n.length?function(t,r){return e(n(t),r)}:n)},oa.shuffle=function(n,t,e){(i=arguments.length)<3&&(e=n.length,2>i&&(t=0));for(var r,u,i=e-t;i;)u=Math.random()*i--|0,r=n[i+t],n[i+t]=n[u+t],n[u+t]=r;return n},oa.permute=function(n,t){for(var e=t.length,r=new Array(e);e--;)r[e]=n[t[e]];return r},oa.pairs=function(n){for(var t,e=0,r=n.length-1,u=n[0],i=new Array(0>r?0:r);r>e;)i[e]=[t=u,u=n[++e]];return i},oa.zip=function(){if(!(r=arguments.length))return[];for(var n=-1,t=oa.min(arguments,a),e=new Array(t);++n=0;)for(r=n[u],t=r.length;--t>=0;)e[--a]=r[t];return e};var Ma=Math.abs;oa.range=function(n,t,e){if(arguments.length<3&&(e=1,arguments.length<2&&(t=n,n=0)),(t-n)/e===1/0)throw new Error("infinite range");var r,u=[],i=o(Ma(e)),a=-1;if(n*=i,t*=i,e*=i,0>e)for(;(r=n+e*++a)>t;)u.push(r/i);else for(;(r=n+e*++a)=i.length)return r?r.call(u,a):e?a.sort(e):a;for(var l,s,f,h,g=-1,p=a.length,v=i[o++],d=new c;++g=i.length)return n;var r=[],u=a[e++];return n.forEach(function(n,u){r.push({key:n,values:t(u,e)})}),u?r.sort(function(n,t){return u(n.key,t.key)}):r}var e,r,u={},i=[],a=[];return u.map=function(t,e){return n(e,t,0)},u.entries=function(e){return t(n(oa.map,e,0),0)},u.key=function(n){return i.push(n),u},u.sortKeys=function(n){return a[i.length-1]=n,u},u.sortValues=function(n){return e=n,u},u.rollup=function(n){return r=n,u},u},oa.set=function(n){var t=new m;if(n)for(var e=0,r=n.length;r>e;++e)t.add(n[e]);return t},l(m,{has:h,add:function(n){return this._[s(n+="")]=!0,n},remove:g,values:p,size:v,empty:d,forEach:function(n){for(var t in this._)n.call(this,f(t))}}),oa.behavior={},oa.rebind=function(n,t){for(var e,r=1,u=arguments.length;++r=0&&(r=n.slice(e+1),n=n.slice(0,e)),n)return arguments.length<2?this[n].on(r):this[n].on(r,t);if(2===arguments.length){if(null==t)for(n in this)this.hasOwnProperty(n)&&this[n].on(r,null);return this}},oa.event=null,oa.requote=function(n){return n.replace(wa,"\\$&")};var wa=/[\\\^\$\*\+\?\|\[\]\(\)\.\{\}]/g,Sa={}.__proto__?function(n,t){n.__proto__=t}:function(n,t){for(var e in t)n[e]=t[e]},ka=function(n,t){return t.querySelector(n)},Na=function(n,t){return t.querySelectorAll(n)},Ea=function(n,t){var e=n.matches||n[x(n,"matchesSelector")];return(Ea=function(n,t){return e.call(n,t)})(n,t)};"function"==typeof Sizzle&&(ka=function(n,t){return Sizzle(n,t)[0]||null},Na=Sizzle,Ea=Sizzle.matchesSelector),oa.selection=function(){return oa.select(sa.documentElement)};var Aa=oa.selection.prototype=[];Aa.select=function(n){var t,e,r,u,i=[];n=A(n);for(var a=-1,o=this.length;++a=0&&"xmlns"!==(e=n.slice(0,t))&&(n=n.slice(t+1)),Ca.hasOwnProperty(e)?{space:Ca[e],local:n}:n}},Aa.attr=function(n,t){if(arguments.length<2){if("string"==typeof n){var e=this.node();return n=oa.ns.qualify(n),n.local?e.getAttributeNS(n.space,n.local):e.getAttribute(n)}for(t in n)this.each(z(t,n[t]));return this}return this.each(z(n,t))},Aa.classed=function(n,t){if(arguments.length<2){if("string"==typeof n){var e=this.node(),r=(n=T(n)).length,u=-1;if(t=e.classList){for(;++uu){if("string"!=typeof n){2>u&&(e="");for(r in n)this.each(P(r,n[r],e));return this}if(2>u){var i=this.node();return t(i).getComputedStyle(i,null).getPropertyValue(n)}r=""}return this.each(P(n,e,r))},Aa.property=function(n,t){if(arguments.length<2){if("string"==typeof n)return this.node()[n];for(t in n)this.each(U(t,n[t]));return this}return this.each(U(n,t))},Aa.text=function(n){return arguments.length?this.each("function"==typeof n?function(){var t=n.apply(this,arguments);this.textContent=null==t?"":t}:null==n?function(){this.textContent=""}:function(){this.textContent=n}):this.node().textContent},Aa.html=function(n){return arguments.length?this.each("function"==typeof n?function(){var t=n.apply(this,arguments);this.innerHTML=null==t?"":t}:null==n?function(){this.innerHTML=""}:function(){this.innerHTML=n}):this.node().innerHTML},Aa.append=function(n){return n=j(n),this.select(function(){return this.appendChild(n.apply(this,arguments))})},Aa.insert=function(n,t){return n=j(n),t=A(t),this.select(function(){return this.insertBefore(n.apply(this,arguments),t.apply(this,arguments)||null)})},Aa.remove=function(){return this.each(F)},Aa.data=function(n,t){function e(n,e){var r,u,i,a=n.length,f=e.length,h=Math.min(a,f),g=new Array(f),p=new Array(f),v=new Array(a);if(t){var d,m=new c,y=new Array(a);for(r=-1;++rr;++r)p[r]=H(e[r]);for(;a>r;++r)v[r]=n[r]}p.update=g,p.parentNode=g.parentNode=v.parentNode=n.parentNode,o.push(p),l.push(g),s.push(v)}var r,u,i=-1,a=this.length;if(!arguments.length){for(n=new Array(a=(r=this[0]).length);++ii;i++){u.push(t=[]),t.parentNode=(e=this[i]).parentNode;for(var o=0,l=e.length;l>o;o++)(r=e[o])&&n.call(r,r.__data__,o,i)&&t.push(r)}return E(u)},Aa.order=function(){for(var n=-1,t=this.length;++n=0;)(e=r[u])&&(i&&i!==e.nextSibling&&i.parentNode.insertBefore(e,i),i=e);return this},Aa.sort=function(n){n=I.apply(this,arguments);for(var t=-1,e=this.length;++tn;n++)for(var e=this[n],r=0,u=e.length;u>r;r++){var i=e[r];if(i)return i}return null},Aa.size=function(){var n=0;return Y(this,function(){++n}),n};var za=[];oa.selection.enter=Z,oa.selection.enter.prototype=za,za.append=Aa.append,za.empty=Aa.empty,za.node=Aa.node,za.call=Aa.call,za.size=Aa.size,za.select=function(n){for(var t,e,r,u,i,a=[],o=-1,l=this.length;++or){if("string"!=typeof n){2>r&&(t=!1);for(e in n)this.each(X(e,n[e],t));return this}if(2>r)return(r=this.node()["__on"+n])&&r._;e=!1}return this.each(X(n,t,e))};var La=oa.map({mouseenter:"mouseover",mouseleave:"mouseout"});sa&&La.forEach(function(n){"on"+n in sa&&La.remove(n)});var qa,Ta=0;oa.mouse=function(n){return J(n,k())};var Ra=this.navigator&&/WebKit/.test(this.navigator.userAgent)?-1:0;oa.touch=function(n,t,e){if(arguments.length<3&&(e=t,t=k().changedTouches),t)for(var r,u=0,i=t.length;i>u;++u)if((r=t[u]).identifier===e)return J(n,r)},oa.behavior.drag=function(){function n(){this.on("mousedown.drag",i).on("touchstart.drag",a)}function e(n,t,e,i,a){return function(){function o(){var n,e,r=t(h,v);r&&(n=r[0]-M[0],e=r[1]-M[1],p|=n|e,M=r,g({type:"drag",x:r[0]+c[0],y:r[1]+c[1],dx:n,dy:e}))}function l(){t(h,v)&&(m.on(i+d,null).on(a+d,null),y(p),g({type:"dragend"}))}var c,s=this,f=oa.event.target,h=s.parentNode,g=r.of(s,arguments),p=0,v=n(),d=".drag"+(null==v?"":"-"+v),m=oa.select(e(f)).on(i+d,o).on(a+d,l),y=W(f),M=t(h,v);u?(c=u.apply(s,arguments),c=[c.x-M[0],c.y-M[1]]):c=[0,0],g({type:"dragstart"})}}var r=N(n,"drag","dragstart","dragend"),u=null,i=e(b,oa.mouse,t,"mousemove","mouseup"),a=e(G,oa.touch,y,"touchmove","touchend");return n.origin=function(t){return arguments.length?(u=t,n):u},oa.rebind(n,r,"on")},oa.touches=function(n,t){return arguments.length<2&&(t=k().touches),t?ca(t).map(function(t){var e=J(n,t);return e.identifier=t.identifier,e}):[]};var Da=1e-6,Pa=Da*Da,Ua=Math.PI,ja=2*Ua,Fa=ja-Da,Ha=Ua/2,Oa=Ua/180,Ia=180/Ua,Ya=Math.SQRT2,Za=2,Va=4;oa.interpolateZoom=function(n,t){var e,r,u=n[0],i=n[1],a=n[2],o=t[0],l=t[1],c=t[2],s=o-u,f=l-i,h=s*s+f*f;if(Pa>h)r=Math.log(c/a)/Ya,e=function(n){return[u+n*s,i+n*f,a*Math.exp(Ya*n*r)]};else{var g=Math.sqrt(h),p=(c*c-a*a+Va*h)/(2*a*Za*g),v=(c*c-a*a-Va*h)/(2*c*Za*g),d=Math.log(Math.sqrt(p*p+1)-p),m=Math.log(Math.sqrt(v*v+1)-v);r=(m-d)/Ya,e=function(n){var t=n*r,e=rn(d),o=a/(Za*g)*(e*un(Ya*t+d)-en(d));return[u+o*s,i+o*f,a*e/rn(Ya*t+d)]}}return e.duration=1e3*r,e},oa.behavior.zoom=function(){function n(n){n.on(L,f).on($a+".zoom",g).on("dblclick.zoom",p).on(R,h)}function e(n){return[(n[0]-k.x)/k.k,(n[1]-k.y)/k.k]}function r(n){return[n[0]*k.k+k.x,n[1]*k.k+k.y]}function u(n){k.k=Math.max(A[0],Math.min(A[1],n))}function i(n,t){t=r(t),k.x+=n[0]-t[0],k.y+=n[1]-t[1]}function a(t,e,r,a){t.__chart__={x:k.x,y:k.y,k:k.k},u(Math.pow(2,a)),i(d=e,r),t=oa.select(t),C>0&&(t=t.transition().duration(C)),t.call(n.event)}function o(){b&&b.domain(x.range().map(function(n){return(n-k.x)/k.k}).map(x.invert)),w&&w.domain(_.range().map(function(n){return(n-k.y)/k.k}).map(_.invert))}function l(n){z++||n({type:"zoomstart"})}function c(n){o(),n({type:"zoom",scale:k.k,translate:[k.x,k.y]})}function s(n){--z||(n({type:"zoomend"}),d=null)}function f(){function n(){o=1,i(oa.mouse(u),h),c(a)}function r(){f.on(q,null).on(T,null),g(o),s(a)}var u=this,a=D.of(u,arguments),o=0,f=oa.select(t(u)).on(q,n).on(T,r),h=e(oa.mouse(u)),g=W(u);Ol.call(u),l(a)}function h(){function n(){var n=oa.touches(p);return g=k.k,n.forEach(function(n){n.identifier in d&&(d[n.identifier]=e(n))}),n}function t(){var t=oa.event.target;oa.select(t).on(x,r).on(b,o),_.push(t);for(var e=oa.event.changedTouches,u=0,i=e.length;i>u;++u)d[e[u].identifier]=null;var l=n(),c=Date.now();if(1===l.length){if(500>c-M){var s=l[0];a(p,s,d[s.identifier],Math.floor(Math.log(k.k)/Math.LN2)+1),S()}M=c}else if(l.length>1){var s=l[0],f=l[1],h=s[0]-f[0],g=s[1]-f[1];m=h*h+g*g}}function r(){var n,t,e,r,a=oa.touches(p);Ol.call(p);for(var o=0,l=a.length;l>o;++o,r=null)if(e=a[o],r=d[e.identifier]){if(t)break;n=e,t=r}if(r){var s=(s=e[0]-n[0])*s+(s=e[1]-n[1])*s,f=m&&Math.sqrt(s/m);n=[(n[0]+e[0])/2,(n[1]+e[1])/2],t=[(t[0]+r[0])/2,(t[1]+r[1])/2],u(f*g)}M=null,i(n,t),c(v)}function o(){if(oa.event.touches.length){for(var t=oa.event.changedTouches,e=0,r=t.length;r>e;++e)delete d[t[e].identifier];for(var u in d)return void n()}oa.selectAll(_).on(y,null),w.on(L,f).on(R,h),N(),s(v)}var g,p=this,v=D.of(p,arguments),d={},m=0,y=".zoom-"+oa.event.changedTouches[0].identifier,x="touchmove"+y,b="touchend"+y,_=[],w=oa.select(p),N=W(p);t(),l(v),w.on(L,null).on(R,t)}function g(){var n=D.of(this,arguments);y?clearTimeout(y):(Ol.call(this),v=e(d=m||oa.mouse(this)),l(n)),y=setTimeout(function(){y=null,s(n)},50),S(),u(Math.pow(2,.002*Xa())*k.k),i(d,v),c(n)}function p(){var n=oa.mouse(this),t=Math.log(k.k)/Math.LN2;a(this,n,e(n),oa.event.shiftKey?Math.ceil(t)-1:Math.floor(t)+1)}var v,d,m,y,M,x,b,_,w,k={x:0,y:0,k:1},E=[960,500],A=Ba,C=250,z=0,L="mousedown.zoom",q="mousemove.zoom",T="mouseup.zoom",R="touchstart.zoom",D=N(n,"zoomstart","zoom","zoomend");return $a||($a="onwheel"in sa?(Xa=function(){return-oa.event.deltaY*(oa.event.deltaMode?120:1)},"wheel"):"onmousewheel"in sa?(Xa=function(){return oa.event.wheelDelta},"mousewheel"):(Xa=function(){return-oa.event.detail},"MozMousePixelScroll")),n.event=function(n){n.each(function(){var n=D.of(this,arguments),t=k;Fl?oa.select(this).transition().each("start.zoom",function(){k=this.__chart__||{x:0,y:0,k:1},l(n)}).tween("zoom:zoom",function(){var e=E[0],r=E[1],u=d?d[0]:e/2,i=d?d[1]:r/2,a=oa.interpolateZoom([(u-k.x)/k.k,(i-k.y)/k.k,e/k.k],[(u-t.x)/t.k,(i-t.y)/t.k,e/t.k]);return function(t){var r=a(t),o=e/r[2];this.__chart__=k={x:u-r[0]*o,y:i-r[1]*o,k:o},c(n)}}).each("interrupt.zoom",function(){s(n)}).each("end.zoom",function(){s(n)}):(this.__chart__=k,l(n),c(n),s(n))})},n.translate=function(t){return arguments.length?(k={x:+t[0],y:+t[1],k:k.k},o(),n):[k.x,k.y]},n.scale=function(t){return arguments.length?(k={x:k.x,y:k.y,k:null},u(+t),o(),n):k.k},n.scaleExtent=function(t){return arguments.length?(A=null==t?Ba:[+t[0],+t[1]],n):A},n.center=function(t){return arguments.length?(m=t&&[+t[0],+t[1]],n):m},n.size=function(t){return arguments.length?(E=t&&[+t[0],+t[1]],n):E},n.duration=function(t){return arguments.length?(C=+t,n):C},n.x=function(t){return arguments.length?(b=t,x=t.copy(),k={x:0,y:0,k:1},n):b},n.y=function(t){return arguments.length?(w=t,_=t.copy(),k={x:0,y:0,k:1},n):w},oa.rebind(n,D,"on")};var Xa,$a,Ba=[0,1/0];oa.color=on,on.prototype.toString=function(){return this.rgb()+""},oa.hsl=ln;var Wa=ln.prototype=new on;Wa.brighter=function(n){return n=Math.pow(.7,arguments.length?n:1),new ln(this.h,this.s,this.l/n)},Wa.darker=function(n){return n=Math.pow(.7,arguments.length?n:1),new ln(this.h,this.s,n*this.l)},Wa.rgb=function(){return cn(this.h,this.s,this.l)},oa.hcl=sn;var Ja=sn.prototype=new on;Ja.brighter=function(n){return new sn(this.h,this.c,Math.min(100,this.l+Ga*(arguments.length?n:1)))},Ja.darker=function(n){return new sn(this.h,this.c,Math.max(0,this.l-Ga*(arguments.length?n:1)))},Ja.rgb=function(){return fn(this.h,this.c,this.l).rgb()},oa.lab=hn;var Ga=18,Ka=.95047,Qa=1,no=1.08883,to=hn.prototype=new on;to.brighter=function(n){return new hn(Math.min(100,this.l+Ga*(arguments.length?n:1)),this.a,this.b)},to.darker=function(n){return new hn(Math.max(0,this.l-Ga*(arguments.length?n:1)),this.a,this.b)},to.rgb=function(){return gn(this.l,this.a,this.b)},oa.rgb=yn;var eo=yn.prototype=new on;eo.brighter=function(n){n=Math.pow(.7,arguments.length?n:1);var t=this.r,e=this.g,r=this.b,u=30;return t||e||r?(t&&u>t&&(t=u),e&&u>e&&(e=u),r&&u>r&&(r=u),new yn(Math.min(255,t/n),Math.min(255,e/n),Math.min(255,r/n))):new yn(u,u,u)},eo.darker=function(n){return n=Math.pow(.7,arguments.length?n:1),new yn(n*this.r,n*this.g,n*this.b)},eo.hsl=function(){return wn(this.r,this.g,this.b)},eo.toString=function(){return"#"+bn(this.r)+bn(this.g)+bn(this.b)};var ro=oa.map({aliceblue:15792383,antiquewhite:16444375,aqua:65535,aquamarine:8388564,azure:15794175,beige:16119260,bisque:16770244,black:0,blanchedalmond:16772045,blue:255,blueviolet:9055202,brown:10824234,burlywood:14596231,cadetblue:6266528,chartreuse:8388352,chocolate:13789470,coral:16744272,cornflowerblue:6591981,cornsilk:16775388,crimson:14423100,cyan:65535,darkblue:139,darkcyan:35723,darkgoldenrod:12092939,darkgray:11119017,darkgreen:25600,darkgrey:11119017,darkkhaki:12433259,darkmagenta:9109643,darkolivegreen:5597999,darkorange:16747520,darkorchid:10040012,darkred:9109504,darksalmon:15308410,darkseagreen:9419919,darkslateblue:4734347,darkslategray:3100495,darkslategrey:3100495,darkturquoise:52945,darkviolet:9699539,deeppink:16716947,deepskyblue:49151,dimgray:6908265,dimgrey:6908265,dodgerblue:2003199,firebrick:11674146,floralwhite:16775920,forestgreen:2263842,fuchsia:16711935,gainsboro:14474460,ghostwhite:16316671,gold:16766720,goldenrod:14329120,gray:8421504,green:32768,greenyellow:11403055,grey:8421504,honeydew:15794160,hotpink:16738740,indianred:13458524,indigo:4915330,ivory:16777200,khaki:15787660,lavender:15132410,lavenderblush:16773365,lawngreen:8190976,lemonchiffon:16775885,lightblue:11393254,lightcoral:15761536,lightcyan:14745599,lightgoldenrodyellow:16448210,lightgray:13882323,lightgreen:9498256,lightgrey:13882323,lightpink:16758465,lightsalmon:16752762,lightseagreen:2142890,lightskyblue:8900346,lightslategray:7833753,lightslategrey:7833753,lightsteelblue:11584734,lightyellow:16777184,lime:65280,limegreen:3329330,linen:16445670,magenta:16711935,maroon:8388608,mediumaquamarine:6737322,mediumblue:205,mediumorchid:12211667,mediumpurple:9662683,mediumseagreen:3978097,mediumslateblue:8087790,mediumspringgreen:64154,mediumturquoise:4772300,mediumvioletred:13047173,midnightblue:1644912,mintcream:16121850,mistyrose:16770273,moccasin:16770229,navajowhite:16768685,navy:128,oldlace:16643558,olive:8421376,olivedrab:7048739,orange:16753920,orangered:16729344,orchid:14315734,palegoldenrod:15657130,palegreen:10025880,paleturquoise:11529966,palevioletred:14381203,papayawhip:16773077,peachpuff:16767673,peru:13468991,pink:16761035,plum:14524637,powderblue:11591910,purple:8388736,rebeccapurple:6697881,red:16711680,rosybrown:12357519,royalblue:4286945,saddlebrown:9127187,salmon:16416882,sandybrown:16032864,seagreen:3050327,seashell:16774638,sienna:10506797,silver:12632256,skyblue:8900331,slateblue:6970061,slategray:7372944,slategrey:7372944,snow:16775930,springgreen:65407,steelblue:4620980,tan:13808780,teal:32896,thistle:14204888,tomato:16737095,turquoise:4251856,violet:15631086,wheat:16113331,white:16777215,whitesmoke:16119285,yellow:16776960,yellowgreen:10145074});ro.forEach(function(n,t){ro.set(n,Mn(t))}),oa.functor=En,oa.xhr=An(y),oa.dsv=function(n,t){function e(n,e,i){arguments.length<3&&(i=e,e=null);var a=Cn(n,t,null==e?r:u(e),i);return a.row=function(n){return arguments.length?a.response(null==(e=n)?r:u(n)):e},a}function r(n){return e.parse(n.responseText)}function u(n){return function(t){return e.parse(t.responseText,n)}}function i(t){return t.map(a).join(n)}function a(n){return o.test(n)?'"'+n.replace(/\"/g,'""')+'"':n}var o=new RegExp('["'+n+"\n]"),l=n.charCodeAt(0);return e.parse=function(n,t){var r;return e.parseRows(n,function(n,e){if(r)return r(n,e-1);var u=new Function("d","return {"+n.map(function(n,t){return JSON.stringify(n)+": d["+t+"]"}).join(",")+"}");r=t?function(n,e){return t(u(n),e)}:u})},e.parseRows=function(n,t){function e(){if(s>=c)return a;if(u)return u=!1,i;var t=s;if(34===n.charCodeAt(t)){for(var e=t;e++s;){var r=n.charCodeAt(s++),o=1;if(10===r)u=!0;else if(13===r)u=!0,10===n.charCodeAt(s)&&(++s,++o);else if(r!==l)continue;return n.slice(t,s-o)}return n.slice(t)}for(var r,u,i={},a={},o=[],c=n.length,s=0,f=0;(r=e())!==a;){for(var h=[];r!==i&&r!==a;)h.push(r),r=e();t&&null==(h=t(h,f++))||o.push(h)}return o},e.format=function(t){if(Array.isArray(t[0]))return e.formatRows(t);var r=new m,u=[];return t.forEach(function(n){for(var t in n)r.has(t)||u.push(r.add(t))}),[u.map(a).join(n)].concat(t.map(function(t){return u.map(function(n){return a(t[n])}).join(n)})).join("\n")},e.formatRows=function(n){return n.map(i).join("\n")},e},oa.csv=oa.dsv(",","text/csv"),oa.tsv=oa.dsv(" ","text/tab-separated-values");var uo,io,ao,oo,lo=this[x(this,"requestAnimationFrame")]||function(n){setTimeout(n,17)};oa.timer=function(){qn.apply(this,arguments)},oa.timer.flush=function(){Rn(),Dn()},oa.round=function(n,t){return t?Math.round(n*(t=Math.pow(10,t)))/t:Math.round(n)};var co=["y","z","a","f","p","n","\xb5","m","","k","M","G","T","P","E","Z","Y"].map(Un);oa.formatPrefix=function(n,t){var e=0;return(n=+n)&&(0>n&&(n*=-1),t&&(n=oa.round(n,Pn(n,t))),e=1+Math.floor(1e-12+Math.log(n)/Math.LN10),e=Math.max(-24,Math.min(24,3*Math.floor((e-1)/3)))),co[8+e/3]};var so=/(?:([^{])?([<>=^]))?([+\- ])?([$#])?(0)?(\d+)?(,)?(\.-?\d+)?([a-z%])?/i,fo=oa.map({b:function(n){return n.toString(2)},c:function(n){return String.fromCharCode(n)},o:function(n){return n.toString(8)},x:function(n){return n.toString(16)},X:function(n){return n.toString(16).toUpperCase()},g:function(n,t){return n.toPrecision(t)},e:function(n,t){return n.toExponential(t)},f:function(n,t){return n.toFixed(t)},r:function(n,t){return(n=oa.round(n,Pn(n,t))).toFixed(Math.max(0,Math.min(20,Pn(n*(1+1e-15),t))))}}),ho=oa.time={},go=Date;Hn.prototype={getDate:function(){return this._.getUTCDate()},getDay:function(){return this._.getUTCDay()},getFullYear:function(){return this._.getUTCFullYear()},getHours:function(){return this._.getUTCHours()},getMilliseconds:function(){return this._.getUTCMilliseconds()},getMinutes:function(){return this._.getUTCMinutes()},getMonth:function(){return this._.getUTCMonth()},getSeconds:function(){return this._.getUTCSeconds()},getTime:function(){return this._.getTime()},getTimezoneOffset:function(){return 0},valueOf:function(){return this._.valueOf()},setDate:function(){po.setUTCDate.apply(this._,arguments)},setDay:function(){po.setUTCDay.apply(this._,arguments)},setFullYear:function(){po.setUTCFullYear.apply(this._,arguments)},setHours:function(){po.setUTCHours.apply(this._,arguments)},setMilliseconds:function(){po.setUTCMilliseconds.apply(this._,arguments)},setMinutes:function(){po.setUTCMinutes.apply(this._,arguments)},setMonth:function(){po.setUTCMonth.apply(this._,arguments)},setSeconds:function(){po.setUTCSeconds.apply(this._,arguments)},setTime:function(){po.setTime.apply(this._,arguments)}};var po=Date.prototype;ho.year=On(function(n){return n=ho.day(n),n.setMonth(0,1),n},function(n,t){n.setFullYear(n.getFullYear()+t)},function(n){return n.getFullYear()}),ho.years=ho.year.range,ho.years.utc=ho.year.utc.range,ho.day=On(function(n){var t=new go(2e3,0);return t.setFullYear(n.getFullYear(),n.getMonth(),n.getDate()),t},function(n,t){n.setDate(n.getDate()+t)},function(n){return n.getDate()-1}),ho.days=ho.day.range,ho.days.utc=ho.day.utc.range,ho.dayOfYear=function(n){var t=ho.year(n);return Math.floor((n-t-6e4*(n.getTimezoneOffset()-t.getTimezoneOffset()))/864e5)},["sunday","monday","tuesday","wednesday","thursday","friday","saturday"].forEach(function(n,t){t=7-t;var e=ho[n]=On(function(n){return(n=ho.day(n)).setDate(n.getDate()-(n.getDay()+t)%7),n},function(n,t){n.setDate(n.getDate()+7*Math.floor(t))},function(n){var e=ho.year(n).getDay();return Math.floor((ho.dayOfYear(n)+(e+t)%7)/7)-(e!==t)});ho[n+"s"]=e.range,ho[n+"s"].utc=e.utc.range,ho[n+"OfYear"]=function(n){var e=ho.year(n).getDay();return Math.floor((ho.dayOfYear(n)+(e+t)%7)/7)}}),ho.week=ho.sunday,ho.weeks=ho.sunday.range,ho.weeks.utc=ho.sunday.utc.range,ho.weekOfYear=ho.sundayOfYear;var vo={"-":"",_:" ",0:"0"},mo=/^\s*\d+/,yo=/^%/;oa.locale=function(n){return{numberFormat:jn(n),timeFormat:Yn(n)}};var Mo=oa.locale({decimal:".",thousands:",",grouping:[3],currency:["$",""],dateTime:"%a %b %e %X %Y",date:"%m/%d/%Y",time:"%H:%M:%S",periods:["AM","PM"],days:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"], +shortDays:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],months:["January","February","March","April","May","June","July","August","September","October","November","December"],shortMonths:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"]});oa.format=Mo.numberFormat,oa.geo={},st.prototype={s:0,t:0,add:function(n){ft(n,this.t,xo),ft(xo.s,this.s,this),this.s?this.t+=xo.t:this.s=xo.t},reset:function(){this.s=this.t=0},valueOf:function(){return this.s}};var xo=new st;oa.geo.stream=function(n,t){n&&bo.hasOwnProperty(n.type)?bo[n.type](n,t):ht(n,t)};var bo={Feature:function(n,t){ht(n.geometry,t)},FeatureCollection:function(n,t){for(var e=n.features,r=-1,u=e.length;++rn?4*Ua+n:n,ko.lineStart=ko.lineEnd=ko.point=b}};oa.geo.bounds=function(){function n(n,t){M.push(x=[s=n,h=n]),f>t&&(f=t),t>g&&(g=t)}function t(t,e){var r=dt([t*Oa,e*Oa]);if(m){var u=yt(m,r),i=[u[1],-u[0],0],a=yt(i,u);bt(a),a=_t(a);var l=t-p,c=l>0?1:-1,v=a[0]*Ia*c,d=Ma(l)>180;if(d^(v>c*p&&c*t>v)){var y=a[1]*Ia;y>g&&(g=y)}else if(v=(v+360)%360-180,d^(v>c*p&&c*t>v)){var y=-a[1]*Ia;f>y&&(f=y)}else f>e&&(f=e),e>g&&(g=e);d?p>t?o(s,t)>o(s,h)&&(h=t):o(t,h)>o(s,h)&&(s=t):h>=s?(s>t&&(s=t),t>h&&(h=t)):t>p?o(s,t)>o(s,h)&&(h=t):o(t,h)>o(s,h)&&(s=t)}else n(t,e);m=r,p=t}function e(){b.point=t}function r(){x[0]=s,x[1]=h,b.point=n,m=null}function u(n,e){if(m){var r=n-p;y+=Ma(r)>180?r+(r>0?360:-360):r}else v=n,d=e;ko.point(n,e),t(n,e)}function i(){ko.lineStart()}function a(){u(v,d),ko.lineEnd(),Ma(y)>Da&&(s=-(h=180)),x[0]=s,x[1]=h,m=null}function o(n,t){return(t-=n)<0?t+360:t}function l(n,t){return n[0]-t[0]}function c(n,t){return t[0]<=t[1]?t[0]<=n&&n<=t[1]:nSo?(s=-(h=180),f=-(g=90)):y>Da?g=90:-Da>y&&(f=-90),x[0]=s,x[1]=h}};return function(n){g=h=-(s=f=1/0),M=[],oa.geo.stream(n,b);var t=M.length;if(t){M.sort(l);for(var e,r=1,u=M[0],i=[u];t>r;++r)e=M[r],c(e[0],u)||c(e[1],u)?(o(u[0],e[1])>o(u[0],u[1])&&(u[1]=e[1]),o(e[0],u[1])>o(u[0],u[1])&&(u[0]=e[0])):i.push(u=e);for(var a,e,p=-(1/0),t=i.length-1,r=0,u=i[t];t>=r;u=e,++r)e=i[r],(a=o(u[1],e[0]))>p&&(p=a,s=e[0],h=u[1])}return M=x=null,s===1/0||f===1/0?[[NaN,NaN],[NaN,NaN]]:[[s,f],[h,g]]}}(),oa.geo.centroid=function(n){No=Eo=Ao=Co=zo=Lo=qo=To=Ro=Do=Po=0,oa.geo.stream(n,Uo);var t=Ro,e=Do,r=Po,u=t*t+e*e+r*r;return Pa>u&&(t=Lo,e=qo,r=To,Da>Eo&&(t=Ao,e=Co,r=zo),u=t*t+e*e+r*r,Pa>u)?[NaN,NaN]:[Math.atan2(e,t)*Ia,tn(r/Math.sqrt(u))*Ia]};var No,Eo,Ao,Co,zo,Lo,qo,To,Ro,Do,Po,Uo={sphere:b,point:St,lineStart:Nt,lineEnd:Et,polygonStart:function(){Uo.lineStart=At},polygonEnd:function(){Uo.lineStart=Nt}},jo=Rt(zt,jt,Ht,[-Ua,-Ua/2]),Fo=1e9;oa.geo.clipExtent=function(){var n,t,e,r,u,i,a={stream:function(n){return u&&(u.valid=!1),u=i(n),u.valid=!0,u},extent:function(o){return arguments.length?(i=Zt(n=+o[0][0],t=+o[0][1],e=+o[1][0],r=+o[1][1]),u&&(u.valid=!1,u=null),a):[[n,t],[e,r]]}};return a.extent([[0,0],[960,500]])},(oa.geo.conicEqualArea=function(){return Vt(Xt)}).raw=Xt,oa.geo.albers=function(){return oa.geo.conicEqualArea().rotate([96,0]).center([-.6,38.7]).parallels([29.5,45.5]).scale(1070)},oa.geo.albersUsa=function(){function n(n){var i=n[0],a=n[1];return t=null,e(i,a),t||(r(i,a),t)||u(i,a),t}var t,e,r,u,i=oa.geo.albers(),a=oa.geo.conicEqualArea().rotate([154,0]).center([-2,58.5]).parallels([55,65]),o=oa.geo.conicEqualArea().rotate([157,0]).center([-3,19.9]).parallels([8,18]),l={point:function(n,e){t=[n,e]}};return n.invert=function(n){var t=i.scale(),e=i.translate(),r=(n[0]-e[0])/t,u=(n[1]-e[1])/t;return(u>=.12&&.234>u&&r>=-.425&&-.214>r?a:u>=.166&&.234>u&&r>=-.214&&-.115>r?o:i).invert(n)},n.stream=function(n){var t=i.stream(n),e=a.stream(n),r=o.stream(n);return{point:function(n,u){t.point(n,u),e.point(n,u),r.point(n,u)},sphere:function(){t.sphere(),e.sphere(),r.sphere()},lineStart:function(){t.lineStart(),e.lineStart(),r.lineStart()},lineEnd:function(){t.lineEnd(),e.lineEnd(),r.lineEnd()},polygonStart:function(){t.polygonStart(),e.polygonStart(),r.polygonStart()},polygonEnd:function(){t.polygonEnd(),e.polygonEnd(),r.polygonEnd()}}},n.precision=function(t){return arguments.length?(i.precision(t),a.precision(t),o.precision(t),n):i.precision()},n.scale=function(t){return arguments.length?(i.scale(t),a.scale(.35*t),o.scale(t),n.translate(i.translate())):i.scale()},n.translate=function(t){if(!arguments.length)return i.translate();var c=i.scale(),s=+t[0],f=+t[1];return e=i.translate(t).clipExtent([[s-.455*c,f-.238*c],[s+.455*c,f+.238*c]]).stream(l).point,r=a.translate([s-.307*c,f+.201*c]).clipExtent([[s-.425*c+Da,f+.12*c+Da],[s-.214*c-Da,f+.234*c-Da]]).stream(l).point,u=o.translate([s-.205*c,f+.212*c]).clipExtent([[s-.214*c+Da,f+.166*c+Da],[s-.115*c-Da,f+.234*c-Da]]).stream(l).point,n},n.scale(1070)};var Ho,Oo,Io,Yo,Zo,Vo,Xo={point:b,lineStart:b,lineEnd:b,polygonStart:function(){Oo=0,Xo.lineStart=$t},polygonEnd:function(){Xo.lineStart=Xo.lineEnd=Xo.point=b,Ho+=Ma(Oo/2)}},$o={point:Bt,lineStart:b,lineEnd:b,polygonStart:b,polygonEnd:b},Bo={point:Gt,lineStart:Kt,lineEnd:Qt,polygonStart:function(){Bo.lineStart=ne},polygonEnd:function(){Bo.point=Gt,Bo.lineStart=Kt,Bo.lineEnd=Qt}};oa.geo.path=function(){function n(n){return n&&("function"==typeof o&&i.pointRadius(+o.apply(this,arguments)),a&&a.valid||(a=u(i)),oa.geo.stream(n,a)),i.result()}function t(){return a=null,n}var e,r,u,i,a,o=4.5;return n.area=function(n){return Ho=0,oa.geo.stream(n,u(Xo)),Ho},n.centroid=function(n){return Ao=Co=zo=Lo=qo=To=Ro=Do=Po=0,oa.geo.stream(n,u(Bo)),Po?[Ro/Po,Do/Po]:To?[Lo/To,qo/To]:zo?[Ao/zo,Co/zo]:[NaN,NaN]},n.bounds=function(n){return Zo=Vo=-(Io=Yo=1/0),oa.geo.stream(n,u($o)),[[Io,Yo],[Zo,Vo]]},n.projection=function(n){return arguments.length?(u=(e=n)?n.stream||re(n):y,t()):e},n.context=function(n){return arguments.length?(i=null==(r=n)?new Wt:new te(n),"function"!=typeof o&&i.pointRadius(o),t()):r},n.pointRadius=function(t){return arguments.length?(o="function"==typeof t?t:(i.pointRadius(+t),+t),n):o},n.projection(oa.geo.albersUsa()).context(null)},oa.geo.transform=function(n){return{stream:function(t){var e=new ue(t);for(var r in n)e[r]=n[r];return e}}},ue.prototype={point:function(n,t){this.stream.point(n,t)},sphere:function(){this.stream.sphere()},lineStart:function(){this.stream.lineStart()},lineEnd:function(){this.stream.lineEnd()},polygonStart:function(){this.stream.polygonStart()},polygonEnd:function(){this.stream.polygonEnd()}},oa.geo.projection=ae,oa.geo.projectionMutator=oe,(oa.geo.equirectangular=function(){return ae(ce)}).raw=ce.invert=ce,oa.geo.rotation=function(n){function t(t){return t=n(t[0]*Oa,t[1]*Oa),t[0]*=Ia,t[1]*=Ia,t}return n=fe(n[0]%360*Oa,n[1]*Oa,n.length>2?n[2]*Oa:0),t.invert=function(t){return t=n.invert(t[0]*Oa,t[1]*Oa),t[0]*=Ia,t[1]*=Ia,t},t},se.invert=ce,oa.geo.circle=function(){function n(){var n="function"==typeof r?r.apply(this,arguments):r,t=fe(-n[0]*Oa,-n[1]*Oa,0).invert,u=[];return e(null,null,1,{point:function(n,e){u.push(n=t(n,e)),n[0]*=Ia,n[1]*=Ia}}),{type:"Polygon",coordinates:[u]}}var t,e,r=[0,0],u=6;return n.origin=function(t){return arguments.length?(r=t,n):r},n.angle=function(r){return arguments.length?(e=ve((t=+r)*Oa,u*Oa),n):t},n.precision=function(r){return arguments.length?(e=ve(t*Oa,(u=+r)*Oa),n):u},n.angle(90)},oa.geo.distance=function(n,t){var e,r=(t[0]-n[0])*Oa,u=n[1]*Oa,i=t[1]*Oa,a=Math.sin(r),o=Math.cos(r),l=Math.sin(u),c=Math.cos(u),s=Math.sin(i),f=Math.cos(i);return Math.atan2(Math.sqrt((e=f*a)*e+(e=c*s-l*f*o)*e),l*s+c*f*o)},oa.geo.graticule=function(){function n(){return{type:"MultiLineString",coordinates:t()}}function t(){return oa.range(Math.ceil(i/d)*d,u,d).map(h).concat(oa.range(Math.ceil(c/m)*m,l,m).map(g)).concat(oa.range(Math.ceil(r/p)*p,e,p).filter(function(n){return Ma(n%d)>Da}).map(s)).concat(oa.range(Math.ceil(o/v)*v,a,v).filter(function(n){return Ma(n%m)>Da}).map(f))}var e,r,u,i,a,o,l,c,s,f,h,g,p=10,v=p,d=90,m=360,y=2.5;return n.lines=function(){return t().map(function(n){return{type:"LineString",coordinates:n}})},n.outline=function(){return{type:"Polygon",coordinates:[h(i).concat(g(l).slice(1),h(u).reverse().slice(1),g(c).reverse().slice(1))]}},n.extent=function(t){return arguments.length?n.majorExtent(t).minorExtent(t):n.minorExtent()},n.majorExtent=function(t){return arguments.length?(i=+t[0][0],u=+t[1][0],c=+t[0][1],l=+t[1][1],i>u&&(t=i,i=u,u=t),c>l&&(t=c,c=l,l=t),n.precision(y)):[[i,c],[u,l]]},n.minorExtent=function(t){return arguments.length?(r=+t[0][0],e=+t[1][0],o=+t[0][1],a=+t[1][1],r>e&&(t=r,r=e,e=t),o>a&&(t=o,o=a,a=t),n.precision(y)):[[r,o],[e,a]]},n.step=function(t){return arguments.length?n.majorStep(t).minorStep(t):n.minorStep()},n.majorStep=function(t){return arguments.length?(d=+t[0],m=+t[1],n):[d,m]},n.minorStep=function(t){return arguments.length?(p=+t[0],v=+t[1],n):[p,v]},n.precision=function(t){return arguments.length?(y=+t,s=me(o,a,90),f=ye(r,e,y),h=me(c,l,90),g=ye(i,u,y),n):y},n.majorExtent([[-180,-90+Da],[180,90-Da]]).minorExtent([[-180,-80-Da],[180,80+Da]])},oa.geo.greatArc=function(){function n(){return{type:"LineString",coordinates:[t||r.apply(this,arguments),e||u.apply(this,arguments)]}}var t,e,r=Me,u=xe;return n.distance=function(){return oa.geo.distance(t||r.apply(this,arguments),e||u.apply(this,arguments))},n.source=function(e){return arguments.length?(r=e,t="function"==typeof e?null:e,n):r},n.target=function(t){return arguments.length?(u=t,e="function"==typeof t?null:t,n):u},n.precision=function(){return arguments.length?n:0},n},oa.geo.interpolate=function(n,t){return be(n[0]*Oa,n[1]*Oa,t[0]*Oa,t[1]*Oa)},oa.geo.length=function(n){return Wo=0,oa.geo.stream(n,Jo),Wo};var Wo,Jo={sphere:b,point:b,lineStart:_e,lineEnd:b,polygonStart:b,polygonEnd:b},Go=we(function(n){return Math.sqrt(2/(1+n))},function(n){return 2*Math.asin(n/2)});(oa.geo.azimuthalEqualArea=function(){return ae(Go)}).raw=Go;var Ko=we(function(n){var t=Math.acos(n);return t&&t/Math.sin(t)},y);(oa.geo.azimuthalEquidistant=function(){return ae(Ko)}).raw=Ko,(oa.geo.conicConformal=function(){return Vt(Se)}).raw=Se,(oa.geo.conicEquidistant=function(){return Vt(ke)}).raw=ke;var Qo=we(function(n){return 1/n},Math.atan);(oa.geo.gnomonic=function(){return ae(Qo)}).raw=Qo,Ne.invert=function(n,t){return[n,2*Math.atan(Math.exp(t))-Ha]},(oa.geo.mercator=function(){return Ee(Ne)}).raw=Ne;var nl=we(function(){return 1},Math.asin);(oa.geo.orthographic=function(){return ae(nl)}).raw=nl;var tl=we(function(n){return 1/(1+n)},function(n){return 2*Math.atan(n)});(oa.geo.stereographic=function(){return ae(tl)}).raw=tl,Ae.invert=function(n,t){return[-t,2*Math.atan(Math.exp(n))-Ha]},(oa.geo.transverseMercator=function(){var n=Ee(Ae),t=n.center,e=n.rotate;return n.center=function(n){return n?t([-n[1],n[0]]):(n=t(),[n[1],-n[0]])},n.rotate=function(n){return n?e([n[0],n[1],n.length>2?n[2]+90:90]):(n=e(),[n[0],n[1],n[2]-90])},e([0,0,90])}).raw=Ae,oa.geom={},oa.geom.hull=function(n){function t(n){if(n.length<3)return[];var t,u=En(e),i=En(r),a=n.length,o=[],l=[];for(t=0;a>t;t++)o.push([+u.call(this,n[t],t),+i.call(this,n[t],t),t]);for(o.sort(qe),t=0;a>t;t++)l.push([o[t][0],-o[t][1]]);var c=Le(o),s=Le(l),f=s[0]===c[0],h=s[s.length-1]===c[c.length-1],g=[];for(t=c.length-1;t>=0;--t)g.push(n[o[c[t]][2]]);for(t=+f;t=r&&c.x<=i&&c.y>=u&&c.y<=a?[[r,a],[i,a],[i,u],[r,u]]:[];s.point=n[o]}),t}function e(n){return n.map(function(n,t){return{x:Math.round(i(n,t)/Da)*Da,y:Math.round(a(n,t)/Da)*Da,i:t}})}var r=Ce,u=ze,i=r,a=u,o=sl;return n?t(n):(t.links=function(n){return or(e(n)).edges.filter(function(n){return n.l&&n.r}).map(function(t){return{source:n[t.l.i],target:n[t.r.i]}})},t.triangles=function(n){var t=[];return or(e(n)).cells.forEach(function(e,r){for(var u,i,a=e.site,o=e.edges.sort(Ve),l=-1,c=o.length,s=o[c-1].edge,f=s.l===a?s.r:s.l;++l=c,h=r>=s,g=h<<1|f;n.leaf=!1,n=n.nodes[g]||(n.nodes[g]=hr()),f?u=c:o=c,h?a=s:l=s,i(n,t,e,r,u,a,o,l)}var s,f,h,g,p,v,d,m,y,M=En(o),x=En(l);if(null!=t)v=t,d=e,m=r,y=u;else if(m=y=-(v=d=1/0),f=[],h=[],p=n.length,a)for(g=0;p>g;++g)s=n[g],s.xm&&(m=s.x),s.y>y&&(y=s.y),f.push(s.x),h.push(s.y);else for(g=0;p>g;++g){var b=+M(s=n[g],g),_=+x(s,g);v>b&&(v=b),d>_&&(d=_),b>m&&(m=b),_>y&&(y=_),f.push(b),h.push(_)}var w=m-v,S=y-d;w>S?y=d+w:m=v+S;var k=hr();if(k.add=function(n){i(k,n,+M(n,++g),+x(n,g),v,d,m,y)},k.visit=function(n){gr(n,k,v,d,m,y)},k.find=function(n){return pr(k,n[0],n[1],v,d,m,y)},g=-1,null==t){for(;++g=0?n.slice(0,t):n,r=t>=0?n.slice(t+1):"in";return e=pl.get(e)||gl,r=vl.get(r)||y,br(r(e.apply(null,la.call(arguments,1))))},oa.interpolateHcl=Rr,oa.interpolateHsl=Dr,oa.interpolateLab=Pr,oa.interpolateRound=Ur,oa.transform=function(n){var t=sa.createElementNS(oa.ns.prefix.svg,"g");return(oa.transform=function(n){if(null!=n){t.setAttribute("transform",n);var e=t.transform.baseVal.consolidate()}return new jr(e?e.matrix:dl)})(n)},jr.prototype.toString=function(){return"translate("+this.translate+")rotate("+this.rotate+")skewX("+this.skew+")scale("+this.scale+")"};var dl={a:1,b:0,c:0,d:1,e:0,f:0};oa.interpolateTransform=$r,oa.layout={},oa.layout.bundle=function(){return function(n){for(var t=[],e=-1,r=n.length;++eo*o/m){if(v>l){var c=t.charge/l;n.px-=i*c,n.py-=a*c}return!0}if(t.point&&l&&v>l){var c=t.pointCharge/l;n.px-=i*c,n.py-=a*c}}return!t.charge}}function t(n){n.px=oa.event.x,n.py=oa.event.y,l.resume()}var e,r,u,i,a,o,l={},c=oa.dispatch("start","tick","end"),s=[1,1],f=.9,h=ml,g=yl,p=-30,v=Ml,d=.1,m=.64,M=[],x=[];return l.tick=function(){if((u*=.99)<.005)return e=null,c.end({type:"end",alpha:u=0}),!0;var t,r,l,h,g,v,m,y,b,_=M.length,w=x.length;for(r=0;w>r;++r)l=x[r],h=l.source,g=l.target,y=g.x-h.x,b=g.y-h.y,(v=y*y+b*b)&&(v=u*a[r]*((v=Math.sqrt(v))-i[r])/v,y*=v,b*=v,g.x-=y*(m=h.weight+g.weight?h.weight/(h.weight+g.weight):.5),g.y-=b*m,h.x+=y*(m=1-m),h.y+=b*m);if((m=u*d)&&(y=s[0]/2,b=s[1]/2,r=-1,m))for(;++r<_;)l=M[r],l.x+=(y-l.x)*m,l.y+=(b-l.y)*m;if(p)for(ru(t=oa.geom.quadtree(M),u,o),r=-1;++r<_;)(l=M[r]).fixed||t.visit(n(l));for(r=-1;++r<_;)l=M[r],l.fixed?(l.x=l.px,l.y=l.py):(l.x-=(l.px-(l.px=l.x))*f,l.y-=(l.py-(l.py=l.y))*f);c.tick({type:"tick",alpha:u})},l.nodes=function(n){return arguments.length?(M=n,l):M},l.links=function(n){return arguments.length?(x=n,l):x},l.size=function(n){return arguments.length?(s=n,l):s},l.linkDistance=function(n){return arguments.length?(h="function"==typeof n?n:+n,l):h},l.distance=l.linkDistance,l.linkStrength=function(n){return arguments.length?(g="function"==typeof n?n:+n,l):g},l.friction=function(n){return arguments.length?(f=+n,l):f},l.charge=function(n){return arguments.length?(p="function"==typeof n?n:+n,l):p},l.chargeDistance=function(n){return arguments.length?(v=n*n,l):Math.sqrt(v)},l.gravity=function(n){return arguments.length?(d=+n,l):d},l.theta=function(n){return arguments.length?(m=n*n,l):Math.sqrt(m)},l.alpha=function(n){return arguments.length?(n=+n,u?n>0?u=n:(e.c=null,e.t=NaN,e=null,c.end({type:"end",alpha:u=0})):n>0&&(c.start({type:"start",alpha:u=n}),e=qn(l.tick)),l):u},l.start=function(){function n(n,r){if(!e){for(e=new Array(u),l=0;u>l;++l)e[l]=[];for(l=0;c>l;++l){var i=x[l];e[i.source.index].push(i.target),e[i.target.index].push(i.source)}}for(var a,o=e[t],l=-1,s=o.length;++lt;++t)(r=M[t]).index=t,r.weight=0;for(t=0;c>t;++t)r=x[t],"number"==typeof r.source&&(r.source=M[r.source]),"number"==typeof r.target&&(r.target=M[r.target]),++r.source.weight,++r.target.weight;for(t=0;u>t;++t)r=M[t],isNaN(r.x)&&(r.x=n("x",f)),isNaN(r.y)&&(r.y=n("y",v)),isNaN(r.px)&&(r.px=r.x),isNaN(r.py)&&(r.py=r.y);if(i=[],"function"==typeof h)for(t=0;c>t;++t)i[t]=+h.call(this,x[t],t);else for(t=0;c>t;++t)i[t]=h;if(a=[],"function"==typeof g)for(t=0;c>t;++t)a[t]=+g.call(this,x[t],t);else for(t=0;c>t;++t)a[t]=g;if(o=[],"function"==typeof p)for(t=0;u>t;++t)o[t]=+p.call(this,M[t],t);else for(t=0;u>t;++t)o[t]=p;return l.resume()},l.resume=function(){return l.alpha(.1)},l.stop=function(){return l.alpha(0)},l.drag=function(){return r||(r=oa.behavior.drag().origin(y).on("dragstart.force",Qr).on("drag.force",t).on("dragend.force",nu)),arguments.length?void this.on("mouseover.force",tu).on("mouseout.force",eu).call(r):r},oa.rebind(l,c,"on")};var ml=20,yl=1,Ml=1/0;oa.layout.hierarchy=function(){function n(u){var i,a=[u],o=[];for(u.depth=0;null!=(i=a.pop());)if(o.push(i),(c=e.call(n,i,i.depth))&&(l=c.length)){for(var l,c,s;--l>=0;)a.push(s=c[l]),s.parent=i,s.depth=i.depth+1;r&&(i.value=0),i.children=c}else r&&(i.value=+r.call(n,i,i.depth)||0),delete i.children;return au(u,function(n){var e,u;t&&(e=n.children)&&e.sort(t),r&&(u=n.parent)&&(u.value+=n.value)}),o}var t=cu,e=ou,r=lu;return n.sort=function(e){return arguments.length?(t=e,n):t},n.children=function(t){return arguments.length?(e=t,n):e},n.value=function(t){return arguments.length?(r=t,n):r},n.revalue=function(t){return r&&(iu(t,function(n){n.children&&(n.value=0)}),au(t,function(t){var e;t.children||(t.value=+r.call(n,t,t.depth)||0),(e=t.parent)&&(e.value+=t.value)})),t},n},oa.layout.partition=function(){function n(t,e,r,u){var i=t.children;if(t.x=e,t.y=t.depth*u,t.dx=r,t.dy=u,i&&(a=i.length)){var a,o,l,c=-1;for(r=t.value?r/t.value:0;++cf?-1:1),p=oa.sum(c),v=p?(f-l*g)/p:0,d=oa.range(l),m=[];return null!=e&&d.sort(e===xl?function(n,t){return c[t]-c[n]}:function(n,t){return e(a[n],a[t])}),d.forEach(function(n){m[n]={data:a[n],value:o=c[n],startAngle:s,endAngle:s+=o*v+g,padAngle:h}}),m}var t=Number,e=xl,r=0,u=ja,i=0;return n.value=function(e){return arguments.length?(t=e,n):t},n.sort=function(t){return arguments.length?(e=t,n):e},n.startAngle=function(t){return arguments.length?(r=t,n):r},n.endAngle=function(t){return arguments.length?(u=t,n):u},n.padAngle=function(t){return arguments.length?(i=t,n):i},n};var xl={};oa.layout.stack=function(){function n(o,l){if(!(h=o.length))return o;var c=o.map(function(e,r){return t.call(n,e,r)}),s=c.map(function(t){return t.map(function(t,e){return[i.call(n,t,e),a.call(n,t,e)]})}),f=e.call(n,s,l);c=oa.permute(c,f),s=oa.permute(s,f);var h,g,p,v,d=r.call(n,s,l),m=c[0].length;for(p=0;m>p;++p)for(u.call(n,c[0][p],v=d[p],s[0][p][1]),g=1;h>g;++g)u.call(n,c[g][p],v+=s[g-1][p][1],s[g][p][1]);return o}var t=y,e=pu,r=vu,u=gu,i=fu,a=hu;return n.values=function(e){return arguments.length?(t=e,n):t},n.order=function(t){return arguments.length?(e="function"==typeof t?t:bl.get(t)||pu,n):e},n.offset=function(t){return arguments.length?(r="function"==typeof t?t:_l.get(t)||vu,n):r},n.x=function(t){return arguments.length?(i=t,n):i},n.y=function(t){return arguments.length?(a=t,n):a},n.out=function(t){return arguments.length?(u=t,n):u},n};var bl=oa.map({"inside-out":function(n){var t,e,r=n.length,u=n.map(du),i=n.map(mu),a=oa.range(r).sort(function(n,t){return u[n]-u[t]}),o=0,l=0,c=[],s=[];for(t=0;r>t;++t)e=a[t],l>o?(o+=i[e],c.push(e)):(l+=i[e],s.push(e));return s.reverse().concat(c)},reverse:function(n){return oa.range(n.length).reverse()},"default":pu}),_l=oa.map({silhouette:function(n){var t,e,r,u=n.length,i=n[0].length,a=[],o=0,l=[];for(e=0;i>e;++e){for(t=0,r=0;u>t;t++)r+=n[t][e][1];r>o&&(o=r),a.push(r)}for(e=0;i>e;++e)l[e]=(o-a[e])/2;return l},wiggle:function(n){var t,e,r,u,i,a,o,l,c,s=n.length,f=n[0],h=f.length,g=[];for(g[0]=l=c=0,e=1;h>e;++e){for(t=0,u=0;s>t;++t)u+=n[t][e][1];for(t=0,i=0,o=f[e][0]-f[e-1][0];s>t;++t){for(r=0,a=(n[t][e][1]-n[t][e-1][1])/(2*o);t>r;++r)a+=(n[r][e][1]-n[r][e-1][1])/o;i+=a*n[t][e][1]}g[e]=l-=u?i/u*o:0,c>l&&(c=l)}for(e=0;h>e;++e)g[e]-=c;return g},expand:function(n){var t,e,r,u=n.length,i=n[0].length,a=1/u,o=[];for(e=0;i>e;++e){for(t=0,r=0;u>t;t++)r+=n[t][e][1];if(r)for(t=0;u>t;t++)n[t][e][1]/=r;else for(t=0;u>t;t++)n[t][e][1]=a}for(e=0;i>e;++e)o[e]=0;return o},zero:vu});oa.layout.histogram=function(){function n(n,i){for(var a,o,l=[],c=n.map(e,this),s=r.call(this,c,i),f=u.call(this,s,c,i),i=-1,h=c.length,g=f.length-1,p=t?1:1/h;++i0)for(i=-1;++i=s[0]&&o<=s[1]&&(a=l[oa.bisect(f,o,1,g)-1],a.y+=p,a.push(n[i]));return l}var t=!0,e=Number,r=bu,u=Mu;return n.value=function(t){return arguments.length?(e=t,n):e},n.range=function(t){return arguments.length?(r=En(t),n):r},n.bins=function(t){return arguments.length?(u="number"==typeof t?function(n){return xu(n,t)}:En(t),n):u},n.frequency=function(e){return arguments.length?(t=!!e,n):t},n},oa.layout.pack=function(){function n(n,i){var a=e.call(this,n,i),o=a[0],l=u[0],c=u[1],s=null==t?Math.sqrt:"function"==typeof t?t:function(){return t};if(o.x=o.y=0,au(o,function(n){n.r=+s(n.value)}),au(o,Nu),r){var f=r*(t?1:Math.max(2*o.r/l,2*o.r/c))/2;au(o,function(n){n.r+=f}),au(o,Nu),au(o,function(n){n.r-=f})}return Cu(o,l/2,c/2,t?1:1/Math.max(2*o.r/l,2*o.r/c)),a}var t,e=oa.layout.hierarchy().sort(_u),r=0,u=[1,1];return n.size=function(t){return arguments.length?(u=t,n):u},n.radius=function(e){return arguments.length?(t=null==e||"function"==typeof e?e:+e,n):t},n.padding=function(t){return arguments.length?(r=+t,n):r},uu(n,e)},oa.layout.tree=function(){function n(n,u){var s=a.call(this,n,u),f=s[0],h=t(f);if(au(h,e),h.parent.m=-h.z,iu(h,r),c)iu(f,i);else{var g=f,p=f,v=f;iu(f,function(n){n.xp.x&&(p=n),n.depth>v.depth&&(v=n)});var d=o(g,p)/2-g.x,m=l[0]/(p.x+o(p,g)/2+d),y=l[1]/(v.depth||1);iu(f,function(n){n.x=(n.x+d)*m,n.y=n.depth*y})}return s}function t(n){for(var t,e={A:null,children:[n]},r=[e];null!=(t=r.pop());)for(var u,i=t.children,a=0,o=i.length;o>a;++a)r.push((i[a]=u={_:i[a],parent:t,children:(u=i[a].children)&&u.slice()||[],A:null,a:null,z:0,m:0,c:0,s:0,t:null,i:a}).a=u);return e.children[0]}function e(n){var t=n.children,e=n.parent.children,r=n.i?e[n.i-1]:null;if(t.length){Du(n);var i=(t[0].z+t[t.length-1].z)/2;r?(n.z=r.z+o(n._,r._),n.m=n.z-i):n.z=i}else r&&(n.z=r.z+o(n._,r._));n.parent.A=u(n,r,n.parent.A||e[0])}function r(n){n._.x=n.z+n.parent.m,n.m+=n.parent.m}function u(n,t,e){if(t){for(var r,u=n,i=n,a=t,l=u.parent.children[0],c=u.m,s=i.m,f=a.m,h=l.m;a=Tu(a),u=qu(u),a&&u;)l=qu(l),i=Tu(i),i.a=n,r=a.z+f-u.z-c+o(a._,u._),r>0&&(Ru(Pu(a,n,e),n,r),c+=r,s+=r),f+=a.m,c+=u.m,h+=l.m,s+=i.m;a&&!Tu(i)&&(i.t=a,i.m+=f-s),u&&!qu(l)&&(l.t=u,l.m+=c-h,e=n)}return e}function i(n){n.x*=l[0],n.y=n.depth*l[1]}var a=oa.layout.hierarchy().sort(null).value(null),o=Lu,l=[1,1],c=null;return n.separation=function(t){return arguments.length?(o=t,n):o},n.size=function(t){return arguments.length?(c=null==(l=t)?i:null,n):c?null:l},n.nodeSize=function(t){return arguments.length?(c=null==(l=t)?null:i,n):c?l:null},uu(n,a)},oa.layout.cluster=function(){function n(n,i){var a,o=t.call(this,n,i),l=o[0],c=0;au(l,function(n){var t=n.children;t&&t.length?(n.x=ju(t),n.y=Uu(t)):(n.x=a?c+=e(n,a):0,n.y=0,a=n)});var s=Fu(l),f=Hu(l),h=s.x-e(s,f)/2,g=f.x+e(f,s)/2;return au(l,u?function(n){n.x=(n.x-l.x)*r[0],n.y=(l.y-n.y)*r[1]}:function(n){n.x=(n.x-h)/(g-h)*r[0],n.y=(1-(l.y?n.y/l.y:1))*r[1]}),o}var t=oa.layout.hierarchy().sort(null).value(null),e=Lu,r=[1,1],u=!1;return n.separation=function(t){return arguments.length?(e=t,n):e},n.size=function(t){return arguments.length?(u=null==(r=t),n):u?null:r},n.nodeSize=function(t){return arguments.length?(u=null!=(r=t),n):u?r:null},uu(n,t)},oa.layout.treemap=function(){function n(n,t){for(var e,r,u=-1,i=n.length;++ut?0:t),e.area=isNaN(r)||0>=r?0:r}function t(e){var i=e.children;if(i&&i.length){var a,o,l,c=f(e),s=[],h=i.slice(),p=1/0,v="slice"===g?c.dx:"dice"===g?c.dy:"slice-dice"===g?1&e.depth?c.dy:c.dx:Math.min(c.dx,c.dy);for(n(h,c.dx*c.dy/e.value),s.area=0;(l=h.length)>0;)s.push(a=h[l-1]),s.area+=a.area,"squarify"!==g||(o=r(s,v))<=p?(h.pop(),p=o):(s.area-=s.pop().area,u(s,v,c,!1),v=Math.min(c.dx,c.dy),s.length=s.area=0,p=1/0);s.length&&(u(s,v,c,!0),s.length=s.area=0),i.forEach(t)}}function e(t){var r=t.children;if(r&&r.length){var i,a=f(t),o=r.slice(),l=[];for(n(o,a.dx*a.dy/t.value),l.area=0;i=o.pop();)l.push(i),l.area+=i.area,null!=i.z&&(u(l,i.z?a.dx:a.dy,a,!o.length),l.length=l.area=0);r.forEach(e)}}function r(n,t){for(var e,r=n.area,u=0,i=1/0,a=-1,o=n.length;++ae&&(i=e),e>u&&(u=e));return r*=r,t*=t,r?Math.max(t*u*p/r,r/(t*i*p)):1/0}function u(n,t,e,r){var u,i=-1,a=n.length,o=e.x,c=e.y,s=t?l(n.area/t):0; +if(t==e.dx){for((r||s>e.dy)&&(s=e.dy);++ie.dx)&&(s=e.dx);++ie&&(t=1),1>e&&(n=0),function(){var e,r,u;do e=2*Math.random()-1,r=2*Math.random()-1,u=e*e+r*r;while(!u||u>1);return n+t*e*Math.sqrt(-2*Math.log(u)/u)}},logNormal:function(){var n=oa.random.normal.apply(oa,arguments);return function(){return Math.exp(n())}},bates:function(n){var t=oa.random.irwinHall(n);return function(){return t()/n}},irwinHall:function(n){return function(){for(var t=0,e=0;n>e;e++)t+=Math.random();return t}}},oa.scale={};var wl={floor:y,ceil:y};oa.scale.linear=function(){return Wu([0,1],[0,1],Mr,!1)};var Sl={s:1,g:1,p:1,r:1,e:1};oa.scale.log=function(){return ri(oa.scale.linear().domain([0,1]),10,!0,[1,10])};var kl=oa.format(".0e"),Nl={floor:function(n){return-Math.ceil(-n)},ceil:function(n){return-Math.floor(-n)}};oa.scale.pow=function(){return ui(oa.scale.linear(),1,[0,1])},oa.scale.sqrt=function(){return oa.scale.pow().exponent(.5)},oa.scale.ordinal=function(){return ai([],{t:"range",a:[[]]})},oa.scale.category10=function(){return oa.scale.ordinal().range(El)},oa.scale.category20=function(){return oa.scale.ordinal().range(Al)},oa.scale.category20b=function(){return oa.scale.ordinal().range(Cl)},oa.scale.category20c=function(){return oa.scale.ordinal().range(zl)};var El=[2062260,16744206,2924588,14034728,9725885,9197131,14907330,8355711,12369186,1556175].map(xn),Al=[2062260,11454440,16744206,16759672,2924588,10018698,14034728,16750742,9725885,12955861,9197131,12885140,14907330,16234194,8355711,13092807,12369186,14408589,1556175,10410725].map(xn),Cl=[3750777,5395619,7040719,10264286,6519097,9216594,11915115,13556636,9202993,12426809,15186514,15190932,8666169,11356490,14049643,15177372,8077683,10834324,13528509,14589654].map(xn),zl=[3244733,7057110,10406625,13032431,15095053,16616764,16625259,16634018,3253076,7652470,10607003,13101504,7695281,10394312,12369372,14342891,6513507,9868950,12434877,14277081].map(xn);oa.scale.quantile=function(){return oi([],[])},oa.scale.quantize=function(){return li(0,1,[0,1])},oa.scale.threshold=function(){return ci([.5],[0,1])},oa.scale.identity=function(){return si([0,1])},oa.svg={},oa.svg.arc=function(){function n(){var n=Math.max(0,+e.apply(this,arguments)),c=Math.max(0,+r.apply(this,arguments)),s=a.apply(this,arguments)-Ha,f=o.apply(this,arguments)-Ha,h=Math.abs(f-s),g=s>f?0:1;if(n>c&&(p=c,c=n,n=p),h>=Fa)return t(c,g)+(n?t(n,1-g):"")+"Z";var p,v,d,m,y,M,x,b,_,w,S,k,N=0,E=0,A=[];if((m=(+l.apply(this,arguments)||0)/2)&&(d=i===Ll?Math.sqrt(n*n+c*c):+i.apply(this,arguments),g||(E*=-1),c&&(E=tn(d/c*Math.sin(m))),n&&(N=tn(d/n*Math.sin(m)))),c){y=c*Math.cos(s+E),M=c*Math.sin(s+E),x=c*Math.cos(f-E),b=c*Math.sin(f-E);var C=Math.abs(f-s-2*E)<=Ua?0:1;if(E&&mi(y,M,x,b)===g^C){var z=(s+f)/2;y=c*Math.cos(z),M=c*Math.sin(z),x=b=null}}else y=M=0;if(n){_=n*Math.cos(f-N),w=n*Math.sin(f-N),S=n*Math.cos(s+N),k=n*Math.sin(s+N);var L=Math.abs(s-f+2*N)<=Ua?0:1;if(N&&mi(_,w,S,k)===1-g^L){var q=(s+f)/2;_=n*Math.cos(q),w=n*Math.sin(q),S=k=null}}else _=w=0;if(h>Da&&(p=Math.min(Math.abs(c-n)/2,+u.apply(this,arguments)))>.001){v=c>n^g?0:1;var T=p,R=p;if(Ua>h){var D=null==S?[_,w]:null==x?[y,M]:Re([y,M],[S,k],[x,b],[_,w]),P=y-D[0],U=M-D[1],j=x-D[0],F=b-D[1],H=1/Math.sin(Math.acos((P*j+U*F)/(Math.sqrt(P*P+U*U)*Math.sqrt(j*j+F*F)))/2),O=Math.sqrt(D[0]*D[0]+D[1]*D[1]);R=Math.min(p,(n-O)/(H-1)),T=Math.min(p,(c-O)/(H+1))}if(null!=x){var I=yi(null==S?[_,w]:[S,k],[y,M],c,T,g),Y=yi([x,b],[_,w],c,T,g);p===T?A.push("M",I[0],"A",T,",",T," 0 0,",v," ",I[1],"A",c,",",c," 0 ",1-g^mi(I[1][0],I[1][1],Y[1][0],Y[1][1]),",",g," ",Y[1],"A",T,",",T," 0 0,",v," ",Y[0]):A.push("M",I[0],"A",T,",",T," 0 1,",v," ",Y[0])}else A.push("M",y,",",M);if(null!=S){var Z=yi([y,M],[S,k],n,-R,g),V=yi([_,w],null==x?[y,M]:[x,b],n,-R,g);p===R?A.push("L",V[0],"A",R,",",R," 0 0,",v," ",V[1],"A",n,",",n," 0 ",g^mi(V[1][0],V[1][1],Z[1][0],Z[1][1]),",",1-g," ",Z[1],"A",R,",",R," 0 0,",v," ",Z[0]):A.push("L",V[0],"A",R,",",R," 0 0,",v," ",Z[0])}else A.push("L",_,",",w)}else A.push("M",y,",",M),null!=x&&A.push("A",c,",",c," 0 ",C,",",g," ",x,",",b),A.push("L",_,",",w),null!=S&&A.push("A",n,",",n," 0 ",L,",",1-g," ",S,",",k);return A.push("Z"),A.join("")}function t(n,t){return"M0,"+n+"A"+n+","+n+" 0 1,"+t+" 0,"+-n+"A"+n+","+n+" 0 1,"+t+" 0,"+n}var e=hi,r=gi,u=fi,i=Ll,a=pi,o=vi,l=di;return n.innerRadius=function(t){return arguments.length?(e=En(t),n):e},n.outerRadius=function(t){return arguments.length?(r=En(t),n):r},n.cornerRadius=function(t){return arguments.length?(u=En(t),n):u},n.padRadius=function(t){return arguments.length?(i=t==Ll?Ll:En(t),n):i},n.startAngle=function(t){return arguments.length?(a=En(t),n):a},n.endAngle=function(t){return arguments.length?(o=En(t),n):o},n.padAngle=function(t){return arguments.length?(l=En(t),n):l},n.centroid=function(){var n=(+e.apply(this,arguments)+ +r.apply(this,arguments))/2,t=(+a.apply(this,arguments)+ +o.apply(this,arguments))/2-Ha;return[Math.cos(t)*n,Math.sin(t)*n]},n};var Ll="auto";oa.svg.line=function(){return Mi(y)};var ql=oa.map({linear:xi,"linear-closed":bi,step:_i,"step-before":wi,"step-after":Si,basis:zi,"basis-open":Li,"basis-closed":qi,bundle:Ti,cardinal:Ei,"cardinal-open":ki,"cardinal-closed":Ni,monotone:Fi});ql.forEach(function(n,t){t.key=n,t.closed=/-closed$/.test(n)});var Tl=[0,2/3,1/3,0],Rl=[0,1/3,2/3,0],Dl=[0,1/6,2/3,1/6];oa.svg.line.radial=function(){var n=Mi(Hi);return n.radius=n.x,delete n.x,n.angle=n.y,delete n.y,n},wi.reverse=Si,Si.reverse=wi,oa.svg.area=function(){return Oi(y)},oa.svg.area.radial=function(){var n=Oi(Hi);return n.radius=n.x,delete n.x,n.innerRadius=n.x0,delete n.x0,n.outerRadius=n.x1,delete n.x1,n.angle=n.y,delete n.y,n.startAngle=n.y0,delete n.y0,n.endAngle=n.y1,delete n.y1,n},oa.svg.chord=function(){function n(n,o){var l=t(this,i,n,o),c=t(this,a,n,o);return"M"+l.p0+r(l.r,l.p1,l.a1-l.a0)+(e(l,c)?u(l.r,l.p1,l.r,l.p0):u(l.r,l.p1,c.r,c.p0)+r(c.r,c.p1,c.a1-c.a0)+u(c.r,c.p1,l.r,l.p0))+"Z"}function t(n,t,e,r){var u=t.call(n,e,r),i=o.call(n,u,r),a=l.call(n,u,r)-Ha,s=c.call(n,u,r)-Ha;return{r:i,a0:a,a1:s,p0:[i*Math.cos(a),i*Math.sin(a)],p1:[i*Math.cos(s),i*Math.sin(s)]}}function e(n,t){return n.a0==t.a0&&n.a1==t.a1}function r(n,t,e){return"A"+n+","+n+" 0 "+ +(e>Ua)+",1 "+t}function u(n,t,e,r){return"Q 0,0 "+r}var i=Me,a=xe,o=Ii,l=pi,c=vi;return n.radius=function(t){return arguments.length?(o=En(t),n):o},n.source=function(t){return arguments.length?(i=En(t),n):i},n.target=function(t){return arguments.length?(a=En(t),n):a},n.startAngle=function(t){return arguments.length?(l=En(t),n):l},n.endAngle=function(t){return arguments.length?(c=En(t),n):c},n},oa.svg.diagonal=function(){function n(n,u){var i=t.call(this,n,u),a=e.call(this,n,u),o=(i.y+a.y)/2,l=[i,{x:i.x,y:o},{x:a.x,y:o},a];return l=l.map(r),"M"+l[0]+"C"+l[1]+" "+l[2]+" "+l[3]}var t=Me,e=xe,r=Yi;return n.source=function(e){return arguments.length?(t=En(e),n):t},n.target=function(t){return arguments.length?(e=En(t),n):e},n.projection=function(t){return arguments.length?(r=t,n):r},n},oa.svg.diagonal.radial=function(){var n=oa.svg.diagonal(),t=Yi,e=n.projection;return n.projection=function(n){return arguments.length?e(Zi(t=n)):t},n},oa.svg.symbol=function(){function n(n,r){return(Pl.get(t.call(this,n,r))||$i)(e.call(this,n,r))}var t=Xi,e=Vi;return n.type=function(e){return arguments.length?(t=En(e),n):t},n.size=function(t){return arguments.length?(e=En(t),n):e},n};var Pl=oa.map({circle:$i,cross:function(n){var t=Math.sqrt(n/5)/2;return"M"+-3*t+","+-t+"H"+-t+"V"+-3*t+"H"+t+"V"+-t+"H"+3*t+"V"+t+"H"+t+"V"+3*t+"H"+-t+"V"+t+"H"+-3*t+"Z"},diamond:function(n){var t=Math.sqrt(n/(2*jl)),e=t*jl;return"M0,"+-t+"L"+e+",0 0,"+t+" "+-e+",0Z"},square:function(n){var t=Math.sqrt(n)/2;return"M"+-t+","+-t+"L"+t+","+-t+" "+t+","+t+" "+-t+","+t+"Z"},"triangle-down":function(n){var t=Math.sqrt(n/Ul),e=t*Ul/2;return"M0,"+e+"L"+t+","+-e+" "+-t+","+-e+"Z"},"triangle-up":function(n){var t=Math.sqrt(n/Ul),e=t*Ul/2;return"M0,"+-e+"L"+t+","+e+" "+-t+","+e+"Z"}});oa.svg.symbolTypes=Pl.keys();var Ul=Math.sqrt(3),jl=Math.tan(30*Oa);Aa.transition=function(n){for(var t,e,r=Fl||++Yl,u=Ki(n),i=[],a=Hl||{time:Date.now(),ease:Nr,delay:0,duration:250},o=-1,l=this.length;++oi;i++){u.push(t=[]);for(var e=this[i],o=0,l=e.length;l>o;o++)(r=e[o])&&n.call(r,r.__data__,o,i)&&t.push(r)}return Wi(u,this.namespace,this.id)},Il.tween=function(n,t){var e=this.id,r=this.namespace;return arguments.length<2?this.node()[r][e].tween.get(n):Y(this,null==t?function(t){t[r][e].tween.remove(n)}:function(u){u[r][e].tween.set(n,t)})},Il.attr=function(n,t){function e(){this.removeAttribute(o)}function r(){this.removeAttributeNS(o.space,o.local)}function u(n){return null==n?e:(n+="",function(){var t,e=this.getAttribute(o);return e!==n&&(t=a(e,n),function(n){this.setAttribute(o,t(n))})})}function i(n){return null==n?r:(n+="",function(){var t,e=this.getAttributeNS(o.space,o.local);return e!==n&&(t=a(e,n),function(n){this.setAttributeNS(o.space,o.local,t(n))})})}if(arguments.length<2){for(t in n)this.attr(t,n[t]);return this}var a="transform"==n?$r:Mr,o=oa.ns.qualify(n);return Ji(this,"attr."+n,t,o.local?i:u)},Il.attrTween=function(n,t){function e(n,e){var r=t.call(this,n,e,this.getAttribute(u));return r&&function(n){this.setAttribute(u,r(n))}}function r(n,e){var r=t.call(this,n,e,this.getAttributeNS(u.space,u.local));return r&&function(n){this.setAttributeNS(u.space,u.local,r(n))}}var u=oa.ns.qualify(n);return this.tween("attr."+n,u.local?r:e)},Il.style=function(n,e,r){function u(){this.style.removeProperty(n)}function i(e){return null==e?u:(e+="",function(){var u,i=t(this).getComputedStyle(this,null).getPropertyValue(n);return i!==e&&(u=Mr(i,e),function(t){this.style.setProperty(n,u(t),r)})})}var a=arguments.length;if(3>a){if("string"!=typeof n){2>a&&(e="");for(r in n)this.style(r,n[r],e);return this}r=""}return Ji(this,"style."+n,e,i)},Il.styleTween=function(n,e,r){function u(u,i){var a=e.call(this,u,i,t(this).getComputedStyle(this,null).getPropertyValue(n));return a&&function(t){this.style.setProperty(n,a(t),r)}}return arguments.length<3&&(r=""),this.tween("style."+n,u)},Il.text=function(n){return Ji(this,"text",n,Gi)},Il.remove=function(){var n=this.namespace;return this.each("end.transition",function(){var t;this[n].count<2&&(t=this.parentNode)&&t.removeChild(this)})},Il.ease=function(n){var t=this.id,e=this.namespace;return arguments.length<1?this.node()[e][t].ease:("function"!=typeof n&&(n=oa.ease.apply(oa,arguments)),Y(this,function(r){r[e][t].ease=n}))},Il.delay=function(n){var t=this.id,e=this.namespace;return arguments.length<1?this.node()[e][t].delay:Y(this,"function"==typeof n?function(r,u,i){r[e][t].delay=+n.call(r,r.__data__,u,i)}:(n=+n,function(r){r[e][t].delay=n}))},Il.duration=function(n){var t=this.id,e=this.namespace;return arguments.length<1?this.node()[e][t].duration:Y(this,"function"==typeof n?function(r,u,i){r[e][t].duration=Math.max(1,n.call(r,r.__data__,u,i))}:(n=Math.max(1,n),function(r){r[e][t].duration=n}))},Il.each=function(n,t){var e=this.id,r=this.namespace;if(arguments.length<2){var u=Hl,i=Fl;try{Fl=e,Y(this,function(t,u,i){Hl=t[r][e],n.call(t,t.__data__,u,i)})}finally{Hl=u,Fl=i}}else Y(this,function(u){var i=u[r][e];(i.event||(i.event=oa.dispatch("start","end","interrupt"))).on(n,t)});return this},Il.transition=function(){for(var n,t,e,r,u=this.id,i=++Yl,a=this.namespace,o=[],l=0,c=this.length;c>l;l++){o.push(n=[]);for(var t=this[l],s=0,f=t.length;f>s;s++)(e=t[s])&&(r=e[a][u],Qi(e,s,a,i,{time:r.time,ease:r.ease,delay:r.delay+r.duration,duration:r.duration})),n.push(e)}return Wi(o,a,i)},oa.svg.axis=function(){function n(n){n.each(function(){var n,c=oa.select(this),s=this.__chart__||e,f=this.__chart__=e.copy(),h=null==l?f.ticks?f.ticks.apply(f,o):f.domain():l,g=null==t?f.tickFormat?f.tickFormat.apply(f,o):y:t,p=c.selectAll(".tick").data(h,f),v=p.enter().insert("g",".domain").attr("class","tick").style("opacity",Da),d=oa.transition(p.exit()).style("opacity",Da).remove(),m=oa.transition(p.order()).style("opacity",1),M=Math.max(u,0)+a,x=Zu(f),b=c.selectAll(".domain").data([0]),_=(b.enter().append("path").attr("class","domain"),oa.transition(b));v.append("line"),v.append("text");var w,S,k,N,E=v.select("line"),A=m.select("line"),C=p.select("text").text(g),z=v.select("text"),L=m.select("text"),q="top"===r||"left"===r?-1:1;if("bottom"===r||"top"===r?(n=na,w="x",k="y",S="x2",N="y2",C.attr("dy",0>q?"0em":".71em").style("text-anchor","middle"),_.attr("d","M"+x[0]+","+q*i+"V0H"+x[1]+"V"+q*i)):(n=ta,w="y",k="x",S="y2",N="x2",C.attr("dy",".32em").style("text-anchor",0>q?"end":"start"),_.attr("d","M"+q*i+","+x[0]+"H0V"+x[1]+"H"+q*i)),E.attr(N,q*u),z.attr(k,q*M),A.attr(S,0).attr(N,q*u),L.attr(w,0).attr(k,q*M),f.rangeBand){var T=f,R=T.rangeBand()/2;s=f=function(n){return T(n)+R}}else s.rangeBand?s=f:d.call(n,f,s);v.call(n,s,f),m.call(n,f,f)})}var t,e=oa.scale.linear(),r=Zl,u=6,i=6,a=3,o=[10],l=null;return n.scale=function(t){return arguments.length?(e=t,n):e},n.orient=function(t){return arguments.length?(r=t in Vl?t+"":Zl,n):r},n.ticks=function(){return arguments.length?(o=ca(arguments),n):o},n.tickValues=function(t){return arguments.length?(l=t,n):l},n.tickFormat=function(e){return arguments.length?(t=e,n):t},n.tickSize=function(t){var e=arguments.length;return e?(u=+t,i=+arguments[e-1],n):u},n.innerTickSize=function(t){return arguments.length?(u=+t,n):u},n.outerTickSize=function(t){return arguments.length?(i=+t,n):i},n.tickPadding=function(t){return arguments.length?(a=+t,n):a},n.tickSubdivide=function(){return arguments.length&&n},n};var Zl="bottom",Vl={top:1,right:1,bottom:1,left:1};oa.svg.brush=function(){function n(t){t.each(function(){var t=oa.select(this).style("pointer-events","all").style("-webkit-tap-highlight-color","rgba(0,0,0,0)").on("mousedown.brush",i).on("touchstart.brush",i),a=t.selectAll(".background").data([0]);a.enter().append("rect").attr("class","background").style("visibility","hidden").style("cursor","crosshair"),t.selectAll(".extent").data([0]).enter().append("rect").attr("class","extent").style("cursor","move");var o=t.selectAll(".resize").data(v,y);o.exit().remove(),o.enter().append("g").attr("class",function(n){return"resize "+n}).style("cursor",function(n){return Xl[n]}).append("rect").attr("x",function(n){return/[ew]$/.test(n)?-3:null}).attr("y",function(n){return/^[ns]/.test(n)?-3:null}).attr("width",6).attr("height",6).style("visibility","hidden"),o.style("display",n.empty()?"none":null);var l,f=oa.transition(t),h=oa.transition(a);c&&(l=Zu(c),h.attr("x",l[0]).attr("width",l[1]-l[0]),r(f)),s&&(l=Zu(s),h.attr("y",l[0]).attr("height",l[1]-l[0]),u(f)),e(f)})}function e(n){n.selectAll(".resize").attr("transform",function(n){return"translate("+f[+/e$/.test(n)]+","+h[+/^s/.test(n)]+")"})}function r(n){n.select(".extent").attr("x",f[0]),n.selectAll(".extent,.n>rect,.s>rect").attr("width",f[1]-f[0])}function u(n){n.select(".extent").attr("y",h[0]),n.selectAll(".extent,.e>rect,.w>rect").attr("height",h[1]-h[0])}function i(){function i(){32==oa.event.keyCode&&(C||(M=null,L[0]-=f[1],L[1]-=h[1],C=2),S())}function v(){32==oa.event.keyCode&&2==C&&(L[0]+=f[1],L[1]+=h[1],C=0,S())}function d(){var n=oa.mouse(b),t=!1;x&&(n[0]+=x[0],n[1]+=x[1]),C||(oa.event.altKey?(M||(M=[(f[0]+f[1])/2,(h[0]+h[1])/2]),L[0]=f[+(n[0]s?(u=r,r=s):u=s),v[0]!=r||v[1]!=u?(e?o=null:a=null,v[0]=r,v[1]=u,!0):void 0}function y(){d(),k.style("pointer-events","all").selectAll(".resize").style("display",n.empty()?"none":null),oa.select("body").style("cursor",null),q.on("mousemove.brush",null).on("mouseup.brush",null).on("touchmove.brush",null).on("touchend.brush",null).on("keydown.brush",null).on("keyup.brush",null),z(),w({type:"brushend"})}var M,x,b=this,_=oa.select(oa.event.target),w=l.of(b,arguments),k=oa.select(b),N=_.datum(),E=!/^(n|s)$/.test(N)&&c,A=!/^(e|w)$/.test(N)&&s,C=_.classed("extent"),z=W(b),L=oa.mouse(b),q=oa.select(t(b)).on("keydown.brush",i).on("keyup.brush",v);if(oa.event.changedTouches?q.on("touchmove.brush",d).on("touchend.brush",y):q.on("mousemove.brush",d).on("mouseup.brush",y),k.interrupt().selectAll("*").interrupt(),C)L[0]=f[0]-L[0],L[1]=h[0]-L[1];else if(N){var T=+/w$/.test(N),R=+/^n/.test(N);x=[f[1-T]-L[0],h[1-R]-L[1]],L[0]=f[T],L[1]=h[R]}else oa.event.altKey&&(M=L.slice());k.style("pointer-events","none").selectAll(".resize").style("display",null),oa.select("body").style("cursor",_.style("cursor")),w({type:"brushstart"}),d()}var a,o,l=N(n,"brushstart","brush","brushend"),c=null,s=null,f=[0,0],h=[0,0],g=!0,p=!0,v=$l[0];return n.event=function(n){n.each(function(){var n=l.of(this,arguments),t={x:f,y:h,i:a,j:o},e=this.__chart__||t;this.__chart__=t,Fl?oa.select(this).transition().each("start.brush",function(){a=e.i,o=e.j,f=e.x,h=e.y,n({type:"brushstart"})}).tween("brush:brush",function(){var e=xr(f,t.x),r=xr(h,t.y);return a=o=null,function(u){f=t.x=e(u),h=t.y=r(u),n({type:"brush",mode:"resize"})}}).each("end.brush",function(){a=t.i,o=t.j,n({type:"brush",mode:"resize"}),n({type:"brushend"})}):(n({type:"brushstart"}),n({type:"brush",mode:"resize"}),n({type:"brushend"}))})},n.x=function(t){return arguments.length?(c=t,v=$l[!c<<1|!s],n):c},n.y=function(t){return arguments.length?(s=t,v=$l[!c<<1|!s],n):s},n.clamp=function(t){return arguments.length?(c&&s?(g=!!t[0],p=!!t[1]):c?g=!!t:s&&(p=!!t),n):c&&s?[g,p]:c?g:s?p:null},n.extent=function(t){var e,r,u,i,l;return arguments.length?(c&&(e=t[0],r=t[1],s&&(e=e[0],r=r[0]),a=[e,r],c.invert&&(e=c(e),r=c(r)),e>r&&(l=e,e=r,r=l),(e!=f[0]||r!=f[1])&&(f=[e,r])),s&&(u=t[0],i=t[1],c&&(u=u[1],i=i[1]),o=[u,i],s.invert&&(u=s(u),i=s(i)),u>i&&(l=u,u=i,i=l),(u!=h[0]||i!=h[1])&&(h=[u,i])),n):(c&&(a?(e=a[0],r=a[1]):(e=f[0],r=f[1],c.invert&&(e=c.invert(e),r=c.invert(r)),e>r&&(l=e,e=r,r=l))),s&&(o?(u=o[0],i=o[1]):(u=h[0],i=h[1],s.invert&&(u=s.invert(u),i=s.invert(i)),u>i&&(l=u,u=i,i=l))),c&&s?[[e,u],[r,i]]:c?[e,r]:s&&[u,i])},n.clear=function(){return n.empty()||(f=[0,0],h=[0,0],a=o=null),n},n.empty=function(){return!!c&&f[0]==f[1]||!!s&&h[0]==h[1]},oa.rebind(n,l,"on")};var Xl={n:"ns-resize",e:"ew-resize",s:"ns-resize",w:"ew-resize",nw:"nwse-resize",ne:"nesw-resize",se:"nwse-resize",sw:"nesw-resize"},$l=[["n","e","s","w","nw","ne","se","sw"],["e","w"],["n","s"],[]],Bl=ho.format=Mo.timeFormat,Wl=Bl.utc,Jl=Wl("%Y-%m-%dT%H:%M:%S.%LZ");Bl.iso=Date.prototype.toISOString&&+new Date("2000-01-01T00:00:00.000Z")?ea:Jl,ea.parse=function(n){var t=new Date(n);return isNaN(t)?null:t},ea.toString=Jl.toString,ho.second=On(function(n){return new go(1e3*Math.floor(n/1e3))},function(n,t){n.setTime(n.getTime()+1e3*Math.floor(t))},function(n){return n.getSeconds()}),ho.seconds=ho.second.range,ho.seconds.utc=ho.second.utc.range,ho.minute=On(function(n){return new go(6e4*Math.floor(n/6e4))},function(n,t){n.setTime(n.getTime()+6e4*Math.floor(t))},function(n){return n.getMinutes()}),ho.minutes=ho.minute.range,ho.minutes.utc=ho.minute.utc.range,ho.hour=On(function(n){var t=n.getTimezoneOffset()/60;return new go(36e5*(Math.floor(n/36e5-t)+t))},function(n,t){n.setTime(n.getTime()+36e5*Math.floor(t))},function(n){return n.getHours()}),ho.hours=ho.hour.range,ho.hours.utc=ho.hour.utc.range,ho.month=On(function(n){return n=ho.day(n),n.setDate(1),n},function(n,t){n.setMonth(n.getMonth()+t)},function(n){return n.getMonth()}),ho.months=ho.month.range,ho.months.utc=ho.month.utc.range;var Gl=[1e3,5e3,15e3,3e4,6e4,3e5,9e5,18e5,36e5,108e5,216e5,432e5,864e5,1728e5,6048e5,2592e6,7776e6,31536e6],Kl=[[ho.second,1],[ho.second,5],[ho.second,15],[ho.second,30],[ho.minute,1],[ho.minute,5],[ho.minute,15],[ho.minute,30],[ho.hour,1],[ho.hour,3],[ho.hour,6],[ho.hour,12],[ho.day,1],[ho.day,2],[ho.week,1],[ho.month,1],[ho.month,3],[ho.year,1]],Ql=Bl.multi([[".%L",function(n){return n.getMilliseconds()}],[":%S",function(n){return n.getSeconds()}],["%I:%M",function(n){return n.getMinutes()}],["%I %p",function(n){return n.getHours()}],["%a %d",function(n){return n.getDay()&&1!=n.getDate()}],["%b %d",function(n){return 1!=n.getDate()}],["%B",function(n){return n.getMonth()}],["%Y",zt]]),nc={range:function(n,t,e){return oa.range(Math.ceil(n/e)*e,+t,e).map(ua)},floor:y,ceil:y};Kl.year=ho.year,ho.scale=function(){return ra(oa.scale.linear(),Kl,Ql)};var tc=Kl.map(function(n){return[n[0].utc,n[1]]}),ec=Wl.multi([[".%L",function(n){return n.getUTCMilliseconds()}],[":%S",function(n){return n.getUTCSeconds()}],["%I:%M",function(n){return n.getUTCMinutes()}],["%I %p",function(n){return n.getUTCHours()}],["%a %d",function(n){return n.getUTCDay()&&1!=n.getUTCDate()}],["%b %d",function(n){return 1!=n.getUTCDate()}],["%B",function(n){return n.getUTCMonth()}],["%Y",zt]]);tc.year=ho.year.utc,ho.scale.utc=function(){return ra(oa.scale.linear(),tc,ec)},oa.text=An(function(n){return n.responseText}),oa.json=function(n,t){return Cn(n,"application/json",ia,t)},oa.html=function(n,t){return Cn(n,"text/html",aa,t)},oa.xml=An(function(n){return n.responseXML}),"function"==typeof define&&define.amd?(this.d3=oa,define(oa)):"object"==typeof module&&module.exports?module.exports=oa:this.d3=oa}(); \ No newline at end of file diff --git a/webroot/rsrc/externals/raphael/g.raphael.js b/webroot/rsrc/externals/raphael/g.raphael.js deleted file mode 100644 index 70936409ad..0000000000 --- a/webroot/rsrc/externals/raphael/g.raphael.js +++ /dev/null @@ -1,12 +0,0 @@ -/** - * @provides raphael-g - * @do-not-minify - * @nolint - */ -/*! - * g.Raphael 0.5 - Charting library, based on Raphaël - * - * Copyright (c) 2009 Dmitry Baranovskiy (http://g.raphaeljs.com) - * Licensed under the MIT (http://www.opensource.org/licenses/mit-license.php) license. - */ -Raphael.el.popup=function(d,k,h,g){var c=this.paper||this[0].paper,f,j,b,e,a;if(!c){return}switch(this.type){case"text":case"circle":case"ellipse":b=true;break;default:b=false}d=d==null?"up":d;k=k||5;f=this.getBBox();h=typeof h=="number"?h:(b?f.x+f.width/2:f.x);g=typeof g=="number"?g:(b?f.y+f.height/2:f.y);e=Math.max(f.width/2-k,0);a=Math.max(f.height/2-k,0);this.translate(h-f.x-(b?f.width/2:0),g-f.y-(b?f.height/2:0));f=this.getBBox();var i={up:["M",h,g,"l",-k,-k,-e,0,"a",k,k,0,0,1,-k,-k,"l",0,-f.height,"a",k,k,0,0,1,k,-k,"l",k*2+e*2,0,"a",k,k,0,0,1,k,k,"l",0,f.height,"a",k,k,0,0,1,-k,k,"l",-e,0,"z"].join(","),down:["M",h,g,"l",k,k,e,0,"a",k,k,0,0,1,k,k,"l",0,f.height,"a",k,k,0,0,1,-k,k,"l",-(k*2+e*2),0,"a",k,k,0,0,1,-k,-k,"l",0,-f.height,"a",k,k,0,0,1,k,-k,"l",e,0,"z"].join(","),left:["M",h,g,"l",-k,k,0,a,"a",k,k,0,0,1,-k,k,"l",-f.width,0,"a",k,k,0,0,1,-k,-k,"l",0,-(k*2+a*2),"a",k,k,0,0,1,k,-k,"l",f.width,0,"a",k,k,0,0,1,k,k,"l",0,a,"z"].join(","),right:["M",h,g,"l",k,-k,0,-a,"a",k,k,0,0,1,k,-k,"l",f.width,0,"a",k,k,0,0,1,k,k,"l",0,k*2+a*2,"a",k,k,0,0,1,-k,k,"l",-f.width,0,"a",k,k,0,0,1,-k,-k,"l",0,-a,"z"].join(",")};j={up:{x:-!b*(f.width/2),y:-k*2-(b?f.height/2:f.height)},down:{x:-!b*(f.width/2),y:k*2+(b?f.height/2:f.height)},left:{x:-k*2-(b?f.width/2:f.width),y:-!b*(f.height/2)},right:{x:k*2+(b?f.width/2:f.width),y:-!b*(f.height/2)}}[d];this.translate(j.x,j.y);return c.path(i[d]).attr({fill:"#000",stroke:"none"}).insertBefore(this.node?this:this[0])};Raphael.el.tag=function(f,b,l,k){var i=3,e=this.paper||this[0].paper;if(!e){return}var c=e.path().attr({fill:"#000",stroke:"#000"}),j=this.getBBox(),m,h,a,g;switch(this.type){case"text":case"circle":case"ellipse":a=true;break;default:a=false}f=f||0;l=typeof l=="number"?l:(a?j.x+j.width/2:j.x);k=typeof k=="number"?k:(a?j.y+j.height/2:j.y);b=b==null?5:b;h=0.5522*b;if(j.height>=b*2){c.attr({path:["M",l,k+b,"a",b,b,0,1,1,0,-b*2,b,b,0,1,1,0,b*2,"m",0,-b*2-i,"a",b+i,b+i,0,1,0,0,(b+i)*2,"L",l+b+i,k+j.height/2+i,"l",j.width+2*i,0,0,-j.height-2*i,-j.width-2*i,0,"L",l,k-b-i].join(",")})}else{m=Math.sqrt(Math.pow(b+i,2)-Math.pow(j.height/2+i,2));c.attr({path:["M",l,k+b,"c",-h,0,-b,h-b,-b,-b,0,-h,b-h,-b,b,-b,h,0,b,b-h,b,b,0,h,h-b,b,-b,b,"M",l+m,k-j.height/2-i,"a",b+i,b+i,0,1,0,0,j.height+2*i,"l",b+i-m+j.width+2*i,0,0,-j.height-2*i,"L",l+m,k-j.height/2-i].join(",")})}f=360-f;c.rotate(f,l,k);if(this.attrs){this.attr(this.attrs.x?"x":"cx",l+b+i+(!a?this.type=="text"?j.width:0:j.width/2)).attr("y",a?k:k-j.height/2);this.rotate(f,l,k);f>90&&f<270&&this.attr(this.attrs.x?"x":"cx",l-b-i-(!a?j.width:j.width/2)).rotate(180,l,k)}else{if(f>90&&f<270){this.translate(l-j.x-j.width-b-i,k-j.y-j.height/2);this.rotate(f-180,j.x+j.width+b+i,j.y+j.height/2)}else{this.translate(l-j.x+b+i,k-j.y-j.height/2);this.rotate(f,j.x-b-i,j.y+j.height/2)}}return c.insertBefore(this.node?this:this[0])};Raphael.el.drop=function(d,g,f){var e=this.getBBox(),c=this.paper||this[0].paper,a,j,b,i,h;if(!c){return}switch(this.type){case"text":case"circle":case"ellipse":a=true;break;default:a=false}d=d||0;g=typeof g=="number"?g:(a?e.x+e.width/2:e.x);f=typeof f=="number"?f:(a?e.y+e.height/2:e.y);j=Math.max(e.width,e.height)+Math.min(e.width,e.height);b=c.path(["M",g,f,"l",j,0,"A",j*0.4,j*0.4,0,1,0,g+j*0.7,f-j*0.7,"z"]).attr({fill:"#000",stroke:"none"}).rotate(22.5-d,g,f);d=(d+90)*Math.PI/180;i=(g+j*Math.sin(d))-(a?0:e.width/2);h=(f+j*Math.cos(d))-(a?0:e.height/2);this.attrs?this.attr(this.attrs.x?"x":"cx",i).attr(this.attrs.y?"y":"cy",h):this.translate(i-e.x,h-e.y);return b.insertBefore(this.node?this:this[0])};Raphael.el.flag=function(e,k,j){var g=3,c=this.paper||this[0].paper;if(!c){return}var b=c.path().attr({fill:"#000",stroke:"#000"}),i=this.getBBox(),f=i.height/2,a;switch(this.type){case"text":case"circle":case"ellipse":a=true;break;default:a=false}e=e||0;k=typeof k=="number"?k:(a?i.x+i.width/2:i.x);j=typeof j=="number"?j:(a?i.y+i.height/2:i.y);b.attr({path:["M",k,j,"l",f+g,-f-g,i.width+2*g,0,0,i.height+2*g,-i.width-2*g,0,"z"].join(",")});e=360-e;b.rotate(e,k,j);if(this.attrs){this.attr(this.attrs.x?"x":"cx",k+f+g+(!a?this.type=="text"?i.width:0:i.width/2)).attr("y",a?j:j-i.height/2);this.rotate(e,k,j);e>90&&e<270&&this.attr(this.attrs.x?"x":"cx",k-f-g-(!a?i.width:i.width/2)).rotate(180,k,j)}else{if(e>90&&e<270){this.translate(k-i.x-i.width-f-g,j-i.y-i.height/2);this.rotate(e-180,i.x+i.width+f+g,i.y+i.height/2)}else{this.translate(k-i.x+f+g,j-i.y-i.height/2);this.rotate(e,i.x-f-g,i.y+i.height/2)}}return b.insertBefore(this.node?this:this[0])};Raphael.el.label=function(){var c=this.getBBox(),b=this.paper||this[0].paper,a=Math.min(20,c.width+10,c.height+10)/2;if(!b){return}return b.rect(c.x-a/2,c.y-a/2,c.width+a,c.height+a,a).attr({stroke:"none",fill:"#000"}).insertBefore(this.node?this:this[0])};Raphael.el.blob=function(z,j,i){var g=this.getBBox(),B=Math.PI/180,n=this.paper||this[0].paper,r,A,q;if(!n){return}switch(this.type){case"text":case"circle":case"ellipse":A=true;break;default:A=false}r=n.path().attr({fill:"#000",stroke:"none"});z=(+z+1?z:45)+90;q=Math.min(g.height,g.width);j=typeof j=="number"?j:(A?g.x+g.width/2:g.x);i=typeof i=="number"?i:(A?g.y+g.height/2:g.y);var m=Math.max(g.width+q,q*25/12),t=Math.max(g.height+q,q*25/12),u=j+q*Math.sin((z-22.5)*B),b=i+q*Math.cos((z-22.5)*B),v=j+q*Math.sin((z+22.5)*B),d=i+q*Math.cos((z+22.5)*B),o=(v-u)/2,l=(d-b)/2,f=m/2,e=t/2,s=-Math.sqrt(Math.abs(f*f*e*e-f*f*l*l-e*e*o*o)/(f*f*l*l+e*e*o*o)),c=s*f*l/e+(v+u)/2,a=s*-e*o/f+(d+b)/2;r.attr({x:c,y:a,path:["M",j,i,"L",v,d,"A",f,e,0,1,1,u,b,"z"].join(",")});this.translate(c-g.x-g.width/2,a-g.y-g.height/2);return r.insertBefore(this.node?this:this[0])};Raphael.fn.label=function(a,d,b){var c=this.set();b=this.text(a,d,b).attr(Raphael.g.txtattr);return c.push(b.label(),b)};Raphael.fn.popup=function(a,f,d,b,c){var e=this.set();d=this.text(a,f,d).attr(Raphael.g.txtattr);return e.push(d.popup(b,c),d)};Raphael.fn.tag=function(a,f,d,c,b){var e=this.set();d=this.text(a,f,d).attr(Raphael.g.txtattr);return e.push(d.tag(c,b),d)};Raphael.fn.flag=function(a,e,c,b){var d=this.set();c=this.text(a,e,c).attr(Raphael.g.txtattr);return d.push(c.flag(b),c)};Raphael.fn.drop=function(a,e,c,b){var d=this.set();c=this.text(a,e,c).attr(Raphael.g.txtattr);return d.push(c.drop(b),c)};Raphael.fn.blob=function(a,e,c,b){var d=this.set();c=this.text(a,e,c).attr(Raphael.g.txtattr);return d.push(c.blob(b),c)};Raphael.el.lighter=function(b){b=b||2;var a=[this.attrs.fill,this.attrs.stroke];this.fs=this.fs||[a[0],a[1]];a[0]=Raphael.rgb2hsb(Raphael.getRGB(a[0]).hex);a[1]=Raphael.rgb2hsb(Raphael.getRGB(a[1]).hex);a[0].b=Math.min(a[0].b*b,1);a[0].s=a[0].s/b;a[1].b=Math.min(a[1].b*b,1);a[1].s=a[1].s/b;this.attr({fill:"hsb("+[a[0].h,a[0].s,a[0].b]+")",stroke:"hsb("+[a[1].h,a[1].s,a[1].b]+")"});return this};Raphael.el.darker=function(b){b=b||2;var a=[this.attrs.fill,this.attrs.stroke];this.fs=this.fs||[a[0],a[1]];a[0]=Raphael.rgb2hsb(Raphael.getRGB(a[0]).hex);a[1]=Raphael.rgb2hsb(Raphael.getRGB(a[1]).hex);a[0].s=Math.min(a[0].s*b,1);a[0].b=a[0].b/b;a[1].s=Math.min(a[1].s*b,1);a[1].b=a[1].b/b;this.attr({fill:"hsb("+[a[0].h,a[0].s,a[0].b]+")",stroke:"hsb("+[a[1].h,a[1].s,a[1].b]+")"});return this};Raphael.el.resetBrightness=function(){if(this.fs){this.attr({fill:this.fs[0],stroke:this.fs[1]});delete this.fs}return this};(function(){var c=["lighter","darker","resetBrightness"],a=["popup","tag","flag","label","drop","blob"];for(var b in a){(function(d){Raphael.st[d]=function(){return Raphael.el[d].apply(this,arguments)}})(a[b])}for(var b in c){(function(d){Raphael.st[d]=function(){for(var e=0;e0?0:0.5))*Math.pow(10,b))/Math.pow(10,b);return{from:e,to:l,power:b}},axis:function(p,o,k,D,e,G,g,J,h,a,q){a=a==null?2:a;h=h||"t";G=G||10;q=arguments[arguments.length-1];var C=h=="|"||h==" "?["M",p+0.5,o,"l",0,0.001]:g==1||g==3?["M",p+0.5,o,"l",0,-k]:["M",p,o+0.5,"l",k,0],s=this.snapEnds(D,e,G),H=s.from,z=s.to,F=s.power,E=0,w={font:"11px 'Fontin Sans', Fontin-Sans, sans-serif"},v=q.set(),I;I=(z-H)/G;var n=H,m=F>0?F:0;r=k/G;if(+g==1||+g==3){var b=o,u=(g-1?1:-1)*(a+3+!!(g-1));while(b>=o-k){h!="-"&&h!=" "&&(C=C.concat(["M",p-(h=="+"||h=="|"?a:!(g-1)*a*2),b+0.5,"l",a*2+1,0]));v.push(q.text(p+u,b,(J&&J[E++])||(Math.round(n)==n?n:+n.toFixed(m))).attr(w).attr({"text-anchor":g-1?"start":"end"}));n+=I;b-=r}if(Math.round(b+r-(o-k))){h!="-"&&h!=" "&&(C=C.concat(["M",p-(h=="+"||h=="|"?a:!(g-1)*a*2),o-k+0.5,"l",a*2+1,0]));v.push(q.text(p+u,o-k,(J&&J[E])||(Math.round(n)==n?n:+n.toFixed(m))).attr(w).attr({"text-anchor":g-1?"start":"end"}))}}else{n=H;m=(F>0)*F;u=(g?-1:1)*(a+9+!g);var c=p,r=k/G,A=0,B=0;while(c<=p+k){h!="-"&&h!=" "&&(C=C.concat(["M",c+0.5,o-(h=="+"?a:!!g*a*2),"l",0,a*2+1]));v.push(A=q.text(c,o+u,(J&&J[E++])||(Math.round(n)==n?n:+n.toFixed(m))).attr(w));var l=A.getBBox();if(B>=l.x-5){v.pop(v.length-1).remove()}else{B=l.x+l.width}n+=I;c+=r}if(Math.round(c-r-p-k)){h!="-"&&h!=" "&&(C=C.concat(["M",p+k+0.5,o-(h=="+"?a:!!g*a*2),"l",0,a*2+1]));v.push(q.text(p+k,o+u,(J&&J[E])||(Math.round(n)==n?n:+n.toFixed(m))).attr(w))}}var K=q.path(C);K.text=v;K.all=q.set([K,v]);K.remove=function(){this.text.remove();this.constructor.prototype.remove.call(this)};return K},labelise:function(a,c,b){if(a){return(a+"").replace(/(##+(?:\.#+)?)|(%%+(?:\.%+)?)/g,function(d,f,e){if(f){return(+c).toFixed(f.replace(/^#+\.?/g,"").length)}if(e){return(c*100/b).toFixed(e.replace(/^%+\.?/g,"").length)+"%"}})}else{return(+c).toFixed(0)}}}; \ No newline at end of file diff --git a/webroot/rsrc/externals/raphael/g.raphael.line.js b/webroot/rsrc/externals/raphael/g.raphael.line.js deleted file mode 100644 index fe8892e190..0000000000 --- a/webroot/rsrc/externals/raphael/g.raphael.line.js +++ /dev/null @@ -1,12 +0,0 @@ -/** - * @provides raphael-g-line - * @do-not-minify - * @nolint - */ -/*! - * g.Raphael 0.5 - Charting library, based on Raphaël - * - * Copyright (c) 2009 Dmitry Baranovskiy (http://g.raphaeljs.com) - * Licensed under the MIT (http://www.opensource.org/licenses/mit-license.php) license. - */ -(function(){function a(g,n){var f=g.length/n,h=0,e=f,m=0,i=[];while(he-2*q){z[ac]=a(z[ac],e-2*q);B=e-2*q}if(A[ac]&&A[ac].length>e-2*q){A[ac]=a(A[ac],e-2*q)}}var W=Array.prototype.concat.apply([],A),U=Array.prototype.concat.apply([],z),u=s.snapEnds(Math.min.apply(Math,W),Math.max.apply(Math,W),A[0].length-1),E=u.from,o=u.to,N=s.snapEnds(Math.min.apply(Math,U),Math.max.apply(Math,U),z[0].length-1),C=N.from,n=N.to,Z=(e-q*2)/((o-E)||1),V=(h-q*2)/((n-C)||1);var G=f.set();if(J.axis){var m=(J.axis+"").split(/[,\s]+/);+m[0]&&G.push(s.axis(P+q,O+q,e-2*q,E,o,J.axisxstep||Math.floor((e-2*q)/20),2,f));+m[1]&&G.push(s.axis(P+e-q,O+h-q,h-2*q,C,n,J.axisystep||Math.floor((h-2*q)/20),3,f));+m[2]&&G.push(s.axis(P+q,O+h-q,e-2*q,E,o,J.axisxstep||Math.floor((e-2*q)/20),0,f));+m[3]&&G.push(s.axis(P+q,O+h-q,h-2*q,C,n,J.axisystep||Math.floor((h-2*q)/20),1,f))}var M=f.set(),aa=f.set(),r;for(ac=0,L=z.length;acf*b.top){e=b.percents[y],p=b.percents[y-1]||0,t=t/b.top*(e-p),o=b.percents[y+1],j=b.anim[e];break}f&&d.attr(b.anim[b.percents[y]])}if(!!j){if(!k){for(var A in j)if(j[g](A))if(U[g](A)||d.paper.customAttributes[g](A)){u[A]=d.attr(A),u[A]==null&&(u[A]=T[A]),v[A]=j[A];switch(U[A]){case C:w[A]=(v[A]-u[A])/t;break;case"colour":u[A]=a.getRGB(u[A]);var B=a.getRGB(v[A]);w[A]={r:(B.r-u[A].r)/t,g:(B.g-u[A].g)/t,b:(B.b-u[A].b)/t};break;case"path":var D=bG(u[A],v[A]),E=D[1];u[A]=D[0],w[A]=[];for(y=0,z=u[A].length;yd)return d;while(cf?c=e:d=e,e=(d-c)/2+c}return e}function n(a,b){var c=o(a,b);return((l*c+k)*c+j)*c}function m(a){return((i*a+h)*a+g)*a}var g=3*b,h=3*(d-b)-g,i=1-g-h,j=3*c,k=3*(e-c)-j,l=1-j-k;return n(a,1/(200*f))}function cd(){return this.x+q+this.y+q+this.width+" × "+this.height}function cc(){return this.x+q+this.y}function bQ(a,b,c,d,e,f){a!=null?(this.a=+a,this.b=+b,this.c=+c,this.d=+d,this.e=+e,this.f=+f):(this.a=1,this.b=0,this.c=0,this.d=1,this.e=0,this.f=0)}function bw(a){var b=[];for(var c=0,d=a.length;d-2>c;c+=2){var e=[{x:+a[c],y:+a[c+1]},{x:+a[c],y:+a[c+1]},{x:+a[c+2],y:+a[c+3]},{x:+a[c+4],y:+a[c+5]}];d-4==c?(e[0]={x:+a[c-2],y:+a[c-1]},e[3]=e[2]):c&&(e[0]={x:+a[c-2],y:+a[c-1]}),b.push(["C",(-e[0].x+6*e[1].x+e[2].x)/6,(-e[0].y+6*e[1].y+e[2].y)/6,(e[1].x+6*e[2].x-e[3].x)/6,(e[1].y+6*e[2].y-e[3].y)/6,e[2].x,e[2].y])}return b}function bv(){return this.hex}function bt(a,b,c){function d(){var e=Array.prototype.slice.call(arguments,0),f=e.join("␀"),h=d.cache=d.cache||{},i=d.count=d.count||[];if(h[g](f)){bs(i,f);return c?c(h[f]):h[f]}i.length>=1e3&&delete h[i.shift()],i.push(f),h[f]=a[m](b,e);return c?c(h[f]):h[f]}return d}function bs(a,b){for(var c=0,d=a.length;c',bk=bj.firstChild,bk.style.behavior="url(#default#VML)";if(!bk||typeof bk.adj!="object")return a.type=p;bj=null}a.svg=!(a.vml=a.type=="VML"),a._Paper=j,a.fn=k=j.prototype=a.prototype,a._id=0,a._oid=0,a.is=function(a,b){b=v.call(b);if(b=="finite")return!M[g](+a);if(b=="array")return a instanceof Array;return b=="null"&&a===null||b==typeof a&&a!==null||b=="object"&&a===Object(a)||b=="array"&&Array.isArray&&Array.isArray(a)||H.call(a).slice(8,-1).toLowerCase()==b},a.angle=function(b,c,d,e,f,g){if(f==null){var h=b-d,i=c-e;if(!h&&!i)return 0;return(180+w.atan2(-i,-h)*180/B+360)%360}return a.angle(b,c,f,g)-a.angle(d,e,f,g)},a.rad=function(a){return a%360*B/180},a.deg=function(a){return a*180/B%360},a.snapTo=function(b,c,d){d=a.is(d,"finite")?d:10;if(a.is(b,E)){var e=b.length;while(e--)if(z(b[e]-c)<=d)return b[e]}else{b=+b;var f=c%b;if(fb-d)return c-f+b}return c};var bl=a.createUUID=function(a,b){return function(){return"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(a,b).toUpperCase()}}(/[xy]/g,function(a){var b=w.random()*16|0,c=a=="x"?b:b&3|8;return c.toString(16)});a.setWindow=function(b){eve("setWindow",a,h.win,b),h.win=b,h.doc=h.win.document,a._engine.initWin&&a._engine.initWin(h.win)};var bm=function(b){if(a.vml){var c=/^\s+|\s+$/g,d;try{var e=new ActiveXObject("htmlfile");e.write(""),e.close(),d=e.body}catch(f){d=createPopup().document.body}var g=d.createTextRange();bm=bt(function(a){try{d.style.color=r(a).replace(c,p);var b=g.queryCommandValue("ForeColor");b=(b&255)<<16|b&65280|(b&16711680)>>>16;return"#"+("000000"+b.toString(16)).slice(-6)}catch(e){return"none"}})}else{var i=h.doc.createElement("i");i.title="Raphaël Colour Picker",i.style.display="none",h.doc.body.appendChild(i),bm=bt(function(a){i.style.color=a;return h.doc.defaultView.getComputedStyle(i,p).getPropertyValue("color")})}return bm(b)},bn=function(){return"hsb("+[this.h,this.s,this.b]+")"},bo=function(){return"hsl("+[this.h,this.s,this.l]+")"},bp=function(){return this.hex},bq=function(b,c,d){c==null&&a.is(b,"object")&&"r"in b&&"g"in b&&"b"in b&&(d=b.b,c=b.g,b=b.r);if(c==null&&a.is(b,D)){var e=a.getRGB(b);b=e.r,c=e.g,d=e.b}if(b>1||c>1||d>1)b/=255,c/=255,d/=255;return[b,c,d]},br=function(b,c,d,e){b*=255,c*=255,d*=255;var f={r:b,g:c,b:d,hex:a.rgb(b,c,d),toString:bp};a.is(e,"finite")&&(f.opacity=e);return f};a.color=function(b){var c;a.is(b,"object")&&"h"in b&&"s"in b&&"b"in b?(c=a.hsb2rgb(b),b.r=c.r,b.g=c.g,b.b=c.b,b.hex=c.hex):a.is(b,"object")&&"h"in b&&"s"in b&&"l"in b?(c=a.hsl2rgb(b),b.r=c.r,b.g=c.g,b.b=c.b,b.hex=c.hex):(a.is(b,"string")&&(b=a.getRGB(b)),a.is(b,"object")&&"r"in b&&"g"in b&&"b"in b?(c=a.rgb2hsl(b),b.h=c.h,b.s=c.s,b.l=c.l,c=a.rgb2hsb(b),b.v=c.b):(b={hex:"none"},b.r=b.g=b.b=b.h=b.s=b.v=b.l=-1)),b.toString=bp;return b},a.hsb2rgb=function(a,b,c,d){this.is(a,"object")&&"h"in a&&"s"in a&&"b"in a&&(c=a.b,b=a.s,a=a.h,d=a.o),a*=360;var e,f,g,h,i;a=a%360/60,i=c*b,h=i*(1-z(a%2-1)),e=f=g=c-i,a=~~a,e+=[i,h,0,0,h,i][a],f+=[h,i,i,h,0,0][a],g+=[0,0,h,i,i,h][a];return br(e,f,g,d)},a.hsl2rgb=function(a,b,c,d){this.is(a,"object")&&"h"in a&&"s"in a&&"l"in a&&(c=a.l,b=a.s,a=a.h);if(a>1||b>1||c>1)a/=360,b/=100,c/=100;a*=360;var e,f,g,h,i;a=a%360/60,i=2*b*(c<.5?c:1-c),h=i*(1-z(a%2-1)),e=f=g=c-i/2,a=~~a,e+=[i,h,0,0,h,i][a],f+=[h,i,i,h,0,0][a],g+=[0,0,h,i,i,h][a];return br(e,f,g,d)},a.rgb2hsb=function(a,b,c){c=bq(a,b,c),a=c[0],b=c[1],c=c[2];var d,e,f,g;f=x(a,b,c),g=f-y(a,b,c),d=g==0?null:f==a?(b-c)/g:f==b?(c-a)/g+2:(a-b)/g+4,d=(d+360)%6*60/360,e=g==0?0:g/f;return{h:d,s:e,b:f,toString:bn}},a.rgb2hsl=function(a,b,c){c=bq(a,b,c),a=c[0],b=c[1],c=c[2];var d,e,f,g,h,i;g=x(a,b,c),h=y(a,b,c),i=g-h,d=i==0?null:g==a?(b-c)/i:g==b?(c-a)/i+2:(a-b)/i+4,d=(d+360)%6*60/360,f=(g+h)/2,e=i==0?0:f<.5?i/(2*f):i/(2-2*f);return{h:d,s:e,l:f,toString:bo}},a._path2string=function(){return this.join(",").replace(X,"$1")};var bu=a._preload=function(a,b){var c=h.doc.createElement("img");c.style.cssText="position:absolute;left:-9999em;top:-9999em",c.onload=function(){b.call(this),this.onload=null,h.doc.body.removeChild(this)},c.onerror=function(){h.doc.body.removeChild(this)},h.doc.body.appendChild(c),c.src=a};a.getRGB=bt(function(b){if(!b||!!((b=r(b)).indexOf("-")+1))return{r:-1,g:-1,b:-1,hex:"none",error:1,toString:bv};if(b=="none")return{r:-1,g:-1,b:-1,hex:"none",toString:bv};!W[g](b.toLowerCase().substring(0,2))&&b.charAt()!="#"&&(b=bm(b));var c,d,e,f,h,i,j,k=b.match(L);if(k){k[2]&&(f=R(k[2].substring(5),16),e=R(k[2].substring(3,5),16),d=R(k[2].substring(1,3),16)),k[3]&&(f=R((i=k[3].charAt(3))+i,16),e=R((i=k[3].charAt(2))+i,16),d=R((i=k[3].charAt(1))+i,16)),k[4]&&(j=k[4][s](V),d=Q(j[0]),j[0].slice(-1)=="%"&&(d*=2.55),e=Q(j[1]),j[1].slice(-1)=="%"&&(e*=2.55),f=Q(j[2]),j[2].slice(-1)=="%"&&(f*=2.55),k[1].toLowerCase().slice(0,4)=="rgba"&&(h=Q(j[3])),j[3]&&j[3].slice(-1)=="%"&&(h/=100));if(k[5]){j=k[5][s](V),d=Q(j[0]),j[0].slice(-1)=="%"&&(d*=2.55),e=Q(j[1]),j[1].slice(-1)=="%"&&(e*=2.55),f=Q(j[2]),j[2].slice(-1)=="%"&&(f*=2.55),(j[0].slice(-3)=="deg"||j[0].slice(-1)=="°")&&(d/=360),k[1].toLowerCase().slice(0,4)=="hsba"&&(h=Q(j[3])),j[3]&&j[3].slice(-1)=="%"&&(h/=100);return a.hsb2rgb(d,e,f,h)}if(k[6]){j=k[6][s](V),d=Q(j[0]),j[0].slice(-1)=="%"&&(d*=2.55),e=Q(j[1]),j[1].slice(-1)=="%"&&(e*=2.55),f=Q(j[2]),j[2].slice(-1)=="%"&&(f*=2.55),(j[0].slice(-3)=="deg"||j[0].slice(-1)=="°")&&(d/=360),k[1].toLowerCase().slice(0,4)=="hsla"&&(h=Q(j[3])),j[3]&&j[3].slice(-1)=="%"&&(h/=100);return a.hsl2rgb(d,e,f,h)}k={r:d,g:e,b:f,toString:bv},k.hex="#"+(16777216|f|e<<8|d<<16).toString(16).slice(1),a.is(h,"finite")&&(k.opacity=h);return k}return{r:-1,g:-1,b:-1,hex:"none",error:1,toString:bv}},a),a.hsb=bt(function(b,c,d){return a.hsb2rgb(b,c,d).hex}),a.hsl=bt(function(b,c,d){return a.hsl2rgb(b,c,d).hex}),a.rgb=bt(function(a,b,c){return"#"+(16777216|c|b<<8|a<<16).toString(16).slice(1)}),a.getColor=function(a){var b=this.getColor.start=this.getColor.start||{h:0,s:1,b:a||.75},c=this.hsb2rgb(b.h,b.s,b.b);b.h+=.075,b.h>1&&(b.h=0,b.s-=.2,b.s<=0&&(this.getColor.start={h:0,s:1,b:b.b}));return c.hex},a.getColor.reset=function(){delete this.start},a.parsePathString=bt(function(b){if(!b)return null;var c={a:7,c:6,h:1,l:2,m:2,r:4,q:4,s:4,t:2,v:1,z:0},d=[];a.is(b,E)&&a.is(b[0],E)&&(d=by(b)),d.length||r(b).replace(Y,function(a,b,e){var f=[],g=b.toLowerCase();e.replace($,function(a,b){b&&f.push(+b)}),g=="m"&&f.length>2&&(d.push([b][n](f.splice(0,2))),g="l",b=b=="m"?"l":"L");if(g=="r")d.push([b][n](f));else while(f.length>=c[g]){d.push([b][n](f.splice(0,c[g])));if(!c[g])break}}),d.toString=a._path2string;return d}),a.parseTransformString=bt(function(b){if(!b)return null;var c={r:3,s:4,t:2,m:6},d=[];a.is(b,E)&&a.is(b[0],E)&&(d=by(b)),d.length||r(b).replace(Z,function(a,b,c){var e=[],f=v.call(b);c.replace($,function(a,b){b&&e.push(+b)}),d.push([b][n](e))}),d.toString=a._path2string;return d}),a.findDotsAtSegment=function(a,b,c,d,e,f,g,h,i){var j=1-i,k=A(j,3),l=A(j,2),m=i*i,n=m*i,o=k*a+l*3*i*c+j*3*i*i*e+n*g,p=k*b+l*3*i*d+j*3*i*i*f+n*h,q=a+2*i*(c-a)+m*(e-2*c+a),r=b+2*i*(d-b)+m*(f-2*d+b),s=c+2*i*(e-c)+m*(g-2*e+c),t=d+2*i*(f-d)+m*(h-2*f+d),u=j*a+i*c,v=j*b+i*d,x=j*e+i*g,y=j*f+i*h,z=90-w.atan2(q-s,r-t)*180/B;(q>s||r1&&(v=w.sqrt(v),c=v*c,d=v*d);var x=c*c,y=d*d,A=(f==g?-1:1)*w.sqrt(z((x*y-x*u*u-y*t*t)/(x*u*u+y*t*t))),C=A*c*u/d+(a+h)/2,D=A*-d*t/c+(b+i)/2,E=w.asin(((b-D)/d).toFixed(9)),F=w.asin(((i-D)/d).toFixed(9));E=aF&&(E=E-B*2),!g&&F>E&&(F=F-B*2)}else E=j[0],F=j[1],C=j[2],D=j[3];var G=F-E;if(z(G)>k){var H=F,I=h,J=i;F=E+k*(g&&F>E?1:-1),h=C+c*w.cos(F),i=D+d*w.sin(F),m=bD(h,i,c,d,e,0,g,I,J,[F,H,C,D])}G=F-E;var K=w.cos(E),L=w.sin(E),M=w.cos(F),N=w.sin(F),O=w.tan(G/4),P=4/3*c*O,Q=4/3*d*O,R=[a,b],S=[a+P*L,b-Q*K],T=[h+P*N,i-Q*M],U=[h,i];S[0]=2*R[0]-S[0],S[1]=2*R[1]-S[1];if(j)return[S,T,U][n](m);m=[S,T,U][n](m).join()[s](",");var V=[];for(var W=0,X=m.length;W"1e12"&&(l=.5),z(n)>"1e12"&&(n=.5),l>0&&l<1&&(q=bE(a,b,c,d,e,f,g,h,l),p.push(q.x),o.push(q.y)),n>0&&n<1&&(q=bE(a,b,c,d,e,f,g,h,n),p.push(q.x),o.push(q.y)),i=f-2*d+b-(h-2*f+d),j=2*(d-b)-2*(f-d),k=b-d,l=(-j+w.sqrt(j*j-4*i*k))/2/i,n=(-j-w.sqrt(j*j-4*i*k))/2/i,z(l)>"1e12"&&(l=.5),z(n)>"1e12"&&(n=.5),l>0&&l<1&&(q=bE(a,b,c,d,e,f,g,h,l),p.push(q.x),o.push(q.y)),n>0&&n<1&&(q=bE(a,b,c,d,e,f,g,h,n),p.push(q.x),o.push(q.y));return{min:{x:y[m](0,p),y:y[m](0,o)},max:{x:x[m](0,p),y:x[m](0,o)}}}),bG=a._path2curve=bt(function(a,b){var c=bA(a),d=b&&bA(b),e={x:0,y:0,bx:0,by:0,X:0,Y:0,qx:null,qy:null},f={x:0,y:0,bx:0,by:0,X:0,Y:0,qx:null,qy:null},g=function(a,b){var c,d;if(!a)return["C",b.x,b.y,b.x,b.y,b.x,b.y];!(a[0]in{T:1,Q:1})&&(b.qx=b.qy=null);switch(a[0]){case"M":b.X=a[1],b.Y=a[2];break;case"A":a=["C"][n](bD[m](0,[b.x,b.y][n](a.slice(1))));break;case"S":c=b.x+(b.x-(b.bx||b.x)),d=b.y+(b.y-(b.by||b.y)),a=["C",c,d][n](a.slice(1));break;case"T":b.qx=b.x+(b.x-(b.qx||b.x)),b.qy=b.y+(b.y-(b.qy||b.y)),a=["C"][n](bC(b.x,b.y,b.qx,b.qy,a[1],a[2]));break;case"Q":b.qx=a[1],b.qy=a[2],a=["C"][n](bC(b.x,b.y,a[1],a[2],a[3],a[4]));break;case"L":a=["C"][n](bB(b.x,b.y,a[1],a[2]));break;case"H":a=["C"][n](bB(b.x,b.y,a[1],b.y));break;case"V":a=["C"][n](bB(b.x,b.y,b.x,a[1]));break;case"Z":a=["C"][n](bB(b.x,b.y,b.X,b.Y))}return a},h=function(a,b){if(a[b].length>7){a[b].shift();var e=a[b];while(e.length)a.splice(b++,0,["C"][n](e.splice(0,6)));a.splice(b,1),k=x(c.length,d&&d.length||0)}},i=function(a,b,e,f,g){a&&b&&a[g][0]=="M"&&b[g][0]!="M"&&(b.splice(g,0,["M",f.x,f.y]),e.bx=0,e.by=0,e.x=a[g][1],e.y=a[g][2],k=x(c.length,d&&d.length||0))};for(var j=0,k=x(c.length,d&&d.length||0);j=j)return p;o=p}if(j==null)return k},cg=function(b,c){return function(d,e,f){d=bG(d);var g,h,i,j,k="",l={},m,n=0;for(var o=0,p=d.length;oe){if(c&&!l.start){m=cf(g,h,i[1],i[2],i[3],i[4],i[5],i[6],e-n),k+=["C"+m.start.x,m.start.y,m.m.x,m.m.y,m.x,m.y];if(f)return k;l.start=k,k=["M"+m.x,m.y+"C"+m.n.x,m.n.y,m.end.x,m.end.y,i[5],i[6]].join(),n+=j,g=+i[5],h=+i[6];continue}if(!b&&!c){m=cf(g,h,i[1],i[2],i[3],i[4],i[5],i[6],e-n);return{x:m.x,y:m.y,alpha:m.alpha}}}n+=j,g=+i[5],h=+i[6]}k+=i.shift()+i}l.end=k,m=b?n:c?l:a.findDotsAtSegment(g,h,i[0],i[1],i[2],i[3],i[4],i[5],1),m.alpha&&(m={x:m.x,y:m.y,alpha:m.alpha});return m}},ch=cg(1),ci=cg(),cj=cg(0,1);a.getTotalLength=ch,a.getPointAtLength=ci,a.getSubpath=function(a,b,c){if(this.getTotalLength(a)-c<1e-6)return cj(a,b).end;var d=cj(a,c,1);return b?cj(d,b).end:d},b$.getTotalLength=function(){if(this.type=="path"){if(this.node.getTotalLength)return this.node.getTotalLength();return ch(this.attrs.path)}},b$.getPointAtLength=function(a){if(this.type=="path")return ci(this.attrs.path,a)},b$.getSubpath=function(b,c){if(this.type=="path")return a.getSubpath(this.attrs.path,b,c)};var ck=a.easing_formulas={linear:function(a){return a},"<":function(a){return A(a,1.7)},">":function(a){return A(a,.48)},"<>":function(a){var b=.48-a/1.04,c=w.sqrt(.1734+b*b),d=c-b,e=A(z(d),1/3)*(d<0?-1:1),f=-c-b,g=A(z(f),1/3)*(f<0?-1:1),h=e+g+.5;return(1-h)*3*h*h+h*h*h},backIn:function(a){var b=1.70158;return a*a*((b+1)*a-b)},backOut:function(a){a=a-1;var b=1.70158;return a*a*((b+1)*a+b)+1},elastic:function(a){if(a==!!a)return a;return A(2,-10*a)*w.sin((a-.075)*2*B/.3)+1},bounce:function(a){var b=7.5625,c=2.75,d;a<1/c?d=b*a*a:a<2/c?(a-=1.5/c,d=b*a*a+.75):a<2.5/c?(a-=2.25/c,d=b*a*a+.9375):(a-=2.625/c,d=b*a*a+.984375);return d}};ck.easeIn=ck["ease-in"]=ck["<"],ck.easeOut=ck["ease-out"]=ck[">"],ck.easeInOut=ck["ease-in-out"]=ck["<>"],ck["back-in"]=ck.backIn,ck["back-out"]=ck.backOut;var cl=[],cm=window.requestAnimationFrame||window.webkitRequestAnimationFrame||window.mozRequestAnimationFrame||window.oRequestAnimationFrame||window.msRequestAnimationFrame||function(a){setTimeout(a,16)},cn=function(){var b=+(new Date),c=0;for(;c1&&!d.next){for(s in k)k[g](s)&&(r[s]=d.totalOrigin[s]);d.el.attr(r),cr(d.anim,d.el,d.anim.percents[0],null,d.totalOrigin,d.repeat-1)}d.next&&!d.stop&&cr(d.anim,d.el,d.next,null,d.totalOrigin,d.repeat)}}a.svg&&m&&m.paper&&m.paper.safari(),cl.length&&cm(cn)},co=function(a){return a>255?255:a<0?0:a};b$.animateWith=function(b,c,d,e,f,g){var h=d?a.animation(d,e,f,g):c,i=b.status(c);return this.animate(h).status(h,i*c.ms/h.ms)},b$.onAnimation=function(a){a?eve.on("anim.frame."+this.id,a):eve.unbind("anim.frame."+this.id);return this},cq.prototype.delay=function(a){var b=new cq(this.anim,this.ms);b.times=this.times,b.del=+a||0;return b},cq.prototype.repeat=function(a){var b=new cq(this.anim,this.ms);b.del=this.del,b.times=w.floor(x(a,0))||1;return b},a.animation=function(b,c,d,e){if(b instanceof cq)return b;if(a.is(d,"function")||!d)e=e||d||null,d=null;b=Object(b),c=+c||0;var f={},h,i;for(i in b)b[g](i)&&Q(i)!=i&&Q(i)+"%"!=i&&(h=!0,f[i]=b[i]);if(!h)return new cq(b,c);d&&(f.easing=d),e&&(f.callback=e);return new cq({100:f},c)},b$.animate=function(b,c,d,e){var f=this;if(f.removed){e&&e.call(f);return f}var g=b instanceof cq?b:a.animation(b,c,d,e);cr(g,f,g.percents[0],null,f.attr());return f},b$.setTime=function(a,b){a&&b!=null&&this.status(a,y(b,a.ms)/a.ms);return this},b$.status=function(a,b){var c=[],d=0,e,f;if(b!=null){cr(a,this,-1,y(b,1));return this}e=cl.length;for(;d.5)*2-1;i(m-.5,2)+i(n-.5,2)>.25&&(n=f.sqrt(.25-i(m-.5,2))*e+.5)&&n!=.5&&(n=n.toFixed(5)-1e-5*e)}return l}),e=e.split(/\s*\-\s*/);if(j=="linear"){var t=e.shift();t=-d(t);if(isNaN(t))return null;var u=[0,0,f.cos(a.rad(t)),f.sin(a.rad(t))],v=1/(g(h(u[2]),h(u[3]))||1);u[2]*=v,u[3]*=v,u[2]<0&&(u[0]=-u[2],u[2]=0),u[3]<0&&(u[1]=-u[3],u[3]=0)}var w=a._parseDots(e);if(!w)return null;k=k.replace(/[\(\)\s,\xb0#]/g,"_"),b.gradient&&k!=b.gradient.id&&(p.defs.removeChild(b.gradient),delete b.gradient);if(!b.gradient){s=q(j+"Gradient",{id:k}),b.gradient=s,q(s,j=="radial"?{fx:m,fy:n}:{x1:u[0],y1:u[1],x2:u[2],y2:u[3],gradientTransform:b.matrix.invert()}),p.defs.appendChild(s);for(var x=0,y=w.length;x1?G.opacity/100:G.opacity});case"stroke":G=a.getRGB(p),i.setAttribute(o,G.hex),o=="stroke"&&G[b]("opacity")&&q(i,{"stroke-opacity":G.opacity>1?G.opacity/100:G.opacity}),o=="stroke"&&d._.arrows&&("startString"in d._.arrows&&t(d,d._.arrows.startString),"endString"in d._.arrows&&t(d,d._.arrows.endString,1));break;case"gradient":(d.type=="circle"||d.type=="ellipse"||c(p).charAt()!="r")&&r(d,p);break;case"opacity":k.gradient&&!k[b]("stroke-opacity")&&q(i,{"stroke-opacity":p>1?p/100:p});case"fill-opacity":if(k.gradient){H=a._g.doc.getElementById(i.getAttribute("fill").replace(/^url\(#|\)$/g,l)),H&&(I=H.getElementsByTagName("stop"),q(I[I.length-1],{"stop-opacity":p}));break};default:o=="font-size"&&(p=e(p,10)+"px");var J=o.replace(/(\-.)/g,function(a){return a.substring(1).toUpperCase()});i.style[J]=p,d._.dirty=1,i.setAttribute(o,p)}}y(d,f),i.style.visibility=m},x=1.2,y=function(d,f){if(d.type=="text"&&!!(f[b]("text")||f[b]("font")||f[b]("font-size")||f[b]("x")||f[b]("y"))){var g=d.attrs,h=d.node,i=h.firstChild?e(a._g.doc.defaultView.getComputedStyle(h.firstChild,l).getPropertyValue("font-size"),10):10;if(f[b]("text")){g.text=f.text;while(h.firstChild)h.removeChild(h.firstChild);var j=c(f.text).split("\n"),k=[],m;for(var n=0,o=j.length;n"));var $=X.getBoundingClientRect();t.W=m.w=($.right-$.left)/Y,t.H=m.h=($.bottom-$.top)/Y,t.X=m.x,t.Y=m.y+t.H/2,("x"in i||"y"in i)&&(t.path.v=a.format("m{0},{1}l{2},{1}",f(m.x*u),f(m.y*u),f(m.x*u)+1));var _=["x","y","text","font","font-family","font-weight","font-style","font-size"];for(var ba=0,bb=_.length;ba.25&&(c=e.sqrt(.25-i(b-.5,2))*((c>.5)*2-1)+.5),m=b+n+c);return o}),f=f.split(/\s*\-\s*/);if(l=="linear"){var p=f.shift();p=-d(p);if(isNaN(p))return null}var q=a._parseDots(f);if(!q)return null;b=b.shape||b.node;if(q.length){b.removeChild(g),g.on=!0,g.method="none",g.color=q[0].color,g.color2=q[q.length-1].color;var r=[];for(var s=0,t=q.length;s')}}catch(c){F=function(a){return b.createElement("<"+a+' xmlns="urn:schemas-microsoft.com:vml" class="rvml">')}}},a._engine.initWin(a._g.win),a._engine.create=function(){var b=a._getContainer.apply(0,arguments),c=b.container,d=b.height,e,f=b.width,g=b.x,h=b.y;if(!c)throw new Error("VML container not found.");var i=new a._Paper,j=i.canvas=a._g.doc.createElement("div"),k=j.style;g=g||0,h=h||0,f=f||512,d=d||342,i.width=f,i.height=d,f==+f&&(f+="px"),d==+d&&(d+="px"),i.coordsize=u*1e3+n+u*1e3,i.coordorigin="0 0",i.span=a._g.doc.createElement("span"),i.span.style.cssText="position:absolute;left:-9999em;top:-9999em;padding:0;margin:0;line-height:1;",j.appendChild(i.span),k.cssText=a.format("top:0;left:0;width:{0};height:{1};display:inline-block;position:relative;clip:rect(0 {0} {1} 0);overflow:hidden",f,d),c==1?(a._g.doc.body.appendChild(j),k.left=g+"px",k.top=h+"px",k.position="absolute"):c.firstChild?c.insertBefore(j,c.firstChild):c.appendChild(j),i.renderfix=function(){};return i},a.prototype.clear=function(){a.eve("clear",this),this.canvas.innerHTML=o,this.span=a._g.doc.createElement("span"),this.span.style.cssText="position:absolute;left:-9999em;top:-9999em;padding:0;margin:0;line-height:1;display:inline;",this.canvas.appendChild(this.span),this.bottom=this.top=null},a.prototype.remove=function(){a.eve("remove",this),this.canvas.parentNode.removeChild(this.canvas);for(var b in this)this[b]=typeof this[b]=="function"?a._removedFactory(b):null;return!0};var G=a.st;for(var H in E)E[b](H)&&!G[b](H)&&(G[H]=function(a){return function(){var b=arguments;return this.forEach(function(c){c[a].apply(c,b)})}}(H))}(window.Raphael) \ No newline at end of file diff --git a/webroot/rsrc/js/application/maniphest/behavior-line-chart.js b/webroot/rsrc/js/application/maniphest/behavior-line-chart.js index 6e66b19d2c..2f63657c56 100644 --- a/webroot/rsrc/js/application/maniphest/behavior-line-chart.js +++ b/webroot/rsrc/js/application/maniphest/behavior-line-chart.js @@ -3,86 +3,121 @@ * @requires javelin-behavior * javelin-dom * javelin-vector + * phui-chart-css */ JX.behavior('line-chart', function(config) { + function fn(n) { + return n + '(' + JX.$A(arguments).slice(1).join(', ') + ')'; + } + var h = JX.$(config.hardpoint); - var p = JX.$V(h); var d = JX.Vector.getDim(h); - var mx = 60; - var my = 30; - var r = new Raphael(h, d.x, d.y); + var padding = { + top: 24, + left: 48, + bottom: 48, + right: 32 + }; - var l = r.linechart( - mx, my, - d.x - (2 * mx), d.y - (2 * my), - config.x, - config.y, - { - nostroke: false, - axis: '0 0 1 1', - shade: true, - gutter: 1, - colors: config.colors || ['#2980b9'] + var size = { + frameWidth: d.x, + frameHeight: d.y, + }; + + size.width = size.frameWidth - padding.left - padding.right; + size.height = size.frameHeight - padding.top - padding.bottom; + + var x = d3.time.scale() + .range([0, size.width]); + + var y = d3.scale.linear() + .range([size.height, 0]); + + var xAxis = d3.svg.axis() + .scale(x) + .orient('bottom'); + + var yAxis = d3.svg.axis() + .scale(y) + .orient('left'); + + var svg = d3.select('#' + config.hardpoint).append('svg') + .attr('width', size.frameWidth) + .attr('height', size.frameHeight) + .attr('class', 'chart'); + + var g = svg.append('g') + .attr('transform', fn('translate', padding.left, padding.top)); + + g.append('rect') + .attr('class', 'inner') + .attr('width', size.width) + .attr('height', size.height); + + var line = d3.svg.line() + .x(function(d) { return x(d.date); }) + .y(function(d) { return y(d.count); }); + + var data = []; + for (var ii = 0; ii < config.x[0].length; ii++) { + data.push( + { + date: new Date(config.x[0][ii] * 1000), + count: +config.y[0][ii] + }); + } + + x.domain(d3.extent(data, function(d) { return d.date; })); + + var yex = d3.extent(data, function(d) { return d.count; }); + yex[0] = 0; + yex[1] = yex[1] * 1.05; + y.domain(yex); + + g.append('path') + .datum(data) + .attr('class', 'line') + .attr('d', line); + + g.append('g') + .attr('class', 'x axis') + .attr('transform', fn('translate', 0, size.height)) + .call(xAxis); + + g.append('g') + .attr('class', 'y axis') + .attr('transform', fn('translate', 0, 0)) + .call(yAxis); + + var div = d3.select('body') + .append('div') + .attr('class', 'chart-tooltip') + .style('opacity', 0); + + g.selectAll('dot') + .data(data) + .enter() + .append('circle') + .attr('class', 'point') + .attr('r', 3) + .attr('cx', function(d) { return x(d.date); }) + .attr('cy', function(d) { return y(d.count); }) + .on('mouseover', function(d) { + var d_y = d.date.getFullYear(); + var d_m = d.date.getMonth(); + var d_d = d.date.getDate(); + + div + .html(d_y + '-' + d_m + '-' + d_d + ': ' + d.count) + .style('opacity', 0.9) + .style('left', (d3.event.pageX - 60) + 'px') + .style('top', (d3.event.pageY - 38) + 'px'); + }) + .on('mouseout', function() { + div.style('opacity', 0); }); - function format(value, type) { - switch (type) { - case 'epoch': - return new Date(parseInt(value, 10) * 1000).toLocaleDateString(); - case 'int': - return parseInt(value, 10); - default: - return value; - } - } - - // Format the X axis. - - var n = 2; - var ii = 0; - var text = l.axis[0].text.items; - for (var k in text) { - if (ii++ % n) { - text[k].attr({text: ''}); - } else { - var cur = text[k].attr('text'); - var str = format(cur, config.xformat); - text[k].attr({text: str}); - } - } - - // Show values on hover. - - l.hoverColumn(function() { - this.tags = r.set(); - for (var yy = 0; yy < config.y.length; yy++) { - var yvalue = 0; - for (var ii = 0; ii < config.x[0].length; ii++) { - if (config.x[0][ii] > this.axis) { - break; - } - yvalue = format(config.y[yy][ii], config.yformat); - } - - var xvalue = format(this.axis, config.xformat); - - var tag = r.tag( - this.x, - this.y[yy], - [xvalue, yvalue].join('\n'), - 180, - 24); - tag - .insertBefore(this) - .attr([{fill : '#fff'}, {fill: '#000'}]); - - this.tags.push(tag); - } - }, function() { - this.tags && this.tags.remove(); - }); - }); From d41aaba2a184e0f57c68dfd56c95e628d55d8995 Mon Sep 17 00:00:00 2001 From: Mike Riley Date: Mon, 1 Feb 2016 19:04:28 +0000 Subject: [PATCH 13/54] Fix coverage line index lookup in diffusion browser Summary: I believe this got clobbered in rP8b6edaa4e238a809fe78e6d14ad0705545f8179f. This index doesn't seem to be present in the line dictionary and we're now relying on `$line_index` for the current position. Test Plan: before {F1085522} after {F1085521} Reviewers: #blessed_reviewers, epriestley Reviewed By: #blessed_reviewers, epriestley Subscribers: epriestley Differential Revision: https://secure.phabricator.com/D15156 --- .../diffusion/controller/DiffusionBrowseController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/applications/diffusion/controller/DiffusionBrowseController.php b/src/applications/diffusion/controller/DiffusionBrowseController.php index e5d2f7bdfa..a2e9a3a4b3 100644 --- a/src/applications/diffusion/controller/DiffusionBrowseController.php +++ b/src/applications/diffusion/controller/DiffusionBrowseController.php @@ -1180,7 +1180,7 @@ final class DiffusionBrowseController extends DiffusionController { if ($this->coverage) { require_celerity_resource('differential-changeset-view-css'); - $cov_index = $line['line'] - 1; + $cov_index = $line_index; if (isset($this->coverage[$cov_index])) { $cov_class = $this->coverage[$cov_index]; From 367b92b7fe8d350239298c4badc6807bebc2f6e4 Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 1 Feb 2016 12:42:38 -0800 Subject: [PATCH 14/54] Fix an issue where drag positions could get out of sync after scrolling Summary: Ref T5240. Currently, we calculate drag positions assuming the "ghost" element is not present (it isn't, usually), then adjust them while dragging to account for the ghost. However, this fails after scrolling: we dirty the cache, but the ghost //is// present. We continue adjusting for it, but essentially double-adjust. This leads to scroll positions being about 80-ish px off from where they should be. Test Plan: - Begin dragging a task in a long task list. - While dragging, use mousewheel to scroll to the bottom of the list. - Drag task downward through the list. - Before fix: ghost is off by, like, an inch or so. - After fix: ghost position is accurate to cursor position. Reviewers: chad Reviewed By: chad Maniphest Tasks: T5240 Differential Revision: https://secure.phabricator.com/D15157 --- resources/celerity/map.php | 22 +++++++++++----------- webroot/rsrc/js/core/DraggableList.js | 24 +++++++----------------- 2 files changed, 18 insertions(+), 28 deletions(-) diff --git a/resources/celerity/map.php b/resources/celerity/map.php index ac56ed3d1b..a51be6d888 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -8,7 +8,7 @@ return array( 'names' => array( 'core.pkg.css' => '0f87bfe0', - 'core.pkg.js' => 'a79eed25', + 'core.pkg.js' => 'bf947f93', 'darkconsole.pkg.js' => 'e7393ebb', 'differential.pkg.css' => '2de124c9', 'differential.pkg.js' => '5c2ba922', @@ -446,7 +446,7 @@ return array( 'rsrc/js/application/uiexample/notification-example.js' => '8ce821c5', 'rsrc/js/core/Busy.js' => '59a7976a', 'rsrc/js/core/DragAndDropFileUpload.js' => 'ad10aeac', - 'rsrc/js/core/DraggableList.js' => 'a16ec1c6', + 'rsrc/js/core/DraggableList.js' => '255d85da', 'rsrc/js/core/FileUpload.js' => '477359c8', 'rsrc/js/core/Hovercard.js' => 'c6f720ff', 'rsrc/js/core/KeyboardShortcut.js' => '1ae869f2', @@ -741,7 +741,7 @@ return array( 'phabricator-countdown-css' => 'e7544472', 'phabricator-dashboard-css' => 'eb458607', 'phabricator-drag-and-drop-file-upload' => 'ad10aeac', - 'phabricator-draggable-list' => 'a16ec1c6', + 'phabricator-draggable-list' => '255d85da', 'phabricator-fatal-config-template-css' => '8e6c6fcd', 'phabricator-feed-css' => 'ecd4ec57', 'phabricator-file-upload' => '477359c8', @@ -1021,6 +1021,14 @@ return array( 'phabricator-drag-and-drop-file-upload', 'phabricator-draggable-list', ), + '255d85da' => array( + 'javelin-install', + 'javelin-dom', + 'javelin-stratcom', + 'javelin-util', + 'javelin-vector', + 'javelin-magical-init', + ), '2926fff2' => array( 'javelin-behavior', 'javelin-dom', @@ -1587,14 +1595,6 @@ return array( 'javelin-dom', 'javelin-reactor-dom', ), - 'a16ec1c6' => array( - 'javelin-install', - 'javelin-dom', - 'javelin-stratcom', - 'javelin-util', - 'javelin-vector', - 'javelin-magical-init', - ), 'a2828756' => array( 'javelin-dom', 'javelin-util', diff --git a/webroot/rsrc/js/core/DraggableList.js b/webroot/rsrc/js/core/DraggableList.js index 1f8f50c7aa..b15179a9a1 100644 --- a/webroot/rsrc/js/core/DraggableList.js +++ b/webroot/rsrc/js/core/DraggableList.js @@ -290,6 +290,11 @@ JX.install('DraggableList', { } this._target = false; + + // Clear the target position cache, since adding or removing ghosts + // changes element positions. + this._dirtyTargetCache(); + return this; }, @@ -298,9 +303,6 @@ JX.install('DraggableList', { var targets = this._getTargets(); var dragging = this._dragging; - var adjust_h = JX.Vector.getDim(ghost).y; - var adjust_y = JX.$V(ghost).y; - // Find the node we're dragging the object underneath. This is the first // node in the list that's above the cursor. If that node is the node // we're dragging or its predecessor, don't select a target, because the @@ -314,34 +316,23 @@ JX.install('DraggableList', { var cur_target = null; var trigger; for (var ii = 0; ii < targets.length; ii++) { - - // If the drop target indicator is above the target, we need to adjust - // the target's trigger height down accordingly. This makes dragging - // items down the list smoother, because the target doesn't jump to the - // next item while the cursor is over it. - trigger = targets[ii].y; - if (adjust_y <= trigger) { - trigger += adjust_h; - } // If the cursor is above this target, we aren't dropping underneath it. - if (trigger >= p.y) { continue; } // Don't choose the dragged row or its predecessor as targets. - cur_target = targets[ii].item; if (!dragging) { // If the item on the cursor isn't from this list, it can't be // dropped onto itself or its predecessor in this list. } else { - if (cur_target == dragging) { + if (cur_target === dragging) { cur_target = false; } - if (targets[ii - 1] && targets[ii - 1].item == dragging) { + if (targets[ii - 1] && (targets[ii - 1].item === dragging)) { cur_target = false; } } @@ -480,7 +471,6 @@ JX.install('DraggableList', { for (var ii = 0; ii < group.length; ii++) { JX.DOM.alterClass(group[ii].getRootNode(), 'drag-target-list', false); group[ii]._clearTarget(); - group[ii]._dirtyTargetCache(); group[ii]._lastAdjust = null; } From 7dfe0444268024429f901d9f855b4cab777499d5 Mon Sep 17 00:00:00 2001 From: Chad Little Date: Mon, 1 Feb 2016 14:39:09 -0800 Subject: [PATCH 15/54] Add ownerheads to workboard cards Summary: Reworks cards to add an assignee head and tooltip on workboards. This feels like a reasonable starting point, but they may move depending on feedback. Test Plan: View a lot of boards. Assign and unassign a task. {F1085739} Reviewers: epriestley Reviewed By: epriestley Subscribers: Luke081515.2, Korvin Differential Revision: https://secure.phabricator.com/D15158 --- resources/celerity/map.php | 20 +++++++++--------- .../project/view/ProjectBoardTaskCard.php | 2 +- src/view/phui/PHUIObjectItemView.php | 14 ++++++++----- .../css/phui/phui-object-item-list-view.css | 21 +++++++------------ .../css/phui/workboards/phui-workcard.css | 10 +++++++-- .../css/phui/workboards/phui-workpanel.css | 2 +- 6 files changed, 37 insertions(+), 32 deletions(-) diff --git a/resources/celerity/map.php b/resources/celerity/map.php index a51be6d888..7e95abd0f2 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -7,7 +7,7 @@ */ return array( 'names' => array( - 'core.pkg.css' => '0f87bfe0', + 'core.pkg.css' => '8b9c004a', 'core.pkg.js' => 'bf947f93', 'darkconsole.pkg.js' => 'e7393ebb', 'differential.pkg.css' => '2de124c9', @@ -142,7 +142,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' => '0d484a97', + 'rsrc/css/phui/phui-object-item-list-view.css' => 'febf4a79', 'rsrc/css/phui/phui-pager.css' => 'bea33d23', 'rsrc/css/phui/phui-pinboard-view.css' => '2495140e', 'rsrc/css/phui/phui-profile-menu.css' => 'ab4fcf5f', @@ -154,8 +154,8 @@ 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' => '0dfd1880', - 'rsrc/css/phui/workboards/phui-workpanel.css' => 'e9339dc3', + 'rsrc/css/phui/workboards/phui-workcard.css' => 'ddb93318', + 'rsrc/css/phui/workboards/phui-workpanel.css' => 'b90970eb', 'rsrc/css/sprite-login.css' => '60e8560e', 'rsrc/css/sprite-menu.css' => '9dd65b92', 'rsrc/css/sprite-tokens.css' => '4f399012', @@ -818,7 +818,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' => '0d484a97', + 'phui-object-item-list-view-css' => 'febf4a79', 'phui-pager-css' => 'bea33d23', 'phui-pinboard-view-css' => '2495140e', 'phui-profile-menu-css' => 'ab4fcf5f', @@ -831,8 +831,8 @@ return array( 'phui-timeline-view-css' => '2efceff8', 'phui-two-column-view-css' => 'c75bfc5b', 'phui-workboard-view-css' => 'b07a5524', - 'phui-workcard-view-css' => '0dfd1880', - 'phui-workpanel-view-css' => 'e9339dc3', + 'phui-workcard-view-css' => 'ddb93318', + 'phui-workpanel-view-css' => 'b90970eb', 'phuix-action-list-view' => 'b5c256b8', 'phuix-action-view' => '8cf6d262', 'phuix-autocomplete' => '9196fb06', @@ -1752,6 +1752,9 @@ return array( 'b6b0d1bb' => array( 'phui-inline-comment-view-css', ), + 'b90970eb' => array( + 'phui-workcard-view-css', + ), 'ba4fa35c' => array( 'javelin-behavior', 'javelin-dom', @@ -1981,9 +1984,6 @@ return array( 'e6e25838' => array( 'javelin-install', ), - 'e9339dc3' => array( - 'phui-workcard-view-css', - ), 'e9581f08' => array( 'javelin-behavior', 'javelin-stratcom', diff --git a/src/applications/project/view/ProjectBoardTaskCard.php b/src/applications/project/view/ProjectBoardTaskCard.php index 86f4359c08..2ac29f11ec 100644 --- a/src/applications/project/view/ProjectBoardTaskCard.php +++ b/src/applications/project/view/ProjectBoardTaskCard.php @@ -80,7 +80,7 @@ final class ProjectBoardTaskCard extends Phobject { ->setBarColor($bar_color); if ($owner) { - $card->addAttribute($owner->renderLink()); + $card->addHandleIcon($owner, $owner->getName()); } $project_phids = array_fuse($task->getProjectPHIDs()); diff --git a/src/view/phui/PHUIObjectItemView.php b/src/view/phui/PHUIObjectItemView.php index b3c4368c22..e89630cedb 100644 --- a/src/view/phui/PHUIObjectItemView.php +++ b/src/view/phui/PHUIObjectItemView.php @@ -452,14 +452,15 @@ final class PHUIObjectItemView extends AphrontTagView { $icon_list); } + $handle_bar = null; if ($this->handleIcons) { $handle_bar = array(); foreach ($this->handleIcons as $handleicon) { $handle_bar[] = $this->renderHandleIcon($handleicon['icon'], $handleicon['label']); } - $icons[] = phutil_tag( - 'div', + $handle_bar = phutil_tag( + 'li', array( 'class' => 'phui-object-item-handle-icons', ), @@ -504,7 +505,7 @@ final class PHUIObjectItemView extends AphrontTagView { } $attrs = null; - if ($this->attributes) { + if ($this->attributes || $handle_bar) { $attrs = array(); $spacer = phutil_tag( 'span', @@ -531,7 +532,10 @@ final class PHUIObjectItemView extends AphrontTagView { array( 'class' => 'phui-object-item-attributes', ), - $attrs); + array( + $handle_bar, + $attrs, + )); } $status = null; @@ -750,7 +754,7 @@ final class PHUIObjectItemView extends AphrontTagView { if (strlen($label)) { $options['sigil'] = 'has-tooltip'; - $options['meta'] = array('tip' => $label); + $options['meta'] = array('tip' => $label, 'align' => 'E'); } return javelin_tag('span', $options, ''); 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 b1e718cc6a..e8ad8b6866 100644 --- a/webroot/rsrc/css/phui/phui-object-item-list-view.css +++ b/webroot/rsrc/css/phui/phui-object-item-list-view.css @@ -297,11 +297,13 @@ ul.phui-object-item-list-view { .phui-object-item-attributes { padding: 0 8px 6px; line-height: 18px; + min-height: 21px; } .phui-object-item-attribute { - display: inline; + display: inline-block; color: {$greytext}; + vertical-align: middle; } .phui-object-item-attribute-spacer { @@ -324,10 +326,6 @@ ul.phui-object-item-list-view { margin: 0 0 4px; } -.phui-object-item-with-handle-icons .phui-object-item-icons { - padding-bottom: 30px; -} - .phui-object-item-icons { padding: 0 4px 0 0; } @@ -537,20 +535,17 @@ ul.phui-object-item-list-view .phui-object-item-selected */ .phui-object-item-handle-icons { - height: 28px; - margin-right: 10px; bottom: 0; - right: 0; - text-align: right; + right: 4px; position: absolute; } .phui-object-item-handle-icon { - margin: 1px; - width: 28px; - height: 28px; + width: 24px; + height: 24px; display: inline-block; - background-size: 28px 28px; + background-size: 100%; + border-radius: 3px; background-repeat: no-repeat; } diff --git a/webroot/rsrc/css/phui/workboards/phui-workcard.css b/webroot/rsrc/css/phui/workboards/phui-workcard.css index c88d3df802..09bb64d59e 100644 --- a/webroot/rsrc/css/phui/workboards/phui-workcard.css +++ b/webroot/rsrc/css/phui/workboards/phui-workcard.css @@ -5,7 +5,7 @@ .phui-workpanel-view .phui-object-item { background-color: #fff; border-radius: 3px; - margin-bottom: 6px; + margin-bottom: 8px; } .phui-workpanel-view .phui-object-item-name { @@ -19,6 +19,8 @@ .phui-workpanel-view .phui-object-item-frame { border-top-right-radius: 3px; border-bottom-right-radius: 3px; + border-color: {$thinblueborder}; + border-bottom-color: {$lightblueborder}; } .phui-workpanel-view .phui-object-item .phui-object-item-objname { @@ -60,7 +62,8 @@ } .phui-workpanel-view .phui-object-item .phui-list-item-href { - height: 26px; + height: 24px; + width: 24px; } .device-desktop .phui-workpanel-view .phui-object-item:hover @@ -79,6 +82,9 @@ display: block; } +.phui-workpanel-view .phui-object-item-attributes { + margin-right: 12px; +} diff --git a/webroot/rsrc/css/phui/workboards/phui-workpanel.css b/webroot/rsrc/css/phui/workboards/phui-workpanel.css index 978a3c4863..28b98f4ea0 100644 --- a/webroot/rsrc/css/phui/workboards/phui-workpanel.css +++ b/webroot/rsrc/css/phui/workboards/phui-workpanel.css @@ -44,7 +44,7 @@ padding: 8px 8px 4px 8px; } -.device-phone .phui-workpanel-view .phui-workpanel-body { +.device .phui-workpanel-view .phui-workpanel-body { padding: 8px 0; } From 730de1b6e5e32499c19e4c8057fedd0c1d8c89be Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 1 Feb 2016 15:59:05 -0800 Subject: [PATCH 16/54] Remove an unused property from draggable lists Summary: Ref T5240. This property does nothing. Test Plan: Search, drag a card around. Reviewers: chad Reviewed By: chad Maniphest Tasks: T5240 Differential Revision: https://secure.phabricator.com/D15159 --- webroot/rsrc/js/core/DraggableList.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/webroot/rsrc/js/core/DraggableList.js b/webroot/rsrc/js/core/DraggableList.js index b15179a9a1..a929c93f66 100644 --- a/webroot/rsrc/js/core/DraggableList.js +++ b/webroot/rsrc/js/core/DraggableList.js @@ -48,7 +48,6 @@ JX.install('DraggableList', { _originScroll : null, _target : null, _targets : null, - _dimensions : null, _ghostHandler : null, _ghostNode : null, _group : null, @@ -171,7 +170,6 @@ JX.install('DraggableList', { this._dragging = e.getNode(this._sigil); this._origin = JX.$V(e); this._originScroll = JX.Vector.getAggregateScrollForNode(this._dragging); - this._dimensions = JX.$V(this._dragging); for (var ii = 0; ii < this._group.length; ii++) { this._group[ii]._clearTarget(); From fce0109822823fcdb217feee9dc3318eb51d5582 Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 1 Feb 2016 16:28:47 -0800 Subject: [PATCH 17/54] When dragging nodes, clone them Summary: Ref T5240. Currently, when dragging nodes, we leave them where they are in the document and apply "position: relative;" so we can move them around on screen. - Pros: All the CSS still works. - Cons: Can't drag them outside the nearest containing element with "overflow: hidden;", many subtle positioning bugs with scrollable containers. Instead, this diff leaves the thing we're dragging exactly where it is, clones it, and drags the clone instead. - Pros: You can drag it anywhere. Seems to fix all the scrolling container problems. - Cons: CSS which depends on a container class no longer works. The CSS thing is bad, but doesn't seem too unreasonable to fix. Basically, we just need to put some `phui-this-is-a-workboard-card` class on the cards, and use that to style them instead of `phui-workboard-view`, and then do something similar for draggable lists. Although we no longer need to drag cards to tabs with the current design, I think there's a reasonable chance we'll revisit that later. The current design also calls for scrollable columns, but there would be no way to drag cards outside of their current column with the current approach. NOTE: This does not attempt to fix the CSS, so dragging is pretty rough, since the "clone" loses a number of container classes and thus a number of rules. I'll clean up the CSS in the next change. Test Plan: - Dragged stuff around on task lists, workboards, and sort lists (e.g., pinned applications) in Safari, Firefox and Chrome. - Scrolled window and containers (workboards) during drag. - Dragged stuff out of the workboard. - Dragged stuff offscreen. - CSS is funky, but I can no longer find any positioning or layout issues in any browser. Reviewers: chad Reviewed By: chad Maniphest Tasks: T5240 Differential Revision: https://secure.phabricator.com/D15160 --- resources/celerity/map.php | 32 ++--- src/view/phui/PHUIObjectItemView.php | 21 ++- webroot/rsrc/css/core/z-index.css | 8 +- .../css/phui/phui-object-item-list-view.css | 27 +++- webroot/rsrc/js/core/DraggableList.js | 135 ++++++++++-------- 5 files changed, 137 insertions(+), 86 deletions(-) diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 7e95abd0f2..26c1d58a6d 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -7,8 +7,8 @@ */ return array( 'names' => array( - 'core.pkg.css' => '8b9c004a', - 'core.pkg.js' => 'bf947f93', + 'core.pkg.css' => 'cd66e467', + 'core.pkg.js' => 'c5178ede', 'darkconsole.pkg.js' => 'e7393ebb', 'differential.pkg.css' => '2de124c9', 'differential.pkg.js' => '5c2ba922', @@ -105,7 +105,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' => 'a36a45da', + 'rsrc/css/core/z-index.css' => '5c7025bf', 'rsrc/css/diviner/diviner-shared.css' => 'aa3656aa', 'rsrc/css/font/font-aleo.css' => '8bdb2835', 'rsrc/css/font/font-awesome.css' => 'c43323c5', @@ -142,7 +142,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' => 'febf4a79', + 'rsrc/css/phui/phui-object-item-list-view.css' => '699de05e', 'rsrc/css/phui/phui-pager.css' => 'bea33d23', 'rsrc/css/phui/phui-pinboard-view.css' => '2495140e', 'rsrc/css/phui/phui-profile-menu.css' => 'ab4fcf5f', @@ -446,7 +446,7 @@ return array( 'rsrc/js/application/uiexample/notification-example.js' => '8ce821c5', 'rsrc/js/core/Busy.js' => '59a7976a', 'rsrc/js/core/DragAndDropFileUpload.js' => 'ad10aeac', - 'rsrc/js/core/DraggableList.js' => '255d85da', + 'rsrc/js/core/DraggableList.js' => 'f1844746', 'rsrc/js/core/FileUpload.js' => '477359c8', 'rsrc/js/core/Hovercard.js' => 'c6f720ff', 'rsrc/js/core/KeyboardShortcut.js' => '1ae869f2', @@ -741,7 +741,7 @@ return array( 'phabricator-countdown-css' => 'e7544472', 'phabricator-dashboard-css' => 'eb458607', 'phabricator-drag-and-drop-file-upload' => 'ad10aeac', - 'phabricator-draggable-list' => '255d85da', + 'phabricator-draggable-list' => 'f1844746', 'phabricator-fatal-config-template-css' => '8e6c6fcd', 'phabricator-feed-css' => 'ecd4ec57', 'phabricator-file-upload' => '477359c8', @@ -780,7 +780,7 @@ return array( 'phabricator-uiexample-reactor-select' => 'a155550f', 'phabricator-uiexample-reactor-sendclass' => '1def2711', 'phabricator-uiexample-reactor-sendproperties' => 'b1f0ccee', - 'phabricator-zindex-css' => 'a36a45da', + 'phabricator-zindex-css' => '5c7025bf', 'phame-css' => '6d5b3682', 'pholio-css' => '95174bdd', 'pholio-edit-css' => '3ad9d1ee', @@ -818,7 +818,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' => 'febf4a79', + 'phui-object-item-list-view-css' => '699de05e', 'phui-pager-css' => 'bea33d23', 'phui-pinboard-view-css' => '2495140e', 'phui-profile-menu-css' => 'ab4fcf5f', @@ -1021,14 +1021,6 @@ return array( 'phabricator-drag-and-drop-file-upload', 'phabricator-draggable-list', ), - '255d85da' => array( - 'javelin-install', - 'javelin-dom', - 'javelin-stratcom', - 'javelin-util', - 'javelin-vector', - 'javelin-magical-init', - ), '2926fff2' => array( 'javelin-behavior', 'javelin-dom', @@ -2019,6 +2011,14 @@ return array( 'javelin-workflow', 'javelin-json', ), + 'f1844746' => array( + 'javelin-install', + 'javelin-dom', + 'javelin-stratcom', + 'javelin-util', + 'javelin-vector', + 'javelin-magical-init', + ), 'f411b6ae' => array( 'javelin-behavior', 'javelin-stratcom', diff --git a/src/view/phui/PHUIObjectItemView.php b/src/view/phui/PHUIObjectItemView.php index e89630cedb..c7a3d074b5 100644 --- a/src/view/phui/PHUIObjectItemView.php +++ b/src/view/phui/PHUIObjectItemView.php @@ -383,17 +383,24 @@ final class PHUIObjectItemView extends AphrontTagView { ), $this->header); - $header = javelin_tag( + // Wrap the header content in a with the "slippery" sigil. This + // prevents us from beginning a drag if you click the text (like "T123"), + // but not if you click the white space after the header. + $header = phutil_tag( 'div', array( 'class' => 'phui-object-item-name', - 'sigil' => 'slippery', ), - array( - $this->headIcons, - $header_name, - $header_link, - )); + javelin_tag( + 'span', + array( + 'sigil' => 'slippery', + ), + array( + $this->headIcons, + $header_name, + $header_link, + ))); $icons = array(); if ($this->icons) { diff --git a/webroot/rsrc/css/core/z-index.css b/webroot/rsrc/css/core/z-index.css index 3025b5dc5c..2d4c050d0c 100644 --- a/webroot/rsrc/css/core/z-index.css +++ b/webroot/rsrc/css/core/z-index.css @@ -81,10 +81,6 @@ div.phui-calendar-day-event { z-index: 5; } -.drag-dragging { - z-index: 5; -} - .phui-calendar-date-number { z-index: 5; } @@ -114,6 +110,10 @@ div.phui-calendar-day-event { z-index: 9; } +.drag-frame { + z-index: 10; +} + .jx-mask { z-index: 10; } 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 e8ad8b6866..1f22d9dedb 100644 --- a/webroot/rsrc/css/phui/phui-object-item-list-view.css +++ b/webroot/rsrc/css/phui/phui-object-item-list-view.css @@ -593,15 +593,36 @@ ul.phui-object-item-list-view .phui-object-item-selected } .drag-dragging { - position: relative; - background: {$sh-yellowbackground}; - opacity: 0.9; + opacity: 0.25; } .drag-sending { opacity: 0.5; } +.drag-clone, +.drag-frame { + /* This allows mousewheel events to pass through the clone and frame while + they are being dragged. Without this, the mousewheel does not work during + a drag operation. */ + pointer-events: none; +} + +.drag-frame { + position: fixed; + overflow: hidden; + left: 0; + right: 0; + top: 0; + bottom: 0; +} + +.drag-clone { + position: absolute; + list-style: none; +} + + /* - State --------------------------------------------------------------------- Provides a list of object status or states, success or fail, etc diff --git a/webroot/rsrc/js/core/DraggableList.js b/webroot/rsrc/js/core/DraggableList.js index a929c93f66..6cb279a89b 100644 --- a/webroot/rsrc/js/core/DraggableList.js +++ b/webroot/rsrc/js/core/DraggableList.js @@ -44,15 +44,15 @@ JX.install('DraggableList', { _root : null, _dragging : null, _locked : 0, - _origin : null, - _originScroll : null, _target : null, _targets : null, _ghostHandler : null, _ghostNode : null, _group : null, _lastMousePosition: null, - _lastAdjust: null, + _frame: null, + _clone: null, + _offset: null, getRootNode : function() { return this._root; @@ -131,7 +131,17 @@ JX.install('DraggableList', { } } - return handler(); + var items = handler(); + + // Make sure the clone element is never included as a target. + for (var ii = 0; ii < items.length; ii++) { + if (items[ii] === this._clone) { + items.splice(ii, 1); + break; + } + } + + return items; }, _ondrag : function(e) { @@ -167,24 +177,67 @@ JX.install('DraggableList', { e.kill(); - this._dragging = e.getNode(this._sigil); - this._origin = JX.$V(e); - this._originScroll = JX.Vector.getAggregateScrollForNode(this._dragging); + var drag = e.getNode(this._sigil); for (var ii = 0; ii < this._group.length; ii++) { this._group[ii]._clearTarget(); } - if (!this.invoke('didBeginDrag', this._dragging).getPrevented()) { - // Set the height of all the ghosts in the group. In the normal case, - // this just sets this list's ghost height. - for (var jj = 0; jj < this._group.length; jj++) { - var ghost = this._group[jj].getGhostNode(); - ghost.style.height = JX.Vector.getDim(this._dragging).y + 'px'; - } + var pos = JX.$V(drag); + var dim = JX.Vector.getDim(drag); - JX.DOM.alterClass(this._dragging, 'drag-dragging', true); + // Create and adjust the ghost nodes. + for (var jj = 0; jj < this._group.length; jj++) { + var ghost = this._group[jj].getGhostNode(); + ghost.style.height = dim.y + 'px'; } + + // Here's what's going on: we're cloning the thing that's being dragged. + // This is the "clone", stored in "this._clone". We're going to leave the + // original where it is in the document, and put the clone at top-level + // so it can be freely dragged around the whole document, even if it's + // inside a container with overflow hidden. + + // Because the clone has been moved up, CSS classes which rely on some + // parent selector won't work. Draggable objects need to pick up all of + // their CSS properties without relying on container classes. This isn't + // great, but leaving them where they are in the document creates a large + // number of positioning problems with scrollable, absolute, relative, + // or overflow hidden containers. + + // Note that we don't actually want to let the user drag it outside the + // document. One problem is that doing so lets the user drag objects + // infinitely far to the right by dragging them to the edge so the + // document extends, scrolling the document, dragging them to the edge + // of the new larger document, scrolling the document, and so on forever. + + // To prevent this, we're putting a "frame" (stored in "this._frame") at + // top level, then putting the clone inside the frame. The frame has the + // same size as the entire viewport, and overflow hidden, so dragging the + // item outside the document just cuts it off. + + // Create the clone for dragging. + var clone = drag.cloneNode(true); + + pos.setPos(clone); + dim.setDim(clone); + + JX.DOM.alterClass(drag, 'drag-dragging', true); + JX.DOM.alterClass(clone, 'drag-clone', true); + + var frame = JX.$N('div', {className: 'drag-frame'}); + frame.appendChild(clone); + + document.body.appendChild(frame); + + this._dragging = drag; + this._clone = clone; + this._frame = frame; + + var cursor = JX.$V(e); + this._offset = new JX.Vector(pos.x - cursor.x, pos.y - cursor.y); + + this.invoke('didBeginDrag', this._dragging); }, _getTargets : function() { @@ -195,18 +248,6 @@ JX.install('DraggableList', { var item = items[ii]; var ipos = JX.$V(item); - if (item == this._dragging) { - // If the item we're measuring is also the item we're dragging, - // we need to measure its position as though it was still in the - // list, not its current position in the document (which is - // under the cursor). To do this, adjust the measured position by - // removing the offsets we added to put the item underneath the - // cursor. - if (this._lastAdjust) { - ipos.x -= this._lastAdjust.x; - ipos.y -= this._lastAdjust.y; - } - } targets.push({ item: items[ii], @@ -398,39 +439,18 @@ JX.install('DraggableList', { } } - // If the drop target indicator is above the cursor in the document, - // adjust the cursor position for the change in node document position. - // Do this before choosing a new target to avoid a flash of nonsense. + var f = JX.$V(this._frame); + p.x -= f.x; + p.y -= f.y; - var scroll = JX.Vector.getAggregateScrollForNode(this._dragging); - - var origin = { - x: this._origin.x + (this._originScroll.x - scroll.x), - y: this._origin.y + (this._originScroll.y - scroll.y) - }; - - var adjust_h = 0; - var adjust_y = 0; - if (this._target !== false) { - var ghost = this.getGhostNode(); - adjust_h = JX.Vector.getDim(ghost).y; - adjust_y = JX.$V(ghost).y; - - if (adjust_y <= origin.y) { - p.y -= adjust_h; - } - } + p.y += this._offset.y; + this._clone.style.top = p.y + 'px'; if (this._canDragX()) { - p.x -= origin.x; - } else { - p.x = 0; + p.x += this._offset.x; + this._clone.style.left = p.x + 'px'; } - p.y -= origin.y; - this._lastAdjust = new JX.Vector(p.x, p.y); - p.setPos(this._dragging); - e.kill(); }, @@ -444,6 +464,10 @@ JX.install('DraggableList', { var dragging = this._dragging; this._dragging = null; + JX.DOM.remove(this._frame); + this._frame = null; + this._clone = null; + var target = false; var ghost = false; @@ -469,7 +493,6 @@ JX.install('DraggableList', { for (var ii = 0; ii < group.length; ii++) { JX.DOM.alterClass(group[ii].getRootNode(), 'drag-target-list', false); group[ii]._clearTarget(); - group[ii]._lastAdjust = null; } if (!this.invoke('didEndDrag', dragging).getPrevented()) { From ffb7978528e4cf937353191c08f172e969881395 Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 1 Feb 2016 17:47:16 -0800 Subject: [PATCH 18/54] Make all CSS rules for draggable cards/items independent of container classes Summary: Ref T5240. With the new approach, the draggable clones lose their containers, so they don't get affected by rules like `.container .item`. Put classes on the cards/items and use `.board-item.item` and `.standard-item.item` to apply rules instead. This didn't turn out //too// gross, and seems relatively OK / not obviously broken. Test Plan: - Dragged cards on a workboard. - Dragged items in normal lists (tasks, pinned apps). Reviewers: chad Reviewed By: chad Maniphest Tasks: T5240 Differential Revision: https://secure.phabricator.com/D15161 --- resources/celerity/map.php | 10 ++-- .../PhabricatorProjectBoardViewController.php | 1 + src/view/phui/PHUIObjectItemListView.php | 11 ++++ .../css/phui/phui-object-item-list-view.css | 9 ++- .../css/phui/workboards/phui-workcard.css | 58 +++++++++---------- 5 files changed, 54 insertions(+), 35 deletions(-) diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 26c1d58a6d..f7e97b9c50 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -7,7 +7,7 @@ */ return array( 'names' => array( - 'core.pkg.css' => 'cd66e467', + 'core.pkg.css' => 'a01828d4', 'core.pkg.js' => 'c5178ede', 'darkconsole.pkg.js' => 'e7393ebb', 'differential.pkg.css' => '2de124c9', @@ -142,7 +142,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' => '699de05e', + 'rsrc/css/phui/phui-object-item-list-view.css' => '56aab18d', 'rsrc/css/phui/phui-pager.css' => 'bea33d23', 'rsrc/css/phui/phui-pinboard-view.css' => '2495140e', 'rsrc/css/phui/phui-profile-menu.css' => 'ab4fcf5f', @@ -154,7 +154,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' => 'ddb93318', + 'rsrc/css/phui/workboards/phui-workcard.css' => '92178913', 'rsrc/css/phui/workboards/phui-workpanel.css' => 'b90970eb', 'rsrc/css/sprite-login.css' => '60e8560e', 'rsrc/css/sprite-menu.css' => '9dd65b92', @@ -818,7 +818,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' => '699de05e', + 'phui-object-item-list-view-css' => '56aab18d', 'phui-pager-css' => 'bea33d23', 'phui-pinboard-view-css' => '2495140e', 'phui-profile-menu-css' => 'ab4fcf5f', @@ -831,7 +831,7 @@ return array( 'phui-timeline-view-css' => '2efceff8', 'phui-two-column-view-css' => 'c75bfc5b', 'phui-workboard-view-css' => 'b07a5524', - 'phui-workcard-view-css' => 'ddb93318', + 'phui-workcard-view-css' => '92178913', 'phui-workpanel-view-css' => 'b90970eb', 'phuix-action-list-view' => 'b5c256b8', 'phuix-action-view' => '8cf6d262', diff --git a/src/applications/project/controller/PhabricatorProjectBoardViewController.php b/src/applications/project/controller/PhabricatorProjectBoardViewController.php index 2c064eaf71..06669aa2be 100644 --- a/src/applications/project/controller/PhabricatorProjectBoardViewController.php +++ b/src/applications/project/controller/PhabricatorProjectBoardViewController.php @@ -288,6 +288,7 @@ final class PhabricatorProjectBoardViewController ->setFlush(true) ->setAllowEmptyList(true) ->addSigil('project-column') + ->setItemClass('phui-workcard') ->setMetadata( array( 'columnPHID' => $column->getPHID(), diff --git a/src/view/phui/PHUIObjectItemListView.php b/src/view/phui/PHUIObjectItemListView.php index bc62d8e5bc..c21ea558a2 100644 --- a/src/view/phui/PHUIObjectItemListView.php +++ b/src/view/phui/PHUIObjectItemListView.php @@ -9,6 +9,7 @@ final class PHUIObjectItemListView extends AphrontTagView { private $flush; private $allowEmptyList; private $states; + private $itemClass = 'phui-object-item-standard'; public function setAllowEmptyList($allow_empty_list) { $this->allowEmptyList = $allow_empty_list; @@ -49,6 +50,11 @@ final class PHUIObjectItemListView extends AphrontTagView { return $this; } + public function setItemClass($item_class) { + $this->itemClass = $item_class; + return $this; + } + protected function getTagName() { return 'ul'; } @@ -89,6 +95,11 @@ final class PHUIObjectItemListView extends AphrontTagView { $item->setUser($viewer); } } + + foreach ($this->items as $item) { + $item->addClass($this->itemClass); + } + $items = $this->items; } else if ($this->allowEmptyList) { $items = null; 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 1f22d9dedb..e7edb585b7 100644 --- a/webroot/rsrc/css/phui/phui-object-item-list-view.css +++ b/webroot/rsrc/css/phui/phui-object-item-list-view.css @@ -374,8 +374,9 @@ ul.phui-object-item-icons { attributes. */ -.phui-workboard-view .phui-object-item { +.phui-workcard.phui-object-item { border-left-width: 4px; + box-sizing: border-box; } .phui-object-item { @@ -682,6 +683,12 @@ ul.phui-object-item-list-view .phui-object-item-selected border-bottom: 1px solid {$thinblueborder}; } +.drag-clone.phui-object-item-standard .phui-object-item-frame { + border: none; + opacity: 0.8; + background: {$sh-bluebackground}; +} + .phui-object-box .phui-object-item-list-header { font-size: {$normalfontsize}; color: {$darkbluetext}; diff --git a/webroot/rsrc/css/phui/workboards/phui-workcard.css b/webroot/rsrc/css/phui/workboards/phui-workcard.css index 09bb64d59e..cef6e98e2c 100644 --- a/webroot/rsrc/css/phui/workboards/phui-workcard.css +++ b/webroot/rsrc/css/phui/workboards/phui-workcard.css @@ -2,28 +2,28 @@ * @provides phui-workcard-view-css */ -.phui-workpanel-view .phui-object-item { +.phui-workcard.phui-object-item { background-color: #fff; border-radius: 3px; margin-bottom: 8px; } -.phui-workpanel-view .phui-object-item-name { +.phui-workcard .phui-object-item-name { padding-bottom: 4px; } -.phui-workpanel-view .phui-object-item-content { +.phui-workcard .phui-object-item-content { margin-top: 0; } -.phui-workpanel-view .phui-object-item-frame { +.phui-workcard .phui-object-item-frame { border-top-right-radius: 3px; border-bottom-right-radius: 3px; border-color: {$thinblueborder}; border-bottom-color: {$lightblueborder}; } -.phui-workpanel-view .phui-object-item .phui-object-item-objname { +.phui-workcard.phui-object-item .phui-object-item-objname { -webkit-touch-callout: text; -webkit-user-select: text; -khtml-user-select: text; @@ -32,57 +32,57 @@ user-select: text; } -.phui-workpanel-view .phui-object-item-link { +.phui-workcard .phui-object-item-link { white-space: normal; font-weight: normal; color: #000; margin-left: 2px; } -.device-desktop .phui-workpanel-view .phui-object-item-with-1-actions +.device-desktop .phui-workcard .phui-object-item-with-1-actions .phui-object-item-content-box { - margin-right: 0; - overflow: hidden; + margin-right: 0; + overflow: hidden; } -.phui-workpanel-view .phui-object-item-objname { +.phui-workcard .phui-object-item-objname { vertical-align: top; } -.phui-workpanel-view .phui-object-item-grippable .phui-object-item-frame { +.phui-workcard.phui-object-item-grippable .phui-object-item-frame { padding-left: 0; } -.phui-workpanel-view .phui-object-item-grip { +.phui-workcard .phui-object-item-grip { display: none; } -.device-desktop .phui-workpanel-view .phui-list-item-icon { +.device-desktop .phui-workcard .phui-list-item-icon { display: none; } -.phui-workpanel-view .phui-object-item .phui-list-item-href { +.phui-workcard.phui-object-item .phui-list-item-href { height: 24px; width: 24px; } -.device-desktop .phui-workpanel-view .phui-object-item:hover +.device-desktop .phui-workcard.phui-object-item:hover .phui-list-item-href { background: #fff; opacity: .7; } -.device-desktop .phui-workpanel-view .phui-object-item +.device-desktop .phui-workcard.phui-object-item .phui-list-item-href:hover { background: {$sh-bluebackground}; opacity: 1; } -.phui-workpanel-view .phui-object-item:hover .phui-list-item-icon { +.phui-workcard.phui-object-item:hover .phui-list-item-icon { display: block; } -.phui-workpanel-view .phui-object-item-attributes { +.phui-workcard .phui-object-item-attributes { margin-right: 12px; } @@ -90,47 +90,47 @@ /* - Draggable Colors --------------------------------------------------------*/ -.phui-workpanel-view .phui-object-item.drag-dragging { +.phui-workcard.phui-object-item.drag-clone { box-shadow: {$dropshadow}; background-color: {$sh-greybackground}; } -.phui-workpanel-view .phui-object-item.drag-dragging .phui-list-item-href { +.phui-workcard.phui-object-item.drag-clone .phui-list-item-href { display: none; } -.phui-workpanel-view .drag-dragging.phui-object-item-bar-color-red { +.phui-workcard.drag-clone.phui-object-item-bar-color-red { background-color: {$sh-redbackground}; } -.phui-workpanel-view .drag-dragging.phui-object-item-bar-color-orange { +.phui-workcard.drag-clone.phui-object-item-bar-color-orange { background-color: {$sh-orangebackground}; } -.phui-workpanel-view .drag-dragging.phui-object-item-bar-color-yellow { +.phui-workcard.drag-clone.phui-object-item-bar-color-yellow { background-color: {$sh-yellowbackground}; } -.phui-workpanel-view .drag-dragging.phui-object-item-bar-color-green { +.phui-workcard.drag-clone.phui-object-item-bar-color-green { background-color: {$sh-greenbackground}; } -.phui-workpanel-view .drag-dragging.phui-object-item-bar-color-blue { +.phui-workcard.drag-clone.phui-object-item-bar-color-blue { background-color: {$sh-bluebackground}; } -.phui-workpanel-view .drag-dragging.phui-object-item-bar-color-indigo { +.phui-workcard.drag-clone.phui-object-item-bar-color-indigo { background-color: {$sh-indigobackground}; } -.phui-workpanel-view .drag-dragging.phui-object-item-bar-color-violet { +.phui-workcard.drag-clone.phui-object-item-bar-color-violet { background-color: {$sh-violetbackground}; } -.phui-workpanel-view .drag-dragging.phui-object-item-bar-color-pink { +.phui-workcard.drag-clone.phui-object-item-bar-color-pink { background-color: {$sh-pinkbackground}; } -.phui-workpanel-view .drag-dragging.phui-object-item-bar-color-sky { +.phui-workcard.drag-clone.phui-object-item-bar-color-sky { background-color: {$sh-bluebackground}; } From a019f1651870a7848d5cf7c1a6525567f7916917 Mon Sep 17 00:00:00 2001 From: cburroughs Date: Tue, 2 Feb 2016 14:37:12 +0000 Subject: [PATCH 19/54] increase team productivity with feline facts Summary: {F1087124} Test Plan: https://en.wikipedia.org/wiki/Cat Reviewers: #blessed_reviewers, epriestley Reviewed By: #blessed_reviewers, epriestley Subscribers: epriestley Differential Revision: https://secure.phabricator.com/D15162 --- .../profilepanel/PhabricatorMotivatorProfilePanel.php | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/applications/search/profilepanel/PhabricatorMotivatorProfilePanel.php b/src/applications/search/profilepanel/PhabricatorMotivatorProfilePanel.php index 3537520903..34c767e99c 100644 --- a/src/applications/search/profilepanel/PhabricatorMotivatorProfilePanel.php +++ b/src/applications/search/profilepanel/PhabricatorMotivatorProfilePanel.php @@ -131,6 +131,15 @@ final class PhabricatorMotivatorProfilePanel pht( 'The Japanese word for cat is "kome", which is also the word for '. 'rice. Japanese cats love to eat rice, so the two are synonymous.'), + pht('Cats have five pointy ends.'), + pht('cat -A can find mice hiding in files.'), + pht('A cat\'s visual, olfactory, and auditory senses, '. + 'Contribute to their hunting skills and natural defenses.'), + pht( + 'Cats with high self-esteem seek out high perches '. + 'to launch their attacks. Watch out!'), + pht('Cats prefer vanilla ice cream.'), + pht('Taco cat spelled backwards is taco cat.'), ); } From 61318a8119d309db62c99eebb4631f38d9e6120b Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 2 Feb 2016 06:26:42 -0800 Subject: [PATCH 20/54] Improve minor workboard drag behaviors Summary: Ref T5240. - Add proper class when dropping cards. - Add proper class when creating new cards. - Make X-drag explicit so that it works if there's only one column. - Stop tootips when dragging, resume them after dropping. - Move CSS rule for consistency. - Allow user to hit "Escape" to cancel an in-progress drag. Test Plan: - Dropped cards. - Created new cards. - X-dragged on a workboard with one column and a dashboard. - Dragged over a tooltip (no tip), dropped, moused over tooltip (tip). - Hit escape during a drag. Reviewers: chad Reviewed By: chad Subscribers: cspeckmim Maniphest Tasks: T5240 Differential Revision: https://secure.phabricator.com/D15163 --- resources/celerity/map.php | 90 +++++++++---------- .../maniphest/editor/ManiphestEditEngine.php | 2 + .../PhabricatorProjectMoveController.php | 2 + .../css/phui/phui-object-item-list-view.css | 5 -- .../css/phui/workboards/phui-workcard.css | 3 +- .../behavior-dashboard-move-panels.js | 3 +- .../projects/behavior-project-boards.js | 3 +- webroot/rsrc/js/core/DraggableList.js | 51 +++++++---- webroot/rsrc/js/core/ToolTip.js | 35 ++++++-- 9 files changed, 115 insertions(+), 79 deletions(-) diff --git a/resources/celerity/map.php b/resources/celerity/map.php index f7e97b9c50..ecb6b89bfb 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -7,8 +7,8 @@ */ return array( 'names' => array( - 'core.pkg.css' => 'a01828d4', - 'core.pkg.js' => 'c5178ede', + 'core.pkg.css' => '764d4c80', + 'core.pkg.js' => '5b832397', 'darkconsole.pkg.js' => 'e7393ebb', 'differential.pkg.css' => '2de124c9', 'differential.pkg.js' => '5c2ba922', @@ -142,7 +142,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' => '56aab18d', + 'rsrc/css/phui/phui-object-item-list-view.css' => 'fe594a65', 'rsrc/css/phui/phui-pager.css' => 'bea33d23', 'rsrc/css/phui/phui-pinboard-view.css' => '2495140e', 'rsrc/css/phui/phui-profile-menu.css' => 'ab4fcf5f', @@ -154,7 +154,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' => '92178913', + 'rsrc/css/phui/workboards/phui-workcard.css' => '42c703d7', 'rsrc/css/phui/workboards/phui-workpanel.css' => 'b90970eb', 'rsrc/css/sprite-login.css' => '60e8560e', 'rsrc/css/sprite-menu.css' => '9dd65b92', @@ -368,7 +368,7 @@ return array( 'rsrc/js/application/countdown/timer.js' => 'e4cc26b3', 'rsrc/js/application/daemon/behavior-bulk-job-reload.js' => 'edf8a145', 'rsrc/js/application/dashboard/behavior-dashboard-async-panel.js' => '469c0d9e', - 'rsrc/js/application/dashboard/behavior-dashboard-move-panels.js' => '82439934', + 'rsrc/js/application/dashboard/behavior-dashboard-move-panels.js' => '019f36c4', 'rsrc/js/application/dashboard/behavior-dashboard-query-panel-select.js' => '453c5375', 'rsrc/js/application/dashboard/behavior-dashboard-tab-panel.js' => 'd4eecc63', 'rsrc/js/application/differential/ChangesetViewManager.js' => 'a2828756', @@ -413,7 +413,7 @@ return array( 'rsrc/js/application/phortune/phortune-credit-card-form.js' => '2290aeef', 'rsrc/js/application/policy/behavior-policy-control.js' => 'ae45872f', 'rsrc/js/application/policy/behavior-policy-rule-editor.js' => '5e9f347c', - 'rsrc/js/application/projects/behavior-project-boards.js' => 'ba4fa35c', + 'rsrc/js/application/projects/behavior-project-boards.js' => '16c76360', '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,7 +446,7 @@ return array( 'rsrc/js/application/uiexample/notification-example.js' => '8ce821c5', 'rsrc/js/core/Busy.js' => '59a7976a', 'rsrc/js/core/DragAndDropFileUpload.js' => 'ad10aeac', - 'rsrc/js/core/DraggableList.js' => 'f1844746', + 'rsrc/js/core/DraggableList.js' => '1fe26f18', 'rsrc/js/core/FileUpload.js' => '477359c8', 'rsrc/js/core/Hovercard.js' => 'c6f720ff', 'rsrc/js/core/KeyboardShortcut.js' => '1ae869f2', @@ -457,7 +457,7 @@ return array( 'rsrc/js/core/ShapedRequest.js' => '7cbe244b', 'rsrc/js/core/TextAreaUtils.js' => '9e54692d', 'rsrc/js/core/Title.js' => 'df5e11d2', - 'rsrc/js/core/ToolTip.js' => '1d298e3a', + 'rsrc/js/core/ToolTip.js' => '6323f942', 'rsrc/js/core/behavior-active-nav.js' => 'e379b58e', 'rsrc/js/core/behavior-audio-source.js' => '59b251eb', 'rsrc/js/core/behavior-autofocus.js' => '7319e029', @@ -579,7 +579,7 @@ return array( 'javelin-behavior-countdown-timer' => 'e4cc26b3', 'javelin-behavior-dark-console' => 'f411b6ae', 'javelin-behavior-dashboard-async-panel' => '469c0d9e', - 'javelin-behavior-dashboard-move-panels' => '82439934', + 'javelin-behavior-dashboard-move-panels' => '019f36c4', 'javelin-behavior-dashboard-query-panel-select' => '453c5375', 'javelin-behavior-dashboard-tab-panel' => 'd4eecc63', 'javelin-behavior-day-view' => '5c46cff2', @@ -653,7 +653,7 @@ return array( 'javelin-behavior-phui-profile-menu' => '12884df9', 'javelin-behavior-policy-control' => 'ae45872f', 'javelin-behavior-policy-rule-editor' => '5e9f347c', - 'javelin-behavior-project-boards' => 'ba4fa35c', + 'javelin-behavior-project-boards' => '16c76360', 'javelin-behavior-project-create' => '065227cc', 'javelin-behavior-quicksand-blacklist' => '7927a7d3', 'javelin-behavior-recurring-edit' => '5f1c4d5f', @@ -741,7 +741,7 @@ return array( 'phabricator-countdown-css' => 'e7544472', 'phabricator-dashboard-css' => 'eb458607', 'phabricator-drag-and-drop-file-upload' => 'ad10aeac', - 'phabricator-draggable-list' => 'f1844746', + 'phabricator-draggable-list' => '1fe26f18', 'phabricator-fatal-config-template-css' => '8e6c6fcd', 'phabricator-feed-css' => 'ecd4ec57', 'phabricator-file-upload' => '477359c8', @@ -768,7 +768,7 @@ return array( 'phabricator-standard-page-view' => '7b0d68d8', 'phabricator-textareautils' => '9e54692d', 'phabricator-title' => 'df5e11d2', - 'phabricator-tooltip' => '1d298e3a', + 'phabricator-tooltip' => '6323f942', 'phabricator-ui-example-css' => '528b19de', 'phabricator-uiexample-javelin-view' => 'd4a14807', 'phabricator-uiexample-reactor-button' => 'd19198c8', @@ -818,7 +818,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' => '56aab18d', + 'phui-object-item-list-view-css' => 'fe594a65', 'phui-pager-css' => 'bea33d23', 'phui-pinboard-view-css' => '2495140e', 'phui-profile-menu-css' => 'ab4fcf5f', @@ -831,7 +831,7 @@ return array( 'phui-timeline-view-css' => '2efceff8', 'phui-two-column-view-css' => 'c75bfc5b', 'phui-workboard-view-css' => 'b07a5524', - 'phui-workcard-view-css' => '92178913', + 'phui-workcard-view-css' => '42c703d7', 'phui-workpanel-view-css' => 'b90970eb', 'phuix-action-list-view' => 'b5c256b8', 'phuix-action-view' => '8cf6d262', @@ -875,6 +875,14 @@ return array( 'javelin-behavior-device', 'javelin-vector', ), + '019f36c4' => array( + 'javelin-behavior', + 'javelin-dom', + 'javelin-util', + 'javelin-stratcom', + 'javelin-workflow', + 'phabricator-draggable-list', + ), '031cee25' => array( 'javelin-behavior', 'javelin-request', @@ -945,6 +953,15 @@ return array( 'javelin-dom', 'javelin-history', ), + '16c76360' => array( + 'javelin-behavior', + 'javelin-dom', + 'javelin-util', + 'javelin-vector', + 'javelin-stratcom', + 'javelin-workflow', + 'phabricator-draggable-list', + ), '1ad0a787' => array( 'javelin-install', 'javelin-reactor', @@ -962,12 +979,6 @@ return array( 'javelin-dom', 'javelin-typeahead-normalizer', ), - '1d298e3a' => array( - 'javelin-install', - 'javelin-util', - 'javelin-dom', - 'javelin-vector', - ), '1d45c74d' => array( 'javelin-behavior', 'javelin-dom', @@ -996,6 +1007,14 @@ return array( 'phuix-icon-view', 'javelin-behavior-phabricator-gesture', ), + '1fe26f18' => array( + 'javelin-install', + 'javelin-dom', + 'javelin-stratcom', + 'javelin-util', + 'javelin-vector', + 'javelin-magical-init', + ), '21ba5861' => array( 'javelin-behavior', 'javelin-dom', @@ -1290,6 +1309,12 @@ return array( 'javelin-install', 'javelin-util', ), + '6323f942' => array( + 'javelin-install', + 'javelin-util', + 'javelin-dom', + 'javelin-vector', + ), '635de1ec' => array( 'javelin-behavior', 'javelin-stratcom', @@ -1430,14 +1455,6 @@ return array( 'javelin-vector', 'javelin-stratcom', ), - 82439934 => array( - 'javelin-behavior', - 'javelin-dom', - 'javelin-util', - 'javelin-stratcom', - 'javelin-workflow', - 'phabricator-draggable-list', - ), '834a1173' => array( 'javelin-behavior', 'javelin-scrollbar', @@ -1747,15 +1764,6 @@ return array( 'b90970eb' => array( 'phui-workcard-view-css', ), - 'ba4fa35c' => array( - 'javelin-behavior', - 'javelin-dom', - 'javelin-util', - 'javelin-vector', - 'javelin-stratcom', - 'javelin-workflow', - 'phabricator-draggable-list', - ), 'bd4c8dca' => array( 'javelin-install', 'javelin-util', @@ -2011,14 +2019,6 @@ return array( 'javelin-workflow', 'javelin-json', ), - 'f1844746' => array( - 'javelin-install', - 'javelin-dom', - 'javelin-stratcom', - 'javelin-util', - 'javelin-vector', - 'javelin-magical-init', - ), 'f411b6ae' => array( 'javelin-behavior', 'javelin-stratcom', diff --git a/src/applications/maniphest/editor/ManiphestEditEngine.php b/src/applications/maniphest/editor/ManiphestEditEngine.php index 058a8a98b6..e338450aa9 100644 --- a/src/applications/maniphest/editor/ManiphestEditEngine.php +++ b/src/applications/maniphest/editor/ManiphestEditEngine.php @@ -338,6 +338,8 @@ final class ManiphestEditEngine ->setCanEdit(true) ->getItem(); + $tasks->addClass('phui-workcard'); + $payload = array( 'tasks' => $tasks, 'data' => $data, diff --git a/src/applications/project/controller/PhabricatorProjectMoveController.php b/src/applications/project/controller/PhabricatorProjectMoveController.php index 3fc72e7341..3edcd01f65 100644 --- a/src/applications/project/controller/PhabricatorProjectMoveController.php +++ b/src/applications/project/controller/PhabricatorProjectMoveController.php @@ -160,6 +160,8 @@ final class PhabricatorProjectMoveController ->setProject($project) ->getItem(); + $card->addClass('phui-workcard'); + return id(new AphrontAjaxResponse())->setContent( array('task' => $card)); } 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 e7edb585b7..551144673b 100644 --- a/webroot/rsrc/css/phui/phui-object-item-list-view.css +++ b/webroot/rsrc/css/phui/phui-object-item-list-view.css @@ -374,11 +374,6 @@ ul.phui-object-item-icons { attributes. */ -.phui-workcard.phui-object-item { - border-left-width: 4px; - box-sizing: border-box; -} - .phui-object-item { border-left-width: 0; } diff --git a/webroot/rsrc/css/phui/workboards/phui-workcard.css b/webroot/rsrc/css/phui/workboards/phui-workcard.css index cef6e98e2c..a921db4d32 100644 --- a/webroot/rsrc/css/phui/workboards/phui-workcard.css +++ b/webroot/rsrc/css/phui/workboards/phui-workcard.css @@ -6,6 +6,8 @@ background-color: #fff; border-radius: 3px; margin-bottom: 8px; + border-left-width: 4px; + box-sizing: border-box; } .phui-workcard .phui-object-item-name { @@ -87,7 +89,6 @@ } - /* - Draggable Colors --------------------------------------------------------*/ .phui-workcard.phui-object-item.drag-clone { diff --git a/webroot/rsrc/js/application/dashboard/behavior-dashboard-move-panels.js b/webroot/rsrc/js/application/dashboard/behavior-dashboard-move-panels.js index f04456bf2a..fbbf411be8 100644 --- a/webroot/rsrc/js/application/dashboard/behavior-dashboard-move-panels.js +++ b/webroot/rsrc/js/application/dashboard/behavior-dashboard-move-panels.js @@ -85,7 +85,8 @@ JX.behavior('dashboard-move-panels', function(config) { for (ii = 0; ii < cols.length; ii++) { col = cols[ii]; var list = new JX.DraggableList(itemSigil, col) - .setFindItemsHandler(JX.bind(null, finditems, col)); + .setFindItemsHandler(JX.bind(null, finditems, col)) + .setCanDragX(true); list.listen('didSend', JX.bind(list, onupdate, col)); list.listen('didReceive', JX.bind(list, onupdate, col)); diff --git a/webroot/rsrc/js/application/projects/behavior-project-boards.js b/webroot/rsrc/js/application/projects/behavior-project-boards.js index 04e99fde28..faaa936f67 100644 --- a/webroot/rsrc/js/application/projects/behavior-project-boards.js +++ b/webroot/rsrc/js/application/projects/behavior-project-boards.js @@ -231,7 +231,8 @@ JX.behavior('project-boards', function(config, statics) { for (ii = 0; ii < cols.length; ii++) { var list = new JX.DraggableList('project-card', cols[ii]) - .setFindItemsHandler(JX.bind(null, finditems, cols[ii])); + .setFindItemsHandler(JX.bind(null, finditems, cols[ii])) + .setCanDragX(true); list.listen('didSend', JX.bind(list, onupdate, cols[ii])); list.listen('didReceive', JX.bind(list, onupdate, cols[ii])); diff --git a/webroot/rsrc/js/core/DraggableList.js b/webroot/rsrc/js/core/DraggableList.js index 6cb279a89b..ae93eb5c7e 100644 --- a/webroot/rsrc/js/core/DraggableList.js +++ b/webroot/rsrc/js/core/DraggableList.js @@ -23,6 +23,7 @@ JX.install('DraggableList', { JX.Stratcom.listen('mousemove', null, JX.bind(this, this._onmove)); JX.Stratcom.listen('scroll', null, JX.bind(this, this._onmove)); JX.Stratcom.listen('mouseup', null, JX.bind(this, this._ondrop)); + JX.Stratcom.listen('keypress', null, JX.bind(this, this._onkey)); }, events : [ @@ -37,7 +38,8 @@ JX.install('DraggableList', { 'didReceive'], properties : { - findItemsHandler : null + findItemsHandler: null, + canDragX: false }, members : { @@ -97,10 +99,6 @@ JX.install('DraggableList', { return this; }, - _canDragX : function() { - return this._hasGroup(); - }, - _hasGroup : function() { return (this._group.length > 1); }, @@ -237,6 +235,8 @@ JX.install('DraggableList', { var cursor = JX.$V(e); this._offset = new JX.Vector(pos.x - cursor.x, pos.y - cursor.y); + JX.Tooltip.lock(); + this.invoke('didBeginDrag', this._dragging); }, @@ -446,7 +446,7 @@ JX.install('DraggableList', { p.y += this._offset.y; this._clone.style.top = p.y + 'px'; - if (this._canDragX()) { + if (this.getCanDragX()) { p.x += this._offset.x; this._clone.style.left = p.x + 'px'; } @@ -454,13 +454,25 @@ JX.install('DraggableList', { e.kill(); }, + _onkey: function(e) { + // Cancel any current drag if the user presses escape. + if (this._dragging && (e.getSpecialKey() == 'esc')) { + e.kill(); + this._drop(null); + return; + } + }, + _ondrop : function(e) { + var p = JX.$V(e); + this._drop(p); + }, + + _drop: function(cursor) { if (!this._dragging) { return; } - var p = JX.$V(e); - var dragging = this._dragging; this._dragging = null; @@ -471,22 +483,24 @@ JX.install('DraggableList', { var target = false; var ghost = false; - var target_list = this._getTargetList(p); - if (target_list) { - target = target_list._target; - ghost = target_list.getGhostNode(); + if (cursor) { + var target_list = this._getTargetList(cursor); + if (target_list) { + target = target_list._target; + ghost = target_list.getGhostNode(); + } } JX.$V(0, 0).setPos(dragging); - if (target !== false) { + if (target === false) { + this.invoke('didCancelDrag', dragging); + } else { JX.DOM.remove(dragging); JX.DOM.replace(ghost, dragging); this.invoke('didSend', dragging, target_list); target_list.invoke('didReceive', dragging, this); target_list.invoke('didDrop', dragging, target, this); - } else { - this.invoke('didCancelDrag', dragging); } var group = this._group; @@ -495,11 +509,12 @@ JX.install('DraggableList', { group[ii]._clearTarget(); } - if (!this.invoke('didEndDrag', dragging).getPrevented()) { - JX.DOM.alterClass(dragging, 'drag-dragging', false); - } + JX.DOM.alterClass(dragging, 'drag-dragging', false); + JX.Tooltip.unlock(); e.kill(); + + this.invoke('didEndDrag', dragging); }, lock : function() { diff --git a/webroot/rsrc/js/core/ToolTip.js b/webroot/rsrc/js/core/ToolTip.js index add181c7e5..90fff2bcbe 100644 --- a/webroot/rsrc/js/core/ToolTip.js +++ b/webroot/rsrc/js/core/ToolTip.js @@ -9,10 +9,17 @@ JX.install('Tooltip', { - statics : { - _node : null, + statics: { + _node: null, + _lock: 0, show : function(root, scale, align, content) { + var self = JX.Tooltip; + + if (self._lock) { + return; + } + if (__DEV__) { switch (align) { case 'N': @@ -45,27 +52,28 @@ JX.install('Tooltip', { node.style.maxWidth = scale + 'px'; JX.Tooltip.hide(); - this._node = node; + self._node = node; // Append the tip to the document, but offscreen, so we can measure it. node.style.left = '-10000px'; document.body.appendChild(node); // Jump through some hoops trying to auto-position the tooltip - var pos = this._getSmartPosition(align, root, node); + var pos = self._getSmartPosition(align, root, node); pos.setPos(node); }, _getSmartPosition: function (align, root, node) { - var pos = JX.Tooltip._proposePosition(align, root, node); + var self = JX.Tooltip; + var pos = self._proposePosition(align, root, node); // If toolip is offscreen, try to be clever if (!JX.Tooltip.isOnScreen(pos, node)) { - align = JX.Tooltip._getImprovedOrientation(pos, node); - pos = JX.Tooltip._proposePosition(align, root, node); + align = self._getImprovedOrientation(pos, node); + pos = self._proposePosition(align, root, node); } - JX.Tooltip._setAnchor(align); + self._setAnchor(align); return pos; }, @@ -167,6 +175,17 @@ JX.install('Tooltip', { JX.DOM.remove(this._node); this._node = null; } + }, + + lock: function() { + var self = JX.Tooltip; + self.hide(); + self._lock++; + }, + + unlock: function() { + var self = JX.Tooltip; + self._lock--; } } }); From e433a09fde9d5935a9703506487346b135ab52fd Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 7 Aug 2014 17:04:35 -0700 Subject: [PATCH 21/54] Scroll parent containers when objects are dragged near the edge of the field of view Summary: Ref T5240. This probably has some bugs and doesn't quite work in Firefox (fine on boards, not quite on the task list -- some issue with body or document being special, I think). I think this is close enough that we can throw it out there and see how users manage to break it, though. It's not worse than what we've got now? I think? Test Plan: dragged things near the edge of other things they seemed to move around OK Reviewers: btrahan Reviewed By: btrahan Subscribers: epriestley Maniphest Tasks: T5240 Differential Revision: https://secure.phabricator.com/D10188 --- resources/celerity/map.php | 44 ++--- .../projects/behavior-project-boards.js | 1 + webroot/rsrc/js/core/DraggableList.js | 155 +++++++++++++++++- 3 files changed, 171 insertions(+), 29 deletions(-) diff --git a/resources/celerity/map.php b/resources/celerity/map.php index ecb6b89bfb..f37efff476 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -8,7 +8,7 @@ return array( 'names' => array( 'core.pkg.css' => '764d4c80', - 'core.pkg.js' => '5b832397', + 'core.pkg.js' => '53c6a7c5', 'darkconsole.pkg.js' => 'e7393ebb', 'differential.pkg.css' => '2de124c9', 'differential.pkg.js' => '5c2ba922', @@ -413,7 +413,7 @@ return array( 'rsrc/js/application/phortune/phortune-credit-card-form.js' => '2290aeef', 'rsrc/js/application/policy/behavior-policy-control.js' => 'ae45872f', 'rsrc/js/application/policy/behavior-policy-rule-editor.js' => '5e9f347c', - 'rsrc/js/application/projects/behavior-project-boards.js' => '16c76360', + 'rsrc/js/application/projects/behavior-project-boards.js' => 'c05fb42a', '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,7 +446,7 @@ return array( 'rsrc/js/application/uiexample/notification-example.js' => '8ce821c5', 'rsrc/js/core/Busy.js' => '59a7976a', 'rsrc/js/core/DragAndDropFileUpload.js' => 'ad10aeac', - 'rsrc/js/core/DraggableList.js' => '1fe26f18', + 'rsrc/js/core/DraggableList.js' => '8905523d', 'rsrc/js/core/FileUpload.js' => '477359c8', 'rsrc/js/core/Hovercard.js' => 'c6f720ff', 'rsrc/js/core/KeyboardShortcut.js' => '1ae869f2', @@ -653,7 +653,7 @@ return array( 'javelin-behavior-phui-profile-menu' => '12884df9', 'javelin-behavior-policy-control' => 'ae45872f', 'javelin-behavior-policy-rule-editor' => '5e9f347c', - 'javelin-behavior-project-boards' => '16c76360', + 'javelin-behavior-project-boards' => 'c05fb42a', 'javelin-behavior-project-create' => '065227cc', 'javelin-behavior-quicksand-blacklist' => '7927a7d3', 'javelin-behavior-recurring-edit' => '5f1c4d5f', @@ -741,7 +741,7 @@ return array( 'phabricator-countdown-css' => 'e7544472', 'phabricator-dashboard-css' => 'eb458607', 'phabricator-drag-and-drop-file-upload' => 'ad10aeac', - 'phabricator-draggable-list' => '1fe26f18', + 'phabricator-draggable-list' => '8905523d', 'phabricator-fatal-config-template-css' => '8e6c6fcd', 'phabricator-feed-css' => 'ecd4ec57', 'phabricator-file-upload' => '477359c8', @@ -953,15 +953,6 @@ return array( 'javelin-dom', 'javelin-history', ), - '16c76360' => array( - 'javelin-behavior', - 'javelin-dom', - 'javelin-util', - 'javelin-vector', - 'javelin-stratcom', - 'javelin-workflow', - 'phabricator-draggable-list', - ), '1ad0a787' => array( 'javelin-install', 'javelin-reactor', @@ -1007,14 +998,6 @@ return array( 'phuix-icon-view', 'javelin-behavior-phabricator-gesture', ), - '1fe26f18' => array( - 'javelin-install', - 'javelin-dom', - 'javelin-stratcom', - 'javelin-util', - 'javelin-vector', - 'javelin-magical-init', - ), '21ba5861' => array( 'javelin-behavior', 'javelin-dom', @@ -1492,6 +1475,14 @@ 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', @@ -1781,6 +1772,15 @@ return array( 'javelin-install', 'javelin-dom', ), + 'c05fb42a' => array( + 'javelin-behavior', + 'javelin-dom', + 'javelin-util', + 'javelin-vector', + 'javelin-stratcom', + 'javelin-workflow', + 'phabricator-draggable-list', + ), 'c1700f6f' => array( 'javelin-install', 'javelin-util', diff --git a/webroot/rsrc/js/application/projects/behavior-project-boards.js b/webroot/rsrc/js/application/projects/behavior-project-boards.js index faaa936f67..bd69642e1d 100644 --- a/webroot/rsrc/js/application/projects/behavior-project-boards.js +++ b/webroot/rsrc/js/application/projects/behavior-project-boards.js @@ -232,6 +232,7 @@ JX.behavior('project-boards', function(config, statics) { 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])); diff --git a/webroot/rsrc/js/core/DraggableList.js b/webroot/rsrc/js/core/DraggableList.js index ae93eb5c7e..efe137e7ab 100644 --- a/webroot/rsrc/js/core/DraggableList.js +++ b/webroot/rsrc/js/core/DraggableList.js @@ -39,7 +39,8 @@ JX.install('DraggableList', { properties : { findItemsHandler: null, - canDragX: false + canDragX: false, + outerContainer: null }, members : { @@ -51,10 +52,15 @@ JX.install('DraggableList', { _ghostHandler : null, _ghostNode : null, _group : null, - _lastMousePosition: null, + _cursorPosition: null, + _cursorOrigin: null, + _cursorScroll: null, _frame: null, _clone: null, _offset: null, + _autoscroll: null, + _autoscroller: null, + _autotimer: null, getRootNode : function() { return this._root; @@ -177,6 +183,10 @@ JX.install('DraggableList', { var drag = e.getNode(this._sigil); + this._autoscroll = {}; + this._autoscroller = setInterval(JX.bind(this, this._onautoscroll), 10); + this._autotimer = null; + for (var ii = 0; ii < this._group.length; ii++) { this._group[ii]._clearTarget(); } @@ -398,14 +408,16 @@ JX.install('DraggableList', { // reuse the known position. if (e.getType() == 'mousemove') { - this._lastMousePosition = JX.$V(e); + this._cursorPosition = JX.$V(e); + this._cursorOrigin = JX.$V(e); + this._cursorScroll = JX.Vector.getScroll(); } if (!this._dragging) { return; } - if (!this._lastMousePosition) { + if (!this._cursorPosition) { return; } @@ -413,9 +425,17 @@ JX.install('DraggableList', { // If this is a scroll event, the positions of drag targets may have // changed. this._dirtyTargetCache(); + + // Correct the cursor position to account for scrolling. + var s = JX.Vector.getScroll(); + this._cursorPosition = new JX.$V( + this._cursorOrigin.x - (this._cursorScroll.x - s.x), + this._cursorOrigin.y - (this._cursorScroll.y - s.y)); } - var p = JX.$V(this._lastMousePosition.x, this._lastMousePosition.y); + this._updateAutoscroll(this._cursorPosition); + + var p = JX.$V(this._cursorPosition.x, this._cursorPosition.y); var group = this._group; var target_list = this._getTargetList(p); @@ -454,6 +474,60 @@ JX.install('DraggableList', { e.kill(); }, + _updateAutoscroll: function(p) { + var container = this._dragging.parentNode; + var autoscroll = {}; + + var outer = this.getOuterContainer(); + + var cpos; + var cdim; + + while (container) { + if (outer && (container == outer)) { + break; + } + + try { + cpos = JX.Vector.getPos(container); + cdim = JX.Vector.getDim(container); + if (container == document.body) { + cdim = JX.Vector.getViewport(); + cpos.x += container.scrollLeft; + cpos.y += container.scrollTop; + } + } catch (ignored) { + break; + } + + var fuzz = 64; + + if (p.y <= cpos.y + fuzz) { + autoscroll.up = container; + } + + if (p.y >= cpos.y + cdim.y - fuzz) { + autoscroll.down = container; + } + + if (p.x <= cpos.x + fuzz) { + autoscroll.left = container; + } + + if (p.x >= cpos.x + cdim.x - fuzz) { + autoscroll.right = container; + } + + if (container == document.body) { + break; + } + + container = container.parentNode; + } + + this._autoscroll = autoscroll; + }, + _onkey: function(e) { // Cancel any current drag if the user presses escape. if (this._dragging && (e.getSpecialKey() == 'esc')) { @@ -464,6 +538,10 @@ JX.install('DraggableList', { }, _ondrop : function(e) { + if (this._dragging) { + e.kill(); + } + var p = JX.$V(e); this._drop(p); }, @@ -475,6 +553,8 @@ JX.install('DraggableList', { var dragging = this._dragging; this._dragging = null; + clearInterval(this._autoscroller); + this._autoscroller = null; JX.DOM.remove(this._frame); this._frame = null; @@ -512,11 +592,72 @@ JX.install('DraggableList', { JX.DOM.alterClass(dragging, 'drag-dragging', false); JX.Tooltip.unlock(); - e.kill(); - this.invoke('didEndDrag', dragging); }, + _onautoscroll: function() { + var u = this._autoscroll.up; + var d = this._autoscroll.down; + var l = this._autoscroll.left; + var r = this._autoscroll.right; + + var now = +new Date(); + + if (!this._autotimer) { + this._autotimer = now; + return; + } + + var delta = now - this._autotimer; + this._autotimer = now; + + var amount = 12 * (delta / 10); + + if (u && (u != d)) { + this._tryScroll(this._dragging, u, 'scrollTop', amount); + } + + if (d && (d != u)) { + this._tryScroll(this._dragging, d, 'scrollTop', -amount); + } + + if (l && (l != r)) { + this._tryScroll(this._dragging, l, 'scrollLeft', amount); + } + + if (r && (r != l)) { + this._tryScroll(this._dragging, r, 'scrollLeft', -amount); + } + }, + + /** + * Walk up the tree from a node to some parent, trying to scroll every + * container. Stop when we find a container which we're able to scroll. + */ + _tryScroll: function(from, to, property, amount) { + var value; + + 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; + } + + if (container == to) { + break; + } + + container = container.parentNode; + } + }, + lock : function() { for (var ii = 0; ii < this._group.length; ii++) { this._group[ii]._lock(); From 1d939e0bd8672fa884b511d0a7830ec040d7ed53 Mon Sep 17 00:00:00 2001 From: Chad Little Date: Tue, 2 Feb 2016 09:44:27 -0800 Subject: [PATCH 22/54] Add project icon/type to Project Profile Summary: Adds basic icon/type to header on Project profiles Test Plan: View different projects, see header. Mobile, Deskop, Tablet. {F1087460} Reviewers: epriestley Reviewed By: epriestley Subscribers: Korvin Differential Revision: https://secure.phabricator.com/D15164 --- resources/celerity/map.php | 4 ++-- .../PhabricatorProjectProfileController.php | 9 ++++++++- .../css/application/project/project-view.css | 19 ++++++++++++++++++- 3 files changed, 28 insertions(+), 4 deletions(-) diff --git a/resources/celerity/map.php b/resources/celerity/map.php index f37efff476..203f285fe3 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-view.css' => 'c6387c87', + 'rsrc/css/application/project/project-view.css' => '99a5023b', 'rsrc/css/application/releeph/releeph-core.css' => '9b3c5733', 'rsrc/css/application/releeph/releeph-preview-branch.css' => 'b7a6f4a5', 'rsrc/css/application/releeph/releeph-request-differential-create-dialog.css' => '8d8b92cd', @@ -843,7 +843,7 @@ return array( 'policy-edit-css' => '815c66f7', 'policy-transaction-detail-css' => '82100a43', 'ponder-view-css' => '7b0df4da', - 'project-view-css' => 'c6387c87', + 'project-view-css' => '99a5023b', 'releeph-core' => '9b3c5733', 'releeph-preview-branch' => 'b7a6f4a5', 'releeph-request-differential-create-dialog' => '8d8b92cd', diff --git a/src/applications/project/controller/PhabricatorProjectProfileController.php b/src/applications/project/controller/PhabricatorProjectProfileController.php index 146c1da96f..8bd03b7053 100644 --- a/src/applications/project/controller/PhabricatorProjectProfileController.php +++ b/src/applications/project/controller/PhabricatorProjectProfileController.php @@ -17,9 +17,16 @@ final class PhabricatorProjectProfileController $project = $this->getProject(); $id = $project->getID(); $picture = $project->getProfileImageURI(); + $icon = $project->getDisplayIconIcon(); + $icon_name = $project->getDisplayIconName(); + $tag = id(new PHUITagView()) + ->setIcon($icon) + ->setName($icon_name) + ->addClass('project-view-header-tag') + ->setType(PHUITagView::TYPE_SHADE); $header = id(new PHUIHeaderView()) - ->setHeader($project->getName()) + ->setHeader(array($project->getName(), $tag)) ->setUser($viewer) ->setPolicyObject($project) ->setImage($picture) diff --git a/webroot/rsrc/css/application/project/project-view.css b/webroot/rsrc/css/application/project/project-view.css index ae1f7ec26f..4e1ab86e49 100644 --- a/webroot/rsrc/css/application/project/project-view.css +++ b/webroot/rsrc/css/application/project/project-view.css @@ -7,6 +7,23 @@ padding-bottom: 64px; } +.project-view-header-tag { + margin-left: 8px; + font-size: {$normalfontsize}; + color: {$bluetext}; + font-family: {$fontfamily}; + font-weight: normal; +} + +.device-phone .project-view-header-tag { + display: block; + margin-left: -4px; +} + +.project-view-header-tag .phui-icon-view { + color: {$bluetext}; +} + .phui-box.phui-box-grey.project-view-properties { margin: 0 16px 0 16px; padding: 4px 12px; @@ -37,7 +54,7 @@ .project-view-feed .phui-header-header { font-size: {$biggerfontsize}; - margin-left: 4px; + margin-left: 8px; } .device-desktop .project-view-feed .phui-feed-story, From 9d125b459ea8f7dbf2d729397613e1422de5a0de Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 2 Feb 2016 09:59:49 -0800 Subject: [PATCH 23/54] Use large text columns to store IP addresses Summary: Fixes T10259. There was no real reason to do this `ip2long()` stuff in the first place -- it's very slightly smaller, but won't work with ipv6 and the savings are miniscule. Test Plan: - Ran migration. - Viewed logs in web UI. - Pulled and pushed. Reviewers: chad Reviewed By: chad Maniphest Tasks: T10259 Differential Revision: https://secure.phabricator.com/D15165 --- resources/sql/autopatches/20160202.ipv6.1.sql | 5 +++ resources/sql/autopatches/20160202.ipv6.2.php | 39 +++++++++++++++++++ src/aphront/AphrontRequest.php | 8 +++- .../schema/PhabricatorConfigSchemaSpec.php | 1 + .../controller/DiffusionServeController.php | 7 ++-- .../engine/DiffusionCommitHookEngine.php | 11 +----- .../view/DiffusionPushLogListView.php | 9 ++--- .../people/view/PhabricatorUserLogView.php | 4 +- .../PhabricatorRepositoryPullEvent.php | 2 +- .../PhabricatorRepositoryPushEvent.php | 2 +- 10 files changed, 62 insertions(+), 26 deletions(-) create mode 100644 resources/sql/autopatches/20160202.ipv6.1.sql create mode 100644 resources/sql/autopatches/20160202.ipv6.2.php diff --git a/resources/sql/autopatches/20160202.ipv6.1.sql b/resources/sql/autopatches/20160202.ipv6.1.sql new file mode 100644 index 0000000000..d6a3ee5ccc --- /dev/null +++ b/resources/sql/autopatches/20160202.ipv6.1.sql @@ -0,0 +1,5 @@ +ALTER TABLE {$NAMESPACE}_repository.repository_pullevent + CHANGE remoteAddress remoteAddress VARBINARY(64); + +ALTER TABLE {$NAMESPACE}_repository.repository_pushevent + CHANGE remoteAddress remoteAddress VARBINARY(64); diff --git a/resources/sql/autopatches/20160202.ipv6.2.php b/resources/sql/autopatches/20160202.ipv6.2.php new file mode 100644 index 0000000000..50def09444 --- /dev/null +++ b/resources/sql/autopatches/20160202.ipv6.2.php @@ -0,0 +1,39 @@ +establishConnection('w'); + +$log_types = array($pull, $push); +foreach ($log_types as $log) { + foreach (new LiskMigrationIterator($log) as $row) { + $addr = $row->getRemoteAddress(); + + $addr = (string)$addr; + if (!strlen($addr)) { + continue; + } + + if (!ctype_digit($addr)) { + continue; + } + + if (!(int)$addr) { + continue; + } + + $ip = long2ip($addr); + if (!is_string($ip) || !strlen($ip)) { + continue; + } + + $id = $row->getID(); + queryfx( + $conn_w, + 'UPDATE %T SET remoteAddress = %s WHERE id = %d', + $log->getTableName(), + $ip, + $id); + } +} diff --git a/src/aphront/AphrontRequest.php b/src/aphront/AphrontRequest.php index e88a5a5e36..f8b01eb94c 100644 --- a/src/aphront/AphrontRequest.php +++ b/src/aphront/AphrontRequest.php @@ -542,8 +542,12 @@ final class AphrontRequest extends Phobject { return $this->isFormPost() && $this->getStr('__dialog__'); } - public function getRemoteAddr() { - return $_SERVER['REMOTE_ADDR']; + public function getRemoteAddress() { + $address = $_SERVER['REMOTE_ADDR']; + if (!strlen($address)) { + return null; + } + return substr($address, 0, 64); } public function isHTTPS() { diff --git a/src/applications/config/schema/PhabricatorConfigSchemaSpec.php b/src/applications/config/schema/PhabricatorConfigSchemaSpec.php index 5b48fcbd03..740402524a 100644 --- a/src/applications/config/schema/PhabricatorConfigSchemaSpec.php +++ b/src/applications/config/schema/PhabricatorConfigSchemaSpec.php @@ -322,6 +322,7 @@ abstract class PhabricatorConfigSchemaSpec extends Phobject { case 'phid': case 'policy'; case 'hashpath64': + case 'ipaddress': $column_type = 'varbinary(64)'; break; case 'bytes64': diff --git a/src/applications/diffusion/controller/DiffusionServeController.php b/src/applications/diffusion/controller/DiffusionServeController.php index 13290b9f41..8f3eb364f6 100644 --- a/src/applications/diffusion/controller/DiffusionServeController.php +++ b/src/applications/diffusion/controller/DiffusionServeController.php @@ -76,8 +76,7 @@ final class DiffusionServeController extends DiffusionController { } try { - $remote_addr = $request->getRemoteAddr(); - $remote_addr = ip2long($remote_addr); + $remote_addr = $request->getRemoteAddress(); $pull_event = id(new PhabricatorRepositoryPullEvent()) ->setEpoch(PhabricatorTime::getNow()) @@ -720,11 +719,11 @@ final class DiffusionServeController extends DiffusionController { } private function getCommonEnvironment(PhabricatorUser $viewer) { - $remote_addr = $this->getRequest()->getRemoteAddr(); + $remote_address = $this->getRequest()->getRemoteAddress(); return array( DiffusionCommitHookEngine::ENV_USER => $viewer->getUsername(), - DiffusionCommitHookEngine::ENV_REMOTE_ADDRESS => $remote_addr, + DiffusionCommitHookEngine::ENV_REMOTE_ADDRESS => $remote_address, DiffusionCommitHookEngine::ENV_REMOTE_PROTOCOL => 'http', ); } diff --git a/src/applications/diffusion/engine/DiffusionCommitHookEngine.php b/src/applications/diffusion/engine/DiffusionCommitHookEngine.php index a723591212..741b21bd19 100644 --- a/src/applications/diffusion/engine/DiffusionCommitHookEngine.php +++ b/src/applications/diffusion/engine/DiffusionCommitHookEngine.php @@ -56,15 +56,6 @@ final class DiffusionCommitHookEngine extends Phobject { return $this->remoteAddress; } - private function getRemoteAddressForLog() { - // If whatever we have here isn't a valid IPv4 address, just store `null`. - // Older versions of PHP return `-1` on failure instead of `false`. - $remote_address = $this->getRemoteAddress(); - $remote_address = max(0, ip2long($remote_address)); - $remote_address = nonempty($remote_address, null); - return $remote_address; - } - public function setSubversionTransactionInfo($transaction, $repository) { $this->subversionTransaction = $transaction; $this->subversionRepository = $repository; @@ -1078,7 +1069,7 @@ final class DiffusionCommitHookEngine extends Phobject { $viewer = $this->getViewer(); return PhabricatorRepositoryPushEvent::initializeNewEvent($viewer) ->setRepositoryPHID($this->getRepository()->getPHID()) - ->setRemoteAddress($this->getRemoteAddressForLog()) + ->setRemoteAddress($this->getRemoteAddress()) ->setRemoteProtocol($this->getRemoteProtocol()) ->setEpoch(time()); } diff --git a/src/applications/diffusion/view/DiffusionPushLogListView.php b/src/applications/diffusion/view/DiffusionPushLogListView.php index 860320e624..7cc02a49b4 100644 --- a/src/applications/diffusion/view/DiffusionPushLogListView.php +++ b/src/applications/diffusion/view/DiffusionPushLogListView.php @@ -42,12 +42,9 @@ final class DiffusionPushLogListView extends AphrontView { $repository = $log->getRepository(); // Reveal this if it's valid and the user can edit the repository. - $remote_addr = '-'; + $remote_address = '-'; if (isset($editable_repos[$log->getRepositoryPHID()])) { - $remote_long = $log->getPushEvent()->getRemoteAddress(); - if ($remote_long) { - $remote_addr = long2ip($remote_long); - } + $remote_address = $log->getPushEvent()->getRemoteAddress(); } $event_id = $log->getPushEvent()->getID(); @@ -76,7 +73,7 @@ final class DiffusionPushLogListView extends AphrontView { ), $repository->getDisplayName()), $handles[$log->getPusherPHID()]->renderLink(), - $remote_addr, + $remote_address, $log->getPushEvent()->getRemoteProtocol(), $log->getRefType(), $log->getRefName(), diff --git a/src/applications/people/view/PhabricatorUserLogView.php b/src/applications/people/view/PhabricatorUserLogView.php index 12bcee9d76..c467a9010d 100644 --- a/src/applications/people/view/PhabricatorUserLogView.php +++ b/src/applications/people/view/PhabricatorUserLogView.php @@ -41,13 +41,13 @@ final class PhabricatorUserLogView extends AphrontView { $ip = phutil_tag( 'a', array( - 'href' => $base_uri.'?ip='.$log->getRemoteAddr().'#R', + 'href' => $base_uri.'?ip='.$ip.'#R', ), $ip); $session = phutil_tag( 'a', array( - 'href' => $base_uri.'?sessions='.$log->getSession().'#R', + 'href' => $base_uri.'?sessions='.$ip.'#R', ), $session); } diff --git a/src/applications/repository/storage/PhabricatorRepositoryPullEvent.php b/src/applications/repository/storage/PhabricatorRepositoryPullEvent.php index d17fded9a8..c1227402d7 100644 --- a/src/applications/repository/storage/PhabricatorRepositoryPullEvent.php +++ b/src/applications/repository/storage/PhabricatorRepositoryPullEvent.php @@ -30,7 +30,7 @@ final class PhabricatorRepositoryPullEvent self::CONFIG_COLUMN_SCHEMA => array( 'repositoryPHID' => 'phid?', 'pullerPHID' => 'phid?', - 'remoteAddress' => 'uint32?', + 'remoteAddress' => 'ipaddress?', 'remoteProtocol' => 'text32?', 'resultType' => 'text32', 'resultCode' => 'uint32', diff --git a/src/applications/repository/storage/PhabricatorRepositoryPushEvent.php b/src/applications/repository/storage/PhabricatorRepositoryPushEvent.php index 2455499b88..2bc751ffca 100644 --- a/src/applications/repository/storage/PhabricatorRepositoryPushEvent.php +++ b/src/applications/repository/storage/PhabricatorRepositoryPushEvent.php @@ -29,7 +29,7 @@ final class PhabricatorRepositoryPushEvent self::CONFIG_AUX_PHID => true, self::CONFIG_TIMESTAMPS => false, self::CONFIG_COLUMN_SCHEMA => array( - 'remoteAddress' => 'uint32?', + 'remoteAddress' => 'ipaddress?', 'remoteProtocol' => 'text32?', 'rejectCode' => 'uint32', 'rejectDetails' => 'text64?', From 5263c5bea4a0e57b15515844b21fa48928792b92 Mon Sep 17 00:00:00 2001 From: Chad Little Date: Tue, 2 Feb 2016 12:42:28 -0800 Subject: [PATCH 24/54] Fix setting of default project tab Summary: I don't PHP. Fixes T10256 Test Plan: Test many menus. Reviewers: epriestley Reviewed By: epriestley Subscribers: Korvin Maniphest Tasks: T10256 Differential Revision: https://secure.phabricator.com/D15166 --- .../search/engine/PhabricatorProfilePanelEngine.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/applications/search/engine/PhabricatorProfilePanelEngine.php b/src/applications/search/engine/PhabricatorProfilePanelEngine.php index 09307cd340..a3907feac8 100644 --- a/src/applications/search/engine/PhabricatorProfilePanelEngine.php +++ b/src/applications/search/engine/PhabricatorProfilePanelEngine.php @@ -663,6 +663,7 @@ abstract class PhabricatorProfilePanelEngine extends Phobject { $header = id(new PHUIHeaderView()) ->setHeader(pht('Profile Menu Items')) + ->setSubHeader(pht('Drag tabs to reorder menu')) ->addActionLink($action_button); $box = id(new PHUIObjectBoxView()) @@ -907,7 +908,7 @@ abstract class PhabricatorProfilePanelEngine extends Phobject { $is_target = (($builtin_key !== null) && ($builtin_key === $key)) || - (($id !== null) && ($id === (int)$key)); + (($id !== null) && ((int)$id === (int)$key)); if ($is_target) { if (!$panel->isDefault()) { From 268a9ced78da9de2e88c6d360a5681b41283df13 Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 2 Feb 2016 12:54:27 -0800 Subject: [PATCH 25/54] Implement subproject/milestone conflict resolution rules Summary: Ref T10010. When you try to add "Sprint 35" to a task, remove "Sprint 34", etc. Briefly: - A task can't be in Sprint 3 and Sprint 4. - A task can't be in "A" and "A > B" (but "A > B" and "A > C" are fine). - When a user makes an edit which would violate one of these rules, preserve the last tag in each group of conflicts. Test Plan: - Added fairly comprehensive tests. - Added a bunch of different tags to things, saw them properly exclude conflicting tags. Reviewers: chad Reviewed By: chad Maniphest Tasks: T10010 Differential Revision: https://secure.phabricator.com/D15167 --- .../PhabricatorProjectCoreTestCase.php | 132 +++++++++++++++++ ...habricatorApplicationTransactionEditor.php | 133 +++++++++++++++++- src/docs/user/userguide/projects.diviner | 33 +++++ 3 files changed, 297 insertions(+), 1 deletion(-) diff --git a/src/applications/project/__tests__/PhabricatorProjectCoreTestCase.php b/src/applications/project/__tests__/PhabricatorProjectCoreTestCase.php index 3bd38353f6..6c0f05ebbb 100644 --- a/src/applications/project/__tests__/PhabricatorProjectCoreTestCase.php +++ b/src/applications/project/__tests__/PhabricatorProjectCoreTestCase.php @@ -808,6 +808,114 @@ final class PhabricatorProjectCoreTestCase extends PhabricatorTestCase { pht('Engineering + Scan')); } + public function testTagAncestryConflicts() { + $user = $this->createUser(); + $user->save(); + + $stonework = $this->createProject($user); + $stonework_masonry = $this->createProject($user, $stonework); + $stonework_sculpting = $this->createProject($user, $stonework); + + $task = $this->newTask($user, array()); + $this->assertEqual(array(), $this->getTaskProjects($task)); + + $this->addProjectTags($user, $task, array($stonework->getPHID())); + $this->assertEqual( + array( + $stonework->getPHID(), + ), + $this->getTaskProjects($task)); + + // Adding a descendant should remove the parent. + $this->addProjectTags($user, $task, array($stonework_masonry->getPHID())); + $this->assertEqual( + array( + $stonework_masonry->getPHID(), + ), + $this->getTaskProjects($task)); + + // Adding an ancestor should remove the descendant. + $this->addProjectTags($user, $task, array($stonework->getPHID())); + $this->assertEqual( + array( + $stonework->getPHID(), + ), + $this->getTaskProjects($task)); + + // Adding two tags in the same hierarchy which are not mutual ancestors + // should remove the ancestor but otherwise work fine. + $this->addProjectTags( + $user, + $task, + array( + $stonework_masonry->getPHID(), + $stonework_sculpting->getPHID(), + )); + + $expect = array( + $stonework_masonry->getPHID(), + $stonework_sculpting->getPHID(), + ); + sort($expect); + + $this->assertEqual($expect, $this->getTaskProjects($task)); + } + + public function testTagMilestoneConflicts() { + $user = $this->createUser(); + $user->save(); + + $stonework = $this->createProject($user); + $stonework_1 = $this->createProject($user, $stonework, true); + $stonework_2 = $this->createProject($user, $stonework, true); + + $task = $this->newTask($user, array()); + $this->assertEqual(array(), $this->getTaskProjects($task)); + + $this->addProjectTags($user, $task, array($stonework->getPHID())); + $this->assertEqual( + array( + $stonework->getPHID(), + ), + $this->getTaskProjects($task)); + + // Adding a milesone should remove the parent. + $this->addProjectTags($user, $task, array($stonework_1->getPHID())); + $this->assertEqual( + array( + $stonework_1->getPHID(), + ), + $this->getTaskProjects($task)); + + // Adding the parent should remove the milestone. + $this->addProjectTags($user, $task, array($stonework->getPHID())); + $this->assertEqual( + array( + $stonework->getPHID(), + ), + $this->getTaskProjects($task)); + + // First, add one milestone. + $this->addProjectTags($user, $task, array($stonework_1->getPHID())); + // Now, adding a second milestone should remove the first milestone. + $this->addProjectTags($user, $task, array($stonework_2->getPHID())); + $this->assertEqual( + array( + $stonework_2->getPHID(), + ), + $this->getTaskProjects($task)); + } + + private function getTaskProjects(ManiphestTask $task) { + $project_phids = PhabricatorEdgeQuery::loadDestinationPHIDs( + $task->getPHID(), + PhabricatorProjectObjectHasProjectEdgeType::EDGECONST); + + sort($project_phids); + + return $project_phids; + } + private function attemptProjectEdit( PhabricatorProject $proj, PhabricatorUser $user, @@ -827,6 +935,30 @@ final class PhabricatorProjectCoreTestCase extends PhabricatorTestCase { } + private function addProjectTags( + PhabricatorUser $viewer, + ManiphestTask $task, + array $phids) { + + $xactions = array(); + + $xactions[] = id(new ManiphestTransaction()) + ->setTransactionType(PhabricatorTransactions::TYPE_EDGE) + ->setMetadataValue( + 'edge:type', + PhabricatorProjectObjectHasProjectEdgeType::EDGECONST) + ->setNewValue( + array( + '+' => array_fuse($phids), + )); + + $editor = id(new ManiphestTransactionEditor()) + ->setActor($viewer) + ->setContentSource(PhabricatorContentSource::newConsoleSource()) + ->setContinueOnNoEffect(true) + ->applyTransactions($task, $xactions); + } + private function newTask( PhabricatorUser $viewer, array $projects, diff --git a/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php b/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php index 7fa83b3fb0..d6f70cac39 100644 --- a/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php +++ b/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php @@ -400,7 +400,15 @@ abstract class PhabricatorApplicationTransactionEditor return $space_phid; } case PhabricatorTransactions::TYPE_EDGE: - return $this->getEdgeTransactionNewValue($xaction); + $new_value = $this->getEdgeTransactionNewValue($xaction); + + $edge_type = $xaction->getMetadataValue('edge:type'); + $type_project = PhabricatorProjectObjectHasProjectEdgeType::EDGECONST; + if ($edge_type == $type_project) { + $new_value = $this->applyProjectConflictRules($new_value); + } + + return $new_value; case PhabricatorTransactions::TYPE_CUSTOMFIELD: $field = $this->getCustomFieldForTransaction($object, $xaction); return $field->getNewValueFromApplicationTransactions($xaction); @@ -3346,4 +3354,127 @@ abstract class PhabricatorApplicationTransactionEditor return $state; } + + /** + * Remove conflicts from a list of projects. + * + * Objects aren't allowed to be tagged with multiple milestones in the same + * group, nor projects such that one tag is the ancestor of any other tag. + * If the list of PHIDs include mutually exclusive projects, remove the + * conflicting projects. + * + * @param list List of project PHIDs. + * @return list List with conflicts removed. + */ + private function applyProjectConflictRules(array $phids) { + if (!$phids) { + return array(); + } + + // Overall, the last project in the list wins in cases of conflict (so when + // you add something, the thing you just added sticks and removes older + // values). + + // Beyond that, there are two basic cases: + + // Milestones: An object can't be in "A > Sprint 3" and "A > Sprint 4". + // If multiple projects are milestones of the same parent, we only keep the + // last one. + + // Ancestor: You can't be in "A" and "A > B". If "A > B" comes later + // in the list, we remove "A" and keep "A > B". If "A" comes later, we + // remove "A > B" and keep "A". + + // Note that it's OK to be in "A > B" and "A > C". There's only a conflict + // if one project is an ancestor of another. It's OK to have something + // tagged with multiple projects which share a common ancestor, so long as + // they are not mutual ancestors. + + $viewer = PhabricatorUser::getOmnipotentUser(); + + $projects = id(new PhabricatorProjectQuery()) + ->setViewer($viewer) + ->withPHIDs(array_keys($phids)) + ->execute(); + $projects = mpull($projects, null, 'getPHID'); + + // We're going to build a map from each project with milestones to the last + // milestone in the list. This last milestone is the milestone we'll keep. + $milestone_map = array(); + + // We're going to build a set of the projects which have no descendants + // later in the list. This allows us to apply both ancestor rules. + $ancestor_map = array(); + + foreach ($phids as $phid => $ignored) { + $project = idx($projects, $phid); + if (!$project) { + continue; + } + + // This is the last milestone we've seen, so set it as the selection for + // the project's parent. This might be setting a new value or overwriting + // an earlier value. + if ($project->isMilestone()) { + $parent_phid = $project->getParentProjectPHID(); + $milestone_map[$parent_phid] = $phid; + } + + // Since this is the last item in the list we've examined so far, add it + // to the set of projects with no later descendants. + $ancestor_map[$phid] = $phid; + + // Remove any ancestors from the set, since this is a later descendant. + foreach ($project->getAncestorProjects() as $ancestor) { + $ancestor_phid = $ancestor->getPHID(); + unset($ancestor_map[$ancestor_phid]); + } + } + + // Now that we've built the maps, we can throw away all the projects which + // have conflicts. + foreach ($phids as $phid => $ignored) { + $project = idx($projects, $phid); + + if (!$project) { + // If a PHID is invalid, we just leave it as-is. We could clean it up, + // but leaving it untouched is less likely to cause collateral damage. + continue; + } + + // If this was a milestone, check if it was the last milestone from its + // group in the list. If not, remove it from the list. + if ($project->isMilestone()) { + $parent_phid = $project->getParentProjectPHID(); + if ($milestone_map[$parent_phid] !== $phid) { + unset($phids[$phid]); + continue; + } + } + + // If a later project in the list is a subproject of this one, it will + // have removed ancestors from the map. If this project does not point + // at itself in the ancestor map, it should be discarded in favor of a + // subproject that comes later. + if (idx($ancestor_map, $phid) !== $phid) { + unset($phids[$phid]); + continue; + } + + // If a later project in the list is an ancestor of this one, it will + // have added itself to the map. If any ancestor of this project points + // at itself in the map, this project should be dicarded in favor of + // that later ancestor. + foreach ($project->getAncestorProjects() as $ancestor) { + $ancestor_phid = $ancestor->getPHID(); + if (isset($ancestor_map[$ancestor_phid])) { + unset($phids[$phid]); + continue 2; + } + } + } + + return $phids; + } + } diff --git a/src/docs/user/userguide/projects.diviner b/src/docs/user/userguide/projects.diviner index b1456a9e77..7d014f3fb7 100644 --- a/src/docs/user/userguide/projects.diviner +++ b/src/docs/user/userguide/projects.diviner @@ -184,6 +184,26 @@ subproject rules. Subprojects can have normal workboards. +The maximum subproject depth is 16. This limit is intended to grossly exceed +the depth necessary in normal usage. + +Objects may not be tagged with multiple projects that are ancestors or +descendants of one another. For example, a task may not be tagged with both +{nav Stonework} and {nav Stonework > Masonry}. + +When a project tag is added that is the ancestor or descendant of one or more +existing tags, the old tags are replaced. For example, adding +{nav Stonework > Masonry} to a task tagged with {nav Stonework} will replace +{nav Stonework} with the newer, more specific tag. + +This restriction does not apply to projects which share some common ancestor +but are not themselves mutual ancestors. For example, a task may be tagged +with both {nav Stonework > Masonry} and {nav Stonework > Sculpting}. + +This restriction //does// apply when the descendant is a milestone. For +example, a task may not be tagged with both {nav Stonework} and +{nav Stonework > Iteration II}. + Milestones ========== @@ -204,6 +224,19 @@ By default, Milestones do not have their own hashtags. Milestones can have normal workboards. +Objects may not be tagged with two different milestones of the same parent +project. For example, a task may not be tagged with both {nav Stonework > +Iteration III} and {nav Stonework > Iteration V}. + +When a milestone tag is added to an object which already has a tag from the +same series of milestones, the old tag is removed. For example, adding the +{nav Stonework > Iteration V} tag to a task which already has the +{nav Stonework > Iteration III} tag will remove the {nav Iteration III} tag. + +This restriction does not apply to milestones which are not part of the same +series. For example, a task may be tagged with both +{nav Stonework > Iteration V} and {nav Heraldry > Iteration IX}. + Parent Projects =============== From d15d0486c848c7f43f9ef7d94fb0f8987c7c6df0 Mon Sep 17 00:00:00 2001 From: Chad Little Date: Tue, 2 Feb 2016 13:41:01 -0800 Subject: [PATCH 26/54] Minor CSS workboard tweaks Summary: Clean up a little spacing. Test Plan: Pixels. Reviewers: epriestley Reviewed By: epriestley Subscribers: Korvin Differential Revision: https://secure.phabricator.com/D15169 --- resources/celerity/map.php | 14 +++++++------- webroot/rsrc/css/phui/workboards/phui-workcard.css | 4 ++++ .../rsrc/css/phui/workboards/phui-workpanel.css | 1 + 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 203f285fe3..16da562d2a 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -154,8 +154,8 @@ 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' => '42c703d7', - 'rsrc/css/phui/workboards/phui-workpanel.css' => 'b90970eb', + 'rsrc/css/phui/workboards/phui-workcard.css' => '0d1aa006', + 'rsrc/css/phui/workboards/phui-workpanel.css' => '68140031', 'rsrc/css/sprite-login.css' => '60e8560e', 'rsrc/css/sprite-menu.css' => '9dd65b92', 'rsrc/css/sprite-tokens.css' => '4f399012', @@ -831,8 +831,8 @@ return array( 'phui-timeline-view-css' => '2efceff8', 'phui-two-column-view-css' => 'c75bfc5b', 'phui-workboard-view-css' => 'b07a5524', - 'phui-workcard-view-css' => '42c703d7', - 'phui-workpanel-view-css' => 'b90970eb', + 'phui-workcard-view-css' => '0d1aa006', + 'phui-workpanel-view-css' => '68140031', 'phuix-action-list-view' => 'b5c256b8', 'phuix-action-view' => '8cf6d262', 'phuix-autocomplete' => '9196fb06', @@ -1319,6 +1319,9 @@ return array( 'javelin-vector', 'phabricator-hovercard', ), + 68140031 => array( + 'phui-workcard-view-css', + ), '6882e80a' => array( 'javelin-dom', ), @@ -1752,9 +1755,6 @@ return array( 'b6b0d1bb' => array( 'phui-inline-comment-view-css', ), - 'b90970eb' => array( - 'phui-workcard-view-css', - ), 'bd4c8dca' => array( 'javelin-install', 'javelin-util', diff --git a/webroot/rsrc/css/phui/workboards/phui-workcard.css b/webroot/rsrc/css/phui/workboards/phui-workcard.css index a921db4d32..671833ea86 100644 --- a/webroot/rsrc/css/phui/workboards/phui-workcard.css +++ b/webroot/rsrc/css/phui/workboards/phui-workcard.css @@ -88,6 +88,10 @@ margin-right: 12px; } +.phui-workpanel-view .drag-ghost { + margin-bottom: 8px; +} + /* - Draggable Colors --------------------------------------------------------*/ diff --git a/webroot/rsrc/css/phui/workboards/phui-workpanel.css b/webroot/rsrc/css/phui/workboards/phui-workpanel.css index 28b98f4ea0..b2945a4e6d 100644 --- a/webroot/rsrc/css/phui/workboards/phui-workpanel.css +++ b/webroot/rsrc/css/phui/workboards/phui-workpanel.css @@ -19,6 +19,7 @@ margin: 0; display: inline-block; color: {$lightgreytext}; + font-size: {$normalfontsize}; } .device .phui-workpanel-view .phui-header-shell { From 95af3624d754bb1da2e61661e6eb74b1e4ada1d9 Mon Sep 17 00:00:00 2001 From: Chad Little Date: Tue, 2 Feb 2016 14:45:50 -0800 Subject: [PATCH 27/54] Flip layout on PhameHome Summary: Centers the page for consistency for the rest of Phame, puts blog list on right for better mobile support. Test Plan: Review PhameHome at all breakpoints. Reviewers: epriestley Reviewed By: epriestley Subscribers: Korvin Differential Revision: https://secure.phabricator.com/D15170 --- resources/celerity/map.php | 10 ++--- .../phame/controller/PhameHomeController.php | 39 +++++-------------- .../application/base/standard-page-view.css | 6 +-- webroot/rsrc/css/application/phame/phame.css | 30 +++++++++----- 4 files changed, 37 insertions(+), 48 deletions(-) diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 16da562d2a..cb5ab3509a 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -7,7 +7,7 @@ */ return array( 'names' => array( - 'core.pkg.css' => '764d4c80', + 'core.pkg.css' => 'e33b14a4', 'core.pkg.js' => '53c6a7c5', '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' => '7b0d68d8', + 'rsrc/css/application/base/standard-page-view.css' => 'c4467133', 'rsrc/css/application/chatlog/chatlog.css' => 'd295b020', 'rsrc/css/application/conduit/conduit-api.css' => '7bc725c4', 'rsrc/css/application/config/config-options.css' => '0ede4c9b', @@ -81,7 +81,7 @@ return array( 'rsrc/css/application/owners/owners-path-editor.css' => '2f00933b', 'rsrc/css/application/paste/paste.css' => 'a5157c48', 'rsrc/css/application/people/people-profile.css' => '2473d929', - 'rsrc/css/application/phame/phame.css' => '6d5b3682', + 'rsrc/css/application/phame/phame.css' => '1dbbacf9', 'rsrc/css/application/pholio/pholio-edit.css' => '3ad9d1ee', 'rsrc/css/application/pholio/pholio-inline-comments.css' => '8e545e49', 'rsrc/css/application/pholio/pholio.css' => '95174bdd', @@ -765,7 +765,7 @@ return array( 'phabricator-side-menu-view-css' => '3a3d9f41', 'phabricator-slowvote-css' => 'da0afb1b', 'phabricator-source-code-view-css' => 'cbeef983', - 'phabricator-standard-page-view' => '7b0d68d8', + 'phabricator-standard-page-view' => 'c4467133', 'phabricator-textareautils' => '9e54692d', 'phabricator-title' => 'df5e11d2', 'phabricator-tooltip' => '6323f942', @@ -781,7 +781,7 @@ return array( 'phabricator-uiexample-reactor-sendclass' => '1def2711', 'phabricator-uiexample-reactor-sendproperties' => 'b1f0ccee', 'phabricator-zindex-css' => '5c7025bf', - 'phame-css' => '6d5b3682', + 'phame-css' => '1dbbacf9', 'pholio-css' => '95174bdd', 'pholio-edit-css' => '3ad9d1ee', 'pholio-inline-comments-css' => '8e545e49', diff --git a/src/applications/phame/controller/PhameHomeController.php b/src/applications/phame/controller/PhameHomeController.php index b9c7fc76d8..cea86735b5 100644 --- a/src/applications/phame/controller/PhameHomeController.php +++ b/src/applications/phame/controller/PhameHomeController.php @@ -55,20 +55,17 @@ final class PhameHomeController extends PhamePostController { ->addAction($create_button); } - $actions = $this->renderActions($viewer); - $action_button = id(new PHUIButtonView()) + $view_all = id(new PHUIButtonView()) ->setTag('a') - ->setText(pht('Actions')) - ->setHref('#') - ->setIcon('fa-bars') - ->addClass('phui-mobile-menu') - ->setDropdownMenu($actions); + ->setText(pht('View All')) + ->setHref($this->getApplicationURI('post/')) + ->setIcon('fa-list-ul'); $title = pht('Recent Posts'); $header = id(new PHUIHeaderView()) ->setHeader($title) - ->addActionLink($action_button); + ->addActionLink($view_all); $crumbs = $this->buildApplicationCrumbs(); $crumbs->setBorder(true); @@ -108,39 +105,21 @@ final class PhameHomeController extends PhamePostController { $blog_list, $draft_list, )) - ->setDisplay(PHUITwoColumnView::DISPLAY_LEFT) - ->addClass('phame-home-view'); + ->addClass('phame-home-container'); + + $phame_home = phutil_tag_div('phame-home-view', $phame_view); return $this->newPage() ->setTitle($title) ->setCrumbs($crumbs) ->appendChild( array( - $phame_view, + $phame_home, )); } - private function renderActions($viewer) { - $actions = id(new PhabricatorActionListView()) - ->setUser($viewer); - - $actions->addAction( - id(new PhabricatorActionView()) - ->setIcon('fa-pencil') - ->setHref($this->getApplicationURI('post/query/draft/')) - ->setName(pht('My Drafts'))); - - $actions->addAction( - id(new PhabricatorActionView()) - ->setIcon('fa-pencil-square-o') - ->setHref($this->getApplicationURI('post/')) - ->setName(pht('All Posts'))); - - return $actions; - } - private function renderBlogs($viewer, $blogs) {} protected function buildApplicationCrumbs() { diff --git a/webroot/rsrc/css/application/base/standard-page-view.css b/webroot/rsrc/css/application/base/standard-page-view.css index 830554db3e..ca69443d07 100644 --- a/webroot/rsrc/css/application/base/standard-page-view.css +++ b/webroot/rsrc/css/application/base/standard-page-view.css @@ -19,14 +19,14 @@ .phabricator-standard-page-footer { text-align: right; - margin: 32px 16px 16px; + margin: 44px 16px 16px; padding: 12px 0; - border-top: 1px solid rgba(71, 87, 120, 0.20); + border-top: 1px solid rgba(55,55,55,.1); color: {$greytext}; } .device .phabricator-standard-page-footer { - margin: 4px 8px; + margin: 24px 8px 16px; } !print .phabricator-standard-page-footer { diff --git a/webroot/rsrc/css/application/phame/phame.css b/webroot/rsrc/css/application/phame/phame.css index be161c1ede..3d151426d6 100644 --- a/webroot/rsrc/css/application/phame/phame.css +++ b/webroot/rsrc/css/application/phame/phame.css @@ -43,24 +43,34 @@ background-color: #fff; } -.device .phame-home-view .phui-side-column { - background-color: transparent; +.phame-home-view { + background-color: #fff; + border-bottom: 1px solid rgba(55,55,55,.1); +} + +.phame-home-view .phame-home-container { + max-width: 980px; + margin: 0 auto; +} + +.phame-home-view .phui-document-container { + border: none; } .phame-blog-list { - margin: 24px 16px 16px 16px; + margin: 96px 16px 16px 16px; +} + +.phame-blog-list + .phame-blog-list { + margin-top: 24px; } .device .phame-blog-list { - padding: 0; - background-color: {$bluebackground}; - margin: 0; - border-radius: 0; - border-bottom: 1px solid {$thinblueborder}; + margin: 16px; } -.phame-blog-list-item:last-child { - margin-bottom: 0; +.device-phone .phame-blog-list { + margin: 16px 8px; } .phame-blog-list-header { From d156da340281973bf87f9829b78a7f890678cf50 Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 3 Feb 2016 05:57:17 -0800 Subject: [PATCH 28/54] Clarify why VCS passwords must be unique Summary: Fixes T10265. Test Plan: Read text. Reviewers: chad Reviewed By: chad Maniphest Tasks: T10265 Differential Revision: https://secure.phabricator.com/D15173 --- src/docs/user/userguide/diffusion_hosting.diviner | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/docs/user/userguide/diffusion_hosting.diviner b/src/docs/user/userguide/diffusion_hosting.diviner index ecf7e1c3e3..6427be92e5 100644 --- a/src/docs/user/userguide/diffusion_hosting.diviner +++ b/src/docs/user/userguide/diffusion_hosting.diviner @@ -127,8 +127,13 @@ If you plan to use authenticated HTTP, you need to set use only anonymous HTTP, you can leave this setting disabled. If you plan to use authenticated HTTP, you'll also need to configure a VCS -password in {nav Settings > VCS Password}. This is a different password than -your main Phabricator password primarily for security reasons. +password in {nav Settings > VCS Password}. + +Your VCS password must be a different password than your main Phabricator +password because VCS passwords are very easy to accidentally disclose. They are +often stored in plaintext in world-readable files, observable in `ps` output, +and present in command output and logs. We strongly encourage you to use SSH +instead of HTTP to authenticate access to repositories. Otherwise, if you've configured system accounts above, you're all set. No additional server configuration is required to make HTTP work. From 6bb24e1d0cbd76573c7dbd0fcb95f9959acfd9ab Mon Sep 17 00:00:00 2001 From: Chad Little Date: Wed, 3 Feb 2016 16:26:30 +0000 Subject: [PATCH 29/54] Move PhabricatorHovercard to PHUIHovercard Summary: No UI changes, just some search and replace for UI consistency. Test Plan: Test person and object hovercards still work. UIExamples too. Reviewers: epriestley Reviewed By: epriestley Subscribers: Korvin Differential Revision: https://secure.phabricator.com/D15172 --- resources/celerity/map.php | 46 +++++++++---------- resources/celerity/packages.php | 4 +- src/__phutil_library_map__.php | 12 ++--- .../DifferentialHovercardEngineExtension.php | 2 +- .../DiffusionHovercardEngineExtension.php | 2 +- .../ManiphestHovercardEngineExtension.php | 2 +- ...php => PeopleHovercardEngineExtension.php} | 4 +- .../markup/PhabricatorMentionRemarkupRule.php | 2 +- .../phid/PhabricatorObjectHandle.php | 2 +- .../PhabricatorSearchHovercardController.php | 2 +- .../PhabricatorHovercardEngineExtension.php | 2 +- ...Example.php => PHUIHovercardUIExample.php} | 12 ++--- src/view/phui/PHUIFeedStoryView.php | 2 +- .../PHUIHovercardView.php} | 24 +++++----- src/view/phui/PHUITagView.php | 2 +- .../phui-hovercard.css} | 34 +++++++------- webroot/rsrc/js/core/Hovercard.js | 2 +- webroot/rsrc/js/core/behavior-hovercard.js | 6 +-- 18 files changed, 81 insertions(+), 81 deletions(-) rename src/applications/people/engineextension/{PhabricatorPeopleHovercardEngineExtension.php => PeopleHovercardEngineExtension.php} (96%) rename src/applications/uiexample/examples/{PhabricatorHovercardUIExample.php => PHUIHovercardUIExample.php} (87%) rename src/view/{widget/hovercard/PhabricatorHovercardView.php => phui/PHUIHovercardView.php} (82%) rename webroot/rsrc/css/{layout/phabricator-hovercard-view.css => phui/phui-hovercard.css} (63%) diff --git a/resources/celerity/map.php b/resources/celerity/map.php index cb5ab3509a..69819d4bd9 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -8,7 +8,7 @@ return array( 'names' => array( 'core.pkg.css' => 'e33b14a4', - 'core.pkg.js' => '53c6a7c5', + 'core.pkg.js' => '7214314b', 'darkconsole.pkg.js' => 'e7393ebb', 'differential.pkg.css' => '2de124c9', 'differential.pkg.js' => '5c2ba922', @@ -112,7 +112,6 @@ return array( 'rsrc/css/font/font-lato.css' => 'c7ccd872', 'rsrc/css/font/phui-font-icon-base.css' => 'ecbbb4c2', 'rsrc/css/layout/phabricator-filetree-view.css' => 'fccf9f82', - 'rsrc/css/layout/phabricator-hovercard-view.css' => '1239cd52', 'rsrc/css/layout/phabricator-side-menu-view.css' => '3a3d9f41', 'rsrc/css/layout/phabricator-source-code-view.css' => 'cbeef983', 'rsrc/css/phui/calendar/phui-calendar-day.css' => 'd1cf6f93', @@ -135,6 +134,7 @@ return array( 'rsrc/css/phui/phui-form-view.css' => '4a1a0f5e', 'rsrc/css/phui/phui-form.css' => '0b98e572', 'rsrc/css/phui/phui-header-view.css' => 'd53cc835', + 'rsrc/css/phui/phui-hovercard.css' => '5684c081', 'rsrc/css/phui/phui-icon-set-selector.css' => '1ab67aad', 'rsrc/css/phui/phui-icon.css' => '3f33ab57', 'rsrc/css/phui/phui-image-mask.css' => '5a8b09c8', @@ -448,7 +448,7 @@ return array( 'rsrc/js/core/DragAndDropFileUpload.js' => 'ad10aeac', 'rsrc/js/core/DraggableList.js' => '8905523d', 'rsrc/js/core/FileUpload.js' => '477359c8', - 'rsrc/js/core/Hovercard.js' => 'c6f720ff', + 'rsrc/js/core/Hovercard.js' => '1bd28176', 'rsrc/js/core/KeyboardShortcut.js' => '1ae869f2', 'rsrc/js/core/KeyboardShortcutManager.js' => 'c1700f6f', 'rsrc/js/core/MultirowRowManager.js' => 'b5d57730', @@ -474,7 +474,7 @@ return array( 'rsrc/js/core/behavior-global-drag-and-drop.js' => 'c8e57404', 'rsrc/js/core/behavior-high-security-warning.js' => 'a464fe03', 'rsrc/js/core/behavior-history-install.js' => '7ee2b591', - 'rsrc/js/core/behavior-hovercard.js' => '66dd6e9e', + 'rsrc/js/core/behavior-hovercard.js' => 'bcaccd64', 'rsrc/js/core/behavior-keyboard-pager.js' => 'a8da01f0', 'rsrc/js/core/behavior-keyboard-shortcuts.js' => 'd75709e6', 'rsrc/js/core/behavior-lightbox-attachments.js' => 'f8ba29d7', @@ -630,7 +630,6 @@ return array( 'javelin-behavior-phabricator-file-tree' => '88236f00', 'javelin-behavior-phabricator-gesture' => '3ab51e2c', 'javelin-behavior-phabricator-gesture-example' => '558829c2', - 'javelin-behavior-phabricator-hovercards' => '66dd6e9e', 'javelin-behavior-phabricator-keyboard-pager' => 'a8da01f0', 'javelin-behavior-phabricator-keyboard-shortcuts' => 'd75709e6', 'javelin-behavior-phabricator-line-linker' => '1499a8cb', @@ -649,6 +648,7 @@ return array( 'javelin-behavior-pholio-mock-edit' => '246dc085', 'javelin-behavior-pholio-mock-view' => 'fbe497e7', 'javelin-behavior-phui-dropdown-menu' => '54733475', + 'javelin-behavior-phui-hovercards' => 'bcaccd64', 'javelin-behavior-phui-object-box-tabs' => '2bfa2836', 'javelin-behavior-phui-profile-menu' => '12884df9', 'javelin-behavior-policy-control' => 'ae45872f', @@ -747,8 +747,6 @@ return array( 'phabricator-file-upload' => '477359c8', 'phabricator-filetree-view-css' => 'fccf9f82', 'phabricator-flag-css' => '5337623f', - 'phabricator-hovercard' => 'c6f720ff', - 'phabricator-hovercard-view-css' => '1239cd52', 'phabricator-keyboard-shortcut' => '1ae869f2', 'phabricator-keyboard-shortcut-manager' => 'c1700f6f', 'phabricator-main-menu-view' => 'd00a795a', @@ -810,6 +808,8 @@ return array( 'phui-form-css' => '0b98e572', 'phui-form-view-css' => '4a1a0f5e', 'phui-header-view-css' => 'd53cc835', + 'phui-hovercard' => '1bd28176', + 'phui-hovercard-view-css' => '5684c081', 'phui-icon-set-selector-css' => '1ab67aad', 'phui-icon-view-css' => '3f33ab57', 'phui-image-mask-css' => '5a8b09c8', @@ -970,6 +970,13 @@ return array( 'javelin-dom', 'javelin-typeahead-normalizer', ), + '1bd28176' => array( + 'javelin-install', + 'javelin-dom', + 'javelin-vector', + 'javelin-request', + 'javelin-uri', + ), '1d45c74d' => array( 'javelin-behavior', 'javelin-dom', @@ -1312,13 +1319,6 @@ return array( 'javelin-request', 'javelin-workflow', ), - '66dd6e9e' => array( - 'javelin-behavior', - 'javelin-behavior-device', - 'javelin-stratcom', - 'javelin-vector', - 'phabricator-hovercard', - ), 68140031 => array( 'phui-workcard-view-css', ), @@ -1755,6 +1755,13 @@ return array( 'b6b0d1bb' => array( 'phui-inline-comment-view-css', ), + 'bcaccd64' => array( + 'javelin-behavior', + 'javelin-behavior-device', + 'javelin-stratcom', + 'javelin-vector', + 'phui-hovercard', + ), 'bd4c8dca' => array( 'javelin-install', 'javelin-util', @@ -1788,13 +1795,6 @@ return array( 'javelin-dom', 'javelin-vector', ), - 'c6f720ff' => array( - 'javelin-install', - 'javelin-dom', - 'javelin-vector', - 'javelin-request', - 'javelin-uri', - ), 'c72aa091' => array( 'javelin-behavior', 'javelin-dom', @@ -2220,8 +2220,8 @@ return array( 'phabricator-file-upload', 'javelin-behavior-global-drag-and-drop', 'javelin-behavior-phabricator-reveal-content', - 'phabricator-hovercard', - 'javelin-behavior-phabricator-hovercards', + 'phui-hovercard', + 'javelin-behavior-phui-hovercards', 'javelin-color', 'javelin-fx', 'phabricator-draggable-list', diff --git a/resources/celerity/packages.php b/resources/celerity/packages.php index 5a3e296a8c..2b0f8b9ff0 100644 --- a/resources/celerity/packages.php +++ b/resources/celerity/packages.php @@ -59,8 +59,8 @@ return array( 'phabricator-file-upload', 'javelin-behavior-global-drag-and-drop', 'javelin-behavior-phabricator-reveal-content', - 'phabricator-hovercard', - 'javelin-behavior-phabricator-hovercards', + 'phui-hovercard', + 'javelin-behavior-phui-hovercards', 'javelin-color', 'javelin-fx', 'phabricator-draggable-list', diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index b986242b04..b345f822d7 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -1493,6 +1493,8 @@ phutil_register_library_map(array( 'PHUIHandleTagListView' => 'applications/phid/view/PHUIHandleTagListView.php', 'PHUIHandleView' => 'applications/phid/view/PHUIHandleView.php', 'PHUIHeaderView' => 'view/phui/PHUIHeaderView.php', + 'PHUIHovercardUIExample' => 'applications/uiexample/examples/PHUIHovercardUIExample.php', + 'PHUIHovercardView' => 'view/phui/PHUIHovercardView.php', 'PHUIIconCircleView' => 'view/phui/PHUIIconCircleView.php', 'PHUIIconExample' => 'applications/uiexample/examples/PHUIIconExample.php', 'PHUIIconView' => 'view/phui/PHUIIconView.php', @@ -1585,6 +1587,7 @@ phutil_register_library_map(array( 'PasteSearchConduitAPIMethod' => 'applications/paste/conduit/PasteSearchConduitAPIMethod.php', 'PeopleBrowseUserDirectoryCapability' => 'applications/people/capability/PeopleBrowseUserDirectoryCapability.php', 'PeopleCreateUsersCapability' => 'applications/people/capability/PeopleCreateUsersCapability.php', + 'PeopleHovercardEngineExtension' => 'applications/people/engineextension/PeopleHovercardEngineExtension.php', 'PeopleUserLogGarbageCollector' => 'applications/people/garbagecollector/PeopleUserLogGarbageCollector.php', 'Phabricator404Controller' => 'applications/base/controller/Phabricator404Controller.php', 'PhabricatorAWSConfigOptions' => 'applications/config/option/PhabricatorAWSConfigOptions.php', @@ -2388,8 +2391,6 @@ phutil_register_library_map(array( 'PhabricatorHomeQuickCreateController' => 'applications/home/controller/PhabricatorHomeQuickCreateController.php', 'PhabricatorHovercardEngineExtension' => 'applications/search/engineextension/PhabricatorHovercardEngineExtension.php', 'PhabricatorHovercardEngineExtensionModule' => 'applications/search/engineextension/PhabricatorHovercardEngineExtensionModule.php', - 'PhabricatorHovercardUIExample' => 'applications/uiexample/examples/PhabricatorHovercardUIExample.php', - 'PhabricatorHovercardView' => 'view/widget/hovercard/PhabricatorHovercardView.php', 'PhabricatorHunksManagementMigrateWorkflow' => 'applications/differential/management/PhabricatorHunksManagementMigrateWorkflow.php', 'PhabricatorHunksManagementWorkflow' => 'applications/differential/management/PhabricatorHunksManagementWorkflow.php', 'PhabricatorIDsSearchEngineExtension' => 'applications/search/engineextension/PhabricatorIDsSearchEngineExtension.php', @@ -2735,7 +2736,6 @@ phutil_register_library_map(array( 'PhabricatorPeopleDisableController' => 'applications/people/controller/PhabricatorPeopleDisableController.php', 'PhabricatorPeopleEmpowerController' => 'applications/people/controller/PhabricatorPeopleEmpowerController.php', 'PhabricatorPeopleExternalPHIDType' => 'applications/people/phid/PhabricatorPeopleExternalPHIDType.php', - 'PhabricatorPeopleHovercardEngineExtension' => 'applications/people/engineextension/PhabricatorPeopleHovercardEngineExtension.php', 'PhabricatorPeopleIconSet' => 'applications/people/icon/PhabricatorPeopleIconSet.php', 'PhabricatorPeopleInviteController' => 'applications/people/controller/PhabricatorPeopleInviteController.php', 'PhabricatorPeopleInviteListController' => 'applications/people/controller/PhabricatorPeopleInviteListController.php', @@ -5669,6 +5669,8 @@ phutil_register_library_map(array( 'PHUIHandleTagListView' => 'AphrontTagView', 'PHUIHandleView' => 'AphrontView', 'PHUIHeaderView' => 'AphrontTagView', + 'PHUIHovercardUIExample' => 'PhabricatorUIExample', + 'PHUIHovercardView' => 'AphrontView', 'PHUIIconCircleView' => 'AphrontTagView', 'PHUIIconExample' => 'PhabricatorUIExample', 'PHUIIconView' => 'AphrontTagView', @@ -5770,6 +5772,7 @@ phutil_register_library_map(array( 'PasteSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod', 'PeopleBrowseUserDirectoryCapability' => 'PhabricatorPolicyCapability', 'PeopleCreateUsersCapability' => 'PhabricatorPolicyCapability', + 'PeopleHovercardEngineExtension' => 'PhabricatorHovercardEngineExtension', 'PeopleUserLogGarbageCollector' => 'PhabricatorGarbageCollector', 'Phabricator404Controller' => 'PhabricatorController', 'PhabricatorAWSConfigOptions' => 'PhabricatorApplicationConfigOptions', @@ -6717,8 +6720,6 @@ phutil_register_library_map(array( 'PhabricatorHomeQuickCreateController' => 'PhabricatorHomeController', 'PhabricatorHovercardEngineExtension' => 'Phobject', 'PhabricatorHovercardEngineExtensionModule' => 'PhabricatorConfigModule', - 'PhabricatorHovercardUIExample' => 'PhabricatorUIExample', - 'PhabricatorHovercardView' => 'AphrontView', 'PhabricatorHunksManagementMigrateWorkflow' => 'PhabricatorHunksManagementWorkflow', 'PhabricatorHunksManagementWorkflow' => 'PhabricatorManagementWorkflow', 'PhabricatorIDsSearchEngineExtension' => 'PhabricatorSearchEngineExtension', @@ -7109,7 +7110,6 @@ phutil_register_library_map(array( 'PhabricatorPeopleDisableController' => 'PhabricatorPeopleController', 'PhabricatorPeopleEmpowerController' => 'PhabricatorPeopleController', 'PhabricatorPeopleExternalPHIDType' => 'PhabricatorPHIDType', - 'PhabricatorPeopleHovercardEngineExtension' => 'PhabricatorHovercardEngineExtension', 'PhabricatorPeopleIconSet' => 'PhabricatorIconSet', 'PhabricatorPeopleInviteController' => 'PhabricatorPeopleController', 'PhabricatorPeopleInviteListController' => 'PhabricatorPeopleInviteController', diff --git a/src/applications/differential/engineextension/DifferentialHovercardEngineExtension.php b/src/applications/differential/engineextension/DifferentialHovercardEngineExtension.php index 325e99f860..d0bd5917dc 100644 --- a/src/applications/differential/engineextension/DifferentialHovercardEngineExtension.php +++ b/src/applications/differential/engineextension/DifferentialHovercardEngineExtension.php @@ -35,7 +35,7 @@ final class DifferentialHovercardEngineExtension } public function renderHovercard( - PhabricatorHovercardView $hovercard, + PHUIHovercardView $hovercard, PhabricatorObjectHandle $handle, $object, $data) { diff --git a/src/applications/diffusion/engineextension/DiffusionHovercardEngineExtension.php b/src/applications/diffusion/engineextension/DiffusionHovercardEngineExtension.php index 64caee879f..0711028796 100644 --- a/src/applications/diffusion/engineextension/DiffusionHovercardEngineExtension.php +++ b/src/applications/diffusion/engineextension/DiffusionHovercardEngineExtension.php @@ -19,7 +19,7 @@ final class DiffusionHovercardEngineExtension } public function renderHovercard( - PhabricatorHovercardView $hovercard, + PHUIHovercardView $hovercard, PhabricatorObjectHandle $handle, $commit, $data) { diff --git a/src/applications/maniphest/engineextension/ManiphestHovercardEngineExtension.php b/src/applications/maniphest/engineextension/ManiphestHovercardEngineExtension.php index 5215232a7d..c49c7b6201 100644 --- a/src/applications/maniphest/engineextension/ManiphestHovercardEngineExtension.php +++ b/src/applications/maniphest/engineextension/ManiphestHovercardEngineExtension.php @@ -19,7 +19,7 @@ final class ManiphestHovercardEngineExtension } public function renderHovercard( - PhabricatorHovercardView $hovercard, + PHUIHovercardView $hovercard, PhabricatorObjectHandle $handle, $task, $data) { diff --git a/src/applications/people/engineextension/PhabricatorPeopleHovercardEngineExtension.php b/src/applications/people/engineextension/PeopleHovercardEngineExtension.php similarity index 96% rename from src/applications/people/engineextension/PhabricatorPeopleHovercardEngineExtension.php rename to src/applications/people/engineextension/PeopleHovercardEngineExtension.php index 8ce8c25a49..e986ec87f7 100644 --- a/src/applications/people/engineextension/PhabricatorPeopleHovercardEngineExtension.php +++ b/src/applications/people/engineextension/PeopleHovercardEngineExtension.php @@ -1,6 +1,6 @@ 'hovercard', diff --git a/src/applications/search/controller/PhabricatorSearchHovercardController.php b/src/applications/search/controller/PhabricatorSearchHovercardController.php index 3f1e197996..2fecf80dfd 100644 --- a/src/applications/search/controller/PhabricatorSearchHovercardController.php +++ b/src/applications/search/controller/PhabricatorSearchHovercardController.php @@ -56,7 +56,7 @@ final class PhabricatorSearchHovercardController $handle = $handles[$phid]; $object = idx($objects, $phid); - $hovercard = id(new PhabricatorHovercardView()) + $hovercard = id(new PHUIHovercardView()) ->setUser($viewer) ->setObjectHandle($handle); diff --git a/src/applications/search/engineextension/PhabricatorHovercardEngineExtension.php b/src/applications/search/engineextension/PhabricatorHovercardEngineExtension.php index 79eb9b60a4..060441c985 100644 --- a/src/applications/search/engineextension/PhabricatorHovercardEngineExtension.php +++ b/src/applications/search/engineextension/PhabricatorHovercardEngineExtension.php @@ -32,7 +32,7 @@ abstract class PhabricatorHovercardEngineExtension extends Phobject { } abstract public function renderHovercard( - PhabricatorHovercardView $hovercard, + PHUIHovercardView $hovercard, PhabricatorObjectHandle $handle, $object, $data); diff --git a/src/applications/uiexample/examples/PhabricatorHovercardUIExample.php b/src/applications/uiexample/examples/PHUIHovercardUIExample.php similarity index 87% rename from src/applications/uiexample/examples/PhabricatorHovercardUIExample.php rename to src/applications/uiexample/examples/PHUIHovercardUIExample.php index 3b3fc323d8..8673441d09 100644 --- a/src/applications/uiexample/examples/PhabricatorHovercardUIExample.php +++ b/src/applications/uiexample/examples/PHUIHovercardUIExample.php @@ -1,6 +1,6 @@ createPanel(pht('Differential Hovercard')); - $panel->appendChild(id(new PhabricatorHovercardView()) + $panel->appendChild(id(new PHUIHovercardView()) ->setObjectHandle($diff_handle) ->addField(pht('Author'), $user->getUsername()) ->addField(pht('Updated'), phabricator_datetime(time(), $user)) @@ -41,7 +41,7 @@ final class PhabricatorHovercardUIExample extends PhabricatorUIExample { ->setType(PHUITagView::TYPE_STATE) ->setName(pht('Closed, Resolved')); $panel = $this->createPanel(pht('Maniphest Hovercard')); - $panel->appendChild(id(new PhabricatorHovercardView()) + $panel->appendChild(id(new PHUIHovercardView()) ->setObjectHandle($task_handle) ->setUser($user) ->addField(pht('Assigned to'), $user->getUsername()) @@ -66,7 +66,7 @@ final class PhabricatorHovercardUIExample extends PhabricatorUIExample { $user_handle->setImageURI( celerity_get_resource_uri('/rsrc/image/people/washington.png')); $panel = $this->createPanel(pht('Whatevery Hovercard')); - $panel->appendChild(id(new PhabricatorHovercardView()) + $panel->appendChild(id(new PHUIHovercardView()) ->setObjectHandle($user_handle) ->addField(pht('Status'), pht('Available')) ->addField(pht('Member since'), '30. February 1750') diff --git a/src/view/phui/PHUIFeedStoryView.php b/src/view/phui/PHUIFeedStoryView.php index 40b7c76290..bacd266089 100644 --- a/src/view/phui/PHUIFeedStoryView.php +++ b/src/view/phui/PHUIFeedStoryView.php @@ -157,7 +157,7 @@ final class PHUIFeedStoryView extends AphrontView { public function render() { require_celerity_resource('phui-feed-story-css'); - Javelin::initBehavior('phabricator-hovercards'); + Javelin::initBehavior('phui-hovercards'); $body = null; $foot = null; diff --git a/src/view/widget/hovercard/PhabricatorHovercardView.php b/src/view/phui/PHUIHovercardView.php similarity index 82% rename from src/view/widget/hovercard/PhabricatorHovercardView.php rename to src/view/phui/PHUIHovercardView.php index 0da3aa9a0a..4f8b8b295b 100644 --- a/src/view/widget/hovercard/PhabricatorHovercardView.php +++ b/src/view/phui/PHUIHovercardView.php @@ -4,7 +4,7 @@ * The default one-for-all hovercard. We may derive from this one to create * more specialized ones. */ -final class PhabricatorHovercardView extends AphrontView { +final class PHUIHovercardView extends AphrontView { /** * @var PhabricatorObjectHandle @@ -78,7 +78,7 @@ final class PhabricatorHovercardView extends AphrontView { $viewer = $this->getUser(); $handle = $this->handle; - require_celerity_resource('phabricator-hovercard-view-css'); + require_celerity_resource('phui-hovercard-view-css'); $title = array( id(new PHUISpacesNamespaceContextView()) @@ -107,7 +107,7 @@ final class PhabricatorHovercardView extends AphrontView { $body_title = $handle->getFullName(); } - $body[] = phutil_tag_div('phabricator-hovercard-body-header', $body_title); + $body[] = phutil_tag_div('phui-hovercard-body-header', $body_title); foreach ($this->fields as $field) { $item = array( @@ -115,7 +115,7 @@ final class PhabricatorHovercardView extends AphrontView { ': ', phutil_tag('span', array(), $field['value']), ); - $body[] = phutil_tag_div('phabricator-hovercard-body-item', $item); + $body[] = phutil_tag_div('phui-hovercard-body-item', $item); } if ($this->badges) { @@ -125,7 +125,7 @@ final class PhabricatorHovercardView extends AphrontView { $body[] = phutil_tag( 'div', array( - 'class' => 'phabricator-hovercard-body-item hovercard-badges', + 'class' => 'phui-hovercard-body-item hovercard-badges', ), $badges); } @@ -136,7 +136,7 @@ final class PhabricatorHovercardView extends AphrontView { $body = phutil_tag( 'div', array( - 'class' => 'phabricator-hovercard-body-image', + 'class' => 'phui-hovercard-body-image', ), phutil_tag( 'div', @@ -149,7 +149,7 @@ final class PhabricatorHovercardView extends AphrontView { phutil_tag( 'div', array( - 'class' => 'phabricator-hovercard-body-details', + 'class' => 'phui-hovercard-body-details', ), $body)); } @@ -178,18 +178,18 @@ final class PhabricatorHovercardView extends AphrontView { $tail = null; if ($buttons) { - $tail = phutil_tag_div('phabricator-hovercard-tail', $buttons); + $tail = phutil_tag_div('phui-hovercard-tail', $buttons); } $hovercard = phutil_tag_div( - 'phabricator-hovercard-container', + 'phui-hovercard-container', array( - phutil_tag_div('phabricator-hovercard-head', $header), - phutil_tag_div('phabricator-hovercard-body grouped', $body), + phutil_tag_div('phui-hovercard-head', $header), + phutil_tag_div('phui-hovercard-body grouped', $body), $tail, )); - return phutil_tag_div('phabricator-hovercard-wrapper', $hovercard); + return phutil_tag_div('phui-hovercard-wrapper', $hovercard); } } diff --git a/src/view/phui/PHUITagView.php b/src/view/phui/PHUITagView.php index 8b6e6aa8d6..ae80fc1731 100644 --- a/src/view/phui/PHUITagView.php +++ b/src/view/phui/PHUITagView.php @@ -122,7 +122,7 @@ final class PHUITagView extends AphrontTagView { } if ($this->phid) { - Javelin::initBehavior('phabricator-hovercards'); + Javelin::initBehavior('phui-hovercards'); $attributes = array( 'href' => $this->href, diff --git a/webroot/rsrc/css/layout/phabricator-hovercard-view.css b/webroot/rsrc/css/phui/phui-hovercard.css similarity index 63% rename from webroot/rsrc/css/layout/phabricator-hovercard-view.css rename to webroot/rsrc/css/phui/phui-hovercard.css index 23b3956763..82a7d3bb36 100644 --- a/webroot/rsrc/css/layout/phabricator-hovercard-view.css +++ b/webroot/rsrc/css/phui/phui-hovercard.css @@ -1,22 +1,22 @@ /** - * @provides phabricator-hovercard-view-css + * @provides phui-hovercard-view-css */ .jx-hovercard-container { position: absolute; } -.phabricator-hovercard-wrapper { +.phui-hovercard-wrapper { float: left; width: 400px; } -.device-phone .phabricator-hovercard-wrapper { +.device-phone .phui-hovercard-wrapper { float: left; width: 300px; } -.phabricator-hovercard-container { +.phui-hovercard-container { float: left; width: 100%; box-shadow: {$dropshadow}; @@ -25,29 +25,29 @@ background-color: #fff; } -.phabricator-hovercard-head .phui-header-shell { +.phui-hovercard-head .phui-header-shell { padding: 6px 8px 6px 12px; background-color: {$bluebackground}; border-top-left-radius: 3px; border-top-right-radius: 3px; } -.phabricator-hovercard-head .phui-header-header { +.phui-hovercard-head .phui-header-header { font-size: 14px; } -.phabricator-hovercard-head .phui-tag-type-state { +.phui-hovercard-head .phui-tag-type-state { color: {$darkbluetext}; text-shadow: none; font-weight: normal; } -.phabricator-hovercard-tags { +.phui-hovercard-tags { float: right; white-space: normal; } -.phabricator-hovercard-body { +.phui-hovercard-body { padding: 12px; color: {$darkgreytext}; border-bottom-right-radius: 3px; @@ -55,26 +55,26 @@ position: relative; } -.phabricator-hovercard-body-item { +.phui-hovercard-body-item { margin: 4px 0 0 0; } -.phabricator-hovercard-body-header { +.phui-hovercard-body-header { font-size: 14px; padding-bottom: 4px; color: {$darkgreytext}; line-height: 18px; } -.phabricator-hovercard-body .phabricator-hovercard-body-image { +.phui-hovercard-body .phui-hovercard-body-image { width: 58px; } -.phabricator-hovercard-body .phabricator-hovercard-body-details { +.phui-hovercard-body .phui-hovercard-body-details { margin-left: 58px; } -.phabricator-hovercard-body .profile-header-picture-frame { +.phui-hovercard-body .profile-header-picture-frame { float: left; width: 50px; height: 50px; @@ -91,7 +91,7 @@ float: left; } -.phabricator-hovercard-tail { +.phui-hovercard-tail { width: 396px; float: left; padding: 2px; @@ -100,7 +100,7 @@ border-bottom-right-radius: 3px; } -.phabricator-hovercard-tail button, -.phabricator-hovercard-tail a.button { +.phui-hovercard-tail button, +.phui-hovercard-tail a.button { margin: 3px; } diff --git a/webroot/rsrc/js/core/Hovercard.js b/webroot/rsrc/js/core/Hovercard.js index 26774d4f25..65e1455252 100644 --- a/webroot/rsrc/js/core/Hovercard.js +++ b/webroot/rsrc/js/core/Hovercard.js @@ -4,7 +4,7 @@ * javelin-vector * javelin-request * javelin-uri - * @provides phabricator-hovercard + * @provides phui-hovercard * @javelin */ diff --git a/webroot/rsrc/js/core/behavior-hovercard.js b/webroot/rsrc/js/core/behavior-hovercard.js index 26e62ad02a..8a1c610752 100644 --- a/webroot/rsrc/js/core/behavior-hovercard.js +++ b/webroot/rsrc/js/core/behavior-hovercard.js @@ -1,14 +1,14 @@ /** - * @provides javelin-behavior-phabricator-hovercards + * @provides javelin-behavior-phui-hovercards * @requires javelin-behavior * javelin-behavior-device * javelin-stratcom * javelin-vector - * phabricator-hovercard + * phui-hovercard * @javelin */ -JX.behavior('phabricator-hovercards', function() { +JX.behavior('phui-hovercards', function() { // We listen for mousemove instead of mouseover to handle the case when user // scrolls with keyboard. We don't want to display hovercard if node gets From 68254a046ff05593297f616161b7a2f0b28a7c07 Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 3 Feb 2016 14:44:19 -0800 Subject: [PATCH 30/54] Fix mishandling of chunk threshold in Diffusion for installs with no chunk engines available Summary: Fixes T10273. The threshold is `null` if no chunk engines are available, but the code didn't handle this properly. Test Plan: Disabled all chunk engines, reloaded, hit issue described in task. Applied patch, got clean file content. Reviewers: chad Reviewed By: chad Maniphest Tasks: T10273 Differential Revision: https://secure.phabricator.com/D15179 --- .../PhabricatorFileUploadSource.php | 30 +++++++++++-------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/src/applications/files/uploadsource/PhabricatorFileUploadSource.php b/src/applications/files/uploadsource/PhabricatorFileUploadSource.php index 74b8e32389..086fd3345e 100644 --- a/src/applications/files/uploadsource/PhabricatorFileUploadSource.php +++ b/src/applications/files/uploadsource/PhabricatorFileUploadSource.php @@ -92,21 +92,27 @@ abstract class PhabricatorFileUploadSource $threshold = PhabricatorFileStorageEngine::getChunkThreshold(); - // If we don't know how large the file is, we're going to read some data - // from it until we know whether it's a small file or not. This will give - // us enough information to make a decision about chunking. - $length = $this->getDataLength(); - if ($length === null) { - $rope = $this->getRope(); - while ($this->readFileData()) { - $length = $rope->getByteLength(); - if ($length > $threshold) { - break; + if ($threshold === null) { + // If there are no chunk engines available, we clearly can't chunk the + // file. + $this->shouldChunk = false; + } else { + // If we don't know how large the file is, we're going to read some data + // from it until we know whether it's a small file or not. This will give + // us enough information to make a decision about chunking. + $length = $this->getDataLength(); + if ($length === null) { + $rope = $this->getRope(); + while ($this->readFileData()) { + $length = $rope->getByteLength(); + if ($length > $threshold) { + break; + } } } - } - $this->shouldChunk = ($length > $threshold); + $this->shouldChunk = ($length > $threshold); + } return $this->shouldChunk; } From 0a735694ae1e15f61bc65c23824409f46663eeca Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 3 Feb 2016 14:48:54 -0800 Subject: [PATCH 31/54] Give project tags hovercards Summary: I don't think these ever had hovercards, but they should with subprojects/new design. Test Plan: pointey pointey, got a card Reviewers: chad Reviewed By: chad Differential Revision: https://secure.phabricator.com/D15180 --- src/applications/project/remarkup/ProjectRemarkupRule.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/applications/project/remarkup/ProjectRemarkupRule.php b/src/applications/project/remarkup/ProjectRemarkupRule.php index 43253e5b67..70d6b8eda3 100644 --- a/src/applications/project/remarkup/ProjectRemarkupRule.php +++ b/src/applications/project/remarkup/ProjectRemarkupRule.php @@ -16,7 +16,9 @@ final class ProjectRemarkupRule extends PhabricatorObjectRemarkupRule { return '#'.$id; } - return $handle->renderTag(); + $tag = $handle->renderTag(); + $tag->setPHID($handle->getPHID()); + return $tag; } protected function getObjectIDPattern() { From 23b835b64731ce0842955591554af17482be146a Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 3 Feb 2016 09:39:17 -0800 Subject: [PATCH 32/54] Begin lifting column layout logic out of ColumnPositionQuery Summary: Ref T10010. This is a precursor to D15171, which I'll eventually rebuild on top of these changes. Currently, ColumnPositionQuery does a lot of "column layout" stuff that's very similar to the Milestone/Subproject stuff that needs to happen in D15171. The current approach there ended up splitting this layout stuff across two unrelated classes (ColumnPositionQuery + BoardViewController), neither of which is a particularly great place to do it -- the Query is too low-level, and the Controller is too high-level. Instead, introduce a new "LayoutEngine" which does all this layout stuff. Swap two of the four places that we query this stuff over to the new engine: - "Project (Column)" on tasks. - Transaction generation when moving cards. These sites aren't swapped by this diff, but will be by the next one: - Actually applying transactions. - Main layout for boards (this could swap easily now, but applying transactions currently relies on position writes having taken place, so it can't swap until the other one swaps). Once everything is swapped over, I should be able to add the D15171 logic to LayoutEngine instead of BoardViewController and end up with a cleaner approach overall. One particularly benefit is that //looking// at a board won't do a bunch of position writes anymore, which wasn't a big deal, but which I was a bit uneasy with. Test Plan: - Viewed tasks that are on boards, saw column annotations in project list. - Moved cards between columns on a board. Reviewers: chad Reviewed By: chad Maniphest Tasks: T10010 Differential Revision: https://secure.phabricator.com/D15174 --- src/__phutil_library_map__.php | 2 + .../PhabricatorProjectMoveController.php | 15 +- .../engine/PhabricatorBoardLayoutEngine.php | 187 ++++++++++++++++++ .../PhabricatorProjectUIEventListener.php | 39 ++-- .../PhabricatorProjectColumnPosition.php | 4 + 5 files changed, 218 insertions(+), 29 deletions(-) create mode 100644 src/applications/project/engine/PhabricatorBoardLayoutEngine.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index b345f822d7..e499fcc68a 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -1814,6 +1814,7 @@ phutil_register_library_map(array( 'PhabricatorBcryptPasswordHasher' => 'infrastructure/util/password/PhabricatorBcryptPasswordHasher.php', 'PhabricatorBinariesSetupCheck' => 'applications/config/check/PhabricatorBinariesSetupCheck.php', 'PhabricatorBitbucketAuthProvider' => 'applications/auth/provider/PhabricatorBitbucketAuthProvider.php', + 'PhabricatorBoardLayoutEngine' => 'applications/project/engine/PhabricatorBoardLayoutEngine.php', 'PhabricatorBot' => 'infrastructure/daemon/bot/PhabricatorBot.php', 'PhabricatorBotChannel' => 'infrastructure/daemon/bot/target/PhabricatorBotChannel.php', 'PhabricatorBotDebugLogHandler' => 'infrastructure/daemon/bot/handler/PhabricatorBotDebugLogHandler.php', @@ -6038,6 +6039,7 @@ phutil_register_library_map(array( 'PhabricatorBcryptPasswordHasher' => 'PhabricatorPasswordHasher', 'PhabricatorBinariesSetupCheck' => 'PhabricatorSetupCheck', 'PhabricatorBitbucketAuthProvider' => 'PhabricatorOAuth1AuthProvider', + 'PhabricatorBoardLayoutEngine' => 'Phobject', 'PhabricatorBot' => 'PhabricatorDaemon', 'PhabricatorBotChannel' => 'PhabricatorBotTarget', 'PhabricatorBotDebugLogHandler' => 'PhabricatorBotHandler', diff --git a/src/applications/project/controller/PhabricatorProjectMoveController.php b/src/applications/project/controller/PhabricatorProjectMoveController.php index 3edcd01f65..3695ad21ac 100644 --- a/src/applications/project/controller/PhabricatorProjectMoveController.php +++ b/src/applications/project/controller/PhabricatorProjectMoveController.php @@ -26,6 +26,8 @@ final class PhabricatorProjectMoveController return new Aphront404Response(); } + $board_phid = $project->getPHID(); + $object = id(new ManiphestTaskQuery()) ->setViewer($viewer) ->withPHIDs(array($object_phid)) @@ -54,11 +56,14 @@ final class PhabricatorProjectMoveController return new Aphront404Response(); } - $positions = id(new PhabricatorProjectColumnPositionQuery()) + $engine = id(new PhabricatorBoardLayoutEngine()) ->setViewer($viewer) - ->withColumns($columns) - ->withObjectPHIDs(array($object_phid)) - ->execute(); + ->setBoardPHIDs(array($board_phid)) + ->setObjectPHIDs(array($object_phid)) + ->executeLayout(); + + $columns = $engine->getObjectColumns($board_phid, $object_phid); + $old_column_phids = mpull($columns, 'getPHID'); $xactions = array(); @@ -80,7 +85,7 @@ final class PhabricatorProjectMoveController ) + $order_params) ->setOldValue( array( - 'columnPHIDs' => mpull($positions, 'getColumnPHID'), + 'columnPHIDs' => $old_column_phids, 'projectPHID' => $column->getProjectPHID(), )); diff --git a/src/applications/project/engine/PhabricatorBoardLayoutEngine.php b/src/applications/project/engine/PhabricatorBoardLayoutEngine.php new file mode 100644 index 0000000000..b5b60c44e4 --- /dev/null +++ b/src/applications/project/engine/PhabricatorBoardLayoutEngine.php @@ -0,0 +1,187 @@ +viewer = $viewer; + return $this; + } + + public function getViewer() { + return $this->viewer; + } + + public function setBoardPHIDs(array $board_phids) { + $this->boardPHIDs = $board_phids; + return $this; + } + + public function getBoardPHIDs() { + return $this->boardPHIDs; + } + + public function setObjectPHIDs(array $object_phids) { + $this->objectPHIDs = $object_phids; + return $this; + } + + public function getObjectPHIDs() { + return $this->objectPHIDs; + } + + public function executeLayout() { + $viewer = $this->getViewer(); + + $boards = $this->loadBoards(); + if (!$boards) { + return $this; + } + + $columns = $this->loadColumns($boards); + $positions = $this->loadPositions($boards); + + foreach ($boards as $board_phid => $board) { + $board_columns = idx($columns, $board_phid); + + // Don't layout boards with no columns. These boards need to be formally + // created first. + if (!$columns) { + continue; + } + + $board_positions = idx($positions, $board_phid, array()); + + $this->layoutBoard($board, $board_columns, $board_positions); + } + + return $this; + } + + public function getObjectColumns($board_phid, $object_phid) { + $board_map = idx($this->objectColumnMap, $board_phid, array()); + + $column_phids = idx($board_map, $object_phid); + if (!$column_phids) { + return array(); + } + + return array_select_keys($this->columnMap, $column_phids); + } + + private function loadBoards() { + $viewer = $this->getViewer(); + $board_phids = $this->getBoardPHIDs(); + + $boards = id(new PhabricatorObjectQuery()) + ->setViewer($viewer) + ->withPHIDs($board_phids) + ->execute(); + $boards = mpull($boards, null, 'getPHID'); + + foreach ($boards as $key => $board) { + if (!$board->getHasWorkboard()) { + unset($boards[$key]); + } + } + + return $boards; + } + + private function loadColumns(array $boards) { + $viewer = $this->getViewer(); + + $columns = id(new PhabricatorProjectColumnQuery()) + ->setViewer($viewer) + ->withProjectPHIDs(array_keys($boards)) + ->execute(); + $columns = msort($columns, 'getSequence'); + $columns = mpull($columns, null, 'getPHID'); + + $this->columnMap = $columns; + $columns = mgroup($columns, 'getProjectPHID'); + + return $columns; + } + + private function loadPositions(array $boards) { + $viewer = $this->getViewer(); + + $positions = id(new PhabricatorProjectColumnPositionQuery()) + ->setViewer($viewer) + ->withBoardPHIDs(array_keys($boards)) + ->withObjectPHIDs($this->getObjectPHIDs()) + ->execute(); + $positions = msort($positions, 'getOrderingKey'); + $positions = mgroup($positions, 'getBoardPHID'); + + return $positions; + } + + private function layoutBoard( + $board, + array $columns, + array $positions) { + + $board_phid = $board->getPHID(); + $position_groups = mgroup($positions, 'getObjectPHID'); + + foreach ($columns as $column) { + if ($column->isDefaultColumn()) { + $default_phid = $column->getPHID(); + break; + } + } + + $layout = array(); + + $object_phids = $this->getObjectPHIDs(); + foreach ($object_phids as $object_phid) { + $positions = idx($position_groups, $object_phid, array()); + + // Remove any positions in columns which no longer exist. + foreach ($positions as $key => $position) { + $column_phid = $position->getColumnPHID(); + if (empty($columns[$column_phid])) { + unset($positions[$key]); + } + } + + // If the object has no position, put it on the default column. + if (!$positions) { + $new_position = id(new PhabricatorProjectColumnPosition()) + ->setBoardPHID($board_phid) + ->setColumnPHID($default_phid) + ->setObjectPHID($object_phid) + ->setSequence(0); + $positions = array( + $new_position, + ); + } + + foreach ($positions as $position) { + $column_phid = $position->getColumnPHID(); + $layout[$column_phid][$object_phid] = $position; + } + } + + foreach ($layout as $column_phid => $map) { + $map = msort($map, 'getOrderingKey'); + $layout[$column_phid] = $map; + + foreach ($map as $object_phid => $position) { + $this->objectColumnMap[$board_phid][$object_phid][] = $column_phid; + } + } + + $this->boardLayout[$board_phid] = $layout; + } + +} diff --git a/src/applications/project/events/PhabricatorProjectUIEventListener.php b/src/applications/project/events/PhabricatorProjectUIEventListener.php index 55a4a2c4a0..c85c036f51 100644 --- a/src/applications/project/events/PhabricatorProjectUIEventListener.php +++ b/src/applications/project/events/PhabricatorProjectUIEventListener.php @@ -49,37 +49,24 @@ final class PhabricatorProjectUIEventListener $annotations = array(); if ($handles && $can_appear_on_boards) { + $engine = id(new PhabricatorBoardLayoutEngine()) + ->setViewer($user) + ->setBoardPHIDs($project_phids) + ->setObjectPHIDs(array($object->getPHID())) + ->executeLayout(); // TDOO: Generalize this UI and move it out of Maniphest. - require_celerity_resource('maniphest-task-summary-css'); - $positions_query = id(new PhabricatorProjectColumnPositionQuery()) - ->setViewer($user) - ->withBoardPHIDs($project_phids) - ->withObjectPHIDs(array($object->getPHID())) - ->needColumns(true); - - // This is important because positions will be created "on demand" - // based on the set of columns. If we don't specify it, positions - // won't be created. - $columns = id(new PhabricatorProjectColumnQuery()) - ->setViewer($user) - ->withProjectPHIDs($project_phids) - ->execute(); - if ($columns) { - $positions_query->withColumns($columns); - } - $positions = $positions_query->execute(); - $positions = mpull($positions, null, 'getBoardPHID'); - foreach ($project_phids as $project_phid) { $handle = $handles[$project_phid]; - $position = idx($positions, $project_phid); - if ($position) { - $column = $position->getColumn(); + $columns = $engine->getObjectColumns( + $project_phid, + $object->getPHID()); + $annotation = array(); + foreach ($columns as $column) { $column_name = pht('(%s)', $column->getDisplayName()); $column_link = phutil_tag( 'a', @@ -89,9 +76,13 @@ final class PhabricatorProjectUIEventListener ), $column_name); + $annotation[] = $column_link; + } + + if ($annotation) { $annotations[$project_phid] = array( ' ', - $column_link, + phutil_implode_html(', ', $annotation), ); } } diff --git a/src/applications/project/storage/PhabricatorProjectColumnPosition.php b/src/applications/project/storage/PhabricatorProjectColumnPosition.php index 7abcd7e372..9815db18e5 100644 --- a/src/applications/project/storage/PhabricatorProjectColumnPosition.php +++ b/src/applications/project/storage/PhabricatorProjectColumnPosition.php @@ -41,6 +41,10 @@ final class PhabricatorProjectColumnPosition extends PhabricatorProjectDAO } public function getOrderingKey() { + if (!$this->getID()) { + return 0; + } + // Low sequence numbers go above high sequence numbers. // High position IDs go above low position IDs. // Broadly, this makes newly added stuff float to the top. From a9e98e42f5ff65a3a71182ba3b30f502d0d40981 Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 3 Feb 2016 10:58:15 -0800 Subject: [PATCH 33/54] Continue lifting column layout logic out of ColumnPositionQuery Summary: Ref T10010. See D15174. This gets rid of the "actually apply the change" callsite and moves it to layout engine. Next up is to make the board view use the layout engine, then throw away all the whack code in ColumnPositionQuery, then move forward with D15171. Test Plan: - Dragged tasks within a column. - Dragged tasks between columns. - Dragged tasks to empty columns. - Created a task in a column. - Swapped board to priority sort, dragged a bunch of stuff all over. Reviewers: chad Reviewed By: chad Maniphest Tasks: T10010 Differential Revision: https://secure.phabricator.com/D15175 --- .../editor/ManiphestTransactionEditor.php | 177 ++++++-------- .../engine/PhabricatorBoardLayoutEngine.php | 215 +++++++++++++++++- .../PhabricatorProjectColumnPosition.php | 2 +- 3 files changed, 280 insertions(+), 114 deletions(-) diff --git a/src/applications/maniphest/editor/ManiphestTransactionEditor.php b/src/applications/maniphest/editor/ManiphestTransactionEditor.php index 72979d3a4e..bc8b5d3d31 100644 --- a/src/applications/maniphest/editor/ManiphestTransactionEditor.php +++ b/src/applications/maniphest/editor/ManiphestTransactionEditor.php @@ -200,18 +200,6 @@ final class ManiphestTransactionEditor 'columnPHIDs')); } - $columns = id(new PhabricatorProjectColumnQuery()) - ->setViewer($this->requireActor()) - ->withPHIDs($new_phids) - ->execute(); - $columns = mpull($columns, null, 'getPHID'); - - $positions = id(new PhabricatorProjectColumnPositionQuery()) - ->setViewer($this->requireActor()) - ->withObjectPHIDs(array($object->getPHID())) - ->withBoardPHIDs(array($board_phid)) - ->execute(); - $before_phid = idx($xaction->getNewValue(), 'beforePHID'); $after_phid = idx($xaction->getNewValue(), 'afterPHID'); @@ -227,111 +215,76 @@ final class ManiphestTransactionEditor // object's position in the "natural" ordering, so we do need to update // some rows. + $object_phid = $object->getPHID(); + + // We're doing layout with the ominpotent viewer to make sure we don't + // remove positions in columns that exist, but which the actual actor + // can't see. + $omnipotent_viewer = PhabricatorUser::getOmnipotentUser(); + + $board_tasks = id(new ManiphestTaskQuery()) + ->setViewer($omnipotent_viewer) + ->withEdgeLogicPHIDs( + PhabricatorProjectObjectHasProjectEdgeType::EDGECONST, + PhabricatorQueryConstraint::OPERATOR_AND, + array($board_phid)) + ->execute(); + + $object_phids = mpull($board_tasks, 'getPHID'); + $object_phids[] = $object_phid; + + $engine = id(new PhabricatorBoardLayoutEngine()) + ->setViewer($omnipotent_viewer) + ->setBoardPHIDs(array($board_phid)) + ->setObjectPHIDs($object_phids) + ->executeLayout(); + + // TODO: This logic needs to be revised if we legitimately support + // multiple column positions. + + // NOTE: When a task is newly created, it's implicitly added to the + // backlog but we don't currently record that in the "$old_phids". Just + // clean it up for now. + $columns = $engine->getObjectColumns($board_phid, $object_phid); + foreach ($columns as $column) { + $engine->queueRemovePosition( + $board_phid, + $column->getPHID(), + $object_phid); + } + // Remove all existing column positions on the board. - - foreach ($positions as $position) { - $position->delete(); + foreach ($old_phids as $column_phid) { + $engine->queueRemovePosition( + $board_phid, + $column_phid, + $object_phid); } - // Add the new column positions. - - foreach ($new_phids as $phid) { - $column = idx($columns, $phid); - if (!$column) { - throw new Exception( - pht('No such column "%s" exists!', $phid)); - } - - // Load the other object positions in the column. Note that we must - // skip implicit column creation to avoid generating a new position - // if the target column is a backlog column. - - $other_positions = id(new PhabricatorProjectColumnPositionQuery()) - ->setViewer($this->requireActor()) - ->withColumns(array($column)) - ->withBoardPHIDs(array($board_phid)) - ->setSkipImplicitCreate(true) - ->execute(); - $other_positions = msort($other_positions, 'getOrderingKey'); - - // Set up the new position object. We're going to figure out the - // right sequence number and then persist this object with that - // sequence number. - $new_position = id(new PhabricatorProjectColumnPosition()) - ->setBoardPHID($board_phid) - ->setColumnPHID($column->getPHID()) - ->setObjectPHID($object->getPHID()); - - $updates = array(); - $sequence = 0; - - // If we're just dropping this into the column without any specific - // position information, put it at the top. - if (!$before_phid && !$after_phid) { - $new_position->setSequence($sequence)->save(); - $sequence++; - } - - foreach ($other_positions as $position) { - $object_phid = $position->getObjectPHID(); - - // If this is the object we're moving before and we haven't - // saved yet, insert here. - if (($before_phid == $object_phid) && !$new_position->getID()) { - $new_position->setSequence($sequence)->save(); - $sequence++; - } - - // This object goes here in the sequence; we might need to update - // the row. - if ($sequence != $position->getSequence()) { - $updates[$position->getID()] = $sequence; - } - $sequence++; - - // If this is the object we're moving after and we haven't saved - // yet, insert here. - if (($after_phid == $object_phid) && !$new_position->getID()) { - $new_position->setSequence($sequence)->save(); - $sequence++; - } - } - - // We should have found a place to put it. - if (!$new_position->getID()) { - throw new Exception( - pht('Unable to find a place to insert object on column!')); - } - - // If we changed other objects' column positions, bulk reorder them. - - if ($updates) { - $position = new PhabricatorProjectColumnPosition(); - $conn_w = $position->establishConnection('w'); - - $pairs = array(); - foreach ($updates as $id => $sequence) { - // This is ugly because MySQL gets upset with us if it is - // configured strictly and we attempt inserts which can't work. - // We'll never actually do these inserts since they'll always - // collide (triggering the ON DUPLICATE KEY logic), so we just - // provide dummy values in order to get there. - - $pairs[] = qsprintf( - $conn_w, - '(%d, %d, "", "", "")', - $id, - $sequence); - } - - queryfx( - $conn_w, - 'INSERT INTO %T (id, sequence, boardPHID, columnPHID, objectPHID) - VALUES %Q ON DUPLICATE KEY UPDATE sequence = VALUES(sequence)', - $position->getTableName(), - implode(', ', $pairs)); + // Add new positions. + foreach ($new_phids as $column_phid) { + if ($before_phid) { + $engine->queueAddPositionBefore( + $board_phid, + $column_phid, + $object_phid, + $before_phid); + } else if ($after_phid) { + $engine->queueAddPositionAfter( + $board_phid, + $column_phid, + $object_phid, + $after_phid); + } else { + $engine->queueAddPosition( + $board_phid, + $column_phid, + $object_phid); } } + + $engine->applyPositionUpdates(); + break; default: break; diff --git a/src/applications/project/engine/PhabricatorBoardLayoutEngine.php b/src/applications/project/engine/PhabricatorBoardLayoutEngine.php index b5b60c44e4..28cecd81ea 100644 --- a/src/applications/project/engine/PhabricatorBoardLayoutEngine.php +++ b/src/applications/project/engine/PhabricatorBoardLayoutEngine.php @@ -10,6 +10,9 @@ final class PhabricatorBoardLayoutEngine extends Phobject { private $objectColumnMap = array(); private $boardLayout = array(); + private $remQueue = array(); + private $addQueue = array(); + public function setViewer(PhabricatorUser $viewer) { $this->viewer = $viewer; return $this; @@ -76,6 +79,207 @@ final class PhabricatorBoardLayoutEngine extends Phobject { return array_select_keys($this->columnMap, $column_phids); } + public function queueRemovePosition( + $board_phid, + $column_phid, + $object_phid) { + + $board_layout = idx($this->boardLayout, $board_phid, array()); + $positions = idx($board_layout, $column_phid, array()); + $position = idx($positions, $object_phid); + + if ($position) { + $this->remQueue[] = $position; + + // If this position hasn't been saved yet, get it out of the add queue. + if (!$position->getID()) { + foreach ($this->addQueue as $key => $add_position) { + if ($add_position === $position) { + unset($this->addQueue[$key]); + } + } + } + } + + unset($this->boardLayout[$board_phid][$column_phid][$object_phid]); + + return $this; + } + + public function queueAddPositionBefore( + $board_phid, + $column_phid, + $object_phid, + $before_phid) { + + return $this->queueAddPositionRelative( + $board_phid, + $column_phid, + $object_phid, + $before_phid, + true); + } + + public function queueAddPositionAfter( + $board_phid, + $column_phid, + $object_phid, + $after_phid) { + + return $this->queueAddPositionRelative( + $board_phid, + $column_phid, + $object_phid, + $after_phid, + false); + } + + public function queueAddPosition( + $board_phid, + $column_phid, + $object_phid) { + return $this->queueAddPositionRelative( + $board_phid, + $column_phid, + $object_phid, + null, + true); + } + + private function queueAddPositionRelative( + $board_phid, + $column_phid, + $object_phid, + $relative_phid, + $is_before) { + + $board_layout = idx($this->boardLayout, $board_phid, array()); + $positions = idx($board_layout, $column_phid, array()); + + // Check if the object is already in the column, and remove it if it is. + $object_position = idx($positions, $object_phid); + unset($positions[$object_phid]); + + if (!$object_position) { + $object_position = id(new PhabricatorProjectColumnPosition()) + ->setBoardPHID($board_phid) + ->setColumnPHID($column_phid) + ->setObjectPHID($object_phid); + } + + $found = false; + if (!$positions) { + $object_position->setSequence(0); + } else { + foreach ($positions as $position) { + if (!$found) { + if ($relative_phid === null) { + $is_match = true; + } else { + $position_phid = $position->getObjectPHID(); + $is_match = ($relative_phid == $position_phid); + } + + if ($is_match) { + $found = true; + + $sequence = $position->getSequence(); + + if (!$is_before) { + $sequence++; + } + + $object_position->setSequence($sequence++); + + if (!$is_before) { + // If we're inserting after this position, continue the loop so + // we don't update it. + continue; + } + } + } + + if ($found) { + $position->setSequence($sequence++); + $this->addQueue[] = $position; + } + } + } + + if ($relative_phid && !$found) { + throw new Exception( + pht( + 'Unable to find object "%s" in column "%s" on board "%s".', + $relative_phid, + $column_phid, + $board_phid)); + } + + $this->addQueue[] = $object_position; + + $positions[$object_phid] = $object_position; + $positions = msort($positions, 'getOrderingKey'); + + $this->boardLayout[$board_phid][$column_phid] = $positions; + + return $this; + } + + public function applyPositionUpdates() { + foreach ($this->remQueue as $position) { + if ($position->getID()) { + $position->delete(); + } + } + $this->remQueue = array(); + + $adds = array(); + $updates = array(); + + foreach ($this->addQueue as $position) { + $id = $position->getID(); + if ($id) { + $updates[$id] = $position; + } else { + $adds[] = $position; + } + } + $this->addQueue = array(); + + $table = new PhabricatorProjectColumnPosition(); + $conn_w = $table->establishConnection('w'); + + $pairs = array(); + foreach ($updates as $id => $position) { + // This is ugly because MySQL gets upset with us if it is configured + // strictly and we attempt inserts which can't work. We'll never actually + // do these inserts since they'll always collide (triggering the ON + // DUPLICATE KEY logic), so we just provide dummy values in order to get + // there. + + $pairs[] = qsprintf( + $conn_w, + '(%d, %d, "", "", "")', + $id, + $position->getSequence()); + } + + if ($pairs) { + queryfx( + $conn_w, + 'INSERT INTO %T (id, sequence, boardPHID, columnPHID, objectPHID) + VALUES %Q ON DUPLICATE KEY UPDATE sequence = VALUES(sequence)', + $table->getTableName(), + implode(', ', $pairs)); + } + + foreach ($adds as $position) { + $position->save(); + } + + return $this; + } + private function loadBoards() { $viewer = $this->getViewer(); $board_phids = $this->getBoardPHIDs(); @@ -114,10 +318,15 @@ final class PhabricatorBoardLayoutEngine extends Phobject { private function loadPositions(array $boards) { $viewer = $this->getViewer(); + $object_phids = $this->getObjectPHIDs(); + if (!$object_phids) { + return array(); + } + $positions = id(new PhabricatorProjectColumnPositionQuery()) ->setViewer($viewer) ->withBoardPHIDs(array_keys($boards)) - ->withObjectPHIDs($this->getObjectPHIDs()) + ->withObjectPHIDs($object_phids) ->execute(); $positions = msort($positions, 'getOrderingKey'); $positions = mgroup($positions, 'getBoardPHID'); @@ -150,6 +359,7 @@ final class PhabricatorBoardLayoutEngine extends Phobject { foreach ($positions as $key => $position) { $column_phid = $position->getColumnPHID(); if (empty($columns[$column_phid])) { + $this->remQueue[] = $position; unset($positions[$key]); } } @@ -161,6 +371,9 @@ final class PhabricatorBoardLayoutEngine extends Phobject { ->setColumnPHID($default_phid) ->setObjectPHID($object_phid) ->setSequence(0); + + $this->addQueue[] = $new_position; + $positions = array( $new_position, ); diff --git a/src/applications/project/storage/PhabricatorProjectColumnPosition.php b/src/applications/project/storage/PhabricatorProjectColumnPosition.php index 9815db18e5..c59676f8e4 100644 --- a/src/applications/project/storage/PhabricatorProjectColumnPosition.php +++ b/src/applications/project/storage/PhabricatorProjectColumnPosition.php @@ -41,7 +41,7 @@ final class PhabricatorProjectColumnPosition extends PhabricatorProjectDAO } public function getOrderingKey() { - if (!$this->getID()) { + if (!$this->getID() && !$this->getSequence()) { return 0; } From e25a40236fe29afe6ffcbbdb138bc45b545526f6 Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 3 Feb 2016 11:58:14 -0800 Subject: [PATCH 34/54] Nearly complete lifting card-move code out of workboards Summary: Ref T10010. This gets rid of the last dependency on the weird ColumnPositionQuery code. Test Plan: - Viewed workboards. - Used batch editor. - Created a new workboard. - Dragged stuff around. - Created new tasks into columns. - Changed order from natural to priority, dragged things around. - Switched filter to custom filter, "all tasks", etc. Reviewers: chad Reviewed By: chad Maniphest Tasks: T10010 Differential Revision: https://secure.phabricator.com/D15176 --- .../PhabricatorProjectBoardViewController.php | 251 ++++++++---------- .../engine/PhabricatorBoardLayoutEngine.php | 22 +- 2 files changed, 125 insertions(+), 148 deletions(-) diff --git a/src/applications/project/controller/PhabricatorProjectBoardViewController.php b/src/applications/project/controller/PhabricatorProjectBoardViewController.php index 06669aa2be..d2fdf8d763 100644 --- a/src/applications/project/controller/PhabricatorProjectBoardViewController.php +++ b/src/applications/project/controller/PhabricatorProjectBoardViewController.php @@ -28,10 +28,93 @@ final class PhabricatorProjectBoardViewController $project = $this->getProject(); $this->readRequestState(); - $columns = $this->loadColumns($project); - // TODO: Expand the checks here if we add the ability - // to hide the Backlog column + $board_uri = $this->getApplicationURI('board/'.$project->getID().'/'); + + $search_engine = id(new ManiphestTaskSearchEngine()) + ->setViewer($viewer) + ->setBaseURI($board_uri) + ->setIsBoardView(true); + + if ($request->isFormPost() && !$request->getBool('initialize')) { + $saved = $search_engine->buildSavedQueryFromRequest($request); + $search_engine->saveQuery($saved); + $filter_form = id(new AphrontFormView()) + ->setUser($viewer); + $search_engine->buildSearchForm($filter_form, $saved); + if ($search_engine->getErrors()) { + return $this->newDialog() + ->setWidth(AphrontDialogView::WIDTH_FULL) + ->setTitle(pht('Advanced Filter')) + ->appendChild($filter_form->buildLayoutView()) + ->setErrors($search_engine->getErrors()) + ->setSubmitURI($board_uri) + ->addSubmitButton(pht('Apply Filter')) + ->addCancelButton($board_uri); + } + return id(new AphrontRedirectResponse())->setURI( + $this->getURIWithState( + $search_engine->getQueryResultsPageURI($saved->getQueryKey()))); + } + + $query_key = $request->getURIData('queryKey'); + if (!$query_key) { + $query_key = 'open'; + } + $this->queryKey = $query_key; + + $custom_query = null; + if ($search_engine->isBuiltinQuery($query_key)) { + $saved = $search_engine->buildSavedQueryFromBuiltin($query_key); + } else { + $saved = id(new PhabricatorSavedQueryQuery()) + ->setViewer($viewer) + ->withQueryKeys(array($query_key)) + ->executeOne(); + + if (!$saved) { + return new Aphront404Response(); + } + + $custom_query = $saved; + } + + if ($request->getURIData('filter')) { + $filter_form = id(new AphrontFormView()) + ->setUser($viewer); + $search_engine->buildSearchForm($filter_form, $saved); + + return $this->newDialog() + ->setWidth(AphrontDialogView::WIDTH_FULL) + ->setTitle(pht('Advanced Filter')) + ->appendChild($filter_form->buildLayoutView()) + ->setSubmitURI($board_uri) + ->addSubmitButton(pht('Apply Filter')) + ->addCancelButton($board_uri); + } + + $task_query = $search_engine->buildQueryFromSavedQuery($saved); + + $tasks = $task_query + ->withEdgeLogicPHIDs( + PhabricatorProjectObjectHasProjectEdgeType::EDGECONST, + PhabricatorQueryConstraint::OPERATOR_AND, + array($project->getPHID())) + ->setOrder(ManiphestTaskQuery::ORDER_PRIORITY) + ->setViewer($viewer) + ->execute(); + $tasks = mpull($tasks, null, 'getPHID'); + + + $board_phid = $project->getPHID(); + + $layout_engine = id(new PhabricatorBoardLayoutEngine()) + ->setViewer($viewer) + ->setBoardPHIDs(array($board_phid)) + ->setObjectPHIDs(array_keys($tasks)) + ->executeLayout(); + + $columns = $layout_engine->getColumns($board_phid); if (!$columns) { $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, @@ -64,124 +147,6 @@ final class PhabricatorProjectBoardViewController ->appendChild($content); } - $board_uri = $this->getApplicationURI('board/'.$project->getID().'/'); - - $engine = id(new ManiphestTaskSearchEngine()) - ->setViewer($viewer) - ->setBaseURI($board_uri) - ->setIsBoardView(true); - - if ($request->isFormPost()) { - $saved = $engine->buildSavedQueryFromRequest($request); - $engine->saveQuery($saved); - $filter_form = id(new AphrontFormView()) - ->setUser($viewer); - $engine->buildSearchForm($filter_form, $saved); - if ($engine->getErrors()) { - return $this->newDialog() - ->setWidth(AphrontDialogView::WIDTH_FULL) - ->setTitle(pht('Advanced Filter')) - ->appendChild($filter_form->buildLayoutView()) - ->setErrors($engine->getErrors()) - ->setSubmitURI($board_uri) - ->addSubmitButton(pht('Apply Filter')) - ->addCancelButton($board_uri); - } - return id(new AphrontRedirectResponse())->setURI( - $this->getURIWithState( - $engine->getQueryResultsPageURI($saved->getQueryKey()))); - } - - $query_key = $request->getURIData('queryKey'); - if (!$query_key) { - $query_key = 'open'; - } - $this->queryKey = $query_key; - - $custom_query = null; - if ($engine->isBuiltinQuery($query_key)) { - $saved = $engine->buildSavedQueryFromBuiltin($query_key); - } else { - $saved = id(new PhabricatorSavedQueryQuery()) - ->setViewer($viewer) - ->withQueryKeys(array($query_key)) - ->executeOne(); - - if (!$saved) { - return new Aphront404Response(); - } - - $custom_query = $saved; - } - - if ($request->getURIData('filter')) { - $filter_form = id(new AphrontFormView()) - ->setUser($viewer); - $engine->buildSearchForm($filter_form, $saved); - - return $this->newDialog() - ->setWidth(AphrontDialogView::WIDTH_FULL) - ->setTitle(pht('Advanced Filter')) - ->appendChild($filter_form->buildLayoutView()) - ->setSubmitURI($board_uri) - ->addSubmitButton(pht('Apply Filter')) - ->addCancelButton($board_uri); - } - - $task_query = $engine->buildQueryFromSavedQuery($saved); - - $tasks = $task_query - ->withEdgeLogicPHIDs( - PhabricatorProjectObjectHasProjectEdgeType::EDGECONST, - PhabricatorQueryConstraint::OPERATOR_AND, - array($project->getPHID())) - ->setOrder(ManiphestTaskQuery::ORDER_PRIORITY) - ->setViewer($viewer) - ->execute(); - $tasks = mpull($tasks, null, 'getPHID'); - - if ($tasks) { - $positions = id(new PhabricatorProjectColumnPositionQuery()) - ->setViewer($viewer) - ->withObjectPHIDs(mpull($tasks, 'getPHID')) - ->withColumns($columns) - ->execute(); - $positions = mpull($positions, null, 'getObjectPHID'); - } else { - $positions = array(); - } - - $task_map = array(); - foreach ($tasks as $task) { - $task_phid = $task->getPHID(); - if (empty($positions[$task_phid])) { - // This shouldn't normally be possible because we create positions on - // demand, but we might have raced as an object was removed from the - // board. Just drop the task if we don't have a position for it. - continue; - } - - $position = $positions[$task_phid]; - $task_map[$position->getColumnPHID()][] = $task_phid; - } - - // If we're showing the board in "natural" order, sort columns by their - // column positions. - if ($this->sortKey == PhabricatorProjectColumn::ORDER_NATURAL) { - foreach ($task_map as $column_phid => $task_phids) { - $order = array(); - foreach ($task_phids as $task_phid) { - if (isset($positions[$task_phid])) { - $order[$task_phid] = $positions[$task_phid]->getOrderingKey(); - } else { - $order[$task_phid] = 0; - } - } - asort($order); - $task_map[$column_phid] = array_keys($order); - } - } - $task_can_edit_map = id(new PhabricatorPolicyFilter()) ->setViewer($viewer) ->requireCapabilities(array(PhabricatorPolicyCapability::CAN_EDIT)) @@ -198,7 +163,10 @@ final class PhabricatorProjectBoardViewController return new Aphront404Response(); } - $batch_task_phids = idx($task_map, $batch_column->getPHID(), array()); + $batch_task_phids = $layout_engine->getColumnObjectPHIDs( + $board_phid, + $batch_column->getPHID()); + foreach ($batch_task_phids as $key => $batch_task_phid) { if (empty($task_can_edit_map[$batch_task_phid])) { unset($batch_task_phids[$key]); @@ -251,9 +219,24 @@ final class PhabricatorProjectBoardViewController $this->handles = ManiphestTaskListView::loadTaskHandles($viewer, $tasks); foreach ($columns as $column) { - $task_phids = idx($task_map, $column->getPHID(), array()); + if (!$this->showHidden) { + if ($column->isHidden()) { + continue; + } + } + + $task_phids = $layout_engine->getColumnObjectPHIDs( + $board_phid, + $column->getPHID()); + $column_tasks = array_select_keys($tasks, $task_phids); + // If we aren't using "natural" order, reorder the column by the original + // query order. + if ($this->sortKey != PhabricatorProjectColumn::ORDER_NATURAL) { + $column_tasks = array_select_keys($column_tasks, array_keys($tasks)); + } + $panel = id(new PHUIWorkpanelView()) ->setHeader($column->getDisplayName()) ->setSubHeader($column->getDisplayType()) @@ -322,7 +305,7 @@ final class PhabricatorProjectBoardViewController $filter_menu = $this->buildFilterMenu( $viewer, $custom_query, - $engine, + $search_engine, $query_key); $manage_menu = $this->buildManageMenu($project, $this->showHidden); @@ -383,25 +366,6 @@ final class PhabricatorProjectBoardViewController $this->sortKey = $sort_key; } - private function loadColumns(PhabricatorProject $project) { - $viewer = $this->getViewer(); - - $column_query = id(new PhabricatorProjectColumnQuery()) - ->setViewer($viewer) - ->withProjectPHIDs(array($project->getPHID())); - - if (!$this->showHidden) { - $column_query->withStatuses( - array(PhabricatorProjectColumn::STATUS_ACTIVE)); - } - - $columns = $column_query->execute(); - $columns = mpull($columns, null, 'getSequence'); - ksort($columns); - - return $columns; - } - private function buildSortMenu( PhabricatorUser $viewer, $sort_key) { @@ -797,6 +761,7 @@ final class PhabricatorProjectBoardViewController $form = id(new AphrontFormView()) ->setUser($viewer) + ->addHiddenInput('initialize', 1) ->appendRemarkupInstructions( pht('The workboard for this project has not been created yet.')) ->appendControl($new_selector) diff --git a/src/applications/project/engine/PhabricatorBoardLayoutEngine.php b/src/applications/project/engine/PhabricatorBoardLayoutEngine.php index 28cecd81ea..8a6143c81d 100644 --- a/src/applications/project/engine/PhabricatorBoardLayoutEngine.php +++ b/src/applications/project/engine/PhabricatorBoardLayoutEngine.php @@ -6,7 +6,7 @@ final class PhabricatorBoardLayoutEngine extends Phobject { private $boardPHIDs; private $objectPHIDs; private $boards; - private $columnMap; + private $columnMap = array(); private $objectColumnMap = array(); private $boardLayout = array(); @@ -68,6 +68,17 @@ final class PhabricatorBoardLayoutEngine extends Phobject { return $this; } + public function getColumns($board_phid) { + $columns = idx($this->boardLayout, $board_phid, array()); + return array_select_keys($this->columnMap, array_keys($columns)); + } + + public function getColumnObjectPHIDs($board_phid, $column_phid) { + $columns = idx($this->boardLayout, $board_phid, array()); + $positions = idx($columns, $column_phid, array()); + return mpull($positions, 'getObjectPHID'); + } + public function getObjectColumns($board_phid, $object_phid) { $board_map = idx($this->objectColumnMap, $board_phid, array()); @@ -342,15 +353,16 @@ final class PhabricatorBoardLayoutEngine extends Phobject { $board_phid = $board->getPHID(); $position_groups = mgroup($positions, 'getObjectPHID'); + $layout = array(); foreach ($columns as $column) { + $column_phid = $column->getPHID(); + $layout[$column_phid] = array(); + if ($column->isDefaultColumn()) { - $default_phid = $column->getPHID(); - break; + $default_phid = $column_phid; } } - $layout = array(); - $object_phids = $this->getObjectPHIDs(); foreach ($object_phids as $object_phid) { $positions = idx($position_groups, $object_phid, array()); From 9961de0e809aa5359f73bf39a50ffd7b64d4ba23 Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 3 Feb 2016 12:12:49 -0800 Subject: [PATCH 35/54] Remove old position-on-read board column code Summary: Ref T10010. This retires the old way of doing things inside ColumnPositionQuery. It is now obsolete and lives in BoardLayoutEngine instead. Test Plan: - Moved cards, created cards, swapped filters, orders, etc. - Some degree of unit testing coming in the next diff. Reviewers: chad Reviewed By: chad Maniphest Tasks: T10010 Differential Revision: https://secure.phabricator.com/D15177 --- .../maniphest/editor/ManiphestEditEngine.php | 3 +- .../PhabricatorProjectColumnPositionQuery.php | 255 +----------------- 2 files changed, 15 insertions(+), 243 deletions(-) diff --git a/src/applications/maniphest/editor/ManiphestEditEngine.php b/src/applications/maniphest/editor/ManiphestEditEngine.php index e338450aa9..6133838975 100644 --- a/src/applications/maniphest/editor/ManiphestEditEngine.php +++ b/src/applications/maniphest/editor/ManiphestEditEngine.php @@ -287,7 +287,8 @@ final class ManiphestEditEngine $positions = id(new PhabricatorProjectColumnPositionQuery()) ->setViewer($viewer) - ->withColumns(array($column)) + ->withBoardPHIDs(array($column->getProjectPHID())) + ->withColumnPHIDs(array($column->getPHID())) ->execute(); $task_phids = mpull($positions, 'getObjectPHID'); diff --git a/src/applications/project/query/PhabricatorProjectColumnPositionQuery.php b/src/applications/project/query/PhabricatorProjectColumnPositionQuery.php index 3348b4054d..438c558e6e 100644 --- a/src/applications/project/query/PhabricatorProjectColumnPositionQuery.php +++ b/src/applications/project/query/PhabricatorProjectColumnPositionQuery.php @@ -6,10 +6,7 @@ final class PhabricatorProjectColumnPositionQuery private $ids; private $boardPHIDs; private $objectPHIDs; - private $columns; - - private $needColumns; - private $skipImplicitCreate; + private $columnPHIDs; public function withIDs(array $ids) { $this->ids = $ids; @@ -26,277 +23,51 @@ final class PhabricatorProjectColumnPositionQuery return $this; } - /** - * Find objects in specific columns. - * - * NOTE: Using this method activates logic which constructs virtual - * column positions for objects not in any column, if you pass a default - * column. Normally these results are not returned. - * - * @param list Columns to look for objects in. - * @return this - */ - public function withColumns(array $columns) { - assert_instances_of($columns, 'PhabricatorProjectColumn'); - $this->columns = $columns; + public function withColumnPHIDs(array $column_phids) { + $this->columnPHIDs = $column_phids; return $this; } - public function needColumns($need_columns) { - $this->needColumns = true; - return $this; - } - - - /** - * Skip implicit creation of column positions which are implied but do not - * yet exist. - * - * This is primarily useful internally. - * - * @param bool True to skip implicit creation of column positions. - * @return this - */ - public function setSkipImplicitCreate($skip) { - $this->skipImplicitCreate = $skip; - return $this; - } - - // NOTE: For now, boards are always attached to projects. However, they might - // not be in the future. This generalization just anticipates a future where - // we let other types of objects (like users) have boards, or let boards - // contain other types of objects. - - private function newPositionObject() { + public function newResultObject() { return new PhabricatorProjectColumnPosition(); } - private function newColumnQuery() { - return new PhabricatorProjectColumnQuery(); - } - - private function getBoardMembershipEdgeTypes() { - return array( - PhabricatorProjectProjectHasObjectEdgeType::EDGECONST, - ); - } - - private function getBoardMembershipPHIDTypes() { - return array( - ManiphestTaskPHIDType::TYPECONST, - ); - } - protected function loadPage() { - $table = $this->newPositionObject(); - $conn_r = $table->establishConnection('r'); - - // We're going to find results by combining two queries: one query finds - // objects on a board column, while the other query finds objects not on - // any board column and virtually puts them on the default column. - - $unions = array(); - - // First, find all the stuff that's actually on a column. - - $unions[] = qsprintf( - $conn_r, - 'SELECT * FROM %T %Q', - $table->getTableName(), - $this->buildWhereClause($conn_r)); - - // If we have a default column, find all the stuff that's not in any - // column and put it in the default column. - - $must_type_filter = false; - if ($this->columns && !$this->skipImplicitCreate) { - $default_map = array(); - foreach ($this->columns as $column) { - if ($column->isDefaultColumn()) { - $default_map[$column->getProjectPHID()] = $column->getPHID(); - } - } - - if ($default_map) { - $where = array(); - - // Find the edges attached to the boards we have default columns for. - - $where[] = qsprintf( - $conn_r, - 'e.src IN (%Ls)', - array_keys($default_map)); - - // Find only edges which describe a board relationship. - - $where[] = qsprintf( - $conn_r, - 'e.type IN (%Ld)', - $this->getBoardMembershipEdgeTypes()); - - if ($this->boardPHIDs !== null) { - // This should normally be redundant, but construct it anyway if - // the caller has told us to. - $where[] = qsprintf( - $conn_r, - 'e.src IN (%Ls)', - $this->boardPHIDs); - } - - if ($this->objectPHIDs !== null) { - $where[] = qsprintf( - $conn_r, - 'e.dst IN (%Ls)', - $this->objectPHIDs); - } - - $where[] = qsprintf( - $conn_r, - 'p.id IS NULL'); - - $where = $this->formatWhereClause($where); - - $unions[] = qsprintf( - $conn_r, - 'SELECT NULL id, e.src boardPHID, NULL columnPHID, e.dst objectPHID, - 0 sequence - FROM %T e LEFT JOIN %T p - ON e.src = p.boardPHID AND e.dst = p.objectPHID - %Q', - PhabricatorEdgeConfig::TABLE_NAME_EDGE, - $table->getTableName(), - $where); - - $must_type_filter = true; - } - } - - $data = queryfx_all( - $conn_r, - '%Q %Q %Q', - implode(' UNION ALL ', $unions), - $this->buildOrderClause($conn_r), - $this->buildLimitClause($conn_r)); - - // If we've picked up objects not in any column, we need to filter out any - // matched objects which have the wrong edge type. - if ($must_type_filter) { - $allowed_types = array_fuse($this->getBoardMembershipPHIDTypes()); - foreach ($data as $id => $row) { - if ($row['columnPHID'] === null) { - $object_phid = $row['objectPHID']; - if (empty($allowed_types[phid_get_type($object_phid)])) { - unset($data[$id]); - } - } - } - } - - $positions = $table->loadAllFromArray($data); - - // Find the implied positions which don't exist yet. If there are any, - // we're going to create them. - $create = array(); - foreach ($positions as $position) { - if ($position->getColumnPHID() === null) { - $column_phid = idx($default_map, $position->getBoardPHID()); - $position->setColumnPHID($column_phid); - - $create[] = $position; - } - } - - if ($create) { - // If we're adding several objects to a column, insert the column - // position objects in object ID order. This means that newly added - // objects float to the top, and when a group of newly added objects - // float up at the same time, the most recently created ones end up - // highest in the list. - - $objects = id(new PhabricatorObjectQuery()) - ->setViewer(PhabricatorUser::getOmnipotentUser()) - ->withPHIDs(mpull($create, 'getObjectPHID')) - ->execute(); - $objects = mpull($objects, null, 'getPHID'); - $objects = msort($objects, 'getID'); - - $create = mgroup($create, 'getObjectPHID'); - $create = array_select_keys($create, array_keys($objects)) + $create; - - $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); - - foreach ($create as $object_phid => $create_positions) { - foreach ($create_positions as $create_position) { - $create_position->save(); - } - } - - unset($unguarded); - } - - return $positions; + return $this->loadStandardPage($this->newResultObject()); } - protected function willFilterPage(array $page) { - - if ($this->needColumns) { - $column_phids = mpull($page, 'getColumnPHID'); - $columns = $this->newColumnQuery() - ->setParentQuery($this) - ->setViewer($this->getViewer()) - ->withPHIDs($column_phids) - ->execute(); - $columns = mpull($columns, null, 'getPHID'); - - foreach ($page as $key => $position) { - $column = idx($columns, $position->getColumnPHID()); - if (!$column) { - unset($page[$key]); - continue; - } - - $position->attachColumn($column); - } - } - - return $page; - } - - protected function buildWhereClause(AphrontDatabaseConnection $conn_r) { + protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) { $where = array(); if ($this->ids !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'id IN (%Ld)', $this->ids); } if ($this->boardPHIDs !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'boardPHID IN (%Ls)', $this->boardPHIDs); } if ($this->objectPHIDs !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'objectPHID IN (%Ls)', $this->objectPHIDs); } - if ($this->columns !== null) { + if ($this->columnPHIDs !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'columnPHID IN (%Ls)', - mpull($this->columns, 'getPHID')); + $this->columnPHIDs); } - // NOTE: Explicitly not building the paging clause here, since it won't - // work with the UNION. - - return $this->formatWhereClause($where); + return $where; } public function getQueryApplicationClass() { From 00165424d0d42c647c505c58311d14744ffdece3 Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 3 Feb 2016 12:26:22 -0800 Subject: [PATCH 36/54] Add some test coverage for board moves Summary: Ref T10010. This isn't totally comprehensive, and a lot of behaviors aren't testable (e.g., all the Javascript stuff) but at least covers the basic create/move/reorder operations. Test Plan: `arc unit` Reviewers: chad Reviewed By: chad Maniphest Tasks: T10010 Differential Revision: https://secure.phabricator.com/D15178 --- .../PhabricatorProjectCoreTestCase.php | 159 ++++++++++++++++++ .../engine/PhabricatorBoardLayoutEngine.php | 4 +- 2 files changed, 161 insertions(+), 2 deletions(-) diff --git a/src/applications/project/__tests__/PhabricatorProjectCoreTestCase.php b/src/applications/project/__tests__/PhabricatorProjectCoreTestCase.php index 6c0f05ebbb..74f637f16d 100644 --- a/src/applications/project/__tests__/PhabricatorProjectCoreTestCase.php +++ b/src/applications/project/__tests__/PhabricatorProjectCoreTestCase.php @@ -906,6 +906,165 @@ final class PhabricatorProjectCoreTestCase extends PhabricatorTestCase { $this->getTaskProjects($task)); } + public function testBoardMoves() { + $user = $this->createUser(); + $user->save(); + + $board = $this->createProject($user); + + $backlog = $this->addColumn($user, $board, 0); + $column = $this->addColumn($user, $board, 1); + + // New tasks should appear in the backlog. + $task1 = $this->newTask($user, array($board)); + $expect = array( + $backlog->getPHID(), + ); + $this->assertColumns($expect, $user, $board, $task1); + + // Moving a task should move it to the destination column. + $this->moveToColumn($user, $board, $task1, $backlog, $column); + $expect = array( + $column->getPHID(), + ); + $this->assertColumns($expect, $user, $board, $task1); + + // Same thing again, with a new task. + $task2 = $this->newTask($user, array($board)); + $expect = array( + $backlog->getPHID(), + ); + $this->assertColumns($expect, $user, $board, $task2); + + // Move it, too. + $this->moveToColumn($user, $board, $task2, $backlog, $column); + $expect = array( + $column->getPHID(), + ); + $this->assertColumns($expect, $user, $board, $task2); + + // Now the stuff should be in the column, in order, with the more recently + // moved task on top. + $expect = array( + $task2->getPHID(), + $task1->getPHID(), + ); + $this->assertTasksInColumn($expect, $user, $board, $column); + + // Move the second task after the first task. + $options = array( + 'afterPHID' => $task1->getPHID(), + ); + $this->moveToColumn($user, $board, $task2, $column, $column, $options); + $expect = array( + $task1->getPHID(), + $task2->getPHID(), + ); + $this->assertTasksInColumn($expect, $user, $board, $column); + + // Move the second task before the first task. + $options = array( + 'beforePHID' => $task1->getPHID(), + ); + $this->moveToColumn($user, $board, $task2, $column, $column, $options); + $expect = array( + $task2->getPHID(), + $task1->getPHID(), + ); + $this->assertTasksInColumn($expect, $user, $board, $column); + + } + + private function moveToColumn( + PhabricatorUser $viewer, + PhabricatorProject $board, + ManiphestTask $task, + PhabricatorProjectColumn $src, + PhabricatorProjectColumn $dst, + $options = null) { + + $xactions = array(); + + if (!$options) { + $options = array(); + } + + $xactions[] = id(new ManiphestTransaction()) + ->setTransactionType(ManiphestTransaction::TYPE_PROJECT_COLUMN) + ->setOldValue( + array( + 'projectPHID' => $board->getPHID(), + 'columnPHIDs' => array($src->getPHID()), + )) + ->setNewValue( + array( + 'projectPHID' => $board->getPHID(), + 'columnPHIDs' => array($dst->getPHID()), + ) + $options); + + $editor = id(new ManiphestTransactionEditor()) + ->setActor($viewer) + ->setContentSource(PhabricatorContentSource::newConsoleSource()) + ->setContinueOnNoEffect(true) + ->applyTransactions($task, $xactions); + } + + private function assertColumns( + array $expect, + PhabricatorUser $viewer, + PhabricatorProject $board, + ManiphestTask $task) { + + $engine = id(new PhabricatorBoardLayoutEngine()) + ->setViewer($viewer) + ->setBoardPHIDs(array($board->getPHID())) + ->setObjectPHIDs( + array( + $task->getPHID(), + )) + ->executeLayout(); + + $columns = $engine->getObjectColumns($board->getPHID(), $task->getPHID()); + $column_phids = mpull($columns, 'getPHID'); + $column_phids = array_values($column_phids); + + $this->assertEqual($expect, $column_phids); + } + + private function assertTasksInColumn( + array $expect, + PhabricatorUser $viewer, + PhabricatorProject $board, + PhabricatorProjectColumn $column) { + + $engine = id(new PhabricatorBoardLayoutEngine()) + ->setViewer($viewer) + ->setBoardPHIDs(array($board->getPHID())) + ->setObjectPHIDs($expect) + ->executeLayout(); + + $object_phids = $engine->getColumnObjectPHIDs( + $board->getPHID(), + $column->getPHID()); + $object_phids = array_values($object_phids); + + $this->assertEqual($expect, $object_phids); + } + + private function addColumn( + PhabricatorUser $viewer, + PhabricatorProject $project, + $sequence) { + + $project->setHasWorkboard(1)->save(); + + return PhabricatorProjectColumn::initializeNewColumn($viewer) + ->setSequence(0) + ->setProperty('isDefault', ($sequence == 0)) + ->setProjectPHID($project->getPHID()) + ->save(); + } + private function getTaskProjects(ManiphestTask $task) { $project_phids = PhabricatorEdgeQuery::loadDestinationPHIDs( $task->getPHID(), diff --git a/src/applications/project/engine/PhabricatorBoardLayoutEngine.php b/src/applications/project/engine/PhabricatorBoardLayoutEngine.php index 8a6143c81d..e578131c83 100644 --- a/src/applications/project/engine/PhabricatorBoardLayoutEngine.php +++ b/src/applications/project/engine/PhabricatorBoardLayoutEngine.php @@ -23,7 +23,7 @@ final class PhabricatorBoardLayoutEngine extends Phobject { } public function setBoardPHIDs(array $board_phids) { - $this->boardPHIDs = $board_phids; + $this->boardPHIDs = array_fuse($board_phids); return $this; } @@ -32,7 +32,7 @@ final class PhabricatorBoardLayoutEngine extends Phobject { } public function setObjectPHIDs(array $object_phids) { - $this->objectPHIDs = $object_phids; + $this->objectPHIDs = array_fuse($object_phids); return $this; } From 90a0459821ea3c7aea270a98cccfba5908e8c430 Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 2 Feb 2016 09:53:18 -0800 Subject: [PATCH 37/54] Roughly implement milestone columns on workboards Summary: Ref T10010. These aren't perfect but I think (?) they aren't horribly broken. - When a project is a parent project, destroy (as far as the user can tell) any custom columns. - When a project has milestones, automatically generate columns on the project's workboard (if it has a workboard). - When you move tasks between milestones, add the proper milestone tag. - When you move tasks out of milestones back into the backlog, add the proper parent project tag. - (Plenty of UI / design stuff to adjust.) Test Plan: - Dragged stuff between milestone columns. - Used a normal workboard. - Wasn't able to find any egregiously bad cases that did anything terrible. {F1088224} Reviewers: chad Reviewed By: chad Maniphest Tasks: T10010 Differential Revision: https://secure.phabricator.com/D15171 --- resources/builtin/image-200x200.png | Bin 0 -> 1261 bytes resources/celerity/map.php | 22 +- .../autopatches/20160202.board.1.proxy.sql | 2 + src/__phutil_library_map__.php | 2 + .../maniphest/editor/ManiphestEditEngine.php | 19 +- .../editor/ManiphestTransactionEditor.php | 14 +- .../PhabricatorProjectCoreTestCase.php | 57 ++++- .../PhabricatorProjectBoardViewController.php | 39 +++- .../PhabricatorProjectMoveController.php | 42 +++- ...atorProjectSubprojectWarningController.php | 2 +- .../engine/PhabricatorBoardLayoutEngine.php | 204 ++++++++++++++++-- .../PhabricatorColumnProxyInterface.php | 7 + .../query/PhabricatorProjectColumnQuery.php | 62 ++++++ .../project/storage/PhabricatorProject.php | 24 ++- .../storage/PhabricatorProjectColumn.php | 37 +++- src/docs/user/userguide/projects.diviner | 9 - src/view/phui/PHUIWorkpanelView.php | 4 +- .../projects/behavior-project-boards.js | 7 +- 18 files changed, 496 insertions(+), 57 deletions(-) create mode 100644 resources/builtin/image-200x200.png create mode 100644 resources/sql/autopatches/20160202.board.1.proxy.sql create mode 100644 src/applications/project/interface/PhabricatorColumnProxyInterface.php diff --git a/resources/builtin/image-200x200.png b/resources/builtin/image-200x200.png new file mode 100644 index 0000000000000000000000000000000000000000..53bc1e785c395c4c5882c1aec3bfc9eac13d4159 GIT binary patch literal 1261 zcmeAS@N?(olHy`uVBq!ia0vp^CqS5k2}mkgS)K$^k|nMYCBgY=CFO}lsSJ)O`AMk? zp1FzXsX?iUDV2pMQ*9U+So%F(978H@y}9l0EnO*b;NvP`ZAXzFZmwIQH=1vJekW)1 zev>-?KEC&_E>7OppL)Hy>Z@!AyLgUw=I#s5N9`4Qm6|R}{eR}w}If+ExGS@ z=H})f?G`Uy{PD-r?hPwUC$^pa^{eXewo8oPyXUE?sa-mFzi7{fsAq1=(kd!`>{2@_ zIC1G;{(Uv+>CdD2gu}GkrcU3oe}8>RiOB5Q+FEC4=b!oSC$-JlP;;jsJwj0|T3uZ| z*7T!_Tj1XpyS#lY5;<2FMhLpIgy>CA-aECZQ@y>toj+{qB%x^=-tC^V^lJIOOJ0jt z+s+*X=n{!AGf#ivszff#c7?YYufD# zJvM6{l9HBw{qm*d+D`S2lGee|A9w8BIrFNFjLebHPuHYQv-7^=lJ;NYp}sET@#Dva z>n@$!W|e<`UoZ2euZCvHvGxWA57q@l@UPz(6!Eol$={DVcJ11=cdzXBuAkHGrKP2- ztE+>9gMS+OE%r`)y!+6fG=u5yEC2til?wm4%g(Fn`@2}HW5;>b!X6iv{(q`d7p)

A!*r2F`cXrOC8Pk)l%RG%2HdoS4J>-UDQX=!P{e*aE(t&bD-PcklZ^U*ZzE*Dg}eKKkbkFz zKh86oK7D#}-qY^2KydP;=kmY^-a}p&?-aXs{{_mtoZL|{iO>7^@oe3DtyeA9NZ4G> z`ugwiqUF=|FCBPRVxNnl(00^Y2~Oy7xlpTJG2EWyjCjJP(N2-dyqP`0Ra#BG(>4J`SVZxGn>?;qc^mtdbLg4Fi(_guKoW{uRne)3>Lc^IqmD$sq^gL z3x%!L7VFlJ`(t5YabwNXsF11kZbP0l+XkKkFP_? literal 0 HcmV?d00001 diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 69819d4bd9..efe626ca21 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -413,7 +413,7 @@ return array( 'rsrc/js/application/phortune/phortune-credit-card-form.js' => '2290aeef', 'rsrc/js/application/policy/behavior-policy-control.js' => 'ae45872f', 'rsrc/js/application/policy/behavior-policy-rule-editor.js' => '5e9f347c', - 'rsrc/js/application/projects/behavior-project-boards.js' => 'c05fb42a', + 'rsrc/js/application/projects/behavior-project-boards.js' => '48470f95', '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', @@ -653,7 +653,7 @@ return array( 'javelin-behavior-phui-profile-menu' => '12884df9', 'javelin-behavior-policy-control' => 'ae45872f', 'javelin-behavior-policy-rule-editor' => '5e9f347c', - 'javelin-behavior-project-boards' => 'c05fb42a', + 'javelin-behavior-project-boards' => '48470f95', 'javelin-behavior-project-create' => '065227cc', 'javelin-behavior-quicksand-blacklist' => '7927a7d3', 'javelin-behavior-recurring-edit' => '5f1c4d5f', @@ -1151,6 +1151,15 @@ 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', @@ -1779,15 +1788,6 @@ return array( 'javelin-install', 'javelin-dom', ), - 'c05fb42a' => array( - 'javelin-behavior', - 'javelin-dom', - 'javelin-util', - 'javelin-vector', - 'javelin-stratcom', - 'javelin-workflow', - 'phabricator-draggable-list', - ), 'c1700f6f' => array( 'javelin-install', 'javelin-util', diff --git a/resources/sql/autopatches/20160202.board.1.proxy.sql b/resources/sql/autopatches/20160202.board.1.proxy.sql new file mode 100644 index 0000000000..a3e5965f26 --- /dev/null +++ b/resources/sql/autopatches/20160202.board.1.proxy.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_project.project_column + ADD proxyPHID VARBINARY(64); diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index e499fcc68a..9910ad1349 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -1889,6 +1889,7 @@ phutil_register_library_map(array( 'PhabricatorChatLogQuery' => 'applications/chatlog/query/PhabricatorChatLogQuery.php', 'PhabricatorChunkedFileStorageEngine' => 'applications/files/engine/PhabricatorChunkedFileStorageEngine.php', 'PhabricatorClusterConfigOptions' => 'applications/config/option/PhabricatorClusterConfigOptions.php', + 'PhabricatorColumnProxyInterface' => 'applications/project/interface/PhabricatorColumnProxyInterface.php', 'PhabricatorCommentEditEngineExtension' => 'applications/transactions/engineextension/PhabricatorCommentEditEngineExtension.php', 'PhabricatorCommentEditField' => 'applications/transactions/editfield/PhabricatorCommentEditField.php', 'PhabricatorCommentEditType' => 'applications/transactions/edittype/PhabricatorCommentEditType.php', @@ -7262,6 +7263,7 @@ phutil_register_library_map(array( 'PhabricatorDestructibleInterface', 'PhabricatorFulltextInterface', 'PhabricatorConduitResultInterface', + 'PhabricatorColumnProxyInterface', ), 'PhabricatorProjectAddHeraldAction' => 'PhabricatorProjectHeraldAction', 'PhabricatorProjectApplication' => 'PhabricatorApplication', diff --git a/src/applications/maniphest/editor/ManiphestEditEngine.php b/src/applications/maniphest/editor/ManiphestEditEngine.php index 6133838975..abbdd4c2e1 100644 --- a/src/applications/maniphest/editor/ManiphestEditEngine.php +++ b/src/applications/maniphest/editor/ManiphestEditEngine.php @@ -280,10 +280,23 @@ final class ManiphestEditEngine return new Aphront404Response(); } - // If the workboard's project has been removed from the card's project - // list, we are going to remove it from the board completely. + // 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. + + $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(); + $project_map = array_fuse($task->getProjectPHIDs()); - $remove_card = empty($project_map[$column->getProjectPHID()]); + $remove_card = !array_intersect_key($board_phids, $project_map); $positions = id(new PhabricatorProjectColumnPositionQuery()) ->setViewer($viewer) diff --git a/src/applications/maniphest/editor/ManiphestTransactionEditor.php b/src/applications/maniphest/editor/ManiphestTransactionEditor.php index bc8b5d3d31..46e2a617e8 100644 --- a/src/applications/maniphest/editor/ManiphestTransactionEditor.php +++ b/src/applications/maniphest/editor/ManiphestTransactionEditor.php @@ -222,12 +222,22 @@ final class ManiphestTransactionEditor // can't see. $omnipotent_viewer = PhabricatorUser::getOmnipotentUser(); + $select_phids = array($board_phid); + + $descendants = id(new PhabricatorProjectQuery()) + ->setViewer($omnipotent_viewer) + ->withAncestorProjectPHIDs($select_phids) + ->execute(); + foreach ($descendants as $descendant) { + $select_phids[] = $descendant->getPHID(); + } + $board_tasks = id(new ManiphestTaskQuery()) ->setViewer($omnipotent_viewer) ->withEdgeLogicPHIDs( PhabricatorProjectObjectHasProjectEdgeType::EDGECONST, - PhabricatorQueryConstraint::OPERATOR_AND, - array($board_phid)) + PhabricatorQueryConstraint::OPERATOR_ANCESTOR, + array($select_phids)) ->execute(); $object_phids = mpull($board_tasks, 'getPHID'); diff --git a/src/applications/project/__tests__/PhabricatorProjectCoreTestCase.php b/src/applications/project/__tests__/PhabricatorProjectCoreTestCase.php index 74f637f16d..ee90afcb3b 100644 --- a/src/applications/project/__tests__/PhabricatorProjectCoreTestCase.php +++ b/src/applications/project/__tests__/PhabricatorProjectCoreTestCase.php @@ -972,7 +972,45 @@ final class PhabricatorProjectCoreTestCase extends PhabricatorTestCase { $task1->getPHID(), ); $this->assertTasksInColumn($expect, $user, $board, $column); + } + public function testMilestoneMoves() { + $user = $this->createUser(); + $user->save(); + + $board = $this->createProject($user); + + $backlog = $this->addColumn($user, $board, 0); + + // Create a task into the backlog. + $task = $this->newTask($user, array($board)); + $expect = array( + $backlog->getPHID(), + ); + $this->assertColumns($expect, $user, $board, $task); + + $milestone = $this->createProject($user, $board, true); + + $this->addProjectTags($user, $task, array($milestone->getPHID())); + + // We just want the side effect of looking at the board: creation of the + // milestone column. + $this->loadColumns($user, $board, $task); + + $column = id(new PhabricatorProjectColumnQuery()) + ->setViewer($user) + ->withProjectPHIDs(array($board->getPHID())) + ->withProxyPHIDs(array($milestone->getPHID())) + ->executeOne(); + + $this->assertTrue((bool)$column); + + // Moving the task to the milestone should have moved it to the milestone + // column. + $expect = array( + $column->getPHID(), + ); + $this->assertColumns($expect, $user, $board, $task); } private function moveToColumn( @@ -1014,7 +1052,14 @@ final class PhabricatorProjectCoreTestCase extends PhabricatorTestCase { PhabricatorUser $viewer, PhabricatorProject $board, ManiphestTask $task) { + $column_phids = $this->loadColumns($viewer, $board, $task); + $this->assertEqual($expect, $column_phids); + } + private function loadColumns( + PhabricatorUser $viewer, + PhabricatorProject $board, + ManiphestTask $task) { $engine = id(new PhabricatorBoardLayoutEngine()) ->setViewer($viewer) ->setBoardPHIDs(array($board->getPHID())) @@ -1028,7 +1073,7 @@ final class PhabricatorProjectCoreTestCase extends PhabricatorTestCase { $column_phids = mpull($columns, 'getPHID'); $column_phids = array_values($column_phids); - $this->assertEqual($expect, $column_phids); + return $column_phids; } private function assertTasksInColumn( @@ -1236,6 +1281,16 @@ final class PhabricatorProjectCoreTestCase extends PhabricatorTestCase { $this->applyTransactions($project, $user, $xactions); + // Force these values immediately; they are normally updated by the + // index engine. + if ($parent) { + if ($is_milestone) { + $parent->setHasMilestones(1)->save(); + } else { + $parent->setHasSubprojects(1)->save(); + } + } + return $project; } diff --git a/src/applications/project/controller/PhabricatorProjectBoardViewController.php b/src/applications/project/controller/PhabricatorProjectBoardViewController.php index d2fdf8d763..b046707c33 100644 --- a/src/applications/project/controller/PhabricatorProjectBoardViewController.php +++ b/src/applications/project/controller/PhabricatorProjectBoardViewController.php @@ -95,17 +95,27 @@ final class PhabricatorProjectBoardViewController $task_query = $search_engine->buildQueryFromSavedQuery($saved); + $select_phids = array($project->getPHID()); + if ($project->getHasSubprojects() || $project->getHasMilestones()) { + $descendants = id(new PhabricatorProjectQuery()) + ->setViewer($viewer) + ->withAncestorProjectPHIDs($select_phids) + ->execute(); + foreach ($descendants as $descendant) { + $select_phids[] = $descendant->getPHID(); + } + } + $tasks = $task_query ->withEdgeLogicPHIDs( PhabricatorProjectObjectHasProjectEdgeType::EDGECONST, - PhabricatorQueryConstraint::OPERATOR_AND, - array($project->getPHID())) + PhabricatorQueryConstraint::OPERATOR_ANCESTOR, + array($select_phids)) ->setOrder(ManiphestTaskQuery::ORDER_PRIORITY) ->setViewer($viewer) ->execute(); $tasks = mpull($tasks, null, 'getPHID'); - $board_phid = $project->getPHID(); $layout_engine = id(new PhabricatorBoardLayoutEngine()) @@ -225,6 +235,13 @@ final class PhabricatorProjectBoardViewController } } + $proxy = $column->getProxy(); + if ($proxy && !$proxy->isMilestone()) { + // TODO: For now, don't show subproject columns because we can't + // handle tasks with multiple positions yet. + continue; + } + $task_phids = $layout_engine->getColumnObjectPHIDs( $board_phid, $column->getPHID()); @@ -247,6 +264,11 @@ final class PhabricatorProjectBoardViewController $panel->setHeaderIcon($header_icon); } + $display_class = $column->getDisplayClass(); + if ($display_class) { + $panel->addClass($display_class); + } + if ($column->isHidden()) { $panel->addClass('project-panel-hidden'); } @@ -582,6 +604,12 @@ final class PhabricatorProjectBoardViewController $column_items = array(); + if ($column->getProxyPHID()) { + $default_phid = $column->getProxyPHID(); + } else { + $default_phid = $column->getProjectPHID(); + } + $column_items[] = id(new PhabricatorActionView()) ->setIcon('fa-plus') ->setName(pht('Create Task...')) @@ -590,6 +618,7 @@ final class PhabricatorProjectBoardViewController ->setMetadata( array( 'columnPHID' => $column->getPHID(), + 'projectPHID' => $default_phid, )); $batch_edit_uri = $request->getRequestURI(); @@ -738,6 +767,10 @@ final class PhabricatorProjectBoardViewController } } + // TODO: Tailor this UI if the project is already a parent project. We + // should not offer options for creating a parent project workboard, since + // they can't have their own columns. + $new_selector = id(new AphrontFormRadioButtonControl()) ->setLabel(pht('Columns')) ->setName('initialize-type') diff --git a/src/applications/project/controller/PhabricatorProjectMoveController.php b/src/applications/project/controller/PhabricatorProjectMoveController.php index 3695ad21ac..e9d8c8be78 100644 --- a/src/applications/project/controller/PhabricatorProjectMoveController.php +++ b/src/applications/project/controller/PhabricatorProjectMoveController.php @@ -139,7 +139,33 @@ final class PhabricatorProjectMoveController ->setTransactionType(ManiphestTransaction::TYPE_SUBPRIORITY) ->setNewValue($sub); } - } + } + + $proxy = $column->getProxy(); + if ($proxy) { + // We're moving the task into a subproject or milestone column, so add + // the subproject or milestone. + $add_projects = array($proxy->getPHID()); + } else if ($project->getHasSubprojects() || $project->getHasMilestones()) { + // We're moving the task into the "Backlog" column on the parent project, + // so add the parent explicitly. This gets rid of any subproject or + // milestone tags. + $add_projects = array($project->getPHID()); + } else { + $add_projects = array(); + } + + if ($add_projects) { + $project_type = PhabricatorProjectObjectHasProjectEdgeType::EDGECONST; + + $xactions[] = id(new ManiphestTransaction()) + ->setTransactionType(PhabricatorTransactions::TYPE_EDGE) + ->setMetadataValue('edge:type', $project_type) + ->setNewValue( + array( + '+' => array_fuse($add_projects), + )); + } $editor = id(new ManiphestTransactionEditor()) ->setActor($viewer) @@ -157,6 +183,18 @@ final class PhabricatorProjectMoveController ->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(); + $card = id(new ProjectBoardTaskCard()) ->setViewer($viewer) ->setTask($object) @@ -169,6 +207,6 @@ final class PhabricatorProjectMoveController return id(new AphrontAjaxResponse())->setContent( array('task' => $card)); - } + } } diff --git a/src/applications/project/controller/PhabricatorProjectSubprojectWarningController.php b/src/applications/project/controller/PhabricatorProjectSubprojectWarningController.php index 9e089f138c..d9dd401101 100644 --- a/src/applications/project/controller/PhabricatorProjectSubprojectWarningController.php +++ b/src/applications/project/controller/PhabricatorProjectSubprojectWarningController.php @@ -35,7 +35,7 @@ final class PhabricatorProjectSubprojectWarningController $conversion_help = pht( "Creating a project's first subproject **moves all ". - "members** and **destroys all workboard columns**.". + "members** to become members of the subproject instead". "\n\n". "See [[ %s | Projects User Guide ]] in the documentation for details. ". "This process can not be undone.", diff --git a/src/applications/project/engine/PhabricatorBoardLayoutEngine.php b/src/applications/project/engine/PhabricatorBoardLayoutEngine.php index e578131c83..31be95816b 100644 --- a/src/applications/project/engine/PhabricatorBoardLayoutEngine.php +++ b/src/applications/project/engine/PhabricatorBoardLayoutEngine.php @@ -320,8 +320,63 @@ final class PhabricatorBoardLayoutEngine extends Phobject { $columns = msort($columns, 'getSequence'); $columns = mpull($columns, null, 'getPHID'); - $this->columnMap = $columns; + $need_children = array(); + foreach ($boards as $phid => $board) { + if ($board->getHasMilestones() || $board->getHasSubprojects()) { + $need_children[] = $phid; + } + } + + if ($need_children) { + $children = id(new PhabricatorProjectQuery()) + ->setViewer($viewer) + ->withParentProjectPHIDs($need_children) + ->execute(); + $children = mpull($children, null, 'getPHID'); + $children = mgroup($children, 'getParentProjectPHID'); + } else { + $children = array(); + } + $columns = mgroup($columns, 'getProjectPHID'); + foreach ($boards as $board_phid => $board) { + $board_columns = idx($columns, $board_phid, array()); + + // If the project has milestones, create any missing columns. + if ($board->getHasMilestones() || $board->getHasSubprojects()) { + $child_projects = idx($children, $board_phid, array()); + + $next_sequence = last($board_columns)->getSequence() + 1; + $proxy_columns = mpull($board_columns, null, 'getProxyPHID'); + foreach ($child_projects as $child_phid => $child) { + if (isset($proxy_columns[$child_phid])) { + continue; + } + + $new_column = PhabricatorProjectColumn::initializeNewColumn($viewer) + ->attachProject($board) + ->attachProxy($child) + ->setSequence($next_sequence++) + ->setProjectPHID($board_phid) + ->setProxyPHID($child_phid); + + $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); + $new_column->save(); + unset($unguarded); + + $board_columns[$new_column->getPHID()] = $new_column; + } + } + + $columns[$board_phid] = $board_columns; + } + + foreach ($columns as $board_phid => $board_columns) { + foreach ($board_columns as $board_column) { + $column_phid = $board_column->getPHID(); + $this->columnMap[$column_phid] = $board_column; + } + } return $columns; } @@ -350,6 +405,8 @@ final class PhabricatorBoardLayoutEngine extends Phobject { array $columns, array $positions) { + $viewer = $this->getViewer(); + $board_phid = $board->getPHID(); $position_groups = mgroup($positions, 'getObjectPHID'); @@ -363,32 +420,143 @@ final class PhabricatorBoardLayoutEngine extends Phobject { } } + // Find all the columns which are proxies for other objects. + $proxy_map = array(); + foreach ($columns as $column) { + $proxy_phid = $column->getProxyPHID(); + if ($proxy_phid) { + $proxy_map[$proxy_phid] = $column->getPHID(); + } + } + $object_phids = $this->getObjectPHIDs(); + + // If we have proxies, we need to force cards into the correct proxy + // columns. + if ($proxy_map) { + $edge_query = id(new PhabricatorEdgeQuery()) + ->withSourcePHIDs($object_phids) + ->withEdgeTypes( + array( + PhabricatorProjectObjectHasProjectEdgeType::EDGECONST, + )); + $edge_query->execute(); + + $project_phids = $edge_query->getDestinationPHIDs(); + $project_phids = array_fuse($project_phids); + } else { + $project_phids = array(); + } + + if ($project_phids) { + $projects = id(new PhabricatorProjectQuery()) + ->setViewer($viewer) + ->withPHIDs($project_phids) + ->execute(); + $projects = mpull($projects, null, 'getPHID'); + } else { + $projects = array(); + } + + // Build a map from every project that any task is tagged with to the + // ancestor project which has a column on this board, if one exists. + $ancestor_map = array(); + foreach ($projects as $phid => $project) { + if (isset($proxy_map[$phid])) { + $ancestor_map[$phid] = $proxy_map[$phid]; + } else { + $seen = array($phid); + foreach ($project->getAncestorProjects() as $ancestor) { + $ancestor_phid = $ancestor->getPHID(); + $seen[] = $ancestor_phid; + if (isset($proxy_map[$ancestor_phid])) { + foreach ($seen as $project_phid) { + $ancestor_map[$project_phid] = $proxy_map[$ancestor_phid]; + } + } + } + } + } + foreach ($object_phids as $object_phid) { $positions = idx($position_groups, $object_phid, array()); - // Remove any positions in columns which no longer exist. - foreach ($positions as $key => $position) { - $column_phid = $position->getColumnPHID(); - if (empty($columns[$column_phid])) { - $this->remQueue[] = $position; - unset($positions[$key]); + // First, check for objects that have corresponding proxy columns. We're + // going to overwrite normal column positions if a tag belongs to a proxy + // column, since you can't be in normal columns if you're in proxy + // columns. + $proxy_hits = array(); + if ($proxy_map) { + $object_project_phids = $edge_query->getDestinationPHIDs( + array( + $object_phid, + )); + + foreach ($object_project_phids as $project_phid) { + if (isset($ancestor_map[$project_phid])) { + $proxy_hits[] = $ancestor_map[$project_phid]; + } } } - // If the object has no position, put it on the default column. - if (!$positions) { - $new_position = id(new PhabricatorProjectColumnPosition()) - ->setBoardPHID($board_phid) - ->setColumnPHID($default_phid) - ->setObjectPHID($object_phid) - ->setSequence(0); + if ($proxy_hits) { + // TODO: For now, only one column hit is permissible. + $proxy_hits = array_slice($proxy_hits, 0, 1); - $this->addQueue[] = $new_position; + $proxy_hits = array_fuse($proxy_hits); - $positions = array( - $new_position, - ); + // Check the object positions: we hope to find a position in each + // column the object should be part of. We're going to drop any + // invalid positions and create new positions where positions are + // missing. + foreach ($positions as $key => $position) { + $column_phid = $position->getColumnPHID(); + if (isset($proxy_hits[$column_phid])) { + // Valid column, mark the position as found. + unset($proxy_hits[$column_phid]); + } else { + // Invalid column, ignore the position. + unset($positions[$key]); + } + } + + // Create new positions for anything we haven't found. + foreach ($proxy_hits as $proxy_hit) { + $new_position = id(new PhabricatorProjectColumnPosition()) + ->setBoardPHID($board_phid) + ->setColumnPHID($proxy_hit) + ->setObjectPHID($object_phid) + ->setSequence(0); + + $this->addQueue[] = $new_position; + + $positions[] = $new_position; + } + } else { + // Ignore any positions in columns which no longer exist. We don't + // actively destory them because the rest of the code ignores them and + // there's no real need to destroy the data. + foreach ($positions as $key => $position) { + $column_phid = $position->getColumnPHID(); + if (empty($columns[$column_phid])) { + unset($positions[$key]); + } + } + + // If the object has no position, put it on the default column. + if (!$positions) { + $new_position = id(new PhabricatorProjectColumnPosition()) + ->setBoardPHID($board_phid) + ->setColumnPHID($default_phid) + ->setObjectPHID($object_phid) + ->setSequence(0); + + $this->addQueue[] = $new_position; + + $positions = array( + $new_position, + ); + } } foreach ($positions as $position) { diff --git a/src/applications/project/interface/PhabricatorColumnProxyInterface.php b/src/applications/project/interface/PhabricatorColumnProxyInterface.php new file mode 100644 index 0000000000..4e3c882d35 --- /dev/null +++ b/src/applications/project/interface/PhabricatorColumnProxyInterface.php @@ -0,0 +1,7 @@ +proxyPHIDs = $proxy_phids; + return $this; + } + public function withStatuses(array $status) { $this->statuses = $status; return $this; @@ -60,6 +66,55 @@ final class PhabricatorProjectColumnQuery $column->attachProject($project); } + $proxy_phids = array_filter(mpull($page, 'getProjectPHID')); + + return $page; + } + + protected function didFilterPage(array $page) { + $proxy_phids = array(); + foreach ($page as $column) { + $proxy_phid = $column->getProxyPHID(); + if ($proxy_phid !== null) { + $proxy_phids[$proxy_phid] = $proxy_phid; + } + } + + if ($proxy_phids) { + $proxies = id(new PhabricatorObjectQuery()) + ->setParentQuery($this) + ->setViewer($this->getViewer()) + ->withPHIDs($proxy_phids) + ->execute(); + $proxies = mpull($proxies, null, 'getPHID'); + } else { + $proxies = array(); + } + + foreach ($page as $key => $column) { + $proxy_phid = $column->getProxyPHID(); + + if ($proxy_phid !== null) { + $proxy = idx($proxies, $proxy_phid); + + // Only attach valid proxies, so we don't end up getting surprsied if + // an install somehow gets junk into their database. + if (!($proxy instanceof PhabricatorColumnProxyInterface)) { + $proxy = null; + } + + if (!$proxy) { + $this->didRejectResult($column); + unset($page[$key]); + continue; + } + } else { + $proxy = null; + } + + $column->attachProxy($proxy); + } + return $page; } @@ -87,6 +142,13 @@ final class PhabricatorProjectColumnQuery $this->projectPHIDs); } + if ($this->proxyPHIDs !== null) { + $where[] = qsprintf( + $conn, + 'proxyPHID IN (%Ls)', + $this->proxyPHIDs); + } + if ($this->statuses !== null) { $where[] = qsprintf( $conn, diff --git a/src/applications/project/storage/PhabricatorProject.php b/src/applications/project/storage/PhabricatorProject.php index 6cf116688e..b3d23a089e 100644 --- a/src/applications/project/storage/PhabricatorProject.php +++ b/src/applications/project/storage/PhabricatorProject.php @@ -9,7 +9,8 @@ final class PhabricatorProject extends PhabricatorProjectDAO PhabricatorCustomFieldInterface, PhabricatorDestructibleInterface, PhabricatorFulltextInterface, - PhabricatorConduitResultInterface { + PhabricatorConduitResultInterface, + PhabricatorColumnProxyInterface { protected $name; protected $status = PhabricatorProjectStatus::STATUS_ACTIVE; @@ -663,4 +664,25 @@ final class PhabricatorProject extends PhabricatorProjectDAO ); } + +/* -( PhabricatorColumnProxyInterface )------------------------------------ */ + + + public function getProxyColumnName() { + return $this->getName(); + } + + public function getProxyColumnIcon() { + return $this->getDisplayIconIcon(); + } + + public function getProxyColumnClass() { + if ($this->isMilestone()) { + return 'phui-workboard-column-milestone'; + } + + return null; + } + + } diff --git a/src/applications/project/storage/PhabricatorProjectColumn.php b/src/applications/project/storage/PhabricatorProjectColumn.php index ca124fec96..a63312438c 100644 --- a/src/applications/project/storage/PhabricatorProjectColumn.php +++ b/src/applications/project/storage/PhabricatorProjectColumn.php @@ -17,10 +17,12 @@ final class PhabricatorProjectColumn protected $name; protected $status; protected $projectPHID; + protected $proxyPHID; protected $sequence; protected $properties = array(); private $project = self::ATTACHABLE; + private $proxy = self::ATTACHABLE; public static function initializeNewColumn(PhabricatorUser $user) { return id(new PhabricatorProjectColumn()) @@ -38,6 +40,7 @@ final class PhabricatorProjectColumn 'name' => 'text255', 'status' => 'uint32', 'sequence' => 'uint32', + 'proxyPHID' => 'phid?', ), self::CONFIG_KEY_SCHEMA => array( 'key_status' => array( @@ -46,6 +49,10 @@ final class PhabricatorProjectColumn 'key_sequence' => array( 'columns' => array('projectPHID', 'sequence'), ), + 'key_proxy' => array( + 'columns' => array('projectPHID', 'proxyPHID'), + 'unique' => true, + ), ), ) + parent::getConfiguration(); } @@ -64,6 +71,15 @@ final class PhabricatorProjectColumn return $this->assertAttached($this->project); } + public function attachProxy($proxy) { + $this->proxy = $proxy; + return $this; + } + + public function getProxy() { + return $this->assertAttached($this->proxy); + } + public function isDefaultColumn() { return (bool)$this->getProperty('isDefault'); } @@ -73,6 +89,11 @@ final class PhabricatorProjectColumn } public function getDisplayName() { + $proxy = $this->getProxy(); + if ($proxy) { + return $proxy->getProxyColumnName(); + } + $name = $this->getName(); if (strlen($name)) { return $name; @@ -96,11 +117,23 @@ final class PhabricatorProjectColumn return null; } + public function getDisplayClass() { + $proxy = $this->getProxy(); + if ($proxy) { + return $proxy->getProxyColumnClass(); + } + + return null; + } + public function getHeaderIcon() { - $icon = null; + $proxy = $this->getProxy(); + if ($proxy) { + return $proxy->getProxyColumnIcon(); + } if ($this->isHidden()) { - $icon = 'fa-eye-slash'; + return 'fa-eye-slash'; } return null; diff --git a/src/docs/user/userguide/projects.diviner b/src/docs/user/userguide/projects.diviner index 7d014f3fb7..6f29e3586c 100644 --- a/src/docs/user/userguide/projects.diviner +++ b/src/docs/user/userguide/projects.diviner @@ -162,7 +162,6 @@ subprojects, parent projects, and milestones. |---|---|---|---|---| | //Members// | Yes | Union of Subprojects | Yes | Same as Parent | | //Policies// | Yes | Yes | Affected by Parent | Same as Parent | -| //Workboard// | Yes | No Custom Columns | Yes | Yes | | //Hashtags// | Yes | Yes | Yes | Special | @@ -257,14 +256,6 @@ parent project is an ancestor of the new subproject. You can edit the project afterward to change or remove members if you want to split membership apart in a more granular way across multiple new subprojects. -**No Workboard Columns**: Parent projects can not have their own workboard -columns: instead, the workboard of a parent project shows columns representing -the child projects. - -Thus, a project's workboard columns are destroyed when you add the first -subproject. All objects on the workboard will be returned to the project's -backlog. The new board will show columns for subprojects instead. - **Searching**: When you search for a parent project, results for any subproject are returned. For example, if you search for {nav Engineering}, your query will match results in {nav Engineering} itself, but also subprojects like diff --git a/src/view/phui/PHUIWorkpanelView.php b/src/view/phui/PHUIWorkpanelView.php index 96978de952..50b2e12161 100644 --- a/src/view/phui/PHUIWorkpanelView.php +++ b/src/view/phui/PHUIWorkpanelView.php @@ -10,8 +10,8 @@ final class PHUIWorkpanelView extends AphrontTagView { private $headerTag; private $headerIcon; - public function setHeaderIcon(PHUIIconView $header_icon) { - $this->headerIcon = $header_icon; + public function setHeaderIcon($icon) { + $this->headerIcon = $icon; return $this; } diff --git a/webroot/rsrc/js/application/projects/behavior-project-boards.js b/webroot/rsrc/js/application/projects/behavior-project-boards.js index bd69642e1d..8d9b61cc8c 100644 --- a/webroot/rsrc/js/application/projects/behavior-project-boards.js +++ b/webroot/rsrc/js/application/projects/behavior-project-boards.js @@ -280,13 +280,16 @@ JX.behavior('project-boards', function(config, statics) { // close the dropdown, but don't want to follow the link. e.prevent(); - var column_phid = e.getNodeData('column-add-task').columnPHID; + var column_data = e.getNodeData('column-add-task'); + var column_phid = column_data.columnPHID; + var request_data = { responseType: 'card', columnPHID: column_phid, - projects: statics.projectPHID, + projects: column_data.projectPHID, order: statics.order }; + var cols = getcolumns(); var ii; var column; From 2bdbd7833d320df71479ff528fe3484a253eb155 Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 3 Feb 2016 17:21:38 -0800 Subject: [PATCH 38/54] Don't show any subproject tags on workboard cards Summary: Ref T10010. This gets rid of, e.g., the "Iteration I" tag in the column for that milestone, as it is redundant with the column itself. Test Plan: {F1090427} Reviewers: chad Reviewed By: chad Maniphest Tasks: T10010 Differential Revision: https://secure.phabricator.com/D15181 --- .../maniphest/editor/ManiphestEditEngine.php | 9 ++++++++- .../PhabricatorProjectBoardViewController.php | 19 +++++++++++++++++- .../PhabricatorProjectMoveController.php | 20 ++++++++++++++++++- .../project/view/ProjectBoardTaskCard.php | 20 +++++++++---------- 4 files changed, 54 insertions(+), 14 deletions(-) diff --git a/src/applications/maniphest/editor/ManiphestEditEngine.php b/src/applications/maniphest/editor/ManiphestEditEngine.php index abbdd4c2e1..3f88a9abe3 100644 --- a/src/applications/maniphest/editor/ManiphestEditEngine.php +++ b/src/applications/maniphest/editor/ManiphestEditEngine.php @@ -344,11 +344,18 @@ final class ManiphestEditEngine ->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()) ->setViewer($viewer) ->setTask($task) ->setOwner($owner) - ->setProject($column->getProject()) + ->setProjectHandles($project_handles) ->setCanEdit(true) ->getItem(); diff --git a/src/applications/project/controller/PhabricatorProjectBoardViewController.php b/src/applications/project/controller/PhabricatorProjectBoardViewController.php index b046707c33..396fb15999 100644 --- a/src/applications/project/controller/PhabricatorProjectBoardViewController.php +++ b/src/applications/project/controller/PhabricatorProjectBoardViewController.php @@ -228,6 +228,20 @@ final class PhabricatorProjectBoardViewController $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); + foreach ($columns as $column) { if (!$this->showHidden) { if ($column->isHidden()) { @@ -308,9 +322,12 @@ final class PhabricatorProjectBoardViewController $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) - ->setProject($project) + ->setProjectHandles($handles) ->setTask($task) ->setOwner($owner) ->setCanEdit($can_edit) diff --git a/src/applications/project/controller/PhabricatorProjectMoveController.php b/src/applications/project/controller/PhabricatorProjectMoveController.php index e9d8c8be78..7cbbf3d0ae 100644 --- a/src/applications/project/controller/PhabricatorProjectMoveController.php +++ b/src/applications/project/controller/PhabricatorProjectMoveController.php @@ -195,12 +195,30 @@ final class PhabricatorProjectMoveController )) ->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(); + } + } + + $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()) ->setViewer($viewer) ->setTask($object) ->setOwner($owner) ->setCanEdit(true) - ->setProject($project) + ->setProjectHandles($project_handles) ->getItem(); $card->addClass('phui-workcard'); diff --git a/src/applications/project/view/ProjectBoardTaskCard.php b/src/applications/project/view/ProjectBoardTaskCard.php index 2ac29f11ec..a614335f59 100644 --- a/src/applications/project/view/ProjectBoardTaskCard.php +++ b/src/applications/project/view/ProjectBoardTaskCard.php @@ -3,7 +3,7 @@ final class ProjectBoardTaskCard extends Phobject { private $viewer; - private $project; + private $projectHandles; private $task; private $owner; private $canEdit; @@ -16,12 +16,13 @@ final class ProjectBoardTaskCard extends Phobject { return $this->viewer; } - public function setProject(PhabricatorProject $project) { - $this->project = $project; + public function setProjectHandles(array $handles) { + $this->projectHandles = $handles; return $this; } - public function getProject() { - return $this->project; + + public function getProjectHandles() { + return $this->projectHandles; } public function setTask(ManiphestTask $task) { @@ -83,14 +84,11 @@ final class ProjectBoardTaskCard extends Phobject { $card->addHandleIcon($owner, $owner->getName()); } - $project_phids = array_fuse($task->getProjectPHIDs()); - unset($project_phids[$this->project->getPHID()]); - - if ($project_phids) { - $handle_list = $viewer->loadHandles($project_phids); + $project_handles = $this->getProjectHandles(); + if ($project_handles) { $tag_list = id(new PHUIHandleTagListView()) ->setSlim(true) - ->setHandles($handle_list); + ->setHandles($project_handles); $card->addAttribute($tag_list); } From 2f0571923c9ae5a2b6cbfb49b25c7776a2d64041 Mon Sep 17 00:00:00 2001 From: Chad Little Date: Thu, 4 Feb 2016 02:22:34 +0000 Subject: [PATCH 39/54] Add project list to user profiles Summary: Adds which Projects a user is a member of to their profile, with a link to more. Build fallback states for no badges or no projects. Test Plan: Review a user with projects, without projects, with badges, without badges. {F1084127} Reviewers: epriestley Reviewed By: epriestley Subscribers: Korvin Differential Revision: https://secure.phabricator.com/D15150 --- ...PhabricatorPeopleProfileViewController.php | 99 +++++++++++++++---- 1 file changed, 79 insertions(+), 20 deletions(-) diff --git a/src/applications/people/controller/PhabricatorPeopleProfileViewController.php b/src/applications/people/controller/PhabricatorPeopleProfileViewController.php index cdbbddd8d8..57f217b7d7 100644 --- a/src/applications/people/controller/PhabricatorPeopleProfileViewController.php +++ b/src/applications/people/controller/PhabricatorPeopleProfileViewController.php @@ -54,23 +54,21 @@ final class PhabricatorPeopleProfileViewController $feed = $this->buildPeopleFeed($user, $viewer); $feed = phutil_tag_div('project-view-feed', $feed); + $projects = $this->buildProjectsView($user); $badges = $this->buildBadgesView($user); - if ($badges) { - $columns = id(new PHUITwoColumnView()) - ->addClass('project-view-badges') - ->setMainColumn( - array( - $properties, - $feed, - )) - ->setSideColumn( - array( - $badges, - )); - } else { - $columns = array($properties, $feed); - } + $columns = id(new PHUITwoColumnView()) + ->addClass('project-view-badges') + ->setMainColumn( + array( + $properties, + $feed, + )) + ->setSideColumn( + array( + $projects, + $badges, + )); $nav = $this->getProfileMenu(); $nav->selectFilter(PhabricatorPeopleProfilePanelEngine::PANEL_PROFILE); @@ -124,12 +122,65 @@ final class PhabricatorPeopleProfileViewController return $view; } + private function buildProjectsView( + PhabricatorUser $user) { + + $viewer = $this->getViewer(); + $projects = id(new PhabricatorProjectQuery()) + ->setViewer($viewer) + ->withMemberPHIDs(array($user->getPHID())) + ->needImages(true) + ->withStatus(PhabricatorProjectQuery::STATUS_OPEN) + ->execute(); + + $header = id(new PHUIHeaderView()) + ->setHeader(pht('Projects')); + + if (!empty($projects)) { + $limit = 5; + $render_phids = array_slice($projects, 0, $limit); + $list = id(new PhabricatorProjectListView()) + ->setUser($viewer) + ->setProjects($render_phids); + + if (count($projects) > $limit) { + $header_text = pht( + 'Projects (%s)', + phutil_count($projects)); + + $header = id(new PHUIHeaderView()) + ->setHeader($header_text) + ->addActionLink( + id(new PHUIButtonView()) + ->setTag('a') + ->setIcon('fa-list-ul') + ->setText(pht('View All')) + ->setHref('/project/?member='.$user->getPHID())); + + } + + } else { + $error = id(new PHUIBoxView()) + ->addClass('mlb') + ->appendChild(pht('User does not belong to any projects.')); + $list = id(new PHUIInfoView()) + ->setSeverity(PHUIInfoView::SEVERITY_NODATA) + ->appendChild($error); + } + + $box = id(new PHUIObjectBoxView()) + ->setHeader($header) + ->appendChild($list) + ->setBackground(PHUIBoxView::GREY); + + return $box; + } + private function buildBadgesView( PhabricatorUser $user) { $viewer = $this->getViewer(); $class = 'PhabricatorBadgesApplication'; - $box = null; if (PhabricatorApplication::isClassInstalledForViewer($class, $viewer)) { $badge_phids = $user->getBadgePHIDs(); @@ -150,13 +201,21 @@ final class PhabricatorPeopleProfileViewController $flex->addItem($item); } - $box = id(new PHUIObjectBoxView()) - ->setHeaderText(pht('Badges')) - ->appendChild($flex) - ->setBackground(PHUIBoxView::GREY); + } else { + $error = id(new PHUIBoxView()) + ->addClass('mlb') + ->appendChild(pht('User does not have any badges.')); + $flex = id(new PHUIInfoView()) + ->setSeverity(PHUIInfoView::SEVERITY_NODATA) + ->appendChild($error); } } + $box = id(new PHUIObjectBoxView()) + ->setHeaderText(pht('Badges')) + ->appendChild($flex) + ->setBackground(PHUIBoxView::GREY); + return $box; } From 42954bc5ac81c0b50fb2dcdb28417b01eb0ff8f3 Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 4 Feb 2016 07:13:47 -0800 Subject: [PATCH 40/54] Fix bad rendering pathway on user profiles for viewers without Badges application Summary: Fixes T10275. We'd fatal on `$flex` not being defined. Test Plan: Uninstalled badges, viewed profile. Before: fatal; now: no badges element appears but profile renders properly. Reviewers: chad Reviewed By: chad Maniphest Tasks: T10275 Differential Revision: https://secure.phabricator.com/D15182 --- ...PhabricatorPeopleProfileViewController.php | 53 ++++++++++--------- 1 file changed, 27 insertions(+), 26 deletions(-) diff --git a/src/applications/people/controller/PhabricatorPeopleProfileViewController.php b/src/applications/people/controller/PhabricatorPeopleProfileViewController.php index 57f217b7d7..45125c28b0 100644 --- a/src/applications/people/controller/PhabricatorPeopleProfileViewController.php +++ b/src/applications/people/controller/PhabricatorPeopleProfileViewController.php @@ -176,39 +176,40 @@ final class PhabricatorPeopleProfileViewController return $box; } - private function buildBadgesView( - PhabricatorUser $user) { + private function buildBadgesView(PhabricatorUser $user) { $viewer = $this->getViewer(); $class = 'PhabricatorBadgesApplication'; - 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(); + if (!PhabricatorApplication::isClassInstalledForViewer($class, $viewer)) { + return null; + } - $flex = new PHUIBadgeBoxView(); - foreach ($badges as $badge) { - $item = id(new PHUIBadgeView()) - ->setIcon($badge->getIcon()) - ->setHeader($badge->getName()) - ->setSubhead($badge->getFlavor()) - ->setQuality($badge->getQuality()); - $flex->addItem($item); - } + $badge_phids = $user->getBadgePHIDs(); + if ($badge_phids) { + $badges = id(new PhabricatorBadgesQuery()) + ->setViewer($viewer) + ->withPHIDs($badge_phids) + ->withStatuses(array(PhabricatorBadgesBadge::STATUS_ACTIVE)) + ->execute(); - } else { - $error = id(new PHUIBoxView()) - ->addClass('mlb') - ->appendChild(pht('User does not have any badges.')); - $flex = id(new PHUIInfoView()) - ->setSeverity(PHUIInfoView::SEVERITY_NODATA) - ->appendChild($error); + $flex = new PHUIBadgeBoxView(); + foreach ($badges as $badge) { + $item = id(new PHUIBadgeView()) + ->setIcon($badge->getIcon()) + ->setHeader($badge->getName()) + ->setSubhead($badge->getFlavor()) + ->setQuality($badge->getQuality()); + $flex->addItem($item); } + + } else { + $error = id(new PHUIBoxView()) + ->addClass('mlb') + ->appendChild(pht('User does not have any badges.')); + $flex = id(new PHUIInfoView()) + ->setSeverity(PHUIInfoView::SEVERITY_NODATA) + ->appendChild($error); } $box = id(new PHUIObjectBoxView()) From c01f23adfb82295588e3a9536f5281f32eef2b6e Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 4 Feb 2016 07:50:58 -0800 Subject: [PATCH 41/54] Improve some column behaviors for Milestone/Subproject columns Summary: Ref T10010. - Don't allow milestones to be reordered. - Hide phantom subproject columns when reodrering. - Don't allow subproject/milestone columns to be renamed. - Force milestones to be ordered at the end, and in the correct order. - Add some missing crumbs. Test Plan: Reordered columns, renamed columns, made a new column, viewed column details. Reviewers: chad Reviewed By: chad Maniphest Tasks: T10010 Differential Revision: https://secure.phabricator.com/D15183 --- ...abricatorProjectBoardReorderController.php | 13 +++++- ...abricatorProjectColumnDetailController.php | 7 ++++ ...PhabricatorProjectColumnEditController.php | 40 ++++++++++--------- .../engine/PhabricatorBoardLayoutEngine.php | 4 +- .../storage/PhabricatorProjectColumn.php | 22 +++++++++- 5 files changed, 64 insertions(+), 22 deletions(-) diff --git a/src/applications/project/controller/PhabricatorProjectBoardReorderController.php b/src/applications/project/controller/PhabricatorProjectBoardReorderController.php index 425c27b5f0..011bbd069a 100644 --- a/src/applications/project/controller/PhabricatorProjectBoardReorderController.php +++ b/src/applications/project/controller/PhabricatorProjectBoardReorderController.php @@ -97,9 +97,20 @@ final class PhabricatorProjectBoardReorderController ->setFlush(true); foreach ($columns as $column) { + // Don't allow milestone columns to be reordered. + $proxy = $column->getProxy(); + if ($proxy && $proxy->isMilestone()) { + continue; + } + + // At least for now, don't show subproject column. + if ($proxy) { + continue; + } + $item = id(new PHUIObjectItemView()) ->setHeader($column->getDisplayName()) - ->addIcon('none', $column->getDisplayType()); + ->addIcon($column->getHeaderIcon(), $column->getDisplayType()); if ($column->isHidden()) { $item->setDisabled(true); diff --git a/src/applications/project/controller/PhabricatorProjectColumnDetailController.php b/src/applications/project/controller/PhabricatorProjectColumnDetailController.php index ca25c089fb..e008c832d9 100644 --- a/src/applications/project/controller/PhabricatorProjectColumnDetailController.php +++ b/src/applications/project/controller/PhabricatorProjectColumnDetailController.php @@ -22,6 +22,8 @@ final class PhabricatorProjectColumnDetailController } $this->setProject($project); + $project_id = $project->getID(); + $column = id(new PhabricatorProjectColumnQuery()) ->setViewer($viewer) ->withIDs(array($id)) @@ -45,6 +47,10 @@ final class PhabricatorProjectColumnDetailController $actions = $this->buildActionView($column); $properties = $this->buildPropertyView($column, $actions); + $crumbs = $this->buildApplicationCrumbs(); + $crumbs->addTextCrumb(pht('Workboard'), "/project/board/{$project_id}/"); + $crumbs->addTextCrumb(pht('Column: %s', $title)); + $box = id(new PHUIObjectBoxView()) ->setHeader($header) ->addPropertyList($properties); @@ -54,6 +60,7 @@ final class PhabricatorProjectColumnDetailController return $this->newPage() ->setTitle($title) ->setNavigation($nav) + ->setCrumbs($crumbs) ->appendChild( array( $box, diff --git a/src/applications/project/controller/PhabricatorProjectColumnEditController.php b/src/applications/project/controller/PhabricatorProjectColumnEditController.php index 77f90b56cb..5ebc721c69 100644 --- a/src/applications/project/controller/PhabricatorProjectColumnEditController.php +++ b/src/applications/project/controller/PhabricatorProjectColumnEditController.php @@ -81,10 +81,12 @@ final class PhabricatorProjectColumnEditController $xactions = array(); - $type_name = PhabricatorProjectColumnTransaction::TYPE_NAME; - $xactions[] = id(new PhabricatorProjectColumnTransaction()) - ->setTransactionType($type_name) - ->setNewValue($v_name); + if (!$column->getProxy()) { + $type_name = PhabricatorProjectColumnTransaction::TYPE_NAME; + $xactions[] = id(new PhabricatorProjectColumnTransaction()) + ->setTransactionType($type_name) + ->setNewValue($v_name); + } $type_limit = PhabricatorProjectColumnTransaction::TYPE_LIMIT; $xactions[] = id(new PhabricatorProjectColumnTransaction()) @@ -105,26 +107,26 @@ final class PhabricatorProjectColumnEditController } } - $form = new AphrontFormView(); - $form - ->setUser($request->getUser()) - ->appendChild( + $form = id(new AphrontFormView()) + ->setUser($request->getUser()); + + if (!$column->getProxy()) { + $form->appendChild( id(new AphrontFormTextControl()) ->setValue($v_name) ->setLabel(pht('Name')) ->setName('name') - ->setError($e_name) - ->setCaption( - pht('This will be displayed as the header of the column.'))) - ->appendChild( - id(new AphrontFormTextControl()) - ->setValue($v_limit) - ->setLabel(pht('Point Limit')) - ->setName('limit') - ->setError($e_limit) - ->setCaption( - pht('Maximum number of points of tasks allowed in the column.'))); + ->setError($e_name)); + } + $form->appendChild( + id(new AphrontFormTextControl()) + ->setValue($v_limit) + ->setLabel(pht('Point Limit')) + ->setName('limit') + ->setError($e_limit) + ->setCaption( + pht('Maximum number of points of tasks allowed in the column.'))); if ($is_new) { $title = pht('Create Column'); diff --git a/src/applications/project/engine/PhabricatorBoardLayoutEngine.php b/src/applications/project/engine/PhabricatorBoardLayoutEngine.php index 31be95816b..4415e2fdf9 100644 --- a/src/applications/project/engine/PhabricatorBoardLayoutEngine.php +++ b/src/applications/project/engine/PhabricatorBoardLayoutEngine.php @@ -317,7 +317,7 @@ final class PhabricatorBoardLayoutEngine extends Phobject { ->setViewer($viewer) ->withProjectPHIDs(array_keys($boards)) ->execute(); - $columns = msort($columns, 'getSequence'); + $columns = msort($columns, 'getOrderingKey'); $columns = mpull($columns, null, 'getPHID'); $need_children = array(); @@ -368,6 +368,8 @@ final class PhabricatorBoardLayoutEngine extends Phobject { } } + $board_columns = msort($board_columns, 'getOrderingKey'); + $columns[$board_phid] = $board_columns; } diff --git a/src/applications/project/storage/PhabricatorProjectColumn.php b/src/applications/project/storage/PhabricatorProjectColumn.php index a63312438c..0ab6ec89c0 100644 --- a/src/applications/project/storage/PhabricatorProjectColumn.php +++ b/src/applications/project/storage/PhabricatorProjectColumn.php @@ -27,7 +27,8 @@ final class PhabricatorProjectColumn public static function initializeNewColumn(PhabricatorUser $user) { return id(new PhabricatorProjectColumn()) ->setName('') - ->setStatus(self::STATUS_ACTIVE); + ->setStatus(self::STATUS_ACTIVE) + ->attachProxy(null); } protected function getConfiguration() { @@ -157,6 +158,25 @@ final class PhabricatorProjectColumn return $this; } + public function getOrderingKey() { + $proxy = $this->getProxy(); + + // Normal columns and subproject columns go first, in a user-controlled + // order. + + // All the milestone columns go last, in their sequential order. + + if (!$proxy || !$proxy->isMilestone()) { + $group = 'A'; + $sequence = $this->getSequence(); + } else { + $group = 'B'; + $sequence = $proxy->getMilestoneNumber(); + } + + return sprintf('%s%012d', $group, $sequence); + } + /* -( PhabricatorApplicationTransactionInterface )------------------------- */ From b9585f29fa2941a70ef3cd96d52cd67c66bc482b Mon Sep 17 00:00:00 2001 From: Chad Little Date: Fri, 5 Feb 2016 16:05:41 +0000 Subject: [PATCH 42/54] Add icon / grey text when task is closed on workboards Summary: Fixes T10281. Adds the closed icon (resolved, dupe, ect) as an attribute and makes the text grey again. Test Plan: View workboard with "All Tasks" {F1092738} Reviewers: epriestley Reviewed By: epriestley Subscribers: Korvin Maniphest Tasks: T10281 Differential Revision: https://secure.phabricator.com/D15187 --- resources/celerity/map.php | 4 ++-- src/applications/project/view/ProjectBoardTaskCard.php | 8 ++++++++ webroot/rsrc/css/phui/workboards/phui-workcard.css | 8 ++++++++ 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/resources/celerity/map.php b/resources/celerity/map.php index efe626ca21..b715abda72 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -154,7 +154,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' => '0d1aa006', + 'rsrc/css/phui/workboards/phui-workcard.css' => 'b4322ca7', 'rsrc/css/phui/workboards/phui-workpanel.css' => '68140031', 'rsrc/css/sprite-login.css' => '60e8560e', 'rsrc/css/sprite-menu.css' => '9dd65b92', @@ -831,7 +831,7 @@ return array( 'phui-timeline-view-css' => '2efceff8', 'phui-two-column-view-css' => 'c75bfc5b', 'phui-workboard-view-css' => 'b07a5524', - 'phui-workcard-view-css' => '0d1aa006', + 'phui-workcard-view-css' => 'b4322ca7', 'phui-workpanel-view-css' => '68140031', 'phuix-action-list-view' => 'b5c256b8', 'phuix-action-view' => '8cf6d262', diff --git a/src/applications/project/view/ProjectBoardTaskCard.php b/src/applications/project/view/ProjectBoardTaskCard.php index a614335f59..2c69e0f37b 100644 --- a/src/applications/project/view/ProjectBoardTaskCard.php +++ b/src/applications/project/view/ProjectBoardTaskCard.php @@ -84,6 +84,14 @@ final class ProjectBoardTaskCard extends Phobject { $card->addHandleIcon($owner, $owner->getName()); } + if ($task->isClosed()) { + $icon = ManiphestTaskStatus::getStatusIcon($task->getStatus()); + $icon = id(new PHUIIconView()) + ->setIcon($icon.' grey'); + $card->addAttribute($icon); + $card->setBarColor('grey'); + } + $project_handles = $this->getProjectHandles(); if ($project_handles) { $tag_list = id(new PHUIHandleTagListView()) diff --git a/webroot/rsrc/css/phui/workboards/phui-workcard.css b/webroot/rsrc/css/phui/workboards/phui-workcard.css index 671833ea86..d0c427da57 100644 --- a/webroot/rsrc/css/phui/workboards/phui-workcard.css +++ b/webroot/rsrc/css/phui/workboards/phui-workcard.css @@ -41,6 +41,14 @@ margin-left: 2px; } +.phui-object-item-disabled.phui-workcard { + background-color: rgba(255,255,255,.67); +} + +.phui-object-item-disabled.phui-workcard .phui-object-item-link { + color: {$greytext}; +} + .device-desktop .phui-workcard .phui-object-item-with-1-actions .phui-object-item-content-box { margin-right: 0; From 8a9f7609755af5e32c2c287c4b732d213cb680ee Mon Sep 17 00:00:00 2001 From: Chad Little Date: Thu, 4 Feb 2016 20:11:36 -0800 Subject: [PATCH 43/54] Add ProjectCardView, use on Hovercards Summary: Builds a new ProjectCardView, starts basic Project Hovercard redesign (needs milestone, subproject support). Ref T10055 Test Plan: View all the colors. {F1092622} Reviewers: epriestley Reviewed By: epriestley Subscribers: Korvin Maniphest Tasks: T10055 Differential Revision: https://secure.phabricator.com/D15186 --- resources/celerity/map.php | 6 +- src/__phutil_library_map__.php | 6 +- .../ProjectHovercardEngineExtension.php | 55 +++++++ .../view/PhabricatorProjectCardView.php | 84 ++++++++++ src/view/phui/PHUIHovercardView.php | 28 +++- .../application/project/project-card-view.css | 150 ++++++++++++++++++ webroot/rsrc/css/phui/phui-hovercard.css | 7 +- 7 files changed, 320 insertions(+), 16 deletions(-) create mode 100644 src/applications/project/events/ProjectHovercardEngineExtension.php create mode 100644 src/applications/project/view/PhabricatorProjectCardView.php create mode 100644 webroot/rsrc/css/application/project/project-card-view.css diff --git a/resources/celerity/map.php b/resources/celerity/map.php index b715abda72..747ee79741 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -93,6 +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' => 'ee1ce91d', 'rsrc/css/application/project/project-view.css' => '99a5023b', 'rsrc/css/application/releeph/releeph-core.css' => '9b3c5733', 'rsrc/css/application/releeph/releeph-preview-branch.css' => 'b7a6f4a5', @@ -134,7 +135,7 @@ return array( 'rsrc/css/phui/phui-form-view.css' => '4a1a0f5e', 'rsrc/css/phui/phui-form.css' => '0b98e572', 'rsrc/css/phui/phui-header-view.css' => 'd53cc835', - 'rsrc/css/phui/phui-hovercard.css' => '5684c081', + 'rsrc/css/phui/phui-hovercard.css' => 'de1a2119', 'rsrc/css/phui/phui-icon-set-selector.css' => '1ab67aad', 'rsrc/css/phui/phui-icon.css' => '3f33ab57', 'rsrc/css/phui/phui-image-mask.css' => '5a8b09c8', @@ -809,7 +810,7 @@ return array( 'phui-form-view-css' => '4a1a0f5e', 'phui-header-view-css' => 'd53cc835', 'phui-hovercard' => '1bd28176', - 'phui-hovercard-view-css' => '5684c081', + 'phui-hovercard-view-css' => 'de1a2119', 'phui-icon-set-selector-css' => '1ab67aad', 'phui-icon-view-css' => '3f33ab57', 'phui-image-mask-css' => '5a8b09c8', @@ -843,6 +844,7 @@ return array( 'policy-edit-css' => '815c66f7', 'policy-transaction-detail-css' => '82100a43', 'ponder-view-css' => '7b0df4da', + 'project-card-view-css' => 'ee1ce91d', 'project-view-css' => '99a5023b', 'releeph-core' => '9b3c5733', 'releeph-preview-branch' => 'b7a6f4a5', diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 9910ad1349..763b41e99b 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -2862,6 +2862,7 @@ phutil_register_library_map(array( 'PhabricatorProjectBoardImportController' => 'applications/project/controller/PhabricatorProjectBoardImportController.php', 'PhabricatorProjectBoardReorderController' => 'applications/project/controller/PhabricatorProjectBoardReorderController.php', 'PhabricatorProjectBoardViewController' => 'applications/project/controller/PhabricatorProjectBoardViewController.php', + 'PhabricatorProjectCardView' => 'applications/project/view/PhabricatorProjectCardView.php', 'PhabricatorProjectColorsConfigOptionType' => 'applications/project/config/PhabricatorProjectColorsConfigOptionType.php', 'PhabricatorProjectColumn' => 'applications/project/storage/PhabricatorProjectColumn.php', 'PhabricatorProjectColumnDetailController' => 'applications/project/controller/PhabricatorProjectColumnDetailController.php', @@ -3816,6 +3817,7 @@ 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', @@ -5672,7 +5674,7 @@ phutil_register_library_map(array( 'PHUIHandleView' => 'AphrontView', 'PHUIHeaderView' => 'AphrontTagView', 'PHUIHovercardUIExample' => 'PhabricatorUIExample', - 'PHUIHovercardView' => 'AphrontView', + 'PHUIHovercardView' => 'AphrontTagView', 'PHUIIconCircleView' => 'AphrontTagView', 'PHUIIconExample' => 'PhabricatorUIExample', 'PHUIIconView' => 'AphrontTagView', @@ -7272,6 +7274,7 @@ phutil_register_library_map(array( 'PhabricatorProjectBoardImportController' => 'PhabricatorProjectBoardController', 'PhabricatorProjectBoardReorderController' => 'PhabricatorProjectBoardController', 'PhabricatorProjectBoardViewController' => 'PhabricatorProjectBoardController', + 'PhabricatorProjectCardView' => 'AphrontTagView', 'PhabricatorProjectColorsConfigOptionType' => 'PhabricatorConfigJSONOptionType', 'PhabricatorProjectColumn' => array( 'PhabricatorProjectDAO', @@ -8449,6 +8452,7 @@ phutil_register_library_map(array( 'ProjectDefaultJoinCapability' => 'PhabricatorPolicyCapability', 'ProjectDefaultViewCapability' => 'PhabricatorPolicyCapability', 'ProjectEditConduitAPIMethod' => 'PhabricatorEditEngineAPIMethod', + 'ProjectHovercardEngineExtension' => 'PhabricatorHovercardEngineExtension', 'ProjectQueryConduitAPIMethod' => 'ProjectConduitAPIMethod', 'ProjectRemarkupRule' => 'PhabricatorObjectRemarkupRule', 'ProjectRemarkupRuleTestCase' => 'PhabricatorTestCase', diff --git a/src/applications/project/events/ProjectHovercardEngineExtension.php b/src/applications/project/events/ProjectHovercardEngineExtension.php new file mode 100644 index 0000000000..deef9a30f8 --- /dev/null +++ b/src/applications/project/events/ProjectHovercardEngineExtension.php @@ -0,0 +1,55 @@ +getViewer(); + $phids = mpull($objects, 'getPHID'); + + $projects = id(new PhabricatorProjectQuery()) + ->setViewer($viewer) + ->withPHIDs($phids) + ->needImages(true) + ->execute(); + $projects = mpull($projects, null, 'getPHID'); + + return array( + 'projects' => $projects, + ); + } + + public function renderHovercard( + PHUIHovercardView $hovercard, + PhabricatorObjectHandle $handle, + $object, + $data) { + $viewer = $this->getViewer(); + + $project = idx($data['projects'], $object->getPHID()); + if (!$project) { + return; + } + + $project_card = id(new PhabricatorProjectCardView()) + ->setProject($project) + ->setViewer($viewer); + + $hovercard->appendChild($project_card); + } + +} diff --git a/src/applications/project/view/PhabricatorProjectCardView.php b/src/applications/project/view/PhabricatorProjectCardView.php new file mode 100644 index 0000000000..d520bc0e5a --- /dev/null +++ b/src/applications/project/view/PhabricatorProjectCardView.php @@ -0,0 +1,84 @@ +project = $project; + 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'; + + $color = $this->project->getColor(); + $classes[] = 'project-card-'.$color; + + return array( + 'class' => implode($classes, ' '), + ); + } + + protected function getTagContent() { + + $project = $this->project; + $viewer = $this->viewer; + require_celerity_resource('project-card-view-css'); + + $icon = $project->getDisplayIconIcon(); + $icon_name = $project->getDisplayIconName(); + $tag = id(new PHUITagView()) + ->setIcon($icon) + ->setName($icon_name) + ->addClass('project-view-header-tag') + ->setType(PHUITagView::TYPE_SHADE); + + $header = id(new PHUIHeaderView()) + ->setHeader(array($project->getName(), $tag)) + ->setUser($viewer) + ->setPolicyObject($project) + ->setImage($project->getProfileImageURI()); + + if ($project->getStatus() == PhabricatorProjectStatus::STATUS_ACTIVE) { + $header->setStatus('fa-check', 'bluegrey', pht('Active')); + } else { + $header->setStatus('fa-ban', 'red', pht('Archived')); + } + + $description = null; + + $card = phutil_tag( + 'div', + array( + 'class' => 'project-card-inner', + ), + array( + $header, + $description, + )); + + return $card; + } + +} diff --git a/src/view/phui/PHUIHovercardView.php b/src/view/phui/PHUIHovercardView.php index 4f8b8b295b..e5c89bb8fd 100644 --- a/src/view/phui/PHUIHovercardView.php +++ b/src/view/phui/PHUIHovercardView.php @@ -4,7 +4,7 @@ * The default one-for-all hovercard. We may derive from this one to create * more specialized ones. */ -final class PHUIHovercardView extends AphrontView { +final class PHUIHovercardView extends AphrontTagView { /** * @var PhabricatorObjectHandle @@ -70,7 +70,16 @@ final class PHUIHovercardView extends AphrontView { return $this; } - public function render() { + protected function getTagAttributes() { + $classes = array(); + $classes[] = 'phui-hovercard-wrapper'; + + return array( + 'class' => implode(' ', $classes), + ); + } + + protected function getTagContent() { if (!$this->handle) { throw new PhutilInvalidStateException('setObjectHandle'); } @@ -80,14 +89,17 @@ final class PHUIHovercardView extends AphrontView { require_celerity_resource('phui-hovercard-view-css'); + // If we're a fully custom Hovercard, skip the common UI + $children = $this->renderChildren(); + if ($children) { + return $children; + } + $title = array( id(new PHUISpacesNamespaceContextView()) ->setUser($viewer) ->setObject($this->getObject()), - pht( - '%s: %s', - $handle->getTypeName(), - $this->title ? $this->title : $handle->getName()), + $this->title ? $this->title : $handle->getName(), ); $header = new PHUIHeaderView(); @@ -182,14 +194,14 @@ final class PHUIHovercardView extends AphrontView { } $hovercard = phutil_tag_div( - 'phui-hovercard-container', + 'phui-hovercard-container grouped', array( phutil_tag_div('phui-hovercard-head', $header), phutil_tag_div('phui-hovercard-body grouped', $body), $tail, )); - return phutil_tag_div('phui-hovercard-wrapper', $hovercard); + return $hovercard; } } diff --git a/webroot/rsrc/css/application/project/project-card-view.css b/webroot/rsrc/css/application/project/project-card-view.css new file mode 100644 index 0000000000..d284804f1d --- /dev/null +++ b/webroot/rsrc/css/application/project/project-card-view.css @@ -0,0 +1,150 @@ +/** + * @provides project-card-view-css + */ + +.project-card-view { + margin: 0 12px 16px 0; + text-align: left; + background: #fff; + border: 1px solid {$lightblueborder}; + border-radius: 3px; + box-shadow: {$dropshadow}; + width: 380px; +} + +.project-card-view .phui-header-shell { + margin: 0; + padding: 12px 12px 16px 12px; + border: none; + border-radius: 3px; +} + +.project-card-view .phui-header-shell .phui-header-image { + border: 3px solid #fff; + border-radius: 3px; +} + +.project-card-view .phui-header-shell .phui-header-header { + font-size: 18px; + font-family: 'Aleo', {$fontfamily}; + width: 290px; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + display: block; +} + +.project-card-view .phui-header-shell .phui-header-col1 { + vertical-align: top; + width: 64px; +} + +.project-card-view .phui-header-subheader { + font-size: {$normalfontsize}; + margin-top: 12px; +} + +.project-card-view .phui-header-header .phui-tag-view { + display: block; + font-weight: normal; + color: {$bluetext}; + font-size: {$normalfontsize}; + font-family: {$fontfamily}; + margin-top: 8px; +} + +.project-card-view .phui-header-header .phui-tag-view .phui-tag-core { + padding: 0; +} + +.project-card-view .phui-header-header .phui-tag-view .phui-icon-view { + margin-left: 0; + color: {$bluetext}; +} + + +/* Colors */ + +.project-card-view.project-card-red { + border-color: {$sh-redborder}; +} + +.project-card-view.project-card-red .phui-header-shell { + background: linear-gradient(to bottom, {$sh-redbackground} 44px, #fff 44px); +} + +.project-card-view.project-card-orange { + border-color: {$sh-orangeborder}; +} + +.project-card-view.project-card-orange .phui-header-shell { + background: linear-gradient(to bottom, {$sh-orangebackground} 44px, #fff 44px); +} + +.project-card-view.project-card-yellow { + border-color: {$sh-yellowborder}; +} + +.project-card-view.project-card-yellow .phui-header-shell { + background: linear-gradient(to bottom, {$sh-yellowbackground} 44px, #fff 44px); +} + +.project-card-view.project-card-green { + border-color: {$sh-greenborder}; +} + +.project-card-view.project-card-green .phui-header-shell { + background: linear-gradient(to bottom, {$sh-greenbackground} 44px, #fff 44px); +} + +.project-card-view.project-card-blue { + border-color: {$sh-blueborder}; +} + +.project-card-view.project-card-blue .phui-header-shell { + background: linear-gradient(to bottom, {$sh-bluebackground} 44px, #fff 44px); +} + +.project-card-view.project-card-indigo { + border-color: {$sh-indigoborder}; +} + +.project-card-view.project-card-indigo .phui-header-shell { + background: linear-gradient(to bottom, {$sh-indigobackground} 44px, #fff 44px); +} + +.project-card-view.project-card-violet { + border-color: {$sh-violetborder}; +} + +.project-card-view.project-card-violet .phui-header-shell { + background: linear-gradient(to bottom, {$sh-violetbackground} 44px, #fff 44px); +} + +.project-card-view.project-card-pink { + border-color: {$sh-pinkborder}; +} + +.project-card-view.project-card-pink .phui-header-shell { + background: linear-gradient(to bottom, {$sh-pinkbackground} 44px, #fff 44px); +} + +.project-card-view.project-card-grey { + border-color: {$sh-greyborder}; +} + +.project-card-view.project-card-grey .phui-header-shell { + background: linear-gradient(to bottom, {$sh-greybackground} 44px, #fff 44px); +} + +.project-card-view.project-card-checkered { + border-color: {$sh-greyborder}; +} + +.project-card-view.project-card-checkered .phui-header-shell { + background-color: #fff; + background-image: linear-gradient(45deg, {$sh-greybackground} 25%, transparent 25%, transparent 75%, {$sh-greybackground} 75%, {$sh-greybackground}), + linear-gradient(45deg, {$sh-greybackground} 25%, transparent 25%, transparent 75%, {$sh-greybackground} 75%, {$sh-greybackground}); + background-size: 16px 16px; + background-position:0 0, 8px 8px; +} diff --git a/webroot/rsrc/css/phui/phui-hovercard.css b/webroot/rsrc/css/phui/phui-hovercard.css index 82a7d3bb36..6431c01cdf 100644 --- a/webroot/rsrc/css/phui/phui-hovercard.css +++ b/webroot/rsrc/css/phui/phui-hovercard.css @@ -7,20 +7,17 @@ } .phui-hovercard-wrapper { - float: left; width: 400px; } .device-phone .phui-hovercard-wrapper { - float: left; width: 300px; } .phui-hovercard-container { - float: left; width: 100%; box-shadow: {$dropshadow}; - border: 1px solid {$blueborder}; + border: 1px solid {$lightblueborder}; border-radius: 3px; background-color: #fff; } @@ -33,7 +30,7 @@ } .phui-hovercard-head .phui-header-header { - font-size: 14px; + font-size: {$biggerfontsize}; } .phui-hovercard-head .phui-tag-type-state { From e1c934ab22d8c0d88ec1ce0d73bc497e563b4acb Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 4 Feb 2016 09:43:40 -0800 Subject: [PATCH 44/54] De-garbage the horrible garbage project section of the policy selection control Summary: Fixes T4136. When listing projects in the "Visible To" selector control: - Instead of showing every project you are a member of, show only a few. - Add an option to choose something else which isn't in the menu. - If you've used the control before, show the stuff you've selected in the recent past. - If you haven't used the control before or haven't used it much, show the stuff you've picked and them some filler. - Don't offer milestones. - Also don't offer milestones in the custom policy UI. Test Plan: {F1091999} {F1092000} - Selected a project. - Used "find" to select a different project. - Saw reasonable defaults. - Saw favorites stick. - Tried to typeahead a milestone (nope). - Used "Custom Policy", tried to typeahead a milestone (nope). - Used "Custom Policy" in general. Reviewers: chad Reviewed By: chad Maniphest Tasks: T4136 Differential Revision: https://secure.phabricator.com/D15184 --- .../PhabricatorPolicyEditController.php | 88 ++++++++++++++++++- .../policy/query/PhabricatorPolicyQuery.php | 46 +++++++++- .../PhabricatorProjectsPolicyRule.php | 8 +- .../PhabricatorProjectDatasource.php | 6 ++ .../storage/PhabricatorUserPreferences.php | 1 + .../form/control/AphrontFormPolicyControl.php | 49 +++++++++-- .../policy/behavior-policy-control.js | 48 ++++++++-- 7 files changed, 224 insertions(+), 22 deletions(-) diff --git a/src/applications/policy/controller/PhabricatorPolicyEditController.php b/src/applications/policy/controller/PhabricatorPolicyEditController.php index be380aa0b1..3dd8924bb6 100644 --- a/src/applications/policy/controller/PhabricatorPolicyEditController.php +++ b/src/applications/policy/controller/PhabricatorPolicyEditController.php @@ -6,7 +6,6 @@ final class PhabricatorPolicyEditController public function handleRequest(AphrontRequest $request) { $viewer = $this->getViewer(); - $object_phid = $request->getURIData('objectPHID'); if ($object_phid) { $object = id(new PhabricatorObjectQuery()) @@ -32,6 +31,17 @@ final class PhabricatorPolicyEditController } } + $phid = $request->getURIData('phid'); + switch ($phid) { + case AphrontFormPolicyControl::getSelectProjectKey(): + return $this->handleProjectRequest($request); + case AphrontFormPolicyControl::getSelectCustomKey(): + $phid = null; + break; + default: + break; + } + $action_options = array( PhabricatorPolicy::ACTION_ALLOW => pht('Allow'), PhabricatorPolicy::ACTION_DENY => pht('Deny'), @@ -55,7 +65,6 @@ final class PhabricatorPolicyEditController 'value' => null, ); - $phid = $request->getURIData('phid'); if ($phid) { $policies = id(new PhabricatorPolicyQuery()) ->setViewer($viewer) @@ -253,4 +262,79 @@ final class PhabricatorPolicyEditController return id(new AphrontDialogResponse())->setDialog($dialog); } + private function handleProjectRequest(AphrontRequest $request) { + $viewer = $this->getViewer(); + + $errors = array(); + $e_project = true; + + if ($request->isFormPost()) { + $project_phids = $request->getArr('projectPHIDs'); + $project_phid = head($project_phids); + + $project = id(new PhabricatorObjectQuery()) + ->setViewer($viewer) + ->withPHIDs(array($project_phid)) + ->executeOne(); + + if ($project) { + // Save this project as one of the user's most recently used projects, + // so we'll show it by default in future menus. + + $pref_key = PhabricatorUserPreferences::PREFERENCE_FAVORITE_POLICIES; + + $preferences = $viewer->loadPreferences(); + $favorites = $preferences->getPreference($pref_key); + if (!is_array($favorites)) { + $favorites = array(); + } + + // Add this, or move it to the end of the list. + unset($favorites[$project_phid]); + $favorites[$project_phid] = true; + + $preferences->setPreference($pref_key, $favorites); + $preferences->save(); + + $data = array( + 'phid' => $project->getPHID(), + 'info' => array( + 'name' => $project->getName(), + 'full' => $project->getName(), + 'icon' => $project->getDisplayIconIcon(), + ), + ); + + return id(new AphrontAjaxResponse())->setContent($data); + } else { + $errors[] = pht('You must choose a project.'); + $e_project = pht('Required'); + } + } + + $project_datasource = id(new PhabricatorProjectDatasource()) + ->setParameters( + array( + 'policy' => 1, + )); + + $form = id(new AphrontFormView()) + ->setUser($viewer) + ->appendControl( + id(new AphrontFormTokenizerControl()) + ->setLabel(pht('Members Of')) + ->setName('projectPHIDs') + ->setLimit(1) + ->setError($e_project) + ->setDatasource($project_datasource)); + + return $this->newDialog() + ->setWidth(AphrontDialogView::WIDTH_FORM) + ->setErrors($errors) + ->setTitle(pht('Select Project')) + ->appendForm($form) + ->addSubmitButton(pht('Done')) + ->addCancelButton('#'); + } + } diff --git a/src/applications/policy/query/PhabricatorPolicyQuery.php b/src/applications/policy/query/PhabricatorPolicyQuery.php index 7ad2630503..df1d6fb0b8 100644 --- a/src/applications/policy/query/PhabricatorPolicyQuery.php +++ b/src/applications/policy/query/PhabricatorPolicyQuery.php @@ -195,10 +195,48 @@ final class PhabricatorPolicyQuery $viewer = $this->getViewer(); if ($viewer->getPHID()) { - $projects = id(new PhabricatorProjectQuery()) - ->setViewer($viewer) - ->withMemberPHIDs(array($viewer->getPHID())) - ->execute(); + $pref_key = PhabricatorUserPreferences::PREFERENCE_FAVORITE_POLICIES; + + $favorite_limit = 10; + $default_limit = 5; + + // If possible, show the user's 10 most recently used projects. + $preferences = $viewer->loadPreferences(); + $favorites = $preferences->getPreference($pref_key); + if (!is_array($favorites)) { + $favorites = array(); + } + $favorite_phids = array_keys($favorites); + $favorite_phids = array_slice($favorite_phids, -$favorite_limit); + + if ($favorite_phids) { + $projects = id(new PhabricatorProjectQuery()) + ->setViewer($viewer) + ->withPHIDs($favorite_phids) + ->withIsMilestone(false) + ->setLimit($favorite_limit) + ->execute(); + $projects = mpull($projects, null, 'getPHID'); + } else { + $projects = array(); + } + + // If we didn't find enough favorites, add some default projects. These + // are just arbitrary projects that the viewer is a member of, but may + // be useful on smaller installs and for new users until they can use + // the control enough time to establish useful favorites. + if (count($projects) < $default_limit) { + $default_projects = id(new PhabricatorProjectQuery()) + ->setViewer($viewer) + ->withMemberPHIDs(array($viewer->getPHID())) + ->withIsMilestone(false) + ->setLimit($default_limit) + ->execute(); + $default_projects = mpull($default_projects, null, 'getPHID'); + $projects = $projects + $default_projects; + $projects = array_slice($projects, 0, $default_limit); + } + foreach ($projects as $project) { $phids[] = $project->getPHID(); } diff --git a/src/applications/project/policyrule/PhabricatorProjectsPolicyRule.php b/src/applications/project/policyrule/PhabricatorProjectsPolicyRule.php index 1782b62a9d..3977b542c1 100644 --- a/src/applications/project/policyrule/PhabricatorProjectsPolicyRule.php +++ b/src/applications/project/policyrule/PhabricatorProjectsPolicyRule.php @@ -48,7 +48,13 @@ final class PhabricatorProjectsPolicyRule } public function getValueControlTemplate() { - return $this->getDatasourceTemplate(new PhabricatorProjectDatasource()); + $datasource = id(new PhabricatorProjectDatasource()) + ->setParameters( + array( + 'policy' => 1, + )); + + return $this->getDatasourceTemplate($datasource); } public function getRuleOrder() { diff --git a/src/applications/project/typeahead/PhabricatorProjectDatasource.php b/src/applications/project/typeahead/PhabricatorProjectDatasource.php index 9a18006e5c..cc0140878f 100644 --- a/src/applications/project/typeahead/PhabricatorProjectDatasource.php +++ b/src/applications/project/typeahead/PhabricatorProjectDatasource.php @@ -32,6 +32,12 @@ final class PhabricatorProjectDatasource $query->withNameTokens($tokens); } + // If this is for policy selection, prevent users from using milestones. + $for_policy = $this->getParameter('policy'); + if ($for_policy) { + $query->withIsMilestone(false); + } + $projs = $this->executeQuery($query); $projs = mpull($projs, null, 'getPHID'); diff --git a/src/applications/settings/storage/PhabricatorUserPreferences.php b/src/applications/settings/storage/PhabricatorUserPreferences.php index 53c080ec88..271fd1afb4 100644 --- a/src/applications/settings/storage/PhabricatorUserPreferences.php +++ b/src/applications/settings/storage/PhabricatorUserPreferences.php @@ -42,6 +42,7 @@ final class PhabricatorUserPreferences extends PhabricatorUserDAO { const PREFERENCE_DESKTOP_NOTIFICATIONS = 'desktop-notifications'; const PREFERENCE_PROFILE_MENU_COLLAPSED = 'profile-menu.collapsed'; + const PREFERENCE_FAVORITE_POLICIES = 'policy.favorites'; // These are in an unusual order for historic reasons. const MAILTAG_PREFERENCE_NOTIFY = 0; diff --git a/src/view/form/control/AphrontFormPolicyControl.php b/src/view/form/control/AphrontFormPolicyControl.php index 71087bfe07..32f5318a18 100644 --- a/src/view/form/control/AphrontFormPolicyControl.php +++ b/src/view/form/control/AphrontFormPolicyControl.php @@ -103,6 +103,24 @@ final class AphrontFormPolicyControl extends AphrontFormControl { protected function getOptions() { $capability = $this->capability; $policies = $this->policies; + $viewer = $this->getUser(); + + // Check if we're missing the policy for the current control value. This + // is unusual, but can occur if the user is submitting a form and selected + // an unusual project as a policy but the change has not been saved yet. + $policy_map = mpull($policies, null, 'getPHID'); + $value = $this->getValue(); + if ($value && empty($policy_map[$value])) { + $handle = id(new PhabricatorHandleQuery()) + ->setViewer($viewer) + ->withPHIDs(array($value)) + ->executeOne(); + if ($handle->isComplete()) { + $policies[] = PhabricatorPolicy::newFromPolicyAndHandle( + $value, + $handle); + } + } // Exclude object policies which don't make sense here. This primarily // filters object policies associated from template capabilities (like @@ -143,12 +161,27 @@ final class AphrontFormPolicyControl extends AphrontFormControl { 'name' => $policy_short_name, 'full' => $policy->getName(), 'icon' => $policy->getIcon(), + 'sort' => phutil_utf8_strtolower($policy->getName()), ); } + $type_project = PhabricatorPolicyType::TYPE_PROJECT; + + $placeholder = id(new PhabricatorPolicy()) + ->setName(pht('Other Project...')) + ->setIcon('fa-search'); + + $options[$type_project] = isort($options[$type_project], 'sort'); + + $options[$type_project][$this->getSelectProjectKey()] = array( + 'name' => $placeholder->getName(), + 'full' => $placeholder->getName(), + 'icon' => $placeholder->getIcon(), + ); + // If we were passed several custom policy options, throw away the ones // which aren't the value for this capability. For example, an object might - // have a custom view pollicy and a custom edit policy. When we render + // have a custom view policy and a custom edit policy. When we render // the selector for "Can View", we don't want to show the "Can Edit" // custom policy -- if we did, the menu would look like this: // @@ -172,7 +205,7 @@ final class AphrontFormPolicyControl extends AphrontFormControl { if (empty($options[$type_custom])) { $placeholder = new PhabricatorPolicy(); $placeholder->setName(pht('Custom Policy...')); - $options[$type_custom][$this->getCustomPolicyPlaceholder()] = array( + $options[$type_custom][$this->getSelectCustomKey()] = array( 'name' => $placeholder->getName(), 'full' => $placeholder->getName(), 'icon' => $placeholder->getIcon(), @@ -266,12 +299,12 @@ final class AphrontFormPolicyControl extends AphrontFormControl { 'options' => $flat_options, 'groups' => array_keys($options), 'order' => $order, - 'icons' => $icons, 'labels' => $labels, 'value' => $this->getValue(), 'capability' => $this->capability, 'editURI' => '/policy/edit/'.$context_path, - 'customPlaceholder' => $this->getCustomPolicyPlaceholder(), + 'customKey' => $this->getSelectCustomKey(), + 'projectKey' => $this->getSelectProjectKey(), 'disabled' => $this->getDisabled(), )); @@ -322,8 +355,12 @@ final class AphrontFormPolicyControl extends AphrontFormControl { )); } - private function getCustomPolicyPlaceholder() { - return 'custom:placeholder'; + public static function getSelectCustomKey() { + return 'select:custom'; + } + + public static function getSelectProjectKey() { + return 'select:project'; } private function buildSpacesControl() { diff --git a/webroot/rsrc/js/application/policy/behavior-policy-control.js b/webroot/rsrc/js/application/policy/behavior-policy-control.js index 9696a09dc8..044b909352 100644 --- a/webroot/rsrc/js/application/policy/behavior-policy-control.js +++ b/webroot/rsrc/js/application/policy/behavior-policy-control.js @@ -7,6 +7,7 @@ * phuix-action-list-view * phuix-action-view * javelin-workflow + * phuix-icon-view * @javelin */ JX.behavior('policy-control', function(config) { @@ -57,6 +58,21 @@ JX.behavior('policy-control', function(config) { .start(); }, phid); + } else if (phid == config.projectKey) { + onselect = JX.bind(null, function(phid) { + var uri = get_custom_uri(phid, config.capability); + + new JX.Workflow(uri) + .setHandler(function(response) { + if (!response.phid) { + return; + } + + add_policy(phid, response.phid, response.info); + select_policy(response.phid); + }) + .start(); + }, phid); } else { onselect = JX.bind(null, select_policy, phid); } @@ -101,20 +117,22 @@ JX.behavior('policy-control', function(config) { name = JX.$N('span', {title: option.full}, name); } - return [JX.$H(config.icons[option.icon]), name]; + return [render_icon(option.icon), name]; }; + var render_icon = function(icon) { + return new JX.PHUIXIconView() + .setIcon(icon) + .getNode(); + }; /** * Get the workflow URI to create or edit a policy with a given PHID. */ var get_custom_uri = function(phid, capability) { - var uri = config.editURI; - if (phid != config.customPlaceholder) { - uri += phid + '/'; - } - uri += '?capability=' + capability; - return uri; + return JX.$U(config.editURI + phid + '/') + .setQueryParam('capability', capability) + .toString(); }; @@ -123,16 +141,28 @@ JX.behavior('policy-control', function(config) { * policies after the user edits them. */ var replace_policy = function(old_phid, new_phid, info) { + return add_policy(old_phid, new_phid, info, true); + }; + + + /** + * Add a new policy above an existing one, optionally replacing it. + */ + var add_policy = function(old_phid, new_phid, info, replace) { + if (config.options[new_phid]) { + return; + } + config.options[new_phid] = info; + for (var k in config.order) { for (var ii = 0; ii < config.order[k].length; ii++) { if (config.order[k][ii] == old_phid) { - config.order[k][ii] = new_phid; + config.order[k].splice(ii, (replace ? 1 : 0), new_phid); return; } } } }; - }); From 8e7e999cc34bd19665fef18abcf9749b7970a0d0 Mon Sep 17 00:00:00 2001 From: Chad Little Date: Fri, 5 Feb 2016 10:30:20 -0800 Subject: [PATCH 45/54] Slightly better spacing, colors on ProjectCards Summary: Fixes transparent background images, removes "checkered". Better spacing for tags. Test Plan: Upload a transparent logo, hover over project. {F1093753} Reviewers: epriestley Reviewed By: epriestley Subscribers: Korvin Differential Revision: https://secure.phabricator.com/D15188 --- resources/celerity/map.php | 27 ++++++------ .../application/project/project-card-view.css | 42 +++++++++---------- 2 files changed, 35 insertions(+), 34 deletions(-) diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 747ee79741..56999335cf 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' => 'ee1ce91d', + 'rsrc/css/application/project/project-card-view.css' => '9c3631e5', 'rsrc/css/application/project/project-view.css' => '99a5023b', 'rsrc/css/application/releeph/releeph-core.css' => '9b3c5733', 'rsrc/css/application/releeph/releeph-preview-branch.css' => 'b7a6f4a5', @@ -412,7 +412,7 @@ return array( 'rsrc/js/application/phortune/behavior-stripe-payment-form.js' => '3f5d6dbf', 'rsrc/js/application/phortune/behavior-test-payment-form.js' => 'fc91ab6c', 'rsrc/js/application/phortune/phortune-credit-card-form.js' => '2290aeef', - 'rsrc/js/application/policy/behavior-policy-control.js' => 'ae45872f', + '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-create.js' => '065227cc', @@ -652,7 +652,7 @@ return array( 'javelin-behavior-phui-hovercards' => 'bcaccd64', 'javelin-behavior-phui-object-box-tabs' => '2bfa2836', 'javelin-behavior-phui-profile-menu' => '12884df9', - 'javelin-behavior-policy-control' => 'ae45872f', + 'javelin-behavior-policy-control' => 'd0c516d5', 'javelin-behavior-policy-rule-editor' => '5e9f347c', 'javelin-behavior-project-boards' => '48470f95', 'javelin-behavior-project-create' => '065227cc', @@ -844,7 +844,7 @@ return array( 'policy-edit-css' => '815c66f7', 'policy-transaction-detail-css' => '82100a43', 'ponder-view-css' => '7b0df4da', - 'project-card-view-css' => 'ee1ce91d', + 'project-card-view-css' => '9c3631e5', 'project-view-css' => '99a5023b', 'releeph-core' => '9b3c5733', 'releeph-preview-branch' => 'b7a6f4a5', @@ -1684,15 +1684,6 @@ return array( 'javelin-uri', 'phabricator-file-upload', ), - 'ae45872f' => array( - 'javelin-behavior', - 'javelin-dom', - 'javelin-util', - 'phuix-dropdown-menu', - 'phuix-action-list-view', - 'phuix-action-view', - 'javelin-workflow', - ), 'b064af76' => array( 'javelin-behavior', 'javelin-stratcom', @@ -1852,6 +1843,16 @@ return array( 'd00a795a' => array( 'phui-theme-css', ), + 'd0c516d5' => array( + 'javelin-behavior', + 'javelin-dom', + 'javelin-util', + 'phuix-dropdown-menu', + 'phuix-action-list-view', + 'phuix-action-view', + 'javelin-workflow', + 'phuix-icon-view', + ), 'd19198c8' => array( 'javelin-install', 'javelin-dom', diff --git a/webroot/rsrc/css/application/project/project-card-view.css b/webroot/rsrc/css/application/project/project-card-view.css index d284804f1d..475f7c76f2 100644 --- a/webroot/rsrc/css/application/project/project-card-view.css +++ b/webroot/rsrc/css/application/project/project-card-view.css @@ -22,6 +22,7 @@ .project-card-view .phui-header-shell .phui-header-image { border: 3px solid #fff; border-radius: 3px; + background-color: #fff; } .project-card-view .phui-header-shell .phui-header-header { @@ -70,7 +71,8 @@ } .project-card-view.project-card-red .phui-header-shell { - background: linear-gradient(to bottom, {$sh-redbackground} 44px, #fff 44px); + background: linear-gradient(to bottom, + {$sh-redbackground} 42px, #fff 42px); } .project-card-view.project-card-orange { @@ -78,7 +80,8 @@ } .project-card-view.project-card-orange .phui-header-shell { - background: linear-gradient(to bottom, {$sh-orangebackground} 44px, #fff 44px); + background: linear-gradient(to bottom, + {$sh-orangebackground} 42px, #fff 42px); } .project-card-view.project-card-yellow { @@ -86,7 +89,8 @@ } .project-card-view.project-card-yellow .phui-header-shell { - background: linear-gradient(to bottom, {$sh-yellowbackground} 44px, #fff 44px); + background: linear-gradient(to bottom, + {$sh-yellowbackground} 42px, #fff 42px); } .project-card-view.project-card-green { @@ -94,7 +98,8 @@ } .project-card-view.project-card-green .phui-header-shell { - background: linear-gradient(to bottom, {$sh-greenbackground} 44px, #fff 44px); + background: linear-gradient(to bottom, + {$sh-greenbackground} 42px, #fff 42px); } .project-card-view.project-card-blue { @@ -102,7 +107,8 @@ } .project-card-view.project-card-blue .phui-header-shell { - background: linear-gradient(to bottom, {$sh-bluebackground} 44px, #fff 44px); + background: linear-gradient(to bottom, + {$sh-bluebackground} 42px, #fff 42px); } .project-card-view.project-card-indigo { @@ -110,7 +116,8 @@ } .project-card-view.project-card-indigo .phui-header-shell { - background: linear-gradient(to bottom, {$sh-indigobackground} 44px, #fff 44px); + background: linear-gradient(to bottom, + {$sh-indigobackground} 42px, #fff 42px); } .project-card-view.project-card-violet { @@ -118,7 +125,8 @@ } .project-card-view.project-card-violet .phui-header-shell { - background: linear-gradient(to bottom, {$sh-violetbackground} 44px, #fff 44px); + background: linear-gradient(to bottom, + {$sh-violetbackground} 42px, #fff 42px); } .project-card-view.project-card-pink { @@ -126,25 +134,17 @@ } .project-card-view.project-card-pink .phui-header-shell { - background: linear-gradient(to bottom, {$sh-pinkbackground} 44px, #fff 44px); -} - -.project-card-view.project-card-grey { - border-color: {$sh-greyborder}; -} - -.project-card-view.project-card-grey .phui-header-shell { - background: linear-gradient(to bottom, {$sh-greybackground} 44px, #fff 44px); + background: linear-gradient(to bottom, + {$sh-pinkbackground} 42px, #fff 42px); } +.project-card-view.project-card-grey, .project-card-view.project-card-checkered { border-color: {$sh-greyborder}; } +.project-card-view.project-card-grey .phui-header-shell, .project-card-view.project-card-checkered .phui-header-shell { - background-color: #fff; - background-image: linear-gradient(45deg, {$sh-greybackground} 25%, transparent 25%, transparent 75%, {$sh-greybackground} 75%, {$sh-greybackground}), - linear-gradient(45deg, {$sh-greybackground} 25%, transparent 25%, transparent 75%, {$sh-greybackground} 75%, {$sh-greybackground}); - background-size: 16px 16px; - background-position:0 0, 8px 8px; + background: linear-gradient(to bottom, + {$sh-greybackground} 42px, #fff 42px); } From 5092bcf533a5998cd171a7a0f246c9df41c960b0 Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 5 Feb 2016 12:17:35 -0800 Subject: [PATCH 46/54] Fix dead column link and provide more milestone UI context Summary: Fixes T10287. Ref T10286. - Link stuff properly. - Generally, show "Parent (Milestone)" instead of "Milestone". - This probably doesn't get 100% of `getName()` -> `getDisplayName()` swaps, but we can get those as we catch them. Test Plan: See T10286. Also clicked stuff. Reviewers: chad Reviewed By: chad Maniphest Tasks: T10286, T10287 Differential Revision: https://secure.phabricator.com/D15189 --- .../PhabricatorProjectBoardViewController.php | 8 ++++++-- .../PhabricatorProjectManageController.php | 6 +++++- .../PhabricatorProjectMembersViewController.php | 2 +- .../PhabricatorProjectProfileController.php | 2 +- .../events/PhabricatorProjectUIEventListener.php | 4 +++- .../phid/PhabricatorProjectProjectPHIDType.php | 2 +- .../project/storage/PhabricatorProject.php | 14 ++++++++++++++ .../typeahead/PhabricatorProjectDatasource.php | 4 ++-- 8 files changed, 33 insertions(+), 9 deletions(-) diff --git a/src/applications/project/controller/PhabricatorProjectBoardViewController.php b/src/applications/project/controller/PhabricatorProjectBoardViewController.php index 396fb15999..d0580a95ad 100644 --- a/src/applications/project/controller/PhabricatorProjectBoardViewController.php +++ b/src/applications/project/controller/PhabricatorProjectBoardViewController.php @@ -149,8 +149,8 @@ final class PhabricatorProjectBoardViewController return $this->newPage() ->setTitle( array( + $project->getDisplayName(), pht('Workboard'), - $project->getName(), )) ->setNavigation($nav) ->setCrumbs($crumbs) @@ -371,7 +371,11 @@ final class PhabricatorProjectBoardViewController $crumbs->addAction($manage_menu); return $this->newPage() - ->setTitle(pht('%s Board', $project->getName())) + ->setTitle( + array( + $project->getDisplayName(), + pht('Workboard'), + )) ->setPageObjectPHIDs(array($project->getPHID())) ->setShowFooter(false) ->setNavigation($nav) diff --git a/src/applications/project/controller/PhabricatorProjectManageController.php b/src/applications/project/controller/PhabricatorProjectManageController.php index 2721265493..87420d1aa3 100644 --- a/src/applications/project/controller/PhabricatorProjectManageController.php +++ b/src/applications/project/controller/PhabricatorProjectManageController.php @@ -51,7 +51,11 @@ final class PhabricatorProjectManageController return $this->newPage() ->setNavigation($nav) ->setCrumbs($crumbs) - ->setTitle($project->getName()) + ->setTitle( + array( + $project->getDisplayName(), + pht('Manage'), + )) ->appendChild( array( $object_box, diff --git a/src/applications/project/controller/PhabricatorProjectMembersViewController.php b/src/applications/project/controller/PhabricatorProjectMembersViewController.php index efe2106a26..88bc56be24 100644 --- a/src/applications/project/controller/PhabricatorProjectMembersViewController.php +++ b/src/applications/project/controller/PhabricatorProjectMembersViewController.php @@ -48,7 +48,7 @@ final class PhabricatorProjectMembersViewController return $this->newPage() ->setNavigation($nav) ->setCrumbs($crumbs) - ->setTitle(array($project->getName(), $title)) + ->setTitle(array($project->getDisplayName(), $title)) ->appendChild( array( $object_box, diff --git a/src/applications/project/controller/PhabricatorProjectProfileController.php b/src/applications/project/controller/PhabricatorProjectProfileController.php index 8bd03b7053..7e5f066607 100644 --- a/src/applications/project/controller/PhabricatorProjectProfileController.php +++ b/src/applications/project/controller/PhabricatorProjectProfileController.php @@ -115,7 +115,7 @@ final class PhabricatorProjectProfileController return $this->newPage() ->setNavigation($nav) ->setCrumbs($crumbs) - ->setTitle($project->getName()) + ->setTitle($project->getDisplayName()) ->setPageObjectPHIDs(array($project->getPHID())) ->appendChild( array( diff --git a/src/applications/project/events/PhabricatorProjectUIEventListener.php b/src/applications/project/events/PhabricatorProjectUIEventListener.php index c85c036f51..afbc0aab4e 100644 --- a/src/applications/project/events/PhabricatorProjectUIEventListener.php +++ b/src/applications/project/events/PhabricatorProjectUIEventListener.php @@ -67,11 +67,13 @@ final class PhabricatorProjectUIEventListener $annotation = array(); foreach ($columns as $column) { + $project_id = $column->getProject()->getID(); + $column_name = pht('(%s)', $column->getDisplayName()); $column_link = phutil_tag( 'a', array( - 'href' => $handle->getURI().'board/', + 'href' => "/project/board/{$project_id}/", 'class' => 'maniphest-board-link', ), $column_name); diff --git a/src/applications/project/phid/PhabricatorProjectProjectPHIDType.php b/src/applications/project/phid/PhabricatorProjectProjectPHIDType.php index b9fdefc6b3..7a04103a7c 100644 --- a/src/applications/project/phid/PhabricatorProjectProjectPHIDType.php +++ b/src/applications/project/phid/PhabricatorProjectProjectPHIDType.php @@ -37,7 +37,7 @@ final class PhabricatorProjectProjectPHIDType extends PhabricatorPHIDType { foreach ($handles as $phid => $handle) { $project = $objects[$phid]; - $name = $project->getName(); + $name = $project->getDisplayName(); $id = $project->getID(); $slug = $project->getPrimarySlug(); diff --git a/src/applications/project/storage/PhabricatorProject.php b/src/applications/project/storage/PhabricatorProject.php index b3d23a089e..ad7fb525c0 100644 --- a/src/applications/project/storage/PhabricatorProject.php +++ b/src/applications/project/storage/PhabricatorProject.php @@ -490,6 +490,20 @@ final class PhabricatorProject extends PhabricatorProjectDAO return $number; } + public function getDisplayName() { + $name = $this->getName(); + + // If this is a milestone, show it as "Parent > Sprint 99". + if ($this->isMilestone()) { + $name = pht( + '%s (%s)', + $this->getParentProject()->getName(), + $name); + } + + return $name; + } + public function getDisplayIconKey() { if ($this->isMilestone()) { $key = PhabricatorProjectIconSet::getMilestoneIconKey(); diff --git a/src/applications/project/typeahead/PhabricatorProjectDatasource.php b/src/applications/project/typeahead/PhabricatorProjectDatasource.php index cc0140878f..d902c9c392 100644 --- a/src/applications/project/typeahead/PhabricatorProjectDatasource.php +++ b/src/applications/project/typeahead/PhabricatorProjectDatasource.php @@ -64,12 +64,12 @@ final class PhabricatorProjectDatasource } $all_strings = mpull($proj->getSlugs(), 'getSlug'); - $all_strings[] = $proj->getName(); + $all_strings[] = $proj->getDisplayName(); $all_strings = implode(' ', $all_strings); $proj_result = id(new PhabricatorTypeaheadResult()) ->setName($all_strings) - ->setDisplayName($proj->getName()) + ->setDisplayName($proj->getDisplayName()) ->setDisplayType(pht('Project')) ->setURI($proj->getURI()) ->setPHID($proj->getPHID()) From 4132ba0853e983df5cc9b463ea3f0ae2912975fc Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 5 Feb 2016 12:39:00 -0800 Subject: [PATCH 47/54] Improve type and icon information in typeahead Summary: Ref T10289. This probably doesn't cover everything but should do a little bit better. Although we should mabye just exlude milestones from this menu completely? Test Plan: {F1093937} Reviewers: chad Reviewed By: chad Maniphest Tasks: T10289 Differential Revision: https://secure.phabricator.com/D15191 --- resources/celerity/map.php | 27 ++++++++++--------- .../typeahead/DiffusionSymbolDatasource.php | 5 +++- .../project/storage/PhabricatorProject.php | 2 +- .../PhabricatorProjectDatasource.php | 2 +- .../rsrc/js/core/behavior-search-typeahead.js | 11 ++++++++ 5 files changed, 31 insertions(+), 16 deletions(-) diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 56999335cf..403b8e0e7b 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -8,7 +8,7 @@ return array( 'names' => array( 'core.pkg.css' => 'e33b14a4', - 'core.pkg.js' => '7214314b', + 'core.pkg.js' => 'ef5e33db', 'darkconsole.pkg.js' => 'e7393ebb', 'differential.pkg.css' => '2de124c9', 'differential.pkg.js' => '5c2ba922', @@ -490,7 +490,7 @@ return array( 'rsrc/js/core/behavior-reorder-applications.js' => '76b9fc3e', 'rsrc/js/core/behavior-reveal-content.js' => '60821bc7', 'rsrc/js/core/behavior-scrollbar.js' => '834a1173', - 'rsrc/js/core/behavior-search-typeahead.js' => '0b7a4f6e', + 'rsrc/js/core/behavior-search-typeahead.js' => '06c32383', 'rsrc/js/core/behavior-select-on-click.js' => '4e3e79a6', 'rsrc/js/core/behavior-time-typeahead.js' => 'f80d6bf0', 'rsrc/js/core/behavior-toggle-class.js' => '5d7c9f33', @@ -640,7 +640,7 @@ return array( 'javelin-behavior-phabricator-oncopy' => '2926fff2', 'javelin-behavior-phabricator-remarkup-assist' => '340c8eff', 'javelin-behavior-phabricator-reveal-content' => '60821bc7', - 'javelin-behavior-phabricator-search-typeahead' => '0b7a4f6e', + 'javelin-behavior-phabricator-search-typeahead' => '06c32383', 'javelin-behavior-phabricator-show-older-transactions' => 'dbbf48b6', 'javelin-behavior-phabricator-tooltips' => '3ee3408b', 'javelin-behavior-phabricator-transaction-comment-form' => 'b23b49e6', @@ -909,6 +909,17 @@ return array( 'javelin-stratcom', 'javelin-workflow', ), + '06c32383' => array( + 'javelin-behavior', + 'javelin-typeahead-ondemand-source', + 'javelin-typeahead', + 'javelin-dom', + 'javelin-uri', + 'javelin-util', + 'javelin-stratcom', + 'phabricator-prefab', + 'phuix-icon-view', + ), '087e919c' => array( 'javelin-install', 'javelin-dom', @@ -922,16 +933,6 @@ return array( 'javelin-dom', 'javelin-router', ), - '0b7a4f6e' => array( - 'javelin-behavior', - 'javelin-typeahead-ondemand-source', - 'javelin-typeahead', - 'javelin-dom', - 'javelin-uri', - 'javelin-util', - 'javelin-stratcom', - 'phabricator-prefab', - ), '0f764c35' => array( 'javelin-install', 'javelin-util', diff --git a/src/applications/diffusion/typeahead/DiffusionSymbolDatasource.php b/src/applications/diffusion/typeahead/DiffusionSymbolDatasource.php index 1c7e463410..1c29c0dde4 100644 --- a/src/applications/diffusion/typeahead/DiffusionSymbolDatasource.php +++ b/src/applications/diffusion/typeahead/DiffusionSymbolDatasource.php @@ -47,7 +47,10 @@ final class DiffusionSymbolDatasource ->setPHID(md5($symbol->getURI())) // Just needs to be unique. ->setDisplayName($name) ->setDisplayType(strtoupper($lang).' '.ucwords($type).' ('.$repo.')') - ->setPriorityType('symb'); + ->setPriorityType('symb') + ->setImageSprite( + 'phabricator-search-icon phui-font-fa phui-icon-view fa-code '. + 'lightgreytext'); } } diff --git a/src/applications/project/storage/PhabricatorProject.php b/src/applications/project/storage/PhabricatorProject.php index ad7fb525c0..093b368ef7 100644 --- a/src/applications/project/storage/PhabricatorProject.php +++ b/src/applications/project/storage/PhabricatorProject.php @@ -379,7 +379,7 @@ final class PhabricatorProject extends PhabricatorProjectDAO $this->getPHID()); $all_strings = ipull($slugs, 'slug'); - $all_strings[] = $this->getName(); + $all_strings[] = $this->getDisplayName(); $all_strings = implode(' ', $all_strings); $tokens = PhabricatorTypeaheadDatasource::tokenizeString($all_strings); diff --git a/src/applications/project/typeahead/PhabricatorProjectDatasource.php b/src/applications/project/typeahead/PhabricatorProjectDatasource.php index d902c9c392..33fb8df0e9 100644 --- a/src/applications/project/typeahead/PhabricatorProjectDatasource.php +++ b/src/applications/project/typeahead/PhabricatorProjectDatasource.php @@ -70,7 +70,7 @@ final class PhabricatorProjectDatasource $proj_result = id(new PhabricatorTypeaheadResult()) ->setName($all_strings) ->setDisplayName($proj->getDisplayName()) - ->setDisplayType(pht('Project')) + ->setDisplayType($proj->getDisplayIconName()) ->setURI($proj->getURI()) ->setPHID($proj->getPHID()) ->setIcon($proj->getDisplayIconIcon()) diff --git a/webroot/rsrc/js/core/behavior-search-typeahead.js b/webroot/rsrc/js/core/behavior-search-typeahead.js index f58cb813d0..a981f3861e 100644 --- a/webroot/rsrc/js/core/behavior-search-typeahead.js +++ b/webroot/rsrc/js/core/behavior-search-typeahead.js @@ -8,6 +8,7 @@ * javelin-util * javelin-stratcom * phabricator-prefab + * phuix-icon-view */ JX.behavior('phabricator-search-typeahead', function(config) { @@ -25,12 +26,22 @@ JX.behavior('phabricator-search-typeahead', function(config) { attr.style = {backgroundImage: 'url('+object.imageURI+')'}; } + var icon = null; + if (object.icon) { + icon = new JX.PHUIXIconView() + .setIcon(object.icon) + .setColor('lightgreytext') + .getNode(); + icon = [icon, ' ']; + } + var render = JX.$N( 'span', attr, [ JX.$N('span', {className: object.sprite}), JX.$N('span', {className: 'result-name'}, object.displayName), + icon, JX.$N('span', {className: 'result-type'}, object.type) ]); From a19be7697ce3afde82e9f2e12153d1f98ae2207e Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 5 Feb 2016 12:56:50 -0800 Subject: [PATCH 48/54] Add user profile icons to Phame authorship Summary: Huge omission. Test Plan: {F1093955} Reviewers: chad Reviewed By: chad Differential Revision: https://secure.phabricator.com/D15192 --- .../phame/controller/post/PhamePostViewController.php | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/applications/phame/controller/post/PhamePostViewController.php b/src/applications/phame/controller/post/PhamePostViewController.php index 68c9f22082..537bfbb713 100644 --- a/src/applications/phame/controller/post/PhamePostViewController.php +++ b/src/applications/phame/controller/post/PhamePostViewController.php @@ -101,9 +101,18 @@ final class PhamePostViewController $subtitle = pht('Written by %s on %s.', $author, $date); } + $user_icon = $blogger_profile->getIcon(); + $user_icon = PhabricatorPeopleIconSet::getIconIcon($user_icon); + $user_icon = id(new PHUIIconView())->setIcon($user_icon); + $about = id(new PhameDescriptionView()) ->setTitle($subtitle) - ->setDescription($blogger_profile->getTitle()) + ->setDescription( + array( + $user_icon, + ' ', + $blogger_profile->getTitle(), + )) ->setImage($blogger->getProfileImageURI()) ->setImageHref('/p/'.$blogger->getUsername()); From e2668a5fd04bfafc25ca82793f01aae6deca7f6d Mon Sep 17 00:00:00 2001 From: Chad Little Date: Fri, 5 Feb 2016 13:02:34 -0800 Subject: [PATCH 49/54] Normalize icon color on user/project lists. Summary: Minor, just fall back to the grey icon in all cases (too much color for me). Test Plan: Review a Project and a Profile Reviewers: epriestley Reviewed By: epriestley Subscribers: Korvin Differential Revision: https://secure.phabricator.com/D15190 --- resources/celerity/map.php | 4 ++-- .../project/view/PhabricatorProjectUserListView.php | 2 +- webroot/rsrc/css/application/project/project-view.css | 4 ++++ 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 403b8e0e7b..86454068d2 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -94,7 +94,7 @@ return array( '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-view.css' => '99a5023b', + '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', 'rsrc/css/application/releeph/releeph-request-differential-create-dialog.css' => '8d8b92cd', @@ -845,7 +845,7 @@ return array( 'policy-transaction-detail-css' => '82100a43', 'ponder-view-css' => '7b0df4da', 'project-card-view-css' => '9c3631e5', - 'project-view-css' => '99a5023b', + 'project-view-css' => '4693497c', 'releeph-core' => '9b3c5733', 'releeph-preview-branch' => 'b7a6f4a5', 'releeph-request-differential-create-dialog' => '8d8b92cd', diff --git a/src/applications/project/view/PhabricatorProjectUserListView.php b/src/applications/project/view/PhabricatorProjectUserListView.php index 715e28f944..e7f6631bfb 100644 --- a/src/applications/project/view/PhabricatorProjectUserListView.php +++ b/src/applications/project/view/PhabricatorProjectUserListView.php @@ -86,7 +86,7 @@ abstract class PhabricatorProjectUserListView extends AphrontView { ->setImageURI($handle->getImageURI()); $icon = id(new PHUIIconView()) - ->setIcon($handle->getIcon().' lightbluetext'); + ->setIcon($handle->getIcon()); $subtitle = $handle->getSubtitle(); diff --git a/webroot/rsrc/css/application/project/project-view.css b/webroot/rsrc/css/application/project/project-view.css index 4e1ab86e49..afbeff3950 100644 --- a/webroot/rsrc/css/application/project/project-view.css +++ b/webroot/rsrc/css/application/project/project-view.css @@ -82,6 +82,10 @@ width: 364px; } +.project-view-home .phui-box-grey .phui-object-item-attribute .phui-icon-view { + color: {$lightgreytext}; +} + .profile-no-badges { padding: 24px 0; } From a69cc99250db38aafffff04f34344b060d98e368 Mon Sep 17 00:00:00 2001 From: Chad Little Date: Fri, 5 Feb 2016 13:11:47 -0800 Subject: [PATCH 50/54] Add getDisplayName to cards and profiles Summary: Show on hovercards and the profile page itself. Test Plan: Review a Milestone. Reviewers: epriestley Reviewed By: epriestley Subscribers: Korvin Differential Revision: https://secure.phabricator.com/D15193 --- .../project/controller/PhabricatorProjectProfileController.php | 2 +- src/applications/project/view/PhabricatorProjectCardView.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/applications/project/controller/PhabricatorProjectProfileController.php b/src/applications/project/controller/PhabricatorProjectProfileController.php index 7e5f066607..4e670475ce 100644 --- a/src/applications/project/controller/PhabricatorProjectProfileController.php +++ b/src/applications/project/controller/PhabricatorProjectProfileController.php @@ -26,7 +26,7 @@ final class PhabricatorProjectProfileController ->setType(PHUITagView::TYPE_SHADE); $header = id(new PHUIHeaderView()) - ->setHeader(array($project->getName(), $tag)) + ->setHeader(array($project->getDisplayName(), $tag)) ->setUser($viewer) ->setPolicyObject($project) ->setImage($picture) diff --git a/src/applications/project/view/PhabricatorProjectCardView.php b/src/applications/project/view/PhabricatorProjectCardView.php index d520bc0e5a..f82ee8c99e 100644 --- a/src/applications/project/view/PhabricatorProjectCardView.php +++ b/src/applications/project/view/PhabricatorProjectCardView.php @@ -55,7 +55,7 @@ final class PhabricatorProjectCardView extends AphrontTagView { ->setType(PHUITagView::TYPE_SHADE); $header = id(new PHUIHeaderView()) - ->setHeader(array($project->getName(), $tag)) + ->setHeader(array($project->getDisplayName(), $tag)) ->setUser($viewer) ->setPolicyObject($project) ->setImage($project->getProfileImageURI()); From d92353930fa98891cf0c4023fe9501acebd50e8b Mon Sep 17 00:00:00 2001 From: Chad Little Date: Fri, 5 Feb 2016 13:32:19 -0800 Subject: [PATCH 51/54] Add a map marker icon for Milestones Summary: Never got added. Test Plan: Select a Milestone Project, edit Picture, see marker. Reviewers: epriestley Reviewed By: epriestley Subscribers: Korvin Differential Revision: https://secure.phabricator.com/D15194 --- resources/builtin/projects/fa-map-marker.png | Bin 0 -> 3115 bytes .../PhabricatorFilesComposeIconBuiltinFile.php | 1 + 2 files changed, 1 insertion(+) create mode 100644 resources/builtin/projects/fa-map-marker.png diff --git a/resources/builtin/projects/fa-map-marker.png b/resources/builtin/projects/fa-map-marker.png new file mode 100644 index 0000000000000000000000000000000000000000..865175bd9aa9c92a56e40e394e05384e02bd4fd6 GIT binary patch literal 3115 zcma)9i$Bx*8{dUtatRS8J4_+>x$NY&F-<5cb3a0UBgB3clUtKpM8>I@dm}?a$8Bqg zT+aNm)lo{=LQOiwH%d`!`K{Oa2hQj9`g}gG=kq+z=Xsv@>v?^i=Tqo+grEf1frCIG zr9+^tn(dzpGJM$%S+7}mbJ3To4%~vA{-sZzZV!hpa`)(}$Uymk>>zwQmmiXL4?@&U7 z@>)QIWyIdK0}%mRP%E_`PKw146|f5AeRd3WyK<)as$|*=TsSJEG${;=vkkK;Jz=lH z7Y;=1>Uznu?{m7}K4^%ZJ8|w5vpg-vUUH4%c97e z4xm6N`pjnejN*_r=?fk>coz$fjK_4yHZKD=4eh}MQxtss`qQ>~2K{8a7IlP|U`sLl zm6%!W5Jo$ZH*8&3n|*5}$^Uj7R4s8S95` zi;n#VE0D;)J1_qt-+ee#_E{@n;CwH%*Nmd?{%(BsrLfHuxfu2>?VQlMWoJbA;m)S1 zl%ixcIM<)YIKMim^NfcCexy|`>)9lKiVHqR^uP|=%uM|K$ZLpb%S?-py*`o%vQG+D z*8NrsTXE}dr|3uQ;cL46)}@K*%U|O3%7{jW&eP{ux-1$AnpXkS9uiU-W&&be)&R|P z-O>M%QbOPCVeEH7F2>%g$(Y{L4^`x^l-bKAZ#qDkQ2m6nfiHB5h_|v6UF~~z@_WYI>&-}&s+sm!J05Z(whwgplD^(3)Wz684 zkC3fESmy1CZDY42rjcpIx5P-bn=lbnA+FuF+fjXru7(ah{K?j}ujAgn=WWNJXYdzw z6xF9o`OZmfV$M~E< z={9HlHCID?YCF`hv_7m5eBN8M{-Mw*%O~ETbTCk~{`-DweWb_Y37d zUf;PJ3A_g*_HOPbxDR8Q-@8Ytqd&ZL-pre&e1MOuUK$`wS(I^0O;V)H$6cdTyz^KT z%MyQbklCyqEkBy2lv)4NSI%Z~8P~UCl=|K9<5t1JJ+95?rCO4?eSuNfka;;uv7})A zR%c|)){BX#%VMWcX-qb_l1wS0p+y6?io`8Pq~2zhDX%Tb*v;Cz{)5?=EGi-SQ;^8! z!+$p&Jbq$qUiLy?5InzRPJJq*do1EB}er+TJi+*(HvNI5_-=t5vr@lhUbrityXo0SQe??m$W& zL}*~1@qnUWl+)#4uW!z+_E5QNX`6AE88NCa(k2|Zi7V)AZ-M0d?#`PmF@iD(+A=wB zpTOzbAY)~vMmQg21U2juPVB|B<%a4L1=A~=vPbs*;1CB82_(yC;Fr6m5Xb_+gw#(6 z30>tR^%XnY{zDF+>VAO|oKx!480_w%o2tckyDjNHCyooOnqjl6J#REqc``p}oKiPL z6lv_XtG@60G%~bu#(2GdyYqokZ!Xld3I~$b295I8su-yRLQoe+Ql+kGrF#P|`7a(; z4TG=oks!cDi?#RX`)G_#$57Nwh;})k%9#TZr=>!FW%lMlO?P;b0{~dQ-?hQuYhdec zu`yX|=+Yi2z!&0`i7*rfO@X?Lc;zQf)qC>6Uv>G%6X-9xN4M{%#SnFAWc-xbleyt1 zKe@eNg#gglAlX4yH`{e7(SX?oRl8EWECL!wGz&app0xqE#h}RPPrHUT1Ow-Mb}2H_ zTGL=)tL_Twhh?BKC(Az_gGI|rUCtqCmQ@h%AloY5#i)YN={vWA0ha?JEjYm)?YRJO zAQyN-0wF`dD-u8FB5iYINMD?~q*Xw#D{L{9PNJP=i4!8u11>g>JYL zh!&ULd4RPG=+4*&^Y?ahU&m^HWNFY=y}y=}2sEeeBHOfNqs0I$?!_z8)gmqYX9hKa zfMGl*hi9emr6C^0?oHXx|MSVs*-PP_bQ;7an;d=kYwgI?^{M+(U1#H!Uow7wO^PTG z4D|RYGQKrvHx;bislLe;Us~Y82}xDBZ>smt2F*54hp0Z2<0CWW3A2~aY)&#xm?9*T zJD%*L%;&wSes8puOP4rO)JL#r&w5@0luQI%LdcE7ldgQ^?^4L1gO7z=jZ?TOR4k01 zOBM~7Q1p*?c`a5VqA}|B zja9M??>hq4#$wc5rMY=1RgJYzYSR;}EwIun>qW+4;o}Ryjo`M~@cJ=N6_&9ZV z=Xo69@~z8LkpW{;efXwThUWrLN3-hZqNU^mRkM}@fr^aggDP<5xRCkOtYy}kVtFza z=dWm+<8u__X8B0S#bhzaKD8v8X09x8|J&!_<;F#&X!=D9NuKg;a?_7Uz@=|}e_o?r z6)xlz;P|xGcb`<>+cu{MWfnPLL(v~P3@4;Z$#EgYaQke*$cQfY351U{x9MJ{>e}>m zg)GKakZ9N0;<};F^oQ{{e`#~nBTXJi;)Gv_BE!HB`wFwqoo_Mt(WXa(CeKUIBPR`) z`$FQ?p5MxcsQErL{6-H^-Bkcldpw{0S>c_NO1qCA(+lp2Qqb}@pSswRszGA~Kn-_t zFBE=P*?YN}iq{~ORZbgt_aaR#oQ;clLgwVXQH|1{Jc! zVGB`vFWj|*)ToA^EW>Nib`1AVmqLy-ZkpX!2!ez)K5g63pcN0-g?x~4fgt303frgj q_%X~tQ2gh1_kZ0? pht('Hired Protection'), 'fa-linux' => pht('M\'Lady'), 'fa-lock' => pht('Policy'), + 'fa-map-marker' => pht('Destination Beacon'), 'fa-microphone' => pht('Podcasting'), 'fa-mobile' => pht('Tiny Pocket Cat Meme Machine'), 'fa-money' => pht('1 of 99 Problems'), From d30b57313eaf661bfbb285b2e367f86cd0852f53 Mon Sep 17 00:00:00 2001 From: Chad Little Date: Fri, 5 Feb 2016 14:05:49 -0800 Subject: [PATCH 52/54] Style Milestone workpanels Summary: Small amount of color, icon spacing Test Plan: Review milestone panels on parent project Reviewers: epriestley Reviewed By: epriestley Subscribers: Korvin Differential Revision: https://secure.phabricator.com/D15196 --- resources/celerity/map.php | 10 +++++----- webroot/rsrc/css/phui/workboards/phui-workpanel.css | 8 ++++++++ 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 86454068d2..9003476d2d 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -156,7 +156,7 @@ return array( '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-workpanel.css' => '68140031', + 'rsrc/css/phui/workboards/phui-workpanel.css' => 'e1bd8d04', 'rsrc/css/sprite-login.css' => '60e8560e', 'rsrc/css/sprite-menu.css' => '9dd65b92', 'rsrc/css/sprite-tokens.css' => '4f399012', @@ -833,7 +833,7 @@ return array( 'phui-two-column-view-css' => 'c75bfc5b', 'phui-workboard-view-css' => 'b07a5524', 'phui-workcard-view-css' => 'b4322ca7', - 'phui-workpanel-view-css' => '68140031', + 'phui-workpanel-view-css' => 'e1bd8d04', 'phuix-action-list-view' => 'b5c256b8', 'phuix-action-view' => '8cf6d262', 'phuix-autocomplete' => '9196fb06', @@ -1331,9 +1331,6 @@ return array( 'javelin-request', 'javelin-workflow', ), - 68140031 => array( - 'phui-workcard-view-css', - ), '6882e80a' => array( 'javelin-dom', ), @@ -1921,6 +1918,9 @@ return array( 'javelin-dom', 'phabricator-prefab', ), + 'e1bd8d04' => array( + 'phui-workcard-view-css', + ), 'e1d25dfb' => array( 'javelin-behavior', 'javelin-stratcom', diff --git a/webroot/rsrc/css/phui/workboards/phui-workpanel.css b/webroot/rsrc/css/phui/workboards/phui-workpanel.css index b2945a4e6d..b600577d33 100644 --- a/webroot/rsrc/css/phui/workboards/phui-workpanel.css +++ b/webroot/rsrc/css/phui/workboards/phui-workpanel.css @@ -35,6 +35,14 @@ user-select: none; } +.phui-workpanel-view.phui-workboard-column-milestone .phui-box-grey { + background-color: rgba(234, 230, 247, 0.75); +} + +.phui-workpanel-view .phui-header-col2 .phui-icon-view { + margin-right: 4px; +} + .phui-workpanel-view .phui-workpanel-header-action { float: right; width: 24px; From 1db4de7dbc2c2462874258b047bc912881785621 Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 5 Feb 2016 13:26:47 -0800 Subject: [PATCH 53/54] Provide "Initial Members" instead of default joining projects Summary: Ref T10010. Instead of autojoining projects, provide "Initial Members: [___]" that the user can fill in. This is only available in the web UI when creating a (non-milestone) project. Test Plan: - Created a new project with no members. - Created a new project with some members. - Created a new milestone (no control). - Created a new project with myself as a member and an "Editable By: Project Members" policy, to verify this use case still works properly. Reviewers: chad Reviewed By: chad Maniphest Tasks: T10010 Differential Revision: https://secure.phabricator.com/D15195 --- .../PhabricatorProjectTransactionEditor.php | 28 +++--------- .../engine/PhabricatorProjectEditEngine.php | 45 ++++++++++++++++++- 2 files changed, 50 insertions(+), 23 deletions(-) diff --git a/src/applications/project/editor/PhabricatorProjectTransactionEditor.php b/src/applications/project/editor/PhabricatorProjectTransactionEditor.php index 0a03f55d7c..867bf7e9fe 100644 --- a/src/applications/project/editor/PhabricatorProjectTransactionEditor.php +++ b/src/applications/project/editor/PhabricatorProjectTransactionEditor.php @@ -219,7 +219,12 @@ final class PhabricatorProjectTransactionEditor foreach ($xactions as $xaction) { switch ($xaction->getTransactionType()) { - case PhabricatorProjectTransaction::TYPE_MEMBERS: + case PhabricatorTransactions::TYPE_EDGE: + $type = $xaction->getMetadataValue('edge:type'); + if ($type != PhabricatorProjectProjectHasMemberEdgeType::EDGECONST) { + break; + } + if ($is_parent) { $errors[] = new PhabricatorApplicationTransactionValidationError( $xaction->getTransactionType(), @@ -792,27 +797,6 @@ final class PhabricatorProjectTransactionEditor $results = parent::expandTransactions($object, $xactions); - // Automatically add the author as a member when they create a project - // if they're using the web interface. - - $content_source = $this->getContentSource(); - $source_web = PhabricatorContentSource::SOURCE_WEB; - $is_web = ($content_source->getSource() === $source_web); - - if ($this->getIsNewObject() && $is_web) { - if ($actor_phid) { - $type_member = PhabricatorProjectProjectHasMemberEdgeType::EDGECONST; - - $results[] = id(new PhabricatorProjectTransaction()) - ->setTransactionType(PhabricatorTransactions::TYPE_EDGE) - ->setMetadataValue('edge:type', $type_member) - ->setNewValue( - array( - '+' => array($actor_phid => $actor_phid), - )); - } - } - $is_milestone = $object->isMilestone(); foreach ($xactions as $xaction) { switch ($xaction->getTransactionType()) { diff --git a/src/applications/project/engine/PhabricatorProjectEditEngine.php b/src/applications/project/engine/PhabricatorProjectEditEngine.php index 95416989df..54144cbbb5 100644 --- a/src/applications/project/engine/PhabricatorProjectEditEngine.php +++ b/src/applications/project/engine/PhabricatorProjectEditEngine.php @@ -176,7 +176,7 @@ final class PhabricatorProjectEditEngine $milestone_phid = null; } - return array( + $fields = array( id(new PhabricatorHandlesEditField()) ->setKey('parent') ->setLabel(pht('Parent')) @@ -243,6 +243,49 @@ final class PhabricatorProjectEditEngine ->setConduitTypeDescription(pht('New list of slugs.')) ->setValue($slugs), ); + + $can_edit_members = (!$milestone) && + (!$object->isMilestone()) && + (!$object->getHasSubprojects()); + + if ($can_edit_members) { + + // Show this on the web UI when creating a project, but not when editing + // one. It is always available via Conduit. + $conduit_only = !$this->getIsCreate(); + + $members_field = id(new PhabricatorUsersEditField()) + ->setKey('members') + ->setAliases(array('memberPHIDs')) + ->setLabel(pht('Initial Members')) + ->setIsConduitOnly($conduit_only) + ->setUseEdgeTransactions(true) + ->setTransactionType(PhabricatorTransactions::TYPE_EDGE) + ->setMetadataValue( + 'edge:type', + PhabricatorProjectProjectHasMemberEdgeType::EDGECONST) + ->setDescription(pht('Initial project members.')) + ->setConduitDescription(pht('Set project members.')) + ->setConduitTypeDescription(pht('New list of members.')) + ->setValue(array()); + + $members_field->setViewer($this->getViewer()); + + $edit_add = $members_field->getConduitEditType('members.add') + ->setConduitDescription(pht('Add members.')); + + $edit_set = $members_field->getConduitEditType('members.set') + ->setConduitDescription( + pht('Set members, overwriting the current value.')); + + $edit_rem = $members_field->getConduitEditType('members.remove') + ->setConduitDescription(pht('Remove members.')); + + $fields[] = $members_field; + } + + return $fields; + } } From 74825fccc4159db96e249b708e6f6ee65bb9d306 Mon Sep 17 00:00:00 2001 From: epriestley Date: Sat, 6 Feb 2016 04:46:46 -0800 Subject: [PATCH 54/54] Fix issue with rendering policy controls when an install has zero projects Fixes T10290. Auditors: cahd --- src/view/form/control/AphrontFormPolicyControl.php | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/view/form/control/AphrontFormPolicyControl.php b/src/view/form/control/AphrontFormPolicyControl.php index 32f5318a18..6448b2b8b2 100644 --- a/src/view/form/control/AphrontFormPolicyControl.php +++ b/src/view/form/control/AphrontFormPolicyControl.php @@ -167,12 +167,17 @@ final class AphrontFormPolicyControl extends AphrontFormControl { $type_project = PhabricatorPolicyType::TYPE_PROJECT; + // Make sure we have a "Projects" group before we adjust it. + if (empty($options[$type_project])) { + $options[$type_project] = array(); + } + + $options[$type_project] = isort($options[$type_project], 'sort'); + $placeholder = id(new PhabricatorPolicy()) ->setName(pht('Other Project...')) ->setIcon('fa-search'); - $options[$type_project] = isort($options[$type_project], 'sort'); - $options[$type_project][$this->getSelectProjectKey()] = array( 'name' => $placeholder->getName(), 'full' => $placeholder->getName(),